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:任务可以同等优先级。
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 分两类:
| 场景 | 用什么 | 例子 |
|---|---|---|
| 任务里调 | 普通 API | vTaskDelay()、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 | 一生一次 |
| PendSV | SysTick 说"该切了" | 保存当前任务 → 恢复下一个任务 | 每秒 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 负责"执行切"。一个决策层,一个执行层,中间隔了所有硬件中断。