单片机 - STM32 非阻塞式编程详解:以 LED 和按键为例(附超详细寄存器级代码)

STM32 非阻塞式编程:以 LED 和按键为例

一、前言

在嵌入式开发中,我们经常需要处理多个任务,比如测距、闪灯、响应用户按键、处理串口接收等等。如果你在主循环中使用 delay() 之类的阻塞函数,那这些任务就只能一个一个做,效率低下,体验不佳。

非阻塞式编程,就是为了解决这个问题的一种思路。它避免在主循环中使用任何会"卡住 CPU"的阻塞操作,从而允许 CPU 每时每刻都能轮询检查所有任务是否需要处理,实现"多任务调度"的效果。

本文将从原理讲起,并用 STM32F407 开发板配合 LED 和按键 做一个完整的寄存器级非阻塞例程,帮你彻底理解这个核心知识点。

二、阻塞 vs 非阻塞:到底有什么区别

类型 示例 优点 缺点
阻塞式 delay(1000); 写起来简单直观 会让 CPU 等待,浪费资源
非阻塞式 if (millis - last >= 1000) 不卡主循环,适合多任务 写起来需要设计思路、变量管理

举个例子:

c 复制代码
// 阻塞式延时方式闪烁 LED
LED_ON();
delay_ms(1000);
LED_OFF();
delay_ms(1000);

这个时候,CPU 就被 delay_ms() 卡死了,什么事也干不了。

而非阻塞的写法如下:

c 复制代码
if (SysTickCounter - lastToggleTime >= 1000) {
    lastToggleTime = SysTickCounter;
    toggle_LED();
}

CPU 每次循环只判断一下时间是否到了,LED 闪烁的同时,主循环还能继续干别的事。

三、非阻塞的核心思路

非阻塞的核心是:"记录上一次事件发生的时间,并每次循环中判断是否满足条件。"

用人话解释:

  • 如果你要等一分钟,就不要站着等(阻塞);
  • 而是每隔几秒瞄一眼表,看时间到了没有(非阻塞)。

四、实现非阻塞:我们用 SysTick 来做时间基准

我们需要一个系统计时器来定时递增变量,用于判断时间间隔。在 STM32F4 中,最简单的方法就是用 SysTick 定时器

1. 初始化 SysTick 计时器(1ms中断一次)

c 复制代码
void SysTick_Init(void)
{
    // 设置重装载值,使得每 1ms 触发一次中断
    // 假设系统主频是 168MHz
    // 168000000 / 1000 = 168000
    SysTick->LOAD = 168000 - 1;

    // 设置时钟源为处理器时钟
    SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk;

    // 使能 SysTick 中断
    SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;

    // 使能 SysTick 定时器
    SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
}

2. SysTick 中断服务函数

c 复制代码
volatile uint32_t systick_ms = 0;  // 毫秒计数变量

void SysTick_Handler(void)
{
    systick_ms++;  // 每毫秒加一
}

五、LED 非阻塞闪烁代码

1. GPIO 初始化(LED 接 PA5)

c 复制代码
void LED_Init(void)
{
    RCC->AHB1ENR |= (1 << 0);  // 使能 GPIOA 时钟

    GPIOA->MODER &= ~(3 << (5 * 2));  // 清除模式位
    GPIOA->MODER |= (1 << (5 * 2));   // 设置为输出模式

    GPIOA->OTYPER &= ~(1 << 5);       // 推挽输出
    GPIOA->OSPEEDR |= (3 << (5 * 2)); // 高速
    GPIOA->PUPDR &= ~(3 << (5 * 2));  // 无上下拉
}

void LED_Toggle(void)
{
    GPIOA->ODR ^= (1 << 5);  // 取反输出
}

2. 非阻塞闪烁逻辑

c 复制代码
void LED_Blink_NonBlocking(void)
{
    static uint32_t last_time = 0;

    // 判断当前时间与上次时间的间隔是否 >= 500ms
    if (systick_ms - last_time >= 500)
    {
        last_time = systick_ms;  // 记录新的时间戳
        LED_Toggle();            // 切换 LED 状态
    }
}

六、按键非阻塞检测(支持消抖)

1. GPIO 初始化(按键接 PC13)

c 复制代码
void Key_Init(void)
{
    RCC->AHB1ENR |= (1 << 2);     // 使能 GPIOC 时钟

    GPIOC->MODER &= ~(3 << (13 * 2));  // 设置为输入模式
    GPIOC->PUPDR |= (1 << (13 * 2));   // 上拉
}

2. 非阻塞方式读取按键(支持简单消抖)

c 复制代码
uint8_t Key_GetPress(void)
{
    static uint8_t key_state = 1;     // 初始为释放状态(PC13 默认为高)
    static uint32_t last_debounce_time = 0;

    if ((GPIOC->IDR & (1 << 13)) == 0)  // 按键被按下(低电平)
    {
        if (systick_ms - last_debounce_time >= 20) // 20ms去抖
        {
            last_debounce_time = systick_ms;

            if (key_state == 1)
            {
                key_state = 0;  // 记录状态为"已按下"
                return 1;       // 返回按键事件
            }
        }
    }
    else
    {
        key_state = 1;  // 松开后恢复初始状态
    }

    return 0;  // 没有新事件
}

七、主函数整合

c 复制代码
int main(void)
{
    SysTick_Init();
    LED_Init();
    Key_Init();

    while (1)
    {
        LED_Blink_NonBlocking();  // 每500ms闪烁一次

        if (Key_GetPress())       // 按键按下就立刻响应
        {
            // 可以加入更多操作,比如切换模式、串口输出等
            GPIOA->ODR ^= (1 << 5);  // 再次切换 LED
        }

        // 可拓展:串口接收处理、传感器轮询等其他非阻塞任务
    }
}

八、总结:非阻塞的精髓

  • 避免用 delay(),而是用时间戳差值判断
  • 每个任务都维护自己的状态变量和计时变量
  • 适合多个任务并发运行的情况(状态机 + 事件驱动)

九、扩展阅读建议

  • 状态机编程模型
  • 事件驱动模型
  • 基于定时器的软定时器管理模块设计
  • 使用 RTOS 代替非阻塞轮询的多任务调度

(完)

相关推荐
沉在嵌入式的鱼1 天前
linux串口对0X0D、0X0A等特殊字符的处理
linux·stm32·单片机·特殊字符·串口配置
学习路上_write1 天前
AD5293驱动学习
c语言·单片机·嵌入式硬件·学习
影阴1 天前
存储器和寄存器
stm32·单片机·嵌入式硬件
吃西瓜的年年1 天前
3. C语言核心语法2
c语言·嵌入式硬件·改行学it
李洛克071 天前
RDMA CM UDP 通信完整指南
单片机·网络协议·udp
思茂信息1 天前
CST电动车EMC仿真——电机控制器MCU滤波仿真
javascript·单片机·嵌入式硬件·cst·电磁仿真
小曹要微笑1 天前
I2C总线技术解析(纯文字版)
单片机·嵌入式硬件·esp32·iic
我送炭你添花1 天前
可编程逻辑器件(PLD)的发展历程、原理、开发与应用详解
嵌入式硬件·fpga开发
袖手蹲1 天前
Arduino UNO Q 从 Arduino Cloud 远程控制闪烁 LED
人工智能·单片机·嵌入式硬件·电脑
平凡灵感码头1 天前
第一次做蓝牙产品,从零开发 嵌入式开发日志(2)AC63NSDK 完整合并版目录说明
stm32·单片机·嵌入式硬件