6

iCE40UP5K实现Σ-Δ ADC采集及电压表

 2 years ago
source link: https://raincorn.top/ice40up5k_sigma_delta_adc/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在本文中,我们将使用硬禾学堂的“基于iCE40UP5K的FPGA学习平台”开发板来实现一个Σ-Δ ADC采集,并制作一个简易的电压表。在了解相关内容与原理时,发现了许多学习过的知识,通信/电信人狂喜。

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

简易电压表效果图

Σ-Δ ADC采集

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

ADC参数

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

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

采样率与量化位数作为两个重要的指标被显著标注,我们可以得知该芯片的采样率为32MSPS(Million Samples Per Second)、量化位数8 bit。后文我们将会通过理论分析来确定这两个参数。

ADC实现原理

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

Σ-Δ ADC原理图

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

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

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

PWM波形示意图

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

不同占空比下PWM波形频谱图

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

200K PWM经过100K低通滤波器

仿真所用的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");

ADC参数选取

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

量化位数N=8bitN=8 bitN=8bit

采样率fs=200KHzf_s=200KHzfs​=200KHz

PWM产生模块时钟fclk=51.2Mf_{clk}=51.2Mfclk​=51.2M

RC滤波器元件R=1000Ω,C=1000pFR=1000\Omega,C=1000pFR=1000Ω,C=1000pF

RC滤波器截至频率fc=160KHzf_c=160KHzfc​=160KHz

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

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

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

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

  • fclkf_{clk}fclk​是否能满足FPGA布局布线的要求?fclkf_{clk}fclk​的大小满足如下条件:fclk=2NT=2N∗fsf_{clk}=\frac{2^N}{T}={2^N}*{f_s}fclk​=T2N​=2N∗fs​,可以观察到模块时钟的频率随着量化位数的增加呈指数倍增加,因此Σ-Δ ADC常用于低频下高精度的检测。当时钟频率提高时会给FPGA的布线带来困难,考虑到iCE40的定位属于低功耗FPGA,故此处选择51.2M作为时钟频率(当然我建议您可以尝试提高频率以获得更稳定的采样效果)。

    51.2M时钟生成模块

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

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

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

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

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

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

如何克服?

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

Verilog代码实现

该模块包含三个输入与两个输出,具体介绍如下:

  • sys_clksys_rst_n分别为模块的时钟输入与复位输入
  • pwm_adc_in连接到比较器的输出,用于获取比较信息
  • pwm_val为该模块输出的ADC数值,范围0-255
  • pwm_adc_out连接到比较器的反相输入端,通过改变占空比以获得不同的直流电压

检测的原理为pwm_adc_out不断提高占空比,当pwm_adc_in由高到低产生下降沿变化时,输出此时的占空比数值,此时的数值即为ADC采样的数值。

module pwm_adc(
	input sys_clk,
	input sys_rst_n,
	input pwm_adc_in,
	output reg [7:0] pwm_val,
	output pwm_adc_out
);

reg r_adc_in;//Thought the D-reg to get the buffer I/O level.
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		r_adc_in <= 1'b0;
	end else begin
		r_adc_in <= pwm_adc_in;
	end
end

wire adc_in_fall;//When the pwm_adc_in is falling,the adc_in_fall will output high.
assign adc_in_fall = (r_adc_in | pwm_adc_in)&(pwm_adc_in == 1'b0);

//-----The pwm generation-----
reg [7:0] pwm_adder;
reg pwm_adder_overflow; //Complete a counter and generate the overflow(one clock period)
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		pwm_adder <= 8'd0;
		pwm_adder_overflow <= 1'b0;
	end else if(pwm_adder == 8'hff) begin
		pwm_adder <= pwm_adder + 1'b1;
		pwm_adder_overflow <= 1'b1;
	end else begin
		pwm_adder <= pwm_adder + 1'b1;
		pwm_adder_overflow <= 1'b0;
	end
end

reg [7:0] pwm_set;
always@(posedge sys_clk or negedge sys_rst_n)begin
	if(!sys_rst_n)begin
		pwm_set <= 8'd0;
	end else if(adc_in_fall == 1'b1)begin
		pwm_val <= pwm_set;
		pwm_set <= 8'd0;
	end else if(pwm_adder_overflow == 1'b1 && pwm_adc_in == 1'b0)begin
		pwm_set <= pwm_set;
	end else if(pwm_adder_overflow == 1'b1 && pwm_adc_in == 1'b1)begin
		pwm_set <= pwm_set + 1'b1;
	end
end

assign pwm_adc_out = (pwm_adder <= pwm_set) ? 1'b1 : 1'b0;

endmodule

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恰对应着左移8位的操作。显示数据的拼接在dis_dat_buff内进行。

	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码
	);

受限于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)
);

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
);

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

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

1、硬禾学堂. 基于iCE40UP5K的FPGA学习平台[EB/OL]. [2022-1-23]. https://www.eetree.cn/project/detail/131.

2、电子森林. PWM的应用及相应的Verilog代码[EB/OL]. [2022-1-23]. https://www.eetree.cn/wiki/pwm_verilog.

3、吴大正. 《信号与线性系统分析》[M]. 第四版. 高等教育出版社, 2005-8.

4、童诗白、华成英. 《模拟电子技术基础(第五版)》[M]. 第五版. 高等教育出版社, 2015-1.

5、邱关源. 《电路》[M]. 第五版. 高等教育出版社, 2006-5.

完整工程资料可在此处下载:Voltmeter_iCE40UP5K.zip


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK