资讯动态

MATLAB多通道串口波形实时绘图:帧协议与animatedline优化

发布时间:2026/10/14 18:27:57 来源:尧图企业网站定制
简介针对MATLAB串口通信与多通道波形绘制的实际需求提供了一套紧凑的单文件实现方案。资源包内含1个.m文件压缩包大小仅1KB代码实现了完整的串口开发流程通过串口对象初始化函数设置波特率、数据位、停止位与校验位利用循环读取功能持续接收下位机数据配合数据帧封装与解析算法对原始字节流进行分包、校验和解码最终使用plot与hold on绘制多通道波形并添加坐标轴标签、图例以便对比不同通道变化。这一流程覆盖了从硬件接口配置到数据可视化的关键环节适用于嵌入式系统调试、实验设备监控与科研数据采集等场景适合高校学生、初学者以及需要快速搭建串口上位机的MATLAB用户。目前已有1366人学习下载整体代码结构清晰各功能模块相对独立用户可针对具体通信协议修改解析部分快速集成至个人项目。1. 串口绘图没那么玄一个可复用的MATLAB多通道波形工具之前帮一个开源硬件项目做上位机调试下位机要同时回传IMU三轴加速度、角速度和一路电池电压共 7 路数据采样率还不低。当时第一反应是开个串口助手看十六进制帧但数据一快人眼根本盯不住更别说分析某个通道的毛刺在哪。后来就用 MATLAB 的串口对象自己搓了一个多通道实时波形图工具从帧协议对齐、数据缓存到坐标轴预分配整套走下来发现这东西比想象中省事但也比想象中容易踩坑。这份资源的定位就是给做嵌入式调试、传感器采集、机器人底层开发的人用的你用单片机或开发板通过串口往外吐多路数据直接就能在 MATLAB 里看到平滑滚动、不卡顿的波形曲线也能把数据落盘做后续分析。2. 把帧结构定死协议设计决定上位机复杂度2.1 协议选型为什么推荐“帧头通道ID高字节低字节校验”串口本身只传字节不关心你传的是什么结构所以下位机必须先定一套协议上位机才能把字节流还原成有意义的物理量。常见做法有两种一种是纯文本行比如用printf(ch1:%d,ch2:%d\r\n)输出字符串另一种是二进制帧。我的建议是别用文本行除非你的通道只有一两个而且数据率极低。二进制帧的好处非常直接省带宽、解析快、不容易错位。一个典型的三字节数据包如果每位数据刚好落在 0~255 区间那你只需要帧头 通道ID 数据字节 校验字节四个字节就能搞定。但如果数据超过 255需要两个字节表示就得把帧结构扩充成帧头 通道ID 高字节 低字节 校验。这套结构中帧头是同步的唯一依据通道ID告诉你这一帧是哪个通道的数据高字节和低字节按大端或小端拼成完整数值校验位用来防止串口干扰导致的错帧。协议里最重要的一个原则是不要只用单个 0xAA 或 0x55 当帧头因为数据字节里完全可能也出现 0xAA一旦撞上解析就乱了。我一般会用两个连续字节做帧头比如0xAA 0x55这样误判概率低很多。2.2 下位机C语言发帧示例与参数说明配合这套协议下位机发帧的代码可以写成这样#define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 typedef struct { uint8_t id; uint16_t value; } SensorData; void send_frame(uint8_t channel_id, uint16_t raw_value) { uint8_t buf[5]; buf[0] FRAME_HEAD1; buf[1] FRAME_HEAD2; buf[2] channel_id; buf[3] (raw_value 8) 0xFF; // 高字节 buf[4] raw_value 0xFF; // 低字节 // 假设串口已经初始化直接通过发送接口发出 uart_write(buf, 5); }代码逻辑不复杂buf[0] 和 buf[1] 是帧头buf[2] 是通道编号buf[3] 是高字节buf[4] 是低字节。这里没有单独的校验位是因为有多通道循环发送场景下帧头已经够用但如果你对数据可靠性要求高建议再加一个buf[5] buf[0] ^ buf[1] ^ buf[2] ^ buf[3] ^ buf[4]的异或校验上位机收到后再算一遍对不上就丢弃。raw_value 是 16 位无符号数移位拆成高低两个字节输出上位机再用high * 256 low拼回来。2.3 波特率、采样率和通道数怎么匹配波特率不是随便定的。115200 波特率意味着每秒约 11520 字节按 10 bit/字节近似如果你一帧是 5 个字节那理论每秒最多 2304 帧。假设你有 6 个通道轮询发送每轮发 6 帧那实际每秒最多 384 组数据。如果你希望每个通道的刷新率达到 100Hz那每秒需要 600 帧也就是刚好约 3000 字节/秒115200 波特率还有一大半余量。如果数据量再往上走比如每个通道 1000Hz 刷新率6 个通道就是 6000 帧/秒单帧 5 字节就是 30000 字节/秒115200 波特率直接不够。这时候要么降通道数要么把数据包头压缩要么换更高波特率如 460800 或 921600。上位机这边需要同步修改串口对象的 BaudRate 属性两边不一致时能读到数据但解析出来全是乱码。最典型的特征就是波形毛刺多、数值突变到几百甚至上千但端口号和波特率设置看起来都正常。这时候第一件事就是回去核对上下位机的波特率是否完全一致而不是怀疑代码写错了。3. 坐标轴预分配用animatedline让波形不卡顿3.1 为什么不用plot直接画hold on模式的性能问题新手拿到数据的第一个想法通常是plot(t, data)然后在循环里hold on再画一条新线。这种做法在数据量小的时候看起来能跑但一旦串口持续接收几分钟绘图窗口就会越来越卡最后整个 MATLAB 界面都动不了。原因在于plot每次调用都会新建一个图形对象旧线条并不会被删除造成了大量的图形对象堆积而且每次画完还要drawnow强制刷新性能自然就崩了。正确做法是用animatedline它本质上是预分配一个动态线条对象后续通过addpoints往里面追加数据点而不是反复创建新对象。这样图形句柄只有一套内存开销稳定在一个固定水平上。3.2 animatedline预分配与初始化代码初始化多通道波形图的核心代码可以这样写numChannels 6; maxPoints 1000; % 每个通道最多保留1000个点超出后旧点自动丢弃 figure; ax gca; hold(ax, on); hLines gobjects(1, numChannels); colors lines(numChannels); for ch 1:numChannels hLines(ch) animatedline(ax, Color, colors(ch, :), ... LineWidth, 1.2, MaximumNumPoints, maxPoints); end % 时间戳与数据缓冲 timeBuffer zeros(1, numChannels); dataBuffer zeros(1, numChannels);这里gobjects预分配了线条句柄数组避免循环里动态扩容animatedline的MaximumNumPoints参数是性能关键它限制了每个通道最多保留的点数超出后自动淘汰旧数据相当于一个滚动窗口。lines(numChannels)生成一组区分度较高的颜色方便在同一个坐标轴里区分不同通道。3.3 绘图循环框架与数据读取节奏绘图循环里的核心操作是两种从串口读取数据并及时更新界面。我一般会在一个while循环里做这两件事循环条件用isOpen和用户界面状态控制例如按钮或键盘中断。绘图更新节奏单独控制避免每收到一个字节就刷新一次界面。每帧数据都刷新会造成界面高频重绘白白消耗 CPU。推荐的做法是串口数据到达后先暂存在缓冲变量里累积到一定点数或间隔一定时间再统一更新一次图形。比如每 50ms 刷新一次界面视觉上完全流畅CPU 负载却能降低一个量级。如果配合drawnow limitrateMATLAB 会限制刷新频率不会让绘图操作拖慢串口读取这是实时绘图里最实用的小技巧。4. 字节解析与实时刷帧串口数据流里的关键路径4.1 字节缓冲与帧同步底层的串口读取逻辑MATLAB 的串口对象有两种数据读取方式同步读取和异步回调。同步读取是在主循环里用read函数主动去读异步回调是用BytesAvailableFcn在串口收到指定数量的字节时触发。我的经验是实时波形绘图用异步回调更顺手因为读取和绘图可以解耦串口数据不会因绘图卡顿而堆积丢失。帧同步是解析里最容易翻车的一环。串口是字节流你不可能保证每次read读到的正好是一帧完整的数据所以必须自己维护一个缓冲区。常见做法是串口每收到一批字节就把它追加到缓冲变量里然后在缓冲里不断查找帧头找到后再检查长度是否够一个完整帧够了就取出来解析不够就留着等下一批数据。% 串口回调函数的骨架 function serialCallback(obj, ~) persistent buffer uint8 uint8.empty(1, 0); newData read(obj, obj.NumBytesAvailable, uint8); buffer [buffer, newData]; while numel(buffer) 2 % 查找帧头 0xAA 0x55 if buffer(1) 0xAA buffer(2) 0x55 if numel(buffer) 5 chID buffer(3); value uint16(buffer(4)) * 256 uint16(buffer(5)); % 通知主程序数据到达例如存入全局队列 appendData(chID, value); buffer(1:5) []; else break; end else buffer(1) []; end end end这段代码最关键的是buffer(1:5) []这行它把已消费的帧从缓冲头部移除让下一个帧头暴露出来。uint8.empty(1, 0)初始化空的 uint8 数组避免数据类型漂移。while循环里的numel(buffer) 2先保证至少有两个字节可以判断帧头。如果缓冲里的数据长度不足一个完整帧就 break 出去等下一批数据。这里面还有一个编码习惯值得注意把所有帧解析逻辑放在回调函数里会让代码变得难以调试尤其是当串口消息频率很高时回调里的 while 循环会阻塞其他操作。我更推荐回调只做一件事把新数据原样追加到缓冲然后直接返回帧解析和绘图放到主循环里处理。这样串口回调逻辑极其轻量不会拖累系统响应。4.2 校验、溢出与半包处理协议里如果加了异或校验解析时就必须同步校验。做法是在帧解析完成后把帧头、通道ID、高低字节全部异或一遍和收到的校验字节比较。如果不一致这一帧直接丢弃并且可以打印一条提示信息帮助下位机侧排查干扰来源。数据长度异常是另一个常见问题。串口数据在传输过程中可能出现丢字节比如下位机发送时被高优先级中断打断或者上位机读串口时系统调度延迟。这种情况下你收到的缓冲里可能残留半包数据即帧头出现了但后面数据不足 5 字节。处理方式很简单每次都先确认长度足够再消费不满足就等待下一次回调。还有一个很多人会忽略的点如果下位机发送速度太快而上位机主循环来处理数据不够快串口对象内部的系统缓冲可能溢出导致最旧的数据被丢弃波形就会呈现周期性丢包。观察方法是看接收到的帧计数是否连续。如果单次解析出的帧序号在协议里可以加一个递增的帧序号字段不连续说明缓冲区溢出要么降低下位机发送频率要么加大read调用频率。4.3 多通道波形绘制的完整脚本结构把上面的所有内容拼起来一个完整的多通道绘制脚本主体如下% 串口初始化 s serialport(COM3, 115200, Timeout, 1); configureTerminator(s, LF); % 预创建数据队列 dataQueue struct(ch, {}, value, {}); lastUpdateTime tic; while isvalid(s) % 每次从缓冲取数据这里假设回调已经填充了全局变量 recvBuffer while ~isempty(recvBuffer) [ch, val, recvBuffer] extractFrame(recvBuffer); if ch 0 dataIndex ch; % 更新对应通道的绘图数据 addpoints(hLines(dataIndex), timeStamp, val); end end % 限制刷新频率 if toc(lastUpdateTime) 0.05 drawnow limitrate; lastUpdateTime tic; end end这段代码里的extractFrame函数负责从recvBuffer里取出一个完整的帧返回通道号和数据值同时返回消费后的剩余缓冲。主循环只做两件事一是尽量从缓冲里提取帧二是把新点追加到对应通道的animatedline对象上。drawnow limitrate限制刷新率在 20fps 左右和串口接收速率解耦。注意这个示例里用了全局变量传递缓冲实际项目中可以用handle类或timer来避免全局变量污染但思路是相通的。5. 避免坑与常见问题排查5.1 串口读取超时导致数据断流现象下位机正常发数据但 MATLAB 端偶尔会卡住一串波形突然停了过一两秒又自己恢复了。原因serialport的ReadTimeout设置过短或主循环里某个操作耗时太长导致系统读取串口数据不及时底层缓冲区满了后新数据被丢弃上位机表现为数据断流。解决把Timeout调大比如设成 2 秒并且把绘图刷新和耗时的保存操作放到单独的timer里避免阻塞主循环的串口读取。这里我一般会把串口读取间隔设为 20ms 定时器绘图刷新用另一个 50ms 定时器互不阻塞。5.2 波形跳变、出现大量随机毛刺现象波形整体趋势正常但隔一段时间就会出现一个异常大的尖峰数值明显超出物理范围。原因大概率是帧同步丢失或字节错位。比如从某个中间字节开始解析把数据字节当成帧头或把通道ID当成高字节。还有一个隐蔽原因是下位机数据类型无符号和有符号混用导致拼出来的值是负数或溢出。解决先看原始数据帧打印每次解析的通道ID和值再把校验加上能滤掉九成以上的错帧。如果数据可能为负建议在协议里用偏移量或符号位标记不要直接强转。5.3 波形图越来越卡内存不断增长现象运行十几分钟后波形实时性明显下降MATLAB 占用的内存一路飙升甚至出现内存不足。原因animatedline的MaximumNumPoints没有设置或者用了plot每条数据新建线条另一个常见原因是把时间戳数组无限增长每次addpoints传的 x 坐标数组越来越长。解决在创建animatedline时指定MaximumNumPoints时间戳用环形缓冲或直接使用相对时间比如每次取toc(startTime)不要存储全部时间向量。还可以定期执行set(hLines(i), XData, [], YData, [])清空缓存的旧点。5.4 串口回调里直接绘图导致界面假死现象程序运行一会后 MATLAB 窗口失去响应需要强制关闭非常典型的假死现象。原因串口回调函数占用了串口对象内部的锁资源回调里又调用了绘图刷新和drawnow它们会尝试获取 MATLAB 图形线程的锁两边互等导致死锁。解决回调函数里绝不调用任何绘图相关代码只把数据放入一个队列或缓冲区所有绘图操作统一在主循环或定时器回调里执行。这是实时串口绘图最容易踩的一个死路很多新手都会在这里白白折腾几天。6. 进阶自动重连与数据落盘让工具变成调试搭档实时波形图能跑通之后下一步要对付的是串口插拔和长时间连续调试的问题。开发板在上电、复位、烧录的过程中USB转串口设备会断开重连端口号可能还变了。如果上位机没有自动重连机制每次插拔都要手动找端口号再重启脚本调试效率太低。自动重连的核心逻辑是先设置一个串口设备列表检测函数定期扫描当前系统中实际存在哪些串口端口号和脚本正在使用的端口号比对。如果检测到当前端口不存在了就尝试在候选端口列表里轮询连接连接成功后自动重新初始化animatedline的坐标轴并清空历史点。这个逻辑放到 1 秒一次的timer里很稳妥不会拖累主流程。数据落盘是调试的另一个刚需。波形图只能看趋势要做量化分析、回放定位异常必须有原始数据文件。我习惯用fopen打开一个 CSV 文件在解析出每一帧数据后直接追加一行% 打开文件并落盘 fileID fopen(capture.csv, w); fprintf(fileID, timestamp,channel,value\r\n); % 主循环中每解析一帧就记录 while isvalid(s) [ch, val] extractNextFrame(); if ch 0 fprintf(fileID, %.4f,%d,%d\r\n, toc(startTime), ch, val); end end fclose(fileID);这里%.4f保留四位小数的时间戳和通道号、数值组合成一行excel 或 MATLAB 的readtable都能直接读取做后处理。注意fopen打开文件后如果程序异常退出fclose不会触发数据可能丢失。我的习惯是写一个cleanup函数在脚本结束时自动关闭文件句柄。从那以后我每次搭串口上位机都会强制走一遍这套流程先定帧协议、再预分配坐标轴、回调尽量轻、刷新限频、最后把落盘和重连加上整套下来数据再也没丢过。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