FPGA 串口通信
简介
串口,即 UART(全称 Universal Asynchronous Receiver/Transmitter,中文正式名称为通用异步收发器),是采用串行通信方式的接口。串行通信将字节数据按比特在一条数据线上逐个传输,其特点是线路简单,但传输速度较慢。
对于传输速度要求不高的场合,如工业控制、嵌入式开发等领域,串口通信是常客。
串口通信方式
UART 通信需要两根信号线来实现,分别是 TXD 和 RXD。TXD 用来发送数据;RXD 用来接收数据。在发送数据时,将并行的 8 比特数据按比特在一条数据线上逐个传输。在接收数据时,将接收到的串行数据按比特解串成并行数据。发送时低位先发。
需要注意的是,两个串口设备通信时,不能把各自的 TXD 和 RXD 直接对应相连(即 TXD 接 TXD、RXD 接 RXD),而应该交叉连接:一个设备的 TXD 接另一个设备的 RXD,同时这个设备的 RXD 接另一个设备的 TXD。具体连接方式如下图所示。
串口通信数据格式
如下图所示,下图为一个字节数据 (字符帧) 的传输格式
- 起始位:标志着一帧数据的开始,该位固定为 0
- 数据位:一帧数据中的有效数据,在串口设置中可配置为 5、6、7、8 位
- 校验位:分为奇校验和偶校验。为了使得整体 1 的个数为对应的奇数/偶数,在该位补齐对应的 0 或 1,例图如下:


- 总结一下就是:奇偶校验是为了让传输的数据(包含校验位)中 1 的个数为奇数/偶数。如果传输字节中 1 的个数为 偶数/奇数,则校验位的数据为 1,否则为 0。
- 停止位:标志着一个数据帧的结束。该位固定为 1
波特率
串口通信中,我们用波特率表示数据传输的快慢。波特率表示每秒传输了多少个二进制位(bit)。通信双方各自使用本地时钟源,通过分频得到所需的波特率时钟信号。这种不需要额外传输时钟信号的方式,就是异步通信——其本质是双方各自依赖本地时钟,依靠起始位来完成字符同步。
常见的波特率有 9600、19200 以及 115200 等。波特率是人为规定的,理论上你可以设置任何速度的波特率,只要通信双方波特率一致,就可以正常通信。但实际硬件和软件通常只支持有限个常用值,这受限于系统时钟的分频:分频只能得到某些特定的数值,例如前面提到的常见波特率。此外,常见的操作系统和串口软件通常也只在常用波特率列表中提供选择,支持手动输入任意波特率的情况并不多。
实现代码
串口发送模块
该模块负责串口数据的发送
// FILE_HEADER_HEADER-------------------------------------------------------------------------------
// Copyright (c) 2026, Mr. Tian. All rights reserved.
//--------------------------------------------------------------------------------------------------
// FILE NAME : uart_tx.v
// AUTHOR : Mr. Tian
// DESCRIPTION : UART transmitter
//--------------------------------------------------------------------------------------------------
// REVISION HISTORY :
// Rev: (2026-05-06) - Mr. Tian
// Initial release.
// FILE_HEADER_FOOTER-------------------------------------------------------------------------------
module uart_tx # (
parameter CLOCK_FREQ = 32'd100_000_000 , // 输入时钟频率 (200MHz)
parameter BAUD_RATE = 32'd115200 , // 波特率
parameter DATA_BITS = 8 , // 数据位
parameter PARITY = "ODD" , // 校验方式,有 ODD(奇校验)、EVEN(偶校验)、SPACE(始终为 0)、MARK(始终为 1)、NONE(无校验)
parameter STOP_BITS = "1" // 停止位: 1, 1.5, 2,输入其它值则默认为 1 停止位
) (
input clk , // [I][ ] 输入的模块时钟,推荐 100.000 MHz
input rst_n , // [I][ ] 模块复位信号,低电平有效
input wire tx_en , // [I][ ] 发送使能信号,高脉冲有效
input wire [07:00] tx_data , // [I][07:00] 要发送的数据
output reg tx_busy , // [O][ ] 发送机忙信号
output reg txd // [O][ ] TXD 信号线
);
localparam BAUD_CNT_MAX = CLOCK_FREQ / BAUD_RATE ; // 计数器的最大计数值
localparam BAUD_CNT_MID = BAUD_CNT_MAX / 2 ; // 计数器值的中点
localparam HAS_PARITY = (PARITY != "NONE") ; // 是否使用了校验位
localparam STOP_BIT_INDEX = DATA_BITS + (HAS_PARITY ? 1 : 0) + 1 ; // 停止位在 bit_cnt 中的位置
// 根据停止位长度计算停止位结束时刻的 baud_cnt 计数值
// 1 -> 在停止位中点(BAUD_CNT_MAX/2 - 1)结束
// 1.5 -> 在停止位中点后继续 1 个位周期(BAUD_CNT_MAX + BAUD_CNT_MAX/2 - 1)
// 2 -> 在停止位中点后继续 1.5 个位周期(2*BAUD_CNT_MAX - 1)
// 为了保持代码一致,减 1 的操作在 always 语句中进行
localparam STOP_BITS1_CNT = BAUD_CNT_MAX/2 ; // 停止位为 1 时计数器的值
localparam STOP_BITS1_5_CNT = BAUD_CNT_MAX + BAUD_CNT_MAX/2 ; // 停止位为 1.5 时计数器的值
localparam STOP_BITS2_CNT = (2 * BAUD_CNT_MAX) ; // 停止位为 2 时计数器的值
localparam STOP_END_CNT = (STOP_BITS == "1" ) ? (STOP_BITS1_CNT ) :
(STOP_BITS == "1.5") ? (STOP_BITS1_5_CNT) :
(STOP_BITS == "2" ) ? (STOP_BITS2_CNT ) :
STOP_BITS1_CNT ; // 停止位计数器的值,默认按停止位 1 处理
reg [31:00] baud_cnt ;
reg [03:00] bit_cnt ;
reg [07:00] data_reg ;
wire parity_bit ;
// 校验位计算,使用的是缩位异或运算
// 缩位异或运算用来检查数据中 1 的个数是否为奇数,对其计算结果取反后可用于检查数据中 1 的个数是否为偶数
assign parity_bit = (PARITY == "ODD" ) ? ~(^tx_data[DATA_BITS-1:0]) :
(PARITY == "EVEN" ) ? ^tx_data[DATA_BITS-1:0] :
(PARITY == "MARK" ) ? 1'b1 :
(PARITY == "SPACE") ? 1'b0 :
(PARITY == "NONE" ) ? 1'b0 :
1'bz;
// 判断校验位的参数是否正确
initial begin
if (
!((PARITY == "ODD" ) ||
(PARITY == "EVEN" ) ||
(PARITY == "MARK" ) ||
(PARITY == "SPACE") ||
(PARITY == "NONE" ))
) begin
$error("Unknown PARITY value: [%s], for parameter PARITY, its value must be one of these values: ODD,EVEN,MARK,SPACE or NONE.", PARITY);
end
end
// 打拍同步 tx_en 信号,检测上升沿
reg [2:0] tx_en_sr;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) tx_en_sr <= 3'b111;
else tx_en_sr <= {tx_en_sr[1:0], tx_en};
end
wire tx_en_rising_edge = (tx_en_sr[2:1] == 2'b01); // 上升降沿检测
// 当 tx_en 检测到上升沿时, 寄存输入的并行数据, 并拉高 busy 信号
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_reg <= 8'b0 ;
tx_busy <= 1'b0 ;
end else if (tx_en_rising_edge) begin
data_reg <= tx_data ; // 检测到上升沿, 寄存输入的并行数据
tx_busy <= 1'b1 ; // 拉高 busy 信号
end else if (bit_cnt == STOP_BIT_INDEX && baud_cnt == STOP_END_CNT - 1'b1) begin
data_reg <= 8'b0 ; // 计数到停止位时清空发送数据寄存器
tx_busy <= 1'b0 ; // 拉低 busy 信号
end else begin
data_reg <= data_reg ;
tx_busy <= tx_busy ;
end
end
// 波特率计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
baud_cnt <= 16'd0 ;
end else if (tx_busy) begin
if(bit_cnt == STOP_BIT_INDEX) begin // 停止位阶段
if(baud_cnt < STOP_END_CNT)
baud_cnt <= baud_cnt + 1'b1 ; // 计数器计数到结束时刻
else
baud_cnt <= 16'd0 ; // 到达停止位结束时刻,清零(随后rx_flag变低)
end else begin
if (baud_cnt < BAUD_CNT_MAX - 1'b1) begin // 正常阶段,非停止位正常循环计数
baud_cnt <= baud_cnt + 1'b1 ; // 接收过程时计数器循环计数
end else begin
baud_cnt <= 16'd0 ; // 完成了一个计数周期后清零
end
end
end else begin
baud_cnt <= 16'd0 ; // 空闲状态清零
end
end
// 接收数据计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_cnt <= 4'd0 ;
end else if (tx_en_rising_edge) begin
bit_cnt <= 4'd0 ;
end else if (tx_busy) begin
if (bit_cnt < STOP_BIT_INDEX) begin
if(baud_cnt == BAUD_CNT_MAX - 1'b1) begin // 每个位周期结束时自增
bit_cnt <= bit_cnt + 1'b1 ;
end else begin
bit_cnt <= bit_cnt ;
end
end else begin // 已到达停止位索引,保持不变,等待 rx_flag 拉低
bit_cnt <= bit_cnt ;
end
end else begin
bit_cnt <= 4'd0 ; // 空闲状态清零
end
end
// 根据 bit_cnt 来发送数据到 txd 端口
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
txd <= 1'b1 ;
else if(tx_busy) begin // 系统发送忙状态时
if(baud_cnt == BAUD_CNT_MID - 1'b1) begin // 判断 baud_cnt 是否计数到数据位的中间
if (bit_cnt == 0) begin
txd <= 1'b0 ; // 起始位
end else if((bit_cnt >= 1) && (bit_cnt <= DATA_BITS)) begin
txd <= data_reg[bit_cnt-1] ; // 由低到高位发送数据
end else if (HAS_PARITY == 1'b1 && bit_cnt == STOP_BIT_INDEX - 1) begin
txd <= parity_bit ; // 发送校验位
end
end else begin
txd <= txd ;
end
end else begin
txd <= 1'b1 ; // 空闲时清零(不保留残余)
end
end
endmodule
串口接收模块
该模块负责串口数据的接收
// FILE_HEADER_HEADER-------------------------------------------------------------------------------
// Copyright (c) 2026, Mr. Tian. All rights reserved.
//--------------------------------------------------------------------------------------------------
// FILE NAME : uart_rx.v
// AUTHOR : Mr. Tian
// DESCRIPTION : UART receiver
//--------------------------------------------------------------------------------------------------
// REVISION HISTORY :
// Rev: (2026-05-06) - Mr. Tian
// Initial release.
// FILE_HEADER_FOOTER-------------------------------------------------------------------------------
module uart_rx # (
parameter CLOCK_FREQ = 32'd100_000_000 , // 输入时钟频率 (200MHz)
parameter BAUD_RATE = 32'd115200 , // 波特率
parameter DATA_BITS = 8 , // 数据位
parameter PARITY = "ODD" , // 校验方式,有 ODD(奇校验)、EVEN(偶校验)、SPACE(始终为 0)、MARK(始终为 1)、NONE(无校验)
parameter STOP_BITS = "1" // 停止位: 1, 1.5, 2,输入其它值则默认为 1 停止位
) (
input clk , // [I][ ] 模块时钟
input rst_n , // [I][ ] 复位信号,低有效
input wire rxd , // [I][ ] RXD 信号线
output reg [07:00] rx_data , // [O][07:00] 接收到的数据
output reg rx_done , // [O][ ] 接收完成标志(高脉冲)
output reg rx_err // [O][ ] 错误标志(校验错误或帧错误)
);
localparam BAUD_CNT_MAX = CLOCK_FREQ / BAUD_RATE ; // 计数器的最大计数值
localparam BAUD_CNT_MID = BAUD_CNT_MAX / 2 ; // 计数器值的中点
localparam HAS_PARITY = (PARITY != "NONE") ; // 是否使用了校验位
localparam STOP_BIT_INDEX = DATA_BITS + (HAS_PARITY ? 1 : 0) + 1 ; // 停止位在 bit_cnt 中的位置
// 根据停止位长度计算停止位结束时刻的 baud_cnt 计数值
// 1 -> 在停止位中点(BAUD_CNT_MAX/2 - 1)结束
// 1.5 -> 在停止位中点后继续 1 个位周期(BAUD_CNT_MAX + BAUD_CNT_MAX/2 - 1)
// 2 -> 在停止位中点后继续 1.5 个位周期(2*BAUD_CNT_MAX - 1)
// 为了保持代码一致,减 1 的操作在 always 语句中进行
localparam STOP_BITS1_CNT = BAUD_CNT_MAX/2 ; // 停止位为 1 时计数器的值
localparam STOP_BITS1_5_CNT = BAUD_CNT_MAX + BAUD_CNT_MAX/2 ; // 停止位为 1.5 时计数器的值
localparam STOP_BITS2_CNT = (2 * BAUD_CNT_MAX) ; // 停止位为 2 时计数器的值
localparam STOP_END_CNT = (STOP_BITS == "1" ) ? (STOP_BITS1_CNT ) :
(STOP_BITS == "1.5") ? (STOP_BITS1_5_CNT) :
(STOP_BITS == "2" ) ? (STOP_BITS2_CNT ) :
STOP_BITS1_CNT ; // 停止位计数器的值,默认按停止位 1 处理
reg [31:00] baud_cnt ;
reg [03:00] bit_cnt ;
reg [07:00] rx_reg ;
reg rx_parity ; // 接收到的校验位
// 打拍同步 rxd 信号,检测下降沿
reg [2:0] rxd_sync;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) rxd_sync <= 3'b111;
else rxd_sync <= {rxd_sync[1:0], rxd};
end
wire rxd_fall = (rxd_sync[2:1] == 2'b10); // 下降沿检测
wire rxd_s = rxd_sync[2]; // 同步后的电平
// 校验计算逻辑
wire parity_check;
assign parity_check = (PARITY == "ODD" ) ? ~(^rx_reg[DATA_BITS-1:0]) :
(PARITY == "EVEN" ) ? ^rx_reg[DATA_BITS-1:0] :
(PARITY == "MARK" ) ? 1'b1 :
(PARITY == "SPACE") ? 1'b0 :
(PARITY == "NONE" ) ? 1'b0 :
1'bz;
initial begin
if (
!((PARITY == "ODD" ) ||
(PARITY == "EVEN" ) ||
(PARITY == "MARK" ) ||
(PARITY == "SPACE") ||
(PARITY == "NONE" ))
) begin
$error("Unknown PARITY value: [%s], for parameter PARITY, its value must be one of these values: ODD,EVEN,MARK,SPACE or NONE.", PARITY);
end
end
// 接收标志指示器
reg rx_flag;
wire start_en = rxd_fall & ~rx_flag;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_flag <= 1'b0 ;
end else if (start_en) begin // 检测到起始位
rx_flag <= 1'b1 ; // 接收过程中,标志信号 rx_flag 拉高
// 根据停止位长度,在指定时刻结束接收
end else if (bit_cnt == STOP_BIT_INDEX && baud_cnt == STOP_END_CNT - 1'b1) begin
rx_flag <= 1'b0 ;
end else begin
rx_flag <= rx_flag ;
end
end
// 波特率计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
baud_cnt <= 16'd0 ;
end else if (rx_flag) begin
if(bit_cnt == STOP_BIT_INDEX) begin // 停止位阶段
if(baud_cnt < STOP_END_CNT)
baud_cnt <= baud_cnt + 1'b1 ; // 计数器计数到结束时刻
else
baud_cnt <= 16'd0 ; // 到达停止位结束时刻,清零(随后rx_flag变低)
end else begin
if (baud_cnt < BAUD_CNT_MAX - 1'b1) begin // 正常阶段,非停止位正常循环计数
baud_cnt <= baud_cnt + 1'b1 ; // 接收过程时计数器循环计数
end else begin
baud_cnt <= 16'd0 ; // 完成了一个计数周期后清零
end
end
end else begin
baud_cnt <= 16'd0 ; // 空闲状态清零
end
end
// 接收数据计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_cnt <= 4'd0 ;
end else if (rx_flag) begin
if (bit_cnt < STOP_BIT_INDEX) begin
if(baud_cnt == BAUD_CNT_MAX - 1'b1) begin // 每个位周期结束时自增
bit_cnt <= bit_cnt + 1'b1 ;
end else begin
bit_cnt <= bit_cnt ;
end
end else begin // 已到达停止位索引,保持不变,等待 rx_flag 拉低
bit_cnt <= bit_cnt ;
end
end else begin
bit_cnt <= 4'd0 ; // 空闲状态清零
end
end
// 根据 bit_cnt 来寄存 rxd 端口数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
rx_reg <= 8'b0 ;
rx_parity <= 1'b0 ;
end else if(rx_flag) begin // 系统处于接收过程时
if(baud_cnt == BAUD_CNT_MID - 1'b1) begin // 判断 baud_cnt 是否计数到数据位的中间
if((bit_cnt >= 1) && (bit_cnt <= DATA_BITS)) begin
rx_reg[bit_cnt-1] <= rxd_s ; // 数据位采样 (按位索引写入)
end else if (HAS_PARITY == 1'b1 && bit_cnt == STOP_BIT_INDEX - 1'b1) begin
rx_parity <= rxd_s ; // 采样校验位
end
end else begin
rx_reg <= rx_reg;
end
end else begin
rx_reg <= 8'b0; // 空闲时清零(不保留残余)
end
end
// 校验位判断, 若校验位不通过则将 rx_err 置 1
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_err <= 1'b0 ;
end else if ((HAS_PARITY == 1'b1) && (rx_parity == parity_check)) begin
rx_err <= 1'b0 ;
end else begin
rx_err <= 1'b1 ;
end
end
// 赋值接收完成信号和接收到的数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
rx_done <= 1'b0;
rx_data <= 8'b0;
end
// 在停止位中点采样完毕时输出数据
else if((bit_cnt == STOP_BIT_INDEX) && (baud_cnt == BAUD_CNT_MID - 1'b1)) begin
rx_done <= 1'b1 ; // 拉高接收完成信号
rx_data <= rx_reg ; // 并对UART接收到的数据进行赋值
end
else begin
rx_done <= 1'b0 ;
rx_data <= rx_data ;
end
end
endmodule
封装后的串口顶层
为了方便使用,直接将发送模块和接收模块作为两个子模块封装到一个顶层模块中。
// FILE_HEADER_HEADER-------------------------------------------------------------------------------
// Copyright (c) 2026, Mr. Tian. All rights reserved.
//--------------------------------------------------------------------------------------------------
// FILE NAME : uart.v
// AUTHOR : Mr. Tian
// DESCRIPTION : Top level UART module encapsulating TX and RX
//--------------------------------------------------------------------------------------------------
// REVISION HISTORY :
// Rev: (2026-05-06) - Mr. Tian
// Initial release.
// FILE_HEADER_FOOTER-------------------------------------------------------------------------------
module uart # (
parameter CLOCK_FREQ = 32'd100_000_000 , // 输入时钟频率 (200MHz)
parameter BAUD_RATE = 32'd115200 , // 波特率
parameter DATA_BITS = 8 , // 数据位
parameter [8*8-1:0] PARITY = "ODD" , // 校验方式,有 ODD(奇校验)、EVEN(偶校验)、SPACE(始终为 0)、MARK(始终为 1)、NONE(无校验)
parameter [8*8-1:0] STOP_BITS = "1" // 停止位: 1, 1.5, 2,输入其它值则默认为 1 停止位
) (
input clk ,
input rst_n ,
// TX 接口
input wire tx_en ,
input wire [07:00] tx_data ,
output wire tx_busy ,
output wire txd ,
// RX 接口
input wire rxd ,
output wire [07:00] rx_data ,
output wire rx_done ,
output wire rx_err
);
// 实例化发送模块
uart_tx #(
.CLOCK_FREQ (CLOCK_FREQ ),
.BAUD_RATE (BAUD_RATE ),
.DATA_BITS (DATA_BITS ),
.PARITY (PARITY ),
.STOP_BITS (STOP_BITS )
) uart_tx_inst (
.clk (clk ),
.rst_n (rst_n ),
.tx_en (tx_en ),
.tx_data (tx_data ),
.tx_busy (tx_busy ),
.txd (txd )
);
// 实例化接收模块
uart_rx #(
.CLOCK_FREQ (CLOCK_FREQ ),
.BAUD_RATE (BAUD_RATE ),
.DATA_BITS (DATA_BITS ),
.PARITY (PARITY ),
.STOP_BITS (STOP_BITS )
) uart_rx_inst (
.clk (clk ),
.rst_n (rst_n ),
.rxd (rxd ),
.rx_data (rx_data ),
.rx_done (rx_done ),
.rx_err (rx_err )
);
endmodule
回环测试(工程顶层)代码
在这里我使用的开发板型号为 XC7K325TFFG900-2,时钟输入为差分信号输入
// FILE_HEADER_HEADER-------------------------------------------------------------------------------
// Copyright (c) 2026, Mr. Tian. All rights reserved.
//--------------------------------------------------------------------------------------------------
// FILE NAME : uart_loopback_top.v
// AUTHOR : Mr. Tian.
// DESCRIPTION : Top module for uart loopback
//--------------------------------------------------------------------------------------------------
// REVISION HISTORY :
// Rev: (2026-05-06) - Mr. Tian.
// Initial release.
// FILE_HEADER_FOOTER-------------------------------------------------------------------------------
module uart_loopback_top # (
parameter CLOCK_FREQ = 32'd200_000_000 , // 输入时钟频率 (200MHz)
parameter BAUD_RATE = 32'd115200 , // 波特率
parameter DATA_BITS = 8 , // 数据位
parameter PARITY = "NONE" , // 校验方式,有 ODD(奇校验)、EVEN(偶校验)、SPACE(始终为 0)、MARK(始终为 1)、NONE(无校验)
parameter STOP_BITS = "1" // 停止位: 1, 1.5, 2,输入其它值则默认为 1 停止位
) (
input sys_clk_p, // 200MHz 差分时钟 P 端
input sys_clk_n, // 200MHz 差分时钟 N 端
input rst_n , // 复位信号(低有效)
// 硬件引脚连接到串口芯片
input uart_rxd, // UART 接收引脚
output uart_txd, // UART 发送引脚
// 连接到 LED 的状态指示
output led_rx_done,
output led_err
);
// 差分时钟输入转单端时钟输出
wire clk_200m_single;
IBUFDS ibufds_inst (
.I ( sys_clk_p ),
.IB ( sys_clk_n ),
.O ( clk_200m_single )
);
// 例化串口模块
wire tx_en ;
wire [07:00] tx_data ;
wire [07:00] rx_data ;
wire tx_busy ;
wire rx_done ;
uart # (
.CLOCK_FREQ (CLOCK_FREQ ),
.BAUD_RATE (BAUD_RATE ),
.DATA_BITS (DATA_BITS ),
.PARITY (PARITY ),
.STOP_BITS (STOP_BITS )
) uart_inst (
.clk ( clk_200m_single ),
.rst_n ( rst_n ),
// TX interface
.tx_en ( tx_en ),
.tx_data ( tx_data ),
.tx_busy ( tx_busy ),
.txd ( uart_txd ),
// RX interface
.rxd ( uart_rxd ),
.rx_data ( rx_data ),
.rx_done ( rx_done ),
.rx_err ( led_err )
);
assign tx_data = rx_data; // 回环测试:发送的数据为接收到的数据
assign tx_en = rx_done; // 回环测试:发送使能信号为接收完成信号
assign led_rx_done = rx_done; // 将接收完成信号输出到板载的一颗 led 上
endmodule