FPGA学习.7——DDS信号发生器

DDS是直接数字式频率合成器(Direct Digital Synthesizer)的英文缩写,是一项关键的数字化技术。与传统的频率合成器相比,DDS具有低成本、低功耗、高分辨率和快速转换时间等优点,广泛使用在电信与电子仪器领域,是实现设备全数字化的一个关键技术。作为设计人员,我们习惯称它为信号发生器,一般用它产生正弦、锯齿、方波等不同波形或不同频率的信号波形,在电子设计和测试中得到广泛应用。

DDS的基本结构主要由相位累加器、相位调制器、波形数据表ROM、D/A转换器等四大结构组成,其中较多设计还会在数模转换器之后增加一个低通滤波器。

系统时钟CLK为整个系统的工作时钟,频率为fCLK;频率字输入F_WORD,一般为整数,数值大小控制输出信号的频率大小,数值越大输出信号频率越高,反之,输出信号频率越低,后文中用K表示;相位字输入P_WORD,为整数,数值大小控制输出信号的相位偏移,主要用于相位的信号调制,后文用P表示;设输出信号为CLK_OUT,频率为fOUT。

图中所展示的四大结构中,相位累加器是整个DDS的核心,在这里完成相位累加,生成相位码。相位累加器的输入为频率字输入K,表示相位增量,设其位宽为N,满足等式K = 2N * fOUT / fCLK 。其在输入相位累加器之前,在系统时钟同步下做数据寄存,数据改变时不会干扰相位累加器的正常工作。

相位调制器接收相位累加器输出的相位码, 在这里加上一个相位偏移值P,主要用于信号的相位调制,如应用于通信方面的相移键控等,不使用此部分时可以去掉,或者将其设为一个常数输入,同样相位字输入也要做寄存。

波形数据表ROM中存有一个完整周期的正弦波信号。假设波形数据ROM的地址位宽为12位,存储数据位宽为8位,即ROM有212 = 4096个存储空间,每个存储空间可存储1字节数据。将一个周期的正弦波信号,沿横轴等间隔采样212 = 4096次,每次采集的信号幅度用1字节数据表示,最大值为255,最小值为0。将4096次采样结果按顺序写入ROM的4096个存储单元,一个完整周期正弦波的数字幅度信号写入了波形数据表ROM中。波形数据表ROM以相位调制器传入的相位码为ROM读地址,将地址对应存储单元中的电压幅值数字量输出。

D/A转换器将输入的电压幅值数字量转换为模拟量输出,就得到输出信号CLK_OUT。

输出信号CLK_OUT的信号频率fOUT = K * fCLK / 2N。当K = 1时,可得DDS最小分辨率为:fOUT = fCLK / 2N,此时输出信号频率最低。根据采样定理,K的最大值应小于2N / 2。

讲到这里,你可能会心存疑虑,相位累加器得到的相位码是如何实现ROM寻址的呢?

对于N位的相位累加器,它对应的相位累加值为2N,如果正弦ROM中存储单元的个数也是2N的话,这个问题就很好解决,但是这对ROM的对存储容量的要求较高。在实际操作中,我们使用相位累加值的高几位对ROM进行寻址,也就是说并不是每个系统时钟都对ROM进行数据读取,而是多个时钟读取一次,因为这样能保证相位累加器溢出时,从正弦ROM表中取出正好一个正弦周期的样点。

因此,相位累加器每计数2N次,对应一个正弦周期。而相位累加器1秒钟计数fCLK次,在k=1时,DDS输出的时钟频率就是频率分辨率。频率控制字K增加时,相位累加器溢出的频率增加,对应DDS输出信号CLK_OUT频率变为K倍的DDS频率分辨率。

举个例子:

设:ROM存储单元个数为4096,每个存储数据用8位二进制表示。即,ROM地址线宽度为12,数据线宽度为8;相位累加器位宽N = 32。

