Implement full FHSS TX/RX, real AES key, button read, slot sync
All checks were successful
CI / Build firmware (push) Successful in 36s
CI / Check formatting (push) Successful in 12s
CI / Static analysis (push) Successful in 12s
CI / Build documentation (push) Successful in 15s

radio_tx_burst(): captures current slot, calls fhss_next_channel() to
advance the sequence, sets channel, transmits ptt_frame_t, then holds
the dwell window via TIMER0 so the receiver has the full 2 ms to listen.

radio_rx_burst(): advances to the same channel, enables RX, waits for
EVENTS_END or TIMER0 COMPARE0 expiry. On a valid CRC the ptt_frame_t
is returned; caller syncs FHSS with fhss_set_slot(frame.slot + 1).

TIMER0: configured once in radio_init() (MODE=Timer, BITMODE=16-bit,
PRESCALER=4 -> 1 MHz tick, CC[0]=2000 -> 2 ms, COMPARE0_STOP shortcut).

fhss.c: replace zero key with a non-trivial 128-bit value; add
fhss_set_slot() and fhss_get_slot() for RX synchronisation.

power.c: add BUTTON_ACTIVE_LOW flag (default 1 = pull-up + GND button);
configure PIN_CNF accordingly; implement power_button_pressed() via
NRF_P0->IN.

regs.h: add gpio_pin_cnf_t with GPIO_PULL_* constants.

main.c: tight PTT loop -- radio_tx_burst() while button held, otherwise
radio_rx_burst() with slot sync on reception. No WFI in RX mode.
This commit is contained in:
Krzysztof Cieślik
2026-05-22 00:40:49 +02:00
parent dca80c7821
commit c9320eb9db
8 changed files with 232 additions and 42 deletions

View File

@@ -10,18 +10,29 @@
#pragma once #pragma once
#include <stdint.h> #include <stdint.h>
/** Dwell time per channel in milliseconds. */
#define FHSS_DWELL_MS 2u
/** Number of channels in the hopping sequence. */
#define FHSS_CHANNELS 40u
/** @brief Reset the slot counter to zero. */ /** @brief Reset the slot counter to zero. */
void fhss_init(void); void fhss_init(void);
/** /**
* @brief Return the next channel in the hopping sequence. * @brief Return the next channel in the hopping sequence and advance the slot.
*
* Encrypts the current slot counter big-endian with AES-128-ECB, returns
* @c block[0] % 40, and advances the slot counter.
*
* @return Channel index in [0, 39]. * @return Channel index in [0, 39].
*/ */
uint8_t fhss_next_channel(void); uint8_t fhss_next_channel(void);
/** @brief Advance the slot counter without transmitting (receiver side). */ /** @brief Advance the slot counter by one (receiver side, no packet received). */
void fhss_sync_tick(void); void fhss_sync_tick(void);
/**
* @brief Force the slot counter to a specific value for RX synchronisation.
* @param s New slot value.
*/
void fhss_set_slot(uint32_t s);
/** @brief Return the current slot counter value. */
uint32_t fhss_get_slot(void);

View File

@@ -1,10 +1,11 @@
/** /**
* @file power.h * @file power.h
* @brief Power management: DC/DC regulator, GPIOTE wakeup, SYSTEM_ON sleep. * @brief Power management: DC/DC regulator, GPIOTE wakeup, WFI sleep.
*/ */
#pragma once #pragma once
#include <stdbool.h>
/** @brief Enable the DC/DC converter and configure GPIOTE wakeup on the PTT button. */ /** @brief Enable the DC/DC converter, configure GPIO input and GPIOTE wakeup on the PTT button. */
void power_init(void); void power_init(void);
/** /**
@@ -14,3 +15,9 @@ void power_init(void);
* interrupt fires (button press) and resumes from here. * interrupt fires (button press) and resumes from here.
*/ */
void power_sleep_until_button(void); void power_sleep_until_button(void);
/**
* @brief Return the current state of the PTT button.
* @return true when the button is pressed.
*/
bool power_button_pressed(void);

View File

