一、我想实现什么功能

用 STM32 检测一次遮挡,每触发一次,计数器加 1,并在 OLED 上显示结果。

这是一个非常经典的 EXTI 入门实验,但我实际做的时候,踩了不少坑。

二、硬件与环境

项目

说明

MCU

STM32F103C8T6

开发环境

Keil uVision5

编译器

ARMCC V5

外设

GPIO、EXTI、NVIC、OLED

输入信号

对射式红外

接线比较简单:

STM32

红外对射模块

PB14

DO

3.3V

VCC

GND

GND

OLED

STM32

GND

PB6

VCC

PB7

SCL

PB8

SDA PB9

PB6,PB7分别接面包板的正负极。

三、EXTI 到底是什么?

EXTI(External Interrupt)就是让 GPIO 引脚在电平变化时打断 CPU,立刻执行一段代码。

整体流程大致是:

GPIO → AFIO → EXTI → NVIC → CPU

  • GPIO:检测电平变化

  • AFIO:选择哪个引脚连到哪根中断线

  • EXTI:管理中断线和触发方式

  • NVIC:决定中断能不能进 CPU、谁先谁后

四、我的 EXTI 初始化代码

1️⃣ 开启时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

注意:AFIO 的时钟一定要开,不然 EXTI 根本不工作。


2️⃣ GPIO 配置

GPIO_InitTypeDef GPIO_InitStructure;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);

我用的是上拉输入,这样未遮挡时是高电平,遮挡后是低电平。


3️⃣ EXTI 配置

GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);

EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line14;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);

这里我选择了下降沿触发


4️⃣ NVIC 配置

NVIC_InitTypeDef NVIC_InitStructure;

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

一个小提醒:

NVIC 中断分组整个工程只需要设置一次,一般放在 main 里。


五、我踩的第一个坑:Keil 编译不过

一开始我写成了这样:

RCC_APB2PeriphClockCmd(...);

GPIO_InitTypeDef GPIO_InitStructure; // ❌

Keil 直接报错:

error #268: declaration may not appear after executable statement

原因是:

👉 Keil ARMCC V5 只支持 C89,变量必须在函数最前面声明。

✅ 正确写法:

void CountSensor_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    EXTI_InitTypeDef EXTI_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    // 后面才是执行语句
}


六、我踩的第二个坑:一次变多次

功能跑起来了,但问题来了:

明明只遮挡一次,计数器却加了 3、4、5……

后来我才明白,这是抖动

高 → 低 → 高 → 低 → 高

每次下降沿都会触发一次中断。

✅ 我的解决方法(软件消抖)

void EXTI15_10_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line14) == SET)
    {
        Delay_ms(10); // 等待抖动结束
        CountSensor_Count++;

        EXTI_ClearITPendingBit(EXTI_Line14);
    }
}

📌 中断里一定要清中断标志位,否则会一直进中断,程序直接卡死。

初期为了快速验证功能,我在中断中使用了 Delay_ms(10)做软件消抖,确实解决了抖动导致的多次计数问题。

但这种方式属于阻塞式消抖,会占用 CPU 并影响系统中断响应,不适合复杂工程。

在实际项目中,应使用非阻塞方式(如定时器消抖或状态机)来替代中断内延时。


七、最终效果

✅ 遮挡一次,计数 +1

✅ OLED 实时显示

✅ 不再误触发

对射式红外传感器计数


八、总结

这是我第一次完整跑通 STM32 外部中断,收获很大:

  • EXTI ≠ NVIC,两者分工不同

  • Keil 使用的是 C89,变量声明位置要注意

  • 抖动是物理现象,不是程序写错了

  • 中断函数名不能乱改,否则进不了中断

  • 中断里不适合写复杂逻辑


九、给同样在入门的朋友一点建议

如果你也在学 STM32:

  1. 先写 GPIO,再写 EXTI

  2. 不要一上来就用 CubeMX + HAL

  3. 多踩坑,多写博客,比只看教程有用得多


这篇是我 EXTI 系列的第一篇,后面我会尝试用 定时器消抖​ ,欢迎一起交流 😊

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