经典三问

Q: 阻塞式程序是什么?
A: 它在一个操作完成前一直停在原地,期间主循环无法及时处理其他任务。

Q: 为什么小车要减少阻塞?
A: 电机更新、按键扫描、显示刷新都有自己的时限,某个延时过长会让其他任务失去响应。

Q: 阻塞问题常见场景?
A: 延时消抖、动画播放、等待松手,以及任何把长操作直接写进 while(1) 的代码。


桌面上有一颗 LED 正在慢慢变亮、再慢慢变暗,OLED 上的时间数字每 100ms 涨一格。这时按下按键,想让蜂鸣器响 800ms 就停——手指按着不松,LED 停在半亮,时间数字不走了,蜂鸣器一声没出。松手以后,时间数字猛地跳出一大截。

这不是芯片罢工。是主循环走进了一扇没有出口的门。

呼吸灯是拿 Delay 一段段推出来的

要让 LED 从暗到亮、再回到暗,可以让“亮一会儿、灭一会儿”的占空比慢慢变:暗的时候大部分时间灭,亮的时候大部分时间亮。逐档推过去,包成一个函数:

static void LED_BreathOnce(void)
{
    uint8_t brightness;
    uint8_t phase;

    for (brightness = 0; brightness <= 20; brightness++)
    {
        for (phase = 0; phase < 20; phase++)
        {
            if (phase < brightness)
            {
                LED_On();        /* 这一相位亮 */
            }
            else
            {
                LED_Off();       /* 这一相位灭 */
            }

            Delay_ms(1);
        }
    }

    for (brightness = 20; brightness > 0; brightness--)
    {
        for (phase = 0; phase < 20; phase++)
        {
            if (phase < brightness)
            {
                LED_On();
            }
            else
            {
                LED_Off();
            }

            Delay_ms(1);
        }
    }
}

代码不长,效果也正常:LED 渐渐亮起来,再渐渐暗下去。可这个函数有个没人注意的代价——它不把控制权交出去。一个相位 Delay_ms(1),一档 20 个相位,从暗到亮 21 档、从亮到暗 20 档,整个函数跑完要 800 多毫秒。这 800ms 里主循环停在原地,呼吸灯后面排着的任务一个都轮不到。

“等它松手”是第二扇门

按键按下要消抖,最直觉的写法是这样:

static uint8_t Wait_KeyRelease(void)
{
    if (Key_IsDown(KEY_A) == 0U)
    {
        return 0U;               /* 没按下,直接返回 */
    }

    Delay_ms(20);                /* 躲过按下瞬间的抖动 */

    if (Key_IsDown(KEY_A) != 0U) /* 20ms 后仍按着,确认是真人 */
    {
        while (Key_IsDown(KEY_A) != 0U)
        {
            Key_Task1ms();       /* 等待松手:扫描不能停,判据才有人更新 */
        }

        return 1U;               /* 等到了松手 */
    }

    return 0U;                   /* 20ms 后已经松了,不算等到了 */
}

Delay_ms(20) 已经让所有任务停了 20ms。真正麻烦的是下面那个 while:只要手指还压着,程序就永远回不到 while(1) 的其他位置。按住两秒,呼吸灯、OLED、蜂鸣器就一起冻两秒;按住十秒,就冻十秒。while 里那行 Key_Task1ms() 不能省——按键的稳定状态只由它更新,少了它,Key_IsDown(KEY_A) 读到的就一直是按下那一刻的值,松手也退不出来。

按键的稳定状态由谁维护?就是这样一个每 1ms 跑一遍的扫描函数——原始电平要连续保持 20 拍,才写进稳定状态。Key_IsDown() 读的是后者,不是引脚:

#define KEY_A        0U
#define KEY_B        1U
#define KEY_COUNT    2U
#define KEY_DEBOUNCE 20U          /* 连续稳定 20 个 1ms 才算数 */

