RTOS-F429-HAL-基础内容认识(2026/7/26)

📅 2026/7/26 21:24:04 👁️ 阅读次数 📝 编程学习
RTOS-F429-HAL-基础内容认识(2026/7/26)

目录

一:rtos 和裸机的特点对比

1:裸机

2:RTOS

3:RTOS的四个特点:

4:两个注意事项

5:configMAX_SYSCALL_INTERRUPT_PRIORITY

6:configUSE_TIME_SLICING

7:vTaskDelay不能在中断里调用:

二:中断优先级 vs 任务优先级

1:两个世界,两套规则

2:硬件中断永远优先于任务

3:中断内的一根分界线

4:为什么 0~4 不能用

5:调 FreeRTOS API 是什么意思

三:FreeRTOS 三种调度方式

1:定义

2“抢占式

3:时间片

4:协程式

5:实际只会用到抢占+时间片

四:任务的运行状态

1:四种状态

2:状态切换

3:三个列表

4:一句话

五:FreeRtosConfig.H

1:PendSV_Handler中断做什么?

2:SVC_Handler中断做什么?

3:上电后执行流程

4: 为什么 PendSV 不直接在 SysTick 中断里切


一:rtos 和裸机的特点对比

RTOS全称为:Real Time OS,就是实时操作系统,强调的是:实时性

*RTOS 四大特点**:

  1. 分而治之:实现功能划分为多个任务。

  2. 延时函数:任务调度。

  3. 抢占式:高优先级任务抢占低优先级任务。

  4. 任务堆栈:每个任务都有自己的栈空间。

注意事项

  • 注意1:中断可以打断任意任务。

  • 注意2:任务可以同等优先级。

1:裸机

main() { Init(); while(1) { 任务A(); // 跑完 任务B(); // 才轮到 任务C(); // 最后 } }

特点:一行等一行。任务 A 里有个HAL_Delay(1000),B 和 C 就得跟着等 1 秒。CPU 全速空转,什么也干不了。

2:RTOS

main() { Init(); xTaskCreate(任务A); xTaskCreate(任务B); xTaskCreate(任务C); vTaskStartScheduler(); ← 永不返回 }

特点:调度器在中间切。任务 A 调vTaskDelay(1000)→ 调度器立刻把 CPU 给 B → B 跑一阵也 delay → 调度器给 C。1 秒到了,A 被按时唤醒。

3:RTOS的四个特点:

1. 分而治之✅ 没错。一个大 while(1) 拆成多个独立任务,每个任务只管自己的事。

2. "延时函数 = 任务调度"不准确。调用vTaskDelay不是调用调度器——调度器不靠延时函数触发,是靠 SysTick 中断每次 1ms 检查有没有任务到期。vTaskDelay干的事是"把我挂起来,让调度器下次扫到我时再唤醒"。任务切换是 PendSV 中断干的,不是 delay 函数。

3. 抢占式✅ 没错。SysTick 中断发现高优先级任务到期 → 立马切过去,正在跑的低优先级任务被"抢走 CPU"。

4. 任务堆栈✅ 没错。每个任务创建时xTaskCreate的第三个参数就是栈大小(单位 word)。A 和 B 各自有栈,A 的局部变量不会串到 B 里去。


4:两个注意事项

中断可以打断任务✅ 但要补一句:不是所有中断都能调 FreeRTOS API。优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断,不能调xQueueSend这类 API,否则可能破坏调度器状态。

同优先级轮流跑✅ 补充:configUSE_TIME_SLICING=1时,同优先级任务每个 SysTick 心跳轮一次,实现时间片轮转。两个LedTask优先级一样 → 一人闪一下。

5:configMAX_SYSCALL_INTERRUPT_PRIORITY

控制:哪些中断能调 FreeRTOS API。

ARM Cortex-M 的优先级数字越小越高。FreeRTOS 用它画了一条分界线:

优先级 0 ──── 最高,紧急硬件故障 优先级 1 优先级 2 优先级 3 优先级 4 ────────── configMAX_SYSCALL_INTERRUPT_PRIORITY = 0x50 (= 5) ← 这里画线 优先级 5 ... 优先级 15 ─── 最低
线以上的 ISR(0~4)线以下的 ISR(5~15)
能调xQueueSendFromISR❌ 不能✅ 能
能调vTaskDelay❌ 不能❌ 不能(ISR 里本来就不能)
能被 FreeRTOS 关中断屏蔽❌ 不能✅ 能
适合放什么紧急故障处理、电机刹车CAN 中断、UART 中断、定时器

为什么线以上不能调?因为这些超高优先级中断不会被 FreeRTOS 临界区屏蔽,但 ISR 里调 API 可能改任务链表,调度器同时也在改 → 数据冲突。所以一刀切:线以上禁止调 API。


6:configUSE_TIME_SLICING

控制:同优先级任务要不要轮流跑。

configUSE_TIME_SLICING = 1(时间片轮转): ​ LedTask1 (优先级1): ██░░░░░░░░████░░░░░░░░ LedTask2 (优先级1): ░░████░░░░░░░░████░░░░ ↑每1ms心跳,两个TASK轮流上阵 谁也别想独占 CPU,每人一个 tick 就换。 ​ ​ configUSE_TIME_SLICING = 0(不轮转): ​ LedTask1 (优先级1): ████████████████████████ LedTask2 (优先级1): ░░░░ ↑只有上面的被vTaskDelay了,底下才有机会跑 先拿到 CPU 的就一直跑,除非自己主动让(vTaskDelay/等队列)。

开还是不开?工控一般开(1),时间片轮转让同优先级任务公平共享 CPU,避免一个任务因为没调 delay 就饿死别人。

7:vTaskDelay不能在中断里调用:

vTaskDelay的意思是"让当前任务睡 n 个 tick后再继续执行"。但 ISR 不是任务——它是硬件中断,CPU 暂停当前任务、冲进去处理紧急事件,处理完必须立刻退出。你让 ISR 里面 sleep,等于让消防员到了火场先喝杯茶。

所以 FreeRTOS 把 API 分两类:

场景用什么例子
任务里调普通 APIvTaskDelay()xQueueSend()
ISR 里调带 FromISR 后缀的xQueueSendFromISR()xSemaphoreGiveFromISR()

vTaskDelay根本没有FromISR版本——因为这件事在 ISR 里毫无意义。ISR 是来干活走的,不是来睡觉的。

二:中断优先级 vs 任务优先级

1:两个世界,两套规则

硬件中断优先级(NVIC)FreeRTOS 任务优先级
在哪设HAL_NVIC_SetPriority(IRQn, 抢占, 响应)xTaskCreate(..., priority, ...)
数字方向小 = 高优先级大 = 高优先级
为什么ARM 芯片架构规范人读代码 3>1 更直观
范围0~15(4 位抢占时)0 ~ configMAX_PRIORITIES-1(我们设置宏为32)
0 是什么最高优先级中断最低优先级任务(空闲任务)
最高值是什么最低优先级中断最高优先级任务

2:硬件中断永远优先于任务

ARM 硬件规定:任何中断来了,正在跑的任务必须让路。跟 FreeRTOS 任务优先级无关。


3:中断内的一根分界线

FreeRTOS 画的一条安全红线,值 = 5。划分了两个区域:

优先级 0 ─┐ 优先级 1 │ 能打断任务 ✅ 优先级 2 │ 能打断任何中断 ✅ 优先级 3 │ 能调 FreeRTOS API ❌ 优先级 4 ─┘ ────────── configMAX_SYSCALL_INTERRUPT_PRIORITY = 5 ────────── 优先级 5 ─┐ 优先级 6 │ 能打断任务 ✅ 优先级 ... │ 可以被 FreeRTOS 临界区屏蔽 ✅ 优先级 15 ─┘ 能调 FromISR API ✅

优先级5~5的硬件中断,打断当前运行的任务时,可以调用rtos 的库函数,优先级0~4的硬件中断不可以。


4:为什么 0~4 不能用

FreeRTOS 操作任务链表时会进入临界区(设置 BASEPRI = 5),暂时屏蔽5~15的中断。但0~~4 的中断拦不住——如果此时冲进来并调了 FreeRTOS API,正在修改的任务链表是半成品,一碰就崩。

跟权限无关,纯粹是时序冲突——调度器改数据改到一半,你闯进来读,读到的是垃圾。


5:调 FreeRTOS API 是什么意思

就是调 FreeRTOS 库函数。ISR 里必须用带FromISR后缀的版本:

普通 API(任务里用)FromISR 版(中断里用)干什么
xQueueSend(Queue, &data, 0)xQueueSendFromISR(Queue, &data, NULL)发数据给任务
xSemaphoreGive(Sem)xSemaphoreGiveFromISR(Sem, NULL)释放信号量通知任务
vTaskDelay(100)不存在 FromISR 版本ISR 不能睡觉,所以没有
xQueueReceive(...)不存在 FromISR 版本ISR 不能阻塞等数据

实际代码长这样

// ── 刹车中断:优先级 0,不准碰 FreeRTOS ── HAL_NVIC_SetPriority(BRK_IRQn, 0, 0); ​ void BRK_IRQHandler(void) { // 直接操作寄存器关 MOSFET,不调任何 FreeRTOS API TIM1->BDTR &= ~TIM_BDTR_MOE; } ​ // ── CAN 中断:优先级 6(≥5),可以调 FromISR API ── HAL_NVIC_SetPriority(CAN_IRQn, 6, 0); ​ void CAN_IRQHandler(void) { CAN_Frame frame; HAL_CAN_Receive(...); // HAL 自己的 API,随便调 xQueueSendFromISR(can_queue, &frame, NULL); // FromISR ✅ 通知任务处理 } ​ // ── 任务:数字大的优先 ── xTaskCreate(CriticalTask, "CRIT", 256, NULL, 3, NULL); // 优先级 3,高 xTaskCreate(IdleWork, "IDLE", 128, NULL, 1, NULL); // 优先级 1,低
硬件中断 FreeRTOS 任务 数字小 = 高 数字大 = 高 ​ 优先级最高: 中断 0 Task 31(优先级最高) ↓ ↑ 中断 4 Task 3 (vTaskDelay 可被中断打断) ─ ─ configMAX = 5 ─ ─ 中断 5 Task 0 (空闲,永远最低) ↓ 优先级最低: 中断 15

三:FreeRTOS 三种调度方式

1:定义

调度器 = 决定当前该跑哪个任务的算法。FreeRTOS 支持三种:

调度方式规则现行状态
抢占式调度高优先级任务就绪 → 立刻抢走 CPU✅ 主力,configUSE_PREEMPTION=1
时间片调度同优先级任务每人一个 SysTick tick,轮流用 CPU✅ 配合抢占式用
协程式调度任务自己主动放 CPU,高优先级也不抢低优先级❌ 官方不再更新

2“抢占式

优先级 2: Task2 ██████░░░░░░██████ 高优先级就绪立刻抢 优先级 1: Task1 ░░░░████░░░░░░░░░░ 被抢走,排队等

优先级高的任务只要就绪,立刻抢占正在跑的低优先级任务。


3:时间片

Task1 (优先级1): ██░░░░██░░░░██░░░░ Task2 (优先级1): ░░██░░░░██░░░░██░░ Task3 (优先级1): ░░░░██░░░░██░░░░██ ├── 1 tick = 1ms ──┤

同优先级任务轮流跑,每次 SysTick 中断切换一次。一个时间片 = 一个 SysTick 周期(你设的 1ms)。

前提:

  • configUSE_PREEMPTION = 1(抢占式必须开,时间片是抢占式的子功能)

  • configUSE_TIME_SLICING = 1

  • 同优先级的多个任务都处于就绪态


4:协程式

Task2 (优先级2): ______ ← 高优先级也没用 Task1 (优先级1): ████████████████ 不管优先级多低,占住就不放 ├──── 一直跑 ────┤ Task2 只能干等

任务不主动放 CPU,谁也赶不走它。这跟裸机while(1)一个德行——一个任务卡住,全院熄灯。官方已弃坑。


5:实际只会用到抢占+时间片

configUSE_PREEMPTION = 1 // 高优先级抢低优先级 configUSE_TIME_SLICING = 1 // 同优先级轮流跑 效果: 优先级 3: 紧急任务 ← 一来就跑,无人能挡 优先级 2: TaskA █░█░█░█░ ← 高优先级不在时跑 优先级 2: TaskB ░█░█░█░█ ← 跟 TaskA 同优先级,轮流 优先级 1: TaskC ░░░░░░░░░ ← 没人跑才轮到你(空闲任务同级)

四:任务的运行状态

任务状态

1:四种状态

状态含义怎么进去怎么出来
运行态CPU 正在跑这个任务调度器选中被抢占/主动让出/调阻塞 API
就绪态能跑了,但 CPU 在忙别人被唤醒/解挂调度器选中你 → 运行态
阻塞态暂时不能跑,在等vTaskDelay()/ 等队列 / 等信号量时间到 / 事件到 → 就绪态
挂起态人为暂停,不参与调度vTaskSuspend()vTaskResume()→ 就绪态

STM32 同一时刻只有一个任务处于运行态(单核 CPU,一次只能跑一个)。

2:状态切换

调度器选中 就绪态 ───────────────────────→ 运行态 ↑ │ │ 被抢占 / 主动让出 │ └──────────────────────────────┘ ↑ 事件到 / 时间到 │ vTaskDelay() / 等队列 │ ↓ 阻塞态 ←────────────────────── 运行态 ↑ vTaskResume() │ vTaskSuspend() │ ↓ 挂起态 ←────────────────────── 运行态 阻塞态 ── vTaskSuspend() ──→ 挂起态

3:三个列表

除了运行态,其他三种状态各有一个任务列表,调度器从列表里找人:

列表变量名干什么
就绪列表pxReadyTasksLists[x]x = 优先级,每个优先级一个链表。调度器找最高优先级链表里第一个任务开跑
阻塞列表pxDelayedTaskList按唤醒时间排序。SysTick 每次检查队首是不是到时间了,到就挪去就绪列表
挂起列表xSuspendedTaskList纯手工操作。不调vTaskResume永远回不去,调度器当它不存在

就绪列表是数组,每个优先级一个槽。32 个优先级的系统就有pxReadyTasksLists[0]pxReadyTasksLists[31]。调度器用位图(一个 32 位变量)快速查哪个槽非空——bit x=1 表示优先级 x 有任务在排队。

三个任务优先级都是 1 的时候才在同一个槽:

pxReadyTasksLists[1] → task1 → task2 → task3(链表串起来) pxReadyTasksLists[2] → 空 pxReadyTasksLists[3] → 空 调度器找:[3]空 → [2]空 → [1]非空 → 取链表第一个 task1 跑 一个 tick 后 → task1 挪到链表尾 → task2 跑 再一个 tick → task2 挪到尾 → task3 跑

谁建的优先级就进谁的槽,同优先级的用链表串在同一个槽里。

4:一句话

运行态只有 1 个,就绪态排队等 CPU,阻塞态在等时间/事件,挂起态死了——得手动复活。

五:FreeRtosConfig.H

#define xPortPendSVHandler PendSV_Handler #define vPortSVCHandler SVC_Handler

两个宏为了兼容不同内核架构的芯片,

比如换成 RISC-V 芯片:

// STM32(Cortex-M) #define xPortPendSVHandler PendSV_Handler // 某个 RISC-V 芯片(假设向量表里叫 Machine_Timer_Handler) #define xPortPendSVHandler Machine_Timer_Handler // ESP32(Xtensa 架构) #define xPortPendSVHandler _xt_timer_handler

FreeRTOS 的port.c一行不改,只换这个宏,就能跑到不同芯片上。这就是跨平台移植的精髓——内核代码不动,只改 Config 头文件里跟硬件对接的那几行。

1:PendSV_Handler中断做什么?

2:SVC_Handler中断做什么?

中断谁触发干什么频率
SVC软件手动svc 0启动第一个任务:从裸机跨进 RTOS一生一次
PendSVSysTick 说"该切了"保存当前任务 → 恢复下一个任务每秒 1000 次

3:上电后执行流程

上电全流程

① 芯片上电 │ ▼ ② 硬件读向量表首地址 → Reset_Handler │ SystemInit()(配置 Flash 延迟、FPU) │ 进 main() │ ▼ ③ main() (裸机) │ HAL_Init() → SysTick_Init() → TIM7_Init() │ LED_Init() → Usart1_Init() → ... │ xTaskCreate(Led_Task) │ xTaskCreate(Uart_Task) │ ▼ ④ vTaskStartScheduler() │ 配 SysTick = 1ms │ 创建空闲任务 │ svc 0 ← 手动触发 SVC │ ▼ ⑤ SVC_Handler (= vPortSVCHandler) │ 切到第一个任务的栈(PSP) │ 弹出任务寄存器 │ 跳转任务函数 → Led_Task 开跑 │ ▼ ⑥ FreeRTOS (永远循环) │ ├─ SysTick 每 1ms 中断: │ xPortSysTickHandler() → xTickCount++ │ 看看要不要切任务? │ ├─ 不用切 → 返回,当前任务继续跑 │ └─ 要切 → 挂起 PendSV │ ├─ PendSV_Handler (= xPortPendSVHandler): │ 压栈当前任务 → 弹栈下个任务 → 切过去 │ └─ Task A ↔ Task B ↔ Idle Task ... 无穷

4: 为什么 PendSV 不直接在 SysTick 中断里切

如果 SysTick 中断里直接切任务: 硬件中断(比如 UART)优先级 > SysTick → UART 中断来,SysTick 切任务切到一半被抢走 → 任务链表是半成品 → 崩 用 PendSV: SysTick 只标记"该切了" → 等所有硬件中断跑完 → PendSV 优先级最低,没中断了才上场 → 安安静静切换,不会被打断

SysTick 负责"判断要不要切",PendSV 负责"执行切"。一个决策层,一个执行层,中间隔了所有硬件中断。