STM32基于M法的编码器测速实现与优化

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

在永磁同步电机(PMSM)的磁场定向控制(FOC)系统中,准确、实时的速度反馈是实现高性能闭环控制的前提。对于采用ABZ三相增量式编码器作为位置传感器的有感FOC方案,速度并非直接测量得到,而是通过对编码器输出的A、B两相信号进行计数,并结合时间基准进行推算。工程实践中,主流的数字测速方法主要有两种:M法(频率法)和T法(周期法)。二者在原理、适用场景及资源开销上存在本质差异,必须根据具体电机运行工况与MCU资源约束进行权衡选择。

M法的核心思想是在一个固定的采样时间窗口内,统计编码器输出的脉冲总数,再通过单位时间内的脉冲数换算为机械转速。其数学表达式为:

$$
n = \frac{60 \times N}{P \times T_s}
$$

其中,$n$ 为电机转速(rpm),$N$ 为采样周期 $T_s$(秒)内累计的编码器脉冲数,$P$ 为电机每转对应的编码器线数(即一圈产生的A/B相脉冲总数)。对于常见的2500线编码器,若采用四倍频计数(即对A/B相的上升沿与下降沿均计数),则 $P = 2500 \times 4 = 10000$。该公式隐含了一个关键前提:编码器脉冲计数必须反映真实的机械位移增量,且需正确处理计数器溢出与方向反转问题。

T法则与此相反,它固定测量对象——即捕获两个相邻脉冲沿之间的时间间隔 $\Delta t$,再通过 $n = \frac{60}{P \times \Delta t}$ 计算转速。此方法在极低速(如 $\Delta t$ 达到毫秒甚至秒级)时精度极高,因为时间测量分辨率通常远高于脉冲计数分辨率。然而,当电机高速旋转时,$\Delta t$ 急剧减小,要求定时器具备极高的计数频率与中断响应能力。以10000线编码器、3000 rpm为例,平均脉冲间隔仅为 $20\,\mu s$,这意味着MCU需在20微秒内完成一次中断进入、时间捕获、中断退出的全过程,对CPU负载与中断延迟构成严峻挑战。在多任务、带通信协议栈或复杂控制算法的嵌入式系统中,高频中断极易引发任务调度失序、通信超时等系统性风险。

因此,在绝大多数中高速运行的工业驱动场景下,M法因其计算简单、资源占用低、抗干扰能力强而成为首选。本章所实现的正是基于M法的编码器测速模块,其设计目标明确:在保证10 ms以内响应延迟的前提下,提供稳定、连续、方向正确的转速值,为后续的速度环PI调节器提供可靠输入。

2. 硬件平台与定时器资源配置

本项目基于STM32F407VGT6微控制器构建FOC驱动平台。该芯片搭载ARM Cortex-M4内核,主频最高可达168 MHz,片上集成多个高级定时器(TIM1/TIM8)与通用定时器(TIM2-TIM5),以及一个基本定时器(TIM6/TIM7)。TIM6与TIM7为16位自动重装载基本定时器,无输入捕获与输出比较功能,但具备独立的时钟源与中断能力,是实现精确周期性任务调度的理想选择。

2.1 定时器6的时钟树配置分析

STM32F4系列的定时器时钟源自APB1总线(最大频率42 MHz)。TIM6挂载于APB1总线上,其时钟源为APB1 Timer Clock。在本工程的RCC初始化中,系统主频配置为168 MHz,APB1总线预分频系数为2,因此APB1总线频率为84 MHz。值得注意的是, TIM6的时钟频率并非直接等于APB1频率,而是其两倍 。这是由STM32的时钟树设计决定的:当APB1预分频系数不为1时,TIMx(x=2..7)的时钟频率为APB1时钟频率的2倍。因此,TIM6的实际输入时钟频率为 $84\,\text{MHz} \times 2 = 168\,\text{MHz}$。

这一细节至关重要。若忽略该倍频关系,将导致定时器溢出时间计算严重错误。例如,若误认为TIM6时钟为84 MHz,并据此设置预分频器与重装载值,则实际中断周期将是理论值的两倍,最终导致测速结果偏低50%。

2.2 定时器6中断周期的精确计算