static uint8_t key_raw[KEY_COUNT]    = {0U, 0U};   /* 上一拍的原始电平 */
static uint8_t key_stable[KEY_COUNT] = {0U, 0U};   /* 消抖后的稳定状态 */
static uint8_t key_cnt[KEY_COUNT]    = {0U, 0U};   /* 已经连续一致了多少拍 */

static uint8_t Key_ReadRaw(uint8_t key)
{
    if (key == KEY_A)
    {
        return (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == Bit_RESET) ? 1U : 0U;
    }

    return (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET) ? 1U : 0U;
}

void Key_Task1ms(void)
{
    uint8_t i;
    uint8_t now;

    for (i = 0U; i < KEY_COUNT; i++)
    {
        now = Key_ReadRaw(i);

        if (now != key_raw[i])
        {
            key_raw[i] = now;
            key_cnt[i] = 0U;
        }
        else if (key_cnt[i] < KEY_DEBOUNCE)
        {
            key_cnt[i]++;

            if (key_cnt[i] == KEY_DEBOUNCE)
            {
                key_stable[i] = now;
            }
        }
    }
}

uint8_t Key_IsDown(uint8_t key)
{
    return key_stable[key];
}

低电平有效:按键 A 接 PB11、B 接 PB1,两脚都配成上拉输入,按下把引脚拉到地。

呼吸灯的代码没有错,OLED 的 I2C 也没有错,蜂鸣器更没有错。它们只是排在按键等待后面,轮不到执行。

蜂鸣器的 800ms 是一整条路障

想让蜂鸣器“响 800ms 就停”,同样可以写成一个函数:

static uint8_t buzzer_active = 0U;   /* 蜂鸣器现在是不是在响,给 OLED 用 */

static void Buzzer_BeepOnce(void)
{
    buzzer_active = 1U;
    Buzzer_Set(1);
    Delay_ms(800);
    Buzzer_Set(0);
    buzzer_active = 0U;
}

几行在纸上很清楚:开、等、关。放进主循环却等于宣布:接下来 800ms,其他任务全部暂停。蜂鸣器响的那段时间,LED 停在半亮,OLED 的时间数字也不走。

三个任务在一条单行道上排队

把前面这三个函数按顺序排进同一个主循环:

while (1)
{
    LED_BreathOnce();   /* 软件 PWM 呼吸,约 800ms 后才返回 */
    if (Wait_KeyRelease() != 0U)   /* 按键死等松手 */
    {
        Buzzer_BeepOnce();         /* 松手才响一次,Delay 800ms */
    }
    OLED_Refresh();     /* 显示刷新,排在最后 */
}

每个函数单独拿出来都正常:呼吸灯会亮暗变化,按键按下会等到松手,蜂鸣器会响够时长。串在一起,却没有任何两个任务在“同时”工作——呼吸灯不返回,按键连一次采样都拿不到;按键不返回,蜂鸣器和 OLED 就一直等;蜂鸣器占住 800ms,下一轮呼吸灯又要晚 800ms 开场。

它们争的不是某个 GPIO,而是主循环返回的机会。

OLED 上的时间数字是台体温计

程序里有一个由 1ms 定时中断维护的毫秒计数,每 1ms 走一步。OLED 每隔 100ms 把这个数除以 100 显示出来,所以正常时屏幕上的时间数字每 100ms 涨一格。主循环里排着的第四个函数就是它:

static void OLED_Refresh(void)
{
    static uint32_t last_ms = 0U;
    uint32_t now = Timer_Millis();

    if ((uint32_t)(now - last_ms) < 100U)
    {
        return;                  /* 还没到 100ms,立刻返回 */
    }

    last_ms = now;

    OLED_ShowNum(0, 0, buzzer_active, 1, OLED_6X8);          /* 第 1 行:蜂鸣器状态 */
    OLED_ShowNum(0, 8, Key_IsDown(KEY_A), 1, OLED_6X8);      /* 第 2 行:按键状态 */
    OLED_ShowNum(0, 16, now / 100U, 5, OLED_6X8);            /* 第 3 行:时间数字 */
    OLED_Update();
}

