分享

CRC校验算法_苗彦朋

 大老渊 2016-06-14
1.生成多项式。
16位的CRC码产生的规则是先将要发送的二进制序列数左移16位(既乘以
)后,再除以一个多项式,最后所得到的余数既是CRC码。任意一个由二进制位串组成的代码都可以和一个系数仅为‘0’和‘1’取值的多项式一一对应。例如:代码1010111对应的多项式为x6+x4+x2+x+1,而多项式为x5+x3+x2+x+1对应的代码101111。
标准CRC生成多项式如下表:
名称 生成多项式 简记式* 标准引用
CRC-4 x4+x+1 3 ITU G.704
CRC-8 x8+x5+x4+1 0x31
CRC-8 x8+x2+x1+1 0x07
CRC-8 x8+x6+x4+x3+x2+x1 0x5E
CRC-12 x12+x11+x3+x+1 80F
CRC-16 x16+x15+x2+1 8005 IBM SDLC
CRC16-CCITT x16+x12+x5+1 1021 ISO HDLC, ITU X.25, V.34/V.41/V.42,PPP-FCS
CRC-32 x32+x26+x23+...+x2+x+1 04C11DB7 ZIP, RAR, IEEE 802 LAN/FDDI,IEEE 1394, PPP-FCS
CRC-32c x32+x28+x27+...+x8+x6+1 1EDC6F41 SCTP//叶子:这里不知道问什么省略了,有些迷惑哦。要是生成多项式要是都省了,那还怎么校验?我猜想可能是中间的全为一吧。
生成多项式的最高位固定的1,故在简记式中忽略最高位1了,如0
x1021实际是0x11021。
I、基本算法(人工笔算):
以CRC16-CCITT为例进行说明,CRC校验码为16位,生成多项式17位。假如数据流为4字节:BYTE[3]、BYTE[2]、BYTE[1]、BYTE[0];
数据流左移16位,相当于扩大256×256倍,再除以生成多项式0x11021,做不借位的除法运算(相当于按位异或),所得的余数就是CRC校验码。
发送时的数据流为6字节:BYTE[3]、BYTE[2]、BYTE[1]、BYTE[0]、CRC[1]、CRC[0];
II、计算机算法1(比特型算法):
1)将扩大后的数据流(6字节)高16位(BYTE[3]、BYTE[2])放入一个长度为16的寄存器;
2)如果寄存器的首位为1,将寄存器左移1位(寄存器的最低位从下一个字节获得),再与生成多项式的简记式异或;
否则仅将寄存器左移1位(寄存器的最低位从下一个字节获得);
3)重复第2步,直到数据流(6字节)全部移入寄存器;
4)寄存器中的值则为CRC校验码CRC[1]、CRC[0]。
III、计算机算法2(字节型算法):256^n表示256的n次方
把按字节排列的数据流表示成数学多项式,设数据流为BYTE[n]BYTE[n-1]BYTE[n-2]、、、BYTE[1]BYTE[0],表示成数学表达式为BYTE[n]×256^n+BYTE[n-1]×256^(n-1)
+...+BYTE[1]*256+BYTE[0],在这里+表示为异或运算。设生成多项式为G17(17bit),CRC码为CRC16。
则,CRC16=(BYTE[n]×256^n+BYTE[n-1]×256^(n-1)+...+BYTE[1]×256+BYTE[0])×256^2/G17,即数据流左移16位,再除以生成多项式G17。
先变换BYTE[n-1]、BYTE[n-1]扩大后的形式,
CRC16=BYTE[n]×256^n×256^2/G17+BYTE[n-1]×256^(n-1)×256^2/G17+...+BYTE[1]×256×256^2/G17+BYTE[0]×256^2/G17
=(Z[n]+Y[n]/G17)×256^n+BYTE[n-1]×256^(n-1)×256^2/G17+...+BYTE[1]×256×256^2/G17+BYTE[0]×256^2/G17
=Z[n]×256^n+{Y[n]×256/G17+BYTE[n-1]×256^2/G17}×256^(n-1)+...+BYTE[1]×256×256^2/G17+BYTE[0]×256^2/G17
=Z[n]×256^n+{(YH8[n]×256+YHL[n])×256/G17+BYTE[n-1]×256^2/G17}×256^(n-1)+...+BYTE[1]×256×256^2/G17+BYTE[0]×256^2/G17
=Z[n]×256^n+{YHL[n]×256/G17+(YH8[n]+BYTE[n-1])×256^2/G17}×256^(n-1)+...+BYTE[1]×256×256^2/G17+BYTE[0]×256^2/G17
这样就推导出,BYTE[n-1]字节的CRC校验码为{YHL[n]×256/G17+(YH8[n]+BYTE[n-1])×256^2/G17},即上一字节CRC校验码Y[n]的高8位(YH8[n])与本字节BYTE[n-1]异或,
该结果单独计算CRC校验码(即单字节的16位CRC校验码,对单字节可建立表格,预先生成对应的16位CRC校验码),所得的CRC校验码与上一字节CRC校验码Y[n]的低8位(YL8[n])
乘以256(即左移8位)异或。然后依次逐个字节求出CRC,直到BYTE[0]。
字节型算法的一般描述为:本字节的CRC码,等于上一字节CRC码的低8位左移8位,与上一字节CRC右移8位同本字节异或后所得的CRC码异或。
字节型算法如下:
1)CRC寄存器组初始化为全'0'(0x0000)。(注意:CRC寄存器组初始化全为1时,最后CRC应取反。)
2)CRC寄存器组向左移8位,并保存到CRC寄存器组。
3)原CRC寄存器组高8位(右移8位)与数据字节进行异或运算,得出一个指向值表的索引。
4)索引所指的表值与CRC寄存器组做异或运算。
5)数据指针加1,如果数据没有全部处理完,则重复步骤2)。
6)得出CRC。
unsigned short GetCrc_16(unsigned char * pData, int nLength)
//函数功能:计算数据流* pData的16位CRC校验码,数据流长度为nLength
{
unsigned short cRc_16 = 0x0000; // 初始化
while(nLength>0)
{
cRc_16 = (cRc_16 < 8)="" ^="" crctable_16[((crc_16="">>8) ^*pData) & 0xff]; //cRctable_16表由函数mK_cRctable生成
nLength--;
pData++;
}
return cRc_16;
}
void mK_cRctable(unsigned short gEnpoly)
//函数功能:生成0-255对应的16CRC校验码,其实就是计算机算法1(比特型算法)
//gEnpoly为生成多项式
//注意,低位先传送时,生成多项式应反转(低位与高位互换)。如CRC16-CCITT为0x1021,反转后为0x8408
{
unsigned short cRc_16=0;
unsigned short i,j,k;
for(i=0,k=0;i<>
{
cRc_16 = i<>
for(j=8;j>0;j--)
{
if(cRc_16&0x8000) //反转时cRc_16&0x0001
cRc_16=(cRc_16<=1)^genpoly; 反转时crc_16="(cRc_16">>=1)^gEnpoly
else
cRc_16<=1; 反转时crc_16="">>=1
}
cRctable_16[k] = cRc_16;
}
}
Cyclic RedundancyCheck循环冗余检验,是基于数据计算一组效验码,用于核对数据传输过程中是否被更改或传输错误。
算法原理
假设数据传输过程中需要发送15位的二进制信息g=101001110100001,这串二进制码可表示为代数多项式g(x) = x^14+ x^12 + x^9 + x^8 + x^7 + x^5 +1,其中g中第k位的值,对应g(x)中x^k的系数。将g(x)乘以x^m,既将g后加m个0,然后除以m阶多项式h(x),得到的(m-1)阶余项r(x)对应的二进制码r就是CRC编码。
h(x)可以自由选择或者使用国际通行标准,一般按照h(x)的阶数m,将CRC算法称为CRC-m,比如CRC-32、CRC-64等。国际通行标准可以参看http://en./wiki/Cyclic_redundancy_check
g(x)和h(x)的除运算,可以通过g和h做xor(异或)运算。比如将11001与10101做xor运算:
CRC校验原理(二) - Ryan - Ryans Fund
明白了xor运算法则后,举一个例子使用CRC-8算法求101001110100001的效验码。CRC-8标准的h(x) = x^8 +x^7 + x^6 + x^4 + x^2 + 1,既h是9位的二进制串111010101。
CRC校验原理(二) - Ryan - Ryans Fund
经过迭代运算后,最终得到的r是10001100,这就是CRC效验码。
通过示例,可以发现一些规律,依据这些规律调整算法:
1.每次迭代,根据gk的首位决定b,b是与gk进行运算的二进制码。若gk的首位是1,则b=h;若gk的首位是0,则b=0,或者跳过此次迭代,上面的例子中就是碰到0后直接跳到后面的非零位。
CRC校验原理(二) - Ryan - Ryans Fund
2.每次迭代,gk的首位将会被移出,所以只需考虑第2位后计算即可。这样就可以舍弃h的首位,将b取h的后m位。比如CRC-8的h是111010101,b只需是11010101。
CRC校验原理(二) - Ryan - Ryans Fund
3.每次迭代,受到影响的是gk的前m位,所以构建一个m位的寄存器S,此寄存器储存gk的前m位。每次迭代计算前先将S的首位抛弃,将寄存器左移一位,同时将g的后一位加入寄存器。若使用此种方法,计算步骤如下:
CRC校验原理(二) - Ryan - Ryans Fund
※蓝色表示寄存器S的首位,是需要移出的,b根据S的首位选择0或者h。黄色是需要移入寄存器的位。S'是经过位移后的S。
查表法
同样是上面的那个例子,将数据按每4位组成1个block,这样g就被分成6个block。
CRC校验原理(二) - Ryan - Ryans Fund
下面的表展示了4次迭代计算步骤,灰色背景的位是保存在寄存器中的。
CRC校验原理(二) - Ryan - Ryans Fund
经4次迭代,B1被移出寄存器。被移出的部分,不我们关心的,我们关心的是这4次迭代对B2和B3产生了什么影响。注意表中红色的部分,先作如下定义:
B23 = 00111010
b1 = 00000000
b2 = 01010100
b3 = 10101010
b4 = 11010101
b' = b1 xor b2 xor b3 xor b4
4次迭代对B2和B3来说,实际上就是让它们与b1,b2,b3,b4做了xor计算,既:
B23 xor b1 xor b2 xor b3 xor b4
可以证明xor运算满足交换律和结合律,于是:
B23 xor b1 xor b2 xor b3 xor b4 = B23 xor (b1 xor b2 xor b3 xor b4)= B23 xor b'
b1是由B1的第1位决定的,b2是由B1迭代1次后的第2位决定(既是由B1的第1和第2位决定),同理,b3和b4都是由B1决定。通过B1就可以计算出b'。另外,B1由4位组成,其一共2^4有种可能值。于是我们就可以想到一种更快捷的算法,事先将b'所有可能的值,16个值可以看成一个表;这样就可以不必进行那4次迭代,而是用B1查表得到b'值,将B1移出,B3移入,与b'计算,然后是下一次迭代。
CRC校验原理(二) - Ryan - Ryans Fund
可看到每次迭代,寄存器中的数据以4位为单位移入和移出,关键是通过寄存器前4位查表获得
,这样的算法可以大大提高运算速度。
上面的方法是半字节查表法,另外还有单字节和双字节查表法,原理都是一样的——事先计算出2^8或2^16个b'的可能值,迭代中使用寄存器前8位或16位查表获得b'。
反向算法
之前讨论的算法可以称为正向CRC算法,意思是将g左边的位看作是高位,右边的位看作低位。G的右边加m个0,然后迭代计算是从高位开始,逐步将低位加入到寄存器中。在实际的数据传送过程中,是一边接收数据,一边计算CRC码,正向算法将新接收的数据看作低位。
逆向算法顾名思义就是将左边的数据看作低位,右边的数据看作高位。这样的话需要在g的左边加m个0,h也要逆向,例如正向CRC-16算法h=0x4c11db8,逆向CRC-16算法h=0xedb88320。b的选择0还是h,由寄存器中右边第1位决定,而不是左边第1位。寄存器仍旧是向左位移,就是说迭代变成从低位到高位。
CRC校验原理(二) - Ryan - Ryans Fund
以下的源程序全部以 CCITT 为例。其实本质都是一样,搞明白一种,其他的都是小菜。
图 1,图 2 说明了 CRC 校验中 CRC 值是如何计算出来的,体现的多项式正是 X16+X12+X5+1。 SerialData即是需要校验的数据。从把数据移位开始计算,将数据位(从最低的数据位开始)逐位移入反向耦合移位寄存器(这个名词我也不懂,觉得蛮酷的,就这样写了,嘿)。当所有数据位都这样操作后,计算结束。此时,16位移位寄存器中的内容就是 CRC 码。


