🚀 我的STM32学习专栏[点击直达 | 持续更新中]

按键按烂了都没进中断?编码器转得飞起计数却纹丝不动?EXTI 配置步骤多、函数杂,很多新手在 RCC、AFIO、NVIC 之间反复横跳,结果还是配不通。其实核心就 5 步,今天不废话,直接上干货,带你从时钟到中断服务函数一次性打通任督二脉。

一、AFIO 配置函数

AFIO 负责引脚重映射EXTI 线路选择,使用 EXTI 前必须先开 AFIO 时钟。

函数 作用 实战重要度
GPIO_AFIODeInit(void) 复位 AFIO 所有寄存器到默认值 ⭐ 调试用
GPIO_PinLockConfig(GPIOx, GPIO_Pin) 锁定引脚配置,防止后续误改 ⭐ 防呆用
GPIO_EventOutputConfig(PortSource, PinSource) 选择事件输出源引脚 ⭐ 事件模式用
GPIO_EventOutputCmd(NewState) 使能/失能事件输出 ⭐ 事件模式用
GPIO_PinRemapConfig(GPIO_Remap, NewState) 引脚重映射(如 USART1 重映射到 PB6/PB7) ⭐⭐⭐ 重映射必用
GPIO_EXTILineConfig(PortSource, PinSource) 选择 EXTI 中断线对应的引脚 ⭐⭐⭐⭐⭐ EXTI 必用

核心记忆点:只要用 EXTI,GPIO_EXTILineConfig() 是 AFIO 里唯一必须调用的函数。它的作用是把 PA0/PB0/PC0... 中的某一个,绑定到 EXTI_Line0 上。


二、EXTI 配置函数

EXTI(External Interrupt/Event Controller)管理外部中断线 0~15 的触发方式和使能。

函数 作用 使用场景
EXTI_DeInit(void) EXTI 寄存器恢复上电默认值 代码初始化前清零用
EXTI_Init(EXTI_InitStruct) 根据结构体初始化 EXTI ⭐⭐⭐⭐⭐ 必用
EXTI_StructInit(EXTI_InitStruct) 给结构体赋默认值 推荐在 EXTI_Init 前调用,避免结构体残留脏数据
EXTI_GenerateSWInterrupt(EXTI_Line) 软件触发一次外部中断 模拟中断、自测试时用
EXTI_GetFlagStatus(EXTI_Line) 读事件/状态寄存器标志 主程序里轮询查看
EXTI_ClearFlag(EXTI_Line) 清除事件/状态寄存器标志 主程序里清标志
EXTI_GetITStatus(EXTI_Line) 读中断挂起标志位 ⭐⭐⭐⭐⭐ 中断函数内判断标志
EXTI_ClearITPendingBit(EXTI_Line) 清除中断挂起标志位 ⭐⭐⭐⭐⭐ 中断函数内必须清,否则卡死

Flag vs IT 区别Flag 系列读写的是状态寄存器,用于主程序查询外设状态;IT 系列读写的是中断挂起寄存器,用于中断服务函数。中断里不清 ITPendingBit,CPU 会反复进入中断,程序直接卡死。


三、NVIC 配置函数

NVIC(Nested Vectored Interrupt Controller)是 Cortex-M3 内核的中断管家,负责分组、优先级、使能通道

函数 作用 实战重要度
NVIC_PriorityGroupConfig(NVIC_PriorityGroup) 设置抢占优先级和响应优先级的分组 ⭐⭐⭐⭐⭐ 必用,且只能设一次
NVIC_Init(NVIC_InitStruct) 根据结构体初始化 NVIC 通道 ⭐⭐⭐⭐⭐ 每开一个中断就要调一次
NVIC_SetVectorTable(VectTab, Offset) 设置中断向量表位置(Flash/RAM) ⭐ 搞 Bootloader、IAP 时才用
NVIC_SystemLPConfig(LowPowerMode, NewState) 系统低功耗配置 ⭐ 低功耗场景用
EXTI_ClearITPendingBit(EXTI_Line) 清 EXTI 中断挂起位 ⭐⭐⭐⭐⭐ 中断函数末尾必加

注意:NVIC 是内核外设,时钟默认开启,不需要 RCC_APBxPeriphClockCmd()。EXTI 时钟也是默认开的,但 GPIO 和 AFIO 必须手动开。


四、EXTI 配置五步法(全局地图)

步骤 操作 核心目的
1 RCC 开时钟 GPIO + AFIO 的时钟必须手动打开
2 配置 GPIO 设为输入模式(上拉/浮空/下拉,看硬件)
3 配置 AFIO GPIO_EXTILineConfig() 把引脚映射到 EXTI 线
4 配置 EXTI 选中断线、触发方式(上升/下降/双边沿)、使能
5 配置 NVIC 分组、设优先级、使能中断通道

注意:EXTI 和 NVIC 时钟默认开启;NVIC 是内核外设,不需要 RCC 开时钟。RCC 只管内核外的外设。


五、实战代码:编码器 EXTI 配置

以下代码基于 STM32F103 标准库,以 GPIOB Pin0 / Pin1 接编码器 A/B 相为例,下降沿触发

#include "stm32f10x.h"

