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

发布于:2025-03-15 ⋅ 阅读:(13) ⋅ 点赞:(0)

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

ADC芯片介绍


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

关键参数


分辨率:以位数表示(如 12 位、16 位)表示将一个模拟量转换为多少位的数字量,例如8位分辨率可表示的范围为0~255(十进制)。
采样率:每秒转换的样本数(单位 Hz),需满足 Nyquist 定理(至少 2 倍信号最高频率)。
转换精度:实际输出与理论值的偏差,受噪声、非线性等因素影响。
输入范围:可转换的模拟信号电压范围(如 0~5V、±10V)。
功耗:低功耗设计适用于电池供电设备
 


SPI 协议基础


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


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规则


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。


 

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

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

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

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


代码展示

//定义输入输出端口
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如下: