FPGA串口接收解帧、并逐帧发送有效数据
工程实现的功能:FPGA串口接收到串口调试助手发来的数据,将其数据解帧。判断到正确的帧头和帧尾之后,将有效数据存入rx_data中;另一方面发送端将有效数据逐帧发送出去。
参考:正点原子官方FPGA串口通信实验
模块构成:
在原子哥的基础上改的代码。添加了接收状态机模块:rx_state_machine ;修改了串口发送模块:uart_send。其余部分代码基本不变(只加了例化,修改数据位宽)
本节接着上一章的内容来写,来写发送端的部分。实现功能:将数据打包发送,发送帧头+接收端解帧后的有效数据+帧尾。
假设发送端打包时,帧头是FF,帧尾是EE.
代码思路分析
首先,给出正点原子的代码
verilog
module uart_send(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
input uart_en, //发送使能信号
input [7:0] uart_din, //待发送数据
output uart_tx_busy, //发送忙状态标志
output en_flag ,
output reg tx_flag, //发送过程标志信号
output reg [ 7:0] tx_data, //寄存发送数据
output reg [ 3:0] tx_cnt, //发送数据计数器
output reg uart_txd //UART发送端口
);
//parameter define
parameter CLK_FREQ = 50000000; //系统时钟频率
parameter UART_BPS = 9600; //串口波特率
localparam BPS_CNT = CLK_FREQ/UART_BPS; //为得到指定波特率,对系统时钟计数BPS_CNT次
//reg define
reg uart_en_d0;
reg uart_en_d1;
reg [15:0] clk_cnt; //系统时钟计数器
//wire define
wire en_flag;
//*****************************************************
//** main code
//*****************************************************
//在串口发送过程中给出忙状态标志
assign uart_tx_busy = tx_flag;
//捕获uart_en上升沿,得到一个时钟周期的脉冲信号
assign en_flag = (~uart_en_d1) & uart_en_d0;
//对发送使能信号uart_en延迟两个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_en_d0 <= 1'b0;
uart_en_d1 <= 1'b0;
end
else begin
uart_en_d0 <= uart_en;
uart_en_d1 <= uart_en_d0;
end
end
//当脉冲信号en_flag到达时,寄存待发送的数据,并进入发送过程
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
tx_flag <= 1'b0;
tx_data <= 8'd0;
end
else if (en_flag) begin //检测到发送使能上升沿
tx_flag <= 1'b1; //进入发送过程,标志位tx_flag拉高
tx_data <= uart_din; //寄存待发送的数据
end
//计数到停止位结束时,停止发送过程
else if ((tx_cnt == 4'd9) && (clk_cnt == BPS_CNT - (BPS_CNT/16))) begin
tx_flag <= 1'b0; //发送过程结束,标志位tx_flag拉低
tx_data <= 8'd0;
end
else begin
tx_flag <= tx_flag;
tx_data <= tx_data;
end
end
//进入发送过程后,启动系统时钟计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
clk_cnt <= 16'd0;
else if (tx_flag) begin //处于发送过程
if (clk_cnt < BPS_CNT - 1)
clk_cnt <= clk_cnt + 1'b1;
else
clk_cnt <= 16'd0; //对系统时钟计数达一个波特率周期后清零
end
else
clk_cnt <= 16'd0; //发送过程结束
end
//进入发送过程后,启动发送数据计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
tx_cnt <= 4'd0;
else if (tx_flag) begin //处于发送过程
if (clk_cnt == BPS_CNT - 1) //对系统时钟计数达一个波特率周期
tx_cnt <= tx_cnt + 1'b1; //此时发送数据计数器加1
else
tx_cnt <= tx_cnt;
end
else
tx_cnt <= 4'd0; //发送过程结束
end
//根据发送数据计数器来给uart发送端口赋值
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
uart_txd <= 1'b1;
else if (tx_flag)
case(tx_cnt)
4'd0: uart_txd <= 1'b0; //起始位
4'd1: uart_txd <= tx_data[0]; //数据位最低位
4'd2: uart_txd <= tx_data[1];
4'd3: uart_txd <= tx_data[2];
4'd4: uart_txd <= tx_data[3];
4'd5: uart_txd <= tx_data[4];
4'd6: uart_txd <= tx_data[5];
4'd7: uart_txd <= tx_data[6];
4'd8: uart_txd <= tx_data[7]; //数据位最高位
4'd9: uart_txd <= 1'b1; //停止位
default: ;
endcase
else
uart_txd <= 1'b1; //空闲时发送端口为高电平
end
endmodule
代码解释:以 uart_en 为发送使能信号。uart_en 的上升沿将启动一次串口发送过程,将 uart_din 接口上的数据通过串口发送端口 uart_txd 发送出去。
tx_cnt
是发送数据计数器,从0-9分别发送起始位、数据位、停止位。
我们要实现的功能是发送帧头+接收端解帧后的有效数据+帧尾。
所以只需要对这个代码进行简单的修改,即可满足功能。
首先,我加了一个计数器
verilog
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
sent_cnt <= 4'd0;
else if(tx_flag) begin //处于发送过程时,开始计数。每个数都对应需要发送的数据
if(sent_cnt < 4'd6)
sent_cnt <= sent_cnt + 1'b1;
else
sent_cnt <= 4'd0;
end
end
reg [3:0] sent_cnt; //发送计数器,用来计数发送的数据是FF、data、EE
在原子哥的代码中,一个case语句,发送的是完整的一个字节(8bit)数据。但是现在,我想要一次发送帧头(1个字节)+数据(4个字节)+帧尾(1个字节)。所以,我用了两个case语句嵌套使用。
我外层的case语句,case (sent_cnt)
,判断发送的是第几个字节(如:第一个字节,发送帧头;第二个字节,发送数据高8位)
内层的case语句,则与原子哥相同,发送完整的一个字节(8bit)数据。
verilog
//根据发送数据计数器来给uart发送端口赋值
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
uart_txd <= 1'b1;
else if (tx_flag)
case (sent_cnt)
4'd0: uart_txd <= uart_txd;
4'd1: begin
case(tx_cnt)
4'd0: uart_txd <= 1'b0; //起始位
4'd1: uart_txd <= 1'b1; //数据位最低位
4'd2: uart_txd <= 1'b1;
4'd3: uart_txd <= 1'b1;
4'd4: uart_txd <= 1'b1; //发送 帧头 FF
4'd5: uart_txd <= 1'b1;
4'd6: uart_txd <= 1'b1;
4'd7: uart_txd <= 1'b1;
4'd8: uart_txd <= 1'b1; //数据位最高位
4'd9: uart_txd <= 1'b1; //停止位
default: uart_txd <= uart_txd;
endcase
end
4'd2: begin
case(tx_cnt)
4'd0: uart_txd <= 1'b0; //起始位
4'd1: uart_txd <= tx_data[0]; //数据位最低位
4'd2: uart_txd <= tx_data[1];
4'd3: uart_txd <= tx_data[2];
4'd4: uart_txd <= tx_data[3];
4'd5: uart_txd <= tx_data[4];
4'd6: uart_txd <= tx_data[5];
4'd7: uart_txd <= tx_data[6];
4'd8: uart_txd <= tx_data[7]; //数据位最高位
4'd9: uart_txd <= 1'b1; //停止位
default: ;
endcase
end
4'd3: begin
case(tx_cnt)
4'd0: uart_txd <= 1'b0; //起始位
4'd1: uart_txd <= tx_data[8]; //数据位最低位
4'd2: uart_txd <= tx_data[9];
4'd3: uart_txd <= tx_data[10];
4'd4: uart_txd <= tx_data[11];
4'd5: uart_txd <= tx_data[12];
4'd6: uart_txd <= tx_data[13];
4'd7: uart_txd <= tx_data[14];
4'd8: uart_txd <= tx_data[15]; //数据位最高位
4'd9: uart_txd <= 1'b1; //停止位
default: ;
endcase
end
4'd4: begin
case(tx_cnt)
4'd0: uart_txd <= 1'b0; //起始位
4'd1: uart_txd <= tx_data[16]; //数据位最低位
4'd2: uart_txd <= tx_data[17];
4'd3: uart_txd <= tx_data[18];
4'd4: uart_txd <= tx_data[19];
4'd5: uart_txd <= tx_data[20];
4'd6: uart_txd <= tx_data[21];
4'd7: uart_txd <= tx_data[22];
4'd8: uart_txd <= tx_data[23]; //数据位最高位
4'd9: uart_txd <= 1'b1; //停止位
default: ;
endcase
end
4'd5: begin
case(tx_cnt)
4'd0: uart_txd <= 1'b0; //起始位
4'd1: uart_txd <= tx_data[24]; //数据位最低位
4'd2: uart_txd <= tx_data[25];
4'd3: uart_txd <= tx_data[26];
4'd4: uart_txd <= tx_data[27];
4'd5: uart_txd <= tx_data[28];
4'd6: uart_txd <= tx_data[29];
4'd7: uart_txd <= tx_data[30];
4'd8: uart_txd <= tx_data[31]; //数据位最高位
4'd9: uart_txd <= 1'b1; //停止位
default: ;
endcase
end
4'd6: begin
case(tx_cnt)
4'd0: uart_txd <= 1'b0; //起始位
4'd1: uart_txd <= 1'b0; //数据位最低位
4'd2: uart_txd <= 1'b1;
4'd3: uart_txd <= 1'b1;
4'd4: uart_txd <= 1'b1; //发送 帧尾 EE
4'd5: uart_txd <= 1'b0;
4'd6: uart_txd <= 1'b1;
4'd7: uart_txd <= 1'b1;
4'd8: uart_txd <= 1'b1; //数据位最高位
4'd9: uart_txd <= 1'b1; //停止位
default: uart_txd <= uart_txd;
endcase
end
endcase
else
uart_txd <= 1'b1; //空闲时发送端口为高电平
end
代码解释:发送过程标志信号tx_flag
拉高时,进入发送状态。
首先case (sent_cnt)
判断发送的是第几个字节
然后case(tx_cnt)
,发送完整的一个字节。
结果展示
verilog
ila_1 ila_tx (
.clk(sys_clk), // input wire clk
.probe0(tx_flag), // input wire [0:0] probe0
.probe1(din), // input wire [31:0] probe1
.probe2(uart_txd), // input wire [0:0] probe2
.probe3(tx_data), // input wire [31:0] probe3
.probe4(tx_data[7:0]), // input wire [7:0] probe4
.probe5(tx_data[15:8]), // input wire [7:0] probe5
.probe6(tx_data[23:16]), // input wire [7:0] probe6
.probe7(tx_data[31:24]) // input wire [7:0] probe7
);
如上投入所示,发送帧头+接收端解帧后的有效数据+帧尾
-
tx_data_1是发送的有效数据
-
din 是接收端的有效数据。可以看出,这两个相同
-
uart_txd是FPGA串口发送的信号,由0和1组成,发送的是整个完整的数据(帧头+有效数据+帧尾)
各个模块的输入输出
发送端相对接收端简单、易理解
接下来我将所有模块的输入输出放下面,方便大家理解整体的框架
uart_loopback_top
verilog
module uart_loopback_top(
input sys_clk, //外部50M时钟
input sys_rst_n, //外部复位信号,低有效
input uart_rxd, //UART接收端口
output uart_txd //UART发送端口
);
uart_recv
verilog
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
);
rx_state_machine
verilog
module rx_state_machine(
input sys_clk ,
input sys_rst_n ,
input uart_done , //接收一帧数据完成标志
input [7:0] uart_data , //串口接收到上位机的所有数据
input rx_flag ,
output reg rx_vaild , //接收解帧有效数据
output reg rx_error , //接收解帧错误信号
output reg [31:0] rx_data //接收有效数据
);
uart_send
verilog
module uart_send(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
input uart_en, //发送使能信号
input [7:0] uart_din, //待发送数据
input [31:0] din,
output uart_tx_busy, //发送忙状态标志
output en_flag ,
output reg tx_flag, //发送过程标志信号
output reg [ 31:0] tx_data, //寄存发送数据
output reg [ 3:0] tx_cnt, //发送数据计数器
output reg uart_txd //UART发送端口
);
uart_loop
verilog
module uart_loop(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
input recv_done, //接收一帧数据完成标志
input [31:0] recv_data, //接收的数据
input tx_busy, //发送忙状态标志
output reg send_en, //发送使能信号
output reg [31:0] send_data //待发送数据
);
注
有几个可能出错的地方:我的发送端的数据高低位可能颠倒了。如果大家觉得确实发送数据高低位错了,请自行改正。
有好些信号我没有用到,但是在代码中我没有删除。
另外,数据环回的功能好像没有做好,我觉得还是数据位宽的原因。