根据上述条件可以知道,相位调制器位宽M = 12,那么根据DDS原理。那么在相位调制器中与相位控制字进行累加时,应用相位累加器的高12位累加。而相位累加器的低20位只与频率控制字累加。

我们以频率控制字K = 1为例,相位累加器的低20位一直会加1,直到低20位溢出向高12位进位,此时ROM为0,也就是说,ROM的0地址中的数据被读了220次,继续下去,ROM中的4096个点,每个点都将会被读220次,最终输出的波形频率应该是参考时钟频率的1 / 220,周期被扩大了220 倍。同样当频率控制字为100时,相位累加器的低20位一直会加100,那么,相位累加器的低20位溢出的时间比上面会快100倍,则ROM中的每个点相比于上面会少读100次,所以最终输出频率是上述的10倍。

自波形数据表ROM输出的波形数据传入D/A转换器转换为模拟信号。D/A转换器即数/模转换器,简称DAC(Digital to Analog Conver),是指将数字信号转换为模拟信号的电子元件或电路。

DAC内部电路构造无太大差异,大多数DAC由电阻阵列和n个电流开关(或电压开关)构成,按照输入的数字值进行开关切换,输出对应电流或电压。因此,按照输出信号类型可分为电压型和电流型,也可以按照DAC能否做乘法运算进行分类。若将DAC分为电压型和电流型两大类,电压型DAC中又有权电阻网络、T形电阻网络、树形开关网络等分别;电流型DAC中又有权电流型电阻网络和倒T形电阻网络等。

电压输出型DAC一般采用内置输出放大器以低阻抗输出,少部分直接通过电阻阵列进行电压输出。直接输出电压的DAC仅用于高阻抗负载,由于无输出放大器部分的延迟,故常作为高速DAC使用。

电流输出型DAC很少直接利用电流输出,大多外接电流 - 电压转换电路进行电压输出。实现电流 - 电压转换,方法有二:一是只在输出引脚上接负载电阻而进行电流- 电压转换,二是外接运算放大器。

DAC的主要技术指标包括分辨率、线性度、转换精度和转换速度。

分辨率指输出模拟电压的最小增量,即表明DAC输入一个最低有效位(LSB)而在输出端上模拟电压的变化量。

线性度在理想情况下,DAC的数字输入量作等量增加时,其模拟输出电压也应作等量增加,但是实际输出往往有偏离。

D/A转换器的转换精度与D/A转换器的集成芯片的结构和接口电路配置有关。如果不考虑其他D/A转换误差时,D/A的转换精度就是分辨率的大小,因此要获得高精度的D/A转换结果,首先要保证选择有足够分辨率的D/A转换器。同时D/A转换精度还与外接电路的配置有关,当外部电路器件或电源误差较大时,会造成较大的D/A转换误差,当这些误差超过一定程度时,D/A转换就产生错误。

转换速度一般由建立时间决定。建立时间是将一个数字量转换为稳定模拟信号所需的时间,也可以认为是转换时间。DA中常用建立时间来描述其速度,而不是AD中常用的转换速率。一般地,电流输出DA建立时间较短,电压输出DA则较长。

开始实操,我们需要一块FPGA开发板和一块DAC转换模块。这里我是使用Altera EP4CE10+AD9708

工程整体框图:.

时钟、复位和代表波形选择的4个按键信号通过顶层传入按键控制模块(key_control),按键控制模块内部实例化4个按键消抖模块,对输入的4路按键信号分别进行消抖处理;消抖处理后的4路按键信号组成波形选择信号输入dds模块(dds), dds模块中实例化一个ROM IP核,按顺序存入了一个完整周期的正弦波、方波、三角波、锯齿波的信号波形,根据输入波形选择信号对rom中对应信号波形进行读取,将读出波形的幅度数字值输出,传入外部挂载的高速AD/DA板卡的DA端,板卡根据输入的数字信号生成对应波形的模拟信号。其中,输出信号的频率和相位的调节可在dds模块中通过修改参数实现。

ROM内波形数据写入

