[这是一篇转载的文章]原文地址见文末
什么是CRC(Cyclic Redundancy Check)?
循环冗余校验CRC(Cyclic Redundancy Check)是数据通信领域常用的一种数据传输检错技术。通过在发送端对数据按照某种算法计算出校验码,并将得到的校验码附在数据帧的后面,一起发送到接收端。接收端对收到的数据和校验码按照相同算法进行验证,以此判断接收到的数据是否正确、完整。
CRC简介
我们知道,数据在传输过程中可能会因为传输介质故障或外界的干扰而产生比特差错(使原来的0变为1,原来的1变为0),从而导致接收方接收到错误的数据。为尽量提高接收方收到数据的正确率,在接收数据之前需要对数据进行差错检测,仅当检测的结果为正确时才接收数据。
差错检测的方式有多种,常见的有奇偶校验、求和校验、CRC校验等。他们的工作原理都是发送端对数据按照某种算法计算出来校验码,将校验码和数据一起发送到接收端,然后接收端进行检验确定数据是否发生变化。
CRC是由W. Wesley Peterson在1961年发表的论文中提出,由于CRC校验在速度、成本、正确率等方面比其他校验方式更具有优势,因此,CRC成为计算机信息、通信领域最为普遍的校验方式。例如在标准的以太帧格式中,最后有4个字节长度的冗余位,用于存储CRC校验的值,这个冗余位又常称为帧检验序列FCS(Frame Check Sequence)。
以太帧结构
CRC是如何计算的?
CRC的思想就是先在要发送的K比特长度的数据后面附加一个R比特长度的校验码,然后生成一个新帧发送给接收端。接收端接收到新帧后,根据收到的数据和校验码来验证接收到的数据是否正确。
数据+校验码示例
当然,这个附加的校验码不是随意添加的,要使所生成的新帧能与发送端和接收端共同选定的某个特定数整除(“模2除法”)。接收端把接收到的新帧除以这个选定的除数。因为在发送数据帧之前就已通过附加一个数,做了“去余”处理(也就已经能整除了),所以结果应该是没有余数。如果有余数,则表明该帧在传输过程中出现了差错。
在K比特数据后面再拼接R比特的校验码,整个编码长度为N比特,这种编码也叫(N,K)码。对于一个给定的(N,K)码,可以证明存在一个最高次幂为N-K=R的多项式g(x),根据g(x)可以生成R比特的校验码。其算法是以GF(2)多项式算术为数学基础的,原理如下图所示。
CRC计算公式
g(x)叫做这个校验码的生成多项式。不同的CRC生成多项式,其检错能力是不同的。要使用R位校验码,生成多项式的次幂应为R。以下为常见的一些标准多项式。
常见生成多项式
这些多项式的值便是模2除法的除数。而根据这个除数获得校验码并进行校验的原理可以分为以下几个步骤:
- 发送端、接收端在通信前,约定好除数P,也就是前面说的多项式的值。P应该是R+1位长度;
- 发送端首先在原来的K位数据后面加R个0,相当于原来的数据左移了R位;
- 然后进行模2除法运算(其实就是异或XOR运算),将加0之后的K+R位的数除以P,循环计算,直到余数的阶数小于R,这个余数就是附加的校验码,如果长度不足R位需要在前面加0补齐;
- 发送端将R位校验码附加在原数据后面发送给接收方;
- 接收方接收到数据后,将数据以模2除法方式除以除数P。如果没有余数,说明在传输过程中没有出现错误,否则说明有错误。
下面以一个简单示例来展示CRC的计算过程:
以g(x)为CRC-4=X4+X+1为例,此时除数P=10011。假设源数据M为10110011。
在发送端将M左移4位,然后除以P。
发送端CRC计算示例
计算得到的余数就是0100,也就是CRC校验码。将0100附加到原始数据帧10110011后,组成新帧101100110100发送给接收端。接收端接收到该帧后,会用该帧去除以上面选定的除数P,验证余数是否为0,如果为0,则表示数据在传输过程中没有出现差错。
接收端CRC校验示例
CRC错误如何处理?
网络设备的接口偶尔出现极少量的CRC错包可以不用关注。如果是接口持续产生CRC错包,一般主要由传输介质导致的,比如连接的双绞线或者光纤质量有问题,接口光模块异常等。此时可尝试更换接口或光模块、线缆等,然后再检查此问题是否已解决。
CRC多项式和除数的关系
通过CRC多项式确定除数的步骤如下:
1. 理解生成多项式的结构
CRC生成多项式的一般形式为:
G(x)=xn+an−1xn−1+⋯+a1x+a0
其中,ai∈{0,1},最高次项为 xn。
2. 转换为二进制除数
将多项式的系数按以下规则转换为二进制数:
- 最高次项 xn 对应最高位(最左边)的1。
- 后续项的系数按降幂排列,填充中间的0。
- 最低位(最右边)对应常数项 x0。
示例:
- 生成多项式 G(x)=x4+x+1(CRC-4):
- 系数展开:x4+0x3+0x2+1x+1。
- 二进制除数:
1 0 0 1 1
→ 10011。
3. 处理不同表示形式
- 十六进制表示:某些标准用十六进制表示生成多项式,需转换为二进制并补全位数。
- 例如,CRC-16-CCITT的生成多项式为
0x1021
,对应二进制0001 0000 0010 0001
。 - 但实际多项式是 x16+x12+x5+1,需补充最高位的1,最终除数应为 1 0001 0000 0010 0001(17位)。
- 例如,CRC-16-CCITT的生成多项式为
4. 特殊情况处理
- 省略最高位:某些表示法可能省略最高位的1(隐含),需手动补上。
- 反转多项式:部分协议要求反转多项式位序(如LSB-first),此时需调整二进制顺序。
- 例如,
10011
反转后为11001
。
- 例如,
5. 示例验证
- 生成多项式:x3+x+1
- 系数展开:1x3+0x2+1x+1
- 除数:
1011
。
- 生成多项式:CRC-32(以太网)
- 多项式:x32+x26+x23+⋯+x2+x+1
- 除数:
1 00000100 11000001 00011101 10110111
(33位)。
总结步骤
- 确定多项式最高次项 xn,总位数 n+1。
- 展开所有项的系数(包括0),从 xn 到 x0。
- 转换为二进制数,直接作为除数。
- 注意十六进制转换和位序反转等特殊情况。
MODBUS与CRC校验
MODBUS 是一种广泛应用于工业自动化领域的通信协议,由 Modicon 公司(现为施耐德电气)于 1979 年推出。它主要用于工业设备之间的主从式通信,支持多种传输介质(如 RS-232、RS-485、以太网等),并具有简单、开放、易于实现的特点。

