首页/新闻资讯/正文详情

QEMU模拟STM32F103外设驱动实战:从GPIO到SPI的全流程调试

发布时间:2026/9/28 1:48:49 来源:云帆数科 栏目:资讯中心
QEMU模拟STM32F103外设驱动实战:从GPIO到SPI的全流程调试
QEMU模拟STM32F103外设驱动这条路我从最开始写一个点灯程序都要折腾半天到后面能把UART DMA、定时器PWM、SPI这类外设驱动全部在模拟环境里跑通差不多用了大半个项目周期。这个系列写到第十三篇前面已经搭好了QEMU的machine、调试链路和基础运行环境这次把矛头对准STM32F103的外设驱动模拟本身也就是当你手上没有开发板、只有一台Linux主机时怎么把裸机外设驱动玩出真机调试的感觉。这篇文章适合正在用QEMU跑STM32F103的同学也适合那些被“硬件没到、驱动先行”逼着往前走的嵌入式开发者。你不需要真机就能完成大部分驱动逻辑验证像GPIO点灯、按键中断、串口收发、PWM输出这类最常见的外设都能在QEMU里拿到足够的调试信息。我会把整个模拟实战的细节拆开讲包括环境怎么配、外设驱动怎么写才适合模拟、踩过的坑有哪些、跟真机有哪些差异尽量照顾到刚上手的新手也让已经入门的人能挖到点干货。1. 环境准备与整体设计思路1.1 QEMU模拟STM32F103外设的定位与价值先说清楚QEMU到底是个什么级别的模拟器。QEMU不是那种“虚拟外设API”的仿真框架它是系统级模拟器直接执行ARM Cortex-M3的指令加载的是你编译出来的完整固件ELF文件跟你在真机上烧录到Flash里的几乎是同一份二进制。这意味着驱动代码里对寄存器的那堆读写操作QEMU会通过它内置的STM32F103 SoC模型去响应GPIO、USART、TIM这些外设都有对应的寄存器模型。你写的*(volatile uint32_t *)0x4001080C value这类操作在QEMU里一样会触发设备模型的更新。这种方式的定位跟HAL库层面的PC仿真完全不同。很多人用Keil的Simulator或者STM32CubeMonitor跑过所谓“模拟”但那些工具在指令级和外设级覆盖上都比较有限。QEMU跑的是一套完整系统从复位向量、时钟初始化、外设寄存器配置到中断触发全都沿着真实硬件的路径走。对于写驱动的人来说最大的价值就是驱动逻辑能不能在目标机架构上正确执行这不是桌面PC上跑一个C函数能做到的验证深度。实际项目中我最常用QEMU的场景有三类第一是硬件还没回来先把驱动框架、协议栈、状态机写好并验证第二是CI回归每次提交都跑一遍外设驱动的基本路径避免改一个模块把另一个模块搞坏第三是复现很难抓的时序问题尤其是中断和DMA配合的竞态问题在QEMU里可以加日志、打断点、甚至故意延迟某个事件来观察异常。当然QEMU不能完全代替真机但它能把“驱动能不能跑”和“驱动写得对不对”这两件事的大部分判断提前完成省下的可都是实打实的开发时间。1.2 从零搭好QEMU加STM32F103工具链如果你是第一次接触这套环境可以按下面这个最小组合来准备。宿主机我用的是Ubuntu 22.04Windows和macOS上也能跑只是包管理命令不一样大体思路相同。需要装的软件一共三样QEMU、ARM交叉编译工具链、Make等构建工具。sudo apt install qemu-system-arm sudo apt install gcc-arm-none-eabi sudo apt install make build-essentialQEMU里自带的-M netduino2机器就是基于STM32F103RBT6的这是最简单的一条路不需要自己写machine定义。netduino2板载的MCU是STM32F103RBT6Cortex-M3内核128KB Flash20KB RAM资源跟常见的“最小系统板”非常接近。启动QEMU加载固件的命令大概是这个样子qemu-system-arm \ -M netduino2 \ -m 128K \ -kernel build/demo.elf \ -nographic这里有个很重要的点-kernel后面跟的是ELF文件不是bin文件。QEMU能解析ELF里的加载地址知道把代码段放到0x08000000把数据段初始化好然后直接从复位向量开始执行。如果你手头只有bin文件就得自己指定加载地址比如-device loader,filefirmware.bin,addr0x08000000,cpu-num0相比之下ELF格式省事得多。工具链方面arm-none-eabi-gcc这个名字里的none表示无操作系统eabi表示嵌入式应用二进制接口它编译出来的固件不含Linux之类的系统组件正好符合裸机开发需求。链接脚本需要你自己准备因为STM32F103的Flash和RAM地址空间跟PC完全不一样。最基础的一个链接脚本可以这样写ENTRY(Reset_Handler) MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 128K RAM (rwx) : ORIGIN 0x20000000, LENGTH 20K } SECTIONS { .text : { KEEP(*(.isr_vector)) *(.text*) *(.rodata*) } FLASH .data : { _sdata .; *(.data*) _edata .; } RAM AT FLASH .bss : { _sbss .; *(.bss*) *(COMMON) _ebss .; } RAM }启动文件里的Reset_Handler要做的事不少包括设置栈指针、拷贝.data段、清零.bss段、调用SystemInit和main。如果你用的是STM32标准外设库或者HAL库它们自带的启动文件和系统初始化文件可以直接拿过来。在QEMU模拟环境里SystemInit里面的时钟配置同样会被执行QEMU会按照STM32F103的时钟树模型计算出外设时钟频率这一点后面讲UART波特率的时候会特别重要。1.3 QEMU外设模拟的层次与调试通道选择QEMU对STM32F103的外设模拟是分层实现的理解这层关系能帮你少走很多弯路。顶层是SoC模型STM32F103里面集成了ARM内核、GPIO、USART、SPI、I2C、TIM、ADC等外设每个外设都有自己的寄存器模型。QEMU不是对每个引脚电平做模拟而是对寄存器和中断行为做模拟也就是说当你给GPIO的ODR寄存器写值时QEMU的GPIO设备会更新内部的输出状态但它不会在一个虚拟示波器上画出波形来。这就引出一个关键问题在QEMU里观察外设行为主要的通道不是“看波形”而是看寄存器、日志和中断状态。常用的调试手段有几种。第一种是GDB调试。QEMU支持-s -S参数启动GDB服务然后你用arm-none-eabi-gdb连上去打断点、单步、查看内存和外设寄存器。这是最接近硬件调试器体验的方式qemu-system-arm -M netduino2 -m 128K -kernel build/demo.elf -nographic -s -S(gdb) target remote :1234 (gdb) break main (gdb) continue (gdb) x/4wx 0x4001080C第二种是添加QEMU本身的日志输出比如-d in_asm,exec,int可以看到执行的指令和中断情况-d int只看中断异常-d mmu看内存访问还有-d guest_errors能显示一些设备模型的告警。这些日志信息量很大排查一些诡异问题时特别好用。第三种是用串口输出。在QEMU里可以把STM32F103的USART映射到宿主机的stdin/stdout实现“固件printf到终端”。这个后面讲UART驱动时再详细展开。调试通道的选择没有一定之规我的习惯是常规逻辑用串口打印难缠问题用GDB怀疑QEMU设备模型本身有问题时才开guest_errors。2. GPIO与外设最基础的模拟调试从点灯到按键中断2.1 在QEMU里跑通GPIO点灯驱动点灯是嵌入式世界的Hello World在QEMU里做这件事的好处是能把整个工具链、链接脚本、启动流程都验证一遍。STM32F103的GPIO配置无非就是开时钟、配模式、写数据三件事。下面是一段最简的标准库风格GPIO点灯代码。以netduino2板载的PA0为例这个引脚上接有一颗LED低电平点亮。#include stm32f10x.h void delay(volatile uint32_t count) { while (count--) { __NOP(); } } int main(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); while (1) { GPIO_ResetBits(GPIOA, GPIO_Pin_0); delay(2000000); GPIO_SetBits(GPIOA, GPIO_Pin_0); delay(2000000); } }把这个代码编译成elf用QEMU跑起来物理板上应该能看到LED闪烁。但在QEMU里你盯着屏幕看是看不到灯的那怎么确认代码真的在跑最直接的办法是用GDB去读GPIOA的ODR寄存器。GPIOA的基地址是0x40010800ODR寄存器偏移是0x0C所以地址是0x4001080C。在GDB里连续查看这个寄存器的值(gdb) watch *(unsigned int *)0x4001080C Hardware watchpoint 1: *(unsigned int *)0x4001080C (gdb) continue如果程序在正常翻转引脚你会在GDB里看到ODR的值在0和1之间变化。这个现象说明GPIO外设模型在正常工作驱动代码、时钟配置、地址映射全都没问题。还有一个更简单的验证方法就是让QEMU把GPIO的变更打印到日志里。netduino2的machine定义里其实已经把LED对应的GPIO口接到了QEMU的图形界面上如果你不启动-nographic而是用-display none但保留图形窗口反而麻烦。实际使用中我用得最多的还是GDB观察点因为它在自动化脚本里也能工作。2.2 按键中断与外部事件模拟要点点灯只能算打通了“写寄存器”这条路驱动开发更关心的是中断。按键驱动的本质就是GPIO外部中断在STM32F103上对应的是EXTI控制器。配置步骤一般是使能AFIO时钟、配置GPIO为输入上拉、选择EXTI触发源、配置NVIC、写中断服务函数。void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { /* 处理按键事件 */ EXTI_ClearITPendingBit(EXTI_Line0); } } void key_init(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); EXTI_InitStructure.EXTI_Line EXTI_Line0; EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStructure); NVIC_InitStructure.NVIC_IRQChannel EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }问题来了QEMU里没有物理按键怎么模拟一次按键事件QEMU的netduino2 machine并没有把按键引脚的输入状态绑定到某个主机事件上但你可以通过两种方式模拟。第一种是在GDB里直接写GPIOA的IDR寄存器对应的输入电平然后让它跳变到低电平这会触发EXTI逻辑第二种是在QEMU monitor里用qemu-io这类工具去触发GPIO状态变化但操作比较繁琐。实际操作中我有一个更省事的办法就是把按键检测改成一个通过串口触发的软中断模拟。这不是在偷懒而是一种嵌入式常用的“遥测指令”设计你在驱动里预留一个测试命令收到特定字节就手动触发按键处理函数。这样在QEMU里验证的是“按键事件产生后中断处理逻辑是否正确”只不过事件源头从物理按键换成了UART数据。// 这段伪代码示意把按键事件做成可注入 void test_inject_key_event(void) { simulate_exti_falling_edge(GPIOA, GPIO_Pin_0); }真机上跑同样的固件物理按键按下产生触发EXTI0_IRQHandler照常执行。代码不需要改只是测试入口不同而已。2.3 基于HAL库还是标准库以及移植到APM32的注意点做QEMU外设模拟之前先想清楚你用的是哪套固件库因为这直接决定你后面调试时看到的寄存器行为。STM32F103有标准外设库和HAL库两套主流方案两者在QEMU里跑起来都没有问题但有几个关键差异需要留意。标准库SPL的特点是直接操作寄存器函数层级少调用的路径很短寄存器写操作的时序跟芯片手册几乎一一对应。在QEMU里用GDB单步调试时你能很清楚地看到每一个寄存器写入发生的时间点。HAL库则封装得很厚HAL_GPIO_WritePin这种调用会经过好几层状态判断和断言检查代码体积和执行路径都更长但好处是HAL库自带超时机制和状态跟踪在模拟环境里出现异常时更容易暴露出是在哪一层卡住的。很多人问过我把STM32F103的HAL库工程移植到APM32这类国产替代MCU上要改什么。我在QEMU里做过一次类似的验证结论是外设寄存器地址几乎不变需要改的是启动文件、系统时钟配置和个别外设库的实现细节。APM32F103跟STM32F103引脚兼容、寄存器兼容但时钟树内部结构有细微差异比如PLL倍频范围、某些RCC标志位定义不完全一样。/* STM32F103 HAL 的时钟配置 */ RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL9; HAL_RCC_OscConfig(RCC_OscInitStruct);只有HSEPredivValue这个字段在APM32上行为不同需要改成对应的APM32定义。我的建议是如果在QEMU里做外设驱动验证用标准库效率更高因为模型执行速度更快、调试更直接如果项目本身就是要基于HAL库开发那QEMU里也要编译HAL版本重点验证HAL初始化和外设处理逻辑不能因为“标准库快”就两头搞否则最后移植回真机时反而多了一道转换工作。3. UART串口通信与外设联动模拟3.1 标准库UART DMA中断接收发送的实现串口是嵌入式系统最常用的调试和外设通信接口QEMU对STM32F103的USART模拟得相对完整包括波特率生成、发送接收寄存器、中断和DMA请求。这也意味着你完全可以在QEMU里验证一套“UART DMA中断接收发送”的驱动而不只是流水账式的轮询收发。网上很多人在问STM32F103标准库UART DMA中断接收发送通信怎么写这里给一个我实际用过的精简版设计。思路是发送用DMA完成接收也用DMA并开空闲中断IDLE把不定长的数据帧切出来。DMA接收配置成环形缓冲区模式DMA用双缓冲第一路DMA把串口数据搬到缓冲区第二路处理缓冲区里的完整帧。void USART_DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); /* USBART1 TX DMA: DMA1_Channel4 */ DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(USART1-DR); DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)uart_tx_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize sizeof(uart_tx_buffer); DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel4, DMA_InitStructure); /* USART1 RX DMA: DMA1_Channel5 */ DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(USART1-DR); DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)uart_rx_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize RX_BUF_SIZE; DMA_Init(DMA1_Channel5, DMA_InitStructure); USART_DMACmd(USART1, USART_DMAReq_Tx | USART_DMAReq_Rx, ENABLE); DMA_ITConfig(DMA1_Channel5, DMA_IT_TC, ENABLE); }这个配置在真机上能跑在QEMU里同样能跑但有一个细节值得单独拿出来讲QEMU的USART模型对“空闲中断”的支持跟硬件行为并不是完全一致。STM32F103在真机上当接收线上出现一个字节间隙USART的IDLE标志会被置位QEMU有时对这个间隙的计时比较敏感如果你的代码依赖IDLE中断来切分不定长帧在QEMU里可能发现IDLE中断来得比预期晚或者根本不触发。解决办法是在QEMU里测试时用固定帧长加DMA传输完成中断的方式来切分数据等回到真机再切回IDLE中断模式。3.2 用-serial把串口接到终端实现模拟输入输出QEMU最有价值的模拟通道之一就是串口。启动时加上-serial mon:stdioSTM32F103的USART1就会被映射到QEMU进程的标准输入输出。这意味着固件里调用printf或者USART_SendData发送的数据会直接显示在终端里你在终端里敲键盘固件就像从串口收到数据一样触发接收中断。我在项目里经常把日志系统直接放在USART1上这样QEMU里跑起来就能实时看到驱动状态输出。比如前面那个GPIO走马灯例子加一行调试打印printf([LED] step %d, GPIO 0x%x\r\n, step, GPIOA-ODR);QEMU里启动qemu-system-arm -M netduino2 -m 128K -kernel build/demo.elf -nographic -serial mon:stdio按一下回车或者输入字符STM32固件里的UART接收中断就发生了这种“所见即所得”的调试方式比单纯看GDB寄存器舒服得多。有一点需要注意-serial mon:stdio里的mon:前缀表示把QEMU monitor和串口数据混在同一终端里按下CtrlA C可以切换进QEMU monitor。如果只想纯串口数据、不要monitor干扰用-serial stdio就行。我用的是带monitor的版本因为有时候需要临时查看设备状态切换进去执行info qtree可以查看当前虚拟设备树很方便。3.3 串口控制LED点灯走马灯的完整实践串口和GPIO联动是嵌入式驱动开发里很典型的一个综合场景上位机发命令下位机执行并返回状态。我拿这个需求写了一个完整的驱动模块在QEMU里把验证结果跑通代码结构如下typedef enum { CMD_LED_STEP 0x31, /* 1 走马灯单步 */ CMD_LED_START 0x32, /* 2 走马灯自动运行 */ CMD_LED_STOP 0x33, /* 3 停止 */ CMD_LED_STATUS 0x34 /* 4 状态查询 */ } led_cmd_t; void uart_parser(uint8_t byte) { static uint8_t cmd_buf[4]; static uint8_t buf_len 0; if (buf_len 4) { cmd_buf[buf_len] byte; } if (byte \n || cmd_buf[0] 4) { switch (cmd_buf[0]) { case CMD_LED_STEP: led_shift_step(); uart_send([OK] step\r\n, 11); break; case CMD_LED_START: led_auto_run_enable(); uart_send([OK] start\r\n, 12); break; case CMD_LED_STOP: led_auto_run_disable(); uart_send([OK] stop\r\n, 11); break; case CMD_LED_STATUS: uart_send_status(); break; default: uart_send([ERR]\r\n, 7); break; } buf_len 0; } }在QEMU里跑这个例子的效果是终端里输入1加回车PA0到PA7上的LED状态往前移一位输入2走马灯自动循环跑输入3停止输入4查询当前状态。全程不用任何硬件你就能把一个“串口命令下发、GPIO执行、状态回传”的最小闭环调通。这个实践的核心意义在于它验证的不只是单个外设驱动而是外设间的协同逻辑。真机上你确实能直接看LED亮灭但QEMU里通过串口回传的状态同样能精确反馈寄存器层面的结果。如果你的项目里需要用到串口收发加逻辑判断这套流程完全可以在没有开发板的情况下提前开发一个可验收的版本。4. 定时器PWM与内部外设联动模拟4.1 定时器PWM输出模式配置与测量方法PWM是STM32F103定时器最常用的功能之一用来驱动蜂鸣器、控制LED亮度、电机调速。网上搜“stm32f103定时器PWM输出模式”一般能找到的都是同一个套路TIM2的CH1配置成PWM模式设置ARR和CCR来调节频率和占空比。void timer_pwm_init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 999; // ARR TIM_TimeBaseStructure.TIM_Prescaler 71; // PSC TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; // CCR TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_Cmd(TIM2, ENABLE); }时钟频率的计算是这样的STM32F103默认使用8MHz外部晶振经过PLL倍频到72MHz系统时钟。APB1预分频如果设置为2那么APB1外设时钟是36MHz但定时器时钟会被自动二倍频到72MHz。所以上面的PSC71意味着计数器时钟是72MHz / (711) 1MHzARR999意味着PWM频率是1MHz / (9991) 1KHz占空比由CCR/(ARR1)决定CCR500时正好50%。在QEMU里验证PWM输出不能像真机那样用示波器量引脚波形。但有一种很实用的做法用GDB读取定时器的CNT寄存器、CCR寄存器和TIMx的SR标志来确认PWM逻辑是否在起作用。例如在程序运行过程中多次读取TIM2的CNT值(gdb) p/x *(unsigned int *)0x40000000 $1 0x1a3 (gdb) p/x *(unsigned int *)0x40000000 $2 0x3f0 (gdb) p/x *(unsigned int *)0x40000000 $3 0x3e8CNT在0到999之间循环说明时基在工作查看CCR寄存器是500说明比较值已装载。虽然没有波形但“时基在走、比较值正确”这两个关键条件已经验证完毕PWM驱动的核心逻辑在软件层面就是可靠的。4.2 利用QEMU调试定时器中断的时序定时器中断比PWM输出更复杂一点因为它牵扯到中断优先级、中断标志清除顺序和主循环之间的竞争。这类问题是真机上最头疼的一类问题因为一旦进了死循环示波器上能看到波形但不知道是哪行代码导致的。QEMU的GDB调试在这个场景下非常好用。假设你有一段每秒触发一次的中断服务程序里面做LED翻转void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); GPIO_ToggleBits(GPIOA, GPIO_Pin_0); } }QEMU里启动带GDB服务的实例然后给TIM2_IRQHandler打断点确认中断有没有进来(gdb) break TIM2_IRQHandler (gdb) continue Breakpoint 1, TIM2_IRQHandler () at main.c:85如果能断下说明定时器初始化、NVIC配置、中断向量表全部正常。如果断不下来优先检查三件事第一中断向量表有没有正确定义在0x08000000起始位置也就是__Vectors符号有没有被链接到Flash起始地址第二NVIC是否使能了TIM2中断第三定时器有没有真正启动也就是TIM_Cmd(TIM2, ENABLE)有没有执行。有一种情况很容易在QEMU里暴露而在真机上不好抓你在中断服务函数里把一个全局变量flag置1主循环里判断flag然后做耗时操作。如果flag的访问没有加volatile编译优化后主循环可能永远读不到更新后的值。QEMU执行的是真实编译出来的指令这种问题在模拟环境里跟真机一样会出现用GDB查看flag实际内存地址就能发现编译器有没有对它做优化。好的模拟环境的价值就在这里它不会因为你是“模拟”就放水。4.3 定时器和其他外设联动的模拟边界定时器在真实项目里很少单独工作它经常跟ADC触发采样、DMA传输、PWM驱动电机这类功能联动。QEMU对这种联动支持的程度需要分情况看。控制器内部的外设间联动QEMU能模拟得不错。比如TIM的更新事件触发ADC转换、ADC转换完成触发DMA搬运这类事件链在QEMU的STM32F103模型里有对应的信号连接。实际验证过TIM触发ADC采样再经DMA搬运的流程QEMU可以正确地把这条链路跑通产生的中断和DMA传输完成事件都能在代码里被捕获。但如果你想让PWM输出驱动一个外部的电机模型或者用ADC采集一个虚拟传感器的模拟电压这就超出了QEMU默认设备模型的范围。遇到这种需求通常的做法是在QEMU里验证“驱动发出的控制量是否正确”比如PWM的占空比、频率参数而把“控制量如何影响物理世界”留给真机联调。等价于把系统分成“计算层”和“物理层”QEMU覆盖计算层物理层留待硬件。这样划分后你在模拟环境里能完成的工作量其实非常大。5. SPIDMA方式读传感器与更多外设模拟实战5.1 SPI通过DMA读取芯片数据的工程配置本来想重点展开SPI外设驱动在QEMU里的验证方法结果发现QEMU对STM32F103的SPI模型支持度还可以至少寄存器级别的行为是可测的。很多人做STM32F103 SPI通过DMA方式读取芯片数据都用CubeMX生成配置整个过程比较固定。CubeMX里SPI配置成全双工主机模式打开硬件NSS或者软件NSS然后DMA设置里把SPI1_RX和SPI1_TX都挂上。DMA通道分配是这样的SPI1_RX对应DMA1_Channel2SPI1_TX对应DMA1_Channel3。如果你用的是SPI2对应关系会不一样SPI2_RX是DMA1_Channel4SPI2_TX是DMA1_Channel5。/* SPI1 DMA RX配置示例 */ DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(SPI1-DR); DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)sensor_rx_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 32; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel2, DMA_InitStructure);SPI的初始化要把CPOL和CPHA配置成和从设备一致的极性相位这是时序能否正确的关键。在QEMU里因为没有真实的SPI从设备SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE)会一直有数据可读或者一直读不到数据取决于QEMU设备的实现细节。所以QEMU对SPI驱动的验证主要集中在代码流程正确性上比如DMA通道配置对不对、中断回调是否触发、缓冲区数据有没有发生意外的覆盖。5.2 在QEMU里验证SPI DMA的地址与长度关系没有从设备SPI驱动怎么验证我目前验证的重点放在地址和长度这类“容易出错但不依赖物理设备”的属性上。比如DMA接收缓冲区的地址是否落到了RAM范围内是否越界传输长度是否和缓冲区大小匹配这些在QEMU里用GDB全部可以精确检查。(gdb) p sensor_rx_buf $4 (uint8_t (*)[32]) 0x200001c0 (gdb) p/x *(unsigned int *)0x4000380C /* DMA1_Channel2_CNDTR */ $5 0x20CNDTR寄存器显示32说明DMA传输长度是32字节跟缓冲区大小一致。如果CNDTR从32递减到0说明DMA确实发生过传输即使没有真实的SPI从设备DMA引擎本身的工作流程已经完整走了一遍。这里有个非常实用的避坑经验在QEMU里验证SPI DMA时DMA传输完成中断比较容易触发但SPI自身的BSY标志和CRC错误标志有可能因为模型细节出现异常值。正式编写测试日志时不要依赖SPI状态寄存器的某些错误标志作为断言条件而是把DMA传输完成事件和接收缓冲区的数据内容作为验证依据这样在QEMU和真机之间会有更好的一致性。5.3 minimp3解码库等纯软件外设在QEMU上的运行验证外设驱动模拟不只是跟硬件寄存器打交道很多项目里的“外设”其实是软件算法比如音频解码库、文件系统、AT指令解析器。有人问过STMF103移植minimp3解码库在真机上跑能不能先在QEMU里验证。这个问题我试过答案是完全可以而且很香。minimp3是一个单头文件的MP3解码库资源占用小适合Cortex-M3。把主程序改成从文件系统读取MP3数据调用minimp3解码成PCM再从DAC或者I2S输出。QEMU里没有实际的DAC设备但解码过程中密集的计算、内存搬移、循环缓冲区的边界操作都是CPU密集型的逻辑QEMU完全能模拟。关键是在QEMU里跑这种纯软件库时应该把精力放在算法正确性和内存安全上。用GDB给mp3dec_decode_frame打断点观察每一帧解码返回的采样数和错误码如果出现MP3D_E_DECODE错误就对比源数据看是损坏帧还是解码库边界处理有bug。内存方面QEMU可以配合AddressSanitizer或者自己维护一个内存池管理器检测越界读写。这些验证在真机上做很费劲但在QEMU里做很快因为它可以随意打断、回放、加日志。无线数传、蓝牙模块、LoRa这些通信类外设的AT指令解析器同样可以用QEMU加虚拟串口来验证。上位机通过串口发AT指令固件解析后返回响应整个过程跟驱动硬件完全解耦把“串口驱动层”和“协议解析层”分成两部分验证每一部分都能在模拟环境里做到很高的覆盖度。6. QEMU外设调试常见问题与排查技巧6.1 程序跑飞、进不了中断、串口无输出的系统级排查QEMU里跑STM32F103外设驱动经常遇到的问题看起来五花八门但排查路径其实有规律可循。最高频的三个问题是程序直接跑飞、中断进不来、串口没有输出。这三个问题有一个共同的排查切入点确认你的程序是不是真的从复位向量正确启动了。在QEMU里程序跑飞最常见的原因是链接脚本出错代码被链接到了错误的地址。一个典型的错误是把.text段放到了0x08000000以外的位置比如默认的0x00000000导致QEMU加载固件后PC跳到非法地址。排查方法是在GDB里查看$pc寄存器和info registers如果PC指向了一个非Flash地址几乎可以断定是链接脚本问题。进不了中断的排查顺序稍微复杂一点。第一步看NVIC有没有使能这对应NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE第二步看外设的中断标志位有没有在中断服务函数里清除如果不清除中断会反复触发或卡死第三步看优先级分组配置是否一致如果主程序里设了NVIC_PriorityGroup_2但某个驱动文件里没有统一配置中断行为会变得很诡异。在QEMU里通过GDB读取NVIC的ISER寄存器和外设的SR寄存器三步就能定位。串口没有输出是最好排查的因为多半是波特率配置问题。QEMU模型会按照你系统初始化代码里设置的时钟频率来计算波特率分频值如果你SystemInit里配置的PLL倍频是9倍但链接进来的启动文件没有正确调用SystemInit那么USART的波特率计算基准可能是8MHz而非72MHz此时主机端看到的波特率和固件实际发送的波特率不一致。QEMU把串口映射到stdio时默认没有“主机侧波特率”的说法理论上不存在波特率不匹配问题因为模型直接按分频后的位时间输出字符。但如果主机端的模拟终端把输入输出设置成了某种古怪模式也会导致显示乱码这时候检查一下终端编码和QEMU的-serial参数即可。6.2 QEMU外设模型与真实STM32F103的差异清单用QEMU做外设驱动模拟最忌讳拎不清“哪些验证结果在真机上可以复现哪些不可复现”。我整理了一份差异清单按外设类型分类直接对照着看就行。外设QEMU模拟情况与真机的关键差异应对建议GPIO寄存器读写、输入输出状态、EXTI中断不模拟引脚电平时序和电气特性用GDB观察寄存器变化不可依赖精确延时USART发送接收、中断标志、DMA请求IDLE空闲中断行为跟真实流控有差异固定帧长切帧回到真机再用IDLETIM时基计数、PWM比较、更新中断没有引脚输出波形只验证寄存器和中断关注CNT/CCR/SR寄存器不追求波形精度SPISPI寄存器、DMA请求没有真实从设备信号时序不真实验证DMA配置和缓冲区管理逻辑ADC转换寄存器可以操作没有真实模拟电压输入转换结果不可控用固定值寄存器注入测试数据DMA传输流程完整模拟性能差异较大仲裁行为比真机简单验证功能逻辑不验证带宽性能NVIC中断分组、抢占、尾链基本支持异常时序跟硬件流水线有差异关注优先级逻辑不验证精确时钟周期这些差异并不意味着QEMU的模拟结果没用而是要你把验证目标调整到正确的层级。QEMU擅长验证的是“代码逻辑、寄存器配置、中断处理流程、缓冲区管理”它不擅长的是“时序性能、电气信号、模拟量精度”。如果你在QEMU里验证的东西属于前者结果完全可以放心如果属于后者就需要谨慎对待。还有一个容易被忽视的点QEMU的执行速度不等于STM32F103真机的执行速度。QEMU在宿主机上跑同样的代码可能比真机快很多也可能慢很多取决于宿主机的CPU性能。如果你的驱动里有基于nop指令的延时函数比如delay(2000000)在QEMU里的实际延时和真机上是不一致的。所以在模拟环境里验证时间敏感型逻辑时尽量用定时器而不是空循环延时。6.3 高频问题速查表建一个速查表把我在QEMU外设模拟实战中遇到的高频问题、现象、排查思路和解决方案整理出来。这个表是这套系列文章里最常被我自己回来翻的一份东西。现象可能原因排查与解决程序跑飞到0xFFFFFFFF中断向量表缺失或链接地址不对检查链接脚本FLASH起始地址是否为0x08000000中断服务函数进不去NVIC未使能或外设中断标志未清除GDB查NVIC-ISER确认中断号和使能位串口输出乱码系统时钟和波特率分频不匹配确认SystemInit执行PLL配置为9倍频到72MHzUART DMA接收不触发完成中断DMA通道错误或CNDTR未设置核对DMA1_Channel5的CNDTR寄存器初值PWM无输出定时器时钟没开或者引脚没有复用检查RCC_APB1Periph_TIM2和GPIO_Mode_AF_PPSPI读取的数据全为0xFF没有从设备应答或SPI模式不匹配在QEMU里重点验证DMA配置数据内容交给真机程序卡在某个while循环外设状态位没有按照预期变化用GDB查看循环条件的寄存器逐步定位复位后变量初值不对.data段没有拷贝或.bss段没有清零检查启动代码里的__copy_data和__clear_bss这个表只是一个起点实际项目中你还会遇到很多跟具体驱动相关的怪问题。但套路是一样的先确认基础环境正常再逐层检查寄存器、中断、DMA最后考虑QEMU模型本身的边界。沉住气总能找到问题所在。6.4 避坑心得QEMU不是万能但也不是玩具写到最后这一段我特别想聊一聊QEMU外设模拟这个工具在团队里应该怎么定位。如果你把它当成一个“真机替代品”期望它能验证所有事情你一定会失望因为GPIO的电气时序、ADC的模拟精度、SPI跟外部芯片的握手细节这些QEMU确实做不到。但如果你把它当成一个“驱动逻辑的预检工具”你会发现它比想象中能做的事情多得多。我的使用习惯是QEMU里验证一切不依赖物理世界的外设逻辑包括寄存器配置是否正确、中断处理是否有遗漏、DMA传输是否完整、缓冲区是否越界、状态机是否正常跳转然后把QEMU无法覆盖的部分比如模拟量采集精度、PWM驱动外部设备的实际效果、无线模块的射频通信留到真机联调时一次性解决。这样切分之后真机上要处理的问题范围大幅缩小联调时间往往能缩短一半以上。从刚开始接触QEMU时踩了无数坑到现在把外设驱动模拟当成日常开发的一部分我最大的体会是模拟环境逼着你把代码写得更规范、更可测。因为没有硬件可以“试错”你必须靠日志、断言、寄存器检查把这些验证手段织成一张网等到代码到真机上时反而比之前直接怼硬件开发更稳。这个系列写了十三篇从环境搭建到外设驱动实战如果你也正在用QEMU做STM32F103相关开发希望这篇能帮你绕开一些我已经踩过的坑把时间花在更有价值的问题上。

相关推荐

TMC2209串口配置实战:UART帧、CRC8校验与寄存器读写详解
TMC2209串口配置实战:UART帧、CRC8校验与寄存器读写详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/28 1:48:43

Java毕业设计三端源码拆包:小程序+后端+爬虫完整链路实战
Java毕业设计三端源码拆包:小程序+后端+爬虫完整链路实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/28 1:48:43

安全背心识别数据集与YOLOv8目标检测训练实战指南
安全背心识别数据集与YOLOv8目标检测训练实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/28 1:48:43

Spingboot启动预热的实现
Spingboot启动预热的实现

启动预热的适用场景启动预热适合以下情况:数据主要来自第三方接口,无法直接从本地数据库读取。第三方接口响应较慢,首次访问容易超时。一个页面需要调用多个第三方接口或逐项查询。数据读取频繁,但变化不频繁。希望服务启动后&… · 2026/9/28 3:40:12

Understanding Driving Risks using Large Language Models: Toward Elderly Driver Assessment
Understanding Driving Risks using Large Language Models: Toward Elderly Driver Assessment

文章主要内容总结 本文研究了多模态大语言模型(具体为ChatGPT-4o)利用静态行车记录仪图像进行类人交通场景解读的潜力,重点聚焦与老年司机评估相关的三项任务:交通密度评估、交叉口可见性评估和停车标志识别。这些任务需上下文推理而非简单目标检测。研究采用零样本、少样… · 2026/9/28 3:32:43

