利用C++实现CRC校验


前言

一、CRC校验用途

 1.1 通信协议与数据传输

 1.2 存储设备

 1.3 工业控制与嵌入式系统

 1.4 软件与文件完整性

 1.5 科学计算 & 深空探测

小结

二、CRC校验原理

 2.1 CRC 的校验本质

 2.2 CRC 校验的核心步骤

  (1)选择 CRC 生成多项式

  (2)发送端计算 CRC 码

  (3)接收端校验

 2.3 CRC 二进制除法示例

三、CRC校验代码实现步骤

1.选择合适的多项式

2. 初始化 CRC 寄存器

3. 预处理数据

4. 逐位计算 CRC

5. 后处理 CRC

6. 输出 CRC 校验码

四、CRC校验代码(以CRC-4为例)

五、总结

前言

CRC校验可以作为c++学习过程中的小项目,帮助我们更好的将c++学习中的理论知识和实践相结合,本项目中的CRC校验代码,涉及基础语法、位运算、条件判断、循环控制、函数、类型转换等。本篇文章从CRC校验的用途、CRC校验代码实现原理、CRC校验代码三方面展开。

一、CRC校验用途

CRC(循环冗余校验,Cyclic Redundancy Check)是一种高效的错误检测技术,它的主要作用是检测数据传输或存储过程中可能发生的错误,确保数据的完整性。下面介绍 CRC 在各个领域的实际用途

1.1 通信协议与数据传输

  • 网络通信:在 以太网 (Ethernet)Wi-Fi 等协议中,数据包都会附带 CRC 校验码,接收端对其进行计算并校验数据是否完整。如果 CRC 校验失败,则表示数据包在传输过程中出现错误,需要 请求重传 (ARQ)丢弃错误数据。以太网 (Ethernet)主要使用CRC-32检测数据帧中的错误。
  • 无线通信:在无线环境下,数据容易受 信号干扰、噪声、抖动 等影响,因此 CRC 被用于数据包的错误检测。5G NR(New Radio) 使用 CRC-24 进行数据完整性校验,以减少误码率。蓝牙 (Bluetooth)、ZigBee、LoRa 等无线通信协议中,通常使用 CRC-8 或 CRC-16进行校验。
  • 数据链路层协议:在 HDLC、PPP(点对点协议)、CAN 总线 等数据链路层协议中,使用 CRC-16 或 CRC-32 进行数据帧的完整性校验。CAN 总线(用于汽车、工业自动化)使用 CRC-15开展校验,USB 协议 采用 CRC-5 和 CRC-16进行数据包的错误检测。

1.2 存储设备

  • 硬盘、SSD、RAID 纠错:在 机械硬盘 (HDD)固态硬盘 (SSD) 读取数据时,数据可能因 磁场干扰、坏扇区 等原因而发生错误,因此使用 CRC 校验 来检测数据损坏。RAID(冗余阵列磁盘) 采用 CRC 结合 ECC(纠错码) 确保数据存储的可靠性。
  • 光盘 (CD/DVD) 读取:CD、DVD、Blu-ray 盘片存储的数据容易受划痕、老化、光学误差影响,因此采用 CRC 进行错误检测,并结合 Reed-Solomon 纠错码 修复错误。

1.3 工业控制与嵌入式系统

  • 物联网(IoT)、嵌入式设备:物联网 (IoT) 设备,如 智能传感器、无线模块、智能家居设备,使用 CRC 校验数据完整性,避免错误数据导致系统失效。
  • 汽车电子(车载网络 CAN 总线):现代汽车的 ECU(电子控制单元)、传感器、自动驾驶系统 都依赖 CAN 总线 进行通信。CAN 总线使用 CRC-15 进行数据帧校验,防止错误命令导致汽车故障。

1.4 软件与文件完整性

  • 数据文件传输校验:在文件下载或传输过程中,使用 CRC 校验文件的完整性,防止文件损坏或篡改。
  • 数字签名 & 数据防篡改:CRC 可用于检测数据篡改,确保文件或数据包在存储或传输过程中未被恶意修改。区块链技术(Bitcoin、Ethereum)在交易数据中使用 哈希 + CRC 机制 检查数据完整性。银行 ATM 交易 采用 CRC-16 或 CRC-32 进行 PIN 码数据完整性校验。
  • 软件校验:某些软件使用 CRC 校验自身代码是否被恶意修改。游戏反作弊系统 通过 CRC 检测游戏文件是否被修改,如 VAC(Valve 反作弊系统)Windows PE 文件校验 通过 CRC-32 计算 PE 头部,防止病毒篡改系统文件。