1. MODBUS 协议概述
1.1 协议特点
- 开放标准:MODBUS 是公开的协议,无需授权即可使用。
- 简单易用:协议结构简单,易于实现和调试。
- 支持多种传输方式:
- MODBUS RTU:基于串行通信(如 RS-485),使用二进制编码。
- MODBUS ASCII:基于串行通信,使用 ASCII 字符编码。
- MODBUS TCP/IP:基于以太网,使用 TCP/IP 协议。
- 主从架构:通信由主设备(Master)发起,从设备(Slave)响应。
1.2 应用领域
- 工业自动化(PLC、传感器、变频器等)。
- 楼宇自动化(HVAC、照明控制等)。
- 能源管理(电表、水表等)。
2. MODBUS 通信模式
MODBUS 支持以下三种通信模式:
2.1 MODBUS RTU
- 特点:
- 使用二进制编码,传输效率高。
- 基于串行通信(RS-232/RS-485)。
- 数据帧中包含 CRC 校验。
- 数据帧格式:
| 从站地址 | 功能码 | 数据 | CRC校验 |
- 从站地址:1 字节,表示从设备的地址(1-247)。
- 功能码:1 字节,表示操作类型(如读寄存器、写寄存器等)。
- 数据:可变长度,具体内容由功能码决定。
- CRC校验:2 字节,用于校验数据完整性。
2.2 MODBUS ASCII
- 特点:
- 使用 ASCII 字符编码,可读性强。
- 基于串行通信(RS-232/RS-485)。
- 数据帧中包含 LRC 校验。
- 数据帧格式:
| 起始符(:) | 从站地址 | 功能码 | 数据 | LRC校验 | 结束符(CRLF) |
- 起始符:1 字节,固定为
:
。 - 从站地址:2 字节,ASCII 字符表示的从设备地址。
- 功能码:2 字节,ASCII 字符表示的操作类型。
- 数据:可变长度,ASCII 字符表示的数据。
- LRC校验:2 字节,ASCII 字符表示的校验值。
- 结束符:2 字节,固定为
\r\n
(CRLF)。
2.3 MODBUS TCP/IP
- 特点:
- 基于以太网,使用 TCP/IP 协议。
- 数据帧中不包含校验字段(由 TCP 层保证数据完整性)。
- 支持高速通信。
- 数据帧格式:
| 事务标识符 | 协议标识符 | 长度 | 单元标识符 | 功能码 | 数据 |
- 事务标识符:2 字节,用于标识请求和响应对应关系。
- 协议标识符:2 字节,固定为
0x0000
(表示 MODBUS 协议)。 - 长度:2 字节,表示后续数据的字节数。
- 单元标识符:1 字节,表示从设备地址。
- 功能码:1 字节,表示操作类型。
- 数据:可变长度,具体内容由功能码决定。
3. MODBUS 功能码
MODBUS 定义了多种功能码,用于实现不同的操作。常见的功能码如下:
功能码 | 名称 | 描述 |
---|---|---|
0x01 | 读线圈状态 | 读取从设备的线圈(开关量)状态。 |
0x02 | 读离散输入状态 | 读取从设备的离散输入状态。 |
0x03 | 读保持寄存器 | 读取从设备的保持寄存器数据。 |
0x04 | 读输入寄存器 | 读取从设备的输入寄存器数据。 |
0x05 | 写单个线圈 | 写入单个线圈状态(开关量)。 |
0x06 | 写单个保持寄存器 | 写入单个保持寄存器的值。 |
0x0F | 写多个线圈 | 写入多个线圈状态。 |
0x10 | 写多个保持寄存器 | 写入多个保持寄存器的值。 |
使用C语言实现MODBUS指令CRC校验:
#include <stdint.h>
#include <stddef.h>
// CRC-16 (MODBUS) 校验函数
uint16_t crc16_modbus(const uint8_t *data, size_t length) {
uint16_t crc = 0xFFFF; // 初始值为0xFFFF
for (size_t i = 0; i < length; i++) {
crc ^= (uint16_t)data[i]; // 将数据与CRC寄存器异或
for (int j = 0; j < 8; j++) {
if (crc & 0x0001) { // 检查最低位是否为1
crc >>= 1; // 右移一位
crc ^= 0xA001; // 如果最低位是1,异或0xA001(0x8005的反转)
} else {
crc >>= 1; // 如果最低位是0,仅右移一位
}
}
}
return crc; // 返回最终的CRC值
}
// 示例:计算CRC-16校验码
int main() {
uint8_t data[] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x02}; // MODBUS RTU示例数据
size_t length = sizeof(data) / sizeof(data[0]);
uint16_t crc = crc16_modbus(data, length);
// 输出CRC校验码(低字节在前,高字节在后)
printf("CRC-16 (MODBUS): 0x%04X\n", crc);
printf("CRC Bytes: 0x%02X 0x%02X\n", (uint8_t)(crc & 0xFF), (uint8_t)(crc >> 8));
return 0;
}
参考文章:https://info.support.huawei.com/info-finder/encyclopedia/zh/CRC.html
Comments NOTHING