这几天在项目中CRC这块扎了一下跟头,于是整理相关的技术知识温习一波!
一、什么是CRC
CRC 是一种高效的差错检测机制,它通过特定的数学运算,为原始数据生成一个校验码。具体来说,发送端在发送数据时,会根据原始数据和一个预先约定好的生成多项式,进行一系列数学运算,得出一个校验码,然后将这个校验码附加在原始数据的末尾,一起发送出去。接收端收到数据后,会采用相同的生成多项式,对收到的数据进行同样的运算。如果运算结果得到的余数为 0,那就说明数据在传输过程中大概率没有出现错误,是完整可靠的;反之,如果余数不为 0,那就表明数据在传输途中可能受到了干扰,出现了错误,需要进行相应的处理,比如请求重发数据。
为什么嵌入式设备离不开 CRC?
在数据校验的领域中,有多种校验方式,比如简单的校验和。但相比之下,CRC 校验具有无可比拟的优势,这也是嵌入式设备对它如此依赖的原因。
简单校验和通常只是将数据中的各个字节进行简单相加,得到一个和值作为校验。这种方式虽然计算简单,但检错能力十分有限,只能检测出部分简单的错误。而 CRC 校验基于多项式模 2 除法,具有强大的检错能力。它不仅能检测出单比特错误,也就是数据中某一位发生错误的情况;还能检测双比特错误,即数据中有两位同时发生错误的情况;对于突发错误,也就是连续多位数据发生错误的情况,CRC 也能有效检测,并且漏检率极低。
在嵌入式设备的通信和数据存储场景中,CRC 校验的身影无处不在。在 UART通信中,数据通过串口线一位一位地传输,很容易受到外界干扰,CRC 校验能够及时发现传输过程中出现的错误,保证数据的正确接收。CAN总线常用于汽车电子、工业自动化等领域,数据在总线上高速传输,对可靠性要求极高,CRC 校验是 CAN 总线协议中不可或缺的一部分,确保了各个节点之间数据通信的稳定可靠。SPI通信中,CRC 校验也能有效保障主设备和从设备之间数据传输的准确性。
除了通信场景,在嵌入式设备的固件升级过程中,CRC 校验同样发挥着重要作用。当设备需要更新固件时,新的固件数据会通过各种方式传输到设备中。在这个过程中,CRC 校验可以验证接收到的固件数据是否完整正确,如果发现数据有误,设备就不会进行固件升级,从而避免因错误的固件导致设备出现故障。在存储介质校验方面,比如嵌入式设备中的闪存、EEPROM 等存储芯片,CRC 校验可以确保存储的数据在读取和写入过程中没有发生错误,保证数据的长期可靠性。
二、CRC 的核心三要素
-
生成多项式(G (x))
生成多项式是 CRC 校验中的关键要素之一,它决定了 CRC 校验的规则和效果。在 CRC 校验过程中,发送端和接收端必须事先约定好使用同一个生成多项式,只有这样,才能保证双方的校验过程一致,从而准确检测数据是否出错。
生成多项式通常用一个二进制多项式来表示,例如 CRC - 16 常用的生成多项式 G (x)=x¹⁶+x¹⁵+x²+1,它对应的二进制表示为 11000000000000101。再比如以太网中常用的 CRC - 32,其生成多项式更为复杂。生成多项式有一个重要的特点,那就是它的最高位和最低位必须为 1。这是因为最高位为 1 可以保证在进行模 2 除法运算时,能够正确地对数据进行处理;最低位为 1 则是为了确保生成的校验码具有足够的检错能力。
生成多项式的阶数(也就是多项式中最高次幂的指数)决定了校验码的长度。例如,CRC - 16 的生成多项式阶数是 16,所以它生成的校验码就是 16 位;CRC - 32 的生成多项式阶数是 32,生成的校验码就是 32 位。不同的应用场景会根据对数据准确性和校验效率的要求,选择不同阶数的生成多项式。一般来说,生成多项式的阶数越高,校验码越长,能够检测出的错误类型就越多,检错能力也就越强,但同时计算的复杂度也会相应增加。
-
模 2 运算
模 2 运算是 CRC 计算的数学基础,它是一种特殊的二进制运算,与我们平常熟悉的二进制运算有所不同。模 2 运算的核心特点是无进位和无借位的二进制运算。
在模 2 加法中,规则非常简单:1+1=0,0+1=1,0+0=0。这里的加法不考虑进位,就像是两个二进制位在独立地进行 “异或” 操作。例如,0101 和 0011 进行模 2 加法,计算过程为:0101⊕0011 = 0110 ,结果就是 0110。同样,模 2 减法与模 2 加法的规则是一样的,因为在不考虑借位的情况下,减法和加法本质上是相同的运算,例如 0110 - 0011 = 0101 ,这其实就是 0110⊕0011 = 0101 。
模 2 乘法的运算过程类似于普通的二进制乘法,但在累加中间结果时采用模 2 加法。例如,1011 和 101 进行模 2 乘法,计算过程如下:
1011
× 101
──────
1011 (1011×1)
0000 (1011×0,这里的0乘以任何数都为0,所以结果为0)
1011 (1011×1)
────────
100111 (将上面的结果进行模2加法,得到最终结果)
模 2 除法是 CRC 计算中最为关键的运算,它通过多次 “被除数与除数对齐后异或” 实现。在模 2 除法中,当余数位数与除数位数相同时,才进行异或运算,余数首位是 1,商就是 1,余数首位是 0,商就是 0。当已经除了几位后,余数位数小于除数,商 0,余数往右补一位,位数仍比除数少,则继续商 0,当余数位数和除数位数一样时,商 1,进行异或运算,得新的余数,以此至被除数最后一位。例如,1100100 除以 1011 的模 2 除法计算过程为:
1110
────────
1011〕1100100
-1011
──────
1111
-1011
──────
1000
-1011
──────
0110
-0000
──────
110
最终得到商为 1110,余数为 110。模 2 运算的这些规则看似简单,但正是它们构成了 CRC 校验强大的数学基础,使得 CRC 能够高效地检测数据错误。
-
校验码(余数)
校验码是 CRC 校验的最终产物,它是通过对原始数据进行一系列运算后得到的余数。在 CRC 校验过程中,首先要将原始数据左移 r 位,这里的 r 就是生成多项式的阶数。左移的目的是为了给校验码腾出足够的空间,相当于在原始数据的末尾添加 r 个 0。
例如,假设原始数据为 101011,生成多项式为 CRC - 4(G (x)=x⁴+x³+1,二进制表示为 11001 ),这里 r = 4。那么将原始数据左移 4 位后,得到 1010110000 。然后,用左移后的这个数据除以生成多项式(进行模 2 除法运算),得到的余数就是校验码。在这个例子中,1010110000 除以 11001 ,经过模 2 除法运算,最终得到的余数是 0100 ,这个 0100 就是 CRC 校验码。
得到校验码后,发送端会将校验码附加在原始数据的末尾,形成一个新的数据帧,然后将这个数据帧发送出去。接收端收到数据帧后,会用同样的生成多项式对整个数据帧进行模 2 除法运算,如果计算结果得到的余数为 0,那就说明数据在传输过程中没有发生错误,是完整可靠的;如果余数不为 0,那就表明数据在传输过程中出现了错误,需要进行相应的处理,比如请求重发数据。
三、CRC多种类型
生成多项式怎么选?常见标准解析
在嵌入式系统中,选择合适的生成多项式是 CRC 校验的关键。不同的生成多项式适用于不同的应用场景,它们在检错能力、计算复杂度等方面存在差异。下面为你介绍一些常见的 CRC 标准及其典型应用场景。
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CRC-8 的生成多项式相对简单,校验码长度为 8 位。由于其计算复杂度低,占用资源少,非常适合对实时性要求较高且数据量相对较小的场景,如传感器数据校验。在一些温度传感器、湿度传感器与微控制器通信时,使用 CRC-8 可以快速校验少量的传感器数据,确保数据的准确性,同时不会对微控制器的资源造成过多负担。
CRC-16 的校验码长度为 16 位,生成多项式比 CRC-8 复杂一些。它在检错能力和计算复杂度之间取得了较好的平衡,常用于工业控制领域的通信协议中。例如,Modbus 协议广泛应用于工业自动化系统,设备之间通过串口通信传输控制指令和数据,CRC-16 能够有效检测传输过程中的错误,保证通信的可靠性。CAN 协议常用于汽车电子和工业自动化中的现场总线通信,CRC-16 也是 CAN 协议中数据校验的重要组成部分,确保各个节点之间数据传输的准确性。
CRC-32 的校验码长度为 32 位,生成多项式更为复杂。它具有极强的检错能力,能够检测出各种类型的错误,适用于对数据完整性要求极高的场景。在以太网通信中,大量的数据在网络中传输,CRC-32 被用于以太网帧的校验,确保数据在网络传输过程中没有发生错误,保障网络通信的稳定性。在 ZIP 文件压缩中,CRC-32 用于校验文件内容,保证压缩和解压缩过程中文件数据的完整性。
选择生成多项式时,需要综合考虑应用场景对数据准确性、实时性以及硬件资源的要求。如果数据量较小且实时性要求高,优先选择 CRC-8;如果是工业控制等对可靠性有一定要求且数据量适中的场景,CRC-16 是不错的选择;而对于像网络通信、文件存储等对数据完整性要求极高的场景,CRC-32 则更为合适。同时,还需要考虑硬件的计算能力和资源限制,确保选择的生成多项式能够在硬件平台上高效运行。
四、硬件CRC与软件CRC
在嵌入式系统中,CRC 校验既可以通过硬件实现,也可以通过软件实现,这两种方式各有优劣,需要根据具体的应用场景来平衡效率与资源。
-
硬件实现:许多微控制器,如 STM32 系列,都内置了 CRC 模块。以 STM32 为例,其内置的 CRC 模块通过寄存器操作直接计算 CRC 校验码。在使用时,只需要将需要计算 CRC 的数据按照规定的方式写入特定的寄存器(如 CRC_DR 寄存器),硬件模块就会自动完成 CRC 计算,并将结果存储在相应的寄存器中。这种硬件实现方式速度极快,通常可以在纳秒级的时间内完成计算,非常适合高频数据校验的场景。比如在一些高速数据采集系统中,大量的传感器数据需要实时进行校验,硬件 CRC 模块能够快速处理这些数据,保证系统的实时性。然而,硬件实现的灵活性较低,一旦硬件设计完成,其使用的生成多项式、计算方式等就相对固定,难以根据不同的应用需求进行灵活调整。
-
软件实现:软件实现 CRC 校验通常基于查表法。这种方法的原理是预先计算出一个包含 2⁸=256 个余数的表,这个表涵盖了所有可能的字节值与生成多项式进行模 2 除法后的余数。在实际计算 CRC 时,每次处理一个字节的数据,通过查找这个表并进行异或操作来提升计算效率。以 CRC16_Modbus 算法为例,每处理一个字节的数据,只需要进行 1 次查表和 1 次异或操作,大大减少了计算量。这种方式非常适合资源有限的 8 位 / 16 位单片机。在一些简单的嵌入式设备中,如智能手环、智能家居传感器等,由于这些设备的硬件资源有限,使用软件实现 CRC 校验可以避免增加额外的硬件成本,同时通过优化算法和查表法,也能在一定程度上满足系统对校验速度的要求。不过,软件实现的速度相对硬件实现较慢,会占用一定的 CPU 资源,在处理大量数据时,可能会影响系统的整体性能。
在实际应用中,就根据自己的需求再具体选择了~
五、:STM32 硬件 CRC 模块的使用
-
初始化:在使用 STM32 的硬件 CRC 模块之前,需要进行初始化操作。首先,复位 CRC 模块,通过将 CRC_CR 寄存器设置为 0x01 来实现,这一步是为了确保 CRC 模块处于初始状态,清除之前可能存在的计算结果。然后,设置初始值为 0xFFFFFFFF,这是标准 CRC-32 配置中的常见初始值设置。在一些工业监控系统中,使用 STM32 作为数据采集和处理的核心,在系统启动时,就需要按照这样的步骤对 CRC 模块进行初始化,为后续的数据校验做好准备。
-
数据计算:完成初始化后,就可以进行数据计算了。在计算过程中,按 32 位字写入 CRC_DR 寄存器,硬件模块会自动进行 CRC 计算并输出校验值。例如,假设有一个包含 5 个 32 位数据的数组 data [5],需要计算其 CRC 校验值。通过一个循环,将数组中的每个 32 位字依次写入 CRC_DR 寄存器,在每次写入后,硬件模块会根据之前的计算结果和新写入的数据自动更新 CRC 值。当所有数据都写入完成后,直接读取 CRC_DR 寄存器,此时寄存器中的值就是整个数组的 CRC 校验值。在实际应用中,如数据存储系统中,需要对存储的数据进行 CRC 校验,就可以按照这种方式对存储的数据进行计算,得到的 CRC 校验值可以用于后续的数据完整性验证。
-
代码示例:
"stm32f4xx.h"
// 初始化CRC模块void CRC_Init(void){ // 复位CRC模块 CRC->CR = 0x01; // 设置初始值为0xFFFFFFFF CRC->DR = 0xFFFFFFFF;}// 计算数据的CRC值uint32_t CRC_Calculate(uint32_t *data, uint16_t len){ uint16_t i; // 清除之前的计算结果(通过复位CRC模块实现,这里可不重复设置) // CRC->CR = 0x01; for (i = 0; i < len; i++) { // 按32位字写入CRC_DR寄存器 CRC->DR = data[i]; } // 返回计算得到的CRC值 return CRC->DR;}int main(void){ uint32_t data[5] = {0x12345678, 0x87654321, 0xABCD1234, 0x4321DCBA, 0xFEDCBA98}; uint32_t crc_value; // 初始化CRC模块 CRC_Init(); // 计算数据的CRC值 crc_value = CRC_Calculate(data, 5); while (1) { // 主循环可以进行其他操作 }}
在上述代码中,首先定义了 CRC_Init 函数用于初始化 CRC 模块,然后定义了 CRC_Calculate 函数用于计算数据的 CRC 值。在 main 函数中,创建了一个包含 5 个 32 位数据的数组,并调用初始化函数和计算函数得到 CRC 值。在实际应用中,可以根据具体需求对代码进行扩展和优化,例如将数据从外部传感器或存储设备中读取,然后进行 CRC 校验,确保数据的准确性。
六、避 坑
你踩过吗?
-
生成多项式不匹配:在实际应用中,生成多项式不匹配是一个常见的错误。例如,在一个工业自动化系统中,发送端使用的是 CRC-16 标准的生成多项式进行数据校验,而接收端却误将其设置为 CRC-CCITT 的生成多项式。当发送端发送数据时,会根据 CRC-16 的生成多项式计算出校验码,然后将数据和校验码一起发送出去。然而,接收端在接收到数据后,使用 CRC-CCITT 的生成多项式进行校验,由于两个生成多项式不同,计算出来的校验码自然也不同,这就导致接收端无法正确验证数据的完整性,从而判定数据出错。在一些老旧设备的改造升级中,可能会因为不同设备厂家对 CRC 标准的理解和应用不同,导致生成多项式不匹配的问题出现。所以,在通信双方进行数据传输之前,一定要确保双方使用的生成多项式完全一致,包括最高位补零规则等细节,避免因为生成多项式不匹配而导致数据校验失败。
-
字节顺序问题:字节顺序问题也是 CRC 校验中容易出现的错误之一。在不同的硬件平台和通信协议中,数据的字节顺序可能不同,存在大端模式和小端模式之分。例如,在一个基于 SPI 通信的嵌入式系统中,主设备采用小端模式发送数据,而从设备却按照大端模式来接收和处理数据。当主设备发送数据时,会按照小端模式将数据的低位字节先发送,高位字节后发送。但从设备在接收数据后,按照大端模式的规则来解析数据,这就导致数据的位处理顺序发生了变化。如果在计算 CRC 校验码时没有考虑到字节顺序的差异,就会导致校验失败。在一些涉及网络通信的嵌入式设备中,不同设备之间的字节顺序可能不同,需要特别注意。像 Modbus RTU 协议就明确规定了 CRC 校验码的低字节在前,高字节在后的传输顺序。在实际应用中,一定要明确数据的传输格式,根据字节顺序的要求对数据进行正确的处理,或者在计算 CRC 校验码时,对多项式进行相应的反转,以确保 CRC 校验的准确性。
-
初始值与反转设置:初始值与反转设置的错误也会导致 CRC 校验出现问题。部分协议对 CRC 的初始值和数据位的反转有特定要求。以 USB 通信协议为例,它要求 CRC 的初始值为 0xFF,并且在计算 CRC 校验码时,需要对输入输出的数据位进行反转。如果在实现 USB 通信的 CRC 校验时,没有按照协议要求将初始值设置为 0xFF,或者没有对数据位进行反转操作,就会导致计算出来的 CRC 校验码与协议规定的不一致,从而使数据校验失败。在一些复杂的通信系统中,可能会涉及多种协议的交互,不同协议对初始值和反转设置的要求各不相同,很容易出现混淆。所以,在进行 CRC 校验实现时,一定要严格按照协议的规定进行初始值和反转设置,仔细核对每一个细节,确保与协议要求完全一致,避免因为这些设置错误而影响数据的正确校验。
七、嵌入式优化技巧
-
预计算 CRC 表:在嵌入式系统中,预计算 CRC 表是一种非常有效的优化技巧。以一个基于 STM32 的智能家居控制系统为例,该系统需要频繁地对传感器数据进行 CRC 校验。在传统的 CRC 计算方式下,每次计算 CRC 校验码时,都需要对数据进行逐位计算,这会消耗大量的 CPU 时间。而采用预计算 CRC 表的方法后,在编译阶段就可以生成一个包含 256 字节的 CRC 表。这个表是通过对 0 到 255 的每一个字节与生成多项式进行模 2 除法运算,预先计算出它们对应的 CRC 值,然后将这些值存储在表中。在运行时,当需要计算 CRC 校验码时,只需要每次处理一个字节的数据,通过查找这个表并进行简单的异或操作,就可以快速得到 CRC 值。这样一来,大大减少了计算量,计算效率得到了显著提升,相比逐位计算方式,效率可以提升 80% 以上。这种方法非常适合高频数据传输的场景,如智能家居系统中传感器数据的实时上传、工业自动化系统中设备之间的频繁通信等,能够在保证数据准确性的同时,提高系统的实时性和响应速度。
-
增量计算:增量计算是另一种重要的嵌入式优化技巧,特别适用于处理长数据的场景。以固件升级过程为例,当嵌入式设备进行固件升级时,新的固件数据通常比较大,可能会分成多个数据块进行传输。在传统的 CRC 计算方式下,每次接收到一个数据块,都需要对整个已接收的数据重新进行 CRC 计算,这会消耗大量的 CPU 资源和时间。而采用增量计算的方法,设备可以支持断点续算。当接收到第一个数据块时,设备会计算该数据块的 CRC 值,并保存当前的 CRC 状态。当接收到下一个数据块时,设备会基于上一个数据块的 CRC 状态,继续计算当前数据块的 CRC 值,而不需要重新计算已处理部分的数据。这样就避免了重复计算已处理的数据,大大节省了 CPU 资源,提高了计算效率。在一些对资源和实时性要求较高的嵌入式系统中,如智能穿戴设备、工业监控设备等,增量计算能够有效地减少系统的负担,确保系统在进行大量数据处理时,仍然能够稳定、高效地运行 。
END