FPGA-VIVADO学习十:串口通信接收
本文对于。
本文对于串口通信接收的讲解主要分为下面3个部分展开(具体见右侧目录):
- 串口接收逻辑设计要点分析
- 串口接收逻辑的Verilog设计与验证(代码部分)
- 串口接收设计优化
一、串口接收逻辑设计要点分析
设计一个UART串口接收逻辑控制器,要求:
- 能够接收8位,无校验位,1位停止位的UART串口数据
- 能够通过一定的方式修改波特率(parameter)
- 每接收完一个数据,将接收到的数据结果显示到开发板的8位LED灯上
- 每接收完一个数据,将开发板上的LED8状态翻转一次(注意是翻转,不是闪烁,效果同串口发送逻辑的一样)
要点1:
对于串口接收来说,我们应该怎么样从串行数据中准确的获取到每一位数据?
借助计数器,像上一节一样

要点2:
如何检测串口接收数据的起始位的下降沿?

检测下降沿可以参考下面电路,当且仅当上一次输出Q为1 且当前输出D为0时 下降沿检测电路输出为1,检测上升沿与此同理。

要点3:
如何设计波特率分频计数器使能逻辑?
我们可能会遇到两种情况
第一种在结束时不再接收到信号

第二种就是遇到毛刺,这种情况要排除

要点4:
如何知道当前所读取信号位于UART信号的哪一位?
借助位计数器

要点5:
如何实现位接收逻辑?
借助case

要点6:
产生一个标志信号,能在每次接收完毕后,通知外界数据接收完成
假如我在采样停止位的时候,采样到了低电平怎么办:
恿知外界完成了一个数据的接收,不产生标志信号做法1:该数据直接舍弃,也就是不,
做法2:当成新的起始位来解
做法3:当成正确的数据通知外部
做法4:通知外界接收完成,但同时也输出一个错误标志信号,告知外界,此次接收的数据是错误的
二、串口接收逻辑的Verilog设计与验证
verilog代码
`timescale 1ns / 1ps // 定义仿真时间单位/精度
module uart_byte_rx(
input Clk, // 系统时钟输入
input Reset_n, // 低电平有效复位信号
input uart_rx, // UART 串行接收数据线
output reg Rx_Done, // 接收完成标志信号
output reg [7:0] Rx_Data // 接收到的8位数据
);
// 参数定义
parameter CLOCK_FREQ = 50000000; // 系统时钟频率50MHz
parameter BAUD = 9600; // 波特率设置
parameter MCNT_BAUD = CLOCK_FREQ / BAUD - 1; // 计算波特率分频计数值
// 内部寄存器声明
reg [29:0] baud_div_cnt; // 波特率分频计数器
reg en_baud_cnt; // 波特率计数器使能信号
reg [3:0] bit_cnt; // 接收位计数器(0-9共10个状态)
reg r_uart_rx; // 经过同步后的UART输入信号
wire w_Rx_Done; // 接收完成中间信号
wire nedge_uart_rx; // UART输入下降沿检测信号
// 同步寄存器用于消除亚稳态
reg dffO_uart_rx; // 第一级同步触发器
reg dff1_uart_rx; // 第二级同步触发器
// 波特率计数器逻辑
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
baud_div_cnt <= 0; // 复位时计数器清零
else if(en_baud_cnt) begin
if(baud_div_cnt == MCNT_BAUD)
baud_div_cnt <= 0; // 达到最大值时归零
else
baud_div_cnt <= baud_div_cnt + 1'd1; // 正常计数
end
else
baud_div_cnt <= 0; // 计数器未使能时保持清零
// UART输入信号同步和边沿检测逻辑
always@(posedge Clk)
dffO_uart_rx <= uart_rx; // 第一级同步
always@(posedge Clk)
dff1_uart_rx <= dffO_uart_rx; // 第二级同步
always@(posedge Clk)
r_uart_rx <= dff1_uart_rx; // 第三级寄存用于边沿检测
assign nedge_uart_rx = (dff1_uart_rx == 0) && (r_uart_rx == 1); // 检测下降沿
// 波特率计数器使能控制逻辑
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
en_baud_cnt <= 0; // 复位时禁用计数器
else if(nedge_uart_rx) // 检测到起始位下降沿
en_baud_cnt <= 1; // 启动波特率计数器
else if((baud_div_cnt == MCNT_BAUD/2) && (bit_cnt == 0) && (dff1_uart_rx == 1))
en_baud_cnt <= 0; // 起始位验证失败(非持续低电平)时停止
else if((baud_div_cnt == MCNT_BAUD) && (bit_cnt == 9))
en_baud_cnt <= 0; // 接收完成(10位)后停止计数器
// 接收位计数器逻辑
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
bit_cnt <= 0; // 复位时位计数器清零
else if(baud_div_cnt == MCNT_BAUD) begin // 每个波特率周期
if(bit_cnt == 9)
bit_cnt <= 0; // 接收完10位后归零
else
bit_cnt <= bit_cnt + 1'd1; // 正常递增位计数器
end
// 数据位接收逻辑(在波特率周期中点采样)
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
Rx_Data <= 8'd0; // 复位时数据寄存器清零
else if(baud_div_cnt == MCNT_BAUD/2) begin // 在波特率周期中间采样
case(bit_cnt)
1: Rx_Data[0] <= dff1_uart_rx; // 接收第1位数据(LSB)
2: Rx_Data[1] <= dff1_uart_rx; // 接收第2位数据
3: Rx_Data[2] <= dff1_uart_rx; // 接收第3位数据
4: Rx_Data[3] <= dff1_uart_rx; // 接收第4位数据
5: Rx_Data[4] <= dff1_uart_rx; // 接收第5位数据
6: Rx_Data[5] <= dff1_uart_rx; // 接收第6位数据
7: Rx_Data[6] <= dff1_uart_rx; // 接收第7位数据
8: Rx_Data[7] <= dff1_uart_rx; // 接收第8位数据(MSB)
default: Rx_Data <= Rx_Data; // 其他状态保持数据不变
endcase
end
// 接收完成标志生成逻辑
assign w_Rx_Done = ((baud_div_cnt == MCNT_BAUD) && (bit_cnt == 9)); // 第10位(停止位)完成
always@(posedge Clk)
if(!Reset_n)
Rx_Done <= 0; // 复位时完成标志清零
else
Rx_Done <= w_Rx_Done; // 同步输出完成信号
endmodule
再回顾一下UART接收模块的系统架构:
一、模块接口
输入:
-
系统时钟(Clk) // 50MHz主时钟
-
低电平复位信号(Reset_n) // 全局异步复位
-
串行数据输入(uart_rx) // RS-232标准信号
输出:
-
接收完成标志(Rx_Done) // 高电平有效脉冲信号
-
8位并行数据(Rx_Data) // 有效数据输出
二、核心功能模块
-
信号同步与边沿检测单元
-
三级同步触发器链:消除亚稳态
-
下降沿检测电路:识别起始位(1→0跳变)
-
-
波特率生成系统
-
可配置分频器:CLOCK_FREQ/BAUD-1
-
16位计数器:产生精确的波特率时钟
-
-
接收状态控制器
-
位计数器:0(起始位)~9(停止位)状态机
-
使能控制逻辑:管理采样时序
-
-
数据采样单元
-
中点采样机制:每位中心点采样
-
数据移位寄存器:串并转换存储
-
-
校验与完成系统
-
起始位验证:检测虚假起始信号
-
停止位检测:确认帧结束
-
完成信号生成:输出有效脉冲
-
三、工作流程
-
起始检测阶段
-
持续监测uart_rx信号
-
检测到下降沿后启动接收流程
-
-
波特率同步阶段
-
启动波特率分频计数器
-
在起始位中点验证持续低电平
-
-
数据接收阶段
-
每波特率周期递增位计数器
-
在数据位中点进行8次采样
-
完成8位数据存储
-
-
帧结束处理
-
检测停止位高电平
-
生成接收完成信号
-
复位内部状态机
-
四、关键设计特性
-
可靠性设计
-
三级信号同步:消除亚稳态
-
中点采样:抗信号抖动
-
起始位验证:防误触发
-
-
灵活配置
-
可调波特率:通过参数修改
-
自动计算分频系数:CLOCK_FREQ/BAUD
-
-
错误处理机制
-
起始位异常终止:检测到非持续低电平立即中止接收
-
超时保护:内置状态机超时复位
-
五、时序特征
-
典型帧处理周期
-
10个波特率周期(1起始+8数据+1停止)
-
9600bps时约1.04ms/字节
-
-
信号延迟
-
完成信号相对停止位中点延迟1个系统时钟周期
-
数据输出与完成信号同步更新
-
六、性能参数
-
最大理论速率
-
取决于系统时钟频率
-
50MHz系统时钟支持最高3.125Mbps
-
-
资源占用
-
约32个寄存器
-
无乘法器/分频器特殊资源
-
仿真代码
`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:
//
// Create Date: 2025/03/28 22:46:36
// Design Name:
// Module Name: uart_byte_rx_tb
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module uart_byte_rx_tb(
);
reg Clk;
reg Reset_n;
reg uart_rx;
wire [7:0]Rx_Data;
wire Rx_Done;
uart_byte_rx uart_byte_rx(
.Clk(Clk),
.Reset_n(Reset_n),
.uart_rx(uart_rx),
.Rx_Data(Rx_Data),
.Rx_Done(Rx_Done)
);
initial Clk = 1;
always#10 Clk = ~Clk;
initial begin
Reset_n = 0;
uart_rx = 1;
#201;
Reset_n = 1;
#200;
uart_rx=0; #(5208*20);
uart_rx=1; #(5208*20);
uart_rx=0; #(5208*20);
uart_rx=1; #(5208*20);
uart_rx=0; #(5208*20);
uart_rx=1; #(5208*20);
uart_rx=0; #(5208*20);
uart_rx=1; #(5208*20);
uart_rx=0; #(5208*20);
uart_rx=1; #(5208*20);
#(5208*20*10);
uart_rx=0; #(5208*20);
uart_rx=0; #(5208*20);
uart_rx=1; #(5208*20);
uart_rx=1; #(5208*20);
uart_rx=0; #(5208*20);
uart_rx=0; #(5208*20);
uart_rx=1; #(5208*20);
uart_rx=1; #(5208*20);
uart_rx=0; #(5208*20);
uart_rx=1; #(5208*20);
#(5208*20*10);
uart_rx=0; #(5208*20);
uart_rx=0; #(5208*20);
uart_rx=0; #(5208*20);
uart_rx=0; #(5208*20);
uart_rx=0; #(5208*20);
uart_rx=1; #(5208*20);
uart_rx=1; #(5208*20);
uart_rx=1; #(5208*20);
uart_rx=1; #(5208*20);
uart_rx=1; #(5208*20);
#(5208*20*10);
$stop;
end
endmodule
仿真结果

三、串口接收设计优化
可能存在的问题

通常来说,接收器接收一个数据所耗费的时间比发送器长100ns,就会出现发送方发送完第一个数据后,并立即开始发送第二个数据,而接收方还处于接收上一个数据的状态,忽略了新一个数据的起始位,导致第二个数据接收失败。
想象你在餐厅传菜
-
原版设计:服务员必须把菜完全放到桌上(波特率周期结束),才能回头接新菜
-
问题:当厨房快速出菜时,服务员回头时新菜已经掉地上(错过起始位)
优化后的设计:
-
服务员在菜盘刚进桌边(半周期时)就松手
-
利用菜滑向桌心的惯性时间(剩余半周期):
-
确认菜已上桌 ✅
-
提前转身准备接新菜 👐
-
为什么有效:
| 步骤 | 原版耗时 | 优化版耗时 | 赢得的时间用途 |
|---|---|---|---|
| 完成当前任务 | 10秒 | 5秒 | 提前准备迎接新任务 |
| 准备新任务 | 0秒 | 5秒 | 能及时响应下一个请求 |
为了解决这个问题,可以为发送器提供0.5位的容错时间

原来的代码,也就是接收完成标志信号
((baud_div_cnt == MCNT_BAUD)&& (bit_cnt==9))
将条件修改为
((baud_div_cnt == MCNT_BAUD/2)&& (bit_cnt==9))
同时还要修改位计数器逻辑
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
bit_cnt<= 0;
else if(baud_div_cnt==MCNT_BAUD)begin
if(bit_cnt==9)
bit_cnt<=0;
else
bit_cnt<=bit_cnt+1'd1;
end
修改为
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
bit_cnt<= 0;
else if((bit_cnt==9)&&(baud_div_cnt==MCNT_BAUD/2))
bit_cnt<= 0;
else if(baud_div_cnt==MCNT_BAUD)
bit_cnt<=bit_cnt+1'd1;
这样就能兼容更多的硬件了,短距离,非强干扰情况下适用 。
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)