STM32学习笔记(六)EXTI外部中断(下)

STM32学习笔记(六)EXTI外部中断(下)

上一篇介绍了中断系统、NVIC、EXTI 和旋转编码器的基本原理,本篇将通过两个实际项目进一步掌握 EXTI:首先使用对射式红外传感器完成物体计次,然后使用旋转编码器实现带方向的加减计数。

两个项目虽然使用的传感器不同,但信号传递过程基本一致:外部模块产生电平变化,GPIO 接收信号,AFIO 选择对应的 EXTI 线路,EXTI 检测有效边沿,NVIC 裁决中断优先级,最后由 CPU 执行中断服务程序。

一、对射式红外传感器计次

1、硬件接线与工作原理

对射式红外传感器通常包含红外发射端和接收端。当挡光片、编码盘或其他物体从发射端与接收端之间经过时,红外光路被切断,模块的数字输出端 DO 随之发生电平变化。

本实验的接线如下:

传感器引脚 连接位置 作用
VCC 电源正极 为传感器模块供电
GND 电源负极 与 STM32 共地
DO PB14 向 STM32 输出数字电平信号

当物体经过传感器时,DO 输出电平跳变。PB14 接收到下降沿后,通过 EXTI14 产生外部中断;CPU 进入中断服务程序,将计数变量加一;主循环再把计数值显示到 OLED 上。

可以把整套系统理解成一道装有电子门铃的闸门:物体经过时遮住红外光,相当于按响门铃;EXTI 负责发现门铃信号,NVIC 负责通知 CPU,计数变量则像门口的计数器,每检测到一次有效通行就增加一。

为了让程序结构更加清晰,可以在 HardWare 组中建立 CountSensor.cCountSensor.h,将传感器初始化、计数读取和中断处理统一封装到 CountSensor 模块中。主程序只需要调用初始化函数和读取函数,不必关心底层的 EXTI 配置细节。

2、外部中断的配置流程

模块的第一个函数通常是初始化函数,它负责配置模块需要使用的全部硬件资源。对射式红外传感器使用外部中断,因此需要把从 GPIO 到 CPU 的整条中断信号通路打通。

配置步骤如下:

  1. 配置 RCC,使能 GPIOB 和 AFIO 的时钟;
  2. 配置 GPIO,将 PB14 设置为输入模式;
  3. 配置 AFIO,把 PB14 连接到 EXTI14;
  4. 配置 EXTI,选择中断模式和有效触发边沿;
  5. 配置 NVIC,使能对应中断通道并设置优先级;
  6. 编写中断服务程序,完成计数并清除挂起标志。

可以把这几个模块理解成一条接力路线:GPIO 接到信号后交给 AFIO,AFIO 把信号送到正确的 EXTI 线路,EXTI 发现边沿后向 NVIC 申请中断,NVIC 最终安排 CPU 处理。

GPIOB 和 AFIO 都位于 APB2 总线上,因此需要通过 RCC 主动使能时钟。EXTI 没有单独的 RCC 时钟使能位,NVIC 则属于 Cortex-M3 内核外设,也不需要通过 RCC 额外开启时钟。

配置 AFIO

ST 没有为 AFIO 单独建立一套标准外设库文件,AFIO 的相关函数位于 GPIO 库文件中。常见函数如下。

c 复制代码
void GPIO_AFIODeInit(void);

该函数用于复位 AFIO 外设。调用后,AFIO 的相关配置会恢复到默认状态。

c 复制代码
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

该函数用于锁定指定 GPIO 引脚的配置,防止程序运行过程中意外修改相关参数。引脚一旦被锁定,通常需要系统复位后才能重新配置。

c 复制代码
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
void GPIO_EventOutputCmd(FunctionalState NewState);

这两个函数用于配置和控制 AFIO 的事件输出功能,在基础项目中使用较少。

c 复制代码
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);

这两个函数比较重要:

  • GPIO_PinRemapConfig() 用于配置外设引脚重映射。第一个参数选择重映射方式,第二个参数用于使能或关闭该重映射;
  • GPIO_EXTILineConfig() 用于选择某条 EXTI 线连接到哪个 GPIO 端口,是本实验配置外部中断时需要使用的函数。
