UART串口简介
串行通信分为两种方式:同步串行通信和异步串行通信。同步串行通信要求通信双方使用同一时钟,异步则没有这个要求。UART是一种采用异步串行通信方式的通用异步收发传输器(universal asynchronous receiver-transmitter),它在发送数据时将接收到的串行数据转成并行数据。
通信时序
如上图所示,一帧数据由起始位(低电平),数据位(5/6/7/8),校验位(奇/偶),停止位(高电平1/1.5/2)构成,数据传输的速率用波特率(每秒传输的比特数bps)来表示。
在以往的单片机开发中,串口往往作为一种外设集成进单片机中,我们只需要通过配置寄存器来确定通信的参数即可。当然,也可以通过软件模拟通信协议。而这里,我们通过FPGA来实现纯数字电路上的串口通信,这里的任务即上位机通过串口发送数据给FPGA,然后FPGA实现回传。
顶层模块设计
上图为顶层模块的设计框图,系统时钟和复位信号不必多说,顶层模块有一个串口的接收信号(传给串口接收子模块)和一个串口的发送信号(传给串口发送子模块)。
在顶层模块中,例化了串口串口接收子模块和串口发送子模块,分别用来处理串口接收到的数据和串口需要发送的数据。
//uart串口收发程序,顶层模块
module uart_top (
input sys_clk,
input sys_rst_n,
input uart_rxd, //FPGA串口接收端
output uart_txd //FPGA串口发送端
);
parameter CLK_FREQ = 50000000;
parameter UART_BPS = 115200;
//模块之间的交互一定要用线网变量
wire [7:0] uart_data;
wire uart_done;
//例化串口接收子模块
uart_recv #(
.CLK_FREQ (CLK_FREQ),
.UART_BPS (UART_BPS)
)u_uart_recv(
.sys_clk (sys_clk),
.sys_rst_n (sys_rst_n),
.uart_rxd (uart_rxd), //接收端口的信号
.uart_done (uart_done), //接收完成
.uart_data (uart_data) //接收的数据,并行
);
//例化串口发送子模块
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_done), //发送使能信号
.uart_data (uart_data), //需要发送的串口数据,并行
.uart_txd (uart_txd), //串口发送端
);
endmodule
串口接收子模块设计
时钟信号、复位信号、串口接收信号均从顶层模块引入
通过信号线上的下降沿确定起始位,开始接收(下降沿通过两个变量的阻塞赋值实现)
开始接收后,需要对时钟数进行计数,到达规定时钟数进入下一个bit的接收,此时bit数也要+1
每一次在时钟数的中间在信号线上进行采样,注意这里的时钟均指一个bit内的时钟
采用一个寄存器专门存放接收到的数据,即串转并
接收到第9个比特(停止位)时跳出,认为接受完成,并输出接收完成信号
//串口接收子模块
module uart_recv (
input sys_clk,
input sys_rst_n,
input uart_rxd, //接收端口的信号
output reg uart_done, //接收完成
output reg[7:0] uart_data //接收的数据,并行
);
parameter CLK_FREQ = 50000000; //时钟频率,50M
parameter UART_BPS = 115200; //波特率,bit/s
parameter BPS_CNT = CLK_FREQ/UART_BPS; //每个bit传输需要的时钟数
reg uart_rxd_d0;
reg uart_rxd_d1;
reg [15:0] clk_cnt; //时钟数,最大到BPS_CNT
reg [3:0] rx_cnt; //接收数据bit的个数,最大到9
reg [7:0] rxdata; //接收的数据寄存器
reg rx_flag; //接收数据的标志位
wire start_flag; //起始位开始的标志位
//起始位触发(捕获下降沿),d1延后d0一个周期,所以当d0为0,d1为1,意味着下降沿
assign start_flag = uart_rxd_d1 & (~uart_rxd_d0);
//d1和d0都是数据线上信号,保证d1延后d0一个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n) begin //复位时数据信号清0
uart_rxd_d0 <= 1'b0;
uart_rxd_d1 <= 1'b0;
end
else begin
uart_rxd_d0 <= uart_rxd;
uart_rxd_d1 <= uart_rxd_d0;
end
end
//确立信号接收标志位
always @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n)
rx_flag <= 1'd0;
else begin
if (start_flag)
rx_flag <= 1'd1;
else if ((rx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2)) //判断停止位,在第9个bit的中间时钟处
rx_flag <= 1'd0;
else
rx_flag <= rx_flag;
end
end
//对时钟进行计数(最大到BPS_CNT),对接收的bit数进行计数,
//根据这两个数判断停止位
always @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n) begin //复位时计数都清零
rx_cnt <= 4'd0;
clk_cnt <= 16'd0;
end
else if (rx_flag) begin //开始数据传输
if (clk_cnt < BPS_CNT)
clk_cnt <= clk_cnt + 1'b1;
else begin //经过了一个bit的时钟数
clk_cnt <= 1'b0; //时钟数清零
rx_cnt <= rx_cnt + 1'b1; //bit数加1,1bit即从第一个数据位开始,包含了起始位
end
end
else begin
clk_cnt <= 16'd0;
rx_cnt <= 4'd0;
end
end
//寄存接收数据
always @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n)
rxdata <= 8'd0;
else if (rx_flag)
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
rxdata <= rxdata;
else
rxdata <= 8'd0;
end
//判断接收数据是否完成以及把数据赋给输出信号
always @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n) begin
uart_done <= 1'd0;
uart_data <= 8'd0;
end
else if (rx_cnt == 4'd9) begin
uart_data <= rxdata;
uart_done <= 1'd1;
end
else begin
uart_data <= 8'd0;
uart_done <= 1'd0;
end
end
endmodule
串口发送子模块设计
时钟信号、复位信号来自顶层模块,需要发送的数据(并行)以及发送使能信号来自串口接收子模块
找到发送使能信号的上升沿,确立起始位,开始发送
发送的时候同样需要对时钟进行计数,达到规定时钟数后进入下一个bit,当然也需要对bit进行计数
发送时根据当前bit数确定发送数据的哪一位,然后对应拉高或者拉低Tx信号线,这里需要注意,起始位和停止位都需要发送
发送完9个比特后拉高信号线
//串口发送子模块
module uart_send(
input sys_clk,
input sys_rst_n,
input uart_en, //发送使能信号
input [7:0] uart_data, //需要发送的串口数据,并行
output reg uart_txd //串口发送端
);
parameter CLK_FREQ = 50000000; //时钟频率,50M
parameter UART_BPS = 115200; //波特率,bit/s
parameter BPS_CNT = CLK_FREQ/UART_BPS; //每个bit传输需要的时钟数
reg uart_en_d0;
reg uart_en_d1;
reg [15:0] clk_cnt; //时钟计数寄存器
reg [3:0] tx_cnt; //发送bit计数寄存器
reg tx_flag; //发送标志位
reg [7:0] tx_data; //寄存发送数据
wire en_flag; //发送使能位
//找到发送使能信号的上升沿,将发送使能位置位
assign en_flag = uart_en_d0 & (~uart_en_d1);
//确定d0和d1,d1滞后d0一个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n) begin
uart_en_d0 <= 1'd0;
uart_en_d1 <= 1'd0;
end
else begin
uart_en_d0 <= uart_en;
uart_en_d1 <= uart_en_d0;
end
end
//确定发送标志位
always @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n)
tx_flag <= 1'd0;
else if (en_flag) begin
tx_flag <= 1'd1;
tx_data <= uart_data;
end
else begin
if ((tx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2)) begin//已经发送了9比特数据,则发送结束,标志位清零
tx_flag <= 1'd0;
tx_data <= 8'd0;
end
else begin
tx_flag <= tx_flag;
tx_data <= tx_data;
end
end
end
//对时钟和发送bit进行计数
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'd1)
clk_cnt <= clk_cnt + 1'd1;
else begin
clk_cnt <= 16'd0;
tx_cnt <= tx_cnt + 1'd1;
end
end
end
//并转串,将数据放到串口发送信号线上
always @(posedge sys_clk or negedge sys_rst_n) begin
if (~sys_rst_n)
uart_txd <= 1'd1;
else if (tx_flag)
case (tx_cnt)
4'd0: uart_txd <= 1'd0;
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'd1;
default:;
endcase
else
uart_txd <= 1'd1;
end
endmodule
结果
直接放抓到的波形图:
时序图很漂亮,接收子模块通过下降沿触发后开始接收数据,接收完后发出完成信号,发送子模块根据这个完成信号的上升沿,发送数据。
在写FPGA的程序时,跟以往的编程差别非常之大,Verilog的并行执行要求写程序时拥有更加严谨的逻辑。现在还处于FPGA的入门阶段,只能隐隐约约地从代码背后感受到一点数字电路的轮廓。
时隔一年多再次回到了硬件编程,需求也今非昔比了,现在的目标是将通信算法移植到硬件平台上,任重而道远,慢慢积累吧!
Ref:
《正点原子新起点FPGA教程》
评论(0)
您还未登录,请登录后发表或查看评论