第08章 PID算法电机控制
约 4211 字大约 14 分钟
2026-01-26
8.1 编码器测量电机转速
8.1.1 编码器介绍

其中编码的的A相和B相 ,用来传输编码器的计数信号。

- 增量编码器会输出两路方波(A/B相),有 90° 相位差。
- Timer 在 Encoder Mode 下会自动检测这两路信号的边沿,并判断转动方向。
- 编码每4000个方波信号代表电机旋转一周。
8.1.2 MCU定时器的编码器模式
STM32 单片机的定时器的编码器模式,是专门为增量式正交编码器设计的硬件接口,能自动识别旋转方向并计数,无需软件轮询或中断处理,非常适合电机测速、位置反馈等应用。
编码器输出 A、B 两相脉冲,相位差 90°(正交)。
当 A 相超前 B 相 90° 时,硬件自动判断为正转,计数器 CNT 递增;
当 B 相超前 A 相 90° 时,判断为反转,CNT 递减。
整个过程由定时器内部状态机完成,只需读取 CNT 值即可获取位置和方向信息。从模式控制寄存器(TIMx_SMCR):





编码器模式详解 :
| 模式名称 | 计数触发条件 | 方向判断依据 | 关键特点与工作逻辑 |
|---|---|---|---|
| 编码器模式 1 | 在 TI2 (B相) 的有效边沿 | 根据 TI1 (A相) 的电平状态 | 1.仅在B相的跳变沿计数。 2.若A相为高电平,则向上计数;若为低电平,则向下计数 3.每个正交信号周期产生2次计数(2倍频) |
| 编码器模式 2 | 在 TI1 (A相) 的有效边沿 | 根据 TI2 (B相) 的电平状态 | 1. 仅在A相的跳变沿计数 2. 若B相为高电平,则向下计数; 若为低电平, 则向上计数 3. 每个正交信号周期产生2次计数(2倍频) |
| 编码器模式 3 | 在 TI1 (A相) 和 TI2 (B相) 的所有有效边沿 | 根据 “另一个信号” 的电平状态 | 1. 在A、B两相信号的每一个跳变沿都检查并计数 2. 计数方向: 当触发来自A相边沿时,方向由B相电平决定 (同模式2) 当触发来自B相边沿时,方向由A相电平决定(同模式1) 3.每个正交信号周期产生4次计数(4倍频),分辨率最高,是最常用的模式。 |
8.1.3 开发实现
① 原理图


② CubeMX设置
在CubeMax中添加定时器TIM4,打开更新中断。注意:不要设置分频系数,频率过低会发出很大的噪音。

添加定时器TIM3,用于测速,并打开更新中断:

