近日,因为项目的需要,重新拾起编码理论,这个项目当中,发送端的信息序列添加了CRC码字,好在接收端进行CRC校验,检测接收到的信息序列是否在传输过程中出现了错误。CRC编码作为循环码字的一种,通常用在数据传输的检错中,其生成原理与循环码字的生成完全一致,其原理下面会进行说明。
1. 编码原理
循环码的码字多项式都是多项式g(D)的倍式,该g(D)的阶数为r=n-k,对于一个信息序列M(D),其码字多项式可以表示为
A(D) = D^(n-k)*M(D) + r(D),
r(D)为监督码多项式,它等于D^(n-k)*M(D)除以g(D)得到的余式,说的通俗一点,循环码的构造就是凑出一个余式r(D)使得码字多项式A(D)能够被生成多项式g(D)整除,在传输过程中码字A(D)一旦发生了错误,则接收端的码字不能够被生成多项式整除,就可以知道在传输过程中发生了错误,在循环码字的纠错范围内时,可以根据错误的图案(接收到的码字除以生成多项式的余式),找到对应错误的信息比特位。余式r(D)可以表示为
r(D) = D^(n-k)*M(D) mod g(D),
下面举个例子看看循环码字是怎么生成的。对于一个(7, 4)的系统循环码,7表示码字长度n=7,4表示编码的信息序列长度k=4,那么监督序列的长度r=n-k=3,系统码表示生成的n比特码字中,前面k个比特的码字与信息码字完全一致,只是在信息码字后添加了r个比特的校验位。言归正传,(7, 4)系统循环码的生成多项式为g(D) = D^3 + D^2 + 1,若信息码为1001,那么怎么求编码后的码字呢?我们知道,只要求出后面r个比特的监督码字就行了,前面的k个比特的码字照抄信息码字就行了。信息码字1001写成多项式的形式为
M(D) = D^3 + 1,
监督多项式通过求余得到:
r(D) = D^3*(D^3 + 1) mod (D^3 + D^2 + 1) = D + 1,
码字多项式:
A(D) = D^3*(D^3 + 1) + D + 1 = D^6 + D^3 + D + 1,
码字多项式写成序列的形式为1001011。值得一提的是,在编码过程中,所采用是GF(2)域上的加减法运算,相当于说减法也可看成加法,所有的加法都是模2加运算,即
1 + 1 = 0, 1 + 0 = 1, 0 + 1 = 1, 0 + 0 = 0,
对于k比较长的情况,写成这种多项式的形式不便于计算,这里提供另外一种表达形式,实际上都是求余式操作,不过在形式上做了简化而已。
1 1 1 1
_____________________
1101 | 1 0 0 1 0 0 0
+ 1 1 0 1
----------------------------
1 0 0 0
+ 1 1 0 1
-----------------------------
1 0 1 0
+ 1 1 0 1
-----------------------------
1 1 1 0
+ 1 1 0 1
-------------------------------
0 1 1
该方法只是将多项式操作变成了数字操作,信息序列1001后添加3个0代表乘上D^3,生成多项式D^3 + D^2 + 1写成序列形式就是1101,每次商上面的一位操作,进行模2加,余式继续同样的操作,直到补零的信息序列截止到最后一位,剩下的余式011即为监督序列,两种方法原理相同,当然结果也是相同的。
2. 硬件电路
上面的硬件电路图就是生成多项式g(D) = D^3 + D^2 + 1,对应的循环码字生成电路,至于为什么是这样,笔者表示上课时,老师也没讲,自己也并未深究,为了应付考试,强行记住了,然而到了做项目的时候,才发现自己对该电路完全没搞懂,于是乎,琢磨了一番,总算是搞明白了,下面且听我细细道来。
首先,讲一下硬件电路对应的时序控制,由于该循环码字是系统码,生成的码字在前k=4个比特是将信息序列直接输出,也即与门1在1拍~4拍是开启的,信息序列进行编码,但输出相关的与门2是关闭的,与门2的输出在前4拍一直是0,在或门处,信息序列直接输出;到了5拍~7拍,需要输出监督序列了,这是信息序列已经全部处理完毕,监督序列完全寄存在了D1~D3这3个寄存器中,只要将这3比特的监督序列直接输出即可,这时与门1在5拍~7拍关闭,与门2在5拍~7拍开启,或门输出的是寄存器直接出来的监督序列。
接下来,我们看看电路的左半部分是如何由信息序列生成监督序列的。我们看生成多项式g(D) = D^3 + D^2 + 1,对应到图上寄存器D3和D2后面的异或门,通过反馈操作实现除法求余操作。它的生成原理与第一小节编码原理给出的基本相同,但不同的地方是,由于信息比特是按照时钟逐比特输入的,没办法做到像编码原理给出的将这个信息序列对生成多项式求余,而是每进来一个信息比特,进行判断,然后操作。具体是啥意思呢,我讲个例子就清楚了。,
信息序列还是1001,初始状态时,D1~D3三个寄存器的值都为0,输入的第一个信息比特是1,与D3的异或是1,这里为什么输入序列与D3做异或呢?其实这里隐含了一个选择操作,D1~D3代表当前的余式,D3表示余式的最高位,如果当前的输入与当前的余式最高位D3相同的话,则当前输入与余式的模2加为0,即当前输入更新使得余式最高位为0,则此时商上面上0,不用对生成多项式做模2加运算,余式只要右移一位完成余式的更新操作(一个比特与0异或等于自身,所以这种情况下,电路中异或不起作用);另一种情况是,当前的余式最高位D3与当前输入序列异或为1,表示当前输入更新后余式包含最高位,需要对生成多项式求余,求余操作通过电路中的异或进行,完成D1~D3的更新。具体步骤如下:
(1) 初始状态: 输入ui D1 D2 D3 输出uo
0 0 0
(2) 1 1 0 1 1
(3) 0 1 1 1 0
(4) 0 1 1 0 0
(5) 1 1 1 0 1
D1~D3剩下的011即为监督比特,剩下3拍,逐比特输出:
(6) 0 0 1 1 0
(7) 0 0 0 1 1
(8) 0 0 0 0 1
3. FPGA实现
根据第二部分的电路原理分析,我们可以将循环码的编码分成两个阶段,第一个阶段为编码阶段,码字输出选择信息序列的输入直接作为输出;第二阶段为监督码字输出阶段,在第一阶段的最后,监督码字实际上已经完全生成,存储在寄存器中,第二阶段的任务就是将监督码字逐个输出,由此可以通过状态机实现该操作。状态机可以设置三种状态:IDLE, ENCODING, OUT。IDLE状态表示状态机处于空闲状态,不进行任何操作;ENCODING状态时,状态机进行输入信息序列的循环编码,同时输出信息码字;OUT状态时,输出监督码字。下面将采用verilog语言实现循环码的编码过程:
module cyc_encoding(
input clk,
input rst_n,
input e_start_i, //编码启动信号,比第一个信息序列早一个时钟周期
input symbol_i, //输入信息序列
output reg e_start_o, //输出启动信号
output reg symbol_o //输出编码序列
);
// 常量定义
parameter INFO_BITS_LEN = 4; //信息序列长度
parameter CODED_BITS_LEN = 7; //编码码字长度
wire enable;
wire encoding_start;
wire encoding_end;
reg [2:0] symbol_cnt;
// 状态机定义
parameter IDLE = 3'b001;
parameter ENCODING = 3'b010;
parameter OUT = 3'b100;
reg [2:0] state;
/**********************************************************/
// 编码状态机
/**********************************************************/
always @(posedge clk)
begin
if(!rst_n)
state <= IDLE;
else begin
case(state)
IDLE: state <= e_start_i ? ENCODING : IDLE;
ENCODING: state <= encoding_start ? OUT : ENCODING;
OUT: state <= encoding_end ? IDLE : OUT;
default: state <= IDLE;
endcase
end
end
assign enable = (state != IDLE);
/**********************************************************/
// 符号计数器
/**********************************************************/
always @(posedge clk)
begin
if(!rst_n)
symbol_cnt <= 3'd0;
else
symbol_cnt <= enable ? (symbol_cnt + 1'b1) : 3'd0;
end
assign encoding_start = (symbol_cnt == INFO_BITS_LEN -1);
assign encoding_end = (symbol_cnt == CODED_BITS_LEN -1);
reg [2:0] encoding_reg;
/**********************************************************/
// 寄存器更新操作
/**********************************************************/
always @(posedge clk)
begin
if(!rst_n)
encoding_reg <= 3'd0;
else begin
case(state)
IDLE: encoding_reg <= 3'd0;
ENCODING:
begin
encoding_reg[0] <= encoding_reg[2] ^ symbol_i;
encoding_reg[1] <= encoding_reg[0];
encoding_reg[2] <= encoding_reg[1] ^ encoding_reg[2] ^ symbol_i;
end
OUT: encoding_reg <= {encoding_reg[1:0], 1'b0};
default: encoding_reg <= 3'd0;
endcase
end
end
/**********************************************************/
// 输出编码后的符号
/**********************************************************/
always @(posedge clk)
begin
if(!rst_n)
symbol_o <= 1'b0;
else begin
case(state)
IDLE: symbol_o <= 1'b0;
ENCODING: symbol_o <= symbol_i; //信息序列
OUT: symbol_o <= encoding_reg[2]; //监督序列
default: symbol_o <= 1'b0;
endcase
end
end
/**********************************************************/
// 启动信号输出
/**********************************************************/
always @(posedge clk)
begin
if(!rst_n)
e_start_o <= 1'b0;
else
e_start_o <= e_start_i;
end