Initial bare-metal foundation for nRF52840 PTT-FHSS

- Fully open toolchain: arm-none-eabi-gcc inside Podman/Docker container
- Platform-agnostic build via justfile (auto-detects podman vs docker)
- CMake + Ninja build system with arm-none-eabi toolchain file
- nRF52840 linker script and startup with full 64-entry vector table
- Register access via typed bitfield unions (include/regs.h)
- FHSS channel sequencer: AES-128-ECB PRNG over 40 channels (2402-2480 MHz)
- Low-power sleep via SYSTEM_ON WFI + GPIOTE wakeup on button press
- Vendor deps as shallow git submodules: nrfx, CMSIS_5, tiny-AES-c
This commit is contained in:
Krzysztof Cieślik
2026-05-21 20:20:21 +02:00
commit 438fca0ace
20 changed files with 739 additions and 0 deletions

41
src/fhss.c Normal file
View File

@@ -0,0 +1,41 @@
#include "fhss.h"
#include <aes.h>
#define FHSS_CHANNELS 40u
#define FHSS_DWELL_MS 2u
/* TODO: replace with a real shared secret before deployment */
static const uint8_t shared_key[16] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static uint32_t slot;
void fhss_init(void)
{
slot = 0u;
}
uint8_t fhss_next_channel(void)
{
uint8_t block[16] = {0};
struct AES_ctx ctx;
/* encode slot counter big-endian into the AES input block */
block[0] = (uint8_t)(slot >> 24);
block[1] = (uint8_t)(slot >> 16);
block[2] = (uint8_t)(slot >> 8);
block[3] = (uint8_t)(slot );
AES_init_ctx(&ctx, shared_key);
AES_ECB_encrypt(&ctx, block); /* encrypts block in-place */
slot++;
return block[0] % FHSS_CHANNELS;
}
void fhss_sync_tick(void)
{
slot++;
}

15
src/main.c Normal file
View File

@@ -0,0 +1,15 @@
#include "radio.h"
#include "fhss.h"
#include "power.h"
int main(void)
{
power_init();
radio_init();
fhss_init();
while (1) {
power_sleep_until_button();
radio_tx_burst();
}
}

37
src/power.c Normal file
View File

@@ -0,0 +1,37 @@
#include "power.h"
#include "regs.h"
#include <nrf52840.h>
#include <cmsis_gcc.h>
/* P0.02 on XIAO BLE - adjust to match your schematic */
#define BUTTON_PIN 2u
void power_init(void)
{
/* DC/DC converter has lower quiescent current than the LDO */
NRF_POWER->DCDCEN = 1u;
NRF_GPIOTE->CONFIG[0] = (gpiote_config_t){
.bit = {
.MODE = GPIOTE_MODE_EVENT,
.PSEL = BUTTON_PIN,
.PORT = 0u,
.POLARITY = GPIOTE_POL_LOTOHI,
}
}.reg;
NRF_GPIOTE->INTENSET = (gpiote_inten_t){ .bit.IN0 = 1u }.reg;
NVIC_EnableIRQ(GPIOTE_IRQn);
}
void power_sleep_until_button(void)
{
NRF_POWER->TASKS_LOWPWR = 1u;
__WFI();
}
void GPIOTE_IRQHandler(void)
{
NRF_GPIOTE->EVENTS_IN[0] = 0u;
}

27
src/radio.c Normal file
View File

@@ -0,0 +1,27 @@
#include "radio.h"
#include "fhss.h"
#include "regs.h"
#include <nrf52840.h>
void radio_init(void)
{
/* TODO: configure RADIO peripheral (MODE, PCNF0/1, BASE/PREFIX, CRC) */
}
void radio_set_channel(uint8_t ch)
{
NRF_RADIO->FREQUENCY = (radio_frequency_t){
.bit = { .FREQUENCY = ch, .MAP = RADIO_MAP_DEFAULT }
}.reg;
}
void radio_tx(const uint8_t *data, uint8_t len)
{
(void)data; (void)len;
/* TODO: load packet, enable TX, wait for END event, disable */
}
void radio_tx_burst(void)
{
/* TODO: hop + TX loop driven by fhss_next_channel() */
}

168
src/startup.c Normal file
View File

