Krzysztof Cieślik 9c0e280fab
Some checks failed
CI / Build firmware (push) Failing after 38s
CI / Check formatting (push) Successful in 12s
CI / Static analysis (push) Successful in 11s
CI / Build documentation (push) Failing after 13s
Add firmware artifacts, memory summary, and Doxygen gh-pages deploy
CMakeLists.txt: generate firmware.map via -Wl,-Map.

CI build job:
- Writes per-section size table to the Gitea job summary
  (visible in the Actions UI after each run).
- Uploads firmware.hex and firmware.map as a downloadable artifact
  named firmware-<sha>.

CI docs job:
- On push to main, force-pushes docs/html/ as an orphan commit to
  the gh-pages branch.  Gitea Pages must be enabled in site admin
  for the HTML to be served; the branch is always available via the
  repo file browser regardless.
2026-05-21 23:20:55 +02:00
2026-05-21 23:15:56 +02:00
2026-05-21 23:15:56 +02:00

ptt-fhss

Bare-metal PTT (Push-to-Talk) firmware for the Seeed XIAO BLE (nRF52840) with frequency-hopping spread spectrum (FHSS) on the 2.4 GHz band.

Designed to be low-power, hard to detect, and built with a fully open toolchain - no Zephyr, no Nordic SDK, no SoftDevice.

Hardware

Component Part
Target Seeed XIAO BLE (nRF52840)
Programmer RP2040 running DAPLink

How it works

FHSS

The radio hops across 40 channels (2402-2480 MHz, 2 MHz steps) every 2 ms. The hopping sequence is generated by AES-128-ECB keyed with a shared secret - both devices derive identical sequences independently without any synchronisation traffic. To an observer with an SDR the transmissions appear as short, scattered impulses with no identifiable pattern.

Low power

The nRF52840 stays in SYSTEM_ON sleep (WFI) with the DC/DC converter active (~1.5 uA quiescent). A GPIOTE edge event on the PTT button wakes the CPU; the radio is active only during the burst.

Register access

All hardware register writes use typed bitfield unions defined in include/regs.h rather than raw bit-shift expressions. The union layout is guaranteed correct with arm-none-eabi-gcc (LSB-first bitfields on Cortex-M).

Toolchain

Everything that produces the binary runs inside a container. The host only needs tools that talk to hardware (pyocd) or manage the build (just).

Tool Purpose Install
podman or docker Container runtime distro package
just Task runner cargo install just or distro package
pyocd Flash / debug via DAPLink pip install pyocd
git Source control + submodules distro package

Inside the container: debian:bookworm-slim + gcc-arm-none-eabi from apt + cmake + ninja.

Quick start

git clone <repo-url> ptt
cd ptt
git submodule update --init --depth=1

just build    # build firmware.hex inside container
just flash    # flash via DAPLink (host pyocd)

On first run just build pulls the container image and compiles. Subsequent builds reuse the cached image and only recompile changed files.

Tasks

just build       compile firmware inside the container
just flash       build + flash via DAPLink
just gdbserver   start pyocd GDB server on port 3333
just clean       remove build/
just clean-all   remove build/ and the container image

Project structure

.
|-- Dockerfile               container image (debian + arm-gcc from apt)
|-- justfile                 build / flash / debug tasks
|-- CMakeLists.txt
|-- cmake/
|   \-- arm-none-eabi.cmake  CMake toolchain file
|-- link/
|   \-- nrf52840.ld          linker script (no SoftDevice, Flash @ 0x00000000)
|-- include/
|   |-- regs.h               hardware register bitfield unions
|   |-- radio.h
|   |-- fhss.h
|   \-- power.h
|-- src/
|   |-- startup.c            vector table (64 entries) + Reset_Handler
|   |-- main.c
|   |-- radio.c              RADIO peripheral driver (stub)
|   |-- fhss.c               AES-ECB hopping sequence generator
|   \-- power.c              DC/DC, GPIOTE wakeup, WFI sleep
\-- vendor/
    |-- nrfx/                Nordic HAL headers, pinned to v2.9.0 (includes mdk/)
    |-- CMSIS_5/             ARM core headers (core_cm4.h etc.)
    \-- tiny-aes-c/          Public domain AES-128 implementation

Before first flash

The XIAO BLE ships with a SoftDevice which occupies the start of Flash. Erase it before flashing bare-metal firmware:

pyocd erase --target nrf52840 --chip

Status

Module State
Startup / vector table done
Linker script done
Power management (sleep + wakeup) done
FHSS sequence generator done
RADIO driver stub - TX loop not yet implemented
Sync protocol not started
Description
No description provided
Readme 412 KiB
Languages
C 83.6%
Just 8.5%
CMake 6.8%
Dockerfile 1.1%