c 复制代码
void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface);

该函数用于配置以太网介质接口。STM32F103C8T6 不包含以太网外设,因此本实验不会使用。

调用 GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14) 后,AFIO 会把 GPIOB 的第 14 号引脚连接到 EXTI14。可以把 AFIO 想象成一个多选一开关:EXTI14 的输出线路固定,但输入端可以在 PA14、PB14、PC14 等同编号引脚之间进行选择,本实验把开关拨到 PB14。

配置 EXTI

AFIO 完成线路选择后,PB14 的电平信号会进入 EXTI14。接下来需要配置 EXTI 的工作模式、触发边沿和线路状态。

c 复制代码
void EXTI_DeInit(void);

该函数用于清除 EXTI 的全部配置,使其恢复到上电后的默认状态。

c 复制代码
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);

该函数根据结构体中的参数初始化 EXTI,是配置外部中断时最主要的函数。

c 复制代码
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct);

该函数用于给 EXTI 初始化结构体赋予默认值。DeInitInitStructInit 是标准外设库中常见的一组函数,许多外设都采用相似的设计方式。

c 复制代码
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);

该函数用于通过软件产生一次 EXTI 请求。调用时指定一条中断线,就可以在没有外部电平变化的情况下模拟一次触发,常用于调试或特殊控制场景。

c 复制代码
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);

该函数用于读取指定 EXTI 线路的挂起标志。

c 复制代码
void EXTI_ClearFlag(uint32_t EXTI_Line);

该函数用于清除指定 EXTI 线路的挂起标志。

c 复制代码
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);

该函数用于判断指定线路是否产生了有效的中断请求,返回值为 SETRESET。它不仅检查挂起标志,还会考虑对应中断线路是否已经使能,因此更适合在中断服务程序中使用。

c 复制代码
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);

该函数用于清除中断挂起标志。EXTI 的挂起位需要通过"写 1"清除,标准外设库函数已经封装了这一操作。

EXTI_GetFlagStatus()EXTI_ClearFlag() 直接读取、清除挂起标志,不关心该线路是否允许产生中断;EXTI_GetITStatus()EXTI_ClearITPendingBit() 更适合在中断服务程序中判断并处理中断请求。

EXTI_InitTypeDef 结构体包含 4 个主要成员:

  • EXTI_Line:选择需要配置的 EXTI 线路;
  • EXTI_LineCmd:使能或关闭所选线路;
  • EXTI_Mode:选择中断模式或事件模式;
  • EXTI_Trigger:选择上升沿、下降沿或双边沿触发。

本实验选择 EXTI14、中断模式和下降沿触发:

c 复制代码
//配置EXTI
	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line14;
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
	
	EXTI_Init(&EXTI_InitStructure);

下降沿表示信号从高电平变为低电平。选择这种方式的前提是传感器在检测到遮挡时,DO 确实会由高电平变为低电平。如果所用模块的输出极性相反,就需要根据实际波形调整触发边沿。

配置 NVIC

NVIC 是 Cortex-M3 内核外设,其标准外设库函数位于 misc.hmisc.c 中。

c 复制代码
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);

该函数用于选择 NVIC 的优先级分组方式。优先级分组决定 4 个有效优先级位如何分配给抢占优先级和响应优先级。

c 复制代码
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);

该函数根据结构体中的参数初始化指定的 NVIC 中断通道。

c 复制代码
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);
void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState);

这两个函数分别用于设置中断向量表位置和系统低功耗模式,在当前实验中不会使用。

NVIC_InitTypeDef 结构体的主要成员如下:

  • NVIC_IRQChannel:选择需要配置的中断通道;
  • NVIC_IRQChannelCmd:使能或关闭所选通道;
  • NVIC_IRQChannelPreemptionPriority:设置抢占优先级;
  • NVIC_IRQChannelSubPriority:设置响应优先级,也叫子优先级。

EXTI14 属于 EXTI15_10_IRQn 通道,因为 EXTI10~EXTI15 共用同一个 NVIC 入口。本实验采用优先级分组 2,并把抢占优先级和响应优先级都设置为 1。

