基于Smart ZYNQ(SP/SP2/SL 版) 的FPGA实验十四 基于PL端的 FPGA UART串口通讯实验

本文将简单介绍UART协议,并用verilog代码去实现一个UART的功能模块。

  • 此章节内容适用于Smart ZYNQ SP SP2和 SL 版的板子 ( 不包含Smart ZYNQ 标准版 ),如是标准版或本站其他板子请自行移植
  • 本文在 vivado2018.3版本上演示

一、关于UAR

什么是UART

UART是一种串行通信协议,用于在设备之间传输数据。它通常用于连接计算机与外部设备(比如传感器、显示器、模块等)。

UART不像其他协议(比如SPI或I2C)需要同步时钟信号,因此被称为异步协议。这意味着发送和接收设备不需要共享同步时钟。每个设备有自己的时钟,并且通过起始位来同步数据传输。

UART的协议构成:

  • 起始位(Start Bit):指示数据传输开始的位。它是逻辑低电平(0)(总线空闲时为高电平,所以开始通信时先发送一个区别于空闲状态的信号,起始位也作时钟同步用)
  • 数据位(Data Bits):包含要传输的数据,通常为8位,先发送数据的最低位,最后发送数据的最高位,
  • 奇偶校验位(Parity Bit)(可选):用于数据校验,有奇偶两种类型,有时用于检测传输中的错误。
  • 停止位(Stop Bit):标志数据传输结束的位,通常是逻辑高电平(1)
  • 空闲时段:UART协议规定,当总线处于空闲状态时信号线的状态为(1)

也就是如果我们观察UART波形可以看到在没有通讯的时候,UART信号线是是保持高电平(即空闲状态),当一个下降沿事件发生时,我们认为将进行一次数据的传输。

两个设备要用UART进行通讯,TX和RX两个信号线是需要交叉的,除了TX和RX信号外,两个设备间还需要共地(GND)。

UART 通讯电平

  • UART的电平,其实是有两种规格的,两者电压不同,逻辑也相反,不能直接连接
    • RS232 ( ±12V, 其中+12V代表逻辑1,-12V代表逻辑0) 多用于老式电脑串口和工业设备的通讯
    • TTL232(3.3V/5V代表逻辑1, 0V代表逻辑0)常用于单片机、开发板等嵌入式系统
    • RS232和TTL232可通过专用芯片进行转换,如(MAX232芯片等)

二、工程创建:

工程创建的过程可以参考实验一中的内容,这里不详细描述了。基于Smart ZYNQ (SP/SP2/SL 版) 的FPGA实验一 用ZYNQ的PL资源点亮一个LED(完整图文) (芯片型号选XC7Z020CLG484-1)

实验内容:

  • 本次实验我们将设计一个简单的UART模块,具体功能如下:
    • 上电后,串口自动发送字符串:hellofpga.com
    • 发送完成后,进入回环模式:串口接收的数据直接原样返回发送(Loopback)
    • 串口模块系统时钟50M,波特率115200,8 位数据,无校验,1 位停止位(8N1)
  • 工程将分为3个模块来进行设计:
    • 顶层模块 uart_top.v(初始化时发送字符串 "hellofpga.com",发送完成后进入回环模式)
    • 串口发送模块 uart_tx.v(将输入的 8 位数据以 UART 格式(1 起始位 + 8 数据位 + 1 停止位)串行发送)
    • 串口接收模块 uart_rx.v(对输入的 RX 信号采样,识别起始位,提取 8 位数据)

a) 创建一个顶层模块(uart_top.v

`timescale 1ns / 1ps
module uart_top(
input wire clk,
input wire rst_n,
input wire rx,
output wire tx
);

localparam INIT = 2'd0;
localparam SENDING = 2'd1;
localparam LOOP = 2'd2;

reg [1:0] state = INIT;
reg start_tx = 0;
reg [7:0] tx_data;
wire tx_busy;
wire [7:0] rx_data;
wire rx_done;

reg [3:0] send_index = 0;
reg [7:0] send_buf [12:0];

reg [7:0] tx_buf;
reg tx_req;

initial begin
send_buf[0] = "h";
send_buf[1] = "e";
send_buf[2] = "l";
send_buf[3] = "l";
send_buf[4] = "o";
send_buf[5] = "f";
send_buf[6] = "p";
send_buf[7] = "g";
send_buf[8] = "a";
send_buf[9] = ".";
send_buf[10] = "c";
send_buf[11] = "o";
send_buf[12] = "m";
end

always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= INIT;
send_index <= 0;
start_tx <= 0;
tx_req <= 0;
end else begin
case (state)
INIT: begin
state <= SENDING;
send_index <= 0;
end

SENDING: begin
if (!tx_busy && !start_tx && send_index < 13) begin
tx_data <= send_buf[send_index];
start_tx <= 1;
send_index <= send_index + 1;
end else if (start_tx && tx_busy) begin
start_tx <= 0;
end else if (send_index >= 13 && !tx_busy) begin
state <= LOOP;
end
end

LOOP: begin
if (rx_done && !tx_req) begin
tx_buf <= rx_data;
tx_req <= 1;
end

if (tx_req && !tx_busy && !start_tx) begin
tx_data <= tx_buf;
start_tx <= 1;
tx_req <= 0;
end else if (start_tx && tx_busy) begin
start_tx <= 0;
end
end
endcase
end
end

uart_tx #(
.CLK_FREQ(50000000),
.BAUD_RATE(115200)
) u_tx (
.clk(clk),
.rst_n(rst_n),
.tx_data(tx_data),
.tx_start(start_tx),
.tx(tx),
.tx_busy(tx_busy)
);

uart_rx #(
.CLK_FREQ(50000000),
.BAUD_RATE(115200)
) u_rx (
.clk(clk),
.rst_n(rst_n),
.rx(rx),
.rx_data(rx_data),
.rx_done(rx_done)
);

endmodule

UART 顶层模块在系统启动时,会先通过串口发送一串字符串 “hellofpga.com”,发送完成后进入回环模式。所谓回环模式,就是收到什么数据,就立即从 TX 发回去,实现 UART 的数据回送功能,方便测试串口是否正常。

整个模块分为三个状态:初始化(INIT)、发送固定字符串(SENDING)和回环(LOOP)。为避免发送冲突,回环部分加入了一个发送请求标志 tx_req,确保接收数据后不会丢包。发送和接收的波特率、时钟频率都可通过参数配置,便于灵活修改。(tx_req 是一个 “发送准备标志”,表示已经收到数据但还没发送出去,等到发送模块空闲时再启动发送,防止数据冲突或遗漏)

b) 创建串口发送模块 (uart_tx.v)

功能:将输入的 8 位数据以 UART 格式(1 起始位 + 8 数据位 + 1 停止位)串行发送,(tx_start 拉高开始发送,发送期间tx_busy 为高表示当前正在发送)

`timescale 1ns / 1ps
module uart_tx #(
parameter CLK_FREQ = 50000000,
parameter BAUD_RATE = 115200
)(
input wire clk,
input wire rst_n,
input wire [7:0] tx_data,
input wire tx_start,
output reg tx,
output reg tx_busy
);

localparam BAUD_TICK = CLK_FREQ / BAUD_RATE;

reg [15:0] baud_cnt = 0;
reg [3:0] bit_idx = 0;
reg [9:0] tx_shift = 10'b1111111111;

always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx <= 1;
tx_busy <= 0;
baud_cnt <= 0;
bit_idx <= 0;
tx_shift <= 10'b1111111111;
end
else begin
if (tx_start && !tx_busy) begin
tx_shift <= {1'b1, tx_data, 1'b0};
tx_busy <= 1;
baud_cnt <= 0;
bit_idx <= 0;
end
else if (tx_busy) begin
if (baud_cnt == BAUD_TICK - 1) begin
baud_cnt <= 0;
tx <= tx_shift[bit_idx];
bit_idx <= bit_idx + 1;

if (bit_idx == 9) begin
tx_busy <= 0;
tx <= 1;
end
end
else begin
baud_cnt <= baud_cnt + 1;
end
end
end
end
endmodule

tx_start 信号为高并且当前发送器空闲时,模块将数据打包成 10 位发送帧(起始位 + 数据 + 停止位),并开始以固定波特率(由 CLK_FREQBAUD_RATE 参数决定)逐位输出到 tx。发送过程中,tx_busy 会保持高电平(表示发送器正忙),直到所有位完成发送。

模块通过参数 CLK_FREQBAUD_RATE 控制波特率,内部计算每个位的发送周期 BAUD_TICK = CLK_FREQ / BAUD_RATE可在例化模块时修改这两个参数以适配不同的系统时钟和串口波特率。

