C#串口助手实战:实时接收数据并写入Excel的WinForms方案
发布时间:2026/10/30 3:50:59 锦皓数字建站

简介这是一份面向C#初学者与工控、物联网方向开发者的串口通信学习资源围绕C#串口助手展开重点解决串口数据收发与Excel表格存储、读取的联动问题。资源包共323个文件以210个ico、37个png、35个icns等界面图标资源为主另含8个cs源码文件、3个exe可执行程序、3个dll动态库及csproj、sln等工程配置压缩包约10.2MB可直接在Visual Studio中打开编译运行。内容涉及SerialPort类的波特率、数据位、停止位、校验位配置与DataReceived事件处理并演示通过Microsoft.Office.Interop.Excel或NPOI将接收数据写入工作簿、从表格加载数据回传串口同时包含错误日志与调试思路。已有274人学习适合需要快速搭建串口调试工具、掌握数据持久化方案的开发者参考借鉴。1. 拆开这个串口助手压缩包它到底能帮你省掉哪段重复劳动如果你手头有一台带 RS232 或 RS485 接口的设备需要一边收数据一边把数据落成表格那你大概率经历过这样的循环先用串口调试助手看波形确认协议通了再切到另一个工具手动复制粘贴最后打开 Excel 一列一列贴进去。这个串口助手.rar里的 C# 工程就是把这个循环压成一步——它把SerialPort收发的实时数据和 Excel 存储放在同一个 WinForms 界面里收一条存一条或者按批次存。从工程文件看这是一个标准的 .NET Framework WinForms 项目串口助手.csproj是项目文件Form1.cs和Form1.Designer.cs是主窗体逻辑与界面布局Class1.cs里通常放串口封装或 Excel 操作辅助类Resources.Designer.cs管资源。那些ResolveAssemblyReference.cache、DesignTimeResolveAssemblyReferences.cache是 Visual Studio 编译时生成的中间缓存不影响运行但说明这个包是从一个真实开发环境里直接打包出来的不是空壳模板。它适合谁做上位机的新手可以拿它当串口通信的骨架把Form1.cs里的收发逻辑拆出来改成自己的协议解析做设备测试的工程师可以直接用它把扭矩值、传感器读数这类周期性数据存成 Excel省掉手抄。不适合谁如果你的数据量是每秒上千帧、要连续跑几天那 Interop 写 Excel 的方式会拖垮界面得换成 NPOI 或直接写 CSV。这个包的价值在于“能跑通、能改”而不是“开箱即用扛生产”。2. 串口参数怎么配从 SerialPort 属性到 DataReceived 事件2.1 先搞清楚 SerialPort 的六个核心属性C# 里操作串口靠的是System.IO.Ports命名空间下的SerialPort类。这个类把底层驱动封装成了几个属性你只要配对通信就能建立。但配对不是随便填得跟设备手册对齐。属性含义常见取值配错的表现PortName串口号COM1、COM3打开就抛异常BaudRate波特率9600、115200收到乱码或收不到DataBits数据位8数据错位StopBits停止位One、Two帧边界识别失败Parity校验位None、Even偶发校验错误Handshake流控None、RequestToSend发得出去收不回来我一般会先写一个枚举把常用波特率列出来界面上用 ComboBox 绑定避免用户手输错。下面这段是初始化串口的最小代码放在Form1.cs的窗体加载或“打开串口”按钮里// 初始化串口对象字段声明在 Form1 类里 private SerialPort _serialPort; private void InitSerialPort() { _serialPort new SerialPort(); _serialPort.PortName cboPort.Text; // 从界面下拉框取串口号 _serialPort.BaudRate int.Parse(cboBaud.Text); // 波特率转整数 _serialPort.DataBits 8; // 数据位固定 8多数设备如此 _serialPort.StopBits StopBits.One; // 停止位 1 _serialPort.Parity Parity.None; // 无校验 _serialPort.ReadTimeout 500; // 读超时 500ms防止卡死 _serialPort.WriteTimeout 500; // 写超时 500ms _serialPort.DataReceived new SerialDataReceivedEventHandler(DataReceivedHandler); }逻辑说明DataReceived是事件注册串口收到数据时会在后台线程触发DataReceivedHandler。注意这个回调不在 UI 线程上直接在里面更新 TextBox 会抛跨线程异常必须用Invoke或BeginInvoke切回 UI 线程。参数上ReadTimeout和WriteTimeout建议都设不设的话设备掉线时读写会一直阻塞界面直接假死。2.2 DataReceived 里怎么收才不丢包DataReceived事件触发时BytesToRead告诉你缓冲区里有多少字节。很多人直接ReadExisting()读成字符串遇到二进制协议就翻车。稳妥做法是按字节读再根据你的协议判断帧头帧尾。private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e) { int bytesToRead _serialPort.BytesToRead; byte[] buffer new byte[bytesToRead]; _serialPort.Read(buffer, 0, bytesToRead); // 读原始字节不转字符串 // 切回 UI 线程更新显示 this.BeginInvoke(new Action(() { string hex BitConverter.ToString(buffer).Replace(-, ); txtReceive.AppendText(hex Environment.NewLine); // 同时把原始字节交给 Excel 存储逻辑 SaveToExcel(buffer); })); }这里有个血泪经验BytesToRead在事件触发瞬间可能只包含部分帧如果你的协议一帧是 20 字节但这次只到了 8 字节直接解析就会错位。常见做法是维护一个Listbyte接收缓冲区每次追加新数据然后循环找帧头凑够一帧再解析。这个包里的Class1.cs很可能就是干这个的你可以打开看看它的缓冲逻辑改成自己的帧格式。2.3 发送数据时要不要加延时发送本身简单_serialPort.Write()就完事。但如果你在循环里连续发多条命令中间不加延时设备可能来不及响应表现为“发出去没反应”。我一般会在两条命令之间加Thread.Sleep(50)到Thread.Sleep(200)具体看设备手册的响应时间。private void btnSend_Click(object sender, EventArgs e) { if (!_serialPort.IsOpen) return; byte[] sendBytes Encoding.ASCII.GetBytes(txtSend.Text); _serialPort.Write(sendBytes, 0, sendBytes.Length); // 如果是多条命令在这里加延时 // Thread.Sleep(100); }参数说明Encoding.ASCII适合文本协议如果是 Modbus RTU 这种二进制协议得自己拼字节数组。Write的第三个参数是长度别传错。3. 把串口数据写进 ExcelInterop 和 NPOI 怎么选3.1 Interop 方式能跑但别在实时循环里用项目正文提到用Microsoft.Office.Interop.Excel这是最直观的方式——你装过 Office就能在引用里加上这个库然后像操作对象一样操作 Excel。下面是一个写入单元格的典型写法using Excel Microsoft.Office.Interop.Excel; private Excel.Application _excelApp; private Excel.Workbook _workbook; private Excel.Worksheet _worksheet; private int _rowIndex 1; private void InitExcel() { _excelApp new Excel.Application(); _excelApp.Visible false; // 后台运行不弹窗 _workbook _excelApp.Workbooks.Add(); _worksheet (Excel.Worksheet)_workbook.Sheets[1]; _worksheet.Cells[1, 1] 时间; _worksheet.Cells[1, 2] 数据; _rowIndex 2; } private void SaveToExcel(byte[] data) { if (_worksheet null) return; _worksheet.Cells[_rowIndex, 1] DateTime.Now.ToString(HH:mm:ss.fff); _worksheet.Cells[_rowIndex, 2] BitConverter.ToString(data); _rowIndex; }逻辑说明Cells[row, col]是行列索引从 1 开始。每次写一行就_rowIndex。但这里有个大坑——每写一个单元格都是一次 COM 调用数据量大了界面会卡成幻灯片。我实测过每秒写 10 行、每行 2 列界面还能忍每秒 100 行就直接无响应。所以 Interop 只适合低频记录比如几秒一条的传感器数据。参数上_excelApp.Visible false必须设否则每写一条就闪一下窗口。另外记得在窗体关闭时释放 COM 对象不然 Excel 进程会残留在任务管理器里private void Form1_FormClosing(object sender, FormClosingEventArgs e) { if (_workbook ! null) _workbook.Close(false); if (_excelApp ! null) _excelApp.Quit(); // 释放 COM 对象 System.Runtime.InteropServices.Marshal.ReleaseComObject(_worksheet); System.Runtime.InteropServices.Marshal.ReleaseComObject(_workbook); System.Runtime.InteropServices.Marshal.ReleaseComObject(_excelApp); }3.2 NPOI 方式不依赖 Office适合批量写如果你不想在目标机器上装 Office或者数据量偏大换成 NPOI 更稳。NPOI 是纯 .NET 库通过 NuGet 安装NPOI包即可。它直接操作 xls/xlsx 文件流没有 COM 开销。using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; private IWorkbook _npoiWorkbook; private ISheet _npoiSheet; private int _npoiRow 1; private void InitNpoi() { _npoiWorkbook new XSSFWorkbook(); // xlsx 格式 _npoiSheet _npoiWorkbook.CreateSheet(串口数据); IRow header _npoiSheet.CreateRow(0); header.CreateCell(0).SetCellValue(时间); header.CreateCell(1).SetCellValue(数据); } private void SaveToNpoi(byte[] data) { IRow row _npoiSheet.CreateRow(_npoiRow); row.CreateCell(0).SetCellValue(DateTime.Now.ToString(HH:mm:ss.fff)); row.CreateCell(1).SetCellValue(BitConverter.ToString(data)); } private void SaveNpoiFile(string path) { using (FileStream fs new FileStream(path, FileMode.Create, FileAccess.Write)) { _npoiWorkbook.Write(fs); } }对比一下Interop 写 1000 行大概要几秒NPOI 写 1000 行基本瞬间完成。但 NPOI 的数据在内存里得手动调SaveNpoiFile落盘或者定时落盘。我一般会设一个定时器每 30 秒存一次防止程序崩溃丢数据。3.3 什么时候该放弃 Excel 直接写 CSV如果你的数据是纯文本、列数固定、不需要公式和格式那 CSV 是最省事的。File.AppendAllText一行代码就搞定还没有任何库依赖。Excel 能直接打开 CSV用户该看的数据一样看。这个包之所以用 Excel我猜是为了演示 Interop 的用法实际项目里我会优先 CSV等用户明确要格式再上 NPOI。4. 避坑排查串口和 Excel 联调时最容易翻车的五个点4.1 串口打开报“访问被拒绝”现象点“打开串口”弹异常提示端口被占用。原因另一个串口调试助手比如 sscom还开着同一个 COM 口Windows 不允许两个进程同时独占。解决关掉其他串口工具或者在设备管理器里确认端口号没变。我习惯在打开前先枚举SerialPort.GetPortNames()把可用端口列出来让用户选减少手输错误。4.2 收到数据但界面不刷新现象DataReceived里打了断点能进但 TextBox 没变化。原因跨线程操作 UI 控件WinForms 默认不抛异常但也不更新。解决所有 UI 更新都包在BeginInvoke里像 2.2 节那样。别用CheckForIllegalCrossThreadCalls false去关检查那是掩耳盗铃后面会出更诡异的崩溃。4.3 Excel 进程残留文件被锁定现象程序关了但任务管理器里还有 EXCEL.EXE下次打开文件提示“正在使用中”。原因Interop 的 COM 对象没释放干净。解决在FormClosing里按 3.1 节的顺序先 Close 再 Quit 再 ReleaseComObject最后调GC.Collect()和GC.WaitForPendingFinalizers()强制回收。这个后悔药我每次写 Interop 都会加。4.4 数据写进 Excel 但全是文本格式现象数字列在 Excel 里左对齐不能求和。原因Cells[row, col] 123写的是字符串。解决能转数值就转_worksheet.Cells[r, c] double.Parse(value)。如果数据里有前导零比如设备编号 00123那就保持字符串别转。4.5 波特率对了但收的是乱码现象能收到字节但显示出来是乱码。原因要么波特率差一个档位比如设备是 115200 你设了 9600要么数据是二进制而你按 ASCII 解码了。解决先用十六进制显示原始字节确认帧头对不对。如果帧头对但内容乱检查设备的字节序和你的解析顺序是否一致。5. 进阶把接收缓冲和 Excel 落盘做成可复用的类这个包里的Class1.cs是个很好的切入点。我一般会把串口接收缓冲和 Excel 写入各抽一个类主窗体只负责界面绑定。这样换协议、换存储方式时不用动Form1.cs。先看缓冲类。核心思路是维护一个Listbyte每次收到新数据就追加然后循环检查是否凑够一帧。帧头帧尾根据你的协议定这里假设帧头是0xAA 0x55帧长固定 20 字节public class FrameBuffer { private Listbyte _buffer new Listbyte(); private const int FrameLength 20; private static readonly byte[] Head { 0xAA, 0x55 }; // 返回解析出的完整帧列表 public Listbyte[] Append(byte[] incoming) { _buffer.AddRange(incoming); Listbyte[] frames new Listbyte[](); while (true) { // 找帧头 int headIndex FindHead(); if (headIndex 0 || _buffer.Count - headIndex FrameLength) break; byte[] frame _buffer.GetRange(headIndex, FrameLength).ToArray(); frames.Add(frame); _buffer.RemoveRange(0, headIndex FrameLength); } return frames; } private int FindHead() { for (int i 0; i _buffer.Count - 1; i) { if (_buffer[i] Head[0] _buffer[i 1] Head[1]) return i; } return -1; } }逻辑说明Append返回的是本次能解析出的所有完整帧。FindHead从头扫描找0xAA 0x55找到后判断剩余长度够不够一帧够就截取并移除。参数FrameLength和Head要根据实际协议改。这个类的好处是不管DataReceived一次来 3 个字节还是 50 个字节它都能正确拼帧。再看存储类。我一般定义一个接口Interop 和 NPOI 各实现一份主窗体只依赖接口public interface IDataWriter { void WriteRow(DateTime time, byte[] frame); void Flush(string path); } public class NpoiWriter : IDataWriter { private IWorkbook _wb; private ISheet _sheet; private int _row 1; public NpoiWriter() { _wb new XSSFWorkbook(); _sheet _wb.CreateSheet(data); IRow h _sheet.CreateRow(0); h.CreateCell(0).SetCellValue(time); h.CreateCell(1).SetCellValue(frame); } public void WriteRow(DateTime time, byte[] frame) { IRow r _sheet.CreateRow(_row); r.CreateCell(0).SetCellValue(time.ToString(yyyy-MM-dd HH:mm:ss.fff)); r.CreateCell(1).SetCellValue(BitConverter.ToString(frame)); } public void Flush(string path) { using (var fs new FileStream(path, FileMode.Create, FileAccess.Write)) _wb.Write(fs); } }主窗体里这样串起来private FrameBuffer _frameBuffer new FrameBuffer(); private IDataWriter _writer new NpoiWriter(); private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e) { int n _serialPort.BytesToRead; byte[] buf new byte[n]; _serialPort.Read(buf, 0, n); var frames _frameBuffer.Append(buf); foreach (var frame in frames) { _writer.WriteRow(DateTime.Now, frame); } }这样改的好处是以后要换 CSV 存储只需要写一个CsvWriter : IDataWriter主窗体一行不用动。验证方法也简单找一个能持续发固定帧的设备跑十分钟然后打开生成的 xlsx 看行数对不对、时间戳有没有断档。如果行数比预期少多半是FindHead漏了帧检查帧头字节有没有被数据内容干扰。从那以后我每次做串口加存储的项目都强制先把缓冲类和存储接口写出来再写界面。界面是最容易改的底层逻辑一旦写死后面换协议就是灾难。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。