FPGA串口接收解帧、并逐帧发送有效数据
工程实现的功能:FPGA串口接收到串口调试助手发来的数据,将其数据解帧。判断到正确的帧头和帧尾之后,将有效数据存入rx_data中;另一方面发送端将有效数据逐帧发送出去。
参考:正点原子官方FPGA串口通信实验
模块构成:
在原子哥的基础上改的代码。添加了接收状态机模块:rx_state_machine;修改了串口发送模块:uart_send。其余部分代码基本不变(只加了例化,修改数据位宽)
本节接着上一章的内容来写,来写发送端的部分。实现功能:将数据打包发送,发送帧头+接收端解帧后的有效数据+帧尾。
假设发送端打包时,帧头是FF,帧尾是EE.
代码思路分析
首先,给出正点原子的代码
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) beginuart_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;endelse begintx_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;elseclk_cnt <= 16'd0; //对系统时钟计数达一个波特率周期后清零endelse 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; //此时发送数据计数器加1elsetx_cnt <= tx_cnt; endelse 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: ;endcaseelse uart_txd <= 1'b1; //空闲时发送端口为高电平
endendmodule
代码解释:以 uart_en 为发送使能信号。uart_en 的上升沿将启动一次串口发送过程,将 uart_din 接口上的数据通过串口发送端口 uart_txd 发送出去。
tx_cnt
是发送数据计数器,从0-9分别发送起始位、数据位、停止位。
我们要实现的功能是发送帧头+接收端解帧后的有效数据+帧尾。
所以只需要对这个代码进行简单的修改,即可满足功能。
首先,我加了一个计数器
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)数据。
//根据发送数据计数器来给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: begincase(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; //发送 帧头 FF4'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;endcaseend4'd2: begincase(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: ;endcaseend4'd3: begincase(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: ;endcaseend 4'd4: begincase(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: ;endcaseend 4'd5: begincase(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: ;endcaseend 4'd6: begincase(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; //发送 帧尾 EE4'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;endcaseend endcaseelse uart_txd <= 1'b1; //空闲时发送端口为高电平
end
代码解释:发送过程标志信号tx_flag
拉高时,进入发送状态。
首先case (sent_cnt)
判断发送的是第几个字节
然后case(tx_cnt)
,发送完整的一个字节。
结果展示
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
module uart_loopback_top(input sys_clk, //外部50M时钟input sys_rst_n, //外部复位信号,低有效input uart_rxd, //UART接收端口output uart_txd //UART发送端口);
uart_recv
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
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
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
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 //待发送数据);
注
有几个可能出错的地方:我的发送端的数据高低位可能颠倒了。如果大家觉得确实发送数据高低位错了,请自行改正。
有好些信号我没有用到,但是在代码中我没有删除。
另外,数据环回的功能好像没有做好,我觉得还是数据位宽的原因。