为实现1 ms的精确采样周期,需对TIM6进行如下配置:
- 预分频器(PSC) :设为169。预分频器的作用是对输入时钟进行分频,其值为实际分频系数减1。因此,分频后频率为 $168\,\text{MHz} / (169 + 1) = 168\,\text{MHz} / 170 = 1\,\text{MHz}$。
- 自动重装载寄存器(ARR) :设为999。该寄存器定义了计数器从0开始计数至该值后产生更新事件(UEV)并清零。因此,一个完整计数周期包含1000个计数脉冲(0至999),对应时间为 $1000 / 1\,\text{MHz} = 1\,\text{ms}$。

综上,TIM6的中断触发频率为1 kHz,即每1 ms执行一次中断服务函数(ISR)。该ISR是整个M法测速逻辑的“心跳”,所有与速度计算相关的数据采集、处理与更新操作均在此上下文中完成。在 main() 函数的硬件初始化阶段,必须显式调用 HAL_TIM_Base_Start_IT(&htim6) 启动TIM6的中断模式,并确保在 stm32f4xx_it.c 中已正确实现 TIM6_DAC_IRQHandler 中断服务函数,且其中调用了 HAL_TIM_IRQHandler(&htim6) 以完成中断标志清除与回调函数调用。

3. 编码器接口与Z相脉冲处理机制

本系统采用ABZ三相增量式编码器,其A、B两相正交输出用于判断旋转方向与进行四倍频计数,Z相(又称索引脉冲或零位脉冲)则在电机每旋转一圈时产生一个固定宽度的脉冲,用于提供绝对位置参考与圈数累计。

3.1 编码器硬件连接与GPIO配置

编码器A、B、Z三相信号分别接入MCU的GPIO引脚,并配置为浮空输入模式( GPIO_MODE_INPUT ),以避免因外部上拉/下拉电阻不匹配导致的电平误判。在STM32 HAL库中,此配置通过 MX_GPIO_Init() 函数完成,例如:

GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

A、B相的正交信号被送入TIM2的编码器接口( TIM_EncoderInterfaceConfig ),由硬件自动完成四倍频计数与方向识别,极大降低了CPU负担。TIM2的计数器(CNT)寄存器即为当前编码器位置的直接映射。Z相脉冲则单独接入一个GPIO引脚(如PA3),并配置为外部中断(EXTI)模式,上升沿触发。

3.2 Z相中断服务函数与圈数累计

Z相中断服务函数( EXTI3_IRQHandler )是实现多圈计数的关键。其核心逻辑极为简洁:在每次检测到Z相上升沿时,对一个全局静态变量 encoder_revolution_count 进行原子性加一操作。由于Z相脉冲宽度通常远大于MCU指令执行时间,且其频率极低(最高仅等于电机转速),因此无需复杂的去抖动处理,直接在中断中累加即可。

// 全局变量,声明于编码器.c文件顶部
static volatile uint32_t encoder_revolution_count = 0;

// Z相中断服务函数
void EXTI3_IRQHandler(void)
{
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_3) != RESET)
    {
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_3); // 清除中断标志
        encoder_revolution_count++;             // 原子性累加圈数
    }
}

该变量与TIM2的16位计数器值共同构成了完整的32位位置信息。 encoder_revolution_count 记录整圈数,TIM2->CNT记录当前圈内的偏移量。这种分离式设计使得位置数据具有天然的“无限”范围,避免了单纯依赖16位计数器带来的溢出困扰。

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

M法测速的核心在于精确计算两次采样时刻之间编码器位置的 净增量 。由于编码器计数器为有限位宽(本例为16位,范围0–65535),且电机可正反转,因此简单的“本次计数值减上次计数值”无法直接反映真实位移。必须结合方向信息与Z相触发次数,进行跨圈与反向的数学修正。

4.1 位置读取与状态快照

在TIM6的1 ms中断服务函数中,首先需要获取一个“原子性”的位置快照。这包括:
- 读取TIM2的当前计数值 current_cnt
- 读取当前方向标志 current_dir (由TIM2编码器接口硬件自动更新);
- 读取当前Z相触发累计值 current_rev

为保证这三个值在逻辑上的一致性,应尽可能缩短读取时间间隔。由于 current_cnt current_dir 均为寄存器值,读取速度极快; current_rev 为内存变量,其读取也属原子操作(32位变量在Cortex-M4上为单条指令)。因此,可认为这三者构成一个瞬时、一致的状态快照。

