项目描述
项目介绍
本文中,我们将通过硬禾学堂的“基于iCE40UP5K的FPGA学习平台”开发板来实现Σ-Δ ADC,并通过该ADC进行电压采集以完成一个简易电压表的设计。
项目需求
-
旋转电位计可以产生0-3.3V的电压
-
利用板上串行ADC对电压进行转换
-
将电压值在板上的OLED屏幕显示出来
完成的功能及达到的性能
-
实现Σ-Δ ADC,8位分辨率,200kHZ采样率
-
采集电压的实时显示(通过OLED屏幕)
具体效果如下图所示:
设计思路
-
ICE40UPK产生51.2MHz PWM波,其占空比由高到低变化;
-
比较器比较PWM波经滤波后的直流电压值与电位计电压值,当检测到比较器输出呈下降沿时,读取PWM波的占空比(0-255),占空比即采样值;
-
ICE40UP5K读取采样值并转换为十进制电压值(电压值 =(占空比/256)*3.3 V),并通过OLED显示出来。
如下图所示:
pwm_adc模块通过pwm_adc_out端口向外输出占空比由高到低(100%-0%)的pwm波,并通过pwm_adc_in端口接收Σ-Δ ADC的输出信号。当其呈现下降沿时,pwm波的占空比即为ADC的采样数据,对该数据进行处理即可得到电压值。此时,pwm_val将记录pwm波此时的占空比的数值,并将其传递给oled12864模块供其译码显示。
oled12864模块通过dis_dat端口接收显示数据,并通过内置的译码模块将其转译成十进制数值,即实时的电压值。
整体思路如下图所示:
Σ-Δ ADC采集
大多数FPGA芯片上并未集成ADC外设,当需要低成本/多通道采集模拟量时,可以考虑本方案。此学习平台上并未集成ADC&DAC模块,其通过PWM+电压比较器实现Σ-Δ ADC,从而实现ADC采集的功能;而DAC功能则通过R-2R权电阻网络实现。本节将阐释ADC实现原理、参数以及代码实现。
ADC参数
在讨论一块ADC性能的时候,往往关注两个指标:采样率、量化位数。
-
采样率:
即采样速率。依据采样定理。A/D转换器的抽样频率 fs应大于2wa(wa为被采样信号的带宽)在实际中,由于A/D转换器件的非线性、量化噪声、失真及接收机噪声等因素的影响,一般选取fs>2.5wa。
-
量化位数:
即分辨率。采样值的位数的选取需要满足一定的动态范围及数字部分处理精度的要求,一般分辨率80dB的动态范围要求下不能低于12位。
ADC实现原理
在该平台上,其PWM+电压比较器实现Σ-Δ ADC的原理图如下:
上图中,比较器同相输入端接模拟输入,反向输入端接PWM输入,比较后输出结果。反相输入PWM_V2与一个电阻和一个电容项链,构成简易的一阶RC滤波器。该一阶RC滤波器截至频率为fc = 1/(2πRC) ,工程上将幅度值下降到原来的0.707倍(-3dB)称作截止频率点,电路的带宽也由-3dB点定义。通过对输入的PWM波形进行滤波可以得到一个近似的直流值,将其与Ain2进行比较输出,通过不断调节占空比(假设由低到高),当输出C_OUT2由高变低时便可使用占空比来表示模拟输入量。
滤波的目的是得到直流量,一个PWM波进行傅里叶变换后可观察到其存在许多高次谐波,一阶RC的目的便是将基频与谐波滤除。
而一个典型的PWM波形脉冲区间包括高电平区间th 与低电平tl区间,其占空比常定义为duty = th/(th+tl) 。当调节占空比时,上述参量也随之变化,将该pwm波经过RC低通滤波器便可得到一个近似直流的电压值。
ADC参数选取
如前文所述,ADC有两个重要的指标:采样率及量化位数。本节将介绍如何选取这两个参数。本文中相关参数选取如下:
-
PWM生成模块时钟 fclk = 51.2M
-
采样率 fs = 200 KHz
-
量化位数 N = 8 bit
-
RC滤波器原件 R = 1000Ω, C = 1000pF
-
RC滤波器截止频率 fc = 160KHz (fc = 1/(2πRC))
在参数的选择过程中,可以参考如下步骤进行综合考量:
-
fs的选择是否满足要求?当采样一个非直流信号时,需要满足奈奎斯特采样定理,即fs ≥2fH。
-
N的选择是否满足要求?一个ADC的分辨率受供电电压与位数二者共同决定,即有fres = VDD/2n。同时,其信噪比满足SNR = 6N ,即每提高一位可以提高 6dB的信噪比。例如在一个3.3V供电的系统中,8位量化最高可以做到0.012890625V的分辨率。
-
是否满足fs >fc?采样率应尽可能大于截止频率,这样信号的直流分量可以较好地滤除出来。
-
fclk是否能满足FPGA布局布线的要求?fclk的大小满足如下条件:fclk = 2N/T = 2N * fs,可以观 察到模块时钟的频率随着量化位数的增加呈指数倍增加,因此Σ-Δ ADC常用于低频下高精度的检测。当时钟频率提高时会给FPGA的布线带来困难,考虑到iCE40的定位属于低功耗FPGA,故选择51.2M作为时钟频率。
在ADC实现原理一节中我们详细分析了滤波器存在的必要。而RC越大,滤波器的截止频率也就越高,也就更有利于PWM信号直流分量的提取。但同时,时间常数τ ≈ 0.69RC,当不受限地提高RC时,时间常数也会随之提高,进而减缓电路的响应时间。故而RC较大时有如下特性:
-
优点:便于直流分量的提取,直流量的纹波小。
-
缺点:时间常数偏大,电路的响应速度降低。
而当RC较小时,特性则与RC较大时相反:
-
优点:时间常数小,电路响应速度快。
-
缺点:由于截至频率提高,需要更大的采样率才能达到合适的采样效果。
故而如果想要同时达到较快的电路响应速度及合适的采样效果,可考虑RLC滤波电路、高阶滤波电路、有源滤波或者提高采样频率等方式。
Verilog代码实现
该模块包含三个输入与两个输出,具体介绍如下:
-
sys_clk 与 sys_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 pwm_adc_out
output reg [7:0] pwm_val,
);
reg r_adc_in;//通过d寄存器获取端口电平
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;//pwm_adc_in 电压下降时,adc_in_fall 输出高电平
assign adc_in_fall = (r_adc_in | pwm_adc_in)&(pwm_adc_in == 1'b0);
//pwm生成
reg [7:0] pwm_adder;
reg pwm_adder_overflow; //pwm_addr 溢出标志
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 <= 1'b0;
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_adc_in 下降沿触发,获取pwm_val的值,即占空比,实现符合要求时读取相应数据
pwm_val <= pwm_set;
pwm_set <= 8'd0;
end else if(pwm_adder_overflow == 1'b1 && pwm_adc_in == 1'b0)begin//pwm_adder溢出&pwm_anc_in无输入时,pwm_set保持
pwm_set <= pwm_set;
end else if(pwm_adder_overflow == 1'b1 && pwm_adc_in == 1'b1)begin//pwm_adder溢出&pwm_adc_in有输入时,pwm_set计数+1,占空比调节
pwm_set <= pwm_set + 1'b1;
end
end
assign pwm_adc_out = (pwm_adder <= pwm_set) ? 1'b1 : 1'b0;//输出pwm,占空比为1-pwm_set/2^8,实现8位ADC
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码
);
//reg 定义
reg [4:0] cnt_shift ; //移位判断计数器
reg [43:0] data_shift ; //移位判断数据寄存器
reg shift_flag ; //移位判断标志信号
//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; //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 原语进行例化。
module voltmeter(
// input in_clk, //使用内部时钟
input in_rst_n,
input pwm_adc_in,
// input debug,
// output oled_csn, //片选,无此端口
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) //pll输出分配给全局时钟网络
);
wire [7:0] pwm_val;
oled12864 u_oled12864(
.clk(sys_clk), //系统时钟
.rst_n(sys_rst_n), //系统复位
.dis_dat(pwm_val),
// .debug(debug),
.oled_csn(), //OLED 使能
.oled_rst(oled_rst), //OLED 复位
.oled_dcn(oled_dcn), //OLED 数据/命令 控制
.oled_clk(oled_clk), //OLED 时钟
.oled_dat(oled_dat) //OLED 数据
);
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)
);
endmodule
其它
资源报告
上图为本工程使用的所有资源报告,可见使用了945个LUT4单元,IO Buffers(IO资源) 使用 7个,PFU 寄存器使用了 286个 ,EBR 资源使用了 6个,后续在有了深入了解之后,资源优化问题将被纳入考虑。
主要难题及解决方法 Σ-Δ ADC设计&OLED驱动修改
电子森林提供的OLED例程为静态显示的代码,无法实现电压值的动态刷新,在删去原本注释的情况下,初步的显示成果为000~001之间跳动,判定为OLED驱动显示部分字符问题及ADC采样部分变化过快,数值不稳定。
后经查阅资料、网站进行学习,发现为ADC采样问题,即原本设计比较器输出为1时,读取占空比,导致pwm波一直变化,占空比也随之变化,进而导致采样值不稳定,后改为下降沿触发,有效缓解该问题。同时,OLED驱动部分参考野火的BCD模块代码以及B站上相关视频,改善了动态显示功能。
未来的计划
-
对Σ-Δ ADC 进行优化,提高其采样正确率,使采集到的电压值更趋近于真实值;
-
在此平台上继续学习,尝试实现其他项目,进而锻炼自身能力。