完整的初始化函数如下:

c 复制代码
void CountSensor_Init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
	
	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);
	
	//配置AFIO
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
	
	//配置EXTI
	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line14;
	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_InitTypeDef NVIC_InitStructure;
	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);
}

3、中断服务程序

STM32 的中断服务函数名称由启动文件中的中断向量表确定。函数名必须与启动文件完全一致,否则链接器可能仍然使用默认中断函数,中断发生后便无法进入我们编写的程序。最稳妥的方法是直接从启动文件中复制对应名称。

EXTI10~EXTI15 共用 EXTI15_10_IRQHandler()。因此,进入这个中断服务程序后,不能仅凭函数名称确定是哪一条 EXTI 线触发了请求,必须调用 EXTI_GetITStatus() 检查 EXTI14 的挂起状态。

确认 EXTI14 触发后,程序将计数变量加一。中断处理结束前,还必须调用 EXTI_ClearITPendingBit() 清除挂起标志。如果不清除,EXTI 会持续向 NVIC 提出中断请求,CPU 退出中断后又会立刻再次进入,主程序几乎无法正常执行。

中断服务程序应当尽量简短,只完成计数、保存状态和清除标志等必要操作。OLED 刷新、长时间延时和复杂计算应尽量放在主循环中完成。

4、主程序与模块调用

主程序先初始化 OLED 和传感器模块,再在 OLED 第一行显示固定文字。进入循环后,程序不断调用 CountSensor_Get() 读取计数值,并将结果显示出来。

c 复制代码
//main.c
/* 头文件声明区域 */
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"

/* 变量声明区域 */

/* Main */
int main()
{	 
	OLED_Init();
	CountSensor_Init();
	
	OLED_ShowString(1,1,"Count:");
	
	
	while(1)
	{
		OLED_ShowNum(1,7,CountSensor_Get(),5); 
	}
}

CountSensor.c 负责配置底层外设、处理中断和保存计数值。主程序不需要直接操作 EXTI 或 NVIC,只需要通过模块公开的函数使用计数结果。

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

uint16_t CountSensor_Count;


void CountSensor_Init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
	
	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);
	
	//配置AFIO
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
	
	//配置EXTI
	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line14;
	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_InitTypeDef NVIC_InitStructure;
	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);
}

//中断函数
void EXTI15_10_IRQHandler(void)
{
	if(EXTI_GetITStatus(EXTI_Line14) == SET)
	{
		CountSensor_Count++;
		EXTI_ClearITPendingBit(EXTI_Line14);
	}
}

//返回函数
uint16_t CountSensor_Get()
{
	return CountSensor_Count;
}

5、重复计数问题

实际测试时,可能出现物体只遮挡一次,计数值却连续增加多次的情况。这通常不是 CountSensor_Count++ 执行异常,而是传感器输出在临界位置反复跳变,导致 EXTI 检测到多个下降沿。

对射式红外传感器的数字输出通常来自比较器。当挡光片恰好位于光路边缘、供电存在噪声或灵敏度设置不合适时,比较器输出可能在高、低电平之间来回变化。EXTI 会把每个高到低的变化都当成一次新的中断,因此一次物理遮挡可能被识别为多次。

简单地在中断服务程序中加入几十毫秒延时,可以过滤一部分短促抖动,但会让 CPU 长时间停留在中断中,影响其他任务和低优先级中断;改成双边沿触发后再把结果除以 2,也不能保证每次遮挡一定只产生两个边沿,遇到抖动时仍会出错。

更稳定的处理方式是采用"状态锁":第一次检测到遮挡后立即计数并锁定,在传感器恢复未遮挡状态且稳定保持一段时间后,再解除锁定,允许下一次计数。这样可以保证一个完整的"未遮挡→遮挡→重新未遮挡"过程最多计数一次。

硬件方面还可以采取以下措施:

  • 调节传感器模块的灵敏度电位器,使输出指示灯切换干净;
  • 确保传感器与 STM32 共地;
  • 缩短信号线,避免信号线靠近强干扰源;
  • 必要时增加合适的外部上拉电阻和滤波电容;
  • 对高速计数场景,结合示波器或逻辑分析仪观察实际输出波形。

