本文首发于“嵌入式软件实战派”。
循环冗余校验(英语:Cyclic redundancy check,通称“CRC”)是一种根据网络数据包或电脑文件等数据产生简短固定位数校验码的一种散列函数,主要用来检测或校验数据传输或者保存后可能出现的错误。
Wikipedia
一句话:CRC是将数据计算出散列的方式,一般用于校验数据的完整性。它具有简单、执行效率高等特点。当然,你可以类比于Checksum,但比Checksum复杂些,防碰撞性更好些。
▍CRC的原理
CRC是基于除法的。实际的输入数据会被解释为一个长二进制位流(除数),再将其除以另一个固定二进制数(除数,即多项式)。该除法的其余部分就是校验值。
但是,现实要复杂一些。二进制数(除数和除数)不被视为普通整数值,而是被视为二进制多项式,其中实际位用作系数。
输入数据,看做一串二进制流,用多项式的方式表示为g(x),而除数是国际标准上的多项式,用h(x)表示。通过g(x)和h(x)做除法,即两者的异或运算,得出的结果,就是我们所说的CRC运算检验结果。
那么,这里有两个疑问:
问题1:多项式和二进制是什么关系?
例如,1x3 + 0x2 + 1x + 1可以表示为1011b,或者1011b表示为1x3 + 0x2 + 1x + 1,其中是0的位,即以0为系数乘以该项。
问题2:除法怎么做?
本质就是在做异或运算
我们来看看一个数据串11010011101100除以x3 + x + 1是怎样进行的?
注意,根据CRC长度,需要将这个数据串补0
整个运算过程是这样的:
实际上就是逐个bit移位做异或。
详见:https://en.wikipedia.org/wiki/Cyclic_redundancy_check
▍CRC的概念
实际上,人们根据不同的需要,做了很多种计算方式,主要差别在于CRC长度、多项式、初始值、结果是否需要异或、是否需要翻转等等。
首先,来看看几个概念:
Length: CRC的长度(按bit算,如8,16,32)
Name: CRC的名字,让人一看就知道这是哪种CRC
Polinomial: 多项式,通过该多项式来计算CRC
InitialValue: CRC的初始值
FinalXorValue: CRC结果做异或运算的值
InputReflected: 指示输出是否需要翻转
OutputReflected: 指示输出是否需要翻转
我将网上搜到的所有的CRC标准分类做了个汇总:
从上面的CRC名称可以看出,不同的算法是有不同用途的,这是国际常规的用法定义,其实如果是用户自己用也没特别要求,可以自由点。
▍CRC的实现
1. 根据多项式二进制数,逐位做异或
按照前面章节的“CRC原理”来做C语言实现,就通过数据移位和异或算得。以CRC8为例,代码如下
#define CRC_POLYNOMIAL_8 0x0C
uint8 crc_8(uint8 crc, uint8* pdata, uint32 len)
{
for (uint32 i = 0; i < len; i++)
{
crc ^= pdata[i];
for (uint8 j = 0; j < 8; j++)
{
if ((crc & 0x80u) > 0)
{
crc = ( (uint8)(crc << 1u) ) ^ CRC_POLYNOMIAL_8;
}
else
{
crc <<= 1u;
}
}
}
return crc;
}
这个代码中有个if ((crc & 0x80u) > 0)要解释下,因为任意数与0做异或,结果是其本身。所以,数据为0时,不做异或。同理,CRC16、CRC32也可以按这种方法做计算,只是多项式和数据位数不一样而已,在此不累述了。
按移位做异或的方法,有个缺点,就是用了两层循环,效率不是那么的高,在嵌入式软件中,不是那么受欢迎。
那需要怎么优化算法呢?那就将多项式预先算个表出来,查表吧。
2. 将多项式(Polynomial)生成一个256长度的表,用查表法实现
2.1 多项式生成的表
在开始这个话题之前,我们得回个头来看看那几个概念中有个InputReflected和OutputReflected。干啥呢,吃饱没事干啊,算就算,还要翻转?呵呵!
其实,通过多项式生成表就可以生成正序和逆序,就是为了应付这个“翻转”的。举个例子以CRC8_ITU(Polynomial=0x07)看看这个表:
正序表为
const unsigned char crc8_itu__table[] =
{
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,
// ...... 省略部分
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,
};
逆序表为
const unsigned char crc8_itu_reversed_table[] =
{
0x00, 0x91, 0xE3, 0x72, 0x07, 0x96, 0xE4, 0x75, 0x0E, 0x9F, 0xED, 0x7C, 0x09, 0x98, 0xEA, 0x7B,
0x1C, 0x8D, 0xFF, 0x6E, 0x1B, 0x8A, 0xF8, 0x69, 0x12, 0x83, 0xF1, 0x60, 0x15, 0x84, 0xF6, 0x67,
// ...... 省略部分
0xA8, 0x39, 0x4B, 0xDA, 0xAF, 0x3E, 0x4C, 0xDD, 0xA6, 0x37, 0x45, 0xD4, 0xA1, 0x30, 0x42, 0xD3,
0xB4, 0x25, 0x57, 0xC6, 0xB3, 0x22, 0x50, 0xC1, 0xBA, 0x2B, 0x59, 0xC8, 0xBD, 0x2C, 0x5E, 0xCF,
};
2.2 通用查表法
网上有很多种查表发计算CRC,不尽相同。于是我做了个通用版,满足各种算法,其中用了宏定义的奇技淫巧。直接上代码:
#define DEF_CRC_FUNC(func, width, crcTable) \
uint##width func(const uint8 *pdata, int len, \
uint##width initial, uint##width finalXor, \
BOOL inputReflected, BOOL resultReflected) \
{ \
uint##width crc = initial; \
uint##width temp1 = 0, temp2 = 0, pos = 0; \
const uint##width *pTable = crcTable; \
for(int i = 0; i < len; i++) \
{ \
uint##width curByte = pdata[i]; \
if(inputReflected) \
{ \
curByte = Reflect8(pdata[i]); \
} \
/* update the MSB of crc value with next input byte */ \
temp1 = (crc ^ (curByte << (width - 8))); \
/* this MSB byte value is the index into the lookup table */ \
pos = (temp1 >> (width - 8)) & 0xFF; \
/* shift out this index */ \
temp2 = (temp1 << 8); \
/* XOR-in remainder from lookup table using the calculated index */ \
crc = (temp2 ^ pTable[pos]); \
} \
if(resultReflected) \
{ \
crc = Reflect##width(crc); \
} \
return (crc ^ finalXor); \
}
怎么用?
先定义,即定义对于位数算法的函数,通过预编译生成:
DEF_CRC_FUNC(gen_crc8, 8, crc8__table);
DEF_CRC_FUNC(gen_crc16_maxim, 16, crc16_maxim__table);
DEF_CRC_FUNC(gen_crc16_a, 16, crc16_a__table);
DEF_CRC_FUNC(gen_crc32_jamcrc, 32, crc32_jamcrc__table);
再调用,测试可以在main函数调用:
int main(void)
{
const uint8 buf[6] = "123456";
uint8 crc8 = gen_crc8(buf, 6, 0x00, 0x00, 0, 0);
uint16 crc16_maxim = gen_crc16_maxim(buf, 6, 0x0000, 0xFFFF, 1, 1);
uint16 crc16_a = gen_crc16_a(buf, 6, 0xC6C6, 0x0000, 1, 1);
uint32 crc32_jamcrc = gen_crc32_jamcrc(buf, 6, 0xFFFFFFFF, 0x00000000, 1, 1);
printf("crc8: %X\n", crc8);
printf("crc16_maxim: %X\n", crc16_maxim);
printf("crc16_a: %X\n", crc16_a);
printf("crc32_jamcrc: %X\n", crc32_jamcrc);
}
这里还是有个问题,就那个“翻转”的问题,每个byte都做翻转,很浪费CPU资源啊。作为“优秀的”嵌入式软件开发人员,我们是追求卓越的算法,于是乎,就有了以下方式:
(1)对于InputReflected和OutputReflected都是False的,我们用正序表,以CRC8_ITU为例:
/*
CRC Info:
Name Polynomial Initial FinalXor InputReflected ResultReflected
CRC8_ITU 0x07 0x00 0x55 false false
*/
unsigned char crc8_itu(unsigned char crc, const unsigned char* buf, unsigned int len)
{
for(unsigned int i = 0; i < len; i++)
{
crc = crc8_itu__table[crc ^ buf[i]];
}
return crc^0x55;
}
(2)对于InputReflected和OutputReflected都是True的,我们用逆序表,以CRC8_EBU为例:
/*
CRC Info:
Name Polynomial Initial FinalXor InputReflected ResultReflected
CRC8_EBU 0x1D 0xFF 0x00 true true
*/
unsigned int crc8_ebu(unsigned int crc, const unsigned char* buf, unsigned int len)
{
for(unsigned int i = 0; i < len; i++)
{
crc = crc8_ebu_reversed_table[crc ^ buf[i]];
}
return crc^0x00;
}
▍CRC的源码
说了这么多,以上都是举例而已(其实直接将上面代码copy过去验证也是木有问题的,不要怀疑),上代码啊
Talk is cheap. Show me the code.
Linus Torvalds
我将上面表格中提到的所有CRC算法,都coding了,就像下图的这些
Name Polynomial Initial FinalXor InputReflected ResultReflected
CRC8 0x07 0x00 0x00 false false
CRC8_SAE_J1850 0x1D 0xFF 0xFF false false
CRC8_SAE_J1850_ZERO 0x1D 0x00 0x00 false false
CRC8_8H2F 0x2F 0xFF 0xFF false false
CRC8_CDMA2000 0x9B 0xFF 0x00 false false
CRC8_DARC 0x39 0x00 0x00 true true
CRC8_DVB_S2 0xD5 0x00 0x00 false false
CRC8_EBU 0x1D 0xFF 0x00 true true
CRC8_ICODE 0x1D 0xFD 0x00 false false
CRC8_ITU 0x07 0x00 0x55 false false
CRC8_MAXIM 0x31 0x00 0x00 true true
CRC8_ROHC 0x07 0xFF 0x00 true true
CRC8_WCDMA 0x9B 0x00 0x00 true true
CRC16_CCIT_ZERO 0x1021 0x0000 0x0000 false false
CRC16_ARC 0x8005 0x0000 0x0000 true true
CRC16_AUG_CCITT 0x1021 0x1D0F 0x0000 false false
CRC16_BUYPASS 0x8005 0x0000 0x0000 false false
CRC16_CCITT_FALSE 0x1021 0xFFFF 0x0000 false false
CRC16_CDMA2000 0xC867 0xFFFF 0x0000 false false
CRC16_DDS_110 0x8005 0x800D 0x0000 false false
CRC16_DECT_R 0x0589 0x0000 0x0001 false false
CRC16_DECT_X 0x0589 0x0000 0x0000 false false
CRC16_DNP 0x3D65 0x0000 0xFFFF true true
CRC16_EN_13757 0x3D65 0x0000 0xFFFF false false
CRC16_GENIBUS 0x1021 0xFFFF 0xFFFF false false
CRC16_MAXIM 0x8005 0x0000 0xFFFF true true
CRC16_MCRF4XX 0x1021 0xFFFF 0x0000 true true
CRC16_RIELLO 0x1021 0xB2AA 0x0000 true true
CRC16_T10_DIF 0x8BB7 0x0000 0x0000 false false
CRC16_TELEDISK 0xA097 0x0000 0x0000 false false
CRC16_TMS37157 0x1021 0x89EC 0x0000 true true
CRC16_USB 0x8005 0xFFFF 0xFFFF true true
CRC16_A 0x1021 0xC6C6 0x0000 true true
CRC16_KERMIT 0x1021 0x0000 0x0000 true true
CRC16_MODBUS 0x8005 0xFFFF 0x0000 true true
CRC16_X_25 0x1021 0xFFFF 0xFFFF true true
CRC16_XMODEM 0x1021 0x0000 0x0000 false false
CRC32 0x04C11DB7 0xFFFFFFFF 0xFFFFFFFF true true
CRC32_BZIP2 0x04C11DB7 0xFFFFFFFF 0xFFFFFFFF false false
CRC32_C 0x1EDC6F41 0xFFFFFFFF 0xFFFFFFFF true true
CRC32_D 0xA833982B 0xFFFFFFFF 0xFFFFFFFF true true
CRC32_MPEG2 0x04C11DB7 0xFFFFFFFF 0x00000000 false false
CRC32_POSIX 0x04C11DB7 0x00000000 0xFFFFFFFF false false
CRC32_Q 0x814141AB 0x00000000 0x00000000 false false
CRC32_JAMCRC 0x04C11DB7 0xFFFFFFFF 0x00000000 true true
CRC32_XFER 0x000000AF 0x00000000 0x00000000 false false
举个例子,CRC32_BZIP2.h文件是的算法是这样的:
真以为我一个一个写的吗,哈哈哈,不是。但我肯定不是在网上一个一个抄的,网上也找不到这么多。
还是那句话:机器能干的事,为啥还要人来干!
我找到了个规律,写了个脚本生成的,多项式的正序表、逆序表是生成的,连代码,我都是用脚本生成的。
这里篇幅有限,就不将代码全部贴在这了,如果你对这些算法感兴趣,关注“嵌入式软件实战派”,聊天界面输入并发送“CRC”获得下载链接。后续适当的时候,我还会将这个生成源文件的脚本也分享给大家。
为了验证其可靠性,我还做了个测试代码(篇幅有限,下面截取的代码已省略了部分):
// TEST:Caculate the string "123456", result list below with different crc models:
crc_assert(0xfd == crc8 (0x00, "123456", 6), "crc8" );
// ...... 省略部分
crc_assert(0x57 == crc8_rohc (0xFF, "123456", 6), "crc8_rohc" );
crc_assert(0xab == crc8_wcdma (0x00, "123456", 6), "crc8_wcdma" );
crc_assert(0x20e4 == crc16_ccit_zero (0x0000, "123456", 6), "crc16_ccit_zero" );
crc_assert(0x29e4 == crc16_arc (0x0000, "123456", 6), "crc16_arc" );
// ...... 省略部分
crc_assert(0x32e4 == crc16_modbus (0xFFFF, "123456", 6), "crc16_modbus" );
crc_assert(0xe672 == crc16_x_25 (0xFFFF, "123456", 6), "crc16_x_25" );
crc_assert(0x20e4 == crc16_xmodem (0x0000, "123456", 6), "crc16_xmodem" );
crc_assert(0x0972d361 == crc32 (0xFFFFFFFF, "123456", 6), "crc32" );
// ...... 省略部分
crc_assert(0xf036f1c2 == crc32_xfer (0x00000000, "123456", 6), "crc32_xfer" );
如果你怀疑我“监守自盗”,你还可以在网上找个在线计算的方式来验证算法的正确性,如:https://crccalc.com/
如果有其他想法和建议,请留言,我们一同探讨。
如果你喜欢我的文章,请扫码关注“嵌入式软件实战派”,我会分享更多技术干货。