int16_t Encoder_Count = 0;  // 编码器计数值

/**
 * @brief 编码器 EXTI 初始化(PB0 -> EXTI0, PB1 -> EXTI1)
 * @note  每一步解析都在注释里,对照五步法食用更佳
 */
void Encoder_Init(void)
{
    /* ========== 第 1 步:RCC 开时钟 ==========
     * GPIOB 在 APB2 总线上,AFIO 负责引脚复用和中断映射,也必须开时钟
     */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);  // 开启 GPIOB 时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);   // 开启 AFIO 时钟

    /* ========== 第 2 步:配置 GPIO 为输入模式 ==========
     * 编码器一般接上拉输入(IPU),避免浮空导致电平乱跳
     */
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;         // 上拉输入模式
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0 | GPIO_Pin_1; // 同时配置 PB0、PB1
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;      // 输入模式下速度参数无实际影响,习惯性写上
    GPIO_Init(GPIOB, &GPIO_InitStructure);                 // 写入配置

    /* ========== 第 3 步:AFIO 映射中断线 ==========
     * 关键函数:GPIO_EXTILineConfig()
     * 作用:把 PB0 映射到 EXTI_Line0,PB1 映射到 EXTI_Line1
     * 同一 EXTI_Line 同一时间只能映射到一个端口(如 PA0/PB0/PC0 三选一)
     */
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0); // PB0 -> EXTI0
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1); // PB1 -> EXTI1

    /* ========== 第 4 步:配置 EXTI 外设 ==========
     * 选择中断线、使能线路、设中断模式、选下降沿触发
     */
    EXTI_InitTypeDef EXTI_InitStructure;
    EXTI_InitStructure.EXTI_Line    = EXTI_Line0 | EXTI_Line1; // 同时配置 Line0 和 Line1
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;                // 使能中断线
    EXTI_InitStructure.EXTI_Mode    = EXTI_Mode_Interrupt;   // 设为中断模式(非事件模式)
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;  // 下降沿触发(编码器常用)
    EXTI_Init(&EXTI_InitStructure);                          // 结构体参数写入寄存器

    /* ========== 第 5 步:配置 NVIC ==========
     * 先分组,再配置具体通道的抢占优先级和响应优先级
     */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);          // 2位抢占 + 2位响应优先级

    NVIC_InitTypeDef NVIC_InitStructure;

    // --- EXTI0 通道配置(PB0)---
    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);

    // --- EXTI1 通道配置(PB1)---
    NVIC_InitStructure.NVIC_IRQChannel                   = EXTI1_IRQn;        // 中断通道:EXTI1
    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;                 // 抢占优先级 1
    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 2;                 // 响应优先级 2(与 EXTI0 区分)
    NVIC_Init(&NVIC_InitStructure);
}

/**
 * @brief 获取编码器计数值并清零
 */
int16_t Encoder_Get(void)
{
    int16_t Temp = Encoder_Count;
    Encoder_Count = 0;
    return Temp;
}

/* ==================== 中断服务函数 ==================== */

/**
 * @brief EXTI0 中断服务函数(对应 PB0)
 * @note  中断函数务必简短!不要在这里写 Delay!
 */
void EXTI0_IRQHandler(void)
{
    // 在中断函数里判断标志位,必须用 EXTI_GetITStatus()
    if (EXTI_GetITStatus(EXTI_Line0) == SET)
    {
        /* 软件消抖:二次读取引脚电平,确认确实是低电平(下降沿) */
        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
        {
            // 判断另一相电平,确定旋转方向
            if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
            {
                Encoder_Count--;  // 反转
            }
        }
        // 中断内清除挂起标志位,必须用 EXTI_ClearITPendingBit()
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

/**
 * @brief EXTI1 中断服务函数(对应 PB1)
 */
void EXTI1_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line1) == SET)
    {
        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
        {
            if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
            {
                Encoder_Count++;  // 正转
            }
        }
        EXTI_ClearITPendingBit(EXTI_Line1);
    }
}

更多工程代码需要可在跳转链接查看:STM32_Project: 基于江科大的STM32的教程编写的程序

六、三大避坑指南

表格

坑点 原因 解决
进不了中断 忘了开 AFIO 时钟,或 GPIO_EXTILineConfig() 映射错了 检查 RCC 和 AFIO 映射
中断只进一次 ISR 末尾没调 EXTI_ClearITPendingBit() 中断内必须用 IT 系列清标志
中断里卡死 在 ISR 里调用了 Delay()SysTick 优先级不够 中断函数只干最小的事,绝不 Delay

七、结语

EXTI 配置的核心就是记住三张函数表 + 五步走流程。AFIO 只管映射,EXTI 只管触发方式,NVIC 只管优先级和使能,各司其职,不要搞混。

上面这套编码器代码经过实际项目验证,复制到 main.c 改改引脚就能编译通过。如果你想看 定时器编码器模式(更精准、不丢步)DMA 双缓冲采集ADC 多通道 DMA 等进阶玩法,可以移步我的 CSDN 专栏,源码和工程模板(STM32_Project: 基于江科大的STM32的教程编写的程序​​​​​​​)持续更新中。

觉得有用的话,点赞收藏不迷路,评论区见! 🎯

Logo

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

更多推荐