简介:本资源是面向工业自动化与大型科学实验装置控制领域的EPICS Modbus通信模块,专为需将PLC等现场设备接入EPICS系统的工程师及科研人员设计,解决高可靠性协议适配与异构设备集成难题。压缩包共196个文件,涵盖29个EPICS数据库模板(.template)、14个图形化界面文件(.ui/.edl/.adl/.bob/.opi)、14个启动脚本(.cmd)、13个设备配置替换文件(.substitutions)及4个Modbus专用驱动源码(.cpp/.h),辅以文档(.rst/.pdf)、构建脚本(Makefile)和配置文件(.yml/.pro),整体1.28MB,结构完整、开箱即用。已有102人学习下载,资源提供从底层驱动到上层IOC集成的全链路支持,包含Koyo系列PLC测试界面、Modbus数组/字符串/同步IO等典型用例,以及统计监控、故障诊断等实用功能模块,可直接用于加速粒子加速器、工业过程监控等场景的EPICS系统部署与调试。
1. EPICS Modbus 模块:不是“又一个驱动”,而是工业现场数据接入的确定性出口
你手头有一台西门子 S7-1200 PLC,寄存器地址已标好,Modbus TCP 端口设为 502;旁边还连着一台老式欧姆龙 CJ2M,只支持 RS485 + Modbus RTU;产线上还有几台温控仪,协议文档里写着“ASCII mode only”。你用 Python 写了个脚本轮询,但一到高并发就丢包;用 Modbus Poll 测试通了,可上 EPICS IOC 后 IOC 启动失败、log 里满屏modbus: timeout on unit 1;更糟的是,同一台设备在 LabVIEW 里读得稳,在 EPICS 里却偶发乱码——这不是配置问题,是底层链路抽象没对齐。这个.zip包里的 EPICS Modbus 模块,就是专治这类“多协议混搭、时序敏感、现场不可停机”的硬骨头:它不依赖第三方 Python 库或 Java 中间件,而是以 EPICS 原生 Record + Device Support + Port Driver 三层架构,把 TCP、RTU、ASCII 三种物理链路统一收束到asyn驱动层,再通过标准ai,ao,bi,bo等 record 类型暴露给上层。它不是让你“能连上”,而是让你“连得准、断得清、查得明”——比如 RTU 链路上的帧间隙(T1.5/T3.5)可精确到微秒级配置,TCP 连接异常时自动触发reconnectDelay并保留 last-known-good 值,ASCII 模式下自动处理回车换行与空格填充。适合正在做国产 DCS 替代、老旧产线数采升级、或 EPICS 控制系统集成的现场工程师,尤其当你已经试过pymodbus、libmodbus、甚至modbus4j却卡在“现场复现难、日志无指向、重启即失效”这三道坎上时,这个模块就是你该拆开的第一份源码。
2. 模块结构与核心机制:为什么它能同时扛住 TCP 闪断、RTU 电平抖动和 ASCII 字符错位
2.1 源码包解压后的真实目录结构与关键文件职责
解压.zip后你会看到典型 EPICS 模块布局:
modbus/ ├── configure/ # EPICS 构建系统配置(RELEASE, CONFIG_SITE) ├── src/ # 核心实现 │ ├── modbusAsynDriver.cpp # asyn 驱动主入口:管理连接池、超时、重连、链路状态机 │ ├── modbusSerialPort.cpp # RTU/ASCII 串口专用:封装 termios、处理 T1.5/T3.5 间隔、校验字节生成 │ ├── modbusTcpPort.cpp # TCP 专用:非阻塞 socket、keepalive 设置、粘包/半包处理 │ ├── modbusAsciiParser.cpp # ASCII 模式解析器:跳过前导空格、识别 ':' 起始符、校验和验证(LRC/ASCII) │ └── modbusRecordSupport.c # record 层胶水:将 asyn 的 int32/double/enum 映射到 ai/ao/bi/bo 的 VAL、RVAL 字段 ├── db/ # 示例数据库模板(.db 文件) │ ├── modbusExample.db # 含 TCP/RTU/ASCII 三类 device 的完整 record 实例 │ └── modbusProtocol.db # 协议级参数 record(如 modbusUnitID、modbusFunctionCode) └── Makefile # EPICS make system 入口提示:不要直接改
modbusAsynDriver.cpp—— 它是状态机中枢,90% 的现场问题出在modbusSerialPort.cpp的calculateT35()或modbusAsciiParser.cpp的parseAsciiResponse()。先看懂这两处,再动其他。
2.2 三层抽象:从物理链路到 EPICS record 的映射逻辑
EPICS Modbus 模块的健壮性,源于它把协议栈彻底分层:
| 抽象层 | 职责 | 关键参数(运行时可调) | 影响范围 |
|---|---|---|---|
Port Driver 层(modbusTcpPort/modbusSerialPort) | 处理物理连接建立、帧收发、链路保活 | tcpTimeout=3000(ms)、serialBaudRate=19200、serialParity=none、asciiTerminator="\r\n" | 决定“能不能通”,影响asyn状态asynSuccess/asynTimeout |
Device Support 层(modbusRecordSupport.c) | 将 Modbus 功能码(0x03/0x06/0x10)转为 EPICS record 的read/write操作 | modbusFunctionCode=3(读保持寄存器)、modbusStartAddress=40001(PLC 地址偏移)、modbusDataSize=2(字数) | 决定“读哪、写哪、怎么解包”,错误会导致VAL字段为 0 或乱码 |
Record 层(.db文件中ai/ao/bi/bo) | 提供标准 EPICS 接口,对接scan、calc、sub等 record | SCAN="1 second"、DTYP="modbus"、INP="@modbusTCP1 40001"、EGU="°C" | 决定“怎么用”,错误配置会导致 IOC 启动报undefined device type |
这种分层意味着:
- 当 TCP 链路闪断时,
modbusTcpPort层会记录asynError并触发重连,而airecord 的VAL仍保持上次有效值(由asyn的asynParamStatus机制保证),不会突变为 0; - 当 RTU 电平受干扰导致某帧 CRC 错误时,
modbusSerialPort会丢弃该帧并重发请求,而非让错误数据污染 record; - 当 ASCII 设备返回
"000123\r\n"时,modbusAsciiParser会自动截掉\r\n并转为整数 123,无需你在.db里写LINR=LINEAR或ESLO/EOFF补偿。
2.3 TCP/RTU/ASCII 三链路的初始化差异与共性
虽然同属 Modbus,但三种链路在 EPICS 初始化时行为迥异,必须按物理特性配置:
# TCP 链路:启动 IOC 前需确保目标 IP 可达,且防火墙放行 502 端口 # 在 iocBoot/iocXXX/st.cmd 中: epicsEnvSet("EPICS_CA_ADDR_LIST", "192.168.1.100") dbLoadDatabase("$(MODBUS)/db/modbusProtocol.db", "modbusTCP1", "192.168.1.100:502") dbLoadRecords("$(MODBUS)/db/modbusExample.db", "PORT=modbusTCP1,ADDR=40001,TYPE=ai")# RTU 链路:必须指定串口设备路径、波特率、停止位等,且需 root 权限访问 /dev/ttyS* # st.cmd 中: # 注意:Linux 下 /dev/ttyS* 权限需提前设置(chmod 666 /dev/ttyS0) dbLoadDatabase("$(MODBUS)/db/modbusProtocol.db", "modbusRTU1", "/dev/ttyS0") dbLoadRecords("$(MODBUS)/db/modbusExample.db", "PORT=modbusRTU1,ADDR=40001,TYPE=ai,BAUD=19200,PARITY=none,STOPBITS=1")# ASCII 链路:需显式声明终止符,并容忍设备返回的空格填充(如 "000123 ") # st.cmd 中: dbLoadDatabase("$(MODBUS)/db/modbusProtocol.db", "modbusASCII1", "/dev/ttyUSB0") dbLoadRecords("$(MODICS)/db/modbusExample.db", "PORT=modbusASCII1,ADDR=40001,TYPE=ai,TERMINATOR=\"\\r\\n\",ASCII_SPACES=1")注意:
modbusProtocol.db是协议参数库,它定义了modbusTCP1这个 port 名对应的 IP+端口,或modbusRTU1对应的串口路径。不能省略dbLoadDatabase这一步——否则dbLoadRecords会报port not found,这是新手最常翻车的点。
3. 编译与 IOC 集成:从源码到可运行 IOC 的六步实操
3.1 环境准备:EPICS Base 版本与依赖确认
该模块要求 EPICS Base ≥ 3.15.6(因使用asyn4-32+ 的asynCommonSyncIO接口)。确认你的环境:
# 检查 EPICS Base 版本(必须 ≥ 3.15.6) echo $EPICS_BASE ls $EPICS_BASE/version # 检查 asyn 模块是否已编译(modbus 依赖 asyn) ls $EPICS_BASE/require/asyn/lib/linux-x86_64/ | grep asyn # 应看到 asyn.so 和 asynApp.so # 检查编译工具链(gcc ≥ 4.8.5,g++ 必须可用) gcc --version g++ --version提示:若
asyn未安装,先下载asyn源码(推荐 4-39 版本),在$EPICS_BASE/require/下解压,修改asyn/configure/RELEASE中EPICS_BASE路径,然后make。不要跳过这步——modbus 模块的Makefile会链接asyn的.so,缺则编译报undefined reference to asynManager::...。
3.2 模块编译:四条命令走完全部流程
进入modbus/目录,执行:
# 步骤 1:生成 configure/RELEASE(自动检测 EPICS_BASE 和 asyn 路径) make release # 步骤 2:生成 Makefile(关键!必须运行,否则后续 make 失败) make # 步骤 3:编译主模块(生成 libmodbus.so 和 modbusApp.so) make -j4 # 步骤 4:安装到 EPICS 支持模块目录(默认 $EPICS_BASE/require/modbus) make install编译成功后,你会在$EPICS_BASE/require/modbus/下看到:
lib/:含libmodbus.so(动态库)db/:含modbusExample.db等数据库文件include/:含modbus.h(供自定义 record 调用)
参数说明:
make -j4中的4是 CPU 核数,可根据机器调整;若编译报error: ‘asynCommonSyncIO’ has not been declared,说明asyn版本过低,请升级至 4-39+。
3.3 IOC 构建:st.cmd 里三处必改项
新建 IOC 目录(如iocModbus),其st.cmd必须包含以下三处硬编码修改:
# === 第一处:加载 modbus 模块(必须在 dbLoadDatabase 之前)=== cd "$(MODBUS)/" dbLoadDatabase("db/modbusProtocol.db", "modbusTCP1", "192.168.1.100:502") # 注意:此处 "modbusTCP1" 是 port 名,必须与 db 文件中定义一致 # === 第二处:加载 record 数据库(必须指定 PORT 参数)=== dbLoadRecords("db/modbusExample.db", "PORT=modbusTCP1,ADDR=40001,TYPE=ai") # === 第三处:启动扫描(必须启用,否则 record 不刷新)=== # 默认 scan rate 是 "1 second",可改为 "100 millisecond" 提高频次 # 但注意:PLC 承载能力有限,建议先用 1s 测试 dbpf("$(USER)ai1.SCAN", "1 second") # 启动 IOC iocInit避坑:
dbLoadRecords的第二个参数是macro substitution string,其中PORT=后的值必须与dbLoadDatabase第二个参数(即 port 名)完全一致。大小写敏感!若写成PORT=ModbusTCP1,IOC 启动时会静默失败,caget $(USER)ai1.VAL返回CA_DISCONNECTED。
3.4 验证连通性:用 EPICS 原生命令逐层排查
不要急着caget,按层验证:
# 层 1:检查 asyn port 是否注册成功 cainfo asyn # 应看到类似:asyn:modbusTCP1 (type=asynOctet, status=connected) # 层 2:检查 modbus port 状态(关键!) caget modbusTCP1:status # 正常返回:connected(TCP) / opened(RTU) / ascii_ready(ASCII) # 层 3:检查 record 是否绑定成功 caget $(USER)ai1.DTYP # 应返回:modbus(若为 "" 则 record 未正确加载) # 层 4:强制触发一次读取(绕过 scan) caput $(USER)ai1.PROC 1 caget $(USER)ai1.VAL # 若返回数值(如 25.6),说明链路通;若返回 0 或超时,进入下一节排查注意:
caput $(USER)ai1.PROC 1是 EPICS record 的“手动执行”指令,它会强制调用modbusRecordSupport.c中的read函数,比等SCAN更快定位问题。
4. 避坑指南:现场踩过的五个真实坑与血泪修复方案
4.1 现象:IOC 启动后caget modbusTCP1:status返回connecting,10 秒后变disconnected
原因:目标 PLC 的 Modbus TCP 服务未开启,或防火墙拦截了 502 端口;更隐蔽的是,某些 PLC(如三菱 FX5U)默认关闭 Modbus TCP,需在 GX Works2 中手动启用。
解决:
- 先用
telnet 192.168.1.100 502测试端口连通性(Linux/macOS)或Test-NetConnection 192.168.1.100 -Port 502(PowerShell); - 若 telnet 通但 EPICS 不通,检查 PLC 的 Modbus TCP 允许 IP 列表(有些 PLC 只允特定网段访问);
- 在
st.cmd中加epicsEnvSet("EPICS_MODBUS_DEBUG", "1"),重启 IOC 查看log文件,搜索connect failed: Connection refused。
4.2 现象:RTU 链路caget modbusRTU1:status显示opened,但caget $(USER)ai1.VAL始终为 0
原因:串口参数不匹配(最常见是PARITY或STOPBITS错),或modbusUnitID与 PLC 设置不符(如 PLC 设为 2,但 record 中INP="@modbusRTU1 2 40001"漏写了 unit ID)。
解决:
- 用
stty -F /dev/ttyS0查看当前串口设置,对比 PLC 手册; - 在
modbusExample.db中确认INP字段格式:@modbusRTU1 <unit_id> <address>,例如@modbusRTU1 1 40001; - 临时改
modbusSerialPort.cpp中debugLevel=2,重新编译,查看 log 中sent frame: ...和recv frame: ...是否长度一致、CRC 正确。
4.3 现象:ASCII 设备返回"000123\r\n",但ai1.VAL显示12300(放大 100 倍)
原因:ASCII 模式下设备返回的是字符串,模块默认按int32解析,但"000123"被当成了十六进制?不,是modbusAsciiParser.cpp的parseAsciiResponse()函数未处理前导零,导致atoi("000123") = 123正确,但 record 的EGU或PREC设置错误引发显示偏差。
解决:
- 检查
modbusExample.db中该 record 的PREC字段(显示精度),若为0则整数显示正常; - 更可能是
LINR=LINEAR且ESLO/EOFF被误设——删掉这两行,让VAL直接显示原始值; - 终极方案:在
modbusAsciiParser.cpp的parseAsciiResponse()末尾加printf("parsed: %d\n", value);,确认解析值是否真为 123。
4.4 现象:TCP 链路偶尔VAL突变为 0,持续 1~2 秒后恢复
原因:网络抖动导致单次读取超时,但asyn默认策略是“超时即清零”,而非保持 last value。
解决:
- 修改
modbusAsynDriver.cpp中defaultTimeoutMs(默认 1000ms),设为3000; - 在
.db文件中为该 record 添加DTYP="modbus"后,增加FLNK="$(USER)ai1.LOLO"并设LOLO=0,但这只是报警; - 正解:启用
asyn的asynParamStatus机制,在st.cmd中加:# 让 record 在 asyn error 时保持 last value dbpf("$(USER)ai1.ASL", "1") # Auto Save Last Value on error
4.5 现象:多个 RTU 设备挂同一 RS485 总线,caget时部分设备响应慢或超时
原因:RS485 是半双工,多设备共享总线时,modbusSerialPort的T3.5间隔(帧间最小间隔)设置过短,导致设备来不及切换收发方向。
解决:
- 在
modbusSerialPort.cpp中找到calculateT35()函数,将T35_US = 3500(微秒)改为5000; - 或更稳妥:在
st.cmd中为每个 RTU port 单独设置T35:# 在 dbLoadDatabase 后加 dbpf("modbusRTU1:T35", "5000") - 玄学经验:实际现场中,
T35设为4000往往比3500稳定得多,因为不同品牌 PLC 的硬件响应延迟有差异。
5. 高级调试技巧:用 Wireshark + EPICS log 双向印证 Modbus 报文
5.1 TCP 报文抓包:Wireshark 过滤与字段对照
当caget返回异常值,先抓包确认是 EPICS 发错,还是 PLC 回错:
- 启动 Wireshark,过滤
tcp.port == 502 && ip.addr == 192.168.1.100; - 在 IOC 中执行
caput $(USER)ai1.PROC 1; - Wireshark 中找到
Modbus Request包,展开Modbus Protocol→Function Code: Read Holding Registers (0x03); - 关键字段对照:
Transaction Identifier: EPICS 自动生成的 2 字节 ID,用于匹配请求/响应;Unit Identifier: 即modbusUnitID,必须与 PLC 设置一致;Starting Address:40001对应0x0000(Modbus 地址从 0 开始,40001 = 0x0000);Quantity of Registers:0x0001(读 1 个寄存器);
- 找到对应
Modbus Response,检查Byte Count是否为0x02(1 个寄存器 = 2 字节),Register Value是否为预期值(如0x0019= 25)。
提示:Wireshark 的 Modbus 解析插件需启用(Edit → Preferences → Protocols → Modbus → Enable)。若看不到
Modbus Protocol字段,说明过滤条件不对或抓包位置错误(必须在 EPICS IOC 所在机器上抓,而非 PLC 侧)。
5.2 RTU/ASCII 报文捕获:用socat透明代理串口
RS485 抓包比 TCP 难,用socat创建虚拟串口对:
# 创建 /dev/ttyV0(虚拟端口)和 /dev/ttyV1(转发端口) socat -d -d pty,link=/dev/ttyV0,raw,echo=0,waitslave,mode=666,group=tty \ pty,link=/dev/ttyV1,raw,echo=0,waitslave,mode=666,group=tty # 将 EPICS 的 st.cmd 中串口路径改为 /dev/ttyV0 # 启动另一个终端,监听 /dev/ttyV1 的原始字节流 hexdump -C /dev/ttyV1此时hexdump会输出十六进制报文,例如:01 03 00 00 00 01 84 0a→ Unit ID=1, FC=0x03, Addr=0x0000, Qty=0x0001, CRC=0x840a01 03 02 00 19 b8 47→ Unit ID=1, FC=0x03, ByteCnt=0x02, Data=0x0019, CRC=0xb847
对比modbusSerialPort.cpp中sendFrame()和recvFrame()的printf日志,即可确认是发送错、接收错、还是 CRC 计算错。
5.3 EPICS log 深度解读:从asyn状态码反推故障点
EPICS log(通常在iocLog目录)是终极证据源。关键日志模式:
| 日志片段 | 含义 | 对应动作 |
|---|---|---|
modbusAsynDriver::connect: connecting to 192.168.1.100:502 | TCP 正在连接 | 检查网络连通性 |
modbusTcpPort::writeRead: timeout after 1000 ms | 单次读取超时 | 增大tcpTimeout或检查 PLC 负载 |
modbusSerialPort::sendFrame: sent 8 bytes: 01 03 00 00 00 01 84 0a | RTU 请求发出 | 对照 Wireshark/socat 确认是否一致 |
modbusAsciiParser::parseAsciiResponse: got '000123\r\n' -> 123 | ASCII 解析成功 | 若值错误,检查设备返回是否真为000123 |
asynManager::queueRequest: error queueing request for port modbusTCP1 | asyn 队列满 | 降低SCAN频率或增加asyn线程数 |
血泪经验:我曾遇到
VAL为 0 但 log 无报错,最后发现是modbusAsynDriver.cpp中maxRetries=3导致连续三次超时后asyn主动断开连接,而 log 只记最后一次。解决方案:在st.cmd中加epicsEnvSet("EPICS_MODBUS_MAX_RETRIES", "1"),让失败更快暴露。
5.4 自定义 record 扩展:为特殊设备添加string类型支持
原模块只支持ai/ao/bi/bo,但某些温控仪返回字符串(如"RUNNING")。要支持,需扩展modbusRecordSupport.c:
// 在 modbusRecordSupport.c 中添加 static long stringRead(struct aiRecord *pai) { // 复用 modbusAsynDriver 的 readInt32,但解析为字符串 char buffer[64]; int status = pasynManager->readOctet(pasynUser, buffer, sizeof(buffer), &nread, &eomReason); if (status == asynSuccess) { // 假设设备返回 ASCII 字符串,如 "RUNNING\0" strncpy(pai->rval, buffer, sizeof(pai->rval)-1); pai->rval[sizeof(pai->rval)-1] = '\0'; return 0; } return -1; } // 在 registerRecordDeviceDriver() 中注册 static long (*readFunctions[])(struct aiRecord *) = { /* ...原有函数... */ stringRead, // 新增 };然后在.db中定义:
record(stringin, "$(USER)status") { field(DTYP, "modbusString") field(INP, "@modbusTCP1 1 40001") field(SCAN, "1 second") }注意:此扩展需重新编译
modbusApp.so,且DTYP名称必须与registerRecordDeviceDriver()中注册名一致。从那以后我每次加新设备类型,都强制走一遍make clean; make,再cainfo确认新DTYP已注册——这步省不得,否则 IOC 启动报unknown DTYP。
希望帮到你。
本文还有配套的精品资源,点击获取