FPGA实战:用移位寄存器实现简易UART接收器
在数字电路设计中,移位寄存器是一个看似简单却功能强大的基础模块。它不仅能用于数据缓冲,还能实现串并转换、数据延迟等实用功能。今天我们就来探索如何用Verilog编写一个基于移位寄存器的简易UART接收器,这个项目非常适合已经掌握Verilog基础语法、想要进阶学习FPGA实际应用的开发者。
1. UART协议与移位寄存器原理
UART(通用异步收发传输器)是最常用的串行通信协议之一,其核心在于将并行数据转换为串行数据发送,以及将接收到的串行数据还原为并行数据。这正是移位寄存器大显身手的地方。
UART帧格式通常包括:
- 1个起始位(低电平)
- 8个数据位(LSB先发送)
- 可选的奇偶校验位
- 1-2个停止位(高电平)
移位寄存器在这里的关键作用是:
- 按位接收串行数据
- 通过移位操作将串行数据转换为并行数据
- 在接收完整帧后输出8位并行数据
波特率匹配是UART通信的另一个关键点。常见的波特率如9600、115200等,意味着每位数据的持续时间是1/波特率秒。我们需要设计一个波特率生成器来准确采样每位数据。
2. Verilog模块设计与代码实现
2.1 顶层模块设计
我们的UART接收器需要以下关键信号:
- 系统时钟(clk)
- 复位信号(rst_n)
- 串行数据输入(rx)
- 并行数据输出(data_out)
- 数据有效指示信号(data_valid)
module uart_rx ( input clk, // 系统时钟(假设50MHz) input rst_n, // 低电平复位 input rx, // 串行数据输入 output reg [7:0] data_out, // 并行数据输出 output reg data_valid // 数据有效标志 ); // 内部信号定义将在这里添加 endmodule2.2 波特率生成与采样控制
对于50MHz系统时钟和115200波特率,每位数据持续约434个时钟周期。我们需要一个计数器来实现精确采样:
localparam CLK_FREQ = 50_000_000; // 50MHz localparam BAUD_RATE = 115200; localparam BAUD_CNT_MAX = CLK_FREQ / BAUD_RATE; reg [15:0] baud_cnt; // 波特率计数器 reg baud_tick; // 波特率采样脉冲 reg [3:0] bit_cnt; // 已接收位数计数 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin baud_cnt <= 0; baud_tick <= 0; end else begin if (baud_cnt == BAUD_CNT_MAX - 1) begin baud_cnt <= 0; baud_tick <= 1; end else begin baud_cnt <= baud_cnt + 1; baud_tick <= 0; end end end2.3 移位寄存器核心实现
这里就是移位寄存器发挥作用的地方。我们使用一个8位寄存器来逐步存储接收到的数据:
reg [7:0] shift_reg; // 8位移位寄存器 reg rx_done; // 接收完成标志 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin shift_reg <= 8'b0; bit_cnt <= 0; rx_done <= 0; end else if (baud_tick) begin if (bit_cnt == 0) begin // 等待起始位 if (!rx) begin bit_cnt <= bit_cnt + 1; end end else if (bit_cnt <= 8) begin // 接收数据位(LSB first) shift_reg <= {rx, shift_reg[7:1]}; bit_cnt <= bit_cnt + 1; end else begin // 等待停止位 rx_done <= 1; bit_cnt <= 0; end end else begin rx_done <= 0; end end // 数据输出控制 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin data_out <= 8'b0; data_valid <= 0; end else if (rx_done) begin data_out <= shift_reg; data_valid <= 1; end else begin data_valid <= 0; end end3. ISE/Vivado工程实现步骤
3.1 创建新工程
- 打开Vivado,选择"Create Project"
- 设置项目名称和位置
- 选择项目类型为RTL Project
- 添加已有的Verilog文件或创建新文件
- 选择目标FPGA器件型号
3.2 添加设计约束文件
创建XDC约束文件,定义时钟和端口约束:
# 时钟约束 create_clock -period 20.000 -name clk [get_ports clk] # 输入输出约束 set_property PACKAGE_PIN "E3" [get_ports clk] set_property PACKAGE_PIN "D9" [get_ports rst_n] set_property PACKAGE_PIN "A10" [get_ports rx] set_property PACKAGE_PIN "B10" [get_ports {data_out[0]}] ... set_property IOSTANDARD LVCMOS33 [get_ports {*}]3.3 综合与实现
- 点击"Run Synthesis"进行综合
- 综合完成后选择"Run Implementation"
- 实现完成后生成比特流文件
4. 仿真验证与调试技巧
4.1 测试平台设计
创建一个测试模块来验证UART接收器的功能:
`timescale 1ns/1ps module uart_rx_tb; reg clk, rst_n, rx; wire [7:0] data_out; wire data_valid; // 实例化被测模块 uart_rx uut ( .clk(clk), .rst_n(rst_n), .rx(rx), .data_out(data_out), .data_valid(data_valid) ); // 时钟生成 initial begin clk = 0; forever #10 clk = ~clk; // 50MHz时钟 end // 测试激励 initial begin rst_n = 0; rx = 1; // 空闲状态为高电平 #100; rst_n = 1; // 发送数据0x55 (01010101) #1000; rx = 0; // 起始位 #8680; // 1/115200秒 ≈ 8680ns rx = 1; // bit0 #8680; rx = 0; // bit1 #8680; rx = 1; // bit2 #8680; rx = 0; // bit3 #8680; rx = 1; // bit4 #8680; rx = 0; // bit5 #8680; rx = 1; // bit6 #8680; rx = 0; // bit7 #8680; rx = 1; // 停止位 #20000; $finish; end endmodule4.2 常见问题与调试方法
在实际实现中可能会遇到以下问题:
数据采样不准确:
- 确保波特率计数器设置正确
- 尝试在数据位中间采样(如计数器达到一半时采样)
接收数据错位:
- 检查起始位检测逻辑
- 验证移位方向是否正确(LSB first)
时序违例:
- 添加适当的寄存器流水
- 检查时钟约束是否设置正确
调试技巧:使用Vivado的ILA(集成逻辑分析仪)实时观察内部信号,特别是移位寄存器的值和状态机变化。
5. 性能优化与扩展思路
5.1 波特率自适应
可以通过检测起始位下降沿的间隔来自动计算波特率:
reg [15:0] start_edge_cnt; reg [15:0] measured_baud_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin start_edge_cnt <= 0; end else if (!rx && bit_cnt == 0) begin // 检测到起始位 if (start_edge_cnt != 0) begin measured_baud_cnt <= start_edge_cnt; end start_edge_cnt <= 0; end else begin start_edge_cnt <= start_edge_cnt + 1; end end5.2 添加奇偶校验
扩展设计以支持奇偶校验:
reg parity_bit; reg parity_error; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin parity_bit <= 0; parity_error <= 0; end else if (baud_tick && bit_cnt == 9) begin // 假设第9位是奇偶校验位 parity_bit <= rx; // 计算接收数据的奇偶性 parity_error <= (^shift_reg) != parity_bit; end end5.3 多字节FIFO缓冲
对于高速数据流,可以添加FIFO缓冲:
reg [7:0] fifo [0:15]; // 16字节FIFO reg [3:0] wr_ptr, rd_ptr; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin wr_ptr <= 0; rd_ptr <= 0; end else if (data_valid) begin fifo[wr_ptr] <= data_out; wr_ptr <= wr_ptr + 1; end end在实际项目中,这个基于移位寄存器的UART接收器已经能够处理基本的串行通信需求。通过这个练习,我们不仅掌握了移位寄存器的实际应用,还深入理解了UART协议和FPGA时序设计的关键点。