port lora e5 mini

This commit is contained in:
liquidraver committed 2026-06-21 20:38:57 +02:00
1 parent c6b4816cc4
commit 2f86310500
13 files changed
+776 -19

No files matched your search

+10 -1
View File
@@ -281,6 +281,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 "lora_e5" OR BOARD MATCHES ".*stm32wl.*")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/common/stm32wl_common.conf")
elseif(BOARD MATCHES "native_sim" OR BOARD MATCHES "native_posix")
set(ZEPHCORE_PLATFORM_CONF "${CMAKE_CURRENT_SOURCE_DIR}/boards/linux_native/linux_common.conf")
else()
@@ -658,7 +660,8 @@ else()
helpers/ui/ui_mesh_actions.cpp
app/CompanionMesh.cpp
)
# Companion transport: TCP socket on native Linux, BLE NUS on MCU builds.
# Companion transport: TCP socket on native Linux, serial (UART) on boards
# with no Bluetooth controller (e.g. STM32WL), BLE NUS everywhere else.
if(CONFIG_ZEPHCORE_TRANSPORT_TCP)
message(STATUS "ZephCore Companion Transport: TCP (Linux)")
target_sources(app PRIVATE adapters/transport/LinuxTCPTransport.c)
@@ -669,6 +672,12 @@ else()
target_include_directories(app PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/helpers/ui
)
elseif(NOT CONFIG_BT)
# No BLE controller (STM32WL) — companion speaks MeshCore serial framing
# over the chosen console UART. SerialCompanionTransport.c is a full
# drop-in zephcore_ble_* provider, just like LinuxTCPTransport.c.
message(STATUS "ZephCore Companion Transport: Serial (UART)")
target_sources(app PRIVATE adapters/transport/SerialCompanionTransport.c)
else()
message(STATUS "ZephCore Companion Transport: BLE NUS")
target_sources(app PRIVATE adapters/ble/ZephyrBLE.cpp)
@@ -38,6 +38,7 @@
#include <zephyr/logging/log.h>
#include "ZephyrBLE.h"
#include "companion_framing.h"
LOG_MODULE_REGISTER(linux_tcp_transport, LOG_LEVEL_INF);
@@ -45,9 +46,9 @@ LOG_MODULE_REGISTER(linux_tcp_transport, LOG_LEVEL_INF);
#define LISTEN_PORT CONFIG_ZEPHCORE_LINUX_TCP_PORT
#define LISTEN_BACKLOG 1
/* TX congestion control (mirrors ZephyrBLE.cpp). Push codes >= 0x80 are
* lossy event signals; protocol response frames < 0x80 are lossless. */
#define PUSH_CODE_BASE 0x80
/* TX congestion control (mirrors ZephyrBLE.cpp). Lossy (>= 0x80) vs lossless
* (< 0x80) classification is shared via companion_is_lossless_protocol_frame()
* in companion_framing.h. */
#define TX_OVERFLOW_RETRY_MS 250
/* Bound on a single blocking send. tx_drain_work_fn runs on the system
@@ -57,10 +58,8 @@ LOG_MODULE_REGISTER(linux_tcp_transport, LOG_LEVEL_INF);
* let the app reconnect rather than stall the node. */
#define TX_SEND_TIMEOUT_SEC 2
struct frame {
uint16_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
/* struct frame + framing constants + lossless classification are shared via
* companion_framing.h (included above). */
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);
@@ -104,14 +103,6 @@ static bool overflow_pending;
static void overflow_retry_work_fn(struct k_work *work);
static K_WORK_DELAYABLE_DEFINE(overflow_retry_work, overflow_retry_work_fn);
static bool is_lossless_protocol_frame(const uint8_t *data, uint16_t len)
{
if (!data || len == 0) {
return false;
}
return data[0] < PUSH_CODE_BASE;
}
static void close_client_locked(void)
{
if (client_fd >= 0) {
@@ -469,7 +460,7 @@ size_t zephcore_ble_send(const uint8_t *data, uint16_t len)
/* Lossless protocol response frames are never dropped — report
* failure so the caller retries instead of clobbering overflow. */
if (is_lossless_protocol_frame(data, len)) {
if (companion_is_lossless_protocol_frame(data, len)) {
LOG_DBG("TX queue full for lossless frame hdr=0x%02x, retry later",
data[0]);
return 0;
@@ -0,0 +1,478 @@
/*
* SPDX-License-Identifier: MIT
* ZephCore serial (UART) companion transport — drop-in zephcore_ble_* provider.
*
* For boards with NO Bluetooth and NO USB device controller (e.g. the Seeed
* LoRa-E5 / STM32WL family), the companion app talks to the node over a plain
* UART — on the LoRa-E5 mini that UART is bridged to USB-C by an onboard
* USB-UART chip, so the official MeshCore serial client connects to it exactly
* like a native-USB companion.
*
* This file is the UART analogue of adapters/transport/LinuxTCPTransport.c: it
* implements the full zephcore_ble_* C API (adapters/ble/ZephyrBLE.h) so the
* companion application is transport-agnostic. It is compiled INSTEAD of
* ZephyrBLE.cpp when CONFIG_BT=n and CONFIG_ZEPHCORE_TRANSPORT_TCP=n
* (see the companion-transport selection in CMakeLists.txt). The MeshCore
* '<'/'>' framing + struct frame + congestion classification are shared with
* the TCP transport via companion_framing.h.
*
* I/O is interrupt-driven (uart_irq) on both directions so a contact-sync burst
* (many ~148B frames back-to-back) drains at full line rate with real
* back-pressure instead of blocking the system workqueue on uart_poll_out.
*
* Session model: a UART has no connect/disconnect line event (no DTR — the
* official client never asserts it), and a connected companion may sit idle for
* many minutes between frames. We therefore mark "connected" on the first
* inbound frame (fires on_connected once, lights the UI indicator) and STAY
* connected; we never infer a disconnect from idleness. Per-session reset on a
* reconnecting client is driven by the protocol's CMD_APP_START handler, not by
* any inactivity timer. The only timeout here is a parser-level resync for a
* partial frame that never completes (below) — it touches the RX byte parser
* only, never the session/connection state.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/logging/log.h>
#include "ZephyrBLE.h"
#include "companion_framing.h"
LOG_MODULE_REGISTER(serial_companion, LOG_LEVEL_INF);
#define FRAME_QUEUE_SIZE CONFIG_ZEPHCORE_BLE_QUEUE_SIZE
#define TX_RING_SIZE 2048 /* ~13 contact frames of headroom */
#define TX_OVERFLOW_RETRY_MS 250
/* Parser-only resync: if a frame has STARTED ('<' seen) but its payload never
* completes within this window, the next inbound byte resets the parser to
* IDLE so a lost/truncated frame can't swallow the following frame. This is NOT
* a session/idle timeout — an idle-but-connected companion sits in RX_IDLE
* between frames and never arms this, and it never affects connection state. */
#define FRAME_PARTIAL_TIMEOUT_MS 2000
/* Companion UART — the chosen console UART (USART1 on the LoRa-E5 mini). */
static const struct device *uart_dev;
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;
/* ---- RX byte-assembly state (runs in UART ISR) ---------------------------- */
enum rx_state {
RX_IDLE = 0, /* waiting for '<' */
RX_LEN_LO,
RX_LEN_HI,
RX_PAYLOAD,
};
static enum rx_state rx_st;
static uint16_t rx_idx;
static uint16_t rx_expect;
static uint32_t rx_frame_start; /* k_uptime_get_32() when the '<' arrived */
static struct frame rx_frame;
/* ---- TX side: interrupt-driven ring (whole frames committed atomically) ---- */
static uint8_t tx_ring_data[TX_RING_SIZE];
static struct ring_buf tx_ring;
static struct k_spinlock tx_lock;
/* Order-preserving holdover: when the ring is momentarily full, the popped
* frame is parked here (NOT requeued at the tail, which would reorder it behind
* later frames) and re-tried first on the next drain. Touched only by
* tx_drain_work_fn, which runs solely on the system workqueue (never concurrent
* with itself), so it needs no lock. */
static struct frame held_frame;
static bool held_pending;
/* ---- TX queue drain (moves frames from ble_send_queue into the TX ring) ----
* Runs on the system workqueue; never blocks on the wire (the ISR does the
* byte-level draining), so a full sync burst can't starve the workqueue. */
static void tx_drain_work_fn(struct k_work *work);
static K_WORK_DEFINE(tx_drain_work, tx_drain_work_fn);
/* on_connected is deferred off the ISR onto the system workqueue (it ends up
* in ui_notify(), which is not guaranteed ISR-safe). */
static void connect_work_fn(struct k_work *work);
static K_WORK_DEFINE(connect_work, connect_work_fn);
/* ---- TX congestion control (mirrors LinuxTCPTransport.c / ZephyrBLE.cpp) ---- */
static bool tx_congested;
static struct frame overflow_frame;
static bool overflow_pending;
static void overflow_retry_work_fn(struct k_work *work);
static K_WORK_DELAYABLE_DEFINE(overflow_retry_work, overflow_retry_work_fn);
static void connect_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
LOG_INF("serial companion session started");
if (transport_cbs && transport_cbs->on_connected) {
transport_cbs->on_connected();
}
}
/* Mark the session connected on first inbound traffic (fires on_connected once
* — the UI "connected" indicator). Called from the UART ISR, so the callback is
* deferred to a workqueue rather than run inline. */
static void mark_connected(void)
{
if (active_iface != ZEPHCORE_IFACE_NONE) {
return;
}
active_iface = ZEPHCORE_IFACE_BLE; /* reuse the BLE path in main_companion */
k_work_submit(&connect_work);
}
/* ---- UART ISR: drains RX into the frame parser, fills TX from the ring ----- */
static void uart_isr(const struct device *dev, void *user_data)
{
ARG_UNUSED(user_data);
while (uart_irq_update(dev) && uart_irq_is_pending(dev)) {
if (uart_irq_rx_ready(dev)) {
uint8_t buf[64];
int n = uart_fifo_read(dev, buf, sizeof(buf));
/* Parser-only resync: drop a partial frame that stalled
* mid-payload so it can't consume the bytes of the next
* frame. Connection state is untouched. */
if (rx_st != RX_IDLE &&
(k_uptime_get_32() - rx_frame_start) > FRAME_PARTIAL_TIMEOUT_MS) {
rx_st = RX_IDLE;
}
for (int i = 0; i < n; i++) {
uint8_t b = buf[i];
switch (rx_st) {
case RX_IDLE:
if (b == COMPANION_FRAME_RX_SYNC) {
rx_st = RX_LEN_LO;
rx_frame_start = k_uptime_get_32();
}
break;
case RX_LEN_LO:
rx_expect = b;
rx_st = RX_LEN_HI;
break;
case RX_LEN_HI:
rx_expect |= ((uint16_t)b) << 8;
rx_idx = 0;
if (rx_expect == 0 || rx_expect > MAX_FRAME_SIZE) {
rx_st = RX_IDLE; /* bad length, resync */
} else {
rx_st = RX_PAYLOAD;
}
break;
default: /* RX_PAYLOAD */
rx_frame.buf[rx_idx++] = b;
if (rx_idx >= rx_expect) {
rx_frame.len = rx_expect;
mark_connected();
/* on_rx_frame queues to ble_recv_queue and
* wakes the mesh thread (ISR-safe: K_NO_WAIT
* k_msgq_put + k_event_post), same as
* ZephyrBLE's secure_nus_rx_write. */
if (transport_cbs && transport_cbs->on_rx_frame) {
transport_cbs->on_rx_frame(rx_frame.buf,
rx_frame.len);
}
rx_st = RX_IDLE;
}
break;
}
}
}
if (uart_irq_tx_ready(dev)) {
uint8_t *out;
bool empty;
k_spinlock_key_t key = k_spin_lock(&tx_lock);
uint32_t claimed = ring_buf_get_claim(&tx_ring, &out, 64);
if (claimed > 0) {
int sent = uart_fifo_fill(dev, out, claimed);
ring_buf_get_finish(&tx_ring, sent > 0 ? sent : 0);
}
empty = ring_buf_is_empty(&tx_ring);
if (empty) {
/* Disable inside the lock so a concurrent enqueue
* can't be stranded (it re-enables after its put). */
uart_irq_tx_disable(dev);
}
k_spin_unlock(&tx_lock, key);
if (empty) {
/* Ring drained — pull the next batch from the queue.
* tx_drain_work fires on_tx_idle when fully drained. */
k_work_submit(&tx_drain_work);
}
}
}
}
/* Queue one framed frame into the TX ring. Returns true if it fit (committed
* atomically — header + payload together — so frames never tear). */
static bool tx_ring_put_frame(const struct frame *f)
{
uint8_t hdr[3] = {
COMPANION_FRAME_TX_SYNC,
(uint8_t)(f->len & 0xFF),
(uint8_t)((f->len >> 8) & 0xFF),
};
size_t total = sizeof(hdr) + f->len;
k_spinlock_key_t key = k_spin_lock(&tx_lock);
if (ring_buf_space_get(&tx_ring) < total) {
k_spin_unlock(&tx_lock, key);
return false;
}
ring_buf_put(&tx_ring, hdr, sizeof(hdr));
ring_buf_put(&tx_ring, f->buf, f->len);
k_spin_unlock(&tx_lock, key);
uart_irq_tx_enable(uart_dev);
return true;
}
static void tx_drain_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
struct frame f;
/* Re-try the order-preserving holdover first. If it still doesn't fit,
* leave it parked — the TX ISR re-submits this work when the ring drains. */
if (held_pending) {
if (!tx_ring_put_frame(&held_frame)) {
return;
}
held_pending = false;
}
/* Move queued frames into the TX ring while it has room. On ring-full,
* park the popped frame in the single holdover slot (preserving FIFO
* order) rather than requeuing it behind later frames. */
while (k_msgq_get(&ble_send_queue, &f, K_NO_WAIT) == 0) {
if (!tx_ring_put_frame(&f)) {
held_frame = f;
held_pending = true;
return;
}
/* Clear congestion at the 1/3 low-water mark (hysteresis). */
if (tx_congested &&
k_msgq_num_used_get(&ble_send_queue) <= FRAME_QUEUE_SIZE / 3) {
tx_congested = false;
LOG_INF("tx: congestion cleared");
}
}
/* Queue + holdover fully drained into the ring. Fire on_tx_idle only once
* the wire is also idle (ring empty) so the contact pump paces to real
* throughput. */
if (transport_cbs && transport_cbs->on_tx_idle && !held_pending) {
bool ring_empty;
k_spinlock_key_t key = k_spin_lock(&tx_lock);
ring_empty = ring_buf_is_empty(&tx_ring);
k_spin_unlock(&tx_lock, key);
if (ring_empty) {
transport_cbs->on_tx_idle();
}
}
}
static void overflow_retry_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
if (!overflow_pending) {
return;
}
if (k_msgq_put(&ble_send_queue, &overflow_frame, K_NO_WAIT) == 0) {
overflow_pending = false;
k_work_submit(&tx_drain_work);
} else {
k_work_schedule(&overflow_retry_work, K_MSEC(TX_OVERFLOW_RETRY_MS));
}
}
/* ========== Public API (matches ZephyrBLE.h) ========== */
void zephcore_ble_init(const struct ble_callbacks *cbs)
{
transport_cbs = cbs;
transport_enabled = true;
ring_buf_init(&tx_ring, sizeof(tx_ring_data), tx_ring_data);
}
void zephcore_ble_start(const char *device_name)
{
ARG_UNUSED(device_name);
uart_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_console));
if (!device_is_ready(uart_dev)) {
LOG_ERR("companion UART not ready");
uart_dev = NULL;
return;
}
uart_irq_callback_set(uart_dev, uart_isr);
uart_irq_rx_enable(uart_dev);
LOG_INF("serial companion transport ready on %s", uart_dev->name);
}
size_t zephcore_ble_send(const uint8_t *data, uint16_t len)
{
if (!uart_dev || len == 0 || len > MAX_FRAME_SIZE) {
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) {
/* Queue full — congestion mode (mirrors LinuxTCPTransport.c). */
if (!tx_congested) {
LOG_WRN("TX queue full (%u/%u), entering congestion",
k_msgq_num_used_get(&ble_send_queue),
(unsigned)FRAME_QUEUE_SIZE);
tx_congested = true;
}
/* Lossless protocol responses are never dropped — report failure
* so the caller retries instead of clobbering overflow. */
if (companion_is_lossless_protocol_frame(data, len)) {
return 0;
}
if (overflow_pending) {
LOG_WRN("overflow full, dropping push hdr=0x%02x", data[0]);
return 0;
}
overflow_frame = f;
overflow_pending = true;
k_work_schedule(&overflow_retry_work, K_MSEC(TX_OVERFLOW_RETRY_MS));
return len;
}
k_work_submit(&tx_drain_work);
return len;
}
void zephcore_ble_set_enabled(bool enable)
{
transport_enabled = enable;
if (!uart_dev) {
return;
}
/* Actually gate the link, not just a flag: stop accepting frames when
* disabled (mirrors the TCP transport closing its socket). */
if (enable) {
uart_irq_rx_enable(uart_dev);
} else {
uart_irq_rx_disable(uart_dev);
}
}
bool zephcore_ble_is_enabled(void)
{
return transport_enabled;
}
bool zephcore_ble_is_active(void)
{
return uart_dev != NULL && active_iface == ZEPHCORE_IFACE_BLE;
}
bool zephcore_ble_is_connected(void)
{
return active_iface == ZEPHCORE_IFACE_BLE;
}
bool zephcore_ble_is_congested(void)
{
return tx_congested;
}
bool zephcore_ble_is_advertising(void)
{
/* A UART is "always advertising" once the transport is up — keeps the
* companion housekeeping loop from trying to (re)start advertising. */
return uart_dev != NULL;
}
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;
}
bool zephcore_ble_iface_try_claim(enum zephcore_iface who)
{
if (active_iface == ZEPHCORE_IFACE_NONE || active_iface == who) {
active_iface = who;
return true;
}
return false;
}
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)
{
active_iface = ZEPHCORE_IFACE_NONE;
if (transport_cbs && transport_cbs->on_disconnected) {
transport_cbs->on_disconnected();
}
}
void zephcore_ble_conn_params_ready(void)
{
/* No link-layer connection parameters on a UART — no-op. */
}
void zephcore_ble_update_name(const char *new_name)
{
/* No advertising payload on a UART — no-op. */
ARG_UNUSED(new_name);
}
@@ -0,0 +1,38 @@
/*
* SPDX-License-Identifier: MIT
* Shared MeshCore companion serial framing — used by the non-BLE companion
* transports (LinuxTCPTransport.c, SerialCompanionTransport.c).
*
* Wire format (MeshCore ArduinoSerialInterface):
* App -> Node: '<' (0x3C) | len_LSB | len_MSB | payload...
* Node -> App: '>' (0x3E) | len_LSB | len_MSB | payload...
*
* `struct frame` is the unit stored in the ble_send_queue/ble_recv_queue; its
* layout must match what main_companion.cpp puts on those queues (len + buf).
*/
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "ZephyrBLE.h" /* MAX_FRAME_SIZE */
#define COMPANION_FRAME_RX_SYNC '<'
#define COMPANION_FRAME_TX_SYNC '>'
/* Push codes >= 0x80 are lossy event signals (droppable under congestion);
* protocol responses < 0x80 are lossless and must never be silently dropped. */
#define COMPANION_PUSH_CODE_BASE 0x80
struct frame {
uint16_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
static inline bool companion_is_lossless_protocol_frame(const uint8_t *data,
uint16_t len)
{
return data != NULL && len > 0 && data[0] < COMPANION_PUSH_CODE_BASE;
}
+10 -1
View File
@@ -1635,8 +1635,17 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
sendPacketError(ERR_ILLEGAL_ARG);
return true;
}
// Reset contact iterator for fresh start
// Reset per-session state for a fresh app session. BLE/USB also run
// this cleanup on disconnect, but the serial transport has no
// disconnect event, so APP_START is its authoritative session-reset
// point: drop a stale contact iteration (a leftover PACKET_CONTACT_END
// would confuse the new session's sync state machine), a half-finished
// message sync, and free an abandoned Ed25519 sign buffer (else an 8KB
// leak if the previous session dropped mid-CMD_SIGN). Idempotent — all
// no-ops when the state is already clean.
_contact_iter_active = false;
cancelSyncPending();
cleanupSignState();
// Return SELF_INFO
uint8_t rsp[90]; // 58 fixed + up to 32 bytes name
@@ -0,0 +1,55 @@
# STM32WL Platform Configuration (Seeed LoRa-E5 / STM32WLE5JC)
#
# Included by all STM32WL boards. STM32WL has NO Bluetooth radio and NO USB
# peripheral, so these boards are UART-only: the repeater CLI (and the serial
# companion) run over USART1 (chosen zephyr,console) through the board's
# onboard USB-UART bridge. Flashing is over SWD/ST-Link (west flash / OpenOCD).
#
# Include order: prj.conf -> zephcore_common.conf -> stm32wl_common.conf -> board.conf
# Only contains STM32WL-specific settings; common app/crypto/storage is in
# zephcore_common.conf.
# ========== Boot / Flash layout ==========
# No MCUboot: the application links at the flash origin via the single
# slot0_partition defined in board.overlay (zephyr,code-partition).
CONFIG_USE_DT_CODE_PARTITION=y
# ========== No Bluetooth ==========
# STM32WL has no BLE radio. Disabling here makes every build on this platform
# BT-free (the companion transport is serial, not NUS). Mirrors repeater.conf.
CONFIG_BT=n
# ========== No USB device stack ==========
# STM32WLE5JC has no USB peripheral. Console/CLI is plain UART
# (UART_INTERRUPT_DRIVEN already set in prj.conf). No UF2 output.
# ========== Entropy / CSPRNG ==========
# STM32WL provides only a raw TRNG (st,stm32-rng), not a hardware CSPRNG.
# Enable the TRNG as the entropy source; with an entropy driver present,
# CONFIG_CSPRNG_ENABLED defaults y and sys_csrand_get() is backed by the
# hardware CS generator. ZephyrRNG additionally conditions identity seeds with
# CPU-jitter + AES-256-CTR (see adapters/rng/ZephyrRNG.cpp), so a weak/slow
# TRNG cannot silently produce correlated keys. CONFIG_CSPRNG_ENABLED (which
# ZephyrRNG BUILD_ASSERTs on) is not directly assignable -- it defaults y once
# an entropy driver is present (subsys/random/Kconfig), so enabling the TRNG
# below is sufficient.
CONFIG_ENTROPY_GENERATOR=y
CONFIG_ENTROPY_STM32_RNG=y
# ========== Memory (64KB SRAM / 256KB flash) ==========
# RAM is the binding constraint on STM32WL (flash sits at ~63%), so spend the
# abundant flash to reclaim SRAM.
#
# AES lookup tables in ROM: the tf-psa-crypto builtin AES otherwise computes
# ~8KB of forward/reverse tables (RT0..RT3, RSb, ...) into .bss at runtime.
# ROM_TABLES makes them const (flash) instead. Single biggest SRAM win here.
CONFIG_MBEDTLS_AES_ROM_TABLES=y
# PSA key slots: default 16 each cost ~40B+ of static slot state. The mesh only
# juggles a couple of ephemeral AES keys at once; 8 is ample.
CONFIG_MBEDTLS_PSA_KEY_SLOT_COUNT=8
# Trim the common 8KB stacks to fit 64KB SRAM. Crypto (Ed25519/PSA) and the
# RNG jitter sampler run on the main thread, so keep main comfortable.
CONFIG_MAIN_STACK_SIZE=6144
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096
+33
View File
@@ -134,6 +134,39 @@ pyocd pack install EFR32MG24B220F1536IM48
Flash with: `west flash --runner pyocd`
### STM32WL (Seeed LoRa-E5)
| Board | Build string | Flash |
|----------------|---------------------------------------|--------------|
| LoRa-E5 mini | `west build -b lora_e5_mini zephcore` | `west flash` |
The STM32WLE5JC integrates an SX1262-class sub-GHz radio (driven by the native
SX126x driver via its STM32WL HAL over the internal `subghzspi` bus). RF
front-end, RF switch (PA4/PA5), TCXO and the +22 dBm RFO_HP PA are described in
upstream `zephyr/dts/arm/seeed_studio/lora-e5.dtsi`.
STM32WL caveats — different from every other ZephCore platform:
- **No Bluetooth and no USB device.** `boards/common/stm32wl_common.conf` forces
`CONFIG_BT=n`. The console/CLI and the companion protocol both run over
**USART1**, bridged to USB-C by the onboard USB-UART chip.
- **Repeater** uses the USART CLI (add `repeater.conf`). The **companion** speaks
MeshCore serial framing over the same UART via `SerialCompanionTransport.c`
(a drop-in `zephcore_ble_*` provider, auto-selected because `CONFIG_BT=n`) —
no BLE pairing, the official serial client connects directly.
- **RAM-bound, not flash-bound:** 64KB SRAM. The companion's contact/queue
arrays are capped hard in `board.conf` (`MAX_CONTACTS=24`, `OFFLINE_QUEUE_SIZE=8`).
AES tables live in ROM (`MBEDTLS_AES_ROM_TABLES`) to reclaim ~8KB SRAM.
- **TRNG only (no HW CSPRNG):** the STM32 TRNG is enabled as the entropy source
and `CSPRNG_ENABLED` auto-resolves on top; `ZephyrRNG` further conditions
identity seeds with jitter + AES-CTR.
- **No MCUboot / UF2:** single app partition at flash origin + a LittleFS volume
(see `board.overlay`). Flash over SWD/ST-Link with `west flash` (OpenOCD).
Build the repeater:
```
west build -b lora_e5_mini zephcore -- -DEXTRA_CONF_FILE="boards/common/repeater.conf"
```
### Building for Repeater Role
Append `-- -DEXTRA_CONF_FILE="boards/common/repeater.conf"` to any build command:
@@ -0,0 +1,33 @@
# Seeed Studio LoRa-E5 mini (STM32WLE5JC, integrated SX126x sub-GHz radio)
# Board-specific configuration.
#
# Hardware:
# - STM32WLE5JC: Cortex-M4 @ 48MHz (MSI), 256KB flash, 64KB SRAM
# - Integrated SX1262-class sub-GHz radio on the internal subghzspi bus
# - RF front-end: RFO_HP, +22 dBm; RF switch on PA4 (TX) / PA5 (RX)
# (radio node configured in zephyr/dts/arm/seeed_studio/lora-e5.dtsi)
# - 32MHz TCXO (DIO3, 1.7V), 32.768kHz LSE
# - USART1 (PB6/PB7) -> onboard USB-UART bridge: console + CLI
# - No Bluetooth, no USB device (see boards/common/stm32wl_common.conf)
#
# Roles: repeater (USART CLI). Companion is serial-over-USART (no BLE).
# Flash via SWD/ST-Link: west flash (OpenOCD runner, no UF2/DFU).
# Board identification
CONFIG_ZEPHCORE_BOARD_NAME="LoRa-E5 mini"
# Device Information Service model name (inert with BT off, kept for consistency)
CONFIG_BT_DIS_MODEL_NUMBER_STR="LoRa-E5 mini"
# TX power: +22 dBm RFO_HP (clamped to rfo-hp-max-power=22 in the DT radio node).
CONFIG_ZEPHCORE_DEFAULT_TX_POWER_DBM=22
# ========== Companion RAM capacity (64KB SRAM) ==========
# The companion build allocates static contacts[] + offline_queue[] arrays.
# The defaults (350 contacts ~66KB, 256-entry queue ~44KB) dwarf 64KB of SRAM,
# so cap them hard. These only allocate in the companion role; the repeater
# build ignores them. A serial companion on this constrained board is for
# modest contact lists.
CONFIG_ZEPHCORE_MAX_CONTACTS=24
CONFIG_ZEPHCORE_OFFLINE_QUEUE_SIZE=8
CONFIG_ZEPHCORE_MAX_CHANNELS=8
@@ -0,0 +1,54 @@
/*
* SPDX-License-Identifier: MIT
* Seeed Studio LoRa-E5 mini (STM32WLE5JC) -- ZephCore overlay
*
* The integrated sub-GHz radio (subghzspi "lora" node), RF switch (PA4/PA5),
* TCXO and RFO_HP front-end are already fully described in
* zephyr/dts/arm/seeed_studio/lora-e5.dtsi, and the lora0 alias + USART1
* console come from the upstream lora_e5_mini board DTS. This overlay only:
* 1. enables the TRNG entropy source, and
* 2. repartitions the 256KB internal flash for a bootloader-less ZephCore
* image plus a LittleFS data volume.
*/
/*
* STM32WL TRNG -- entropy source for the CSPRNG used by Ed25519 key
* generation and LoRa contention jitter. STM32WL has only a raw TRNG;
* CONFIG_CSPRNG_ENABLED (stm32wl_common.conf) layers the CS generator on top.
* Matches the bare enable used by upstream nucleo_wl55jc.
*/
&rng {
status = "okay";
};
/*
* Flash repartition -- no MCUboot (flashed over SWD/ST-Link; STM32WL has no
* USB for native DFU/UF2). Total 256KB, 2KB erase pages.
*
* 0x00000 - 0x38000 (224KB) Application (slot0, links at flash origin)
* 0x38000 - 0x40000 ( 32KB) LittleFS /lfs (identity, prefs, contacts)
*
* The upstream board reserves mcuboot(48K) + dual 96K slots + 16K storage,
* whose 96K app slot cannot hold ZephCore -- so we delete and rebuild.
*/
/delete-node/ &boot_partition;
/delete-node/ &slot0_partition;
/delete-node/ &slot1_partition;
/delete-node/ &storage_partition;
&flash0 {
partitions {
slot0_partition: partition@0 {
label = "image-0";
reg = <0x00000000 0x00038000>;
};
lfs_partition: partition@38000 {
label = "lfs";
reg = <0x00038000 0x00008000>;
};
};
};
/* LittleFS auto-mount at /lfs (defines the &lfs fstab node) */
#include "../../common/filesystem.dtsi"
+4
View File
@@ -8,6 +8,10 @@
#include <stdint.h>
#include <math.h>
/* Neutralize STM32 CMSIS peripheral-instance macros (RNG, AES, ...) that
* collide with portable mesh identifiers. No-op off STM32. */
#include <mesh/stm32_cmsis_fixup.h>
#define MAX_HASH_SIZE 8 /* SHA256 truncated to 8 bytes for dedup */
#define PUB_KEY_SIZE 32 /* Ed25519 public key */
#define PRV_KEY_SIZE 64 /* Ed25519 expanded private key */
+4
View File
@@ -8,6 +8,10 @@
#include <stdint.h>
#include <stddef.h>
/* On STM32, CMSIS defines an object-like macro `RNG` that collides with the
* mesh::RNG class below. The fixup header neutralizes it order-independently. */
#include <mesh/stm32_cmsis_fixup.h>
namespace mesh {
class RNG {
+47
View File
@@ -0,0 +1,47 @@
/*
* SPDX-License-Identifier: MIT
* STM32 CMSIS peripheral-macro fixup for the portable mesh:: core.
*
* STM32 CMSIS device headers (e.g. stm32wle5xx.h, pulled in transitively via
* <zephyr/kernel.h> -> soc.h) define object-like macros for every on-chip
* peripheral instance: `RNG`, `AES`, `ADC`, `RTC`, `CRC`, ... Several collide
* with generic identifiers in the portable mesh core -- most notably the
* mesh::RNG class.
*
* The collision is order-sensitive: zephcore .cpp files include mesh headers
* (which declare e.g. class RNG) and only afterwards include a Zephyr header
* (e.g. <zephyr/logging/log.h>) that pulls CMSIS in. So a naive `#undef RNG`
* placed next to the class declaration runs before the macro even exists.
*
* Fix: force the CMSIS device header to be processed NOW (via <soc.h>) so its
* include guard trips, then undef the colliding instance macros. The later
* pull through <zephyr/kernel.h> then re-enters a guarded, already-processed
* header and is a no-op, so the macros stay undef'd for the rest of the
* translation unit -- regardless of whether this header was reached before or
* after the first Zephyr include.
*
* Only zephcore C++ TUs include this; Zephyr's own STM32 drivers (which use the
* CMSIS peripheral pointers legitimately) are separate translation units and
* are unaffected. No-op on every non-STM32 platform.
*/
#pragma once
#if defined(CONFIG_SOC_FAMILY_STM32)
#include <soc.h>
/* Peripheral-instance macros whose names can collide with portable identifiers.
* #undef of an undefined macro is a harmless no-op, so no per-symbol guards. */
#undef RNG
#undef AES
#undef ADC
#undef DAC
#undef RTC
#undef CRC
#undef PWR
#undef PKA
#undef TAMP
#undef COMP
#endif /* CONFIG_SOC_FAMILY_STM32 */
@@ -846,10 +846,11 @@ index 30243ba5dc7..69eefee6e0f 100644
data->tx_async_signal = async;
k_msgq_purge(&data->tx_msgq);
@@ -777,6 +1333,29 @@ static int sx126x_lora_send_async(const struct device *dev,
@@ -777,6 +1333,31 @@ static int sx126x_lora_send_async(const struct device *dev,
/* Enable antenna and set TX path */
sx126x_set_rf_path(dev, true, true);
+#ifdef CONFIG_LORA_SX126X_NATIVE_STANDALONE /* STM32WL HAL clamps in its own configure_tx_params; its config has no is_sx1261 */
+ /* Sec 15.2 TX Clamp workaround (datasheet errata) -- SX1262 only.
+ * Set bits [4:1] of register 0x08D8 before SetTx to prevent
+ * PA overshoot that can damage the device. */
@@ -872,6 +873,7 @@ index 30243ba5dc7..69eefee6e0f 100644
+ }
+ }
+ }
+#endif /* CONFIG_LORA_SX126X_NATIVE_STANDALONE */
+
/* Start transmission with 10 second timeout */
ret = sx126x_set_tx(dev, 10000);