diff --git a/include/fhss.h b/include/fhss.h index 804c425..bcbaa02 100644 --- a/include/fhss.h +++ b/include/fhss.h @@ -10,18 +10,29 @@ #pragma once #include +/** 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. */ void fhss_init(void); /** - * @brief Return the next channel in the hopping sequence. - * - * Encrypts the current slot counter big-endian with AES-128-ECB, returns - * @c block[0] % 40, and advances the slot counter. - * + * @brief Return the next channel in the hopping sequence and advance the slot. * @return Channel index in [0, 39]. */ 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); + +/** + * @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); diff --git a/include/power.h b/include/power.h index 20435a1..de2011d 100644 --- a/include/power.h +++ b/include/power.h @@ -1,10 +1,11 @@ /** * @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 +#include -/** @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); /** @@ -14,3 +15,9 @@ void power_init(void); * interrupt fires (button press) and resumes from here. */ 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); diff --git a/include/radio.h b/include/radio.h index e93e93b..019b370 100644 --- a/include/radio.h +++ b/include/radio.h @@ -3,9 +3,23 @@ * @brief RADIO peripheral driver -- NRF_1Mbit proprietary mode. */ #pragma once +#include #include -/** @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); /** @@ -18,13 +32,30 @@ void radio_set_channel(uint8_t ch); * @brief Transmit one packet synchronously. * * Loads @p data into the internal packet buffer, asserts TASKS_TXEN, and - * returns after EVENTS_END fires. The RADIO is DISABLED automatically via - * the END_DISABLE shortcut before the function returns. + * returns after EVENTS_END fires. RADIO is DISABLED automatically via the + * END_DISABLE shortcut before the function returns. * * @param data Payload bytes. * @param len Payload length (0-255 bytes). */ 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); + +/** + * @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); diff --git a/include/regs.h b/include/regs.h index 2000307..897ccd0 100644 --- a/include/regs.h +++ b/include/regs.h @@ -153,3 +153,24 @@ typedef union { } bit; uint32_t reg; } 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 diff --git a/src/fhss.c b/src/fhss.c index dd946e5..5ac5a33 100644 --- a/src/fhss.c +++ b/src/fhss.c @@ -4,12 +4,12 @@ #include "fhss.h" #include -#define FHSS_CHANNELS 40u -#define FHSS_DWELL_MS 2u - -/* TODO: replace with a real shared secret before deployment */ +/* + * Shared secret -- both devices must carry the same key. + * Change before deployment; never commit a production key to source control. + */ 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; @@ -41,3 +41,13 @@ void fhss_sync_tick(void) { slot++; } + +void fhss_set_slot(uint32_t s) +{ + slot = s; +} + +uint32_t fhss_get_slot(void) +{ + return slot; +} diff --git a/src/main.c b/src/main.c index ca6f3a0..29244a3 100644 --- a/src/main.c +++ b/src/main.c @@ -1,19 +1,29 @@ /** @file main.c * @brief Entry point: initialise peripherals and run the PTT event loop. */ -#include "radio.h" +#include "fhss.h" #include "power.h" -#include - -static const uint8_t test_frame[] = {0xDE, 0xAD, 0xBE, 0xEF}; +#include "radio.h" int main(void) { power_init(); radio_init(); + fhss_init(); while (1) { - power_sleep_until_button(); - radio_tx(test_frame, sizeof(test_frame)); + if (power_button_pressed()) { + 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); + } + } } } diff --git a/src/power.c b/src/power.c index 49a0b94..643c51e 100644 --- a/src/power.c +++ b/src/power.c @@ -6,20 +6,32 @@ #include #include -/* 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_ACTIVE_LOW 1u void power_init(void) { /* DC/DC converter has lower quiescent current than the LDO */ 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){ .bit = { .MODE = GPIOTE_MODE_EVENT, .PSEL = BUTTON_PIN, .PORT = 0u, - .POLARITY = GPIOTE_POL_LOTOHI, + .POLARITY = BUTTON_ACTIVE_LOW ? GPIOTE_POL_HITOLO : GPIOTE_POL_LOTOHI, }}.reg; NRF_GPIOTE->INTENSET = (gpiote_inten_t){.bit.IN0 = 1u}.reg; @@ -33,6 +45,12 @@ void power_sleep_until_button(void) __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) { NRF_GPIOTE->EVENTS_IN[0] = 0u; diff --git a/src/radio.c b/src/radio.c index 2cdfa5b..5008a19 100644 --- a/src/radio.c +++ b/src/radio.c @@ -2,37 +2,65 @@ * @brief RADIO peripheral driver implementation. */ #include "radio.h" +#include "fhss.h" #include "regs.h" #include #include /* - * Packet buffer layout (S0=1B, LENGTH=8-bit, S1=0, payload up to 255B): - * [0] LENGTH - payload byte count (written by radio_tx) - * [1..1+len] payload - caller-supplied 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. + * Packet buffer layout (S0=0, LENGTH=8-bit, S1=0, payload up to 255B): + * [0] LENGTH -- payload byte count + * [1..1+len] payload -- caller data */ - #define MAX_PAYLOAD 255u #define BUF_SIZE (1u + MAX_PAYLOAD) static uint8_t pkt_buf[BUF_SIZE]; -/* Logical channel 20 -> 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 DEFAULT_CHANNEL 20u /* 2400 + 20 = 2420 MHz (MAP=0) */ #define RADIO_BASE0 0x12345678u #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) { 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 = (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 = (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->PACKETPTR = (uint32_t)pkt_buf; + + timer_init(); } +/* ---------- channel select ----------------------------------------------- */ + void radio_set_channel(uint8_t ch) { NRF_RADIO->FREQUENCY = (radio_frequency_t){.bit = {.FREQUENCY = ch, .MAP = RADIO_MAP_DEFAULT}}.reg; } +/* ---------- synchronous TX ---------------------------------------------- */ + void radio_tx(const uint8_t *data, uint8_t len) { /* 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->TASKS_TXEN = 1; - /* Busy-wait for END event (ramp-up ~40us + TX time, total <1ms for short frames) */ while (!NRF_RADIO->EVENTS_END) ; - NRF_RADIO->EVENTS_END = 0; /* RADIO is now DISABLED via the END_DISABLE shortcut */ } +/* ---------- FHSS TX burst ----------------------------------------------- */ + 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; }