简介面向需要开展MATLAB串口通信与实时数据可视化的工程师或研究人员资源通过RS232.m脚本完整示范了串口对象初始化、波特率数据位停止位校验位等参数配置、数据读取与帧解析以及基于plot函数绘制多通道波形的方法可直接迁移到嵌入式设备监控、实验数据采集与实时波形显示场景。资源包包含1个MATLAB源文件.m压缩后约1KB体量精简但功能覆盖完整适合有基础MATLAB使用经验并希望快速搭建串口采集与多通道波形显示原型的读者。目前已有1366人学习脚本中依次展示了串口缓冲区与超时设置、循环读取数据、数据帧封装与通道分离、多曲线绘图及图例标注等关键思路读者可依据自身硬件协议调整解析算法也可补充校验与解码逻辑直接用于实际项目验证。1. 串口绘图为什么还要单独写一套MATLAB 串口通信与多通道波形图的真实门槛很多人以为串口绘图就是把串口读到的数字直接plot出来真动手才发现设备端一次发几十个字节MATLAB 这边读到的却是断断续续的碎片波形要么飞线要么错位。用 MATLAB 做串口通信并绘制多通道波形图核心不是绘图函数而是「字节流怎么切成一帧帧完整数据、多路信号怎么对齐到同一条时间轴、实时刷新怎么不卡」。这套方案解决的问题很具体单片机或采集卡通过串口连续上报多路传感器数据PC 端 MATLAB 负责接收、解析、实时显示波形用于联调、数据验证和算法离线分析。适合做嵌入式调试、仪器数据采集、机器人状态监控的工程师和学生尤其是已经写好了下位机、就差一个能看清多通道波形的上位机调试工具的人。2. 串口拿到的不是波形先定数据帧协议2.1 从字节流到通道为什么帧结构决定成败串口是字节流不是消息流。下位机send出去的几十个字节到 MATLAB 这边可能一次读回 3 个、下一次读回 50 个中间没有任何天然边界。如果直接把读到的字节按顺序硬塞给通道结果就是通道错位、波形乱跳而且这种错位是随机的越调越玄学。所以在写任何绘图代码之前先把帧协议定死。帧协议解决三件事这一帧从哪开始、这一帧有多长、这一帧的数据对不对。常见做法是「帧头 通道数 数据长度 数据体 校验字节」。帧头用两个固定字节比如0xAA 0x55告诉接收端「从这里开始算一帧」通道数和长度告诉接收端「这帧该读多少字节」校验字节用来过滤噪声和错位残帧。通道数大于等于 2 时帧里各通道数据按固定顺序排列接收端解析时按同样的顺序切分即可。这里有一个容易被忽略的点多通道数据是「时分复用」排列的即一帧里先放通道 1 的全部采样点还是按采样时刻交叉排列这两种方式对应完全不同的解析逻辑。最常见的做法是「按时刻交叉排列」也就是一帧里依次放 [ch1_k, ch2_k, ch3_k]下一帧放 [ch1_k1, ch2_k1, ch3_k1]。这样接收端每收到一帧就拿到所有通道在同一时刻的一个采样点画图时各通道天然对齐。2.2 一个能落地的帧格式帧头、长度、校验与解析伪代码我一般用下面这个帧格式在模拟项目 X 里验证过稳定性和可读性都够用字节偏移内容长度说明0帧头 11 字节固定0xAA1帧头 21 字节固定0x552通道数1 字节本次上报的通道个数如0x03表示 3 通道3数据长度1 字节数据体总字节数单位是字节4 ~ 4L-1数据体L 字节各通道数据int16 小端按时刻交叉排列4L校验1 字节从帧头开始到数据体末尾的累加和取低 8 位int16 小端的意思是每个采样点占 2 字节低字节在前。比如通道 1 的数值0x1234在帧里就是0x34 0x12。这个字节序必须在 MATLAB 里指定否则解析出来全是乱码。数据长度字段为什么单独留 1 字节因为这样可以支持非固定帧长。比如某次上报只带 2 个通道下次带 4 个通道接收端读数据长度字段即可知道该切多少字节不用烧录一次固件改一次协议。如果通道数和采样率都固定也可以省略长度字段直接在代码里写死但一旦后续需求变化又要改协议。我的建议是保留成本只有 1 个字节。校验算法用累加和最省事下位机 C 代码和 MATLAB 都一行写完function isValid checkSum(frameData) % 累加和校验从帧头到数据体末尾逐字节相加取低 8 位 sumBytes mod(sum(uint16(frameData(1:end-1))), 256); isValid (uint8(sumBytes) frameData(end)); end逻辑说明frameData是完整的一帧最后一字节是下位机算好的校验值。MATLAB 这边把前面所有字节累加取低 8 位和最后一字节比对。uint16是为了防止累加时单字节溢出变成 0mod(..., 256)等效于取低 8 位。注意这里不能用uint8直接累加MATLAB 的uint8加法会自动饱和到 255而不是回绕算出来的校验永远不对。参数说明如果下位机用的是 CRC8这里改成 CRC8 查表即可但累加和已经能挡住绝大多数串口噪声和高位误码够用。3. MATLAB 串口通信配置、读取与解析3.1 用 serialport 打开串口句柄配置与回调挂接MATLAB 操作串口有两套接口老的serial和新的serialport。新接口返回对象句柄属性和读写方法更规范读数据直接read不用fread也没有fopen那套繁琐流程。我用的是serialport下面所有代码都是这个接口。% 打开串口波特率 115200小端字节序 s serialport(COM7, 115200, ByteOrder, little-endian); s.Timeout 1; % 清空接收缓冲区避免旧数据混入新帧 flush(s); % 设置 ByteOrder 属性也可以在构造时指定 configureTerminator(s, LF);逻辑说明serialport第一个参数是串口名Windows 一般是COMxLinux 是/dev/ttyUSBxmacOS 是/dev/tty.usbserial-xxx。ByteOrder必须和下位机一致这里指定little-endian对应 2.2 节的帧格式。flush(s)清掉串口缓冲里残留的旧字节否则程序启动后第一次解析可能碰到半截残帧。configureTerminator设置终止符如果下位机每帧末尾带换行符0x0A这里设置LF后可以用readline按行读取如果不带终止符就不用配置。参数说明波特率要和下位机一致常见的 115200、460800、921600 都行。串口数据量不大时 115200 足够一秒约 11.5 KB8 通道、每通道 100Hz 采样、每个采样点 2 字节一秒钟才 1.6 KB远小于带宽。如果采样率提到 1kHz 且 16 通道1 秒 32 KB115200 波特率就开始吃力这时候要么降采样率要么换 460800 以上波特率。3.2 字节缓冲与帧同步解析函数怎么写才不丢帧串口读到的数据是流式的不能保证每次read都恰好拿到完整一帧。所以接收端必须维护一个字节缓冲每次从串口读新数据先追加到缓冲末尾再从缓冲里找帧头、切帧、校验。function [buffer, chData] parseFrames(buffer, maxChannels) % buffer: uint8 行向量累积的未处理数据 % chData: 解析出的最新一帧按通道排列的 double 向量 % maxChannels: 最大通道数防止异常数据导致数组越界 chData []; frameFlag false; while numel(buffer) 6 % 至少够帧头通道数长度校验 % 找帧头连续两个字节 0xAA 0x55 headIdx find(buffer(1:end-1) 0xAA buffer(2:end) 0x55, 1); if isempty(headIdx) % 缓冲里找不到完整帧头保留最后一字节等下一批数据 buffer buffer(end); break; end % 丢弃帧头前的垃圾字节 buffer buffer(headIdx:end); if numel(buffer) 4 break; end numCh double(buffer(3)); dataLen double(buffer(4)); frameLen 5 dataLen; % 帧头2 通道数1 长度1 数据 校验1 if numel(buffer) frameLen % 帧还没收全等下一批数据 break; end % 校验 if checkSum(buffer(1:frameLen)) % 解析数据体按通道数切分每通道 dataLen/numCh/2 个采样点 payload buffer(5:4dataLen); values typecast(payload, int16); samplesPerCh dataLen / (numCh * 2); % 取每个通道最新一个采样点 for ch 1:numCh idx (ch - 1) * samplesPerCh samplesPerCh; chData(ch) double(values(idx)); end frameFlag true; % 移除已解析的帧 buffer buffer(frameLen1:end); else % 校验失败跳过 1 字节继续找帧头 buffer buffer(2:end); end end end逻辑说明这个函数的核心是「只处理完整帧处理完立即从缓冲里移除」。find定位帧头定位不到就只保留最后一个字节防止帧头0xAA恰好落在缓冲末尾而0x55在下一批数据里。校验失败时逐字节跳过而不是整帧丢弃是为了防止帧头误匹配导致的错位。typecast把字节流按int16小端重解释效率远高于逐字节移位拼接。参数说明maxChannels是安全上限防的是下位机异常上报导致samplesPerCh算出来的数组越界。samplesPerCh的意义是下位机一帧里每通道上报了几个采样点。如果下位机每帧每通道只发 1 个点那dataLen numCh * 2samplesPerCh 1循环里取的就是唯一那一个点。3.3 读写延迟与缓存水位该调的两个参数串口通信的延迟主要来自两个地方MATLAB 读串口的轮询间隔以及数据在系统驱动缓冲区的堆积时间。serialport有Timeout和NumBytesAvailable两个属性直接影响行为。Timeout影响阻塞式read的等待时长。我把它设成 1 秒意思是如果串口暂时没数据read最多等 1 秒再返回空结果。设太大会让程序停顿感明显设太短会频繁空转占用 CPU。NumBytesAvailable是当前驱动缓冲里可读的字节数读之前先查这个值避免read阻塞。常见做法是循环里先判断s.NumBytesAvailable 0再读读多少就read(s, s.NumBytesAvailable, uint8)一次全读上来交给parseFrames处理。这里有一个最常见的性能陷阱如果把解析函数放在while循环里、每轮都等待串口数据CPU 占用会很高而且 MATLAB 单线程执行绘图会被阻塞。后面第 4 章会讲怎么把接收和绘制解耦这里先记住一条纪律——读串口永远用非阻塞方式循环里先查NumBytesAvailable有数据才读。4. 多通道实时波形绘制从数据到曲线4.1 双通道绘图的最小实现line 句柄与滚动窗口有了解析出的chData下一步就是把它画出来。直接plot会不断追加新线条画几千帧后图窗里全是对象内存涨、刷新慢。正确做法是创建line对象然后每帧更新它的XData/YData让曲线在固定窗口内滚动。% 初始化在指定坐标区创建线条对象初始数据为空 ax axes(Parent, gcf); hLine1 plot(ax, NaN, NaN, r-, LineWidth, 1.2); hLine2 plot(ax, NaN, NaN, b-, LineWidth, 1.2); windowSize 1000; % 滚动窗口最多显示 1000 个采样点 xData 1:windowSize; y1 nan(1, windowSize); y2 nan(1, windowSize); % 每来一帧更新数据 y1 [y1(2:end), chData(1)]; y2 [y2(2:end), chData(2)]; set(hLine1, XData, xData, YData, y1); set(hLine2, XData, xData, YData, y2); drawnow(limitrate);逻辑说明NaN在plot里表示断点初始曲线不显示之后每帧把新采样点追加到末尾同时丢弃最旧的一个点形成滚动效果。set(hLine, XData, ...)比重新plot快得多因为对象复用绘制引擎只需要更新顶点数据。drawnow(limitrate)限制刷新率不超过 20 fps避免高频数据把 GUI 线程拖死。参数说明windowSize是滚动窗口长度设 1000 时 100Hz 采样率能显示 10 秒波形。窗口越长波形细节越模糊越短越能看清瞬时波动。多通道场景下每个通道可以单独设窗口长度比如看长时间趋势用 5000看细节用 200。LineWidth设 1.2 是为了在屏幕缩放后仍然清晰导出图片时建议设 1.5 以上。4.2 多通道图怎么排子图、归一化与波形重叠通道数多于两个时绘图排版影响可读性。最常见的是两种多子图上下排列或者单图重叠不同颜色。多子图的优点是每个通道有独立 Y 轴量程单位不同也不冲突缺点是图上面积小通道路数多时每个子图高度不够。重叠图正好相反所有通道共享时间轴适合量程相近的信号比如多路温度、多路电压但量程差异大的通道会互相挤压小信号被压成一条直线。我一般按信号性质决定同量纲、量程差异小于 10 倍的重叠画否则子图画。子图用subplot或tiledlayout都行前者是老接口后者新一点但布局更整齐。% 3 通道子图布局 t tiledlayout(gcf, 3, 1); ax1 nexttile(t); hold(ax1, on); ax2 nexttile(t); hold(ax2, on); ax3 nexttile(t); hold(ax3, on); h(1) plot(ax1, NaN, NaN, r-); h(2) plot(ax2, NaN, NaN, g-); h(3) plot(ax3, NaN, NaN, b-); % 各通道独立 Y 轴避免量程互相影响 ylim(ax1, [-5, 5]); ylim(ax2, [0, 100]); ylim(ax3, [-50, 50]);逻辑说明tiledlayout创建的布局对象可以用nexttile依次获取子图坐标区比subplot更容易控制子图间距和整体比例。hold on后plot不会清空坐标区配合初始NaN数据后续直接用set更新即可。各子图ylim根据通道量程单独设置是防止量程差异大时小信号看不见的关键一步。linkaxes不需要用因为时间轴本身就是共用的如果各子图采样率相同XLim天然一致。如果通道数到 8 个以上子图高度会非常小这时候优先考虑重叠图加透明度或者把信号先做归一化再重叠。归一化用(x - min(x)) / (max(x) - min(x))映射到 [0,1]但这个做法会丢失绝对幅值信息只适合观察波形形状和相位关系不适合做幅值测量。所以归一化重叠图我一般只用于「快速巡检」正式的测量看子图。4.3 控制刷新率绘制频率与数据积压的关系实时绘图最容易翻车的点是下位机数据源源不断进来MATLAB 每帧都drawnow结果绘图速度跟不上数据到达速度串口缓冲越积越多最终丢数据。这个问题的根源是绘制频率和数据采样率不匹配。% 主循环里用计数控制绘制频率 % drawCount 每 5 帧刷新一次即数据 100Hz 时绘图 20fps drawCount drawCount 1; if drawCount 5 drawnow(limitrate); drawCount 0; end逻辑说明drawnow(limitrate)本身有内部限速逻辑大约每秒 20 帧上限但在数据到达率远高于 20 帧时每次调用仍会同步绘制造成阻塞。手动加一个跳帧计数实际绘制频率降到数据帧率的 1/5能显著减少 GUI 线程压力。代价是波形有轻微视觉延迟5 帧的数据延迟在 100Hz 采样下是 50ms肉眼几乎无感。如果数据到达率超过 1kHz跳帧还不够应该改成「定时器读取 主循环绘制」双线程模型。timer对象里只做串口读取和数据解析把结果存到全局变量主循环只管画。这样绘图卡顿时数据不会丢串口缓冲也能保持低位。缺点是代码复杂度上升但这是高频采集场景下唯一不掉数据的稳定方案。5. 串口波形翻车现场串口绘图常见的 5 个坑与排查5.1 波形毛刺多不是噪声是校验位和缓冲溢出现象波形整体形状正确但每个采样点附近有成簇的尖刺看起来像叠加了高频噪声。原因最常见的不是电磁干扰而是下位机波特率不准确或串口驱动缓冲溢出导致字节丢失校验不过关时整帧被丢弃而丢弃的恰好是某个通道的某个点波形上就出现单独跳变的尖刺。解决先用示波器或上位机自发自收确认波特率误差在 2% 以内然后在下位机发送端加小延时比如每帧 1ms降低瞬时写入压力最后在 MATLAB 侧将校验失败的帧计数打印出来如果高频尖刺和丢弃帧数成正比基本可以定位是缓冲溢出而非噪声。5.2 通道值互相串位帧头误匹配现象通道 1 偶尔变成通道 2 的波形几帧后又自己恢复看起来像「串台」。原因数据体里出现了和帧头相同的字节组合0xAA 0x55解析逻辑把数据体中的两个字节误认为新帧头切帧位置偏移后面所有通道数据全错位。解决加帧头转义机制下位机发送时遇到0xAA后紧跟0x55就在中间插入填充字节0x00接收端解析时删除填充字节或者把帧头从 2 字节扩到 3 字节0xAA 0x55 0x01降低误匹配概率。注意帧头字节不能和数据体可能出现的值过于接近这是协议设计阶段就要考虑的问题。5.3 一卡一卡不流畅回调里绘图现象波形能显示但刷新有明显卡顿像幻灯片。原因如果用了BytesAvailableFcn回调回调函数里直接做plot或drawnowMATLAB 的图形刷新会阻塞串口回调线程数据积压越来越多卡顿越来越严重。解决回调函数里只做读取和解析把结果存到全局变量或 handle 对象属性中主循环或者timer里只绘图不读串口。这条我在模拟项目 X 里的第一版就踩了改成「回调存数据、主循环绘图」后帧率翻了不止一倍。5.4 打开串口报错端口被占用现象serialport(COM7, 115200)直接报错说无法打开。原因COM7 被设备厂商自带软件、其他串口助手或上一次异常退出的 MATLAB 进程占用也可能实际端口号不是 COM7。解决先关掉所有串口工具在 MATLAB 里执行instrfind查看所有串口对象instrfind这里列出的每个对象如果对应的是被占用的串口执行delete删除旧对象再重新创建新的serialport。如果删完还是报错去设备管理器确认端口号有没有变化部分 USB 转串口模块每次插入可能分配不同 COM 号。最后检查驱动是否正常设备管理器里端口是黄色感叹号的话串口工具里能看到但打不开。5.5 程序跑起来没数据终止符和流控不匹配现象串口打开了循环一直提示NumBytesAvailable为 0。原因下位机每帧末尾没有换行符而 MATLAB 配置了configureTerminator(s, LF)或者下位机和 MATLAB 的流控设置不一致比如下位机开启了 RTS/CTSMATLAB 默认没开。解决用s.PinStatus查看 CTS/DSR 状态如果 CTS 一直是低电平说明流控不匹配把下位机关掉流控MATLAB 侧用configureTerminator(s, )清空终止符改成纯字节流读取。还有一个小坑是 MATLAB 的readline必须配合终止符使用如果只用read就不需要终止符两者不要混用。6. 把波形画得更顺手数据对齐、时间戳与离线回放多通道波形做好以后下一步通常是对齐和回放。串口本身不携带绝对时间信息MATLAB 里看到的 X 轴是采样点序号不是真实时间。要换算时间给每帧打时间戳是最实用的做法。% 记录每帧到达的时刻秒计算实际采样间隔 tStamp(k) toc(startTimer); if k 1 actualDt tStamp(k) - tStamp(k-1); end逻辑说明toc(startTimer)返回从启动到当前帧的秒数存入数组后可以算出实际采样周期。下位机标称 100Hz实际可能是 98Hz 或 103Hz长时间运行后纯按序号画的时间轴会漂移而按真实时间戳画就不会。这个差异在做频率分析、计算信号周期时非常关键我在一个振动数据采集的联调里因为没打时间戳FFT 峰值频率偏了 2%排查了很久才发现是时间轴假设错误。如果要在 MATLAB 里对多通道波形做离线分析建议把解析出的原始数据连同时间戳一起存成 MAT 文件而不是存图。图是给人看的数据才是给算法用的。存成结构体save(waveData.mat, t, chDataAll)后续加载后可以用相同的时间轴重新绘图、滤波、或者做频谱分析不用重新接硬件。形成一个习惯先在脚本里把接收、解析、存储做成三个独立函数绘图只是调用方之一这样后面要加自动保存、异常报警、频谱显示都能快速接入。离线回放是另一个值得提前做的功能。把录好的数据用for循环模拟实时更新YData加一个pause(0.01)模拟帧间隔就能复现当时的波形变化而不用依赖硬件。调滚动窗口、调颜色、调 Y 轴量程都可以在回放模式里完成不会因为没接设备而干等。我做这类上位机调试工具积累了一条经验先写一个「假数据源」函数能生成已知波形正弦、方波、随机噪声混合的模拟帧验证解析和绘图逻辑正确后再接真实串口。因为真实数据一进来你根本分不清是解析写错了还是传感器本来就那样而模拟数据永远可以对照正确答案。现在每次新写串口绘图程序我都会先把模拟数据这 20 行跑通再接硬件这个习惯帮我省掉了很多无效联调时间。希望帮到你。本文还有配套的精品资源点击获取