native linux initial commit

This commit is contained in:
liquidraver
2026-05-24 20:06:49 +02:00
parent c7a00b9533
commit 051adef93e
26 changed files with 2736 additions and 1 deletions
+20 -1
View File
@@ -260,6 +260,8 @@ elseif(BOARD MATCHES ".*mg24.*" OR BOARD MATCHES ".*efr32.*")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/mg24_common.conf")
elseif(DEFINED BOARD_QUALIFIERS AND BOARD_QUALIFIERS MATCHES ".*mg24.*")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/mg24_common.conf")
elseif(BOARD MATCHES "native_sim" OR BOARD MATCHES "native_posix")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/linux_common.conf")
else()
set(ZEPHCORE_PLATFORM_CONF "")
endif()
@@ -537,13 +539,30 @@ else()
message(STATUS "ZephCore Role: COMPANION")
target_sources(app PRIVATE
src/main_companion.cpp
adapters/ble/ZephyrBLE.cpp
adapters/datastore/ZephyrDataStore.cpp
helpers/BaseChatMesh.cpp
helpers/TransportKeyStore.cpp
helpers/ui/ui_mesh_actions.cpp
app/CompanionMesh.cpp
)
# Companion transport: TCP socket on native Linux, BLE NUS on MCU builds.
if(CONFIG_ZEPHCORE_TRANSPORT_TCP)
message(STATUS "ZephCore Companion Transport: TCP (Linux)")
target_sources(app PRIVATE adapters/transport/LinuxTCPTransport.c)
# Headless Linux build: no display/buttons/buzzer source files are
# compiled, but companion code calls ui_* unconditionally. Weak no-op
# stubs satisfy the link.
target_sources(app PRIVATE helpers/ui/ui_headless_stubs.c)
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/helpers/ui
)
else()
message(STATUS "ZephCore Companion Transport: BLE NUS")
target_sources(app PRIVATE adapters/ble/ZephyrBLE.cpp)
endif()
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/adapters/transport
)
# USB CDC companion transport (only in debug builds with logging)
if(CONFIG_LOG)
target_sources(app PRIVATE
+26
View File
@@ -232,6 +232,32 @@ config ZEPHCORE_BLE_ADV_SLOW_INTERVAL
endmenu # BLE Configuration
menu "Linux Companion Transport (native_sim)"
config ZEPHCORE_TRANSPORT_TCP
bool "Use TCP socket transport instead of BLE NUS"
default n
help
Replaces the ZephyrBLE adapter with LinuxTCPTransport, exposing
the companion NUS protocol over a TCP socket on Linux SBC builds
(BOARD=native_sim). Auto-enabled by boards/common/linux_common.conf.
The wire format is a 2-byte big-endian length prefix followed by
the raw NUS payload, matching upstream MeshCore Linux companion
service convention.
config ZEPHCORE_LINUX_TCP_PORT
int "TCP listen port"
depends on ZEPHCORE_TRANSPORT_TCP
default 5000
range 1 65535
help
Port the companion TCP transport binds to. Default 5000 matches
the upstream MeshCore Linux companion service so a single mobile
app TCP configuration works against both backends.
endmenu # Linux Companion Transport
endif # ZEPHCORE_ROLE_COMPANION
config ZEPHCORE_HOUSEKEEPING_INTERVAL_MS
+312
View File
@@ -0,0 +1,312 @@
# ZephCore Native Linux Port
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.
---
## Prerequisites
On the **build host** (where you run `west build`):
```bash
# Zephyr SDK and west workspace as per the main CLAUDE.md.
# Cross-compile toolchains (for SBC targets):
sudo apt install gcc-arm-linux-gnueabihf # Femtofox (RV1103 ARMv7-A)
sudo apt install gcc-aarch64-linux-gnu # Raspberry Pi 4/5 (aarch64)
```
On the **target SBC** (where the binary runs):
```bash
# Grant your user access to /dev/spidev and /dev/gpiochip (or run as root).
sudo usermod -a -G spi,gpio $USER
```
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
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.
**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.
---
## Quick Start
### 1. Host smoke build (x86-64 Linux, no real radio)
```bash
west build -b native_sim/native/64 zephcore --pristine
./build/zephyr/zephcore_native_linux.exe
```
The binary prints something like:
```
UART_0 connected to pseudotty: /dev/pts/3
TCP companion transport listening on :5000
```
`Ctrl-C` to stop.
### 2. Femtofox (Luckfox Pico Mini, ARMv7-A)
```bash
west build -b native_sim/native/64 zephcore --pristine -- \
-DZEPHYR_TOOLCHAIN_VARIANT=cross-compile \
-DNATIVE_TARGET_HOST=arm \
-DCROSS_COMPILE=/usr/bin/arm-linux-gnueabihf- \
-DEXTRA_CONF_FILE="boards/common/femtofox.conf"
scp build/zephyr/zephcore_native_linux.exe root@femtofox.local:/opt/zephcore/
ssh root@femtofox.local /opt/zephcore/zephcore_native_linux.exe
```
### 3. Raspberry Pi 4 + RAK6421 (aarch64)
```bash
west build -b native_sim/native/64 zephcore --pristine -- \
-DZEPHYR_TOOLCHAIN_VARIANT=cross-compile \
-DNATIVE_TARGET_HOST=aarch64 \
-DCROSS_COMPILE=/usr/bin/aarch64-linux-gnu- \
-DEXTRA_CONF_FILE="boards/common/rpi_rak6421.conf"
```
For RPi 5, use `boards/common/rpi5_rak6421.conf` (same wiring, `gpiochip4` instead of `gpiochip0`).
### 4. Repeater role (no companion)
Add `boards/common/repeater.conf` to the `EXTRA_CONF_FILE` list. The repeater CLI is accessible via the native PTY printed at boot (`/dev/pts/N`):
```bash
screen /dev/pts/3 # local
# or remotely:
socat /dev/pts/3 TCP-LISTEN:6000,reuseaddr,fork &
# then on a remote machine:
nc <sbc-ip> 6000
```
---
## Hardware Wiring
### Femtofox / Luckfox Pico Mini + E22-900M30S (TCXO)
Pin offsets are computed as `(Rockchip GPIO# 32)` because the kernel splits banks into separate `gpiochip` devices.
| Signal | gpiochip | Offset | Rockchip GPIO |
|---|---|---|---|
| SPI bus | `/dev/spidev0.0` @ 2 MHz | — | — |
| CS | gpiochip1 | 16 | GPIO48 (1C0) |
| DIO1/IRQ | gpiochip1 | 23 | GPIO55 (1C7) |
| BUSY | gpiochip1 | 22 | GPIO54 (1C6) |
| RESET | gpiochip1 | 25 | GPIO57 (1D1) |
| RXEN | gpiochip1 | 24 | GPIO56 (1D0) |
SX1262 extras: DIO2 drives the RF switch (`dio2-tx-enable`), DIO3 powers the TCXO at 1.8V.
Source: `github.com/femtofox/femtofox``foxbuntu/.../femtofox_SX1262_TCXO.yaml`.
### Raspberry Pi 4/5 + RAK6421 HAT + RAK13300/RAK13302 (IO Slot 1)
BCM GPIO numbers (RPi 4: gpiochip0; RPi 5: gpiochip4):
| Signal | BCM | WisBlock slot 1 pin |
|---|---|---|
| SPI bus | `/dev/spidev0.0` (CE0 = GPIO 8) | 2528 |
| DIO1/IRQ | 17 | 29 |
| BUSY | 12 | 30 |
| RESET | 13 | 31 |
No DIO2 RF switch (RAK13300 has discrete switch), no DIO3 TCXO.
Source: RAK6421 datasheet IO slot table + RAKWireless `meshtastic-rak6421-guide`.
---
## Running the Binary
The binary prints its PTY path and TCP listen port at startup. Logs go to stderr; pipe them where you want them.
### Companion app connection
In the ZephCore companion mobile app, choose TCP/Network mode and connect to:
- **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.
### Repeater CLI
The repeater role's USB CDC CLI maps onto Zephyr's `CONFIG_UART_NATIVE_PTY`. At boot:
```
UART_0 connected to pseudotty: /dev/pts/3
```
`screen /dev/pts/3` to attach locally. For remote access, bridge the PTY to TCP with `socat`:
```bash
socat /dev/pts/3 TCP-LISTEN:6000,reuseaddr,fork &
nc <sbc-ip> 6000
```
---
## Runtime Pin Override
For one-off hardware setups or custom wiring, override DT defaults at runtime:
```bash
./zephcore_native_linux.exe \
--lora-spidev=/dev/spidev0.0 \
--lora-gpio-chip=/dev/gpiochip1
```
Currently only the **paths** are runtime-overridable; pin offsets come from the DTS overlay you build with. To change pin numbers without rebuilding, create your own `boards/common/<custom>.conf` + `.overlay` and pass it via `-DEXTRA_CONF_FILE`.
Run `./zephcore_native_linux.exe --help` to see all available command-line arguments registered by the native_sim infrastructure (Zephyr drivers, our SPI/GPIO drivers, etc.).
---
## Troubleshooting
### Permission denied opening /dev/spidev0.0 or /dev/gpiochip*
```bash
sudo usermod -a -G spi,gpio $USER
# Log out and back in for group membership to take effect.
```
### `/dev/spidev0.0: No such file or directory`
SPI isn't enabled in the device tree. See "Enabling spidev on the SBC" above.
### `Linux kernel headers too old; GPIO V2 chardev uAPI required`
The host adapter requires the GPIO V2 uAPI (kernel ≥ 5.10, late 2020). All current SBC distros (Debian 12+, Ubuntu 22.04+, Raspberry Pi OS bookworm+) ship recent enough kernels. If you hit this, your distro is ancient — upgrade.
### TCP port 5000 already in use
Another service (Flask, Docker registry, AirPlay…) has port 5000. Override:
```bash
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.
### LoRa packets fly out but nothing receives them
Check that DIO1 is wired and configured correctly. The interrupt path is the primary RX trigger. If DIO1 is misrouted, the driver will appear to TX fine but never reports RX.
Verify on the host with `gpioget`:
```bash
gpioget /dev/gpiochip1 23 # should toggle when a peer transmits
```
If the host can't see DIO1 transitioning, the radio isn't bringing the line up — check SX1262 RESET sequencing and DIO1 wiring.
### `bind(5000) failed: 98` (EADDRINUSE)
Another instance of the binary is already running, or you killed the last one with `kill -9` and the socket is still in TIME_WAIT. `SO_REUSEADDR` is set so this should clear in <1 minute; or `pkill zephcore_native_linux.exe`.
---
## Architecture
```
┌─────────────────────────────────────────────┐
│ ZephCore Mesh (C++) │
│ CompanionMesh / RepeaterMesh │
└──────────────┬──────────────────────────────┘
│ Zephyr APIs (k_event, k_msgq, k_thread)
┌──────────────▼──────────────────────────────┐
│ Zephyr RTOS @ native_sim (running as Linux │
│ process via pthreads) │
│ │
│ • SX1262 driver (unchanged) │
│ • New spi_native_linux driver ──┐ │
│ • New gpio_native_linux driver ──┤ │
│ • LinuxTCPTransport (replaces BLE) ──┤ │
└──────────────────────────────────────────┼──┘
host syscalls + zsock_*
┌──────────────────────────────────────────▼──┐
│ Linux kernel │
│ /dev/spidevX.Y /dev/gpiochipN │
│ AF_INET TCP socket /dev/pts/N │
└─────────────────────────────────────────────┘
```
The mesh C++ layer and the patched SX1262 Zephyr driver are bit-identical to the MCU builds. Only the SPI/GPIO host bridges and the companion transport differ.
---
## Limitations
- **Not BLE.** The companion app must support TCP/Network mode. No BlueZ integration.
- **Not for production use** per Zephyr's `native_sim` documentation. Works fine for hobby/lab deployments.
- **Single companion client.** One TCP connection at a time, mirroring `BT_MAX_CONN=1`.
- **No persistent flash.** Storage uses the host filesystem under `/lfs` via LittleFS over a flat backing file. Survives reboot if you persist the file; otherwise the node is regenerated each run.
---
## Files
- `boards/common/linux_common.conf` + `.overlay` — auto-applied when `BOARD=native_sim`
- `boards/common/femtofox.conf` / `.overlay` — Femtofox preset
- `boards/common/rpi_rak6421.conf` / `.overlay` — Raspberry Pi 4 preset
- `boards/common/rpi5_rak6421.conf` / `.overlay` — Raspberry Pi 5 preset
- `adapters/transport/LinuxTCPTransport.c` — TCP companion transport
- `patches/zephyr-new/drivers/spi/spi_native_linux*` — spidev SPI driver
- `patches/zephyr-new/drivers/gpio/gpio_native_linux*` — libgpiod v2 GPIO driver
- `patches/zephyr/0007-spi-gpio-native-linux.patch` — wires the new drivers into Zephyr's `drivers/spi/` and `drivers/gpio/` CMakeLists + Kconfig
---
## Debugging Log
(Updated as issues are found during bringup.)
### Verification status
End-to-end verified under WSL Ubuntu 24.04 (gcc 13.3):
- ✅ All 7 `patches/zephyr/*.patch` apply cleanly (including the new `0007-spi-gpio-native-linux.patch`).
- ✅ All files in `patches/zephyr-new/` copy correctly into the Zephyr tree.
- ✅ Platform detection routes `BOARD=native_sim` to `boards/common/linux_common.conf`.
-`native_sim/native/64` builds clean → `build/zephyr/zephcore_native_linux.exe` (~4.3 MB ELF).
- ✅ Binary runs. Zephyr OS boots, mesh event loop starts.
-`spi_native_linux` driver loads and attempts to open `/dev/spidev0.0` (fails in WSL: no SPI hardware).
-`LinuxTCPTransport` listens on port 5000.
- ✅ TCP client connect/disconnect works; 2-byte BE length framing parses correctly.
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
```
The default 32-bit `native_sim` variant additionally requires `libc6-dev-i386`; the 64-bit variant works with stock `libc6-dev`.
### Known caveats found during implementation
- **Double-overlay listing** in `EXTRA_DTC_OVERLAY_FILE`: `linux_common.overlay` gets auto-paired twice — once when the platform conf is selected, once again when the EXTRA_CONF_FILE list is re-walked. Pre-existing CMakeLists.txt quirk affecting all platform confs; harmless (DTC merges idempotently) but cosmetic.
- **Pin paths are runtime-overridable, pin numbers are not.** Only `--lora-spidev=<path>` and `--lora-gpio-chip=<path>` are wired as command-line args. To change actual pin numbers without rebuilding, create a custom preset overlay and pass it via `-DEXTRA_CONF_FILE`. (A future iteration could expose pin offsets as cmdline args too.)
@@ -0,0 +1,444 @@
/*
* SPDX-License-Identifier: Apache-2.0
* 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.
*
* 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.
* - 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.
*
* Used only when CONFIG_ZEPHCORE_TRANSPORT_TCP=y and CONFIG_BT=n
* (the linux_common.conf preset turns CONFIG_BT off).
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <zephyr/kernel.h>
#include <zephyr/net/socket.h>
#include <zephyr/net/net_ip.h>
#include <zephyr/logging/log.h>
#include "ZephyrBLE.h"
LOG_MODULE_REGISTER(linux_tcp_transport, LOG_LEVEL_INF);
/* Keep in sync with MAX_FRAME_SIZE in CompanionMesh.h. ZephyrBLE.cpp
* uses the same constant; we mirror it here so a single transport build
* doesn't have to pull the C++ header. */
#ifndef MAX_FRAME_SIZE
#define MAX_FRAME_SIZE 172
#endif
#define FRAME_QUEUE_SIZE CONFIG_ZEPHCORE_BLE_QUEUE_SIZE
#define LISTEN_PORT CONFIG_ZEPHCORE_LINUX_TCP_PORT
#define LISTEN_BACKLOG 1
struct frame {
uint16_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
K_MSGQ_DEFINE(ble_send_queue, sizeof(struct frame), FRAME_QUEUE_SIZE, 4);
K_MSGQ_DEFINE(ble_recv_queue, sizeof(struct frame), FRAME_QUEUE_SIZE, 4);
static const struct ble_callbacks *transport_cbs;
static enum zephcore_iface active_iface = ZEPHCORE_IFACE_NONE;
static bool transport_enabled;
static uint32_t passkey;
/* Socket state */
static int listen_fd = -1;
static int client_fd = -1;
static struct k_mutex sock_mu;
/* Listen + RX thread */
static K_KERNEL_STACK_DEFINE(listen_thread_stack, 4096);
static struct k_thread listen_thread;
static bool listen_thread_started;
/* TX work — drains ble_send_queue */
static void tx_drain_work_fn(struct k_work *work);
static K_WORK_DEFINE(tx_drain_work, tx_drain_work_fn);
static void close_client_locked(void)
{
if (client_fd >= 0) {
zsock_close(client_fd);
client_fd = -1;
}
}
/* Write `len` bytes, retrying on partial sends. Returns 0 or -errno. */
static int sock_send_all(int fd, const uint8_t *buf, size_t len)
{
size_t off = 0;
while (off < len) {
ssize_t n = zsock_send(fd, buf + off, len - off, 0);
if (n < 0) {
if (errno == EINTR) {
continue;
}
return -errno;
}
if (n == 0) {
return -ECONNRESET;
}
off += (size_t)n;
}
return 0;
}
/* Read exactly `len` bytes (blocking). Returns 0 on success, -errno on
* error, -ECONNRESET on clean EOF. */
static int sock_recv_all(int fd, uint8_t *buf, size_t len)
{
size_t off = 0;
while (off < len) {
ssize_t n = zsock_recv(fd, buf + off, len - off, 0);
if (n < 0) {
if (errno == EINTR) {
continue;
}
return -errno;
}
if (n == 0) {
return -ECONNRESET;
}
off += (size_t)n;
}
return 0;
}
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);
int fd = client_fd;
if (fd < 0) {
k_mutex_unlock(&sock_mu);
/* No client — drop the frame. */
LOG_DBG("tx: no client, dropping len=%u", f.len);
continue;
}
hdr[0] = (uint8_t)(f.len >> 8);
hdr[1] = (uint8_t)(f.len & 0xff);
int err = sock_send_all(fd, hdr, 2);
if (err == 0) {
err = sock_send_all(fd, f.buf, f.len);
}
k_mutex_unlock(&sock_mu);
if (err != 0) {
LOG_WRN("tx send err=%d, closing client", err);
k_mutex_lock(&sock_mu, K_FOREVER);
close_client_locked();
active_iface = ZEPHCORE_IFACE_NONE;
k_mutex_unlock(&sock_mu);
if (transport_cbs && transport_cbs->on_disconnected) {
transport_cbs->on_disconnected();
}
break;
}
}
if (transport_cbs && transport_cbs->on_tx_idle &&
k_msgq_num_used_get(&ble_send_queue) == 0) {
transport_cbs->on_tx_idle();
}
}
static void listen_thread_fn(void *a, void *b, void *c)
{
ARG_UNUSED(a);
ARG_UNUSED(b);
ARG_UNUSED(c);
struct net_sockaddr_in addr = {0};
addr.sin_family = NET_AF_INET;
addr.sin_addr.s_addr = 0; /* NET_INADDR_ANY */
addr.sin_port = net_htons(LISTEN_PORT);
listen_fd = zsock_socket(NET_AF_INET, NET_SOCK_STREAM, NET_IPPROTO_TCP);
if (listen_fd < 0) {
LOG_ERR("zsock_socket failed: %d", errno);
return;
}
int one = 1;
(void)zsock_setsockopt(listen_fd, ZSOCK_SOL_SOCKET, ZSOCK_SO_REUSEADDR,
&one, sizeof(one));
if (zsock_bind(listen_fd, (struct net_sockaddr *)&addr, sizeof(addr)) < 0) {
LOG_ERR("bind(%d) failed: %d", LISTEN_PORT, errno);
zsock_close(listen_fd);
listen_fd = -1;
return;
}
if (zsock_listen(listen_fd, LISTEN_BACKLOG) < 0) {
LOG_ERR("listen failed: %d", errno);
zsock_close(listen_fd);
listen_fd = -1;
return;
}
LOG_INF("TCP companion transport listening on :%u", (unsigned)LISTEN_PORT);
while (true) {
struct net_sockaddr_in caddr;
net_socklen_t clen = sizeof(caddr);
int fd = zsock_accept(listen_fd, (struct net_sockaddr *)&caddr, &clen);
if (fd < 0) {
if (errno == EINTR) {
continue;
}
LOG_ERR("accept failed: %d", errno);
k_sleep(K_MSEC(100));
continue;
}
k_mutex_lock(&sock_mu, K_FOREVER);
if (client_fd >= 0) {
LOG_WRN("Second client rejected (already connected)");
zsock_close(fd);
k_mutex_unlock(&sock_mu);
continue;
}
client_fd = fd;
active_iface = ZEPHCORE_IFACE_BLE;
k_mutex_unlock(&sock_mu);
LOG_INF("TCP companion client connected");
if (transport_cbs && transport_cbs->on_connected) {
transport_cbs->on_connected();
}
/* RX loop on this client until it disconnects. */
while (true) {
uint8_t hdr[2];
int err = sock_recv_all(fd, hdr, 2);
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;
}
struct frame f;
f.len = flen;
err = sock_recv_all(fd, f.buf, flen);
if (err != 0) {
break;
}
if (k_msgq_put(&ble_recv_queue, &f, K_NO_WAIT) != 0) {
LOG_WRN("RX queue full, dropping frame");
continue;
}
if (transport_cbs && transport_cbs->on_rx_frame) {
transport_cbs->on_rx_frame(f.buf, f.len);
}
}
k_mutex_lock(&sock_mu, K_FOREVER);
close_client_locked();
active_iface = ZEPHCORE_IFACE_NONE;
k_mutex_unlock(&sock_mu);
LOG_INF("TCP companion client disconnected");
if (transport_cbs && transport_cbs->on_disconnected) {
transport_cbs->on_disconnected();
}
}
}
/* ========== Public API (matches ZephyrBLE.h) ========== */
void zephcore_ble_init(const struct ble_callbacks *cbs)
{
transport_cbs = cbs;
k_mutex_init(&sock_mu);
transport_enabled = true;
}
void zephcore_ble_start(const char *device_name)
{
ARG_UNUSED(device_name);
if (listen_thread_started) {
return;
}
k_thread_create(&listen_thread, listen_thread_stack,
K_KERNEL_STACK_SIZEOF(listen_thread_stack),
listen_thread_fn, NULL, NULL, NULL,
K_PRIO_COOP(7), 0, K_NO_WAIT);
k_thread_name_set(&listen_thread, "linux_tcp_listen");
listen_thread_started = true;
}
size_t zephcore_ble_send(const uint8_t *data, uint16_t len)
{
if (len == 0 || len > MAX_FRAME_SIZE) {
return 0;
}
k_mutex_lock(&sock_mu, K_FOREVER);
int fd = client_fd;
k_mutex_unlock(&sock_mu);
if (fd < 0) {
return 0;
}
struct frame f;
f.len = len;
memcpy(f.buf, data, len);
if (k_msgq_put(&ble_send_queue, &f, K_NO_WAIT) != 0) {
LOG_WRN("TX queue full, dropping len=%u", len);
return 0;
}
k_work_submit(&tx_drain_work);
return len;
}
void zephcore_ble_set_enabled(bool enable)
{
transport_enabled = enable;
if (!enable) {
k_mutex_lock(&sock_mu, K_FOREVER);
close_client_locked();
k_mutex_unlock(&sock_mu);
}
}
bool zephcore_ble_is_enabled(void)
{
return transport_enabled;
}
bool zephcore_ble_is_active(void)
{
bool active;
k_mutex_lock(&sock_mu, K_FOREVER);
active = (client_fd >= 0);
k_mutex_unlock(&sock_mu);
return active;
}
bool zephcore_ble_is_connected(void)
{
return zephcore_ble_is_active();
}
bool zephcore_ble_is_congested(void)
{
return k_msgq_num_used_get(&ble_send_queue) >=
(CONFIG_ZEPHCORE_BLE_QUEUE_SIZE * 2 / 3);
}
bool zephcore_ble_is_advertising(void)
{
/* TCP transport "advertises" by listening. Always true once started. */
return listen_fd >= 0;
}
void zephcore_ble_set_passkey(uint32_t pk)
{
passkey = pk;
}
uint32_t zephcore_ble_get_passkey(void)
{
return passkey;
}
enum zephcore_iface zephcore_ble_get_active_iface(void)
{
return active_iface;
}
void zephcore_ble_set_active_iface(enum zephcore_iface iface)
{
active_iface = iface;
}
struct k_msgq *zephcore_ble_get_recv_queue(void)
{
return &ble_recv_queue;
}
struct k_msgq *zephcore_ble_get_send_queue(void)
{
return &ble_send_queue;
}
void zephcore_ble_kick_tx(void)
{
k_work_submit(&tx_drain_work);
}
void zephcore_ble_disconnect(void)
{
k_mutex_lock(&sock_mu, K_FOREVER);
close_client_locked();
active_iface = ZEPHCORE_IFACE_NONE;
k_mutex_unlock(&sock_mu);
if (transport_cbs && transport_cbs->on_disconnected) {
transport_cbs->on_disconnected();
}
}
void zephcore_ble_conn_params_ready(void)
{
/* No BLE conn-param negotiation on TCP — no-op. */
}
void zephcore_ble_update_name(const char *new_name)
{
/* TCP transport has no advertising payload to update. */
ARG_UNUSED(new_name);
}
+2
View File
@@ -1181,7 +1181,9 @@ uint8_t CompanionMesh::onContactRequest(const ContactInfo &contact, uint32_t sen
void CompanionMesh::logTx(mesh::Packet *, int)
{
#if ZEPHCORE_HAS_UI_TASK
ui_notify_packet_sent();
#endif
}
void CompanionMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len)
+9
View File
@@ -0,0 +1,9 @@
# Femtofox (Luckfox Pico Mini + E22-900M30S TCXO) preset.
#
# Pin defaults already match Femtofox in linux_common.overlay; this preset
# exists for clarity ("I am building for Femtofox") and to set the board
# name string. Add to EXTRA_CONF_FILE alongside linux_common implicitly.
#
# Source: github.com/femtofox/femtofox foxbuntu meshtasticd config.
CONFIG_ZEPHCORE_BOARD_NAME="Femtofox"
+9
View File
@@ -0,0 +1,9 @@
/*
* Femtofox (Luckfox Pico Mini + E22-900M30S TCXO).
*
* Femtofox pinout already matches the linux_common.overlay defaults
* (gpiochip1 with offsets CS=16, BUSY=22, DIO1=23, RXEN=24, RESET=25).
* This overlay is intentionally empty — adding boards/common/femtofox.conf
* to EXTRA_CONF_FILE is enough to label the build "Femtofox" without
* changing any pins.
*/
+95
View File
@@ -0,0 +1,95 @@
# ZephCore Native Linux Platform Configuration
#
# Auto-applied by zephcore/CMakeLists.txt when BOARD matches native_sim.
# Runs the full ZephCore stack as a native Linux process via Zephyr's
# native_sim board, talking to real SPI/GPIO via /dev/spidev + libgpiod v2.
#
# See LINUX_NATIVE.md for prerequisites and per-device wiring.
# ========== Disable BLE — Linux uses TCP transport instead ==========
# zephcore_common.conf turns CONFIG_BT=y; override here. None of the BT
# child Kconfigs survive without CONFIG_BT, so this single line is enough.
CONFIG_BT=n
CONFIG_BT_SETTINGS=n
CONFIG_BT_DIS=n
CONFIG_BT_PERIPHERAL=n
CONFIG_BT_SMP=n
# ========== Disable Zephyr's emulated SPI/GPIO from native_sim ==========
# native_sim's base DTS has emulated SPI/GPIO controllers; we replace them
# with our native_linux drivers via the board.overlay paired with this conf.
CONFIG_GPIO_EMUL=n
CONFIG_SPI_EMUL=n
# ========== Enable our native Linux drivers ==========
CONFIG_SPI=y
CONFIG_SPI_NATIVE_LINUX=y
CONFIG_GPIO=y
CONFIG_GPIO_NATIVE_LINUX=y
# ========== TCP companion transport ==========
CONFIG_ZEPHCORE_TRANSPORT_TCP=y
CONFIG_ZEPHCORE_LINUX_TCP_PORT=5000
# NSOS: native_sim socket offload — Zephyr zsock_* calls go straight to
# host Linux sockets. No native IP stack, no TAP device, no root needed.
# Matches Zephyr's samples/net/sockets/echo_server/overlay-nsos.conf.
CONFIG_NETWORKING=y
CONFIG_ETH_NATIVE_TAP=n
CONFIG_NET_DRIVERS=y
CONFIG_NET_SOCKETS=y
CONFIG_NET_SOCKETS_OFFLOAD=y
CONFIG_NET_NATIVE_OFFLOADED_SOCKETS=y
# ========== Storage — back LittleFS with a host file ==========
# native_sim has zephyr,sim-flash that gives us a flash-emulated backing
# store. We can stay on LittleFS so the existing ZephyrDataStore code
# works unchanged. Disable bond settings (BT off).
CONFIG_SETTINGS_FILE_PATH="/lfs/settings"
# ========== Repeater CLI over native PTY ==========
# native_sim's default uart0 is zephyr,native-pty-uart — at boot Zephyr
# prints "UART_0 connected to pseudotty: /dev/pts/N". screen /dev/pts/N
# to attach. Remote: socat /dev/pts/N TCP-LISTEN:6000,fork
CONFIG_UART_NATIVE_PTY=y
CONFIG_UART_INTERRUPT_DRIVEN=y
# ========== Output binary name ==========
# zephyr.exe → zephcore_native_linux.exe (the .exe suffix is native_sim
# convention; the file is a normal Linux ELF executable).
CONFIG_KERNEL_BIN_NAME="zephcore_native_linux"
# ========== Heap ==========
# Larger than MCU baseline — we have host RAM to spare and the SPI
# transceive path allocates flat scatter/gather buffers per call.
CONFIG_HEAP_MEM_POOL_SIZE=131072
# ========== Disable MCU-specific options that don't apply ==========
CONFIG_USB_DEVICE_STACK_NEXT=n
CONFIG_USB_DEVICE_STACK=n
# native_sim provides hwinfo / reboot / poweroff stubs; keep them on so
# ZephyrBoard.cpp and main_companion's reset-cause code link cleanly.
CONFIG_HWINFO=y
CONFIG_REBOOT=y
CONFIG_POWEROFF=y
# native_sim has no ADC hardware; battery readback is N/A.
CONFIG_ADC=n
# Headless SBC: no display, no buzzer, no buttons.
# Keep UI_BUTTONS=y so the helpers/ui/ include path is added (CompanionMesh
# unconditionally includes ui_task.h). The display/buzzer/LED hardware
# isn't present so the actual UI code paths are no-ops at runtime.
CONFIG_ZEPHCORE_UI_DISPLAY=n
CONFIG_ZEPHCORE_UI_BUTTONS=y
CONFIG_ZEPHCORE_UI_BUZZER=n
CONFIG_ZEPHCORE_UI_DESIGN_BUTTON=n
CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK=n
CONFIG_ZEPHCORE_UI_MULTI_TAP=n
# Disable I2C + sensors (no I2C bus on a headless SBC, and oled_power.c
# references native_sim's i2c0 emul controller which has no real driver).
CONFIG_I2C=n
CONFIG_SENSOR=n
CONFIG_GNSS=n
CONFIG_MODEM_MODULES=n
# Crypto: native_sim runs PSA in software, same as MCU.
@@ -0,0 +1,97 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* ZephCore native Linux DT overlay (auto-paired with linux_common.conf).
*
* Disables the native_sim emulated GPIO/SPI controllers and adds our
* libgpiod/spidev-backed native_linux versions, with an SX1262 child
* node on the SPI bus.
*
* Generic defaults below match the Femtofox SX1262 wiring. Override per
* device by adding boards/common/<device>.conf + .overlay via
* EXTRA_CONF_FILE, or use --lora-* command-line args at runtime.
*/
#include <zephyr/dt-bindings/gpio/gpio.h>
#include <zephyr/dt-bindings/lora/sx126x.h>
/* Disable native_sim emulated controllers (replaced below). */
&spi0 {
status = "disabled";
};
&gpio0 {
status = "disabled";
};
/* 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 {
status = "disabled";
};
&input_sdl_touch {
status = "disabled";
};
/* Drop the chosen zephyr,display node so HAS_SDL doesn't get selected.
* Also drop the led0 alias — native_sim's base DTS attaches it to the
* emulated gpio0 that we just disabled, which would otherwise cause a
* dangling DEVICE_DT_GET in helpers/ui/ui_common.c. */
/ {
chosen {
/delete-property/ zephyr,display;
/delete-property/ zephyr,touch;
};
aliases {
/delete-property/ led0;
};
};
/ {
/delete-node/ leds;
};
/ {
aliases {
/* The mesh adapter reads lora0 alias to bind the SX126x driver. */
lora0 = &lora_sx1262;
};
/* libgpiod v2 GPIO controller bound to a host /dev/gpiochipX. */
zephcore_gpio0: zephcore_gpio0 {
status = "okay";
compatible = "zephcore,gpio-native-linux";
gpio-controller;
gpio-chip = "/dev/gpiochip1";
ngpios = <64>;
#gpio-cells = <2>;
};
/* spidev-backed SPI bus on /dev/spidev0.0 with one SX1262 child. */
zephcore_spi0: zephcore_spi0 {
status = "okay";
compatible = "zephcore,spi-native-linux";
spi-dev = "/dev/spidev0.0";
clock-frequency = <2000000>;
spi-mode = <0>;
#address-cells = <1>;
#size-cells = <0>;
/* CS line: GPIO 16 on chip 1 = GPIO48 = 1C0 (Femtofox default). */
cs-gpios = <&zephcore_gpio0 16 GPIO_ACTIVE_LOW>;
lora_sx1262: lora@0 {
compatible = "semtech,sx1262";
reg = <0>;
spi-max-frequency = <2000000>;
/* Femtofox defaults; overridden by per-device overlays. */
reset-gpios = <&zephcore_gpio0 25 GPIO_ACTIVE_LOW>;
busy-gpios = <&zephcore_gpio0 22 GPIO_ACTIVE_HIGH>;
dio1-gpios = <&zephcore_gpio0 23 GPIO_ACTIVE_HIGH>;
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>;
};
};
};
+7
View File
@@ -0,0 +1,7 @@
# Raspberry Pi 5 + RAK6421 WisBlock HAT + RAK13300/RAK13302 (SX1262).
#
# Same WisBlock IO Slot 1 wiring as RPi 4 (rpi_rak6421.conf), but RPi 5
# moves the 40-pin header GPIOs to /dev/gpiochip4. Everything else is
# identical.
CONFIG_ZEPHCORE_BOARD_NAME="RPi5 RAK6421"
@@ -0,0 +1,25 @@
/*
* Raspberry Pi 5 + RAK6421 + RAK13300/RAK13302 SX1262 (IO Slot 1).
* Same as rpi_rak6421.overlay but with gpiochip4 (RPi 5 moved the
* 40-pin header GPIOs).
*/
#include <zephyr/dt-bindings/gpio/gpio.h>
&zephcore_gpio0 {
gpio-chip = "/dev/gpiochip4";
};
&zephcore_spi0 {
cs-gpios = <&zephcore_gpio0 8 GPIO_ACTIVE_LOW>;
};
&lora_sx1262 {
reset-gpios = <&zephcore_gpio0 13 GPIO_ACTIVE_LOW>;
busy-gpios = <&zephcore_gpio0 12 GPIO_ACTIVE_HIGH>;
dio1-gpios = <&zephcore_gpio0 17 GPIO_ACTIVE_HIGH>;
/delete-property/ dio2-tx-enable;
/delete-property/ dio3-tcxo-voltage;
/delete-property/ tcxo-power-startup-delay-ms;
/delete-property/ rx-enable-gpios;
};
+13
View File
@@ -0,0 +1,13 @@
# Raspberry Pi 4 + RAK6421 WisBlock HAT + RAK13300/RAK13302 (SX1262).
#
# IO Slot 1 wiring (BCM GPIO numbers on gpiochip0):
# SPI: /dev/spidev0.0 (CE0 = GPIO 8, hardware CS)
# DIO1/IRQ: GPIO 17 (slot pin 29)
# BUSY: GPIO 12 (slot pin 30)
# RESET: GPIO 13 (slot pin 31)
# No DIO2 RF switch, no DIO3 TCXO (RAK13300 has discrete switch + ext TCXO).
#
# Source: docs.rakwireless.com/.../rak6421/datasheet IO slot table +
# RAKWireless meshtastic-rak6421-guide config.yaml
CONFIG_ZEPHCORE_BOARD_NAME="RPi RAK6421"
@@ -0,0 +1,28 @@
/*
* Raspberry Pi 4 + RAK6421 + RAK13300/RAK13302 SX1262 (IO Slot 1).
* Override Femtofox defaults from linux_common.overlay.
*/
#include <zephyr/dt-bindings/gpio/gpio.h>
&zephcore_gpio0 {
/* RPi 4: header GPIOs live on gpiochip0. */
gpio-chip = "/dev/gpiochip0";
};
&zephcore_spi0 {
/* SPI CE0 is GPIO 8 (driven as a regular GPIO from the kernel). */
cs-gpios = <&zephcore_gpio0 8 GPIO_ACTIVE_LOW>;
};
&lora_sx1262 {
reset-gpios = <&zephcore_gpio0 13 GPIO_ACTIVE_LOW>;
busy-gpios = <&zephcore_gpio0 12 GPIO_ACTIVE_HIGH>;
dio1-gpios = <&zephcore_gpio0 17 GPIO_ACTIVE_HIGH>;
/* RAK13300 uses discrete RF switch, not DIO2. Discard the Femtofox
* dio2-tx-enable and dio3-tcxo-voltage from the base overlay. */
/delete-property/ dio2-tx-enable;
/delete-property/ dio3-tcxo-voltage;
/delete-property/ tcxo-power-startup-delay-ms;
/delete-property/ rx-enable-gpios;
};
+145
View File
@@ -0,0 +1,145 @@
/*
* SPDX-License-Identifier: Apache-2.0
* No-op stubs for the UI API on headless builds.
*
* Companion code (CompanionMesh, main_companion, ui_mesh_actions) calls
* ui_init / ui_notify / ui_set_* / etc. unconditionally -- these are normally
* provided by helpers/ui-button/ui_task.c or helpers/ui-joystick/.
* On a Linux SBC build with no display/buttons/buzzer those source files
* are not compiled, so we provide weak no-op implementations here.
*
* Signatures must match ui_task.h. Weak symbols are overridden by the
* real ui_task.c on any board where a UI variant is enabled, so this
* file is harmless to include in every build (we only add it when no
* UI variant is active).
*/
#include <stdbool.h>
#include <stdint.h>
#include <zephyr/kernel.h>
#include "ui_task.h"
#define WEAK __attribute__((weak))
WEAK int ui_init(void) { return 0; }
WEAK void ui_play_startup_chime(void) { }
WEAK void ui_led_heartbeat_init(void) { }
WEAK void ui_notify(enum ui_event event)
{
ARG_UNUSED(event);
}
WEAK void ui_set_msg_count(uint16_t count)
{
ARG_UNUSED(count);
}
WEAK void ui_set_ble_status(bool connected, const char *name)
{
ARG_UNUSED(connected); ARG_UNUSED(name);
}
WEAK void ui_set_radio_params(uint32_t freq_hz, uint8_t sf,
uint16_t bw_khz_x10, uint8_t cr,
int8_t tx_power, int16_t noise_floor)
{
ARG_UNUSED(freq_hz); ARG_UNUSED(sf); ARG_UNUSED(bw_khz_x10);
ARG_UNUSED(cr); ARG_UNUSED(tx_power); ARG_UNUSED(noise_floor);
}
WEAK void ui_set_gps_data(bool has_fix, uint8_t sats,
int32_t lat_mdeg, int32_t lon_mdeg, int32_t alt_mm)
{
ARG_UNUSED(has_fix); ARG_UNUSED(sats);
ARG_UNUSED(lat_mdeg); ARG_UNUSED(lon_mdeg); ARG_UNUSED(alt_mm);
}
WEAK void ui_set_battery(uint16_t mv, uint8_t pct)
{
ARG_UNUSED(mv); ARG_UNUSED(pct);
}
WEAK void ui_set_clock(uint32_t epoch)
{
ARG_UNUSED(epoch);
}
WEAK void ui_add_recent(const char *name, int16_t rssi, uint32_t age_s)
{
ARG_UNUSED(name); ARG_UNUSED(rssi); ARG_UNUSED(age_s);
}
WEAK void ui_set_node_name(const char *name)
{
ARG_UNUSED(name);
}
WEAK void ui_clear_recent(void) { }
WEAK void ui_set_gps_available(bool available)
{
ARG_UNUSED(available);
}
WEAK void ui_set_gps_enabled(bool enabled)
{
ARG_UNUSED(enabled);
}
WEAK void ui_set_gps_state(uint8_t state, uint32_t last_fix_age_s,
uint32_t next_search_s)
{
ARG_UNUSED(state); ARG_UNUSED(last_fix_age_s); ARG_UNUSED(next_search_s);
}
WEAK void ui_set_ble_enabled(bool enabled)
{
ARG_UNUSED(enabled);
}
WEAK void ui_set_buzzer_quiet(bool quiet)
{
ARG_UNUSED(quiet);
}
WEAK void ui_set_leds_disabled(bool disabled)
{
ARG_UNUSED(disabled);
}
WEAK void ui_set_heartbeat_led(bool enabled)
{
ARG_UNUSED(enabled);
}
WEAK void ui_set_offgrid_mode(bool enabled)
{
ARG_UNUSED(enabled);
}
WEAK void ui_set_battery_provider(uint16_t (*provider)(void))
{
ARG_UNUSED(provider);
}
WEAK void ui_refresh_battery(void) { }
WEAK void ui_invalidate_battery_cache(void) { }
WEAK void ui_notify_contact_msg(uint8_t path_len, const char *from_name,
const char *text, uint16_t msg_count)
{
ARG_UNUSED(path_len); ARG_UNUSED(from_name);
ARG_UNUSED(text); ARG_UNUSED(msg_count);
}
WEAK void ui_notify_channel_msg(const char *channel_name, const char *text,
uint32_t ts, uint8_t path_len,
uint16_t msg_count)
{
ARG_UNUSED(channel_name); ARG_UNUSED(text); ARG_UNUSED(ts);
ARG_UNUSED(path_len); ARG_UNUSED(msg_count);
}
WEAK void ui_notify_packet_sent(void) { }
@@ -0,0 +1,19 @@
# Copyright (c) 2026 ZephCore
# SPDX-License-Identifier: Apache-2.0
config GPIO_NATIVE_LINUX
bool "ZephCore native Linux GPIO driver (kernel V2 chardev)"
default y
depends on DT_HAS_ZEPHCORE_GPIO_NATIVE_LINUX_ENABLED
depends on ARCH_POSIX
help
Enables the gpio_native_linux driver, which bridges Zephyr GPIO
calls to a Linux /dev/gpiochipX character device via the kernel's
GPIO V2 chardev uAPI (ioctl on /dev/gpiochipN). Used by native_sim
builds running on real Linux SBCs.
No userspace library dependency — uses kernel ioctls directly.
Requires Linux >= 5.10 (GPIO V2 uAPI, released Dec 2020).
GPIO chip device path and line offsets are configured via DT,
with optional runtime override via --lora-gpio-chip=<path>.
@@ -0,0 +1,460 @@
/*
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: Apache-2.0
*
* Zephyr GPIO controller that bridges to a Linux /dev/gpiochipX device
* via libgpiod v2. Runs under native_sim on a real Linux host (not in
* QEMU or simulation).
*
* Each pin owns its own gpiod_line_request, lazily (re)created when
* pin_configure or pin_interrupt_configure changes its settings. A
* dedicated k_thread polls() the fds of all edge-detection-enabled pins
* and fires the registered Zephyr gpio_callbacks on each event.
*
* Host-side libgpiod calls live in gpio_native_linux_adapt.c (compiled
* into the native_simulator INTERFACE) to keep libgpiod headers out of
* the Zephyr translation unit.
*/
#define DT_DRV_COMPAT zephcore_gpio_native_linux
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/gpio/gpio_utils.h>
#include <zephyr/logging/log.h>
#include <cmdline.h>
#include <posix_native_task.h>
#include "gpio_native_linux_adapt.h"
LOG_MODULE_REGISTER(gpio_native_linux, CONFIG_GPIO_LOG_LEVEL);
/* Max pins this driver tracks per chip. SX126x needs ~5; allow plenty. */
#define GNL_MAX_PINS 64
struct gnl_pin_state {
gnl_line_t line;
gpio_flags_t cfg_flags;
enum gpio_int_mode int_mode;
enum gpio_int_trig int_trig;
bool requested;
};
struct gpio_native_linux_config {
struct gpio_driver_config common;
const char *chip_path;
uint32_t ngpios;
};
struct gpio_native_linux_data {
struct gpio_driver_data common;
gnl_chip_t chip;
struct gnl_pin_state pins[GNL_MAX_PINS];
sys_slist_t callbacks;
struct k_mutex mu;
struct k_thread evt_thread;
K_KERNEL_STACK_MEMBER(evt_thread_stack,
CONFIG_ARCH_POSIX_RECOMMENDED_STACK_SIZE);
bool evt_thread_started;
struct k_sem reconfig_sem;
const struct device *self;
};
/* Runtime cmdline override of the gpiochip path (applies to instance 0). */
static char *chip_path_cmd_opt;
static int translate_edge(enum gpio_int_trig trig)
{
switch (trig) {
case GPIO_INT_TRIG_LOW: return GNL_EDGE_FALLING;
case GPIO_INT_TRIG_HIGH: return GNL_EDGE_RISING;
case GPIO_INT_TRIG_BOTH: return GNL_EDGE_BOTH;
default: return GNL_EDGE_NONE;
}
}
/* (Re)request a line based on cfg_flags + int settings. Releases prior. */
static int rebuild_line(const struct device *dev, gpio_pin_t pin)
{
struct gpio_native_linux_data *data = dev->data;
struct gnl_pin_state *p = &data->pins[pin];
if (p->line != NULL) {
gnl_line_release(p->line);
p->line = NULL;
p->requested = false;
}
gpio_flags_t f = p->cfg_flags;
if ((f & GPIO_DISCONNECTED) == GPIO_DISCONNECTED) {
return 0;
}
bool active_low = (f & GPIO_ACTIVE_LOW) != 0;
bool pull_up = (f & GPIO_PULL_UP) != 0;
bool pull_down = (f & GPIO_PULL_DOWN) != 0;
if ((f & GPIO_OUTPUT) != 0) {
int init_val = -1;
if ((f & GPIO_OUTPUT_INIT_HIGH) != 0) {
init_val = 1;
} else if ((f & GPIO_OUTPUT_INIT_LOW) != 0) {
init_val = 0;
} else {
init_val = 0;
}
p->line = gnl_request_output(data->chip, pin, init_val, active_low);
} else if ((f & GPIO_INPUT) != 0) {
if (p->int_mode == GPIO_INT_MODE_DISABLED) {
p->line = gnl_request_input(data->chip, pin,
pull_up, pull_down, active_low);
} else {
int edge = translate_edge(p->int_trig);
if (edge == GNL_EDGE_NONE) {
p->line = gnl_request_input(data->chip, pin,
pull_up, pull_down,
active_low);
} else {
p->line = gnl_request_input_edge(data->chip, pin,
edge,
pull_up,
pull_down,
active_low);
}
}
}
if (p->line == NULL) {
LOG_ERR("Failed to request line %u on host chip", pin);
return -EIO;
}
p->requested = true;
return 0;
}
static int gnl_pin_configure(const struct device *dev, gpio_pin_t pin,
gpio_flags_t flags)
{
const struct gpio_native_linux_config *cfg = dev->config;
struct gpio_native_linux_data *data = dev->data;
int ret;
if (pin >= cfg->ngpios || pin >= GNL_MAX_PINS) {
return -EINVAL;
}
k_mutex_lock(&data->mu, K_FOREVER);
data->pins[pin].cfg_flags = flags;
ret = rebuild_line(dev, pin);
k_mutex_unlock(&data->mu);
/* Wake the event thread to re-collect fds. */
k_sem_give(&data->reconfig_sem);
return ret;
}
static int gnl_port_get_raw(const struct device *dev, gpio_port_value_t *value)
{
const struct gpio_native_linux_config *cfg = dev->config;
struct gpio_native_linux_data *data = dev->data;
gpio_port_value_t v = 0;
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];
if (!p->requested || p->line == NULL) {
continue;
}
int x = gnl_line_get_value(p->line);
if (x > 0) {
v |= ((gpio_port_value_t)1U << i);
}
}
k_mutex_unlock(&data->mu);
*value = v;
return 0;
}
static int gnl_port_set_masked_raw(const struct device *dev,
gpio_port_pins_t mask,
gpio_port_value_t value)
{
const struct gpio_native_linux_config *cfg = dev->config;
struct gpio_native_linux_data *data = dev->data;
k_mutex_lock(&data->mu, K_FOREVER);
for (uint32_t i = 0; i < cfg->ngpios && i < GNL_MAX_PINS; i++) {
if ((mask & ((gpio_port_pins_t)1U << i)) == 0) {
continue;
}
struct gnl_pin_state *p = &data->pins[i];
if (!p->requested || p->line == NULL) {
continue;
}
int bit = (value >> i) & 1U;
(void)gnl_line_set_value(p->line, bit);
}
k_mutex_unlock(&data->mu);
return 0;
}
static int gnl_port_set_bits_raw(const struct device *dev,
gpio_port_pins_t pins)
{
return gnl_port_set_masked_raw(dev, pins, pins);
}
static int gnl_port_clear_bits_raw(const struct device *dev,
gpio_port_pins_t pins)
{
return gnl_port_set_masked_raw(dev, pins, 0);
}
static int gnl_port_toggle_bits(const struct device *dev,
gpio_port_pins_t pins)
{
gpio_port_value_t cur;
int ret = gnl_port_get_raw(dev, &cur);
if (ret != 0) {
return ret;
}
return gnl_port_set_masked_raw(dev, pins, ~cur);
}
static int gnl_pin_interrupt_configure(const struct device *dev,
gpio_pin_t pin,
enum gpio_int_mode mode,
enum gpio_int_trig trig)
{
const struct gpio_native_linux_config *cfg = dev->config;
struct gpio_native_linux_data *data = dev->data;
int ret;
if (pin >= cfg->ngpios || pin >= GNL_MAX_PINS) {
return -EINVAL;
}
/* libgpiod v2 only supports edge detection on input lines. */
if (mode == GPIO_INT_MODE_LEVEL && trig != GPIO_INT_TRIG_LOW &&
trig != GPIO_INT_TRIG_HIGH) {
return -ENOTSUP;
}
if (mode == GPIO_INT_MODE_LEVEL) {
/* Level interrupts not supported by libgpiod v2 edge events. */
LOG_WRN("Level interrupts unsupported on pin %u; ignoring", pin);
return -ENOTSUP;
}
k_mutex_lock(&data->mu, K_FOREVER);
data->pins[pin].int_mode = mode;
data->pins[pin].int_trig = trig;
ret = rebuild_line(dev, pin);
k_mutex_unlock(&data->mu);
k_sem_give(&data->reconfig_sem);
return ret;
}
static int gnl_manage_callback(const struct device *dev,
struct gpio_callback *cb, bool set)
{
struct gpio_native_linux_data *data = dev->data;
return gpio_manage_callback(&data->callbacks, cb, set);
}
static DEVICE_API(gpio, gpio_native_linux_api) = {
.pin_configure = gnl_pin_configure,
.port_get_raw = gnl_port_get_raw,
.port_set_masked_raw = gnl_port_set_masked_raw,
.port_set_bits_raw = gnl_port_set_bits_raw,
.port_clear_bits_raw = gnl_port_clear_bits_raw,
.port_toggle_bits = gnl_port_toggle_bits,
.pin_interrupt_configure = gnl_pin_interrupt_configure,
.manage_callback = gnl_manage_callback,
};
/*
* Event polling thread.
*
* Collects fds from all currently-edge-enabled pin requests, calls poll(),
* and on each event drains it and fires the registered gpio_callbacks
* (using the Zephyr gpio_fire_callbacks helper).
*
* Wakes on reconfig_sem whenever a pin's settings change so the fd set
* stays in sync.
*/
static void gnl_evt_thread(void *arg1, void *arg2, void *arg3)
{
const struct device *dev = arg1;
struct gpio_native_linux_data *data = dev->data;
const struct gpio_native_linux_config *cfg = dev->config;
ARG_UNUSED(arg2);
ARG_UNUSED(arg3);
int fds[GNL_MAX_PINS];
gpio_pin_t pin_for_fd[GNL_MAX_PINS];
while (true) {
size_t nfds = 0;
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];
if (!p->requested || p->line == NULL) {
continue;
}
if (p->int_mode != GPIO_INT_MODE_EDGE) {
continue;
}
int fd = gnl_line_get_fd(p->line);
if (fd < 0) {
continue;
}
fds[nfds] = fd;
pin_for_fd[nfds] = (gpio_pin_t)i;
nfds++;
if (nfds >= 32) {
break;
}
}
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. */
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);
/* Process any reconfig requests first (cheap). */
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);
for (size_t i = 0; i < nfds; i++) {
if ((ready_mask & (1U << i)) == 0) {
continue;
}
gpio_pin_t pin = pin_for_fd[i];
struct gnl_pin_state *p = &data->pins[pin];
if (p->line == NULL) {
continue;
}
(void)gnl_line_drain_events(p->line);
fired |= ((gpio_port_pins_t)1U << pin);
}
k_mutex_unlock(&data->mu);
if (fired != 0) {
gpio_fire_callbacks(&data->callbacks, dev, fired);
}
}
}
static int gpio_native_linux_init(const struct device *dev)
{
const struct gpio_native_linux_config *cfg = dev->config;
struct gpio_native_linux_data *data = dev->data;
const char *path;
path = (chip_path_cmd_opt != NULL) ? chip_path_cmd_opt : cfg->chip_path;
LOG_INF("Opening GPIO chip: %s", path);
data->chip = gnl_chip_open(path);
if (data->chip == NULL) {
LOG_ERR("Failed to open chip %s", path);
return -ENODEV;
}
k_mutex_init(&data->mu);
k_sem_init(&data->reconfig_sem, 0, K_SEM_MAX_LIMIT);
sys_slist_init(&data->callbacks);
data->self = 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),
gnl_evt_thread, (void *)dev, NULL, NULL,
K_PRIO_COOP(7), 0, K_NO_WAIT);
data->evt_thread_started = true;
LOG_INF("GPIO native_linux ready (%s, %u pins)",
path, cfg->ngpios);
return 0;
}
#define GPIO_NATIVE_LINUX_INIT(inst) \
static const struct gpio_native_linux_config \
gpio_native_linux_cfg_##inst = { \
.common = { \
.port_pin_mask = \
GPIO_PORT_PIN_MASK_FROM_DT_INST(inst), \
}, \
.chip_path = DT_INST_PROP(inst, gpio_chip), \
.ngpios = DT_INST_PROP(inst, ngpios), \
}; \
\
static struct gpio_native_linux_data gpio_native_linux_data_##inst; \
\
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, \
&gpio_native_linux_api);
DT_INST_FOREACH_STATUS_OKAY(GPIO_NATIVE_LINUX_INIT)
/* Command-line arg: --lora-gpio-chip=<path> overrides DT gpio-chip. */
static void gpio_native_linux_add_cmdline_opts(void)
{
static struct args_struct_t gpio_native_options[] = {
{
.option = "lora-gpio-chip",
.name = "path",
.type = 's',
.dest = (void *)&chip_path_cmd_opt,
.descript = "Linux gpiochip device path (overrides DT gpio-chip)",
},
ARG_TABLE_ENDMARKER,
};
native_add_command_line_opts(gpio_native_options);
}
NATIVE_TASK(gpio_native_linux_add_cmdline_opts, PRE_BOOT_1, 12);
@@ -0,0 +1,299 @@
/*
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: Apache-2.0
*
* Host-side Linux GPIO chardev V2 wrapper for gpio_native_linux.
*
* Compiled into the native_simulator INTERFACE target. Keeps host
* headers (<linux/gpio.h>) out of the Zephyr translation unit.
*
* Uses the kernel's GPIO V2 character-device uAPI directly via ioctl
* no libgpiod dependency. Available on every Linux 5.10 (Dec 2020).
* This is the same ABI libgpiod v2 wraps; we just skip the wrapper to
* avoid the libgpiod v1-vs-v2 packaging mess on common SBC distros
* (Ubuntu 24.04 ships v1; Debian 13 ships v2; we work on both).
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#ifdef __linux
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <linux/gpio.h>
#else
#error "gpio_native_linux only builds on Linux hosts"
#endif
#include "gpio_native_linux_adapt.h"
#ifndef GPIO_V2_GET_LINE_IOCTL
#error "Linux kernel headers too old; GPIO V2 chardev uAPI (>= 5.10) required"
#endif
/* Host-side per-line state: line request fd + the offset we requested. */
struct gnl_line_handle {
int line_fd;
uint32_t offset;
};
gnl_chip_t gnl_chip_open(const char *path)
{
int fd = open(path, O_RDWR | O_CLOEXEC);
if (fd < 0) {
return NULL;
}
/* Encode fd in the pointer so callers see "non-NULL = success".
* fd of 0 would alias NULL, but Linux never hands /dev/gpiochipN
* out as fd 0 in practice (stdin keeps that). Still, defend: */
if (fd == 0) {
int newfd = fcntl(fd, F_DUPFD_CLOEXEC, 1);
close(fd);
if (newfd < 0) {
return NULL;
}
fd = newfd;
}
return (gnl_chip_t)(intptr_t)fd;
}
void gnl_chip_close(gnl_chip_t chip)
{
if (chip != NULL) {
close((int)(intptr_t)chip);
}
}
static struct gnl_line_handle *do_request(int chip_fd, unsigned int offset,
uint64_t flags)
{
struct gpio_v2_line_request req;
memset(&req, 0, sizeof(req));
req.offsets[0] = offset;
req.num_lines = 1;
req.config.flags = flags;
strncpy(req.consumer, "zephcore", sizeof(req.consumer) - 1);
if (ioctl(chip_fd, GPIO_V2_GET_LINE_IOCTL, &req) < 0) {
return NULL;
}
struct gnl_line_handle *h = calloc(1, sizeof(*h));
if (h == NULL) {
close(req.fd);
return NULL;
}
h->line_fd = req.fd;
h->offset = offset;
return h;
}
gnl_line_t gnl_request_output(gnl_chip_t chip, unsigned int offset,
int init_val, bool active_low)
{
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;
}
struct gnl_line_handle *h = do_request(chip_fd, offset, flags);
if (h == NULL) {
return NULL;
}
/* Drive the initial value. */
struct gpio_v2_line_values vals;
memset(&vals, 0, sizeof(vals));
vals.mask = 1ULL;
vals.bits = init_val ? 1ULL : 0ULL;
(void)ioctl(h->line_fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &vals);
return (gnl_line_t)h;
}
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;
}
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;
}
return flags;
}
gnl_line_t gnl_request_input(gnl_chip_t chip, unsigned int offset,
bool pull_up, bool pull_down,
bool active_low)
{
int chip_fd = (int)(intptr_t)chip;
uint64_t flags = input_flags(pull_up, pull_down, active_low);
return (gnl_line_t)do_request(chip_fd, offset, flags);
}
gnl_line_t gnl_request_input_edge(gnl_chip_t chip, unsigned int offset,
int edge, bool pull_up, bool pull_down,
bool active_low)
{
int chip_fd = (int)(intptr_t)chip;
uint64_t flags = input_flags(pull_up, pull_down, active_low);
switch (edge) {
case GNL_EDGE_RISING:
flags |= GPIO_V2_LINE_FLAG_EDGE_RISING;
break;
case GNL_EDGE_FALLING:
flags |= GPIO_V2_LINE_FLAG_EDGE_FALLING;
break;
case GNL_EDGE_BOTH:
flags |= GPIO_V2_LINE_FLAG_EDGE_RISING |
GPIO_V2_LINE_FLAG_EDGE_FALLING;
break;
default:
break;
}
return (gnl_line_t)do_request(chip_fd, offset, flags);
}
void gnl_line_release(gnl_line_t line)
{
struct gnl_line_handle *h = (struct gnl_line_handle *)line;
if (h == NULL) {
return;
}
if (h->line_fd >= 0) {
close(h->line_fd);
}
free(h);
}
int gnl_line_get_value(gnl_line_t line)
{
struct gnl_line_handle *h = (struct gnl_line_handle *)line;
if (h == NULL) {
return -EINVAL;
}
struct gpio_v2_line_values vals;
memset(&vals, 0, sizeof(vals));
vals.mask = 1ULL;
if (ioctl(h->line_fd, GPIO_V2_LINE_GET_VALUES_IOCTL, &vals) < 0) {
return -errno;
}
return (vals.bits & 1ULL) ? 1 : 0;
}
int gnl_line_set_value(gnl_line_t line, int value)
{
struct gnl_line_handle *h = (struct gnl_line_handle *)line;
if (h == NULL) {
return -EINVAL;
}
struct gpio_v2_line_values vals;
memset(&vals, 0, sizeof(vals));
vals.mask = 1ULL;
vals.bits = value ? 1ULL : 0ULL;
if (ioctl(h->line_fd, GPIO_V2_LINE_SET_VALUES_IOCTL, &vals) < 0) {
return -errno;
}
return 0;
}
int gnl_line_get_fd(gnl_line_t line)
{
struct gnl_line_handle *h = (struct gnl_line_handle *)line;
if (h == NULL) {
return -1;
}
return h->line_fd;
}
int gnl_line_drain_events(gnl_line_t line)
{
struct gnl_line_handle *h = (struct gnl_line_handle *)line;
if (h == NULL) {
return -EINVAL;
}
struct gpio_v2_line_event ev;
int count = 0;
/* Drain all currently-buffered events (non-blocking by way of poll). */
while (true) {
struct pollfd pfd = { .fd = h->line_fd, .events = POLLIN };
int pr = poll(&pfd, 1, 0);
if (pr <= 0) {
break;
}
ssize_t n = read(h->line_fd, &ev, sizeof(ev));
if (n != (ssize_t)sizeof(ev)) {
break;
}
count++;
}
return count;
}
uint32_t gnl_poll_fds(const int *fds, size_t count, int timeout_ms)
{
if (count > 32) {
count = 32;
}
struct pollfd pfds[32];
for (size_t i = 0; i < count; i++) {
pfds[i].fd = fds[i];
pfds[i].events = POLLIN;
pfds[i].revents = 0;
}
int ret = poll(pfds, count, timeout_ms);
if (ret <= 0) {
return 0;
}
uint32_t mask = 0;
for (size_t i = 0; i < count; i++) {
if (pfds[i].revents & POLLIN) {
mask |= (1U << i);
}
}
return mask;
}
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: Apache-2.0
*
* Host-side libgpiod v2 wrapper for gpio_native_linux.
* Compiled into the native_simulator INTERFACE so libgpiod headers
* never reach the Zephyr translation unit.
*/
#ifndef ZEPHCORE_GPIO_NATIVE_LINUX_ADAPT_H
#define ZEPHCORE_GPIO_NATIVE_LINUX_ADAPT_H
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Opaque handles — actual types live host-side. */
typedef void *gnl_chip_t;
typedef void *gnl_line_t;
/* Edge mode constants (mirror libgpiod v2 enum) */
#define GNL_EDGE_NONE 0
#define GNL_EDGE_RISING 1
#define GNL_EDGE_FALLING 2
#define GNL_EDGE_BOTH 3
/* Open chip. Returns handle or NULL on error. */
gnl_chip_t gnl_chip_open(const char *path);
void gnl_chip_close(gnl_chip_t chip);
/* Request a single line as OUTPUT with initial value `init_val` (0/1).
* Returns handle or NULL on error. */
gnl_line_t gnl_request_output(gnl_chip_t chip, unsigned int offset,
int init_val, bool active_low);
/* Request a single line as INPUT (no edge detection).
* Returns handle or NULL on error. */
gnl_line_t gnl_request_input(gnl_chip_t chip, unsigned int offset,
bool pull_up, bool pull_down,
bool active_low);
/* Request a single line as INPUT with edge-event detection.
* edge: one of GNL_EDGE_*.
* Returns handle or NULL on error. */
gnl_line_t gnl_request_input_edge(gnl_chip_t chip, unsigned int offset,
int edge, bool pull_up, bool pull_down,
bool active_low);
/* Release a line request. Safe to pass NULL. */
void gnl_line_release(gnl_line_t line);
/* Read current value of an input line (0/1) or -errno on error. */
int gnl_line_get_value(gnl_line_t line);
/* Set output line value (0/1). Returns 0 or -errno. */
int gnl_line_set_value(gnl_line_t line, int value);
/* Get pollable fd for edge events on this line, or -1 if none. */
int gnl_line_get_fd(gnl_line_t line);
/* Drain pending edge events on this line. Returns number drained, or -errno. */
int gnl_line_drain_events(gnl_line_t line);
/* Block on poll() across an array of fds, up to timeout_ms (-1 = forever).
* Returns: bitmask of which fds (by index into the input array) are ready,
* or 0 on timeout, -errno on error. Max 32 fds supported. */
uint32_t gnl_poll_fds(const int *fds, size_t count, int timeout_ms);
#ifdef __cplusplus
}
#endif
#endif /* ZEPHCORE_GPIO_NATIVE_LINUX_ADAPT_H */
@@ -0,0 +1,14 @@
# Copyright (c) 2026 ZephCore
# SPDX-License-Identifier: Apache-2.0
config SPI_NATIVE_LINUX
bool "ZephCore native Linux SPI driver (spidev)"
default y
depends on DT_HAS_ZEPHCORE_SPI_NATIVE_LINUX_ENABLED
depends on ARCH_POSIX
help
Enables the spi_native_linux driver, which forwards Zephyr SPI
transceive calls to a Linux /dev/spidevX.Y character device via
SPI_IOC_MESSAGE ioctl. Used by native_sim builds running on real
Linux SBCs (Femtofox, Raspberry Pi, etc.) that have a LoRa radio
wired up to a hardware SPI bus.
@@ -0,0 +1,332 @@
/*
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: Apache-2.0
*
* Zephyr SPI driver that bridges to a Linux /dev/spidev character device.
* Runs under native_sim on a real Linux host (not in QEMU/simulation).
*
* Host-side ioctls live in spi_native_linux_adapt.c (compiled into the
* native_simulator INTERFACE) to keep host headers out of the Zephyr
* driver translation unit.
*/
#define DT_DRV_COMPAT zephcore_spi_native_linux
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(spi_native_linux, CONFIG_SPI_LOG_LEVEL);
#include <cmdline.h>
#include <posix_native_task.h>
#include "spi_native_linux_adapt.h"
/* spi_context.h uses LOG_* macros, so it must come AFTER LOG_MODULE_REGISTER. */
#include "spi_context.h"
struct spi_native_linux_config {
const char *spi_dev_path;
uint8_t spi_mode;
};
struct spi_native_linux_data {
struct spi_context ctx;
int fd;
uint32_t current_speed_hz;
uint8_t current_mode;
uint8_t current_bits;
bool configured;
};
/* Runtime cmdline override of the SPI device path (applies to instance 0). */
static char *spi_dev_cmd_opt;
static int spi_native_linux_configure(const struct device *dev,
const struct spi_config *config)
{
struct spi_native_linux_data *data = dev->data;
uint8_t mode_byte = 0;
uint8_t bits = 8;
uint32_t speed_hz;
int ret;
if (spi_context_configured(&data->ctx, config)) {
return 0;
}
if (config->operation & SPI_HALF_DUPLEX) {
LOG_ERR("Half-duplex SPI not supported by spidev");
return -ENOTSUP;
}
if ((config->operation & SPI_TRANSFER_LSB) != 0) {
LOG_ERR("LSB-first not supported by spidev");
return -ENOTSUP;
}
if (SPI_WORD_SIZE_GET(config->operation) != 8) {
LOG_ERR("Only 8-bit words supported");
return -ENOTSUP;
}
if (config->operation & SPI_MODE_CPOL) {
mode_byte |= 0x02;
}
if (config->operation & SPI_MODE_CPHA) {
mode_byte |= 0x01;
}
speed_hz = config->frequency;
if (speed_hz == 0U) {
speed_hz = 2000000U;
}
if (mode_byte != data->current_mode) {
ret = spi_native_linux_set_mode(data->fd, mode_byte);
if (ret < 0) {
LOG_ERR("set_mode(%u) failed: %d", mode_byte, ret);
return -EIO;
}
data->current_mode = mode_byte;
}
if (bits != data->current_bits) {
ret = spi_native_linux_set_bits(data->fd, bits);
if (ret < 0) {
LOG_ERR("set_bits(%u) failed: %d", bits, ret);
return -EIO;
}
data->current_bits = bits;
}
if (speed_hz != data->current_speed_hz) {
ret = spi_native_linux_set_speed(data->fd, speed_hz);
if (ret < 0) {
LOG_ERR("set_speed(%u) failed: %d", speed_hz, ret);
return -EIO;
}
data->current_speed_hz = speed_hz;
}
data->ctx.config = config;
data->configured = true;
return 0;
}
/* Sum buffer lengths in a buf_set, treating NULL as zero. */
static size_t bufset_total_len(const struct spi_buf_set *set)
{
size_t total = 0;
if (set == NULL) {
return 0;
}
for (size_t i = 0; i < set->count; i++) {
total += set->buffers[i].len;
}
return total;
}
/*
* Flatten scatter-gather buffers into a single contiguous TX/RX pair,
* issue one SPI_IOC_MESSAGE, then scatter the RX result back.
*
* spidev's SPI_IOC_MESSAGE does support multiple transfers in one ioctl,
* but flattening is dramatically simpler and matches how the SX126x driver
* actually uses SPI (one address byte + payload, no large gathers).
*/
static int spi_native_linux_transceive(const struct device *dev,
const struct spi_config *config,
const struct spi_buf_set *tx_bufs,
const struct spi_buf_set *rx_bufs)
{
struct spi_native_linux_data *data = dev->data;
int ret;
spi_context_lock(&data->ctx, false, NULL, NULL, config);
ret = spi_native_linux_configure(dev, config);
if (ret != 0) {
spi_context_release(&data->ctx, ret);
return ret;
}
size_t tx_total = bufset_total_len(tx_bufs);
size_t rx_total = bufset_total_len(rx_bufs);
size_t xfer_len = tx_total > rx_total ? tx_total : rx_total;
if (xfer_len == 0U) {
spi_context_release(&data->ctx, 0);
return 0;
}
uint8_t *tx_flat = k_malloc(xfer_len);
uint8_t *rx_flat = k_malloc(xfer_len);
if (tx_flat == NULL || rx_flat == NULL) {
k_free(tx_flat);
k_free(rx_flat);
LOG_ERR("Out of memory for %zu-byte SPI buffer", xfer_len);
spi_context_release(&data->ctx, -ENOMEM);
return -ENOMEM;
}
memset(tx_flat, 0, xfer_len);
memset(rx_flat, 0, xfer_len);
/* Pack TX bytes */
if (tx_bufs != NULL) {
size_t off = 0;
for (size_t i = 0; i < tx_bufs->count; i++) {
const struct spi_buf *b = &tx_bufs->buffers[i];
if (b->buf != NULL && b->len > 0U) {
memcpy(tx_flat + off, b->buf, b->len);
}
off += b->len;
}
}
/* Manually drive CS via spi_context (which uses GPIO from DT spi-cs-gpios). */
spi_context_cs_control(&data->ctx, true);
ret = spi_native_linux_xfer(data->fd, tx_flat, rx_flat, xfer_len,
data->current_speed_hz);
spi_context_cs_control(&data->ctx, false);
if (ret < 0) {
LOG_ERR("SPI_IOC_MESSAGE failed: %d", ret);
k_free(tx_flat);
k_free(rx_flat);
spi_context_release(&data->ctx, -EIO);
return -EIO;
}
/* Scatter RX bytes back */
if (rx_bufs != NULL) {
size_t off = 0;
for (size_t i = 0; i < rx_bufs->count; i++) {
const struct spi_buf *b = &rx_bufs->buffers[i];
if (b->buf != NULL && b->len > 0U) {
memcpy(b->buf, rx_flat + off, b->len);
}
off += b->len;
}
}
k_free(tx_flat);
k_free(rx_flat);
spi_context_release(&data->ctx, 0);
return 0;
}
static int spi_native_linux_release(const struct device *dev,
const struct spi_config *config)
{
struct spi_native_linux_data *data = dev->data;
spi_context_unlock_unconditionally(&data->ctx);
return 0;
}
static DEVICE_API(spi, spi_native_linux_driver_api) = {
.transceive = spi_native_linux_transceive,
.release = spi_native_linux_release,
};
static int spi_native_linux_init(const struct device *dev)
{
const struct spi_native_linux_config *cfg = dev->config;
struct spi_native_linux_data *data = dev->data;
const char *path;
int ret;
path = (spi_dev_cmd_opt != NULL) ? spi_dev_cmd_opt : cfg->spi_dev_path;
LOG_INF("Opening SPI host device: %s", path);
data->fd = spi_native_linux_open(path);
if (data->fd < 0) {
LOG_ERR("Failed to open %s: %d", path, data->fd);
return -ENODEV;
}
data->current_speed_hz = 0;
data->current_mode = 0xFF;
data->current_bits = 0xFF;
data->configured = false;
/* Apply initial mode hint from DT so the line is in a sane state
* before the first transceive() configures it from spi_config. */
if (cfg->spi_mode != 0xFF) {
ret = spi_native_linux_set_mode(data->fd, cfg->spi_mode);
if (ret == 0) {
data->current_mode = cfg->spi_mode;
}
}
ret = spi_context_cs_configure_all(&data->ctx);
if (ret < 0) {
LOG_ERR("CS GPIO configure failed: %d", ret);
spi_native_linux_close(data->fd);
data->fd = -1;
return ret;
}
spi_context_unlock_unconditionally(&data->ctx);
LOG_INF("SPI native_linux driver ready (fd=%d)", data->fd);
return 0;
}
#define SPI_NATIVE_LINUX_INIT(inst) \
static const struct spi_native_linux_config \
spi_native_linux_cfg_##inst = { \
.spi_dev_path = DT_INST_PROP(inst, spi_dev), \
.spi_mode = DT_INST_PROP_OR(inst, spi_mode, 0), \
}; \
\
static struct spi_native_linux_data spi_native_linux_data_##inst = { \
SPI_CONTEXT_INIT_LOCK(spi_native_linux_data_##inst, ctx), \
SPI_CONTEXT_INIT_SYNC(spi_native_linux_data_##inst, ctx), \
SPI_CONTEXT_CS_GPIOS_INITIALIZE(DT_DRV_INST(inst), ctx) \
}; \
\
SPI_DEVICE_DT_INST_DEFINE(inst, \
spi_native_linux_init, \
NULL, \
&spi_native_linux_data_##inst, \
&spi_native_linux_cfg_##inst, \
POST_KERNEL, CONFIG_SPI_INIT_PRIORITY, \
&spi_native_linux_driver_api);
DT_INST_FOREACH_STATUS_OKAY(SPI_NATIVE_LINUX_INIT)
/* Command-line arg registration: --lora-spidev=<path> overrides DT spi-dev */
static void spi_native_linux_add_cmdline_opts(void)
{
static struct args_struct_t spi_native_options[] = {
{
.option = "lora-spidev",
.name = "path",
.type = 's',
.dest = (void *)&spi_dev_cmd_opt,
.descript = "Linux SPI device path (overrides DT spi-dev)",
},
ARG_TABLE_ENDMARKER,
};
native_add_command_line_opts(spi_native_options);
}
NATIVE_TASK(spi_native_linux_add_cmdline_opts, PRE_BOOT_1, 11);
@@ -0,0 +1,105 @@
/*
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: Apache-2.0
*
* Host-side spidev wrapper functions for spi_native_linux.
*
* This file is compiled into the native_simulator INTERFACE target, NOT
* the Zephyr application. That keeps host headers (<linux/spi/spidev.h>,
* <sys/ioctl.h>) out of the Zephyr translation unit, avoiding type
* collisions between the Zephyr kernel and the host C library.
*
* Only plain C ABI types cross the boundary back into the Zephyr-side
* driver (see spi_native_linux_adapt.h).
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>
#ifdef __linux
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
#else
#error "spi_native_linux only builds on Linux hosts"
#endif
#include "spi_native_linux_adapt.h"
int spi_native_linux_open(const char *path)
{
int fd = open(path, O_RDWR);
if (fd < 0) {
return -errno;
}
return fd;
}
void spi_native_linux_close(int fd)
{
if (fd >= 0) {
close(fd);
}
}
int spi_native_linux_set_mode(int fd, uint8_t mode)
{
uint8_t m = mode;
if (ioctl(fd, SPI_IOC_WR_MODE, &m) < 0) {
return -errno;
}
return 0;
}
int spi_native_linux_set_bits(int fd, uint8_t bits)
{
uint8_t b = bits;
if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &b) < 0) {
return -errno;
}
return 0;
}
int spi_native_linux_set_speed(int fd, uint32_t speed_hz)
{
uint32_t s = speed_hz;
if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &s) < 0) {
return -errno;
}
return 0;
}
int spi_native_linux_xfer(int fd, const uint8_t *tx, uint8_t *rx,
size_t len, uint32_t speed_hz)
{
struct spi_ioc_transfer xfer;
if (len == 0) {
return 0;
}
memset(&xfer, 0, sizeof(xfer));
xfer.tx_buf = (uintptr_t)tx;
xfer.rx_buf = (uintptr_t)rx;
xfer.len = (uint32_t)len;
xfer.speed_hz = speed_hz;
xfer.bits_per_word = 8;
xfer.cs_change = 0;
xfer.delay_usecs = 0;
int ret = ioctl(fd, SPI_IOC_MESSAGE(1), &xfer);
if (ret < 0) {
return -errno;
}
return ret;
}
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2026 ZephCore
* SPDX-License-Identifier: Apache-2.0
*
* Host-side spidev wrapper functions for spi_native_linux.
* Compiled into the native_simulator INTERFACE so host headers
* (<linux/spi/spidev.h>, etc.) never reach the Zephyr translation unit.
*/
#ifndef ZEPHCORE_SPI_NATIVE_LINUX_ADAPT_H
#define ZEPHCORE_SPI_NATIVE_LINUX_ADAPT_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Open /dev/spidevX.Y, return fd >= 0 on success or -errno. */
int spi_native_linux_open(const char *path);
/* Close the fd. */
void spi_native_linux_close(int fd);
/* Set SPI mode (0..3) via SPI_IOC_WR_MODE. Returns 0 or -errno. */
int spi_native_linux_set_mode(int fd, uint8_t mode);
/* Set bits-per-word via SPI_IOC_WR_BITS_PER_WORD. Returns 0 or -errno. */
int spi_native_linux_set_bits(int fd, uint8_t bits);
/* Set max clock speed via SPI_IOC_WR_MAX_SPEED_HZ. Returns 0 or -errno. */
int spi_native_linux_set_speed(int fd, uint32_t speed_hz);
/* Full-duplex transfer of `len` bytes via SPI_IOC_MESSAGE(1).
* tx and rx must each be `len` bytes (rx receives the input, tx is sent).
* Returns bytes transferred on success, -errno on failure.
*/
int spi_native_linux_xfer(int fd, const uint8_t *tx, uint8_t *rx,
size_t len, uint32_t speed_hz);
#ifdef __cplusplus
}
#endif
#endif /* ZEPHCORE_SPI_NATIVE_LINUX_ADAPT_H */
@@ -0,0 +1,40 @@
# Copyright (c) 2026 ZephCore
# SPDX-License-Identifier: Apache-2.0
description: |
ZephCore GPIO controller using Linux libgpiod v2.
Bridges Zephyr's GPIO subsystem to a Linux /dev/gpiochipX character
device via libgpiod v2 (gpiod_line_request_*). Used by native_sim
builds running on real Linux SBCs.
The host gpiochip path defaults to the DT property below, but can be
overridden at runtime with the --lora-gpio-chip=<path> command-line
argument.
compatible: "zephcore,gpio-native-linux"
include: [gpio-controller.yaml, base.yaml]
properties:
gpio-chip:
type: string
required: true
description: |
Linux gpiochip character device path, e.g. "/dev/gpiochip0".
Overridable at runtime via --lora-gpio-chip=<path>.
ngpios:
type: int
required: true
description: |
Number of pins exposed by this controller. The actual number of
lines on the host gpiochip can be larger; we just cap our pin
space here. Should be at least 32 for typical SX126x setups.
"#gpio-cells":
const: 2
gpio-cells:
- pin
- flags
@@ -0,0 +1,32 @@
# Copyright (c) 2026 ZephCore
# SPDX-License-Identifier: Apache-2.0
description: |
ZephCore SPI driver using Linux spidev.
Bridges Zephyr's SPI subsystem to a Linux /dev/spidevX.Y character device
via SPI_IOC_MESSAGE ioctl. CS is driven via the linked GPIO controller
(spi-cs-gpios), not by spidev's hardware CS, so the SX126x driver's
software CS sequencing keeps working unchanged.
The host SPI device path defaults to the DT property below, but can be
overridden at runtime with the --lora-spidev=<path> command-line argument.
compatible: "zephcore,spi-native-linux"
include: [spi-controller.yaml]
properties:
spi-dev:
type: string
required: true
description: |
Linux host SPI character device path, e.g. "/dev/spidev0.0".
Overridable at runtime via --lora-spidev=<path>.
spi-mode:
type: int
default: 0
description: |
SPI mode byte passed to SPI_IOC_WR_MODE (0..3).
0 = CPOL=0/CPHA=0 (SX126x default).
@@ -0,0 +1,71 @@
diff --git a/drivers/spi/CMakeLists.txt b/drivers/spi/CMakeLists.txt
--- a/drivers/spi/CMakeLists.txt
+++ b/drivers/spi/CMakeLists.txt
@@ -88,3 +88,16 @@ if(CONFIG_SPI_SC18IS606)
zephyr_library_include_directories(${ZEPHYR_BASE}/drivers/mfd)
zephyr_library_sources(spi_sc18is606.c)
endif()
+
+# ZephCore patch: native_sim Linux spidev SPI driver.
+# Zephyr-side driver compiled as part of the application, host-side
+# adapt file compiled into the native_simulator INTERFACE so host
+# headers (<linux/spi/spidev.h>) don't reach the Zephyr translation unit.
+if(CONFIG_SPI_NATIVE_LINUX)
+ if(${CMAKE_HOST_SYSTEM_NAME} STREQUAL Linux)
+ zephyr_library_sources(spi_native_linux.c)
+ target_sources(native_simulator INTERFACE spi_native_linux_adapt.c)
+ else()
+ message(FATAL_ERROR "CONFIG_SPI_NATIVE_LINUX is only available on Linux hosts")
+ endif()
+endif()
diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -1,4 +1,5 @@
# SPI driver configuration options
+# ZephCore patch: adds native_linux SPI driver Kconfig.
# Copyright (c) 2015-2016 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
@@ -129,6 +130,7 @@ source "drivers/spi/Kconfig.mcux_ecspi"
source "drivers/spi/Kconfig.mcux_flexcomm"
source "drivers/spi/Kconfig.mcux_flexio"
source "drivers/spi/Kconfig.npcx"
+source "drivers/spi/Kconfig.native_linux"
source "drivers/spi/Kconfig.nrfx"
source "drivers/spi/Kconfig.numaker"
source "drivers/spi/Kconfig.nxp_s32"
diff --git a/drivers/gpio/CMakeLists.txt b/drivers/gpio/CMakeLists.txt
--- a/drivers/gpio/CMakeLists.txt
+++ b/drivers/gpio/CMakeLists.txt
@@ -150,3 +150,13 @@ if(CONFIG_GPIO_SC18IS606)
zephyr_library_include_directories(${ZEPHYR_BASE}/drivers/mfd)
zephyr_library_sources(gpio_sc18is606.c)
endif()
+
+# ZephCore patch: native_sim Linux GPIO driver via kernel V2 chardev ioctl.
+if(CONFIG_GPIO_NATIVE_LINUX)
+ if(${CMAKE_HOST_SYSTEM_NAME} STREQUAL Linux)
+ zephyr_library_sources(gpio_native_linux.c)
+ target_sources(native_simulator INTERFACE gpio_native_linux_adapt.c)
+ else()
+ message(FATAL_ERROR "CONFIG_GPIO_NATIVE_LINUX is only available on Linux hosts")
+ endif()
+endif()
diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -1,4 +1,5 @@
# GPIO configuration options
+# ZephCore patch: adds native_linux GPIO driver Kconfig.
# Copyright (c) 2015 Intel Corporation
# SPDX-License-Identifier: Apache-2.0
@@ -154,6 +155,7 @@ source "drivers/gpio/Kconfig.mcux_rgpio"
source "drivers/gpio/Kconfig.mfxstm32l152"
source "drivers/gpio/Kconfig.mmio32"
source "drivers/gpio/Kconfig.mspm0"
+source "drivers/gpio/Kconfig.native_linux"
source "drivers/gpio/Kconfig.nct38xx"
source "drivers/gpio/Kconfig.neorv32"
source "drivers/gpio/Kconfig.npcx"
+9
View File
@@ -500,6 +500,7 @@ static void gps_fix_callback(double lat, double lon, int64_t utc_time)
#endif
}
#if IS_ENABLED(CONFIG_BT)
/* bt_ready callback — BLE stack is up, start advertising */
static void bt_ready(int err)
{
@@ -517,6 +518,7 @@ static void bt_ready(int err)
zephcore_ble_set_enabled(false);
}
}
#endif /* CONFIG_BT */
int main(void)
{
@@ -749,9 +751,16 @@ int main(void)
&zephyr_board);
#endif
#if IS_ENABLED(CONFIG_BT)
if (bt_enable(bt_ready) != 0) {
LOG_ERR("bt_enable failed");
}
#else
/* No BLE controller — TCP companion transport starts itself.
* zephcore_ble_start() is provided by LinuxTCPTransport.c when
* CONFIG_ZEPHCORE_TRANSPORT_TCP=y. */
zephcore_ble_start(companion_mesh.getDeviceName());
#endif
/*
* FULLY EVENT-DRIVEN architecture: main thread runs mesh event loop.