
1. 项目概述与技术选型思路1.1 为什么是STM32G4而不是F1或者F4先说结论如果你手里正好有一块G4系列开发板比如NUCLEO-G474RE或者G431RB拿它入门FreeRTOS完全没问题。G4这颗芯片在STM32家族里属于偏“硬核”的存在主频170MHz带FPU内置数学加速单元还有一堆高级定时器和运放比较器平时多用来做数字电源、电机控制这类对实时性要求很高的场景。但放到我们今天这个“多任务LED控制”的例子里G4其实属于“杀鸡用牛刀”。那为什么还要拿它来写这篇东西因为很多朋友直接上手就是G4板子网上大量教程还停留在F103时代照着F103的步骤在CubeMX里折腾G4经常会在时钟配置、外设型号差异这些地方卡半天。我写这篇就是想告诉你G4跑FreeRTOS跟F1/F4没有本质区别核心思路一通百通但有几个G4专属的坑必须先避开。另外说个题外话G4的Flash在写入32位数据时有些注意事项如果你后面要做Bootloader或者OTA升级会发现G4的Flash编程模型跟F1不太一样。这里不展开做多任务用不上但先给你提个醒别到时候拿着F1的Flash操作代码往G4上直接搬。1.2 五分钟搞定先把工具的坑填平标题里写了“五分钟搞定”这话得加个前提你的CubeMX和Keil环境已经装好并且能正常建工程。很多新手第一次跑这个例程时间全浪费在装软件和环境配置上了。我实际测过CubeMX从官网下载安装包、装好G4的固件包STM32CubeG4再到Keil里装好对应的Device Pack这步快的人十分钟能搞定慢的人能折腾一晚上。卡点基本就两个一是CubeMX下载慢这个只能耐心等或者找靠谱的镜像二是Keil的Pack安装失败常见原因是网络问题或者Keil版本太老。我建议的版本组合是这样的工具推荐版本备注STM32CubeMX6.9以上6.x版本界面差异不大新版本对G4支持更完整STM32CubeG4固件包1.5.x以上在CubeMX里在线安装或者官网手动下载后导入Keil MDK5.30以上太低版本对G4支持有问题特别是CMSIS版本太老STM32G4 Device Pack最新版Keil Pack Installer里安装确保能识别G474/G431注意Keil安装和注册授权的问题属于商业软件范畴这里不做讨论你只要保证自己的开发环境是合法可用状态就行。我只说技术路线。2. CubeMX工程配置细节与参数选择2.1 新建工程时的几个关键选项打开CubeMX选择芯片型号。如果你用的是NUCLEO-G474RE直接搜“G474RE”就行。这里有两个容易踩的坑第一个是芯片封装和型号后缀。G474RE和G474RET6是同一个东西但CubeMX搜索时输入关键词要准确别选成G474RC或者G473引脚数量不同后面配外设时会发现管脚对不上。我见过不止一个朋友在这个环节就选错了片子后面全部白搭。第二个是时钟源选择。新建工程后会弹出“Initialize all peripherals with their default mode”之类的提示先别管。第一步去System Core - RCC把HSE高速外部时钟设为Crystal/Ceramic Resonator。如果你用的是NUCLEO板板载ST-Link上带了一个24MHz的晶振给芯片做HSE输入但注意G4的HSE最大支持24MHz而很多F1板子上的HSE是8MHz核对了再配。然后你可能会想直接用手头板子自带的内部时钟不行吗可以HSI1616MHz内部RC也能让系统跑起来但后面你要是用到USB、定时器做精确延时这类功能内部RC的精度不够建议从一开始就养成用外部晶振的习惯。2.2 时钟树配置核心频率倍频到170MHz时钟树是CubeMX里最容易被跳过但又最重要的一步。G4系列最高主频170MHz默认情况下系统时钟可能是16MHzHSI16直通你不去动时钟树系统跑起来也能点灯但性能完全发挥不出来。我的配置参数供参考HSE24MHzNUCLEO板载晶振PLLM除以624 / 6 4MHzPLLN乘以854 x 85 340MHzPLLP除以2340 / 2 170MHz系统时钟源选择PLLCLK最后在Clock Configuration页面里能看到CPU Clock显示为170MHz那就对了。这个倍频计算逻辑我解释一下G4的PLL输入范围要求1~16MHz所以24MHz的HSE先要降下来PLLM6得到4MHz满足输入范围PLLN可以在8~128之间选我们选85得到340MHz的VCO输出最后PLLP分频到170MHz。这些都是G4参考手册里的硬性参数CubeMX会在你配置时自动校验合法性如果填了非法值它会提示红色警告。有些人喜欢直接把PLLN填一个很大的数字让频率飙上去千万别这么做。G4的VCO输出限制是128~512MHz超出范围轻则系统不稳定重则芯片直接不启动。2.3 GPIO配置三个LED三个引脚今天的实验是三路LED独立闪烁控制我用的是NUCLEO-G474RE板载的三个LEDLED引脚说明LED1绿色PA5板载低电平点亮LED2黄色PB13板载低电平点亮LED3红色PB14板载低电平点亮如果你手里是别的板子或者你想外接LED记住一个原则通过一个限流电阻330Ω到1kΩ把LED接到GPIO上。STM32的GPIO推挽输出能力大概在20mA左右直接驱动LED没问题但不加电阻会烧LED甚至损伤引脚。在CubeMX里把PA5、PB13、PB14都配置为GPIO_Output初始电平设为High为什么因为板载LED是低电平点亮初始拉高即熄灭避免上电瞬间LED全亮。这里有个细节很多人不管GPIO的Maximum output speed。LED控制对这种低速信号来说Low或者Medium都行但如果你选的引脚同时要复用其他高速功能比如定时器PWM速度等级会影响信号的上升沿质量。今天这个实验无脑选Low就好省得引入信号完整性问题。2.4 中间件选择开启FreeRTOS在Middleware and Software Packs里找到FREERTOSInterface选CMSIS_V1还是CMSIS_V2这个纠结了很多新手我直接给答案新工程一律选CMSIS_V2。V2是ARM官方的CMSIS-RTOS2标准封装更清晰CubeMX生成的代码更规范而且Keil的RTX5调试插件支持也更好。V1是老的CMSIS-RTOS v1标准兼容老代码用新项目没必要。然后下面的Configuration页有几个参数值得说一下Color for Memory不用管CubeMX生成的一个标记。USE_NEWLIB_REENTRANT如果你的代码里用了printf、malloc这类C库函数建议Enable。我今天这个例程只用HAL库和FreeRTOS API不需要开。但如果开了会占用更多RAM不开又用printf容易出问题具体项目按需来。TOTAL_HEAP_SIZE默认给的是3072字节太小了。我建议直接改到81928KB以上。G4的SRAM有128KB给FreeRTOS堆8KB完全没压力。堆太小会导致创建任务失败这是新手最容易碰到的问题之一后面我在问题排查里细说。USE_PREEMPTION抢占式调度保持Enable。抢占式调度是多任务实时性的基础不开启的话任务切换要靠主动让出CPULED闪烁这种低优先级任务没问题但体现不出FreeRTOS的价值。USE_TIME_SLICING时间片调度Enable这样同优先级任务可以轮流执行。USE_MUTEXES这次例程用不到但建议勾上。反正编译进去不占什么资源以后用得上。2.5 任务配置三个任务三种节奏在Tasks and Queues页面里添加任务我建了三个任务名优先级栈大小Words功能TaskLED1Normal1128控制LED1间隔200ms翻转TaskLED2Normal1128控制LED2间隔500ms翻转TaskLED3Normal1128控制LED3间隔1000ms翻转这三个任务逻辑一样就是不同的延时节奏这样LED1快速闪烁、LED2中速、LED3慢速视觉上很直观地体现多任务并行。优先级我故意设成一样方便演示时间片轮转的效果。如果你想看抢占式调度的效果可以把某个任务优先级调高它就会先执行调度顺序差别肉眼可见。栈大小128个字等于512字节对LED这么简单的任务绰绰有余。但注意CubeMX这里默认可能是128如果你任务里要定义大数组或者调用printf之类会消耗栈空间的函数就要加大。栈溢出是FreeRTOS最常见的坑后面我详细说怎么排查。3. 代码逻辑分析与关键函数解读3.1 CubeMX生成了什么代码哪些能碰哪些不能碰CubeMX配置完点击GENERATE CODE它会生成一个MDK-ARM工程。打开Keil编译如果你前面的配置没问题一次能过。这时候你打开main.c会发现代码结构跟手写的工程不太一样。核心逻辑在三个地方main()函数硬件初始化然后调用osKernelStart()启动FreeRTOS调度器。注意main函数里看不到我们自己的业务逻辑业务逻辑都在任务函数里。app_freertos.c这是FreeRTOS相关代码的大本营CubeMX生成的默认任务函数在MX_FREERTOS_Init()里创建然后调用StartDefaultTask()。任务函数CubeMX默认只会生成一个StartDefaultTask示例任务我们自己的三个任务要自己补全。这里有个新手经常犯的错误直接去改main.c里CubeMX生成的初始化代码或者改app_freertos.c里那些标记为“USER CODE BEGIN/END”之外的区域。结果下次从CubeMX重新生成代码手动改的内容被全部覆盖又得重新弄。正确做法是你自己的代码都写在USER CODE段里面或者干脆新建一个.c文件放自己的任务代码CubeMX生成的文件不要去动它。3.2 手写任务代码vTaskDelay与HAL_Delay的混用陷阱在主循环或者任务函数里最核心的延时函数有两个HAL_Delay()和vTaskDelay()。这两个函数功能相似但在FreeRTOS环境里用起来有本质区别。HAL_Delay()是死等CPU在那空转。vTaskDelay()是把当前任务阻塞让出CPU给其他任务。听上去vTaskDelay更好对吧但问题来了CubeMX生成的HAL库代码内部比如某些外设驱动里调用的是HAL_Delay如果你在任务里也用HAL_Delay这会导致调度器没办法在延时期间切换任务多任务就变成伪并发了。还有个更严重的问题如果某个中断里调用了HAL_Delay而中断优先级高于FreeRTOS管理的最高优先级configMAX_SYSCALL_INTERRUPT_PRIORITY系统会直接卡死或者跑飞。具体原因涉及FreeRTOS对中断安全API的限定简单说就是低优先级中断里可以调用FreeRTOS的API但高优先级中断里不行HAL_Delay内部用了SysTick冲突概率极高。所以今天我们的三个任务函数统一用osDelay()或者vTaskDelay()void TaskLED1(void *argument) { for(;;) { HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); osDelay(200); } } void TaskLED2(void *argument) { for(;;) { HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin); osDelay(500); } } void TaskLED3(void *argument) { for(;;) { HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin); osDelay(1000); } }HAL_GPIO_TogglePin用来翻转引脚电平比分别调用SetPin和ResetPin省事。如果你的板子是低电平点亮翻转操作天然就兼容因为任何一次翻转都是从亮到灭或者从灭到亮。注意在CMSIS_V2接口下CubeMX生成的代码用的是osDelay()而不是直接调vTaskDelay()二者本质一样osDelay内部就是调用vTaskDelay。我建议你直接使用CubeMX生成的这套CMSIS接口好处是以后你把代码移植到其他RTOS时也能少改几行。3.3 任务创建函数的返回值判断免费的调试信息不要浪费在app_freertos.c里任务是通过osThreadNew()创建的LED1TaskHandle osThreadNew(TaskLED1, NULL, LED1Task_attributes); if (LED1TaskHandle NULL) { Error_Handler(); }CubeMX默认生成的代码已经带上了对返回值的简单判断如果任务创建失败就进Error_Handler。但Error_Handler里通常是死循环你并不知道失败原因是什么。我建议你把它改一下把失败的具体信息打印出来或者设置一个断点方便调试LED1TaskHandle osThreadNew(TaskLED1, NULL, LED1Task_attributes); if (LED1TaskHandle NULL) { /* 任务创建失败常见原因堆内存不足 */ __BKPT(0); // 停下来看调用栈 }任务创建失败最典型的原因就是TOTAL_HEAP_SIZE太小。我之前测试过如果你一个任务栈开512字2KB再开几个任务默认的3072字节堆肯定不够。把堆调到8KB以上基本就没这个问题了。不要把printf那套串口调试一开始就引进来先把逻辑跑通再说。多任务系统出问题时用调试器看变量和调用栈的效率远高于串口打印。4. 编译烧录与调试实操4.1 Keil工程选项里必须检查的三个配置CubeMX生成完工程打开Keil不要急着编译。先花30秒检查工程选项魔术棒图标Device页确认芯片型号正确最常见的就是G474RE识别成G474RC或者别的影响不大但代码运行可能有未知问题。Target页确认ARM Compiler版本建议用V6.x默认可能是V5.06V6编译出来代码更紧凑但对老代码的兼容性查一些CubeMX生成的代码在V6下编译没有压力。Debug页确认调试器选的是ST-Link并点击右边的Settings如果能看到芯片ID说明ST-Link连接正常。这三个检查点每个都能救命。特别是Debug页很多朋友板子插上电脑但Keil报“No Target Connected”十有八九是这里没选对调试器或者驱动没装好。4.2 编译报错的常见类型与处理思路CubeMX生成的工程理论上不该有编译错误但实际操作中总有些意外。我见过的几种典型报错错误一Error: L6218E: Undefined symbol这个一般是CubeMX生成的代码依赖某个库文件没被加进工程或者是代码中某个函数声明了但没实现。不用急着改代码先看Keil的Build Output窗口找到具体是哪个符号未定义再去搜索它应该从哪来。如果是HAL库的函数那多半是某个外设的源文件没勾选。CubeMX在Project Manager - Code Generator里有个“Add necessary library files as reference in the toolchain”选项确保它是勾选状态。错误二Error: Q0147E: Failed to create directory .\obj\...这个报错我之前还真遇到过就是工程路径有问题。Keil默认会把中间文件放到.\obj目录如果你的工程路径里有中文或者特殊字符或者目录权限不对就会创建失败。解决方法是把整个工程移到纯英文路径下再打开。错误三warning: #1-D: last line of file ends without a newline这个是警告不是错误但提醒你编译器版本对代码格式比较敏感在源码最后一行加个回车就行。CubeMX生成的代码一般不会有这个问题但你自己手敲的任务函数文件容易犯。4.3 烧录运行第一次看到三个LED各自闪烁编译通过后点击LOAD按钮烧录。如果一切顺利板子上三个LED会以200ms、500ms、1000ms的节奏各自闪烁互不干扰。第一次运行看到这个效果说明FreeRTOS已经在你的G4上跑起来了。这时候你可以做个实验加深理解在TaskLED1的while循环里加个延时比如osDelay(1000)你会发现LED1的闪烁频率马上改变了但另外两个任务不受任何影响。这就是多任务并发的意义——每个任务独立运行有自己的时间节奏。如果烧录后板子没反应或者LED状态不对先别慌大概率不是代码逻辑问题而是硬件配置问题。比如GPIO初始电平设置反了板载LED低电平点亮你初始设成了Low那上电就等于全亮或者时钟配置错了导致系统跑不起来。5. 常见问题排查与避坑技巧5.1 任务没有正常运行或系统卡死这个现象分两种一种是三个灯都不亮一种是有灯亮但不闪。三个灯都不亮大概率是FreeRTOS调度器没启动成功。检查方式在osKernelStart()前后各加一个断点如果执行不到osKernelStart()之后的代码看看是不是卡在某个硬件初始化里比如HAL_Init、SystemClock_Config。如果执行到了但任务还没跑检查堆大小是否足够创建所有任务。有灯亮但不闪说明某个任务创建成功了但卡在了里面。最常见的原因是任务函数里用了while(1)死循环但没有调用任何阻塞函数延时、等待信号量等低优先级任务永远占着CPU其他任务抢不到。FreeRTOS的抢占式调度虽然能让高优先级任务抢占低优先级任务但如果所有任务优先级一样谁先运行谁就占着CPU不放。用调试器暂停运行然后在Keil的Call Stack窗口看看CPU当前停在哪个函数里一查一个准。5.2 栈溢出检测打开之后还是找不到爆栈点FreeRTOS提供了两套栈溢出检测机制在FreeRTOSConfig.h里配置#define configCHECK_FOR_STACK_OVERFLOW 2设成1表示只在任务切换时检查栈指针是否溢出设成2表示除了检查栈指针还会检查栈末尾的临界值canary。设成2更可靠但会略微增加系统开销。当栈溢出被检测到默认会调用vApplicationStackOverflowHook()这个钩子函数CubeMX会帮你生成一个弱定义你可以在这里加个断点或者点亮一个错误LED。不过说实话栈溢出检测是“事后诸葛亮”更实用的方法是在设计阶段就估算好栈大小。经验法则一个简单的任务调几个函数、用GPIO翻转128字完全够任务里如果定义了一个512字节的数组栈就得加到至少256字调printf这类库函数建议至少512字。5.3 Keil调试时怎么查看任务运行状态和变量值热搜词里有朋友问到“keil调试助手里面的debug模式如何显示结构体变量”这里我顺便讲一下。在Keil的Debug模式下Watch窗口可以查看变量的实时值。如果你要查看FreeRTOS内部的结构体比如任务控制块TCB直接在Watch窗口输入pxCurrentTCB或者pxReadyTasksLists然后展开结构体就能看到每个任务的栈指针、优先级、状态等信息。更直观的方式是使用Keil的RTOS插件Debug - RTX RTOS - System Viewer它能以图形化方式显示当前所有任务的状态运行、就绪、阻塞等。前提是你用的是CMSIS_V2接口并且Keil能识别到RTOS内核。这个功能对多任务调试很有用当你的系统“看起来卡死了”打开System Viewer一看哪个任务处于Running状态哪个任务在阻塞等待一目了然。比盲猜快得多。5.4 我在实际操作中踩过的几个坑说实话光这一节写两千字都不够我只挑几个对新手最致命的坑一CubeMX生成的工程在Keil里编译没有勾选“Use MicroLIB”。这会导致你用printf的时候程序卡死或输出乱码。解决在Keil魔法棒 - Target页勾选Use MicroLIB。这个坑成了很多新手调串口时的噩梦如果碰到串口输出异常先检查这一项。坑二G4的PA5引脚冲突。NUCLEO-G474RE板子上PA5是蓝色用户按键或者某个传感器的引脚不同的板子引脚功能定义不一样。别以为所有NUCLEO板的PA5都接LED看原理图最靠谱。坑三调试器下载报错“RDDI-DAP Error”。这通常是芯片处于低功耗模式或者调试口被复用。G4在配置了某些休眠模式后ST-Link连不上芯片这时候按住板子上的RESET键再点下载时机对就能连上。坑四GPIO初始电平导致LED上电瞬间闪一下。CubeMX里GPIO初始电平默认是Low如果你的LED是低电平点亮那上电瞬间LED会亮一下然后进入程序控制逻辑视觉效果很不好。解决办法就是把初始电平设置为High。6. 从点灯到进阶还能往哪个方向扩展三路LED闪烁只是FreeRTOS多任务的最小示例但背后这套“创建任务-设置优先级-分配栈空间-任务间通信”的思维模型是通用的。你可以在G4上继续扩展这些内容用信号量同步任务一个任务产生事件另一个任务等待事件实现生产者消费者模式。用消息队列传递数据比如ADC采集任务把数据放进队列LCD显示任务从队列取数据刷新屏幕。用软件定时器做周期任务FreeRTOS的软件定时器基于Tick实现比任务延时的方案更适合执行周期性的后台任务。增加任务数量比如加入按键扫描、OLED显示、传感器读取等每个独立任务观察它们之间如何抢占CPU。如果你想让系统更复杂一点可以试着把LED的闪烁时间不用固定延时而是用一个全局变量来控制再开个串口指令解析任务通过串口发送命令实时改变LED闪烁频率。这样你的G4就从“点灯板”变成了一个实打实的“RTOS控制系统”。我的建议是在G4上把FreeRTOS的核心API都过一遍——任务管理、信号量、互斥锁、消息队列、事件组、软件定时器——这套东西吃透了你将来到哪个平台都是无缝迁移。今天的LED多任务只是热身。