CRC(循环冗余校验)是一种广泛应用于数据通信和存储领域的错误检测技术。它的核心思想是通过多项式除法来生成一个固定长度的校验码,这个校验码可以用于验证数据的完整性。
CRC校验本质上是一种基于模2运算的校验方法。模2运算的特点是:
在CRC计算中,数据被视为一个大的二进制数,与一个预定义的多项式进行模2除法运算,得到的余数就是CRC校验值。这个多项式被称为生成多项式(Generator Polynomial),它的选择直接影响CRC的检错能力。
不同的CRC标准使用不同的生成多项式。以下是几种常见的CRC标准及其对应的多项式:
| CRC类型 | 多项式(十六进制) | 多项式(二进制) | 位宽 |
|---|---|---|---|
| CRC-7 | 0x89 | 10001001 | 7 |
| CRC-8 | 0x107 | 100000111 | 8 |
| CRC-16 | 0x11021 | 10001000000100001 | 16 |
| CRC-32 | 0x104C11DB7 | 100000100110000010001110110110111 | 32 |
这些多项式都是经过精心选择的,具有良好的错误检测特性。例如,CRC-32可以检测所有奇数个错误、所有双位错误,以及所有长度小于等于32位的突发错误。
直接计算法是最直观的CRC实现方式,它完全模拟了手工计算CRC的过程。其基本步骤如下:
这种方法的优点是直观易懂,缺点是效率较低,特别是对于大位宽的CRC(如CRC-32)和大数据量时。
让我们以CRC-7的实现为例,详细解析代码的工作原理:
c复制uint8_t calc_crc7(const void *data, int len)
{
const uint8_t *p = data;
int i, j;
uint16_t temp = 0;
if (len != 0)
temp |= p[0] << 8;
for (i = 1; i <= len; i++)
{
if (i != len)
temp |= p[i];
for (j = 0; j < 8; j++)
{
if (temp & 0x8000)
temp ^= POLYNOMIAL_CRC7 << 8;
temp <<= 1;
}
}
return temp >> 9;
}
这段代码的关键点在于:
temp来存储中间结果(CRC-7只需要7位,但为了处理方便使用了更大的空间)temp的高字节注意:这里多项式左移8位是为了对齐到
temp的高位,因为CRC-7的多项式本身只有7位。
虽然CRC-7、CRC-8、CRC-16和CRC-32的位宽不同,但它们的计算模式是相似的。主要区别在于:
使用的临时变量大小不同:
多项式的移位量不同:
最终结果的移位量不同:
这种统一的设计模式使得代码结构清晰,便于理解和维护。
在某些CRC标准中(如CRC-32/MPEG-2),要求对输入数据和输出结果进行反转(bit-reverse)。反转操作的主要目的是:
反转操作包括:
反转操作的核心是位反转函数。以下是8位和32位反转的实现:
c复制uint8_t reverse(uint8_t data)
{
int i;
uint8_t result = 0;
for (i = 0; i < 8; i++)
{
result <<= 1;
if (data & 1)
result |= 1;
data >>= 1;
}
return result;
}
uint32_t reverse32(uint32_t data)
{
uint32_t result;
result = reverse((data >> 24) & 0xff);
result |= reverse((data >> 16) & 0xff) << 8;
result |= reverse((data >> 8) & 0xff) << 16;
result |= reverse(data & 0xff) << 24;
return result;
}
反转式CRC32的计算流程与普通CRC32类似,但增加了反转和异或操作:
c复制uint32_t calc_crc32_rev(const void *data, int len)
{
const uint8_t *p = data;
int i, j;
uint64_t temp = INIT_CRC32 << 32;
if (len != 0)
temp ^= (uint64_t)reverse(p[0]) << 56;
// ... 其余数据处理 ...
for (i = 7; i <= len + 6; i++)
{
if (i < len)
temp |= reverse(p[i]);
// ... CRC计算 ...
}
return reverse32(temp >> 32) ^ 0xffffffff;
}
在某些嵌入式平台(如STM32的部分型号)上,可能不支持64位整数运算。这时可以将64位变量拆分为两个32位变量来实现相同的功能。
关键观察点:
c复制if (temp & 0x8000000000000000)
{
temp <<= 1;
temp ^= POLYNOMIAL_CRC32 << 32;
}
else
temp <<= 1;
可以等价转换为:
c复制msb = (temp_high & 0x80000000); // 获取高32位的最高位
temp_high = (temp_high << 1) | (temp_low >> 31); // 整体左移
temp_low <<= 1;
if (msb)
temp_high ^= POLYNOMIAL_CRC32; // 异或操作
优化后的实现使用两个32位变量代替一个64位变量:
c复制uint32_t calc_crc32(const void *data, int len)
{
const uint8_t *p = data;
int i, j;
uint32_t temp = 0;
uint32_t temp2 = 0;
uint32_t msb;
// 数据填充
if (len != 0)
temp |= p[0] << 24;
if (len > 1)
temp |= p[1] << 16;
if (len > 2)
temp |= p[2] << 8;
if (len > 3)
temp |= p[3];
if (len > 4)
temp2 |= p[4] << 24;
if (len > 5)
temp2 |= p[5] << 16;
if (len > 6)
temp2 |= p[6] << 8;
for (i = 7; i <= len + 6; i++)
{
if (i < len)
temp2 |= p[i];
for (j = 0; j < 8; j++)
{
msb = (temp & 0x80000000);
temp <<= 1;
if (temp2 & 0x80000000)
temp |= 1;
temp2 <<= 1;
if (msb)
temp ^= POLYNOMIAL_CRC32;
}
}
return temp;
}
在64位实现中,多项式需要左移31位(因为CRC-32的多项式是33位,最高位总是1)。而在32位实现中,由于我们将计算限制在32位内,可以省略多项式的最高位,使用0x04C11DB7而不是0x104C11DB7。
这是因为:
省略最高位不会影响计算结果,因为在模2运算中,最高位的1已经隐含在算法的判断条件中(当最高位为1时才进行异或操作)。
为了验证CRC实现的正确性,我们需要设计全面的测试用例:
可以使用在线CRC计算工具来验证我们的实现是否正确。例如:
这些工具通常支持多种CRC标准和参数设置,可以方便地进行交叉验证。
虽然直接计算法实现简单,但在处理大量数据时可能效率不高。在实际应用中,可以考虑以下优化策略:
对于资源受限的嵌入式系统,需要在代码大小、内存占用和计算速度之间找到平衡点。
在嵌入式开发中,我经常遇到CRC校验不匹配的问题。一个实用的技巧是创建一个包含所有参数(多项式、初始值、输入输出反转等)的结构体,这样可以方便地切换不同配置进行测试。另外,在STM32项目中,如果同时使用硬件CRC和软件CRC,要特别注意它们的默认参数可能不同,需要仔细对照数据手册。