ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从零开发上位机:通信协议、串口通信与固件升级实战

从零开发上位机:通信协议、串口通信与固件升级实战

前两篇把Status Deck的机械结构、电源、主控板和固件流程聊完了,手里这块小面板已经能通电、能显示、能跑本地逻辑。可真正把它从“一个会亮屏的摆件”变成“一台能配置、能监控、能远程更新的桌面设备”,中间还差了最关键的一环——PC端的上位机。

这一篇就专门拆解桌面端上位机程序是怎么从零写出来的。我会按实际开发顺序走一遍:先讲整机逻辑里上位机到底负责什么,再讲我踩了三次坑才定下来的通信协议,然后给出发送接收、UI刷新、数据可视化、固件升级这些核心模块的可落地方案,最后把调试期遇到的一堆“文档里查不到”的问题整理成速查表。如果你也在做一个“硬件+PC软件”的全栈小项目,这篇应该能帮你省下至少一周的试错时间。

1. 整体设计拆解:先把上位机的职责和边界画清楚

1.1 Status Deck 上位机到底管什么

动手写代码之前,我先在一张白纸上把“上位机该干什么、不该干什么”列了出来。很多人一听到上位机,第一反应就是“打开串口、读数据、画曲线”,如果只做到这个程度,那它充其量是个调试助手,不是产品的一部分。

在Status Deck这个项目里,设备本身是一个带屏幕、带传感器的小盒子,固件负责渲染、按键、传感器采集和本地显示。但它有几个天然短板:一是配置特别痛苦,显示主题、采集周期、联网参数都写死在固件里,每次改一点就得重新烧录;二是缺少全局视角,设备端只能看到自己的传感器数据,PC的CPU占用、网速这些状态它拿不到;三是固件出了问题没法远程补救,只能拆壳接烧录器。

上位机就是来解决这三件事的。我把它拆成四个职责:配置管理(把设备参数下发并持久化在PC端)、数据中转(采集PC侧状态,按协议推给设备;同时接收设备上报的传感器数据)、可视化监控(把设备状态展示成曲线、卡片、日志,能回溯)、固件升级(作为设备更新的入口,承担文件传输与校验)。

这四个职责决定了技术选型。如果只是“读串口画曲线”,那几乎任何语言都能干;但要同时做配置持久化、界面交互、文件传输、异常恢复,还得让非技术用户也能顺手用,桌面端原生应用依然是性价比最高的选择。

1.2 技术选型:这次我为什么用 C# WinForms

上位机的主流方案就那么几类,我列了个对比表,把每条路都盘了一遍:

方案开发效率跨平台部署体积串口/系统API支持复杂UI表现力
C# WinForms高仅Windows小(自包含40MB左右)极好够用
C# WPF中高仅Windows小极好强
Qt(C++/PyQt/PySide)中好中好强
Python + Tkinter/PySide高好中(需打包解释器)好一般
Electron + Web技术中好大(100MB+)需走Node集成强

最后选了C# WinForms,理由很现实:串口支持是System.IO.Ports原生提供的,API简单稳定;WinForms的UI模型直白,做一个“设备列表+参数面板+实时曲线+日志框”的主界面,不需要引入重量级MVVM框架;发布时用.NET 8自包含单文件,扔到没有开发环境的Windows机器上照样跑。

WPF我考虑过,动画和样式确实更现代,但Status Deck的界面核心是实时数据和状态展示,不是复杂动态交互,WinForms加一点自绘就完全够了。Qt我也考虑过,如果这台上位机未来要跑在Linux工控机上,我大概率会换PySide6;但眼下项目的固定部署环境就是一台日常用的Windows PC,没必要一上来就把跨平台成本付掉。

顺带说一句,如果你是从零开始且C#不熟,Python + PySide6其实也很好上手,通信协议部分的设计思路完全通用,只是实现语言不同。

1.3 分层架构:通信层、协议层与 UI 解耦

个人项目最容易犯的错误,就是把所有代码堆在窗体文件里,一个Button_Click里既写串口读写,又写协议解析,还直接改控件文本。前期爽,后期改一个功能能牵扯出一堆副作用。

