From 39a89036ccd24962fc494d6a696e48dd68081ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Cie=C5=9Blik?= Date: Thu, 21 May 2026 23:07:05 +0200 Subject: [PATCH] Add Doxygen, clang-format, cppcheck, and Gitea CI Doxygen: - Doxyfile: minimal config, HTML output to docs/, no LaTeX - @file/@brief on all source files, full @param/@return on public API - docs/ added to .gitignore clang-format (14, Linux brace style, 4-space, column 100): - .clang-format added - Applied to entire codebase; this commit is the canonical baseline - just format rewrites in-place; just format-check is the CI gate cppcheck (--enable=warning,style,performance,portability): - Linker-symbol pointer comparisons in startup.c suppressed with inline cppcheck-suppress (false positives, not real bugs) - just lint runs cppcheck; zero warnings required to pass Dockerfile gains clang-format, cppcheck, doxygen packages so all tools run inside the existing container -- host stays clean. Gitea Actions (.gitea/workflows/ci.yml): - Four parallel jobs: build, format, lint, docs - All jobs use the same Dockerfile-based image - Doxygen job fails on any warning line in output --- .clang-format | 15 ++++ .gitea/workflows/ci.yml | 99 ++++++++++++++++++++++ .gitignore | 1 + Dockerfile | 3 + Doxyfile | 21 +++++ include/fhss.h | 27 +++++- include/power.h | 14 +++- include/radio.h | 31 ++++++- include/regs.h | 181 ++++++++++++++++++++-------------------- justfile | 46 +++++++++- src/fhss.c | 16 ++-- src/main.c | 5 +- src/power.c | 16 ++-- src/radio.c | 62 +++++++------- src/startup.c | 136 ++++++++++++++++-------------- 15 files changed, 460 insertions(+), 213 deletions(-) create mode 100644 .clang-format create mode 100644 .gitea/workflows/ci.yml create mode 100644 Doxyfile diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..6049b71 --- /dev/null +++ b/.clang-format @@ -0,0 +1,15 @@ +--- +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 100 +AlignConsecutiveBitFields: true +AlignConsecutiveAssignments: false +AlignTrailingComments: true +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: Never +BreakBeforeBraces: Linux +SortIncludes: false +SpaceAfterCStyleCast: false +SpaceBeforeParens: ControlStatements diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..400570b --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,99 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + IMAGE: ptt-builder + +jobs: + build: + name: Build firmware + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Build container image + run: docker build -t $IMAGE . + + - name: Compile firmware + run: | + docker run --rm \ + --user "$(id -u):$(id -g)" \ + -v "$PWD:/src" \ + $IMAGE \ + sh -c "cmake -B /src/build -G Ninja \ + -DCMAKE_BUILD_TYPE=MinSizeRel /src \ + && ninja -C /src/build" + + - name: Print size + run: | + docker run --rm \ + -v "$PWD:/src" \ + $IMAGE \ + arm-none-eabi-size /src/build/firmware + + format: + name: Check formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build container image + run: docker build -t $IMAGE . + + - name: clang-format check + run: | + docker run --rm \ + --user "$(id -u):$(id -g)" \ + -v "$PWD:/src" \ + $IMAGE \ + sh -c "find /src/src /src/include -name '*.c' -o -name '*.h' | \ + xargs clang-format --dry-run --Werror \ + --style=file:/src/.clang-format" + + lint: + name: Static analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build container image + run: docker build -t $IMAGE . + + - name: cppcheck + run: | + docker run --rm \ + --user "$(id -u):$(id -g)" \ + -v "$PWD:/src" \ + $IMAGE \ + sh -c "cppcheck --error-exitcode=1 \ + --enable=warning,style,performance,portability \ + --suppress=missingInclude \ + --inline-suppr \ + --std=c11 \ + -I /src/include \ + /src/src/" + + docs: + name: Build documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build container image + run: docker build -t $IMAGE . + + - name: Doxygen + run: | + docker run --rm \ + --user "$(id -u):$(id -g)" \ + -v "$PWD:/src" \ + $IMAGE \ + sh -c "cd /src && doxygen Doxyfile 2>&1 | tee /tmp/doxy.log && \ + ! grep -q 'warning:' /tmp/doxy.log" diff --git a/.gitignore b/.gitignore index 567609b..58b2223 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ build/ +docs/ diff --git a/Dockerfile b/Dockerfile index e8ea987..adbe32e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libnewlib-arm-none-eabi \ cmake \ ninja-build \ + clang-format \ + cppcheck \ + doxygen \ && rm -rf /var/lib/apt/lists/* WORKDIR /src diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..bf26cee --- /dev/null +++ b/Doxyfile @@ -0,0 +1,21 @@ +PROJECT_NAME = "ptt-fhss" +PROJECT_BRIEF = "Bare-metal PTT firmware for nRF52840 with FHSS" +PROJECT_NUMBER = +OUTPUT_DIRECTORY = docs +INPUT = include src +FILE_PATTERNS = *.h *.c +RECURSIVE = NO +EXTRACT_ALL = YES +EXTRACT_STATIC = YES +QUIET = YES +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_NO_PARAMDOC = YES +GENERATE_HTML = YES +HTML_OUTPUT = html +HTML_TIMESTAMP = NO +GENERATE_LATEX = NO +HAVE_DOT = NO +OPTIMIZE_OUTPUT_FOR_C = YES +JAVADOC_AUTOBRIEF = YES +PREDEFINED = NRF52840_XXAA diff --git a/include/fhss.h b/include/fhss.h index fb1b95b..804c425 100644 --- a/include/fhss.h +++ b/include/fhss.h @@ -1,6 +1,27 @@ +/** + * @file fhss.h + * @brief FHSS channel sequencer based on AES-128-ECB. + * + * Both link endpoints derive the same hopping sequence independently from a + * shared 128-bit key and a monotonically increasing slot counter. No + * synchronisation traffic is required as long as both sides start from the + * same slot. + */ #pragma once #include -void fhss_init(void); -uint8_t fhss_next_channel(void); /* next channel from AES-ECB PRNG sequence */ -void fhss_sync_tick(void); /* call every FHSS_DWELL_MS ms */ +/** @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. + * + * @return Channel index in [0, 39]. + */ +uint8_t fhss_next_channel(void); + +/** @brief Advance the slot counter without transmitting (receiver side). */ +void fhss_sync_tick(void); diff --git a/include/power.h b/include/power.h index 119e403..20435a1 100644 --- a/include/power.h +++ b/include/power.h @@ -1,4 +1,16 @@ +/** + * @file power.h + * @brief Power management: DC/DC regulator, GPIOTE wakeup, SYSTEM_ON sleep. + */ #pragma once +/** @brief Enable the DC/DC converter and configure GPIOTE wakeup on the PTT button. */ void power_init(void); -void power_sleep_until_button(void); /* SYSTEM_ON WFI, woken by GPIOTE event */ + +/** + * @brief Enter SYSTEM_ON low-power sleep and return on the next GPIOTE event. + * + * Sets TASKS_LOWPWR then executes WFI. The CPU wakes when the GPIOTE + * interrupt fires (button press) and resumes from here. + */ +void power_sleep_until_button(void); diff --git a/include/radio.h b/include/radio.h index 8a8a62d..e93e93b 100644 --- a/include/radio.h +++ b/include/radio.h @@ -1,7 +1,30 @@ +/** + * @file radio.h + * @brief RADIO peripheral driver -- NRF_1Mbit proprietary mode. + */ #pragma once #include -void radio_init(void); -void radio_set_channel(uint8_t ch); /* 0-39, maps to 2402-2480 MHz */ -void radio_tx(const uint8_t *data, uint8_t len); -void radio_tx_burst(void); /* TX with FHSS hopping */ +/** @brief Configure the RADIO peripheral (mode, packet format, address, CRC, power, channel). */ +void radio_init(void); + +/** + * @brief Set the RF channel. + * @param ch Channel index 0-39, maps to 2400+ch MHz (MAP=0). + */ +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. + * + * @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). */ +void radio_tx_burst(void); diff --git a/include/regs.h b/include/regs.h index ff21679..2000307 100644 --- a/include/regs.h +++ b/include/regs.h @@ -1,154 +1,155 @@ +/** + * @file regs.h + * @brief Hardware register bitfield unions for nRF52840 peripherals. + * + * Layout is guaranteed correct only with arm-none-eabi-gcc (LSB-first + * bitfields). Bit ranges match nRF52840 Product Specification v1.7. + */ #pragma once #include -/* - * Hardware register bitfield unions for nRF52840 peripherals. - * Layout is guaranteed correct only with arm-none-eabi-gcc (LSB-first bitfields). - * Bit ranges match nRF52840 Product Specification v1.7. - */ - /* GPIOTE */ -/* CONFIG[n]: channel configuration */ +/** @brief GPIOTE CONFIG[n]: channel configuration register. */ typedef union { struct { - uint32_t MODE : 2; /* [1:0] 0=Disabled 1=Event 3=Task */ - uint32_t : 6; /* [7:2] reserved */ - uint32_t PSEL : 5; /* [12:8] pin number within port */ - uint32_t PORT : 1; /* [13] 0=Port0 1=Port1 */ - uint32_t : 2; /* [15:14] reserved */ - uint32_t POLARITY : 2; /* [17:16] 0=None 1=LoToHi 2=HiToLo 3=Toggle */ - uint32_t : 2; /* [19:18] reserved */ - uint32_t OUTINIT : 1; /* [20] initial output value for Task mode */ - uint32_t : 11; /* [31:21] reserved */ + uint32_t MODE : 2; /* [1:0] 0=Disabled 1=Event 3=Task */ + uint32_t : 6; /* [7:2] reserved */ + uint32_t PSEL : 5; /* [12:8] pin number within port */ + uint32_t PORT : 1; /* [13] 0=Port0 1=Port1 */ + uint32_t : 2; /* [15:14] reserved */ + uint32_t POLARITY : 2; /* [17:16] 0=None 1=LoToHi 2=HiToLo 3=Toggle */ + uint32_t : 2; /* [19:18] reserved */ + uint32_t OUTINIT : 1; /* [20] initial output value for Task mode */ + uint32_t : 11; /* [31:21] reserved */ } bit; uint32_t reg; } gpiote_config_t; -#define GPIOTE_MODE_DISABLED 0u -#define GPIOTE_MODE_EVENT 1u -#define GPIOTE_MODE_TASK 3u -#define GPIOTE_POL_NONE 0u -#define GPIOTE_POL_LOTOHI 1u -#define GPIOTE_POL_HITOLO 2u -#define GPIOTE_POL_TOGGLE 3u +#define GPIOTE_MODE_DISABLED 0u +#define GPIOTE_MODE_EVENT 1u +#define GPIOTE_MODE_TASK 3u +#define GPIOTE_POL_NONE 0u +#define GPIOTE_POL_LOTOHI 1u +#define GPIOTE_POL_HITOLO 2u +#define GPIOTE_POL_TOGGLE 3u -/* INTENSET / INTENCLR: interrupt enable */ +/** @brief GPIOTE INTENSET / INTENCLR: interrupt enable register. */ typedef union { struct { - uint32_t IN0 : 1; /* [0] channel 0 input event */ - uint32_t IN1 : 1; /* [1] channel 1 input event */ - uint32_t IN2 : 1; /* [2] */ - uint32_t IN3 : 1; /* [3] */ - uint32_t IN4 : 1; /* [4] */ - uint32_t IN5 : 1; /* [5] */ - uint32_t IN6 : 1; /* [6] */ - uint32_t IN7 : 1; /* [7] */ - uint32_t : 23; /* [30:8] reserved */ - uint32_t PORT : 1; /* [31] PORT event */ + uint32_t IN0 : 1; /* [0] channel 0 input event */ + uint32_t IN1 : 1; /* [1] channel 1 input event */ + uint32_t IN2 : 1; /* [2] */ + uint32_t IN3 : 1; /* [3] */ + uint32_t IN4 : 1; /* [4] */ + uint32_t IN5 : 1; /* [5] */ + uint32_t IN6 : 1; /* [6] */ + uint32_t IN7 : 1; /* [7] */ + uint32_t : 23; /* [30:8] reserved */ + uint32_t PORT : 1; /* [31] PORT event */ } bit; uint32_t reg; } gpiote_inten_t; /* RADIO */ -/* FREQUENCY: radio channel */ +/** @brief RADIO FREQUENCY: RF channel selection register. */ typedef union { struct { - uint32_t FREQUENCY : 7; /* [6:0] offset from base frequency in MHz */ - uint32_t : 1; /* [7] reserved */ - uint32_t MAP : 1; /* [8] 0: base=2400 MHz 1: base=2360 MHz */ - uint32_t : 23; /* [31:9] reserved */ + uint32_t FREQUENCY : 7; /* [6:0] offset from base frequency in MHz */ + uint32_t : 1; /* [7] reserved */ + uint32_t MAP : 1; /* [8] 0: base=2400 MHz 1: base=2360 MHz */ + uint32_t : 23; /* [31:9] reserved */ } bit; uint32_t reg; } radio_frequency_t; -#define RADIO_MAP_DEFAULT 0u /* channel n -> 2400+n MHz */ -#define RADIO_MAP_BLE 1u /* channel n -> 2360+n MHz */ +#define RADIO_MAP_DEFAULT 0u /* channel n -> 2400+n MHz */ +#define RADIO_MAP_BLE 1u /* channel n -> 2360+n MHz */ -/* TXPOWER: transmit power */ +/** @brief RADIO TXPOWER: transmit power register. */ typedef union { struct { - int32_t TXPOWER : 8; /* [7:0] signed dBm: +8, +7, +6, +5, +4, +3, +2, - 0, -4, -8, -12, -16, -20, -40 */ - uint32_t : 24; /* [31:8] reserved */ + int32_t TXPOWER : 8; /* [7:0] signed dBm: +8, +7, +6, +5, +4, +3, +2, + 0, -4, -8, -12, -16, -20, -40 */ + uint32_t : 24; /* [31:8] reserved */ } bit; uint32_t reg; } radio_txpower_t; -/* MODE: radio data rate and modulation */ +/** @brief RADIO MODE: data rate and modulation register. */ typedef union { struct { - uint32_t MODE : 4; /* [3:0] 0=NRF_1Mbit 1=NRF_2Mbit 4=BLE_1Mbit ... */ - uint32_t : 28; /* [31:4] reserved */ + uint32_t MODE : 4; /* [3:0] 0=NRF_1Mbit 1=NRF_2Mbit 4=BLE_1Mbit ... */ + uint32_t : 28; /* [31:4] reserved */ } bit; uint32_t reg; } radio_mode_t; -#define RADIO_MODE_NRF_1MBIT 0u -#define RADIO_MODE_NRF_2MBIT 1u -#define RADIO_MODE_BLE_1MBIT 4u +#define RADIO_MODE_NRF_1MBIT 0u +#define RADIO_MODE_NRF_2MBIT 1u +#define RADIO_MODE_BLE_1MBIT 4u -/* PCNF0: packet configuration register 0 */ +/** @brief RADIO PCNF0: packet configuration register 0 (header fields). */ typedef union { struct { - uint32_t LFLEN : 4; /* [3:0] length of LENGTH field in bits */ - uint32_t : 4; /* [7:4] reserved */ - uint32_t S0LEN : 1; /* [8] length of S0 field in bytes (0 or 1) */ - uint32_t : 7; /* [15:9] reserved */ - uint32_t S1LEN : 4; /* [19:16] length of S1 field in bits */ - uint32_t S1INCL : 1; /* [20] include S1 field in RAM even if zero length */ - uint32_t : 3; /* [23:21] reserved */ - uint32_t PLEN : 1; /* [24] 0=8-bit preamble 1=16-bit preamble */ - uint32_t : 6; /* [30:25] reserved */ - uint32_t CRCINC : 1; /* [31] include CRC in LENGTH field */ + uint32_t LFLEN : 4; /* [3:0] length of LENGTH field in bits */ + uint32_t : 4; /* [7:4] reserved */ + uint32_t S0LEN : 1; /* [8] length of S0 field in bytes (0 or 1) */ + uint32_t : 7; /* [15:9] reserved */ + uint32_t S1LEN : 4; /* [19:16] length of S1 field in bits */ + uint32_t S1INCL : 1; /* [20] include S1 field in RAM even if zero length */ + uint32_t : 3; /* [23:21] reserved */ + uint32_t PLEN : 1; /* [24] 0=8-bit preamble 1=16-bit preamble */ + uint32_t : 6; /* [30:25] reserved */ + uint32_t CRCINC : 1; /* [31] include CRC in LENGTH field */ } bit; uint32_t reg; } radio_pcnf0_t; -/* PCNF1: packet configuration register 1 */ +/** @brief RADIO PCNF1: packet configuration register 1 (payload and address). */ typedef union { struct { - uint32_t MAXLEN : 8; /* [7:0] maximum payload length in bytes */ - uint32_t STATLEN : 8; /* [15:8] static length added to payload */ - uint32_t BALEN : 3; /* [18:16] base address length (2-4 bytes) */ - uint32_t : 5; /* [23:19] reserved */ - uint32_t ENDIAN : 1; /* [24] 0=little-endian 1=big-endian */ - uint32_t WHITEEN : 1; /* [25] 1=enable data whitening */ - uint32_t : 6; /* [31:26] reserved */ + uint32_t MAXLEN : 8; /* [7:0] maximum payload length in bytes */ + uint32_t STATLEN : 8; /* [15:8] static length added to payload */ + uint32_t BALEN : 3; /* [18:16] base address length (2-4 bytes) */ + uint32_t : 5; /* [23:19] reserved */ + uint32_t ENDIAN : 1; /* [24] 0=little-endian 1=big-endian */ + uint32_t WHITEEN : 1; /* [25] 1=enable data whitening */ + uint32_t : 6; /* [31:26] reserved */ } bit; uint32_t reg; } radio_pcnf1_t; -/* CRCCNF: CRC configuration */ +/** @brief RADIO CRCCNF: CRC configuration register. */ typedef union { struct { - uint32_t LEN : 2; /* [1:0] 0=disabled 1=1 byte 2=2 bytes 3=3 bytes */ - uint32_t : 6; /* [7:2] reserved */ - uint32_t SKIPADDR : 1; /* [8] 1=skip address field in CRC calculation */ - uint32_t : 23; /* [31:9] reserved */ + uint32_t LEN : 2; /* [1:0] 0=disabled 1=1 byte 2=2 bytes 3=3 bytes */ + uint32_t : 6; /* [7:2] reserved */ + uint32_t SKIPADDR : 1; /* [8] 1=skip address field in CRC calculation */ + uint32_t : 23; /* [31:9] reserved */ } bit; uint32_t reg; } radio_crccnf_t; -#define RADIO_CRCCNF_LEN_DISABLED 0u -#define RADIO_CRCCNF_LEN_ONE 1u -#define RADIO_CRCCNF_LEN_TWO 2u -#define RADIO_CRCCNF_LEN_THREE 3u +#define RADIO_CRCCNF_LEN_DISABLED 0u +#define RADIO_CRCCNF_LEN_ONE 1u +#define RADIO_CRCCNF_LEN_TWO 2u +#define RADIO_CRCCNF_LEN_THREE 3u -/* SHORTS: shortcut register */ +/** @brief RADIO SHORTS: hardware shortcut register. */ typedef union { struct { - uint32_t READY_START : 1; /* [0] READY -> TASKS_START */ - uint32_t END_DISABLE : 1; /* [1] END -> TASKS_DISABLE */ - uint32_t DISABLED_TXEN : 1; /* [2] DISABLED -> TASKS_TXEN */ - uint32_t DISABLED_RXEN : 1; /* [3] DISABLED -> TASKS_RXEN */ - uint32_t ADDRESS_RSSISTART : 1; /* [4] ADDRESS -> TASKS_RSSISTART */ - uint32_t END_START : 1; /* [5] END -> TASKS_START */ - uint32_t ADDRESS_BCSTART : 1; /* [6] ADDRESS -> TASKS_BCSTART */ - uint32_t : 1; /* [7] reserved */ - uint32_t DISABLED_RSSISTOP : 1; /* [8] DISABLED -> TASKS_RSSISTOP */ - uint32_t : 23; /* [31:9] reserved */ + uint32_t READY_START : 1; /* [0] READY -> TASKS_START */ + uint32_t END_DISABLE : 1; /* [1] END -> TASKS_DISABLE */ + uint32_t DISABLED_TXEN : 1; /* [2] DISABLED -> TASKS_TXEN */ + uint32_t DISABLED_RXEN : 1; /* [3] DISABLED -> TASKS_RXEN */ + uint32_t ADDRESS_RSSISTART : 1; /* [4] ADDRESS -> TASKS_RSSISTART */ + uint32_t END_START : 1; /* [5] END -> TASKS_START */ + uint32_t ADDRESS_BCSTART : 1; /* [6] ADDRESS -> TASKS_BCSTART */ + uint32_t : 1; /* [7] reserved */ + uint32_t DISABLED_RSSISTOP : 1; /* [8] DISABLED -> TASKS_RSSISTOP */ + uint32_t : 23; /* [31:9] reserved */ } bit; uint32_t reg; } radio_shorts_t; diff --git a/justfile b/justfile index 147bf92..4110b06 100644 --- a/justfile +++ b/justfile @@ -12,6 +12,8 @@ user_ns := `command -v podman >/dev/null 2>&1 \ && echo "--userns=keep-id" \ || echo "--user $(id -u):$(id -g)"` +src_files := "find /src/src /src/include -name '*.c' -o -name '*.h'" + # recipes # build container image (only rebuilt when Dockerfile changes) image-build: @@ -27,6 +29,44 @@ build: image-build -DCMAKE_BUILD_TYPE=MinSizeRel /src \ && ninja -C /src/{{build_dir}}" +# reformat all source files in-place with clang-format +format: image-build + {{engine}} run --rm \ + {{user_ns}} \ + -v "{{justfile_directory()}}:/src:z" \ + {{image}} \ + sh -c "{{src_files}} | xargs clang-format -i --style=file:/src/.clang-format" + +# check formatting without modifying files (used in CI) +format-check: image-build + {{engine}} run --rm \ + {{user_ns}} \ + -v "{{justfile_directory()}}:/src:z" \ + {{image}} \ + sh -c "{{src_files}} | xargs clang-format --dry-run --Werror --style=file:/src/.clang-format" + +# static analysis with cppcheck +lint: image-build + {{engine}} run --rm \ + {{user_ns}} \ + -v "{{justfile_directory()}}:/src:z" \ + {{image}} \ + sh -c "cppcheck --error-exitcode=1 \ + --enable=warning,style,performance,portability \ + --suppress=missingInclude \ + --inline-suppr \ + --std=c11 \ + -I /src/include \ + /src/src/" + +# generate HTML documentation with Doxygen +docs: image-build + {{engine}} run --rm \ + {{user_ns}} \ + -v "{{justfile_directory()}}:/src:z" \ + {{image}} \ + sh -c "cd /src && doxygen Doxyfile" + # flash firmware via pyocd on the host (requires USB / DAPLink) flash: build pyocd flash --target nrf52840 {{build_dir}}/firmware.hex @@ -35,10 +75,10 @@ flash: build gdbserver: pyocd gdbserver --target nrf52840 --port 3333 -# remove build artifacts +# remove build artifacts and generated docs clean: - rm -rf {{build_dir}} + rm -rf {{build_dir}} docs -# remove build artifacts AND the container image +# remove build artifacts, docs AND the container image clean-all: clean {{engine}} rmi {{image}} 2>/dev/null || true diff --git a/src/fhss.c b/src/fhss.c index 7bd39d8..dd946e5 100644 --- a/src/fhss.c +++ b/src/fhss.c @@ -1,13 +1,15 @@ +/** @file fhss.c + * @brief FHSS channel sequencer implementation. + */ #include "fhss.h" #include -#define FHSS_CHANNELS 40u -#define FHSS_DWELL_MS 2u +#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, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; static uint32_t slot; @@ -25,11 +27,11 @@ uint8_t fhss_next_channel(void) /* 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 ); + 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 */ + AES_ECB_encrypt(&ctx, block); /* encrypts block in-place */ slot++; return block[0] % FHSS_CHANNELS; diff --git a/src/main.c b/src/main.c index de9c36d..ca6f3a0 100644 --- a/src/main.c +++ b/src/main.c @@ -1,8 +1,11 @@ +/** @file main.c + * @brief Entry point: initialise peripherals and run the PTT event loop. + */ #include "radio.h" #include "power.h" #include -static const uint8_t test_frame[] = { 0xDE, 0xAD, 0xBE, 0xEF }; +static const uint8_t test_frame[] = {0xDE, 0xAD, 0xBE, 0xEF}; int main(void) { diff --git a/src/power.c b/src/power.c index e3ede6a..49a0b94 100644 --- a/src/power.c +++ b/src/power.c @@ -1,10 +1,13 @@ +/** @file power.c + * @brief Power management implementation. + */ #include "power.h" #include "regs.h" #include #include /* P0.02 on XIAO BLE - adjust to match your schematic */ -#define BUTTON_PIN 2u +#define BUTTON_PIN 2u void power_init(void) { @@ -13,14 +16,13 @@ void power_init(void) NRF_GPIOTE->CONFIG[0] = (gpiote_config_t){ .bit = { - .MODE = GPIOTE_MODE_EVENT, - .PSEL = BUTTON_PIN, - .PORT = 0u, + .MODE = GPIOTE_MODE_EVENT, + .PSEL = BUTTON_PIN, + .PORT = 0u, .POLARITY = 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; NVIC_EnableIRQ(GPIOTE_IRQn); } diff --git a/src/radio.c b/src/radio.c index 64549a2..2cdfa5b 100644 --- a/src/radio.c +++ b/src/radio.c @@ -1,3 +1,6 @@ +/** @file radio.c + * @brief RADIO peripheral driver implementation. + */ #include "radio.h" #include "regs.h" #include @@ -13,8 +16,8 @@ * TX is synchronous: function returns after EVENTS_END fires. */ -#define MAX_PAYLOAD 255u -#define BUF_SIZE (1u + MAX_PAYLOAD) +#define MAX_PAYLOAD 255u +#define BUF_SIZE (1u + MAX_PAYLOAD) static uint8_t pkt_buf[BUF_SIZE]; @@ -22,59 +25,52 @@ static uint8_t pkt_buf[BUF_SIZE]; #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_PREFIX0 0xABu /* logical address 0: RADIO_BASE0 + RADIO_PREFIX0[7:0] */ +#define RADIO_BASE0 0x12345678u +#define RADIO_PREFIX0 0xABu /* logical address 0: RADIO_BASE0 + RADIO_PREFIX0[7:0] */ 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 */ - NRF_RADIO->PCNF0 = (radio_pcnf0_t){ - .bit = { .LFLEN = 8, .S0LEN = 0, .S1LEN = 0, .PLEN = 0, .CRCINC = 0 } - }.reg; + NRF_RADIO->PCNF0 = + (radio_pcnf0_t){.bit = {.LFLEN = 8, .S0LEN = 0, .S1LEN = 0, .PLEN = 0, .CRCINC = 0}}.reg; /* max 255-byte payload, 3-byte base address, little-endian, no whitening */ - NRF_RADIO->PCNF1 = (radio_pcnf1_t){ - .bit = { .MAXLEN = MAX_PAYLOAD, .STATLEN = 0, .BALEN = 3, - .ENDIAN = 0, .WHITEEN = 0 } - }.reg; + NRF_RADIO->PCNF1 = + (radio_pcnf1_t){ + .bit = {.MAXLEN = MAX_PAYLOAD, .STATLEN = 0, .BALEN = 3, .ENDIAN = 0, .WHITEEN = 0}} + .reg; - NRF_RADIO->BASE0 = RADIO_BASE0; + NRF_RADIO->BASE0 = RADIO_BASE0; NRF_RADIO->PREFIX0 = RADIO_PREFIX0; - NRF_RADIO->TXADDRESS = 0; /* transmit on logical address 0 */ - NRF_RADIO->RXADDRESSES = 1; /* receive on logical address 0 */ + NRF_RADIO->TXADDRESS = 0; /* transmit on logical address 0 */ + NRF_RADIO->RXADDRESSES = 1; /* receive on logical address 0 */ /* 2-byte CRC, skip address field */ - NRF_RADIO->CRCCNF = (radio_crccnf_t){ - .bit = { .LEN = RADIO_CRCCNF_LEN_TWO, .SKIPADDR = 1 } - }.reg; - NRF_RADIO->CRCPOLY = 0x11021u; /* CRC-16/CCITT */ + NRF_RADIO->CRCCNF = (radio_crccnf_t){.bit = {.LEN = RADIO_CRCCNF_LEN_TWO, .SKIPADDR = 1}}.reg; + NRF_RADIO->CRCPOLY = 0x11021u; /* CRC-16/CCITT */ NRF_RADIO->CRCINIT = 0xFFFFu; - NRF_RADIO->TXPOWER = (radio_txpower_t){ - .bit = { .TXPOWER = 0 } /* 0 dBm */ - }.reg; + NRF_RADIO->TXPOWER = + (radio_txpower_t){ + .bit = {.TXPOWER = 0} /* 0 dBm */ + } + .reg; - NRF_RADIO->FREQUENCY = (radio_frequency_t){ - .bit = { .FREQUENCY = DEFAULT_CHANNEL, .MAP = RADIO_MAP_DEFAULT } - }.reg; + 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->SHORTS = (radio_shorts_t){.bit = {.READY_START = 1, .END_DISABLE = 1}}.reg; NRF_RADIO->PACKETPTR = (uint32_t)pkt_buf; } void radio_set_channel(uint8_t ch) { - NRF_RADIO->FREQUENCY = (radio_frequency_t){ - .bit = { .FREQUENCY = ch, .MAP = RADIO_MAP_DEFAULT } - }.reg; + NRF_RADIO->FREQUENCY = + (radio_frequency_t){.bit = {.FREQUENCY = ch, .MAP = RADIO_MAP_DEFAULT}}.reg; } void radio_tx(const uint8_t *data, uint8_t len) diff --git a/src/startup.c b/src/startup.c index da72a6a..adc3edd 100644 --- a/src/startup.c +++ b/src/startup.c @@ -1,12 +1,15 @@ +/** @file startup.c + * @brief Vector table and Reset_Handler for nRF52840 (no SoftDevice). + */ #include /* 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 _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 uint32_t _estack; /* address equals initial SP value */ extern int main(void); @@ -14,71 +17,73 @@ void Reset_Handler(void); static void __attribute__((used)) Default_Handler(void) { - while (1); + 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 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"))); +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"))); +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] = { +__attribute__((section(".isr_vector"), used)) const vector_fn vectors[64] = { /* 0 */ (vector_fn)&_estack, /* 1 */ Reset_Handler, /* 2 */ NMI_Handler, @@ -126,8 +131,8 @@ const vector_fn vectors[64] = { /* IRQ27 */ TIMER4_IRQHandler, /* IRQ28 */ PWM0_IRQHandler, /* IRQ29 */ PDM_IRQHandler, - /* IRQ30 */ 0, /* reserved */ - /* IRQ31 */ 0, /* reserved */ + /* IRQ30 */ 0, /* reserved */ + /* IRQ31 */ 0, /* reserved */ /* IRQ32 */ MWU_IRQHandler, /* IRQ33 */ PWM1_IRQHandler, /* IRQ34 */ PWM2_IRQHandler, @@ -139,10 +144,10 @@ const vector_fn vectors[64] = { /* IRQ40 */ UARTE1_IRQHandler, /* IRQ41 */ QSPI_IRQHandler, /* IRQ42 */ CRYPTOCELL_IRQHandler, - /* IRQ43 */ 0, /* reserved */ - /* IRQ44 */ 0, /* reserved */ + /* IRQ43 */ 0, /* reserved */ + /* IRQ44 */ 0, /* reserved */ /* IRQ45 */ PWM3_IRQHandler, - /* IRQ46 */ 0, /* reserved */ + /* IRQ46 */ 0, /* reserved */ /* IRQ47 */ SPIM3_IRQHandler, }; @@ -153,16 +158,19 @@ void Reset_Handler(void) /* copy .data initializers from Flash to RAM */ uint32_t *src = &_sidata; uint32_t *dst = &_sdata; + // cppcheck-suppress comparePointers while (dst < &_edata) { *dst++ = *src++; } /* zero .bss */ dst = &_sbss; + // cppcheck-suppress comparePointers while (dst < &_ebss) { *dst++ = 0u; } main(); - while (1); + while (1) + ; }