我们设计的DDS简易信号发生器想要实现正弦波、方波、三角波和锯齿波4种波形的输出,需要事先在波形数据表ROM中存入4种波形信号各自的完整周期波形数据。ROM作为只读存储器,在进行IP核设置时需要指定初始化文件,我们将波形数据作为初始化文件写入其中,文件格式为COE文件。

使用MatLab绘制4种信号波形,对波形进行等间隔采样,以采样次数作为ROM存储地址,将采集的波形幅值数据做为存储数据写入存储地址对应的存储空间。在本次实验中我们对4种信号波形进行分别采样,采样次数为212 = 4096次,采集的波形幅值数据位宽为8bit,将采集数据保存为MIF文件。

以下为matlab代码

正弦信号波形采集参考代码:

clc;                    %清除命令行命令
clear all;              %清除工作区变量,释放内存空间
F1=1;                   %信号频率
Fs=2^12;                %采样频率
P1=0;                   %信号初始相位
N=2^12;                 %采样点数
t=[0:1/Fs:(N-1)/Fs];    %采样时刻
ADC=2^7 - 1;            %直流分量
A=2^7;                  %信号幅度
%生成正弦信号
s=A*sin(2*pi*F1*t + pi*P1/180) + ADC;
plot(s);                %绘制图形
%创建coe文件
fild = fopen('sin_wave_4096x8.coe','wt');
%写入coe文件头
fprintf(fild, '%s\n','MEMORY_INITIALIZATION_RADIX=10;'); %10进制数
fprintf(fild, '%s\n','MEMORY_INITIALIZATION_VECTOR='); 
for i = 1:N
    s0(i) = round(s(i));    %对小数四舍五入以取整
    if s0(i) <0             %负1强制置零
        s0(i) = 0
    end
    if i == N
            fprintf(fild, '%d',s0(i));      %数据写入
            fprintf(fild, '%s',';');        %最后一个数据使用分号
    else
            fprintf(fild, '%d',s0(i));      %数据写入
            fprintf(fild, '%s\n',',');      %逗号,换行
    end     
end
fclose(fild);

方波信号波形采集参考代码:

clc;                    %清除命令行命令
clear all;              %清除工作区变量,释放内存空间
F1=1;                   %信号频率
Fs=2^12;                %采样频率
P1=0;                   %信号初始相位
N=2^12;                 %采样点数
t=[0:1/Fs:(N-1)/Fs];    %采样时刻
ADC=2^7 - 1;            %直流分量
A=2^7;                  %信号幅度
%生成方波信号
s=A*square(2*pi*F1*t + pi*P1/180) + ADC;
plot(s);                %绘制图形
%创建coe文件
fild = fopen('squ_wave_4096x8.coe','wt');
%写入coe文件头
fprintf(fild, '%s\n','MEMORY_INITIALIZATION_RADIX=10;'); %10进制数
fprintf(fild, '%s\n','MEMORY_INITIALIZATION_VECTOR='); 
for i = 1:N
    s0(i) = round(s(i));    %对小数四舍五入以取整
    if s0(i) <0             %负1强制置零
        s0(i) = 0
    end
    if i == N
            fprintf(fild, '%d',s0(i));      %数据写入
            fprintf(fild, '%s',';');        %最后一个数据使用分号结束
    else
            fprintf(fild, '%d',s0(i));      %数据写入
            fprintf(fild, '%s\n',',');      %逗号,换行
    end     
end
fclose(fild);

三角波信号波形采集参考代码:

clc;                    %清除命令行命令
clear all;              %清除工作区变量,释放内存空间
F1=1;                   %信号频率
Fs=2^12;                %采样频率
P1=0;                   %信号初始相位
N=2^12;                 %采样点数
t=[0:1/Fs:(N-1)/Fs];    %采样时刻
ADC=2^7 - 1;            %直流分量
A=2^7;                  %信号幅度
%生成三角波信号
s=A*sawtooth(2*pi*F1*t + pi*P1/180,0.5) + ADC;
plot(s);                %绘制图形
%创建coe文件
fild = fopen('tri_wave_4096x8.coe','wt');
%写入coe文件头
fprintf(fild, '%s\n','MEMORY_INITIALIZATION_RADIX=10;'); %10进制数
fprintf(fild, '%s\n','MEMORY_INITIALIZATION_VECTOR='); 
for i = 1:N
    s0(i) = round(s(i));    %对小数四舍五入以取整
    if s0(i) <0             %负1强制置零
        s0(i) = 0
    end
    if i == N
            fprintf(fild, '%d',s0(i));      %数据写入
            fprintf(fild, '%s',';');        %最后一个数据使用分号结束
    else
            fprintf(fild, '%d',s0(i));      %数据写入
            fprintf(fild, '%s\n',',');      %逗号,换行
    end     
end
fclose(fild);

锯齿波信号波形采集参考代码:

clc;                    %清除命令行命令
clear all;              %清除工作区变量,释放内存空间
F1=1;                   %信号频率
Fs=2^12;                %采样频率
P1=0;                   %信号初始相位
N=2^12;                 %采样点数
t=[0:1/Fs:(N-1)/Fs];    %采样时刻
ADC=2^7 - 1;            %直流分量
A=2^7;                  %信号幅度
%生成锯齿波信号
s=A*sawtooth(2*pi*F1*t + pi*P1/180) + ADC;
plot(s);                %绘制图形
%创建coe文件
fild = fopen('saw_wave_4096x8.coe','wt');
%写入coe文件头
fprintf(fild, '%s\n','MEMORY_INITIALIZATION_RADIX=10;'); %10进制数
fprintf(fild, '%s\n','MEMORY_INITIALIZATION_VECTOR='); 
for i = 1:N
    s0(i) = round(s(i));    %对小数四舍五入以取整
    if s0(i) <0             %负1强制置零
        s0(i) = 0
    end
    if i == N
            fprintf(fild, '%d',s0(i));      %数据写入
            fprintf(fild, '%s',';');        %最后一个数据使用分号结束
    else
            fprintf(fild, '%d',s0(i));      %数据写入
            fprintf(fild, '%s\n',',');      %逗号,换行
    end     
end
fclose(fild);

使用MatLab对4种波形进行采样后,生成4个MIF文件,分别对应4种波形,我们可以调用4个深度为4096,位宽为8bit的ROM IP核,将4种波形对应的COE文件分别写入,生成4个波形数据表ROM。

也可以调用一个深度为4096*4,位宽为8bit的ROM,生成一个COE文件,这个COE文件位上述4个COE文件的集合波形数据按照正弦波、方波、三角波、锯齿波的顺序写入。总的COE文件生成代码如下所示。

clc;                    %清除命令行命令
clear all;              %清除工作区变量,释放内存空间
F1=1;                   %信号频率
Fs=2^12;                %采样频率
P1=0;                   %信号初始相位
N=2^12;                 %采样点数
t=[0:1/Fs:(N-1)/Fs];    %采样时刻
ADC=2^7 - 1;            %直流分量
A=2^7;                  %信号幅度
s1=A*sin(2*pi*F1*t + pi*P1/180) + ADC;          %正弦波信号
s2=A*square(2*pi*F1*t + pi*P1/180) + ADC;       %方波信号
s3=A*sawtooth(2*pi*F1*t + pi*P1/180,0.5) + ADC; %三角波信号
s4=A*sawtooth(2*pi*F1*t + pi*P1/180) + ADC;     %锯齿波信号
%创建coe文件
fild = fopen('wave_16384x8.coe','wt');
%写入coe文件头
fprintf(fild, '%s\n','MEMORY_INITIALIZATION_RADIX=10;'); %10进制数
fprintf(fild, '%s\n','MEMORY_INITIALIZATION_VECTOR=');
for j = 1:4
    for i = 1:N
        if j == 1       %打印正弦信号数据
            s0(i) = round(s1(i));    %对小数四舍五入以取整
        end

        if j == 2       %打印方波信号数据
            s0(i) = round(s2(i));    %对小数四舍五入以取整
        end

        if j == 3       %打印三角波信号数据
            s0(i) = round(s3(i));    %对小数四舍五入以取整
        end

        if j == 4       %打印锯齿波信号数据
            s0(i) = round(s4(i));    %对小数四舍五入以取整
        end

        if s0(i) <0             %负1强制置零
            s0(i) = 0
        end
        
        if j == 4 && i == N
            fprintf(fild, '%d',s0(i));      %数据写入
            fprintf(fild, '%s',';');        %最后一个数使用分号结束
        else
            fprintf(fild, '%d',s0(i));      %数据写入
            fprintf(fild, '%s\n',',');      %逗号,换行
        end
    end
