在Vivado/Quartus里一步步搭建ADC到UART的数据通路:从模块例化到ModelSim仿真验证全流程
从ADC到UART的FPGA数据通路实战:Vivado/Quartus全流程开发指南
在嵌入式系统开发中,将模拟信号通过ADC转换为数字数据,再经由UART传输到上位机是一个经典场景。这个看似简单的数据通路背后,隐藏着时钟域同步、数据缓冲、协议转换等关键技术挑战。本文将带您使用Vivado/Quartus工具链,从模块例化到ModelSim仿真验证,完整实现这一数据通路。
1. 系统架构设计与工具准备
1.1 数据通路整体架构
一个典型的ADC到UART数据通路包含三个核心组件:
- ADC接口模块:负责与ADC芯片通信,控制采样时序并读取转换结果
- FIFO缓冲器:解决ADC采样速率与UART发送速率不匹配的问题
- UART发送模块:将缓冲数据按照串口协议发送给上位机
这三个模块通过数据总线和控制信号相互连接,形成完整的数据流。在FPGA中,我们需要用Verilog或VHDL描述这些模块的行为,并通过顶层模块将它们实例化连接。
1.2 开发环境配置
根据您使用的FPGA平台,需要准备相应的开发工具:
| 工具类型 | Xilinx平台 | Intel平台 |
|---|---|---|
| 开发环境 | Vivado | Quartus Prime |
| 仿真工具 | ModelSim/QuestaSim | ModelSim-Intel |
| 硬件调试工具 | ILA(Integrated Logic Analyzer) | SignalTap Logic Analyzer |
硬件准备清单:
- FPGA开发板(如Xilinx Artix-7或Intel Cyclone IV系列)
- ADC芯片(如ADS7886等SPI接口ADC)
- USB转UART模块(如CH340、CP2102等)
- 必要的连接线和电源
2. 模块设计与实现
2.1 ADC接口模块设计
ADC接口模块需要实现与具体ADC芯片的通信协议。以SPI接口的ADC为例,核心功能包括:
module adc_interface ( input clk, input reset_n, input start, output reg cs_n, output reg din, output reg sclk, input dout, output reg [11:0] data_out, output reg data_valid ); // 状态机定义 typedef enum { IDLE, INIT_CONV, READING, DONE } state_t; state_t current_state; reg [4:0] bit_counter; always @(posedge clk or negedge reset_n) begin if (!reset_n) begin current_state <= IDLE; cs_n <= 1'b1; sclk <= 1'b0; din <= 1'b0; data_out <= 12'h000; data_valid <= 1'b0; end else begin case (current_state) IDLE: begin if (start) begin current_state <= INIT_CONV; cs_n <= 1'b0; end end INIT_CONV: begin // 发送配置命令 din <= 1'b1; // 启动转换 sclk <= 1'b1; bit_counter <= 5'd0; current_state <= READING; end READING: begin sclk <= ~sclk; if (sclk) begin // 下降沿采样数据 data_out[11-bit_counter] <= dout; bit_counter <= bit_counter + 1; if (bit_counter == 5'd11) begin current_state <= DONE; end end end DONE: begin cs_n <= 1'b1; data_valid <= 1'b1; current_state <= IDLE; end endcase end end endmodule注意:ADC接口的具体实现需要根据您使用的ADC芯片手册进行调整,重点关注时序参数如SCLK频率、建立保持时间等。
2.2 FIFO缓冲器实现
FIFO(First In First Out)缓冲器是解决数据速率不匹配的关键组件。在FPGA中,我们可以使用厂商提供的IP核或自己编写FIFO逻辑。
使用Xilinx FIFO IP核的配置要点:
- 在Vivado中打开IP Catalog,搜索"FIFO Generator"
- 配置参数:
- 接口类型:Native
- 实现方式:Independent Clocks Block RAM
- 数据宽度:12位(匹配ADC分辨率)
- 深度:根据数据量需求选择(通常512-2048)
- 满/空标志:使能
关键信号说明:
| 信号名 | 方向 | 描述 |
|---|---|---|
| wr_clk | 输入 | 写时钟(通常接ADC时钟) |
| rd_clk | 输入 | 读时钟(通常接系统时钟) |
| din | 输入 | 写入数据 |
| wr_en | 输入 | 写使能 |
| rd_en | 输入 | 读使能 |
| dout | 输出 | 读出数据 |
| full | 输出 | FIFO满标志 |
| empty | 输出 | FIFO空标志 |
2.3 UART发送模块设计
UART发送模块需要将并行数据转换为符合串口协议的串行数据流。核心是波特率生成器和移位寄存器。
module uart_tx ( input clk, input reset_n, input [7:0] data_in, input start, output reg tx, output reg busy ); // 波特率配置(系统时钟50MHz,波特率9600) localparam BAUD_DIV = 5208; // 50MHz / 9600 reg [12:0] baud_counter; reg [3:0] bit_counter; reg [9:0] shift_reg; // 包含起始位、8位数据、停止位 typedef enum { IDLE, START, SENDING, STOP } state_t; state_t current_state; always @(posedge clk or negedge reset_n) begin if (!reset_n) begin current_state <= IDLE; tx <= 1'b1; busy <= 1'b0; end else begin case (current_state) IDLE: begin if (start) begin shift_reg <= {1'b1, data_in, 1'b0}; // 停止位+数据+起始位 baud_counter <= 0; bit_counter <= 0; current_state <= START; busy <= 1'b1; end end START: begin tx <= 1'b0; // 起始位 if (baud_counter == BAUD_DIV-1) begin baud_counter <= 0; current_state <= SENDING; end else begin baud_counter <= baud_counter + 1; end end SENDING: begin tx <= shift_reg[bit_counter]; if (baud_counter == BAUD_DIV-1) begin baud_counter <= 0; if (bit_counter == 9) begin current_state <= STOP; end else begin bit_counter <= bit_counter + 1; end end else begin baud_counter <= baud_counter + 1; end end STOP: begin tx <= 1'b1; // 停止位 if (baud_counter == BAUD_DIV-1) begin baud_counter <= 0; current_state <= IDLE; busy <= 1'b0; end else begin baud_counter <= baud_counter + 1; end end endcase end end endmodule3. 系统集成与仿真验证
3.1 顶层模块集成
顶层模块负责实例化并连接所有子模块。以下是关键连接点:
ADC接口到FIFO:
- ADC的data_out连接到FIFO的din
- ADC的data_valid连接到FIFO的wr_en
FIFO到UART发送:
- FIFO的dout连接到UART发送模块的data_in
- FIFO的empty信号控制UART发送的start信号
module adc_uart_top ( input clk, input reset_n, input adc_start, output adc_cs_n, output adc_din, output adc_sclk, input adc_dout, output uart_tx ); wire [11:0] adc_data; wire adc_data_valid; wire [7:0] uart_data; wire fifo_empty; wire fifo_full; wire uart_busy; // ADC接口模块 adc_interface adc_inst ( .clk(clk), .reset_n(reset_n), .start(adc_start), .cs_n(adc_cs_n), .din(adc_din), .sclk(adc_sclk), .dout(adc_dout), .data_out(adc_data), .data_valid(adc_data_valid) ); // FIFO实例 fifo_generator_0 fifo_inst ( .wr_clk(clk), .rd_clk(clk), .din({4'b0, adc_data}), // 扩展为16位 .wr_en(adc_data_valid & !fifo_full), .rd_en(!fifo_empty & !uart_busy), .dout(uart_data), .full(fifo_full), .empty(fifo_empty) ); // UART发送模块 uart_tx uart_inst ( .clk(clk), .reset_n(reset_n), .data_in(uart_data[7:0]), .start(!fifo_empty & !uart_busy), .tx(uart_tx), .busy(uart_busy) ); endmodule3.2 Testbench设计与仿真
完整的验证需要设计Testbench来模拟ADC行为和检查UART输出。以下是关键任务:
`timescale 1ns/1ps module adc_uart_top_tb; reg clk; reg reset_n; reg adc_start; wire adc_cs_n; wire adc_din; wire adc_sclk; reg adc_dout; wire uart_tx; // 实例化被测设计 adc_uart_top dut ( .clk(clk), .reset_n(reset_n), .adc_start(adc_start), .adc_cs_n(adc_cs_n), .adc_din(adc_din), .adc_sclk(adc_sclk), .adc_dout(adc_dout), .uart_tx(uart_tx) ); // 时钟生成 initial begin clk = 1'b0; forever #10 clk = ~clk; // 50MHz时钟 end // 测试序列 initial begin reset_n = 1'b0; adc_start = 1'b0; adc_dout = 1'b0; #100 reset_n = 1'b1; #50 adc_start = 1'b1; #20 adc_start = 1'b0; // 模拟ADC输出 fork generate_adc_data(16'h1234); generate_adc_data(16'h5678); generate_adc_data(16'h9ABC); join #10000 $finish; end // ADC数据生成任务 task generate_adc_data; input [15:0] data; integer i; begin wait(!adc_cs_n); for (i = 0; i < 16; i = i + 1) begin @(negedge adc_sclk) adc_dout = data[15-i]; end end endtask // UART接收监控 reg [7:0] uart_rx_data; reg uart_rx_done; initial begin uart_rx_done = 1'b0; forever begin // 检测起始位 wait(uart_tx == 0); #(5208*10); // 跳过起始位 // 接收8位数据 uart_rx_data = 0; for (int i = 0; i < 8; i = i + 1) begin #(5208*10); uart_rx_data[i] = uart_tx; end // 检查停止位 #(5208*10); if (uart_tx == 1) begin uart_rx_done = 1'b1; #10 uart_rx_done = 1'b0; $display("Received UART data: %h", uart_rx_data); end end end endmodule3.3 仿真波形分析要点
在ModelSim中运行仿真后,需要关注以下关键信号:
ADC接口信号:
- adc_cs_n:片选信号,低电平有效
- adc_sclk:SPI时钟
- adc_dout:ADC数据输出
FIFO控制信号:
- wr_en:写使能,应与adc_data_valid同步
- rd_en:读使能,应与UART发送需求同步
- empty/full:缓冲状态指示
UART信号:
- uart_tx:串行输出,检查起始位、数据位和停止位
- busy:发送状态指示
常见问题排查:
- 如果FIFO一直为空,检查ADC模块的data_valid信号是否正确生成
- 如果UART发送数据错误,检查波特率分频系数是否正确
- 如果数据丢失,检查FIFO深度是否足够处理速率差异
4. 硬件调试与优化技巧
4.1 在线逻辑分析仪使用
当系统在硬件上运行时,可以使用厂商提供的在线逻辑分析仪进行调试:
Vivado ILA使用步骤:
- 在设计中标记需要观察的信号为(* MARK_DEBUG="true" *)
- 综合实现后,打开Hardware Manager
- 连接设备并配置触发条件
- 捕获信号并分析波形
常见调试场景:
- ADC数据是否正确采集
- FIFO的读写指针是否正常移动
- UART波特率是否准确
4.2 性能优化建议
- 时序约束: 确保为ADC接口和UART模块添加适当的时序约束,特别是跨时钟域的信号。
# 示例:时钟约束 create_clock -period 20.000 -name clk [get_ports clk] # ADC SPI时钟约束 create_generated_clock -name adc_sclk -source [get_pins adc_interface/sclk_reg] \ -divide_by 1 [get_ports adc_sclk] # 跨时钟域约束 set_false_path -from [get_clocks clk] -to [get_clocks adc_sclk]资源优化:
- 根据数据量调整FIFO深度
- 考虑使用异步FIFO IP核处理跨时钟域数据
- 对于低速UART,可以使用LUT实现而非块RAM
功耗优化:
- 当ADC不采样时,关闭相关时钟
- 使用时钟门控技术降低动态功耗
4.3 扩展功能建议
数据预处理: 在ADC数据存入FIFO前,可以添加数字滤波或校准模块
协议扩展: 将简单的UART协议升级为Modbus等工业标准协议
错误处理: 添加CRC校验或重传机制提高通信可靠性
多通道支持: 扩展系统以支持多路ADC同时采样
在实际项目中,这种ADC到UART的数据通路经常作为更复杂系统的基础组件。掌握其实现细节后,可以轻松扩展到各种数据采集和传输场景。
