A hardware-validated wireless MAC layer implementing 5G NR MAC concepts — HARQ, CQI→MCS link adaptation, and TDMA scheduling — built from scratch in C++ on ESP32, tested against a controlled ALOHA baseline, and instrumented with a real-time Python KPI dashboard.
Most networking projects use pre-built stacks. This one builds the MAC layer from the ground up — every retransmission decision, every scheduling grant, every timing slot is written explicitly in C++ with direct reference to 3GPP specifications.
The protocol implements the core MAC-layer mechanisms that sit inside a 5G NR modem:
| Concept | 3GPP Reference | What was implemented |
|---|---|---|
| TDMA superframe | TS 38.211 (μ=0, scaled 10×) | Beacon + slot + guard interval structure |
| HARQ 8 processes | TS 38.321 §5.4 | NDI toggling, RV rotation {0,2,3,1} |
| CQI → MCS adaptation | TS 38.214 §5.1.1 | Outer-loop LA, step-up/down on ACK/NACK |
| UL DCI grant scheduling | TS 38.212 DCI format 0_1 | BSR-aware gNB scheduler in beacon |
| BSR / Scheduling Request | TS 38.321 §5.5 | Buffer reporting, SR on missing grant |
| BLER tracking | TS 38.314 | Per-UE block error rate, first-attempt rate |
This is not a simulation. The protocol runs on real ESP32 hardware, over a live 2.4 GHz radio link, with all metrics captured from actual Serial output.
┌─────────────────┐ 2.4 GHz ESP-NOW ┌─────────────────┐
│ ESP32 #1 │ ◄──────────────────────────────── │ ESP32 #2 │
│ Coordinator │ ──────────────────────────────────►│ Sensor Node │
│ (gNB role) │ Star Topology │ (UE role) │
└─────────────────┘ └─────────────────┘
│
│ Serial (230400 baud)
▼
PC running plot_metrics.py
(live 8-panel KPI dashboard)
- 2 × ESP32 Dev Module — no external radio modules needed
- Topology: Star (one coordinator, one sensor node)
- Radio: ESP-NOW at 2.4 GHz, channel 6
- Baud rate: 230400 (analytically derived to fit within superframe dead time)
┌──────────┬──────────┬─────┬──────────┬─────┬──────────┬─────┐
│ BEACON │ SLOT 0 │ G │ SLOT 1 │ G │ SLOT 2 │ G │
│ 10 ms │ 10 ms │ 3ms │ 10 ms │ 3ms │ 10 ms │ 3ms │
└──────────┴──────────┴─────┴──────────┴─────┴──────────┴─────┘
←─────────────────── 62 ms (one hyper-frame) ──────────────────→
Timing is scaled 10× vs real 5G NR μ=0 slots (1 ms → 10 ms) to accommodate ESP32/FreeRTOS scheduler jitter (~420 µs σ). The guard interval is analytically sized at 3 ms to provide 7× jitter margin.
Two controlled runs on the same hardware, same distance, same channel:
Run 1 — TDMA (proposed protocol)
- Full MAC stack active: slot coordination, HARQ, link adaptation, DCI grants
- Duration: 54 minutes | 52,750 hyper-frames
Run 2 — ALOHA baseline
- Slot-wait removed, no coordination — node transmits immediately on new frame
- HARQ retransmissions disabled (MAX_RETRIES = 0)
- Duration: 15 minutes | 13,416 hyper-frames
The ALOHA run is the control experiment. Every improvement number below is the delta between these two runs on identical hardware.
| Metric | ALOHA Baseline | TDMA (This Project) | Improvement |
|---|---|---|---|
| BLER | 98.5% | 8.6% | 11.5× better |
| PDR | 1.5% | 91.4% | 60.9× better |
| Throughput | 367 bytes/s | 1,747 bytes/s | 4.8× better |
| MCS mean | 1.2 | 5.9 | +4.7 MCS levels |
| Latency p95 | — | 19.9 ms | — |
| Latency p99 | — | 21.6 ms | — |
| Worst-case bound | Unbounded | 62 ms | Deterministic |
| 1st-attempt decode | — | 100% | — |
BLER 8.6% sits just below the 10% NR outer-loop target defined in 3GPP standards — the protocol operates in the correct design region without over-engineering.
100% first-attempt decode rate on received packets was the most diagnostic result. It means every packet that arrived at the coordinator decoded correctly on the first try — the 8.6% BLER was entirely from whole-slot timing misses, not RF corruption. HARQ retransmission would not have helped these losses; the root cause is ESP32 FreeRTOS scheduler jitter causing occasional slot boundary slips.
4.8× throughput improvement comes from two compounding effects: (1) slot coordination eliminating timing misses, and (2) link adaptation climbing from MCS 1.2 (ALOHA, barely functional) to MCS 5.9 (TDMA, near-optimal for -50 dBm RSSI).
Deterministic 62 ms worst-case latency is a provable bound — no packet can take longer than one hyper-frame. Under ALOHA, worst-case latency is theoretically unbounded.
This surprised me initially. With only one transmitting node, collisions with other nodes are impossible. The 98.5% BLER under ALOHA is caused entirely by slot-timing mismatch — without the slot-wait, the node transmits immediately when a new frame starts, but the coordinator's receive window for that UE opens at a fixed offset from the beacon. The node transmits at the wrong time, the coordinator isn't listening, and the packet is missed. This is the exact problem TDMA solves.
tdma_wsn_5gnr/
├── common.h ← MAC header, frame structs, HARQ params,
│ MCS table, CQI→MCS mapping, timing constants
├── coordinator/
│ └── coordinator.ino ← gNB: beacon TX, HARQ manager, DCI scheduler,
│ link adaptation, BLER tracking, CSV metrics
├── node/
│ └── node.ino ← UE: RRC state machine, HARQ client, CQI
│ measurement, BSR, SR, slot synchronisation
└── tools/
└── plot_metrics.py ← Live Serial capture + 8-panel KPI dashboard
- Arduino IDE 1.8.19+ with ESP32 board package v2.0.11+
- Python 3.10+ with
pip install pyserial matplotlib numpy pandas
- Flash
coordinator/coordinator.inoto ESP32 #1 - Set
#define NODE_ID 1innode/node.ino, flash to ESP32 #2 - Both boards boot and join automatically — no configuration needed
python3 tools/plot_metrics.py --port COM13 --baud 230400 --save log.txtLive 8-panel dashboard refreshes every 5 seconds. Close the window to save the final PNG and print the performance summary.
In common.h: set #define MAX_RETRIES 0
In node.ino: remove the slot-wait spin and slot window check (see comments in code)
Reflash node only. Run for 10 minutes. Compare BLER.
| Panel | What it shows |
|---|---|
| BLER over time | Rolling 20-frame block error rate — target < 10% |
| Latency CDF | p50 / p95 / p99 with markers |
| Throughput vs MCS | Operating points vs theoretical maximum |
| RSSI + CQI | Signal quality over time with CQI overlay |
| HARQ retry distribution | % of TBs at TX0 / TX1 / TX2 |
| CQI histogram | Distribution of channel quality reports |
| MCS over time | Link adaptation convergence curve |
| Resource grid heatmap | Slot utilisation — green=ACK, red=BLER |
Most wireless projects either use a pre-built networking library (no understanding of the protocol) or simulate everything in software (no hardware validation). This project:
- Builds the entire MAC layer from scratch with explicit 3GPP spec references
- Runs on real hardware with a live radio link
- Has a controlled experiment (TDMA vs ALOHA) with quantified results
- Derives timing parameters analytically (baud rate, guard interval, print scheduling) rather than picking arbitrary values
- Produces industry-standard KPIs (BLER, latency CDF, MCS convergence) rather than just "it worked"
This is the MAC layer of a three-project wireless stack:
Project 1 — OFDM PHY Transceiver (FPGA, Verilog)
↓ CQI / BER curve → feeds LNA spec
Project 2 — This project (MAC layer, C++, ESP32)
↓ MAC FSM → RTL specification
Project 3 — RF Receiver Front-End (LNA + Mixer + IF Filter, Cadence Virtuoso)
The CQI-to-MCS interface between Project 1 and Project 2, and the link budget derivation feeding into Project 3, mirror the PHY↔MAC boundary in a real modem SoC.
- 3GPP TS 38.321 — NR MAC specification (HARQ, BSR, SR)
- 3GPP TS 38.214 — Physical layer procedures (MCS/CQI tables)
- 3GPP TS 38.211 — Frame structure and numerology
- 3GPP TS 38.314 — NR layer 2 measurements (BLER, throughput KPIs)