一、为什么要用RTOS?

RTOS 通常比通用操作系统体积更小、重量更轻,因此 RTOS 非常适用于 内存、计算和功率受限的设备。

Why use an RTOS?

You do not need to use an RTOS to write good embedded software. At some point though, as your application grows in size or complexity, the services of an RTOS might become beneficial for one or more of the reasons listed below. These are not absolutes, but opinion. As with everything else, selecting the right tools for the job in hand is an important first step in any project.

英文太痛了直接看翻译吧,想看原文的话去这个网站看看FreeRTOS FAQ - What is This All About? - FreeRTOS™

image-20251017165950797

英语不好的同学来和我看翻译,这里给大家推荐一个好用的网页翻页助手,适用于PC端沉浸式翻译 - 新一代AI翻译软件 | 双语对照网页翻译/PDF翻译/视频字幕翻译/漫画&图片翻译

image-20251017170031008

总结一下:

1、可以简化程序代码;

2、提高可维护性和可扩展性

3、

image-20251017171538587

这里主播没有看懂,于是请教了一下豆书记(豆包)

这句话的核心是讲 “应用程序该怎么拆分成‘任务’才合理”—— 核心思路是 “按功能拆成独立任务,让任务自己顺次跑完,别为了‘快结束’把简单功能拆得支离破碎”。用通俗的例子和概念拆解,就能轻松理解:

先拆 3 个关键概念:自主任务的核心要求

要理解这句话,先搞懂 “自主任务” 该满足的 3 个条件,这是基础:

  1. 功能上有凝聚力一个任务只干一件 “完整的小事”,比如 “读取温湿度传感器数据”“把数据上传到云端”“在屏幕上显示数据”—— 每个任务的功能单一且完整,不混杂其他无关逻辑。

  2. 松散耦合任务之间不 “紧绑”,比如 “读传感器” 和 “传数据” 之间,只需要 “读” 的任务把数据传给 “传” 的任务(比如通过一个共享变量),不用互相管对方的内部逻辑(“读” 不用关心 “传” 是用 WiFi 还是蓝牙,“传” 也不用关心 “读” 是怎么从硬件里拿数据的)。

  3. 内部顺序执行每个任务自己的步骤是 “一步接一步顺次跑”,比如 “读传感器” 的任务,就是 “启动传感器→等待数据→读取数值→校验数据” 按顺序做完,中间不随便中断或拆分。

再用 “智能家居数据采集” 的例子理解

假设你要写一个 “智能家居温湿度监控” 的程序,核心功能是 “读传感器→处理数据→传云端→显屏幕”。我们对比两种写法,就能明白这句话的意思:

错误写法:没按 “自主任务” 拆,反而拆成 “迷你状态机”

 如果担心 “读传感器 + 处理数据” 这个过程太长,影响其他功能(比如屏幕显示不能卡),就把它拆成 “迷你状态机”—— 把一个完整功能拆成多个 “碎片化小步骤”,每次只跑一步,频繁切换:
 状态 1:只 “启动传感器”(跑 10ms 就暂停);
 状态 2:等其他任务跑完,再 “等待传感器数据”(跑 5ms 又暂停);
 状态 3:再等一轮,“读取传感器数值”(跑 8ms 暂停);
 状态 4:最后 “校验数据”(跑 6ms)。
 结果就是:一个简单的 “读传感器” 功能,被拆成 4 个零散状态,需要频繁记录 “现在跑到哪一步了”,还得和其他任务抢时间,代码又乱又难维护 —— 这就是这句话说的 “不用拆成迷你状态机” 的原因。

正确写法:按 “自主任务” 拆,每个任务顺次执行

 把程序拆成 4 个自主任务,每个任务完整顺次跑,反而更简单:
 任务 1:读传感器数据(功能凝聚:只负责 “拿数据”)
 内部顺序执行:启动传感器→等待数据→读取数值→校验数据→把数据存到共享变量,然后自己 “休息”,等下一轮再跑。
 任务 2:处理数据(功能凝聚:只负责 “转格式”)
 内部顺序执行:从共享变量拿数据→把 “原始数值” 转成 “摄氏度 + 百分比湿度”→存到新的共享变量。
 任务 3:上传云端(功能凝聚:只负责 “传数据”)
 内部顺序执行:拿处理后的数据→连 WiFi→发请求→等服务器确认。
 任务 4:显示屏幕(功能凝聚:只负责 “显数据”)
 内部顺序执行:拿处理后的数据→清屏幕→画数字→刷新显示。
 这 4 个任务松散耦合(只通过共享变量传数据,互不干涉内部),每个任务自己顺次跑完 —— 哪怕 “上传云端” 因为网络慢,多跑了几百毫秒,也不用拆成 “连 WiFi→暂停→发请求→再暂停” 的状态机,因为它是独立任务,跑完自己的流程就行,不影响其他任务的逻辑,代码也清晰易维护。

让我想到了之前听到的一句话,写代码应该要高内聚低耦合

高内聚低耦合

“高内聚低耦合” 是软件设计的黄金法则,和你之前理解的 “自主任务拆分” 本质完全一致 —— 核心是让代码 “好维护、好修改、好扩展”。用 “模块”(可以是函数、类、任务,甚至整个系统)作为讨论对象,一句话就能说清:

高内聚 = 一个模块只干一件事,且把这件事干完整;低耦合 = 模块之间尽量少依赖,即使依赖也只通过简单方式沟通

