在通信过程中由于存在各种各样的干扰因素,可能会导致发送的信息与接收的信息不一致,比如发送数据为 1010_1010,传输过程中由于某些干扰,导致接收方接收的数据却成了0110_1010。为了保证数据传输的正确性,工程师们发明了一些检错方法,比如奇偶校验和CRC校验。
CRC校验(Cyclic Redundancy Check,循环冗余校验)是数据传输过程中常用的一种检错方法,针对要发送的数据,其使用一些特定的多项式可以计算出CRC检验结果,CRC校验结果与原始数据一起传输到接收端。
接收端在接收数据的同时按照相同的多项式对接收数据进行校验预算,并将校验结果和接收的结果进行对比,如果二者相同则认为没有发生传输错误;如果不同则认为发生了传输错误。如果发生了错误,接收端可以舍弃该帧数据并通知发送端重新传输数据。
尽管理论上仍然可能存在传输数据有误,但确无法被CRC校验出来的情况,但是这个概率极低,实际应用过程中基本可以忽略。
简单来讲,CRC校验,就是通信双方事先约定好一个CRC模型(一般来讲这个模型最重要的就是多项式),假如发送的数据除以多项式后得到的余数是X,那么接收方接收到的数据除以同样的多项式后,其得到的余数也应该为A。若二者得到的余数不同,则可认为是传输过程中发生了错误。
不同的协议在传输数据时采用了不同的CRC参数模型,模型由以下参数来定义。
NAME:模型名称,比如CRC-8,CRC-16/USB,CRC-32等。
WIDTH:宽度,即生成的CRC校验位宽,如CRC-8生成的结果为8位。
POLY:生成多项式的简写(16进制表示)。省略最高位1,如CRC-8的多项式为 x8 + x2 + x + 1,则二进制为1_0000_0111,省略最高位1,二进制0000_0111转换为十六进制为0x07。
INIT:这是算法开始时寄存器的初始化值,十六进制表示。
REFIN:true或false,在进行计算之前,原始数据是否翻转。原始数据:0xAA = 1010_1010,如果REFIN为true,则翻转后为0101_0101 = 0x55。
XOROUT:计算结果与此参数进行异或运算后得到的CRC值,和WIDTH位宽一致。
REFOUT:true或false,运算完成之后,得到的CRC值是否进行翻转,方法同上。
下面是一些常见的CRC参数模型:
假设通信双方约定的CRC模型为CRC-5/USB,发送的数据为8bits数据0xAA,即1010_1010。
CRC对应的多项式为0x05,补上省略的高位的1,则为10_0101。
REFIN为ture,所以需要翻转数据,即1010_1010 -> 0101_0101。
多项式的最高项为5,所以在发送数据末端补5个0,即0101_0101_00000。
发送数据除以多项式,即二者进行模2除法,过程如下(注意需要先异或一次初始值,由于很多模型的初始值为0,因为任何数异或0都等于它本身,所以这一步被省略掉了)。得到结果为00011。
将该结果与XOROUT值做异或运算,即 00011 ^ 11111 = 11100。
REFOUT为ture,所以需要翻转数据,即11100 -> 00111。即最终的CRC校验值为00111。
一般有两种电路可以用来实现CRC算法,串行CRC电路和并行CRC电路。
串行CRC电路以LFSR为基础,每个时钟周期输入一个数据的单bit,在数据宽度个周期后移位寄存器的值即为CRC校验值。下图是CRC8-CCITT模型的电路示意图。
接下来仍然以CRC-8模型为例,编写RTL代码(CRC-8的模型比较简单,没有反转那些步骤):
//串行CRC-8:宽度8,多项式0x07(x^8+x^2+x+1),初始值00,XOROUT00,REFIN/REFOUT均为FALSE module test( input clk, //时钟 input rst, //异步复位 input ser_in, //串行输入数据 input ser_in_val, //串行输入数据有效 output [7:0] crc_data, //CRC校验数据 output crc_data_val //CRC校验数据有效 ); //定义寄存器变量 reg [7:0] crc; //LFSR寄存器实现移位、异或操作 reg [2:0] cnt; //串行输入数据计数 reg data_val; //数据输出有效信号 //连接端口 assign crc_data = crc; assign crc_data_val = data_val; //构建串行输入LFSR always @(posedge clk or posedge rst)begin if(rst) crc <= 8'd0; //复位载入初始值 else if(data_val) crc <= 8'd0; //校验完成后恢复初始值 else if(ser_in_val) begin crc[0] <= crc[7] ^ ser_in; crc[1] <= crc[0] ^ crc[7] ^ ser_in; crc[2] <= crc[1] ^ crc[7] ^ ser_in; crc[3] <= crc[2]; crc[4] <= crc[3]; crc[5] <= crc[4]; crc[6] <= crc[5]; crc[7] <= crc[6]; end end //串行输入数据计数,0~7共8个周期 always @(posedge clk or posedge rst)begin if(rst) cnt <= 0; else if(ser_in_val) begin cnt <= cnt + 1; end end //输出8个数据后,拉高校验完成信号 always @(posedge clk or posedge rst)begin if(rst) data_val <= 0; else if(ser_in_val && (cnt == 7)) data_val <= 1; //8位串行输入传输完成,则拉高该信号 else data_val <= 0; //其他时间均为低电平 end endmodule
因为8位输入数据一共只有256个,所以我们可以用穷举法来进行测试,编写TB:依次产生0~255这256个数据并串行输入,CRC转换完成后在窗口打印信息。
`timescale 1ns/1ns module tb_test(); //信号声明 reg clk; reg rst; reg ser_in; reg ser_in_val; wire[7:0] crc_data; wire crc_data_val; //被测模块实例化 test inst_test( .clk (clk ), //时钟信号 .rst (rst ), //时钟信号 .ser_in (ser_in ), //串行输入数据 .ser_in_val (ser_in_val ), //串行输入数据有效信号 .crc_data (crc_data ), .crc_data_val (crc_data_val) ); //生成时钟信号 initial begin clk = 1'b0; forever #5 clk = ~clk; end //生成复位信号 initial begin rst = 1'b1; //复位 #40 rst = 1'b0; //取消复位 end //构建一个生成串行数据的任务 task data_gen( input [7:0] data );begin for(integer i=0;i<=7;i=i+1)begin @(posedge clk) ser_in_val <= 1; ser_in <= data[7-i]; #10; end ser_in_val <= 0; @(posedge crc_data_val); //当输出数据有效时,打印到窗口 $display("data = %h,crc = %h",data,crc_data); #10; end endtask //仿真过程 initial begin ser_in = 0; ser_in_val = 0; @(negedge rst); //等待复位完成 //依次产生0-255这256个8bits数据,通过任务data_gen转换成串行输入 for(integer j=0;j<=255;j=j+1)begin data_gen(j); end #10 $stop; //关闭仿真 end endmodule
部分仿真波形如下:
可以看到数据00、01、02、04分别产生的CRC-8校验结果为00、07、0E、09。与这个网站产生的正确结果(CRC(循环冗余校验)在线计算)比对,发现测试结果正确。
同时,Tcl窗口也打印了全部的校验结果(截图只有部分):
了解了串行CRC的结构,实现并行CRC就比较简单了,因为串行CRC是每个时钟周期都寄存了输入数据,然后在各个周期进行计算,那么并行CRC无非就是将电路展开,把所有的运算都放到一个时钟周期来实现。这是一种典型的“面积换时间”,通过增大电路面积来实现低延时latency(即高速率)。
为了说明方便一点,这里选取多项式为 x^4 + x^1 + 1 的CRC4校验来说明,它的串行电路生成部分是这样的:
crc[0] <= crc[3] ^ data ; crc[1] <= crc[0] ^ crc[3] ^ data; crc[2] <= crc[1]; crc[3] <= crc[2];
这是时序逻辑,转换成组合逻辑就是当前寄存器的值等于之前寄存器的值和当前输入做运算,用变量init来表示寄存器初始值,然后做8次迭代,
crc[0] = init[3] ^ data ; crc[1] = init[0] ^ init[3] ^ data; crc[2] = init[1]; crc[3] = init[2];
为了方便,在crc后面用迭代次数-1表示,那么第1次迭代:输入数据的最高位data[7]和上一轮的LFSR寄存器值(因为才开始,所以是初始值)做异或运算。
data[7]: crc7[0] = init[3] ^ data[7] ; crc7[1] = init[0] ^ init[3] ^ data[7]; crc7[2] = init[1]; crc7[3] = init[2];
同理,有第2次迭代:输入数据的次高位data[6]和上一轮的LFSR寄存器值(即crc7变量)做异或运算。
data[6]: crc6[0] = crc7[3] ^ data[6] = init[2] ^ data[6]; crc6[1] = crc7[0] ^ crc7[3] ^ data[6] = init[3] ^ data[7] ^ init[2] ^ data[6]; crc6[2] = crc7[1] = init[0] ^ init[3] ^ data[7]; crc6[3] = crc7[2] = init[1];
直到第8次迭代,输入最后一个数据:
data[5]: crc5[0] = crc6[3] ^ data[5] = init[1] ^ data[5]; crc5[1] = crc6[0] ^ crc6[3] ^ data[5] = init[2] ^ data[6] ^ init[1] ^ data[5]; crc5[2] = crc6[1] = init[3] ^ data[7] ^ init[2] ^ data[6]; crc5[3] = crc6[2] = init[0] ^ init[3] ^ data[7]; data[4]: crc4[0] = crc5[3] ^ data[4] = init[0] ^ init[3] ^ data[7] ^ data[4] crc4[1] = crc5[0] ^ crc5[3] ^ data[4] = init[1] ^ data[5] ^ init[0] ^ init[3] ^ data[7] ^ data[4]; crc4[2] = crc5[1] = init[2] ^ data[6] ^ init[1] ^ data[5]; crc4[3] = crc5[2] = init[3] ^ data[7] ^ init[2] ^ data[6]; data[3]: crc3[0] = crc4[3] ^ data[3] = init[3] ^ data[7] ^ init[2] ^ data[6] ^ data[3] crc3[1] = crc4[0] ^ crc4[3] ^ data[3] = init[0] ^ init[3] ^ data[7] ^ data[4] ^ init[3] ^ data[7] ^ init[2] ^ data[6] ^ data[3] crc3[2] = crc4[1] = init[1] ^ data[5] ^ init[0] ^ init[3] ^ data[7] ^ data[4]; crc3[3] = crc4[2] = init[2] ^ data[6] ^ init[1] ^ data[5]; data[2]: crc2[0] = crc3[3] ^ data[2] = init[2] ^ data[6] ^ init[1] ^ data[5] ^ data[2] crc2[1] = crc3[0] ^ crc3[3] ^ data[2] = init[3] ^ data[7] ^ init[2] ^ data[6] ^ data[3] ^ init[2] ^ data[6] ^ init[1] ^ data[5] ^ data[2] crc2[2] = crc3[1] = init[0] ^ init[3] ^ data[7] ^ data[4] ^ init[3] ^ data[7] ^ init[2] ^ data[6] ^ data[3] crc2[3] = crc3[2] = init[1] ^ data[5] ^ init[0] ^ init[3] ^ data[7] ^ data[4] data[1]: crc1[0] = crc2[3] ^ data[1] = init[1] ^ data[5] ^ init[0] ^ init[3] ^ data[7] ^ data[4] ^ data[1] crc1[1] = crc2[0] ^ crc2[3] ^ data[1] = init[2] ^ data[6] ^ init[1] ^ data[5] ^ data[2] ^ init[1] ^ data[5] ^ init[0] ^ init[3] ^ data[7] ^ data[4] ^ data[1] crc1[2] = crc2[1] = init[3] ^ data[7] ^ init[2] ^ data[6] ^ data[3] ^ init[2] ^ data[6] ^ init[1] ^ data[5] ^ data[2] crc1[3] = crc2[2] = init[0] ^ init[3] ^ data[7] ^ data[4] ^ init[3] ^ data[7] ^ init[2] ^ data[6] ^ data[3] data[0]: crc0[0] = crc1[3] ^ data[0] = init[0] ^ init[3] ^ data[7] ^ data[4] ^ init[3] ^ data[7] ^ init[2] ^ data[6] ^ data[3] ^ data[0] = init[0] ^ data[4] ^ init[2] ^ data[6] ^ data[3] ^ data[0] crc0[1] = crc1[0] ^ crc1[3] ^ data[0] = init[1] ^ data[5] ^ init[0] ^ init[3] ^ data[7] ^ data[4] ^ data[1] ^ init[0] ^ init[3] ^ data[7] ^ data[4] ^ init[3] ^ data[7] ^ init[2] ^ data[6] ^ data[3] ^ data[0] = init[1] ^ data[5]^ init[3] ^ data[4] ^ data[1]^ data[7] ^ data[4] ^ init[2] ^ data[6] ^ data[3] ^ data[0] crc0[2] = crc1[1] = init[2] ^ data[6] ^ init[1] ^ data[5] ^ data[2] ^ init[1] ^ data[5] ^ init[0] ^ init[3] ^ data[7] ^ data[4] ^ data[1] crc0[3] = crc1[2] = init[3] ^ data[7] ^ init[2] ^ data[6] ^ data[3] ^ init[2] ^ data[6] ^ init[1] ^ data[5] ^ data[2]
把crc0的表达式整理一下:
crc0[0] = data[6] ^ data[4] ^ data[3] ^ data[0] ^ init[0] ^ init[2]; crc0[1] = data[7] ^ data[6] ^ data[5] ^ data[1] ^ data[3] ^ data[0] ^ init[1] ^ init[2] ^ init[3]; crc0[2] = data[7] ^ data[6] ^ data[4] ^ data[2] ^ data[1] ^ init[0] ^ init[2] ^ init[3]; crc0[3] = data[7] ^ data[5] ^ data[3] ^ data[2] ^ init[1] ^ init[3];
和这个网站http://crctool.easics.be/用工具生成的代码(核心部分)做对比,二者结果一致。
newcrc[0] = data[6] ^ data[4] ^ data[3] ^ data[0] ^ crc[0] ^ crc[2]; newcrc[1] = data[7] ^ data[6] ^ data[5] ^ data[3] ^ data[1] ^ data[0] ^ crc[1] ^ crc[2] ^ crc[3]; newcrc[2] = data[7] ^ data[6] ^ data[4] ^ data[2] ^ data[1] ^ crc[0] ^ crc[2] ^ crc[3]; newcrc[3] = data[7] ^ data[5] ^ data[3] ^ data[2] ^ crc[1] ^ crc[3];
接着编写并行CRC-8的代码:
//并行CRC-8:宽度8,多项式0x07(x^8+x^2+x+1),初始值00,XOROUT00,REFIN/REFOUT均为FALSE module test( input clk, //时钟 input rst, //异步复位 input [7:0] pal_in, //并行8位输入数据 input pal_in_val, //并行输入数据有效 output [7:0] crc_data, //CRC校验数据 output crc_data_val //CRC校验数据有效 ); //定义寄存器变量 reg [7:0] crc; //CRC校验数据 reg data_val; //CRC校验数据输出有效信号 localparam CRC_INIT = 8'd0; //LFSR寄存器初始值 //连接端口 assign crc_data = crc; assign crc_data_val = data_val; //并行电路使用函数来构造 function [7:0] CRC8; input [7:0] data; //被校验数据 input [7:0] crc_init; //寄存器初值 begin CRC8[0] = data[7] ^ data[6] ^ data[0] ^ crc_init[0] ^ crc_init[6] ^ crc_init[7]; CRC8[1] = data[6] ^ data[1] ^ data[0] ^ crc_init[0] ^ crc_init[1] ^ crc_init[6]; CRC8[2] = data[6] ^ data[2] ^ data[1] ^ data[0] ^ crc_init[0] ^ crc_init[1] ^ crc_init[2] ^ crc_init[6]; CRC8[3] = data[7] ^ data[3] ^ data[2] ^ data[1] ^ crc_init[1] ^ crc_init[2] ^ crc_init[3] ^ crc_init[7]; CRC8[4] = data[4] ^ data[3] ^ data[2] ^ crc_init[2] ^ crc_init[3] ^ crc_init[4]; CRC8[5] = data[5] ^ data[4] ^ data[3] ^ crc_init[3] ^ crc_init[4] ^ crc_init[5]; CRC8[6] = data[6] ^ data[5] ^ data[4] ^ crc_init[4] ^ crc_init[5] ^ crc_init[6]; CRC8[7] = data[7] ^ data[6] ^ data[5] ^ crc_init[5] ^ crc_init[6] ^ crc_init[7]; end endfunction //当输入有效时进行CRC校验 always @(posedge clk or posedge rst)begin if(rst) crc <= 8'd0; else if(pal_in_val) //输入有效时 crc <= CRC8(pal_in,CRC_INIT); //校验结果落后一拍 end //在输入数据有效的下一个周期输出有效 always @(posedge clk or posedge rst)begin if(rst) data_val <= 0; else if(pal_in_val) data_val <= 1; else data_val <= 0; //其他时间均为低电平 end endmodule
测试逻辑和上面的串行CRC一样:依次输入0~255这256个数据,观察CRC校验结果:
`timescale 1ns/1ns module tb_test(); //信号声明 reg clk; reg rst; reg [7:0] pal_in; reg pal_in_val; wire[7:0] crc_data; wire crc_data_val; //被测模块实例化 test inst_test( .clk (clk ), //时钟信号 .rst (rst ), //时钟信号 .pal_in (pal_in ), //串行输入数据 .pal_in_val (pal_in_val ), //串行输入数据有效信号 .crc_data (crc_data ), .crc_data_val (crc_data_val) ); //生成时钟信号 initial begin clk = 1'b0; forever #5 clk = ~clk; end //生成复位信号 initial begin rst = 1'b1; //复位 #40 rst = 1'b0; //取消复位 end //生成输入数据与时钟使能信号 always @(posedge clk or posedge rst)begin if(rst)begin pal_in <= 255; pal_in_val <= 0; end else begin pal_in <= pal_in + 1; pal_in_val <= 1; end end //仿真过程 initial begin #1000 $stop; //关闭仿真 end endmodule
仿真结果如下,CRC校验结果依次输出00-07-0e-09······,与正确结果对比后,发现仿真无误。