尧图精选

嵌入式PID整定必备:给STM32固件加上在线调参人机界面

🕒 发布时间:2026/9/9 8:55:38 📁 来源:尧图网络
前阵子做一个温度闭环控制板主控是STM32F103加热端用PWM控传感器用DS18B20。电路、底层驱动、PID核心算法都调通了接下来是整定。当时我干了一件非常蠢的事改一个Kp编译烧录等下位机重启盯温度曲线肉眼判断超调再改再烧录。一个晚上下来板子没事人先麻了。问题不是PID算法写得不行而是我没有一个能“现场看数据、现场改参数”的手段。也就是从那天起我意识到整定之前得先给固件“长出”人机界面。这是这个系列的第七期前面几篇把传感器采集、执行器驱动、PID运算都说完了这一期专门记录我补上人机界面这层壳的思路和实现。重点不是把界面做得花哨而是用最低成本换回真正的“调参自由”。1. 为什么抢在整定之前先把界面做出来1.1 整定不是一个动作而是一条观测闭环很多人把PID整定理解为“算三个数”按公式算出Kp、Ki、Kd填进去完事。真做过温控或者电机控制的人都知道公式给的只是初值最终组合必须靠在真实系统响应上磨出来。磨的过程说白了就是一句循环观察系统响应改一个参数观察系统响应再改一个参数。问题在于“观察”和“修改”这两个动作如果都以重新烧录为代价整个循环就慢得离谱。我这里随便算一笔账改一个参数、重新编译、下载固件、等待系统重新上电、温度爬升到稳定、观察超调和稳定时间这一整套下来五六分钟是保守的。PID参数通常要试十几个来回还不算中途改错方向重试的情况。一晚上满打满算也就能磨出两三组参数而且每次都烧录整片固件Flash擦写寿命也在被白白消耗。更隐蔽的成本是心态。反复烧录试错的时候人很容易越调越急躁参数越改越激进最后根本记不清当前板上跑的到底是不是上一版更谈不上复盘。在线调参至少让我能每次只在面板上动一个数眼睛看着趋势反应整个过程是连续、可复现的。这点对收敛参数帮助极大。所以我的结论是整定开始之前人机界面不是锦上添花而是整定工作的前置条件。它要把“观察”和“修改”这两个动作都压缩到秒级让整定变成一个顺畅的交互过程。1.2 一个能显示、能修改、能保存的界面到底替你省了什么设计之前我给自己列了一份“缺了它就不能开始整定”的功能清单实时显示当前测量值、目标值、输出占空比在线修改Kp、Ki、Kd三个参数参数断电之后不丢能一键恢复默认参数最好还能用一块迷你趋势框看响应走向。把这五条翻译成工程语言其实就是三条链路显示链路、输入链路、存储链路。显示链路解决“数据怎么上屏”输入链路解决“旋转编码器和按键怎么转化成菜单操作”存储链路解决“参数从RAM落到Flash、上电再读回来”。三条链路做完后面的整定才谈得上有交互基础。这里要特别强调人机界面在这里的角色不是“产品包装”而是“调试工具”。它能替你省下的不是屏幕钱而是整定周期里最贵的部分——调试者的注意力和时间。我之前看过很多方案一上来就规划触摸屏、彩色LCD、云端监控功能做了一堆唯独没解决“顺手改一个Kp看看响应”这个最原始的需求。方向就偏了。2. 调参界面的硬件选型我的“刚好够用”方案2.1 从操作流程反推功能需求选型之前我先想清楚了整定过程中的典型操作路径开机进主界面看实时数据和当前参数旋转编码器高亮选中想改的参数项按下进入编辑模式旋转加值或减值按确认参数生效观察曲线若干秒继续改下一个参数觉得这组参数值得保留再手动保存到Flash。这个流程对应到UI上非常简单一个主画面显示实时数据和三五行参数菜单加一条迷你趋势框就够。整定场景不需要华丽的二级页面也不需要复杂的图形控件。核心原则是“一次只改一个参数改完能立刻看到响应”。基于这个原则一目了然能选的屏幕和输入方式其实范围很小。2.2 为什么是OLED加编码器而不是串口屏和矩阵按键当时我认真对比过三种组合0.96寸OLED加EC11编码器、3.5寸串口屏、矩阵键盘加LCD1602。直接说结论选OLED加编码器。对比项0.96寸OLED EC11编码器3.5寸串口屏矩阵键盘 LCD1602IO占用OLED用I2C两线编码器两相按键共3个GPIO一个串口UART8个IO起步界面复杂度自绘菜单轻量功能强画面漂亮文本显示不直观成本最低高中二次开发量需自写渲染逻辑但代码量小依赖厂商上位机组态软件需自写按键映射整定体验手不离开旋钮连续可调操作有延迟感需熟悉页面按键编号难记现场手忙脚乱串口屏的优点是显示效果好但代价也很明确一块屏抵好几个OLED的价格而且大部分串口屏要靠厂商提供的组态工具来画界面固件里还要额外解析它那一套通信协议等于把一部分维护成本转嫁给了嵌入式代码。矩阵键盘加LCD1602的问题是操作不直观你得记着哪个按键对应哪个菜单层级整定现场一旦分心很容易按错而且1602能显示的内容实在太少。编码器在我看来是“调参界的鼠标”旋转就是翻页和加减按下就是确认和返回直觉且高效。EC11只需要两个GPIO读正交信号中间再加一个按键引脚三个IO就解决全部输入需求。OLED走I2C两根线搞定显示。这套组合下来STM32F103C8T6这颗主控跑起来毫无压力外围成本几乎可以忽略。2.3 轻量自绘还是直接上LittlevGL这一点容易被带偏。网上现在很多教程一上来就让接LVGLLittlevGL字体、控件、主题一套组合拳。但对128x64的单色OLED、主控只有20KB RAM的F103来说跑LVGL属于杀鸡用牛刀内存和裁剪成本都不划算。我选择自绘因为那个菜单结构实在太固定了选中行反白、参数值变化、趋势框滚动。全部渲染逻辑加起来不到300行C代码而且画面完全可控。如果你手里的硬件换成了240x320以上的TFT LCDRAM也超过100KB那接LVGL是完全合理的它能省掉你大量维护自绘控件的时间。关键是先评估屏幕分辨率和RAM再决定要不要引入GUI框架而不是一上来就选重框架。3. 表驱动菜单与状态机让UI代码不像面条3.1 用结构体数组描述整个菜单嵌入式HMI代码写多了最常见的坏味道就是一长串if else判断当前在第几层菜单、当前高亮第几项、旋转编码器该加谁、按一下该进哪一页。越写越长最后自己都不敢改。我用的是表驱动方式把可调参数全部放到一个结构体数组里渲染和输入逻辑只跟这张表打交道。typedef struct { const char *name; // 菜单项名称 float *value_ptr; // 指向实际参数变量 float min_val; // 参数下限 float max_val; // 参数上限 float step; // 常规步进 float fast_step; // 快速步进 const char *unit; // 单位 void (*on_confirm)(void); // 确认回调 } menu_item_t; menu_item_t g_menu[] { { Kp, g_pid_param.kp, 0.0f, 9999.0f, 1.0f, 10.0f, , pid_param_changed }, { Ki, g_pid_param.ki, 0.0f, 9999.0f, 0.1f, 1.0f, , pid_param_changed }, { Kd, g_pid_param.kd, 0.0f, 9999.0f, 0.1f, 1.0f, , pid_param_changed }, { Target, g_target_temp, 0.0f, 300.0f, 1.0f, 10.0f, degC, target_changed }, { Save, NULL, 0.0f, 0.0f, 0.0f, 0.0f, , save_params_to_flash }, { Default,NULL, 0.0f, 0.0f, 0.0f, 0.0f, , restore_default_params }, }; #define MENU_ITEM_COUNT (sizeof(g_menu) / sizeof(g_menu[0]))整张菜单表的核心在于每个参数项自带变量指针、上下限、步进和确认回调。界面框架渲染的时候只需要遍历这个数组高亮行由当前索引决定。旋转编码器修改的是value_ptr所指向的变量并且做范围钳制。以后想加一个参数只需要在数组里加一行渲染和按键逻辑一行都不用改。这才是“给固件长出人机界面”的正确姿势界面框架是固定的参数变成了数据。3.2 编码器、按键状态机与两层菜单EC11编码器输出A、B两相信号两相之间有90度相位差。旋转方向不同两相的变换顺序也不同。我是在1ms定时器中断里对A、B两个引脚采样做简单退抖后根据相位组合判断方向。判断逻辑不复杂记录上一拍的AB状态和当前状态组成一个4位数值查一个16项的方向表就能得出正转、反转还是无效跳变。按键单独做一个短按/长按状态机。短按用于“进入编辑”和“确认修改”长按我设计成“退出当前修改并放弃”。菜单维护两层状态主页面状态和编辑状态。主页面下旋转编码器负责移动高亮行编辑状态下旋转编码器负责修改值。代码里就是用一个menu_state变量区分状态进入编辑时把当前值存到临时缓冲区退出编辑时根据结果决定丢弃还是提交。快速调整还有一个小技巧根据两次旋转脉冲的间隔判断旋转速度。间隔短说明用户想快速拉数值就用fast_step大步进间隔长就用step小步进。这个对Kp这种需要跨数量级调整的参数特别有用慢调细、快调粗不需要额外键位。3.3 参数“确认后生效”界面层如何温和地通知执行层参数不是改完立刻生效就最好。比如你正在把Kp从50往30转PID控制还在跑着如果每次旋转都直接写到实时运行的结构体里输出占空比可能跟着猛跳。我采用的是“编辑缓冲加确认提交”模式界面上改的是菜单项指向的临时值按下确认才把临时值写入真正的g_pid_param结构体同时置一个param_updated标志。控制任务每个周期检查这个标志只在标志置位时刷新PID参数。实际实现里可以直接memcpy整个结构体保证参数更新对控制循环而言是“原子操作”不会有半新半旧的状态。这个确认机制看着多了一步实际上避免了大量现场误触导致的执行层抖动我一直推荐保留。4. 从面板操作到Flash存储整定参数的完整链路4.1 128x64屏幕上的分区布局与局部刷新0.96寸OLED分辨率128x64要同时放下实时数据、参数菜单和趋势框分区得精打细算。我当时是这么分的顶部两行测量温度用大字号显示第二行放目标温度和输出占空比小字号中间三行参数菜单当前选中的行反白显示底部约20像素高迷你趋势框每隔200ms采一个点画在最右列整幅画面向左平移一列。趋势框不用追求坐标精确能看到走向就够。真正需要注意的是刷新策略。OLED全屏刷新在72MHz主频下走I2C一帧大约要30到40毫秒如果主循环频繁全刷屏幕闪烁且占用总线时间。我改成局部刷新测量值每200ms更新一次菜单行只在索引变化时重绘趋势框只平移一次再补一个新点。这样I2C的占用大幅下降也为主循环留出了更多余量。4.2 内部Flash的参数读改写参数丢了等于没调。我用的是STM32F103内部Flash的最后一页存参数F103C8T6整个Flash是64KB最后一页从0x0800FC00开始大小1KB。存一个几十字节的参数结构体绰绰有余。写入流程是解锁Flash擦除整页按半字编程写入上锁。typedef struct { uint32_t magic; // 魔数用于识别有效数据 uint16_t version; // 参数版本号 uint16_t crc; // 简单校验 float kp; float ki; float kd; float target; } param_store_t; #define PARAM_MAGIC 0xA5A5A5A5 #define PARAM_FLASH_ADDR 0x0800FC00 void save_params_to_flash(void) { param_store_t data; data.magic PARAM_MAGIC; data.version PARAM_VERSION; data.kp g_pid_param.kp; data.ki g_pid_param.ki; data.kd g_pid_param.kd; data.target g_target_temp; data.crc calc_crc16((uint8_t *)data, sizeof(data) - 2); FLASH_Unlock(); FLASH_ErasePage(PARAM_FLASH_ADDR); uint32_t *p (uint32_t *)data; for (int i 0; i sizeof(data) / 4; i) { FLASH_ProgramWord(PARAM_FLASH_ADDR i * 4, p[i]); } FLASH_Lock(); }上电加载的流程反过来读取首地址数据先检查魔数再算CRC校验通过才装载到g_pid_param任何一步失败都回退到代码常量区的默认参数。这里的默认参数不是随便给一组而是我之前用齐格勒-尼科尔斯整定法粗算过的至少保证系统能稳定运行让用户从默认值开始微调而不是从零摸索。4.3 一次完整的调参操作路径实际整定时我的操作路径是这样的上电进主界面看到温度和参数列表。旋转编码器把高亮移到Kp这一行按一下进入编辑Kp的值开始闪烁。旋转调值慢旋步进1快旋步进10调到感觉差不多的值再按一下确认退出编辑。此时Kp在RAM里已经生效但还没落Flash。接下来观察趋势框里的温度走向决定继续改还是保留。只有当光标移到“Save”这一项并确认参数才真正写入内部Flash。生效和落盘分开是我刻意保留的设计。整定过程中参数改来改去是常态如果每次确认都写Flash既拖慢节奏又没有意义。等一组参数确实值得留再手动保存一次就够了。这个交互细节对整定体验影响很大。5. 界面做完之后想提醒你的三件事5.1 渲染刷新必须与控制周期分离这是我最想强调的一点。最容易踩的坑就是把OLED刷新写在主循环里不做任务切分结果屏幕刷新时占用的时间直接影响控制时序。我的做法很明确1ms定时器中断里只做编码器扫描、按键状态机、PID计算和PWM占空比更新主循环只做OLED渲染、Flash操作这类耗时事务。这样不管显示多慢1ms的控制周期始终稳定。实测起来PID运算在72MHz的Cortex-M3上也就几微秒跟I2C刷屏完全不是一个量级但只要不分离I2C等待就可能拖垮实时性。5.2 保存要克制Flash寿命经不起频繁写入STM32F103内部Flash的擦写寿命标称大约一万次。整定阶段一晚上写几十次没问题但一个设备如果长期运行或者固件里加了周期性的自动保存逻辑就需要认真对待了。我的策略是三重保护只有显式选择“Save”才写Flash保存前让菜单项闪烁两次提示确认参数结构体里带版本号固件升级后如果版本对不上或CRC校验失败自动走恢复默认流程。这个机制帮我省掉了后来不少“参数莫名其妙丢失”的排查时间。5.3 给“手滑”留后路恢复默认与参数校验调参调试中非常容易手一抖把参数改成离谱值。比如Kp设到9999输出直接跑到限幅温度猛冲。所以菜单里必须有“Default”项而且恢复默认也要做二级确认防止误触把辛苦调好的数据清掉。校验失败时上电自动恢复默认同样是兜底的关键环节保证任何异常情况下设备都能以一个确定的状态启动而不是带着半损坏的参数跑起来。做完这套界面之后我再也没回到“改代码、烧录、观察”的老路上去。后来在其他项目上不同主控、不同屏幕我复用同一个表驱动菜单框架需要改的只有参数表和像素布局。整定效率也明显提升一套PID参数从原来的一晚上两三个小时压缩到二十分钟以内。我想这才是给固件“长出人机界面”最大的价值它不只是给设备加了一块显示屏而是把调试过程本身变成了一个肉眼可见、随手可控的交互闭环。如果现在有人问我要不要先做界面再做整定我的答案永远是先做哪怕功能简陋也远比盲调来得踏实。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →