Why this is one post
Most of these labs aren’t worth a dedicated writeup. A Verilog up/down counter is a homework assignment; so is a digital ohmmeter; so is a FreeRTOS scheduling exercise. Individually, none of them is a portfolio entry. Collectively, they are.
The point of an engineering education isn’t that any one lab matters. It’s that by the end of it, you’ve built things at every layer of the stack with your own hands — HDL up through application protocols — and the connections between layers become things you have intuitions about rather than things you read in a textbook. Spending a semester writing Verilog after a semester writing C-on-bare-metal after a semester writing real-time scheduling code is how you start to understand what an instruction is, what a peripheral does, what an interrupt costs. None of that is communicable from a single lab; all of it shows up in the breadth.
This post is a vertical-slice tour of that breadth, organized by where each lab sits in the stack. The labs themselves are brief; the connective tissue is what makes them worth reading together.
[TODO: add hero image — a multi-lab collage or a single iconic photo (oscilloscope on a bench, FPGA dev board, motor rig, whatever reads best)]
Layer 1 — HDL and digital design
The bottom of the stack, where signals are wires and time is measured in clock edges.
Verilog up/down counter. The first lab in the digital design sequence. A parameterized N-bit counter with synchronous reset, count-enable, and a direction input. Trivial as a description, formative as practice: it’s where you learn what blocking vs. non-blocking assignment actually means, why always @(posedge clk) is the right abstraction and always @(*) is the wrong one for state, and what gate-level simulation reveals that behavioral simulation hides. The lab where I first wrote a testbench that drove every input combination and watched the waveform viewer until I trusted it.
Traffic light controller. A small FSM with timed states, a pedestrian-crossing input, and an emergency-vehicle preemption input. The first time the design has more than one state machine talking to another state machine — the main signal controller, the pedestrian timer, the preemption handler — and the first time you have to think about how those FSMs synchronize when their clock domains and trigger conditions are different. Spanning real-world inputs (the pedestrian button bouncing, the emergency input arriving asynchronously) is where the work is.
Basic ALU. An arithmetic-logic unit supporting add, subtract, AND, OR, XOR, shift, comparison. The lab is straightforward; the lesson is in the testbench. An ALU is a pure combinational block — no clocks, no state — and so the verification strategy is exhaustive across the input space (small enough that you can sweep every input pair) rather than constrained-random across time. Different shape of testbench, same underlying methodology.
Coffee machine FSM. Pulled out into its own writeup — see that case study for the UVM-style verification methodology that grew out of this lab and stuck.
[TODO: figure placeholder — Verilog waveform, FPGA dev board, or ALU schematic]
The HDL labs are where I learned that digital design is verification first. The design is the easy part; convincing yourself the design is correct is the actual job. That stance carried directly into how I work on real circuits now — Fabrica’s three-tier verification, NeuroMatrix’s cross-scale validation, all of that is the same instinct scaled up.
Layer 2 — Embedded C and instrumentation
Moving up the stack: now there’s a CPU, there’s memory, there’s I/O, but no operating system. Everything happens because you wrote the code that makes it happen.
Digital ohmmeter in C. A bare-metal application reading a voltage divider through the MCU’s ADC and computing resistance with calibration tables stored in flash. The interesting part isn’t the ohmmeter — it’s that every step of the data path is yours: the ADC sample rate is something you configure, the calibration arithmetic is something you write, the display update happens because your code wrote to the display buffer at the right time. There’s no library between you and the silicon. The lab where you find out that a 12-bit ADC sample is a 16-bit integer with 4 leading zeros, that ADC reference voltages drift with temperature, that calibration math done in 16-bit fixed-point produces different results from the same math in floating-point, and that all of these matter.
Signal analyzer / oscilloscope. A more ambitious version of the same instinct: sample a signal at a known rate, display the waveform on an LCD, compute frequency-domain features (peak frequency, RMS, THD) in real time. This is where DSP stops being a textbook subject and starts being a thing you implement: writing an FFT in C on a Cortex-M, knowing why you’d choose a window function and which one, finding out experimentally why integer arithmetic overflows in the FFT butterflies and what to do about it. The lab where Nyquist becomes a constraint you actually trip over.
ADC sampling lab. A focused study of how ADC sampling actually works: aperture time, conversion time, sample-and-hold dynamics, why over-sampling and decimation can buy you effective resolution beyond the ADC’s native bit depth. Setting up DMA to ferry samples from the ADC to memory without CPU intervention, and seeing the difference in jitter between CPU-polled, interrupt-driven, and DMA-driven sampling. The same physical ADC produces noticeably different data quality depending on how you ask it for samples.
UART/SPI/I²C drivers. Bare-metal serial protocol drivers, written from the datasheet rather than from libraries. UART is the easy one — start bit, data bits, parity, stop bit, you can do it with a timer and a GPIO pin. SPI introduces clock-data relationships and the idea that the master generates the clock and the slave latches on the edge you tell it to. I²C is where it gets interesting: open-drain bus, multi-master arbitration, clock stretching, the protocol’s strange rules about START and STOP conditions. Writing these drivers from the datasheet — and watching them fail on a logic analyzer when the timing is off — is how you stop treating “the bus” as an abstraction and start treating it as a set of voltages over time.
[TODO: figure placeholder — logic analyzer trace, breadboard with sensors, MCU dev board]
The embedded-C labs are where I learned that abstractions cost you when they break, and they always break eventually. Knowing the layer below the abstraction is the only protection. The ohmmeter calibration math, the FFT integer overflow, the I²C clock stretch — these are all situations where the library would have hidden the problem, and writing the code yourself made the problem visible.
Layer 3 — Real-time systems
Up another layer: now there’s an RTOS, multiple tasks, scheduling decisions, and the new and exciting category of bugs that come from concurrency.
FreeRTOS PID motor controller. The lab that earns the layer. A brushed DC motor, a quadrature encoder for position feedback, a current sensor for torque feedback, and a FreeRTOS application running three tasks: a high-priority control task running the PID loop at 1 kHz, a medium-priority logging task writing samples to a buffer, and a low-priority comms task pushing telemetry over UART. Tuning the PID gains (proportional, integral, derivative) is the visible work; the invisible work is making sure the control task actually runs at 1 kHz — that no other task starves it, that interrupt latency doesn’t push its deadline, that the timing jitter on the control loop is bounded. The lab where you find out that “real-time” doesn’t mean “fast”; it means predictable, and predictable is much harder than fast.
Multitask scheduling lab. Several tasks with different priorities, periods, and computational loads, all running on the same MCU. Setting up rate-monotonic scheduling, observing where deadlines are met, deliberately creating overload conditions to see which tasks degrade first, and learning to use FreeRTOS’s runtime statistics to figure out where CPU is actually going. The lab where task starvation becomes a thing you’ve seen happen in front of you instead of a thing you read about.
IPC and semaphore correctness. Two tasks sharing a resource — a buffer, a peripheral, a counter — and the lab is to get the synchronization right. Starts with the obvious mutex-protected version, then moves into priority inheritance to prevent priority inversion, then introduces classic concurrency bugs (the producer-consumer with the wrong wakeup, the lost wakeup race, the deadlock from inverse lock ordering) and asks you to debug them. The lab where you stop trusting that your code is correct just because it runs once without crashing.
[TODO: figure placeholder — motor rig with encoder, oscilloscope showing PID step response, RTOS trace]
The RTOS labs are where I learned that correctness in concurrent systems is structural, not behavioral. A program that works on Tuesday doesn’t necessarily work on Wednesday if you didn’t argue it correct from the structure of the locks and the priorities. The discipline of thinking through who holds what lock at what priority, before running the code, is the only thing that scales.
Layer 4 — Control systems and signal processing
Now the stack widens out: from individual programs running on individual MCUs to control problems and signal-processing problems framed in their own terms.
PLC programming — OpenPLC, Structured Text, Ladder Logic. The control engineer’s stack rather than the embedded engineer’s. PLCs (Programmable Logic Controllers) are what runs industrial control — factory floors, water treatment plants, the elevator in your building. The programming languages are deliberately different from general-purpose code: Ladder Logic is graphical and looks like relay diagrams (because that’s what PLCs replaced), Structured Text is Pascal-like and meant to be readable by automation engineers rather than computer scientists. Both are designed to make control logic verifiable at a glance — you should be able to look at a rung of ladder logic and see what input conditions produce what output, without tracing through call graphs. The discipline is industrial in the literal sense: the program has to be inspectable and modifiable by a maintenance technician at 3 AM, not by the original author after coffee.
I wrote PLC programs implementing Boolean state machines for process control — interlocks, sequencing logic, fail-safe behaviors — and the experience reshaped how I think about real-time control code generally. PLCs scan their inputs in a fixed cycle (the scan time, typically a few milliseconds), execute the entire program, then update outputs, then start over. There’s no “interrupt-driven”; everything is polled, deterministically. It’s a stricter discipline than RTOS programming, and the strictness is the point.
MATLAB signal processing — Fourier, BER, NIST randomness. A series of labs on the mathematical side rather than the implementation side. Fourier series and transforms applied to real waveforms — taking an audio sample, decomposing it into frequency components, reconstructing it, observing what happens when you truncate the high frequencies. Bit Error Rate curves for digital modulation schemes — sweeping signal-to-noise ratio across QPSK, BPSK, 16-QAM and plotting how often the receiver guesses the wrong bit. NIST randomness tests applied to pseudo-random sequences, including the surprising result that most “random” sequences a naïve implementation produces fail at least one of the NIST tests.
The MATLAB labs are where the math you learned in signals-and-systems class becomes operational. Fourier theory is one thing on paper; running an FFT on a real signal and seeing the spectrum match the math is another. BER curves are a textbook concept until you generate them yourself for a modulation scheme you implemented from scratch.
[TODO: figure placeholder — OpenPLC ladder diagram, MATLAB spectrum plot, BER curve]
The control / signal-processing labs are where I learned that different domains have different right answers for the same problem. A C-on-bare-metal control loop and a PLC ladder rung might both solve the same physical control problem — and the right one depends on who maintains the system, what failure modes matter, and how it gets verified. Engineering judgment isn’t just “can I do it”; it’s “which way of doing it suits the context.”
Layer 5 — Networking
Top of the stack: now there’s a protocol, there are remote endpoints, and there are all the failure modes that come from the network being a physical medium that loses, duplicates, reorders, and delays.
Protocol stack implementation. A semester-spanning lab where you implement a small TCP-like protocol from scratch in C, layer by layer: framing on top of an unreliable channel, sequence numbers and acknowledgments for reliability, sliding-window flow control, retransmission timers, connection setup and teardown handshakes. You don’t implement real TCP — that would be a thesis — but you implement enough of TCP’s shape that you understand why it’s shaped that way. The first time you implement retransmission you’ll get the timer constant wrong; the first time you implement sliding windows you’ll find a corner case where the sender and receiver disagree about window size; the first time you implement teardown you’ll realize TCP’s four-way handshake isn’t ceremony, it’s load-bearing for the lost-FIN case.
Network simulation labs. Using discrete-event network simulators (ns-3 and similar) to study protocol behavior at scales you can’t reproduce on a bench. Setting up topologies with different link properties — bandwidth, latency, loss rate — and observing how TCP’s congestion control responds. Comparing TCP variants (Tahoe, Reno, CUBIC) under the same conditions. Watching what happens to a network of well-behaved TCP flows when one greedy UDP flow shows up alongside them. The labs are where you develop intuitions about protocol fairness, congestion collapse, and queue dynamics — concepts that are hard to learn from the textbook because the textbook can’t show you the time-domain plots.
[TODO: figure placeholder — Wireshark capture, ns-3 topology, protocol state machine diagram]
The networking labs are where I learned that protocols are conversations between distrustful parties under adversarial conditions. Every design choice in TCP — every retransmit, every checksum, every window-management subtlety — exists because some specific bad thing could happen on the wire if it didn’t. The same applies to every protocol I’ve designed since. You write the protocol that survives the failure modes you’ve imagined, and the failure modes you imagine are limited by how many you’ve seen.
What this layer-by-layer education actually gives you
Reading back through the labs, the pattern that emerges is the one worth naming explicitly: each layer’s right answers are constrained by the layer below it and the layer above it. The Verilog ALU exists in the context of an instruction set that will use it; the C-on-bare-metal ohmmeter exists in the context of an MCU whose silicon you have to respect; the FreeRTOS PID controller exists in the context of a physical motor with its own dynamics; the PLC program exists in the context of an industrial process and a maintenance technician; the protocol stack exists in the context of a network that loses things.
You can’t really design at one layer without understanding the layers next to it. The labs are how that understanding gets built — not by being told, but by doing the work at each layer in turn and noticing where the layers meet.
If you’re an EE/CE student deciding whether to take the verification methodology in your digital design class seriously, or whether to write your I²C driver from the datasheet versus pulling in a library, or whether to actually tune your PID gains rather than just turning them up until the motor responds — do the harder version. The harder version is what you’re paying tuition for. The labs are the only chance most engineering students get to work at every layer of the stack with someone looking over their shoulder, and that’s the part of the education that doesn’t come back.