1. 项目缘起:从“数不清”的脉冲到精准的转速测量
最近在做一个需要精确控制电机转速的小项目,用到了带霍尔传感器的直流有刷电机。电机转起来,霍尔传感器会输出两路相位差90度的方波,也就是我们常说的正交编码信号。一开始图省事,我直接用STM32的外部中断,在A相的上升沿和下降沿都触发中断,然后在中断服务函数里对一个全局变量进行加或减操作来计数。想法很美好:A相跳变一次就计一次数,再根据B相的电平判断方向,这不就把位置和速度都算出来了吗?
实际跑起来就傻眼了。电机转速稍微一高,比如超过每分钟几百转,MCU就开始“丢脉冲”,计数值完全对不上。打开调试器一看,CPU几乎被中断给“淹”了,大部分时间都在进出中断,主程序根本跑不动。这让我意识到,对于旋转编码器这种高频脉冲信号的处理,靠软件中断计数是条死胡同,必须得用硬件定时器的“正交解码”模式,把计数的活儿完全交给硬件,解放CPU。
正交解码模式听起来高级,其实理解透了就很简单。你可以把它想象成硬件里内置了一个聪明的“计数员”。这个计数员不眠不休,专门盯着编码器A、B两根线上的电平变化。它有一套严格的规则:只有当A、B两线的电平按照特定顺序变化时,才认为发生了一次有效的计数。比如,常见的一种规则是,在A相上升沿时,如果B相是低电平,就向上计数(正转);如果B相是高电平,就向下计数(反转)。这样,硬件自动完成了脉冲计数和方向判断,精度高、零延迟,而且完全不占用CPU时间。
我这个项目里,不仅要测位置(累计脉冲数),更要实时计算速度。速度怎么来?就是单位时间内的脉冲数。这里就引出了另一个核心功能:输入捕获。特别是用PWM输入模式,可以非常方便地测量编码器输出波形的周期,从而反推出转速。所以,这次实验的目标很明确:利用STM32定时器的正交解码模式获取编码器位置,同时使用同一个定时器的PWM输入模式来测量编码器输出信号的频率(周期),实现位置与速度的同步、高精度测量。
2. 硬件选型与电路连接:为什么是EC11与STM32F103?
工欲善其事,必先利其器。编码器和MCU的选型是第一步。
我手头正好有几个EC11旋转编码器,这是一种非常常见的增量式编码器,价格便宜,手感清晰(带按键),在多媒体旋钮、菜单选择等场合应用极广。它内部就是一个机械触点开关,通过旋转产生两路相位差90度的方波(A相、B相)和一个独立的按键信号(SW)。注意,EC11是机械式的,存在抖动,但对我们演示正交解码和测频原理来说完全够用,而且其抖动问题本身也是一个很好的学习点。
MCU我选择了经典的STM32F103C8T6,也就是常说的“蓝莓派”或“最小系统板”核心芯片。原因有三:第一,它资源足够,拥有多个高级/通用定时器,都支持正交编码器模式和输入捕获;第二,资料丰富,社区支持好,踩了坑容易找到解决方案;第三,性价比无敌。我们用到的是TIM2定时器,它是一个通用定时器,正好同时支持编码器模式和PWM输入模式,完美契合需求。
接线很简单,但有几个细节需要注意:
- EC11 A相接PA0。PA0是TIM2的通道1(CH1),后续用于PWM输入捕获。
- EC11 B相接PA1。PA1是TIM2的通道2(CH2),与CH1配合用于正交解码和PWM输入。
- EC11 SW(按键)接PA2,并通过一个10KΩ电阻上拉到3.3V。同时,PA2配置为下拉输入,这样按下时读到高电平,松开为低电平。按键用于清零计数器等操作。
- 在A相、B相输入端,我强烈建议各串联一个100Ω-1kΩ的电阻,并接一个0.1uF的电容到地,组成简单的RC低通滤波。这对EC11这种机械编码器至关重要,可以滤除大部分触点抖动产生的毛刺,避免误触发。硬件消抖的成本很低,效果却比纯软件消抖好得多。
注意:STM32的定时器引脚是复用的。TIM2_CH1对应PA0,TIM2_CH2对应PA1。务必查阅芯片数据手册的“复用功能映射”章节确认,不同型号的STM32引脚映射可能不同。
3. 定时器核心模式解析:正交解码与PWM输入如何协同工作
这是整个项目的理论核心。STM32的定时器功能强大,我们这里需要让它“一心二用”,但这两个功能在硬件逻辑上是紧密关联的。
3.1 正交编码器模式:让硬件成为“计数员”
首先,我们把TIM2配置为编码器模式。在这个模式下,定时器不再由内部时钟驱动,而是将A相(PA0)和B相(PA1)的信号作为自己的时钟源。定时器的计数寄存器(CNT)会根据A、B信号的电平变化和相位关系自动增减。
配置的关键在于两个参数:极性和计数模式。
- 极性:指的是定时器通道对输入信号的捕捉边沿。我们可以选择在TI1(A相)的上升沿、下降沿或双边沿,TI2(B相)同理。对于正交解码,通常设置为“在TI1和TI2的边沿处计数”,这样灵敏度最高,每个脉冲周期可以产生4次计数(A上升、A下降、B上升、B下降),这就是所谓的“4倍频”模式,能将编码器的物理分辨率提高4倍。
- 计数模式:这决定了计数方向与A、B相位的关系。STM32提供了几种模式,例如:
- 模式1:仅在TI1边沿计数,根据TI2电平决定方向。
- 模式2:仅在TI2边沿计数,根据TI1电平决定方向。
- 模式3:在TI1和TI2的边沿都计数,方向由两者的相位关系决定。
我们选择模式3,这是最常用也是最强大的模式,实现了真正的4倍频计数。硬件内部会判断:当TI1领先TI2 90度时(正转),计数器递增;当TI2领先TI1 90度时(反转),计数器递减。这一切都是硬件自动完成的,读取CNT寄存器的值,我们就得到了经过4倍频后的、带方向的位置信息。
3.2 PWM输入模式:精准捕获脉冲周期
PWM输入模式是输入捕获的一个特殊应用,专门用于测量周期和占空比。它巧妙地利用了定时器的一个从模式控制器。
它的工作原理是这样的:
- 我们指定一个通道(比如TI1,即A相)作为触发输入。
- 当TI1出现上升沿时,从模式控制器被触发,同时发生三件事:
- 定时器的计数器CNT被清零。
- 输入捕获寄存器CCR1锁存当前CNT值(此时是0,但这个值不重要)。
- 输入捕获寄存器CCR2也锁存当前CNT值(此时也是0)。
- 计数器CNT开始从0自由递增(由内部时钟驱动)。
- 当TI1出现下一个上升沿时,再次触发。
- CCR1再次锁存此时的CNT值。这个值,就是两个上升沿之间的时间间隔,也就是信号周期对应的计数值。
- 同时,CCR2会在TI1的下降沿(或根据配置的其他边沿)被触发锁存,这个值对应高电平时间,可用于计算占空比。
精妙之处在于:PWM输入模式将两个输入捕获寄存器(CCR1和CCR2)关联到同一个输入通道(TI1),但捕捉不同的边沿(通常CCR1捕捉上升沿,CCR2捕捉下降沿),并且配合从模式复位,实现了仅用一个外部信号就能自动、连续、高精度地测量周期和占空比,无需软件频繁清零计数器。
在我们的场景中,编码器A相输出的就是PWM波形(占空比通常是50%的方波)。我们启用PWM输入模式来测量A相的周期T。那么,转速n(转/秒)就可以通过公式计算:n = (编码器线数 * 4倍频) / T因为我们是4倍频计数,所以每转产生的脉冲数是线数 * 4。测出脉冲周期T,其倒数就是频率f(脉冲数/秒),再除以每转的脉冲数,就得到转速。
3.3 模式共存:一个定时器如何同时胜任?
你可能会问,一个定时器既能做编码器模式又能做PWM输入模式吗?答案是:不能同时以这两种模式运行。因为编码器模式下,时钟源是外部AB相,计数器由它们驱动;而PWM输入模式下,时钟源是内部时钟,计数器由内部驱动。
我们的策略是:分时复用,以编码器模式为主,在需要测速时短暂切换模式。
- 常态运行:定时器配置为编码器模式(模式3)。此时CNT寄存器随着编码器旋转实时、准确地计数,反映绝对位置。PWM输入相关的从模式、捕获比较通道均不使能。
- 定时测速:在主循环或一个定时中断里,我们每隔一段时间(比如100ms)进行一次速度采样。在这段采样时间内: a.保存并重置:先记录下当前编码器模式下的CNT值(作为位置采样),然后将CNT清零(或通过改变计数方向等方式,但清零最直接)。 b.切换模式:快速将定时器重新配置为PWM输入模式(针对TI1)。这个重配置过程要快,期间编码器脉冲会丢失,所以采样间隔不能太短。 c.等待捕获:让定时器在PWM输入模式下运行一小段时间(例如20ms),确保至少捕获到一个完整的周期。 d.读取周期:从CCR1寄存器中读取捕获到的周期值(计数值)。 e.切回编码器模式:立即将定时器配置回编码器模式。同时,根据刚捕获的周期值计算该时间段内的平均转速。 f.处理位置:由于之前CNT被清零了,重新进入编码器模式后,CNT会从0开始根据新的AB相状态计数。我们需要将之前保存的位置采样值与后续的CNT变化量进行累加,以维持一个连续的、不因模式切换而断裂的“绝对位置”。这是一个软件层面的处理技巧。
这种方案是一种权衡,它牺牲了极短时间内(模式切换期间)的位置连续性,但换来了对同一路信号(A相)周期的高精度测量能力,同时保证了大部分时间内位置计数的硬件高效性。对于中等速度、不需要绝对连续位置的应用(如速度环控制),这是一个非常实用的方法。
4. 代码实现与配置详解:从CubeMX到Keil
理论通了,接下来就是实操。我使用STM32CubeMX进行图形化配置,然后用Keil MDK-ARM编写业务逻辑代码。
4.1 CubeMX基础配置
系统核心:选择外部高速晶振(HSE),配置系统时钟树,将主频推到72MHz(STM32F103的极限)。更高的主频意味着定时器计数时钟更快,测频精度更高。
GPIO配置:
- PA0、PA1:配置为复用功能上拉输入(Alternate Function Pull-up)。CubeMX会自动将其映射到TIM2_CH1和TIM2_CH2。
- PA2:配置为GPIO输入,下拉模式(Pull-down),用于编码器按键。
定时器TIM2配置:这是重头戏。
- 编码器模式配置:
- Combined Channels: 选择
Encoder Mode。 - Encoder Mode: 选择
Encoder Mode TI1 and TI2。这就是我们说的模式3,在TI1和TI2的边沿都计数。 - Polarity: TI1和TI2都选择
Rising Edge(实际上在Encoder Mode下,此设置可能被忽略或代表初始状态,具体以生成的代码为准,核心是选对模式)。 - Counter Period (ARR): 设置为65535(16位定时器的最大值)。编码器模式下,ARR通常设置为最大,因为CNT是16位无符号数,溢出后会从0开始循环。我们需要在软件中处理溢出。
- Prescaler (PSC): 设置为0。编码器模式下,预分频器不起作用,时钟直接来自编码器信号。
- Combined Channels: 选择
- PWM输入模式配置(需手动修改代码,CubeMX无法直接同时配置):
- 在CubeMX中,我们暂时只配置编码器模式。PWM输入模式的配置我们将在代码中动态修改。
- 需要确保的是,时钟源
Clock Source选择Internal Clock,因为PWM输入模式需要内部时钟驱动计数器。
- 编码器模式配置:
中断与DMA(可选):
- 使能TIM2的更新中断(Update Interrupt)。在编码器模式下,我们可以利用更新中断来处理计数器溢出。当CNT从ARR溢出到0,或从0下溢到ARR时,会产生更新中断。
- 使能TIM2的捕获/比较1中断(CC1IE)和捕获/比较2中断(CC2IE)。这是在PWM输入模式下,用于通知我们捕获完成的中断。注意:在编码器模式下,这些捕获比较通道用于正交解码,不应开启中断,否则会误触发。我们将在代码中动态开启和关闭。
生成代码:生成基于HAL库的初始化代码。
4.2 关键代码实现
生成的代码提供了框架,核心逻辑需要我们填充。下面列出关键部分:
全局变量定义:
// 位置相关(考虑溢出) volatile int32_t g_total_pulse_count = 0; // 软件维护的累计脉冲数(32位,防溢出) uint16_t g_last_cnt = 0; // 上一次读取的CNT值 // 速度测量相关 volatile uint32_t g_captured_period = 0; // 捕获到的周期值(计数值) volatile uint8_t g_speed_measurement_done = 0; // 速度测量完成标志 uint32_t g_speed_rpm = 0; // 计算出的转速,单位:转/分钟 #define ENCODER_PPR 20 // 编码器物理线数,EC11通常是20 #define ENCODER_MULTIPLIER 4 // 4倍频 #define TIMER_CLK_FREQ 72000000 // 定时器时钟频率,72MHz #define SPEED_SAMPLE_MS 100 // 速度采样周期,100msTIM2初始化后,默认处于编码器模式。我们编写模式切换函数:
// 切换到PWM输入模式(测量A相周期) void TIM2_Switch_To_PWMInputMode(void) { HAL_TIM_IC_Stop_IT(&htim2, TIM_CHANNEL_1); HAL_TIM_IC_Stop_IT(&htim2, TIM_CHANNEL_2); HAL_TIM_Encoder_Stop(&htim2, TIM_CHANNEL_ALL); // 重新配置TIM2为PWM输入模式(通道1作为输入) TIM2->SMCR &= ~(TIM_SMCR_SMS); // 清除从模式 TIM2->SMCR |= TIM_SMCR_SMS_2; // 触发模式:复位模式 (010) TIM2->SMCR |= TIM_SMCR_TS_2 | TIM_SMCR_TS_0; // 触发选择:TI1FP1 (101) TIM2->CCMR1 &= ~(TIM_CCMR1_CC1S | TIM_CCMR1_CC2S); // 清除通道模式 TIM2->CCMR1 |= TIM_CCMR1_CC1S_0; // CC1通道映射到TI1 (01) TIM2->CCMR1 |= TIM_CCMR1_CC2S_1; // CC2通道也映射到TI1 (10) TIM2->CCER &= ~(TIM_CCER_CC1P | TIM_CCER_CC2P); // 上升沿捕获 TIM2->CCER |= TIM_CCER_CC2P; // CC2改为下降沿捕获(用于占空比,可选) TIM2->CCER |= TIM_CCER_CC1E | TIM_CCER_CC2E; // 使能捕获 TIM2->DIER |= TIM_DIER_CC1IE | TIM_DIER_CC2IE; // 使能捕获中断 TIM2->CNT = 0; // 计数器清零 __HAL_TIM_ENABLE(&htim2); // 使能定时器 HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2); } // 切换回编码器模式 void TIM2_Switch_To_EncoderMode(void) { HAL_TIM_IC_Stop_IT(&htim2, TIM_CHANNEL_1); HAL_TIM_IC_Stop_IT(&htim2, TIM_CHANNEL_2); __HAL_TIM_DISABLE(&htim2); // 重新配置为编码器模式 TIM2->SMCR &= ~(TIM_SMCR_SMS); // 清除从模式 TIM2->SMCR |= TIM_SMCR_SMS_1 | TIM_SMCR_SMS_0; // 编码器模式3 (011) TIM2->CCMR1 &= ~(TIM_CCMR1_CC1S | TIM_CCMR1_CC2S); TIM2->CCMR1 |= TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_0; // CC1->TI1, CC2->TI2 TIM2->CCER &= ~(TIM_CCER_CC1P | TIM_CCER_CC2P); // 上升沿 TIM2->CCER |= TIM_CCER_CC1E | TIM_CCER_CC2E; // 使能输入 TIM2->DIER &= ~(TIM_DIER_CC1IE | TIM_DIER_CC2IE); // 禁用捕获中断,防止干扰 TIM2->DIER |= TIM_DIER_UIE; // 使能更新中断(用于处理溢出) TIM2->CNT = 0; // 计数器清零,软件位置从g_total_pulse_count重新开始累积 __HAL_TIM_ENABLE(&htim2); HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL); }位置处理与溢出中断:在编码器模式下,CNT是16位的,当正转超过65535或反转低于0时,会触发更新中断(UIE)。我们需要在这个中断里维护一个32位的全局位置。
// 在stm32f1xx_it.c的TIM2_IRQHandler中 void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET) { if (__HAL_TIM_GET_IT_SOURCE(&htim2, TIM_IT_UPDATE) != RESET) { __HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE); // 判断溢出方向:读取当前计数方向位(DIR) if (__HAL_TIM_GET_DIRECTION(&htim2) == TIM_COUNTERMODE_UP) { // 向上溢出,说明正转经过了65535->0 g_total_pulse_count += 65536; } else { // 向下溢出,说明反转经过了0->65535 g_total_pulse_count -= 65536; } } } // ... 其他中断处理(如捕获中断) }主循环中的速度采样逻辑:
while (1) { // 1. 读取并更新位置(非中断安全,简单演示) uint16_t current_cnt = TIM2->CNT; int16_t diff = (int16_t)(current_cnt - g_last_cnt); // 差值可能为负(反转) g_total_pulse_count += diff; g_last_cnt = current_cnt; // 2. 每隔SPEED_SAMPLE_MS毫秒进行一次速度测量 static uint32_t last_speed_tick = 0; if (HAL_GetTick() - last_speed_tick >= SPEED_SAMPLE_MS) { last_speed_tick = HAL_GetTick(); // 保存当前位置快照 int32_t position_before = g_total_pulse_count; // 切换到PWM输入模式 TIM2_Switch_To_PWMInputMode(); g_speed_measurement_done = 0; // 等待测量完成(这里用简单延时等待中断标志,实际应用建议用信号量或标志位) HAL_Delay(5); // 等待足够时间捕获至少一个周期 // 通常等待捕获中断置位g_speed_measurement_done if (g_captured_period > 0) { // 计算频率:定时器时钟 / 周期计数值 float frequency_hz = (float)TIMER_CLK_FREQ / g_captured_period; // 计算转速:频率 / (每转脉冲数) * 60 g_speed_rpm = (uint32_t)((frequency_hz * 60.0f) / (ENCODER_PPR * ENCODER_MULTIPLIER)); } else { g_speed_rpm = 0; // 未捕获到有效周期,可能转速极低或停止 } // 切换回编码器模式 TIM2_Switch_To_EncoderMode(); // 恢复位置:由于CNT被清零,需要基于之前保存的快照和新的CNT值重新计算g_total_pulse_count g_total_pulse_count = position_before; // 先恢复到切换前的值 g_last_cnt = TIM2->CNT; // 更新为编码器模式下的新CNT起始点 // 后续的位置累加将基于这个新的g_last_cnt继续 } // 3. 按键处理(例如清零位置) if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET) { HAL_Delay(50); // 简单消抖 if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET) { g_total_pulse_count = 0; TIM2->CNT = 0; g_last_cnt = 0; while (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET); // 等待松开 } } // 其他应用逻辑... }PWM输入捕获中断处理:
// 在输入捕获中断回调函数中 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) { // CCR1捕获到上升沿,即周期值 g_captured_period = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); g_speed_measurement_done = 1; } // 如果需要占空比,可以处理CHANNEL_2的中断 } }5. 实测调试与常见问题排查
代码写好了,下载到板子上,连接好EC11编码器。上电后,打开串口助手,打印位置和速度信息。旋转编码器,你应该能看到位置值平滑变化,速度值也能大致反映你旋转的快慢。但这个过程很少一帆风顺,下面是我踩过的一些坑和解决办法。
5.1 位置计数跳动、不准或反向
- 现象:轻轻旋转编码器,位置值乱跳,增加减少没有规律。
- 排查:
- 硬件消抖:这是EC11这类机械编码器最常见的问题。首先检查是否按前述加了RC滤波(如100Ω电阻串联,103电容对地)。用示波器看A、B相波形,理想应为干净方波,如果有密集毛刺,说明消抖不够,可增大电容(如0.1uF)或电阻值。
- 接线错误:确认A相接PA0(TIM2_CH1),B相接PA1(TIM2_CH2)。接反了会导致方向判断错误。可以尝试在代码中交换
TIM_ENCODERMODE_TI1和TIM_ENCODERMODE_TI2的配置,或者直接调换A、B相的接线。 - GPIO模式:确保PA0和PA1配置为复用模式(Alternate Function),而不是普通的输入模式。普通输入模式无法连接到定时器的编码器接口。
- 上拉电阻:编码器内部如果是开漏输出,则需要MCU内部或外部上拉。CubeMX中配置为上拉(Pull-up)是稳妥的做法。
5.2 速度测量值为零、异常大或不变
- 现象:转速显示为0,或者是一个巨大的固定值,不随旋转变化。
- 排查:
- 模式切换时序:这是最关键的。确保从“切换到PWM输入模式”到“读取捕获值”之间,有足够的延迟(
HAL_Delay(5)只是一个例子)。这个延迟必须大于编码器输出信号的一个周期。转速越慢,周期越长,所需延迟越长。更好的做法是用g_speed_measurement_done标志位,在捕获中断置位后才读取数据。 - 捕获中断未触发:检查是否使能了TIM2的捕获比较中断(CC1IE),并在NVIC中开启了中断。确认
HAL_TIM_IC_Start_IT函数被成功调用。 - 周期值溢出:如果编码器转速极慢,两个上升沿之间的时间可能超过定时器ARR的最大值(65535 * 时钟周期)。此时计数器会溢出,捕获值不正确。解决方法是降低定时器时钟(增大PSC分频),或者使用32位定时器(如TIM5,如果芯片支持)。在我们的测速函数中,如果
g_captured_period为0或接近0,应视为无效数据。 - 计算公式错误:核对转速计算公式。
TIMER_CLK_FREQ必须是定时器实际的计数时钟,考虑APB1预分频器的影响。STM32F103中,如果APB1预分频系数不为1,定时器时钟会是APB1时钟的2倍。使用HAL_RCC_GetPCLK1Freq()* 2 来获取更准确的值。
- 模式切换时序:这是最关键的。确保从“切换到PWM输入模式”到“读取捕获值”之间,有足够的延迟(
5.3 位置值在模式切换后发生跳变
- 现象:每次测速后,位置值会有一个明显的突变。
- 原因与解决:这是我们方案固有的问题。因为在切换到PWM输入模式前,我们保存了
g_total_pulse_count,然后清零了CNT。测速完成后切回编码器模式,CNT从0开始根据当前AB相状态计数。但在这段模式切换的“盲区”时间内,编码器可能已经产生了几个脉冲,这些脉冲被丢失了。 - 优化策略:
- 缩短模式切换时间:将模式切换的代码优化到极致,使用寄存器直接操作(如上面代码所示)而不是频繁调用HAL库函数,尽量减少盲区。
- 提高测速频率:在高速时,盲区丢失的脉冲数占比较小,影响不大。在低速或需要极高位置精度时,此方案不适用。
- 备用方案:如果必须同时要求高精度位置和速度,且MCU资源允许,最好的方法是使用两个定时器。一个定时器(如TIM2)始终工作在编码器模式,用于位置计数。另一个定时器(如TIM3或TIM4)配置为输入捕获模式,专门测量编码器某一相(如A相)的脉冲周期。两个定时器互不干扰,但需要额外的硬件定时器资源。
5.4 按键读取不稳定
- 现象:按键按下一次,程序判断为多次触发。
- 解决:这是机械按键的经典问题。除了前面代码中简单的延时消抖,更可靠的方法是使用状态机进行消抖。或者利用定时器周期中断(如SysTick)来扫描按键状态,只有连续多次采样到稳定状态才认为按键有效。
6. 进阶优化与扩展思路
当基础功能跑通后,可以考虑以下优化,让项目更健壮、更实用。
6.1 低转速与零速检测
在极低转速下,两次捕获间隔可能很长,甚至无限长(停止)。我们的“定时采样+等待捕获”策略会阻塞或返回无效值。
- 改进:在PWM输入模式下,可以启用定时器的溢出中断(UIE)。如果在设定的超时时间内(比如100ms)都没有捕获到新的上升沿,则认为转速为0或低于可测量范围。在溢出中断里,将
g_captured_period设为一个标志值(如0xFFFFFFFF),并在速度计算中将其解释为0 RPM。
6.2 速度滤波
直接计算出的瞬时速度值可能因为单个周期测量的微小抖动而波动很大,不适合直接用于闭环控制。
- 应用滑动平均滤波:维护一个包含最近N次速度测量值的队列,每次计算当前速度时,取这个队列的平均值。例如:
滤波窗口大小需要根据系统响应速度和稳定性要求折中选取。#define SPEED_FILTER_SIZE 5 uint32_t speed_buffer[SPEED_FILTER_SIZE] = {0}; uint8_t speed_index = 0; uint32_t filtered_speed_rpm = 0; // 每次计算得到g_speed_rpm后 speed_buffer[speed_index] = g_speed_rpm; speed_index = (speed_index + 1) % SPEED_FILTER_SIZE; filtered_speed_rpm = 0; for (int i = 0; i < SPEED_FILTER_SIZE; i++) { filtered_speed_rpm += speed_buffer[i]; } filtered_speed_rpm /= SPEED_FILTER_SIZE;
6.3 使用更高精度定时器或DMA
- 32位定时器:如果MCU支持(如STM32F103的TIM2在某些型号是32位的,或者使用TIM5),使用32位定时器作为编码器计数器,可以极大扩展计数范围,减少溢出中断频率,甚至在某些应用中可以不处理溢出。
- DMA传输:可以将定时器的捕获比较寄存器(CCR1)与一个内存数组通过DMA连接。设置DMA为循环模式,每当捕获发生时,DMA自动将CCR1的值传输到数组下一个位置。这样可以在不消耗CPU中断资源的情况下,连续记录多个周期值,用于更复杂的信号分析或滤波。
6.4 从“测量”到“控制”
这个实验为我们提供了一个精准的位置和速度反馈源。接下来,你可以很容易地将其融入一个闭环控制系统。例如,一个简单的速度PID控制回路:
- 设定目标转速:通过电位器、串口命令等方式给定。
- 获取反馈转速:使用本项目测量到的
filtered_speed_rpm。 - 计算PID输出:
输出 = Kp * 误差 + Ki * 误差积分 + Kd * 误差微分。其中误差 = 目标转速 - 反馈转速。 - 驱动执行器:将PID输出值映射到PWM占空比,控制电机驱动芯片(如L298N、TB6612)来调整电机电压,从而改变转速。
- 循环:以固定的周期(例如每10ms)执行步骤2-4。
通过这个完整的“传感器-控制器-执行器”链条,你就实现了一个真正的电机调速系统。而这一切的起点,就是稳定可靠地读取旋转编码器这个小小的部件发出的脉冲信号。从最初被中断淹没的困境,到后来驾驭硬件定时器游刃有余,这个过程让我深刻体会到,在嵌入式开发中,理解硬件、善用硬件,往往比写出复杂的软件算法更能高效、优雅地解决问题。