CRC校验的概念及具体实现

概念

CRC(Cyclic redundancy check),循环冗余校验
CRC校验是用于检测一帧数据发送是否正确,只有确认对错的作用,并没有纠错的能力。
还有一点就是CRC校验通过了,并不代表这个数据肯定就是正确的,只能说尽可能减少出错的概率,当然
CRC错了那么这个数据肯定是不正确的
而这个概率是跟CRC的位数相关,也跟选择的多项式有关,大致可以理解为CRC8,就是1/(28),CRC16则是1/(216)以此类推。

对于检验一帧数据是否正确有很多算法,CRC只是其中的一种,SUM的形式也可以的,只是算法不同对于校验结果的效果也是不一样的,最好的效果是,每一位的变化都可以引起最终checksum的值发生较大的改变。引入除法计算是一种很好的方法,每一位发生改变对于最后的余数都会引起较大的变化。

多项式(Polynomical)

多项式即CRC除法的除数,而且多项式是总于高于CRCN中N的一位,这样可以保证余数的位数与N相同。同时多项式也有好坏之分,区别就是在于出错的概率,至于哪种多项式好一些,这个一般来说是数学家的事情,我们工程上拿过来用就好,而且一般的协议中也已经规定了这个CRC的多项式。
其实多项式只是一种表现方式,当然也可以直接用16进制表示
以CRC-CCITT为例

[displaystyle x^{16}+x^{12}+x^{5}+1 ]

也可以表示为0x1021

计算例子

引用别人文档中的例子来说明CRC机制,如下是一个CRC4计算的例子

            1100001010 = Quotient (nobody cares about the quotient)
       _______________
10011 ) 11010110110000 = Augmented message (1101011011 + 0000)
=Poly   10011,,.,,....
        -----,,.,,....
         10011,.,,....
         10011,.,,....
         -----,.,,....
          00001.,,....
          00000.,,....
          -----.,,....
           00010,,....
           00000,,....
           -----,,....
            00101,....
            00000,....
            -----,....
             01011....
             00000....
             -----....
              10110...
              10011...
              -----...
               01010..
               00000..
               -----..
                10100.
                10011.
                -----.
                 01110
                 00000
                 -----
                  1110 = Remainder = THE CHECKSUM!!!!

先将要计算的后方填充相应位数的0(CRC4,4位),再对POLY进行求余操作,这个余数就是我们要的checksum
这个操作就是一个除法操作,只是在减的时候用XOR来代替减法,这样就不要考虑进位借位的问题,而且XOR来代替减法也不会使CRC的效果变差,因为每一位的改变还是会引起checksum较大的变化。

计算方法

了解了CRC的原理,接下来就对CRC校验进行计算,接下来的讨论都以CRC16为模板。

一、直接计算法

这个方法就是根据CRC的定义,进行按位操作,先将数据移位,若高位是1的话,就将当前的CRC与poly进行XOR操作来更新当的CRC值,直至所有的数据被更新后,再在数据的结尾添加相应的0位来得到最终的CRC结果,对于CRC16来说这个零位就是2个字节。总体来说,这种实现方式是根据定义来的,比较好理解,不过运行速度有待提高。
示例代码如下:

unsigned int poly = 0x11021;
unsigned char testData[10] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa};
unsigned short crcUpdate(unsigned short crc, unsigned char data)
{
    int i;
    unsigned short result;
    unsigned int tmp;
    tmp = crc;
    for(i = 0; i < 8; i++)
    {
        tmp <<= 1;
        if(data & 0x80)
        {
            tmp += 1;
        }
        data <<= 1;
        
        if(tmp & 0x10000)
        {
            tmp ^= poly;
        }
    }
    result = tmp &0xffff;
    
    return result;
}

unsigned short crcCheck(unsigned char *pData, unsigned char size)
{
    unsigned short result = 0;
    int i;
    
    for(i = 0; i < size; i++)
    {
        result = crcUpdate(result, *(pData+i));
    }
    //补零
    for(i = 0; i < 2; i++)
    {
        result = crcUpdate(result, 0);
    }
    return result;
}