一、先懂 “高内聚”:模块要 “专一且完整”

“内聚” 描述的是模块内部的紧密程度—— 模块里的代码是不是都围绕同一个目标服务,有没有混杂无关逻辑。

1. 高内聚的核心:“一事一责”

就像公司里的部门分工:

  • 行政部只负责 “办公用品采购、考勤管理”(高内聚),不跑去帮财务部做报表、帮技术部写代码;

  • 如果行政部既管考勤,又管财务报销,还管产品设计(低内聚),不仅效率低,出了问题也找不到具体负责人。

2. 代码例子:从 “低内聚” 到 “高内聚”

以之前的 “智能家居温湿度监控” 为例,对比两种写法:

  • 低内聚反例(一个函数干所有事):

     // 一个函数又读传感器、又处理数据、又上传云端、又显示——逻辑混杂,低内聚
     void do_everything() {
       // 1. 读传感器(硬件操作)
       float temp = sensor_read(); 
       // 2. 处理数据(格式转换)
       char temp_str[20];
       sprintf(temp_str, "温度:%.1f℃", temp); 
       // 3. 上传云端(网络操作)
       wifi_send(temp_str); 
       // 4. 显示屏幕(UI操作)
       lcd_show(temp_str); 
     }

    问题:想改 “上传云端” 的逻辑(比如从 WiFi 改成蓝牙),必须动这个大函数;想单独测试 “数据处理”,也得先跑通读传感器、上传等无关步骤 —— 维护成本极高。

  • 高内聚正例(拆成 4 个专一函数):

     // 1. 只负责“读传感器”——高内聚
     float sensor_read_task() {
       return sensor_get_raw_data(); // 只干“拿原始数据”这一件事
     }
     ​
     // 2. 只负责“处理数据”——高内聚
     char* data_process_task(float raw_temp) {
       static char temp_str[20];
       sprintf(temp_str, "温度:%.1f℃", raw_temp); // 只干“格式转换”
       return temp_str;
     }
     ​
     // 3. 只负责“上传云端”——高内聚
     void cloud_upload_task(char* data) {
       wifi_send(data); // 只干“传数据”
     }
     ​
     // 4. 只负责“显示”——高内聚
     void lcd_show_task(char* data) {
       lcd_refresh(data); // 只干“显数据”
     }

    好处:改上传逻辑只动cloud_upload_task,测试数据处理只调用data_process_task—— 每个模块目标明确,出问题能快速定位。

二、再懂 “低耦合”:模块间要 “少依赖、弱关联”

“耦合” 描述的是模块之间的依赖程度—— 一个模块的改动,会不会让其他模块跟着改;模块之间要不要知道对方的内部细节。

1. 低耦合的核心:“只传必要信息,不探他人隐私”

还是用公司部门举例:

  • 行政部要报销,只需要把 “报销单” 交给财务部(低耦合),不用知道财务部是怎么记账、怎么跟银行对接的;

  • 如果行政部要管财务部的记账软件操作、还要帮着核对银行流水(高耦合),财务部改了记账规则,行政部的工作也得跟着变 —— 牵一发而动全身。

2. 代码例子:从 “高耦合” 到 “低耦合”

还是用 “传温度数据” 举例,看模块间怎么沟通:

  • 高耦合反例(模块直接操作对方内部变量):

 // 模块A(读传感器):定义全局变量存数据
 float g_raw_temp; 
 void sensor_read_task() {
   g_raw_temp = sensor_get_raw_data(); 
 }
 ​
 // 模块B(处理数据):直接操作模块A的全局变量
 char* data_process_task() {
   static char temp_str[20];
   // 直接用模块A的全局变量——耦合高!模块A改了变量名,模块B就崩了
   sprintf(temp_str, "温度:%.1f℃", g_raw_temp); 
   return temp_str;
 }
  • 问题:模块 A 如果把g_raw_temp改名为g_sensor_temp,模块 B 的代码直接报错;模块 A 如果以后不存全局变量,模块 B 就得彻底重写 —— 依赖太强。

  • 低耦合正例(模块通过 “参数 / 接口” 传数据,不碰内部):

    // 模块A(读传感器):只返回数据,不暴露内部变量
     float sensor_read_task() {
       float raw_temp = sensor_get_raw_data(); // 内部变量,不对外暴露
       return raw_temp; // 只传必要的结果
     }
     ​
     // 模块B(处理数据):通过参数接收数据,不管模块A怎么来的
     char* data_process_task(float raw_temp) { // 用参数接收,不依赖模块A的内部
       static char temp_str[20];
       sprintf(temp_str, "温度:%.1f℃", raw_temp);
       return temp_str;
     }
     ​
     // 调用时:模块A的结果传给模块B,互不干涉
     void main() {
       float temp = sensor_read_task();
       char* show_data = data_process_task(temp);
     }

    好处:模块 A 不管是从 “硬件传感器” 还是 “模拟数据” 里拿的raw_temp,模块 B 都不用管;模块 A 改内部逻辑(比如加数据校验),模块 B 一行代码都不用动 —— 依赖极弱。

三、高内聚和低耦合的关系:相辅相成,缺一不可

