STM32 外部中断实战:对射式计数器和编码器计数器,两个实验跑通 EXTI

本文是上一篇《STM32 中断入门:中断系统 + 外部中断 EXTI,一篇讲透》的

续篇(实验篇)

。上一篇讲了 EXTI 的原理,这一篇直接上手做两个实验:对射式计数器、编码器计数器,把外部中断的 "五步配置法" 和中断函数写法彻底跑通。

如果你还没看原理篇,建议先读一下,尤其是外部中断的配置流程,这篇的代码就是它的落地。


实验一:对射式计数器

1. 实验现象与原理

先用对射式红外传感器做个计数器。硬件连接如下:

原理:

  • OLED 用来显示程序的现象(计数值);

  • 当挡光片或编码盘在对射式红外传感器中间经过时,传感器 DO 口就会输出电平跳变的信号;

  • 这个电平跳变信号触发 STM32 PB14 号口的外部中断;

  • 在中断函数内执行变量 ++ 的程序;

  • 主循环里调用 OLED 显示这个变量。

简单说就是:传感器挡一下 → PB14 产生一个下降沿 → 触发外部中断 → 计数加 1 → OLED 刷新显示。

2. 外部中断的 "五步配置流程"

写代码之前,先把配置流程记下来,这是外部中断的通用套路(库函数方式):

  1. 配置 RCC:把用到的外设时钟都打开(GPIO、AFIO);

  2. 配置 GPIO:选择端口为输入模式(浮空 / 上拉 / 下拉);

  3. 配置 AFIO:选择我们用的这一路 GPIO,连接到后面的 EXTI;

  4. 配置 EXTI:选择边沿触发方式(上升沿 / 下降沿 / 双边沿),以及触发响应方式(中断响应或事件响应);

  5. 配置 NVIC:给这个中断选一个合适的优先级。

最后,通过 NVIC,外部中断信号就能进入 CPU,CPU 才能收到中断信号、跳转到中断函数执行中断程序。这五步就是外部中断的配置流程。

3. CountSensor.c 完整代码与解析

复制代码
#include "stm32f10x.h"    // Device header

//第一步都是先写初始化函数
void CountSensor_init(void)
{
    //在这里就要进行外部中断的配置
    //a.配置RCC,把用到的外设时钟都打开(GPIO、AFIO)
    //b.配置GPIO,选择端口为输入模式
    //c.配置AFIO,选择这一路GPIO连接到后面的EXTI
    //d.配置EXTI,选择边沿触发方式、触发响应方式
    //e.配置NVIC,给这个中断选一个合适的优先级
    //这五步就是外部中断的配置流程

    /*第一步:开启时钟*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);    //开启GPIOB的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);    //AFIO也是APB2总线上的外设
    //如不清楚哪个外设接在哪个总线上,可以转到函数定义,看参数列表
    //EXTI和NVIC两个外设的时钟是一直都打开着的,不需要再开启,内核的外设不需要开启时钟
    //RCC管理的都是外核的外设

    /*第二步:配置GPIO*/
    GPIO_InitTypeDef GPIO_InitStruture;    //定义结构体
    GPIO_InitStruture.GPIO_Mode = GPIO_Mode_IPU;    //选择上拉输入,默认高电平
    GPIO_InitStruture.GPIO_Pin = GPIO_Pin_14;
    GPIO_InitStruture.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruture);    //初始化GPIOB外设
    //其他外设使用GPIO时,如果不清楚该配置什么模式,可以看参考手册GPIO一章的"外设GPIO配置表"

    /*第三步:配置AFIO外设*/
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);    //把PB14连接到EXTI的中断线

    /*第四步:配置EXTI外设*/
    EXTI_InitTypeDef EXTI_InitStructure;
    EXTI_InitStructure.EXTI_Line = EXTI_Line14;    //指定要配置的中断线(第14号线)
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;    //使能
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;    //中断模式
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;    //指定触发边沿:下降沿
    EXTI_Init(&EXTI_InitStructure);
    //将EXTI第14线路配置为:中断模式 + 下降沿触发 + 开启中断
    //这样PB14的电平信号就能通过EXTI通向下一级NVIC了

    /*第五步:配置NVIC外设*/
    //NVIC是内核外设,它的库函数在杂项文件(misc.h)里
    //配置中断之前,先指定中断分组,再用NVIC_Init初始化
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);    //分组2:2位抢占 + 2位响应
    //注意:分组方式整个芯片只能用一种,这个分组的代码整个工程只需要执行一次
    //如果放在模块里分组,要确保每个模块选的都是同一个分组
    //也可以放在主函数最开头,这样模块里就不用再分组了
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;    //指定中断通道(EXTI10~15共用这个通道)
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;    //使能
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;    //抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;    //响应优先级
    NVIC_Init(&NVIC_InitStructure);
}

