声明
文中内容为观看 BiliBili 视频【STM32入门教程-2023版 细致讲解 中文字幕】后学习并扩展总结。
本文章为个人学习使用,版面观感若有不适请谅解,文中知识仅代表个人观点,若出现错误,欢迎各位批评指正。
一、中断系统
1.1 中断的基本概念
在 STM32 微控制器的主程序线性执行流程中,当片上外设(如 GPIO、定时器、USART 等)或外部信号满足预设的中断触发条件 (即中断源,如电平变化、数据接收完成、定时溢出等)时,中断控制器会向 CPU(Cortex-M 内核)发送中断请求,CPU 即刻暂停当前指令流的执行,保存程序计数器(PC)、程序状态字(PSR)等关键寄存器值至栈区,转而跳转到该中断对应的中断服务函数(ISR) 执行处理逻辑;待中断服务函数执行完毕,CPU 从栈区恢复现场寄存器值,返回主程序被暂停的指令位置继续执行。
STM32 的中断系统基于 Cortex-M 内核的嵌套向量中断控制器(NVIC)实现,中断源分为两大类:
(1)内核异常 :如硬故障、内存管理故障、SysTick 定时器等,属于内核级中断;
(2)外设中断:如 GPIO_EXTI(外部中断)、TIM(定时器中断)、USART(串口中断)、ADC(模数转换中断)等,由 STM32 片上外设触发,是应用开发中最常用的中断类型。
1.2 STM32 中断优先级的分级与配置
中断优先级是 STM32 中断仲裁的核心规则,多中断源同时申请时按轻重缓急裁决,其硬件实现和配置逻辑如下:
(1)优先级的分级维度
STM32 的中断优先级由抢占优先级 (Preemption Priority) 和响应优先级(Sub Priority) 两级构成:
- a. 抢占优先级:决定中断的 "抢占权",优先级数值越小,中断越紧急;高抢占优先级的中断可打断低抢占优先级的中断服务函数(即中断嵌套的核心依据);
- b. 响应优先级:仅当多个中断源的抢占优先级相同时生效,用于裁决同时申请的同级别中断的响应顺序,数值越小优先级越高,但不具备抢占权。
(2)优先级分组机制
Cortex-M 内核的 NVIC 支持优先级分组(NVIC_PriorityGroup) ,共 5 种分组方式,本质是分配 "抢占优先级位数" 和 "响应优先级位数":
| 分组号 | 抢占优先级位数 | 响应优先级位数 | 示例(以 STM32F103 为例) |
|---|---|---|---|
| 0 | 0 位 | 4 位 | 仅响应优先级,0-15 级 |
| 1 | 1 位 | 3 位 | 抢占 0-1 级,响应 0-7 级 |
| 2 | 2 位 | 2 位 | 抢占 0-3 级,响应 0-3 级 |
| 3 | 3 位 | 1 位 | 抢占 0-7 级,响应 0-1 级 |
| 4 | 4 位 | 0 位 | 仅抢占优先级,0-15 级 |
1.3 STM32 中断嵌套的实现逻辑
中断嵌套是 STM32 中断系统灵活性的核心体现,由高优先级中断打断正在执行的低优先级中断,其硬件执行流程可拆解为:
(1)嵌套触发条件
a. 已有低抢占优先级的中断服务函数正在执行;
b. 新的中断源触发,且其抢占优先级高于当前执行的中断;
c. 全局中断使能位(PRIMASK)未关闭(PRIMASK=0 时允许中断,=1 时屏蔽所有可屏蔽中断)。
(2)嵌套执行流程
a. CPU 暂停当前中断服务函数的执行,将当前程序计数器(PC)、PSR、通用寄存器等保存至栈区(二次入栈);
b. 跳转到新的高优先级中断服务函数执行;
c. 高优先级中断处理完成后,CPU 从栈区恢复上一级中断的现场,继续执行被打断的低优先级中断服务函数;
d. 低优先级中断处理完成后,恢复主程序现场,返回主程序执行。
(3)嵌套限制与注意事项
a. 仅抢占优先级决定嵌套权限,响应优先级不影响嵌套;
b. 同一抢占优先级的中断无法嵌套,仅能按响应优先级顺序执行;
c. 中断嵌套深度受 STM32 栈空间大小限制,过度嵌套易导致栈溢出,需在开发中合理规划栈大小。
1.4 EXTI 外部中断
外部中断控制器(EXTI, Extern Interrupt)可实现对通用输入输出口(GPIO)电平信号的实时监测,当指定 GPIO 口发生电平变化时,EXTI 会即时向嵌套向量中断控制器(NVIC)发起中断请求,经 NVIC 优先级裁决后,可中断中央处理器(CPU)主程序流程,使 CPU 转去执行 EXTI 对应的中断服务程序;该控制器支持上升沿、下降沿、双边沿及软件触发四种中断触发方式,可适配所有 GPIO 口,且存在同引脚不可同时触发中断的使用约束,其包含 16 个 GPIO 引脚对应的中断通道,同时扩展支持可编程电压监测器(PVD)输出、实时时钟(RTC)闹钟、通用串行总线(USB)唤醒及以太网唤醒的中断触发,触发后的响应方式分为中断响应与事件响应两类。
从硬件架构来看,EXTI 的基本结构主要包含信号输入、选择映射、边沿检测与控制、中断事件输出四个核心环节:首先,来自 GPIOA、GPIOB、GPIOC 等多个 GPIO 端口的 16 路引脚信号,会先进入 AFIO(复用功能 IO)模块,由其完成中断引脚的选择映射,最终输出 16 路可配置的信号送入 EXTI 模块;同时,PVD、RTC、USB、以太网等外设的触发信号也会直接接入 EXTI。在 EXTI 内部,边沿检测及控制单元会对输入信号进行触发类型的识别与判定,支持上升沿、下降沿、双边沿及软件触发等多种方式。处理后的信号会分为两类输出:一类是 EXTI0-EXTI4、EXTI9_5、EXTI15_10 等中断请求信号,被送往NVIC(嵌套向量中断控制器)进行优先级裁决,以决定是否中断 CPU 主程序;另一类是事件信号,直接输出至其他外设以触发相应的硬件事件。这种架构既实现了 GPIO 引脚的灵活中断配置,也支持多外设的中断与事件触发,是实现系统实时响应的关键硬件基础。下图所示分别为 EXTI 控制器框图及 EXTI 基本结构图。


