循环冗余校验 CRC (Cyclic Redundancy Check) 教程(一)

本文翻译并修订自Bastian Molkenthin的文章《Understanding and implementing CRC (Cyclic Redundancy Check) calculation》之部分内容


1. 前言与概述

此文是本人终于找到时间探究CRC后得到的结果。在阅读了维基及其他一些文章之后,我始终感觉未能深入理解CRC。因此我决定写下这篇文章,试图覆盖所有我觉得比较困难的部分。这实际上也是我自己逐步去探索CRC的过程。请注意,本文不是一份关于CRC的综合性完全手册,并不会解释所有细节。本文可以作为一份补充性的、以实践为导向的应用笔记,用来与网上林林总总的说明进行对照参考。

概述:
首先,讨论CRC的总体构思及其功能。
其次,提供一些手算的示例,用以熟悉CRC的计算过程。
然后,基于以上的观察结果,从简易的方法过渡到更高效的算法,一步一步地展示CRC计算如何实现。
此时我们的重点在于对照手算过程,找到计算代码的关键点。使用的范例为一个CRC-8多项式。
最后,将获取的结论扩展到CRC-16。

2. CRC介绍

CRC (Cyclic Redundancy Check) 是一种用于检测数据是否存在不一致问题的校验和算法,例如数据传输过程中发生的位错误。通过CRC计算得到的校验和被附加到数据后面,以便接收者检查数据中是否存在错误。参考 [1] 了解CRC的简单介绍,参考 [4] 了解CRC更详尽一些的介绍。
CRC是基于除法的。实际输入的数据被解析为一个长的二进制比特流(被除数),除以一个固定的二进制数(除数)。这个除法运算所得的余数即为校验和的值。
看似简单,实际过程则稍显复杂。这些二进制数(除数与被除数)并不像整数那样计算,而是当作一个二进制多项式来对待。每个二进制数的每个比特位,作为该多项式的系数

举个例子:
输入数据为
0x25 = 0010 0101
表示为
0*x7 + 0*x6 + 1*x5 + 0*x4 + 0*x3 + 1*x2 + 0*x1 + 1*x0

多项式除法与整数除法不同。此处并不赘言,只说明CRC所使用的算术是基于 XOR( 按位异或) 操作的。

  • 被除数是完整的输入数据(解析为二进制位流)。
  • 除数也称为生成多项式,是被所选用的CRC算法静态定义的一个多项式。CRC-n表示计算中使用一个固定的 (n+1) 位生成多项式。
  • CRC校验和的值即为被除数 % 除数(求余操作所得到的余数)。

异或真值表
XOR真值表
对于手动计算而言,在进行CRC计算(多项式除法)之前,n个bit 0被补充到输入数据后面。让我们进行一次计算示例。

示例1:
输入数据 0xC2 = b 11000010 \color{red}{11000010} 11000010,生成多项式(除数)我们使用 0x11D = b 100011101 \color{blue}{100011101} 100011101
除数有9个bit(因此它是一个CRC-8多项式),所以先补充8个bit'0'到输入数据后面。
将除数最前面的'1' 与被除数的第一个 '1' 对齐,然后像在学校那样一步一步的做除法,对每一个bit执行XOR操作。
在这里插入图片描述
实际得到的CRC值即为0x0F(除法运算最后的余数)。

有用的结论:

  • 每步操作中,除数最前面'1'永远和被除数的第一个'1' 对齐。这意味着除数在每一步的操作中向右移动的位数并不仅限于1,有时会移动好几位(比如标星号* 的那一行)。(编者注:相当于每次右移1位的步骤重复进行好几次,这对于后面理解代码有帮助)
  • 计算重复进行,直到除数将实际输入数据的每一个bit(不包括实际数据后面追加的字节)都 “消零” 之后才停止下来。此处,实际数据是从第A列到第H列。到最后一步时,第H列和它之前的所有列都已经是'0'(编者注:高字节的0无意义所以没有全部写出来),所以计算停止。
  • 余数(=CRC的值)即最后剩下的,处在补充的bit 0位置下方的值(第I列到第P列的值)。因为我们补充了n个bit,所以实际的CRC也是n个bit。
  • 每一步操作中,只有余数是有意义的,除法所得的商我们根本不去管它。

2.1 CRC 校验