void demo(void)
{
    printf("Result %04x
",crcCheck(testData,10)); //print 0xd877
}

二、表驱动法(Table-Driven Implementation)

在直接计算的情况下,为了提高运行的速度别人又提出了用表驱动的方法(根据当前的值来查找相应的CRC的结果,再代入公式进行计算最终的结果)。换句话,直接计算是通过一次移一位的操作来进行,而表驱动法刚是采用一次移多位的形式来进行CRC计算,来提高运行速度。
原理XOR也是满足交换律如下

[(A xor B) xor C = A xor (B xor C) ]

根据这个交换律,我们可以先将POLY进行移位XOR,再将结果同最初的值来进行XOR,来得来相应的移位。
下面以CRC8来说明

                //以1位为单位为进行XOR
                                   1 0 1 1 1 0 0 0 
                  ________________________________
1 0 0 0 1 1 1 0 0/ 1 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0
                   1 0 0 0 1 1 1 0 0
                     0 0 0 0 0 0 0 0 0
                       1 0 0 0 1 1 1 0 0
                         1 0 0 0 1 1 1 0 0
                           1 0 0 0 1 1 1 0 0
                             0 0 0 0 0 0 0 0 0
                               0 0 0 0 0 0 0 0 0
                                 0 0 0 0 0 0 0 0 0
                   -------------------------------
                                   1 0 1 0 0 0 0 0
                                   
                  //先将POLY的多次移位进行XOR                 
                   1 0 0 0 1 1 1 0 0
                     0 0 0 0 0 0 0 0 0
                       1 0 0 0 1 1 1 0 0
                         1 0 0 0 1 1 1 0 0
                           1 0 0 0 1 1 1 0 0
                             0 0 0 0 0 0 0 0 0
                               0 0 0 0 0 0 0 0 0
                                 0 0 0 0 0 0 0 0 0
                    -------------------------------
                  (1 0 1 1 0 1 0 0)1 0 1 0 0 0 0 0

相当于是这样次一次性移位8位,8位的值为(10110100)其对于应是这样的操作,后8位直接XOR(10100000)
这个其实跟直接除xor是一样的,只是这样一次移位得多,可以加快计算结果。

表的生成
表的长度

是由一次移动的位数决定,如一次性移4bit,那么表的长度就是24(16),如果一次性移8bit(1byte),那么表的长度就是28(256),一般都是256为长度。

表的一个单元

这个就是由CRCN,这个N来决定,CRC8为8bit,CRC16为16bit

表的生成代码
unsigned short crc16Table[256] = {0};
unsigned short poly = 0x8005;
void crcTableCreate(void)
{
    int i = 0;
    for(i = 0; i < 256; i++)
    {
        unsigned short crc;
        crc = i << 8;
        for(int j = 0; j < 8; j++)
        {
            if(crc & 0x8000)
            {
                crc = (crc << 1) ^ poly;
            }
            else
            {
                crc <<= 1;
            }
        }
        crc16Table[i] = crc;
    }
}

//crc16Table
0000,8005,800f,000a,801b,001e,0014,8011,8033,0036,003c,8039,0028,802d,8027,0022,
8063,0066,006c,8069,0078,807d,8077,0072,0050,8055,805f,005a,804b,004e,0044,8041,
80c3,00c6,00cc,80c9,00d8,80dd,80d7,00d2,00f0,80f5,80ff,00fa,80eb,00ee,00e4,80e1,
00a0,80a5,80af,00aa,80bb,00be,00b4,80b1,8093,0096,009c,8099,0088,808d,8087,0082,
8183,0186,018c,8189,0198,819d,8197,0192,01b0,81b5,81bf,01ba,81ab,01ae,01a4,81a1,
01e0,81e5,81ef,01ea,81fb,01fe,01f4,81f1,81d3,01d6,01dc,81d9,01c8,81cd,81c7,01c2,
0140,8145,814f,014a,815b,015e,0154,8151,8173,0176,017c,8179,0168,816d,8167,0162,
8123,0126,012c,8129,0138,813d,8137,0132,0110,8115,811f,011a,810b,010e,0104,8101,
8303,0306,030c,8309,0318,831d,8317,0312,0330,8335,833f,033a,832b,032e,0324,8321,
0360,8365,836f,036a,837b,037e,0374,8371,8353,0356,035c,8359,0348,834d,8347,0342,
03c0,83c5,83cf,03ca,83db,03de,03d4,83d1,83f3,03f6,03fc,83f9,03e8,83ed,83e7,03e2,
83a3,03a6,03ac,83a9,03b8,83bd,83b7,03b2,0390,8395,839f,039a,838b,038e,0384,8381,
0280,8285,828f,028a,829b,029e,0294,8291,82b3,02b6,02bc,82b9,02a8,82ad,82a7,02a2,
82e3,02e6,02ec,82e9,02f8,82fd,82f7,02f2,02d0,82d5,82df,02da,82cb,02ce,02c4,82c1,
8243,0246,024c,8249,0258,825d,8257,0252,0270,8275,827f,027a,826b,026e,0264,8261,
0220,8225,822f,022a,823b,023e,0234,8231,8213,0216,021c,8219,0208,820d,8207,0202,

unsigned short crcUpdate2(unsigned short crcIn, unsigned char data)
{
    unsigned short result = 0;
    result = (crcIn << 8 | data) ^ crc16Table[(crcIn >> 8) & 0xff];
    return result;
}

unsigned short crcCheck2(unsigned char *pData, unsigned char size)
{
    unsigned short crcResult = 0;//Initial Value
    for(int i = 0; i < size; i++)
    {
        crcResult = crcUpdate2(crcResult, *(pData+i));
    }
    // add zero to the tail
    for(int i = 0; i < 2; i++)
    {
        crcResult = crcUpdate2(crcResult, 0);
    }
    return crcResult;
}

void demo(void)
{
    crcTableCreate();
    printf("Result %04x
",crcCheck2(testData,10)); //print 0x2a62
}

三、直驱表法(Slightly Mangled Table-Driven Implementation)

直驱表法这种翻译说法,我也不知道是否合理,照我个理解来说,这个应该叫做一种表驱动法的变种
这个变种作用是在于在进行CRC计算之后不需要进行填充相应位数的0。
驱动表还是一样的,只是计算的公式不一样

unsigned short crcUpdate3(unsigned short crcIn, unsigned char data)
{
    unsigned short result = 0;
    result = (crcIn << 8) ^ crc16Table[(crcIn >> 8) ^ data];
    return result;
}

unsigned short crcCheck3(unsigned char *pData, unsigned char size)
{
    unsigned short crcResult = 0;//Initial Value
    for(int i = 0; i < size; i++)
    {
        crcResult = crcUpdate3(crcResult, *(pData+i));
    }
    return crcResult;
}

void demo(void)
{
    crcTableCreate();
    printf("Result %04x
",crcCheck3(testData,10)); //print 0x2a62
}

这个做法是这样的,至于这个公式是如何推导出来的,看的资料也没有解释清楚的,本人暂时也还不太理解,不过实际工程上基本都用的是这个方法。
而实际表驱动法与直驱表法,其实就是两种算法,虽然两者可以用的是相同的表,只有在驱动表法的初始值为0与填充值为0才与驱动表法初始值为0的情况下结果是一样的,其他情况下值应该都是不一样的。
对于CRC的真正用途来说,算法没有具体的意义,只是有在数据发生改变的时候,CHECKSUM就可以发生较大的改变,且重复的概率比较小,那么这种算法就是一种比较好的算法。工程上用这种算法,就可以省掉补零的操作。

CRC的其他述语

上面说的是CRC的基本概念以及实现的方式,而在实际的用途中这个CRC还是会一些细微的参数。
如下是一些CRC的表述方式

   Name   : "CRC-16/CITT"
   Width  : 16
   Poly   : 1021
   Init   : FFFF
   RefIn  : False
   RefOut : False
   XorOut : 0000
   Check  : ?

   Name   : "XMODEM"
   Width  : 16
   Poly   : 8408
   Init   : 0000
   RefIn  : True
   RefOut : True
   XorOut : 0000
   Check  : ?

   Name   : "ARC"
   Width  : 16
   Poly   : 8005
   Init   : 0000
   RefIn  : True
   RefOut : True
   XorOut : 0000
   Check  : ?
标识 含义
Name 名字标识
Width CRC长度
Poly 多项式
Init 寄存器初始值,针对直驱表法的寄存器初始值
RefIn 输入是否是低位在前
RefOut 输出是否低位在前
XorOut 输出前与这个值进行XOR再输出
Check 表示的一个参考输出,以STRING(123456789)为例

这里的参数无论哪一个修改了,最终的值都会发生变化,相当于就生成一个新的CRC校验方式


网上写CRC的文章很多,记录自己的理解前也参考了很多的文章

主要参考如下几篇文章

我学习 CRC32、CRC16、CRC 原理和算法的总结(与 WINRAR 结果一致)

【脑冻结】CRC我就拿下了

A PAINLESS GUIDE TO CRC ERROR DETECTION ALGORITHMS

原文地址:https://www.cnblogs.com/stupidpeng/p/13266346.html