单片机固件PID整定界面搭建:OLED波形显示与编码器交互实战
2026/9/7 13:15:49 网站建设 项目流程

做过闭环控制调试的朋友应该都有同感:整定PID参数这件事,最折磨人的往往不是算法本身,而是怎么把当前系统的状态、历史曲线、参数修改过程摆到眼前。我见过不少同事的调试现场,改一次Kp就重新编译烧录一次固件,几分钟一次,连下载器都插拔到怀疑人生。所以到了第七期,我决定先不急着讲整定方法,而是把固件的人机界面先搭出来——这个东西看着不起眼,却是整定工作能否顺畅推进的前提。

这期的定位很明确:在一个单片机固件里,从零搭建一套适合参数整定的人机交互界面。它要能显示当前温度/速度/位置等被控量的数值,能实时画出响应曲线,能方便地修改Kp、Ki、Kd等参数,还要支持掉电保存。做完这个界面,后面无论你用临界比例法还是试凑法做整定,效率都能翻几倍。这篇博文适合正在做嵌入式控制项目、想给自己固件加个交互入口的朋友,也适合只接触过串口打印调试、想进阶到图形化界面的开发者。

1. 整定工作为什么如此依赖人机界面

写控制程序的人一开始往往都是串口打印党——把PV值不断通过UART发到上位机,再用串口助手或者脚本画曲线。这个方式做验证没问题,但真正进入整定阶段就非常痛苦。整定本质上是一个高频交互过程:改一个参数,观察系统响应,再改一个参数,再观察。如果每次交互都走"改代码-编译-烧录-重新运行"这条链路,一次整定下来基本就是半天起步。

我把整定时的真实工作流拆开看,就会发现界面起码要在三个环节里发挥作用。

第一个环节是参数调节。试凑法要求交替调整比例增益、积分时间、微分时间,需要实时看到当前参数值、可调范围和修改后的数值反馈。没有界面的话,改参数就得在源码里搜一个全局变量然后手动改数字,效率极低,也容易改错。

第二个环节是响应观察。判断一个参数组合好不好,关键看阶跃响应曲线的超调量、振荡次数、稳态误差、调节时间。这些特征肉眼扫一眼曲线就能判断个七八分,但前提是曲线必须直观可见。OLED这种低分辨率屏虽然画不了精细的波形,但看个大致形态完全够用,比串口文本要直观得多。

第三个环节是参数的持久化。整定不是一次定死就完事,不同工况可能需要不同参数组。靠重新编译烧录来"记住"一组参数是不现实的,界面加一个保存命令,把参数写进Flash或EEPROM,上电自动加载,这才叫真正可用的固件。

所以说,界面不是摆设,它是整定工作里把人、控制器、被控对象连接起来的关键通道。没有界面的固件,控制算法写得再好,调试效率上不去,项目就卡在那里动弹不得。

2. 人机界面的方案选型与整体设计

2.1 显示载体怎么选

很多人一上来就在纠结用什么屏,其实这个选择的核心取决于两个变量:信息密度需求成本约束。我自己的经验是这三类方案比较典型:

显示方案信息密度交互复杂度成本适用场景
串口终端文本为主低(仅输出)最低临时验证、日志记录
数码管+按键数字为主中(翻页/修改)工业仪表、极简交互
LCD/OLED屏图形+文本高(菜单/曲线)参数整定、通用控制面板

做整定界面,我强烈推荐OLED屏,尤其是0.96寸或1.3寸的SSD1306/SSH1106系列。理由有三条:首先它是图形屏,能画曲线,这个是数码管永远做不到的;其次它自带控制器,单片机只要通过I2C或SPI写显存数据就行,驱动工作量比RGB屏小一个数量级;最后它的成本也就十几块钱,对绝大多数项目来说完全在预算范围内。

当然,如果你的产品本身就要做一个嵌入式的正式交互面板,那直接上串口屏或者组态屏也完全可以,界面开发效率更高,只是这期我讲的思路是自绘型界面,因为它在底层逻辑上更可控,后续扩展也灵活。

2.2 交互方式的选择:编码器真的比按键好用

界面不是给人看的,是给人"用"的。显示层想好了,交互层还要设计。我试过纯按键方案,也试过带旋转编码器的方案,后来基本固定在编码器方案上。