余数就是CRC的值,它将作为CRC数据随输入数据(实际数据)一起被发送给接收者。接收者可以计算接收数据(编者注:这里所说的接收数据指实际数据,不包括随实际数据一起发送的CRC数据)的CRC值,并拿这个值与接收数据中已经写明的CRC数据进行比较。
或者,更常用的办法是,接收者直接对接受到的全部数据(实际数据 + CRC数据)进行CRC计算,如果计算得到的CRC值为0,那就表示数据传送过程没有发生位错误。

让我们用举例来说明校验过程。

示例2:
发送数据(实际数据 + CRC)为b 11000010 \color{red}{11000010} 11000010 00001111 \color{green}{00001111} 00001111(编者注:这里继续使用示例1中所用的输入数据及CRC值),注意我们的数据中已经使用了8位的CRC,所以计算得到的CRC也是8位的。
生成多项式是被所选用的CRC算法静态定义的,因此接收者是事先知道的(编者注:这里继续使用示例1中所用的生成多项式)
在这里插入图片描述

3. CRC移位寄存器的概念

我们已经看过了如何手动计算CRC校验和的值了,但是怎么用机器实现呢?
输入数据流通常都很长(位数超过1个bit),所以不可能像“输入数据%生成多项式”那样简单地做除法求余。计算必须分步进行,这就引出了移位寄存器的概念。

移位寄存器具有固定的宽度。
它能够将其存储的内容移动1位,即:向左边或右边移出一个bit,然后将一个新的bit移入到空出的位置上。
移位完成后,原MSB(最高有效位)被弹出寄存器,原MSB-1位置上的bit移动1位成为新的MSB,原MSB-2移动1位成为新的MSB-1,后面的bit同样如此这般操作。
最后,原LSB(最低有效位)的位置被空出来了,这样输入数据流的下一个bit便可以移入到寄存器内部了。

     ---- ---- ---- ----     ---- ----
  <- |   |   |   |   | … |   |   | <-- (将输入数据的bit依次移入)
     ---- ---- ---- ----     ---- ----

使用移位寄存器进行CRC计算的大致过程如下:

  1. 将寄存器初始化为0。
  2. 将输入数据流的1个bit移入寄存器。若寄存器弹出的原MSB是一个’1’,则将寄存器的内容(编者注:原MSB仍当作寄存器的内容一起参与计算)与生成多项式进行XOR操作(编者注:然后用所得结果去更新寄存器的内容)
  3. 若输入数据流的所有bit都被处理过了,那么寄存器此时所包含的内容即为CRC值。

我们继续使用上面例子中的数据,将整个计算过程变得可视化。

CRC-8 移位寄存器示例:
输入数据 = 0xC2 = b11000010 (末尾补充8个bit 0后为b1100001000000000),生成多项式 = b100011101。

  1. CRC-8 寄存器初始化为0。
    ---- ---- ---- ---- ---- ----
    | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | <-- b1100001000000000
    ---- ---- ---- ---- ---- ----

  2. 寄存器左移1位。原MSB是0,故不做任何操作,仅将输入数据的1个bit移入寄存器。
    ---- ---- ---- ---- ---- ----
    | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | <-- b100001000000000
    ---- ---- ---- ---- ---- ----

  3. 重复这些步骤直到MSB是1,此时的状态是下面这样:
    ---- ---- ---- ---- ---- ----
    | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | <-- b00000000
    ---- ---- ---- ---- ---- ----

  4. 寄存器左移1位,MSB(1)弹出。
         ---- ---- ---- ---- ---- ----
    1 <- | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | <-- b0000000
         ---- ---- ---- ---- ---- ----
    因为弹出的原MSB是1,故将寄存器的内容(连同已被弹出的原MSB)与生成多项式进行XOR操作: b110000100 XOR b100011101 = b010011001 = 0x99。丢弃MSB,新的CRC寄存器 = b10011001。此时的状态如下:
    ---- ---- ---- ---- ---- ----
    | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | <-- b0000000
    ---- ---- ---- ---- ---- ----

  5. 寄存器左移1位,MSB(1)弹出。同上所述, b100110010 XOR b100011101 = b000101111 = 0x2F。
    ---- ---- ---- ---- ---- ----
    | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | <-- b000000
    ---- ---- ---- ---- ---- ----

  6. 寄存器左移,直到MSB的值为1。
    ---- ---- ---- ---- ---- ----
    | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 0 | <-- b0000
    ---- ---- ---- ---- ---- ----

  7. 寄存器左移1位,MSB(1)弹出。同上所述, b101111000 XOR b100011101 = b001100101 = 0x65。
    ---- ---- ---- ---- ---- ----
    | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | <-- b000
    ---- ---- ---- ---- ---- ----

  8. 寄存器左移,直到MSB的值为1。
    ---- ---- ---- ---- ---- ----
    | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | <-- b00
    ---- ---- ---- ---- ---- ----

  9. 寄存器左移1位,MSB(1)弹出。同上所述,b110010100 XOR b100011101 = b010001001 = 0x89。
    ---- ---- ---- ---- ---- ----
    | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | <-- b0
    ---- ---- ---- ---- ---- ----

  10. 寄存器左移1位,MSB(1)弹出。同上所述,b10001001 XOR b100011101 = b000001111 = 0x0F。
    ---- ---- ---- ---- ---- ----
    | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | <--
    ---- ---- ---- ---- ---- ----