4.2 净增量计算:正转与反转的统一模型

假设上一次采样时刻的状态为 (last_cnt, last_dir, last_rev) ,本次为 (current_cnt, current_dir, current_rev) 。净增量 $\Delta N$ 的计算需分情况讨论:

正转情形( current_dir == 1

电机顺时针旋转,计数器值单调递增(忽略溢出)。此时,若未发生Z相触发( current_rev == last_rev ),则 $\Delta N = current_cnt - last_cnt$。若发生了一次Z相触发( current_rev == last_rev + 1 ),说明计数器已从65535溢出归零,真实增量为 (current_cnt + 65536) - last_cnt 。若发生多次Z相触发,则为 (current_cnt + 65536 * (current_rev - last_rev)) - last_cnt

反转情形( current_dir == 0

电机逆时针旋转,计数器值单调递减。此时,若未发生Z相触发,$\Delta N = current_cnt - last_cnt$ 的结果为负数,其绝对值即为实际减少的脉冲数。但若发生Z相触发,意味着计数器已从0向下溢出至65535,真实增量为 current_cnt - (last_cnt + 65536 * (current_rev - last_rev)) ,结果仍为负数。

为统一处理正反向,可将上述逻辑抽象为一个通用公式:
$$
\Delta N = (current_cnt - last_cnt) + 65536 \times (current_rev - last_rev) \times \text{sign}(current_dir)
$$
其中,$\text{sign}(current_dir)$ 在正转时为+1,反转时为-1。但更简洁、无分支的实现方式是利用补码特性:

int32_t delta_cnt = (int32_t)current_cnt - (int32_t)last_cnt;
uint32_t rev_delta = current_rev - last_rev;

if (current_dir == 1) // 正转
{
    delta_cnt += (int32_t)(rev_delta * 65536U);
}
else // 反转
{
    delta_cnt -= (int32_t)(rev_delta * 65536U);
}

此代码段清晰地表达了物理含义:正转时,每多一圈,就在差值上加一整圈的脉冲数;反转时,则减去。

4.3 速度计算与单位转换

获得净增量 $\Delta N$ 后,即可代入M法公式计算转速:
$$
n = \frac{60 \times \Delta N}{P \times T_s}
$$
其中,$T_s = 0.001\,\text{s}$(1 ms),$P$ 为每转总脉冲数(本例中,2500线编码器四倍频后 $P = 10000$)。为提高计算效率并避免浮点运算,可将公式预计算为整数比例:
$$
n = \Delta N \times \frac{60}{10000 \times 0.001} = \Delta N \times 6
$$
即,每1 ms内计数增加1,对应转速为6 rpm。这是一个极其重要的简化,它意味着速度计算可完全规避除法与浮点运算,仅需一次整数乘法,对实时性要求苛刻的FOC系统而言,这是巨大的性能优势。

最终的速度值 speed_rpm 为带符号整数,其符号直接继承自 delta_cnt 的符号,完美反映了电机的旋转方向。

5. 软件架构与代码实现

5.1 模块化设计与头文件声明

遵循嵌入式软件工程最佳实践,测速功能被封装为独立的 encoder_speed.c/h 模块。头文件 encoder_speed.h 对外暴露最小必要接口:

#ifndef ENCODER_SPEED_H
#define ENCODER_SPEED_H

#include "stdint.h"

// 初始化函数,应在main()中调用
void Encoder_Speed_Init(void);

// 获取当前计算出的速度(rpm),线程安全
int32_t Encoder_Speed_Get(void);

#endif /* ENCODER_SPEED_H */

5.2 核心变量与临界区保护

模块内部维护一组静态变量,用于存储历史状态与当前结果:

// encoder_speed.c
static volatile int32_t speed_rpm = 0;           // 当前计算出的速度(rpm)
static volatile uint16_t last_cnt = 0;          // 上次采样的计数器值
static volatile uint32_t last_rev = 0;          // 上次采样的圈数
static volatile uint8_t last_dir = 0;            // 上次采样的方向

// 供TIM6 ISR调用的内部更新函数
void Encoder_Speed_Update(void);

由于 speed_rpm last_cnt last_rev last_dir 均可能被中断服务函数(写)与主循环(读)同时访问,必须确保其读写操作的原子性。对于32位的 speed_rpm last_rev ,在Cortex-M4上单次读写为原子操作;对于16位的 last_cnt 与8位的 last_dir ,同样为原子操作。因此,此处无需引入 __disable_irq() 等重量级临界区保护,既保证了数据一致性,又最大限度减少了中断延迟。

5.3 TIM6中断服务函数实现

TIM6_IRQHandler 是整个测速逻辑的执行引擎。其实现必须精炼、高效,避免任何可能阻塞的操作(如 printf HAL_Delay ):

// stm32f4xx_it.c
extern void Encoder_Speed_Update(void);

void TIM6_DAC_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim6);
}

