CRC(循环冗余校验)见解
CRC算法深入原理已有阐述:CRC算法原理.
本文围绕实用CRC算法为核心, 顺带结合个人见解深入浅出CRC原理.
CRC用途
CRC常用来定性的校验一堆数据(bit)是否完整 or 发生了变化, 它能反映一堆数据(bit)的特征, 而且它的校验方法错误率低, 而且特别高效和在硬件容易实现. 位数有限的情况下, 一堆数据(bit)和另一堆数据(bit)的CRC是有可能恰好相同的
CRC相关的概念
不妨从高斯的本源多项式讲起, 不必慌, 懂除法就看得懂.
生成多项式
这是各类CRC至关重要的参数. 我们可以理解为它将扮演 接下来的介绍的CRC核心原理二进制除法中"除数"角色, 类似与普通的除法, 我们希望找到一些除数(多项式)能够尽量不整除任意给定的数(多项式), 不整除就意味有余数, 而余数则与任意给定的数相关, 从而反映出任意给定的数的特征. 在代数学中, 除法被推广到多项式, 无独有偶, 高斯发现的本源多项式就是 我们想找的多项式的一部分. 当然, 生成多项式不都是本源多项式, 只要满足一定条件即可, 这里就不再深入.
本源多项式是唯一分解整环上满足所有系数的最大公因数为1的多项式. (逃…)
一些本源多项式, 理论上我们可以选择其中一个作为CRCn的生成多项式, 比如:
CRC3可以选择:
f
(
x
)
=
x
3
+
x
1
+
1
f(x)= x^3+x^1+1
f(x)=x3+x1+1
CRC7可以选择:
f
(
x
)
=
x
7
+
x
3
+
1
f(x)= x^7+x^3+1
f(x)=x7+x3+1
更多通用CRC生成多项式可以查看CRC(循环冗余校验)在线计算. 这是个非常实用的网站工具, 也是个卖鱼的好地方.
我们处理的数据是以二进制的形式储存的. 上述的多项式骤眼看似毫无关联. 不妨把 x = 2 x=2 x=2代入 f ( x ) = x 7 + x 3 + 1 f(x)= x^7+x^3+1 f(x)=x7+x3+1得:
f ( 2 ) = 2 7 + 2 3 + 1 f(2)= 2^7+2^3+1 f(2)=27+23+1
也就不难用二进制和16进制表示:
f ( 2 ) = 10001001 B = 89 H f(2)=10001001B=89H f(2)=10001001B=89H
二进制除法(异或除法)
读者应熟知异或. 该除法与整数除法最大的区别是:
- 不必相减,以异或代替过程中的减法
- 除法过程的终止条件不同.
举两个二进制除法(
÷
\div
÷)的栗子:(CRC7, 生成多项式:
1000
1001
B
1000\,1001B
10001001B )
(1)
1
B
÷
1000
1001
B
1\!B\div1000\,1001\!B
1B÷10001001B
(2)
1010
B
÷
1000
1001
B
1010\!B \div 1000\,1001\!B
1010B÷10001001B
用"异或"替代减法的好处就是避免了减法之间进退位的细节操作, 硬件极易实现, 所以效率极高. 最重要的是它依旧保留了整数除法的一些好的性质, 不仅余数能反映输入数据特征, 而且还能巧妙利用余数的位置实现简易的CRC校验.
- 余数与输入数据相关
类似于一般的除法, 针对有规律递增的被除数, 取模(%)运算得到的余数也是周而复始的递增出现的有限个余数(同余系).
123%97 = 26
124%97 = 27
125%97 = 28
…
193%97 = 96
194%97=0
195%97=1
再来看看CRC7的情况, 其中的7代表校验码的长度为7bit, 也就意味着余数最多只有128个.
c r c ( 00 H ) = 00 H crc(00H)=00H crc(00H)=00H
c r c ( 01 H ) = 09 H crc(01H)=09H crc(01H)=09H
c r c ( 02 H ) = 12 H crc(02H)=12H crc(02H)=12H
…
c r c ( 88 H ) = 09 H crc(88H)=09H crc(88H)=09H
c r c ( 89 H ) = 00 H crc(89H)=00H crc(89H)=00H
c r c ( 8 A H ) = 1 B H crc(8AH)=1BH crc(8AH)=1BH
c r c ( 8 B H ) = 12 H crc(8BH)=12H crc(8BH)=12H
由此可以看出二进制除法虽然丢失了有序性, 但得益于生成多项式的遵循的一些规则, 对于递增的前128个校验得出的结果均不相同.
-
巧妙摆放余数的位置实现校验
整数除法,有这样的规律:(%代表取模)( 被 除 数 + 余 数 ) % 除 数 = 0 (被除数+余数)\%除数=0 (被除数+余数)%除数=0
CRC的二进制除法也有类似规律, 而且更加简单粗暴:
将 CRC校验得出的7位bit添加到待校验数据的末尾得到一个整体, 再对该整体再进行一次CRC校验. 若结果为0, 则通过校验, 反之不通过.
例如:
这里是引用待校验的数据: 1010B
CRC7校验码: 1011010B
组成整体: 10101011010B
于是有:
c r c 7 ( 10101011010 B ) = 0 crc7(10101011010B)=0 crc7(10101011010B)=0
也就是
c r c ( 待 校 验 数 据 + c r c 码 ) = 0 crc(待校验数据+crc码)=0 crc(待校验数据+crc码)=0
这也不难解释, 为什么通讯协议对CRC情有独钟, 接收信号帧连提取校验码步骤都给省了, 直接扔CRC校验可不香么?
关于其它的CRC相关概念, 比如初始值, 结果异或等概念就比较浅显了, 读者可以自行了解
算法实例
(最近比较忙, 详细解析还没写. 具体细节预计写到注释里)
CRC8
生成多项式: x 8 + x 2 + x + 1 x^8+x^2+x+1 x8+x2+x+1
uint8 calcCRC8(uint8* vptr, uint8 len)
{
const uint8* data = vptr;
unsigned short crc = 0;
int i, j;
for (j = len; j; j--, data++) {
// 这里把运算区域左移了8位,是因为生成多项式0x107(1 0000 0111)
// 的最高位恰好在第9位. 一个字节够不着. 干脆就把 被除数和除数 都
// 左移到一个适合左移的位置, 即:
// xxxx xxxx 0000 0000 (*data<<8)
// 1000 0000 0000 0000 (0x8000)
crc ^= (*data << 8);
for (i = 8; i; i--) {
if (crc & 0x8000){
// 0x07对应的是生成多项式0x107的略写, 因为 0x107 = 0x100 | 0x07
// 这里若 crc&0x8000 == 1, 相当于假定了crc的最高位已经被左移丢弃掉了,
// 所以直接进行0x07的异或.
crc ^= (0x07 << 7);
}
crc <<= 1;
}
}
return (uint8)((crc >> 8) &0xFF);
}
CRC7
生成多项式: x 7 + x 3 + 1 x^7+x^3+1 x7+x3+1
typedef unsigned char uint8;
//CRC-8 x8+x2+x+1
uint8 calcCRC7(uint8 *data, int len){
uint8 crc = 0;
for (int i = 0; i < len; i++)
{
crc ^= data[i];
for (int j = 0; j < 8; j++)
{
if (crc & 0x80)
crc ^= 0x89;
crc <<= 1;
}
}
crc >>= 1;
return crc;
}
CRC3
查表法
重用上述CRC7算法为例. 其实就是把给定"除数"对应的有限个余数算出来, 存起来. 然后施加著名的算法规律:用空间换时间. 直接用索引代替计算.
生成表方法
7bit最多有128种组合, 可以一一对应0-127:
void makeCRC7Table()
{
putchar('{');
for (int i = 0; i <= 0x7F; i++)
{
uint8 tmp = i & 0xFF;
printf("0x%02X", calcCRC7(&tmp, 1));
if (i != 0x7F)
putchar(',');
else {
printf("}\n");
return;
}
if (i!=0 && ((i+1)%32 == 0))
printf("\n");
}
}
查询表方法
(施工中…)
Emm…自己写算法好像卡在了对数据bit分块的地方. 如果能给定bit长度还好写, 但是给定字节数组和字节长度, 好像遇到了瓶颈.
CRC8, CRC32网上已有完善的版本. 读者急用请再找找.