CRC校验值是如何计算出来的?

本文在我已在知乎发过地址-->addr 

最近用到CRC-16/CCITT-FALSE算法校验,找了很多资料,
发现代码和线上校验的值对不上,所以花了时间深入了解其原理,并
将
CRC-8/CRC-8/ITU/ROHC/MAXIM
CRC-16/IBM/MAXIM/USB/MODBUS/CCITT/CCITT-FALSE/X25/XMODEM/DNP
CRC-32/MPEG-2
这些校验算法都实现了一遍,
大部分算法实现都有普通的遍历校验
全部算法都有查表法实现的代码,
每个算法都是独立单个函数

点击源码地址-->addr

1.什么是CRC

CRC:循环冗余校验
以CRC-32为例
是根据CRC-32的生成多项式x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+1
计算出任意字符或字符串的一个32位值
作用:用来核实数据传输/保存的正确性和完整性

2.什么生成多项式 

以CRC-32的生成多项式x32+x26+x23+x22+x16+x12+x11+x10+x8+x7+x5+x4+x2+x+为例
其数学生成多项表达式如下
    GF(2)=k*x^32+k*x^31+....k*x^1+1

CRC多项式的计算是以GF(2)(2元素伽罗瓦域)多项式算术为数学基础的
    GF(2):2元素伽罗瓦域,其加减法可认为是无进位的二进制加减法,
          这样的话相当于两个bit位的异或运算

因为是2元素,x的系数K的取值为0和1
如此CRC-32的生成多项式可以转换为二进制(幂次对应二进制位的位置,系数对应1和0)

====二进制==>10000100110000010001110110110111b====16进制==>0x84C11DB7
但是CRC-32的生成多项式的16进制表示法:0x04C11DB7
这是因为16进制表示法去除了最高次项,之所以这么写是由其计算方式决定,
这个后面会讲到。

 3.CRC的计算原理

将要校验的数据看作一个多项式:NM
生成多项为G
校验值CRC=NM除G的余数

 4.GF(2)多项式之间的计算

1.多项式之间的加减法
   即系数K之间进行模2算术执行,本质上就是二进制的异或运算,
   模2运算加减法都是一样的
例如
    多项式A=x^3+x^2+1
    多项式B=x^3+x^1+1

    A+B=:
        A和B x^3的系数都为1,异或结果K=0,K*x^3=0
        A的x^2的系数k=1,B的x^2的系数k=0,异或结果k=1 相加结果是K*x^2=1*x^2
        A的x^1的系数0,B的该位系数为1,相加结果为 1*x^1
        A和B的x^0的系数都为1,相加结果为0

    A+B=x^2+x 


2.多项式之间的乘法
   与普通多项式一样,只是各项在相加时按照模2算术执行
   A*B=(x^3+x^2+1)(x^3+x^1+1)
   =(x^(3+3)+x^(3+1)+x^(3+0))+(x^(2+3)+x^(2+1)+x^(2+0))
   +(x^3+x^1+1) 
   =(x^6+x^4+x^3)+(x^5+x^3+x^2)+(x^3+x^1+1) 
   =x^6+x^5+x^4+x^3+x^3+x^2+x^1+1

3.多项式之间的除法
  与普通多项式一样,只是各项在相加时按照模2减法算术执行
  C=x^7 + x^6 + x^5 + x^2 + x
C/B=下图运算方式
    商为:X^4+X^3+1
    余数为:X^2+1  

 

5.以生成多项式x^3 + x + 1来探讨CRC的二进制数据的计算过程

将长度为m位的信息对应一个GF(2)的多项式M,该数据是从高位到低位传输的

m=11100110====对应的多项式=====  M=x^7 + x^6 + x^5 + x^2 + x

发送端和接收端约定了一个次数为r的GF(2)多项式G,G称为生成多项式
比如 
    G=1011=x^3 + x + 1  
    r=3  (CRC-8的r=8,CRC16的r=16)
 在m后面加上r个0的多项式NM
NM=m+r个0=11100110000=x^10+x^9+x^8+x^5+x^4

 NM与M的关系如下
  NM=M*x^r=(x^7 + x^6 + x^5 + x^2 + x)*x^3
       =x^10+x^9+x^8+x^5+x^4

NM/G 获得的对应r长度的余数就是校验码了
计算步骤看下图
  
注意:
  除数G应该按照生成多项式来转为二进制,
  而不是直接取它的16进制表示,因为16进制表示法去掉了最高次项 

发送端将m+R(校验码)发送到接收端
接收端可以根据NM以及G生成多项式计算出校验码比较
或者
接受端可以用(NM或运算R)/G 判断余数是否为0

下面是一些生成多项式G
注意:
   G应该按照生成多项式来转为二进制,
  而不是直接取它的16进制表示,因为16进制表示法去掉了最高次项 

