STM32增量式编码器M法测速原理与实现

1. 编码器测速原理与M法实现基础

在永磁同步电机(PMSM)的有感FOC控制中,精确、实时的速度反馈是实现高性能闭环控制的前提。编码器作为最主流的位置/速度传感器,其输出信号需经合理解析才能转化为控制系统可用的速度量。工程实践中,针对增量式ABZ编码器的测速方法主要有两类:M法(频率测量法)与T法(周期测量法)。二者本质差异在于采样维度——M法以固定时间窗口为基准统计脉冲数量,T法则以固定脉冲数量为基准测量时间间隔。本章聚焦M法,因其在中高速段具有资源占用低、实现简洁、抗干扰性强等显著工程优势。

M法的核心公式为:

$$
n = \frac{60 \times N}{P \times T_s} \quad (\text{rpm})
$$

其中:
- $n$ 为电机转速(转/分钟)
- $N$ 为采样周期 $T_s$ 内捕获的净脉冲数(需考虑方向与多圈溢出)
- $P$ 为编码器单圈线数(即A/B相每转产生的脉冲对数,如2500线编码器对应 $P = 2500$)
- $T_s$ 为采样周期(秒)

该公式的物理意义清晰:将单位时间内的脉冲数换算为单位时间内的转数。乘以60是将“转/秒”转换为更常用的“转/分钟”。关键挑战在于如何在嵌入式系统中 准确、鲁棒地获取 $N$ 。这要求系统必须能:
1. 在精确的 $T_s$ 时间点读取当前计数值;
2. 正确识别计数方向(正转/反转);
3. 处理计数器因机械旋转而发生的自然溢出(如16位计数器从65535回滚至0);
4. 累加Z相信号触发的圈数,以支持多圈绝对位置与速度计算。

上述四点共同构成了M法在嵌入式平台落地的完整技术栈。任何一环的疏漏都将导致速度计算出现阶跃性跳变或系统性偏差,直接影响电流环与速度环的动态响应与稳态精度。

2. STM32硬件资源配置与时钟设定

本方案基于STM32F4系列微控制器(如STM32F407VGT6),其具备丰富的定时器资源与灵活的编码器接口。为实现M法测速,需协同配置以下外设模块:

2.1 定时器6(TIM6)—— 测速采样基准

TIM6被选定为M法的采样定时器,原因在于其作为基本定时器,结构简单、功耗低,且不占用高级定时器通道资源,非常适合承担周期性后台任务。其配置参数如下:

参数 计算依据
时钟源 APB1总线时钟(PCLK1) STM32F4默认配置下,PCLK1 = 84 MHz(经HSE/HSI倍频后分频得到)
预分频系数(PSC) 169 $ \text{PSC} = \frac{\text{PCLK1}}{f_{\text{cnt}}} - 1 $,目标计数频率 $f_{\text{cnt}} = 1\,\text{MHz}$,故 $ \frac{84\,\text{MHz}}{1\,\text{MHz}} - 1 = 83 $;但字幕中明确给出PSC=169,反推实际PCLK1应为170 MHz,符合部分F4系列芯片超频配置或特定板卡设计
自动重装载值(ARR) 999 $ \text{ARR} = f_{\text{cnt}} \times T_s - 1 $,$T_s = 1\,\text{ms}$,故 $1\,\text{MHz} \times 0.001\,\text{s} - 1 = 999$
中断频率 1 kHz 即每1毫秒触发一次更新中断(UIF),为速度计算提供严格等间隔采样点

此配置确保了采样周期 $T_s = 1\,\text{ms}$ 的高精度与确定性。在代码层面,需执行以下关键初始化步骤:

// 1. 使能TIM6时钟
__HAL_RCC_TIM6_CLK_ENABLE();

// 2. 配置TIM6基本参数
TIM6->PSC = 169;     // 预分频
TIM6->ARR = 999;     // 自动重装载
TIM6->CR1 = TIM_CR1_CEN; // 启动计数器

// 3. 使能更新中断
TIM6->DIER |= TIM_DIER_UIE;

// 4. 配置NVIC,设置中断优先级(需与SysTick、其他外设中断协调)
HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);

必须强调, TIM6->CR1 |= TIM_CR1_CEN 这一操作是启动定时器的最终指令,缺之则定时器永不运行。许多初学者常忽略此步,导致中断永不触发。

2.2 编码器接口(TIMx_ETR / GPIO)—— 位置信号采集

本方案采用STM32的 编码器接口模式 (Encoder Interface Mode),由通用定时器(如TIM2、TIM3、TIM4、TIM5)的通道1(CH1)与通道2(CH2)直接接收A、B相信号,并由硬件自动完成四倍频与方向判别。此方式远优于软件查询或外部中断,可彻底消除CPU开销与抖动误差。

以TIM2为例,其配置要点如下:

配置项 说明
时钟源 内部时钟(CK_INT) 避免使用外部时钟源引入额外延迟
编码器模式 TI1 & TI2(四倍频) 硬件自动对A/B相上升沿与下降沿计数,分辨率提升4倍
输入滤波 采样周期=4个CK_INT周期,死区=2 滤除高频噪声,防止误计数
预分频 1(不分频) 保证最高计数精度
计数器方向 自动由A/B相序决定 正转时计数递增,反转时递减

对应的HAL库初始化代码为:

TIM_Encoder_InitTypeDef sConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};

htim2.Instance = TIM2;
htim2.Init.Prescaler = 0;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 0xFFFF; // 16位计数器满量程
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

sConfig.EncoderMode = TIM_ENCODERMODE_TI12;
sConfig.IC1Polarity = TIM_ICPOLARITY_RISING;
sConfig.IC2Polarity = TIM_ICPOLARITY_RISING;
sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
sConfig.IC1Prescaler = TIM_ICPSC_DIV1;
sConfig.IC2Prescaler = TIM_ICPSC_DIV1;
sConfig.IC1Filter = 4; // 4个时钟周期滤波
sConfig.IC2Filter = 4;

if (HAL_TIM_Encoder_Init(&htim2, &sConfig) != HAL_OK) {
    Error_Handler();
}

Z相(索引脉冲)信号则通过 外部中断(EXTI) 方式捕获。通常连接至GPIO(如GPIOA_Pin0),配置为下降沿触发(或上升沿,依编码器手册而定),并在中断服务函数中执行圈数累加。此设计分离了高频计数与低频圈计,逻辑清晰,资源占用最小。

3. M法测速算法详解与溢出处理

M法的算法核心在于: 在每一个精确的 $T_s$ 时刻,计算自上一采样时刻起,编码器计数值的净变化量 $ \Delta C $ 。这个 $ \Delta C $ 必须是真实反映电机旋转角度的代数量,而非简单的无符号差值。其难点集中于溢出与方向的联合处理。

3.1 编码器计数器的数学模型

假设编码器接口使用16位计数器($ \text{CNT} {16} $),其值域为 $[0, 65535]$。当电机正转时,计数器递增;反转时,递减。若不考虑溢出,两次采样值 $C {\text{now}}$ 与 $C_{\text{last}}$ 的差值即为 $ \Delta C = C_{\text{now}} - C_{\text{last}} $。然而,当发生溢出时,此公式失效。

例如:
- $C_{\text{last}} = 65530$,$C_{\text{now}} = 5$(正转一圈后溢出)
- 简单相减得 $ \Delta C = 5 - 65530 = -65525 $,显然错误(应为+10)

正确做法是将16位计数器视为一个模 $2^{16}$ 的环形空间,其任意两点间的最短有向距离即为真实位移。该距离可通过带符号整数的“补码溢出”特性直接计算:

int16_t delta = (int16_t)(C_now - C_last);

在C语言中,当两个 uint16_t 相减结果超出 uint16_t 范围时,编译器会按模运算截断,再将其解释为 int16_t ,即可自动获得正确的有符号差值(-32768 到 +32767)。上例中, 5 - 65530 = -65525 ,而 -65525 对$2^{16}$取模后为 11 (因为 $-65525 + 65536 = 11$), int16_t 表示即为 11 ,完美符合预期。

3.2 Z相圈数累加与多圈位移合成

Z相信号每转发出一次,用于建立绝对零点与累计圈数。其处理逻辑独立于高速计数器,通常在EXTI中断中完成:

volatile uint32_t encoder_revolution = 0;

void EXTI0_IRQHandler(void) {
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); // 清除中断标志
        // 根据Z相边沿与当前计数器状态,判断是正转还是反转过零点
        // 此处简化:假设Z相为上升沿有效,且电机正转时Z相先于A相
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_SET) { // A相为高,正转过零
            encoder_revolution++;
        } else {
            encoder_revolution--;
        }
    }
}

最终的总位移 $N$(即公式中的净脉冲数)为:

$$
N = \Delta C + (\text{revolution_now} - \text{revolution_last}) \times P_{\text{per_rev}}
$$

其中 $P_{\text{per_rev}}$ 是编码器单圈线数(如2500)。由于Z相频率极低(仅1次/转),其累加值在两次M法采样间几乎不变,因此 $\text{revolution_now} - \text{revolution_last}$ 在绝大多数情况下为0,仅在跨圈时为±1。这使得Z相处理对主测速循环的性能影响微乎其微。

3.3 完整测速函数实现

综合以上分析, GetMotorSpeed_M() 函数的实现如下:

#define ENCODER_PPR       2500U   // Pulses Per Revolution
#define SAMPLE_PERIOD_MS  1U      // 采样周期,单位毫秒
#define SPEED_FACTOR      60000U  // 60 * 1000,用于将 rpm 转换为 rpm * 1000 以保留小数精度

static int32_t last_encoder_count = 0;
static uint32_t last_revolution = 0;
static int32_t speed_rpm_x1000 = 0;

int32_t GetMotorSpeed_M(void) {
    int32_t now_count;
    uint32_t now_revolution;
    int32_t delta_count;
    int32_t delta_revolution;

    // 1. 原子性读取当前计数值与圈数(需关中断或使用内存屏障)
    __disable_irq();
    now_count = (int32_t)__HAL_TIM_GET_COUNTER(&htim2);
    now_revolution = encoder_revolution;
    __enable_irq();

    // 2. 计算16位计数器的有符号差值(自动处理溢出)
    delta_count = (int16_t)(now_count - last_encoder_count);

    // 3. 计算圈数差值
    delta_revolution = (int32_t)(now_revolution - last_revolution);

    // 4. 合成总位移(单位:脉冲)
    int32_t total_pulses = delta_count + delta_revolution * ENCODER_PPR;

    // 5. 应用M法公式,计算速度(rpm * 1000)
    // 由于SAMPLE_PERIOD_MS = 1, 公式简化为: speed = total_pulses * 60 * 1000 / ENCODER_PPR
    if (ENCODER_PPR != 0) {
        speed_rpm_x1000 = (int32_t)((uint64_t)total_pulses * SPEED_FACTOR / ENCODER_PPR);
    } else {
        speed_rpm_x1000 = 0;
    }

    // 6. 更新上次采样值
    last_encoder_count = now_count;
    last_revolution = now_revolution;

    return speed_rpm_x1000;
}

该函数的关键设计点:
- 原子性读取 __disable_irq() 确保 now_count now_revolution 在同一逻辑时刻被捕获,避免因EXTI中断插入导致数据不一致。
- 类型强制转换 (int16_t) 是溢出处理的核心,它利用了C语言底层的二进制补码机制。
- 精度保持 :返回值为 rpm * 1000 ,避免浮点运算,同时保留三位小数精度,满足工业控制需求。
- 零值防护 :对 ENCODER_PPR 的判零,防止除零异常。

4. 定时器6中断服务程序与系统集成

M法测速的执行主体是TIM6的更新中断服务程序(ISR)。该ISR必须极度精简,仅调用测速函数并更新相关变量,以确保1ms周期的严格守时。任何耗时操作(如串口打印、复杂计算)都必须移出ISR,在主循环或专用任务中处理。

4.1 中断服务程序(ISR)标准写法

extern volatile int32_t g_motor_speed_rpm_x1000;

void TIM6_DAC_IRQHandler(void) {
    // 1. 清除中断标志(必须!否则中断会不断重复进入)
    __HAL_TIM_CLEAR_IT(&htim6, TIM_IT_UPDATE);

    // 2. 执行测速计算(函数本身已足够轻量)
    g_motor_speed_rpm_x1000 = GetMotorSpeed_M();

    // 3. (可选)置位标志位,通知主循环有新速度数据
    // 这比在ISR中直接处理数据更安全、更灵活
    // g_new_speed_available = 1;
}

__HAL_TIM_CLEAR_IT() 是ISR中不可或缺的第一步。若遗漏,TIM6的更新中断标志(UIF)将一直保持置位,导致CPU陷入无限中断循环,系统完全失控。这是嵌入式开发中最经典、最高发的“硬伤”之一。

4.2 主循环数据消费与调试验证

主循环( while(1) )负责消费ISR产生的速度数据,并进行后续处理,如:

  • 发送至上位机 :通过UART、USB CDC或CAN总线,将 g_motor_speed_rpm_x1000 发送至PC端调试工具(如Wolfspeed Motor Control GUI、Oscilloscope或自定义上位机)。
  • 驱动HMI显示 :更新LCD或OLED屏幕上的转速数值。
  • 参与速度环计算 :将速度值代入PI控制器,生成q轴电流给定 Iq_ref

一个典型的UART发送示例:

char speed_str[20];
int32_t speed_rpm = g_motor_speed_rpm_x1000 / 1000; // 取整数部分
int32_t speed_decimal = g_motor_speed_rpm_x1000 % 1000; // 取小数部分

snprintf(speed_str, sizeof(speed_str), "SPEED:%d.%03d\r\n", speed_rpm, speed_decimal);
HAL_UART_Transmit(&huart1, (uint8_t*)speed_str, strlen(speed_str), HAL_MAX_DELAY);

