跳转至

编码器接口与速度测量:正交编码器驱动开发

概述

增量式正交编码器(Incremental Quadrature Encoder)是电机控制系统中最常用的位置/速度反馈传感器。 它输出两路相位差90°的方波信号(A相和B相),通过计数脉冲数量得到位置,通过单位时间内的脉冲数得到速度。 STM32的定时器内置硬件编码器模式,可以无需CPU干预地自动计数,大幅降低软件开销。

本文学习目标

目标 技能等级
理解绝对式与增量式编码器的区别及选型 基础
掌握光电式与磁电式编码器的技术对比 基础
配置STM32 TIM2/TIM5(32位)编码器模式 中级
实现M法、T法、M/T法三种测速算法 中级
实现Z相归零(Homing)序列 中级
设计位置→速度→电流三环级联控制器 高级
实现带抗积分饱和的PID位置控制 高级
完成直流电机闭环位置控制完整项目 高级

前置知识: - STM32 GPIO与定时器基础配置 - 中断优先级与NVIC配置(参见 中断优先级配置) - PID控制算法基础 - 直流电机驱动基础(参见 直流电机控制

背景知识

绝对式编码器 vs 增量式编码器

编码器按输出信号类型分为两大类,选型时需根据应用场景权衡:

┌─────────────────────────────────────────────────────────────────┐
│              编码器类型对比                                       │
├──────────────┬──────────────────────┬──────────────────────────┤
│ 特性         │ 增量式编码器          │ 绝对式编码器              │
├──────────────┼──────────────────────┼──────────────────────────┤
│ 输出信号     │ A/B/Z 方波脉冲        │ 并行二进制/SSI/BiSS/EnDat│
│ 断电记忆     │ 无(需重新归零)       │ 有(断电后位置保持)      │
│ 分辨率       │ PPR × 4(四倍频)     │ 位数决定(12~26位)       │
│ 典型分辨率   │ 100~10000 PPR        │ 4096~67M counts/转       │
│ 成本         │ 低(¥10~¥200)       │ 高(¥100~¥5000)         │
│ 接口复杂度   │ 简单(3根信号线)     │ 复杂(多线并行或串行协议)│
│ 抗干扰       │ 差分RS422可增强       │ 通常内置差分驱动          │
│ 适用场景     │ 速度控制、相对位置    │ 机器人关节、数控机床      │
│ 典型产品     │ 欧姆龙E6B2、编码器盘 │ 多摩川TS5700、海德汉ERN  │
└──────────────┴──────────────────────┴──────────────────────────┘

选型建议: - 速度控制、相对位移测量 → 增量式(成本低,接口简单) - 上电即需知道绝对位置、安全关键场合 → 绝对式 - 多圈绝对位置(如机械臂关节)→ 多圈绝对式编码器(带电池或齿轮组)

光电式 vs 磁电式编码器

┌─────────────────────────────────────────────────────────────────┐
│              传感原理对比                                         │
├──────────────┬──────────────────────┬──────────────────────────┤
│ 特性         │ 光电式编码器          │ 磁电式编码器              │
├──────────────┼──────────────────────┼──────────────────────────┤
│ 工作原理     │ 光栅码盘+光电管       │ 磁性码盘+霍尔/磁阻传感器  │
│ 分辨率       │ 高(可达10000 PPR)   │ 中(通常≤2048 PPR)      │
│ 抗污染       │ 差(油污/灰尘影响大) │ 好(密封性强)            │
│ 抗振动       │ 中(码盘易碎)        │ 好(无脆性部件)          │
│ 温度范围     │ -10~70°C             │ -40~125°C                │
│ 响应频率     │ 高(>500kHz)        │ 中(<200kHz)             │
│ 典型应用     │ 精密数控、实验室      │ 工业电机、汽车、户外设备  │
│ 代表产品     │ 欧姆龙E6B2-CWZ6C     │ AS5048A、MA730、TLE5012B │
└──────────────┴──────────────────────┴──────────────────────────┘

磁电编码器工作原理(以AS5048A为例): - 磁铁固定在电机轴端,随轴旋转 - 芯片内部霍尔阵列检测磁场方向 - CORDIC算法计算角度,输出14位绝对角度(016383对应0360°) - SPI接口读取,支持菊花链连接多个编码器

增量式编码器信号详解

正转(A相超前B相90°):
         ┌──┐  ┌──┐  ┌──┐  ┌──┐
A相:────┘  └──┘  └──┘  └──┘  └──
      ┌──┐  ┌──┐  ┌──┐  ┌──┐
B相:─┘  └──┘  └──┘  └──┘  └────

反转(B相超前A相90°):
      ┌──┐  ┌──┐  ┌──┐  ┌──┐
A相:─┘  └──┘  └──┘  └──┘  └────
         ┌──┐  ┌──┐  ┌──┐  ┌──┐
B相:────┘  └──┘  └──┘  └──┘  └──

Z相(每转一个脉冲,宽度通常=1个A相周期):
                    ┌─┐
Z相:───────────────┘ └──────────

四倍频计数原理: - 模式1(TI1):仅A相边沿计数,分辨率 = PPR - 模式2(TI2):仅B相边沿计数,分辨率 = PPR - 模式3(TI12):A、B两相上升沿+下降沿均计数,分辨率 = PPR × 4

四倍频计数示意(1000 PPR编码器):
A相:  ↑    ↓    ↑    ↓    ↑
B相:    ↑    ↓    ↑    ↓    ↑
计数:  1    2    3    4    5  ...
每转:1000 × 4 = 4000 counts
角度分辨率:360° / 4000 = 0.09°

编码器信号调理:施密特触发器与RS422差分

原始编码器信号在长距离传输或噪声环境中容易失真,需要信号调理:

施密特触发器(Schmitt Trigger)整形

噪声信号(未整形):
     ___/\/\/\___/\/\/\___
    /                     \
───/                       \───

经施密特触发器整形后:
    ┌────────────┐
────┘            └──────────────

施密特触发器迟滞特性:
  VT+ = 上升阈值(如3.0V)
  VT- = 下降阈值(如1.5V)
  迟滞 = VT+ - VT- = 1.5V(防止噪声引起误触发)

常用芯片:74HC14(六路反相施密特触发器)、SN74LVC1G17(单路同相)

RS422差分传输(长距离/强干扰场合):

编码器端(差分驱动):
  A+  ──────────────────────────→ 接收端 A+
  A-  ──────────────────────────→ 接收端 A-
  B+  ──────────────────────────→ 接收端 B+
  B-  ──────────────────────────→ 接收端 B-
  Z+  ──────────────────────────→ 接收端 Z+
  Z-  ──────────────────────────→ 接收端 Z-

差分信号抗共模干扰:
  干扰噪声同时叠加在A+和A-上
  接收端取差值:(A+ + noise) - (A- + noise) = A+ - A-
  共模噪声被完全消除

RS422接收芯片:AM26LS32(四路差分接收器),接STM32前需加100Ω终端电阻:

// RS422接收电路(硬件层面,无需软件配置)
// AM26LS32输出为TTL电平,直接连接STM32 GPIO
// 注意:STM32 GPIO为3.3V,AM26LS32输出为5V,需加分压电阻或使用5V容忍引脚

// 推荐接法:
// AM26LS32 OUT → 1kΩ → STM32 TIMx_CH1
//                    ↓
//                  2kΩ → GND
// 分压后:5V × 2/(1+2) = 3.33V ≈ 3.3V(安全)

核心内容

STM32定时器编码器模式

STM32的TIM1/TIM2/TIM3/TIM4/TIM5/TIM8均支持编码器模式,硬件自动处理方向判断和计数。

16位 vs 32位定时器选择

┌─────────────────────────────────────────────────────────────────┐
│              STM32定时器编码器模式对比                            │
├──────────────┬──────────────────────┬──────────────────────────┤
│ 定时器       │ 计数位宽              │ 适用场景                  │
├──────────────┼──────────────────────┼──────────────────────────┤
│ TIM3/TIM4   │ 16位(0~65535)       │ 低速、短时间测量           │
│ TIM1/TIM8   │ 16位(高级定时器)    │ 低速、需要互补PWM同步      │
│ TIM2/TIM5   │ 32位(0~4294967295) │ 高速、长时间、高精度       │
└──────────────┴──────────────────────┴──────────────────────────┘

16位溢出分析(1000 PPR编码器,四倍频=4000 CPR):
  最大计数:65535 counts
  对应圈数:65535 / 4000 = 16.38 圈
  3000 RPM时溢出时间:16.38 / (3000/60) = 0.33秒 → 频繁溢出!

32位溢出分析:
  最大计数:4294967295 counts
  对应圈数:4294967295 / 4000 = 1,073,741 圈
  3000 RPM时溢出时间:1,073,741 / (3000/60) = 21,474 秒 ≈ 5.96小时
  → 实际应用中几乎不会溢出

结论:高速电机控制(>500 RPM)或需要长时间累积位置时,务必使用TIM2或TIM5(32位)。

TIM2/TIM5 32位编码器配置

// 文件:encoder.h
// 芯片:STM32F4xx,HAL库版本:STM32F4 HAL v1.8.x
// CubeMX配置:TIM2 → Encoder Mode → Encoder Mode TI1 and TI2

#ifndef ENCODER_H
#define ENCODER_H

#include "stm32f4xx_hal.h"
#include <stdint.h>
#include <stdbool.h>

// 编码器参数(根据实际编码器修改)
#define ENCODER_PPR         1000        // 编码器线数(每转脉冲数,单相)
#define ENCODER_CPR         (ENCODER_PPR * 4)  // 四倍频后每转计数 = 4000
#define ENCODER_GEAR_RATIO  1.0f        // 减速比(直连=1.0)

// 速度计算参数
#define SPEED_SAMPLE_MS     10          // 速度采样周期(毫秒)
#define SPEED_FILTER_ALPHA  0.3f        // 低通滤波系数(0~1,越小越平滑)

// 位置控制参数
#define POSITION_COUNTS_PER_DEG  (ENCODER_CPR / 360.0f)  // 每度对应计数

typedef struct {
    TIM_HandleTypeDef *htim;        // 定时器句柄
    int64_t  total_count;           // 累积总计数(64位,永不溢出)
    int32_t  last_count;            // 上次读取的32位计数值
    float    speed_rpm;             // 当前转速(RPM)
    float    position_deg;          // 当前位置(度)
    bool     direction;             // true=正转,false=反转
    bool     index_found;           // Z相是否已找到
    int64_t  index_count;           // Z相对应的累积计数
} Encoder_t;

// 函数声明
void     encoder_init(Encoder_t *enc, TIM_HandleTypeDef *htim);
void     encoder_update(Encoder_t *enc);        // 在定时器中断中调用
float    encoder_get_speed(Encoder_t *enc);
float    encoder_get_position(Encoder_t *enc);
int64_t  encoder_get_count(Encoder_t *enc);
void     encoder_reset_position(Encoder_t *enc);
bool     encoder_check_error(Encoder_t *enc, float max_rpm);

#endif // ENCODER_H
// 文件:encoder.c
#include "encoder.h"
#include <math.h>

// TIM2硬件初始化(32位编码器模式)
// 引脚:PA0(TIM2_CH1/A相), PA1(TIM2_CH2/B相)
static void encoder_hw_init(TIM_HandleTypeDef *htim) {
    TIM_Encoder_InitTypeDef enc_cfg = {0};

    // 定时器基础配置
    htim->Instance               = TIM2;
    htim->Init.Prescaler         = 0;           // 不分频,直接计数
    htim->Init.CounterMode       = TIM_COUNTERMODE_UP;
    htim->Init.Period            = 0xFFFFFFFF;  // 32位最大值
    htim->Init.ClockDivision     = TIM_CLOCKDIVISION_DIV1;
    htim->Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;

    // 编码器模式:同时检测A、B两相的上升沿和下降沿(四倍频)
    enc_cfg.EncoderMode  = TIM_ENCODERMODE_TI12;

    // A相(CH1)配置
    enc_cfg.IC1Polarity  = TIM_ICPOLARITY_RISING;
    enc_cfg.IC1Selection = TIM_ICSELECTION_DIRECTTI;
    enc_cfg.IC1Prescaler = TIM_ICPSC_DIV1;
    enc_cfg.IC1Filter    = 0x0F;  // 数字滤波:8个采样周期确认,抑制毛刺

    // B相(CH2)配置
    enc_cfg.IC2Polarity  = TIM_ICPOLARITY_RISING;
    enc_cfg.IC2Selection = TIM_ICSELECTION_DIRECTTI;
    enc_cfg.IC2Prescaler = TIM_ICPSC_DIV1;
    enc_cfg.IC2Filter    = 0x0F;

    HAL_TIM_Encoder_Init(htim, &enc_cfg);

    // GPIO配置(PA0, PA1 → TIM2_CH1, TIM2_CH2)
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_TIM2_CLK_ENABLE();

    GPIO_InitTypeDef gpio = {
        .Pin       = GPIO_PIN_0 | GPIO_PIN_1,
        .Mode      = GPIO_MODE_AF_PP,
        .Pull      = GPIO_PULLUP,   // 上拉防悬空
        .Speed     = GPIO_SPEED_FREQ_HIGH,
        .Alternate = GPIO_AF1_TIM2
    };
    HAL_GPIO_Init(GPIOA, &gpio);

    // 启动编码器计数
    HAL_TIM_Encoder_Start(htim, TIM_CHANNEL_ALL);
}

void encoder_init(Encoder_t *enc, TIM_HandleTypeDef *htim) {
    enc->htim        = htim;
    enc->total_count = 0;
    enc->last_count  = 0;
    enc->speed_rpm   = 0.0f;
    enc->position_deg = 0.0f;
    enc->direction   = true;
    enc->index_found = false;
    enc->index_count = 0;

    encoder_hw_init(htim);

    // 将计数器初始化为中间值,使正负方向均可计数
    // 对于32位定时器,使用0即可(有符号处理)
    __HAL_TIM_SET_COUNTER(htim, 0);
}

// 更新编码器状态(在10ms定时器中断中调用)
void encoder_update(Encoder_t *enc) {
    // 读取当前32位计数值
    int32_t current = (int32_t)__HAL_TIM_GET_COUNTER(enc->htim);

    // 计算增量(32位有符号差值,自动处理溢出)
    int32_t delta = current - enc->last_count;
    enc->last_count = current;

    // 累积到64位总计数(永不溢出)
    enc->total_count += delta;

    // 更新方向
    enc->direction = (delta >= 0);

    // 计算速度(M法,单位:RPM)
    // RPM = (delta / CPR) × (60 / T_s)
    // T_s = SPEED_SAMPLE_MS / 1000
    float rpm_raw = (float)delta / ENCODER_CPR
                    * (60000.0f / SPEED_SAMPLE_MS);

    // 一阶低通滤波
    enc->speed_rpm = SPEED_FILTER_ALPHA * rpm_raw
                   + (1.0f - SPEED_FILTER_ALPHA) * enc->speed_rpm;

    // 更新位置(度)
    enc->position_deg = (float)enc->total_count / ENCODER_CPR * 360.0f;
}

float   encoder_get_speed(Encoder_t *enc)    { return enc->speed_rpm; }
float   encoder_get_position(Encoder_t *enc) { return enc->position_deg; }
int64_t encoder_get_count(Encoder_t *enc)    { return enc->total_count; }

void encoder_reset_position(Encoder_t *enc) {
    enc->total_count  = 0;
    enc->position_deg = 0.0f;
    __HAL_TIM_SET_COUNTER(enc->htim, 0);
}

Z相归零(Homing)序列

Z相(Index)每转产生一个脉冲,用于建立绝对参考位置。归零序列是精密运动控制的必要步骤:

// 文件:homing.c
// Z相归零状态机

typedef enum {
    HOMING_IDLE,            // 空闲
    HOMING_MOVING,          // 正在移动寻找Z相
    HOMING_FOUND,           // 找到Z相,减速停止
    HOMING_COMPLETE,        // 归零完成
    HOMING_ERROR            // 超时或错误
} HomingState_t;

typedef struct {
    HomingState_t state;
    uint32_t      start_time_ms;    // 归零开始时间
    uint32_t      timeout_ms;       // 超时时间(默认5000ms)
    float         search_speed_rpm; // 搜索速度(慢速,如50 RPM)
    int64_t       home_count;       // 归零后的参考计数
    bool          z_triggered;      // Z相中断标志(在ISR中置位)
} Homing_t;

static Homing_t homing = {
    .state           = HOMING_IDLE,
    .timeout_ms      = 5000,
    .search_speed_rpm = 50.0f,
    .z_triggered     = false
};

// Z相外部中断回调(配置PA2为EXTI,上升沿触发)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == GPIO_PIN_2) {  // Z相引脚
        homing.z_triggered = true;
    }
}

