FPGAScope is a digital oscilloscope implemented on an FPGA with a VGA UI, a PS/2 input for controls, and an external ADC interface. The system captures samples into a ring buffer, applies trigger logic, computes measurements, and renders waveforms, axes, and text overlays in real time. The code for this project was written form the ground up, this means that the VGA Driver, PS/2 Driver, and ADC Driver for the ltc2308 were written from scratch.
I find the display pipeline to be most important, and so have provided some insight on how it works. The VGA display pipeline consists of the following stages: generating VGA timing signals, producing pixel data for each layer (waveform, grid, triggers, cursors, UI, text), and finally combining these layers in a pixel arbiter to produce the final VGA output. Below are code snippets illustrating key parts of the implementation.
The VGA driver generates a 25MHz pixel clock, horizontal/vertical counters, and sync pulses. The output coordinates xOrd and yOrd are used by every pixel generator to decide whether to draw.
// vgaDriver: pixel clock and counters
pixelClock pc (.clock(clock50MHz), .resetn(resetn), .pulse(pixelClk));
hCounter hc (
.pixelClock(pixelClk),
.resetn(resetn),
.hTotal(hTotal),
.xOrd(xCoord),
.hEnd(hEnd)
);
vCounter vc (
.pixelClock(pixelClk),
.resetn(resetn),
.hEnd(hEnd),
.vTotal(vTotal),
.yOrd(yCoord)
);
syncGenerator sg (
.xOrd(xCoord),
.yOrd(yCoord),
.hVisible(resolutionX),
.hSyncStart(hSyncStart),
.hSyncEnd(hSyncEnd),
.vVisible(resolutionY),
.vSyncStart(vSyncStart),
.vSyncEnd(vSyncEnd),
.hSync(hSync),
.vSync(vSync),
.visible(vis)
);
The waveform generator draws the trace by connecting the current sample to the previous one and coloring pixels in the vertical span between them. This avoids gaps on steep edges.
// waveformGenerator: connect consecutive samples
wire [9:0] yMin = (scaledY < prevScaledY) ? scaledY : prevScaledY;
wire [9:0] yMax = (scaledY > prevScaledY) ? scaledY : prevScaledY;
wire drawPixel = (yOrd + 10'd1 >= yMin) && (yOrd <= yMax + 10'd1);
always @(*) begin
if (visible && inDisplayArea && drawPixel) begin
pixelR = colorR;
pixelG = colorG;
pixelB = colorB;
end else begin
pixelR = 8'h00;
pixelG = 8'h00;
pixelB = 8'h00;
end
end
The pixel arbiter picks the top-most non-transparent layer in priority order. Text sits above UI, then cursors, triggers, waveform, and grid. The result is a clean composite VGA output.
// pixelArbiter: priority mux
if (!visible) begin
vgaR = 8'h00; vgaG = 8'h00; vgaB = 8'h00;
end else if (textActive) begin
vgaR = textR; vgaG = textG; vgaB = textB;
end else if (uiActive) begin
vgaR = uiR; vgaG = uiG; vgaB = uiB;
end else if (cursorActive) begin
vgaR = cursorR; vgaG = cursorG; vgaB = cursorB;
end else if (trigActive) begin
vgaR = trigR; vgaG = trigG; vgaB = trigB;
end else if (waveActive) begin
vgaR = waveR; vgaG = waveG; vgaB = waveB;
end else if (axisActive) begin
vgaR = axisR; vgaG = axisG; vgaB = axisB;
end else begin
vgaR = 8'h00; vgaG = 8'h30; vgaB = 8'h40;
end