ARTICLE DETAIL

资讯详情

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

嵌入式开发中,PID整定前先搭建人机界面的实战方法

嵌入式开发中,PID整定前先搭建人机界面的实战方法 说实话干嵌入式这么多年每次项目到“参数整定”这一步我都有点心态崩。改一个PID的Kp听起来多简单编译、烧录、上电、初始化、复现工况一套下来三五分钟没了。调三个参数半小时过去了整定整定整的是参数费的是工程师的命。后来我学乖了。在真正动手整定之前我强迫自己先做一件事给固件长出人机界面。听起来像绕路但实际上这才是我踩过无数坑之后找到的最短路径。这个系列正好写到第7期今天就把“为什么整定之前先要有界面”这件事掰开揉碎聊清楚顺便把我实践下来的一套低成本、高回报的固件人机界面搭建方案分享出来。这篇文章适合谁正在做电机驱动、电源控制、仪表仪器或者任何需要反复调节参数的固件开发者。尤其是那种“参数调一次就得重新烧录一次”的项目看完这篇你应该能少走一大半弯路。1. 为什么整定之前先要有人机界面1.1 没有界面的整定到底卡在哪我们先复盘一下没有界面的时候大家是怎么做整定的。最常见的做法是“改代码-编译-烧录-观察-再改代码”。我见过很多工程师包括我自己早期都是这么干的。拿电机驱动来说调一个速度环的Kp得先把代码里的常量改了然后编译下载复位等系统跑起来再通过示波器或者串口打印看波形判断超调量、稳定时间。如果波形不理想继续改继续烧。一次两次还好问题是参数整定从来不是一锤子买卖。形式整定、抗扰动整定、不同负载工况下的整定一轮下来要调整的参数数量可能超过两位数。每次整套流程重复一遍操作本身的时间消耗已经很大更要命的是频繁烧录会打断你的思路。调试状态是讲究连续性的你正在观察一个参数变化对系统响应的影响结果为了改下一个参数你得把设备重启一次所有状态清零从头复现工况。除了反复烧录还有两个痛点也很突出。一个是“看不见”。整定过程中最需要的是实时观察变量变化。速度、电流、位置、温度、母线电压这些量在系统运行中的瞬态变化才是判断整定效果的依据。没有界面你只能依赖示波器探头一根一根去点或者靠串口往PC端疯狂打日志。串口打印的问题在于数据是流动的没有上下文很难直观看到几个变量之间的相互关系。另一个是“记不住”。你今天调了一组参数觉得效果不错明天想对比另一组结果断电之后参数没了。代码里的常量要么覆盖要么注释掉换一组。想对比A组和B组的差异只能靠脑子记或者拿个小本本抄。我在早期项目里就干过这种事小本本上写了一堆 Kp0.35、Ki0.02 的记录结果本子找不到全部白干。1.2 “人机界面”不只是屏幕很多人一听“人机界面”第一反应就是“给设备加一块屏”。这个理解不算错但太窄了。人机界面本质上是“人与固件之间用来交换状态的通道”。它的核心价值不是好看也不是炫酷而是让工程师在设备运行的过程中——注意是不中断运行、不重新烧录——能够读写固件内部的变量和状态。基于这个定义界面形态就很多样了。带液晶屏的本地菜单是一种界面串口命令行是一种界面Web配置页面是一种界面蓝牙/WiFi上位机也是一种界面。它们的共同点只有一个把固件内部的参数和状态开放出来让人可以不打扰系统运行的前提下完成观测和修改。所以我们在这里讲的“先给固件长出人机界面”不是说“先搞个炫酷的UI”而是说“先打通一条人跟固件交互的通道”。这条通道打通之后整定才谈得上“边跑边调”。为什么一定要在整定之前做这个事因为整定本质上是一个迭代优化的过程迭代效率决定项目进度。界面的本质是降低迭代成本。一次投入整个调试阶段反反复复受益。而且不光是整定阶段后续的现场维护、故障诊断、产线测试这套界面全部能用上。越早搭越划算。2. 给固件选界面形态三种方案一次说清确定了“要有人机界面”这个方向接下来就是选形态。很多人在这一步就纠结住了。其实不用纠结我个人的经验是按你手头的硬件条件和调试场景来选就行了。2.1 本地屏幕适合现场整定与独立设备如果设备本身在产品形态上就需要显示信息或者你要在没有PC的环境里做现场调试那本地屏幕是最直接的方案。本地屏幕常见的有三种第一种是OLED小屏。0.91寸、1.3寸这种I2C接口驱动非常简单显示个数字和简单菜单足够。适合界面内容比较少、设备结构空间有限的场合。第二种是TFT彩屏。ILI9341、ST7789这类控制器SPI接口或并口分辨率240x320起步能显示曲线、波形、中文菜单信息承载能力强。代价是驱动复杂一些占用MCU资源也多一些。我自己做电机驱动调试用的就是这类屏幕实时画电流波形效果非常直观。第三种是串口屏。这是我最推荐给想快速出活的人。串口屏内部有自己的主控和GUI引擎MCU只需要通过串口发指令就能让它显示按钮、文本框、进度条、曲线。开发工作量比直接驱动TFT小了一个量级。缺点是成本高一点而且部分场景下实时性受串口波特率限制。本地屏幕方案最大的价值在于“现场可调”。你人站在设备旁边按几下按键参数改了观察运行状态变化这比跑回电脑前改代码高效太多了。2.2 串口命令行成本最低几行代码就能跑如果现在设备已经能通过串口输出日志了那恭喜你离界面其实只差一步。串口命令行可能是性价比最高的固件人机界面方案。不需要加任何硬件不需要动结构只要在固件里加一个命令解析器把几个核心参数暴露出来就能实现“边跑边调”。命令解析的原理非常简单。第一通过串口接收字符串按约定格式解析出命令名和参数第二根据命令名查注册表找到对应的处理函数第三执行读写操作。典型交互是这样的get kp # 读取Kp当前值 set kp 0.35 # 把Kp设为0.35 save # 保存参数到Flash reset # 恢复默认参数它是纯文本交互人类可读也方便PC端写脚本批量执行。如果你的PC端有串口工具支持脚本发送甚至可以写一个自动扫参的脚本连续修改Kp值并采集响应数据。串口命令行适合什么场景MCU资源特别紧张、没有屏幕和网络硬件、产品还在功能调试早期。另外一个很适合的场景是产测产线工人不需要理解界面页面层级给一张命令清单照着敲就行。2.3 Web或蓝牙上位机适合远程和批量产测当设备已经带了ESP8266、ESP32、蓝牙模块这类无线通信能力时可以考虑Web页面或者手机App作为界面。Web方案的思路是MCU内部跑一个极简的HTTP服务器提供GET/POST接口。工程师在笔记本电脑的浏览器里输入设备IP打开页面修改表单参数提交后设备自动更新并返回当前状态。这种交互方式的体验已经非常接近“产品级”了。蓝牙方案类似用手机App连接设备App端做UI通过BLE的Characteristic来读写参数。这类方案的优点很明显第一不依赖物理连接适合设备已经安装到机柜里、不方便插线调试的场景第二界面开发不消耗MCU资源复杂UI全在上位机端第三配合网络可以做到多设备批量管理产测效率很高。缺点也现实协议栈复杂度上来了系统集成工作量不小。如果设备本身没有无线模块硬件成本也会增加。2.4 怎么选一张综合对比表说了这么多直接给大家一张我自己做选型时用的对比表界面形态最小硬件要求开发周期交互实时性适用阶段我的建议本地液晶屏MCU屏幕按键视屏幕类型而定OLED最快TFT最慢高现场整定、独立设备产品定型后有显示需求时选它串口命令行仅需MCU串口1~2天就能跑通高功能调试、早期整定最优先做性价比之王Web页面MCUWiFi/以太网3~5天中远程调试、批量产测设备联网时强烈推荐蓝牙AppMCUBLE模块4~7天中移动场景、现场运维看产品需求非首选如果你的设备什么都还没有我的建议很明确先从串口命令行起步它只需要一个串口。等界面需求明确了再决定是加屏幕还是加无线。这样你在“先有界面”这件事上的ROI最高。3. 为整定设计界面菜单、监控与参数管理选定了形态下一步就是设计界面内容。这里有一个很重要的原则整定用的界面不是给人用的产品界面优先级完全不一样。3.1 顶层菜单结构三个页面打天下我做整定界面的顶层菜单通常只保留三个入口再多都嫌烦。第一状态监控。整定过程中需要观察的变量比如电流、电压、转速、温度、运行状态等全部集中在这里。做整定的人视线90%的时间都停留在这个页面。第二参数调节。要修改的目标参数按功能分组。比如P组放比例相关的I组放积分相关的D组放微分相关的。每一组内部按参数名排列。第三系统信息。固件版本、编译时间、运行时长、错误标志位。这个页面不是每次都用但出问题时它就是救命的。还有一些辅助功能比如“保存参数”“恢复默认参数”不要单独做成顶层菜单应该作为参数调节页面的操作项按确认键之后弹出确认对话框。这样菜单层级少操作路径短。为什么这样设计因为整定过程中你的注意力应该集中在“观察响应”和“调整参数”这个循环上。菜单层级越深按键次数越多注意力被打断的概率就越大。界面是服务整定的不是让工程师来学习菜单结构的。3.2 状态监控让参数变化“看得到”状态监控页面做得好不好直接决定整定效率。最基本的能力是“实时数字刷新”。选定几个关键变量以固定的刷新率在屏幕上更新数字。这个实现不难核心是“读变量-格式化-刷新显示”三步。再进一步是“实时趋势曲线”。用点阵屏把变量的采样值画成波形比如让屏幕横轴代表时间纵轴代表数值每来一个采样点往左推一列就能形成滚动的波形窗口。电机调速时的超调、振荡、稳态误差在曲线上看得清清楚楚。在整定PID时我基本不看数字只看曲线。曲线页面的刷新机制需要单独处理。屏幕局部刷新就好不要每次全屏重绘不然滚动过程中屏幕会闪得厉害也拖慢主循环。如果设备没有屏幕状态监控页面的等价物就是“串口周期性推送数据”。比如固件每一百毫秒往串口发一行 CSV 格式的数据PC端用串口助手或者 Python 脚本接收画出一张实时曲线图。成本几乎为零但效果和屏幕曲线一样。这个小工具链值得常备。3.3 参数编辑步进、范围与长按加速参数编辑界面是整套界面里交互逻辑最重的部分。核心交互是“选中一个参数-修改数值-确认生效”。参数对象需要定义几个属性参数名、数值指针、最小值、最大值、步进值。步进值的选取有讲究。步进太大细微调节做不到步进太小调一个值按几十下按键。我的习惯是把步进设为该参数典型量程的百分之一左右同时提供“长按加速”功能按住按键超过500ms后步进自动放大十倍。这样既能快速扫范围又能精细微调。顺便提一句显示精度的问题。很多控制参数是浮点数比如 Kp0.35Ki0.02。直接在屏幕上打印浮点数需要 printf 家族函数这对部分MCU的闪存和栈资源是不小的开销。我的做法是显示时转成整数的千分值或万分值进行处理比如内部存 float显示时按公式换算成 x.xxx 格式用整数运算生成字符串。这样避免了浮点格式化输出的开销和潜在的性能差异。参数修改后的生效时机需要认真设计。我的做法是设计成“临时生效”和“保存持久化”两个动作分离。修改完参数先写进RAM控制算法立刻用新值运行工程师可以马上看到效果。但这时候参数还在内存里没有写Flash。确认这一组参数没问题之后再进“保存”操作统一写入。避免了一次一次写Flash导致存储介质过早磨损。3.4 参数存储Flash分区与掉电保护说到参数保存这里面的坑实在多我单独拿出来讲。首先是“不要每个参数一变就写Flash”。Flash的擦写寿命是有限的常见NOR Flash的擦写次数是一万次到十万次。如果你把“每次修改都写”当成默认行为整定一天调几十次参数不到半年Flash就废了。所以前面说的“确认保存”在这里非常重要。其次是“参数区和代码区分开”。固件升级和参数保存不能共用同一个Flash区域。我的参数存储方案是在链接脚本里单独划出一个扇区专门存放参数结构体。每次保存时先擦除该扇区再写入整块参数结构体。同时在这个结构体头部放一个魔数和校验和读取时先校验校验失败就表示参数区损坏或未初始化自动加载代码里烧录的默认参数。更进一步可以做双备份。用两个扇区交替存储参数写的时候先写备用区再更新主区掉电时总有一个分区是可用的。这个方案我是在一个量产产品上验证过的可靠性比单区高很多。对于还没有量产压力、只是开发阶段调试用的固件单区加校验就够了双备份属于锦上添花。4. 实操记录STM32上搭一个调参界面理论说了这么多来点实际的。下面这套东西是我在一款STM32F103主控的设备上跑通的不算复杂但足够作为“整定前先有人机界面”的起步模板。4.1 硬件准备与工程结构硬件清单主控STM32F103C8T6ARM Cortex-M364KB Flash20KB RAM。这套方案跑起来绰绰有余。屏幕1.3寸或者2.4寸TFT屏驱动常见的就是ILI9341或ST7789。SPI接口就行省引脚。输入两个独立按键加一个EC11旋转编码器。编码器用来上下移动菜单和调整数值按键用来确认和返回。没有编码器的话三个按键也能实现同样的操作。存储芯片的Flash里划一个扇区存参数。通信一个UART转USB接PC备用调试和命令行。工程结构建议分四层app/ ├─ ui/ 界面层菜单绘制、按键解析、页面逻辑 ├─ bsp/ 硬件驱动层屏幕驱动、按键驱动、编码器驱动、Flash读写 ├─ control/ 业务逻辑层控制算法、参数计算 └─ main.c 主循环和初始化分层的目的是为了隔离。界面层只负责把人操作翻译成“设置某个参数”的请求控制层只负责干自己的控制算法。两层之间通过参数注册表松耦合实现起来非常舒服。4.2 菜单状态机怎么实现实现菜单最直接的方式是状态机。每个页面定义成一种状态按键事件驱动状态切换。先定义一个菜单项结构体typedef struct menu_item { const char *name; uint8_t page_id; void (*draw)(void); void (*on_key)(uint8_t key); } menu_item_t;顶层菜单用一张数组管理static const menu_item_t menu_root[] { {状态监控, PAGE_MONITOR, draw_monitor, key_handler_monitor}, {参数调节, PAGE_PARAM, draw_param, key_handler_param}, {系统信息, PAGE_INFO, draw_info, key_handler_info}, };主循环里的按键处理就一行menu_root[current_index].on_key(key);每个页面的按键回调函数内部再做分支。比如“参数调节”页面按下“确认”进入参数列表按下“返回”回到顶层菜单。页面内再维护一个当前选中的参数索引。这个结构的好处是菜单的扩展完全靠加表项所有页面逻辑封装在自己的回调里互相不干扰。实测维护起来非常轻松。4.3 让参数项可配置把参数表做成注册表这一点是我觉得最值得分享的设计。参数调节页面不针对具体参数写死逻辑而是做一个“参数注册表”。定义一个参数描述结构体typedef struct param_item { const char *name; void *value_ptr; float min; float max; float step; uint8_t precision; } param_item_t;系统全部的整定参数放在一张全局表里static param_item_t g_param_table[] { {Kp, pid.kp, 0.0f, 100.0f, 0.01f, 3}, {Ki, pid.ki, 0.0f, 100.0f, 0.01f, 3}, {Kd, pid.kd, 0.0f, 100.0f, 0.01f, 3}, {目标转速, target_speed, -3000.0f, 3000.0f, 10.0f, 1}, };“参数调节”页面根本不需要关心它操作的具体是什么参数只需要遍历这张表画参数名、画当前值、响应按键修改指向地址的数值。新增一个可调参数只需在表里加一行。控制层和界面层的解耦非常彻底。控制算法运行时直接读pid.kp、pid.ki这些变量界面层修改的也正是这些变量的内存地址。双方都不需要知道对方的存在。这就是“注册表模式”的妙处。调整逻辑的伪代码大致是void param_on_encoder_step(signed char step) { param_item_t *item g_param_table[g_cur_param_idx]; float *val (float *)item-value_ptr; float delta step * item-step; *val delta; if (*val item-max) *val item-max; if (*val item-min) *val item-min; }很简单但足够好用。如果参数类型是整数可以把value_ptr换成int*处理逻辑稍微改一下就行。4.4 和整定流程打通最后一步是把这套界面和实际整定流程串起来。我的做法是这样的在设备通电进入主循环后界面层初始化完成进入“状态监控”页面。控制算法独立运行实时把关键运行数据填充到全局结构体里界面层从这些结构体读数据做显示。整定流程大概是这样的先在“状态监控”页观察系统当前稳态记下转速误差、电流纹波等基线数据。转到“参数调节”页选中 Kp按编码器调整数值。调整完立即返回“状态监控”页观察速度阶跃响应的变化。超调大就减小 Kp响应太慢就增大 Kp。整个过程不需要断电不需要重新编译烧录。当一组参数手感满意后在“参数调节”页按一次“保存”键写入Flash。换一组工况重复上述过程。实测下来原来改一次参数需要三五分钟的流程压缩到十几秒。而且由于不需要频繁重启设备整定过程的可重复性也大大提高。系统状态不会因为重启被重置干扰因素少了参数对比更有说服力。5. 界面做完之后踩过的坑与排查技巧界面不是搭完就万事大吉。运行过程中出了不少幺蛾子我把典型的几个问题整理一下大家遇到了可以直接对着查。5.1 屏幕刷新把自己刷死最大的坑就是屏幕刷新把主循环拖死了。TFT屏如果做全屏刷新一帧数据量很大SPI频率不高的情况下一帧刷下来可能几十毫秒到上百毫秒。如果主循环里每次都全屏刷新控制算法的执行周期会被严重拉长整定出来的参数参考价值就没了。我的解决方案是“分时刷新 脏矩形标记”。屏幕只在内容变化时局部刷新比如数字变化只刷新数字区域菜单移动只刷新标题行。曲线绘制走DMA数据填充和传输异步进行避免阻塞CPU。如果屏幕本身不支持局部刷新串口屏方案一般都有区域刷新指令效果类似。还有一个比较土的招就是降低刷新率。状态监控页1秒钟刷新10次已经足够看趋势不需要30帧以上的刷新率。整定又不是看电影不需要高帧率。5.2 Flash写入死在“改一次写一次”前面说过的“临时生效”和“保存持久化”分离绝不是我拍脑袋想出来的。真在项目里踩过坑。早期版本我图方便参数一变就往Flash写。结果整定到第三天开始出现诡异问题参数设置成某个值读回来却是另一个值。排查半天Flash擦写寿命到极限了。那块Flash的标称擦写次数十万次理论上够用但整定阶段一天改几百次加上我保存的是整块参数结构体每次保存都要擦写一次算下来一年不到的擦写频率就能磨掉一个扇区。从那以后“确认保存”成了我的设计铁律。另外写Flash前务必关中断防止写入过程中被中断打断导致半写状态。写完再恢复中断。这个细节虽然老生常谈但我见过好几个人因为没做而吃大亏。5.3 编码器和按键的抖动问题旋转编码器好用是好用但抖动处理不好会让人疯掉。你明明只转了半格界面跳了两三个参数你按一下确认直接跳过确认对话框执行了操作。我的经验是两层处理。按键用简单的软件消抖检测到电平变化后延时10ms再确认。编码器用状态机处理正反转不使用单纯的电平变化。编码器状态机的基本原理是根据A相和B相的电平组合判断旋转方向。static uint8_t enc_state 0; signed char encoder_step(uint8_t a, uint8_t b) { uint8_t new_state (a 1) | b; static const int8_t table[4][4] { // 根据当前状态和下一状态查表判断方向 }; int8_t step table[enc_state][new_state]; enc_state new_state; return step; }查表法的好处是能够滤掉抖动产生的非法状态跳变输出的step要么是 1、-1要么是0不会出现连续跳变。实际用下来手感和稳定性都很好。5.4 中文显示与字库乱码TFT屏显示中文最大的坑是编码和字库。MCU的编译工具链默认可能用UTF-8而很多中文字库芯片和字模工具用的是GBK。同一个字符串两套编码下文件字节流不同显示就成了乱码。我的解决办法是项目统一使用UTF-8编码显示到屏幕前把中文字符串先转成字库对应的索引。如果不想折腾编码转换还有更省心的方案界面英文为主。整定参数名称无非是 Kp、Ki、Kd、Target、Speed、Current英文缩写完全够用还能让固件和产线文档保持一致。我的多个量产项目界面都是英文为主实用优先没有人在意菜单是英文还是中文。5.5 参数精度和浮点显示STC和STM32这种不带FPU的M3系列浮点运算本来就靠软件模拟慢是慢点但显示数据还好。真正烦人的是浮点数的格式化输出。printf(%.3f, value)看着简单但printf的实现体积很大而且如果微小系统栈不够可能直接卡死。我的做法是把浮点数转定点后做整数格式化提前处理int32_t fixed (int32_t)(value * 1000); snprintf(buf, sizeof(buf), %d.%03d, (int)(fixed/1000), (int)(fixed%1000));这样既节省了Flash空间显示格式也能精确控制在“参数调节”页面和高频实时的“状态监控”页面都很稳。实测在STM32F103和Cortex-M0平台上这个方案比直接上 printf 稳定性高了一大截。最后再说一点个人体会做了这么多项目踩过这么多坑我越来越觉得“整定之前先给固件长出人机界面”不是矫情更不是过度设计。它的本质是把“观察-调整-验证”的闭环做得足够短。人机界面不一定要多豪华但一定要让“改参数”这个动作从“重新编译的系统级操作”降级为“随手按几个键的日常操作”。当这个降级完成之后整定这件事才是真正的整定而不是一次次痛苦的循环。如果你现在手头正好有个项目卡在反复烧录调试的泥潭里不妨今天就停下来花一两天搭一个最简陋、最朴素的界面通道——串口命令行都可以。相信我等你在系统运行的状态下用一行set kp 0.35让设备重新驯服的时候你会回来感谢这一刻的自己。
返回列表