1. CODESYS串口通信基础与MODBUS协议解析
在工业自动化领域,CODESYS作为IEC 61131-3标准的编程环境,其串口通信能力是设备间数据交互的重要桥梁。最近在给某包装产线升级控制系统时,发现老式称重仪表仅支持MODBUS RTU协议,这让我不得不深入研究了CODESYS下的串口通信实现方案。
MODBUS RTU协议采用主从架构,其数据帧结构包含四个关键部分:
code复制[设备地址][功能码][数据域][CRC校验]
其中CRC16-MODBUS校验是整个通信可靠性的基石。曾遇到过因电磁干扰导致数据位翻转的情况,如果没有严格的CRC校验,错误数据将直接进入控制系统引发误动作。
1.1 MODBUS CRC16校验算法特性
MODBUS使用的CRC16校验有独特的参数配置,这些参数直接影响校验结果:
| 参数 | 值 | 技术说明 |
|---|---|---|
| 多项式 | 0x8005 | 对应x¹⁶ + x¹⁵ + x² + 1多项式 |
| 初始值 | 0xFFFF | 计算前CRC寄存器的初始化值 |
| 输入反转 | True | 每个字节的位顺序需要反转 |
| 输出反转 | True | 最终结果的高低位字节需要互换 |
| 结果异或值 | 0x0000 | 计算完成后不与任何值进行异或操作 |
现场经验:实际计算时使用的多项式是0xA001(0x8005的位反转),这个细节在CODESYS实现时需要特别注意。某次调试中因误用0x8005导致校验失败,排查了整整两天才发现这个隐蔽问题。
2. CODESYS中的CRC16-MODBUS实现
2.1 直接计算法实现
在资源受限的PLC环境中,通常采用直接计算法。以下是CODESYS ST语言实现:
pascal复制FUNCTION CRC16_MODBUS : WORD
VAR_INPUT
pData : POINTER TO BYTE;
nLength : UINT;
END_VAR
VAR
crc : WORD := 16#FFFF;
i : UINT;
j : UINT;
polynomial : WORD := 16#A001;
temp : WORD;
END_VAR
FOR i := 0 TO nLength-1 DO
crc := crc XOR WORD(pData^);
pData := pData + 1;
FOR j := 0 TO 7 DO
temp := crc AND 1;
crc := SHR(crc, 1);
IF temp <> 0 THEN
crc := crc XOR polynomial;
END_IF
END_FOR
END_FOR
// 交换高低字节
CRC16_MODBUS := WORD_TO_BYTE(crc, 1) * 256 + WORD_TO_BYTE(crc, 0);
关键点说明:
- 使用指针遍历数据字节,避免频繁数组拷贝
- 内层循环处理每个bit,根据LSB决定是否异或多项式
- 最终结果需进行字节序转换(MODBUS采用小端格式)
2.2 查表法优化
对于高频通信场景,可采用查表法提升性能。首先在全局变量中预计算CRC表:
pascal复制VAR_GLOBAL CONSTANT
CRC16_TABLE : ARRAY[0..255] OF WORD := [
16#0000, 16#C0C1, 16#C181, 16#0140, ..., 16#8201, 16#42C0];
END_VAR
查表法实现函数:
pascal复制FUNCTION CRC16_MODBUS_TABLE : WORD
VAR_INPUT
pData : POINTER TO BYTE;
nLength : UINT;
END_VAR
VAR
crc : WORD := 16#FFFF;
i : UINT;
index : BYTE;
END_VAR
FOR i := 0 TO nLength-1 DO
index := BYTE(crc) XOR pData^;
pData := pData + 1;
crc := SHR(crc, 8) XOR CRC16_TABLE[index];
END_FOR
CRC16_MODBUS_TABLE := WORD_TO_BYTE(crc, 1) * 256 + WORD_TO_BYTE(crc, 0);
性能对比:在某食品灌装产线测试中,处理100字节数据帧时,查表法耗时仅0.12ms,而直接计算法需要2.3ms,性能提升近20倍。
3. 串口数据格式转换实战
3.1 基本串口配置
在CODESYS中配置串口通信需要设置以下参数(以RS485为例):
pascal复制PROGRAM MAIN
VAR
hCom : RTS_IEC_HANDLE;
stParams : COM_PARAM := (
baudrate := 9600,
databits := 8,
parity := 2, // 1=奇校验 2=无校验 3=偶校验
stopbits := 0, // 0=1位停止位
flowcontrol := 0
);
END_VAR
hCom := COM_Open(port := 'COM1', pParam := ADR(stParams));
IF hCom = RTS_INVALID_HANDLE THEN
// 错误处理
END_IF
3.2 数据帧封装与解析
完整MODBUS RTU通信示例:
pascal复制FUNCTION BuildReadHoldingRegisters : ARRAY[0..7] OF BYTE
VAR_INPUT
nSlaveAddr : BYTE;
nRegAddr : WORD;
nRegCount : WORD;
END_VAR
VAR
aFrame : ARRAY[0..7] OF BYTE;
wCRC : WORD;
END_VAR
// 构建PDU
aFrame[0] := nSlaveAddr;
aFrame[1] := 3; // 功能码:读保持寄存器
aFrame[2] := WORD_TO_BYTE(nRegAddr, 1);
aFrame[3] := WORD_TO_BYTE(nRegAddr, 0);
aFrame[4] := WORD_TO_BYTE(nRegCount, 1);
aFrame[5] := WORD_TO_BYTE(nRegCount, 0);
// 计算CRC
wCRC := CRC16_MODBUS_TABLE(ADR(aFrame), 6);
aFrame[6] := WORD_TO_BYTE(wCRC, 0); // 低位在前
aFrame[7] := WORD_TO_BYTE(wCRC, 1);
BuildReadHoldingRegisters := aFrame;
响应帧解析函数:
pascal复制FUNCTION ParseModbusResponse : BOOL
VAR_INPUT
pData : POINTER TO BYTE;
nLength : UINT;
pnSlaveAddr : POINTER TO BYTE;
pnFuncCode : POINTER TO BYTE;
pRegValues : POINTER TO WORD;
pnRegCount : POINTER TO UINT;
END_VAR
VAR
wCalcCRC : WORD;
wRecvCRC : WORD;
i : UINT;
END_VAR
// 检查最小长度
IF nLength < 5 THEN RETURN FALSE; END_IF
// 校验CRC
wRecvCRC := WORD(pData[nLength-2]) OR (WORD(pData[nLength-1]) << 8);
wCalcCRC := CRC16_MODBUS_TABLE(pData, nLength-2);
IF wRecvCRC <> wCalcCRC THEN RETURN FALSE; END_IF
// 解析字段
pnSlaveAddr^ := pData[0];
pnFuncCode^ := pData[1];
// 处理读寄存器响应
IF pData[1] = 3 THEN
pnRegCount^ := pData[2] / 2;
FOR i := 0 TO pnRegCount^-1 DO
pRegValues[i] := WORD(pData[3+i*2]) << 8 OR WORD(pData[4+i*2]);
END_FOR
END_IF
RETURN TRUE;
3.3 数据类型转换技巧
工业设备常用数据格式处理:
pascal复制// 32位浮点数转换(大端序)
FUNCTION BytesToFloat : REAL
VAR_INPUT
pData : POINTER TO BYTE;
END_VAR
VAR
dwTemp : DWORD;
fValue : REAL;
END_VAR
dwTemp := SHL(UNION(pData[0], DWORD), 24) OR
SHL(UNION(pData[1], DWORD), 16) OR
SHL(UNION(pData[2], DWORD), 8) OR
UNION(pData[3], DWORD);
MEMCPY(ADR(fValue), ADR(dwTemp), SIZEOF(REAL));
BytesToFloat := fValue;
4. 工业现场问题排查指南
4.1 典型故障分析表
| 故障现象 | 可能原因 | 解决方案 |
|---|---|---|
| 通信超时无响应 | 1. 物理线路断路 2. 波特率不匹配 3. 从站地址错误 |
1. 检查接线与终端电阻 2. 确认主从设备波特率一致 3. 使用调试工具扫描从站地址 |
| CRC校验持续失败 | 1. 字节序错误 2. 多项式使用错误 3. 数据域长度错误 |
1. 确认CRC高低字节顺序 2. 检查是否使用0xA001多项式 3. 验证数据域长度与功能码匹配 |
| 数据偶发性错误 | 1. 电磁干扰 2. 接地不良 3. 电源波动 |
1. 改用屏蔽双绞线 2. 检查接地电阻<4Ω 3. 加装电源滤波器 |
| 通信速率突然下降 | 1. 线路阻抗增大 2. 节点数超限 3. 串口缓冲区溢出 |
1. 检查连接器氧化情况 2. RS485总线节点不超过32个 3. 优化查询间隔增加超时处理 |
4.2 调试工具开发建议
在CODESYS中构建简易协议分析器:
pascal复制FUNCTION AnalyzeModbusFrame
VAR_INPUT
pFrame : POINTER TO BYTE;
nLength : UINT;
sTitle : STRING;
END_VAR
VAR
sOutput : STRING;
i : UINT;
END_VAR
sOutput := CONCAT('==== ', sTitle, ' ====\n');
sOutput := CONCAT(sOutput, 'Length: ', UINT_TO_STRING(nLength), ' bytes\n');
sOutput := CONCAT(sOutput, 'Hex Data: ');
FOR i := 0 TO nLength-1 DO
sOutput := CONCAT(sOutput, BYTE_TO_HEXSTR(pFrame[i]), ' ');
END_FOR
// 解析基本字段
IF nLength >= 2 THEN
sOutput := CONCAT(sOutput, '\nSlave Address: ', BYTE_TO_STRING(pFrame[0]));
sOutput := CONCAT(sOutput, '\nFunction Code: ', BYTE_TO_STRING(pFrame[1]));
END_IF
// CRC校验
IF nLength >= 4 THEN
sOutput := CONCAT(sOutput, '\nCRC Received: ',
BYTE_TO_HEXSTR(pFrame[nLength-2]), ' ',
BYTE_TO_HEXSTR(pFrame[nLength-1]));
sOutput := CONCAT(sOutput, '\nCRC Calculated: ',
BYTE_TO_HEXSTR(WORD_TO_BYTE(CRC16_MODBUS_TABLE(pFrame, nLength-2), 0)), ' ',
BYTE_TO_HEXSTR(WORD_TO_BYTE(CRC16_MODBUS_TABLE(pFrame, nLength-2), 1)));
END_IF
// 输出到调试界面
DebugPrint(sOutput);
4.3 性能优化实践
在物流分拣系统项目中,通过以下优化将通信效率提升300%:
- 批量读取优化:
pascal复制// 原单寄存器读取
aFrame := BuildReadHoldingRegisters(1, 40001, 1);
// 优化为多寄存器读取
aFrame := BuildReadHoldingRegisters(1, 40001, 10);
- 查询间隔动态调整:
pascal复制// 根据设备响应时间自动调整轮询间隔
IF nResponseTime < 50 THEN
nPollInterval := 100; // ms
ELSIF nResponseTime < 200 THEN
nPollInterval := 300;
ELSE
nPollInterval := 1000;
END_IF
- 错误处理强化:
pascal复制// 带自动恢复的通信重试机制
FOR i := 1 TO 3 DO
nResult := COM_Write(hCom, ADR(aRequest), SIZEOF(aRequest));
IF nResult > 0 THEN
nResult := COM_Read(hCom, ADR(aResponse), SIZEOF(aResponse), 500);
IF ParseModbusResponse(ADR(aResponse), nResult, ...) THEN
BREAK;
END_IF
END_IF
DELAY(100 * i);
END_FOR
5. 进阶应用:自定义协议扩展
5.1 混合数据格式处理
某纺织机械项目需要同时处理整数、浮点和ASCII码:
pascal复制FUNCTION ParseMixedData
VAR_INPUT
pData : POINTER TO BYTE;
nType : UINT; // 1=INT16, 2=UINT32, 3=FLOAT, 4=ASCII
nLength : UINT;
END_VAR
VAR
i : UINT;
sResult : STRING;
BEGIN
CASE nType OF
1: // INT16
RETURN INT_TO_STRING(SHL(pData[0], 8) OR pData[1]);
2: // UINT32
RETURN UDINT_TO_STRING(SHL(pData[0], 24) OR SHL(pData[1], 16)
OR SHL(pData[2], 8) OR pData[3]);
3: // FLOAT
RETURN REAL_TO_STRING(BytesToFloat(pData));
4: // ASCII
FOR i := 0 TO nLength-1 DO
sResult := CONCAT(sResult, BYTE_TO_CHAR(pData[i]));
END_FOR
RETURN sResult;
ELSE
RETURN 'UNKNOWN_TYPE';
END_CASE
END_FUNCTION
5.2 通信安全增强
为满足制药行业GMP规范,增加帧序列号校验:
pascal复制// 发送端添加序列号
aFrame[2] := nSequenceID;
wCRC := CRC16_MODBUS_TABLE(ADR(aFrame), 6);
aFrame[6] := WORD_TO_BYTE(wCRC, 0);
aFrame[7] := WORD_TO_BYTE(wCRC, 1);
// 接收端验证
IF aResponse[2] <> (nLastSequence + 1) MOD 256 THEN
nSecurityErrorCnt := nSecurityErrorCnt + 1;
RETURN FALSE;
END_IF
nLastSequence := aResponse[2];
5.3 通信质量监控
实时统计通信指标:
pascal复制TYPE CommStats :
STRUCT
nTotalFrames : UDINT;
nErrorFrames : UDINT;
nMaxResponseTime : UINT;
nMinResponseTime : UINT;
fAvgResponseTime : REAL;
END_STRUCT
END_TYPE
VAR_GLOBAL
stCommStats : CommStats;
nResponseTimeSum : UDINT;
END_VAR
// 更新统计信息
stCommStats.nTotalFrames := stCommStats.nTotalFrames + 1;
IF bError THEN
stCommStats.nErrorFrames := stCommStats.nErrorFrames + 1;
ELSE
nResponseTimeSum := nResponseTimeSum + nCurrentResponseTime;
stCommStats.fAvgResponseTime := UDINT_TO_REAL(nResponseTimeSum) /
(stCommStats.nTotalFrames - stCommStats.nErrorFrames);
IF nCurrentResponseTime > stCommStats.nMaxResponseTime THEN
stCommStats.nMaxResponseTime := nCurrentResponseTime;
END_IF
// 类似更新MinResponseTime...
END_IF
