说实话,做单片机开发和自动化控制这么多年,我踩过的最大一个坑就是PID调参。早些年调PID,全靠串口打印数据,然后眼睛盯着那串刷得飞快的数字,心里默默估算转速到底稳不稳、超调了多少。调试一个稍复杂的系统,往往要折腾一下午,还经常调完Kp忘了Kd,改完Ki又把之前的波形全打乱。后来我换上了SerialPlot这款开源串口示波器软件,配合STM32的串口把实时数据发出来,PID调参效率直接翻了一倍不止。这篇文章就分享一下我实际用下来的完整流程、代码写法、调参思路和踩坑记录,希望能帮那些还在“盲调”里挣扎的朋友少走点弯路。
SerialPlot是什么呢?简单说,它就是把串口收到的数据实时画成波形曲线的工具,类似一个迷你示波器。你只需要在STM32代码里用串口把PID的设定值、实际值、输出值发出来,电脑端打开SerialPlot,就能像看示波器波形一样观察整个控制过程。这篇文章适合正在做电机调速、温度控制、平衡小车、四轴飞行器姿态控制,或者任何还在用串口数字debug PID的朋友。不管你是刚入门的大学生,还是已经有几年经验的嵌入式工程师,这套方法都能让你的调试过程从“靠猜”变成“看得见”。
1. 为什么说PID调参最大的痛点是“看不见”
1.1 经典串口打印调试的局限
我们先聊聊以前最常用的调试方式。STM32代码里写一句printf("target=%d,current=%d,pwm=%d\r\n", target, current, pwm);,然后打开串口助手,看着滚动的数据。看起来很直观对不对?但真正调起来你会发现问题很大。
第一,数据是离散的,而且是文本形式。你在串口助手里看到的是“target=1000,current=850,pwm=300”这样一行行字符串。转速从850到900再到980,这个过程在纯文本里很难形成直观感受。你得靠大脑在几千行数据里脑补出一条曲线,这本身就极其反人类。
第二,临时修改代码看波形这件事效率极低。比如你想观察PID输出量的变化趋势,就得改代码加变量、重新编译、烧录、打开串口助手。改一个参数至少一两分钟,而一次完整调试往往要改几十次参数,时间全耗在编译烧录上了。
第三,PID参数之间是相互耦合的,看不清就很容易调乱。你把Kp调大了,系统开始振荡,但振荡到底是Kp太大还是Ki太大引起的?如果没有波形图,单靠看数字,很容易误判,结果就是改了一个参数,之前好不容易调好的另一个参数也被带歪了。
1.2 可视化调试到底改变些什么
用SerialPlot这种可视化工具之后,最大的变化就是:你从“盲人摸象”变成了“对着屏幕看心电图”。
举个我实际调试直流电机转速闭环的例子。以前调Kp的时候,我只能在串口助手里看到转速数字在目标值附近上下跳,但跳动的幅度、频率、收敛速度完全靠感受。用SerialPlot之后,转速曲线直接画出来,超调量是多少、振荡频率多高、稳定时间多长,一眼就能看明白。
最关键的是,它能同时显示多条曲线。我把目标转速、实际转速、PID输出值三路数据都发出来,调Kp的时候同时盯着实际转速有没有超调、PID输出有没有饱和;调Ki的时候观察稳态误差有没有消除;调Kd的时候看曲线前段有没有变“肉”。每个参数的作用边界清清楚楚,再也不会出现“调完Kp忘了Kd”的尴尬。
1.3 可视化背后调参效率质变的逻辑
很多人可能会觉得,可视化只是把数据变成了曲线,本质上该懂PID原理还是得懂。这话没错,但可视化的价值在于大幅缩短了“调整参数→观察结果→判断方向”这个循环的周期。
以前我观察一组结果可能要花几分钟在数据里找规律,现在波形出来一秒就能判断“超调多了,得降Kp”还是“响应太慢,得升Kp”。判断周期从几分钟缩到几秒,等于把调试迭代速度提升了十几倍。5分钟搞定一组初始参数,放在以前我想都不敢想,但用可视化之后,这个速度是真实可以达到的。
2. 用SerialPlot前的准备工作
2.1 SerialPlot的下载与安装
SerialPlot是SourceForge上的一个开源免费软件,直接在SourceForge搜索SerialPlot就能找到下载页面,支持Windows和Linux。下载时注意选对系统版本,Windows的话直接下exe安装包就行。安装过程很简单,一路Next就完了。
软件很小,只有几MB,不像那些工业级的上位机软件动辄几百MB。这也是我喜欢它的原因之一——轻量、免安装、打开就能用。不过轻量也意味着界面比较朴素,功能上肯定比不了专业的示波器上位机,但应对PID调试这个场景绰绰有余。
2.2 通讯参数设置与硬件连接
打开SerialPlot后,主界面左边是串口参数设置区。这里要设置串口号、波特率、数据位、停止位、校验位。STM32端我用的是USART1,波特率默认设为115200,8位数据位,1位停止位,无校验。电脑端在设备管理器里确认一下COM口号,选对端口,然后点Connect连接。
需要注意的一点是:SerialPlot的串口是独占模式,连接之前必须确保串口助手、其他上位机软件都已经关掉了。如果你用ST-Link的虚拟串口,还要注意ST-Link驱动的版本,老版本驱动有时候会让串口无法识别,这时候去ST官网下载最新的ST-Link驱动重装就行。
连接成功之后,主界面右侧会开始接收数据。但这时候如果STM32还没发数据,波形区是空的,需要把下位机代码先跑起来。
2.3 波形窗口的常用设置
SerialPlot的波形区支持多条通道同时显示,每条通道有自己的颜色。左下角可以设置每条曲线的名称,我一般按“target”“current”“output”这样命名,方便区分。
还有一个很实用的功能:波形视窗的缩放。在波形区按住鼠标左键拖动可以框选放大,右键可以回退缩放。调PID的时候如果曲线跑出可视范围,不用改代码,直接在界面上调整缩放比例就行。坐标范围也可以在左侧手动设置,把Y轴范围设成实际数据的上下限,比如转速0到1500,这样波形看起来更饱满,细节更清晰。
SerialPlot还有一档“Stop”按钮,相当于示波器的暂停功能。调参的时候如果看到一组特别好的波形,可以按Stop把画面冻结,然后慢慢分析波形形态,这点对复盘调参过程特别有用。
3. STM32端发送数据的代码实现
3.1 数据发送框架的整体思路
要把PID数据发到SerialPlot,核心思路就一句话:把需要观察的变量打包成多路通道,统一通过串口发送上位机。但具体发送时有几个细节处理不好,容易出现波形错乱、数据解析不了的问题。
我的做法是在工程里建一个独立的debug.c文件,专门放调试相关的数据发送函数。这样调试代码和业务代码分离,等参数调完了,把debug.c的调用注释掉就能快速移除,不影响主程序结构。这个习惯我从用SerialPlot开始就养成了,后来在其他项目里也沿用,确实能省不少事。
数据发送的频率不用太高。很多新手喜欢在定时器中断里1ms发一次,结果上位机根本显示不过来,波形反而卡顿。我实测下来,20ms到50ms发一次就足够观察到完整的PID调节过程了。毕竟PID的调节周期一般在几毫秒到几十毫秒,20ms采样显示曲线完全能看出趋势。
3.2 基于DMA的串口发送实现
如果用的是STM32标准库或者HAL库,我推荐用DMA发送配合一个发送缓冲区。这样发送过程不占用CPU,不会影响PID运算的实时性。我用的是HAL库,代码大致如下:
#include "debug.h" uint8_t debug_buf[64]; void debug_send_pid_data(volatile int16_t target, volatile int16_t actual, volatile int16_t output) { // 用自定义的格式化函数把三个变量填入缓冲区 uint8_t len = 0; len += sprintf((char *)&debug_buf[len], "t:%d,a:%d,o:%d\r\n", target, actual, output); // 使用DMA发送,不阻塞主流程 HAL_UART_Transmit_DMA(&huart1, debug_buf, len); }注意这里我用的是t:、a:、o:这样的通道标识前缀。SerialPlot有个文本数据解析功能,可以通过关键字把不同通道的值提取出来。比如波形界面如果设置了两条线,它会根据t和a分别提取目标值和实际值。
为什么不用逗号分隔的三个裸数字?因为第一次用SerialPlot的时候我就踩过坑,它默认的二进制数据格式要求每一帧的字节数固定,三个变量如果发送浮点数,长度不固定很容易错位。加上文字关键字之后,SerialPlot就能稳定地把通道对应起来,波形不乱跳。
3.3 浮点数发送的另一种思路
如果你要观察的是带小数点的PID参数,比如PID输出是一个浮点数,直接在串口发%f有时候会出问题。因为printf的浮点数格式在嵌入式中需要额外支持,而且处理起来慢。我有两个替代方案。
方案一:把浮点数放大成整数再发。比如PID输出在-100.0到100.0之间,就乘以10变成-1000到1000的整数发送。上位机看到数值后再手动除以10理解真实值。这个方法简单可靠,就是看波形时心理上要算一下换算关系。
方案二:用固定点数协议。把浮点数分成整数部分和小数部分,分别发送。比如发送a:85,frac:3代表85.3。这个方法稍微复杂,但适合对精度要求高的场合。
我建议大多数场景用方案一就够了。PID调试主要看趋势、超调、振荡,分辨率做到0.1已经完全足够。
3.4 发送位置式PID还是增量式PID数据
这个点我想单独说一下,因为很多朋友分不清调试时要观察哪组数据。
在PID控制里,位置式PID的输出是控制量的绝对值,比如PWM的占空比500/1000;增量式PID的输出是控制量的增量,比如每次加5或者减3。调参时观察的数据类型不同,判断标准也有差别。
我调电机速度环用的是增量式PID。增量式输出的是每次的调整量,所以波形上看起来是一个在零点附近波动的信号。当系统稳定时,增量值会趋近于0,说明输出不再变化。用SerialPlot同时显示目标转速、实际转速和PID增量输出,能非常直观地看到:转速误差大的时候增量值大,误差收敛的时候增量值变小,最终趋近于0。这个观察点在调Ki参数时尤其有用。
位置式PID的场景,比如温控系统或者舵机角度控制,观察的是输出绝对量和目标值、实际值的关系。输出曲线如果顶到饱和上限长时间不动,说明积分在“抱死”,这就提示你要加积分限幅了。这些判断完全依赖可视化波形,靠看数字很难快速看出来。
4. 用SerialPlot进行PID参数调节的完整流程
4.1 第一步:先只调P,找到系统的“脾气”
拿到一套新系统,不管它是电机、加热片还是自平衡小车,我的调参铁律永远是:先把Ki和Kd设为0,只留Kp,从小到大慢慢加。这个阶段关注两个核心问题:一是响应速度能不能接受,二是会不会出现等幅振荡。
用SerialPlot观察的时候,我会同时盯着“目标值”和“实际值”两条曲线。Kp很小的时候,实际值曲线像没睡醒一样,慢吞吞地逼近目标值,而且永远差一截,这时就在误差较大时增大Kp。
我举个例子,调一个直流电机转速环。设定目标1000转,Kp=0.5时,曲线要将近2秒才爬上950转,而且离目标值始终差30转左右。这时候把Kp加到1.0,上升时间缩短到0.8秒,稳态误差缩小到10转。继续加到1.5,上升时间明显快了,但曲线开始出现小幅振荡,大概围绕目标值波动±15转。再往上加到2.0,系统直接开始大幅振荡,波形像正弦波一样绕目标值来回扫,振幅甚至超过100转。这时候我就知道了:这个系统的Kp临界值在1.5到2.0之间。
SerialPlot好就好在,临界振荡的波形变化是有渐变过程的。你能看着它是从“有点抖”还是“已经疯了”逐步过渡的,心里有个底。如果只有串口数字,你看到转速从950到1050来回跳,可能根本分不清是正常波动还是临界振荡。
4.2 第二步:加入D抑制超调
Kp选定在临界值以下之后,系统虽然稳定,但超调量往往比较大。用一个Kp=1.0的电机为例,设定1000转时,实际转速冲到了1200转才回落,这个超调量对很多应用场景是致命的。
这时候就轮到Kd出场了。Kd的作用是“看趋势”的——误差在缩小的时候输出一个反向修正量,把过冲的势头压住。Kd从0.1开始加,我一般按成倍往上试。
加了Kd之后,用SerialPlot能很清楚地观察到变化:转速冲到1100就回头了,然后第二次冲到1050,第三次基本就贴着1000了。超调量一次比一次小,曲线收敛得又快又稳。
但Kd不是越大越好。我踩过一个坑:在电机系统里把Kd加得过大,系统出现了一种高频抖动的现象。波形图上能看到实际值在目标值附近以极高频小幅颤动,有点像“局部振荡”。更麻烦的是,电机还会发出尖锐的啸叫声,这是因为微分项放大了噪声。后来把Kd降下来,啸叫才消失。
所以学到的经验是:Kd要根据系统的噪声水平选择,不是波形的超调压得越低就越好,还要结合PWM输出曲线的平滑度一起判断。看到输出曲线毛刺特别多、抖动特别剧烈的时候,第一反应应该是降低Kd,而不是继续加。
4.3 第三步:用Ki消除稳态误差
P和D都调好之后,系统一般来说已经很能用了:响应快,超调小,跟手跟得稳。但细看波形的话,实际值曲线永远和目标值差了那么一丁点。比如目标1000,实际才稳定在978,差了22转。这就是所谓的稳态误差。
稳态误差靠PD是消除不掉的,必须上积分项Ki。积分项会慢慢积累误差,让输出逐渐加大,直到把实际值顶到目标值。
Ki的调试在我用过的系统里有两种典型习惯。如果是位置式PID,我会设Ki=0.05、0.1、0.2这样成倍往上试,每次改完观察稳态误差是否被完全消除。如果Ki太大,波形会出现低频振荡——实际转速在目标值上下做完一次大幅摆动之后,摆回原点又继续摆,形成周期性的震荡包络。这种振荡和Kp过大时的高频振荡不一样,频率低很多。
如果是增量式PID更简单。当波形稳定后,我直接看增量输出那一路曲线是否在零点附近波动。误差为0时增量自然为0,如果曲线一直停在某个正数位置不动,说明还有误差在积累,Ki还得加。这个判断方式因为可视化变得一目了然。
4.4 一套快而不乱的实操调参口诀
纸上得来终觉浅,我把自己调了这一年多PID后总结的口诀分享出来:
先P后I再D,逐个单独试,波形做裁判。P大超调动得快,P小拖着走不动。I大低频嗡嗡叫,I小永远差一截。D大尖锐高频振,D小超调摇头摆。
实际操作中我一般按三步走:P调到系统“能稳但超调”,再加D把超调“压下去”,最后加I把误差“清零”。每调一步都用SerialPlot保存一组波形截图,方便对比前后变化。
用SerialPlot还有个好处是调节全程不用开系统的定时器断点,不用改代码重新烧录。直接在代码顶部定义几个宏,然后编译一次,剩下全部在上位机操作。大部分时间都花在“改数值→看波形→改数值”上,5分钟把一组可用参数调出来,真的不是吹牛。
4.5 PID参数调节速查表
我把不同波形形态对应的调整方向整理成了一张表,方便大家对着查。
| 波形表现 | 判断方向 | 调整策略 |
|---|---|---|
| 响应慢,曲线爬坡迟缓 | 比例P太小 | 增大Kp或Kd |
| 大幅超调后缓慢回落 | 比例P偏大 | 减小Kp或增加Kd |
| 稳态时在目标附近高频抖动 | 微分D过大或有噪声 | 减小Kd |
| 稳态时与目标差一截不动 | 积分I不足 | 增大Ki |
| 低频振荡,像波浪一样摆动 | 积分I过大 | 减小Ki |
| 输出曲线粗糙,毛刺多 | 微分项放大噪声 | 降低Kd,或在反馈上加滤波 |
| 输出饱和,长时间顶在上限 | 积分积累过多 | 加积分限幅,检查执行机构饱和 |
这张表不敢说覆盖所有系统,但覆盖了90%以上我用PID调过的场景。控制领域还有模糊PID、神经网络参数自整定这些高级玩法,但基础PID可视化调参的思维是通用的,先把这套跑通,后面学高级算法会轻松很多。
5. 常见问题与排查技巧
5.1 SerialPlot收不到数据或波形空白
这个问题我遇到最多。排查顺序按照优先级来:第一步查串口号是否选对,第二步查波特率是否一致,第三步查TX和RX有没有接反。如果用了USB转TTL模块,还要确认模块芯片驱动装好了没有,电脑端能不能识别到设备。
还有一个特别隐蔽的坑:STM32的串口发送引脚没配置成复用功能。比如我用PA9作为TX,初始化时必须把PA9设为GPIO_MODE_AF_PP复用推挽模式,如果只配了普通推挽输出,串口是不工作的。这个错我犯过不止一次,每次都是打开SerialPlot看到一条直线,排查半天才发现引脚配置错了。
5.2 波形串位:多条通道数据对不上
通道对不上的问题主要出在发送格式上。如果我用的是逗号分隔数字的格式,比如1000,985,350\r\n,SerialPlot有时候会把第一路当成第二路,第二路当成第三路,错位显示。
解决办法有两种:一是用SerialPlot的文本解析模式,发送带关键字的数据,比如t:1000,a:985,o:350,在SerialPlot的通道设置里选择对应的关键字,就不会错位了。二是用定长的二进制帧,每个通道固定占2字节或4字节,这样即使不写关键字,软件也能按字节顺序正确分割数据。
我现在的项目基本都用关键字方案,因为时序要求不高,而且错了能直接看到字段名在波形图上显示出来,排障成本最低。
5.3 波形刷新卡顿或丢包
卡顿多数情况下是发送频率太高导致的。我见过有的朋友用1ms定时器发数据,串口的吞吐量虽然够,但上位机界面刷新不过来,波形就看起来一顿一顿的。
解决方案很简单:把数据发送放到一个20ms或50ms的缓慢定时器里。如果希望观察更细的瞬间过程,可以把波特率调到460800甚至921600,同时用DMA发送保证不丢失。
另外,如果你在调试中断里直接用printf发送,那些阻塞等待串口发送完成的时间会影响控制周期的精确性。我强烈建议数据发送不要放在高优先级中断里,最好放在主循环的flag判断里,或者用DMA双缓冲区的方式,把发送时机和业务逻辑解耦。
5.4 一个容易被忽略的细节:共地问题
电脑通过USB转TTL模块和STM32板子连接时,除了TX到RX、RX到TX之外,GND也必须连在一起。否则电平参考不一致,串口数据经常乱码或者完全收不到。
这个问题在学生朋友的调试现场非常常见,尤其是用了蓝牙无线串口模块的时候,更容易把地线忘掉。SerialPlot界面上能看到数据有乱码、波形有随机毛刺,很多时候都是共地没做好。排查这个问题可以先用示波器量一下STM32串口引脚有没有正常波形,如果没有波形,再看地线是否接好。
5.5 常用排查问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 完全收不到数据 | 串口号不对、TX/RX接反 | 设备管理器中确认端口号,交换TX/RX |
| 数据乱码 | 波特率不一致、共地未接 | 统一波特率,连接GND |
| 波形只有一条直线 | 代码没发送,或引脚配置错误 | 检查发送函数被调用与否,检查GPIO复用配置 |
| 通道数据错位 | 发送格式不匹配 | 改用关键字文本协议或定长二进制协议 |
| 波形密密麻麻看不清 | 发送频率过高 | 降低发送频率至20ms,或提高波特率 |
| 数据实时性差 | DMA或中断配置不当 | 使用DMA发送,中断里只置标志位 |
这些坑每一个我都是真金白银踩出来的,特别是通信协议格式那块,第一次把数据发上去看到波形错得离谱,差点以为是软件坏了。后来老老实实用了关键字格式,之后就一直平稳运行到现在。
写在最后:可视化调试带来的工作习惯改变
老实说,刚开始接触PID控制的时候,我一度觉得调参是门“玄学”。同样的参数,别人调出来稳得一批,自己复现的时候就是振荡。后来用了SerialPlot这类可视化工具,才真正体会到“说不出哪里不对就画出来看看”的力量。
现在我的习惯是:拿到一个新的控制系统,第一件事先搭好调试的数据通道,把目标值、实际值、控制输出三路曲线全部接好,然后再开始调PID。这一步看起来多花了半小时,但后面调试节省的时间远超这个数。哪怕是给别人代码做移植适配,我也是先看波形曲线再改参数,基本不会再出现“调参靠运气”的情况。
如果你现在手头正在被PID调参折磨,建议你今天就花10分钟把SerialPlot和STM32的数据发送代码配上,把一组简单的转速或者温度波形画出来,你一定会发现:原来PID参数优化的过程,可以这么直观。