C# 学习研究CRC校验

最近一直在研究CRC校验,网上搜了一堆资料,研究了老半天终于算是搞明白了,下面全是基于我自己的理解,如有错误,望指出!

1.定义

CRC(Cyclic Redundancy Check)即【循环冗余校验】,是一种数据检验算法,被广泛的用来验证数据传输前后的一致性,

2.原理

其原理就是把整个字节流数据当作一个【被除数】,再设定一个【除数】,然后使用【模二除法】运算求其余数,这个余数就是CRC校验码,

其中的【除数】比较特殊,其最高位和最低位必须为1,理论上可以自己设定除数,但是其取值貌似有一定的讲究,网络上有很多大家都在用的除数,比如CRC16中的0x18005,0x13D65等等,

模二除法是一种二进制算法,它不考虑进位和错位,使用异或运算替代每一步的减法,具体如下图,

6

CRC校验的的时候,通常会在数据的尾部加上余数长度的比特位,用来存放余数,

3.模二除法编程

原理搞清楚了,接下来我们就可以构思编程逻辑了(CRC校验时只需要余数,所以下面编程的时候将不再关心商):

1,判断被除数的最高位,如果是0,直接左移一位,

2,如果是1,就把除数和被除数高位对其后进行异或计算,运算后最高位必然为0,左移一位,

3,循环1和2,直到将被除数的所有位数走完,最终得到的数据就是余数,余数会比除数少一位,

举例,假如【被除数】(即数据)为两个字节 0xC981,二进制0b1100_1001_1000_0001,【除数】为0x1A001,二进制0b0001_1010_0000_0000_0001,则计算过程如下,

微信截图_20250806161348

根据上述构思编写的代码:

        ushort Mod2Division(byte[] data, int completePoly = 0x1A001) {
            int buf = 0; //因为我们想要一个16位的余数,所以除数要17位,缓冲区只能用32位的整型
            completePoly = completePoly << (32 - 17); //除数高位对其
            foreach (byte b in data) { //循环处理每个字节
                buf = buf ^ (b << 24); //将每个字节和上一次缓冲区的中间结果异或
                for (int i = 0; i < 8; i++) { //判断每个字节的每个比特
                    if ((buf & 0x80000000) != 0) { //判断最高位是否为1
                        buf = buf ^ completePoly; //和除数异或存储中间结果
                        buf <<= 1;
                    } else {
                        buf <<= 1;
                    }
                }
            }
            return (ushort)(buf >> 16); //高位2个字节即为余数
        }
View Code

注意!上面的代码只是单纯的实现了模二除法的逻辑,很不优雅,主要是用来理解模二除法的原理,

4.常规的CRC16算法

现实中CRC算法除了基于模二除法外,还有其他额外的因素能对结果直接造成影响,

poly:多项式,可以理解为省略了最高位的【除数】,例如上文的0x18005,其对应的多项式就是0x8005,使用多项式这个概念能让算法更加的简洁,

init:初始值,因为原始数据可能以不同位的0开头,而异或计算无法区分有多少个0,所以需要设置初始值用以区分,

refin:数据的每个字节是否按位反转,有些硬件为了方便处理,会对输入的数据的每个字节按位反转,

refout:计算出来的结果(CRC码)整体是否按位反转,

xorout:计算出来的结果再要异或的值,为0时结果不变,