③ 代码
Int_Encoder.h
#ifndef __INT_ENCODER_H__
#define __INT_ENCODER_H__
#include <stdio.h>
#include "tim.h"
#include "Com_Global.h"
/**
* @brief 启动编码器
*
*/
void Int_Encoder_Start(void);
/**
* @brief 停止编码器
*
*/
void Int_Encoder_Stop(void);
/**
* @brief 获取编码器计数
*
* @return uint32_t 编码器计数
*/
uint32_t Int_Encoder_GetCount(void);
/**
* @brief 编码器模式下定时器更新中断触发后调用的回调函数
*
*/
void Int_Encoder_UpdateCallback(void);
#endif /* __INT_ENCODER_H__ */Int_Encoder.c
#include "Int_Encoder.h"
// 定义静态全局变量:记录TIM4的溢出次数
uint32_t gs_overflow_count = 0;
/**
* @brief 启动编码器
*
*/
void Int_Encoder_Start(void)
{
// 启动TIM4的编码器模式
HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL);
// 启动TIM4的更新中断, 清除中断标志位
HAL_TIM_Base_Start_IT(&htim4);
// TIM4启动之后可以稳定一会
HAL_Delay(100);
// 清零溢出次数
gs_overflow_count = 0;
// 清零TIM4的计数器
__HAL_TIM_SET_COUNTER(&htim4, 0);
}
/**
* @brief 停止编码器
*
*/
void Int_Encoder_Stop(void)
{
// 停止TIM4的编码器模式
HAL_TIM_Encoder_Stop(&htim4, TIM_CHANNEL_ALL);
// 停止TIM4的更新中断
HAL_TIM_Base_Stop_IT(&htim4);
}
/**
* @brief 获取编码器计数
*
* @return uint32_t 编码器计数
*/
uint32_t Int_Encoder_GetCount(void)
{
// 如果是正转 向下计数, 注意:向下计数时定时器一上电就会溢出1次
if (g_target_angle > 0)
{
// 计数值 = 溢出次数 * 65536 - 当前计数器值
return gs_overflow_count * 65536 - __HAL_TIM_GET_COUNTER(&htim4);
}
// 如果是反转 向上计数
else
{
// 计数值 = 溢出次数 * 65536 + 当前计数器值
return gs_overflow_count * 65536 + __HAL_TIM_GET_COUNTER(&htim4);
// uint32_t val = gs_overflow_count * 65536 + __HAL_TIM_GET_COUNTER(&htim4);
// printf("gs_overflow_count:%d val:%d\n",gs_overflow_count, val);
// return val;
}
}
/**
* @brief 编码器模式下定时器更新中断触发后调用的回调函数
*
*/
void Int_Encoder_UpdateCallback(void)
{
// 溢出次数增加
gs_overflow_count++;
}Int_Motor.h
添加头文件包含:
#include "Int_Encoder.h"Int_Motor.c
改为如下代码:
#include "Int_Motor.h"
// 定义静态全局变量:保存电机已经旋转角度对应的ITM1更新周期数
static uint32_t gs_current_angle_update_periods = 0;
// 定义静态全局变量:保存电机目标旋转角度对应的TIM1更新周期数
static uint32_t gs_target_angle_update_periods = 0;
// 定义静态全局变量:保存电机目标速度对应的TIM1更新周期数(周期数/s)
static uint32_t gs_target_speed_update_periods = 0;
// 定义静态全局变量:保存电机当前速度对应的TIM1更新周期数(周期数/s)
static uint32_t gs_current_speed_update_periods = 0;
// 定义静态全局变量:保存加速度所需时间(用周期数表示)
static uint32_t gs_acc_speed_update_periods = 0;
// 定义静态全局变量:保存是否需要匀速阶段, 1表示需要;0表示不需要
static uint8_t gs_need_const_speed_stage = 0;
// 定义静态全局变量:记录编码器的上一次计数值
static uint32_t gs_last_encoder_count = 0;
// 静态函数:计算加速度所需时间(用周期数表示)
static void Int_Motor_CalcAccSpeedUpdatePeriods(void)
{
/*
公式: V1? - V0? = 2as
计算s;
s = (V1? - V0?) / 2 / a
*/
gs_acc_speed_update_periods = (gs_target_speed_update_periods * gs_target_speed_update_periods - gs_current_speed_update_periods * gs_current_speed_update_periods) / 2 / MOTOR_ACC_SPEED;
printf("加速度所需周期数:%d\n", gs_acc_speed_update_periods);
// 判断是否需要允许阶段; 如果加速度所需周期数大于等于总周期数的一半,不需要匀速阶段
if (gs_acc_speed_update_periods >= gs_target_angle_update_periods / 2)
{
gs_need_const_speed_stage = 0;
}
else
{
gs_need_const_speed_stage = 1;
}
}
// 静态函数:实时更新速度
static void Int_Motor_UpdateSpeed(void)
{
// 定义变量,保存下一个速度
uint32_t v1 = 0;
// 加速阶段: 已经旋转的周期数小于加速度所需周期数
if (gs_current_angle_update_periods < gs_acc_speed_update_periods)
{
// 公式:V1? - V0? = 2as; 这里s取1,该函数一个周期调用一次
v1 = sqrt(2 * MOTOR_ACC_SPEED * 1 + gs_current_speed_update_periods * gs_current_speed_update_periods);
}
// 匀速阶段:条件1:已经旋转的周期数 >= 加速所需周期数; 只要能执行到else,该条件一定是满足的
// 条件2: 剩余周期数 > 减速所需周期数(等于加速所需周期数)
// 条件3:需要匀速阶段
else if (gs_target_angle_update_periods - gs_current_angle_update_periods > gs_acc_speed_update_periods &&
gs_need_const_speed_stage)
{
// 匀速阶段,速度保持不变
v1 = gs_target_speed_update_periods;
}
// 减速阶段
else
{
// 公式: V0? - V1? = 2as; 这里s取1,该函数一个周期调用一次
v1 = sqrt(gs_current_speed_update_periods * gs_current_speed_update_periods - 2 * MOTOR_ACC_SPEED * 1);
}
// 限制V1范围
if (v1 > gs_target_speed_update_periods)
{
v1 = gs_target_speed_update_periods;
}
else if (v1 < MIN_TARGET_SPEED * 3200 / 60)
{
v1 = MIN_TARGET_SPEED * 3200 / 60;
}
// 更新当前速度
gs_current_speed_update_periods = v1;
//printf("Speed:%d\n", gs_current_speed_update_periods);
// 设置TIM1的自动重装载值
__HAL_TIM_SET_AUTORELOAD(&htim1, 1000000 / gs_current_speed_update_periods - 1);
}
/**
* @brief 启动电机
*
*/
void Int_Motor_Start(void)
{
// 1. 设置电机的旋转方向,目标角度是正数,设置为顺时针;目标角度是负数,设置为逆时针
if (g_target_angle > 0)
{
// 设置 MOTOR_DIR 为高电平
HAL_GPIO_WritePin(MOTOR_DIR_GPIO_Port, MOTOR_DIR_Pin, GPIO_PIN_SET);
}
else
{
// 设置 MOTOR_DIR 为低电平
HAL_GPIO_WritePin(MOTOR_DIR_GPIO_Port, MOTOR_DIR_Pin, GPIO_PIN_RESET);
}
// 2. 将目标速度(转/分)转换为对应的周期数(周期数/秒)
gs_target_speed_update_periods = g_target_speed * 3200 / 60;
// 3. 设置最小速度作为当前速度(初始速度); 在转为对应的周期数
g_current_speed = MIN_TARGET_SPEED;
gs_current_speed_update_periods = g_current_speed * 3200 / 60;
// 4. 设置TIM1的自动重装载值
__HAL_TIM_SET_AUTORELOAD(&htim1, 1000000 / gs_current_speed_update_periods - 1);
// 5. 计算目标角度对应的TIM1更新周期数
gs_target_angle_update_periods = fabs(g_target_angle) / 360 * 3200;
// 6. 清零已经旋转的角度对应的ITM1更新周期数
gs_current_angle_update_periods = 0;
// 7. 计算加速度所需时间(用周期数表示) !!!这一步一定一定一定要等待计算完gs_target_angle_update_periods
Int_Motor_CalcAccSpeedUpdatePeriods();
// 启动编码器
Int_Encoder_Start();
// 清零编码器的计数值
gs_last_encoder_count = 0;
// 启动TIM3更新中断
HAL_TIM_Base_Start_IT(&htim3);
// 8. 启动电机,设置 MOTOR_EN 为高电平
HAL_GPIO_WritePin(MOTOR_EN_GPIO_Port, MOTOR_EN_Pin, GPIO_PIN_SET);
// 9. 启动TIM1定时器的更新中断和TIM1_Channel1输出比较
HAL_TIM_Base_Start_IT(&htim1);
HAL_TIM_OC_Start_IT(&htim1, TIM_CHANNEL_1);
}
/**
* @brief 停止电机
*
*/
void Int_Motor_Stop(void)
{
// 停止电机,设置 MOTOR_EN 为低电平
HAL_GPIO_WritePin(MOTOR_EN_GPIO_Port, MOTOR_EN_Pin, GPIO_PIN_RESET);
// 停止TIM1定时器的更新中断和TIM1_Channel1输出比较
HAL_TIM_Base_Stop_IT(&htim1);
HAL_TIM_OC_Stop_IT(&htim1, TIM_CHANNEL_1);
// 强制清零当前速度
gs_current_speed_update_periods = 0;
g_current_speed = 0.0;
// 停止编码器
Int_Encoder_Stop();
// 停止TIM3更新中断
HAL_TIM_Base_Stop_IT(&htim3);
}
/**
* @brief 设置电机速度
*
*/
void Int_Motor_SetSpeed(void)
{
/*
定时器溢出(更新)2次,产生1个上升沿
转1圈,需要需要200*8个上升沿;合计需要3200个更新周期
TIM1
PSC=71; 1us计1个数
根据转速计算ARR值,转速用rpm表示(转速:转/分钟)
一圈所用时间: (1 / rpm) * 60 * 1000 * 1000 us
定时器1次更新(溢出)所用时间: (1 / rpm) * 60 * 1000 * 1000 / 3200 us
定时器ARR的值:(1 / rpm) * 60 * 1000 * 1000 / 3200 - 1
*/
// 设置当前速度为目标速度
g_current_speed = g_target_speed;
// 计算用周期数表示的速度
gs_current_speed_update_periods = g_current_speed * 3200 / 60;
gs_target_speed_update_periods = g_target_speed * 3200 / 60;
// 设置定时器ARR值
__HAL_TIM_SET_AUTORELOAD(&htim1, 1000000 / gs_current_speed_update_periods - 1);
}
/**
* @brief 获取已经旋转的角度
*
*/
void Int_Motor_GetCurrentAngle(void)
{
// 根据周期数计算已经旋转的角度
g_current_angle = gs_current_angle_update_periods / 3200.0f * 360.0f;
// 如果是逆时针转,将g_current_angle转为负数
if (g_target_angle < 0)
{
g_current_angle = -g_current_angle;
}
}
/**
* @brief 获取当前速度,单位转/分
*
*/
void Int_Motor_GetCurrentSpeed(void)
{
// // 根据当前速度对应的周期数计算当前速度
// g_current_speed = gs_current_speed_update_periods / 3200.0f * 60.0f;
// 获取当前编码器的计数值
uint32_t current_encoder_count = Int_Encoder_GetCount();
// 计算当前计数值和上一次计数值的差值
int32_t diff_encoder_count = current_encoder_count - gs_last_encoder_count;
// printf("last:%d,current:%d,diff:%d\n",gs_last_encoder_count,current_encoder_count,diff_encoder_count);
// 根据差值计算电机速度
g_current_speed = diff_encoder_count / 4000.0 * (60000 / 50);
// 更新上一次计数值
gs_last_encoder_count = current_encoder_count;
}
/**
* @brief TIM1更新中断触发后被调用的函数
*
*/
void Int_Motor_UpdateCallback(void)
{
// 增加已经旋转角度对应的ITM1更新周期数
gs_current_angle_update_periods++;
// 实时更新速度
Int_Motor_UpdateSpeed();
// 如果已经旋转角度对应的ITM1更新周期数大于等于目标角度对应的TIM1更新周期数
if (gs_current_angle_update_periods >= gs_target_angle_update_periods)
{
// 停止电机
Int_Motor_Stop();
}
}main.c
在定时器更新中断的弱函数中调用 Int_Motor_GetCurrentSpeed():
/* 代码省略 ... */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* USER CODE BEGIN Callback 0 */
// 如果是TIM1
if (htim->Instance == TIM1) {
// 调用电机更新回调函数
Int_Motor_UpdateCallback();
}
/* USER CODE END Callback 0 */
if (htim->Instance == TIM2) {
HAL_IncTick();
}
/* USER CODE BEGIN Callback 1 */
// 如果是TIM3
if (htim->Instance == TIM3) {
// 实时获取速度
Int_Motor_GetCurrentSpeed();
}
// 如果是TIM4
if (htim->Instance == TIM4) {
// 调用编码器更新回调函数
Int_Encoder_UpdateCallback();
}
/* USER CODE END Callback 1 */
}
/* 代码省略 ... */8.2 PID 算法控制转速
8.2.1 PID算法介绍