//外部中断的信号从GPIO到AFIO,再到EXTI,再到NVIC,最终通向CPU,
//这样CPU才能由主程序跳转到中断程序执行。
//那么中断程序放在哪里呢?这就需要写一个中断函数了。
//STM32中中断函数的名字是固定的,每个中断通道对应一个中断函数,
//名字可以参考启动文件(startup_stm32f10x_md.s),
//里面以IRQHandler结尾的字符串就是中断函数的名字。

uint16_t countsensor_Get(void)    //在这个模块写一个Get函数,返回计数变量
{
    return countsensor_count;
}

void EXTI15_10_IRQHandler(void)    //中断函数都是无参无返回值的,名字写错就进不了中断
{
    //中断函数里,一般先进行中断标志位判断,确保是我们想要的中断源触发的
    //因为这个函数EXTI10~EXTI15都能进来,所以要判断是不是EXTI14进来的
    if (EXTI_GetITStatus(EXTI_Line14) == SET)
    {
        //如果是,就执行中断程序
        countsensor_count++;
        EXTI_ClearITPendingBit(EXTI_Line14);
        //中断程序结束后,一定要调用清除中断标志位的函数
        //只有中断标志位置1了,程序才会跳转到中断函数
        //如果不清除标志位,它会一直申请中断,程序就会不断响应中断,卡死在中断函数里
    }
}
//注意:中断函数不需要声明,因为它不需要被调用,是自动执行的

几个容易忽略的点:

  • 为什么选上拉输入:传感器 DO 空闲时输出高电平,挡光时拉低,配合下降沿触发正好。上拉输入保证默认电平稳定为高。

  • 为什么是 EXTI15_10_IRQn :PB14 属于 EXTI14,而 EXTI10~15 共用同一个中断通道(中断函数 EXTI15_10_IRQHandler),所以要在函数里判断到底是哪条线触发的。

  • 清除标志位不能忘 :EXTI_ClearITPendingBit 不调用,中断会反复触发,程序卡死在中断里。

4. CountSensor.h

复制代码
#ifndef ___COUNT_SENSOR_H
#define ___COUNT_SENSOR_H

void CountSensor_init(void);
uint16_t countsensor_Get(void);

#endif

5. Main.c

复制代码
#include "stm32f10x.h"    // Device header
#include "delay.h"
#include "led.h"
#include "key.h"
#include "LightSensor.h"
#include "OLED.h"
#include "CountSensor.h"

uint8_t Key_Num;    //全局变量
uint8_t LightSensor;

int main()
{
    OLED_Init();
    CountSensor_init();

    OLED_ShowString(2, 1, "COUNT:");
    while (1)
    {
        OLED_ShowNum(2, 8, countsensor_Get(), 5);
    }
}

主循环里只做一件事:不断把 countsensor_Get() 的值刷到 OLED 上。计数本身完全在中断里完成 ------ 这就是 "中断式" 和 "轮询式" 最本质的区别。


实验二:编码器计数器

1. 原理与接线

旋转编码器旋转时会输出 A、B 两路相位相差 90° 的方波。通过判断两路信号的相位关系,就能知道旋转方向和圈数。接线如下:

(上图为 OLED 下方被遮住的接线图)

这里把编码器的 A、B 两相分别接到 PB0 和 PB1,各用一路外部中断(EXTI0、EXTI1),配合电平判断实现 "正转加一、反转减一"。

2. Encoder.c 完整代码与解析

复制代码
#include "stm32f10x.h"    // Device header

void Encoder_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);    //开启GPIOB的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);    //开启AFIO的时钟,外部中断必须开启AFIO的时钟

    /*GPIO初始化*/
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);    //将PB0、PB1引脚初始化为上拉输入

    /*AFIO选择中断引脚*/
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);    //将外部中断0号线映射到GPIOB,即选择PB0为外部中断引脚
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);

    /*EXTI初始化*/
    EXTI_InitTypeDef EXTI_InitStructure;    //定义结构体变量
    EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;    //选择配置外部中断的0号线和1号线
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;    //使能
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;    //中断模式
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;    //下降沿触发
    EXTI_Init(&EXTI_InitStructure);    //配置EXTI外设

    /*NVIC中断分组*/
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);    //配置NVIC为分组2
    //即抢占优先级范围:0~3,响应优先级范围:0~3
    //此分组配置在整个工程中仅需调用一次
    //若有多个中断,可以把此代码放在main函数内、while循环之前
    //若调用多次配置分组的代码,后执行的配置会覆盖先执行的配置

    /*NVIC配置*/
    NVIC_InitTypeDef NVIC_InitStructure;    //定义结构体变量
    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;    //选择EXTI0线
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;    //使能
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;    //抢占优先级1
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;    //响应优先级1
    NVIC_Init(&NVIC_InitStructure);

    NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;    //选择EXTI1线
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;    //使能
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;    //抢占优先级1
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;    //响应优先级2
    NVIC_Init(&NVIC_InitStructure);
}

int16_t Encoder_Num;

int16_t Encoder_Get(void)
{
    int16_t temp;
    temp = Encoder_Num;
    Encoder_Num = 0;    //读完后清零,下次从0开始累计
    return temp;
}