二、旋转编码器计次

1、硬件接线与模块划分

把旋转编码器模块插入面包板,VCCGND 分别连接电源正、负极,A、B 两相输出分别连接到 PB0 和 PB1。单片机与编码器必须共地,否则两路电平没有统一的参考,程序可能无法正确读取。

编码器引脚 连接位置 作用
VCC 电源正极 为模块供电
GND 电源负极 与 STM32 共地
A PB0 输出 A 相方波信号
B PB1 输出 B 相方波信号

在 Keil 中继续采用模块化编程,将旋转编码器相关程序封装到 Encounter.cEncounter.h 中。模块负责初始化 GPIO、AFIO、EXTI 和 NVIC,并向主程序提供旋转增量。

2、为什么需要 A、B 两相

旋转编码器转动时,A 相和 B 相都会输出方波,但两路信号并不是同时变化,而是存在固定的相位差。旋转方向改变后,两路信号的先后顺序也会交换。

可以把 A、B 两相想象成两名间隔半步行走的人:向一个方向前进时,A 先迈步、B 后迈步;反方向前进时,B 先迈步、A 后迈步。单片机只要观察谁先发生变化,就能判断旋转方向。

如果只读取一路信号,程序只能知道编码器发生了转动,却无法判断方向。加入第二路信号后,程序便可以在一相触发中断时读取另一相的电平,从而区分正转和反转。

本实验把 A 相连接到 PB0,也就是 EXTI0;B 相连接到 PB1,也就是 EXTI1。两条线路都采用下降沿触发:

  • EXTI0 触发时,程序读取 PB1 的电平;
  • EXTI1 触发时,程序读取 PB0 的电平;
  • 根据另一相当前的高、低电平决定计数值增加还是减少。

正转对应加一还是减一,取决于编码器结构、A/B 接线顺序和程序中的方向定义。如果实际旋转方向与显示结果相反,可以交换 A、B 两路接线,或者在后续程序中交换加减逻辑。

3、初始化配置

旋转编码器模块的初始化过程与对射式红外传感器相似,但需要同时配置 PB0 和 PB1 两个输入引脚:

  1. 使能 GPIOB 和 AFIO 时钟;
  2. 将 PB0、PB1 配置为上拉输入;
  3. 通过 AFIO 将 PB0 映射到 EXTI0,将 PB1 映射到 EXTI1;
  4. 将 EXTI0 和 EXTI1 配置为下降沿中断;
  5. 在 NVIC 中分别使能 EXTI0_IRQnEXTI1_IRQn
  6. 为两个中断通道设置合适的优先级。

PB0 和 PB1 的编号不同,因此可以分别连接到 EXTI0 和 EXTI1,不会出现同编号 GPIO 争用同一条 EXTI 线的问题。

完整的模块程序如下:

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

int16_t Encounter_Count;

void Encounter_Init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
	
	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);
	
	//配置AFIO
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0);
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
	
	//配置EXTI
	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line1 | EXTI_Line0;
	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_InitTypeDef NVIC_InitStructure;
	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 = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
	
	NVIC_Init(&NVIC_InitStructure);
}

//中断函数
void EXTI0_IRQHandler()
{
	if(EXTI_GetITStatus(EXTI_Line0) == SET)
	{
		if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0)//反转
		{
			Encounter_Count--;
		}
			
		
		EXTI_ClearITPendingBit(EXTI_Line0);
	}
}

void EXTI1_IRQHandler()
{
	if(EXTI_GetITStatus(EXTI_Line1) == SET)
	{
		if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0) == 0)//反转
		{
			Encounter_Count++;
		}
		
		EXTI_ClearITPendingBit(EXTI_Line1);
	}
}

//返回函数
int16_t Encounter_Get()
{
	int16_t temp;
	temp = Encounter_Count;
	Encounter_Count = 0;
	return temp;
}

4、两个中断函数如何判断方向

EXTI0 和 EXTI1 分别拥有独立的中断服务函数,因此程序可以判断是哪一相先出现下降沿。

