【DAQ Systems】高速ADC的HDL设计(一)“触发-采样-传输”系统

【DAQ Systems】异步高速ADC的HDL设计(一)“触发-采样-传输”系统

0. 前言

在接下来的一段时间内,我将结合我的工作内容对数据采集系统(DAQ)进行讨论。

高速ADC是数据采集系统中极为重要的组成部分。在日常的设计中,我们会遇到各种各样不同参数、不同种类的ADC。而本文主要介绍异步高速ADC驱动模块的一种基础思想。

首先,本文主要讨论的是异步传输。如果不使用异步传输,显然ADC的采样速率需要和后级处理保持同步,这样就会带来问题。如果高速ADC的后级是AXI-Stream总线,那么整个总线的速率都会影响ADC的采样率,如果把它调的太低则会影响总线效率,太高又不满足ADC的设计参数。因此,异步传输系统的意义十分重大。

本文主要介绍的异步传输系统,其大致概括为一个“触发——采样——传输”的过程。这种传输主要用于短时突发采集大量数据,能够将高速的ADC连接至一个低速系统,例如FPGA-STM32的数据链路,因STM32无法完成较高采样率的信号采集的应用。其中,三个状态独立工作相互转换,状态的切换由两边的状态信号进行控制,可以将其理解为:将高速系统作为低速系统的一个外设来使用

虽然该系统框架十分简单,且容易理解,但是在实际实现过程中仍然会遇到非常多的问题。本文将以一个FPGA-STM32高速AD采集系统为例,详细分析各个设计的细节,最终对其进行综合分析以及提出修改意见。

1. 高速AD的时钟

在一些应用中,高速AD在FPGA中属于跨时钟域的外设,他的速度要么太快,要么太慢,还可能和FPGA不是同源时钟。虽然我们管他叫高速AD,但是实际上为了能够及时处理如此之高的数据吞吐量,其处理部分的电路可能运行速度更快。当然,也会有类似高速摄像机一样,短时间突发采集大量数据的应用,这种情况下采集的速率更快,而传输、处理的速率更慢。总而言之,需要采取一些措施来处理这个跨时钟域的问题。

我们来看这样一道题:

(大疆2020数字芯片)下列关于多bit数据跨时钟域的处理思路,错误的有()

A. 发送方给出数据,接收方用本地时钟同步两拍再使用;

B. 发送方把数据写到异步fifo,接收方从异步fifo里读出;

C. 对于连续变化的信号,发送方转为格雷码发送,接收方收到后再转为二进制;

D. 发送方给出数据,发送方给出握手请求,接收方收到后回复,发送方撤销数据。

答案:A

解析:多bit跨时钟域不能简单使用打两拍,打拍后可能数据错乱;

题中提到的异步FIFO是用来处理跨时钟域数据同步的常用手段。

使用异步FIFO,写一侧可以直接连接高速ADC,数据连接到ADC_DATA总线,时钟连接到锁相环输出的高速ADC_CLK。读一侧则可以直接连接到下一级的处理,时钟和处理系统同步。这就构成了一个非常简单的ADC-FIFO采集器,现在它已经可以实现数据的采集,只要写使能,FIFO就会随着ADC时钟的跳变而源源不断的存储ADC的数据。而在需要取出数据的时候,将读使能,数据就会随着读一侧的时钟跳变而向Q端口输出,从而输出数据。

image-20220523102824382

显然,我们还需要某种方式将数据取回,我们将放在第二节进行讨论。

值得一提的是,不同高速ADC的时钟提供方式不同。例如,AD9280只有一个时钟端口。

image-20220523094556777

可以看到其实时数据与数字化的数据相差四个时钟周期。在实际处理的过程中,可以用多个寄存器“打拍”进行同步。

