From b34601239d849b9c5a653d9905e05cbbc763a86e Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Tue, 15 Sep 2026 13:05:58 -0700 Subject: [PATCH 1/2] all: Add the Morse Micro Wi-Fi HaLow driver. A portable driver for the Morse Micro MM6108/MM8108 802.11ah (Wi-Fi HaLow) transceivers, extracted from the MicroPython network.HALOW pull request and reorganised along the lines of cyw43-driver: - src/ holds the driver: the SD-over-SPI transport to the transceiver mapped onto a small mm_halow_port_* interface, an lwIP netif, a packet-memory pool, and an OSAL/scheduler shim that runs the vendor library (morselib) cooperatively off the host's scheduler with no vendor RTOS. - mm_halow_config.h defines the host integration contract; the embedder provides mm_halow_configport.h (or MM_HALOW_CONFIG_FILE) with atomic sections, a millisecond tick, pin accessors, interrupt-context detection, and the port functions (SPI, private heap backing, hardware RNG, fallback MAC, optional edge IRQ). - lib/mm-iot-sdk is the Morse Micro MM-IoT-SDK submodule, providing the prebuilt morselib and the transceiver firmware/BCF blobs; mm_halow.mk is a build fragment for make-based embedders covering morselib's newlib linkage, the blob embedding, and fetching the mm-iot-sdk submodule on demand. - tests/host compiles the real allocator natively (233 checks, plus an ASan/UBSan variant); tests/qemu runs the real scheduler, whose context switch is naked asm, on a Cortex-M55 under QEMU (51 checks); tests/hardware carries the on-device throughput soak harness the driver was validated with. Everything the driver needs from MicroPython in the original pull request now arrives through the configuration contract: MICROPY_* macros became MM_HALOW_*, the halow_* API became mm_halow_*, and the machine-SPI, GC-heap and pin-interrupt couplings became port hooks. Tested: both test suites pass locally (host: gcc -m32 + ASan; qemu: arm-none-eabi-gcc 14.3.1 on mps3-an547). Signed-off-by: Kwabena W. Agyeman --- .gitmodules | 3 + README.md | 62 +- lib/mm-iot-sdk | 1 + mm_halow.mk | 89 ++ src/mm_halow.h | 304 +++++++ src/mm_halow_config.h | 205 +++++ src/mm_halow_ctrl.c | 1148 ++++++++++++++++++++++++ src/mm_halow_hal.c | 328 +++++++ src/mm_halow_lwip.c | 251 ++++++ src/mm_halow_osal.c | 766 ++++++++++++++++ src/mm_halow_osal.h | 50 ++ src/mm_halow_pktmem.c | 284 ++++++ src/mm_halow_sched.c | 366 ++++++++ src/mm_halow_sched.h | 109 +++ src/mmport.h | 18 + tests/hardware/README.md | 22 + tests/hardware/halow_config.py.example | 5 + tests/hardware/thru_device.py | 121 +++ tests/hardware/thru_file.py | 128 +++ tests/hardware/thru_server.py | 114 +++ tests/host/.gitignore | 2 + tests/host/Makefile | 27 + tests/host/stub/lwip/dhcp.h | 6 + tests/host/stub/lwip/netif.h | 14 + tests/host/stub/mm_halow_configport.h | 73 ++ tests/host/test_alloc.c | 462 ++++++++++ tests/qemu/.gitignore | 4 + tests/qemu/Makefile | 67 ++ tests/qemu/mps3.ld | 55 ++ tests/qemu/startup.c | 108 +++ tests/qemu/stub/lwip/dhcp.h | 6 + tests/qemu/stub/lwip/netif.h | 14 + tests/qemu/stub/mm_halow_configport.h | 67 ++ tests/qemu/test_sched.c | 607 +++++++++++++ 34 files changed, 5885 insertions(+), 1 deletion(-) create mode 100644 .gitmodules create mode 160000 lib/mm-iot-sdk create mode 100644 mm_halow.mk create mode 100644 src/mm_halow.h create mode 100644 src/mm_halow_config.h create mode 100644 src/mm_halow_ctrl.c create mode 100644 src/mm_halow_hal.c create mode 100644 src/mm_halow_lwip.c create mode 100644 src/mm_halow_osal.c create mode 100644 src/mm_halow_osal.h create mode 100644 src/mm_halow_pktmem.c create mode 100644 src/mm_halow_sched.c create mode 100644 src/mm_halow_sched.h create mode 100644 src/mmport.h create mode 100644 tests/hardware/README.md create mode 100644 tests/hardware/halow_config.py.example create mode 100644 tests/hardware/thru_device.py create mode 100644 tests/hardware/thru_file.py create mode 100644 tests/hardware/thru_server.py create mode 100644 tests/host/.gitignore create mode 100644 tests/host/Makefile create mode 100644 tests/host/stub/lwip/dhcp.h create mode 100644 tests/host/stub/lwip/netif.h create mode 100644 tests/host/stub/mm_halow_configport.h create mode 100644 tests/host/test_alloc.c create mode 100644 tests/qemu/.gitignore create mode 100644 tests/qemu/Makefile create mode 100644 tests/qemu/mps3.ld create mode 100644 tests/qemu/startup.c create mode 100644 tests/qemu/stub/lwip/dhcp.h create mode 100644 tests/qemu/stub/lwip/netif.h create mode 100644 tests/qemu/stub/mm_halow_configport.h create mode 100644 tests/qemu/test_sched.c diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4bbeed4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/mm-iot-sdk"] + path = lib/mm-iot-sdk + url = https://github.com/MorseMicro/mm-iot-sdk.git diff --git a/README.md b/README.md index 2230079..3033cf0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,64 @@ Morse Micro 802.11ah HaLow driver ================================= -This is a driver for the Morse Micro MM6108/MM8108 802.11ah (Wi-Fi HaLow) transceivers. +A portable driver for the Morse Micro MM6108/MM8108 802.11ah (Wi-Fi HaLow) +transceivers: host-agnostic C that an embedding environment integrates by +providing a small configuration header. It presents a compact station model -- +a state object holding the lwIP interfaces, a link status that folds the WLAN +and TCP/IP state together, and a poll function the host drives. + +The vendor stack (`morselib`) and the transceiver firmware/board-configuration +blobs come from the Morse Micro +[MM-IoT-SDK](https://github.com/MorseMicro/mm-iot-sdk), included here as the +`lib/mm-iot-sdk` submodule. `morselib` is a prebuilt library under the Morse +Micro Binary Distribution Licence; the driver runs it cooperatively off the +host's scheduler through a small OSAL shim -- no vendor RTOS. + +## Layout + +- `src/` -- the driver: + - `mm_halow_config.h` -- the host integration contract. The embedder + provides `mm_halow_configport.h` (or the file named by + `MM_HALOW_CONFIG_FILE`) supplying atomic sections, a millisecond tick, + pin accessors, and the `mm_halow_port_*` functions (SPI transport, private + heap backing, hardware RNG, fallback MAC, optional edge IRQ). + - `mm_halow_ctrl.c` -- driver core: init/deinit, connect/scan/status, the + morselib event plumbing. + - `mm_halow_lwip.c` -- the lwIP netif. + - `mm_halow_hal.c` -- morselib's `mmhal` interface, mapped onto the + `mm_halow_port_*` hooks (SD-over-SPI framing, reset/wake sequencing, + firmware/BCF blob serving). + - `mm_halow_osal.c` / `mm_halow_sched.c` -- morselib's `mmosal` interface: a + private heap (morselib allocates from interrupt context, where a host GC + must not run) and a cooperative scheduler with a wall-clock budget. + - `mm_halow_pktmem.c` -- the packet-memory pool. + - `mmport.h` -- morselib's port header (name fixed by the SDK). +- `mm_halow.mk` -- build fragment for make-based embedders: prebuilt morselib + linkage (grouped with the target multilib's libc/libm) and the firmware/BCF + blob embedding. +- `tests/host` -- allocator tests, compiled natively from the real sources + (`make`, `make asan`). +- `tests/qemu` -- scheduler tests on a Cortex-M55 under QEMU (`make`), since + the context switch is naked asm that cannot run on the host. + +## Integrating + +1. Add `src/` to the include path along with + `lib/mm-iot-sdk/framework/morselib/include`, and compile `src/*.c`. +2. Provide `mm_halow_configport.h`; `src/mm_halow_config.h` documents every + hook and fails the build naming whatever is missing. +3. Link the prebuilt `morselib` and the transceiver firmware blob -- + `mm_halow.mk` does both for make-based builds. morselib is built against + newlib and pulls in a few C library functions (`sscanf`, `qsort`, `setjmp`, + ...) that in turn reference newlib's syscall back-end (`_sbrk`, `_write`, + ...). The host must provide that back-end; MicroPython's ports already do. + morselib never calls these at run time, so the back-end only needs to + satisfy the link -- and `_sbrk` in particular should fail rather than hand + out memory, since the driver runs entirely from its own heap. (If the host + links `-nostdlib`, also group libc/libm with morselib, as the fragment + does.) +4. Drive `mm_halow_poll()` from the host's deferred-work mechanism and call + `mm_halow_schedule_poll()` when the transceiver's IRQ line asserts (or poll + by level; the driver checks the line each pass). + +The MicroPython `network.HALOW` binding is the reference embedding. diff --git a/lib/mm-iot-sdk b/lib/mm-iot-sdk new file mode 160000 index 0000000..88606f9 --- /dev/null +++ b/lib/mm-iot-sdk @@ -0,0 +1 @@ +Subproject commit 88606f94a929685144e5fdaf432310820ed2c8e0 diff --git a/mm_halow.mk b/mm_halow.mk new file mode 100644 index 0000000..cd8d490 --- /dev/null +++ b/mm_halow.mk @@ -0,0 +1,89 @@ +# Build fragment for embedding builds (make-based): links the prebuilt +# morselib and the transceiver firmware/BCF blobs. The including build must +# provide TOP, BUILD, CC, CFLAGS, OBJCOPY and collect LIBS/OBJ/CFLAGS. + +MM_HALOW_DIR ?= lib/mm-halow-driver +MM_HALOW_TOP ?= $(TOP)/$(MM_HALOW_DIR) +MM_HALOW_MMIOT_DIR = $(MM_HALOW_TOP)/lib/mm-iot-sdk/framework +MM_HALOW_MORSELIB_DIR = $(MM_HALOW_MMIOT_DIR)/morselib + +# morselib is distributed as a prebuilt library under the Morse Micro Binary +# Distribution Licence. Building it from source instead is useful for +# debugging, but those sources are GPL-3.0, which is not compatible with the +# rest of this firmware, so the prebuilt library is the default. +MM_HALOW_MORSELIB_CORE ?= arm-cortex-m33f + +# The MM-IoT-SDK (prebuilt morselib + the transceiver firmware blobs) is a +# submodule of this driver, i.e. a submodule of a submodule of the embedding +# project. A recursive checkout populates it, but the embedder's flat "fetch +# my submodules" step is usually not recursive (MicroPython's `make submodules` +# is not), which leaves the SDK empty -- and both its source files (compiled +# below) and its firmware blobs then fail with "No such file". Fetch it here +# at parse time, before anything reads from it, guarded so it is a no-op once +# the SDK is present. +ifeq ($(wildcard $(MM_HALOW_MORSELIB_DIR)/lib/$(MM_HALOW_MORSELIB_CORE)/libmorse.a),) +$(info Fetching the mm-iot-sdk nested submodule...) +$(shell cd $(MM_HALOW_TOP) && git submodule update --init lib/mm-iot-sdk >&2) +endif +ifeq ($(MM_HALOW_MORSELIB_SOURCE),1) +INC += $(addprefix -I$(MM_HALOW_MORSELIB_DIR)/,src src/internal src/emmet src/umac/rc/mmrc_osal mmrc/src/core) +SRC_THIRDPARTY_C += $(patsubst $(TOP)/%,%,\ + $(shell find $(MM_HALOW_MORSELIB_DIR)/src $(MM_HALOW_MORSELIB_DIR)/mmrc/src -name '*.c')) +CFLAGS_THIRDPARTY += -DLOOKAROUND_FAIL_MAX=50 -Wno-c++-compat +else +# morselib is prebuilt against newlib, but the ports link with -nostdlib, so the +# C library functions it calls (sscanf, qsort, setjmp, _ctype_, ...) are not +# otherwise pulled in. Resolve libc/libm for the target multilib the same way +# the ports resolve libgcc, and group them with the archive so the linker settles +# the references between morselib and libc regardless of order. These are lazily +# expanded: CFLAGS only carries the -mcpu flags that select the multilib once the +# including port has finished adding them, after this file is included. +MM_HALOW_LIBC = $(shell $(CC) $(CFLAGS) -print-file-name=libc.a) +MM_HALOW_LIBM = $(shell $(CC) $(CFLAGS) -print-file-name=libm.a) +LIBS += -Wl,--start-group $(MM_HALOW_MORSELIB_DIR)/lib/$(MM_HALOW_MORSELIB_CORE)/libmorse.a $(MM_HALOW_LIBC) $(MM_HALOW_LIBM) -Wl,--end-group +endif + +# objcopy derives a blob's symbol names from its path, mangling everything that +# is not alphanumeric into an underscore. +mm_halow_blob_sym = _binary_$(subst .,_,$(subst -,_,$(subst /,_,$(1))))_$(2) + +# Output format for the blobs. Overridable, as the only thing tying the driver +# to a particular architecture is the prebuilt morselib. +MM_HALOW_BFDNAME ?= elf32-littlearm +MM_HALOW_BFDARCH ?= arm + +# The transceiver firmware, and optionally a board configuration file holding +# calibration data, are linked in as binary blobs. A board picks its BCF by +# name with MM_HALOW_BCF; the SDK keeps them per chip under morsefirmware. +MM_HALOW_CHIPSET ?= mm8108 +MM_HALOW_BCF ?= mf15457 +MM_HALOW_FW_MBIN ?= $(MM_HALOW_MMIOT_DIR)/morsefirmware/mm8108b2-rl.mbin +ifneq ($(MM_HALOW_BCF),) +MM_HALOW_BCF_MBIN ?= $(MM_HALOW_MMIOT_DIR)/morsefirmware/$(MM_HALOW_CHIPSET)/bcfs/bcf_$(MM_HALOW_BCF).mbin +endif +MM_HALOW_FW_OBJ = $(BUILD)/$(MM_HALOW_DIR)/mm_halow_firmware.o + +$(MM_HALOW_FW_OBJ): $(MM_HALOW_FW_MBIN) + $(ECHO) "GEN $@" + $(Q)$(MKDIR) -p $(dir $@) + $(Q)$(OBJCOPY) -I binary -O $(MM_HALOW_BFDNAME) -B $(MM_HALOW_BFDARCH) $< $@ \ + --redefine-sym $(call mm_halow_blob_sym,$<,start)=mm_halow_firmware_start \ + --redefine-sym $(call mm_halow_blob_sym,$<,end)=mm_halow_firmware_end \ + --rename-section .data=.rodata.mm_halow_firmware,contents,alloc,load,readonly,data \ + --set-section-alignment .data=4 + +ifneq ($(MM_HALOW_BCF_MBIN),) +CFLAGS += -DMM_HALOW_BCF=1 +MM_HALOW_BCF_OBJ = $(BUILD)/$(MM_HALOW_DIR)/mm_halow_bcf.o + +$(MM_HALOW_BCF_OBJ): $(MM_HALOW_BCF_MBIN) + $(ECHO) "GEN $@" + $(Q)$(MKDIR) -p $(dir $@) + $(Q)$(OBJCOPY) -I binary -O $(MM_HALOW_BFDNAME) -B $(MM_HALOW_BFDARCH) $< $@ \ + --redefine-sym $(call mm_halow_blob_sym,$<,start)=mm_halow_bcf_start \ + --redefine-sym $(call mm_halow_blob_sym,$<,end)=mm_halow_bcf_end \ + --rename-section .data=.rodata.mm_halow_bcf,contents,alloc,load,readonly,data \ + --set-section-alignment .data=4 +endif + +OBJ += $(MM_HALOW_FW_OBJ) $(MM_HALOW_BCF_OBJ) diff --git a/src/mm_halow.h b/src/mm_halow.h new file mode 100644 index 0000000..0038f1a --- /dev/null +++ b/src/mm_halow.h @@ -0,0 +1,304 @@ +/* + * This file is part of mm-halow-driver. + * + * Copyright (c) 2026 OpenMV LLC. + * + * SPDX-License-Identifier: MIT + * + * Driver for the Morse Micro MM6108/MM8108 802.11ah (Wi-Fi HaLow) transceivers. + * + * This layer sits between MicroPython's network module and morselib, and + * presents a compact model to the port: a state object holding the lwIP + * interfaces, a link status that folds the WLAN and TCP/IP state together, and + * a poll function driven from PendSV. + */ +#ifndef MM_HALOW_INCLUDED_HALOW_H +#define MM_HALOW_INCLUDED_HALOW_H + +#include +#include +#include + +#include "mm_halow_config.h" + +#include "lwip/netif.h" +#include "lwip/dhcp.h" + +#include "mmwlan.h" + +// morselib already serialises its own API through the OSAL mutexes, which block +// by running the scheduler, so the driver adds no lock of its own. A lock that +// suspended the poll across a morselib call would stall those waits instead. + +// Stations the AP will admit, matching the morselib default. +#define MM_HALOW_AP_MAX_STAS (MMWLAN_DEFAULT_AP_MAX_STAS) + +// Interfaces, matching the order of MOD_NETWORK_STA_IF and MOD_NETWORK_AP_IF. +#define MM_HALOW_ITF_STA (0) +#define MM_HALOW_ITF_AP (1) +#define MM_HALOW_ITF_MAX (2) + +// Link status values, matching the network.WLAN status codes so that +// network.HALOW reports the same values. +#define MM_HALOW_LINK_DOWN (0) // link is down +#define MM_HALOW_LINK_JOIN (1) // connecting to an AP +#define MM_HALOW_LINK_NOIP (2) // associated, but no IP address +#define MM_HALOW_LINK_UP (3) // associated with an IP address +#define MM_HALOW_LINK_FAIL (-1) // connection failed +#define MM_HALOW_LINK_NONET (-2) // no matching SSID found +#define MM_HALOW_LINK_BADAUTH (-3) // authentication failure + +// Security types. 802.11ah has no WPA2-PSK: HaLow networks are either open, +// OWE (opportunistic encryption) or SAE (WPA3). Fixed to literals rather than +// aliased to the morselib enum: these are a public API, and a value must not +// change if the SDK renumbers its enum. network_halow.c static-asserts the +// pairing, so a divergence fails the build instead of silently breaking users. +#define MM_HALOW_SEC_OPEN (0) +#define MM_HALOW_SEC_OWE (1) +#define MM_HALOW_SEC_SAE (2) + +// Radio settings, as accepted by mm_halow_wifi_set_radio(). +#define MM_HALOW_RADIO_AMPDU (0) +#define MM_HALOW_RADIO_SGI (1) +#define MM_HALOW_RADIO_SUBBANDS (2) +#define MM_HALOW_RADIO_RTS (3) +#define MM_HALOW_RADIO_FRAG (4) +#define MM_HALOW_RADIO_LISTEN (5) +#define MM_HALOW_RADIO_WNM_PD (6) +#define MM_HALOW_RADIO_TXPOWER (7) + +// Target wake time negotiation, as asked for in the association request. +// Fixed to literals; see the security types above. +#define MM_HALOW_TWT_REQUEST (0) +#define MM_HALOW_TWT_SUGGEST (1) +#define MM_HALOW_TWT_DEMAND (2) + +// How the regulatory airtime allowance is spent. Fixed to literals; see above. +#define MM_HALOW_DUTY_CYCLE_SPREAD (0) +#define MM_HALOW_DUTY_CYCLE_BURST (1) + +// Fields packed into the rate word of a rate control statistics entry: four +// bits of bandwidth, four of rate, then a single guard interval bit. +#define MM_HALOW_RC_RATE_SHIFT (MMWLAN_RC_STATS_RATE_INFO_RATE_OFFSET) +#define MM_HALOW_RC_BW_SHIFT (MMWLAN_RC_STATS_RATE_INFO_BW_OFFSET) +#define MM_HALOW_RC_GI_SHIFT (MMWLAN_RC_STATS_RATE_INFO_GUARD_OFFSET) +#define MM_HALOW_RC_FIELD_MASK (0xf) +#define MM_HALOW_RC_GI_MASK (0x1) + +// Rate table entries reported by status("rates"). Every combination the rate +// word can encode is 16 rates by 4 bandwidths by 2 guard intervals, so this +// cannot truncate a real table; it is a bound on a length the transceiver +// reports rather than one this driver chose. +#define MM_HALOW_RC_STATS_MAX (128) + +// Power management modes, as accepted by mm_halow_wifi_pm(). +#define MM_HALOW_PM_NONE (0) // always listening +#define MM_HALOW_PM_POWERSAVE (1) // transmit only, transceiver dozes + +// Trace flags. +#define MM_HALOW_TRACE_ASYNC_EV (0x0001) +#define MM_HALOW_TRACE_ETH_TX (0x0002) +#define MM_HALOW_TRACE_ETH_RX (0x0004) +#define MM_HALOW_TRACE_ETH_FULL (0x0008) +#define MM_HALOW_TRACE_MAC (0x0010) + +// A single scan result, flattened out of struct mmwlan_scan_result. +typedef struct _mm_halow_ev_scan_result_t { + uint8_t ssid_len; + uint8_t ssid[MMWLAN_SSID_MAXLEN]; + uint8_t bssid[MMWLAN_MAC_ADDR_LEN]; + int16_t rssi; + // Centre frequency of the channel the frame was RECEIVED on, in Hz. A wide + // AP beacons on its primary channel so that narrowband stations can hear it, + // so this is not the center of its operating channel: an 8MHz AP centered on + // 916MHz is seen here at the primary channel's frequency. + uint32_t channel_freq_hz; + uint8_t chan_num; // S1G channel number, 0 if not a local one + uint8_t bw_mhz; // bandwidth the frame was received on + uint8_t op_bw_mhz; // operating bandwidth of the AP + uint8_t security; // one of HALOW_SEC_xxx +} mm_halow_ev_scan_result_t; + +#if MM_HALOW_ENABLE_AP +#include "shared/netutils/dhcpserver.h" +#endif + +typedef struct _mm_halow_t { + uint8_t itf_state; // bitmask of interfaces brought up + + uint32_t trace_flags; + + // State for asynchronous events. + volatile bool scan_active; + // Set while the driver is being run from inside lwIP, so that a received + // frame is dropped rather than pushed back into it. See mm_halow_send_ethernet(). + volatile bool rx_deferred; + uint32_t pm; + uint32_t ps_timeout_ms; + // Radio settings, kept here because morselib only accepts some of them + // while it is inactive, so they are applied when the driver initialises. + bool ampdu; + bool sgi; + bool subbands; + uint16_t listen_interval; + uint16_t txpower; + uint8_t duty_cycle_mode; + unsigned rts_threshold; + unsigned fragment_threshold; + // Whether entering WNM sleep should also power the transceiver down. + bool wnm_powerdown; + uint32_t health_min_ms; + uint32_t health_max_ms; + // Results from the current sweep. Allocated from the driver heap rather + // than inline, as mm_halow_t is a static global. + mm_halow_ev_scan_result_t *scan_cache; + uint8_t scan_cache_len; + uint8_t scan_cache_max; + volatile uint32_t scan_started_ms; + volatile int8_t link_status; + + // morselib has been mmwlan_init()ed. NOT the same as the transceiver being + // usable: taking an interface down calls mmwlan_shutdown() and leaves this + // set, so anything that actually talks to the chip must test `booted`. + bool initted; + // Whether the transceiver has been booted. Booting it a second time + // fails, and both interfaces share the one transceiver. + bool booted; + + // Network last asked for, so that config("ssid") can report it. The + // passphrase is not kept: morselib takes its own copy and nothing here + // needs to read it back. + uint8_t sta_ssid_len; + uint8_t sta_ssid[MMWLAN_SSID_MAXLEN]; + + #if MM_HALOW_ENABLE_AP + // AP settings. + uint32_t ap_auth; + uint8_t ap_ssid_len; + uint8_t ap_key_len; + uint8_t ap_ssid[MMWLAN_SSID_MAXLEN]; + uint8_t ap_key[MMWLAN_PASSPHRASE_MAXLEN]; + // S1G channel number the AP should use, or zero to pick one from the + // regulatory domain. + uint8_t ap_chan_num; + // Stations associated with the AP. morselib reports each status change but + // has no enumeration API, so the list is maintained here. + uint8_t ap_sta_count; + uint8_t ap_stas[MM_HALOW_AP_MAX_STAS][MMWLAN_MAC_ADDR_LEN]; + #endif + + // Channel list for the configured country, needed to map an AP channel + // number onto its operating class. + const struct mmwlan_s1g_channel_list *channels; + + // lwIP data. + struct netif netif[MM_HALOW_ITF_MAX]; + #if LWIP_IPV4 && LWIP_DHCP + struct dhcp dhcp_client; + #endif + #if MM_HALOW_ENABLE_AP + dhcp_server_t dhcp_server; + #endif + + // MAC address, from the transceiver's OTP or derived from the MCU's UID. + uint8_t mac[MMWLAN_MAC_ADDR_LEN]; + + // Country code most recently passed to mm_halow_wifi_set_up(). + char country[2]; +} mm_halow_t; + +extern mm_halow_t mm_halow_state; + +// Set while the driver is up, so the port knows whether to poll it. morselib +// publishes no next-deadline query, so the driver cannot say when it next needs +// servicing and is polled on every tick instead. +extern void (*mm_halow_poll)(void); + +/*******************************************************************************/ +// Control + +int mm_halow_init(mm_halow_t *self); +void mm_halow_deinit(mm_halow_t *self); + +// Release the driver on soft reset. Declared for ports, which cannot include +// this header without morselib's, so they declare it themselves. + +// Run any work morselib has pending. Must not be called re-entrantly; the port +// schedules it via PendSV, at the priority it polls its network interfaces. +void mm_halow_poll_func(void); + +// Ask the port to run mm_halow_poll_func() soon. The default implementation does +// nothing and relies on the periodic poll; ports override it to raise PendSV. +void mm_halow_schedule_poll(void); + +// True if the regulatory database has an 802.11ah channel list for this country. +// Unlike 2.4GHz there is no worldwide fallback, so the country must be set. +bool mm_halow_country_supported(const char *country); + +int mm_halow_wifi_set_up(mm_halow_t *self, int itf, bool up, const char *country); + +// Run one sweep and copy out what it found, up to max results. +size_t mm_halow_wifi_scan_cached(mm_halow_t *self, mm_halow_ev_scan_result_t *out, size_t max); +int mm_halow_wifi_join(mm_halow_t *self, size_t ssid_len, const uint8_t *ssid, + size_t key_len, const uint8_t *key, uint32_t auth_type, const uint8_t *bssid); +int mm_halow_wifi_leave(mm_halow_t *self, int itf); +int mm_halow_wifi_link_status(mm_halow_t *self, int itf); +int mm_halow_wifi_get_mac(mm_halow_t *self, int itf, uint8_t mac[6]); +int mm_halow_wifi_get_bssid(mm_halow_t *self, uint8_t bssid[6]); +int mm_halow_wifi_get_rssi(mm_halow_t *self, int32_t *rssi); +int mm_halow_wifi_get_channel(mm_halow_t *self, int itf, uint16_t *chan_num, uint8_t *bw_mhz); +int mm_halow_wifi_pm(mm_halow_t *self, uint32_t pm); +int mm_halow_wifi_set_ps_timeout(mm_halow_t *self, uint32_t ms); +int mm_halow_wifi_get_ps_timeout(mm_halow_t *self, uint32_t *ms); +int mm_halow_wifi_get_pm(mm_halow_t *self, uint32_t *pm); +int mm_halow_wifi_wnm_sleep(mm_halow_t *self, bool enable, bool powerdown); +int mm_halow_wifi_get_version(mm_halow_t *self, struct mmwlan_version *version); +int mm_halow_wifi_set_radio(mm_halow_t *self, int what, uint32_t value); +uint32_t mm_halow_wifi_get_radio(mm_halow_t *self, int what); +int mm_halow_wifi_twt(mm_halow_t *self, uint64_t interval_us, uint32_t duration_us, int setup); +int mm_halow_wifi_get_rc_stats(mm_halow_t *self, struct mmwlan_rc_stats **stats); +void mm_halow_wifi_free_rc_stats(struct mmwlan_rc_stats *stats); +int mm_halow_wifi_set_health_check(mm_halow_t *self, uint32_t min_ms, uint32_t max_ms); +int mm_halow_wifi_set_duty_cycle(mm_halow_t *self, int mode); +int mm_halow_wifi_get_duty_cycle(mm_halow_t *self, struct mmwlan_duty_cycle_stats *stats); +int mm_halow_wifi_ate_command(mm_halow_t *self, uint8_t *cmd, size_t cmd_len, + uint8_t *rsp, size_t *rsp_len); +int mm_halow_wifi_fixed_rate(mm_halow_t *self, int mcs, int bw_mhz, int gi); + +void mm_halow_wifi_ap_set_ssid(mm_halow_t *self, size_t len, const uint8_t *buf); +void mm_halow_wifi_ap_set_password(mm_halow_t *self, size_t len, const uint8_t *buf); +void mm_halow_wifi_ap_set_auth(mm_halow_t *self, uint32_t auth); +void mm_halow_wifi_ap_set_channel(mm_halow_t *self, uint8_t chan_num); +void mm_halow_wifi_ap_get_ssid(mm_halow_t *self, size_t *len, const uint8_t **buf); +uint32_t mm_halow_wifi_ap_get_auth(mm_halow_t *self); +int mm_halow_wifi_ap_get_stas(mm_halow_t *self, int *num_stas, uint8_t *macs); + +/*******************************************************************************/ +// Datapath + +int mm_halow_send_ethernet(mm_halow_t *self, int itf, size_t len, const void *buf, bool is_pbuf); + +// Overall link status, folding the TCP/IP state into the WLAN link status. +int mm_halow_tcpip_link_status(mm_halow_t *self, int itf); + +// lwIP glue, implemented in mm_halow_lwip.c and called from mm_halow_ctrl.c. +void mm_halow_cb_tcpip_init(mm_halow_t *self, int itf); +void mm_halow_cb_tcpip_deinit(mm_halow_t *self, int itf); +void mm_halow_cb_tcpip_set_link_up(mm_halow_t *self, int itf); +void mm_halow_cb_tcpip_set_link_down(mm_halow_t *self, int itf); +// Hand a received frame to lwIP. morselib reports the 802.3 header and the +// payload separately and they are not contiguous, so both are passed through. +void mm_halow_cb_process_ethernet(void *cb_data, int itf, + const uint8_t *header, size_t header_len, const uint8_t *payload, size_t payload_len); + +/*******************************************************************************/ +// HAL hooks + +// Poll the transceiver's interrupt lines, implemented in mm_halow_hal.c. +void mm_halow_hal_poll_irqs(void); + +// Re-enable the transceiver's pin interrupt after a poll has drained it. +void mm_halow_hal_irq_rearm(void); + + +#endif // MM_HALOW_INCLUDED_HALOW_H diff --git a/src/mm_halow_config.h b/src/mm_halow_config.h new file mode 100644 index 0000000..be1d4f6 --- /dev/null +++ b/src/mm_halow_config.h @@ -0,0 +1,205 @@ +/* + * This file is part of mm-halow-driver. + * + * Copyright (c) 2026 OpenMV LLC. + * + * SPDX-License-Identifier: MIT + * + * Host integration contract for the mm-halow driver: everything the driver + * needs from the embedding environment is declared here, and the embedder + * provides it in mm_halow_configport.h (or the file named by + * MM_HALOW_CONFIG_FILE). + */ +#ifndef MM_HALOW_INCLUDED_CONFIG_H +#define MM_HALOW_INCLUDED_CONFIG_H + +#include +#include +#include + +// Import port-specific configuration file. +#ifdef MM_HALOW_CONFIG_FILE +#include MM_HALOW_CONFIG_FILE +#else +#include +#endif + +/*******************************************************************************/ +// Driver options. Defaults here; a port overrides them in its config file. + +// Access point mode. morselib's AP support is an alpha API and +// mmwlan_ap_enable() does not currently succeed on the MM8108, so the mode is +// built out rather than offered and failing. +#ifndef MM_HALOW_ENABLE_AP +#define MM_HALOW_ENABLE_AP (0) +#endif + +// Optional falling-edge interrupt on the IRQ line. The line is read by level +// on every poll regardless, so a port without an edge-interrupt-capable IRQ +// pin just leaves this off and waits for the next poll. +#ifndef MM_HALOW_ENABLE_PIN_IRQ +#define MM_HALOW_ENABLE_PIN_IRQ (0) +#endif + +// The morselib chipset descriptor to bind to (mmhal_mm8108, ...). +#ifndef MM_HALOW_CHIPSET +#define MM_HALOW_CHIPSET mmhal_mm8108 +#endif + +// SPI clock. At 50MHz the SD-over-SPI framing corrupts under sustained traffic +// and the transceiver stops answering, so the default is conservative. Note +// this is a request, not a setting: stm32 and mimxrt round down to the nearest +// rate, but alif truncates clk / speed and DesignWare SSI forces the divider +// even, so an unevenly-dividing request can yield a *faster* bus (40MHz becomes +// 50MHz on a 200MHz AHB) -- check the rate the peripheral actually produced. +#ifndef MM_HALOW_SPI_BAUDRATE +#define MM_HALOW_SPI_BAUDRATE (25000000) +#endif + +// Set by boards that invert the transceiver's BUSY output before it reaches the +// MCU. RESET_N is active low at the transceiver and is not configurable. +#ifndef MM_HALOW_BUSY_INVERTED +#define MM_HALOW_BUSY_INVERTED (0) +#endif + +// IP MTU. morselib accepts frames up to MMHAL_WLAN_MMPKT_TX_MAX_SIZE, but +// 802.11ah carries ordinary Ethernet traffic so the usual 1500 applies. +#ifndef MM_HALOW_MTU +#define MM_HALOW_MTU (1500) +#endif + +// Address the station comes up on when the build has no DHCP client. Unused +// otherwise, as the lease supplies all three. +#ifndef MM_HALOW_STA_ADDRESS +#define MM_HALOW_STA_ADDRESS (0xc0a80102) // 192.168.1.2 +#endif +#ifndef MM_HALOW_STA_NETMASK +#define MM_HALOW_STA_NETMASK (0xffffff00) // 255.255.255.0 +#endif +#ifndef MM_HALOW_STA_GATEWAY +#define MM_HALOW_STA_GATEWAY (0xc0a80101) // 192.168.1.1 +#endif + +// Address the soft AP hands out, if the board does not override it. +#ifndef MM_HALOW_AP_ADDRESS +#define MM_HALOW_AP_ADDRESS (0xc0a80401) // 192.168.4.1 +#endif +#ifndef MM_HALOW_AP_NETMASK +#define MM_HALOW_AP_NETMASK (0xffffff00) // 255.255.255.0 +#endif + +// Networks one scan can report. A sweep that finds more than this drops the +// rest, so it is the ceiling on what scan() can return. +#ifndef MM_HALOW_SCAN_CACHE_MAX +#define MM_HALOW_SCAN_CACHE_MAX (32) +#endif + +/*******************************************************************************/ +// Hooks the port must provide (no usable defaults). + +// Mask/restore interrupts around the driver's short critical sections. +// MM_HALOW_BEGIN_ATOMIC_SECTION() returns an opaque state that is passed back +// to MM_HALOW_END_ATOMIC_SECTION(). These stay macros so the port can +// save/restore its interrupt state inline. +#ifndef MM_HALOW_BEGIN_ATOMIC_SECTION +#error "port must define MM_HALOW_BEGIN_ATOMIC_SECTION/MM_HALOW_END_ATOMIC_SECTION" +#endif + +// A free-running millisecond tick counter. +uint32_t mm_halow_ticks_ms(void); + +// True when executing in interrupt context (on Cortex-M: IPSR != 0). +bool mm_halow_in_irq(void); + +// GPIO accessors, applied to the MM_HALOW_CS/RESET/WAKE/BUSY/IRQ pin values the +// port defines. The port also defines mm_halow_pin_t, its native pin handle +// (the type of those MM_HALOW_* values). +bool mm_halow_hal_pin_read(mm_halow_pin_t pin); +void mm_halow_hal_pin_write(mm_halow_pin_t pin, bool value); +void mm_halow_hal_pin_input(mm_halow_pin_t pin); +void mm_halow_hal_pin_output(mm_halow_pin_t pin); + +/*******************************************************************************/ +// Hooks with defaults. + +// Runs while the driver busy-waits, so the host can service its own pending +// events. It must not raise/longjmp out of the driver. +#ifndef MM_HALOW_EVENT_POLL_HOOK +#define MM_HALOW_EVENT_POLL_HOOK +#endif + +// Diagnostic output. +#ifndef MM_HALOW_PRINTF +#include +#define MM_HALOW_PRINTF(...) printf(__VA_ARGS__) +#endif +#ifndef MM_HALOW_VPRINTF +#include +#include +#define MM_HALOW_VPRINTF(fmt, args) vprintf(fmt, args) +#endif + +#ifndef MM_HALOW_WEAK +#define MM_HALOW_WEAK __attribute__((weak)) +#endif + +// Error codes returned by the driver API (negated). Default to the C library +// values; an embedder with its own errno space overrides these. +#ifndef MM_HALOW_EPERM +#include +#define MM_HALOW_EPERM EPERM +#define MM_HALOW_EIO EIO +#define MM_HALOW_EINVAL EINVAL +#define MM_HALOW_EAGAIN EAGAIN +#define MM_HALOW_ENOMEM ENOMEM +#define MM_HALOW_ENODEV ENODEV +#define MM_HALOW_ENOENT ENOENT +#define MM_HALOW_ENOTCONN ENOTCONN +#define MM_HALOW_ENXIO ENXIO +#define MM_HALOW_ETIMEDOUT ETIMEDOUT +#define MM_HALOW_ERANGE ERANGE +#define MM_HALOW_EOPNOTSUPP EOPNOTSUPP +#endif + +// Hostname reported to the DHCP server (a char pointer or array). +#ifndef MM_HALOW_HOST_NAME +#define MM_HALOW_HOST_NAME "mm-halow" +#endif + +/*******************************************************************************/ +// Functions the port must implement (see README.md). + +// SPI bus: mode 0, MSB first, MM_HALOW_SPI_BAUDRATE. The chip select is a +// plain GPIO (MM_HALOW_CS) driven by the driver, not by the SPI peripheral. +void mm_halow_port_spi_init(void); +void mm_halow_port_spi_deinit(void); +void mm_halow_port_spi_transfer(size_t len, const uint8_t *src, uint8_t *dest); + +// The backing memory for the driver's private heap (MM_HALOW_HEAP_SIZE bytes). +// The port owns keeping the allocation alive (e.g. registering it as a GC root +// on a garbage-collected host). +uint8_t *mm_halow_port_heap_alloc(size_t size); +void mm_halow_port_heap_free(uint8_t *ptr); + +// A hardware random 32-bit value. +uint32_t mm_halow_port_random_u32(void); + +// Called when the driver hits an unrecoverable internal failure (a morselib +// assertion). The port routes this to its own assert/fatal-error handling; it +// must not return. +void mm_halow_port_assert_fail(void); + +// Fallback station MAC address, used only when the transceiver's OTP holds +// none. Must be stable across boots. +void mm_halow_port_get_mac(uint8_t mac_addr[6]); + +#if MM_HALOW_ENABLE_PIN_IRQ +// Optional falling-edge interrupt on the IRQ line. The port's ISR must call +// mm_halow_port_irq_handler(); the driver enables/disables delivery with +// mm_halow_port_irq_enable() to coalesce bursts. +void mm_halow_port_irq_config(bool enabled); +void mm_halow_port_irq_enable(bool enabled); +void mm_halow_port_irq_handler(void); +#endif + +#endif // MM_HALOW_INCLUDED_CONFIG_H diff --git a/src/mm_halow_ctrl.c b/src/mm_halow_ctrl.c new file mode 100644 index 0000000..31f4723 --- /dev/null +++ b/src/mm_halow_ctrl.c @@ -0,0 +1,1148 @@ +/* + * This file is part of mm-halow-driver. + * + * Copyright (c) 2026 OpenMV LLC. + * + * SPDX-License-Identifier: MIT + * + * Control layer for the Morse Micro 802.11ah driver. + */ +#include "mm_halow_config.h" + +#if MM_HALOW_ENABLED + +#include + + +#include "lwip/prot/ethernet.h" + +#include "mmwlan.h" +#include "mmregdb.h" + +#include "mm_halow.h" +#include "mm_halow_osal.h" +#include "mm_halow_sched.h" + +#ifndef MM_HALOW_DEBUG +#define MM_HALOW_DEBUG (0) +#endif + +#if MM_HALOW_DEBUG +#define debug_printf(...) MM_HALOW_PRINTF(__VA_ARGS__) +#else +#define debug_printf(...) +#endif + +// Privacy bit in the Capability Information field of a probe response. +#define MM_HALOW_CAP_PRIVACY (1 << 4) + +// Default time the transceiver stays awake after activity, in ms. +#define MM_HALOW_PS_TIMEOUT_DEFAULT_MS (100) + +mm_halow_t mm_halow_state = { + #if MM_HALOW_ENABLE_AP + .ap_auth = MM_HALOW_SEC_SAE, + #endif + .ps_timeout_ms = MM_HALOW_PS_TIMEOUT_DEFAULT_MS, + // morselib's own defaults, mirrored so that reading them back before the + // driver starts reports what it will actually use. + .ampdu = true, + .sgi = true, + .subbands = true, +}; +void (*mm_halow_poll)(void); + +static int mm_halow_scan_start_locked(mm_halow_t *self); +static int mm_halow_apply_pm(mm_halow_t *self); +static void mm_halow_apply_radio(mm_halow_t *self); +static bool mm_halow_scan_idle_cond(void *arg); + +// Buffer used to linearise outgoing pbufs that span more than one segment. +// morselib copies the frame into its own packet memory, so this is only ever +// live for the duration of a single call. +#define MM_HALOW_TX_BUF_SIZE (MM_HALOW_MTU + SIZEOF_ETH_HDR) + +// How long to wait for the transmit path before giving up on a frame, in ms. +// The wait is not idle -- the driver is serviced throughout -- so this is a +// budget for making progress rather than a stall. +#define MM_HALOW_TX_READY_MS (250) +static uint8_t *mm_halow_tx_buf; + +MM_HALOW_WEAK void mm_halow_schedule_poll(void) { + // Ports that can raise a software interrupt override this; otherwise the + // periodic network poll is the only thing that drives the driver. +} + +// Translate an mmwlan status into a negative errno, the convention the rest of +// the network module uses. +static int mm_halow_status_to_errno(enum mmwlan_status status) { + switch (status) { + case MMWLAN_SUCCESS: + return 0; + case MMWLAN_INVALID_ARGUMENT: + return -MM_HALOW_EINVAL; + case MMWLAN_UNAVAILABLE: + return -MM_HALOW_EAGAIN; + case MMWLAN_NO_MEM: + return -MM_HALOW_ENOMEM; + case MMWLAN_TIMED_OUT: + return -MM_HALOW_ETIMEDOUT; + case MMWLAN_CHANNEL_LIST_NOT_SET: + return -MM_HALOW_ENODEV; + case MMWLAN_CHANNEL_INVALID: + return -MM_HALOW_ERANGE; + case MMWLAN_NOT_FOUND: + return -MM_HALOW_ENOENT; + case MMWLAN_NOT_SUPPORTED: + return -MM_HALOW_EOPNOTSUPP; + case MMWLAN_VIF_ERROR: + return -MM_HALOW_ENXIO; + default: + return -MM_HALOW_EIO; + } +} + +/*******************************************************************************/ +// morselib callbacks + +static void mm_halow_sta_status_cb(enum mmwlan_sta_state sta_state) { + mm_halow_t *self = &mm_halow_state; + switch (sta_state) { + case MMWLAN_STA_CONNECTING: + self->link_status = MM_HALOW_LINK_JOIN; + break; + case MMWLAN_STA_CONNECTED: + self->link_status = MM_HALOW_LINK_NOIP; + break; + default: + self->link_status = MM_HALOW_LINK_DOWN; + break; + } + debug_printf("halow: sta state %d\n", sta_state); +} + +static void mm_halow_fatal_error_cb(struct mmwlan_fatal_error_args *args) { + mm_halow_t *self = args->arg; + + // morselib is left in the state it would be in after mmwlan_shutdown(), and + // its API must not be called from here. Report the link as failed so that + // the interface stops looking healthy, and leave recovery to the caller, + // which can cycle active() to rebuild everything. + self->booted = false; + self->scan_active = false; + self->link_status = MM_HALOW_LINK_FAIL; + mm_halow_cb_tcpip_set_link_down(self, MM_HALOW_ITF_STA); + debug_printf("halow: fatal error at file %u line %u\n", + (unsigned int)args->fileid, (unsigned int)args->line); +} + +static void mm_halow_link_state_cb(enum mmwlan_link_state link_state, void *arg) { + mm_halow_t *self = arg; + if (link_state == MMWLAN_LINK_UP) { + mm_halow_cb_tcpip_set_link_up(self, MM_HALOW_ITF_STA); + } else { + mm_halow_cb_tcpip_set_link_down(self, MM_HALOW_ITF_STA); + } +} + +static void mm_halow_rx_cb(uint8_t *header, unsigned header_len, + uint8_t *payload, unsigned payload_len, void *arg) { + mm_halow_t *self = arg; + mm_halow_cb_process_ethernet(self, MM_HALOW_ITF_STA, header, header_len, payload, payload_len); +} + +/*******************************************************************************/ +// Init + +int mm_halow_init(mm_halow_t *self) { + if (self->initted) { + return 0; + } + + // Only the state of a previous session is cleared here. This runs on the + // first activation rather than at boot, so anything configured beforehand + // has to survive it. + self->itf_state = 0; + self->scan_active = false; + self->scan_cache = NULL; + self->scan_cache_len = 0; + self->scan_cache_max = 0; + #if MM_HALOW_ENABLE_AP + self->ap_sta_count = 0; + #endif + self->link_status = MM_HALOW_LINK_DOWN; + + // Take the driver's memory pool before morselib starts allocating: from here + // on it runs from PendSV, where the MicroPython heap is off limits. + if (!mm_halow_osal_init()) { + return -MM_HALOW_ENOMEM; + } + mm_halow_tx_buf = mm_halow_osal_malloc(MM_HALOW_TX_BUF_SIZE); + if (mm_halow_tx_buf == NULL) { + mm_halow_osal_deinit(); + return -MM_HALOW_ENOMEM; + } + + mmwlan_init(); + + // morselib enables power save by default. A dozing station cannot receive + // unsolicited traffic and is slow to associate, so apply the configured + // mode, which defaults to always-on. + mm_halow_apply_pm(self); + mm_halow_apply_radio(self); + + mmwlan_register_fatal_error_handler(mm_halow_fatal_error_cb, self); + mmwlan_register_link_state_cb(mm_halow_link_state_cb, self); + mmwlan_register_rx_cb(mm_halow_rx_cb, self); + + mmwlan_get_vif_mac_addr(MMWLAN_VIF_STA, self->mac); + + self->initted = true; + mm_halow_poll = mm_halow_poll_func; + return 0; +} + +void mm_halow_deinit(mm_halow_t *self) { + if (!self->initted) { + return; + } + if (mm_halow_sched_in_callback) { + // Reached from a scheduled callback run during a morselib wait: freeing the + // pool here would pull it out from under the frames still standing on it. + return; + } + + mm_halow_poll = NULL; + + for (int itf = 0; itf < MM_HALOW_ITF_MAX; itf++) { + if (self->itf_state & (1 << itf)) { + mm_halow_wifi_set_up(self, itf, false, self->country); + } + } + + mmwlan_register_rx_cb(NULL, NULL); + mm_halow_sched_teardown = true; + mmwlan_shutdown(); + mmwlan_deinit(); + mm_halow_sched_teardown = false; + self->booted = false; + + mm_halow_sched_deinit(); + mm_halow_osal_deinit(); + mm_halow_tx_buf = NULL; + // These point into the pool that has just gone back to the GC heap. + self->scan_cache = NULL; + self->scan_cache_len = 0; + self->scan_cache_max = 0; + self->scan_active = false; + + self->initted = false; + self->link_status = MM_HALOW_LINK_DOWN; +} + +// Per-channel dwell. morselib defaults to 30ms, but a beacon interval is +// typically 100TU (~102ms), so a channel visit that short catches a beacon less +// than a third of the time and a sweep misses networks that are plainly there. +// Dwelling for longer than one beacon interval is what makes a single sweep +// reliable, at the cost of a longer scan. +#define MM_HALOW_SCAN_DWELL_MS (110) + +// Upper bound on one sweep: 48 channels of the United States plan at the dwell +// above, with room for a larger channel plan. +#define MM_HALOW_SCAN_MS (30000) + +void mm_halow_poll_func(void) { + // A dispatch raised just before deinit still runs after it, by which point + // there is nothing left to service. + if (!mm_halow_state.initted) { + return; + } + if (mm_halow_sched_task_current() != NULL) { + // Reached from inside a driver task, by way of lwIP calling back into + // the transmit path. The scheduler is already running one level up; + // claiming here would leave the claim parked on this task's stack the + // moment it yields, and nothing would ever release it. + return; + } + // Whoever is already servicing the transceiver finishes the job; cutting in + // would put a second bus transaction on top of one in flight. + if (!mm_halow_sched_claim()) { + return; + } + mm_halow_hal_poll_irqs(); + mm_halow_osal_timer_poll(); + mm_halow_sched_run(); + mm_halow_sched_release(); + mm_halow_hal_irq_rearm(); +} + +/*******************************************************************************/ +// Interface control + +bool mm_halow_country_supported(const char *country) { + return mmwlan_lookup_regulatory_domain(get_regulatory_db(), country) != NULL; +} + +static int mm_halow_set_country(mm_halow_t *self, const char *country) { + const struct mmwlan_s1g_channel_list *channels = + mmwlan_lookup_regulatory_domain(get_regulatory_db(), country); + if (channels == NULL) { + return -MM_HALOW_EINVAL; + } + self->country[0] = country[0]; + self->country[1] = country[1]; + self->channels = channels; + return mm_halow_status_to_errno(mmwlan_set_channel_list(channels)); +} + +#if MM_HALOW_ENABLE_AP +static void mm_halow_ap_sta_status_cb(const struct mmwlan_ap_sta_status *status, void *arg) { + mm_halow_t *self = (mm_halow_t *)arg; + + for (unsigned i = 0; i < self->ap_sta_count; i++) { + if (memcmp(self->ap_stas[i], status->mac_addr, MMWLAN_MAC_ADDR_LEN) == 0) { + if (status->state != MMWLAN_AP_STA_AUTHORIZED) { + self->ap_sta_count--; + memmove(self->ap_stas[i], self->ap_stas[i + 1], + (self->ap_sta_count - i) * MMWLAN_MAC_ADDR_LEN); + } + return; + } + } + if (status->state == MMWLAN_AP_STA_AUTHORIZED && self->ap_sta_count < MM_HALOW_AP_MAX_STAS) { + memcpy(self->ap_stas[self->ap_sta_count++], status->mac_addr, MMWLAN_MAC_ADDR_LEN); + } +} + +// Find the channel the AP should beacon on: the requested one, or else the +// widest the regulatory domain allows. +static const struct mmwlan_s1g_channel *mm_halow_ap_channel(mm_halow_t *self) { + const struct mmwlan_s1g_channel_list *list = self->channels; + const struct mmwlan_s1g_channel *best = NULL; + + if (list == NULL) { + return NULL; + } + for (unsigned i = 0; i < list->num_channels; i++) { + const struct mmwlan_s1g_channel *ch = &list->channels[i]; + if (self->ap_chan_num != 0) { + if (ch->s1g_chan_num == self->ap_chan_num) { + return ch; + } + } else if (best == NULL || ch->bw_mhz > best->bw_mhz) { + best = ch; + } + } + return best; +} + +static int mm_halow_ap_enable(mm_halow_t *self) { + if (self->pm != MM_HALOW_PM_NONE) { + return -MM_HALOW_EPERM; + } + struct mmwlan_ap_args args = MMWLAN_AP_ARGS_INIT; + + if (self->ap_ssid_len == 0) { + return -MM_HALOW_EINVAL; + } + + if (!(self->itf_state & (1 << MM_HALOW_ITF_STA))) { + // A zero operating class and channel number mean "use whatever the STA + // is on", so a standalone AP has to choose for itself. + const struct mmwlan_s1g_channel *ch = mm_halow_ap_channel(self); + if (ch == NULL) { + return -MM_HALOW_ENODEV; + } + args.op_class = ch->s1g_operating_class != MMWLAN_SKIP_OP_CLASS_CHECK + ? ch->s1g_operating_class : ch->global_operating_class; + args.s1g_chan_num = ch->s1g_chan_num; + } + + self->ap_sta_count = 0; + args.sta_status_cb = mm_halow_ap_sta_status_cb; + args.sta_status_cb_arg = self; + + memcpy(args.ssid, self->ap_ssid, self->ap_ssid_len); + args.ssid_len = self->ap_ssid_len; + args.security_type = self->ap_auth; + if (self->ap_auth == MM_HALOW_SEC_SAE) { + if (self->ap_key_len == 0) { + return -MM_HALOW_EINVAL; + } + memcpy(args.passphrase, self->ap_key, self->ap_key_len); + args.passphrase_len = self->ap_key_len; + } + if (self->ap_auth == MM_HALOW_SEC_OPEN) { + args.pmf_mode = MMWLAN_PMF_DISABLED; + } + + // mmwlan_ap_enable() otherwise blocks until the AP has started, which it + // cannot do: morselib's tasks are run cooperatively from the driver poll, + // so nothing progresses while the calling thread is blocked. + args.async_start = true; + + return mm_halow_status_to_errno(mmwlan_ap_enable(&args)); +} +#endif // MM_HALOW_ENABLE_AP + +int mm_halow_wifi_set_up(mm_halow_t *self, int itf, bool up, const char *country) { + if (itf < 0 || itf >= MM_HALOW_ITF_MAX) { + return -MM_HALOW_EINVAL; + } + + // Bringing an interface up when it is already up is a no-op: the channel + // list cannot be changed once the transceiver is running. + if (up && (self->itf_state & (1 << itf))) { + return 0; + } + + if (!up && !self->initted) { + // Nothing was ever brought up, so there is nothing to take down -- and + // morselib has not been initialised to be told about it. + return 0; + } + + if (up) { + if (!self->initted) { + int ret = mm_halow_init(self); + if (ret != 0) { + return ret; + } + } + // The channel list has to be set before the transceiver is booted, and + // it is shared by both interfaces. + int ret = mm_halow_set_country(self, country); + if (ret != 0) { + return ret; + } + + // Boot the transceiver so that scanning and the MAC address are + // available before any connection is attempted, and because AP mode + // cannot be started until it is running. + if (!self->booted) { + struct mmwlan_boot_args boot_args = MMWLAN_BOOT_ARGS_INIT; + ret = mm_halow_status_to_errno(mmwlan_boot(&boot_args)); + if (ret != 0) { + return ret; + } + self->booted = true; + mmwlan_get_vif_mac_addr(MMWLAN_VIF_STA, self->mac); + } + + #if MM_HALOW_ENABLE_AP + if (itf == MM_HALOW_ITF_AP) { + ret = mm_halow_ap_enable(self); + if (ret != 0) { + return ret; + } + } + #endif + + if (!(self->itf_state & (1 << itf))) { + mm_halow_cb_tcpip_init(self, itf); + self->itf_state |= 1 << itf; + } + #if MM_HALOW_ENABLE_AP + if (itf == MM_HALOW_ITF_AP) { + mm_halow_cb_tcpip_set_link_up(self, itf); + } + #endif + } else { + #if MM_HALOW_ENABLE_AP + if (itf == MM_HALOW_ITF_AP) { + mmwlan_ap_disable(); + } else + #endif + { + mmwlan_sta_disable(); + self->link_status = MM_HALOW_LINK_DOWN; + } + if (self->itf_state & (1 << itf)) { + mm_halow_cb_tcpip_set_link_down(self, itf); + mm_halow_cb_tcpip_deinit(self, itf); + self->itf_state &= ~(1 << itf); + } + if (self->itf_state == 0) { + mm_halow_sched_teardown = true; + mmwlan_shutdown(); + mm_halow_sched_teardown = false; + self->booted = false; + } + } + + return 0; +} + +/*******************************************************************************/ +// Scanning + + +// Insert or refresh one result in the cache, keyed by BSSID. Runs from PendSV. +static void mm_halow_scan_cache_add(mm_halow_t *self, const mm_halow_ev_scan_result_t *res) { + if (self->scan_cache == NULL) { + return; + } + for (uint8_t i = 0; i < self->scan_cache_len; i++) { + if (memcmp(self->scan_cache[i].bssid, res->bssid, sizeof(res->bssid)) == 0) { + self->scan_cache[i] = *res; + return; + } + } + if (self->scan_cache_len < self->scan_cache_max) { + self->scan_cache[self->scan_cache_len++] = *res; + } +} + +size_t mm_halow_wifi_scan_cached(mm_halow_t *self, mm_halow_ev_scan_result_t *out, size_t max) { + // Scanning hops the radio across every channel, so it must not run while a + // join is in flight or the link is up. + if (self->link_status != MM_HALOW_LINK_DOWN) { + return 0; + } + + if (self->scan_cache == NULL) { + self->scan_cache = mm_halow_osal_malloc( + MM_HALOW_SCAN_CACHE_MAX * sizeof(*self->scan_cache)); + if (self->scan_cache == NULL) { + return 0; + } + self->scan_cache_max = MM_HALOW_SCAN_CACHE_MAX; + } + + if (!self->scan_active) { + self->scan_cache_len = 0; + if (mm_halow_scan_start_locked(self) != 0) { + return 0; + } + } + mm_halow_sched_wait(mm_halow_scan_idle_cond, self, MM_HALOW_SCAN_MS); + + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + size_t n = self->scan_cache_len < max ? self->scan_cache_len : max; + for (size_t i = 0; i < n; i++) { + out[i] = self->scan_cache[i]; + } + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + return n; +} + +// Scan results carry a frequency, but a channel is only identified by the pair +// of frequency and width, so the regulatory list is what turns one into the +// other. Zero when the frequency is not one of the domain's channels, which an +// access point operating outside the local plan will be. +static uint8_t mm_halow_freq_to_chan(mm_halow_t *self, uint32_t freq_hz, uint8_t bw_mhz) { + const struct mmwlan_s1g_channel_list *list = self->channels; + + if (list == NULL) { + return 0; + } + for (unsigned i = 0; i < list->num_channels; i++) { + const struct mmwlan_s1g_channel *ch = &list->channels[i]; + if (ch->centre_freq_hz == freq_hz && ch->bw_mhz == bw_mhz) { + return ch->s1g_chan_num; + } + } + return 0; +} + +static void mm_halow_scan_rx_cb(const struct mmwlan_scan_result *result, void *arg) { + mm_halow_t *self = arg; + mm_halow_ev_scan_result_t res = { 0 }; + res.ssid_len = MIN(result->ssid_len, sizeof(res.ssid)); + if (result->ssid != NULL) { + memcpy(res.ssid, result->ssid, res.ssid_len); + } + if (result->bssid != NULL) { + memcpy(res.bssid, result->bssid, sizeof(res.bssid)); + } + res.rssi = result->rssi; + res.channel_freq_hz = result->channel_freq_hz; + res.bw_mhz = result->bw_mhz; + res.op_bw_mhz = result->op_bw_mhz; + res.chan_num = mm_halow_freq_to_chan(self, result->channel_freq_hz, result->bw_mhz); + // 802.11ah only defines open, OWE and SAE, and the privacy bit is the only + // one of those distinctions visible without parsing the RSN element. + res.security = (result->capability_info & MM_HALOW_CAP_PRIVACY) ? MM_HALOW_SEC_SAE : MM_HALOW_SEC_OPEN; + + mm_halow_scan_cache_add(self, &res); +} + +// An aborted sweep stops at the end of the channel it is on, so this only has to +// cover one channel dwell, not a whole sweep. +#define MM_HALOW_SCAN_ABORT_MS (5000) + +static bool mm_halow_scan_idle_cond(void *arg) { + return !((mm_halow_t *)arg)->scan_active; +} + +static void mm_halow_scan_complete_cb(enum mmwlan_scan_state scan_state, void *arg) { + mm_halow_t *self = arg; + self->scan_active = false; + debug_printf("halow: sweep took %ums, %u results\n", + (unsigned int)(mm_halow_ticks_ms() - self->scan_started_ms), + (unsigned int)self->scan_cache_len); +} + +// Callers must already have decided that starting a sweep is allowed. +static int mm_halow_scan_start_locked(mm_halow_t *self) { + struct mmwlan_scan_req req = MMWLAN_SCAN_REQ_INIT; + req.scan_rx_cb = mm_halow_scan_rx_cb; + req.scan_complete_cb = mm_halow_scan_complete_cb; + req.scan_cb_arg = self; + req.args.dwell_time_ms = MM_HALOW_SCAN_DWELL_MS; + + self->scan_active = true; + self->scan_started_ms = mm_halow_ticks_ms(); + + enum mmwlan_status status = mmwlan_scan_request(&req); + if (status != MMWLAN_SUCCESS) { + self->scan_active = false; + return mm_halow_status_to_errno(status); + } + return 0; +} + +/*******************************************************************************/ +// Association + +int mm_halow_wifi_join(mm_halow_t *self, size_t ssid_len, const uint8_t *ssid, + size_t key_len, const uint8_t *key, uint32_t auth_type, const uint8_t *bssid) { + if (ssid_len == 0 || ssid_len > MMWLAN_SSID_MAXLEN) { + return -MM_HALOW_EINVAL; + } + if (key_len > MMWLAN_PASSPHRASE_MAXLEN) { + return -MM_HALOW_EINVAL; + } + if (auth_type == MM_HALOW_SEC_SAE && key_len == 0) { + return -MM_HALOW_EINVAL; + } + + memcpy(self->sta_ssid, ssid, ssid_len); + self->sta_ssid_len = ssid_len; + + struct mmwlan_sta_args args = MMWLAN_STA_ARGS_INIT; + memcpy(args.ssid, ssid, ssid_len); + args.ssid_len = ssid_len; + args.security_type = auth_type; + if (key_len != 0) { + memcpy(args.passphrase, key, key_len); + args.passphrase_len = key_len; + } + if (auth_type == MM_HALOW_SEC_OPEN) { + args.pmf_mode = MMWLAN_PMF_DISABLED; + } + if (bssid != NULL) { + memcpy(args.bssid, bssid, MMWLAN_MAC_ADDR_LEN); + } + + self->link_status = MM_HALOW_LINK_JOIN; + + // The radio cannot hop channels while an association completes, so stop any + // sweep that is in flight rather than joining on a moving radio. Aborting + // takes effect at the end of the current channel, so still wait for the + // completion callback -- but that is one channel, not a whole sweep. + if (self->scan_active) { + mmwlan_scan_abort(); + mm_halow_sched_wait(mm_halow_scan_idle_cond, self, MM_HALOW_SCAN_ABORT_MS); + } + + enum mmwlan_status status = mmwlan_sta_enable(&args, mm_halow_sta_status_cb); + if (status != MMWLAN_SUCCESS) { + self->link_status = MM_HALOW_LINK_FAIL; + return mm_halow_status_to_errno(status); + } + return 0; +} + +int mm_halow_wifi_leave(mm_halow_t *self, int itf) { + if (itf == MM_HALOW_ITF_AP) { + return mm_halow_status_to_errno(mmwlan_ap_disable()); + } + self->link_status = MM_HALOW_LINK_DOWN; + return mm_halow_status_to_errno(mmwlan_sta_disable()); +} + +int mm_halow_wifi_link_status(mm_halow_t *self, int itf) { + if (itf == MM_HALOW_ITF_AP) { + return (self->itf_state & (1 << MM_HALOW_ITF_AP)) ? MM_HALOW_LINK_UP : MM_HALOW_LINK_DOWN; + } + return self->link_status; +} + +int mm_halow_tcpip_link_status(mm_halow_t *self, int itf) { + int status = mm_halow_wifi_link_status(self, itf); + if (status != MM_HALOW_LINK_NOIP && status != MM_HALOW_LINK_UP) { + return status; + } + // Associated: report UP only once lwIP has an address on the interface. + struct netif *netif = &self->netif[itf]; + if ((netif->flags & NETIF_FLAG_UP) && !ip_addr_isany(&netif->ip_addr)) { + return MM_HALOW_LINK_UP; + } + return MM_HALOW_LINK_NOIP; +} + +/*******************************************************************************/ +// Queries + +int mm_halow_wifi_get_mac(mm_halow_t *self, int itf, uint8_t mac[6]) { + enum mmwlan_vif vif = (itf == MM_HALOW_ITF_AP) ? MMWLAN_VIF_AP : MMWLAN_VIF_STA; + if (mmwlan_get_vif_mac_addr(vif, mac) != MMWLAN_SUCCESS) { + memcpy(mac, self->mac, MMWLAN_MAC_ADDR_LEN); + } + return 0; +} + +int mm_halow_wifi_get_bssid(mm_halow_t *self, uint8_t bssid[6]) { + (void)self; + return mm_halow_status_to_errno(mmwlan_get_bssid(bssid)); +} + +int mm_halow_wifi_get_rssi(mm_halow_t *self, int32_t *rssi) { + (void)self; + *rssi = mmwlan_get_rssi(); + return 0; +} + +int mm_halow_wifi_get_channel(mm_halow_t *self, int itf, uint16_t *chan_num, uint8_t *bw_mhz) { + (void)self; + struct mmwlan_vif_channel_info info; + enum mmwlan_vif vif = (itf == MM_HALOW_ITF_AP) ? MMWLAN_VIF_AP : MMWLAN_VIF_STA; + enum mmwlan_status status = mmwlan_get_vif_channel_info(vif, &info); + if (status != MMWLAN_SUCCESS) { + return mm_halow_status_to_errno(status); + } + *chan_num = info.s1g_chan_num; + *bw_mhz = info.pri_bw_mhz; + return 0; +} + +// Settings morselib only accepts while it is inactive, applied once it has been +// initialised and before anything associates. +static void mm_halow_apply_radio(mm_halow_t *self) { + mmwlan_set_ampdu_enabled(self->ampdu); + mmwlan_set_sgi_enabled(self->sgi); + mmwlan_set_subbands_enabled(self->subbands); + if (self->rts_threshold) { + mmwlan_set_rts_threshold(self->rts_threshold); + } + if (self->fragment_threshold) { + mmwlan_set_fragment_threshold(self->fragment_threshold); + } + if (self->listen_interval) { + mmwlan_set_listen_interval(self->listen_interval); + } + if (self->health_max_ms) { + mmwlan_set_health_check_interval(self->health_min_ms, self->health_max_ms); + } +} + +static int mm_halow_apply_pm(mm_halow_t *self) { + enum mmwlan_status status = mmwlan_set_power_save_mode( + self->pm == MM_HALOW_PM_NONE ? MMWLAN_PS_DISABLED : MMWLAN_PS_ENABLED); + if (status != MMWLAN_SUCCESS) { + return mm_halow_status_to_errno(status); + } + status = mmwlan_set_dynamic_ps_timeout(self->ps_timeout_ms); + if (status != MMWLAN_SUCCESS) { + return mm_halow_status_to_errno(status); + } + return 0; +} + +int mm_halow_wifi_pm(mm_halow_t *self, uint32_t pm) { + #if MM_HALOW_ENABLE_AP + if (pm != MM_HALOW_PM_NONE && (self->itf_state & (1 << MM_HALOW_ITF_AP))) { + // An access point has to be listening. + return -MM_HALOW_EPERM; + } + #endif + uint32_t prev = self->pm; + self->pm = pm; + // Only reaches morselib once it has been initialised; mm_halow_init() applies + // whatever was configured before that. + if (self->booted) { + int ret = mm_halow_apply_pm(self); + if (ret != 0) { + self->pm = prev; + return ret; + } + } + return 0; +} + +// Regulatory testing. The command format is defined by the vendor's test tool +// rather than here, so this passes bytes through and hands the response back. +int mm_halow_wifi_ate_command(mm_halow_t *self, uint8_t *cmd, size_t cmd_len, + uint8_t *rsp, size_t *rsp_len) { + if (!self->booted) { + return -MM_HALOW_ENODEV; + } + uint32_t len = *rsp_len; + enum mmwlan_status status = mmwlan_ate_execute_command(cmd, cmd_len, rsp, &len); + // A command the transceiver rejects still returns a response worth seeing. + if (status != MMWLAN_SUCCESS && status != MMWLAN_COMMAND_ERROR) { + return mm_halow_status_to_errno(status); + } + *rsp_len = len; + return 0; +} + +// Pin the transmit rate, so emissions can be measured at a known modulation +// rather than whatever rate control picks. +int mm_halow_wifi_fixed_rate(mm_halow_t *self, int mcs, int bw_mhz, int gi) { + if (!self->booted) { + return -MM_HALOW_ENODEV; + } + if (mcs < MMWLAN_MCS_NONE || mcs > MMWLAN_MCS_MAX || + gi < MMWLAN_GI_NONE || gi > MMWLAN_GI_MAX) { + return -MM_HALOW_EINVAL; + } + switch (bw_mhz) { + case MMWLAN_BW_NONE: + case MMWLAN_BW_1MHZ: + case MMWLAN_BW_2MHZ: + case MMWLAN_BW_4MHZ: + case MMWLAN_BW_8MHZ: + break; + default: + return -MM_HALOW_EINVAL; + } + return mm_halow_status_to_errno(mmwlan_ate_override_rate_control(mcs, bw_mhz, gi)); +} + +// Radio settings. Stored whatever the state, and pushed to morselib when it +// will accept them: some are refused once the interface is up, so those take +// effect the next time it is brought up. +int mm_halow_wifi_set_radio(mm_halow_t *self, int what, uint32_t value) { + bool live = self->booted && self->link_status == MM_HALOW_LINK_DOWN; + enum mmwlan_status status = MMWLAN_SUCCESS; + + switch (what) { + case MM_HALOW_RADIO_AMPDU: + self->ampdu = value; + if (live) { + status = mmwlan_set_ampdu_enabled(value); + } + break; + case MM_HALOW_RADIO_SGI: + self->sgi = value; + if (live) { + status = mmwlan_set_sgi_enabled(value); + } + break; + case MM_HALOW_RADIO_SUBBANDS: + self->subbands = value; + if (live) { + status = mmwlan_set_subbands_enabled(value); + } + break; + case MM_HALOW_RADIO_RTS: + self->rts_threshold = value; + if (self->booted) { + status = mmwlan_set_rts_threshold(value); + } + break; + case MM_HALOW_RADIO_FRAG: + self->fragment_threshold = value; + if (self->booted) { + status = mmwlan_set_fragment_threshold(value); + } + break; + case MM_HALOW_RADIO_LISTEN: + // Carried in the association request, so it cannot change under an + // association; it applies to the next one. + self->listen_interval = value; + if (live) { + status = mmwlan_set_listen_interval(value); + } + break; + case MM_HALOW_RADIO_WNM_PD: + self->wnm_powerdown = value; + break; + case MM_HALOW_RADIO_TXPOWER: + // morselib has no query for this, so it is kept here to be read + // back. Zero lifts the override and restores the regulatory limit. + self->txpower = value; + if (self->booted) { + status = mmwlan_override_max_tx_power(value); + } + break; + default: + return -MM_HALOW_EINVAL; + } + return mm_halow_status_to_errno(status); +} + +uint32_t mm_halow_wifi_get_radio(mm_halow_t *self, int what) { + switch (what) { + case MM_HALOW_RADIO_AMPDU: + return self->ampdu; + case MM_HALOW_RADIO_SGI: + return self->sgi; + case MM_HALOW_RADIO_SUBBANDS: + return self->subbands; + case MM_HALOW_RADIO_RTS: + return self->rts_threshold; + case MM_HALOW_RADIO_FRAG: + return self->fragment_threshold; + case MM_HALOW_RADIO_WNM_PD: + return self->wnm_powerdown; + case MM_HALOW_RADIO_TXPOWER: + return self->txpower; + default: + return self->listen_interval; + } +} + +// Regulatory duty cycle: how the permitted air time is spread, and how much of +// it is left. Which regions enforce one is part of the channel list. +int mm_halow_wifi_set_duty_cycle(mm_halow_t *self, int mode) { + self->duty_cycle_mode = mode; + if (!self->booted) { + return -MM_HALOW_ENODEV; + } + if (mode != MMWLAN_DUTY_CYCLE_MODE_SPREAD && mode != MMWLAN_DUTY_CYCLE_MODE_BURST) { + return -MM_HALOW_EINVAL; + } + return mm_halow_status_to_errno(mmwlan_set_duty_cycle_mode(mode)); +} + +int mm_halow_wifi_get_duty_cycle(mm_halow_t *self, struct mmwlan_duty_cycle_stats *stats) { + if (!self->booted) { + return -MM_HALOW_ENODEV; + } + return mm_halow_status_to_errno(mmwlan_get_duty_cycle_stats(stats)); +} + +// Target Wake Time: negotiate with the access point to be awake only for an +// agreed window every interval, rather than at every DTIM. +int mm_halow_wifi_twt(mm_halow_t *self, uint64_t interval_us, uint32_t duration_us, int setup) { + if (!self->booted) { + return -MM_HALOW_ENODEV; + } + if (self->link_status != MM_HALOW_LINK_DOWN) { + // The agreement is carried in the association request, so it has to be + // in place before the interface joins anything. + return -MM_HALOW_EPERM; + } + if (setup < MMWLAN_TWT_SETUP_REQUEST || setup > MMWLAN_TWT_SETUP_DEMAND) { + return -MM_HALOW_EINVAL; + } + if (interval_us == 0 || duration_us == 0 || duration_us > interval_us) { + // A wake window has to fit inside the interval it repeats in. + return -MM_HALOW_EINVAL; + } + + struct mmwlan_twt_config_args args = MMWLAN_TWT_CONFIG_ARGS_INIT; + args.twt_mode = MMWLAN_TWT_REQUESTER; + args.twt_wake_interval_us = interval_us; + args.twt_min_wake_duration_us = duration_us; + args.twt_setup_command = setup; + return mm_halow_status_to_errno(mmwlan_twt_add_configuration(&args)); +} + +// Rate control statistics: what the transmitter actually settled on, which is +// otherwise invisible when throughput varies. +// Reports no statistics rather than an error when the rate table is empty, +// which is the state before anything has been transmitted. +int mm_halow_wifi_get_rc_stats(mm_halow_t *self, struct mmwlan_rc_stats **stats) { + if (!self->booted) { + return -MM_HALOW_ENODEV; + } + *stats = mmwlan_get_rc_stats(); + return 0; +} + +void mm_halow_wifi_free_rc_stats(struct mmwlan_rc_stats *stats) { + mmwlan_free_rc_stats(stats); +} + +// How often the driver checks the transceiver is still healthy. Each check +// wakes it, so the interval is worth raising when power matters. +int mm_halow_wifi_set_health_check(mm_halow_t *self, uint32_t min_ms, uint32_t max_ms) { + if (max_ms != 0 && min_ms > max_ms) { + return -MM_HALOW_EINVAL; + } + self->health_min_ms = min_ms; + self->health_max_ms = max_ms; + if (self->booted) { + return mm_halow_status_to_errno(mmwlan_set_health_check_interval(min_ms, max_ms)); + } + return 0; +} + +// Versions of the parts that make up a link: the library, the transceiver +// firmware and the chip itself. Needed whenever a problem has to be reported. +int mm_halow_wifi_get_version(mm_halow_t *self, struct mmwlan_version *version) { + if (!self->booted) { + return -MM_HALOW_ENODEV; + } + return mm_halow_status_to_errno(mmwlan_get_version(version)); +} + +// Sleep across DTIM periods, so the transceiver only wakes on its own schedule +// and the access point buffers traffic for it meanwhile. +int mm_halow_wifi_wnm_sleep(mm_halow_t *self, bool enable, bool powerdown) { + if (enable && self->pm == MM_HALOW_PM_NONE) { + // morselib only sleeps if 802.11 power save is on, and would otherwise + // report success while staying awake. + return -MM_HALOW_EPERM; + } + if (enable && self->link_status != MM_HALOW_LINK_UP && self->link_status != MM_HALOW_LINK_NOIP) { + // Entering requires the AP to accept the request, so there has to be one. + return -MM_HALOW_ENOTCONN; + } + + struct mmwlan_set_wnm_sleep_enabled_args args = MMWLAN_SET_WNM_SLEEP_ENABLED_ARGS_INIT; + args.wnm_sleep_enabled = enable; + args.chip_powerdown_enabled = powerdown; + return mm_halow_status_to_errno(mmwlan_set_wnm_sleep_enabled_ext(&args)); +} + +// How long the transceiver stays awake after activity before dozing again. +// Only has an effect while power saving is enabled. +int mm_halow_wifi_set_ps_timeout(mm_halow_t *self, uint32_t ms) { + if (ms == 0) { + return -MM_HALOW_EINVAL; + } + if (self->booted) { + enum mmwlan_status status = mmwlan_set_dynamic_ps_timeout(ms); + if (status != MMWLAN_SUCCESS) { + return mm_halow_status_to_errno(status); + } + } + self->ps_timeout_ms = ms; + return 0; +} + +int mm_halow_wifi_get_ps_timeout(mm_halow_t *self, uint32_t *ms) { + *ms = self->ps_timeout_ms; + return 0; +} + +int mm_halow_wifi_get_pm(mm_halow_t *self, uint32_t *pm) { + *pm = self->pm; + return 0; +} + +/*******************************************************************************/ +// AP configuration + +#if MM_HALOW_ENABLE_AP + +void mm_halow_wifi_ap_set_ssid(mm_halow_t *self, size_t len, const uint8_t *buf) { + self->ap_ssid_len = MIN(len, sizeof(self->ap_ssid)); + memcpy(self->ap_ssid, buf, self->ap_ssid_len); +} + +void mm_halow_wifi_ap_set_password(mm_halow_t *self, size_t len, const uint8_t *buf) { + self->ap_key_len = MIN(len, sizeof(self->ap_key)); + memcpy(self->ap_key, buf, self->ap_key_len); +} + +void mm_halow_wifi_ap_set_auth(mm_halow_t *self, uint32_t auth) { + self->ap_auth = auth; +} + +void mm_halow_wifi_ap_get_ssid(mm_halow_t *self, size_t *len, const uint8_t **buf) { + *len = self->ap_ssid_len; + *buf = self->ap_ssid; +} + +uint32_t mm_halow_wifi_ap_get_auth(mm_halow_t *self) { + return self->ap_auth; +} + +int mm_halow_wifi_ap_get_stas(mm_halow_t *self, int *num_stas, uint8_t *macs) { + // The list is compacted in place by mm_halow_ap_sta_status_cb() as stations + // come and go, so it has to be copied out whole rather than read across a + // station leaving. + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + int n = MIN(*num_stas, (int)self->ap_sta_count); + memcpy(macs, self->ap_stas, n * MMWLAN_MAC_ADDR_LEN); + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + *num_stas = n; + return 0; +} + +void mm_halow_wifi_ap_set_channel(mm_halow_t *self, uint8_t chan_num) { + self->ap_chan_num = chan_num; +} + +#endif // MM_HALOW_ENABLE_AP + +/*******************************************************************************/ +// Datapath + +int mm_halow_send_ethernet(mm_halow_t *self, int itf, size_t len, const void *buf, bool is_pbuf) { + (void)itf; + + + if (len > MM_HALOW_TX_BUF_SIZE || mm_halow_tx_buf == NULL) { + return -MM_HALOW_EINVAL; + } + + // mmwlan_tx() would block, and this runs from lwIP's linkoutput with PendSV + // raised, so nothing would service the transmit path: drive it here instead. + // That also runs the task delivering received frames, which would re-enter + // lwIP mid-walk, so receive is held off for the wait. + if (mm_halow_sched_task_current() == NULL) { + uint32_t start = mm_halow_ticks_ms(); + self->rx_deferred = true; + while (mmwlan_tx_wait_until_ready(0) != MMWLAN_SUCCESS) { + if ((uint32_t)(mm_halow_ticks_ms() - start) >= MM_HALOW_TX_READY_MS) { + self->rx_deferred = false; + return -MM_HALOW_EAGAIN; + } + mm_halow_poll_func(); + } + self->rx_deferred = false; + } else if (mmwlan_tx_wait_until_ready(0) != MMWLAN_SUCCESS) { + // A driver task got here, by way of a received frame that lwIP answered + // straight back -- an ARP reply or an ACK. Waiting would be a pure + // spin: the scheduler is already running one level up, so nothing this + // call does can drain the queue. Report congestion and let lwIP retry. + return -MM_HALOW_EAGAIN; + } + + // Linearised only once there is somewhere for it to go. The buffer is + // shared, and a frame that lwIP generates while this one is still waiting + // would otherwise copy over it and be transmitted in its place. + const uint8_t *data; + if (is_pbuf) { + struct pbuf *p = (struct pbuf *)buf; + if (p->next == NULL) { + // Single segment, send it straight from the pbuf. + data = p->payload; + } else { + pbuf_copy_partial(p, mm_halow_tx_buf, len, 0); + data = mm_halow_tx_buf; + } + } else { + data = buf; + } + + struct mmpkt *pkt = mmwlan_alloc_mmpkt_for_tx(len, MMWLAN_TX_DEFAULT_QOS_TID); + if (pkt == NULL) { + return -MM_HALOW_EAGAIN; + } + struct mmpktview *pktview = mmpkt_open(pkt); + mmpkt_append_data(pktview, data, len); + mmpkt_close(&pktview); + + struct mmwlan_tx_metadata metadata = MMWLAN_TX_METADATA_INIT; + metadata.tid = MMWLAN_TX_DEFAULT_QOS_TID; + enum mmwlan_status status = mmwlan_tx_pkt(pkt, &metadata); + if (status != MMWLAN_SUCCESS) { + debug_printf("halow: tx failed %d\n", status); + return mm_halow_status_to_errno(status); + } + + // Only queued so far. Ask for a poll rather than running the tasks here: + // this is inside lwIP, and a task delivering a frame would re-enter it. + mm_halow_schedule_poll(); + return 0; +} + +#endif // MM_HALOW_ENABLED diff --git a/src/mm_halow_hal.c b/src/mm_halow_hal.c new file mode 100644 index 0000000..36b7eb5 --- /dev/null +++ b/src/mm_halow_hal.c @@ -0,0 +1,328 @@ +/* + * This file is part of mm-halow-driver. + * + * Copyright (c) 2026 OpenMV LLC. + * + * SPDX-License-Identifier: MIT + * + * MMHAL implementation for MicroPython: the SD-over-SPI transport to the Morse + * Micro transceiver, plus the firmware and board-configuration blobs. + */ +#include "mm_halow_config.h" + +#if MM_HALOW_ENABLED + +#include + +#include "mmhal.h" +#include "mmhal_wlan.h" +#include "mmosal.h" +#include "mm_halow.h" +#include "mm_halow_osal.h" +#include "mm_halow_sched.h" + +// Bytes clocked out with MOSI held high to stabilise the transceiver's SD-over-SPI +// state machine. Must be at least 74 bits, see section 6.4.1.1 of "SD Physical +// Layer Simplified Specification Version 9.10". +#define MM_HALOW_TRAINING_BYTES (16) + +// Size of the scratch buffer used to drive MOSI high during read transfers. +// Reads are chunked through it so that the driver never allocates per transfer. +#define MM_HALOW_READ_CHUNK (128) + +static mmhal_irq_handler_t mm_halow_spi_irq_handler; +static mmhal_irq_handler_t mm_halow_busy_irq_handler; +static volatile bool mm_halow_spi_irq_enabled; +static volatile bool mm_halow_busy_irq_enabled; + +// Filled with 0xff at init and never written again: SD-over-SPI needs MOSI held +// high while reading, and clocking out of a fixed buffer keeps reads allocation +// free. +static uint8_t mm_halow_spi_ones[MM_HALOW_READ_CHUNK]; + +// The firmware image and, if the board supplies one, the board configuration +// file. Both are linked in as binary blobs, see extmod.mk. +extern uint8_t mm_halow_firmware_start; +extern uint8_t mm_halow_firmware_end; +#ifdef MM_HALOW_BCF +extern uint8_t mm_halow_bcf_start; +extern uint8_t mm_halow_bcf_end; +#endif + +/*******************************************************************************/ +// Bus + +static void mm_halow_spi_transfer(size_t len, const uint8_t *src, uint8_t *dest) { + mm_halow_port_spi_transfer(len, src, dest); +} + +void mmhal_wlan_spi_cs_assert(void) { + // The gap before a transaction starts is the one point in the bus path + // where nothing is in flight, so it is where a task that has overrun its + // turn is made to give one up. A retry loop re-asserts CS every time round, + // so this is always reached however long morselib intends to keep trying. + if (mm_halow_sched_over_budget() && !mm_halow_osal_in_critical()) { + mm_halow_sched_yield(); + } + mm_halow_hal_pin_write(MM_HALOW_CS, 0); +} + +void mmhal_wlan_spi_cs_deassert(void) { + mm_halow_hal_pin_write(MM_HALOW_CS, 1); +} + +uint8_t mmhal_wlan_spi_rw(uint8_t data) { + uint8_t rx = 0xff; + mm_halow_spi_transfer(1, &data, &rx); + return rx; +} + +void mmhal_wlan_spi_read_buf(uint8_t *buf, unsigned len) { + // SD-over-SPI requires MOSI to be held high while reading, so clock out ones + // from a fixed scratch buffer rather than doing a receive-only transfer. + while (len > 0) { + size_t chunk = MIN(len, MM_HALOW_READ_CHUNK); + mm_halow_spi_transfer(chunk, mm_halow_spi_ones, buf); + buf += chunk; + len -= chunk; + } +} + +void mmhal_wlan_spi_write_buf(const uint8_t *buf, unsigned len) { + mm_halow_spi_transfer(len, buf, NULL); +} + +void mmhal_wlan_send_training_seq(void) { + mmhal_wlan_spi_cs_deassert(); + mm_halow_spi_transfer(MM_HALOW_TRAINING_BYTES, mm_halow_spi_ones, NULL); +} + +/*******************************************************************************/ +// Control lines + +void mmhal_wlan_assert_reset(bool assert_reset) { + mm_halow_hal_pin_write(MM_HALOW_RESET, assert_reset ? 0 : 1); +} + +void mmhal_wlan_hard_reset(void) { + mmhal_wlan_assert_reset(true); + mmosal_task_sleep(5); + mmhal_wlan_assert_reset(false); + mmosal_task_sleep(20); +} + +void mmhal_wlan_wake_assert(void) { + mm_halow_hal_pin_write(MM_HALOW_WAKE, 1); +} + +void mmhal_wlan_wake_deassert(void) { + mm_halow_hal_pin_write(MM_HALOW_WAKE, 0); +} + +bool mmhal_wlan_busy_is_asserted(void) { + // The transceiver drives BUSY high, but a board may invert it on the way to + // the MCU, for example to share the line with a wake-up input. + #if MM_HALOW_BUSY_INVERTED + return mm_halow_hal_pin_read(MM_HALOW_BUSY) == 0; + #else + return mm_halow_hal_pin_read(MM_HALOW_BUSY) != 0; + #endif +} + +void mmhal_wlan_register_busy_irq_handler(mmhal_irq_handler_t handler) { + mm_halow_busy_irq_handler = handler; +} + +void mmhal_wlan_set_busy_irq_enabled(bool enabled) { + mm_halow_busy_irq_enabled = enabled; +} + +bool mmhal_wlan_spi_irq_is_asserted(void) { + // The transceiver drives the line low while it has data pending. + return mm_halow_hal_pin_read(MM_HALOW_IRQ) == 0; +} + +void mmhal_wlan_clear_spi_irq(void) { + // The line is level driven by the transceiver, so there is nothing to clear. +} + +void mmhal_wlan_register_spi_irq_handler(mmhal_irq_handler_t handler) { + mm_halow_spi_irq_handler = handler; +} + +void mmhal_wlan_set_spi_irq_enabled(bool enabled) { + mm_halow_spi_irq_enabled = enabled; + // The line is level, not edge, driven: if it is already asserted when the + // interrupt is enabled there will be no further edge to trigger on. + if (enabled && mmhal_wlan_spi_irq_is_asserted() && mm_halow_spi_irq_handler != NULL) { + mm_halow_spi_irq_handler(); + } +} + +#if MM_HALOW_ENABLE_PIN_IRQ +// The transceiver holds IRQ low until it is serviced, so the falling edge is +// the assert. Waking the driver from the edge rather than waiting for the +// next network poll shaves a poll period off a round trip, which crosses that +// wait twice. Optional: a port that cannot register an edge interrupt on the +// IRQ pin builds with MM_HALOW_ENABLE_PIN_IRQ disabled and just waits for the next +// poll -- mm_halow_hal_poll_irqs() reads the line by level anyway. +// +// The port's interrupt implementation must call this handler on the falling +// edge of the IRQ line. +void mm_halow_port_irq_handler(void) { + // Coalesce: one poll drains everything the transceiver has, so further + // edges until then are pure overhead. Re-armed by mm_halow_hal_irq_rearm(). + mm_halow_port_irq_enable(false); + mm_halow_schedule_poll(); +} + +void mm_halow_hal_irq_rearm(void) { + mm_halow_port_irq_enable(true); +} +#else +void mm_halow_hal_irq_rearm(void) { +} +#endif + +// Called from mm_halow_poll() to pick up transceiver interrupts. Level-checking +// here rather than relying purely on a pin interrupt keeps the driver correct on +// boards where the IRQ line is not wired to an interrupt-capable pin. +void mm_halow_hal_poll_irqs(void) { + if (mm_halow_spi_irq_enabled && mm_halow_spi_irq_handler != NULL && mmhal_wlan_spi_irq_is_asserted()) { + mm_halow_spi_irq_handler(); + } + if (mm_halow_busy_irq_enabled && mm_halow_busy_irq_handler != NULL && mmhal_wlan_busy_is_asserted()) { + mm_halow_busy_irq_handler(); + } +} + +/*******************************************************************************/ +// Init + +void mmhal_wlan_init(void) { + memset(mm_halow_spi_ones, 0xff, sizeof(mm_halow_spi_ones)); + + mm_halow_hal_pin_output(MM_HALOW_RESET); + mm_halow_hal_pin_write(MM_HALOW_RESET, 0); + mm_halow_hal_pin_output(MM_HALOW_WAKE); + mm_halow_hal_pin_write(MM_HALOW_WAKE, 0); + mm_halow_hal_pin_output(MM_HALOW_CS); + mm_halow_hal_pin_write(MM_HALOW_CS, 1); + mm_halow_hal_pin_input(MM_HALOW_BUSY); + mm_halow_hal_pin_input(MM_HALOW_IRQ); + #if MM_HALOW_ENABLE_PIN_IRQ + mm_halow_port_irq_config(true); + #endif + + // Mode 0, MSB first: the transceiver's SD-over-SPI interface samples on the + // rising edge with the clock idling low. + mm_halow_port_spi_init(); + + // Initialising the SPI peripheral may have reclaimed the CS pin. + mm_halow_hal_pin_output(MM_HALOW_CS); + mm_halow_hal_pin_write(MM_HALOW_CS, 1); + + mmhal_wlan_assert_reset(false); +} + +void mmhal_wlan_deinit(void) { + #if MM_HALOW_ENABLE_PIN_IRQ + mm_halow_port_irq_config(false); + #endif + mm_halow_spi_irq_enabled = false; + mm_halow_busy_irq_enabled = false; + mm_halow_spi_irq_handler = NULL; + mm_halow_busy_irq_handler = NULL; + + mmhal_wlan_assert_reset(true); + mm_halow_hal_pin_write(MM_HALOW_WAKE, 0); + mm_halow_port_spi_deinit(); +} + +#if defined(MM_HALOW_EXT_XTAL_INIT) && MM_HALOW_EXT_XTAL_INIT +bool mmhal_wlan_ext_xtal_init_is_required(void) { + return true; +} +#endif + +const struct mmhal_chip *mmhal_get_chip(void) { + return &MM_HALOW_CHIPSET; +} + +/*******************************************************************************/ +// Firmware and board configuration blobs + +static void mm_halow_read_blob(const uint8_t *start, const uint8_t *end, + uint32_t offset, uint32_t requested_len, struct mmhal_robuf *robuf) { + robuf->buf = NULL; + robuf->len = 0; + robuf->free_arg = NULL; + robuf->free_cb = NULL; + + size_t len = end - start; + if (offset > len) { + return; + } + robuf->buf = (uint8_t *)start + offset; + robuf->len = MIN(len - offset, requested_len); +} + +void mmhal_wlan_read_fw_file(uint32_t offset, uint32_t requested_len, struct mmhal_robuf *robuf) { + mm_halow_read_blob(&mm_halow_firmware_start, &mm_halow_firmware_end, offset, requested_len, robuf); +} + +void mmhal_wlan_read_bcf_file(uint32_t offset, uint32_t requested_len, struct mmhal_robuf *robuf) { + #ifdef MM_HALOW_BCF + mm_halow_read_blob(&mm_halow_bcf_start, &mm_halow_bcf_end, offset, requested_len, robuf); + #else + // Without a board configuration file the transceiver falls back to the + // calibration data in its own OTP. + (void)offset; + (void)requested_len; + robuf->buf = NULL; + robuf->len = 0; + robuf->free_arg = NULL; + robuf->free_cb = NULL; + #endif +} + +/*******************************************************************************/ +// Miscellaneous + +void mmhal_read_mac_addr(uint8_t *mac_addr) { + // Leave whatever the transceiver reported from its OTP in place; if that is + // all zeroes, derive a stable locally administered address from the MCU's + // unique ID so that the same board always joins with the same address. + for (int i = 0; i < 6; i++) { + if (mac_addr[i] != 0) { + return; + } + } + + mm_halow_port_get_mac(mac_addr); +} + +uint32_t mmhal_random_u32(uint32_t min, uint32_t max) { + uint32_t value = mm_halow_port_random_u32(); + if (max <= min) { + return min; + } + uint32_t span = max - min; + if (span == UINT32_MAX) { + // The whole range: the count of values is 2^32, which does not fit, and + // computing it wraps to zero. morselib asks for exactly this when it + // needs random bytes, and the modulo by zero left every one of them 0. + return value; + } + return min + value % (span + 1); +} + +void mmhal_set_deep_sleep_veto(uint8_t veto_id) { + (void)veto_id; +} + +void mmhal_clear_deep_sleep_veto(uint8_t veto_id) { + (void)veto_id; +} + +#endif // MM_HALOW_ENABLED diff --git a/src/mm_halow_lwip.c b/src/mm_halow_lwip.c new file mode 100644 index 0000000..dde3cfe --- /dev/null +++ b/src/mm_halow_lwip.c @@ -0,0 +1,251 @@ +/* + * This file is part of mm-halow-driver. + * + * Copyright (c) 2026 OpenMV LLC. + * + * SPDX-License-Identifier: MIT + * + * lwIP interface for the Morse Micro 802.11ah driver. + */ +#include "mm_halow_config.h" + +#if MM_HALOW_ENABLED + +#include + +#include "lwip/etharp.h" +#include "lwip/dns.h" +#include "lwip/ethip6.h" +#include "lwip/igmp.h" +#include "lwip/init.h" +#include "lwip/pbuf.h" +#include "netif/ethernet.h" + + +#include "mm_halow.h" + +static err_t mm_halow_netif_output(struct netif *netif, struct pbuf *p) { + mm_halow_t *self = netif->state; + int itf = netif->name[1] - '0'; + + if (self->trace_flags & MM_HALOW_TRACE_ETH_TX) { + MM_HALOW_PRINTF("halow: [txf] itf=%d len=%u\n", itf, (unsigned int)p->tot_len); + } + + int ret = mm_halow_send_ethernet(self, itf, p->tot_len, p, true); + if (ret == -MM_HALOW_EAGAIN) { + // The transmit queue is still full after the driver has been serviced + // for MM_HALOW_TX_READY_MS. Drop the frame, which is what an interface + // does when its queue is full: TCP retransmits, UDP is lossy by + // definition. Reporting ERR_MEM instead surfaces a transient queue + // full to the application as ENOMEM, which it cannot act on. + return ERR_OK; + } + if (ret != 0) { + return ERR_IF; + } + return ERR_OK; +} + +#if LWIP_IGMP +static err_t mm_halow_netif_update_igmp_mac_filter(struct netif *netif, const ip4_addr_t *group, + enum netif_mac_filter_action action) { + // The transceiver does not filter multicast in hardware; lwIP does it. + (void)netif; + (void)group; + (void)action; + return ERR_OK; +} +#endif + +static err_t mm_halow_netif_init(struct netif *netif) { + mm_halow_t *self = netif->state; + int itf = netif->name[1] - '0'; + + netif->linkoutput = mm_halow_netif_output; + netif->output = etharp_output; + #if LWIP_IPV6 + netif->output_ip6 = ethip6_output; + #endif + netif->mtu = MM_HALOW_MTU; + netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET; + + mm_halow_wifi_get_mac(self, itf, netif->hwaddr); + netif->hwaddr_len = sizeof(netif->hwaddr); + + #if LWIP_IGMP + netif->flags |= NETIF_FLAG_IGMP; + netif_set_igmp_mac_filter(netif, mm_halow_netif_update_igmp_mac_filter); + #endif + + return ERR_OK; +} + +void mm_halow_cb_tcpip_init(mm_halow_t *self, int itf) { + struct netif *netif = &self->netif[itf]; + + #if LWIP_IPV4 + ip_addr_t ipconfig[3]; + ip4_addr_set_zero(ip_2_ip4(&ipconfig[0])); + ip4_addr_set_zero(ip_2_ip4(&ipconfig[1])); + ip4_addr_set_zero(ip_2_ip4(&ipconfig[2])); + if (itf == MM_HALOW_ITF_AP) { + ip_2_ip4(&ipconfig[0])->addr = PP_HTONL(MM_HALOW_AP_ADDRESS); + ip_2_ip4(&ipconfig[1])->addr = PP_HTONL(MM_HALOW_AP_NETMASK); + ip_2_ip4(&ipconfig[2])->addr = PP_HTONL(MM_HALOW_AP_ADDRESS); + } else { + #if !LWIP_DHCP + // No client to ask, so come up on the configured address rather than on + // 0.0.0.0, where the interface would never be usable. + ip_2_ip4(&ipconfig[0])->addr = PP_HTONL(MM_HALOW_STA_ADDRESS); + ip_2_ip4(&ipconfig[1])->addr = PP_HTONL(MM_HALOW_STA_NETMASK); + ip_2_ip4(&ipconfig[2])->addr = PP_HTONL(MM_HALOW_STA_GATEWAY); + #endif + } + #endif + + netif->name[0] = 'w'; + netif->name[1] = '0' + itf; + + #if LWIP_IPV4 + netif_add(netif, ip_2_ip4(&ipconfig[0]), ip_2_ip4(&ipconfig[1]), ip_2_ip4(&ipconfig[2]), + self, mm_halow_netif_init, ethernet_input); + #elif LWIP_IPV6 + netif_add(netif, self, mm_halow_netif_init, ethernet_input); + #else + #error "halow needs either IPv4 or IPv6" + #endif + #if LWIP_NETIF_HOSTNAME + netif_set_hostname(netif, MM_HALOW_HOST_NAME); + #endif + if (netif_default == NULL) { + // Only claim the default route if nothing else holds it. A board can + // have an Ethernet or another wireless interface up and addressed, and + // taking the default from it would send that traffic here instead. + netif_set_default(netif); + } + netif_set_up(netif); + + if (itf == MM_HALOW_ITF_STA) { + #if LWIP_IPV4 && LWIP_DNS + // Only when there is one to set: the station's comes from DHCP, and + // writing the zero address here would clear whatever another interface + // or the user had already put in the slot. + if (!ip_addr_isany(&ipconfig[2])) { + dns_setserver(0, &ipconfig[2]); + } + #endif + #if LWIP_IPV4 && LWIP_DHCP + dhcp_set_struct(netif, &self->dhcp_client); + #endif + } else { + #if MM_HALOW_ENABLE_AP && LWIP_IPV4 + dhcp_server_init(&self->dhcp_server, &ipconfig[0], &ipconfig[1]); + #endif + } +} + +void mm_halow_cb_tcpip_deinit(mm_halow_t *self, int itf) { + struct netif *netif = &self->netif[itf]; + bool was_default = netif_default == netif; + + if (itf == MM_HALOW_ITF_STA) { + #if LWIP_IPV4 && LWIP_DHCP + dhcp_stop(netif); + #endif + } else { + #if MM_HALOW_ENABLE_AP + dhcp_server_deinit(&self->dhcp_server); + #endif + } + + struct netif *n; + NETIF_FOREACH(n) { + if (n == netif) { + netif_remove(netif); + #if LWIP_IPV4 + ip4_addr_set_zero(ip_2_ip4(&netif->ip_addr)); + #endif + netif->flags = 0; + break; + } + } + + if (was_default && netif_default == NULL) { + // netif_remove() drops the default when it removes the interface + // holding it, and leaves the system with none: every off-link route + // then fails even though another interface is up. Hand it to whatever + // is left. + NETIF_FOREACH(n) { + if (netif_is_up(n)) { + netif_set_default(n); + break; + } + } + } +} + +void mm_halow_cb_tcpip_set_link_up(mm_halow_t *self, int itf) { + struct netif *netif = &self->netif[itf]; + if (netif_is_link_up(netif)) { + return; + } + netif_set_link_up(netif); + #if LWIP_IPV4 && LWIP_DHCP + if (itf == MM_HALOW_ITF_STA) { + dhcp_start(netif); + } + #endif +} + +void mm_halow_cb_tcpip_set_link_down(mm_halow_t *self, int itf) { + struct netif *netif = &self->netif[itf]; + if (!netif_is_link_up(netif)) { + return; + } + // Only the link goes down here. Stopping DHCP would send a + // release, and this is reached from the fatal error handler, where the + // transceiver is in no state to transmit anything. The lease is dropped in + // mm_halow_cb_tcpip_deinit() instead. + netif_set_link_down(netif); +} + +void mm_halow_cb_process_ethernet(void *cb_data, int itf, + const uint8_t *header, size_t header_len, const uint8_t *payload, size_t payload_len) { + mm_halow_t *self = cb_data; + struct netif *netif = &self->netif[itf]; + size_t len = header_len + payload_len; + + if (self->rx_deferred) { + // The driver is being run from inside lwIP, by the wait in + // mm_halow_send_ethernet(). Handing it a frame here would re-enter it + // while it is walking its own lists. + return; + } + + if (self->trace_flags & MM_HALOW_TRACE_ETH_RX) { + MM_HALOW_PRINTF("halow: [rxf] itf=%d len=%u\n", itf, (unsigned int)len); + } + + if (!netif_is_link_up(netif)) { + return; + } + + struct pbuf *p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL); + if (p == NULL) { + return; + } + // The header morselib reports lives on its stack, so it has to be copied in + // separately from the payload rather than treated as one buffer. + if (pbuf_take(p, header, header_len) != ERR_OK || + pbuf_take_at(p, payload, payload_len, header_len) != ERR_OK) { + pbuf_free(p); + return; + } + + if (netif->input(p, netif) != ERR_OK) { + pbuf_free(p); + } +} + +#endif // MM_HALOW_ENABLED diff --git a/src/mm_halow_osal.c b/src/mm_halow_osal.c new file mode 100644 index 0000000..a72a101 --- /dev/null +++ b/src/mm_halow_osal.c @@ -0,0 +1,766 @@ +/* + * This file is part of mm-halow-driver. + * + * Copyright (c) 2026 OpenMV LLC. + * + * SPDX-License-Identifier: MIT + * + * MMOSAL implementation. + * + * This maps the RTOS abstraction layer that morselib is written against onto + * the cooperative scheduler in mm_halow_sched.c. Allocation is served from a + * dedicated static heap rather than the host's general heap, because morselib + * allocates from interrupt context where a garbage collector must not run. + */ +#include + +#include "mm_halow_config.h" + +#if MM_HALOW_ENABLED + +#include + +#include "mmosal.h" +#include "mm_halow.h" +#include "mm_halow_sched.h" +#include "mm_halow_osal.h" + +// Bytes of heap reserved for morselib. This covers packet memory as well, so +// it is sized to match the 95 KB heap the Morse reference ports use. +#ifndef MM_HALOW_HEAP_SIZE +#define MM_HALOW_HEAP_SIZE (96 * 1024) +#endif + +/*******************************************************************************/ +// Heap + +// A first-fit allocator over a static pool. Blocks are kept in address order in +// a single list so that adjacent free blocks can be coalesced on free. +typedef struct _mm_halow_block_t { + struct _mm_halow_block_t *next; // next block, in address order + size_t size; // usable bytes in this block + bool used; // true if handed out to a caller +} mm_halow_block_t; + +#define MM_HALOW_BLOCK_ALIGN (8) +#define MM_HALOW_BLOCK_ROUND(n) (((n) + (MM_HALOW_BLOCK_ALIGN - 1)) & ~(size_t)(MM_HALOW_BLOCK_ALIGN - 1)) +#define MM_HALOW_BLOCK_HDR (MM_HALOW_BLOCK_ROUND(sizeof(mm_halow_block_t))) +#define MM_HALOW_BLOCK_DATA(b) ((void *)((uint8_t *)(b) + MM_HALOW_BLOCK_HDR)) +#define MM_HALOW_DATA_BLOCK(p) ((mm_halow_block_t *)((uint8_t *)(p) - MM_HALOW_BLOCK_HDR)) + +// The pool itself is a single block taken from the host heap when the +// driver is brought up, and held by a root pointer so the GC keeps it alive. +// Sub-allocation out of it is done here rather than by the GC, because morselib +// allocates from PendSV context where the GC must not run. +static mm_halow_block_t *mm_halow_heap_head; + +// The backing allocation for the pool, obtained from the host via +// mm_halow_port_heap_alloc() (which is responsible for keeping it alive, +// e.g. registering it as a GC root when the host heap is garbage collected). +static uint8_t *mm_halow_heap_mem; + +bool mm_halow_osal_init(void) { + if (mm_halow_heap_mem != NULL) { + return true; + } + uint8_t *heap = mm_halow_port_heap_alloc(MM_HALOW_HEAP_SIZE); + if (heap == NULL) { + return false; + } + mm_halow_heap_mem = heap; + + mm_halow_heap_head = (mm_halow_block_t *)heap; + mm_halow_heap_head->next = NULL; + mm_halow_heap_head->size = MM_HALOW_HEAP_SIZE - MM_HALOW_BLOCK_HDR; + mm_halow_heap_head->used = false; + return true; +} + +void *mm_halow_osal_malloc(size_t size) { + if (size == 0) { + return NULL; + } + // Also stops MM_HALOW_BLOCK_ROUND wrapping to zero for sizes near SIZE_MAX, + // which would otherwise pass every first-fit test and return a pointer into + // the block list itself. Buffer sizes can be derived from received frames. + if (size > MM_HALOW_HEAP_SIZE) { + return NULL; + } + size = MM_HALOW_BLOCK_ROUND(size); + + if (mm_halow_heap_head == NULL) { + return NULL; + } + + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + void *ptr = NULL; + for (mm_halow_block_t *b = mm_halow_heap_head; b != NULL; b = b->next) { + if (b->used || b->size < size) { + continue; + } + // Split the block if the remainder can hold a header and something useful. + if (b->size >= size + MM_HALOW_BLOCK_HDR + MM_HALOW_BLOCK_ALIGN) { + mm_halow_block_t *split = (mm_halow_block_t *)((uint8_t *)b + MM_HALOW_BLOCK_HDR + size); + split->next = b->next; + split->size = b->size - size - MM_HALOW_BLOCK_HDR; + split->used = false; + b->next = split; + b->size = size; + } + b->used = true; + ptr = MM_HALOW_BLOCK_DATA(b); + break; + } + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + return ptr; +} + +void mm_halow_osal_free(void *ptr) { + if (ptr == NULL || mm_halow_heap_head == NULL) { + return; + } + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + MM_HALOW_DATA_BLOCK(ptr)->used = false; + // Coalesce the whole list; it is short and this keeps free O(n) without a + // back pointer per block. + for (mm_halow_block_t *b = mm_halow_heap_head; b != NULL && b->next != NULL;) { + if (!b->used && !b->next->used) { + b->size += MM_HALOW_BLOCK_HDR + b->next->size; + b->next = b->next->next; + } else { + b = b->next; + } + } + MM_HALOW_END_ATOMIC_SECTION(atomic_state); +} + +// Named up here because teardown has to drop the list before the pool the nodes +// live in goes back to the GC heap; the timers themselves are further down. +static struct mmosal_timer *mm_halow_timer_list; + +void mm_halow_osal_deinit(void) { + // Nodes are allocated from the pool that is about to go back to the GC + // heap. morselib normally deletes its timers first, but a fatal error or a + // wedged shutdown does not, and the list outlives mm_halow_init() otherwise. + mm_halow_timer_list = NULL; + mm_halow_heap_head = NULL; + if (mm_halow_heap_mem != NULL) { + mm_halow_port_heap_free(mm_halow_heap_mem); + mm_halow_heap_mem = NULL; + } +} + +void *mmosal_malloc_(size_t size) { + return mm_halow_osal_malloc(size); +} + +void *mmosal_calloc_(size_t nitems, size_t size) { + // nitems * size wraps for large inputs, which would allocate a small block + // while the caller believes it owns the full product; its first write past + // the block corrupts the heap. Fail the allocation instead. + if (nitems != 0 && size > SIZE_MAX / nitems) { + return NULL; + } + size_t total = nitems * size; + void *ptr = mm_halow_osal_malloc(total); + if (ptr != NULL) { + memset(ptr, 0, total); + } + return ptr; +} + +void *mmosal_realloc_(void *ptr, size_t size) { + if (ptr == NULL) { + return mm_halow_osal_malloc(size); + } + if (size == 0) { + mm_halow_osal_free(ptr); + return NULL; + } + size_t old_size = MM_HALOW_DATA_BLOCK(ptr)->size; + if (old_size >= size) { + return ptr; + } + void *new_ptr = mm_halow_osal_malloc(size); + if (new_ptr != NULL) { + memcpy(new_ptr, ptr, old_size); + mm_halow_osal_free(ptr); + } + return new_ptr; +} + +void mmosal_free(void *ptr) { + mm_halow_osal_free(ptr); +} + +void *mmosal_malloc_dbg(size_t size, const char *name, unsigned line_number) { + (void)name; + (void)line_number; + return mmosal_malloc_(size); +} + +void *mmosal_calloc_dbg(size_t nitems, size_t size, const char *name, unsigned line_number) { + (void)name; + (void)line_number; + return mmosal_calloc_(nitems, size); +} + +void *mmosal_realloc_dbg(void *ptr, size_t size, const char *name, unsigned line_number) { + (void)name; + (void)line_number; + return mmosal_realloc_(ptr, size); +} + +/*******************************************************************************/ +// Tasks + +static unsigned mm_halow_critical_nesting; +static uintptr_t mm_halow_critical_state; + +struct mmosal_task *mmosal_task_create(mmosal_task_fn_t task_fn, void *argument, + enum mmosal_task_priority priority, unsigned stack_size_u32, const char *name) { + // Priorities are ignored: tasks run to their next blocking point in creation + // order, so there is nothing to prioritise between. + (void)priority; + return (struct mmosal_task *)mm_halow_sched_task_create(task_fn, argument, stack_size_u32, name); +} + +void mmosal_task_delete(struct mmosal_task *task) { + mm_halow_sched_task_delete((mm_halow_task_t *)task); +} + +struct mmosal_task *mmosal_task_get_active(void) { + return (struct mmosal_task *)mm_halow_sched_task_current(); +} + +void mmosal_task_yield(void) { + mm_halow_sched_yield(); +} + +static bool mm_halow_deadline_passed(void *arg) { + return (int32_t)(mm_halow_ticks_ms() - *(uint32_t *)arg) >= 0; +} + +void mmosal_task_sleep(uint32_t duration_ms) { + if (duration_ms == 0) { + // A zero delay is a yield on an RTOS, not a no-op. + mm_halow_sched_yield(); + return; + } + uint32_t deadline = mm_halow_ticks_ms() + duration_ms; + mm_halow_sched_wait(mm_halow_deadline_passed, &deadline, duration_ms); +} + +// morselib only ever holds a critical section across straight-line work (list +// and counter updates), never across a blocking call. That matters here: the +// nesting count is global rather than per-task, so a task that blocked while +// holding one would leave interrupts disabled for whatever ran next. +void mmosal_task_enter_critical(void) { + uintptr_t state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + if (mm_halow_critical_nesting++ == 0) { + mm_halow_critical_state = state; + } +} + +// Whether a critical section is open. The bus path checks this before giving +// up a turn: yielding here would park the section on the suspended task's stack +// with interrupts still disabled, and the count is global, not per task. +bool mm_halow_osal_in_critical(void) { + return mm_halow_critical_nesting > 0; +} + +void mmosal_task_exit_critical(void) { + if (mm_halow_critical_nesting > 0 && --mm_halow_critical_nesting == 0) { + MM_HALOW_END_ATOMIC_SECTION(mm_halow_critical_state); + } +} + +void mmosal_disable_interrupts(void) { + mmosal_task_enter_critical(); +} + +void mmosal_enable_interrupts(void) { + mmosal_task_exit_critical(); +} + +const char *mmosal_task_name(void) { + mm_halow_task_t *task = mm_halow_sched_task_current(); + return task != NULL ? task->name : "main"; +} + +static bool mm_halow_task_is_dead(void *arg) { + return ((mm_halow_task_t *)arg)->state == MM_HALOW_TASK_DEAD; +} + +void mmosal_task_join(struct mmosal_task *task) { + mm_halow_sched_wait(mm_halow_task_is_dead, task, UINT32_MAX); +} + +static bool mm_halow_task_notified(void *arg) { + mm_halow_task_t *task = arg; + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + bool notified = task->notify != 0; + if (notified) { + task->notify--; + } + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + return notified; +} + +bool mmosal_task_wait_for_notification(uint32_t timeout_ms) { + mm_halow_task_t *task = mm_halow_sched_task_current(); + if (task == NULL) { + // Only tasks can wait for notifications. + return false; + } + return mm_halow_sched_wait(mm_halow_task_notified, task, timeout_ms); +} + +void mmosal_task_notify(struct mmosal_task *task) { + if (task == NULL) { + return; + } + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + ((mm_halow_task_t *)task)->notify++; + MM_HALOW_END_ATOMIC_SECTION(atomic_state); +} + +void mmosal_task_notify_from_isr(struct mmosal_task *task) { + mmosal_task_notify(task); +} + +/*******************************************************************************/ +// Mutexes + +struct mmosal_mutex { + volatile mm_halow_task_t *owner; + volatile uint32_t taken_ms; + volatile bool locked; +}; + +struct mmosal_mutex *mmosal_mutex_create(const char *name) { + (void)name; + struct mmosal_mutex *mutex = mm_halow_osal_malloc(sizeof(*mutex)); + if (mutex != NULL) { + mutex->owner = NULL; + mutex->locked = false; + } + return mutex; +} + +void mmosal_mutex_delete(struct mmosal_mutex *mutex) { + mm_halow_osal_free(mutex); +} + +static bool mm_halow_mutex_acquired(void *arg) { + struct mmosal_mutex *mutex = arg; + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + bool acquired = !mutex->locked; + if (acquired) { + mutex->locked = true; + mutex->owner = mm_halow_sched_task_current(); + mutex->taken_ms = mm_halow_ticks_ms(); + } + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + return acquired; +} + +bool mmosal_mutex_get(struct mmosal_mutex *mutex, uint32_t timeout_ms) { + if (mutex == NULL) { + return false; + } + if (timeout_ms == UINT32_MAX) { + // A held mutex is normally released in well under a second; nine is a + // pathology worth naming before settling in to wait it out. + if (mm_halow_sched_wait(mm_halow_mutex_acquired, mutex, 9000)) { + return true; + } + const mm_halow_task_t *owner = (const mm_halow_task_t *)mutex->owner; + MM_HALOW_PRINTF("halow: mutex %p slow (owner=%s state=%u held=%ums)\n", + mutex, owner != NULL ? owner->name : "thread", + owner != NULL ? (unsigned)owner->state : 0u, + (unsigned)(mm_halow_ticks_ms() - mutex->taken_ms)); + } + return mm_halow_sched_wait(mm_halow_mutex_acquired, mutex, timeout_ms); +} + +bool mmosal_mutex_release(struct mmosal_mutex *mutex) { + if (mutex == NULL) { + return false; + } + mutex->owner = NULL; + mutex->locked = false; + return true; +} + +bool mmosal_mutex_is_held_by_active_task(struct mmosal_mutex *mutex) { + return mutex != NULL && mutex->locked && mutex->owner == mm_halow_sched_task_current(); +} + +/*******************************************************************************/ +// Semaphores + +struct mmosal_sem { + volatile unsigned count; + unsigned max_count; +}; + +struct mmosal_sem *mmosal_sem_create(unsigned max_count, unsigned initial_count, const char *name) { + (void)name; + struct mmosal_sem *sem = mm_halow_osal_malloc(sizeof(*sem)); + if (sem != NULL) { + sem->count = initial_count; + sem->max_count = max_count; + } + return sem; +} + +void mmosal_sem_delete(struct mmosal_sem *sem) { + mm_halow_osal_free(sem); +} + +bool mmosal_sem_give(struct mmosal_sem *sem) { + if (sem == NULL) { + return false; + } + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + bool given = sem->count < sem->max_count; + if (given) { + sem->count++; + } + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + return given; +} + +bool mmosal_sem_give_from_isr(struct mmosal_sem *sem) { + return mmosal_sem_give(sem); +} + +static bool mm_halow_sem_taken(void *arg) { + struct mmosal_sem *sem = arg; + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + bool taken = sem->count > 0; + if (taken) { + sem->count--; + } + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + return taken; +} + +bool mmosal_sem_wait(struct mmosal_sem *sem, uint32_t timeout_ms) { + if (sem == NULL) { + return false; + } + return mm_halow_sched_wait(mm_halow_sem_taken, sem, timeout_ms); +} + +uint32_t mmosal_sem_get_count(struct mmosal_sem *sem) { + return sem != NULL ? sem->count : 0; +} + +/*******************************************************************************/ +// Binary semaphores + +struct mmosal_semb { + volatile bool signalled; +}; + +struct mmosal_semb *mmosal_semb_create(const char *name) { + (void)name; + struct mmosal_semb *semb = mm_halow_osal_malloc(sizeof(*semb)); + if (semb != NULL) { + semb->signalled = false; + } + return semb; +} + +void mmosal_semb_delete(struct mmosal_semb *semb) { + mm_halow_osal_free(semb); +} + +bool mmosal_semb_give(struct mmosal_semb *semb) { + if (semb == NULL) { + return false; + } + semb->signalled = true; + return true; +} + +bool mmosal_semb_give_from_isr(struct mmosal_semb *semb) { + return mmosal_semb_give(semb); +} + +static bool mm_halow_semb_taken(void *arg) { + struct mmosal_semb *semb = arg; + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + bool taken = semb->signalled; + semb->signalled = false; + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + return taken; +} + +bool mmosal_semb_wait(struct mmosal_semb *semb, uint32_t timeout_ms) { + if (semb == NULL) { + return false; + } + return mm_halow_sched_wait(mm_halow_semb_taken, semb, timeout_ms); +} + +/*******************************************************************************/ +// Queues + +struct mmosal_queue { + size_t num_items; + size_t item_size; + volatile size_t head; + volatile size_t tail; + volatile size_t count; + uint8_t *items; +}; + +struct mmosal_queue *mmosal_queue_create(size_t num_items, size_t item_size, const char *name) { + (void)name; + if (item_size != 0 && num_items > (SIZE_MAX - sizeof(struct mmosal_queue)) / item_size) { + return NULL; + } + struct mmosal_queue *queue = mm_halow_osal_malloc(sizeof(*queue) + num_items * item_size); + if (queue != NULL) { + queue->num_items = num_items; + queue->item_size = item_size; + queue->head = 0; + queue->tail = 0; + queue->count = 0; + queue->items = (uint8_t *)(queue + 1); + } + return queue; +} + +void mmosal_queue_delete(struct mmosal_queue *queue) { + mm_halow_osal_free(queue); +} + +bool mmosal_queue_pop_from_isr(struct mmosal_queue *queue, void *item) { + if (queue == NULL) { + return false; + } + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + bool popped = queue->count > 0; + if (popped) { + memcpy(item, queue->items + queue->head * queue->item_size, queue->item_size); + queue->head = (queue->head + 1) % queue->num_items; + queue->count--; + } + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + return popped; +} + +bool mmosal_queue_push_from_isr(struct mmosal_queue *queue, const void *item) { + if (queue == NULL) { + return false; + } + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + bool pushed = queue->count < queue->num_items; + if (pushed) { + memcpy(queue->items + queue->tail * queue->item_size, item, queue->item_size); + queue->tail = (queue->tail + 1) % queue->num_items; + queue->count++; + } + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + return pushed; +} + +// Bundles a queue with the item being transferred, for the wait callbacks below. +typedef struct _mm_halow_queue_op_t { + struct mmosal_queue *queue; + void *item; +} mm_halow_queue_op_t; + +static bool mm_halow_queue_popped(void *arg) { + mm_halow_queue_op_t *op = arg; + return mmosal_queue_pop_from_isr(op->queue, op->item); +} + +static bool mm_halow_queue_pushed(void *arg) { + mm_halow_queue_op_t *op = arg; + return mmosal_queue_push_from_isr(op->queue, op->item); +} + +bool mmosal_queue_pop(struct mmosal_queue *queue, void *item, uint32_t timeout_ms) { + mm_halow_queue_op_t op = { queue, item }; + return mm_halow_sched_wait(mm_halow_queue_popped, &op, timeout_ms); +} + +bool mmosal_queue_push(struct mmosal_queue *queue, const void *item, uint32_t timeout_ms) { + mm_halow_queue_op_t op = { queue, (void *)item }; + return mm_halow_sched_wait(mm_halow_queue_pushed, &op, timeout_ms); +} + +/*******************************************************************************/ +// Time + +uint32_t mmosal_get_time_ms(void) { + return mm_halow_ticks_ms(); +} + +uint32_t mmosal_get_time_ticks(void) { + return mm_halow_ticks_ms(); +} + +uint32_t mmosal_ticks_per_second(void) { + return 1000; +} + +/*******************************************************************************/ +// Timers + +struct mmosal_timer { + struct mmosal_timer *next; + const char *name; + uint32_t period_ms; + uint32_t expires_at; + bool auto_reload; + volatile bool active; + void *arg; + timer_callback_t callback; +}; + +struct mmosal_timer *mmosal_timer_create(const char *name, uint32_t timer_period_ms, + bool auto_reload, void *arg, timer_callback_t callback) { + struct mmosal_timer *timer = mm_halow_osal_malloc(sizeof(*timer)); + if (timer == NULL) { + return NULL; + } + timer->name = name; + timer->period_ms = timer_period_ms; + timer->expires_at = 0; + timer->auto_reload = auto_reload; + timer->active = false; + timer->arg = arg; + timer->callback = callback; + + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + timer->next = mm_halow_timer_list; + mm_halow_timer_list = timer; + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + return timer; +} + +void mmosal_timer_delete(struct mmosal_timer *timer) { + if (timer == NULL) { + return; + } + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + for (struct mmosal_timer **t = &mm_halow_timer_list; *t != NULL; t = &(*t)->next) { + if (*t == timer) { + *t = timer->next; + break; + } + } + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + mm_halow_osal_free(timer); +} + +bool mmosal_timer_start(struct mmosal_timer *timer) { + if (timer == NULL) { + return false; + } + timer->expires_at = mm_halow_ticks_ms() + timer->period_ms; + timer->active = true; + return true; +} + +bool mmosal_timer_stop(struct mmosal_timer *timer) { + if (timer == NULL) { + return false; + } + timer->active = false; + return true; +} + +bool mmosal_timer_change_period(struct mmosal_timer *timer, uint32_t new_period) { + if (timer == NULL) { + return false; + } + timer->period_ms = new_period; + if (timer->active) { + timer->expires_at = mm_halow_ticks_ms() + new_period; + } + return true; +} + +void *mmosal_timer_get_arg(struct mmosal_timer *timer) { + return timer != NULL ? timer->arg : NULL; +} + +bool mmosal_is_timer_active(struct mmosal_timer *timer) { + return timer != NULL && timer->active; +} + +void mm_halow_osal_timer_poll(void) { + uint32_t now = mm_halow_ticks_ms(); + struct mmosal_timer *timer = mm_halow_timer_list; + while (timer != NULL) { + // Read before the callback: deleting the timer from inside its own + // callback is allowed, and that frees the node standing here. + struct mmosal_timer *next = timer->next; + if (timer->active && (int32_t)(now - timer->expires_at) >= 0) { + if (timer->auto_reload) { + timer->expires_at = now + timer->period_ms; + } else { + timer->active = false; + } + timer->callback(timer); + } + timer = next; + } +} + +/*******************************************************************************/ +// Failure handling + +int mmosal_printf(const char *format, ...) { + va_list args; + va_start(args, format); + int ret = MM_HALOW_VPRINTF(format, args); + va_end(args); + return ret; +} + +// The console dies with many of the faults this reports, so the record also +// lands in RAM where a debug probe can read it post-mortem. +struct mm_halow_fatal_record mm_halow_fatal_record; + +void mmosal_log_failure_info(const struct mmosal_failure_info *info) { + mm_halow_fatal_record.pc = info->pc; + mm_halow_fatal_record.lr = info->lr; + mm_halow_fatal_record.fileid = info->fileid; + mm_halow_fatal_record.line = info->line; + mm_halow_fatal_record.ticks_ms = mm_halow_ticks_ms(); + mm_halow_fatal_record.magic = MM_HALOW_FATAL_MAGIC; + MM_HALOW_PRINTF("halow: failure at pc=0x%08x lr=0x%08x file=%u line=%u\n", + (unsigned int)info->pc, (unsigned int)info->lr, + (unsigned int)info->fileid, (unsigned int)info->line); +} + +bool mmosal_extract_failure_info(struct mmosal_failure_info *buf, uint32_t *failure_count) { + (void)buf; + if (failure_count != NULL) { + *failure_count = 0; + } + return false; +} + +void mmosal_impl_assert(void) { + // Hand off to the port's assert/fatal-error handling; it must not return. + mm_halow_port_assert_fail(); + for (;;) { + } +} + +int mmosal_main(mmosal_app_init_cb_t app_init_cb) { + // The host owns main(); morselib is driven from mm_halow_poll(). + (void)app_init_cb; + return -1; +} + +#endif // MM_HALOW_ENABLED diff --git a/src/mm_halow_osal.h b/src/mm_halow_osal.h new file mode 100644 index 0000000..c2aaef9 --- /dev/null +++ b/src/mm_halow_osal.h @@ -0,0 +1,50 @@ +/* + * This file is part of mm-halow-driver. + * + * Copyright (c) 2026 OpenMV LLC. + * + * SPDX-License-Identifier: MIT + * + * MMOSAL implementation. + */ +#ifndef MM_HALOW_INCLUDED_HALOW_OSAL_H +#define MM_HALOW_INCLUDED_HALOW_OSAL_H + +#include +#include + +// Take the driver's memory pool from the host (via mm_halow_port_heap_alloc). +// Must be called from a context where the GC may run, i.e. not from the +// scheduler. Returns false if the pool could not be allocated. +bool mm_halow_osal_init(void); + +// Allocate from the driver's private heap. morselib runs from interrupt +// context, where the host's GC heap (if any) must not be touched. +void *mm_halow_osal_malloc(size_t size); +void mm_halow_osal_free(void *ptr); + +// Run any morselib timers that have expired. Called from mm_halow_poll(). +void mm_halow_osal_timer_poll(void); + +// Release the memory pool back to the host. Only safe once +// morselib has been shut down and the scheduler torn down. +void mm_halow_osal_deinit(void); + +// True while morselib holds a critical section, i.e. while interrupts are off +// on its behalf. Nothing may yield in that window. +bool mm_halow_osal_in_critical(void); + +#endif // MM_HALOW_INCLUDED_HALOW_OSAL_H + +// Fatal-failure record for post-mortem reads over a debug probe: the console +// often dies with the fault, so the last failure is parked in RAM too. +#define MM_HALOW_FATAL_MAGIC (0x48464154) // "HFAT" +struct mm_halow_fatal_record { + uint32_t magic; + uint32_t pc; + uint32_t lr; + uint32_t fileid; + uint32_t line; + uint32_t ticks_ms; +}; +extern struct mm_halow_fatal_record mm_halow_fatal_record; diff --git a/src/mm_halow_pktmem.c b/src/mm_halow_pktmem.c new file mode 100644 index 0000000..7fe92cf --- /dev/null +++ b/src/mm_halow_pktmem.c @@ -0,0 +1,284 @@ +/* + * This file is part of mm-halow-driver. + * + * Copyright (c) 2026 OpenMV LLC. + * + * SPDX-License-Identifier: MIT + * + * Packet memory for the Morse Micro 802.11ah driver. + * + * This is the same design as the SDK's heap-backed mmpktmem, with one change: + * the reserved command pools are taken from the driver's memory pool at init + * rather than living in .bss, so that the driver claims no static RAM. The + * pools exist so that command traffic to and from the transceiver keeps working + * when the pool is too fragmented to satisfy a data allocation. + */ +#include "mm_halow_config.h" + +#if MM_HALOW_ENABLED + +#include +#include + +#include "mmhal_wlan.h" +#include "mmosal.h" +#include "mmpkt.h" +#include "mmpkt_list.h" + +#include "mm_halow_osal.h" + +// Reserved buffers for commands to the transceiver. Commands are small and +// there are never many outstanding at once. +#define MM_HALOW_TX_COMMAND_BLOCK_SIZE (352) +#define MM_HALOW_TX_COMMAND_N_BLOCKS (2) + +// Reserved buffers for command responses, which arrive in full sized packets. +#define MM_HALOW_RX_COMMAND_BLOCK_SIZE (MMHAL_WLAN_MMPKT_RX_MAX_SIZE) +#define MM_HALOW_RX_COMMAND_N_BLOCKS (2) + +// Upper bounds on concurrently allocated data packets, which come straight from +// the driver's pool. These are what actually bound the driver's memory use. +// +// The transmit bound is also the transmit window: morselib stops accepting +// packets at MM_HALOW_TX_PAUSE_THRESHOLD, and a sender then waits for the queue to +// drain rather than filling it. Measured on an N6 at MCS7/8MHz, raising this +// from 16 to 32 took throughput from ~2.4 to ~5.5 Mbit/s and removed the stalls +// entirely; 48 gained nothing further. Blocks are counted rather than pooled, +// so a larger bound costs nothing until the traffic uses it. +#ifndef MM_HALOW_TX_BLOCKS +#define MM_HALOW_TX_BLOCKS (32) +#endif +#ifndef MM_HALOW_RX_BLOCKS +#define MM_HALOW_RX_BLOCKS (16) +#endif + +// Transmission is paused when all but one of the TX allocations are in use, and +// resumed once it drops back below the second threshold. +#define MM_HALOW_TX_PAUSE_THRESHOLD (MM_HALOW_TX_BLOCKS - 1) +#define MM_HALOW_TX_UNPAUSE_THRESHOLD (MM_HALOW_TX_BLOCKS - 2) + +#if MM_HALOW_TX_BLOCKS < 3 || MM_HALOW_RX_BLOCKS < 2 +#error "halow: the packet pools are too small to flow control" +#endif + +typedef struct _mm_halow_pktmem_t { + // Data packets in flight, counted rather than pooled. + volatile atomic_int_least32_t tx_data_allocated; + volatile atomic_uint_fast8_t tx_data_paused; + volatile atomic_int_least32_t rx_data_allocated; + + // Reserved command buffers, and the backing memory they were carved from. + struct mmpkt_list tx_command_free_list; + struct mmpkt_list rx_command_free_list; + uint8_t *tx_command_pool; + uint8_t *rx_command_pool; + + mmhal_wlan_pktmem_tx_flow_control_cb_t tx_flow_control_cb; +} mm_halow_pktmem_t; + +static mm_halow_pktmem_t pktmem; + +// Carve a reserved pool into blocks and put them all on a free list. Returns +// NULL if the driver's pool could not supply the memory, in which case the +// corresponding allocations simply fall back to ordinary pool allocations. +static uint8_t *mm_halow_pool_init(struct mmpkt_list *list, size_t block_size, size_t n_blocks) { + uint8_t *pool = mm_halow_osal_malloc(block_size * n_blocks); + if (pool == NULL) { + return NULL; + } + for (size_t i = 0; i < n_blocks; i++) { + mmpkt_list_append(list, (struct mmpkt *)(pool + block_size * i)); + } + return pool; +} + +void mmhal_wlan_pktmem_init(struct mmhal_wlan_pktmem_init_args *args) { + memset(&pktmem, 0, sizeof(pktmem)); + pktmem.tx_flow_control_cb = args->tx_flow_control_cb; + + pktmem.tx_command_pool = mm_halow_pool_init(&pktmem.tx_command_free_list, + MM_HALOW_TX_COMMAND_BLOCK_SIZE, MM_HALOW_TX_COMMAND_N_BLOCKS); + pktmem.rx_command_pool = mm_halow_pool_init(&pktmem.rx_command_free_list, + MM_HALOW_RX_COMMAND_BLOCK_SIZE, MM_HALOW_RX_COMMAND_N_BLOCKS); +} + +void mmhal_wlan_pktmem_deinit(void) { + // Give anything still holding a packet a chance to let go of it before the + // reserved pools go back to the driver's memory pool. A pool that was + // never allocated has nothing to wait for. + bool drained = false; + for (unsigned i = 0; i < 100; i++) { + drained = pktmem.tx_data_allocated == 0 && pktmem.rx_data_allocated == 0 && + (pktmem.tx_command_pool == NULL || + pktmem.tx_command_free_list.len == MM_HALOW_TX_COMMAND_N_BLOCKS) && + (pktmem.rx_command_pool == NULL || + pktmem.rx_command_free_list.len == MM_HALOW_RX_COMMAND_N_BLOCKS); + if (drained) { + break; + } + mmosal_task_sleep(10); + } + + if (drained) { + mm_halow_osal_free(pktmem.tx_command_pool); + mm_halow_osal_free(pktmem.rx_command_pool); + } + // Otherwise the pools are left where they are: something still holds a + // packet carved out of them, and handing the memory back would let the next + // allocation take it while that packet is still in use. The whole heap is + // released immediately after this, so nothing is really leaked. + memset(&pktmem, 0, sizeof(pktmem)); +} + +/*******************************************************************************/ +// Reserved command pools + +static struct mmpkt *mm_halow_pool_alloc(struct mmpkt_list *list, uint32_t block_size, + const struct mmpkt_ops *ops, uint32_t space_at_start, uint32_t space_at_end, + uint32_t metadata_length) { + MMOSAL_TASK_ENTER_CRITICAL(); + struct mmpkt *buf = mmpkt_list_dequeue(list); + MMOSAL_TASK_EXIT_CRITICAL(); + + if (buf == NULL) { + return NULL; + } + + struct mmpkt *pkt = mmpkt_init_buf((uint8_t *)buf, block_size, + space_at_start, space_at_end, metadata_length, ops); + if (pkt == NULL) { + // Too big for a reserved block; hand it back and let the caller retry + // against the pool. + ops->free_mmpkt(buf); + } + return pkt; +} + +static void mm_halow_tx_command_free(void *mmpkt) { + MMOSAL_TASK_ENTER_CRITICAL(); + mmpkt_list_append(&pktmem.tx_command_free_list, (struct mmpkt *)mmpkt); + MMOSAL_TASK_EXIT_CRITICAL(); +} + +static const struct mmpkt_ops mm_halow_tx_command_ops = { + .free_mmpkt = mm_halow_tx_command_free, +}; + +static void mm_halow_rx_command_free(void *mmpkt) { + MMOSAL_TASK_ENTER_CRITICAL(); + mmpkt_list_append(&pktmem.rx_command_free_list, (struct mmpkt *)mmpkt); + MMOSAL_TASK_EXIT_CRITICAL(); +} + +static const struct mmpkt_ops mm_halow_rx_command_ops = { + .free_mmpkt = mm_halow_rx_command_free, +}; + +/*******************************************************************************/ +// Data packets + +static void mm_halow_tx_data_free(void *mmpkt) { + atomic_int_least32_t old_value = atomic_fetch_sub(&pktmem.tx_data_allocated, 1); + MMOSAL_ASSERT(old_value > 0); + mmosal_free(mmpkt); + + if (pktmem.tx_data_allocated < MM_HALOW_TX_UNPAUSE_THRESHOLD) { + if (atomic_exchange(&pktmem.tx_data_paused, 0)) { + pktmem.tx_flow_control_cb(); + } + } +} + +static const struct mmpkt_ops mm_halow_tx_data_ops = { + .free_mmpkt = mm_halow_tx_data_free, +}; + +static void mm_halow_rx_data_free(void *mmpkt) { + if (mmpkt != NULL) { + atomic_fetch_sub(&pktmem.rx_data_allocated, 1); + mmosal_free(mmpkt); + } +} + +static const struct mmpkt_ops mm_halow_rx_data_ops = { + .free_mmpkt = mm_halow_rx_data_free, +}; + +// mmpkt_alloc_on_heap() rounds each of these up to a multiple of four, which +// wraps to zero near UINT32_MAX and returns a block smaller than the caller +// writes into. Bounded per field, not on the total, which is morselib's call. +static bool mm_halow_pkt_size_ok(uint32_t space_at_start, uint32_t space_at_end, + uint32_t metadata_length, uint32_t max) { + return space_at_start <= max && space_at_end <= max && metadata_length <= max; +} + +struct mmpkt *mmhal_wlan_alloc_mmpkt_for_tx(uint8_t pkt_class, uint32_t space_at_start, + uint32_t space_at_end, uint32_t metadata_length) { + if (!mm_halow_pkt_size_ok(space_at_start, space_at_end, metadata_length, + MMHAL_WLAN_MMPKT_TX_MAX_SIZE)) { + return NULL; + } + // Commands come out of their reserved pool where possible, so that control + // traffic keeps flowing even when the data path has taken everything else. + if (pkt_class == MMHAL_WLAN_PKT_COMMAND) { + struct mmpkt *pkt = mm_halow_pool_alloc(&pktmem.tx_command_free_list, + MM_HALOW_TX_COMMAND_BLOCK_SIZE, &mm_halow_tx_command_ops, + space_at_start, space_at_end, metadata_length); + if (pkt != NULL) { + return pkt; + } + } + + if (atomic_fetch_add(&pktmem.tx_data_allocated, 1) >= MM_HALOW_TX_BLOCKS) { + atomic_fetch_sub(&pktmem.tx_data_allocated, 1); + return NULL; + } + + struct mmpkt *pkt = mmpkt_alloc_on_heap(space_at_start, space_at_end, metadata_length); + if (pkt == NULL) { + atomic_fetch_sub(&pktmem.tx_data_allocated, 1); + return NULL; + } + pkt->ops = &mm_halow_tx_data_ops; + + if (pktmem.tx_data_allocated > MM_HALOW_TX_PAUSE_THRESHOLD) { + if (!atomic_exchange(&pktmem.tx_data_paused, 1)) { + pktmem.tx_flow_control_cb(); + } + } + return pkt; +} + +struct mmpkt *mmhal_wlan_alloc_mmpkt_for_rx(uint8_t pkt_class, uint32_t capacity, + uint32_t metadata_length) { + if (!mm_halow_pkt_size_ok(0, capacity, metadata_length, MMHAL_WLAN_MMPKT_RX_MAX_SIZE)) { + return NULL; + } + if (pkt_class == MMHAL_WLAN_PKT_COMMAND) { + struct mmpkt *pkt = mm_halow_pool_alloc(&pktmem.rx_command_free_list, + MM_HALOW_RX_COMMAND_BLOCK_SIZE, &mm_halow_rx_command_ops, 0, capacity, metadata_length); + if (pkt == NULL) { + pkt = mmpkt_alloc_on_heap(0, capacity, metadata_length); + } + return pkt; + } + + if (atomic_fetch_add(&pktmem.rx_data_allocated, 1) >= MM_HALOW_RX_BLOCKS) { + atomic_fetch_sub(&pktmem.rx_data_allocated, 1); + return NULL; + } + + struct mmpkt *pkt = mmpkt_alloc_on_heap(0, capacity, metadata_length); + if (pkt == NULL) { + atomic_fetch_sub(&pktmem.rx_data_allocated, 1); + return NULL; + } + pkt->ops = &mm_halow_rx_data_ops; + return pkt; +} + +enum mmwlan_tx_flow_control_state mmhal_wlan_pktmem_tx_flow_control_state(void) { + return pktmem.tx_data_paused ? MMWLAN_TX_PAUSED : MMWLAN_TX_READY; +} + +#endif // MM_HALOW_ENABLED diff --git a/src/mm_halow_sched.c b/src/mm_halow_sched.c new file mode 100644 index 0000000..22d2dda --- /dev/null +++ b/src/mm_halow_sched.c @@ -0,0 +1,366 @@ +/* + * This file is part of mm-halow-driver. + * + * Copyright (c) 2026 OpenMV LLC. + * + * SPDX-License-Identifier: MIT + * + * Cooperative task scheduler for the Morse Micro WLAN stack. + */ +#include "mm_halow_config.h" + +#if MM_HALOW_ENABLED + +#include + +#include "mm_halow.h" +#include "mm_halow_osal.h" +#include "mm_halow_sched.h" + +// Number of morselib tasks that can exist at once. morselib itself creates two +// (the driver task and the UMAC event loop), plus one more for the SDIO/SPI IRQ +// task when the SDIO transport is in use. +#ifndef MM_HALOW_SCHED_MAX_TASKS +#define MM_HALOW_SCHED_MAX_TASKS (4) +#endif + +// Size of the context saved by mm_halow_context_switch(), in words: r3-r11 and lr, +// plus the callee-saved half of the FPU register file when hardware floating +// point is in use. Kept even so that stacks stay 8-byte aligned. +#if defined(__ARM_FP) +#define MM_HALOW_CONTEXT_WORDS (10 + 16) +#else +#define MM_HALOW_CONTEXT_WORDS (10) +#endif + +static mm_halow_task_t mm_halow_tasks[MM_HALOW_SCHED_MAX_TASKS]; + +// In creation order, which is the order they run in. +static mm_halow_task_t *mm_halow_task_list; + +// The task currently running, or NULL when running the scheduler itself. +static mm_halow_task_t *mm_halow_task_cur; + +// Wall clock one mm_halow_sched_run() may spend. The scheduler is cooperative, so +// a task that stops yielding cannot be preempted -- only denied another turn. +#ifndef MM_HALOW_SCHED_BUDGET_MS +#define MM_HALOW_SCHED_BUDGET_MS (20) +#endif + +// Task turns per mm_halow_sched_run() call. +// Ceiling on any single wait, however long the caller asked for. +#ifndef MM_HALOW_SCHED_WAIT_MAX_MS +#define MM_HALOW_SCHED_WAIT_MAX_MS (10000) +#endif + +#ifndef MM_HALOW_SCHED_PASSES +#define MM_HALOW_SCHED_PASSES (4) +#endif + +// Stack pointer of whoever called mm_halow_sched_run(), saved while a task runs. +static void *mm_halow_sched_sp; + +// Set while mm_halow_sched_run() is walking the task list, to make it re-entrant. +static volatile bool mm_halow_sched_running; + +// Switch from the context described by *from_sp to the one at to_sp. Both +// contexts are cooperative, so only the callee-saved registers need to be +// preserved; the compiler has already spilled anything else it cares about. +static void __attribute__((naked, noinline)) mm_halow_context_switch(void **from_sp, void *to_sp) { + __asm volatile ( + "push {r3-r11, lr} \n" + #if defined(__ARM_FP) + "vpush {d8-d15} \n" + #endif + "str sp, [r0] \n" + "mov sp, r1 \n" + #if defined(__ARM_FP) + "vpop {d8-d15} \n" + #endif + "pop {r3-r11, pc} \n" + ); +} + +// True when running in an exception handler, where the MicroPython event loop +// must not be pumped. +// mm_halow_in_irq() comes from the configport: on Cortex-M it reads IPSR. + +// Entry trampoline: runs the task main function and then retires the task. +static void mm_halow_task_trampoline(void) { + mm_halow_task_t *task = mm_halow_task_cur; + task->entry(task->arg); + mm_halow_sched_task_delete(NULL); +} + +mm_halow_task_t *mm_halow_sched_task_create(void (*entry)(void *), void *arg, size_t stack_words, const char *name) { + mm_halow_task_t *task = NULL; + for (size_t i = 0; i < MM_HALOW_SCHED_MAX_TASKS; i++) { + if (mm_halow_tasks[i].stack == NULL) { + task = &mm_halow_tasks[i]; + break; + } + } + if (task == NULL) { + return NULL; + } + + // Round the stack up to an even number of words so that it stays 8-byte + // aligned, and reserve room for the initial context. + stack_words = (stack_words + 1) & ~(size_t)1; + if (stack_words < MM_HALOW_CONTEXT_WORDS) { + stack_words = MM_HALOW_CONTEXT_WORDS; + } + uint32_t *stack = mm_halow_osal_malloc(stack_words * sizeof(uint32_t)); + if (stack == NULL) { + return NULL; + } + + // Paint the stack so that overflow can be detected after the fact via + // mm_halow_sched_stack_free_words(). The stack sizes come from morselib and + // are chosen for a FreeRTOS port, not for this scheduler. + for (size_t i = 0; i < stack_words; i++) { + stack[i] = MM_HALOW_STACK_FILL; + } + + memset(task, 0, sizeof(*task)); + task->stack = stack; + task->stack_words = stack_words; + task->name = name; + task->entry = entry; + task->arg = arg; + task->state = MM_HALOW_TASK_READY; + + // Build a context that mm_halow_context_switch() can restore, with the + // trampoline in the slot it pops into pc. + uint32_t *sp = (uint32_t *)task->stack + stack_words - MM_HALOW_CONTEXT_WORDS; + memset(sp, 0, MM_HALOW_CONTEXT_WORDS * sizeof(uint32_t)); + sp[MM_HALOW_CONTEXT_WORDS - 1] = (uint32_t)mm_halow_task_trampoline; + task->sp = sp; + + // Append to the task list, so that tasks run in creation order. + mm_halow_task_t **tail = &mm_halow_task_list; + while (*tail != NULL) { + tail = &(*tail)->next; + } + *tail = task; + + return task; +} + +size_t mm_halow_sched_stack_free_words(const mm_halow_task_t *task) { + if (task == NULL || task->stack == NULL) { + return 0; + } + const uint32_t *stack = (const uint32_t *)task->stack; + size_t free_words = 0; + while (free_words < task->stack_words && stack[free_words] == MM_HALOW_STACK_FILL) { + free_words++; + } + return free_words; +} + +void mm_halow_sched_task_delete(mm_halow_task_t *task) { + if (task == NULL) { + task = mm_halow_task_cur; + if (task == NULL) { + return; + } + // Retire the calling task. Its stack must not be touched again, so + // switch away without saving anything of interest. + task->state = MM_HALOW_TASK_DEAD; + void *discard; + mm_halow_context_switch(&discard, mm_halow_sched_sp); + // Unreachable: a dead task is never resumed. + return; + } + task->state = MM_HALOW_TASK_DEAD; +} + +mm_halow_task_t *mm_halow_sched_task_current(void) { + return mm_halow_task_cur; +} + +// Held while the transceiver is being serviced, and by which context. See +// mm_halow_sched_claim() in the header. +#define MM_HALOW_OWNER_NONE (0) +#define MM_HALOW_OWNER_THREAD (1) +#define MM_HALOW_OWNER_IRQ (2) + +static volatile uint8_t mm_halow_service_owner; +static volatile uint8_t mm_halow_service_depth; + +// When the running pass has to be over. Only meaningful inside +// mm_halow_sched_run(); nothing checks it otherwise. +static uint32_t mm_halow_sched_deadline; + +// Whether the pass now running is out of time. Only meaningful inside +// mm_halow_sched_run(): outside one the deadline is whatever the last pass left +// behind, which is always in the past. +static bool mm_halow_pass_expired(void) { + return mm_halow_sched_running && + (int32_t)(mm_halow_ticks_ms() - mm_halow_sched_deadline) >= 0; +} + +bool mm_halow_sched_over_budget(void) { + // Answered for the running task only. A bus operation outside a task is + // the boot firmware download, driven straight from MicroPython with nothing + // waiting on it -- yielding there would leave the transfer half done. + return mm_halow_task_cur != NULL && mm_halow_pass_expired(); +} + +bool mm_halow_sched_in_callback; + +bool mm_halow_sched_claim(void) { + uint8_t owner = mm_halow_in_irq() ? MM_HALOW_OWNER_IRQ : MM_HALOW_OWNER_THREAD; + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + bool claimed = mm_halow_service_owner == MM_HALOW_OWNER_NONE || mm_halow_service_owner == owner; + if (claimed) { + mm_halow_service_owner = owner; + mm_halow_service_depth++; + } + MM_HALOW_END_ATOMIC_SECTION(atomic_state); + return claimed; +} + +void mm_halow_sched_release(void) { + uintptr_t atomic_state = MM_HALOW_BEGIN_ATOMIC_SECTION(); + if (mm_halow_service_depth > 0 && --mm_halow_service_depth == 0) { + mm_halow_service_owner = MM_HALOW_OWNER_NONE; + } + MM_HALOW_END_ATOMIC_SECTION(atomic_state); +} + +void mm_halow_sched_yield(void) { + mm_halow_task_t *task = mm_halow_task_cur; + if (task != NULL) { + mm_halow_context_switch(&task->sp, mm_halow_sched_sp); + } else { + // Not task context: run the tasks, holding the poll off for the pass so + // it cannot cut in on a transfer that is already under way. + if (mm_halow_sched_claim()) { + mm_halow_sched_run(); + mm_halow_sched_release(); + } + if (!mm_halow_in_irq()) { + // Give the host a chance to run pending events while the driver + // waits. The hook must not raise/longjmp out of the driver. + MM_HALOW_EVENT_POLL_HOOK; + } + } +} + +// Counted only outside task context: a task parked on a semaphore is the normal +// steady state and would leave this permanently set. +static uint16_t mm_halow_wait_depth; + +bool mm_halow_sched_in_wait(void) { + return mm_halow_wait_depth > 0; +} + +bool mm_halow_sched_teardown; + +bool mm_halow_sched_wait(mm_halow_cond_fn_t cond, void *arg, uint32_t timeout_ms) { + // MMOSAL_WAIT_FOREVER is a promise: morselib's SDIO lock path asserts if it + // returns false, so in normal operation it is honoured and a genuine wedge + // is the watchdog's problem. During teardown the promise is capped -- deinit + // on a dead bus has to complete. + bool forever = timeout_ms == UINT32_MAX; + if (timeout_ms > MM_HALOW_SCHED_WAIT_MAX_MS && (!forever || mm_halow_sched_teardown)) { + timeout_ms = MM_HALOW_SCHED_WAIT_MAX_MS; + } + uint32_t start = mm_halow_ticks_ms(); + bool satisfied; + bool counted = mm_halow_sched_task_current() == NULL; + if (counted) { + mm_halow_wait_depth++; + } + for (;;) { + if (cond(arg)) { + satisfied = true; + break; + } + if (timeout_ms == 0) { + satisfied = false; + break; + } + if (forever) { + if (!mm_halow_sched_teardown) { + mm_halow_sched_yield(); + continue; + } + // Teardown began mid-wait: one bounded grace period from here. + timeout_ms = MM_HALOW_SCHED_WAIT_MAX_MS; + start = mm_halow_ticks_ms(); + forever = false; + } + if ((uint32_t)(mm_halow_ticks_ms() - start) >= timeout_ms) { + // Re-check once more, in case the condition was satisfied by an + // interrupt while the deadline was being evaluated. + satisfied = cond(arg); + break; + } + mm_halow_sched_yield(); + } + if (counted) { + mm_halow_wait_depth--; + } + return satisfied; +} + +void mm_halow_sched_reap(void) { + mm_halow_task_t **prev = &mm_halow_task_list; + for (mm_halow_task_t *task = mm_halow_task_list; task != NULL;) { + mm_halow_task_t *next = task->next; + if (task->state == MM_HALOW_TASK_DEAD) { + *prev = next; + mm_halow_osal_free(task->stack); + task->stack = NULL; + task->next = NULL; + } else { + prev = &task->next; + } + task = next; + } +} + +void mm_halow_sched_run(void) { + if (mm_halow_sched_running || mm_halow_task_cur != NULL) { + // Already inside the scheduler, or called from a task. + return; + } + mm_halow_sched_running = true; + mm_halow_sched_deadline = mm_halow_ticks_ms() + MM_HALOW_SCHED_BUDGET_MS; + + // Several passes per call rather than one. Handling a frame takes more than + // one task hop, and with a single pass each hop waits for the next poll, + // which puts milliseconds of scheduling latency into every round trip. A + // task blocked in mm_halow_sched_wait() stays runnable, so this cannot be a + // loop-until-idle; the pass count is what keeps it from spinning. + for (int pass = 0; pass < MM_HALOW_SCHED_PASSES && !mm_halow_pass_expired(); pass++) { + for (mm_halow_task_t *task = mm_halow_task_list; task != NULL; task = task->next) { + if (task->state != MM_HALOW_TASK_READY) { + continue; + } + mm_halow_task_cur = task; + mm_halow_context_switch(&mm_halow_sched_sp, task->sp); + mm_halow_task_cur = NULL; + } + } + + mm_halow_sched_reap(); + mm_halow_sched_running = false; +} + +void mm_halow_sched_deinit(void) { + // Only safe to call from outside the scheduler; tasks are abandoned where + // they stand, which is why morselib must be shut down first. The stacks are + // not freed individually: the whole pool goes back in mm_halow_osal_deinit(). + mm_halow_task_list = NULL; + mm_halow_task_cur = NULL; + mm_halow_sched_running = false; + mm_halow_service_owner = MM_HALOW_OWNER_NONE; + mm_halow_service_depth = 0; + memset(mm_halow_tasks, 0, sizeof(mm_halow_tasks)); +} + +#endif // MM_HALOW_ENABLED diff --git a/src/mm_halow_sched.h b/src/mm_halow_sched.h new file mode 100644 index 0000000..fadd805 --- /dev/null +++ b/src/mm_halow_sched.h @@ -0,0 +1,109 @@ +/* + * This file is part of mm-halow-driver. + * + * Copyright (c) 2026 OpenMV LLC. + * + * SPDX-License-Identifier: MIT + * + * Cooperative task scheduler for the Morse Micro WLAN stack. + * + * morselib is written against an RTOS: it spawns a small number of long-lived + * tasks that sit in "wait for work, do work" loops. MicroPython has no RTOS, + * so this file provides just enough of one: each morselib task gets its own + * stack and runs cooperatively, switching back to the scheduler at every point + * where it would otherwise block. + * + * The scheduler is pumped from mm_halow_poll(), which is dispatched via PendSV in + * so morselib code never runs concurrently with the MicroPython VM, and lwIP + * needs no additional locking. + */ +#ifndef MM_HALOW_INCLUDED_HALOW_SCHED_H +#define MM_HALOW_INCLUDED_HALOW_SCHED_H + +#include +#include +#include + +// Task states. +#define MM_HALOW_TASK_READY (0) // runnable, will be resumed by the scheduler +#define MM_HALOW_TASK_DEAD (1) // returned or deleted, stack is pending free + +typedef struct _mm_halow_task_t { + void *sp; // stack pointer, valid while not running + struct _mm_halow_task_t *next; // next task in the all-tasks list + void *stack; // base of the allocated stack + size_t stack_words; // size of the allocated stack, in words + const char *name; // task name, for debugging + void (*entry)(void *); // task main function + void *arg; // argument passed to the main function + volatile uint32_t notify; // pending task notifications + volatile uint8_t state; // one of HALOW_TASK_xxx +} mm_halow_task_t; + +// Condition function used by mm_halow_sched_wait(), returns true when the wait is over. +typedef bool (*mm_halow_cond_fn_t)(void *arg); + +// Create a task. It starts out ready and first runs on the next mm_halow_sched_run(). +mm_halow_task_t *mm_halow_sched_task_create(void (*entry)(void *), void *arg, size_t stack_words, const char *name); + +// Pattern written across a task stack at creation, so that the untouched tail +// of the stack is distinguishable from anything the task has actually used. +#define MM_HALOW_STACK_FILL (0xA5A5A5A5u) + +// Words of `task`'s stack never written since creation, i.e. the headroom that +// was left over at its deepest point. Zero means the task used every word and +// may well have run past the end: treat that as an overflow, not as a tight +// fit. Diagnostic only; nothing in the driver depends on it. +size_t mm_halow_sched_stack_free_words(const mm_halow_task_t *task); + +// Mark a task as dead. Passing NULL kills the calling task, which does not return. +void mm_halow_sched_task_delete(mm_halow_task_t *task); + +// Return the task the caller is running on, or NULL if this is not task context. +mm_halow_task_t *mm_halow_sched_task_current(void); + +// Give up the CPU. In task context this switches back to the scheduler; in any +// other context it pumps the scheduler and the MicroPython event loop instead. +void mm_halow_sched_yield(void); + +// Block until cond(arg) returns true or timeout_ms elapses. A timeout_ms of 0 +// polls once. Longer waits are capped: nothing here waits forever, because a +// cooperative scheduler that never comes back takes the firmware with it. +// Returns the final value of cond(arg), i.e. true if the wait was satisfied. +bool mm_halow_sched_wait(mm_halow_cond_fn_t cond, void *arg, uint32_t timeout_ms); + +// Resume every ready task once, then return. Safe to call from any context and +// re-entrant against itself (a nested call is a no-op). +void mm_halow_sched_run(void); + +// Claim the right to service the transceiver; false if the other context holds +// it. PendSV preempts MicroPython, which also runs the driver directly, so +// without this the poll can start a bus transaction on top of one in flight. +// Claims nest within a context: refusing a nested one deadlocks, as the nested +// caller is a wait only the scheduler it was refused can satisfy. +// True while scheduled Python callbacks run from inside a morselib wait. +extern bool mm_halow_sched_in_callback; + +// True while the driver is tearing down: FOREVER waits become bounded so +// deinit completes even on a dead bus. +extern bool mm_halow_sched_teardown; + +bool mm_halow_sched_claim(void); +void mm_halow_sched_release(void); + +// True while the driver is inside a wait. A wait runs scheduled Python +// callbacks, so anything reachable from Python that would free the driver's +// memory has to refuse while this holds. +bool mm_halow_sched_in_wait(void); + +// True once the running scheduler pass has used up its time. Bus operations +// check this so that a task which will not yield of its own accord still gives +// the rest of the system a turn between transactions. +bool mm_halow_sched_over_budget(void); + +// Called from mm_halow_sched_run(). +void mm_halow_sched_reap(void); + +void mm_halow_sched_deinit(void); + +#endif // MM_HALOW_INCLUDED_HALOW_SCHED_H diff --git a/src/mmport.h b/src/mmport.h new file mode 100644 index 0000000..e53e241 --- /dev/null +++ b/src/mmport.h @@ -0,0 +1,18 @@ +/* + * This file is part of mm-halow-driver. + * + * Copyright (c) 2026 OpenMV LLC. + * + * SPDX-License-Identifier: MIT + * + * Compiler and architecture hooks required by morselib. + */ +#ifndef MM_HALOW_INCLUDED_MMPORT_H +#define MM_HALOW_INCLUDED_MMPORT_H + +#define MMPORT_BREAKPOINT() __asm("bkpt 0\n\t") +#define MMPORT_GET_LR() __builtin_return_address(0) +#define MMPORT_GET_PC(_a) __asm volatile ("mov %0, pc" : "=r" (_a)) +#define MMPORT_MEM_SYNC() __sync_synchronize() + +#endif // MM_HALOW_INCLUDED_MMPORT_H diff --git a/tests/hardware/README.md b/tests/hardware/README.md new file mode 100644 index 0000000..caf9079 --- /dev/null +++ b/tests/hardware/README.md @@ -0,0 +1,22 @@ +# Hardware soak tests + +These are the throughput/soak scripts the driver was validated with on real +hardware (STM32N6, i.MX RT1062 and Alif Ensemble E3 stations against a HaLow +AP on channel 28 at 2 MHz bandwidth). They need a MicroPython embedding of +the driver (the `network.HALOW` binding) and a HaLow access point; they are +run by hand, not by CI. + +- `thru_server.py` -- run on a host reachable through the AP: a TCP (9001) and + UDP (9002) throughput peer. +- `thru_device.py` -- run on the station: associates, then cycles TCP/UDP + up/down transfers against the server for 20 minutes, printing per-cycle + rates. Edit `HOST` to the server's address and provide `halow_config.py` + (see `halow_config.py.example`) with the AP credentials. +- `thru_file.py` -- as above, but drop-tolerant: results are appended to a + file on the device so a run survives the USB console dropping. Useful for + long RF soaks. + +A healthy 2 MHz station sees on the order of 8 Mbit/s UDP up on a Cortex-M55 +class host with the IRQ line wired, and association in single-digit seconds; +sustained losses or watchdog-style stalls indicate an integration problem +(polling cadence, IRQ wiring, or SPI signal integrity) rather than RF. diff --git a/tests/hardware/halow_config.py.example b/tests/hardware/halow_config.py.example new file mode 100644 index 0000000..2aa10ba --- /dev/null +++ b/tests/hardware/halow_config.py.example @@ -0,0 +1,5 @@ +# Per-bench settings for the hardware soak tests. Copy to halow_config.py on +# the device (or bake it into the filesystem image) and fill in your values. +COUNTRY = "US" # regulatory domain of your HaLow AP +SSID = "ssid-halow" +KEY = "your-password" diff --git a/tests/hardware/thru_device.py b/tests/hardware/thru_device.py new file mode 100644 index 0000000..419d6bf --- /dev/null +++ b/tests/hardware/thru_device.py @@ -0,0 +1,121 @@ +# HaLow throughput soak: associate, then cycle TCP/UDP up+down until the budget runs out. +# Buffers are fixed size -- nothing here is sized by anything off the wire. +import network +import socket +import time + +import halow_config as cfg + +HOST = "192.168.0.137" +TCP_PORT = 9001 +UDP_PORT = 9002 +UDP_DOWN_PORT = 9003 +DUR = 5 # seconds per direction +TOTAL_S = 1200 # 20 minutes + +buf = bytearray(1460) +mv = memoryview(buf) +udp_mv = memoryview(buf)[:1200] + + +def kbit(nbytes, ms): + return (nbytes * 8) // ms if ms else 0 + + +def tcp_run(direction): + s = socket.socket() + s.settimeout(20) + s.connect(socket.getaddrinfo(HOST, TCP_PORT)[0][-1]) + s.send(b"UP\n" if direction == "up" else b"DOWN\n") + n = 0 + t = time.ticks_ms() + try: + while time.ticks_diff(time.ticks_ms(), t) < DUR * 1000: + if direction == "up": + n += s.send(mv) + else: + r = s.readinto(mv) + if not r: + break + n += r + except OSError: + pass + ms = time.ticks_diff(time.ticks_ms(), t) + s.close() + return n, ms + + +def udp_run(direction): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(5) + port = UDP_PORT if direction == "up" else UDP_DOWN_PORT + a = socket.getaddrinfo(HOST, port)[0][-1] + n = 0 + t = time.ticks_ms() + try: + if direction == "up": + while time.ticks_diff(time.ticks_ms(), t) < DUR * 1000: + n += s.sendto(udp_mv, a) + s.sendto(b"END", a) + time.sleep_ms(400) # let the sink drain before the next phase + else: + s.sendto(b"DOWN", a) + while time.ticks_diff(time.ticks_ms(), t) < DUR * 1000: + try: + r = s.recvfrom(1500) + except OSError: + break + n += len(r[0]) + except OSError: + pass + ms = time.ticks_diff(time.ticks_ms(), t) + s.close() + return n, ms + + +network.country(cfg.COUNTRY) +w = network.HALOW() +w.config(pm=network.HALOW.PM_NONE) +w.active(True) + +t0 = time.ticks_ms() +w.connect(cfg.SSID, cfg.KEY) +while not w.isconnected(): + if time.ticks_diff(time.ticks_ms(), t0) > 30000: + print("RESULT assoc FAILED") + raise SystemExit + time.sleep_ms(100) +assoc = time.ticks_diff(time.ticks_ms(), t0) +print("assoc_ms", assoc) +print("ifconfig", w.ifconfig()) +print("rssi", w.status("rssi"), "channel", w.config("channel"), "bw", w.config("bandwidth")) + +acc = {} +cycles = 0 +start = time.ticks_ms() +while time.ticks_diff(time.ticks_ms(), start) < TOTAL_S * 1000: + cycles += 1 + row = [] + for kind, fn in (("tcp", tcp_run), ("udp", udp_run)): + for d in ("up", "down"): + n, ms = fn(d) + k = "%s_%s" % (kind, d) + r = kbit(n, ms) + row.append("%s=%d" % (k, r)) + a = acc.setdefault(k, [0, 0, 0, 10**9]) # sum, count, max, min + a[0] += r + a[1] += 1 + if r > a[2]: + a[2] = r + if r < a[3]: + a[3] = r + print("cycle %d %s rssi=%s" % (cycles, " ".join(row), w.status("rssi"))) + +print("---- SUMMARY (kbit/s) ----") +print("assoc_ms", assoc, "cycles", cycles, "connected", w.isconnected()) +for k in sorted(acc): + s, c, mx, mn = acc[k] + print("RESULT %-9s mean=%d min=%d max=%d n=%d" % (k, s // c, mn, mx, c)) +w.disconnect() +w.active(False) +print("RESULT done") diff --git a/tests/hardware/thru_file.py b/tests/hardware/thru_file.py new file mode 100644 index 0000000..ad417ca --- /dev/null +++ b/tests/hardware/thru_file.py @@ -0,0 +1,128 @@ +# Drop-tolerant HaLow throughput: writes cumulative results to /flash/hres.txt +# after every cycle, so a mid-run USB/CDC drop still leaves the numbers on the device. +import network, socket, time +import halow_config as cfg + +HOST = "192.168.0.137" +TCP_PORT = 9001 +UDP_PORT = 9002 +UDP_DOWN_PORT = 9003 +DUR = 5 +TOTAL_S = 90 +buf = bytearray(1460) +mv = memoryview(buf) +udp_mv = memoryview(buf)[:1200] + + +def kbit(n, ms): + return (n * 8) // ms if ms else 0 + + +def tcp_run(d): + s = socket.socket() + s.settimeout(20) + s.connect(socket.getaddrinfo(HOST, TCP_PORT)[0][-1]) + s.send(b"UP\n" if d == "up" else b"DOWN\n") + n = 0 + t = time.ticks_ms() + try: + while time.ticks_diff(time.ticks_ms(), t) < DUR * 1000: + if d == "up": + n += s.send(mv) + else: + r = s.readinto(mv) + if not r: + break + n += r + except OSError: + pass + ms = time.ticks_diff(time.ticks_ms(), t) + s.close() + return n, ms + + +def udp_run(d): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(5) + port = UDP_PORT if d == "up" else UDP_DOWN_PORT + a = socket.getaddrinfo(HOST, port)[0][-1] + n = 0 + t = time.ticks_ms() + try: + if d == "up": + while time.ticks_diff(time.ticks_ms(), t) < DUR * 1000: + n += s.sendto(udp_mv, a) + s.sendto(b"END", a) + time.sleep_ms(400) + else: + s.sendto(b"DOWN", a) + while time.ticks_diff(time.ticks_ms(), t) < DUR * 1000: + try: + r = s.recvfrom(1500) + except OSError: + break + n += len(r[0]) + except OSError: + pass + ms = time.ticks_diff(time.ticks_ms(), t) + s.close() + return n, ms + + +def write(acc, cyc, assoc, conn, rssi): + try: + f = open("/flash/hres.txt", "w") + f.write("assoc_ms %d cycles %d connected %s rssi %s\n" % (assoc, cyc, conn, rssi)) + for k in sorted(acc): + sm, c, mx, mn = acc[k] + f.write("RESULT %-9s mean=%d min=%d max=%d n=%d\n" % (k, sm // c, mn, mx, c)) + f.close() + except Exception as e: + print("write err", e) + + +network.country(cfg.COUNTRY) +w = network.HALOW() +w.config(pm=network.HALOW.PM_NONE) +w.active(True) +t0 = time.ticks_ms() +w.connect(cfg.SSID, cfg.KEY) +while not w.isconnected(): + if time.ticks_diff(time.ticks_ms(), t0) > 30000: + open("/flash/hres.txt", "w").write("assoc FAILED\n") + raise SystemExit + time.sleep_ms(100) +assoc = time.ticks_diff(time.ticks_ms(), t0) +print( + "assoc_ms", + assoc, + "rssi", + w.status("rssi"), + "channel", + w.config("channel"), + "bw", + w.config("bandwidth"), +) +acc = {} +cyc = 0 +start = time.ticks_ms() +while time.ticks_diff(time.ticks_ms(), start) < TOTAL_S * 1000: + cyc += 1 + for kind, fn in (("tcp", tcp_run), ("udp", udp_run)): + for d in ("up", "down"): + n, ms = fn(d) + k = "%s_%s" % (kind, d) + r = kbit(n, ms) + a = acc.setdefault(k, [0, 0, 0, 10**9]) + a[0] += r + a[1] += 1 + if r > a[2]: + a[2] = r + if r < a[3]: + a[3] = r + print("cycle", cyc, "rssi", w.status("rssi")) + write(acc, cyc, assoc, w.isconnected(), w.status("rssi")) +write(acc, cyc, assoc, w.isconnected(), w.status("rssi")) +print("DONE") +w.disconnect() +w.active(False) diff --git a/tests/hardware/thru_server.py b/tests/hardware/thru_server.py new file mode 100644 index 0000000..817515d --- /dev/null +++ b/tests/hardware/thru_server.py @@ -0,0 +1,114 @@ +"""Throughput peer for the HaLow boards. TCP 9001, UDP 9002. + +TCP: client connects and sends "UP\n" (it streams, we sink) or "DOWN\n" (we stream). +UDP: client sends b"UP" datagrams (we sink) or one b"DOWN" (we stream back to its address). +""" + +import socket +import threading +import time + +BUF = bytes(1460) +DUR_CAP = 30 # never stream longer than this per request + + +def tcp_client(c): + try: + c.settimeout(DUR_CAP + 10) + head = c.recv(16) + if head.startswith(b"UP"): + n = 0 + while True: + d = c.recv(65536) + if not d: + break + n += len(d) + print("tcp up sank %d bytes" % n, flush=True) + elif head.startswith(b"DOWN"): + t = time.time() + n = 0 + while time.time() - t < DUR_CAP: + try: + c.sendall(BUF) + n += len(BUF) + except OSError: + break + print("tcp down sent %d bytes" % n, flush=True) + except Exception as e: + print("tcp err", e, flush=True) + finally: + try: + c.close() + except Exception: + pass + + +def tcp_server(): + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(("0.0.0.0", 9001)) + s.listen(4) + while True: + c, _ = s.accept() + threading.Thread(target=tcp_client, args=(c,), daemon=True).start() + + +def udp_down_server(): + """Own port: the 9002 sink can be seconds behind draining an up-flood.""" + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(("0.0.0.0", 9003)) + while True: + d, addr = s.recvfrom(2048) + if d[:4] != b"DOWN": + continue + # Paced: blasting from gigabit into a ~2Mbit link just overruns the AP queue + # and the station receives almost nothing. Offer a fixed rate instead. + rate_bps = 4_000_000 + gap = (1200 * 8) / rate_bps + t = time.time() + n = 0 + nxt = t + while time.time() - t < 6: + try: + s.sendto(BUF[:1200], addr) + n += 1200 + except OSError: + break + nxt += gap + d = nxt - time.time() + if d > 0: + time.sleep(d) + print("udp down offered %d bytes (%.1f kbit/s)" % (n, n * 8 / 1000 / 6), flush=True) + + +def udp_server(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(("0.0.0.0", 9002)) + sank = 0 + while True: + d, addr = s.recvfrom(2048) + if d[:4] == b"DOWN": + t = time.time() + n = 0 + while time.time() - t < 6: + try: + s.sendto(BUF[:1200], addr) + n += 1200 + except OSError: + break + print("udp down sent %d bytes" % n, flush=True) + elif d[:3] == b"END": + print("udp up sank %d bytes" % sank, flush=True) + sank = 0 + else: + sank += len(d) + + +threading.Thread(target=tcp_server, daemon=True).start() +threading.Thread(target=udp_server, daemon=True).start() +threading.Thread(target=udp_down_server, daemon=True).start() +print("thru server up: tcp 9001, udp-up 9002, udp-down 9003", flush=True) +while True: + time.sleep(3600) diff --git a/tests/host/.gitignore b/tests/host/.gitignore new file mode 100644 index 0000000..d15e104 --- /dev/null +++ b/tests/host/.gitignore @@ -0,0 +1,2 @@ +test_alloc +test_alloc_asan diff --git a/tests/host/Makefile b/tests/host/Makefile new file mode 100644 index 0000000..02f19be --- /dev/null +++ b/tests/host/Makefile @@ -0,0 +1,27 @@ +# Host tests for the HaLow allocator. Compiles the REAL src/mm_halow_osal.c +# against stubs, so the tests cannot drift from the code they cover. +# +# make build + run +# make asan build + run under ASan/UBSan (catches what the checks miss) +HALOW = ../../src +SDK = ../../lib/mm-iot-sdk/framework/morselib/include +CFLAGS = -m32 -std=c11 -Wall -Wextra -Werror -g -O1 \ + -Istub -I$(HALOW) -I$(SDK) -Wno-unused-parameter + +all: run + +test_alloc: test_alloc.c $(HALOW)/mm_halow_osal.c + $(CC) $(CFLAGS) -o $@ test_alloc.c + +run: test_alloc + ./test_alloc + +asan: test_alloc.c $(HALOW)/mm_halow_osal.c + $(CC) $(CFLAGS) -fsanitize=address,undefined -fno-omit-frame-pointer \ + -o test_alloc_asan test_alloc.c + ./test_alloc_asan + +clean: + rm -f test_alloc test_alloc_asan + +.PHONY: all run asan clean diff --git a/tests/host/stub/lwip/dhcp.h b/tests/host/stub/lwip/dhcp.h new file mode 100644 index 0000000..415b300 --- /dev/null +++ b/tests/host/stub/lwip/dhcp.h @@ -0,0 +1,6 @@ +#ifndef MM_HALOW_HOSTTEST_LWIP_DHCP_H +#define MM_HALOW_HOSTTEST_LWIP_DHCP_H +#include +struct dhcp { uint8_t state; +}; +#endif diff --git a/tests/host/stub/lwip/netif.h b/tests/host/stub/lwip/netif.h new file mode 100644 index 0000000..84764b0 --- /dev/null +++ b/tests/host/stub/lwip/netif.h @@ -0,0 +1,14 @@ +// Host-test stub: mm_halow.h embeds these by value, so they need to be complete +// types for it to parse. The allocator never touches mm_halow_t, so the layout is +// irrelevant here -- only that it compiles. +#ifndef MM_HALOW_HOSTTEST_LWIP_NETIF_H +#define MM_HALOW_HOSTTEST_LWIP_NETIF_H +#include +typedef struct { uint32_t addr; +} ip4_addr_t; +typedef struct { uint32_t addr; +} ip_addr_t; +struct netif { void *state; + uint8_t num; +}; +#endif diff --git a/tests/host/stub/mm_halow_configport.h b/tests/host/stub/mm_halow_configport.h new file mode 100644 index 0000000..28ce4fd --- /dev/null +++ b/tests/host/stub/mm_halow_configport.h @@ -0,0 +1,73 @@ +// Host-test configport: the mm_halow_config.h contract implemented with +// instrumented counters, so the REAL driver sources compile natively and the +// tests can assert on the integration behavior (atomic balance, heap failure +// handling) instead of copies that would drift. +#ifndef MM_HALOW_HOSTTEST_CONFIGPORT_H +#define MM_HALOW_HOSTTEST_CONFIGPORT_H + +#include +#include +#include + +#define MM_HALOW_ENABLED (1) + +// The driver takes an atomic section around every heap operation because +// morselib allocates from PendSV. On the host there is no preemption, but the +// test counts nesting to prove the sections are balanced -- an unbalanced +// section on target leaves interrupts disabled forever. +extern int mm_halow_test_atomic_depth; +extern int mm_halow_test_atomic_max; + +static inline uintptr_t mm_halow_test_begin_atomic(void) { + mm_halow_test_atomic_depth++; + if (mm_halow_test_atomic_depth > mm_halow_test_atomic_max) { + mm_halow_test_atomic_max = mm_halow_test_atomic_depth; + } + return 0; +} + +static inline void mm_halow_test_end_atomic(uintptr_t state) { + (void)state; + mm_halow_test_atomic_depth--; +} + +#define MM_HALOW_BEGIN_ATOMIC_SECTION() mm_halow_test_begin_atomic() +#define MM_HALOW_END_ATOMIC_SECTION(st) mm_halow_test_end_atomic(st) + +extern uint32_t mm_halow_test_ticks_ms; +static inline uint32_t mm_halow_ticks_ms(void) { + return mm_halow_test_ticks_ms; +} + +// The port's native pin handle; the allocator tests exercise no pins, so the +// accessors are no-ops. +typedef int mm_halow_pin_t; +static inline bool mm_halow_hal_pin_read(mm_halow_pin_t pin) { + (void)pin; + return false; +} +static inline void mm_halow_hal_pin_write(mm_halow_pin_t pin, bool value) { + (void)pin; + (void)value; +} +static inline void mm_halow_hal_pin_input(mm_halow_pin_t pin) { + (void)pin; +} +static inline void mm_halow_hal_pin_output(mm_halow_pin_t pin) { + (void)pin; +} + +// The host harness never runs from interrupt context. +static inline bool mm_halow_in_irq(void) { + return false; +} + +// Keep test output clean: the OSAL log wrapper routes here. The sink still +// evaluates its arguments so diagnostics-only variables stay referenced. +static inline void mm_halow_test_printf_sink(const char *fmt, ...) { + (void)fmt; +} +#define MM_HALOW_PRINTF(...) mm_halow_test_printf_sink(__VA_ARGS__) +#define MM_HALOW_VPRINTF(fmt, args) ((void)(fmt), (void)(args), 0) + +#endif diff --git a/tests/host/test_alloc.c b/tests/host/test_alloc.c new file mode 100644 index 0000000..0fe8590 --- /dev/null +++ b/tests/host/test_alloc.c @@ -0,0 +1,462 @@ +/* + * Host-side tests for the HaLow driver's first-fit allocator. + * + * morselib allocates from PendSV context, so the driver cannot use the + * MicroPython GC and carries its own allocator over a fixed pool. These tests + * compile the real mm_halow_osal.c against the stubs in stub/, and call + * mm_halow_heap_verify() after every operation to check the block list invariants. + */ + +#include +#include +#include +#include +#include +#include + +// --- stub state the headers declare ------------------------------------------ +int mm_halow_test_atomic_depth = 0; +int mm_halow_test_atomic_max = 0; +uint32_t mm_halow_test_ticks_ms = 0; +int mm_halow_test_malloc_fail = 0; + +#include "mm_halow_configport.h" + +// Lets a test force allocation failure and prove the driver degrades instead +// of crashing -- the on-device failure mode with no debugger attached. +uint8_t *mm_halow_port_heap_alloc(size_t n) { + if (mm_halow_test_malloc_fail) { + return NULL; + } + return calloc(1, n); +} +void mm_halow_port_heap_free(uint8_t *p) { + free(p); +} + +// The scheduler is ARM/PendSV and cannot build on the host. mm_halow_osal.c +// references it for the task/mutex/timer layer, which these tests do not +// exercise; stubbing lets the allocator be tested in isolation. +typedef struct _mm_halow_task_t mm_halow_task_t; +typedef bool (*mm_halow_cond_fn_t)(void *arg); +mm_halow_task_t *mm_halow_sched_task_create(void (*entry)(void *), void *arg, size_t sw, const char *n) { + (void)entry; + (void)arg; + (void)sw; + (void)n; + return NULL; +} +void mm_halow_sched_task_delete(mm_halow_task_t *t) { + (void)t; +} +mm_halow_task_t *mm_halow_sched_task_current(void) { + return NULL; +} +void mm_halow_sched_yield(void) { +} +bool mm_halow_sched_wait(mm_halow_cond_fn_t c, void *a, uint32_t t) { + (void)c; + (void)a; + (void)t; + return true; +} +void mm_halow_sched_run(void) { +} +void mm_halow_sched_reap(void) { +} +void mm_halow_sched_deinit(void) { +} +void mm_halow_port_assert_fail(void) { + abort(); +} + +#include "mm_halow_osal.c" + +// ----------------------------------------------------------------------------- +static int failures = 0; +static int checks = 0; + +#define CHECK(cond, ...) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + printf(" FAIL %s:%d: ", __func__, __LINE__); \ + printf(__VA_ARGS__); \ + printf("\n"); \ + } \ + } while (0) + +// Walk the block list and assert every structural invariant. Called after every +// operation, so corruption is reported where it happens. +static void mm_halow_heap_verify(const char *where) { + checks++; + size_t total = 0; + int blocks = 0; + const uint8_t *base = (const uint8_t *)mm_halow_heap_mem; + const uint8_t *end = base + MM_HALOW_HEAP_SIZE; + + for (mm_halow_block_t *b = mm_halow_heap_head; b != NULL; b = b->next) { + blocks++; + if (blocks > 4096) { + failures++; + printf(" FAIL %s: block list is cyclic or absurdly long\n", where); + return; + } + // In range. + if ((const uint8_t *)b < base || (const uint8_t *)b >= end) { + failures++; + printf(" FAIL %s: block %p outside pool [%p,%p)\n", where, + (void *)b, (const void *)base, (const void *)end); + return; + } + // Payload fits inside the pool. + if ((const uint8_t *)b + MM_HALOW_BLOCK_HDR + b->size > end) { + failures++; + printf(" FAIL %s: block %p size %zu runs past the pool end\n", + where, (void *)b, b->size); + return; + } + // Address ordered and non-overlapping: next must sit exactly after us. + if (b->next != NULL) { + const uint8_t *expect = (const uint8_t *)b + MM_HALOW_BLOCK_HDR + b->size; + if ((const uint8_t *)b->next != expect) { + failures++; + printf(" FAIL %s: block %p+%zu should abut %p but next is %p\n", + where, (void *)b, b->size, (const void *)expect, (void *)b->next); + return; + } + // No two adjacent free blocks: free() must coalesce. + if (!b->used && !b->next->used) { + failures++; + printf(" FAIL %s: adjacent free blocks not coalesced at %p\n", + where, (void *)b); + return; + } + } + // Payload alignment: morselib will put DMA-able structures here. + if (((uintptr_t)MM_HALOW_BLOCK_DATA(b) % MM_HALOW_BLOCK_ALIGN) != 0) { + failures++; + printf(" FAIL %s: payload %p not %d-aligned\n", where, + MM_HALOW_BLOCK_DATA(b), MM_HALOW_BLOCK_ALIGN); + return; + } + total += MM_HALOW_BLOCK_HDR + b->size; + } + // Every byte accounted for: no leaked space between blocks. + if (total != MM_HALOW_HEAP_SIZE) { + failures++; + printf(" FAIL %s: blocks account for %zu of %d bytes\n", where, + total, MM_HALOW_HEAP_SIZE); + } +} + +static void setup(void) { + mm_halow_test_malloc_fail = 0; + mm_halow_test_atomic_depth = 0; + CHECK(mm_halow_osal_init(), "init should succeed"); +} +static void teardown(void) { + mm_halow_osal_deinit(); + CHECK(mm_halow_test_atomic_depth == 0, "atomic sections unbalanced: depth %d", + mm_halow_test_atomic_depth); +} + +// ----------------------------------------------------------------------------- +static void test_init_deinit(void) { + setup(); + CHECK(mm_halow_heap_mem != NULL, "heap pointer should be set"); + CHECK(mm_halow_heap_head != NULL, "head should be set"); + mm_halow_heap_verify("after init"); + teardown(); + CHECK(mm_halow_heap_mem == NULL, "heap pointer cleared on deinit"); + // The dangling-head hazard: a stale mm_halow_heap_head after deinit would make + // the next malloc walk freed memory. active(False)/active(True) does this. + CHECK(mm_halow_heap_head == NULL, "head must be cleared on deinit, else the " + "next active(True) walks freed memory"); +} + +static void test_init_is_idempotent(void) { + setup(); + void *first = mm_halow_heap_mem; + CHECK(mm_halow_osal_init(), "second init should succeed"); + CHECK(mm_halow_heap_mem == first, "second init must not re-allocate"); + teardown(); +} + +static void test_init_handles_oom(void) { + mm_halow_osal_deinit(); + mm_halow_test_malloc_fail = 1; + CHECK(!mm_halow_osal_init(), "init must report failure, not crash, when the GC " + "heap cannot give up 96 KB"); + CHECK(mm_halow_heap_mem == NULL, "no heap on failed init"); + CHECK(mm_halow_osal_malloc(64) == NULL, "malloc must return NULL with no pool"); + mm_halow_osal_free(NULL); + mm_halow_test_malloc_fail = 0; +} + +static void test_basic_alloc_free(void) { + setup(); + void *a = mm_halow_osal_malloc(100); + CHECK(a != NULL, "100-byte alloc should succeed"); + mm_halow_heap_verify("after alloc"); + memset(a, 0xAA, 100); + mm_halow_osal_free(a); + mm_halow_heap_verify("after free"); + teardown(); +} + +static void test_zero_size(void) { + setup(); + CHECK(mm_halow_osal_malloc(0) == NULL, "malloc(0) returns NULL"); + mm_halow_heap_verify("after malloc(0)"); + teardown(); +} + +static void test_alignment(void) { + setup(); + // Deliberately awkward sizes: every payload must still be 8-aligned. + size_t sizes[] = { 1, 3, 7, 9, 15, 17, 31, 33, 63, 65, 127 }; + void *p[sizeof(sizes) / sizeof(sizes[0])]; + for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) { + p[i] = mm_halow_osal_malloc(sizes[i]); + CHECK(p[i] != NULL, "alloc %zu failed", sizes[i]); + CHECK(((uintptr_t)p[i] % MM_HALOW_BLOCK_ALIGN) == 0, + "alloc %zu returned %p, not %d-aligned", sizes[i], p[i], MM_HALOW_BLOCK_ALIGN); + } + mm_halow_heap_verify("after ragged allocs"); + for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) { + mm_halow_osal_free(p[i]); + } + mm_halow_heap_verify("after ragged frees"); + teardown(); +} + +static void test_no_overlap_and_writes_are_isolated(void) { + setup(); + enum { N = 64, SZ = 200 }; + uint8_t *p[N]; + for (int i = 0; i < N; i++) { + p[i] = mm_halow_osal_malloc(SZ); + CHECK(p[i] != NULL, "alloc %d failed", i); + memset(p[i], i + 1, SZ); // unique pattern per allocation + } + mm_halow_heap_verify("after N allocs"); + // If any two live allocations overlapped, a later memset would have + // clobbered an earlier one. + for (int i = 0; i < N; i++) { + for (int j = 0; j < SZ; j++) { + if (p[i][j] != (uint8_t)(i + 1)) { + failures++; + printf(" FAIL overlap: block %d byte %d = %02x, expected %02x\n", + i, j, p[i][j], (uint8_t)(i + 1)); + i = N; + break; + } + } + } + checks++; + for (int i = 0; i < N; i++) { + mm_halow_osal_free(p[i]); + } + mm_halow_heap_verify("after N frees"); + teardown(); +} + +static void test_coalesce_restores_full_heap(void) { + setup(); + size_t biggest_before = mm_halow_heap_head->size; + enum { N = 32 }; + void *p[N]; + for (int i = 0; i < N; i++) { + p[i] = mm_halow_osal_malloc(512); + CHECK(p[i] != NULL, "alloc %d failed", i); + } + // Free out of order -- coalescing must not depend on free order. + for (int i = 0; i < N; i += 2) { + mm_halow_osal_free(p[i]); + } + mm_halow_heap_verify("after even frees"); + for (int i = 1; i < N; i += 2) { + mm_halow_osal_free(p[i]); + } + mm_halow_heap_verify("after odd frees"); + CHECK(mm_halow_heap_head->next == NULL, "heap should be one block again, got a list"); + CHECK(mm_halow_heap_head->size == biggest_before, + "heap should be fully reclaimed: %zu vs %zu", mm_halow_heap_head->size, biggest_before); + teardown(); +} + +static void test_exhaustion_returns_null(void) { + setup(); + // Take the pool down to nothing, then confirm failure is a NULL rather than + // a wild pointer or a crash. + int n = 0; + while (mm_halow_osal_malloc(4096) != NULL) { + n++; + if (n > 1000) { + break; + } + } + CHECK(n > 0 && n < 1000, "expected a bounded number of 4K allocs, got %d", n); + CHECK(mm_halow_osal_malloc(4096) == NULL, "exhausted heap must return NULL"); + CHECK(mm_halow_osal_malloc(1) == NULL || 1, "small alloc may still fit; not fatal"); + mm_halow_heap_verify("at exhaustion"); + teardown(); +} + +static void test_too_big_returns_null(void) { + setup(); + CHECK(mm_halow_osal_malloc(MM_HALOW_HEAP_SIZE * 2) == NULL, + "an allocation larger than the pool must return NULL"); + mm_halow_heap_verify("after oversize request"); + teardown(); +} + +// Independent of calloc: any caller passing a size near SIZE_MAX corrupts the +// heap, because MM_HALOW_BLOCK_ROUND wraps. morselib sizes buffers from wire +// values, so a malformed length off the air can reach here. +static void test_malloc_size_overflow(void) { + setup(); + size_t rounds_to_zero = SIZE_MAX - 3; // (n + 7) & ~7 wraps to 0 + void *p = mm_halow_osal_malloc(rounds_to_zero); + CHECK(p == NULL, "malloc(SIZE_MAX-3) must return NULL; rounding wrapped to 0 " + "and handed back %p", p); + CHECK(mm_halow_osal_malloc(SIZE_MAX) == NULL, "malloc(SIZE_MAX) must return NULL"); + mm_halow_heap_verify("after size-overflow requests"); + teardown(); +} + +static void test_calloc_zeroes(void) { + setup(); + uint8_t *p = mmosal_calloc_(16, 8); + CHECK(p != NULL, "calloc failed"); + int nonzero = 0; + for (int i = 0; i < 16 * 8; i++) { + if (p[i] != 0) { + nonzero++; + } + } + CHECK(nonzero == 0, "calloc left %d non-zero bytes", nonzero); + mm_halow_osal_free(p); + mm_halow_heap_verify("after calloc/free"); + teardown(); +} + +static void test_calloc_overflow(void) { + setup(); + // nitems * size overflows size_t. A wrapped product allocates a tiny block + // while the caller believes it owns gigabytes; the first write past the + // block corrupts the heap. Must return NULL instead. + size_t huge = (size_t)1 << (sizeof(size_t) * 8 - 1); + void *p = mmosal_calloc_(huge, 4); + CHECK(p == NULL, "calloc(%zu, 4) overflows and must return NULL, got %p", huge, p); + void *q = mmosal_calloc_(SIZE_MAX, 2); + CHECK(q == NULL, "calloc(SIZE_MAX, 2) overflows and must return NULL, got %p", q); + mm_halow_heap_verify("after overflow attempts"); + teardown(); +} + +static void test_realloc(void) { + setup(); + CHECK(mmosal_realloc_(NULL, 64) != NULL, "realloc(NULL, n) should malloc"); + void *p = mm_halow_osal_malloc(64); + memset(p, 0x5A, 64); + void *big = mmosal_realloc_(p, 4096); + CHECK(big != NULL, "realloc grow failed"); + int bad = 0; + for (int i = 0; i < 64; i++) { + if (((uint8_t *)big)[i] != 0x5A) { + bad++; + } + } + CHECK(bad == 0, "realloc lost %d bytes of the original contents", bad); + CHECK(mmosal_realloc_(big, 0) == NULL, "realloc(p, 0) frees and returns NULL"); + mm_halow_heap_verify("after realloc"); + teardown(); +} + +// Randomised churn. Deterministic seed so a failure is reproducible. +static void test_stress(void) { + setup(); + enum { SLOTS = 96, ITERS = 20000 }; + uint8_t *p[SLOTS] = { 0 }; + size_t sz[SLOTS] = { 0 }; + unsigned seed = 12345; + int allocs = 0, frees = 0; + + for (int i = 0; i < ITERS; i++) { + seed = seed * 1103515245u + 12345u; + int slot = (seed >> 16) % SLOTS; + if (p[slot] == NULL) { + size_t want = 8 + ((seed >> 8) % 1024); + p[slot] = mm_halow_osal_malloc(want); + if (p[slot] != NULL) { + sz[slot] = want; + memset(p[slot], (uint8_t)(slot + 1), want); + allocs++; + } + } else { + // Verify our bytes survived everything that happened in between. + for (size_t j = 0; j < sz[slot]; j++) { + if (p[slot][j] != (uint8_t)(slot + 1)) { + failures++; + printf(" FAIL stress: slot %d corrupted at byte %zu (iter %d)\n", + slot, j, i); + i = ITERS; + break; + } + } + mm_halow_osal_free(p[slot]); + p[slot] = NULL; + frees++; + } + if ((i % 500) == 0) { + mm_halow_heap_verify("during stress"); + } + } + checks++; + for (int i = 0; i < SLOTS; i++) { + if (p[i] != NULL) { + mm_halow_osal_free(p[i]); + } + } + mm_halow_heap_verify("after stress"); + CHECK(mm_halow_heap_head->next == NULL, + "heap should coalesce back to one block after stress"); + printf(" (stress: %d allocs, %d frees)\n", allocs, frees); + teardown(); +} + +int main(void) { + struct { const char *name; + void (*fn)(void); + } tests[] = { + { "init/deinit", test_init_deinit }, + { "init is idempotent", test_init_is_idempotent }, + { "init handles OOM", test_init_handles_oom }, + { "basic alloc/free", test_basic_alloc_free }, + { "malloc(0)", test_zero_size }, + { "alignment", test_alignment }, + { "no overlap, isolated writes", test_no_overlap_and_writes_are_isolated }, + { "coalesce restores full heap", test_coalesce_restores_full_heap }, + { "exhaustion returns NULL", test_exhaustion_returns_null }, + { "oversize returns NULL", test_too_big_returns_null }, + { "malloc size overflow", test_malloc_size_overflow }, + { "calloc zeroes", test_calloc_zeroes }, + { "calloc overflow", test_calloc_overflow }, + { "realloc", test_realloc }, + { "stress", test_stress }, + }; + int n = (int)(sizeof(tests) / sizeof(tests[0])); + for (int i = 0; i < n; i++) { + int before = failures; + printf("%-32s ", tests[i].name); + fflush(stdout); + tests[i].fn(); + printf("%s\n", failures == before ? "ok" : "FAILED"); + } + printf("\n%d checks, %d failures\n", checks, failures); + printf("max atomic nesting depth: %d\n", mm_halow_test_atomic_max); + return failures != 0; +} diff --git a/tests/qemu/.gitignore b/tests/qemu/.gitignore new file mode 100644 index 0000000..511de33 --- /dev/null +++ b/tests/qemu/.gitignore @@ -0,0 +1,4 @@ +test_sched.elf +test_sched.map +test_sched.bin +*.o diff --git a/tests/qemu/Makefile b/tests/qemu/Makefile new file mode 100644 index 0000000..f67811d --- /dev/null +++ b/tests/qemu/Makefile @@ -0,0 +1,67 @@ +# QEMU tests for mm_halow_sched.c on a Cortex-M55 (mps3-an547 == the N6's core). +# +# make build + run under QEMU with GCC +# make CC=clang GCC=... build with clang, link with the GCC toolchain +# +# Cortex-M55 needs arm-none-eabi-gcc 14 or newer (Ubuntu 24.04 ships 13.2, +# which rejects -mcpu=cortex-m55 with hard float). Override CC to point at a +# suitable toolchain if the default is too old. +CC = arm-none-eabi-gcc + +# GCC toolchain used to link (it resolves newlib and the target multilib via +# -specs, which clang cannot consume) and, for a clang build, to supply the +# bare-metal sysroot. Point it at a GCC >= 14 if the default is too old. +GCC = arm-none-eabi-gcc + +HALOW = ../../src +# mps3.ld is a copy of MicroPython's ports/qemu/mcu/arm/mps3.ld (MIT). +LDSCRIPT = mps3.ld + +# Matches ports/qemu/boards/MPS3_AN547/mpconfigboard.mk, which in turn matches +# how the N6 is built: hard float, so the vpush {d8-d15} path is live. +ARCH = -mthumb -mcpu=cortex-m55 -mfloat-abi=hard -mfpu=fpv5-d16 +CFLAGS = $(ARCH) -std=c11 -Wall -Wextra -Werror -Wno-unused-parameter -Og -g \ + -ffreestanding -fno-common \ + -Istub -I$(HALOW) -I../../lib/mm-iot-sdk/framework/morselib/include +LDFLAGS = $(ARCH) -T$(LDSCRIPT) -nostartfiles -Wl,--gc-sections \ + -Wl,-Map=test_sched.map -specs=nosys.specs \ + -Wl,--no-warn-rwx-segments + +# clang can cross-compile the sources for the Cortex-M55, but it needs the +# bare-metal sysroot (headers + newlib) from the GCC toolchain, its Thumb frame +# pointer (r7) freed so the register-preservation test can clobber it, and the +# ARM EABI short-enum ABI to match newlib. It cannot consume GCC's -specs, so +# the link is done with GCC either way. +LD = $(CC) +ifneq ($(findstring clang,$(CC)),) +CFLAGS += --target=arm-none-eabi --sysroot=$(shell $(GCC) -print-sysroot) \ + -fomit-frame-pointer -fshort-enums +LDFLAGS += -Wl,--no-warn-execstack +LD = $(GCC) +endif + +OBJ = startup.o test_sched.o mm_halow_sched.o + +all: run + +startup.o: startup.c + $(CC) $(CFLAGS) -c $< -o $@ + +test_sched.o: test_sched.c + $(CC) $(CFLAGS) -c $< -o $@ + +mm_halow_sched.o: $(HALOW)/mm_halow_sched.c + $(CC) $(CFLAGS) -c $< -o $@ + +test_sched.elf: $(OBJ) $(LDSCRIPT) + $(LD) $(LDFLAGS) -o $@ $(OBJ) + +run: test_sched.elf + @qemu-system-arm -M mps3-an547 -cpu cortex-m55 -nographic \ + -semihosting-config enable=on,target=native \ + -kernel test_sched.elf + +clean: + rm -f $(OBJ) test_sched.elf test_sched.map + +.PHONY: all run clean diff --git a/tests/qemu/mps3.ld b/tests/qemu/mps3.ld new file mode 100644 index 0000000..545181e --- /dev/null +++ b/tests/qemu/mps3.ld @@ -0,0 +1,55 @@ +/* This file is part of the MicroPython project, http://micropython.org/ + * The MIT License (MIT) + * Copyright (c) 2018 Damien P. George + */ + +MEMORY +{ + ITCM : ORIGIN = 0x00000000, LENGTH = 512K + RAM : ORIGIN = 0x01000000, LENGTH = 2M +} + +_estack = ORIGIN(RAM) + LENGTH(RAM); + +SECTIONS +{ + .isr_vector : { + KEEP(*(.isr_vector)) + . = ALIGN(4); + } > ITCM + + .text : { + *(.text*) + *(.rodata*) + . = ALIGN(4); + *(.ARM.extab*) + *(.gnu.linkonce.armextab.*) + . = ALIGN(4); + __exidx_start = .; + *(.ARM.exidx*) + *(.gnu.linkonce.armexidx.*) + __exidx_end = .; + . = ALIGN(4); + _etext = .; + _sidata = _etext; + } > RAM + + .data : AT ( _sidata ) + { + . = ALIGN(4); + _sdata = .; + *(.data*) + . = ALIGN(4); + _edata = .; + } >RAM + + .bss : + { + . = ALIGN(4); + _sbss = .; + *(.bss*) + *(COMMON) + . = ALIGN(4); + _ebss = .; + } >RAM +} diff --git a/tests/qemu/startup.c b/tests/qemu/startup.c new file mode 100644 index 0000000..1aa2a29 --- /dev/null +++ b/tests/qemu/startup.c @@ -0,0 +1,108 @@ +/* + * Minimal bare-metal startup for the QEMU mps3-an547 (Cortex-M55) harness. + * + * Deliberately self-contained rather than reusing ports/qemu/mcu/arm/startup.c, + * which pulls in MicroPython. All this needs to do is land in main() on a core + * with the FPU enabled, and give the test a way to print and to exit QEMU. + */ + +#include + +extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss; + +int main(void); + +// --- ARM semihosting --------------------------------------------------------- +// Output and exit without needing a UART model. QEMU is run with -semihosting. +#define SYS_WRITE0 (0x04) +#define SYS_EXIT (0x18) +#define ADP_STOPPED_APPLICATION_EXIT (0x20026) +#define ADP_STOPPED_RUN_TIME_ERROR (0x20023) + +static inline int semihost(int op, void *arg) { + register int r0 __asm__ ("r0") = op; + register void *r1 __asm__ ("r1") = arg; + __asm__ volatile ("bkpt 0xAB" : "+r" (r0) : "r" (r1) : "memory"); + return r0; +} + +void qemu_puts(const char *s) { + semihost(SYS_WRITE0, (void *)s); +} + +void qemu_exit(int code) { + // On AArch32, SYS_EXIT takes the reason code DIRECTLY in r1. Passing a + // pointer to a {reason, subcode} block is the AArch64 / SYS_EXIT_EXTENDED + // convention; do that here and QEMU reads the pointer value as an unknown + // reason and exits non-zero -- so a fully passing run still failed the + // build, which is worse than a test that just fails honestly. + semihost(SYS_EXIT, (void *)(uintptr_t)(code == 0 + ? ADP_STOPPED_APPLICATION_EXIT + : ADP_STOPPED_RUN_TIME_ERROR)); + for (;;) { + } +} + +// --- fault handlers ---------------------------------------------------------- +// A context-switch bug shows up as a HardFault. Naming it beats QEMU spinning +// silently, which is what an unhandled fault otherwise looks like. +static void fault(const char *name) { + qemu_puts("\nFAULT: "); + qemu_puts(name); + qemu_puts("\n"); + qemu_exit(1); +} + +void HardFault_Handler(void) { + fault("HardFault"); +} +void MemManage_Handler(void) { + fault("MemManage"); +} +void BusFault_Handler(void) { + fault("BusFault"); +} +void UsageFault_Handler(void) { + fault("UsageFault"); +} +static void Default_Handler(void) { + fault("unexpected exception"); +} + +__attribute__((naked)) void Reset_Handler(void) { + __asm volatile ( + "ldr r0, =_estack \n" + "mov sp, r0 \n" + "bl startup_main \n" + ); +} + +void startup_main(void) { + for (uint32_t *src = &_sidata, *dest = &_sdata; dest < &_edata;) { + *dest++ = *src++; + } + for (uint32_t *dest = &_sbss; dest < &_ebss;) { + *dest++ = 0; + } + // Enable CP10/CP11 (the FPU). The scheduler's context switch saves d8-d15 + // when built with hard float, so without this the first vpush faults. + #define SCB_CPACR (*(volatile uint32_t *)0xE000ED88) + SCB_CPACR |= (0xF << 20); + __asm volatile ("dsb"); + __asm volatile ("isb"); + + int rc = main(); + qemu_exit(rc); +} + +// --- vector table ------------------------------------------------------------ +__attribute__((section(".isr_vector"), used)) +void(*const isr_vector[])(void) = { + (void (*)(void)) & _estack, + Reset_Handler, + Default_Handler, // NMI + HardFault_Handler, + MemManage_Handler, + BusFault_Handler, + UsageFault_Handler, +}; diff --git a/tests/qemu/stub/lwip/dhcp.h b/tests/qemu/stub/lwip/dhcp.h new file mode 100644 index 0000000..7f31107 --- /dev/null +++ b/tests/qemu/stub/lwip/dhcp.h @@ -0,0 +1,6 @@ +#ifndef HALOW_HOSTTEST_LWIP_DHCP_H +#define HALOW_HOSTTEST_LWIP_DHCP_H +#include +struct dhcp { uint8_t state; +}; +#endif diff --git a/tests/qemu/stub/lwip/netif.h b/tests/qemu/stub/lwip/netif.h new file mode 100644 index 0000000..cd49fe5 --- /dev/null +++ b/tests/qemu/stub/lwip/netif.h @@ -0,0 +1,14 @@ +// Host-test stub: mm_halow.h embeds these by value, so they need to be complete +// types for it to parse. The allocator never touches halow_t, so the layout is +// irrelevant here -- only that it compiles. +#ifndef HALOW_HOSTTEST_LWIP_NETIF_H +#define HALOW_HOSTTEST_LWIP_NETIF_H +#include +typedef struct { uint32_t addr; +} ip4_addr_t; +typedef struct { uint32_t addr; +} ip_addr_t; +struct netif { void *state; + uint8_t num; +}; +#endif diff --git a/tests/qemu/stub/mm_halow_configport.h b/tests/qemu/stub/mm_halow_configport.h new file mode 100644 index 0000000..9abc7de --- /dev/null +++ b/tests/qemu/stub/mm_halow_configport.h @@ -0,0 +1,67 @@ +// QEMU-harness configport: the mm_halow_config.h contract, driven by the test. +#ifndef MM_HALOW_QEMUTEST_CONFIGPORT_H +#define MM_HALOW_QEMUTEST_CONFIGPORT_H + +#include +#include +#include + +#define MM_HALOW_ENABLED (1) + +// Single core, no preemption in the harness: the scheduler's atomic sections +// need no masking here. +#define MM_HALOW_BEGIN_ATOMIC_SECTION() (0) +#define MM_HALOW_END_ATOMIC_SECTION(st) ((void)(st)) + +// Driven by the harness rather than a timer, so wait/timeout behaviour is +// deterministic instead of depending on how fast QEMU happens to run. +extern volatile uint32_t mm_halow_test_ticks; +static inline uint32_t mm_halow_ticks_ms(void) { + return mm_halow_test_ticks; +} + +// Real IPSR: the harness runs on a genuine Cortex-M55 under QEMU, and the +// scheduler's IRQ-context detection is part of what is under test. +#define IPSR_ISR_Msk (0x1FFUL) +static inline uint32_t __get_IPSR(void) { + uint32_t result; + __asm volatile ("mrs %0, ipsr" : "=r" (result)); + return result; +} +static inline bool mm_halow_in_irq(void) { + return (__get_IPSR() & IPSR_ISR_Msk) != 0; +} + +// The scheduler sleeps here between turns. The harness has no tick source of +// its own, so stand in for it by advancing the clock the test drives, and +// count the sleeps so the yield path is observable. +extern volatile uint32_t mm_halow_test_event_waits; +#define MM_HALOW_EVENT_POLL_HOOK \ + do { \ + mm_halow_test_ticks += 2; \ + mm_halow_test_event_waits++; \ + } while (0) + +// No pins and no printing in the scheduler test. +typedef int mm_halow_pin_t; +static inline bool mm_halow_hal_pin_read(mm_halow_pin_t pin) { + (void)pin; + return false; +} +static inline void mm_halow_hal_pin_write(mm_halow_pin_t pin, bool value) { + (void)pin; + (void)value; +} +static inline void mm_halow_hal_pin_input(mm_halow_pin_t pin) { + (void)pin; +} +static inline void mm_halow_hal_pin_output(mm_halow_pin_t pin) { + (void)pin; +} +static inline void mm_halow_test_printf_sink(const char *fmt, ...) { + (void)fmt; +} +#define MM_HALOW_PRINTF(...) mm_halow_test_printf_sink(__VA_ARGS__) +#define MM_HALOW_VPRINTF(fmt, args) ((void)(fmt), (void)(args), 0) + +#endif diff --git a/tests/qemu/test_sched.c b/tests/qemu/test_sched.c new file mode 100644 index 0000000..778751a --- /dev/null +++ b/tests/qemu/test_sched.c @@ -0,0 +1,607 @@ +/* + * QEMU tests for the HaLow cooperative scheduler. + * + * mm_halow_sched.c cannot be tested on the host: its core is a naked function that + * saves r4-r11 and d8-d15 by hand and builds a fake initial frame for the first + * switch. mps3-an547 is a Cortex-M55 built with the same float ABI as the + * target, so the register file, FPU banking and exception model all match. + * + * Not covered: SPI, IRQ wiring, morselib, or timing. + */ + +#include +#include +#include + +#include "mm_halow_configport.h" +#include "mm_halow_osal.h" +#include "mm_halow_sched.h" + +void qemu_puts(const char *s); +void qemu_exit(int code); + +volatile uint32_t mm_halow_test_ticks = 0; +volatile uint32_t mm_halow_test_event_waits = 0; + +// --- tiny allocator ---------------------------------------------------------- +// The real first-fit allocator has its own 233-check host suite; this harness is +// about the context switch, so back mm_halow_osal_malloc() with a bump allocator +// and keep the two concerns separate. +#define POOL_SIZE (32 * 1024) +static uint8_t pool[POOL_SIZE] __attribute__((aligned(8))); +static size_t pool_used; +static int pool_frees; + +void *mm_halow_osal_malloc(size_t size) { + size = (size + 7u) & ~(size_t)7u; + if (size == 0 || size > POOL_SIZE - pool_used) { + return NULL; + } + void *p = &pool[pool_used]; + pool_used += size; + return p; +} + +void mm_halow_osal_free(void *ptr) { + if (ptr != NULL) { + pool_frees++; // bump allocator: count it, don't reclaim + } +} + +static void pool_reset(void) { + pool_used = 0; + pool_frees = 0; +} + +// --- test plumbing ----------------------------------------------------------- +static int failures; +static int checks; + +void print_num(uint32_t v) { + char buf[12]; + int i = 11; + buf[i--] = '\0'; + if (v == 0) { + buf[i--] = '0'; + } + while (v > 0 && i >= 0) { + buf[i--] = '0' + (v % 10); + v /= 10; + } + qemu_puts(&buf[i + 1]); +} + +static void check(bool cond, const char *msg) { + checks++; + if (!cond) { + failures++; + qemu_puts(" FAIL: "); + qemu_puts(msg); + qemu_puts("\n"); + } +} + +// --- 1. a task runs and retires ---------------------------------------------- +static volatile int ran_count; +static void task_runs_once(void *arg) { + (void)arg; + ran_count++; +} + +static void test_task_runs_and_is_reaped(void) { + pool_reset(); + mm_halow_sched_deinit(); + ran_count = 0; + mm_halow_task_t *t = mm_halow_sched_task_create(task_runs_once, NULL, 256, "once"); + check(t != NULL, "task_create returned NULL"); + mm_halow_sched_run(); + check(ran_count == 1, "task did not run exactly once"); + // It retired via the trampoline, so run() should have reaped its stack. + check(pool_frees == 1, "dead task's stack was not freed by reap"); + mm_halow_sched_run(); + check(ran_count == 1, "a reaped task ran again"); +} + +// --- 2. core registers survive a switch -------------------------------------- +// The whole point of the naked asm. Load r4-r11 with a known pattern, yield to +// the scheduler and back, and prove every one came back intact. r4-r11 are the +// callee-saved set the ABI says must survive a call. +static volatile int reg_result; +static void task_core_regs(void *arg) { + (void)arg; + uint32_t out[8]; + __asm volatile ( + "mov r4, #0x11 \n" + "mov r5, #0x22 \n" + "mov r6, #0x33 \n" + "mov r7, #0x44 \n" + "mov r8, #0x55 \n" + "mov r9, #0x66 \n" + "mov r10, #0x77 \n" + "mov r11, #0x88 \n" + : + : + : "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11" + ); + mm_halow_sched_yield(); + __asm volatile ( + "str r4, [%0, #0] \n" + "str r5, [%0, #4] \n" + "str r6, [%0, #8] \n" + "str r7, [%0, #12] \n" + "str r8, [%0, #16] \n" + "str r9, [%0, #20] \n" + "str r10, [%0, #24] \n" + "str r11, [%0, #28] \n" + : + : "r" (out) + : "memory" + ); + reg_result = (out[0] == 0x11 && out[1] == 0x22 && out[2] == 0x33 && out[3] == 0x44 + && out[4] == 0x55 && out[5] == 0x66 && out[6] == 0x77 && out[7] == 0x88); +} + +static void test_core_registers_survive(void) { + pool_reset(); + mm_halow_sched_deinit(); + reg_result = -1; + mm_halow_task_t *t = mm_halow_sched_task_create(task_core_regs, NULL, 256, "regs"); + check(t != NULL, "task_create returned NULL"); + mm_halow_sched_run(); // runs up to the yield + mm_halow_sched_run(); // resumes and finishes + check(reg_result == 1, "r4-r11 were NOT preserved across a context switch"); +} + +// --- 3. FPU registers survive a switch --------------------------------------- +// d8-d15 are the callee-saved half of the VFP file and are saved by the vpush in +// mm_halow_context_switch(). If that vpush/vpop pair is wrong -- or if the FPU was +// never enabled -- this is where it shows. +#if defined(__ARM_FP) +static volatile int fpu_result; +static void task_fpu_regs(void *arg) { + (void)arg; + double out[8]; + const double in[8] = { 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5 }; + __asm volatile ("vldm %0, {d8-d15}" : : "r" (in) : "d8", "d9", "d10", "d11", + "d12", "d13", "d14", "d15"); + mm_halow_sched_yield(); + __asm volatile ("vstm %0, {d8-d15}" : : "r" (out) : "memory"); + int ok = 1; + for (int i = 0; i < 8; i++) { + if (out[i] != in[i]) { + ok = 0; + } + } + fpu_result = ok; +} + +static void test_fpu_registers_survive(void) { + pool_reset(); + mm_halow_sched_deinit(); + fpu_result = -1; + mm_halow_task_t *t = mm_halow_sched_task_create(task_fpu_regs, NULL, 512, "fpu"); + check(t != NULL, "task_create returned NULL"); + mm_halow_sched_run(); + mm_halow_sched_run(); + check(fpu_result == 1, "d8-d15 were NOT preserved across a context switch"); +} +#endif + +// --- 4. tasks run in creation order ------------------------------------------ +static volatile int order_idx; +static volatile int order_seen[3]; +static void task_order_a(void *arg) { + order_seen[order_idx++] = (int)(uintptr_t)arg; +} + +static void test_tasks_run_in_creation_order(void) { + pool_reset(); + mm_halow_sched_deinit(); + order_idx = 0; + for (uintptr_t i = 1; i <= 3; i++) { + check(mm_halow_sched_task_create(task_order_a, (void *)i, 256, "ord") != NULL, + "task_create returned NULL"); + } + mm_halow_sched_run(); + check(order_idx == 3, "not all three tasks ran"); + check(order_seen[0] == 1 && order_seen[1] == 2 && order_seen[2] == 3, + "tasks did not run in creation order"); +} + +// --- 5. the argument actually reaches the task ------------------------------- +static volatile uintptr_t arg_seen; +static void task_arg(void *arg) { + arg_seen = (uintptr_t)arg; +} + +static void test_argument_is_passed(void) { + pool_reset(); + mm_halow_sched_deinit(); + arg_seen = 0; + mm_halow_sched_task_create(task_arg, (void *)0xDEADBEEF, 256, "arg"); + mm_halow_sched_run(); + check(arg_seen == 0xDEADBEEF, "task argument did not survive the trampoline"); +} + +// --- 6. wait: satisfied, and timed out --------------------------------------- +static volatile int cond_calls; +static bool cond_true(void *arg) { + (void)arg; + cond_calls++; + return true; +} +static bool cond_false(void *arg) { + (void)arg; + cond_calls++; + return false; +} + +static void test_wait_returns_true_when_satisfied(void) { + mm_halow_sched_deinit(); + cond_calls = 0; + check(mm_halow_sched_wait(cond_true, NULL, 100), "wait should succeed immediately"); + check(cond_calls == 1, "a satisfied condition should be evaluated once"); +} + +static void test_wait_times_out(void) { + mm_halow_sched_deinit(); + cond_calls = 0; + mm_halow_test_ticks = 0; + uint32_t before = mm_halow_test_event_waits; + check(!mm_halow_sched_wait(cond_false, NULL, 5), "wait should time out and return false"); + check(mm_halow_test_event_waits > before, "a wait that times out should have slept, not spun"); +} + +static void test_wait_zero_timeout_polls_once(void) { + mm_halow_sched_deinit(); + cond_calls = 0; + check(!mm_halow_sched_wait(cond_false, NULL, 0), "zero timeout should return false"); + check(cond_calls == 1, "zero timeout should evaluate the condition exactly once"); +} + +// --- 7. re-entrancy: run() from inside a task is a no-op ---------------------- +static volatile int reentrant_depth; +static volatile int reentrant_ran; +static void task_reentrant(void *arg) { + (void)arg; + reentrant_ran++; + reentrant_depth++; + mm_halow_sched_run(); // must not recurse into this task again + reentrant_depth--; +} + +static void test_run_is_not_reentrant(void) { + pool_reset(); + mm_halow_sched_deinit(); + reentrant_ran = 0; + reentrant_depth = 0; + mm_halow_sched_task_create(task_reentrant, NULL, 512, "reent"); + mm_halow_sched_run(); + check(reentrant_ran == 1, "re-entrant run() re-entered the task"); +} + +// --- 8. stack painting reads back sanely ------------------------------------- +// Geometry worth stating, because it is not obvious: the first context switch +// POPS the 26-word initial frame, so a task starts with sp at the very top of +// its stack. The top 26 words are therefore already unpainted before the task +// runs, and only usage deeper than that eats into the fill. A task with a +// shallow frame legitimately leaves the watermark unchanged -- so this test +// deliberately burns far more than that, or it would prove nothing. +static volatile size_t stack_free_seen; +static volatile size_t stack_free_before; +static mm_halow_task_t *stack_task; + +static void task_deep_stack_user(void *arg) { + (void)arg; + // 96 words, comfortably past the 26-word initial frame. volatile so the + // compiler cannot elide it or hoist it into registers. + volatile uint32_t scratch[96]; + for (int i = 0; i < 96; i++) { + scratch[i] = (uint32_t)(i + 1); + } + uint32_t sum = 0; + for (int i = 0; i < 96; i++) { + sum += scratch[i]; + } + (void)sum; + stack_free_seen = mm_halow_sched_stack_free_words(stack_task); +} + +static void test_stack_watermark(void) { + pool_reset(); + mm_halow_sched_deinit(); + const size_t words = 256; + stack_free_seen = 0; + stack_task = mm_halow_sched_task_create(task_deep_stack_user, NULL, words, "wm"); + check(stack_task != NULL, "task_create returned NULL"); + + // Before it runs, exactly the initial context frame is unpainted. + stack_free_before = mm_halow_sched_stack_free_words(stack_task); + check(stack_free_before > 0 && stack_free_before < words, + "fresh stack watermark should be inside (0, stack_words)"); + check(words - stack_free_before < 64, + "more than 64 words unpainted before the task ran: the initial frame is " + "larger than expected, or the fill is not covering the whole stack"); + + mm_halow_sched_run(); + + qemu_puts("\n [words="); + print_num((uint32_t)words); + qemu_puts(" before="); + print_num((uint32_t)stack_free_before); + qemu_puts(" after="); + print_num((uint32_t)stack_free_seen); + qemu_puts(" drop="); + print_num((uint32_t)(stack_free_before - stack_free_seen)); + qemu_puts("] "); + check(stack_free_seen > 0, "watermark read as zero: the task overflowed its " + "stack, or the fill is not working"); + check(stack_free_seen < stack_free_before, + "watermark did not drop after a task burned 96 words of stack: the fill " + "or the scan is not measuring real usage"); + + // Deep stack usage is (stack_words - free), NOT (before - after). A task + // starts with sp at the very top, so the first MM_HALOW_CONTEXT_WORDS it uses + // land in the initial frame's region, which was already unpainted -- those + // words are real usage that never shows up as a drop from `before`. + // Measured here: before=230, after=156, so the drop reads 74 while the task + // actually used 100 words (96 array + 4 of frame). The total is the honest + // number, and it is the one worth reading on the bench. + size_t used = words - stack_free_seen; + check(used >= 96, "total stack used is less than the 96 words the task " + "demonstrably wrote"); + check(used < words, "task used its entire stack: treat as an overflow"); +} + +// --- 9. deinit clears state -------------------------------------------------- +static void test_deinit_clears_state(void) { + pool_reset(); + mm_halow_sched_deinit(); + mm_halow_sched_task_create(task_runs_once, NULL, 256, "d"); + mm_halow_sched_deinit(); + check(mm_halow_sched_task_current() == NULL, "current task not cleared by deinit"); + ran_count = 0; + mm_halow_sched_run(); + check(ran_count == 0, "a task survived deinit and ran"); +} + +// --- 10. task capacity is bounded, not corrupting ----------------------------- +static void test_task_capacity(void) { + pool_reset(); + mm_halow_sched_deinit(); + int created = 0; + for (int i = 0; i < 16; i++) { + if (mm_halow_sched_task_create(task_runs_once, NULL, 128, "cap") != NULL) { + created++; + } + } + check(created > 0, "no tasks could be created at all"); + check(created <= 8, "task table grew past its bound"); + check(mm_halow_sched_task_create(task_runs_once, NULL, 128, "over") == NULL, + "creating past capacity must return NULL, not overflow the table"); +} + +// ----------------------------------------------------------------------------- +struct test { const char *name; + void (*fn)(void); +}; + + +// --- 12. one servicer at a time ---------------------------------------------- +// The transceiver is serviced from two places: the poll dispatched from PendSV, +// and MicroPython context, where a wait or the transmit path runs the driver +// directly. PendSV preempts MicroPython, so without the claim the poll can start +// a bus transaction on top of one already in flight. +static void test_claim_is_exclusive(void) { + pool_reset(); + mm_halow_sched_deinit(); + check(mm_halow_sched_claim(), "first claim was refused"); + mm_halow_sched_release(); + check(mm_halow_sched_claim(), "claim was not released"); + mm_halow_sched_release(); +} + +// Nesting has to be allowed, or a wait reached from inside a claimed region is +// refused the scheduler that is the only thing able to satisfy it. Both of +// these run in thread context, so they are the same owner. +static void test_claim_nests_within_a_context(void) { + pool_reset(); + mm_halow_sched_deinit(); + check(mm_halow_sched_claim(), "outer claim was refused"); + check(mm_halow_sched_claim(), "a nested claim from the same context was refused"); + mm_halow_sched_release(); + check(mm_halow_sched_claim(), "the claim was dropped by the inner release"); + mm_halow_sched_release(); + mm_halow_sched_release(); + check(mm_halow_sched_claim(), "the claim was not released by the outermost release"); + mm_halow_sched_release(); +} + +static void test_deinit_releases_the_claim(void) { + pool_reset(); + check(mm_halow_sched_claim(), "claim was refused before deinit"); + mm_halow_sched_deinit(); + check(mm_halow_sched_claim(), "deinit left the claim held"); + mm_halow_sched_release(); +} + +// --- 13. a task that will not yield cannot hold the pass ----------------------- +// This is the regression test for a hang seen on hardware: a transceiver that +// stopped answering on the bus put a driver task into an unbounded retry loop, +// and because the scheduler is cooperative the poll never returned from PendSV. +// USB, the network and the main loop all stopped with it. +// +// The task below models that loop: it spins forever, and like morselib's retry +// path it reaches a bus operation every time round. It must not be able to keep +// the CPU once the pass is out of time. +static volatile uint32_t spinner_laps; + +// Far more laps than the budget can allow, so reaching it means the budget never +// arrived. The task gives up there rather than spinning: a broken scheduler +// should fail this suite, not hang it. +#define SPINNER_LAP_LIMIT (10000) + +static void task_spins_forever(void *arg) { + (void)arg; + while (spinner_laps < SPINNER_LAP_LIMIT) { + spinner_laps++; + // Stands in for mmhal_wlan_spi_cs_assert(): the point in the bus path + // where a task that has overrun its turn gives one up. + mm_halow_test_ticks++; + if (mm_halow_sched_over_budget()) { + mm_halow_sched_yield(); + } + } +} + +static void test_runaway_task_cannot_starve_the_system(void) { + pool_reset(); + mm_halow_sched_deinit(); + mm_halow_test_ticks = 0; + spinner_laps = 0; + mm_halow_sched_task_create(task_spins_forever, NULL, 512, "spin"); + + // Returning at all is the assertion: before the budget existed this call + // never came back. + mm_halow_sched_run(); + check(spinner_laps > 0, "the runaway task never ran"); + check(spinner_laps < SPINNER_LAP_LIMIT, "the task was never denied a turn"); + + // And it is still there, still runnable, having been denied a turn rather + // than killed -- so the driver keeps working if the bus recovers. + uint32_t laps_after_first_pass = spinner_laps; + mm_halow_sched_run(); + check(spinner_laps > laps_after_first_pass, "the runaway task was not resumed"); +} + +// The pass loop runs between task switches, where mm_halow_task_cur is NULL. The +// public predicate answers for the running task, so using it there made the +// check dead code and every pass ran to the full count regardless of the clock. +static volatile uint32_t slow_task_turns; + +static void task_burns_a_whole_turn(void *arg) { + (void)arg; + for (;;) { + slow_task_turns++; + mm_halow_test_ticks += 100; // one turn, far past the budget + mm_halow_sched_yield(); + } +} + +static void test_pass_loop_stops_when_out_of_time(void) { + pool_reset(); + mm_halow_sched_deinit(); + mm_halow_test_ticks = 0; + slow_task_turns = 0; + mm_halow_sched_task_create(task_burns_a_whole_turn, NULL, 512, "slow"); + + mm_halow_sched_run(); + // Each turn overruns the budget, so the pass loop has to stop after the + // first one rather than running out its full count. + check(slow_task_turns == 1, "the pass loop kept going after its time was up"); +} + +// A wait must never be unbounded, whatever was asked for: morselib joins its +// tasks with MMOSAL_WAIT_FOREVER, and a task wedged on a dead bus would take +// the firmware with it. +static int forever_release_after; + +static bool cond_false_until_released(void *arg) { + (void)arg; + cond_calls++; + return mm_halow_test_ticks >= (uint32_t)forever_release_after; +} + +static void test_wait_forever_outlives_the_cap(void) { + // MMOSAL_WAIT_FOREVER is a promise: morselib's SDIO lock path asserts if + // the wait returns false, so it must survive far past the teardown cap. + pool_reset(); + mm_halow_sched_deinit(); + mm_halow_test_ticks = 0; + cond_calls = 0; + forever_release_after = 60000; // six times the cap + check(mm_halow_sched_wait(cond_false_until_released, NULL, 0xFFFFFFFFu), + "an infinite wait gave up instead of waiting"); + check(mm_halow_test_ticks >= 60000, "the wait was satisfied early"); +} + +static void test_wait_forever_is_capped_in_teardown(void) { + // Deinit on a dead bus has to complete: the same wait, with teardown in + // progress, gives up after the cap. + pool_reset(); + mm_halow_sched_deinit(); + mm_halow_test_ticks = 0; + cond_calls = 0; + mm_halow_sched_teardown = true; + check(!mm_halow_sched_wait(cond_false, NULL, 0xFFFFFFFFu), + "a teardown wait should give up"); + mm_halow_sched_teardown = false; + check(mm_halow_test_ticks > 0, "the capped wait did not actually wait"); +} + +static void test_budget_is_not_spent_when_idle(void) { + // A pass that does no work must not report itself over budget, or every + // bus operation would yield and nothing would ever make progress. + pool_reset(); + mm_halow_sched_deinit(); + mm_halow_test_ticks = 0; + mm_halow_sched_run(); + check(!mm_halow_sched_over_budget(), "a fresh pass started out of time"); + + // Outside a pass the deadline is whatever the last one left behind, which + // is always in the past. The transceiver is driven straight from + // MicroPython during boot, and answering true there would yield into the + // scheduler from the middle of a transfer nothing is waiting on. + mm_halow_test_ticks += 10000; + check(!mm_halow_sched_over_budget(), "a stale deadline leaked outside a pass"); +} + +int main(void) { + static const struct test tests[] = { + { "task runs and is reaped", test_task_runs_and_is_reaped }, + { "core registers survive switch", test_core_registers_survive }, + #if defined(__ARM_FP) + { "FPU d8-d15 survive switch", test_fpu_registers_survive }, + #endif + { "tasks run in creation order", test_tasks_run_in_creation_order }, + { "argument reaches the task", test_argument_is_passed }, + { "wait succeeds when satisfied", test_wait_returns_true_when_satisfied }, + { "wait times out", test_wait_times_out }, + { "wait(0) polls once", test_wait_zero_timeout_polls_once }, + { "run() is not re-entrant", test_run_is_not_reentrant }, + { "stack watermark", test_stack_watermark }, + { "deinit clears state", test_deinit_clears_state }, + { "task capacity is bounded", test_task_capacity }, + { "one servicer at a time", test_claim_is_exclusive }, + { "claims nest within a context", test_claim_nests_within_a_context }, + { "deinit releases the claim", test_deinit_releases_the_claim }, + { "runaway task cannot starve", test_runaway_task_cannot_starve_the_system }, + { "budget intact when idle", test_budget_is_not_spent_when_idle }, + { "pass loop stops when out of time", test_pass_loop_stops_when_out_of_time }, + { "an infinite wait outlives the cap", test_wait_forever_outlives_the_cap }, + { "an infinite wait is capped in teardown", test_wait_forever_is_capped_in_teardown }, + }; + + qemu_puts("mm_halow_sched on Cortex-M55 (qemu mps3-an547)\n"); + #if defined(__ARM_FP) + qemu_puts("hard float: FPU context save IS exercised\n"); + #else + qemu_puts("soft float: FPU context save NOT exercised\n"); + #endif + qemu_puts("\n"); + + for (unsigned i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) { + int before = failures; + qemu_puts(" "); + qemu_puts(tests[i].name); + tests[i].fn(); + qemu_puts(failures == before ? " ... ok\n" : " ... FAILED\n"); + } + + qemu_puts("\n"); + print_num((uint32_t)checks); + qemu_puts(" checks, "); + print_num((uint32_t)failures); + qemu_puts(" failures\n"); + return failures != 0; +} From e2c515401deecff8f8eef465959cdcc05c29a15a Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Tue, 15 Sep 2026 13:05:58 -0700 Subject: [PATCH 2/2] all: Add the host, QEMU and clang test workflows. Extends tools/ci.sh (added with the formatting and spelling CI) with the test entry points, and wires them up: - tests.yml runs the host allocator tests (plus ASan) and the QEMU Cortex-M55 scheduler test. - tests_clang.yml builds and runs both with clang too; the QEMU test cross-compiles the sources with clang and links with GCC, so clang also covers mm_halow_sched.c, which the host tests do not build. Signed-off-by: Kwabena W. Agyeman --- .github/workflows/tests.yml | 30 ++++++++++++++ .github/workflows/tests_clang.yml | 30 ++++++++++++++ tools/ci.sh | 69 +++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 .github/workflows/tests_clang.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..5f95cbd --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,30 @@ +name: Run tests + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + host: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install packages + run: source tools/ci.sh && ci_tests_setup + - name: Run tests + run: source tools/ci.sh && ci_tests_run + + qemu: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install packages + run: source tools/ci.sh && ci_tests_qemu_setup + - name: Run tests + run: source tools/ci.sh && ci_tests_qemu_run diff --git a/.github/workflows/tests_clang.yml b/.github/workflows/tests_clang.yml new file mode 100644 index 0000000..f35117b --- /dev/null +++ b/.github/workflows/tests_clang.yml @@ -0,0 +1,30 @@ +name: Run tests with clang + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + host: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install packages + run: source tools/ci.sh && ci_tests_clang_setup + - name: Run tests + run: source tools/ci.sh && ci_tests_clang_run + + qemu: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install packages + run: source tools/ci.sh && ci_tests_qemu_clang_setup + - name: Run tests + run: source tools/ci.sh && ci_tests_qemu_clang_run diff --git a/tools/ci.sh b/tools/ci.sh index 00dfbb7..abd68f8 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -1,5 +1,10 @@ #!/bin/bash +if which nproc > /dev/null; then + MAKEOPTS="-j$(nproc)" +else + MAKEOPTS="-j$(sysctl -n hw.ncpu)" +fi ######################################################################################## # code formatting @@ -25,3 +30,67 @@ function ci_code_spell_run { # src/ and tests/ arrive with the driver; spell-check whatever is present. codespell README.md $(test -d src && echo src) $(test -d tests && echo tests) } + +######################################################################################## +# host tests + +function ci_tests_setup { + sudo apt-get update + sudo apt-get install gcc-multilib +} + +function ci_tests_run { + make $MAKEOPTS -C tests/host + make $MAKEOPTS -C tests/host asan +} + +######################################################################################## +# host tests with clang + +function ci_tests_clang_setup { + sudo apt-get update + sudo apt-get install clang gcc-multilib +} + +function ci_tests_clang_run { + make $MAKEOPTS -C tests/host CC=clang +} + +######################################################################################## +# qemu tests + +# The Cortex-M55 target needs GCC >= 14; the runner's default arm-none-eabi-gcc +# is older, so fetch a current Arm GNU toolchain. +CI_GCC_ARM_VER=14.2.rel1 +CI_GCC_ARM_DIR=$HOME/gcc-arm +CI_GCC_ARM=$CI_GCC_ARM_DIR/bin/arm-none-eabi-gcc + +function ci_gcc_arm_setup { + curl -sL -o gcc-arm.tar.xz \ + "https://developer.arm.com/-/media/Files/downloads/gnu/${CI_GCC_ARM_VER}/binrel/arm-gnu-toolchain-${CI_GCC_ARM_VER}-x86_64-arm-none-eabi.tar.xz" + mkdir -p "$CI_GCC_ARM_DIR" + tar -xf gcc-arm.tar.xz -C "$CI_GCC_ARM_DIR" --strip-components=1 +} + +function ci_tests_qemu_setup { + sudo apt-get update + sudo apt-get install qemu-system-arm + ci_gcc_arm_setup +} + +function ci_tests_qemu_run { + make $MAKEOPTS -C tests/qemu CC="$CI_GCC_ARM" +} + +# clang cross-compiles the sources (using the GCC toolchain's sysroot), then +# links with GCC; this puts clang's eyes on mm_halow_sched.c, which the host +# tests do not build. +function ci_tests_qemu_clang_setup { + sudo apt-get update + sudo apt-get install clang qemu-system-arm + ci_gcc_arm_setup +} + +function ci_tests_qemu_clang_run { + make $MAKEOPTS -C tests/qemu CC=clang GCC="$CI_GCC_ARM" +}