首页 / 资讯中心 / 文章详情

FreeRTOS在IoT设备中的实战:从裸机到RTOS的完整指南

FreeRTOS在IoT设备中的实战:从裸机到RTOS的完整指南 ★ FEATURED ARTICLE
1. 为什么我最终选择了FreeRTOS作为IoT设备的系统底座搞嵌入式物联网开发的人绕不开一个话题设备端到底跑什么系统。裸机大循环能搞定的事情一旦上了多传感器采集、无线通信协议栈、低功耗管理、OTA升级这些需求代码就会迅速膨胀成一团乱麻。我最早做环境监测节点的时候就是用一个while(1)加一堆状态机硬扛结果加了LoRa通信之后时序冲突、数据丢包、看门狗复位轮番上阵调试到怀疑人生。后来转向开源RTOS试过不止一个方案最终在多个量产项目里稳定用的是FreeRTOS。这篇文章不是官方文档的搬运而是把我这几年在IoT设备上使用FreeRTOS的完整思路、踩过的坑、调优的参数、排查问题的方法系统地讲一遍。如果你正在做低功耗传感器节点、智能家居终端、工业数据采集器这类产品或者单纯想从裸机过渡到RTOS这篇内容应该能帮你少走不少弯路。FreeRTOS本质上是一个微内核实时操作系统核心就是一个调度器和一组任务间通信机制。它不帮你做文件系统、不帮你做网络协议栈、不帮你做UI这些都要自己配。但正是这种“只做最核心的事”的设计哲学让它的内核可以裁剪到几KB级别跑在RAM只有十几KB的MCU上毫无压力。对于IoT设备来说资源受限是常态这也是FreeRTOS在这个领域占据主导地位的根本原因。我下面会从整体设计思路、核心机制拆解、实际移植与任务划分、问题排查几个维度展开尽量把每个“为什么这么做”讲清楚而不是只丢一堆API让你自己猜。2. FreeRTOS的整体设计思路与核心机制拆解2.1 微内核架构到底“微”在哪里FreeRTOS的内核文件其实就几个tasks.c、queue.c、list.c、timers.c、event_groups.c加上一堆portable目录下的移植层文件。编译出来的内核代码在Cortex-M3上大概6到10KB FlashRAM占用取决于你创建了多少任务和用了哪些通信对象。它的“微”体现在几个方面。第一没有内存管理单元的概念所有任务共享同一个地址空间没有进程隔离。这意味着一个任务跑飞了可能把整个系统带崩但也省掉了MMU带来的复杂性和开销。第二没有设备驱动框架串口怎么初始化、SPI怎么配置全是你自己的事FreeRTOS不掺和。第三没有系统调用的概念任务通过API函数直接和内核交互没有用户态和内核态的切换开销。这种设计对IoT设备来说非常合适。因为IoT设备通常功能单一、实时性要求明确、资源紧张不需要通用操作系统的那些复杂抽象。你需要的只是一个可靠的任务调度器和一套好用的通信机制FreeRTOS刚好就提供这些。2.2 任务调度器的抢占式逻辑与配置选择FreeRTOS默认是抢占式调度高优先级任务就绪时立刻抢占低优先级任务。这个行为由configUSE_PREEMPTION宏控制设成1就是抢占式设成0就是协作式任务主动让出才切换。我几乎所有项目都用抢占式因为IoT设备里经常有硬实时的需求比如无线接收中断触发的数据处理任务必须立刻响应。调度器的核心是一个就绪列表数组每个优先级对应一个链表。当任务就绪时它被挂到对应优先级的链表上。调度器每次切换时从最高优先级的非空链表里取第一个任务运行。同优先级任务之间默认是轮转调度由configUSE_TIME_SLICING控制设成1就开启时间片轮转每个tick切换一次。这里有个关键配置configTICK_RATE_HZ。它决定了系统节拍的频率默认是1000Hz也就是1ms一个tick。这个值直接影响时间精度和调度开销。tick频率越高延时精度越好但每次tick中断的CPU开销也越大。我一般这么选应用场景推荐tick频率理由低功耗传感器节点100Hz降低tick中断唤醒次数省电一般控制类应用1000Hz1ms精度够用开销可接受高速数据采集1000Hz以上需要更细的时间粒度注意tick频率改了之后所有用vTaskDelay()的地方都要重新算时间。vTaskDelay(100)在1000Hz下是100ms在100Hz下就变成1秒了。我见过有人改完tick频率忘了改延时参数结果任务跑得慢如蜗牛还找不到原因。2.3 任务间通信队列、信号量、事件组的选型逻辑FreeRTOS提供了几种任务间通信机制很多人刚上手时不知道该用哪个。我按实际使用频率和场景来说。队列是最常用的。它是任务间传递数据的首选方式支持FIFO和LIFO两种模式LIFO通过xQueueSendToFront实现。队列的本质是一块环形缓冲区加上两个阻塞列表发送等待列表和接收等待列表。发送任务在队列满时可以阻塞等待接收任务在队列空时也可以阻塞等待这个阻塞机制是FreeRTOS队列最核心的价值。队列的深度和每个元素的大小在创建时指定。比如你要传一个结构体xQueueCreate(5, sizeof(MyStruct))就是创建一个能存5个元素的队列。这里有个坑队列是值拷贝不是引用传递。你发送时是把数据拷贝进队列接收时再拷贝出来。所以元素大小别搞太大否则拷贝开销和RAM占用都会上去。二值信号量本质上就是深度为1、元素大小为0的队列。它用于任务和中断之间的同步比如中断里xSemaphoreGiveFromISR()任务里xSemaphoreTake()等待。计数信号量则是深度可配的用于管理多个资源的访问比如一个资源池有3个可用资源就创建一个初始计数为3的信号量。互斥量是特殊的二值信号量带优先级继承机制。优先级继承解决的是优先级翻转问题低优先级任务持有互斥量时如果高优先级任务来抢低优先级任务的优先级会被临时提升到和高优先级一样避免被中间优先级的任务插队导致高优先级任务长时间等待。IoT设备里多个任务访问同一个SPI总线或I2C总线时互斥量是必须的。事件组适合“多个条件满足才继续”的场景。比如一个任务要等WiFi连接成功、传感器数据就绪、电池电量正常三个条件都满足才开始上报用事件组就比用三个信号量清爽得多。每个事件组有24个可用位在32位系统上高8位被内核保留每个位代表一个事件。2.4 内存管理方案的选择与实测对比FreeRTOS提供了5种内存管理方案在heap_1.c到heap_5.c里。这个选择很多人不重视但实际影响很大。heap_1最简单只分配不释放适合那些所有任务和队列都在启动时创建、之后不再动态创建删除的应用。heap_2支持释放但不合并相邻空闲块容易产生碎片。heap_3是对标准malloc/free的封装线程安全但依赖库函数代码体积大。heap_4支持释放并合并相邻空闲块是最常用的方案。heap_5在heap_4基础上支持多个不连续的内存区域。我大多数项目用heap_4因为它在碎片管理和代码体积之间取得了比较好的平衡。但要注意heap_4的合并只在相邻空闲块之间进行如果频繁地创建删除不同大小的对象时间长了还是会有碎片。对于长期运行的IoT设备我的做法是尽量在系统启动阶段就把所有任务、队列、信号量创建好运行期间不再动态创建删除这样heap_4的碎片问题基本不会出现。内存池的大小由configTOTAL_HEAP_SIZE定义。这个值怎么定我的经验是先给一个估计值编译后看xPortGetFreeHeapSize()的返回值如果剩余很多就调小如果快用完了就调大。一般留20%到30%的余量比较稳妥。3. 在IoT设备上移植FreeRTOS的完整实操流程3.1 移植前的准备工作与文件结构规划移植FreeRTOS到一颗新MCU上核心工作是实现portable层的那几个文件。以Cortex-M系列为例需要的是port.c和portmacro.h以及启动文件里的三个中断处理函数SVC_Handler、PendSV_Handler、SysTick_Handler。我一般这样组织目录结构project/ ├── FreeRTOS/ │ ├── Source/ │ │ ├── tasks.c │ │ ├── queue.c │ │ ├── list.c │ │ ├── timers.c │ │ ├── event_groups.c │ │ ├── include/ │ │ └── portable/ │ │ └── GCC/ARM_CM4F/ │ │ ├── port.c │ │ └── portmacro.h │ └── Config/ │ └── FreeRTOSConfig.h ├── BSP/ │ ├── bsp_uart.c │ ├── bsp_spi.c │ └── bsp_gpio.c ├── App/ │ ├── app_main.c │ ├── task_sensor.c │ ├── task_comm.c │ └── task_power.c └── main.cFreeRTOSConfig.h是整个系统的配置中心每个宏都影响内核行为。我下面列几个最关键的配置项和我的常用值#define configUSE_PREEMPTION 1 #define configUSE_TIME_SLICING 1 #define configTICK_RATE_HZ 1000 #define configMAX_PRIORITIES 8 #define configMINIMAL_STACK_SIZE 128 #define configTOTAL_HEAP_SIZE (20 * 1024) #define configUSE_MUTEXES 1 #define configUSE_RECURSIVE_MUTEXES 0 #define configUSE_COUNTING_SEMAPHORES 1 #define configUSE_EVENT_GROUPS 1 #define configUSE_TASK_NOTIFICATIONS 1 #define configCHECK_FOR_STACK_OVERFLOW 2 #define configUSE_IDLE_HOOK 1 #define configUSE_TICK_HOOK 0 #define configUSE_MALLOC_FAILED_HOOK 1configMAX_PRIORITIES设成8意味着优先级范围是0到7数字越大优先级越高。0是空闲任务的优先级用户任务最低从1开始。我一般不会设太多优先级因为优先级越多调度器维护的就绪列表越多而且优先级分配本身也容易乱。8个级别对大多数IoT设备足够了。configMINIMAL_STACK_SIZE是空闲任务的栈大小单位是word在32位MCU上是4字节。128个word就是512字节。这个值不能太小因为空闲任务里可能跑钩子函数而且有些移植层会在空闲任务里做低功耗处理。configCHECK_FOR_STACK_OVERFLOW设成2是最严格的检查方式会在任务切换时检查栈指针是否越界并在栈末尾填充魔术字来检测溢出。调试阶段一定要开量产时可以关掉省点开销。3.2 启动流程与第一个任务的创建FreeRTOS的启动流程很清晰硬件初始化、创建任务、启动调度器。我一般把硬件初始化放在main()里调度器启动前的最后一步。int main(void) { BSP_Init(); // 时钟、GPIO、串口等硬件初始化 App_TaskCreate(); // 创建所有应用任务 vTaskStartScheduler(); // 启动调度器正常情况下不会返回 while (1) {} // 如果调度器启动失败会走到这里 }vTaskStartScheduler()会创建空闲任务和定时器任务如果configUSE_TIMERS为1然后启动SysTick中断最后触发SVC异常来启动第一个任务。如果堆内存不够导致空闲任务或定时器任务创建失败调度器会直接返回所以后面的while(1)是一个安全兜底。创建任务用xTaskCreate()BaseType_t xTaskCreate( TaskFunction_t pxTaskCode, // 任务函数指针 const char * const pcName, // 任务名调试用 const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小单位word void * const pvParameters, // 传给任务的参数 UBaseType_t uxPriority, // 优先级 TaskHandle_t * const pxCreatedTask // 任务句柄 );任务函数的原型是void vTaskFunction(void *pvParameters)里面通常是一个无限循环。任务函数绝对不能返回如果返回了FreeRTOS会触发断言失败。如果某个任务确实只需要执行一次执行完后要调用vTaskDelete(NULL)把自己删掉。栈大小的确定是个经验活。我一般先给一个偏大的值然后用uxTaskGetStackHighWaterMark()查看栈使用峰值再逐步调小。这个函数返回的是栈剩余的最小值单位word如果返回值接近0就说明栈快溢出了。我通常留20%到30%的余量。3.3 任务划分的实操原则与优先级分配任务划分是RTOS应用设计里最考验经验的部分。我的原则是按功能模块划分按实时性要求分配优先级按数据流方向设计通信。以一个典型的IoT传感器节点为例我会这样划分任务名优先级栈大小职责触发方式Task_Sensor3256 word周期采集传感器数据定时阻塞Task_Process2512 word数据处理与打包队列驱动Task_Comm4512 word无线通信收发事件驱动Task_Power1128 word电源管理与低功耗定时阻塞Task_Debug1256 word串口日志输出队列驱动Task_Comm优先级最高因为无线通信的时序要求最严格收到数据后必须尽快处理否则可能丢包。Task_Sensor次之采集周期要准。Task_Process优先级较低因为数据处理可以稍微延迟。Task_Power和Task_Debug优先级最低它们不影响核心功能。优先级分配有个常见误区把所有任务都设成高优先级。这样等于没有优先级而且高优先级任务多了之后低优先级任务可能长时间得不到执行看门狗都会饿死。我的经验是真正需要硬实时的任务不超过2到3个其余任务用中低优先级就够了。3.4 中断与任务的配合FromISR函数的正确使用FreeRTOS里中断服务程序ISR和任务之间的交互必须用带FromISR后缀的API。这些函数和普通版本的区别在于它们不会阻塞而是返回一个BaseType_t值表示是否有更高优先级的任务被唤醒如果有需要在ISR退出前调用portYIELD_FROM_ISR()触发一次上下文切换。void UART_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; uint8_t data UART_ReadByte(); xQueueSendFromISR(xUartQueue, data, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }这里有个关键点中断的优先级配置。在Cortex-M上FreeRTOS通过configMAX_SYSCALL_INTERRUPT_PRIORITY或configMAX_API_CALL_INTERRUPT_PRIORITY来限定哪些中断可以调用FreeRTOS的API。优先级高于这个阈值的中断不受FreeRTOS管理不能调用任何FromISR函数优先级低于或等于这个阈值的中断可以安全调用。我见过太多人在这里翻车中断优先级设成了最高然后在中断里调用xQueueSendFromISR()结果系统随机崩溃。原因就是高优先级中断打断了内核的临界区破坏了内核数据结构。记住一个原则要调用FreeRTOS API的中断优先级数值必须大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY在Cortex-M上数值越大优先级越低。3.5 低功耗设计Tickless Idle模式的配置与实测IoT设备很多是电池供电的低功耗是刚需。FreeRTOS的Tickless Idle模式是省电的关键。默认情况下SysTick每1ms中断一次即使系统空闲也会不断唤醒CPU。Tickless模式下当所有任务都阻塞时内核会计算下一个任务唤醒的时间然后关闭SysTick让MCU进入低功耗模式等到时间到了再唤醒。配置Tickless需要几个步骤。首先在FreeRTOSConfig.h里#define configUSE_TICKLESS_IDLE 1 #define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2然后实现两个钩子函数void vPortSuppressTicksAndSleep(TickType_t xExpectedIdleTime) { // 1. 停止SysTick // 2. 计算实际可睡眠的时间 // 3. 配置唤醒源RTC或低功耗定时器 // 4. 进入低功耗模式 // 5. 唤醒后补偿tick计数 }这个函数的实现和具体MCU的低功耗模式强相关。以STM32L系列为例可以进入STOP模式用RTC的唤醒定时器来唤醒。实测下来一个采集周期为10秒的传感器节点用Tickless模式后平均电流从8mA降到了200uA左右效果非常明显。注意Tickless模式下vTaskDelay()的精度会受低功耗定时器精度的影响。如果低功耗定时器的时钟源是32.768kHz的LSE那么唤醒时间的误差可能在几十微秒级别。对于大多数IoT应用这个精度够了但如果你的应用需要微秒级延时就不能依赖Tickless模式下的vTaskDelay()。4. 实际项目中踩过的坑与排查技巧实录4.1 栈溢出最隐蔽也最致命的崩溃原因栈溢出是FreeRTOS项目里最常见的崩溃原因而且症状千奇百怪有时候是HardFault有时候是任务行为异常有时候是随机重启。因为栈溢出会覆盖相邻内存区域的数据破坏什么取决于内存布局。排查栈溢出的第一步是打开configCHECK_FOR_STACK_OVERFLOW并实现钩子函数void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { printf(Stack overflow in task: %s\n, pcTaskName); taskDISABLE_INTERRUPTS(); while (1) {} }这个钩子函数会在检测到栈溢出时被调用打印出任务名。但要注意方法2的检查是在任务切换时进行的如果任务在两次切换之间就溢出了可能检测不到。所以调试阶段我还会用uxTaskGetStackHighWaterMark()定期打印各任务的栈使用峰值。栈大小的估算有个经验公式局部变量大小 函数调用深度 × 每层开销 中断嵌套开销 安全余量。但实际很难精确算因为编译器优化、函数内联都会影响。我的做法是先用一个偏大的值跑起来观察高水位线再逐步调整。4.2 优先级翻转与互斥量的正确使用姿势优先级翻转是RTOS里的经典问题。场景是这样的低优先级任务A持有互斥量高优先级任务C等待这个互斥量而阻塞中优先级任务B就绪后抢占了A导致A迟迟不释放互斥量C就一直等。结果就是高优先级的C被中优先级的B间接阻塞了。FreeRTOS的互斥量通过优先级继承来解决这个问题当C等待A持有的互斥量时A的优先级被临时提升到和C一样这样B就无法抢占A了。A释放互斥量后优先级恢复原值。但优先级继承不是万能的。它只能解决单层互斥量的情况如果有多层嵌套的互斥量或者任务在持有互斥量时又去等另一个互斥量就可能出现死锁。我的原则是持有互斥量时不做任何可能阻塞的操作尽快释放。另外互斥量不能在中断里使用。中断里要用二值信号量来做同步。互斥量的xSemaphoreTake()有超时参数我一般会设一个合理的超时值而不是portMAX_DELAY这样即使出现异常也能恢复不会永久阻塞。4.3 队列使用中的常见错误与规避方法队列用错的地方主要有几个。第一是在中断里用xQueueSend()而不是xQueueSendFromISR()这个前面说过了后果是系统随机崩溃。第二是队列元素太大导致拷贝开销大、RAM占用高。如果数据量确实大应该传指针而不是传数据本身但要注意指针指向的内存的生命周期管理。第三是队列深度设置不合理。深度太小生产者频繁阻塞深度太大浪费RAM而且增加延迟。我的经验是根据生产者和消费者的速度差来定。如果生产者突发性强队列深度要能缓冲一个突发周期的数据量。比如传感器每秒产生10个数据通信任务每5秒处理一次那队列深度至少要能存50个数据。第四是忘记检查创建返回值。xQueueCreate()在堆内存不足时会返回NULL如果不检查就直接用会触发断言或HardFault。所有创建类的API都要检查返回值这是基本习惯。4.4 系统节拍与延时精度的那些坑vTaskDelay()的延时精度受tick频率限制。在1000Hz下延时精度是1ms但实际延时时间会在[n, n1)个tick之间平均误差是0.5个tick。如果需要更精确的延时可以用vTaskDelayUntil()来做周期性延时它会补偿任务执行时间保持固定的周期。TickType_t xLastWakeTime xTaskGetTickCount(); const TickType_t xPeriod pdMS_TO_TICKS(100); while (1) { // 任务处理 vTaskDelayUntil(xLastWakeTime, xPeriod); }vTaskDelayUntil()和vTaskDelay()的区别在于前者是绝对延时保证周期固定后者是相对延时从调用时刻开始算。对于周期性采集任务必须用vTaskDelayUntil()否则任务执行时间会累积到周期里导致实际周期越来越长。还有一个坑是tick计数溢出。TickType_t在32位系统上是uint32_t在1000Hz下大约49.7天溢出一次。FreeRTOS内部用无符号运算处理了溢出问题但如果你自己直接比较tick值就要注意用(a - b) timeout的方式而不是a b的方式。4.5 常见问题速查表现象可能原因排查方法解决方案HardFault栈溢出、空指针、中断优先级错误查看LR和PC寄存器用栈回溯开栈检查、检查指针、调整中断优先级任务不执行优先级太低被饿死、栈太小创建失败打印各任务状态和高水位线调整优先级、增大栈随机重启看门狗饿死、堆耗尽检查看门狗喂狗任务优先级、打印剩余堆提高喂狗任务优先级、增大堆队列数据错乱中断里用了非FromISR版本检查所有ISR中的API调用改用FromISR版本延时不准tick频率配置错误、用了vTaskDelay做周期检查configTICK_RATE_HZ改用vTaskDelayUntil低功耗模式唤醒后异常tick补偿错误、外设状态未恢复检查vPortSuppressTicksAndSleep实现修正tick补偿、恢复外设时钟5. 从裸机到FreeRTOS的思维转变与进阶建议5.1 裸机思维与RTOS思维的核心差异从裸机转到RTOS最大的挑战不是API怎么用而是思维方式的转变。裸机编程是“顺序执行中断”的模型代码逻辑是线性的你很清楚每一步在哪里执行。RTOS编程是“并发任务通信”的模型多个任务在宏观上是同时运行的你需要考虑任务之间的时序关系、资源共享、优先级影响。举个例子裸机里你要读一个传感器直接调用读取函数等它返回就行了。RTOS里读传感器可能是一个独立任务读完之后通过队列把数据发给处理任务。你不能再假设“我调用完这个函数数据就准备好了”而要考虑“数据什么时候到队列里处理任务什么时候能拿到”。另一个差异是阻塞的概念。裸机里你写delay_ms(100)CPU就在那里空转100ms。RTOS里vTaskDelay(100)会让出CPU其他任务可以在这100ms里运行。这个差异意味着RTOS里可以用阻塞式编程模型代码逻辑更清晰而CPU利用率反而更高。5.2 调试手段与工具链的配合FreeRTOS的调试除了传统的断点和打印还有几个特有的手段。任务状态查看可以通过vTaskList()和vTaskGetRunTimeStats()来实现前者列出所有任务的状态、优先级、栈高水位线后者统计各任务的CPU占用率。这两个函数需要配置configUSE_TRACE_FACILITY和configGENERATE_RUN_TIME_STATS。char pcWriteBuffer[512]; vTaskList(pcWriteBuffer); printf(%s\n, pcWriteBuffer);输出大概是这样的TaskName State Priority Stack Num Task_Comm R 4 120 3 Task_Sensor B 3 200 2 Task_Process B 2 350 4 IDLE R 0 80 1这个表在排查任务饿死、栈溢出、优先级配置问题时非常有用。我一般在调试串口里定期打印这个表系统运行状态一目了然。运行时统计需要提供一个高精度的计时器通常是另一个定时器频率是tick的10到20倍。vTaskGetRunTimeStats()会输出每个任务占用的CPU时间百分比对于优化任务划分和优先级很有参考价值。5.3 什么场景不适合用FreeRTOS虽然我是FreeRTOS的重度用户但也要客观地说它不是万能的。以下几种情况我会考虑不用RTOS极简应用如果设备只做一件事比如一个按键控制一个继电器裸机大循环就够了上RTOS反而增加复杂性和资源开销。RAM极度受限如果MCU的RAM只有2KBFreeRTOS的内核数据结构加上每个任务的栈可能就占了一大半留给应用的空间太少。这种情况下裸机或者极简的调度器更合适。硬实时要求极高FreeRTOS的调度延迟在微秒级别对于大多数应用够了。但如果需要纳秒级的响应比如某些高速控制环路可能要用裸机加中断或者专用的硬件方案。认证要求严格的场景虽然FreeRTOS有安全认证版本但如果项目需要特定的行业认证可能需要考虑其他有相应认证的RTOS方案。5.4 后续进阶方向FreeRTOS本身只是内核实际IoT项目还需要很多配套组件。网络协议栈方面FreeRTOS有配套的FreeRTOS-Plus-TCP也可以配lwIP。文件系统方面有FreeRTOS-Plus-FAT但更常用的是LittleFS或FatFS。OTA升级方面需要自己实现或者用MCU厂商提供的方案。我的建议是先把内核用熟理解任务调度、通信机制、内存管理的原理然后再逐步引入其他组件。不要一上来就搞一个大而全的框架那样出了问题很难定位是内核的问题还是组件的问题。另外阅读FreeRTOS的源码是提升功力的好方法。它的代码量不大核心文件加起来几千行而且写得比较清晰。看一遍tasks.c里调度器的实现你对RTOS的理解会上一个台阶。特别是xTaskIncrementTick()和vTaskSwitchContext()这两个函数是调度器的核心值得反复看。我个人在实际项目中的体会是FreeRTOS最大的价值不在于它提供了多少功能而在于它用最简单的机制解决了嵌入式开发中最核心的问题如何让多个功能模块可靠地并发运行。把这个核心问题解决了剩下的就是在这个框架上添砖加瓦。踩过几次栈溢出和优先级翻转的坑之后你会对“并发”这件事有更深刻的理解这种理解会反过来提升你的裸机代码质量。最后分享一个小技巧每次创建任务时把任务名起得有意义一点调试的时候vTaskList()的输出会清晰很多这个习惯能帮你省下不少排查时间。
阅读完成 · 觉得有帮助?
咨询建站