Skip to content

lora: replace custom fri3d_2026 SX1262 driver with upstream micropython-lib driver + adapter - #231

Draft
ThomasFarstrike wants to merge 62 commits into
mainfrom
lora-upstream
Draft

ThomasFarstrike wants to merge 62 commits into
mainfrom
lora-upstream

Conversation

@ThomasFarstrike

Copy link
Copy Markdown
Contributor

Replace the 2426-line forked drivers/lora/sx1262.py with the official upstream micropython-lib lora driver (4 files, ~1450 lines verbatim) plus thin adapter layers that encapsulate all board-specific quirks.

New files:
lib/lora/ - upstream lora package (modem, sx126x, sync_modem, init)
mpos/lora_spi_adapter.py - wraps SPI.Device as standard machine.SPI
mpos/lora_adapter.py - wraps upstream SX1262 in existing caller API

Enhanced:
mpos/lora_manager.py - acquire/release lock, CH32 reset_chip(),
optional watchdog with auto-recovery

Updated callers:
board/fri3d_2026.py - import from mpos.lora_adapter
apps/lora_chat/lora_chat.py - import + acquire/release + watchdog

Deleted:
drivers/lora/sx1262.py - replaced by upstream + adapter

Single upstream deviation: from machine import Pin guarded with try/except in sx126x.py (Unix desktop port has no machine.Pin). No other changes to upstream files.

LoRaManager now provides:
acquire(app_name)/release(app_name) - exclusive radio lock
reset_chip() - CH32 IO expander reset
start_watchdog()/stop_watchdog() - auto-recovery from SPI wedges
is_healthy() - status probe

micropySX126X/ (lilygo watcher driver) left untouched for a follow-up PR.

Refs: PR #222, issue #229

…on-lib + adapter

Replace the 2426-line forked drivers/lora/sx1262.py with the official
upstream micropython-lib lora driver (4 files, ~1450 lines verbatim)
plus thin adapter layers that encapsulate all board-specific quirks.

New files:
  lib/lora/                 - upstream lora package (modem, sx126x, sync_modem, __init__)
  mpos/lora_spi_adapter.py  - wraps SPI.Device as standard machine.SPI
  mpos/lora_adapter.py      - wraps upstream SX1262 in existing caller API

Enhanced:
  mpos/lora_manager.py      - acquire/release lock, CH32 reset_chip(),
                               optional watchdog with auto-recovery

Updated callers:
  board/fri3d_2026.py       - import from mpos.lora_adapter
  apps/lora_chat/lora_chat.py - import + acquire/release + watchdog

Deleted:
  drivers/lora/sx1262.py    - replaced by upstream + adapter

Single upstream deviation: from machine import Pin guarded with
try/except in sx126x.py (Unix desktop port has no machine.Pin).
No other changes to upstream files.

LoRaManager now provides:
  acquire(app_name)/release(app_name)  - exclusive radio lock
  reset_chip()                         - CH32 IO expander reset
  start_watchdog()/stop_watchdog()     - auto-recovery from SPI wedges
  is_healthy()                         - status probe

micropySX126X/ (lilygo watcher driver) left untouched for a follow-up PR.

Refs: PR #222, issue #229
@github-actions

github-actions Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

_irq_handler called start_recv(continuous=True) on TX_DONE but
start_recv checks self._tx and returns early if it's still True.
Since the adapter's non-blocking send() path never calls poll_send(),
self._tx stayed True permanently — the radio never re-entered
continuous RX after the first TX, and subsequent sends wedged.

Fix: call poll_send() instead of start_recv() in the TX_DONE branch.
poll_send() clears self._tx, then _check_recv() properly restarts RX.
…me to 5ms

Two fixes in the adapter (no upstream changes):

1. Clear device errors before configure() in begin(). After TCXO init
   in the upstream constructor, the datasheet-expected XOSC_START_ERR
   (0x20) can reappear between __init__ and the first configure()
   call on some boards. _check_error() at the end of configure()
   treats it as fatal → RuntimeError. Clearing stale errors first
   fixes this; any genuine errors from configure() itself are still
   caught by its own _check_error() call.

2. Bump dio3_tcxo_start_time_us from 1000→5000 to match the old
   driver's default TCXO delay, giving slow-starting oscillators
   more margin.