纯按键的问题在于:数值调整效率太低了。Kp要从1.0调到5.0,假如步进是0.1,你要按40下加号键。如果在长按连发机制做不好,这个体验会非常煎熬。编码器则完全不同,手指轻轻一旋就能连续改变数值,旋得快数值跳得快,旋得慢跳得慢,天然具备速度自适应的手感,这在整定场景里极其重要——粗调快旋,细调慢旋。

硬件上我用的是EC11旋转编码器,带一个开关按键。接线也简单:A相、B相接两个普通GPIO,按键接一个GPIO。读取方式有两种经典做法:一种是把A相接外部中断,在中断里判断B相电平来确定旋转方向;另一种是用定时器周期性扫描两相电平变化,解算正反转。我推荐定时扫描法,代码简单,不容易被中断优先级问题坑到。

2.3 界面整体架构设计

一套完整的整定界面,在我的设计里至少要有这几个状态面:

  • 运行主界面:显示当前目标值SP、测量值PV、输出值OUT、运行模式(自动/手动),一屏看全核心信息。
  • 参数设置界面:以菜单列表形式展示Kp、Ki、Kd、采样周期Ts、输出限幅等关键参数,支持选中、修改、确认。
  • 曲线回放界面:在OLED上滚动显示最近几百个周期的PV/SP曲线,用于整定后的响应观察。
  • 保存/恢复界面:把当前参数写入存储区,或者从存储区恢复一组出厂参数。

这四个状态面通过一个简单的菜单状态机管理。状态机的好处是结构清晰,每个界面是一个状态,进入、退出、事件处理都集中在switch-case里,逻辑不容易乱。

界面层的代码还要考虑和底层控制逻辑解耦。我的做法是:控制核心代码完全不知道界面的存在,它只暴露读PV、读输出、写PID参数这样的接口。界面层作为独立的模块通过接口去读写数据。这样做的最大好处是——控制代码稳定可靠,界面怎么改都不会影响控制实时性。

3. 核心模块实现与操作要点

3.1 显示框架搭建

先说OLED的驱动。以SSD1306为例,底层就是I2C写命令和数据,但直接操作寄存器太原始,一定要在芯片内部建立一块显存buffer,所有绘制操作都在这块缓冲上完成,最后统一刷新到屏幕。这样有几个好处:一是避免单点绘制时频繁刷新导致闪烁;二是可以方便地实现画点、画线、画矩形等绘图函数,它们都只是往buffer里写数据。

显存大小计算:128×64像素,每像素1bit,所以是128×64/8=1024字节。在STM32这类MCU上,直接在RAM里声明一个1024字节的数组毫无压力。如果是内存更小的MCU,比如只有2KB RAM的芯片,就要考虑分页刷新了,不过能用OLED做界面的项目,内存一般都能腾出这1KB。

绘图函数的核心就是画点。SSD1306的显存是按页(8像素一行)组织的,要把一个像素坐标(x,y)映射到buffer的对应bit位,写成代码就是:

void ssd1306_draw_pixel(uint8_t x, uint8_t y, uint8_t color) { if (x >= 128 || y >= 64) return; uint16_t index = x + (y / 8) * 128; if (color) { buffer[index] |= (1 << (y % 8)); } else { buffer[index] &= ~(1 << (y % 8)); } }

有了画点函数,画线段、画字符、画曲线就都是组合工作了。注意一点:如果你不想自己从零抠字符点阵,可以找一份常用的ASCII点阵字库表,放到const数组里,就能实现中英文混合显示了。

3.2 菜单与参数修改逻辑

菜单层是这个界面的灵魂。整定参数修改的体验好坏,基本由菜单逻辑决定。我推荐用一个菜单项结构体数组来组织参数,而不是手写一堆if-else。核心结构长这样:

typedef struct { const char* name; // 参数显示名,如"Kp" float* value; // 指向实际参数变量的指针 float min_value; // 最小值 float max_value; // 最大值 float step; // 步进值 } menu_item_t;

这样设计的好处是数据和逻辑分离:参数列表的每一项只描述"我叫什么、值在哪里、范围多大",至于怎么显示、怎么修改,全部由一个通用的处理函数统一完成。后面想增加一个参数,只需要往数组里加一行,这个扩展性用起来非常舒服。

参数修改的速度控制是关键细节。编码器旋转一格改一个step,但Kp需要粗调时,一格0.1也太慢。我的做法是动态步进:根据当前数值的大小自动调整步进。数值小的时候步进小,数值大的时候步进大,让调节速度曲线接近对数关系,这样无论参数在哪个量级,调节手感都很顺。下面这段是核心逻辑:

void modify_param(menu_item_t* item, int8_t direction) { float step = item->step; // 动态扩大步进,数值越大,单次调节量越大 if (fabs(*item->value) > 100) step *= 10; else if (fabs(*item->value) > 10) step *= 5; *item->value += direction * step; if (*item->value < item->min_value) *item->value = item->min_value; if (*item->value > item->max_value) *item->value = item->max_value; }

状态机部分我用枚举定义状态面,主循环里根据当前状态分发事件。事件来源主要是编码器的旋转方向和按键动作,都通过一个统一的input_event结构上报。这样设计的好处是后期如果要接蓝牙/串口遥控,只需要把外部数据翻译成input_event就能复用整个界面逻辑。

3.3 整定辅助功能:实时波形显示

如果说参数菜单解决的是"调"的问题,那曲线界面解决的就是"看"的问题。OLED虽然分辨率低,但滚动显示一个几百采样点的PV曲线完全够用。曲线的实现思路是经典的环形缓冲区+坐标映射

数据采集侧,控制核心每个采样周期把PV值写入一个环形数组,数组大小比如240个点,满了就覆盖最旧的数据。显示侧,每次刷新时把buffer里的点按顺序映射到OLED坐标上,y轴方向做归一化处理。自动归一化很重要:如果PV在20到30之间波动,而整个波形图始终映射到0到100的满量程,那曲线就会变成几乎看不出起伏的一条直线。正确做法是追踪当前显示窗口内PV的最大值和最小值,动态调整y轴刻度范围,让波形充满显示区域。

曲线绘制还有一个细节:点与点之间要连线,而不是只画孤立的点。两个像素点之间用刚才提到的画线函数补点,曲线看起来就连续了。当然OLED像素间距大,波形本来就会有点锯齿感,这属于正常现象,不影响看趋势。

为了提高可读性,我习惯在曲线界面同时画两条线:一条是目标值SP的水平虚线,一条是PV的实线曲线。这样一眼就能看出超调量是多少、有没有等幅振荡、稳态误差有多远。SP虚线的绘制逻辑很简单,就是y轴坐标固定在目标值对应的像素位置,每隔几个像素画一个点,形成间断效果。

刷新率不用太高。OLED的刷新率控制在10到20Hz就很舒服,肉眼看着流畅,也不会占太多CPU。要注意的是刷新逻辑不要放进控制中断里,最好在主循环里按时间片定期刷新,否则很容易干扰控制时序。

3.4 参数存储与掉电保护

整定参数如果不保存,断电就回到默认值,等于每次重新上电都要重新调一遍,这在很多项目里是无法接受的。所以我给界面加了一个保存功能:进入保存确认界面后,按一下确认键,把当前所有参数写入存储介质。

存储介质的选择取决于硬件平台。STM32系列很多内置Flash,可以划出一页专门存参数,这个方案不增加硬件成本,但要处理好Flash擦写寿命。Flash的擦除次数一般在一万次以上,看起来不少,但如果你每次改参数都写一次,天天调试的话很快就磨完了。我建议把存储策略设计成"只在界面明确执行保存操作时才写一次",而不是改了参数就自动保存。参数修改中途断电顶多丢失本次修改,不会损害Flash寿命。

如果你用的是I2C接口的外部EEPROM,比如AT24C02,代码更简单,直接按地址写即可。但要注意EEPROM也有写周期,写完一个字节要等几毫秒才能再写下一个,最好用页写模式批量写入,效率能高很多。另外我强烈建议在保存的数据头部放一个校验标记,比如固定的magic number加一个简单的累加校验值,上电加载时先校验,校验不过就回退到默认参数——这个防呆设计能在固件升级或存储异常时避免控制参数变成一堆垃圾值。

4. 与PID整定的衔接:界面如何辅助整定

4.1 在线调参与整定流程结合

界面搭好之后,整定流程会发生质的变化。以最经典的试凑法为例,原来的流程是:改Kp→烧录→运行→看串口→改Kp→烧录,现在的流程变成:

  1. 在主界面启动电机/加热器,让系统进入稳定运行状态。
  2. 进入参数菜单,把积分时间Ti先设成最大值(等效于暂时去掉积分),微分时间Td设成0。
  3. 设置一个初始Kp,比如0.5。
  4. 切回主界面,看PV是否靠近SP,观察曲线是否出现振荡。
  5. 如果振荡明显,旋编码器调小Kp;如果响应太慢,调大Kp。
  6. 观察曲线形态稳定后,再进入参数菜单设置Ti、Td。
  7. 满意后进入保存界面,按确认存储参数,断电重启验证加载是否正常。

整个过程完全不需要重新编译烧录,参数修改即时生效,观察实时响应。这种高频交互才是整定的正常节奏。我需要提醒一点:参数修改一定要加上输出限幅保护,不能让参数修改瞬间导致输出跳变到极端值,否则可能损坏执行器。我的做法是在参数修改后的回调函数里,立即用新的参数重新计算一次输出值,并做限幅钳位。

4.2 用波形界面判断整定质量

整定质量的好坏,很少靠计算出来,更多靠看曲线形态来判断。有了波形界面,你就能直观识别几种典型的整定状态。

欠阻尼/过冲:PV曲线冲过SP线,然后来回振荡,如果振荡能快速收敛,说明当前Kp偏大或者积分偏强,可以尝试降低Kp或者增加一点微分;如果振荡不收敛甚至越来越发散,那说明增益已经过大,必须立即调小。

过阻尼/响应太慢:PV曲线慢慢腾腾往SP爬,半天不接近,说明Kp太小或者积分时间太大,响应速度不能满足要求。这时候应该增大Kp。

等幅振荡:PV在SP附近来回等幅摆动,这正好是临界比例法的起点。记住此时当前Kp值,再从波形上读取振荡周期,用Ziegler-Nichols公式就能算出推荐参数。在这个场景下,界面上的波形显示和当前参数显示配合起来,价值直接翻倍——以前还要用上位机记录数据再手动游标测量周期,现在直接在OLED上估个大概,心里就有谱了。

还有一个容易忽略的技巧:曲线的横轴时间尺度要可以切换。整定一开始可能要看秒级的响应,等系统稳定后想观察稳态波动,又需要分钟级的历史数据。我在设计环形buffer时预留了不同的采样间隔档位,通过界面可以切换,适配不同阶段的观察需求。这个功能看起来简单,实际使用中的价值非常大。

5. 调试中遇到的坑与排查实录

5.1 OLED显示异常排查

用OLED屏时最容易踩的坑就是I2C地址问题。SSD1306常见的从机地址是0x3C和0x3D两种,跟模块上地址选择电阻有关。如果你发现屏幕完全没反应,先用I2C扫描程序把总线上所有设备的地址打印出来,确认屏的真实地址再初始化。这个坑我踩过不止一次,换模块厂家就踩一次。

还有一个很诡异的坑:屏幕刚上电能亮,运行几十分钟后花屏或者熄灭。这种问题多半是I2C总线时序被干扰,常见原因是上拉电阻太小导致总线负载过重,或者电源纹波过大。排查时可以给OLED模块单独并一个100nF的陶瓷电容,总线上的上拉电阻换成4.7kΩ到10kΩ之间再试。

5.2 编码器方向不对和数值抖动

编码器在调试中有两个经典问题。第一个是方向反了,顺时针旋竟然减数值,逆时针反而增加。这个不是硬件坏了,只是AB相序接反了。解决办法很简单,在编码器读取函数里把正反方向判断反过来就行,或者在代码里加一个direction标志位,软件层面一键翻转。千万别去焊线换引脚,软件改一行比硬件改线省事一百倍。

第二个问题是数值跳动,明明只旋了一格,数值却跳了两三格。这个问题的根源是机械抖动,即触点接触过程中产生了多次电平跳变。如果编码器接在外部中断上,抖动会导致中断多次触发,每触发一次就解算一次步进。解决方案是加软件消抖,检测到AB相变化后延时2到5毫秒再采样确认状态,确认之后才认为这是一次有效旋转。注意消抖延时不能太长,否则快速旋转时会丢步,让调节手感变得迟钝。

5.3 参数存储丢数据

参数保存到Flash后,上电加载时校验失败,这属于比较麻烦的问题。常见原因有三个:一是写入地址越界,覆盖了别的代码区域或者超出了Flash容量;二是写Flash前没有先擦除整个页,导致写进去的数据和旧数据做了按位与,结果变成乱码;三是写入过程中突然断电,数据写了一半。前两个问题靠仔细检查地址映射和擦除流程就能解决,第三个问题只能靠校验标记来兜底,加载时发现校验失败就使用默认参数并给出提示。

5.4 共享变量与中断竞争

最后必须强调一个嵌入式开发的基础问题:界面层和控制层共享变量时,一定要处理好并发访问。比如PID参数Kp是一个全局变量,界面菜单在修改它,而控制中断在读取它参与计算,如果修改过程不是原子操作,就可能出现控制中断读到半更新状态的Kp,导致输出毛刺。

对于float类型变量,在32位单片机上一般可以保证原子读写,但不建议依赖这个假设。稳妥的做法是:在修改参数前临时关中断,修改完成后马上开中断,或者把参数修改放在控制中断里执行,界面只是把修改请求通过一个队列传给中断。我通常用前一种简单粗暴的方式,只要关中断的时间足够短(几个微秒),对实时性几乎没有影响。

显示器刷新也有类似的注意点:图像buffer可以分块刷新,但如果你在绘制的过程中控制中断也在读同一个buffer,建议用双缓冲机制,一个buffer给绘制用,另一个buffer给刷新用,绘制完成后交换。不这样做的话,偶尔会出现画面撕裂,看起来像屏闪,排查起来非常费劲。

6. 实际调试过程实录:一次典型的参数整定

说了这么多设计思路,还是拿一个实际场景串一遍流程更有说服力。我最近调一个直流电机转速闭环,硬件就是STM32F103加0.96寸OLED和EC11编码器,电机用PWM驱动,编码器测速。整定目标是把转速精确稳定在一个设定值上。

上电后界面启动,主界面显示SP=0、PV=0、OUT=0,模式为手动。我先把电机开环输出拉到50% PWM,等转速稳定,然后切到自动模式,设置SP=1000转/分。第一次用Kp=0.8、Ti=5000、Td=0跑起来,波形界面马上看到转速先冲上去,冲到1400,然后掉下来又冲上去,明显超调很大,而且振荡了三四次才稳住。

于是我去参数菜单,把Kp从0.8调到0.4、Ti从5000调到2000,确认后切回波形界面。这次曲线明显收敛快很多,超调从40%降到了15%左右。但静差还是有点大,转速离1000差着20转左右,因为积分太弱。我再把Ti从2000调到800,这次静差没了,超调略升到18%,波形最终收敛到±5转以内。

整个过程大概5分钟,没有重新烧录过一次固件,全程就在界面上旋旋钮、看曲线。这就是人机界面的价值——工具链顺了,整定效率完全不是一个量级。

按我个人经验,这套界面思路并不局限于PID整定。任何需要频繁交互调参的固件场景——滤波器参数、电机控制环、温控曲线规划、甚至简单的传感器校准流程——都可以套用同一个框架:菜单结构管理参数,曲线显示观察效果,存储机制保证可复现。界面是一种基础设施,建一次,后面所有调试工作都会受益。

最后分享一个容易忽视但非常实用的小建议:界面的操作逻辑一定要保持肌肉记忆友好。比如编码器单击是进入菜单/确认,长按是返回上级,这些操作要在所有界面里保持一致,不要在不同的界面用不同的交互方式。我在早期版本里犯过这个错误,主界面单击进入菜单,曲线界面单击却变成暂停刷新,结果每次调试都要抬头想一下"我现在按下这个键会发生什么",这种心智负担在长时间调试时特别消磨人。把交互规则统一后,整定流畅度又提升了一截。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询