6.CRC8代码的几种实现方式
以CRC-8 多项式为x^8+x^2+x+1 16进制表示法 0x07 为例
方式一,以上面的竖式运算来算

#include "crc_8.h"
#include "stdio.h"
/**
 * 获得data 数据为1的最高位的位置
 * @param data
 * @return
 */
uint8_t getMbsBitIndex(uint8_t data){
    uint8_t index;
    for (index=7;  index>0 ; index--) {
        if((0x01<<index)&data){
            return index;
        }
    }

}

/**
 * 通过生成式
 *          G的16进制表示法 0x07
 *          对应的多项式为 x^8+x^2+x+1===>0x107==>对应的二进制是==>100000111
 *          G的真实值=0x107
 *          r=8
 *          m=data
 *          NM:二进制的data在后面+r个0
 *
 * 原理:将data<<8 获得NM
 *      NM/G 的余数即data的校验码,这里的除法为多项式除法,且取余的减法是模2运算,即二进制的异或
 *      通过G多项式对应的二进制为1的最高位与NM对应的二进制为1的最高位对齐进行异或运算获得的结果temp_value
 *      如果temp_value的二进制值为1的最高位>=r,也就是8,则将temp_value看作NM,继续与G进行对齐模2运算
 *      直到temp_value的二进制值为1的最高位<r ,此时temp_value就是最终的校验值了
 *
 * @param data 单个字符校验
 * @return
 */
uint8_t getCRC_8(uint8_t data) {
    uint16_t G=0x107;
    //1。先计算NM的值
    uint32_t NM=data<<8;
//    printf("NM=%X\n",NM);
    //2.计算出data为1的最高位
    uint8_t index=getMbsBitIndex(data);
//    printf("index=%d\n",index);
    //3.G为1的最高位与NM为1的最高位对齐
    uint32_t tG=(G<<(index));
//    printf("tG=%X\n",tG);
    //4.对NM与tG进行异或运算
    uint32_t temp_value=NM^tG;
//    printf("temp_value=%X\n",temp_value);
    //取temp_value的[15:8]
    uint8_t tm=((temp_value&0xFF00)>>8);
    //5.获取temp_value的为1的最高位
    index=getMbsBitIndex(tm);
//    printf("index=%d\n",index);
    //一直循环到 index==0,并且temp_value的[15:8]==0
    while (index!=0||tm){
    //G为1的最高位与NM为1的最高位对齐
        tG=(G<<(index));
        //对temp_value与tG进行异或运算
        temp_value=temp_value^tG;
        tm=((temp_value&0xFF00)>>8);
        //获取temp_value的为1的最高位
        index=getMbsBitIndex(tm);
//        printf("while_index=%d\n",index);
    }
//    printf("temp_value=%X\n",temp_value);
    return temp_value;
}

实现方式二,寄存器(内存模仿寄存器)移位方式

/**
 * 原理
 *    G=0x07; 用CRC-8的16进制表示法 而不是用生成表达式的值0x107
 *    NM=data<<8;
 *    先将NM的高8位加载到8位长度的内存temp_value中r
 *
 *    如果内存中的最高位为1,将内存中的值左移一位,
 *    并将NM后面的位依次移入内存
 *    因为单个字节,后面全是8个0,左移自动补零相当于帮将NM后面的位自动移入该内存中了
 *    然后再与G进行异或并将结果保存在该内存中,这里的G=0x07,是因为这里将最高的1位移出,相当于除去了0x107的最高位
 *
 *    如果内存中的最高位为0,将内存中的值左移一位
 *    并将NM后面的位依次移入内存
 *    因为单个字节,后面全是8个0,左移自动补零相当于帮将NM后面的位自动移入该内存中了
 *
 *    直到NM的最低位也被加载到内存中,该内存中的值就是校验值
 *
 * @param data
 * @return
 */
uint8_t gencrc8_2(uint8_t data){
    uint8_t G=0x07;
    //这里可以直接用data加载到8位长度的内存中,之所以移位是对照公式的NM,便于理解
    uint16_t NM=data<<8;
    //把NM开头的8位加载到内存中
    uint8_t temp_value=(NM&0xFF00)>>8;

    //循环8次是为了将NM全部移动到8位内存中
    for (int i = 0; i <8; ++i) {
        //判断8位内存中的最高位是否是1
        uint8_t h=(temp_value&0x80);
        if(h==0x80){
            //如果是1,则左移1位,并与G进行异或,并保存在8位内存中
            temp_value=((temp_value<<1)&0xFF);
            temp_value=(temp_value^G);
        } else{
            //如果是0,则左移1位,不进行异或运算,并保存在8位内存中
            temp_value=((temp_value<<1)&0xFF);
        }
    }
//    printf("%X\n",temp_value);
    return temp_value;
}

方式三,使用查表法以空间换时间计算多个数据的校验值

//1.计算出0~0xFF的所有校验值并将其放入一个256的数组中
const uint8_t crc_table [256]={
        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,
        0xE0,0xE7,0xEE,0xE9,0xFC,0xFB,0xF2,0xF5,
        0xD8,0xDF,0xD6,0xD1,0xC4,0xC3,0xCA,0xCD,
        0x90,0x97,0x9E,0x99,0x8C,0x8B,0x82,0x85,
        0xA8,0xAF,0xA6,0xA1,0xB4,0xB3,0xBA,0xBD,
        0xC7,0xC0,0xC9,0xCE,0xDB,0xDC,0xD5,0xD2,
        0xFF,0xF8,0xF1,0xF6,0xE3,0xE4,0xED,0xEA,
        0xB7,0xB0,0xB9,0xBE,0xAB,0xAC,0xA5,0xA2,
        0x8F,0x88,0x81,0x86,0x93,0x94,0x9D,0x9A,
        0x27,0x20,0x29,0x2E,0x3B,0x3C,0x35,0x32,
        0x1F,0x18,0x11,0x16,0x03,0x04,0x0D,0x0A,
        0x57,0x50,0x59,0x5E,0x4B,0x4C,0x45,0x42,
        0x6F,0x68,0x61,0x66,0x73,0x74,0x7D,0x7A,
        0x89,0x8E,0x87,0x80,0x95,0x92,0x9B,0x9C,
        0xB1,0xB6,0xBF,0xB8,0xAD,0xAA,0xA3,0xA4,
        0xF9,0xFE,0xF7,0xF0,0xE5,0xE2,0xEB,0xEC,
        0xC1,0xC6,0xCF,0xC8,0xDD,0xDA,0xD3,0xD4,
        0x69,0x6E,0x67,0x60,0x75,0x72,0x7B,0x7C,
        0x51,0x56,0x5F,0x58,0x4D,0x4A,0x43,0x44,
        0x19,0x1E,0x17,0x10,0x05,0x02,0x0B,0x0C,
        0x21,0x26,0x2F,0x28,0x3D,0x3A,0x33,0x34,
        0x4E,0x49,0x40,0x47,0x52,0x55,0x5C,0x5B,
        0x76,0x71,0x78,0x7F,0x6A,0x6D,0x64,0x63,
        0x3E,0x39,0x30,0x37,0x22,0x25,0x2C,0x2B,
        0x06,0x01,0x08,0x0F,0x1A,0x1D,0x14,0x13,
        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
};

/**
 * /**
 * 查表法计算 crc
 *  生成多项式:x^8+x^2+x+1
 *  16进制表示:0x07
 *  初始值:0x00
 *  输入输出反转:False
 *
 *
 * 该查表法原理
 *      以  E9AAEE为例
 *          先计算E9的crc值===>91
 *          先算E9AA =>gencrc8_temp(0xE9AA)==>3B
 *          恰好相当于E9的crc值异或AA:gencrc8_temp(0xE9)^AA==>3B
 *          接着以3B作为EE的高8位计算getcrc8_temp(3BEE)===>4F
 *          恰好相当于3B的crc值异或EE:gencrc8_temp(0x3B)^AA==>4F
 *          最后计算4F的crc gencrc8_temp(0x4F)===>EA 这个值机最终的CRC值
 *
 *          数学原理
 *          NM=M*X^r
 *          E900=E9*X^8==>
 *                      (X^7+X6+X^5+X^3+1)*X8=X^15+X^14+x^13+X^11+^X8
 *          由因为求E9的校验值,其实就是求E900/G 的余数CRC1
 *          有因为只算高8位,所以E900/G和E9AA/G的商是相等的
 *          设E9为K,AA为L,K/G和AA/G的商为N
 *          E900=K*X^8
 *          GN+CRC1=K*X^8
 *          E9AA=K*X^8+L
 *          GN+CRC2=K*X^8+L
 *          K*X^8-CRC1=K*X^8+L-CRC2
 *          CRC2=K*X^8+L-K*X^8+CRC1
 *              =L+CRC1
 *         由因为模2运算的加减法相当于异或运算
 *         所以CRC2=CRC1^L
 *         E9AA的校验值CRC2相当于E900的CRC1^AA
 *
 *
            对应的多项式为 x^8+x^2+x+1===>0x107==>对应的二进制是==>100000111
 * @param datas
 * @param len
 * @return
 */

uint8_t crc8(unsigned char * datas, uint16_t len) {
    uint8_t crc=0;
    for (int i = 0; i < len; ++i) {
            crc=(crc_table[crc]^datas[i]);
//            printf("table[%d]=%X  crc=%X\n",i,datas[i],crc);
    }
    return crc_table[crc];
}

 

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值