// 归零状态机(在主循环或低优先级任务中调用)
HomingState_t homing_run(Encoder_t *enc, void (*set_speed)(float)) {
    switch (homing.state) {
        case HOMING_IDLE:
            // 开始归零:以慢速正转
            homing.start_time_ms = HAL_GetTick();
            homing.z_triggered   = false;
            set_speed(homing.search_speed_rpm);
            homing.state = HOMING_MOVING;
            break;

        case HOMING_MOVING:
            // 检查超时
            if (HAL_GetTick() - homing.start_time_ms > homing.timeout_ms) {
                set_speed(0.0f);
                homing.state = HOMING_ERROR;
                break;
            }
            // 检查Z相触发
            if (homing.z_triggered) {
                set_speed(0.0f);                    // 立即停止
                homing.home_count = enc->total_count; // 记录归零位置
                encoder_reset_position(enc);          // 清零位置
                homing.state = HOMING_FOUND;
            }
            break;

        case HOMING_FOUND:
            // 等待电机完全停止(速度<1 RPM)
            if (fabsf(encoder_get_speed(enc)) < 1.0f) {
                homing.state = HOMING_COMPLETE;
            }
            break;

        case HOMING_COMPLETE:
        case HOMING_ERROR:
            // 终态,等待外部重置
            break;
    }
    return homing.state;
}

