基于iCE40UP5K的数字电压表
在本文中,我们将使用硬禾学堂的“基于iCE40UP5K的FPGA学习平台”开发板来实现一个Σ-Δ ADC采集,并制作一个简易的数字电压表。
标签
FPGA
测试
数字逻辑
显示
ADC
电压表
2022寒假在家练
raincorn
更新2022-03-02
河南工业大学
714

在本文中,我们将使用硬禾学堂的“基于iCE40UP5K的FPGA学习平台”开发板来实现一个Σ-Δ ADC采集,并制作一个简易的电压表。

1、目标

基于Lattice iCE40UP5K实现一个Σ-Δ ADC采集,采集后的电压将会在OLED屏幕上显示,实现一个简易的电压表,效果如下图所示:

FgW2txV3aXUVJVu-ui5ClGVAovYF

该项目实现的流程图暂时如下:

FnsqecEOeXBxhU3VwV47aFECJ5Ii

2、Σ-Δ ADC采集

在大多数FPGA芯片上均无ADC外设,当需要低成本/多通道采集模拟量时,可以考虑此方案。同样地,此学习平台上也没有使用集成ADC与DAC模块,其ADC采集使用了PWM+电压比较器实现Σ-Δ ADC,其DAC输出使用了R-2R权电阻网络来实现。

2.1、ADC参数

在讨论一块ADC性能的时候,往往关注两个指标:采样率、量化位数。比如我们常用的黑金AN108模块上,采用了AD9280作为其ADC,查阅ADI官网其介绍如下:

AD9280是一款单芯片、8位、32 MSPS模数转换器(ADC),采用单电源供电,内置一个片内采样保持放大器和基准电压源。它采用多级差分流水线架构,数据速率达32 MSPS,在整个工作温度范围内保证无失码。

采样率与量化位数作为两个重要的指标被显著标注,我们可以得知该芯片的采样率为32MSPS(Million Samples Per Second)、量化位数8 bit。

2.2、ADC实现原理

在该学习平台上,其PWM+电压比较器实现Σ-Δ ADC的原理图如下:

FvtgI3_ZMWin2vZYAgR9Y7sHhbPF

可以看到,在该电路图中包含一个比较器,其同相输入端接模拟输入,反向输入端接PWM输入,比较后输出结果。在反相输入端PWM_V2连接着一个电阻和一个电容,其构成一个简易的一阶RC滤波器。该一阶RC滤波器截至频率为fc​=1/2*pi*RC​,工程上将幅度值下降到原来的0.707倍(-3dB)称作截止频率点,电路的带宽也由-3dB点定义。通过对输入的PWM波形进行滤波可以得到一个近似的直流值,与Ain2进行比较输出,通过不断调节占空比(假设由低到高),当输出C_OUT2由高变低时便可使用占空比来表示模拟输入量。

滤波的目的是得到直流量,一个PWM波进行傅里叶变换后可观察到其存在许多高次谐波,一阶RC的目的便是将基频与谐波滤除。

我们知道,一个典型的PWM波形脉冲区间包括高电平区间tH​与低电平区间tL​,其占空比常定义为duty=tH​/tL+​tH​​。接触过单片机的读者可能会联想起PWM控制电机转速,LED亮度的实验,当调节占空比时可以改变上述参量,本电路中经过RC低通滤波器便可得到一个近似的直流电压值。

FpHQ5gxI8-dHAc5ASsx8TCZdHlF2

当对不同占空比的PWM波形(频率均为200K)做傅里叶变换进行频谱分析时,可以得到如下频谱图。可以观察到,当改变不同占空比时,PWM波的主要频率分量仍集中在200K,400K附近,改变的主要是直流分量。

Fg80hS4L_EJPxng5irdGb6lolzz0

当给此PWM信号加以截至频率100KHz,阻带衰减60dB的理想低通滤波器后可以得到如下波形图。可以观察到随着时间的推移信号逐渐趋近于直流,也可以理解为200K与400K的分量被滤除得到直流分量。

FlCiu7wYkAPFFrJv_3nVv6A7jrZw

