ARTICLE DETAIL

资讯详情

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

基于Qt/C++的PID实时调参可视化控件设计与实现

基于Qt/C++的PID实时调参可视化控件设计与实现 1. 从“调参只能靠猜”到“参数曲线同屏可见”去年年中我接了一个电机闭环控制的项目用的是一块ARM Cortex-M7内核的板子电机带增量式编码器要求转速稳态误差在正负5转以内。板子跑起来不难真正让我头疼的是调PID参数。每次改一组Kp、Ki、Kd都要重新编译、烧录、看串口打印的波形再打开MATLAB或者Excel画图效率低到让人怀疑人生。那阵子天天跟串口终端打交道屏幕上全是密密麻麻的数字。转速目标值是1200转实际值从900冲到1400再慢慢收敛光看数字完全没法直观判断超调量、调节时间、稳态误差这些指标。我当时的想法特别朴素要是能把参数曲线实时画出来再做个旋钮边调边看效果该多爽。于是就有了这个项目——用Qt/C手写一个PID实时调参可视化控件。不依赖Qwt、QCustomPlot这类第三方绘图库波形区域、旋钮控件、参数面板全部自己绘制PID运算也直接在控件内部实现支持串口和共享内存两种方式对接真实设备数据离线时还能用内置的仿真模型跑闭环。从动手写到基本能用前后花了大概两个周末。代码量不大核心控件类不到1000行但踩的坑不少。这篇文章把我整个设计思路、实现细节、调参经验和踩坑过程完整记录下来想自己写调参工具的朋友可以直接参考。2. PID闭环控制的关键概念与离散化实现2.1 增量式PID比位置式更合适在动手画界面之前先得把PID运算这件事想清楚。PID控制器的连续形式是u(t) Kp * e(t) Ki * ∫e(t)dt Kd * de(t)/dt这个公式是教科书上的标准形式但放在MCU或者上位机里必须离散化。离散化有两种常见做法位置式和增量式。位置式PID直接计算执行机构的绝对位置输出u(k)和过去所有误差的累积有关一旦积分项出问题输出可能跳变很大。增量式PID计算的是输出的增量Δu(k)只跟最近三次误差有关Δu(k) Kp * [e(k) - e(k-1)] Ki * e(k) Kd * [e(k) - 2e(k-1) e(k-2)]实际输出u(k) u(k-1) Δu(k)。这种方式的好处是执行器只需要叠加增量误动作影响小而且不需要累加所有历史误差积分饱和问题好处理。我在控件里做的是通用PID计算模块位置式和增量式都实现了通过一个枚举类型切换。跟设备通信时按下发的是增量式的结果因为我们的电机驱动板接收的就是PWM增量。2.2 离散化公式里的时间基准这里有个特别容易忽视的细节Kd项和Ki项跟控制周期Ts是耦合的。同一个Kd值Ts如果是1ms和10ms微分作用完全不同。我最初犯过这个错误直接在代码里写死Kd0.1结果在仿真模型上表现很好接到实际设备上剧烈振荡检查了半天才发现是采样周期不一致导致的。正确做法是把Ts显式放进离散化公式位置式比例项Kp * e(k)积分项Ki * Ts * Σe(i)微分项Kd / Ts * [e(k) - e(k-1)]增量式Δu(k) Kp * [e(k) - e(k-1)] Ki * Ts * e(k) Kd / Ts * [e(k) - 2e(k-1) e(k-2)]控件里我给PID参数配了一个配套的控制周期设置项默认1ms用户可以改。参数下发时把Ts也一并发给设备端避免两边对不上。这是调参工具最容易被忽略、但实际影响最大的一个设计点。2.3 积分限幅和微分滤波是必须的光有基础公式还不够真实现场里积分饱和和微分噪声能把人折磨疯。我做了一个二阶惯性环节做微分滤波同时给积分项加了限幅。积分限幅的实现很简单integral Ki * Ts * error; if (integral integralLimit) integral integralLimit; if (integral -integralLimit) integral -integralLimit;微分滤波我用的是一阶低通filteredDerivative alpha * derivative (1 - alpha) * previousFilteredDerivative;alpha一般取0.1到0.3之间太小反应慢太大滤不掉噪声。实际调试中发现加了滤波之后Kd能比原来调大50%而不振荡效果非常明显。3. 控件架构设计先搭骨架再画皮肤3.1 用QWidget还是QQuickPaintedItem第一步要确定控件跑在什么载体上。我做了两个版本一个基于QWidget一个基于QQuickPaintedItem后者是为了以后集成到QML界面里预留的。核心绘图代码两头共用上层只是一个适配壳。Qt的QWidget在Windows和Linux桌面环境下表现稳定开发调试效率高新手也好上手所以我主推QWidget版本。控件的继承体系是这样class PidTuningWidget : public QWidget { Q_OBJECT public: explicit PidTuningWidget(QWidget *parent nullptr); ~PidTuningWidget(); // 数据源配置 void setDataSource(DataSourceType type, const QVariantMap config); void setControlPeriod(double periodMs); // PID参数设置与获取 void setPidParams(double kp, double ki, double kd); PidParams pidParams() const; // 运行控制 void start(); void stop(); void reset(); // 目标值设置 void setTargetValue(double target); signals: void pidParamsChanged(double kp, double ki, double kd); void targetValueChanged(double target); void actualValueUpdated(double actual); void dataSourceError(const QString message); protected: void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; void keyPressEvent(QKeyEvent *event) override; private: PidTuningWidgetPrivate *d; };接口设计上数据源是独立于控件之外的抽象层。控件本身只负责三件事接收目标值、接收实际值、显示PID参数调节结果。至于数据从哪来——串口、共享内存、还是内置仿真——都由外部设置。3.2 代码模块划分整个项目我分成了这么几块模块文件职责主控件PidTuningWidget.cpp/h绘制、事件、参数管理PID计算PidController.cpp/h位置式/增量式PID运算数据源抽象DataSource.h统一数据接口定义串口数据源SerialDataSource.cpp/h串口读写与协议解析共享内存数据源SharedMemoryDataSource.cpp/h跨进程数据交换仿真数据源SimulatedPlant.cpp/h内置被控对象模型应用窗口MainWindow.cpp/h布局、菜单、状态栏PidTuningWidget内部用了Qt的d-pointer模式把私有成员都放到一个Private类里这样头文件干净二进制兼容性也好。虽然是单人项目但这个习惯我一直保留着后面如果要把控件做成商业库或者给别人用这个设计能省不少事。3.3 绘制渲染用QPainter还是OpenGL绘图方案我评估过两条路QPainter软件渲染和QOpenGLWidget硬件加速。QPainter画线条、文字、填充扇区非常方便性能在普通PC上画几千个点完全够用Windows下用自带的Raster引擎Linux下走X11或Wayland都能稳定跑到60帧。OpenGL性能上限高很多但写起来复杂文字渲染、DPI适配、跨平台兼容全是坑尤其在高DPI屏幕上OpenGL的文本渲染要自己做缩放处理很麻烦。结论先用QPainter把功能跑通如果后面要画上万点的密集波形或者做3D效果再迁移OpenGL。实际测试下来QPainter在1080p窗口下画5000个点的波形图单帧耗时大概4到6毫秒完全够用。这里有一个我先说为敬的教训不要一上来就追求高大上的渲染方案先把数据链路跑通、把交互逻辑调对效果比用什么渲染API重要得多。4. 每帧画面上有什么波形区、仪表区、状态栏4.1 界面布局与坐标换算控件整体分三个区域。左侧是主波形区占宽度的70%垂直方向占满右侧上半部分是PID参数旋钮区右侧下半部分是目标值、实际值、偏差的显示区和启停按钮。波形区画两条曲线目标值曲线绿色和实际值曲线黄色背景是深色网格这是我做示波器类界面一贯的配色习惯绿黄在深灰背景上的对比度高长时间盯着看不累。绘图第一步是坐标换算。波形区内部有一个数据矩形区域叫plotRect控件本身的尺寸变化时plotRect跟随变化但排除掉坐标轴标签和留白的尺寸。横向坐标是时间单位是秒纵向坐标是数值范围可以自动跟随数据变化也可以手动锁定。换算逻辑很简单int x plotRect.left() (time - timeStart) / timeSpan * plotRect.width(); int y plotRect.bottom() - (value - valueMin) / (valueMax - valueMin) * plotRect.height();这里的timeStart和timeSpan控制时间窗比如显示最近10秒的数据。valueMin和valueMax控制纵轴范围。自动模式下取当前缓存里所有数据的最大值和最小值再上下各留10%的余量防止曲线顶到边界。4.2 波形区数据缓存与动态缩放波形数据不是来一点画一点那样屏幕刷新时没法做平移缩放。我用了一个环形缓冲区固定存最近60000个点10秒1kHz采样的话就是10万点我用的是10秒100Hz采样60000点足够。绘图时不是每帧都重新扫描所有数据点而是基于Qt的QPixmap做两级缓存。数据区维护一个QImage新的数据点到达时增量绘制只有缩放、平移、清屏时才整体重绘。这个优化很关键否则每帧都画几万个点CPU占用率直接拉满。动态缩放我用鼠标滚轮实现。滚轮向上放大向下缩小缩放中心是鼠标当前所在的时间点。逻辑不复杂void PidTuningWidget::wheelEvent(QWheelEvent *event) { double factor event-angleDelta().y() 0 ? 0.8 : 1.25; double mouseTime timeStart (event-position().x() - plotRect.left()) / plotRect.width() * timeSpan; timeSpan * factor; if (timeSpan 0.5) timeSpan 0.5; if (timeSpan 120) timeSpan 120; timeStart mouseTime - (mouseTime - timeStart) * factor; update(); }这里的0.8和1.25是互倒数保证放大缩小对称体验比较跟手。左右平移用鼠标拖拽实现鼠标按下时记录基准点和当时的timeStart移动时计算偏移量换算成时间偏移。4.3 仪表区显示哪些数据右侧上部我用四个自绘旋钮控件分别控制Kp、Ki、Kd和Target目标值。旋钮下面是三行文本显示目标值、当前实际值、偏差百分比。偏差百分比我按(实际值 - 目标值) / 目标值 * 100%计算如果目标值为0就显示横杠。这个指标比绝对值直观因为不同设备的量纲不一样百分比能通用比较。仪表区下面是一个启停按钮和一个重置按钮。启停按钮控制数据源是否向PID模块输送数据重置按钮清空波形缓存并把积分项清零。这个reset功能在调参过程中非常常用每次修改参数后建议先重置再跑否则旧的积分项残留会干扰判断。4.4 自绘网格与抗锯齿的平衡绘制波形区网格时如果每条线都开抗锯齿性能会下降。我的做法是主网格线带刻度标签用抗锯齿绘制次网格线密集的辅助线关闭抗锯齿。人眼对密集细线的锯齿不敏感但线条一多、抗锯齿一开CPU消耗翻倍肉眼却几乎看不出差异。这个取舍实测下来效果很好。在4K分辨率下主网格线8条、次网格线40条关掉次网格的抗锯齿后整个控件的绘制时间从8毫秒降到了3毫秒。5. 旋钮、键盘与参数限幅调参交互设计5.1 从零绘制一个旋钮控件虽然Qt自带QDial但它的样式灵活性有限我想做一个带角度指示和数值刻度的旋钮干脆自己画。旋钮控件的绘制核心是三部分底部圆盘、指示线、中心圆点。圆盘是一个渐变色圆环指示线从圆心延伸到圆环边缘数值显示在圆盘中心下方。旋钮的输入方式鼠标在旋钮上垂直拖动向上增大、向下减小鼠标滚轮每格调整一个步长双击旋钮弹出输入框直接输入数值。鼠标调节的关键是把鼠标位移映射到数值变化。我的实现是记录鼠标按下时的Y坐标和当时的参数值拖动时用位移差乘以灵敏度系数void KnobWidget::mouseMoveEvent(QMouseEvent *event) { if (!dragging) return; double deltaY pressPos.y() - event-position().y(); double newValue pressValue deltaY * sensitivity; setValue(clamp(newValue, minimum, maximum)); update(); }这里有个体验细节灵敏度系数不是固定值而是根据参数的取值范围动态计算。Kp的取值范围如果是0到10灵敏度设为0.05即每像素0.05Target取值范围如果是0到2000灵敏度设为5。这样不管哪个参数拖满整个控件高度都能覆盖全部取值范围操作手感一致。5.2 旋钮参数范围与默认值调参控件的参数范围不能乱设得贴合实际设备。我做过这么多闭环项目Kp、Ki、Kd的大致范围可以根据被控对象特性分几类被控对象Kp范围Ki范围Kd范围直流电机转速0.1 ~ 100.01 ~ 10 ~ 0.5温控系统10 ~ 5000.1 ~ 100 ~ 50四轴飞行器姿态1 ~ 500.1 ~ 50.1 ~ 2舵机角度0.5 ~ 200 ~ 20 ~ 0.1控件里这几个值默认按电机转速场景设置用户可以手工修改。取值范围不设死只做软限幅超范围时颜色变红提示但允许强制设置。5.3 属性绑定参数变化信号与设备下发联动旋钮数值变化后要触发两件事立即更新PID计算模块的参数以及把参数打包下发到设备端。这里的下发策略有个讲究如果每次旋钮动一下都发一帧数据串口压力大不说设备端的指令队列也容易堆积。我做的策略是拖动过程中不实时下发等鼠标释放时才发送一次最终参数滚轮和双击输入则立即发送。用Qt的信号槽实现connect(knobKp, KnobWidget::valueChanged, this, [this](double value) { pidController-setKp(value); paramsDirty true; }); connect(knobKp, KnobWidget::editingFinished, this, [this]() { sendPidParamsToDevice(); });这个设计避免了下发风暴实测效果很好。拖动时本地计算实时更新波形立即响应松手后参数同步到设备端体验流畅且不会把设备端串口缓冲区打爆。5.4 键盘快捷键与焦点处理用鼠标拖动旋钮虽然方便但精细调节时不如键盘精准。我给旋钮加了键盘支持选中某个旋钮后点击选中按上下方向键调整一个步长按PageUp/PageDown调整十个步长按Ctrl上下方向键调整0.1个步长。实现方式是重写keyPressEvent按键事件先给当前焦点控件处理处理不了再冒泡到主控件。这里有个Qt的细节QWidget默认不接收键盘事件必须调用setFocusPolicy(Qt::StrongFocus)否则按方向键没反应。为了让用户知道当前焦点在哪个旋钮上选中状态我用一个橙色光晕绘制在旋钮外圈和未选中的灰色边框区分明显实测用起来非常顺手。6. 实时数据怎么进控件串口、共享内存与文件回放6.1 串口数据源设计与协议串口数据源是调参工具最常见的接入方式。硬件端发送的数据帧格式我设计了简单的文本协议方便调试$DATA,目标值,实际值,时间戳\r\n例如$DATA,1200,1187,123456\r\n之所以用文本协议而不是二进制是因为调试阶段用串口助手就能直接看数据内容排查问题方便很多。等整个系统稳定了需要提高吞吐量时再考虑切到二进制协议。Qt串口通信用QSerialPort类。关键配置是波特率115200、8数据位、1停止位、无校验。读取方式用readyRead信号驱动每次读取缓冲区里的所有数据按行分割解析以$DATA开头的行。有个血泪教训串口数据是流式的数据帧可能被截断或粘包。必须用累积缓冲区加换行符分割的方式解析不能指望每次readyRead恰好收到一帧完整数据。我的解析器是这样void SerialDataSource::onReadyRead() { buffer.append(serial-readAll()); int newlineIndex; while ((newlineIndex buffer.indexOf(\n)) ! -1) { QString line buffer.left(newlineIndex).trimmed(); buffer.remove(0, newlineIndex 1); parseLine(line); } }这里还有个坑QSerialPort在数据持续高速到达时如果主线程处理不过来缓冲区会被填满导致数据丢失。我的解决方法是在读取端用一个QByteArray环形缓冲解析放在QtConcurrent::run的线程池里做解析完成后再通过信号回到主线程更新UI。实测115200波特率下完全不会丢数据。6.2 共享内存数据源共享内存是我给场景准备的。有时候设备端和上位机跑在同一台工控机上设备软件是C写的调参工具也是本地的走串口还得虚拟串口麻烦。Qt的QSharedMemory类可以直接把数据写到共享内存段里控件这边定时读取。共享内存里的数据布局我定义成一个结构体#pragma pack(push, 1) struct SharedData { quint32 magic; // 魔数0x50494431 quint64 timestamp; // 时间戳 float target; // 目标值 float actual; // 实际值 float kp; // 当前下发到设备的Kp float ki; float kd; }; #pragma pack(pop)写入端往共享内存塞这个结构体读取端用QSharedMemory::lock()、memcpy、unlock()的方式读取。注意QSharedMemory的lock是进程间互斥量操作要快不能在锁内做耗时操作。用共享内存有个好处是零拷贝、低延迟但调试起来比串口麻烦因为看不到原始数据内容。我通常先用串口把功能调通再切共享内存。6.3 文件回放调试神器这个功能是我被逼出来的。有几次在设备旁边调参参数还没调好就得撤了回到工位上想继续分析数据发现现场数据没保存。于是我给控件加了个离线回放功能。串口数据源在接收数据的同时把所有原始帧写入一个文本文件。需要回放时选择文件、设定回放速度控件就按原始时间戳重新模拟数据到达。回放的文件格式和串口协议一致每一行是$DATA,目标值,实际值,时间戳这样文件可以直接用文本编辑器查看也能用脚本做后处理。回放速度可调1倍速、2倍速、4倍速。这个功能看似简单实际非常实用。现在我做现场调试时数据源一直是打开的每次调参过程都有完整记录回去可以复盘哪个参数对应什么波形效率提升非常明显。7. 100ms还是16ms渲染性能优化经验7.1 两个拖垮性能的坑第一个坑是文字渲染。最开始我每帧都用QPainter直接绘制所有刻度标签和数值画得多了发现CPU占用率到40%。这是因为Qt的文字绘制要经过FreeType的字体解析和形状计算非常耗时。优化方案把所有静态文本渲染到一个QPixmap上比如纵轴刻度标签、横轴时间标签只有当缩放比例变化时才重新渲染每帧绘制时直接drawPixmap拷贝几乎不消耗CPU。第二个坑是update()的调用频率。一开始图省事用QTimer每隔10毫秒强制刷新一次结果波形区一直在重绘CPU占用率居高不下。实际上波形更新时间取决于数据源的采样率如果数据是100Hz采样50毫秒刷一次就够了设置为40毫秒的刷新定时器既能保证波形更新不卡顿CPU占用率又低很多。如果数据源采样率更低比如10Hz刷新频率可以降到100毫秒完全不影响视觉体验。7.2 性能实测数据场景绘制耗时CPU占用率(四核)备注5000点波形无旋钮3.2ms8%100Hz数据5000点波形4个旋钮3.5ms9%旋钮静态5000点波形旋钮拖动6.1ms15%旋钮重绘10000点波形全区域动9.8ms21%接近极限在普通i5-8400处理器、1080p分辨率下控件的性能表现足够日常使用。最高负载场景是旋钮拖拽配合波形滚动6到10毫秒的帧耗时完全在可接受范围内。7.3 还有余力的加速方案如果数据量再翻倍比如同时显示4条曲线、每帧画5万个点QPainter就开始吃力了。这时候有两个方向一个是把波形绘制切换到QOpenGLWidget用GL_LINE_STRIP绘制性能提升非常明显另一个是数据降采样屏幕上每像素列的多个点只取最大最小值画一条竖线这种画法在金融K线图里很常见视觉效果其实更好——比全点连线清晰还能避免波形重叠。我目前实现的是第二种降采样方案。当数据点数超过plotRect宽度两倍时按像素列分组取最大最小值画竖线。这种方法在数据量达到10万点时依然流畅且不影响观察波形形状。8. 在线参数下发的几种方案对比调参工具不仅要看波形更重要的是把参数下发到设备端。我调研并实现了三种方式各有适用场景。方案一串口直接下发设备端开启串口接收线程解析到$PID,kp,ki,kd指令就更新控制参数。这种方案实时性最好指令延迟在毫秒级适合现场联调。缺点是设备端需要实现指令解析和参数热更新功能对MCU代码有要求。方案二共享内存下发上位机和设备端在同一台机器时把参数写入共享内存结构体设备端在每个控制周期读取。这种方式延迟低但设备端软件需要配合修改。方案三配置文件设备重启调参结果保存到一个INI或JSON配置文件里设备启动时加载。这种方案适合参数已经调好、进入生产阶段的场景不适合现场调参。我在控件里三套都做了界面上可以切换。现场联调用串口同机调参用共享内存调完保存到配置文件方便复现。下发协议设计成循环冗余校验CRC16的二进制帧防止参数传输出错。文本协议虽然调试方便但线上参数传输我建议至少加个校验和否则某个字节突变可能导致设备执行异常实测出现过一次。9. 调参工具的核心价值用实时反馈代替盲调9.1 内置仿真模型没有硬件也能练手控件内置了一个二阶系统仿真模型用来模拟被控对象。模型是典型的电机转速模型G(s) 1 / (0.1s^2 1.2s 1)我在代码里用双线性变换离散化然后用PID闭环控制这个仿真模型。没有硬件时把数据源切换到仿真模型控件自己就能形成一个完整的闭环回路直接在界面上调Kp、Ki、Kd看波形变化。这个内置模型不仅方便演示更重要的是让新手快速理解PID各个参数的作用增大Kp响应变快但超调变大过大会震荡增大Ki能消除稳态误差但过大会引入低频振荡增大Kd抑制超调但对噪声敏感过大会高频抖动9.2 实际调参案例复盘我在电机上实测了一个完整调参过程。电机目标转速1200转空载。初始参数Kp0.5Ki0.05Kd0。实测响应很慢约1.2秒才达到目标值附近稳态误差约30转。原因是比例系数偏小积分作用弱。把Kp逐步提高到2.0后响应时间缩短到0.3秒但出现约8%的超调来回震荡了3次才稳定下来。这时把Kd从0调到0.12超调压到2%以内调节时间缩短到0.15秒左右效果非常好。如果光靠串口打印数字这个过程至少要反复烧写程序十几次每次烧录加复位几十秒很耗时间。用这个控件大概5分钟就完成了这还是因为我前前后后试了很多组参数才摸到最佳值。9.3 一键导出波形成图片调参结果总要留档或者写进调试报告里。控件加了截图功能按CtrlS保存当前整个控件渲染结果到PNG图片图片里包含波形区域的时间段、参数值、目标值和实际值曲线。导出时我额外叠加了当前PID参数的水印文字后期整理报告时能直接看出哪张图对应哪组参数不用再手动记录。这个截图功能看似不起眼实际用起来非常方便调试记录再也不会丢。10. 源码里的几个协调细节10.1 信号槽用AutoConnection还是DirectConnection控件里的信号跨线程传递很频繁。串口数据源跑在子线程解析完数据后通过信号把值传给主线程的控件。这里注意必须用Qt::QueuedConnection把跨线程的信号放到接收线程的事件循环里执行确保UI更新在主线程完成。我实际踩过坑串口数据源里直接调用控件的addDataPoint方法而不是发信号UI更新发生在子线程结果是控件有时能更新有时不能还偶发崩溃。后来全部改成信号槽跨线程传递没有问题了。10.2 定时器精度数据到达频率和刷新频率的匹配控件的刷新定时器频率是40毫秒而数据源采样率可能到100Hz甚至1kHz。每次刷新时从缓存里捞出这段时间内到达的所有数据点一次性绘制而不是到达一个画一个。这样避免UI刷新和数据到达的频率不匹配导致的漏画。缓存的容量设计要考虑最大数据流的持续时间。1kHz采样率下如果缓存10万点只能显示10秒的数据窗。实际场景中电机转速控制的趋势观察需要至少30秒的窗口所以我默认缓存30万点内存占用约2.4MB每点8字节在PC上完全可接受。10.3 参数持久化重启后参数还在PidTuningWidget支持把当前参数组合以JSON格式保存到指定文件也支持从文件恢复。保存的内容包括Kp、Ki、Kd、目标值、时间窗大小、纵轴范围。这样下次打开工具直接恢复到上次的调参状态不用重新设置一遍。持久化我用QSettings实现保存在~/.config/PidTuner/config.ini。为什么不放在工程目录因为发布到别的机器后用户目录更可靠不会因为权限问题写不进去。11. 这个控件的边界条件和后续规划测试过程中我发现两个边界情况需要专门处理。第一个是目标值为0时偏差百分比的分母是0界面显示--而不是除零错误。第二个是数据源断开连接后控件要自动进入暂停状态波形区顶部显示数据源断开的提示防止用户误以为当前波形还在实时刷新。控件后续方向我很清楚一是增加多曲线支持同时对比不同参数组的阶跃响应二是支持导出数据为CSV方便在外部工具里做频谱分析三是触屏版调参界面工业现场设备很多带触摸屏旋钮改成滑杆手势操作会更顺手。这个项目本身其实不算难但把调参这件事从“改代码-烧录-看串口”变成“拖旋钮-看波形-存配置”整个调试效率的提升是质变的。我印象最深的一次现场调试客户给的时间窗口只有半小时我用这个工具配合提前做好的参数粗调值十来分钟就完成了闭环调试剩下的时间全部用来验证最优参数。放在过去光是烧写程序就得花掉一半时间。如果你也要做类似的调参工具我的建议是第一批先把数据链路跑通别急着美化界面。波形能实时显示、参数能实时下发这个闭环通了后面加什么功能都是锦上添花。
返回列表