Skip to content

extmod/network_halow: Add the network.HALOW 802.11ah bindings. - #19617

Open
kwagyeman wants to merge 14 commits into
micropython:masterfrom
kwagyeman:kwabena/network_halow_upstream
Open

extmod/network_halow: Add the network.HALOW 802.11ah bindings.#19617
kwagyeman wants to merge 14 commits into
micropython:masterfrom
kwagyeman:kwabena/network_halow_upstream

Conversation

@kwagyeman

Copy link
Copy Markdown
Contributor

Summary

This PR adds network.HALOW, a MicroPython network interface for the Morse Micro MM6108/MM8108 802.11ah (Wi-Fi HaLow) sub-GHz Wi-Fi transceivers, and wires it into the stm32, alif and mimxrt ports.

HaLow trades raw throughput for range and power, so the API follows the existing network.WLAN/cyw43 station model — active(), connect(), isconnected(), ifconfig(), config(), status() — plus the HaLow-specific pieces: regulatory network.country(), S1G channel/bandwidth reporting, OPEN/OWE/SAE (WPA3) security, and TWT-based power save.

What's included:

  • drivers/halow/ — the driver core: the SPI/SDIO transport to the transceiver, an lwIP netif, a packet-memory pool, and a small OSAL/scheduler shim so the vendor library runs cooperatively off the MicroPython scheduler + a PendSV dispatch (no vendor RTOS), mirroring how the cyw43 driver is integrated.
  • extmod/network_halow.c — the network.HALOW Python binding.
  • lib/mm-iot-sdk — the Morse Micro MM-IoT-SDK as a submodule, providing the prebuilt morselib and the transceiver firmware/board-config blobs.
  • Port integration for stm32/alif/mimxrt (each gains a small mp_hal_pin_interrupt C-level IRQ-registration helper), docs, and hardware tests under tests/extmod_hardware/.
  • HaLow enabled on OPENMV_N6 and OPENMV_AE3.

The driver is only compiled when a board sets MICROPY_PY_NETWORK_HALOW=1; it has zero impact on other boards.

Testing

Tested on hardware against a HaLow AP — association plus 20-minute TCP/UDP throughput soaks (hundreds of connect/transfer cycles, no leaks or asserts). All three integrated ports (stm32/alif/mimxrt) compile.

Throughput (bench soak means, station→AP, channel 28, 2 MHz bandwidth):

Board MCU Transport UDP up TCP up TCP down Assoc
OPENMV_N6 STM32N6 (Cortex-M55) hardware IRQ 8.1 Mbit/s 6.0 Mbit/s 5.0 Mbit/s 6.3 s
OPENMV_RT1060 i.MX RT1062 (Cortex-M7) hardware IRQ 6.1 Mbit/s 2.7 Mbit/s 3.2 Mbit/s 5.0 s
OPENMV_AE3 Alif Ensemble E3 (Cortex-M55) polled 3.2 Mbit/s 2.1 Mbit/s 2.6 Mbit/s 3.6 s

UDP-up is the cleanest capability figure (no retransmit/pacing); ~8 Mbit/s on the N6 is close to the practical ceiling for 2 MHz bandwidth. TCP is lower across the board due to link-level loss/retransmit behaviour.

OPENMV_N6 and OPENMV_AE3 are enabled in this PR. The mimxrt port integration is included and compiles; it's exercised on hardware by an out-of-tree board (the OpenMV RT1060, an i.MX RT1062, numbers above), but no in-tree board enables HaLow on mimxrt yet.

Trade-offs and Alternatives

  • morselib is a prebuilt binary (Morse Micro Binary Distribution Licence), added via the lib/mm-iot-sdk submodule — the same model as other vendor Wi-Fi stacks (cyw43, NINA-W10). Building it from source (HALOW_MORSELIB_SOURCE=1) is supported for debugging, but those sources are GPL-3.0 and incompatible with the rest of the firmware, so the prebuilt library is the default.
  • morselib is compiled against newlib, so on -nostdlib ports the driver links the target's libc/libm and ships weak newlib syscall stubs to satisfy it (a failing _sbrk, so the C library never touches MicroPython's heap). This is contained entirely within the driver and costs nothing when HaLow is disabled.
  • The cooperative-scheduler integration avoids pulling in a vendor RTOS; it reuses the MicroPython scheduler the same way the cyw43 driver does.

