CRC(循环冗余校验)查表算法的代码实现
前言
CRC基本原理、数学描述和直接计算法的编程实现请参考笔者之前的劣文:
https://blog.csdn.net/weixin_44256803/article/details/105805628
本文假设读者已有以上CRC基础
各种CRC类型汇总:https://reveng.sourceforge.io/crc-catalogue/
准备工作
在正式进入查表算法的代码实现之前,需要先明白算法原理。
查表算法的由来
通过分析直接计算法的实现,很容易看出直接计算法中对于每一位数据都做了移位运算,遇到置1的位还会进行与多项式的模二减法(异或运算),这会大大降低算法的效率(时间复杂度过大):
能不能不要逐位计算,一次算好几位来提高速度呢?
查表算法的基本原理
模二减法(异或运算)的性质
在学习查表算法原理之前需要先了解模二减法(异或运算)的性质:
交换律:a ^ b = b ^ a
结合律:a ^ b ^ c = a ^ (b ^ c)
交换律很容易理解,结合律举例:
0110B ^ 0101B ^ 1001B = 1010B
0110B ^ (0101B ^ 1001B) = 0110B ^ 1100B = 1010B
对直接计算法进行分析
假设要传输的原始数据为1101011011B,发送方和接收方在通信前约定好的除数为10011B。由于除数10011B是五位数(5bit),那么假设余数(即CRC码)为四位数(4bit)。因为现在余数未知,所以在进行模二除法运算前先将余数设为0000B,即待发送的数据为11010110110000B。下面开始进行模二除法运算来确定余数(即CRC码):
我们将原始数据每两位拆开看:11B, 01B, 01B, 10B, 11B.
一一分解计算步骤并运用模二减法的交换律和结合律(即先算某部分的CRC码,再与原始数据的某部分作模二减法)不难发现:第一个两位(11B)的CRC校验值(0101B)的前两位(01B)会与接下来的第二个两位(01B)做模二减法(异或运算),运算结果(00B)的CRC校验值(0000B不妨记为temp)的前两位(00B)又会与第一个两位(11B)的CRC校验值(0101B)的后两位(01B)做模二减法(异或运算), 运算结果(01B)与temp的后两位(00B)即组成了原始数据前四位(1101B)的CRC码(0100B);接下来原始数据前四位(1101B)CRC校验值的前两位又会与第三个两位(01B)做模二减法(异或运算)……以此类推,最终算出的CRC校验值即为原始数据的CRC码。
由此抽象出数学模型:将计算CRC校验值这一过程记为函数f();将第一个两位(11B)记作sh1;将第二个两位(01B)记作sh2。则得到:
f(sh1 << 2 + sh2) = (f(sh1) << 2) ^ f((f(sh1) >> 2) ^ sh2)
至此,已经得到按两位查表算法的基本原理,等式右侧的f()函数调用即为查表操作。
基本原理的推广
实际应用中,按两位查表算法有时仍然不够高效。因此,可以推广到按四位查表、按字节查表。但很容易发现,随着查表位数的增加,所查的表会指数级变大:两位表的大小22=4;四位表的大小24=16;八位表的大小28=256等等。这说明随着查表算法的时间复杂度减小,空间复杂度会增加。而具体的查表位数则根据实际的应用场景来选取,例如:对于嵌入式而言,受RAM空间大小限制,可以提前计算好余式表,这样一来就会将其写入静态常量区而节省RAM;若资源紧张,在意表的大小,也可以选择较小的四位表;而对于PC,由于RAM足够,可以选择按字节查表甚至更大;若时间要求高,在意性能,则可以选择数据吞吐量更大的多表查询算法、四切片法、八切片法等,对于这些时间复杂度小、空间复杂度高的算法此处不再扩展。若有兴趣请参考http://create.stephan-brumme.com/crc32/。
查表算法的代码实现
测试用例
在算法的代码实现之前,可以先提前写好测试用例。这里以C++为例。选取CRC-16/CCITT-FALSE为测试的CRC类型,该类型的参数如下:
width=16
poly=0x1021
init=0xffff
refin=false
refout=false
xorout=0x0000
check=0x29b1
residue=0x0000
name=“CRC-16/IBM-3740”
Alias: CRC-16/AUTOSAR, CRC-16/CCITT-FALSE
因为CRC类型繁多且位数差异较大,有64位甚至大于64位的,也有3位、4位的,所以考虑用模板实现。例如大于16位小于等于32位的,采用unsigned int类型实现,大于32位小于等于64位的,采用unsigned long long类型实现。
//CrcTest.h
#pragma once
#include<iostream>
#include<iomanip>
#define WIDTH 16
#define POLY 0x1021
#define INIT 0xffff
#define REFIN false
#define REFOUT false
#define XOROUT 0x0000
#ifdef WIDTH
#if WIDTH <= 8
typedef unsigned char crcType;
#elif WIDTH > 8 && WIDTH <= 16
typedef unsigned short crcType;
#elif WIDTH > 16 && WIDTH <= 32
typedef unsigned int crcType;
#elif WIDTH > 32 && WIDTH <= 64
typedef unsigned long long crcType;
#endif // WIDTH <= 8
#endif // WIDTH
void testCalculateCrc();
//CrcTest.h
//CrcTest.cpp
#include"CrcTest.h"
#include"CrcCalculator.h"
using namespace std;
int main()
{
unsigned char input[] = { '1', '2', '3', '4', '5', '6', '7', '8', '9' };
cout << "0x" << setw((size_t)ceil(WIDTH / 4)) << setfill('0') << hex <<
calculateCrc<crcType>(input, sizeof(input), WIDTH, POLY, INIT, REFIN, REFOUT, XOROUT) << endl;
cin.get();
return 0;
}
//CrcTest.cpp
正式编码
最后,终于到了正式编码的部分了!这里抛砖引玉,以实现按字节查表为例。
首先,我们根据测试用例,规划好将要编程的文件:CrcCalculator.h,因为要用到模板,所以索性将函数的实现与声明写在一块儿避免麻烦。由于余式表的生成还是需要直接计算得到的,需要先编写直接计算CRC的方法,笔者之前博文有提及所以这里直接贴代码:
//CrcCalculator.h
#pragma once
/// <summary>
/// 将输入的数按位倒序
/// </summary>
/// <param name="input">待按位倒序的数</param>
/// <param name="width">CRC位宽</param>
/// <returns>按位倒序后的数</returns>
template<typename T> T reverseBit(T input, size_t width)
{
T output = 0;
for (size_t i = 0; i < width; i++)//根据CRC位宽确定循环次数
{
output <<= 1;//将output左移使上一次循环中确定的位变为高位,同时在本次循环中确定LSB
if (input & 1)//根据当前input的LSB确定当前output的LSB
{
output |= 1;
}
input >>= 1;//将input右移以便在下一次循环中获取下一个高位
}
return output;
}
/// <summary>
/// 直接计算CRC校验值
/// </summary>
/// <param name="pData">待计算数据的头指针</param>
/// <param name="length">待计算数据长度</param>
/// <param name="width">CRC位宽</param>
/// <param name="poly">CRC多项式</param>
/// <param name="init">输入初始值</param>
/// <param name="refin">输入是否反转</param>
/// <param name="refout">输出是否反转</param>
/// <param name="xorout">输出异或值</param>
/// <returns>CRC校验值</returns>
template<typename T> T calculateCrcDirectly(
unsigned char* pData,
size_t length,
size_t width,
T poly,
T init,
bool refin,
bool refout,
T xorout)
{
//计算掩码
T mask = 0;
for (size_t i = 0; i < width; i++)
{
mask = (mask << 1) | 1;
}
T ret = init;
while (length-- > 0)//根据输入数据的字节数依次计算
{
if (refin)
{
ret ^= reverseBit<T>(*pData++, width);
}
else
{
ret ^= static_cast<T>(*pData++) << (width - 8);
}
for (size_t i = 0; i < 8; i++)//按输入字节的每一位进行计算
{
if (ret & (static_cast<T>(1) << (width - 1)))//若首位是1则进行左移并与多项式进行模二减法(异或运算)
{
ret = ((ret << 1) & mask) ^ poly;
}
else//否则继续左移
{
ret = ((ret << 1) & mask);
}
}
}
if (refout)
{
return reverseBit<T>(ret ^ xorout, width);
}
else
{
return ret ^ xorout;
}
}
//CrcCalculator.h
接下来就是如何实现查表算法。首先,需要利用直接计算法生成一张余式表:
//CrcCalculator.h
/// <summary>
/// 计算CRC余式表
/// </summary>
/// <param name="table">CRC余式表头指针</param>
/// <param name="width">CRC位宽</param>
/// <param name="poly">CRC多项式</param>
/// <param name="refin">输入是否反转</param>
/// <param name="refout">输出是否反转</param>
template<typename T> void calculateCrcTable(
T* table,
size_t width,
T poly,
bool refin,
bool refout)
{
for (size_t i = 0; i < 256; i++)
{
unsigned char data = static_cast<unsigned char>(i);
table[i] = calculateCrcDirectly<T>(&data, sizeof(data), width, poly, 0, refin, refout, 0);
}
}
//CrcCalculator.h
注意,为了避免之后的循环所可能产生的反复按位反转(当refin、refout为真时),我们在生成余式表的时候就带上这层信息,后续的循环中就可以靠左移、右移来区分是否反转。
基于上面探索基本原理时所抽象的数学模型我们可以总结得到基本循环体伪代码(按字节查找):
本次CRC = (上次CRC左移一个字节) ^ 查表获得CRC(上次CRC的最高字节 ^ 本次读取的字节数据)
进一步地用代码表达:
thisCrc = (lastCrc << 8) ^ crcTable[(lastCrc >> (crcWidth - 8)) ^ thisByte];
而对于按位反转的CRC类型而言,我们已经在生成余式表时进行过反转,因此CRC已经是高低位互换,在循环中只需逆向位移即可:
thisCrc = (lastCrc >> 8) ^ crcTable[(lastCrc ^ thisByte) & 0xff];
注意:由于CRC位宽可能不止8位,因此需要与掩码0xff作按位与以剔除其余字节。
下面贴出实现细节:
//CrcCalculator.h
/// <summary>
/// 计算CRC校验值
/// </summary>
/// <param name="pData">待计算数据的头指针</param>
/// <param name="length">待计算数据长度</param>
/// <param name="width">CRC位宽</param>
/// <param name="poly">CRC多项式</param>
/// <param name="init">输入初始值</param>
/// <param name="refin">输入是否反转</param>
/// <param name="refout">输出是否反转</param>
/// <param name="xorout">输出异或值</param>
/// <returns>CRC校验值</returns>
template<typename T> T calculateCrc(
unsigned char* pData,
size_t length,
size_t width,
T poly,
T init,
bool refin,
bool refout,
T xorout)
{
//计算掩码
T mask = 0;
for (size_t i = 0; i < width; i++)
{
mask = (mask << 1) | 1;
}
T table[256];
calculateCrcTable<T>(table, width, poly, refin, refout);
T ret = init;
while (length-- > 0)
{
if (refin)
{
ret = (ret >> 8) ^ table[(ret ^ *pData++) & 0xff];
}
else
{
ret = ((ret << 8) & mask) ^ table[(ret >> (width - 8)) ^ *pData++];
}
}
return ret ^ xorout;
}
//CrcCalculator.h
代码的测试
在VS中进行调试:
与该模型的check值一致。
此外,对于嵌入式开发,可以先提前计算好余式表节省RAM。因此,添加一个打印余式表的测试用例:
//CrcTest.h
void testCalculateCrcTable()
{
crcType table[256];
calculateCrcTable<crcType>(table, WIDTH, POLY, REFIN, REFOUT);
cout << "{" << endl;
for (size_t i = 0; i < 256; i++)
{
cout << "0x" << setw((size_t)ceil(WIDTH / 4)) << setfill('0') << hex << table[i];
if (i < 255)
{
cout << ", ";
}
if (i % 8 == 7)
{
cout << endl;
}
}
cout << "}";
}
//CrcTest.h
仓库地址
https://github.com/blingbling-110/CrcLib
读者可以自行测试其他更低位/更高位CRC类型并打印出余式表以使用。