有助于理解的网站:https://www.longluo.me/projects/pid/
相关专业术语:
系统性能与误差术语:
稳态误差/静态偏差:系统稳定后,被控量与设定值之间仍存在的微小恒定偏差,是衡量控制精度的重要指标。
动态偏差:系统过渡过程中某一时刻被控量与设定值之间的偏差,反映系统动态响应中的波动情况。
超调:系统响应超过设定值的现象,通常由比例或积分作用过强引起。
振荡:系统在设定值附近来回波动的现象,与 PID 参数整定不当有关。
回调:系统受到干扰后,被控量偏离设定值,随后在控制器作用下向设定值方向恢复的过程。
控制结构与算法术语:
闭环控制:通过传感器采集被控量反馈给控制器,形成闭环回路,PID 属于典型的闭环控制算法。
开环控制:控制输出不依赖被控量反馈,无法自动修正偏差,与闭环控制相对。
位置式 PID:直接计算控制量的绝对值,常用于位置环控制。
增量式 PID:计算控制量的增量,常用于速度环控制,抗干扰能力强。
串级 PID:包含主回路和副回路两个控制环,主控制器输出作为副控制器的设定值,用于抑制干扰、提高控制精度。
并联 PID:多个 PID 控制器独立计算控制量,再合并输出,用于多变量解耦控制。
模糊 PID:根据误差和误差变化率动态调整 PID 参数,适用于控制范围较大的场合。8.2.2 开发实现
① 软件流程图

