From dee945cd6ac33c2b8209b47d1f17445d62bf2c9c Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Thu, 1 Oct 2026 23:57:06 +0000 Subject: [PATCH] CH341: fewer waits on the USB bus per radio command (#12013) * CH341: fewer waits on the USB bus per radio command Every RadioLib command to a radio behind a CH341 waits on the USB bus about five times: the BUSY read before it, CS low, the command, CS high, and the BUSY read after, with a 1 us settle between the last two that usleep() turns into ~50 us of timer slack. - A BUSY read that follows the read that found BUSY low after a command is answered from it, once, within 1 ms and only if nothing else has been sent to the adapter in between: RadioLib's wait before a command then costs nothing after the wait that ended the last one. - Delays under 100 us spin on the steady clock instead of sleeping. - With a libch341 that has pinedio_transceive_select(), and where Lora.CS is D0, the CS levels travel with the SPI transfer and RadioLib's separate CS writes are dropped. Against an older libch341 this part compiles out. The HAL learns the radio's CS and BUSY pins from initLoRa(), since USBHal.h is included by PortduinoGlue.h ahead of portduino_config. Measured on an SX1262 over a CH341 (SHORT_TURBO, 6 v 6 runs of 300 s): the three-command CAD setup fell 1.01 -> 0.62 ms, CAD arm to air 6.60 -> 5.23 ms, and total packet loss 1.51 -> 1.13% (p=0.048), with collisions 4.83 -> 0.67 per run (p=0.002). Co-Authored-By: Jonathan Bennett Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9 * Bump libch341-spi-userspace to 027cde5 for pinedio_transceive_select() Picks up libch341#6 (1 ms interrupt poll, poll thread woken on detach) and #7 (pinedio_transceive_select()), so the packed-CS path in USBHal.h builds in rather than compiling out. Co-Authored-By: Jonathan Bennett Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01REkPVFh6kvG4AZJ5A8AtM9 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Ben Meadors --- src/mesh/RadioInterface.cpp | 1 + src/platform/portduino/USBHal.h | 61 +++++++++++++++++++++++++++++++++ variants/native/portduino.ini | 2 +- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index c7ea0c23d..c35a2f125 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -423,6 +423,7 @@ std::unique_ptr initLoRa() portduino_config.lora_spi_dev.c_str()); if (portduino_config.lora_spi_dev == "ch341") { RadioLibHAL = ch341Hal.get(); // non-owning: the ch341 HAL stays owned by the global unique_ptr + ch341Hal->setRadioPins(portduino_config.lora_cs_pin.pin, portduino_config.lora_busy_pin.pin); } else { if (RadioLibHAL != nullptr) { delete RadioLibHAL; diff --git a/src/platform/portduino/USBHal.h b/src/platform/portduino/USBHal.h index e07fb70dc..7e3906bb2 100644 --- a/src/platform/portduino/USBHal.h +++ b/src/platform/portduino/USBHal.h @@ -4,10 +4,12 @@ // include RadioLib #include "platform/portduino/PortduinoGlue.h" #include +#include #include #include #include #include +#include #include // gettimeofday(), previously pulled in via libusb.h #include @@ -25,6 +27,9 @@ extern uint32_t rebootAtMsec; #define CH341_PIN_CS (101) #define CH341_PIN_IRQ (0) +// The adapter's chip select, D0, which pinedio_transceive_select() drives +#define CH341_PIN_D0 (0) + // the HAL must inherit from the base RadioLibHal class // and implement all of its virtual methods class Ch341Hal : public RadioLibHal @@ -59,6 +64,18 @@ class Ch341Hal : public RadioLibHal ~Ch341Hal() { pinedio_deinit(&pinedio); } + /** The radio's CS and BUSY pins, as handed to its RadioLib Module, so the HAL can save bus round trips on them */ + void setRadioPins(uint32_t csPin, uint32_t busyPin) + { + this->busyPin = busyPin; + busyLowAtUs.reset(); +#ifdef PINEDIO_HAS_TRANSCEIVE_SELECT + // pinedio_transceive_select() drives D0, so it only stands in for RadioLib's CS writes where D0 is the CS line + packedCs = csPin == CH341_PIN_D0; + LOG_INFO("CH341: CS %s", packedCs ? "carried in each SPI transfer" : "written separately, as it is not D0"); +#endif + } + void getSerialString(char *_serial, size_t len) { if (len == 0) @@ -101,6 +118,10 @@ class Ch341Hal : public RadioLibHal if (pin == RADIOLIB_NC) { return; } + busyLowAtUs.reset(); // a write can start the chip on something, so the last BUSY read no longer holds + if (packedCs && pin == CH341_PIN_D0) { + return; // spiTransfer() drives both levels itself + } auto res = pinedio_digital_write(&pinedio, pin, value); if (res < 0 && rebootAtMsec == 0) { LOG_ERROR("USBHal digitalWrite: Can't write pin %u: %d", pin, res); @@ -116,12 +137,26 @@ class Ch341Hal : public RadioLibHal if (pin == RADIOLIB_NC) { return 0; } + // RadioLib waits for BUSY low after each command and again before the next. With nothing sent to the chip in + // between, the second wait can only repeat the first, so it is answered from it, once, saving a round trip. + const bool busyStillLow = pin == busyPin && busyLowAtUs && micros() - *busyLowAtUs < BUSY_REUSE_US; + busyLowAtUs.reset(); + if (busyStillLow) { + return 0; + } auto res = pinedio_digital_read(&pinedio, pin); if (res < 0 && rebootAtMsec == 0) { LOG_ERROR("USBHal digitalRead: Can't read pin %u: %d", pin, res); portduino_status.LoRa_in_error = true; return 0; } + // The reads after a transfer are RadioLib's wait for BUSY low; the one that ends it is the one worth keeping + if (afterSpi && pin == busyPin && res == 0) { + busyLowAtUs = micros(); + } + if (pin != busyPin || res == 0) { + afterSpi = false; + } return res; } @@ -157,6 +192,13 @@ class Ch341Hal : public RadioLibHal sched_yield(); return; } + // usleep() rounds a few microseconds up to the timer slack, ~50 us, and RadioLib waits 1 us around each command + if (us < SPIN_DELAY_MAX_US) { + const auto until = std::chrono::steady_clock::now() + std::chrono::microseconds(us); + while (std::chrono::steady_clock::now() < until) { + } + return; + } usleep(us); } @@ -190,7 +232,16 @@ class Ch341Hal : public RadioLibHal if (checkError()) { return; } + busyLowAtUs.reset(); + afterSpi = true; +#ifdef PINEDIO_HAS_TRANSCEIVE_SELECT + // RadioLib does exactly one spiTransfer per CS window, so the chip sees the same traffic, but select, command + // and deselect cost one wait on the bus instead of three + int32_t ret = + packedCs ? pinedio_transceive_select(&this->pinedio, out, in, len) : pinedio_transceive(&this->pinedio, out, in, len); +#else int32_t ret = pinedio_transceive(&this->pinedio, out, in, len); +#endif if (ret < 0) { std::cerr << "Could not perform SPI transfer: " << ret << std::endl; } @@ -212,8 +263,18 @@ class Ch341Hal : public RadioLibHal } private: + static constexpr unsigned long BUSY_REUSE_US = 1000; + static constexpr unsigned long SPIN_DELAY_MAX_US = 100; + pinedio_inst pinedio = {0}; bool has_warned = false; + uint32_t busyPin = RADIOLIB_NC; + /** CS is D0 and the library can carry it in the SPI transfer */ + bool packedCs = false; + /** An SPI transfer went out and nothing has yet found BUSY low after it */ + bool afterSpi = false; + /** When the first read after a transfer found BUSY low; cleared once anything else touches the adapter */ + std::optional busyLowAtUs; }; #endif diff --git a/variants/native/portduino.ini b/variants/native/portduino.ini index ebc0d514a..8ae53ff0b 100644 --- a/variants/native/portduino.ini +++ b/variants/native/portduino.ini @@ -28,7 +28,7 @@ lib_deps = # renovate: datasource=custom.pio depName=LovyanGFX packageName=lovyan03/library/LovyanGFX lovyan03/LovyanGFX@1.2.28 ; # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/meshtastic/libch341-spi-userspace gitBranch=main - https://github.com/meshtastic/libch341-spi-userspace/archive/d85aceb760da291d90e0ff526ba17b2e1323d988.zip + https://github.com/meshtastic/libch341-spi-userspace/archive/027cde5d9c0cecc91fffbd2d1ed4e41e32eeda79.zip # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library adafruit/Adafruit seesaw Library@1.7.9 # renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main