什么是CRC???
你说小学3年级的小明同学不知好歹喜欢村长女儿王大麻子,于是羞涩的他想到写一封情书已表心意。正所谓闺女似情人,爱女心切的村长凡是信件统统要过他之手。如果这份情书被她爸稍加“几笔”岂不悲剧了?
奇偶验证
如何验证情书是否被动过手脚(验证数据是否损坏),介于王大麻子数学不行,数数还行。作为数学课代表的小明同学立刻想到一个好主意:将所有的文字转化二进制,只要数一数1的数量是奇数还是偶数不就可以了吗!
比如我要传递M这个字符那么他的ASCII的值为0100 1101(M),就是数据末尾加上一个1或0,使其整个数据的1的数量可以凑齐奇数或偶数。
如果规定信件所有1的个数为奇数的话,那么0100 1101(M)才4个1,我们就数据的末尾加上一个1凑齐奇数(5个1):0100 1101 1;如果数据中的1刚好奇数,我们就在末尾加上一个0就可(这个方法叫做奇校验),当然相反如果规定的信件所有1的个数为偶数(偶校验),刚好处理的方法也是相反。
虽然这个方法简单到连他没有上学的弟弟都做得起(很容易通过硬件方式实现验证),但是这个的出错率是五五开啊(刚好少/多偶数个1),且永远检查不出0的错误(就算多100个零也看不出来)。
累加和校验
你说奇偶不行,哪我相加总该行吧?于是乎小明同学又推出第二号方案:将每一个文字(字节)的值相加来验证。
比如传递LOVE,我们就可以得到四个数字76(L) 79(O) 86(V) 69(E),他们相加可得310,如果这个数字嫌他太大了不好写,我们可以限制他在0~255(一个字节)这个范围之内,那么我们就得到一个验证数字55 =310 – 255(最简单的办法就是减去255的倍数)。然后将数字附加数据后面,对方接受到数据执行同样的操作对比验证数字是否一致。
虽说这个方法要比上面的方法要可靠一些,但凡事总有列外:
一、比如数据错误刚好等于验证数字
我们要传输的数据:76 79 86 69 310(验证数字)
发生错误的数据: 76 78(-1) 87(+1) 69 310(验证数字还是一样的)
二、单个字符的出错率为1/256
CRC验证原理
无数日夜小明同学冥思苦想,最终还剩最后三根头发之际他学会除法,头顶一凉想到一个绝佳主意:如果我将信件上的所有文字组合成一个长数字,然后用一个我和王大麻子都知道的数字(约定的多项式)去除以,将余数作为一个验证数字的话……
假设我要传输一个字符87(W),和王大麻子约定除数为6,那么结果很明显就是87 ÷ 6 = 14……3,那么我们可以将3作为验证数字附加原始数据的末尾发送。
但明显我们常规的借位除法大大超出了王大麻子的数学水平,于是小明同学决定不做人了发明一个二进制除法,并取名为“模二除法”。
所谓模二除法实际形式上和我们的平常使用的除法一样的(用于二进制运算),但是唯一不一同的是关于计算当中一切的加减法统统换成“异或”(XOR),一句话概括异或运算就是:异性相吸(返回真1),同性相斥(返回假0):1 Xor 0 = 1,0 Xor 1 = 1, 1 Xor 1 = 0, 0 Xor 0 = 0。
那么我们用上面列子来试一下模二除法(模二除法的结果不等于普通除法)
王大麻子表示:我去!算完之后我还要核对余数?!不能再简单点吗?
于是乎小明同学又开始没日没夜苦想,最终当最后的狼牙山“三壮士”也离他而去时,他头顶一凉想到一个绝佳主意:在信件数据的末尾(即数据低位LSB)补上我和王大麻子都知道的数字的长度-1的0(生成项长度-1)然后在被同知数字相除,得到的余数再附加原始数据的末尾上。(这里说的原始数据是指没有补零的,且余数长度一定与补零的长度一致)
口说无凭,我们来重新用这个升级计算方法试一试。
我们将余数10补在原始数据1010111的末尾得到了一个新数:101011110,然后我们再用110去除,神奇的事情发生了:没有余数了。(接受端只要将已修改的数据与生成项模二相除没有余数即代表数据无误)
这便是CRC(循环冗余校验,Cyclic Redundancy Check)是一种为了检测数据是否损坏处理办法。而当中的模二除法是无借位(不向高位借位的)的性质十分重要:意味我们计算CRC只需要简单的数据位移和Xor即可(到后面你就知道了)。
当然理论上讲CRC还是出错的可能(不过已经比程序猿能找到女朋友的几率要低了),所以选择一个好的除数就是一个非常重要的事情,关于CRC的除数有个专业的名称:生成多项式,简称生成项。如何选择生成项这是需要一定的代数编码知识(已经不是我这种咸鱼能搞懂的问题了)。好在我们可以直接用大佬计算好的生成项,不同的生成项有不同的CRC,如:CRC8、CRC16、CRC-CCITT、CRC32……。
从数学的角度来理解CRC(如果您只是了解如何计算CRC的话可以直接跳过本节)
我们不妨尝试将二进制转化一种多项式:数字中多少位1代表的是x的多少次方,0乘以任何数都是0所以不用管。
比如101010(42),可以转化为:x^5+x^3+x。(注意最低位0次方,任何数零次方都等于1)
假设用x^3+x^2+x除去x+1,可以得到如下式子:
很好我们在两边再乘一个x+1:
这里不难看出:得到一个商×除数+余数的式子,其中的(x^2+1)就是商,-1就是余数(我也不没懂为啥余数是负数)。我们还可以对左边式子再提取一次x,可得:
我们可以理解这里提取的x是对于x^2+x+1(1011)进行补一次零变成了10110,实际上这个补零还有个说法叫做左移(要注意数据的方向高位在左边)。
如何理解补零的性质这个很简单,我们类比十进制的补零如:1要补两次零变成100,很明显补零就是乘与10的几次方。回到二进制中就是2的几次方,而多项式中的x就可以代表2。
通过以上的式子的整理可以得出通用公式即是:
就代表的是原始数据, 代表的是数据末补n个0, 代表的是Key也就是生成项, 代表的余数也是上一节提到FCS。接收者接受到 看看是能被 整除不,可以即为正确数据。
要注意的一点 的长度(补零长度)受限于 (目的是可以直接追加在末尾,而不影响原始数据):他们长度一致,且一定比 的长度少1。
关于 为什么一定比 的长度少1?我个人愚见是特殊的模二除法: 的最高位一定1(不然没有意义啊),而数据处理过程中需要计算数据的最高位也是1(可以对照着除法的当中与除数对应的那个被除数的那部分数据),他们进行Xor就变成0,实际计算往往是剩下的部分( 长度-1)(在程序设计中反正都会变成0干脆都不计算首位,这就是为啥网上的CRC生成多项式的简记码都是默认舍弃首位1的原因)。
CRC的原理实际上远比这些要复杂的多,涉及到群论这类我们这些吃瓜群众可望不可即的数理知识。以上也只是我对Wiki的搬运和理解瞎猜,希望大家能通过这个简单列子大概了解CRC的性质,如有不对之处有望大佬不惜赐教!
直接计算法
虽说上一节我们已经知道CRC是怎么计算的,但明显电脑可不会写除法公式(程序很难直接用这种方法)。且不说要对齐一个超长二进制数据进行逐位计算的困难不说,单单是像vbscript这种既没有几个位操作符又用起来蛋疼的语言基本上是很难实现(大佬:喵喵喵?)。不过可以转化一个思路:比如每次只取一部分数据来计算如何?
仔细观察计算的过程,可以发现其实每一次Xor都是固定不动的生成项与其对应的数据首位“消1”。那我们就可以假想出一个与生成项长度一致的“盒子”,取出一部分的数据出来若首位是1时就进行一次Xor,遇到0则左移到1为止,左移造成的右端的空缺用0补充。而这里0希望理解为一种“存储”,它“存储” 生成项中未和数据进行计算的那一部分,按顺序先后附加被计算数据的后面,当先一部分的数据全部计算之后,实际上“盒子”中剩下都是未和数据计算的部分的“和”11011 xor 10110 = 11011 xor ( 10000 xor 00100 xor 00010)(这里实际上就是Xor的交换律到后面就会体会到他的强大)
驱动表法
实际上CRC就像开心消消乐一样,就是不断消除首位数据。这时你想:要是能一口气消除一个字节(8bit)以上的数据那该多好!
一般来讲文艺青年会这么做(CRC的正常计算):
但是2B青年会这么想:可不可以将生成项先Xor了?
我们可以先提前计算出与数据前几位相符的生成项之和再Xor从而到达一口气消掉了多位数据的目的,Xor的乘法交换律允许我们提前计算出需要的数据A Xor B Xor C = A Xor ( B Xor C )
既然如此干脆直接将前八位0000 0000 ~ 1111 1111(0 ~ 255,一个字节)这个范围所有的生产项的和全部计算存储成表格,等计算的时候直接取出数据的首字节出来作为索引找到对应表格的中生存项的和与去掉首位字节的数据进行Xor不就可以了吗。
表的由来
虽说想法很美好,但是如何实现前8位从0~255所有的生成项之和了?我们来思考一下CRC计算的本质是什么?复读没错是不断地消除首位数据,那么在Xor运算下消除方法就是:数据一样!
那么我们将0~255这256个数字进行CRC逐位计算后剩下不就是已经消除掉前8位数据的生成项之和吗!(因为前8位数据一样所以被消除了)
用通俗点语言就是:我们提前将一个字节的CRC验证码计算出来。
以下就是关于CRC16的正序表格计算代码:
Const PLAY = &H8005& '这里生成项(CRC16),注意这里默认首位1是去掉的
ReDim Table(255) '这里存储表格
'// 计算表格部分
Dim I,J,Temp
'对于0~255这256个数字进行CRC计算,并将计算好的CRC验证码按顺序存储在表格(数组)中
For I = 0 To 255
Temp = (I * &H100&) And &HFFFF&
For J = 0 To 7
If (Temp And &H8000) Then
Temp = (Temp * 2) And &HFFFF&
Temp = Temp Xor PLAY
Else
Temp = (Temp * 2) And &HFFFF&
End If
Next
Table(I) = Temp And &HFFFF&
Next
// 输出CRC16表格代码(使用VbsEdit的调试)
Dim y,x,u
Debug.WriteLine "Dim CRC16_Table:CRC16_Table = Array( _"
For y = 1 To 64
For x = 1 To 4
Debug.Write "&H",String(4 - Len(Hex(Table(u))),"0"),Hex(Table(u)),"&"
If u <> 255 Then Debug.Write ", " Else Debug.Write " "
u = u + 1
Next
Debug.WriteLine "_"
Next
Debug.WriteLine ")"
代码无误的话,应该会在调试框中得到如下代码:
1 Dim CRC16_Table:CRC16_Table = Array( _
2 &H0000&, &H8005&, &H800F&, &H000A&, _
3 &H801B&, &H001E&, &H0014&, &H8011&, _
4 &H8033&, &H0036&, &H003C&, &H8039&, _
5 &H0028&, &H802D&, &H8027&, &H0022&, _
6 &H8063&, &H0066&, &H006C&, &H8069&, _
7 &H0078&, &H807D&, &H8077&, &H0072&, _
8 &H0050&, &H8055&, &H805F&, &H005A&, _
9 &H804B&, &H004E&, &H0044&, &H8041&, _
10 &H80C3&, &H00C6&, &H00CC&, &H80C9&, _
11 &H00D8&, &H80DD&, &H80D7&, &H00D2&, _
12 &H00F0&, &H80F5&, &H80FF&, &H00FA&, _
13 &H80EB&, &H00EE&, &H00E4&, &H80E1&, _
14 &H00A0&, &H80A5&, &H80AF&, &H00AA&, _
15 &H80BB&, &H00BE&, &H00B4&, &H80B1&, _
16 &H8093&, &H0096&, &H009C&, &H8099&, _
17 &H0088&, &H808D&, &H8087&, &H0082&, _
18 &H8183&, &H0186&, &H018C&, &H8189&, _
19 &H0198&, &H819D&, &H8197&, &H0192&, _
20 &H01B0&, &H81B5&, &H81BF&, &H01BA&, _
21 &H81AB&, &H01AE&, &H01A4&, &H81A1&, _
22 &H01E0&, &H81E5&, &H81EF&, &H01EA&, _
23 &H81FB&, &H01FE&, &H01F4&, &H81F1&, _
24 &H81D3&, &H01D6&, &H01DC&, &H81D9&, _
25 &H01C8&, &H81CD&, &H81C7&, &H01C2&, _
26 &H0140&, &H8145&, &H814F&, &H014A&, _
27 &H815B&, &H015E&, &H0154&, &H8151&, _
28 &H8173&, &H0176&, &H017C&, &H8179&, _
29 &H0168&, &H816D&, &H8167&, &H0162&, _
30 &H8123&, &H0126&, &H012C&, &H8129&, _
31 &H0138&, &H813D&, &H8137&, &H0132&, _
32 &H0110&, &H8115&, &H811F&, &H011A&, _
33 &H810B&, &H010E&, &H0104&, &H8101&, _
34 &H8303&, &H0306&, &H030C&, &H8309&, _
35 &H0318&, &H831D&, &H8317&, &H0312&, _
36 &H0330&, &H8335&, &H833F&, &H033A&, _
37 &H832B&, &H032E&, &H0324&, &H8321&, _
38 &H0360&, &H8365&, &H836F&, &H036A&, _
39 &H837B&, &H037E&, &H0374&, &H8371&, _
40 &H8353&, &H0356&, &H035C&, &H8359&, _
41 &H0348&, &H834D&, &H8347&, &H0342&, _
42 &H03C0&, &H83C5&, &H83CF&, &H03CA&, _
43 &H83DB&, &H03DE&, &H03D4&, &H83D1&, _
44 &H83F3&, &H03F6&, &H03FC&, &H83F9&, _
45 &H03E8&, &H83ED&, &H83E7&, &H03E2&, _
46 &H83A3&, &H03A6&, &H03AC&, &H83A9&, _
47 &H03B8&, &H83BD&, &H83B7&, &H03B2&, _
48 &H0390&, &H8395&, &H839F&, &H039A&, _
49 &H838B&, &H038E&, &H0384&, &H8381&, _
50 &H0280&, &H8285&, &H828F&, &H028A&, _
51 &H829B&, &H029E&, &H0294&, &H8291&, _
52 &H82B3&, &H02B6&, &H02BC&, &H82B9&, _
53 &H02A8&, &H82AD&, &H82A7&, &H02A2&, _
54 &H82E3&, &H02E6&, &H02EC&, &H82E9&, _
55 &H02F8&, &H82FD&, &H82F7&, &H02F2&, _
56 &H02D0&, &H82D5&, &H82DF&, &H02DA&, _
57 &H82CB&, &H02CE&, &H02C4&, &H82C1&, _
58 &H8243&, &H0246&, &H024C&, &H8249&, _
59 &H0258&, &H825D&, &H8257&, &H0252&, _
60 &H0270&, &H8275&, &H827F&, &H027A&, _
61 &H826B&, &H026E&, &H0264&, &H8261&, _
62 &H0220&, &H8225&, &H822F&, &H022A&, _
63 &H823B&, &H023E&, &H0234&, &H8231&, _
64 &H8213&, &H0216&, &H021C&, &H8219&, _
65 &H0208&, &H820D&, &H8207&, &H0202& _
66 )
我们可以以空间换取时间,直接将CRC表格计算好作为一个常数数组使用。
得到表格之后回到驱动表法的计算:取出CRC寄存器中的首位字节,然后将CRC左移去掉首字节然后取出一字节新数据装入CRC低端空出字节中,根据取出首字节找到对应表格中的生成项之和与CRC寄存器进行Xor,然后重复这个步骤直到数据全部取完计算完。要注意的是:因为驱动表法是一个一个字节计算,所以他必须计算之前在原始数据上补零(不能像直接计算法那样通过逐位左移方式自己完成补零)。