侧边栏壁纸
    • 累计撰写 296 篇文章
    • 累计收到 520 条评论
    【FPGA】【SPI】线性序列机与串行接口 DAC 驱动设计与验证
    我的学记|刘航宇的博客

    【FPGA】【SPI】线性序列机与串行接口 DAC 驱动设计与验证

    刘航宇
    2022-12-15 / 0 评论 / 550 阅读 / 正在检测是否收录...

    概述:ADC和DAC是FPGA与外部信号的接口,从数据接口类型的角度划分,有低速的串行接口和高速的并行接口。FPGA经常用来采集中高频信号,因此使用并行ADC和DAC居多。并行接口包括两种数字编码方式:带符号数signed与无符号数unsigned。

    DAC

    DA转化一般将数字信号“010101”转化为模拟信号去控制其它电子设备。
    导读:
    数模转换器即 D/A 转换器,或简称 DAC(Digital to Analog Conver),是指将数字信号转变为模拟信号的电子元件。
    DAC 的内部电路构成无太大差异,一般按输出是电流还是电压、能否作乘法运算等进行分类。大多数 DAC 由电阻阵列和 n 个电流开关(或电压开关)构成,按数字输入值切换开关,产生比例于输入的电流(或电压) 。此外,也有为了改善精度而把恒流源放入器件内部的。
    DAC 可分为电压型和电流型两大类,电压型 DAC 有权电阻网络、T 型电阻网络和树形开关网络等;电流型 DAC 有权电流型电阻网络和倒 T 型电阻网络等。
    电压输出型(如 TLV5618) 。电压输出型 DAC 虽有直接从电阻阵列输出电压的,但一般采用内置输出放大器以低阻抗输出。直接输出电压的器件仅用于高阻抗负载,由于无输出放大器部分的延迟,故常作为高速 DAC 使用。
    电流输出型(如 THS5661A ) 。电流输出型 DAC 很少直接利用电流输出,大多外接电流- 电压转换电路得到电压输出,后者有两种方法:一是只在输出引脚上接负载电阻而进行电流- 电压转换,二是外接运算放大器。
    乘算型(如 AD7533) 。DAC 中有使用恒定基准电压的,也有在基准电压输入上加交流信号的,后者由于能得到数字输入和基准电压输入相乘的结果而输出,因而称为乘算型 DAC。
    乘算型 DAC 一般不仅可以进行乘法运算,而且可以作为使输入信号数字化地衰减的衰减器及对输入信号进行调制的调制器使用。
    一位 DAC。一位 DAC 与前述转换方式全然不同,它将数字值转换为脉冲宽度调制或频率调制的输出,然后用数字滤波器作平均化而得到一般的电压输出,用于音频等场合。
    本章以 TLV5618 为例介绍 DAC 的工作原理及时序图解释,并用线性序列机(LSM)来描述时序图进而正确驱动此类设备。在 Quartus Pime 软件中,使用 ISSP 工具输入希望输出的电压值,控制 FPGA 进而操作 TLV5618 芯片输出对应的电压值。

    任务

    使用FPGA芯片控制DAC采集芯片,输出指定的电压值。

    硬件部分

    为了将FPGA输出的数字电压转换成模拟电压,使用到了数模转换芯片(简称DAC)TLV5618。进行设计前, 需要查看该芯片的数据手册
    1.芯片功能图
    本章使用的 DAC 芯片为 TLV5618,其芯片内部结构如图所示。TLV5618 是一个基于电压输出型的双通道 12 位单电源数模转换器,其由串行接口、一个速度和电源控制器、电阻网络、轨到轨输出缓冲器组成。

    TLV5618 使用 CMOS 电平兼容的三线制串行总线与各种处理器进行连接,接收控制器发送的 16 位的控制字,这 16 位的控制字被分为 2 个部分,包括 4 位的编程位,12 位的数据位。
    2.端口功能表

    从功能图和功能表中我们可以看出,TLV5618有四个输入端口:
    片选信号CS、数据串行输入端口DIN、模拟参考电压REF、数字时钟SCLK。
    两个输出端分别为OUTA和OUTB,均为对应的模拟电压输出端。
    3.时序图
    当片选(CS)信号为低电平时,输入数据以最高有效位在前的方式被读入 16 位移位寄存器。在 SCLK 输入信号的下降沿,把数据移入寄存器 A、B。当片选(CS)信号进入上升沿时,再把数据送至 12 位 A/D 转换器。

    从时序图中我们可以看到使用该芯片时要注意这几个参数:
    tw(L):低电平最小宽度,25ns。
    tw(H):高电平最小宽度,25ns。
    tsu(D):数据最短建立时间。
    th(D):数据最短保持时间。
    tsu(CS-CK):片选信号下降沿到第一个时钟下降沿最短时间。
    th(CSH):片选信号最短拉高时间。
    TLV5618 的 16 位数据格式如下:

    其中,SPD 为速度控制位,PWR 为电源控制位。上电时,SPD 和 PWR 复位到 0(低速模式和正常工作)。

    R1 与 R0 所有可能的组合以及代表的含义如下所示。如果其中一个寄存器或者缓冲区被选择,那么 12 位数据将决定新的 DAC 输出电压值。

    这样针对 D[15:12]不同组合构成的典型操作如下:
    1)设置 DAC A 输出,选择快速模式:写新的 DAC A 的值,更新 DAC A 输出。DAC A 的输出在 D0 后的时钟上升沿更新。

    2)设置 DAC B 输出,选择快速模式: 写新的 DAC B 的值到缓冲区,并且更新 DAC B 输出。DAC B 的输出在 D0 后的时钟上升沿更新。

    3) 设置 DAC A、DAC B 的值,选择低速模式:在写 DAC A 的数据 D0 后的时钟上升沿 DAC A 和 B 同时更新输出。
    a.写 DAC B 的数据到缓冲区:

    b.写新的 DAC A 的值并且同时更新 DAC A 和 B:

    4) 设置掉电模式:×=不关心

    4.输出电压计算


    由手册给出的公式知,输出电压与输入的编码值成正比,同时还要乘以一个系数REF,这个系数从芯片的REF引脚输入。我们打开并查看开发板的原理图:其中参考电压为由 LM4040 提供的 2.048V,与FPGA 采用三线制 SPI 通信。

    从图中知,我们用到了芯片LM4040-2.0给DAC供电,这个芯片工作时输出电压为4.028V(即精度为12位),故参数REF为4.028。
    5.时钟频率与刷新率计算

    我们查阅手册后知道,使用该芯片时,时钟最大频率为20MHz,刷新率为时钟频率的1/16。而开发板提供的原始时钟为50MHz,因此可以采用四分频后得到12.5MHz的时钟频率。

    线性序列机设计思想与接口时序设计

    从图 24.3 中可以看出,该接口的时序是一个很有规律的序列,SCLK 信号什么时候该由变高,什么时候由高变低。DIN 信号什么时候该传输哪一位数据,都是可以根据时序参数唯一确定下来的。
    这样就可以将该数据波形放到以时间为横轴的一个二维坐标系中,纵轴就是每个信号对应的状态:

    因此只需要在逻辑中使用一个计数器来计数,然后每个计数值时就相当于在 t 轴上对应了一个相应的时间点,那么在这个时间点上,各个信号需要进行什么操作,直接赋值即可。
    经查阅手册可知器件工作频率SCLK最大为20MHz,这里定义其工作频率为12.5MHz。设置一个两倍于 SCLK 的采样时钟 SCLK2X,使用 50M 系统时钟二分频而来即 SCLK2X 为25MHz。针对 SCLK2X 进行计数来确定图 24.4 中各个信号的状态。可得出每个时间点对应信号操作详表。


    线性序列机计数器的控制逻辑判断依据,如表 24.7 所示。

    以上就是通过线性序列机设计接口时序的一个典型案例,可以看到,线性序列机可以大大简化设计思路。线性序列机的设计思想就是使用一个计数器不断计数,由于每个计数值都会对应一个时间,那么当该时间符合需要操作信号的时刻时,就对该信号进行操作。这样,就能够轻松的设计出各种时序接口了。

    基于线性序列机的 DAC 驱动设计

    模块接口设计
    设计 TLV5618 接口逻辑的模块如图 24.8 所示。

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

    生成使能信号,当输入使能信号有效后便将使能信号 en 置 1,当转换完成信号有效时便将其重新置 0。

     reg en;//转换使能信号 
     always@(posedge Clk or negedge Rst_n)
     if(!Rst_n)
     en <= 1'b0;
     else if(Start)
     en <= 1'b1;
     else if(Set_Done)
     en <= 1'b0;
     else
     en <= en;

    在数据手册中SCLK的频率范围为0.8~3.2MHz。这里为了方便适配不同的频率需求率,设置了一个可调的计数器,改变 DIV_PARAM 的值即可改变 DAC 工作频率。根据表 中可以看出,需要根据计数器的值周期性的产生 SCLK 时钟信号,这里可以将计数器的值等倍数放大,形成过采样。这里产生一个两倍于 SCLK 的信号,命名为 SCLK2X。
    首先编写分频计数器,时钟 SCLK2X 的计数器。

    //生成 2 倍 SCLK 使能时钟计数器
     reg [7:0]DIV_CNT;//分频计数器 
     always@(posedge Clk or negedge Rst_n)
     if(!Rst_n)
     DIV_CNT <= 4'd0;
     else if(en)begin
     if(DIV_CNT == (DIV_PARAM - 1'b1))//2-1=1,cnt=0,25MHZ,cnt=1为12.5MHZ
     DIV_CNT <= 4'd0;
     else
     DIV_CNT <= DIV_CNT + 1'b1;
     end else 
     DIV_CNT <= 4'd0;

    根据使能信号以及计数器状态生成 SCLK2X 时钟。

    //生成 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;

    每当使能转换后,对 SCLK2X 时钟进行计数。

     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'd32)
     SCLK_GEN_CNT <= 6'd0;
     else
     SCLK_GEN_CNT <= SCLK_GEN_CNT + 1'd1;
     end else
     SCLK_GEN_CNT <= SCLK_GEN_CNT;

    根据 SCLK2X 计数器的值来确认工作状态以及数据传输进程。

     //依次将数据移出到 DAC 芯片 
     always@(posedge Clk or negedge Rst_n)
     if(!Rst_n)begin
     DIN <= 1'b1;
     SCLK <= 1'b0;
     r_DAC_DATA <= 16'd0;
     end else begin
     if(Start)//收到开始发送命令时,寄存 DAC_DATA 值 
     r_DAC_DATA <= DAC_DATA;
     if(!Set_Done && SCLK2X) begin
     if(!SCLK_GEN_CNT[0])begin //偶数,
    0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30:
     SCLK <= 1'b1;
     DIN <= r_DAC_DATA[15];
     r_DAC_DATA <= #1 r_DAC_DATA << 1;
     end else //奇数,
    1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31:
     SCLK <= 1'b0;
     end
     end

    DAC 工作状态,处于数据传输状态时 CS_N 为低电平状态,空闲时为高。

     always@(posedge Clk or negedge Rst_n)
     if(!Rst_n)
     CS_N <= 1'b1;
     else if(en && SCLK2X)
     CS_N <= SCLK_GEN_CNT[5];
     else
     CS_N <= CS_N;

    一次转换结束的标志,即 SCLK_GEN_CNT[5] && SCLK2X,并产生一个高脉冲的转换完成标志信号 Set_Done。

    assign Set_Done = SCLK_GEN_CNT[5] && SCLK2X;

    仿真及板级测试
    这里仿真文件需输出几次并行数据,观测串行数据输出 DIN 的状态即可判断是否能驱动正常。这里输出四次数据分别为 C_AAAh、4_555h、1_555h、F_555h。部分代码如下,只写出了前两个数据,后面两个可直接复制修改即可。

     initial begin
     Rst_n = 0; Start = 0; DAC_DATA = 0; #201;
     Rst_n = 1; #200;
     
     DAC_DATA = 16'hC_AAA; Start = 1; #20;
     Start = 0;
     #200;
     wait(Set_Done);
     
     #20000; 
     DAC_DATA = 16'h4_555; Start = 1; #20;
     Start = 0;
     #200;
     wait(Set_Done);
     $stop;
     end

    开始仿真后,可看出人为控制 DAC_DATA 数据输入状态正常。

    放大第一个数据传输过程,可以看出正常 1100_1010_1010_1010b,计数器计数到'd32 符合设计要求,且每个传输过程中 CS_N 为低。传输完成后产生一个时钟周期的 Set_Done 标志信号。

    为了再次验证 TLV5618 驱动模块设计的正确性,使用 ISSP 在线调试工具。创建一个 ISSPIP 核,主要配置如图 24.7 所示。

    加入工程后新建顶层文件 DAC_test.v,并对 ISSP 以及设计好的 TLV5618 进行例化.
    在接口时序介绍中指出,TLV5618 有三种更新输出电压方式,下面分别测试这三种电压更新方式。上电复位后两通道输出电压初始值均为 0V。
    1.单独测试 A 通道,依次输入 CFFFh、C7FFh、C1FFh,理论输出电压值应为 4.096、2.048、0.512。可在通道 A 测量输出电压依次为 4.10、2.05、0.51,此时通道 B 电压一直保持 0,电压输出在误差允许范围内。
    2.单独测试 B 通道,依次输入 4FFFh、47FFh、4000h,可在通道 B 测量输出电压依次为4.10、2.05、0。此时通道 A 电压一直保持 0.51,电压输出在误差允许范围内。
    3.测量 AB 两通道同时更新,首先输入 1FFF 将数据写入通道 B 寄存器,再写入 8FFF 到通道 A 寄存器。这样可以测量出写完后会两个通道输出电压会同时变为 4.10。
    通过以上三组测试数据的,可以发现 DAC 芯片输出电压数据更新正常。
    这样就完成了一个 DAC 模块的设计与仿真验证,基于本讲以及 14 讲即可实现信号发
    生器,详细内容可以参考第五篇中的进阶课程 DDS2。

    设计工程


    我们考虑用FPGA设计一个DAC驱动,通过CS、sclk、din三根信号线与DAC芯片连接,设计输入端口Data[15:0]。同时为了便于与其他模块共同协作,我们加上了使能端口en和转换完成标志位Conv_done,这是FPGA设计时必须考虑的一点,对于复杂的驱动模块,这两个信号是不可或缺的。

    软件部分

    //驱动部分
    module tlv5618(
        Clk,
        Rst_n,
    
        DAC_DATA, //并行数据输入端
        Start,//开始标志位
        Set_Done,//完成标志位
    
        DAC_CS_N,//片选
        DAC_DIN,//串行数据送给ADC芯片
        DAC_SCLK,//工作时钟SCLK
        DAC_State//工作状态
    );
        parameter fCLK=50;//50MHZ时钟参数
        parameter DIV_PARAM=2;//分频参数
    
        input Clk;
        input Rst_n;
        input[15:0] DAC_DATA;
        input Start;
        output reg Set_Done;
    
        output reg DAC_CS_N;
        output reg DAC_DIN;
        output reg DAC_SCLK;
        output DAC_State;
    
        assign DAC_State=DAC_CS_N;//工作状态标志与片选信号相同
    
        reg [15:0] r_DAC_DATA;//DAC数据寄存器
        reg[3:0] DIV_CNT;//分频计数器
        reg SCLK2X;//2倍SCLK的采样时钟
    
        reg[5:0] SCLK_GEN_CNT;//SCLK生成暨序列机计数器
        
        reg en;
    
        wire trans_done;//转化序列完成标志信号
    
      always @(posedge Clk or negedge Rst_n) begin
        if(!Rst_n)
            en<=1'b0;
        else if(Start)
            en<=1'b1;
        else if(trans_done)
            en<=1'b0;//转换完成后将使能关闭
        else if(treans_done)
            en<=1'b0;//转换完成后将使能关闭
        else
            en<en;
      end
    
      //分频计数器
      always@(posedge Clk or negedge Rst_n)begin
        if(!Rst_n)
            DIV_CNT<=4'd0;
        else if(en)begin
            if(DIV_CNT==(DIV_PARAM-1'b1))//前面设置了分频系数为2,这里计数器能够容纳2拍时钟脉冲
                DIV_CNT<=4'b0;
            else
                DIV_CNT<=DIV_CNT+1'b1;
            end
        else
            DIV_CNT<=4'd0;
      end
    
    //二分频
    always@(posedge Clk or negedge Rst_n)begin
    if(!Rst_n)
        SCLK2X<=1'b0;
    else if(en && (DIV_CNT==(DIV_PARAM-1'b1)))
        SCLK2X<=1'b1;
    else
        SCLK2X<=1'b0;
    end
    
    //生成序列计数器,对SCLK脉冲进行计数
    always@(posedge Clk or negedge Rst_n)begin
    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;
    end
    
    always@(posedge Clk or negedge Rst_n)begin
    if(!Rst_n)
        r_DAC_DATA<=16'd0;
    else if(Start) //收到开始发送命令时候,寄存DAC_DATA值
        r_DAC_DATA<=DAC_DATA;
    else
        r_DAC_DATA<=r_DAC_DATA;
    end
    
    //依次将数据移出到DAC芯片
    always@(posedge Clk or negedge Rst_n)
    if(!Rst_n)begin
        DAC_DIN<=1'b1;
        DAC_SCLK<=1'b0;
        DAC_CS_N<=1'b1;
    end
    else if(!Set_Done && SCLK2X)begin
            case(SCLK_GEN_CNT)
                0:
                    begin    //高脉冲期间内,计数为0时了,打开片选使能,给予时钟上升沿,将最高位数据送给DAC芯片
                        DAC_CS_N <= 1'b0;
                        DAC_DIN <= r_DAC_DATA[15];
                        DAC_SCLK <= 1'b1;
                    end
            
                1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31:
                    begin
                        DAC_SCLK <= 1'b0;    //时钟低电平
                    end
                
                2:  begin DAC_DIN <= r_DAC_DATA[14]; DAC_SCLK <= 1'b1; end
                4:  begin DAC_DIN <= r_DAC_DATA[13]; DAC_SCLK <= 1'b1; end
                6:  begin DAC_DIN <= r_DAC_DATA[12]; DAC_SCLK <= 1'b1; end
                8:  begin DAC_DIN <= r_DAC_DATA[11]; DAC_SCLK <= 1'b1; end            
                10: begin DAC_DIN <= r_DAC_DATA[10]; DAC_SCLK <= 1'b1; end
                12: begin DAC_DIN <= r_DAC_DATA[9];  DAC_SCLK <= 1'b1; end
                14: begin DAC_DIN <= r_DAC_DATA[8];  DAC_SCLK <= 1'b1; end
                16: begin DAC_DIN <= r_DAC_DATA[7];  DAC_SCLK <= 1'b1; end    
                18: begin DAC_DIN <= r_DAC_DATA[6];  DAC_SCLK <= 1'b1; end
                20: begin DAC_DIN <= r_DAC_DATA[5];  DAC_SCLK <= 1'b1; end                
                22: begin DAC_DIN <= r_DAC_DATA[4];  DAC_SCLK <= 1'b1; end
                24: begin DAC_DIN <= r_DAC_DATA[3];  DAC_SCLK <= 1'b1; end
                26: begin DAC_DIN <= r_DAC_DATA[2];  DAC_SCLK <= 1'b1; end
                28: begin DAC_DIN <= r_DAC_DATA[1];  DAC_SCLK <= 1'b1; end            
                30: begin DAC_DIN <= r_DAC_DATA[0];  DAC_SCLK <= 1'b1; end
                
                32: DAC_SCLK <= 1'b1;     //时钟拉高
                33: DAC_CS_N <= 1'b1;    //关闭片选
                default:;
            endcase
    end
    
    assign trans_done = (SCLK_GEN_CNT == 33) && SCLK2X;
    
        always@(posedge Clk or negedge Rst_n)
        if(!Rst_n)
            Set_Done <= 1'b0;
        else if(trans_done)
            Set_Done <= 1'b1;
        else
            Set_Done <= 1'b0;
    
    endmodule
    //顶层模块
    module DAC_test(
                Clk,//模块时钟50M
                Rst_n,//模块复位
    
                DAC_CS_N,  //TLV5618的CS_N接口
                DAC_DIN,   //TLV5618的DIN接口
                DAC_SCLK   //TLV5618的SCLK接口
    );
        input Clk;
        input Rst_n;
        
        output DAC_CS_N;
        output DAC_DIN;
        output DAC_SCLK;
        
        reg Start;
        reg [15:0]r_DAC_DATA;
        wire DAC_State;
        wire [15:0]DAC_DATA;
        wire Set_Done;
    
        tlv5618 tlv5618(
            .Clk(Clk),
            .Rst_n(Rst_n),
            
            .DAC_DATA(DAC_DATA),
            .Start(Start),
            .Set_Done(Set_Done),
            
            .DAC_CS_N(DAC_CS_N),
            .DAC_DIN(DAC_DIN),
            .DAC_SCLK(DAC_SCLK),
            .DAC_State(DAC_State)
        );
    
        always@(posedge Clk or negedge Rst_n)
        if(!Rst_n)
            r_DAC_DATA <= 16'd0;
        else if(DAC_State)
            r_DAC_DATA <= DAC_DATA;
            
        always@(posedge Clk or negedge Rst_n)
        if(!Rst_n)
            Start <= 1'd0;
        else if(r_DAC_DATA != DAC_DATA) 
            Start <= 1'b1;
        else
            Start <= 1'd0;
    
    endmodule
    `timescale 1ns/1ns
    
    module tlv5618_tb();
    
        reg Clk;
        reg Rst_n;
        reg [15:0]DAC_DATA;
        reg Start;
        wire Set_Done;
        
        wire DAC_CS_N;
        wire DAC_DIN;
        wire DAC_SCLK;
        
        tlv5618 tlv5618(
            .Clk(Clk),
            .Rst_n(Rst_n),
            
            .DAC_DATA(DAC_DATA),
            .Start(Start),
            .Set_Done(Set_Done),
            
            .DAC_CS_N(DAC_CS_N),
            .DAC_DIN(DAC_DIN),
            .DAC_SCLK(DAC_SCLK),
            .DAC_State()
        );
    
        initial Clk = 1;
        always#10 Clk = ~Clk;
        
        initial begin
            Rst_n = 0;
            Start = 0;
            DAC_DATA = 0;
            #201;
            Rst_n = 1;
            #200;
            
            DAC_DATA = 16'hC_AAA;
            Start = 1;
            #20;
            Start = 0;
            #200;
            wait(Set_Done);
            
            #20000;
            
            DAC_DATA = 16'h4_555;
            Start = 1;
            #20;
            Start = 0;
            #200;
            wait(Set_Done);
            
            #20000;    
            
            DAC_DATA = 16'h1_555;
            Start = 1;
            #20;
            Start = 0;
            #200;
            wait(Set_Done);
            #20000;
            DAC_DATA = 16'hf_555;
            Start = 1;
            #20;
            Start = 0;
            #200;
            wait(Set_Done);        
            #20000;        
            $stop;
        end
        
    endmodule

    仿真

    1
    【FPGA】深度理解串口发送模块设计与验证
    « 上一篇 2022-12-18
    VLSI设计-基4 Booth乘法器前端与中端实现
    下一篇 » 2022-11-30

    评论 (0)

    取消