ARTICLE DETAIL

资讯详情

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

整定前先做界面:嵌入式PID调试的菜单设计实战

整定前先做界面:嵌入式PID调试的菜单设计实战 “整定”俩字一提很多玩嵌入式控制的朋友眼睛就亮了。但真正动手调 PID 的时候才发现最花时间的根本不是算法本身而是怎么把参数送进去、怎么把实时状态捞出来。上一期咱们讲了怎么把基础固件框架搭起来这一期我想换个思路聊一件事——在把精力投入整定之前先把人机界面做出来。可能有人会问界面不是最后才做的吗我一开始也觉得嵌入式开发就该“底层先行、界面殿后”后来实际调了几块板子才明白没有一个看得见、摸得着的界面你连参数要从哪儿改、改完什么效果都说不清楚整定纯粹是盲人摸象。这篇文章就以“先做界面再做整定”为主线聊聊我在实际项目中是怎么在资源有限的 MCU 上给人机界面腾出位置的包括方案选型、菜单结构设计、整定联动逻辑和一些踩坑经验。适合手里已经有一块能跑起来的目标板、正准备开始调参数的朋友参考。1. 整定之前为什么偏偏要先做界面1.1 没有界面的整定就像蒙着眼睛调琴先描述一个非常常见的场景。你拿到一块电机控制板固件下载进去了电机也转了开始调 PID。第一次改 P 值改完重新编译烧录上电观察几个波形发现不对再改再烧录。一次五分钟十次五十分钟看起来好像也能调实际上效率极低。而且这还只是单参数调整。如果三个参数 P、I、D 同时要调加上目标转速、加速度、限幅值参数组合一多你根本记不住上一次试的是哪一组。没有界面就没有“现场感”你只能通过串口打印或者示波器间接判断整个调试过程非常痛苦。我之前帮一个朋友调过一台小型风机控制器他的原方案就是串口打印调试信息。问题是风机的转速波动是动态的串口打印每次只输出一屏看完就过去了根本没法对比不同参数下的响应曲线。后来我在他的固件里加了个简单的 OLED 菜单界面参数现场就能改曲线现场就能画整个调试时间缩短了大概三分之二。这件事给我的触动挺大——界面不是最后糊上去的一个壳它本身就是调试工具的一部分甚至是整定流程里最关键的“仪表盘”。1.2 界面承担的四个核心职责细想一下调试用的人机界面在整定场景里其实承担了四件事。第一是“看状态”。系统当前的输出、反馈、误差、温度、电压这些实时量没有界面就只能靠猜。有了界面一眼就能看明白系统现在到底处于什么状态。第二是“改参数”。整定的核心动作就是反复修改 P、I、D 的值。一个能直接在目标板上修改参数并立即生效的界面比电脑上改代码再烧录不知道高效多少倍。第三是“存现场”。界面不只是给你看的还可以把整定过程中产生的“好参数”“坏参数”分别存下来掉电不丢失。调试到一半有事走开回来接着调参数还在这个体验非常舒服。第四是“验手感”。在界面上手动给定一个目标值、手动调一个输出可以快速判断执行机构有没有问题排除机械和驱动部分带来的干扰。没有界面这些动作都要靠外部设备模拟麻烦得很。1.3 不是所有场景都需要先做界面当然我也得说句公道话不是所有固件都要在整定之前做人机界面。如果你的产品本身就没有显示需求而且整定场景非常固定比如只需要在产线上用电脑标定一次用完就不再调整那完全可以通过上位机来做。用 Qt 写个小工具串口连上参数协议发过去也一样能完成整定。这种情况下固件里只需要预留一个简单的通信协议接口就够了界面做在电脑上比做在板子上更灵活。但反过来如果你的设备是现场使用的、需要经常调整工作模式的或者你本人经常在实验室里反复折腾同一块板子那固件里就应该有一块界面。我的取舍标准很简单看“是否需要在脱离上位机的情况下对运行参数进行频繁的查看和修改”。需要就老老实实在固件里做界面不需要再考虑电脑上位机方案。2. 资源受限 MCU 上的人机界面方案选型2.1 有屏还是无屏这是一个先决问题做界面第一步是选硬件。常见的有屏方案大致分这么几类OLED 小屏、LCD 彩屏、串口屏还有一类是“无屏但有人机交互”比如按键加状态灯、手机 APP 通过蓝牙连接、纯串口命令行等。OLED 小屏尤其是 0.96 寸 SSD1306 驱动的是调试界面里最实用的方案之一。128×64 分辨率虽然不高但显示参数名称和数值绰绰有余。驱动简单I2C 两根线就能搞定内存占用也不大。我自己的调试板十块有八块都装了这种屏。LCD 彩屏适合需要画实时曲线、显示内容特别多的场景。比如要在界面上同时显示目标转速、实际转速、PWM 占空比、母线电压、温度再画一条实时波形OLED 那点分辨率就不够用了。ILI9341 这类 320×240 的屏配上 STM32F4 或更高性能的 MCU效果会好很多。缺点是接线多、缓存大、驱动复杂硬件成本也上去了。串口屏就是那种屏幕自己有 MCU 和通信固件的显示屏主控 MCU 通过串口发指令来控制显示和交互。好处是主控端几乎不占资源缺点是人机交互逻辑分散在屏幕端和主控端调试起来思维负担比较重。我一般不建议在调试用的板子上引入串口屏除非你非常熟悉那款屏的指令集。2.2 按键交互的设计思路有屏还不行得考虑用户怎么操作它。大多数嵌入式界面不是触摸屏所以按键交互是核心。最朴素的方案是“两个键加一个确定键”。上下键切换菜单项、调整参数值确认键进入子菜单或保存当前值。这种方案逻辑简单、代码好写、硬件成本极低唯一的问题是菜单层级深了以后操作起来很累。稍微进阶一点的是“旋转编码器方案”。一个带按键的旋转编码器旋转用来上下移动或增减数值按下用来确认手感比双键方案好很多代码也没复杂到哪里去。我特别推荐在调试界面上用编码器因为整定参数的时候需要频繁微调数值编码器一次转半格、一格、五格调节精度和速度控制比按键舒服太多了。还有一种方案是矩阵键盘适合菜单结构复杂、需要快速跳转的场景。但矩阵键盘处理起来复杂度成倍上升多键组合、防冲突、按优先级这些都要考虑调试板不太建议上。2.3 没有屏幕的“界面方案”——HID 命令行与终端交互有些项目板子小到连 OLED 都塞不进去但你还是需要人机交互。这种情况下我的做法是用 HID 或者虚拟串口做“命令行界面”。具体来说通过 USB 把 MCU 模拟成一个 HID 设备或者 CDC 串口设备在电脑上开一个终端输入类似set p1.2、get feedback这样的命令来控制固件。本质上跟“界面”是一样的只是交互通道从屏幕换成了字符终端。前阵子我还在一批量产表计上看到过类似思路量产固件本身不带任何调试界面但预留了一组 AT 指令。产线通过串口发送特定指令让固件进入标定模式整个过程不需要烧录专用标定固件。这个思路很值得借鉴——界面形式和硬件选型无关核心是“提供一套可交互的参数访问通道”。3. 实际动手给固件设计一套菜单界面3.1 核心设计参数、界面、动作三层解耦很多人一提到界面就想到各种 UI 库、控件、事件驱动实际上一个调试界面根本不需要那么复杂。我的经验是三层结构就够用。最底层是“参数层”。所有需要暴露给用户的数据不管是只读的状态量还是可读写的参数量都用一个统一的结构体数组描述。每个元素说明它的名字、类型、当前值的指针、上下限、步进值。中间是“界面层”。界面层不直接操作参数而是通过索引号引用参数层的某个元素。比如菜单第 2 项显示“P 值”它背后对应的就是参数层里索引为 5 的那个浮点数。最上面是“动作层”处理用户按键。按“上”就改变当前选中参数的数值按“确认”就调用参数层的写入回调函数把当前值存入 Flash。这个三层解耦的好处是界面代码和业务代码互不干扰。以后如果你想把 OLED 换成 LCD或者想加一个上位机界面只需要把中间层和动作层重写一遍参数层和数据层完全不用动。3.2 菜单树怎么设计菜单树的设计直接决定调试效率。我见过一些人把菜单设计成“一层套一层”的树形结构这种结构信息容纳量大但调试时来回切换非常麻烦。我建议调试界面用“扁平化 分组”的方式。什么是“扁平化 分组”呢把参数按功能分成几组启动界面显示分组列表按确定键进入某一组后组内的参数全是平铺的。这样你调 P 值和调 I 值只需要在同一个菜单里上下移动不用反复进出子菜单。举例来说主菜单状态监控 / 参数设置 / 系统工具参数设置又分成速度环参数、电流环参数、位置环参数、输出限幅参数速度环参数里就是平铺的 P、I、D、积分饱和下限、积分饱和上限这套菜单最多三层基本上两三次按键就能定位到一个参数。相反如果你把每个参数都设计成独立菜单节点七层八层的每天调参光按按键就能按到手酸。3.3 代码结构示例一个简洁的菜单框架下面给出一段我实际用过的简化版菜单框架用 C 语言实现方便大家对照理解。逻辑上就是一个“当前选中项”加上“按键回调”。typedef struct { const char* name; // 参数名称 float* value; // 指向参数实体的指针 float min; // 上限 float max; // 下限 float step; // 步进值 void (*save)(void); // 保存回调 } menu_item_t; typedef struct { const menu_item_t* items; uint8_t count; uint8_t selected; } menu_page_t; static menu_item_t speed_loop_params[] { { Kp, pid_speed.kp, 0.0f, 100.0f, 0.1f, save_params }, { Ki, pid_speed.ki, 0.0f, 100.0f, 0.1f, save_params }, { Kd, pid_speed.kd, 0.0f, 100.0f, 0.1f, save_params }, { Sat, pid_speed.sat, 0.0f, 500.0f, 1.0f, save_params }, }; void menu_on_key_up(void) { menu_current-selected (menu_current-selected 1) % menu_current-count; } void menu_on_key_down(void) { if (menu_current-selected 0) { menu_current-selected menu_current-count - 1; } else { menu_current-selected--; } } void menu_on_key_enter(void) { menu_item_t* item menu_current-items[menu_current-selected]; item-value item-step; // 或者根据按键类型做加减 if (item-value item-min) item-value item-min; if (item-value item-max) item-value item-max; if (item-save) item-save(); }这里有个细节值得说save()回调函数里不能每次都写 Flash。Flash 的擦写寿命一般在十万次左右如果你每次按一下键就存一次几天折腾下来 Flash 就废了。我的做法是界面上加一个“存”动作只有当用户明确按“保存”键时才把当前整组参数写入 Flash。修改当前生效用 RAM 变量持久化保存才进 Flash两者分开。3.4 实时数据显示区域的实现界面上除了改参数还要实时显示状态量。SSD1306 这类小屏刷新率有限你不能每毫秒刷新一次整屏会闪到怀疑人生。我的经验是区分“快变量”和“慢变量”。快变量比如 PID 输出值、编码器反馈速度这些用屏幕顶部或底部的固定区域显示每 50 到 100 毫秒刷新一次数值。慢变量比如温度、电压、累计运行时间放在详情页里每 500 毫秒刷新一次就够了。两种变量刷新频率差异很大如果都按最快的那档刷新MCU 的负载会平白无故多出很多而且屏幕还会闪。至于实时曲线的绘制OLED 这种小屏其实也能画。思路是用内存里的一段环形缓冲存储历史数据然后按时间顺序把数据点映射到屏幕坐标。分辨率不够没关系能看出波动趋势和超调量就达到目的了。如果确实需要高分辨率的曲线那就得上彩屏 LCD。4. 界面和整定流程的联动设计4.1 界面要能“现场改现场生效”整定过程中最忌讳的就是改完参数还要重启或者重新编译。既然界面都做了参数修改就应该立即生效。我在实现里是把 PID 参数结构体的每个浮点数直接映射到菜单项上。菜单里面改PID 控制器下一次控制周期读到的就是新值连“应用参数”这个动作都不需要因为指针直连。这种做法唯一的风险是如果你在参数更新了一半的时候系统刚好执行控制计算比如 P 已经改成 2.0 了I 还是旧值那么这一拍控制量可能会出现跳变。在电流环这类快速系统里这个跳变可能是致命的。所以我在做这种“热修改”时会在修改入口加一个全局的“参数更新中”标志位控制任务在标志位置位时暂停一次等菜单操作完成后统一启用新值。看起来是个小细节实际能避免很多莫名其妙的问题。4.2 手动模式与自动模式的切换整定的时候还有一个高频操作是“切换手动/自动模式”。手动模式下手动给定输出用来验证执行机构是否正常自动模式下由 PID 计算输出用来观察闭环响应。界面上一定要安排一个醒目的手动/自动切换入口最好是物理上独立的一个按键或者编码器的按下动作而不是藏在菜单深处。我吃过这个亏之前有一版界面把模式切换放在菜单第三层有一次现场调试发现执行机构不动想切手动直接给输出结果按了五六次按键才进去操作一气都憋没了。后来我就把模式切换提出来作为全局动作任何界面下都能一键切换并且切换的时候界面顶部状态栏显示当前模式避免误操作。4.3 参数组保存与恢复整定过程中最宝贵的资产是那些“碰巧调出来”的好参数。你花了两个小时在 3 组参数里反复对比这时候界面如果没有多组参数存储功能就只能拿笔往纸上抄抄错一个数前功尽弃。我的做法是在系统工具菜单里放三组存储槽位A、B、C。每组槽位可以保存当前全部参数下次整定可以从任意槽位加载。实际整定流程就是初始参数存到 A 槽微调一段时间存到 B 槽再微调存到 C 槽。来回切换对比哪个效果好保留哪个。这个功能做起来很简单就是把参数结构体整体搬进 Flash但用起来价值极高。4.4 界面辅助整定的完整流程示例写到这里不妨把整套流程串起来演示一遍。假设我现在要调一个直流有刷电机的速度环。第一步上电进入“状态监控”页面确认电机反馈速度显示正常PWM 输出在安全范围内。第二步进入“参数设置”先把 Kp 设成一个很小的值比如 0.05Ki 和 Kd 设为 0。第三步切到手动手动给一个 20% 的占空比确认电机转动正常机械部分没有异响。第四步切回自动模式给定一个目标转速观察“状态监控”页面里的实时反馈值。这时候你会看到一个典型的响应慢速上升、有稳态误差。第五步回到“参数设置”把 Kp 逐步往上加每加一次观察反馈看到轻微震荡就退回到上一个值。第六步加 Ki 消除稳态误差同时观察是否出现超调。第七步把折腾了一下午调出来的参数存到 A 槽再尝试另一组激进参数存到 B 槽对比后留下更好的那组。整个过程全部在板子上完成不需要碰电脑、不需要重新编译烧录。这就是界面先行的价值。5. 常见问题与排查技巧实录5.1 按键没反应或者操作“卡”在半路这是界面开发最容易遇到的问题。排查顺序我从经验上排一下先量电压看按键引脚有没有拉到正确电平再看代码确认按键触发模式是边沿触发还是电平触发最后看中断优先级如果你把按键放在低优先级中断里而控制中断又特别频繁按键事件很可能一直被打断表现出来就是“按了十次只有一两次有效”。解决按键响应问题的通用做法是“边沿检测 软件消抖 事件队列”。按键检测在主循环里做不用中断设置一个 20 到 50 毫秒的消抖窗口检测到有效边沿后把事件丢进环形队列界面任务从队列里取事件处理。这样即使控制中断很重按键事件也不会丢只是处理稍延后一点对用户体验影响不大。5.2 菜单层级深了以后“迷路”菜单层级深了就很容易出现“我在哪一页、当前选中的是什么”的困惑。解决这个问题有两个笨但有效的办法。一个是在界面顶部常驻显示“当前菜单路径”比如设置/速度环/Kp不管你在哪一层都能一眼看到当前位置。另一个是做“长按返回顶层”的快捷操作比如长按确认键两秒直接回到主菜单。这两个功能代码量很小但对调试体验的提升非常明显。5.3 参数显示跳变、精度不对数据显示不对先检查数据类型。浮点数在 OLED 上显示通常要用%d或者固定小数点的方式如果直接把float传给printf的%f在小型嵌入式的标准库里可能会出现意外的舍入行为。稳妥的做法是把浮点数放大十倍再转换为整数显示用户看到的是带一位小数的值底层实际是浮点。比如 P 值是 0.83显示成0.8或者0.83取决于你要显示几位小数转换方法可以这样处理。另一个常见问题是负数的显示。整定过程中比例项、积分项都可能出现负值显示模块如果只处理无符号数你会看到一大串乱七八糟的数字。调试界面一定要处理有符号数并且预留足够的显示宽度不然调负数参数的时候界面显示毫无参考意义。5.4 Flash 写入频繁导致参数丢失我之前遇到过一块板子调参调到一半突然参数全部恢复默认值了查了很久才发现是 Flash 写入过于频繁导致某个扇区提前老化。前面提过“修改用 RAM、保存才进 Flash”这里还要补充一点Flash 写入不能按单个浮点数来写应该整组参数打包后一起写并且写入前先擦除写入后最好校验回读。数据量小的话一次打包不超过几十个字节写起来很快也能减少擦写次数。如果硬件上还有 EEPROM那就更省心可以按参数逐个读写不用处理扇区对齐这些事。不过注意我踩过的一个坑有些 MCU 的模拟 EEPROM 底层还是 Flash擦写次数一样有限只是在驱动层帮你封装好了所以“写入策略”这个事绕不开该限频还得限频。5.5 一个容易被忽略的细节界面冻结与看门狗调试界面如果卡死了比如说 OLED 的 I2C 总线被一根错误接线拉死而你没有对 I2C 通信做超时处理界面任务可能会永久阻塞在等待 ACK 的状态。系统的控制环路还在跑但你已经失去了对板子的控制。这个问题在现场调试时非常致命因为你可能人不在板子旁边电机还在转但无论如何都没法停下来。应对方法有两个一是在 I2C 或者 SPI 通信的驱动层加超时计数总线无响应超过一定时间就主动复位外设二是界面任务定期喂一个独立的调试看门狗界面卡死超过阈值就自动重启界面任务而不是复位整个系统。两种做法配合起来界面可以做到“不死不休”出问题了也会自动恢复。写在最后的实操体会我本来想把这期内容收在问题排查那里但想了想还是多说几句个人体会。做嵌入式这么多年我最大的感受是先做人机界面不是浪费时间而是给后面所有调试工作“铺路”。你越早把一个可看、可改、可存的界面塞进固件后面每一次整定、每一次测试、每一次现场支持省下的时间就越多。界面本身也分层次一开始不用做得太花哨。一个小 OLED、三个按键、一组保存槽位就足够应付 90% 的调试场景。等真的需要更高分辨率、更复杂交互时再升级也不迟因为界面层的代码框架是通用的换屏只是换驱动而已。如果你正打算开始一个带控制算法的固件项目我建议你给自己提个要求在参数整定之前先把人机界面做出来。哪怕只是一个小小的菜单、几行参数显示代码也一定会让你的调试过程省下大把时间。这期内容先聊到这儿后面有机会我继续拆一下界面和上位机如何联合调试感兴趣的朋友可以留意“第 8 期”。
返回列表