lora_adapter.py:
- Add sys.print_exception(e) in both blocking and non-blocking send()
  branches so the actual exception is visible on serial (was swallowed
  by bare except: return 0, -1)
- Same in _irq_handler for poll_send() exceptions

lora_manager.py:
- reset_chip(): wrap exp.config writes with
  mpos.ui.task_handler.disable()/enable() to avoid I2C bus contention
  between the config write and LVGL's periodic expander reads
  (buttons, joystick). Without this, config register writes silently
  fail on CH32 firmware v2.0.1 — see issue #224.
- Use try/finally to guarantee task_handler is re-enabled even if the
  write raises.
The upstream prepare_send() calls _standby() with STDBY_XOSC mode (1),
which turns on the crystal oscillator. Per DS 13.3.6, this sets
XOSC_START_ERR (0x20). On some boards the XOSC stabilizes fast enough
that the flag clears before _check_error() runs — on others (socketed
modules, different TCXO path) it doesn't, causing RuntimeError on
every send.

Fix: _clear_errors() before prepare_send() in both blocking and
non-blocking paths. Safe because prepare_send() has its own
_check_error() that catches genuinely new errors from the standby
and setup commands.
Per DS 13.3.6, XOSC_START_ERR (0x20) is expected after any transition
that restarts the crystal oscillator — notably _standby() with
STDBY_XOSC mode (1), called by prepare_send() and start_recv().
The upstream constructor clears it once after TCXO init, but the error
reappears after every subsequent STDBY_XOSC transition. On some
hardware (socketed modules, slower oscillators) the flag stays set
long enough for the immediately-following _check_error() to raise.

Fix: skip raising on op_error 0x20. All other errors still raise.
This is the second upstream deviation in sx126x.py (after the
machine.Pin try/except for Unix desktop compat).
MicroPython const() values in compiled .mpy modules are compile-time
inlined and not importable across modules. The previous attempt to
import _IRQ_TX_DONE etc. from lora.sx126x fails at boot with
'ImportError: cant import name _IRQ_TX_DONE'.

Define the hardware register masks locally with private names
(_TX_DONE, _RX_DONE, _CRC_ERR) and expose them as public class
attributes (PolledSX126x.TX_DONE etc.).
Use SPI.Device.lock()/unlock() (C-level spi_device_acquire_bus /
spi_device_release_bus) in write_readinto() and readinto() so the shared
SPI bus can't be preempted between per-byte calls while CS is held low.

Closes the remaining gap from PR #222.
@ThomasFarstrike

ThomasFarstrike commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Integrating the upstream LoRa driver itself was relatively easy but so far I was unable to make the whole perfectly stable.

Some challenges:

  • The SPI bus being shared with the display (which is doing lots of SPI writes, especially during scrolling and animations) and also with the SD card, means there's no way to talk to the LoRa chip without blocking the display and the SD card.
  • MicroPython ignoring IRQ (interrupts) when the system is busy (like when updating the display etc)
  • There was also a bug in the lvgl_micropython SPI driver which prevented DMA transfers, now fixed
  • The LoRa chip seems to prefer byte-by-byte SPI communication instead of entire buffers, possibly due to some timing sensitivity?
  • The LoRa chip is throwing errors, which are accidentally ignored when doing byte-by-byte transfers but not when doing entire buffers?
  • Resetting the LoRa chip using the expander.config register had a bug (fri3d_2026: Expander.config register write doesn't take effect at runtime (CH32 fw v2.0.1) #224) which should now be fixed, but working around that also cost time and muddied the water
  • Testing the LoRa requires 3 devices (one for sending, one for receiving, one for observing) all having working software so it's quite tedious

So we made good progress, but since the final result isn't perfectly stable.

Meanwhile, people have built working apps on the existing driver, and Fri3d Camp 2026 is about to start.

So I think it's safer to play it safe by shipping only:

  • the SPI DMA fix
  • the SPI locking exposure

...while staying on the current LoRa driver for now.

Let's see how much the fixes help, how stable we can get the LoRa, and then revisit this.

@ThomasFarstrike
ThomasFarstrike marked this pull request as draft August 19, 2026 10:18

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant