串口接收多字节 Verilog程序
在FPGA开发中,我们经常需要与PC、MCU或其他设备通过UART进行通信。虽然单字节接收看似简单,但在实际项目中,大多数协议都要求一次性接收多个字节——比如一条包含命令、地址和校验的完整数据帧。如果只用基础的UART接收模块,主控逻辑就得频繁响应中断或轮询,不仅效率低,还容易因响应不及时导致后续数据被覆盖。
有没有一种方式,能让FPGA自动把一整帧数据收完再通知CPU?答案是肯定的: 设计一个多字节串口接收器 。它不仅能提升通信可靠性,还能显著降低系统负担。
本文将带你从零实现一个参数化、高鲁棒性的多字节UART接收模块。我们将深入探讨采样机制、状态机设计、缓冲管理以及抗干扰策略,并提供可综合的Verilog代码,适用于Xilinx、Intel等主流FPGA平台。
异步串行通信的本质:时间同步的艺术
UART之所以被称为“异步”,是因为它没有专用时钟线。发送端和接收端仅靠事先约定的波特率来对齐每一位数据。这意味着,接收端必须精准地知道每个比特的起始和结束位置。
以115200 bps为例,每位持续时间为约8.68 μs。为了准确判断电平值,通常采用 16倍过采样 ——即每比特采样16次,在中间时刻(第7~9次)取多数表决结果作为该位最终值。这种做法能有效抵抗毛刺和噪声。
假设你的FPGA主频为50 MHz,那么分频系数就是:
50_000_000 / (115200 × 16) ≈ 27
也就是说,每27个系统时钟周期产生一次采样脉冲。这个数值越接近整数,波特率误差就越小。一般建议控制在±3%以内,否则长期累积可能导致错位。
更关键的是,我们必须识别出 起始位 (下降沿)才能开始同步。一旦错过,整个字节都会错乱。因此,很多设计会在进入接收前先检测rx信号是否为空闲高电平,避免误触发。
多字节接收的核心挑战
比起单字节接收,多字节处理面临几个新问题:
-
如何确定帧边界?
是固定长度?还是遇到特定结束符(如0x0D 0x0A)才停止? -
收到的数据存在哪?
如果直接输出,主控来不及读取就会丢失;需要缓存。 -
何时通知外部逻辑?
每收到一字节就中断?还是整帧收完再发完成信号?
最合理的方案是:
使用内部缓冲区 + 完成标志机制
。当指定数量的字节全部接收完毕后,拉高
frame_done
信号,由主控逻辑主动读取所有数据并清除标志。这样既保证了数据完整性,又减少了中断频率。
此外,还要考虑错误处理:
- 停止位不是高电平 → 帧错误
- 新数据到来时缓冲区未清空 → 溢出
- 连续接收超时 → 可设置超时计数器防止死锁
这些细节决定了模块在真实环境中的稳定性。
状态机驱动的设计思路
整个接收过程可以用一个简洁的状态机来描述:
typedef enum logic [2:0] {
IDLE,
START,
DATA,
STOP
} state_t;
-
IDLE:等待起始位下降沿 -
START:确认起始位有效,准备接收数据 -
DATA:逐位采样并移入移位寄存器 -
STOP:验证停止位是否为高
每一个状态都在 采样时钟的驱动下推进 。这里的关键是“何时切换状态”——我们不能依赖绝对时间,而是根据 位计数器 和 分频器归零信号 来判断是否该进入下一位。
例如,在
DATA
状态下,每当
next_bit
信号到来(表示已过完一个比特周期),就把当前采样值写入
shift_reg[bit_counter]
,然后递增
bit_counter
。当计数达到7(8位数据),说明本字节已完成,下一步应进入
STOP
状态。
这种基于事件而非延时的设计,更具可移植性和精度保障。
抗干扰采样:不只是简单的边沿检测
你可能见过一些简化的UART接收代码,只在每位中间采样一次。这在理想环境下没问题,但现实中电磁干扰无处不在——尤其是工业现场或长线传输。
我们的解决方案是: 16次连续采样 + 多数表决 。
reg [15:0] sample_reg;
...
sample_reg <= {sample_reg[0], rx_in};
每次分频计数结束时,将当前rx电平移入
sample_reg
的最高位。经过16次移位后,这个寄存器就记录了该比特周期内的全部采样序列。
然后我们统计其中“1”的个数:
function reg get_sampled_bit;
input reg [SAMPLES_PER_BIT-1:0] reg_in;
integer i, sum;
begin
sum = 0;
for (i = 0; i < SAMPLES_PER_BIT; i = i + 1)
sum = sum + reg_in[i];
get_sampled_bit = (sum > (SAMPLES_PER_BIT >> 1));
end
endfunction
只要超过半数为高,就认为该位是“1”。这种方法能有效过滤短暂的尖峰脉冲,即使有几个采样点出错也不影响最终判断。
当然,也可以进一步优化,比如跳过首尾几个采样点(避开跳变沿不稳定区域),只取中间9~12次做表决,效果更好。
缓冲区管理:静态数组还是FIFO?
对于固定长度帧(如4字节指令包),使用寄存器数组是最轻量的选择:
reg [7:0] data_buffer [0:FRAME_LEN-1];
reg [$clog2(FRAME_LEN)-1:0] buf_ptr;
每收到一个有效字节,就将其存入
data_buffer[buf_ptr]
,然后
buf_ptr++
。当
buf_ptr == FRAME_LEN - 1
时,说明最后一字节已存入,此时拉高
frame_done
。
优点是资源消耗极小,适合小型FPGA;缺点是长度固定,不够灵活。
如果你希望支持可变长度帧(比如直到收到0xFF为止),那就得引入 超时机制 或 特殊字符检测 。例如:
if (shift_reg == 8'hFF) begin
frame_done <= 1;
// 不再递增buf_ptr,直接结束
end
或者配合一个自由运行的计数器,若超过一定时间未收到新字节,则强制结束当前帧。
至于异步FIFO,则更适合跨时钟域场景。比如UART使用外部24MHz晶振,而系统主频为100MHz。这时需要用双端口RAM+异步指针同步技术来安全传递数据。
完整Verilog实现(精简可综合版本)
module uart_multi_rx #(
parameter DATA_WIDTH = 8,
parameter FRAME_LEN = 4,
parameter CLK_FREQ = 50_000_000,
parameter BAUD_RATE = 115200
) (
input clk,
input rst_n,
input rx_in,
output [DATA_WIDTH-1:0] data_out,
output [3:0] byte_cnt,
output frame_done,
output data_valid,
output overflow
);
localparam SAMPLES_PER_BIT = 16;
localparam CLK_DIV = CLK_FREQ / (BAUD_RATE * SAMPLES_PER_BIT);
// 信号声明
reg [SAMPLES_PER_BIT-1:0] sample_reg;
reg [CLK_DIV-1:0] clk_divider;
reg [2:0] sample_counter;
reg [3:0] bit_counter;
reg start_flag;
reg [DATA_WIDTH-1:0] shift_reg;
reg [DATA_WIDTH-1:0] data_buffer [0:FRAME_LEN-1];
reg [$clog2(FRAME_LEN):0] buf_ptr;
reg load_data;
wire next_bit = (sample_counter == 0 && clk_divider == CLK_DIV - 1);
wire start_condition = (rx_in == 1'b0) && !start_flag;
// 分频器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
clk_divider <= 0;
else if (start_flag)
clk_divider <= (clk_divider == CLK_DIV - 1) ? 0 : clk_divider + 1;
else
clk_divider <= 0;
end
// 采样控制
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sample_counter <= 0;
sample_reg <= 0;
end else if (start_flag) begin
if (clk_divider == CLK_DIV - 1) begin
sample_reg <= {sample_reg[0], rx_in};
sample_counter <= (sample_counter == SAMPLES_PER_BIT - 1) ? 0 : sample_counter + 1;
end
end else begin
sample_counter <= 0;
sample_reg <= 0;
end
end
// 多数表决函数
function reg majority_vote(input reg [SAMPLES_PER_BIT-1:0] sreg);
integer ones;
ones = 0;
for (integer i = 0; i < SAMPLES_PER_BIT; i++)
ones = ones + sreg[i];
return (ones >= (SAMPLES_PER_BIT >> 1));
endfunction
// 状态机定义
typedef enum logic [1:0] { IDLE, START, DATA, STOP } state_t;
state_t state;
// 主状态机
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else begin
case (state)
IDLE:
if (start_condition) state <= START;
START:
if (next_bit && bit_counter == DATA_WIDTH - 1) state <= DATA;
DATA:
if (next_bit && bit_counter == DATA_WIDTH - 1) state <= STOP;
STOP:
if (next_bit) state <= IDLE;
endcase
end
end
// 控制逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_counter <= 0;
shift_reg <= 0;
start_flag <= 0;
load_data <= 0;
buf_ptr <= 0;
frame_done <= 0;
overflow <= 0;
end else begin
load_data <= 0;
case (state)
IDLE:
if (start_condition) begin
bit_counter <= 0;
start_flag <= 1;
frame_done <= 0;
end
START:
if (next_bit) begin
if (bit_counter < DATA_WIDTH - 1)
bit_counter <= bit_counter + 1;
else
bit_counter <= 0;
end
DATA:
if (next_bit) begin
shift_reg[bit_counter] <= majority_vote(sample_reg);
if (bit_counter < DATA_WIDTH - 1) begin
bit_counter <= bit_counter + 1;
end else begin
data_buffer[buf_ptr] <= shift_reg;
load_data <= 1;
if (buf_ptr == FRAME_LEN - 1) begin
frame_done <= 1;
buf_ptr <= 0;
end else begin
buf_ptr <= buf_ptr + 1;
end
end
end
STOP:
if (next_bit) begin
if (!majority_vote(sample_reg))
overflow <= 1; // 停止位错误
start_flag <= 0;
end
endcase
end
end
// 输出映射
assign data_out = data_buffer[byte_cnt];
assign byte_cnt = buf_ptr;
assign data_valid = load_data;
endmodule
实际应用中的工程技巧
1. 输入信号预处理
原始
rx_in
可能带有抖动或亚稳态风险。建议加两级D触发器同步:
reg rx_sync1, rx_sync2;
always @(posedge clk) begin
rx_sync1 <= rx_in;
rx_sync2 <= rx_sync1;
end
后续逻辑使用
rx_sync2
作为输入。
2. 参数化配置提升复用性
将
FRAME_LEN
、
BAUD_RATE
设为参数后,同一份代码可用于不同项目。甚至可以通过添加配置寄存器,实现运行时动态调整帧长。
3. 调试建议
在关键节点插入ILA(Xilinx Integrated Logic Analyzer)探针,特别是:
-
rx_in
和同步后的信号
-
state
状态变量
-
frame_done
和
buf_ptr
这样可以在Vivado中实时观察波形,快速定位起始位误判、采样偏移等问题。
4. 避免常见陷阱
- 分频系数非整数 :会导致相位漂移,建议选择能整除的主频/波特率组合
-
未清空缓冲区就启动新接收
:应检查
frame_done是否已被软件清除 - 忽略停止位验证 :某些干扰可能让停止位变低,此时应视为帧错误
结语
一个多字节UART接收器看似只是个小功能模块,但它背后涉及了时序控制、抗干扰设计、状态机建模等多个数字系统核心概念。通过合理运用16倍过采样、多数表决、缓冲管理和完成标志机制,我们可以构建出一个稳定可靠的通信前端。
这类模块特别适合用于工业控制、传感器采集、配置加载等场景。未来还可在此基础上扩展:
- 支持奇偶校验
- 添加CRC校验单元
- 封装为AXI-Lite从设备,便于集成到MicroBlaze/Zynq系统中
- 结合DMA实现大批量数据零干预传输
掌握这种底层通信设计能力,意味着你不仅能“让东西工作”,更能“让它可靠地工作”。这才是FPGA工程师真正的价值所在。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
442

被折叠的 条评论
为什么被折叠?



