DDS详解——以安路PH1A90实现为例
DDS详解——以安路PH1A90实现为例
- 正弦波有哪些要素?
- DDS是怎么工作的?
- 调制对象
- 调制方法
- 如何用FPGA实现一个DDS?
- 硬件平台
- 顶层设计与通信
- 控制字选择矩阵
- 核心功能
- 直接数据字生成器
- 数字斜坡生成器
- ADC实时调制
- RAM调制(未实现)
- 除法器调幅
- 外部接口
- 参考资料
- 附件链接
正弦波有哪些要素?
简单地理解,一个正弦波的表达式如下:
y = A ( t ) ∗ cos ( θ ) , θ = f ( t ) (式 1 ) y=A (t)*\cos(\theta),\theta=f(t)(式1) y=A(t)∗cos(θ),θ=f(t)(式1)
令相位θ为对时间的某个函数上的积分,即:
θ = ∫ t 1 t 2 f ( t ) d t (式 2 ) \theta = \int_{t_{1}}^{t_{2}} f(t) dt(式2) θ=∫t1t2f(t)dt(式2)
将式2代入到式1,则有:
y = A ( t ) ∗ cos ( ∫ t 1 t 2 f ( t ) d t ) (式 3 ) y=A (t)*\cos( \int_{t_{1}}^{t_{2}} f(t) dt)(式3) y=A(t)∗cos(∫t1t2f(t)dt)(式3)
将这个式子离散化。也就是转换成能够编程的形式,则可以得到:
y = A ( t ) ∗ cos ( ∑ 0 N x ( n ) ) (式 4 ) y=A (t)*\cos( \sum_{0}^{N} x(n))(式4) y=A(t)∗cos(0∑Nx(n))(式4)
现在来简单解释一下这个式子。首先,A(t)
是一个时间的函数,指示当前信号的幅度;f(t)
也是一个时间的函数,指示正弦波序列的下一个元素应该相对于上一个元素增加多少相位,简单来说就是指示了相位的增量。相位如果增加了2PI,就是过了一个周期,而增量的大小,就对应了周期的大小,也就对应了频率的大小。也就是说,f(t)
实际上指示的是正弦波的频率。
通常,在实际工程中,在一些情况下,几个同时钟源生成的正弦波之间会有可以调节的相位差。那么,就可以给式4增加一个常量:
y = A ( n ) ∗ cos ( ∑ 0 N x ( n ) + C ( n ) ) (式 5 ) y=A (n)*\cos( \sum_{0}^{N} x(n)+C(n))(式5) y=A(n)∗cos(0∑Nx(n)+C(n))(式5)
其中,C(n)
为初相位,也称相位偏移序列,可以是时间的函数。x(n)
是相位增量序列,A(n)
是幅度序列。
DDS是怎么工作的?
调制对象
从对于正弦波的基本概念讨论可以得知,我们要控制的主要有三个量:幅度、相位增量(频率)和相位偏移。
调制方法
从对于正弦波的基本概念讨论可以得知,我们要控制的三个量都是时间的函数。而“时间”在数字电路中的表述就是一个序列的索引值。数字逻辑电路的基本特性决定了它本身并不知道“时间”的概念,只会由每个时钟沿的状态决定下一个状态。但是一个数字逻辑系统的运行频率是可以被人为定义的,也就是下一时钟沿对于本时钟沿的时刻是可以确定的。
也就是说,可以确定唯一的A(n)
、x(n)
和C(n)
,同时引入一些其他自变量,用于确定某一个时刻的正弦波值。这些“其他自变量”可以是一个RAM储存的数值序列,也可以是ADC实时转换进来的数据。
但是,因为安路的DDS IP并不支持幅度调节,并未实现幅线性度调制,只是实现了一个非线性调幅器用作演示,并预留了相关接口。
如何用FPGA实现一个DDS?
硬件平台
FPGA:米联客的安路PH1A90SBG484-2一体化板子,带有一个残血FMC-HPC,当作FMC-LPC用。
也不知道多引出来那几个引脚有个啥用,用来碰运气看看哪个HPC子卡能上吗???要么就满血LPC的同时增加几个排针,要么就砍别的接口凑一个满血HPC得了。这比上不足比下有余的,也不知道我这钱是白花了还是花值了。
ADC:黑金AD9238模块,65MSPS,跑满(实测最高70MSPS时精度还是很高)
DAC:黑金AD9767模块,125MSPS,跑满(实测可以跑140MSPS以上)
ADC和DAC都是黑金经典40pin排针,用一个FMC-LPC转接板转接到安路开发板的FMC上。
不得不说,黑金的东西还是很好的,输入输出直接做了50Ω阻抗匹配了,AD模块采样电压在±3V以内都没问题,DA模块输出可以达到峰峰值8V不失真。最重要的是,板上有单电源转双电源的电路,FPGA只需要给子板5V供电即可,十分人性化,极大满足了当代大学生的电赛需求。
顶层设计与通信
看图说话。首先是SPI主从通道结构。这个结构是FPGA与MCU通信的主要渠道,双方均通过主机通道发起主动数据传输,均通过从通道接收数据并且返回应答。SPI通道模式可配置,但是双方模式都要一致。
MCU在发起一次通信时,先传给FPGA一个0x5A的开始字节,然后是要写入的寄存器的一个8位地址,然后是要写入的4字节数据,低位字节先传输。这样就完成了一次寄存器写入的操作。而在FPGA内部,会用一个FSM实现SPI的读取,改变相应的RAM内容,进行寄存器的相应配置。
大多数现代计算机系统都是小端序储存数据,也就是一个32位数据的高位字节储存在高位空间,低位字节储存在低位空间;而发送缓冲区一般而言是低地址先发送的。如果先传输低位字节,则可以直接使用内存复制(memcpy)函数将32位数据复制到SPI的发送缓冲区中,方便了MCU端的发送操作。
相应的代码如下:
// src/spi/spi.v
module spi #(parameter SYSCLK_FREQ = 50000000, // System clock frequency, default 50MHzparameter SPICLK_FREQ = 1000000, // SPI clock frequency, default 1MHzparameter CPOL = 0, // Clock Polarity, 0 and 1 allowedparameter CPHA = 0, // Clock Phase, 0 and 1 allowedparameter FSB = 1, // First bit, 0-LSB, 1-MSBparameter WIDTH = 8 // Data bit width
) (input clk,input rstn,input spi_s_sclk,input spi_s_ss_n,input spi_s_mosi,output spi_s_miso,output spi_m_sclk,output spi_m_ss_n,output spi_m_mosi,input spi_m_miso,output reg param_wen,output [31 : 0] dds_fword_source,output [31 : 0] dds_pword_source,output [31 : 0] dds_amp_source,output [31 : 0] direct_fword,output [31 : 0] direct_pword,output [31 : 0] direct_amp,output [31 : 0] drg_freq_start,output [31 : 0] drg_freq_end,output [31 : 0] drg_freq_step,output [31 : 0] drg_freq_pulse,output [31 : 0] drg_phase_start,output [31 : 0] drg_phase_end,output [31 : 0] drg_phase_step,output [31 : 0] drg_phase_pulse,output [31 : 0] drg_amp_start,output [31 : 0] drg_amp_end,output [31 : 0] drg_amp_step,output [31 : 0] drg_amp_pulse,output [31 : 0] adc_freq_center,output [31 : 0] adc_freq_kf,output [31 : 0] adc_freq_ch_sel,output [31 : 0] adc_freq_zero_cal,output [31 : 0] adc_phase_center,output [31 : 0] adc_phase_kf,output [31 : 0] adc_phase_ch_sel,output [31 : 0] adc_phase_zero_cal,output [31 : 0] adc_amp_center,output [31 : 0] adc_amp_kf,output [31 : 0] adc_amp_ch_sel,output [31 : 0] adc_amp_zero_cal
);wire spi_m_valid;reg spi_m_ready;wire [WIDTH - 1 : 0] spi_m_rx_data;reg [WIDTH - 1 : 0] spi_m_tx_data;spi_master #(.SYSCLK_FREQ(SYSCLK_FREQ),.SPICLK_FREQ(SPICLK_FREQ),.CPOL (CPOL),.CPHA (CPHA),.FSB (FSB),.WIDTH(WIDTH)) spi_master_inst (.clk (clk),.rstn(rstn),.spi_sclk(spi_m_sclk),.spi_ss_n(spi_m_ss_n),.spi_mosi(spi_m_mosi),.spi_miso(spi_m_miso),.spi_valid (spi_m_valid),.spi_ready (spi_m_ready),.spi_rx_data(spi_m_rx_data),.spi_tx_data(spi_m_tx_data));wire spi_s_valid;reg spi_s_ready;wire [WIDTH - 1 : 0] spi_s_rx_data;reg [WIDTH - 1 : 0] spi_s_tx_data;spi_slave #(.CPOL (CPOL),.CPHA (CPHA),.FSB (FSB),.WIDTH(WIDTH)) spi_slave_inst (.clk (clk),.rstn(rstn),.spi_sclk(spi_s_sclk),.spi_ss_n(spi_s_ss_n),.spi_mosi(spi_s_mosi),.spi_miso(spi_s_miso),.spi_valid (spi_s_valid),.spi_ready (spi_s_ready),.spi_rx_data(spi_s_rx_data),.spi_tx_data(spi_s_tx_data));localparam CONFIG_STATE_RESET = 8'h00;localparam CONFIG_STATE_IDEL = 8'h01;localparam CONFIG_STATE_RECV_ADDR = 8'h02;localparam CONFIG_STATE_RECV_DATA_B0 = 8'h03;localparam CONFIG_STATE_RECV_DATA_B1 = 8'h04;localparam CONFIG_STATE_RECV_DATA_B2 = 8'h05;localparam CONFIG_STATE_RECV_DATA_B3 = 8'h06;localparam CONFIG_STATE_WRITE = 8'h07;reg [ 7 : 0] config_state;reg [ 7 : 0] addr_buf;reg [31 : 0] data_buf;localparam CONFIGURE_RAM_CI_MAX = 256;reg [31 : 0] configure_ram[0 : 255];reg [15 : 0] configure_ram_ci;always @(posedge clk) beginif (!rstn) beginconfigure_ram_ci <= 8'h0;addr_buf <= 8'h0;data_buf <= 32'h0;spi_s_ready <= 1'b0;spi_s_tx_data <= 8'h0;param_wen <= 1'b0;config_state <= CONFIG_STATE_RESET;end else beginif (configure_ram_ci < CONFIGURE_RAM_CI_MAX) beginconfigure_ram[configure_ram_ci] <= 32'h0;configure_ram_ci <= configure_ram_ci + 1;addr_buf <= 8'h0;data_buf <= 32'h0;spi_s_ready <= 1'b0;spi_s_tx_data <= 8'h0;param_wen <= 1'b0;config_state <= CONFIG_STATE_RESET;end else beginconfigure_ram_ci <= CONFIGURE_RAM_CI_MAX;case (config_state)CONFIG_STATE_RESET: beginaddr_buf <= 8'h0;data_buf <= 32'h0;spi_s_ready <= 1'b0;spi_s_tx_data <= 8'h0;param_wen <= 1'b0;config_state <= CONFIG_STATE_IDEL;endCONFIG_STATE_IDEL: beginaddr_buf <= 8'h0;data_buf <= 32'h0;spi_s_tx_data <= 8'h0;param_wen <= 1'b0;if ((spi_s_valid && spi_s_ready) & (spi_s_rx_data == 8'h5a)) beginspi_s_ready <= 1'b0;config_state <= CONFIG_STATE_RECV_ADDR;end else beginspi_s_ready <= 1'b1;config_state <= CONFIG_STATE_IDEL;endendCONFIG_STATE_RECV_ADDR: begindata_buf <= 32'h0;spi_s_tx_data <= 8'h0;param_wen <= 1'b0;if (spi_s_valid && spi_s_ready) beginspi_s_ready <= 1'b0;addr_buf <= spi_s_rx_data;config_state <= CONFIG_STATE_RECV_DATA_B0;end else beginspi_s_ready <= 1'b1;config_state <= CONFIG_STATE_RECV_ADDR;endendCONFIG_STATE_RECV_DATA_B0: beginaddr_buf <= addr_buf;spi_s_tx_data <= addr_buf;param_wen <= 1'b0;if (spi_s_valid && spi_s_ready) beginspi_s_ready <= 1'b0;data_buf[7 : 0] <= spi_s_rx_data;config_state <= CONFIG_STATE_RECV_DATA_B1;end else beginspi_s_ready <= 1'b1;config_state <= CONFIG_STATE_RECV_DATA_B0;endendCONFIG_STATE_RECV_DATA_B1: beginaddr_buf <= addr_buf;spi_s_tx_data <= 8'h0;param_wen <= 1'b0;if (spi_s_valid && spi_s_ready) beginspi_s_ready <= 1'b0;data_buf[15 : 8] <= spi_s_rx_data;config_state <= CONFIG_STATE_RECV_DATA_B2;end else beginspi_s_ready <= 1'b1;config_state <= CONFIG_STATE_RECV_DATA_B1;endendCONFIG_STATE_RECV_DATA_B2: beginaddr_buf <= addr_buf;spi_s_tx_data <= 8'h0;param_wen <= 1'b0;if (spi_s_valid && spi_s_ready) beginspi_s_ready <= 1'b0;data_buf[23 : 16] <= spi_s_rx_data;config_state <= CONFIG_STATE_RECV_DATA_B3;end else beginspi_s_ready <= 1'b1;config_state <= CONFIG_STATE_RECV_DATA_B2;endendCONFIG_STATE_RECV_DATA_B3: beginaddr_buf <= addr_buf;spi_s_tx_data <= 8'h0;param_wen <= 1'b0;if (spi_s_valid && spi_s_ready) beginspi_s_ready <= 1'b0;data_buf[31 : 24] <= spi_s_rx_data;config_state <= CONFIG_STATE_WRITE;end else beginspi_s_ready <= 1'b1;config_state <= CONFIG_STATE_RECV_DATA_B3;endendCONFIG_STATE_WRITE: beginaddr_buf <= addr_buf;data_buf <= data_buf;spi_s_ready <= 1'b0;spi_s_tx_data <= 8'h0;param_wen <= 1'b1;configure_ram[addr_buf] <= data_buf;config_state <= CONFIG_STATE_IDEL;enddefault: beginaddr_buf <= 8'h0;data_buf <= 32'h0;spi_s_ready <= 1'b0;spi_s_tx_data <= 8'h0;param_wen <= 1'b0;config_state <= CONFIG_STATE_RESET;endendcaseendendendassign dds_fword_source = configure_ram[8'h01];assign dds_pword_source = configure_ram[8'h02];assign dds_amp_source = configure_ram[8'h03];assign direct_fword = configure_ram[8'h11];assign direct_pword = configure_ram[8'h12];assign direct_amp = configure_ram[8'h13];assign drg_freq_start = configure_ram[8'h21];assign drg_freq_end = configure_ram[8'h22];assign drg_freq_step = configure_ram[8'h23];assign drg_freq_pulse = configure_ram[8'h24];assign drg_phase_start = configure_ram[8'h25];assign drg_phase_end = configure_ram[8'h26];assign drg_phase_step = configure_ram[8'h27];assign drg_phase_pulse = configure_ram[8'h28];assign drg_amp_start = configure_ram[8'h29];assign drg_amp_end = configure_ram[8'h2a];assign drg_amp_step = configure_ram[8'h2b];assign drg_amp_pulse = configure_ram[8'h2c];assign adc_freq_center = configure_ram[8'h31];assign adc_freq_kf = configure_ram[8'h32];assign adc_freq_ch_sel = configure_ram[8'h33];assign adc_freq_zero_cal = configure_ram[8'h34];assign adc_phase_center = configure_ram[8'h35];assign adc_phase_kf = configure_ram[8'h36];assign adc_phase_ch_sel = configure_ram[8'h37];assign adc_phase_zero_cal = configure_ram[8'h38];assign adc_amp_center = configure_ram[8'h39];assign adc_amp_kf = configure_ram[8'h3a];assign adc_amp_ch_sel = configure_ram[8'h3b];assign adc_amp_zero_cal = configure_ram[8'h3c];
endmodule
继续看代码说话。我们先对前面的一大堆一看就知道是参数的输出接口和最后几行的所有assign语句进行选择性失明,在脑子里先留下主要的状态机部分。仔细观察状态机,从复位开始,先对configure_ram_ci
进行清零操作。复位结束后,如果configure_ram_ci
小于CONFIGURE_RAM_CI_MAX
,则将对应的RAM条目进行清零,并且对configure_ram_ci
进行加一操作。**这样,在复位之后,就可以保证全部RAM的数据都为零,也就是完成了RAM的全部清零。**在RAM清零完成之后,进入CONFIG_STATE_RESET
状态。其实这个状态没啥用,大可以抛弃掉,因为进入这个状态之前已经对要操作的寄存器清零了很多遍了。
然后状态机直接进入CONFIG_STATE_IDEL
并拉高spi_s_ready
表示已经准备好数据接收,等待spi_s_valid
被拉高时传递数据。如果spi_s_valid
被拉高,则在spi_s_valid
和spi_s_ready
同时被拉高的一个时钟周期内,spi_s_rx_data
和spi_s_tx_data
有效,也就是数据可以进行发送和接收了。在CONFIG_STATE_IDEL
状态时,如果接收到的数据是0x5A,则进入下一个状态,如果接收到其他数据或者没有接收到任何数据,则停留在本状态。
在状态机进入CONFIG_STATE_RECV_ADDR
、CONFIG_STATE_RECV_B0
、CONFIG_STATE_RECV_B1
、CONFIG_STATE_RECV_B2
以及CONFIG_STATE_RECV_B3
时,则进行相应字节数据的接收,具体接收过程同CONFIG_STATE_IDEL
状态。
再回到那一串参数和assign语句。再状态机完成一次传输之后,将会进入CONFIG_STATE_WRITE
状态。这个状态中,状态机会将param_wen
拉高一个时钟周期,也就是意味着进行了一次新的配置,FPGA内部的所有配置都更新一次。
控制字选择矩阵
先摆上代码:
// src/top.valways @(posedge dac_clk) beginif (!rstn) begindirect_en <= 3'b0;drg_en <= 3'b0;adc_en <= 3'b0;phase_fword_i <= 32'h0;phase_pword_i <= 32'h0;amptitude <= 32'h0;endbegincase (dds_fword_source)32'h0000_0001: begindirect_en[0] <= 1'b1;drg_en[0] <= 1'b0;adc_en[0] <= 1'b0;phase_fword_i <= direct_output_fword;end32'h0000_0002: begindirect_en[0] <= 1'b0;drg_en[0] <= 1'b1;adc_en[0] <= 1'b0;phase_fword_i <= drg_output_fword;end32'h0000_0003: begindirect_en[0] <= 1'b0;drg_en[0] <= 1'b0;adc_en[0] <= 1'b1;phase_fword_i <= adc_output_fword;enddefault: begindirect_en[0] <= 1'b0;drg_en[0] <= 1'b0;adc_en[0] <= 1'b0;phase_fword_i <= 32'h0;endendcasecase (dds_pword_source)32'h0000_0001: begindirect_en[1] <= 1'b1;drg_en[1] <= 1'b0;adc_en[1] <= 1'b0;phase_pword_i <= direct_output_pword;end32'h0000_0002: begindirect_en[1] <= 1'b0;drg_en[1] <= 1'b1;adc_en[1] <= 1'b0;phase_pword_i <= drg_output_pword;end32'h0000_0003: begindirect_en[1] <= 1'b0;drg_en[1] <= 1'b0;adc_en[1] <= 1'b1;phase_pword_i <= adc_output_pword;enddefault: begindirect_en[1] <= 1'b0;drg_en[1] <= 1'b0;adc_en[1] <= 1'b0;phase_pword_i <= 32'h0;endendcasecase (dds_amp_source)32'h0000_0001: begindirect_en[2] <= 1'b1;drg_en[2] <= 1'b0;adc_en[2] <= 1'b0;amptitude <= direct_output_amp;end32'h0000_0002: begindirect_en[2] <= 1'b0;drg_en[2] <= 1'b1;adc_en[2] <= 1'b0;amptitude <= drg_output_amp;end32'h0000_0003: begindirect_en[2] <= 1'b0;drg_en[2] <= 1'b0;adc_en[2] <= 1'b1;amptitude <= adc_output_amp;enddefault: begindirect_en[2] <= 1'b0;drg_en[2] <= 1'b0;adc_en[2] <= 1'b0;amptitude <= 32'h0;endendcaseend
其实所谓的选择矩阵就是一堆case语句。按照一般逻辑,一个数据源可以驱动多个数据受体(也就是数据的接收者,这里就是DDS IP核和输出除法器),但是一个数据受体是不能同时接收来自多个数据源的数据的。因此,这一堆条件判断就以数据受体为主体进行编写。通过判断dds_fword_source
、dds_pword_source
和dds_amp_source
,使能不同的数据源的不同通道,给三个数据受体发送不同的数据。注意,每一个数据受体在这个逻辑中都只能在同一时刻接收一个数据源的数据。
核心功能
直接数据字生成器
这个模块相对简单,使用外部配置接口进行参数配置后可以直接进行输出。具体代码如下:
// src/dds/direct.v
module direct_core #(parameter DDS_FREQ = 32'd120000000
) (input clk,input rstn,input param_wen,input [31 : 0] direct_word,input direct_en,input dds_clk,output reg [31 : 0] direct_output
);reg [31 : 0] direct_word_buf;always @(posedge clk) beginif (!rstn) begindirect_word_buf <= 32'h0;end else beginif (param_wen) begindirect_word_buf <= direct_word;end else begindirect_word_buf <= direct_word_buf;endendendalways @(posedge dds_clk) beginif (!rstn) begindirect_output <= 32'h0;end else beginif (direct_en) begindirect_output <= direct_word_buf;end else begindirect_output <= direct_output;endendend
endmodulemodule direct #(parameter DDS_FREQ = 32'd120000000
) (input clk,input rstn,input param_wen,input [31 : 0] direct_fword,input [31 : 0] direct_pword,input [31 : 0] direct_amp,input [2 : 0] direct_en,input dds_clk,output [31 : 0] direct_output_fword,output [31 : 0] direct_output_pword,output [31 : 0] direct_output_amp
);direct_core #(.DDS_FREQ(DDS_FREQ)) direct_freq_inst (.clk (clk),.rstn(rstn),.param_wen (param_wen),.direct_word(direct_fword),.direct_en(direct_en[0]),.dds_clk(dds_clk),.direct_output(direct_output_fword));direct_core #(.DDS_FREQ(DDS_FREQ)) direct_phase_inst (.clk (clk),.rstn(rstn),.param_wen (param_wen),.direct_word(direct_pword),.direct_en(direct_en[1]),.dds_clk(dds_clk),.direct_output(direct_output_pword));direct_core #(.DDS_FREQ(DDS_FREQ)) direct_amp_inst (.clk (clk),.rstn(rstn),.param_wen (param_wen),.direct_word(direct_amp),.direct_en(direct_en[2]),.dds_clk(dds_clk),.direct_output(direct_output_amp));endmodule
明显,direct_core
模块没啥好说的,只是将数据打了一拍,使其从系统主时钟域进入DDS时钟域。这个模块同时提供了使能信号输入,在不使能的情况下将会保持之前的输出(而不是清零)。
着重讲一讲direct
模块。这个模块是直接数据字生成器的顶层模块,例化了三个direct_core
模块,分别输出频率字、相位字和幅度字。这样的例化保证了这三个生成器都是对称的,保证了逻辑唯一性,也减少了后期的维护成本。接下来的数字斜坡生成器和ADC调制生成器都将使用这个模式构建。
数字斜坡生成器
摆上代码好说话:
// src/dds/drg.v
module drg_core #(parameter DDS_FREQ = 32'd120000000
) (input clk,input rstn,input param_wen,input [31 : 0] drg_start,input [31 : 0] drg_end,input [31 : 0] drg_step,input [31 : 0] drg_pulse,input drg_en,input dds_clk,output reg [31 : 0] drg_output
);reg [31 : 0] drg_start_buf;reg [31 : 0] drg_end_buf;reg [31 : 0] drg_step_buf;reg [31 : 0] drg_pulse_buf;always @(posedge clk) beginif (!rstn) begindrg_start_buf <= 32'h0;drg_end_buf <= 32'h0;drg_step_buf <= 32'h0;drg_pulse_buf <= 32'h0;end else beginif (param_wen) begindrg_start_buf <= drg_start;drg_end_buf <= drg_end;drg_step_buf <= drg_step;drg_pulse_buf <= drg_pulse;end else begindrg_start_buf <= drg_start_buf;drg_end_buf <= drg_end_buf;drg_step_buf <= drg_step_buf;drg_pulse_buf <= drg_pulse_buf;endendendreg [31 : 0] drg_pulse_cnt;always @(posedge dds_clk) beginif (!rstn) begindrg_pulse_cnt <= 32'h0;drg_output <= drg_start_buf;end else beginif (drg_en) beginif (drg_pulse_cnt >= drg_pulse_buf) begindrg_pulse_cnt <= 32'h0;if (drg_output + drg_step_buf < drg_end_buf) begindrg_output <= drg_output + drg_step_buf;end else begindrg_output <= drg_start_buf;endend else begindrg_pulse_cnt <= drg_pulse_cnt + 1;drg_output <= drg_output;endend else begindrg_output <= drg_output;endendendendmodulemodule drg #(parameter DDS_FREQ = 32'd120000000
) (input clk,input rstn,input param_wen,input [31 : 0] drg_freq_start,input [31 : 0] drg_freq_end,input [31 : 0] drg_freq_step,input [31 : 0] drg_freq_pulse,input [31 : 0] drg_phase_start,input [31 : 0] drg_phase_end,input [31 : 0] drg_phase_step,input [31 : 0] drg_phase_pulse,input [31 : 0] drg_amp_start,input [31 : 0] drg_amp_end,input [31 : 0] drg_amp_step,input [31 : 0] drg_amp_pulse,input [2 : 0] drg_en,input dds_clk,output [31 : 0] drg_output_fword,output [31 : 0] drg_output_pword,output [31 : 0] drg_output_amp
);drg_core #(.DDS_FREQ(DDS_FREQ)) drg_freqreq_inst (.clk (clk),.rstn(rstn),.param_wen(param_wen),.drg_start(drg_freq_start),.drg_end (drg_freq_end),.drg_step (drg_freq_step),.drg_pulse(drg_freq_pulse),.drg_en(drg_en[0]),.dds_clk (dds_clk),.drg_output(drg_output_fword));drg_core #(.DDS_FREQ(DDS_FREQ)) drg_phasehase_inst (.clk (clk),.rstn(rstn),.param_wen(param_wen),.drg_start(drg_phase_start),.drg_end (drg_phase_end),.drg_step (drg_phase_step),.drg_pulse(drg_phase_pulse),.drg_en(drg_en[1]),.dds_clk (dds_clk),.drg_output(drg_output_pword));drg_core #(.DDS_FREQ(DDS_FREQ)) drg_ampmp_inst (.clk (clk),.rstn(rstn),.param_wen(param_wen),.drg_start(drg_amp_start),.drg_end (drg_amp_end),.drg_step (drg_amp_step),.drg_pulse(drg_amp_pulse),.drg_en(drg_en[2]),.dds_clk (dds_clk),.drg_output(drg_output_amp));endmodule
在配置接口中,drg_core
模块需要四个参数: drg_start
、drg_end
、drg_step
和drg_pulse
。这四个参数分别是开始值、结束值、步进值和等待周期值。在复位时,drg_core
模块将drg_start
提前装入输出寄存器drg_output
;在复位信号结束时,如果drg_core
被使能,则每经过drg_pulse
个DDS时钟周期之后,会将输出寄存器drg_output
增加drg_step
。与此同时,drg_core
模块保证输出值不超过drg_end
。如果在下一次输出寄存器drg_output
增加之后会超过drg_end
,则drg_core
模块不会增加drg_output
,而是会将drg_output
重新装载 drg_start
值。这样可以保证数据不超过限度,最大程度保证系统的稳定性。
同样地,作为数字斜坡发生器的顶层模块,drg
也例化了三个drg_core
实例,对称操作频率字、相位字和幅度字。
ADC实时调制
代码先端上来罢:
module adc_core #(parameter DDS_FREQ = 32'd120000000,parameter ADC_WIDTH = 12
) (input clk,input rstn,input param_wen,input [31 : 0] adc_center,input [31 : 0] adc_kf,input [31 : 0] adc_ch_sel,input [31 : 0] adc_zero_cal,input adc_en,input adc_clk,input [ADC_WIDTH - 1 : 0] adc_data_1,input [ADC_WIDTH - 1 : 0] adc_data_2,input dds_clk,output [31 : 0] adc_core_output
);reg [31 : 0] adc_center_buf;reg [31 : 0] adc_kf_buf;reg [31 : 0] adc_ch_sel_buf;reg [31 : 0] adc_zero_cal_buf;always @(posedge clk) beginif (!rstn) beginadc_center_buf <= 32'h0;adc_kf_buf <= 32'h0;adc_ch_sel_buf <= 32'h0;adc_zero_cal_buf <= 32'h0;end else beginif (param_wen) beginadc_center_buf <= adc_center;adc_kf_buf <= adc_kf;adc_ch_sel_buf <= adc_ch_sel;adc_zero_cal_buf <= adc_zero_cal;end else beginadc_center_buf <= adc_center_buf;adc_kf_buf <= adc_kf_buf;adc_ch_sel_buf <= adc_ch_sel_buf;adc_zero_cal_buf <= adc_zero_cal_buf;endendendreg [ADC_WIDTH - 1 : 0] adc_data;always @(posedge adc_clk) beginif (!rstn) beginadc_data <= {ADC_WIDTH{1'b0}};end else beginif (adc_ch_sel_buf == 32'h0000_0001) beginadc_data <= adc_data_1;end else if (adc_ch_sel_buf == 32'h0000_0002) beginadc_data <= adc_data_2;end else beginadc_data <= {ADC_WIDTH{1'b0}};endendendreg [31 : 0] adc_core_output_i;wire [31 : 0] adc_mul_temp_0;always @(posedge adc_clk) beginif (!rstn) beginadc_core_output_i <= 32'h0;end else beginif (adc_data < adc_zero_cal_buf) beginadc_core_output_i <= adc_center_buf - adc_mul_temp_0;end else beginadc_core_output_i <= adc_center_buf + adc_mul_temp_0;endendenddds_adc_mul_0 dds_adc_mul_0_inst (.clk(adc_clk),.rst(~rstn),.a(adc_kf_buf),.y(adc_data < adc_zero_cal_buf ? adc_zero_cal_buf - adc_data : adc_data - adc_zero_cal_buf),.p(adc_mul_temp_0));adc_core_fifo_0 adc_core_fifo_0_inst (.rst(~rstn),.clkw(adc_clk),.we (adc_en),.di (adc_core_output_i),.clkr(dds_clk),.re (adc_en),.dout(adc_core_output),.valid (),.full_flag (),.empty_flag(),.afull (),.aempty (),.wrusedw (),.rdusedw ());endmodulemodule adc #(parameter DDS_FREQ = 32'd120000000,parameter ADC_WIDTH = 12
) (input clk,input rstn,input param_wen,input [31 : 0] adc_freq_center,input [31 : 0] adc_freq_kf,input [31 : 0] adc_freq_ch_sel,input [31 : 0] adc_freq_zero_cal,input [31 : 0] adc_phase_center,input [31 : 0] adc_phase_kf,input [31 : 0] adc_phase_ch_sel,input [31 : 0] adc_phase_zero_cal,input [31 : 0] adc_amp_center,input [31 : 0] adc_amp_kf,input [31 : 0] adc_amp_ch_sel,input [31 : 0] adc_amp_zero_cal,input [2 : 0] adc_en,input adc_clk,input [11 : 0] adc_data_1,input [11 : 0] adc_data_2,input dds_clk,output [31 : 0] adc_output_fword,output [31 : 0] adc_output_pword,output [31 : 0] adc_output_amp
);adc_core #(.DDS_FREQ (DDS_FREQ),.ADC_WIDTH(ADC_WIDTH)) adc_freq_inst (.clk (clk),.rstn(rstn),.param_wen (param_wen),.adc_center (adc_freq_center),.adc_kf (adc_freq_kf),.adc_ch_sel (adc_freq_ch_sel),.adc_zero_cal(adc_freq_zero_cal),.adc_en(adc_en[0]),.adc_clk (adc_clk),.adc_data_1(adc_data_1),.adc_data_2(adc_data_2),.dds_clk (dds_clk),.adc_core_output(adc_output_fword));adc_core #(.DDS_FREQ (DDS_FREQ),.ADC_WIDTH(ADC_WIDTH)) adc_phase_inst (.clk (clk),.rstn(rstn),.param_wen (param_wen),.adc_center (adc_phase_center),.adc_kf (adc_phase_kf),.adc_ch_sel (adc_phase_ch_sel),.adc_zero_cal(adc_phase_zero_cal),.adc_en(adc_en[1]),.adc_clk (adc_clk),.adc_data_1(adc_data_1),.adc_data_2(adc_data_2),.dds_clk (dds_clk),.adc_core_output(adc_output_pword));adc_core #(.DDS_FREQ (DDS_FREQ),.ADC_WIDTH(ADC_WIDTH)) adc_amp_inst (.clk (clk),.rstn(rstn),.param_wen (param_wen),.adc_center (adc_amp_center),.adc_kf (adc_amp_kf),.adc_ch_sel (adc_amp_ch_sel),.adc_zero_cal(adc_amp_zero_cal),.adc_en(adc_en[2]),.adc_clk (adc_clk),.adc_data_1(adc_data_1),.adc_data_2(adc_data_2),.dds_clk (dds_clk),.adc_core_output(adc_output_amp));endmodule
ADC调制生成器的核心模块adc_core
稍微有点复杂,具体可以参照以下框图:
明显,这个模块跨越了3个异步时钟域,可以说是一个有大凶之兆(必出问题)的模块。首先,从系统时钟域到ADC时钟域的问题比较小一点,因为这个参数的更新频率低,大部分时间时不会改变的,哪怕一个时钟周期传不过来,两个、三个之后总能稳定下来。事实上,我连拍都没打,也没有在这里出现任何时序问题。
但是,从ADC时钟域到DDS时钟域的数据可是时时刻刻会更新的。这就需要一个低深度异步时钟FIFO来进行数据过渡了。这里选择了安路的Soft_FIFO,很好用。没加这个FIFO之前,DDS打出来的数据根本不能看。用CWC抓了一下内部逻辑,发现是ADC调制发生器的输出数据紊乱了,所以立马加了一个FIFO,数据果然变得稳如老狗。
RAM调制(未实现)
按理来说,像AD9910那样,搞个1024深度的RAM也不难。但是我不想用这么low的办法,我想用的是DDR的512MByte深度,有点好高骛远,因此到现在为止暂时还没实现,还请等待一下吧,哈哈(悲伤的笑脸)。
除法器调幅
在dds的顶层模块中,实现了一个除法器作为数字调幅的部件。但是说实话,还不如不加,因为除法运算是非线性的,用drg作为幅度字、direct作为频率字输出时,得到波形包络并非一条直线,而是一个反比例函数的一部分。
但是技术细节还是可以讲一讲的。首先使用安路的DDS IP进行DDS初始数据的输出。这个IP核输出的数据位宽为32位,选择sin+cos(正交信号输出)模式,为后面的除法运算提供足够巨大的数据。
官方IP手册说DDS的IP核输出数据最多可以到26位,但是我写32的时候,TD并没有表现出什么异议。这句话的意思不是说安路的TD写得好,而是在催安路官方赶紧更新一下数据手册和参考手册。说实话,不是国产的器件我还懒得骂(甚至懒得用)了,就是因为安路是国产FPGA第一梯队之一,才要特地提醒一下
然后就是直接将DDS核的输出接到除法器中。注意,因为DDS核输出的是补码,因此除法器的分子应当设为singed(有符号数)。而我产生的幅度字是原码,因此分母设置为unsigned(无符号数)。这样,除法器就会输出一个有符号数的32位商一个有符号数的32位余数。余数直接忽略掉,把商的最高位取反(这里用的处理方法是在最高位加1)就是原码。此时的32位的商取低n位(n为DAC位数)就可以输出到DAC了。
外部接口
其实现在主要就是一个SPI Slave(FPGA作为从机)首先MCU到FPGA的数据传输。如果后续有需要的话会考虑实现一个SPI主从通道,实现FPGA高速采集之后向MCU的数据传输。不过高深度的数据采集仍然需要DDR3作为储存支撑,而一旦涉及DDR内存读写,就必然不可回避高速仲裁器的问题。如果以后写出来了,一定会第一时间出文章的。
参考资料
安路DDS IP核用户中文手册
AD9910官方数据手册(英文)
附件链接
这一组代码的Github仓库链接,恳请各位给个star鼓励鼓励吧(扭曲地渴望)
DDS详解——以安路PH1A90实现为例相关推荐
- 搞一下SOA | 03 DDS详解
前言 搞一下SOA系列会从EEA趋势.SOA实例分析.SOME/IP.MQTT.HTTP.DDS.E2E保护.SOA软件架构设计.ARXML设计等方面进行分享 上期我们分享了SOA协议之SOME/IP ...
- 消息队列超详解(以RabbitMQ和Kafka为例,为何使用消息队列、优缺点、高可用性、问题解决)
消息队列超详解(以RabbitMQ和Kafka为例) 为什么要用消息队列这个东西? 先说一下消息队列的常见使用场景吧,其实场景有很多,但是比较核心的有3个:解耦.异步.削峰. 解耦:现场画个图来说明一 ...
- 智能小区 安防技术详解及安防隐患杂谈
随着人们生活水平的提高,对各方面的需求也相应的提高,对小区的安防也不例外.从原来简单的对讲系统,到如今的智能化小区,经过了十几年不断地研究和发展,我国的智能小区应用与产业都走到了新的阶段. 智能小区安 ...
- Linux TC 流量控制与排队规则 qdisc 树型结构详解(以HTB和RED为例)
1. 背景 Linux 操作系统中的流量控制器 TC (Traffic Control) 用于Linux内核的流量控制,它规定建立处理数据包的队列,并定义队列中的数据包被发送的方式,从而实现对流量的控 ...
- java 接口访问权限_详解Java之路(五) 访问权限控制
在Java中,所有事物都具有某种形式的访问权限控制. 访问权限的控制等级从最大到最小依次为:public,protected,包访问权限(无关键词)和private. public,protected ...
- 【人工智能】基于百度AI和Python编程的简单实现:通过QQ/Tim截图进行文本识别功能的分析实战详解——以获取百度文库付费内容为例
前两天,博主在摸鱼时偶然接触到了百度AI,一时间来了兴趣.在实战测试了其中的"通用文字识别"后,发现效果还是蛮不错的.所以通过本次文章记录一下,以作备忘. 前期准备 百度AI前期准 ...
- 卷积神经网络(CNN)详解与代码实现
目录 1.应用场景 2.卷积神经网络结构 2.1 卷积(convelution) 2.2 Relu激活函数 2.3 池化(pool) 2.4 全连接(full connection) 2.5 损失函数 ...
- Popular Cows POJ - 2186(tarjan算法)+详解
题意: 每一头牛的愿望就是变成一头最受欢迎的牛.现在有 N头牛,给你M对整数(A,B),表示牛 A认为牛B受欢迎.这种关系是具有传递性的,如果 A认为 B受欢迎, B认为 C受欢迎,那么牛 A也认为牛 ...
- 【java8新特性】——Stream API详解(二)
一.简介 java8新添加了一个特性:流Stream.Stream让开发者能够以一种声明的方式处理数据源(集合.数组等),它专注于对数据源进行各种高效的聚合操作(aggregate operation ...
最新文章
- 独家 | 带你认识机器学习的的本质(附资料)
- golang 下划线
- 实现连麦_微信年底放了个大招,视频号重磅升级,打赏直播连麦美颜抽奖齐上...
- Package ‘*****‘ has no installation candidate
- linux ssh注册码,linux ssh -l 命令运用
- oracle 00350,Oracle错误编码大全
- 神经网络技巧篇之寻找最优超参数
- 天涯明月刀大地的服务器位置,天涯明月刀东海玉涡位置坐标指南[图]
- 工程制图与计算机绘图试卷A,工程制图与计算机绘图第4章
- 58.3万笔/秒!看阿里的黑科技
- 带你入门Java网络爬虫
- 怀旧服私聊显示服务器后缀,聊天窗口相关设置:有爱怀旧服聊天增强插件简易指南...
- Java将文件转换成二维码
- 微信公众号第三方平台开发笔记--01创建第三方平台
- python 爬虫下载网易歌单歌曲
- 一款Excel导入导出解决方案组成的轻量级开源组件
- 为什么白素贞能生文曲星转世许仕林? 和她的另一个身份有关
- java md5库_Java常用类库API之MD5简单使用
- 前端之扇形图实现案例
- python实现文档图像倾斜矫正,实现类似扫描仪功能