FPGA串口接收解帧、并逐帧发送有效数据
工程实现的功能:FPGA串口接收到串口调试助手发来的数据,将其数据解帧。判断到正确的帧头和帧尾之后,将有效数据存入rx_data中;另一方面发送端将有效数据逐帧发送出去。
参考:正点原子官方FPGA串口通信实验
模块构成:
在原子哥的基础上改的代码。添加了接收状态机模块:rx_state_machine;修改了串口发送模块:uart_send。其余部分代码基本不变(只加了例化,修改数据位宽)
接收状态机模块rx_state_machine——进行解帧处理,接收有效数据
假设:帧头为AA,帧尾为55,有效数据为32bit
思路:使用三段式状态机
接收状态机标志位是什么?
module uart_recv(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
input uart_rxd, //UART接收端口
output reg uart_done, //接收一帧数据完成标志
output reg rx_flag, //接收过程标志信号
output reg [ 3:0] rx_cnt, //接收数据计数器
output reg [ 7:0] rxdata,
output reg [7:0] uart_data, //接收的数据
output reg [31:0] rx_data
);
在uart_recv中,有一个uart_done信号,是接收一帧数据完成标志,当uart_done拉高,表明接收了一帧数据(8bit),并存到了 uart_data 中。
我们可以用uart_done的上升沿作为状态机跳转的使能信号。
边沿检测如下:
//边沿检测 uart_done 信号
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)begin
uart_done_prev1 <= 0;
uart_done_prev2 <= 0;
end
else begin
uart_done_prev1 <= uart_done;
uart_done_prev2 <= uart_done_prev1; //延迟两拍
end
end
//上升沿检测
assign uart_done_rise = uart_done_prev1 & ~uart_done_prev2;
检测到uart_done的上升沿时,uart_done_rise拉高一个时钟周期。当uart_done_rise拉高时(表明接收了一帧数据,并存到了 uart_data 中),状态机进入跳转判断。
状态机设计
在上面的假设中,帧头为AA,帧尾为55,有效数据为4帧(32bit)。
所以状态机的状态有:
-
起始状态IDLE
-
如果接收到AA,则进入DATA_RX状态
-
如果接收的数据>=4帧 ,进入下一个状态,即帧尾检测状态
-
如果检测到了帧尾,则进入DONE状态,表明解帧完成
在接收帧头或帧尾的状态中,只要不是AA或55,则重新进入起始状态。
localparam SOF = 8'haa; //帧头
localparam EOF = 8'h55; //帧尾
localparam IDLE = 4'd0;
localparam DATA_SOF = 4'd1;
localparam DATA_RX = 4'd2;
localparam DATA_EOF = 4'd3;
localparam DONE = 4'd4;
//时序逻辑状态切换
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
c_status <= 4'd0;
else
c_status <= n_status;
end
//组合逻辑状态切换
always @(*) begin
case(c_status)
IDLE : begin //起始状态
if(uart_done_rise) begin
if(uart_data == SOF)
n_status = DATA_RX; //如果接收到AA,则进入DATA_RX状态
else
n_status = IDLE;
end
else
n_status = IDLE;
end
DATA_RX: begin //接收有效数据状态
if(uart_done_rise) begin
if(r_bytecnt >= 3'd3)
n_status = DATA_EOF;
else
n_status = DATA_RX;
end
else n_status = DATA_RX;
end
DATA_EOF: begin //接收帧尾状态
if(uart_done_rise) begin
if(uart_data == EOF)
n_status = DONE;
else
n_status = IDLE;
end
else n_status = DATA_EOF;
end
DONE: begin //解帧完成
n_status = IDLE;
end
default: begin
n_status = IDLE;
end
endcase
end
//接收4个有效数据的计数器
always @(posedge sys_clk) begin
if(c_status == DATA_RX) begin
if(uart_done_rise)
r_bytecnt <= r_bytecnt+1;
else ;
end
else r_bytecnt <= 3'b0;
end
代码解释:首先定义帧头SOF= 8’haa;(start of flag),帧尾EOF= 8’h55;(end of flag),以及5个状态(在后面设计状态机时发现DATA_SOF状态用不到)
-
第一个always语句块,时序逻辑状态切换,保证了当前状态c_status和下个状态n_status的切换。
-
第二个always语句块,4个状态所执行的操作。
-
c_status为IDLE,uart_done_rise拉高时,状态机进入跳转判断。
如果此时uart_data中的数据是帧头SOF,则准备进入下一个状态,进行有效数据的接收DATA_RX状态;
否则继续停留在IDLE状态。
-
c_status为DATA_RX,uart_done_rise拉高时,状态机进入跳转判断。
在DATA_RX状态中,如果接收的字节数
r_bytecnt>= 3'd3
,说明已经完整地接收了4帧的有效数据,准备进入下个状态,进行帧尾检测DATA_EOF;否则,说明数据还没有接收完,继续停留在DATA_RX状态接收数据。
-
c_status为DATA_EOF,uart_done_rise拉高时,状态机进入跳转判断。
如果此时uart_data中的数据是帧尾EOF,则进入解帧完成状态DONE;
否则,说明帧尾不正确,该组数据无效,状态进入IDLE,重新解帧新的数据
-
-
第三个always语句中,是接收4个有效数据的计数器。c_status处于有效数据的接收状态时,每次uart_done_rise拉高(表明接收了一帧数据),计数器r_bytecnt加1;
上面的三个always语句块,实现了状态的跳转和判断。接下来需要计算数据,并判断什么情况下进行输出。
//将所有接收到的值赋给寄存器 reg_rx_data
always @(posedge sys_clk) begin
if((c_status == DATA_RX) && uart_done_rise) begin
case(r_bytecnt)
3'd0: reg_rx_data[31:24] <= uart_data;
3'd1: reg_rx_data[23:16] <= uart_data;
3'd2: reg_rx_data[15:8] <= uart_data;
3'd3: reg_rx_data[7:0] <= uart_data;
default: ;
endcase
end
end
首先,将有效数据存入寄存器reg_rx_data中。
在c_status = DATA_RX
状态下,一共接收了4帧的数据,现在把这4帧数据组合成一个32bit的数据。
当r_bytecnt为0时,这时候接收的是最高位,存入reg_rx_data[31:24],
当r_bytecnt为1时,将此时刻接收的数据存入reg_rx_data[23:16],
依此类推。
//判断数据是否有效
always @(posedge sys_clk) begin
if((c_status == DONE)) begin
rx_data <= reg_rx_data;
rx_vaild <= 1'b1;
rx_error <= 1'b0;
end
else if((c_status == DATA_EOF) && (uart_data != EOF)) begin
rx_data <= rx_data;
rx_error <= 1'b1;
rx_vaild <= 1'b0;
end
else begin
rx_data <= rx_data;
rx_vaild <= 1'b0;
rx_error <= 1'b1;
end
end
接下来,判断什么情况下进行输出 / 判断该组数据是否有效
c_status == DONE
时,表明完整地解帧完了一组数据,rx_data <= reg_rx_data;
将寄存器reg_rx_data的值赋给输出rx_data;同时给两个标志位rx_vaild、rx_error赋值。- 当解帧的帧尾不等于EOF时,接收数据无效,rx_data保持不变。
注释:至于为什么这里只判断了帧尾,没有判断帧头,是因为帧头的判断在状态机里就实现了:如果帧头不正确,则直接回到起始状态,进行不到接收有效数据的状态。但是帧尾还需要再判断,是因为进行到帧尾的状态时,就已经接收完有效数据了,并已经存入了reg_rx_data中。所以,还要再对帧尾进行判断,如果不符合,则rx_data不更新,认为这个数据无效。
接收解帧结果
ila_0 ila_rx (
.clk(sys_clk), // input wire clk
.probe0(uart_done), // input wire [0:0] probe0
.probe1(uart_data), // input wire [7:0] probe1
.probe2(rx_vaild), // input wire [0:0] probe2
.probe3(rx_data), // input wire [31:0] probe3
.probe4(uart_done_rise), // input wire [0:0] probe4
.probe5(c_status), // input wire [2:0] probe5
.probe6(r_bytecnt) // input wire [2:0] probe6
);
在串口调试助手中,我发送的是aa1234567855
,结果如上图,有效数据12345678存入了rx_data。
在下一节继续写逐帧发送部分