基于FPGA的AD7606并行接口驱动程序

作者:数字站

前文讲解了AD7606的功能及原理,本文通过FPGA实现AD7606的并行接口数据采集,对应时序如下所示。

回顾时序

上电后首先拉高RESET复位AD7606,每隔一段时间给CONSTA/CONSTB一个上升沿锁存八个模拟通道当前数据,AD7606转换锁存的数据,转换完成时会将BUSY信号拉低,FPGA检测BUSY下降沿,拉低片选CS开始读取转换后的数据。


图1 采样转换时序.JPG


将片选CS和读使能短接,生成读使能RD信号,AD7606在RD下降沿依次输出8个通道转换后的16位补码数据,FPGA在RD上升沿接收对应数据。图片


图2 并行接口时序.JPG


采集数据时序如上所示,后文是使用FPGA实现该接口时序,注意前文提到的各个时序参数,比如复位RESET高电平最短持续时间为50ns,复位RESET下降沿与CONVSTA/CONVSTB上升沿最少间隔25ns等。

FPGA实现思路

通过使用状态机嵌套两个计数器的方式实现该接口时序,状态转换图如下所示。

初始位于空闲状态,上电经过一段时间(确保模块也已经上电,最好不要FPGA复位后就直接复位AD7606)后跳转到复位状态RST,复位AD7606。
之后在空闲状态IDLE每经过5us(最大采样率200KSPS对应周期)会跳转到采样锁存状态CON,给AD7606提供一个CONVSTA/CONVSTB上升沿,CONVSTA/CONVSTB低电平最少持续25ns。



图3 状态转换图.JPG


然后跳转到BUSY状态,等待AD7606转换完成。当检测到BUSY下降沿时,状态机跳转到读取数据状态(DATA),FPGA产生片选CS和读使能RD,在RD的上升沿采集AD7606输出数据,当读取8个通道数据后,状态机回到空闲状态。
注意AD7606逻辑接口供电为3.3V,CS和RD的低电平持续时间必须大于21ns,高电平持续时间必须大于22ns。如果选择100MHz作为时钟信号,那么CS和RD的高低电平至少持续三个系统时钟周期。
复位(RST)状态、采样信号生成(CON)状态、读数据(DATA)状态会共用同一个计数器cnt,来计数该状态需要持续的时间。

代码实现

接口信号如下所示,data是采集到的16位补码数据,data_vld[7:0]用于指示data数据是哪个通道的有效数据。例如data_vld[0]为高电平时,表示data是通道一的有效数据。
module ad7606_drive #(
    parameter   FCLK    =   100_000_000     ,//系统时钟频率,单位Hz,默认100MHz;
    parameter   SMAPLE  =   200_000          //AD7606采样频率,单位Hz,默认200KHz;
)(
    input                   clk             ,//系统时钟,100MHz;
    input                   rst_n           ,//系统复位,低电平有效;

    input                   busy            ,//转换完成指示信号,下降沿有效;
    input                   frstdata        ,//指示采集到的第一个数据;
    input       [15 : 0]    adc_din         ,//AD7606所采集到的十六位数据信号;

    output  reg             cs      = 1'b1  ,//AD7606片选信号,读数据时拉低;
    output  reg             rd      = 1'b1  ,//AD7606读使能信号,读数据时拉低,下降沿时AD7606将数据发送到数据线上,上升沿时可以读出数据;
    output  reg             reset   = 1'b0  ,//AD7606复位信号,高电平有效,每次复位至少拉高50ns;
    output      [2 : 0]     os              ,//AD7606过采样模式信号,默认不使用过采样;
    output  reg             convst  = 1'b1  ,//AD7606采样启动信号,无效时高电平,采样计数器完成时拉低两个时钟;
    
    output  reg [15 : 0]    data    = 'd0   ,//AD7606采集到的数据.数据均为补码;
    output  reg [7 : 0]     data_vld= 'd0    //指示AD7606输出的数据来自哪个数据通道;
);

