【小梅哥FPGA进阶教程】第十一章 四通道幅频相可调DDS信号发生器

十一、四通道幅频相可调DDS信号发生器

 

本文由山东大学研友袁卓贡献,特此感谢

实验目标

实现多通道可调信号发生器

实验平台

芯航线FPGA核心板、ADDA模块

图片1

图片2

实验现象

实现基于FPGA的多通道可调信号发生器,其中频率、相位以及幅值均可通过PC端串口发送数据对应调节,并可实现4路信号的同步。

实验原理及设计过程

经过前面小梅哥基础课程的学习,相信已经对FPGA的设计有了一定程度的了解,现在提出一个相对综合的工程应用来深入了解FPGA的设计思路以及工程思想等。

针对以上预期实验现象可以分析出最少需要DDS模块、TLC5620控制模块、串口接收模块以及控制模块。

DDS原理与实现

DDS(Direct Digital Synthesizer)即数字合成器,是一种新型的频率合成技术,具有相对带宽大,频率转换时间短、分辨率高和相位连续性好等优点。较容易实现频率、相位以及幅度的数控调制,广泛应用于通信领域。

DDS 的基本结构图如图1所示。

图片3

图1 DDS基本结构图

由图1可以看出,DDS主要由相位累加器、相位调制器、波形数据表以及 D/A 转换器构成。

其中相位累加器由 N位加法器与N位寄存器构成。每来一个时钟,加法器就将频率控制字与累加寄存器输出的相位数据相加,相加的结果又反馈至累加寄存器的数据输入端,以使加法器在下一个时钟脉冲的作用下继续与频率控制字相加。这样,相位累加器在时钟作用下,不断对频率控制字进行线性相位累加。即在每一个时钟脉冲输入时,相位累加器便把频率控制字累加一次。

相位累加器输出的数据就是合成信号的相位。相位累加器的溢出频率,就是 DDS输出的信号频率。用相位累加器输出的数据,作为波形存储器的相位采样地址,这样就可以把存储在波形存储器里的波形采样值经查表找出,完成相位到幅度的转换。波形存储器的输出送到 D/A 转换器,由 D/A 转换器将数字信号转换成模拟信号输出。

DDS信号流程示意图如图2所示。

图片4

图2 DDS原理流程图

这里相位累加器位数为N位(N的取值范围实际应用中一般为24~32),相当于把正弦信号在相位上的精度定义为N位,所以其分辨率为图片4-1

若DDS的时钟频率为图片5,频率控制字fword为 1,则输出频率为图片6,这个频率相当于“基频”。若fword为 B,则输出频率为图片7

因此理论上由以上三个参数就可以得出任意的图片6-1输出频率。且可得出频率分辨率由时钟频率和累加器的位数决定。当参考时钟频率越高,累加器位数越高,输出频率分辨率就越高。

从上式分析可得,当系统输入时钟频率图片7-1不变时,输出信号频率由频率控制字M所决定,由上式可得:图片8。其中B为频率字且只能取整数。为了合理控制ROM的容量此处选取ROM查询的地址时,可以采用截断式,即只取32位累加器的高M位。这里相位寄存器输出的位数一般取10~16位。

在本设计中参考时钟图片8-1频率为50 MHz,相位累加器位数N取32位,频率控制字位数M 取12位。

经过以上的分析,可以得出DDS模块的端口模块图如图3所示。

图片9

图3 DDS模块接口示意图

其中,每个端口的功能描述如表1所示。

图片10

表1 DDS模块功能描述

新建DDS_Module.v保存至rtl文件夹下。从图3以及表1就得到了端口列表:

    input Clk;
    input Rst_n;
    input EN;
    input [31:0]Fword;
    input [11:0]Pword;
    
    output DA_Clk;
    output [9:0]DA_Data;

