From eded1d7f04a631c1838b08b6ed55e207d813bc79 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:22:58 +0200 Subject: [PATCH] linux native fixup #1 --- zephcore/LINUX_NATIVE.md | 36 ++++- zephcore/adapters/rng/ZephyrRNG.cpp | 12 +- .../adapters/transport/LinuxTCPTransport.c | 63 +++++--- zephcore/boards/common/linux_common.conf | 26 ++++ zephcore/boards/common/linux_common.overlay | 20 ++- .../drivers/entropy/fake_entropy_native_sim.c | 142 ++++++++++++++++++ .../drivers/gpio/gpio_native_linux.c | 72 +++++++-- .../drivers/gpio/gpio_native_linux_adapt.c | 50 ++++-- 8 files changed, 365 insertions(+), 56 deletions(-) create mode 100644 zephcore/patches/zephyr-new/drivers/entropy/fake_entropy_native_sim.c diff --git a/zephcore/LINUX_NATIVE.md b/zephcore/LINUX_NATIVE.md index 63c310c..acb43fc 100644 --- a/zephcore/LINUX_NATIVE.md +++ b/zephcore/LINUX_NATIVE.md @@ -2,7 +2,7 @@ Run ZephCore as a native Linux process on SBCs like the **Femtofox** (Luckfox Pico Mini + E22-900M30S) or **Raspberry Pi 4/5 + RAK6421 HAT**. The full mesh stack runs on top of Zephyr's `native_sim` board, talking to real SPI/GPIO via `/dev/spidev*` and `libgpiod v2`. -The companion app connects via a TCP socket on port **5000** (matching upstream MeshCore Linux convention), so the same mobile app TCP config works against both ZephCore Linux and MeshCore Linux. +The companion app connects via a TCP socket on port **5000**. Wire format is **raw NUS bytes with no length prefix** — each BLE NUS write becomes one TCP write, matching the MeshCore Windows companion "Connect via WiFi" protocol. --- @@ -25,6 +25,15 @@ On the **target SBC** (where the binary runs): sudo usermod -a -G spi,gpio $USER ``` +**Femtofox only:** the Femtofox image ships with `meshtasticd` pre-installed and it holds +the SPI bus / GPIO lines. Uninstall it before running ZephCore or the radio will be +unavailable. Use `foxbuntu-config` → uninstall meshtasticd, or: + +```bash +sudo systemctl stop meshtasticd +sudo apt remove meshtasticd +``` + No userspace library dependencies — the GPIO driver talks to `/dev/gpiochipN` directly via the kernel's GPIO V2 chardev uAPI (ioctl). Requires Linux ≥ 5.10 (released Dec 2020 — every modern SBC distro has it). The SPI driver uses @@ -32,7 +41,9 @@ spidev the same way (no library either). ### Enabling spidev on the SBC -**Femtofox / Luckfox Pico Mini:** edit `/etc/luckfox.conf` to enable SPI0, then reboot. +**Femtofox / Luckfox Pico Mini:** the official Femtofox image ships with SPI and GPIO +already enabled — `/dev/spidev0.0` and `/dev/gpiochip0`–`3` are present out of the box. +No extra configuration needed; skip straight to adding your user to the `spi`/`gpio` groups. **Raspberry Pi:** add `dtparam=spi=on` to `/boot/firmware/config.txt` (RPi 5) or `/boot/config.txt` (RPi 4) and reboot. Verify `/dev/spidev0.0` exists. @@ -58,8 +69,11 @@ TCP companion transport listening on :5000 ### 2. Femtofox (Luckfox Pico Mini, ARMv7-A) +Note: use `native_sim` (32-bit), **not** `native_sim/native/64` — the Luckfox Pico Mini +is ARMv7-A (32-bit) and the 64-bit variant will error at CMake configure time. + ```bash -west build -b native_sim/native/64 zephcore --pristine -- \ +west build -b native_sim zephcore -- \ -DZEPHYR_TOOLCHAIN_VARIANT=cross-compile \ -DNATIVE_TARGET_HOST=arm \ -DCROSS_COMPILE=/usr/bin/arm-linux-gnueabihf- \ @@ -137,12 +151,16 @@ The binary prints its PTY path and TCP listen port at startup. Logs go to stderr ### Companion app connection -In the ZephCore companion mobile app, choose TCP/Network mode and connect to: +Use the MeshCore companion app's **"Connect via WiFi"** (TCP/Network) mode: - **Host:** the SBC's IP address - **Port:** `5000` (default; configurable via `CONFIG_ZEPHCORE_LINUX_TCP_PORT`) -The wire framing is **2-byte big-endian length prefix + raw NUS payload**, matching upstream MeshCore Linux. Only one client connects at a time. +Wire format: **MeshCore SerialWifiInterface framing** (matches ESP32 Arduino reference in `src/helpers/esp32/SerialWifiInterface.cpp`): +- App → Node: `['<' (0x3C)][length_LSB][length_MSB][NUS payload...]` +- Node → App: `['>' (0x3E)][length_LSB][length_MSB][NUS payload...]` + +Only one client connects at a time. ### Repeater CLI @@ -204,7 +222,7 @@ west build … -- -DCONFIG_ZEPHCORE_LINUX_TCP_PORT=15000 ### "TCP companion client disconnected" loops -The wire framing is `2-byte BE length + payload`. If the companion app sends a different framing (raw NUS bytes, or 1-byte length, etc.), the transport will read a bogus length and bail. Verify the app is configured for MeshCore Linux TCP mode (port 5000 framing), not BLE-direct. +The transport expects raw NUS bytes with no length prefix. If the connection drops immediately, capture traffic with `tcpdump -i any -X port 5000` and verify the first bytes the app sends look like a MeshCore opcode (e.g. `0x01` = CMD_APP_START), not an HTTP request or other framed protocol. ### LoRa packets fly out but nothing receives them @@ -300,10 +318,12 @@ Build command verified (run from inside WSL with workspace at `/mnt/d/zephcore`) ```bash export PATH="$HOME/.local/bin:$PATH" export ZEPHYR_BASE=/mnt/d/zephcore/zephyr -cd /mnt/d/zephcore && west build -b native_sim/native/64 zephcore --pristine +cd /mnt/d/zephcore && west build -b native_sim zephcore ``` -The default 32-bit `native_sim` variant additionally requires `libc6-dev-i386`; the 64-bit variant works with stock `libc6-dev`. +Use `native_sim` (32-bit) for x86-64 host smoke builds and for ARM cross-compile targets +(Femtofox). Use `native_sim/native/64` only if targeting a 64-bit host natively (no +cross-compile). The 32-bit variant requires `libc6-dev-i386` on the build host. ### Known caveats found during implementation diff --git a/zephcore/adapters/rng/ZephyrRNG.cpp b/zephcore/adapters/rng/ZephyrRNG.cpp index 93c575b..000e6b7 100644 --- a/zephcore/adapters/rng/ZephyrRNG.cpp +++ b/zephcore/adapters/rng/ZephyrRNG.cpp @@ -220,7 +220,14 @@ void ZephyrRNG::mixIdentitySeed(uint8_t *out, size_t out_len, for (size_t i = 0; i < n; i++) pool[80 + i] ^= extra[i]; } - /* Stage 4: CPU cycle-counter jitter, 200ms */ + /* Stage 4: CPU cycle-counter jitter, 200ms. + * + * Skipped on POSIX arch (native_sim / Linux): the simulated clock only + * advances when Zephyr threads yield, so k_uptime_get() is frozen while + * this loop spins → infinite loop. On Linux we have /dev/urandom (via + * sys_csrand_get in stages 1 and 5) which is a far stronger source than + * jitter sampling anyway. */ +#ifndef CONFIG_ARCH_POSIX bool health_ok = sample_cpu_jitter(pool, sizeof(pool), 112, 200); if (!health_ok) { printk("ZephyrRNG: jitter health check failed, resampling 400ms\n"); @@ -229,13 +236,16 @@ void ZephyrRNG::mixIdentitySeed(uint8_t *out, size_t out_len, printk("ZephyrRNG: jitter health still failing — continuing with mixed sources\n"); } } +#endif /* CONFIG_ARCH_POSIX */ /* Stage 5: late CSPRNG — catches any mid-boot radio init that * warmed the TRNG during the 200ms jitter window */ (void)sys_csrand_get(pool + 368, 64); /* Stage 6: second jitter sample, independent timing window */ +#ifndef CONFIG_ARCH_POSIX (void)sample_cpu_jitter(pool, sizeof(pool), 432, 50); +#endif /* CONFIG_ARCH_POSIX */ /* Final conditioning: AES-256-CTR over the pool. Extracts a 32-byte * AES key via SHA-256(pool), then expands to out_len bytes via diff --git a/zephcore/adapters/transport/LinuxTCPTransport.c b/zephcore/adapters/transport/LinuxTCPTransport.c index f6cc3fc..14efa2b 100644 --- a/zephcore/adapters/transport/LinuxTCPTransport.c +++ b/zephcore/adapters/transport/LinuxTCPTransport.c @@ -3,15 +3,22 @@ * ZephCore TCP companion transport — drop-in replacement for ZephyrBLE. * * Implements the exact zephcore_ble_* C API from adapters/ble/ZephyrBLE.h - * over a TCP socket. Matches the wire format used by upstream MeshCore's - * Linux companion service (port 5000 by default): each frame is prefixed - * by a 2-byte big-endian length, followed by the raw NUS payload. + * over a TCP socket. + * + * Wire format: MeshCore SerialWifiInterface framing (ESP32 Arduino reference + * implementation: src/helpers/esp32/SerialWifiInterface.cpp): + * + * App → Node: [ '<' (0x3C) | length_LSB | length_MSB | payload... ] + * Node → App: [ '>' (0x3E) | length_LSB | length_MSB | payload... ] + * + * 3-byte header: 1-byte direction marker + 2-byte LE payload length. + * Frames with type != '<' are silently skipped (matches Arduino behavior). * * A single client at a time is supported (one BT_MAX_CONN=1 analogue). * * Architecture: - * - Listen thread accepts a client, then loops reading framed packets - * and posting them to ble_recv_queue + firing on_rx_frame. + * - Listen thread accepts a client, then loops reading frames and + * firing on_rx_frame (which queues to ble_recv_queue via the callback). * - TX is a work-queue item driven by zephcore_ble_kick_tx(), draining * ble_send_queue with zsock_send() until empty or the socket dies. * @@ -131,7 +138,6 @@ static void tx_drain_work_fn(struct k_work *work) ARG_UNUSED(work); struct frame f; - uint8_t hdr[2]; while (k_msgq_get(&ble_send_queue, &f, K_NO_WAIT) == 0) { k_mutex_lock(&sock_mu, K_FOREVER); @@ -144,10 +150,14 @@ static void tx_drain_work_fn(struct k_work *work) continue; } - hdr[0] = (uint8_t)(f.len >> 8); - hdr[1] = (uint8_t)(f.len & 0xff); + /* SerialWifiInterface framing: '>' + length LE + payload */ + uint8_t hdr[3]; - int err = sock_send_all(fd, hdr, 2); + hdr[0] = '>'; + hdr[1] = (uint8_t)(f.len & 0xFF); + hdr[2] = (uint8_t)(f.len >> 8); + + int err = sock_send_all(fd, hdr, 3); if (err == 0) { err = sock_send_all(fd, f.buf, f.len); @@ -243,19 +253,30 @@ static void listen_thread_fn(void *a, void *b, void *c) transport_cbs->on_connected(); } - /* RX loop on this client until it disconnects. */ + /* RX loop: SerialWifiInterface framing. + * Each frame: ['<':1][length:2LE][payload]. + * Frames with type != '<' are skipped (matches Arduino). */ while (true) { - uint8_t hdr[2]; - int err = sock_recv_all(fd, hdr, 2); + uint8_t hdr[3]; + int err = sock_recv_all(fd, hdr, 3); if (err != 0) { break; } - uint16_t flen = ((uint16_t)hdr[0] << 8) | hdr[1]; - if (flen == 0 || flen > MAX_FRAME_SIZE) { - LOG_WRN("Invalid frame length %u, closing", flen); - break; + uint16_t flen = (uint16_t)hdr[1] | ((uint16_t)hdr[2] << 8); + + /* Skip frames not from app ('<'), or bad length. */ + if (hdr[0] != '<' || flen == 0 || flen > MAX_FRAME_SIZE) { + /* Drain and discard the payload. */ + for (uint16_t i = 0; i < flen && flen <= MAX_FRAME_SIZE; i++) { + uint8_t discard; + if (sock_recv_all(fd, &discard, 1) != 0) { + goto client_done; + } + } + LOG_WRN("Skipping frame: type=0x%02x len=%u", hdr[0], flen); + continue; } struct frame f; @@ -266,15 +287,15 @@ static void listen_thread_fn(void *a, void *b, void *c) break; } - if (k_msgq_put(&ble_recv_queue, &f, K_NO_WAIT) != 0) { - LOG_WRN("RX queue full, dropping frame"); - continue; - } - + /* Do NOT k_msgq_put here — ble_on_rx_frame() in + * main_companion.cpp handles queueing, matching the + * ZephyrBLE.cpp pattern (secure_nus_rx_write just calls + * on_rx_frame without touching the recv queue). */ if (transport_cbs && transport_cbs->on_rx_frame) { transport_cbs->on_rx_frame(f.buf, f.len); } } + client_done: k_mutex_lock(&sock_mu, K_FOREVER); close_client_locked(); diff --git a/zephcore/boards/common/linux_common.conf b/zephcore/boards/common/linux_common.conf index 50ee1e8..77a8e92 100644 --- a/zephcore/boards/common/linux_common.conf +++ b/zephcore/boards/common/linux_common.conf @@ -92,4 +92,30 @@ CONFIG_I2C=n CONFIG_SENSOR=n CONFIG_GNSS=n CONFIG_MODEM_MODULES=n +# ========== Storage — LittleFS on native_sim simulated flash ========== +# The sim-flash controller is auto-selected by the zephyr,sim-flash DTS node. +# Flash map + LittleFS must be explicitly enabled; FSTAB automount reads the +# DTS fstab node added in linux_common.overlay. +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_FILE_SYSTEM=y +CONFIG_FILE_SYSTEM_LITTLEFS=y +CONFIG_FUSE_FS_ACCESS=n + +# ========== Logging ========== +# LOG_BACKEND_NATIVE_POSIX is default y on ARCH_POSIX — writes to stdout. +# Enable LOG so radio/mesh activity appears in the terminal. +CONFIG_LOG=y +# 3 = INF +CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_LOG_BUFFER_SIZE=4096 +CONFIG_LOG_PROCESS_TRIGGER_THRESHOLD=1 +CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=2048 +# Silence noisy subsystems +CONFIG_NET_LOG=n + # Crypto: native_sim runs PSA in software, same as MCU. +# Entropy: fake_entropy_native_sim.c is replaced by our patches/zephyr-new/ +# version that reads /dev/urandom instead of seeded libc random(). +# No Kconfig change needed — FAKE_ENTROPY_NATIVE_SIM=y is still selected +# by the DT node; we just ship a better implementation of the driver. diff --git a/zephcore/boards/common/linux_common.overlay b/zephcore/boards/common/linux_common.overlay index c2c835b..3761760 100644 --- a/zephcore/boards/common/linux_common.overlay +++ b/zephcore/boards/common/linux_common.overlay @@ -24,6 +24,24 @@ status = "disabled"; }; +/* LittleFS on native_sim's simulated flash. + * The native_sim board DTS allocates the first 1MB (0x0..0xfffff) for + * MCUBoot + OTA slots. The second half (0x100000..0x1fffff = 1MB) is unused. + * Use 512KB of it for LittleFS — 128 × 4KB blocks, plenty for identity, + * prefs, contacts, and channels. + * The sim-flash is in-memory (lost on exit); add + * CONFIG_FLASH_SIMULATOR_STATS_FILE for file-backed persistence. */ +&flash0 { + partitions { + lfs_partition: partition@100000 { + label = "lfs"; + reg = <0x00100000 0x00080000>; /* 512 KB */ + }; + }; +}; + +#include "filesystem.dtsi" + /* Disable the SDL emulated display + touch (we don't use them and the * SDL2 dev library isn't a useful dep on a headless SBC). */ &sdl_dc { @@ -91,7 +109,7 @@ rx-enable-gpios = <&zephcore_gpio0 24 GPIO_ACTIVE_HIGH>; dio2-tx-enable; dio3-tcxo-voltage = ; - tcxo-power-startup-delay-ms = <5>; + tcxo-power-startup-delay-ms = <10>; }; }; }; diff --git a/zephcore/patches/zephyr-new/drivers/entropy/fake_entropy_native_sim.c b/zephcore/patches/zephyr-new/drivers/entropy/fake_entropy_native_sim.c new file mode 100644 index 0000000..64408ff --- /dev/null +++ b/zephcore/patches/zephyr-new/drivers/entropy/fake_entropy_native_sim.c @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2018 Oticon A/S + * Copyright (c) 2026 ZephCore + * + * SPDX-License-Identifier: Apache-2.0 + * + * ZephCore replacement for Zephyr's fake_entropy_native_sim.c. + * + * Reads entropy from /dev/urandom via NSI host trampolines — same DTS + * compatible (zephyr,native-sim-rng), same Kconfig symbol + * (FAKE_ENTROPY_NATIVE_SIM), no new DTS plumbing needed. + * + * Differences from upstream: + * - Reads real entropy from /dev/urandom on every call. + * - No "WARNING: Using a test - not safe - entropy source" printed. + * - --seed / --seed-random CLI options kept for reproducible test runs: + * pass --seed= to force a fixed seed (falls back to libc random()). + * - Default (no CLI args): /dev/urandom, proper entropy, no warning. + */ + +#define DT_DRV_COMPAT zephyr_native_sim_rng + +#include +#include +#include +#include +#include +#include +#include +#include "cmdline.h" +#include "posix_native_task.h" +#include "nsi_host_trampolines.h" + +/* O_RDONLY = 0 on every Linux ABI we target. */ +#define NSI_O_RDONLY 0 + +static unsigned int seed; +static bool use_seed; /* true → fall back to seeded libc random() */ +static bool seed_set; /* set by --seed CLI callback */ + +static int entropy_native_sim_get_entropy(const struct device *dev, + uint8_t *buffer, uint16_t length) +{ + ARG_UNUSED(dev); + + if (use_seed) { + /* Reproducible mode: use libc random() (same as upstream). */ + while (length) { + long value = nsi_host_random(); + size_t to_copy = MIN(length, 3); + + memcpy(buffer, &value, to_copy); + buffer += to_copy; + length -= to_copy; + } + return 0; + } + + /* Default: read from /dev/urandom. */ + int fd = nsi_host_open("/dev/urandom", NSI_O_RDONLY); + + if (fd < 0) { + posix_print_error_and_exit("entropy: failed to open " + "/dev/urandom\n"); + return -EIO; /* unreachable */ + } + + while (length > 0) { + long n = nsi_host_read(fd, buffer, length); + + if (n <= 0) { + nsi_host_close(fd); + posix_print_error_and_exit("entropy: read from " + "/dev/urandom failed\n"); + return -EIO; + } + buffer += n; + length -= (uint16_t)n; + } + + nsi_host_close(fd); + return 0; +} + +static int entropy_native_sim_get_entropy_isr(const struct device *dev, + uint8_t *buf, uint16_t len, + uint32_t flags) +{ + ARG_UNUSED(flags); + entropy_native_sim_get_entropy(dev, buf, len); + return len; +} + +static int entropy_native_sim_init(const struct device *dev) +{ + ARG_UNUSED(dev); + if (seed_set) { + /* User passed --seed: reproducible mode. */ + use_seed = true; + nsi_host_srandom(seed); + } + /* No warning: /dev/urandom is a legitimate entropy source. */ + return 0; +} + +static DEVICE_API(entropy, entropy_native_sim_api_funcs) = { + .get_entropy = entropy_native_sim_get_entropy, + .get_entropy_isr = entropy_native_sim_get_entropy_isr, +}; + +DEVICE_DT_INST_DEFINE(0, + entropy_native_sim_init, NULL, + NULL, NULL, + PRE_KERNEL_1, CONFIG_ENTROPY_INIT_PRIORITY, + &entropy_native_sim_api_funcs); + +static void seed_was_set(char *argv, int offset) +{ + ARG_UNUSED(argv); + ARG_UNUSED(offset); + seed_set = true; +} + +static void add_fake_entropy_option(void) +{ + static struct args_struct_t entropy_options[] = { + { + .option = "seed", + .name = "r_seed", + .type = 'u', + .dest = (void *)&seed, + .call_when_found = seed_was_set, + .descript = "Fix the entropy seed for reproducible runs " + "(disables /dev/urandom). E.g. --seed=97229", + }, + ARG_TABLE_ENDMARKER, + }; + + native_add_command_line_opts(entropy_options); +} + +NATIVE_TASK(add_fake_entropy_option, PRE_BOOT_1, 10); diff --git a/zephcore/patches/zephyr-new/drivers/gpio/gpio_native_linux.c b/zephcore/patches/zephyr-new/drivers/gpio/gpio_native_linux.c index 9ef9adb..2792077 100644 --- a/zephcore/patches/zephyr-new/drivers/gpio/gpio_native_linux.c +++ b/zephcore/patches/zephyr-new/drivers/gpio/gpio_native_linux.c @@ -62,8 +62,12 @@ struct gpio_native_linux_data { struct k_mutex mu; struct k_thread evt_thread; - K_KERNEL_STACK_MEMBER(evt_thread_stack, - CONFIG_ARCH_POSIX_RECOMMENDED_STACK_SIZE); + /* K_THREAD_STACK_MEMBER expands to a section attribute that is only + * valid at file scope — illegal inside a struct on native_sim. + * K_KERNEL_STACK_MEMBER is struct-safe. Use a literal 2048 rather + * than CONFIG_ARCH_POSIX_RECOMMENDED_STACK_SIZE, which evaluates to + * 24 bytes on a cross-compiled native_sim ARM build. */ + K_KERNEL_STACK_MEMBER(evt_thread_stack, 2048); bool evt_thread_started; struct k_sem reconfig_sem; const struct device *self; @@ -96,7 +100,11 @@ static int rebuild_line(const struct device *dev, gpio_pin_t pin) gpio_flags_t f = p->cfg_flags; - if ((f & GPIO_DISCONNECTED) == GPIO_DISCONNECTED) { + /* "Disconnected" means neither INPUT nor OUTPUT is requested. Note + * GPIO_DISCONNECTED is 0, so the naive test (f & GPIO_DISCONNECTED) + * is always true and would skip requesting EVERY line -- mask the + * direction bits explicitly instead. */ + if ((f & (GPIO_INPUT | GPIO_OUTPUT)) == GPIO_DISCONNECTED) { return 0; } @@ -159,6 +167,9 @@ static int gnl_pin_configure(const struct device *dev, gpio_pin_t pin, k_mutex_lock(&data->mu, K_FOREVER); data->pins[pin].cfg_flags = flags; ret = rebuild_line(dev, pin); + LOG_WRN("pin_configure: dev=%p data=%p pin=%u flags=0x%x ret=%d requested=%d", + (void *)dev, (void *)data, pin, flags, ret, + data->pins[pin].requested); k_mutex_unlock(&data->mu); /* Wake the event thread to re-collect fds. */ @@ -173,6 +184,9 @@ static int gnl_port_get_raw(const struct device *dev, gpio_port_value_t *value) struct gpio_native_linux_data *data = dev->data; gpio_port_value_t v = 0; + static int dbgn; + bool dbg = dbgn < 20; + k_mutex_lock(&data->mu, K_FOREVER); for (uint32_t i = 0; i < cfg->ngpios && i < GNL_MAX_PINS; i++) { struct gnl_pin_state *p = &data->pins[i]; @@ -182,12 +196,21 @@ static int gnl_port_get_raw(const struct device *dev, gpio_port_value_t *value) } int x = gnl_line_get_value(p->line); + if (dbg) { + LOG_WRN("port_get_raw: pin %u requested, value=%d", i, x); + } if (x > 0) { v |= ((gpio_port_value_t)1U << i); } } k_mutex_unlock(&data->mu); + if (dbg) { + LOG_WRN("port_get_raw: dev=%p data=%p portval=0x%08x", + (void *)dev, (void *)data, (uint32_t)v); + dbgn++; + } + *value = v; return 0; } @@ -344,25 +367,32 @@ static void gnl_evt_thread(void *arg1, void *arg2, void *arg3) k_mutex_unlock(&data->mu); if (nfds == 0) { - /* No edge-enabled pins: sleep waiting for reconfig. */ - (void)k_sem_take(&data->reconfig_sem, K_MSEC(1000)); - /* Drain any extra gives during the sleep. */ + /* No edge-enabled pins: sleep properly so the Zephyr CPU + * is released (unlike blocking poll which holds it). */ + k_sleep(K_MSEC(10)); while (k_sem_take(&data->reconfig_sem, K_NO_WAIT) == 0) { } continue; } - /* Poll with 200 ms cap so reconfig_sem wakeups stay responsive. */ - uint32_t ready_mask = gnl_poll_fds(fds, nfds, 200); + /* Non-blocking poll: check for events instantly without holding + * the Zephyr CPU mutex (a blocking poll blocks ALL Zephyr threads + * for its duration, starving the SX126x work queue and causing + * CAD/TX-DONE timeouts). + * + * Then k_sleep(1ms): properly releases the Zephyr CPU to any + * thread (unlike k_yield which only yields to same-priority). + * During the 1ms, the SX126x work queue processes the IRQ and + * signals cad_sem/tx_done. GPIO event latency ≤ 1ms — well + * within the 200ms CAD and TX_DONE timeout budgets. + * + * TODO: replace with NSI interrupt model for true zero latency. */ + uint32_t ready_mask = gnl_poll_fds(fds, nfds, 0); - /* Process any reconfig requests first (cheap). */ + /* Drain reconfig requests. */ while (k_sem_take(&data->reconfig_sem, K_NO_WAIT) == 0) { } - if (ready_mask == 0) { - continue; - } - gpio_port_pins_t fired = 0; k_mutex_lock(&data->mu, K_FOREVER); @@ -384,6 +414,12 @@ static void gnl_evt_thread(void *arg1, void *arg2, void *arg3) if (fired != 0) { gpio_fire_callbacks(&data->callbacks, dev, fired); } + + /* Sleep 1ms every loop iteration — properly releases the Zephyr + * CPU to any ready thread (SX126x work queue, mesh event loop, + * timers). k_yield() is insufficient: it only yields to threads + * of the same cooperative priority. */ + k_sleep(K_MSEC(1)); } } @@ -409,7 +445,7 @@ static int gpio_native_linux_init(const struct device *dev) memset(data->pins, 0, sizeof(data->pins)); k_thread_create(&data->evt_thread, data->evt_thread_stack, - K_KERNEL_STACK_SIZEOF(data->evt_thread_stack), + 2048, /* literal: K_KERNEL_STACK_SIZEOF unreliable on cross-compiled native_sim */ gnl_evt_thread, (void *)dev, NULL, NULL, K_PRIO_COOP(7), 0, K_NO_WAIT); data->evt_thread_started = true; @@ -432,10 +468,16 @@ static int gpio_native_linux_init(const struct device *dev) \ static struct gpio_native_linux_data gpio_native_linux_data_##inst; \ \ + /* POST_KERNEL: k_thread_create with K_NO_WAIT requires the scheduler + * run queue to be initialized, which only happens by POST_KERNEL. + * PRE_KERNEL_1 crashes on native_sim (POSIX arch) because the dlist + * backing _kernel.ready_q is still NULL at that stage. + * GPIO_INIT_PRIORITY (40) < SPI_INIT_PRIORITY (70), both POST_KERNEL, + * so CS GPIO is still available when the SPI driver inits. */ \ DEVICE_DT_INST_DEFINE(inst, gpio_native_linux_init, NULL, \ &gpio_native_linux_data_##inst, \ &gpio_native_linux_cfg_##inst, \ - PRE_KERNEL_1, CONFIG_GPIO_INIT_PRIORITY, \ + POST_KERNEL, CONFIG_GPIO_INIT_PRIORITY, \ &gpio_native_linux_api); DT_INST_FOREACH_STATUS_OKAY(GPIO_NATIVE_LINUX_INIT) diff --git a/zephcore/patches/zephyr-new/drivers/gpio/gpio_native_linux_adapt.c b/zephcore/patches/zephyr-new/drivers/gpio/gpio_native_linux_adapt.c index 01a48d7..cbd92ea 100644 --- a/zephcore/patches/zephyr-new/drivers/gpio/gpio_native_linux_adapt.c +++ b/zephcore/patches/zephyr-new/drivers/gpio/gpio_native_linux_adapt.c @@ -105,9 +105,12 @@ gnl_line_t gnl_request_output(gnl_chip_t chip, unsigned int offset, int chip_fd = (int)(intptr_t)chip; uint64_t flags = GPIO_V2_LINE_FLAG_OUTPUT; - if (active_low) { - flags |= GPIO_V2_LINE_FLAG_ACTIVE_LOW; - } + /* Do NOT apply ACTIVE_LOW at the kernel level: Zephyr's generic GPIO + * layer already converts logical<->physical for GPIO_ACTIVE_LOW (and + * hands us a physical init value + raw set/get). Inverting again here + * double-inverts active-low pins (e.g. SX126x RESET), holding the chip + * in reset. The driver's port_*_raw contract is physical/raw. */ + (void)active_low; struct gnl_line_handle *h = do_request(chip_fd, offset, flags); @@ -130,16 +133,19 @@ static uint64_t input_flags(bool pull_up, bool pull_down, bool active_low) { uint64_t flags = GPIO_V2_LINE_FLAG_INPUT; - if (active_low) { - flags |= GPIO_V2_LINE_FLAG_ACTIVE_LOW; - } + /* See gnl_request_output: Zephyr's generic layer owns ACTIVE_LOW + * inversion; applying it here too would double-invert. */ + (void)active_low; if (pull_up) { flags |= GPIO_V2_LINE_FLAG_BIAS_PULL_UP; } else if (pull_down) { flags |= GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN; - } else { - flags |= GPIO_V2_LINE_FLAG_BIAS_DISABLED; } + /* else: leave bias AS-IS (no flag). Forcing BIAS_DISABLED can turn off + * the pin's input path on some SoCs (e.g. Rockchip), making a + * push-pull-driven input (SX126x BUSY) read stuck-low. libgpiod + * defaults to AS-IS, which is what meshtasticd uses to read these pins + * correctly. */ return flags; } @@ -203,7 +209,9 @@ int gnl_line_get_value(gnl_line_t line) memset(&vals, 0, sizeof(vals)); vals.mask = 1ULL; - if (ioctl(h->line_fd, GPIO_V2_LINE_GET_VALUES_IOCTL, &vals) < 0) { + int rc = ioctl(h->line_fd, GPIO_V2_LINE_GET_VALUES_IOCTL, &vals); + + if (rc < 0) { return -errno; } return (vals.bits & 1ULL) ? 1 : 0; @@ -223,7 +231,29 @@ int gnl_line_set_value(gnl_line_t line, int value) vals.mask = 1ULL; vals.bits = value ? 1ULL : 0ULL; - if (ioctl(h->line_fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &vals) < 0) { + int rc = ioctl(h->line_fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &vals); + + /* DIAGNOSTIC (temporary): confirm output writes reach the kernel and + * read the line back immediately to see if the drive physically took. */ + { + static int dbgn; + + if (dbgn < 40) { + dbgn++; + struct gpio_v2_line_values rb; + + memset(&rb, 0, sizeof(rb)); + rb.mask = 1ULL; + int grc = ioctl(h->line_fd, GPIO_V2_LINE_GET_VALUES_IOCTL, &rb); + + fprintf(stderr, + "GNL set_value: off=%u fd=%d val=%d set_rc=%d errno=%d readback=%d (grc=%d)\n", + h->offset, h->line_fd, value, rc, rc < 0 ? errno : 0, + (grc == 0) ? (int)(rb.bits & 1ULL) : -1, grc); + } + } + + if (rc < 0) { return -errno; } return 0;