// encoder_speed.c
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6)
    {
        Encoder_Speed_Update();
    }
}

void Encoder_Speed_Update(void)
{
    uint16_t current_cnt = __HAL_TIM_GET_COUNTER(&htim2);
    uint8_t current_dir = (__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2)) ? 0 : 1;
    uint32_t current_rev = encoder_revolution_count;

    int32_t delta_cnt = (int32_t)current_cnt - (int32_t)last_cnt;
    uint32_t rev_delta = current_rev - last_rev;

    if (current_dir == 1)
    {
        delta_cnt += (int32_t)(rev_delta * 65536U);
    }
    else
    {
        delta_cnt -= (int32_t)(rev_delta * 65536U);
    }

    // M法计算:n = ΔN * 6 (rpm per ms)
    speed_rpm = delta_cnt * 6;

    // 更新历史状态
    last_cnt = current_cnt;
    last_rev = current_rev;
    last_dir = current_dir;
}

HAL_TIM_PeriodElapsedCallback 是HAL库提供的标准回调函数,当TIM6计数器溢出时被自动调用。此设计将硬件中断处理与业务逻辑解耦,符合模块化编程思想。

5.4 主循环中的速度使用

在主控制循环中,可通过 Encoder_Speed_Get() 安全地获取最新速度值,并将其送入速度环PI控制器:

// main.c
while (1)
{
    int32_t measured_speed = Encoder_Speed_Get();

    // 速度环PI计算,输出为q轴电压参考值Uq_ref
    float Uq_ref = Speed_PI_Controller(measured_speed, speed_ref);

    // 将Uq_ref传递给SVPWM模块,生成PWM波形
    SVPWM_Generate(Uq_ref, Ud_ref);

    HAL_Delay(1); // 保持主循环节奏,非必需
}

6. 调试验证与在线观测

在嵌入式开发中,“能跑”不等于“正确”。一个健壮的测速模块必须经过严格的边界条件测试。本节介绍几种关键的验证手段。

6.1 使用ST-Link Utility进行寄存器级观测

最底层的验证是直接观察TIM2计数器与Z相中断计数器的实时变化。通过ST-Link Utility连接目标板,打开“Memory Browser”,定位到TIM2的 CNT 寄存器地址(0x4000 0024)与 encoder_revolution_count 变量的RAM地址。手动缓慢旋转电机,观察 CNT 值是否随A/B相变化而平稳增减, encoder_revolution_count 是否在每转一圈时精确加一。这是排除硬件连接与基础驱动错误的第一步。

6.2 串口打印与上位机绘图

Encoder_Speed_Update() 末尾添加轻量级串口打印(务必关闭优化,避免编译器优化掉调试代码):

// 仅用于调试,发布版本应移除
char buf[32];
sprintf(buf, "Speed: %d rpm\r\n", speed_rpm);
HAL_UART_Transmit(&huart2, (uint8_t*)buf, strlen(buf), HAL_MAX_DELAY);

配合PC端串口助手(如XShell、Tera Term)或专业上位机(如PlotJuggler),可实时绘制速度曲线。当电机静止时, speed_rpm 应稳定在0附近(±1~2 rpm的微小波动属正常噪声);当施加正向Uq时,速度应平滑上升;施加负向Uq时,速度应平滑下降至负值。任何跳变、卡顿或符号错误都指向算法或硬件问题。

6.3 示波器交叉验证

