CRC的全称为CyclicRedundancyCheck,中文名称为循环冗余校验。它是一类重要的线性分组码,编码和解码方法简单,检错和纠错能力强,在通信领域广泛地用于实现差错控制。实际上,除数据通信外,CRC在其它很多领域也是大有用武之地的。例如我们读软盘上的文件,以及解压一个ZIP文件时,偶尔会碰到“BadCRC”错误,由此它在数据存储方面的应用可略见一斑。
差错控制理论是在代数理论基础上建立起来的。这里我们着眼于介绍CRC的算法与实现,对原理只能捎带说明一下。若需要进一步了解线性码、分组码、循环码、纠错编码等方面的原理,可以阅读有关资料。
利用CRC进行检错的过程可简单描述为:在发送端根据要传送的k位二进制码序列,以一定的规则产生一个校验用的r位监督码(CRC码),附在原始信息后边,构成一个新的二进制码序列数共k+r位,然后发送出去。在接收端,根据信息码和CRC码之间所遵循的规则进行检验,以确定传送中是否出错。这个规则,在差错控制理论中称为“生成多项式”。
1代数学的一般性算法
在代数编码理论中,将一个码组表示为一个多项式,码组中各码元当作多项式的系数。例如1100101表示为
1·x6+1·x5+0·x4+0·x3+1·x2+0·x+1,即x6+x5+x2+1。
设编码前的原始信息多项式为P(x),P(x)的最高幂次加1等于k;生成多项式为G(x),G(x)的最高幂次等于r;CRC多项式为R(x);编码后的带CRC的信息多项式为T(x)。
发送方编码方法:将P(x)乘以xr(即对应的二进制码序列左移r位),再除以G(x),所得余式即为R(x)。用公式表示为
T(x)=xrP(x)+R(x)
接收方解码方法:将T(x)除以G(x),如果余数为0,则说明传输中无错误发生,否则说明传输有误。
举例来说,设信息码为1100,生成多项式为1011,即P(x)=x3+x2,G(x)=x3+x+1,计算CRC的过程为
xrP(x)x3(x3+x2)x6+x5x
--------=----------=--------=(x3+x2+x)+--------
G(x)x3+x+1x3+x+1x3+x+1
即R(x)=x。注意到G(x)最高幂次r=3,得出CRC为010。
如果用竖式除法,计算过程为
1110
-------
1011/1100000(1100左移3位)
1011
----
1110
1011
-----
1010
1011
-----
0010
0000
----
010
因此,T(x)=(x6+x5)+(x)=x6+x5+x,即1100000+010=1100010
如果传输无误,
T(x)x6+x5+x
------=---------=x3+x2+x,
G(x)x3+x+1
无余式。回头看一下上面的竖式除法,如果被除数是1100010,显然在商第三个1时,就能除尽。
上述推算过程,有助于我们理解CRC的概念。但直接编程来实现上面的算法,不仅繁琐,效率也不高。实际上在工程中不会直接这样去计算和验证CRC。
下表中列出了一些见于标准的CRC资料:
名称 | 生成多项式 | 简记式* | 应用举例 |
CRC-4 | x4+x+1 | ITUG.704 | |
CRC-12 | x12+x11+x3+x+1 | ||
CRC-16 | x16+x12+x2+1 | 1005 | IBMSDLC |
CRC-ITU** | x16+x12+x5+1 | 1021 | ISOHDLC,ITUX.25,V.34/V.41/V.42,PPP-FCS |
CRC-32 | x32+x26+x23+...+x2+x+1 | 04C11DB7 | ZIP,RAR,IEEE802LAN/FDDI,IEEE1394,PPP-FCS |
CRC-32c | x32+x28+x27+...+x8+x6+1 | 1EDC6F41 | SCTP |
*生成多项式的最高幂次项系数是固定的1,故在简记式中,将最高的1统一去掉了,
如04C11DB7实际上是104C11DB7。
**前称CRC-CCITT。ITU的前身是CCITT。
4.CRC算法的实现
---------------
要用程序实现CRC算法,考虑对第2节的长除法做一下变换,依然是M=11100110,G=1011,
其系数r为3。
11001100
------------------------
1011)11100110000
1011.......
----.......
1010......
1011......
----......
1110...
1011...
------...
1010..
1011..
-------
100<---校验码
程序可以如下实现:
1)将Mx^r的前r位放入一个长度为r的寄存器;
2)如果寄存器的首位为1,将寄存器左移1位(将Mx^r剩下部分的MSB移入寄存器的LSB),
再与G的后r位异或,否则仅将寄存器左移1位(将Mx^r剩下部分的MSB移入寄存器的LSB);
3)重复第2步,直到M全部Mx^r移入寄存器;
4)寄存器中的值则为校验码。
基于以上算法,我们可以看一下上面例子的程序计算过程:(r=3)
首先,11100110000前三位进入寄存器,即111
这时寄存器首位为1,执行第2步,移位成1100110000,这时寄存器中为前三位110,将其与011(生成多项式后三位)异或,得1010110000.
然后继续第2步,101首位为1,移位010110000,然后010与011异或,得001110000
前面两个0,连续以为2次且不用计算异或,得1110000,接着移位110000,异或得101000
第一位为1,移位得01000,前三位异或得00100
最后因为前面两个0,直接移位两次后得寄存器中的内容100,这时Mx^r位的所有内容都移入寄存器,运算结束,记得检验码为100。(关键先判断首位是否为1,然后移位,然后计算)
11100110000移位->11100110000
011
1010110000-->101第一位为1,移位且计算
1010110000
011
001110000-->001第一位第二位均为0,移位2次
001110000-->111第一位为1,移位且计算
1110000
011
101000-->101第一位为1,移位且计算
101000
011
00100-->移位2次得100
unsignedshortdo_crc(unsignedchar*message,unsignedintlen)
{
inti,j;
unsignedshortcrc_reg;
crc_reg=(message[0]<<8)+message[1];
for(i=0;i<len;i++)
{
if(i<len-2)
for(j=0;j<=7;j++)
{
if((short)crc_reg<0)
crc_reg=((crc_reg<<1)+(message[i+2]>>(7-i)))^0x1021;
else
crc_reg=(crc_reg<<1)+(message[i+2]>>(7-i));
}
else
for(j=0;j<=7;j++)
{
if((short)crc_reg<0)
crc_reg=(crc_reg<<1)^0x1021;
else
crc_reg<<=1;
}
}
returncrc_reg;
}
显然,每次内循环的行为取决于寄存器首位。由于异或运算满足交换率和结合律,以及与0异或无影响,消息可以不移入寄存器,而在每次内循环的时候,寄存器首位再与对应的消息位异或。改进的代码如下:
unsignedshortdo_crc(unsignedchar*message,unsignedintlen)
{
inti,j;
unsignedshortcrc_reg=0;
unsignedshortcurrent;
for(i=0;i<len;i++)
{
current=message[i]<<8;
for(j=0;j<8;j++)
{
if((short)(crc_reg^current)<0)
crc_reg=(crc_reg<<1)^0x1021;
else
crc_reg<<=1;
current<<=1;
}
}
returncrc_reg;
}
以上的讨论中,消息的每个字节都是先传输MSB,CRC16-CCITT标准却是按照先传输LSB,消息右移进寄存器来计算的。只需将代码改成判断寄存器的LSB,将0x1021按位颠倒后(0x8408)与寄存器异或即可,如下所示:
unsignedshortdo_crc(unsignedchar*message,unsignedintlen)
{
inti,j;
unsignedshortcrc_reg=0;
unsignedshortcurrent;
for(i=0;i<len;i++)
{
current=message[i];
for(j=0;j<8;j++)
{
if((crc_reg^current)&0x0001)
crc_reg=(crc_reg>>1)^0x8408;
else
crc_reg>>=1;
current>>=1;
}
}
returncrc_reg;
}
该算法使用了两层循环,对消息逐位进行处理,这样效率是很低的。为了提高时间效率,通常的思想是以空间换时间。考虑到内循环只与当前的消息字节和crc_reg的低字节有关,对该算法做以下等效转换:
unsignedshortdo_crc(unsignedchar*message,unsignedintlen)
{
inti,j;
unsignedshortcrc_reg=0;
unsignedcharindex;
unsignedshortto_xor;
for(i=0;i<len;i++)
{
index=(crc_reg^message[i])&0xff;
to_xor=index;
for(j=0;j<8;j++)
{
if(to_xor&0x0001)
to_xor=(to_xor>>1)^0x8408;
else
to_xor>>=1;
}
crc_reg=(crc_reg>>8)^to_xor;
}
returncrc_reg;
}
现在内循环只与index相关了,可以事先以数组形式生成一个表crc16_ccitt_table,使得to_xor=crc16_ccitt_table[index],于是可以简化为:
unsignedshortdo_crc(unsignedchar*message,unsignedintlen)
{
unsignedshortcrc_reg=0;
while(len--)
crc_reg=(crc_reg>>8)^crc16_ccitt_table[(crc_reg^*message++)&0xff];
returncrc_reg;
}
crc16_ccitt_table通过以下代码生成:
intmain()
{
unsignedcharindex=0;
unsignedshortto_xor;
inti;
printf("unsignedshortcrc16_ccitt_table[256]=\n{");
while(1)
{
if(!(index%8))
printf("\n");
to_xor=index;
for(i=0;i<8;i++)
{
if(to_xor&0x0001)
to_xor=(to_xor>>1)^0x8408;
else
to_xor>>=1;
}
printf("0x%04x",to_xor);
if(index==255)
{
printf("\n");
break;
}
else
{
printf(",");
index++;
}
}
printf("};");
return0;
}
生成的表如下:
unsignedshortcrc16_ccitt_table[256]=
{
0x0000,0x1189,0x2312,0x329b,0x4624,0x57ad,0x6536,0x74bf,
0x8c48,0x9dc1,0xaf5a,0xbed3,0xca6c,0xdbe5,0xe97e,0xf8f7,
0x1081,0x0108,0x3393,0x221a,0x56a5,0x472c,0x75b7,0x643e,
0x9cc9,0x8d40,0xbfdb,0xae52,0xdaed,0xcb64,0xf9ff,0xe876,
0x2102,0x308b,0x0210,0x1399,0x6726,0x76af,0x4434,0x55bd,
0xad4a,0xbcc3,0x8e58,0x9fd1,0xeb6e,0xfae7,0xc87c,0xd9f5,
0x3183,0x200a,0x1291,0x0318,0x77a7,0x662e,0x54b5,0x453c,
0xbdcb,0xac42,0x9ed9,0x8f50,0xfbef,0xea66,0xd8fd,0xc974,
0x4204,0x538d,0x6116,0x709f,0x0420,0x15a9,0x2732,0x36bb,
0xce4c,0xdfc5,0xed5e,0xfcd7,0x8868,0x99e1,0xab7a,0xbaf3,
0x5285,0x430c,0x7197,0x601e,0x14a1,0x0528,0x37b3,0x263a,
0xdecd,0xcf44,0xfddf,0xec56,0x98e9,0x8960,0xbbfb,0xaa72,
0x6306,0x728f,0x4014,0x519d,0x2522,0x34ab,0x0630,0x17b9,
0xef4e,0xfec7,0xcc5c,0xddd5,0xa96a,0xb8e3,0x8a78,0x9bf1,
0x7387,0x620e,0x5095,0x411c,0x35a3,0x242a,0x16b1,0x0738,
0xffcf,0xee46,0xdcdd,0xcd54,0xb9eb,0xa862,0x9af9,0x8b70,
0x8408,0x9581,0xa71a,0xb693,0xc22c,0xd3a5,0xe13e,0xf0b7,
0x0840,0x19c9,0x2b52,0x3adb,0x4e64,0x5fed,0x6d76,0x7cff,
0x9489,0x8500,0xb79b,0xa612,0xd2ad,0xc324,0xf1bf,0xe036,
0x18c1,0x0948,0x3bd3,0x2a5a,0x5ee5,0x4f6c,0x7df7,0x6c7e,
0xa50a,0xb483,0x8618,0x9791,0xe32e,0xf2a7,0xc03c,0xd1b5,
0x2942,0x38cb,0x0a50,0x1bd9,0x6f66,0x7eef,0x4c74,0x5dfd,
0xb58b,0xa402,0x9699,0x8710,0xf3af,0xe226,0xd0bd,0xc134,
0x39c3,0x284a,0x1ad1,0x0b58,0x7fe7,0x6e6e,0x5cf5,0x4d7c,
0xc60c,0xd785,0xe51e,0xf497,0x8028,0x91a1,0xa33a,0xb2b3,
0x4a44,0x5bcd,0x6956,0x78df,0x0c60,0x1de9,0x2f72,0x3efb,
0xd68d,0xc704,0xf59f,0xe416,0x90a9,0x8120,0xb3bb,0xa232,
0x5ac5,0x4b4c,0x79d7,0x685e,0x1ce1,0x0d68,0x3ff3,0x2e7a,
0xe70e,0xf687,0xc41c,0xd595,0xa12a,0xb0a3,0x8238,0x93b1,
0x6b46,0x7acf,0x4854,0x59dd,0x2d62,0x3ceb,0x0e70,0x1ff9,
0xf78f,0xe606,0xd49d,0xc514,0xb1ab,0xa022,0x92b9,0x8330,
0x7bc7,0x6a4e,0x58d5,0x495c,0x3de3,0x2c6a,0x1ef1,0x0f78
};
这样对于消息unsignedcharmessage[len],校验码为:
unsignedshortcode=do_crc(message,len);
并且按以下方式发送出去:
message[len]=code&0x00ff;
message[len+1]=(code>>8)&0x00ff;
接收端对收到的len+2字节执行do_crc,如果没有差错发生则结果应为0。
在一些传输协议中,发送端并不指出消息长度,而是采用结束标志,考虑以下几种差错:
1)在消息之前,增加1个或多个0字节;
2)消息以1个或多个连续的0字节开始,丢掉1个或多个0;
3)在消息(包括校验码)之后,增加1个或多个0字节;
4)消息(包括校验码)以1个或多个连续的0字节结尾,丢掉1个或多个0;
显然,这几种差错都检测不出来,其原因就是如果寄存器值为0,处理0消息字节(或位),寄存器值不变。为了解决前2个问题,只需寄存器的初值非0即可,对do_crc作以下改进:
unsignedshortdo_crc(unsignedshortreg_init,unsignedchar*message,unsignedintlen)
{
unsignedshortcrc_reg=reg_init;
while(len--)
crc_reg=(crc_reg>>8)^crc16_ccitt_table[(crc_reg^*message++)&0xff];
returncrc_reg;
}
在CRC16-CCITT标准中reg_init=0xffff,为了解决后2个问题,在CRC16-CCITT标准中将计算出的校验码与0xffff进行异或,即:
unsignedshortcode=do_crc(0xffff,message,len);
code^=0xffff;
message[len]=code&0x00ff;
message[len+1]=(code>>8)&0x00ff;
显然,现在接收端对收到的所有字节执行do_crc,如果没有差错发生则结果应为某一常值GOOD_CRC。其满足以下关系:
unsignedcharp[]={0xff,0xff};
GOOD_CRC=do_crc(0,p,2);
其结果为GOOD_CRC=0xf0b8。
在同一程序中验证如下(放在main函数中可试验):
unsignedcharp[]={0xa0,0xb0,0xff,0xff};
unsignedshortcrc;
crc=do_crc(0xffff,p,2);//计算前两位的CRC码
crc^=0xffff;//对其取反
p[2]=crc&0x00ff;//将计算的CRC码加到信息序列后面
p[3]=crc>>8&0x00ff;
printf("p[2]=%x,p3=%x\n",p[2],p[3]);
crc=do_crc(0xffff,p,4);//对信息码+CRC码共同计算得出CRC=0xf0b8
printf("crcis%x\n",crc);
假设发送的信息是p[0],p[1];低位先发,对其计算的CRC加到信息码后面
然后对信息码+CRC码共同计算CRC值,此时应该是常数0xf0b8。不管信息码如何变化,内容和长度都可变,只要把计算的CRC码加进去一起计算CRC,就应该是得该常数GOOD_CRC。
--------
[1]RossN.Williams,"APAINLESSGUIDETOCRCERRORDETECTIONALGORITHMS",Version3,
http://www.ross.net/crc/crcpaper.html,August1993
[2]Simpson,W.,Editor,"PPPinHDLCFraming",RFC1549,December1993
[3]P.E.Boudreau,W.C.BergmanandD.R.lrvin,"Performanceofacyclicredundancycheckanditsinteractionwithadatascrambler",IBMJ.RES.DEVELOP.,VOL.38NO.6,November1994