目录

FPGA初级项目9基于SPI的ADC芯片进行模数转换

FPGA初级项目9——基于SPI的ADC芯片进行模数转换

FPGA初级项目9——基于SPI的ADC芯片进行模数转换

ADC芯片介绍

ADC(Analog-to-Digital Converter)芯片是一种 将连续变化的模拟信号转换为离散数字信号 的电子器件,广泛应用于电子系统中,是连接现实世界与数字世界的桥梁。可将电压、电流等模拟量转换为二进制数字信号,供计算机、微控制器等数字设备处理。

关键参数

分辨率: 以位数表示(如 12 位、16 位) 表示将一个模拟量转换为多少位的数字量 ,例如8位分辨率可表示的范围为0~255(十进制)。

采样率: 每秒转换的样本数(单位 Hz),需满足 Nyquist 定理(至少 2 倍信号最高频率)。

转换精度: 实际输出与理论值的偏差,受噪声、非线性等因素影响。

输入范围: 可转换的模拟信号电压范围(如 0~5V、±10V)。

功耗: 低功耗设计适用于电池供电设备

https://i-blog.csdnimg.cn/direct/91cab086b1a1437baa7ae801cee915f7.png


SPI 协议基础

SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工、同步的 通信协议 (是一种规则),广泛用于微控制器(如 Arduino、STM32)与外设(如 ADC、传感器、存储器)之间的短距离通信。

https://i-blog.csdnimg.cn/direct/5a4f3357ae9a4c248e819c59f5a049c7.png

SPI由来

SPI最初由摩托罗拉(现 NXP)于 1980 年代开发,并未通过 ISO、IEEE 等国际组织的正式认证。但由于其简单高效, 逐渐成为事实上的工业标准,被全球半导体厂商(如 ADI、TI、Microchip)广泛支持主流 MCU(如 Arduino、STM32、ESP32)均内置硬件 SPI 控制器,且 ADC、传感器等外设芯片的 SPI 接口严格遵循摩托罗拉定义的时序规则。 这种生态共识使 SPI 成为嵌入式领域的 “通用语言”。

SPI参数

主从架构 :一个主设备(如 MCU或FPGA)控制多个从设备(如 ADC 芯片)。

四线通信

SCK(时钟线):主设备发送时钟信号,同步数据传输。

MOSI(主出从入):主设备向从设备发送数据。

MISO(主入从出):从设备向主设备返回数据。

CS(片选线):主设备通过拉低特定从设备的 CS 线选择目标。

高速传输 :支持兆赫兹级速率(如 STM32 可达 42MHz),适合高频数据场景。

全双工 :数据可同时双向传输。

SPI规则

https://i-blog.csdnimg.cn/direct/39e8a6df39be402ba32624445c81a019.png

SPI怎么用,需要自己编写吗?

需要强调的是:是否需要自己编写 SPI 逻辑代码,取决于你使用的开发环境、硬件平台以及具体需求。

  1. 有些较为基础、低成本的微控制器可能没有内置 SPI 硬件模块,例如一些简单的 8 位单片机。在这种情况下,你只能通过软件来模拟 SPI 的通信逻辑,也就是自己编写代码来控制通用输入输出引脚(GPIO),模拟时钟信号(SCK)、数据传输(MOSI 和 MISO)以及片选信号(CS)的时序。

  2. 当 你有特殊的通信需求,比如需要修改 SPI 的通信时序、协议格式,或者实现一些自定义的通信规则时,就可能需要自己编写 SPI 逻辑代码。 例如,你可能需要调整时钟极性(CPOL)和时钟相位(CPHA)来满足特定 ADC 芯片的通信要求。

  3. 大多数现代微控制器,如常见的 STM32 系列、Arduino 的部分型号等,都内置了 SPI 硬件外设。这些硬件外设可以直接配置和使用,芯片厂商通常会提供相应的库函数来简化 SPI 通信的操作。


工作原理

ADC 芯片通过 SPI 接口接收主设备的控制指令(如启动转换、配置参数),并将转换后的数字信号通过 SPI 返回给主设备。 我们需要完成的任务即ADC芯片的驱动逻辑,依照芯片手册实现其逻辑代码编写(严格遵循SPI协议)。

问题分析

1. 我们需要编写的是芯片ADC128S102的驱动逻辑,有 4个主要端口: cs_n(片选端); sclk(ADC芯片时钟); DIN(模拟量输入端口); DOUT(数字量输出端口); 根据芯片手册要满足其时序要求。其中SCLK频率为12.5HZ。

https://i-blog.csdnimg.cn/direct/e7df81e810ee4880b93173af60c2bc9d.png

2. 我们依旧使用 线性序列机(LSM) 的思想来定义每个时刻该做的事情。 以SCLK半个周期为最小时间单元。 DOUT端口靠ADC芯片来驱动,FPGA端来接收。每个输出的数据信号在SCLK下降沿改变,同时FPGA在SCLK上升沿读取(此时信号已稳定);而DIN信号决定ADC芯片的哪个通道输入,在SCLK下降沿FPGA将信号输入到ADC芯片。 详细参数可参照文末代码!

https://i-blog.csdnimg.cn/direct/e68eac160fd14bacaf4b054dc58f75b1.png

3. 同时还有一些额外的端口需要设置,例如用户的通道选择输入端口[2:0]addr(因ADC芯片有8个选择输入端口,所以需要3位来表示);数字数据输出端口 ;采样信号(决定什么时候采样)与采样完成信号端口的设置等。

https://i-blog.csdnimg.cn/direct/987884069b454428b69142a3fbf4fe96.png

