赞
踩
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模块和电脑连接。最后选择一款串口助手软件,即可观察实验结果。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。