STM32F103C8T6+ITR9606传感器计次实战:5分钟搞定电机转速检测(附完整代码)
STM32F103C8T6+ITR9606传感器计次实战:5分钟搞定电机转速检测(附完整代码)
最近在调试一个小型直流电机项目时,遇到了一个很实际的需求:如何精准、实时地测量电机的转速?无论是做个小风扇、小车底盘,还是简单的自动化装置,转速反馈都是实现闭环控制的第一步。网上资料虽多,但要么过于理论化,要么代码臃肿,对新手不太友好。今天,我就结合手头最常用的“蓝核”STM32F103C8T6和成本仅几块钱的ITR9606槽型光电传感器,分享一套从硬件连接到软件防抖的完整测速方案。这套方案代码干净,思路清晰,你甚至可以直接复制到自己的项目里用。我们的目标很明确:用最少的代码,解决最常见的电机测速问题,并让结果稳定地显示在OLED屏幕上。
1. 硬件选型与连接:为什么是ITR9606和STM32F103C8T6?
在开始敲代码之前,理解硬件是基础。很多初学者卡在第一步,不是因为程序写不出来,而是硬件没接对或者选型不合适。
ITR9606槽型光电传感器,本质上是一个对射式光电开关。它有一个U型槽,一侧是红外发射管,另一侧是接收管。当槽内无遮挡时,接收管能收到红外光,输出低电平;当有物体(比如电机转盘上的挡光片)穿过槽时,光路被阻断,接收管截止,输出变为高电平。这个过程就产生了一个脉冲信号。
注意:市面上常见的槽型光电传感器输出逻辑可能不同,有的是有遮挡输出低电平。务必查阅你手中传感器的数据手册或通过万用表实测确认。ITR9606的典型逻辑是:无遮挡输出低电平(LED亮),有遮挡输出高电平(LED灭)。
选择它的理由很充分:
- 抗干扰能力强:相比反射式传感器,对射式受环境光、物体表面颜色影响极小。
- 响应速度快:适合高速计数,轻松应对几千RPM的电机。
- 信号干净:内置比较器,输出的是规整的数字信号(0或1),单片机可以直接读取。
- 安装方便:自带固定孔,可以稳稳地固定在电机支架上。
至于主控,STM32F103C8T6(俗称“蓝核板”或“最小系统板”)几乎是嵌入式入门的神器。它资源足够(72MHz主频,64KB Flash,20KB RAM),价格低廉,外设丰富。对于测速应用,我们需要用到它的GPIO外部中断功能,用来捕获传感器输出的脉冲边沿。
硬件连接示意图(以常见接法为例):
| 模块 | 引脚 | 连接至 STM32F103C8T6 | 说明 |
|---|---|---|---|
| ITR9606 | VCC | 3.3V 或 5V | 模块工作电压,接3.3V可直接与STM32电平匹配 |
| ITR9606 | GND | GND | 共地 |
| ITR9606 | OUT | GPIO Pin (如 PB14) | 信号输出端,接STM32的任意GPIO,用于中断输入 |
| OLED (I2C) | VCC | 3.3V | |
| OLED (I2C) | GND | GND | |
| OLED (I2C) | SCL | PB8 (或其他I2C1 SCL) | 时钟线 |
| OLED (I2C) | SDA | PB9 (或其他I2C1 SDA) | 数据线 |
这里我将传感器的OUT引脚连接到了PB14。你可以根据自己板子的布局选择其他引脚,只需在代码中相应修改即可。OLED屏用于实时显示转速,直观反馈调试结果。
2. 工程搭建与核心思路:从脉冲到转速
拿到一个开发板,第一步永远是建立工程。如果你用的是Keil MDK,那么创建一个基于STM32F10x标准外设库的工程是标准操作。这里我不再赘述创建过程,假设你已经有了一个可以编译的空工程。我们重点关注如何将物理脉冲转化为屏幕上跳动的转速数字。
核心思路可以分解为以下几步:
- 信号捕获:配置一个GPIO(如PB14)为外部中断模式,设置为下降沿触发(假设传感器是无遮挡低电平,挡片经过时变为高电平,形成上升沿;挡片离开时恢复低电平,形成我们需要的下降沿)。
- 计数:在中断服务函数里,每进入一次中断,就让一个计数器(比如
g_pulse_count)加1。这个值代表在特定时间内捕获到的脉冲数。 - 定时采样:开启一个定时器(如SysTick或通用定时器),每隔固定时间(例如1秒)中断一次。在这个定时器中断里,读取步骤2中的脉冲计数值。
- 计算转速:根据脉冲数和电机转盘上的挡片数量,计算转速。公式很简单:转速 (RPM) = (脉冲数 / 挡片数) * (60 / 采样时间秒)。例如,1秒内捕获到20个脉冲,转盘上只有1个挡片,那么转速就是
20 * 60 = 1200 RPM。 - 显示与重置:将计算出的转速发送到OLED显示,同时将脉冲计数器清零,为下一个采样周期做准备。
这个流程形成了一个稳定的测量循环。关键在于两个中断的协作:外部中断负责“数数”,定时器中断负责“定时读数并计算”。
// 全局变量声明(通常在.c文件开头)
volatile uint32_t g_pulse_count = 0; // 脉冲计数器,用于外部中断累加
volatile uint32_t g_rpm_value = 0; // 计算出的转速值
uint8_t g_obstacle_num = 1; // 转盘上的挡片数量,默认为1
3. 代码精讲:防抖、中断与OLED显示
直接上代码,我们分模块拆解。为了保持清晰,我建议将代码分为几个文件:main.c, sensor.c/h(传感器驱动), timer.c/h(定时器驱动), oled.c/h(显示驱动)。
3.1 传感器驱动与硬件防抖 (sensor.c)
这是项目的核心之一。配置外部中断不难,但信号防抖是决定测量是否可用的关键。机械触点或光电传感器在电平跳变时可能会产生细微的抖动,导致多次误触发。
// sensor.c
#include "stm32f10x.h"
#include "sensor.h"
volatile uint32_t g_pulse_count = 0; // 在中断中修改,需加volatile
void Sensor_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
EXTI_InitTypeDef EXTI_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
// 1. 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
// 2. 配置GPIO为上拉输入(根据传感器默认输出电平决定)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入,默认高电平
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
// 3. 将PB14映射到EXTI14
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
// 4. 配置EXTI线
EXTI_InitStruct.EXTI_Line = EXTI_Line14;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
// 5. 配置NVIC(嵌套向量中断控制器)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 整个工程设置一次即可
NVIC_InitStruct.NVIC_IRQChannel = EXTI15_10_IRQn; // EXTI10-15共享中断
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
}
// 外部中断服务函数
void EXTI15_10_IRQHandler(void) {
if (EXTI_GetITStatus(EXTI_Line14) != RESET) {
// 硬件防抖关键步骤:再次读取引脚电平确认
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0) {
g_pulse_count++; // 确认是低电平,才计数
}
EXTI_ClearITPendingBit(EXTI_Line14); // 必须清除中断标志!
}
}
uint32_t Sensor_GetCount(void) {
return g_pulse_count;
}
void Sensor_ClearCount(void) {
g_pulse_count = 0;
}
提示:
EXTI15_10_IRQHandler是STM32标准库中预定义的中断函数名。当中断线10到15中的任意一个触发时,都会进入这个函数。所以我们在函数内部首先要EXTI_GetITStatus来判断是不是我们关心的EXTI_Line14触发的。
防抖逻辑解读:在中断发生后,我们并没有立即认为一次有效的挡片通过,而是再次读取GPIO引脚的实际电平。如果此时读到的仍然是低电平(假设下降沿有效),才认为这是一次有效的触发。这个简单的操作可以滤除大部分由于接触抖动产生的毛刺脉冲。
3.2 定时器驱动与转速计算 (timer.c)
我们使用SysTick定时器,因为它最简单,无需额外配置,通常用于产生精确的延时。这里我们用它产生1秒的定时中断。
// timer.c
#include "stm32f10x.h"
#include "timer.h"
#include "sensor.h"
#include "oled.h"
extern uint8_t g_obstacle_num; // 声明外部变量
volatile uint32_t g_rpm_value = 0;
volatile uint8_t g_timer_flag = 0; // 定时标志位,主循环查询用
// SysTick初始化,配置为1秒中断一次
void Timer_SysTick_Init(void) {
// SystemCoreClock 是系统时钟频率,对于STM32F103,通常为72MHz
// SysTick_Config的参数是重装载值,每计数这么多下产生一次中断
// 72000000 / 1000 = 72000,即1ms中断一次。这里我们想要1秒,所以用循环。
// 更优做法是配置为1ms中断,然后在中断里累加计数到1000。
if (SysTick_Config(SystemCoreClock / 1000)) { // 配置为1ms中断
while (1); // 初始化失败,死循环
}
}
static uint32_t s_time_ms = 0; // 毫秒计数器
// SysTick中断服务函数(在 core_cm3.h 中已定义弱类型,需重写)
void SysTick_Handler(void) {
s_time_ms++;
if (s_time_ms >= 1000) { // 每1000ms = 1秒
s_time_ms = 0;
g_timer_flag = 1; // 置位标志,通知主循环
}
}
// 在定时标志置位后,由主循环调用的转速计算函数
void Timer_CalculateRPM(void) {
uint32_t pulse_count = Sensor_GetCount(); // 获取过去1秒的脉冲数
// 转速计算:RPM = (脉冲数 / 挡片数) * (60秒 / 采样时间1秒)
g_rpm_value = (pulse_count * 60) / g_obstacle_num;
Sensor_ClearCount(); // 清空计数器,开始下一个周期
g_timer_flag = 0; // 清除标志
}
这里采用了一种主循环查询标志位的模式,而不是在SysTick中断里直接进行复杂的计算和显示操作。这样做是为了保持中断服务函数的简短,避免因执行时间过长影响其他中断的响应。g_timer_flag是一个桥梁。
3.3 主程序逻辑整合 (main.c)
主程序的任务是初始化所有模块,然后在一个无限循环中等待定时标志,一旦标志置位,就计算转速并更新显示。
// main.c
#include "stm32f10x.h"
#include "sensor.h"
#include "timer.h"
#include "oled.h"
#include "delay.h" // 如果需要微秒级延时,可以包含
uint8_t g_obstacle_num = 1; // 全局定义挡片数
int main(void) {
// 初始化各模块
OLED_Init(); // 初始化OLED显示屏
Sensor_Init(); // 初始化传感器和外部中断
Timer_SysTick_Init(); // 初始化SysTick定时器
// 显示静态标题
OLED_ShowString(1, 1, "Speed:");
OLED_ShowString(2, 1, "RPM:");
while (1) {
// 查询定时标志
if (g_timer_flag) {
Timer_CalculateRPM(); // 计算转速
// 在OLED上显示转速值,假设显示在第二行第6列,最多5位数
OLED_ShowNum(2, 6, g_rpm_value, 5);
}
// 这里可以添加其他非实时任务,如按键扫描修改挡片数g_obstacle_num
// Delay_ms(10); // 如果使用延时函数,注意不要阻塞太久
}
}
整个程序的脉络非常清晰:硬件中断负责采集,定时器负责节拍,主循环负责调度和显示。这是一种在单片机编程中非常经典的结构。
4. 进阶优化与常见问题排查
项目能跑起来只是第一步,让它跑得稳、测得准才是工程师价值的体现。下面分享几个我踩过坑后总结的优化点和排查技巧。
4.1 软件防抖进阶:去抖算法
前面的硬件二次电平确认是基础防抖。对于要求更高的场景,或者传感器本身抖动比较严重,可以引入软件去抖算法。一个简单有效的方法是延时再判断。
我们可以修改外部中断服务函数,不立即计数,而是设置一个“待确认”标志,并启动一个短定时器(比如5ms)。在短定时器中断里,再次读取引脚电平,如果仍然是稳定的有效电平,才执行计数。这能更彻底地滤除抖动。不过这会增加代码复杂度,并且需要另一个定时器资源。对于大多数电机测速应用(转速不是极高),前面的简单防抖已经足够。
4.2 应对高速测量:输入捕获模式
当电机转速非常高,或者转盘上挡片很多时,脉冲频率可能很高。此时,外部中断的响应和处理时间可能成为瓶颈,导致漏计数。STM32的定时器有一个强大的功能叫输入捕获,它可以自动记录脉冲边沿发生的精确时刻,几乎不占用CPU资源。
具体做法是:将传感器输出信号连接到定时器的输入捕获通道(如TIM2_CH1)。配置定时器在内部以很高的频率计数(比如1MHz),当捕获到边沿时,硬件会自动将当前计数值锁存到寄存器,并产生中断。我们只需要在中断里读取两个边沿之间的计数值差,就能算出脉冲周期,进而得到频率和转速。这种方法精度高,适合高速测量,但配置相对复杂。
4.3 常见问题与调试技巧
-
问题:转速显示为0或不变。
- 检查硬件连接:用万用表测量传感器VCC和GND电压是否正常。用示波器或逻辑分析仪查看OUT引脚在挡片经过时是否有电平跳变。没有示波器?可以接一个LED到OUT引脚(串联电阻),观察LED是否明暗变化。
- 检查中断配置:确认GPIO模式是否正确(应为浮空输入/上拉输入/下拉输入),确认EXTI触发边沿设置是否与传感器实际输出逻辑匹配。
- 检查中断服务函数:是否清除了中断挂起位(
EXTI_ClearITPendingBit)?函数名是否写错?(必须与启动文件中的向量表名称完全一致)。
-
问题:转速数值乱跳,明显不准。
- 首要怀疑对象是抖动:加强防抖处理,如前面所述的延时再判断法。
- 检查供电稳定性:电机启停时会产生较大的电流波动,可能影响传感器和单片机的电源。尝试在传感器VCC和GND之间并联一个10uF电解电容和一个0.1uF瓷片电容,进行电源滤波。
- 检查机械安装:挡片是否在槽内居中?是否会有其他部分误遮挡传感器?确保每次旋转只有挡片能触发一次信号。
-
问题:转速显示偶尔跳变很大。
- 可能是计算溢出或数据类型问题:确保
g_pulse_count和g_rpm_value的数据类型(uint32_t)足够大,不会在1秒内溢出。例如,32位无符号整数最大为4294967295,远超出实际脉冲数。 - 检查全局变量访问:
g_pulse_count在中断中被修改,在主循环或定时器中断中被读取。虽然这里没有进行复杂的运算,但严格来说,对于32位变量在8位或16位架构上的非原子访问,可能需要临时关中断进行保护。不过在Cortex-M3的STM32上,对32位对齐变量的读写是原子的,通常问题不大。如果担心,可以在读取时临时关中断:__disable_irq(); count = g_pulse_count; __enable_irq();。
- 可能是计算溢出或数据类型问题:确保
调试时,善用OLED显示中间变量。比如,你可以直接把g_pulse_count实时显示出来,看看脉冲数是否在稳定增加,这能帮你快速定位问题是出在计数环节还是计算环节。
最后,把完整的项目文件整理好,main.c, sensor.c/h, timer.c/h, oled.c/h,以及对应的stm32f10x_标准库文件。在Keil中设置好包含路径,编译下载到你的蓝核板。接上电机和传感器,上电,你应该能看到OLED上稳定显示着电机的实时转速。这个过程从硬件连接到软件调试,自己走一遍,远比只看文档收获大得多。遇到问题别怕,那正是理解底层原理的最好时机。
更多推荐


所有评论(0)