UART即通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),相信常和硬件打交道的同学都不会陌生,在学习过程中最常见的就是使用在开发板和PC或另一台开发板的通信。作为一种简单的通信协议,UART在实际使用中非常广泛。如果是学习Verilog HDL或者FPGA的新手,UART也是一个必不可少的入门例程。
这里本人对UART进行一次简单的实现,并且加入了LED和拨码开关作为外设实现对UART收发控制,可以直接在FPGA开发板上观察实验结果。因为这里讲解的定位是入门,实现最基础的功能,如果对各位小伙伴有帮助,还请赏个脸点个赞,如有不足,欢迎评论指出ଘ(੭ˊᵕˋ)੭
既然前面说到UART是一种非常简单的通信传输协议,那我们这里就以此为设计目标,由目标去引导设计流程,循序渐进,一直到最后的实现。
假设我们希望两台设备之间进行通信,需要一次传输一个byte,也就是8-bit,那么利用8跟线一次将数据全部发送当然是最简单不过的,但实际上由于我们对数据传输的速率要求并不高(本实验中默认波特率115200),而在实际情况下芯片的IO数量是非常稀缺的资源,为了节省IO,像这样的低速接口普遍采用串行传输的方式将8-bit数据放在一根线上依次发送(如UART/IIC/SPI)以此来选择牺牲速度去节省IO资源(但实际上有些高速接口也用串行传输如SERDES,但完全是两种东西这里不讲了)。
好了这里我们就妥协一下,选择串行的方式,将8-bit数据依次放在一根线上,一个一个依次发送好了。既然如此,如果要实现两台设备间的数据收发,首先想到的就是一根线发送,一根线接收,另一台设备也是如此,让发送对接收,接收对发送。我们最常见的UART模块设备通常就是这样使用两条线实现通信,分别是用来发送数据的TXD,和用来接收数据的RXD(实际上一根线也可以进行数据收发,如IIC的SDA)。
前面我们打算将8-bit数据依次放在一根线上面进行发送,但紧接着我们不得不面临两个问题:
而UART就为了解决各种问题而制定了一系列规则。其中面对问题1,协议指明了单个电平需要持续的时间,让接收方知道发送的是1个bit,而不是2个或更多,这个时间对应的频率就是比特率/bps(由于一个符号用1-bit表示,所以也可以叫波特率)。面对问题2,协议对将要传输的8-bit开头加1-bit起始位,末尾加1-bit停止位,也就是将数据封装为一帧。在没有数据需要发送时,TXD处于闲置状态,默认为高电平,当有数据需要发送时,先传输1-bit低电平,这样接收方由闲置状态感知到电平变化,明白要开始传输数据了,这1-bit就是起始位。起始位发送完毕后,紧接着是8-bit数据位依次发送,这里采用的是LSB FIRST,也就是并串转换时先发送低位,最后发送高位。8-bit数据全部发送完毕后,紧接着发送1-bit高电平,将TXD重新拉回闲置状态,这一bit就是停止位。这样这两个问题是不是就解决了呢,我们赶紧动手写代码去试一下~
动手开始写代码之前,我们再回顾一下前面的内容,先把图纸画好。
设计如上图所示的模块结构。顶层模块FPGA_Top实现UART模块的例化,并连接用户接口SW和LED。需要发送数据时,transmit模块获取波特率脉冲,并以此将SW的8-bit数据封装为一帧后通过TXD依次发送出来。在receive模块检测到RXD从空闲的高电平状态拉低,也就是检测到起始位,则开始按波特率脉冲对RXD进行采样,完成串并转换后传入LED显示。
为了实现UART的功能,我们首先要考虑的就是,如何将一个8-bit的数据放在TXD上发送出去。前面已经讲到,为了让接收方能够正确识别,需要对数据约定波特率,并封帧发送。波特率就是利用时钟分频得到一个目标频率的脉冲,封帧就是在数据首位各加1-bit,接下来按位依次发送。这样再看,逻辑是不是就清晰许多了呢。
为了得到固定频率的脉冲,我们设计一个模块进行时钟分频;为了实现数据的最终发送,我们设计一个模块进行封帧和并串转换。接收部分亦然。
更进一步,当需要通过UART发送数据时,transmit模块会请求波特率发生器模块开始工作,紧接着产生所需频率的脉冲,transmit模块对要发送的8-bit数据进行封帧,紧接着进行并串转换,完成数据的发送。而当外界向UART发送数据时,会将处于高电平闲置的RXD拉低,一旦receive检测到RXD从闲置状态拉低,则判定当前为起始位,紧接着请求波特率发生器模块按所需频率产生采样脉冲,将数据按脉冲一一接收,完毕后掐头去尾,得到所需要的8-bit数据。
功能和接口
波特率发生器模块负则输出一个固定频率的脉冲,要设计这样的时序逻辑电路,时钟和复位当然必不可少。为了使输出的脉冲相位可控,我们添加一个en信号,当en信号使能时,模块才开始工作。为了使输出的脉冲频率可调,我们添加一个分频控制信号prescale。
设计思路
波特率发生器模块产生一个需要频率的脉冲,类似于分频器,实际上其逻辑就是用计数器对输入参考时钟进行计数,当计到N时,这段时间对应的频率就是所需要的采样脉冲频率。
其中,
N = 参 考 时 钟 频 率 f r e f / 目 标 波 特 率 f B N=参考时钟频率f_{ref}/目标波特率f_B N=参考时钟频率fref/目标波特率fB
比如参考时钟为板载晶振50MHz,目标波特率为115200B,则N为50,000,000/115200=434
代码实现
module baud_gen
#(
parameter simPresent = 0
)(
input clk,
input rst_n,
input [15:0] prescale, // 分频
input baud_en, // 使能
output baud_pulse // 采样脉冲
);
reg [15:0] cnt; // 计数器
reg baud_pulse_r; // 采样脉冲
wire rst_flag; // 计数器复位标志
wire sample_flag; // 计数器采样标志
assign rst_flag = (cnt==(prescale-1));
// 这里以cnt计到7时,也就是每8个clk输出一个采样脉冲,因此注意prescale不要小于8
assign sample_flag = (cnt==7);
assign baud_pulse = baud_pulse_r;
always @(posedge clk, negedge rst_n) begin
if(!rst_n) cnt <= 0;
else if(rst_flag) cnt <= 0;
else if(baud_en) cnt <= cnt + 1;
else cnt <= 0;
end
always @(posedge clk, negedge rst_n) begin
if(!rst_n) baud_pulse_r <= 0;
else baud_pulse_r <= sample_flag;
end
endmodule
功能和接口
发送模块需要实现并串转换,就是将8-bit作为输入数据接口,1-bit作为输出数据接口。此外还需要发送标志,来告诉模块当前的8-bit数据有效可以开始处理发送;模块进入发送状态时,会输出en信号告知波特率发生器开始工作;波特率发生器会产生采样脉冲标志,告诉发送模块可以发送下一bit;最后输出done信号告知外界本次8-bit数据发送完毕。
设计思路
发送模块的核心思想其实就是移位寄存器实现并串转换。但是考虑到串口发送的流程中,涉及到数据封帧以及等待采样脉冲,因此这里为了使整个流程更于直观,使用状态机的方式进行设计。
i)IDLE闲置状态下,当发送标志信号tx_flag有效时,通知transmit模块开始工作,状态机首先进入SYNC同步状态,等待波特率发生器的首个采样脉冲,紧接着将baud_en使能信号拉高,通知波特率发生器开始工作。
ii)SYNC同步状态下,当获取到波特率发生器的首个采样脉冲,transmit模块进入START起始位发送状态。与此同时移位寄存器置为{din, 1’b0},最低位为0,作为输出到TXD的起始位。
iii)START起始位发送状态下,当获取到采样脉冲,transmit模块进入DATA数据位发送状态。与此同时移位寄存器data_shift最低位置为din[0],将8-bit数据最低位输出到TXD。
iv)DATA数据位发送状态下,当获取到采样脉冲,计数器cnt按脉冲从0计到7共8次。与此同时每个采样脉冲下移位寄存器data_shift向右移位,依次将8-bit数据通过TXD发送。
v)DATA数据位发送状态下,当cnt已经计到7并且获取到采样脉冲,transmit模块进入STOP停止位发送状态。与此同时移位寄存器data_shift将1移到最低为,作为TXD最后的停止位。
vi)STOP停止位发送状态下,当获取到采样脉冲,表明停止位发送完毕,transmit模块重新进入IDLE状态,本次8-bit传输到此结束。
代码实现
module transmit(
input clk,
input rst_n,
input [7:0] din, // 8-bit数据输入
input tx_flag, // 发送标志
input sample_flag, // 采样标志
output baud_en, // 使能波特率发生器
output txd, // UART_TXD
output done // 发送完毕
);
reg [8:0] data_shift; // 移位寄存器
reg [3:0] cnt; // 计数器
reg [3:0] state; // 状态机
reg [3:0] state_next;
localparam ST_IDLE = 0; // 闲置状态
localparam ST_SYNC = 1; // 同步,等待波特率采样脉冲
localparam ST_START = 2; // 起始位
localparam ST_DATA = 3; // 数据位
localparam ST_STOP = 4; // 停止位
always @(posedge clk, negedge rst_n) begin
if(!rst_n) state <= ST_IDLE;
else state <= state_next;
end
always @(*) begin
if(!rst_n) state_next = ST_IDLE;
else begin
case(state)
// 闲置状态下,等待tx_flag发送标志信号,进入同步状态
ST_IDLE: begin
if(tx_flag) state_next = ST_SYNC;
else state_next = state;
end
// 同步状态下,等待sample_flag采样标志信号,进入起始位发送状态
ST_SYNC: begin
if(sample_flag) state_next = ST_START;
else state_next = state;
end
// 起始位发送状态下,等待sample_flag采样标志信号,进入数据位发送状态
ST_START: begin
if(sample_flag) state_next = ST_DATA;
else state_next = state;
end
// 数据位发送状态下,等待计数器从0数到7共8-bit发送完毕,进入停止位发送状态
ST_DATA: begin
if((cnt==7)&sample_flag)
state_next = ST_STOP;
else state_next = state;
end
// 停止位发送状态下,等待sample_flag采样标志信号,结束本次发送,进入闲置状态
ST_STOP: begin
if(sample_flag) state_next = ST_IDLE;
else state_next = state;
end
default: state_next <= ST_IDLE;
endcase
end
end
// 计数器,在ST_DATA状态下数采样脉冲sample_flag
always @(posedge clk, negedge rst_n) begin
if(!rst_n) cnt <= 0;
else if(state!=ST_DATA) cnt <= 0;
else if(sample_flag) cnt <= cnt + 1;
end
// 移位寄存器,实现并串转换
always @(posedge clk, negedge rst_n) begin
if(!rst_n) data_shift <= 9'b1_1111_1111;
else if(state==ST_IDLE) data_shift <= 9'b1_1111_1111;
else if((state==ST_SYNC)&sample_flag)
data_shift <= {din, 1'b0};
else if(sample_flag) data_shift <= {1'b1, data_shift[8:1]};
end
assign txd = data_shift[0];
assign baud_en = (state!=ST_IDLE); // 只要不在闲置状态,就令波特率发生器开始工作
assign done = (state==ST_IDLE); // 如果处于闲置状态,表明发送完毕
endmodule
module receive(
input clk,
input rst_n,
input rxd, // UART_RXD
output [7:0] rx_data, // 8-bit数据输出
input sample_flag, // 采样标志
output baud_en, // 使能波特率发生器
output valid // 接收完毕,数据有效标志
);
reg [7:0] data_shift; // 移位寄存器
reg [3:0] cnt; // 计数器
reg [3:0] state; // 状态机
reg [3:0] state_next;
reg [7:0] rx_data_r;
reg valid_r;
localparam ST_IDLE = 0; // 闲置状态
localparam ST_SYNC = 1; // 同步,等待波特率采样脉冲
localparam ST_START = 2; // 起始位
localparam ST_DATA = 3; // 数据位
localparam ST_STOP = 4; // 停止位
always @(posedge clk, negedge rst_n) begin
if(!rst_n) state <= ST_IDLE;
else state <= state_next;
end
always @(*) begin
if(!rst_n) state_next = ST_IDLE;
else begin
case(state)
// 闲置状态下,等待RXD电平拉低,进入同步状态
ST_IDLE: begin
if(!rxd) state_next = ST_SYNC;
else state_next = state;
end
// 同步状态下,等待sample_flag采样标志信号,进入起始位接收状态
ST_SYNC: begin
if(sample_flag) state_next = ST_START;
else state_next = state;
end
// 起始位接收状态下,等待sample_flag采样标志信号,进入数据位接收状态
ST_START: begin
if(sample_flag) state_next = ST_DATA;
else state_next = state;
end
// 数据位接收状态下,等待计数器从0数到7共8-bit接收完毕,进入停止位接收状态
ST_DATA: begin
if((cnt==7)&sample_flag)
state_next = ST_STOP;
else state_next = state;
end
// 停止位发送状态下,等待sample_flag采样标志信号,结束本次发送,进入闲置状态
ST_STOP: state_next <= ST_IDLE;
default: state_next <= ST_IDLE;
endcase
end
end
always @(posedge clk, negedge rst_n) begin
if(!rst_n) cnt <= 0;
else if(state!=ST_DATA) cnt <= 0;
else if(sample_flag) cnt <= cnt + 1;
end
always @(posedge clk, negedge rst_n) begin
if(!rst_n) data_shift <= 0;
else if(state==ST_IDLE)
data_shift <= 0;
else if(sample_flag)
data_shift <= {rxd, data_shift[7:1]};
end
always @(posedge clk, negedge rst_n) begin
if(!rst_n) rx_data_r <= 0;
else if((cnt==7)&sample_flag)
rx_data_r <= data_shift;
end
always @(posedge clk, negedge rst_n) begin
if(!rst_n) valid_r <= 0;
else valid_r <= (cnt==7)&sample_flag;
end
assign baud_en = (state!=ST_IDLE);
assign rx_data = rx_data_r;
assign valid = valid_r;
endmodule
核心功能在自模块已经逐个实现,顶层模块只需要按照功能将自模块进行拼装。这里对子模块封装了两层,分别是UART功能的UART_Top,以及用来将模块和板载外设连接的FPGA_TOP。
module UART_Top
#(
parameter simPresent = 0
)(
input CLK,
input RSTn,
input tx_flag,
input [7:0] tx_data,
output tx_done,
output rx_valid,
output [7:0] rx_data,
input UART_RXD,
output UART_TXD
);
wire [15:0] prescale;
wire tx_baud_en;
wire tx_baud_pulse;
wire rx_baud_en;
wire rx_baud_pulse;
generate
if(simPresent) assign prescale = 16;
else assign prescale = 434;
endgenerate
baud_gen u_tx_baud(
.clk (CLK ),
.rst_n (RSTn ),
.prescale (prescale ),
.baud_en (tx_baud_en ),
.baud_pulse (tx_baud_pulse )
);
transmit u_tx(
.clk (CLK ),
.rst_n (RSTn ),
.din (tx_data ),
.txd (UART_TXD ),
.sample_flag (tx_baud_pulse ),
.tx_flag (tx_flag ),
.baud_en (tx_baud_en ),
.done (tx_done )
);
baud_gen u_rx_baud(
.clk (CLK ),
.rst_n (RSTn ),
.prescale (prescale ),
.baud_en (rx_baud_en ),
.baud_pulse (rx_baud_pulse )
);
receive u_rx(
.clk (CLK ),
.rst_n (RSTn ),
.rxd (UART_RXD ),
.rx_data (rx_data ),
.baud_en (rx_baud_en ),
.sample_flag (rx_baud_pulse ),
.valid (rx_valid )
);
endmodule
module FPGA_TOP
#(
parameter simPresent = 0
)(
// global signals
input CLK,
input RSTn,
// control & status signals
input [7:0] SW,
input [0:0] KEY,
output [7:0] LED,
// data
input UART_RXD,
output UART_TXD
);
wire [7:0] tx_data;
wire tx_flag;
wire tx_done;
wire [7:0] rx_data;
assign tx_data = SW;
assign tx_flag = KEY & tx_done;
assign LED = rx_data;
UART_Top #( .simPresent(simPresent) )
u_uart
(
.CLK (CLK ),
.RSTn (RSTn ),
.tx_flag (tx_flag ),
.tx_data (tx_data ),
.tx_done (tx_done ),
.rx_data (rx_data ),
.rx_valid (),
.UART_RXD (UART_RXD ),
.UART_TXD (UART_TXD )
);
endmodule
工程综合后,通过rtl viewer可以看到整个模型的结构。
对于testbench,这里利用task的方式模拟UART的传输行为,并以此作为输入激励,测试我们UART模块的接收功能。
module tb;
reg clk;
reg rst_n;
wire UART_TXD;
reg UART_RXD;
reg uart_pulse;
localparam CLK_PERIOD = 20;
initial begin
clk = 0;
rst_n = 0;
UART_RXD = 1;
#200 rst_n = 1;
uart_pulse = 0;
TaskUartTest(8'h20);
TaskUartTest(8'h4d);
TaskUartTest(8'h65);
TaskUartTest(8'h72);
TaskUartTest(8'h52);
TaskUartTest(8'h79);
TaskUartTest(8'h20);
TaskUartTest(8'h43);
TaskUartTest(8'h68);
TaskUartTest(8'h72);
TaskUartTest(8'h69);
TaskUartTest(8'h73);
TaskUartTest(8'h74);
TaskUartTest(8'h6d);
TaskUartTest(8'h61);
TaskUartTest(8'h73);
TaskUartTest(8'h21);
#50000 $stop;
end
always #(CLK_PERIOD/2) clk = ~clk;
FPGA_TOP
#( .simPresent(1) )
dut(
.CLK (clk ),
.RSTn (rst_n ),
.SW (8'h55 ),
.KEY (1 ),
.UART_RXD (UART_RXD),
.UART_TXD (UART_TXD)
);
always #(16*CLK_PERIOD/2) uart_pulse = ~uart_pulse;
task TaskUartTest;
input [7:0] putchar;
begin: taskUartTest
automatic integer i;
UART_RXD = 1;
@(posedge uart_pulse) UART_RXD = 0;
for(i=0; i<8; i=i+1)
@(posedge uart_pulse) UART_RXD = putchar[i];
@(posedge uart_pulse) UART_RXD = 1;
end
endtask
endmodule
在发送部分,模块将0x55封装并持续发送,可以通过TXD的输出结果。
在接收部分,LED显示了UART模块的接收结果,可以看到和tb中的输入一致。
引脚约束->综合->实现->生成比特流文件
SW作为要发送的数据,KEY作为发送使能。对于有些已经有串口转USB芯片的开发板,直接以此进行引脚约束,并连接电脑即可。但在大多数情况下,需要将TXD和RXD约束至开发板的GPIO,并通过CH340串口转USB模块和电脑连接。最后选择一款串口助手软件,即可观察实验结果。