简介
串口,即 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.//--------------------------------------------------------------------------------------------------// 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); endend
// 打拍同步 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};endwire 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 ; endend
// 波特率计数器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 ; // 空闲状态清零 endend
// 接收数据计数器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 ; // 空闲状态清零 endend
// 根据 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 ; // 空闲时清零(不保留残余) endend
endmodule串口接收模块
该模块负责串口数据的接收
// FILE_HEADER_HEADER-------------------------------------------------------------------------------// Copyright (c) 2026, Mr. Tian. All rights reserved.//--------------------------------------------------------------------------------------------------// 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};endwire 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); endend
// 接收标志指示器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 ; endend
// 波特率计数器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 ; // 空闲状态清零 endend
// 接收数据计数器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 ; // 空闲状态清零 endend
// 根据 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; // 空闲时清零(不保留残余) endend
// 校验位判断, 若校验位不通过则将 rx_err 置 1always @(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 ; endend
// 赋值接收完成信号和接收到的数据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 ; endend
endmodule封装后的串口顶层
为了方便使用,直接将发送模块和接收模块作为两个子模块封装到一个顶层模块中。
// FILE_HEADER_HEADER-------------------------------------------------------------------------------// Copyright (c) 2026, Mr. Tian. All rights reserved.//--------------------------------------------------------------------------------------------------// 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.//--------------------------------------------------------------------------------------------------// 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