UART 在发送或接收过程中的一帧数据由4部分组成,起始位、数据位、奇偶校验位和停止位,如图所示。其中,起始位标志着一帧数据的开始,停止位标志着一帧数据的结束,数据位是一帧数据中的有效数据。
通常用的串口数据帧格式是:8位数据位,无校验位,1位停止位。
所以一帧数据有10个bit:1bit起始位,8bit数据位,1bit停止位。
串口波特率是指串口通信的速率,它表示每秒传输二进制数据的位数,单位是bps(位 /秒),常用的波特率有9600、19200、38400、57600以及115200等。
FPGA如何通过系统时钟来得到串口波特率呢?
已知串口时钟为50Mhz = 50000000hz,生成串口波特率为9600。
那么我们需要在FPGA里构建两个计数器。一个系统时钟计数器用来计数系统时钟周期数,一个串口时钟计数器,用来计数对应串口波特率的时钟周期数。
系统时钟:sys_clk,系统时钟计数值:sys_cnt。
串口时钟计数值:tx_cnt。
当sys_cnt = 50000000 / 9600时,tx_cnt计数一次,相当于串口时钟的一个周期。
数据是怎么发送出去的呢?
由上一个问题的解答已经得到了【串口时钟】这么一个东西,那么发送数据都是在该时钟下进行发送。
按照前面所提到的数据格式【1bit起始位,8bit数据位,1bit停止位】,那么就可以知道,发送一个8位的数据,需要在10个串口时钟周期下,将10bit的数据对应好时序一个个发出去。
数据是怎么接收的呢?
类似于上个问题,只需要按照时序,检测起始位,接收数据,再检测停止位即可。
那么这样是否就能够搭建好串口模块呢?
答案明显是不能的,FPGA并不知道什么时候该发数据,什么时候该收数据,什么时候进入发送状态,什么时候脱离发送状态。这时候还要添加其他信号来打辅助。比如添加发送使能信号来确定什么时候发数据,添加状态信号来表示串口是空闲还是忙碌等一系列的状态。
module uart_send(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
//由其他模块输入
input uart_en, //发送使能信号
input [7:0] uart_din, //待发送数据
//输出给其他模块
output uart_tx_busy, //发送忙状态标志
output reg tx_flag, //发送过程标志信号
output reg [ 7:0] tx_data, //寄存发送数据
output reg [ 3:0] tx_cnt, //发送数据计数器
output reg uart_txd //UART发送端口,即tx引脚
);
//parameter define
parameter CLK_FREQ = 50000000; //系统时钟频率
parameter UART_BPS = 9600; //串口波特率
//为得到指定波特率,对系统时钟计数BPS_CNT次
localparam BPS_CNT = CLK_FREQ/UART_BPS;
//reg define
reg uart_en_d0;
reg uart_en_d1;
reg [15:0] clk_cnt; //系统时钟计数器
//wire define
wire en_flag;
在该子模块中的参数可分为三个部分:
根据这些参数,可以大致知道串口发送1个bit数据的流程:
//捕获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
//计数到停止位结束时,停止发送过程(并提前1/16个串口时钟)
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
子模块写好后,可以再做一个顶层模块,用来发送字符串。
发送字符串的思路:
当满足某个条件后,使能串口发送信号,此时串口就开始发送数据了,当在串口发送数据的时候,串口就处在发送忙状态,此时我们就可以更新一次待发送的数据。为什么可以在发送的时候更新待发送数据呢?因为在进入发送过程后,数据还没发送前的时间里,待发送的数据就已经寄存在了子模块里的一个寄存器里了。这样就节约了数据更新的时间了。
相关代码如下:
module uart_loopback_top(
input sys_clk, //外部50M时钟
input sys_rst_n, //外部复位信号,低有效
output uart_txd //UART发送端口
);
//parameter define
parameter CLK_FREQ = 50000000; //定义系统时钟频率
parameter UART_BPS = 115200; //定义串口波特率
//wire define
reg [7:0] uart_send_data; //UART发送数据
wire uart_tx_busy; //UART发送忙状态标志
reg uart_send_en; //UART发送使能
reg [31:0] data_cnt;
reg send_d0;
reg send_d1;
wire send_en;
reg string_end; //停止字符串发送
reg [ 31:0] Data_Count; //字符计数器
parameter [31:0] Data_Len=32'd10; //字符串的长度
reg [7:0] arry [Data_Len-1:0]; //定义要发送的字符串
//初始化字符串
initial begin
arry[0] = "H";
arry[1] = "e";
arry[2] = "l";
arry[3] = "l";
arry[4] = "o";
arry[5] = "W";
arry[6] = "o";
arry[7] = "r";
arry[8] = "l";
arry[9] = "d";
end
//使能数据更新位,获得串口数据更新的脉冲
assign send_en = (~send_d1) & send_d0;
//数据更新标志,寄存两次uart_send_en的数据,为构成发送使能脉冲做准备
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
send_d0 <= 1'b0;
send_d1 <= 1'b0;
end
else begin
send_d0 <= uart_send_en;
send_d1 <= send_d0;
end
end
//串口发送字符串
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_send_en <= 1'd0; //初始化串口发送使能
data_cnt <= 32'd0; //初始化数据计数器
uart_send_data <= arry[0]; //初始化首字符
string_end <= 1'd0; //控制字符串发送结束
end
else begin
//当串口处于空闲状态,且数据计数器小于10的时候
//条件:data_cnt < Data_Len 不要也可以
//条件必须满足TX空闲,字符串发送结束标志位0,可在此基础上添加其它条件
if((~uart_tx_busy) && (data_cnt < Data_Len) && (string_end == 1'b0)) begin
//使能串口发送使能寄存器
uart_send_en <= 1'b1;
end
//当数据计数器大于等于字符串长度时
if(data_cnt == Data_Len) begin
string_end <= 1'b1;
uart_send_en <= 1'b0; //失能串口发送使能,停止串口发送数据
data_cnt <= 32'b0;
end
//如果数据发送使能脉冲到来
else if((send_en) && (data_cnt < Data_Len)) begin
data_cnt <= data_cnt + 32'b1; //则数据计数器加一
end
//如果串口有数据在发送,并且结束位没有拉高
else if((uart_tx_busy) && (string_end == 1'b0)) begin
//则失能串口发送使能,为下一个数据的发送做准备
uart_send_en <= 1'b0;
uart_send_data <= arry[data_cnt];
end
end
end
//串口发送模块
uart_send #(
.CLK_FREQ (CLK_FREQ), //设置系统时钟频率
.UART_BPS (UART_BPS)) //设置串口发送波特率
u_uart_send(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.uart_en (uart_send_en),
.uart_din (uart_send_data),
.uart_tx_busy (uart_tx_busy),
.uart_txd (uart_txd)
);
//例化ILA IP核
//ila_0 your_instance_name (
// .clk(sys_clk), // input wire clk
//
// .probe0(uart_send_en), // input wire [0:0] probe0
// .probe1(data_cnt), // input wire [7:0] probe1
// .probe2(uart_tx_busy), // input wire [0:0] probe2
// .probe3(send_d0),
// .probe4(send_d1),
// .probe5(send_en),
// .probe6(uart_send_data),
// .probe7(string_end)
//);
endmodule
如果要发送多个字符串,可以在更新字符串数据时加入状态机,进行不同字符串的转换。