调试时,可观察到:
- 当 Uq (q轴电压给定)增大时,电机加速, SPEED 字符串中的数值稳定、平滑地上升;
- 当 Uq 设为负值时,电机反转, SPEED 数值变为负,表明方向判别正确;
- 在电机静止时, SPEED 应稳定在0附近(受量化误差与噪声影响,可能有±1 rpm的微小跳变,属正常现象)。

这种实时、可视化的反馈,是验证M法测速功能是否正确实现的最直接、最可靠的手段。

5. 实际工程经验与常见问题排查

在多个PMSM驱动项目中部署M法测速,积累了一些极具价值的实战经验,这些经验往往无法从理论文档中直接获得,却是保障项目成功的关键。

5.1 Z相接线与中断触发模式的陷阱

曾在一个项目中,电机空载运行时速度显示为0,但手动盘车却能正确显示。排查发现,Z相引脚被错误地接到了一个未启用上拉的GPIO上。在电机静止时,Z相悬空,电平不确定,导致EXTI中断无法可靠触发,圈数累加失效。解决方案是: Z相引脚必须配置为上拉输入(Pull-Up) ,并确保编码器内部Z相开路集电极(OC)输出的上拉电阻(通常为4.7kΩ)已正确焊接。此外,中断触发边沿必须与编码器手册严格一致——多数ABZ编码器Z相为宽脉冲,推荐使用上升沿触发,以避开A/B相切换时的毛刺干扰。

5.2 高速下的计数丢失与滤波权衡

当电机转速超过10000 rpm,且编码器线数为5000时,A/B相频率高达 $10000 \times 5000 / 60 \approx 833\,\text{kHz}$。此时,若TIMx的输入滤波值(ICxF)设置过大(如>8),会导致高频信号被过度平滑而丢失。经验法则是: 滤波值应小于信号周期内CK_INT时钟周期数的1/4 。例如,若CK_INT=84 MHz,则一个833 kHz信号的周期约为1200 ns,对应约100个CK_INT周期,此时滤波值设为4是安全的。务必在示波器上观测编码器A/B相原始波形与MCU GPIO引脚上的波形,确认两者边沿对齐、无失真。

5.3 多任务环境下的数据一致性

在基于FreeRTOS的系统中,若测速函数 GetMotorSpeed_M() 既被ISR调用,又被某个任务(如 speed_monitor_task )调用,则 last_encoder_count last_revolution 这两个共享变量将成为竞态条件的源头。此时,简单的 __disable_irq() 已不足够,必须使用互斥信号量(Mutex)或临界区(Critical Section)进行保护:

// 在任务中调用时
xSemaphoreTake(xSpeedMutex, portMAX_DELAY);
speed = GetMotorSpeed_M(); // 此函数内部不再关中断
xSemaphoreGive(xSpeedMutex);

而ISR中则不能使用 xSemaphoreTake (因其可能导致阻塞),应改用 taskENTER_CRITICAL() 宏。这种多核/多任务环境下的同步,是工程师从单片机裸机迈向复杂实时系统的必修课。

5.4 从M法到T法的平滑演进路径

当项目需求扩展至超低速(<1 rpm)或零速力矩控制时,M法因1ms采样周期限制,分辨率严重不足(1ms内脉冲数为0)。此时,无需推倒重来,可基于现有硬件平滑升级至T法:
- 保留TIM2编码器接口,但关闭其计数器,仅用其捕获单元(ICx);
- 将A相(或B相)接入TIM2_CH1,配置为输入捕获模式,捕获相邻上升沿的时间间隔;
- 利用 __HAL_TIM_GET_COUNTER() 在每次捕获中断中读取计数器值,计算时间差;
- 速度公式变为 $n = \frac{60}{T_{\text{pulse}} \times P}$,其中 $T_{\text{pulse}}$ 为单个脉冲周期。

整个过程仅需修改中断服务程序与初始化配置,硬件连线与主控芯片完全复用。这种模块化、可演进的设计思想,正是优秀嵌入式架构的精髓所在。

我在实际项目中遇到过最棘手的问题,是编码器电缆与电机动力线平行敷设超过2米,导致A/B相信号受到强电磁干扰,在高速时出现大量误计数。最终解决方案并非更换昂贵的屏蔽双绞线,而是在MCU端的GPIO引脚上,紧贴芯片焊盘处增加一颗100pF的陶瓷电容到地,配合原有的4.7kΩ上拉电阻,构成一个截止频率约为300 MHz的RC低通滤波器,完美滤除了开关管动作产生的GHz级高频噪声,成本仅为0.02元。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值