1. 从电机角度到小车姿态:为什么串级PID是平衡小车的“定海神针”

大家好,我是老张,一个在嵌入式控制领域摸爬滚打了十多年的工程师。今天想和大家聊聊一个非常经典且实用的项目——用STM32CubeMX和串级PID来控制一个平衡小车。你可能已经看过不少用单级PID控制电机速度或角度的教程,甚至自己动手试过,但当你把电机装到小车上,试图让它自己站起来的时候,是不是发现事情完全不一样了?小车要么像喝醉了一样来回剧烈晃动,要么干脆一启动就直接“躺平”,根本站不住。这背后的核心原因,就是我们今天要深入探讨的:从简单的电机角度控制,升级到复杂的动态系统姿态控制,串级PID是如何成为那个不可或缺的“定海神针”的。

简单来说,单级PID就像一个只会看最终结果的指挥官。你告诉它:“我要小车倾斜角度是0度(直立)。”它看到当前角度是10度,就拼命输出一个PWM信号想让电机转回来。但问题在于,电机收到PWM信号后,产生的是扭矩和速度,而不是直接产生角度。这个从“力”到“角度”的过程,中间隔着一个“速度”和“加速度”的动态环节。在小车这个不稳定系统中,这个环节充满了惯性、摩擦和外部干扰。单级PID的“蛮力”控制,很容易因为反应过激(超调)或反应迟钝(响应慢)而导致系统彻底失稳。

而串级PID的聪明之处在于,它设置了两个指挥官:一个外环的“角度指挥官”和一个内环的“速度指挥官”。角度指挥官只关心最终姿态:“我们的目标是直立!”它根据角度偏差计算出一个“理想的速度指令”发给速度指挥官。速度指挥官则是个执行者,它不管最终姿态,只专注于一件事:“立刻、准确地达到外环老大要求的速度!”这样一来,内环的速度控制极大地抑制了电机本身和外界干扰带来的速度波动,相当于给整个系统增加了一个“阻尼器”,让外环的角度控制能够在一个更平稳、更可控的“速度平台”上工作。对于平衡小车这种天生不稳定的倒立摆系统,这种内外协同、逐级稳定的策略,是让它稳稳立住的关键。接下来,我们就用STM32CubeMX,一步步把这个理论变成可以跑起来的代码。

2. 硬件基石:用STM32CubeMX快速搭建平衡小车控制平台

在开始写PID算法之前,一个稳定可靠的硬件软件基础至关重要。STM32CubeMX这个图形化工具能帮我们省去大量底层配置的麻烦,把精力集中在核心算法上。对于一辆典型的平衡小车,我们需要配置以下几个核心外设。

首先是最关键的定时器(TIM)。我们需要至少两个定时器通道:一个用于产生驱动电机的PWM波,通常配置在向上计数模式,PWM模式1或2,频率一般在10kHz到20kHz之间,太高了MOS管开关损耗大,太低了电机会有噪音。另一个定时器用于编码器接口模式,来读取电机旋转的速度和角度。STM32的定时器直接支持正交编码器模式,可以自动识别A、B相的相位关系,实现四倍频计数,精度非常高。在CubeMX里,你只需要找到对应的TIM,在“Combined Channels”中选择“Encoder Mode”,然后配置好编码器每圈的脉冲数(PPR)即可,剩下的硬件会自动帮你计数。

其次是通信接口。为了获取小车的姿态角,我们一定需要一个惯性测量单元(IMU),最常见的是MPU6050(集成三轴陀螺仪和三轴加速度计),它通过I2C接口与单片机通信。在CubeMX中配置好I2C的引脚和时钟速度(比如400kHz Fast Mode)。别忘了,MPU6050的数据读取和姿态解算(通常用互补滤波或卡尔曼滤波)会消耗不少CPU时间,最好放在定时器中断里周期性地执行。此外,为了调试时能看到实时的角度、速度、PID输出等数据,强烈建议配置一个串口(USART),用于连接电脑的上位机软件(如匿名上位机、VOFA+等),这样调参时就能直观地看到波形,效率提升十倍不止。

最后是系统时钟和中断。确保系统时钟(SYSCLK)设置正确,主频跑满(比如STM32F103是72MHz,F407是168MHz),这决定了你控制循环能跑多快。然后,我们需要一个高优先级的定时器中断来作为整个控制系统的“心跳”。这个控制周期非常重要,对于平衡小车,角度环和速度环的控制频率通常在1ms到5ms之间(即200Hz到1000Hz)。我个人的经验是从5ms开始调,稳定后再尝试缩短周期提高响应速度。在CubeMX里,你可以轻松配置一个定时器,开启它的更新中断,并在代码中生成对应的中断回调函数框架。

把这些硬件模块在CubeMX里用图形化方式点选配置好,生成初始化代码,一个为平衡小车量身定制的软件工程骨架就搭建完成了。这比手动去查手册、写寄存器要可靠和快速得多,也能避免很多低级错误。接下来,我们就可以在这个坚实的舞台上,上演串级PID的控制大戏了。

3. 核心算法拆解:平衡小车串级PID的双环设计与代码实现

理解了为什么用串级PID,也搭好了硬件平台,现在我们来深入它的内核,看看这两层PID究竟是怎么协作的,并把它们写成实实在在的C语言代码。平衡小车的串级PID,最经典的结构是外环为角度环,内环为速度环。有些更复杂的方案还会加入位置环或转向环,但对于最基本的自平衡,角度-速度双环已经足够。

3.1 角度环(外环):姿态的“决策者”

角度环的输入,是我们期望的小车姿态角(通常为0度,即完全直立)和IMU实时解算出的当前姿态角。它的输出,不是一个直接的PWM值,而是一个目标角速度。怎么理解呢?外环PID的公式依然是经典的:输出 = Kp * 误差 + Ki * 误差积分 + Kd * 误差微分。这里的误差就是 目标角度 - 当前角度。当小车向前倾斜时(角度为正),误差为负,外环PID计算出一个负的目标角速度,意思是:“需要让小车向后转动(产生一个向后的角速度)来回到直立位置。”这个计算出来的目标角速度,就是给内环速度环的“指令”。角度环的比例系数(Kp) 决定了小车有多“想”回到直立位置,Kp越大,回复力越强,但也更容易引发振荡。

3.2 速度环(内环):执行的“突击队”

速度环的输入,就是外环给它的“目标角速度”,以及通过电机编码器实时计算得到的“当前角速度”。它的任务非常纯粹:尽一切可能,让电机的实际角速度紧紧跟随目标角速度。它的输出,才是最终驱动电机的PWM占空比。速度环的引入,完美解决了单级PID中那个棘手的“f(PWM)”函数问题。无论是因为电池电压波动、地面摩擦变化还是小车负载改变,只要当前速度偏离了目标速度,速度环就会立刻调整PWM输出进行纠正。速度环的积分项(Ki) 在这里尤为重要,因为它可以消除静态误差,确保小车在受到一个持续干扰(比如轻微的斜坡)时,能通过微调电机速度来维持平衡,而不是慢慢倒下去。

3.3 代码结构:清晰、模块化是关键

在代码实现上,清晰的结构能让调试事半功倍。首先,我们需要定义两个PID结构体,分别给内外环使用,最好再把它们封装成一个串级PID结构体。

// 单级PID结构体
typedef struct {
    float target;          // 目标值
    float feedback;        // 反馈值
    float kp, ki, kd;     // PID参数
    float error;           // 当前误差
    float last_error;      // 上次误差
    float integral;        // 积分项
    float output;          // 输出值
    float output_max;      // 输出限幅
    float integral_max;    // 积分限幅(抗饱和)
} PID_t;

// 串级PID结构体
typedef struct {
    PID_t angle_pid;  // 外环角度PID
    PID_t speed_pid;  // 内环速度PID
    float output;     // 最终输出(PWM值)
} CascadePID_t;

然后,实现一个通用的PID计算函数,供内外环调用。注意要处理好积分限幅和输出限幅,这是工程实践中避免系统失控的关键。

void PID_Calculate(PID_t *pid) {
    // 计算误差
    pid->error = pid->target - pid->feedback;

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

    // 积分项(并做限幅抗饱和)
    pid->integral += pid->error;
    if (pid->integral > pid->integral_max) pid->integral = pid->integral_max;
    if (pid->integral < -pid->integral_max) pid->integral = -pid->integral_max;
    float i_out = pid->ki * pid->integral;

    // 微分项(常用不完全微分或微分先行)
    float d_out = pid->kd * (pid->error - pid->last_error);
    pid->last_error = pid->error;

    // 合成输出并限幅
    pid->output = p_out + i_out + d_out;
    if (pid->output > pid->output_max) pid->output = pid->output_max;
    if (pid->output < -pid->output_max) pid->output = -pid->output_max;
}

最后,在定时器中断服务函数中,按照“先外环,后内环”的顺序执行串级PID计算。这个顺序不能乱,因为内环需要外环的输出作为它的目标。

// 假设在1ms定时中断中执行
CascadePID_t balance_pid; // 全局平衡PID结构体
float imu_angle;          // 从IMU读取的角度
float motor_speed;        // 从编码器计算的速度

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim == &htim3) { // 控制周期定时器
        // 1. 更新反馈值
        balance_pid.angle_pid.feedback = imu_angle;
        balance_pid.speed_pid.feedback = motor_speed;

        // 2. 串级PID计算
        // 外环角度PID计算,输出为目标速度
        PID_Calculate(&balance_pid.angle_pid);
        // 将外环输出作为内环的目标
        balance_pid.speed_pid.target = balance_pid.angle_pid.output;
        // 内环速度PID计算,输出为最终PWM
        PID_Calculate(&balance_pid.speed_pid);
        balance_pid.output = balance_pid.speed_pid.output;

        // 3. 输出PWM到电机驱动
        Motor_SetPWM(balance_pid.output);
    }
}

代码写到这里,控制逻辑就已经通了。但让小车真正站起来,更考验人的是接下来的调参过程,这就像给一个复杂的机械钟表校准,需要极大的耐心和一点点技巧。

4. 调参实战:让平衡小车从“摇摆”到“稳如老狗”的必经之路

参数调试是PID控制中最具“玄学”色彩,但也最有规律可循的一环。对于串级PID,一个黄金法则是:先内后外,先P后I再D,由简入繁。绝对不要一上来就把六个参数(角度环Kp,Ki,Kd;速度环Kp,Ki,Kd)全都加上,那会让你陷入参数的汪洋大海,彻底迷失方向。

4.1 第一步:孤立内环,调好速度环

在调平衡之前,我们先不让小车站立,而是把它架起来,让轮子空转。此时,我们暂时屏蔽外环角度环,只使用速度环。你可以写一个简单的测试程序,让速度环的目标值是一个固定值(比如每秒转两圈),或者是一个缓慢的正弦波。然后,像调普通的单级PID一样去调速度环的参数。

  1. 调Kp(比例):先将Ki和Kd设为0。逐渐增大Kp,你会观察到电机的转速开始对目标有响应。目标是让电机能快速跟上目标速度的变化,但又不会在目标值附近来回剧烈振荡。如果听到电机发出“滋滋”的高频振荡声,说明Kp太大了,需要减小。
  2. 调Ki(积分):加入Ki是为了消除静差。比如你给定一个目标速度,电机稳定后却总是差一点点。慢慢增加Ki,直到这个静态误差被消除。但Ki太大会导致系统反应迟钝,甚至出现低频振荡。一定要给积分项加上限幅,防止它无限累积(积分饱和)。
  3. 调Kd(微分):在速度环中,微分项的作用是抑制超调,让速度变化更平滑。但微分项对噪声非常敏感,编码器速度如果稍有抖动,经过微分放大后可能会引起PWM剧烈跳变。所以,如果速度反馈信号噪声大,可以暂时不加D,或者使用“不完全微分”来滤波。一个调好的速度环,应该能快速、平稳、准确地跟踪你给它的任何速度指令。

4.2 第二步:闭合外环,精调角度环

当速度环能独立工作得很好之后,我们就可以把角度环加回来了,让小车尝试站立。此时,速度环的参数基本固定不动,我们只调节角度环的参数。同样采用“先P后I再D”的策略。

  1. 角度环Kp:这是让小车站起来的核心力量。用手扶住小车,给它一个很小的初始角度,然后松手。从小到大地增加角度环Kp。你会发现,Kp太小时,小车无力回正,会慢慢倒下;Kp增加到某个临界点,小车开始有能力往回摆,但可能会来回摆动几次才停下,或者直接开始发散振荡。我们的目标是找到一个Kp,让小车在受到小扰动后,能在一两个周期内平稳地回到直立位置,不超调也不振荡。这个过程非常刺激,小车可能会剧烈跳动,一定要做好保护,防止飞出去摔坏。
  2. 角度环Kd:在角度环中,微分项至关重要,因为它提供了“阻尼”。想象一下,小车在回正的过程中速度很快,如果没有微分阻尼,它就会冲过平衡点,然后反向加速,形成振荡。加入Kd,相当于给小车运动增加了一个与角速度相反的力,能有效抑制振荡。增大Kd,小车的运动会变得更“沉稳”,但Kd过大会让系统反应变慢,抗干扰能力变差。通常,在调好Kp让小车能站但会抖之后,加入Kd就能让它立刻稳定下来,效果立竿见影。
  3. 角度环Ki:对于平衡小车,角度环的积分项需要非常谨慎。因为我们的目标角度是0度,这是一个动态平衡点,理论上不应该存在静态误差。加入Ki主要是为了补偿一些不对称性,比如小车重心微微偏左或偏右。但Ki很容易引起低频振荡,我个人的经验是,很多时候只用PD控制就能获得很好的效果,Ki可以设为一个非常小的值,或者干脆为0。