// 归零完成后,位置0对应Z相位置
// 可以进一步移动到机械零点(如偏移固定角度)
void homing_go_to_mechanical_zero(float offset_deg) {
    // 移动到 offset_deg 位置(由位置控制器执行)
    // position_controller_set_target(offset_deg);
}

Z相归零时序图

电机转速:
  ──────────────────────────────────────────────────────
  50 RPM ████████████████████████████████████▌
                                              ↓ Z相触发
  0 RPM  ─────────────────────────────────────────────

Z相信号:
  ─────────────────────────────────────────┐ ┌─────────
                                           └─┘
  ↑ 开始归零                               ↑ 触发,停止

位置计数:
  0 → 4000 → 8000 → ... → N × 4000 → 重置为0

编码器错误检测

在高可靠性应用中,需要检测编码器故障:

// 编码器错误检测
typedef enum {
    ENC_OK           = 0,
    ENC_ERR_SIGNAL   = 1,   // 信号丢失(长时间无脉冲但命令有速度)
    ENC_ERR_OVERSPEED = 2,  // 超速(超过最大允许转速)
    ENC_ERR_DIRECTION = 3,  // 方向异常(命令正转但反转)
} EncoderError_t;

// 错误检测(在速度控制循环中调用)
// max_rpm:最大允许转速
// cmd_speed:命令速度(正=正转,负=反转)
EncoderError_t encoder_check_error(Encoder_t *enc, float max_rpm) {
    float speed = enc->speed_rpm;

    // 检查超速
    if (fabsf(speed) > max_rpm) {
        return ENC_ERR_OVERSPEED;
    }

    return ENC_OK;
}

// 信号丢失检测(需要额外的硬件或软件机制)
// 方法1:硬件 - 用定时器捕获检测脉冲间隔,超时则报警
// 方法2:软件 - 命令速度非零但编码器速度持续为0超过阈值时间
typedef struct {
    uint32_t no_pulse_start_ms;     // 无脉冲开始时间
    uint32_t no_pulse_timeout_ms;   // 无脉冲超时阈值(如200ms)
    int64_t  last_total_count;      // 上次检查的总计数
} SignalLossDetector_t;

bool encoder_signal_lost(Encoder_t *enc, SignalLossDetector_t *det,
                         float cmd_speed) {
    // 如果命令速度接近0,不检测
    if (fabsf(cmd_speed) < 5.0f) {
        det->no_pulse_start_ms = 0;
        det->last_total_count  = enc->total_count;
        return false;
    }

    // 检查计数是否变化
    if (enc->total_count != det->last_total_count) {
        det->no_pulse_start_ms = 0;  // 有脉冲,重置计时
        det->last_total_count  = enc->total_count;
        return false;
    }

    // 计数未变化,开始计时
    if (det->no_pulse_start_ms == 0) {
        det->no_pulse_start_ms = HAL_GetTick();
    }

    // 超时判断
    return (HAL_GetTick() - det->no_pulse_start_ms > det->no_pulse_timeout_ms);
}

测速算法详解

M法(频率法)

