一 、双路DAC
这次实验使用的双路 DA 模块是正点原子推出的一款双路高速数模转换模块(ATK_DUAL_HS_DA), 高速 DA 转换芯片是由思瑞浦公司生产的 3PD5651E 芯片。
3PD5651E 芯片输出的是一对差分电流信号,为了防止受到噪声干扰,电路中接入了低通滤 波器,然后通过高性能和高带宽的运放电路,实现差分变单端以及幅度调节等功能,使整个电路性能得到 了最大限度的提升,最终输出的模拟电压范围是-5V~+5V .
芯片特点:
3PD5651E 的数模转换位数为 10 位,最大转换速度为 125MSPS(每秒采样百万次,Million Samples per Second)
两个电流输出端 IOUTA 和 IOUTB 为一 对差分电流,当输入数据为 0(DB9~DB0=10’h000)时,IOUTA 的输出电流为 0,而 IOUTB 的输出电流达 到最大,最大值的大小跟参考电压有关;当输入数据全为高点平(DB9~DB0=10’h3ff)时,IOUTA 的输出 电流达到最大,最大值的大小跟参考电压有关,而 IOUTB 的输出电流为 0
由图 可知,数据在时钟的上升沿锁存,因此我们可以在时钟的下降 沿发送数据,这样使 DA 芯片在数据的中央采样,保证数据采样的准确性。值得注意的是采样时钟越快,数模转换越快
其实就使用来说是很简单的。我们只需要给芯片提供驱动时钟和DATA就可以。然后数据由ROM提供。于是我们只需要设计1时钟模块提供da_clk;2rom读模块,从rom中读出数据给DAC用;3DAC驱动模块,驱动DAC工作。双端口输出我们只需要定义两个端口输出就可以,如果是多端口可能需要配置寄存器。
这次使用BMG块来当ROM用。
下面是主要的DAC驱动模块。由PLL提供时钟,ROM提供数据。
module da_wave_send(
input clk , //时钟
input rst_n , //复位信号,低电平有效
input [9:0] rd_data, //ROM 读出的数据
output reg [9:0] rd_addr, //读 ROM 地址
//DA 芯片接口
output da_clk , //DA(AD9708)驱动时钟,最大支持 125Mhz 时钟
output [9:0] da_data //输出给 DA 的数据
);
//parameter
//频率调节控制
parameter FREQ_ADJ = 10'd5; //频率调节,FREQ_ADJ 的越大,最终输出的频率越低,范围 0~255
//reg define
reg [9:0] freq_cnt ; //频率调节计数器
//数据 rd_data 是在 clk 的上升沿更新的,所以 DA 芯片在 clk 的下降沿锁存数据是稳定的时刻
//而 DA 实际上在 da_clk 的上升沿锁存数据,所以时钟取反,这样 clk 的下降沿相当于 da_clk 的上升沿
assign da_clk = ~clk;
assign da_data = rd_data; //将读到的 ROM 数据赋值给 DA 数据端口
//频率调节计数器
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
freq_cnt <= 10'd0;
else if(freq_cnt == FREQ_ADJ)
freq_cnt <= 10'd0;
else
freq_cnt <= freq_cnt + 10'd1;
end
//读 ROM 地址
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
rd_addr <= 10'd0;
else begin
if(freq_cnt == FREQ_ADJ) begin
rd_addr <= rd_addr + 10'd1;
end
end
end
endmodule
这里两路数据接一块给DAC两个输入端口,所以DAC输出是一样的。DAC输出一般都是一对差分电流信号。这个实验用的DAC芯片,将其稳定滤波并转换成单端的电压信号。
上面代码是读模块,读出ROM数据输出给DAC并输出驱动时钟给DAC。clk来自PLL,rd_data来自ROM。这些ip设置这里不做详细介绍。
二、多路ADC
(21条消息) FPGA学习专题-ADC的使用_南山维拉的博客-CSDN博客_adc fpga
这里以某芯片为例,其他的八九不离十。先看芯片引脚图和SPI时序
IN0toIN7就是我们的模拟输入,多通道。而我们的输出只有一个串行输出。所以我们要配置寄存器来指明是要哪个通道的的转换输出。还有就是SPI时序了。直接上图
也就是说我们还有四个引脚来完成我们的时序。首先是在SCLK高电平时,CS片选端由高拉低就是数据处理开始(即输入输出有效),结束反之。一个是DIN一个是DOUT,这两个一个是来自FPGA的控制寄存器DIN一个是ADC的输出DOUT。
需要关注的是,我们在编写时序代码的时候,是站在FPGA的角度来看的,即:对应于一个已知的SCLK,我需要在SCLK的下降沿把数据放到DIN上,在SCLK的上升沿采样DOUT上的数据。
所以这里的时序就是ADC在SCLK上升沿采样并输出DOUT,同时FPGA也采样DOUT。FPGA在下降沿输出DIN,ADC就能在SCLK上升沿接受到DIN。
这个芯片的SCLK要求是0.8M-3.2MHz我们就需要分频到这么多。
接下来就分析代码
这个模块最终就是要输出我们ADC得到的数据,方便后续处理。除了与ADC之间的接口外我们还需要设置,
Channel,通道选择一共三位在实际使用中我们填入我们需要的通道。或者灵活设置。
Data, 是FPGA采集后输出给其他模块的数字信号,方便后续处理比如FFT。
En_Conv,
Conv_Done, 完成标志
ADC_State, 状态
DIV_PARAM, 分频系数。
注意里面有个2倍SCLK的信号,这个时钟是用来驱动状态机并产生SCLK的。
输出是寄存型。要用移位方法表示过程,最后完成一次转换后将当前寄存的值赋值给DATA。
module adc128s022(
Clk,
Rst_n,
Channel,
Data,
En_Conv,
Conv_Done,
ADC_State,
DIV_PARAM,
ADC_SCLK,
ADC_DOUT,
ADC_DIN,
ADC_CS_N
);
input Clk; //输入时钟
input Rst_n; //复位输入,低电平复位
input [2:0]Channel; //ADC转换通道选择
output reg [11:0]Data; //ADC转换结果
input En_Conv; //使能单次转换,该信号为单周期有效,高脉冲使能一次转换
output reg Conv_Done; //转换完成信号,完成转换后产生一个时钟周期的高脉冲
output ADC_State; //ADC工作状态,ADC处于转换时为低电平,空闲时为高电平
input [7:0]DIV_PARAM; //时钟分频设置,实际SCLK时钟 频率 = fclk / (DIV_PARAM * 2)
output reg ADC_SCLK; //ADC 串行数据接口时钟信号
output reg ADC_CS_N; //ADC 串行数据接口使能信号
input ADC_DOUT; //ADC转换结果,由ADC输给FPGA
output reg ADC_DIN; //ADC控制信号输出,由FPGA发送通道控制字给ADC
reg [2:0]r_Channel; //通道选择内部寄存器
reg [11:0]r_data; //转换结果读取内部寄存器
reg [7:0]DIV_CNT;//分频计数器
reg SCLK2X;//2倍SCLK的采样时钟
reg [5:0]SCLK_GEN_CNT;//SCLK生成暨序列机计数器
reg en;//转换使能信号
//在每个使能转换的时候,寄存Channel的值,防止在转换过程中该值发生变化
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
r_Channel <= 3'd0;
else if(En_Conv)
r_Channel <= Channel;
else
r_Channel <= r_Channel;
//产生使能转换信号
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
en <= 1'b0;
else if(En_Conv&&(Conv_Done==1'b0))
en <= 1'b1;
else if(Conv_Done)
en <= 1'b0;
else
en <= en;
//生成2倍SCLK使能时钟计数器
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
DIV_CNT <= 8'd0;
else if(en)begin
if(DIV_CNT == (DIV_PARAM - 1'b1))
DIV_CNT <= 8'd0;
else
DIV_CNT <= DIV_CNT + 1'b1;
end else
DIV_CNT <= 8'd0;
//生成2倍SCLK使能时钟
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
SCLK2X <= 1'b0;
else if(en && (DIV_CNT == (DIV_PARAM - 1'b1)))
SCLK2X <= 1'b1;
else
SCLK2X <= 1'b0;
//生成序列计数器
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)
SCLK_GEN_CNT <= 6'd0;
else if(SCLK2X && en)begin
if(SCLK_GEN_CNT == 6'd33)
SCLK_GEN_CNT <= 6'd0;
else
SCLK_GEN_CNT <= SCLK_GEN_CNT + 1'd1;
end else
SCLK_GEN_CNT <= SCLK_GEN_CNT;
//序列机实现ADC串行数据接口的数据发送和接收
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)begin
ADC_SCLK <= 1'b1;
ADC_CS_N <= 1'b1;
ADC_DIN <= 1'b1;
end
else if(en) begin//2
if(SCLK2X)begin//1
case(SCLK_GEN_CNT)
6'd0:begin ADC_CS_N <= 1'b0; end
6'd1:begin ADC_SCLK <= 1'b0; ADC_DIN <= 1'b0; end
6'd2:begin ADC_SCLK <= 1'b1; end
6'd3:begin ADC_SCLK <= 1'b0; end
6'd4:begin ADC_SCLK <= 1'b1; end
6'd5:begin ADC_SCLK <= 1'b0; ADC_DIN <= r_Channel[2];end //addr[2]
6'd6:begin ADC_SCLK <= 1'b1; end
6'd7:begin ADC_SCLK <= 1'b0; ADC_DIN <= r_Channel[1];end //addr[1]
6'd8:begin ADC_SCLK <= 1'b1; end
6'd9:begin ADC_SCLK <= 1'b0; ADC_DIN <= r_Channel[0];end //addr[0]
//每个上升沿,寄存ADC串行数据输出线上的转换结果
6'd10,6'd12,6'd14,6'd16,6'd18,6'd20,6'd22,6'd24,6'd26,6'd28,6'd30,6'd32:
begin ADC_SCLK <= 1'b1; r_data <= {r_data[10:0], ADC_DOUT}; end //循环移位寄存DOUT上的12个数据
6'd11,6'd13,6'd15,6'd17,6'd19,6'd21,6'd23,6'd25,6'd27,6'd29,6'd31:
begin ADC_SCLK <= 1'b0; end
6'd33:begin ADC_CS_N <= 1'b1; end
default:begin ADC_CS_N <= 1'b1; end //将转换结果输出
endcase
end//1
else ;
end//2
else begin
ADC_CS_N <= 1'b1;
end
//转换完成时,将转换结果输出到Data端口,同时产生一个时钟周期的高脉冲信号
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)begin
Data <= 12'd0;
Conv_Done <= 1'b0;
end else if(en && SCLK2X && (SCLK_GEN_CNT == 6'd33))begin
Data <= r_data;
Conv_Done <= 1'b1;
end else begin
Data <= Data;
Conv_Done <= 1'b0;
end
//产生ADC工作状态指示信号
assign ADC_State = ADC_CS_N;
endmodule
三、双路ADC并行输出
这个就比较简单了,我们的接口只有驱动时钟、使能,输入就是数据和量程标志。程序设计也简单。我们只需要用PLL生成对应CLK就可以,然后和上面一样的时序要求,取反接到ADC。使能信号倒是我们需要考虑的。比如完成一次转换后还需不需要什么处理。
//adc采集模块
module hs_dual_ad(
input sys_clk , //系统时钟
input rst_n ,
//AD0
input [9:0] ad0_data , //AD0数据
input ad0_otr , //输入电压超过量程标志
output ad0_clk , //AD0采样时钟
output ad0_oe , //AD0输出使能
//AD1
input [9:0] ad1_data , //AD1数据
input ad1_otr , //输入电压超过量程标志
output ad1_clk , //AD1采样时钟
output ad1_oe //AD1输出使能
//输出数据
output [9:0] data0 ,
output [9:0] data1 ,
);
//wire define
wire clk_out1;
wire clk_out2;
//*****************************************************
//** main code
//*****************************************************
assign ad0_oe = 1'b0;
assign ad1_oe = 1'b0;
assign ad0_clk = ~clk_out1;
assign ad1_clk = ~clk_out1;
assign data0 = ad0_data;
assign data1 = ad1_data;
//并行ADC,一个驱动时钟周期采样一次,在上升沿采样
/*always@(posedge clk_out1 or negedge rst_n)begin
if(!rst_n)begin
data0<=10'b00000_00000;
data1<=10'b00000_00000;
end
else begin
data0<=ad0_data;
data1<=ad1_data;
end
end */
//时钟ip核例化
clk_wiz_0 u_clk_wiz_0
(
// Clock out ports
.clk_out1(clk_out1), // output clk_out1
// Status and control signals
.reset(1'b0), // input reset
.locked(locked), // output locked
// Clock in ports
.clk_in1(sys_clk)); // input clk_in1
endmodule
这个ADC是并行的,也就是一个时钟采样一次然后输出一个10位数据,ADC对应输出有10个输出接口。我们在FPGA中用10位输入来接上,然后再输出给下一个模块。