06-中断管理与时间管理.md 7.1 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之FreeRTOS实时操作系统 V1.0.3 — 第7、9章 + 配套代码04/05/08" author: "尚硅谷研究院" date: 2026-07-12 url: ""

created: 2026-07-12

中断管理与时间管理

用生活理解:中断就像你在写代码时,突然有人打电话找你。如果你接电话(响应中断),当前写代码的工作就被"打断"了。FreeRTOS 的中断管理就是决定:谁的电话必须接(高优先级中断),谁的电话可以稍等(低优先级中断被屏蔽)。

BASEPRI 中断管理

STM32 的中断优先级 0~15(0 最高,15 最低)。FreeRTOS 用 BASEPRI 寄存器 设置一个"门槛":

BASEPRI = 5(实际存的是0x50)
    ↓
中断优先级 0~4  → 正常响应(这些是紧急的)
中断优先级 5~15 → 全部屏蔽(这些可以等)

就像公司前台:优先级 0~4 是老板的电话,必须立刻接通;优先级 5~15 是推销电话,先不接。

示例 1:开关中断实验(项目 04)

情景

  • 两个定时器:TIM2(优先级=4,在门槛之上)和 TIM3(优先级=6,在门槛之下)
  • task1:按 KEY1 关中断,按 KEY2 开中断
  • 观察:关中断后 TIM3 停止打印(被屏蔽了),但 TIM2 仍在工作

核心代码

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)

示例 2:时间片调度(项目 05)

核心概念

时间片 = 每个任务能连续运行的最长时间(一个 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,我们想观察的是"时间片到了被迫让出"的效果。


示例 3:相对延时 vs 绝对延时(项目 08)

理解两者的区别

vTaskDelay(500) — 相对延时:

    任务开始执行 ─→ vTaskDelay(500) ─→ 睡500ms ─→ 醒来继续
    就像说"从现在开始算,我睡500ms"
    如果你干活花了 10ms,下次醒来是 510ms 后

xTaskDelayUntil() — 绝对延时:

    任务开始执行 ─→ vTaskDelayUntil(500) ─→ 睡到固定时间点
    就像说"不管我这次干了多久,下次准时在整秒时刻醒来"
    周期是固定的!

用 LED 波形观察最直观:

  • 相对延时:LED 亮的时长 = 固定延时 + 干活时间(累计误差)
  • 绝对延时: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 与 SysTick 中断设计哲学

PendSV(上下文切换中断)

为什么选择 PendSV?

  • PendSV 是一个系统级异常,可以通过写寄存器手动触发(ICSR bit28)
  • 可以在其他 ISR 中设置——Tick ISR 发现需要切换时,只需"挂起 PendSV"然后继续执行,等 Tick ISR 退出后再真正切换
  • 被设为最低中断优先级,避免任务切换干扰 UART、定时器、SPI 等外设中断

Tick ISR 的完整工作流程

  1. tick 计数加 1
  2. 遍历阻塞列表,找出到期任务并移至就绪列表
  3. 检查就绪列表是否需要切换(更高优先级就绪 / 同优先级时间片用完)
  4. 如果需要切换,挂起 PendSV(等 Tick ISR 退出后再切,不抢占当前中断)

主动切换:任务提前结束时(如调用 vTaskDelay()),直接主动调用任务切换函数,无需等 tick 中断——这就是"时间片提前结束不浪费"的实现机制。

SysTick 的两个作用

  1. 为时间片轮转提供基准时钟
  2. 为阻塞列表中的任务提供超时计时

后续章节:[[07-消息队列与队列集]]