Matlab多通道波形图上位机:串口通信、帧解析与实时绘图实战
发布时间:2026/9/9 2:07:11 作者:尧图编辑部 阅读量:1,286

简介这套MATLAB串口通信与多通道波形绘制源码适合需要实现硬件数据采集与实时可视化的工程师、科研人员及自动化专业学生。核心文件RS232.m完整演示了串口对象创建、波特率及缓冲区参数配置、数据帧封装与解析以及基于plot函数的多通道曲线绘制流程可直接移植到嵌入式设备监控、实验数据采集等场景。压缩包内仅含1个m文件整体大小约1KB结构精简、逻辑清晰便于快速阅读和二次开发。资源发布至今已有1365人学习下载尤其适合正在搭建串口上位机或希望掌握MATLAB与外部设备通信的初中级用户。通过反复研读代码能够理解串口通信初始化、读取循环、数据校验与波形图输出的关键写法减少自行调试中遇到的连接失败、数据错位等常见问题。1. 别让串口助手耽误调试这套多通道波形工具要解决什么问题1.1 我在什么场景下决定自己写多通道波形上位机前段时间调一套多通道传感器采集系统单片机通过串口通信把四路 ADC 数据发到电脑上一行一行的十六进制数在串口助手窗口里翻滚。我盯着屏幕看了几十秒就放弃了这种原始输出根本看不到信号长什么样哪一路有毛刺、哪一路相位不对完全靠猜。于是索性用 Matlab 写了个多通道波形图上位机把串口通信、帧解析、实时绘图串成一条龙问题一下就清楚了。先说说这类需求最典型的几个场景。第一个是传感器阵列调试三轴加速度计、六轴 IMU、或者温湿度加气压这种混合采集下位机一次性把好几路数据发上来。单独看某一个数值你很难判断它是否正常但如果你同时看到三路曲线姿态变化、冲击、漂移立刻就能从波形上看出来。第二个是运动控制调试调电机位置环或速度环的时候需要把目标值和实际反馈值放在同一张图里对比响应是快了还是慢了、有没有超调曲线比数字直观太多。第三个是多路 ADC 同步采样比如电力监测里的三相电压电流、音频阵列的多个麦克风输入这种场合通道之间的时序一致性很关键波形图能快速暴露某一通道是否延迟或者串扰。我自己触发点就是调试一块四通道采集板用户反馈数据“偶尔跳变”盯着串口助手翻了半天也没定位。后来把波形画出来跳变发生在哪个通道、大概多久一次一眼就看清楚了。1.2 为什么选了 Matlab 而不是串口助手或 Python这个方案选型不是拍脑袋我把常见几条路都对比过方案优点缺点串口助手打开即用零门槛只能看十六进制/ASCII多通道无法对比无法缩放数据一多直接刷屏Python matplotlib免费灵活要搭环境实时绘图的交互和信号处理工具链不如 Matlab 顺手嵌入式显示屏便携适合现场调试期改显示界面代价高屏幕分辨率有限Matlab绘图交互成熟信号处理工具箱直接可用需要授权启动偏慢我选 Matlab 的核心理由很简单调试工作流不是“看一眼数据就结束”而是 采集 → 看波形 → 滤波/FFT/特征提取 → 改算法 → 再采集。Matlab 恰好覆盖全流程采完的数据可以直接接着做频谱分析或滤波验证不用导出到另一个工具。Python 当然也能做但在这个流程里 Matlab 少一道数据交接。另外animatedline这套实时绘图接口做得足够稳不用担心自绘曲线时性能翻车。1.3 方案全貌一条从下位机到屏幕的数据链整套方案的链路是下位机按固定帧格式发送二进制数据——帧头、通道数、多通道数据、校验、帧尾Matlab 端用 SerialPort 对象打开串口循环读取字节字节流先进入一个缓冲区解析函数在缓冲区里找帧头、做校验、拆出各通道数值最后用animatedline把新数据追加到对应通道的曲线上。这篇文章里的协议是我按自己使用习惯设计的最小可用版本你完全可以按自己的数据格式改。但有两个原则建议别省一是用二进制帧而不是纯文本二是必须有同步头和校验。理由下面展开说这两点才是这套工具能稳定工作的根基。2. 先把帧格式定清楚多通道数据在串口线上怎么排2.1 二进制帧格式设计串口本身是流式协议它不知道什么叫“一帧数据”所以应用层必须自己定义边界。我用的是下面这个帧结构字段长度字节说明帧头20xAA 0x55通道数1例如 0x04 表示 4 通道数据区2 × N每通道一个 int16小端字节序校验和1从帧头到数据区累加和的低 8 位帧尾20x0D 0x0A帧头用 0xAA 0x55 两个字节而不是一个字节主要是降低误同步概率。0xAA 二进制是 101010100x55 是 01010101在示波器上看就是标准方波出问题时用逻辑分析仪排查也更方便。为什么数据区用 int16 而不是 ASCII 文本这是很多初学串口最容易走偏的地方。如果下位机用 printf 发一行123,456,789\n一个通道值要占 3 到 5 个字节还要在解析时切割字符串带宽至少浪费一倍负号和小数点还容易错位。用 int16 小端直接发二进制每个通道固定占 2 字节下位机把内存指针强制转成 uint8_t 数组发出去就行Matlab 端用typecast一次还原既省带宽又不容易出错。这里还要强调字节序。我全部约定小端因为绝大多数 MCU 都是小端直接 memcpy 或指针强转就是小端序下位机不需要额外处理。如果哪天下位机换成大端芯片Matlab 端就必须在typecast后做字节交换。协议文档里把字节序写清楚能省掉后面很多沟通成本。2.2 校验字段累加和够用别过度设计串口底层本身没有包级别的检错机制普通 UART 只保证字节层面的起止位如果下位机没开奇偶校验一个字节的误码在应用层是完全无感的。所以应用层必须自己加校验。我选的是最简单的累加和从帧头开始到数据区结束逐字节累加取低 8 位作为校验字段。缺点是检错能力不如 CRC但在串口线不长、现场干扰不严重的场景下它已经能挡住绝大多数偶发错误而且下位机实现只要两三行。如果传输距离长、电磁环境差或者传的是不能出错的电机控制指令那就升级成 CRC8 甚至 CRC16代价是下位机要写查表或位运算Matlab 端也要对应实现。以我的经验先用累加和把链路快速跑通如果实测坏帧率偏高再升 CRC 也不迟。但校验字段绝对不能省否则一个字节错了后面的数据全部错位直到碰巧撞到下一个帧头才恢复——波形上就是一根莫名其妙的尖刺你根本分不清是传感器真跳了还是数据坏了。2.3 下位机发送端的参考实现下位机发送端反而简单把结构体拼好后往串口发送缓冲里塞就行核心代码如下void send_frame(int16_t *ch_data, uint8_t ch_cnt) { uint8_t buf[64]; // 按最大通道数调整大小 uint8_t n 0; uint8_t sum 0; buf[n] 0xAA; buf[n] 0x55; buf[n] ch_cnt; sum 0xAA 0x55 ch_cnt; for (uint8_t i 0; i ch_cnt; i) { buf[n] (uint8_t)(ch_data[i] 0xFF); buf[n] (uint8_t)((ch_data[i] 8) 0xFF); sum (uint8_t)(ch_data[i] 0xFF); sum (uint8_t)((ch_data[i] 8) 0xFF); } buf[n] sum; buf[n] 0x0D; buf[n] 0x0A; // 把 buf[0..n-1] 依次写入串口发送寄存器 }这里有个关键习惯发送节奏要用定时器定频触发而不是在主循环里“有数据就发”。比如你希望每通道 200Hz 采样率4 通道一帧那就把定时器设成 5ms 发一帧。定时器保证帧间隔均匀后面画出来的波形时间轴才可信。如果放在主循环里随手发帧间隔会被其他任务挤成乱七八糟的分布波形形状可能还看不出来但拿去做 FFT 就会看到一堆杂散频谱到时候排查起来非常痛苦。3. Matlab 端完整实现串口读取、帧解析与实时绘图3.1 串口连接的推荐写法Matlab 从 R2019b 开始主推 SerialPort 对象老式 serial 对象虽然还能用但官方一直在推动迁移。新代码我建议直接用 serialport接口更干净缓冲管理也更可靠。连接代码很简单s serialport(COM3, 115200); s.Timeout 2; flush(s);COM3 换成你自己的口号波特率必须和下位机一致。端口可以在设备管理器里查也可以先用serialportlist(available)列出当前可用的口。连接后立刻flush(s)把上电瞬间缓冲区里的残留字节清掉不然解析函数可能拿半截脏数据开刀。这里必须提一个高频坑Matlab 脚本异常退出后串口对象可能还在后台占着端口下一次运行会报打不开。最省事的办法是在脚本开头加一段清理兼容代码% 清理历史串口对象残留兼容老版 serial新版 serialport 也走 try 兜底 if ~isempty(instrfindall) fclose(instrfindall); delete(instrfindall); end try delete(s); catch end clear s3.2 字节流变数据帧缓冲拼接与同步解析串口数据是流式的一帧可能一次到达也可能被拆成两段中间还夹杂着上电时的乱码。所以上位机必须维护一个字节缓冲区收到新数据先拼进去再尝试在流里找完整帧。核心函数如下可以直接放进脚本里function parsed parseFrameBuffer(raw, chCnt) persistent buf; if isempty(buf) buf uint8.empty; end buf [buf, raw(:)]; frameLen 6 chCnt * 2; parsed zeros(0, chCnt); while numel(buf) frameLen hdr find(buf(1:end-1) 0xAA buf(2:end) 0x55, 1); if isempty(hdr) % 没找到完整帧头保留最后2字节因为可能是帧头的后半截 buf buf(end-1:end); break; end % 丢弃帧头前的所有垃圾字节 if hdr 1 buf(1:hdr-1) []; end % 数据不足等下一批字节 if numel(buf) frameLen break; end % 通道数校验 if buf(3) ~ chCnt buf(1) []; continue; end % 校验和 帧尾 checkSum mod(sum(buf(1:frameLen-3)), 256); if checkSum buf(frameLen-2) ... buf(frameLen-1) 0x0D buf(frameLen) 0x0A dataBytes buf(4:3 chCnt * 2); vals double(typecast(uint8(dataBytes(:)), int16)); parsed(end 1, :) vals; buf(1:frameLen) []; else % 校验失败丢掉1个字节继续找同步头 buf(1) []; end end end这个函数有几个细节值得解释。第一persistent变量相当于 C 里的 static函数被多次调用时缓冲区能跨调用保存数据。第二查找帧头用find(buf(1:end-1) 0xAA buf(2:end) 0x55, 1)一次定位连续帧头比逐字节扫描高效。第三校验失败时只弹出一个字节而不是丢掉整帧这样错位的字节迟早会滚出缓冲区绝不会因为“丢整帧”错过后续正确的数据。第四typecast(uint8(dataBytes(:)), int16)会把连续的 2×N 个字节按小端还原成 N 个 int16这就是二进制帧设计带来的解析效率优势。3.3 多通道实时曲线绘制绘制部分我推荐用animatedline它内部维护了一个动态点列自带MaximumNumPoints滚动窗口数据量再大也不会无限占内存。每条通道创建一个animatedline对象颜色自动从lines色图里取多通道在图上能清楚区分chCnt 4; hLines gobjects(chCnt, 1); colors lines(chCnt); ax gca; for i 1:chCnt hLines(i) animatedline(ax, Color, colors(i, :), ... LineWidth, 1.2, MaximumNumPoints, 20000); end grid(ax, on); xlabel(ax, 帧序号); ylabel(ax, 数值);追加新数据点时每个点都要addpoints然后统一drawnow。这里有个性能关键drawnow limitrate会把重绘频率限制在每秒 20 帧左右对肉眼观察完全够用但 CPU 占用会大幅下降。如果你用的是老版本 Matlab 不支持limitrate就自己用 tic/toc 控制或者干脆每攒够 50 到 100 个点刷新一次。3.4 完整脚本骨架把上面几块拼起来就是一个可直接运行的采集绘图脚本%% 参数 port COM3; baudRate 115200; chCnt 4; scale 1; % 原始值到物理量的缩放系数 %% 连接串口 if ~isempty(instrfindall) fclose(instrfindall); delete(instrfindall); end try delete(s); catch end clear s s serialport(port, baudRate); s.Timeout 2; flush(s); %% 图形窗口 figure(Name, Multi-Channel Waveform, NumberTitle, off); ax gca; colors lines(chCnt); hLines gobjects(chCnt, 1); for i 1:chCnt hLines(i) animatedline(ax, Color, colors(i, :), ... LineWidth, 1.2, MaximumNumPoints, 20000); end grid(ax, on); xlabel(ax, 帧序号); ylabel(ax, 数值); %% 主循环 t 0; while ishandle(hLines(1)) if s.NumBytesAvailable 0 continue; end raw read(s, s.NumBytesAvailable, uint8); parsed parseFrameBuffer(raw, chCnt); for k 1:size(parsed, 1) t t 1; for i 1:chCnt addpoints(hLines(i), t, parsed(k, i) * scale); end xlim(ax, [max(0, t - 1000), t 10]); end drawnow limitrate; end delete(s);窗口一次显示最近 1000 帧也就是一屏 1000 个点。想看长时趋势就把滚动范围调大MaximumNumPoints也要同步调大。这里提醒一句xlim会触发重绘所以不要放在内层 for 里放外层循环就够了我这里为了代码简短放到了外层 for 下面实际用的时候保持这个位置就行。4. 波特率、通道数与波形刷新率动手前先算一笔账4.1 波特率背后的字节速率不是直觉那个数很多人设置波特率靠感觉结果要么太慢导致波形“卡成 PPT”要么设得太高导致下位机或 USB 转串口扛不住。其实计算很简单。UART 在常规 8-N-1 模式下每发送 1 个字节要占用 10 个 bit1 个起始位 8 个数据位 1 个停止位。所以有效数据字节速率 波特率 / 10。9600 波特率不是 1200 字节/秒而是约 960 字节/秒这个差异经常被忽略波特率有效数据速率9600约 960 B/s57600约 5760 B/s115200约 11520 B/s921600约 92160 B/s4.2 通道数、帧长和采样率上限前面定的帧结构里固定开销是 6 字节帧头 2 通道数 1 校验 1 帧尾 2每增加一个通道加 2 字节所以帧长 6 2N。一帧包含所有通道各一个样本因此每通道的采样率就等于帧率。用有效字节速率除以帧长就得到理论最大帧率通道数 N帧长115200 波特率下理论采样率留 30% 裕量后的参考值414 B约 822 Hz约 500 Hz822 B约 523 Hz约 350 Hz1638 B约 303 Hz约 200 Hz注意这只是串口链路的理论上限还没算下位机采集和发送代码的耗时、USB 转串口延迟、Matlab 读取和绘图的 CPU 开销。所以我实际估算时都会再打七折左右。如果你的应用需要每通道 1kHz 以上采样要么把波特率升到 460800 或 921600要么在一帧里打包多个采样点减少固定开销占比。还要留意一个现实问题有些 USB 转串口芯片在高波特率下并不稳定尤其是线材质量差的时候115200 以内通常没事上到 500000 以上就可能丢字节。所以调试初期我建议先用 115200 跑通全链路确认无误后再根据需求往上加。4.3 时间轴用帧序号还是真实时间戳绝大多数场景下横轴用帧序号就够了因为下位机用定时器定频发送帧间隔基本均匀波形形状不会失真。但如果要做 FFT、功率谱或者严格的时序分析强烈建议在下位机帧里加一个 uint32 毫秒时间戳上位机用真实时间轴还原。这样即使某一帧在传输中延迟也不会被均匀假设给骗过去。我用 Windows USB 转串口实测过下位机严格 5ms 发一帧Matlab 端read到的字节并不是严格均匀到达的有时一次读进两帧有时几百微秒就来了。整体节奏看起来没问题但逐帧看时间间隔是有抖动的。如果横轴只是帧计数这种抖动不影响曲线形状但如果把帧计数直接当成毫秒去算频率就会引入可观的误差。这是很多人画波形没事、一做频谱就发现谱线变“糊”的根本原因。5. 实际运行中我踩过的坑毛刺、丢帧与串口占用5.1 现象波形上隔一阵冒出一根尖刺我调试四通道采集板时四路曲线大部分时间很干净但每隔一两秒就冒出一根尖刺尖刺只持续一个点位置不固定。传感器信号本身是平滑的所以第一反应就是数据链路出了问题而不是现场真有冲击。这种“孤立尖刺”最常见的来源有两个一是某个字节在传输中变了导致当前帧校验不过被解析函数按错误位置拆出一个假数值二是帧头误同步数据区里恰好出现了 0xAA 0x55被当成了新帧头。无论哪种本质都是“应用层没守住帧边界”。所以排查的第一步不是盯着波形猜而是去看原始字节流。5.2 排查链路先抓原始字节再谈画图我给自己定了一个排查顺序遇到数据异常先别急着改上位机代码按下面几步走把波形循环停下来在读取位置加一行临时代码把 raw 原始字节按 hex 存成 bin 文件或者直接显示前几百个字节。对照协议逐帧人工查看是不是连续的 AA 55 04 ... 0D 0A每帧长度对不对有没有多字节、少字节、帧头错位用串口助手接同一路下位机输出抓一段 hex 对比。如果串口助手正常问题大概率在 Matlab 读取或解析逻辑如果串口助手也有同样的问题就要往下位机和物理链路查。再用逻辑分析仪夹到下位机 UART TX 引脚上看 MCU 实际发出的字节流是否正确。这一步能精确区分是 MCU 代码 bug、还是 USB 转串口芯片或线材在传输中造成的问题。一般走到第 4 步就能定位了。我实际碰到过两个很有代表性的根因一是下位机固件里校验和计算和上位机不一致发送端没把帧头纳入累加和上位机却按包含帧头校验数据内容一变就偶尔坏帧波形上表现为尖刺二是数据区里恰好出现了 0xAA 0x55再叠加低概率的校验巧合被解析器当成了新帧头于是拆出一个错误数值。前者改统一协议就好后者可以靠把帧头加长到三字节、或者在帧里增加帧计数和长度字段来降低误同步概率。波形图这类调试工具宁可帧开销多一点也要让同步可靠性先上去。5.3 校验失败后不要硬拼帧要逐字节找同步头这是解析函数里最值得强调的设计校验失败时只丢弃一个字节然后继续找 0xAA 0x55。很多人第一版代码是“校验失败就把这一段全部跳过或者清空缓冲区”这样反而容易丢帧。试想一条字节流AA 55 04 xx xx 校验错 0D 0A AA 55 04 ...如果因为第一帧校验错就清空整个缓冲区后面本来正确的帧头和数据也没了但如果只丢一个字节下一次循环立刻就能在流里重新找到第二帧的 AA 55恢复同步。同时我建议在脚本里加一组健康度计数器总帧数、坏帧数、读取次数实时显示在 figure 标题上。不要等波形乱了才去猜数字会直接告诉你链路有多干净frameCnt frameCnt 1; badCnt badCnt (校验失败 ? 1 : 0); title(sprintf(总帧数 %d, 坏帧率 %.2f%%, frameCnt, badCnt / frameCnt * 100), ... FontWeight, normal);我自己的标准是USB 转串口115200 波特率正常连接的坏帧率应该是 0偶尔出现 0.1% 以内可以接受但不该持续如果持续超过 1%先检查波特率配置、接线长度、USB 转串口芯片质量大概率不是 Matlab 代码的问题。5.4 串口被占用脚本跑第二次就打不开端口另一个高频问题上次脚本用 Ctrl-C 中断或者直接关了 figure 窗口串口对象没来得及删除下次运行就报端口被占用。处理方法我在第 3 章已经给过清理代码这里再说完整一点。if ~isempty(instrfindall) fclose(instrfindall); delete(instrfindall); end这里要区分两个版本instrfindall只管老式 serial 对象新版 serialport 对象只要工作区里没有句柄引用就会被系统自动释放。所以更可靠的顺序是先clear s再用instrfindall兜底清老对象。如果你的采集循环封装在函数里函数异常退出后对象仍然存在但不可见这时候最省心的办法是主循环里用 try-catch 包住catch 里 delete(s) 再 rethrow保证端口一定会释放。最后一个小建议给脚本留一个“窗口关闭即退出”的机制也就是主循环里的while ishandle(hLines(1))。figure 一被关闭循环条件失效脚本走完最后的delete(s)释放端口这样正常退出不会残留占用。这套流程后来被我在好几个项目里复用。说句实在话波形绘图本身不是难点真正花时间的永远是搞清楚串口线上到底发生了什么。把协议设计、缓冲区解析、健康度统计这几件事做扎实Matlab 多通道波形图这套工具就能从“写着玩玩”变成调试台上离不开的标配。本文还有配套的精品资源点击获取