void EXTI0_IRQHandler(void)
{
    if (EXTI_GetFlagStatus(EXTI_Line0) == SET)
    {
        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)    //PB0跳变时判断PB1电平
        {
            Encoder_Num--;
        }
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

void EXTI1_IRQHandler(void)
{
    if (EXTI_GetFlagStatus(EXTI_Line1) == SET)
    {
        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)    //PB1跳变时判断PB0电平
        {
            Encoder_Num++;
        }
        EXTI_ClearITPendingBit(EXTI_Line1);
    }
}

方向判断的核心思路(正交解码的 "土办法"):

  • 当 PB0(A 相) 产生下降沿时,去读 PB1(B 相) :如果 B 相是低电平,说明是正转,Encoder_Num++;反过来如果在 PB1 下降沿时读 PB0 为低,则 Encoder_Num--。

  • 因为 A、B 相相位差 90°,旋转方向不同时,一个边沿到来时另一相的电平状态也不同 ------看 "谁先谁后" 就知道转了哪个方向。

Encoder_Get() 读完后把内部变量清零,主函数里 Num += Encoder_Get() 累加,这样计数不会丢,也不会重复。

3. Encoder.h

复制代码
#ifndef ___ENCODER_H
#define ___ENCORDE_H

void Encoder_Init(void);
uint16_t Encoder_Get(void);

#endif

4. Main.c

复制代码
#include "stm32f10x.h"    // Device header
#include "delay.h"
#include "led.h"
#include "key.h"
#include "LightSensor.h"
#include "OLED.h"
#include "Encoder.h"

uint8_t Key_Num;    //全局变量
uint8_t LightSensor;
int16_t Num;

int main()
{
    OLED_Init();
    Encoder_Init();

    while (1)
    {
        Num += Encoder_Get();
        OLED_ShowString(3, 1, "count");
        OLED_ShowSignedNum(3, 7, Num, 5);
    }
}

注意这里用 OLED_ShowSignedNum(有符号显示),因为 Num 是 int16_t,正转显示正数、反转显示负数。


中断编程的三个注意点

做完两个实验,最后记住这三条,能帮你少踩很多坑:

1. 中断函数里不要执行耗时过长的代码

中断是处理 "突发事情" 的,函数要简短快速 。别一进中断就 Delay 几百毫秒 ------ 你为了一个突发的事情赖在中断里不出来,主程序就会被严重阻塞。

2. 不要在中断函数和主函数里操作同一个硬件

尤其是 OLED 这类显示函数:既在主程序里调用、又在中断里调用,OLED 就会显示错乱。正确做法是:在中断里只操作变量或标志位,中断返回后,主循环再根据变量去显示、去操作硬件。这样既保证中断简短快速,又避免硬件冲突。

3. 多用变量和标志位,减少代码耦合

在其它地方也可以多用变量、标志位来降低代码之间的耦合:让各部分代码相互独立,仅使用变量、标志位或函数作为接口,程序会更清晰,也更健壮。


小结

这两个实验把上一篇的原理全部落到了代码上:

  • 对射式计数器:传感器边沿 → PB14 外部中断 → 中断里计数 → 主循环 OLED 显示;

  • 编码器计数器:A/B 两相各接一路外部中断,在中断里互读另一相电平判断方向,实现正反转计数;

  • 两段代码都遵循同样的五步配置法:RCC → GPIO → AFIO → EXTI → NVIC;

  • 中断函数统一套路:判断标志位 → 执行逻辑 → 清除标志位。

把这套配置流程和中断写法记住,STM32 的其它中断(定时器、串口、DMA 事件)配置思路都是一样的,只是把 "GPIO + AFIO" 换成对应的外设而已。

相关推荐
2501_922639582 小时前
率能半导体 SS6286L直流双向马达驱动芯片:18V/7A,覆盖电子锁、玩具、无线充电、机器人四大应用场景
stm32·单片机·嵌入式硬件·硬件工程
搁浅小泽3 小时前
IGBT 与散热片间未加绝缘垫片导致桥臂直通原理分析
单片机·嵌入式硬件·设计规范
动词ing3 小时前
【51单片机】开始软件安装和构成
单片机·嵌入式硬件·51单片机
铅笔小新z3 小时前
【stm32】PWM呼吸灯
stm32·单片机·嵌入式硬件
深圳老胡4 小时前
RTX 与其他 RTOS 操作系统的差异及优缺点分析
笔记·stm32·单片机·代码规范
洞察物理世界4 小时前
【SI_PRBS 01】快速了解光通信PRBS
单片机·嵌入式硬件
happygrilclh4 小时前
LVGL 移植整体流程
stm32
续写烂尾5 小时前
SDNAND 硬件电路设计:供电、上拉与走线全景
单片机·嵌入式硬件·sd nand·sdnand
Brilliantwxx6 小时前
【STM32】 RTC 实时时钟详解:从概念到预分频器
stm32·单片机·嵌入式硬件·mcu·bash