仿真所用的MATLAB代码如下:

 
close all;clear;clc;

fs = 1e6; %sample rate is 1M
t = 0:1/fs:1e-1-1/fs; %generate the data between 0-1ms to rise the fft resulation
n = length(t);
n_index = 0:n-1;
f_index = n_index*fs/n;

% 25%
x = square(2*pi*200e3*t,25) + 1; %generate the PWM with special duty
subplot(321);stem(t,x);axis([0 2e-5 0 2]);title("Time: 25% Duty");
mag_x= 20*log10(abs(fft(x)));
subplot(322);plot(f_index(1:n/2),mag_x(1:n/2));title("Spec(dB): 25% Duty");

% 50%
x = square(2*pi*200e3*t,50) + 1; %generate the PWM with special duty
subplot(323);stem(t,x);axis([0 2e-5 0 2]);title("Time: 50% Duty");
mag_x= 20*log10(abs(fft(x)));
subplot(324);plot(f_index(1:n/2),mag_x(1:n/2));title("Spec(dB): 50% Duty");

% 75%
x = square(2*pi*200e3*t,75) + 1; %generate the PWM with special duty
subplot(325);stem(t,x);axis([0 2e-5 0 2]);title("Time: 75% Duty");
mag_x= 20*log10(abs(fft(x)));
subplot(326);plot(f_index(1:n/2),mag_x(1:n/2));title("Spec(dB): 75% Duty");
 

2.3、ADC参数选取

前文所述,ADC有两个重要的指标:采样率、量化位数。本节将介绍如何选取这两个颇为重要的参数,很多时候参数的选择涉及多方面的权衡。

在参数的选择过程中,可以参考如下步骤进行综合考量:

  • fs​的选择是否满足要求?当需要采样一个非直流信号时,需要满足奈奎斯特采样定理,即fs≥2fH​。

  • N的选择是否满足要求?一个ADC的分辨率很大程度上取决于量化位数,分辨率受供电电压与位数二者共同决定,即有fres​=VDD/2^n​​。同时,其信噪比满足SNR=6N,即每提高一位可以提高6dB的信噪比。例如在一个3.3V供电的系统中,8位量化最高可以做到0.012890625V的分辨率。

  • fs​>fc​是否满足?很多情况下要求采样率应尽可能大于截止频率,这样信号的直流分量便可以较好地滤除出来。

RC较大

在ADC实现原理一节中我们详细分析了滤波器存在的必要,可以观察到当RC越大时滤波器的截止频率也就越高,更有利于PWM信号直流分量的提取。但同时存在一个时间常数的概念,时间常数τ≈0.69RC ,当不受限制地提高RC将会提高时间常数进而减缓电路的响应时间。故我们可以总结如下特性:

优点:便于直流分量的提取,减小滤波后直流信号出现波动。

缺点:提高时间常数,电路的响应速度降低。

RC较小

RC较小时的特性与之相反,总结如下:

优点:时间常数小,电路响应速度快。

缺点:由于截至频率提高,因此需要更大的采样率才能达到较好的效果。

如何克服?

加入电感,提高滤波阶次,加入有源滤波,提高采样率。

2.4、Verilog代码实现

直接在顶层例化Lattice提供的ADC采样代码有:

wire [7:0] pwm_val;
wire sample_rdy;
ADC_top u_ADC_top(
	.clk_in(clk_pwm_adc),
	.rstn(sys_rst_n),
	.digital_out(pwm_val),
	.analog_cmp(pwm_adc_in),	
	.analog_out(pwm_adc_out),
	.sample_rdy(sample_rdy)
);

3、BCD码生成

由于ADC模块产生的数值为十进制,如需将其显示出来则需要一个BCD码型转换模块提取个、十、百、千位。不同于单片机内部使用乘除法获取各位的操作,在FPGA内部乘除法十分消耗资源,因此往往采用移位判断法,具体代码参考野火。

