在数字系统中,数据完全有可能被损坏,特别是当它流经通信介质时。在数字电子学中,消息是等于 0 或 1 的比特流,当这些比特中的一个或多个在传输过程中意外更改时,它就会损坏。因此,消息中始终有一些额外的数据用于检测原始消息是否已损坏。在前面,我们分析了与数据传输相关的一种早期错误检测形式: 奇偶校验位是添加到消息中的附加位,用于跟踪等于 1 的位数是否为奇数或偶数(取决于奇偶校验的类型)。但是,如果两个或多个 bit 同时更改,则此方法无法检测到错误。
循环冗余校验 (CRC) 是一种广泛使用的技术,用于检测数字数据在传输和存储过程中的错误。在 CRC 方法中,几个校验位(称为校验和)被附加到正在传输的消息中。接收方可以确定校验位是否与数据一致,以便在传输过程中发生错误时以一定概率进行断言。如果是这样,接收方可以要求发送方再次重新传输消息。
此技术也适用于某些数据存储设备,例如硬盘驱动器。在这种情况下,磁盘上的每个块都有一定的校验位,硬件可能会在检测到错误时自动启动块的重新读取,或者可能会将错误报告给软件。需要强调的是,CRC 是识别消息损坏的好方法,但不是在检测到错误时进行更正的好方法。
由于许多通信外设和协议(如以太网、MODBUS 等)都使用 CRC 方法,在微控制器中很常见,专用硬件外设能够计算字节流的 CRC 校验和,从而将 CPU 从软件中执行此操作中解放出来。所有 STM32 微控制器都提供专用的 CRC 外设,本章简要介绍如何使用相应的 CubeHAL 模块。
像往常一样,在进入实现细节之前,我们将首先简要介绍 CRC 技术背后的数学原理。zlib.net/crc_v3.txt
1、CRC 计算简介
CRC 技术基于众所周知的多项式算术特性。为了计算比特流的校验和,该消息被视为一个多项式,该多项式除以另一个固定多项式(称为多项式生成器)。此操作的余式就是校验和,添加到原始消息中。接收方将使用它以及多项式生成器来检查消息是否正确。
在实践中,所有 CRC 方法都使用 GF(2ⁿ) 中的多项式。GF(pⁿ) 代表伽罗瓦域,也称为有限域,即具有有限元素数量的域。伽罗瓦域是定义乘、加、减、除运算并满足某些基本规则的集合。有限域的最常见示例由整数模 p 给出,其中 p 是素数。在我们的例子中,p 等于 2,这意味着当 n=1 时,GF(2ⁿ) 字段只包含两个元素:0 和 1。
在 GF(2ⁿ) 中,加法和减法以 2 为模数执行,也就是说,它们对应于 XOR 逻辑运算。
乘法对应于 AND 逻辑运算。
GF(2ⁿ) 中的多项式是单个变量 x 中的多项式,其系数为 0 或 1。CRC 技术将数据消息的位解释为 GF(2ⁿ) 中多项式的系数,其度数等于 n − 1,其中 n 是消息的长度。例如,假设消息 11100110 的长度等于 8,这对应于多项式:
如前所述,在 GF(2ⁿ) 中,加法和减法对应于 XOR 逻辑运算。这意味着多项式 x 4 + x 3 + 1 和 x 3 + x + 1 之和等于 x 4 + x。(在普通代数中,加法等于 x 4 + 2x 3 + x + 2。)显然,这两个多项式的减法也是一样。GF(2ⁿ) 中的多项式乘法,像往常一样,很像乘以十进制整数,跟踪 x 的幂而不是小数位。例如,将前两个多项式相乘,得到:
第一个项中的每个项都乘以第二个项中的每个项,然后我们按照 GF(2ⁿ) 中的加法规则将它们相加。
在 GF(2ⁿ) 中,一个多项式除以另一个多项式类似于整数的长除法(余数),只是没有借用或进位。例如,让我们将多项式 x 7 + x 6 + x 5 + x 2 + x 除以多项式 x 3 + x + 1。
我们首先将被除数的第一项除以除数的最高项(即具有最高 x 次方的项,在本例中为 x 3 )。接下来,我们将除数乘以刚刚获得的结果(最终商的第一项)。
现在我们应用了 GF(2ⁿ) 中的减法规则,从原始被除数的适当项中减去刚刚得到的乘积。
我们重复前面的步骤,只是这次使用刚刚写为减法后被除数的两个项。
该过程一直持续到获得的被除数低于除数的幂次。我们这样获得了除法的余数,它表示要附加到原始消息的校验和。
接收方有两种方法可以评估传输的正确性。它可以从接收到的数据的前 n 位计算校验和,并验证它是否与最后 r 个接收到的位一致。或者,按照通常的做法,接收器可以将所有接收到的位除以多项式生成器,并检查 r 位余数是否为 0。
但是,CRC 计算的确切算法通常与正常的多项式除法不同。此外,多项式生成器可以定义特定的初始和最终条件。这意味着多项式生成器不能任其更改,而是从经过充分研究http://bit.ly/293h2Hd的多项式组合中保留下来。例如,广泛采用的 CRC-32 多项式的形式为:
可以用二进制表示,序列为 00000100110000010001110110110111,用十六进制表示,数字0x04C1 1DB7。它被许多传输和存储协议采用,如以太网、串行 ATA、MPEG-2、BZip2 和 PNG。
1.1、STM32F1/F2/F4/L1 MCU 中的 CRC 计算
多项式的长除法适合执行手动计算。但是,另一种更有效的 CRC 算法是使用按位消息 XOR 技术的多项式除法,这适合使用专用硬件电路: Shift Registers来实现。
STM32 微控制器中 CRC 计算的过程与 CRC-32 多项式定义的算法有关,即以下:
• 用 0xFFFF FFFF 与数据值进行 XOR 来初始化 CRC 寄存器。
• 逐位移动输入流。如果弹出的 MSB 为 '1',则对 CRC 寄存器值与多项式生成器进行 XOR 运算。
• 如果所有输入位都被处理,则 CRC 移位寄存器包含 CRC 值。
假设数据值等于 10101101(0xAD),CRC 多项式等于 00010110 (0x16),STM32 MCU 实现的算法是这样工作的(上图示意图化了这个过程):
1. CRC 寄存器的初始内容是通过 0xFF 和 0xAD 进行 XOR 运算来计算的。
2. 由于 MSB 位为0,CRC 寄存器只是左移。
3. 现在 CRC 寄存器的 MSB 位为 1。因此,我们首先左移寄存器,然后使用 CRC 多项式 (0x16) 执行 XOR。
4. 由于 MSB 位为0,CRC 寄存器只是左移。
5. 现在 CRC 寄存器的 MSB 位为1。因此,我们首先左移寄存器,然后使用 CRC 多项式 (0x16) 执行 XOR。
6.由于 MSB 位为0,CRC 寄存器只是左移。
7. 现在 CRC 寄存器的 MSB 位为 1。因此,我们首先左移寄存器,然后使用 CRC 多项式 (0x16) 执行 XOR。
8. 现在 CRC 寄存器的 MSB 位为 1。因此,我们首先左移寄存器,然后使用 CRC 多项式 (0x16) 执行 XOR。
9. 最后,MSB 再次为 0。因此,我们对 CRC 寄存器执行左移。final 值表示要添加到消息前面的校验和。
上述算法只是 STM32 MCU 中实际实现的算法的简化。事实上,它的不同有两个主要原因:
• CRC 多项式是固定的,对应于 CRC-32 (0x04C1 1DB7)。
• 单个输入/输出数据寄存器为 32 位宽,CRC 校验和是在整个 32 位寄存器上计算的,而不是逐字节计算的。http://bit.ly/29303sh、http://bit.ly/293067u
这极大地限制了此外设的有效可用性。
1.2、STM32F0/F3/F7/L0/L4/L5/G0/G4 MCU 中的 CRC 外设
在上一段中,我们已经看到一些 STM32 MCU 提供的 STM32 外设仅限于使用 CRC-32 以太网多项式计算 CRC。此外,每次计算的处理数据大小为 32 位。在较新的 STM32 系列中,此限制已被取代。事实上,STM32F0/F3/F7/L0/L4/L5/G0/G4 MCU 提供了更高级的 CRC 外设,如下表所示。
在这些 MCU 中,CRC 外设设计为默认与 STM32F1/F2/F4/L1 MCU 提供的更简单的 CRC 外设兼容。这意味着,在没有显式配置的情况下,设计为在 STM32F1/F2/F4/L1 MCU 上运行的代码将在 STM32F0/F3/F7/L0/L4 MCU 上运行,无需任何更改。
2、HAL_CRC 模块
CubeHAL 提供了一个专用模块来操作 CRC 外设寄存器:HAL_CRC。CRC 外设通过使用 CRC_HandleTypeDef 结构的实例来引用。在提供最简单的 CRC 外设的 STM32F1/F2/F4/L1 MCU 中,此结构体按以下方式定义:
typedef struct {
CRC_TypeDef *Instance; /* CRC registers base address */
HAL_LockTypeDef Lock; /* CRC locking object */
__IO HAL_CRC_StateTypeDef State; /* CRC communication state */
} CRC_HandleTypeDef;
唯一相关的字段是 Instance 1,它是指向 CRC 外设描述符的指针(其基址由 CRC 宏定义)。
相反,在 STM32F0/F3/F7/L0/L4 MCU 中,CRC_HandleTypeDef 结构体的定义方式如下:
typedef struct {
CRC_TypeDef *Instance; /* Register base address */
CRC_InitTypeDef Init; /* CRC configuration parameters */
HAL_LockTypeDef Lock; /* CRC Locking object */
__IO HAL_CRC_StateTypeDef State; /* CRC communication state */
uint32_t InputDataFormat; /* Specifies input data format. */
} CRC_HandleTypeDef;
唯一相关的区别是 Init 字段的存在,该字段用于配置 CRC 外设,以及 InputDataFormat 字段,该字段指定输入数据的数据大小:它可以采用下表中的值。
为了在这些 MCU 中配置 CRC 外设,我们使用 CRC_InitTypeDef 结构的实例, 其定义如下:
typedef struct {
uint8_t DefaultPolynomialUse; /* Indicates if default polynomial is used */
uint8_t DefaultInitValueUse; /* Indicates if default init value is used */
uint32_t GeneratingPolynomial; /* Set CRC generating polynomial */
uint32_t CRCLength; /* Indicates CRC length */
uint32_t InitValue; /* Set the initial value to start CRC computation */
uint32_t InputDataInversionMode; /* Specifies input data inversion mode */
uint32_t OutputDataInversionMode; /* Specifies output data (i.e. CRC) inversion mode */
} CRC_InitTypeDef;
• DefaultPolynomialUse:这个字段指示使用的是默认多项式(即 CRC-32)还是自定义多项式。它可以采用 DEFAULT_POLYNOMIAL_ENABLE 或 DEFAULT_POLYNOMIAL_DISABLE 的值。在最后一种情况下,必须设置字段 GeneratingPolynomial 和 CRCLength。
• DefaultInitValueUse:此字段指示是使用默认 CRC 初始化值 (即 0xFFFF FFFF) 还是自定义值。它可以采用 DEFAULT_INIT_VALUE_ENABLE 或 DEFAULT_INIT_VALUE_DISABLE 的值。在最后一种情况下,必须设置字段 InitValue。
• GeneratingPolynomial:设置 CRC 生成多项式。7、8、16 或 32 位长值,表示多项式次数等于 7、8、16 或 32。该字段以正常表示形式书写,例如,对于 7 次多项式,X7 + X6 + X5 + X2 + 1 写0x65。
• CRCLength:此字段表示 CRC 的长度,它可以采用下表中的值。
• InitValue:设置自定义初始值以开始 CRC 计算。
• InputDataInversionMode:指定是否必须反转输入数据。它可以采下用中的值。
• OutputDataInversionMode:指定输出数据(计算的 CRC)是否必须反转。它可以采用值 CRC_OUTPUTDATA_INVERSION_DISABLE 和 CRC_OUTPUTDATA_INVERSION_ENABLE。在最后一种情况下,操作在位级别完成:例如,0x1122 3344 的输出数据转换为 0x22CC 4488。
一旦定义 CRC_InitTypeDef 结构体的实例,并正确填充其字段,我们通过调用函数来配置 CRC 外设:
HAL_StatusTypeDef HAL_CRC_Init(CRC_HandleTypeDef *hcrc);
要计算数据缓冲区的 CRC 校验和,我们使用函数:
uint32_t HAL_CRC_Calculate(CRC_HandleTypeDef *hcrc, uint32_t pBuffer[], uint32_t BufferLength);
它接受指向 uint32_t 数组及其长度的指针。该函数将默认 CRC 初始值设置为 0xFFFF FFFF,如果我们使用的是 STM32F0/F3/F7/L0/L4 MCU,则为指定值。相反,如果我们需要从先前计算的 CRC 作为初始值开始计算 CRC,那么我们可以使用函数:
uint32_t HAL_CRC_Accumulate(CRC_HandleTypeDef *hcrc, uint32_t pBuffer[], uint32_t BufferLength);
当我们使用小于源块的临时缓冲区计算大块数据的 CRC 校验和时,这尤其有用。