在基础数字电路实验部分我们已经掌握了FPGA驱动独立数码管的原理及方法,本实验主要学习模数转换器ADC的相关知识,串行(SPI接口)ADC芯片ADC081S101的驱动设计,同时学习二进制数转换BCD码的设计方法。
根据前面的实验解析我们可以得知,该设计可以拆分成三个功能模块实现,
数字系统,是用数字信号完成对数字量进行算术运算和逻辑运算的电路称为数字电路,或数字系统。而我们生活的世界是模拟的,想要让数字系统帮我们处理我们模拟世界的问题,就需要一个桥梁来沟通数字系统和模拟系统。
模数转换器即A/D转换器,或简称ADC,通常是指一个将模拟信号转变为数字信号的电子元件。通常的模数转换器是将一个输入电压信号转换为一个输出的数字信号。由于数字信号本身不具有实际意义,仅仅表示一个相对大小。故任何一个模数转换器都需要一个参考模拟量作为转换的标准,比较常见的参考标准为最大的可转换信号大小。而输出的数字量则表示输入信号相对于参考信号的大小。
数模转换器,又称D/A转换器,简称DAC,它是把数字量转变成模拟的器件。D/A转换器基本上由4个部分组成,即权电阻网络、运算放大器、基准电源和模拟开关。模数转换器中一般都要用到数模转换器,模数转换器即A/D转换器,简称ADC,它是把连续的模拟信号转变为离散的数字信号的器件。
作为模拟系统与数字系统转换的桥梁,ADC和DAC有很多参数指标来标识其性能:
上图两个都是8位ADC模型,分辨率为 2的8次方等于256,即将Vref分成256份,能够分辨的模拟步进为Vref / 256,量化数据N = 256 * Vin / Vref 。
这里我们以STEP BaseBoard V3.0底板上的ADC模块电路,其电路图如下:
如ADC模块电路所示,FPGA直接连接ADC081S101芯片的控制端,ADC有6个管脚,3脚Vin为VCC和Vref功能复用,即Vin = VCC = Vref。ADC前端是运放电路LMV721,运放模块为电压跟随电路,再往前端是一个跳冒排针,用来选择ADC采样信号的来源,当短路帽将1、2脚短路时,ADC采集电位计电压,当短路帽将2、3脚短路时,ADC采射频端子或P4排针信号。本设计我们是采样旋转编码器的电压,所以需要用短路帽将1、2脚短路。
前面我们了解ADC081S101芯片和FPGA之间连接有三根线(cs、clk、din),兼容SPI总线,SPI是串行外设接口(Serial Peripheral Interface)的缩写。SPI是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线(cs、sck、mosi、miso),事实上3根也可以(单向传输时),占用管脚少节约了芯片的管脚,同时为PCB的布局上节省空间,正是出于这种简单易用的特性,如今越来越多的芯片集成这种通信协议。
SPI设备分为主设备和从设备,设备之间共用sck、mosi和miso,另外每个从设备有一根cs线(不共用),通信在主设备和从设备之间进行,从设备与从设备之间无法直接通信,主设备可以同时连接多个从设备,当主设备和某个从设备通信时,先控制该从设备cs信号拉低,然后通过sck、mosi和miso进行数据传输。
为了让SPI总线更加灵活应用,SPI总线分为4种模式,由两个参数控制:
![]() | ![]() |
![]() | ![]() |
SPI总线协议4种模式
ADC081S101管脚说明表:
注:SDATA信号在SCLK的节拍下传输数据,当SCLK下降沿时SDATA更新数据输出,当驱动程序编程时我们要在上升沿采样数据可以得到稳定的输出。
ADC081S101串行通信时序如下图:
注:
针对ADC081S101时序,我们用Verilog设计一个计数器,当计数器值不同时完成不同操作,实现一次ADC采样,程序实现如下:
reg[7:0]cnt;//计数器 always@(posedgeclkornegedgerst_n) if(!rst_n)cnt<=1'b0; elseif(cnt>=8'd34)cnt<=1'b0; elsecnt<=cnt+1'b1; reg[7:0]data;always@(posedgeclkornegedgerst_n) if(!rst_n)begin adc_cs<=HIGH;adc_clk<=HIGH; endelsecase(cnt) 8'd0:beginadc_cs<=HIGH; adc_clk<=HIGH;end 8'd1:beginadc_cs<=LOW; adc_clk<=HIGH;end 8'd2,8'd4,8'd6,8'd8,8'd10,8'd12,8'd14,8'd16, 8'd18,8'd20,8'd22,8'd24,8'd26,8'd28,8'd30,8'd32: beginadc_cs<=LOW; adc_clk<=LOW; end 8'd3:beginadc_cs<=LOW; adc_clk<=HIGH; end//0 8'd5:beginadc_cs<=LOW; adc_clk<=HIGH; end//1 8'd7:beginadc_cs<=LOW; adc_clk<=HIGH; end//2 8'd9:beginadc_cs<=LOW; adc_clk<=HIGH; data[7]<=adc_dat; end//3 8'd11:beginadc_cs<=LOW; adc_clk<=HIGH;data[6]<=adc_dat; end//4 8'd13:beginadc_cs<=LOW; adc_clk<=HIGH;data[5]<=adc_dat; end//5 8'd15:beginadc_cs<=LOW; adc_clk<=HIGH; data[4]<=adc_dat; end//6 8'd17:beginadc_cs<=LOW; adc_clk<=HIGH;data[3]<=adc_dat; end//7 8'd19:beginadc_cs<=LOW; adc_clk<=HIGH;data[2]<=adc_dat; end//8 8'd21:beginadc_cs<=LOW; adc_clk<=HIGH; data[1]<=adc_dat; end//9 8'd23:beginadc_cs<=LOW; adc_clk<=HIGH;data[0]<=adc_dat; end//10 8'd25:beginadc_cs<=LOW; adc_clk<=HIGH;adc_data<=data; end//11 8'd27:beginadc_cs<=LOW; adc_clk<=HIGH;adc_done<=HIGH; end//12 8'd29:beginadc_cs<=LOW; adc_clk<=HIGH;adc_done<=LOW; end//13 8'd31:beginadc_cs<=LOW; adc_clk<=HIGH; end//14 8'd33:beginadc_cs<=LOW; adc_clk<=HIGH; end//15 8'd34:beginadc_cs<=HIGH; adc_clk<=HIGH; end default:beginadc_cs<=HIGH; adc_clk<=HIGH; end endcase
到这我们就完成了串行ADC芯片ADC081S101的驱动设计,整个采样周期用了35个系统时钟,如果我们采用12MHz时钟作为该模块系统时钟,采样率Fs = 12M/35 = 343Ksps,ADC主频Fsclk = 12MHz/2 = 6MHz。
ADC081S101主频及采样率要求如下,按照要求我们当前的主频和采样率不足,所以在使用该模块时,可以使用更高的时钟(比如24MHz)以达到芯片的要求
注:时钟频率Fsclk,最小值为10MHz,最大值为20MHz,采样率在500Ksps~1Msps
模块接口如下:clk和rstn为系统时钟及复位,adccs,adcclk和adcdat为ADC控制管脚,adcdata为ADC采样数据,adcdone产生一个脉冲对应adc_data得到一个有效数据
moduleADC081S101_driver ( input clk, //系统时钟 input rst_n, //系统复位,低有效 output reg adc_cs, //SPI总线CS output reg adc_clk, //SPI总线SCK input adc_dat, //SPI总线SDA output reg adc_done, //ADC采样完成标志 output reg[7:0] adc_data //ADC采样数据 );
因为需要更高的时钟供ADC模块使用,我们例化pll核得到24MHz时钟,例化PLL的方法我们在基础数字电路实验部分已经练习过,这里就简单描述一下过程
打开Tools菜单下的IP Catalog工具,依次找到Libraty → Basic Functions → Clocks; PLLs and Resets → PLL → ALTPLL,打开ALTPLL弹出配置界面,配置inclk0输入为12MHz,配置c0的时钟输出为24MHz,其他所有选项全部默认,点击Finish完成pll的IP核例化。
在顶层模块VoltageMeas中,同时例化pll模块和ADC081S101driver模块,并将pll的c0输出与ADC081S101_driver模块的clk连线。
Pll模块和ADC081S101_driver模块的连接程序实现如下:
wireclk_24mhz,locked; pllu1 ( .areset (!rst_n ),//pll模块的复位为高有效 .inclk0 (clk ),//12MHz系统时钟输入 .c0 (clk_24mhz ),//24MHz时钟输出 .locked (locked )//plllock信号输出 ); wireadc_done; wire[7:0]adc_data;//使用I2C总线驱动PCF8591的ADC功能,例化 ADC081S101_driveru2(.clk (clk_24mhz ), //系统时钟 .rst_n (rst_n ), //系统复位,低有效 .adc_cs (adc_cs ), //SPI总线CS .adc_clk (adc_clk ), //SPI总线SCK .adc_dat (adc_dat ), //SPI总线SDA .adc_done (adc_done ), //ADC采样完成标志 .adc_data (adc_data ) //ADC采样数据 );
现在可以得到ADC采样数据了,假设ADC模拟输入电压为3.3V,理论上我们得到的采样数据adc_data应该为8’hff,而电压表最终显示在数码管上的数据应该为3.3,我们如何将8’hff转换成可以显示的3.3数据呢?这就设计到ADC量化数据的逆向运算了,
我们知道量化运算 N = 256 * Vin / Vref,
那么逆向运算为Vin = N * Vref / 256,其中Vref = 3.3V,所以Vin = N * 0.0129
所以我们需要用FPGA计算adc_data * 0.0129的结果,然后为了使用十进制的显示,先将结果进行BCD转码,然后显示在数码管上。
将ADC采样数据按规则转换为电压数据(乘以0.0129),这里我们直接乘以129,得到的数据经过BCD转码后小数点左移4位即可,程序实现如下:
wire[15:0]bin_code=adc_data*16'd129;
将二进制数转换成BCD码的形式,采用左移加三的算法(以8’hff为例): 1、左移要转换的二进制码1位 2、左移之后,BCD码分别置于百位、十位、个位 3、如果移位后所在的BCD码列大于或等于5,则对该值加3 4、继续左移的过程直至全部移位完成
二进制转BCD码程序实现如下:
reg [35:0] shift_reg; always@(bin_codeorrst_n)begin shift_reg={20'h0,bin_code}; if(!rst_n)bcd_code=0; elsebegin repeat(16)begin//循环16次 //BCD码各位数据作满5加3操作, if(shift_reg[19:16]>=5)shift_reg[19:16]=shift_reg[19:16]+2'b11; if(shift_reg[23:20]>=5)shift_reg[23:20]=shift_reg[23:20]+2'b11; if(shift_reg[27:24]>=5)shift_reg[27:24]=shift_reg[27:24]+2'b11; if(shift_reg[31:28]>=5)shift_reg[31:28]=shift_reg[31:28]+2'b11; if(shift_reg[35:32]>=5)shift_reg[35:32]=shift_reg[35:32]+2'b11; shift_reg=shift_reg<<1; end bcd_code=shift_reg[35:16]; end end
最后得到20位的数据输出,每4位表示一个BCD码,所以有5位有效数据,这里我们还需要将小数点左移4位,计算出来的数应该是X.XXXX伏特,1个整数位和4个小数位,核心板上只有两个数码管,取最高的两个BCD码显示到数码管X.X伏特,个位小数点点亮,分位小数点熄灭,程序实现如下:
//个位数码管模块例化 Segment_ledu4(.seg_dot (1'b1 ), //seg_dotinput .seg_data (bcd_code[19:16]), //seg_datainput .segment_led (seg_1 ) //MSB~LSB=SEG,DP,G,F,E,D,C,B,A );//分位数码管模块例化 Segment_ledu5(.seg_dot (1'b0 ), //seg_dotinput .seg_data (bcd_code[15:12]), //seg_datainput .segment_led (seg_2 ) //MSB~LSB=SEG,DP,G,F,E,D,C,B,A );
综合后的设计框图如下:
将程序下载到FPGA中,P3接口用短路帽将1、2脚短路,旋转底板右上角的电位计,观察核心板数码管变化,如果有万用表可以测量P3短路处的电压,与数码管显示对比。