输入数据流的所有bit都处理过了(编者注:所有bit都依次移入寄存器了),计算停止。此时寄存器内包含的内容就是CRC的值0x0F。

4. CRC-8算法的实现

这一章节讲解计算CRC-8校验和的不同算法以及它们在C#下的实现方法。针对有限的输入数据进行计算,以简单的算法开始,以基于查询表的高效算法结束。

4.1 用于1个byte输入数据的简易型CRC-8移位寄存器实现方法

让我们从只含1个byte的输入数据的CRC-8算法开始讲解。此实现过程与上面例子中所讲的移位寄存器非常接近。
CRC-8算法的生成多项式实际是9位的,它让我们在计算过程中追踪这个“非对齐”的值显得很累赘。所幸,就像我们在上面的章节里所讲的那样,它的MSB是可以被丢弃掉的。首先,这个MSB总是'1'。其次,由于除数的首个'1'总是和下一个被除数的'1'对齐,它们XOR的结果总是'0'
这意味着我们可以摆脱这个MSB (‘1’)。因此,从现在开始,我们使用
在这里插入图片描述
作为示例。

下面,我们使用移位寄存器来实现算法。

public static byte Compute_CRC8_Simple_OneByte_ShiftReg(byte byteVal)
{
    const byte generator = 0x1D;
    byte crc = 0; /* crc寄存器初始化为 0 */
    /* 补充8个0到输入数据流后部 */
    byte[] inputstream = new byte[] { byteVal, 0x00 };
    /* 迭代处理输入数据流的每一个bit */
    foreach (byte b in inputstream)
    {
        for (int i = 7; i >= 0; i--)
        {
            /* 检查寄存器MSB是否为1 */
            if ((crc & 0x80) != 0)
            {   /* 寄存器MSB为1,将其移出 */
                crc = (byte)(crc << 1);
                /* 移入输入数据流的下一个bit
                 * 若此bit为1,将crc寄存器的LSB赋值为1
                 * 若此bit为0,将crc寄存器的LSB赋值为0*/
                crc = ((byte)(b & (1 << i)) != 0) ? (byte)(crc | 0x01) : (byte)(crc & 0xFE);
                /* 执行除法操作,crc寄存器XOR生成多项式*/
                crc = (byte)(crc ^ generator);
            }
            else
            {   /*寄存器MSB不为1,将其移出,并将输入数据流的下一个bit移入。
                 *与上面一样,只是不执行除法操作*/
                crc = (byte)(crc << 1);
                crc = ((byte)(b & (1 << i)) != 0) ? (byte)(crc | 0x01) : (byte)(crc & 0xFE);
            }
        }
    }
    return crc;
}

4.2 用于1个byte输入数据的改进型CRC-8逐位运算实现方法

上面的实现方法看起来略显复杂,如何简化呢?
开始的8次左移其实是无用的,因为CRC被初始化为0所以没有执行XOR操作。这意味着我们可以直接用输入数据(1个byte)对CRC进行初始化。这样操作之后,输入数据流里就只剩下了0(补充的8个'0')。我们不必显式地将它们移入寄存器,因为C#的左移操作符<<会自动往LSB内填入'0'。这暗示我们不再需要inputstream 这个数组了。
这样的简化将导致下面的实现方法(是不是更好一点?)

public static byte Compute_CRC8_Simple_OneByte(byte byteVal)
{
    const byte generator = 0x1D;
    /*直接用输入数据(1个byte)对crc初始化,而不是用0来初始化。这样就避免了8次无用的移位。*/
    byte crc = byteVal; 

    for (int i = 0; i < 8; i++)
    {
        if ((crc & 0x80) != 0)
        { /* MSB为1,crc寄存器移位,执行XOR操作(执行XOR时,要考虑到丢掉的那个MSB)*/
            crc = (byte)((crc << 1) ^ generator);
        }
        else
        { /* MSB不为1,直接左移去处理下一个bit */
            crc <<= 1;
        }
    }

    return crc;
}

为了更好的描述算法是如何工作的,我们再重复一下上面的计算过程。这一次,我们把每一步计算的中间值也显示出来。

Compute_CRC8_Simple_OneByte()计算过程可视化图:
在这里插入图片描述
现在,为什么CRC寄存器在与除数进行XOR之前要先左移1位就变得清楚了。就是我们前面已经讨论过的原因:生成多项式的MSB没用,不必存储,XOR计算所得的结果同样如此。

4.3 通用型CRC-8逐位运算实现方法

到目前为止,我们的输入数据只包含1个byte。接下来,我们看一看当输入数据是一个byte型数组时的情况。
可以很简单地采用第1个函数Compute_CRC8_Simple_OneByte_ShiftReg(), 只不过函数的传入参数将是一个数组。那么Compute_CRC8_Simple_OneByte()怎么样呢?
有趣的地方就处在2个byte的边界:若1个byte已经全部完成了处理,紧跟在它后面的那个byte将怎样参与到计算中来呢?

我们再次通过一个简单的例子开始探讨(即便要做更多的手算):
在这里插入图片描述
实际上算法每次只处理了1个byte,并且在这个byte完全处理结束之前,不考虑后面的那个byte。
当使用Compute_CRC8_Simple_OneByte()时, crc的值被初始化为 0x01,输入数据是这样的:
在这里插入图片描述
当第1个byte完全处理完时的状态:
在这里插入图片描述
当第2个byte进入运算范围内时的状态:
在这里插入图片描述
两相对比,很显然,我们发现第2个byte必须与当前的CRC进行XOR操作:
在这里插入图片描述
然后算法拿着这个新得到的CRC继续运算下去。知道这一点后,我们就能很轻易将算法推广到任意长度的byte型数组:

public static byte Compute_CRC8_Simple(byte[] bytes)
{
    const byte generator = 0x1D;
    byte crc = 0; /* 以0进行初始化,故第1个byte可以通过XOR的方式移入寄存器 */

    foreach (byte currByte in bytes)
    {
        crc ^= currByte; /* 通过XOR的方式将下一个byte移入寄存器 */

        for (int i = 0; i < 8; i++)
        {
            if ((crc & 0x80) != 0)
            {
                crc = (byte)((crc << 1) ^ generator);
            }
            else
            {
                crc <<= 1;
            }
        }
    }

    return crc;
}

编者注:若读者怀疑用{0x01,0x02}作为输入数据太简单,不具有普遍性,那么看一下下面的公式:
[(A << 8) ^ B] ^ C = (A << 8) ^ B ^ C = (A << 8) ^ C ^ B,
将0x01代入A,0x02代入B,0x1D代入C,然后脑补一下滚动运算的过程,会不会发现上面的运算其实是符合数学运算法则的,是具有普遍性的,并不是简单的巧合?

4.4 改进型 CRC-8逐字节运算实现方法(基于查询表)

到目前为止,我们的算法效率很低,因为它是通过逐位计算进行工作的。对于更大量的输入数据,运算速度会变得非常慢。那么我们的CRC-8算法可以加速工作吗?
被除数是当前的CRC值(1个byte),而1个byte只能表示256个不同的数值。生成多项式(除数)是固定的。那么我们为什么不利用固定的多项式对1个byte能够表示的所有可能数值进行提前计算,然后将计算得到的CRC值放到一张表格里存储起来呢?这是完全可以的,因为对于同样的被除数与除数,计算得到的余数总是相同的。这样,输入数据流可以逐字节的进行处理,而不再是逐位进行处理了。

我们还是使用经常用到的那个例子来演示运算过程:
在这里插入图片描述
第3步和第5步中,使用了查询表而没有使用逐位的计算,这当然就可以加快运算速度了。


下面是计算256个元素的查询表的实现方法:

public static void CalulateTable_CRC8()
{
    const byte generator = 0x1D;
    crctable = new byte[256];
    /* 对byte所有可能的值 0 - 255 进行迭代计算 */
    for (int divident = 0; divident < 256; divident++)
    {
        byte currByte = (byte)divident;
        /* 对当前字节计算CRC-8的值*/
        for (byte bit = 0; bit < 8; bit++)
        {
            if ((currByte & 0x80) != 0)
            {
                currByte <<= 1;
                currByte ^= generator;
            }
            else
            {
                currByte <<= 1;
            }
        }
        /* 将CRC值存入查询表 */
        crctable[divident] = currByte;
    }
}