根据校验码的位数,CRC校验可以分为CRC8,CRC16,CRC32,接下来咱们根据上述的若干参数实现一个常规的CRC16算法,

        /// <summary>
        /// CRC16算法
        /// </summary>
        /// <param name="data">数据</param>
        /// <param name="init">初始值</param>
        /// <param name="poly">多项式</param>
        /// <param name="refin">输入数据是否每个字节按位反转</param>
        /// <param name="refout">输出结果是否整体按位反转</param>
        /// <param name="xorout">输出结果前要异或的参数</param>
        /// <returns></returns>
        public ushort CRC16(byte[] data, ushort init = 0xFFFF, ushort poly = 0x8005, bool refin = false, bool refout = false, ushort xorout = 0) {
            ushort crc = init;
            foreach (byte b in data) {
                var byt = b;
                if (refin) { //每个字节按位反转
                    byt = ReverseBits_UInt8(byt);
                }
                crc ^= (ushort)(byt << 8); //高位对齐
                for (int i = 0; i < 8; i++) {
                    if ((crc & 0x8000) != 0) {
                        crc = (ushort)((crc << 1) ^ poly); //此时多项式忽略了最高位的1,因此可以先移位
                    } else {
                        crc <<= 1;
                    }
                }
            }
            if (refout) { //整个结果(2个字节的短整型)按位反转
                crc = ReverseBits_UInt16(crc);
            }
            crc ^= xorout; //计算结果与此参数异或后得到最终的CRC值
            return crc;
        }
        /// <summary>
        /// 将字节按位反转
        /// </summary>
        public static byte ReverseBits_UInt8(byte num) {
            byte reversed = 0;
            for (int i = 0; i < 8; i++) {
                reversed <<= 1;
                reversed |= (byte)(num & 1);
                num >>= 1;
            }
            return reversed;
        }
        /// <summary>
        /// 将短整型整体按位反转
        /// </summary>
        public static ushort ReverseBits_UInt16(ushort num) {
            ushort reversed = 0;
            for (int i = 0; i < 16; i++) {
                reversed <<= 1;
                reversed |= (ushort)(num & 1);
                num >>= 1;
            }
            return reversed;
        }
View Code

上述代码需要注意一下几点,

1,多项式是忽略最高位的除数,上述代码的poly=0x8005,其完整多项式(即模二除法中的除数)为0x18005,模二除法中是拿除数参与运算,是先异或,再移位,而使用多项式参与运算时,就相当于忽略了最高位(先移位)后再运算剩下的比特位,两者的结果是一样的,也算是一定程度上简化逻辑了,而且使用多项式还能让缓冲区使用和验证码一样的比特长度,整体运算起来更加的简洁清晰和快捷,

2,如果refin为真,每个字节都要按位反转,

3,如果refout为真,得到的结果要整体按位反转,目前所有的流行的算法中refin和refout都是一致的,即要么都为true,要么都为false,

4,最后结果还要和xorout进行异或才是最终结果,不过一般xorout都是0,异或后结果不变

5.右移算法

到了这里,理论已经能够正确的计算出CRC验证码了,

但是我发现,网上很多实例代码和我的不一样,他们的代码的逻辑是低位对齐,并通过右移位来消数据的,看上去很优雅,比如下面的代码:

        public ushort Modbus_crc16(byte[] data) {
            ushort crc = 0xFFFF; // 初始值
            for (int i = 0; i < data.Length; i++) {
                crc ^= data[i];
                for (int j = 0; j < 8; j++) {
                    if ((crc & 0x0001) != 0) {
                        crc = (ushort)((crc >>= 1) ^ 0xA001); // 反射多项式
                    } else {
                        crc >>= 1;
                    }
                }
            }
            return crc;
        }
View Code

研究了一番,发现原来是部分硬件设备出于实现效率、通信协议兼容性和历史设计惯性等原因,选择了refin=true,refout=true,但是人家并没有傻傻的真的把数据的每一个字节都反转一下,这样太影响性能了,他们使用了一种简单的右移算法实现了对数据的计算,得到的结果和左移是完全一样的,

即当【原数据】+【反射多项式】+【低位对其】+【右移消数据】后,其结果和【数据的每个字节按位反转】+【原多项式】+【高位对齐】+【左移消数据】+【结果数据整体按位反转】的结果是一致的,两者就像反射镜像一样,在refin和refout都为true的场景下,明显采用右移算法比左移算法更优雅,代码也更简洁,性能也更好,如下图:

444

6.整合优化CRC16算法

