UART简单实现
1. 各级模块调用关系
以“50MHz 9600bps”为例
2. 接收模块
// 串行接收模块
`timescale 1ns / 1ps
module uart_recv (
output reg [7:0] uart_data,
output reg uart_done,
input wire sys_clk, // 系统时钟,50M
input wire sys_rst_n, // 系统复位,低有效
input wire uart_rxd // UART接收端口
);
//
parameter CLK_FREQ = 50_000_000; // 系统时钟频率
parameter UART_BPS = 9600; // 串口波特率
parameter BPS_CNT = CLK_FREQ / UART_BPS; // 为得到指定波特率,需要对系统时钟计数BPS_CNT次
// 即每接收一个bit数据需要经过多少个系统时钟
//
reg uart_rxd_d0;
reg uart_rxd_d1;
reg [15:0] clk_cnt; // 系统时钟计数器
reg [4:0] rx_cnt; // 接收数据计数器
reg rx_flag; // 接收过程标志信号
reg [7:0] rxdata; // 接收数据寄存器
//
wire start_flag; // 接收数据起始信号
reg [15:0] bps_value;
// 捕获接受端口下降沿(起始位),得到一个时钟周期的脉冲信号
// 按键消抖模块也用到了类似的方法检测沿的变化
assign start_flag = uart_rxd_d1 & (~uart_rxd_d0);
// 对UART接收端口的数据延迟两个时钟周期
// 当数据经过uart_rxd,传送到uart_rxd_d0,再传送到uart_rxd_d1,经过了两层寄存器,想要捕获到下降沿,就需要分析当前d1寄存器的值
// 可以认为d1寄存器捕获到了下降沿,一切都以d1寄存器的值为当前需要进行分析处理的数据,起始位、数据位、校验位、结束位
always @ (posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_rxd_d0 <= 1'b0;
uart_rxd_d1 <= 1'b0;
bps_value <= 16'd0;
end else begin
uart_rxd_d0 <= uart_rxd;
uart_rxd_d1 <= uart_rxd_d0;
bps_value <= CLK_FREQ / UART_BPS;
end
end
// 当脉冲信号start_flag到达时,进入接收过程
always @ (posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
rx_flag <= 1'b0;
end else begin
if (start_flag) begin // 检测到了起始位
rx_flag <= 1'b1; // 进入接收过程,接收过程标志信号rx_flag拉高
end else if ( (rx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2) ) begin
rx_flag <= 1'b0; // 计数到停止位的数据采样时刻,停止接受过程
end else begin
rx_flag <= rx_flag;
end
end
end
// 进入接收过程后,启动系统时钟计数器和接收数据计数器
always @ (posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
clk_cnt <= 16'd0;
rx_cnt <= 4'd0;
end else if (rx_flag) begin // 处于接收过程
if (clk_cnt < BPS_CNT - 1) begin // 接收一个bit需要BPS_CNT = CLK_FREQ / UART_BPS个系统时钟
clk_cnt <= clk_cnt + 1'b1;
rx_cnt <= rx_cnt;
end else begin
clk_cnt <= 16'd0; // 系统时钟计数达一个波特率周期后清零
rx_cnt <= rx_cnt + 1'b1; // 此时接收数据计数器加1
end
end else begin
clk_cnt <= 16'd0;
rx_cnt <= 4'd0;
end
end
// 根据接收数据计数器来寄存uart接收端口数据
// 接收1bit需要BPS_CNT个系统时钟,在中间位置BPS_CNT / 2处进行数据采样
always @ (posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
rxdata <= 8'b0;
end else if (rx_flag) begin // 系统处于接收过程
if (clk_cnt == BPS_CNT / 2) begin // 判断系统时钟计数器是否到达采样点
case (rx_cnt)
4'd1: rxdata[0] <= uart_rxd_d1; // 寄存数据最低位
4'd2: rxdata[1] <= uart_rxd_d1;
4'd3: rxdata[2] <= uart_rxd_d1;
4'd4: rxdata[3] <= uart_rxd_d1;
4'd5: rxdata[4] <= uart_rxd_d1;
4'd6: rxdata[5] <= uart_rxd_d1;
4'd7: rxdata[6] <= uart_rxd_d1;
4'd8: rxdata[7] <= uart_rxd_d1; // 寄存数据最高位
default: ;
endcase
end else begin
rxdata <= rxdata;
end
end else begin
rxdata <= 8'b0;
end
end
// 数据接收完毕后给出标志信号并寄存输出接收到的数据
// 当寄存完8bit数据后,rx_cnt加1,进入接收结束位的波特率周期,因为结束位固定是1,可以不用等到采样数据,直接完成接收过程
always @ (posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_data <= 8'b0;
uart_done <= 1'b0;
end else if (rx_cnt == 4'd9) begin // 接收数据计数器计数到停止位时
uart_data <= rxdata; // 寄存输出接收到的8bit数据
uart_done <= 1'b1; // 并将接收完成标志信号位拉高
end else begin
uart_data <= 8'b0;
uart_done <= 1'b0;
end
end
endmodule
3. 发送模块
// uart_send发送模块
// 接收模块接收完成uart_done发出高脉冲,连接发送模块的发送使能信号uart_en,所以uart_en默认0,检测到上升沿即可认为一字节数据接收完毕,可以传回
`timescale 1ns / 1ps
module uart_send (
output reg uart_txd, // UART发送端口
input wire sys_clk, // 系统时钟
input wire sys_rst_n, // 系统复位,低有效
input wire uart_en, // 发送使能信号
input wire [7:0] uart_din // 带待发送数据
);
//
parameter CLK_FREQ = 50_000_000; // 系统时钟频率
parameter UART_BPS = 9600; // 串口波特率
parameter BPS_CNT = CLK_FREQ / UART_BPS; // 为得到指定波特率,需要对系统时钟计数BPS_CNT次
// 即每接收一个bit数据需要经过多少个系统时钟
//
reg uart_en_d0;
reg uart_en_d1;
reg [15:0] clk_cnt; // 系统时钟计数器
reg [3:0] tx_cnt; // 发送数据计数器
reg tx_flag; // 发送过程标志信号
reg [7:0] tx_data; // 寄存发送数据
//
wire en_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'b1;
end else begin
uart_en_d0 <= uart_en;
uart_en_d1 <= uart_en_d0;
end
end
// 当脉冲en_flag到达时,寄存待发送的数据,并进入发送过程
// en_flag是wire型数据,使能后tx_flag发送过程标志信号被拉高,在发送过程中依据该信号发送数据,直到发送完停止位,发送标志信号被拉低,结束发送
always @ (posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
tx_flag <= 1'b0;
tx_data <= 8'b0;
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 / 2) ) begin // 计数到停止位的数据采样时刻,停止发送过程
tx_flag <= 1'b0; // 发送过程结束,标志位tx_flag拉低
tx_data <= 8'b0;
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) begin
clk_cnt <= 16'd0;
tx_cnt <= 4'd0;
end else if (tx_flag) begin // 处于发送过程
if (clk_cnt < BPS_CNT - 1) begin // 发送一个bit需要BPS_CNT = CLK_FREQ / UART_BPS个系统时钟
clk_cnt <= clk_cnt + 1;
tx_cnt <= tx_cnt;
end else begin
clk_cnt <= 16'd0; // 系统时钟计数达一个波特率周期后清零
tx_cnt <= tx_cnt + 1; // 发送数据计数器加1
end
end else begin // 发送过程结束
clk_cnt <= 16'd0;
tx_cnt <= 4'd0;
end
end
// 根据发送数据计数器来给uart发送端口赋值
always @ (posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_txd <= 1'b1;
end else if (tx_flag) 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 else begin
uart_txd <= 1'b1; // 空闲时发送端口为高电平
end
end
endmodule
4. 顶层模块
`timescale 1ns / 1ps
module top_uart (
output wire uart_txd, // 发送端口
input wire sys_clk,
input wire sys_rst_n,
input wire uart_rxd // 接收端口
);
parameter CLK_FREQ = 50_000_000;
parameter UART_BPS = 9600;
parameter CNT_BPS = CLK_FREQ / UART_BPS;
wire uart_en;
wire [7:0] uart_data;
uart_recv #(
.CLK_FREQ (CLK_FREQ),
.UART_BPS (UART_BPS)
)
uart_recv0 (
.uart_data(uart_data),
.uart_done(uart_en),
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.uart_rxd(uart_rxd)
);
uart_send #(
.CLK_FREQ (CLK_FREQ),
.UART_BPS (UART_BPS)
)
uart_send0 (
.uart_txd(uart_txd),
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.uart_en(uart_en),
.uart_din(uart_data)
);
endmodule
5. 测试
`timescale 1ns / 1ps
module sim_uart ();
wire uart_txd; // 发送端口
reg sys_clk; // 系统时钟
reg sys_rst_n; // 系统复位,低有效
reg uart_rxd; // 接收端口
top_uart top_uart0 (
.uart_txd(uart_txd),
.sys_clk(sys_clk),
.sys_rst_n(sys_rst_n),
.uart_rxd(uart_rxd)
);
initial begin
sys_clk = 0;
sys_rst_n = 0;
uart_rxd = 0;
end
// 产生50MHz的系统时钟
parameter clk_period = 20;
always #(clk_period / 2) sys_clk = ~sys_clk;
initial begin
#20
sys_rst_n = 1;
uart_rxd = 1;
#100560
sys_rst_n = 1;
uart_rxd = 0; //起始位:-1-0-
#100560 //传输11001011 (倒序)
sys_rst_n = 1;
uart_rxd = 1;
#100560
sys_rst_n = 1;
uart_rxd = 1;
#100560
sys_rst_n = 1;
uart_rxd = 0;
#100560
sys_rst_n = 1;
uart_rxd = 1;
#100560
sys_rst_n = 1;
uart_rxd = 0;
#100560
sys_rst_n = 1;
uart_rxd = 0;
#100560
sys_rst_n = 1;
uart_rxd = 1;
#100560
sys_rst_n = 1;
uart_rxd = 1;
#100560 //结束位 -0-1-
sys_rst_n = 1;
uart_rxd = 0;
#100560
sys_rst_n = 1;
uart_rxd = 1;
#1100000 //复位
sys_rst_n = 0;
uart_rxd = 0;
end
endmodule
按照仿真的输入,0(起始位) 1101_0011
输出的顺序, 0(起始位) 1101_0011 1(停止位)
6. 问题分析与总结
6.1. 测试延时#100560计算
parameter CLK_FREQ = 50_000_000;
parameter UART_BPS = 9600;
parameter CNT_BPS = CLK_FREQ / UART_BPS;
系统时钟频率为50MHz,波特率为9600,CNT_BPS即“50_000_000 / 9600 ≈ 5208”,即每接收1bit数据需要经过5208个clk,然后在这5208个的中间部分,也就是BPS_CNT / 2的位置进行当前数据的数据采样
由于50MHz的时钟周期为20ns,所以控制延时为5208 * 20ns = 100560ns
说明:这里的5208 * 20ns = 104160ns,上面计口算失败,但是问题不大,重新加上波形
数据采样每次都在BPS_CNT / 2位置,也就是BPS_CNT个clk中间位置(这个位置是固定的),在进行rxd_data数据输入的可以允许一定幅度的延时偏差,这种偏差会累积,越往后,每一次的数据采样越来越靠近rxd_data持续范围的偏后部分。
下面两张图中红色光标的时间差可以说明这个道理,数据更新有快有慢,一定范围内都可以取到正确的数
-
延时#104160,rxd数据更新适中
-
延时#100560,数据更新过快
6.2. 扩展“100MHz 9600bps”
更改各个模块参数定义,修改测试模块100MHz系统时钟的产生
延时计算:
100_000_000 / 9600 ≈ 10417
10417 * 10ns = 104170ns
6.3. 补充
从波形可以看到,输出的顺序和输入一样,都是1101_0011,接收模块采集到数据后从最低位开始寄存,传送到发送模块后,也是从最低位[0]开始发送