赞
踩
一、通讯校检
在一个p位二进制数据序列之后附加一个r位二进制校检码,构成一个总长为p+r的二进制序列。附加在数据序列之后的这个校检码与p位二进制序列之间存在一个特定的关系,如果因干扰等原因使得数据序列中的一些位发生错误,这种特性的关系就会破坏。因此,可以通过检查该关系,实现对接收到的数据正确性的检验。
根据校检码与p位二进制序列之间的关系,可以将通讯校检方式分为:
a. 奇偶校检:每个字节的校检码与该字节(包括校检码)中1的个数对应;
b. 累加和校检:每个数据包的校检码为该数据包中所有数据忽略进位的累加和;
c. CRC-xx校检:每个二进制序列的校检码为该序列与所选择的GF(2)多项式模2除法的余数。
奇偶校验检测错误概率大约为50%,简单但传输效率低。奇偶校验多用于低速度数据通讯,如RS232。
累加和校验检测错误概率大概为1/256,实现简单,也被广泛的采用。
CRC校检,只要选择的除数GF(2)多项式位数足够多,检测错误的概率几乎不存在。
二、CRC算法背景
1. 几个基本的概念
1) 帧检测序列FCS(Frame CheckSequence):为进行差错检验而添加的冗余码。
2) 多项式模2除法:不考虑进位、错位的二进制加减法;
3) 生成多项式:当进行CRC检验时,发送方和接受方事先约定一个除数,即生成多项式G(x),常用的CRC码的生成多项式为:
- CRC8=X8+X5+X4+1
-
- CRC-CCITT=X16+X12+X5+1
-
- CRC16=X16+X15+X5+1
-
- CRC12=X12+X11+X3+X2+1
-
- CRC32=X32+X26+X23+X22+X16+X12+X11+X10+X8+X7+X5+X4+X2+X1+1
每一个生成二项式与一个二进制序列对应,如CC8对应的二进制序列为:100110001。
2. CRC校检码的计算
设信息字段为K位,校验字段为R位,则码字长度为N(N=K+R)。设双方事先约定了一个R次多项式g(x),则CRC码:
V(x)=A(x)g(x)=xRm(x)+r(x)
其中: m(x)为K次信息多项式, r(x)为R-1次校验多项式。
这里r(x)对应的代码即为冗余码,加在原信息字段后即形成CRC码。
r(x)的计算方法为:在K位信息字段的后面添加R个0,再除以g(x)对应的代码序列,得到的余数即为r(x)对应的代码(应为R-1位;若不足,而在高位补0)。
计算示例:
设需要发送的信息为M = 1010001101,产生多项式对应的代码为P = 110101,R=5。在M后加5个0,然后对P做模2除法运算,得余数r(x)对应的代码:01110。故实际需要发送的数据是101000110101110。
当接收方收到数据后,用收到的数据对P(事先约定的)进行模2除法,若余数为0,则认为数据传输无差错;若余数不为0,则认为数据传输出现了错误,由于不知道错误发生在什么地方,因而不能进行自动纠正,一般的做法是丢弃接收的数据。
3. 数学推理
设欲传输的信息有K位,如下图所示,首先将欲传输的数据序列m(x)乘以 XR , 其中R为g(x)的最高次冥。将得到的多项式XR m(x)除以约定的多项式g(x),忽略除法结果的“商”,取出其余数,并与XRm(x)相加,形成K+R位的发送序列,即:m’(x) = XRm(x) +r(x)。
CRC编码过程如下:
设待校验的信息码有k位,即:m = (mk-1、mk-2、mk-3……m1、m0), 多项式m(x)可表示为
m(x) = mk-1xk-1+ mk-2xk-2 +……m1x1+ m0x0 --------式(1)
用多项式g(x)的最高次幂R对应的XR 乘以m(x),将得到式(2)
XR m(x) = mk-1xk+R-1+mk-2xk+R-2 +……m1x1+R+ m0x0+R -- 式(2)
将XR m(x) 模2除以g(x),得到多项式商为A(x),余数为r(x),即:
A(x)g(x) = XR m(x) +r(x) -----------------------------式(3)
余数多项式r(x)可表示为
r(x) = rR-1xR-1+rR-2xR-2 +……r1x1+ r0x0 -------------式(4)
将式(2)和式(4)代入式(3)得
A(x)g(x) = mk-1xk+R-1+mk-2xk+R-2 +……m1x1+R+ m0x0+R + rR-1xR-1+rR-2xR-2 +……r1x1+ r0x0 -----------------------式(5)
式(5)对应的码组为K+R位,即:
N = (mk-1+ mk-2 +……m1+ m0 + rR-1+rR-2 +……r1+ r0)-式(6)
从M到N就是CRC的编码过程mk-1+ mk-2 +……m1+ m0 为k位信息码;rR-1+ rR-2 +……r1+ r0为R位校验码。
在信息接收端,将接受到的K+R位码除以相同的多项式g(x),根据式(3)所产生的余数为0,则接受到的数据信息正确无误,否则则认为信息在传输过程中产生的误码。
根据式(1)~式(6),CRC编码必须进行模2除运算,CRC的校验位就是模2除得到的余数,如果余数用寄存器的存数表示,模2除用异或门表示,那么通用的CRC串行电路就可以同下图所示的电路来实现。
4. “余数初始值”和“结果异或值”
“余数初始值”就是在计算CRC值的开始,给CRC寄存器一个初始值。“结果异或值”是在其余计算完成后将CRC寄存器的值在与这个值进行一下异或操作作为最后的校验值。
三、CRC的参数模型CRC32 的参数模型是:
生成项的简写。以 16 进制表示, 即是 0x04C11DB7。 忽略了最高位的"1",即完整的生成项是
0x104C11DB7。
重要的一点是,这是“未颠倒”的生成项! 前面说过,“颠倒的” 生成项是 0xEDB88320。
INIT
这是算法开始时寄存器的初始化预置值,十六进制表示。这个值可以直接赋值给“直驱查表法” 算法中的寄存器, 作为寄存器的初始值!
REFIN
这个值是真 TRUE 或假 FALSE。如果这个值是 FALSE, 表示待测数据的每个字节都不用“颠倒”, 即 BIT7 仍是作为最高位, BIT0作为最低位。
如果这个值是 TRUE,表示待测数据的每个字节都要先“颠倒”, 即 BIT7 作为最低位, BIT0作为最高位。
REFOUT
这个值是真 TRUE 或假 FALSE。如果这个值是 FALSE, 表示计算结束后, 寄存器中的值直接进入 XOROUT 处理即可。如果这个值是 TRUE, 表示计算结束后, 寄存器中的值要先“颠倒”, 再进入 XOROUT 处理。注意,这是将整个寄存器的值颠倒, 因为寄存器的各个字节合起来表达了一个值, 如果只是对各个字节各自颠倒, 那结果值就错误了。
XOROUT
这是 W 位长的 16 进制数值。这个值与经REFOUT 后的寄存器的值相 XOR, 得到的值就是最终正式的 CRC 值!CHECK这不是定义值的一部分,这只是字串 "123456789"用这个 CRC 参数模型计算后得到的 CRC 值, 作为参考。
我们发现, CRC32 模型的 Init=0xFFFFFFFF, 就是说寄存器要用 0xFFFFFFFF 进行初始化,而不是 0。为什么? 因为待测数据的内容和长度是随机的, 如果寄存器初始值为 0,那么, 待测字节是 1 字节的0x00, 与待测字节是 N 字节的 0x00, 计算出来的CRC32 值都是 0, 那 CRC 值就没有意义了!所以寄存器用0xFFFFFFFF 进行初始化, 就可以避免这个问题了!RefIn=True,表示输入数据的每个字节需要“颠倒”! 为什么要“颠倒”, 因为很多硬件在发送时是先发送最低位 LSB 的! 比如 UART 等。
字节顺序不用颠倒, 只是每个字节内部的比特进行颠倒。例如待测的字串是"1234",这时也是一样先处理"1",再处理"2",一直到处理"4"。处理字符"1"时, 它是 0x31, 即 0011 0001, 需要先将它颠倒, 变成低位在前, 即 1000 1100,即 0x8C, 再进行处理。
也就是说, 待处理的数据是0x31 32 33 34, 颠倒后就变成 0x8C 4C CC 2C, 再进行 CRC 计算。RefOut=True, 表示计算完成后, 要将寄存器中的值再颠倒。注意, 这是将整个寄存器的值颠倒, 即如果寄存器中的值是 0x31 32 33 34, 颠倒后就变成 0x2C CC 4C 8C!XorOut=FFFFFFFF, 表示还需要将结果值与 0xffffffff 进行 XOR,这样就得到最终的 CRC32 值了!
不同的通讯方式采用不同的CRC模型,更多CRC模型:
http://reveng.sourceforge.net/crc-catalogue/
常见的三种CRC 标准用到个各个参数如下表。
四、CRC算法的编程实现
1. 直接计算法
假设有一个4 bits的寄存器,通过反复的移位和进行CRC的除法,最终该寄存器中的值就是我们所要求的余数。
最最简单的算法:
把register中的值置0.
把原始的数据后添加w个0.
While (还有剩余没有处理的数据)
Begin
把register中的值左移一位,读入一个新的数据并置于register最低位的位置。
If (如果上一步的左移操作中的移出的一位是1)
register = register XOR Poly.
End
/****************************************************************
* 函数名 : CalcCrcDirectly
* 函数描述 : 直接计算法计算CRC-32校验码
* 输入参数 : data-待计算数据块的首地址
size-待计算数据块的32字数量
* 输出结果 : 无
* 返回值 : 无
****************************************************************/
- u32 CalcCrcDirectly (u32* data, u32size)
-
- {
-
- u32 i,j,temp,crc = 0xFFFFFFFF;
-
- for(i=0; i<size; i++)
-
- {
-
- temp = data[i];
-
- for(j=0; j<32; j++)
-
- {
-
- if( (crc ^ temp) & 0x80000000 )
-
- {
-
- crc = 0x04C11DB7 ^(crc<<1);
-
- }
-
- else
-
- {
-
- crc <<=1;
-
- }
-
- temp<<=1;
-
- }
-
- crc&=0xFFFFFFFF;
-
- }
-
- return crc;
-
- }
2. 查表法
直接计算法很直观但是却非常低效。为了加快它的速度,我们可以通过查表法使它一次能处理大于4bit的数据。
我们想要实现的 32 bit 的 CRC 校验。我们还是假设有和原来一样的一个 4 "bit"的 register, 但它的每一位是一个 8 bit 的字节。
根据同样的原理我们可以得到如下的算法:
While (还有剩余没有处理的数据)
Begin
检查 register 头字节, 并取得它的值
求不同偏移处多项式的 XOR
register 左移一个字节, 最右处存入新读入的一个字节
把 register 的值 和 多项式的 XOR 结果进行 XOR 运算
End
同样我们还是以一个简单的例子说明问题:
为了简单起见,我们假设一次只移出 4 个比特! 而不是8 个比特。
生成多项式为: 1 0101 1100, 即宽度W=8, 即 CRC8,这样寄存器为 8 位
待测数据是 1011 0100 1101
按正常的算法做:
将 1011 0100 放入寄存器中, 然后开始计算 CRC。
先将高 4 位移出寄存器:
当前 register 中的值: 0100 1101
4 bit 应该被移出的值: 1011
生成多项式为: 1010 1110 0
第一步:
- Top Register ( top 指移出的数据)
- ---- --------
- 1011 0100 1101 待测数
- 1010 1110 0 + (CRC XOR) POLY
- -------------
- 0001 1010 1101 第一次 XOR 后的值
第二步:
这时, 首 4 bits 不为 0 说明没有除尽,要继续除:
0001 1010 1101
1 0101 1100 + (CRC XOR) 将 POLY 右移 3 位后, 再做 XOR
-------------
0000 1111 0001 第二次 XOR 后的值
这时, 首 4 bits 全 0 说明不用继续除了,结果满足要求了。
也就是说: 待测数据与 POLY 相 XOR, 得到的结果再与 POLY 相 XOR, POLY 要适当移位,以消掉 1。 重复
进行, 直到结果满足要求。
下面,我们换一种算法,来达到相同的目的:
POLY 与 POLY 自 已先进行 XOR, 当然 POLY 要进行适当移位。 使得得到的结果值的高 4 位与待测数据相
同。
第一步:
- 1010 1110 0 POLY
- 1 0101 1100 + 右移 3 位后的 POLY
- -------------
- 1011 1011 1100 POLY 与 POLY 自 已进行 XOR 后得到的值
第二步:
- 1011 1011 1100 POLY 相 XOR 后得到的值
- 1011 0100 1101+ 待测数据
- -------------
- 0000 11110001 得到的结果值和上面是一样的(说明可以先把 POLY 预先 XOR 好, 再与待
- 测数据 XOR, 就能得到结果)
结论:
现在我们看到,这二种算法计算的结果是一致的! 这是基于 XOR 的交换律, 即(a XOR b) XOR c = a XOR。而后一种算法可以通过查表来快速完成, 叫做“查找表法” 算法。
也就是说, 根据 4 bit 被移出的值 1011,我们就可以知道要用 POLY 自 身 XOR 后得到的 10111011 1100
来对待测数据 1011 0100 1101 进行 XOR,这样一次就能消掉 4BIT 待测数据。 (注意蓝色的最高 4 位要一样,这样 XOR 后才能得0000, 就能消掉了)。
即 1011 对应 1011 10111100, 实际只需要用到后 8 位, 即 1011 对应 1011 1100用查表法来得到, 即 1011 作为索引值, 查表, 得到表值 1011 1100。
表格可以预先生成。
这里是每次移出 4 位, 则 POLY 与 POLY 进行 XOR 的组合有2^4= 16 种, 即从 0000 到 1111。
注意, POLY 自 身与自 身相 XOR 时,要先对齐到和寄存器一样的长度, 再 XOR。 相当于有 12 位进行 XOR。
这里是每次移出 4 位, 则 POLY 与 POLY 进行 XOR 的组合有2^4= 16 种, 即从 0000 到 1111。
注意, POLY 自 身与自 身相 XOR 时,要先对齐到和寄存器一样的长度, 再 XOR。 相当于有 12 位进行 XOR。
组合后的结果有 16 种: (黑色的 0 表示对齐到和寄存器一样的长度)
- 1. 0000 0000 0000 即表示待测数据移出的 4 位都是 0,不需要与 POLY 相 XOR, 即相当于待测
- 数据移出的 4 位后, 与 0000 0000 0000 相 XOR
- 2. 0001 0101 1100 即表示待测数据移出的 4 位是 0001, 需要与右移过 3 位的POLY 相 XOR
- 3. 0010 1011 1000
- 4. 0010 1011 1000 与 0001 0101 1100 相 XOR, XOR 后前 4 位为0011 即表示待测数据移出的 4 位
- 后, 需要与 POLY 进行二次相 XOR,结果才能满足要求。
- 5. 0101 0111 0000 与 0001 0101 1100 相 XOR, XOR 后前 4 位为0100
- 6. 0101 0111 0000 , 前 4 位为 0101
- 7. 0101 0111 0000 与 0010 1011 1000、 0001 0101 1100 相 XOR, XOR 后前 4 位为0110
- 8. 0101 0111 0000 与 0010 1011 1000, XOR 后前 4 位为 0111
- 9. 1010 1110 0000 与 0010 1011 1000相 XOR, XOR 后前 4 位为 1000
- 10. 1010 1110 0000 与 0010 1011 1000、 0001 0101 1100 相 XOR, XOR 后前 4 位为1001
- 11. 1010 1110 0000, 前 4 位为 1010
- 12. 1010 1110 0000 与 0001 0101 1100 相 XOR, XOR 后前 4 位为1011
- 13. 1010 1110 0000 与 0101 0111 0000、 0010 1011 1000、 0001 0101 1100 相 XOR, XOR 后前 4 位为
- 1100
- 14. 1010 1110 0000 与 0101 0111 0000、 0010 1011 1000相 XOR, XOR 后前 4 位为 1101
- 15. 1010 1110 0000 与 0101 0111 0000、 0001 0101 1100 相 XOR, XOR 后前 4 位为1110
- 16. 1010 1110 0000 与 0101 0111 0000 相 XOR, XOR 后前 4 位为1111
以 XOR 后得到的结果的前 4 位做为索引值,以 XOR 后得到的结果的后 8 位做为表值, 生成一张表, 即:
- TABLE[0]=0000 0000B;
- TABLE[1]=0101 1100B;
- TABLE[2]=1011 1000B;
- TABLE[3]=[(0010 1011 1000B ^ 0001 0101 1100B) >> 4 ] & 0xff
....
这张表我叫它为“直接查询表”。
就是说,一次移出的待测数据的 4 位 bit,有 2^4 个值, 即 0000,0001,0010,....,1111, 根据这个值
来查表, 找到相应的表值, 再用表值来 XOR 寄存器中的待测数据。
所以, 如果一次移出待测数据的 8 位 bit,即一次进行一个字节的计算, 则表格有 2^8= 256 个表值。
CRC16 和 CRC32 都是一次处理一个字节的,所以它们的查询表有 256 个表值。
/********************************************************************
- * 函数名 : CalcCrcByTable
-
- * 函数描述 : 查表计算法计算CRC-32校验码
-
- * 输入参数 : pData-待计算数据块的首地址
-
- Length-待计算数据块的32字数量
-
- * 输出结果 : 无
-
- * 返回值 : 无
********************************************************************/
- u32 CalcCrcByTable(u32 *pData,u32 Length)
-
- {
-
- u32 nReg =0xFFFFFFFF;//CRC寄存器
-
- u32 nTemp=0;
-
- u16 i, n;
-
- for(n=0; n<Length;n++)
-
- {
-
- nReg ^=(u32)pData[n];
-
- for(i=0; i<4;i++)
-
- {
-
- nTemp =Crc32Table[(u8)(( nReg >> 24 ) & 0xff)];
- nReg<<= 8;
-
- nReg ^=nTemp;
-
- }
-
- nReg &=0xFFFFFFFF;
-
- }
-
- return nReg;
-
- }
这个“直接查表法” 算法和“直接计算法” 是完全一样的,不仅结果完全一样, 处理方式也是完全一样的,所以“直接查表法”可以完全替代“直接计算法”!
原始数据都需要先用 0 扩展 W 位; 最开始的几次循环的实质都只是先将待测数据移动到寄存器中去而已。
3. 颠倒的查表法
先决条件是 REFIN=TRUE 并且REFOUT=TRUE 的 CRC 参数模型。
举例来说, 假设待测的原始数据是 10H, 简单起见, 不考虑寄存器移出的字节的影响(即假设它是 00H):
“直接查表法”, 原始数据先颠倒为 01H, 根据 01H 查表得 04C11DB7H, 寄存器移出的字节是向左移。“颠倒的查表法”, 直接根据原始数据10H 查表得 EDB88320H, 寄存器移出的字节是向右移。
可见,这时这二个方法本质上是一样的。
对于“直接查表法”, 颠倒的数据用不颠倒的表索引值、 得到不颠倒的表值寄存器进入寄存器, 得到的寄存器结果值是不颠倒的, 还要再颠倒,变成“颠倒的 CRC”。对于“颠倒的查表法”,不颠倒的数据用颠倒的表索引值、 得到颠倒的表值寄存器进入寄存器,得到的寄存器结果值就已经是“颠倒的 CRC”,不用再颠倒了。
可见, 对于REFIN=TRUE 并且 REFOUT=TRUE 的 CRC模型来说(注意这个先决条件), 就可以直接用“颠倒的查表法” 来代替“直接查表法”,这样原始数据的比特不用镜像, 处理起来就很简单。
- //计算 CRC32 值
- unsigned long CRC32;
- BYTE DataBuf[512]={0x31, 0x32,0x33,0x34} ; //待测数据,为字串"1234"
- unsigned long len;
- unsigned long ii;
- unsigned long m_CRC = 0xFFFFFFFF; //寄存器中预置初始值
- BYTE *p;
- len=4; //待测数据的字节长度
- p = DataBuf;
- for(ii=0; ii <len; ii++)
- {undefined
- m_CRC = crc32_table[( m_CRC^(*(p+ii) ) ) & 0xff] ^ (m_CRC >> 8) ; //计算
- }
- CRC32= ~m_CRC; //取反。 经 WINRAR 对比, CRC32 值正确!!
- //这时, CRC32 中的值就是 CRC
附CRC16和CRC32查找表算法:
- // 构造 16 位 CRC 表 "直接查询表"
- unsigned short i16, j16;
- unsigned short nData16;
- unsigned short nAccum16;
- for ( i16 = 0; i16 < 256; i16++ )
- {undefined
- nData16 = ( unsigned short ) ( i16 << 8 ) ;
- nAccum16 = 0;
- for ( j16 = 0; j16 < 8; j16++ )
- {undefined
- if ( ( nData16 ^ nAccum16 ) & 0x8000 )
- nAccum16 = ( nAccum16 << 1 ) ^ cnCRC_16; //也可以用cnCRC_CCITT
- else
- nAccum16 <<= 1;
- nData16 <<= 1;
- }
- Table_CRC16[i16] = ( unsigned long ) nAccum16;
- }
- // 构造 32 位 CRC 表 "直接查询表"
- unsigned long i32, j32;
- unsigned long nData32;
- unsigned long nAccum32;
- for ( i32 = 0; i32 < 256; i32++ )
- {undefined
- nData32 = ( unsigned long )( i32 << 24 ) ;
- nAccum32 = 0;
- for ( j32 = 0; j32 < 8; j32++ )
- {undefined
- if ( ( nData32 ^ nAccum32 ) & 0x80000000 )
- nAccum32 = ( nAccum32 << 1 ) ^ cnCRC_32;
- else
- nAccum32 <<= 1;
- nData32 <<= 1;
- }
- Table_CRC32[i32] = nAccum32;
- }
颠倒比特的程序:
- unsigned long intReflect(unsigned long int ref, char ch)
- {
- unsigned long int value=0;
- // 交换 bit0 和 bit7, bit1 和 bit6, 类推
- for(int i = 1; i < (ch + 1); i++)
- {undefined
- if(ref & 1)
- value |= 1 << (ch - i);
- ref >>= 1;
- }
- return value;
- }
颠倒查找表:
- unsigned long intcrc32_table[256];
- unsigned long int ulPolynomial = 0x04c11db7;
- unsigned long int crc,temp;
- for(int i = 0; i <= 0xFF; i++) // 生成 CRC32“正规查询表”
- {undefined
- temp=Reflect(i, 8);
- crc32_table[i]= temp<< 24;
- for (int j = 0; j < 8; j++)
- {undefined
- unsigned long int t1, t2;
- unsigned long int flag=crc32_table[i] &0x80000000;
- t1=(crc32_table[i] << 1) ;
- if(flag==0)
- t2=0;
- else
- t2=ulPolynomial;
- crc32_table[i] =t1^t2 ;
- }
- crc=crc32_table[i];
- crc32_table[i] = Reflect(crc32_table[i], 32) ;
- }
参考博客:https://blog.csdn.net/liyuanbhu/article/details/7882789
https://www.cnblogs.com/dacainiao/p/5565046.html
https://blog.csdn.net/huang_shiyang/article/details/50881305
参考书籍:《STM32自学笔记(第2版)》
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。