以下只需按照图1进行编写。相位累加器此处即为一个32bit的加法器。

    reg [31:0]Fre_acc;  
    
    always @(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        Fre_acc <= 32'd0;
    else if(!EN)
        Fre_acc <= 32'd0;   
    else 
        Fre_acc <= Fre_acc + Fword;

查找表地址生成,此处即为12bit的加法器。这里直接截取32位累加器结果中的高12位作为ROM的查询地址,这样产生的误差会对频谱纯度有影响,但是对波形的精度的影响是可以忽略的。

    reg [11:0]Rom_Addr;
    
    always @(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        Rom_Addr <= 12'd0;
    else if(!EN)
        Rom_Addr <= 12'd0;
    else
        Rom_Addr <= Fre_acc[31:20] + Pword; 

DA数据输出时钟模块使能,通过选择器来进行控制。

    assign DA_Clk = (EN)?Clk:1'b1;

现在只需要例化存有波形文件的ROM即可。其中单端口的ROM主要配置数据如图4所示,其初始化文件选为已经生成的正弦mif文件。此处mif位宽为10,深度为4096。

图片15

图4 ROM主要配置参数

这样例化到DDS_Module中即为如下内容。

    ddsrom ddsrom(
        .address(Rom_Addr),
        .clock(Clk),
        .q(DA_Data)
    );

本模块编译无误后,点击RTL Viewer后可以看到如图5所示的模块逻辑电路图,可与DDS原理图对比。这里有两个选择器的原因是加了使能端的缘故,如果将使能端去掉即可看到如图6所示的电路图

图片17

图5 DDS模块设计逻辑电路图

图片18

图6 DDS模块简化逻辑电路图

设置此文件为顶层进行功能仿真。激励文件中除了产生正常的时钟以及模块例化调用,还需使能模块以及设置自加字。此处简化将Fword设置为固定值’d5000,。

initial begin
    Rst_n = 1'b0;
    EN = 1'b0;
    Fword = 32'd0;
    Pword = 12'd0;
    
    #(`clk_period*20)
    Rst_n = 1'b1;
    #(`clk_period*20)
    EN = 1'b1;
    Fword = 32'd5000;

    #(`clk_period*2000000)
    $stop;
end 

编译无误后设置好仿真脚本并进行仿真,可以看到如图7所示的功能仿真波形文件,可以看出波形数据正常。放大仿真开始位置,可以看出输出DA时钟使能设计也正常。初始值为’d511也就是’h1ff,与初始化mif文件一致。

图片20

图7 DDS模块功能仿真波形

图片21

图8 DDS模块部分放大波形文件

这样一个DDS模块即设计完成,这里可以自行修改Pword的值进行观察波形相位是否发生相应变化。

数模转换器(DAC)控制模块设计

这里采用DAC芯片为芯航线AD/DA模块上的TLC5620。其中TLC5620模块的设计与实现在基础课第17讲已经详细阐述,此处不再对本部分进行解释。其模块接口示意图9所示,其接口对应的功能描述如表2所示。

图片22

图9 DA模块端口接口示意图

图片23

表2 DA模块接口功能描述

多通道数据输出实现

本节需实现的是多通道的信号发生器。而此处的ADDA模块最多支持四通道,这里就要轮询控制各个通道并输入相应的控制字。将此模块命名为DAC_4CH,其模块接口示意图如图10所示。

图片24

图10 DAC通道选择模块接口示意图

其每个接口功能描述如表3所示。

图片25

表3 DAC通道选择模块接口功能描述

由以上表分析可知其端口列表如下:

    input Clk;
    input Rst_n;
    input [7:0]DATA_A,DATA_B,DATA_C,DATA_D;
    input [1:0]CH_SUM;  

    output TLC5620_CLK;     
    output TLC5620_DATA;    
    output TLC5620_LOAD;    
    output TLC5620_LDAC;

这里在调用TLC5620时,直接将其转换使能信号UpdateDone信号置1来简化控制。

    TLC5620_CTRL TLC5620_CTRL0(
        .Clk(Clk),
        .Rst_n(Rst_n),
        .UpdateReq(1'b1),
        .CtrlWord(CtrlWord),

        .UpdateDone(UpdateDone),
        .TLC5620_CLK(TLC5620_CLK),
        .TLC5620_DATA(TLC5620_DATA),
        .TLC5620_LOAD(TLC5620_LOAD),
        .TLC5620_LDAC(TLC5620_LDAC)
    );

实现轮询,就是在时钟上升沿到来时,每当一次TLC5620转换完成后,就开始选通下一个通道。

    wire UpdateDone; 
    
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        Current_CH <= 0;
    else if(UpdateDone)begin
        if(Current_CH < CH_SUM)
            Current_CH <= Current_CH + 1'b1;
        else
            Current_CH <= 0;
    end
    else
        Current_CH <= Current_CH;

在选通对应通道后,DDS产生的数据只需给控制字即可。

reg [7:0]DATA;      

    always@(*)
    begin
        case(Current_CH)
            0: DATA = DATA_A;
            1: DATA = DATA_B;
            2: DATA = DATA_C;
            3: DATA = DATA_D;
            default:DATA = DATA_A;
        endcase 
    end

由基础课程可知TLC5620控制字的构成是两位的通道选择字、电压增益选择字以及八位数据位。因此直接采取位拼接方式即可。

wire [10:0]CtrlWord;  
 
  assign CtrlWord = {Current_CH,1'b0,DATA};

串口命令接收与解析

这里使用的串口接收模块波特率为9600,具体实现方式在基础课程的第12讲已经阐述,此处不再详细解释。其模块接口示意图如图11所示,接口对应功能描述如表4所示。

 

图片31

图11 串口接收模块接口示意图

图片32

表4 串口接收模块接口功能描述

信号发生器之自定义帧

由前面的实验目的可知,要实现利用串口来控制波形的参数。一般来讲当然可以通过一次发送一个数据来进行控制,但是由于这里需要的数据量太多因而这不是一个很好的解决办法。

在通信行业,存在各种符合一定标准的通信协议,里面会定义数据传输的帧格式以及时许要求。有时候也会有自己定义的一些数据帧格式。

这里采用自定义帧的方式来进行数据传输。其中串口发送数据的格式如表5所示。其中一帧数据为8个字节,整体数据由帧头+地址+数据+帧尾的格式组成。这样就可以实现对固定的寄存器地址写入不同的数据。

图片33

表5 自定义帧数据格式

不同的寄存器地址对应不同通道的不同的被控制数。系统设计了12个实体寄存器分别存储每个通道的相关控制参数。12个寄存器分为4组,分别对应了4个通道。每组包含3个寄存器,频率控制字寄存器,相位控制字寄存器以及幅度控制字寄存器。具体的各实体控制寄存器地址与对应功能如表6所示。当想控制某个通道的信号参数时,直接写实体寄存器,数据就会立即更新的控制参数到对应通道上

图片34

表6 实体寄存器地址与功能对应表

现在举例说明如何使用实体寄存器控制对应控制字。

1.使用实体寄存器控制某通道信号频率:

例如:设置通道0的频率为100Hz

地址:0x10

控制字:图片35

8590换算成16进制就是0x218E。因此数据段为8E 21 00 00

这样最终发送的指令为:图片36

2.使用实体寄存器控制某通道信号相位:

例如:设置通道1的相位为90度

地址:0x15

控制字:图片37

1024换算成16进制就是0x400。因此数据段为00 04 00 00

这样最终发送的指令为:图片38

3.使用实体寄存器控制某通道信号幅度:

系统设置支持8级信号幅度调节,此8级幅度的信号控制字与满幅的对应关系如下表7所示。

图片39

表7 控制字与幅度关系

需要注意的是,虽然系统设计支持8级调幅,然而缩减越大,则信号失真越严重,实际有效的缩减建议不超过1/8。

例如:设置通道2的幅度为半幅

地址:0x1a

控制字:01 因此数据段为01 00 00 00

这样最终发送的指令为图片40

另外,为了实现同时控制所有通道同时更新,特为以上所有实体寄存器设置了影子寄存器。当需要设置某通道的数据,但并不立即执行时,可使用影子寄存器功能。影子寄存器会预先存储所有的设置数据,但并不立即更新到每个模块,而是在发出更新指令后,系统同时更新所有的通道。在操作时只需要先将要更新的数据逐次写入每个通道需要更新的寄存器中的影子寄存器中,然后发出更新指令,则系统会自动将影子寄存器中的数据同时更到实体寄存器。更新指令由更新寄存器完成,更新寄存器是一个特殊的寄存器,占一个地址,对该地址写任意数据都能执行更新影子寄存器中的数据到对应实体寄存器中。影子寄存器地址、名称与功能对应表如表8所示。

图片41

表8 影子寄存器名称与功能对应表

更新寄存器以及通道控制地址与功能对应关系,如表9所示

图片42

表9 更新寄存器以及通道控制地址与功能对应表

在使用影子寄存器更新多个通道多个寄存器的操作顺序如下:

1. 写一个或多个通道中一个或多个影子寄存器的值

2. 发出更新指令

例如,需要设置通道0的频率为100Hz,相位为90度,幅度为满幅、设置通道1的频率为100Hz,相位为180度,幅度为半幅。则可按照如下指令顺序进行发送:

1. AA 03 00 8E 21 00 00 88 (设置通道0的频率为100Hz)

2. AA 03 04 00 04 00 00 88 (设置通道0的相位为90度)

3. AA 03 08 00 00 00 00 88 (设置通道0的幅度为满幅)

4. AA 03 01 8E 21 00 00 88 (设置通道1的频率为100Hz)

5. AA 03 05 00 08 00 00 88 (设置通道1的相位为180度)

6. AA 03 09 01 00 00 00 88 (设置通道1的幅度为半幅)

7. AA 03 0d 00 00 00 00 88 (更新所有影子寄存器中的数据到实体寄存器)

地址0c为控制4个通道的运行和停止的寄存器。该寄存器占4位,每一位对应一个通道的开关。当需要同步某几个通道时,也可使用这个寄存器来实现,实现方式为首先设置这几个通道的寄存器对应的控制位为0,然后重新设置这几个通道的寄存器位为1,即可实现同步。

串口数据帧接收

使用前面定义的数据帧格式,就需要一次发送多个字节的数据,这样就不排除多个字节发送错误或者多发、少发。因此就需要除了判断每一帧中的数据个数以及帧头、帧尾与定义一致以外还需检查每一字节数据间隔,是否间隔太久,如果间隔太久就说明这帧数据结束。

这里新建uart_rx_frameend.v,来进行帧格式判断。其模块接口示意图如图12所示。

 

图片43

图12 帧判断模块接口示意图

帧判断模块的接口功能描述如表10所示。

图片44

表10 帧判断模块接口功能描述

为了实现对一帧数据的判断,首先产生内部参考时钟。这里为了扩展模块的使用范围,增加了mode信号来选择参考时钟是内部产生的1K频率还是其他频率。此处的频率需要根据数据的传输速率进行相对应设置。

    reg [15:0]internal_base_cnt;
    reg internal_base_clk;
    wire base_clk;
    
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        internal_base_cnt <= 16'd0;
    else if(internal_base_cnt == 49_999)
        internal_base_cnt <= 16'd0;
    else
        internal_base_cnt <= internal_base_cnt + 1'd1;
        
    assign base_clk = mode?clk_cnt_base:internal_base_clk;
        
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        internal_base_clk <= 1'd0;
    else if(internal_base_cnt == 49_999)
        internal_base_clk <= 1'd1;
    else
        internal_base_clk <= 1'd0; 

串口数据标志接收部分,每当接收到一字节的数据后均会产生一个时钟周期的高电平标志信号。此处以cnt_state为标志来定义状态进而使能计数器。每当接收到1字节数据即开始计数,当帧超时则停止计数。

    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        cnt_state <= 1'b0;
    else if(Rx_int)
        cnt_state <= 1'd1;
    else if(frameend)
        cnt_state <= 1'b0;

计数器使能时每当到来一个参考时钟就将计数器加一。当接收到新的字节数据时或者当达到预设的帧结束判定时间就将计数器清零。且当到达帧结束判定时间时产生一帧数据接收完成标志信号。

    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        cnt <= 16'd0;
    else if(Rx_int || (base_clk && (cnt == endtimeset)))
        cnt <= 16'd0;
    else if(base_clk && cnt_state)
        cnt <= cnt + 16'd1;        
        
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        frameend <= 1'b0;
    else if(base_clk && (cnt == endtimeset))
        frameend <= 1'b1;
    else
        frameend <= 1'b0;

为了简化测试激励,这里先使用外部时钟进行功能仿真。因此在激励文件中例化待测试文件如下所示。

    uart_rx_frameend uart_rx_frameend(
        .Clk(Clk),
        .Rst_n(Rst_n),
        .clk_cnt_base(clk_cnt_base),    
        .mode(1),
        .Rx_int(Rx_int),    
        .endtimeset(endtimeset), 
        .frameend(frameend) 
    );

激励文件中除产生正常的系统时钟50MHz以外,还产生了4.5MHz的外部时钟。

    initial clk_cnt_base = 0;
    always begin
        clk_cnt_base = 1;
        #20;
        clk_cnt_base = 0;
        #200;
    end

循环产生接收到的数据标志信号36次,且最后一个个延迟足够长的时间。这里也就是用时钟clk_cnt_base计数20以上即可(大约5000ns)。

    integer i;
    
    initial begin
        Rst_n = 0;
        endtimeset = 19;
        Rx_int = 0;
        #201;
        Rst_n = 1;
        #200;
        for(i=0;i<36;i=i+1)begin
            Rx_int = 1;
            #20;
            Rx_int = 0;
            #2000;
        end
        #200000;
        for(i=0;i<36;i=i+1)begin
            Rx_int = 1;
            #20;
            Rx_int = 0;
            #2000;
        end
        #200000;
        $stop;    
    end

设置好仿真脚本以后,开始运行,大致在图13-1可以看出Rx_int信号产生正常,且计数使能信号正常,在一次帧数据传输结束后经过一定时间后frameend有一个标志信号。现在放大部分数据,在图13-2查看此时的cnt信号可以看出 19时产生信号,符合预期设计。

图片51

图13-1 帧判断模块功能仿真波形

图片52

图13-2 帧判断模块部分波形

串口数据解析

由上面自定义了数据格式,总共为8个8位数据,因此先用移位寄存器把八个数据移入进来。串口数据解析模块接口示意图如图14所示。

图片53

图14 CMD模块接口示意图

因此其端口名称及功能描述如表11所示。

图片54

表9 模块接口及功能描述

首先利用移位寄存器将接收到的数据放到一个寄存器中。这里定义了一个清零端,其作用是在一帧数据接收完成后将计数器以及移位寄存器数据清零。

    reg [63:0]shift_data;
    reg clr;
    
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        shift_data <= 64'd0;
    else if(clr)
        shift_data <= 64'd0;
    else if(Rx_Int)
        shift_data <= {Rx_Byte,shift_data[63:8]};

对接收到的数据个数进行计数,直到产生清零信号。

    reg [3:0]data_cnt;
    
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)
        data_cnt <= 4'd0;
    else if(clr)
        data_cnt <= 4'd0;
    else if(Rx_Int)
        data_cnt <= data_cnt + 1'b1;

进行数据接收解析,即把对应位送给对应的控制字。

    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)begin 
        Fword0 <= 0; Fword1 <= 0; Fword2 <= 0; Fword3 <= 0;     
        Pword0 <= 0; Pword1 <= 0; Pword2 <= 0; Pword3 <= 0;
        Aword0 <= 0; Aword1 <= 0; Aword2 <= 0; Aword3 <= 0;
        
        s_Fword0 <= 0; s_Fword1 <= 0; s_Fword2 <= 0; s_Fword3 <= 0;
        s_Pword0 <= 0; s_Pword1 <= 0; s_Pword2 <= 0; s_Pword3 <= 0;
        s_Aword0 <= 0; s_Aword1 <= 0; s_Aword2 <= 0; s_Aword3 <= 0;
        
        CH_Sync <= 4'b0000;
        clr <= 1'b0;
end
else if(frameend)begin
。。。。。。。。。。。。。。。
    end

    else
        clr <= 1'b0;       

每当frameend变为高电平,即代表一次数据传输结束。注意此处一次数据传输结束,可能是正常的一帧数据,也可能是不正常的数据接收。这样就需要对此次接受的数据进行判断是否为正常的一帧数据。这里定义的正常一帧数据如表5所示,数据个数为8且帧头为’haa、’h03,帧尾分别为’h88。这样判断正确后才认为是正常的数据,这样即可以根据寄存器地址来进行相对应的操作。一次传输结束后就将计数器清零等待下一次传输。

    else if(frameend)begin
        clr <= 1'b1;
        if(data_cnt == 8 && shift_data[7:0] == Header0 && shift_data[15:8] == Header1 && shift_data[63:56] == Tail)begin
            case(shift_data[23:16])
                8'h10:Fword0 <= shift_data[55:24];
                8'h11:Fword1 <= shift_data[55:24];
                8'h12:Fword2 <= shift_data[55:24];
                8'h13:Fword3 <= shift_data[55:24];
                
                8'h14:Pword0 <= shift_data[35:24];
                8'h15:Pword1 <= shift_data[35:24];
                8'h16:Pword2 <= shift_data[35:24];
                8'h17:Pword3 <= shift_data[35:24];
                
                8'h18:Aword0 <= shift_data[27:24];
                8'h19:Aword1 <= shift_data[27:24];
                8'h1a:Aword2 <= shift_data[27:24];
                8'h1b:Aword3 <= shift_data[27:24];
                
                8'h00:s_Fword0 <= shift_data[55:24];
                8'h01:s_Fword1 <= shift_data[55:24];
                8'h02:s_Fword2 <= shift_data[55:24];
                8'h03:s_Fword3 <= shift_data[55:24];

                8'h04:s_Pword0 <= shift_data[35:24];
                8'h05:s_Pword1 <= shift_data[35:24];
                8'h06:s_Pword2 <= shift_data[35:24];
                8'h07:s_Pword3 <= shift_data[35:24];

                8'h08:s_Aword0 <= shift_data[27:24];
                8'h09:s_Aword1 <= shift_data[27:24];
                8'h0a:s_Aword2 <= shift_data[27:24];
                8'h0b:s_Aword3 <= shift_data[27:24];
                
                8'h0c:CH_Sync <= shift_data[27:24];
                
                8'h0d:begin             
                    Fword0 <= s_Fword0;  Fword1 <= s_Fword1;
                    Fword2 <= s_Fword2;  Fword3 <= s_Fword3;
                                
                    Pword0 <= s_Pword0;  Pword1 <= s_Pword1;
                    Pword2 <= s_Pword2;  Pword3 <= s_Pword3;
                                
                    Aword0 <= s_Aword0;  Aword1 <= s_Aword1;
                    Aword2 <= s_Aword2;  Aword3 <= s_Aword3;             
                end
                default:;
            endcase
        end
    end

在调用写好的一次数据传输结束模块时,使用内部的1K时钟,且将帧结束判定时间设置为9。这里的时钟速度选择以及判定时间设置需根据不同的情况采用不同的设置。

    uart_rx_frameend uart_rx_frameend(
        .Clk(Clk),
        .Rst_n(Rst_n),
        .clk_cnt_base(1'b0),    
        .mode(1'b0),
        .Rx_int(Rx_Int),    
        .endtimeset(4'd9), 
        .frameend(frameend)
    );

由待测试文件可以看出如果产生帧结束标志信号会至少经过9/1000s,这里激励文件发送一次正常的数据为AA 03 10 8E 21 00 00 88然后延时0.01s,这样0.01s大于0.009s,再发送83 00数据后再延迟0.01s。具体实现如下所示。这里也故意使每一次数据持续时间不同。

    initial begin
        Rst_n = 1'b0;
        Rx_Byte = 8'd0;
        Rx_Int = 1'b0;
        #(`clk_period*20 + 1 )
        Rst_n = 1'b1;
        
        #(`clk_period*50);
        Rx_Byte = 8'haa;
        #`clk_period;
        Rx_Int = 1'b1;  
        #`clk_period;
        Rx_Int = 1'd0;
        
        #(`clk_period*5000);
        Rx_Byte = 8'h03;
        #`clk_period;
        Rx_Int = 1'b1;  
        #`clk_period;
        Rx_Int = 1'd0;
        
        #(`clk_period*1500);
        Rx_Byte = 8'h10;
        #`clk_period;
        Rx_Int = 1'b1;  
        #`clk_period;
        Rx_Int = 1'd0;
        
        #(`clk_period*500);
        Rx_Byte = 8'h8e;
        #`clk_period;
        Rx_Int = 1'b1;  
        #`clk_period;
        Rx_Int = 1'd0;
        
        #(`clk_period*500);
        Rx_Byte = 8'h21;
        #`clk_period;
        Rx_Int = 1'b1;  
        #`clk_period;
        Rx_Int = 1'd0;
        
        #(`clk_period*500);
        Rx_Byte = 8'h00;
        #`clk_period;
        Rx_Int = 1'b1;  
        #`clk_period;
        Rx_Int = 1'd0;
        
        #(`clk_period*500);
        Rx_Byte = 8'h00;
        #`clk_period;
        Rx_Int = 1'b1;  
        #`clk_period;
        Rx_Int = 1'd0;
        
        #(`clk_period*500);
        Rx_Byte = 8'h88;
        #`clk_period;
        Rx_Int = 1'b1;  
        #`clk_period;
        Rx_Int = 1'd0;
        
        #(`clk_period*500000);
    //////////////////////////  
        #(`clk_period*1000);
        Rx_Byte = 8'h83;
        #`clk_period;
        Rx_Int = 1'b1;  
        #`clk_period;
        Rx_Int = 1'd0;
        
        #(`clk_period*15000);
        Rx_Byte = 8'h00;
        #`clk_period;
        Rx_Int = 1'b1;  
        #`clk_period;
        Rx_Int = 1'd0;
        
        #(`clk_period*500000);
        $stop;  
    end

编译无误后设置好仿真脚本,可以看到如图15所示的现象,每当接收一个字节数据时Rx_Int信号均会有一个系统周期的高电平,且数据计数器cnt能够正常的自加及清零。8byte数据接收过程中移位寄存器shift_data工作正常,且当完成后会控制字Fword0操作正常。当一帧数据接收完成后,再次发送一字节数据时cnt又开始重新计数。

图片61

图15 串口接收即命令解析模块功能仿真波形

如果想看到一次数据发送中data_cnt以及shift_data细节,则可以增加base_clk频率以及修改计数器,可以看到如图16的结果,可以自行分析。这里修改频率的同时也修改了计数器值,从仿真波形中看出同样可以进行帧结束位的判定。

图片62

图16 模块部分仿真波形

信号发生器顶层设计

综合以上的分析,可以得出其中模块顶层如下图17所示。将内部信号全部定义为wire即可,其接口功能描述如表10所示。

图片63

图18 顶层模块接口示意图

图片64

表10 顶层模块接口及功能描述

其输入输出端口定义如下所示。

    input Clk;
    input Rst_n;
    input Rs232_Rx;
    output TLC5620_CLK;
    output TLC5620_DATA;
    output TLC5620_LOAD;
    output TLC5620_LDAC;

将DA通道全部选通,并进行幅度调制的运算。

 wire [2:0]CH_SUM;    
    assign CH_SUM = 4-1;
    
    wire [3:0]Aword0,Aword1,Aword2,Aword3;  
    wire [9:0]DA_Data0,DA_Data1,DA_Data2,DA_Data3;  
wire [7:0]DATA_A,DATA_B,DATA_C,DATA_D; 
 
    assign DATA_A = DA_Data0[9:2] >> Aword0;
    assign DATA_B = DA_Data1[9:2] >> Aword1;
    assign DATA_C = DA_Data2[9:2] >> Aword2;
    assign DATA_D = DA_Data3[9:2] >> Aword3;

根据实际使用情况将各个模块例化进来,例化后的顶层应为如图19所示。

 

图片67

图19 顶层文件RTL Viewer视图

如有更多问题,欢迎加入芯航线 FPGA 技术支持群交流学习:472607506

小梅哥

芯航线电子工作室

关于学习资料,小梅哥系列所有能够开放的资料和更新(包括视频教程,程序代码,教程文档,工具软件,开发板资料)都会发布在我的云分享。(记得订阅)链接:http://yun.baidu.com/share/home?uk=402885837&view=share#category/type=0

赠送芯航线AC6102型开发板配套资料预览版下载链接:链接:http://pan.baidu.com/s/1slW2Ojj 密码:9fn3

赠送SOPC公开课链接和FPGA进阶视频教程。链接:http://pan.baidu.com/s/1bEzaFW 密码:rsyh

posted @ 2017-02-20 11:30  小梅哥  阅读(7219)  评论(1编辑  收藏  举报