ARTICLE DETAIL

资讯详情

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

FreeRTOS实战笔记:STM32移植、任务调度与HardFault排查

FreeRTOS实战笔记:STM32移植、任务调度与HardFault排查 最开始认真啃 FreeRTOS是在一块多路采集加显示的板子上被逼出来的。裸机那套 while(1) 里塞着按键扫描、串口收包、屏幕刷新、传感器读取功能一叠加某个环节稍微耗时整个界面就卡住不动。后来把这几件事拆成独立任务、给每件事单独的时间片问题就从能不能跑通变成了优先级怎么排、栈给多大。这份 FreeRTOS 个人笔记就是那段时间从零开始的记录从 STM32F103C8T6 起步的手动移植到用 CubeMX 一键生成工程再到任务、信号量、队列的实际写法以及后来换到 H743 上踩到的缓存和对齐那些事。如果你手里正攥着一块板子纠结要不要上 OS或者移植完之后任务偶尔 HardFault这里的东西应该能帮你省下不少翻手册的时间。1. 裸机跑得好好的为什么还要把 FreeRTOS 搬进来1.1 一个真实的分工需求场景我最早做的是一个数据采集板功能清单其实不复杂每 10ms 采一次传感器、每 100ms 刷一次屏幕、随时响应串口指令、还要处理按键。裸机的写法就是主循环里挨个调用谁排前面谁先跑。问题出在屏幕刷新上一次全屏刷新要几十毫秒这几十毫秒里按键按下去没反应串口来了指令也要等下一个循环。这就是最典型的该上 RTOS 了的信号——不是功能做不出来而是几个功能之间的时间要求互相打架。FreeRTOS 这类实时操作系统的价值说白了就是把原来一个人排队干所有事换成几个人分工、由调度器决定谁先上。采集任务该 10ms 跑一次就 10ms 跑一次屏幕刷新慢一点也没关系因为它不会再把按键响应堵死。理解这一点很重要因为它决定了你后面所有设计动作的出发点任务划分不是按功能数量分而是按时间特性和阻塞情况分。把会阻塞的、慢的放一个任务把要求响应快的放另一个任务这是后面排优先级的根。1.2 上了系统之后收益到底体现在哪第一是响应实时性。用 vTaskDelay 或者 vTaskDelayUntil 做周期任务只要优先级合理高优先级任务的延迟抖动通常能控制在一个节拍以内。裸机里那种刷屏时按键丢失的现象基本消失。第二是代码结构。每个功能一个任务函数各自一个 while(1)变量作用域清晰不用再靠一堆全局标志位在主循环里做状态机。后期加功能往往是新增一个任务而不是往主循环里再塞一段。第三是模块解耦。任务之间用队列、信号量通信而不是直接读写对方变量。这一层解耦带来的好处是调试时能明确知道数据在哪一条队列里卡住了比在主循环里断点追全局变量舒服太多。1.3 但也别把 OS 当成万能药我见过一些项目两三个简单功能也硬上 FreeRTOS结果 RAM 被几个任务的栈吃掉一大半还得为配置参数纠结半天。判断标准其实很简单如果各个功能之间没有明确的时间冲突裸机的前后台架构完全够用。还有一点常被忽略——FreeRTOS 本身是有开销的。SysTick 中断、上下文切换、临界区都会消耗 CPU 周期。任务切得越频繁、优先级层次越多这部分开销越明显。所以在资源紧张的 F103C8T6只有 20KB RAM上任务数量和栈大小都要抠着算。我第一版就吃过亏五个任务每个给了 256 字的栈编译出来 RAM 直接爆了链接器报错才发现。2. 内核里到底在转什么调度、堆栈与节拍2.1 Cortex-M3 上一次任务切换的真实过程要想在后面排查 HardFault 时不抓瞎这一节值得花时间。Cortex-M3 的内核设计天然适合跑 RTOS关键在于它有两个异常PendSV 和 SysTick。SysTick 负责产生时钟节拍PendSV 负责真正的任务切换而 PendSV 被特意设为最低优先级这样它就不会打断其他中断。一次切换的流程大致是SysTick 中断触发节拍计数器加一判断是否有任务延时到期需要唤醒如果需要切换就把 PendSV 异常挂起。等所有高优先级中断都处理完PendSV 才执行。进入 PendSV 时硬件已经自动把 R0-R3、R12、LR、PC、xPSR 这八个寄存器压进了当前任务的栈——这叫硬件压栈。剩下的 R4-R11 由软件在 PendSV 处理函数里手动压栈。接着从就绪列表里挑出下一个任务把它的栈指针加载进 PSP再恢复 R4-R11最后触发异常返回硬件自动弹出那八个寄存器。理解这段流程之后你再看 trace 里的切换时机、看某个任务栈里的数据心里就有谱了。我第一次看 PSP 指向的内存前几个字正是 R4-R11后面才是压进去的 PC 和 xPSR和手册完全对得上那一刻才算真正把这套机制记住。2.2 每个任务那一段栈里装了些什么任务栈不只是拿来存局部变量的。它至少要装下三层东西上下文现场上面说的那十几个寄存器、函数调用栈局部变量、返回地址、中间结果、以及中断嵌套时的额外现场如果任务执行期间来了中断。这也解释了为什么中断服务函数里不能随便申请大数组——那段空间是从当前任务栈里切的栈本来就不宽裕。我有个任务里写了 sprintf 拼一个 128 字节的字符串结果任务栈直接溢出现象是任务偶尔跑飞。后来把 buf 改成 static 或者直接从堆里申请问题就没了。栈大小怎么估经验做法是先给一个偏大的值开启栈溢出检测跑一段时间后用 uxTaskGetStackHighWaterMark 查每个任务的历史最低水位再按水位砍到合理值。这个方法比拍脑袋靠谱得多尤其是那些调用层次深的浮点运算任务。2.3 时钟节拍、延时与优先级的关系configTICK_RATE_HZ 决定了一秒钟系统节拍多少次。常见配置是 1000也就是 1ms 一个节拍。这个值越高时间精度越好但中断开销也越大太低则延时粒度粗糙。1000 在 F103 和 H743 上都是比较舒服的折中。有个细节必须说清楚vTaskDelay 的延时至少是一个节拍而且是至少。如果节拍是 1ms你调用 vTaskDelay(1)实际延迟可能是 1ms 也可能是接近 2ms取决于调用时机。要做精确的固定周期任务应该用 vTaskDelayUntil它根据上一次唤醒时间累加周期避免节拍错位导致的累积误差。我一开始做 10ms 采样用 vTaskDelay时间长了发现采样点会慢慢漂换成 vTaskDelayUntil 之后曲线才稳。另外优先级是抢占式的。高优先级任务一旦就绪立刻抢占当前低优先级任务。所以优先级排序的常见原则是对实时性要求越高的任务优先级越高但千万别把所有任务都设成高优先级那样等于没排。空闲任务优先级固定为 0是系统自动创建的一定要留够它的运行时间否则有些清理和回收动作会跑不起来。3. 移植这条路F103C8T6 起步H743 上量的差异3.1 手动移植必须拿到的几份源文件虽然现在大多用 CubeMX 生成但手动移植的过程值得走一遍因为生成出来的代码出问题时你得知道每一部分来自哪里。FreeRTOS 源码目录下真正要用的其实不多tasks.c、list.c、queue.c是核心timers.c用软件定时器才需要event_groups.c用事件组才需要stream_buffer.c用流缓冲或消息缓冲才需要portable/Keil/ARM_CM3/port.c或portable/IAR/ARM_CM3/port.c是内核相关的移植层取决于你的编译器portable/MemMang/heap_4.c是内存管理方案heap 有 1 到 5后面细说。移植层那一份文件最容易搞错。F103 是 Cortex-M3H743 是 Cortex-M7两者在基础移植上可以用 ARM_CM3/ARM_CM4 系列但 M7 涉及数据缓存和内存对齐直接搬过去可能能跑但并不稳这个后面第 3.4 节专门讲。3.2 FreeRTOSConfig.h 里那几个必须改的参数这个头文件是移植的总开关里面几十个宏新手容易被吓到。其实真正必须动的不多我列几个关键的宏名作用常见取值configCPU_CLOCK_HZ内核主频节拍计算依据72000000F103configTICK_RATE_HZ每秒节拍数1000configMAX_PRIORITIES最大优先级数5 到 8 够用configMINIMAL_STACK_SIZE空闲任务栈大小128字configTOTAL_HEAP_SIZE堆总大小按剩余 RAM 定configUSE_PREEMPTION是否抢占调度1configCHECK_FOR_STACK_OVERFLOW栈溢出检测等级1 或 2configCPU_CLOCK_HZ 填错是很隐蔽的坑。填错了不会报错但所有延时都会成比例地偏快或偏慢。我一开始填的是 8MHz 的晶振频率而不是 72MHz 的系统频率结果任务延时全都慢了好几倍查了半天才反应过来是这里。记住要填的是内核实际运行频率也就是 Systick 的时钟源频率不是晶振频率。3.3 Keil 与 IAR 两套环境下的差异处理KeilARMCC/ARMCLANG和 IAR 在移植时的差异主要在两处移植层源文件不同Keil 用 portable/Keil 下的IAR 用 portable/IAR 下的内联汇编和修饰符写法不同好在这些差异已在各自移植层里封装好了你只要选对目录就行。另外还有个容易忘的点Keil 里要把 FreeRTOSConfig.h 的路径加进头文件搜索路径同时确保它没有被工程里另一个同名文件覆盖。IAR 那边则要检查链接配置文件里堆栈段的定义别和 FreeRTOS 自己管理的堆打架。我见过 IAR 工程里因为链接脚本里 CSP 堆和 FreeRTOS 的堆都在用导致偶发内存分配失败。还有一点是中断优先级分组。FreeRTOS 要求能调用内核 API 的中断优先级必须低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 对应的阈值也就是这些中断的抢占优先级数值要足够大。如果某个中断优先级配得太高又调用了 FromISR 版本 API会触发断言或直接跑飞。3.4 换到 H743 之后的缓存与对齐问题从 F103 升到 STM32H743 是个不小的跳跃。H743 是 Cortex-M7主频更高还有数据缓存D-Cache。很多人在 F103 上跑得好好的 FreeRTOS 工程搬到 H743 上遇到两类问题第一类是DMA 与缓存的一致性问题。如果 DMA 向一段内存写数据而 CPU 那边缓存里还存着旧值读到就是脏数据。解决办法是 DMA 缓冲区对齐到缓存行通常是 32 字节必要时在读写前后做缓存失效或回写。第二类是栈指针和访问对齐。M7 对非对齐访问的容忍度比 M3 差某些没对齐的指针强转会直接触发总线错误。FreeRTOS 的任务栈本身会对齐到 8 字节但如果你的任务里强转乱指问题还是可能冒出来。此外M7 上还有指令缓存和分支预测上下文切换的实测开销比 M3 大一些但这些对普通应用影响不大。真正需要留意的是——在 M7 上开缓存之前先把没有缓存的情况下跑通再逐段打开缓存并验证这样出问题时范围好缩小。4. 用 CubeMX 把第一份能跑的工程生成出来4.1 时钟树配置里容易忽略的地方CubeMX 里第一步是把时钟树配好这一步如果偷懒后面 systick 频率、串口波特率全都会歪。以 F103C8T6 为例外部晶振接 8MHz经过 PLL 倍频到 72MHz这是最常见也最稳的配置。配好之后要确认 CubeMX 界面上显示的 HCLK 是 72MHz别只看你输入的数值。还有个隐蔽点如果你在 FreeRTOS 使能之后才回头改时钟树记得重新生成并检查 configCPU_CLOCK_HZ 是否跟着更新了。CubeMX 一般会帮忙更新但偶尔会有版本差异我遇到过生成的工程里这个宏没同步的情况所以习惯性手动核对一遍。时钟节拍源默认用 SysTick。如果工程里其他中间件也想用 SysTick比如某些 HAL 延时可能会有冲突这时候可以改成用某个通用定时器如 TIM来当节拍源。这个选项在 CubeMX 的 FreeRTOS 配置页里能选遇到节拍异常可以去看看这里。4.2 FreeRTOS 中间件参数逐项拆解CubeMX 的 FreeRTOS 配置分好几个标签页新手最容易只看默认值就直接生成。我挑几个真正影响行为的说一下Interface 选择 CMSIS_V1 还是 CMSIS_V2。V2 的 API 更现代封装了 osThreadNew 这类接口还支持更多内核对象类型。如果只是学习用 V1 更接近原生 FreeRTOS 写法调试时对照官方文档也方便。两者不要混用混用会出现头文件冲突。Memory Management scheme决定用哪份 heap 文件。这个选择很重要我放到下一节单独讲。USE_PREEMPTION保持开启。关掉就是时间片轮转大多数场景不适用。TOTAL_HEAP_SIZE直接决定你能创建多少任务和队列。CubeMX 默认值往往偏小按你的实际需求往上调但要给栈留够空间。MINIMAL_STACK_SIZE是空闲任务的栈。默认 128 字通常够但如果在空闲钩子里做了事情就得加。4.3 生成的代码结构以及该不该手改生成之后你会看到 Core/Src 下多出 freertos.c里面是默认的空任务。系统初始化顺序大致是HAL_Init、SystemClock_Config、MX_GPIO_Init、MX_FREERTOS_Init然后 osKernelStart 启动调度器。启动之后主函数就再也不返回了永远不会走到 while(1)。有一个点必须提醒调度器启动前可以创建任务和队列但不能用带阻塞的 API。我见过有人在 main 里、osKernelStart 之前用 osDelay结果卡死。因为那时调度器还没跑起来延时没人处理。至于该不该手改生成的代码——我的建议是FreeRTOSConfig.h 里的东西尽量让 CubeMX 管业务逻辑放在你自己新建的文件里在 freertos.c 的任务函数里调用你的函数。这样下次重新生成不会把你的代码冲掉。USER CODE 注释块里的内容会被保留但要小心别把那块写得太复杂生成器偶尔对格式敏感。5. 任务、信号量与队列这些 API 得亲手写一遍5.1 任务的创建、删除与优先级设计原生 API 创建任务TaskHandle_t xLedTaskHandle NULL; xTaskCreate(vLedTask, /* 任务函数 */ LED, /* 任务名调试用 */ 128, /* 栈深度单位是字 */ NULL, /* 传给任务的参数 */ 2, /* 优先级 */ xLedTaskHandle);栈深度的单位是字word不是字节。128 字在 32 位机上是 512 字节这一点新手常搞混给成 128 以为是 128 字节结果栈严重不足。任务函数必须是死循环或者在结束前调用 vTaskDelete(NULL) 删除自己。如果任务函数直接 return 了行为未定义通常会触发断言。优先级设计上我的习惯是把周期紧、要响应的任务设高优先把界面刷新、日志这类慢任务设低优先。相同优先级的任务会按时间片轮转前提是 configUSE_TIME_SLICING 为 1。任务数量上F103 这种 RAM 紧张的平台建议控制在 5 到 6 个以内。5.2 二值信号量与计数信号量的使用边界二值信号量就是只有 0 和 1 两态的同步工具最典型用法是中断给任务发信号。比如按键中断里给出信号量任务里阻塞等待这个信号量收到就处理。SemaphoreHandle_t xBtnSem NULL; /* 中断里 */ BaseType_t xHigher pdFALSE; xSemaphoreGiveFromISR(xBtnSem, xHigher); portYIELD_FROM_ISR(xHigher); /* 任务里 */ if (xSemaphoreTake(xBtnSem, portMAX_DELAY) pdTRUE) { /* 处理按键 */ }计数信号量则是能累加的适合资源池或者累积事件计数的场景——比如串口每收一帧给一次任务按能力消费。两者的选择标准很简单只需要一个有/无的状态用二值需要累加次数或管理多个同类资源用计数。这里最容易踩的坑是优先级翻转。假设一个低优先级任务持有互斥信号量高优先级任务在等它而此时中优先级任务抢占了低优先级任务高优先级任务就被间接拖住了。解决方法是使用互斥信号量Mutex而不是普通二值信号量互斥信号量带优先级继承能缓解这个问题。记住用于资源保护的要用互斥量用于同步的才用二值信号量。5.3 队列传字符串和结构体的两种做法队列传数据时要分清传值和传指针。队列在创建时指定每个元素的长度如果传的是指针队列里存的是指针本身指向的数据生命周期要自己管。传结构体推荐做法数据拷贝进队列安全typedef struct { uint8_t len; char buf[32]; } Msg_t; QueueHandle_t xMsgQueue xQueueCreate(8, sizeof(Msg_t)); Msg_t msg; xQueueSend(xMsgQueue, msg, portMAX_DELAY); /* 数据被拷贝进队列 */ xQueueReceive(xMsgQueue, msg, portMAX_DELAY); /* 拷贝出来 */传字符串指针省内存但要小心QueueHandle_t xStrQueue xQueueCreate(8, sizeof(char *)); char *p pvPortMalloc(32); strcpy(p, hello); xQueueSend(xStrQueue, p, portMAX_DELAY); /* 接收方用完必须 vPortFree否则内存泄漏 */能传值就别传指针。传指针的写法在中断和任务混用时极易出错如果发送方用的是局部缓冲区地址等接收方处理时那块内存早就被覆盖了。我早期就因为这个 bug 排查了很久现象是偶尔收到乱码。后来统一改成结构体传值问题再没复现。5.4 任务同步中最常见的几个错误第一个是在中断里用了非 FromISR 版本 API。队列、信号量在中断里必须用 xQueueSendFromISR、xSemaphoreGiveFromISR否则会破坏内核状态跑飞没商量。第二个是忘记处理 FromISR 的返回和 yield。收到 pdTRUE 且需要切换时才调用 portYIELD_FROM_ISR漏了会导致任务响应变慢但不一定出错很难查。第三个是在临界区或中断里做了耗时操作。临界区会关中断里面做的事越多系统响应越差。我见过有人在临界区里刷屏整个系统卡到没法用。第四个是共享资源没保护。两个任务同时读写同一个全局变量或外设寄存器必须加互斥量或者关中断。这类问题往往是间歇性的最难定位。6. 堆栈溢出与 HardFault一次把问题查透6.1 栈溢出检测两个宏能帮你到什么程度configCHECK_FOR_STACK_OVERFLOW 有两个等级。设为 1 时切换任务时检查当前栈指针是否越界能抓到比较明显的溢出设为 2 时任务创建时把整个栈填成特定图案比如 0xA5切换时检查栈末尾的图案是否被破坏能抓到使用稍超的情况。等级 2 更灵敏但更耗时间。开启检测后还要实现钩子函数void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { /* 断点或点亮错误灯把 pcTaskName 打出来 */ (void)xTask; (void)pcTaskName; for (;;) { } }注释里强烈建议把任务名打出来或存到一个全局变量里因为钩子函数运行时栈很可能已经乱了printf 不一定靠得住。我一般是在这里点个错误灯然后结合任务名手工缩小范围。另外一个实用工具是 uxTaskGetStackHighWaterMark返回任务栈的历史最小剩余量。定期打印这个值哪个任务水位快见底就加哪个比盲目加栈科学。6.2 HardFault 的完整排查链路遇到 HardFault很多人第一反应是到处加 printf其实有更系统的办法。常见触发原因按概率排空指针解引用、数组越界、栈溢出、非对齐访问、访问了未使能时钟的外设。系统的排查思路是这样的先在 HardFault_Handler 里挂一个断点运行到断点时查看几个关键寄存器的值。简单做法是在处理函数里读取 MSP、PSP 以及几个寄存器把发生异常时的 PC 值取出来然后在反汇编窗口或 map 文件里定位那条指令。更省事的做法是让处理函数保存现场然后复位把 PC、LR 这些值存到 RAM 里再读出来。具体来说在 HardFault 时判断是从任务模式还是中断模式进入根据 LR 的值判断用的是哪个栈指针然后从对应栈里取出压入的 PC这个 PC 指向的就是出问题的那条指令。这一步是分水岭——拿到 PC问题就解决了一半剩下的就是看那条指令在处理什么数据。我自己的经验是十次 HardFault 里至少六次是空指针或野指针两三次是栈溢出剩下的才是外设配置问题。所以第一时间检查传进去的指针是不是 NULL往往能快速命中。6.3 那个 failed to create directory 的编译报错.\obj\freertos.hex: error: q0147e: failed to create directory .\obj\freertos这类报错本质上是链接器或生成 hex 的工具没法创建输出目录。常见原因有几个一是输出目录名和输出文件名撞了。比如工程里存在一个叫 freertos 的文件夹而输出文件名也叫 freertos生成 hex 时工具想创建同名目录就失败了。解决方法是改输出文件名或者清理掉冲突的目录。二是路径过长或含特殊字符。Keil 对长路径和中文路径有时支持不好把工程移到浅一点的纯英文路径下往往能解决。三是权限问题输出目录被其他程序占用或没有写权限。关掉占用的软件、换个目录重新生成。遇到这类报错我的处理顺序是先清空编译中间文件重新构建不行再看路径和目录名冲突最后才怀疑工程配置。九成的构建类报错都能靠清理 换短英文路径解决没必要一上来就折腾编译选项。7. 把 LVGL 挂到 FreeRTOS 上的几个实际经验7.1 为什么 LVGL 适合独立成一个任务LVGL 是个图形库刷屏和控件处理本身很耗时间。如果把它塞进主循环或者某个采集任务里界面卡顿会直接影响其他功能。把它单独做成一个任务好处是刷屏这件事被隔离在自己的时间片里采集、通信任务的实时性不受影响。通常的做法是创建一个优先级较低的任务里面循环调用 lv_task_handler()并配合一个节拍源给 LVGL 提供毫秒级的时基。任务里加一个小的延时比如 5ms让出 CPU 给别的任务。7.2 显示刷新与系统心跳的配合LVGL 需要知道自己过了多少毫秒用来处理动画、超时这些逻辑。两种常见做法一是开一个定时器中断每毫秒给 LVGL 的时基加一二是在 LVGL 任务里根据系统节拍算出流逝的时间传给它。我更推荐第二种因为它在 FreeRTOS 里好控制。在任务里记录上一次调用时的时间戳xTaskGetTickCount差值转成毫秒传给 lv_tick_inc再调用 lv_task_handler。这样时基和系统节拍统一不会出现对不齐的问题。还要注意刷屏动作最好在任务里同步进行别在中断里直接操作显示接口容易和其他任务抢总线。7.3 内存分配方案的选择LVGL 自己有内存管理FreeRTOS 也有 heap。两者要分开看LVGL 内部的对象、缓冲区用它自己的分配器而 FreeRTOS 的策略是给任务和内核对象用的。如果你在显示驱动里用 pvPortMalloc 申请帧缓冲那就要保证 FreeRTOS 的堆够大。说到 heap 方案的选型FreeRTOS 提供五种heap_1 最简单但不能释放heap_2 能释放但不合并容易产生碎片heap_3 直接包装标准库的 malloc/free需要链接脚本和线程安全配合heap_4 支持释放和合并是最常用的heap_5 在 heap_4 基础上支持多块不连续内存区域适合复杂内存布局。对大多数项目heap_4 是默认首选。如果只是学习或者任务固定不变用 heap_1 也行它没有碎片问题因为根本不释放。用 heap_5 的场景一般是内部 RAM 和外部 SRAM 混用需要把两块内存都纳入管理。8. 面试和日常里绕不开的那几个内核问题8.1 关于 heap 的常见追问面试里问得最多的是heap_4 和 heap_2 的区别。答案核心在于是否做相邻空闲块合并heap_2 释放后不合并相邻块长期运行会产生外部碎片最终即使总空闲够也分配不出连续大块heap_4 会合并抗碎片能力强。理解这个区别也就能理解为什么 heap_4 是推荐方案。还可能问到 pvPortMalloc 和 malloc 的区别、堆溢出怎么检测configUSE_MALLOC_FAILED_HOOK。这些问题空背答案没用实际操作过一遍知道分配失败钩子在哪里触发印象才深。8.2 调度与临界区相关的问题FreeRTOS 抢占式调度是怎么实现的这类问题答到 SysTick 产生节拍、PendSV 完成切换、内核用汇编保存 R4-R11 就够了再深可以讲就绪列表和延时列表的结构。临界区的问题常问 portENTER_CRITICAL 和 portDISABLE_INTERRUPTS 的区别。前者是嵌套安全的临界区宏会记录进入次数后者是直接操作中断屏蔽寄存器。在需要嵌套进入临界区的场景必须用前者否则关中断次数对不上。还有一类问题是优先级继承如何解决优先级翻转答清楚互斥信号量在持有者被高优先级任务等待时会临时提升持有者优先级就行。8.3 我自己复盘的方式与其背题库我更倾向于把每个知识点落到一次实际调试上。看到信号量问题就回想自己那次传指针导致乱码的经历看到栈溢出就回想那个 sprintf 大数组导致的跑飞。带着场景去记比死背概念牢靠得多。另外我有个习惯每做完一个项目就把当时遇到的报错和解决办法记到一个单独的文档里标注是哪块内存、哪个任务、哪个宏引起的。时间长了这本错题集比任何教程都好用因为里面的每个坑都是真的复现路径清清楚楚。这套 FreeRTOS 的笔记也是这么一条条攒起来的从头到尾没有刻意去系统学习基本都是被实际项目逼着一点一点补齐的。
返回列表