我这次强制自己按四层来组织工程:

  • 通信层:负责串口扫描、打开关闭、字节收发、热插拔监测。对外只暴露Send(bytes)和Receive事件,不关心字节内容是什么。
  • 协议层:负责组帧、校验、解析、会话管理。输入是通信层抛出的原始字节,输出是结构化的业务消息。
  • 业务层:负责具体场景,比如“采集PC性能数据并推送给设备”“收到传感器上报后更新最新值缓存”“触发固件升级流程”。
  • UI层:只做展示和用户操作转发,不直接碰串口。

工程目录大致是这样的:

StatusDeckStudio/ ├─ Comms/ // 通信层 │ ├─ SerialTransport.cs │ └─ DeviceDiscovery.cs ├─ Protocol/ // 协议层 │ ├─ FrameBuilder.cs │ ├─ FrameParser.cs │ └─ Commands.cs ├─ Services/ // 业务层 │ ├─ DeviceSession.cs │ ├─ TelemetryService.cs │ └─ FirmwareUpdater.cs ├─ UI/ // 界面 │ ├─ MainForm.cs │ └─ Controls/ └─ Program.cs

分层带来的最大收益是:协议解析这套逻辑,我既可以用在上位机里,也可以在写测试工具时直接复用;以后如果设备改成WiFi/TCP通信,只需新增一个TcpTransport实现通信层接口,协议层以上完全不动。

提示:不要为了分层而分层。四层是合理的下限,但这个项目没必要再引入依赖注入容器、仓储模式之类的重型结构,过度设计在小项目里同样是坑。

2. 通信协议先行:先把数据帧“合同”定下来

2.1 帧格式:帧头、命令字、长度和 CRC16

写上位机最容易犯的顺序错误,就是一上来先写界面,写完了才发现跟设备端对不上数据格式。我的建议是:打开IDE的第一件事不是建窗体和拖控件,而是把通信协议文档以注释形式写进代码顶部。

Status Deck的协议帧格式我定得比较传统,属于串口通信里最常见、也最不容易出错的一类:

字段长度说明
帧头2字节固定 0xA5 0x5A
命令字1字节区分消息类型
数据长度2字节小端序,表示数据段字节数
数据段N字节具体业务数据
CRC162字节对“命令字+长度+数据段”做CRC16-MODBUS

帧头为什么用0xA5 0x5A而不是0xAA 0x55?因为0xA5、0x5A这两个字节在普通ASCII报文里出现概率低,而且二进制形态分别是10100101和01011010,视觉上容易辨认,抓包时一眼能看到帧边界。

长度字段是解决“粘包”和“半包”的关键。设备端可能一次发来多帧,也可能一帧分两三次才发完,没有长度字段就只能靠猜;有了长度字段,解析器就知道“凑够这些字节才算一帧”。

CRC16我直接用MODBUS参数集(多项式0x8005、初值0xFFFF、输出异或0x0000)。它计算量小、嵌入式端几行C代码就能实现,C#端也到处有现成实现。有些方案会直接省掉校验,或者用简单的累加和,实测下来在USB转串口环境里一般问题不大,但一旦线缆质量差或者干扰强,偶发单字节错误会让整帧数据变成脏数据,而且很难排查。加上校验后,解析器可以直接丢弃错误帧并计数,可靠性完全不一样。

举个例子,设备上报一条环境温度数据,十六进制大概是:

A5 5A 10 03 00 01 D2 15 F5 9B

其中0x10是“上报传感器数据”命令字,0x03 0x00表示数据段长度3字节,数据段依次是传感器类型01、温度高字节D2、低字节15(也就是0x15D2=6610,除以100就是26.10℃),最后两个字节F5 9B是CRC16。

2.2 命令字表与双向数据流

通信协议最忌讳“想到哪儿加到哪儿”。我在文档里把命令字分成三组:主机主动请求、设备主动上报、双方向应答。

方向命令字名称数据段说明
下行0x01查询设备信息无
下行0x02心跳请求4字节随机数
上行0x81设备信息应答设备型号、固件版本、设备序列号
上行0x82心跳应答4字节随机数原样返回
上行0x10传感器数据上报传感器类型+数值
下行0x20下发显示配置JSON配置文本
下行0x30开始固件升级固件总大小、总包数
下行0x31固件数据包包序号+128字节固件数据
下行0x32升级结束固件CRC32总校验值

