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的通道1CH1后续用于PWM输入捕获。EC11 B相接PA1。PA1是TIM2的通道2CH2与CH1配合用于正交解码和PWM输入。EC11 SW按键接PA2并通过一个10KΩ电阻上拉到3.3V。同时PA2配置为下拉输入这样按下时读到高电平松开为低电平。按键用于清零计数器等操作。在A相、B相输入端我强烈建议各串联一个100Ω-1kΩ的电阻并接一个0.1uF的电容到地组成简单的RC低通滤波。这对EC11这种机械编码器至关重要可以滤除大部分触点抖动产生的毛刺避免误触发。硬件消抖的成本很低效果却比纯软件消抖好得多。注意STM32的定时器引脚是复用的。TIM2_CH1对应PA0TIM2_CH2对应PA1。务必查阅芯片数据手册的“复用功能映射”章节确认不同型号的STM32引脚映射可能不同。3. 定时器核心模式解析正交解码与PWM输入如何协同工作这是整个项目的理论核心。STM32的定时器功能强大我们这里需要让它“一心二用”但这两个功能在硬件逻辑上是紧密关联的。3.1 正交编码器模式让硬件成为“计数员”首先我们把TIM2配置为编码器模式。在这个模式下定时器不再由内部时钟驱动而是将A相PA0和B相PA1的信号作为自己的时钟源。定时器的计数寄存器CNT会根据A、B信号的电平变化和相位关系自动增减。配置的关键在于两个参数极性和计数模式。极性指的是定时器通道对输入信号的捕捉边沿。我们可以选择在TI1A相的上升沿、下降沿或双边沿TI2B相同理。对于正交解码通常设置为“在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配置系统时钟树将主频推到72MHzSTM32F103的极限。更高的主频意味着定时器计数时钟更快测频精度更高。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): 设置为6553516位定时器的最大值。编码器模式下ARR通常设置为最大因为CNT是16位无符号数溢出后会从0开始循环。我们需要在软件中处理溢出。Prescaler (PSC): 设置为0。编码器模式下预分频器不起作用时钟直接来自编码器信号。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相接PA0TIM2_CH1B相接PA1TIM2_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 来获取更准确的值。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 使用更高精度定时器或DMA32位定时器如果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。通过这个完整的“传感器-控制器-执行器”链条你就实现了一个真正的电机调速系统。而这一切的起点就是稳定可靠地读取旋转编码器这个小小的部件发出的脉冲信号。从最初被中断淹没的困境到后来驾驭硬件定时器游刃有余这个过程让我深刻体会到在嵌入式开发中理解硬件、善用硬件往往比写出复杂的软件算法更能高效、优雅地解决问题。