稍微整合一下,我们就能在refin和refout都为false的时候使用左移算法,都为true的时候使用右移算法了,非常的优雅

    /// <summary>
    /// CRC16校验
    /// </summary>
    public class CRC16 {
        readonly ushort _init;
        readonly ushort _poly;
        readonly ushort _xorout;
        /// <summary>
        /// 具体的算法函数,在初始化的时候确定使用哪个算法
        /// </summary>
        readonly Func<byte[], ushort> _func;
        /// <summary>
        /// 创建一个CRC16校验对象
        /// </summary>
        /// <param name="init">初始值</param>
        /// <param name="poly">多项式</param>
        /// <param name="refin">输入数据是否每个字节按位反转</param>
        /// <param name="refout">输出数据是否整体按位反转</param>
        /// <param name="xorout">输出数据要异或的值</param>
        /// <exception cref="Exception"></exception>
        public CRC16(ushort init = 0xFFFF, ushort poly = 0x8005, bool refin = false, bool refout = false, ushort xorout = 0) {
            if (refin != refout) {
                throw new Exception("refin和refout必须一致");
            }
            _init = init;
            if (refin == false) { //数据不反转,就使用和原理一致的左移算法
                _poly = poly;
                _func = CRC16_left; //指向左移方法
            } else { //数据要反转时,反转多项式,使用右移算法
                _poly = ReverseBits(poly); //反射多项式
                _func = CRC16_rigth; //指向右移
            }
            _xorout = xorout;
        }
        /// <summary>
        /// 对一个短整型整体按位反转
        /// </summary>
        public static ushort ReverseBits(ushort num) {
            ushort reversed = 0;
            for (int i = 0; i < 16; i++) {
                reversed <<= 1;
                reversed |= (ushort)(num & 1);
                num >>= 1;
            }
            return reversed;
        }
        /// <summary>
        /// 对传入的字节数组进行CRC16校验,返回校验码
        /// </summary>
        /// <param name="data">数据</param>
        /// <returns>校验码</returns>
        public ushort Check(byte[] data) {
            return (ushort)(_func(data) ^ _xorout);
        }
        /// <summary>
        /// 左移算法
        /// </summary>
        ushort CRC16_left(byte[] data) {
            ushort crc = _init;
            foreach (byte b in data) {
                crc ^= (ushort)(b << 8);
                for (int i = 0; i < 8; i++) {
                    if ((crc & 0x8000) != 0) {
                        crc = (ushort)((crc << 1) ^ _poly);
                    } else {
                        crc <<= 1;
                    }
                }
            }
            return crc;
        }
        /// <summary>
        /// 右移算法,适用于refin和refout都为真的时候
        /// </summary>
        ushort CRC16_rigth(byte[] data) {
            ushort crc = _init;
            for (int i = 0; i < data.Length; i++) {
                crc ^= data[i];
                for (int j = 0; j < 8; j++) {
                    if ((crc & 0x0001) != 0) {
                        crc = (ushort)((crc >> 1) ^ _poly);
                    } else {
                        crc >>= 1;
                    }
                }
            }
            return crc;
        }
    }
View Code

7.查表法

CRC还有一种通过空间换时间的算法,也就是查表法,

CRC校验可以理解为根据数据的比特位进行有规律的异或多项式计算,由于数据是固定的,也就说具体要异或几次,以及要在哪对齐异或,这些都是已知固定的,

而且在异或运算中,异或运算的先后顺序并不会影响结果,即a^b^c=(c^a)^b,如下图,

5

因此,我们完全可以先算出0-255每个数值和多项式运算后的余数(在CRC16中,经过运算后,作为数据的8位会消掉,后续受影响的16位即为余数),将其存到crc表中,在校验的时候就可以直接使用数据作为索引从crc表中拿到提前计算的余数来使用,这样每次直接就能消掉1个字节,就不用每次都循环8次运算了,

根据原理,先实现一个左移查表法,

    /// <summary>
    /// crc16左移查表法
    /// </summary>
    internal class CRC16TableLeft {
        public readonly ushort Poly;
        readonly ushort[] crcTable;
        public CRC16TableLeft(ushort poly) {
            Poly = poly;
            crcTable = GenerateCRCTable();
            Console.WriteLine(ShowTable(crcTable));
        }
        /// <summary>
        /// 根据多项式,生成crc表
        /// </summary>
        /// <returns></returns>
        ushort[] GenerateCRCTable() {
            var table = new ushort[256];
            for (int idx = 0; idx < table.Length; idx++) {
                ushort crc = (ushort)(idx << 8); //每次计算单独的余数
                for (int j = 0; j < 8; j++) {
                    if ((crc & 0x8000) != 0) {
                        crc = (ushort)((crc << 1) ^ Poly);
                    } else {
                        crc = (ushort)(crc << 1);
                    }
                }
                table[idx] = crc; //该值为不受前面计算影响的余数
            }
            return table;
        }
        /// <summary>
        /// 得到检验码
        /// </summary>
        /// <param name="init"></param>
        /// <param name="data"></param>
        /// <returns></returns>
        public ushort Check(ushort init, byte[] data) {
            ushort buf = init; //2个字节的缓冲区
            foreach (var p in data) {
                var val = (buf >> 8) ^ p; //缓冲区中的数据记录了上次运算的结果,两者高位对其后异或,得到的字节就是当前要运算的数据,
                var crc = crcTable[val]; //查表找到val对应的余数
                buf <<= 8; //已经通过高8位的余数得到val了,可以直接消掉了
                buf = (ushort)(buf ^ crc); //将当前运算的结果覆盖到缓冲区中,用于下一次运算
                //buf = (ushort)((buf << 8) ^ crcTable[(buf >> 8) ^ p]); //整合代码
            }
            return buf; //最终缓冲区中的数据就是校验码
        }
        string ShowTable(ushort[] data) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < data.Length; i++) {
                if (i % 8 == 0) {
                    sb.Append(Environment.NewLine);
                }
                sb.Append($"0x{data[i]:X4},");
            }
            return sb.ToString(0, sb.Length - 1);
        }
    }
