You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 02:05:32