CRC又称循环冗余校验,是数据通信领域中最常用的一种查错校验码。说是常用,但我在单片机、FPGA的开发中从来没有用过,别说是CRC了,就是串口的奇偶校验都很少用。一个课程设计要做些和通信相关的东西,题目里面好像就只有这个CRC比较有意思,就研究起了这个。
乍一看起来这个东西好像挺高大上的:还循环、还冗余,要是碰到个语文差点的,冗余的冗都不知道怎么读……开始时我也觉得这个玩意好复杂、看起来好厉害啊,但理解一下发现也不是很难嘛。(以下内容为个人理解,不当之处,还请指出)
1、理论知识部分
这个校验方式的根本思想是在发送数据的后面拼接上一些额外的数据位,使得这个拼接之后的数据可以被一个固定的数模2整除(这里的模2整除是一种区别于普通的除法的运算方法,也不难理解),之后将数据发送……接收端同样将整个拼接后的数据与之前确定的固定的数据做模2除法,若能整除,则说明传输过程没有出错,若不能整除,则可以根据余数判断是哪一位发生了错误。
上面说到的要发送的数据被称为信息码;额外的数据位被称为校验码;在模2除法中做除数的那个固定的数被称为生成多项式;拼接完成之后的整个数据被称为CRC 码;
一些关于生成多项式、模2除法的知识在这里就不细说了,可以参考这一篇博客,我也是参考了这篇博客里的好多内容理解的这个CRC:
http://www.cnblogs.com/BitArt/archive/2012/12/26/2833100.html
2、实验部分
首先为了完成这个实验,就需要有CRC数据的编码发送端和接收解码端
发送端:
信息码数据:使用7位拨码开关来作为数据的输入。
生成多项式是: 。即 二进制的“111010001”。
7 位信息码与生成多项式相除就能得到 8 位校验码,将校验码拼接在信息码后就得到 15 位的CRC 码。
Verilog代码如下:
/*****************获得CRC码部分**************/
reg fd;
reg [7:0] crc_tmp=8'd0;
reg [7:0] crc=8'd0;
integer i;
/*使用1Hz的时钟来控制CRC校验码的生成*/
always @(posedge clk or negedge rst)
begin
if(!rst)
crc <= 8'b0;
else if(clk_1Hz_posedge==1'b0)
crc <= crc_tmp;
end
always@( data_in or crc) begin
crc_tmp = 8'd0;
for(i=6; i>=0; i=i-1) begin
fd = crc_tmp[7] ^ data_in[i];
crc_tmp[7] = crc_tmp[6] ^ fd;
crc_tmp[6] = crc_tmp[5] ^ fd;
crc_tmp[5] = crc_tmp[4] ;
crc_tmp[4] = crc_tmp[3] ^ fd;
crc_tmp[3] = crc_tmp[2] ;
crc_tmp[2] = crc_tmp[1] ;
crc_tmp[1] = crc_tmp[0] ;
crc_tmp[0] = fd ;
end
end
//-----------------分频-----1Hz----------------
reg [26:0] Count_1Hz;
reg clk_1Hz;
always @ (posedge clk) begin
if(Count_1Hz >= 27'd24_999_999) begin
Count_1Hz <= 27'd0;
clk_1Hz <= ~clk_1Hz;
end
else
Count_1Hz <= Count_1Hz +27'd1;
end
//--------------采样clk_1Hz的上升沿-------------
reg clk_1Hz_d0;
reg clk_1Hz_d1;
wire clk_1Hz_posedge;
assign clk_1Hz_posedge = clk_1Hz_d0 && (~clk_1Hz_d1);
always @ (posedge clk ) begin
clk_1Hz_d0 <= clk_1Hz;
clk_1Hz_d1 <= clk_1Hz_d0;
end
使用modelsim软件仿真得到的波形如图所示:
在此可以得到生成的8位的校验码“crc”。使用四位数码管的前两位显示信息码“0X2A”,后两位显示计算得到的CRC校验码0X1A:
为了展示CRC的纠错能力,在已经生成并且计算好的数据上“做个手脚”:使用7个按键控制七位数据码,检测到哪一位按下就将当前位对应的数据取反,得到最终发送给接受端的带有“人为干扰”的数据。
/*-------------故意制造错误部分-------------*/
reg [7:0] Num_Disp1_temp;
always @ (posedge clk) begin
Num_Disp1_temp <= Num_Disp1;
case(key)
7'b1111110: Num_Disp1_temp[0] <= ~Num_Disp1_temp[0];
7'b1111101: Num_Disp1_temp[1] <= ~Num_Disp1_temp[1];
7'b1111011: Num_Disp1_temp[2] <= ~Num_Disp1_temp[2];
7'b1110111: Num_Disp1_temp[3] <= ~Num_Disp1_temp[3];
7'b1101111: Num_Disp1_temp[4] <= ~Num_Disp1_temp[4];
7'b1011111: Num_Disp1_temp[5] <= ~Num_Disp1_temp[5];
7'b0111111: Num_Disp1_temp[6] <= ~Num_Disp1_temp[6];
endcase
end
将最终的信息码与生成的crc校验码拼接成数据包,通过串口发送到接收端。
/*---------------串口发送模块--------------*/
reg bit_flag = 1'd0;
always @ (posedge clk) begin
if(ready) begin
if(bit_flag) begin
bit_flag <= 1'd0;
txd_data <= Num_Disp1_temp;
tx_data_en <= 1'd1;
end
else begin
bit_flag <= 1'd1;
txd_data <= Num_Disp2;
tx_data_en <= 1'd1;
end
end
else
tx_data_en <= 1'd0;
end
reg tx_data_en = 1'd1;
reg [7:0] txd_data;
Uart_Txd Uart_U1
(
//input
.clk_in(clk),
.rst_n(rst),
.tx_data_en(tx_data_en),
.txd_data(txd_data),
//output
.ready(ready), //空闲状态下ready为1,发送数据时为0
.txd(txd)
);
接受端:
数据来源:接收串口发送来的数据
数据处理:CRC解码
数据显示:将信息码显示在数码管的前两位上,后两位显示计算后的CRC余数(为0表示传输没有错误)
串口接收数据:
/*----------------------串口接收------------------------*/
wire [7:0] DATA;
wire Uart_Get;
reg bit_flag = 1'd1;
Data_Rece Data_Rece_U1
(
//input
.clk_in(clk),
.rst_n(rst_n),
.rxd(rxd),
//output
.Data_Out(DATA),
.Rxd_Data_En(Uart_Get)
);
reg [7:0] UART_DATA1;
reg [7:0] UART_DATA2;
always @ ( posedge clk ) begin
if(Uart_Get_posedge) begin
if(bit_flag) begin
bit_flag <= 1'd0;
UART_DATA1 <= DATA;
end
else begin
bit_flag <= 1'd1;
UART_DATA2 <= DATA;
end
end
end
//采样Uart_Get的上升沿
reg Uart_Get_d0;
reg Uart_Get_d1;
wire Uart_Get_posedge;
assign Uart_Get_posedge = Uart_Get_d0 && (~Uart_Get_d1);
always @ (posedge clk ) begin
Uart_Get_d0 <= Uart_Get;
Uart_Get_d1 <= Uart_Get_d0;
end
得到发送端发来的数据包,此处的数据包使用两个寄存器保存,分别为UART_DATA1和UART_DATA2,DATA1的低7位中存的是信息码,最高位为0,DATA2中是CRC校验码,将两个数据拼接成一个15位的数据传入CRC校验模块,得到余数CRC_DATA。
/*-----------------------------------CRC校验部分----------------------------------------*/
wire [7:0] CRC_DATA;
crc CRC_U1
(
.rst(rst_n),
.clk(clk),
.data_in( { UART_DATA1[6:0] , UART_DATA2 } ),
.crc( CRC_DATA )
);
CRC校验模块与发送端的CRC模块不同,此处传入的数据为15个bit,得到的余数位仍然为8位。模块代码如下:
module crc(
rst,
clk,
data_in,
crc
);
/*************************************/
input rst;
input clk;
input [14:0] data_in;
output reg[7:0] crc;
reg feedback;
reg [7:0] crc_tmp=8'd0;
wire crc_start;
assign crc_start = clk_1Hz_posedge;
always @(posedge clk or negedge rst)
begin
if(!rst)
crc <= 8'b0;
else if(crc_start==1'b0)
crc <= crc_tmp;
end
integer i;
always@( data_in or crc)
begin
crc_tmp = 8'd0;
for(i=14; i>=0; i=i-1)
begin
feedback = crc_tmp[7] ^ data_in[i];
crc_tmp[7] = crc_tmp[6] ^ feedback;
crc_tmp[6] = crc_tmp[5] ^ feedback;
crc_tmp[5] = crc_tmp[4] ;
crc_tmp[4] = crc_tmp[3] ^ feedback;
crc_tmp[3] = crc_tmp[2] ;
crc_tmp[2] = crc_tmp[1] ;
crc_tmp[1] = crc_tmp[0] ;
crc_tmp[0] = feedback ;
end
end
//----------------------------------------分频-----1Hz-----------------------
reg [26:0] Count_1Hz;
reg clk_1Hz;
always @ (posedge clk) begin
if(Count_1Hz >= 27'd24_999_999) begin
Count_1Hz <= 27'd0;
clk_1Hz <= ~clk_1Hz;
end
else
Count_1Hz <= Count_1Hz +27'd1;
end
//采样clk_1Hz的上升沿
reg clk_1Hz_d0;
reg clk_1Hz_d1;
wire clk_1Hz_posedge;
assign clk_1Hz_posedge = clk_1Hz_d0 && (~clk_1Hz_d1);
always @ (posedge clk ) begin
clk_1Hz_d0 <= clk_1Hz;
clk_1Hz_d1 <= clk_1Hz_d0;
end
endmodule
仿真波形如图:CRC码为0x2A1A,校验完成后得到的crc余数为0,表明数据传输没有错误。
实际的板子上的显示情况与之一致:后两位显示0x00;
手动制作一个错误,按下第5个按键,对应的第五位数取反,发送的数据成了0x0A,而此时校验码不变,还是为0x1A,仿真波形如图:发现CRC余数不为0,为0x40
此时电路板上的显示为0x0A40(0A表示手动制造误差后的数据,40表示CRC余数):
查阅余数与出错位置表可以验证正好是按键按下的第五位发生了错误
至此,CRC的产生与验证便完成了。
来张合照留个念:
2018//11/30