固定时间T内统计脉冲数,适合**高速**场景(>100 RPM):

// M法测速:每T秒读取一次脉冲数
// 速度(RPM) = (脉冲数 / CPR) × (60 / T)
#define SAMPLE_TIME_MS  10      // 采样周期10ms

float speed_m_method(Encoder_t *enc) {
    static int64_t last_count = 0;
    int64_t current = enc->total_count;
    int64_t delta   = current - last_count;
    last_count = current;

    // RPM = (delta / CPR) × (60000 / SAMPLE_TIME_MS)
    return (float)delta / ENCODER_CPR * (60000.0f / SAMPLE_TIME_MS);
}

// M法误差分析:
// 最小可分辨速度 = 1 count / (CPR × T_s) × 60
// 以CPR=4000, T_s=10ms为例:
// 最小速度 = 1/4000/0.01×60 = 1.5 RPM
// 低于1.5 RPM时,速度分辨率为1.5 RPM(误差大)

T法(周期法)

测量相邻脉冲的时间间隔,适合**低速**场景(<50 RPM):

// T法测速:测量两个脉冲之间的时间
// 使用TIM5(32位,1MHz时钟)捕获A相脉冲时间戳

static volatile uint32_t t_method_period_us = 0;  // 脉冲周期(微秒)
static volatile uint32_t t_method_last_us   = 0;  // 上次脉冲时间戳

// 在A相外部中断中调用(或使用定时器输入捕获)
void encoder_a_phase_isr(void) {
    uint32_t now = TIM5->CNT;  // TIM5配置为1MHz自由运行计数器
    if (t_method_last_us != 0) {
        t_method_period_us = now - t_method_last_us;
    }
    t_method_last_us = now;
}

float speed_t_method(void) {
    uint32_t period = t_method_period_us;
    if (period == 0) return 0.0f;

    // 超时检测:如果超过500ms无脉冲,认为速度为0
    uint32_t elapsed = TIM5->CNT - t_method_last_us;
    if (elapsed > 500000) return 0.0f;  // 500ms超时

    // RPM = 60 × 10^6 / (CPR × 脉冲周期us)
    // 注意:T法计数的是单相脉冲,CPR需除以4
    return 60.0f * 1000000.0f / (ENCODER_PPR * period);
}

M/T法(推荐,全速度范围高精度)

// M/T法:同时计数脉冲数和精确时间
// 在固定采样窗口内,同时记录脉冲数和高精度时间
typedef struct {
    int64_t  pulse_count;   // 采样窗口内的脉冲数
    uint32_t time_us;       // 实际计时时间(微秒,用TIM5测量)
    float    rpm;           // 计算结果
} MTSpeed_t;

void speed_mt_method(Encoder_t *enc, MTSpeed_t *s) {
    static uint32_t start_time_us = 0;
    static int64_t  start_count   = 0;

    uint32_t now   = TIM5->CNT;  // 1MHz高精度时间戳
    int64_t  count = enc->total_count;

    s->pulse_count = count - start_count;
    s->time_us     = now - start_time_us;

    if (s->time_us > 0 && s->pulse_count != 0) {
        // RPM = (脉冲数 / CPR) × (60 × 10^6 / 时间us)
        s->rpm = (float)s->pulse_count / ENCODER_CPR
                 * 60.0f * 1000000.0f / (float)s->time_us;
    } else {
        s->rpm = 0.0f;
    }

    start_time_us = now;
    start_count   = count;
}

// M/T法误差分析:
// 误差来源:时间测量误差(1μs)+ 计数误差(±1 count)
// 在低速时:时间长,计数误差相对小 → 精度高
// 在高速时:计数多,时间误差相对小 → 精度高
// 全速度范围误差均匀,优于单独使用M法或T法

深入原理

速度滤波算法

原始速度数据通常含有噪声,需要滤波处理:

// 一阶低通滤波(指数移动平均,IIR滤波器)
// 传递函数:H(z) = α / (1 - (1-α)z^{-1})
// alpha越小,截止频率越低,滤波越强,但响应越慢
// 推荐:alpha = 0.1~0.5,根据噪声水平调整
float speed_lpf(float new_speed, float *state, float alpha) {
    *state = alpha * new_speed + (1.0f - alpha) * (*state);
    return *state;
}

// 滑动平均滤波(FIR滤波器,线性相位,延迟均匀)
#define MA_SIZE  8
float speed_moving_average(float new_speed) {
    static float buf[MA_SIZE] = {0};
    static int   idx = 0;
    static float sum = 0.0f;

    sum -= buf[idx];
    buf[idx] = new_speed;
    sum += new_speed;
    idx = (idx + 1) % MA_SIZE;
    return sum / MA_SIZE;
}

// 卡尔曼滤波(最优估计,适合噪声特性已知的场合)
typedef struct {
    float x;   // 状态估计(速度)
    float p;   // 估计误差协方差
    float q;   // 过程噪声协方差(系统噪声)
    float r;   // 测量噪声协方差(传感器噪声)
} KalmanFilter_t;

float speed_kalman(KalmanFilter_t *kf, float measurement) {
    // 预测步骤
    // x_pred = x(匀速模型,无加速度输入)
    float p_pred = kf->p + kf->q;

    // 更新步骤
    float k = p_pred / (p_pred + kf->r);  // 卡尔曼增益
    kf->x = kf->x + k * (measurement - kf->x);
    kf->p = (1.0f - k) * p_pred;

    return kf->x;
}

// 卡尔曼滤波初始化示例
// KalmanFilter_t kf = {.x=0, .p=1.0f, .q=0.01f, .r=0.1f};
// q越大:相信测量值,响应快但噪声大
// r越大:相信预测值,响应慢但平滑

位置→速度→电流三环级联控制

工业伺服驱动器的标准控制架构,三个控制环从外到内嵌套:

┌─────────────────────────────────────────────────────────────────┐
│                    三环级联控制架构                               │
│                                                                  │
│  位置指令    ┌──────────┐  速度指令   ┌──────────┐  电流指令    │
│  θ_ref ────→│ 位置环   │──→ ω_ref ──→│ 速度环   │──→ I_ref ──→│
│             │ PID控制  │             │ PID控制  │             │
│             └──────────┘             └──────────┘             │
│                  ↑                        ↑                    │
│             θ_feedback              ω_feedback                 │
│             (编码器位置)             (编码器速度)               │
│                                                                  │
│                                          ┌──────────┐  PWM     │
│                                 I_ref ──→│ 电流环   │──→ 电机  │
│                                          │ PI控制   │          │
│                                          └──────────┘          │
│                                               ↑                │
│                                          I_feedback            │
│                                          (电流传感器)           │
│                                                                  │
│  带宽关系:ω_current >> ω_velocity >> ω_position               │
│  典型值:  电流环1kHz,  速度环100Hz,  位置环10Hz               │
└─────────────────────────────────────────────────────────────────┘

各环职责: - 电流环(最内环):控制电机电流(即转矩),响应最快(1kHz),使用PI控制 - 速度环(中间环):控制电机转速,响应中等(100Hz),使用PI控制 - 位置环(最外环):控制电机位置,响应最慢(10Hz),通常使用P控制(加速度前馈)

带抗积分饱和的PID位置控制

积分饱和(Integral Windup)是位置控制中的常见问题:当误差长时间存在(如机械限位),积分项持续累积,导致超调严重。

// 文件:pid_controller.h
// 带抗积分饱和、微分滤波的完整PID实现

typedef struct {
    // PID参数
    float kp;           // 比例增益
    float ki;           // 积分增益
    float kd;           // 微分增益
    float n;            // 微分滤波系数(通常5~20)

    // 输出限幅
    float out_min;      // 输出下限
    float out_max;      // 输出上限

    // 积分限幅(抗饱和)
    float int_min;      // 积分项下限
    float int_max;      // 积分项上限

    // 内部状态
    float integral;     // 积分累积值
    float prev_error;   // 上次误差(用于微分)
    float prev_deriv;   // 上次微分值(用于滤波)

    // 采样时间
    float dt;           // 采样周期(秒)
} PID_t;

void pid_init(PID_t *pid, float kp, float ki, float kd,
              float out_min, float out_max, float dt) {
    pid->kp       = kp;
    pid->ki       = ki;
    pid->kd       = kd;
    pid->n        = 10.0f;      // 微分滤波系数
    pid->out_min  = out_min;
    pid->out_max  = out_max;
    pid->int_min  = out_min;    // 积分限幅与输出限幅相同
    pid->int_max  = out_max;
    pid->integral = 0.0f;
    pid->prev_error = 0.0f;
    pid->prev_deriv = 0.0f;
    pid->dt       = dt;
}

float pid_compute(PID_t *pid, float setpoint, float measurement) {
    float error = setpoint - measurement;

    // 比例项
    float p_term = pid->kp * error;

    // 积分项(带限幅抗饱和)
    pid->integral += pid->ki * error * pid->dt;
    // 积分限幅:防止积分项无限增长
    if (pid->integral > pid->int_max) pid->integral = pid->int_max;
    if (pid->integral < pid->int_min) pid->integral = pid->int_min;
    float i_term = pid->integral;

    // 微分项(带低通滤波,抑制微分噪声)
    // 滤波微分:D(s) = kd × N × s / (s + N)
    // 离散化:d[k] = (1 - N×dt) × d[k-1] + kd × N × (e[k] - e[k-1])
    float deriv_raw = (error - pid->prev_error) / pid->dt;
    float alpha_d   = pid->n * pid->dt;
    pid->prev_deriv = (1.0f - alpha_d) * pid->prev_deriv + pid->kd * alpha_d * deriv_raw;
    float d_term    = pid->prev_deriv;

    pid->prev_error = error;

    // 输出求和与限幅
    float output = p_term + i_term + d_term;
    if (output > pid->out_max) output = pid->out_max;
    if (output < pid->out_min) output = pid->out_min;

    // 条件积分(Back-Calculation抗饱和)
    // 当输出饱和时,停止积分累积
    float output_unclamped = p_term + i_term + d_term;
    if ((output_unclamped > pid->out_max && error > 0) ||
        (output_unclamped < pid->out_min && error < 0)) {
        // 输出饱和且误差方向相同,停止积分
        pid->integral -= pid->ki * error * pid->dt;
    }

    return output;
}

void pid_reset(PID_t *pid) {
    pid->integral   = 0.0f;
    pid->prev_error = 0.0f;
    pid->prev_deriv = 0.0f;
}

抗积分饱和效果对比

无抗饱和(普通PID):
  位置误差:
  ████████████████████████████████████████████████████
  ↓ 到达目标                                          ↓ 超调严重
  ─────────────────────────────────────────────────────
  积分项:持续累积到很大值,到达目标后仍有大量积分残留

带抗饱和PID:
  位置误差:
  ████████████████████████████████████████
  ↓ 到达目标                              ↓ 轻微超调
  ─────────────────────────────────────────────────────
  积分项:饱和时停止累积,到达目标时积分值合理

速度前馈控制

纯反馈控制存在跟踪延迟,加入速度前馈可显著改善动态响应:

// 带速度前馈的位置控制器
// 速度前馈:根据位置指令的变化率预测所需速度
typedef struct {
    PID_t    pos_pid;       // 位置PID
    PID_t    vel_pid;       // 速度PID
    float    ff_gain;       // 前馈增益(通常0.5~1.0)
    float    prev_setpoint; // 上次位置指令
    float    dt;            // 采样周期
} CascadeController_t;

float cascade_compute(CascadeController_t *ctrl,
                      float pos_setpoint,
                      float pos_feedback,
                      float vel_feedback) {
    // 位置环:计算速度指令
    float vel_cmd = pid_compute(&ctrl->pos_pid, pos_setpoint, pos_feedback);

    // 速度前馈:根据位置指令变化率计算前馈速度
    float pos_rate = (pos_setpoint - ctrl->prev_setpoint) / ctrl->dt;
    ctrl->prev_setpoint = pos_setpoint;
    vel_cmd += ctrl->ff_gain * pos_rate;

    // 速度环:计算电流/PWM指令
    float pwm_cmd = pid_compute(&ctrl->vel_pid, vel_cmd, vel_feedback);

    return pwm_cmd;
}

完整项目实战:直流电机闭环位置控制

项目概述

本项目实现一个完整的直流电机闭环位置控制系统,具备以下功能: - 编码器位置/速度实时测量(TIM2,32位) - 双环PID控制(位置环 + 速度环) - Z相归零序列 - 串口命令接口(设置目标位置、查询状态) - 过流/超速保护

物料清单(BOM)

序号 元件 型号 数量 参考价格
1 主控MCU STM32F411CEU6(黑药丸) 1 ¥15
2 直流电机 25GA370减速电机,12V,带1000线编码器 1 ¥45
3 电机驱动 TB6612FNG双路H桥模块 1 ¥8
4 电源 12V/2A开关电源 1 ¥20
5 电源模块 AMS1117-3.3V稳压模块 1 ¥2
6 差分接收 AM26LS32(可选,长距离用) 1 ¥5
7 电流传感器 ACS712-5A(可选,电流环用) 1 ¥8
8 调试接口 USB-TTL模块(CH340) 1 ¥5

硬件连接图

STM32F411 引脚分配:
┌─────────────────────────────────────────────────────────────────┐
│  STM32F411CEU6                                                   │
│                                                                  │
│  PA0 (TIM2_CH1) ──────────────────→ 编码器 A相                  │
│  PA1 (TIM2_CH2) ──────────────────→ 编码器 B相                  │
│  PA2 (EXTI2)    ──────────────────→ 编码器 Z相                  │
│                                                                  │
│  PB6 (TIM4_CH1/PWM) ──────────────→ TB6612 PWMA                │
│  PB7 (GPIO_OUT)     ──────────────→ TB6612 AIN1                 │
│  PB8 (GPIO_OUT)     ──────────────→ TB6612 AIN2                 │
│  PB9 (GPIO_OUT)     ──────────────→ TB6612 STBY (高电平使能)    │
│                                                                  │
│  PA9  (USART1_TX) ────────────────→ USB-TTL RX                  │
│  PA10 (USART1_RX) ────────────────→ USB-TTL TX                  │
│                                                                  │
│  3.3V ────────────────────────────→ 编码器 VCC(若5V需分压)    │
│  GND  ────────────────────────────→ 编码器 GND, TB6612 GND      │
└─────────────────────────────────────────────────────────────────┘