end
fclose(fild);

DDS模块

DDS模块有3路输入信号,1路输出信号,其内部例化了前面生成的波形数据表ROM;输入信号中有时钟sys_clk、复位sys_rst_n和按键控制模块输入的波形选择信号wave_select。输入的波形选择信号有4种状态,分别对应4中波形,根据输入的波形选择信号的不同,对ROM中波形选择信号对应波形的存储位置进行数据读取,将读出波形幅值数据通过输出信号data_out输出到外部挂载DA板块,进行数模转换。

本模块包含3路输入信号,1路输出信号,内部声明3个寄存器变量。输入信号包括时钟、复位和输出波形选择信号wave_select。波形选择信号有4个状态,对应4种波形。

内部声明3个寄存器变量。其中fre_add表示相位累加器输出值,位宽为32位,系统上电后,fre_add信号一直执行自加操作,每个时钟周期自加参数FREQ_CTRL,参数FREQ_CTRL就是在之前理论知识部分提到的频率字输入K,它的具体数值可通过公式计算得到,前面我们也讲到过。

寄存器变量rom_addr_reg表示相位调制器输出值,将相位累加器输出值的高12位与相位偏移量PHASE_CTRL相加,参数PHASE_CTRL就是我们之前提到过的相位字输入P。之所以使用高12位,与存储波形的ROM深度有关。按理论讲,将得到的变量rom_addr_reg,可直接作为ROM读地址输入波形数据表进行数据读取,但是我们将4中波形存储在了同一ROM中,所以还需要对读数据地址做进一步计算。

ROM读地址rom_addr是输入波形数据表的ROM读地址,是在rom_addr_reg的基础上计算得到。我们之前将4种信号波形数据按照正弦波、方波、三角波、锯齿波的顺序写入ROM。若需要读取正弦波波形数据,rom_addr_reg可直接赋值给rom_addr;但是要进行方波波形数据的读取,rom_addr_reg需要再加上正弦波存储单元个数才能赋值给rom_addr;剩余两信号同理。

对于参数FREQ_CTRL和PHASE_CTRL,我们可以修改其参数值,实现不同频率、不同初相位波形的输出。

本实验,我们希望输出一个频率为500Hz,初相位为π/2的正弦波信号。

计算参数FREQ_CTRL,即频率输入字K。

FREQ_CTRL = K  = 2N * fOUT / fCLK,其中N = 32(相位累加器输出值fre_add的位宽)、fOUT = 500Hz,fCLK = 50MHz,带入公式,FREQ_CTRL = K = 42949.67296 ,取整数部分为42949;

计算参数PHASE_CTRL,即相位输入字P。

PHASE_CTRL = P = q / (2π / 2M),其中M =12(输入ROM地址位宽)、q = π / 2,带入公式,PHASE_CTRL = P = 1024。

ROM读地址rom_addr写入波形数据表ROM中,读出地址对应波形波形数据,通过输出端口data_out输入到外部挂载板卡数模转换部分。

上代码:

module  dds
(
    input   wire            sys_clk     ,   //系统时钟,50MHz
    input   wire            sys_rst_n   ,   //复位信号,低电平有效
    input   wire    [3:0]   wave_select ,   //输出波形选择

    output  wire    [7:0]   data_out        //波形输出
);

//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//parameter define
parameter   sin_wave    =   4'b0001     ,   //正弦波
            squ_wave    =   4'b0010     ,   //方波
            tri_wave    =   4'b0100     ,   //三角波
            saw_wave    =   4'b1000     ;   //锯齿波
parameter   FREQ_CTRL   =   32'd42949   ,   //相位累加器单次累加值
            PHASE_CTRL  =   12'd1024    ;   //相位偏移量

//reg   define
reg     [31:0]  fre_add     ;   //相位累加器
reg     [11:0]  rom_addr_reg;   //相位调制后的相位码
reg     [13:0]  rom_addr    ;   //ROM读地址

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//fre_add:相位累加器
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        fre_add <=  32'd0;
    else
        fre_add <=  fre_add + FREQ_CTRL;

//rom_addr:ROM读地址
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        begin
            rom_addr        <=  14'd0;
            rom_addr_reg    <=  11'd0;
        end
    else
    case(wave_select)
        sin_wave:
            begin
                rom_addr_reg    <=  fre_add[31:20] + PHASE_CTRL;
                rom_addr        <=  rom_addr_reg;
            end     //正弦波
        squ_wave:
            begin
                rom_addr_reg    <=  fre_add[31:20] + PHASE_CTRL;
                rom_addr        <=  rom_addr_reg + 14'd4096;
            end     //方波
        tri_wave:
            begin
                rom_addr_reg    <=  fre_add[31:20] + PHASE_CTRL;
                rom_addr        <=  rom_addr_reg + 14'd8192;
            end     //三角波
        saw_wave:
        begin
                rom_addr_reg    <=  fre_add[31:20] + PHASE_CTRL;
                rom_addr        <=  rom_addr_reg + 14'd12288;
            end     //锯齿波
        default:
            begin
                rom_addr_reg    <=  fre_add[31:20] + PHASE_CTRL;
                rom_addr        <=  rom_addr_reg;
            end     //正弦波
    endcase

//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//------------------------- rom_wave_inst ------------------------

rom_wave    rom_wave_inst
(
  .clka(sys_clk), // input clka
  .addra(rom_addr), // input [13 : 0] addra
  .douta(data_out) // output [7 : 0] douta
);

endmodule

代码里调用了一个ROM IP核,我们的ROM文件也要在生成IP时写进去。

 

顶层:

module  top_dds
(
    input   wire            sys_clk     ,   //系统时钟,50MHz
    input   wire            sys_rst_n   ,   //复位信号,低电平有效
    input   wire    [3:0]   key         ,   //输入4位按键

    output  wire            dac_clk     ,   //输入DAC模块时钟
    output  wire    [7:0]   dac_data        //输入DAC模块波形数据
);

//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//wire  define
wire    [3:0]   wave_select ;   //波形选择

//dac_clka:DAC模块时钟
assign  dac_clk  = ~sys_clk;

//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//-------------------------- dds_inst -----------------------------
dds     dds_inst
(
    .sys_clk        (sys_clk    ),   //系统时钟,50MHz
    .sys_rst_n      (sys_rst_n  ),   //复位信号,低电平有效
    .wave_select    (wave_select),   //输出波形选择

    .data_out       (dac_data   )    //波形输出
);

//----------------------- key_control_inst ------------------------
key_control key_control_inst
(
    .sys_clk        (sys_clk    ),   //系统时钟,50MHz
    .sys_rst_n      (sys_rst_n  ),   //复位信号,低电平有效
    .key            (key        ),   //输入4位按键

    .wave_select    (wave_select)    //输出波形选择
 );

endmodule

顶层代码很简单,就例化一个按键模块和DDS模块,按不同的按键,输出不同的波形,按键模块很简单就不说了。

结果如下:

 

 

 

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值