Dual-port RAM
Now that the trigger is available, we need a dual-port RAM to store the data.
Notice how each side of the RAM uses a different clock.
ram512 ram_flash(
.data(data_flash_reg), .wraddress(wraddress), .wren(Acquiring), .wrclock(clk_flash),
.q(ram_output), .rdaddress(rdaddress), .rden(rden), .rdclock(clk)
);
The ram address buses are created easily using binary counters.
First the write address:
reg [8:0] wraddress;
always @(posedge clk_flash) if(Acquiring) wraddress <= wraddress + 1;
and the read address:
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
Notice how each counter uses a different clock.
Finally we send data to the 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));
The complete design
module oscillo(clk, RxD, TxD, clk_flash, data_flash);
input clk;
input RxD;
output TxD;
input clk_flash;
input [7:0] data_flash;
///////////////////////////////////////////////////////////////////
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_flash) startAcquisition1 <= startAcquisition ;
reg startAcquisition2; always @(posedge clk_flash) startAcquisition2 <= startAcquisition1;
reg Acquiring;
always @(posedge clk_flash)
if(~Acquiring)
Acquiring <= startAcquisition2;
else
if(&wraddress)
Acquiring <= 0;
reg [8:0] wraddress;
always @(posedge clk_flash) 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_flash_reg; always @(posedge clk_flash) data_flash_reg <= data_flash;
ram512 ram_flash(
.data(data_flash_reg), .wraddress(wraddress), .wren(Acquiring), .wrclock(clk_flash),
.q(ram_output), .rdaddress(rdaddress), .rden(rden), .rdclock(clk)
);
endmodule
Digital oscilloscope - part 3
Our first trigger is simple - we detect a rising edge crossing a fixed threshold. Since we use an 8-bit ADC, the acquisition range goes from 0x00 to 0xFF.
So let's set the threshold to 0x80 for now.
Detecting a rising edge
If a sample is above the threshold, but the previous sample was below, trigger!
reg Threshold1, Threshold2;
always @(posedge clk_flash) Threshold1 <= (data_flash_reg>=8'h80);
always @(posedge clk_flash) Threshold2 <= Threshold1;
assign Trigger = Threshold1 & ~Threshold2; // if positive edge, trigger!
Mid-display trigger
One great feature about a digital scope is the ability to see what's going on before the trigger.
How does that work?
The oscilloscope is continuously acquiring. The oscilloscope memory gets overwritten over and over - when we reach the end, we start over at the beginning. But if a trigger happens, the oscilloscope keeps acquiring for half more of its memory depth, and then stops. So it keeps half of its memory with what happened before the trigger, and half of what happened after.
We are using here a 50% or "mid-display trigger" (other popular settings would have been 25% and 75% settings, but that's easy to add later).
The implementation is easy. First we have to keep track of how many bytes have been stored.
reg [8:0] samplecount;
With a memory depth of 512 bytes, we first make sure to acquire at least 256 bytes, then stop counting but keep acquiring while waiting for a trigger. Once the trigger comes, we start counting again to acquire 256 more bytes, and stop.
reg PreTriggerPointReached;
always @(posedge clk_flash) PreTriggerPointReached <= (samplecount==256);
The decision logic deals with all these steps:
always @(posedge clk_flash)
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_flash) if(Acquiring) wraddress <= wraddress + 1;
always @(posedge clk_flash) if(PreOrPostAcquiring) samplecount <= samplecount + 1;
reg Acquiring1; always @(posedge clk) Acquiring1 <= AcquiringAndTriggered;
reg Acquiring2; always @(posedge clk) Acquiring2 <= Acquiring1;
assign AcquisitionStarted = Acquiring2;
Notice that we took care of remembering where the trigger happened. That's used to determine the beginning of the sample window in the RAM to send to the PC.
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
With this design, we finally get a useful oscilloscope. We just need to customize it now.
Digital oscilloscope - part 4
Now that the oscilloscope skeleton is working, it is easy to add more functionality.
Edge-slope trigger
Let's add the ability to trigger on a rising-edge or falling-edge. Any oscilloscope can do that.
We need one bit of information to decide with direction we want to trigger on. Let's use bit-0 of the data sent by the PC.
assign Trigger = (RxD_data[0] ^ Threshold1) & (RxD_data[0] ^ ~Threshold2);
That was easy.
More options
Let's add the ability to control the trigger threshold. That's an 8-bits value. Then we require horizontal acquisition rate control, filtering control... That requires multiple control bytes from the PC to control the oscilloscope.
The simplest approach is to use the "async_receiver" gap detection feature. The PC sends control bytes in burst, and when it stops sending, the FPGA detects it and assert an "RxD_gap" signal. 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];
We've also added a 4 bits register (HDiv[3:0]) to control the horizontal acquisition rate. When we want to decrease the acquisition rate, either we discard samples coming from the ADC, or we filter/downsample them at the frequency we are interested in.
[
本帖最后由 老夫子 于 2009-10-29 17:29 编辑 ]