高内聚是 “做好自己”,低耦合是 “友好待人”—— 两者结合才能让代码真正好维护:

  • 只高内聚,不低耦合:每个模块都很专一,但模块间互相紧绑(比如都用全局变量),改一个模块还是会影响一堆;

  • 只低耦合,不高内聚:模块间依赖少,但每个模块都干一堆杂事,改一个功能还是要翻遍整个模块;

  • 只有 “高内聚 + 低耦合”:模块专一(好定位问题)、依赖弱(好修改扩展)—— 比如想把 “上传云端” 改成 “上传到本地服务器”,只需要改cloud_upload_task一个模块,其他读传感器、处理数据、显示的模块完全不动。

最后总结:一句话记住核心

高内聚 = 模块是 “专才”(只干一件事),不是 “全才”(啥都干);低耦合 = 模块间是 “合作关系”(只传必要信息),不是 “连体婴”(互相插足内部)。这和你之前理解的 “自主任务拆分”(功能凝聚、松散耦合)完全是同一个逻辑 —— 写代码时照着这个原则拆模块,不管代码量多大,都能保持清晰。

在文章的结尾我再放上豆书记给的《高内聚低耦合代码设计 Checklist》

二、开始尝试

视频教程我看的是keysking的FreeRTOS【【keysking】全网最好的FreeRTOS教程!】https://www.bilibili.com/video/BV1mhWqzFEzn?vd_source=04ec85bac321be52c18d0e8083e4c6bc

先来让我们从点亮一个小灯开始(这里我是使用keil mdk + cubemax开发)

1、cubemax开始配置

image-20251017174727984

image-20251017174841830

image-20251017175048491

image-20251017175244770

或许你现在会有疑问,为什么要选这个CMSIS_V2,这个CMSIS是什么? -----让我们往下看

一、CMSIS 是什么?

CMSIS(Cortex Microcontroller Software Interface Standard)是 ARM 公司制定的微控制器软件接口标准,本质是一套 “硬件抽象层(HAL)”,作用是:

  1. 统一软件接口:让不同芯片厂商(如 ST、NXP、TI)生产的 Cortex 内核芯片(如 STM32、Kinetis),能使用相同的代码逻辑操作内核和外设(比如配置 GPIO、中断),解决 “同内核不同芯片软件移植难” 的问题。

  2. 简化开发流程:提供标准化的 API(如内核访问、中断管理、DSP 库、RTOS 接口),开发者无需深入研究芯片手册的寄存器细节,直接调用通用函数即可。

用一个生活化的例子理解:

把 Cortex 芯片比作 “手机”:

  • 内核 = 手机的 “CPU 芯片”(比如都是骁龙 8 Gen3),负责核心计算,所有用这款 CPU 的手机,计算逻辑、基础指令是一样的。

  • 片上外设 = 手机的 “摄像头、充电接口、按键” 等外围硬件,不同品牌(如小米、华为、苹果)会用不同型号的摄像头、不同位置的按键,甚至不同的充电协议。

这时候,如果写一个 “控制摄像头拍照” 的软件:

  • 小米手机的摄像头需要按 “指令 A” 启动(比如寄存器地址 0x1234);

  • 华为手机的同型号 CPU 手机,摄像头可能需要按 “指令 B” 启动(寄存器地址 0x5678)。

即使两款手机的 CPU(内核)完全相同,这个 “拍照软件” 也不能直接从小米手机复制到华为手机上用 —— 因为操作外围硬件(外设)的具体方式不一样,这就是 “软件移植困难”。

回到 Cortex 芯片的具体例子:

假设两款芯片都是Cortex-M4 内核(内核相同),分别来自 ST(STM32F407)和 NXP(MK60DN512),现在要写一段 “让 LED 灯闪烁” 的代码(LED 接在 GPIO 上,GPIO 是典型的片上外设):

1. 操作 STM32F407 的 GPIO(外设):

要让 PA5 引脚输出高低电平,需要操作 STM32 的 GPIOA 寄存器:

  • GPIOA 的 “模式寄存器” 地址是0x40020000,需设置为输出模式;

  • GPIOA 的 “输出数据寄存器” 地址是

     0x40020014,写 1 让 PA5 输出高电平,写 0 输出低电平。

    代码可能是:

 // STM32F407的GPIO初始化
 *(volatile uint32_t*)0x40020000 |= (1 << 10);  // 设置PA5为输出模式
 // 闪烁逻辑
 while(1) {
   *(volatile uint32_t*)0x40020014 |= (1 << 5);  // PA5输出高电平(LED亮)
   delay(1000);
   *(volatile uint32_t*)0x40020014 &= ~(1 << 5); // PA5输出低电平(LED灭)
   delay(1000);
 }
2. 操作 NXP MK60DN512 的 GPIO(外设):

同样是让 PA5 引脚闪烁,但 NXP 的 GPIO 寄存器设计完全不同:

  • GPIOA 的 “方向寄存器” 地址是0x400FF000,需设置为输出;

  • GPIOA 的 “输出寄存器” 地址是

     0x400FF00C,写 1 让 PA5 输出高电平。

    代码必须改成:

 // NXP MK60的GPIO初始化
 *(volatile uint32_t*)0x400FF000 |= (1 << 5);  // 设置PA5为输出模式(注意寄存器地址和位定义都变了)
 // 闪烁逻辑
 while(1) {
   *(volatile uint32_t*)0x400FF00C |= (1 << 5);  // PA5输出高电平(寄存器地址变了)
   delay(1000);
   *(volatile uint32_t*)0x400FF00C &= ~(1 << 5); // PA5输出低电平
   delay(1000);
 }
