CRC8/CRC16/CRC32最全总结

CRC8/CRC16/CRC32最全总结

本文首发于“嵌入式软件实战派”。

循环冗余校验(英语:Cyclic redundancy check,通称“CRC”)是一种根据网络数据包或电脑文件等数据产生简短固定位数校验码的一种散列函数,主要用来检测或校验数据传输或者保存后可能出现的错误。

Wikipedia

一句话:CRC是将数据计算出散列的方式,一般用于校验数据的完整性。它具有简单、执行效率高等特点。当然,你可以类比于Checksum,但比Checksum复杂些,防碰撞性更好些。


▍CRC的原理

CRC是基于除法的。实际的输入数据会被解释为一个长二进制位流(除数),再将其除以另一个固定二进制数(除数,即多项式)。该除法的其余部分就是校验值。

但是,现实要复杂一些。二进制数(除数和除数)不被视为普通整数值,而是被视为二进制多项式,其中实际位用作系数。

输入数据,看做一串二进制流,用多项式的方式表示为g(x),而除数是国际标准上的多项式,用h(x)表示。通过g(x)和h(x)做除法,即两者的异或运算,得出的结果,就是我们所说的CRC运算检验结果。

那么,这里有两个疑问:

问题1:多项式和二进制是什么关系?

例如,1x3 + 0x2 + 1x + 1可以表示为1011b,或者1011b表示为1x3 + 0x2 + 1x + 1,其中是0的位,即以0为系数乘以该项。

问题2:除法怎么做?
本质就是在做异或运算

我们来看看一个数据串11010011101100除以x3 + x + 1是怎样进行的?

注意,根据CRC长度,需要将这个数据串补0

整个运算过程是这样的:

CRC8/CRC16/CRC32最全总结_第1张图片

实际上就是逐个bit移位做异或。

详见:https://en.wikipedia.org/wiki/Cyclic_redundancy_check

 


▍CRC的概念

实际上,人们根据不同的需要,做了很多种计算方式,主要差别在于CRC长度、多项式、初始值、结果是否需要异或、是否需要翻转等等。

首先,来看看几个概念:

  • Length: CRC的长度(按bit算,如8,16,32)

  • Name: CRC的名字,让人一看就知道这是哪种CRC

  • Polinomial: 多项式,通过该多项式来计算CRC

  • InitialValue: CRC的初始值

  • FinalXorValue: CRC结果做异或运算的值

  • InputReflected: 指示输出是否需要翻转

  • OutputReflected: 指示输出是否需要翻转

我将网上搜到的所有的CRC标准分类做了个汇总:

CRC8/CRC16/CRC32最全总结_第2张图片

从上面的CRC名称可以看出,不同的算法是有不同用途的,这是国际常规的用法定义,其实如果是用户自己用也没特别要求,可以自由点。


▍CRC的实现

1. 根据多项式二进制数,逐位做异或

按照前面章节的“CRC原理”来做C语言实现,就通过数据移位和异或算得。以CRC8为例,代码如下

#define CRC_POLYNOMIAL_8    0x0C
uint8 crc_8(uint8 crc, uint8* pdata, uint32 len)
{
    for (uint32 i = 0; i < len; i++)
    {
        crc ^= pdata[i];
        for (uint8 j = 0; j < 8; j++)
        {
            if ((crc & 0x80u) > 0)
            {
                crc = ( (uint8)(crc << 1u) ) ^ CRC_POLYNOMIAL_8;
            }
            else
            {
                crc <<= 1u;
            }
        }
    }
    return crc;
}

这个代码中有个if ((crc & 0x80u) > 0)要解释下,因为任意数与0做异或,结果是其本身。所以,数据为0时,不做异或。同理,CRC16、CRC32也可以按这种方法做计算,只是多项式和数据位数不一样而已,在此不累述了。

按移位做异或的方法,有个缺点,就是用了两层循环,效率不是那么的高,在嵌入式软件中,不是那么受欢迎。

那需要怎么优化算法呢?那就将多项式预先算个表出来,查表吧。

2. 将多项式(Polynomial)生成一个256长度的表,用查表法实现

2.1 多项式生成的表

