从零写一个工控多协议通讯库(一):架构与取舍
上一篇把 Modbus 一帧报文逐字节拆开讲了。这一篇往上走一层:与其每个项目都重写一遍 Modbus/西门子/三菱的收发,不如把它做成一套能长期用、能跑嵌入式、还能给上位机绑定的库。这是这个系列的第一篇,先把架构和取舍说清楚——协议实现的细节留给后面每一篇。
项目代号 talkplc。纯净室开发,只依据公开协议规范,个人时间与设备。
为什么又要造一个通讯库
工控现场的协议是一堆的:Modbus RTU/TCP、西门子 S7、三菱 MC、欧姆龙 FINS/Hostlink、CODESYS…… 每接一个新设备、每起一个新项目,都在重复写”打开串口 → 拼一帧 → 算校验 → 收一帧 → 解出寄存器”。
已有的轮子里,HslCommunication 很成熟,但主要面向 .NET。我想要的不太一样:
- 以 C 为核心,能直接跑在嵌入式 Linux / 网关上,不背运行时;
- 对外暴露干净的 C API,方便再包一层给 C# / Python 做上位机;
- 模块解耦,用哪层链哪层,别一上来就拖一坨依赖。
对我个人,它同时是三样东西:一个开源作品集、这个博客的选题引擎(每实现一个协议写一篇),以及一个长期想做下去的方向。
一开始就定死的几条原则
- 纯 C11 内核 + 干净 C API——最大可移植性,绑定友好。
- 传输与协议分离——协议层不关心底下是串口、TCP 还是内存模拟。
- 每层单独成库、可单独链接——只想要 Modbus 帧逻辑?那就只链协议模块,不拖串口。
- 没有硬件也能测——内置一个内存里的从站模拟,帧逻辑不靠真设备就能验证。
架构总览
分层大概长这样,依赖方向自上而下、绝不反向:
1 | 应用 / 上位机 / 绑定 (C#、Python…) |
每层的职责:
| 模块 | 干什么 | 依赖 |
|---|---|---|
core |
状态码、传输接口、收发帧回调(trace) |
— |
serial |
串口 I/O(Windows / POSIX),以后同法加 TCP | core |
modbus |
Modbus 帧的拼装 / 解析 / 校验,不关心串口 | core |
datatype |
寄存器 ↔ 数据类型(u16/i16/u32/i32/f32/bool + 字序) | — |
driver |
点位模型 + 轮询 + 批量读 | modbus, datatype |
poll |
把驱动跑在后台线程上,对外给线程安全快照 | driver |
gui |
工业风监视界面(Qt) | poll |
下面挑三个最关键的设计点讲讲——只讲思路,代码点到为止。
设计一:传输层是一个”字节管道”
整套库的地基,是把”底层怎么收发字节”抽象成一个接口。协议层只认这个接口,永远不知道自己是在串口上、TCP 上,还是在一段内存里:
1 | /* 一个"字节管道"。串口 / TCP / 内存模拟都实现它就行。 */ |
就这么几行,换来两个大好处:
- 以后加 Modbus TCP,协议帧逻辑一行不用改,只写一个新的传输后端;
- 测试时塞一个内存里的假从站进去,不用真设备就能把收发全流程跑通。
设计二:协议层只管”一帧”
Modbus RTU 的一帧,结构很朴素(细节见上一篇拆解):
1 | ┌────────┬────────┬──────────────┬────────┬────────┐ |
校验用的是 CRC-16/MODBUS,公开标准算法,多项式 0xA001:
1 | uint16_t crc16(const uint8_t *p, size_t n) { |
小验证:对 ASCII 串
"123456789"算出来应是0x4B37——这是 CRC-16/MODBUS 的标准校验值,拿它当单元测试的”已知答案”很省心。
协议层还做了一件对界面很有用的事:每收发一帧,就通过一个回调把原始字节抛给上层。上层想怎么显示(HEX / ASCII)、要不要记录,都随意——这就是监视界面里”收发帧”那一栏的数据来源。
设计三:点位模型 + 后台线程
再往上,”寄存器地址 + 数据类型”被抽象成点位(tag)。你只描述”我要读哪个地址、当成什么类型”,轮询之后值就自动填好:
1 | typedef struct { |
这一层还顺手做了两件事:
- 批量读:把同一区域里地址连续的点位,合并成一次 Modbus 请求,而不是一个点位发一帧——少一半以上的往返;
- 并发放在 C 库里:轮询跑在一个后台线程上(C11
<threads.h>那套 API),界面只去读一份线程安全的快照。于是就算某个从站不应答、读超时卡住,界面照样丝滑,不会连”断开”都点不动。
值得一提的是并发没有塞进界面框架里——它在纯 C 层。这样哪天不用这个界面、换别的前端,甚至做成命令行守护进程,后台采集这套照样能用。
配套:一个工业风监视界面
光有库不够直观,所以配了个 Qt 写的监视器:左边选协议,右边配串口、列采集点位、看实时值(支持不同数据类型显示),底下是收发帧监视,可在 HEX / ASCII 之间切。

界面只依赖最上面的 poll 那层,通过线程安全快照拿数据——它对底下协议一无所知,以后加了新协议,界面几乎不用动。
反过来,界面本身也是可换的。它只和纯 C 的 poll 层打交道,所以 Qt 只是”其中一种前端”:桌面 / 上位机拿 Qt 写省事;而同一套 C 内核,也能驱动一个 LVGL 写的界面,直接跑在嵌入式 HMI、触摸屏上。正因为核心是纯 C、不背运行时,这条”下沉到设备端”的路才走得通——这也是当初咬定纯 C 的原因之一。
接下来
这个系列会一个协议一篇往下写:
- Modbus TCP(复用现成的帧逻辑,换个传输后端 + MBAP 头)
- 西门子 S7、三菱 MC、欧姆龙 FINS/Hostlink……
界面也不只一种:桌面 / 上位机继续用 Qt,嵌入式 HMI 用 LVGL,两者共用同一套纯 C 内核——协议、驱动、后台采集一行都不用改。
每加一层,都会回来对照这张架构图看看——如果新协议逼着我改了地基,那多半是当初某个抽象没做对。这也是我留着这套分层、公开写出来的原因之一:架构是会被新需求反复拷问的。
下一篇,Modbus TCP。