基于上一章串口回环的内容,进行一个串口的拓展学习。本章节使用串口回环进行一个对接收模块接收的数据进行一个编码以及解码的过程,通过解码通过发送模块将接收到的指令发送出去。
1.设计需求
通过串口助手发送某个关键字,比如:通过串口助手发送“hello”指令给FPGA,FPGA在检测接收到“hello”后,通过发送端口给上位机发送一行指令如“hello verilog!”
根据设计需求可以绘制我们的整体串口回环模块:
根据绘制的模块框图可以知道,本次设计只需要在回环中添加一个编码模块以及一个解码模块。
2.编码模块分析
编码模块的主要目的是将FPGA接收到的数据进行一个编码处理,判断接收的数据是否为连续的“hello”字符。一般检测数据常用的两种方式,一种是状态机,另一种为线性序列机,本次使用状态机。
上图为状态机,有点丑,但是大概就是这个意思。首先为IDLE状态,如果检测到的字符为h,则跳转到h,否则则继续再IDLE状态进行检测;在h状态检测是否为e,如果为e跳转到e的状态,否则跳转回IDLE状态;同理一直检测跳转到O。
根据编码模块以及状态机可以绘制波形图:
编码模块波形很简单,只是做一个打拍然后取一个上升沿作为接收完成标志位进行状态检测,如果检测到字符为“hello”,则发送使能拉高。
代码如下:
//接收数据编码模块
module uart_coding(
input sys_clk , // 系统时钟
input sys_rst_n , // 系统复位,低电平有效
input recv_done , // 接收一帧数据完成标志
input [7:0] recv_data , // 接收的数据
input send_all_done , // 数据全部发送完成标志
output reg send_start // 开始发送数据标志
);
// parameter define
parameter IDLE = 6'b000_001; //空闲状态
parameter STATE_H = 6'b000_010; //h
parameter STATE_E = 6'b000_100; //e
parameter STATE_L = 6'b001_000; //l
parameter STATE_L1 = 6'b010_000; //l
parameter STATE_O = 6'b100_000; //o
parameter CHAR_H = 8'd104,
CHAR_E = 8'd101,
CHAR_L = 8'd108,
CHAR_O = 8'd111;
//reg dfine
reg recv_done_d0 ;
reg recv_done_d1 ;
reg recv_done_d2 ;
reg [5:0] current_state ; //状态机的现态
reg [5:0] next_state ; //状态机的次态
//wire define
wire recv_done_flag;
//*****************************************************
//** main code
//*****************************************************
//捕获 recv_done 上升沿,得到一个时钟周期的脉冲信号
assign recv_done_flag = (~recv_done_d2) & recv_done_d1;
//延迟两个时钟周期消除信号亚稳态
always@(posedge sys_clk or negedge sys_rst_n )begin
if(!sys_rst_n)begin
recv_done_d0 <= 1'b0;
recv_done_d1 <= 1'b0;
recv_done_d2 <= 1'B0;
end
else begin
recv_done_d0 <= recv_done ;
recv_done_d1 <= recv_done_d0;
recv_done_d2 <= recv_done_d1;
end
end
//同步时序模块
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
//转态转换条件
always@(*)begin
if(!sys_rst_n)
next_state <= 1'b0;
else begin
case(current_state)
IDLE:
if(recv_done_flag) // 检测串口接收到数据
if(recv_data == CHAR_H) // 检测字符"h"条件判断
next_state <= STATE_H; // 前往状态"STATE_H"
else
next_state <= IDLE; // 没有检测到“h”,返回初始态
else
next_state <= IDLE; // 未检测到串口接收到数据
STATE_H:
if(recv_done_flag) // 检测串口接收到数据
if(recv_data == CHAR_E) // 检测字符"e"条件判断
next_state <= STATE_E; // 前往状态"STATE_E"
else if(recv_data == CHAR_H) // 检测到“h”
next_state <= STATE_H; // 前往状态"STATE_H"
else
next_state <= IDLE; // 检测到数据但不是"e",返回初始态
else
next_state <= STATE_H; // 未检测到串口接收到数据
STATE_E:
if(recv_done_flag) // 检测串口接收到数据
if(recv_data == CHAR_L) // 检测字符"l"
next_state <= STATE_L; // 前往状态"STATE_L"
else if(recv_data == CHAR_H) // 检测到"h"
next_state <= STATE_H; // 前往状态"STATE_H"
else
next_state <= IDLE; // 检测到数据但不是"l",返回初始态
else
next_state <= STATE_E; //未检测到串口接收到数据
STATE_L:
if(recv_done_flag) // 检测串口接收到数据
if(recv_data == CHAR_L) // 检测字符"l"
next_state <= STATE_L1; // 前往状态"STATE_L1"
else if(recv_data == CHAR_H) // 检测到"h"
next_state <= STATE_H; // 前往状态"STATE_H"
else
next_state <= IDLE; // 检测到数据但不是"l",返回初始态
else
next_state <= STATE_L; // 未检测到串口接收到数据
STATE_L1:
if(recv_done_flag) // 检测串口接收到数据
if(recv_data ==CHAR_O) // 检测字符“o”
next_state <= STATE_O; // 前往状态"STATE_O"
else if(recv_data == CHAR_H) // 检测到"h"
next_state <= STATE_H; // 前往状态"STATE_H"
else
next_state <= IDLE; // 检测到数据但不是"o",返回初始态
else
next_state <= STATE_L1; // 未检测到串口接收到数据
STATE_O:
if(send_all_done) // 一帧数据全部发送完成
next_state <= IDLE; // 返回初始态
else
next_state <= STATE_O;
default:
next_state <= IDLE;
endcase
end
end
//赋值模块
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
send_start <= 1'b0;
else if(current_state==STATE_O) //成功检测到“hello”
send_start <= 1'b1;
//检测过程结束,标志位 send_start 拉高
else
send_start <= 1'b0;
end
endmodule
仿真代码参考串口回环部分,这边直接看一下 仿真波形:
首先是打三拍进行取一个上升沿作为发送完成标志,等于h则跳转到下一个状态,最好检测完整个字符跳转到100_000状态,发送使能拉高。
3.解码模块分析
同样先绘制解码模块框图:
解码模块需要两个输出信号,一个发送使能,一个发送的数据。输入的send_start信号同样需要进行一个打拍操作,单纯为了防止亚稳态,uart_tx_busy为发送模块中发送中的一个标志信号。发送数据端口总共需要发送“hello verilog!”,总共14个字符,所以还需定义一个data_cnt模块。
下面是波形图:
同样根据波形图可以绘制代码:
// 发送数据准备模块
module uart_decoding(
input sys_clk , // 系统时钟
input sys_rst_n , // 系统复位,低电平有效
input send_start , // 开始发送数据标志
input uart_tx_busy , // UART发送忙状态标志
output reg send_all_done , // 数据全部发送完成标志
output reg send_en , // 串口发送使能信号
output reg [7:0] send_data // 发送的寄存数据
);
parameter CHAR_H = 8'd104,
CHAR_E = 8'd101,
CHAR_L = 8'd108,
CHAR_O = 8'd111,
CHAR_V = 8'd118,
CHAR_R = 8'd114,
CHAR_I = 8'd105,
CHAR_G = 8'd103,
CHAR_32 = 8'd32,
CHAR_33 = 8'd33;
//reg define
reg send_done_d0 ;
reg send_done_d1 ;
reg send_done_d2 ;
reg send_start_d0 ;
reg send_start_d1 ;
reg send_start_d2 ;
reg [4:0] data_cnt ; //发送数据寄存器
//*****************************************************
//** main code
//*****************************************************
//检测到一个下降沿,得到一个时钟周期信号
assign send_done = send_done_d2 & (~send_done_d1 ) ;
//检测到一个上升沿,得到一个时钟周期信号
assign send_start_done = (~send_start_d2) & send_start_d1 ;
//延迟3个时钟周期消除数据亚稳态
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
send_start_d0 <= 1'b0 ;
send_start_d1 <= 1'b0 ;
send_start_d2 <= 1'b0 ;
end
else begin
send_start_d0 <= send_start ;
send_start_d1 <= send_start_d0 ;
send_start_d2 <= send_start_d1 ;
end
end
//延迟3个时钟周期消除数据亚稳态
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
send_done_d0 <= 1'b0 ;
send_done_d1 <= 1'b0 ;
send_done_d2 <= 1'b0 ;
end
else begin
send_done_d0 <= uart_tx_busy ;
send_done_d1 <= send_done_d0 ;
send_done_d2 <= send_done_d1 ;
end
end
// 发送数据计数器
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
data_cnt <= 5'b0;
else if(send_done) begin // 一个字节数据发送完成,send_done信号拉高
if (data_cnt == 13) // 计数到最后一个数据,计数器清零
data_cnt <= 1'b0;
else
data_cnt <= data_cnt +1;
end
end
//数据全部发送完成的过程
always@(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
send_all_done <= 1'b0;
else if((data_cnt == 13) && send_done )
//数据计数到最后一位并且发送完成
send_all_done <= 1'b1; // 数据全部发送完成,send_all_done拉高
else
send_all_done <= 1'b0;
end
//判断向上位机发送数据信号,并在串口发送模块空闲时给出发送使能信号
always@(posedge sys_clk or negedge sys_rst_n )begin
if(!sys_rst_n)
send_en <= 1'b0 ;
else if(send_start_done || (send_done && data_cnt < 13))
//接收数据完成开始发送或者寄存的一个字节数据发送完成
send_en <= 1'b1;
else
send_en <= 1'b0;
end
//将对应的发送数据赋值
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
send_data <= 8'b0;
else if(send_start)begin //检测到开始发送数据标志
case(data_cnt)
5'd0 : send_data <= CHAR_H; // h
5'd1 : send_data <= CHAR_E; // e
5'd2 : send_data <= CHAR_L; // l
5'd3 : send_data <= CHAR_L; // l
5'd4 : send_data <= CHAR_O; // o
5'd5 : send_data <= CHAR_32; //
5'd6 : send_data <= CHAR_V; // v
5'd7 : send_data <= CHAR_E; // e
5'd8 : send_data <= CHAR_R; // r
5'd9 : send_data <= CHAR_I; // i
5'd10 : send_data <= CHAR_L; // l
5'd11 : send_data <= CHAR_O; // o
5'd12 : send_data <= CHAR_G; // g
5'd13 : send_data <= CHAR_33; // !
default:;
endcase
end
else
send_data <= send_data;
end
endmodule
波形图如下:
同样先对sen_start_done打三拍取一下他的上升沿,send_en信号只有在发送开始信号为1的适合,或者在data_cnt小于13的起始位拉高。
而send_done信号则是对发送忙碌信号拉低之后进行打3拍之后取它们的一个下降沿,作为发送1位数据的完成信号进行拉高一个时钟周期。
再send_en发送使能拉高时,进行一位数据的发送,当所有数据发送完成指挥,send_all_done信号拉高,表示解码模块的数据已经发送完成。
最好将编码解码模块例化到之前的串口回环模块中,下载验证图片如下:
如果有小伙伴需要工程可以私一下我,这个实验相对来说还是比较简单,应该是没有什么问题。