在本文中,我们将使用硬禾学堂的“基于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滤波器截至频率为$f_{c}=\frac{1}{2\pi RC}$,工程上将幅度值下降到原来的0.707倍(-3dB)称作截止频率点,电路的带宽也由-3dB点定义。通过对输入的PWM波形进行滤波可以得到一个近似的直流值,与Ain2进行比较输出,通过不断调节占空比(假设由低到高),当输出C_OUT2由高变低时便可使用占空比来表示模拟输入量。

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

我们知道,一个典型的PWM波形脉冲区间包括高电平区间$t_H$与低电平区间$t_L$,其占空比常定义为$duty=\frac{t_H}{t_H+t_L}$。接触过单片机的读者可能会联想起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=8 bit$

采样率$f_s=200KHz$

PWM产生模块时钟$f_{clk}=51.2M$

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

RC滤波器截至频率$f_c=160KHz$

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

  • $f_s$的选择是否满足要求?当需要采样一个非直流信号时,需要满足奈奎斯特采样定理,即$f_s\ge2f_H$。
  • $N$的选择是否满足要求?一个ADC的分辨率很大程度上取决于量化位数,分辨率受供电电压与位数二者共同决定,即有$f_{res}=\frac{V_{DD}}{2^n}$。同时,其信噪比满足$SNR=6N$,即每提高一位可以提高$6dB$的信噪比。例如在一个3.3V供电的系统中,8位量化最高可以做到0.012890625V的分辨率。
  • $f_s > f_c$是否满足?很多情况下要求采样率应尽可能大于截止频率,这样信号的直流分量便可以较好地滤除出来。
  • $f_{clk}$是否能满足FPGA布局布线的要求?$f_{clk}$的大小满足如下条件:$f_{clk}=\frac{2^N}{T}={2^N}*{f_s}$,可以观察到模块时钟的频率随着量化位数的增加呈指数倍增加,因此Σ-Δ ADC常用于低频下高精度的检测。当时钟频率提高时会给FPGA的布线带来困难,考虑到iCE40的定位属于低功耗FPGA,故此处选择51.2M作为时钟频率(当然我建议您可以尝试提高频率以获得更稳定的采样效果)。

    51.2M时钟生成模块

RC较大

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

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

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

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

标签: FPGA, ADC, 模拟

添加新评论