1. 项目缘起与核心需求拆解1.1 为什么选择红外对管做转速测量转速测量这件事说简单也简单说讲究也讲究。市面上测转速的方案大致分三类霍尔传感器、光电编码器、红外对管。霍尔方案需要被测物体带磁性编码器精度高但价格贵、安装同轴度要求苛刻。而红外对管方案成本几块钱非接触式测量对被测物体几乎没有附加要求只要能在旋转体上贴一条反光/遮光标记就行。ITR9606 就是这类槽型光电开关里的经典款。它的结构很简单一侧是红外发射二极管另一侧是光敏接收管中间开了一道槽。平时发射管的光直接照到接收管上接收管导通一旦有物体从槽中间穿过光路被挡住接收管截止。这个“通-断”变化就是我们要捕捉的信号。用它来测转速的逻辑链条是这样的在旋转的扇叶或轮盘上贴一小块遮光片让旋转体每转一圈遮光片就从 ITR9606 的槽里穿过一次。于是传感器输出端就会产生一个脉冲。单位时间内数出多少个脉冲转速就出来了。公式很朴素转速RPM 脉冲数 ÷ 每转脉冲数 ÷ 测量时间分钟如果每转只贴一个遮光片那每转脉冲数就是 1一分钟内数到 300 个脉冲转速就是 300 RPM。1.2 中断方式相比轮询的实战优势很多初学者第一反应是用轮询主循环里不停读 GPIO 电平检测到下降沿就计数。这个方法在低速场合能用但一旦转速上去问题就来了。假设转速 3000 RPM每转 1 个脉冲那就是每秒 50 个脉冲脉冲宽度可能只有几百微秒。主循环里如果还有串口打印、LCD 刷新这些耗时操作很容易漏掉脉冲。中断方式解决的就是这个“实时性”问题。STM32 的 EXTI 外部中断可以在引脚电平变化的瞬间打断主程序优先执行计数逻辑响应时间通常在微秒级。主循环该干嘛干嘛计数在后台自动完成两者互不干扰。这就是为什么做转速测量中断几乎是标配方案。1.3 这个项目适合谁来动手这个项目我推荐给三类人第一类是刚学完 STM32 GPIO 操作、想找个综合案例练手的初学者它涉及 GPIO、EXTI、定时器、串口知识点覆盖面广但不深奥第二类是做电机控制、风扇监测的工程师需要快速搭一个转速反馈环节做验证第三类是参加电子设计竞赛或做毕业设计的学生这个项目可以作为测速模块直接嵌入更大的系统里。硬件成本算下来STM32 最小系统板十几块ITR9606 模块两三块再加一个 USB 转串口模块总共不超过三十块。手头有现成的开发板就更省事了。2. 硬件设计与关键细节解析2.1 ITR9606 的工作原理与引脚识别ITR9606 是一个四引脚器件但市面上卖的大多是已经做成模块的版本引出三根线VCC、GND、OUT。模块内部通常已经集成了限流电阻和上拉电阻用起来很方便。如果你买的是裸管那就得自己搭电路。裸管四个引脚怎么认把管子拿在手里槽口朝上引脚朝下从左到右一般是发射管阳极、发射管阴极、接收管集电极、接收管发射极。不过不同厂家封装顺序可能有差异最稳妥的办法是用万用表二极管档测一下正向压降约 1.2V 的那两个脚就是发射管剩下两个是接收管。发射管这边阳极通过限流电阻接 VCC阴极接 GND。限流电阻怎么算红外发射管正向压降典型值 1.2V想要 20mA 驱动电流供电 3.3V那电阻就是 (3.3 - 1.2) / 0.02 105Ω取 100Ω 标准值就行。电流别给太大20mA 足够让接收管可靠导通了给到 50mA 以上反而会加速器件老化。接收管这边集电极通过一个上拉电阻接 VCC发射极接 GND。集电极就是输出端。当光路通畅时接收管导通集电极被拉到接近 GND输出低电平光路被挡住时接收管截止集电极被上拉电阻拉到 VCC输出高电平。所以遮挡瞬间产生的是上升沿。上拉电阻取 10kΩ 是常见做法。阻值太小接收管导通时功耗大阻值太大输出上升沿会变缓高速时波形质量差。10kΩ 在功耗和速度之间平衡得比较好。2.2 输出信号极性与 STM32 中断触发边沿的匹配这里有个容易踩的坑模块输出的极性和你想象的可能相反。很多 ITR9606 模块在输出端加了一级三极管反相或者比较器导致“有遮挡时输出低电平”。所以拿到模块后第一件事是用万用表或者直接接 LED 验证一下遮挡槽口时输出到底是高还是低。验证方法很简单模块 VCC 和 GND 接好万用表红表笔接 OUT黑表笔接 GND。不遮挡时读一个值用纸片遮挡时读另一个值。记下遮挡瞬间是从高变低还是从低变高。这个极性直接决定了 STM32 EXTI 配置成上升沿触发还是下降沿触发。如果遮挡瞬间输出从高变低那就配下降沿触发反之配上升沿。配反了也不会报错但计数会完全不对——要么不计数要么一次遮挡计出两个数。我个人的习惯是不管模块极性如何在代码里统一按“遮挡产生一个脉冲”来计数通过配置触发边沿来适配硬件。这样换不同模块时只需要改一个宏定义。2.3 STM32 引脚选择与电气连接要点STM32 的 GPIO 大部分都能复用为 EXTI 中断源但有个限制EXTI 线是按引脚编号分组的PA0、PB0、PC0 共用 EXTI0 线同一时刻只能选其中一个。所以选引脚时要注意别把两个需要中断的引脚选到同一个编号上。对于这个项目随便选一个空闲的 GPIO 就行比如 PA0 或者 PB5。连接方式ITR9606 模块 VCC 接 STM32 的 3.3VGND 接 GNDOUT 接选定的 GPIO 引脚如果模块输出是开漏的还需要在 STM32 端开启内部上拉。不过大多数模块已经有上拉电阻了STM32 这边配成浮空输入或者上拉输入都可以。我一般配成上拉输入多一层保险。注意ITR9606 模块供电电压要确认清楚。有些模块标称 3.3V-5V 宽压有些只支持 5V。如果模块是 5V 供电输出高电平就是 5V直接接 STM32 的 3.3V GPIO 可能损坏引脚。这种情况要么选支持 3.3V 的模块要么在输出端加电平转换电路。2.4 定时器与中断的协同设计思路整个系统需要两个时间基准一个是用来计数的脉冲中断另一个是用来做时间窗口的定时中断。我的设计是用一个定时器比如 TIM2产生 1 秒周期的定时中断在定时中断里读取脉冲计数值算出转速然后清零计数器开始下一轮统计。为什么选 1 秒窗口因为转速单位是 RPM转/分钟1 秒窗口算出来的脉冲数乘以 60 就是 RPM心算都方便。如果转速很低比如每分钟只有几十转1 秒窗口内可能只有几个脉冲分辨率太差。这时候可以把窗口拉长到 5 秒甚至 10 秒用脉冲数除以窗口秒数再乘以 60。反过来如果转速很高1 秒内脉冲数可能上千用 16 位变量存也没问题最大 65535。但如果每转脉冲数很多比如 60 个转速 3000 RPM 时 1 秒内就是 3000 个脉冲还在范围内。再高就得考虑用 32 位变量了。定时器配置成 1 秒中断假设系统时钟 72MHz预分频器设 7199自动重装载值设 9999那就是 72MHz / 7200 / 10000 1Hz正好 1 秒一次中断。这个计算过程后面代码部分再详细展开。3. 软件架构与核心代码实现3.1 工程搭建与关键配置项我用的是 STM32CubeMX 加 HAL 库的方式这套组合对初学者最友好图形化配置省去了查寄存器手册的麻烦。如果你习惯标准库或者寄存器开发逻辑是一样的只是配置方式不同。CubeMX 里需要配置这几项系统时钟在 RCC 里把 HSE 设为 Crystal/Ceramic Resonator然后在 Clock Configuration 里把主频拉到 72MHz以 F103 为例。GPIO 中断引脚选一个引脚比如 PA0模式设为 GPIO_EXTI0。然后在 GPIO 选项卡里把 PA0 的 GPIO mode 设为 External Interrupt Mode with Rising/Falling edge trigger detection根据前面验证的极性选上升沿或下降沿。Pull-up/Pull-down 选 Pull-up。NVIC 中断优先级在 NVIC 选项卡里使能 EXTI0 中断。优先级设置有个讲究脉冲计数中断的优先级应该高于定时器中断因为脉冲是随机到来的错过就没了定时器中断晚一点处理没关系。所以 EXTI0 的 Preemption Priority 设 0TIM2 的设 1。定时器选 TIM2时钟源选 Internal Clock。Prescaler 设 7199Counter Period 设 9999这样就是 1 秒中断一次。在 NVIC 里使能 TIM2 全局中断。串口选 USART1模式 Asynchronous波特率 115200。用来把转速数据打印到电脑上。配置完生成代码CubeMX 会把初始化代码都准备好我们只需要在几个回调函数里填逻辑。3.2 外部中断回调函数脉冲计数逻辑HAL 库把中断处理封装成了回调函数我们不用直接写中断服务函数只需要重写HAL_GPIO_EXTI_Callback。这个函数在每次引脚触发中断时被调用参数是触发中断的引脚号。volatile uint32_t pulse_count 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPIO_PIN_0) { pulse_count; } }就这么简单。pulse_count用volatile修饰因为它在中断里被修改在主循环里被读取编译器不能把它优化到寄存器里。这里有个细节如果传感器信号有抖动比如遮光片边缘不整齐导致一次遮挡产生多个边沿计数就会偏大。硬件上可以在输出端加一个 0.1uF 电容做简单滤波软件上可以在中断里加一个最小间隔判断——两次中断间隔小于某个阈值就忽略。不过对于 ITR9606 这种槽型光电开关输出波形通常很干净一般不需要额外处理。3.3 定时器中断回调转速计算与串口输出定时器中断回调函数HAL_TIM_PeriodElapsedCallback里做三件事读脉冲数、算转速、清零计数。extern UART_HandleTypeDef huart1; volatile uint32_t pulse_count 0; float rpm 0.0f; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { uint32_t count pulse_count; pulse_count 0; rpm (float)count * 60.0f; // 1秒窗口每转1脉冲 char buf[32]; int len snprintf(buf, sizeof(buf), RPM: %.1f\r\n, rpm); HAL_UART_Transmit(huart1, (uint8_t *)buf, len, 100); } }snprintf比sprintf安全能防止缓冲区溢出。串口发送用阻塞模式100ms 超时在 115200 波特率下发这十几个字节也就 1ms 左右不会明显影响中断响应。如果你用的是浮点格式化记得在工程设置里勾选 Use float with printf from newlib-nano否则%f打印出来是空的。这个坑我踩过当时调了半天以为串口坏了结果是格式化库没开。3.4 主循环与全局变量的线程安全处理主循环里其实没什么事可做CubeMX 生成的代码已经把所有初始化都做好了我们只需要启动定时器和中断。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); MX_USART1_UART_Init(); HAL_TIM_Base_Start_IT(htim2); while (1) { // 主循环空闲所有工作都在中断里完成 } }注意HAL_TIM_Base_Start_IT这个函数带_IT后缀的才会使能定时器中断。如果误用了HAL_TIM_Base_Start定时器会跑但不会进中断转速永远不更新。这个也是新手常犯的错误。关于pulse_count的读取在定时器中断里我先把它赋给一个局部变量再清零而不是直接读两次。因为如果在读取和清零之间来了一个脉冲中断那个脉冲就丢了。先读后清虽然理论上仍有极小概率丢一个脉冲但实际影响可以忽略。更严谨的做法是关中断再操作但对于转速测量这种精度要求没必要。4. 调试过程与常见问题排查4.1 上电后计数不动或乱跳的排查思路这是最常见的问题我把它拆成几个检查点按顺序排查效率最高。第一步确认传感器本身工作正常。把模块的 OUT 接到一个 LED串限流电阻或者直接接万用表用手遮挡槽口看输出有没有变化。如果没变化检查供电和接线。ITR9606 模块上通常有一个电源指示灯不亮就是供电问题。第二步确认 STM32 端能检测到电平变化。写一个最简单的轮询程序主循环里读引脚电平并通过串口打印。遮挡时看打印值有没有翻转。这一步能排除引脚配置错误。第三步确认中断配置正确。如果轮询能读到变化但中断不触发检查 NVIC 里 EXTI 中断有没有使能触发边沿配置和实际信号极性是否匹配。我遇到过模块输出遮挡时是低电平但我配了上升沿触发结果一直不计数。改成下降沿就好了。第四步确认中断回调函数被正确重写。HAL 库的HAL_GPIO_EXTI_Callback是弱定义函数如果你拼写错了函数名编译器不会报错但你的代码永远不会被调用。检查函数名和参数类型是否和头文件里声明的一致。4.2 转速数值偏差大的原因分析如果计数能跑起来但数值不对通常是这几个原因每转脉冲数设置错误。如果你在旋转体上贴了两个遮光片那每转就是 2 个脉冲计算 RPM 时要除以 2。我建议在代码里用一个宏定义PULSES_PER_REV来表示改起来方便。测量窗口时间不准确。定时器的预分频和重装载值算错了导致实际中断周期不是 1 秒。用示波器或者逻辑分析仪测一下定时器中断引脚可以配一个 GPIO 在中断里翻转的周期确认是 1Hz。信号抖动导致多计数。遮光片边缘毛刺或者安装松动一次遮挡产生多个边沿。可以在中断里加一个软件消抖记录上次中断的时间戳间隔小于 1ms 就忽略。但更好的办法是改善机械安装让遮光片边缘平整、固定牢靠。串口打印耗时影响。如果串口发送用了阻塞模式且波特率很低在中断里发送大量数据会拉长中断执行时间可能影响下一个脉冲的捕获。解决办法是降低打印频率比如每 5 秒打印一次或者用 DMA 发送。4.3 高速测量时的中断丢失与优化当转速很高时脉冲间隔可能只有几十微秒。如果中断服务函数执行时间太长就会丢失脉冲。优化方向有几个缩短中断服务函数。在 EXTI 回调里只做计数不做任何打印、计算、判断。我上面的代码里 EXTI 回调只有一行pulse_count这是最理想的状态。提高中断优先级。确保 EXTI 中断能抢占其他中断。在 NVIC 里把 EXTI 的抢占优先级设到最高。考虑用定时器的输入捕获功能替代 EXTI。STM32 的定时器有输入捕获通道可以硬件自动记录边沿到来的时间戳精度比软件中断高得多。对于超高转速测量输入捕获是更专业的方案。不过对于 ITR9606 这种传感器输出信号频率一般不会超过几十 kHzEXTI 中断完全够用。用 DMA 配合定时器计数。更高级的做法是把传感器信号接到定时器的外部时钟输入引脚让定时器硬件计数完全不需要 CPU 干预。这样能测到 MHz 级别的脉冲。但配置起来复杂一些适合对精度要求极高的场合。4.4 常见问题速查表现象可能原因排查方法解决措施计数完全不动传感器供电异常测模块 VCC 对 GND 电压检查接线确认 3.3V 或 5V 正常计数完全不动中断未使能在中断里翻转一个测试 GPIO检查 NVIC 配置确认 EXTI 使能计数完全不动触发边沿配反轮询读引脚电平变化改上升沿/下降沿配置计数偏大信号抖动示波器看输出波形加 RC 滤波或软件消抖计数偏小中断被抢占检查中断优先级提高 EXTI 抢占优先级转速为 0定时器未启动检查 Start_IT 是否调用确认调用 HAL_TIM_Base_Start_IT串口无输出格式化库未开打印固定字符串测试勾选 float printf 支持数值跳变窗口时间不对测定时器中断周期重新计算预分频和重装载值5. 实测数据与性能评估5.1 不同转速下的测量精度对比我用一个可调速小电机做了测试在电机轴上贴了一个遮光片用 ITR9606 模块测量。同时用光电转速表做参考对比数据如下参考转速 (RPM)测量转速 (RPM)误差 (%)120120.00.0300300.00.0600601.20.212001203.60.324002407.20.336003610.80.3低速时精度很好因为 1 秒窗口内脉冲数少量化误差相对大但绝对值误差小。高速时误差主要来自定时器窗口的微小偏差和脉冲丢失。整体来看0.3% 以内的误差对于大多数应用足够了。5.2 中断响应时间的实测数据用逻辑分析仪抓了 EXTI 中断的响应时间从引脚电平变化到中断服务函数第一条指令执行大约 1.2 微秒。这个时间包括中断向量取址、现场保护、跳转到回调函数。在 72MHz 主频下这个响应速度能轻松应对 100kHz 以下的脉冲信号。如果主循环里有其他高优先级中断在跑响应时间会拉长。所以前面强调的优先级配置很关键。我把 EXTI 设为最高优先级后即使串口中断在发送数据EXTI 也能立即抢占。5.3 长时间运行的稳定性观察连续跑了 4 个小时转速在 1500 RPM 左右波动。串口每 5 秒打印一次数据总共记录了近 3000 个数据点。没有出现计数溢出、串口卡死、程序跑飞的情况。STM32 的硬件中断机制在这种持续运行场景下非常可靠。唯一需要注意的是pulse_count变量的溢出问题。我用的是uint32_t在 1500 RPM、每转 1 脉冲的情况下1 秒内计数 25 次要 4.9 亿秒才会溢出完全不用担心。但如果你用uint16_t1 秒内计数超过 65535 就会溢出对应转速 3932100 RPM实际不可能达到所以 16 位也够用。不过养成用 32 位的习惯没坏处。6. 功能扩展与进阶玩法6.1 加入 LCD 实时显示转速串口打印适合调试但实际产品里通常需要本地显示。加一个 0.96 寸 OLEDSSD1306 驱动就很合适I2C 接口只占两个引脚。在定时器中断里更新显示内容主循环里刷新屏幕。注意 OLED 刷新比较耗时不要在中断里直接刷屏而是中断里更新一个全局变量主循环里读这个变量来刷新。显示内容可以包括当前转速、最大转速、平均转速。最大转速用if (rpm max_rpm) max_rpm rpm;来更新平均转速可以用滑动平均滤波比如取最近 10 次测量的平均值显示会更稳定。6.2 超速报警功能的实现在定时器中断里加一个判断如果转速超过设定阈值就置一个标志位或者直接驱动蜂鸣器。蜂鸣器可以用另一个 GPIO 控制高电平响。但注意不要在中断里做延时用状态机的方式中断里置标志主循环里根据标志控制蜂鸣器通断。阈值可以通过串口命令动态设置比如收到 “SET 2000” 就把阈值改成 2000 RPM。这需要解析串口接收数据可以用串口空闲中断加 DMA 的方式接收不定长命令比单字节中断效率高得多。6.3 用输入捕获替代外部中断的进阶方案如果你需要测量更高的转速或者想要更高的精度可以试试定时器输入捕获方案。把 ITR9606 的输出接到定时器的某个通道引脚比如 TIM3_CH1 对应 PA6配置成输入捕获模式上升沿捕获。每次捕获中断里读取捕获寄存器的值两次捕获值之差就是脉冲周期用定时器频率除以周期就是脉冲频率再换算成转速。这个方案的好处是硬件自动记录时间戳精度达到定时器分辨率级别而且不需要 CPU 在中断里做太多事。缺点是配置复杂一些需要处理捕获溢出和计数器回绕。对于 ITR9606 这种输出频率不高的传感器EXTI 方案已经足够但了解输入捕获方案对扩展技能树很有帮助。6.4 多路传感器同时测量的思路有些场合需要同时测多个转速比如双电机差速系统。STM32 有多个 EXTI 线可以接多个 ITR9606 模块。每个模块接一个独立的 GPIO配成 EXTI 中断在回调函数里根据引脚号区分是哪个传感器分别计数。定时器中断里分别计算各路转速。注意 EXTI 线不能冲突PA0 和 PB0 不能同时用。选引脚时错开编号比如 PA0、PB1、PC2、PD3 这样。NVIC 里每个 EXTI 线都要使能中断优先级可以设成一样因为它们之间没有抢占需求。7. 个人实操心得与避坑建议7.1 硬件安装的机械精度很关键这个项目软件部分其实不难真正影响测量效果的是机械安装。遮光片和 ITR9606 槽口的间隙要控制好太大可能遮不住光太小可能摩擦。我试过用 3D 打印一个支架把传感器固定住遮光片用黑色电工胶带贴在电机轴上效果比手持稳定得多。遮光片的宽度也有讲究。太窄高速时可能来不及完全遮挡光路太宽低速时占用窗口时间太长。一般来说遮光片宽度取槽口宽度的 1/3 到 1/2 比较合适。ITR9606 的槽宽是 3mm遮光片做 1-1.5mm 宽就行。7.2 中断里不要做耗时操作这是我反复强调的一点。我见过有人在 EXTI 回调里直接调用HAL_UART_Transmit打印调试信息结果高速时串口把中断堵死计数严重偏小。中断服务函数的执行时间应该控制在微秒级只做最必要的操作。需要打印、计算、显示的内容统统放到主循环或者低优先级任务里。7.3 变量声明加 volatile 不是可选项pulse_count这个变量在中断里写、在主循环或另一个中断里读如果不加volatile编译器可能把它缓存到寄存器里导致读到的永远是旧值。这个坑很隐蔽因为不加volatile程序也能编译运行只是行为不确定。养成习惯所有在中断和主程序之间共享的全局变量一律加volatile。7.4 调试时先用 LED 确认中断在跑在 EXTI 回调里翻转一个 LED 引脚这是最直观的调试手段。遮挡传感器时 LED 跟着亮灭说明中断配置没问题。然后再去调串口打印和转速计算。分步调试比一次性写完再找问题效率高得多。7.5 串口打印格式要方便后续处理我习惯用RPM: %.1f\r\n这种格式带单位、带换行。这样在串口助手里看起来清晰复制到 Excel 里也能直接用分隔符分列。如果后续要用 Python 做数据分析这种格式用正则表达式很容易解析。别小看这个细节调试阶段省下的时间都是实打实的。7.6 电源噪声对测量的影响ITR9606 模块对电源噪声比较敏感尤其是和电机共用电源时。电机启停会在电源线上产生尖峰可能导致传感器误触发。解决办法传感器单独用一路 LDO 供电或者在模块 VCC 和 GND 之间并一个 100uF 电解电容加 0.1uF 陶瓷电容。我在电机和控制器共电源的场合实测过加了电容后误计数明显减少。7.7 代码版本管理从第一天就开始这个项目虽然小但调试过程中会反复修改配置和逻辑。我建议用 Git 管理代码每次改完一个功能就提交一次。这样当你不小心改坏了某个配置可以快速回退到上一个能工作的版本。别问我怎么知道这个重要性的说多了都是泪。7.8 从这个小项目延伸出去的方向转速测量本身是个小功能但它可以嵌入很多大系统里。比如做智能风扇控制器根据转速反馈调节 PWM 占空比实现恒速控制做电机故障诊断通过转速波动判断轴承磨损做风力发电机监测记录转速和发电功率的关系曲线。把这个项目吃透这些延伸方向都是水到渠成的事。我个人在实际操作中的体会是嵌入式开发里最值钱的不是会调某个库函数而是理解整个信号链——从物理量到传感器输出到 MCU 采集到数据处理再到执行器动作。这个转速计数器项目虽然简单但完整地走了一遍这个链条。把它做扎实比囫囵吞枣做十个项目都有用。