@@ -3,9 +3,23 @@
* @brief RADIO peripheral driver -- NRF_1Mbit proprietary mode. * @brief RADIO peripheral driver -- NRF_1Mbit proprietary mode.
*/ */
#pragma once #pragma once
#include <stdbool.h>
#include <stdint.h> #include <stdint.h>
/** @brief Configure the RADIO peripheral (mode, packet format, address, CRC, power, channel). */ /**
* @brief PTT packet transmitted on every FHSS hop.
*
* The receiver uses @p slot to resynchronise its FHSS counter after
* receiving the first packet.
*/
typedef struct __attribute__((packed)) {
uint32_t slot; /**< Sender's FHSS slot number at time of transmission. */
uint8_t flags; /**< Bitmask: PTT_FLAG_ACTIVE when voice channel is open. */
} ptt_frame_t;
#define PTT_FLAG_ACTIVE 0x01u /**< PTT button is held on the transmitting side. */
/** @brief Configure the RADIO peripheral (mode, packet format, address, CRC, power). */
void radio_init(void); void radio_init(void);
/** /**
@@ -18,13 +32,30 @@ void radio_set_channel(uint8_t ch);
* @brief Transmit one packet synchronously. * @brief Transmit one packet synchronously.
* *
* Loads @p data into the internal packet buffer, asserts TASKS_TXEN, and * Loads @p data into the internal packet buffer, asserts TASKS_TXEN, and
* returns after EVENTS_END fires. The RADIO is DISABLED automatically via * returns after EVENTS_END fires. RADIO is DISABLED automatically via the
* the END_DISABLE shortcut before the function returns. * END_DISABLE shortcut before the function returns.
* *
* @param data Payload bytes. * @param data Payload bytes.
* @param len Payload length (0-255 bytes). * @param len Payload length (0-255 bytes).
*/ */
void radio_tx(const uint8_t *data, uint8_t len); void radio_tx(const uint8_t *data, uint8_t len);
/** @brief Transmit a burst with FHSS hopping (not yet implemented). */ /**
* @brief Transmit one FHSS hop: advance channel, send PTT frame, hold dwell time.
*
* Call repeatedly in a loop while the PTT button is held. Each call occupies
* exactly FHSS_DWELL_MS milliseconds.
*/
void radio_tx_burst(void); void radio_tx_burst(void);
/**
* @brief Receive one FHSS hop: advance channel, listen for FHSS_DWELL_MS ms.
*
* If a packet with a valid CRC arrives during the dwell window, @p frame_out
* is filled and the function returns true. The caller should then call
* fhss_set_slot(frame_out->slot + 1) to synchronise the hopping sequence.
*
* @param frame_out Destination for the received frame (must not be NULL).
* @return true if a valid packet was received, false on timeout or CRC error.
*/
bool radio_rx_burst(ptt_frame_t *frame_out);

View File

@@ -153,3 +153,24 @@ typedef union {
} bit; } bit;
uint32_t reg; uint32_t reg;
} radio_shorts_t; } radio_shorts_t;
/* GPIO */
/** @brief GPIO PIN_CNF[n]: pin configuration register. */
typedef union {
struct {
uint32_t DIR : 1; /* [0] 0=Input 1=Output */
uint32_t INPUT : 1; /* [1] 0=Connect 1=Disconnect */
uint32_t PULL : 2; /* [3:2] 0=Disabled 1=Pulldown 3=Pullup */
uint32_t : 4; /* [7:4] reserved */
uint32_t DRIVE : 3; /* [10:8] 0=S0S1 standard drive */
uint32_t : 5; /* [15:11] reserved */
uint32_t SENSE : 2; /* [17:16] 0=Disabled 2=SenseHigh 3=SenseLow */
uint32_t : 14; /* [31:18] reserved */
} bit;
uint32_t reg;
} gpio_pin_cnf_t;
#define GPIO_PULL_DISABLED 0u
#define GPIO_PULL_PULLDOWN 1u
#define GPIO_PULL_PULLUP 3u

View File

@@ -4,12 +4,12 @@
#include "fhss.h" #include "fhss.h"
#include <aes.h> #include <aes.h>
#define FHSS_CHANNELS 40u /*
#define FHSS_DWELL_MS 2u * Shared secret -- both devices must carry the same key.
* Change before deployment; never commit a production key to source control.
/* TODO: replace with a real shared secret before deployment */ */
static const uint8_t shared_key[16] = { static const uint8_t shared_key[16] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA3, 0x4F, 0x2C, 0x8B, 0xE1, 0x76, 0x0D, 0x95, 0x4A, 0xB8, 0x3E, 0x72, 0x1F, 0xC9, 0x56, 0x0A,
}; };
static uint32_t slot; static uint32_t slot;
@@ -41,3 +41,13 @@ void fhss_sync_tick(void)
{ {
slot++; slot++;
} }
void fhss_set_slot(uint32_t s)
{
slot = s;
}
uint32_t fhss_get_slot(void)
{
return slot;
}