在开始这个话题之前,我们得回个头来看看那几个概念中有个InputReflectedOutputReflected。干啥呢,吃饱没事干啊,算就算,还要翻转?呵呵!

其实,通过多项式生成表就可以生成正序和逆序,就是为了应付这个“翻转”的。举个例子以CRC8_ITU(Polynomial=0x07)看看这个表:

正序表为

const unsigned char crc8_itu__table[] = 
{
    0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D, 
    0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D, 
    // ...... 省略部分
    0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, 
    0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3, 
};
 

逆序表为

const unsigned char crc8_itu_reversed_table[] = 
{
    0x00, 0x91, 0xE3, 0x72, 0x07, 0x96, 0xE4, 0x75, 0x0E, 0x9F, 0xED, 0x7C, 0x09, 0x98, 0xEA, 0x7B, 
    0x1C, 0x8D, 0xFF, 0x6E, 0x1B, 0x8A, 0xF8, 0x69, 0x12, 0x83, 0xF1, 0x60, 0x15, 0x84, 0xF6, 0x67, 
   // ...... 省略部分
    0xA8, 0x39, 0x4B, 0xDA, 0xAF, 0x3E, 0x4C, 0xDD, 0xA6, 0x37, 0x45, 0xD4, 0xA1, 0x30, 0x42, 0xD3, 
    0xB4, 0x25, 0x57, 0xC6, 0xB3, 0x22, 0x50, 0xC1, 0xBA, 0x2B, 0x59, 0xC8, 0xBD, 0x2C, 0x5E, 0xCF, 
};

 

2.2 通用查表法

网上有很多种查表发计算CRC,不尽相同。于是我做了个通用版,满足各种算法,其中用了宏定义的奇技淫巧。直接上代码:

#define DEF_CRC_FUNC(func, width, crcTable)                                                         \
uint##width func(const uint8 *pdata, int len,                                                       \
uint##width initial, uint##width finalXor,                                                          \
BOOL inputReflected, BOOL resultReflected)                                                          \
{                                                                                                   \
    uint##width crc = initial;                                                                      \
    uint##width temp1 = 0, temp2 = 0, pos = 0;                                                      \
    const uint##width *pTable = crcTable;                                                           \
    for(int i = 0; i < len; i++)                                                                    \
    {                                                                                               \
        uint##width curByte = pdata[i];                                                             \
        if(inputReflected)                                                                          \
        {                                                                                           \
          curByte = Reflect8(pdata[i]);                                                             \
        }                                                                                           \
        /* update the MSB of crc value with next input byte */                                      \
        temp1 = (crc ^ (curByte << (width - 8)));                                                   \
        /* this MSB byte value is the index into the lookup table */                                \
        pos = (temp1 >> (width - 8)) & 0xFF;                                                        \
        /* shift out this index */                                                                  \
        temp2 = (temp1 << 8);                                                                       \
        /* XOR-in remainder from lookup table using the calculated index */                         \
        crc = (temp2 ^ pTable[pos]);                                                                \
    }                                                                                               \
    if(resultReflected)                                                                             \
    {                                                                                               \
        crc = Reflect##width(crc);                                                                  \
    }                                                                                               \
    return (crc ^ finalXor);                                                                        \
}

怎么用?

先定义,即定义对于位数算法的函数,通过预编译生成:

DEF_CRC_FUNC(gen_crc8, 8, crc8__table);
DEF_CRC_FUNC(gen_crc16_maxim, 16, crc16_maxim__table);
DEF_CRC_FUNC(gen_crc16_a, 16, crc16_a__table);
DEF_CRC_FUNC(gen_crc32_jamcrc, 32, crc32_jamcrc__table);

再调用,测试可以在main函数调用:

int main(void)
{
    const uint8 buf[6] = "123456";
    uint8 crc8 = gen_crc8(buf, 6, 0x00, 0x00, 0, 0);
    uint16 crc16_maxim = gen_crc16_maxim(buf, 6, 0x0000, 0xFFFF, 1, 1);
    uint16 crc16_a = gen_crc16_a(buf, 6, 0xC6C6, 0x0000, 1, 1);
    uint32 crc32_jamcrc = gen_crc32_jamcrc(buf, 6, 0xFFFFFFFF, 0x00000000, 1, 1);
    printf("crc8: %X\n", crc8);
    printf("crc16_maxim: %X\n", crc16_maxim);
    printf("crc16_a: %X\n", crc16_a);
    printf("crc32_jamcrc: %X\n", crc32_jamcrc);
}

 

这里还是有个问题,就那个“翻转”的问题,每个byte都做翻转,很浪费CPU资源啊。作为“优秀的”嵌入式软件开发人员,我们是追求卓越的算法,于是乎,就有了以下方式:

(1)对于InputReflectedOutputReflected都是False的,我们用正序表,以CRC8_ITU为例:

/*
CRC Info:
Name                 Polynomial Initial    FinalXor   InputReflected ResultReflected
CRC8_ITU             0x07       0x00       0x55       false          false    
*/
unsigned char crc8_itu(unsigned char crc, const unsigned char* buf, unsigned int len)
{
    for(unsigned int i = 0; i < len; i++)
    {
        crc = crc8_itu__table[crc ^ buf[i]];
    }
    return crc^0x55;
}

(2)对于InputReflectedOutputReflected都是True的,我们用逆序表,以CRC8_EBU为例:

/*
CRC Info:
Name                 Polynomial Initial    FinalXor   InputReflected ResultReflected
CRC8_EBU             0x1D       0xFF       0x00       true           true           
*/
unsigned int crc8_ebu(unsigned int crc, const unsigned char* buf, unsigned int len)
{
    for(unsigned int i = 0; i < len; i++)
    {
        crc = crc8_ebu_reversed_table[crc ^ buf[i]];
    }
    return crc^0x00;
}

▍CRC的源码

说了这么多,以上都是举例而已(其实直接将上面代码copy过去验证也是木有问题的,不要怀疑),上代码啊

Talk is cheap. Show me the code.

Linus Torvalds

我将上面表格中提到的所有CRC算法,都coding了,就像下图的这些

CRC8/CRC16/CRC32最全总结_第3张图片

Name                 Polynomial Initial    FinalXor   InputReflected ResultReflected
CRC8                 0x07       0x00       0x00       false          false
CRC8_SAE_J1850       0x1D       0xFF       0xFF       false          false
CRC8_SAE_J1850_ZERO  0x1D       0x00       0x00       false          false
CRC8_8H2F            0x2F       0xFF       0xFF       false          false
CRC8_CDMA2000        0x9B       0xFF       0x00       false          false
CRC8_DARC            0x39       0x00       0x00       true           true
CRC8_DVB_S2          0xD5       0x00       0x00       false          false
CRC8_EBU             0x1D       0xFF       0x00       true           true
CRC8_ICODE           0x1D       0xFD       0x00       false          false
CRC8_ITU             0x07       0x00       0x55       false          false
CRC8_MAXIM           0x31       0x00       0x00       true           true
CRC8_ROHC            0x07       0xFF       0x00       true           true
CRC8_WCDMA           0x9B       0x00       0x00       true           true
CRC16_CCIT_ZERO      0x1021     0x0000     0x0000     false          false
CRC16_ARC            0x8005     0x0000     0x0000     true           true
CRC16_AUG_CCITT      0x1021     0x1D0F     0x0000     false          false
CRC16_BUYPASS        0x8005     0x0000     0x0000     false          false
CRC16_CCITT_FALSE    0x1021     0xFFFF     0x0000     false          false
CRC16_CDMA2000       0xC867     0xFFFF     0x0000     false          false
CRC16_DDS_110        0x8005     0x800D     0x0000     false          false
CRC16_DECT_R         0x0589     0x0000     0x0001     false          false
CRC16_DECT_X         0x0589     0x0000     0x0000     false          false
CRC16_DNP            0x3D65     0x0000     0xFFFF     true           true
CRC16_EN_13757       0x3D65     0x0000     0xFFFF     false          false
CRC16_GENIBUS        0x1021     0xFFFF     0xFFFF     false          false
CRC16_MAXIM          0x8005     0x0000     0xFFFF     true           true
CRC16_MCRF4XX        0x1021     0xFFFF     0x0000     true           true
CRC16_RIELLO         0x1021     0xB2AA     0x0000     true           true
CRC16_T10_DIF        0x8BB7     0x0000     0x0000     false          false
CRC16_TELEDISK       0xA097     0x0000     0x0000     false          false
CRC16_TMS37157       0x1021     0x89EC     0x0000     true           true
CRC16_USB            0x8005     0xFFFF     0xFFFF     true           true
CRC16_A              0x1021     0xC6C6     0x0000     true           true
CRC16_KERMIT         0x1021     0x0000     0x0000     true           true
CRC16_MODBUS         0x8005     0xFFFF     0x0000     true           true
CRC16_X_25           0x1021     0xFFFF     0xFFFF     true           true
CRC16_XMODEM         0x1021     0x0000     0x0000     false          false
CRC32                0x04C11DB7 0xFFFFFFFF 0xFFFFFFFF true           true
CRC32_BZIP2          0x04C11DB7 0xFFFFFFFF 0xFFFFFFFF false          false
CRC32_C              0x1EDC6F41 0xFFFFFFFF 0xFFFFFFFF true           true
CRC32_D              0xA833982B 0xFFFFFFFF 0xFFFFFFFF true           true
CRC32_MPEG2          0x04C11DB7 0xFFFFFFFF 0x00000000 false          false
CRC32_POSIX          0x04C11DB7 0x00000000 0xFFFFFFFF false          false
CRC32_Q              0x814141AB 0x00000000 0x00000000 false          false
CRC32_JAMCRC         0x04C11DB7 0xFFFFFFFF 0x00000000 true           true
CRC32_XFER           0x000000AF 0x00000000 0x00000000 false          false

