1. PID控制的本质与工程必要性
在嵌入式运动控制系统中,PID(Proportional-Integral-Derivative)并非一种玄学调参技巧,而是一种基于物理系统动态特性的闭环反馈控制范式。其核心价值在于解决开环控制无法克服的系统不确定性问题——这种不确定性既来自外部扰动(如地面摩擦系数变化、负载突变、坡度变化),也源于系统自身参数漂移(如电机绕组温升导致内阻变化、供电电压波动)。
以直流减速电机为例,其转速与施加的PWM占空比之间并不存在理想的线性关系。当电机空载运行时,20%占空比可能对应1000 RPM;但当同一电机驱动小车在水泥地面上匀速前进时,20%占空比可能仅能维持800 RPM;若小车开始爬坡,该占空比甚至可能使电机停转。造成这一现象的根本原因在于:电机输出的电磁转矩必须持续克服负载阻力矩才能维持恒定转速,而负载阻力矩是高度时变且不可预知的。开环控制将占空比视为对转速的直接映射,本质上忽略了“力-速度”这一动力学转换环节,因此必然失效。
PID控制器通过引入实时误差反馈,构建了一个动态补偿机制。其数学表达式为:
$$ u(t) = K_p e(t) + K_i \int_0^t e(\tau) d\tau + K_d \frac{de(t)}{dt} $$
其中 $ e(t) = r(t) - y(t) $ 是设定值 $ r(t) $ 与实际输出 $ y(t) $ 之间的瞬时误差。该公式揭示了PID的三层补偿逻辑:
-
比例项(P)
提供即时响应,其增益 $ K_p $ 决定了系统对当前误差的“敏感度”。$ K_p $ 过小则响应迟钝,过大则易引发超调与振荡。
-
积分项(I)
消除稳态误差,通过累积历史误差,强制系统在长时间尺度上收敛至设定值。其增益 $ K_i $ 控制积分作用的强弱,过大会导致积分饱和与响应 sluggish。
-
微分项(D)
预测未来趋势,通过对误差变化率的抑制,有效阻尼系统振荡,提升动态稳定性。$ K_d $ 过大则会放大测量噪声,恶化控制品质。
在STM32平台上实现一个有效的速度环PID控制器,其技术挑战不在于算法本身,而在于整个闭环链路的工程实现质量:从高精度、低延迟的速度测量,到确定性、低抖动的PWM输出,再到抗干扰的信号调理与鲁棒的参数整定策略。任何一个环节的缺陷,都会被PID算法无情地放大,最终表现为电机转速的剧烈抖动、缓慢爬升或持续振荡。
2. 速度闭环的硬件基石:编码器选型与接口设计
构建一个可靠的速度闭环,首要任务是选择并正确接入一个高信噪比、低延迟的速度传感器。在成本、精度与鲁棒性综合考量下,增量式光电编码器(Quadrature Encoder)是直流减速电机应用中最主流的选择。其核心优势在于:无需绝对位置参考即可精确解算旋转方向与速度,且原生支持STM32芯片内置的定时器正交编码器接口(QEI),可实现硬件级计数,极大降低CPU负担与测量延迟。
2.1 编码器工作原理与信号特征
一个典型的增量式光电编码器包含一个带有等距透光缝隙的码盘和一对空间上错开90°(即四分之一周期)的光电接收单元,分别输出A相与B相信号。当电机轴带动码盘旋转时,两路信号产生相位差为90°的方波序列。
其工作机理可分解为两个维度:
-
速度测量
:单路信号(如A相)的脉冲频率 $ f $ 直接正比于电机转速 $ n $。若编码器线数为 $ N $(即每转产生 $ N $ 个A相脉冲),则转速计算公式为 $ n = \frac{60 \cdot f}{N} $(单位:RPM)。此方法仅需单路信号,但无法判别方向。
-
方向判别
:利用A、B两相信号的相位关系。当A相上升沿领先B相时(即A相先于B相跳变),判定为正向旋转;反之,B相上升沿领先A相,则为反向旋转。这种四倍频(X4)模式将有效分辨率提升至 $ 4N $ 脉冲/转,显著提高了低速下的测速精度与分辨率。
2.2 STM32正交解码器的硬件配置
STM32F1系列MCU(如常见的STM32F103C8T6)的通用定时器(TIM2, TIM3, TIM4, TIM5)均支持正交编码器模式。该模式将定时器的两个输入通道(TI1与TI2)配置为专用的编码器接口,硬件自动完成以下关键操作:
-
四倍频计数
:对A、B两相的所有上升沿与下降沿进行计数,将原始脉冲频率提升4倍。
-
方向识别
:根据A、B相的相位关系,自动更新计数器的增减方向(DIR位)。
-
自动重装载(ARR)
:当计数器溢出或下溢时,自动加载重装载寄存器(ARR)的值,并触发更新事件(UEV),为速度计算提供精确的时间基准。
在本案例中,选用TIM3定时器,其通道1(CH1)与通道2(CH2)分别映射至GPIOB引脚PB6与PB7。此引脚组合是STM32F103标准外设库(SPL)与HAL库中为TIM3 QEI模式预定义的复用功能,无需额外的引脚重映射(Remap)操作,确保了配置的简洁性与可靠性。
2.3 编码器电气接口与电源设计
编码器电机是一个机电一体化模块,其内部由独立的电机绕组与编码器电路构成,二者电气隔离。典型接线包括:
-
电机部分
:
M+
与
M-
为直流电机驱动端子,额定工作电压范围通常为11V–16V(如本例中的3S/4S锂电池组)。
-
编码器部分
:
VCC
(5V)、
GND
、
A
、
B
四根线。其中
VCC
与
GND
为编码器光电管及逻辑电路供电,
A
、
B
为差分输出信号线。
电源设计是系统稳定性的第一道防线。若直接使用12V电池为编码器供电,将导致其内部LDO严重发热甚至损坏。因此,必须设计一个高效的5V稳压电路。本方案采用LM2596开关降压模块,其优点在于:
- 输入电压范围宽(4.5V–40V),完美兼容3S(10.8V–12.6V)与4S(14.4V–16.8V)锂电池。
- 效率高达85%以上,远优于线性稳压器(如7805),大幅降低热耗散。
- 输出纹波低(<50mV),为编码器提供纯净的电源,避免因电源噪声导致的误计数。
PCB布局上,LM2596的输入/输出电容应尽可能靠近芯片引脚放置,并确保GND走线宽大低阻。编码器的
A
、
B
信号线应采用双绞线或相邻平行布线,以抑制共模噪声。在MCU端,可在PB6、PB7引脚后串联100Ω电阻,并在每个信号线与GND之间并联10nF陶瓷电容,构成简单的RC低通滤波器,有效滤除高频毛刺,同时不影响10kHz量级的编码器信号边沿陡度。
3. STM32正交解码器的软件初始化与驱动
在STM32 HAL库框架下,正交解码器的初始化是一个严谨的多步骤过程,其目标是将TIM3配置为一个高精度、低延迟、自动处理方向的硬件计数器。任何一步的疏忽都可能导致计数错误、方向颠倒或中断丢失。
3.1 定时器基础时钟配置
正交解码器的计数精度直接依赖于其时钟源。TIM3挂载在APB1总线上,其默认时钟源为APB1时钟(PCLK1)。在本项目中,系统主频(SYSCLK)配置为72MHz,APB1总线预分频器(PRES)设置为2,因此PCLK1 = 72MHz / 2 = 36MHz。此即为TIM3的输入时钟频率。
// 确保APB1总线时钟已使能
__HAL_RCC_TIM3_CLK_ENABLE();
3.2 GPIO引脚初始化
PB6与PB7必须配置为复用推挽输出(Alternate Function Push-Pull),并启用内部上拉电阻(Pull-Up)。上拉电阻至关重要,它确保在编码器未连接或信号线悬空时,输入引脚处于确定的高电平状态,防止因浮空输入导致的随机翻转与误计数。
// 初始化PB6 (TIM3_CH1) 和 PB7 (TIM3_CH2)
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽
GPIO_InitStruct.Pull = GPIO_PULLUP; // 强制上拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
3.3 定时器QEI模式核心配置
这是整个驱动的核心。TIM3必须被配置为编码器模式,其关键参数如下:
-
编码器模式
:
TIM_ENCODERMODE_TI12
,表示同时使用TI1(PB6)和TI2(PB7)作为编码器输入。
-
预分频器(PSC)
:设置为0,意味着不对输入时钟进行分频,计数器以PCLK1频率(36MHz)进行计数,获得最高时间分辨率。
-
自动重装载值(ARR)
:设置为65535(0xFFFF)。这是16位定时器的最大计数值。选择最大值的原因在于:它为编码器提供了最大的无溢出计数范围,允许电机在极低速下也能积累足够多的脉冲以进行精确计算,同时简化了软件处理逻辑——我们只需关注计数器的当前值(CNT),并在每次读取后手动清零或计算差值,而无需在每次溢出中断中处理复杂的重装载逻辑。对于一个1024线的编码器,四倍频后为4096脉冲/转,65535计数可覆盖约16圈,这在绝大多数应用场景中已足够。
TIM_Encoder_InitTypeDef sConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim3.Instance = TIM3;
htim3.Init.Prescaler = 0; // 无预分频
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 65535; // 自动重装载值
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
sConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 使用TI1和TI2
sConfig.IC1Polarity = TIM_ICPOLARITY_RISING; // TI1上升沿有效
sConfig.IC2Polarity = TIM_ICPOLARITY_RISING; // TI2上升沿有效
sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; // 直接TI1
sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI; // 直接TI2
sConfig.IC1Prescaler = TIM_ICPSC_DIV1; // 输入捕获不分频
sConfig.IC2Prescaler = TIM_ICPSC_DIV1;
sConfig.IC1Filter = 0; // 无滤波
sConfig.IC2Filter = 0;
if (HAL_TIM_Encoder_Init(&htim3, &sConfig) != HAL_OK) {
Error_Handler(); // 错误处理函数
}
3.4 启动计数与数据读取
初始化完成后,调用
HAL_TIM_Encoder_Start()
即可启动硬件计数。此后,定时器将自动根据A、B相的相位关系递增或递减计数器(CNT)的值。
// 启动TIM3的编码器计数
HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
速度计算的关键在于获取“单位时间内”的脉冲数。一个健壮的策略是使用一个高优先级的定时器中断(例如TIM2,周期设为10ms)作为采样时钟。在每次中断服务程序(ISR)中,执行以下原子操作:
1. 读取当前计数值
CNT
。
2. 将
CNT
值写入一个全局变量(如
encoder_count
)。
3. 立即将
CNT
清零(
__HAL_TIM_SET_COUNTER(&htim3, 0)
)。
这样,在主循环中,我们每隔10ms就能得到一个精确的、代表该时间段内脉冲数的整数值。该值即为速度的原始测量值(Raw Speed Value),后续PID算法将直接以此为基础进行运算。
// 在TIM2的中断服务程序中
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
// 原子读取并清零
encoder_count = __HAL_TIM_GET_COUNTER(&htim3);
__HAL_TIM_SET_COUNTER(&htim3, 0);
}
}
4. 速度环PID控制器的嵌入式实现
在嵌入式系统中,PID控制器的实现必须兼顾计算精度、执行效率与实时性。浮点运算虽直观,但在资源受限的Cortex-M3内核上开销巨大;而纯整数运算又易引入量化误差。本方案采用定点数(Q15格式)实现,它在精度与效率间取得了最佳平衡。
4.1 定点数PID算法设计
Q15格式将一个16位有符号整数解释为一个介于-1.0到+0.99997的定点小数,其缩放因子为 $ 2^{-15} $。所有PID参数($ K_p, K_i, K_d $)与中间变量均按此格式存储与运算。
假设:
- 设定值
setpoint
(期望速度)与测量值
measured_value
(
encoder_count
)均为16位整数,单位为“脉冲/10ms”。
- 采样周期
Ts
为10ms。
则PID离散化公式(位置式)可重写为:
$$ u[n] = K_{pQ15} \cdot e[n] + K_{iQ15} \cdot \sum_{k=0}^{n} e[k] \cdot T_s + K_{dQ15} \cdot \frac{e[n] - e[n-1]}{T_s} $$
其中,
K_iQ15
与
K_dQ15
已将采样周期 $ T_s $ 的影响内建于其数值中,从而避免了在实时循环中进行耗时的浮点除法。
4.2 核心PID计算函数
#define PID_Q15_SCALE (32768) // 2^15
typedef struct {
int16_t setpoint; // 设定值 (pulse/10ms)
int16_t measured_value; // 测量值 (pulse/10ms)
int16_t error; // 当前误差
int16_t last_error; // 上一次误差
int32_t integral; // 积分项 (累加,32位防溢出)
int16_t output; // 最终输出 (PWM占空比)
int16_t kp_q15; // 比例增益 (Q15)
int16_t ki_q15; // 积分增益 (Q15)
int16_t kd_q15; // 微分增益 (Q15)
} PID_ControllerTypeDef;
PID_ControllerTypeDef pid_speed = {
.kp_q15 = 2000, // 对应Kp ≈ 0.061
.ki_q15 = 100, // 对应Ki ≈ 0.003
.kd_q15 = 5000 // 对应Kd ≈ 0.153
};
void PID_Speed_Calculate(PID_ControllerTypeDef *pid) {
int32_t temp;
// 1. 计算当前误差
pid->error = pid->setpoint - pid->measured_value;
// 2. 比例项: Kp * error
temp = (int32_t)pid->kp_q15 * pid->error;
pid->output = (int16_t)(temp >> 15); // Q15右移15位得整数
// 3. 积分项: Ki * sum(error) * Ts (Ki已含Ts)
pid->integral += pid->error;
// 积分限幅,防止饱和
if (pid->integral > 32767) pid->integral = 32767;
if (pid->integral < -32768) pid->integral = -32768;
temp = (int32_t)pid->ki_q15 * pid->integral;
pid->output += (int16_t)(temp >> 15);
// 4. 微分项: Kd * (error - last_error) / Ts (Kd已含1/Ts)
temp = (int32_t)pid->kd_q15 * (pid->error - pid->last_error);
pid->output += (int16_t)(temp >> 15);
// 5. 输出限幅: 0-100% PWM
if (pid->output > 1000) pid->output = 1000; // 1000 = 100%
if (pid->output < 0) pid->output = 0;
pid->last_error = pid->error;
}
4.3 PWM输出与电机驱动
PID计算出的
output
值(0–1000)需映射为TIM1(高级定时器)的PWM占空比。TIM1的ARR(自动重装载值)设为999,因此
output
可直接作为比较寄存器(CCR1)的值,实现0%–100%的线性映射。
// 在主循环中,每10ms调用一次
pid_speed.setpoint = 500; // 期望500 pulse/10ms
pid_speed.measured_value = encoder_count;
PID_Speed_Calculate(&pid_speed);
// 更新TIM1 CH1的PWM占空比
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, pid_speed.output);
电机驱动芯片(如L298N)的使能端(EN)连接至TIM1_CH1,IN1与IN2则由GPIO控制方向。当
output > 0
时,置
IN1=1, IN2=0
;当
output < 0
时,置
IN1=0, IN2=1
;
output = 0
则
IN1=IN2=0
(刹车)。
5. 实时监控与调试:串口数据可视化
一个未经验证的PID控制器如同一个盲人骑手。在嵌入式开发中,实时、可视化的数据流是理解系统行为、诊断问题、优化参数的唯一途径。本方案摒弃了低效的printf重定向,采用二进制协议与上位机软件(如FreeMODBUS、Oscilloscope类工具)协同工作。
5.1 高效二进制串口协议设计
为最小化通信开销与MCU负担,定义一个轻量级的二进制帧结构:
-
帧头(2字节)
:
0xAA, 0x55
-
数据长度(1字节)
: 后续数据字节数
-
数据域(N字节)
: 包含
setpoint
(2B)、
measured_value
(2B)、
output
(2B)、
error
(2B)共8字节
-
校验和(1字节)
: 数据域字节异或(XOR)结果
该协议在115200波特率下,每帧传输仅需约1.5ms,远低于10ms的控制周期,确保了数据的实时性与完整性。
5.2 HAL库DMA非阻塞发送
使用HAL库的DMA模式发送,可将数据打包与发送过程完全卸载给硬件,CPU在调用
HAL_UART_Transmit_DMA()
后即可立即返回执行PID计算,无需等待发送完成。
uint8_t tx_buffer[12];
tx_buffer[0] = 0xAA; tx_buffer[1] = 0x55;
tx_buffer[2] = 8; // 数据长度
// 填充数据域...
tx_buffer[10] = checksum;
HAL_UART_Transmit_DMA(&huart1, tx_buffer, 12);
5.3 上位机数据解析与绘图
上位机软件(如本例中提到的“福特加”)负责:
- 接收串口数据流,依据帧头与长度字段进行帧同步。
- 解析出四个16位有符号整数。
- 将
setpoint
、
measured_value
、
output
三个变量绘制在同一坐标系下,形成实时趋势图。
- 提供缩放、平移、导出CSV等功能,便于深入分析超调、调节时间、稳态误差等性能指标。
通过观察图形,工程师可以直观地判断:
- 若
measured_value
曲线始终低于
setpoint
,表明积分作用不足(
Ki
过小)或存在未建模的负载。
- 若
measured_value
出现大幅、高频振荡,表明比例增益过高(
Kp
过大)或微分作用过强(
Kd
过大)。
- 若
output
曲线在
measured_value
稳定后仍持续小幅波动,表明微分项在对测量噪声进行过度响应,此时应降低
Kd
或增加输入滤波。
6. 系统集成与实验平台搭建
一个成功的嵌入式运动控制系统,是硬件、固件与机械结构精密耦合的产物。本节详细阐述如何将前述所有模块整合为一个可工作的物理平台。
6.1 电源系统架构
整个系统采用双电源域设计,彻底隔离数字电路与功率电路的噪声耦合:
-
数字电源域(5V)
: 由LM2596模块从锂电池降压生成,专供STM32 MCU、编码器及逻辑电路使用。其GND(DGND)与功率地严格分离。
-
功率电源域(12V/16V)
: 直接由3S或4S锂电池提供,专供电机与L298N驱动芯片使用。其GND(PGND)通过一根粗导线,在L298N的接地引脚处与DGND单点连接,形成“星型接地”,最大限度抑制地弹噪声。
6.2 机械结构与传感器安装
编码器电机的安装方式直接影响测量精度。本平台采用“悬臂梁”式固定:
- 电机主体通过螺栓牢固固定于一块刚性铝板上。
- 电机输出轴末端安装一个直径50mm的橡胶轮,轮子边缘与地面接触。
- 此种安装方式确保了电机轴在旋转过程中无轴向窜动与径向跳动,避免了因机械松动导致的编码器码盘晃动,从而杜绝了误计数的物理根源。
6.3 实验现象与初步验证
当系统上电并启动后,通过串口上位机可观察到清晰的物理现象:
-
初始状态
:电机未供电,
measured_value
稳定在0,
output
为0。
-
启动瞬间
:给电机供电,
measured_value
从0开始指数上升,
output
因PID的积分作用而缓慢增大,
setpoint
保持恒定。此时曲线呈现典型的“一阶惯性”响应。
-
稳态运行
:
measured_value
渐近并稳定于
setpoint
附近,
output
波动收敛至一个恒定值,
error
趋近于0。此时系统的稳态误差应小于±1%。
-
扰动测试
:用手轻触橡胶轮施加瞬时阻力,
measured_value
瞬时下跌,
error
突然增大,
output
迅速拉升以补偿,
measured_value
在1–2个控制周期内恢复。这直观地验证了PID的抗扰能力。
这一系列可重复、可预测的物理现象,是整个理论体系与工程实现正确性的最有力证明。它标志着一个从抽象数学公式到具象物理世界的成功跨越,也为后续的精细化参数整定奠定了坚实的基础。

3153

被折叠的 条评论
为什么被折叠?



