Seven labs, one FPGA: from a blinking LED to a DDR3 video pipeline.
Each lab added a layer to the same board: serial protocols, then video timing, then a camera, then real-time image filtering, then external memory. By the end, 720p video crosses three clock domains and a DDR3 chip on its way to the screen, with timing met at every step.
00At a glance
Mine
The RTL for each lab's modules: counters and PWM, debouncing, SPI and UART, video timing, TMDS encoding, Pong, the frame buffer and pipeline alignment, center of mass, line buffers and convolution, the DDR3 traffic generator and command FIFO; plus my cocotb testbenches.Course-provided
Top-level skeletons and pin constraints, clock-wizard and memory wrappers, the camera front end, the kernel coefficient table and a divider.Open source
The DDR3 memory controller in Lab 7, which I drive over its Wishbone interface.01The progression
Counters, PWM and a seven-segment decoder
The vocabulary: a parameterized counter, a pulse-width modulator built on it, an RGB LED controller and a binary-to-seven-segment decoder, each with a testbench before it touched the board.
Talking to the outside world over SPI
Button debouncing and edge detection, a multiplexed seven-segment controller, and an SPI controller that reads an external ADC: 17-bit transactions on a 2 MHz serial clock, three channels sampled round-robin every millisecond.
UART both ways, and five seconds of audio in block RAM
A UART transmitter and receiver at 115,200 baud. A microphone is sampled at 8 kHz through the SPI ADC and streamed to a laptop; audio sent back from the laptop lands in a 40,000-sample block RAM and plays through an 8-bit PWM output with a 392 kHz carrier.
720p HDMI from first principles, then Pong
A video timing generator for 1280×720 at 60 Hz, an 8b/10b TMDS encoder that tracks running disparity, and a serializer clocked at five times the pixel rate from an MMCM with a 742.5 MHz VCO. Test patterns first, then a playable Pong drawn in hardware.
A camera, a frame buffer and tracking
Camera pixels are written into a 320×180, 16-bit dual-clock block RAM on a 200 MHz clock and read on the 74.25 MHz pixel clock, so the memory itself is the clock-domain crossing. A thresholded color mask feeds a center-of-mass module that steers a crosshair. Every signal is delayed to match the memory's two-cycle latency.
Real-time 3×3 convolution
A four-row line buffer in block RAM presents three vertically aligned rows; a three-stage pipelined convolution multiplies, sums, shifts and clamps each color channel. Six filters run in parallel on the 320-wide stream and a seventh blurs the full 1280-wide image, all at pixel rate.
A 720p frame buffer in DDR3
A full 1280×720 frame does not fit in block RAM. Pixels are packed eight to a 128-bit word, cross into the memory controller's clock domain through an asynchronous FIFO, are written to and read from DDR3 by a traffic generator that interleaves both directions, and cross again to the pixel clock: 115,200 words per frame.
02Serial: one frame, bit by bit
The receiver is a five-state machine: idle, start, data, stop, and a one-cycle valid pulse. At 115,200 baud from a 100 MHz clock, each bit lasts 868 clock cycles, and the asynchronous input passes through a two-flip-flop synchronizer before the state machine sees it.
03Video: the raster nobody sees
A 720p frame is not 1280×720 pixel slots. It is 1650×750, and the invisible margin carries the sync pulses that tell the monitor where lines and frames begin. Multiply by 60 frames per second and you get the pixel clock exactly: 74.25 MHz. Each pixel then becomes ten bits per color channel, sent serially at five times that rate on both clock edges.
04Convolution at pixel rate
A 3×3 filter needs three rows at once from a stream that arrives one pixel at a time. The line buffer supplies them; the convolution keeps a 3×3 window in registers and computes one output per clock. This is the same arithmetic the hardware performs, including the 5-6-5 bit channels, the right shift and the clamp:
05Three clock domains and a DDR3 chip
The camera produces pixels on its own schedule, the monitor consumes them on another, and the memory controller runs on a third clock. My traffic generator sits in the middle: it alternates read and write requests, records each in a command FIFO, and throttles reads when the output FIFO approaches full, so neither side ever waits on the other.