=====简易电压表设计=====

实验任务

  • 任务:基于 STEP-MAX10M08核心板 和 STEP BaseBoard V3.0底板 完成简易电压表设计并观察调试结果
  • 要求:通过底板上的串行模数转换器ADC芯片测量可调电位计输出电压,并将电压信息显示在核心板的数码管上。
  • 解析:通过FPGA编程驱动串行ADC芯片,得到数字量化的电压信息,将量化的数字信息转换成BCD码形式,同时驱动独立数码管将电压值显示出来。

实验目的

在基础数字电路实验部分我们已经掌握了FPGA驱动独立数码管的原理及方法,本实验主要学习模数转换器ADC的相关知识,串行(SPI接口)ADC芯片ADC081S101的驱动设计,同时学习二进制数转换BCD码的设计方法。

  • 学习模数转换器ADC的相关知识
  • 串行(SPI接口)ADC芯片ADC081S101的驱动设计
  • 学习二进制数转换BCD码的设计方法
  • 完成简易电压表设计实现

设计框图

根据前面的实验解析我们可以得知,该设计可以拆分成三个功能模块实现,

  • ADC081S101driver: 驱动SPI接口ADC芯片实现模拟电压信号采集。 * bintobcd:将二进制数据转换成BCD码的方法。 * Segmentled:通过驱动独立式数码管将电压数据显示出来。

Top-Down层次设计 模块结构设计

实验原理

ADC介绍

数字系统,是用数字信号完成对数字量进行算术运算和逻辑运算的电路称为数字电路,或数字系统。而我们生活的世界是模拟的,想要让数字系统帮我们处理我们模拟世界的问题,就需要一个桥梁来沟通数字系统和模拟系统。

模拟数字系统通信

模数转换器即A/D转换器,或简称ADC,通常是指一个将模拟信号转变为数字信号的电子元件。通常的模数转换器是将一个输入电压信号转换为一个输出的数字信号。由于数字信号本身不具有实际意义,仅仅表示一个相对大小。故任何一个模数转换器都需要一个参考模拟量作为转换的标准,比较常见的参考标准为最大的可转换信号大小。而输出的数字量则表示输入信号相对于参考信号的大小。

数模转换器,又称D/A转换器,简称DAC,它是把数字量转变成模拟的器件。D/A转换器基本上由4个部分组成,即权电阻网络、运算放大器、基准电源和模拟开关。模数转换器中一般都要用到数模转换器,模数转换器即A/D转换器,简称ADC,它是把连续的模拟信号转变为离散的数字信号的器件。

作为模拟系统与数字系统转换的桥梁,ADC和DAC有很多参数指标来标识其性能:

  • 分辨率(转换精度):指ADC或DAC能够采集或输出最小电压与最大电压之比,也是最小输入数字量1与最大输入数字量2n-1之比。分辨率通常用数字量的位数表示,一般为8位、12位、16位等,N位的ADC或DAC的分辨率为2的N次方。
  • 量程(满刻度范围 FSR):指ADC或DAC能够输入或输出模拟电压的变化范围。
  • 建立时间:建立时间是衡量DAC输出达到最终值所需的时间,指接收到要求输出的命令至输出建立到一定精度范围内(通常是0.5LSB、1LSB、2LSB)的时间。
  • 转换时间:指ADC从发出转换指令开始到获得稳定的二进制代码所需要的时间,转换时间与ADC的类型、原理和位数有关。

并行ADC和串行ADC模型

上图两个都是8位ADC模型,分辨率为 2的8次方等于256,即将Vref分成256份,能够分辨的模拟步进为Vref / 256,量化数据N = 256 * Vin / Vref 。

  • 并行ADC与数字电路接口包含一根clk和8根data管脚,clk为芯片时钟管脚,data为芯片数据管脚,每个clk周期从data管脚采集8bit的数据,完成一次模数转换,所以clk频率等于采样率。
  • 串行ADC(以ADC081S101为例)与数字电路接口为三根线(cs,clk,din),兼容三线SPI总线,cs为芯片使能管脚,clk为芯片时钟管脚,din为芯片数据管脚,当ADC芯片使能时每个clk周期从din采集1bit的数据,但是根据ADC081S101的时序,需要16个clk完成一次采样,所以clk频率至少等于采样率的16倍。