当 EXTI0 触发时,说明 A 相刚刚从高电平变为低电平。此时程序读取 B 相,也就是 PB1:如果 PB1 已经处于低电平,就按照当前接线定义让 Encounter_Count 减一。

当 EXTI1 触发时,说明 B 相刚刚从高电平变为低电平。此时程序读取 A 相,也就是 PB0:如果 PB0 已经处于低电平,就让 Encounter_Count 加一。

这种判断方式利用了 A、B 两相之间的时间差。假设某个方向下 A 先下降、B 后下降,那么 B 下降时 A 已经为低电平,可以满足加一条件;反方向旋转时,B 先下降、A 后下降,A 下降时 B 已经为低电平,可以满足减一条件。

可以把这个过程概括为:

触发线路 说明 读取另一相 满足条件后的操作
EXTI0 A 相出现下降沿 读取 PB1,也就是 B 相 Encounter_Count--
EXTI1 B 相出现下降沿 读取 PB0,也就是 A 相 Encounter_Count++

两个中断服务程序结束前都需要清除各自的 EXTI 挂起标志,否则程序会反复进入同一个中断。

5、增量计数的返回方式

Encounter_Count 保存的是从上一次读取之后新产生的旋转增量,而不是最终累计值。Encounter_Get() 先把当前增量保存到局部变量 temp,再把模块内部计数清零,最后将本次增量返回给主程序。

可以把它理解成收银台定期清点零钱:模块只记录"这一小段时间里增加或减少了多少",主程序每次取走记录后,模块重新从 0 开始统计。主程序中的 Num 才负责保存从上电开始的累计结果。

这种设计的好处是模块与具体业务逻辑相互独立。主程序既可以把返回值累加到菜单选项,也可以用于调整音量、亮度、速度或其他参数。

在低速旋转的基础实验中,这种先读取再清零的写法通常可以正常工作。如果编码器转速很高,中断可能恰好发生在"读取"和"清零"之间,存在丢失一次增量的可能。更严谨的工程可以在读取和清零的极短时间内临时保护临界区,或者让模块直接维护累计值。

6、主程序

主程序使用有符号变量 Num 保存累计结果。因为旋转编码器既可能正转,也可能反转,所以计数值可能增加或减少,需要使用 int16_t,并通过 OLED_ShowSignedNum() 显示符号。

c 复制代码
//main.c
/* 头文件声明区域 */
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "Encounter.h"

/* 变量声明区域 */
int16_t Num;

/* Main */
int main()
{	 
	OLED_Init();
	Encounter_Init();
	
	OLED_ShowString(1,1,"Num:");
	
	while(1)
	{
		Num += Encounter_Get();
		OLED_ShowSignedNum(1,5,Num,5);
	}
}

循环中的 Encounter_Get() 返回本轮新增的旋转量:正方向可能返回正数,反方向可能返回负数,没有转动时返回 0。主程序使用 Num += Encounter_Get() 将增量累加到最终结果中,然后刷新 OLED 显示。

7、机械抖动与调试方法

机械旋转编码器内部依靠金属触点通断产生信号,触点切换瞬间可能发生抖动。如果一次拨动导致计数连续增加或减少多次,需要检查模块是否已经带有 RC 滤波电路,并结合实际波形增加软件消抖或更完整的正交状态机。

不建议直接在 EXTI 中断服务程序中加入很长的延时,因为编码器转动时可能连续产生脉冲,延时会降低可检测的最高转速,还可能阻塞其他中断。对于低速实验,可以通过短时间滤波改善稳定性;对于要求较高的项目,可以使用定时器编码器接口或状态机对完整的 A、B 相序列进行解码。

调试时可以按照以下顺序检查:

  1. 确认 A、B 两路分别连接 PB0 和 PB1,并与 STM32 共地;
  2. 静止时读取 PB0、PB1,确认电平不会无故跳变;
  3. 缓慢旋转编码器,观察 A、B 两相是否按顺序变化;
  4. 确认 EXTI0、EXTI1 都使用下降沿触发;
  5. 检查两个中断服务程序是否分别清除了 EXTI0、EXTI1 的挂起标志;
  6. 如果方向相反,交换 A、B 接线或调整加减逻辑;
  7. 如果高速旋转时漏计,减少中断内工作量并考虑定时器编码器模式。

