FPGA入门笔记009——UART串口发送模块设计
1、UART通信原理
如图1为UART通信连接图,其中tx为输入,rx为输出。通过tx连接rx进行数据间的发送和接收。
UART 通信在使用前需要做多项设置,最常见的设置包括:数据位数、波特率大小、奇偶校验类型和停止位数:
(1)数据位(Data bits):该参数定义单个 UART 数据传输在开始到停止期间发送的数据位数。可选择为:5、6、7 或者 8(默认)。
(2)波特率(Baud)大小:是指从一设备发到另一设备的波特率,即:每秒钟可以通信的数据比特个数。典型的波特率有 300bps/Hz, 1200bps/Hz, 2400bps/Hz, 9600bps/Hz, 19200bps/Hz, 115200bps/Hz 等。一般通信两端设备都要设为相同的波特率,但有些设备也可设置为自动检测波特率。
(3)奇偶校验类型(Parity Type):是用来验证数据的正确性。奇偶校验一般不使用,如果使用,则既可以做奇校验(Odd)也可以做偶校验(Even)。
(4)停止位(Stop bits):在每个字节的数据位发送完成之后,发送停止位,来标志着一次数据传输完成,同时用来帮助接受信号方硬件重同步。可选择为:1(默认)、1.5 或者 2 位。
在 RS-232 标准中,最常用的配置是 8N1(即八个数据位、一个停止位),其发送一个字节时序图如图 2 所示。
按照一个完整的字节包括:一位起始位、8 位数据位、一位停止位,总共十位数据来算, 要想完整的实现这十位数据的发送,就需要 11 个波特率时钟脉冲,第 1 个脉冲标记一次传输的起始,第 11 个脉冲标记一次传输的结束,如图 2 所示,其中BPS_CLK为波特率时钟信号:
2、串口发送模块整体设计
基于上述原理,本章要实现的串口发送模块整体框图,如图 3 所示,其接口列表如表 1 所示。
根据功能需求,串口发送模块可进一步细化为如图 4 所示详细结构图,其中每一子模块的作用如表 2 所示。其中绿色的框代表单一结构的寄存器,来实现数据的稳定输入以及输出。
3、接口设置
module uart_byte_tx( Clk, Rst_n, Send_En, Data_Byte, Baud_Set, Rs232_Tx, Tx_Done, UART_state ); input Clk; input Rst_n; input Send_En; input [2:0]Baud_Set; input [7:0]Data_Byte; output reg Rs232_Tx; output reg Tx_Done; output reg UART_state; endmodule
4、波特率时钟生成模块设计
从原理部分可知,波特率是 UART 通信中需要设置的参数之一。在波特率时钟生成模 块中,计数器需要的计数值与波特率之间的关系如表 3 所示,其中系统时钟周期为 clk_period,这里为 20ns。如果接入到该模块的时钟频率为其他值,需要根据具体的 频率值修改该参数。
本模块的设计是为了保证模块的复用性。当需要不同的波特率时,只需设置不同的波特 率时钟计数器的计数值。使用查找表即可实现,下面的设计代码中(DR_LUT)只包含了针对 5 个波特率 的设置,如需要其他波特率可根据实际使用情况具体修改。
//波特率时钟生成模块 reg [15:0]bps_DR; //分频计数最大值 reg [15:0]div_cnt; //分频计数器 reg bps_clk; //波特率时钟 reg [3:0]bps_cnt; //波特率时钟计数器 //查找表(DR_LUT) always@(posedge Clk or negedge Rst_n) if(!Rst_n) bps_DR <= 16'd5207; else begin case(Baud_Set) 0:bps_DR <= 16'd5207; 1:bps_DR <= 16'd2603; 2:bps_DR <= 16'd1302; 3:bps_DR <= 16'd867; 4:bps_DR <= 16'd433; default:bps_DR <= 16'd5207; endcase end //分频计数器(Div_Cnt) always@(posedge Clk or negedge Rst_n) if(!Rst_n) div_cnt <= 16'd0; else if(UART_state)begin if(div_cnt == bps_DR) div_cnt <= 16'd0; else div_cnt <= div_cnt + 1'b1; end else div_cnt <= 16'd0; //产生bps_clk脉冲信号(Div_Cnt) always@(posedge Clk or negedge Rst_n) if(!Rst_n) bps_clk <= 1'b0; else if(div_cnt == 16'd1) //当计数器开始工作时,bps_clk就产生一个高脉冲,若计数器计数到bps_DR,则脉冲信号会延后一位。 bps_clk <= 1'b1; else bps_clk <= 1'b0; //波特率时钟计数器(bps_cnt) always@(posedge Clk or negedge Rst_n) if(!Rst_n) bps_cnt <= 4'd0; else if(Tx_Done) bps_cnt <= 4'd0; else if(bps_clk) bps_cnt <= bps_cnt + 1'b1; else bps_cnt <= bps_cnt; //发送结束信号生成(Tx_Done) always@(posedge Clk or negedge Rst_n) if(!Rst_n) Tx_Done <= 1'b0; else if(bps_cnt == 4'd11) Tx_Done <= 1'b1; else Tx_Done <= 1'b0;
5、数据输出模块设计
//数据输出模块 //为了保证输出稳定,设置一个内部寄存器r_data_byte = Data_Byte。这样当Send_En = 1的时候,无论data_byte怎么变化,r_data_byte的值都不变。 reg [7:0]r_data_byte; always@(posedge Clk or negedge Rst_n) if(!Rst_n) r_data_byte <= 8'd0; else if(Send_En) r_data_byte <= Data_Byte; //10选1多路器 localparam START_BIT = 1'b0; localparam STOP_BIT = 1'b1; always@(posedge Clk or negedge Rst_n) //用'*'号可能会出现毛刺,所有用时序逻辑来编写组合逻辑电路,较稳定 if(!Rst_n) Rs232_Tx <= 1'b1; //Rs232_Tx默认为1 else begin case(bps_cnt) 0:Rs232_Tx <= 1'b1; 1:Rs232_Tx <= START_BIT; 2:Rs232_Tx <= r_data_byte[0]; 3:Rs232_Tx <= r_data_byte[1]; 4:Rs232_Tx <= r_data_byte[2]; 5:Rs232_Tx <= r_data_byte[3]; 6:Rs232_Tx <= r_data_byte[4]; 7:Rs232_Tx <= r_data_byte[5]; 8:Rs232_Tx <= r_data_byte[6]; 9:Rs232_Tx <= r_data_byte[7]; 10:Rs232_Tx <= STOP_BIT; default:Rs232_Tx <= 1'b1; endcase end
6、发送状态模块
//发送状态模块(UART_state) always@(posedge Clk or negedge Rst_n) if(!Rst_n) UART_state <= 1'b0; else if(Send_En) UART_state <= 1'b1; else if(Tx_Done) UART_state <= 1'b0; else UART_state <= uart_state;
7、仿真
`timescale 1ns/1ns `define clk_period 20 module uart_byte_tx_tb; reg Clk; reg Rst_n; reg Send_En; reg [2:0]Baud_Set; reg [7:0]Data_Byte; wire Rs232_Tx; wire Tx_Done; wire UART_state; uart_byte_tx uart_byte_tx( .Clk(Clk), .Rst_n(Rst_n), .Send_En(Send_En), .Data_Byte(Data_Byte), .Baud_Set(Baud_Set), .Rs232_Tx(Rs232_Tx), .Tx_Done(Tx_Done), .UART_state(UART_state) ); initial Clk = 1'b1; always#(`clk_period/2) Clk = ~Clk; initial begin Rst_n = 1'b0; Data_Byte = 8'b0; Send_En = 1'b0; Baud_Set = 3'd4; #(`clk_period*20 + 1) //让复位信号不与时钟边沿对其,更好观察时序关系 Rst_n = 1'b1; #(`clk_period*50) Data_Byte = 8'haa; Send_En = 1'b1; #`clk_period Send_En = 1'd0; @(posedge Tx_Done) //当Tx_Done信号出现上升沿时,进行接下来的语句 #(`clk_period*5000) Data_Byte = 8'h55; Send_En = 1'b1; #`clk_period Send_En = 1'd0; @(posedge Tx_Done) #(`clk_period*5000) $stop; end endmodule
8、板级调试
为了实现导读中所设定的目标,将以前编写好的按键消抖模块添加到工程当中,并再次 使用 ISSP,其参数配置如图 5 所示,并加入到工程中。
然后,新建一个顶层文件,并将按键消抖、串口发送以及 ISSP 例化,并将按键状态与串口发送使能端连接即可。设计如下所示,并将串口发送状态(UART_state)连接到 LED 上,以更好的观察数据发送状态。
module uart_byte_tx_top( Clk, Rst_n, Rs232_Tx, key_in0, led ); input Clk; input Rst_n; input key_in0; output Rs232_Tx; output led; wire Send_En; wire [7:0]Dta_Byte; wire key_flag0; wire key_state0; assign Send_En = key_flag0 & !key_state0; //对Send_En进行按键消抖 //uart串口发送模块 uart_byte_tx uart_byte_tx( .Clk(Clk), .Rst_n(Rst_n), .Send_En(Send_En), .Data_Byte(Data_Byte), .Baud_Set(3'd4), //波特率为115200 .Rs232_Tx(Rs232_Tx), .Tx_Done(), .UART_state(led) ); //按键消抖模块 key_filter key_filter( .Clk(Clk), .Rst_n(Rst_n), .key_in(key_in0), .key_flag(key_flag0), .key_state(key_state0) ); //ISSP激励源 issp issp( .probe(), .source(Data_Byte) ); endmodule
编译无误后,点击 RTL_viewer 可以看到如图 6 的各模块连接图。
到此,程序和模块设计部分的内容完成。
9、串口发送多个字节的数据方案
目标:通过串口发送“HELLO”字符串。
在uart_tx_top文件中,去掉ISSP模块和该行代码:assign Send_En = key_flag0 & !key_state0; ,并编写如下代码:
//字符串"HELLO\0"发送模块 reg [2:0] cnt; localparam byte1 = "H", byte2 = "E", byte3 = "L", byte4 = "L", byte5 = "O", byte6 = "\0"; //计数器计数 always@(posedge Clk or negedge Rst_n) if(!Rst_n) cnt <= 3'd0; else if(Tx_Done) //D触发器的特征:当检测到Tx_Done时,cnt不会立即更新,cnt会在下一个时钟周期上升沿到来时才跟新(延迟一拍)。 cnt <= cnt + 1'b1; else if(key_flag0 & !key_state0) //key_flag0 & !key_state0表示按键按下并状态稳定,没有滞后 cnt <= 3'd0; //通过发送结果控制发送 always@(posedge Clk or negedge Rst_n) if(!Rst_n) Send_En <= 1'b0; else if(key_flag0 & !key_state0) //第一次按下按键,启动一次发送,没有滞后 Send_En <= 1'b1; else if(Tx_Done &(cnt < 3'd5)) //后面每次发送完成,Tx_Done产生一次高脉冲,启动下一次发送。最后一个字节发送完成的Tx_Done不能触发新一次的发送 Send_En <= 1'b1; //Send_En滞后一拍变成0,当第6个字节发送完成,产生Tx_Done脉冲时,此时cnt还没自加1,还是5 else Send_En <= 1'b0; //要发送的数据 always@(*) //组合逻辑 case(cnt) 3'd0:Data_Byte <= byte1; 3'd1:Data_Byte <= byte2; 3'd2:Data_Byte <= byte3; 3'd3:Data_Byte <= byte4; 3'd4:Data_Byte <= byte5; 3'd5:Data_Byte <= byte6; default:Data_Byte = 0; endcase
仿真代码如下所示:
`timescale 1ns/1ns `define clk_period 20 module uart_byte_tx_top_tb; reg Clk; reg Rst_n; wire key; wire led; wire Rs232_Tx; reg press; uart_byte_tx_top uart_byte_tx_top( .Clk(Clk), .Rst_n(Rst_n), .Rs232_Tx(Rs232_Tx), .key_in0(key), .led(led) ); key_model key_model( .press(press), .key(key) ); initial Clk = 1'b1; always#(`clk_period/2) Clk = ~Clk; initial begin Rst_n = 1'b0; press = 1'b0; #(`clk_period*20 + 1); Rst_n = 1'b1; #(`clk_period*20 + 1); press = 1'b1; #(`clk_period*20 + 1); press = 1'b0; wait(uart_byte_tx_top.Tx_Done &(uart_byte_tx_top.cnt == 3'd5)); //wait函数调用、层次的引用 #(`clk_period*200 + 1); $stop; end endmodule
在如上所示的代码中,所用到的知识点为:
1、加入一个仿真模块:key_model;
2、wait函数调用
3、层次的引用
本文作者:Yamada_Ryo
本文链接:https://www.cnblogs.com/little55/p/18095872
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步