Lab 11.46 — A message from one Zynq console to another¶
Idea¶
This is not another BER lab. It is the user-visible capstone of the main educational path:
board A console
↓
PS A
↓ AXI-Lite mailbox
PL A: packet source → existing QPSK TX
↓
AD936x A
↓ RF / cable
AD936x B
↓
PL B: existing QPSK RX → packet recovery
↓ AXI-Lite mailbox
PS B
↓
board B console
The learner types text on the first board and sees the same text on the second.
Why this matters more than another DSP primitive¶
By this point the course already contains mapper/RRC work, synchronization, CFO/timing, BER and two-board measurements. Without user data, however, those results can still feel like separate experiments.
Lab 11.46 turns them into one system:
- PS owns the user interface and packet-level software;
- PL owns the deterministic bit/symbol/sample datapath;
- AXI is the concrete boundary between them;
- AD936x transports a meaningful message instead of only a test pattern.
Prerequisites¶
Before this lab, understand:
- Zynq: where PS ends and PL begins;
- Lab 5.12: PS↔PL message mailbox;
- the existing two-board QPSK path and the Lab 11.45 result.
What is reused¶
Do not build a new PHY. Reuse the proven Block 11 path:
QPSK mapper
→ RRC TX
→ AD936x
→ RX matched filter / timing / carrier recovery
→ frame synchronization
→ recovered bits
The new work belongs before the mapper and after recovered bits:
PS message bytes → packet serializer → [existing modem] → packet decoder → PS message bytes
Baseline size limit¶
The existing working frame provides 256 payload bits = 32 bytes. Therefore the first radio version accepts one fixed 32-byte packet payload.
This is a deliberate baseline limit, not a mailbox shortcoming:
- the PS/PL mailbox holds up to 64 bytes;
- the v1 radio bridge uses one existing 32-byte QPSK payload;
- fragmentation over several frames is a separate extension after the first PASS.
Hello from board A fits in one frame.
Packet payload v1¶
Use the existing 32 payload bytes as:
byte 0 : application length N, 0…27
bytes 1..2 : sequence, uint16 little-endian
bytes 3..29 : UTF-8/application bytes, zero padded
bytes 30..31: CRC-16/CCITT over bytes 0..29
The maximum user message in v1 is therefore 27 bytes.
The numerical conventions are fixed, not implementation-defined:
| Item | Packet-v1 convention |
|---|---|
| CRC variant | CRC-16/CCITT-FALSE |
| Polynomial | 0x1021 |
| Initial value | 0xffff |
refin / refout |
false / false |
| Final XOR | 0x0000 |
| Standard check | CRC("123456789") = 0x29b1 |
| Sequence and CRC byte order | little-endian |
| 256-bit bus byte lanes | packet byte n maps to payload[8*n +: 8] |
| Bit serialization | least-significant bit of each byte first |
The mailbox already packs bytes little-endian into 32-bit words, so this mapping does not require a word swap at the PS/PL boundary.
For the first board implementation, packet assembly and the final CRC decision may remain in PS. The synthesizable codec is an executable cross-check and enables the self-contained RTL loopback from mailbox fields; the AXI integration must choose one boundary and must not encode an already assembled packet twice. PL transports the resulting fixed 32-byte payload deterministically as bits/symbols/samples.
That boundary is deliberately educational: it demonstrates a real PS/PL partition instead of moving the entire application into FPGA fabric.
Stage 1: software contract without RF¶
Run:
python tools/zynq_message_console.py --mock demo "Hello Zynq" --sequence 17
This is not RF simulation. It validates only the software-visible mailbox contract.
Stage 2: one-board PS↔PL hardware echo¶
Implement the AXI-Lite mailbox and prove:
Linux PS → AXI-Lite → PL echo → AXI-Lite → Linux PS
Expected result:
zynq$ sudo python3 tools/zynq_message_console.py --base <mailbox_addr> send "Hello PL" --sequence 1
TX sequence=1 bytes=8 payload="Hello PL"
and the same payload read back from the RX mailbox.
The physical address must come from the actual Vivado Address Editor map.
Stage 3: digital modem loopback¶
Replace the PL echo with the packet serializer plus the existing QPSK digital loopback:
PS → mailbox → packet bytes → QPSK TX → digital loopback → QPSK RX → mailbox → PS
RF is still absent, but the real modem datapath now carries application bytes.
The repository has an executable Stage 3 baseline:
qpsk_packet_v1.pyis the independent Python packet/CRC reference;qpsk_packet_v1_codec.vis the synthesizable ready/valid packet codec;qpsk_packet_frame_bridge.vprepends the existing 24-bit preamble and collects the normalized 256 payload bits after the existing frame synchronizer;qpsk_packet_digital_loopback.vreuses the existing mapper, RRC TX, matched filter, timing sampler, hard decision and quadrant-resolving frame sync;tb_qpsk_packet_digital_loopback.svsends"Hello from board A", sequence17, and requires the same payload withCRC=OK.
Run the focused regression:
python tools/run_block5_hdl_smoke.py --no-generate \
--test tb_qpsk_packet_v1_codec \
--test tb_qpsk_packet_digital_loopback
Expected evidence is strictly:
packet digital loopback PASS
All new packet blocks use one clock, synchronous active-high reset, and
ready/valid transfer. A producer holds payload stable while valid=1 and
ready=0; no CDC is introduced by this baseline.
This result does not prove AXI/Vivado integration, FPGA synthesis/timing, RF transport, or a two-board link.
Stage 4: two boards¶
Start with a safe conducted connection and the required attenuation; move to OTA only when appropriate.
Board A:
board-a$ sudo python3 tools/zynq_message_console.py --base <addr> send \
"Hello from board A" --sequence 17
TX sequence=17 bytes=18 payload="Hello from board A"
Board B:
board-b$ sudo python3 tools/zynq_message_console.py --base <addr> receive --wait 10
RX sequence=17 bytes=18 crc=OK payload="Hello from board A"
That is the primary acceptance criterion.
As run on the course bench (2026-10-14)¶
The first two-board message crossed the conducted RF path by a shorter route than the one above, reusing what already works on silicon:
- Board A replays a Lab 11.45 frame whose 256-bit payload is the packet-v1 frame, using
iio_writedev. - Board B receives it with its fabric QPSK modem. The host reads the recovered bits from the
bridge's decoded-bit register (
gp_ctrl[16]) and checks the CRC.
python blocks/block_11_integrated_sdr_project/python/two_board_message.py send "Hello from board A" --sequence 17 --whiten
TX sequence=17 bytes=18 payload="Hello from board A"
RX sequence=17 bytes=18 crc=OK payload="Hello from board A"
Ten messages × 20 receptions: 189 of 200 delivered intact, PER 5.5 %. All 11 failures were a missed lock or a CRC failure; no corrupted message was accepted (report).
Two lessons from getting there:
- A differential frame cannot simply be tiled unless its total phase increment is a whole turn; otherwise every frame boundary carries a phase jump that breaks the next preamble.
- A packet padded with zeros becomes a long run of one repeated symbol. PN9 whitening of the payload (on both ends) fixed it: 24 of 24 instead of 17 of 24.
From the board consoles, with no Python on the boards: sw/zmsg.c, built with the Vitis
arm-linux-gnueabihf-gcc -static, does the same on both ends.
board-a$ ./zmsg send "Hello from board A" --seq 17
TX sequence=17 bytes=18 payload="Hello from board A"
board-b$ ./zmsg receive
RX sequence=17 bytes=18 crc=OK payload="Hello from board A"
board-a$ ./zmsg stop
Ten messages × 20 receptions from the consoles: 199 of 200 delivered, one CRC failure, no wrong
message accepted (re-measured on 2026-10-15). The first measurement, 200 of 200, was made with a
version of zmsg that sometimes lost its start pulse and then read the previous frame again.
With a transmitter that repeats one message that can only turn a missed reception into a
counted one, so the number was rechecked after the fix (see Lab 11.48).
Board A here repeats the message cyclically, and each reception on board B catches one of many
copies. Sending a message once, the way a real radio does, needs a receiver that waits for it:
that is Lab 11.48, with zmsg send --once and zmsg receive --listen.
What is still open in this stage: the PS/PL mailbox feeding the modem. The mailbox passes its own echo test on board A (Lab 5.12), but the message above goes from the console to the modem without it.
Record in the report¶
- bitstream/build identity on both boards;
- mailbox physical base address;
- RF frequency, sample rate, attenuation and gains;
- transmitted string and UTF-8 byte length;
- sequence number;
- RX CRC result;
- number of repetitions;
- packet success rate;
- on failure, the raw 32-byte packet or a hex dump.
Minimum campaign¶
After the first successful message, run at least 100 short transmissions and record:
packets sent
packets received
CRC OK
CRC failed / timeout
PER
BER from the modem evidence remains useful, but PER / successful message delivery becomes the user-facing metric.
Do not add before the first PASS¶
Do not simultaneously add DMA, interrupts, a variable-length streaming protocol, fragmentation, encryption, OFDM, LoRa, or an Ethernet bridge.
First prove the simplest visible chain:
text → PS → PL → RF → PL → PS → text
Completion criterion¶
The lab is complete only with two-board hardware evidence:
- transmit command on Board A;
- received string on Board B;
- matching sequence;
CRC=OK;- documented RF settings;
- a short repeated campaign with measured PER.
Mock, RTL and digital-loopback results remain intermediate evidence until the two-board message has actually crossed the radio path.