4. 根据芯片时序手册,处理完一轮数据周期需要35个时钟,所以counter1的计数最大值为35,即需要6位二进制数来表示,所以为[5:0]counter1。ADC芯片的电路结构如下所示可作为了解, 但是我们需要再次强调我们所写的是该芯片的驱动电路!!!

https://i-blog.csdnimg.cn/direct/6e59b2d7c0c5432c9a15767547d7a42c.png

好的分析完上述问题,我们上代码


代码展示

//定义输入输出端口
module ADC_driver(
        clk,
        reset_n,
        conv_go,//采样请求信号
        addr,//用户输入选择通道端口
        conv_done,//采样完成信号
        data,//已经转换完成的数字信号
        
        sclk,
        cs_n,
        DOUT,
        DIN
    );
        input clk;
        input reset_n;
        input conv_go;
        input [2:0]addr;
        output reg conv_done;
        output reg [11:0]data;
        
        output reg sclk;
        output reg cs_n;
        output reg DIN;
        input DOUT;
        
 //定义相关时钟参数
        parameter clock_freq = 50_000_000;
        parameter sclk_freq = 12_500_000;//ADC芯片SCLK频率
        parameter mcnt = clock_freq / (sclk_freq * 2) - 1;
     
       
        
//定义最小时间单元counter0
        reg en_counter0;
        reg [7:0]counter0;
always@(posedge clk or negedge reset_n)
if(!reset_n)
        counter0 <= 0;
else if(en_counter0) begin
        if(counter0 == mcnt)
        counter0 <= 0;
        else 
        counter0 <= counter0 +1'd1;
        end
else
        counter0 <= 0;
        
        
//定义位计数器counter1
        reg [5:0]counter1;
always@(posedge clk or negedge reset_n)
if(!reset_n)
        counter1 <= 6'd0;
else if(counter0 == mcnt) begin
        if(counter1 == 6'd34)//处理完一轮数据需要35个周期时钟
        counter1 <= 6'd0;
        else
        counter1 <= counter1 + 1'd1;
        end
else
        counter1 <= counter1;
        
 
 //定义存储器,防止数据发生变化
        reg [2:0]r_addr;
always@(posedge clk)
if(conv_go)
        r_addr <= addr;
else
        r_addr <= r_addr;
        
 
 //定义数据处理完成信号与处理后的数据存贮
        reg [11:0]data_r;
always@(posedge clk or negedge reset_n)
if(!reset_n) begin
        data <= 12'd0;
        conv_done <= 0;
        end
else if((counter1 == 34)&&(counter0 == mcnt)) begin
        data <= data_r;
        conv_done <= 1'd1;
        end
else begin
        data <= data;
        conv_done <= 0;
        end
       
        
//定义使能信号en_counter0
always@(posedge clk or negedge reset_n)
if(!reset_n) 
        en_counter0 <= 1'd0;
else if(conv_go)
        en_counter0 <= 1'd1;
else if((counter1 == 34)&&(counter0 == mcnt))
        en_counter0 <= 1'd0;
else
        en_counter0 <= en_counter0;               
        

        
//定义每个时间点的操作,按照时序图来填表即可
always@(posedge clk or negedge reset_n)
if(!reset_n)begin
   data_r <= 12'd0;
   sclk <= 1'd1;
   DIN <= 1'd1;
   cs_n <= 1'd1;
   end
else if(counter0 == mcnt)begin
  case(counter1)
  0:begin cs_n <= 1'd1; sclk <= 1'd1; end
  1:begin sclk <= 1'd0; end
  2:begin sclk <= 1'd0; end
  3:begin sclk <= 1'd1; end
  4:begin sclk <= 1'd0; end
  5:begin sclk <= 1'd1; end
  6:begin sclk <= 1'd0; DIN <= r_addr[2];end
  7:begin sclk <= 1'd1; end
  8:begin sclk <= 1'd0; DIN <= r_addr[21];end
  9:begin sclk <= 1'd1; end
  10:begin sclk <= 1'd0; DIN <= r_addr[0];end
  11:begin sclk <= 1'd1; data_r[11] <= DOUT;end
  12:begin sclk <= 1'd0; end
  13:begin sclk <= 1'd1; data_r[10] <= DOUT;end
  14:begin sclk <= 1'd0; end
  15:begin sclk <= 1'd1; data_r[9] <= DOUT;end
  16:begin sclk <= 1'd0; end
  17:begin sclk <= 1'd1; data_r[8] <= DOUT;end
  18:begin sclk <= 1'd0; end
  19:begin sclk <= 1'd1; data_r[7] <= DOUT;end
  20:begin sclk <= 1'd0; end
  21:begin sclk <= 1'd1; data_r[6] <= DOUT;end
  22:begin sclk <= 1'd0; end
  23:begin sclk <= 1'd1; data_r[5] <= DOUT;end
  24:begin sclk <= 1'd0; end
  25:begin sclk <= 1'd1; data_r[4] <= DOUT;end
  26:begin sclk <= 1'd0; end
  27:begin sclk <= 1'd1; data_r[3] <= DOUT;end
  28:begin sclk <= 1'd0; end
  29:begin sclk <= 1'd1; data_r[2] <= DOUT;end
  30:begin sclk <= 1'd0; end
  31:begin sclk <= 1'd1; data_r[1] <= DOUT;end
  32:begin sclk <= 1'd0; end
  33:begin sclk <= 1'd1; data_r[0] <= DOUT;end
  34:begin sclk <= 1'd0; end
  default: cs_n <= 1'd1;
  endcase
end

        
endmodule

综合出来的底层系统逻辑图schematic如下:

https://i-blog.csdnimg.cn/direct/e0495de2567f4589a0dd27e073e4a164.png