ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

QEMU实战:STM32F103外设模拟驱动与调试指南

QEMU实战:STM32F103外设模拟驱动与调试指南 做嵌入式开发的朋友应该都有过这种体会驱动代码写好了板子还在打样或者板子只有一两块部门里好几个人排队等着用。真板子缺、调试手段又有限遇到问题只能靠示波器慢慢戳。QEMU在Linux、网络设备模拟上已经很成熟但提到模拟STM32F103很多人第一反应是“玩具”觉得外设模型肯定缺胳膊少腿。实际上只要把环境搭对、知道哪些外设能信、哪些不能信这套模拟器是相当能打的。它能帮你快速验证寄存器配置、跑通DMA和中断流程还能直接扔进CI流程做回归测试。这篇文章是QEMU-STM32系列的第十三篇主题就是STM32F103外设驱动模拟实战重点聊GPIO、UART、定时器、SPI和DMA这几类最常用的外设怎么在QEMU上驱动起来以及在调试中会踩到哪些坑。1. 整体思路与外设模拟的定位1.1 为什么拿QEMU来模拟STM32F103外设嵌入式驱动开发有个很尴尬的阶段外设驱动代码写完了但硬件平台还没就绪。如果没有模拟器只能干等板子。而QEMU恰好能把“外设寄存器”这一层模拟出来让驱动代码在PC上先跑起来。这里要强调一个认知QEMU模拟的不是电气特性而是寄存器行为和中断行为。它适合验证“我的初始化顺序对不对”“DMA搬运完有没有进中断”“UART发送时TXE标志位有没有被清掉”这类逻辑问题不适合验证“上电瞬间GPIO会不会抖动”“I2C上升沿是不是符合时序规范”这类硬件问题。我经常把QEMU下的外设调试比作“照着乐高图纸搭积木”。寄存器就是积木块QEMU负责检查你是不是搭对了位置。如果你把UART的波特率寄存器写错了模拟器不会像真板子那样“偶尔收发错误”它会严格按寄存器模型计算出错位的数据。对比真板子这反而更利于定位问题因为结果可复现。1.2 选择支持STM32F103的QEMU方案先泼一盆冷水QEMU主线对STM32F103的支持不算完善主线更完整的是STM32F100stm32vldiscovery板型和STM32F405stm32f405-soc。要专门跑STM32F103社区里最常用的是qemu_stm32这个fork它基于老版本QEMU增加了一部分STM32F103VCT6的外设模型包括GPIO、USART、SPI、I2C、TIM、DMA、EXTI、ADC等。因为是fork版本比较老但恰恰是外设种类最贴合F103的一个分支。启动命令每个人手里的版本可能不太一样常见的是这样qemu-system-arm -M stm32f103 -kernel firmware.bin -serial stdio如果机器名不对先用-M help看看当前版本支持哪些板子。拿不到qemu_stm32的环境也可以先用主线QEMU的stm32vldiscovery凑合学外设框架但要注意F100和F103的Flash大小、RAM大小和部分外设型号不完全一致。我的建议是能找qemu_stm32就用qemu_stm32毕竟少踩一个移植坑。1.3 交叉编译工具链与工程骨架模拟器层面搞定了剩下的关键就是交叉编译。STM32F103是Cortex-M3核需要用arm-none-eabi-gcc工具链。装好后要确认能正常跑arm-none-eabi-gcc --version一个最小工程通常包含四样东西启动文件startup.s、链接脚本stm32f103_flash.ld、主程序main.c、Makefile。我习惯自己维护一个精简版物料库避免每次翻Cube库。链接脚本最重要的一点是Flash地址必须从0x08000000开始RAM从0x20000000开始。F103VCT6有256K Flash和48K RAM但qemu_stm32如果按VCT6建模可以用这个配置如果模拟的容量小一些就按实际改。MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 256K RAM (rwx) : ORIGIN 0x20000000, LENGTH 48K } _estack ORIGIN(RAM) LENGTH(RAM); SECTIONS { .isr_vector : { KEEP(*(.isr_vector)) } FLASH .text : { *(.text*) *(.rodata*) } FLASH .data : { _sdata .; *(.data*) ; _edata .; } RAM AT FLASH .bss : { _sbss .; *(.bss*) ; _ebss .; } RAM }这里有个细节_estack不能写在.data段后面它是向量表第一个Word的初始SP值必须在链接阶段确定。启动文件里第一行引用的就是它。启动文件也不复杂最精简版本就是向量表加上Reset_Handler然后在Reset_Handler里拷贝数据段、清零BSS段最后跳转到main.syntax unified .cpu cortex-m3 .thumb .section .isr_vector .global _start .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler .section .text .thumb_func .global Reset_Handler Reset_Handler: ldr r0, _sdata ldr r1, _edata ldr r2, _sidata copy_loop: cmp r0, r1 bge copy_done ldr r3, [r2], #4 str r3, [r0], #4 b copy_loop copy_done: ldr r0, _sbss ldr r1, _ebss bss_loop: cmp r0, r1 bge bss_done movs r3, #0 str r3, [r0], #4 b bss_loop bss_done: bl main b . .thumb_func .global NMI_Handler NMI_Handler: b . .thumb_func .global HardFault_Handler HardFault_Handler: b .这段代码在真板子上能跑在QEMU里同样能跑。很多人一上来就写完整版启动文件其实没必要。模拟阶段只要保证向量表和Reset处理流程正确就行。Makefile我习惯写成无标准库的极简版本CROSSarm-none-eabi- CC$(CROSS)gcc LD$(CROSS)gcc OBJCOPY$(CROSS)objcopy CFLAGS-mcpucortex-m3 -mthumb -O2 -Wall -nostdlib -ffreestanding all: firmware.bin firmware.bin: firmware.elf $(OBJCOPY) -O binary firmware.elf firmware.bin firmware.elf: main.c startup.s stm32f103_flash.ld $(LD) $(CFLAGS) -T stm32f103_flash.ld -o firmware.elf main.c startup.s clean: rm -f firmware.elf firmware.bin2. 从GPIO到UART让外设“开口说话”2.1 点灯不是目的验证GPIO寄存器才是第一个实战例子我一般选GPIO点灯。不搞复杂初始化直接寄存器操作这样能在最短时间内确认三件事CPU能不能从Flash取指执行、GPIO时钟能不能打开、ODR能不能翻转。在QEMU里LED不会真的亮但你可以用QEMU monitor读GPIO寄存器也可以在GPIO翻转的地方加UART打印或者直接跑一个好认的延时逻辑。下面这段代码是我早期验证用的直接操作寄存器不依赖标准库#define RCC_BASE 0x40021000 #define RCC_APB2ENR (*(volatile uint32_t *)(RCC_BASE 0x18)) #define GPIOC_BASE 0x40011000 #define GPIOC_CRH (*(volatile uint32_t *)(GPIOC_BASE 0x04)) #define GPIOC_ODR (*(volatile uint32_t *)(GPIOC_BASE 0x0C)) static void delay(volatile int count) { while (count--) { __asm__ volatile(nop); } } int main(void) { /* 使能 GPIOC 时钟RCC_APB2ENR 的 bit4 是 IOPCEN */ RCC_APB2ENR | (1 4); /* PC13 对应 CRH 的 bit[23:20]配置为推挽输出模式 */ GPIOC_CRH ~(0xF 20); GPIOC_CRH | (0x2 20); while (1) { GPIOC_ODR ^ (1 13); delay(1000000); } return 0; }为什么把PC13作为口子因为很多F103开发板的板载LED就在PC13这么写比较有代入感。QEMU不会校验你是不是真连了LED它只负责把ODR寄存器的bit13翻来翻去。调试时你用GDB读0x4001100C这个地址能看到数值在变化就说明GPIO外设模型已经正常工作了。2.2 用UART输出日志模拟器的“显示屏”跑点灯还不够调试驱动力还是得看日志。在QEMU里UART最直观的出口是-serial stdio也就是把串口重定向到终端。STM32F103的USART1基地址是0x40013800配置过程分三步打开USART1和GPIOA的时钟配置PA9为TX复用推挽输出最后设置波特率和使能。先看初始化代码#define RCC_APB2ENR (*(volatile uint32_t *)(RCC_BASE 0x18)) #define GPIOA_CRH (*(volatile uint32_t *)(GPIOA_BASE 0x04)) #define USART1_BASE 0x40013800 #define USART1_SR (*(volatile uint32_t *)(USART1_BASE 0x00)) #define USART1_DR (*(volatile uint32_t *)(USART1_BASE 0x04)) #define USART1_BRR (*(volatile uint32_t *)(USART1_BASE 0x08)) #define USART1_CR1 (*(volatile uint32_t *)(USART1_BASE 0x0C)) void uart_init(void) { /* 使能 USART1 和 GPIOA 时钟 */ RCC_APB2ENR | (1 14); RCC_APB2ENR | (1 2); /* PA9复用推挽输出50MHz */ GPIOA_CRH ~(0xF 4); GPIOA_CRH | (0xB 4); /* * 波特率计算72MHz 时钟115200bps * USARTDIV 72MHz / (16 * 115200) 39.0625 * BRR 整数部分 39小数部分 0.0625 * 16 1 * BRR (39 4) | 1 0x271 */ USART1_BRR 0x271; /* UE1TE1RE1 */ USART1_CR1 | (1 13) | (1 3) | (1 2); } void uart_putc(char c) { /* 等待 TXE 位置位 */ while (!(USART1_SR (1 7))); USART1_DR c; } void uart_puts(const char *s) { while (*s) { uart_putc(*s); } }这里有个容易踩的坑USART1挂的时钟是APB2频率是72MHz不是APB1的36MHz。如果按36MHz计算BRR输出会是乱码。QEMU的UART模型会严格按照BRR配置产生波特率虽然它不产生真实波形但你用串口重定向看ASCII字符时如果配置错字符照样会变成乱码或偏移。在main里调用int main(void) { uart_init(); uart_puts(Hello from QEMU STM32F103\r\n); while (1); }启动QEMU时加-serial stdioqemu-system-arm -M stm32f103 -kernel firmware.bin -serial stdio终端里能看到Hello from QEMU STM32F103就说明UART外设通了。这一步是整个系列的地基后续所有外设调试日志都靠它输出。2.3 几个链接层面的小坑用-nostdlib编出来的固件很多人会把printf直接拿过来用结果发现编译报错或者链接不过。原因很简单没有C标准库。在模拟阶段我更建议自己实现简单的uart_puts或者用半主机semihosting方式。半主机可以让你在QEMU里用printf重定向到GDB或主机终端但要额外写_write和_sbrk函数有点绕。真正量产驱动时大家也不会靠printf输出日志都是走串口协议或者RTT所以一开始就别依赖printf。另外链接脚本里RAM AT FLASH这种写法要理解透.data段在运行时要放到RAM里但初始值存储在Flash里。Reset_Handler里的拷贝循环就是干这个的。如果忘了做这一步全局变量初值会是0甚至随机值。在QEMU里表现就是用一个初始化为0x12345678的全局变量读出来是0后来排查半天发现是数据段没拷贝。3. 定时器、PWM与中断相信状态别相信真实时间3.1 TIM定时器QEMU里的“时间”是虚拟时间STM32F103的TIM2挂在APB1上很多需求是“每1ms进一次中断”。在真板子上这取决于晶振和时钟树。在QEMU里定时器会按照SoC模型的主频跑但QEMU是动态二进制翻译执行速度受宿主机负载影响很大所以你看到的“1ms”并不是墙上的真实时间。换句话说可以验证逻辑上的定时关系但别拿QEMU的定时器去测真实的wall-clock耗时。一个典型的问题在QEMU里配置好TIM2定时中断期望每1秒翻转一次LED结果发现屏幕上的打印快得飞起或者卡得莫名其妙。这太正常了因为模拟器时间不等于现实时间。正确的做法是把“1秒”理解成虚拟时间里的周期然后看中断标志、计数器值是否符合预期。代码层面和真板子一样#include stm32f10x.h volatile uint32_t tick_count 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); tick_count; } } void tim2_init(void) { TIM_TimeBaseInitTypeDef tb; NVIC_InitTypeDef nvic; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); tb.TIM_Prescaler 7200 - 1; tb.TIM_CounterMode TIM_CounterMode_Up; tb.TIM_Period 10000 - 1; tb.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, tb); nvic.NVIC_IRQChannel TIM2_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 0; nvic.NVIC_IRQChannelSubPriority 0; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); }这里TIM_Prescaler 7200-1和TIM_Period 10000-1配合理论上72MHz经过7200分频后是10kHz计数10000次就是1秒。但QEMU里的“秒”只是虚拟时钟刻度的换算结果不是真实秒。调试时我更建议在中断里翻转GPIO或计数然后通过UART把tick_count打出来观察是否按预期递增。3.2 PWM输出怎么“看见”PWM在QEMU里看起来有点抽象没有示波器你没法直观看到占空比波形。很多人以为PWM没跑起来其实只是不知道怎么验证。我的经验是分成两步看。第一步看计数器CNT和比较寄存器CCR的值。PWM的本质就是“CNT和CCR比较后翻转电平”所以只要TIM-CNT在不断变化TIM-CCR是你设定的值就说明时序逻辑在工作。可以用GDB或者UART把这两个值读出来。第二步在PWM回调或更新中断里统计翻转行为。比如你用TIM1产生PWM配置PWM2模式高电平比较值设为500周期设为1000那一个周期内高电平比例就是50%。QEMU的定时器模型会更新OCx输出但不会给你拉一根虚拟示波器线。你可以在更新中断里判断当前CNT和CCR的比较状态然后打日志这样能侧面验证PWM频率和占空比。我自己常用的一个土办法是把某个GPIO在PWM更新中断里取反再用另一个定时器统计这个GPIO每秒钟翻转了多少次。虽然绕但能在纯模拟环境里把PWM频率验出来。这种方法放到真板子上同样有效只要别把调试代码带到发布版本里就行。3.3 中断向量表与NVIC的隐藏坑在QEMU里调试中断外设最常遇到的问题不是“没进中断”而是“进错了中断”或者“进中断后死循环”。F103的中断控制器NVIC已经模拟得比较完善向量表偏移的处理和真芯片几乎一样。如果用自定义链接脚本向量表默认就在0x08000000这个没问题。但如果你用HAL库的BootLoader模式要把向量表偏移到别的地址务必设置SCB-VTOR。F103的SCB地址是0xE000ED00VTOR偏移是0x08。在QEMU里不设置VTOR中断来了还是从0x08000000取向量跑飞是必然的。另一个常见坑是启动文件中向量表顺序。Cortex-M3的向量表第0项是初始SP第1项是Reset_Handler第2项是NMI第3项是HardFault。如果是标准库这些符号名称只要和链接结果对上就行。很多人自己写启动文件时把第0项写成了某个函数地址一上电就HardFault。QEMU会把这个错误暴露得很彻底第一条指令就进HardFault而且GDB里看PC指针跑到了不可预期的位置。4. SPI、I2C与DMA模拟“外部芯片”的边界在哪里4.1 SPIDMA读取外部芯片的模拟思路STM32F103的SPI1在APB2上最高18MHz。日常开发里常配合DMA去读外部Flash或传感器。标题里的热门搜索词“stm32f103 spi通过dma方式读取芯片数据 cubemx”正好命中这个场景。但这里必须说实话QEMU的F103 SPI模型虽然存在但能挂载的外部SPI从设备模型非常有限。如果你用qemu_stm32它可能只实现了SPI控制器自身不会自动帮你把一个虚拟Flash挂在SPI总线上。所以“SPIDMA读外部Flash”在纯模拟环境里的可行做法是验证DMA的搬运过程而不是验证外部Flash里的数据内容。具体怎么验证先初始化SPI1的发送和接收DMA通道。F103的DMA1请求映射中SPI1_RX对应Channel2SPI1_TX对应Channel3。使用CubeMX配置时它会在HAL_SPI_TransmitReceive_DMA函数里自动关联这些通道。但如果你用标准库就要自己填DMA_InitTypeDef。一个可用的DMA接收配置思路如下DMA_InitTypeDef dma; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); dma.DMA_PeripheralBaseAddr (uint32_t)SPI1-DR; dma.DMA_MemoryBaseAddr (uint32_t)rx_buffer; dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize 64; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; dma.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; dma.DMA_Mode DMA_Mode_Normal; dma.DMA_Priority DMA_Priority_High; dma.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel2, dma);在QEMU里跑这套代码重点观察两点一是DMA的DMA_ISR寄存器里传输完成标志有没有置位二是rx_buffer有没有被写入数据。如果QEMU模型没有数据源rx_buffer可能一直保持初始值这时候不要慌不是DMA配置错了而是总线上没有从机响应。想要看到真实数据可以走两条路。第一条写一个小型QEMU设备模型给SPI控制器挂上一个虚拟从设备每收到一个字节就回一个固定字节。第二条利用SPI的环回模式把MOSI和MISO短接让发送的字节直接回到接收路径。第二种在没硬件的情况下很简单但要看QEMU模型是否支持这种环回。我自己的习惯是先用GDB写SPI1-DR产生一个虚假数据源再把DMA跑通至少能验证DMA的数据搬运逻辑没毛病。4.2 用GPIO模拟I2C绕开坑人的硬件I2CSTM32F103的硬件I2C模块在真板子上都出了名的难调更别说QEMU里模型是否完整。所以我做模拟验证时更倾向用GPIO模拟I2C也就是bit-bang。这样有几个好处第一GPIO模型在QEMU里非常稳定第二不依赖I2C外设模型的实现质量第三代码移植到任何MCU都通用。GPIO模拟I2C的核心就是控制SCL和SDA两根线的电平。标准I2C时序里起始条件是SCL高电平时SDA拉低停止条件是SCL高电平时SDA拉高。写入一个字节时从最高位开始先把SDA设置为数据位然后拉高SCL、延时、拉低SCL。这是一个很机械的过程但恰恰是验证模拟器GPIO翻转频率的好机会。#define I2C_PORT GPIOB #define I2C_SCL GPIO_Pin_6 #define I2C_SDA GPIO_Pin_7 static void i2c_delay(void) { volatile int i; for (i 0; i 200; i); } static void i2c_scl_high(void) { GPIO_SetBits(I2C_PORT, I2C_SCL); i2c_delay(); } static void i2c_scl_low(void) { GPIO_ResetBits(I2C_PORT, I2C_SCL); i2c_delay(); } static void i2c_sda_high(void) { GPIO_SetBits(I2C_PORT, I2C_SDA); i2c_delay(); } static void i2c_sda_low(void) { GPIO_ResetBits(I2C_PORT, I2C_SDA); i2c_delay(); } void i2c_start(void) { i2c_sda_high(); i2c_scl_high(); i2c_sda_low(); i2c_scl_low(); } void i2c_stop(void) { i2c_scl_low(); i2c_sda_low(); i2c_scl_high(); i2c_sda_high(); } unsigned char i2c_write_byte(unsigned char byte) { unsigned char i; for (i 0; i 8; i) { if (byte 0x80) { i2c_sda_high(); } else { i2c_sda_low(); } byte 1; i2c_scl_high(); i2c_scl_low(); } /* 第9个时钟释放SDA并读取ACK */ i2c_sda_high(); i2c_scl_high(); unsigned char ack GPIO_ReadInputDataBit(I2C_PORT, I2C_SDA); i2c_scl_low(); return ack; }这段代码在QEMU里验证时有一点特别有意思由于没有真实外部器件拉低SDAACK通常是高电平。如果只简单地等待低电平ACK程序会卡在ACK检查里。所以模拟阶段我建议先不看ACK或者规定“读到高电平也算成功”重点验证字节发送的波形顺序。当然这只是策略真实硬件上绝不能这么干。4.3 UART DMA中断收发通信的模拟重点标题热词里还有“stm32f103 标准库uart dma中断接收发送通信”这个场景在QEMU里属于最容易验证的DMA应用。因为UART模型和DMA模型的联动在qemu_stm32里已经相对成熟可以直接跑完整流程。USART1的接收DMA请求是DMA1 Channel5发送是DMA1 Channel4。配置时先把USART1的DR地址给DMA外设地址内存地址给缓冲区方向按收发区分。接收DMA通常设置成循环模式并开启传输过半、传输完成中断。发送DMA则是一次性模式发送完成后在USART的TC中断里关闭DMA通道。模拟器里验证时我喜欢用-serial telnet:127.0.0.1:1234,server,nowait把串口暴露成TCP端口再用Python脚本往端口发一串数据。STM32F103的UART DMA模型会把收到的数据搬进内存缓冲区并触发DMA中断。这样一来自动化和CI化就能实现了。注意一点启动QEMU后用-serial stdio时终端输入会直接变成UART接收数据可以用它手工触发测试。但这种方式不适合自动化测试。用telnet模式再配一个测试脚本能让QEMU里的固件跑“回环测试”。我的习惯是固件收到一帧数据后用DMA原样发回去脚本比对收发是否一致。这个测试在真板子上也能用只是QEMU里跑起来不占硬件资源。5. 常见问题与调试技巧实录5.1 外设寄存器写了没反应这是模拟器里最常遇到的问题。配置了GPIOB时钟也改了CRL但读寄存器还是全零。绝大多数情况是时钟没使能。F103外设众多但每个外设挂在不同总线APB1、APB2、AHB各有各的时钟开关。比如USART1挂在APB2使能位是RCC_APB2ENR的bit14USART2挂在APB1使能位是RCC_APB1ENR的bit17。把RCC_APB2ENR当万能开关后面外设自然不响应。还有一种情况是QEMU模型本身没实现该外设寄存器读回总是0写进去也没有行为。这时需要查QEMU源码看看这个外设到底有没有被建模。以qemu_stm32为例它实现了不少外设但像SDIO、USB OTG这类复杂外设通常是没有的。日常用的GPIO/UART/SPI/I2C/TIM/DMA都有一般够用。5.2 程序跑飞和HardFault怎么查QEMU里查HardFault比真板子舒服得多。启动QEMU时加-s -S然后另开终端用arm-none-eabi-gdb连接arm-none-eabi-gdb firmware.elf target remote :1234 continue程序跑飞后GDB里能看到PC指针也能读到0xE000ED38的CFSR寄存器里面记录着HardFault的来源。常见的有总线错误、未对齐访问、无效指令等。F103是Cortex-M3对未对齐访问容忍度很低驱动代码里如果出现非对齐的结构体指针访问真板子可能偶尔正常QEMU里很容易直接HardFault。我遇到过最多的情况是DMA内存地址没按对齐要求写例如把DMA_MemoryBaseAddr设成了一个奇地址。DMA的数据宽度配置成字传输时地址必须4字节对齐。QEMU对地址的检查比真芯片严格出错就会挂。把数据宽度改成字节传输或者保证缓冲区4字节对齐问题就能解掉。5.3 常见问题速查表现象可能原因排查方法UART输出乱码波特率BRR计算错误或时钟源不对确认APB1/APB2频率重新计算BRRGPIO电平不变对应GPIO时钟没开或CRL/CRH配置错位读RCC_APB2ENR和GPIOx_CRL/CRH定时器不中断NVIC没使能或中断标志没清除检查NVIC_IRQHandler是否注册IRQHandler里清标志DMA搬运结果全0外设地址写错或外围设备没有数据源GDB读DMA_CCR和DMA_NDTR确认通道配置HardFault非对齐访问或地址越界通过GDB查看CFSR检查指针和缓冲区地址程序卡死中断向量表偏移未设或死循环等待中断检查VTOR检查中断使能位5.4 两个提升模拟效率的操作习惯第一个习惯是给固件加“自检模式”。启动后自动跑一遍外设自检GPIO翻转、UART发送、定时器计数、DMA搬运然后把结果通过UART打印出来。在QEMU里这个自检模式可以当CI测试用例用。每次改动QEMU机器配置或外设驱动代码后跑一次自检看到特定输出就算通过。第二个习惯是用QEMU monitor读出指定地址。QEMU启动后按CtrlA C进入monitor用xp /4wx 0x4001100C可以看GPIO ODR寄存器的当前值用info registers看CPU寄存器状态。这个比GDB轻量很多适合快速检查外设寄存器状态。如果发现QEMU monitor功能不全再用GDB也不迟。6. 最后分享一点个人体会从第一块真板子到现在用QEMU做外设模拟开发我最大的感受是模拟器不会替你解决硬件设计问题但它能把软件问题隔离出来让你在前置阶段就把寄存器配置、外设中断、DMA链路这些逻辑调得明明白白。以前调SPI传感器驱动要在示波器上反复看波形猜是时序问题还是数据格式问题。现在先在QEMU里把DMA搬运、中断标志这些软件逻辑验完再上板子时只需要关注真实传感器和电气特性调试范围小了很多。再分享一个小技巧跑QEMU模拟器时把出问题的固件加-d in_asm参数QEMU会把执行的指令流打出来。刚开始会觉得刷屏太快但遇到HardFault时看最后几十条指令往往能直接找到是哪条访问了非法地址。这个技巧我用了很久比单纯用GDB打断点更高效。如果你的工作流里也有“等待板子”的阶段不妨把这套外设模拟方案加进去。哪怕只是跑通UART日志和GPIO控制对团队协作和自动化测试都会有很大帮助。
返回列表