FPGA(五)UART串口通信
5.1 串行通信基础
同步与异步通信
同步通信:带时钟同步信号的数据传输,发送方和接收方在同一时钟下工作。
异步通信:不带时钟同步信号的数据传输,发送方和接收方使用各自的时钟。
通信方向:单工、半双工、全双工。
常见串行通信接口:
通信标准 引脚 通信方式 通信方向 UART TXD:发送端
RXD:接收端
GND:公共接地异步 全双工 单总线(1-wire) DQ:发送/接收端 异步 半双工 SPI SCK:同步时钟
MISO:主机输入从机输出
MOSI:主机输出从机输入同步 全双工 I2C SCL:同步时钟
SDA:数据输入/输出同步 半双工
5.2 UART基础
UART(Universal Asynchronous Receiver/Transmitter):通用异步接收器/发送器。
5.2.1 协议层
数据格式

- 空闲:拉高。
- 起始位:拉低。
- 传输固定bit数据(上图是7bit)。
- 校验位:奇偶校验(比如奇校验就是这8bit中1的个数为奇数)。
- 停止位:拉高(长度不一定是一个波特率)。
传输速率
波特率:每秒传输的二进制位数,单位bps。常用波特率有9600、19200、38400、57600、115200。
比如波特率为115200,时钟频率为50MHz。那么每发送1位的时钟周期是:50M/115200=434.028≈434。
5.2.2 物理层
接口标准

RS422和RS485是差分传输。差分传输:两组信号相位相反,接收时用它们的差值来判断01,对共模噪声(两根线分别对地的噪声)抑制好。
DB9接口
常见的RS232接口。主要关注RXD、TXD、GND引脚。

拓展一下USB接口的定义。下图从左到右分别是USB-A、USB-B、USB-micro、USB-mini。
USB-A和USB-B:
引脚 名称 说明 1 VCC +5V电源 2 Data- 数据- 3 Data+ 数据+ 4 GND 地 USB-micro和USB-mini
引脚 名称 说明 1 VCC +5V电源 2 Data- 数据- 3 Data+ 数据+ 4 ID A型:与地相连
B型:不接地(空)5 GND 地
5.3 UART通信实验代码
整个代码由以下3部分构成:
uart_recv:接收主机的uart数据;uart_loop:环回模块,即简单的将接收到的数据发送出去,以及一些控制逻辑;uart_send:发送数据给主机。
下图是接收的时序示意图(发送也是类似的,只不过rx变为tx),对其中的信号/变量作简单说明:
本实验接收的停止位长度设为1/2数据位的长度(434/2=217),实际上设置比较自由,1倍、2倍数据位长度都可以。
start_flag:检测到uart_rxd的下降沿时拉高;rx_flag:拉高表示正在接收数据,start_flag拉高时拉高,接收完停止位时拉低;clk_cnt:计数发送一位的时钟周期,本实验为434;uart_done:接收完数据位,此时uart_data有效。

