FPGA实现UART串口通信(原理+完整代码实例,新手可直接复用)
UART串口通信是FPGA入门阶段最经典、最实用的异步通信案例,无论是FPGA与单片机、电脑的交互,还是项目中的简单数据传输,UART都能发挥作用。本文将从核心原理、关键参数(重点讲波特率)、Verilog完整代码,到仿真/硬件验证,一步步拆解,新手看完也能快速上手,代码可直接适配Altera、Xilinx等主流FPGA开发板。
一、UART串口通信核心原理
UART(通用异步收发传输器),核心关键词是“异步”——无需专门的时钟同步线,收发双方只需约定好相同的通信参数,就能实现数据的可靠传输。不同于SPI、I2C的同步通信,UART的优势的是接线简单(仅需TX发送线、RX接收线,共地即可),成本低、实用性强。
1. 核心通信参数(新手必记,最常用配置)
UART的通信稳定性,完全依赖于收发双方参数一致,其中最关键的5个参数如下,新手建议先掌握默认配置:
-
波特率:下文重点详解,核心是“每秒传输的二进制位数”,决定通信速度;
-
数据位:通常设为8位,即每次传输1个字节(0~255的数值);
-
停止位:通常设为1位,用于标识一个字节的数据传输结束;
-
校验位:新手建议设为“无校验”,简化设计(校验位用于检测传输错误,实际项目可根据需求添加);
-
帧结构:一个完整的UART数据帧 = 1位起始位(低电平0) + 8位数据位(低位先行) + 1位停止位(高电平1),共10位/字节。
2. 波特率详解(新手最易困惑,重点突破)
很多新手第一次接触UART,都会被“波特率”搞懵,其实一句话就能看懂,结合实例拆解更易理解:
(1)波特率的核心定义
波特率(Baud Rate)的本质是:串口通信中每秒传输的二进制位数(bit/s),单位是bps(bits per second)。简单来说,它就是串口传输“0”和“1”这些电信号的“速度”,和我们下载文件的“1MB/s”逻辑一致,只是单位不同(一个是bit,一个是Byte)。
(2)直观实例,一看就懂
结合我们常用的波特率和UART帧结构(10位/字节),举两个最常见的例子:
-
波特率=9600 → 每秒传输9600个二进制位 → 每秒可传输 9600÷10=960 个字节;
-
波特率=115200 → 每秒传输115200个二进制位 → 每秒可传输 115200÷10=11520 个字节;
很明显,波特率越高,通信速度越快,但同时对FPGA的时钟精度、硬件接线的抗干扰能力要求也越高。新手入门建议先从9600波特率开始,稳定性最好,不易出错。
(3)波特率的核心作用:保证收发同步
前面说过,UART是“异步通信”,没有专门的时钟线来同步收发双方的节奏,那怎么保证接收端能正确识别发送端的数据呢?答案就是“约定波特率”。
-
发送端:按照约定的波特率,每发送1个bit,就等待固定的时间(比如9600波特率下,每个bit占用 1÷9600≈104μs);
-
接收端:同样按照约定的波特率,每隔固定时间去采样一次接收引脚的电平(高电平=1,低电平=0),从而解析出正确的数据。
重点提醒:收发两端的波特率必须完全一致,否则会出现“采样错位”,最终解析出错误的数据。比如发送端用9600波特率,接收端用4800波特率,接收端采样的时间会越来越偏,传过来的“0101”可能会被解析成“0011”,导致通信失败。
(4)FPGA中波特率的具体实现(对应后续代码)
FPGA的系统时钟通常是固定的(比如50MHz、100MHz),而波特率是我们约定的(比如9600),如何用FPGA的高频时钟,模拟出波特率对应的“bit时间”?核心就是“时钟分频”。
分频系数计算公式:分频系数 = 系统时钟频率 ÷ 波特率
举例(后续代码用的就是这个配置):系统时钟=50MHz,波特率=9600,那么分频系数=50_000_000 ÷ 9600≈5208。也就是说,FPGA每计数5208个系统时钟脉冲,就对应UART的1个bit位,这样就能精准匹配波特率的速度。
(5)工程常用波特率(标准化,别自定义)
波特率是标准化的参数,实际项目中不要随意自定义(比如设为10000),否则容易出现兼容性问题,常用的标准化波特率如下,新手优先掌握9600和115200:
|
9600 |
104μs |
960 |
|
19200 |
52μs |
1920 |
|
38400 |
26μs |
3840 |
|
115200 |
8.68μs |
11520 |
3. FPGA实现UART的核心思路
FPGA实现UART通信,不需要复杂的IP核(新手建议手写代码,理解更透彻),核心只需要两个模块,再加上一个顶层模块整合,就能完成完整的收发功能:
-
发送模块(UART_TX):负责将FPGA内部的并行数据(比如8位的二进制数),转换成UART串行数据(按照“起始位+8位数据+停止位”的帧结构),通过TX引脚发送出去;
-
接收模块(UART_RX):负责通过RX引脚接收外部的UART串行数据,转换成FPGA内部能直接使用的并行数据,同时给出“接收完成”的标志,方便后续逻辑调用;
-
顶层模块(UART_TOP):将发送模块和接收模块例化,统一接口(时钟、复位、TX/RX引脚、数据接口等),方便下载到FPGA开发板验证。
核心逻辑总结:用时钟分频生成波特率节拍,用状态机控制UART帧结构的收发,用寄存器缓存数据和状态,实现并行与串行数据的转换。
二、FPGA UART完整代码实例(Verilog实现,新手可直接复用)
以下代码基于「50MHz系统时钟、9600波特率、8位数据位、1位停止位、无校验」的默认配置,包含发送模块、接收模块、顶层模块,注释详细,新手能看懂每一行的作用,下载到FPGA开发板后,配合串口助手就能快速验证通信功能。
1. 发送模块(uart_tx.v)
功能:接收FPGA内部的8位并行数据,当检测到“发送使能”信号后,按照UART帧结构,将并行数据转换成串行数据,通过tx_pin引脚发送出去,发送完成后给出“发送完成”标志。
module uart_tx #( parameter CLK_FREQ = 50_000_000, // 系统时钟频率(50MHz) parameter BAUD_RATE = 9600 // 约定波特率(9600) ) ( input clk, // 系统时钟(输入) input rst_n, // 低电平复位(输入,复位时停止发送) input [7:0] tx_data, // 待发送的8位并行数据(输入) input tx_en, // 发送使能(输入,高电平有效,触发发送) output reg tx_done, // 发送完成标志(输出,高电平表示发送结束) output reg tx_pin // UART发送引脚(输出,串行数据) ); // 计算波特率分频系数(核心:用系统时钟分频匹配波特率) localparam BAUD_DIV = CLK_FREQ / BAUD_RATE; // 50_000_000 / 9600 ≈ 5208 localparam BAUD_DIV_BIT = $clog2(BAUD_DIV); // 计算分频系数所需的寄存器位数 // 内部寄存器定义 reg [BAUD_DIV_BIT-1:0] baud_cnt; // 波特率计数器(计数到BAUD_DIV,对应1个bit) reg [3:0] bit_cnt; // 数据位计数器(0-9:起始位+8位数据+停止位) reg [7:0] tx_data_reg; // 数据缓存寄存器(缓存待发送数据,防止发送中数据变化) reg tx_state; // 发送状态寄存器(0:空闲,1:发送中) // 状态机逻辑(核心:控制UART帧结构的发送顺序) always @(posedge clk or negedge rst_n) begin if(!rst_n) begin // 复位状态:所有寄存器清零,tx_pin置高(空闲状态) tx_state <= 1'b0; baud_cnt <= 0; bit_cnt <= 0; tx_pin <= 1'b1; // UART空闲时,TX引脚为高电平 tx_done <= 1'b0; tx_data_reg <= 8'd0; end else begin tx_done <= 1'b0; // 默认清零发送完成标志(避免持续高电平) case(tx_state) 1'b0: begin // 空闲状态:等待发送使能信号 tx_pin<= 1'b1; // 空闲时保持高电平 if(tx_en) begin // 检测到发送使能,开始发送 tx_state <= 1'b1; // 切换到发送状态 tx_data_reg <= tx_data; // 缓存待发送数据 baud_cnt <= 0; // 波特率计数器清零 bit_cnt <= 0; // 数据位计数器清零 end end 1'b1: begin // 发送状态:按照帧结构发送数据 baud_cnt <= baud_cnt + 1'b1; // 波特率计数器累加 if(baud_cnt == BAUD_DIV – 1) begin // 计数到分频系数,完成1个bit的传输时间 baud_cnt <= 0; // 波特率计数器清零,准备下一个bit bit_cnt <= bit_cnt + 1'b1; // 数据位计数器累加 // 根据计数器值,发送对应位的数据 case(bit_cnt) 4'd0: tx_pin <= 1'b0; // 发送起始位(低电平0) 4'd1: tx_pin <= tx_data_reg[0]; // 发送第0位(最低位,UART默认低位先行) 4'd2: tx_pin <= tx_data_reg[1]; 4'd3: tx_pin <= tx_data_reg[2]; 4'd4: tx_pin <= tx_data_reg[3]; 4'd5: tx_pin <= tx_data_reg[4]; 4'd6: tx_pin <= tx_data_reg[5]; 4'd7: tx_pin <= tx_data_reg[6]; 4'd8: tx_pin <= tx_data_reg[7]; // 发送第7位(最高位) 4'd9: tx_pin <= 1'b1; // 发送停止位(高电平1) default: begin // 计数超出范围,发送完成 tx_state <= 1'b0; // 切换回空闲状态 tx_done <= 1'b1; // 置位发送完成标志 end endcase end end endcase end end endmodule
2. 接收模块(uart_rx.v)
功能:通过rx_pin引脚接收外部的UART串行数据,按照约定的波特率和帧结构,将串行数据转换成8位并行数据,接收完成后给出“接收完成”标志,同时输出并行数据供FPGA内部使用。
关键优化:对rx_pin引脚打两拍(消抖+同步),解决外部异步信号的亚稳态问题;在每个bit的中间位置采样,提高采样准确性,避免边沿干扰。
module uart_rx #( parameter CLK_FREQ = 50_000_000, // 系统时钟频率(50MHz) parameter BAUD_RATE = 9600 // 约定波特率(9600) ) ( input clk, // 系统时钟(输入) input rst_n, // 低电平复位(输入,复位时停止接收) input rx_pin, // UART接收引脚(输入,串行数据) output reg [7:0] rx_data, // 接收完成的8位并行数据(输出) output reg rx_done // 接收完成标志(输出,高电平表示接收有效) ); // 波特率相关参数计算 localparam BAUD_DIV = CLK_FREQ / BAUD_RATE; // 分频系数,50_000_000 / 9600 ≈ 5208 localparam BAUD_DIV_BIT = $clog2(BAUD_DIV); // 分频系数所需寄存器位数 localparam BAUD_MID = BAUD_DIV / 2; // 采样点(每个bit的中间位置,提高采样准确性) // 内部寄存器定义 reg [BAUD_DIV_BIT-1:0] baud_cnt; // 波特率计数器 reg [3:0] bit_cnt; // 数据位计数器(0-9:起始位+8位数据+停止位) reg rx_state; // 接收状态寄存器(0:空闲,1:接收中) reg rx_pin_reg1, rx_pin_reg2; // 输入引脚打两拍(同步+消抖,解决亚稳态) wire rx_pin_sync; // 同步后的RX引脚信号(消除亚稳态后的信号) // 引脚同步逻辑(解决外部异步信号的亚稳态问题,FPGA设计必加) always @(posedge clk or negedge rst_n) begin if(!rst_n) begin rx_pin_reg1 <= 1'b1; rx_pin_reg2 <= 1'b1; end else begin rx_pin_reg1 <= rx_pin; // 第一拍缓存 rx_pin_reg2 <= rx_pin_reg1; // 第二拍缓存,输出同步后的信号 end end assign rx_pin_sync = rx_pin_reg2; // 同步后的RX信号,用于后续采样 // 状态机逻辑(核心:控制UART帧结构的接收和采样) always @(posedge clk or negedge rst_n) begin if(!rst_n) begin // 复位状态:所有寄存器清零 rx_state <= 1'b0; baud_cnt <= 0; bit_cnt <= 0; rx_data <= 8'd0; rx_done <= 1'b0; end else begin rx_done <= 1'b0; // 默认清零接收完成标志 case(rx_state) 1'b0: begin // 空闲状态:等待起始位(低电平0) baud_cnt <= 0; bit_cnt <= 0; if(rx_pin_sync == 1'b0) begin // 检测到起始位,开始接收 rx_state <= 1'b1; // 切换到接收状态 end end 1'b1: begin // 接收状态:采样串行数据,转换为并行数据 baud_cnt <= baud_cnt + 1'b1; // 波特率计数器累加 if(baud_cnt == BAUD_DIV – 1) begin // 计数到分频系数,完成1个bit的接收时间 baud_cnt<= 0; // 波特率计数器清零 bit_cnt <= bit_cnt + 1'b1; // 数据位计数器累加 end // 在每个bit的中间位置采样(重点优化,提高采样准确性) if(baud_cnt == BAUD_MID – 1) begin case(bit_cnt) 4'd0: ; // 起始位,不采样(仅用于同步) 4'd1: rx_data[0] <= rx_pin_sync; // 采样第0位(最低位) 4'd2: rx_data[1] <= rx_pin_sync; 4'd3: rx_data[2] <= rx_pin_sync; 4'd4: rx_data[3] <= rx_pin_sync; 4'd5: rx_data[4] <= rx_pin_sync; 4'd6: rx_data[5] <= rx_pin_sync; 4'd7: rx_data[6] <= rx_pin_sync; 4'd8: rx_data[7] <= rx_pin_sync; // 采样第7位(最高位) 4'd9: begin // 停止位,判断接收是否有效 rx_state <= 1'b0; // 切换回空闲状态 if(rx_pin_sync == 1'b1) begin // 停止位为高电平,说明接收有效 rx_done <= 1'b1; // 置位接收完成标志 end end endcase end end endcase end end endmodule
3. 顶层模块(uart_top.v)
功能:将发送模块(uart_tx)和接收模块(uart_rx)例化,统一所有接口,方便下载到FPGA开发板使用。顶层模块的接口可直接对应FPGA的引脚(时钟、复位、TX/RX引脚等),无需修改内部逻辑,仅需根据开发板修改引脚约束即可。
module uart_top #( parameter CLK_FREQ = 50_000_000, // 系统时钟频率(50MHz) parameter BAUD_RATE = 9600 // 约定波特率(9600) ) ( input clk, // 系统时钟(输入,50MHz) input rst_n, // 低电平复位(输入,建议接FPGA的复位按键) input rx_pin, // UART_RX引脚(输入,接CH340的TX引脚) output tx_pin, // UART_TX引脚(输出,接CH340的RX引脚) input [7:0] tx_data_in, // 外部输入待发送数据(输入,可接拨码开关、按键等) input tx_en_in, // 外部发送使能(输入,可接按键,按下触发发送) output tx_done_out,// 发送完成输出(输出,可接LED,提示发送完成) output [7:0] rx_data_out,// 接收数据输出(输出,可接LED、数码管,显示接收数据) output rx_done_out // 接收完成输出(输出,可接LED,提示接收完成) ); // 例化发送模块(将发送模块接入顶层,连接对应接口) uart_tx #( .CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE) ) uart_tx_inst ( .clk(clk), .rst_n(rst_n), .tx_data(tx_data_in), .tx_en(tx_en_in), .tx_done(tx_done_out), .tx_pin(tx_pin) ); // 例化接收模块(将接收模块接入顶层,连接对应接口) uart_rx #( .CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE) ) uart_rx_inst ( .clk(clk), .rst_n(rst_n), .rx_pin(rx_pin), .rx_data(rx_data_out), .rx_done(rx_done_out) ); endmodule
三、代码验证方法(新手必看,快速上手)
代码写好后,不能直接下载到FPGA,需要先仿真验证逻辑正确性,再进行硬件验证,两步都很简单,新手跟着做就能完成。
1. 仿真验证(关键步骤,避免硬件调试踩坑)
仿真的目的是验证:发送模块能正确输出UART帧结构,接收模块能正确解析串行数据,核心步骤如下(以ModelSim为例):
-
新建仿真工程,添加uart_tx.v、uart_rx.v、uart_top.v三个文件;
-
编写测试激励(testbench):给顶层模块输入50MHz时钟、低电平复位(复位100ns后释放);
-
驱动tx_en_in为高电平,tx_data_in赋值(比如8'h55,对应二进制01010101),观察tx_pin输出是否符合“1位起始位+8位数据+1位停止位”的帧结构;
-
向rx_pin输入模拟的UART串行数据(比如8'hAA对应的串行帧),观察rx_data_out是否能正确解析出8'hAA,rx_done_out是否能正常置位。
小技巧:仿真时重点观察“波特率计数器”和“数据位计数器”,确保计数节奏正确,采样位置在每个bit的中间。
2. 硬件验证(最终验证,看到实际效果)
硬件验证需要准备:FPGA开发板(任意型号均可)、CH340 USB转TTL模块(实现电脑与FPGA的UART通信)、杜邦线、串口助手(SSCOM、SecureCRT均可),步骤如下:
引脚约束:根据自己的FPGA开发板,编写引脚约束文件,将顶层模块的clk、rst_n、rx_pin、tx_pin等接口,绑定到FPGA的实际引脚上(比如clk接50MHz时钟引脚,rst_n接复位按键);
接线:用杜邦线连接FPGA和CH340模块——FPGA的tx_pin接CH340的RX引脚,FPGA的rx_pin接CH340的TX引脚,FPGA和CH340共地(重要,否则通信失败);
下载程序:将编译好的程序(bit文件)下载到FPGA开发板;
串口助手配置:打开串口助手,选择CH340对应的COM口,波特率设为9600,数据位8位,停止位1位,无校验,点击“打开串口”;
测试通信:
FPGA发送数据:驱动tx_en_in为高电平(按下按键),tx_data_in赋值(比如拨码开关调到8'h55),串口助手会接收到对应的数据;
FPGA接收数据:用串口助手发送数据(比如输入“123”),FPGA的rx_data_out会解析出对应的数据,rx_done_out置位(LED点亮)。
四、关键设计要点与常见问题(新手避坑指南)
1. 关键设计要点(保证通信稳定)
-
引脚同步:接收模块中对rx_pin打两拍,是FPGA设计中处理外部异步信号的标准操作,能有效解决亚稳态问题,避免接收数据出错;
-
采样位置:接收时在每个bit的中间位置采样,是提高采样准确性的关键,能避免起始位、停止位的边沿干扰;
-
状态机设计:收发模块均采用简单的两段式状态机(空闲+工作),逻辑清晰,易于调试,新手不要用复杂的状态机;
-
复位处理:所有寄存器都必须有复位逻辑,保证FPGA上电后,所有模块都处于正确的初始状态(比如tx_pin置高、状态为空闲)。
2. 常见问题与解决方法
-
问题1:串口助手接收不到数据,或接收的数据乱码? 解决:检查收发两端波特率是否一致;检查接线是否正确(FPGA TX→CH340 RX,FPGA RX→CH340 TX);检查FPGA时钟是否为50MHz(若时钟不同,修改分频系数)。
-
问题2:接收完成标志(rx_done)不置位? 解决:检查rx_pin引脚约束是否正确;检查采样点是否设置正确(BAUD_MID = BAUD_DIV / 2);检查停止位是否为高电平(若停止位采样错误,接收会被判定为无效)。
-
问题3:发送数据不稳定,偶尔出错? 解决:增加数据缓存寄存器(如tx_data_reg),避免发送过程中待发送数据被修改;检查复位信号是否稳定(避免复位信号抖动)。
五、总结与扩展
本文完整讲解了FPGA实现UART串口通信的核心原理(重点解析波特率)、完整Verilog代码实例,以及仿真/硬件验证方法,新手按照步骤操作,就能快速实现FPGA与电脑的UART通信,代码可直接复用在实际项目中。
核心总结:FPGA实现UART的关键,是“时钟分频”(匹配波特率)和“状态机”(控制帧结构),再加上简单的同步、采样优化,就能保证通信稳定。
扩展建议(进阶学习):
-
修改波特率:只需修改顶层模块的BAUD_RATE参数,代码会自动计算分频系数,无需手动修改其他逻辑;
-
添加校验位:在发送模块中添加奇偶校验逻辑,接收模块中添加校验判断,提高通信的可靠性;
-
实现中断机制:在接收完成、发送完成后,添加中断信号,配合FPGA的中断控制器,实现更高效的数据处理;
-
多字节收发:扩展代码,实现连续多字节的发送和接收,支持字符串、数组的传输。
如果在调试过程中遇到问题,可留言讨论,后续会补充测试激励(testbench)代码,帮助新手快速完成仿真验证。
网硕互联帮助中心




评论前必须登录!
注册