差别

这里会显示出您选择的修订版和当前版本之间的差别。

到此差别页面的链接

两侧同时换到之前的修订记录 前一修订版
后一修订版
前一修订版
scope_verilog [2020/08/21 22:12]
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个时钟域之间的数据交换:
   - 一个慢的系统时钟 - 固定为25MHz   - 一个慢的系统时钟 - 固定为25MHz
行 20: 行 20:
 reg [7:0] data_adc_reg;​ reg [7:0] data_adc_reg;​
 always @(posedge clk_adc) data_adc_reg <= data_adc; always @(posedge clk_adc) data_adc_reg <= data_adc;
 +
 +</​code>​
  
 FIFO为1024个word的深度 * 8bit的宽度,50Msps的采样率,需要花费大约20μs填满FIFO。FIFO可以用FPGA内部的同步SRAM来构成,不同FPGA厂商的IP构成方式不同,但功能都是一样的。 FIFO为1024个word的深度 * 8bit的宽度,50Msps的采样率,需要花费大约20μs填满FIFO。FIFO可以用FPGA内部的同步SRAM来构成,不同FPGA厂商的IP构成方式不同,但功能都是一样的。
  
 完成了FIFO的例化以后,我们就可以通过FIFO将数据连接起来: 完成了FIFO的例化以后,我们就可以通过FIFO将数据连接起来:
 +
 <code verilog> <code verilog>
  
行 31: 行 34:
  
 向FIFO中写数据 向FIFO中写数据
 +
 为向FIFO中写数据,我们需要等待到空的状态,一旦FIFO的状态变“满”,则停止写数据,代码逻辑如下: 为向FIFO中写数据,我们需要等待到空的状态,一旦FIFO的状态变“满”,则停止写数据,代码逻辑如下:
 +
 <code verilog> <code verilog>
 reg fillfifo; reg fillfifo;
-always @(posedge ​clk_flash)+always @(posedge ​clk_adc)
 if(~fillfifo) if(~fillfifo)
   fillfifo <= wrempty; // start when empty   fillfifo <= wrempty; // start when empty
行 45: 行 50:
  
 从FIFO中读取数据 从FIFO中读取数据
 +
 只要FIFO不空,就可以从中读取数据,读出的每一个字节发送到串行输出模块 只要FIFO不空,就可以从中读取数据,读出的每一个字节发送到串行输出模块
  
行 94: 行 100:
 </​code>​ </​code>​
  
-#### 1.2 双口RAM的方式+#### 1.2 基于双口RAM的机制
 先说一下“触发” 先说一下“触发”
  
 +现在,每次从串行端口接收到字符时,示波器都会被触发。 当然,这仍然不是一个非常有用的设计,但是稍后我们将对其进行改进。
 +
 +我们从串行端口接收数据:
 +<code verilog>
 +wire [7:0] RxD_data;
 +async_receiver async_rxd(.clk(clk),​ .RxD(RxD), .RxD_data_ready(RxD_data_ready),​ .RxD_data(RxD_data));​
 +</​code>​
 +
 +每次接收到新字符时,“RxD_data_ready”都会变高一个时钟。 我们用它来触发示波器。
 +
 +再谈一下“同步”
 +
 +我们需要将此“RxD_data_ready变高”信息从“clk”(25MHz)域传输到“clk_adc”(100MHz)域。
 +
 +首先,当接收到字符时,信号“ startAcquisition”变高。
 +
 +<code verilog>
 +reg startAcquisition;​
 +wire AcquisitionStarted;​
 +
 +always @(posedge clk)
 +if(~startAcquisition)
 +  startAcquisition <= RxD_data_ready;​
 +else
 +if(AcquisitionStarted)
 +  startAcquisition <= 0;
 +  ​
 +</​code>​
 +
 +我们使用2个触发器形式的同步器(将“ startAcquisition”转移到另一个时钟域)。
 +
 +<code verilog>
 +reg startAcquisition1;​ always @(posedge cll_adc) startAcquisition1 <= startAcquisition;​
 +reg startAcquisition2;​ always @(posedge clk_adc) startAcquisition2 <= startAcquisition1;​
 +</​code>​
 +
 +最后,一旦另一个时钟域“看到”信号,它就会“回复”(使用另一个同步器“正在获取”)。
 +
 +<code verilog>
 +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;
 +</​code>​
 +
 +回复将重置原始信号。
 +
 +#### 双口RAM
 +
 +既然触发器可用,我们需要一个双端口RAM来存储数据,注意RAM的每一侧如何使用不同的时钟。
 +
 +<code verilog>
 +ram512 ram_adc(
 +  .data(data_adc_reg),​ .wraddress(wraddress),​ .wren(Acquiring),​ .wrclock(clk_adc),​
 +  .q(ram_output),​ .rdaddress(rdaddress),​ .rden(rden),​ .rdclock(clk)
 +);
 +</​code>​
 +
 +使用二进制计数器可以轻松创建ram地址总线。
 +
 +首先写地址:
 +<code verilog>
 +reg [8:0] wraddress;
 +always @(posedge clk_adc) if(Acquiring) wraddress <= wraddress + 1;
 +</​code>​
 +
 +以及读取的地址:
 +<code verilog>
 +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
 +</​code>​
 +
 +注意每个计数器如何使用不同的时钟。
 +
 +最后,我们将数据发送到PC:
 +<code verilog>
 +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));​
 +
 +</​code>​
  
 完整的设计 完整的设计
 <code verilog> <code verilog>