Generative AI

I used generative AI tools when creating this PR, but a human has checked the code and is responsible for the code and the description above.

@kwagyeman kwagyeman moved this to In progress in OpenMV Features Aug 13, 2026
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.58%. Comparing base (83f92d5) to head (23a14ee).
⚠️ Report is 17 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #19617      +/-   ##
==========================================
+ Coverage   98.55%   98.58%   +0.03%     
==========================================
  Files         182      182              
  Lines       23320    23322       +2     
  Branches        5        5              
==========================================
+ Hits        22984    22993       +9     
+ Misses        335      328       -7     
  Partials        1        1              
Flag Coverage Δ
unix-coverage-32bit 98.59% <ø> (+0.03%) ⬆️
unix-coverage-64bit 98.52% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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 <kwagyeman@live.com>
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 <kwagyeman@live.com>
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 <kwagyeman@live.com>
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 <kwagyeman@live.com>
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

Code size report:

Reference:  stm32/mpu: Reuse MPU_ATTRIBUTES_NUMBER0 for all non-cacheable uses. [1827631]
Comparison: alif/boards/OPENMV_AE3: Enable HaLow. [merge of 23a14ee]
  mpy-cross:    +0 +0.000% 
   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:    +0 +0.000% standard
      stm32:    +0 +0.000% PYBV10
      esp32:    +0 +0.000% ESP32_GENERIC
     mimxrt:   -16 -0.004% TEENSY40
        rp2:    +0 +0.000% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@kwagyeman
kwagyeman force-pushed the kwabena/network_halow_upstream branch from e651f0f to fc8e8eb Compare August 13, 2026 20:59
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 <kwagyeman@live.com>
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 <kwagyeman@live.com>
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 <kwagyeman@live.com>
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 <kwagyeman@live.com>
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 <kwagyeman@live.com>
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 <kwagyeman@live.com>
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 <kwagyeman@live.com>
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 <kwagyeman@live.com>
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 <kwagyeman@live.com>
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 <kwagyeman@live.com>
@kwagyeman
kwagyeman force-pushed the kwabena/network_halow_upstream branch from fc8e8eb to 23a14ee Compare August 13, 2026 21:11
@kwagyeman

kwagyeman commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

@dpgeorge - I added this to OpenMV's feature list tracker. I understand it will take a while for this to be reviewed and that the review will be lengthy. However, it's now working. Note that the entire PR is AI-generated and verified on real hardware. I drove the AI to max performance and fixed all encountered bugs. Any issues we encountered during bring-up were not overlooked; they were resolved. Original performance when the driver was complete was like < 100KB/s. Scan and association were also problematic along with crashing all over. All that has been fixed and driven to be good. Whatever changes you want to make that are possible given the limitations of the Morse microdriver can be done quickly and reverified thanks to the HIL I set up to test this code. I have a test system for the RT1062, N6, and AE3.

You need to buy this to actually test against the shields I sent you. Let me know when I should send you one: https://www.mouser.com/en/ProductDetail/Morse-Micro/MM-HL2-EXT-A-AU?qs=sGAEpiMZZMv0DJfhVcWlK%2FsNvTRZof5PRDeHTp4nalmrn%252B38j3g9qA%3D%3D

# Set COUNTRY / SSID / KEY below to match your access point.

import time
import network

# --- Access point -----------------------------------------------------------
COUNTRY = "US"                 # your regulatory domain, e.g. "US", "AU", "EU", "JP"
SSID = "ssid-halow"
KEY = "your-password"

# --- Bring up HaLow ---------------------------------------------------------
network.country(COUNTRY)
wlan = network.HALOW()
wlan.active(True)
wlan.connect(SSID, KEY)

print("HaLow: associating with %r ..." % SSID)
deadline = time.ticks_add(time.ticks_ms(), 30000)
while not wlan.isconnected():
    if time.ticks_diff(deadline, time.ticks_ms()) < 0:
        raise RuntimeError("HaLow association timed out")
    time.sleep_ms(200)

ip = wlan.ifconfig()[0]
print("HaLow: connected  ip=%s  rssi=%d dBm  channel=%d  bandwidth=%d MHz"
      % (ip, wlan.status("rssi"), wlan.config("channel"), wlan.config("bandwidth")))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

1 participant