@@ -0,0 +1,168 @@
#include <stdint.h>
/* linker script symbols */
extern uint32_t _sidata; /* load address of .data in Flash */
extern uint32_t _sdata; /* start of .data in RAM */
extern uint32_t _edata; /* end of .data in RAM */
extern uint32_t _sbss;
extern uint32_t _ebss;
extern uint32_t _estack; /* address equals initial SP value */
extern int main(void);
void Reset_Handler(void);
static void __attribute__((used)) Default_Handler(void)
{
while (1);
}
/* ARM Cortex-M4 core exceptions */
void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void MemManage_Handler(void) __attribute__((weak, alias("Default_Handler")));
void BusFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void UsageFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SVC_Handler(void) __attribute__((weak, alias("Default_Handler")));
void DebugMon_Handler(void) __attribute__((weak, alias("Default_Handler")));
void PendSV_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_Handler")));
/* nRF52840 peripheral IRQs (IRQ0-IRQ47) */
void POWER_CLOCK_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void RADIO_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void UARTE0_UART0_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void SPIM1_SPIS1_TWIM1_TWIS1_SPI1_TWI1_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void NFCT_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void GPIOTE_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void SAADC_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void TIMER0_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void TIMER1_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void TIMER2_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void RTC0_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void TEMP_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void RNG_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void ECB_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void CCM_AAR_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void WDT_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void RTC1_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void QDEC_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void COMP_LPCOMP_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void SWI0_EGU0_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void SWI1_EGU1_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void SWI2_EGU2_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void SWI3_EGU3_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void SWI4_EGU4_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void SWI5_EGU5_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void TIMER3_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void TIMER4_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void PWM0_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void PDM_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void MWU_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void PWM1_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void PWM2_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void SPIM2_SPIS2_SPI2_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void RTC2_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void I2S_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void FPU_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void USBD_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void UARTE1_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void QSPI_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void CRYPTOCELL_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void PWM3_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void SPIM3_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
/* vector table: 64 entries (16 core + 48 IRQs) */
typedef void (*vector_fn)(void);
__attribute__((section(".isr_vector"), used))
const vector_fn vectors[64] = {
/* 0 */ (vector_fn)&_estack,
/* 1 */ Reset_Handler,
/* 2 */ NMI_Handler,
/* 3 */ HardFault_Handler,
/* 4 */ MemManage_Handler,
/* 5 */ BusFault_Handler,
/* 6 */ UsageFault_Handler,
/* 7 */ 0,
/* 8 */ 0,
/* 9 */ 0,
/* 10 */ 0,
/* 11 */ SVC_Handler,
/* 12 */ DebugMon_Handler,
/* 13 */ 0,
/* 14 */ PendSV_Handler,
/* 15 */ SysTick_Handler,
/* IRQ0 */ POWER_CLOCK_IRQHandler,
/* IRQ1 */ RADIO_IRQHandler,
/* IRQ2 */ UARTE0_UART0_IRQHandler,
/* IRQ3 */ SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQHandler,
/* IRQ4 */ SPIM1_SPIS1_TWIM1_TWIS1_SPI1_TWI1_IRQHandler,
/* IRQ5 */ NFCT_IRQHandler,
/* IRQ6 */ GPIOTE_IRQHandler,
/* IRQ7 */ SAADC_IRQHandler,
/* IRQ8 */ TIMER0_IRQHandler,
/* IRQ9 */ TIMER1_IRQHandler,
/* IRQ10 */ TIMER2_IRQHandler,
/* IRQ11 */ RTC0_IRQHandler,
/* IRQ12 */ TEMP_IRQHandler,
/* IRQ13 */ RNG_IRQHandler,
/* IRQ14 */ ECB_IRQHandler,
/* IRQ15 */ CCM_AAR_IRQHandler,
/* IRQ16 */ WDT_IRQHandler,
/* IRQ17 */ RTC1_IRQHandler,
/* IRQ18 */ QDEC_IRQHandler,
/* IRQ19 */ COMP_LPCOMP_IRQHandler,
/* IRQ20 */ SWI0_EGU0_IRQHandler,
/* IRQ21 */ SWI1_EGU1_IRQHandler,
/* IRQ22 */ SWI2_EGU2_IRQHandler,
/* IRQ23 */ SWI3_EGU3_IRQHandler,
/* IRQ24 */ SWI4_EGU4_IRQHandler,
/* IRQ25 */ SWI5_EGU5_IRQHandler,
/* IRQ26 */ TIMER3_IRQHandler,
/* IRQ27 */ TIMER4_IRQHandler,
/* IRQ28 */ PWM0_IRQHandler,
/* IRQ29 */ PDM_IRQHandler,
/* IRQ30 */ 0, /* reserved */
/* IRQ31 */ 0, /* reserved */
/* IRQ32 */ MWU_IRQHandler,
/* IRQ33 */ PWM1_IRQHandler,
/* IRQ34 */ PWM2_IRQHandler,
/* IRQ35 */ SPIM2_SPIS2_SPI2_IRQHandler,
/* IRQ36 */ RTC2_IRQHandler,
/* IRQ37 */ I2S_IRQHandler,
/* IRQ38 */ FPU_IRQHandler,
/* IRQ39 */ USBD_IRQHandler,
/* IRQ40 */ UARTE1_IRQHandler,
/* IRQ41 */ QSPI_IRQHandler,
/* IRQ42 */ CRYPTOCELL_IRQHandler,
/* IRQ43 */ 0, /* reserved */
/* IRQ44 */ 0, /* reserved */
/* IRQ45 */ PWM3_IRQHandler,
/* IRQ46 */ 0, /* reserved */
/* IRQ47 */ SPIM3_IRQHandler,
};
/* Reset_Handler */
void Reset_Handler(void)
{
/* copy .data initializers from Flash to RAM */
uint32_t *src = &_sidata;
uint32_t *dst = &_sdata;
while (dst < &_edata) {
*dst++ = *src++;
}
/* zero .bss */
dst = &_sbss;
while (dst < &_ebss) {
*dst++ = 0u;
}
main();
while (1);
}