如何在Qemu模拟的Raspi3b中通过miniUART实现打印输出?
树莓派3B miniUART无输出问题排查与解决(M1 macOS环境)
一、先确认Qemu启动参数
M1 macOS下运行Raspi3b裸机镜像,必须指定正确的机器型号和串口转发参数,否则终端收不到输出:
qemu-system-aarch64 -M raspi3b -kernel kernel8.img -serial stdio
-M raspi3b:明确指定模拟树莓派3B硬件-serial stdio:将miniUART绑定到终端标准输入输出,这是输出能显示到终端的核心参数kernel8.img:必须是AArch64架构的二进制镜像,树莓派3B默认运行64位模式
二、miniUART寄存器配置必须按顺序来
miniUART的配置有严格的先后顺序,乱序会导致配置失效,以下是正确的配置流程:
1. 先禁用中断与FIFO
#define AUX_BASE 0x3F215000 #define AUX_ENABLES (*(volatile unsigned int*)(AUX_BASE + 0x04)) #define AUX_MU_IER_REG (*(volatile unsigned int*)(AUX_BASE + 0x44)) #define AUX_MU_IIR_REG (*(volatile unsigned int*)(AUX_BASE + 0x48)) // 先关闭miniUART,再配置中断和FIFO AUX_ENABLES = 0; AUX_MU_IER_REG = 0; // 禁用所有中断 AUX_MU_IIR_REG = 0xC6; // 清空接收/发送FIFO,禁用FIFO(基础调试先关FIFO)
2. 配置GPIO引脚复用(miniUART对应GPIO14/TX、GPIO15/RX)
#define GPIO_BASE 0x3F200000 #define GPPUD (*(volatile unsigned int*)(GPIO_BASE + 0x94)) #define GPPUDCLK0 (*(volatile unsigned int*)(GPIO_BASE + 0x98)) // 设置GPIO14/15为Alt5模式(对应miniUART) GPPUD = 0; // 禁用上下拉 // 等待配置生效(空循环模拟延时) for(int i=0; i<150; i++) asm volatile("nop"); GPPUDCLK0 = (1<<14)|(1<<15); // 锁定GPIO14/15的配置 for(int i=0; i<150; i++) asm volatile("nop"); GPPUDCLK0 = 0; // 释放锁定
3. 设置波特率与数据格式
树莓派3B系统时钟默认400MHz,115200波特率对应的寄存器值计算:(400000000/(8*115200)) -1 ≈ 433
#define AUX_MU_BAUD_REG (*(volatile unsigned int*)(AUX_BASE + 0x68)) #define AUX_MU_LCR_REG (*(volatile unsigned int*)(AUX_BASE + 0x4C)) AUX_MU_BAUD_REG = 433; // 115200波特率 AUX_MU_LCR_REG = 0x3; // 8位数据位、无校验、1位停止位
4. 最后启用miniUART
#define AUX_MU_CNTL_REG (*(volatile unsigned int*)(AUX_BASE + 0x60)) AUX_ENABLES = 1; // 启用miniUART AUX_MU_CNTL_REG = 0x3; // 启用发送和接收功能
三、代码实现细节补全
1. loader.s的基础初始化(必须进入EL1模式,禁用缓存)
.global _start _start: // 设置EL1栈指针 mov x0, #0x80000 msr sp_el1, x0 // 禁用MMU、数据缓存、指令缓存(裸机环境必须) mrs x0, sctlr_el1 bic x0, x0, #(1<<0) bic x0, x0, #(1<<2) bic x0, x0, #(1<<12) msr sctlr_el1, x0 isb // 跳转到main函数 bl main // 死循环防止程序跑飞 loop: b loop
2. kernel.c中的串口输出函数
必须等待发送缓冲区为空再写入字符,否则输出会丢失:
#define AUX_MU_IO_REG (*(volatile unsigned int*)(AUX_BASE + 0x40)) #define AUX_MU_LSR_REG (*(volatile unsigned int*)(AUX_BASE + 0x54)) void uart_putc(char c) { // 等待发送缓冲区为空(LSR第5位为1表示空) while (!(AUX_MU_LSR_REG & (1<<5))); AUX_MU_IO_REG = c; } void uart_puts(const char* str) { while (*str) { uart_putc(*str++); } } void uart_init() { // 把前面的配置代码封装到这里 // ...(省略上述配置代码) } int main() { uart_init(); uart_puts("Hello Raspi3b!\n"); while(1); return 0; }
3. linker.ld的链接地址
树莓派3B的AArch64代码加载起始地址是0x80000,链接脚本必须指定:
ENTRY(_start) SECTIONS { . = 0x80000; .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss) } }
4. Makefile适配M1 macOS
先通过brew安装交叉编译工具链:brew install aarch64-linux-gnu-gcc,然后编写Makefile:
CROSS_COMPILE = aarch64-linux-gnu- CC = $(CROSS_COMPILE)gcc AS = $(CROSS_COMPILE)as LD = $(CROSS_COMPILE)ld OBJCOPY = $(CROSS_COMPILE)objcopy all: kernel8.img loader.o: loader.s $(AS) -o loader.o loader.s kernel.o: kernel.c $(CC) -c -o kernel.o kernel.c -ffreestanding -O2 -Wall -Wextra kernel8.elf: loader.o kernel.o linker.ld $(LD) -T linker.ld -o kernel8.elf loader.o kernel.o kernel8.img: kernel8.elf $(OBJCOPY) -O binary kernel8.elf kernel8.img clean: rm -f loader.o kernel.o kernel8.elf kernel8.img
四、常见坑点排查
- 外设基地址错误:树莓派3B的外设物理地址是
0x3F000000开头,不要用树莓派2的0x20000000 - 缓存未禁用:裸机环境下必须禁用MMU和缓存,否则寄存器读写会被缓存,配置不生效
- Qemu版本过旧:用
brew upgrade qemu更新到最新版,旧版本的Raspi3b模拟存在串口bug - 字符发送未等缓冲区空:必须检查LSR寄存器的发送空标志,否则字符会直接丢失
内容的提问来源于stack exchange,提问作者Denis Steinman
相关产品推荐
相关产品推荐

