## 基于FPGA逻辑的示波器设计
{{drawio>pocketinstru.scope}}
### 1 ADC采集数据的搬移
#### 1.1 基于FIFO的机制
FPGA处理2个时钟域之间的数据交换:
- 一个慢的系统时钟 - 固定为25MHz
- ADC采样时钟(一般为快速时钟,比如说50MHz),它由FPGA产生并送给ADC
从ADC采集到的数据以ADC的采样频率存储到FIFO中,FIFO的数据被另一个时钟读出,并通过并/串变换发往串口(比如115200波特率)或SPI总线驱动的LCD屏幕。
假如采用的FIFO的深度为1024个字节,50MHz的采样频率,花费大约20μS的时间将FIFO填满,一旦填满,我们就不再向FIFO发送数据,存储在FIFO中的数据需要使用低速的时钟全部读出并发送出去。如果采用115200波特率的串行通信方式,大约每秒可以传送10KBytes,1024个采样点数据需要大约100ms全部发送完毕,在此期间,示波器是“盲”的,因为其采集到的数据都会被丢弃掉,不会存储到FIFO中,也就是说有99.99%的时间是“盲”的,采用这种架构带来的这种结果是非常正常的。
如果添加了“触发”的功能,则可以靠触发条件来选择数据。
ADC输出的数据总线通过8个管脚连接到FPGA,我们命名这8位的数据为"data_adc[7:0]", 速率很高, 最好先用寄存器来同步一下:
reg [7:0] data_adc_reg;
always @(posedge clk_adc) data_adc_reg <= data_adc;
FIFO为1024个word的深度 * 8bit的宽度,50Msps的采样率,需要花费大约20μs填满FIFO。FIFO可以用FPGA内部的同步SRAM来构成,不同FPGA厂商的IP构成方式不同,但功能都是一样的。
完成了FIFO的例化以后,我们就可以通过FIFO将数据连接起来:
fifo myfifo(.data(data_adc_reg), .wrreq(wrreq), .wrclk(clk_adc), .wrfull(wrfull), .wrempty(wrempty), .q(q_fifo), .rdreq(rdreq), .rdclk(clk), .rdempty(rdempty));
向FIFO中写数据
为向FIFO中写数据,我们需要等待到空的状态,一旦FIFO的状态变“满”,则停止写数据,代码逻辑如下:
reg fillfifo;
always @(posedge clk_adc)
if(~fillfifo)
fillfifo <= wrempty; // start when empty
else
fillfifo <= ~wrfull; // stop when full
assign wrreq = fillfifo;
从FIFO中读取数据
只要FIFO不空,就可以从中读取数据,读出的每一个字节发送到串行输出模块
wire TxD_start = ~TxD_busy & ~rdempty;
assign rdreq = TxD_start;
async_transmitter async_txd(.clk(clk), .TxD(TxD), .TxD_start(TxD_start), .TxD_busy(TxD_busy), .TxD_data(q_fifo));
然后我们就可以通过调用“异步发送”的功能模块将数据编程变成串行,并通过“TxD”信号管脚发送出去。
完整的设计:
module oscillo(clk, TxD, clk_adc, data_adc);
input clk;
output TxD;
input clk_adc;
input [7:0] data_adc;
reg [7:0] data_adc_reg; always @(posedge clk_adc) data_adc_reg <= data_adc;
wire [7:0] q_fifo;
fifo myfifo(.data(data_adc_reg), .wrreq(wrreq), .wrclk(clk_adc), .wrfull(wrfull), .wrempty(wrempty), .q(q_fifo), .rdreq(rdreq), .rdclk(clk), .rdempty(rdempty));
// The ADC side starts filling the fifo only when it is completely empty,
// and stops when it is full, and then waits until it is completely empty again
reg fillfifo;
always @(posedge clk_adc)
if(~fillfifo)
fillfifo <= wrempty; // start when empty
else
fillfifo <= ~wrfull; // stop when full
assign wrreq = fillfifo;
// the manager side sends when the fifo is not empty
wire TxD_busy;
wire TxD_start = ~TxD_busy & ~rdempty;
assign rdreq = TxD_start;
async_transmitter async_txd(.clk(clk), .TxD(TxD), .TxD_start(TxD_start), .TxD_busy(TxD_busy), .TxD_data(q_fifo));
endmodule
#### 1.2 基于双口RAM的机制
先说一下“触发”
现在,每次从串行端口接收到字符时,示波器都会被触发。 当然,这仍然不是一个非常有用的设计,但是稍后我们将对其进行改进。
我们从串行端口接收数据:
wire [7:0] RxD_data;
async_receiver async_rxd(.clk(clk), .RxD(RxD), .RxD_data_ready(RxD_data_ready), .RxD_data(RxD_data));
每次接收到新字符时,“RxD_data_ready”都会变高一个时钟。 我们用它来触发示波器。
再谈一下“同步”
我们需要将此“RxD_data_ready变高”信息从“clk”(25MHz)域传输到“clk_adc”(100MHz)域。
首先,当接收到字符时,信号“ startAcquisition”变高。
reg startAcquisition;
wire AcquisitionStarted;
always @(posedge clk)
if(~startAcquisition)
startAcquisition <= RxD_data_ready;
else
if(AcquisitionStarted)
startAcquisition <= 0;
我们使用2个触发器形式的同步器(将“ startAcquisition”转移到另一个时钟域)。
reg startAcquisition1; always @(posedge cll_adc) startAcquisition1 <= startAcquisition;
reg startAcquisition2; always @(posedge clk_adc) startAcquisition2 <= startAcquisition1;
最后,一旦另一个时钟域“看到”信号,它就会“回复”(使用另一个同步器“正在获取”)。
reg Acquiring;
always @(posedge clk_adc)
if(~Acquiring)
Acquiring <= startAcquisition2; // start acquiring?
else
if(&wraddress) // done acquiring?
Acquiring <= 0;
reg Acquiring1; always @(posedge clk) Acquiring1 <= Acquiring;
reg Acquiring2; always @(posedge clk) Acquiring2 <= Acquiring1;
assign AcquisitionStarted = Acquiring2;
回复将重置原始信号。
#### 双口RAM
既然触发器可用,我们需要一个双端口RAM来存储数据,注意RAM的每一侧如何使用不同的时钟。
ram512 ram_adc(
.data(data_adc_reg), .wraddress(wraddress), .wren(Acquiring), .wrclock(clk_adc),
.q(ram_output), .rdaddress(rdaddress), .rden(rden), .rdclock(clk)
);
使用二进制计数器可以轻松创建ram地址总线。
首先写地址:
reg [8:0] wraddress;
always @(posedge clk_adc) if(Acquiring) wraddress <= wraddress + 1;
以及读取的地址:
reg [8:0] rdaddress;
reg Sending;
wire TxD_busy;
always @(posedge clk)
if(~Sending)
Sending <= AcquisitionStarted;
else
if(~TxD_busy)
begin
rdaddress <= rdaddress + 1;
if(&rdaddress) Sending <= 0;
end
注意每个计数器如何使用不同的时钟。
最后,我们将数据发送到PC:
wire TxD_start = ~TxD_busy & Sending;
wire rden = TxD_start;
wire [7:0] ram_output;
async_transmitter async_txd(.clk(clk), .TxD(TxD), .TxD_start(TxD_start), .TxD_busy(TxD_busy), .TxD_data(ram_output));
完整的设计
module oscillo(clk, RxD, TxD, clk_adc, data_adc);
input clk;
input RxD;
output TxD;
input clk_adc;
input [7:0] data_adc;
///////////////////////////////////////////////////////////////////
wire [7:0] RxD_data;
async_receiver async_rxd(.clk(clk), .RxD(RxD), .RxD_data_ready(RxD_data_ready), .RxD_data(RxD_data));
reg startAcquisition;
wire AcquisitionStarted;
always @(posedge clk)
if(~startAcquisition)
startAcquisition <= RxD_data_ready;
else
if(AcquisitionStarted)
startAcquisition <= 0;
reg startAcquisition1; always @(posedge clk_adc) startAcquisition1 <= startAcquisition ;
reg startAcquisition2; always @(posedge clk_adc) startAcquisition2 <= startAcquisition1;
reg Acquiring;
always @(posedge clk_adc)
if(~Acquiring)
Acquiring <= startAcquisition2;
else
if(&wraddress)
Acquiring <= 0;
reg [8:0] wraddress;
always @(posedge clk_adc) if(Acquiring) wraddress <= wraddress + 1;
reg Acquiring1; always @(posedge clk) Acquiring1 <= Acquiring;
reg Acquiring2; always @(posedge clk) Acquiring2 <= Acquiring1;
assign AcquisitionStarted = Acquiring2;
reg [8:0] rdaddress;
reg Sending;
wire TxD_busy;
always @(posedge clk)
if(~Sending)
Sending <= AcquisitionStarted;
else
if(~TxD_busy)
begin
rdaddress <= rdaddress + 1;
if(&rdaddress) Sending <= 0;
end
wire TxD_start = ~TxD_busy & Sending;
wire rden = TxD_start;
wire [7:0] ram_output;
async_transmitter async_txd(.clk(clk), .TxD(TxD), .TxD_start(TxD_start), .TxD_busy(TxD_busy), .TxD_data(ram_output));
///////////////////////////////////////////////////////////////////
reg [7:0] data_adc_reg; always @(posedge clk_adc) data_adc_reg <= data_adc;
ram512 ram_adc(
.data(data_adc_reg), .wraddress(wraddress), .wren(Acquiring), .wrclock(clk_adc),
.q(ram_output), .rdaddress(rdaddress), .rden(rden), .rdclock(clk)
);
endmodule
#### 1.3 触发
我们的第一个触发机制非常简单 - 检测一个上升沿穿过一个设定好的阈值,我们使用的是8位的ADC,因此采集到的数据的范围为0x00到0xFF,我们先假设阈值为ox80.
**检测一个上升沿**
如果一个采样点的值高于设定的阈值,而前一个采样点的值低于该阈值,则进行触发!
reg Threshold1, Threshold2;
always @(posedge clk_adc) Threshold1 <= (data_adc_reg>=8'h80);
always @(posedge clk_adc) Threshold2 <= Threshold1;
assign Trigger = Threshold1 & ~Threshold2; // if positive edge, trigger!
**显示中间触发**
数字示波器的一项重要功能是能够查看在触发之前发生了什么。
这是如何实现的?
示波器不断采集,示波器的存储器一遍又一遍地被覆盖-当到达终点时,我们从头开始。 但是,如果发生触发,则示波器将继续获取其一半以上的存储深度,然后停止。 因此,它保留了一半的内存与触发之前发生的事件,以及一半的触发之后发生的事件。
我们在这里使用的是50%或“显示中间触发条件”(其它流行的设置本来是25%和75%的设置,但是以后可以轻松添加)。
实施很容易,首先,我们必须跟踪已存储的字节数。
reg [8:0] samplecount;
对于512字节的存储深度,我们首先确保至少获取256个字节,然后停止计数,但在等待触发时继续获取。 触发条件到来后,我们再次开始计数以获取另外256个字节,然后停止。
reg PreTriggerPointReached;
always @(posedge clk_adc) PreTriggerPointReached <= (samplecount==256);
决策逻辑处理所有这些步骤:
always @(posedge clk_adc)
if(~Acquiring)
begin
Acquiring <= startAcquisition2; // start acquiring?
PreOrPostAcquiring <= startAcquisition2;
end
else
if(&samplecount) // got 511 bytes? stop acquiring
begin
Acquiring <= 0;
AcquiringAndTriggered <= 0;
PreOrPostAcquiring <= 0;
end
else
if(PreTriggerPointReached) // 256 bytes acquired already?
begin
PreOrPostAcquiring <= 0;
end
else
if(~PreOrPostAcquiring)
begin
AcquiringAndTriggered <= Trigger; // Trigger? 256 more bytes and we're set
PreOrPostAcquiring <= Trigger;
if(Trigger) wraddress_triggerpoint <= wraddress; // keep track of where the trigger happened
end
always @(posedge clk_adc) if(Acquiring) wraddress <= wraddress + 1;
always @(posedge clk_adc) if(PreOrPostAcquiring) samplecount <= samplecount + 1;
reg Acquiring1; always @(posedge clk) Acquiring1 <= AcquiringAndTriggered;
reg Acquiring2; always @(posedge clk) Acquiring2 <= Acquiring1;
assign AcquisitionStarted = Acquiring2;
请注意,我们已经记住了触发发生的位置。 这用于确定要发送到PC的RAM中示例窗口的开始。
reg [8:0] rdaddress, SendCount;
reg Sending;
wire TxD_busy;
always @(posedge clk)
if(~Sending)
begin
Sending <= AcquisitionStarted;
if(AcquisitionStarted) rdaddress <= (wraddress_triggerpoint ^ 9'h100);
end
else
if(~TxD_busy)
begin
rdaddress <= rdaddress + 1;
SendCount <= SendCount + 1;
if(&SendCount) Sending <= 0;
end
通过这种设计,我们终于得到了一个有用的示波器。 我们只需要现在对其进行自定义。
以上完成了一个数字示波器的框架,后面就比较容易添加更多的功能了。
**边沿触发**
让我们添加在上升沿或下降沿触发的功能。 任何示波器都可以做到这一点。
我们需要一点信息来决定要触发的方向。 让我们使用PC发送的数据的bit-0。
assign Trigger = (RxD_data[0] ^ Threshold1) & (RxD_data[0] ^ ~Threshold2);
非常简单
**更多的选择:**
让我们添加控制触发阈值的功能。 这是一个8位的值。 然后,我们需要水平采集速率控制,滤波控制...这需要PC上的多个控制字节来控制示波器。
最简单的方法是使用 “async_receiver” 间隙检测功能。 PC突发发送控制字节,当它停止发送时,FPGA对其进行检测并断言“ RxD_gap”信号。
wire RxD_gap;
async_receiver async_rxd(.clk(clk), .RxD(RxD), .RxD_data_ready(RxD_data_ready), .RxD_data(RxD_data), .RxD_gap(RxD_gap));
reg [1:0] RxD_addr_reg;
always @(posedge clk) if(RxD_gap) RxD_addr_reg <= 0; else if(RxD_data_ready) RxD_addr_reg <= RxD_addr_reg + 1;
// register 0: TriggerThreshold
reg [7:0] TriggerThreshold;
always @(posedge clk) if(RxD_data_ready & (RxD_addr_reg==0)) TriggerThreshold <= RxD_data;
// register 1: "0 0 0 0 HDiv[3] HDiv[2] HDiv[1] HDiv[0]"
reg [3:0] HDiv;
always @(posedge clk) if(RxD_data_ready & (RxD_addr_reg==1)) HDiv <= RxD_data[3:0];
// register 2: "StartAcq TriggerPolarity 0 0 0 0 0 0"
reg TriggerPolarity;
always @(posedge clk) if(RxD_data_ready & (RxD_addr_reg==2)) TriggerPolarity <= RxD_data[6];
wire StartAcq = RxD_data_ready & (RxD_addr_reg==2) & RxD_data[7];
我们还添加了一个4位寄存器(HDiv[3:0])以控制水平采集速率。 当我们想降低采集速率时,要么丢弃来自ADC的采样,要么以我们感兴趣的频率对它们进行滤波/降采样。
### 2. LCD的波形和界面显示
参见[[graplcd_verilog|图形化LCD的显示]]
### 3. UART的数据传输
参见[[uart_verilog|串行接口RS-232通信的Verilog代码]]
### 4. 通过SPI的数据传输
### 5. 增益及直流偏移的控制
参见[[pwm_verilog|PWM的应用及相应的Verilog代码]]
### 6. 参数的自动测量
### 7. 校准和自动设置
### 硬禾学堂的仪器传输及控制协议
[[instru_protocol|仪器传输及控制协议]]