② 代码
Com_PID.h
#ifndef __COM_PID_H__
#define __COM_PID_H__
// 定义PID的结构体类型
typedef struct
{
float target; // 目标值
float current; // 当前值
float result; // 输出值
float last_error; // 上一次误差
float error_sum; // 误差和,用于积分项计算
float Kp; // 比例
float Ki; // 积分
float Kd; // 微分
float Kt; // 单位时间
} Com_PID_Typedef;
/**
* @brief 计算PID输出,需要被轮询调用
*
* @param pid PID结构体指针变量
*/
void Com_PID_Compute(Com_PID_Typedef *pid);
#endif /* __COM_PID_H__ */Com_PID.c
#include "Com_PID.h"
/**
* @brief 计算PID输出,需要被轮询调用
*
* @param pid PID结构体变量
*/
void Com_PID_Compute(Com_PID_Typedef *pid)
{
// 1. 计算误差值
float error = pid->target - pid->current;
// 2. 积分项累加
pid->error_sum += error;
// 3. PID 计算
pid->result = pid->Kp * error + pid->Ki * pid->error_sum * pid->Kt + pid->Kd * (error - pid->last_error) / pid->Kt;
// 4. 更新上一次误差
pid->last_error = error;
}Int_Motor.h
添加头文件包含:
#include "Com_PID.h"Int_Motor.c
修改为如下代码:
#include "Int_Motor.h"
// 定义静态全局变量:保存电机已经旋转角度对应的ITM1更新周期数
static uint32_t gs_current_angle_update_periods = 0;
// 定义静态全局变量:保存电机目标旋转角度对应的TIM1更新周期数
static uint32_t gs_target_angle_update_periods = 0;
// 定义静态全局变量:记录编码器的上一次计数值
static uint32_t gs_last_encoder_count = 0;
// 定义静态全局结构体变量:作为PID对象
static Com_PID_Typedef gs_pid = {
.Kp = 0.01f,
.Ki = 0.01f,
.Kd = 0.0f,
.Kt = 1,
.target = 0.0f,
.current = 0.0f,
.result = 0.0f, // 单位 转/分
.error_sum = 0.0f,
.last_error = 0.0f,
};
// 静态函数:实时更新速度
static void Int_Motor_UpdateSpeed(void)
{
// 1. 设置目标速度
gs_pid.target = g_target_speed;
// 2. 设置当前速度
gs_pid.current = g_current_speed;
// 3. 进行PID计算
Com_PID_Compute(&gs_pid);
// 4. 对PID结果进行范围限制
if (gs_pid.result > g_target_speed)
{
gs_pid.result = g_target_speed;
}
else if (gs_pid.result < MIN_TARGET_SPEED)
{
gs_pid.result = MIN_TARGET_SPEED;
}
printf("Result:%f\n", gs_pid.result);
// 5. 设置电机速度
Int_Motor_SetSpeed();
}
/**
* @brief 启动电机
*
*/
void Int_Motor_Start(void)
{
// 1. 设置电机的旋转方向,目标角度是正数,设置为顺时针;目标角度是负数,设置为逆时针
if (g_target_angle > 0)
{
// 设置 MOTOR_DIR 为高电平
HAL_GPIO_WritePin(MOTOR_DIR_GPIO_Port, MOTOR_DIR_Pin, GPIO_PIN_SET);
}
else
{
// 设置 MOTOR_DIR 为低电平
HAL_GPIO_WritePin(MOTOR_DIR_GPIO_Port, MOTOR_DIR_Pin, GPIO_PIN_RESET);
}
// 5. 计算目标角度对应的TIM1更新周期数
gs_target_angle_update_periods = fabs(g_target_angle) / 360 * 3200;
// 6. 清零已经旋转的角度对应的ITM1更新周期数
gs_current_angle_update_periods = 0;
// 启动编码器
Int_Encoder_Start();
// 清零编码器的计数值
gs_last_encoder_count = 0;
// 启动TIM3更新中断
HAL_TIM_Base_Start_IT(&htim3);
// 8. 启动电机,设置 MOTOR_EN 为高电平
HAL_GPIO_WritePin(MOTOR_EN_GPIO_Port, MOTOR_EN_Pin, GPIO_PIN_SET);
// 9. 启动TIM1定时器的更新中断和TIM1_Channel1输出比较
HAL_TIM_Base_Start_IT(&htim1);
HAL_TIM_OC_Start_IT(&htim1, TIM_CHANNEL_1);
}
/**
* @brief 停止电机
*
*/
void Int_Motor_Stop(void)
{
// 停止电机,设置 MOTOR_EN 为低电平
HAL_GPIO_WritePin(MOTOR_EN_GPIO_Port, MOTOR_EN_Pin, GPIO_PIN_RESET);
// 停止TIM1定时器的更新中断和TIM1_Channel1输出比较
HAL_TIM_Base_Stop_IT(&htim1);
HAL_TIM_OC_Stop_IT(&htim1, TIM_CHANNEL_1);
// 停止编码器
Int_Encoder_Stop();
// 停止TIM3更新中断
HAL_TIM_Base_Stop_IT(&htim3);
// 强制清零当前速度
g_current_speed = 0.0;
// 强制清零gs_pid中累加误差和上次误差
gs_pid.error_sum = 0.0f;
gs_pid.last_error = 0.0f;
}
/**
* @brief 设置电机速度
*
*/
void Int_Motor_SetSpeed(void)
{
// 根据PID的结果,设置TIM1的ARR值
__HAL_TIM_SET_AUTORELOAD(&htim1, (1 / gs_pid.result) * 60 * 1000 * 1000 / 3200 - 1);
}
/**
* @brief 获取已经旋转的角度
*
*/
void Int_Motor_GetCurrentAngle(void)
{
// 根据周期数计算已经旋转的角度
g_current_angle = gs_current_angle_update_periods / 3200.0f * 360.0f;
// 如果是逆时针转,将g_current_angle转为负数
if (g_target_angle < 0)
{
g_current_angle = -g_current_angle;
}
}
/**
* @brief 获取当前速度,单位转/分
*
*/
void Int_Motor_GetCurrentSpeed(void)
{
// // 根据当前速度对应的周期数计算当前速度
// g_current_speed = gs_current_speed_update_periods / 3200.0f * 60.0f;
// 获取当前编码器的计数值
uint32_t current_encoder_count = Int_Encoder_GetCount();
// 计算当前计数值和上一次计数值的差值
int32_t diff_encoder_count = current_encoder_count - gs_last_encoder_count;
// 根据差值计算电机速度
g_current_speed = diff_encoder_count / 4000.0 * (60000 / 50);
// 更新上一次计数值
gs_last_encoder_count = current_encoder_count;
}
/**
* @brief TIM1更新中断触发后被调用的函数
*
*/
void Int_Motor_UpdateCallback(void)
{
// 增加已经旋转角度对应的ITM1更新周期数
gs_current_angle_update_periods++;
// 实时更新速度
Int_Motor_UpdateSpeed();
// 如果已经旋转角度对应的ITM1更新周期数大于等于目标角度对应的TIM1更新周期数
if (gs_current_angle_update_periods >= gs_target_angle_update_periods)
{
// 停止电机
Int_Motor_Stop();
}
}