STM32+FreeRTOS入门:任务调度、通信与中断处理实战
STM32+FreeRTOS 是嵌入式开发里最常见的一套组合,也是很多从裸机转向实时操作系统时最先接触的方案。很多初学者以为它很难,其实真正要过的坎只有三个:会建工程、理解任务调度、会用任务之间同步和通信的机制。这篇文章就把这三个坎拆开揉碎,从环境搭建到常见报错一次讲清楚。它适合刚用 STM32 做过裸机小项目的开发者,也适合学过一点 FreeRTOS 但总觉得概念没有落到代码上的朋友。
先说结论:STM32+FreeRTOS 没那么神秘。它解决的核心问题是让多个功能模块“看起来同时在跑”,并且能够按优先级响应紧急事件。下面我按自己的实际学习路径,从裸机痛点、环境搭建、任务创建、任务通信、中断处理、调试排查到学习路线,完整过一遍。
1. 先想清楚:裸机已经够用,为什么还要引入 FreeRTOS
1.1 裸机超级循环的真正痛点
多数人学 STM32 时都写过类似的代码:
这个“超级大循环”初学者会觉得简单,可一旦项目复杂起来就很麻烦。假设 read_sensor() 里有一段等待传感器响应,用了 200ms,那么这一轮循环里按键、显示、串口消息全都卡住 200ms。如果串口同时还收到了一条紧急控制指令,程序根本没有机会立刻处理它。
我见过很多初学者的项目卡在这个阶段:功能明明在裸机下也能跑,但一旦把液晶屏显示、按键扫描、传感器采集、串口通信、电机控制全部叠在一起,整个程序就变得越来越难改。加一个功能,很容易影响另一个功能的时序。
也有人说可以用事件驱动来解决,把耗时操作拆成状态机。这确实是一条路,很多裸机项目也确实靠状态机做到了不错的实时性。但状态机需要自己设计状态、事件、过渡关系,对新手来说维护成本不低。
1.2 RTOS 到底解决了什么问题
FreeRTOS 做的事情很直接:把不同功能拆成独立任务,每个任务都有自己的函数、栈和优先级。调度器负责决定现在该运行哪个任务。
它带来的实际好处是这三个:
- 实时性更好。高优先级任务一旦就绪,就能抢占当前低优先级任务,快速响应。
- 代码更好维护。每个功能独立成一个任务,写起来更像“我自己单独处理这一路”。
- 模块之间更好解耦。任务之间通过队列、信号量交换数据,而不是靠全局变量满天飞。
但这里要泼一盆冷水:不是所有项目都必须上 RTOS。如果只是控制一盏灯、读一个按键、逻辑简单到不超过两个状态,裸机反而更好。RTOS 会增加调度、内存管理、任务切换的开销,也会引入很多新的坑。真正适合上 RTOS 的项目,通常是多外设同时工作、通信协议复杂、有多个实时处理需求的项目。
1.3 为什么入门选 STM32+FreeRTOS
STM32 芯片资源丰富、资料多、开发板便宜,遇到问题基本都能找到案例。FreeRTOS 则是免费开源、文档完善、被 ST 官方深度适配,最方便的是 STM32CubeMX 可以直接生成 FreeRTOS 基础工程,省掉手写移植的麻烦。
选这套组合的另一个原因是生态。很多经典的嵌入式方案,比如 Modbus 通信、LCD 界面、网络协议栈、文件系统,都能在 STM32+FreeRTOS 上跑通。学会了这套组合,后续往嵌入式 Linux 或者更复杂的多任务方向走,思维上是顺的。
2. 环境搭建:用 CubeMX 生成第一个 FreeRTOS 工程
2.1 工具清单和安装要点
我建议准备这几样东西:
- 一块 STM32 开发板,比如 STM32F103C8T6 最小系统板
- STM32CubeMX,用于图形化配置引脚、时钟和中间件
- STM32CubeIDE 或者 Keil MDK,用于编译、下载、调试
- ST-Link 下载器或者板载 ST-Link
- 一个 USB 转串口模块,方便看日志
很多人卡在第一步:CubeMX 下载芯片支持包很慢,或者下载失败。这个其实不用硬等联网。你可以直接从 ST 官网下载对应芯片的固件包,放到 CubeMX 的 repository 目录里,然后在 CubeMX 的 Manage embedded software packages 里手动导入。F1 系列通常一个包就够用,选的时候认准具体芯片型号。
另一个容易忽略的点是 Keil 的许可证和编译器版本。Keil MDK 默认安装后需要注册许可证,否则编译到一定代码量就会受限。如果是学习用途,建议先用 STM32CubeIDE,它免费、不需要额外破解、调试功能也够用。
2.2 新建工程时的关键配置
下面以 STM32F103C8T6 为例,走一遍 CubeMX 的关键配置。这里每一条都有实际原因。
第一步,先选芯片型号,不要选错了封装。建议直接在 Part Number 搜索框输入 STM32F103C8T6,确认是 48 引脚、64KB Flash、20KB RAM 的型号。
第二步,进入 Pinout & Configuration 视图。先找到 SYS,把 Debug 设置为 Serial Wire。这一步非常关键,如果不选,芯片的下载引脚可能被复用成普通 GPIO,会导致程序第一次烧进去之后第二次就连接不上了。新手最常见的问题就是从这里开始的。
第三步,配置时钟。在 Clock Configuration 页面里,把 HSE 设置为 Crystal/Ceramic Resonator,然后在右侧把 HCLK 拉到 72MHz。系统会自动帮你算 PLL 的分频倍频。F103 的最高主频是 72MHz,超过这个值芯片跑不稳定。
第四步,配置一个 LED 引脚。比如把 PC13 设置为 GPIO Output,这种引脚在不少开发板上接了一个 LED。也可以换成 PA1、PB1,具体以你的板子为准。
第五步,添加 FreeRTOS。在 Middleware and Software Packs 里找到 FREERTOS,Interface 选择 CMSIS_V2。这里解释一下:CMSIS_V2 是 ST 官方封装的 CMSIS-RTOS2 接口,里面也包含了原生 FreeRTOS。两种写法都能用。新手直接选 CMSIS_V2,生成的代码里既有类似 osThreadNew 的封装,也可以直接调用 xTaskCreate 等原生 API。
接着在 Tasks and Queues 里可以看到一个默认的 defaultTask。它的优先级默认是 osPriorityNormal,栈大小默认 128。先保留,后面会改。
第六步,回到 Project Manager 页面,设置工程名和路径,在 Toolchain 里选择 MDK-ARM 或 STM32CubeIDE。我习惯先用 STM32CubeIDE,因为能看到图形化的调试寄存器,排查 HardFault 比较方便。
最后点 GENERATE CODE,生成工程。
2.3 第一个 LED 任务:代码剖析
生成完工程后,打开 freertos.c,会看到 CubeMX 自动创建一个任务入口函数:
如果你用原生 FreeRTOS API,代码是这样:
这里有两个重点。
第一,任务函数永远不要 return。如果任务函数返回了,FreeRTOS 会认为这个任务结束了,先执行 vTaskDelete(NULL) 自我清理。如果你希望任务一直循环,就必须用 for(;;) 包住。
第二,vTaskDelay 的参数单位是 tick,不是毫秒。在 CubeMX 生成的工程里,默认 configTICK_RATE_HZ 通常是 1000,也就是 1 个 tick 等于 1ms。建议用 pdMS_TO_TICKS(500) 这种写法,避免以后改 tick 频率时把延时弄错。
烧写之后如果看到 LED 以 1 秒间隔闪烁,说明从头到任务创建整个链路已经通了。这是第一个验收节点。
2.4 用 Log 验证任务是否真的在跑
只看 LED 闪烁还不够,因为裸机也能闪烁。建议顺便输出串口日志:
- 先初始化 UART1,重定向
printf。 - 在任务循环里打印任务信息和系统 tick。
如果你用的是 CubeMX,注意每个任务的栈大小默认是 128 个字,对 F103 来说就是 128 × 4 = 512 字节。如果任务函数里用了 printf,这个栈可能会不够,程序跑起来容易异常。遇到这种情况,先把 defaultTask 的栈调到 256 或 512,再试。这不是玄学,而是真实项目里最常遇到的一类问题。
3. 把任务和调度拆明白:优先级、状态、延时
3.1 xTaskCreate 参数逐个解释
创建任务常用的是原生 API:
pxTask:任务函数指针,形式是void task(void *arg)。pcName:任务名,只用于调试和统计,不影响调度。usStackDepth:栈深度,单位是字(word),不是字节。在 Cortex-M3/M4 上,1 字等于 4 字节。pvParameters:传给任务函数的参数。uxPriority:优先级,数字越大优先级越高。0 是空闲任务用的最低优先级。pxCreatedTask:任务句柄,可以用来删除、挂起、恢复这个任务。
建议每个任务都用句柄存下来,以后排查和动态管理会方便很多。如果不存句柄,删除任务只能靠任务自身调用 vTaskDelete(NULL) 或者从某个任务引用另一个任务,非常别扭。
3.2 抢占式调度和任务状态
FreeRTOS 在默认配置下是抢占式调度。意思是只要高优先级任务进入就绪状态,它会立刻打断当前运行的低优先级任务,让出 CPU 给高优先级任务。
任务在系统中总共有几种状态:
- 运行态:当前正在使用 CPU。
- 就绪态:已经可以运行,但要等调度器分配 CPU。
- 阻塞态:任务因为阻塞在延时、队列、信号量上,暂时无法运行。
- 挂起态:通过
vTaskSuspend主动挂起,只有调用vTaskResume才会回到就绪态。
新手最容易不理解的一个点是:调用 vTaskDelay 后任务进入的是阻塞态,不是挂起态。阻塞态会在延时结束后自动回到就绪态,而挂起态必须被外部恢复。
同优先级的任务会按时间片轮转。假设两个任务优先级相同,每个任务运行一个 tick 后,调度器就会切换到另一个。这个时间片长度由 configTICK_RATE_HZ 决定。
3.3 为什么不能用 HAL_Delay
很多人刚开始会把裸机里的 HAL_Delay 直接搬到 FreeRTOS 任务里。它能跑,但性质完全不同。
HAL_Delay 是忙等,也就是让 CPU 一直在那里空转。在裸机里这没有多大问题,但在 RTOS 里,一个任务占用 CPU 空转,等于让其他所有任务都失去调度机会,实时性完全被破坏。最明显的影响是:其他高优先级任务无法及时响应。
vTaskDelay 是把当前任务主动从运行态切到阻塞态,让出 CPU,同时告诉调度器“过了多少 tick 再叫醒我”。这样别的任务就能利用这段时间运行。
低功耗场景下区别更大。任务阻塞的时候,MCU 可以进入更深的睡眠模式,而忙等会一直维持高频时钟和供电,功耗差别非常大。
3.4 空闲任务和优先级设计
FreeRTOS 启动后会自动创建一个空闲任务,优先级是 0。它是系统中最低优先级的任务,平时没事干才轮得到它。空闲任务很重要,它负责回收被删除的任务的资源。
如果你发现某个任务始终不执行,先检查优先级是不是设计成了这样:高优先级任务一直在就绪或运行,低优先级任务永远抢不到 CPU。这就是“任务饿死”。
设计任务优先级时,我一般遵守几条经验:
- 优先级不要铺得太满,尽量少用很高的数字。
- 实时性要求高的任务,比如通信接收、采集和响应,放高优先级。
- 耗时长的业务逻辑,比如数据处理、界面刷新,放低优先级。
- 任务里不要再做超长忙等,否则高优先级形同虚设。
4. 任务之间的通信:队列、信号量和互斥量
4.1 什么时候需要通信
任务拆完之后,任务和任务之间不可能完全没关系。最典型的三类场景是:
- 一个任务采集传感器数据,另一个任务负责发送数据。
- 一个任务等待外部事件,比如按键按下了,通知另一个任务执行。
- 多个任务都要访问同一个外设或临界资源,比如同一个串口、同一个显示器。
如果这时候还用裸机那套“全局变量 + 标志位”的做法,在 FreeRTOS 里会越来越难维护。全局变量无法解决任务间的时序同步,也容易产生数据不一致。FreeRTOS 为此提供了标准机制:队列、信号量、互斥量、事件组等。
4.2 队列:任务之间的数据管道
队列本质上是一个环形缓冲区,生产者往队列里发数据,消费者从队列里取数据。常用 API 是:
创建队列时,uxItemLength 是队列能容纳几条消息,uxItemSize 是每条消息的大小,单位是字节。需要注意的是,队列发送是复制数据,不是传递指针。如果你发一个结构体,这个结构体会整体复制到队列内部缓冲区里。所以消息不要设计得太大,否则会占用大量 RAM。
下面是一个简单的生产者-消费者示例:
发送阻塞时间参数的含义是:如果队列满了,最多等多久。接收阻塞时间参数的含义是:如果队列为空,最多等多久。实际项目里,发送阻塞时间一般不设置太长,因为队列满往往意味着消费端处理不过来,这时候阻塞太久只会拖累生产者。
4.3 信号量:事件通知和任务同步
信号量有两种常见用法。
二进制信号量适合做事件通知。比如串口收到一帧数据,任务在信号量上等待,ISR 里释放信号量,任务被唤醒并处理数据。这是中断与任务通信最经典的模式。
计数信号量适合表示有 N 个事件待处理。比如消息队列一次来了多条,每处理一条,计数减一。
常用 API:
二进制信号量创建后,默认处于未释放状态。第一次 xSemaphoreTake 会阻塞,直到有人 xSemaphoreGive 或者信号量释放。
注意一点:二进制信号量没有“积累效应”。如果 ISR 在任务还没取走时连续给了两次信号量,第二次可能会被丢掉。如果需要记录多次事件,用计数信号量或队列更合适。
4.4 互斥量和优先级反转
互斥量看起来和二进制信号量很像,但用途完全不一样。互斥量是用来保护共享资源访问的,并且它带优先级继承机制。
什么叫优先级反转?简单说就是:高优先级任务在等一个低优先级任务释放资源,而中优先级任务先抢占了低优先级任务的 CPU,导致高优先级任务反而无法执行。互斥量的优先级继承机制,会让低优先级任务在持有互斥量的这段时间临时提高优先级,避免被中优先级任务插队,从而缓解反转问题。
使用互斥量的典型写法:
这里要提醒几个点:
- 互斥量只能在任务中使用,不能在 ISR 中使用。
- 临界资源保护期间,代码要尽量短,不要占用太长时间。
- 加锁和解锁必须成对出现。如果某个分支提前 return 忘了解锁,其他任务会永久卡死。
5. 中断处理:如何在 ISR 中安全地和任务互动
5.1 中断为什么不能随便调用 FreeRTOS API
FreeRTOS 有一套专门给中断使用的带 FromISR 后缀的 API。核心原因是普通 API 需要切换任务调度器、修改内核链表,如果在中断里调用,可能破坏中断上下文,导致系统不可重入。
所以最简单的规则就是:中断里如果要和 RTOS 任务通信,必须使用带 FromISR 的函数,比如 xQueueSendFromISR、xSemaphoreGiveFromISR。如果中断里只需要置个标志,任务里再轮询这个标志,也可以,但实时性会降低。
中断里不要做耗时处理。常规做法是:ISR 里只接收数据、释放信号量或发消息,然后立即退出。具体的数据解析、协议处理、业务逻辑全部放到任务里。
5.2 串口空闲中断:不定长数据接收的常用方案
STM32 的串口接收最常用的是 HAL 库的 HAL_UART_Receive_IT,它每收到一个字节就触发一次回调。这种方式能处理固定长度数据,但处理不定长数据会比较麻烦。更好的方案是配合空闲中断一起使用。
实现思路是:
- 开启串口接收中断。
- 在中断回调里把当前收到的数据存到接收缓冲区。
- 当检测到串口空闲中断时,说明一帧数据接收完毕。
- 在空闲中断回调里释放一个信号量,通知任务处理这帧数据。
- 任务收到信号量后,从缓冲区取数据并解析。
用 HAL 库做空闲中断时,可以启用 HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE),然后配合空闲中断 DMA 的方式。简单点说,CubeMX 里开启 UART 全局中断后,可以在 UART 中断回调中判断 __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) 是否被置位,然后把数据送到 RTOS 队列里。
实际测试时最常遇到的问题是:空闲中断回调里调用 HAL_UART_IRQHandler 的流程写错,导致接收一次后就不再触发。常见的做法是在空闲中断里先执行 __HAL_UART_CLEAR_IDLEFLAG(&huart1),再把 rx_len 记录下来,并调用 HAL_UART_Receive_DMA 重新开始接收。只要把“接收完成 + 信号量通知 + 任务处理”这条链路理顺,几乎所有的串口协议解析都能套用。
5.3 临界区和调度器挂起
临界区的接口是:
进入临界区后,内核会关掉中断,直到退出临界区才恢复。它适合保护一段很短、必须原子执行的代码,比如修改一个被多个任务共享的变量。
但临界区不能随便用。临界区内相当于短暂关闭了系统中断,所以会影响实时性。如果代码段太长,中断响应会明显变慢,调试时还很难发现。
另一种是挂起调度器:
挂起调度器不关中断,它只是禁止任务切换。适合需要临时保证一组操作不会被其他任务打断的场合。
新手经常混淆这两个机制。简单记:临界区适合保护极小段代码,调度器挂起适合保护稍长一点的代码段。它们都不能解决“多个任务同时访问共享数据”的问题,只能保证不被打断,最终还是要配合队列或者互斥量才能把数据安全地传递出去。
6. 排查与调试:堆栈溢出、HardFault 和常见坑
6.1 堆栈大小怎么估
任务栈是 FreeRTOS 项目里最容易出问题的资源。栈大小就是任务函数使用的局部变量、函数调用、中断嵌套的总开销。
CubeMX 默认给每个任务 128 字,也就是 512 字节。这个值对大部分简单任务勉强够用,但一旦任务里用了比较大的局部数组、printf、文件系统或者复杂的协议栈,就会不够。
我一般会这样估:
- 简单任务,比如翻转 GPIO、延时循环,128 到 256 字。
- 用到串口打印的任务,256 到 512 字。
- 用到多个库函数、结构体、较大缓冲区的任务,512 到 1024 字。
- 极少数大任务,比如文件系统读写,可能需要 1024 字以上。
但估算只是起点。真正靠谱的方式是运行时监测栈余量。
6.2 堆栈溢出检测方法
FreeRTOS 提供了两种堆栈溢出检测机制。在 FreeRTOSConfig.h 中配置 configCHECK_FOR_STACK_OVERFLOW:
- 设置为 1,在任务切换时检测一次,比较粗糙。
- 设置为 2,在任务切换时和任务运行时都检测,更可靠,但会略微增加系统开销。
同时需要实现钩子函数:
运行时可以用 uxTaskGetStackHighWaterMark 查看任务栈到底剩了多少:
这个函数返回的是历史最低剩余值,也就是栈余量的“水位线”。如果这个值接近 0,说明栈快用完了,要立刻调大。我每次写完一个任务都会把这个信息打印出来,跑一段时间后确认余量足够,再删掉调试代码。
6.3 HardFault 调试思路
HardFault 是所有 STM32 开发者都会遇到的异常。它本身不是一个“错误原因”,而是一堆错误的最终表现。常见原因有:
- 空指针或非法访问。
- 数组越界,写坏了栈或者堆。
- 任务栈溢出,破坏了函数调用栈。
- 非法外设访问,比如时钟没开启就访问外设寄存器。
调试 HardFault 的第一步不是翻代码,而是先看调试器给出的现场信息。在 STM32CubeIDE 里停下程序后,打开 Registers 窗口,查找这几项:
PC和LR,直接能看到出错时执行到哪条指令。MSP或PSP,看看当前线程在用什么栈。CFSR寄存器,能把错误类型进一步拆分,比如总线错误、使用错误、内存管理错误。
拿到 PC 值后,回退到 Disassembly 窗口,看 PC 附近的反汇编代码,再对应到 C 源码行。这个过程看起来麻烦,但比瞎猜快很多。
排查顺序建议是:
- 先看 PC 和 LR。
- 确认是不是栈溢出,打开堆栈溢出检测并看剩余栈量。
- 检查任务栈大小和局部数组大小。
- 检查中断优先级配置。
- 检查外设时钟是否打开。
- 如果还查不出,用二分法屏蔽任务,缩小范围。
6.4 中断优先级和 FreeRTOS 的一个经典坑
Cortex-M 的优先级数值和优先级大小是反的。数值越小,优先级越高。FreeRTOS 里有一个宏 configMAX_SYSCALL_INTERRUPT_PRIORITY,它表示“能调用 FreeRTOS API 的最高中断优先级”。
在 STM32 上,CubeMX 默认的 configMAX_SYSCALL_INTERRUPT_PRIORITY 通常是 5,也就是“优先级数值小于 5 的中断不能调用 FreeRTOS API”。但很多人在配置串口中断优先级时,直接用了 0,结果中断里调用了 xSemaphoreGiveFromISR,程序就异常了。
所以实际项目里,串口等需要调用 FreeRTOS API 的中断,优先级数值要设置成 >= 5。对于不需要调用 FreeRTOS API 的极少数紧急中断,比如系统故障检测,才可以用更高的优先级。
这个坑非常隐蔽,因为它不是必然报错的。有些情况可能偶尔正常,偶尔卡死,特别难排查。如果你发现中断相关的 FreeRTOS API 调用时好时坏,先去查中断优先级。
7. 学习路线:从入门到能独立做项目
7.1 入门阶段:先把基础外设打牢
不要一上来就做多任务。FreeRTOS 是建立在 STM32 基础之上的,如果连 GPIO 翻转、串口收发、定时器中断都不熟,直接学 FreeRTOS 会非常痛苦。
我的建议顺序是:
- C 语言:指针、数组、结构体、函数指针、内存分区。
- STM32 裸机外设:GPIO、UART、TIM、I2C、SPI。
- 做 2 到 3 个裸机小项目,比如按键控制 LED、串口收发回显、温度传感器采集。
- 至少完整用 CubeMX 生成过一个工程,理解引脚复用和时钟树。
这个阶段不需要碰 RTOS。把外设用熟,后面很多问题都不是 RTOS 的问题,而是外设配置问题。
7.2 进阶阶段:把 FreeRTOS 的核心机制逐个跑通
在能熟练写 STM32 裸机代码之后,开始学 FreeRTOS。
先跑通一个 LED 任务,理解任务创建和调度;然后创建两个任务,一个打印日志一个翻转 LED,看优先级和时间片;再把串口接收改成“中断接收 + 信号量 + 任务处理”的模型;最后用一个综合项目把所有机制串起来。
这个阶段可以做一个小项目,比如:
- 串口接收上位机指令,解析后控制 LED、电机、蜂鸣器。
- 按键扫描任务和显示任务通过队列交互。
- 传感器采集任务定时运行,数据放到队列,另一个任务负责打印。
- 两个任务同时操作 OLED 或串口时,用互斥量保护。
做完这个项目,FreeRTOS 的核心机制就基本掌握了。
有条件的话,可以看看 FreeRTOS 的源码,比如任务切换的完整流程、队列的阻塞唤醒机制。面试时经常问的任务调度流程、优先级反转、栈溢出检测,也都是从这些源码里提炼出来的。看源码不是背代码,而是理解它为什么这么设计,后面排查问题会非常有底气。
7.3 生产化阶段:从“能跑”到“能稳定跑”
很多人项目能跑通,但一跑十几个小时就崩溃,或者偶发不稳定,问题往往不在功能逻辑本身,而在工程细节。
这个阶段要补的是工程能力:
- 统一的日志系统,包含时间戳、任务名、错误码。
- 统一的任务栈检查和堆余量监控。
- 中断和任务的职责边界意识。
- 通信协议解析的状态机设计,而不是简单 if-else 处理。
- 失败重试和超时机制。
还可以引入单元测试。Unity 是嵌入式领域比较常用的测试框架,可以直接在主机上或目标板上跑。给底层协议解析、十进制转换、校验算法这些无硬件依赖的函数写单元测试,能大幅减少回归问题。
如果后续想往更大型的方向走,可以再接触嵌入式 Linux、LwIP 网络协议栈、LVGL 图形界面、文件系统,甚至尝试把一些轻量 AI 模型部署到更高配置的嵌入式开发板上。这些方向都建立在同一个基础上:能理解系统资源,能设计任务,能排查问题。
一些最后的实在建议
写到最后,给你几条我踩过的坑。
第一,不要一上来就把任务栈和优先级往大里调。优先级越高,不代表越稳。先把单任务跑通,再逐步增加任务,每增加一个都确认一次日志和栈余量。
第二,遇到异常别急着改参数,先记录现象、打开调试器、看 PC 和栈。多数 HardFault 都是数组越界或者栈溢出,不是 FreeRTOS 本身的问题。
第三,中断里的代码越短越好。凡是能放到任务里的,都放到任务里。ISR 里只负责事件通知和数据最小化保存。
第四,全局变量一定要谨慎使用。在 FreeRTOS 项目里,跨任务共享数据建议优先用队列或信号量,而不是裸奔的全局变量。这样即使以后重构,也不至于把整个程序改崩。
STM32+FreeRTOS 的学习曲线没有想象中陡峭。真正难的不是 API 背不下来,而是没有先想清楚为什么要引入多任务,以及每个机制适用什么场景。把环境搭起来、把第一个任务跑通、把串口和中断整合进去,你就算真正入门了。之后无论做控制类项目、物联网节点,还是往嵌入式 Linux 方向发展,这套思维都够用。