c) 创建串口接收模块( uart_rx.v

功能:对输入的 rx 信号采样,识别起始位,提取 8 位数据,然后输出这个提取到的8位数据(rx_data),并拉高rx_done信号(一个周期)以表示接收完成。

module uart_rx #(
parameter CLK_FREQ = 50000000,
parameter BAUD_RATE = 115200
)(
input wire clk,
input wire rst_n,
input wire rx,
output reg [7:0] rx_data,
output reg rx_done
);

localparam BAUD_TICK = CLK_FREQ / BAUD_RATE;
localparam HALF_BAUD = BAUD_TICK / 2;

reg [15:0] baud_cnt = 0;
reg [3:0] bit_cnt = 0;
reg [7:0] rx_shift = 0;
reg [1:0] rx_sync = 2'b11;
reg rx_busy = 0;
reg start_edge = 0;

always @(posedge clk) begin
rx_sync <= {rx_sync[0], rx};
end

always @(posedge clk or negedge rst_n) begin
if (!rst_n)
start_edge <= 0;
else
start_edge <= (!rx_busy && rx_sync == 2'b10);
end

always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_busy <= 0;
rx_done <= 0;
baud_cnt <= 0;
bit_cnt <= 0;
rx_shift <= 0;
rx_data <= 0;
end
else begin
rx_done <= 0;

if (start_edge) begin
rx_busy <= 1;
baud_cnt <= 0;
bit_cnt <= 0;
end

if (rx_busy) begin
baud_cnt <= baud_cnt + 1;

if ((bit_cnt == 0 && baud_cnt == HALF_BAUD) || (bit_cnt > 0 && baud_cnt == BAUD_TICK)) begin
baud_cnt <= 0;
bit_cnt <= bit_cnt + 1;

if (bit_cnt >= 1 && bit_cnt <= 8) begin
rx_shift[bit_cnt - 1] <= rx_sync[1];
end

if (bit_cnt == 9) begin
rx_data <= rx_shift;
rx_done <= 1;
rx_busy <= 0;
end
end
end
end
end

endmodule

由于 rx 对于系统来说是异步信号,所以这里首先对输入信号 rx 进行同步。使用了 rx_sync 寄存器进行同步,以消除时钟域交叉可能带来的问题。通过 start_edge 信号检测 UART 数据的起始边沿(下降沿),以确定接收数据的开始位置。

当数据开始接收后,模块会根据波特率设置的时间间隔(通过 baud_cnt 计数器)来按位接收数据。每当接收到一位数据,就会将数据存储到一个寄存器 rx_shift 中,直到接收到 8 位数据(即一个完整的字节)。一旦接收完一个字节,就将这个字节输出到 rx_data,并通过 rx_done 信号告知外部,数据已经准备好。

另外数据的采样是在数据的中心点位置进行采样的(当 baud_cnt 计数到HALF_BAUD时进行采样,即捕捉数据位的中心点)确保数据在最稳定的时刻被采样,以保证了数据接收的可靠性和精度。

d)最后再增加约束文件,对管脚进行定义 (PIN.XDC)

set_property PACKAGE_PIN M19 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports rx]
set_property IOSTANDARD LVCMOS33 [get_ports tx]
set_property IOSTANDARD LVCMOS33 [get_ports rst_n]
set_property PACKAGE_PIN M17 [get_ports rx]
set_property PACKAGE_PIN L17 [get_ports tx]
set_property PACKAGE_PIN K21 [get_ports rst_n]

三、编译综合,并运行代码

1)将主板的UART口(TYPE C口)用数据线与电脑进行连接 。(如果是SL主板,还需要外接JTAG下载器,如果是SP/SP2主板则下载器与UART共用一个TYPE C口)2)在电脑端提前打开串口助手,波特率设置成115200,并开启串口功能。 3)在Vivado 上对FPGA芯片进行Program。

成功后可以从串口助手上看到hellofpga.com的字符内容,之后主板系统进入回环模式, 回环模式下我们通过串口助手向主板发送任何内容, 主板都会将解析到的数据进行回传并显示在串口助手的接收页面,如下图所示,证明我们的串口模块工作成功。

下列是本次实验的完整工程:

  • 本文的完整工程下载:14_PL_UART_TEST.zip
  • VIVADO的版本:2018.3
  • 工程创建目录:E:\Smart_ZYNQ_SP_SL\FPGA\14_PL_UART_TEST
  • 工程适用主板: Smart ZYNQ (SP / SP2 / SL) (不适用于Smart ZYNQ 标准版 

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注