[技术栈]CRC校验原理及C#代码实现CRC16、CRC32计算FCS校验码


1.CRC、FCS是什么

CRC,全称Cyclic Redundancy Check,中文名称为循环冗余校验,是一种根据网络数据包或计算机文件等数据产生简短固定位数校验码的一种信道编码技术,主要用来检测或校验数据传输或者保存后可能出现的错误。它是利用除法及余数的原理来作错误侦测的。

FCS,全称Frame Check Sequence,中文名称为帧校验序列,俗称帧尾,即计算机网络数据链路层的协议数据单元(帧)的尾部字段,是一段4个字节的循环冗余校验码。

注:CRC循环冗余校验和FCS帧校验序列是单独的概念,CRC是一种错误校验方法,FCS是帧尾校验码,FCS可以采用CRC校验方法,也可以采用其他校验方法。

2.CRC算法原理

我们可以把任意的一串二进制数据表示为一个与之对应的多项式。比如:

二进制数据:1100101
多项式:$x^6 + x^5 + x^2+1$

多项式: $x^6 + x^4+x^3 + x^2+1$
二进制数据:1011101

有了这样的对应关系,对二进制数据的CRC校验就可以利用多项式运算规则进行校验计算。
CRC校验算法正是采用了模2除法,在数据处理里的具体表现为异或运算。

CRC的具体运算规则为:假设要传输的二进制数据为:10010110,对应的m阶多项式为:$M =x^7+x^4+x^2+x^1$,除数为h阶的多项式为:$H=x^4+x$,对应的二进制码为:10010,先将M乘以$x^h$,即将M对应的二进制数据后面加h个0,然后除以h阶的多项式H,得到的h-1阶的余数项R对应的二进制数据即为数据10010110的CRC校验码。

3.计算CRC校验

3.1.手工计算CRC校验码

MH的多项式除法运算,可以用模2除法运算计算。下面为以生成多项式为H10010110的CRC校验码运算过程:
crc_handle_item.png

对应到异或运算:
crc_handle_item_xor.png
通过示例即其他自定义的一些数据运算后,根据运算现象总结可以得到一些规律:

1.每次异或运算,当从左到右首位为1的时候,就与生成多项式H异或运算,然后再左移1位;当首位为0的时候只将数据左移1位。

2.每次异或运算后的数据,首位必定为0,因为首位为1的时候进行异或运算,而生成多项式的首位也必定为1。所以当需要进行异或运算时,可以舍弃H的首位,舍弃后为H',直接将数据左移一位后再与H'异或。
crc_handle_item_xor_move_bit.png
3.每次运算,参与运算的是数据的前h位,可以用一个存储h位二进制数据的寄存器S,将数据的前h位存储到这个寄存器中。每次运算先将寄存器的首位移除,然后将二进制数据后一位移入,然后再参与运算,最后寄存器中的值即为CRC校验码。

3.2.C#代码计算CRC校验码

//代码验证如下:
static void Main(string[] args)
{
    int data = 0b10010110;
    int ploy = 0b0010;
    ploy <<= 4;
    Console.WriteLine($"第0次运算结果:"+Convert.ToString(data, 2));
    for (int i = 0; i < 8; i++)
    {
        if ((data & 0b10000000) == 0b10000000)
        {
            data = (data << 1) ^ ploy;

        }
        else
        {
            data <<= 1;
        }

        Console.WriteLine($"第{i+1}次运算结果:"+Convert.ToString(data, 2));
    }
    Console.WriteLine($" 最终运算结果:"+Convert.ToString(data, 2));          
    Console.ReadKey();
}

crc_handle_item_xor_code_result.png

这里用int的第5位到第8位作为一个四位寄存器,可以看到与手算一致,最后算得校验位1100

4.查表法

crc_handle_item_xor_table.png

可以看到,参与运算的始终只有4位,所以在移位D1数据时,参与运算的数据只有D1D2,经过四次移位运算,D1被移除寄存器,这个时候受到影响的只有D2。而将D2的初值经过四次异或运算后的值就可以获得四次移位后的新数据$D2'=D2\bigoplus H1 \bigoplus H2\bigoplus H3\bigoplus H4 = D2 \bigoplus \sum{h}$。

每一次D2是异或0还是异或生成多项式H',与D2本身的值无关,仅与D1中被移除的数据有关(首位为0还是1),所以这里引入了一个查表法,即先将所有可能的D1组合都计算出对应的$\sum{h}$,一次性移除四位,然后以$D2\bigoplus{\sum{h}}$即可以获得D2'

生成多项式为H,则一共有$2^h$种可能,代码如下:

class CalcByCrcTable
{
    private byte[] CrcTable;
    private void CteateTable()
    {
        int ploy = 0b0010;            
        CrcTable = new byte[(int)Math.Pow(2,4)];
        ploy <<= 4;
        for (int i = 0; i < CrcTable.Length ; i++)
        {
            int data = i<<4;
            for (int j = 0; j < 4; j++)
            {
                if ((data & 0b10000000) == 0b10000000)
                {
                    data = (data << 1) ^ ploy;
                }
                else
                {
                    data <<= 1;
                }                  
            }
            CrcTable[i] = Convert.ToByte((data & 0xf0)>>4);
        }
    }
    public byte CalcCrc()
    {
        CteateTable();
        int data = 0b10010110;
        byte crchigh4 = CrcTable[(data>>4)&0x0f];//用查表法先查到data的高四位1001的crc值;
        byte value = Convert.ToByte((data & 0x0f) ^ crchigh4);//将高四位的CRC与低四位异或,得到移位四次后的数据值;
        byte crc = CrcTable[value]; //在用移位后的数据值查出数据的CRC校验码;
        return crc;

    }
}

 static void Main(string[] args)
 {
        CalcByCrcTable calcByCrcTable = new CalcByCrcTable();
        byte crc = calcByCrcTable.CalcCrc();           
        Console.WriteLine($" CRC校验码为:" + Convert.ToString(crc, 2));
        Console.ReadKey();
}

//打印结果如下

CRC校验码为:1100

可以看到与前面的计算法结果一致。

5.反向校验

上面所诉的均为正向检验(Normal),当然也有反向校验(Reversed),反向校验是将数据和生成多项式均进行了一个镜像,当然算法也需要镜像,即镜像后从右往左运算。

5.1手工计算CRC反向校验码

原二进制数据:10010110

原生成多项式:0010

正向CRC校验码:1100

镜像二进制数据:01101001

镜像生成多项式:0100

镜像算法:

crc_handle_item_xor_reversed_move_bit.png

反向CRC校验码:0011

5.2.C#代码计算CRC反向校验码

class CalcByCrcTable
{
    private byte[] CrcTable;
    private void CteateReversedTable()
    {
        int ploy = 0b0100;
        CrcTable = new byte[(int)Math.Pow(2, 4)];           
        for (int i = 0; i < CrcTable.Length; i++)
        {
            int data = i;
            for (int j = 0; j < 4; j++)
            {
                if ((data & 1) == 1)
                {
                    data = (data >> 1) ^ ploy;
                }
                else
                {
                    data >>= 1;
                }
            }
            CrcTable[i] = Convert.ToByte((data & 0x0f));
        }
    }
 public byte CalcReversedCrc()
    {
        CteateReversedTable();
        int data = 0b01101001;
        byte crclow4 = CrcTable[data & 0x0f];//用用查表法先查到data的低四位1001的crc值;
        byte value = Convert.ToByte(((data>>4) & 0x0f) ^ crclow4);//将第四位的CRC与低四位异或,得到移位四次后的数据值;
        byte crc = CrcTable[value]; //在用移位后的数据值查出数据的CRC校验码;
        return crc;

    }
}

 static void Main(string[] args)
{
     CalcByCrcTable calcByCrcTable = new CalcByCrcTable();
    byte crc = calcByCrcTable.CalcReversedCrc();           
    Console.WriteLine($" CRC反向校验码为:" + Convert.ToString(crc, 2));
    Console.ReadKey();
}

//打印结果如下

CRC反向校验码为:11

6.C#查表法计算CRC16校验码