buzzer_active 是蜂鸣器的开关状态,Key_IsDown(KEY_A) 是按键的稳定状态,第三行那个 now / 100U 就是显示在屏幕上的时间数字。

OLED 的刷新本身也排在主循环里。主循环被哪段 Delay 占住,OLED 就轮不到刷新,屏幕上的数字停在最后一格。等主循环终于转回来,OLED 刷出的是此刻的真实时间——于是数字猛跳。跳了多少格,就是刚才被冻住的时长。

按住 KEY_A 五秒再松手,数字大约跳 50 格。那不是显示坏了,是把欠了五秒的时间一次补上了。

桌面实验:把三件事同时摆上桌

这里的演示不接电机,是一块桌面小系统:按键 A 接 PB11,蜂鸣器接 PB12(低电平触发,Buzzer_Set(1) 让它响),呼吸灯用板载 LED PC13,OLED 按之前的软件 I2C 接线。PB12 在整车中属于左电机方向脚,这里只做独立桌面演示,不要把蜂鸣器并到已经接电机的车上。

烧录后先看正常画面:LED 呼吸、OLED 时间数字每 100ms 涨一格、蜂鸣器安静。然后按住 KEY_A 不松,依次看三处:

  • LED 停在某一个亮度,呼吸中断;
  • OLED 的时间数字停住;
  • 蜂鸣器没有响——按键等待还没结束,蜂鸣器根本没轮到。

松手后,时间数字猛跳一格大的,呼吸从半亮继续。把手指按久一点,数字跳得更多。

亲手量一量:阻塞把时间冻了多久

按键按多久,时间就冻多久

按住 KEY_A 分别撑 1 秒、3 秒、5 秒,每次松手后记下时间数字跳了几格。跳的格数约等于按住时长除以 100ms。按键等待把主循环按停多久,时间数字就停多久——它们本来就是同一件事。

蜂鸣器响两秒,程序就等两秒

把蜂鸣器那段 Delay_ms(800) 改成 Delay_ms(2000),重新下载。单击 KEY_A 再立刻松手:蜂鸣器开始响,响满 2 秒才停。这 2 秒里 LED 呼吸停、时间数字停。改回 800ms,时间数字停的时长也跟着缩短。

呼吸灯慢十倍,按键也跟着慢

把呼吸灯相位里的 Delay_ms(1) 改成 Delay_ms(10),一轮呼吸从约 800ms 变成 8 秒。运行后立刻按 KEY_A:呼吸灯正在跑,按键任务排在它后面,只能等这一轮呼吸结束才轮到。呼吸被调慢,按键的响应时间也被拖慢——阻塞会沿着主循环的顺序,传染给后面每一个任务。恢复 1ms。

三处 Delay 各调过一次以后,规律已经很清楚:主循环里任何一段不返回的等待,都会让排在后面的任务一起陪冻。呼吸灯、按键、蜂鸣器谁都没坏,它们只是没有机会运行。

最后橘猫说

阻塞不是原罪。上电后等待硬件复位、初始化里的微秒级时序,通常可以接受——那段时间系统还没有进入正常任务调度,也没有别的任务在等。

问题出在进入 while(1) 以后。按键、显示、电机、编码器都在抢主循环返回的机会,这时一段 Delay 或一个“等松手”的 while,就把一条本来应该反复检查的路,变成了只允许一辆车通过的窄巷。桌面演示里按住一个按键,呼吸灯和蜂鸣器就全停;把这些写法搬进小车,按住按键的瞬间,轮子、传感器和屏幕会一起停下来等。

阻塞最危险的地方在于它不会报错。程序不崩溃、不重启,看起来只是“反应慢了一点”——慢到某个任务永远轮不到,才以死机的样子出现。


原文地址: https://www.cveoy.top/t/topic/qHMG 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录