linux native fixup #1

This commit is contained in:
liquidraver
2026-06-01 22:29:53 +02:00
parent 09074d8852
commit eded1d7f04
8 changed files with 365 additions and 56 deletions
+28 -8
View File
@@ -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
+11 -1
View File
@@ -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
+42 -21
View File
@@ -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();
+26
View File
@@ -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.
+19 -1
View File
@@ -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;