1.5 AFIO复用IO口
在 STM32 微控制器体系中,AFIO(Alternate Function I/O,复用功能 IO) 是负责管理 GPIO 引脚功能映射与中断信号路由的关键外设模块,其核心作用是为通用 IO 引脚赋予灵活的复用功能,并保障中断信号的精准传输。AFIO 主要承担两大核心任务:
(1)复用功能引脚重映射 :可将部分片内外设的默认引脚映射到其他 GPIO 引脚上,从而优化 PCB 布局并避免引脚资源冲突。例如,可将 SPI、USART 等外设的通信引脚重映射到更便于布线的 GPIO 端口。
(2)中断引脚选择 :作为 EXTI(外部中断控制器)与 GPIO 端口之间的桥梁,AFIO 从 GPIOA、GPIOB 等多个端口的 16 路引脚中选择特定信号,将其映射到 EXTI 的 16 个中断通道。这一机制确保了任意 GPIO 引脚均可触发外部中断,且同一引脚不会被多个中断通道同时占用。
此外,AFIO 还支持对 GPIO 引脚的模拟功能控制与事件输出配置,是提升 STM32 引脚资源利用率与系统硬件设计灵活性的重要支撑。
二、对射式红外传感器及旋转编码器计数的实现
2.1 对射式红外传感器介绍
对射式红外传感器是一种基于红外光发射与接收的非接触式光电检测器件,由红外发射端与接收端分体式布置构成,发射端持续向外发射红外光束,接收端则实时接收对应光束,当检测目标进入发射与接收端的光路之间时,光束被遮挡,接收端的光电转换模块因光信号消失产生电平变化,以此实现对目标存在、位置或运动状态的精准检测,该传感器具备响应速度快、检测精度高、抗干扰能力较强的特性,且检测距离可根据实际需求适配调整,广泛应用于物体计数、行程限位、障碍物检测等工业控制与智能感知场景。