TB6612FNG 接线:
  VM  → 12V电机电源
  VCC → 3.3V逻辑电源
  GND → 公共地
  AO1, AO2 → 电机两端

完整代码实现

// 文件:main.c
// 直流电机闭环位置控制主程序
// 芯片:STM32F411CEU6,HAL库,系统时钟96MHz

#include "stm32f4xx_hal.h"
#include "encoder.h"
#include "pid_controller.h"
#include <stdio.h>
#include <string.h>
#include <math.h>

// ─── 全局变量 ───────────────────────────────────────────────────
TIM_HandleTypeDef htim2;    // 编码器定时器(32位)
TIM_HandleTypeDef htim4;    // PWM定时器
UART_HandleTypeDef huart1;  // 调试串口

Encoder_t encoder;          // 编码器对象
PID_t     pos_pid;          // 位置PID
PID_t     vel_pid;          // 速度PID

float target_position_deg = 0.0f;   // 目标位置(度)
float target_speed_rpm    = 0.0f;   // 速度环目标(由位置环输出)

// 控制模式
typedef enum { MODE_IDLE, MODE_HOMING, MODE_POSITION } ControlMode_t;
ControlMode_t ctrl_mode = MODE_IDLE;

// ─── 电机PWM输出 ─────────────────────────────────────────────────
// duty: -100.0 ~ +100.0(负值反转)
void motor_set_pwm(float duty) {
    // 限幅
    if (duty >  100.0f) duty =  100.0f;
    if (duty < -100.0f) duty = -100.0f;

    uint32_t pwm_val = (uint32_t)(fabsf(duty) / 100.0f * 999);  // 0~999

    if (duty >= 0) {
        // 正转:AIN1=1, AIN2=0
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
    } else {
        // 反转:AIN1=0, AIN2=1
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
    }
    __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, pwm_val);
}

void motor_stop(void) {
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
    __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 0);
}

// ─── 10ms控制中断(TIM3)────────────────────────────────────────
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance != TIM3) return;

    // 1. 更新编码器状态
    encoder_update(&encoder);

    float pos = encoder_get_position(&encoder);
    float vel = encoder_get_speed(&encoder);

    // 2. 根据控制模式执行控制
    switch (ctrl_mode) {
        case MODE_POSITION: {
            // 位置环:计算速度指令
            float vel_cmd = pid_compute(&pos_pid, target_position_deg, pos);

            // 速度环:计算PWM指令
            float pwm_cmd = pid_compute(&vel_pid, vel_cmd, vel);

            motor_set_pwm(pwm_cmd);

            // 到位检测(误差<0.5度且速度<5RPM)
            if (fabsf(target_position_deg - pos) < 0.5f &&
                fabsf(vel) < 5.0f) {
                // 到位,保持位置(PID继续运行,自动补偿扰动)
            }
            break;
        }
        case MODE_IDLE:
            motor_stop();
            break;
        default:
            break;
    }

    // 3. 超速保护
    if (fabsf(vel) > 3500.0f) {  // 超过3500 RPM
        motor_stop();
        ctrl_mode = MODE_IDLE;
        printf("ERROR: Overspeed! %.1f RPM\r\n", vel);
    }
}
// 文件:main.c(续)—— 初始化与主循环

// ─── 系统初始化 ──────────────────────────────────────────────────
void system_init(void) {
    HAL_Init();
    SystemClock_Config();  // 96MHz

    // 初始化编码器(TIM2,32位)
    encoder_init(&encoder, &htim2);

    // 初始化位置PID
    // kp=0.8, ki=0.05, kd=0.02, 输出限幅±300 RPM, 采样10ms
    pid_init(&pos_pid, 0.8f, 0.05f, 0.02f, -300.0f, 300.0f, 0.01f);

    // 初始化速度PID
    // kp=0.3, ki=0.1, kd=0.0, 输出限幅±100% PWM, 采样10ms
    pid_init(&vel_pid, 0.3f, 0.1f, 0.0f, -100.0f, 100.0f, 0.01f);

    // 初始化PWM(TIM4,10kHz)
    pwm_init();

    // 初始化串口(115200 baud)
    uart_init();

    // 初始化10ms控制定时器(TIM3)
    control_timer_init();

    printf("System initialized. Type 'help' for commands.\r\n");
}

// ─── 串口命令处理 ────────────────────────────────────────────────
// 支持命令:
//   pos <degrees>  — 移动到指定位置(度)
//   home           — 执行归零序列
//   stop           — 停止电机
//   status         — 查询当前状态
//   pid p <kp> <ki> <kd>  — 设置位置PID参数
void process_command(const char *cmd) {
    float val;
    if (sscanf(cmd, "pos %f", &val) == 1) {
        target_position_deg = val;
        ctrl_mode = MODE_POSITION;
        printf("Moving to %.1f degrees\r\n", val);
    } else if (strcmp(cmd, "home") == 0) {
        ctrl_mode = MODE_HOMING;
        printf("Starting homing sequence...\r\n");
    } else if (strcmp(cmd, "stop") == 0) {
        ctrl_mode = MODE_IDLE;
        motor_stop();
        pid_reset(&pos_pid);
        pid_reset(&vel_pid);
        printf("Motor stopped\r\n");
    } else if (strcmp(cmd, "status") == 0) {
        printf("Position: %.2f deg | Speed: %.1f RPM | Target: %.1f deg\r\n",
               encoder_get_position(&encoder),
               encoder_get_speed(&encoder),
               target_position_deg);
    } else {
        printf("Commands: pos <deg>, home, stop, status\r\n");
    }
}

int main(void) {
    system_init();

    char cmd_buf[64];
    uint32_t last_print_ms = 0;

    while (1) {
        // 处理串口命令(非阻塞)
        if (uart_readline(cmd_buf, sizeof(cmd_buf))) {
            process_command(cmd_buf);
        }

        // 每500ms打印一次状态
        if (HAL_GetTick() - last_print_ms >= 500) {
            last_print_ms = HAL_GetTick();
            printf("Pos:%.1fdeg Vel:%.1fRPM Target:%.1fdeg\r\n",
                   encoder_get_position(&encoder),
                   encoder_get_speed(&encoder),
                   target_position_deg);
        }
    }
}

调试与参数整定

PID参数整定步骤(Ziegler-Nichols临界增益法):

步骤1:速度环整定(先整定内环)
  1. 将ki=0, kd=0,逐渐增大kp
  2. 直到速度响应出现持续振荡,记录此时kp为Ku(临界增益)
  3. 记录振荡周期Tu
  4. 设置:kp = 0.45×Ku, ki = 0.54×Ku/Tu, kd = 0

步骤2:位置环整定(速度环稳定后)
  1. 将速度环参数固定
  2. 位置环通常只需P控制:kp = 0.1~0.5
  3. 过大的kp导致超调,过小导致响应慢
  4. 加入少量ki消除稳态误差

典型参数(25GA370电机,1000线编码器):
  速度环:kp=0.3, ki=0.1, kd=0.0
  位置环:kp=0.8, ki=0.05, kd=0.02

串口调试输出示例

System initialized. Type 'help' for commands.
> pos 360
Moving to 360.0 degrees
Pos:0.0deg Vel:0.0RPM Target:360.0deg
Pos:45.2deg Vel:285.3RPM Target:360.0deg
Pos:180.5deg Vel:312.1RPM Target:360.0deg
Pos:320.8deg Vel:198.4RPM Target:360.0deg
Pos:358.2deg Vel:45.6RPM Target:360.0deg
Pos:360.1deg Vel:2.3RPM Target:360.0deg   ← 到位
> status
Position: 360.05 deg | Speed: 0.8 RPM | Target: 360.0 deg

性能调优

编码器分辨率与控制精度的关系

位置分辨率 = 360° / (PPR × 4 × 减速比)

示例:
  1000线编码器,直连(减速比=1):
    分辨率 = 360 / (1000 × 4) = 0.09°/count

  1000线编码器,100:1减速箱:
    分辨率 = 360 / (1000 × 4 × 100) = 0.0009°/count = 3.24角秒

速度分辨率(M法,10ms采样):
  最小可分辨速度 = 1 count / (CPR × T_s) × 60
  = 1 / (4000 × 0.01) × 60 = 1.5 RPM

提高速度分辨率的方法:
  1. 增大采样周期(但会增加控制延迟)
  2. 使用M/T法(低速时精度更高)
  3. 使用更高线数编码器(2500线、5000线)
  4. 增大减速比(同时提高位置和速度分辨率)

中断优先级配置

编码器相关中断的优先级设置对系统稳定性至关重要:

// 推荐优先级配置(数字越小优先级越高)
// 电流环中断(最高优先级,1kHz)
HAL_NVIC_SetPriority(TIM1_UP_TIM10_IRQn, 0, 0);

// 速度/位置控制中断(次高优先级,100Hz/10Hz)
HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0);

// Z相外部中断(高优先级,确保归零精度)
HAL_NVIC_SetPriority(EXTI2_IRQn, 1, 1);

// 串口中断(低优先级)
HAL_NVIC_SetPriority(USART1_IRQn, 3, 0);

// 注意:编码器计数由硬件完成,无需中断,不占用CPU

常见问题与调试

问题1:计数方向反了

现象:电机正转时位置减小,反转时位置增大。

原因:A、B相接线顺序与定时器期望的相位关系相反。

解决方案

// 方法1(推荐):交换A、B相物理接线
// 将编码器A相接到TIM2_CH2,B相接到TIM2_CH1

// 方法2:软件取反(不改变硬件)
int32_t encoder_get_count_corrected(Encoder_t *enc) {
    return -(enc->total_count);  // 取反
}

// 方法3:修改定时器极性配置
// 将IC1Polarity改为TIM_ICPOLARITY_FALLING
enc_cfg.IC1Polarity = TIM_ICPOLARITY_FALLING;  // 反转A相极性

问题2:噪声引起的误计数

现象:电机静止时计数值仍在跳动(±1~±5 counts)。

原因: - 编码器信号线受到电机PWM干扰(共地噪声、辐射耦合) - 编码器信号边沿不干净(上升/下降时间过长) - GPIO上拉电阻过大,信号边沿缓慢

解决方案

// 方案1:增大硬件数字滤波(IC1Filter/IC2Filter)
// 值越大,需要更多连续采样才确认边沿,抑制毛刺
enc_cfg.IC1Filter = 0x0F;  // 最大滤波(8个采样周期)
enc_cfg.IC2Filter = 0x0F;

// 方案2:软件死区滤波(静止时忽略小抖动)
#define NOISE_THRESHOLD  3  // 小于3 counts认为是噪声

int32_t encoder_get_filtered_delta(Encoder_t *enc) {
    static int64_t last_stable_count = 0;
    int64_t delta = enc->total_count - last_stable_count;

    if (llabs(delta) > NOISE_THRESHOLD) {
        last_stable_count = enc->total_count;
        return (int32_t)delta;
    }
    return 0;  // 小抖动忽略
}

// 方案3:硬件措施
// - 编码器信号线使用屏蔽双绞线
// - 信号线远离电机驱动线(至少5cm间距)
// - 在编码器信号线上串联100Ω电阻 + 对地100pF电容(RC滤波)
// - 使用RS422差分传输(彻底消除共模干扰)

噪声诊断方法

// 统计静止时的计数抖动量
void encoder_noise_test(Encoder_t *enc, uint32_t duration_ms) {
    int64_t start_count = enc->total_count;
    int64_t max_count   = start_count;
    int64_t min_count   = start_count;
    uint32_t start_time = HAL_GetTick();

    printf("Noise test started (motor must be stationary)...\r\n");

    while (HAL_GetTick() - start_time < duration_ms) {
        int64_t c = enc->total_count;
        if (c > max_count) max_count = c;
        if (c < min_count) min_count = c;
        HAL_Delay(1);
    }

    printf("Noise range: %lld counts (%.3f degrees)\r\n",
           max_count - min_count,
           (float)(max_count - min_count) / ENCODER_CPR * 360.0f);
}

问题3:16位定时器溢出导致位置跳变

现象:高速运行时位置突然跳变±16384度(对应65536 counts)。

原因:使用16位定时器(TIM3/TIM4),计数器从65535溢出到0(或从0下溢到65535),差值计算错误。

解决方案

// 方案1(根本解决):改用32位定时器TIM2或TIM5
// 32位定时器在实际应用中几乎不会溢出(见前文分析)

// 方案2:16位定时器的溢出处理(若必须使用16位)
// 关键:使用有符号16位差值,自动处理溢出
int32_t encoder_read_delta_16bit(TIM_HandleTypeDef *htim) {
    static uint16_t last_count = 0;
    uint16_t current = (uint16_t)__HAL_TIM_GET_COUNTER(htim);

    // 有符号16位差值:自动处理0→65535和65535→0的溢出
    int16_t delta = (int16_t)(current - last_count);
    last_count = current;
    return (int32_t)delta;
}

// 原理:
// 正转:current=100, last=65500 → delta = (int16_t)(100-65500) = (int16_t)(-65400) = 136
// 反转:current=65500, last=100 → delta = (int16_t)(65500-100) = (int16_t)(65400) = -136
// 利用16位整数溢出的环绕特性,自动得到正确的有符号差值

问题4:低速时速度为0或跳动

现象:电机低速运行(<20 RPM)时,M法测速结果为0或在0和某个值之间跳动。

原因:M法在低速时,采样周期内脉冲数太少(0或1个),分辨率不足。

解决方案

// 方案1:改用T法或M/T法(见前文)

// 方案2:增大M法采样周期(降低速度更新频率)
// 将采样周期从10ms增大到100ms,低速分辨率提高10倍
// 代价:速度反馈延迟增大

