1. 项目概述:从定时器到精准测速
在嵌入式开发,尤其是涉及电机控制、机器人底盘、智能小车等项目中,实时且准确地获取电机的转速是一个基础且关键的需求。直接测量电机轴的物理旋转速度往往不切实际,而编码器,特别是霍尔编码器,因其非接触、高可靠性、成本适中的特点,成为了最常用的解决方案。本项目标题“STM32定时器(4.JGA25-370霍尔编码器测速)”清晰地指向了一个具体的实战场景:使用STM32微控制器的定时器模块,来解码一款名为JGA25-370的直流减速电机上集成的霍尔编码器信号,并计算出电机的实时转速。
JGA25-370是一款非常常见的微型减速电机,在智能小车、机械臂关节等DIY和教学项目中出镜率极高。它尾部集成了一个简单的双路霍尔编码器,通常输出两路相位差90度的方波信号(即正交编码信号)。STM32的定时器,特别是通用定时器(TIM2, TIM3, TIM4等)和高级定时器(TIM1, TIM8),其编码器接口模式正是为直接处理这种正交信号而设计的硬件功能。这个项目的核心,就是打通从硬件脉冲到软件可读速度值(如RPM-转每分钟)的完整链路。这不仅仅是配置几个寄存器那么简单,它涉及到定时器工作模式的理解、脉冲计数与方向判别的硬件机制、采样周期的设计、速度计算算法的选择,以及如何应对低速和高速情况下的精度与溢出问题。对于刚接触电机控制的开发者来说,成功实现这个功能,意味着迈过了从控制电机“转起来”到“知道它转得多快”的关键一步。
2. 核心硬件与原理深度解析
2.1 JGA25-370霍尔编码器信号剖析
JGA25-370电机尾部的编码器通常是一个简单的双霍尔传感器方案。电机轴带动一个多极磁环旋转,霍尔传感器感应磁场变化,输出数字信号。关键参数在于它的“线数”。对于JGA25-370,常见的编码器分辨率是每转11个脉冲(即11 CPR - Counts Per Revolution)。这里有一个非常重要的概念需要厘清:这里的“11个脉冲”指的是单路信号(A相或B相)每转输出的脉冲数。
由于输出是A、B两相正交信号,定时器的编码器接口在识别到A、B相的每个边沿(上升沿和下降沿)时都可以进行计数。因此,对于11 CPR的编码器,在“编码器模式3”(TI1和TI2的边沿都触发计数)下,定时器计数器每转可以记录到的脉冲数为11 * 4 = 44。这个值被称为“每转计数”(PPR - Pulses Per Revolution)。在计算速度时,我们必须使用PPR,而不是电机铭牌或商家标注的CPR。混淆这两个概念是导致计算结果错误的最常见原因之一。
注意:务必查阅你所购买电机的具体资料,或通过示波器观察、手动旋转计数的方式来确认编码器的真实CPR。不同批次或供应商的JGA25-370可能存在差异。
信号质量方面,霍尔编码器输出通常是开漏输出,需要上拉电阻(通常电机模块已集成)才能得到稳定的高电平。A、B两相信号在电机正转和反转时,相位关系会互换,这是硬件判别方向的基础。
2.2 STM32定时器编码器接口模式工作机制
STM32的编码器接口模式是其定时器外设一个极为巧妙和高效的设计。它将复杂的边沿检测、方向逻辑和计数全部交由硬件完成,极大减轻了CPU负担,并保证了计数的实时性和准确性。
其工作原理可以概括为:
- 信号输入:将编码器的A相和B相信号分别接入定时器的两个输入通道(例如TIMx_CH1和TIMx_CH2)。
- 边沿检测与滤波:定时器可配置对输入信号的边沿进行检测,并可以通过数字滤波器消除可能的毛刺干扰。
- 方向解码:硬件根据A、B两相的相对相位关系自动判断旋转方向。例如,当A相超前B相90度时,定义为正转,计数器递增;当B相超前A相90度时,定义为反转,计数器递减。
- 计数:根据配置的编码器模式(模式1、2、3),在相应的边沿事件上更新16位或32位的自动重载寄存器(CNT)。这个CNT值就是一个带符号的计数值,其变化量直接反映了在一定时间内电机转过的“脉冲数”。
模式选择详解:
- 编码器模式1:仅在TI1(A相)的边沿计数,计数方向由TI2(B相)的电平决定。
- 编码器模式2:仅在TI2(B相)的边沿计数,计数方向由TI1(A相)的电平决定。
- 编码器模式3:在TI1和TI2的所有边沿都计数。这是最常用的模式,因为它能提供最高的分辨率(4倍频),即前面提到的将CPR转化为PPR。
在项目中,我们几乎总是选择模式3,以获得最精细的位置和速度反馈。
2.3 测速算法:M法与T法
获取了脉冲计数后,如何将其转化为速度?主要有两种经典算法:M法(频率法)和T法(周期法)。
M法(频率法):
- 原理:在固定的采样时间
T(例如10ms)内,统计编码器脉冲的个数M。 - 速度计算:速度
V = (M / PPR) / T。单位是 RPS(转每秒),乘以60即得 RPM(转每分钟)。 - 特点:在高速时精度高,因为脉冲数多,统计误差小;在低速时,可能一个采样周期内只收到几个甚至零个脉冲,误差极大,甚至无法测量。
T法(周期法):
- 原理:测量固定数量的脉冲(例如1个脉冲)所花费的时间
T。 - 速度计算:速度
V = (1 / PPR) / T。同样可换算为RPM。 - 特点:在低速时精度高,因为低速时脉冲周期长,时间测量相对准确;在高速时,脉冲周期极短,对计时器的精度要求极高,且容易受到中断响应延迟等因素影响,误差大。
M/T法(混合法): 在实际工程中,为了在全速度范围内都有较好的精度,常采用M/T法。其思想是:同时固定采样时间和最小脉冲数。例如,设定一个采样时间窗口,但确保在这个窗口内至少会计数到N个脉冲。如果时间到了但脉冲数不足N,则延长采样时间直到脉冲数达到N,然后用实际的脉冲数和实际的时间进行计算。这种方法软件实现稍复杂,但效果最好。
对于JGA25-370这类减速电机,其空载转速通常在百转每分钟的量级,经过减速箱后输出轴转速更低。因此,M法配合一个合适的、稍长的采样周期(如50ms或100ms)是简单有效的选择。我们本项目也将基于M法实现。
3. 系统设计与硬件连接
3.1 整体软件架构设计
一个健壮的测速模块不应只是简单地在主循环里读数和计算。我们需要考虑实时性、数据同步和模块化。一个推荐的设计架构如下:
- 硬件层:STM32定时器配置为编码器模式,自动完成脉冲计数。
- 驱动层:提供初始化函数、获取当前计数器值的函数。关键点:由于CNT是16位变量,需要考虑溢出问题。我们需要使用
int16_t或int32_t类型来读取CNT,并处理其有符号性。 - 应用层(测速任务):
- 创建一个周期性的定时中断(如SysTick或另一个通用定时器),作为速度采样的时间基准。
- 在中断服务函数中,读取当前编码器计数器值,与上一次的值做差,得到
ΔCnt。这个差值就是采样周期内的净脉冲数。 - 根据M法公式计算速度。
- 对计算出的速度进行简单的滤波(如一阶低通滤波),以抑制噪声。
- 更新速度变量,供上层应用(如PID控制器、显示模块)读取。
- 数据输出:可以通过串口打印到PC端调试,或者用于闭环控制。
3.2 硬件电路连接详解
JGA25-370电机通常引出5根线:电机电源正负(M+, M-)和编码器线(VCC, GND, A, B)。有的模块可能将编码器电源与电机电源隔离。
连接步骤:
- 电机驱动:将M+和M-连接到电机驱动芯片(如L298N、TB6612)的输出端。电机驱动模块的输入逻辑和电源按常规连接。
- 编码器供电:将编码器的VCC和GND连接到STM32开发板的3.3V和GND。务必确认电压匹配,大多数JGA25-370编码器工作电压为3.3V-5V,连接3.3V是安全的。
- 信号线连接:将编码器的A相输出线连接到STM32定时器的CH1引脚,B相连接到CH2引脚。例如,选择TIM2,则A相接PA0,B相接PA1。
- 上拉电阻:如果编码器模块内部没有集成上拉电阻,需要在STM32的这两个输入引脚上外部连接10kΩ左右的上拉电阻到3.3V,以确保信号高电平稳定。
实操心得:在焊接或接线时,最好给A、B相信号线套上不同颜色的热缩管或使用彩线,并在代码中做好注释。调试阶段,用示波器同时观察A、B两相信号是最直观的验证方法,可以看到清晰的正交方波以及正反转时的相位关系变化。
3.3 STM32定时器资源选择与配置要点
不是所有STM32定时器都支持编码器模式。以STM32F103C8T6(蓝色药丸核心板)为例:
- 高级定时器:TIM1, TIM8。
- 通用定时器:TIM2, TIM3, TIM4, TIM5。 这些定时器都支持编码器接口。
选择哪个定时器主要看引脚布局是否方便。TIM2和TIM3是最常用的选择。配置的关键步骤如下(以HAL库为例):
引脚复用:配置对应的GPIO为复用上拉模式,速度设为高速。
定时器基本参数:
Prescaler(预分频器):设置为0,即不分频。编码器模式会忽略这个参数。CounterMode(计数模式):设置为TIM_COUNTERMODE_UP(实际上在编码器模式下此配置无效,方向由硬件决定)。Period(自动重载值):设置为最大值65535(对于16位定时器)。这个值决定了计数器的溢出上限。有时为了简化溢出处理,可以将其设为0,但设为最大值是最通用的做法。ClockDivision:通常为0。AutoReloadPreload:通常禁用。
编码器模式配置:调用
HAL_TIM_Encoder_Init,并在其关联的TIM_Encoder_InitTypeDef结构中设置:EncoderMode:设置为TIM_ENCODERMODE_TI12,即模式3(在TI1和TI2的所有边沿计数)。IC1Polarity和IC2Polarity:设置为TIM_INPUTCHANNELPOLARITY_RISING或TIM_ICPOLARITY_BOTHEDGE,具体取决于库版本,目的是让定时器捕获两个边沿。通常设为TIM_INPUTCHANNELPOLARITY_RISING即可,在编码器模式下它会自动处理双边沿。IC1Selection和IC2Selection:设置为TIM_ICSELECTION_DIRECTTI,表示信号直接连接到输入捕获单元。IC1Filter和IC2Filter:设置一个合适的滤波值,如0x0F,可以滤除信号上的高频毛刺,提高稳定性。值越大,滤波时间常数越大。
启动编码器:调用
HAL_TIM_Encoder_Start(&htimx, TIM_CHANNEL_ALL)。
配置完成后,定时器的CNT寄存器就会随着编码器的旋转而实时变化。
4. 软件实现与代码逐行解析
4.1 定时器编码器模式初始化代码
我们以STM32CubeIDE和HAL库为例,展示核心配置代码。假设使用TIM3,通道1(PA6)接A相,通道2(PA7)接B相。
// tim.c 中的初始化片段 TIM_Encoder_InitTypeDef sConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; htim3.Instance = TIM3; htim3.Init.Prescaler = 0; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 65535; // 16位最大值 htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_Encoder_Init(&htim3, &sConfig) != HAL_OK) { Error_Handler(); } sConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 模式3 sConfig.IC1Polarity = TIM_INPUTCHANNELPOLARITY_RISING; sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; sConfig.IC1Prescaler = TIM_ICPSC_DIV1; sConfig.IC1Filter = 0xF; // 滤波系数,可根据实际情况调整 sConfig.IC2Polarity = TIM_INPUTCHANNELPOLARITY_RISING; sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI; sConfig.IC2Prescaler = TIM_ICPSC_DIV1; sConfig.IC2Filter = 0xF; if (HAL_TIM_Encoder_Init(&htim3, &sConfig) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK) { Error_Handler(); } // 在main.c或应用层启动编码器 HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);4.2 速度采样与计算任务实现
我们使用SysTick定时器产生一个固定的时间中断(例如10ms)作为采样周期。在中断服务程序中计算速度。
首先,定义一些全局变量:
// 编码器相关 #define ENCODER_PPR 44 // JGA25-370, 11 CPR * 4 = 44 PPR int16_t encoder_last_count = 0; int32_t encoder_total_count = 0; // 用于累计计数,防止溢出带来的长期误差 float motor_rpm = 0.0; // 计算得到的转速 // 速度计算周期 (秒) const float speed_sample_time = 0.010f; // 10ms在SysTick中断(或其他定时器中断)中:
void SysTick_Handler(void) { static uint32_t tick = 0; tick++; // 每10ms执行一次速度计算 (假设SysTick是1ms中断) if (tick % 10 == 0) { // 1. 获取当前编码器计数值 int16_t encoder_current_count = (int16_t)TIM3->CNT; // 直接读寄存器,注意类型转换 // 2. 计算差值 (处理16位计数器溢出) int16_t delta_count = encoder_current_count - encoder_last_count; // 由于CNT是16位无符号,但硬件按有符号计数,当计数值从+32767到-32768时,直接相减会得到错误的大数。 // 更稳健的方法是使用int16_t类型读取,硬件会自动进行有符号转换。 // 另一种方法是:delta_count = (int16_t)(encoder_current_count - encoder_last_count); // 编译器会处理这个有符号差值。 // 3. 更新累计计数(可选,用于位置反馈) encoder_total_count += delta_count; // 4. 计算转速 (M法) // 转速 (RPS) = 脉冲差 / PPR / 采样时间 // 转速 (RPM) = 脉冲差 / PPR / 采样时间 * 60 motor_rpm = (float)delta_count / ENCODER_PPR / speed_sample_time * 60.0f; // 5. 应用低通滤波 (可选,使速度值更平滑) // motor_rpm_filtered = 0.9 * motor_rpm_filtered + 0.1 * motor_rpm; // 6. 更新上一次计数值 encoder_last_count = encoder_current_count; // 7. (调试用) 可以通过串口打印 motor_rpm } }代码关键点解析:
- 直接寄存器访问:
TIM3->CNT比调用__HAL_TIM_GET_COUNTER(&htim3)效率稍高,在中断中是可取的。 - 有符号差值处理:这是代码中最容易出错的地方。
encoder_current_count和encoder_last_count必须定义为int16_t。当CNT从65535(0xFFFF, 对于int16_t是-1)加1变成0时,int16_t视角下是从-1到0,差值为1,这是正确的。如果定义为uint16_t,差值计算将非常麻烦。 - 浮点运算:在中断中进行浮点运算会增加中断执行时间。如果主频较低(如72MHz),且中断频率不高(100Hz),通常可以接受。如果追求效率,可以全部使用整数运算,最后通过定点数或缩放因子来输出。
- 滤波:计算出的原始
motor_rpm可能因脉冲采集的微小抖动而波动。加入一阶低通滤波(代码注释部分)可以显著平滑曲线,提高可用性,但会引入相位滞后。
4.3 方向判断与累计位移计算
编码器接口模式的一个巨大优势是方向已经由硬件体现在计数的增减上。我们的delta_count正负就代表了方向。正数表示正转(计数器递增),负数表示反转(计数器递减)。
encoder_total_count这个累计值非常有用,它代表了从系统启动以来,电机转过的总脉冲数。结合PPR,可以换算成总圈数或直线位移(如果连接了轮子):
float total_revolutions = (float)encoder_total_count / ENCODER_PPR; // 总圈数 float distance = total_revolutions * wheel_circumference; // 行驶距离这对于机器人的里程计(Odometry)是基础数据。
5. 调试技巧与常见问题排查
5.1 调试阶段的核心检查点
- 信号是否接入:用万用表测量编码器输出引脚电压,旋转电机时,电压应在0V和3.3V之间跳变。最好用示波器观察A、B相波形,确认是正交方波且无异常毛刺。
- 计数器是否响应:在调试器中实时观察
TIMx->CNT寄存器的值。手动缓慢旋转电机,该值应有规律地增减。快速旋转时,数值应快速变化。 - 方向是否正确:定义电机的“正转”,然后观察CNT是增加还是减少。如果方向反了,可以交换A、B相的接线,或者在软件中将
delta_count取反。 - 速度计算是否准确:用手持转速表(或通过已知电压和电机KV值估算)测量电机实际转速,与串口打印的
motor_rpm对比。如果数值差一个固定倍数(如4倍),很可能是CPR和PPR搞错了。
5.2 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 排查方法与解决方案 |
|---|---|---|
| CNT值始终为0,不变化 | 1. 编码器供电不正常。 2. 信号线连接错误或虚焊。 3. 定时器编码器模式未正确启动。 4. GPIO未配置为复用模式。 | 1. 检查VCC和GND电压。 2. 用示波器检查信号。 3. 检查 HAL_TIM_Encoder_Start是否调用。4. 检查CubeMX或代码中的GPIO配置。 |
| CNT值只增不减或只减不增 | 1. 电机只朝一个方向转。 2. A、B相序接反,导致方向判断固定。 | 1. 尝试让电机反转。 2. 交换A、B相接线。 |
| 速度值跳动非常大,不稳定 | 1. 采样时间太短,M法在低速下脉冲数少,量化误差大。 2. 编码器信号有噪声。 3. 未进行软件滤波。 | 1. 增加采样周期(如从10ms改为50ms)。 2. 检查硬件连接,确保电源稳定,信号线远离干扰源。适当增加定时器输入滤波参数( ICxFilter)。3. 加入一阶低通滤波算法。 |
| 计算出的转速是实际值的2倍或4倍 | 混淆了CPR和PPR。 | 确认编码器单路CPR,并在代码中使用PPR = CPR * 4。 |
| 电机高速旋转时,速度值出现周期性错误或归零 | 计数器溢出处理不当。在高速下,delta_count可能超过int16_t能表示的单次最大变化。 | 1. 确保使用int16_t类型读取CNT并计算差值,这是处理硬件有符号溢出的正确方式。2. 如果速度极高,考虑缩短采样周期,使单次 delta_count不会超过±32767。 |
| 累计位置(encoder_total_count)长时间运行后漂移 | delta_count计算在极端溢出情况下仍有小概率出错。 | 使用int32_t或int64_t作为累计值。采用更鲁棒的差值计算函数:int16_t get_encoder_delta() {int16_t cnt = (int16_t)TIM3->CNT;int16_t delta = cnt - last_cnt;last_cnt = cnt;return delta;} |
5.3 高级优化与扩展思路
- 抗溢出处理增强:上述代码中的差值计算在绝大多数情况下是可靠的。但对于追求极致稳健性的场合,可以封装一个函数,该函数内部处理了所有边界情况。
- 使用32位定时器:如果使用的STM32型号有32位定时器(如TIM2或TIM5在某些系列是32位的),可以将其用作编码器计数器,这样几乎不用担心溢出问题,
encoder_total_count也可以直接从CNT获取。 - 利用定时器溢出中断:可以开启定时器的更新(溢出)中断。在中断中,对一个扩展的软件计数器(如
int32_t encoder_overflows)进行加减(根据方向标志)。这样,结合CNT和溢出次数,就能得到一个范围极大的绝对位置信息。 - 动态调整采样周期(自适应M/T法):在低速时自动延长采样时间,在高速时自动缩短采样时间,以保持测量精度。这需要更复杂的状态机管理。
- 多电机同步:如果需要同时测量多个电机的速度(如双轮差速小车),可以为每个电机分配一个支持编码器模式的定时器。注意定时器中断不要冲突,计算负载要均衡。
实现JGA25-370霍尔编码器测速,是掌握STM32高级外设和电机控制入门的一块重要拼图。它不仅仅是一个功能的实现,更是一种硬件与软件协同解决问题的思维训练。从理解正交编码原理,到配置STM32的硬件编码器接口,再到处理数据溢出和软件滤波,每一步都需要清晰的逻辑和细致的调试。当你看到串口终端上稳定、准确地显示出电机的实时转速时,就意味着你已经成功地将物理世界的旋转运动,转化为了数字世界可精确度量的信息,这为后续的PID速度闭环控制、位置伺服等更高级的应用奠定了坚实的基础。在实际项目中,我习惯将测速模块封装成一个独立的.c/.h文件,提供Encoder_Init,Encoder_GetSpeedRPM,Encoder_GetTotalPulses等接口,这样在主循环或RTOS任务中调用起来非常清晰,也便于代码复用和移植。