2.2 旋转编码器介绍
旋转编码器是一种用于精准检测旋转机构的位置、转速及旋转方向的机电传感装置,其核心工作原理为旋转轴随被测机构转动时,输出端可同步产生与旋转速度、方向相关的方波电信号,通过采集并解析该信号的频率与相位特征,即可量化获取旋转轴的实时转速与转向信息;该传感器依据检测原理可分为机械触点式、霍尔传感器式与光栅式三类,不同类型依托机械接触、电磁感应、光电转换的不同检测机制实现信号输出,适配于工业控制、精密测量、智能装备等不同场景下的旋转运动参数检测需求。

2.3 对射式红外传感器计数
-
首先,按下图接线方式,搭建面包板电路并连接 OLED 显示屏、对射式红外传感器和旋转编码器,然后将 DAP-Link / ST-Link 连接到 STM32 最小系统板上,为使 OLED 显示屏的 VCC 和 GND 正确连接正负极,请先连接对应正负极跳线(或直接使用 GPIO 口进行供电)。

-
直接复制先前演示的已有文件目录,重命名并双击后缀名为 .uvprojx 的文件打开工程文件,并对 main.c 进行修改,工程中所使用的全部头文件其详细内容已放于文末,
CountSensor.h头文件中封装了对射式红外传感器中断计数功能实现所需的全部底层配置逻辑,具体涵盖 STM32F103 系列单片机的时钟使能、GPIO 接口配置、AFIO(复用功能 IO)映射配置、EXTI(外部中断 / 事件控制器)参数配置,以及 NVIC(嵌套向量中断控制器)优先级与通道配置。#include "stm32f10x.h" // Device header
#include <OLED.h>
#include <CountSensor.h>int main(void){
OLED_Init();
CountSensor_Init();
OLED_ShowString(1, 1, "Count:");while(1){ OLED_ShowNum(1, 7, CountSensor_Get(), 5); }}
-
在依据教学视频开展实验效果复现过程中,观测到如下异常现象:当挡光片通过对射式红外传感器检测区域时,计数模块的显示数值单次跳变增量并非设定值 1,而是出现 2、3 等无规律的随机增量;经查阅相关资料,判定该异常由传感器检测信号的抖动效应所致,因此提供以下两种解决方案。

-
方案一:在
CountSensor.c源文件的外部中断服务函数EXTI15_10_IRQHandler中,于计数逻辑判定语句前嵌入Delay_ms(500);毫秒级延时函数(详细代码已在文末提供)。需说明的是,中断服务函数的设计原则为精简高效、快速响应,延时操作本身违背该原则,但本方案仅作为传感器信号抖动抑制的演示性验证手段,旨在快速复现并消除挡光片单次触发传感器时计数增量异常(非设定值 1)的抖动效应,不涉及最终工程化实现。 -
方案二:在
CountSensor.c源文件的外部中断服务函数EXTI15_10_IRQHandler中, 针对中断标志位判定语句if (EXTI_GetITStatus(EXTI_Line12) == SET)增设调试断点;随后将系统切换至调试运行模式,当程序执行至该断点处触发暂停时,手动点击调试器的 "Run (F5)" 运行按钮,通过人为干预中断执行流程的方式,实现单次挡光触发仅完成一次计数操作,以此规避传感器信号抖动引发的多次中断触发问题。 -
通过上述两种验证方案的实施,传感器信号抖动引发的计数增量异常现象已完全消除;如下图所示,OLED 显示模块的计数数值能够严格匹配对射式红外传感器的单次挡光触发动作,即挡光片每通过一次传感器检测区域,计数模块仅完成 1 次增量更新,计数功能恢复至设计预期的正常状态。

2.4 旋转编码器计数
-
保持上述对射式红外传感器计数的面包板连线方式,再次复制已有文件目录,重命名并双击后缀名为 .uvprojx 的文件打开工程文件,并对 main.c 进行修改。
Encoder.h头文件中封装了旋转编码器中断计数功能实现所需的全部底层配置逻辑,具体涵盖 STM32F103 系列单片机的时钟使能、GPIO 接口配置、AFIO(复用功能 IO)映射配置、EXTI(外部中断 / 事件控制器)参数配置,以及 NVIC(嵌套向量中断控制器)优先级与通道配置。#include "stm32f10x.h" // Device header
#include <OLED.h>
#include <Encoder.h>int16_t Num;
int main(void){
OLED_Init();
Encoder_Init();
OLED_ShowString(1, 1, "Count:");while(1){ Num += Encoder_Get(); OLED_ShowSignedNum(1, 7, Num, 5); }}

-
在依据教学视频开展旋转编码器计数功能的实验复现过程中,发现程序下载验证阶段出现功能异常:程序下载成功但旋转编码器无计数输出。经排查,该异常源于硬件接线错误 ------ 因演示所用 B1、B0 GPIO 接口位于排针中段,且 STM32 开发板 GPIO 接口呈密集排列状态,操作中误将旋转编码器的 A、B 相信号线接至 B10 与 B1 接口(非预设的 B0、B1 接口),导致硬件电气连接与程序逻辑不匹配,最终引发计数功能失效。
为此,在复现该实验功能且出现计数无效现象时,应优先排查硬件接线的正确性;同时可通过下述 LED 点灯验证代码,预先对 GPIO 接口的电气连通性及接线正确性进行确认,避免因接线错误导致的功能异常。#include "stm32f10x.h" // Device header
int main(void){
GPIO_InitTypeDef GPIO_InitStructures;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructures.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructures.GPIO_Pin = GPIO_Pin_6; // 修改为需要验证的接口 GPIO_InitStructures.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructures); GPIO_WriteBit(GPIOA, GPIO_Pin_6, Bit_RESET); // 修改为需要验证的接口 while(1){ }}

三、演示代码关联的头文件与源文件说明
-
OLED 相关头文件请从 STM32 学习 ------ 个人学习笔记4(OLED 显示屏及调试工具) 文末查看,此处不重复展示。
-
CountSensor.c
#include "stm32f10x.h" // Device header
#include <Delay.h>uint16_t CountSensor_Count;
void CountSensor_Init(void){
// 提前声明需要用到的结构体
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;// 配置时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 配置 GPIO GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // 配置 AFIO GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource12); // 配置 EXTI EXTI_InitStructure.EXTI_Line = EXTI_Line12; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;// EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
// EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
EXTI_Init(&EXTI_InitStructure);// 配置 NVIC NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure);}
uint16_t CountSensor_Get(void){
return CountSensor_Count;
}void EXTI15_10_IRQHandler(void){
Delay_ms(500);
if (EXTI_GetITStatus(EXTI_Line12) == SET){
CountSensor_Count ++;
EXTI_ClearITPendingBit(EXTI_Line12);
}
} -
CountSensor.h
#include "stm32f10x.h" // Device header
#ifndef __COUNT_SENSOR_H
#define __COUNT_SENSOR_Hvoid CountSensor_Init(void);
uint16_t CountSensor_Get(void);#endif
-
Encoder.c
#include "stm32f10x.h" // Device header
int16_t Encoder_Count;
void Encoder_Init(void){
// 提前声明需要用到的结构体
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;// 配置时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 配置 GPIO 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); // 配置 AFIO GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1); // 配置 EXTI EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_Init(&EXTI_InitStructure); // 配置 NVIC NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; NVIC_Init(&NVIC_InitStructure);}
uint16_t Encoder_Get(void){
int16_t Temp;
Temp = Encoder_Count;
Encoder_Count = 0;
return Temp;
}void EXTI0_IRQHandler(void){
if (EXTI_GetITStatus(EXTI_Line0) == SET){
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0){
Encoder_Count --;
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
}void EXTI1_IRQHandler(void){
if (EXTI_GetITStatus(EXTI_Line1) == SET){
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0 ) == 0){
Encoder_Count ++;
}
EXTI_ClearITPendingBit(EXTI_Line1);
}
} -
Encoder.h
#include "stm32f10x.h" // Device header
#ifndef __ENCODER_H
#define __ENCODER_Hvoid Encoder_Init(void);
uint16_t Encoder_Get(void);#endif
文中部分知识参考:B 站 ------ 江协科技;百度百科