STM32定时器正交解码与PWM输入模式实现编码器高精度测速

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输入模式是输入捕获的一个特殊应用,专门用于测量周期和占空比。它巧妙地利用了定时器的一个从模式控制器。

它的工作原理是这样的:

  1. 我们指定一个通道(比如TI1,即A相)作为触发输入。
  2. 当TI1出现上升沿时,从模式控制器被触发,同时发生三件事:
    • 定时器的计数器CNT被 清零
    • 输入捕获寄存器CCR1锁存当前CNT值(此时是0,但这个值不重要)。
    • 输入捕获寄存器CCR2 也锁存当前CNT值(此时也是0)。
  3. 计数器CNT开始从0自由递增(由内部时钟驱动)。
  4. 当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输入模式下,时钟源是内部时钟,计数器由内部驱动。

我们的策略是: 分时复用,以编码器模式为主,在需要测速时短暂切换模式

  1. 常态运行 :定时器配置为编码器模式(模式3)。此时CNT寄存器随着编码器旋转实时、准确地计数,反映绝对位置。PWM输入相关的从模式、捕获比较通道均不使能。
  2. 定时测速 :在主循环或一个定时中断里,我们每隔一段时间(比如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基础配置

  1. 系统核心 :选择外部高速晶振(HSE),配置系统时钟树,将主频推到72MHz(STM32F103的极限)。更高的主频意味着定时器计数时钟更快,测频精度更高。

  2. GPIO配置

    • PA0、PA1:配置为复用功能上拉输入(Alternate Function Pull-up)。CubeMX会自动将其映射到TIM2_CH1和TIM2_CH2。
    • PA2:配置为GPIO输入,下拉模式(Pull-down),用于编码器按键。
  3. 定时器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。编码器模式下,预分频器不起作用,时钟直接来自编码器信号。
    • PWM输入模式配置(需手动修改代码,CubeMX无法直接同时配置)
      • 在CubeMX中,我们暂时只配置编码器模式。PWM输入模式的配置我们将在代码中动态修改。
      • 需要确保的是,时钟源 Clock Source 选择 Internal Clock ,因为PWM输入模式需要内部时钟驱动计数器。
  4. 中断与DMA(可选)

    • 使能TIM2的更新中断(Update Interrupt)。在编码器模式下,我们可以利用更新中断来处理计数器溢出。当CNT从ARR溢出到0,或从0下溢到ARR时,会产生更新中断。
    • 使能TIM2的捕获/比较1中断(CC1IE)和捕获/比较2中断(CC2IE)。这是在PWM输入模式下,用于通知我们捕获完成的中断。 注意 :在编码器模式下,这些捕获比较通道用于正交解码,不应开启中断,否则会误触发。我们将在代码中动态开启和关闭。
  5. 生成代码 :生成基于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 // 速度采样周期,100ms

TIM2初始化后,默认处于编码器模式。我们编写模式切换函数:

// 切换到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 位置计数跳动、不准或反向

  • 现象 :轻轻旋转编码器,位置值乱跳,增加减少没有规律。
  • 排查
    1. 硬件消抖 :这是EC11这类机械编码器最常见的问题。首先检查是否按前述加了RC滤波(如100Ω电阻串联,103电容对地)。用示波器看A、B相波形,理想应为干净方波,如果有密集毛刺,说明消抖不够,可增大电容(如0.1uF)或电阻值。
    2. 接线错误 :确认A相接PA0(TIM2_CH1),B相接PA1(TIM2_CH2)。接反了会导致方向判断错误。可以尝试在代码中交换 TIM_ENCODERMODE_TI1 TIM_ENCODERMODE_TI2 的配置,或者直接调换A、B相的接线。
    3. GPIO模式 :确保PA0和PA1配置为复用模式(Alternate Function),而不是普通的输入模式。普通输入模式无法连接到定时器的编码器接口。
    4. 上拉电阻 :编码器内部如果是开漏输出,则需要MCU内部或外部上拉。CubeMX中配置为上拉(Pull-up)是稳妥的做法。

5.2 速度测量值为零、异常大或不变

  • 现象 :转速显示为0,或者是一个巨大的固定值,不随旋转变化。
  • 排查
    1. 模式切换时序 :这是最关键的。确保从“切换到PWM输入模式”到“读取捕获值”之间,有足够的延迟( HAL_Delay(5) 只是一个例子)。这个延迟必须大于编码器输出信号的一个周期。转速越慢,周期越长,所需延迟越长。更好的做法是用 g_speed_measurement_done 标志位,在捕获中断置位后才读取数据。
    2. 捕获中断未触发 :检查是否使能了TIM2的捕获比较中断(CC1IE),并在NVIC中开启了中断。确认 HAL_TIM_IC_Start_IT 函数被成功调用。
    3. 周期值溢出 :如果编码器转速极慢,两个上升沿之间的时间可能超过定时器ARR的最大值(65535 * 时钟周期)。此时计数器会溢出,捕获值不正确。解决方法是降低定时器时钟(增大PSC分频),或者使用32位定时器(如TIM5,如果芯片支持)。在我们的测速函数中,如果 g_captured_period 为0或接近0,应视为无效数据。
    4. 计算公式错误 :核对转速计算公式。 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 使用更高精度定时器或DMA

  • 32位定时器 :如果MCU支持(如STM32F103的TIM2在某些型号是32位的,或者使用TIM5),使用32位定时器作为编码器计数器,可以极大扩展计数范围,减少溢出中断频率,甚至在某些应用中可以不处理溢出。
  • DMA传输 :可以将定时器的捕获比较寄存器(CCR1)与一个内存数组通过DMA连接。设置DMA为循环模式,每当捕获发生时,DMA自动将CCR1的值传输到数组下一个位置。这样可以在不消耗CPU中断资源的情况下,连续记录多个周期值,用于更复杂的信号分析或滤波。

6.4 从“测量”到“控制”

这个实验为我们提供了一个精准的位置和速度反馈源。接下来,你可以很容易地将其融入一个闭环控制系统。例如,一个简单的速度PID控制回路:

  1. 设定目标转速 :通过电位器、串口命令等方式给定。
  2. 获取反馈转速 :使用本项目测量到的 filtered_speed_rpm
  3. 计算PID输出 输出 = Kp * 误差 + Ki * 误差积分 + Kd * 误差微分 。其中误差 = 目标转速 - 反馈转速。
  4. 驱动执行器 :将PID输出值映射到PWM占空比,控制电机驱动芯片(如L298N、TB6612)来调整电机电压,从而改变转速。
  5. 循环 :以固定的周期(例如每10ms)执行步骤2-4。

通过这个完整的“传感器-控制器-执行器”链条,你就实现了一个真正的电机调速系统。而这一切的起点,就是稳定可靠地读取旋转编码器这个小小的部件发出的脉冲信号。从最初被中断淹没的困境,到后来驾驭硬件定时器游刃有余,这个过程让我深刻体会到,在嵌入式开发中,理解硬件、善用硬件,往往比写出复杂的软件算法更能高效、优雅地解决问题。

打开链接下载源码: https://pan.quark.cn/s/05da658a2377 在信息技术领域中,输入法作为操作系统的一个核心构成部分,赋予了用户利用键盘输入多语种文字的能力。"ime-日语输入法安装必须文件"这一资源是一套为日语输入法部署而设计、包含全部必要元素的集成包,对于那些需要在个人计算机上执行日语文字输入的操作者而言具有不可替代的作用。接下来将深入剖析其中所包含的核心概念。 IME(Input Method Editor,输入法编辑器)是操作系统内的一种软件支持服务,其功能在于为非拉丁字符环境提供文字输入方案,例如中文、日文、韩文等文字系统。在日本地区,IME通常被用来将罗马字(罗马拼音)形式的输入转换为平假名、片假名乃至汉字。此压缩文件内含的日语IME文件夹即为执行这一转换功能的关键要素。 kbdjpn.dll被视为一个关键的系统性文件,其意指“Japanese Keyboard Layout”(日语键盘布局)。该动态链接库文件负责设定日语键盘的排列方式及快捷操作组合,使用户能够借助常规的QWERTY键盘输入日语文字。倘若缺少这一文件,即便已经安装了日语输入法,依然无法正常显示及输入日语字符。 另外,imjp81k.dll同样是一个重要的系统性构成,它属于日语IME的范畴,全称为“Input Method Japanese for Windows 8.1 and later, Katakana mode”(适用于Windows 8.1及更新版本的日语输入法,片假名模式)。该文件支持日语的片假名输入,是处理日语输入的核心组成部分。在安装或升级日语输入法的过程中,保证imjp81k.dll的准确性完整性显得尤为关键。 压缩包所含的"Window...
内容概要:本文研究了基于DPWMA调制正负序分离的ANPC三电平并网逆变器前馈控制策略,旨在解决传统三电平逆变器在谐波抑制、电网不平衡适应性及动态响应方面的技术瓶颈。通过构建融合双极性倍频脉宽调制(DPWMA)、正负序分离锁相控制电网电压前馈的一体化控制体系,全面优化逆变器的输出波形质量、相位同步精度抗扰能力。文章深入分析了ANPC三电平拓扑的结构优势,如开关损耗均衡、中点电位可控性强和电压利用率高等特点,并设计了包含信号采集、核心控制调制驱动三层架构的完整控制系统。通过Simulink仿真平台对稳态运行、电网不平衡及动态扰动等多种工况进行验证,结果表明该策略显著降低了总谐波畸变率,提升了锁相精度系统动态稳定性,有效增强了逆变器在复杂电网环境下的适应能力和运行可靠性。; 适合人群:具备电力电子、自动控制及新能源并网相关基础知识,从事新能源发电、微电网、电力系统仿真等领域的科研人员工程技术人员,特别适合研究生及以上层次的研究者。; 使用场景及目标:①用于提升大功率并网逆变器在电网电压不平衡、谐波干扰和动态扰动等复杂工况下的运行性能;②为高电能质量要求的应用场景提供先进控制解决方案;③支持科研仿真、论文复现实际工程项目中的高性能并网控制系统设计优化。; 阅读建议:建议结合提供的Simulink仿真模型进行实践操作,重点理解DPWMA调制机制、正负序分离锁相算法电网电压前馈控制之间的协同作用,按照文档结构系统学习,并传统控制策略进行对比分析,以深入掌握改进策略的技术优势实现细节。
代码下载链接: https://pan.quark.cn/s/d9794888cbc0 ### G代码经典解释程序知识点详解 #### 一、引言 随着数控技术的持续进步,尤其是开放式数控系统的广泛应用,软件层面的设计在数控领域占据了核心地位。G代码作为数控机床编程的基础语言,在自动化生产流程中发挥着不可或缺的作用。本文的核心内容是关于一个基于Linux平台、采用C语言开发的G代码解释程序的设计思路及其具体实现。 #### 二、G代码解释器概述 **1. 设计背景** - 当前数控技术发展的主要方向是开放式数控系统,这类系统具备出色的可扩展能力、良好的移植性、高度的互换性以及优异的互操作性等优势。 - 计算机硬件技术的快速发展使得在PC平台上构建数控系统成为可能,进而推动了全软件式数控系统的普及。 **2. G代码解释器的重要性** - G代码解释器在全软件式数控系统中是至关重要的组成部分,其主要职责是将G代码转化为数控系统能够识别的数据格式。 - 为了提升数控系统的开放程度,G代码解释器的设计必须兼顾开放性和灵活性。 #### 三、G代码解释器设计实现 **1. 总体结构设计** - G代码解释器主要由两个核心部分构成:G代码关键字函数表(GKFT)和G代码分组(GG)。 - GKFT用于解析G代码中的关键字,它是解释器的核心骨架;而GG则是语法检查的基础框架。 **2. G代码关键字函数表(GKFT)** - GKFT是一种专门用于存储G代码关键字及其关联处理函数的数据结构。 - 解释器通过查询GKFT,能够根据特定的G代码关键字调用相应的处理函数,从而完成对G代码的有效解析。 - 此种设计方法不仅简化了解释器的构建过程,同时也增强了其可扩展性,因为新增功能...
已经博主授权,源码转载自 https://pan.quark.cn/s/458849d2eac8 Microblaze代表由Xilinx公司研发的一款软核处理器,其核心特性在于使用户能够针对FPGA(Field Programmable Gate Array)平台进行嵌入式系统的个性化构建。此“Xinlin中Microblaze的培训教程”致力于辅助学习人员深入理解和熟练掌握Microblaze在Xilinx开发环境中的实际应用。 一、Microblaze基础 Microblaze作为一款可配置的32位RISC处理器,具备高度适应性,允许在设计中根据具体需求对性能、功耗及面积进行灵活调整。Microblaze支持多种指令集架构(ISA),涵盖Xtensa-like和Classic两种模式,并且包括UART、SPI、I2C在内的多种外设接口标准保持兼容。 二、Xilinx ISEVivado工具 Xilinx ISE(Integrated Software Environment)是一个用于FPGA系统设计、实现和调试的集成开发平台,而Vivado则是一款功能更为先进且全面的工具套件。在本次教程中,学员将学会如何在上述工具中配置和执行Microblaze处理器,以及如何开发相关的硬件描述语言(HDL)代码。 三、Microblaze硬件设计 在Xinlin提供的教程里,学员将学习如何在Xilinx FPGA中部署Microblaze处理器。这涉及到选择合适的处理器配置参数,如时钟频率、缓存容量和外设接口设置。此外,学员还将接触到创建和连接内存模块、中断控制器以及其他必需硬件组件的方法。 四、软件开发 Microblaze的软件开发通常涉及嵌入式编程,采用C或C++语言来...
内容概要:本文围绕并网离网模式下的风光互补制氢合成氨系统,开展容量配置运行调度的联合优化分析,并提供了完整的Python代码实现。研究构建了综合考虑风能、太阳能发电特性、电解水制氢、合成氨工艺及储能环节的系统模型,重点解决了在不同运行模式(并网/离网)下,如何通过优化算法确定各单元的最佳容量配置,并在此基础上实现系统经济高效的运行调度。文中详细阐述了数学模型的建立过程,包括以最小化综合成本为目标的目标函数,以及涵盖功率平衡、设备容量、物料守恒等多方面的约束条件体系,并利用Python编程语言调用专业优化求解器进行仿真求解,最终获得系统的最优容量配置方案精细化的调度策略。; 适合人群:具备一定Python编程基础和优化理论知识,从事新能源系统规划、综合能源系统、氢能或化工过程优化等相关领域的研究生、科研人员及工程技术人员。; 使用场景及目标:①学习如何对复杂的“电-氢-氨”多能转换存储系统进行一体化建模仿真;②掌握使用Python实现能源系统容量优化运行调度联合求解的具体方法技术路线;③为相关领域的科研项目、学位论文撰写或实际工程设计提供可复现的代码参考和系统性的解决方案借鉴。; 阅读建议:在阅读时应重点关注模型构建的逻辑框架严谨的数学表达,并结合所提供的Python代码逐行理解其具体实现方式,建议读者务必自行复现代码以加深对优化算法求解过程和系统运行机制的理解,同时可尝试修改模型参数或拓展系统结构以适应不同的研究需求和应用场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值