差别
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录 前一修订版 后一修订版 | 前一修订版 | ||
scope_verilog [2020/08/22 20:46] gongyu |
scope_verilog [2021/10/24 20:45] (当前版本) gongyusu |
||
---|---|---|---|
行 1: | 行 1: | ||
- | ## 与示波器相关的Verilog代码 | + | ## 基于FPGA逻辑的示波器设计 |
{{drawio>pocketinstru.scope}} | {{drawio>pocketinstru.scope}} | ||
- | ### 1 ADC采集 | + | ### 1 ADC采集数据的搬移 |
#### 1.1 基于FIFO的机制 | #### 1.1 基于FIFO的机制 | ||
FPGA处理2个时钟域之间的数据交换: | FPGA处理2个时钟域之间的数据交换: | ||
行 34: | 行 34: | ||
向FIFO中写数据 | 向FIFO中写数据 | ||
+ | |||
为向FIFO中写数据,我们需要等待到空的状态,一旦FIFO的状态变“满”,则停止写数据,代码逻辑如下: | 为向FIFO中写数据,我们需要等待到空的状态,一旦FIFO的状态变“满”,则停止写数据,代码逻辑如下: | ||
行 49: | 行 50: | ||
从FIFO中读取数据 | 从FIFO中读取数据 | ||
+ | |||
只要FIFO不空,就可以从中读取数据,读出的每一个字节发送到串行输出模块 | 只要FIFO不空,就可以从中读取数据,读出的每一个字节发送到串行输出模块 | ||
行 100: | 行 102: | ||
#### 1.2 基于双口RAM的机制 | #### 1.2 基于双口RAM的机制 | ||
先说一下“触发” | 先说一下“触发” | ||
+ | |||
现在,每次从串行端口接收到字符时,示波器都会被触发。 当然,这仍然不是一个非常有用的设计,但是稍后我们将对其进行改进。 | 现在,每次从串行端口接收到字符时,示波器都会被触发。 当然,这仍然不是一个非常有用的设计,但是稍后我们将对其进行改进。 | ||
行 130: | 行 133: | ||
我们使用2个触发器形式的同步器(将“ startAcquisition”转移到另一个时钟域)。 | 我们使用2个触发器形式的同步器(将“ startAcquisition”转移到另一个时钟域)。 | ||
+ | |||
<code verilog> | <code verilog> | ||
reg startAcquisition1; always @(posedge cll_adc) startAcquisition1 <= startAcquisition; | reg startAcquisition1; always @(posedge cll_adc) startAcquisition1 <= startAcquisition; | ||
行 136: | 行 140: | ||
最后,一旦另一个时钟域“看到”信号,它就会“回复”(使用另一个同步器“正在获取”)。 | 最后,一旦另一个时钟域“看到”信号,它就会“回复”(使用另一个同步器“正在获取”)。 | ||
- | |||
<code verilog> | <code verilog> | ||
行 155: | 行 158: | ||
#### 双口RAM | #### 双口RAM | ||
+ | |||
既然触发器可用,我们需要一个双端口RAM来存储数据,注意RAM的每一侧如何使用不同的时钟。 | 既然触发器可用,我们需要一个双端口RAM来存储数据,注意RAM的每一侧如何使用不同的时钟。 | ||
+ | |||
<code verilog> | <code verilog> | ||
- | ram512 ram_flash( | + | ram512 ram_adc( |
- | .data(data_flash_reg), .wraddress(wraddress), .wren(Acquiring), .wrclock(clk_flash), | + | .data(data_adc_reg), .wraddress(wraddress), .wren(Acquiring), .wrclock(clk_adc), |
.q(ram_output), .rdaddress(rdaddress), .rden(rden), .rdclock(clk) | .q(ram_output), .rdaddress(rdaddress), .rden(rden), .rdclock(clk) | ||
); | ); | ||
行 164: | 行 169: | ||
使用二进制计数器可以轻松创建ram地址总线。 | 使用二进制计数器可以轻松创建ram地址总线。 | ||
+ | |||
首先写地址: | 首先写地址: | ||
<code verilog> | <code verilog> | ||
reg [8:0] wraddress; | reg [8:0] wraddress; | ||
- | always @(posedge clk_flash) if(Acquiring) wraddress <= wraddress + 1; | + | always @(posedge clk_adc) if(Acquiring) wraddress <= wraddress + 1; |
</code> | </code> | ||
行 206: | 行 212: | ||
output TxD; | output TxD; | ||
- | input clk_flash; | + | input clk_adc; |
input [7:0] data_adc; | input [7:0] data_adc; | ||
行 273: | 行 279: | ||
#### 1.3 触发 | #### 1.3 触发 | ||
- | 检测一个上升沿 | + | |
+ | 我们的第一个触发机制非常简单 - 检测一个上升沿穿过一个设定好的阈值,我们使用的是8位的ADC,因此采集到的数据的范围为0x00到0xFF,我们先假设阈值为ox80. | ||
+ | |||
+ | **检测一个上升沿** | ||
+ | 如果一个采样点的值高于设定的阈值,而前一个采样点的值低于该阈值,则进行触发! | ||
<code verilog> | <code verilog> | ||
reg Threshold1, Threshold2; | reg Threshold1, Threshold2; | ||
行 283: | 行 294: | ||
</code> | </code> | ||
+ | **显示中间触发** | ||
+ | |||
+ | 数字示波器的一项重要功能是能够查看在触发之前发生了什么。 | ||
+ | |||
+ | 这是如何实现的? | ||
+ | |||
+ | 示波器不断采集,示波器的存储器一遍又一遍地被覆盖-当到达终点时,我们从头开始。 但是,如果发生触发,则示波器将继续获取其一半以上的存储深度,然后停止。 因此,它保留了一半的内存与触发之前发生的事件,以及一半的触发之后发生的事件。 | ||
+ | |||
+ | 我们在这里使用的是50%或“显示中间触发条件”(其它流行的设置本来是25%和75%的设置,但是以后可以轻松添加)。 | ||
+ | |||
+ | 实施很容易,首先,我们必须跟踪已存储的字节数。 | ||
+ | |||
+ | <code verilog> | ||
+ | reg [8:0] samplecount; | ||
+ | </code> | ||
+ | |||
+ | 对于512字节的存储深度,我们首先确保至少获取256个字节,然后停止计数,但在等待触发时继续获取。 触发条件到来后,我们再次开始计数以获取另外256个字节,然后停止。 | ||
+ | |||
+ | <code verilog> | ||
+ | reg PreTriggerPointReached; | ||
+ | always @(posedge clk_adc) PreTriggerPointReached <= (samplecount==256); | ||
+ | </code> | ||
+ | |||
+ | 决策逻辑处理所有这些步骤: | ||
+ | <code verilog> | ||
+ | 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; | ||
+ | </code> | ||
+ | |||
+ | 请注意,我们已经记住了触发发生的位置。 这用于确定要发送到PC的RAM中示例窗口的开始。 | ||
+ | <code verilog> | ||
+ | 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 | ||
+ | </code> | ||
+ | |||
+ | 通过这种设计,我们终于得到了一个有用的示波器。 我们只需要现在对其进行自定义。 | ||
+ | |||
+ | 以上完成了一个数字示波器的框架,后面就比较容易添加更多的功能了。 | ||
+ | |||
+ | **边沿触发** | ||
+ | |||
+ | 让我们添加在上升沿或下降沿触发的功能。 任何示波器都可以做到这一点。 | ||
+ | 我们需要一点信息来决定要触发的方向。 让我们使用PC发送的数据的bit-0。 | ||
+ | <code verilog> | ||
+ | assign Trigger = (RxD_data[0] ^ Threshold1) & (RxD_data[0] ^ ~Threshold2); | ||
+ | |||
+ | </code> | ||
+ | 非常简单 | ||
+ | |||
+ | **更多的选择:** | ||
+ | |||
+ | 让我们添加控制触发阈值的功能。 这是一个8位的值。 然后,我们需要水平采集速率控制,滤波控制...这需要PC上的多个控制字节来控制示波器。 | ||
+ | |||
+ | 最简单的方法是使用 “async_receiver” 间隙检测功能。 PC突发发送控制字节,当它停止发送时,FPGA对其进行检测并断言“ RxD_gap”信号。 | ||
+ | |||
+ | <code verilog> | ||
+ | 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]; | ||
+ | |||
+ | </code> | ||
+ | |||
+ | 我们还添加了一个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|仪器传输及控制协议]] |