FPGAScope Project Documentation

Hold Left Click to Pan, Mouse Wheel to Zoom

Overview

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.

Technical Specifications

Features

Bug Fixes and Lessons Learned

Future Work

Presentation and Milestones

Display Pipeline

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.

VGA timing and pixel coordinates

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)
);

Waveform pixel generation

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

Pixel arbiter priority

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