这里面有一个细节值得说明:0x02/0x82的心跳交互带随机数,是为了防止历史帧被意外重放。这个问题在串口环境不太常见,但设备可能因为看门狗重启留下半截响应,带随机数校验后,上位机能确认这帧心跳就是“针对我刚才那次请求”的,而不是一条滞后的旧消息。

另一个细节是版本协商。0x81应答里带固件版本号,上位机拿到后要先判断版本,再决定是否下发0x20配置、是否允许进入0x30升级流程。老固件可能不认识新配置字段,直接下发会导致设备解析异常。协议有版本意识,比盲目兼容稳妥得多。

2.3 解析器核心:用一个状态机搞定粘包和半包

协议解析这块,新手最容易写成一坨“找帧头-截字符串-硬切分”的临时逻辑,遇到半包就崩。我这次直接用了一个简单的状态机,这也是串口解析里最标准的做法。

状态流转简单说就是:等待帧头1 → 等待帧头2 → 等待命令字 → 等待长度低字节 → 等待长度高字节 → 等待数据段 → 等待CRC低字节 → 等待CRC高字节 → 完成。每收到一个字节,就推进一次状态;任何一步校验失败,立刻回到等待帧头1。

public class FrameParser { private enum State { WaitHeader1, WaitHeader2, WaitCmd, WaitLenLo, WaitLenHi, WaitData, WaitCrcLo, WaitCrcHi } private State _state = State.WaitHeader1; private byte[] _payload = new byte[0]; private int _payloadIndex; private ushort _payloadLength; private ushort _crc; private int _crcIndex; public event Action<ProtocolFrame> FrameReceived; public void Push(byte[] buffer, int offset, int count) { for (int i = offset; i < offset + count; i++) { byte b = buffer[i]; switch (_state) { case State.WaitHeader1: if (b == 0xA5) _state = State.WaitHeader2; break; case State.WaitHeader2: _state = b == 0x5A ? State.WaitCmd : State.WaitHeader1; break; case State.WaitCmd: _cmd = b; _state = State.WaitLenLo; break; case State.WaitLenLo: _payloadLength = b; _state = State.WaitLenHi; break; case State.WaitLenHi: _payloadLength |= (ushort)(b << 8); _payload = new byte[_payloadLength]; _payloadIndex = 0; _crc = Crc16Modbus.InitialValue; _crcIndex = 0; _state = _payloadLength > 0 ? State.WaitData : State.WaitCrcLo; break; case State.WaitData: _payload[_payloadIndex++] = b; if (_payloadIndex == _payloadLength) _state = State.WaitCrcLo; break; case State.WaitCrcLo: _crc = (ushort)(b & 0xFF); _state = State.WaitCrcHi; break; case State.WaitCrcHi: _crc |= (ushort)((b << 8) & 0xFF00); ushort calc = Crc16Modibus.Compute(_cmd, _payload); if (calc == _crc) FrameReceived?.Invoke(new ProtocolFrame(_cmd, _payload)); Reset(); break; } } } }

这个状态机的价值在于:不管对方是每次发一个字节还是一个字节一个字节地挤出来,只要字节流按顺序到达,它都能正确拼出一整帧;长度异常时自动丢弃还原,不会把后续帧的字节吃掉。

我还会在解析器里加一个最大帧长保护,比如超过2KB直接重置。因为设备如果跑飞了,可能不停地吐0xFF,没有上限保护的话,解析器会一直缓存垃圾字节直到内存被撑爆。

3. 串口模块实操:收发、热插拔与跨线程 UI 刷新

3.1 串口参数与打开逻辑:基本配置不是玄学

串口参数这块,很多人直接照抄“9600,8,N,1”,但Status Deck的数据量不大,我最后把波特率定在115200。这个速率下理论约11.5KB/s,一帧温度上报最多几十字节,余量很足;而且现在所有USB转串口芯片都稳定支持115200,没必要更低。

var sp = new SerialPort(portName, 115200, Parity.None, 8, StopBits.One) { ReadTimeout = 500, WriteTimeout = 500 }; sp.DataReceived += OnDataReceived; sp.Open();

打开串口有两点需要特别小心。第一,Open()会抛异常,常见原因包括端口被占用、设备被拔出、权限不足。上位机启动时不能让异常直接弹崩溃,我会在设备面板上把可用端口枚举出来,用户选择后再打开,打开失败只提示原因并保持面板可用。

