From 88ccae4c7b37fdaa86516f665fc84743b794c904 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 20:41:04 -0700 Subject: [PATCH 01/15] mimxrt/machine_pin: Add mp_hal_pin_interrupt for IRQ registration. Some drivers need to register a GPIO interrupt from C rather than through the machine.Pin.irq() Python method -- for example a network driver that wakes on a transceiver's IRQ line. Add mp_hal_pin_interrupt(), matching the stm32 and rp2 ports, plus mp_hal_pin_interrupt_enable() to mask and unmask an already-registered interrupt, which is useful for interrupt coalescing. The registered interrupt is given the lowest priority, as eth.c already does for the Ethernet IRQs: it only schedules work and must not preempt USB or DMA. Signed-off-by: Kwabena W. Agyeman --- ports/mimxrt/machine_pin.c | 60 ++++++++++++++++++++++++++++++++++++++ ports/mimxrt/mphalport.h | 2 ++ 2 files changed, 62 insertions(+) diff --git a/ports/mimxrt/machine_pin.c b/ports/mimxrt/machine_pin.c index 61b540076c1..fd66f2b6f9e 100644 --- a/ports/mimxrt/machine_pin.c +++ b/ports/mimxrt/machine_pin.c @@ -385,6 +385,66 @@ static mp_obj_t machine_pin_init(size_t n_args, const mp_obj_t *args, mp_map_t * MP_DEFINE_CONST_FUN_OBJ_KW(machine_pin_init_obj, 1, machine_pin_init); // pin.irq(handler=None, trigger=IRQ_FALLING|IRQ_RISING, hard=False) +// Mask/unmask an already-registered pin interrupt, for interrupt coalescing. +void mp_hal_pin_interrupt_enable(mp_hal_pin_obj_t pin, bool enable) { + machine_pin_obj_t *self = (machine_pin_obj_t *)pin; + if (enable) { + GPIO_PortClearInterruptFlags(self->gpio, 1U << self->pin); + GPIO_PortEnableInterrupts(self->gpio, 1U << self->pin); + } else { + GPIO_PortDisableInterrupts(self->gpio, 1U << self->pin); + } +} + +// Register a pin interrupt from C, for drivers that cannot go through +// machine.Pin.irq(). Unlike the Python method the trigger is a raw +// MP_HAL_PIN_TRIGGER_* value, not an index into IRQ_mapping. +void mp_hal_pin_interrupt(mp_hal_pin_obj_t pin, mp_obj_t handler, mp_uint_t trigger, bool hard) { + machine_pin_obj_t *self = (machine_pin_obj_t *)pin; + uint32_t gpio_nr = GPIO_get_instance(self->gpio); + uint32_t index = GET_PIN_IRQ_INDEX(gpio_nr, self->pin); + if (index >= ARRAY_SIZE(MP_STATE_PORT(machine_pin_irq_objects))) { + return; + } + uint32_t irq_num = self->pin < 16 ? GPIO_combined_low_irqs[gpio_nr] : GPIO_combined_high_irqs[gpio_nr]; + + if (handler == mp_const_none || trigger == MP_HAL_PIN_TRIGGER_NONE) { + GPIO_PortDisableInterrupts(self->gpio, 1U << self->pin); + GPIO_PortClearInterruptFlags(self->gpio, 1U << self->pin); + MP_STATE_PORT(machine_pin_irq_objects[index]) = NULL; + return; + } + + if (trigger != MP_HAL_PIN_TRIGGER_FALL && trigger != MP_HAL_PIN_TRIGGER_RISE + && trigger != MP_HAL_PIN_TRIGGER_RISE_FALL) { + return; + } + + machine_pin_irq_obj_t *irq = MP_STATE_PORT(machine_pin_irq_objects[index]); + if (irq == NULL) { + // Allocates: register from thread context at init, not from an ISR. + irq = m_new_obj(machine_pin_irq_obj_t); + irq->base.base.type = &mp_irq_type; + irq->base.methods = (mp_irq_methods_t *)&machine_pin_irq_methods; + irq->base.parent = MP_OBJ_FROM_PTR(self); + MP_STATE_PORT(machine_pin_irq_objects[index]) = irq; + } + + DisableIRQ(irq_num); + GPIO_PortDisableInterrupts(self->gpio, 1U << self->pin); + irq->base.handler = handler; + irq->base.ishard = hard; + irq->flags = 0; + irq->trigger = trigger; + GPIO_PinSetInterruptConfig(self->gpio, self->pin, irq->trigger); + GPIO_PortEnableInterrupts(self->gpio, 1U << self->pin); + GPIO_PortClearInterruptFlags(self->gpio, 1U << self->pin); + // Bottom of the pile, as eth.c does for the Ethernet IRQs: this only ever + // schedules work, and must not cut in front of USB or DMA. + NVIC_SetPriority(irq_num, IRQ_PRI_PENDSV); + EnableIRQ(irq_num); +} + static mp_obj_t machine_pin_irq(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { enum { ARG_handler, ARG_trigger, ARG_hard }; static const mp_arg_t allowed_args[] = { diff --git a/ports/mimxrt/mphalport.h b/ports/mimxrt/mphalport.h index a3ce4cbc483..cecf10eeb5e 100644 --- a/ports/mimxrt/mphalport.h +++ b/ports/mimxrt/mphalport.h @@ -80,6 +80,8 @@ extern ringbuf_t stdin_ringbuf; #define uwTick systick_ms #define mp_hal_pin_obj_t const machine_pin_obj_t * +void mp_hal_pin_interrupt(mp_hal_pin_obj_t pin, mp_obj_t handler, mp_uint_t trigger, bool hard); +void mp_hal_pin_interrupt_enable(mp_hal_pin_obj_t pin, bool enable); #define mp_hal_get_pin_obj(o) pin_find(o) #define mp_hal_pin_name(p) ((p)->name) #define mp_hal_pin_input(p) machine_pin_set_mode(p, PIN_MODE_IN); From a6ffa1d8a378819a97b354ee522fc81360f97a0f Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 20:41:04 -0700 Subject: [PATCH 02/15] alif/machine_pin: Add mp_hal_pin_interrupt for IRQ registration. Some drivers need to register a GPIO interrupt from C rather than through the machine.Pin.irq() Python method. Factor the IRQ-object allocation out of machine_pin_irq() into a shared helper, then build mp_hal_pin_interrupt() and mp_hal_pin_interrupt_enable() on top of it. The latter masks and unmasks an already-registered interrupt, which is useful for interrupt coalescing. The registered interrupt is given the same low priority this port already uses for the cyw43 host-wake IRQ, since it only schedules work. Signed-off-by: Kwabena W. Agyeman --- ports/alif/machine_pin.c | 71 ++++++++++++++++++++++++++++++---------- ports/alif/mphalport.h | 3 ++ 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/ports/alif/machine_pin.c b/ports/alif/machine_pin.c index 979cf4ad2ed..517a8fc9130 100644 --- a/ports/alif/machine_pin.c +++ b/ports/alif/machine_pin.c @@ -339,7 +339,8 @@ static mp_uint_t machine_pin_irq_trigger(mp_obj_t self_in, mp_uint_t trigger) { // Clear GPIO IRQ (must be done after configuring trigger) gpio_interrupt_eoi(self->gpio, self->pin); - // Clear and enable NVIC GPIO IRQ. + // Clear and enable NVIC GPIO IRQ. A C-level registration lowers this + // afterwards -- see mp_hal_pin_interrupt(). NVIC_ClearPendingIRQ(irq->irq_num); NVIC_SetPriority(irq->irq_num, IRQ_PRI_GPIO); NVIC_EnableIRQ(irq->irq_num); @@ -365,25 +366,12 @@ static const mp_irq_methods_t machine_pin_irq_methods = { }; // pin.irq(handler=None, trigger=IRQ_FALLING|IRQ_RISING, hard=False) -static mp_obj_t machine_pin_irq(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { - enum { ARG_handler, ARG_trigger, ARG_hard }; - static const mp_arg_t allowed_args[] = { - { MP_QSTR_handler, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, - { MP_QSTR_trigger, MP_ARG_INT, {.u_int = MP_HAL_PIN_TRIGGER_FALL | MP_HAL_PIN_TRIGGER_RISE} }, - { MP_QSTR_hard, MP_ARG_BOOL, {.u_bool = false} }, - }; - - machine_pin_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); - mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; - mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); - +static machine_pin_irq_obj_t *machine_pin_irq_obj_get(machine_pin_obj_t *self) { machine_pin_irq_obj_t *irq = MACHINE_PIN_IRQ_OBJECT(self->port, self->pin); - - // Allocate a new IRQ object if it doesn't exist. if (irq == NULL) { - irq = m_new_obj(machine_pin_irq_obj_t); uint32_t idx = MACHINE_PIN_IRQ_INDEX(self->port, self->pin); - + // Allocates: register from thread context at init, not from an ISR. + irq = m_new_obj(machine_pin_irq_obj_t); irq->base.base.type = &mp_irq_type; irq->base.methods = (mp_irq_methods_t *)&machine_pin_irq_methods; irq->base.parent = MP_OBJ_FROM_PTR(self); @@ -393,6 +381,55 @@ static mp_obj_t machine_pin_irq(size_t n_args, const mp_obj_t *pos_args, mp_map_ irq->irq_num = (self->port < 15) ? (GPIO0_IRQ0_IRQn + idx) : (LPGPIO_IRQ0_IRQn + self->pin); MP_STATE_PORT(machine_pin_irq_obj[idx]) = irq; } + return irq; +} + +// Mask/unmask an already-registered pin interrupt. Cheap enough to use for +// interrupt coalescing: mask on the edge, unmask once the work has been done. +void mp_hal_pin_interrupt_enable(mp_hal_pin_obj_t pin, bool enable) { + machine_pin_obj_t *self = (machine_pin_obj_t *)pin; + machine_pin_irq_obj_t *irq = MACHINE_PIN_IRQ_OBJECT(self->port, self->pin); + if (irq == NULL) { + return; + } + if (enable) { + gpio_unmask_interrupt(self->gpio, self->pin); + NVIC_EnableIRQ(irq->irq_num); + } else { + NVIC_DisableIRQ(irq->irq_num); + gpio_mask_interrupt(self->gpio, self->pin); + } +} + +// Register a pin interrupt from C, for drivers that cannot go through +// machine.Pin.irq(). A NULL handler or a zero trigger disables it. +void mp_hal_pin_interrupt(mp_hal_pin_obj_t pin, mp_obj_t handler, mp_uint_t trigger, bool hard) { + machine_pin_obj_t *self = (machine_pin_obj_t *)pin; + machine_pin_irq_obj_t *irq = machine_pin_irq_obj_get(self); + if (irq->reserved) { + return; + } + irq->base.handler = handler; + irq->base.ishard = hard; + machine_pin_irq_trigger(MP_OBJ_FROM_PTR(self), trigger); + // A driver wake-up only schedules work, so it belongs near the bottom -- + // the same place this port puts the cyw43 host-wake IRQ. + NVIC_SetPriority(irq->irq_num, IRQ_PRI_CYW43); +} + +static mp_obj_t machine_pin_irq(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_handler, ARG_trigger, ARG_hard }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_handler, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_trigger, MP_ARG_INT, {.u_int = MP_HAL_PIN_TRIGGER_FALL | MP_HAL_PIN_TRIGGER_RISE} }, + { MP_QSTR_hard, MP_ARG_BOOL, {.u_bool = false} }, + }; + + machine_pin_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + machine_pin_irq_obj_t *irq = machine_pin_irq_obj_get(self); if (n_args > 1 || kw_args->used != 0) { if (irq->reserved) { diff --git a/ports/alif/mphalport.h b/ports/alif/mphalport.h index a68960d2b3e..b3f08849218 100644 --- a/ports/alif/mphalport.h +++ b/ports/alif/mphalport.h @@ -90,6 +90,7 @@ extern ringbuf_t stdin_ringbuf; #define MP_HAL_PIN_SPEED_LOW (0) #define MP_HAL_PIN_SPEED_HIGH (PADCTRL_SLEW_RATE_FAST) +#define MP_HAL_PIN_TRIGGER_NONE (0) #define MP_HAL_PIN_TRIGGER_FALL (1) #define MP_HAL_PIN_TRIGGER_RISE (2) @@ -286,6 +287,8 @@ typedef struct _machine_pin_obj_t { } machine_pin_obj_t; mp_hal_pin_obj_t mp_hal_get_pin_obj(mp_obj_t pin_in); +void mp_hal_pin_interrupt(mp_hal_pin_obj_t pin, mp_obj_t handler, mp_uint_t trigger, bool hard); +void mp_hal_pin_interrupt_enable(mp_hal_pin_obj_t pin, bool enable); static inline qstr mp_hal_pin_name(mp_hal_pin_obj_t pin) { return pin->name; From d568e43123fbdd7dc093a002173555b4f8d2fd71 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:52:11 -0700 Subject: [PATCH 03/15] stm32/mphalport: Add mp_hal_pin_interrupt_enable. Add mp_hal_pin_interrupt_enable() to mask and unmask an already-registered pin interrupt, matching the mimxrt and alif ports. A driver can use it for interrupt coalescing: mask on the edge, unmask once the work is done. It wraps the port's existing extint_enable() / extint_disable(). Signed-off-by: Kwabena W. Agyeman --- ports/stm32/mphalport.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ports/stm32/mphalport.h b/ports/stm32/mphalport.h index fe144c6eedd..8480425ca74 100644 --- a/ports/stm32/mphalport.h +++ b/ports/stm32/mphalport.h @@ -148,6 +148,8 @@ static inline mp_uint_t mp_hal_ticks_cpu(void) { #define mp_hal_pin_read(p) (((p)->gpio->IDR >> (p)->pin) & 1) #define mp_hal_pin_write(p, v) ((v) ? mp_hal_pin_high(p) : mp_hal_pin_low(p)) #define mp_hal_pin_interrupt(pin, handler, trigger, hard) extint_register_pin(pin, trigger, hard, handler) +#define mp_hal_pin_interrupt_enable(p, enable) \ + ((enable) ? extint_enable((p)->pin) : extint_disable((p)->pin)) enum mp_hal_pin_interrupt_trigger { MP_HAL_PIN_TRIGGER_NONE, @@ -160,6 +162,8 @@ void mp_hal_pin_config(mp_hal_pin_obj_t pin, uint32_t mode, uint32_t pull, uint3 bool mp_hal_pin_config_alt(mp_hal_pin_obj_t pin, uint32_t mode, uint32_t pull, uint8_t fn, uint8_t unit); void mp_hal_pin_config_speed(mp_hal_pin_obj_t pin_obj, uint32_t speed); void extint_register_pin(const machine_pin_obj_t *pin, uint32_t mode, bool hard_irq, mp_obj_t callback_obj); +void extint_enable(uint line); +void extint_disable(uint line); mp_obj_base_t *mp_hal_get_spi_obj(mp_obj_t spi_in); From 3cbc726296b0395ddabf401b7354d55140f81943 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:23:11 -0700 Subject: [PATCH 04/15] lib/mm-iot-sdk: Add the Morse Micro MM-IoT-SDK submodule. Carries morselib -- the vendor's prebuilt WLAN library -- and the transceiver firmware that the 802.11ah driver is built on. Pinned at release 2.12.3. morselib is distributed as a prebuilt archive under the Morse Micro Binary License; the rest of the SDK is Apache-2.0. Signed-off-by: Kwabena W. Agyeman --- .gitmodules | 3 +++ lib/mm-iot-sdk | 1 + 2 files changed, 4 insertions(+) create mode 160000 lib/mm-iot-sdk diff --git a/.gitmodules b/.gitmodules index d2c229dd6d7..aacc56fb36a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -74,3 +74,6 @@ [submodule "lib/alif-security-toolkit"] path = lib/alif-security-toolkit url = https://github.com/micropython/alif-security-toolkit.git +[submodule "lib/mm-iot-sdk"] + path = lib/mm-iot-sdk + url = https://github.com/MorseMicro/mm-iot-sdk.git diff --git a/lib/mm-iot-sdk b/lib/mm-iot-sdk new file mode 160000 index 00000000000..88606f94a92 --- /dev/null +++ b/lib/mm-iot-sdk @@ -0,0 +1 @@ +Subproject commit 88606f94a929685144e5fdaf432310820ed2c8e0 From eb1eb1089ab009e66892db675114322bb71f6670 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:26:52 -0700 Subject: [PATCH 05/15] drivers/halow: Add the Morse Micro 802.11ah driver core. The portable core that the network.HALOW binding is built on, sitting between morselib and the MicroPython runtime: - a cooperative scheduler that runs morselib's tasks on their own stacks, pumped from the network poll; - an OSAL providing the RTOS primitives morselib expects -- tasks, mutexes, semaphores, queues and timers -- on top of that scheduler; - a HAL driving the transceiver over SPI, with an optional pin interrupt for low-latency receive; - packet memory and the lwIP interface, allocated from the MicroPython heap so the driver keeps no static pool of its own; - build rules that link the prebuilt morselib archive and the firmware blobs. Scheduler and allocator come with qemu and host unit tests. Signed-off-by: Kwabena W. Agyeman --- drivers/halow/halow.h | 378 ++++++ drivers/halow/halow.mk | 74 ++ drivers/halow/halow_ctrl.c | 1169 +++++++++++++++++ drivers/halow/halow_hal.c | 390 ++++++ drivers/halow/halow_libc.c | 99 ++ drivers/halow/halow_lwip.c | 272 ++++ drivers/halow/halow_osal.c | 800 +++++++++++ drivers/halow/halow_osal.h | 68 + drivers/halow/halow_pktmem.c | 303 +++++ drivers/halow/halow_sched.c | 392 ++++++ drivers/halow/halow_sched.h | 127 ++ drivers/halow/mmport.h | 36 + drivers/halow/tests/host/.gitignore | 1 + drivers/halow/tests/host/Makefile | 28 + drivers/halow/tests/host/stub/lwip/dhcp.h | 6 + drivers/halow/tests/host/stub/lwip/netif.h | 14 + drivers/halow/tests/host/stub/py/mphal.h | 43 + drivers/halow/tests/host/stub/py/mpprint.h | 12 + drivers/halow/tests/host/stub/py/runtime.h | 24 + .../host/stub/shared/netutils/dhcpserver.h | 6 + drivers/halow/tests/host/test_alloc.c | 475 +++++++ drivers/halow/tests/qemu/.gitignore | 3 + drivers/halow/tests/qemu/Makefile | 39 + drivers/halow/tests/qemu/startup.c | 108 ++ drivers/halow/tests/qemu/stub/halow.h | 6 + drivers/halow/tests/qemu/stub/lwip/dhcp.h | 6 + drivers/halow/tests/qemu/stub/lwip/netif.h | 14 + drivers/halow/tests/qemu/stub/py/mphal.h | 48 + drivers/halow/tests/qemu/stub/py/runtime.h | 19 + .../qemu/stub/shared/netutils/dhcpserver.h | 6 + drivers/halow/tests/qemu/test_sched.c | 608 +++++++++ tools/codeformat.py | 1 + 32 files changed, 5575 insertions(+) create mode 100644 drivers/halow/halow.h create mode 100644 drivers/halow/halow.mk create mode 100644 drivers/halow/halow_ctrl.c create mode 100644 drivers/halow/halow_hal.c create mode 100644 drivers/halow/halow_libc.c create mode 100644 drivers/halow/halow_lwip.c create mode 100644 drivers/halow/halow_osal.c create mode 100644 drivers/halow/halow_osal.h create mode 100644 drivers/halow/halow_pktmem.c create mode 100644 drivers/halow/halow_sched.c create mode 100644 drivers/halow/halow_sched.h create mode 100644 drivers/halow/mmport.h create mode 100644 drivers/halow/tests/host/.gitignore create mode 100644 drivers/halow/tests/host/Makefile create mode 100644 drivers/halow/tests/host/stub/lwip/dhcp.h create mode 100644 drivers/halow/tests/host/stub/lwip/netif.h create mode 100644 drivers/halow/tests/host/stub/py/mphal.h create mode 100644 drivers/halow/tests/host/stub/py/mpprint.h create mode 100644 drivers/halow/tests/host/stub/py/runtime.h create mode 100644 drivers/halow/tests/host/stub/shared/netutils/dhcpserver.h create mode 100644 drivers/halow/tests/host/test_alloc.c create mode 100644 drivers/halow/tests/qemu/.gitignore create mode 100644 drivers/halow/tests/qemu/Makefile create mode 100644 drivers/halow/tests/qemu/startup.c create mode 100644 drivers/halow/tests/qemu/stub/halow.h create mode 100644 drivers/halow/tests/qemu/stub/lwip/dhcp.h create mode 100644 drivers/halow/tests/qemu/stub/lwip/netif.h create mode 100644 drivers/halow/tests/qemu/stub/py/mphal.h create mode 100644 drivers/halow/tests/qemu/stub/py/runtime.h create mode 100644 drivers/halow/tests/qemu/stub/shared/netutils/dhcpserver.h create mode 100644 drivers/halow/tests/qemu/test_sched.c diff --git a/drivers/halow/halow.h b/drivers/halow/halow.h new file mode 100644 index 00000000000..e86f270cf7a --- /dev/null +++ b/drivers/halow/halow.h @@ -0,0 +1,378 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Driver for the Morse Micro MM6108/MM8108 802.11ah (Wi-Fi HaLow) transceivers. + * + * This layer sits between MicroPython's network module and morselib, and mirrors + * the shape of the cyw43 driver so that the two present the same 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 MICROPY_INCLUDED_DRIVERS_HALOW_HALOW_H +#define MICROPY_INCLUDED_DRIVERS_HALOW_HALOW_H + +#include +#include +#include + +#include "lwip/netif.h" +#include "lwip/dhcp.h" +#include "shared/netutils/dhcpserver.h" + +#include "mmwlan.h" + +// No CYW43_THREAD_ENTER equivalent: morselib already serialises its own API +// through the OSAL mutexes, which block by running the scheduler. A lock that +// suspends the poll across a morselib call stalls those waits instead. + +// Optional: the IRQ line is read by level on every poll regardless, and +// mp_hal_pin_interrupt() is not part of the common mphal API. +#ifndef MICROPY_PY_NETWORK_HALOW_PIN_IRQ +#define MICROPY_PY_NETWORK_HALOW_PIN_IRQ (0) +#endif + +// 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 MICROPY_PY_NETWORK_HALOW_AP +#define MICROPY_PY_NETWORK_HALOW_AP (0) +#endif + +#ifndef MICROPY_HW_HALOW_CHIP +#define MICROPY_HW_HALOW_CHIP mmhal_mm8108 +#endif + +// 25MHz: at 50MHz the SD-over-SPI framing corrupts under sustained traffic and +// the transceiver stops answering. +#ifndef MICROPY_HW_HALOW_SPI_BAUDRATE +#define MICROPY_HW_HALOW_SPI_BAUDRATE (25000000) +#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 MICROPY_HW_HALOW_MTU +#define MICROPY_HW_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 MICROPY_HW_HALOW_STA_ADDRESS +#define MICROPY_HW_HALOW_STA_ADDRESS (0xc0a80102) // 192.168.1.2 +#endif +#ifndef MICROPY_HW_HALOW_STA_NETMASK +#define MICROPY_HW_HALOW_STA_NETMASK (0xffffff00) // 255.255.255.0 +#endif +#ifndef MICROPY_HW_HALOW_STA_GATEWAY +#define MICROPY_HW_HALOW_STA_GATEWAY (0xc0a80101) // 192.168.1.1 +#endif + +// Address the soft AP hands out, if the board does not override it. +#ifndef MICROPY_HW_HALOW_AP_ADDRESS +#define MICROPY_HW_HALOW_AP_ADDRESS (0xc0a80401) // 192.168.4.1 +#endif +#ifndef MICROPY_HW_HALOW_AP_NETMASK +#define MICROPY_HW_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 HALOW_SCAN_CACHE_MAX +#define HALOW_SCAN_CACHE_MAX (32) +#endif + +// Stations the AP will admit, matching the morselib default. +#define HALOW_AP_MAX_STAS (MMWLAN_DEFAULT_AP_MAX_STAS) + +// Interfaces, matching the order of MOD_NETWORK_STA_IF and MOD_NETWORK_AP_IF. +#define HALOW_ITF_STA (0) +#define HALOW_ITF_AP (1) +#define HALOW_ITF_MAX (2) + +// Link status, with the same meaning as the CYW43_LINK_xxx values so that the +// network.HALOW status values match network.WLAN. +#define HALOW_LINK_DOWN (0) // link is down +#define HALOW_LINK_JOIN (1) // connecting to an AP +#define HALOW_LINK_NOIP (2) // associated, but no IP address +#define HALOW_LINK_UP (3) // associated with an IP address +#define HALOW_LINK_FAIL (-1) // connection failed +#define HALOW_LINK_NONET (-2) // no matching SSID found +#define 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 HALOW_SEC_OPEN (0) +#define HALOW_SEC_OWE (1) +#define HALOW_SEC_SAE (2) + +// Radio settings, as accepted by halow_wifi_set_radio(). +#define HALOW_RADIO_AMPDU (0) +#define HALOW_RADIO_SGI (1) +#define HALOW_RADIO_SUBBANDS (2) +#define HALOW_RADIO_RTS (3) +#define HALOW_RADIO_FRAG (4) +#define HALOW_RADIO_LISTEN (5) +#define HALOW_RADIO_WNM_PD (6) +#define HALOW_RADIO_TXPOWER (7) + +// Target wake time negotiation, as asked for in the association request. +// Fixed to literals; see the security types above. +#define HALOW_TWT_REQUEST (0) +#define HALOW_TWT_SUGGEST (1) +#define HALOW_TWT_DEMAND (2) + +// How the regulatory airtime allowance is spent. Fixed to literals; see above. +#define HALOW_DUTY_CYCLE_SPREAD (0) +#define 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 HALOW_RC_RATE_SHIFT (MMWLAN_RC_STATS_RATE_INFO_RATE_OFFSET) +#define HALOW_RC_BW_SHIFT (MMWLAN_RC_STATS_RATE_INFO_BW_OFFSET) +#define HALOW_RC_GI_SHIFT (MMWLAN_RC_STATS_RATE_INFO_GUARD_OFFSET) +#define HALOW_RC_FIELD_MASK (0xf) +#define 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 HALOW_RC_STATS_MAX (128) + +// Power management modes, as accepted by halow_wifi_pm(). +#define HALOW_PM_NONE (0) // always listening +#define HALOW_PM_POWERSAVE (1) // transmit only, transceiver dozes + +// Trace flags, matching cyw43's. +#define HALOW_TRACE_ASYNC_EV (0x0001) +#define HALOW_TRACE_ETH_TX (0x0002) +#define HALOW_TRACE_ETH_RX (0x0004) +#define HALOW_TRACE_ETH_FULL (0x0008) +#define HALOW_TRACE_MAC (0x0010) + +// A single scan result, flattened out of struct mmwlan_scan_result. +typedef struct _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 +} halow_ev_scan_result_t; + +typedef struct _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 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 halow_t is a static global. + 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 MICROPY_PY_NETWORK_HALOW_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[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[HALOW_ITF_MAX]; + #if LWIP_IPV4 && LWIP_DHCP + struct dhcp dhcp_client; + #endif + #if MICROPY_PY_NETWORK_HALOW_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 halow_wifi_set_up(). + char country[2]; +} halow_t; + +extern halow_t halow_state; + +// Set while the driver is up, so that the port knows whether to poll it, as +// cyw43_poll is. There is no equivalent of cyw43_sleep: morselib does not +// publish a next-deadline query, so the driver cannot say when it next needs +// servicing and is polled on every tick instead. +extern void (*halow_poll)(void); + +/*******************************************************************************/ +// Control + +int halow_init(halow_t *self); +void halow_deinit(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 same priority it uses for cyw43_poll(). +void halow_poll_func(void); + +// Ask the port to run halow_poll_func() soon. The default implementation does +// nothing and relies on the periodic poll; ports override it to raise PendSV. +void 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 halow_country_supported(const char *country); + +int halow_wifi_set_up(halow_t *self, int itf, bool up, const char *country); + +// Give up on an in-flight scan. morselib has no abort, so the scan keeps running +// in the transceiver; this only detaches our side of it. + + +// Copy the current cache out. Returns the number of entries written. +// Run one sweep and copy out what it found, up to max results. +size_t halow_wifi_scan_cached(halow_t *self, halow_ev_scan_result_t *out, size_t max); +int halow_wifi_join(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 halow_wifi_leave(halow_t *self, int itf); +int halow_wifi_link_status(halow_t *self, int itf); +int halow_wifi_get_mac(halow_t *self, int itf, uint8_t mac[6]); +int halow_wifi_get_bssid(halow_t *self, uint8_t bssid[6]); +int halow_wifi_get_rssi(halow_t *self, int32_t *rssi); +int halow_wifi_get_channel(halow_t *self, int itf, uint16_t *chan_num, uint8_t *bw_mhz); +int halow_wifi_pm(halow_t *self, uint32_t pm); +int halow_wifi_set_ps_timeout(halow_t *self, uint32_t ms); +int halow_wifi_get_ps_timeout(halow_t *self, uint32_t *ms); +int halow_wifi_get_pm(halow_t *self, uint32_t *pm); +int halow_wifi_wnm_sleep(halow_t *self, bool enable, bool powerdown); +int halow_wifi_get_version(halow_t *self, struct mmwlan_version *version); +int halow_wifi_set_radio(halow_t *self, int what, uint32_t value); +uint32_t halow_wifi_get_radio(halow_t *self, int what); +int halow_wifi_twt(halow_t *self, uint64_t interval_us, uint32_t duration_us, int setup); +int halow_wifi_get_rc_stats(halow_t *self, struct mmwlan_rc_stats **stats); +void halow_wifi_free_rc_stats(struct mmwlan_rc_stats *stats); +int halow_wifi_set_health_check(halow_t *self, uint32_t min_ms, uint32_t max_ms); +int halow_wifi_set_duty_cycle(halow_t *self, int mode); +int halow_wifi_get_duty_cycle(halow_t *self, struct mmwlan_duty_cycle_stats *stats); +int halow_wifi_ate_command(halow_t *self, uint8_t *cmd, size_t cmd_len, + uint8_t *rsp, size_t *rsp_len); +int halow_wifi_fixed_rate(halow_t *self, int mcs, int bw_mhz, int gi); + +void halow_wifi_ap_set_ssid(halow_t *self, size_t len, const uint8_t *buf); +void halow_wifi_ap_set_password(halow_t *self, size_t len, const uint8_t *buf); +void halow_wifi_ap_set_auth(halow_t *self, uint32_t auth); +void halow_wifi_ap_set_channel(halow_t *self, uint8_t chan_num); +void halow_wifi_ap_get_ssid(halow_t *self, size_t *len, const uint8_t **buf); +uint32_t halow_wifi_ap_get_auth(halow_t *self); +int halow_wifi_ap_get_stas(halow_t *self, int *num_stas, uint8_t *macs); + +/*******************************************************************************/ +// Datapath + +int halow_send_ethernet(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 halow_tcpip_link_status(halow_t *self, int itf); + +// lwIP glue, implemented in halow_lwip.c and called from halow_ctrl.c. +void halow_cb_tcpip_init(halow_t *self, int itf); +void halow_cb_tcpip_deinit(halow_t *self, int itf); +void halow_cb_tcpip_set_link_up(halow_t *self, int itf); +void halow_cb_tcpip_set_link_down(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 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 halow_hal.c. +void halow_hal_poll_irqs(void); + +// Re-enable the transceiver's pin interrupt after a poll has drained it. +void halow_hal_irq_rearm(void); + + +#endif // MICROPY_INCLUDED_DRIVERS_HALOW_HALOW_H diff --git a/drivers/halow/halow.mk b/drivers/halow/halow.mk new file mode 100644 index 00000000000..3cb42669f90 --- /dev/null +++ b/drivers/halow/halow.mk @@ -0,0 +1,74 @@ +# The prebuilt morselib and transceiver blobs; extmod.mk links no libraries. Needs BUILD set. + +HALOW_DIR ?= drivers/halow +HALOW_TOP ?= $(TOP) +HALOW_MMIOT_DIR = $(HALOW_TOP)/lib/mm-iot-sdk/framework +HALOW_MORSELIB_DIR = $(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. +HALOW_MORSELIB_CORE ?= arm-cortex-m33f +ifeq ($(HALOW_MORSELIB_SOURCE),1) +INC += $(addprefix -I$(HALOW_MORSELIB_DIR)/,src src/internal src/emmet src/umac/rc/mmrc_osal mmrc/src/core) +SRC_THIRDPARTY_C += $(patsubst $(HALOW_TOP)/%,%,\ + $(shell find $(HALOW_MORSELIB_DIR)/src $(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. +HALOW_LIBC = $(shell $(CC) $(CFLAGS) -print-file-name=libc.a) +HALOW_LIBM = $(shell $(CC) $(CFLAGS) -print-file-name=libm.a) +LIBS += -Wl,--start-group $(HALOW_MORSELIB_DIR)/lib/$(HALOW_MORSELIB_CORE)/libmorse.a $(HALOW_LIBC) $(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. +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. +HALOW_BFDNAME ?= elf32-littlearm +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 HALOW_BCF; the SDK keeps them per chip under morsefirmware. +HALOW_CHIP ?= mm8108 +HALOW_BCF ?= mf15457 +HALOW_FW_MBIN ?= $(HALOW_MMIOT_DIR)/morsefirmware/mm8108b2-rl.mbin +ifneq ($(HALOW_BCF),) +HALOW_BCF_MBIN ?= $(HALOW_MMIOT_DIR)/morsefirmware/$(HALOW_CHIP)/bcfs/bcf_$(HALOW_BCF).mbin +endif +HALOW_FW_OBJ = $(BUILD)/$(HALOW_DIR)/halow_firmware.o + +$(HALOW_FW_OBJ): $(HALOW_FW_MBIN) + $(ECHO) "GEN $@" + $(Q)$(MKDIR) -p $(dir $@) + $(Q)$(OBJCOPY) -I binary -O $(HALOW_BFDNAME) -B $(HALOW_BFDARCH) $< $@ \ + --redefine-sym $(call halow_blob_sym,$<,start)=halow_firmware_start \ + --redefine-sym $(call halow_blob_sym,$<,end)=halow_firmware_end \ + --rename-section .data=.rodata.halow_firmware,contents,alloc,load,readonly,data \ + --set-section-alignment .data=4 + +ifneq ($(HALOW_BCF_MBIN),) +CFLAGS += -DMICROPY_HW_HALOW_BCF=1 +HALOW_BCF_OBJ = $(BUILD)/$(HALOW_DIR)/halow_bcf.o + +$(HALOW_BCF_OBJ): $(HALOW_BCF_MBIN) + $(ECHO) "GEN $@" + $(Q)$(MKDIR) -p $(dir $@) + $(Q)$(OBJCOPY) -I binary -O $(HALOW_BFDNAME) -B $(HALOW_BFDARCH) $< $@ \ + --redefine-sym $(call halow_blob_sym,$<,start)=halow_bcf_start \ + --redefine-sym $(call halow_blob_sym,$<,end)=halow_bcf_end \ + --rename-section .data=.rodata.halow_bcf,contents,alloc,load,readonly,data \ + --set-section-alignment .data=4 +endif + +OBJ += $(HALOW_FW_OBJ) $(HALOW_BCF_OBJ) diff --git a/drivers/halow/halow_ctrl.c b/drivers/halow/halow_ctrl.c new file mode 100644 index 00000000000..55736fe122a --- /dev/null +++ b/drivers/halow/halow_ctrl.c @@ -0,0 +1,1169 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Control layer for the Morse Micro 802.11ah driver. + */ + +#include "py/mphal.h" + +#if MICROPY_PY_NETWORK_HALOW + +#include + +#include "py/mperrno.h" +#include "py/runtime.h" + +#include "lwip/prot/ethernet.h" + +#include "mmwlan.h" +#include "mmregdb.h" + +#include "halow.h" +#include "halow_osal.h" +#include "halow_sched.h" + +#ifndef HALOW_DEBUG +#define HALOW_DEBUG (0) +#endif + +#if HALOW_DEBUG +#define debug_printf(...) mp_printf(&mp_plat_print, __VA_ARGS__) +#else +#define debug_printf(...) +#endif + +// Privacy bit in the Capability Information field of a probe response. +#define HALOW_CAP_PRIVACY (1 << 4) + +// Default time the transceiver stays awake after activity, in ms. +#define HALOW_PS_TIMEOUT_DEFAULT_MS (100) + +halow_t halow_state = { + #if MICROPY_PY_NETWORK_HALOW_AP + .ap_auth = HALOW_SEC_SAE, + #endif + .ps_timeout_ms = 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 (*halow_poll)(void); + +static int halow_scan_start_locked(halow_t *self); +static int halow_apply_pm(halow_t *self); +static void halow_apply_radio(halow_t *self); +static bool 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 HALOW_TX_BUF_SIZE (MICROPY_HW_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 HALOW_TX_READY_MS (250) +static uint8_t *halow_tx_buf; + +MP_WEAK void 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 halow_status_to_errno(enum mmwlan_status status) { + switch (status) { + case MMWLAN_SUCCESS: + return 0; + case MMWLAN_INVALID_ARGUMENT: + return -MP_EINVAL; + case MMWLAN_UNAVAILABLE: + return -MP_EAGAIN; + case MMWLAN_NO_MEM: + return -MP_ENOMEM; + case MMWLAN_TIMED_OUT: + return -MP_ETIMEDOUT; + case MMWLAN_CHANNEL_LIST_NOT_SET: + return -MP_ENODEV; + case MMWLAN_CHANNEL_INVALID: + return -MP_ERANGE; + case MMWLAN_NOT_FOUND: + return -MP_ENOENT; + case MMWLAN_NOT_SUPPORTED: + return -MP_EOPNOTSUPP; + case MMWLAN_VIF_ERROR: + return -MP_ENXIO; + default: + return -MP_EIO; + } +} + +/*******************************************************************************/ +// morselib callbacks + +static void halow_sta_status_cb(enum mmwlan_sta_state sta_state) { + halow_t *self = &halow_state; + switch (sta_state) { + case MMWLAN_STA_CONNECTING: + self->link_status = HALOW_LINK_JOIN; + break; + case MMWLAN_STA_CONNECTED: + self->link_status = HALOW_LINK_NOIP; + break; + default: + self->link_status = HALOW_LINK_DOWN; + break; + } + debug_printf("halow: sta state %d\n", sta_state); +} + +static void halow_fatal_error_cb(struct mmwlan_fatal_error_args *args) { + 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 = HALOW_LINK_FAIL; + halow_cb_tcpip_set_link_down(self, HALOW_ITF_STA); + debug_printf("halow: fatal error at file %u line %u\n", + (unsigned int)args->fileid, (unsigned int)args->line); +} + +static void halow_link_state_cb(enum mmwlan_link_state link_state, void *arg) { + halow_t *self = arg; + if (link_state == MMWLAN_LINK_UP) { + halow_cb_tcpip_set_link_up(self, HALOW_ITF_STA); + } else { + halow_cb_tcpip_set_link_down(self, HALOW_ITF_STA); + } +} + +static void halow_rx_cb(uint8_t *header, unsigned header_len, + uint8_t *payload, unsigned payload_len, void *arg) { + halow_t *self = arg; + halow_cb_process_ethernet(self, HALOW_ITF_STA, header, header_len, payload, payload_len); +} + +/*******************************************************************************/ +// Init + +int halow_init(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 MICROPY_PY_NETWORK_HALOW_AP + self->ap_sta_count = 0; + #endif + self->link_status = 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 (!halow_osal_init()) { + return -MP_ENOMEM; + } + halow_tx_buf = halow_osal_malloc(HALOW_TX_BUF_SIZE); + if (halow_tx_buf == NULL) { + halow_osal_deinit(); + return -MP_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. + halow_apply_pm(self); + halow_apply_radio(self); + + mmwlan_register_fatal_error_handler(halow_fatal_error_cb, self); + mmwlan_register_link_state_cb(halow_link_state_cb, self); + mmwlan_register_rx_cb(halow_rx_cb, self); + + mmwlan_get_vif_mac_addr(MMWLAN_VIF_STA, self->mac); + + self->initted = true; + halow_poll = halow_poll_func; + return 0; +} + +void halow_deinit(halow_t *self) { + if (!self->initted) { + return; + } + if (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; + } + + halow_poll = NULL; + + for (int itf = 0; itf < HALOW_ITF_MAX; itf++) { + if (self->itf_state & (1 << itf)) { + halow_wifi_set_up(self, itf, false, self->country); + } + } + + mmwlan_register_rx_cb(NULL, NULL); + halow_sched_teardown = true; + mmwlan_shutdown(); + mmwlan_deinit(); + halow_sched_teardown = false; + self->booted = false; + + halow_sched_deinit(); + halow_osal_deinit(); + 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 = 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 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 HALOW_SCAN_MS (30000) + +void halow_poll_func(void) { + // A dispatch raised just before deinit still runs after it, by which point + // there is nothing left to service. + if (!halow_state.initted) { + return; + } + if (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 (!halow_sched_claim()) { + return; + } + halow_hal_poll_irqs(); + halow_osal_timer_poll(); + halow_sched_run(); + halow_sched_release(); + halow_hal_irq_rearm(); +} + +/*******************************************************************************/ +// Interface control + +bool halow_country_supported(const char *country) { + return mmwlan_lookup_regulatory_domain(get_regulatory_db(), country) != NULL; +} + +static int halow_set_country(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 -MP_EINVAL; + } + self->country[0] = country[0]; + self->country[1] = country[1]; + self->channels = channels; + return halow_status_to_errno(mmwlan_set_channel_list(channels)); +} + +#if MICROPY_PY_NETWORK_HALOW_AP +static void halow_ap_sta_status_cb(const struct mmwlan_ap_sta_status *status, void *arg) { + halow_t *self = (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 < 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 *halow_ap_channel(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 halow_ap_enable(halow_t *self) { + if (self->pm != HALOW_PM_NONE) { + return -MP_EPERM; + } + struct mmwlan_ap_args args = MMWLAN_AP_ARGS_INIT; + + if (self->ap_ssid_len == 0) { + return -MP_EINVAL; + } + + if (!(self->itf_state & (1 << 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 = halow_ap_channel(self); + if (ch == NULL) { + return -MP_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 = 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 == HALOW_SEC_SAE) { + if (self->ap_key_len == 0) { + return -MP_EINVAL; + } + memcpy(args.passphrase, self->ap_key, self->ap_key_len); + args.passphrase_len = self->ap_key_len; + } + if (self->ap_auth == 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 halow_status_to_errno(mmwlan_ap_enable(&args)); +} +#endif // MICROPY_PY_NETWORK_HALOW_AP + +int halow_wifi_set_up(halow_t *self, int itf, bool up, const char *country) { + if (itf < 0 || itf >= HALOW_ITF_MAX) { + return -MP_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 = 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 = 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 = 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 MICROPY_PY_NETWORK_HALOW_AP + if (itf == HALOW_ITF_AP) { + ret = halow_ap_enable(self); + if (ret != 0) { + return ret; + } + } + #endif + + if (!(self->itf_state & (1 << itf))) { + halow_cb_tcpip_init(self, itf); + self->itf_state |= 1 << itf; + } + #if MICROPY_PY_NETWORK_HALOW_AP + if (itf == HALOW_ITF_AP) { + halow_cb_tcpip_set_link_up(self, itf); + } + #endif + } else { + #if MICROPY_PY_NETWORK_HALOW_AP + if (itf == HALOW_ITF_AP) { + mmwlan_ap_disable(); + } else + #endif + { + mmwlan_sta_disable(); + self->link_status = HALOW_LINK_DOWN; + } + if (self->itf_state & (1 << itf)) { + halow_cb_tcpip_set_link_down(self, itf); + halow_cb_tcpip_deinit(self, itf); + self->itf_state &= ~(1 << itf); + } + if (self->itf_state == 0) { + halow_sched_teardown = true; + mmwlan_shutdown(); + 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 halow_scan_cache_add(halow_t *self, const 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 halow_wifi_scan_cached(halow_t *self, 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 != HALOW_LINK_DOWN) { + return 0; + } + + if (self->scan_cache == NULL) { + self->scan_cache = halow_osal_malloc( + HALOW_SCAN_CACHE_MAX * sizeof(*self->scan_cache)); + if (self->scan_cache == NULL) { + return 0; + } + self->scan_cache_max = HALOW_SCAN_CACHE_MAX; + } + + if (!self->scan_active) { + self->scan_cache_len = 0; + if (halow_scan_start_locked(self) != 0) { + return 0; + } + } + halow_sched_wait(halow_scan_idle_cond, self, HALOW_SCAN_MS); + + mp_uint_t atomic_state = MICROPY_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]; + } + MICROPY_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 halow_freq_to_chan(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 halow_scan_rx_cb(const struct mmwlan_scan_result *result, void *arg) { + halow_t *self = arg; + 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 = 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 & HALOW_CAP_PRIVACY) ? HALOW_SEC_SAE : HALOW_SEC_OPEN; + + 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 HALOW_SCAN_ABORT_MS (5000) + +static bool halow_scan_idle_cond(void *arg) { + return !((halow_t *)arg)->scan_active; +} + +static void halow_scan_complete_cb(enum mmwlan_scan_state scan_state, void *arg) { + halow_t *self = arg; + self->scan_active = false; + debug_printf("halow: sweep took %ums, %u results\n", + (unsigned int)(mp_hal_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 halow_scan_start_locked(halow_t *self) { + struct mmwlan_scan_req req = MMWLAN_SCAN_REQ_INIT; + req.scan_rx_cb = halow_scan_rx_cb; + req.scan_complete_cb = halow_scan_complete_cb; + req.scan_cb_arg = self; + req.args.dwell_time_ms = HALOW_SCAN_DWELL_MS; + + self->scan_active = true; + self->scan_started_ms = mp_hal_ticks_ms(); + + enum mmwlan_status status = mmwlan_scan_request(&req); + if (status != MMWLAN_SUCCESS) { + self->scan_active = false; + return halow_status_to_errno(status); + } + return 0; +} + +/*******************************************************************************/ +// Association + +int halow_wifi_join(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 -MP_EINVAL; + } + if (key_len > MMWLAN_PASSPHRASE_MAXLEN) { + return -MP_EINVAL; + } + if (auth_type == HALOW_SEC_SAE && key_len == 0) { + return -MP_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 == HALOW_SEC_OPEN) { + args.pmf_mode = MMWLAN_PMF_DISABLED; + } + if (bssid != NULL) { + memcpy(args.bssid, bssid, MMWLAN_MAC_ADDR_LEN); + } + + self->link_status = 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(); + halow_sched_wait(halow_scan_idle_cond, self, HALOW_SCAN_ABORT_MS); + } + + enum mmwlan_status status = mmwlan_sta_enable(&args, halow_sta_status_cb); + if (status != MMWLAN_SUCCESS) { + self->link_status = HALOW_LINK_FAIL; + return halow_status_to_errno(status); + } + return 0; +} + +int halow_wifi_leave(halow_t *self, int itf) { + if (itf == HALOW_ITF_AP) { + return halow_status_to_errno(mmwlan_ap_disable()); + } + self->link_status = HALOW_LINK_DOWN; + return halow_status_to_errno(mmwlan_sta_disable()); +} + +int halow_wifi_link_status(halow_t *self, int itf) { + if (itf == HALOW_ITF_AP) { + return (self->itf_state & (1 << HALOW_ITF_AP)) ? HALOW_LINK_UP : HALOW_LINK_DOWN; + } + return self->link_status; +} + +int halow_tcpip_link_status(halow_t *self, int itf) { + int status = halow_wifi_link_status(self, itf); + if (status != HALOW_LINK_NOIP && status != 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 HALOW_LINK_UP; + } + return HALOW_LINK_NOIP; +} + +/*******************************************************************************/ +// Queries + +int halow_wifi_get_mac(halow_t *self, int itf, uint8_t mac[6]) { + enum mmwlan_vif vif = (itf == 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 halow_wifi_get_bssid(halow_t *self, uint8_t bssid[6]) { + (void)self; + return halow_status_to_errno(mmwlan_get_bssid(bssid)); +} + +int halow_wifi_get_rssi(halow_t *self, int32_t *rssi) { + (void)self; + *rssi = mmwlan_get_rssi(); + return 0; +} + +int halow_wifi_get_channel(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 == 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 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 halow_apply_radio(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 halow_apply_pm(halow_t *self) { + enum mmwlan_status status = mmwlan_set_power_save_mode( + self->pm == HALOW_PM_NONE ? MMWLAN_PS_DISABLED : MMWLAN_PS_ENABLED); + if (status != MMWLAN_SUCCESS) { + return halow_status_to_errno(status); + } + status = mmwlan_set_dynamic_ps_timeout(self->ps_timeout_ms); + if (status != MMWLAN_SUCCESS) { + return halow_status_to_errno(status); + } + return 0; +} + +int halow_wifi_pm(halow_t *self, uint32_t pm) { + #if MICROPY_PY_NETWORK_HALOW_AP + if (pm != HALOW_PM_NONE && (self->itf_state & (1 << HALOW_ITF_AP))) { + // An access point has to be listening. + return -MP_EPERM; + } + #endif + uint32_t prev = self->pm; + self->pm = pm; + // Only reaches morselib once it has been initialised; halow_init() applies + // whatever was configured before that. + if (self->booted) { + int ret = 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 halow_wifi_ate_command(halow_t *self, uint8_t *cmd, size_t cmd_len, + uint8_t *rsp, size_t *rsp_len) { + if (!self->booted) { + return -MP_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 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 halow_wifi_fixed_rate(halow_t *self, int mcs, int bw_mhz, int gi) { + if (!self->booted) { + return -MP_ENODEV; + } + if (mcs < MMWLAN_MCS_NONE || mcs > MMWLAN_MCS_MAX || + gi < MMWLAN_GI_NONE || gi > MMWLAN_GI_MAX) { + return -MP_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 -MP_EINVAL; + } + return 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 halow_wifi_set_radio(halow_t *self, int what, uint32_t value) { + bool live = self->booted && self->link_status == HALOW_LINK_DOWN; + enum mmwlan_status status = MMWLAN_SUCCESS; + + switch (what) { + case HALOW_RADIO_AMPDU: + self->ampdu = value; + if (live) { + status = mmwlan_set_ampdu_enabled(value); + } + break; + case HALOW_RADIO_SGI: + self->sgi = value; + if (live) { + status = mmwlan_set_sgi_enabled(value); + } + break; + case HALOW_RADIO_SUBBANDS: + self->subbands = value; + if (live) { + status = mmwlan_set_subbands_enabled(value); + } + break; + case HALOW_RADIO_RTS: + self->rts_threshold = value; + if (self->booted) { + status = mmwlan_set_rts_threshold(value); + } + break; + case HALOW_RADIO_FRAG: + self->fragment_threshold = value; + if (self->booted) { + status = mmwlan_set_fragment_threshold(value); + } + break; + case 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 HALOW_RADIO_WNM_PD: + self->wnm_powerdown = value; + break; + case 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 -MP_EINVAL; + } + return halow_status_to_errno(status); +} + +uint32_t halow_wifi_get_radio(halow_t *self, int what) { + switch (what) { + case HALOW_RADIO_AMPDU: + return self->ampdu; + case HALOW_RADIO_SGI: + return self->sgi; + case HALOW_RADIO_SUBBANDS: + return self->subbands; + case HALOW_RADIO_RTS: + return self->rts_threshold; + case HALOW_RADIO_FRAG: + return self->fragment_threshold; + case HALOW_RADIO_WNM_PD: + return self->wnm_powerdown; + case 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 halow_wifi_set_duty_cycle(halow_t *self, int mode) { + self->duty_cycle_mode = mode; + if (!self->booted) { + return -MP_ENODEV; + } + if (mode != MMWLAN_DUTY_CYCLE_MODE_SPREAD && mode != MMWLAN_DUTY_CYCLE_MODE_BURST) { + return -MP_EINVAL; + } + return halow_status_to_errno(mmwlan_set_duty_cycle_mode(mode)); +} + +int halow_wifi_get_duty_cycle(halow_t *self, struct mmwlan_duty_cycle_stats *stats) { + if (!self->booted) { + return -MP_ENODEV; + } + return 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 halow_wifi_twt(halow_t *self, uint64_t interval_us, uint32_t duration_us, int setup) { + if (!self->booted) { + return -MP_ENODEV; + } + if (self->link_status != HALOW_LINK_DOWN) { + // The agreement is carried in the association request, so it has to be + // in place before the interface joins anything. + return -MP_EPERM; + } + if (setup < MMWLAN_TWT_SETUP_REQUEST || setup > MMWLAN_TWT_SETUP_DEMAND) { + return -MP_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 -MP_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 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 halow_wifi_get_rc_stats(halow_t *self, struct mmwlan_rc_stats **stats) { + if (!self->booted) { + return -MP_ENODEV; + } + *stats = mmwlan_get_rc_stats(); + return 0; +} + +void 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 halow_wifi_set_health_check(halow_t *self, uint32_t min_ms, uint32_t max_ms) { + if (max_ms != 0 && min_ms > max_ms) { + return -MP_EINVAL; + } + self->health_min_ms = min_ms; + self->health_max_ms = max_ms; + if (self->booted) { + return 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 halow_wifi_get_version(halow_t *self, struct mmwlan_version *version) { + if (!self->booted) { + return -MP_ENODEV; + } + return 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 halow_wifi_wnm_sleep(halow_t *self, bool enable, bool powerdown) { + if (enable && self->pm == HALOW_PM_NONE) { + // morselib only sleeps if 802.11 power save is on, and would otherwise + // report success while staying awake. + return -MP_EPERM; + } + if (enable && self->link_status != HALOW_LINK_UP && self->link_status != HALOW_LINK_NOIP) { + // Entering requires the AP to accept the request, so there has to be one. + return -MP_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 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 halow_wifi_set_ps_timeout(halow_t *self, uint32_t ms) { + if (ms == 0) { + return -MP_EINVAL; + } + if (self->booted) { + enum mmwlan_status status = mmwlan_set_dynamic_ps_timeout(ms); + if (status != MMWLAN_SUCCESS) { + return halow_status_to_errno(status); + } + } + self->ps_timeout_ms = ms; + return 0; +} + +int halow_wifi_get_ps_timeout(halow_t *self, uint32_t *ms) { + *ms = self->ps_timeout_ms; + return 0; +} + +int halow_wifi_get_pm(halow_t *self, uint32_t *pm) { + *pm = self->pm; + return 0; +} + +/*******************************************************************************/ +// AP configuration + +#if MICROPY_PY_NETWORK_HALOW_AP + +void halow_wifi_ap_set_ssid(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 halow_wifi_ap_set_password(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 halow_wifi_ap_set_auth(halow_t *self, uint32_t auth) { + self->ap_auth = auth; +} + +void halow_wifi_ap_get_ssid(halow_t *self, size_t *len, const uint8_t **buf) { + *len = self->ap_ssid_len; + *buf = self->ap_ssid; +} + +uint32_t halow_wifi_ap_get_auth(halow_t *self) { + return self->ap_auth; +} + +int halow_wifi_ap_get_stas(halow_t *self, int *num_stas, uint8_t *macs) { + // The list is compacted in place by 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. + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + int n = MIN(*num_stas, (int)self->ap_sta_count); + memcpy(macs, self->ap_stas, n * MMWLAN_MAC_ADDR_LEN); + MICROPY_END_ATOMIC_SECTION(atomic_state); + *num_stas = n; + return 0; +} + +void halow_wifi_ap_set_channel(halow_t *self, uint8_t chan_num) { + self->ap_chan_num = chan_num; +} + +#endif // MICROPY_PY_NETWORK_HALOW_AP + +/*******************************************************************************/ +// Datapath + +int halow_send_ethernet(halow_t *self, int itf, size_t len, const void *buf, bool is_pbuf) { + (void)itf; + + + if (len > HALOW_TX_BUF_SIZE || halow_tx_buf == NULL) { + return -MP_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 (halow_sched_task_current() == NULL) { + uint32_t start = mp_hal_ticks_ms(); + self->rx_deferred = true; + while (mmwlan_tx_wait_until_ready(0) != MMWLAN_SUCCESS) { + if ((uint32_t)(mp_hal_ticks_ms() - start) >= HALOW_TX_READY_MS) { + self->rx_deferred = false; + return -MP_EAGAIN; + } + 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 -MP_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, halow_tx_buf, len, 0); + data = halow_tx_buf; + } + } else { + data = buf; + } + + struct mmpkt *pkt = mmwlan_alloc_mmpkt_for_tx(len, MMWLAN_TX_DEFAULT_QOS_TID); + if (pkt == NULL) { + return -MP_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 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. + halow_schedule_poll(); + return 0; +} + +#endif // MICROPY_PY_NETWORK_HALOW diff --git a/drivers/halow/halow_hal.c b/drivers/halow/halow_hal.c new file mode 100644 index 00000000000..877bcf0cfb2 --- /dev/null +++ b/drivers/halow/halow_hal.c @@ -0,0 +1,390 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * MMHAL implementation for MicroPython: the SD-over-SPI transport to the Morse + * Micro transceiver, plus the firmware and board-configuration blobs. + */ + +#include "py/mphal.h" + +#if MICROPY_PY_NETWORK_HALOW + +#include + +#include "py/runtime.h" +#include "extmod/modmachine.h" + +#include "mmhal.h" +#include "mmhal_wlan.h" +#include "mmosal.h" +#include "halow.h" +#include "halow_osal.h" +#include "halow_sched.h" + +// 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 yields a *faster* bus (40MHz becomes 50MHz on a +// 200MHz AHB). Check the rate you got. +#ifndef MICROPY_HW_HALOW_SPI_BAUDRATE +#define MICROPY_HW_HALOW_SPI_BAUDRATE (12 * 1000 * 1000) +#endif + +// 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 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 HALOW_READ_CHUNK (128) + +// 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 MICROPY_HW_HALOW_BUSY_INVERTED +#define MICROPY_HW_HALOW_BUSY_INVERTED (0) +#endif + +static mmhal_irq_handler_t halow_spi_irq_handler; +static mmhal_irq_handler_t halow_busy_irq_handler; +static volatile bool halow_spi_irq_enabled; +static volatile bool 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 halow_spi_ones[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 halow_firmware_start; +extern uint8_t halow_firmware_end; +#ifdef MICROPY_HW_HALOW_BCF +extern uint8_t halow_bcf_start; +extern uint8_t halow_bcf_end; +#endif + +/*******************************************************************************/ +// Bus + +static void halow_spi_transfer(size_t len, const uint8_t *src, uint8_t *dest) { + mp_obj_t spi = MP_STATE_PORT(mp_halow_spi); + if (spi == MP_OBJ_NULL) { + return; + } + const mp_machine_spi_p_t *spi_proto = MP_OBJ_TYPE_GET_SLOT(&machine_spi_type, protocol); + spi_proto->transfer(spi, 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 (halow_sched_over_budget() && !halow_osal_in_critical()) { + halow_sched_yield(); + } + mp_hal_pin_write(MICROPY_HW_HALOW_CS, 0); +} + +void mmhal_wlan_spi_cs_deassert(void) { + mp_hal_pin_write(MICROPY_HW_HALOW_CS, 1); +} + +uint8_t mmhal_wlan_spi_rw(uint8_t data) { + uint8_t rx = 0xff; + 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, HALOW_READ_CHUNK); + halow_spi_transfer(chunk, halow_spi_ones, buf); + buf += chunk; + len -= chunk; + } +} + +void mmhal_wlan_spi_write_buf(const uint8_t *buf, unsigned len) { + halow_spi_transfer(len, buf, NULL); +} + +void mmhal_wlan_send_training_seq(void) { + mmhal_wlan_spi_cs_deassert(); + halow_spi_transfer(HALOW_TRAINING_BYTES, halow_spi_ones, NULL); +} + +/*******************************************************************************/ +// Control lines + +void mmhal_wlan_assert_reset(bool assert_reset) { + mp_hal_pin_write(MICROPY_HW_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) { + mp_hal_pin_write(MICROPY_HW_HALOW_WAKE, 1); +} + +void mmhal_wlan_wake_deassert(void) { + mp_hal_pin_write(MICROPY_HW_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 MICROPY_HW_HALOW_BUSY_INVERTED + return mp_hal_pin_read(MICROPY_HW_HALOW_BUSY) == 0; + #else + return mp_hal_pin_read(MICROPY_HW_HALOW_BUSY) != 0; + #endif +} + +void mmhal_wlan_register_busy_irq_handler(mmhal_irq_handler_t handler) { + halow_busy_irq_handler = handler; +} + +void mmhal_wlan_set_busy_irq_enabled(bool enabled) { + 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 mp_hal_pin_read(MICROPY_HW_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) { + halow_spi_irq_handler = handler; +} + +void mmhal_wlan_set_spi_irq_enabled(bool enabled) { + 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() && halow_spi_irq_handler != NULL) { + halow_spi_irq_handler(); + } +} + +#if MICROPY_PY_NETWORK_HALOW_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: mp_hal_pin_interrupt() is not available on every port, and +// halow_hal_poll_irqs() reads the line by level anyway, so a port without it +// just waits for the next poll. +static mp_obj_t halow_irq_callback(mp_obj_t arg) { + (void)arg; + // Coalesce: one poll drains everything the transceiver has, so further edges + // until then are pure overhead. Re-armed by halow_hal_irq_rearm(). + mp_hal_pin_interrupt_enable(MICROPY_HW_HALOW_IRQ, false); + halow_schedule_poll(); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(halow_irq_callback_obj, halow_irq_callback); + +static void halow_config_irq_pin(bool enabled) { + mp_hal_pin_interrupt(MICROPY_HW_HALOW_IRQ, + MP_OBJ_FROM_PTR(&halow_irq_callback_obj), + enabled ? MP_HAL_PIN_TRIGGER_FALL : MP_HAL_PIN_TRIGGER_NONE, + true); +} +#else +static void halow_config_irq_pin(bool enabled) { + (void)enabled; +} +#endif + +#if MICROPY_PY_NETWORK_HALOW_PIN_IRQ +void halow_hal_irq_rearm(void) { + mp_hal_pin_interrupt_enable(MICROPY_HW_HALOW_IRQ, true); +} +#else +void halow_hal_irq_rearm(void) { +} +#endif + +// Called from 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 halow_hal_poll_irqs(void) { + if (halow_spi_irq_enabled && halow_spi_irq_handler != NULL && mmhal_wlan_spi_irq_is_asserted()) { + halow_spi_irq_handler(); + } + if (halow_busy_irq_enabled && halow_busy_irq_handler != NULL && mmhal_wlan_busy_is_asserted()) { + halow_busy_irq_handler(); + } +} + +/*******************************************************************************/ +// Init + +void mmhal_wlan_init(void) { + memset(halow_spi_ones, 0xff, sizeof(halow_spi_ones)); + + mp_hal_pin_output(MICROPY_HW_HALOW_RESET); + mp_hal_pin_write(MICROPY_HW_HALOW_RESET, 0); + mp_hal_pin_output(MICROPY_HW_HALOW_WAKE); + mp_hal_pin_write(MICROPY_HW_HALOW_WAKE, 0); + mp_hal_pin_output(MICROPY_HW_HALOW_CS); + mp_hal_pin_write(MICROPY_HW_HALOW_CS, 1); + mp_hal_pin_input(MICROPY_HW_HALOW_BUSY); + mp_hal_pin_input(MICROPY_HW_HALOW_IRQ); + halow_config_irq_pin(true); + + // Mode 0, MSB first: the transceiver's SD-over-SPI interface samples on the + // rising edge with the clock idling low. + mp_obj_t args[] = { + MP_OBJ_NEW_SMALL_INT(MICROPY_HW_HALOW_SPI_ID), + MP_OBJ_NEW_SMALL_INT(MICROPY_HW_HALOW_SPI_BAUDRATE), + MP_OBJ_NEW_QSTR(MP_QSTR_polarity), MP_OBJ_NEW_SMALL_INT(0), + MP_OBJ_NEW_QSTR(MP_QSTR_phase), MP_OBJ_NEW_SMALL_INT(0), + }; + MP_STATE_PORT(mp_halow_spi) = + MP_OBJ_TYPE_GET_SLOT(&machine_spi_type, make_new)((mp_obj_t)&machine_spi_type, 2, 2, args); + + // Initialising the SPI peripheral may have reclaimed the CS pin. + mp_hal_pin_output(MICROPY_HW_HALOW_CS); + mp_hal_pin_write(MICROPY_HW_HALOW_CS, 1); + + mmhal_wlan_assert_reset(false); +} + +void mmhal_wlan_deinit(void) { + halow_config_irq_pin(false); + halow_spi_irq_enabled = false; + halow_busy_irq_enabled = false; + halow_spi_irq_handler = NULL; + halow_busy_irq_handler = NULL; + + mmhal_wlan_assert_reset(true); + mp_hal_pin_write(MICROPY_HW_HALOW_WAKE, 0); + MP_STATE_PORT(mp_halow_spi) = MP_OBJ_NULL; +} + +#if defined(MICROPY_HW_HALOW_EXT_XTAL_INIT) && MICROPY_HW_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 &MICROPY_HW_HALOW_CHIP; +} + +/*******************************************************************************/ +// Firmware and board configuration blobs + +static void 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) { + halow_read_blob(&halow_firmware_start, &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 MICROPY_HW_HALOW_BCF + halow_read_blob(&halow_bcf_start, &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; + } + } + + mp_hal_get_mac(MP_HAL_MAC_WLAN0, mac_addr); +} + +uint32_t mmhal_random_u32(uint32_t min, uint32_t max) { + uint32_t value; + #ifdef MICROPY_PY_RANDOM_SEED_INIT_FUNC + value = MICROPY_PY_RANDOM_SEED_INIT_FUNC; + #else + #error "halow requires a hardware random number generator" + #endif + 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 // MICROPY_PY_NETWORK_HALOW diff --git a/drivers/halow/halow_libc.c b/drivers/halow/halow_libc.c new file mode 100644 index 00000000000..adb8069c5cd --- /dev/null +++ b/drivers/halow/halow_libc.c @@ -0,0 +1,99 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * newlib back-end stubs for the prebuilt morselib. + * + * morselib is compiled against newlib and pulls in a few C library functions + * (sscanf, qsort, setjmp, ...). Those transitively reference newlib's syscall + * back-end, which the ports do not provide: MicroPython links -nostdlib and uses + * its own heap, so nothing else needs it. morselib never calls these at run + * time, so the stubs only exist to satisfy the link, and _sbrk deliberately + * fails rather than hand out any of MicroPython's memory. They are weak so that + * a port supplying real implementations takes precedence. + */ + +#include +#include +#include + +__attribute__((weak)) void *_sbrk(ptrdiff_t incr) { + (void)incr; + errno = ENOMEM; + return (void *)-1; +} + +__attribute__((weak)) int _write(int fd, const char *buf, int len) { + (void)fd; + (void)buf; + (void)len; + errno = ENOSYS; + return -1; +} + +__attribute__((weak)) int _read(int fd, char *buf, int len) { + (void)fd; + (void)buf; + (void)len; + errno = ENOSYS; + return -1; +} + +__attribute__((weak)) int _close(int fd) { + (void)fd; + errno = ENOSYS; + return -1; +} + +__attribute__((weak)) int _lseek(int fd, int ptr, int dir) { + (void)fd; + (void)ptr; + (void)dir; + errno = ENOSYS; + return -1; +} + +__attribute__((weak)) int _fstat(int fd, struct stat *st) { + (void)fd; + (void)st; + errno = ENOSYS; + return -1; +} + +__attribute__((weak)) int _isatty(int fd) { + (void)fd; + errno = ENOSYS; + return 0; +} + +__attribute__((weak)) int _getpid(void) { + return 1; +} + +__attribute__((weak)) int _kill(int pid, int sig) { + (void)pid; + (void)sig; + errno = ENOSYS; + return -1; +} diff --git a/drivers/halow/halow_lwip.c b/drivers/halow/halow_lwip.c new file mode 100644 index 00000000000..87e09ece022 --- /dev/null +++ b/drivers/halow/halow_lwip.c @@ -0,0 +1,272 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * lwIP interface for the Morse Micro 802.11ah driver. + */ + +#include "py/mperrno.h" +#include "py/mphal.h" + +#if MICROPY_PY_NETWORK_HALOW + +#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 "extmod/modnetwork.h" + +#include "halow.h" + +static err_t halow_netif_output(struct netif *netif, struct pbuf *p) { + halow_t *self = netif->state; + int itf = netif->name[1] - '0'; + + if (self->trace_flags & HALOW_TRACE_ETH_TX) { + mp_printf(&mp_plat_print, "halow: [txf] itf=%d len=%u\n", itf, (unsigned int)p->tot_len); + } + + int ret = halow_send_ethernet(self, itf, p->tot_len, p, true); + if (ret == -MP_EAGAIN) { + // The transmit queue is still full after the driver has been serviced + // for 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 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 halow_netif_init(struct netif *netif) { + halow_t *self = netif->state; + int itf = netif->name[1] - '0'; + + netif->linkoutput = halow_netif_output; + netif->output = etharp_output; + #if LWIP_IPV6 + netif->output_ip6 = ethip6_output; + #endif + netif->mtu = MICROPY_HW_HALOW_MTU; + netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET; + + 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, halow_netif_update_igmp_mac_filter); + #endif + + return ERR_OK; +} + +void halow_cb_tcpip_init(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 == HALOW_ITF_AP) { + ip_2_ip4(&ipconfig[0])->addr = PP_HTONL(MICROPY_HW_HALOW_AP_ADDRESS); + ip_2_ip4(&ipconfig[1])->addr = PP_HTONL(MICROPY_HW_HALOW_AP_NETMASK); + ip_2_ip4(&ipconfig[2])->addr = PP_HTONL(MICROPY_HW_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(MICROPY_HW_HALOW_STA_ADDRESS); + ip_2_ip4(&ipconfig[1])->addr = PP_HTONL(MICROPY_HW_HALOW_STA_NETMASK); + ip_2_ip4(&ipconfig[2])->addr = PP_HTONL(MICROPY_HW_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, halow_netif_init, ethernet_input); + #elif LWIP_IPV6 + netif_add(netif, self, halow_netif_init, ethernet_input); + #else + #error "halow needs either IPv4 or IPv6" + #endif + #if LWIP_NETIF_HOSTNAME + netif_set_hostname(netif, mod_network_hostname_data); + #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 == 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 MICROPY_PY_NETWORK_HALOW_AP && LWIP_IPV4 + dhcp_server_init(&self->dhcp_server, &ipconfig[0], &ipconfig[1]); + #endif + } +} + +void halow_cb_tcpip_deinit(halow_t *self, int itf) { + struct netif *netif = &self->netif[itf]; + bool was_default = netif_default == netif; + + if (itf == HALOW_ITF_STA) { + #if LWIP_IPV4 && LWIP_DHCP + dhcp_stop(netif); + #endif + } else { + #if MICROPY_PY_NETWORK_HALOW_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 halow_cb_tcpip_set_link_up(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 == HALOW_ITF_STA) { + dhcp_start(netif); + } + #endif +} + +void halow_cb_tcpip_set_link_down(halow_t *self, int itf) { + struct netif *netif = &self->netif[itf]; + if (!netif_is_link_up(netif)) { + return; + } + // Only the link goes down here, as in cyw43. 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 + // halow_cb_tcpip_deinit() instead. + netif_set_link_down(netif); +} + +void 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) { + 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 + // halow_send_ethernet(). Handing it a frame here would re-enter it + // while it is walking its own lists. + return; + } + + if (self->trace_flags & HALOW_TRACE_ETH_RX) { + mp_printf(&mp_plat_print, "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 // MICROPY_PY_NETWORK_HALOW diff --git a/drivers/halow/halow_osal.c b/drivers/halow/halow_osal.c new file mode 100644 index 00000000000..e91dc125e2a --- /dev/null +++ b/drivers/halow/halow_osal.c @@ -0,0 +1,800 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * MMOSAL implementation for MicroPython. + * + * This maps the RTOS abstraction layer that morselib is written against onto + * the cooperative scheduler in halow_sched.c. Allocation is served from a + * dedicated static heap rather than the MicroPython GC heap, because morselib + * allocates from PendSV context where the GC must not run. + */ + +// stdarg.h must come first: py/mpprint.h only declares mp_vprintf() if va_list +// is already available. +#include + +#include "py/mphal.h" + +#if MICROPY_PY_NETWORK_HALOW + +#include + +#include "py/mpprint.h" +#include "py/runtime.h" + +#include "mmosal.h" +#include "halow.h" +#include "halow_sched.h" +#include "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 MICROPY_HW_HALOW_HEAP_SIZE +#define MICROPY_HW_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 _halow_block_t { + struct _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 +} halow_block_t; + +#define HALOW_BLOCK_ALIGN (8) +#define HALOW_BLOCK_ROUND(n) (((n) + (HALOW_BLOCK_ALIGN - 1)) & ~(size_t)(HALOW_BLOCK_ALIGN - 1)) +#define HALOW_BLOCK_HDR (HALOW_BLOCK_ROUND(sizeof(halow_block_t))) +#define HALOW_BLOCK_DATA(b) ((void *)((uint8_t *)(b) + HALOW_BLOCK_HDR)) +#define HALOW_DATA_BLOCK(p) ((halow_block_t *)((uint8_t *)(p) - HALOW_BLOCK_HDR)) + +// The pool itself is a single block taken from the MicroPython 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 halow_block_t *halow_heap_head; + +bool halow_osal_init(void) { + if (MP_STATE_PORT(halow_heap) != NULL) { + return true; + } + uint8_t *heap = m_malloc_maybe(MICROPY_HW_HALOW_HEAP_SIZE); + if (heap == NULL) { + return false; + } + MP_STATE_PORT(halow_heap) = heap; + + halow_heap_head = (halow_block_t *)heap; + halow_heap_head->next = NULL; + halow_heap_head->size = MICROPY_HW_HALOW_HEAP_SIZE - HALOW_BLOCK_HDR; + halow_heap_head->used = false; + return true; +} + +void *halow_osal_malloc(size_t size) { + if (size == 0) { + return NULL; + } + // Also stops 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 > MICROPY_HW_HALOW_HEAP_SIZE) { + return NULL; + } + size = HALOW_BLOCK_ROUND(size); + + if (halow_heap_head == NULL) { + return NULL; + } + + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + void *ptr = NULL; + for (halow_block_t *b = 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 + HALOW_BLOCK_HDR + HALOW_BLOCK_ALIGN) { + halow_block_t *split = (halow_block_t *)((uint8_t *)b + HALOW_BLOCK_HDR + size); + split->next = b->next; + split->size = b->size - size - HALOW_BLOCK_HDR; + split->used = false; + b->next = split; + b->size = size; + } + b->used = true; + ptr = HALOW_BLOCK_DATA(b); + break; + } + MICROPY_END_ATOMIC_SECTION(atomic_state); + return ptr; +} + +void halow_osal_free(void *ptr) { + if (ptr == NULL || halow_heap_head == NULL) { + return; + } + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + 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 (halow_block_t *b = halow_heap_head; b != NULL && b->next != NULL;) { + if (!b->used && !b->next->used) { + b->size += HALOW_BLOCK_HDR + b->next->size; + b->next = b->next->next; + } else { + b = b->next; + } + } + MICROPY_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 *halow_timer_list; + +void 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 halow_init() otherwise. + halow_timer_list = NULL; + halow_heap_head = NULL; + if (MP_STATE_PORT(halow_heap) != NULL) { + m_free(MP_STATE_PORT(halow_heap)); + MP_STATE_PORT(halow_heap) = NULL; + } +} + +void *mmosal_malloc_(size_t size) { + return 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 = 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 halow_osal_malloc(size); + } + if (size == 0) { + halow_osal_free(ptr); + return NULL; + } + size_t old_size = HALOW_DATA_BLOCK(ptr)->size; + if (old_size >= size) { + return ptr; + } + void *new_ptr = halow_osal_malloc(size); + if (new_ptr != NULL) { + memcpy(new_ptr, ptr, old_size); + halow_osal_free(ptr); + } + return new_ptr; +} + +void mmosal_free(void *ptr) { + 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 halow_critical_nesting; +static mp_uint_t 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 *)halow_sched_task_create(task_fn, argument, stack_size_u32, name); +} + +void mmosal_task_delete(struct mmosal_task *task) { + halow_sched_task_delete((halow_task_t *)task); +} + +struct mmosal_task *mmosal_task_get_active(void) { + return (struct mmosal_task *)halow_sched_task_current(); +} + +void mmosal_task_yield(void) { + halow_sched_yield(); +} + +static bool halow_deadline_passed(void *arg) { + return (int32_t)(mp_hal_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. + halow_sched_yield(); + return; + } + uint32_t deadline = mp_hal_ticks_ms() + duration_ms; + halow_sched_wait(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) { + mp_uint_t state = MICROPY_BEGIN_ATOMIC_SECTION(); + if (halow_critical_nesting++ == 0) { + 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 halow_osal_in_critical(void) { + return halow_critical_nesting > 0; +} + +void mmosal_task_exit_critical(void) { + if (halow_critical_nesting > 0 && --halow_critical_nesting == 0) { + MICROPY_END_ATOMIC_SECTION(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) { + halow_task_t *task = halow_sched_task_current(); + return task != NULL ? task->name : "main"; +} + +static bool halow_task_is_dead(void *arg) { + return ((halow_task_t *)arg)->state == HALOW_TASK_DEAD; +} + +void mmosal_task_join(struct mmosal_task *task) { + halow_sched_wait(halow_task_is_dead, task, UINT32_MAX); +} + +static bool halow_task_notified(void *arg) { + halow_task_t *task = arg; + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + bool notified = task->notify != 0; + if (notified) { + task->notify--; + } + MICROPY_END_ATOMIC_SECTION(atomic_state); + return notified; +} + +bool mmosal_task_wait_for_notification(uint32_t timeout_ms) { + halow_task_t *task = halow_sched_task_current(); + if (task == NULL) { + // Only tasks can wait for notifications. + return false; + } + return halow_sched_wait(halow_task_notified, task, timeout_ms); +} + +void mmosal_task_notify(struct mmosal_task *task) { + if (task == NULL) { + return; + } + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + ((halow_task_t *)task)->notify++; + MICROPY_END_ATOMIC_SECTION(atomic_state); +} + +void mmosal_task_notify_from_isr(struct mmosal_task *task) { + mmosal_task_notify(task); +} + +/*******************************************************************************/ +// Mutexes + +struct mmosal_mutex { + volatile 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 = halow_osal_malloc(sizeof(*mutex)); + if (mutex != NULL) { + mutex->owner = NULL; + mutex->locked = false; + } + return mutex; +} + +void mmosal_mutex_delete(struct mmosal_mutex *mutex) { + halow_osal_free(mutex); +} + +static bool halow_mutex_acquired(void *arg) { + struct mmosal_mutex *mutex = arg; + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + bool acquired = !mutex->locked; + if (acquired) { + mutex->locked = true; + mutex->owner = halow_sched_task_current(); + mutex->taken_ms = mp_hal_ticks_ms(); + } + MICROPY_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 (halow_sched_wait(halow_mutex_acquired, mutex, 9000)) { + return true; + } + const halow_task_t *owner = (const halow_task_t *)mutex->owner; + mp_printf(&mp_plat_print, "halow: mutex %p slow (owner=%s state=%u held=%ums)\n", + mutex, owner != NULL ? owner->name : "thread", + owner != NULL ? (unsigned)owner->state : 0u, + (unsigned)(mp_hal_ticks_ms() - mutex->taken_ms)); + } + return halow_sched_wait(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 == 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 = 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) { + halow_osal_free(sem); +} + +bool mmosal_sem_give(struct mmosal_sem *sem) { + if (sem == NULL) { + return false; + } + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + bool given = sem->count < sem->max_count; + if (given) { + sem->count++; + } + MICROPY_END_ATOMIC_SECTION(atomic_state); + return given; +} + +bool mmosal_sem_give_from_isr(struct mmosal_sem *sem) { + return mmosal_sem_give(sem); +} + +static bool halow_sem_taken(void *arg) { + struct mmosal_sem *sem = arg; + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + bool taken = sem->count > 0; + if (taken) { + sem->count--; + } + MICROPY_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 halow_sched_wait(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 = halow_osal_malloc(sizeof(*semb)); + if (semb != NULL) { + semb->signalled = false; + } + return semb; +} + +void mmosal_semb_delete(struct mmosal_semb *semb) { + 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 halow_semb_taken(void *arg) { + struct mmosal_semb *semb = arg; + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + bool taken = semb->signalled; + semb->signalled = false; + MICROPY_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 halow_sched_wait(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 = 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) { + halow_osal_free(queue); +} + +bool mmosal_queue_pop_from_isr(struct mmosal_queue *queue, void *item) { + if (queue == NULL) { + return false; + } + mp_uint_t atomic_state = MICROPY_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--; + } + MICROPY_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; + } + mp_uint_t atomic_state = MICROPY_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++; + } + MICROPY_END_ATOMIC_SECTION(atomic_state); + return pushed; +} + +// Bundles a queue with the item being transferred, for the wait callbacks below. +typedef struct _halow_queue_op_t { + struct mmosal_queue *queue; + void *item; +} halow_queue_op_t; + +static bool halow_queue_popped(void *arg) { + halow_queue_op_t *op = arg; + return mmosal_queue_pop_from_isr(op->queue, op->item); +} + +static bool halow_queue_pushed(void *arg) { + 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) { + halow_queue_op_t op = { queue, item }; + return halow_sched_wait(halow_queue_popped, &op, timeout_ms); +} + +bool mmosal_queue_push(struct mmosal_queue *queue, const void *item, uint32_t timeout_ms) { + halow_queue_op_t op = { queue, (void *)item }; + return halow_sched_wait(halow_queue_pushed, &op, timeout_ms); +} + +/*******************************************************************************/ +// Time + +uint32_t mmosal_get_time_ms(void) { + return mp_hal_ticks_ms(); +} + +uint32_t mmosal_get_time_ticks(void) { + return mp_hal_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 = 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; + + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + timer->next = halow_timer_list; + halow_timer_list = timer; + MICROPY_END_ATOMIC_SECTION(atomic_state); + return timer; +} + +void mmosal_timer_delete(struct mmosal_timer *timer) { + if (timer == NULL) { + return; + } + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + for (struct mmosal_timer **t = &halow_timer_list; *t != NULL; t = &(*t)->next) { + if (*t == timer) { + *t = timer->next; + break; + } + } + MICROPY_END_ATOMIC_SECTION(atomic_state); + halow_osal_free(timer); +} + +bool mmosal_timer_start(struct mmosal_timer *timer) { + if (timer == NULL) { + return false; + } + timer->expires_at = mp_hal_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 = mp_hal_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 halow_osal_timer_poll(void) { + uint32_t now = mp_hal_ticks_ms(); + struct mmosal_timer *timer = 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 = mp_vprintf(&mp_plat_print, 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 halow_fatal_record halow_fatal_record; + +void mmosal_log_failure_info(const struct mmosal_failure_info *info) { + halow_fatal_record.pc = info->pc; + halow_fatal_record.lr = info->lr; + halow_fatal_record.fileid = info->fileid; + halow_fatal_record.line = info->line; + halow_fatal_record.ticks_ms = mp_hal_ticks_ms(); + halow_fatal_record.magic = HALOW_FATAL_MAGIC; + mp_printf(&mp_plat_print, "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) { + mp_printf(&mp_plat_print, "halow: assertion failed\n"); + #ifdef MICROPY_BOARD_FATAL_ERROR + MICROPY_BOARD_FATAL_ERROR("halow assertion failed"); + #endif + for (;;) { + } +} + +// morselib links against parts of libc that expect a hosted environment. These +// are only reachable from abort(), which cannot happen here: mmosal_impl_assert() +// handles failures first. +MP_WEAK int _kill(int pid, int sig) { + (void)pid; + (void)sig; + return -1; +} + +MP_WEAK int _getpid(void) { + return 0; +} + +int mmosal_main(mmosal_app_init_cb_t app_init_cb) { + // The MicroPython runtime owns main(); morselib is driven from halow_poll(). + (void)app_init_cb; + return -1; +} + +#endif // MICROPY_PY_NETWORK_HALOW diff --git a/drivers/halow/halow_osal.h b/drivers/halow/halow_osal.h new file mode 100644 index 00000000000..d31f71a571b --- /dev/null +++ b/drivers/halow/halow_osal.h @@ -0,0 +1,68 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * MMOSAL implementation for MicroPython. + */ +#ifndef MICROPY_INCLUDED_DRIVERS_HALOW_HALOW_OSAL_H +#define MICROPY_INCLUDED_DRIVERS_HALOW_HALOW_OSAL_H + +#include +#include + +// Take the driver's memory pool from the MicroPython heap. 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 halow_osal_init(void); + +// Allocate from the driver's private heap. morselib runs from PendSV, where the +// MicroPython GC heap must not be touched. +void *halow_osal_malloc(size_t size); +void halow_osal_free(void *ptr); + +// Run any morselib timers that have expired. Called from halow_poll(). +void halow_osal_timer_poll(void); + +// Release the memory pool back to the MicroPython heap. Only safe once +// morselib has been shut down and the scheduler torn down. +void 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 halow_osal_in_critical(void); + +#endif // MICROPY_INCLUDED_DRIVERS_HALOW_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 HALOW_FATAL_MAGIC (0x48464154) // "HFAT" +struct halow_fatal_record { + uint32_t magic; + uint32_t pc; + uint32_t lr; + uint32_t fileid; + uint32_t line; + uint32_t ticks_ms; +}; +extern struct halow_fatal_record halow_fatal_record; diff --git a/drivers/halow/halow_pktmem.c b/drivers/halow/halow_pktmem.c new file mode 100644 index 00000000000..3963a9f1021 --- /dev/null +++ b/drivers/halow/halow_pktmem.c @@ -0,0 +1,303 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * 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 "py/mphal.h" + +#if MICROPY_PY_NETWORK_HALOW + +#include +#include + +#include "mmhal_wlan.h" +#include "mmosal.h" +#include "mmpkt.h" +#include "mmpkt_list.h" + +#include "halow_osal.h" + +// Reserved buffers for commands to the transceiver. Commands are small and +// there are never many outstanding at once. +#define HALOW_TX_COMMAND_BLOCK_SIZE (352) +#define HALOW_TX_COMMAND_N_BLOCKS (2) + +// Reserved buffers for command responses, which arrive in full sized packets. +#define HALOW_RX_COMMAND_BLOCK_SIZE (MMHAL_WLAN_MMPKT_RX_MAX_SIZE) +#define 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 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 MICROPY_HW_HALOW_TX_BLOCKS +#define MICROPY_HW_HALOW_TX_BLOCKS (32) +#endif +#ifndef MICROPY_HW_HALOW_RX_BLOCKS +#define MICROPY_HW_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 HALOW_TX_PAUSE_THRESHOLD (MICROPY_HW_HALOW_TX_BLOCKS - 1) +#define HALOW_TX_UNPAUSE_THRESHOLD (MICROPY_HW_HALOW_TX_BLOCKS - 2) + +#if MICROPY_HW_HALOW_TX_BLOCKS < 3 || MICROPY_HW_HALOW_RX_BLOCKS < 2 +#error "halow: the packet pools are too small to flow control" +#endif + +typedef struct _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; +} halow_pktmem_t; + +static 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 *halow_pool_init(struct mmpkt_list *list, size_t block_size, size_t n_blocks) { + uint8_t *pool = 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 = halow_pool_init(&pktmem.tx_command_free_list, + HALOW_TX_COMMAND_BLOCK_SIZE, HALOW_TX_COMMAND_N_BLOCKS); + pktmem.rx_command_pool = halow_pool_init(&pktmem.rx_command_free_list, + HALOW_RX_COMMAND_BLOCK_SIZE, 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 == HALOW_TX_COMMAND_N_BLOCKS) && + (pktmem.rx_command_pool == NULL || + pktmem.rx_command_free_list.len == HALOW_RX_COMMAND_N_BLOCKS); + if (drained) { + break; + } + mmosal_task_sleep(10); + } + + if (drained) { + halow_osal_free(pktmem.tx_command_pool); + 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 *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 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 halow_tx_command_ops = { + .free_mmpkt = halow_tx_command_free, +}; + +static void 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 halow_rx_command_ops = { + .free_mmpkt = halow_rx_command_free, +}; + +/*******************************************************************************/ +// Data packets + +static void 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 < HALOW_TX_UNPAUSE_THRESHOLD) { + if (atomic_exchange(&pktmem.tx_data_paused, 0)) { + pktmem.tx_flow_control_cb(); + } + } +} + +static const struct mmpkt_ops halow_tx_data_ops = { + .free_mmpkt = halow_tx_data_free, +}; + +static void 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 halow_rx_data_ops = { + .free_mmpkt = 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 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 (!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 = halow_pool_alloc(&pktmem.tx_command_free_list, + HALOW_TX_COMMAND_BLOCK_SIZE, &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) >= MICROPY_HW_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 = &halow_tx_data_ops; + + if (pktmem.tx_data_allocated > 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 (!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 = halow_pool_alloc(&pktmem.rx_command_free_list, + HALOW_RX_COMMAND_BLOCK_SIZE, &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) >= MICROPY_HW_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 = &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 // MICROPY_PY_NETWORK_HALOW diff --git a/drivers/halow/halow_sched.c b/drivers/halow/halow_sched.c new file mode 100644 index 00000000000..2b8e77124cf --- /dev/null +++ b/drivers/halow/halow_sched.c @@ -0,0 +1,392 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Cooperative task scheduler for the Morse Micro WLAN stack. + */ + +#include "py/mphal.h" + +#if MICROPY_PY_NETWORK_HALOW + +#include + +#include "py/runtime.h" +#include "halow.h" +#include "halow_osal.h" +#include "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 MICROPY_HW_HALOW_MAX_TASKS +#define MICROPY_HW_HALOW_MAX_TASKS (4) +#endif + +// Size of the context saved by 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 HALOW_CONTEXT_WORDS (10 + 16) +#else +#define HALOW_CONTEXT_WORDS (10) +#endif + +static halow_task_t halow_tasks[MICROPY_HW_HALOW_MAX_TASKS]; + +// In creation order, which is the order they run in. +static halow_task_t *halow_task_list; + +// The task currently running, or NULL when running the scheduler itself. +static halow_task_t *halow_task_cur; + +// Wall clock one halow_sched_run() may spend. The scheduler is cooperative, so +// a task that stops yielding cannot be preempted -- only denied another turn. +#ifndef HALOW_SCHED_BUDGET_MS +#define HALOW_SCHED_BUDGET_MS (20) +#endif + +// Task turns per halow_sched_run() call. +// Ceiling on any single wait, however long the caller asked for. +#ifndef HALOW_WAIT_MAX_MS +#define HALOW_WAIT_MAX_MS (10000) +#endif + +#ifndef HALOW_SCHED_PASSES +#define HALOW_SCHED_PASSES (4) +#endif + +// Stack pointer of whoever called halow_sched_run(), saved while a task runs. +static void *halow_sched_sp; + +// Set while halow_sched_run() is walking the task list, to make it re-entrant. +static volatile bool 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)) 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. +static inline bool halow_in_irq(void) { + return (__get_IPSR() & IPSR_ISR_Msk) != 0; +} + +// Entry trampoline: runs the task main function and then retires the task. +static void halow_task_trampoline(void) { + halow_task_t *task = halow_task_cur; + task->entry(task->arg); + halow_sched_task_delete(NULL); +} + +halow_task_t *halow_sched_task_create(void (*entry)(void *), void *arg, size_t stack_words, const char *name) { + halow_task_t *task = NULL; + for (size_t i = 0; i < MICROPY_HW_HALOW_MAX_TASKS; i++) { + if (halow_tasks[i].stack == NULL) { + task = &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 < HALOW_CONTEXT_WORDS) { + stack_words = HALOW_CONTEXT_WORDS; + } + uint32_t *stack = 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 + // 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] = 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 = HALOW_TASK_READY; + + // Build a context that halow_context_switch() can restore, with the + // trampoline in the slot it pops into pc. + uint32_t *sp = (uint32_t *)task->stack + stack_words - HALOW_CONTEXT_WORDS; + memset(sp, 0, HALOW_CONTEXT_WORDS * sizeof(uint32_t)); + sp[HALOW_CONTEXT_WORDS - 1] = (uint32_t)halow_task_trampoline; + task->sp = sp; + + // Append to the task list, so that tasks run in creation order. + halow_task_t **tail = &halow_task_list; + while (*tail != NULL) { + tail = &(*tail)->next; + } + *tail = task; + + return task; +} + +size_t halow_sched_stack_free_words(const 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] == HALOW_STACK_FILL) { + free_words++; + } + return free_words; +} + +void halow_sched_task_delete(halow_task_t *task) { + if (task == NULL) { + task = 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 = HALOW_TASK_DEAD; + void *discard; + halow_context_switch(&discard, halow_sched_sp); + // Unreachable: a dead task is never resumed. + return; + } + task->state = HALOW_TASK_DEAD; +} + +halow_task_t *halow_sched_task_current(void) { + return halow_task_cur; +} + +// Held while the transceiver is being serviced, and by which context. See +// halow_sched_claim() in the header. +#define HALOW_OWNER_NONE (0) +#define HALOW_OWNER_THREAD (1) +#define HALOW_OWNER_IRQ (2) + +static volatile uint8_t halow_service_owner; +static volatile uint8_t halow_service_depth; + +// When the running pass has to be over. Only meaningful inside +// halow_sched_run(); nothing checks it otherwise. +static uint32_t halow_sched_deadline; + +// Whether the pass now running is out of time. Only meaningful inside +// halow_sched_run(): outside one the deadline is whatever the last pass left +// behind, which is always in the past. +static bool halow_pass_expired(void) { + return halow_sched_running && + (int32_t)(mp_hal_ticks_ms() - halow_sched_deadline) >= 0; +} + +bool 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 halow_task_cur != NULL && halow_pass_expired(); +} + +bool halow_sched_in_callback; + +bool halow_sched_claim(void) { + uint8_t owner = halow_in_irq() ? HALOW_OWNER_IRQ : HALOW_OWNER_THREAD; + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + bool claimed = halow_service_owner == HALOW_OWNER_NONE || halow_service_owner == owner; + if (claimed) { + halow_service_owner = owner; + halow_service_depth++; + } + MICROPY_END_ATOMIC_SECTION(atomic_state); + return claimed; +} + +void halow_sched_release(void) { + mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION(); + if (halow_service_depth > 0 && --halow_service_depth == 0) { + halow_service_owner = HALOW_OWNER_NONE; + } + MICROPY_END_ATOMIC_SECTION(atomic_state); +} + +void halow_sched_yield(void) { + halow_task_t *task = halow_task_cur; + if (task != NULL) { + halow_context_switch(&task->sp, 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 (halow_sched_claim()) { + halow_sched_run(); + halow_sched_release(); + } + if (!halow_in_irq()) { + // Not mp_event_wait_ms(): it raises, and a KeyboardInterrupt thrown out + // of morselib abandons its mutexes. Callbacks only, so a pending + // exception stays pending and the VM raises it somewhere safe. + halow_sched_in_callback = true; + mp_handle_pending(MP_HANDLE_PENDING_CALLBACKS_ONLY); + halow_sched_in_callback = false; + MICROPY_INTERNAL_WFE(1); + } + } +} + +// 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 halow_wait_depth; + +bool halow_sched_in_wait(void) { + return halow_wait_depth > 0; +} + +bool halow_sched_teardown; + +bool halow_sched_wait(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 > HALOW_WAIT_MAX_MS && (!forever || halow_sched_teardown)) { + timeout_ms = HALOW_WAIT_MAX_MS; + } + uint32_t start = mp_hal_ticks_ms(); + bool satisfied; + bool counted = halow_sched_task_current() == NULL; + if (counted) { + halow_wait_depth++; + } + for (;;) { + if (cond(arg)) { + satisfied = true; + break; + } + if (timeout_ms == 0) { + satisfied = false; + break; + } + if (forever) { + if (!halow_sched_teardown) { + halow_sched_yield(); + continue; + } + // Teardown began mid-wait: one bounded grace period from here. + timeout_ms = HALOW_WAIT_MAX_MS; + start = mp_hal_ticks_ms(); + forever = false; + } + if ((uint32_t)(mp_hal_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; + } + halow_sched_yield(); + } + if (counted) { + halow_wait_depth--; + } + return satisfied; +} + +void halow_sched_reap(void) { + halow_task_t **prev = &halow_task_list; + for (halow_task_t *task = halow_task_list; task != NULL;) { + halow_task_t *next = task->next; + if (task->state == HALOW_TASK_DEAD) { + *prev = next; + halow_osal_free(task->stack); + task->stack = NULL; + task->next = NULL; + } else { + prev = &task->next; + } + task = next; + } +} + +void halow_sched_run(void) { + if (halow_sched_running || halow_task_cur != NULL) { + // Already inside the scheduler, or called from a task. + return; + } + halow_sched_running = true; + halow_sched_deadline = mp_hal_ticks_ms() + 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 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 < HALOW_SCHED_PASSES && !halow_pass_expired(); pass++) { + for (halow_task_t *task = halow_task_list; task != NULL; task = task->next) { + if (task->state != HALOW_TASK_READY) { + continue; + } + halow_task_cur = task; + halow_context_switch(&halow_sched_sp, task->sp); + halow_task_cur = NULL; + } + } + + halow_sched_reap(); + halow_sched_running = false; +} + +void 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 halow_osal_deinit(). + halow_task_list = NULL; + halow_task_cur = NULL; + halow_sched_running = false; + halow_service_owner = HALOW_OWNER_NONE; + halow_service_depth = 0; + memset(halow_tasks, 0, sizeof(halow_tasks)); +} + +#endif // MICROPY_PY_NETWORK_HALOW diff --git a/drivers/halow/halow_sched.h b/drivers/halow/halow_sched.h new file mode 100644 index 00000000000..91dfdf464ec --- /dev/null +++ b/drivers/halow/halow_sched.h @@ -0,0 +1,127 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * 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 halow_poll(), which is dispatched via PendSV in + * exactly the same way as cyw43_poll(), so morselib code never runs concurrently + * with the MicroPython VM and lwIP needs no additional locking. + */ +#ifndef MICROPY_INCLUDED_DRIVERS_HALOW_HALOW_SCHED_H +#define MICROPY_INCLUDED_DRIVERS_HALOW_HALOW_SCHED_H + +#include +#include +#include + +// Task states. +#define HALOW_TASK_READY (0) // runnable, will be resumed by the scheduler +#define HALOW_TASK_DEAD (1) // returned or deleted, stack is pending free + +typedef struct _halow_task_t { + void *sp; // stack pointer, valid while not running + struct _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 +} halow_task_t; + +// Condition function used by halow_sched_wait(), returns true when the wait is over. +typedef bool (*halow_cond_fn_t)(void *arg); + +// Create a task. It starts out ready and first runs on the next halow_sched_run(). +halow_task_t *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 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 halow_sched_stack_free_words(const halow_task_t *task); + +// Mark a task as dead. Passing NULL kills the calling task, which does not return. +void halow_sched_task_delete(halow_task_t *task); + +// Return the task the caller is running on, or NULL if this is not task context. +halow_task_t *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 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 halow_sched_wait(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 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 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 halow_sched_teardown; + +bool halow_sched_claim(void); +void 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 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 halow_sched_over_budget(void); + +// Called from halow_sched_run(). +void halow_sched_reap(void); + +void halow_sched_deinit(void); + +#endif // MICROPY_INCLUDED_DRIVERS_HALOW_HALOW_SCHED_H diff --git a/drivers/halow/mmport.h b/drivers/halow/mmport.h new file mode 100644 index 00000000000..f70a4d8bb42 --- /dev/null +++ b/drivers/halow/mmport.h @@ -0,0 +1,36 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Compiler and architecture hooks required by morselib. + */ +#ifndef MICROPY_INCLUDED_DRIVERS_HALOW_MMPORT_H +#define MICROPY_INCLUDED_DRIVERS_HALOW_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 // MICROPY_INCLUDED_DRIVERS_HALOW_MMPORT_H diff --git a/drivers/halow/tests/host/.gitignore b/drivers/halow/tests/host/.gitignore new file mode 100644 index 00000000000..c52b3fd7482 --- /dev/null +++ b/drivers/halow/tests/host/.gitignore @@ -0,0 +1 @@ +test_alloc diff --git a/drivers/halow/tests/host/Makefile b/drivers/halow/tests/host/Makefile new file mode 100644 index 00000000000..aea029d90f2 --- /dev/null +++ b/drivers/halow/tests/host/Makefile @@ -0,0 +1,28 @@ +# Host tests for the HaLow allocator. Compiles the REAL drivers/halow/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) +CC ?= gcc +HALOW := ../.. +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)/halow_osal.c + $(CC) $(CFLAGS) -o $@ test_alloc.c + +run: test_alloc + ./test_alloc + +asan: test_alloc.c $(HALOW)/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/drivers/halow/tests/host/stub/lwip/dhcp.h b/drivers/halow/tests/host/stub/lwip/dhcp.h new file mode 100644 index 00000000000..7f311078620 --- /dev/null +++ b/drivers/halow/tests/host/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/drivers/halow/tests/host/stub/lwip/netif.h b/drivers/halow/tests/host/stub/lwip/netif.h new file mode 100644 index 00000000000..d677084ee5b --- /dev/null +++ b/drivers/halow/tests/host/stub/lwip/netif.h @@ -0,0 +1,14 @@ +// Host-test stub: 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/drivers/halow/tests/host/stub/py/mphal.h b/drivers/halow/tests/host/stub/py/mphal.h new file mode 100644 index 00000000000..6b207155e94 --- /dev/null +++ b/drivers/halow/tests/host/stub/py/mphal.h @@ -0,0 +1,43 @@ +// Host-test stub. Only the handful of MicroPython facilities halow_osal.c +// actually touches, so the REAL allocator source can be compiled and tested +// natively instead of copied into a test where it would silently drift. +#ifndef HALOW_HOSTTEST_MPHAL_H +#define HALOW_HOSTTEST_MPHAL_H + +#include +#include +#include + +typedef uintptr_t mp_uint_t; + +#define MP_WEAK + +// 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 halow_test_atomic_depth; +extern int halow_test_atomic_max; + +static inline mp_uint_t halow_test_begin_atomic(void) { + halow_test_atomic_depth++; + if (halow_test_atomic_depth > halow_test_atomic_max) { + halow_test_atomic_max = halow_test_atomic_depth; + } + return 0; +} + +static inline void halow_test_end_atomic(mp_uint_t state) { + (void)state; + halow_test_atomic_depth--; +} + +#define MICROPY_BEGIN_ATOMIC_SECTION() halow_test_begin_atomic() +#define MICROPY_END_ATOMIC_SECTION(st) halow_test_end_atomic(st) + +extern uint32_t halow_test_ticks_ms; +static inline mp_uint_t mp_hal_ticks_ms(void) { + return halow_test_ticks_ms; +} + +#endif diff --git a/drivers/halow/tests/host/stub/py/mpprint.h b/drivers/halow/tests/host/stub/py/mpprint.h new file mode 100644 index 00000000000..88bf842192e --- /dev/null +++ b/drivers/halow/tests/host/stub/py/mpprint.h @@ -0,0 +1,12 @@ +// Host-test stub: printing surface only. +#ifndef HALOW_HOSTTEST_MPPRINT_H +#define HALOW_HOSTTEST_MPPRINT_H +#include +#include +typedef struct _mp_print_t { void *data; + void *print_strn; +} mp_print_t; +extern const mp_print_t mp_plat_print; +int mp_printf(const mp_print_t *print, const char *fmt, ...); +int mp_vprintf(const mp_print_t *print, const char *fmt, va_list args); +#endif diff --git a/drivers/halow/tests/host/stub/py/runtime.h b/drivers/halow/tests/host/stub/py/runtime.h new file mode 100644 index 00000000000..a125681dfd0 --- /dev/null +++ b/drivers/halow/tests/host/stub/py/runtime.h @@ -0,0 +1,24 @@ +// Host-test stub: heap + root-pointer surface only. +// +// m_malloc_maybe is modelled faithfully: it RETURNS NULL on failure rather than +// raising, which is the whole reason the driver uses it. If the driver is ever +// changed to m_malloc (which raises), this stub will not compile -- that is +// deliberate, because a raise out of morselib's frames is unrecoverable. +#ifndef HALOW_HOSTTEST_RUNTIME_H +#define HALOW_HOSTTEST_RUNTIME_H +#include +#include "py/mphal.h" + +typedef struct _halow_test_state_t { + void *halow_heap; + void *mp_halow_spi; +} halow_test_state_t; +extern halow_test_state_t halow_test_state; +#define MP_STATE_PORT(x) (halow_test_state.x) + +// Lets a test force allocation failure and prove the driver degrades instead of +// crashing -- the on-device failure mode with no debugger attached. +extern int halow_test_malloc_fail; +void *m_malloc_maybe(size_t n); +void m_free(void *p); +#endif diff --git a/drivers/halow/tests/host/stub/shared/netutils/dhcpserver.h b/drivers/halow/tests/host/stub/shared/netutils/dhcpserver.h new file mode 100644 index 00000000000..8def83ccda8 --- /dev/null +++ b/drivers/halow/tests/host/stub/shared/netutils/dhcpserver.h @@ -0,0 +1,6 @@ +#ifndef HALOW_HOSTTEST_DHCPSERVER_H +#define HALOW_HOSTTEST_DHCPSERVER_H +#include +typedef struct _dhcp_server_t { uint8_t dummy; +} dhcp_server_t; +#endif diff --git a/drivers/halow/tests/host/test_alloc.c b/drivers/halow/tests/host/test_alloc.c new file mode 100644 index 00000000000..d87edd0d1fe --- /dev/null +++ b/drivers/halow/tests/host/test_alloc.c @@ -0,0 +1,475 @@ +/* + * 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 halow_osal.c against the stubs in stub/, and call + * halow_heap_verify() after every operation to check the block list invariants. + */ + +#include +#include +#include +#include +#include +#include + +// --- stub state the headers declare ------------------------------------------ +int halow_test_atomic_depth = 0; +int halow_test_atomic_max = 0; +uint32_t halow_test_ticks_ms = 0; +int halow_test_malloc_fail = 0; + +#include "py/mphal.h" +#include "py/mpprint.h" +#include "py/runtime.h" + +halow_test_state_t halow_test_state = { NULL, NULL }; +const mp_print_t mp_plat_print = { NULL, NULL }; + +int mp_printf(const mp_print_t *print, const char *fmt, ...) { + (void)print; + (void)fmt; + return 0; +} +int mp_vprintf(const mp_print_t *print, const char *fmt, va_list args) { + (void)print; + (void)fmt; + (void)args; + return 0; +} + +void *m_malloc_maybe(size_t n) { + if (halow_test_malloc_fail) { + return NULL; + } + return calloc(1, n); +} +void m_free(void *p) { + free(p); +} + +// The scheduler is ARM/PendSV and cannot build on the host. 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 _halow_task_t halow_task_t; +typedef bool (*halow_cond_fn_t)(void *arg); +halow_task_t *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 halow_sched_task_delete(halow_task_t *t) { + (void)t; +} +halow_task_t *halow_sched_task_current(void) { + return NULL; +} +void halow_sched_yield(void) { +} +bool halow_sched_wait(halow_cond_fn_t c, void *a, uint32_t t) { + (void)c; + (void)a; + (void)t; + return true; +} +void halow_sched_run(void) { +} +void halow_sched_reap(void) { +} +void halow_sched_deinit(void) { +} + +#define MICROPY_PY_NETWORK_HALOW (1) +#include "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 halow_heap_verify(const char *where) { + checks++; + size_t total = 0; + int blocks = 0; + const uint8_t *base = (const uint8_t *)MP_STATE_PORT(halow_heap); + const uint8_t *end = base + MICROPY_HW_HALOW_HEAP_SIZE; + + for (halow_block_t *b = 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 + 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 + 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)HALOW_BLOCK_DATA(b) % HALOW_BLOCK_ALIGN) != 0) { + failures++; + printf(" FAIL %s: payload %p not %d-aligned\n", where, + HALOW_BLOCK_DATA(b), HALOW_BLOCK_ALIGN); + return; + } + total += HALOW_BLOCK_HDR + b->size; + } + // Every byte accounted for: no leaked space between blocks. + if (total != MICROPY_HW_HALOW_HEAP_SIZE) { + failures++; + printf(" FAIL %s: blocks account for %zu of %d bytes\n", where, + total, MICROPY_HW_HALOW_HEAP_SIZE); + } +} + +static void setup(void) { + halow_test_malloc_fail = 0; + halow_test_atomic_depth = 0; + CHECK(halow_osal_init(), "init should succeed"); +} +static void teardown(void) { + halow_osal_deinit(); + CHECK(halow_test_atomic_depth == 0, "atomic sections unbalanced: depth %d", + halow_test_atomic_depth); +} + +// ----------------------------------------------------------------------------- +static void test_init_deinit(void) { + setup(); + CHECK(MP_STATE_PORT(halow_heap) != NULL, "heap pointer should be set"); + CHECK(halow_heap_head != NULL, "head should be set"); + halow_heap_verify("after init"); + teardown(); + CHECK(MP_STATE_PORT(halow_heap) == NULL, "heap pointer cleared on deinit"); + // The dangling-head hazard: a stale halow_heap_head after deinit would make + // the next malloc walk freed memory. active(False)/active(True) does this. + CHECK(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 = MP_STATE_PORT(halow_heap); + CHECK(halow_osal_init(), "second init should succeed"); + CHECK(MP_STATE_PORT(halow_heap) == first, "second init must not re-allocate"); + teardown(); +} + +static void test_init_handles_oom(void) { + halow_osal_deinit(); + halow_test_malloc_fail = 1; + CHECK(!halow_osal_init(), "init must report failure, not crash, when the GC " + "heap cannot give up 96 KB"); + CHECK(MP_STATE_PORT(halow_heap) == NULL, "no heap on failed init"); + CHECK(halow_osal_malloc(64) == NULL, "malloc must return NULL with no pool"); + halow_osal_free(NULL); + halow_test_malloc_fail = 0; +} + +static void test_basic_alloc_free(void) { + setup(); + void *a = halow_osal_malloc(100); + CHECK(a != NULL, "100-byte alloc should succeed"); + halow_heap_verify("after alloc"); + memset(a, 0xAA, 100); + halow_osal_free(a); + halow_heap_verify("after free"); + teardown(); +} + +static void test_zero_size(void) { + setup(); + CHECK(halow_osal_malloc(0) == NULL, "malloc(0) returns NULL"); + 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] = halow_osal_malloc(sizes[i]); + CHECK(p[i] != NULL, "alloc %zu failed", sizes[i]); + CHECK(((uintptr_t)p[i] % HALOW_BLOCK_ALIGN) == 0, + "alloc %zu returned %p, not %d-aligned", sizes[i], p[i], HALOW_BLOCK_ALIGN); + } + halow_heap_verify("after ragged allocs"); + for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) { + halow_osal_free(p[i]); + } + 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] = halow_osal_malloc(SZ); + CHECK(p[i] != NULL, "alloc %d failed", i); + memset(p[i], i + 1, SZ); // unique pattern per allocation + } + 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++) { + halow_osal_free(p[i]); + } + halow_heap_verify("after N frees"); + teardown(); +} + +static void test_coalesce_restores_full_heap(void) { + setup(); + size_t biggest_before = halow_heap_head->size; + enum { N = 32 }; + void *p[N]; + for (int i = 0; i < N; i++) { + p[i] = 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) { + halow_osal_free(p[i]); + } + halow_heap_verify("after even frees"); + for (int i = 1; i < N; i += 2) { + halow_osal_free(p[i]); + } + halow_heap_verify("after odd frees"); + CHECK(halow_heap_head->next == NULL, "heap should be one block again, got a list"); + CHECK(halow_heap_head->size == biggest_before, + "heap should be fully reclaimed: %zu vs %zu", 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 (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(halow_osal_malloc(4096) == NULL, "exhausted heap must return NULL"); + CHECK(halow_osal_malloc(1) == NULL || 1, "small alloc may still fit; not fatal"); + halow_heap_verify("at exhaustion"); + teardown(); +} + +static void test_too_big_returns_null(void) { + setup(); + CHECK(halow_osal_malloc(MICROPY_HW_HALOW_HEAP_SIZE * 2) == NULL, + "an allocation larger than the pool must return NULL"); + halow_heap_verify("after oversize request"); + teardown(); +} + +// Independent of calloc: any caller passing a size near SIZE_MAX corrupts the +// heap, because 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 = 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(halow_osal_malloc(SIZE_MAX) == NULL, "malloc(SIZE_MAX) must return NULL"); + 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); + halow_osal_free(p); + 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); + 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 = 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"); + 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] = 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; + } + } + halow_osal_free(p[slot]); + p[slot] = NULL; + frees++; + } + if ((i % 500) == 0) { + halow_heap_verify("during stress"); + } + } + checks++; + for (int i = 0; i < SLOTS; i++) { + if (p[i] != NULL) { + halow_osal_free(p[i]); + } + } + halow_heap_verify("after stress"); + CHECK(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", halow_test_atomic_max); + return failures != 0; +} diff --git a/drivers/halow/tests/qemu/.gitignore b/drivers/halow/tests/qemu/.gitignore new file mode 100644 index 00000000000..64457dc10bf --- /dev/null +++ b/drivers/halow/tests/qemu/.gitignore @@ -0,0 +1,3 @@ +test_sched.elf +test_sched.map +test_sched.bin diff --git a/drivers/halow/tests/qemu/Makefile b/drivers/halow/tests/qemu/Makefile new file mode 100644 index 00000000000..c897fd5e6bf --- /dev/null +++ b/drivers/halow/tests/qemu/Makefile @@ -0,0 +1,39 @@ +# QEMU tests for halow_sched.c on a Cortex-M55 (mps3-an547 == the N6's core). +# +# make build + run under QEMU +# +# Uses the OpenMV SDK toolchain, same as the firmware build: Ubuntu's +# arm-none-eabi-gcc 13.2 is rejected for Cortex-M55 by check_toolchain.mk. +SDK ?= $(HOME)/openmv-sdk-1.6.0 +CC := $(SDK)/gcc/bin/arm-none-eabi-gcc +HALOW := ../.. +MPY := ../../../.. +LDSCRIPT := $(MPY)/ports/qemu/mcu/arm/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 \ + -DMICROPY_PY_NETWORK_HALOW=1 \ + -Istub -I$(HALOW) -I$(MPY)/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 + +SRC := startup.c test_sched.c $(HALOW)/halow_sched.c + +all: run + +test_sched.elf: $(SRC) $(LDSCRIPT) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(SRC) + +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 test_sched.elf test_sched.map + +.PHONY: all run clean diff --git a/drivers/halow/tests/qemu/startup.c b/drivers/halow/tests/qemu/startup.c new file mode 100644 index 00000000000..1aa2a2908f1 --- /dev/null +++ b/drivers/halow/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/drivers/halow/tests/qemu/stub/halow.h b/drivers/halow/tests/qemu/stub/halow.h new file mode 100644 index 00000000000..98864f9b6cd --- /dev/null +++ b/drivers/halow/tests/qemu/stub/halow.h @@ -0,0 +1,6 @@ +#ifndef HALOW_QEMUTEST_HALOW_H +#define HALOW_QEMUTEST_HALOW_H +#include +#include +#include +#endif diff --git a/drivers/halow/tests/qemu/stub/lwip/dhcp.h b/drivers/halow/tests/qemu/stub/lwip/dhcp.h new file mode 100644 index 00000000000..7f311078620 --- /dev/null +++ b/drivers/halow/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/drivers/halow/tests/qemu/stub/lwip/netif.h b/drivers/halow/tests/qemu/stub/lwip/netif.h new file mode 100644 index 00000000000..d677084ee5b --- /dev/null +++ b/drivers/halow/tests/qemu/stub/lwip/netif.h @@ -0,0 +1,14 @@ +// Host-test stub: 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/drivers/halow/tests/qemu/stub/py/mphal.h b/drivers/halow/tests/qemu/stub/py/mphal.h new file mode 100644 index 00000000000..474d7c3705c --- /dev/null +++ b/drivers/halow/tests/qemu/stub/py/mphal.h @@ -0,0 +1,48 @@ +// QEMU-harness stub: the MicroPython surface halow_sched.c touches. +#ifndef HALOW_QEMUTEST_MPHAL_H +#define HALOW_QEMUTEST_MPHAL_H + +#include +#include +#include + +typedef uintptr_t mp_uint_t; +#define MP_WEAK + +#define MICROPY_BEGIN_ATOMIC_SECTION() (0) +#define MICROPY_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 halow_test_ticks; +static inline mp_uint_t mp_hal_ticks_us(void) { + extern volatile uint32_t halow_test_ticks; + return halow_test_ticks * 1000u; +} + +static inline mp_uint_t mp_hal_ticks_ms(void) { + return halow_test_ticks; +} + +// CMSIS bits halow_sched.c uses to detect exception context. Defined here so +// the harness does not need a CMSIS device header for a machine that is not the +// real target. +#define IPSR_ISR_Msk (0x1FFUL) +static inline uint32_t __get_IPSR(void) { + uint32_t result; + __asm volatile ("mrs %0, ipsr" : "=r" (result)); + return result; +} + +#endif + +// The scheduler sleeps here between turns. On the target this is a WFE with a +// timeout; the harness has no tick source of its own, so stand in for it by +// advancing the clock the test drives. +#define MICROPY_INTERNAL_WFE(TIMEOUT_MS) \ + do { \ + extern volatile uint32_t halow_test_ticks; \ + extern volatile uint32_t halow_test_event_waits; \ + halow_test_ticks += (TIMEOUT_MS); \ + halow_test_event_waits++; \ + } while (0) diff --git a/drivers/halow/tests/qemu/stub/py/runtime.h b/drivers/halow/tests/qemu/stub/py/runtime.h new file mode 100644 index 00000000000..6713ee05edc --- /dev/null +++ b/drivers/halow/tests/qemu/stub/py/runtime.h @@ -0,0 +1,19 @@ +#ifndef HALOW_QEMUTEST_RUNTIME_H +#define HALOW_QEMUTEST_RUNTIME_H +#include "py/mphal.h" +// The scheduler services pending callbacks and then sleeps when idling outside +// task context. The harness counts the calls instead, so the yield path is +// still exercised and observable. +typedef enum { + MP_HANDLE_PENDING_CALLBACKS_AND_CLEAR_EXCEPTIONS = 0, + MP_HANDLE_PENDING_CALLBACKS_AND_EXCEPTIONS = 1, + MP_HANDLE_PENDING_CALLBACKS_ONLY = 2, +} mp_handle_pending_behaviour_t; + +extern volatile uint32_t halow_test_event_waits; +static inline void mp_handle_pending(mp_handle_pending_behaviour_t behaviour) { + (void)behaviour; + halow_test_event_waits++; + halow_test_ticks++; +} +#endif diff --git a/drivers/halow/tests/qemu/stub/shared/netutils/dhcpserver.h b/drivers/halow/tests/qemu/stub/shared/netutils/dhcpserver.h new file mode 100644 index 00000000000..8def83ccda8 --- /dev/null +++ b/drivers/halow/tests/qemu/stub/shared/netutils/dhcpserver.h @@ -0,0 +1,6 @@ +#ifndef HALOW_HOSTTEST_DHCPSERVER_H +#define HALOW_HOSTTEST_DHCPSERVER_H +#include +typedef struct _dhcp_server_t { uint8_t dummy; +} dhcp_server_t; +#endif diff --git a/drivers/halow/tests/qemu/test_sched.c b/drivers/halow/tests/qemu/test_sched.c new file mode 100644 index 00000000000..8f0c2c312b8 --- /dev/null +++ b/drivers/halow/tests/qemu/test_sched.c @@ -0,0 +1,608 @@ +/* + * QEMU tests for the HaLow cooperative scheduler. + * + * 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 "py/mphal.h" +#include "py/runtime.h" +#include "halow_osal.h" +#include "halow_sched.h" + +void qemu_puts(const char *s); +void qemu_exit(int code); + +volatile uint32_t halow_test_ticks = 0; +volatile uint32_t 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 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 *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 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(); + halow_sched_deinit(); + ran_count = 0; + halow_task_t *t = halow_sched_task_create(task_runs_once, NULL, 256, "once"); + check(t != NULL, "task_create returned NULL"); + 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"); + 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" + ); + 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(); + halow_sched_deinit(); + reg_result = -1; + halow_task_t *t = halow_sched_task_create(task_core_regs, NULL, 256, "regs"); + check(t != NULL, "task_create returned NULL"); + halow_sched_run(); // runs up to the yield + 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 +// 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"); + 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(); + halow_sched_deinit(); + fpu_result = -1; + halow_task_t *t = halow_sched_task_create(task_fpu_regs, NULL, 512, "fpu"); + check(t != NULL, "task_create returned NULL"); + halow_sched_run(); + 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(); + halow_sched_deinit(); + order_idx = 0; + for (uintptr_t i = 1; i <= 3; i++) { + check(halow_sched_task_create(task_order_a, (void *)i, 256, "ord") != NULL, + "task_create returned NULL"); + } + 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(); + halow_sched_deinit(); + arg_seen = 0; + halow_sched_task_create(task_arg, (void *)0xDEADBEEF, 256, "arg"); + 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) { + halow_sched_deinit(); + cond_calls = 0; + check(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) { + halow_sched_deinit(); + cond_calls = 0; + halow_test_ticks = 0; + uint32_t before = halow_test_event_waits; + check(!halow_sched_wait(cond_false, NULL, 5), "wait should time out and return false"); + check(halow_test_event_waits > before, "a wait that times out should have slept, not spun"); +} + +static void test_wait_zero_timeout_polls_once(void) { + halow_sched_deinit(); + cond_calls = 0; + check(!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++; + halow_sched_run(); // must not recurse into this task again + reentrant_depth--; +} + +static void test_run_is_not_reentrant(void) { + pool_reset(); + halow_sched_deinit(); + reentrant_ran = 0; + reentrant_depth = 0; + halow_sched_task_create(task_reentrant, NULL, 512, "reent"); + 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 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 = halow_sched_stack_free_words(stack_task); +} + +static void test_stack_watermark(void) { + pool_reset(); + halow_sched_deinit(); + const size_t words = 256; + stack_free_seen = 0; + stack_task = 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 = 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"); + + 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 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(); + halow_sched_deinit(); + halow_sched_task_create(task_runs_once, NULL, 256, "d"); + halow_sched_deinit(); + check(halow_sched_task_current() == NULL, "current task not cleared by deinit"); + ran_count = 0; + 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(); + halow_sched_deinit(); + int created = 0; + for (int i = 0; i < 16; i++) { + if (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(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(); + halow_sched_deinit(); + check(halow_sched_claim(), "first claim was refused"); + halow_sched_release(); + check(halow_sched_claim(), "claim was not released"); + 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(); + halow_sched_deinit(); + check(halow_sched_claim(), "outer claim was refused"); + check(halow_sched_claim(), "a nested claim from the same context was refused"); + halow_sched_release(); + check(halow_sched_claim(), "the claim was dropped by the inner release"); + halow_sched_release(); + halow_sched_release(); + check(halow_sched_claim(), "the claim was not released by the outermost release"); + halow_sched_release(); +} + +static void test_deinit_releases_the_claim(void) { + pool_reset(); + check(halow_sched_claim(), "claim was refused before deinit"); + halow_sched_deinit(); + check(halow_sched_claim(), "deinit left the claim held"); + 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. + halow_test_ticks++; + if (halow_sched_over_budget()) { + halow_sched_yield(); + } + } +} + +static void test_runaway_task_cannot_starve_the_system(void) { + pool_reset(); + halow_sched_deinit(); + halow_test_ticks = 0; + spinner_laps = 0; + 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. + 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; + halow_sched_run(); + check(spinner_laps > laps_after_first_pass, "the runaway task was not resumed"); +} + +// The pass loop runs between task switches, where 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++; + halow_test_ticks += 100; // one turn, far past the budget + halow_sched_yield(); + } +} + +static void test_pass_loop_stops_when_out_of_time(void) { + pool_reset(); + halow_sched_deinit(); + halow_test_ticks = 0; + slow_task_turns = 0; + halow_sched_task_create(task_burns_a_whole_turn, NULL, 512, "slow"); + + 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 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(); + halow_sched_deinit(); + halow_test_ticks = 0; + cond_calls = 0; + forever_release_after = 60000; // six times the cap + check(halow_sched_wait(cond_false_until_released, NULL, 0xFFFFFFFFu), + "an infinite wait gave up instead of waiting"); + check(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(); + halow_sched_deinit(); + halow_test_ticks = 0; + cond_calls = 0; + halow_sched_teardown = true; + check(!halow_sched_wait(cond_false, NULL, 0xFFFFFFFFu), + "a teardown wait should give up"); + halow_sched_teardown = false; + check(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(); + halow_sched_deinit(); + halow_test_ticks = 0; + halow_sched_run(); + check(!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. + halow_test_ticks += 10000; + check(!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("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; +} diff --git a/tools/codeformat.py b/tools/codeformat.py index 588e0582136..0236dc21eac 100755 --- a/tools/codeformat.py +++ b/tools/codeformat.py @@ -34,6 +34,7 @@ # Relative to top-level repo dir. PATHS = [ + "drivers/halow/*.[ch]", "drivers/ninaw10/*.[ch]", "extmod/*.[ch]", "extmod/btstack/*.[ch]", From e9a327ecaffb0a56e23871f66ea661744bb3a617 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:28:47 -0700 Subject: [PATCH 06/15] extmod/network_halow: Add the network.HALOW 802.11ah binding. The MicroPython network interface on top of the drivers/halow core: a station-mode network.HALOW class with connect/disconnect, scan, config and status accessors, and an ioctl passthrough for certification tooling, plus the SEC_/TWT_/DUTY_CYCLE_/PM_/STAT_ constants. The constants are fixed to literals and static-asserted against the morselib enums, so a future SDK renumbering fails the build here rather than silently changing what a user's stored value means. Signed-off-by: Kwabena W. Agyeman --- extmod/extmod.mk | 49 +++ extmod/modnetwork.c | 8 + extmod/network_halow.c | 836 +++++++++++++++++++++++++++++++++++++++++ extmod/network_halow.h | 35 ++ 4 files changed, 928 insertions(+) create mode 100644 extmod/network_halow.c create mode 100644 extmod/network_halow.h diff --git a/extmod/extmod.mk b/extmod/extmod.mk index 961699f089e..4b2e823b94d 100644 --- a/extmod/extmod.mk +++ b/extmod/extmod.mk @@ -53,6 +53,7 @@ SRC_EXTMOD_C += \ extmod/modwebsocket.c \ extmod/network_cyw43.c \ extmod/network_esp_hosted.c \ + extmod/network_halow.c \ extmod/network_lwip.c \ extmod/network_ninaw10.c \ extmod/network_ppp_lwip.c \ @@ -468,6 +469,54 @@ SRC_THIRDPARTY_C += $(addprefix $(CYW43_DIR)/src/,\ $(BUILD)/$(CYW43_DIR)/src/cyw43_%.o: CFLAGS += -std=c11 endif # MICROPY_PY_NETWORK_CYW43 +################################################################################ +# halow (Morse Micro 802.11ah) + +ifeq ($(MICROPY_PY_NETWORK_HALOW),1) +HALOW_DIR = drivers/halow +MMIOT_DIR = lib/mm-iot-sdk/framework +GIT_SUBMODULES += lib/mm-iot-sdk + +CFLAGS += -DMICROPY_PY_NETWORK_HALOW=1 +CFLAGS_EXTMOD += -DMICROPY_PY_NETWORK_HALOW=1 + +ifeq ($(MICROPY_PY_NETWORK_HALOW_AP),1) +CFLAGS += -DMICROPY_PY_NETWORK_HALOW_AP=1 +CFLAGS_EXTMOD += -DMICROPY_PY_NETWORK_HALOW_AP=1 +endif + +INC += -I$(TOP)/$(HALOW_DIR) +INC += -I$(TOP)/$(MMIOT_DIR)/morselib/include +INC += -I$(TOP)/$(MMIOT_DIR)/src/mmutils +INC += -I$(TOP)/$(MMIOT_DIR)/src/mmregdb + +DRIVERS_SRC_C += $(addprefix $(HALOW_DIR)/,\ + halow_ctrl.c \ + halow_hal.c \ + halow_libc.c \ + halow_lwip.c \ + halow_osal.c \ + halow_pktmem.c \ + halow_sched.c \ + ) + +# Support code that morselib expects the integrator to provide: the packet +# memory pools, the regulatory database and assorted helpers. +# Packet memory is provided by halow_pktmem.c rather than the SDK's mmpktmem, +# so that all of the driver's memory comes from the MicroPython heap. +SRC_THIRDPARTY_C += $(addprefix $(MMIOT_DIR)/src/,\ + mmregdb/mmregdb.c \ + mmutils/mmbuf.c \ + mmutils/mmcrc.c \ + mmutils/mmutils_wlan.c \ + ) + +# The prebuilt morselib and the transceiver's firmware blobs are not listed here: +# this file is shared by every port, and none of that belongs in it. A board +# that wants the driver includes drivers/halow/halow.mk for the linkage. + +endif # MICROPY_PY_NETWORK_HALOW + ifneq ($(MICROPY_PY_NETWORK_WIZNET5K),) ifneq ($(MICROPY_PY_NETWORK_WIZNET5K),0) WIZNET5K_DIR=lib/wiznet5k diff --git a/extmod/modnetwork.c b/extmod/modnetwork.c index b6855fcaaab..c6e26444270 100644 --- a/extmod/modnetwork.c +++ b/extmod/modnetwork.c @@ -43,6 +43,10 @@ extern const struct _mp_obj_type_t mp_network_cyw43_type; #endif +#if MICROPY_PY_NETWORK_HALOW +extern const struct _mp_obj_type_t mp_network_halow_type; +#endif + #if MICROPY_PY_NETWORK_WIZNET5K extern const struct _mp_obj_type_t mod_network_nic_type_wiznet5k; #endif @@ -193,6 +197,10 @@ static const mp_rom_map_elem_t mp_module_network_globals_table[] = { #endif #endif + #if MICROPY_PY_NETWORK_HALOW + { MP_ROM_QSTR(MP_QSTR_HALOW), MP_ROM_PTR(&mp_network_halow_type) }, + #endif + #if MICROPY_PY_NETWORK_WIZNET5K { MP_ROM_QSTR(MP_QSTR_WIZNET5K), MP_ROM_PTR(&mod_network_nic_type_wiznet5k) }, #endif diff --git a/extmod/network_halow.c b/extmod/network_halow.c new file mode 100644 index 00000000000..372c54dcb89 --- /dev/null +++ b/extmod/network_halow.c @@ -0,0 +1,836 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include "py/runtime.h" +#include "py/objstr.h" +#include "py/mperrno.h" +#include "py/mphal.h" + +#if MICROPY_PY_NETWORK_HALOW + +#include "lwip/netif.h" +#include "extmod/network_halow.h" +#include "modnetwork.h" + +#include "drivers/halow/halow.h" +#include "drivers/halow/halow_sched.h" + +typedef struct _network_halow_obj_t { + mp_obj_base_t base; + halow_t *halow; + int itf; +} network_halow_obj_t; + +static const network_halow_obj_t network_halow_wl_sta = { { &mp_network_halow_type }, &halow_state, HALOW_ITF_STA }; +#if MICROPY_PY_NETWORK_HALOW_AP +static const network_halow_obj_t network_halow_wl_ap = { { &mp_network_halow_type }, &halow_state, HALOW_ITF_AP }; +#endif + +// Tracks the last up or down request made for each interface, to avoid races +// with the callbacks. +static bool if_active[2]; + +// if_active[] is a static that would otherwise survive into the next run. +void network_halow_deinit_all(void) { + halow_deinit(&halow_state); + if_active[0] = false; + if_active[1] = false; +} + +static void network_halow_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + network_halow_obj_t *self = MP_OBJ_TO_PTR(self_in); + struct netif *netif = &self->halow->netif[self->itf]; + int status = halow_tcpip_link_status(self->halow, self->itf); + const char *status_str; + if (status == HALOW_LINK_DOWN) { + status_str = "down"; + } else if (status == HALOW_LINK_JOIN || status == HALOW_LINK_NOIP) { + status_str = "join"; + } else if (status == HALOW_LINK_UP) { + status_str = "up"; + } else if (status == HALOW_LINK_NONET) { + status_str = "nonet"; + } else if (status == HALOW_LINK_BADAUTH) { + status_str = "badauth"; + } else { + status_str = "fail"; + } + ip4_addr_t *addr = ip_2_ip4(&netif->ip_addr); + mp_printf(print, "", + self->itf == HALOW_ITF_STA ? "STA" : "AP", + status_str, + addr->addr & 0xff, + addr->addr >> 8 & 0xff, + addr->addr >> 16 & 0xff, + addr->addr >> 24 + ); +} + +static mp_obj_t network_halow_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { + mp_arg_check_num(n_args, n_kw, 0, 1, false); + if (n_args == 0 || mp_obj_get_int(args[0]) == MOD_NETWORK_STA_IF) { + return MP_OBJ_FROM_PTR(&network_halow_wl_sta); + } + #if MICROPY_PY_NETWORK_HALOW_AP + return MP_OBJ_FROM_PTR(&network_halow_wl_ap); + #else + mp_raise_ValueError(MP_ERROR_TEXT("AP not supported")); + #endif +} + +static mp_obj_t network_halow_send_ethernet(mp_obj_t self_in, mp_obj_t buf_in) { + network_halow_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_buffer_info_t buf; + mp_get_buffer_raise(buf_in, &buf, MP_BUFFER_READ); + int ret = halow_send_ethernet(self->halow, self->itf, buf.len, buf.buf, false); + if (ret) { + mp_raise_OSError(-ret); + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_2(network_halow_send_ethernet_obj, network_halow_send_ethernet); + +// A vendor command passthrough, in the shape of cyw43's ioctl(): the buffer +// carries the command in and the response out. morselib's test interface has no +// command code of its own, so both ioctl(buf) and cyw43's ioctl(cmd, buf) are +// accepted, and in the second form the command is ignored. +static mp_obj_t network_halow_ioctl(size_t n_args, const mp_obj_t *args) { + network_halow_obj_t *self = MP_OBJ_TO_PTR(args[0]); + mp_buffer_info_t buf; + mp_get_buffer_raise(args[n_args - 1], &buf, MP_BUFFER_READ | MP_BUFFER_WRITE); + + // The command is copied out first: morselib may modify the buffer it is + // given, and the response is written back over the same one. + vstr_t vstr; + vstr_init_len(&vstr, buf.len); + memcpy(vstr.buf, buf.buf, buf.len); + + size_t rsp_len = buf.len; + int ret = halow_wifi_ate_command(self->halow, (uint8_t *)vstr.buf, buf.len, + buf.buf, &rsp_len); + vstr_clear(&vstr); + if (ret) { + mp_raise_OSError(-ret); + } + return MP_OBJ_NEW_SMALL_INT(rsp_len); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(network_halow_ioctl_obj, 2, 3, network_halow_ioctl); + +/*******************************************************************************/ +// network API + +static const char *get_country_code(void) { + return mod_network_country_code; +} + +static mp_obj_t network_halow_deinit(mp_obj_t self_in) { + if (halow_sched_in_wait()) { + // Reached from a scheduled callback that ran while the driver was + // waiting inside morselib. Giving the memory pool back here would free + // it out from under the frames that are still standing on it. + mp_raise_OSError(MP_EBUSY); + } + network_halow_obj_t *self = MP_OBJ_TO_PTR(self_in); + halow_deinit(self->halow); + if_active[HALOW_ITF_STA] = false; + if_active[HALOW_ITF_AP] = false; + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(network_halow_deinit_obj, network_halow_deinit); + +static mp_obj_t network_halow_active(size_t n_args, const mp_obj_t *args) { + network_halow_obj_t *self = MP_OBJ_TO_PTR(args[0]); + if (n_args == 1) { + return mp_obj_new_bool(if_active[self->itf]); + } else { + bool value = mp_obj_is_true(args[1]); + if (value && !halow_country_supported(get_country_code())) { + // 802.11ah channel plans are country specific and there is no + // worldwide default, so refuse to come up until one is chosen. + mp_raise_ValueError(MP_ERROR_TEXT("country not set")); + } + if (!value && self->itf == HALOW_ITF_STA) { + halow_wifi_leave(self->halow, self->itf); + } + int ret = halow_wifi_set_up(self->halow, self->itf, value, get_country_code()); + if (ret) { + mp_raise_OSError(-ret); + } + if_active[self->itf] = value; + return mp_const_none; + } +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(network_halow_active_obj, 1, 2, network_halow_active); + + +static mp_obj_t network_halow_scan(mp_obj_t self_in) { + network_halow_obj_t *self = MP_OBJ_TO_PTR(self_in); + + if (self->itf != HALOW_ITF_STA) { + mp_raise_ValueError(MP_ERROR_TEXT("STA required")); + } + + uint32_t pm; + halow_wifi_get_pm(self->halow, &pm); + if (pm != HALOW_PM_NONE) { + // Power saving is a transmit-only mode; the transceiver is not + // listening, so it cannot see probe responses. + mp_raise_OSError(MP_EPERM); + } + + // Off the heap rather than the stack: the sweep can report a good number of + // networks and each result is not small. + halow_ev_scan_result_t *results = + m_new(halow_ev_scan_result_t, HALOW_SCAN_CACHE_MAX); + size_t n = halow_wifi_scan_cached(self->halow, results, HALOW_SCAN_CACHE_MAX); + + mp_obj_t list = mp_obj_new_list(0, NULL); + for (size_t i = 0; i < n; i++) { + const halow_ev_scan_result_t *r = &results[i]; + mp_obj_t tuple[6] = { + mp_obj_new_bytes(r->ssid, r->ssid_len), + mp_obj_new_bytes(r->bssid, sizeof(r->bssid)), + // The S1G channel number, as config("channel") reports it. Zero + // for an access point outside the local regulatory plan. + MP_OBJ_NEW_SMALL_INT(r->chan_num), + MP_OBJ_NEW_SMALL_INT(r->rssi), + MP_OBJ_NEW_SMALL_INT(r->security), + // The transceiver does not report whether the network is hidden, + // and an empty SSID is the only sign of it in a beacon. + mp_obj_new_bool(r->ssid_len == 0), + }; + mp_obj_list_append(list, mp_obj_new_tuple(6, tuple)); + } + m_del(halow_ev_scan_result_t, results, HALOW_SCAN_CACHE_MAX); + return list; +} +static MP_DEFINE_CONST_FUN_OBJ_1(network_halow_scan_obj, network_halow_scan); + +static mp_obj_t network_halow_connect(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_ssid, ARG_key, ARG_security, ARG_bssid }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_ssid, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_key, MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_security, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + { MP_QSTR_bssid, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} }, + }; + + network_halow_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + mp_buffer_info_t ssid; + mp_get_buffer_raise(args[ARG_ssid].u_obj, &ssid, MP_BUFFER_READ); + + mp_buffer_info_t key; + key.buf = NULL; + key.len = 0; + if (args[ARG_key].u_obj != mp_const_none) { + mp_get_buffer_raise(args[ARG_key].u_obj, &key, MP_BUFFER_READ); + } + + mp_buffer_info_t bssid; + bssid.buf = NULL; + if (args[ARG_bssid].u_obj != mp_const_none) { + mp_get_buffer_raise(args[ARG_bssid].u_obj, &bssid, MP_BUFFER_READ); + if (bssid.len != 6) { + mp_raise_ValueError(NULL); + } + } + + uint32_t auth_type; + if (args[ARG_security].u_obj == mp_const_none) { + if (key.buf == NULL || key.len == 0) { + // Default to open when no password set. + auth_type = HALOW_SEC_OPEN; + } else { + // 802.11ah has no WPA2-PSK, so a passphrase always means SAE. + auth_type = HALOW_SEC_SAE; + } + } else { + auth_type = mp_obj_get_int(args[ARG_security].u_obj); + } + + int ret = halow_wifi_join(self->halow, ssid.len, ssid.buf, key.len, key.buf, + auth_type, bssid.buf); + if (ret != 0) { + mp_raise_OSError(-ret); + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(network_halow_connect_obj, 1, network_halow_connect); + +static mp_obj_t network_halow_disconnect(mp_obj_t self_in) { + network_halow_obj_t *self = MP_OBJ_TO_PTR(self_in); + halow_wifi_leave(self->halow, self->itf); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(network_halow_disconnect_obj, network_halow_disconnect); + +static mp_obj_t network_halow_isconnected(mp_obj_t self_in) { + network_halow_obj_t *self = MP_OBJ_TO_PTR(self_in); + bool result = (halow_tcpip_link_status(self->halow, self->itf) == HALOW_LINK_UP); + + #if MICROPY_PY_NETWORK_HALOW_AP + if (result && self->itf == HALOW_ITF_AP) { + // For AP we need to not only know if the link is up, but also if any stations + // have associated. + uint8_t mac_buf[6]; + int num_stas = 1; + halow_wifi_ap_get_stas(self->halow, &num_stas, mac_buf); + result = num_stas > 0; + } + #endif + return mp_obj_new_bool(result); +} +static MP_DEFINE_CONST_FUN_OBJ_1(network_halow_isconnected_obj, network_halow_isconnected); + +static mp_obj_t network_halow_ifconfig(size_t n_args, const mp_obj_t *args) { + network_halow_obj_t *self = MP_OBJ_TO_PTR(args[0]); + return mod_network_nic_ifconfig(&self->halow->netif[self->itf], n_args - 1, args + 1); +} +MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(network_halow_ifconfig_obj, 1, 2, network_halow_ifconfig); + +static mp_obj_t network_halow_ipconfig(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) { + network_halow_obj_t *self = MP_OBJ_TO_PTR(args[0]); + return mod_network_nic_ipconfig(&self->halow->netif[self->itf], n_args - 1, args + 1, kwargs); +} +static MP_DEFINE_CONST_FUN_OBJ_KW(network_halow_ipconfig_obj, 1, network_halow_ipconfig); + +static mp_obj_t network_halow_status(size_t n_args, const mp_obj_t *args) { + network_halow_obj_t *self = MP_OBJ_TO_PTR(args[0]); + + if (n_args == 1) { + // no arguments: return link status + return MP_OBJ_NEW_SMALL_INT(halow_tcpip_link_status(self->halow, self->itf)); + } + + // one argument: return status based on query parameter + switch (mp_obj_str_get_qstr(args[1])) { + case MP_QSTR_rssi: { + if (self->itf != HALOW_ITF_STA) { + mp_raise_ValueError(MP_ERROR_TEXT("STA required")); + } + int32_t rssi; + halow_wifi_get_rssi(self->halow, &rssi); + return mp_obj_new_int(rssi); + } + case MP_QSTR_rates: { + // (mcs, bandwidth_mhz, gi, sent, success) per rate table entry that + // has been used, so the list stays about the link rather than the + // whole table. + // Room for the copy is taken first: the vendor's block is not + // garbage collected, so an allocation that raises after asking for + // it would strand it for good. + uint32_t *info = m_new(uint32_t, HALOW_RC_STATS_MAX * 3); + + struct mmwlan_rc_stats *st; + int ret = halow_wifi_get_rc_stats(self->halow, &st); + if (ret) { + m_del(uint32_t, info, HALOW_RC_STATS_MAX * 3); + mp_raise_OSError(-ret); + } + if (st == NULL) { + m_del(uint32_t, info, HALOW_RC_STATS_MAX * 3); + return mp_obj_new_list(0, NULL); + } + uint32_t n = MIN(st->n_entries, (uint32_t)HALOW_RC_STATS_MAX); + for (uint32_t i = 0; i < n; i++) { + info[i * 3] = st->rate_info[i]; + info[i * 3 + 1] = st->total_sent[i]; + info[i * 3 + 2] = st->total_success[i]; + } + halow_wifi_free_rc_stats(st); + + mp_obj_t list = mp_obj_new_list(0, NULL); + for (uint32_t i = 0; i < n; i++) { + if (info[i * 3 + 1] == 0) { + continue; + } + uint32_t rate = info[i * 3]; + // The bandwidth field is an index, not a width: 0 is 1MHz. + uint32_t bw = (rate >> HALOW_RC_BW_SHIFT) & HALOW_RC_FIELD_MASK; + mp_obj_t entry[5] = { + MP_OBJ_NEW_SMALL_INT((rate >> HALOW_RC_RATE_SHIFT) & HALOW_RC_FIELD_MASK), + MP_OBJ_NEW_SMALL_INT(1 << bw), + MP_OBJ_NEW_SMALL_INT((rate >> HALOW_RC_GI_SHIFT) & HALOW_RC_GI_MASK), + mp_obj_new_int_from_uint(info[i * 3 + 1]), + mp_obj_new_int_from_uint(info[i * 3 + 2]), + }; + mp_obj_list_append(list, mp_obj_new_tuple(5, entry)); + } + m_del(uint32_t, info, HALOW_RC_STATS_MAX * 3); + return list; + } + case MP_QSTR_duty_cycle: { + struct mmwlan_duty_cycle_stats st; + int ret = halow_wifi_get_duty_cycle(self->halow, &st); + if (ret) { + mp_raise_OSError(-ret); + } + mp_obj_t items[4] = { + mp_obj_new_int_from_uint(st.duty_cycle), + MP_OBJ_NEW_SMALL_INT(st.mode), + mp_obj_new_int_from_uint(st.burst_airtime_remaining_us), + mp_obj_new_int_from_uint(st.burst_window_duration_us), + }; + return mp_obj_new_tuple(4, items); + } + case MP_QSTR_bssid: { + if (self->itf != HALOW_ITF_STA) { + mp_raise_ValueError(MP_ERROR_TEXT("STA required")); + } + uint8_t bssid[6]; + int ret = halow_wifi_get_bssid(self->halow, bssid); + if (ret) { + mp_raise_OSError(-ret); + } + return mp_obj_new_bytes(bssid, sizeof(bssid)); + } + #if MICROPY_PY_NETWORK_HALOW_AP + case MP_QSTR_stations: { + // return list of connected stations + if (self->itf != HALOW_ITF_AP) { + mp_raise_ValueError(MP_ERROR_TEXT("AP required")); + } + static const unsigned mac_len = 6; + static const unsigned max_stas = 32; + int num_stas = max_stas; + uint8_t macs[max_stas * mac_len]; + halow_wifi_ap_get_stas(self->halow, &num_stas, macs); + mp_obj_t list = mp_obj_new_list(num_stas, NULL); + for (int i = 0; i < num_stas; ++i) { + mp_obj_t tuple[1] = { + mp_obj_new_bytes(&macs[i * mac_len], mac_len), + }; + ((mp_obj_list_t *)MP_OBJ_TO_PTR(list))->items[i] = mp_obj_new_tuple(1, tuple); + } + return list; + } + #endif + } + + mp_raise_ValueError(MP_ERROR_TEXT("unknown status param")); +} +static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(network_halow_status_obj, 1, 2, network_halow_status); + +// Settings that reach a narrow field. Without this a negative or oversized +// value wraps: txpower=-5 became 65531, which the transceiver reads as "no +// limit" -- the opposite of what was asked for. +static uint32_t halow_arg_range(mp_obj_t obj, uint32_t max) { + mp_int_t value = mp_obj_get_int(obj); + if (value < 0 || (uint32_t)value > max) { + mp_raise_ValueError(MP_ERROR_TEXT("value out of range")); + } + return (uint32_t)value; +} + +static void halow_set_radio(network_halow_obj_t *self, int what, uint32_t value) { + int ret = halow_wifi_set_radio(self->halow, what, value); + if (ret != 0) { + mp_raise_OSError(-ret); + } +} + +static mp_obj_t network_halow_config(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) { + network_halow_obj_t *self = MP_OBJ_TO_PTR(args[0]); + + if (kwargs->used == 0) { + // Get config value + if (n_args != 2) { + mp_raise_TypeError(MP_ERROR_TEXT("can query only one param")); + } + + switch (mp_obj_str_get_qstr(args[1])) { + case MP_QSTR_channel: { + // The S1G channel number, as used by 802.11ah operating classes. + uint16_t chan_num; + uint8_t bw_mhz; + int ret = halow_wifi_get_channel(self->halow, self->itf, &chan_num, &bw_mhz); + if (ret) { + mp_raise_OSError(-ret); + } + return MP_OBJ_NEW_SMALL_INT(chan_num); + } + case MP_QSTR_bandwidth: { + // Width of the primary channel in MHz. A frequency on its own + // does not identify an S1G channel; the width is needed too. + uint16_t chan_num; + uint8_t bw_mhz; + int ret = halow_wifi_get_channel(self->halow, self->itf, &chan_num, &bw_mhz); + if (ret) { + mp_raise_OSError(-ret); + } + return MP_OBJ_NEW_SMALL_INT(bw_mhz); + } + case MP_QSTR_ssid: + case MP_QSTR_essid: { + #if MICROPY_PY_NETWORK_HALOW_AP + if (self->itf == HALOW_ITF_AP) { + size_t len; + const uint8_t *buf; + halow_wifi_ap_get_ssid(self->halow, &len, &buf); + return mp_obj_new_str((const char *)buf, len); + } + #endif + return mp_obj_new_str((const char *)self->halow->sta_ssid, self->halow->sta_ssid_len); + } + #if MICROPY_PY_NETWORK_HALOW_AP + case MP_QSTR_security: { + return MP_OBJ_NEW_SMALL_INT(halow_wifi_ap_get_auth(self->halow)); + } + #endif + case MP_QSTR_version: { + struct mmwlan_version v; + int ret = halow_wifi_get_version(self->halow, &v); + if (ret) { + mp_raise_OSError(-ret); + } + mp_obj_t items[3] = { + mp_obj_new_str_from_cstr(v.morselib_version), + mp_obj_new_str_from_cstr(v.morse_fw_version), + mp_obj_new_str_from_cstr(v.morse_chip_id_string), + }; + return mp_obj_new_tuple(3, items); + } + case MP_QSTR_ampdu: + return mp_obj_new_bool(halow_wifi_get_radio(self->halow, HALOW_RADIO_AMPDU)); + case MP_QSTR_sgi: + return mp_obj_new_bool(halow_wifi_get_radio(self->halow, HALOW_RADIO_SGI)); + case MP_QSTR_subbands: + return mp_obj_new_bool(halow_wifi_get_radio(self->halow, HALOW_RADIO_SUBBANDS)); + case MP_QSTR_rts_threshold: + return mp_obj_new_int_from_uint(halow_wifi_get_radio(self->halow, HALOW_RADIO_RTS)); + case MP_QSTR_fragment_threshold: + return mp_obj_new_int_from_uint(halow_wifi_get_radio(self->halow, HALOW_RADIO_FRAG)); + case MP_QSTR_wnm_powerdown: + return mp_obj_new_bool(halow_wifi_get_radio(self->halow, HALOW_RADIO_WNM_PD)); + case MP_QSTR_listen_interval: + return MP_OBJ_NEW_SMALL_INT(halow_wifi_get_radio(self->halow, HALOW_RADIO_LISTEN)); + case MP_QSTR_txpower: + return MP_OBJ_NEW_SMALL_INT(halow_wifi_get_radio(self->halow, HALOW_RADIO_TXPOWER)); + case MP_QSTR_duty_cycle: + return MP_OBJ_NEW_SMALL_INT(self->halow->duty_cycle_mode); + case MP_QSTR_mac: { + uint8_t buf[6]; + halow_wifi_get_mac(self->halow, self->itf, buf); + return mp_obj_new_bytes(buf, 6); + } + case MP_QSTR_pm: { + uint32_t pm; + halow_wifi_get_pm(self->halow, &pm); + return MP_OBJ_NEW_SMALL_INT(pm); + } + case MP_QSTR_ps_timeout: { + uint32_t ms; + halow_wifi_get_ps_timeout(self->halow, &ms); + return mp_obj_new_int_from_uint(ms); + } + case MP_QSTR_hostname: { + // TODO: Deprecated. Use network.hostname() instead. + return mod_network_hostname(0, NULL); + } + default: + mp_raise_ValueError(MP_ERROR_TEXT("unknown config param")); + } + } else { + // Set config value(s) + if (n_args != 1) { + mp_raise_TypeError(MP_ERROR_TEXT("can't specify pos and kw args")); + } + + // A number of these options only update buffers in memory, and + // won't do anything until the interface is cycled down and back up + bool cycle_active = false; + + for (size_t i = 0; i < kwargs->alloc; ++i) { + if (MP_MAP_SLOT_IS_FILLED(kwargs, i)) { + mp_map_elem_t *e = &kwargs->table[i]; + switch (mp_obj_str_get_qstr(e->key)) { + #if MICROPY_PY_NETWORK_HALOW_AP + case MP_QSTR_ssid: + case MP_QSTR_essid: + case MP_QSTR_security: + case MP_QSTR_key: + case MP_QSTR_password: + // These describe the network this interface hands out, + // so they only mean anything on the access point. On + // the station they would write the AP's settings and + // then cycle the station, dropping its association. + if (self->itf != HALOW_ITF_AP) { + mp_raise_ValueError(MP_ERROR_TEXT("AP required")); + } + break; + #endif + default: + break; + } + + switch (mp_obj_str_get_qstr(e->key)) { + #if MICROPY_PY_NETWORK_HALOW_AP + case MP_QSTR_ssid: + case MP_QSTR_essid: { + size_t len; + const char *str = mp_obj_str_get_data(e->value, &len); + halow_wifi_ap_set_ssid(self->halow, len, (const uint8_t *)str); + cycle_active = true; + break; + } + case MP_QSTR_security: { + halow_wifi_ap_set_auth(self->halow, mp_obj_get_int(e->value)); + cycle_active = true; + break; + } + case MP_QSTR_channel: { + if (self->itf != HALOW_ITF_AP) { + mp_raise_ValueError(MP_ERROR_TEXT("AP required")); + } + halow_wifi_ap_set_channel(self->halow, mp_obj_get_int(e->value)); + cycle_active = true; + break; + } + case MP_QSTR_key: + case MP_QSTR_password: { + size_t len; + const char *str = mp_obj_str_get_data(e->value, &len); + halow_wifi_ap_set_password(self->halow, len, (const uint8_t *)str); + cycle_active = true; + break; + } + #endif + case MP_QSTR_ampdu: { + halow_set_radio(self, HALOW_RADIO_AMPDU, mp_obj_is_true(e->value)); + break; + } + case MP_QSTR_sgi: { + halow_set_radio(self, HALOW_RADIO_SGI, mp_obj_is_true(e->value)); + break; + } + case MP_QSTR_subbands: { + halow_set_radio(self, HALOW_RADIO_SUBBANDS, mp_obj_is_true(e->value)); + break; + } + case MP_QSTR_rts_threshold: { + halow_set_radio(self, HALOW_RADIO_RTS, halow_arg_range(e->value, UINT16_MAX)); + break; + } + case MP_QSTR_fragment_threshold: { + halow_set_radio(self, HALOW_RADIO_FRAG, halow_arg_range(e->value, UINT16_MAX)); + break; + } + case MP_QSTR_listen_interval: { + halow_set_radio(self, HALOW_RADIO_LISTEN, + halow_arg_range(e->value, UINT16_MAX)); + break; + } + case MP_QSTR_wnm_sleep: { + int ret = halow_wifi_wnm_sleep(self->halow, + mp_obj_is_true(e->value), + halow_wifi_get_radio(self->halow, HALOW_RADIO_WNM_PD)); + if (ret != 0) { + mp_raise_OSError(-ret); + } + break; + } + case MP_QSTR_wnm_powerdown: { + halow_set_radio(self, HALOW_RADIO_WNM_PD, mp_obj_is_true(e->value)); + break; + } + case MP_QSTR_twt: { + // (interval_us, duration_us[, setup]) + size_t n; + mp_obj_t *items; + mp_obj_get_array(e->value, &n, &items); + if (n < 2 || n > 3) { + mp_raise_ValueError(MP_ERROR_TEXT("bad twt")); + } + int ret = halow_wifi_twt(self->halow, + mp_obj_get_ll(items[0]), + halow_arg_range(items[1], UINT32_MAX), + n == 3 ? mp_obj_get_int(items[2]) : HALOW_TWT_REQUEST); + if (ret != 0) { + mp_raise_OSError(-ret); + } + break; + } + case MP_QSTR_fixed_rate: { + // (mcs, bandwidth, gi), -1 for automatic + size_t n; + mp_obj_t *items; + mp_obj_get_array(e->value, &n, &items); + if (n != 3) { + mp_raise_ValueError(MP_ERROR_TEXT("bad fixed_rate")); + } + int ret = halow_wifi_fixed_rate(self->halow, + mp_obj_get_int(items[0]), mp_obj_get_int(items[1]), + mp_obj_get_int(items[2])); + if (ret != 0) { + mp_raise_OSError(-ret); + } + break; + } + case MP_QSTR_health_check: { + // (min_ms, max_ms) + size_t n; + mp_obj_t *items; + mp_obj_get_array(e->value, &n, &items); + if (n != 2) { + mp_raise_ValueError(MP_ERROR_TEXT("bad health_check")); + } + int ret = halow_wifi_set_health_check(self->halow, + mp_obj_get_int(items[0]), mp_obj_get_int(items[1])); + if (ret != 0) { + mp_raise_OSError(-ret); + } + break; + } + case MP_QSTR_duty_cycle: { + int ret = halow_wifi_set_duty_cycle(self->halow, + mp_obj_get_int(e->value)); + if (ret != 0) { + mp_raise_OSError(-ret); + } + break; + } + case MP_QSTR_pm: { + int ret = halow_wifi_pm(self->halow, mp_obj_get_int(e->value)); + if (ret != 0) { + mp_raise_OSError(-ret); + } + break; + } + case MP_QSTR_ps_timeout: { + int ret = halow_wifi_set_ps_timeout(self->halow, mp_obj_get_int(e->value)); + if (ret != 0) { + mp_raise_OSError(-ret); + } + break; + } + case MP_QSTR_trace: { + self->halow->trace_flags = mp_obj_get_int(e->value); + break; + } + case MP_QSTR_txpower: + halow_set_radio(self, HALOW_RADIO_TXPOWER, + halow_arg_range(e->value, UINT16_MAX)); + break; + case MP_QSTR_hostname: { + // TODO: Deprecated. Use network.hostname(name) instead. + mod_network_hostname(1, &e->value); + break; + } + default: + mp_raise_ValueError(MP_ERROR_TEXT("unknown config param")); + } + } + } + + // If the interface is already active, cycle it down and up + if (cycle_active && if_active[self->itf]) { + const char *country = get_country_code(); + halow_wifi_set_up(self->halow, self->itf, false, country); + halow_wifi_set_up(self->halow, self->itf, true, country); + } + + return mp_const_none; + } +} +static MP_DEFINE_CONST_FUN_OBJ_KW(network_halow_config_obj, 1, network_halow_config); + +/*******************************************************************************/ +// class bindings + +// The public SEC_/TWT_/DUTY_CYCLE_ constants are fixed to literals in halow.h so +// that a morselib SDK update renumbering its enums cannot silently change what a +// user's stored value means. These assert the literals still match the SDK, so +// such a divergence fails the build here instead of shipping. +MP_STATIC_ASSERT(HALOW_SEC_OPEN == MMWLAN_OPEN); +MP_STATIC_ASSERT(HALOW_SEC_OWE == MMWLAN_OWE); +MP_STATIC_ASSERT(HALOW_SEC_SAE == MMWLAN_SAE); +MP_STATIC_ASSERT(HALOW_TWT_REQUEST == MMWLAN_TWT_SETUP_REQUEST); +MP_STATIC_ASSERT(HALOW_TWT_SUGGEST == MMWLAN_TWT_SETUP_SUGGEST); +MP_STATIC_ASSERT(HALOW_TWT_DEMAND == MMWLAN_TWT_SETUP_DEMAND); +MP_STATIC_ASSERT(HALOW_DUTY_CYCLE_SPREAD == MMWLAN_DUTY_CYCLE_MODE_SPREAD); +MP_STATIC_ASSERT(HALOW_DUTY_CYCLE_BURST == MMWLAN_DUTY_CYCLE_MODE_BURST); + +static const mp_rom_map_elem_t network_halow_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_send_ethernet), MP_ROM_PTR(&network_halow_send_ethernet_obj) }, + { MP_ROM_QSTR(MP_QSTR_ioctl), MP_ROM_PTR(&network_halow_ioctl_obj) }, + + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&network_halow_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR_active), MP_ROM_PTR(&network_halow_active_obj) }, + { MP_ROM_QSTR(MP_QSTR_scan), MP_ROM_PTR(&network_halow_scan_obj) }, + { MP_ROM_QSTR(MP_QSTR_connect), MP_ROM_PTR(&network_halow_connect_obj) }, + { MP_ROM_QSTR(MP_QSTR_disconnect), MP_ROM_PTR(&network_halow_disconnect_obj) }, + { MP_ROM_QSTR(MP_QSTR_isconnected), MP_ROM_PTR(&network_halow_isconnected_obj) }, + { MP_ROM_QSTR(MP_QSTR_ifconfig), MP_ROM_PTR(&network_halow_ifconfig_obj) }, + { MP_ROM_QSTR(MP_QSTR_ipconfig), MP_ROM_PTR(&network_halow_ipconfig_obj) }, + { MP_ROM_QSTR(MP_QSTR_status), MP_ROM_PTR(&network_halow_status_obj) }, + { MP_ROM_QSTR(MP_QSTR_config), MP_ROM_PTR(&network_halow_config_obj) }, + + // Class constants. + { MP_ROM_QSTR(MP_QSTR_IF_STA), MP_ROM_INT(MOD_NETWORK_STA_IF) }, + #if MICROPY_PY_NETWORK_HALOW_AP + { MP_ROM_QSTR(MP_QSTR_IF_AP), MP_ROM_INT(MOD_NETWORK_AP_IF) }, + #endif + { MP_ROM_QSTR(MP_QSTR_SEC_OPEN), MP_ROM_INT(HALOW_SEC_OPEN) }, + { MP_ROM_QSTR(MP_QSTR_SEC_OWE), MP_ROM_INT(HALOW_SEC_OWE) }, + { MP_ROM_QSTR(MP_QSTR_SEC_WPA3), MP_ROM_INT(HALOW_SEC_SAE) }, + { MP_ROM_QSTR(MP_QSTR_SEC_SAE), MP_ROM_INT(HALOW_SEC_SAE) }, + + { MP_ROM_QSTR(MP_QSTR_STAT_IDLE), MP_ROM_INT(HALOW_LINK_DOWN) }, + { MP_ROM_QSTR(MP_QSTR_STAT_CONNECTING), MP_ROM_INT(HALOW_LINK_JOIN) }, + { MP_ROM_QSTR(MP_QSTR_STAT_NOIP), MP_ROM_INT(HALOW_LINK_NOIP) }, + { MP_ROM_QSTR(MP_QSTR_STAT_GOT_IP), MP_ROM_INT(HALOW_LINK_UP) }, + { MP_ROM_QSTR(MP_QSTR_STAT_CONNECT_FAIL), MP_ROM_INT(HALOW_LINK_FAIL) }, + + { MP_ROM_QSTR(MP_QSTR_PM_NONE), MP_ROM_INT(HALOW_PM_NONE) }, + { MP_ROM_QSTR(MP_QSTR_PM_POWERSAVE), MP_ROM_INT(HALOW_PM_POWERSAVE) }, + + { MP_ROM_QSTR(MP_QSTR_TWT_REQUEST), MP_ROM_INT(HALOW_TWT_REQUEST) }, + { MP_ROM_QSTR(MP_QSTR_TWT_SUGGEST), MP_ROM_INT(HALOW_TWT_SUGGEST) }, + { MP_ROM_QSTR(MP_QSTR_TWT_DEMAND), MP_ROM_INT(HALOW_TWT_DEMAND) }, + + { MP_ROM_QSTR(MP_QSTR_DUTY_CYCLE_SPREAD), MP_ROM_INT(HALOW_DUTY_CYCLE_SPREAD) }, + { MP_ROM_QSTR(MP_QSTR_DUTY_CYCLE_BURST), MP_ROM_INT(HALOW_DUTY_CYCLE_BURST) }, +}; +static MP_DEFINE_CONST_DICT(network_halow_locals_dict, network_halow_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + mp_network_halow_type, + MP_QSTR_HALOW, + MP_TYPE_FLAG_NONE, + make_new, network_halow_make_new, + print, network_halow_print, + locals_dict, &network_halow_locals_dict + ); + +MP_REGISTER_ROOT_POINTER(struct _machine_spi_obj_t *mp_halow_spi); +MP_REGISTER_ROOT_POINTER(void *halow_heap); + +#endif // MICROPY_PY_NETWORK_HALOW diff --git a/extmod/network_halow.h b/extmod/network_halow.h new file mode 100644 index 00000000000..5a18e521695 --- /dev/null +++ b/extmod/network_halow.h @@ -0,0 +1,35 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2026 OpenMV LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef MICROPY_INCLUDED_EXTMOD_NETWORK_HALOW_H +#define MICROPY_INCLUDED_EXTMOD_NETWORK_HALOW_H + +extern const mp_obj_type_t mp_network_halow_type; + +// Releases the driver's memory pool, which comes from the GC heap, so call +// before the heap is swept. +void network_halow_deinit_all(void); + +#endif // MICROPY_INCLUDED_EXTMOD_NETWORK_HALOW_H From b4849dde378eb0cd320193defef2ca8997bcc6fd Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:35:53 -0700 Subject: [PATCH 07/15] docs/library: Add network.HALOW. Documents the network.HALOW 802.11ah station interface: its methods, the config and status parameters, the security/power/duty-cycle/TWT constants and the link states, with notes on the ways 802.11ah differs from 2.4GHz Wi-Fi -- no WPA2-PSK, a mandatory country/channel plan, and a transmit-only power-save mode. Signed-off-by: Kwabena W. Agyeman --- docs/library/network.HALOW.rst | 305 +++++++++++++++++++++++++++++++++ docs/library/network.rst | 1 + 2 files changed, 306 insertions(+) create mode 100644 docs/library/network.HALOW.rst diff --git a/docs/library/network.HALOW.rst b/docs/library/network.HALOW.rst new file mode 100644 index 00000000000..c1aaabedfff --- /dev/null +++ b/docs/library/network.HALOW.rst @@ -0,0 +1,305 @@ +.. currentmodule:: network +.. _network.HALOW: + +class HALOW -- control 802.11ah (Wi-Fi HaLow) network interfaces +================================================================ + +This class provides a driver for Morse Micro MM6108/MM8108 802.11ah +transceivers. 802.11ah is sub-1 GHz Wi-Fi: much longer range and much lower +power than 2.4GHz Wi-Fi, at a fraction of the throughput. It carries ordinary +Ethernet frames, so sockets, DHCP and DNS all work as they do on +:class:`network.WLAN`. + +Example usage:: + + import network + # The channel plan is set by regulation and there is no worldwide default, + # so the country must be set before the interface is brought up. + network.country('US') + + nic = network.HALOW(network.HALOW.IF_STA) + nic.active(True) + nic.connect('your-ssid', 'your-passphrase') + while not nic.isconnected(): + pass + print(nic.ifconfig()) + +Constructors +------------ + +.. class:: HALOW(interface_id) + + Create a HALOW network interface object. The only supported interface is + ``network.HALOW.IF_STA`` (station), which is the default. + +Methods +------- + +.. method:: HALOW.active([is_active]) + + Query or set whether the interface is active. Bringing the station + interface up powers on the transceiver, which takes a moment: the firmware + image is loaded over the bus before the interface is usable. + + Raises ``ValueError`` if no country has been set; see + :func:`network.country`. + +.. method:: HALOW.connect(ssid, key=None, *, security=None, bssid=None) + + Connect to a network. ``security`` is one of the ``HALOW.SEC_*`` constants; + when it is not given, an open network is assumed if no key is supplied and + ``SEC_SAE`` otherwise. 802.11ah has no WPA2-PSK, so a passphrase always + means SAE (WPA3). + +.. method:: HALOW.disconnect() + + Disconnect from the currently connected network. + +.. method:: HALOW.deinit() + + Deactivate the interface and release the memory the driver allocated. + +.. method:: HALOW.scan() + + Scan for available networks. Only usable on ``IF_STA``. + + Sweeps every channel in the regulatory domain and returns what it found, up + to 32 networks; a sweep that sees more drops the rest. It takes around + 5 seconds in the United States. Most of that is dwell time: + the sweep waits longer than a beacon interval on each channel, because a + shorter visit misses networks that are plainly there. Returns an empty list + while a connection is in progress or established, as the radio cannot scan + and hold a link at the same time. + + Fails with ``EPERM`` when power saving is enabled; see ``HALOW.PM_NONE``. + + Returns a list of tuples with the fields + ``(ssid, bssid, channel, rssi, security, hidden)``, where ``security`` is one + of the ``SEC_*`` constants. + + ``channel`` is the S1G channel the beacon was *received* on, and is ``0`` + for an access point operating outside the local regulatory plan. An access + point using a wide channel beacons on its primary channel so that narrowband + stations can hear it, so this is not its operating channel: an 8 MHz access + point whose management page reports 916 MHz is seen here on its primary + channel, 1 MHz lower. Use ``config('channel')`` and ``config('bandwidth')`` + once associated to read the operating channel itself. + +.. method:: HALOW.status([param]) + + With no argument, return the link status, one of the ``STAT_*`` constants. + With + ``'rssi'``, return the signal strength of the connected AP in dBm; with + ``'bssid'``, its BSSID; with ``'duty_cycle'``, the regulatory duty cycle as + ``(duty_cycle, mode, burst_remaining_us, burst_window_us)`` where + ``duty_cycle`` is in hundredths of a percent; with ``'rates'``, the rate + control table as a list of ``(mcs, bandwidth_mhz, gi, sent, success)``, + covering only the entries that have been used. + +.. method:: HALOW.isconnected() + + Returns ``True`` when connected to an AP *and* an IP address has been + obtained. + +.. method:: HALOW.ifconfig([(ip, subnet, gateway, dns)]) + + See :meth:`network.WLAN.ifconfig`. + +.. method:: HALOW.ipconfig('param') + HALOW.ipconfig(param=value, ...) + + See :meth:`network.AbstractNIC.ipconfig`. + +.. method:: HALOW.config('param') + HALOW.config(param=value, ...) + + Get or set general network interface parameters: + + ================== ======================================================== + Parameter Description + ================== ======================================================== + mac MAC address (bytes), read only + version ``(morselib, firmware, chip)`` version strings, read + only; fails with ``ENODEV`` before the interface is + active + ssid network name of the connected network (string), read + only + channel S1G channel number the interface is operating on, read + only + bandwidth width of the primary channel in MHz, read only + hostname the hostname used by DHCP (deprecated, use + :func:`network.hostname`) + pm power management mode, one of the ``PM_*`` constants + ps_timeout time in ms the transceiver stays awake after activity + before dozing again; only has an effect when *pm* is + ``PM_POWERSAVE`` + txpower maximum transmit power in dBm + ampdu whether frame aggregation is enabled (default ``True``) + sgi whether the short guard interval is enabled (default + ``True``) + subbands whether subband operation is enabled (default ``True``) + rts_threshold RTS threshold in octets, ``0`` to disable + fragment_threshold fragmentation threshold in octets, ``0`` to disable + listen_interval beacons the station may sleep for between wakes, which + the access point uses to size how long it buffers + frames; ``0`` disables + duty_cycle how permitted air time is spread, ``DUTY_CYCLE_SPREAD`` + or ``DUTY_CYCLE_BURST``. How much of the allowance is + left is reported by ``status('duty_cycle')`` + twt ``(interval_us, duration_us[, setup])``, write only. + Asks the access point for a Target Wake Time agreement: + awake for *duration_us* every *interval_us* rather than + at every DTIM. *setup* is ``TWT_REQUEST``, + ``TWT_SUGGEST`` or ``TWT_DEMAND``. Carried in the + association request, so it fails with ``EPERM`` once + associated + wnm_sleep write only. Enter or leave WNM sleep, in which the + transceiver sleeps across many DTIM periods and the + access point buffers traffic for it. Requires + ``PM_POWERSAVE`` and an established connection. Do not + transmit while asleep + wnm_powerdown whether entering WNM sleep also powers the transceiver + down, which uses the least power but takes longer to + wake + health_check ``(min_ms, max_ms)``, write only. How often the driver + checks the transceiver is responding; each check wakes + it + fixed_rate ``(mcs, bandwidth, gi)``, write only. Pins the transmit + rate so emissions can be measured at a known modulation. + *mcs* is 0 to 9, *bandwidth* is 1, 2, 4 or 8 MHz, *gi* + is 0 for short or 1 for long. ``(-1, -1, -1)`` + restores automatic selection + ================== ======================================================== + + ``ampdu``, ``sgi``, ``subbands`` and ``listen_interval`` are only accepted + by the transceiver while it is not associated -- the last is carried in the + association request -- so setting them on a connected interface takes effect + the next time it associates. + + .. note:: + + Only station mode is currently supported. Access-point operation -- + and with it setting ``ssid`` and ``channel`` -- is not available. + +Regulatory testing +------------------ + +.. method:: HALOW.ioctl(cmd, buf) + + Execute a vendor command. ``buf`` holds the command on entry and receives + the response, so it must be a writable buffer large enough for both; the + length of the response is returned. ``cmd`` is reserved and should be + ``None``, as the transceiver's test interface has no command code. + + The command format is defined by Morse Micro's tooling rather than here. + Intended for certification work, where it is used together with the + ``fixed_rate`` configuration option. + + +Constants +--------- + +.. data:: HALOW.IF_STA + + Interface identifier, for the constructor. + +.. data:: HALOW.SEC_OPEN + HALOW.SEC_OWE + HALOW.SEC_SAE + HALOW.SEC_WPA3 + + Security modes. ``SEC_OWE`` is opportunistic wireless encryption + (unauthenticated but encrypted) and ``SEC_SAE`` is WPA3; ``SEC_WPA3`` is an + alias for ``SEC_SAE``. 802.11ah does not define WPA2-PSK. + +.. data:: HALOW.TWT_REQUEST + HALOW.TWT_SUGGEST + HALOW.TWT_DEMAND + + How firmly a Target Wake Time agreement is asked for. + +.. data:: HALOW.DUTY_CYCLE_SPREAD + HALOW.DUTY_CYCLE_BURST + + Duty cycle modes. Where regulation limits air time, ``DUTY_CYCLE_SPREAD`` + spreads it evenly and ``DUTY_CYCLE_BURST`` makes it available in bursts. + Regions without a limit are unaffected. + +.. data:: HALOW.STAT_IDLE + HALOW.STAT_CONNECTING + HALOW.STAT_NOIP + HALOW.STAT_GOT_IP + HALOW.STAT_CONNECT_FAIL + + Link states, as returned by :meth:`HALOW.status()`. ``STAT_NOIP`` means + associated but without an address yet; :meth:`HALOW.isconnected()` is only + true at ``STAT_GOT_IP``. + + .. warning:: + + There is no state for "wrong passphrase" or "no such network". The + transceiver reports only connecting, connected and disabled, and it + retries a failed association indefinitely with a growing backoff, so + :meth:`HALOW.status()` alternates between ``STAT_CONNECTING`` and + ``STAT_IDLE`` for as long as it keeps trying. ``STAT_CONNECT_FAIL`` + means the transceiver itself failed, not that the join was refused. + + Enforce your own deadline, and to work out *why* it is not joining, + :meth:`HALOW.disconnect()` first and then :meth:`HALOW.scan()` -- + scanning returns an empty list while a join is in progress:: + + nic.connect(ssid, key) + start = time.ticks_ms() + while not nic.isconnected(): + if time.ticks_diff(time.ticks_ms(), start) > 30000: + nic.disconnect() + seen = [n[0] for n in nic.scan()] + raise OSError("not found" if ssid.encode() not in seen + else "found, but would not associate") + time.sleep_ms(100) + +.. data:: HALOW.PM_NONE + HALOW.PM_POWERSAVE + + Power management modes. The default is ``PM_NONE``, in which the + transceiver is always listening and the interface behaves like any other + network interface. + + ``PM_POWERSAVE`` puts the interface into a **transmit-only mode**, intended + for battery powered sensors that wake, send, and sleep again. A dozing + transceiver is not listening, so in this mode: + + * the interface cannot be reached by an incoming connection; + * :meth:`HALOW.scan` fails with ``EPERM``, as probe responses cannot be + received; + * association takes considerably longer. + + Outgoing connections and the replies to them continue to work. How long + the transceiver stays awake after activity is set by the ``ps_timeout`` + configuration option. + + Associating with power saving already enabled is slow and unreliable, as + the transceiver is dozing while it tries. Associate first and enable it + afterwards:: + + nic.active(True) + nic.connect(ssid, key) + while not nic.isconnected(): + pass + nic.config(pm=network.HALOW.PM_POWERSAVE) + + +Link status +----------- + +:meth:`HALOW.status` with no argument returns one of: + +======================= ===== ============================= +Constant Value Meaning +======================= ===== ============================= +``STAT_IDLE`` 0 down +``STAT_CONNECTING`` 1 connecting +``STAT_NOIP`` 2 associated, no IP address yet +``STAT_GOT_IP`` 3 up, with an IP address +``STAT_CONNECT_FAIL`` -1 connection failed +======================= ===== ============================= diff --git a/docs/library/network.rst b/docs/library/network.rst index d05d17132dc..ad3e5fc0de2 100644 --- a/docs/library/network.rst +++ b/docs/library/network.rst @@ -190,6 +190,7 @@ provide a way to control networking interfaces of various kinds. network.WLAN.rst network.WLANWiPy.rst + network.HALOW.rst network.WIZNET5K.rst network.LAN.rst network.PPP.rst From 0164c7640984ea7cd7f7e0dd9490ef6fcc429259 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:37:31 -0700 Subject: [PATCH 08/15] tests/extmod_hardware: Add network.HALOW tests. Two hardware suites for the network.HALOW interface. network_halow.py needs only the transceiver: it checks the constants, active() cycling, config validation and out-of-range rejection, scan, and that use-after-deinit and config-after-deactivate are refused rather than fatal. network_halow_sta.py needs a HaLow access point and credentials in a halow_config module, and covers association, a socket round trip, and the association-only settings. Signed-off-by: Kwabena W. Agyeman --- tests/extmod_hardware/network_halow.py | 226 +++++++++++++++++++++ tests/extmod_hardware/network_halow_sta.py | 132 ++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 tests/extmod_hardware/network_halow.py create mode 100644 tests/extmod_hardware/network_halow_sta.py diff --git a/tests/extmod_hardware/network_halow.py b/tests/extmod_hardware/network_halow.py new file mode 100644 index 00000000000..c8c0c8e9fd4 --- /dev/null +++ b/tests/extmod_hardware/network_halow.py @@ -0,0 +1,226 @@ +# Test network.HALOW, the parts that need no access point. +# +# IMPORTANT: This test requires hardware: a Morse Micro transceiver on the bus +# the board is configured for. It brings the interface up but does not +# associate, so nothing else is needed. See network_halow_sta.py for the tests +# that do need a network. + +try: + import network + + network.HALOW +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit + +import unittest + +COUNTRY = "US" + + +class Test(unittest.TestCase): + @classmethod + def setUpClass(cls): + network.country(COUNTRY) + cls.nic = network.HALOW() + cls.nic.active(True) + + @classmethod + def tearDownClass(cls): + cls.nic.active(False) + + def test_singleton(self): + self.assertIs(network.HALOW(), network.HALOW(network.HALOW.IF_STA)) + + def test_constants(self): + # Security modes are distinct, and 802.11ah has no WPA2-PSK. + self.assertNotEqual(network.HALOW.SEC_OPEN, network.HALOW.SEC_SAE) + self.assertNotEqual(network.HALOW.SEC_OPEN, network.HALOW.SEC_OWE) + self.assertNotEqual(network.HALOW.PM_NONE, network.HALOW.PM_POWERSAVE) + + def test_active(self): + self.assertTrue(self.nic.active()) + + def test_mac(self): + mac = self.nic.config("mac") + self.assertEqual(len(mac), 6) + self.assertNotEqual(mac, b"\x00" * 6) + self.assertNotEqual(mac, b"\xff" * 6) + + def test_version(self): + morselib, firmware, chip = self.nic.config("version") + self.assertTrue(morselib) + self.assertTrue(firmware) + self.assertTrue(chip) + + def test_scan_returns_tuples(self): + # A sweep covers every channel of the regulatory domain, so it takes a + # few seconds; what is found depends on the environment. + for net in self.nic.scan(): + ssid, bssid, channel, rssi, security, hidden = net + self.assertEqual(len(bssid), 6) + self.assertTrue(-120 < rssi < 0) + self.assertIn( + security, + (network.HALOW.SEC_OPEN, network.HALOW.SEC_OWE, network.HALOW.SEC_SAE), + ) + + def test_scan_takes_no_arguments(self): + with self.assertRaises(TypeError): + self.nic.scan(ssid="anything") + + def test_radio_settings(self): + before = [self.nic.config(k) for k in ("ampdu", "sgi", "subbands")] + self.nic.config(ampdu=False, sgi=False, subbands=False) + self.assertFalse(self.nic.config("ampdu")) + self.assertFalse(self.nic.config("sgi")) + self.assertFalse(self.nic.config("subbands")) + self.nic.config(ampdu=before[0], sgi=before[1], subbands=before[2]) + + def test_thresholds(self): + self.nic.config(rts_threshold=512, fragment_threshold=1024) + self.assertEqual(self.nic.config("rts_threshold"), 512) + self.assertEqual(self.nic.config("fragment_threshold"), 1024) + self.nic.config(rts_threshold=0, fragment_threshold=0) + + def test_listen_interval(self): + self.nic.config(listen_interval=5) + self.assertEqual(self.nic.config("listen_interval"), 5) + self.nic.config(listen_interval=0) + + def test_power_management(self): + self.nic.config(pm=network.HALOW.PM_POWERSAVE) + self.assertEqual(self.nic.config("pm"), network.HALOW.PM_POWERSAVE) + self.nic.config(pm=network.HALOW.PM_NONE) + self.assertEqual(self.nic.config("pm"), network.HALOW.PM_NONE) + + def test_duty_cycle(self): + limit, mode, burst_left, burst_window = self.nic.status("duty_cycle") + # In hundredths of a percent, so a full duty cycle is 10000. + self.assertTrue(0 < limit <= 10000) + self.nic.config(duty_cycle=network.HALOW.DUTY_CYCLE_BURST) + self.assertEqual(self.nic.config("duty_cycle"), network.HALOW.DUTY_CYCLE_BURST) + self.nic.config(duty_cycle=network.HALOW.DUTY_CYCLE_SPREAD) + self.assertEqual(self.nic.config("duty_cycle"), network.HALOW.DUTY_CYCLE_SPREAD) + + def test_txpower(self): + before = self.nic.config("txpower") + self.nic.config(txpower=10) + self.assertEqual(self.nic.config("txpower"), 10) + self.nic.config(txpower=before) + + def test_status_reports_a_known_state(self): + self.assertIn( + self.nic.status(), + ( + network.HALOW.STAT_IDLE, + network.HALOW.STAT_CONNECTING, + network.HALOW.STAT_NOIP, + network.HALOW.STAT_GOT_IP, + network.HALOW.STAT_CONNECT_FAIL, + ), + ) + + def test_health_check(self): + self.nic.config(health_check=(30000, 120000)) + # A minimum above the maximum is not a usable interval. + with self.assertRaises(OSError): + self.nic.config(health_check=(120000, 30000)) + + def test_rates(self): + for mcs, bandwidth, gi, sent, success in self.nic.status("rates"): + self.assertTrue(0 <= mcs <= 10) + self.assertIn(bandwidth, (1, 2, 4, 8)) + self.assertTrue(success <= sent) + + def test_scan_refused_while_dozing(self): + # A dozing transceiver cannot hear probe responses. + self.nic.config(pm=network.HALOW.PM_POWERSAVE) + try: + with self.assertRaises(OSError): + self.nic.scan() + finally: + self.nic.config(pm=network.HALOW.PM_NONE) + + def test_wnm_sleep_refused_without_power_save(self): + with self.assertRaises(OSError): + self.nic.config(wnm_sleep=True) + + def test_wnm_powerdown_setting(self): + self.nic.config(wnm_powerdown=True) + self.assertTrue(self.nic.config("wnm_powerdown")) + self.nic.config(wnm_powerdown=False) + self.assertFalse(self.nic.config("wnm_powerdown")) + + def test_fixed_rate(self): + # Pin the rate, then hand it back to rate control. + self.nic.config(fixed_rate=(0, 1, 1)) + self.nic.config(fixed_rate=(-1, -1, -1)) + + def test_ioctl_rejects_a_bad_command(self): + with self.assertRaises(OSError): + self.nic.ioctl(None, bytearray(32)) + + def test_unknown_config_key(self): + with self.assertRaises(ValueError): + self.nic.config("not_a_parameter") + + def test_settings_reject_out_of_range_values(self): + # These reach fields narrower than a Python integer, where a negative or + # oversized value would wrap rather than be refused: txpower=-5 read + # back as 65531, which the transceiver takes as no limit at all. + for key, value in ( + ("txpower", -5), + ("txpower", 1 << 20), + ("listen_interval", -1), + ("listen_interval", 100000), + ): + with self.assertRaises(ValueError): + self.nic.config(**{key: value}) + + def test_twt_interval_is_not_a_machine_word(self): + # An hour between wakes is a legitimate request and does not fit in a + # machine word, so converting it as one raised OverflowError before the + # interface could see it. Accepting it is the assertion. + self.nic.config(twt=(3600000000, 100000)) + + def test_twt_rejects_a_window_longer_than_its_interval(self): + with self.assertRaises(OSError): + self.nic.config(twt=(1000, 100000)) + + def test_rates_guard_interval_is_one_bit(self): + for _, _, gi, _, _ in self.nic.status("rates"): + self.assertIn(gi, (0, 1)) + + def test_config_after_deactivate(self): + # Taking the interface down shuts the transceiver down but leaves the + # driver initialised, and the cached settings used to be applied anyway + # -- straight into a morselib that was no longer running, which answered + # EAGAIN. Settings made while down are meant to be kept for the next + # time it comes up. + try: + self.nic.active(False) + self.nic.config(pm=network.HALOW.PM_NONE) + self.nic.config(rts_threshold=512) + self.assertEqual(self.nic.config("rts_threshold"), 512) + finally: + self.nic.active(True) + self.nic.config(rts_threshold=0) + + def test_use_after_deinit_is_refused_not_fatal(self): + # deinit() hands the driver's memory pool back to the collector. The + # scan cache was left pointing into it, so the next sweep wrote into + # memory Python had already been given -- which a soft reset reaches + # too, since it tears the driver down the same way. Restores the + # interface either way, so the rest of the suite still has one. + try: + self.nic.scan() + self.nic.deinit() + self.assertEqual(self.nic.scan(), []) + self.assertFalse(self.nic.active()) + finally: + self.nic.active(True) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/extmod_hardware/network_halow_sta.py b/tests/extmod_hardware/network_halow_sta.py new file mode 100644 index 00000000000..306939f9115 --- /dev/null +++ b/tests/extmod_hardware/network_halow_sta.py @@ -0,0 +1,132 @@ +# Test network.HALOW associating with an access point. +# +# IMPORTANT: This test requires hardware: a Morse Micro transceiver, and an +# 802.11ah access point to join. Its credentials are not in this file; put them +# in a halow_config module on the device, as: +# +# SSID = "..." +# KEY = "..." +# COUNTRY = "US" +# +# The test is skipped when that module is absent. + +try: + import network + + network.HALOW +except (ImportError, AttributeError): + print("SKIP") + raise SystemExit + +try: + from halow_config import SSID, KEY, COUNTRY +except ImportError: + print("SKIP") + raise SystemExit + +import socket +import time +import unittest + +# Association covers a channel sweep and a four way handshake. +CONNECT_TIMEOUT_MS = 30000 + + +class Test(unittest.TestCase): + @classmethod + def setUpClass(cls): + network.country(COUNTRY) + cls.nic = network.HALOW() + cls.nic.config(pm=network.HALOW.PM_NONE) + cls.nic.active(True) + cls.nic.connect(SSID, KEY) + start = time.ticks_ms() + while not cls.nic.isconnected(): + if time.ticks_diff(time.ticks_ms(), start) > CONNECT_TIMEOUT_MS: + raise Exception("did not associate with %s" % SSID) + time.sleep_ms(100) + + @classmethod + def tearDownClass(cls): + cls.nic.disconnect() + cls.nic.active(False) + + def test_connected(self): + self.assertTrue(self.nic.isconnected()) + self.assertEqual(self.nic.status(), network.HALOW.STAT_GOT_IP) + + def test_ifconfig(self): + ip, netmask, gateway, dns = self.nic.ifconfig() + self.assertNotEqual(ip, "0.0.0.0") + self.assertNotEqual(gateway, "0.0.0.0") + + def test_status(self): + self.assertTrue(-120 < self.nic.status("rssi") < 0) + self.assertEqual(len(self.nic.status("bssid")), 6) + + def test_channel(self): + # Reported once associated, unlike a scan result, this is the channel + # being operated on. + self.assertTrue(self.nic.config("channel") > 0) + self.assertIn(self.nic.config("bandwidth"), (1, 2, 4, 8)) + + def test_ssid(self): + self.assertEqual(self.nic.config("ssid"), SSID) + + def test_rates_after_traffic(self): + # Move enough frames for rate control to have made a choice. + gateway = self.nic.ifconfig()[2] + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + for _ in range(64): + s.sendto(b"x" * 512, (gateway, 9)) + finally: + s.close() + # Frames are still on their way out when sendto() returns, and the rate + # table is updated as they complete. + time.sleep(1) + rates = self.nic.status("rates") + self.assertTrue(any(sent > 0 for _, _, _, sent, _ in rates)) + + def test_gateway_round_trip(self): + # Connecting to the gateway proves address resolution, transmit and + # receive all work; a refusal is as good as an accept here. + gateway = self.nic.ifconfig()[2] + s = socket.socket() + s.settimeout(10) + try: + try: + s.connect((gateway, 80)) + except OSError as er: + self.assertNotIn("ETIMEDOUT", repr(er)) + finally: + s.close() + + def test_twt_refused_while_associated(self): + # The agreement is carried in the association request. + with self.assertRaises(OSError): + self.nic.config(twt=(1000000, 20000, network.HALOW.TWT_SUGGEST)) + + def test_listen_interval_refused_while_associated(self): + # Also carried in the association request, so it applies to the next one. + self.nic.config(listen_interval=5) + self.assertEqual(self.nic.config("listen_interval"), 5) + self.nic.config(listen_interval=0) + + def test_scan_empty_while_associated(self): + # The radio cannot sweep and hold a link at the same time. + self.assertEqual(self.nic.scan(), []) + + def test_wnm_sleep_round_trip(self): + self.nic.config(pm=network.HALOW.PM_POWERSAVE) + try: + self.nic.config(wnm_sleep=True) + time.sleep(1) + self.nic.config(wnm_sleep=False) + self.assertTrue(self.nic.isconnected()) + finally: + self.nic.config(pm=network.HALOW.PM_NONE) + + +if __name__ == "__main__": + unittest.main() From 116b6254bda2ae966f52424a38b45e61b360dc57 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:47:08 -0700 Subject: [PATCH 09/15] alif: Add support for the HaLow network interface. Wire the drivers/halow driver into the alif port: include its build rules, tear it down on soft reset, and service it. The transceiver is polled from a dedicated 1ms soft timer -- more often than lwIP's 64ms tick, because morselib always has timers due and this poll is what notices a received frame -- and halow_schedule_poll() raises a PendSV dispatch so a queued transmit does not wait for the next tick. Signed-off-by: Kwabena W. Agyeman --- ports/alif/alif.mk | 4 ++++ ports/alif/main.c | 9 +++++++++ ports/alif/mpnetworkport.c | 39 ++++++++++++++++++++++++++++++++++++++ ports/alif/pendsv.h | 3 +++ 4 files changed, 55 insertions(+) diff --git a/ports/alif/alif.mk b/ports/alif/alif.mk index 16490b3355b..c3b16ea4662 100644 --- a/ports/alif/alif.mk +++ b/ports/alif/alif.mk @@ -19,6 +19,10 @@ QSTR_DEFS += qstrdefsport.h include $(TOP)/py/py.mk include $(TOP)/extmod/extmod.mk +ifeq ($(MICROPY_PY_NETWORK_HALOW),1) +include $(TOP)/drivers/halow/halow.mk +endif + ################################################################################ # Project specific settings and compiler/linker flags diff --git a/ports/alif/main.c b/ports/alif/main.c index 03d9705c325..566ae276541 100644 --- a/ports/alif/main.c +++ b/ports/alif/main.c @@ -32,6 +32,9 @@ #include "extmod/modbluetooth.h" #include "extmod/modmachine.h" #include "extmod/modnetwork.h" +#if MICROPY_PY_NETWORK_HALOW +#include "extmod/network_halow.h" +#endif #include "shared/readline/readline.h" #include "shared/runtime/gchelper.h" #include "shared/runtime/pyexec.h" @@ -172,6 +175,12 @@ int main(void) { #if MICROPY_PY_MACHINE_I2C_TARGET mp_machine_i2c_target_deinit_all(); #endif + #if MICROPY_PY_NETWORK + mod_network_deinit(); + #endif + #if MICROPY_PY_NETWORK_HALOW + network_halow_deinit_all(); + #endif soft_timer_deinit(); machine_pwm_deinit_all(); machine_pin_irq_deinit(); diff --git a/ports/alif/mpnetworkport.c b/ports/alif/mpnetworkport.c index 40def7f7fd7..9e53cfe5144 100644 --- a/ports/alif/mpnetworkport.c +++ b/ports/alif/mpnetworkport.c @@ -36,12 +36,25 @@ #include "lib/cyw43-driver/src/cyw43.h" #endif +#if MICROPY_PY_NETWORK_HALOW +#include "drivers/halow/halow.h" +#include "pendsv.h" +#endif + // Poll lwIP every 64ms by default #define LWIP_TICK_RATE_MS 64 +#if MICROPY_PY_NETWORK_HALOW +#define HALOW_TICK_RATE_MS 1 +#endif + // Soft timer for running lwIP in the background. static soft_timer_entry_t mp_network_soft_timer; +#if MICROPY_PY_NETWORK_HALOW +static soft_timer_entry_t mp_halow_soft_timer; +#endif + u32_t sys_now(void) { return mp_hal_ticks_ms(); } @@ -66,6 +79,21 @@ static void mp_network_soft_timer_callback(soft_timer_entry_t *self) { #endif } +#if MICROPY_PY_NETWORK_HALOW +static void mp_halow_soft_timer_callback(soft_timer_entry_t *self) { + (void)self; + if (halow_poll) { + halow_poll(); + } +} + +void halow_schedule_poll(void) { + if (halow_poll) { + pendsv_schedule_dispatch(PENDSV_DISPATCH_HALOW, halow_poll); + } +} +#endif + void mod_network_lwip_init(void) { soft_timer_static_init( &mp_network_soft_timer, @@ -75,6 +103,17 @@ void mod_network_lwip_init(void) { ); soft_timer_reinsert(&mp_network_soft_timer, LWIP_TICK_RATE_MS); + + #if MICROPY_PY_NETWORK_HALOW + soft_timer_static_init( + &mp_halow_soft_timer, + SOFT_TIMER_MODE_PERIODIC, + HALOW_TICK_RATE_MS, + mp_halow_soft_timer_callback + ); + + soft_timer_reinsert(&mp_halow_soft_timer, HALOW_TICK_RATE_MS); + #endif } #endif // MICROPY_PY_LWIP diff --git a/ports/alif/pendsv.h b/ports/alif/pendsv.h index 17d7e82dfc7..a7866c865bc 100644 --- a/ports/alif/pendsv.h +++ b/ports/alif/pendsv.h @@ -37,6 +37,9 @@ enum { #if MICROPY_PY_NETWORK_CYW43 PENDSV_DISPATCH_CYW43, #endif + #if MICROPY_PY_NETWORK_HALOW + PENDSV_DISPATCH_HALOW, + #endif MICROPY_BOARD_PENDSV_ENTRIES PENDSV_DISPATCH_MAX }; From 221c11c5e6fedb64f4415958b0a28e25c9ef55d7 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:47:27 -0700 Subject: [PATCH 10/15] mimxrt: Add support for the HaLow network interface. Wire the drivers/halow driver into the mimxrt port: include its build rules, tear it down on soft reset, and service the transceiver from the network poll each systick, with halow_schedule_poll() raising a PendSV dispatch so a queued transmit does not wait for the next tick. Signed-off-by: Kwabena W. Agyeman --- ports/mimxrt/Makefile | 4 ++++ ports/mimxrt/main.c | 6 ++++++ ports/mimxrt/mpnetworkport.c | 18 ++++++++++++++++++ ports/mimxrt/pendsv.h | 3 +++ 4 files changed, 31 insertions(+) diff --git a/ports/mimxrt/Makefile b/ports/mimxrt/Makefile index c4f592ff77d..214771171c7 100644 --- a/ports/mimxrt/Makefile +++ b/ports/mimxrt/Makefile @@ -51,6 +51,10 @@ FROZEN_MANIFEST ?= boards/manifest.py include $(TOP)/py/py.mk include $(TOP)/extmod/extmod.mk +ifeq ($(MICROPY_PY_NETWORK_HALOW),1) +include $(TOP)/drivers/halow/halow.mk +endif + # Set SDK directory based on MCU_SERIES MCUX_SDK_DIR = lib/nxp_driver/sdk MCU_DIR ?= $(MCUX_SDK_DIR)/devices/$(MCU_SERIES) diff --git a/ports/mimxrt/main.c b/ports/mimxrt/main.c index 664d1c4c16f..af80735177a 100644 --- a/ports/mimxrt/main.c +++ b/ports/mimxrt/main.c @@ -57,6 +57,9 @@ #include "systick.h" #include "extmod/modmachine.h" #include "extmod/modnetwork.h" +#if MICROPY_PY_NETWORK_HALOW +#include "extmod/network_halow.h" +#endif #include "extmod/vfs.h" extern uint8_t _sstack, _estack, _gc_heap_start, _gc_heap_end; @@ -189,6 +192,9 @@ int main(void) { #if MICROPY_PY_NETWORK mod_network_deinit(); #endif + #if MICROPY_PY_NETWORK_HALOW + network_halow_deinit_all(); + #endif #if MICROPY_PY_MACHINE_UART machine_uart_deinit_all(); #endif diff --git a/ports/mimxrt/mpnetworkport.c b/ports/mimxrt/mpnetworkport.c index aa8eef3ee23..54c419593f0 100644 --- a/ports/mimxrt/mpnetworkport.c +++ b/ports/mimxrt/mpnetworkport.c @@ -48,6 +48,10 @@ #include "lib/cyw43-driver/src/cyw43.h" #endif +#if MICROPY_PY_NETWORK_HALOW +#include "drivers/halow/halow.h" +#endif + // Poll lwIP every 128ms #define LWIP_TICK(tick) (((tick) & ~(SYSTICK_DISPATCH_NUM_SLOTS - 1) & 0x7f) == 0) @@ -74,6 +78,20 @@ void mod_network_lwip_poll_wrapper(uint32_t ticks_ms) { } } #endif + + #if MICROPY_PY_NETWORK_HALOW + if (halow_poll) { + pendsv_schedule_dispatch(PENDSV_DISPATCH_HALOW, halow_poll); + } + #endif } +#if MICROPY_PY_NETWORK_HALOW +void halow_schedule_poll(void) { + if (halow_poll) { + pendsv_schedule_dispatch(PENDSV_DISPATCH_HALOW, halow_poll); + } +} +#endif + #endif // MICROPY_PY_LWIP diff --git a/ports/mimxrt/pendsv.h b/ports/mimxrt/pendsv.h index d68c5aa2d5d..2cffa587895 100644 --- a/ports/mimxrt/pendsv.h +++ b/ports/mimxrt/pendsv.h @@ -34,6 +34,9 @@ enum { #if MICROPY_PY_NETWORK_CYW43 PENDSV_DISPATCH_CYW43, #endif + #if MICROPY_PY_NETWORK_HALOW + PENDSV_DISPATCH_HALOW, + #endif MICROPY_BOARD_PENDSV_ENTRIES PENDSV_DISPATCH_MAX }; From d603e27297682e4a411a9e083f3e62a8c350c840 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:47:42 -0700 Subject: [PATCH 11/15] stm32: Add support for the HaLow network interface. Wire the drivers/halow driver into the stm32 port: include its build rules, tear it down on soft reset, and service the transceiver from the network poll each systick, with halow_schedule_poll() raising a PendSV dispatch so a queued transmit does not wait for the next tick. Signed-off-by: Kwabena W. Agyeman --- ports/stm32/Makefile | 4 ++++ ports/stm32/main.c | 6 ++++++ ports/stm32/mpnetworkport.c | 18 ++++++++++++++++++ ports/stm32/pendsv.h | 3 +++ 4 files changed, 31 insertions(+) diff --git a/ports/stm32/Makefile b/ports/stm32/Makefile index dfd268851e7..2bb151759c9 100644 --- a/ports/stm32/Makefile +++ b/ports/stm32/Makefile @@ -58,6 +58,10 @@ MBOOT_TEXT0_ADDR ?= 0x08000000 include $(TOP)/py/py.mk include $(TOP)/extmod/extmod.mk +ifeq ($(MICROPY_PY_NETWORK_HALOW),1) +include $(TOP)/drivers/halow/halow.mk +endif + GIT_SUBMODULES += lib/libhydrogen lib/stm32lib lib/tinyusb CROSS_COMPILE ?= arm-none-eabi- diff --git a/ports/stm32/main.c b/ports/stm32/main.c index 17111c6df98..3d9aa10182d 100644 --- a/ports/stm32/main.c +++ b/ports/stm32/main.c @@ -41,6 +41,9 @@ #include "lib/littlefs/lfs2_util.h" #include "extmod/modmachine.h" #include "extmod/modnetwork.h" +#if MICROPY_PY_NETWORK_HALOW +#include "extmod/network_halow.h" +#endif #include "extmod/machine_can.h" #include "extmod/vfs.h" #include "extmod/vfs_fat.h" @@ -752,6 +755,9 @@ void stm32_main(uint32_t reset_mode) { #if MICROPY_PY_NETWORK mod_network_deinit(); #endif + #if MICROPY_PY_NETWORK_HALOW + network_halow_deinit_all(); + #endif soft_timer_deinit(); timer_deinit(); uart_deinit_all(); diff --git a/ports/stm32/mpnetworkport.c b/ports/stm32/mpnetworkport.c index 6db3e91ba90..89c1b81bfa6 100644 --- a/ports/stm32/mpnetworkport.c +++ b/ports/stm32/mpnetworkport.c @@ -48,6 +48,10 @@ #include "lib/cyw43-driver/src/cyw43.h" #endif +#if MICROPY_PY_NETWORK_HALOW +#include "drivers/halow/halow.h" +#endif + // Poll lwIP every 128ms #define LWIP_TICK(tick) (((tick) & ~(SYSTICK_DISPATCH_NUM_SLOTS - 1) & 0x7f) == 0) @@ -92,6 +96,20 @@ void mod_network_lwip_poll_wrapper(uint32_t ticks_ms) { } } #endif + + #if MICROPY_PY_NETWORK_HALOW + if (halow_poll) { + pendsv_schedule_dispatch(PENDSV_DISPATCH_HALOW, halow_poll); + } + #endif } +#if MICROPY_PY_NETWORK_HALOW +void halow_schedule_poll(void) { + if (halow_poll) { + pendsv_schedule_dispatch(PENDSV_DISPATCH_HALOW, halow_poll); + } +} +#endif + #endif // MICROPY_PY_LWIP diff --git a/ports/stm32/pendsv.h b/ports/stm32/pendsv.h index b90d2227ee4..0ebc51b2839 100644 --- a/ports/stm32/pendsv.h +++ b/ports/stm32/pendsv.h @@ -35,6 +35,9 @@ enum { #if MICROPY_PY_NETWORK_CYW43 PENDSV_DISPATCH_CYW43, #endif + #if MICROPY_PY_NETWORK_HALOW + PENDSV_DISPATCH_HALOW, + #endif #if MICROPY_PY_NETWORK_WIZNET5K PENDSV_DISPATCH_WIZNET, #endif From 58f35386d9b251be774b77548c6715f3708473dc Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:48:08 -0700 Subject: [PATCH 12/15] alif: Increase the lwIP heap to 64K. At the port's 16K MEM_SIZE, sustained TCP exhausts the lwIP heap: it returns ENOMEM and throughput collapses, recovers, and collapses again in a repeating pattern. Raise it to 64K, matching the stm32 port, which holds steady. Measured on an AE3 driving a HaLow uplink: tens of ENOMEM failures per 20-minute soak at 16K, none at 64K. Signed-off-by: Kwabena W. Agyeman --- ports/alif/lwip_inc/lwipopts.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/alif/lwip_inc/lwipopts.h b/ports/alif/lwip_inc/lwipopts.h index 62b6a84b609..88cd1b07670 100644 --- a/ports/alif/lwip_inc/lwipopts.h +++ b/ports/alif/lwip_inc/lwipopts.h @@ -10,7 +10,7 @@ #define LWIP_RAND() se_services_rand64() -#define MEM_SIZE (16 * 1024) +#define MEM_SIZE (64 * 1024) #define TCP_MSS (1460) #define TCP_OVERSIZE (TCP_MSS) #define TCP_WND (8 * TCP_MSS) From 9b68cd326bf0b26a34e946392d83860b1b241852 Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:57:37 -0700 Subject: [PATCH 13/15] stm32/boards/OPENMV_N6: Enable HaLow. Enable the Morse Micro MM8108 802.11ah shield on the N6: SPI2 on P0/P1/P2, the shield's control lines on the expansion header, and the pin interrupt for low-latency receive. Signed-off-by: Kwabena W. Agyeman --- ports/stm32/boards/OPENMV_N6/mpconfigboard.h | 12 ++++++++++++ ports/stm32/boards/OPENMV_N6/mpconfigboard.mk | 1 + 2 files changed, 13 insertions(+) diff --git a/ports/stm32/boards/OPENMV_N6/mpconfigboard.h b/ports/stm32/boards/OPENMV_N6/mpconfigboard.h index 751da650bfe..7ffd97914f7 100644 --- a/ports/stm32/boards/OPENMV_N6/mpconfigboard.h +++ b/ports/stm32/boards/OPENMV_N6/mpconfigboard.h @@ -150,6 +150,18 @@ #define CYW43_WIFI_NVRAM_INCLUDE_FILE "lib/cyw43-driver/firmware/wifi_nvram_1yn.h" #define CYW43_BT_FIRMWARE_INCLUDE_FILE "lib/cyw43-driver/firmware/cyw43_btfw_1yn.h" +// Morse Micro MM8108 802.11ah shield +#if MICROPY_PY_NETWORK_HALOW +#define MICROPY_HW_HALOW_SPI_ID (2) // P0/P1/P2 are SPI2 +#define MICROPY_HW_HALOW_CS (pyb_pin_P3) +#define MICROPY_HW_HALOW_IRQ (pyb_pin_P8) +#define MICROPY_PY_NETWORK_HALOW_PIN_IRQ (1) +#define MICROPY_HW_HALOW_RESET (pyb_pin_P7) +#define MICROPY_HW_HALOW_WAKE (pyb_pin_P9) +#define MICROPY_HW_HALOW_BUSY (pyb_pin_P11) +#define MICROPY_HW_HALOW_BUSY_INVERTED (1) // shield inverts BUSY +#endif + // Bluetooth config #define MICROPY_HW_BLE_UART_ID (PYB_UART_2) #define MICROPY_HW_BLE_UART_BAUDRATE (115200) diff --git a/ports/stm32/boards/OPENMV_N6/mpconfigboard.mk b/ports/stm32/boards/OPENMV_N6/mpconfigboard.mk index 8f748a0b8de..d1f60c86348 100644 --- a/ports/stm32/boards/OPENMV_N6/mpconfigboard.mk +++ b/ports/stm32/boards/OPENMV_N6/mpconfigboard.mk @@ -22,6 +22,7 @@ MICROPY_BLUETOOTH_NIMBLE ?= 1 MICROPY_BLUETOOTH_BTSTACK ?= 0 MICROPY_PY_LWIP ?= 1 MICROPY_PY_NETWORK_CYW43 ?= 1 +MICROPY_PY_NETWORK_HALOW ?= 1 MICROPY_PY_SSL ?= 1 MICROPY_SSL_MBEDTLS ?= 1 MICROPY_VFS_LFS2 ?= 0 From b7c429bed9fb281e626e2a8be7026808b0051e2c Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:58:04 -0700 Subject: [PATCH 14/15] alif/boards/OPENMV_AE3: Enable HaLow. Enable the Morse Micro MM8108 802.11ah shield on the AE3's HP core: SPI0 on P0/P1/P2, with the shield's control lines sharing the JTAG pins on the header. The transceiver is polled -- the IRQ line is mapped but the pin interrupt is left off, as it gives no throughput gain on this port. Signed-off-by: Kwabena W. Agyeman --- ports/alif/boards/OPENMV_AE3/mpconfigboard.h | 10 ++++++++++ ports/alif/boards/OPENMV_AE3/mpconfigboard.mk | 1 + 2 files changed, 11 insertions(+) diff --git a/ports/alif/boards/OPENMV_AE3/mpconfigboard.h b/ports/alif/boards/OPENMV_AE3/mpconfigboard.h index a4981ce167e..11ceb3e2967 100644 --- a/ports/alif/boards/OPENMV_AE3/mpconfigboard.h +++ b/ports/alif/boards/OPENMV_AE3/mpconfigboard.h @@ -80,3 +80,13 @@ extern void board_exit_standby(void); // Bluetooth config #define MICROPY_HW_BLE_UART_ID (0) #define MICROPY_HW_BLE_UART_BAUDRATE (115200) + +// Morse Micro MM8108 802.11ah shield +#if MICROPY_PY_NETWORK_HALOW +#define MICROPY_HW_HALOW_SPI_ID (0) // P0/P1/P2 are SPI0 +#define MICROPY_HW_HALOW_CS (pin_P5_2) // P3 +#define MICROPY_HW_HALOW_IRQ (pin_P4_4) // JTAG_TCK +#define MICROPY_HW_HALOW_RESET (pin_P4_7) // JTAG_TDO +#define MICROPY_HW_HALOW_WAKE (pin_P4_5) // JTAG_TMS +#define MICROPY_HW_HALOW_BUSY (pin_P4_6) // JTAG_TDI +#endif diff --git a/ports/alif/boards/OPENMV_AE3/mpconfigboard.mk b/ports/alif/boards/OPENMV_AE3/mpconfigboard.mk index 83cc17dd15e..dc12687a014 100644 --- a/ports/alif/boards/OPENMV_AE3/mpconfigboard.mk +++ b/ports/alif/boards/OPENMV_AE3/mpconfigboard.mk @@ -16,6 +16,7 @@ MICROPY_PY_BLUETOOTH = $(CORE_M55_HP) MICROPY_BLUETOOTH_NIMBLE = $(CORE_M55_HP) MICROPY_PY_LWIP = $(CORE_M55_HP) MICROPY_PY_NETWORK_CYW43 = $(CORE_M55_HP) +MICROPY_PY_NETWORK_HALOW ?= $(CORE_M55_HP) MICROPY_PY_SSL = $(CORE_M55_HP) MICROPY_SSL_MBEDTLS = $(CORE_M55_HP) MICROPY_PY_OPENAMP = 1 From 7f83845b082eed34e5b2040e85fff015d2545e1b Mon Sep 17 00:00:00 2001 From: "Kwabena W. Agyeman" Date: Wed, 12 Aug 2026 21:58:33 -0700 Subject: [PATCH 15/15] mimxrt/boards/OPENMV_RT1060: Enable HaLow. Enable the Morse Micro MM8108 802.11ah shield on the RT1060: LPSPI3 (logical SPI 1) on P0/P1/P2, the shield's control lines on the header, and the pin interrupt for low-latency receive. morselib is linked from its Cortex-M7 build. Signed-off-by: Kwabena W. Agyeman --- ports/mimxrt/boards/OPENMV_RT1060/mpconfigboard.h | 12 ++++++++++++ ports/mimxrt/boards/OPENMV_RT1060/mpconfigboard.mk | 2 ++ 2 files changed, 14 insertions(+) diff --git a/ports/mimxrt/boards/OPENMV_RT1060/mpconfigboard.h b/ports/mimxrt/boards/OPENMV_RT1060/mpconfigboard.h index 017528334d0..fae9f5bd4bd 100644 --- a/ports/mimxrt/boards/OPENMV_RT1060/mpconfigboard.h +++ b/ports/mimxrt/boards/OPENMV_RT1060/mpconfigboard.h @@ -245,3 +245,15 @@ extern void mimxrt_hal_bootloader(void); #define CFG_TUD_CDC_EP_BUFSIZE (4096) #define CFG_TUD_CDC_RX_BUFSIZE (4096) #define CFG_TUD_CDC_TX_BUFSIZE (4096) + +// Morse Micro MM8108 802.11ah shield +#if MICROPY_PY_NETWORK_HALOW +#define MICROPY_HW_HALOW_SPI_ID (1) // P0/P1/P2 are LPSPI3, logical SPI 1 +#define MICROPY_HW_HALOW_CS (pin_GPIO_AD_B0_03) // P3 +#define MICROPY_HW_HALOW_IRQ (pin_GPIO_B0_07) // P8 +#define MICROPY_PY_NETWORK_HALOW_PIN_IRQ (1) +#define MICROPY_HW_HALOW_RESET (pin_GPIO_B0_06) // P7 +#define MICROPY_HW_HALOW_WAKE (pin_GPIO_B1_00) // P9 +#define MICROPY_HW_HALOW_BUSY (pin_WAKEUP) // P11 +#define MICROPY_HW_HALOW_BUSY_INVERTED (1) // shield inverts BUSY +#endif diff --git a/ports/mimxrt/boards/OPENMV_RT1060/mpconfigboard.mk b/ports/mimxrt/boards/OPENMV_RT1060/mpconfigboard.mk index 466621a8d59..3ced19a5e1b 100644 --- a/ports/mimxrt/boards/OPENMV_RT1060/mpconfigboard.mk +++ b/ports/mimxrt/boards/OPENMV_RT1060/mpconfigboard.mk @@ -16,6 +16,8 @@ MICROPY_PY_LWIP = 1 MICROPY_PY_USSL = 1 MICROPY_SSL_MBEDTLS = 1 MICROPY_PY_NETWORK_CYW43 = 1 +MICROPY_PY_NETWORK_HALOW ?= 1 +HALOW_MORSELIB_CORE = arm-cortex-m7f MICROPY_PY_NETWORK_PHYKSZ8081RND = 1 MICROPY_PY_BLUETOOTH = 1 MICROPY_BLUETOOTH_NIMBLE = 1