5.3.1 接收模块
module uart_recv #(
parameter CLK_FREQ = 50000000, // 系统时钟频率
parameter UART_BPS = 115200 // 串口波特率
)(
input sys_clk, // 系统时钟
input sys_rst_n, // 系统复位
input uart_rxd, // UART接收端口
output logic recv_done, // 接收一帧数据完成标志
output logic [7:0] recv_data // 接收的数据
);
// 传输一位数据需要的时钟周期数(434)
localparam BPS_CNT = CLK_FREQ / UART_BPS;
wire start_flag; // 检测下降沿,说明起始位来了
logic rx_flag; // 接收过程标志信号
logic [15:0] clk_cnt; // 系统时钟计数器
logic [ 3:0] rx_cnt; // 接收数据计数器
logic [ 7:0] rx_data; // 暂存接收的数据
// 1. 打两拍捕捉下降沿
logic uart_rxd_d0;
logic uart_rxd_d1;
assign start_flag = (~uart_rxd_d0) & uart_rxd_d1;
always_ff @(posedge sys_clk, negedge sys_rst_n) begin
if (!sys_rst_n) begin
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
// 2. 控制接收过程:为rx_flag赋值
always_ff @(posedge sys_clk, negedge sys_rst_n) begin
if (!sys_rst_n) rx_flag <= 1'b0;
// 检测到起始位时拉高
else if (start_flag) rx_flag <= 1'b1;
// 停止位结束时拉低(本实验停止位只有1/2数据位的长度)
else if (rx_cnt == 4'd9 && clk_cnt == BPS_CNT / 2) rx_flag <= 1'b0;
end
// 3. 计数接收数据
always_ff @(posedge sys_clk, negedge sys_rst_n) begin
if (!sys_rst_n) begin
clk_cnt <= 16'd0;
rx_cnt <= 4'd0;
end
else if (rx_flag) begin // 接收过程中计数
if (clk_cnt == BPS_CNT - 1) begin
clk_cnt <= 16'd0;
rx_cnt <= rx_cnt + 1'b1;
end
else begin
clk_cnt <= clk_cnt + 1'b1;
rx_cnt <= rx_cnt;
end
end
else begin
clk_cnt <= 16'd0;
rx_cnt <= 4'd0;
end
end
// 4. 根据计数值接收数据
always_ff @(posedge sys_clk, negedge sys_rst_n) begin
if (!sys_rst_n) rx_data <= 8'd0;
else if (rx_flag) begin
if (clk_cnt == BPS_CNT / 2) begin // 计数到一半时认为数据稳定,赋值
case (rx_cnt)
4'd1 : rx_data[0] <= uart_rxd_d1;
4'd2 : rx_data[1] <= uart_rxd_d1;
4'd3 : rx_data[2] <= uart_rxd_d1;
4'd4 : rx_data[3] <= uart_rxd_d1;
4'd5 : rx_data[4] <= uart_rxd_d1;
4'd6 : rx_data[5] <= uart_rxd_d1;
4'd7 : rx_data[6] <= uart_rxd_d1;
4'd8 : rx_data[7] <= uart_rxd_d1;
endcase
end
end
else rx_data <= 8'd0;
end
// 5. 数据接收完毕后给出标志信号并寄存输出接收到的数据
always_ff @(posedge sys_clk, negedge sys_rst_n) begin
if (!sys_rst_n) begin
recv_data <= 8'd0;
recv_done <= 1'b0;
end
else if (rx_cnt == 4'd9) begin // 接收数据计数器计数到停止位时
recv_data <= rx_data; // 寄存输出接收到的数据
recv_done <= 1'b1; // 将接收完成标志位拉高
end
else begin
recv_data <= 8'd0;
recv_done <= 1'b0;
end
end
endmodule5.3.2 发送模块
module uart_send #(
parameter CLK_FREQ = 50000000, // 系统时钟频率
parameter UART_BPS = 115200 // 串口波特率
)(
input sys_clk,
input sys_rst_n,
input send_en, // 发送使能信号
input [7:0] send_data, // 待发送数据
output send_busy, // 发送忙状态标志
output logic uart_txd // UART发送端口
);
localparam BPS_CNT = CLK_FREQ / UART_BPS;
wire en_flag; // 捕获发送使能信号
logic tx_flag; // 发送过程标志信号
logic [15:0] clk_cnt; // 系统时钟计数器
logic [ 3:0] tx_cnt; // 发送数据计数器
logic [ 7:0] tx_data; // 寄存发送数据
// 在串口发送过程中给出忙状态标志
assign send_busy = tx_flag;
// 1. 打两拍,捕获uart_en上升沿,得到一个时钟周期的脉冲信号
logic uart_en_d0;
logic uart_en_d1;
assign en_flag = uart_en_d0 & (~uart_en_d1);
always @(posedge sys_clk, negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_en_d0 <= 1'b0;
uart_en_d1 <= 1'b0;
end
else begin
uart_en_d0 <= send_en;
uart_en_d1 <= uart_en_d0;
end
end
// 2. 当脉冲信号en_flag到达时,寄存待发送的数据,并进入发送过程
always @(posedge sys_clk, negedge sys_rst_n) begin
if (!sys_rst_n) begin
tx_flag <= 1'b0;
tx_data <= 8'd0;
end
// 检测到发送使能上升沿时,tx_flag拉高,寄存待发送数据
else if (en_flag) begin
tx_flag <= 1'b1;
tx_data <= send_data;
end
// 计数到停止位结束时,停止发送过程(这里停止位的长度设置为15/16数据位的长度)
else if (tx_cnt == 4'd9 && clk_cnt == BPS_CNT - BPS_CNT / 16) begin
tx_flag <= 1'b0;
tx_data <= 8'd0;
end
end
// 3. 进入发送过程后,启动系统时钟计数器
always @(posedge sys_clk, 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) begin
clk_cnt <= 16'd0;
tx_cnt <= tx_cnt + 1'b1;
end
else begin
clk_cnt <= clk_cnt + 1'b1;
tx_cnt <= tx_cnt;
end
end
else begin
clk_cnt <= 16'd0;
tx_cnt <= 4'd0;
end
end
// 4. 根据发送数据计数器来给uart发送端口赋值
always @(posedge sys_clk, negedge sys_rst_n) begin
if (!sys_rst_n) uart_txd <= 1'b1; // 默认拉高
else if (tx_flag)
case(tx_cnt)
4'd0: uart_txd <= 1'b0; // 起始位
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'b1; // 停止位
endcase
else uart_txd <= 1'b1;
end
endmodule5.3.3 环回模块
module uart_loop(
input sys_clk, // 系统时钟
input sys_rst_n, // 系统复位,低电平有效
input recv_done, // 接收一帧数据完成标志
input [7:0] recv_data, // 接收的数据
input send_busy, // 发送忙状态标志
output logic send_en, // 发送使能信号
output logic [7:0] send_data // 待发送数据
);
logic recv_done_d0;
logic recv_done_d1;
logic tx_ready;
wire recv_done_flag;
// 捕获recv_done上升沿,得到一个时钟周期的脉冲信号
assign recv_done_flag = recv_done_d0 & (~recv_done_d1);
// 对发送使能信号recv_done延迟两个时钟周期
always @(posedge sys_clk, negedge sys_rst_n) begin
if (!sys_rst_n) begin
recv_done_d0 <= 1'b0;
recv_done_d1 <= 1'b0;
end
else begin
recv_done_d0 <= recv_done;
recv_done_d1 <= recv_done_d0;
end
end
//判断接收完成信号,并在串口发送模块空闲时给出发送使能信号
always @(posedge sys_clk, negedge sys_rst_n) begin
if (!sys_rst_n) begin
tx_ready <= 1'b0;
send_en <= 1'b0;
send_data <= 8'd0;
end
else begin
if (recv_done_flag) begin // 检测串口接收到数据
tx_ready <= 1'b1; // 准备启动发送过程
send_en <= 1'b0;
send_data <= recv_data; // 寄存串口接收的数据
end
else if (tx_ready && (~send_busy)) begin // 检测串口发送模块空闲
tx_ready <= 1'b0; // 准备过程结束
send_en <= 1'b1; // 拉高发送使能信号
end
end
end
endmodule5.3.4 顶层模块
module uart_loopback_top #(
parameter CLK_FREQ = 50000000, // 定义系统时钟频率
parameter UART_BPS = 115200 // 定义串口波特率
)(
input sys_clk, // 外部50M时钟
input sys_rst_n, // 外部复位信号,低有效
input uart_rxd, // UART接收端口
output uart_txd // UART发送端口
);
wire recv_done; // UART接收完成
wire [7:0] recv_data; // UART接收数据
wire send_en; // UART发送使能
wire [7:0] send_data; // UART发送数据
wire send_busy; // UART发送忙状态标志
//串口接收模块
uart_recv #(.CLK_FREQ(CLK_FREQ), .UART_BPS(UART_BPS)) u_uart_recv (.*);
//串口发送模块
uart_send #(.CLK_FREQ(CLK_FREQ), .UART_BPS(UART_BPS)) u_uart_send (.*);
//串口环回模块
uart_loop u_uart_loop (.*);
endmodule5.3.5 约束文件
由于alinx7010的uart连到了PS端,因此没法在FPGA上测。下面的约束文件只是示意。
create_clock -period 20.000 -name clk [get_ports sys_clk]
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN N15 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]
set_property -dict {PACKAGE_PIN K14 IOSTANDARD LVCMOS33} [get_ports uart_rxd]
set_property -dict {PACKAGE_PIN M15 IOSTANDARD LVCMOS33} [get_ports uart_txd]FPGA(五)UART串口通信
https://shuusui.site/blog/2025/12/22/fpga-5/