举个例子,CRC32_BZIP2.h文件是的算法是这样的:

CRC8/CRC16/CRC32最全总结_第4张图片

真以为我一个一个写的吗,哈哈哈,不是。但我肯定不是在网上一个一个抄的,网上也找不到这么多。

还是那句话:机器能干的事,为啥还要人来干!

我找到了个规律,写了个脚本生成的,多项式的正序表、逆序表是生成的,连代码,我都是用脚本生成的。

这里篇幅有限,就不将代码全部贴在这了,如果你对这些算法感兴趣,关注“嵌入式软件实战派”,聊天界面输入并发送“CRC”获得下载链接。后续适当的时候,我还会将这个生成源文件的脚本也分享给大家。

为了验证其可靠性,我还做了个测试代码(篇幅有限,下面截取的代码已省略了部分):

// TEST:Caculate the string "123456", result list below with different crc models:
    crc_assert(0xfd ==  crc8                 (0x00, "123456", 6), "crc8"                  );
    // ...... 省略部分
    crc_assert(0x57 ==  crc8_rohc            (0xFF, "123456", 6), "crc8_rohc"             );
    crc_assert(0xab ==  crc8_wcdma           (0x00, "123456", 6), "crc8_wcdma"            );

    crc_assert(0x20e4 ==  crc16_ccit_zero   (0x0000, "123456", 6), "crc16_ccit_zero"    );
    crc_assert(0x29e4 ==  crc16_arc         (0x0000, "123456", 6), "crc16_arc"          );

    // ...... 省略部分
    crc_assert(0x32e4 ==  crc16_modbus      (0xFFFF, "123456", 6), "crc16_modbus"       );
    crc_assert(0xe672 ==  crc16_x_25        (0xFFFF, "123456", 6), "crc16_x_25"         );
    crc_assert(0x20e4 ==  crc16_xmodem      (0x0000, "123456", 6), "crc16_xmodem"       );


    crc_assert(0x0972d361 ==  crc32         (0xFFFFFFFF, "123456", 6), "crc32"          );
    // ...... 省略部分
    crc_assert(0xf036f1c2 ==  crc32_xfer    (0x00000000, "123456", 6), "crc32_xfer"     );

如果你怀疑我“监守自盗”,你还可以在网上找个在线计算的方式来验证算法的正确性,如:https://crccalc.com/

如果有其他想法和建议,请留言,我们一同探讨。

如果你喜欢我的文章,请扫码关注“嵌入式软件实战派”,我会分享更多技术干货。

CRC8/CRC16/CRC32最全总结_第5张图片

你可能感兴趣的:(C语言,算法,c语言)