// 方案3:自适应测速(根据速度自动切换算法)
float speed_adaptive(Encoder_t *enc) {
    float m_speed = speed_m_method(enc);

    if (fabsf(m_speed) < 30.0f) {
        // 低速:使用T法(需要额外的脉冲捕获硬件)
        return speed_t_method();
    } else {
        // 高速:使用M法
        return m_speed;
    }
}

问题5:归零失败(Z相未找到)

现象:执行归零序列后,状态机进入HOMING_ERROR

排查步骤

// 步骤1:验证Z相信号是否存在
// 用示波器或逻辑分析仪检查Z相引脚,手动转动电机一圈,应看到一个脉冲

// 步骤2:检查Z相中断配置
void z_phase_debug_init(void) {
    // 配置PA2为外部中断,上升沿触发
    GPIO_InitTypeDef gpio = {
        .Pin  = GPIO_PIN_2,
        .Mode = GPIO_MODE_IT_RISING,
        .Pull = GPIO_PULLUP
    };
    HAL_GPIO_Init(GPIOA, &gpio);
    HAL_NVIC_SetPriority(EXTI2_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(EXTI2_IRQn);
}

// 步骤3:在Z相中断中打印调试信息
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == GPIO_PIN_2) {
        printf("Z phase triggered! Count=%lld\r\n", encoder.total_count);
        homing.z_triggered = true;
    }
}

// 步骤4:增大归零超时时间(默认5秒可能不够)
homing.timeout_ms = 10000;  // 增大到10秒

// 步骤5:降低归零速度(Z相脉冲宽度窄,速度过快可能错过)
homing.search_speed_rpm = 20.0f;  // 降低到20 RPM

问题6:位置控制超调严重

现象:电机到达目标位置后继续运动,超过目标后再反向,来回振荡。

原因:PID参数不合适(kp过大、ki过大、kd不足)或速度前馈增益过大。

调试方法

// 通过串口实时调整PID参数(无需重新编译)
void process_pid_command(const char *cmd) {
    float kp, ki, kd;
    if (sscanf(cmd, "pid p %f %f %f", &kp, &ki, &kd) == 3) {
        pos_pid.kp = kp;
        pos_pid.ki = ki;
        pos_pid.kd = kd;
        pid_reset(&pos_pid);
        printf("Position PID: kp=%.3f ki=%.3f kd=%.3f\r\n", kp, ki, kd);
    } else if (sscanf(cmd, "pid v %f %f %f", &kp, &ki, &kd) == 3) {
        vel_pid.kp = kp;
        vel_pid.ki = ki;
        vel_pid.kd = kd;
        pid_reset(&vel_pid);
        printf("Velocity PID: kp=%.3f ki=%.3f kd=%.3f\r\n", kp, ki, kd);
    }
}

// 超调问题的典型解决方案:
// 1. 减小位置环kp(从0.8降到0.4)
// 2. 增大位置环kd(加入微分抑制超调)
// 3. 减小速度环ki(减少积分超调)
// 4. 在接近目标时降低速度限幅(减速区间)

测试与验证

编码器功能验证测试

// 测试1:方向验证
// 手动正转电机,计数应增加;反转,计数应减少
void test_encoder_direction(void) {
    printf("=== Direction Test ===\r\n");
    printf("Manually rotate motor FORWARD, count should INCREASE\r\n");
    HAL_Delay(3000);
    printf("Count: %lld\r\n", encoder.total_count);

    printf("Manually rotate motor BACKWARD, count should DECREASE\r\n");
    HAL_Delay(3000);
    printf("Count: %lld\r\n", encoder.total_count);
}

// 测试2:分辨率验证
// 手动转动一圈,计数应等于CPR(4000)
void test_encoder_resolution(void) {
    encoder_reset_position(&encoder);
    printf("=== Resolution Test ===\r\n");
    printf("Rotate motor exactly ONE full revolution, then press Enter\r\n");
    // 等待用户输入...
    printf("Count: %lld (expected: %d)\r\n",
           encoder.total_count, ENCODER_CPR);
    printf("Error: %.2f%%\r\n",
           fabsf((float)encoder.total_count - ENCODER_CPR) / ENCODER_CPR * 100.0f);
}

// 测试3:速度精度验证
// 以已知转速运行电机,验证测速精度
void test_speed_accuracy(float target_rpm) {
    printf("=== Speed Accuracy Test ===\r\n");
    printf("Running at %.1f RPM for 5 seconds...\r\n", target_rpm);

    // 设置开环PWM(需要预先标定PWM-速度关系)
    // motor_set_pwm(rpm_to_pwm(target_rpm));
    HAL_Delay(2000);  // 等待速度稳定

    float sum = 0;
    int   n   = 0;
    uint32_t start = HAL_GetTick();
    while (HAL_GetTick() - start < 3000) {
        sum += encoder_get_speed(&encoder);
        n++;
        HAL_Delay(10);
    }
    float avg_rpm = sum / n;
    printf("Measured: %.2f RPM, Error: %.2f%%\r\n",
           avg_rpm, fabsf(avg_rpm - target_rpm) / target_rpm * 100.0f);
}

延伸阅读

知识系统内部链接

进阶主题

FOC(磁场定向控制): - 无刷电机的高性能控制方案 - 需要高分辨率编码器(>2000线)或磁编码器(AS5048A) - 参考:ST AN5397《STM32 FOC SDK用户手册》

EtherCAT编码器接口: - 工业伺服系统的标准接口 - 支持EnDat 2.2、BiSS-C、HIPERFACE等协议 - 参考:Beckhoff EL5101编码器接口模块

软件编码器(QSPI/SPI磁编码器): - AS5048A:14位绝对角度,SPI接口,适合无刷电机FOC - MA730:14位,SPI,-40~125°C,适合工业场合 - TLE5012B:GMR磁阻传感器,15位,SPI/SSC接口

参考资料

  1. STM32F4 Reference Manual (RM0090) — Timer Encoder Interface Mode,第14章
  2. 详细描述TIM2/TIM5 32位编码器模式寄存器配置
  3. ST Application Note AN4013 — STM32 timer overview
  4. 各定时器功能对比,编码器模式最佳实践
  5. 《现代电机控制技术》 — 王兆安,机械工业出版社
  6. 第8章:位置/速度传感器,第10章:数字PID控制
  7. 《机器人学导论》 — John J. Craig,机械工业出版社
  8. 里程计章节:编码器在移动机器人中的应用
  9. AM26LS32 Datasheet — Texas Instruments
  10. RS422差分接收器,编码器长距离传输方案
  11. AS5048A Datasheet — ams AG
  12. 14位磁编码器,SPI/PWM接口,适合无刷电机
  13. 《传感器原理及工程应用》 — 郭长城,电子工业出版社
  14. 第6章:位移与速度传感器
  15. Ziegler-Nichols PID Tuning Method — J.G. Ziegler, N.B. Nichols (1942)
  16. 经典PID参数整定方法,适用于速度/位置控制
  17. 《嵌入式实时操作系统FreeRTOS原理与实践》 — 刘火良
  18. 多任务环境下编码器数据共享与同步
  19. GRBL开源固件 — github.com/gnea/grbl
    • 基于AVR/STM32的步进电机控制,包含编码器反馈实现参考