ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

单片机固件PID整定界面搭建:OLED波形显示与编码器交互实战

单片机固件PID整定界面搭建:OLED波形显示与编码器交互实战 做过闭环控制调试的朋友应该都有同感整定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/81024字节。在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→烧录现在的流程变成在主界面启动电机/加热器让系统进入稳定运行状态。进入参数菜单把积分时间Ti先设成最大值等效于暂时去掉积分微分时间Td设成0。设置一个初始Kp比如0.5。切回主界面看PV是否靠近SP观察曲线是否出现振荡。如果振荡明显旋编码器调小Kp如果响应太慢调大Kp。观察曲线形态稳定后再进入参数菜单设置Ti、Td。满意后进入保存界面按确认存储参数断电重启验证加载是否正常。整个过程完全不需要重新编译烧录参数修改即时生效观察实时响应。这种高频交互才是整定的正常节奏。我需要提醒一点参数修改一定要加上输出限幅保护不能让参数修改瞬间导致输出跳变到极端值否则可能损坏执行器。我的做法是在参数修改后的回调函数里立即用新的参数重新计算一次输出值并做限幅钳位。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驱动编码器测速。整定目标是把转速精确稳定在一个设定值上。上电后界面启动主界面显示SP0、PV0、OUT0模式为手动。我先把电机开环输出拉到50% PWM等转速稳定然后切到自动模式设置SP1000转/分。第一次用Kp0.8、Ti5000、Td0跑起来波形界面马上看到转速先冲上去冲到1400然后掉下来又冲上去明显超调很大而且振荡了三四次才稳住。于是我去参数菜单把Kp从0.8调到0.4、Ti从5000调到2000确认后切回波形界面。这次曲线明显收敛快很多超调从40%降到了15%左右。但静差还是有点大转速离1000差着20转左右因为积分太弱。我再把Ti从2000调到800这次静差没了超调略升到18%波形最终收敛到±5转以内。整个过程大概5分钟没有重新烧录过一次固件全程就在界面上旋旋钮、看曲线。这就是人机界面的价值——工具链顺了整定效率完全不是一个量级。按我个人经验这套界面思路并不局限于PID整定。任何需要频繁交互调参的固件场景——滤波器参数、电机控制环、温控曲线规划、甚至简单的传感器校准流程——都可以套用同一个框架菜单结构管理参数曲线显示观察效果存储机制保证可复现。界面是一种基础设施建一次后面所有调试工作都会受益。最后分享一个容易忽视但非常实用的小建议界面的操作逻辑一定要保持肌肉记忆友好。比如编码器单击是进入菜单/确认长按是返回上级这些操作要在所有界面里保持一致不要在不同的界面用不同的交互方式。我在早期版本里犯过这个错误主界面单击进入菜单曲线界面单击却变成暂停刷新结果每次调试都要抬头想一下我现在按下这个键会发生什么这种心智负担在长时间调试时特别消磨人。把交互规则统一后整定流畅度又提升了一截。
返回列表