STM32实战:红外传感器与旋转编码器的中断驱动计数实现
1. 从零开始:为什么我们需要中断驱动的传感器计数?
如果你玩过STM32,或者任何单片机,肯定试过用轮询的方式去读取一个传感器的状态。比如,你想知道一个红外传感器前面有没有东西经过,或者一个旋转编码器转了多少格。最直接的想法就是,在主循环里不停地去读那个引脚的电平,一旦发现变化,就加个1。这个方法简单粗暴,刚开始学的时候我也这么干过。
但很快你就会发现不对劲。你的主循环可能还要干别的事,比如刷新屏幕、处理数据、响应按键。如果传感器信号变化很快,比如电机转速很高,或者你手动快速转动编码器,你的主循环很可能“看漏”几次变化。更糟糕的是,轮询本身就很耗费CPU时间,让整个系统变得笨重、反应迟钝。这就好比你在厨房一边烧水,一边炒菜,但你非要死死盯着水壶,等它烧开,结果锅里的菜糊了。你明明可以设个闹钟(中断),水开了闹钟响,你再过来关火,中间的时间完全可以安心炒菜。
中断,就是单片机里的这个“闹钟”。它允许外部事件(比如传感器引脚电平变化)主动打断CPU当前的工作,让CPU立刻去处理这个紧急事件,处理完了再回来继续干原来的活。对于计数这种对实时性要求高、但又不想占用太多CPU资源的任务,中断简直是量身定做的解决方案。
所以,今天我们要聊的,就是如何用STM32的中断功能,优雅且高效地实现对射式红外传感器和旋转编码器的计数。我会把我自己调试过程中踩过的坑、总结的技巧都分享出来,目标是让你看完就能动手做出来,并且真正理解背后的原理。无论你是刚接触STM32的新手,还是想优化现有项目的朋友,这篇文章都会很有用。
2. 硬件伙伴:认识对射式红外传感器与旋转编码器
在写代码之前,我们得先了解手头的“兵器”。知其然,更要知其所以然,这样出了问题你才知道往哪儿排查。
2.1 对射式红外传感器:你的电子“门卫”
我们用的这种槽型对射式红外传感器,里面其实就两样东西:一个红外发光二极管(发射端)和一个光电三极管(接收端),它们面对面放在一个U型槽里。没东西挡在槽中间时,发射管发出的红外光被接收管收到,接收管导通,输出低电平(通常是0V)。一旦有东西(比如电机码盘的齿、传送带上的工件)穿过这个槽,挡住了光路,接收管就收不到光了,于是截止,输出高电平(比如3.3V或5V)。
关键点:它的输出是数字开关量,只有0和1两种状态,非常干净,可以直接接到单片机的GPIO口上。我们就是通过检测这个电平从低到高(或从高到低)的跳变沿来计数的。每遮挡一次,就产生一个脉冲。数脉冲的个数,就等于数了遮挡的次数。这玩意儿精度高、反应快,而且不受环境可见光影响,非常适合做转速测量、产品计数或者位置限位。
我手头这个模块工作电压是3.3V-5V,和STM32开发板完美兼容。DO(数字输出)引脚接STM32的IO口,VCC和GND接好,电源指示灯亮起,就可以开始工作了。接线时千万注意电压别接反,模块上的LED指示灯能帮你快速判断状态:无遮挡时,开关指示灯常亮;有遮挡时,开关指示灯熄灭。
2.2 旋转编码器:不仅仅是“旋钮”
旋转编码器,尤其是EC11这种增量式编码器,在旋钮、调参场合太常见了。它内部相当于两个靠得很近的机械开关(对应A、B相),但通过光电或机械方式实现,输出的是两路相位差90度的方波信号。
它的精妙之处就在于这两路信号的相位关系。当你顺时针旋转时,A相信号的上升沿会领先于B相;逆时针旋转时,则B相信号的上升沿领先于A相。通过检测A相变化时B相的电平状态,我们就能判断出旋转方向。同时,每转动一个固定的最小角度(称为一“格”或一个“脉冲”),A相和B相都会各自产生一个跳变。我们可以对其中一路的跳变进行计数,来知道转了多少格。
它通常有5个引脚:VCC(电源)、GND(地)、SW(按键开关,按下时接通)、DT(B相)、CLK(A相)。我们做计数和方向判断,主要就用A(CLK)和B(DT)这两相。注意,有些编码器内部有上拉电阻,有些没有。为了确保信号稳定,我们一般在单片机内部将对应的GPIO口配置为上拉输入模式,这样引脚默认就是高电平,编码器转动时将其拉低,产生下降沿或低电平信号。
理解了这个,你就明白为什么我们的代码里需要在A相中断发生时,去读取B相的电平状态了。这是判断方向的核心逻辑。
3. 中断系统揭秘:STM32如何响应“紧急呼叫”
前面我们把中断比作闹钟,现在我们来拆解一下这个“闹钟系统”在STM32里是怎么工作的。STM32的中断系统涉及几个关键角色:GPIO、EXTI(外部中断/事件控制器)、NVIC(嵌套向量中断控制器)。它们的关系,有点像公司的前台(GPIO)、行政部(EXTI)和总经理办公室(NVIC)。
GPIO(通用输入输出口):这是第一道门,传感器信号就从这里进来。你需要把对应的引脚(比如PB14)配置为输入模式,并且使能它的时钟。
EXTI:你可以把它想象成一个非常专业的“信号路由和触发器”。STM32有好多GPIO口,但EXTI线只有有限的那么十几条(比如EXTI0, EXTI1...EXTI15)。这就需要“映射”:告诉EXTI,你现在要监听来自PB14引脚的事件。这就是代码里GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14)干的事。然后,你要在EXTI里设置:你想监听什么类型的事件?是上升沿(从低到高)、下降沿(从高到低),还是双边沿都监听?对于红外传感器计数,我们通常只关心遮挡发生的时刻,也就是输出从低变高的上升沿,所以配置为EXTI_Trigger_Rising。
NVIC:这是最终拍板的中断调度中心。即使EXTI收到了信号,要不要打断CPU,什么时候打断,谁先谁后,都得NVIC说了算。这里涉及两个重要概念:抢占优先级和子优先级。抢占优先级高的中断可以打断正在执行的抢占优先级低的中断,形成“嵌套”。子优先级则用于当两个相同抢占优先级的中断同时到来时,决定谁先执行。对于我们的传感器计数,实时性要求高,但一般不会和更紧急的中断(如电机急停)冲突,所以设置一个中等优先级即可。代码里NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)是选择了优先级分组方式2,然后给这个中断通道分配了具体的抢占和子优先级。
整个流程就是:传感器遮挡 → GPIO引脚电平跳变 → EXTI检测到预设的边沿事件 → EXTI向NVIC发出中断请求 → NVIC根据优先级安排 → CPU暂停当前工作,跳转到对应的中断服务函数(比如EXTI15_10_IRQHandler) → 我们在中断函数里完成“计数加一”和“清除中断标志”的动作 → CPU返回原来的任务继续执行。
这个过程是硬件自动完成的,速度极快(微秒级),保证了计数的准确性和实时性,同时CPU在大部分时间可以处理其他任务,效率极高。
4. 实战第一步:红外传感器中断计数代码逐行解析
理论说再多,不如一行代码。我们直接上手,把红外传感器的计数功能做出来。我建议你打开你的工程,跟着我一起写。
4.1 工程与文件准备
首先,确保你有一个基本的STM32工程模板,包含系统初始化、延时函数等。然后,我们在Hardware文件夹(或者你自己命名的外设驱动文件夹)里新建两个文件:CountSensor.c和CountSensor.h。把硬件相关的驱动代码集中管理是个好习惯。
在CountSensor.h里,我们进行简单的声明:
#ifndef __COUNT_SENSOR_H
#define __COUNT_SENSOR_H
#include "stm32f10x.h" // 根据你的芯片型号调整头文件
void CountSensor_Init(void);
uint16_t CountSensor_Get(void);
#endif
这个头文件告诉其他文件:“我这提供了传感器初始化和获取计数值的函数”。
4.2 初始化函数:搭建中断的舞台
真正的核心在CountSensor.c。我们先看初始化函数CountSensor_Init。我把它拆成几个逻辑块,方便你理解。
第一步:开时钟,好比给设备通电。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
任何外设要工作,必须先开启它的时钟。GPIOB的时钟好理解,AFIO(Alternate Function I/O,复用功能I/O)的时钟也必须开,因为我们要用到引脚复用功能里的“外部中断线映射”。
第二步:配置GPIO引脚为上拉输入。
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; // 假设传感器接在PB14
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 输入模式速度可随意,通常选最高
GPIO_Init(GPIOB, &GPIO_InitStructure);
设置为上拉输入(IPU),意味着引脚内部通过一个电阻连接到VCC。当传感器无输出(开路)时,引脚会被拉成高电平。我们的传感器在无遮挡时输出低电平(0V),它会把这个高电平“拉低”。这种配置可以增强抗干扰能力。
第三步:将PB14映射到EXTI14中断线。
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
这条命令很关键,它把具体的物理引脚PB14,和系统内部的EXTI14中断线连接起来。STM32规定,PAx, PBx, PCx... 同一个编号x的引脚(比如PA0, PB0, PC0)都共用EXTIx这条线。所以PB14用的是EXTI14线。
第四步:配置EXTI线的工作模式。
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line14; // 指定第14号线
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式(还有事件模式)
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; // 上升沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 使能该线
EXTI_Init(&EXTI_InitStructure);
这里我们选择了中断模式(触发后产生中断请求)和上升沿触发。为什么是上升沿?回忆一下传感器特性:遮挡时,DO输出从低电平跳变到高电平。这个跳变的瞬间(上升沿)就是我们想要计数的时刻。
第五步:配置NVIC,管理中断的“交通”。
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 选择优先级分组2
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; // 中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能该中断通道
NVIC_Init(&NVIC_InitStructure);
注意,EXTI10到EXTI15这6条线共享一个中断向量,名字叫EXTI15_10_IRQn。所以无论你用的是PB14还是PC12,只要中断线编号在10-15之间,都进同一个服务函数。抢占和子优先级根据你的系统整体中断规划来设,这里设为1只是示例。
4.3 中断服务函数与计数变量
初始化搭好了舞台,演员(中断服务函数)该上场了。我们在文件开头定义一个全局变量来存储计数值:
uint16_t CountSensor_Count = 0; // 计数值,初始为0
这个变量需要在中断函数里修改,在主循环里读取,所以定义为全局变量。
接下来是中断服务函数,它的名字是固定的,在启动文件里已经声明好了,我们必须按这个名字写:
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) == SET) // 判断是否是EXTI14产生的中断
{
CountSensor_Count++; // 计数值加1
EXTI_ClearITPendingBit(EXTI_Line14); // 清除中断标志位!这一步至关重要!
}
}
这个函数是中断发生后,CPU自动跳转过来执行的地方。第一件事就是检查中断标志,确认是不是我们关心的EXTI14线产生的中断(因为EXTI15_10共享一个入口)。第二件事,执行核心操作:计数加一。第三件事,也是新手最容易忘记导致程序卡死的一步:清除中断标志位。如果不清除,CPU会认为中断一直存在,就会不断地跳进这个函数,导致主程序无法继续执行。用EXTI_ClearITPendingBit函数清除对应的标志位。
最后,我们提供一个安全的函数让主程序获取计数值:
uint16_t CountSensor_Get(void)
{
return CountSensor_Count;
}
这里只是简单地返回。在一些更复杂的应用里,你可能会在这里关中断、读取、开中断,以防止在读取的瞬间被中断打断导致数据错乱。但对于一个16位的变量在STM32上,通常一次读操作是原子的(不会被打断),所以这样写问题不大。
4.4 主函数与效果验证
主函数就非常简洁了:
#include "stm32f10x.h"
#include "Delay.h"
#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行第7列开始,显示5位数字
Delay_ms(10); // 延时一小会儿,避免刷新太快
}
}
把代码编译下载到开发板,用杜邦线连接传感器模块(VCC-3.3V, GND-GND, DO-PB14)。上电后,屏幕显示“Count: 0”。每当你用一张纸片划过传感器的凹槽一次,计数值就应该稳稳地增加1。即使你快速连续划过,只要硬件反应得过来,计数应该都是准确的,而且屏幕刷新流畅,不会因为你的快速操作而卡顿。这就是中断的魅力——后台默默计数,前台从容显示。
5. 进阶挑战:旋转编码器双向计数与防抖处理
红外传感器是单方向的计数,旋转编码器则更有趣,我们需要分辨顺时针和逆时针。思路是:利用A、B两相的相位差。我们以检测A相(CLK)的上升沿(或下降沿)为例,在A相变化的那一刻,立刻去检查B相(DT)的电平。根据前面的相位关系图,如果此时B相是低电平,代表一个方向;如果是高电平,则代表另一个方向。
5.1 编码器初始化与双中断配置
编码器需要两个引脚,我们接PB0(A相)和PB1(B相)。初始化流程和红外传感器类似,但需要配置两个引脚和两条EXTI线。
void Encoder_Init(void)
{
// 1. 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 2. 配置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);
// 3. 将GPIO引脚映射到EXTI线
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;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; // 检测上升沿
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 5. 配置NVIC(需要为两个中断通道分别配置)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
// 配置EXTI0中断
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级可设为不同
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 配置EXTI1中断
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; // 给予不同的子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
注意,EXTI0和EXTI1有各自独立的中断向量EXTI0_IRQn和EXTI1_IRQn,所以需要分别配置NVIC。我给了它们相同的抢占优先级和不同的子优先级,这意味着它们不能互相打断,但当两者同时 pending 时,EXTI0会先被处理。
5.2 方向判断逻辑与中断服务函数
这里是核心逻辑。我们定义了一个有符号的全局变量EncoderCount,正转加,反转减。
int16_t EncoderCount = 0; // 有符号,正负代表方向
中断服务函数A(处理A相信号):
void EXTI0_IRQHandler(void) // A相(PB0)中断
{
if (EXTI_GetITStatus(EXTI_Line0) == SET)
{
// 在A相上升沿到来时,立即读取B相的电平
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) // 如果B相是低电平
{
EncoderCount--; // 根据相位图,此时为逆时针旋转
}
// 如果B相是高电平,则为顺时针旋转,我们不做操作(在B相中断里处理)
// 或者也可以在这里做顺时针的加操作,而在B相中断里做逆时针的减操作。两种逻辑等价,选一种即可。
EXTI_ClearITPendingBit(EXTI_Line0); // 清除标志
}
}
中断服务函数B(处理B相信号):
void EXTI1_IRQHandler(void) // B相(PB1)中断
{
if (EXTI_GetITStatus(EXTI_Line1) == SET)
{
// 在B相上升沿到来时,立即读取A相的电平
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0) // 如果A相是低电平
{
EncoderCount++; // 根据相位图,此时为顺时针旋转
}
// 如果A相是高电平,则为逆时针旋转,在A相中断里已处理
EXTI_ClearITPendingBit(EXTI_Line1);
}
}
这种实现方式被称为“四倍频”计数吗?不完全是。标准的四倍频是在A、B相的每个上升沿和下降沿都进行判断和计数,这样可以将编码器的物理分辨率提高4倍。我们这里只在上升沿判断,可以认为是“一倍频”或“单边沿”计数,但已经能准确判断方向和计数了。如果你想提高精度,可以将触发方式改为EXTI_Trigger_Rising_Falling(双边沿触发),然后在中断里根据当前A、B相的电平组合状态查表判断方向,逻辑会稍复杂一些。
5.3 机械抖动与软件防抖
当你实际转动编码器时,可能会发现偶尔会跳数、或者向一个方向转动却读了两个数。这很可能不是你的代码错了,而是机械抖动在作祟。编码器的机械触点闭合或断开时,在毫秒级的时间内会产生多次快速的通断,产生多个边沿信号。
解决这个问题,硬件上可以在A、B引脚对地加一个104(0.1uF)左右的电容来滤波。软件上,我们也有办法:
- 延时消抖:在中断发生后,延时几毫秒再读取引脚状态。但在中断服务函数里进行长时间延时是绝对禁止的,这会阻塞所有同级和低级中断,破坏系统的实时性。
- 定时器消抖:这是更优雅的方法。当进入中断后,不立即判断,而是启动一个定时器(比如SysTick或通用定时器),设置一个几毫秒的延时。定时器溢出中断到来时,再去读取引脚状态进行判断。这样中断服务函数执行时间极短。
- 状态机与时间戳:记录上次有效触发的时间,如果两次中断间隔太短(比如小于5ms),就认为是抖动,忽略这次触发。这需要在中断里读取一个精确的计时器值(如
SysTick->VAL),对代码能力要求稍高。
对于大多数应用,如果对精度要求不是极端高,使用优质编码器并加上合适的硬件电容,软件上采用简单的“只处理单边沿”方式,已经能得到很不错的效果了。我的经验是,EC11编码器在引脚加上0.1uF电容后,抖动现象基本消失。
5.4 主函数调用与演示
主函数和红外传感器的类似:
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "Encoder.h"
int main(void)
{
int16_t totalCount = 0; // 用于累积计数值
OLED_Init();
Encoder_Init(); // 初始化编码器中断
OLED_ShowString(1, 1, "Encoder:");
while (1)
{
// Encoder_Get() 函数会返回自上次读取以来的净变化量,并清零内部计数器
totalCount += Encoder_Get();
OLED_ShowSignedNum(1, 10, totalCount, 6); // 显示有符号数
Delay_ms(50); // 主循环延时可以稍长,显示变化更平滑
}
}
这里我展示了一个常用的技巧:在Encoder.c里提供一个Encoder_Get()函数,它返回当前的EncoderCount值,并同时将其清零。这样在主循环中,我们每次得到的是从上一次读取到这一次读取之间,编码器净转动的“格数”。这种方式在用于调节菜单、设置参数时非常方便,直接把这个变化量加到目标变量上就行。
6. 调试技巧与常见问题排坑指南
代码写完了,但一次成功是小概率事件。下面这些是我调试时经常遇到的问题和解决方法,希望能帮你快速定位。
问题1:下载程序后,没有任何反应,计数值不变。
- 检查供电:首先确认传感器/编码器模块的VCC和GND是否接对,模块上的电源指示灯是否亮起。
- 检查接线:再三确认数据线(DO, CLK, DT)是否接到了代码中指定的GPIO引脚上(比如PB14, PB0, PB1)。最好用万用表通断档测一下。
- 检查初始化顺序:确保在
main函数里调用了CountSensor_Init()或Encoder_Init()。 - 检查中断服务函数名:这是最容易出错的地方之一!中断服务函数的名字必须和启动文件里定义的向量表名字完全一致,大小写都不能错。
EXTI15_10_IRQHandler、EXTI0_IRQHandler,一个字母都不能差。写错了,中断发生了也找不到处理函数。 - 检查中断标志清除:在中断服务函数里,有没有调用
EXTI_ClearITPendingBit()?没清除的话,进一次中断后就卡死了。
问题2:计数不准确,偶尔漏数或多计数。
- 触发边沿选择:确认你设置的触发边沿和传感器实际输出是否匹配。用示波器或者逻辑分析仪看下波形是最直接的。红外传感器遮挡时是上升沿还是下降沿?编码器你希望检测哪个边沿?
- 消抖问题:对于编码器,多计数很可能是抖动。尝试在A、B引脚对GND焊接一个0.1uF的瓷片电容。
- 中断优先级冲突:如果你的系统里有其他更频繁或更高优先级的中断(比如定时器中断、串口中断),可能会偶尔打断你的传感器中断,导致丢失脉冲。可以适当提高传感器中断的抢占优先级。
- 变量类型与范围:计数值变量
CountSensor_Count或EncoderCount用的什么类型?uint16_t最大65535,如果计数超过这个值会溢出归零。根据应用场景选择合适的类型(uint32_t甚至uint64_t)。
问题3:编码器方向判断反了。
- 相位逻辑错误:这是最可能的原因。回顾一下,你的代码是在A相中断里读B相,还是在B相中断里读A相?判断条件是
==0还是==1?这取决于你的编码器相位定义和你的接线。最简单的方法:如果方向反了,就把两个中断函数里的++和--逻辑对调一下试试。
问题4:程序运行一段时间后死机。
- 中断服务函数执行时间过长:绝对不要在中断服务函数里调用
Delay_ms()这种阻塞延时!也不要在里面进行复杂的浮点运算或打印大量数据到串口。中断服务函数的黄金原则是:快进快出。只做最必要的操作(设置标志位、简单加减),把复杂的处理留给主循环。 - 栈溢出:如果中断嵌套层次太深,或者中断/函数调用里使用了很大的局部数组,可能导致栈空间不足。可以尝试在启动文件或链接脚本里增大栈大小。
调试时,善用OLED_ShowString函数在屏幕上打印关键变量的状态,或者用串口打印调试信息,比单纯看一个计数值要直观得多。例如,在编码器中断里,可以把当前A、B相的电平状态打印出来,这样你就能清楚地看到转动时的相位变化关系。
7. 举一反三:中断驱动计数的更多应用场景
掌握了这两个经典例子,你可以把中断计数的思路应用到很多地方,让你的项目变得更“聪明”。
1. 电机测速与闭环控制:在电机轴上安装一个光栅盘(就像码盘),配合对射式红外传感器,每转过一个缝隙就产生一个脉冲。用定时器固定时间(比如每秒)读取中断累计的脉冲数,就能算出转速(RPM)。结合PID算法,就能实现精确的电机速度闭环控制。这里,定时器中断用于定时采样,外部中断用于计数,分工合作。
2. 流量监测:很多水流传感器、霍尔流量计输出的也是脉冲信号,每个脉冲代表固定体积的流体通过。用同样的中断计数方法,累计脉冲数再乘以系数,就能得到累计流量。你可以定时将累计值保存到EEPROM或Flash,实现断电保存。
3. 位置检测与限位:在直线导轨、传送带的起点和终点安装红外或光电传感器,作为限位开关。当设备运行到限位处触发中断,在中断里立即停止电机或反转,可以实现精确的自动回零和行程保护,比轮询检测要可靠及时得多。
4. 人机交互增强:旋转编码器加中断,是实现参数调节的绝配。你可以用编码器的“旋转”调节数值,“按下”确认选择。结合状态机编程,可以做出非常流畅的菜单界面。我做过一个温控器,就是用编码器来设定目标温度,手感比按键好太多。
5. 高频信号计数:当脉冲频率非常高,快到主循环根本无法轮询时,中断(配合定时器)是唯一的解决方案。虽然STM32的外部中断响应频率也有上限(通常几MHz级别,具体看芯片手册),但对于大多数传感器信号(通常KHz级别)已经绰绰有余。
说到底,中断是一种“事件驱动”的编程思想。它让我们的程序从“不停地问”(轮询)变成了“有事叫你”(中断)。在资源有限的嵌入式世界里,这种思想能极大地提高CPU的利用效率,让系统响应更及时。从红外传感器和旋转编码器这个小起点出发,希望你能够把中断机制灵活运用到更多地方,做出反应更敏捷、功能更强大的嵌入式作品。
更多推荐


所有评论(0)