ARTICLE DETAIL

资讯详情

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

Proteus仿真STM32跑FreeRTOS:从CubeMX配置到任务调度与排错

Proteus仿真STM32跑FreeRTOS:从CubeMX配置到任务调度与排错 这个仿真系列写到第10篇了。前面9篇里GPIO点灯、串口printf、定时器中断、ADC采样都是用HAL库在Proteus里一步一步搭起来的全程没碰真实开发板。到了这一篇终于轮到FreeRTOS。说实话Proteus上做FreeRTOS仿真比普通外设仿真要敏感得多——时钟配置、SysTick归属、堆栈参数随便错一个系统要么卡死在启动阶段要么任务跑着跑着就hard fault。但好处也很明显任务优先级的影响、时间片轮转的节奏、队列通信的数据流这些抽象概念在仿真里变成LED闪烁和串口日志看得见摸得着比干读《FreeRTOS手册》强太多。这篇东西适合三种人一是刚接触RTOS、想先看调度效果再上手硬件的学生二是手头没板子但想验证FreeRTOS移植思路的工程师三是已经跟了这个系列、想把前面外设Demo升级成多任务工程的读者。我会把CubeMX配置、代码生成、Proteus图纸搭建到调试排错整个链路都过一遍尤其后面那节排查记录全是我自己跑仿真时踩过的坑。1. 先说清楚Proteus里跑FreeRTOS到底图什么1.1 仿真能验证的和验证不了的很多工程师一上来就否定Proteus仿真这玩意根本做不了时序仿真和真实硬件差远了。这话对但不全对。Proteus的STM32模型是基于指令级模拟的确实做不到真实芯片那种精确到纳秒的外设时序但它的优势在于你能在5分钟内改一个任务优先级然后立刻看到行为变化能在不烧录的情况下验证队列数据有没有按期望流转能在一台没有开发板只有笔记本电脑的机器上完成整个RTOS入门学习。具体到FreeRTOS场景Proteus能验证的是这几类东西任务创建与调度的基本行为高优先级任务是否抢占低优先级任务、同等优先级任务是否按时间片轮转阻塞与延时的逻辑osDelay、信号量等待、队列读取这些阻塞点是否按预期挂起和恢复任务间通信的数据流队列里放进的数据是否被正确消费互斥锁有没有挡住共享资源冲突栈深度和堆大小够不够在仿真环境里故意调大任务栈观察行为比在硬件上看hard fault直观得多。验证不了的东西也要心里有数极端的实时性要求、中断响应时间、功耗以及那些依赖芯片内部模拟特性的外设行为。把这几条边界想清楚Proteus就能成为一个高效的学习验证工具而不是一个玩具。1.2 为什么这个系列坚持用HAL库这个系列从第1篇开始就用HAL库不是因为标准库不行而是因为HAL库有完整的CubeMX图形化配置支持。做FreeRTOS移植第一步永远是配置时钟树、配置外设、生成初始化代码这些在CubeMX里就是勾几个选项的事。标准库当然可以移植FreeRTOS但所有初始化要手写光时钟配置就够写一屏对学习RTOS本身没有帮助。HAL库里还带了一个特别关键的机制——HAL_Delay和HAL_GetTick依赖一个1ms时基。默认情况下这个时基来自SysTick而FreeRTOS调度器也需要SysTick来产生系统节拍。这两件事撞在一起就是你后面会遇到的最大一个坑。HAL库的优势恰恰在于它的时基源可以通过CubeMX一键切换给FreeRTOS腾出SysTick。这个细节下一节重点拆。2. CubeMX端配置时间基准、堆大小与任务清单2.1 SysTick让位时间基准必须改到其他定时器打开CubeMX先按照前面9篇的套路选好STM32F103C8T6配置好RCC。这里有个动作特别容易被忽略在左侧引脚分类里找到SYS把Timebase Source时基源从默认的SysTick改成TIM1或TIM2。我习惯用TIM1因为它挂在APB2总线上而且一般没有被外设占用TIM2、TIM3留给编码器或者PWM更实用。这个改动的原理是HAL库需要一个1ms周期中断来驱动HAL_IncTick函数累加出HAL_GetTick的毫秒数。SysTick刚好能干这个活但FreeRTOS的vTaskDelay、任务切换同样依赖一个周期性中断它也看上了SysTick。两个主人抢一个中断结果就是系统跑一会儿就崩——这是FreeRTOS HAL库最经典的冲突场景。把HAL的时基切到TIM1之后SysTick_Handler中断就完全由FreeRTOS接管CubeMX生成的代码里SysTick_Handler会自动调用xPortSysTickHandler()两边各用各的定时器互不干扰。切换完之后注意一下生成的工程CubeMX会额外生成stm32f1xx_hal_timebase_tim.c这类源文件HAL_InitTick会被改写为用TIM1产生1ms中断。如果你在Keil的编译日志里没看到这个文件参与编译那就说明你的时基没有真正切过来。2.2 FreeRTOS参数里最关键的三个配置在CubeMX左侧打开Middleware and Software Packs - FreeRTOS首先把Interface选成CMSIS_V2。这是ARM标准的CMSIS-RTOS v2 API封装层更干净osDelay、osMessageQueuePut这些函数都是v2风格。CMSIS_V1是老接口虽然也能用但新项目不建议再开倒车。然后看Config parameters选项卡重点看三个参数参数推荐值作用configTOTAL_HEAP_SIZE8192 ~ 12288FreeRTOS所有任务栈、队列、信号量共享的堆大小configCHECK_FOR_STACK_OVERFLOWEnable上下文切换时检查任务栈是否越界触发钩子函数configUSE_MALLOC_FAILED_HOOKEnable堆耗尽时调用钩子第一时间暴露内存不够configTOTAL_HEAP_SIZE这个值STM32F103C8T6有20KB SRAM仿真里一般给8KB到12KB就够。你如果建了三四个任务外加一两个队列8KB会比较紧张给12KB比较稳。但这也不是越大越好堆声明的是一个静态数组直接占SRAM给大了空闲时也白白耗着。这几个配置对应了FreeRTOSConfig.h里的宏。CubeMX生成代码后你当然可以手改但直接在图形界面配好省得重新生成代码又被覆盖回去。2.3 在CubeMX里直接定义任务还是手写CubeMX的Tasks and Queues选项卡里可以预先定义任务和队列生成代码时会直接生成对应的任务句柄和入口函数骨架。我推荐在CubeMX里先建好任务和队列因为生成的骨架代码会主动挂在创建函数里你只需要往入口函数里填业务逻辑不会出现忘了调用osThreadNew导致任务没创建这种低级错误。比如建两个任务LED1_Task优先级osPriorityNormal栈大小128 wordsUART_Task优先级osPriorityBelowNormal栈大小256 words——因为要调printf栈需求偏大。再建一个队列myQueue长度10消息大小4字节也就是放uint32_t用的。这些参数生成之后main.c里能看到对应的句柄定义入口函数在freertos.c里直接往里面写字就行。3. 代码侧HAL库和FreeRTOS怎么协作3.1 生成代码后的文件结构和入口CubeMX生成的FreeRTOS代码分散在两个文件main.c负责初始化所有外设然后调用MX_FREERTOS_Init()freertos.c负责创建任务、队列、信号量最后调用osKernelStart()启动调度器。main函数执行完osKernelStart()之后程序控制权就交给调度器了main函数后面再写任何代码都执行不到这个要有个概念。任务入口函数是void函数带一个void* argument参数函数内部几乎一定是个while(1)死循环配合一个阻塞调用osDelay、队列读、信号量等待释放CPU。没有阻塞的话任务就一直占着CPU同优先级甚至低优先级任务就没机会跑。很多初学者写RTOS任务喜欢在里面写一个for(;;)空转也不放延时结果另一个任务一动不动这就是典型的调度没有被让出来。3.2 用两个LED任务验证调度逻辑先看最基础的验证方法。两个任务各自翻转一个LED延时不同如果两个LED都在按自己的节奏闪说明调度器正常工作。/* freertos.c 里两个任务入口 */ void LED1_Task(void *argument) { while (1) { HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); osDelay(300); } } void LED2_Task(void *argument) { while (1) { HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin); osDelay(700); } }两个任务优先级相同、都用osDelay阻塞自己那么FreeRTOS就会按时间片轮转调度。LED1闪得快一点LED2慢一点两个都在动这就能说明多任务在跑。要观察抢占效果也很简单建一个高优先级任务在循环里做一堆空运算并且不延时另一个低优先级任务闪灯。高优先级任务只要不需要等待它就会一直霸占CPU低优先级任务几乎抢不到执行时间LED闪得极慢甚至不动。把高优先级任务里加一个osDelay调度器立刻让出CPULED恢复正常闪烁。这个实验在真实板子上也能做但Proteus里改优先级只需要在CubeMX下拉框里选一下重新生成代码烧进去对比特别直观。3.3 队列收发HAL与FreeRTOS共存的典型写法任务调度跑通之后队列通信是第二个必须练手的点。用CubeMX定义好的myQueue代码里这样写生产者和消费者/* 生产者定时往队列里塞一个递增计数 */ void Producer_Task(void *argument) { uint32_t cnt 0; while (1) { osMessageQueuePut(myQueueHandle, cnt, 0, 0); cnt; osDelay(500); } } /* 消费者阻塞等待队列数据收到后通过串口打印 */ void Consumer_Task(void *argument) { uint32_t rx; char buf[32]; while (1) { if (osMessageQueueGet(myQueueHandle, rx, 0, portMAX_DELAY) osOK) { sprintf(buf, rx: %lu\r\n, (unsigned long)rx); HAL_UART_Transmit(huart1, (uint8_t*)buf, strlen(buf), 100); } } }注意osMessageQueueGet的最后一个参数是等待时间portMAX_DELAY表示无限等待任务在这里挂起只有队列里有数据才被唤醒。这就是RTOS事件驱动模型最核心的一点没有数据的时候任务不轮询、不空转CPU时间留给其他任务。HAL_UART_Transmit是阻塞式的在仿真里串口打印本身耗时这会间接影响任务节拍但用于观察数据流完全够用。4. Proteus 8.9图纸搭建与hex装载4.1 元件选型与最小系统连接Proteus 8.9的元件库里已经带了STM32F103系列模型搜索STM32F103C8就能找到。如果你搜不到说明元件库不完整需要重新安装Proteus的MCU库补丁或者检查是不是装成了不含ARM模型的简化版。这一点很多新手卡了半天先把这个确认了再继续。图纸上放置STM32F103C8后最少要接这几根线VDD、VDDA接到3.3V电源网络VSS、VSSA接到GNDNRST接一个10k电阻上拉到3.3V保证上电不复位BOOT0和BOOT1都接到GND让芯片从内部Flash启动如果CubeMX里配置的是HSE外部晶振图纸上必须在OSC_IN和OSC_OUT之间接一个8MHz晶振两边各接一个20pF电容到地如果用的是HSI内部时钟OSC引脚悬空就行。4.2 时钟设置仿真模型时钟与固件配置必须一致这是Proteus跑FreeRTOS最容易翻车的点单独拿出来说。STM32F103C8T6的最大主频是72MHzCubeMX里用HSE 8MHz经过PLL倍频到72MHz很常规的配置。但Proteus的STM32模型里有一个Clock Frequency属性双击芯片在属性框里找到它必须填成和固件实际运行时钟一致的值也就是72MHz。填错会怎样固件里osDelay(500)按72MHz的节拍来算Proteus模型却按8MHz来模拟运行所有时间相关的行为全部错乱UART波特率也会不对虚拟终端打出来全是乱码。这里有个实操技巧CubeMX里SYS的Debug选项如果配了Serial WireProteus里也要把PA13/PA14留出来否则仿真时芯片可能进不了正常模式。我在前几篇仿真里踩过这个坑这次提醒一下。另外Proteus虚拟仿真跑FreeRTOS时整体速度会比真实芯片慢LED闪烁看起来会拖沓一点这正常不影响逻辑判断。4.3 装载hex和启动仿真Keil工程编译前确认Output选项卡里勾选了Create HEX File这样编译才会在.\obj\目录下生成hex文件。Keil里偶尔会报一个错误error: q0147e: failed to create directory .\obj\freertos这通常是因为工程名或输出路径里带了空格、中文或者文件夹权限不对把输出路径改短、保证全是英文就能解决。在Proteus里双击芯片Program File那一栏填上hex文件的完整路径也可以点文件夹图标浏览选择。时钟频率按上一节说的填好点左下角的播放按钮开始仿真。如果一切正常两个LED按各自的频率闪起来虚拟终端里能看到队列消费者打印出来的递增计数这一套就算通了。5. 实测中的坑任务不跑、系统死机和栈溢出的完整排查5.1 症状一仿真窗口一切正常但任务一个都不动这个症状的出现最有迷惑性Proteus里芯片引脚没有任何反应也没有报错程序就像困在某个死循环里。我遇到过两次一次是CubeMX里配置了HSE外部晶振但Proteus图纸上没放晶振芯片时钟起不来整个系统卡在SystemClock_Config的等待HSE就绪死循环里另一次是Proteus芯片属性里Clock Frequency填的和固件不一致FreeRTOS的系统节拍算出来的时间全不对任务也处于一种奇怪的僵持状态。排查顺序可以固定下来先看仿真运行后芯片有没有进入主函数最简单的方法是临时在系统启动完成之后、创建任务之前翻转一个GPIO接LED。LED不亮说明卡在更早的位置优先检查时钟配置和芯片启动条件LED亮了但任务没动作说明卡在任务创建或调度器启动这一步。第二步检查fill波形如果HSE有问题Proteus的仿真日志里通常会有振荡器相关的警告把晶振加上再去掉对比一下就能确认。5.2 症状二一调用HAL_Delay就卡死这个坑上一节已经埋了伏笔。如果在CubeMX里没有改SYS的Timebase Source让HAL库继续用SysTick作为时基同时又打开了FreeRTOS那么生成的代码里SysTick_Handler直接被FreeRTOS占用HAL_IncTick永远得不到调用。后果就是HAL_GetTick永远返回0HAL_Delay内部while循环条件永远不满足程序死在延时函数里。你的任务可能正常运行一两次然后一碰到某个函数里的HAL_Delay就再也回不来。这个问题的根源就是时基冲突。解决办法在CubeMX的SYS设置里把Timebase Source从SysTick改成TIM1重新生成代码后你会发现stm32f1xx_it.c里的SysTick_Handler变成了调用xPortSysTickHandler()而TIM1的中断处理函数负责调用HAL_IncTick。两个中断各司其职HAL_Delay恢复正常。这个坑排查起来其实非常简单但没搞清楚原理时很容易在那里瞎试。5.3 症状三任务跑一会儿就hard fault任务能跑但跑一会儿就进入HardFault_Handler最常见的元凶就是任务栈溢出。FreeRTOS给每个任务单独分配栈空间你在CubeMX里填任务参数时的Stack Size就是它单位是word不是字节。如果默认填了128 words也就是512字节任务里又用了sprintf这类吃栈大户栈很容易越界。前面让在CubeMX里打开configCHECK_FOR_STACK_OVERFLOW就是在这里发挥作用的。栈溢出检测开启后FreeRTOS会在上下文切换时检查栈是否越界一旦发现就调用vApplicationStackOverflowHook。自己动手实现这个钩子把错误状态变得可视化void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { /* 死循环里闪烁错误LED方便在仿真里观察 */ while (1) { HAL_GPIO_TogglePin(ERR_LED_GPIO_Port, ERR_LED_Pin); HAL_Delay(100); } }在仿真里看到错误LED闪烁就知道某个任务栈不够了把对应任务的Stack Size从128翻到256再试。除了这招另一个更精准的手段是调用uxTaskGetStackHighWaterMark它可以查到任务历史最低剩余栈空间/* 在主任务里打印每个任务的栈余量HighWaterMark单位也是word */ printf(led1 hwm: %u\r\n, (unsigned int)uxTaskGetStackHighWaterMark(LED1_TaskHandle));把这个函数放到周期任务里周期性打印观察每个任务栈余量有没有逼近0就可以把一个任务的实际栈需求测出来再反推CubeMX里该配多少。这个接口对排查任务跑着跑着崩了非常有用比猜参数强多了。5.4 在Proteus里怎么抓正在跑哪个任务FreeRTOS在仿真里不像Keil的RTX调试插件那样有现成的任务列表窗口但有几个土办法可以绕。最简单的就是在任务切换点翻转一个GPIO用Proteus左侧工具栏里的虚拟示波器挂上去看波形。比如在LED1_Task里把PB0拉高在LED2_Task里把PB0拉低示波器上能看到方波的上下沿密度就能直观看出两个任务谁占用CPU多。再就是串口日志里故意打出任务名每个任务进入循环体时向UART打印一行标识把波特率调到115200接上虚拟终端任务切换序列一目了然。这个方法只适合调试日志本身会占用额外CPU时间、改变时序但观察调度逻辑足够了。在仿真里养成这个习惯后上真机也照样用。6. 仿真和真实硬件的差距心里要有个数跑通了Proteus上的FreeRTOS不代表嵌入式就入门了但至少把RTOS最核心的调度逻辑、任务通信、内存管理这些抽象概念建立起来了。在仿真里学到的任务要有阻塞点“高优先级会抢占“栈空间要留余量”这些理念拿到真实板子上同样成立。区别在于真实芯片上你会遇到仿真里遇不到的问题中断优先级分组对FreeRTOS临界区的影响、看门狗喂狗跟任务时序的纠缠、DMA中断和任务切换之间的竞态……这些问题都得靠真机调。Proteus的价值是把前面80%的逻辑性错误挡在电脑里让你上真机时只专心对付剩下20%跟硬件强相关的问题。根据我个人的体会最舒服的学习路径是Proteus里把任务调度和队列通信练熟形成直觉然后再买一块开发板把同一个工程烧进去观察真实时序和仿真有多大差异。你在仿真里用uxTaskGetStackHighWaterMark测出来的栈水位和真机上测的基本一致这类经验可以直接迁移。最后再分享一个小技巧FreeRTOS的堆状态可以用xPortGetFreeHeapSize()周期打印堆余量如果持续下降而不是稳定在一个值说明你的系统里存在消息或内存泄漏这个在仿真里暴露得比真机还快值得多用。这个系列到这篇为止仿真环境下的基础外设和RTOS就都覆盖了。下一篇我打算把FreeRTOS和前面写的串口命令解析结合起来做一个能在虚拟终端里动态查看任务状态的小工具那样调试起来会更顺手。如果你正卡在某个和这篇相关的坑上欢迎在评论里把现象发出来我看到会回的。
返回列表