4.3 调试工具与技巧:用好你的“眼睛”和“耳朵”

调参不能盲调,必须借助工具。上位机软件是你的“眼睛”。通过串口把角度、目标速度、实际速度、PWM输出等关键数据实时发送到电脑绘图,你能清晰地看到每一次振荡、每一次超调的波形。比如,看到角度曲线是发散的正弦波,说明Kp太大或Kd太小;看到小车缓慢地向一个方向倒下,说明可能需要一点Ki或者机械重心没调好。你的耳朵也是很好的工具,电机发出的声音能反映它的工作状态。平稳的嗡嗡声是好的,尖锐的啸叫声或咔咔的堵转声,说明参数有问题或负载过大。

另外,记得在代码中加入参数在线微调的功能。比如通过按键或者上位机指令,实时微调Kp, Ki, Kd的值,这样你就能在小车运行的同时观察参数变化的影响,效率远比改一次代码、编译、下载、复位高得多。调参是个反复迭代的过程,可能今天调好的参数,明天换了块地面(摩擦力不同)又不行了,所以理解原理比记住一组具体参数更重要。

5. 超越基础:平衡小车姿态控制中的进阶问题与优化

当你的小车能稳稳立住,甚至能用手推一下又自己恢复平衡时,恭喜你,已经成功了90%。但剩下的10%,才是区分“玩具”和“稳定作品”的关键。在实际项目中,你会遇到一些更棘手的问题。

第一个大问题是传感器噪声与数据融合。 MPU6050提供的原始陀螺仪和加速度计数据是充满噪声的。陀螺仪短期精度高,但存在漂移,积分算角度会越来越偏;加速度计长期稳定,但瞬间动态响应差,小车一运动加速度计数据就乱跳。直接用一个数据都不行。这时就必须用到传感器融合算法。最经典的是互补滤波,代码简单,效果不错。它的思想很直观:用高通滤波器滤掉加速度计的低频噪声(动态干扰),用低通滤波器滤掉陀螺仪的高频噪声(漂移),然后把两者融合。更高级的可以用卡尔曼滤波,它能提供一个最优估计,抗干扰能力更强,但理论复杂,参数难调。我建议新手先从互补滤波开始,它能让你快速得到一个可用的姿态角。

第二个问题是控制量的输出与电机死区。 我们PID计算出来的PWM值,最终要作用到电机驱动芯片(如TB6612、DRV8833)上。这里有个坑:电机有死区电压。也就是说,当PWM值很小的时候,不足以克服电机静摩擦,电机是不转的。但你的PID控制器不知道,它发现输出PWM了角度却没变化,就会继续增大输出,直到某一下电机突然“蹦”一下启动,导致小车剧烈抖动。解决办法是在输出PWM时,加入一个死区补偿。当计算出的PWM绝对值小于某个阈值时,直接输出0;大于阈值时,再加上一个偏置值,确保电机能平滑启动。

第三个问题是更复杂的运动控制。 小车光站住还不够,我们还想让它前进、后退、转弯。这就需要在双环的基础上,引入第三个环——位置环。你可以把位置环作为最外环,它的输入是目标位移(比如遥控器指令),输出是目标角度。是的,为了让小车前进,你需要让它“故意”向前倾斜一个角度,这样重力分量会产生一个前进的加速度,速度环和角度环会协同工作来维持这个倾斜角,从而实现匀速运动。转向控制则可以通过差速来实现,即左右轮给予不同的速度目标。这时,你的串级PID结构就变成了位置-角度-速度三级串级,或者角度环和速度环并行处理左右轮差速,系统的复杂度和调试难度都会上一个台阶。

调试这些进阶功能时,一定要有耐心,并且做好数据记录。每一个参数的改变,都对应着上位机波形上的一次变化。多思考背后的物理意义:这个振荡是哪个环节引起的?这个超调是哪个环的响应慢了?当你能够通过观察现象来反推系统问题时,你就真正掌握了串级PID的精髓。平衡小车项目虽小,但它涵盖了嵌入式系统开发中硬件配置、传感器处理、控制算法、实时调试的完整链条,把它吃透,你对闭环控制的理解会上一个大台阶。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