zoukankan      html  css  js  c++  java
  • 循环冗余校验(CRC)算法入门引导

    写给嵌入式程序猿的循环冗余校验(CRC)算法入门引导

    前言

    CRC校验(循环冗余校验)是数据通讯中最常採用的校验方式。在嵌入式软件开发中,常常要用到CRC 算法对各种数据进行校验。因此,掌握主要的CRC算法应是嵌入式程序猿的基本技能。但是,我认识的嵌入式程序猿中能真正掌握CRC算法的人却非常少,寻常在项目中见到的CRC的代码多数都是那种效率非常低下的实现方式。

    事实上,在网上有一篇介绍CRC 算法的很好的文章,作者是Ross Williams,题目叫:“A PAINLESS GUIDE TO CRC ERROR DETECTION ALGORITHMS”。我常将这篇文章推荐给向我询问CRC算法的朋友,但不少朋友向我抱怨原文太长了,并且是英文的。希望我能写篇短点的文章,因此就有了本文。只是,我的水平比不了Ross Williams,我的文章肯定也没Ross Williams的写的好。因此,阅读英文没有障碍的朋友还是去读Ross Williams的原文吧。

    本文的读者群设定为软件开发者,尤其是从事嵌入式软件开发的程序猿,而不是专业从事数学或通讯领域研究的学者(我也没有这个水平写的这么高深)。因此,本文的目标是介绍CRC算法的基本原理和实现方式,用到的数学尽量控制在高中生能够理解的深度。

    另外,鉴于大多数嵌入式程序猿都是半路出家转行过来的,不少人仅仅会C语言。因此,文中的演示样例代码所有採用C语言来实现。作为一篇入门短文,文中给出的代码更注重于示范性,尽可能的保持易读性。因此,文中的代码并不追求最高效的实现,但对于一般的应用却也足够高速了。

    从奇偶校验说起

    所谓通讯过程的校验是指在通讯数据后加上一些附加信息,通过这些附加信息来推断接收到的数据是否和发送出的数据同样。比方说RS232串行通讯能够设置奇偶校验位,所谓奇偶校验就是在发送的每个字节后都加上一位,使得每个字节中1的个数为奇数个或偶数个。比方我们要发送的字节是0x1a,二进制表示为0001 1010。

    採用奇校验,则在数据后补上个0,数据变为0001 1010 0,数据中1的个数为奇数个(3个)

    採用奇校验,则在数据后补上个1,数据变为0001 1010 1,数据中1的个数为偶数个(4个)

    接收方通过计算数据中1个数是否满足奇偶性来确定数据是否有错。

    奇偶校验的缺点也非常明显,首先,它对错误的检測概率大约仅仅有50%。也就是仅仅有一半的错误它能够检測出来。另外,每传输一个字节都要附加一位校验位,对传输效率的影响非常大。因此,在快速数据通讯中非常少採用奇偶校验。奇偶校验长处也非常明显,它非常easy,因此能够用硬件来实现,这样能够降低软件的负担。因此,奇偶校验也被广泛的应用着。

    奇偶校验就先介绍到这来,之所以从奇偶校验说起,是由于这样的校验方式最简单,并且后面将会知道奇偶校验事实上就是CRC 校验的一种(CRC-1)。

    累加和校验

    还有一种常见的校验方式是累加和校验。所谓累加和校验实现方式有非常多种,最经常使用的一种是在一次通讯数据包的最后增加一个字节的校验数据。这个字节内容为前面数据包中所有数据的忽略进位的按字节累加和。比方以下的样例:

    我们要传输的信息为: 6、23、4

    加上校验和后的数据包:6、23、4、33

    这里 33 为前三个字节的校验和。接收方收到所有数据后对前三个数据进行相同的累加计算,假设累加和与最后一个字节相同的话就觉得传输的数据没有错误。

    累加和校验由于实现起来很easy,也被广泛的採用。可是这样的校验方式的检错能力也比較一般,对于单字节的校验和大概有1/256 的概率将原本是错误的通讯数据误判为正确数据。之所以这里介绍这样的校验,是由于CRC校验在数据传输的形式上与累加和校验是同样的,都能够表示为:通讯数据 校验字节(也可能是多个字节)

    初识 CRC 算法

    CRC 算法的基本思想是将传输的数据当做一个位数非常长的数。将这个数除以还有一个数。得到的余数作为校验数据附加到原数据后面。还以上面样例中的数据为例:

    6、23、4 能够看做一个2进制数: 0000011000010111 00000010

    假如被除数选9,二进制表示为:1001

    则除法运算能够表示为:

    能够看到,最后的余数为1。假设我们将这个余数作为校验和的话,传输的数据则是:6、23、4、1

    CRC 算法和这个过程有点类似,只是採用的不是上面样例中的通常的这样的除法。在CRC算法中,将二进制数据流作为多项式的系数,然后进行的是多项式的乘除法。还是举个样例吧。

    比方说我们有两个二进制数,分别为:1101 和1011。

    1101 与例如以下的多项式相联系:1x3+1x2+0x1+1x0=x3+x2+x0

    1011与例如以下的多项式相联系:1x3+0x2+1x1+1x0=x3+x1+x0

    两个多项式的乘法:(x3+x2+x0)(x3+x1+x0)=x6+x5+x4+x3+x3+x3+x2+x1+x0

    得到结果后,合并同类项时採用模2运算。也就是说乘除法採用正常的多项式乘除法,而加减法都採用模2运算。所谓模2运算就是结果除以2后取余数。比方3 mod 2 = 1。因此,上面终于得到的多项式为:x6+x5+x4+x3+x2+x1+x0,相应的二进制数:111111

    加减法採用模2运算后事实上就成了一种运算了,就是我们通常所说的异或运算:

    0+0=0

    0+1=1

    1+0=1

    1+1=0

    0-0=0

    1-0=1

    0-1=1

    1-1=0

    上面说了半天多项式,事实上就算是不引入多项式乘除法的概念也能够说明这些运算的特殊之处。仅仅只是差点儿全部解说 CRC 算法的文献中都会提到多项式,因此这里也简单的写了一点主要的概念。只是总用这样的多项式表示也非常罗嗦,以下的解说中将尽量採用更简洁的写法。

    除法运算与上面给出的乘法概念类似,还是遇到加减的地方都用异或运算来取代。以下是一个样例:

    要传输的数据为:1101011011

    除数设为:10011

    在计算前先将原始数据后面填上4个0:11010110110000,之所以要补0,后面再做解释。

    从这个样例能够看出,採用了模2的加减法后,不需要考虑借位的问题,所以除法变简单了。最后得到的余数就是CRC 校验字。为了进行CRC运算,也就是这样的特殊的除法运算,必需要指定个被除数,在CRC算法中,这个被除数有一个专有名称叫做“生成多项式”。生成多项式的选取是个非常有难度的问题,假设选的不好,那么检出错误的概率就会低非常多。好在这个问题已经被专家们研究了非常长一段时间了,对于我们这些使用者来说,仅仅要把现成的成果拿来用即可了。

    最经常使用的几种生成多项式例如以下:

    CRC8=X8+X5+X4+X0

    CRC-CCITT=X16+X12+X5+X0

    CRC16=X16+X15+X2+X0

    CRC12=X12+X11+X3+X2+X0

    CRC32=X32+X26+X23+X22+X16+X12+X11+X10+X8+X7+X5+X4+X2+X1+X0

    有一点要特别注意,文献中提到的生成多项式常常会说到多项式的位宽(Width,简记为W),这个位宽不是多项式相应的二进制数的位数,而是位数减1。比方CRC8中用到的位宽为8的生成多项式,事实上相应得二进制数有九位:100110001。另外一点,多项式表示和二进制表示都非常繁琐,交流起来不方便,因此,文献中多用16进制简写法来表示,由于生成多项式的最高位肯定为1,最高位的位置由位宽可知,故在简记式中,将最高的1统一去掉了,如CRC32的生成多项式简记为04C11DB7实际上表示的是104C11DB7。当然,这样简记除了方便外,在编程计算时也有它的用处。

    对于上面的样例,位宽为4(W=4),依照CRC算法的要求,计算前要在原始数据后填上W个0,也就是4个0。

    位宽W=1的生成多项式(CRC1)有两种,各自是X1和X1+X0,读者能够自己证明10 相应的就是奇偶校验中的奇校验,而11相应则是偶校验。因此,写到这里我们知道了奇偶校验事实上就是CRC校验的一种特例,这也是我要以奇偶校验作为开篇介绍的原因了。

    CRC算法的编程实现

    说了这么多总算到了核心部分了。从前面的介绍我们知道CRC校验核心就是实现无借位的除法运算。以下还是通过一个样例来说明怎样实现CRC校验。

    如果我们的生成多项式为:100110001(简记为0x31),也就是CRC-8

    则计算过程例如以下:

    (1)      将CRC寄存器(8-bits,比生成多项式少1bit)赋初值0

    (2)      在待传输信息流后面增加8个0

    (3)      While (数据未处理完)

    (4)      Begin

    (5)          If (CRC寄存器首位是1)

    (6)              reg = reg XOR 0x31

    (7)          CRC寄存器左移一位,读入一个新的数据于CRC寄存器的0 bit的位置。

    (8)      End

    (9)      CRC寄存器就是我们所要求的余数。

    实际上,真正的CRC 计算通常与上面描写叙述的还有些出入。这是由于这样的最主要的CRC除法有个非常明显的缺陷,就是数据流的开头加入一些0并不影响最后校验字的结果。这个问题非常让人恼火啊,因此真正应用的CRC 算法基本都在原始的CRC算法的基础上做了些小的修改。

    所谓的修改,也就是添加了两个概念,第一个是“余数初始值”,第二个是“结果异或值”。

    所谓的“余数初始值”就是在计算CRC值的開始,给CRC寄存器一个初始值。“结果异或值”是在其余计算完毕后将CRC寄存器的值在与这个值进行一下异或操作作为最后的校验值。

    常见的三种CRC 标准用到个各个參数例如以下表。

    CCITT

    CRC16

    CRC32

    校验和位宽W

    16

    16

    32

    生成多项式

    x16+x12+x5+1

    x16+x15+x2+1

    x32+x26+x23+x22+x16+

    x12+x11+x10+x8+x7+x5+

    x4+x2+x1+1

    除数(多项式)

    0x1021

    0x8005

    0x04C11DB7

    余数初始值

    0xFFFF

    0x0000

    0xFFFFFFFF

    结果异或值

    0x0000

    0x0000

    0xFFFFFFFF

    增加这些变形后,常见的算法描写叙述形式就成了这个样子了:

    (1)      设置CRC寄存器,并给其赋值为“余数初始值”。

    (2)      将数据的第一个8-bit字符与CRC寄存器进行异或,并把结果存入CRC寄存器。

    (3)      CRC寄存器向右移一位,MSB补零,移出并检查LSB。

    (4)      假设LSB为0,反复第三步;若LSB为1,CRC寄存器与0x31相异或。

    (5)      反复第3与第4步直到8次移位所有完毕。此时一个8-bit数据处理完毕。

    (6)      反复第2至第5步直到所有数据所有处理完毕。

    (7)      终于CRC寄存器的内容与“结果异或值”进行或非操作后即为CRC值。

    演示样例性的C代码例如以下所看到的,由于效率非常低,项目中如对计算时间有要求应该避免採用这种代码。只是这个代码已经比网上常见的计算代码要好了,由于这个代码有一个crc的參数,能够将上次计算的crc结果传入函数中作为这次计算的初始值,这对大数据块的CRC计算是非常实用的,不须要一次将全部数据读入内存,而是读一部分算一次,全读完后就计算完了。这对内存受限系统还是非常实用的。

    #define POLY        0x1021
    /**
     * Calculating CRC-16 in 'C'
     * @para addr, start of data
     * @para num, length of data
     * @para crc, incoming CRC
     */
    uint16_t crc16(unsigned char *addr, int num, uint16_t crc)
    {
        int i;
        for (; num > 0; num--)              /* Step through bytes in memory */
        {
            crc = crc ^ (*addr++ << 8);     /* Fetch byte from memory, XOR into CRC top byte*/
            for (i = 0; i < 8; i++)             /* Prepare to rotate 8 bits */
            {
                if (crc & 0x8000)            /* b15 is set... */
                    crc = (crc << 1) ^ POLY;    /* rotate and XOR with polynomic */
                else                          /* b15 is clear... */
                    crc <<= 1;                  /* just rotate */
            }                             /* Loop for 8 bits */
            crc &= 0xFFFF;                  /* Ensure CRC remains 16-bit value */
        }                               /* Loop until num=0 */
        return(crc);                    /* Return updated CRC */
    }
    

    上面的代码是我从http://mdfs.net/Info/Comp/Comms/CRC16.htm找到的,只是原始代码有错误,我做了些小的改动。

    以下对这个函数给出个样例片段代码:

        unsigned char data1[] = {'1', '2', '3', '4', '5', '6', '7', '8', '9'};
        unsigned char data2[] = {'5', '6', '7', '8', '9'};
        unsigned short c1, c2;
        c1 = crc16(data1, 9, 0xffff);
        c2 = crc16(data1, 4, 0xffff);
        c2 = crc16(data2, 5, c2);
        printf("%04x
    ", c1);
        printf("%04x
    ", c2);
    

    读者能够验算,c1、c2 的结果都为 29b1。上面代码中crc 的初始值之所以为0xffff,是由于CCITT标准要求的除数初始值就是0xffff。

    上面的算法对数据流逐位进行计算,效率非常低。实际上细致分析CRC计算的数学性质后我们能够多位多位计算,最经常使用的是一种按字节查表的高速算法。该算法基于这样一个事实:计算本字节后的CRC码,等于上一字节余式CRC码的低8位左移8位,加上上一字节CRC右移 8位和本字节之和后所求得的CRC码。假设我们把8位二进制序列数的CRC(共256个)所有计算出来,放在一个表里,编码时仅仅要从表中查找相应的值进行处理就可以。

    依照这种方法,能够有例如以下的代码(这个代码也不是我写的,是我在Micbael Barr的书“Programming Embedded Systems in C and C++” 中找到的,相同,我做了点小小的修改。):

    /*
    crc.h
    */
    
    #ifndef CRC_H_INCLUDED
    #define CRC_H_INCLUDED
    
    /*
    * The CRC parameters. Currently configured for CCITT.
    * Simply modify these to switch to another CRC Standard.
    */
    /*
    #define POLYNOMIAL          0x8005
    #define INITIAL_REMAINDER   0x0000
    #define FINAL_XOR_VALUE     0x0000
    */
    #define POLYNOMIAL          0x1021
    #define INITIAL_REMAINDER   0xFFFF
    #define FINAL_XOR_VALUE     0x0000
    
    /*
    #define POLYNOMIAL          0x1021
    #define POLYNOMIAL          0xA001
    #define INITIAL_REMAINDER   0xFFFF
    #define FINAL_XOR_VALUE     0x0000
    */
    
    /*
    * The width of the CRC calculation and result.
    * Modify the typedef for an 8 or 32-bit CRC standard.
    */
    typedef unsigned short width_t;
    #define WIDTH (8 * sizeof(width_t))
    #define TOPBIT (1 << (WIDTH - 1))
    
    /**
     * Initialize the CRC lookup table.
     * This table is used by crcCompute() to make CRC computation faster.
     */
    void crcInit(void);
    
    /**
     * Compute the CRC checksum of a binary message block.
     * @para message, 用来计算的数据
     * @para nBytes, 数据的长度
     * @note This function expects that crcInit() has been called
     *       first to initialize the CRC lookup table.
     */
    width_t crcCompute(unsigned char * message, unsigned int nBytes);
    
    #endif // CRC_H_INCLUDED
    
    /*
     *crc.c
     */
    
    #include "crc.h"
    /*
    * An array containing the pre-computed intermediate result for each
    * possible byte of input. This is used to speed up the computation.
    */
    static width_t crcTable[256];
    
    /**
     * Initialize the CRC lookup table.
     * This table is used by crcCompute() to make CRC computation faster.
     */
    void crcInit(void)
    {
        width_t remainder;
        width_t dividend;
        int bit;
        /* Perform binary long division, a bit at a time. */
        for(dividend = 0; dividend < 256; dividend++)
        {
            /* Initialize the remainder.  */
            remainder = dividend << (WIDTH - 8);
            /* Shift and XOR with the polynomial.   */
            for(bit = 0; bit < 8; bit++)
            {
                /* Try to divide the current data bit.  */
                if(remainder & TOPBIT)
                {
                    remainder = (remainder << 1) ^ POLYNOMIAL;
                }
                else
                {
                    remainder = remainder << 1;
                }
            }
            /* Save the result in the table. */
            crcTable[dividend] = remainder;
        }
    } /* crcInit() */
    
    /**
     * Compute the CRC checksum of a binary message block.
     * @para message, 用来计算的数据
     * @para nBytes, 数据的长度
     * @note This function expects that crcInit() has been called
     *       first to initialize the CRC lookup table.
     */
    width_t crcCompute(unsigned char * message, unsigned int nBytes)
    {
        unsigned int offset;
        unsigned char byte;
        width_t remainder = INITIAL_REMAINDER;
        /* Divide the message by the polynomial, a byte at a time. */
        for( offset = 0; offset < nBytes; offset++)
        {
            byte = (remainder >> (WIDTH - 8)) ^ message[offset];
            remainder = crcTable[byte] ^ (remainder << 8);
        }
        /* The final remainder is the CRC result. */
        return (remainder ^ FINAL_XOR_VALUE);
    } /* crcCompute() */
    

    上面代码中crcInit() 函数用来计算crcTable,因此在调用 crcCompute 前必须先调用 crcInit()。只是,对于嵌入式系统,RAM是非常紧张的,最好将 crcTable 提前算好,作为常量数据存到程序存储区而不占用RAM空间。CRC 计算实际上还有非常多内容能够介绍,只是对于一般的程序猿来说,知道这些也就差点儿相同了。余下的部分以后有时间了我再写篇文章来介绍吧。

  • 相关阅读:
    实验吧因缺思汀的绕过注入解析
    python模块中requests参数stream
    土司论坛nc反弹神器使用方法
    msf web脚本反弹shell
    python 多线程
    metasploit后门维持技术
    ew做socks5代理
    双内网渗透代理之reGeorg+Proxifier
    SVN源码泄露漏洞
    Java反射的实例
  • 原文地址:https://www.cnblogs.com/blfshiye/p/4329769.html
Copyright © 2011-2022 走看看