基于FPGA实现ADC366X系列芯片配置及数据采集
简介本文详细讲解如何使用FPGA通过SPI对ADC366X系列芯片进行配置借助Xilinx的VIO(Virtual Input/Output)快速实现代码的调试为读者们分享SPI接口逻辑实现以及ADC366X系列在使用基本功能过程中需要注意的地方。通过本文的内容分享希望读者可以从中有所得掌握FPGA与ADC之间的通信机制熟悉VIO在实际调试过程中的应用并具备在实际项目中进行配置与数据采集的能力不再为ADC应用而发愁。1 SPI通信协议基础原理SPISerial Peripheral Interface是一种高速、全双工、同步串行通信协议大多数情况下都是通过四根信号线实现工作SCLKSerial Clock由主设备生成的时钟信号用于同步数据传输MOSIMaster Out Slave In主设备发送数据到从设备的通道MISOMaster In Slave Out从设备返回数据到主设备的通道CS/SSChip Select/Slave Select选择当前通信的从设备低电平有效。从它的接口定义我们可以看出SPI是采用主从架构通信由主设备发起数据在SCLK的上升沿或下降沿进行采样与输出具体时序依赖于时钟极性CPOL与时钟相位CPHA的设置这也构成了SPI四种不同的时钟模式。那么其实我们对于SPI代码的实现也就是围绕这四根线时序进行开发在不同ADC配置场景中只需根据具体芯片的寄存器配置时序调整即可。博主在整理SPI原理相关内容时反复修改总结总觉得差点意思最后发现一篇总结十分详细的高质量文章在文章末尾分享给大家。下面就让我们看一看SPI在配置ADC366X系列芯片时是怎么应用的吧。2 ADC366X系列2.1 基本概述ADC366x系列是低噪声、超低功耗、16位分辨率、双通道模数转换器采样率覆盖0.5MSPS~65MSPS适合对功耗、噪声、延迟要求严苛的工业、通信、测试测量类应用。型号差异2.2 核心特性2.2.1 性能参数延迟极低延迟1线SLVDS接口下仅1个采样时钟周期延迟2线接口下2个周期适合高速控制环路应用。精度指标无丢码16位全精度输出积分非线性INL±3LSB典型值±5LSB最大值微分非线性DNL±0.7LSB典型值±0.85LSB最大值输入全量程3.2Vpp差分输入共模电压0.95V输入带宽900MHz-3dB支持中频采样。频谱性能典型值f_IN10MHz信噪比SNR81.9dBFS含二次/三次谐波的无杂散动态范围SFDR92dBc排除二次/三次谐波的最差毛刺SFDR99dBFS有效位数ENOB13.3位2.2.2 功能特性a 基准源选项支持三种基准配置外部1.6V基准直接输入最高精度、最低温漂外部1.2V基准内部缓冲增益至1.6V内部1.2V基准内部增益至1.6V默认上电配置b 片上数字滤波器可选● 支持2/4/8/16/32倍抽取分为实抽取低通滤波输出带宽为抽取率的40%和复抽取带32位NCO混频输出带宽为抽取率的80%● 抽取滤波器阻带抑制≥85dB内部计算精度20位避免量化噪声损失● 复抽取支持6dB数字增益补偿混频损耗实抽取支持3dB增益补偿● 可选FS/4混频功能将复抽取输出转换为实输出信号中心频率位于Fout/4c 数字接口串行LVDSSLVDS接口支持三种模式● 支持输出位宽灵活配置14/16/18/20位通过输出位映射器Bit Mapper可自定义输出位序● 可选输出加扰功能减少数据跳变带来的EMI● 输出格式支持默认二进制补码/偏移二进制可选d 其他功能● 自动调零Auto-Zero前端放大器改善1/f闪烁噪声ADC3661/2默认开启ADC3663可通过SPI开启● 双通道数字平均功能两通道输入相同信号时内部平均可降低3dB不相关噪声提升动态范围● 单端/差分模拟输入、单端/差分时钟输入可选单端时钟可额外节省~1mA模拟电流● 测试模式支持斜坡RAMP测试图案、自定义常量测试图案方便数字接口调试● 同步SYNC功能可通过PDN/SYNC引脚或SPI触发同步多片ADC的抽取滤波器时钟分频器、NCO相位保证多器件相位对齐ADC366X系列芯片可配置选项较多感兴趣的小伙伴可以自行查看官方芯片手册。我们在实际使用中仅考虑基本的功能使用链接: ADC366X芯片手册2.2.3 引脚特性2.3 使用说明2.3.1 典型电路这里不便直接放出博主自己的原理图我们参考官方手册提供的典型应用电路即可实现。唯一区别就是我们在实际使用中只是将control部分的管脚接到了FPGA端来实现配置ADC驱动。结合2.2.3小节以及上图可知ADC366X系列的SPI控制管脚中的读写数据通道复用了一根数据线。这对于我们MISO和MOSI逻辑的开发来讲区别不大只需要注意控制读写方向以及三态门的使用。2.3.2 寄存器配置时序任何芯片上电后都会有自己的默认配置ADC366X系列也不例外。但在实际使用过程中我们往往不仅仅局限于某一特定功能所以需要能够灵活运用并学会自己配置芯片的寄存器。写操作SEN拉低A15位写0接着送12位寄存器地址8位数据每24个SCLK上升沿锁存一次数据。读操作SEN拉低A15位写1送12位寄存器地址随后器件在SCLK下降沿从SDIO输出8位寄存器数据控制器在SCLK上升沿采样。对SCLK的时序要求因此后续我们代码为了省事并未严格按照50%占空比来分频只需要满足时序要求即可。2.3.3 配置流程手册中给出了一种以16位、单线、8倍复抽取方式的配置流程我们可作为参考但具体配置流程还得以我们实际使用为准。每一个寄存器对应的具体含义感兴趣的可以参考芯片手册8.6小节我们将在第三节实现过程中使用ADC3662和ADC3663双线16bit工作模式。3 FPGA实现ADC366X数据采集3.1 模块划分我们需要实现的功能包括adc366x系列工作模式的配置、数据的转换和采集此处我们对于采集后的数据去向不做赘述主要在于驱动配置以及数据的采集和ADC366x数据交互以及时钟相关信号均为差分信号。我们此处要特别注意●管脚属性的约束●差分转单端●终端电阻的选择3.2 代码实现这一小节我们主要描述代码的具体实现。在2.3.2小节我们已经对与ADC366X系列的读写寄存器时序有详细的了解我们将根据手册时序进行代码的逻辑实现。3.2.1 adc366x_drv主要是对与ADC366X芯片的控制驱动配置相关逻辑。接口module adc366x_drv #(REG_ADD_WID12,//register addrsee widthREG_BIT_NUM8//data width)(input clk_i,//40MHzinput rstn,input w_cfg_start,input[REG_BIT_NUM-1:0]w_cfg_data,input r_cfg_start,input[REG_ADD_WID-1:0]reg_addr,output adc366X_sclk_o,inout adc366X_sdio_io,output adc366X_sen_o,output adc366X_miso);配置读写时序实现注意三态门的控制逻辑/////////////sen//////////////always (posedge clk_i or negedge rstn)beginif(!rstn)begin r_adc366x_sen1b1;endelseif((r_bit_cnt24)(r_div_shift[0]1b1))begin r_adc366x_sen1b1;endelseif((w_cfg_start_neg1b1) || (r_cfg_start_neg 1b1))begin r_adc366x_sen1b0;endelsebegin r_adc366x_senr_adc366x_sen;end end assign adc366X_sen_or_adc366x_sen;/////////////sclk/////////////////////////将40M时钟进行4分频////////always (posedge clk_i or negedge rstn)beginif(!rstn)begin r_div_shift4b0001;endelsebegin r_div_shift{r_div_shift[2:0],r_div_shift[3]};end end always (posedge clk_i or negedge rstn)beginif(!rstn)begin r_sclk1b0;endelseif((r_adc366x_sen1b0) (r_div_shift[1] 1b1))begin r_sclk1b1;endelsebegin r_sclk1b0;end end assign adc366X_sclk_or_sclk;/////////////sdio//////////////////读写的主要差别就在于SDIO的最高位以及需要注意SDIO管脚的IN和OUT方向的区分//////always (posedge clk_i or negedge rstn)beginif(!rstn)begin r_bit_cnt5d0;endelseif(!r_adc366x_sen)beginif(r_div_shift[1]1b1)begin r_bit_cntr_bit_cnt1b1;endelsebegin r_bit_cntr_bit_cnt;end endelsebegin r_bit_cnt5d0;end end assign once_cfg_end(r_bit_cnt5d24) (r_div_shift[1] 1b1);assign rd_cfg_en(r_bit_cnt5d16) (rw_cfg_flag 1b1);always (posedge clk_i or negedge rstn)beginif(!rstn)begin rw_cfg_flag1b0;endelseif(w_cfg_start_pos1b1)begin rw_cfg_flag1b0;endelseif(r_cfg_start_pos1b1)begin rw_cfg_flag1b1;endelseif(once_cfg_end1b1)begin rw_cfg_flag1b0;end end always (posedge clk_i or negedge rstn)beginif(!rstn)begin r_sda{4b0000,12h007,8h4b};endelseif(r_adc366x_sen1b0)beginif((r_div_shift[0]1b1) (r_bit_cnt 5d0))begin r_sda{r_sda[22:0],1b0};endelsebegin r_sdar_sda;end endelsebegin r_sda{rw_cfg_flag,3b000,reg_addr,w_cfg_data};end end assign adc366X_sdio_io(rd_cfg_en1b0) ? r_sda[23] : 1bz;assign adc366X_misoadc366X_sdio_io;3.2.2 data_rcv该模块主要是实现与ADC366X之间的数据采集并提供外部需要时钟。接口moduleadc3663_152v2_drive_u13(input high_clk_i,// 160Mhzinput rstn_i,input da0_p_i,input da0_n_i,input da1_p_i,input da1_n_i,input db0_p_i,input db0_n_i,input db1_p_i,input db1_n_i,input fclk_p_i,input fclk_n_i,input dclk_p_i,input dclk_n_i,output dclkin_p_o,// default 40Mhzoutput dclkin_n_o,output clk_p_o,output clk_n_o,// default 10Mhzoutput[15:0]adc_cha_tdata_o,output[15:0]adc_chb_tdata_o,output adc_data_tvalid_o);由于ADC366X芯片工作出了正常的供电以及配置寄存器之外还需要外部提供采样时钟CLKP/M、外部串行LVDS输入时钟DCLKINP/M。对于差分信号的处理我们直接使用I/OBUFDS原语来进行处理。对于I/OBUFDS原语我们可以参考vivado提供的用法在tools的Language Template打开直接搜索相关原语即可//--------- output dclkin --------------------------always (posedge high_clk_i or negedge rstn_i)beginif(rstn_i1b0)begin r_dclkin1d0;endelseif((r_div_shfit[1])||(r_div_shfit[3])||(r_div_shfit[5])||(r_div_shfit[7]))begin r_dclkin~r_dclkin;endelsebegin r_dclkinr_dclkin;end end OBUFDS #(.IOSTANDARD(DEFAULT),// Specify the output I/O standard.SLEW(FAST)// Specify the output slew rate)OBUFDS_dclkin(.O(dclkin_p_o),// Diff_p output (connect directly to top-level port).OB(dclkin_n_o),// Diff_n output (connect directly to top-level port).I(r_dclkin)// Buffer input);//-------- output clk -------------------------------------always (posedge high_clk_i or negedge rstn_i)beginif(rstn_i1b0)begin r_clk1d0;endelseif(r_div_shfit[7])begin r_clk~r_clk;endelsebegin r_clkr_clk;end end OBUFDS #(.IOSTANDARD(DEFAULT),// Specify the output I/O standard.SLEW(FAST)// Specify the output slew rate)OBUFDS_clk(.O(clk_p_o),// Diff_p output (connect directly to top-level port).OB(clk_n_o),// Diff_n output (connect directly to top-level port).I(r_clk)// Buffer input);我们实际调试过程中使用的是芯片的2-wire模式因此数据的解析和采集也是根据手册2-wire模式来处理。差分信号的转换//------- da0 -------------------------------------------------------IBUFDS #(.DIFF_TERM(TRUE),// Differential Termination.IBUF_LOW_PWR(FALSE),// Low powerTRUE, Highest performanceFALSE.IOSTANDARD(LVDS)// Specify the input I/O standard)u1_IBUFDS_da0(.O(w_da0),// Buffer output.I(da0_p_i),// Diff_p buffer input (connect directly to top-level port).IB(da0_n_i)// Diff_n buffer input (connect directly to top-level port));//------- da1 -------------------------------------------------------IBUFDS #(.DIFF_TERM(TRUE),// Differential Termination.IBUF_LOW_PWR(FALSE),// Low powerTRUE, Highest performanceFALSE.IOSTANDARD(LVDS)// Specify the input I/O standard)u1_IBUFDS_da1(.O(w_da1),// Buffer output.I(da1_p_i),// Diff_p buffer input (connect directly to top-level port).IB(da1_n_i)// Diff_n buffer input (connect directly to top-level port));//------- db0 -------------------------------------------------------IBUFDS #(.DIFF_TERM(TRUE),// Differential Termination.IBUF_LOW_PWR(FALSE),// Low powerTRUE, Highest performanceFALSE.IOSTANDARD(LVDS)// Specify the input I/O standard)u1_IBUFDS_db0(.O(w_db0),// Buffer output.I(db0_p_i),// Diff_p buffer input (connect directly to top-level port).IB(db0_n_i)// Diff_n buffer input (connect directly to top-level port));//------- db1 -------------------------------------------------------IBUFDS #(.DIFF_TERM(TRUE),// Differential Termination.IBUF_LOW_PWR(FALSE),// Low powerTRUE, Highest performanceFALSE.IOSTANDARD(LVDS)// Specify the input I/O standard)u1_IBUFDS_db1(.O(w_db1),// Buffer output.I(db1_p_i),// Diff_p buffer input (connect directly to top-level port).IB(db1_n_i)// Diff_n buffer input (connect directly to top-level port));由于从芯片给FPGA的信号和FPGA内部不属于同一时钟域因此需要在FPGA内部使用一个高速的时钟满足采样定理进行跨时钟域处理。//-------- delay 2clk -------------------------------------always (posedge high_clk_i or negedge rstn_i)beginif(rstn_i1b0)begin r_da0_r11d0;r_da0_r21d0;r_da1_r11d0;r_da1_r21d0;r_db0_r11d0;r_db0_r21d0;r_db1_r11d0;r_db1_r21d0;endelsebegin r_da0_r1w_da0;r_da0_r2r_da0_r1;r_da1_r1w_da1;r_da1_r2r_da1_r1;r_db0_r1w_db0_inv;// w_db0r_db0_r2r_db0_r1;r_db1_r1w_db1;r_db1_r2r_db1_r1;end end数据转换//-------- r_cha/b_tdata -------------------------------------always (posedge high_clk_i or negedge rstn_i)beginif(rstn_i1b0)begin r_cha_tdata16d0;r_chb_tdata16d0;endelseif((r_div_shfit8h01) || (r_div_shfit 8h04)||(r_div_shfit8h10) || (r_div_shfit 8h40))begin r_cha_tdata{r_cha_tdata[13:0],r_da1_r2,r_da0_r2};r_chb_tdata{r_chb_tdata[13:0],r_db1_r2,r_db0_r2};endelsebegin r_cha_tdatar_cha_tdata;r_chb_tdatar_chb_tdata;end end assign adc_cha_tdata_or_cha_tdata;assign adc_chb_tdata_or_chb_tdata;/////////////data_valid/////////always (posedge high_clk_i or negedge rstn_i)beginif(rstn_i1b0)begin r_fclk_r11d0;r_fclk_r21d0;r_fclk_r31d0;endelsebegin r_fclk_r1w_fclk;// w_fclkr_fclk_r2r_fclk_r1;r_fclk_r3r_fclk_r2;end end assign w_fclk_posr_fclk_r2(!r_fclk_r3);assign w_fclk_neg(!r_fclk_r2)r_fclk_r3;assign w_tvalidw_fclk_pos|w_fclk_neg;3.2.3 配置流程参考2.3.2小节对于ADC3663我们上电工作后默认配置即可实现2-wire 16bit工作模式对于ADC3662我们则需要对其进行配置即给对应寄存器写配置数据此处的代码较为简单我们可以按照上述配置流程自己尝试着实现部分逻辑。按照上述配置流程实现adc366x_drv模块接口的w_cfg_start、w_cfg_data、reg_addr信号逻辑确保正常上电之后开始配置ADC即可。本文验证阶段主要想让大家能了解VIO IP的使用。ps感兴趣想要完整工程或有疑惑的地方没看懂可以联系博主沟通交流学习3.2.4 时钟及复位我们此处以Xilinx的PLL IP为例。外部晶振差分输入时钟200MHz。这里需要特别注意输入信号source的选择。输出160MHz作为数据采集模块主时钟40MHz作为SPI配置模块的输入时钟lock信号标志是否失锁也用做上述两个模块的复位。3.3 测试与验证我们在测试验证环接为了能更快速的验证逻辑的准确性使用ILAVIO的方式来验证ADC366X是否正常工作。3.3.1 VIOVIOVirtual Input/Output是一款可定制化的内核能够实时监控和驱动FPGA现场可编程门阵列内部信号。其输入和输出端口的数量及位宽均可根据需求定制以便与FPGA设计进行接口对接。新建IP首先确定IP名然后第一个界面我们需要确定我们需要用到的端口数量我们验证过程中将寄存器的地址和数值以及读和写的指令通过VIO来控制因此我们需要4个OUTPUT接口。根据实际情况我们确定每个接口的位宽以及初值。我们打开debug界面将VIO参数添加进界面设置好我们需要发送的地址和数据点击写或读使能。3.3.2 时序验证使用在线调试设置好触发条件抓取我们所需观察的信号线。a 写寄存器时序写寄存器地址07值为4B。b 读寄存器时序读寄存器地址07的值:c 功能验证我们为了更便于查看芯片是否配置正确并工作正常选择使用ADC366X芯片的测试模式。测试模式配置主要涉及0x14、0x15、0x16三个寄存器。我们这里将测试模式配置为16bit、递增数加1的工作模式。因此需要将0x14寄存器配置为0x040x15寄存器配置为0x000x16寄存器配置为0x48。可以看出在数据有效时从ADC366X芯片采集到的数据是一个加1的递增数。4 总结从3.2节SPI主从数据传输时序的实现可以看出SPI通信的灵活性很大程度上取决于其时钟模式的选择其中CPOLClock Polarity和CPHAClock Phase的组合决定了数据在时钟边沿的采样和发送时机。这也构成了SPI的不同工作模式。我们如果接触到的ADC芯片比较多的话就可以发现其实有一大部分芯片的驱动都是可以通过SPI方式来实现配置。只要我们认真吸收上面所讲的原理和逻辑就能举一反三轻松拿下SPI接口的应用。SPI基本原理参考链接: FPGA通过SPI实现ADC配置技术详解