mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 21:08:19 +00:00
linux native fixup #1
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = <SX126X_DIO3_TCXO_1V8>;
|
||||
tcxo-power-startup-delay-ms = <5>;
|
||||
tcxo-power-startup-delay-ms = <10>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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=<N> 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 <zephyr/device.h>
|
||||
#include <zephyr/drivers/entropy.h>
|
||||
#include <zephyr/init.h>
|
||||
#include <zephyr/sys/util.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zephyr/arch/posix/posix_trace.h>
|
||||
#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);
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user