ADC模块电路连接

这里我们以STEP BaseBoard V3.0底板上的ADC模块电路,其电路图如下:

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脚短路。

ADC模块驱动设计

前面我们了解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总线更加灵活应用,SPI总线分为4种模式,由两个参数控制:

  • CPOL:时钟极性选择,为0时SCK空闲为低电平,为1时SCK空闲为高电平
  • CPHA:时钟相位选择,为0时在SCK第一个跳变沿采样,为1时在SCK第二个跳变沿采样
MODE0 MODE1
MODE2 MODE3

SPI总线协议4种模式

  • 模式1:CPOL=0,CPHA=0:此时空闲态时,SCLK处于低电平,数据采样是在第1个边沿,也就是 SCLK由低电平到高电平的跳变,所以数据采样是在上升沿,数据发送是在下降沿。
  • 模式2:CPOL=0,CPHA=1:此时空闲态时,SCLK处于低电平,数据发送是在第1个边沿,也就是 SCLK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。
  • 模式3:CPOL=1,CPHA=0:此时空闲态时,SCLK处于高电平,数据采集是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。
  • 模式4:CPOL=1,CPHA=1:此时空闲态时,SCLK处于高电平,数据发送是在第1个边沿,也就是 SCLK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。

ADC081S101管脚说明表:

ADC081S101管脚说明

注:SDATA信号在SCLK的节拍下传输数据,当SCLK下降沿时SDATA更新数据输出,当驱动程序编程时我们要在上升沿采样数据可以得到稳定的输出。

ADC081S101串行通信时序如下图:

ADC081S101通信时序

注:

  1. SCLK空闲时为高电平,CPOL = 1,上升沿(第二个边沿)采样,CPHA = 1,如果例化通用SPI核完成设计,需要采用SPI的第四种工作模式。
  2. CS信号拉低有效,经过16个时钟完成一次ADC转换并采样,采样回来的数据前3位无效,接下来为DB7~DB0(有效数据),再接下来为无效数据。

针对ADC081S101时序,我们用Verilog设计一个计数器,当计数器值不同时完成不同操作,实现一次ADC采样,程序实现如下:

reg [7:0] cnt; //计数器
always @(posedge clk or negedge rst_n)
	if(!rst_n) cnt <= 1'b0;
	else if(cnt >= 8'd34) cnt <= 1'b0;
	else cnt <= cnt + 1'b1;
 
reg [7:0] data;
always @(posedge clk or negedge rst_n)
	if(!rst_n) begin
		adc_cs <= HIGH; adc_clk <= HIGH;
	end else case(cnt)
		8'd0 :  begin adc_cs <= HIGH; adc_clk <= HIGH; end
		8'd1 :  begin adc_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:	
				begin adc_cs <= LOW;  adc_clk <= LOW;  end
		8'd3 :  begin adc_cs <= LOW;  adc_clk <= HIGH; end //0
		8'd5 :  begin adc_cs <= LOW;  adc_clk <= HIGH; end //1
		8'd7 :  begin adc_cs <= LOW;  adc_clk <= HIGH; end //2
		8'd9 :  begin adc_cs <= LOW;  adc_clk <= HIGH; data[7] <= adc_dat; end //3
		8'd11 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[6] <= adc_dat; end //4
		8'd13 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[5] <= adc_dat; end //5
		8'd15 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[4] <= adc_dat; end //6
		8'd17 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[3] <= adc_dat; end //7
		8'd19 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[2] <= adc_dat; end //8
		8'd21 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[1] <= adc_dat; end //9
		8'd23 : begin adc_cs <= LOW;  adc_clk <= HIGH; data[0] <= adc_dat; end //10
		8'd25 : begin adc_cs <= LOW;  adc_clk <= HIGH; adc_data <= data; end //11
		8'd27 : begin adc_cs <= LOW;  adc_clk <= HIGH; adc_done <= HIGH; end //12
		8'd29 : begin adc_cs <= LOW;  adc_clk <= HIGH; adc_done <= LOW; end //13
		8'd31 : begin adc_cs <= LOW;  adc_clk <= HIGH; end //14
		8'd33 : begin adc_cs <= LOW;  adc_clk <= HIGH; end //15
		8'd34 : begin adc_cs <= HIGH;  adc_clk <= HIGH; end
		default : begin adc_cs <= HIGH;  adc_clk <= HIGH;  end
	endcase

到这我们就完成了串行ADC芯片ADC081S101的驱动设计,整个采样周期用了35个系统时钟,如果我们采用12MHz时钟作为该模块系统时钟,采样率Fs = 12M/35 = 343Ksps,ADC主频Fsclk = 12 MHz /2 = 6MHz。

ADC081S101主频及采样率要求如下,按照要求我们当前的主频和采样率不足,所以在使用该模块时,可以使用更高的时钟(比如24MHz)以达到芯片的要求

ADC081S101速度

注:时钟频率Fsclk,最小值为10MHz,最大值为20MHz,采样率在500Ksps~1Msps

模块接口如下:clk和rstn为系统时钟及复位,adccs,adcclk和adcdat为ADC控制管脚,adcdata为ADC采样数据,adcdone产生一个脉冲对应adc_data得到一个有效数据

module ADC081S101_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 PLL

Pll模块和ADC081S101_driver模块的连接程序实现如下:

wire clk_24mhz,locked;
pll u1
(
.areset				(!rst_n			), //pll模块的复位为高有效
.inclk0				(clk			), //12MHz系统时钟输入
.c0					(clk_24mhz		), //24MHz时钟输出
.locked				(locked			)  //pll lock信号输出
);
 
wire adc_done;
wire [7:0] adc_data;
//使用I2C总线驱动PCF8591的ADC功能,例化
ADC081S101_driver u2
(
.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、继续左移的过程直至全部移位完成

Bin to BCD

二进制转BCD码程序实现如下:

reg		[35:0]		shift_reg; 
always@(bin_code or rst_n)begin
	shift_reg = {20'h0,bin_code};
	if(!rst_n) bcd_code = 0; 
	else begin 
		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_led u4
(
.seg_dot			(1'b1			),	//seg_dot input
.seg_data			(bcd_code[19:16]),	//seg_data input
.segment_led		(seg_1			)	//MSB~LSB = SEG,DP,G,F,E,D,C,B,A
);
 
//分位数码管模块例化
Segment_led u5
(
.seg_dot			(1'b0			),	//seg_dot input
.seg_data			(bcd_code[15:12]),	//seg_data input
.segment_led		(seg_2			)	//MSB~LSB = SEG,DP,G,F,E,D,C,B,A
);

综合后的设计框图如下:

RTL设计框图

实验步骤

  1. 双击打开Quartus Prime工具软件;
  2. 新建工程:File → New Project Wizard(工程命名,工程目录选择,设备型号选择,EDA工具选择);
  3. 新建文件:File → New → Verilog HDL File,键入设计代码并保存;
  4. 设计综合:双击Tasks窗口页面下的Analysis & Synthesis对代码进行综合;
  5. 管脚约束:Assignments → Assignment Editor,根据项目需求分配管脚;
  6. 设计编译:双击Tasks窗口页面下的Compile Design对设计进行整体编译并生成配置文件;
  7. 程序烧录:点击Tools → Programmer打开配置工具,Program进行下载;
  8. 观察设计运行结果。

实验现象

将程序下载到FPGA中,P3接口用短路帽将1、2脚短路,旋转底板右上角的电位计,观察核心板数码管变化,如果有万用表可以测量P3短路处的电压,与数码管显示对比。