问题的核心:

两款芯片的Cortex-M4 内核完全相同(计算逻辑、指令集一致),但GPIO 外设的寄存器地址、配置方式完全由厂商自主设计,导致操作 GPIO 的代码无法直接复用 —— 这就是 “同内核、不同外设的芯片上移植困难”。

CMSIS 标准的作用:

CMSIS 就像给所有厂商制定了一套 “外设操作说明书模板”:

  • 规定 “设置 GPIO 为输出” 必须提供GPIO_SetMode()函数;

  • 规定 “控制引脚高低电平” 必须提供GPIO_WritePin()函数。

无论 ST 还是 NXP,都要按照这个模板实现函数(内部细节厂商自己处理,但接口统一)。这样,上面的闪烁代码可以写成:

 // 符合CMSIS标准的代码(在STM32和NXP上通用)
 GPIO_SetMode(GPIOA, GPIO_PIN_5, GPIO_MODE_OUTPUT); // 统一接口,不用关心具体寄存器
 while(1) {
   GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);  // 统一接口
   delay(1000);
   GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
   delay(1000);
 }

此时,代码可以直接在同内核的不同芯片上移植,解决了兼容性问题。

cuebmax中会自动生成一个任务

image-20251017220638753

然后这里我们注意一下改一下时间基准---------------------从systick 切换到一个我们不常用的定时器(这里我选择定时器4)

image-20251017220916517

到这里可能就有人思考了,为什么平时我们都是用systick作为 Timebase Source,而现在要切换呢?

-----因为如果freeertos将systick中断作为是时钟基准,因而将Systick中断的优先级设置的比较低,甚至可能会关闭中断。

核心原因
1. SysTick冲突问题
 // 问题根源:两个系统都想用SysTick
 FreeRTOS: 使用SysTick作为任务调度的时钟源
 HAL库:    使用SysTick作为HAL_Delay()等函数的时基

当两者同时使用SysTick时会产生:

  • 中断优先级冲突

  • 时间基准不准确

  • 系统调度混乱

2. 具体冲突场景
 // FreeRTOS的SysTick中断处理
 void SysTick_Handler(void)
 {
     if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
     {
         xPortSysTickHandler();  // FreeRTOS调度
     }
     HAL_IncTick();  // HAL时基递增
 }

问题:

  • FreeRTOS需要最高优先级的SysTick来保证实时性

  • 但HAL_IncTick()也在同一个中断里执行

  • 可能导致HAL延迟函数不准确

总结
 平时裸机开发:
 SysTick → HAL_Delay()等函数  ✓ 没问题
 ​
 加入FreeRTOS后:
 SysTick → FreeRTOS任务调度   ← 最高优先级
 TIM6/7  → HAL_Delay()等函数  ← 较低优先级
               ↑
          必须分离!

本质原因:避免两个操作系统级的时钟源争抢同一个硬件定时器,导致调度混乱和时间不准确。

然后我们继续配置cubemax

image-20251017222016515

然后生成文件

然后我们打开keil MDK来分析一下新增的几个函数

image-20251018100104210

我们跳转到这个函数相关文件里面去 MX_FREERTOS_Init()

 app_freertos.c文件
 ​
 
/* USER CODE END Variables */
 /* Definitions for defaultTask */
 osThreadId_t defaultTaskHandle;
 const osThreadAttr_t defaultTask_attributes = {
   .name = "defaultTask",
   .priority = (osPriority_t) osPriorityNormal,
   .stack_size = 128 * 4
 };
 ​
 /* Private function prototypes -----------------------------------------------*/
 /* USER CODE BEGIN FunctionPrototypes */
 ​
 /* USER CODE END FunctionPrototypes */
 ​
 void StartDefaultTask(void *argument);
 ​
 void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */
 ​
 /**
   * @brief  FreeRTOS initialization
   * @param  None
   * @retval None
   */
 void MX_FREERTOS_Init(void) {
   /* USER CODE BEGIN Init */
 ​
   /* USER CODE END Init */
 ​
   /* USER CODE BEGIN RTOS_MUTEX */
   /* add mutexes, ... */
   /* USER CODE END RTOS_MUTEX */
 ​
   /* USER CODE BEGIN RTOS_SEMAPHORES */
   /* add semaphores, ... */
   /* USER CODE END RTOS_SEMAPHORES */
 ​
   /* USER CODE BEGIN RTOS_TIMERS */
   /* start timers, add new ones, ... */
   /* USER CODE END RTOS_TIMERS */
 ​
   /* USER CODE BEGIN RTOS_QUEUES */
   /* add queues, ... */
   /* USER CODE END RTOS_QUEUES */
 ​
   /* Create the thread(s) */
   /* creation of defaultTask */
   defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
 ​
   /* USER CODE BEGIN RTOS_THREADS */
   /* add threads, ... */
   /* USER CODE END RTOS_THREADS */
 ​
   /* USER CODE BEGIN RTOS_EVENTS */
   /* add events, ... */
   /* USER CODE END RTOS_EVENTS */
 ​
 }
 ​
 /* USER CODE BEGIN Header_StartDefaultTask */
 /**
   * @brief  Function implementing the defaultTask thread.
   * @param  argument: Not used
   * @retval None
   */
 /* USER CODE END Header_StartDefaultTask */
 void StartDefaultTask(void *argument)
 {
   /* USER CODE BEGIN StartDefaultTask */
   /* Infinite loop */
   for(;;)
   {
     osDelay(1);
   }
   /* USER CODE END StartDefaultTask */
 }

大家这里的代码要是和我不一样的话那可能是这里没有选cmsis v2

image-20251018100442996

CMSIS-RTOS v1 vs v2 的区别
API命名差异对照表
功能 CMSIS-RTOS v1 CMSIS-RTOS v2
任务创建 osThreadCreate() osThreadNew()
任务延迟 osDelay() osDelay() 相同
任务删除 osThreadTerminate() osThreadTerminate()
互斥量创建 osMutexCreate() osMutexNew()
信号量创建 osSemaphoreCreate() osSemaphoreNew()
消息队列 osMessageCreate() osMessageQueueNew()
你的代码解析
当前代码结构
 void MX_FREERTOS_Init(void) 
 {
     /* 创建默认任务 */
     defaultTaskHandle = osThreadNew(
         StartDefaultTask,           // 任务函数
         NULL,                       // 传递给任务的参数
         &defaultTask_attributes     // ← v2新增:任务属性结构体
     );
 }
 ​
 void StartDefaultTask(void *argument)
 {
     for(;;)
     {
         osDelay(1);  // 延迟1ms
     }
 }
v2 API的新特性:任务属性结构体
 // CubeMX会自动生成类似这样的代码
 const osThreadAttr_t defaultTask_attributes = {
     .name = "defaultTask",          // 任务名称
     .stack_size = 128 * 4,          // 栈大小(字节)
     .priority = osPriorityNormal,   // 优先级
 };
在 CMSIS-RTOS v2 中添加自定义任务
方法1:在CubeMX图形界面添加(推荐)
  1. 打开CubeMX项目

  2. 点击 Middleware → FREERTOS

  3. 点击 Tasks and Queues 标签

  4. 点击 Add 按钮

  5. 配置任务参数

     Task Name: MyTask
     Priority: Normal
     Stack Size: 128 words
     Entry Function: StartMyTask
  6. 重新生成代码

CubeMX会自动生成:

 /* Definitions for MyTask */
 osThreadId_t MyTaskHandle;
 const osThreadAttr_t MyTask_attributes = {
     .name = "MyTask",
     .stack_size = 128 * 4,
     .priority = osPriorityNormal,
 };
 ​
 void MX_FREERTOS_Init(void) 
 {
     /* 创建默认任务 */
     defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
 ​
     /* 创建自定义任务 */
     MyTaskHandle = osThreadNew(StartMyTask, NULL, &MyTask_attributes);
 }
 ​
 /* USER CODE BEGIN Header_StartMyTask */
 void StartMyTask(void *argument)
 {
     for(;;)
     {
         HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
         osDelay(500);
     }
 }
方法2:手动在用户代码区添加
 
/* USER CODE BEGIN Header */
 osThreadId_t MyTaskHandle;  // 在文件开头声明
 /* USER CODE END Header */
 ​
 void MX_FREERTOS_Init(void) 
 {
     /* USER CODE BEGIN RTOS_THREADS */
     
     // 定义任务属性
     const osThreadAttr_t MyTask_attributes = {
         .name = "MyTask",
         .stack_size = 128 * 4,
         .priority = osPriorityNormal,
     };
     
     // 创建任务
     MyTaskHandle = osThreadNew(StartMyTask, NULL, &MyTask_attributes);
     
     /* USER CODE END RTOS_THREADS */
 }
 ​
 /* USER CODE BEGIN Application */
 void StartMyTask(void *argument)
 {
     for(;;)
     {
         printf("My Task Running\n");
         osDelay(1000);
     }
 }
 /* USER CODE END Application */
完整示例:LED闪烁 + 串口打印
 /* USER CODE BEGIN Header */
 #include <stdio.h>
 ​
 osThreadId_t ledTaskHandle;
 osThreadId_t uartTaskHandle;
 /* USER CODE END Header */
 ​
 void MX_FREERTOS_Init(void) 
 {
     /* USER CODE BEGIN RTOS_THREADS */
     
     // LED任务
     const osThreadAttr_t ledTask_attributes = {
         .name = "LED_Task",
         .stack_size = 128 * 4,
         .priority = osPriorityNormal,
     };
     ledTaskHandle = osThreadNew(StartLedTask, NULL, &ledTask_attributes);
     
     // 串口任务
     const osThreadAttr_t uartTask_attributes = {
         .name = "UART_Task",
         .stack_size = 256 * 4,
         .priority = osPriorityNormal,
     };
     uartTaskHandle = osThreadNew(StartUartTask, NULL, &uartTask_attributes);
     
     /* USER CODE END RTOS_THREADS */
 }
 ​
 /* USER CODE BEGIN Application */
 ​
 void StartLedTask(void *argument)
 {
     for(;;)
     {
         HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
         osDelay(500);  // 500ms闪烁
     }
 }
 ​
 void StartUartTask(void *argument)
 {
     uint32_t count = 0;
     for(;;)
     {
         printf("UART Task: %lu\n", count++);
         osDelay(1000);  // 1秒打印一次
     }
 }
 ​
 /* USER CODE END Application */

除了MX_FREERTOS_Init()

