Vivado中EGO1开发板实战:手把手教你实现SPI通信与ADC采样系统
从一个“卡住”的大作业说起
你是不是也经历过这样的时刻?
课程设计截止前两天,打开Vivado,对着EGO1开发板发呆——老师布置的“基于FPGA的数据采集系统”听起来很基础,但真要动手写代码、调时序、接外设的时候,却发现SPI协议手册看不懂,ADC读不出数据,波形全是噪声。
别慌。这正是我们今天要一起解决的问题。
在嵌入式系统教学中,“用FPGA通过SPI读取ADC数据并上传PC”是EGO1开发板最常见的综合性大作业之一。它看似简单,实则融合了状态机设计、跨时钟域处理、硬件接口驱动和信号完整性等多个关键知识点,稍有不慎就会陷入“逻辑没错,但就是没输出”的调试地狱。
本文不讲空泛理论,也不堆砌术语,而是带你从零搭建一套完整可运行的ADC采样系统,重点解决你在实际开发中最可能遇到的坑点,并给出经过验证的Verilog实现方案。最终目标:让你的大作业不仅能跑通,还能拿高分。
为什么选SPI + 外部ADC?
很多同学第一反应是:“为什么不直接用Zynq的XADC?”
答案很简单:精度不够、灵活性差、不符合工程实践要求。
EGO1虽然小巧,但它搭载的是Xilinx Artix-7系列FPGA(XC7A35T),本身没有集成高精度ADC模块。即使有,片上XADC通常只有10位左右分辨率,采样率低,还容易受内部数字噪声干扰。
而外部SPI ADC(比如MCP3204、ADS7886等)可以做到12~16位分辨率,支持差分输入、独立参考电压,更适合做精确测量。更重要的是——你需要掌握如何让FPGA和真实世界交互,这才是FPGA学习的核心价值。
所以,这个项目的真正意义不是“读个ADC”,而是:
学会如何在FPGA上构建一个可靠的、可扩展的传感器接口子系统。
SPI通信:不只是四根线那么简单
协议本质:同步串行,主从分明
SPI是一种典型的主-从结构同步通信协议,由四条基本信号组成:
-SCLK:主设备提供的时钟
-MOSI:主出从入(命令下发)
-MISO:主入从出(数据回传)
-CS_N:片选,低电平有效
它的最大优势在于高速、全双工、协议开销小,非常适合短距离高速器件通信,比如ADC、DAC、Flash、显示屏等。
但在FPGA设计中,最容易翻车的地方,往往就藏在那些“看起来理所当然”的细节里。
模式选择:CPOL 和 CPHA 到底怎么配?
这是90%初学者都会困惑的问题。
不同ADC芯片对SPI模式的要求不同。以常用的MCP3204为例,它工作在Mode 0(CPOL=0, CPHA=0):
- 空闲时SCLK为低电平(CPOL=0)
- 数据在上升沿采样(CPHA=0)
这意味着:
- 主设备应在下降沿改变数据(MOSI),上升沿读取MISO;
- CS拉低后第一个边沿就是有效采样边沿。
| Mode | CPOL | CPHA | SCLK空闲态 | 数据采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低 | 上升沿 |
| 1 | 0 | 1 | 低 | 下降沿 |
| 2 | 1 | 0 | 高 | 下降沿 |
| 3 | 1 | 1 | 高 | 上升沿 |
📌经验提示:如果你发现读回来的数据总是错一位或全为0,大概率是CPHA配反了!
自研SPI主控制器:比IP核更灵活
虽然Vivado提供了AXI Quad SPI IP核,但对于学生项目来说,自己写一个轻量级SPI Master更有教学价值,也更容易定制时序。
下面是一个经过实测可用的精简版SPI控制器,专为ADC读取优化。
module spi_master_controller #( parameter CLK_FREQ = 100_000_000, parameter SPI_FREQ = 1_000_000, parameter DATA_WIDTH = 16 )( input clk, input rst_n, input start, output reg cs_n, output reg sclk, output reg mosi, input miso, output reg [DATA_WIDTH-1:0] data_out, output done ); localparam SCLK_HALF_PERIOD = CLK_FREQ / (2 * SPI_FREQ); // 分频系数 reg [31:0] clk_div; reg [3:0] bit_cnt; reg [DATA_WIDTH-1:0] shift_reg; typedef enum logic [1:0] { IDLE, TRANSFER } state_t; state_t state; assign done = (state == IDLE) && (bit_cnt == DATA_WIDTH); // 状态机与时钟分频统一处理 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; clk_div <= 0; bit_cnt <= 0; sclk <= 0; cs_n <= 1; shift_reg <= 0; data_out <= 0; mosi <= 0; end else begin clk_div <= clk_div + 1; case (state) IDLE: begin if (start) begin cs_n <= 0; bit_cnt <= 0; shift_reg <= {DATA_WIDTH{1'b1}}; // 发送启动/配置字 data_out <= 0; if (clk_div >= SCLK_HALF_PERIOD - 1) begin clk_div <= 0; state <= TRANSFER; end end end TRANSFER: begin if (clk_div == SCLK_HALF_PERIOD - 1) begin // 上升沿:锁存MISO(符合Mode 0) data_out <= {data_out[DATA_WIDTH-2:0], miso}; end else if (clk_div == 2*SCLK_HALF_PERIOD - 1) begin // 下降沿:更新MOSI & 移位 clk_div <= 0; mosi <= shift_reg[DATA_WIDTH-1]; shift_reg <= {shift_reg[DATA_WIDTH-2:0], 1'b0}; bit_cnt <= bit_cnt + 1; if (bit_cnt == DATA_WIDTH - 1) begin sclk <= 0; // 最后半个周期保持低 cs_n <= 1; // 结束传输 state <= IDLE; end else begin sclk <= ~sclk; end end end endcase // 单独控制SCLK翻转(避免毛刺) if (clk_div == SCLK_HALF_PERIOD - 1 && state == TRANSFER && bit_cnt < DATA_WIDTH) sclk <= 1; else if (clk_div == 2*SCLK_HALF_PERIOD - 1 && state == TRANSFER && bit_cnt < DATA_WIDTH - 1) sclk <= 0; end end endmodule🔧代码亮点解析:
- 使用单状态机简化逻辑,仅保留IDLE和TRANSFER两个状态;
- 在SCLK下降沿发送MOSI,上升沿采样MISO,严格遵循Mode 0时序;
-clk_div计数器同时用于分频和边沿控制,避免多时钟逻辑混乱;
- 支持任意DATA_WIDTH,适配多种ADC(如MCP3204为12位,AD7680为16位);
-cs_n在最后一比特结束后释放,确保时序合规。
💡调试建议:用ILA抓取sclk,mosi,miso,cs_n四根信号,观察是否满足ADC手册中的时序图要求。
ADC采样控制:触发、同步与去抖
有了SPI主控,下一步是如何协调ADC的转换流程。
典型SAR ADC的工作流程如下:
1. FPGA发出“开始转换”指令(可通过GPIO脉冲或SPI写入);
2. ADC进入忙状态(BUSY引脚拉高);
3. 转换完成后,FPGA发起SPI读操作获取结果。
但由于大多数EGO1配套实验板上的ADC并未引出BUSY引脚,因此我们采用软件定时触发+固定延迟读取的方式。
module adc_sampler ( input clk, // 100MHz系统时钟 input rst_n, input trigger, // 外部启动信号(按键或定时器) output spi_cs_n, output spi_sclk, output spi_mosi, input spi_miso, output [11:0] adc_result, output data_valid ); wire spi_start = trigger; wire spi_done; wire [15:0] spi_data_raw; // 实例化SPI控制器 spi_master_controller #( .CLK_FREQ(100_000_000), .SPI_FREQ(1_000_000), .DATA_WIDTH(16) ) u_spi ( .clk(clk), .rst_n(rst_n), .start(spi_start), .cs_n(spi_cs_n), .sclk(spi_sclk), .mosi(spi_mosi), .miso(spi_miso), .data_out(spi_data_raw), .done(spi_done) ); // 提取有效数据(以MCP3204为例:低12位为ADC值) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin adc_result <= 0; data_valid <= 0; end else begin data_valid <= spi_done; // 标志位,可用于驱动FIFO或UART if (spi_done) adc_result <= spi_data_raw[11:0]; end end endmodule🎯关键设计考量:
-trigger可来自定时器(实现周期采样)或按键(手动触发);
- 假设ADC响应时间为10μs,则SPI时钟设为1MHz足够安全;
- 若需更高性能,可在SPI后插入几周期延迟再读取,模拟“t_conv”。
实际部署中的五大坑点与避坑指南
⚠️ 坑点1:电源噪声导致ADC跳动剧烈
现象:采样值在相同输入下频繁波动±10LSB以上。
✅解决方案:
- 在ADC的VDD和GND之间加0.1μF陶瓷电容;
- 使用独立LDO供电,避免与FPGA共用开关电源;
- 参考电压使用专用基准源(如REF3030)而非FPGA IO供电。
⚠️ 坑点2:SPI速率过高导致通信失败
现象:始终读到0或0xFFF。
✅解决方案:
- 先将SPI_FREQ降至500kHz调试,确认功能正常后再逐步提升;
- 查阅ADC手册中的最大SCLK频率(如MCP3204为3.5MHz);
- PCB走线过长时应降低速率或添加串联电阻匹配阻抗。
⚠️ 坑点3:误用MSB/LSB顺序
现象:高位恒为0或数据移位。
✅解决方案:
- 明确ADC是MSB first还是LSB first(MCP3204为MSB);
- 在移位寄存器中正确对齐位宽;
- 可通过ILA查看原始spi_data_raw判断方向。
⚠️ 坑点4:未处理复位异步问题
现象:上电后首次采样异常。
✅解决方案:
- 所有寄存器必须在rst_n下清零;
- 使用同步复位(推荐)或两级异步复位同步器;
- 避免组合逻辑中出现未初始化变量。
⚠^坑点5:忽略时钟域交叉风险
现象:FIFO溢出或亚稳态崩溃。
✅解决方案:
- 当SPI时钟与系统时钟不同时,使用双端口FIFO缓存数据;
- 或采用握手信号(valid/ready)进行跨时钟传递;
- 不要用单比特信号直接跨越时钟域!
系统整合:打造你的迷你示波器
现在我们将各模块整合成一个完整的采集上传系统:
[模拟输入] ↓ [ADC芯片] --SPI--> [FPGA] ↓ [adc_sampler] → [Data FIFO] ↓ [UART TX Bridge] ↓ [USB转串口] → PC你可以进一步扩展功能:
- 添加1ms定时器实现1kHz采样;
- 使用Block RAM构建深度缓冲区;
- 通过UART 115200bps连续发送数据帧;
- 在Python端用Matplotlib实时绘图,实现简易示波器。
写给正在做“ego1开发板大作业”的你
这个项目之所以常被选作大作业,是因为它像一块“试金石”:
- 它足够小,能在两周内完成;
- 它又足够深,能暴露出你在时序理解、模块划分和调试方法上的所有短板。
但只要你坚持走完以下几步:
1.读懂ADC手册的时序图
2.写出符合Mode 0的SPI控制器
3.用ILA验证每一根信号
4.加入合理注释便于答辩讲解
你就已经超越了大多数只拷贝代码的同学。
记住,FPGA开发的本质不是写代码,而是构建一个能可靠工作的硬件行为。每一次成功的采样,都是你对物理世界的掌控力的一次提升。
如果你在实现过程中遇到了其他挑战——比如多通道切换、DMA传输、FFT分析——欢迎留言交流。我们可以一步步把这套系统升级成真正的高性能数据采集平台。
毕竟,一个好的“vivado中ego1开发板大作业”,不该止步于点亮LED,而应始于一次精准的ADC采样。