使用双通道示波器,一通道接编码器A相,另一通道接TIM6的更新事件(可通过 TIM6->EGR = TIM_EGR_UG 在ISR中手动触发一个脉冲,或利用 TIM6->DIER |= TIM_DIER_UDE 使能更新事件DMA请求,再用DMA触发一个GPIO翻转)。测量A相脉冲周期 $T_a$,则理论转速为 $n_{theory} = \frac{60}{P \times T_a}$。将此值与软件计算值 speed_rpm 进行对比,偏差应小于±1%。这是对整个测速链路(硬件捕获、软件计算、时钟精度)的终极检验。

7. 工程经验与常见陷阱

在多个实际FOC项目中,我曾反复踩过一些看似微小、却足以让系统失控的坑。这些经验比教科书上的理论更为珍贵。

7.1 “假溢出”陷阱:Z相脉冲宽度与中断响应

Z相脉冲宽度通常为数毫秒。若MCU在Z相脉冲期间恰好处于高优先级中断(如ADC转换完成中断)中,可能导致Z相中断被延迟响应。当电机高速旋转时,下一个Z相脉冲可能已在前一个尚未处理完毕时到来,造成“漏计”。解决方案是: 将Z相中断优先级设为全系统最高 (NVIC_SetPriority(EXTI3_IRQn, 0)),并确保其ISR内代码极度精简,绝不调用任何可能引起阻塞的HAL函数。

7.2 方向误判:A/B相硬件相位与软件配置错配

TIM2编码器接口的 TIM_EncoderMode_TI12 TIM_EncoderMode_TI1 等模式,对A/B相的相位关系有严格要求。若硬件上A相超前B相90度,但软件配置为 TI12 (期望B相超前),则方向信号将永远为0。验证方法:在电机静止时,用万用表测量A、B相电压,确认相位关系;再在代码中临时打印 __HAL_TIM_IS_TIM_COUNTING_DOWN(&htim2) 的返回值,手动旋转电机,观察其是否随转向而翻转。

7.3 时间基准漂移:APB1时钟源的稳定性

本方案依赖TIM6的1 ms中断作为唯一时间基准。若系统时钟源(HSE或HSI)本身存在温漂或老化,会导致 T_s 偏离1 ms,进而使 speed_rpm 产生系统性偏差。在要求高精度的场合,应考虑使用更高稳定性的外部晶振(如TCXO),或引入一个独立的、高精度实时时钟(RTC)作为校准源,定期对TIM6的PSC/ARR进行微调。

7.4 多任务环境下的数据竞争

若系统移植到FreeRTOS上, Encoder_Speed_Get() 可能被多个任务并发调用。此时,仅靠变量原子性已不够。必须使用互斥量(Mutex)或临界区( taskENTER_CRITICAL() )进行保护:

// FreeRTOS环境下
static SemaphoreHandle_t speed_mutex;

int32_t Encoder_Speed_Get(void)
{
    int32_t ret;
    xSemaphoreTake(speed_mutex, portMAX_DELAY);
    ret = speed_rpm;
    xSemaphoreGive(speed_mutex);
    return ret;
}

Encoder_Speed_Init() 中创建该互斥量。忽视此点,在多任务系统中将导致难以复现的随机速度跳变故障。

8. 从M法到T法的演进思考

虽然本章聚焦于M法,但理解其局限性是迈向更高阶控制的起点。M法在1 ms采样周期下,对低于1 rpm的转速已失去分辨力($\Delta N < 1$),表现为速度显示为0。要突破此瓶颈,必须转向T法或其混合变种。

一种实用的混合方案是:在高速段(如|n| > 10 rpm)使用M法,因其鲁棒性强;在低速段(|n| < 10 rpm)无缝切换至T法,利用TIM2的输入捕获功能,精确测量A相两个上升沿之间的时间,并通过 __HAL_TIM_GET_COUNTER(&htim2) 读取计数器值来计算周期。切换点的选择需结合电机参数与应用需求,避免在切换点附近出现速度抖动。

另一种前沿思路是放弃传统的脉冲计数,直接对编码器A/B相的模拟正弦信号进行ADC采样,然后运用CORDIC算法或PLL锁相环实时解算位置与速度。这种方法理论上可提供无限分辨率,但对MCU的ADC性能、运算能力与算法鲁棒性提出了全新挑战。它代表了未来高精度伺服驱动的发展方向,而扎实掌握M法,正是攀登这座高峰的坚实基石。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值