还有osKernelInitialize()函数,他的主要作用是为--freertos对象调用init函数

image-20251018102101231

然后osThreadNew()这个函数在MX_FREERTOS_Init()函数内部,可以往上翻一下,主要作用就是创建一个新任务

然后是osKernelStart(),这个函数的作用是什么呢? osKernelStart()CMSIS-RTOS 中启动 FreeRTOS 内核的关键函数。它的作用是启动任务调度器,让所有创建的任务开始运行。

osKernelStart()
核心作用
 osKernelStart();
 // ↓ 执行后发生:
 // 1. FreeRTOS调度器启动
 // 2. 系统时钟开始计数
 // 3. 所有已创建的任务开始运行
 // 4. 主程序永不返回
执行流程
调用前(初始化阶段)
 int main(void)
 {
     HAL_Init();                    // ① HAL库初始化
     SystemClock_Config();          // ② 时钟配置
     MX_GPIO_Init();                // ③ GPIO初始化
     MX_USART1_UART_Init();         // ④ 串口初始化
     MX_FREERTOS_Init();            // ⑤ FreeRTOS初始化
     //   ├─ 创建任务
     //   ├─ 创建队列
     //   └─ 创建信号量等
     
     osKernelStart();               // ⑥ ← 启动内核!
     //
     // 以下代码永远不会执行!
     for(;;);
 }
调用后(运行阶段)
 osKernelStart() 调用后的时间轴:
 ​
 时间0ms:   FreeRTOS调度器启动
            ├─ SysTick中断处理配置
            ├─ 系统心跳开始
            └─ 第一次任务切换
 ​
 时间~100ms:
            Task1 运行 ──┐
            ↓           ├─ 时间片轮转
            Task2 运行 ──┤
            ↓           ├─ 或优先级调度
            Task3 运行 ──┘
            ↓
            ...一直循环...
 ​
 程序永不停止!
详细工作原理
1. 初始化前的状态
void MX_FREERTOS_Init(void)
 {
     // 创建了任务,但还没有运行
     osThreadNew(StartTask1, NULL, &attr1);  // Task1已创建,等待
     osThreadNew(StartTask2, NULL, &attr2);  // Task2已创建,等待
     osThreadNew(StartTask3, NULL, &attr3);  // Task3已创建,等待
     
     // 此时这些任务处于"Ready"状态,但不执行
 }
2. osKernelStart()做了什么
 osKernelStart();  // 内部实现类似:
 ​
 // 伪代码
 void osKernelStart(void)
 {
     // ① 配置SysTick定时器
     vPortSetupTimerInterrupt();
     
     // ② 设置PendSV中断(用于任务切换)
     NVIC_SetPriority(PendSV_IRQn, LOWEST_PRIORITY);
     
     // ③ 启用中断
     __enable_irq();
     
     // ④ 切换到第一个任务
     xPortStartScheduler();  // 永远不返回!
     
     // 以下代码永不执行
     return osOK;  // 不可能到达这里
 }
3. 启动后的运行状态
 // 启动前:
 Kernel State: NOT_STARTED
 Task1 State:  SUSPENDED (已创建,等待)
 Task2 State:  SUSPENDED
 Task3 State:  SUSPENDED
 ​
 // osKernelStart()之后:
 Kernel State: RUNNING  ← ← ← 内核开始运行
 Task1 State:  READY (可以运行)
 Task2 State:  READY
 Task3 State:  READY
 ​
 // 然后调度器开始工作:
 时间片0: Task1 RUN
 时间片1: Task2 RUN
 时间片2: Task3 RUN
 时间片3: Task1 RUN
 ...
实际代码示例
完整的main()函数
int main(void)
 {
     // 硬件初始化
     HAL_Init();
     SystemClock_Config();
     MX_GPIO_Init();
     MX_USART1_UART_Init();
     
     // FreeRTOS初始化(创建所有任务)
     MX_FREERTOS_Init();
     
     printf("System initialized, starting FreeRTOS kernel...\n");
     
     // 启动内核
     osKernelStart();  // ← 从这里开始,由FreeRTOS接管
     
     // 以下代码永不执行!
     printf("This will never be printed!\n");
     while(1);
 }
带初始化状态检查的版本
 
int main(void)
 {
     HAL_Init();
     SystemClock_Config();
     MX_GPIO_Init();
     
     // 检查内核初始化状态
     if(osKernelInitialize() == osOK)
     {
         printf("Kernel initialized successfully\n");
         
         MX_FREERTOS_Init();  // 创建任务
         
         // 检查内核启动状态
         if(osKernelStart() == osOK)
         {
             printf("Kernel started\n");  // 最后的可见信息
         }
     }
     
     // 都不会执行
     return 0;
 }
重要特性
1. 永不返回
 osKernelStart();  // ← 到这里就永不返回!
 ​
 // 不可能到达这里的代码:
 printf("Never printed");  // ✗
 while(1);                 // ✗
 return 0;                 // ✗
2. 检查内核状态
 void SomeTask(void *argument)
 {
     // 检查内核是否运行
     osKernelState_t state = osKernelGetState();
     
     if(state == osKernelRunning)
     {
         printf("Kernel is running\n");
     }
 }
3. 获取已运行时间
 void PrintSystemUptime(void)
 {
     uint32_t uptime = osKernelGetTickCount();  // 获取系统心跳数
     printf("System uptime: %lu ms\n", uptime);
 }
与osKernelInitialize()的区别
函数 作用 返回值 之后
osKernelInitialize() 初始化FreeRTOS内核 osOK/Error 内核就绪
osKernelStart() 启动任务调度器 永不返回 任务开始运行
 // 分开调用方式
 osKernelInitialize();  // 初始化
 MX_FREERTOS_Init();    // 创建任务
 osKernelStart();       // 启动(永不返回)
 ​
 // CubeMX简化方式(只需调用)
 MX_FREERTOS_Init();    // 内部会调用osKernelInitialize()和osKernelStart()
流程总结
 main()
   ↓
 硬件初始化(HAL_Init等)
   ↓
 MX_FREERTOS_Init()
   ├─ osKernelInitialize()
   ├─ 创建Task1、Task2、Task3
   └─ osKernelStart()  ← ← ← 永不返回
        ↓
    调度器启动
        ↓
    Task1/Task2/Task3轮流运行
        ↓
    一直运行到系统关闭

核心记忆:osKernelStart()是FreeRTOS中"启动按钮",按下后程序永不返回main,而是由调度器永远接管。

osKernelStart() 之后的代码都是死代码

       ↓

不会执行

       ↓

因为CPU已被FreeRTOS调度器完全接管

       ↓

 只有FreeRTOS任务会执行

       ↓

系统一直运行到关机为止

这也是为什么所有初始化代码都要在 osKernelStart() 之前完成。

2、现在让我们来点个灯

led选哪个引脚请大家根据自己单片机的原理图选择

image-20251018103523908

我就选个PC8

image-20251018104936343

image-20251018105028886

然后编译烧录我们就可以看到进入FreeRTOS的第一个小灯的闪烁了。

 3、思考:为什么FreeRTOS中要用osDelay而不用HAL_delay?

这是一个非常重要的问题!在FreeRTOS中**必须使用`osDelay()`而不能使用`HAL_Delay()`**,原因涉及多任务系统的核心机制。

核心区别对比

详细原理分析
1. HAL_Delay()的工作方式(忙等待)
void HAL_Delay(uint32_t Delay)
{
    uint32_t tickstart = HAL_GetTick();
    uint32_t wait = Delay;

    while ((HAL_GetTick() - tickstart) < wait)
    {
        ;  // ← 死循环空等待!CPU被占用
    }
}

问题:CPU被完全占用,其他任务无法运行


时间轴(使用HAL_Delay):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Task1: ████████████ HAL_Delay(1000) ████████████
                     ↑ CPU被占用,干等1秒
Task2:              【无法运行,被饿死】
Task3:              【无法运行,被饿死】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 

2. osDelay()的工作方式(任务挂起)
void osDelay(uint32_t ticks)
{
    vTaskDelay(ticks);  // 内部调用FreeRTOS API
}

// vTaskDelay内部逻辑(简化)
void vTaskDelay(TickType_t xTicksToDelay)
{
    // 1. 将当前任务状态改为"阻塞"
    // 2. 将任务从就绪列表移到延迟列表
    // 3. 触发任务切换,让出CPU
    // 4. 延迟时间到后自动恢复为"就绪"
}

优势:释放CPU,让其他任务运行**

时间轴(使用osDelay):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Task1: ███ osDelay(1000) 【挂起,释放CPU】
Task2:    ██████ 运行 ██████
Task3:          ████████ 运行 ████████
       ↑ 所有任务都有机会运行
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 

———————————————————————————————————————————

一些拓展(看完上面再看下面)

《高内聚低耦合代码设计 Checklist》

本清单聚焦 “可落地、可对照”,涵盖高内聚判断标准与低耦合实现方法,适用于函数、类、模块等不同代码层级,写代码 / 改 bug 时可直接对照检查。

一、判断模块是否 “高内聚” 的 3 个标准

标准 1:功能单一性 —— 模块只干 “一件完整的小事”

  • 判断要点:能否用 1 句简单的话描述模块的核心功能?如果需要用 “和 / 还 / 同时” 连接多个动作,大概率低内聚。

  • 反例:一个叫temp_sensor_do_all()的函数,同时负责 “读传感器原始数据 + 转换温度格式 + 上传云端”(含 3 个不同功能)。

  • 正例:拆成 3 个函数 ——temp_sensor_read()(仅读原始数据)、temp_data_convert()(仅转格式)、temp_data_upload()(仅上传)。

标准 2:逻辑关联性 —— 模块内代码 “都为同一个目标服务”

  • 判断要点:删除模块内任意一段代码后,是否会导致模块核心功能失效?如果有 “凑数” 的无关代码(比如在 “读传感器” 模块里加 “屏幕清屏” 逻辑),则低内聚。

  • 反例lcd_show_task()函数里,除了 “显示温度”,还夹杂 “检查 WiFi 连接状态” 的代码(WiFi 属于另一个模块的逻辑)。

  • 正例lcd_show_task()里只保留 “清屏→加载数据→刷新显示” 的代码,WiFi 检查交给wifi_check_task()处理。

标准 3:完整性 —— 模块能把 “一件事干到底”

  • 判断要点:模块是否覆盖了核心功能的全流程?不会把 “半成品” 丢给其他模块处理(除非是刻意设计的分步流程)。

  • 反例sensor_read()函数只读取传感器原始数据,不做 “数据校验”(比如判断数值是否在合理范围:-40℃~85℃),把校验逻辑丢给后续模块,导致后续模块要额外处理异常数据。

  • 正例sensor_read()函数内包含 “读原始数据→校验合理性→返回有效数据 / 报错” 的全流程,后续模块直接用结果即可。