/* Async data to sync clock domain */
always@(posedge adc_clk or negedge adc_rst_n)
begin
    if(adc_rst_n == 1'b0)
    begin
        sample_start_d0    <= 1'b0;
        sample_start_d1    <= 1'b0;
        sample_start_d2    <= 1'b0;
    end 
    else 
    begin
         sample_start_d0    <= sample_start;
         sample_start_d1    <= sample_start_d0;
         sample_start_d2    <= sample_start_d1;
     end    
end

而LTC2208具有两个时钟端口。一个是编码时钟输入,一个是数据时钟输出。其中,编码时钟输入通常是一个整数锁相环,例如许多示波器设计中所使用的ADF4360-N系列整数PLL。我们在图中可以观察到,其ENC信号与CLKOUT_{A/B}具有一个长达t_{AP}的延迟,这是十分合理的现象,因为ADC的采样率已经在130Msps,相较于低速ADC,它留给FPGA处理的时间更短,因此需要单独的数据时钟进行同步。同样的,LTC2208的数据输出慢了7拍,也要在设计模块时予以延迟。

image-20220523094809895

设一个高速ADC的采样速率为20Msps。这已经能满足许多中频采样系统的应用,然而处理它的数据链路的时钟速率很可能达到100MHz以上(例如AXI总线的频率,通常设为100M),这些模块通常伴有一定的传输延迟,因此为了克服级间传输延迟造成的阻塞,使用更高速率的时钟去驱动显得十分合理。

不过,在另一些应用中,也有整个系统同步使用同一个时钟的,这种设计十分简单。往往在每一级传递的过程中会产生数个时钟周期的延迟,我们在设计的过程中要特别注意到这点,尤其是有一些ADC芯片本身就是滞后几个时钟的数据输出,在采集数据的实时性上要做更多的考虑。而抛开延迟不谈,这样做避免了跨时钟域的同步问题,在一定意义上简化了系统的设计。因此,在一些对于实时性要求没有那么高的系统中,也可以用这种架构,通过软件补偿、增加采样点、删除无效数据的方式进行简单修正,大大简化了逻辑设计。

hforsten FMCW3 RTL图

这没什么好设计的(当然,这里还有一个DDR(双倍数据速率)以及Ping-Pong操作,以后再做讨论),时钟做好同步,直接往后面一级里灌就完事儿了。一般是在ADC的module中添加一级缓冲寄存器,以达到类似Sample-Hold的操作,防止数据意外跳变。

2. 如何控制ADC采样

在上文中,我们刚刚讨论了高速ADC的时钟问题。在继续讨论之前,我想强调一下我们主要讨论异步的HDL驱动设计,因为同步的没什么好设计的,时钟做好同步,直接往后面一级里灌就完事儿了。而异步就比较复杂,往往需要等待、应答之类的操作,因此我们迫切的需要引入状态机来对异步采样进行控制。

其状态转移图如下图所示:

image-20220523112012244

图中有三个状态,分别为空闲状态、采样状态与传输状态。

空闲态:该状态为默认状态,如果采样触发置高,就进入下一状态。

采样状态:该状态有效时,FIFO写控制信号置高,FIFO的数据输入连接至外部引脚连接到ADC,FIFO中存入ADC数据。如果检测到FIFO写满标志位,即切换到下一状态。

传输状态:该状态有效时,FIFO读控制标志位置高,FIFO输出口的数据随着FIFO读时钟跳变而变化。如果检测到FIFO读空标志位,即切换到下一状态。

通过编写一个三段式状态机,就可以很轻松的实现采样功能。通常,后级在FIFO读空标志位置高之前,都会源源不断的取出数据。但是在处理FIFO输出侧的数据时,如果主机极慢,而且不能够提供一个较好的时钟信号,则遇奇怪的问题。

拿我实现的采样系统为例,FIFO的后级是一个SPI控制器。在第一次尝试的过程中,我使用SPI的片选CS信号作为时钟提供给FIFO读一侧。这就造成了一个十分严重的问题——时钟混乱

image-20220523154145797

从Intel提供的手册来看,FIFO ip核中rdreq与rdemp还有q均存在一定的输出延迟,这里的延迟应该为内部寄存器所致,FIFO是RAM+Controller的组合体,其内部必然有一定的控制电路,这是产生延迟的原因,但是如果我们提供一个很差的时钟(不规范的时钟,占空比不定,时有时无)给他,会发生什么事情,用户手册中并没有提到,但是在使用过程中的确遇到了十分奇怪的问题。

image-20220523154603418

如图所示的fifo结构中,我使用了一个SPI CS信号作为FIFO的rdclk,一开始的调试过程还好挺顺利的,到了第二天,我改了点代码,整个数据全部乱了,甚至出现了不知道为何片选突然被拉下导致FIFO乱吐数据的问题。STM32读到的数据要么是一个跳几个,要么是头尾空好多。虽然暂时不能下定论,但是我可以大胆的猜测,这个bug的原因大概率是我给FIFO一个非常垃圾的时钟,然而fifo内部工作又需要时钟,导致各种紊乱,乱蹦数据。

警告:使用不规范的时钟去驱动一些IP核,这种操作应当被避免。!!

在Xilinx ILA中,如果你连接了一个很烂的时钟给他,他就会直接告诉你这个时钟不稳定不能作为逻辑分析的参考时钟,况且我当时用的是FT2232芯片的DCLK,该时钟信号也是时有时无,是在传输发生的时候才有,因此可以理解其不能作为逻辑分析仪的参考时钟,因为iLA工作本身需要一定的时钟,不论是初始化、复位、装载数据等等。

我认为,比较合理的做法是使用打拍的方式,建立一个rdreq的寄存器,使其保持一个时钟周期置高后迅速置低。但是说着容易,实际上实现起来十分困难,如果你有什么好的思路欢迎建议~

于是,后来我将FIFO替换成了RAM,并手动写了一个RAM控制器,稳定性就得到了极大的改善。

image-20220523162648544

image-20220523162603387

首先由ARM处理器产生一个正脉冲触发一次采样,状态机在满足收到脉冲触发信号后,由ST_IDLE(空闲态)状态跳转至ST_SAMPLE(采样态)开始信号的采样。信号的采样由一个内存控制器控制,内存控制器中有一个地址累加器。当状态机的状态变为ST_SAMPLE时,内存控制器写使能,并累加写地址寄存器。高速ADC的编码时钟与内存控制器的时钟属于同一个时钟,均是由FPGA内置的锁相环IP核产生,该锁相环输入50MHz的参考基准时钟信号,锁相产生10MHz的低频采样时钟分配给ADC与RAM模块。这样一来,随着时钟的变化,ADC输出的数据源源不断的存入RAM中,地址累加器也随着内存控制器的时钟累加计数。

image-20220523163638825

当写地址寄存器的地址为0xFFF,即4095时,表示RAM已经存储满,这意味着采样任务已经完成,在下一时钟周期,RAM满标志位寄存器置1。状态机检测到RAM写满标志位后,会自动向下一状态切换,也就是传输态。在传输态,RAM地址选择器会将RAM的地址输入进行切换,由写地址累加器切换为读地址累加器,这么做的目的是防止两个状态中对同一个寄存器操作,导致条件过多逻辑混乱。

image-20220523163858173

在传输态,内存控制器的地址选择器由写地址累加器切换为读地址累加器。读地址累加器由SPI总线的CS控制,每传输一个数据帧,CS拉低一次,写地址累加器的地址加1,而RAM输出端口Q直接连接到SPI控制器的数据输入端,随着RAM的更新,SPI数据发送寄存器也会更新。

image-20220523164158929

当读地址寄存器为0xFFF,RAM读结束,这样一来,随着FSMC数据总线请求数据量的累加,所有RAM的字都会被遍历到,以达到STM32取回所有采样数据的目的,最终,在空闲态,将写地址寄存器与读地址寄存器均清零,就完成了一次发送操作。

两个满标志位寄存器如图:

image-20220523163927906

时序图,可以观察到单片机发送的ACK脉冲与RAM数据输出的延迟关系:

image-20220523165435244

单片机与FPGA的连接:

image-20220523165810775

3. 总结与展望

经过以上的设计,便可以实现STM32将FPGA作为一个高速ADC采样器来使用,通过置不同的信号,达到控制FPGA的目的,最后将数据通过SPI总线进行取回。

该系统仍有许多不足,但是我们的确已经达到了目的,进行了稳定的传输。至于为何这个系统的设计到这里戛然而止,那必定是我们已经有了更好的代替,毕竟这种胶水双核的做法在集成度这么高的今天属实不是一种优雅的做法。如今的FPGA可以内嵌软核,从而构建出一个单片机,而使用内部总线进行数据交互不仅突发数据量巨大,延迟又低,使用内部中断信号又可以不用连线就进行控制,拥有诸多优点,这便是我们不对这个系统继续进行深入研究的原因。

IMG_3681

在接下来的文章中,我将会继续介绍更先进更简洁更高效的ADC采集系统,本文到此结束,感谢观看。

附录: 核心代码

    /**********************************FSM Part**********************************/
    // reg fsm states
    reg [2:0] curr_state;
    reg [2:0] next_state;

    parameter ST_IDLE   = 3'd0; // IDLE
    parameter ST_SAMPLE = 3'd1; // Sampling
    parameter ST_TX     = 3'd2; // Transmit DATA

    // state reg
    always@(posedge sys_clk_pll_50 or negedge rst_n)
    begin
        if (~rst_n) curr_state <= ST_IDLE;
        else        curr_state <= next_state;
    end

    // ! 临时信号用于测试
    wire flag_tx_finish;
    assign flag_tx_finish = (reg_ram_output_addr == 12'd4095);
    // next state logic
    always@(*)
    begin
        case (curr_state)
            /* if trigger sample button pushed, move into ST_SAMPLE */
            ST_IDLE         : if (fpga_sample_trig == 1) next_state = ST_SAMPLE;
            else next_state             = ST_IDLE;
            /* if adc_fifo_wr_full is high, that means ADC sample has finished, move into ST_SAMPLE_HOLD
             send data ready signal to STM32, and wait for the ACK signal */
            ST_SAMPLE       : if (flag_ram_full == 1) next_state = ST_TX;
            else next_state                     = ST_SAMPLE;
            /* if received STM32 ACK signal, begin SPI transmittion, step into ST_TX */
            /* if fifo is empty, that means SPI transmittion has done, return to ST_IDLE and wait for next trigger */
            ST_TX           : if (fpga_reg_reset == 1) next_state = ST_IDLE;
            else next_state                      = ST_TX;
            default :          next_state        = ST_IDLE;
        endcase
    end

    //output logic
    always@(*)
    begin
        case (curr_state)
            ST_IDLE     : led = 8'b01111111;
            ST_SAMPLE   : led = 8'b10111111;
            ST_TX       : led = 8'b11011111;
            default     : led = 8'b11111111;
        endcase
    end

    // when current state is ST_SAMPLE enables adc fifo write request
    // assign RAM_write_full = (curr_state == ST_SAMPLE) && (reg_ram_addr == 12d'4095);
    assign RAM_write_enable  = (curr_state == ST_SAMPLE) && (!flag_ram_full);

    /********************************** other signals **********************************/

    // test data generator
    reg [11:0] data_adder_for_test;
    always@ (negedge adc_clk_10M or negedge rst_n)// negative edge trigger, data +1
    begin
        if (!rst_n)
        begin
            data_adder_for_test <= 0;
        end
        else if (RAM_write_enable)
        begin
            data_adder_for_test <= data_adder_for_test + 1'd1;
        end
        else
        begin
            data_adder_for_test <= data_adder_for_test;
        end
    end

    // ram address accumulator for write
    always@ (posedge adc_clk_10M or negedge rst_n)
    begin
        if (!rst_n)begin
            reg_ram_input_addr <= 0;
        end
        else if (RAM_write_enable)begin
            reg_ram_input_addr <= reg_ram_input_addr + 1'b1;
        end
        else begin
            reg_ram_input_addr <= reg_ram_input_addr;
        end
    end

    // RAM full indicator
    reg flag_ram_full;
     assign fpga_ram_full = flag_ram_full;
    always@ (posedge adc_clk_10M or negedge rst_n)begin
        if (!rst_n)begin
            flag_ram_full <= 0;
        end
        else if (reg_ram_input_addr == 12'd4095)begin
            flag_ram_full <= 1;
        end
        else begin
            flag_ram_full <= flag_ram_full;
        end
    end

    // ram address accumulator for read
    always@ (posedge RAM_output_update_trig or negedge rst_n)
    begin
        if (!rst_n)begin
            reg_ram_output_addr <= 0;
        end
        else if (curr_state == ST_TX)begin
            reg_ram_output_addr <= reg_ram_output_addr + 1'b1;
        end
        else begin
            reg_ram_output_addr <= reg_ram_output_addr;
        end
    end

    // RAM full indicator
    assign fpga_read_end = flag_ram_read_end;
    reg flag_ram_read_end;
    always@ (posedge adc_clk_10M or negedge rst_n)begin
        if (!rst_n)begin
            flag_ram_read_end <= 0;
        end
        else if (reg_ram_output_addr == 12'd4095)begin
            flag_ram_read_end <= 1;
        end
        else begin
            flag_ram_read_end <= flag_ram_read_end;
        end
    end

发表回复