-module oscillo(clk,​ RxD, TxD, clk_flashdata_flash);+module oscillo(clk,​ RxD, TxD, clk_adcdata_adc);
 input clk; input clk;
 input RxD; input RxD;
 output TxD; output TxD;
  
-input clk_flash+input clk_adc
-input [7:0] data_flash;+input [7:0] data_adc;
  
 ///////////////////////////////////////////////////////////////////​ ///////////////////////////////////////////////////////////////////​
行 122: 行 229:
   startAcquisition <= 0;   startAcquisition <= 0;
  
-reg startAcquisition1;​ always @(posedge ​clk_flash) startAcquisition1 <= startAcquisition ; +reg startAcquisition1;​ always @(posedge ​clk_adc) startAcquisition1 <= startAcquisition ; 
-reg startAcquisition2;​ always @(posedge ​clk_flash) startAcquisition2 <= startAcquisition1;​+reg startAcquisition2;​ always @(posedge ​clk_adc) startAcquisition2 <= startAcquisition1;​
  
 reg Acquiring; reg Acquiring;
-always @(posedge ​clk_flash)+always @(posedge ​clk_adc)
 if(~Acquiring) if(~Acquiring)
   Acquiring <= startAcquisition2;​   Acquiring <= startAcquisition2;​
行 134: 行 241:
  
 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;
  
 reg Acquiring1; always @(posedge clk) Acquiring1 <= Acquiring; reg Acquiring1; always @(posedge clk) Acquiring1 <= Acquiring;
行 161: 行 268:
  
 ///////////////////////////////////////////////////////////////////​ ///////////////////////////////////////////////////////////////////​
-reg [7:0] data_flash_reg; always @(posedge ​clk_flashdata_flash_reg ​<= data_flash;+reg [7:0] data_adc_reg; always @(posedge ​clk_adcdata_adc_reg ​<= data_adc;
  
-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)
 ); );
行 172: 行 279:
  
 #### 1.3 触发 #### 1.3 触发
-检测一个上升沿+ 
 +我们的第一个触发机制非常简单 - 检测一个上升沿穿过一个设定好的阈值,我们使用的是8位的ADC,因此采集到的数据的范围为0x00到0xFF,我们先假设阈值为ox80. 
 + 
 +**检测一个上升沿** 
 +如果一个采样点的值高于设定的阈值,而前一个采样点的值低于该阈值,则进行触发! 
 <code verilog> <code verilog>
 reg Threshold1, Threshold2; reg Threshold1, Threshold2;
行 182: 行 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|仪器传输及控制协议]]