最近做了两种通信协议的实现的练习(uart,spi),此文介绍uart串口协议(串口发送)的verilog实现和testbench的编写,考虑到还有部分同学使用vhdl,vhdl版本会随后发布。在以后的系列里,还会有介绍spi协议的文章,把我自己学习中遇到的困难和正确的解决方案记录下来,仿真环境为vivado 2018.3.
一、串口通信协议(uart)
串口作为常用的三大低速总线(UART、SPI、IIC)之一,在设计众多通信接口和调试时占有重要地位。串口(UART)全称通用异步收发传输器(Universal Asynchronous Receiver/Transmitter),主要用于数据间的串行传递,是一种全双工传输模式。它在发送数据时将并行数据转换成串行数据来传输,在接收数据时将接收到的串行数据转换成并行数据。
此文目前只是实现串口发送过程,即将并行数据转化成串行数据发送给其他设备。
作为新手,我觉得串口通信主要要注意以下几点:
1.什么叫异步?异步,即意味着在数据传递的两个模块之间使用的不是同步时钟。实际上在异步串口的传输中是不需要时钟的,而是通过特定的时序来标志传输的开始(起始位--由高到低)和结束(结束位,拉高)。而我们后面讲的SPI协议就是同步方式,是通过一个sclk信号来协同主从设备。
2.物理层,如下图所示。我们其实只用关心RXD、TXD、GND端口,其他其实不用知道太多,有兴趣的同学可以自行百度。
3.看懂uart发送的时序图。(非常重要!!!!)
二、串口发送的时序图
1.首先要了解 串口发送的数据格式,因为是异步方式,所以数据格式就显得尤为重要,就是要知道通信的起始标志和终止标志,即通信是什么时候开始,是什么时候结束。
UART 在发送或接收过程中的一帧数据由4部分组成,起始位、数据位、奇偶校验位和停止位,如上图所示。其中,起始位标志着一帧数据的开始,停止位标志着一帧数据的结束,数据位是一帧数据中的有效数据。
起始位:起始位标志。低有效。
数据位:一帧数据中的有效数据。注意,串口协议规定了数据位长度只能位 6 7 8 位。要想发多位数据,只能将其切分为多个过程发送。
奇偶校验位:是用来验证数据的正确性。奇偶校验一般不使用,如果 使用,则既可以做奇校验(Odd)也可以做偶校验(Even)。其实就是发的时候如果奇校验位为1,表面数据中1的个数为奇数个,再看接受端的奇校验位是否依旧为1。如果是的话,校验成功。在一定概率上保证了传输的正确性。当然,只是概率上的传输正确。此次试验不同奇偶校验。
停止位:停止位标志着一帧数据的结束,高有效。
空闲时,传输 1 。
波特率设置 : 由于是异步通信,所以没有一个统一的时钟,需要双方约定传输传输速率。常见的波特率有:300, 1200, 2400, 9600, 19200, 115200 等.即一秒发送多少位数据。
所以我们需要实现下面的时序图。当使能信号到来时,发送起始位,数据位,停止位,一帧数据发送结束。
三、FPGA实现。
任务: uart_tx模块由单脉冲信号send_go使能,将data[7:0]读入uart_tx模块,发送完成后,输出单脉冲tx_done。
模块框图:
设计文件:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2022/04/28 09:19:27
// Design Name:
// Module Name: uart_tx
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module uart_byte_tx (
clk,
reset,
send_go, // 改成单脉冲启动信号 启动一个内部 send_en 信号
data,
baud_set,
uart_tx,
tx_done
);
input clk;
input reset;
input send_go;
input [7:0]data;
input [2:0]baud_set;
output reg uart_tx;
output reg tx_done;
// every data remain time
// baud_set = 0 bps = 9600 bps_max = 1000 000 000 / 9600 / 20
reg [17:0]bps_max;
always@(*)
case(baud_set)
0:bps_max = 1000000000/9600/20; // clk = 50MHZ
0:bps_max = 1000000000/19200/20;
0:bps_max = 1000000000/38400/20;
0:bps_max = 1000000000/57600/20;
0:bps_max = 1000000000/115200/20;
default:bps_max = 1000000000/9600/20;
endcase
reg send_en;
always@(posedge clk or negedge reset)
if (!reset)
send_en <= 0;
else if(send_go)
send_en <= 1;
else if(tx_done)
send_en <= 0;
// 一旦开始发送,send_go 有效,就将外部的data 锁存起来,,这样防止外部数据变化而采错data【i】
reg [7:0]r_data;
always@(posedge clk or negedge reset)
if (send_go)
r_data <= data;
else
r_data <= r_data;
wire bps_clk;
assign bps_clk = (div_cnt == 1);
reg [17:0]div_cnt;
always @(posedge clk or negedge reset) begin
if(!reset)
div_cnt <= 0;
else if(send_en)
begin
if(div_cnt == bps_max - 1)
div_cnt <= 0;
else
div_cnt <= div_cnt + 1'b1;
end
else
div_cnt <= 0;
end
reg [3:0]bps_cnt; // 11
always @(posedge clk or negedge reset) begin
if(!reset)
bps_cnt <= 0;
else if(send_en)begin
if(bps_clk)begin
//if(div_cnt == 1)begin // 当send_en有效事 bps_cnt 才开始加 bps_cnt 不要等到最大再加一 这样会滞后
if(bps_cnt == 12)
bps_cnt <= 0;
else
bps_cnt <= bps_cnt + 1'b1;
end
end
else
bps_cnt <= 0;
end
// send
always @(posedge clk or negedge reset)
if(!reset)begin
uart_tx <= 1'b1;
tx_done <= 1'b0;
end
else begin
case(bps_cnt)
// 0:begin uart_tx <= 1'b0;tx_done <= 1'b0;end 这样写,bps_cnt 上面是 send_en 为零的时候就 为0 用0 不科学
// 1:uart_tx <= data[0];
// 2:uart_tx <= data[1];
// 3:uart_tx <= data[2];
// 4:uart_tx <= data[3];
// 5:uart_tx <= data[4];
// 6:uart_tx <= data[5];
// 7:uart_tx <= data[6];
// 8:uart_tx <= data[7];
// 9:uart_tx <= 1'b1;
// 10:begin uart_tx <= 1'b1;tx_done <= 1'b1;end
// default:uart_tx <= 1'b1;
0:tx_done<=1'b0;
1:uart_tx <= 1'b0;
2:uart_tx <= r_data[0];
3:uart_tx <= r_data[1];
4:uart_tx <= r_data[2];
5:uart_tx <= r_data[3];
6:uart_tx <= r_data[4];
7:uart_tx <= r_data[5];
8:uart_tx <= r_data[6];
9:uart_tx <= r_data[7];
10:uart_tx <= 1'b1;
11:begin uart_tx <= 1'b1;end
default:uart_tx <= 1'b1;
endcase
end
// 为了保证 tx_done 只持续一个时钟周 单独写一个process
always @(posedge clk or negedge reset)
if(!reset)
tx_done <= 1'b0;
else if ((bps_clk == 1) && (bps_cnt == 10))
tx_done <= 1'b1;
else
tx_done <= 1'b0;
endmodule
testbench文件:
`timescale 1ns / 1ps
//
// Company:
// Engineer:
//
// Create Date: 2022/05/01 14:23:59
// Design Name:
// Module Name: uart_byte_tx_tb
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//
module uart_byte_tx_tb(
);
reg clk;
reg reset;
reg send_go;
reg [7:0]data;
wire uart_tx;
wire tx_done;
uart_byte_tx uart_byte_tx (
.clk(clk),
.reset(reset),
.send_go(send_go), // 改成单脉冲启动信号 启动一个内部 send_en 信号
.data(data),
.baud_set(3'd4),
.uart_tx(uart_tx),
.tx_done(tx_done)
);
initial clk = 1;
always#10 clk = ~clk;
initial begin
reset = 0;
#201;
reset = 1;
send_go = 1;
data = 8'h57;
#20
send_go = 0;
@(posedge tx_done);
#201;
reset = 1;
send_go = 1;
data = 8'h75;
#20
send_go = 0;
@(posedge tx_done);
#20000;
$stop;
end
endmodule
仿真结果:
由上图结果我们可以看到: 当send_go信号来临时(单脉冲),至于为什么设计成单脉冲,因为单脉冲更符合模块与模块之间的握手习惯。这个主要参考B站上小梅哥的视频。现在我们继续看这个仿真结果,首先发送01010111这个数据,串口规定从低位到高位,所以加上起始标志和结束标志,就是0111010101十位数据,证明时序的正确性。每发送完一个数据,产生一个tx_done单脉冲。工程文件可以直接下载我的资源(0积分)串口通信实现(verilog带testbench文件)-嵌入式文档类资源-CSDN文库。