以下是状态机的跳转,与前面的状态转换图相对应。
//状态机次态到现态的转换;
    always@(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            state_c <= IDLE;
        end
        else begin
            state_c <= state_n;
        end
    end

    //状态机次态的跳转;
    always@(*)begin
        case(state_c)
            IDLE : begin
                if(end_delay_cnt)begin//延时计数器计数结束时
                    state_n = rst_flag ? CON : RST;//如果上电复位过,则直接采样数据,否则先进行复位;
                end
                else begin
                    state_n = state_c;
                end
            end
            RST : begin//该状态高电平至少持续50ns;复位低电平到convst高电平至少持续25ns。
                if(end_cnt)begin
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            CON : begin//该状态至少持续25ns;
                if(end_cnt)begin
                    state_n = BUSY;
                end
                else begin
                    state_n = state_c;
                end
            end
            BUSY : begin
                if(busy_neg)begin//检测到busy下降沿后,开始采集数据;
                    state_n = DATA;
                end
                else begin
                    state_n = state_c;
                end
            end
            DATA : begin//读数据状态的读使能高电平至少持续25ns,低电平至少持续32ns。
                if(end_cnt)begin
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            default : begin
                state_n = IDLE;
            end
        endcase
    end

下面是生成复位标志信号rst_flag,初始值为低电平,状态机处于复位状态结束时拉高,表示上电后已经对AD7606复位一次了。
//复位状态指示信号,高电平表示上电后完成过复位;
    always@(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            rst_flag <= 'd0;
        end
        else if(state_c == RST && end_cnt)begin
            rst_flag <= 1'b1;//复位完成时拉高,之后保持不变;
        end
    end

//复位状态指示信号,高电平表示上电后完成过复位;

延时计数器delay_cnt对时钟计数,每隔采样率对应周期后清零。状态机处于空闲状态时检测到该计数器计数结束,如果复位过AD7606(rst_flag为高电平),则状态机跳转到采样保存状态(CON),生成CONVSTA/B信号上升沿。
//延时计数器,初始值为0。
    always@(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            delay_cnt <= 'd0;
        end
        else if(end_delay_cnt)begin
            delay_cnt <= 'd0;
        end
        else begin
            delay_cnt <= delay_cnt + 'd1;
        end
    end

    //延时计数器计数到最大减2之后拉高,其余时间均拉低;
    always@(posedge clk)begin
        end_delay_cnt <= (delay_cnt == DIV_NUM - 'd2);
    end

复位信号RESET、采样保持信号CONVSTA/B的电平持续时间都有最短要求,因此需要计数器cnt计数系统时钟周期。同时可以用来计数状态机处于读数据状态时已经读取的并行数据个数。
//计数器cnt,加一条件处于采样,读数据,复位三个阶段,结束条件为计数cnt_num个时钟;
    always@(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            cnt <= 'd0;
        end
        else if(add_cnt)begin
            if(end_cnt)
                cnt <= 'd0;
            else
                cnt <= cnt + 'd1;
        end
    end

    assign add_cnt = (state_c==CON) || ((state_c==DATA) && (rdata_cnt == 'd6)) || (state_c==RST);       
    assign end_cnt = add_cnt && (cnt == cnt_num - 'd1);

计数器cnt的最大值取决于状态机的状态,CONVSTA/B低电平最少持续25ns,系统时钟周期10ns,此处取3个系统时钟,即30ns。复位RESET高电平最少持续50ns,此处取8个时钟,即80ns。每次需要读取8个通道的数据,因此读数据状态计数器最大值为8。
//cnt_num的值,采样阶段为3,读数据阶段为8,复位阶段为5(复位这里也使用8个时钟);
    always@(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            cnt_num <= 'd3;
        end
        else if(state_c == IDLE)begin
            cnt_num <= 'd3;
        end
        else if(state_c == BUSY)begin
            cnt_num <= 'd8;
        end
        else if(state_c == RST)begin
            cnt_num <= 'd8;
        end
    end

处于采样保持状态时拉低con,处于复位状态时拉高reset,如下所示。Busy是ad7606输出给FPGA的异步信号,因此先使用两级触发器同步信号,然后在检测下降沿作为状态机的判断条件。
always@(posedge clk)begin
        convst <= (state_c != CON);//采样触发信号,处于采样阶段时拉低,其余时间拉高;
        reset <= (state_c == RST);//复位电平最少持续50ns;
        busy_r <= {busy_r[1:0] , busy};//使用移位寄存器将采集的信号暂存;
        busy_neg <= busy_r[2] && (~busy_r[1]);//检测busy下降沿,表示数据是否完成采样;
    end

还需要一个计数器在读数据状态下计数片选CS和读使能RD的周期,当CS和RD短接(同时变化)时,逻辑接口3.3V供电,低电平持续最短32ns,高电平持续最短时间为22ns。此处低电平取4个时钟,高电平取3个时钟,因此计数器最大值为7-1,如下所示。
//计数器rdata_cnt,计数读数据时每读一个数据所需要的时钟,加一条件为处于读数据阶段,结束条件为计数6个时钟;
    always@(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            rdata_cnt <= 'd0;
        end
        else if(state_c == DATA)begin
            if(rdata_cnt == 7 - 1)
                rdata_cnt <= 'd0;
            else
                rdata_cnt <= rdata_cnt + 'd1;
        end
        else begin
            rdata_cnt <= 'd0;
        end
    end

生成片选CS和读使能信号,当计数器rd_cnt等于2时拉低,等于6时拉高,状态机不处于读数据状态时拉高。
//片选和读使能信号,当处于读数据阶段并且计数器cnt1计数大于2时拉低,其余时间拉高;
    always@(posedge clk)begin
        if(state_c == DATA)begin
            if(rdata_cnt == 6)begin//当计数器计数结束时拉高,高电平最少持续22ns;
                cs <= 1'b1;
                rd <= 1'b1;
            end
            else if(rdata_cnt == 2)begin//当计数器计数到2时拉低,低电平最好大于32ns;
                cs <= 1'b0;
                rd <= 1'b0;
            end
        end
        else begin//状态机处于其余状态时片选和读使能信号拉高;
            cs <= 1'b1;
            rd <= 1'b1;
        end
    end

因此计数器rd_cnt等于6表示读使能RD上升沿,此时依次读取AD76068个通道转换完成的16位补码数据,同时将输出数据有效指示信号对应位拉高。
//当读数据计数器计数结束时,根据计数器的数值读取对应通道数据;
    always@(posedge clk)begin
        if((state_c == DATA) && (rdata_cnt == 6) && add_cnt)begin
            data <= adc_din;
            data_vld <= (8'h01 << cnt);
        end
        else begin
            data <= data;
            data_vld <= 8'd0;
        end
    end

整个模块参考代码如下所示,后文进行仿真和上板测试(不是所有触发器都有复位,很多没有必要的复位可以去掉,进而减小复位信号扇出,在大项目中很有用):
//--###############################################################################################
//--#
//--# File Name    : ad7606_control
//--# Designer    : 数字站
//--# Tool      : Quartus 2018.1
//--# Design Date  : 2024.10.10
//--# Description  : ad7606接口驱动
//--# Version    : 0.0
//--# Coding scheme  : UTF-8(If the Chinese comment of the file is garbled, please do not save it and check whether the file is opened in GBK encoding mode)
//--#
//--###############################################################################################
module ad7606_drive #(
    parameter   FCLK    =   100_000_000     ,//系统时钟频率,单位Hz,默认100MHz;
    parameter   SMAPLE  =   200_000          //AD7606采样频率,单位Hz,默认200KHz;
)(
    input                   clk             ,//系统时钟,100MHz;
    input                   rst_n           ,//系统复位,低电平有效;

    input                   busy            ,//转换完成指示信号,下降沿有效;
    input                   frstdata        ,//指示采集到的第一个数据;
    input       [15 : 0]    adc_din         ,//AD7606所采集到的十六位数据信号;

    output  reg             cs      = 1'b1  ,//AD7606片选信号,读数据时拉低;
    output  reg             rd      = 1'b1  ,//AD7606读使能信号,读数据时拉低,下降沿时AD7606将数据发送到数据线上,上升沿时可以读出数据;
    output  reg             reset   = 1'b0  ,//AD7606复位信号,高电平有效,每次复位至少拉高50ns;
    output      [2 : 0]     os              ,//AD7606过采样模式信号,默认不使用过采样;
    output  reg             convst  = 1'b1  ,//AD7606采样启动信号,无效时高电平,采样计数器完成时拉低两个时钟;
    
    output  reg [15 : 0]    data    = 'd0   ,//AD7606采集到的数据.数据均为补码;
    output  reg [7 : 0]     data_vld= 'd0    //指示AD7606输出的数据来自哪个数据通道;
);
    localparam              IDLE    =  5'b00001 ;//空闲状态;
    localparam              CON     =  5'b00010 ;//采样状态;
    localparam              BUSY    =  5'b00100 ;//等待模数转换完成;
    localparam              DATA    =  5'b01000 ;//读数据状态;
    localparam              RST     =  5'b10000 ;//复位ADC状态;

    localparam  DIV_NUM     = FCLK / SMAPLE     ;//计算采样率对应时钟个数;
    localparam  DIV_NUM_W   = $clog2(DIV_NUM-1) ;//使用函数自动计算采样率对应位宽;

    reg         [4 : 0]     state_c             ;
    reg         [4 : 0]     state_n             ;
    reg         [3 : 0]     cnt                 ;
    reg         [3 : 0]     cnt_num             ;//
    reg                     busy_neg    = 1'b0  ;
    reg         [2 : 0]     busy_r      = 3'b111;
    reg         [2 : 0]     rdata_cnt           ;
    reg [DIV_NUM_W - 1 : 0] delay_cnt           ;
    reg                     end_delay_cnt = 'd0 ;
    reg                     rst_flag            ;
    
    wire                    add_cnt             ;
    wire                    end_cnt             ;

    assign os = 3'b000;//默认不使用过采样;

    //状态机次态到现态的转换;
    always@(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            state_c <= IDLE;
        end
        else begin
            state_c <= state_n;
        end
    end

    //状态机次态的跳转;
    always@(*)begin
        case(state_c)
            IDLE : begin
                if(end_delay_cnt)begin//延时计数器计数结束时
                    state_n = rst_flag ? CON : RST;//如果上电复位过,则直接采样数据,否则先进行复位;
                end
                else begin
                    state_n = state_c;
                end
            end
            RST : begin//该状态高电平至少持续50ns;复位低电平到convst高电平至少持续25ns。
                if(end_cnt)begin
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            CON : begin//该状态至少持续25ns;
                if(end_cnt)begin
                    state_n = BUSY;
                end
                else begin
                    state_n = state_c;
                end
            end
            BUSY : begin
                if(busy_neg)begin//检测到busy下降沿后,开始采集数据;
                    state_n = DATA;
                end
                else begin
                    state_n = state_c;
                end
            end
            DATA : begin//读数据状态的读使能高电平至少持续25ns,低电平至少持续32ns。
                if(end_cnt)begin
                    state_n = IDLE;
                end
                else begin
                    state_n = state_c;
                end
            end
            default : begin
                state_n = IDLE;
            end
        endcase
    end

    //复位状态指示信号,高电平表示上电后完成过复位;
    always@(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            rst_flag <= 'd0;
        end
        else if(state_c == RST && end_cnt)begin
            rst_flag <= 1'b1;//复位完成时拉高,之后保持不变;
        end
    end

    //延时计数器,初始值为0。
    always@(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            delay_cnt <= 'd0;
        end
        else if(end_delay_cnt)begin
            delay_cnt <= 'd0;
        end
        else begin
            delay_cnt <= delay_cnt + 'd1;
        end
    end

    //延时计数器计数到最大减2之后拉高,其余时间均拉低;
    always@(posedge clk)begin
        end_delay_cnt <= (delay_cnt == DIV_NUM - 'd2);
    end

    //计数器cnt,加一条件处于采样,读数据,复位三个阶段,结束条件为计数cnt_num个时钟;
    always@(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            cnt <= 'd0;
        end
        else if(add_cnt)begin
            if(end_cnt)
                cnt <= 'd0;
            else
                cnt <= cnt + 'd1;
        end
    end

    assign add_cnt = (state_c==CON) || ((state_c==DATA) && (rdata_cnt == 'd6)) || (state_c==RST);       
    assign end_cnt = add_cnt && (cnt == cnt_num - 'd1);

    //cnt_num的值,采样阶段为3,读数据阶段为8,复位阶段为5(复位这里也使用8个时钟);
    always@(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            cnt_num <= 'd3;
        end
        else if(state_c == IDLE)begin
            cnt_num <= 'd3;
        end
        else if(state_c == BUSY)begin
            cnt_num <= 'd8;
        end
        else if(state_c == RST)begin
            cnt_num <= 'd8;
        end
    end
    
    always@(posedge clk)begin
        convst <= (state_c != CON);//采样触发信号,处于采样阶段时拉低,其余时间拉高;
        reset <= (state_c == RST);//复位电平最少持续50ns;
        busy_r <= {busy_r[1:0] , busy};//使用移位寄存器将采集的信号暂存;
        busy_neg <= busy_r[2] && (~busy_r[1]);//检测busy下降沿,表示数据是否完成采样;
    end

    //计数器rdata_cnt,计数读数据时每读一个数据所需要的时钟,加一条件为处于读数据阶段,结束条件为计数6个时钟;
    always@(posedge clk or negedge rst_n)begin
        if(~rst_n)begin
            rdata_cnt <= 'd0;
        end
        else if(state_c == DATA)begin
            if(rdata_cnt == 7 - 1)
                rdata_cnt <= 'd0;
            else
                rdata_cnt <= rdata_cnt + 'd1;
        end
        else begin
            rdata_cnt <= 'd0;
        end
    end

    //片选和读使能信号,当处于读数据阶段并且计数器cnt1计数大于2时拉低,其余时间拉高;
    always@(posedge clk)begin
        if(state_c == DATA)begin
            if(rdata_cnt == 6)begin//当计数器计数结束时拉高,高电平最少持续22ns;
                cs <= 1'b1;
                rd <= 1'b1;
            end
            else if(rdata_cnt == 2)begin//当计数器计数到2时拉低,低电平最好大于32ns;
                cs <= 1'b0;
                rd <= 1'b0;
            end
        end
        else begin//状态机处于其余状态时片选和读使能信号拉高;
            cs <= 1'b1;
            rd <= 1'b1;
        end
    end

    //当读数据计数器计数结束时,根据计数器的数值读取对应通道数据;
    always@(posedge clk)begin
        if((state_c == DATA) && (rdata_cnt == 6) && add_cnt)begin
            data <= adc_din;
            data_vld <= (8'h01 << cnt);
        end
        else begin
            data <= data;
            data_vld <= 8'd0;
        end
    end
    
    endmodule

文末提供了完整代码,后台不再提供获取方式,下文会提供对应工程获取方式。

本文转载自:数字站

最新文章

最新文章