1. 模数转换理论
模数转换器又称(A/D转换器),通常是指一个将模拟信号转变为数字信号的电子元件或电路。常见的转换方式使将模拟量与基准量比对得到便于传输的二进制信号。生活中常见的模拟量有温湿度、图像、声音等。模拟信号与数字信号的转换过程一般分为四个步骤:采样、保持、量化、编码。前两个步骤在采样-保持电路中完成,后两步则在 ADC 芯片中完成。ADC 的主要技术指标包括:分辨率、转换速率、量化误差、满刻度误差、线性度。
以上内容了解即可,重点学习如何将8位二进制数转换为常见的数字电压表。
2. 实验
实验目标:外部挂载的高速 AD/DA 板卡的 A/D 部分将输入其中的模拟信号(电压范围 -5V~5V)转换为数字量,将数字量传入 FPGA, FPGA 将传入的数字量通过计数转化为电压数值。通过数码管显示转化后的电压值,实现模拟信号的电压测量。
2.1 硬件资源介绍
板载AD/DA板卡:AD部分
对应电路图:
外载 AD/DA 板卡的 AD 部分使用高速 AD 芯片 AD9280, AD9280 由 ANALOG 公司生产, 是一款单芯片、 8 位、 32 MSPS 模数转换器(ADC),采用单电源供电,内置一个片内采样保持放大器和基准电压源。
AD9280 采用+2.7 V 至+5.5 V 电源供电,非常适合高速应用中的低功耗操作,额定温度范围为-40°C 至+85°C 工业温度范围。 AD9280 的输入经过设计,用户可以选择各种输入范围和偏移,并可通过单端或差分方式驱动输入。
AD9280 具有一个片上可编程基准电压源。也可以选用外部基准电压,以满足应用的直流精度与温度漂移要求。采用一个单时钟输入来控制所有内部转换周期。数字输出数据格式为标准二进制。
2.2 程序设计
2.2.1 整体模块设计
将待测量的模拟信号接入外部挂载的高速 AD/DA 板卡的模拟信号输入端,板卡会将输入的模拟信号进行采样、量化、编码后,将模拟电压转换为数字值传给 FPGA;内部 adc模块接收到传入的电压数字值,经过运算,转换为可用于显示的电压值输入给数码管显示模块,将电压值显示在数码管上。
2.2.2 ADC 模块
1. 模块框图
输入的ad_data信号模拟信号电压数字值,输出volt 信号则是经过运算处理后的电压值,传入数码管显示模块用于电压显示,ad_clk 信号为传入外载板卡的时钟信号,频率为 12.5MH 中,由系统时钟信号 4 分频得到,供ADC 芯片在此时钟同步下进行模拟信号 的采样、量化和编码。
由上可知,难点在于如何将ad_data转换为我们便于识别的电压值。
本质算法:
已知ADC 芯片位宽为 8 位,板卡模拟电压输入范围为-5v~+5v,即电压表测量范围, 最大值和最小值压降为 10v,分辨率为 10/2^8。
当 ADC 芯片采集后的电压数值 ad_data 位于 0 - 127 范围内,表示测量电压位于-5V ~0V 范围内,换算为电压值: Vin = - (10 / 2^8 * (127 - ad_data ));
当 ADC 芯片采集后的电压数值 ad_data 位于 127 - 255 范围内,表示测量电压位于 0V ~ 5V 范围内,换算为电压值: Vin= (10 / 2^8 * (ad_data - 127))。
改良算法:
为了提高测量结果的精确性和实现均值滤波,使用定义中值的测量方法。采样累计采集1024次数据,计算一次平均值方法,这样采样结果显示准确、稳定,常用电压表中往往也采用这种方式。
在电压表上电后未接入测量电压时,取 ADC 芯片采集的最初的若干测量值(8位二进制数)(我觉得时电路噪声干扰产生的),对其取平均,作为测量中值M,与实际测量值 0V 对应。
当 ADC 芯片采集后的电压数值 ad_data 位于 0 - M范围内,表示测量电压位于-5V ~ 0V 范围内,分辨率为 10/((M + 1) *2),换算为电压值:Vin = - ((10 /((M + 1) * 2)) * (M - ad_data));
当 ADC芯片采集后的电压数值 ad_data 位于M - 255 范围内,表示测量电压位于 0V ~ 5V范围内,分辨率为 10/((255 -(M + 1) * 2),换算为电压值: Vin = ((10 /((255 -(M + 1) * 2)) * (ad_data - M))。下文的中值用data_median替代。
2. 波形图分析
首先分析data_median的由来。cnt_median对AD板卡模块的时钟进行1024次的计数,在每一次计数中完成对ad_data数据的累加,当计数1025次时拉高median_en信号,对数次的累加求中值。
中值 data_median 确定后,开始测量电压的计算。当 ADC 芯片采集后的电压数值ad_data 位于 0 - data_median 范围内,表示测量电压位于-5V ~ 0V 范围内,分辨率为10/((data_median + 1) * 2),换算为电压值: Vin = - ((10 /((data_median + 1) * 2)) *(data_median - ad_data));当 ADC 芯片采集后的电压数值 ad_data 位于 data_median - 255 范围内,表示测量电压位于 0V ~ 5V 范围内,分辨率为 10/((255 - data_median + 1) * 2),换算为电压值: Vin = ((10 /((255 - data_median + 1) * 2)) * (ad_data - data_median))。之所以放大倍数为(2^13 * 1000)倍方便电压值的计算(整数计算比小数简单)。
使用 “>> 13”对计算值进行右移 13 位,由于抵消分辨率放大的 2^13 倍,分辨率中放大的 1000 倍,可以通过将数码管显示值小数点左移 3 位来抵消;正负号通过 ad_data 与中值 data_median 的比较来确定, sign = (ad_data < data_median) ? 1'b1 : 1'b0, sign 为高电平,代表测量结果为负向电压,反之为正向电压。
3. 编写RTL代码
`timescale 1ns/1ns
module adc
(
input wire sys_clk , //时钟
input wire sys_rst_n , //复位信号,低电平有效
input wire [7:0] ad_data , //AD输入数据
output wire ad_clk , //AD驱动时钟,最大支持20Mhz时钟
output wire sign , //正负符号位
output wire [15:0] volt //数据转换后的电压值
);
//parameter define
parameter CNT_DATA_MAX = 11'd1024; //数据累加次数
//wire define
wire [27:0] data_p ; //根据中值计算出的正向电压AD分辨率
wire [27:0] data_n ; //根据中值计算出的负向电压AD分辨率
//reg define
reg median_en ; //中值使能
reg [10:0] cnt_median ; //中值数据累加计数器
reg [18:0] data_sum_m ; //1024次中值数据累加总和
reg [7:0] data_median ; //中值数据
reg [1:0] cnt_sys_clk ; //时钟分频计数器
reg clk_sample ; //采样数据时钟
reg [27:0] volt_reg ; //电压值寄存
//数据ad_data是在ad_sys_clk的上升沿更新
//所以在ad_sys_clk的下降沿采集数据是数据稳定的时刻
//FPGA内部一般使用上升沿锁存数据,所以时钟取反
//这样ad_sys_clk的下降沿相当于sample_sys_clk的上升沿
assign ad_clk = ~clk_sample;
//sign:正负符号位
assign sign = (ad_data < data_median) ? 1'b1 : 1'b0;
//时钟分频(4分频,时钟频率为12.5Mhz),产生采样AD数据时钟
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
cnt_sys_clk <= 2'd0;
clk_sample <= 1'b0;
end
else
begin
cnt_sys_clk <= cnt_sys_clk + 2'd1;
if(cnt_sys_clk == 2'd1)
begin
cnt_sys_clk <= 2'd0;
clk_sample <= ~clk_sample;
end
end
//中值使能信号
always@(posedge clk_sample or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
median_en <= 1'b0;
else if(cnt_median == CNT_DATA_MAX)
median_en <= 1'b1;
else
median_en <= median_en;
//cnt_median:中值数据累加计数器
always@(posedge clk_sample or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_median <= 11'd0;
else if(median_en == 1'b0)
cnt_median <= cnt_median + 1'b1;
//data_sum_m:1024次中值数据累加总和
always@(posedge clk_sample or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_sum_m <= 19'd0;
else if(cnt_median == CNT_DATA_MAX)
data_sum_m <= 19'd0;
else
data_sum_m <= data_sum_m + ad_data;
//data_median:中值数据
always@(posedge clk_sample or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
data_median <= 8'd0;
else if(cnt_median == CNT_DATA_MAX)
data_median <= data_sum_m / CNT_DATA_MAX;
else
data_median <= data_median;
//data_p:根据中值计算出的正向电压AD分辨率(放大2^13*1000倍)
//data_n:根据中值计算出的负向电压AD分辨率(放大2^13*1000倍)
assign data_p = (median_en == 1'b1) ? 8192_0000 / ((255 - data_median) * 2) : 0;
assign data_n = (median_en == 1'b1) ? 8192_0000 / ((data_median + 1) * 2) : 0;
//volt_reg:处理后的稳定数据
always@(posedge clk_sample or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
volt_reg <= 'd0;
else if(median_en == 1'b1)
if((ad_data > (data_median - 3))&&(ad_data < (data_median + 3)))
volt_reg <= 'd0;
else if(ad_data < data_median) //负值
volt_reg <= (data_n *(data_median - ad_data)) >> 13;
else if(ad_data > data_median) //正值
volt_reg <= (data_p *(ad_data - data_median)) >> 13;
else
volt_reg <= 'd0;
//volt:数据转换后的电压值
assign volt = volt_reg;
endmodule
2.2.3 顶层模块
1. 编写RTL代码
`timescale 1ns/1ns
module dig_volt
(
input wire sys_clk , //系统时钟,50MHz
input wire sys_rst_n , //复位信号,低有效
input wire [7:0] ad_data , //AD输入数据
output wire ad_clk , //AD驱动时钟,最大支持20Mhz时钟
output wire [5:0] sel , //串行数据输入
output wire [7:0] seg //使能信号
);
//wire define
wire [15:0] volt ; //数据转换后的电压值
wire sign ; //正负符号位
//------------- adc_inst -------------
adc adc_inst
(
.sys_clk (sys_clk ), //时钟
.sys_rst_n (sys_rst_n ), //复位信号,低电平有效
.ad_data (ad_data ), //AD输入数据
.ad_clk (ad_clk ), //AD驱动时钟,最大支持20Mhz时钟
.sign (sign ), //正负符号位
.volt (volt ) //数据转换后的电压值
);
//------------- seg_dynamic_inst --------------
seg_dynamic seg_dynamic_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.sys_rst_n (sys_rst_n ), //复位信号,低有效
.data ({4'b0,volt}), //数码管要显示的值
.point (6'b001000 ), //小数点显示,高电平有效
.seg_en (1'b1 ), //数码管使能信号,高电平有效
.sign (sign ), //符号位,高电平显示负号
.sel (sel ), //串行数据输入
.seg (seg ) //输出使能信号
);
endmodule
2.2.4 仿真验证
1. 仿真代码
`timescale 1ns/1ns
module tb_dig_volt();
//wire define
wire ad_clk ;
wire [5:0] sel ;
wire [7:0] seg ;
//reg define
reg sys_clk ;
reg sys_rst_n ;
reg [7:0] ad_data ;
reg clk_sample ;
reg data_en ;
reg [7:0] ad_data_reg ;
//sys_rst_n,sys_clk,ad_data
initial
begin
sys_clk = 1'b1;
clk_sample = 1'b1;
sys_rst_n = 1'b0;
#200;
sys_rst_n = 1'b1;
data_en = 1'b0;
#499990;
data_en = 1'b1;
end
always #10 sys_clk = ~sys_clk;
always #40 clk_sample = ~clk_sample;
always@(posedge clk_sample or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
ad_data_reg <= 8'd0;
else if(data_en == 1'b1)
ad_data_reg <= ad_data_reg + 1'b1;
else
ad_data_reg <= 8'd0;
always@(posedge clk_sample or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
ad_data <= 8'd0;
else if(data_en == 1'b0)
ad_data <= 8'd125;
else if(data_en == 1'b1)
ad_data <= ad_data_reg;
else
ad_data <= ad_data;
//------------- dig_volt_inst -------------
dig_volt dig_volt_inst
(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n ),
.ad_data (ad_data ),
.ad_clk (ad_clk ),
.sel (sel ),
.seg (seg )
);
endmodule
2. 仿真结果
子模块的仿真我就不展示了,结果是正确的。
2.3 上板验证
缺少AD板卡,这里展示网上的结果,供大家参考。
总结:重点掌握如何计算电压的公式。