tags: [source-summary] type: source source: "尚硅谷嵌入式技术之FreeRTOS实时操作系统 V1.0.3 — 第7、9章 + 配套代码04/05/08" author: "尚硅谷研究院" date: 2026-07-12 url: ""
用生活理解:中断就像你在写代码时,突然有人打电话找你。如果你接电话(响应中断),当前写代码的工作就被"打断"了。FreeRTOS 的中断管理就是决定:谁的电话必须接(高优先级中断),谁的电话可以稍等(低优先级中断被屏蔽)。
STM32 的中断优先级 0~15(0 最高,15 最低)。FreeRTOS 用 BASEPRI 寄存器 设置一个"门槛":
BASEPRI = 5(实际存的是0x50)
↓
中断优先级 0~4 → 正常响应(这些是紧急的)
中断优先级 5~15 → 全部屏蔽(这些可以等)
就像公司前台:优先级 0~4 是老板的电话,必须立刻接通;优先级 5~15 是推销电话,先不接。
void task1(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
key = Key_Detect();
if (key == KEY1_PRESS)
{
// 关中断:BASEPRI = 5<<4 = 0x50
// 屏蔽优先级 5~15 的中断
portDISABLE_INTERRUPTS();
printf(">>>>关了中断,TIM3(优先级6)被屏蔽了\r\n");
}
else if (key == KEY2_PRESS)
{
// 开中断:BASEPRI = 0
// 所有中断都能响应
portENABLE_INTERRUPTS();
printf(">>>>开了中断,TIM3又能工作了\r\n");
}
// 注意:不能用 vTaskDelay!因为 vTaskDelay 底层也会开关中断
// 改用 HAL_Delay(前提是 HAL 时基用别的定时器)
HAL_Delay(500);
}
}
// FreeRTOSConfig.h 中的关键配置
// 内核自身使用的中断优先级(设为最低,避免屏蔽其他中断)
#define configKERNEL_INTERRUPT_PRIORITY (15 << 4)
// FreeRTOS 管理的最高中断优先级(门槛值)
// 优先级小于 5 的中断不受 FreeRTOS 管理
#define configMAX_SYSCALL_INTERRUPT_PRIORITY (5 << 4)
时间片 = 每个任务能连续运行的最长时间(一个 SysTick 周期)。 同优先级任务轮流干活,就像"轮班制"——每人干一个时间片,时间到了换下一个人。
配置方法:
// FreeRTOSConfig.h
#define configUSE_TIME_SLICING 1 // 开启时间片轮转
#define configUSE_PREEMPTION 1 // 开启抢占式调度
#define configTICK_RATE_HZ 20 // 每秒20个tick = 每个时间片50ms
// task1 和 task2 优先级相同(都是2),会轮流执行
#define TASK1_PRIORITY 2
#define TASK2_PRIORITY 2 // ⚠️ 相同优先级!
void task1(void *pvParameters)
{
uint16_t count = 0;
while (1)
{
taskENTER_CRITICAL(); // 防止 printf 被中途打断
printf("task1第[%d]次运行\r\n", ++count);
HAL_Delay(10); // 模拟干活(不用vTaskDelay,因为想看到切换)
taskEXIT_CRITICAL();
}
}
void task2(void *pvParameters)
{
uint16_t count = 0;
while (1)
{
taskENTER_CRITICAL();
printf("task2第[%d]次运行\r\n", ++count);
HAL_Delay(10);
taskEXIT_CRITICAL();
}
}
为什么这里用 HAL_Delay 而不是 vTaskDelay? 因为 vTaskDelay 会让任务进入阻塞态主动让出 CPU,我们想观察的是"时间片到了被迫让出"的效果。
vTaskDelay(500) — 相对延时:
任务开始执行 ─→ vTaskDelay(500) ─→ 睡500ms ─→ 醒来继续
就像说"从现在开始算,我睡500ms"
如果你干活花了 10ms,下次醒来是 510ms 后
xTaskDelayUntil() — 绝对延时:
任务开始执行 ─→ vTaskDelayUntil(500) ─→ 睡到固定时间点
就像说"不管我这次干了多久,下次准时在整秒时刻醒来"
周期是固定的!
用 LED 波形观察最直观:
void task1(void *pvParameters)
{
while (1)
{
LED_Toggle(LED1_Pin); // 翻转LED1
HAL_Delay(20); // 模拟干了20ms的活
vTaskDelay(500); // 再睡500ms
// 实际周期 = 20ms + 500ms = 520ms(误差累积)
}
}
void task2(void *pvParameters)
{
TickType_t last_wake = xTaskGetTickCount(); // 记录第一次醒来的时间
while (1)
{
LED_Toggle(LED2_Pin);
HAL_Delay(20); // 模拟干了20ms的活
vTaskDelayUntil(&last_wake, 500); // 睡到"上次醒来时间 + 500ms"
// 实际周期 = 固定500ms(干活时间不计入)
// 即使这次多干了一会儿,下次也会准时
}
}
| vTaskDelay | xTaskDelayUntil | |
|---|---|---|
| 周期 | 不固定(随干活时间漂移) | 固定 |
| 适用场景 | 普通延时(LED闪烁) | 定时采样、PWM控制、通信时序 |
| 误差 | 累积误差 | 只有单次抖动,不累积 |
code/04_中断管理/code/05_时间片调度/code/08_时间延时函数/为什么选择 PendSV?
Tick ISR 的完整工作流程:
主动切换:任务提前结束时(如调用 vTaskDelay()),直接主动调用任务切换函数,无需等 tick 中断——这就是"时间片提前结束不浪费"的实现机制。
后续章节:[[07-消息队列与队列集]]