在本文中,我们将使用硬禾学堂的“基于iCE40UP5K的FPGA学习平台”开发板来实现一个Σ-Δ ADC采集,并制作一个简易的电压表。
1、目标
基于Lattice iCE40UP5K实现一个Σ-Δ ADC采集,采集后的电压将会在OLED屏幕上显示,实现一个简易的电压表,效果如下图所示:
该项目实现的流程图暂时如下:
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的原理图如下:
可以看到,在该电路图中包含一个比较器,其同相输入端接模拟输入,反向输入端接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低通滤波器便可得到一个近似的直流电压值。
当对不同占空比的PWM波形(频率均为200K)做傅里叶变换进行频谱分析时,可以得到如下频谱图。可以观察到,当改变不同占空比时,PWM波的主要频率分量仍集中在200K,400K附近,改变的主要是直流分量。
当给此PWM信号加以截至频率100KHz,阻带衰减60dB的理想低通滤波器后可以得到如下波形图。可以观察到随着时间的推移信号逐渐趋近于直流,也可以理解为200K与400K的分量被滤除得到直流分量。
仿真所用的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、其它
本项目所占用的资源情况如下:
之前一直使用Xilinx、Altera的FPGA进行开发,相比于Lattice而言,其开发工具更显繁琐。Lattice的开发工具使用十分清爽且快,其综合布线一个OLED显示的工程只需要30s,在Vivado上都不够软件的加载时间。
当然,Lattice的开发工具也存在缺点。譬如:时钟网络的布线资源受限;Lattice LSE的综合工具并不好用,从其带有一个Synplify Pro的选项便可看出,很多时候LSE综合不出来,更换Synplify便可解决。当然,以上这些缺点在学习时综合布线飞快面前显得就不那么重要。