Leveraging Large Language Models for Classifying App Users‘ Feedback
Leveraging Large Language Models for Classifying App Users‘ Feedback

文章主要内容总结 本文聚焦于利用大型语言模型(LLMs)解决应用用户反馈分类的挑战,传统方法依赖有监督机器学习,但受限于标注数据集的规模和质量。研究通过三个核心实验评估了4种先进LLMs(GPT-3.5-Turbo、GPT-4o、Flan-T5、Llama3-70b)的性能: LLMs在用户反馈分类中的基… · 2026/9/28 3:32:43

Using Large Language Models for Legal Decision-Making in Austrian Value-Added Tax Law: An Experim...
Using Large Language Models for Legal Decision-Making in Austrian Value-Added Tax Law: An Experim...

文章主要内容总结 本文通过实验评估了大型语言模型(LLMs)在奥地利及欧盟增值税(VAT)法框架下辅助法律决策的能力。研究聚焦于两种提升LLM性能的方法——微调(fine-tuning)和检索增强生成(RAG),并在两类案例中进行验证:一是权威教科书案例,二是税务咨询公司的真实案… · 2026/9/28 3:32:43

学Java别走弯路,这5个方向最吃香
学Java别走弯路,这5个方向最吃香

学Java的人很多,但学明白的人不多。有人学了半年还在写控制台程序,有人一年就能独当一面。差别不在天赋,而在方向。Java生态太庞大了,什么都学等于什么都没学。选对方向,事半功倍。今天盘点当前最吃香的5个Java方向&am… · 2026/9/28 3:32:15

AlphaAgents: Large Language Model based Multi-Agents for Equity Portfolio Constructions
AlphaAgents: Large Language Model based Multi-Agents for Equity Portfolio Constructions

AlphaAgents相关总结与翻译 一、文章主要内容总结 (一)研究背景与问题 传统股票投资组合管理依赖人类分析师处理海量信息(如财务披露、财报、市场新闻等),存在信息处理效率低、易受认知偏差(如损失厌恶、过度自信)影响的问题,可能错失投资收益机会。尽管AI在数据处理… · 2026/9/28 3:32:08

MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现

简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01

汕头网站建设制作厂家避坑指南:5大注意事项救急
汕头网站建设制作厂家避坑指南:5大注意事项救急

汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01

多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习

简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01

制作网页比较方便的软件怎么选?一文搞懂避坑指南
制作网页比较方便的软件怎么选?一文搞懂避坑指南

制作网页比较方便的软件怎么选?一文搞懂避坑指南 很多老板一上来就问:做个网站多少钱?但我反问他:你的域名买了吗?服务器租了吗?他一脸懵。这就是典型的“域名服务器搞不懂”。别急,今天咱们不聊虚的,直接 一文搞懂 那些让你头秃的技术名词。… · 2026/9/28 0:00:06

婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量
婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量

婚恋网站实战案例:避开3个高价坑,省钱50%还能跑赢流量 找婚恋网站建站公司,最怕的就是被坑高价。很多同行跟我吐槽,报价单上写得模棱两可,功能栏里全是“高级定制”、“专属UI”,结果落地全是套壳。今天不聊虚的,直接甩几个我经手的 实战案例… · 2026/9/28 0:00:19

济南做网站多少钱:3个案例拆解,防黑源码下载全攻略
济南做网站多少钱:3个案例拆解,防黑源码下载全攻略

济南做网站多少钱:3个案例拆解,防黑源码下载全攻略 上周济南一个做建材的老板找我,脸都绿了。他的官网首页弹出了赌博广告,后台被植入了挖矿脚本。他慌得问我:“网站被黑挂马不知道怎么办?能不能直接找之前的外包公司要源码下载,看看哪里被动了手脚?… · 2026/9/28 0:00:25

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码