1、CCITT CRC-16 计算原理与实现CRC 的全称为 Cyclic Redundancy Check,中文名称为循环冗余校验。它是一类重要的线性分组码,编码和解码方法简单,检错和纠错能力强,在通信领域广泛地用于实现差错控制。实际上,除数据通信外,CRC 在其它很多领域也是大有用武之地的。例如我们读软盘上的文件,以及解压一个 ZIP 文件时,偶尔会碰到“Bad CRC”错误,由此它在数据存储方面的应用可略见一斑。 差错控制理论是在代数理论基础上建立起来的。这里我们着眼于介绍 CRC 的算法与实现,对原理只能捎带说明一下。若需要进一步了解线性码、分组码、循环码、纠错编码等方面的原理,可以阅读有
2、关资料。 利用 CRC 进行检错的过程可简单描述为:在发送端根据要传送的 k 位二进制码序列,以一定的规则产生一个校验用的 r 位监督码(CRC 码),附在原始信息后边,构成一个新的二进制码序列数共 k+r 位,然后发送出去。在接收端,根据信息码和 CRC 码之间所遵循的规则进行检验,以确定传送中是否出错。这个规则,在差错控制理论中称为“生成多项式”。 1 代数学的一般性算法 在代数编码理论中,将一个码组表示为一个多项式,码组中各码元当作多项式的系数。例如 1100101 表示为 1x6+1x5+0x4+0x3+1x2+0x+1,即 x6+x5+x2+1。 设编码前的原始信息多项式为 P(x)
3、,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) x
4、6+x5 x - = - = - = (x3+x2+x) + - G(x) x3+x+1 x3+x+1 x3+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 无余式。
5、回头看一下上面的竖式除法,如果被除数是1100010,显然在商第三个 1 时,就能除尽。 上述推算过程,有助于我们理解 CRC 的概念。但直接编程来实现上面的算法,不仅繁琐,效率也不高。实际上在工程中不会直接这样去计算和验证 CRC。 下表中列出了一些见于标准的 CRC 资料: 名称 生成多项式 简记式* 应用举例 CRC-4 x4+x+1 ITU G.704 CRC-12 x12+x11+x3+x+1 CRC-16 x16+x12+x2+1 1005 IBM SDLC CRC-ITU* x16+x12+x5+1 1021 ISO HDLC, ITU X.25, V.34/V.41/V.42,
6、 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统一去掉了,如 04C11DB7 实际上是 104C11DB7。 * 前称 CRC-CCITT。ITU的前身是 CCITT。 4.CRC 算法的实现 - 要用程序实现 CRC 算法,考虑对第 2 节的长除法做一下变换,依然是 M = 11100110,G
7、 = 1011, 其系数 r 为 3。 11001100 - 1011 )11100110000 1011. -. 1010 1011 - 1110. 1011. -. 1010 1011 - 100 1 110 0110000 011 101 0110000 101第一位为 1,移位且计算 1 010 110000 011 001 110000001第一位第二位均为 0,移位 2 次 00 111 0000111第一位为 1,移位且计算 1 110 000 011 101 000101 第一位为 1,移位且计算 1 010 00 011 001 00移位 2 次得 100 用 CRC16-C
8、CITT 的生成多项式 0x1021,其 C 代码(本文所有代码假定系统为 32位,且都在 VC6 上编译通过)如下: unsigned short do_crc(unsigned char *message, unsigned int len) int i, j; unsigned short crc_reg; crc_reg = (message0 (7 - i) 0x1021; else crc_reg = (crc_reg (7 - i); else for (j = 0; j 1) 0x8408; 14. else 15. crc_reg = 1; 16. current = 1;
9、17. 18. 19. return crc_reg; 20. 该算法使用了两层循环,对消息逐位进行处理,这样效率是很低的。为了提高时间效率,通常的思想是以空间换时间。考虑到内循环只与当前的消息字节和crc_reg 的低字节有关,对该算法做以下等效转换: Java 代码 1. unsigned short do_crc(unsigned char *message, unsigned int len)2. 3. int i, j; 4. unsigned short crc_reg = 0; 5. unsigned char index; 6. unsigned short to_xor; 7
10、. 8. for (i = 0; i 1) 0x8408; 16. else 17. to_xor = 1; 18. 19. crc_reg = (crc_reg 8) to_xor; 20. 21. return crc_reg; 22. 现在内循环只与 index 相关了,可以事先以数组形式生成一个表crc16_ccitt_table,使得 to_xor = crc16_ccitt_tableindex,于是可以简化为: Java 代码 1. unsigned short do_crc(unsigned char *message, unsigned int len)2. 3. unsig
11、ned short crc_reg = 0; 4. 5. while (len-) 6. crc_reg = (crc_reg 8) crc16_ccitt_table(crc_reg *message+) 7. 8. return crc_reg; 9. crc16_ccitt_table 通过以下代码生成: Java 代码 1. int main() 2. 3. unsigned char index = 0; 4. unsigned short to_xor; 5. int i; 6. 7. printf(“unsigned short crc16_ccitt_table256 =n“)
12、; 8. while (1) 9. 10. if (!(index % 8) 11. printf(“n“); 12. 13. to_xor = index; 14. for (i = 0; i 1) 0x8408; 18. else 19. to_xor = 1; 20. 21. printf(“0x%04x“, to_xor); 22. 23. if (index = 255) 24. 25. printf(“n“); 26. break; 27. 28. else 29. 30. printf(“, “); 31. index+; 32. 33. 34. printf(“;“); 35.
13、 return 0; 36. 37. 38.生成的表如下: 39. 40.unsigned short crc16_ccitt_table256 = 41. 42.0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 43.0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 44.0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 45.0x9cc9, 0x8d40, 0
14、xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 46.0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 47.0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 48.0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 49.0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0
15、xd8fd, 0xc974, 50.0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, 51.0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 52.0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 53.0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 54.0x6306, 0x728f
16、, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 55.0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 56.0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 57.0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, 58.0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5
17、, 0xe13e, 0xf0b7, 59.0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, 60.0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 61.0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 62.0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 63.0x2942, 0x3
18、8cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 64.0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, 65.0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 66.0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, 67.0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1
19、de9, 0x2f72, 0x3efb, 68.0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 69.0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 70.0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 71.0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 72.0xf78f,
20、0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 73.0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 74.; 这样对于消息 unsigned char messagelen,校验码为: unsigned short code = do_crc(message, len); 并且按以下方式发送出去: messagelen = code messagelen + 1 = (code 接收端对收到的 len + 2 字节执行 do_crc,如果没有差错发生则
21、结果应为 0。 在一些传输协议中,发送端并不指出消息长度,而是采用结束标志,考虑以下几种差错: 1)在消息之前,增加 1 个或多个 0 字节; 2)消息以 1 个或多个连续的 0 字节开始,丢掉 1 个或多个 0; 3)在消息(包括校验码)之后,增加 1 个或多个 0 字节; 4)消息(包括校验码)以 1 个或多个连续的 0 字节结尾,丢掉 1 个或多个0; 显然,这几种差错都检测不出来,其原因就是如果寄存器值为 0,处理 0 消息字节(或位),寄存器值不变。为了解决前 2 个问题,只需寄存器的初值非 0 即可,对 do_crc 作以下改进: Java 代码 1. unsigned short
22、 do_crc(unsigned short reg_init, unsigned char *message, unsigned int len) 2. 3. unsigned short crc_reg = reg_init; 4. 5. while (len-) 6. crc_reg = (crc_reg 8) crc16_ccitt_table(crc_reg *message+) 7. 8. return crc_reg; 9. 在 CRC16-CCITT 标准中 reg_init = 0xffff,为了解决后 2 个问题,在 CRC16-CCITT 标准中将计算出的校验码与 0xf
23、fff 进行异或,即: unsigned short code = do_crc(0xffff, message, len); code = 0xffff; messagelen = code messagelen + 1 = (code 显然,现在接收端对收到的所有字节执行 do_crc,如果没有差错发生则结果应为某一常值 GOOD_CRC。其满足以下关系: unsigned char p= 0xff, 0xff; GOOD_CRC = do_crc(0, p, 2); 其结果为 GOOD_CRC = 0xf0b8。 在同一程序中验证如下(放在 main 函数中可试验): Java 代码 1
24、. unsigned char p= 0xa0,0xb0,0xff, 0xff; 2. unsigned short crc; 3. crc= do_crc(0xffff, p, 2); /计算前两位的 CRC 码 4. crc=0xffff; /对其取反 5. p2=crc /将计算的 CRC 码加到信息序列后面 6. p3=crc8 7. printf(“p2=%x,p3=%xn“,p2,p3); 8. crc=do_crc(0xffff,p,4); /对信息码CRC 码共同计算得出CRC=0xf0b8 9. printf(“crc is %xn“,crc); 假设发送的信息是 p0,p1;低位先发,对其计算的 CRC 加到信息码后面 然后对信息码CRC 码共同计算 CRC 值,此时应该是常数 0xf0b8。不管信息码如何变化,内容和长度都可变,只要把计算的 CRC 码加进去一起计算 CRC,就应该是得该常数 GOOD_CRC。