图中进行 XOR 运算的位与多项式的表达相对应。
X5 代表 Bit5,X12 代表 Bit12,1 自然是代表 Bit0,X16 比较特别,是指移位寄存器移出的数据,即图中的DATAOUT。可以这样理解,与数据位做XOR运算的是上次 CRC值的 Bit15。
根据以上说明,可以依葫芦画瓢的写出以下程序。(程序都是在 keil C 7.10 下调试的)
typedef unsigned char uchar;
typedef unsigned int uint;
code uchar crcbuff [] = {0x00,0x00,0x00,0x00,0x06,0x0d,0xd2,0xe3};
uint crc; // CRC 码
void main(void)
{
uchar *ptr;
crc = 0; // CRC 初值
ptr = crcbuff; // 指向第一个 Byte 数据
crc = crc16l(ptr,8);
while(1);
}
uint crc16l(uchar *ptr,uchar len) // ptr 为数据指针,len 为数据长度
{
uchar i;
while(len--)
{
for(i=0x80; i!=0; i>>=1)
{
if((crc&0x8000)!=0) {crc<=1; crc^="0x1021;}">
else crc<=1;>
if((*ptr&i)!=0) crc^=0x1021; 1-3
}
ptr++;
}
return(crc);
}
执行结果 crc = 0xdbc0;
程序 1-1,1-2,1-3 可以理解成移位前 crc 的 Bit15 与数据对应的 Bit(*ptr&i)做XOR运算,根据此结果来决定是否执行 crc^=0x1021。只要明白两次异或运算与原值相同,就不难理解这个程序。
很多资料上都写了查表法来计算,当时是怎么也没想通。其实蛮简单的。假设通过移位处理了 8 个 bit 的数据,相当于把之前的 CRC码的高字节(8bit)全部移出,与一个 byte 的数据做XOR 运算,根据运算结果来选择一个值(称为余式),与原来的 CRC码再做一次 XOR 运算,就可以得到新的 CRC 码。
不难看出,余式有 256 种可能的值,实际上就是 0~255 以 X16+X12+X5+1 为权得到的 CRC码,可以通过函数crc16l来计算。以1 为例。
code test[]={0x01};
crc = 0;
ptr = test;
crc = crc16l(ptr,1);
执行结果 crc = 1021,这就是1 对应的余式。
进一步修改函数,我这里就懒得写了,可得到 X16+X12+X5+1 的余式表。
code uint crc_ta[256]={ // X16+X12+X5+1 余式表
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6,0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce,0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7,0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff,0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4,0x5485,
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac,0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695,0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d,0xc7bc,
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802,0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a,0xb92b,
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33,0x2a12,
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b,0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60,0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68,0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51,0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59,0x8f78,
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e,0xe16f,
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046,0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f,0xf35e,
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277,0x7256,
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c,0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424,0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d,0xd73c,
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615,0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a,0xa9ab,
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882,0x28a3,
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb,0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3,0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8,0x8dc9,
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0,0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9,0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1,0x1ef0
};
根据这个思路,可以写出以下程序:
uint table_crc(uchar *ptr,uchar len) // 字节查表法求 CRC
{
uchar da;
while(len--!=0)
{
da=(uchar) (crc/256); // 以 8 位二进制数暂存 CRC 的高 8 位
crc<=8; 左移="" 8="">
crc^=crc_ta[da^*ptr]; // 高字节和当前数据 XOR 再查表
ptr++;
}
return(crc);
}
本质上 CRC 计算的就是移位和异或。所以一次处理移动几位都没有关系,只要做相应的处理就好了。
下面给出半字节查表的处理程序。其实和全字节是一回事。
code uint crc_ba[16]={
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6,0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce,0xf1ef,
};
uint ban_crc(uchar *ptr,uchar len)
{
uchar da;
while(len--!=0)
{
da = ((uchar)(crc/256))/16;
crc <=>
crc ^=crc_ba[da^(*ptr/16)];
da = ((uchar)(crc/256)/16);
crc <=>
crc ^=crc_ba[da^(*ptr&0x0f)];
ptr++;
}
return(crc);
}
crc_ba[16]和crc_ta[256]的前 16 个余式是一样的。
其实讲到这里,就已经差不多了。反正当时我以为自己是懂了。结果去看别人的源代码的时候,也是说采用 CCITT,但是是反相的。如图3

反过来,一切都那么陌生,faint.吐血,吐血。
仔细分析一下,也可以很容易写出按位异或的程序。只不过由左移变成右移。
uint crc16r(unsigned char *ptr, unsigned char len)
{
unsigned char i;
while(len--!=0)
{
for(i=0x01;i!=0;i <=>
{
if((crc&0x0001)!=0) {crc >>= 1; crc ^= 0x8408;}
else crc >>= 1;
if((*ptr&i)!=0) crc ^= 0x8408;
}
ptr++;
}
return(crc);
}
0x8408 就是 CCITT 的反转多项式。
套用别人资料上的话
“反转多项式是指在数据通讯时,信息字节先传送或接收低位字节,如重新排位影响 CRC计算速度,故设反转多项式。”

code uchar crcbuff [] = {0x00,0x00,0x00,0x00,0x06,0x0d,0xd2,0xe3};
反过来就是
code uchar crcbuff_fan[] ={0xe3,0xd2,0x0d,0x06,0x00,0x00,0x00,0x00};
crc = 0;
ptr = crcbuff_fan;
crc = crc16r(ptr,8);
执行结果 crc = 0x5f1d;
如想验证是否正确,可改
code uchar crcbuff_fan_result[] ={0xe3,0xd2,0x0d,0x06,0x00,0x00,0x00,0x00,0x1d,0x5f};
ptr = crcbuff_fan_result;
crc = crc16r(ptr,10);
执行结果 crc = 0; 符合 CRC 校验的原理。
请注意 0x5f1d 在数组中的排列中低位在前,正是反相运算的特点。不过当时是把我搞的晕头转向。
在用半字节查表法进行反相运算要特别注意一点,因为是右移,所以 CRC 移出的 4Bit与数据 XOR 的操作是在 CRC的高位端。因此余式表的产生是要以下列数组通过修改函数crc16r 产生。
code uchar ban_fan[]=
{0,0x10,0x20,0x30,0x40,0x50,0x60,0x70,0x80,0x90,0xa0,0xb0,0xc0,0xd0,0xe0,0xf0};
得出余式表
code uint fan_yushi[16]={
0x0000, 0x1081, 0x2102, 0x3183,
0x4204, 0x5285, 0x6306, 0x7387,
0x8408, 0x9489, 0xa50a, 0xb58b,
0xc60c, 0xd68d, 0xe70e, 0xf78f
};
uint ban_fan_crc(uchar *ptr,uchar len)
{
uchar da;
while(len--!=0)
{
da = (uchar)(crc&0x000f);
crc >>= 4;
crc ^= fan_yushi [da^(*ptr&0x0f)];
da = (uchar)(crc&0x000f);
crc >>= 4;
crc ^= fan_yushi [da^(*ptr/16)];
ptr++;
}
return(crc);
}
主程序中
crc = 0;
ptr = crcbuff_fan;
crc = ban_fan_crc(ptr,8);
执行结果 crc = 0x5f1d;

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多