//多线程使用时请注意干扰 
class CalcOnCrc16
 {
    private ushort[] Crc16NormalTable;

     private ushort[] Crc16ReversedTable;

     private void CreateNormalCrc16Table(ushort ploy)
     {
       ushort data;
       Crc16NormalTable = new ushort[256];
       int i, j;
       for (i = 0; i < 256; i++)
       {
           data = (ushort)(i << 8);
           for (j = 0; j < 8; j++)
           {
               if ((data & 0x8000) == 0x8000)
                   data = Convert.ToUInt16((ushort)(data << 1) ^ ploy);
               else
                   data <<= 1;                    
           }
           Crc16NormalTable[i] = data;
       }
     }

     private void CreateReversedCrc16Table(ushort ploy)
     {
       ushort data;
       Crc16ReversedTable = new ushort[256];
       int i, j;
       for (i = 0; i < 256; i++)
       {
           data = (ushort)i;
           for (j = 0; j < 8; j++)
           {
               if ((data & 1) == 1)
                   data = Convert.ToUInt16((ushort)(data >>1) ^ ploy);
               else
                   data >>= 1;
           }
           Crc16ReversedTable[i] = data;
       }
     }

    /// <summary>
    /// 正向计算CRC16校验码
    /// </summary>
    /// <param name="bytes">校验数据</param>
    /// <param name="poly">生成多项式</param>
    /// <param name="crcInit">校验码初始值</param>
    /// <returns></returns>
    public ushort CalcNoemalCrc16(byte[] bytes,ushort poly,ushort crcInit)
    {
        CreateNormalCrc16Table(poly);
   
        ushort crc = crcInit;
        for (int i = 0; i < bytes.Length; i++)
        {
            crc = Convert.ToUInt16((ushort)(crc << 8) ^ Crc16NormalTable[((crc >> 8) & 0xff) ^ bytes[i]]);
        }
        return crc;
    }

     /// <summary>
     /// 反向计算CRC16校验码
     /// </summary>
     /// <param name="bytes">校验数据</param>
     /// <param name="poly">反向生成多项式</param>
     /// <param name="crcInit">校验码初始值</param>
     /// <returns></returns>
    public ushort CalcReversedCrc16(byte[] bytes, ushort poly, ushort crcInit)
    {
        CreateReversedCrc16Table(poly);
   
        ushort crc = crcInit;
        for (int i = 0; i < bytes.Length; i++)
        {
            crc = Convert.ToUInt16((ushort)(crc >> 8) ^ Crc16ReversedTable[(crc & 0xff) ^ bytes[i]]);
        }
        return crc;
    }
 }

7.C#查表法计算CRC32校验码

class CalcOnCrc32
{
    private uint[] Crc32NormalTable;
    
    private uint[] Crc32ReversedTable;

    private void CreateNormalCrc32Table(uint ploy)
    {
        uint data;
        Crc32NormalTable = new uint[256];
        int i, j;
        for (i = 0; i < 256; i++)
        {
            data = (uint)(i << 24);
            for (j = 0; j < 8; j++)
            {
                if ((data & 0x80000000) == 0x80000000)
                    data = Convert.ToUInt32((uint)(data << 1) ^ ploy);
                else
                    data <<= 1;
            }
            Crc32NormalTable[i] = data;
        }
    }

    private void CreateReversedCrc32Table(uint ploy)
    {
        uint data;
        Crc32ReversedTable = new uint[256];
        int i, j;
        for (i = 0; i < 256; i++)
        {
            data = (uint)i;
            for (j = 0; j < 8; j++)
            {
                if ((data & 1) == 1)
                    data = Convert.ToUInt32((uint)(data >> 1) ^ ploy);
                else
                    data >>= 1;
            }
            Crc32ReversedTable[i] = data;
        }
    }

    /// <summary>
    /// 正向计算CRC32校验码
    /// </summary>
    /// <param name="bytes">校验数据</param>
    /// <param name="poly">生成多项式</param>
    /// <param name="crcInit">校验码初始值</param>
    /// <returns></returns>
    public uint CalcNoemalCrc32(byte[] bytes, uint poly, uint crcInit)
    {
        CreateNormalCrc32Table(poly);
        uint crc = crcInit;
        for (int i = 0; i < bytes.Length; i++)
        {
            crc = Convert.ToUInt32((uint)(crc << 8) ^ Crc32NormalTable[((crc >> 24) & 0xff) ^ bytes[i]]);
        }
        return crc;
    }

    /// <summary>
    /// 反向计算CRC32校验码
    /// </summary>
    /// <param name="bytes">校验数据</param>
    /// <param name="poly">反向生成多项式</param>
    /// <param name="crcInit">校验码初始值</param>
    /// <returns></returns>
    public uint CalcReversedCrc32(byte[] bytes, uint poly, uint crcInit)
    {
        CreateReversedCrc32Table(poly);
        uint crc = crcInit;
        for (int i = 0; i < bytes.Length; i++)
        {
            crc = Convert.ToUInt32((uint)(crc >> 8) ^ Crc32ReversedTable[(crc & 0xff) ^ bytes[i]]);
        }
        return crc;
    }
}

参考资料

循环冗余检验 (CRC) 算法原理

CRC查找表法推导及代码实现比较

CRC(循环冗余校验)在线计算

声明:武小栈|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - [技术栈]CRC校验原理及C#代码实现CRC16、CRC32计算FCS校验码


CQUer,NETer