2. RTOS选型与配置:FreeRTOS vs RT-Thread,任务优先级设计,内核裁剪

好,咱们直接进入正题。这一章聊的是RTOS选型和配置,说白了就是给窗帘电机找个合适的“大脑操作系统”。我这些年做嵌入式项目,选错RTOS的坑踩过不少,今天把经验掰开揉碎了讲给你听。

2.1 FreeRTOS vs RT-Thread:怎么选?

先说说这两个主流RTOS。我个人习惯是,先看项目资源再选型,别盲目跟风。

对比项 FreeRTOS RT-Thread
内核大小 极小,ROM 4-9KB 标准版约20KB,nano版约3KB
任务数量 无硬限制,但建议≤32 无硬限制,支持动态创建
生态组件 少,需自己移植 丰富,有设备驱动框架
学习曲线 平缓,文档清晰 稍陡,但中文资料多
商业许可 MIT,无额外费用 Apache 2.0,商业友好

我在项目中遇到过这样的情况:一个窗帘电机项目,MCU是STM32F030,Flash只有64KB。当时团队有人坚持用RT-Thread标准版,结果编译完固件直接超了30%。后来换成FreeRTOS,内核只占了不到8KB,剩下的空间全给业务逻辑了。

我的建议是:

  • 如果MCU资源紧张(Flash < 128KB,RAM < 20KB),优先选FreeRTOS
  • 如果项目需要网络、文件系统等复杂功能,RT-Thread的组件生态能省很多事
  • 如果团队都是新手,FreeRTOS的文档和社区支持更友好
小技巧: 别只看内核大小。RT-Thread的nano版其实也能裁剪到3KB左右,但需要你手动去掉不需要的组件。我一般先评估项目需要的功能列表,再决定用哪个。

2.2 任务优先级设计:别让电机“饿死”

任务优先级设计,说白了就是决定“谁先干活”。窗帘电机系统里,任务优先级设计不好,轻则响应迟钝,重则电机失控。

我一般把任务分成三类:

  • 硬实时任务: 比如电机堵转检测、限位开关响应。优先级最高,必须立即处理
  • 软实时任务: 比如窗帘位置控制、速度调节。优先级中等,允许少量延迟
  • 后台任务: 比如按键扫描、状态上报。优先级最低,有空再做

举个例子,一个典型的窗帘电机系统任务优先级分配:

任务名称 优先级 说明
电机堵转检测 5(最高) 一旦堵转,必须在1ms内停止电机
限位开关中断处理 4 到达限位立即停止,防止机械损坏
窗帘位置控制 3 根据目标位置计算PWM占空比
按键扫描 2 20ms扫描一次,允许抖动
Wi-Fi状态上报 1(最低) 空闲时上报,延迟几秒无所谓

避坑指南: 我曾经犯过一个错误——把按键扫描任务的优先级设得比电机控制还高。结果用户按一下按键,电机就抖一下,因为按键任务抢占了电机控制的CPU时间。后来我把按键任务降到最低优先级,问题就解决了。

注意: 优先级不要超过RTOS支持的最大值。FreeRTOS默认支持5-32级,RT-Thread支持256级。但实际项目中,我建议优先级数量控制在8级以内,否则调度开销会明显增加。

2.3 内核裁剪:给RTOS“瘦身”

内核裁剪,说白了就是去掉你用不到的功能。我见过不少项目,RTOS里一堆功能开着,结果Flash和RAM都浪费了。

以FreeRTOS为例,常用的裁剪配置项:

// FreeRTOSConfig.h 裁剪示例
#define configUSE_PREEMPTION          1   // 抢占式调度,必须开
#define configUSE_IDLE_HOOK           0   // 空闲钩子,不用就关
#define configUSE_TICK_HOOK           0   // 时钟钩子,不用就关
#define configUSE_CO_ROUTINES         0   // 协程,现在基本不用
#define configUSE_MUTEXES             1   // 互斥量,需要就开
#define configUSE_RECURSIVE_MUTEXES   0   // 递归互斥量,不用就关
#define configUSE_COUNTING_SEMAPHORES 0   // 计数信号量,不用就关
#define configUSE_QUEUE_SETS          0   // 队列集,不用就关
#define configUSE_TIMERS              1   // 软件定时器,需要就开
#define configUSE_TASK_NOTIFICATIONS  1   // 任务通知,轻量级通信

我个人的经验是:

  • 先开所有功能,编译看看ROM和RAM占用
  • 然后逐个关闭不需要的功能,每关一个就重新编译对比
  • 最后保留最小功能集

举个例子,窗帘电机系统里,我一般会关掉:

  • 协程(现在没人用这个了)
  • 递归互斥量(电机控制不需要嵌套锁)
  • 计数信号量(用队列替代更简单)
  • 队列集(多个队列同步,窗帘系统用不到)

这样裁剪下来,FreeRTOS的内核可以从9KB降到5KB左右。你想想看,省下来的4KB Flash,够放多少电机控制算法了?

小技巧: 裁剪后一定要做功能测试。我遇到过裁剪掉某个功能后,任务调度突然变慢的情况。后来发现是裁剪了configUSE_TICKLESS_IDLE,导致低功耗模式下的时钟节拍处理出了问题。所以,每裁剪一个功能,就跑一遍所有任务,确保没问题。

2.4 实际配置案例:一个窗帘电机项目的RTOS配置

最后,给你看一个我实际用过的配置。这个项目用的是FreeRTOS,MCU是STM32G030,Flash 64KB,RAM 8KB。

// FreeRTOSConfig.h 实际项目配置
#define configCPU_CLOCK_HZ            ( ( unsigned long ) 64000000 )
#define configTICK_RATE_HZ            ( ( TickType_t ) 1000 )  // 1ms时钟节拍
#define configMAX_PRIORITIES          ( 6 )                     // 6级优先级
#define configMINIMAL_STACK_SIZE      ( ( unsigned short ) 128 )
#define configTOTAL_HEAP_SIZE         ( ( size_t ) ( 4 * 1024 ) ) // 4KB堆

// 任务栈大小分配
#define TASK_STACK_MOTOR_CONTROL      ( 256 )   // 电机控制任务
#define TASK_STACK_KEY_SCAN           ( 128 )   // 按键扫描任务
#define TASK_STACK_WIFI_REPORT        ( 256 )   // Wi-Fi上报任务

// 裁剪选项
#define configUSE_PREEMPTION          1
#define configUSE_IDLE_HOOK           0
#define configUSE_TICK_HOOK           0
#define configUSE_CO_ROUTINES         0
#define configUSE_MUTEXES             1
#define configUSE_RECURSIVE_MUTEXES   0
#define configUSE_COUNTING_SEMAPHORES 0
#define configUSE_QUEUE_SETS          0
#define configUSE_TIMERS              1
#define configUSE_TASK_NOTIFICATIONS  1
#define configUSE_TRACE_FACILITY      0   // 调试跟踪,发布版关掉
#define configUSE_STATS_FORMATTING_FUNCTIONS 0

嗯,这里要注意:堆大小4KB看起来很小,但实际够用。因为窗帘电机系统里,动态内存分配主要用在创建任务和队列上,一旦系统启动完成,就不再动态分配了。所以4KB堆完全够用。

最后总结一下:RTOS选型看资源,优先级设计看实时性,内核裁剪看需求。这三步走对了,你的窗帘电机系统就能跑得又稳又快。