改进后的使用查询表的CRC-8算法如下:

public static byte Compute_CRC8(byte[] bytes)
{
    byte crc = 0;
    foreach (byte b in bytes)
    {
        /* XOR,移入下一个字节 */
        byte data = (byte)(b ^ crc);
        /* 获取当前值对应的CRC值 */
        crc = (byte)(crctable[data]);
    }

    return crc;
}

速度提升的代价是计算表格导致的运算时间消耗和更多的内存开销(256个byte)。但这是值得的!😃

5. 扩展到CRC-16

CRC的位数越多,冲突的可能性就越低:对于CRC-8,仅有256个不同的CRC值。这意味着如果数据在发送者和接收者之间被扰乱或者被改动,将有1/256的可能性出现这个现象:改动过的数据流与原始数据流具有相同的CRC值。这将导致错误不能被检测到。除了其他的因素之外,增加CRC的位宽度能带来更好的错误检测效果。

这就带来了一个问题:如果我们希望把CRC-8算法推广到CRC-16,对于算法的实现会有什么影响?

  1. 显然CRC-16使用了17位的多项式,但与CRC-8类似,它的MSB必然也是1。因此生成多项式和CRC也都是16位的数据类型。
  2. 怎样通过XOR将输入字节(8位)移入CRC寄存器(16位)?答案是输入字节移入寄存器的高字节位置。
  3. 对MSB的检查变成了bit15而不是bit7,即0x80 -> 0x8000。

与4.3节一样,我们举个例子并进行可视化。
首先,我们进行一次手算,这一次的CRC-16使用的多项式为0x1021:
在这里插入图片描述
然后,看一下当第1个字节0x01全部处理完时的状态:
在这里插入图片描述
这里我们看到,下一个输入字节0x02 = 00000010 \color{red}{00000010 } 00000010 必须被通过XOR操作移入 00001000000100001 \color{blue}{00001000000100001 } 00001000000100001 的高字节部分,得到 00001001000100001 \color{CadetBlue}{00001001000100001} 00001001000100001 然后再继续运算。
因此,CRC-16 的简单型实现方法如下所示:

public static ushort Compute_CRC16_Simple(byte[] bytes)
{
    const ushort generator = 0x1021;	/* 除数为16位 */
    ushort crc = 0; /* CRC值为16bit */

    foreach (byte b in bytes)
    {
        crc ^= (ushort(b << 8); /* 将字节移入CRC寄存器的高字节位置 */

        for (int i = 0; i < 8; i++)
        {
            if ((crc & 0x8000) != 0) /* 检查MSB是否为1 */
            {
                crc = (ushort((crc << 1) ^ generator);
            }
            else
            {
                crc <<= 1;
            }
        }
    }

    return crc;

CRC-16查询表的计算方法修改起来也很容易:

public static void CalculateTable_CRC16()
{
    const ushort generator = 0x1021;
    crctable16 = new ushort[256];

    /* 对byte所有可能的值 0 - 255 进行迭代计算 */
    for (int divident = 0; divident < 256; divident++) 
    {
        /* 将被除数字节移入16位CRC的高字节位置 */
        ushort curByte = (ushort(divident << 8);   

        for (byte bit = 0; bit < 8; bit++)
        {
            if ((curByte & 0x8000) != 0)
            {
                curByte <<= 1;
                curByte ^= generator;
            }
            else
            {
                curByte <<= 1;
            }
        }

        crctable16[divident] = curByte;
    }
}

实际的逐字节运算稍稍有点复杂,所以我们先看一下以前的例子:
在这里插入图片描述
此处很重要的一点是:将当前字节移入CRC中间值的高字节位置后,此高字节部分的值就是查询表的索引值。
由此,得到基于表格的CRC-16算法:

public static ushort Compute_CRC16(byte[] bytes)
{
    ushort crc = 0;
    foreach (byte b in bytes)
    {
        /* 将下一个输入字节与CRC的高字节位置进行XOR,所得结果的高字节部分为查表时需要用到的索引值 */
        byte pos = (byte)( (crc >> 8) ^ b); /* equal: ((crc ^ (b << 8)) >> 8) */
        /* 移出CRC的高字节部分,将其与查表得到的余数进行XOR,得到新的CRC */
        crc = (ushort)((crc << 8) ^ (ushort)(crctable16[pos]));

    }

    return crc;
}
  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值