二、降低模块 “耦合度” 的 4 个实用方法

方法 1:用 “参数 / 接口传值”,拒绝全局变量 / 全局函数

  • 核心逻辑:模块间传递数据,只通过 “函数参数” 或 “统一接口”,不直接访问对方的全局变量(全局变量是耦合的 “重灾区”)。

  • 反例:模块 A 定义float g_raw_temp;,模块 B 直接读取g_raw_temp计算,模块 A 改变量名后模块 B 直接崩。

  • 正例:模块 A 提供float sensor_get_temp()接口,模块 B 调用该接口获取数据(float temp = sensor_get_temp();),模块 A 内部变量怎么改,模块 B 都不用动。

方法 2:依赖 “抽象定义”,而非 “具体实现”

  • 核心逻辑:模块间尽量依赖 “通用规则”(比如函数指针、抽象类、接口),不依赖具体的函数或变量,方便后续替换实现。

  • 反例:模块 B 直接调用sensor_stm32_read()(特指 STM32 的传感器读取函数),后续换成 NXP 芯片时,模块 B 要全量修改。

  • 正例:定义抽象接口typedef float (*SensorReadFunc)();,模块 B 依赖SensorReadFunc类型的函数指针,模块 A 根据芯片类型赋值(SensorReadFunc read_func = sensor_stm32_read;read_func = sensor_nxp_read;),模块 B 只需调用read_func()即可。

方法 3:用 “中间层” 隔离,减少直接交互

  • 核心逻辑:如果两个模块需要频繁交互,不要让它们直接通信,新增 “中间层”(比如数据结构体、消息队列)承接,降低互相依赖。

  • 反例:模块 A(读传感器)和模块 B(显示)直接互相调用对方的函数(A调用B的lcd_update(),B调用A的get_temp()),改 A 的逻辑会影响 B,改 B 也会影响 A。

  • 正例:新增 “数据中间层”—— 定义typedef struct { float temp; bool is_valid; } TempData;,模块 A 把数据写入TempData,模块 B 从TempData读取,A 和 B 不再直接调用对方函数,互不影响。

方法 4:避免 “硬编码依赖”,用配置 / 注入实现灵活切换

  • 核心逻辑:模块间的依赖项(比如端口号、IP 地址、设备型号)不要 “写死在代码里”,用配置文件、宏定义或参数注入的方式,方便修改。

  • 反例:模块 A 的wifi_upload()函数里硬编码const char* server_ip = "192.168.1.100";,后续服务器 IP 变更,要改代码重新编译。

  • 正例:把 IP 地址放在配置结构体里(typedef struct { char server_ip[20]; } WifiConfig;),通过wifi_set_config(&config)注入,改 IP 只需改配置,不用动核心代码。

三、使用说明

  1. 设计阶段:拆分模块前,先对照 “高内聚 3 个标准”,确认每个模块的功能边界(比如 “这个模块到底干哪一件事?”)。

  2. 编码阶段:写代码时,用 “降低耦合 4 个方法” 约束交互方式(比如 “要不要用全局变量?换成参数传值会不会更好?”)。

  3. 复盘阶段:写完模块后,对照清单 “反向检查”—— 如果发现某条不满足,及时重构(比如模块里有无关代码,就抽成新模块;用了全局变量,就改成接口传值)。

如果在具体场景(比如嵌入式开发、C++ 类设计)中不知道怎么应用,随时可以拿具体代码片段来讨论,我帮你分析如何优化~

CMSIS-RTOS v2 常用API

任务管理

 // 创建任务
 osThreadId_t handle = osThreadNew(task_func, NULL, &attr);
 ​
 // 删除任务
 osThreadTerminate(handle);
 ​
 // 挂起任务(v2中使用osThreadSuspend)
 osThreadSuspend(handle);
 ​
 // 恢复任务
 osThreadResume(handle);
 ​
 // 延迟
 osDelay(1000);  // 延迟1000个系统节拍

互斥量

 // 创建互斥量
 osMutexId_t mutexHandle = osMutexNew(NULL);
 ​
 // 获取互斥量
 osMutexAcquire(mutexHandle, osWaitForever);
 ​
 // 释放互斥量
 osMutexRelease(mutexHandle);

信号量

 // 创建二值信号量
 osSemaphoreId_t semHandle = osSemaphoreNew(1, 1, NULL);
 ​
 // 获取信号量
 osSemaphoreAcquire(semHandle, osWaitForever);
 ​
 // 释放信号量
 osSemaphoreRelease(semHandle);

消息队列

 // 创建消息队列
 osMessageQueueId_t queueHandle = osMessageQueueNew(10, sizeof(uint32_t), NULL);
 ​
 // 发送消息
 uint32_t data = 123;
 osMessageQueuePut(queueHandle, &data, 0, 0);
 ​
 // 接收消息
 uint32_t received;
 osMessageQueueGet(queueHandle, &received, NULL, osWaitForever);

如果你看到这里,感谢你能耐心看完我的文章,如果有什么讲错的地方或者有什么讲得您觉得不够好的地方还请指点主播下

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