ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

固件人机界面设计:让PID整定告别反复编译烧录

固件人机界面设计:让PID整定告别反复编译烧录 写固件的人应该都有过这种经历板子还在桌上PID 还没调数据散乱一堆每次改个 Kp 都得重新编译烧录一晚上光等下载器来回跑了。这个系列做到第 7 期我最大的体会就是整定之前先别急着调参得先把人机界面在固件里立起来。什么叫人机界面不一定是大屏触控可以是串口命令行、OLED 菜单甚至一组按键加数码管。它解决的核心问题只有一个让你在运行中能看状态、改参数、做控制而不是每次改一行代码就重新烧录。这篇文章就把我这些年攒下来的固件人机界面设计思路、关键代码骨架和踩坑记录完整梳理一遍给正准备调参的朋友做个参考。1. 整定之前为什么先做界面1.1 你被“改个参数就重编译”劝退过几次很多做嵌入式控制的朋友起步都是这么调的先用串口往电脑上打印一组数据比如当前误差、PWM 输出、转速或者温度然后看一眼波形觉得超调大了就回去改 Kp重新编译重新下载再开机看波形。运气好一次改对运气不好来回十几次光浪费时间不说关键是自己根本看不到系统在整个动态过程中的变化只能靠事后打印的数据猜。我当时做温控项目时也被这个流程折磨够了。后来想明白一件事所谓整定说白了就是在线观察系统响应、在线改参数、看结果立刻反馈。如果你没有一个能在运行时修改参数的入口整定就是盲人摸象。所以第 7 期这个阶段我先把人机界面这块底子补上了后面再动 PID 参数的时候才顺手。1.2 整定不是只改三个数字那么简单很多人以为人机界面就是做个菜单能改 Kp、Ki、Kd 就算完事。真去现场调过就知道整定需要的界面远不止改数字这么简单。整定过程中你要做几类操作一是实时观察目标值、反馈值和输出值确认当前系统处于什么状态二是临时切换手动输出先看执行器是否正常动作三是扫描式微调参数并观察趋势四是保存一组觉得靠谱的参数防止掉电丢失五是有时候还需要强制让系统跑一个阶跃信号这样才能看出动态性能。这几类需求加在一起界面就不该只是一个“改数页面”了它应该是一个具备状态显示、参数编辑、执行控制、参数存储四个基本能力的交互层。我这一期在固件里搭建的就是这么一套完整的交互框架。2. 方案选型串口命令、OLED 菜单还是上位机2.1 命令行界面调试阶段最不能省的基础设施人机界面不一定是实体屏幕。很多项目前期硬件还没定或者根本没地方放屏幕这时候 串口命令行界面CLI 就是性价比最高的选择。我在固件里实现了一个非常轻量的命令解析器通过 UART 接收字符串解析命令动词和参数然后调用对应的处理函数。比如输入pid.set 12.5 0.8 0.02就把 Kp、Ki、Kd 三个值同时写入 RAM 副本下次控制周期立刻生效。输入output 40就直接输出 40% 的 PWM方便测试执行器。输入save就把当前 RAM 里的参数整体写入 Flash。CLI 的好处是开发成本极低只要有串口就能跑而且调试信息天然就在同一个通道里。坏处是不直观改完参数还得靠另一条数据流看效果如果你只打印一行行裸数字人脑处理起来很累。所以我把 CLI 定位成底线设施有它至少不瞎。2.2 OLED 菜单界面现场调试最实用的形态等硬件条件允许加一块 0.96 寸 OLED 或者 12864 点阵屏配两个按键加一个旋转编码器现场不需要接电脑就能操作。这是我最推荐的一种组合因为在工业现场很多设备摆在柜子里旁边根本没有电脑你不可能抱着笔记本去拧螺丝。按键加屏幕的界面实现起来也不难关键是你要想清楚交互逻辑。我用的方案是一个根页面显示实时数据例如设定值、反馈值、PWM 输出每 200ms 刷新一次长按确认键进入参数菜单用旋转编码器上下翻动短按确认进入编辑态旋转编码器改变数值再次短按确认生效。这套交互逻辑在单片机资源紧张的前提下完全可以跑得很流畅。2.3 上位机和 Web 界面曲线观察是整定刚需真到整定阶段最需要的不是看数字而是看曲线。数字看不出趋势曲线才知道超调多少、震荡频率多高、收敛快不快。所以我强烈建议在固件端预留一个实时数据输出通道往串口周期性发送一组格式化数据再由上位机软件或者脚本接收绘图。我用过一个比较偷懒的办法固件端以固定周期比如 10ms通过串口发送T,目标值,反馈值,输出值这种文本帧然后在上位机用 Python 的 pyserial 读数据再用 matplotlib 实时刷新画线。虽然刷新率不高但对整定来说足够用。想更流畅就用 pyqtgraph曲线拖拽缩放都不卡。2.4 三种方案怎么选的情况对照界面形态硬件成本开发量适用场景缺点串口 CLI零成本小开发前期、桌面调试不直观曲线观察弱OLED 菜单二三十元中现场无电脑、产线维护屏幕小图表展示有限上位机/Web 界面中高较大复杂波形分析、参数扫描依赖电脑移植工作量偏大我建议的开发路线是先用 CLI 把底层参数读写跑通再加 OLED 菜单最后补一条串口数据输出给上位机画曲线。这三件事是递进关系不是互斥关系而且底层代码可以大量复用。3. 固件里把人机界面“长”出来的骨架设计3.1 界面层、控制层和参数层千万别揉在一起很多刚入行的朋友写固件界面容易犯一个毛病屏幕刷新、按键扫描、控制计算全塞在同一个主循环里代码越写越长最后加一个功能就要动三处。我在这个项目里一开始就做了严格分层。界面层只负责把菜单画到屏幕上以及把按键动作翻译成菜单事件参数层维护一个参数结构体里面放着所有可调参数任何界面改的数值都先写进这个结构体的 RAM 副本控制层则在每个控制周期里读取这个结构体计算完输出 PWM然后更新反馈数据。这样哪怕界面写得再花哨只要不直接碰控制变量控制逻辑就不容易被带崩。参数结构体本身我习惯用一个大结构体集中管理比如typedef struct { float kp; float ki; float kd; float target; uint8_t mode; // 0: 自动 1: 手动 float manual_out; // 手动输出百分比 uint16_t crc; } param_block_t;所有功能模块只用变量名访问参数界面层只管修改这些成员保存函数把这个结构体整体搬到 Flash。这样最直观也不会丢字段。3.2 菜单状态机怎么搭才不晕菜单看似简单但要支持多级导航、编辑态、确认态和返回态不用状态机写会乱成一锅粥。我实现了一个非常经典的四态菜单状态机主界面、菜单列表、参数编辑、数据保存。状态定义成枚举typedef enum { UI_MAIN, UI_MENU_LIST, UI_PARAM_EDIT, UI_SAVE_CONFIRM } ui_state_t;每次按键事件进来状态机做一次跳转。比如在UI_MENU_LIST里短按编码器进入选中项的编辑态在UI_PARAM_EDIT里短按编码器确认修改并退出编辑态在UI_SAVE_CONFIRM里再次短按触发 Flash 写入。状态跳转之间通过一个小的事件队列传递使用一个ui_event_t结构体避免在中断里直接改菜单状态引发竞态。我踩过的一个教训是千万不要在按键中断里直接调用显示刷新函数。中断里只把事件放进环形队列主循环里统一处理否则一个长按按键就能让整块板子卡死因为中断嵌套会一直在刷屏。3.3 参数掉电保存要做得可靠而不是“能存”参数保存这块我见过太多同学直接往内部 Flash 一个固定地址写数据重启后读回来乱码还找不到原因。这多半是没管几个细节写入前要擦除整个扇区、要加魔数和 CRC 校验、要处理写入过程中掉电导致的数据损坏。我的做法是在 Flash 里分配两个备份区每次先写备份区 B写完校验通过后再写主区 A。启动时先读 A如果 CRC 校验失败就自动加载 B两边都失败才恢复默认参数。这样虽然多占一点 Flash 空间但可靠性提升非常多现场设备最怕的就是参数莫名丢失然后背锅。另外要注意内部 Flash 有擦写寿命限制虽然没有机械硬盘那么脆弱但也不要每次保存都整片擦写。批量修改完参数后再统一按“保存键”而不是每改一个数就立刻触发一次 Flash 写入这样既能保护 Flash也符合用户心智。4. 给整定量身加上这几个实用功能4.1 参数热修改改完立刻生效不重启不重新烧录这是整个界面的核心价值。参数热修改的本质是所有 PID 参数都放在 RAM 里的结构体上控制中断或者控制任务每个周期读取的是这个 RAM 副本而不是 Flash 里的固定值。所以你在菜单里改 Kp 时改变的只是 RAM 里的param_block.kp下一拍控制计算立刻就能用上新值。这个改动对用户来说是“热”的不用重启、不用重新烧录、不用等 Flash 写入完成。在实现上有一点要特别注意如果控制计算在执行于中断中而参数修改在后台任务中就涉及临界区保护。最保险的办法是把参数读取设计成一次“快照式”读取也就是控制周期开始时一次性拷贝当前参数到局部变量整个周期都用这份副本避免 Kp 读到一半时 Ki 被改掉导致计算错乱。4.2 手动输出模式先让执行器听话再说自动的事很多人一上来就直接跑 PID结果输出根本没反应查半天发现是 PWM 极性反了或者执行器卡死。所以我在界面里专门留了一个手动模式可以在菜单里设一个 0 到 100 的输出百分比系统直接输出对应的 PWM 占空比。这个功能在整定前的硬件排查阶段特别管用。你可以先手动给定 30% 的 PWM观察电机转速或者加热功率是否正常再手动给到 60%看响应是否符合预期。执行器没问题了再切回自动模式去做闭环整定。我习惯在菜单里做一个“模式切换”页面一个按键就能在自动和手动之间切换现场调试效率提高很多。4.3 阶跃信号发生器看响应曲线的标准方法调 PID 想看出动态性能最简单有效的方法就是给系统一个阶跃目标。比如目标值从 30% 瞬间跳到 50%然后观察反馈值怎么跟踪、超调多少、多久稳定下来。有些朋友直接在整定界面里把目标值一改再用上位机画曲线这也算阶跃但操作起来不够标准。更好的办法是在固件里做一个“自动阶跃测试”功能启动测试后目标值按照预设的起止值和持续时间自动切换比如先输出 30% 的阶跃序列然后每 5 秒交替切换到 50%上位机记录整个过程的响应曲线。这样就非常方便地比较不同参数下的超调和响应时间了。4.4 参数组保存和还原敢大胆尝试才叫整定整定时免不了反复试参数。如果每试一组就手动记一下数值再改下一组换来换去很容易记混。我在参数系统里加了三组参数存储可以用菜单切换“参数组 1 / 2 / 3”每组独立保存到 Flash 的不同位置。这样做的意义是现场整定时我先存一组初始参数然后大胆往一个方向调如果效果不好一键切回上一组。这个“后悔药”体验对有动手强迫症的人太友好了也鼓励你多尝试参数组合找到最优解而不是靠感觉一次定死。5. 实测遇坑记录和排查技巧5.1 界面一刷新控制波形就抖动这是我这个项目里第一次遇到也是最典型的问题。OLED 刷新一帧画面的时间大概在 20 到 40 毫秒如果在主循环里刷新屏幕时控制周期恰好在同一线程里被打断波形上就会出现周期性尖刺看上去像控制环路不稳其实纯粹是显示刷新抢占了控制周期。解决思路很明确把控制任务和界面任务彻底分离。控制计算放在定时器中断或者 FreeRTOS 高优先级任务里界面刷新放在后台低优先级任务里并且界面刷新时不阻塞控制周期。实测下来把 OLED 的刷新频率限制在 20 帧以内再配合控制周期 1ms 的中断波形就不再受界面干扰了。5.2 改成 “保存”重启后参数还是旧值这个问题排查起来也很有代表性。起初我在菜单里修改完参数后直接调用 Flash 写入函数重启后参数却还是旧的。后来发现是参数结构体里有浮点数结构体对齐和实际大小在不同编译选项下会变化导致我写入 Flash 的长度和启动时读取的长度不一致后几个字段根本没被读到。解决办法是使用#pragma pack(push, 1)定义参数结构体并且在保存和读取时都用sizeof(param_block_t)同时写入魔数和 CRC32 做校验。另外把 Flash 写入函数改成“写前擦、写后备、再校验读回”这样每次保存都应该能可靠恢复。5.3 旋转编码器经常跳数按键偶发失灵旋转编码器如果不用硬件滤波或者软件滤波稍微有一点抖动就会一次跳好几个数尤其是在金属机箱附近干扰较大的场景下。我在编码器处理里加了简单的状态机消抖和方向判断并且把采样间隔控制在 2ms 以上跳数问题基本消失。按键问题也类似最好的办法是在 GPIO 外部中断里加软件延时消抖或者干脆用一个 10ms 的定时器轮询按键状态并做连续 N 次一致的判定。不要在中断里直接修改界面参数事件入队再统一处理这一点前面也提过但值得再强调一次因为它能帮你省掉大量调试时间。5.4 整定曲线数据串口打印和 CLI 命令互相打架了CLI 和实时曲线数据都走同一个串口时经常出现命令行输出的字符被曲线数据打断或者在调试终端里看曲线时命令区的显示全乱了。虽然不影响实际数据但体验很差。我的处理办法是把串口资源分成两条逻辑通道一条是命令通道采用行缓冲输入命令按回车才解析另一条是数据通道用逗号分隔的 CSV 帧输出。如果硬件只有一个串口可以在时间上分片命令输入时暂时抑制数据打印或者把数据显示放到一个独立的 DMA 缓冲区由上位机单独处理。用两个串口当然最省心一个接调试终端一个接数据采集。6. 最后分享一点实操感受这几期项目做下来我最大的感觉是人机界面看着不起眼但它是整定这项工作的地基。把界面提前做好后面改 PID 参数就从“重新编译烧录”变成“拧拧旋钮看曲线”效率差别不是一点半点。如果你也在做一个带闭环控制的固件我建议先花一两天把上面说的 CLI 加参数系统搭起来这套框架后面加 OLED 和上位机都只是锦上添花而且对你理解整个控制系统的行为会很有帮助。真到整定那天你就能体会到“参数在指尖跳动波形在屏幕上流动”的顺畅感了。
返回列表