三、附录

1、对射式红外传感器计次

c 复制代码
//main.c
/* 头文件声明区域 */
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"

/* 变量声明区域 */

/* Main */
int main()
{	 
	OLED_Init();
	CountSensor_Init();
	
	OLED_ShowString(1,1,"Count:");
	
	
	while(1)
	{
		OLED_ShowNum(1,7,CountSensor_Get(),5); 
	}
}

//CountSensor.c
#include "stm32f10x.h"                  // Device header

uint16_t CountSensor_Count;


void CountSensor_Init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
	
	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);
	
	//配置AFIO
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
	
	//配置EXTI
	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line14;
	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_InitTypeDef NVIC_InitStructure;
	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);
}

//中断函数
void EXTI15_10_IRQHandler(void)
{
	if(EXTI_GetITStatus(EXTI_Line14) == SET)
	{
		CountSensor_Count++;
		EXTI_ClearITPendingBit(EXTI_Line14);
	}
}

//返回函数
uint16_t CountSensor_Get()
{
	return CountSensor_Count;
}

2、旋转编码器计次

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

int16_t Encounter_Count;

void Encounter_Init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
	
	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);
	
	//配置AFIO
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0);
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
	
	//配置EXTI
	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line1 | EXTI_Line0;
	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_InitTypeDef NVIC_InitStructure;
	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 = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
	
	NVIC_Init(&NVIC_InitStructure);
}

//中断函数
void EXTI0_IRQHandler()
{
	if(EXTI_GetITStatus(EXTI_Line0) == SET)
	{
		if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0)//反转
		{
			Encounter_Count--;
		}
			
		
		EXTI_ClearITPendingBit(EXTI_Line0);
	}
}

void EXTI1_IRQHandler()
{
	if(EXTI_GetITStatus(EXTI_Line1) == SET)
	{
		if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0) == 0)//反转
		{
			Encounter_Count++;
		}
		
		EXTI_ClearITPendingBit(EXTI_Line1);
	}
}

//返回函数
int16_t Encounter_Get()
{
	int16_t temp;
	temp = Encounter_Count;
	Encounter_Count = 0;
	return temp;
}

//main.c
/* 头文件声明区域 */
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "Encounter.h"

/* 变量声明区域 */
int16_t Num;

/* Main */
int main()
{	 
	OLED_Init();
	Encounter_Init();
	
	OLED_ShowString(1,1,"Num:");
	
	while(1)
	{
		Num += Encounter_Get();
		OLED_ShowSignedNum(1,5,Num,5);
	}
}
相关推荐
MartinYeung51 小时前
[论文学习]Latent Fusion Jailbreak: 融合有害与无害表征以诱导大语言模型产生不安全输出
学习·安全·语言模型
星恒随风1 小时前
C++ 哈希详解(二):开放定址、哈希桶与 C++ 哈希表底层实现
c++·笔记·学习·哈希算法·散列表
迪丽热爱1 小时前
每日英语-17
学习
2501_926978331 小时前
AGI封锁的物理边界:发现模式决定封锁可行性
人工智能·经验分享·笔记·ai写作·agi
Mr_liu_6662 小时前
Claude非专业入门实战笔记(附录):关闭Telemetry-遥测,消除无用网络请求与日志噪音
linux·笔记·ubuntu·ai编程
打工陈2 小时前
电路理论知识学习--(4)RC、LC、LR低通滤波对比
学习·智能硬件
Hhy_11073 小时前
《C++深度解构02》类和对象(上)——定义规范、内存对齐与 this 指针全解
c语言·开发语言·c++·学习·visual studio
水水不水啊3 小时前
DeepSeek自带的一些插件的功能介绍
笔记·ai·deepseek·harness·dsh
我爱cope3 小时前
【计算机网络 | 数据链路层3:MAC 地址是什么?它和 IP 地址有什么区别?】
学习·tcp/ip·计算机网络·智能路由器·数据链路层