1.5 科学计算 & 深空探测

  • NASA 深空探测:在深空探测任务(如 火星探测器、哈勃望远镜)中,由于数据传输距离极远,CRC 与 Forward Error Correction (FEC) 结合,确保数据可靠传输。

小结

图片描述

二、CRC校验原理

2.1 CRC 的校验本质

数据被视为一个二进制数,选择一个固定的 CRC 生成多项式(比如 x^4 + x + 1),然后使用二进制除法(不进位) 计算出余数(即 CRC 校验码)。将余数附加到数据后面,接收方可以通过相同的计算过程来检查数据是否正确。

2.2 CRC 校验的核心步骤

(1)选择 CRC 生成多项式

下图是常用的CRC模型,参照红框。通信双方可以约定一个相同的多项式。
图片描述
其中:

  • width是CRC码的位宽;
  • poly是省略最高位的多项式,因为更利于编程实现,即移位之后不需要考虑最高位;
  • init是CRC码初始值,因为原始数据可能以不同位数的0开头,所以需要设置初始值来区分;
  • refin是输入逆向标志位,这里为真,表示输入数据需要先进行逆向(即按位倒序),再进行后续运算;
  • refout是输出逆向标志位,这里为真,表示CRC码需要先进行逆向(即按位倒序),再输出;
  • xorout是输出异或值,是输出CRC码前与其进行异或运算的数;

(2)发送端计算 CRC 码

  • 将数据看作一个二进制数,假设发送 1011001(7 位)。
  • 在数据后面添加 4 个 0(因为使用 CRC-4),变成 10110010000(11 位)。
  • 用二进制除法(不进位) 计算 余数: 用 10110010000 除以 0011,得到余数(例如 0110)。
  • 将余数 0110 附加到数据末尾,得到 10110010110。

(3)接收端校验

 接收端收到 10110010110,用相同的 二进制除法 计算 CRC 余数:

  • 如果余数为 0000,表示数据未出错 。
  • 如果余**数 不为 **0000,表示数据出错 。

2.3 CRC 二进制除法示例

图片描述

三、CRC校验代码实现步骤

注意实际编程时并不直接使用上述方法,以下是编程实现步骤:

1.选择合适的多项式

  • CRC 校验的第一步是选择一个“生成多项式”。这个多项式决定了 CRC 计算的规则,不同的应用可能会选择不同的多项式。例如,CRC-16、CRC-32 分别使用不同的多项式。
  • 多项式通常以标准形式给出,如 CRC-16-CCITT 使用的是 0x1021。

2. 初始化 CRC 寄存器

  • CRC 计算开始前,CRC 寄存器(通常是一个与多项式位宽相同的寄存器)被初始化,常见的初始化值有全0或全1。

3. 预处理数据

  • 根据具体的 CRC 类型,可能需要对数据进行预处理,如反转数据位(反射)。

4. 逐位计算 CRC

  • 数据的每一位被逐一处理。每处理一个数据位,CRC 寄存器左移一位(模拟除法中的移位),并根据数据位决定是否执行 多项式异或。
  • 如果 CRC 最高位为 1,则与多项式进行异或运算。

5. 后处理 CRC

  • 根据特定的 CRC 标准,可能需要对最终的 CRC 寄存器进行后处理,例如可能需要再次进行位反射。
  • 最终的 CRC 值可能还会与一个最终的异或值进行异或,以得到最终的 CRC 校验码。

note:此步骤是否执行需要参照下图红框
图片描述

6. 输出 CRC 校验码

  • 确保 CRC 结果为所需的位数(例如 CRC-4 需要 4 位),将最终的 CRC 校验码作为结果返回。

四、CRC校验代码(以CRC-4为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <iostream>
#include <iomanip>
#include <string>
#include <bitset>
#include <stdexcept> // 用于异常处理

using namespace std;

// CRC-4 校验参数
const uint8_t CRC_POLYNOMIAL = 0x3; // CRC-4 的多项式(x^4 + x + 1),十六进制为 0x3
const uint8_t CRC_INITIAL_VALUE = 0x0; // 初始值
const uint8_t CRC_XOR_OUT = 0x0; // 反向输出,通常为 0

// 反转输入数据的函数
uint8_t reflect(uint8_t data)
{
uint8_t reflected = 0;
for (int i = 0; i < 8; i++) {
if (data & (1 << i)) {
reflected |= (1 << (7 - i));
}
}
return reflected;
}
// 计算 CRC-4 校验
uint8_t calculateCRC4(uint8_t data, bool reflectInput, bool reflectOutput)
{
uint8_t crc = CRC_INITIAL_VALUE;
// 如果反转输入数据
if (reflectInput)
{
data = reflect(data); // 反转 8 位数据
}
// 进行 CRC 计算
for (int i = 0; i < 8; i++)
{
bool bit = (data >> (7 - i)) & 1; // 提取数据的当前位
crc <<= 1; // CRC 左移,准备处理下一个数据位

if (bit)
{ // 如果当前数据位是 1,将其加入 CRC
crc |= 1;
}

if (crc & 0x08)
{ // 如果最高位是 1,执行多项式异或
crc ^= CRC_POLYNOMIAL;
}
}
// 如果反转输出数据
if (reflectOutput)
{
crc = reflect(crc); // 只反转 4 位,而不是 8 位!
}
return crc & 0x0F; // 确保 CRC 结果为 4 位
}

int main()
{
string hexInput;
bool reflectInput, reflectOutput;
int tempData;

cout << "请输入十六进制数据(例如:1A):";
cin >> hexInput;

// 处理问题1:异常捕获,防止非十六进制输入
try
{
tempData = stoi(hexInput, nullptr, 16); //将用户输入的十六进制字符串转换为十进制整数
}
catch (const invalid_argument& e)
{
cerr << "输入错误: 请输入有效的十六进制数!" << endl;
return 1; // 退出程序
}
catch (const out_of_range& e)
{
cerr << "输入超出范围: 请输入 0x00 - 0xFF 之间的十六进制数!" << endl;
return 1; // 退出程序
}

// 处理问题 2:防止数据溢出 uint8_t
if (tempData > 255)
{
cerr << "输入值超出范围!请输入 0x00 - 0xFF 之间的数。" << endl;
return 1; // 退出程序
}
// 安全转换为 uint8_t
uint8_t data = static_cast<uint8_t>(tempData);
cout << "是否反转输入数据?(0: 否, 1: 是): ";
cin >> reflectInput;

cout << "是否反转输出数据?(0: 否, 1: 是): ";
cin >> reflectOutput;

// 计算 CRC 校验码
uint8_t crc = calculateCRC4(data, reflectInput, reflectOutput);

// 输出结果
cout << "计算得到的CRC-4 校验码为: " << hex << uppercase << (int)crc << endl;

return 0;
}

以下是运行结果示例:
图片描述

五、总结

1.系统性的了解了CRC校验的用途、原理、编程实现,深刻理解了CRC校验的本质。

2.对于c++学习:

(1)学习了两个头文件iomanip 、stdexcept 。其中:

(I/O 操作库)主要用于 控制输出格式:

  • 设置十六进制输出格式(hex)
  • 大写字母格式(uppercase)
  • 设置输出宽度和填充字符(setw()、setfill())

  是 C++ 标准库中的 异常处理头文件:

  • std::invalid_argument (无效参数异常)
  • std::out_of_range (超出范围异常)
  • std::runtime_error (运行时错误异常)

这些异常可以用 throw 抛出,也可以用** **try-catch 语句捕获,防止程序崩溃。

(2)数据类型:uint8_t

使用 uint8_t(无符号 8 位整数)来定义 CRC_POLYNOMIAL(CRC 多项式)的原因是为了确保在内存中占用最小的空间,并且它的范围适合存储 CRC 多项式值。

(3)反转函数中的位运算符

reflected:作为存储反转后的数据//data & (1 << i): 用于检查 data 的第i位是否为1 这里”1”二进制数为0000 0001/reflected |= (1 << (7 - i)):这里一定要用 “|=” (按位或赋值运算符),若使用”=”,下次循环新的reflected会覆盖上一轮的结果。

(4)^= 表示异或运算

(5)static_cast(): static_cast(value) 是 C++ 最常用的类型转换操作

  • 显式转换不同类型的数据(如 int → uint8_t)。
  • 确保转换是安全的(编译时进行检查)。
  • 不会影响底层数据的存储方式。

为什么不直接使用 uint8_t data = tempData;?
tempData 是 int 类型(32 位),直接赋值给 uint8_t(8 位)时,可能产生溢出。


利用C++实现CRC校验
http://example.com/2025/03/21/利用C-实现CRC校验/
作者
Yunqiu Zhou
发布于
2025年3月21日
许可协议