View File

@@ -1,19 +1,29 @@
/** @file main.c /** @file main.c
* @brief Entry point: initialise peripherals and run the PTT event loop. * @brief Entry point: initialise peripherals and run the PTT event loop.
*/ */
#include "radio.h" #include "fhss.h"
#include "power.h" #include "power.h"
#include <stdint.h> #include "radio.h"
static const uint8_t test_frame[] = {0xDE, 0xAD, 0xBE, 0xEF};
int main(void) int main(void)
{ {
power_init(); power_init();
radio_init(); radio_init();
fhss_init();
while (1) { while (1) {
power_sleep_until_button(); if (power_button_pressed()) {
radio_tx(test_frame, sizeof(test_frame)); radio_tx_burst();
} else {
ptt_frame_t frame;
if (radio_rx_burst(&frame)) {
/*
* Received a valid packet: synchronise FHSS slot.
* TX was on slot frame.slot; next hop is frame.slot+1
* for both sides.
*/
fhss_set_slot(frame.slot + 1u);
}
}
} }
} }

View File

@@ -6,20 +6,32 @@
#include <nrf52840.h> #include <nrf52840.h>
#include <cmsis_gcc.h> #include <cmsis_gcc.h>
/* P0.02 on XIAO BLE - adjust to match your schematic */ /*
* BUTTON_PIN: P0.02 on XIAO BLE -- adjust to match your schematic.
* BUTTON_ACTIVE_LOW: 1 = button connects pin to GND (pull-up); 0 = button connects to VCC.
*/
#define BUTTON_PIN 2u #define BUTTON_PIN 2u
#define BUTTON_ACTIVE_LOW 1u
void power_init(void) void power_init(void)
{ {
/* DC/DC converter has lower quiescent current than the LDO */ /* DC/DC converter has lower quiescent current than the LDO */
NRF_POWER->DCDCEN = 1u; NRF_POWER->DCDCEN = 1u;
/* configure pin as input with pull matching button wiring */
NRF_P0->PIN_CNF[BUTTON_PIN] = (gpio_pin_cnf_t){
.bit = {
.DIR = 0u,
.INPUT = 0u,
.PULL = BUTTON_ACTIVE_LOW ? GPIO_PULL_PULLUP : GPIO_PULL_PULLDOWN,
}}.reg;
NRF_GPIOTE->CONFIG[0] = (gpiote_config_t){ NRF_GPIOTE->CONFIG[0] = (gpiote_config_t){
.bit = { .bit = {
.MODE = GPIOTE_MODE_EVENT, .MODE = GPIOTE_MODE_EVENT,
.PSEL = BUTTON_PIN, .PSEL = BUTTON_PIN,
.PORT = 0u, .PORT = 0u,
.POLARITY = GPIOTE_POL_LOTOHI, .POLARITY = BUTTON_ACTIVE_LOW ? GPIOTE_POL_HITOLO : GPIOTE_POL_LOTOHI,
}}.reg; }}.reg;
NRF_GPIOTE->INTENSET = (gpiote_inten_t){.bit.IN0 = 1u}.reg; NRF_GPIOTE->INTENSET = (gpiote_inten_t){.bit.IN0 = 1u}.reg;
@@ -33,6 +45,12 @@ void power_sleep_until_button(void)
__WFI(); __WFI();
} }
bool power_button_pressed(void)
{
uint32_t high = (NRF_P0->IN >> BUTTON_PIN) & 1u;
return BUTTON_ACTIVE_LOW ? !high : !!high;
}
void GPIOTE_IRQHandler(void) void GPIOTE_IRQHandler(void)
{ {
NRF_GPIOTE->EVENTS_IN[0] = 0u; NRF_GPIOTE->EVENTS_IN[0] = 0u;

View File

@@ -2,37 +2,65 @@
* @brief RADIO peripheral driver implementation. * @brief RADIO peripheral driver implementation.
*/ */
#include "radio.h" #include "radio.h"
#include "fhss.h"
#include "regs.h" #include "regs.h"
#include <nrf52840.h> #include <nrf52840.h>
#include <string.h> #include <string.h>
/* /*
* Packet buffer layout (S0=1B, LENGTH=8-bit, S1=0, payload up to 255B): * Packet buffer layout (S0=0, LENGTH=8-bit, S1=0, payload up to 255B):
* [0] LENGTH - payload byte count (written by radio_tx) * [0] LENGTH -- payload byte count
* [1..1+len] payload - caller-supplied data * [1..1+len] payload -- caller data
*
* RADIO is configured for NRF_1Mbit proprietary mode, fixed channel,
* no data whitening, 2-byte CRC over payload only.
* TX is synchronous: function returns after EVENTS_END fires.
*/ */
#define MAX_PAYLOAD 255u #define MAX_PAYLOAD 255u
#define BUF_SIZE (1u + MAX_PAYLOAD) #define BUF_SIZE (1u + MAX_PAYLOAD)
static uint8_t pkt_buf[BUF_SIZE]; static uint8_t pkt_buf[BUF_SIZE];
/* Logical channel 20 -> 2400 + 20 = 2420 MHz (MAP=0) */ #define DEFAULT_CHANNEL 20u /* 2400 + 20 = 2420 MHz (MAP=0) */
#define DEFAULT_CHANNEL 20u
/* 4-byte base address (3-byte BALEN field means 3+1=4 total address bytes) */
#define RADIO_BASE0 0x12345678u #define RADIO_BASE0 0x12345678u
#define RADIO_PREFIX0 0xABu /* logical address 0: RADIO_BASE0 + RADIO_PREFIX0[7:0] */ #define RADIO_PREFIX0 0xABu /* logical address 0: RADIO_BASE0 + RADIO_PREFIX0[7:0] */
/* ---------- dwell timer (TIMER0) ---------------------------------------- */
/*
* TIMER0 is configured once in radio_init():
* MODE=Timer, BITMODE=16-bit, PRESCALER=4 -> 1 MHz tick (1 us per count)
* CC[0] = FHSS_DWELL_MS * 1000 -> 2000 us = 2 ms
* SHORTS: COMPARE0_STOP -- timer halts on match, event stays set
*/
static void timer_init(void)
{
NRF_TIMER0->TASKS_STOP = 1;
NRF_TIMER0->MODE = 0u; /* Timer */
NRF_TIMER0->BITMODE = 1u; /* 16-bit */
NRF_TIMER0->PRESCALER = 4u; /* 16 MHz / 2^4 = 1 MHz */
NRF_TIMER0->CC[0] = FHSS_DWELL_MS * 1000u;
NRF_TIMER0->SHORTS = (1u << 8); /* COMPARE0_STOP */
}
static void dwell_start(void)
{
NRF_TIMER0->TASKS_STOP = 1;
NRF_TIMER0->TASKS_CLEAR = 1;
NRF_TIMER0->EVENTS_COMPARE[0] = 0;
NRF_TIMER0->TASKS_START = 1;
}
static void dwell_wait(void)
{
while (!NRF_TIMER0->EVENTS_COMPARE[0])
;
}
/* ---------- radio init --------------------------------------------------- */
void radio_init(void) void radio_init(void)
{ {
NRF_RADIO->MODE = (radio_mode_t){.bit = {.MODE = RADIO_MODE_NRF_1MBIT}}.reg; NRF_RADIO->MODE = (radio_mode_t){.bit = {.MODE = RADIO_MODE_NRF_1MBIT}}.reg;
/* 8-bit LENGTH field, no S0/S1, 8-bit preamble, CRC not part of LENGTH */ /* 8-bit LENGTH field, no S0/S1, 8-bit preamble, CRC not counted in LENGTH */
NRF_RADIO->PCNF0 = NRF_RADIO->PCNF0 =
(radio_pcnf0_t){.bit = {.LFLEN = 8, .S0LEN = 0, .S1LEN = 0, .PLEN = 0, .CRCINC = 0}}.reg; (radio_pcnf0_t){.bit = {.LFLEN = 8, .S0LEN = 0, .S1LEN = 0, .PLEN = 0, .CRCINC = 0}}.reg;
@@ -61,18 +89,23 @@ void radio_init(void)
NRF_RADIO->FREQUENCY = NRF_RADIO->FREQUENCY =
(radio_frequency_t){.bit = {.FREQUENCY = DEFAULT_CHANNEL, .MAP = RADIO_MAP_DEFAULT}}.reg; (radio_frequency_t){.bit = {.FREQUENCY = DEFAULT_CHANNEL, .MAP = RADIO_MAP_DEFAULT}}.reg;
/* READY -> START and END -> DISABLE shortcuts so CPU only triggers TXEN */
NRF_RADIO->SHORTS = (radio_shorts_t){.bit = {.READY_START = 1, .END_DISABLE = 1}}.reg; NRF_RADIO->SHORTS = (radio_shorts_t){.bit = {.READY_START = 1, .END_DISABLE = 1}}.reg;
NRF_RADIO->PACKETPTR = (uint32_t)pkt_buf; NRF_RADIO->PACKETPTR = (uint32_t)pkt_buf;
timer_init();
} }
/* ---------- channel select ----------------------------------------------- */
void radio_set_channel(uint8_t ch) void radio_set_channel(uint8_t ch)
{ {
NRF_RADIO->FREQUENCY = NRF_RADIO->FREQUENCY =
(radio_frequency_t){.bit = {.FREQUENCY = ch, .MAP = RADIO_MAP_DEFAULT}}.reg; (radio_frequency_t){.bit = {.FREQUENCY = ch, .MAP = RADIO_MAP_DEFAULT}}.reg;
} }
/* ---------- synchronous TX ---------------------------------------------- */
void radio_tx(const uint8_t *data, uint8_t len) void radio_tx(const uint8_t *data, uint8_t len)
{ {
/* len is uint8_t so it cannot exceed MAX_PAYLOAD=255 by type guarantee */ /* len is uint8_t so it cannot exceed MAX_PAYLOAD=255 by type guarantee */
@@ -82,15 +115,64 @@ void radio_tx(const uint8_t *data, uint8_t len)
NRF_RADIO->EVENTS_END = 0; NRF_RADIO->EVENTS_END = 0;
NRF_RADIO->TASKS_TXEN = 1; NRF_RADIO->TASKS_TXEN = 1;
/* Busy-wait for END event (ramp-up ~40us + TX time, total <1ms for short frames) */
while (!NRF_RADIO->EVENTS_END) while (!NRF_RADIO->EVENTS_END)
; ;
NRF_RADIO->EVENTS_END = 0; NRF_RADIO->EVENTS_END = 0;
/* RADIO is now DISABLED via the END_DISABLE shortcut */ /* RADIO is now DISABLED via the END_DISABLE shortcut */
} }
/* ---------- FHSS TX burst ----------------------------------------------- */
void radio_tx_burst(void) void radio_tx_burst(void)
{ {
/* placeholder: FHSS TX loop not yet implemented */ ptt_frame_t frame;
frame.slot = fhss_get_slot(); /* capture slot before next_channel increments it */
frame.flags = PTT_FLAG_ACTIVE;
uint8_t ch = fhss_next_channel();
radio_set_channel(ch);
dwell_start();
radio_tx((const uint8_t *)&frame, sizeof(frame));
dwell_wait(); /* hold the full 2 ms dwell so RX side has time to listen */
}
/* ---------- FHSS RX burst ----------------------------------------------- */
bool radio_rx_burst(ptt_frame_t *frame_out)
{
uint8_t ch = fhss_next_channel();
radio_set_channel(ch);
NRF_RADIO->SHORTS = (radio_shorts_t){.bit = {.READY_START = 1, .END_DISABLE = 1}}.reg;
NRF_RADIO->EVENTS_END = 0;
NRF_RADIO->EVENTS_CRCOK = 0;
NRF_RADIO->TASKS_RXEN = 1;
dwell_start();
/* wait for a packet or dwell timeout -- check both flags after exit */
while (!NRF_RADIO->EVENTS_END && !NRF_TIMER0->EVENTS_COMPARE[0])
;
bool got_packet = (NRF_RADIO->EVENTS_END != 0u);
NRF_RADIO->EVENTS_END = 0;
if (!got_packet) {
/* dwell expired with no packet -- force radio to DISABLED */
NRF_RADIO->TASKS_DISABLE = 1;
while (NRF_RADIO->STATE != 0u)
;
return false;
}
/* packet received -- check CRC and extract frame */
bool crc_ok = (NRF_RADIO->EVENTS_CRCOK != 0u);
NRF_RADIO->EVENTS_CRCOK = 0;
if (crc_ok && pkt_buf[0] >= sizeof(ptt_frame_t)) {
memcpy(frame_out, &pkt_buf[1], sizeof(ptt_frame_t));
return true;
}
return false;
} }