3、RTOS选型与移植:FreeRTOS vs RT-Thread 对比、任务划分原则、在STM32上的移植步骤与踩坑记录

做嵌入式电梯控制,选RTOS是绕不开的一步。裸机跑?说实话,电梯这种多任务、强实时的系统,裸机根本扛不住。你想想看,一个按键扫描还没处理完,门控信号就丢了,那还得了?

我个人习惯,在项目初期就把RTOS定下来。选型不对,后面移植起来全是泪。今天咱们就聊聊FreeRTOS和RT-Thread怎么选,任务怎么切,以及我在STM32上移植时踩过的那些坑。

3.1 FreeRTOS vs RT-Thread:到底选哪个?

很多新手问我:“这两个哪个好?” 我的回答是:没有绝对的好,只有合不合适。

先看一张对比表,心里有个底:

对比项 FreeRTOS RT-Thread
内核大小 极小,ROM 4-9KB 标准版约 10KB+
任务数量 无限制(受内存限制) 无限制
实时性 优秀,抢占式调度 优秀,支持抢占+时间片
组件生态 较少,需自己移植 丰富,有设备框架、AT组件等
学习曲线 平缓,API简洁 稍陡,但功能强大
商业许可 MIT,商用友好 Apache 2.0,商用友好

我个人习惯怎么选?看项目复杂度。

  • 如果只是简单的电梯控制面板,几个任务来回切换,FreeRTOS就够了。轻量、稳定,我用了好多年没出过问题。
  • 如果要做带物联网、人机交互的智能电梯,我建议上RT-Thread。它的设备框架和软件包生态,能省下大量开发时间。

我记得有一次,一个客户非要在一个资源紧张的STM32F103上跑RT-Thread完整版。结果呢?内存爆了。后来我给他换成了FreeRTOS,一切顺畅。所以,选型时一定要看硬件资源,别盲目追新。

我的小建议: 如果项目周期紧、团队对RTOS不熟,优先选FreeRTOS。如果项目需要长期维护、功能迭代多,RT-Thread更合适。

3.2 任务划分原则:电梯控制怎么切?

任务划分是RTOS设计的核心。切得太粗,实时性差;切得太细,上下文切换开销大。说白了,就是找平衡点。

在电梯控制中,我一般按功能模块来划分任务:

  1. 主控任务:处理电梯运行逻辑,比如楼层判断、方向决策。优先级最高。
  2. 门控任务:控制开关门,检测门状态。优先级中等。
  3. 按键扫描任务:读取内外呼梯按钮。优先级较低。
  4. 显示刷新任务:更新楼层显示、方向箭头。优先级最低。
  5. 通信任务:处理与上位机或物联网模块的通信。优先级中等。

你可能会问:“为什么按键扫描优先级要低?” 嗯,这里要注意。按键是人按的,人的反应速度在100ms级别。而门控信号是毫秒级的,必须优先响应。优先级分配,要按实时性需求来排。

我曾经在一个项目中,把按键扫描优先级设高了。结果呢?电梯运行时,按键中断频繁触发,导致门控任务被抢占,门关到一半又打开了。乘客吓一跳,我也被骂了一顿。从那以后,我定了个规矩:与人交互的任务,优先级永远低于与安全相关的任务

踩坑提醒: 任务优先级不要超过5级。优先级太多,调度器开销会变大,反而影响实时性。我一般只用3-4级。

3.3 在STM32上的移植步骤

移植RTOS,说白了就是把内核代码放到你的工程里,然后配置好时钟和中断。以FreeRTOS移植到STM32F4为例,我总结了几步:

第一步:准备源码

去官网下载FreeRTOS源码。我习惯只拿核心文件:

  • tasks.cqueue.clist.ctimers.c
  • 对应的头文件
  • 移植层文件:port.cportmacro.h

第二步:配置系统时钟

FreeRTOS需要一个滴答定时器(SysTick)来产生系统节拍。在STM32上,我一般这样配置:

// 在 FreeRTOSConfig.h 中配置
#define configCPU_CLOCK_HZ        ( ( unsigned long ) 168000000 )  // STM32F4主频
#define configTICK_RATE_HZ        ( ( TickType_t ) 1000 )         // 1ms一个节拍
#define configMAX_PRIORITIES      ( 5 )                           // 最大优先级数

这里有个坑:configCPU_CLOCK_HZ一定要和实际主频一致。我曾经因为写错了数值,导致系统节拍快了10倍,任务调度全乱套。

第三步:实现中断处理

stm32f4xx_it.c中,添加SysTick中断处理:

void SysTick_Handler(void)
{
    if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
    {
        xPortSysTickHandler();
    }
}

注意:调度器启动前,不要调用xPortSysTickHandler(),否则会死机。我刚开始移植时没注意这个,调试了一整天。

第四步:创建任务并启动调度

main()中,先初始化硬件,再创建任务,最后启动调度器:

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    xTaskCreate(vTaskMainControl, "MainCtrl", 256, NULL, 4, NULL);
    xTaskCreate(vTaskDoorControl, "DoorCtrl", 128, NULL, 3, NULL);
    xTaskCreate(vTaskKeyScan,     "KeyScan",  128, NULL, 2, NULL);

    vTaskStartScheduler();  // 启动调度器

    // 正常情况下不会执行到这里
    while(1);
}
关键点: 任务栈大小要合理。我一般给主控任务256字,其他任务128字。太小会栈溢出,太大浪费内存。调试时可以用 uxTaskGetStackHighWaterMark() 查看栈使用情况。

3.4 踩坑记录:那些年我掉过的坑

移植RTOS,看起来简单,实际上一堆坑。我挑几个典型的说说:

  • 坑一:中断优先级配置。FreeRTOS要求中断优先级分组为4位抢占优先级(即NVIC_PriorityGroup_4)。如果不配置,系统会跑飞。我曾经在移植时忘了这步,结果一进中断就死机。
  • 坑二:临界区嵌套。在中断中调用FreeRTOS的API,必须使用FromISR结尾的函数。我刚开始不知道,直接在中断里调xQueueSend(),结果队列数据全乱了。
  • 坑三:堆大小设置configTOTAL_HEAP_SIZE设得太小,任务创建失败。设得太大,内存浪费。我一般先设个2KB,调试时用xPortGetFreeHeapSize()查看剩余堆,再调整。

我记得有一次,项目交付前夜,电梯控制板突然死机。排查了一整晚,最后发现是任务栈溢出,导致内存被踩。从那以后,我每个任务都会加上栈溢出检测钩子函数:

void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
    // 记录错误日志,然后复位系统
    printf("Stack overflow in task: %s\n", pcTaskName);
    NVIC_SystemReset();
}

这个钩子函数,关键时刻能救命。

我的经验: 移植完成后,先跑一个简单的点灯任务,确认调度器正常工作。然后再逐步添加其他任务。别一上来就全堆上去,出了问题很难定位。

好了,关于RTOS选型与移植,今天就聊到这儿。下一章咱们讲讲电梯控制中的通信协议设计,那又是另一番天地了。