第二,串口句柄是稀缺资源,关闭时必须Dispose。如果程序退出时没释放端口,其他工具就暂时打不开这个COM口。我一般把SerialPort放在DeviceSession里统一管理,Dispose时先关事件再关端口。

热插拔检测我用的是轮询法,500ms一次枚举SerialPort.GetPortNames(),跟当前列表对比,新增就刷进下拉框,减少就清除状态。更精细的做法是监听Windows的WM_DEVICECHANGE,但轮询委托简单可靠,对个人项目已经足够。

3.2 接收数据处理:不要在 DataReceived 里干重活

SerialPort的DataReceived事件是在后台线程触发的,这意味着你永远不应该在这个事件里直接改UI,也不应该在里面做耗时解析。正确做法是:快速把系统缓冲区的字节读出来,塞进自己的接收缓冲区,然后统一交给协议层处理。

private readonly object _rxLock = new object(); private readonly List<byte> _rxBuffer = new List<byte>(); private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { var sp = (SerialPort)sender; int available = sp.BytesToRead; byte[] chunk = new byte[available]; int read = sp.Read(chunk, 0, available); lock (_rxLock) { _rxBuffer.AddRange(chunk); while (TryExtractOneFrame(out var frame)) // 协议层解析 { // 交给业务层处理 _session.OnFrameReceived(frame); } } }

这个while循环很关键:串口一次事件可能带过来多帧数据,只解析一帧会把剩下的帧留在缓冲区里,下一轮又跟新数据混在一起。循环解析直到缓冲区里凑不出完整帧为止,剩下留到下次事件再处理。

3.3 跨线程刷新 UI:Invoke 与聚合刷新

WinForms的控件只能由创建它的线程操作,子线程直接改Text百分百抛InvalidOperationException。我刚上手时第一次跨线程刷新UI就吃了这个亏,当时的想法很朴素:“不就是加个锁吗”,后来才理解线程亲和性这个概念。

最简单的做法是Control.Invoke:

this.Invoke(new Action(() => labelTemp.Text = $"{temp:F1} ℃"));

Invoke是同步的,会阻塞后台线程等待UI线程执行完;BeginInvoke是异步的,不会阻塞。但这两个方法还有个隐形问题:如果设备以高频上报,每条数据都Invoke一次,UI线程的消息队列会被刷爆,界面卡成幻灯片。

所以我采用了“定时器聚合刷新”方案。UI线程挂一个100ms的System.Windows.Forms.Timer,每次Tick去拿业务层缓存的最新状态,批量刷新标签和曲线;串口事件线程只负责把数据写进缓存,完全不碰控件。

private void uiTimer_Tick(object sender, EventArgs e) { var snapshot = _session.GetSnapshot(); // 线程安全缓存 lblCpu.Text = snapshot.CpuLoad.ToString("F1") + " %"; lblTemp.Text = snapshot.Temperature.ToString("F1") + " ℃"; chart.PushPoint(snapshot.Temperature); }

100ms的刷新间隔对桌面仪表盘来说肉眼已经足够顺滑,而且UI线程压力极小。这个方案的通用性很强:不管底层是串口、TCP还是别的什么高频数据源,只要数据先落缓存,再由UI定时器统一取,界面就永远稳。

3.4 数据可视化:滚动曲线和状态卡片的落地做法

Status Deck上位机主要展示两类信息:实时数值(温度、湿度、CPU占用)和变化趋势(传感器历史曲线)。

实时数值我用最简单的Label摆成状态卡片,值变化时通过颜色和前景图标提示高低,这块没什么技术含量,关键在于色彩规范:正常绿色、警告橙色、异常红色,这套规则我在数据进入UI层之前就用枚举定义好,避免UI里散落一堆裸的色值判断。

趋势曲线这块有两条路。一是用现成库,比如ScottPlot,WinForms下集成非常顺滑,性能也能扛几千个点的实时滚动;二是自己做轻量自绘,在Panel的Paint事件里用Graphics画线。我最后选了ScottPlot,因为它自带双缓冲和坐标轴,缩放拖拽都不用自己造轮子。如果你不想引第三方依赖,自绘方案也可行,但必须记得把画线控件的DoubleBuffered设为true,否则滚动刷新时会闪得怀疑人生。

自绘滚动曲线的核心思路并不复杂:保留最近N个数据点,横轴按时间均匀排布,每次Paint重新计算坐标并画折线,超过N个点就丢弃最早的。

4. 配置、日志与固件升级:让上位机具备交付级品质

4.1 配置持久化:JSON 文件优于注册表

很多“电子工程师风格”的上位机喜欢把配置写进注册表,跨机器部署麻烦,备份也麻烦。我的做法是:所有配置集中到一个JSON配置文件,放在程序目录下,程序启动时读取,没有就生成默认配置。

{ "serial": { "port": "COM3", "baudRate": 115200, "reconnectOnOpen": true }, "device": { "expectedSerial": "SD-2025-001", "pollIntervalMs": 1000 }, "features": { "showCpuLoad": true, "showNetworkSpeed": true, "tempAlertHigh": 32.0 } }

这里有一个我踩过的坑:配置文件路径不能用Environment.CurrentDirectory,因为这个值会随着用户从哪个目录启动程序而变化。必须用AppContext.BaseDirectory,它永远指向exe所在目录,稳定可靠。

JSON序列化在.NET里直接System.Text.Json就够用,不需要引入第三方库。加载时要做容错:文件损坏就备份一份.bad,然后生成默认配置,绝不能一崩溃就白屏。

4.2 日志系统:调试期和交付期都需要

上位机开发中日志是最容易被忽略的,直到你遇上一个“偶尔发生、但抓不住现场”的Bug。我的做法是写一个极简的日志工具,按天滚动文件,单文件超过5MB就重命名成.log.1再新建。

日志分四级:Debug、Info、Warn、Error。日常运行只写Info以上,调试模式才开Debug。特别要提的是“协议帧日志”——我把串口收发的原始十六进制帧也落进日志,但只在调试开关打开时记录。这个功能在接线不稳、设备跑飞、协议对不上的时候堪称救命稻草。

帧日志的格式大概是:

[2025-06-01 21:03:12.458] RX: A5 5A 10 03 00 01 D2 15 F5 9B [2025-06-01 21:03:13.012] TX: A5 5A 02 04 00 12 34 56 78 C4 2A

出现问题时,不用用户在设备上测半天,直接把日志发回来,一个十六进制序列就能定位是设备没发、发错、还是上位机解析错。

4.3 固件升级:上位机承担设备更新入口

把固件升级放进上位机,是Status Deck从“开发板”走向“产品”的一步。用户不需要拆壳烧录,打开软件点一下就能更新固件。

我的升级流程参考了XMODEM的思路,简化成五步:

  1. 上位机发送0x30开始升级命令,数据段包含固件总字节数和总包数。
  2. 设备端应答后进入Bootloader模式,关闭业务逻辑、擦写应用区。
  3. 上位机按128字节分包循环发送0x31数据包,每包带包序号。
  4. 设备每收一包回一个ACK,收包出错回NAK;上位机收到NAK或超时未收到ACK就重发,连续3次失败则终止升级。
  5. 全部发完后发送0x32结束命令,附带全文件CRC32;设备校验通过后重启进入新固件。

这里有几个非常关键的细节。第一,升级期间用户不能拔串口或断电,否则设备变砖的概率很高;因此我做完开始升级后会在界面上锁定关闭按钮。第二,上位机发送每个数据包间隔要有短暂延时,避免设备端Flash擦写来不及处理,流控做得不好会引发一连串NAK。第三,升级开始前把当前设备配置读取出来并保存一份副本,升级完成后自动重连并恢复配置,体验会好很多。

固件文件本身用拖拽方式放进上位机,我在拖放区域做了文件类型过滤,只接受.bin,读取后立即校验文件头魔数,防止用户选错文件。

5. 真实调试实录:常见问题和避坑清单

5.1 粘包、半包与设备启动垃圾数据

第一次联调时,上位机一打开就疯狂收到乱码帧,程序直接卡死。查了半天发现罪魁祸首是设备上电瞬间,串口芯片和主控还在初始化,会吐出一串无意义的0xFF和中断字符。

这个问题的解决靠两件事:一是状态机本来就天然过滤非法字节,非帧头一律忽略;二是解析器对“最大帧长度”做了上限,超过就丢弃并重置。从那以后我再也不怕设备上电抽风,随便它先吐多少垃圾。

粘包问题则出现在设备连续上报两条传感器数据时,上位机偶尔只显示最新一条,偶尔显示一条错误数据。检查日志才发现,接收事件一次读回了多帧,旧代码只取了一帧就返回,剩下的字节留在缓冲区跟下一条数据混在一起。改成while循环解析后问题消失。

5.2 COM 口漂移与权限问题

USB转串口的COM口漂移是所有串口工具的痛点。你把设备插在USB3.0口和USB2.0口上,Windows分配的COM号可能不一样;重启一次键盘上的某个设备占了COM3,你的设备就变COM4了。

做上位机时我给“选择端口”这步加了一个辅助提示:不仅列出COM编号,还显示设备描述符(通过Win32 API读取),比如“USB-SERIAL CH340 (COM5)”。同时设备应答的0x81信息里带了设备序列号,上位机连接后会校验这个序列号,不对就警告“端口选择可能错误”。这样即使COM口换了,用户也能快速找到正确的那个。

权限问题也有一次典型踩坑:设备串口被另一个程序占着,或者用户用非管理员账号运行但串口驱动有权限限制,Open()会直接拒绝。我的处理是把所有串口异常统一捕获,并让主界面保持可用,而不是弹出未处理异常框。

5.3 高 DPI 与界面显示模糊

WinForms在高DPI显示器上默认会模糊,这在开发机上不明显,换到用户的4K屏上就非常明显。解决办法:在app.manifest里声明PerMonitorV2,并在程序入口调用SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)。

[DllImport("user32.dll")] private static extern bool SetProcessDpiAwarenessContext(IntPtr value); SetProcessDpiAwarenessContext((IntPtr)(-4)); // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2

加完之后还有一个连带问题:表格、分组框的字体大小在不同DPI下可能错位,我在UI布局时强制统一使用固定的布局边距,并给关键控件设置了AutoSize,实测下来缩放基本正常。

5.4 断线重连与异常恢复策略

设备在运行过程中被拔掉USB线,或者设备端看门狗重启,上位机的表现决定了这工具“像不像正经产品”。

我的处理分三层。第一,设备端每500ms发一次心跳,超过3个周期没收到就判定设备离线;第二,上位机发现离线后自动进入重连循环,每2秒尝试重新打开串口,同时UI显示“设备离线,正在重连”状态,不弹模态框打扰用户;第三,重连成功后自动重新下发一次配置快照,让设备恢复到断开前的显示状态。

这个重连策略在稳定性测试里跑了几千次,只有一个问题需要注意:重试打开串口的操作不能放在UI线程,否则每2秒卡一次界面,我是用System.Threading.Timer做后台重试。

5.5 常见问题速查表

问题现象可能原因排查/解决方法
串口打开失败端口被占用、驱动异常、权限不足关闭占用程序;检查设备管理器;以管理员身份运行
收不到设备数据波特率不一致、接错引脚、线缆不良用串口工具确认基础通信;核对波特率和校验位
数据乱码波特率不一致、地线接触不良固定115200,检查线缆接线
偶发解析错误校验缺失、半包处理不当协议层启用CRC16;解析器状态机完整实现
界面卡顿高频Invoke刷新UI改为定时器聚合刷新,100ms间隔
界面模糊未做DPI适配manifest声明PerMonitorV2并调用API
COM口漂移USB口变更、驱动程序分配变化显示设备描述符;连接后校验设备序列号
升级失败中断供电不稳、串口被拔、包序错乱禁止升级中断电与拔线;每包ACK+超时重传

调试期我还有一个很顺手的技巧:用虚拟串口工具成对创建COM口,上位机连一端,另一端跑一个模拟设备脚本,自动化造帧压测解析和重连逻辑。这样不等硬件到位就能把上位机软件测到八成稳定,真正联调时省了很多时间。

Status Deck这个项目的上位机部分,我从开工到稳定运行,最大的感受是“协议先行比编码先行重要得多”。协议定了,上位机和固件就是两条并行线,各自照着同一份“合同”开发,最后接起来几乎不用改。第二个感受是日志必须从一开始就写,尤其是帧日志,很多问题没有日志根本无从下手排查。第三个感受是UI一定不要跟数据收发纠缠在一起,缓存加定时器刷新这套模式,看着笨,但确实怎么压都不出问题。

如果你也在写自己的上位机,我建议你也先花两天时间把帧格式、命令字、异常流程写在纸上,再碰IDE。后面即使换语言、换平台,这套思路都能原样搬过去。

返回列表