module  bcd_8421
(
    input   wire            sys_clk     ,   //系统时钟,频率50MHz
    input   wire            sys_rst_n   ,   //复位信号,低电平有效
    input   wire    [19:0]  data        ,   //输入需要转换的数据

    output  reg     [3:0]   unit        ,   //个位BCD码
    output  reg     [3:0]   ten         ,   //十位BCD码
    output  reg     [3:0]   hun         ,   //百位BCD码
    output  reg     [3:0]   tho         ,   //千位BCD码
    output  reg     [3:0]   t_tho       ,   //万位BCD码
    output  reg     [3:0]   h_hun           //十万位BCD码
);

//********************************************************************//
//******************** Parameter And Internal Signal *****************//
//********************************************************************//

//reg   define
reg     [4:0]   cnt_shift   ;   //移位判断计数器
reg     [43:0]  data_shift  ;   //移位判断数据寄存器
reg             shift_flag  ;   //移位判断标志信号

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//

//cnt_shift:从0到21循环计数
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_shift   <=  5'd0;
    else    if((cnt_shift == 5'd21) && (shift_flag == 1'b1))
        cnt_shift   <=  5'd0;
    else    if(shift_flag == 1'b1)
        cnt_shift   <=  cnt_shift + 1'b1;
    else
        cnt_shift   <=  cnt_shift;
       
//data_shift:计数器为0时赋初值,计数器为1~20时进行移位判断操作
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_shift  <=  44'b0;
    else    if(cnt_shift == 5'd0)
        data_shift  <=  {24'b0,data};
    else    if((cnt_shift <= 20) && (shift_flag == 1'b0))
        begin
            data_shift[23:20]   <=  (data_shift[23:20] > 4) ? (data_shift[23:20] + 2'd3) : (data_shift[23:20]);
            data_shift[27:24]   <=  (data_shift[27:24] > 4) ? (data_shift[27:24] + 2'd3) : (data_shift[27:24]);
            data_shift[31:28]   <=  (data_shift[31:28] > 4) ? (data_shift[31:28] + 2'd3) : (data_shift[31:28]);
            data_shift[35:32]   <=  (data_shift[35:32] > 4) ? (data_shift[35:32] + 2'd3) : (data_shift[35:32]);
            data_shift[39:36]   <=  (data_shift[39:36] > 4) ? (data_shift[39:36] + 2'd3) : (data_shift[39:36]);
            data_shift[43:40]   <=  (data_shift[43:40] > 4) ? (data_shift[43:40] + 2'd3) : (data_shift[43:40]);
        end
    else    if((cnt_shift <= 20) && (shift_flag == 1'b1))
        data_shift  <=  data_shift << 1;
    else
        data_shift  <=  data_shift;

//shift_flag:移位判断标志信号,用于控制移位判断的先后顺序
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        shift_flag  <=  1'b0;
    else
        shift_flag  <=  ~shift_flag;

//当计数器等于20时,移位判断操作完成,对各个位数的BCD码进行赋值
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        begin
            unit    <=  4'b0;
            ten     <=  4'b0;
            hun     <=  4'b0;
            tho     <=  4'b0;
            t_tho   <=  4'b0;
            h_hun   <=  4'b0;
        end
    else    if(cnt_shift == 5'd21)
        begin
            unit    <=  data_shift[23:20];
            ten     <=  data_shift[27:24];
            hun     <=  data_shift[31:28];
            tho     <=  data_shift[35:32];
            t_tho   <=  data_shift[39:36];
            h_hun   <=  data_shift[43:40];
        end

endmodule

BCD模块在OLED模块内部例化,其例化代码如下。注意此处对ADC采样进来的数值进行了近似操作以减少乘法器的使用,由于在3.3V下8位量化的分辨率为0.012890625,故将其扩大10000倍取128恰对应着左移7位的操作。显示数据的拼接在dis_dat_buff内进行。当然,为了提高精度也可直接将移位运算改为乘以129。

wire [15:0]				dis_dat_mult = dis_dat << 7; //The voltage multed by 128(3.3/256 = 0.01289) to get a similar number
	wire [(6*8-1):0]		dis_dat_buff; //The buffer of dis_dat
	wire [3:0]				dis_ten;
	wire [3:0]				dis_hun;
	wire [3:0]				dis_tho;
	wire [3:0]				dis_t_tho;
	assign dis_dat_buff = {4'd0,dis_t_tho,".",4'd0,dis_tho,4'd0,dis_hun,4'd0,dis_ten,"V"};
	bcd_8421 u_bcd_8421(
	.sys_clk(clk), //系统时钟,频率50MHz
	.sys_rst_n(rst_n), //复位信号,低电平有效
    .data(dis_dat_mult), //输入需要转换的数据

    .unit(), //个位BCD码
    .ten(dis_ten), //十位BCD码
    .hun(dis_hun), //百位BCD码
    .tho(dis_tho), //千位BCD码
    .t_tho(dis_t_tho), //万位BCD码
    .h_hun() //十万位BCD码
	);

4、顶层例化

受限于iCE40UP5K布线资源的问题,当系统锁相环时钟由外部输入时,外部时钟便不可再用于其它模块的时钟。因此此处使用了该FPGA芯片的内部时钟,通过HSOSC原语进行例化,注意Radiant中的原语与其它IDE不相同。

module voltmeter(
//	input in_clk, //Use the internal clock source to avoid restrict
	input in_rst_n,
	input pwm_adc_in,
//	input debug, //The debug wire is connected to switch
//	output oled_csn, //The cs pin is disconnected
	output oled_rst,
	output oled_dcn,
	output oled_clk,
	output oled_dat,
	output pwm_adc_out
);

wire sys_clk;
HSOSC 
#( 
  .CLKHF_DIV ("0b10") 
) u_HSOSC ( 
  .CLKHFEN (1'b1), 
  .CLKHFPU (1'b1), 
  .CLKHF   (sys_clk) 
);

wire sys_rst_n,clk_gen_locked,clk_pwm_adc;
assign sys_rst_n = in_rst_n & clk_gen_locked;
clk_gen u_clk_gen(
	.ref_clk_i(sys_clk),
	.rst_n_i(in_rst_n),
	.lock_o(clk_gen_locked),
	.outcore_o(),
	.outglobal_o(clk_pwm_adc) //The pll out is connected with the global clock network
);

wire [7:0] pwm_val;
//pwm_adc u_pwm_adc(
	//.sys_clk(clk_pwm_adc),
	//.sys_rst_n(sys_rst_n),
	
	//.pwm_adc_in(pwm_adc_in),
	
	//.pwm_val(pwm_val),
	//.pwm_adc_out(pwm_adc_out)
//);

wire sample_rdy;
ADC_top u_ADC_top(
	.clk_in(clk_pwm_adc),
	.rstn(sys_rst_n),
	.digital_out(pwm_val),
	.analog_cmp(pwm_adc_in),	
	.analog_out(pwm_adc_out),
	.sample_rdy(sample_rdy)
);

oled12864 u_oled12864(
	.clk(sys_clk),		//The system clock
	.rst_n(sys_rst_n),		//The system reset
	
	.dis_dat(pwm_val),
//	.debug(debug),
	
	.oled_csn(),	//OLED ENABLE
	.oled_rst(oled_rst),	//OLED RESET
	.oled_dcn(oled_dcn),	//OLED DATA/COMMAND CONTROL
	.oled_clk(oled_clk),	//OLED CLOCK
	.oled_dat(oled_dat)	//OLED DATA
);



endmodule

5、其它

本项目所占用的资源情况如下:

Fq-XnZ1S3rUMPSITskYa7jLV0PVn

之前一直使用Xilinx、Altera的FPGA进行开发,相比于Lattice而言,其开发工具更显繁琐。Lattice的开发工具使用十分清爽且快,其综合布线一个OLED显示的工程只需要30s,在Vivado上都不够软件的加载时间。

当然,Lattice的开发工具也存在缺点。譬如:时钟网络的布线资源受限;Lattice LSE的综合工具并不好用,从其带有一个Synplify Pro的选项便可看出,很多时候LSE综合不出来,更换Synplify便可解决。当然,以上这些缺点在学习时综合布线飞快面前显得就不那么重要。

 

附件下载
project.zip
Voltmeter_impl_1.rbt
团队介绍
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号