View Code

整合一个包括左移和右移两种方式的CRC16查表功能类

    public class CRC16Table {
        public readonly ushort _init;
        public readonly ushort _poly;
        public readonly bool _refin;
        public readonly bool _refout;
        public readonly ushort _xorout;
        public readonly ushort[] _table;
        public CRC16Table(ushort init = 0xFFFF, ushort poly = 0x8005, bool refin = false, bool refout = false, ushort xorout = 0) {
            if (refin != refout) {
                throw new Exception("refin和refout的值必须是一致的!");
            }
            _init = init;
            _poly = poly;
            _refin = refin;
            _refout = refout;
            _xorout = xorout;
            if (_refin == false) {
                _table = GenerateTable_Left(_poly);
            } else {
                _table = GenerateTable_Right(ReverseBits(_poly)); //反射多项式
            }
        }
        public static ushort ReverseBits(ushort num) {
            ushort reversed = 0;
            for (int i = 0; i < 16; i++) {
                reversed <<= 1;
                reversed |= (ushort)(num & 1);
                num >>= 1;
            }
            return reversed;
        }
        static ushort[] GenerateTable_Left(ushort poly) {
            var table = new ushort[256];
            for (int idx = 0; idx < table.Length; idx++) {
                ushort crc = (ushort)(idx << 8);
                for (int j = 0; j < 8; j++) {
                    if ((crc & 0x8000) != 0) {
                        crc = (ushort)((crc << 1) ^ poly);
                    } else {
                        crc = (ushort)(crc << 1);
                    }
                }
                table[idx] = crc;
            }
            return table;
        }
        static ushort[] GenerateTable_Right(ushort poly) {
            var table = new ushort[256];
            for (int idx = 0; idx < table.Length; idx++) {
                ushort crc = (ushort)idx;
                for (int j = 0; j < 8; j++) {
                    if ((crc & 0x0001) != 0) {
                        crc = (ushort)((crc >> 1) ^ poly);
                    } else {
                        crc = (ushort)(crc >> 1);
                    }
                }
                table[idx] = crc;
            }
            return table;
        }
        public ushort Check(byte[] data) {
            ushort buf = _init;
            if (_refin) {
                foreach (var p in data) {
                    buf = (ushort)((buf >> 8) ^ _table[(buf ^ p) & 0xFF]);
                }
            } else {
                foreach (var p in data) {
                    buf = (ushort)((buf << 8) ^ _table[(buf >> 8) ^ p]);
                }
            }
            return (ushort)(buf ^ _xorout);
        }
        public string GetTableString() {
            var sb = new StringBuilder();
            for (int i = 0; i < _table.Length; i++) {
                if (i % 8 == 0) {
                    sb.Append(Environment.NewLine);
                }
                sb.Append($"0x{_table[i]:X4},");
            }
            return sb.ToString(0, sb.Length - 1);
        }
    }
View Code

 测试代码

        public void Test() {
            byte[] data = new byte[] { 0,1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
            CRC16Table ct_left = new CRC16Table(0xFFFF, 0x8005, false, false);
            var crc_L = ct_left.Check(data);
            Console.WriteLine($"crc_L,{crc_L:X4}");
            CRC16Table ct_rigth = new CRC16Table(0xFFFF, 0x8005, true, true);
            var crc_R = ct_rigth.Check(data);
            Console.WriteLine($"crc_R,{crc_R:X4}");
            Console.WriteLine(ct_left.GetTableString());
            Console.WriteLine(ct_rigth.GetTableString());
        }
View Code

 终于算是搞清楚了~

posted @ 2025-08-06 18:24  WmW  阅读(64)  评论(0)    收藏  举报