commit d074cd2e437a0c12299de8f16d948227041758ea Author: maze Date: Wed Mar 25 18:14:45 2026 +0100 Initial import diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..edd0c2a --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,59 @@ +# Copilot instructions for loramodem + +Repository at a glance +- Project: LoRa KISS modem (Zephyr RTOS) +- Purpose: Bridge a KISS serial interface to a LoRa radio (two threads: serial → LoRa, LoRa → serial). +- Key components: app/ (Zephyr application), drivers/lora/lr11xx (Semtech LR11xx driver), boards/ (board overlays). + +Quick commands +- Build for a board (recommended): + west build -b app + Example: west build -b heltec_wifi_lora32_v3 app +- Flash the built image: + west flash +- Open Zephyr menuconfig: + west build -b app -t menuconfig +- CMake alternative (repo root): + cmake -S app -B build && cmake --build build + +Testing & lint +- Testing strategy (from CLAUDE.md): unit tests run on host (arm64/x86) with a mocked HAL; integration tests run on target hardware. No repo-level test runner or lint targets are present by default — look for a tests/ or scripts/ directory if added. + +High-level architecture (big picture) +- app/ contains the Zephyr application (app/src/*.c). main.c starts two cooperative threads: + - kiss_rx_thread: reads bytes from the KISS UART, decodes frames (kiss.c), handles SetHardware commands, and calls lora_modem_send(). + - lora_rx_thread: polls lora_modem_recv() with a finite timeout and writes received payloads as KISS frames to the UART. +- KISS protocol implementation: app/src/kiss.c, kiss.h — encoder/decoder and SetHardware sub-command definitions. +- Radio abstraction: app/src/lora_modem.c, lora_modem.h — stores runtime params (g_params) protected by a mutex and snapshots params at send/recv time so changes take effect on the next call. +- HW driver: drivers/lora/lr11xx implements Zephyr's lora_driver_api. Hardware-specific SPI/GPIO HAL pieces live in lr11xx_hal.c. +- Build integration: app/CMakeLists.txt registers drivers/lora/lr11xx as an extra Zephyr module (ZEPHYR_EXTRA_MODULES). + +Key repository conventions and patterns +- Chosen nodes and aliases: + - The KISS UART is resolved from a chosen node: loramodem_kiss_uart (board overlay must define it). + - LoRa device is expected as DT alias lora0 (DEVICE_DT_GET(DT_ALIAS(lora0))). +- Kconfig / defaults: + - app/prj.conf contains project defaults (frequency, SF, BW, TX power, stack sizes, heap pool setting used by the app). + - Runtime radio params are read from Kconfig at init (lora_modem_init) and stored in g_params. +- Radio parameter updates: + - lora_modem_set_params() validates values and updates g_params under mutex; changes take effect when lora_modem_send() or lora_modem_recv() is next called (thread-safe snapshotting). +- KISS SetHardware mapping: + - See kiss.h for sub-commands (SET_FREQ, GET_PARAMS, etc.) and main.c for how responses are encoded and sent. +- Board overlays and confs: + - Overlays live in boards/ or board-specific .overlay files. App-level overrides can live in app.overlay or per-board .conf files. +- Memory & testing notes (from CLAUDE.md): + - Target memory budget and testing strategy are documented in CLAUDE.md (unit tests on host, integration on target). + +Files to read first when exploring code +- app/src/main.c, app/src/kiss.c/h, app/src/lora_modem.c/h, drivers/lora/lr11xx/{*.c,*.h}, app/prj.conf, app/CMakeLists.txt, CLAUDE.md + +AI & editor configs found +- CLAUDE.md contains project constraints and test strategy; consult it for memory and testing guidance. +- .vscode/settings.json points the CMake source directory to app/ in the original developer setup. +- .claude/settings.local.json contains allowed webfetch domains for the Claude assistant. + +If you (the user) want, Copilot sessions should: +- Prefer reading the files listed above first. +- Respect chosen DT nodes (loramodem_kiss_uart, alias lora0) and Kconfig defaults in app/prj.conf. +- When suggesting code changes, keep stack/heap and RAM budgets in CLAUDE.md in mind. + diff --git a/Kconfig b/Kconfig new file mode 100644 index 0000000..cbc77b5 --- /dev/null +++ b/Kconfig @@ -0,0 +1,2 @@ +# Root Kconfig — required by zephyr/module.yml (cmake: .) +# The application Kconfig is sourced from app/Kconfig via CMake. diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..6bf0fca --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.20.0) + +# Set BOARD_ROOT and DTS_ROOT to the repo root so that boards/// +# is searched for out-of-tree board definitions. +# Must be set BEFORE find_package(Zephyr). +list(APPEND BOARD_ROOT ${CMAKE_CURRENT_LIST_DIR}/..) +list(APPEND DTS_ROOT ${CMAKE_CURRENT_LIST_DIR}/..) + +# Register the LR11xx out-of-tree driver as an extra Zephyr module. +# It is only compiled when a semtech,lr11xx DTS node is enabled. +list(APPEND ZEPHYR_EXTRA_MODULES + ${CMAKE_CURRENT_LIST_DIR}/../drivers/lora/lr11xx +) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) + +project(loramodem LANGUAGES C) + +target_sources(app PRIVATE + src/main.c + src/kiss.c + src/lora_modem.c +) + +target_include_directories(app PRIVATE src) diff --git a/app/Kconfig b/app/Kconfig new file mode 100644 index 0000000..85e0a5a --- /dev/null +++ b/app/Kconfig @@ -0,0 +1,71 @@ +mainmenu "LoRa KISS Modem Configuration" + +source "Kconfig.zephyr" + +menu "KISS Interface" + +config LORAMODEM_KISS_IFACE_USB + bool "Use USB CDC-ACM as KISS serial interface" + depends on USB_DEVICE_STACK || USB_DEVICE_STACK_NEXT + help + Use USB CDC-ACM virtual serial port for the KISS interface. + Requires the host to enumerate the device before framing begins. + The board overlay must define the loramodem,kiss-uart chosen node + pointing to a cdc_acm_uart instance. + +config LORAMODEM_KISS_IFACE_UART + bool "Use hardware UART as KISS serial interface" + default y if !LORAMODEM_KISS_IFACE_USB + help + Use a hardware UART for the KISS interface. + The board overlay must define the loramodem,kiss-uart chosen node + pointing to the desired uart instance. + +endmenu + +menu "LoRa Radio Parameters" + +config LORAMODEM_LORA_FREQ_HZ + int "Center frequency (Hz)" + default 915000000 + help + LoRa radio center frequency in Hz. + Common values: 915000000 (US/AU), 868000000 (EU), 433000000 (Asia). + +config LORAMODEM_LORA_SF + int "Spreading factor" + default 9 + range 7 12 + help + LoRa spreading factor. Higher values increase range and time-on-air. + +config LORAMODEM_LORA_BW + int "Bandwidth (kHz)" + default 125 + help + LoRa bandwidth in kHz. Valid values: 125, 250, 500. + +config LORAMODEM_LORA_CR + int "Coding rate denominator" + default 5 + range 5 8 + help + LoRa coding rate as the denominator N of 4/N. + 5 = 4/5 (lowest overhead), 8 = 4/8 (highest redundancy). + +config LORAMODEM_LORA_TX_POWER_DBM + int "TX power (dBm)" + default 14 + range 2 22 + +config LORAMODEM_LORA_PREAMBLE_LEN + int "Preamble length (symbols)" + default 8 + range 6 65535 + +endmenu + +config LORAMODEM_MAX_PACKET_SIZE + int "Maximum LoRa packet payload size (bytes)" + default 255 + range 1 255 diff --git a/app/prj.conf b/app/prj.conf new file mode 100644 index 0000000..993c27a --- /dev/null +++ b/app/prj.conf @@ -0,0 +1,28 @@ +# LoRa KISS Modem — base project configuration +# Board-specific .conf overlays extend or override these settings. + +# Logging +CONFIG_LOG=y +CONFIG_PRINTK=y + +# LoRa subsystem +CONFIG_LORA=y + +# UART (interrupt-driven for non-blocking byte-by-byte processing) +CONFIG_SERIAL=y +CONFIG_UART_INTERRUPT_DRIVEN=y + +# Heap for KISS frame buffers and dynamic allocations +CONFIG_HEAP_MEM_POOL_SIZE=4096 + +# Zephyr kernel threads +CONFIG_MAIN_STACK_SIZE=2048 + +# Default radio parameters (US 915 MHz, SF9, BW125, CR 4/5) +CONFIG_LORAMODEM_LORA_FREQ_HZ=915000000 +CONFIG_LORAMODEM_LORA_SF=9 +CONFIG_LORAMODEM_LORA_BW=125 +CONFIG_LORAMODEM_LORA_CR=5 +CONFIG_LORAMODEM_LORA_TX_POWER_DBM=14 +CONFIG_LORAMODEM_LORA_PREAMBLE_LEN=8 +CONFIG_LORAMODEM_MAX_PACKET_SIZE=255 diff --git a/app/src/kiss.c b/app/src/kiss.c new file mode 100644 index 0000000..32f6eda --- /dev/null +++ b/app/src/kiss.c @@ -0,0 +1,109 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * KISS protocol encoder and decoder implementation. + */ + +#include "kiss.h" +#include +#include + +int kiss_encode_cmd(uint8_t cmd, const uint8_t *data, size_t data_len, + uint8_t *out_buf, size_t out_size) +{ + size_t out_idx = 0; + + /* Worst-case output: FEND + CMD + (2 * data_len) + FEND */ + if (out_size < (2 + 2 * data_len + 1)) { + return -ENOMEM; + } + + out_buf[out_idx++] = KISS_FEND; + out_buf[out_idx++] = cmd; + + for (size_t i = 0; i < data_len; i++) { + if (data[i] == KISS_FEND) { + out_buf[out_idx++] = KISS_FESC; + out_buf[out_idx++] = KISS_TFEND; + } else if (data[i] == KISS_FESC) { + out_buf[out_idx++] = KISS_FESC; + out_buf[out_idx++] = KISS_TFESC; + } else { + out_buf[out_idx++] = data[i]; + } + } + + out_buf[out_idx++] = KISS_FEND; + return (int)out_idx; +} + +int kiss_encode(const uint8_t *data, size_t data_len, + uint8_t *out_buf, size_t out_size) +{ + return kiss_encode_cmd(KISS_CMD_DATA, data, data_len, out_buf, out_size); +} + +void kiss_decoder_init(struct kiss_decoder_ctx *ctx, + uint8_t *buf, size_t size) +{ + ctx->frame_buf = buf; + ctx->frame_buf_size = size; + ctx->frame_len = 0; + ctx->in_frame = false; + ctx->in_escape = false; + ctx->cmd_received = false; + ctx->cmd = 0; +} + +bool kiss_decoder_feed(struct kiss_decoder_ctx *ctx, uint8_t byte) +{ + if (byte == KISS_FEND) { + if (ctx->in_frame && ctx->frame_len > 0) { + /* Closing delimiter — frame complete */ + ctx->in_frame = false; + ctx->in_escape = false; + ctx->cmd_received = false; + return true; + } + /* Opening delimiter (or spurious FEND) — start fresh */ + ctx->in_frame = true; + ctx->in_escape = false; + ctx->cmd_received = false; + ctx->frame_len = 0; + return false; + } + + if (!ctx->in_frame) { + return false; + } + + /* Handle escape sequences */ + if (byte == KISS_FESC) { + ctx->in_escape = true; + return false; + } + + if (ctx->in_escape) { + ctx->in_escape = false; + if (byte == KISS_TFEND) { + byte = KISS_FEND; + } else if (byte == KISS_TFESC) { + byte = KISS_FESC; + } + /* Unknown escape sequences: discard the escape, keep the byte */ + } + + /* First unescaped byte after opening FEND is the CMD byte */ + if (!ctx->cmd_received) { + ctx->cmd = byte; + ctx->cmd_received = true; + return false; + } + + /* Append to payload buffer (silently drop if full) */ + if (ctx->frame_len < ctx->frame_buf_size) { + ctx->frame_buf[ctx->frame_len++] = byte; + } + + return false; +} diff --git a/app/src/kiss.h b/app/src/kiss.h new file mode 100644 index 0000000..1a11161 --- /dev/null +++ b/app/src/kiss.h @@ -0,0 +1,125 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * KISS (Keep It Simple Stupid) protocol encoder/decoder. + * + * Frame format: + * FEND | CMD | | FEND + * + * Escape sequences (inside frame data only): + * 0xC0 → 0xDB 0xDC (FESC TFEND) + * 0xDB → 0xDB 0xDD (FESC TFESC) + */ +#ifndef KISS_H +#define KISS_H + +#include +#include +#include + +/* Special bytes */ +#define KISS_FEND 0xC0u /* Frame delimiter */ +#define KISS_FESC 0xDBu /* Escape byte */ +#define KISS_TFEND 0xDCu /* Escaped FEND */ +#define KISS_TFESC 0xDDu /* Escaped FESC */ + +/* Command codes (upper nibble = port 0) */ +#define KISS_CMD_DATA 0x00u +#define KISS_CMD_TXDELAY 0x01u +#define KISS_CMD_PERSISTENCE 0x02u +#define KISS_CMD_SLOTTIME 0x03u +#define KISS_CMD_TXTAIL 0x04u +#define KISS_CMD_FULLDUPLEX 0x05u +#define KISS_CMD_SETHARDWARE 0x06u +#define KISS_CMD_RETURN 0xFFu + +/* + * SetHardware (0x06) sub-commands — device-specific radio configuration. + * + * Frame payload format: [sub-cmd 1 byte] [params...] + * + * SET commands change a parameter and apply it immediately. + * GET commands cause the device to respond with a SetHardware frame + * containing the same sub-command byte followed by the current value(s). + * + * Frequency: uint32_t big-endian, Hz (e.g. 0x36892580 = 915000000) + * BW: uint16_t big-endian, kHz (125, 250, or 500) + * SF: uint8_t 7..12 + * CR: uint8_t coding-rate denominator 5..8 (meaning 4/5 .. 4/8) + * TX power: int8_t dBm + */ +#define KISS_HW_SET_FREQ 0x01u /* payload: 4 bytes freq_hz (BE uint32) */ +#define KISS_HW_GET_FREQ 0x02u /* no params; response: sub-cmd + 4 bytes */ +#define KISS_HW_SET_SF 0x03u /* payload: 1 byte sf (7-12) */ +#define KISS_HW_SET_BW 0x04u /* payload: 2 bytes bw_khz (BE uint16: 125/250/500) */ +#define KISS_HW_SET_CR 0x05u /* payload: 1 byte cr denom (5-8) */ +#define KISS_HW_SET_TXPWR 0x06u /* payload: 1 byte tx_power_dbm (int8) */ +#define KISS_HW_GET_PARAMS 0x07u /* no params; response: sub-cmd + all params (10 bytes) */ +/* + * GET_PARAMS response payload (10 bytes after sub-cmd byte): + * [0..3] freq_hz BE uint32 + * [4] sf uint8 + * [5..6] bw_khz BE uint16 + * [7] cr uint8 (denominator) + * [8] tx_power int8 (dBm) + * [9] preamble uint8 + */ + +/** + * @brief Encode data into a KISS frame with an explicit command byte. + * + * Writes FEND + cmd + escaped data + FEND into @p out_buf. + * + * @param cmd KISS command byte (e.g. KISS_CMD_DATA, KISS_CMD_SETHARDWARE). + * @param data Payload bytes (may be NULL when data_len == 0). + * @param data_len Length of payload. + * @param out_buf Output buffer for the encoded frame. + * @param out_size Size of the output buffer. + * @return Number of bytes written, or -ENOMEM if @p out_buf is too small. + */ +int kiss_encode_cmd(uint8_t cmd, const uint8_t *data, size_t data_len, + uint8_t *out_buf, size_t out_size); + +/** + * @brief Encode raw data into a KISS data frame (CMD = 0x00). + * + * Convenience wrapper around kiss_encode_cmd() using KISS_CMD_DATA. + */ +int kiss_encode(const uint8_t *data, size_t data_len, + uint8_t *out_buf, size_t out_size); + +/** + * @brief KISS decoder context. Maintains state between calls to kiss_decoder_feed(). + */ +struct kiss_decoder_ctx { + uint8_t *frame_buf; /**< Caller-supplied buffer for decoded payload. */ + size_t frame_buf_size; /**< Size of frame_buf. */ + size_t frame_len; /**< Number of payload bytes decoded so far. */ + bool in_frame; /**< True after the opening FEND has been seen. */ + bool in_escape; /**< True after FESC has been seen. */ + bool cmd_received; /**< True once the CMD byte has been consumed. */ + uint8_t cmd; /**< KISS command byte of the current frame. */ +}; + +/** + * @brief Initialize a KISS decoder context. + * + * @param ctx Context to initialize. + * @param buf Buffer for decoded frame payload. + * @param size Size of @p buf. + */ +void kiss_decoder_init(struct kiss_decoder_ctx *ctx, + uint8_t *buf, size_t size); + +/** + * @brief Feed one byte into the KISS decoder state machine. + * + * @param ctx Decoder context. + * @param byte Next byte from the serial stream. + * @return true when a complete frame has been received. + * ctx->frame_buf[0..ctx->frame_len-1] holds the payload. + * ctx->cmd holds the KISS command byte. + */ +bool kiss_decoder_feed(struct kiss_decoder_ctx *ctx, uint8_t byte); + +#endif /* KISS_H */ diff --git a/app/src/lora_modem.c b/app/src/lora_modem.c new file mode 100644 index 0000000..3c2746b --- /dev/null +++ b/app/src/lora_modem.c @@ -0,0 +1,218 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * LoRa modem abstraction layer. + * Maps runtime lora_radio_params to Zephyr's lora_modem_config enums + * and provides a simple send/recv API for the KISS bridge. + * + * Runtime parameters are stored in g_params, protected by g_params_mutex. + * send() and recv() snapshot the params under the mutex before configuring + * the radio, so a concurrent frequency change takes effect on the next call. + */ + +#include "lora_modem.h" + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(lora_modem, LOG_LEVEL_INF); + +#define LORA_DEV DEVICE_DT_GET(DT_ALIAS(lora0)) + +/* ── Runtime parameter state ─────────────────────────────────────────────── */ + +static struct lora_radio_params g_params; +static K_MUTEX_DEFINE(g_params_mutex); + +/* ── Enum conversion helpers ─────────────────────────────────────────────── */ + +static enum lora_signal_bandwidth bw_to_enum(uint16_t bw_khz) +{ + switch (bw_khz) { + case 500: return BW_500_KHZ; + case 250: return BW_250_KHZ; + case 125: /* fall through */ + default: return BW_125_KHZ; + } +} + +static enum lora_datarate sf_to_enum(uint8_t sf) +{ + switch (sf) { + case 6: return SF_6; + case 7: return SF_7; + case 8: return SF_8; + case 9: return SF_9; + case 10: return SF_10; + case 11: return SF_11; + case 12: /* fall through */ + default: return SF_12; + } +} + +static enum lora_coding_rate cr_to_enum(uint8_t cr_denom) +{ + switch (cr_denom) { + case 5: return CR_4_5; + case 6: return CR_4_6; + case 7: return CR_4_7; + case 8: /* fall through */ + default: return CR_4_8; + } +} + +/* Build a lora_modem_config from a params snapshot */ +static void params_to_cfg(const struct lora_radio_params *p, + struct lora_modem_config *cfg, bool tx) +{ + cfg->frequency = p->freq_hz; + cfg->bandwidth = bw_to_enum(p->bw_khz); + cfg->datarate = sf_to_enum(p->sf); + cfg->coding_rate = cr_to_enum(p->cr); + cfg->preamble_len = p->preamble_len; + cfg->tx_power = p->tx_power; + cfg->tx = tx; +} + +/* ── Public API ──────────────────────────────────────────────────────────── */ + +int lora_modem_init(void) +{ + const struct device *lora_dev = LORA_DEV; + + if (!device_is_ready(lora_dev)) { + LOG_ERR("LoRa device not ready"); + return -ENODEV; + } + + /* Load compile-time defaults into runtime params */ + g_params.freq_hz = CONFIG_LORAMODEM_LORA_FREQ_HZ; + g_params.sf = CONFIG_LORAMODEM_LORA_SF; + g_params.bw_khz = CONFIG_LORAMODEM_LORA_BW; + g_params.cr = CONFIG_LORAMODEM_LORA_CR; + g_params.tx_power = CONFIG_LORAMODEM_LORA_TX_POWER_DBM; + g_params.preamble_len = CONFIG_LORAMODEM_LORA_PREAMBLE_LEN; + + LOG_INF("LoRa init: %u Hz SF%u BW%ukHz CR4/%u %d dBm", + g_params.freq_hz, g_params.sf, g_params.bw_khz, + g_params.cr, g_params.tx_power); + + struct lora_modem_config cfg; + + params_to_cfg(&g_params, &cfg, true); + return lora_config(lora_dev, &cfg); +} + +void lora_modem_get_params(struct lora_radio_params *params) +{ + k_mutex_lock(&g_params_mutex, K_FOREVER); + *params = g_params; + k_mutex_unlock(&g_params_mutex); +} + +int lora_modem_set_params(const struct lora_radio_params *params) +{ + if (params->sf < 6 || params->sf > 12) { + return -EINVAL; + } + if (params->bw_khz != 125 && params->bw_khz != 250 && + params->bw_khz != 500) { + return -EINVAL; + } + if (params->cr < 5 || params->cr > 8) { + return -EINVAL; + } + if (params->freq_hz == 0) { + return -EINVAL; + } + + k_mutex_lock(&g_params_mutex, K_FOREVER); + g_params = *params; + k_mutex_unlock(&g_params_mutex); + + LOG_INF("Params updated: %u Hz SF%u BW%ukHz CR4/%u %d dBm", + params->freq_hz, params->sf, params->bw_khz, + params->cr, params->tx_power); + return 0; +} + +int lora_modem_set_freq(uint32_t freq_hz) +{ + if (freq_hz == 0) { + return -EINVAL; + } + + k_mutex_lock(&g_params_mutex, K_FOREVER); + g_params.freq_hz = freq_hz; + k_mutex_unlock(&g_params_mutex); + + LOG_INF("Frequency set to %u Hz", freq_hz); + return 0; +} + +int lora_modem_send(const uint8_t *data, size_t len) +{ + const struct device *lora_dev = LORA_DEV; + struct lora_radio_params snap; + struct lora_modem_config tx_cfg; + int ret; + + k_mutex_lock(&g_params_mutex, K_FOREVER); + snap = g_params; + k_mutex_unlock(&g_params_mutex); + + params_to_cfg(&snap, &tx_cfg, true); + + ret = lora_config(lora_dev, &tx_cfg); + if (ret < 0) { + LOG_ERR("lora_config(TX) failed: %d", ret); + return ret; + } + + ret = lora_send(lora_dev, (uint8_t *)data, (uint32_t)len); + if (ret < 0) { + LOG_ERR("lora_send failed: %d", ret); + } + + return ret; +} + +int lora_modem_recv(uint8_t *buf, size_t buf_size, k_timeout_t timeout, + int16_t *rssi_out, int8_t *snr_out) +{ + const struct device *lora_dev = LORA_DEV; + struct lora_radio_params snap; + struct lora_modem_config rx_cfg; + int ret; + + k_mutex_lock(&g_params_mutex, K_FOREVER); + snap = g_params; + k_mutex_unlock(&g_params_mutex); + + params_to_cfg(&snap, &rx_cfg, false); + + ret = lora_config(lora_dev, &rx_cfg); + if (ret < 0) { + LOG_ERR("lora_config(RX) failed: %d", ret); + return ret; + } + + int16_t rssi = 0; + int8_t snr = 0; + + ret = lora_recv(lora_dev, buf, (uint8_t)buf_size, timeout, &rssi, &snr); + + if (ret >= 0) { + LOG_DBG("RX %d bytes RSSI=%d SNR=%d", ret, rssi, snr); + if (rssi_out) { + *rssi_out = rssi; + } + if (snr_out) { + *snr_out = snr; + } + } + + return ret; +} diff --git a/app/src/lora_modem.h b/app/src/lora_modem.h new file mode 100644 index 0000000..505a149 --- /dev/null +++ b/app/src/lora_modem.h @@ -0,0 +1,92 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * LoRa modem abstraction layer. + * Wraps Zephyr's lora_driver_api with application-level configuration. + */ +#ifndef LORA_MODEM_H +#define LORA_MODEM_H + +#include +#include +#include + +/** + * @brief Runtime radio parameters. + * + * All fields are in user-friendly units. Modified at runtime via + * lora_modem_set_params() / lora_modem_set_freq(); read via + * lora_modem_get_params(). + */ +struct lora_radio_params { + uint32_t freq_hz; /**< Center frequency in Hz */ + uint8_t sf; /**< Spreading factor (7-12) */ + uint16_t bw_khz; /**< Bandwidth in kHz (125, 250, or 500) */ + uint8_t cr; /**< Coding rate denominator (5..8, meaning 4/5..4/8) */ + int8_t tx_power; /**< TX power in dBm */ + uint8_t preamble_len; /**< Preamble length in symbols */ +}; + +/** + * @brief Initialize and configure the LoRa radio. + * + * Loads compile-time Kconfig defaults into the runtime params struct + * and applies them. Call once at startup before any send/recv. + * + * @return 0 on success, negative errno on failure. + */ +int lora_modem_init(void); + +/** + * @brief Get the current radio parameters. + * + * @param params Output: filled with current settings. + */ +void lora_modem_get_params(struct lora_radio_params *params); + +/** + * @brief Set all radio parameters at once. + * + * Parameters are stored for use on the next send/recv call. + * The radio is NOT immediately reconfigured; the change takes effect + * at the start of the next lora_modem_send() or lora_modem_recv() call. + * + * @param params New parameter values. + * @return 0 on success, -EINVAL if a parameter is out of range. + */ +int lora_modem_set_params(const struct lora_radio_params *params); + +/** + * @brief Change only the center frequency. + * + * Convenience wrapper around lora_modem_set_params() for the common + * case of a frequency-only change. + * + * @param freq_hz New center frequency in Hz. + * @return 0 on success, -EINVAL if out of range. + */ +int lora_modem_set_freq(uint32_t freq_hz); + +/** + * @brief Transmit a packet over LoRa. + * + * @param data Payload bytes. + * @param len Payload length (must be <= CONFIG_LORAMODEM_MAX_PACKET_SIZE). + * @return 0 on success, negative errno on failure. + */ +int lora_modem_send(const uint8_t *data, size_t len); + +/** + * @brief Receive a packet from LoRa (blocking). + * + * @param buf Buffer for received payload. + * @param buf_size Size of @p buf. + * @param timeout How long to wait (use K_FOREVER to wait indefinitely). + * @param rssi_out If non-NULL, receives RSSI of the packet (dBm). + * @param snr_out If non-NULL, receives SNR of the packet (dB). + * @return Number of bytes received (>= 0), or negative errno on error/timeout. + */ +int lora_modem_recv(uint8_t *buf, size_t buf_size, k_timeout_t timeout, + int16_t *rssi_out, int8_t *snr_out); + +#endif /* LORA_MODEM_H */ diff --git a/app/src/main.c b/app/src/main.c new file mode 100644 index 0000000..73b0684 --- /dev/null +++ b/app/src/main.c @@ -0,0 +1,362 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * LoRa KISS Modem — main application. + * + * Two threads bridge a KISS serial interface to a LoRa radio: + * + * kiss_rx_thread: reads bytes from the KISS UART, decodes KISS frames, + * dispatches data frames to lora_modem_send(), and + * handles SetHardware (0x06) frames for radio parameter + * changes (frequency, SF, BW, CR, TX power). + * + * lora_rx_thread: loops with a 1-second timeout on lora_modem_recv(); + * on each successful receive it encodes the payload as + * a KISS data frame and writes it to the KISS UART. + * The finite timeout ensures frequency changes applied + * by kiss_rx_thread take effect within 1 second. + * + * SetHardware sub-command protocol (see kiss.h for KISS_HW_* defines): + * + * Host → device: FEND | 0x06 | [params...] | FEND + * Device → host: FEND | 0x06 | [current-value...] | FEND + * (only sent in response to GET commands) + * + * SET_FREQ (0x01): payload = 4 bytes freq_hz big-endian uint32 + * GET_FREQ (0x02): no params; response = sub-cmd + 4-byte freq + * SET_SF (0x03): payload = 1 byte sf (7-12) + * SET_BW (0x04): payload = 2 bytes bw_khz big-endian uint16 (125/250/500) + * SET_CR (0x05): payload = 1 byte cr denominator (5-8) + * SET_TXPWR(0x06): payload = 1 byte tx_power_dbm (int8) + * GET_PARAMS(0x07): no params; response = sub-cmd + 10-byte param block + * + * GET_PARAMS response (10 bytes after sub-cmd): + * [0..3] freq_hz BE uint32 + * [4] sf uint8 + * [5..6] bw_khz BE uint16 + * [7] cr uint8 (denominator) + * [8] tx_power int8 (dBm) + * [9] preamble uint8 + */ + +#include "kiss.h" +#include "lora_modem.h" + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(main, LOG_LEVEL_INF); + +/* Resolve the KISS UART from the board overlay */ +#define KISS_UART_NODE DT_CHOSEN(loramodem_kiss_uart) +#define KISS_UART DEVICE_DT_GET(KISS_UART_NODE) + +/* Thread stack sizes */ +#define KISS_RX_STACK_SIZE 2048 +#define LORA_RX_STACK_SIZE 2048 + +/* lora_rx_fn polls with this timeout so frequency changes take effect promptly */ +#define LORA_RX_TIMEOUT_MS 1000 + +/* Packet buffers */ +static uint8_t lora_rx_buf[CONFIG_LORAMODEM_MAX_PACKET_SIZE]; +/* kiss_tx_buf is shared by both threads — protected by uart_tx_mutex */ +static uint8_t kiss_tx_buf[CONFIG_LORAMODEM_MAX_PACKET_SIZE * 2 + 4]; + +/* Mutex protecting writes to the KISS UART (used by both threads) */ +static K_MUTEX_DEFINE(uart_tx_mutex); + +/* ── UART helpers ─────────────────────────────────────────────────────────── */ + +static void uart_write_bytes(const struct device *uart, + const uint8_t *buf, size_t len) +{ + for (size_t i = 0; i < len; i++) { + uart_poll_out(uart, buf[i]); + } +} + +/* Encode and send a KISS SetHardware response frame to the host */ +static void send_hw_response(const struct device *uart, + const uint8_t *payload, size_t payload_len) +{ + k_mutex_lock(&uart_tx_mutex, K_FOREVER); + int enc = kiss_encode_cmd(KISS_CMD_SETHARDWARE, + payload, payload_len, + kiss_tx_buf, sizeof(kiss_tx_buf)); + if (enc > 0) { + uart_write_bytes(uart, kiss_tx_buf, (size_t)enc); + } + k_mutex_unlock(&uart_tx_mutex); +} + +/* ── SetHardware dispatcher ──────────────────────────────────────────────── */ + +static void handle_sethardware(const struct device *uart, + const uint8_t *payload, size_t len) +{ + if (len == 0) { + LOG_WRN("SetHardware: empty payload"); + return; + } + + uint8_t sub_cmd = payload[0]; + const uint8_t *args = payload + 1; + size_t args_len = len - 1; + + switch (sub_cmd) { + + case KISS_HW_SET_FREQ: { + if (args_len < 4) { + LOG_WRN("SET_FREQ: need 4 bytes, got %zu", args_len); + return; + } + uint32_t freq = ((uint32_t)args[0] << 24) | + ((uint32_t)args[1] << 16) | + ((uint32_t)args[2] << 8) | + ((uint32_t)args[3]); + if (lora_modem_set_freq(freq) < 0) { + LOG_WRN("SET_FREQ: invalid frequency %u Hz", freq); + } + break; + } + + case KISS_HW_GET_FREQ: { + struct lora_radio_params p; + + lora_modem_get_params(&p); + uint8_t resp[5]; + + resp[0] = KISS_HW_GET_FREQ; + resp[1] = (uint8_t)(p.freq_hz >> 24); + resp[2] = (uint8_t)(p.freq_hz >> 16); + resp[3] = (uint8_t)(p.freq_hz >> 8); + resp[4] = (uint8_t)(p.freq_hz); + send_hw_response(uart, resp, sizeof(resp)); + break; + } + + case KISS_HW_SET_SF: { + if (args_len < 1) { + LOG_WRN("SET_SF: need 1 byte"); + return; + } + struct lora_radio_params p; + + lora_modem_get_params(&p); + p.sf = args[0]; + if (lora_modem_set_params(&p) < 0) { + LOG_WRN("SET_SF: invalid SF %u", args[0]); + } + break; + } + + case KISS_HW_SET_BW: { + if (args_len < 2) { + LOG_WRN("SET_BW: need 2 bytes"); + return; + } + uint16_t bw = ((uint16_t)args[0] << 8) | args[1]; + struct lora_radio_params p; + + lora_modem_get_params(&p); + p.bw_khz = bw; + if (lora_modem_set_params(&p) < 0) { + LOG_WRN("SET_BW: invalid BW %u kHz", bw); + } + break; + } + + case KISS_HW_SET_CR: { + if (args_len < 1) { + LOG_WRN("SET_CR: need 1 byte"); + return; + } + struct lora_radio_params p; + + lora_modem_get_params(&p); + p.cr = args[0]; + if (lora_modem_set_params(&p) < 0) { + LOG_WRN("SET_CR: invalid CR %u", args[0]); + } + break; + } + + case KISS_HW_SET_TXPWR: { + if (args_len < 1) { + LOG_WRN("SET_TXPWR: need 1 byte"); + return; + } + struct lora_radio_params p; + + lora_modem_get_params(&p); + p.tx_power = (int8_t)args[0]; + lora_modem_set_params(&p); + break; + } + + case KISS_HW_GET_PARAMS: { + struct lora_radio_params p; + + lora_modem_get_params(&p); + + uint8_t resp[11]; + + resp[0] = KISS_HW_GET_PARAMS; + resp[1] = (uint8_t)(p.freq_hz >> 24); + resp[2] = (uint8_t)(p.freq_hz >> 16); + resp[3] = (uint8_t)(p.freq_hz >> 8); + resp[4] = (uint8_t)(p.freq_hz); + resp[5] = p.sf; + resp[6] = (uint8_t)(p.bw_khz >> 8); + resp[7] = (uint8_t)(p.bw_khz); + resp[8] = p.cr; + resp[9] = (uint8_t)p.tx_power; + resp[10] = p.preamble_len; + send_hw_response(uart, resp, sizeof(resp)); + break; + } + + default: + LOG_WRN("SetHardware: unknown sub-cmd 0x%02x", sub_cmd); + break; + } +} + +/* ── KISS RX thread: serial → LoRa ────────────────────────────────────────── */ + +K_THREAD_STACK_DEFINE(kiss_rx_stack, KISS_RX_STACK_SIZE); +static struct k_thread kiss_rx_thread; + +static void kiss_rx_fn(void *p1, void *p2, void *p3) +{ + ARG_UNUSED(p1); + ARG_UNUSED(p2); + ARG_UNUSED(p3); + + const struct device *uart = KISS_UART; + struct kiss_decoder_ctx dec; + static uint8_t dec_buf[CONFIG_LORAMODEM_MAX_PACKET_SIZE]; + + kiss_decoder_init(&dec, dec_buf, sizeof(dec_buf)); + +#if defined(CONFIG_LORAMODEM_KISS_IFACE_USB) + uint32_t dtr = 0; + + LOG_INF("Waiting for USB host..."); + while (!dtr) { + uart_line_ctrl_get(uart, UART_LINE_CTRL_DTR, &dtr); + k_sleep(K_MSEC(100)); + } + LOG_INF("USB host connected"); +#endif + + LOG_INF("KISS RX thread started"); + + uint8_t byte; + + while (true) { + if (uart_poll_in(uart, &byte) == 0) { + if (kiss_decoder_feed(&dec, byte)) { + if (dec.cmd == KISS_CMD_DATA && + dec.frame_len > 0) { + LOG_DBG("TX %zu bytes via LoRa", + dec.frame_len); + lora_modem_send(dec.frame_buf, + dec.frame_len); + } else if (dec.cmd == KISS_CMD_SETHARDWARE) { + handle_sethardware(uart, + dec.frame_buf, + dec.frame_len); + } + kiss_decoder_init(&dec, dec_buf, + sizeof(dec_buf)); + } + } else { + k_yield(); + } + } +} + +/* ── LoRa RX thread: LoRa → serial ────────────────────────────────────────── */ + +K_THREAD_STACK_DEFINE(lora_rx_stack, LORA_RX_STACK_SIZE); +static struct k_thread lora_rx_thread; + +static void lora_rx_fn(void *p1, void *p2, void *p3) +{ + ARG_UNUSED(p1); + ARG_UNUSED(p2); + ARG_UNUSED(p3); + + const struct device *uart = KISS_UART; + int16_t rssi; + int8_t snr; + + LOG_INF("LoRa RX thread started"); + + while (true) { + /* + * Use a finite timeout so any frequency change applied by + * kiss_rx_thread takes effect within LORA_RX_TIMEOUT_MS. + * lora_modem_recv() snapshots g_params at the start of each + * call, so the new frequency is picked up automatically on + * the next iteration. + */ + int len = lora_modem_recv(lora_rx_buf, sizeof(lora_rx_buf), + K_MSEC(LORA_RX_TIMEOUT_MS), + &rssi, &snr); + + if (len == -ETIMEDOUT) { + continue; + } + + if (len < 0) { + LOG_WRN("lora_modem_recv error: %d", len); + continue; + } + + LOG_DBG("RX %d bytes RSSI=%d SNR=%d", len, rssi, snr); + + k_mutex_lock(&uart_tx_mutex, K_FOREVER); + int enc_len = kiss_encode(lora_rx_buf, (size_t)len, + kiss_tx_buf, sizeof(kiss_tx_buf)); + if (enc_len > 0) { + uart_write_bytes(uart, kiss_tx_buf, (size_t)enc_len); + } else { + LOG_WRN("kiss_encode error: %d", enc_len); + } + k_mutex_unlock(&uart_tx_mutex); + } +} + +/* ── Entry point ──────────────────────────────────────────────────────────── */ + +int main(void) +{ + int ret; + + LOG_INF("LoRa KISS Modem starting"); + + ret = lora_modem_init(); + if (ret < 0) { + LOG_ERR("LoRa modem init failed: %d", ret); + return ret; + } + + k_thread_create(&kiss_rx_thread, kiss_rx_stack, + K_THREAD_STACK_SIZEOF(kiss_rx_stack), + kiss_rx_fn, NULL, NULL, NULL, + K_PRIO_COOP(5), 0, K_NO_WAIT); + k_thread_name_set(&kiss_rx_thread, "kiss_rx"); + + k_thread_create(&lora_rx_thread, lora_rx_stack, + K_THREAD_STACK_SIZEOF(lora_rx_stack), + lora_rx_fn, NULL, NULL, NULL, + K_PRIO_COOP(5), 0, K_NO_WAIT); + k_thread_name_set(&lora_rx_thread, "lora_rx"); + + return 0; +} diff --git a/boards/heltec/heltec_meshpocket/Kconfig.defconfig b/boards/heltec/heltec_meshpocket/Kconfig.defconfig new file mode 100644 index 0000000..46cb2eb --- /dev/null +++ b/boards/heltec/heltec_meshpocket/Kconfig.defconfig @@ -0,0 +1,6 @@ +if BOARD_HELTEC_MESHPOCKET + +config BOARD + default "heltec_meshpocket" + +endif # BOARD_HELTEC_MESHPOCKET diff --git a/boards/heltec/heltec_meshpocket/Kconfig.heltec_meshpocket b/boards/heltec/heltec_meshpocket/Kconfig.heltec_meshpocket new file mode 100644 index 0000000..642e49c --- /dev/null +++ b/boards/heltec/heltec_meshpocket/Kconfig.heltec_meshpocket @@ -0,0 +1,2 @@ +config BOARD_HELTEC_MESHPOCKET + select SOC_NRF52840_QIAA diff --git a/boards/heltec/heltec_meshpocket/board.cmake b/boards/heltec/heltec_meshpocket/board.cmake new file mode 100644 index 0000000..ed4c971 --- /dev/null +++ b/boards/heltec/heltec_meshpocket/board.cmake @@ -0,0 +1,2 @@ +# Heltec MeshPocket — board.cmake +include(${ZEPHYR_BASE}/boards/nordic/common/board.cmake) diff --git a/boards/heltec/heltec_meshpocket/board.yml b/boards/heltec/heltec_meshpocket/board.yml new file mode 100644 index 0000000..bdc506b --- /dev/null +++ b/boards/heltec/heltec_meshpocket/board.yml @@ -0,0 +1,7 @@ +board: + name: heltec_meshpocket + full_name: "Heltec MeshPocket" + vendor: heltec + url: https://heltec.org/project/mesh-pocket/ + socs: + - name: nrf52840 diff --git a/boards/heltec/heltec_meshpocket/heltec_meshpocket-pinctrl.dtsi b/boards/heltec/heltec_meshpocket/heltec_meshpocket-pinctrl.dtsi new file mode 100644 index 0000000..52480eb --- /dev/null +++ b/boards/heltec/heltec_meshpocket/heltec_meshpocket-pinctrl.dtsi @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Heltec MeshPocket — pin control definitions + * SoC: nRF52840-QIAA + */ + +#include +#include + +&pinctrl { + uart0_default: uart0_default { + group1 { + psels = ; + }; + group2 { + psels = ; + bias-pull-up; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* SPI1 for SX1262 */ + spi1_default: spi1_default { + group1 { + psels = , + , + ; + }; + }; + + spi1_sleep: spi1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; +}; diff --git a/boards/heltec/heltec_meshpocket/heltec_meshpocket.dts b/boards/heltec/heltec_meshpocket/heltec_meshpocket.dts new file mode 100644 index 0000000..4bd31db --- /dev/null +++ b/boards/heltec/heltec_meshpocket/heltec_meshpocket.dts @@ -0,0 +1,96 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Heltec MeshPocket — Device Tree Source + * SoC: nRF52840-QIAA + * Radio: SX1262 (crystal oscillator, no TCXO) + */ + +/dts-v1/; +#include +#include "heltec_meshpocket-pinctrl.dtsi" + +/ { + model = "Heltec MeshPocket"; + compatible = "heltec,meshpocket"; + + chosen { + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,console = &uart0; + zephyr,shell-uart = &uart0; + zephyr,code-partition = &slot0_partition; + }; + + aliases { + lora0 = &lora; + }; +}; + +&uicr { + gpio-as-nreset; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +&uart0 { + compatible = "nordic,nrf-uarte"; + status = "okay"; + current-speed = <115200>; + pinctrl-0 = <&uart0_default>; + pinctrl-1 = <&uart0_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi1 { + compatible = "nordic,nrf-spim"; + status = "okay"; + pinctrl-0 = <&spi1_default>; + pinctrl-names = "default"; + cs-gpios = <&gpio0 24 GPIO_ACTIVE_LOW>; + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <1000000>; + reset-gpios = <&gpio0 21 GPIO_ACTIVE_LOW>; + busy-gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>; + dio1-gpios = <&gpio1 4 GPIO_ACTIVE_HIGH>; + dio2-tx-enable; + }; +}; + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + boot_partition: partition@0 { + label = "mcuboot"; + reg = <0x00000000 0x0000C000>; + }; + slot0_partition: partition@c000 { + label = "image-0"; + reg = <0x0000C000 0x00067000>; + }; + slot1_partition: partition@73000 { + label = "image-1"; + reg = <0x00073000 0x00067000>; + }; + storage_partition: partition@da000 { + label = "storage"; + reg = <0x000da000 0x00006000>; + }; + }; +}; diff --git a/boards/heltec/heltec_meshpocket/heltec_meshpocket_defconfig b/boards/heltec/heltec_meshpocket/heltec_meshpocket_defconfig new file mode 100644 index 0000000..e47cb3f --- /dev/null +++ b/boards/heltec/heltec_meshpocket/heltec_meshpocket_defconfig @@ -0,0 +1,7 @@ +CONFIG_SOC_SERIES_NRF52X=y +CONFIG_BOARD_HELTEC_MESHPOCKET=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_UART_CONSOLE=y +CONFIG_SPI=y +CONFIG_GPIO=y diff --git a/boards/heltec/heltec_t114/Kconfig.defconfig b/boards/heltec/heltec_t114/Kconfig.defconfig new file mode 100644 index 0000000..d8554da --- /dev/null +++ b/boards/heltec/heltec_t114/Kconfig.defconfig @@ -0,0 +1,6 @@ +if BOARD_HELTEC_T114 + +config BOARD + default "heltec_t114" + +endif # BOARD_HELTEC_T114 diff --git a/boards/heltec/heltec_t114/Kconfig.heltec_t114 b/boards/heltec/heltec_t114/Kconfig.heltec_t114 new file mode 100644 index 0000000..6ee3103 --- /dev/null +++ b/boards/heltec/heltec_t114/Kconfig.heltec_t114 @@ -0,0 +1,2 @@ +config BOARD_HELTEC_T114 + select SOC_NRF52840_QIAA diff --git a/boards/heltec/heltec_t114/board.cmake b/boards/heltec/heltec_t114/board.cmake new file mode 100644 index 0000000..90397c6 --- /dev/null +++ b/boards/heltec/heltec_t114/board.cmake @@ -0,0 +1,7 @@ +# Heltec T114 — board.cmake +# Uses nrfjprog or pyocd for flashing via SWD. +# The board exposes a SWD header for programming. +board_runner_args(nrfjprog "--nrf-family=NRF52") +board_runner_args(pyocd "--target=nrf52840") +include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) +include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) diff --git a/boards/heltec/heltec_t114/board.yml b/boards/heltec/heltec_t114/board.yml new file mode 100644 index 0000000..66ec7b8 --- /dev/null +++ b/boards/heltec/heltec_t114/board.yml @@ -0,0 +1,7 @@ +board: + name: heltec_t114 + full_name: "Heltec Mesh Node T114" + vendor: heltec + url: https://heltec.org/project/mesh-node-t114/ + socs: + - name: nrf52840 diff --git a/boards/heltec/heltec_t114/heltec_t114-pinctrl.dtsi b/boards/heltec/heltec_t114/heltec_t114-pinctrl.dtsi new file mode 100644 index 0000000..d4d7a42 --- /dev/null +++ b/boards/heltec/heltec_t114/heltec_t114-pinctrl.dtsi @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Heltec T114 — pin control definitions + */ + +#include +#include + +&pinctrl { + uart1_default: uart1_default { + group1 { + psels = ; + }; + group2 { + psels = ; + bias-pull-up; + }; + }; + + uart1_sleep: uart1_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* SPI1 for LR1110 */ + spi1_default: spi1_default { + group1 { + psels = , + , + ; + }; + }; + + spi1_sleep: spi1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; +}; diff --git a/boards/heltec/heltec_t114/heltec_t114.dts b/boards/heltec/heltec_t114/heltec_t114.dts new file mode 100644 index 0000000..94c95da --- /dev/null +++ b/boards/heltec/heltec_t114/heltec_t114.dts @@ -0,0 +1,115 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Heltec Mesh Node T114 — Device Tree Source + * SoC: nRF52840 QIAAVariant + * Radio: SX1262 + * + * NOTE: GPIO pin numbers are placeholders. Verify against the T114 + * schematic/documentation before using with real hardware. + * The T114 uses the nRF52840 SoC in a similar form factor to the + * RAK4631, but with different GPIO assignments. + */ + +/dts-v1/; +#include +#include "heltec_t114-pinctrl.dtsi" + +/ { + model = "Heltec Mesh Node T114"; + compatible = "heltec,mesh-node-t114"; + + chosen { + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,code-partition = &slot0_partition; + }; + + aliases { + lora0 = &lora; + led0 = &led0; + }; + + leds { + compatible = "gpio-leds"; + led0: led_0 { + /* TODO: verify LED GPIO from T114 schematic */ + gpios = <&gpio0 13 GPIO_ACTIVE_LOW>; + label = "LED 0"; + }; + }; +}; + +&uicr { + gpio-as-nreset; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +&uart1 { + compatible = "nordic,nrf-uarte"; + status = "okay"; + current-speed = <115200>; + pinctrl-0 = <&uart1_default>; + pinctrl-1 = <&uart1_sleep>; + pinctrl-names = "default", "sleep"; +}; + +&spi1 { + compatible = "nordic,nrf-spim"; + status = "okay"; + pinctrl-0 = <&spi1_default>; + pinctrl-names = "default"; + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <1000000>; + reset-gpios = <&gpio0 25 GPIO_ACTIVE_LOW>; + busy-gpios = <&gpio0 17 GPIO_ACTIVE_HIGH>; + dio1-gpios = <&gpio0 20 GPIO_ACTIVE_HIGH>; + dio2-tx-enable; + dio3-tcxo-voltage = ; + tcxo-power-startup-delay-ms = <5>; + }; +}; + +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; +}; + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + boot_partition: partition@0 { + label = "mcuboot"; + reg = <0x00000000 0x0000C000>; + }; + slot0_partition: partition@c000 { + label = "image-0"; + reg = <0x0000C000 0x00067000>; + }; + slot1_partition: partition@73000 { + label = "image-1"; + reg = <0x00073000 0x00067000>; + }; + storage_partition: partition@da000 { + label = "storage"; + reg = <0x000da000 0x00006000>; + }; + }; +}; diff --git a/boards/heltec/heltec_t114/heltec_t114_defconfig b/boards/heltec/heltec_t114/heltec_t114_defconfig new file mode 100644 index 0000000..c8fb2d2 --- /dev/null +++ b/boards/heltec/heltec_t114/heltec_t114_defconfig @@ -0,0 +1,8 @@ +CONFIG_SOC_NRF52840_QIAA=y +CONFIG_BOARD_HELTEC_T114=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_GPIO=y +CONFIG_SPI=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_NRFX_POWER=y diff --git a/boards/heltec/heltec_wifi_lora32_v3/Kconfig.defconfig b/boards/heltec/heltec_wifi_lora32_v3/Kconfig.defconfig new file mode 100644 index 0000000..50574f8 --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v3/Kconfig.defconfig @@ -0,0 +1,6 @@ +if BOARD_HELTEC_WIFI_LORA32_V3 + +config BOARD + default "heltec_wifi_lora32_v3" + +endif # BOARD_HELTEC_WIFI_LORA32_V3 diff --git a/boards/heltec/heltec_wifi_lora32_v3/Kconfig.heltec_wifi_lora32_v3 b/boards/heltec/heltec_wifi_lora32_v3/Kconfig.heltec_wifi_lora32_v3 new file mode 100644 index 0000000..3d54440 --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v3/Kconfig.heltec_wifi_lora32_v3 @@ -0,0 +1,3 @@ +config BOARD_HELTEC_WIFI_LORA32_V3 + select SOC_ESP32S3_PROCPU if BOARD_HELTEC_WIFI_LORA32_V3_PROCPU + select SOC_ESP32S3_APPCPU if BOARD_HELTEC_WIFI_LORA32_V3_APPCPU diff --git a/boards/heltec/heltec_wifi_lora32_v3/board.cmake b/boards/heltec/heltec_wifi_lora32_v3/board.cmake new file mode 100644 index 0000000..9ce9e75 --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v3/board.cmake @@ -0,0 +1,3 @@ +# Heltec WiFi LoRa 32 V3 — board.cmake +# Uses esptool for flashing over CP2102 USB-UART bridge +include(${ZEPHYR_BASE}/boards/espressif/common/board.cmake) diff --git a/boards/heltec/heltec_wifi_lora32_v3/board.yml b/boards/heltec/heltec_wifi_lora32_v3/board.yml new file mode 100644 index 0000000..4b2b4a8 --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v3/board.yml @@ -0,0 +1,10 @@ +board: + name: heltec_wifi_lora32_v3 + full_name: "Heltec WiFi LoRa 32 V3" + vendor: heltec + url: https://heltec.org/project/wifi-lora-32-v3/ + socs: + - name: esp32s3 + variants: + - name: procpu + - name: appcpu diff --git a/boards/heltec/heltec_wifi_lora32_v3/heltec_wifi_lora32_v3-pinctrl.dtsi b/boards/heltec/heltec_wifi_lora32_v3/heltec_wifi_lora32_v3-pinctrl.dtsi new file mode 100644 index 0000000..d23f703 --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v3/heltec_wifi_lora32_v3-pinctrl.dtsi @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Heltec WiFi LoRa 32 V3 — pin control definitions + */ + +#include +#include +#include + +&pinctrl { + uart0_default: uart0_default { + group1 { + pinmux = ; + output-enable; + }; + group2 { + pinmux = ; + bias-pull-up; + }; + }; + + spim2_default: spim2_default { + group1 { + pinmux = , + , + ; + }; + group2 { + pinmux = ; + output-enable; + }; + }; +}; diff --git a/boards/heltec/heltec_wifi_lora32_v3/heltec_wifi_lora32_v3_procpu.dts b/boards/heltec/heltec_wifi_lora32_v3/heltec_wifi_lora32_v3_procpu.dts new file mode 100644 index 0000000..cb0643d --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v3/heltec_wifi_lora32_v3_procpu.dts @@ -0,0 +1,109 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Heltec WiFi LoRa 32 V3 — Device Tree Source (PROCPU) + * + * SoC: ESP32-S3 + * Radio: SX1262 + * Console: UART0 via CP2102 USB-UART bridge (GPIO43=TX, GPIO44=RX) + */ + +/dts-v1/; +#include +#include "heltec_wifi_lora32_v3-pinctrl.dtsi" +#include + +/ { + model = "Heltec WiFi LoRa 32 V3 PROCPU"; + compatible = "heltec,wifi-lora-32-v3"; + + chosen { + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,console = &uart0; + zephyr,shell-uart = &uart0; + zephyr,flash-controller = &flash; + }; + + aliases { + lora0 = &lora; + led0 = &led_white; + }; + + leds { + compatible = "gpio-leds"; + led_white: led_0 { + gpios = <&gpio0 35 GPIO_ACTIVE_HIGH>; + label = "White LED"; + }; + }; + + buttons { + compatible = "gpio-keys"; + boot_button: button_0 { + gpios = <&gpio0 0 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + label = "Boot Button"; + zephyr,code = ; + }; + }; +}; + +&uart0 { + status = "okay"; + current-speed = <115200>; + pinctrl-0 = <&uart0_default>; + pinctrl-names = "default"; +}; + +&spi2 { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + pinctrl-0 = <&spim2_default>; + pinctrl-names = "default"; + cs-gpios = <&gpio0 8 GPIO_ACTIVE_LOW>; + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <4000000>; + reset-gpios = <&gpio0 12 (GPIO_OPEN_DRAIN | GPIO_ACTIVE_LOW)>; + busy-gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>; + dio1-gpios = <&gpio0 14 GPIO_ACTIVE_HIGH>; + dio2-tx-enable; + tcxo-power-startup-delay-ms = <5>; + }; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +&flash0 { + status = "okay"; + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + boot_partition: partition@0 { + label = "mcuboot"; + reg = <0x00000000 0x00010000>; + }; + slot0_partition: partition@10000 { + label = "image-0"; + reg = <0x00010000 0x00300000>; + }; + slot1_partition: partition@310000 { + label = "image-1"; + reg = <0x00310000 0x00300000>; + }; + storage_partition: partition@610000 { + label = "storage"; + reg = <0x00610000 0x001f0000>; + }; + }; +}; diff --git a/boards/heltec/heltec_wifi_lora32_v3/heltec_wifi_lora32_v3_procpu_defconfig b/boards/heltec/heltec_wifi_lora32_v3/heltec_wifi_lora32_v3_procpu_defconfig new file mode 100644 index 0000000..dce4f83 --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v3/heltec_wifi_lora32_v3_procpu_defconfig @@ -0,0 +1,7 @@ +CONFIG_SOC_SERIES_ESP32S3=y +CONFIG_BOARD_HELTEC_WIFI_LORA32_V3=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_UART_CONSOLE=y +CONFIG_SPI=y +CONFIG_GPIO=y diff --git a/boards/heltec/heltec_wifi_lora32_v4/Kconfig.defconfig b/boards/heltec/heltec_wifi_lora32_v4/Kconfig.defconfig new file mode 100644 index 0000000..75b29a9 --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v4/Kconfig.defconfig @@ -0,0 +1,6 @@ +if BOARD_HELTEC_WIFI_LORA32_V4 + +config BOARD + default "heltec_wifi_lora32_v4" + +endif # BOARD_HELTEC_WIFI_LORA32_V4 diff --git a/boards/heltec/heltec_wifi_lora32_v4/Kconfig.heltec_wifi_lora32_v4 b/boards/heltec/heltec_wifi_lora32_v4/Kconfig.heltec_wifi_lora32_v4 new file mode 100644 index 0000000..314d870 --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v4/Kconfig.heltec_wifi_lora32_v4 @@ -0,0 +1,3 @@ +config BOARD_HELTEC_WIFI_LORA32_V4 + select SOC_ESP32S3_PROCPU if BOARD_HELTEC_WIFI_LORA32_V4_PROCPU + select SOC_ESP32S3_APPCPU if BOARD_HELTEC_WIFI_LORA32_V4_APPCPU diff --git a/boards/heltec/heltec_wifi_lora32_v4/board.cmake b/boards/heltec/heltec_wifi_lora32_v4/board.cmake new file mode 100644 index 0000000..9aaf5c4 --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v4/board.cmake @@ -0,0 +1,3 @@ +# Heltec WiFi LoRa 32 V4 — board.cmake +# Uses esptool for flashing over USB-C (direct ESP32-S3 USB) +include(${ZEPHYR_BASE}/boards/espressif/common/board.cmake) diff --git a/boards/heltec/heltec_wifi_lora32_v4/board.yml b/boards/heltec/heltec_wifi_lora32_v4/board.yml new file mode 100644 index 0000000..f6d279e --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v4/board.yml @@ -0,0 +1,10 @@ +board: + name: heltec_wifi_lora32_v4 + full_name: "Heltec WiFi LoRa 32 V4" + vendor: heltec + url: https://heltec.org/project/wifi-lora-32-v4/ + socs: + - name: esp32s3 + variants: + - name: procpu + - name: appcpu diff --git a/boards/heltec/heltec_wifi_lora32_v4/heltec_wifi_lora32_v4-pinctrl.dtsi b/boards/heltec/heltec_wifi_lora32_v4/heltec_wifi_lora32_v4-pinctrl.dtsi new file mode 100644 index 0000000..33de8e8 --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v4/heltec_wifi_lora32_v4-pinctrl.dtsi @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Heltec WiFi LoRa 32 V4 — pin control definitions + */ + +#include +#include +#include + +&pinctrl { + spim2_default: spim2_default { + group1 { + pinmux = , + , + ; + }; + group2 { + pinmux = ; + output-enable; + }; + }; +}; diff --git a/boards/heltec/heltec_wifi_lora32_v4/heltec_wifi_lora32_v4_procpu.dts b/boards/heltec/heltec_wifi_lora32_v4/heltec_wifi_lora32_v4_procpu.dts new file mode 100644 index 0000000..8dcb45b --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v4/heltec_wifi_lora32_v4_procpu.dts @@ -0,0 +1,111 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Heltec WiFi LoRa 32 V4 — Device Tree Source (PROCPU) + * + * Based on Heltec WiFi LoRa 32 V3 with the following differences: + * - No CP2102 USB-UART bridge; direct ESP32-S3 USB-C + * - Console routed to usb_serial instead of uart0 + */ + +/dts-v1/; +#include +#include "heltec_wifi_lora32_v4-pinctrl.dtsi" +#include + +/ { + model = "Heltec WiFi LoRa 32 V4 PROCPU"; + compatible = "heltec,wifi-lora-32-v4"; + + chosen { + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,console = &usb_serial; + zephyr,shell-uart = &usb_serial; + zephyr,flash-controller = &flash; + }; + + aliases { + lora0 = &lora; + led0 = &led_white; + }; + + leds { + compatible = "gpio-leds"; + led_white: led_0 { + /* V4 LED pin — verify from schematic */ + gpios = <&gpio0 35 GPIO_ACTIVE_HIGH>; + label = "White LED"; + }; + }; + + buttons { + compatible = "gpio-keys"; + boot_button: button_0 { + gpios = <&gpio0 0 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + label = "Boot Button"; + zephyr,code = ; + }; + }; +}; + +&usb_serial { + status = "okay"; +}; + +&uart0 { + status = "disabled"; +}; + +&spi2 { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + pinctrl-0 = <&spim2_default>; + pinctrl-names = "default"; + cs-gpios = <&gpio0 8 GPIO_ACTIVE_LOW>; + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <4000000>; + reset-gpios = <&gpio0 12 (GPIO_OPEN_DRAIN | GPIO_ACTIVE_LOW)>; + busy-gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>; + dio1-gpios = <&gpio0 14 GPIO_ACTIVE_HIGH>; + dio2-tx-enable; + tcxo-power-startup-delay-ms = <5>; + }; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +&flash0 { + status = "okay"; + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + boot_partition: partition@0 { + label = "mcuboot"; + reg = <0x00000000 0x00010000>; + }; + slot0_partition: partition@10000 { + label = "image-0"; + reg = <0x00010000 0x00300000>; + }; + slot1_partition: partition@310000 { + label = "image-1"; + reg = <0x00310000 0x00300000>; + }; + storage_partition: partition@610000 { + label = "storage"; + reg = <0x00610000 0x001f0000>; + }; + }; +}; diff --git a/boards/heltec/heltec_wifi_lora32_v4/heltec_wifi_lora32_v4_procpu_defconfig b/boards/heltec/heltec_wifi_lora32_v4/heltec_wifi_lora32_v4_procpu_defconfig new file mode 100644 index 0000000..1022d48 --- /dev/null +++ b/boards/heltec/heltec_wifi_lora32_v4/heltec_wifi_lora32_v4_procpu_defconfig @@ -0,0 +1,7 @@ +CONFIG_SOC_SERIES_ESP32S3=y +CONFIG_BOARD_HELTEC_WIFI_LORA32_V4=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_UART_CONSOLE=y +CONFIG_SPI=y +CONFIG_GPIO=y diff --git a/boards/heltec_t114.conf b/boards/heltec_t114.conf new file mode 100644 index 0000000..33fc5f1 --- /dev/null +++ b/boards/heltec_t114.conf @@ -0,0 +1,10 @@ +# Heltec T114 (Mesh Node T114) — nRF52840 + SX1262 +# Uses USB CDC-ACM + +CONFIG_LORAMODEM_KISS_IFACE_USB=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_USB_CDC_ACM=y +CONFIG_UART_LINE_CTRL=y + +# SX1262 driver +CONFIG_LORA_SX12XX=y diff --git a/boards/heltec_t114.overlay b/boards/heltec_t114.overlay new file mode 100644 index 0000000..ebe2ebc --- /dev/null +++ b/boards/heltec_t114.overlay @@ -0,0 +1,17 @@ +/* + * Heltec T114 (Mesh Node T114) — application DTS overlay + * KISS interface: USB CDC-ACM + * LoRa: SX1262 on SPI1 (defined in custom board DTS) + */ + +/ { + chosen { + loramodem,kiss-uart = &cdc_acm_uart0; + }; +}; + +&zephyr_udc0 { + cdc_acm_uart0: cdc_acm_uart0 { + compatible = "zephyr,cdc-acm-uart"; + }; +}; diff --git a/boards/heltec_wifi_lora32_v2.conf b/boards/heltec_wifi_lora32_v2.conf new file mode 100644 index 0000000..92128a3 --- /dev/null +++ b/boards/heltec_wifi_lora32_v2.conf @@ -0,0 +1,7 @@ +# Heltec WiFi LoRa 32 V2 — ESP32 + SX1276 +# Uses hardware UART over CP2102 USB-Serial bridge + +CONFIG_LORAMODEM_KISS_IFACE_UART=y + +# SX1276 driver (via loramac-node) +CONFIG_LORA_SX12XX=y diff --git a/boards/heltec_wifi_lora32_v2.overlay b/boards/heltec_wifi_lora32_v2.overlay new file mode 100644 index 0000000..f4dad5b --- /dev/null +++ b/boards/heltec_wifi_lora32_v2.overlay @@ -0,0 +1,11 @@ +/* + * Heltec WiFi LoRa 32 V2 — application DTS overlay + * KISS interface: UART0 (via CP2102 USB-Serial bridge) + * LoRa: SX1276 on SPI2 (already defined in upstream board DTS) + */ + +/ { + chosen { + loramodem,kiss-uart = &uart0; + }; +}; diff --git a/boards/heltec_wifi_lora32_v3_procpu.conf b/boards/heltec_wifi_lora32_v3_procpu.conf new file mode 100644 index 0000000..22d613d --- /dev/null +++ b/boards/heltec_wifi_lora32_v3_procpu.conf @@ -0,0 +1,10 @@ +# Heltec WiFi LoRa 32 V3 (procpu) — ESP32-S3 + SX1262 +# Uses USB CDC-ACM (direct ESP32-S3 USB, no CP2102) + +CONFIG_LORAMODEM_KISS_IFACE_USB=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_USB_CDC_ACM=y +CONFIG_UART_LINE_CTRL=y + +# SX1262 driver +CONFIG_LORA_SX12XX=y diff --git a/boards/heltec_wifi_lora32_v3_procpu.overlay b/boards/heltec_wifi_lora32_v3_procpu.overlay new file mode 100644 index 0000000..8f8958b --- /dev/null +++ b/boards/heltec_wifi_lora32_v3_procpu.overlay @@ -0,0 +1,17 @@ +/* + * Heltec WiFi LoRa 32 V3 (procpu) — application DTS overlay + * KISS interface: USB CDC-ACM + * LoRa: SX1262 on SPI2 (already defined in upstream board DTS) + */ + +/ { + chosen { + loramodem,kiss-uart = &cdc_acm_uart0; + }; +}; + +&zephyr_udc0 { + cdc_acm_uart0: cdc_acm_uart0 { + compatible = "zephyr,cdc-acm-uart"; + }; +}; diff --git a/boards/heltec_wifi_lora32_v4_procpu.conf b/boards/heltec_wifi_lora32_v4_procpu.conf new file mode 100644 index 0000000..d13a381 --- /dev/null +++ b/boards/heltec_wifi_lora32_v4_procpu.conf @@ -0,0 +1,10 @@ +# Heltec WiFi LoRa 32 V4 (procpu) — ESP32-S3 + SX1262 +# Uses USB CDC-ACM (direct USB-C, no CP2102) + +CONFIG_LORAMODEM_KISS_IFACE_USB=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_USB_CDC_ACM=y +CONFIG_UART_LINE_CTRL=y + +# SX1262 driver +CONFIG_LORA_SX12XX=y diff --git a/boards/heltec_wifi_lora32_v4_procpu.overlay b/boards/heltec_wifi_lora32_v4_procpu.overlay new file mode 100644 index 0000000..8e6a9b3 --- /dev/null +++ b/boards/heltec_wifi_lora32_v4_procpu.overlay @@ -0,0 +1,17 @@ +/* + * Heltec WiFi LoRa 32 V4 (procpu) — application DTS overlay + * KISS interface: USB CDC-ACM + * LoRa: SX1262 on SPI2 (defined in custom board DTS) + */ + +/ { + chosen { + loramodem,kiss-uart = &cdc_acm_uart0; + }; +}; + +&zephyr_udc0 { + cdc_acm_uart0: cdc_acm_uart0 { + compatible = "zephyr,cdc-acm-uart"; + }; +}; diff --git a/boards/lilygo/lilygo_tbeam/Kconfig.defconfig b/boards/lilygo/lilygo_tbeam/Kconfig.defconfig new file mode 100644 index 0000000..22d24ec --- /dev/null +++ b/boards/lilygo/lilygo_tbeam/Kconfig.defconfig @@ -0,0 +1,6 @@ +if BOARD_LILYGO_TBEAM + +config BOARD + default "lilygo_tbeam" + +endif # BOARD_LILYGO_TBEAM diff --git a/boards/lilygo/lilygo_tbeam/Kconfig.lilygo_tbeam b/boards/lilygo/lilygo_tbeam/Kconfig.lilygo_tbeam new file mode 100644 index 0000000..53c5cdb --- /dev/null +++ b/boards/lilygo/lilygo_tbeam/Kconfig.lilygo_tbeam @@ -0,0 +1,3 @@ +config BOARD_LILYGO_TBEAM + select SOC_ESP32_PROCPU if BOARD_LILYGO_TBEAM_PROCPU + select SOC_ESP32_APPCPU if BOARD_LILYGO_TBEAM_APPCPU diff --git a/boards/lilygo/lilygo_tbeam/board.cmake b/boards/lilygo/lilygo_tbeam/board.cmake new file mode 100644 index 0000000..86ac1fc --- /dev/null +++ b/boards/lilygo/lilygo_tbeam/board.cmake @@ -0,0 +1,3 @@ +# LilyGo T-Beam — board.cmake +# Uses esptool for flashing over USB-to-serial (CP2104) +include(${ZEPHYR_BASE}/boards/espressif/common/board.cmake) diff --git a/boards/lilygo/lilygo_tbeam/board.yml b/boards/lilygo/lilygo_tbeam/board.yml new file mode 100644 index 0000000..5fd7063 --- /dev/null +++ b/boards/lilygo/lilygo_tbeam/board.yml @@ -0,0 +1,10 @@ +board: + name: lilygo_tbeam + full_name: "LilyGo T-Beam" + vendor: lilygo + url: https://www.lilygo.cc/products/t-beam-v1-1 + socs: + - name: esp32 + variants: + - name: procpu + - name: appcpu diff --git a/boards/lilygo/lilygo_tbeam/lilygo_tbeam-pinctrl.dtsi b/boards/lilygo/lilygo_tbeam/lilygo_tbeam-pinctrl.dtsi new file mode 100644 index 0000000..16428e2 --- /dev/null +++ b/boards/lilygo/lilygo_tbeam/lilygo_tbeam-pinctrl.dtsi @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * LilyGo T-Beam V1.1 — pin control definitions + * SoC: ESP32-D0WD + */ + +#include +#include +#include + +&pinctrl { + uart0_default: uart0_default { + group1 { + pinmux = ; + output-enable; + }; + group2 { + pinmux = ; + bias-pull-up; + }; + }; + + /* SPI3 (VSPI) for SX1276/SX1278 */ + spim3_default: spim3_default { + group1 { + pinmux = , + , + ; + }; + group2 { + pinmux = ; + output-enable; + }; + }; +}; diff --git a/boards/lilygo/lilygo_tbeam/lilygo_tbeam_procpu.dts b/boards/lilygo/lilygo_tbeam/lilygo_tbeam_procpu.dts new file mode 100644 index 0000000..6beee0b --- /dev/null +++ b/boards/lilygo/lilygo_tbeam/lilygo_tbeam_procpu.dts @@ -0,0 +1,102 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * LilyGo T-Beam V1.1 — Device Tree Source (PROCPU) + * + * SoC: ESP32-D0WD + * Radio: SX1276 / SX1278 (LORA32 module) + * Flash: 4 MB + * + * LoRa: MOSI=27 MISO=19 SCK=5 NSS=18 RST=23 DIO0=26 + * UART0: TX=1 RX=3 (via CP2104 USB-serial) + */ + +/dts-v1/; +#include +#include "lilygo_tbeam-pinctrl.dtsi" +#include + +/ { + model = "LilyGo T-Beam V1.1 PROCPU"; + compatible = "lilygo,t-beam"; + + chosen { + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,console = &uart0; + zephyr,shell-uart = &uart0; + zephyr,flash-controller = &flash; + }; + + aliases { + lora0 = &lora; + }; + + buttons { + compatible = "gpio-keys"; + boot_button: button_0 { + gpios = <&gpio0 38 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>; + label = "Boot Button"; + zephyr,code = ; + }; + }; +}; + +&uart0 { + status = "okay"; + current-speed = <115200>; + pinctrl-0 = <&uart0_default>; + pinctrl-names = "default"; +}; + +&spi3 { + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + pinctrl-0 = <&spim3_default>; + pinctrl-names = "default"; + cs-gpios = <&gpio0 18 GPIO_ACTIVE_LOW>; + + lora: lora@0 { + compatible = "semtech,sx1276"; + reg = <0>; + spi-max-frequency = <8000000>; + reset-gpios = <&gpio0 23 GPIO_ACTIVE_LOW>; + dio1-gpios = <&gpio0 26 GPIO_ACTIVE_HIGH>; + power-amplifier-output = "pa-boost"; + }; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +&flash0 { + status = "okay"; + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + boot_partition: partition@0 { + label = "mcuboot"; + reg = <0x00000000 0x00010000>; + }; + slot0_partition: partition@10000 { + label = "image-0"; + reg = <0x00010000 0x00180000>; + }; + slot1_partition: partition@190000 { + label = "image-1"; + reg = <0x00190000 0x00180000>; + }; + storage_partition: partition@310000 { + label = "storage"; + reg = <0x00310000 0x000F0000>; + }; + }; +}; diff --git a/boards/lilygo/lilygo_tbeam/lilygo_tbeam_procpu_defconfig b/boards/lilygo/lilygo_tbeam/lilygo_tbeam_procpu_defconfig new file mode 100644 index 0000000..70bf38d --- /dev/null +++ b/boards/lilygo/lilygo_tbeam/lilygo_tbeam_procpu_defconfig @@ -0,0 +1,7 @@ +CONFIG_SOC_SERIES_ESP32=y +CONFIG_BOARD_LILYGO_TBEAM=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_UART_CONSOLE=y +CONFIG_SPI=y +CONFIG_GPIO=y diff --git a/boards/rak/rak_wismesh_pocket/Kconfig.defconfig b/boards/rak/rak_wismesh_pocket/Kconfig.defconfig new file mode 100644 index 0000000..cd01a4f --- /dev/null +++ b/boards/rak/rak_wismesh_pocket/Kconfig.defconfig @@ -0,0 +1,6 @@ +if BOARD_RAK_WISMESH_POCKET + +config BOARD + default "rak_wismesh_pocket" + +endif # BOARD_RAK_WISMESH_POCKET diff --git a/boards/rak/rak_wismesh_pocket/Kconfig.rak_wismesh_pocket b/boards/rak/rak_wismesh_pocket/Kconfig.rak_wismesh_pocket new file mode 100644 index 0000000..fa4ba91 --- /dev/null +++ b/boards/rak/rak_wismesh_pocket/Kconfig.rak_wismesh_pocket @@ -0,0 +1,2 @@ +config BOARD_RAK_WISMESH_POCKET + select SOC_NRF52840_QIAA diff --git a/boards/rak/rak_wismesh_pocket/board.cmake b/boards/rak/rak_wismesh_pocket/board.cmake new file mode 100644 index 0000000..133e123 --- /dev/null +++ b/boards/rak/rak_wismesh_pocket/board.cmake @@ -0,0 +1,9 @@ +# RAK WisMesh Pocket — board.cmake +# Uses nrfjprog or pyocd for flashing via SWD. +# J-Link is the recommended debugger for RAK WisBlock-family boards. +board_runner_args(nrfjprog "--nrf-family=NRF52") +board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000" "--reset-after-load") +board_runner_args(pyocd "--target=nrf52840") +include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) +include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) +include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) diff --git a/boards/rak/rak_wismesh_pocket/board.yml b/boards/rak/rak_wismesh_pocket/board.yml new file mode 100644 index 0000000..6a79ce5 --- /dev/null +++ b/boards/rak/rak_wismesh_pocket/board.yml @@ -0,0 +1,7 @@ +board: + name: rak_wismesh_pocket + full_name: "RAK WisMesh Pocket" + vendor: rak + url: https://docs.rakwireless.com/Product-Categories/WisMesh/ + socs: + - name: nrf52840 diff --git a/boards/rak/rak_wismesh_pocket/rak_wismesh_pocket-pinctrl.dtsi b/boards/rak/rak_wismesh_pocket/rak_wismesh_pocket-pinctrl.dtsi new file mode 100644 index 0000000..f9257f7 --- /dev/null +++ b/boards/rak/rak_wismesh_pocket/rak_wismesh_pocket-pinctrl.dtsi @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * RAK WisMesh Pocket — pin control definitions + * Matches RAK4631 GPIO assignments. Verify from WisMesh Pocket schematic. + */ + +#include +#include + +&pinctrl { + uart0_default: uart0_default { + group1 { + psels = ; + }; + group2 { + psels = ; + bias-pull-up; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* SPI1 for SX1262 — RAK4631 pin assignments */ + spi1_default: spi1_default { + group1 { + psels = , + , + ; + }; + }; + + spi1_sleep: spi1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; +}; diff --git a/boards/rak/rak_wismesh_pocket/rak_wismesh_pocket_nrf52840.dts b/boards/rak/rak_wismesh_pocket/rak_wismesh_pocket_nrf52840.dts new file mode 100644 index 0000000..d647981 --- /dev/null +++ b/boards/rak/rak_wismesh_pocket/rak_wismesh_pocket_nrf52840.dts @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * RAK WisMesh Pocket — Device Tree Source + * SoC: nRF52840 (same as RAK4631 WisBlock Core) + * Radio: SX1262 + * + * The WisMesh Pocket is derived from the RAK4631 with a different + * form factor optimized for portable deployment. GPIO assignments + * are assumed identical to RAK4631; verify against the WisMesh + * Pocket schematic before using with real hardware. + */ + +/dts-v1/; +#include +#include "rak_wismesh_pocket-pinctrl.dtsi" + +/ { + model = "RAK WisMesh Pocket"; + compatible = "rak,wismesh-pocket"; + + chosen { + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,code-partition = &slot0_partition; + }; + + aliases { + lora0 = &lora; + led0 = &led_green; + }; + + leds { + compatible = "gpio-leds"; + led_green: led_0 { + /* RAK4631 green LED — verify for WisMesh Pocket */ + gpios = <&gpio1 1 GPIO_ACTIVE_HIGH>; + label = "Green LED"; + }; + }; +}; + +&uicr { + gpio-as-nreset; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +&uart0 { + compatible = "nordic,nrf-uarte"; + status = "okay"; + current-speed = <115200>; + pinctrl-0 = <&uart0_default>; + pinctrl-names = "default"; +}; + +/* + * SX1262 on SPI1 + * GPIO assignments match RAK4631 — verify for WisMesh Pocket. + */ +&spi1 { + compatible = "nordic,nrf-spim"; + status = "okay"; + pinctrl-0 = <&spi1_default>; + pinctrl-names = "default"; + cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>; + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <1000000>; + reset-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>; + busy-gpios = <&gpio1 14 GPIO_ACTIVE_HIGH>; + dio1-gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>; + dio2-tx-enable; + dio3-tcxo-voltage = ; + tcxo-power-startup-delay-ms = <5>; + }; +}; + +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; +}; + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + boot_partition: partition@0 { + label = "mcuboot"; + reg = <0x00000000 0x0000C000>; + }; + slot0_partition: partition@c000 { + label = "image-0"; + reg = <0x0000C000 0x00067000>; + }; + slot1_partition: partition@73000 { + label = "image-1"; + reg = <0x00073000 0x00067000>; + }; + storage_partition: partition@da000 { + label = "storage"; + reg = <0x000da000 0x00006000>; + }; + }; +}; diff --git a/boards/rak/rak_wismesh_pocket/rak_wismesh_pocket_nrf52840_defconfig b/boards/rak/rak_wismesh_pocket/rak_wismesh_pocket_nrf52840_defconfig new file mode 100644 index 0000000..d997eff --- /dev/null +++ b/boards/rak/rak_wismesh_pocket/rak_wismesh_pocket_nrf52840_defconfig @@ -0,0 +1,8 @@ +CONFIG_SOC_NRF52840_QIAA=y +CONFIG_BOARD_RAK_WISMESH_POCKET=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_GPIO=y +CONFIG_SPI=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_NRFX_POWER=y diff --git a/boards/rak/rak_wismesh_tag/Kconfig.defconfig b/boards/rak/rak_wismesh_tag/Kconfig.defconfig new file mode 100644 index 0000000..0edb9f5 --- /dev/null +++ b/boards/rak/rak_wismesh_tag/Kconfig.defconfig @@ -0,0 +1,6 @@ +if BOARD_RAK_WISMESH_TAG + +config BOARD + default "rak_wismesh_tag" + +endif # BOARD_RAK_WISMESH_TAG diff --git a/boards/rak/rak_wismesh_tag/Kconfig.rak_wismesh_tag b/boards/rak/rak_wismesh_tag/Kconfig.rak_wismesh_tag new file mode 100644 index 0000000..6ba2df3 --- /dev/null +++ b/boards/rak/rak_wismesh_tag/Kconfig.rak_wismesh_tag @@ -0,0 +1,2 @@ +config BOARD_RAK_WISMESH_TAG + select SOC_NRF52840_QIAA diff --git a/boards/rak/rak_wismesh_tag/board.cmake b/boards/rak/rak_wismesh_tag/board.cmake new file mode 100644 index 0000000..66f1f8c --- /dev/null +++ b/boards/rak/rak_wismesh_tag/board.cmake @@ -0,0 +1,9 @@ +# RAK WisMesh Tag — board.cmake +# Uses nrfjprog or pyocd for flashing via SWD. +# J-Link is the recommended debugger for RAK WisBlock-family boards. +board_runner_args(nrfjprog "--nrf-family=NRF52") +board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000" "--reset-after-load") +board_runner_args(pyocd "--target=nrf52840") +include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) +include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) +include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) diff --git a/boards/rak/rak_wismesh_tag/board.yml b/boards/rak/rak_wismesh_tag/board.yml new file mode 100644 index 0000000..bb9a9e7 --- /dev/null +++ b/boards/rak/rak_wismesh_tag/board.yml @@ -0,0 +1,7 @@ +board: + name: rak_wismesh_tag + full_name: "RAK WisMesh Tag" + vendor: rak + url: https://docs.rakwireless.com/Product-Categories/WisMesh/ + socs: + - name: nrf52840 diff --git a/boards/rak/rak_wismesh_tag/rak_wismesh_tag-pinctrl.dtsi b/boards/rak/rak_wismesh_tag/rak_wismesh_tag-pinctrl.dtsi new file mode 100644 index 0000000..30fe206 --- /dev/null +++ b/boards/rak/rak_wismesh_tag/rak_wismesh_tag-pinctrl.dtsi @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * RAK WisMesh Tag — pin control definitions + * Matches RAK4631 GPIO assignments. Verify from WisMesh Tag schematic. + */ + +#include +#include + +&pinctrl { + uart0_default: uart0_default { + group1 { + psels = ; + }; + group2 { + psels = ; + bias-pull-up; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* SPI1 for SX1262 — RAK4631 pin assignments */ + spi1_default: spi1_default { + group1 { + psels = , + , + ; + }; + }; + + spi1_sleep: spi1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; +}; diff --git a/boards/rak/rak_wismesh_tag/rak_wismesh_tag_nrf52840.dts b/boards/rak/rak_wismesh_tag/rak_wismesh_tag_nrf52840.dts new file mode 100644 index 0000000..e3339e3 --- /dev/null +++ b/boards/rak/rak_wismesh_tag/rak_wismesh_tag_nrf52840.dts @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * RAK WisMesh Tag — Device Tree Source + * SoC: nRF52840 (same as RAK4631 WisBlock Core) + * Radio: SX1262 + * + * The WisMesh Tag is derived from the RAK4631 with a different + * form factor. GPIO assignments are assumed identical to RAK4631; + * verify against the WisMesh Tag schematic before using with + * real hardware. + */ + +/dts-v1/; +#include +#include "rak_wismesh_tag-pinctrl.dtsi" + +/ { + model = "RAK WisMesh Tag"; + compatible = "rak,wismesh-tag"; + + chosen { + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,code-partition = &slot0_partition; + }; + + aliases { + lora0 = &lora; + led0 = &led_green; + }; + + leds { + compatible = "gpio-leds"; + led_green: led_0 { + /* RAK4631 green LED — verify for WisMesh Tag */ + gpios = <&gpio1 1 GPIO_ACTIVE_HIGH>; + label = "Green LED"; + }; + }; +}; + +&uicr { + gpio-as-nreset; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +&uart0 { + compatible = "nordic,nrf-uarte"; + status = "okay"; + current-speed = <115200>; + pinctrl-0 = <&uart0_default>; + pinctrl-names = "default"; +}; + +/* + * SX1262 on SPI1 + * GPIO assignments match RAK4631 — verify for WisMesh Tag. + */ +&spi1 { + compatible = "nordic,nrf-spim"; + status = "okay"; + pinctrl-0 = <&spi1_default>; + pinctrl-names = "default"; + cs-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>; + + lora: lora@0 { + compatible = "semtech,sx1262"; + reg = <0>; + spi-max-frequency = <1000000>; + reset-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>; + busy-gpios = <&gpio1 14 GPIO_ACTIVE_HIGH>; + dio1-gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>; + dio2-tx-enable; + dio3-tcxo-voltage = ; + tcxo-power-startup-delay-ms = <5>; + }; +}; + +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; +}; + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + boot_partition: partition@0 { + label = "mcuboot"; + reg = <0x00000000 0x0000C000>; + }; + slot0_partition: partition@c000 { + label = "image-0"; + reg = <0x0000C000 0x00067000>; + }; + slot1_partition: partition@73000 { + label = "image-1"; + reg = <0x00073000 0x00067000>; + }; + storage_partition: partition@da000 { + label = "storage"; + reg = <0x000da000 0x00006000>; + }; + }; +}; diff --git a/boards/rak/rak_wismesh_tag/rak_wismesh_tag_nrf52840_defconfig b/boards/rak/rak_wismesh_tag/rak_wismesh_tag_nrf52840_defconfig new file mode 100644 index 0000000..45f5d70 --- /dev/null +++ b/boards/rak/rak_wismesh_tag/rak_wismesh_tag_nrf52840_defconfig @@ -0,0 +1,8 @@ +CONFIG_SOC_NRF52840_QIAA=y +CONFIG_BOARD_RAK_WISMESH_TAG=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_GPIO=y +CONFIG_SPI=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_NRFX_POWER=y diff --git a/boards/rak4631_nrf52840.conf b/boards/rak4631_nrf52840.conf new file mode 100644 index 0000000..0141497 --- /dev/null +++ b/boards/rak4631_nrf52840.conf @@ -0,0 +1,10 @@ +# RAK WisBlock (RAK4631) — nRF52840 + SX1262 +# Uses USB CDC-ACM + +CONFIG_LORAMODEM_KISS_IFACE_USB=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_USB_CDC_ACM=y +CONFIG_UART_LINE_CTRL=y + +# SX1262 driver +CONFIG_LORA_SX12XX=y diff --git a/boards/rak4631_nrf52840.overlay b/boards/rak4631_nrf52840.overlay new file mode 100644 index 0000000..05420d2 --- /dev/null +++ b/boards/rak4631_nrf52840.overlay @@ -0,0 +1,17 @@ +/* + * RAK WisBlock (RAK4631 / nRF52840) — application DTS overlay + * KISS interface: USB CDC-ACM + * LoRa: SX1262 on SPI1 (already defined in upstream board DTS) + */ + +/ { + chosen { + loramodem,kiss-uart = &cdc_acm_uart0; + }; +}; + +&zephyr_udc0 { + cdc_acm_uart0: cdc_acm_uart0 { + compatible = "zephyr,cdc-acm-uart"; + }; +}; diff --git a/boards/rak_wismesh_pocket.conf b/boards/rak_wismesh_pocket.conf new file mode 100644 index 0000000..5779a75 --- /dev/null +++ b/boards/rak_wismesh_pocket.conf @@ -0,0 +1,10 @@ +# RAK WisMesh Pocket — nRF52840 + SX1262 +# Uses USB CDC-ACM + +CONFIG_LORAMODEM_KISS_IFACE_USB=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_USB_CDC_ACM=y +CONFIG_UART_LINE_CTRL=y + +# SX1262 driver +CONFIG_LORA_SX12XX=y diff --git a/boards/rak_wismesh_pocket.overlay b/boards/rak_wismesh_pocket.overlay new file mode 100644 index 0000000..b649076 --- /dev/null +++ b/boards/rak_wismesh_pocket.overlay @@ -0,0 +1,17 @@ +/* + * RAK WisMesh Pocket — application DTS overlay + * KISS interface: USB CDC-ACM + * LoRa: SX1262 on SPI1 (defined in custom board DTS) + */ + +/ { + chosen { + loramodem,kiss-uart = &cdc_acm_uart0; + }; +}; + +&zephyr_udc0 { + cdc_acm_uart0: cdc_acm_uart0 { + compatible = "zephyr,cdc-acm-uart"; + }; +}; diff --git a/boards/rak_wismesh_tag.conf b/boards/rak_wismesh_tag.conf new file mode 100644 index 0000000..ee08f10 --- /dev/null +++ b/boards/rak_wismesh_tag.conf @@ -0,0 +1,10 @@ +# RAK WisMesh Tag — nRF52840 + SX1262 +# Uses USB CDC-ACM + +CONFIG_LORAMODEM_KISS_IFACE_USB=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_USB_CDC_ACM=y +CONFIG_UART_LINE_CTRL=y + +# SX1262 driver +CONFIG_LORA_SX12XX=y diff --git a/boards/rak_wismesh_tag.overlay b/boards/rak_wismesh_tag.overlay new file mode 100644 index 0000000..634e102 --- /dev/null +++ b/boards/rak_wismesh_tag.overlay @@ -0,0 +1,17 @@ +/* + * RAK WisMesh Tag — application DTS overlay + * KISS interface: USB CDC-ACM + * LoRa: SX1262 on SPI1 (defined in custom board DTS) + */ + +/ { + chosen { + loramodem,kiss-uart = &cdc_acm_uart0; + }; +}; + +&zephyr_udc0 { + cdc_acm_uart0: cdc_acm_uart0 { + compatible = "zephyr,cdc-acm-uart"; + }; +}; diff --git a/boards/seeed/seeed_wio_tracker/Kconfig.defconfig b/boards/seeed/seeed_wio_tracker/Kconfig.defconfig new file mode 100644 index 0000000..0d30f78 --- /dev/null +++ b/boards/seeed/seeed_wio_tracker/Kconfig.defconfig @@ -0,0 +1,6 @@ +if BOARD_SEEED_WIO_TRACKER + +config BOARD + default "seeed_wio_tracker" + +endif # BOARD_SEEED_WIO_TRACKER diff --git a/boards/seeed/seeed_wio_tracker/Kconfig.seeed_wio_tracker b/boards/seeed/seeed_wio_tracker/Kconfig.seeed_wio_tracker new file mode 100644 index 0000000..2ab1195 --- /dev/null +++ b/boards/seeed/seeed_wio_tracker/Kconfig.seeed_wio_tracker @@ -0,0 +1,2 @@ +config BOARD_SEEED_WIO_TRACKER + select SOC_NRF52840_QIAA diff --git a/boards/seeed/seeed_wio_tracker/board.cmake b/boards/seeed/seeed_wio_tracker/board.cmake new file mode 100644 index 0000000..d2753ba --- /dev/null +++ b/boards/seeed/seeed_wio_tracker/board.cmake @@ -0,0 +1,6 @@ +# Seeed Wio Tracker 1110 — board.cmake +# Uses nrfjprog or pyocd for flashing via SWD. +board_runner_args(nrfjprog "--nrf-family=NRF52") +board_runner_args(pyocd "--target=nrf52840") +include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) +include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) diff --git a/boards/seeed/seeed_wio_tracker/board.yml b/boards/seeed/seeed_wio_tracker/board.yml new file mode 100644 index 0000000..b30f0e0 --- /dev/null +++ b/boards/seeed/seeed_wio_tracker/board.yml @@ -0,0 +1,7 @@ +board: + name: seeed_wio_tracker + full_name: "Seeed Wio Tracker 1110" + vendor: seeed + url: https://wiki.seeedstudio.com/wio_tracker_1110_intro/ + socs: + - name: nrf52840 diff --git a/boards/seeed/seeed_wio_tracker/seeed_wio_tracker-pinctrl.dtsi b/boards/seeed/seeed_wio_tracker/seeed_wio_tracker-pinctrl.dtsi new file mode 100644 index 0000000..1dd3af8 --- /dev/null +++ b/boards/seeed/seeed_wio_tracker/seeed_wio_tracker-pinctrl.dtsi @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Seeed Wio Tracker 1110 — pin control definitions + * NOTE: GPIO numbers are placeholders. Verify from schematic. + */ + +#include +#include + +&pinctrl { + uart0_default: uart0_default { + group1 { + psels = ; + }; + group2 { + psels = ; + bias-pull-up; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* SPI1 for LR1110 — verify pins from Wio Tracker 1110 schematic */ + spi1_default: spi1_default { + group1 { + psels = , + , + ; + }; + }; + + spi1_sleep: spi1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; +}; diff --git a/boards/seeed/seeed_wio_tracker/seeed_wio_tracker.dts b/boards/seeed/seeed_wio_tracker/seeed_wio_tracker.dts new file mode 100644 index 0000000..d304de1 --- /dev/null +++ b/boards/seeed/seeed_wio_tracker/seeed_wio_tracker.dts @@ -0,0 +1,117 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Seeed Wio Tracker 1110 — Device Tree Source + * SoC: nRF52840 + * Radio: Semtech LR1110 (LoRa + GNSS multi-protocol transceiver) + * + * NOTE: GPIO pin numbers are placeholders. Verify against the + * Wio Tracker 1110 schematic before using with real hardware. + * The LR1110 SPI interface differs from SX126x: it uses 2-byte + * opcodes and requires the BUSY pin to be checked between transactions. + */ + +/dts-v1/; +#include +#include "seeed_wio_tracker-pinctrl.dtsi" + +/ { + model = "Seeed Wio Tracker 1110"; + compatible = "seeed,wio-tracker-1110"; + + chosen { + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,code-partition = &slot0_partition; + }; + + aliases { + lora0 = &lora; + led0 = &led_green; + }; + + leds { + compatible = "gpio-leds"; + led_green: led_0 { + /* TODO: verify LED GPIO from Wio Tracker 1110 schematic */ + gpios = <&gpio0 13 GPIO_ACTIVE_LOW>; + label = "Green LED"; + }; + }; +}; + +&uicr { + gpio-as-nreset; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +&uart0 { + compatible = "nordic,nrf-uarte"; + status = "okay"; + current-speed = <115200>; + pinctrl-0 = <&uart0_default>; + pinctrl-names = "default"; +}; + +/* + * LR1110 on SPI1 + * Pin assignments are placeholders — must be verified from schematic. + * The LR1110 BUSY and EVENT (DIO9) pins are both required by the driver. + */ +&spi1 { + compatible = "nordic,nrf-spim"; + status = "okay"; + pinctrl-0 = <&spi1_default>; + pinctrl-names = "default"; + cs-gpios = <&gpio0 24 GPIO_ACTIVE_LOW>; + + lora: lora@0 { + compatible = "semtech,lr11xx"; + reg = <0>; + spi-max-frequency = <4000000>; + reset-gpios = <&gpio0 25 GPIO_ACTIVE_LOW>; + busy-gpios = <&gpio0 26 GPIO_ACTIVE_HIGH>; + event-gpios = <&gpio0 27 GPIO_ACTIVE_HIGH>; + }; +}; + +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; +}; + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + boot_partition: partition@0 { + label = "mcuboot"; + reg = <0x00000000 0x0000C000>; + }; + slot0_partition: partition@c000 { + label = "image-0"; + reg = <0x0000C000 0x00067000>; + }; + slot1_partition: partition@73000 { + label = "image-1"; + reg = <0x00073000 0x00067000>; + }; + storage_partition: partition@da000 { + label = "storage"; + reg = <0x000da000 0x00006000>; + }; + }; +}; diff --git a/boards/seeed/seeed_wio_tracker/seeed_wio_tracker_defconfig b/boards/seeed/seeed_wio_tracker/seeed_wio_tracker_defconfig new file mode 100644 index 0000000..f96fb03 --- /dev/null +++ b/boards/seeed/seeed_wio_tracker/seeed_wio_tracker_defconfig @@ -0,0 +1,8 @@ +CONFIG_SOC_NRF52840_QIAA=y +CONFIG_BOARD_SEEED_WIO_TRACKER=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_GPIO=y +CONFIG_SPI=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_NRFX_POWER=y diff --git a/boards/seeed_wio_tracker.conf b/boards/seeed_wio_tracker.conf new file mode 100644 index 0000000..ee7b3bb --- /dev/null +++ b/boards/seeed_wio_tracker.conf @@ -0,0 +1,10 @@ +# Seeed Wio Tracker 1110 — nRF52840 + LR1110 +# Uses USB CDC-ACM + +CONFIG_LORAMODEM_KISS_IFACE_USB=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_USB_CDC_ACM=y +CONFIG_UART_LINE_CTRL=y + +# LR11xx driver (out-of-tree) +CONFIG_LORA_LR11XX=y diff --git a/boards/seeed_wio_tracker.overlay b/boards/seeed_wio_tracker.overlay new file mode 100644 index 0000000..830bf9b --- /dev/null +++ b/boards/seeed_wio_tracker.overlay @@ -0,0 +1,17 @@ +/* + * Seeed Wio Tracker 1110 — application DTS overlay + * KISS interface: USB CDC-ACM + * LoRa: LR1110 on SPI1 (defined in custom board DTS) + */ + +/ { + chosen { + loramodem,kiss-uart = &cdc_acm_uart0; + }; +}; + +&zephyr_udc0 { + cdc_acm_uart0: cdc_acm_uart0 { + compatible = "zephyr,cdc-acm-uart"; + }; +}; diff --git a/boards/sensecap/sensecap_t1000e/Kconfig.defconfig b/boards/sensecap/sensecap_t1000e/Kconfig.defconfig new file mode 100644 index 0000000..02d8791 --- /dev/null +++ b/boards/sensecap/sensecap_t1000e/Kconfig.defconfig @@ -0,0 +1,6 @@ +if BOARD_SENSECAP_T1000E + +config BOARD + default "sensecap_t1000e" + +endif # BOARD_SENSECAP_T1000E diff --git a/boards/sensecap/sensecap_t1000e/Kconfig.sensecap_t1000e b/boards/sensecap/sensecap_t1000e/Kconfig.sensecap_t1000e new file mode 100644 index 0000000..9915dd5 --- /dev/null +++ b/boards/sensecap/sensecap_t1000e/Kconfig.sensecap_t1000e @@ -0,0 +1,2 @@ +config BOARD_SENSECAP_T1000E + select SOC_NRF52840_QIAA diff --git a/boards/sensecap/sensecap_t1000e/board.cmake b/boards/sensecap/sensecap_t1000e/board.cmake new file mode 100644 index 0000000..a8af2a8 --- /dev/null +++ b/boards/sensecap/sensecap_t1000e/board.cmake @@ -0,0 +1,6 @@ +# SenseCAP T1000-E — board.cmake +# Uses nrfjprog or pyocd for flashing via SWD. +board_runner_args(nrfjprog "--nrf-family=NRF52") +board_runner_args(pyocd "--target=nrf52840") +include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake) +include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) diff --git a/boards/sensecap/sensecap_t1000e/board.yml b/boards/sensecap/sensecap_t1000e/board.yml new file mode 100644 index 0000000..cf58c0a --- /dev/null +++ b/boards/sensecap/sensecap_t1000e/board.yml @@ -0,0 +1,7 @@ +board: + name: sensecap_t1000e + full_name: "SenseCAP T1000-E" + vendor: sensecap + url: https://www.seeedstudio.com/SenseCAP-Card-Tracker-T1000-E-p-5913.html + socs: + - name: nrf52840 diff --git a/boards/sensecap/sensecap_t1000e/sensecap_t1000e-pinctrl.dtsi b/boards/sensecap/sensecap_t1000e/sensecap_t1000e-pinctrl.dtsi new file mode 100644 index 0000000..597b510 --- /dev/null +++ b/boards/sensecap/sensecap_t1000e/sensecap_t1000e-pinctrl.dtsi @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * SenseCAP T1000-E — pin control definitions + * NOTE: GPIO numbers are placeholders. Verify from schematic. + */ + +#include +#include + +&pinctrl { + uart0_default: uart0_default { + group1 { + psels = ; + }; + group2 { + psels = ; + bias-pull-up; + }; + }; + + uart0_sleep: uart0_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + /* SPI1 for LR1110 — verify pins from T1000-E schematic */ + spi1_default: spi1_default { + group1 { + psels = , + , + ; + }; + }; + + spi1_sleep: spi1_sleep { + group1 { + psels = , + , + ; + low-power-enable; + }; + }; +}; diff --git a/boards/sensecap/sensecap_t1000e/sensecap_t1000e.dts b/boards/sensecap/sensecap_t1000e/sensecap_t1000e.dts new file mode 100644 index 0000000..02344bd --- /dev/null +++ b/boards/sensecap/sensecap_t1000e/sensecap_t1000e.dts @@ -0,0 +1,116 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * SenseCAP T1000-E — Device Tree Source + * SoC: nRF52840 + * Radio: Semtech LR1110 (LoRa + GNSS multi-protocol transceiver) + * + * NOTE: GPIO pin numbers are placeholders. Verify against the + * SenseCAP T1000-E schematic before using with real hardware. + * The T1000-E is a different product from the Seeed Wio Tracker 1110, + * despite sharing the same SoC and radio chip. + */ + +/dts-v1/; +#include +#include "sensecap_t1000e-pinctrl.dtsi" + +/ { + model = "SenseCAP T1000-E"; + compatible = "sensecap,t1000e"; + + chosen { + zephyr,sram = &sram0; + zephyr,flash = &flash0; + zephyr,code-partition = &slot0_partition; + }; + + aliases { + lora0 = &lora; + led0 = &led_green; + }; + + leds { + compatible = "gpio-leds"; + led_green: led_0 { + /* TODO: verify LED GPIO from T1000-E schematic */ + gpios = <&gpio0 14 GPIO_ACTIVE_LOW>; + label = "Green LED"; + }; + }; +}; + +&uicr { + gpio-as-nreset; +}; + +&gpiote { + status = "okay"; +}; + +&gpio0 { + status = "okay"; +}; + +&gpio1 { + status = "okay"; +}; + +&uart0 { + compatible = "nordic,nrf-uarte"; + status = "okay"; + current-speed = <115200>; + pinctrl-0 = <&uart0_default>; + pinctrl-names = "default"; +}; + +/* + * LR1110 on SPI1 + * Pin assignments are placeholders — must be verified from schematic. + */ +&spi1 { + compatible = "nordic,nrf-spim"; + status = "okay"; + pinctrl-0 = <&spi1_default>; + pinctrl-names = "default"; + cs-gpios = <&gpio1 0 GPIO_ACTIVE_LOW>; + + lora: lora@0 { + compatible = "semtech,lr11xx"; + reg = <0>; + spi-max-frequency = <4000000>; + reset-gpios = <&gpio1 1 GPIO_ACTIVE_LOW>; + busy-gpios = <&gpio1 2 GPIO_ACTIVE_HIGH>; + event-gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>; + }; +}; + +zephyr_udc0: &usbd { + compatible = "nordic,nrf-usbd"; + status = "okay"; +}; + +&flash0 { + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + boot_partition: partition@0 { + label = "mcuboot"; + reg = <0x00000000 0x0000C000>; + }; + slot0_partition: partition@c000 { + label = "image-0"; + reg = <0x0000C000 0x00067000>; + }; + slot1_partition: partition@73000 { + label = "image-1"; + reg = <0x00073000 0x00067000>; + }; + storage_partition: partition@da000 { + label = "storage"; + reg = <0x000da000 0x00006000>; + }; + }; +}; diff --git a/boards/sensecap/sensecap_t1000e/sensecap_t1000e_defconfig b/boards/sensecap/sensecap_t1000e/sensecap_t1000e_defconfig new file mode 100644 index 0000000..7dc3500 --- /dev/null +++ b/boards/sensecap/sensecap_t1000e/sensecap_t1000e_defconfig @@ -0,0 +1,8 @@ +CONFIG_SOC_NRF52840_QIAA=y +CONFIG_BOARD_SENSECAP_T1000E=y +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y +CONFIG_GPIO=y +CONFIG_SPI=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_NRFX_POWER=y diff --git a/boards/sensecap_t1000e.conf b/boards/sensecap_t1000e.conf new file mode 100644 index 0000000..28a2625 --- /dev/null +++ b/boards/sensecap_t1000e.conf @@ -0,0 +1,10 @@ +# SenseCAP T1000-E — nRF52840 + LR1110 +# Uses USB CDC-ACM + +CONFIG_LORAMODEM_KISS_IFACE_USB=y +CONFIG_USB_DEVICE_STACK=y +CONFIG_USB_CDC_ACM=y +CONFIG_UART_LINE_CTRL=y + +# LR11xx driver (out-of-tree) +CONFIG_LORA_LR11XX=y diff --git a/boards/sensecap_t1000e.overlay b/boards/sensecap_t1000e.overlay new file mode 100644 index 0000000..f9e1913 --- /dev/null +++ b/boards/sensecap_t1000e.overlay @@ -0,0 +1,17 @@ +/* + * SenseCAP T1000-E — application DTS overlay + * KISS interface: USB CDC-ACM + * LoRa: LR1110 on SPI1 (defined in custom board DTS) + */ + +/ { + chosen { + loramodem,kiss-uart = &cdc_acm_uart0; + }; +}; + +&zephyr_udc0 { + cdc_acm_uart0: cdc_acm_uart0 { + compatible = "zephyr,cdc-acm-uart"; + }; +}; diff --git a/drivers/lora/lr11xx/CMakeLists.txt b/drivers/lora/lr11xx/CMakeLists.txt new file mode 100644 index 0000000..e41442d --- /dev/null +++ b/drivers/lora/lr11xx/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright (c) 2024 loramodem contributors +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() + +zephyr_library_sources_ifdef(CONFIG_LORA_LR11XX + lr11xx.c + lr11xx_hal.c +) + +zephyr_library_include_directories(.) diff --git a/drivers/lora/lr11xx/Kconfig b/drivers/lora/lr11xx/Kconfig new file mode 100644 index 0000000..a41d3ff --- /dev/null +++ b/drivers/lora/lr11xx/Kconfig @@ -0,0 +1,16 @@ +# Copyright (c) 2024 loramodem contributors +# SPDX-License-Identifier: Apache-2.0 + +config LORA_LR11XX + bool "Semtech LR11xx LoRa driver" + default y + depends on DT_HAS_SEMTECH_LR11XX_ENABLED + depends on SPI + select LORA + help + Enable driver for Semtech LR1110/LR1120 multi-protocol radio + transceivers. This driver implements Zephyr's LoRa subsystem API + for LoRa packet mode operation only. + + The LR11xx uses a 2-byte SPI opcode protocol. BUSY pin must be + polled between transactions. diff --git a/drivers/lora/lr11xx/lr11xx.c b/drivers/lora/lr11xx/lr11xx.c new file mode 100644 index 0000000..fa048c8 --- /dev/null +++ b/drivers/lora/lr11xx/lr11xx.c @@ -0,0 +1,479 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Semtech LR11xx LoRa driver for Zephyr RTOS. + * + * Implements Zephyr's lora_driver_api for LoRa packet mode operation. + * Supports: LR1110, LR1120. + * + * The LR11xx SPI protocol uses 2-byte opcodes and requires BUSY polling + * between transactions. See lr11xx.h for opcode definitions. + * + * Only LoRa packet mode is implemented. LoRaWAN and GNSS functions + * of the LR11xx are not used by the KISS modem application. + */ + +#define DT_DRV_COMPAT semtech_lr11xx + +#include "lr11xx.h" + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(lr11xx, CONFIG_LORA_LOG_LEVEL); + +/* ── Helpers to map Zephyr lora_modem_config to LR11xx codes ─────────────── */ + +static uint8_t zephyr_sf_to_lr11xx(enum lora_datarate dr) +{ + switch (dr) { + case SF_5: return LR11XX_LORA_SF5; + case SF_6: return LR11XX_LORA_SF6; + case SF_7: return LR11XX_LORA_SF7; + case SF_8: return LR11XX_LORA_SF8; + case SF_9: return LR11XX_LORA_SF9; + case SF_10: return LR11XX_LORA_SF10; + case SF_11: return LR11XX_LORA_SF11; + case SF_12: /* fall through */ + default: return LR11XX_LORA_SF12; + } +} + +static uint8_t zephyr_bw_to_lr11xx(enum lora_signal_bandwidth bw) +{ + switch (bw) { + case BW_500_KHZ: return LR11XX_LORA_BW_500; + case BW_250_KHZ: return LR11XX_LORA_BW_250; + case BW_125_KHZ: /* fall through */ + default: return LR11XX_LORA_BW_125; + } +} + +static uint8_t zephyr_cr_to_lr11xx(enum lora_coding_rate cr) +{ + switch (cr) { + case CR_4_5: return LR11XX_LORA_CR_4_5; + case CR_4_6: return LR11XX_LORA_CR_4_6; + case CR_4_7: return LR11XX_LORA_CR_4_7; + case CR_4_8: /* fall through */ + default: return LR11XX_LORA_CR_4_8; + } +} + +/* ── Radio configuration ─────────────────────────────────────────────────── */ + +static int lr11xx_configure(const struct device *dev, + struct lora_modem_config *cfg) +{ + int ret; + uint8_t params[8]; + + /* Enter standby (RC oscillator) */ + params[0] = LR11XX_STANDBY_RC; + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_SET_STANDBY, params, 1); + if (ret < 0) { + return ret; + } + + /* Set packet type to LoRa */ + params[0] = LR11XX_PKT_TYPE_LORA; + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_SET_PACKET_TYPE, params, 1); + if (ret < 0) { + return ret; + } + + /* Set RF frequency */ + uint32_t freq = (uint32_t)cfg->frequency; + + params[0] = (freq >> 24) & 0xFF; + params[1] = (freq >> 16) & 0xFF; + params[2] = (freq >> 8) & 0xFF; + params[3] = (freq >> 0) & 0xFF; + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_SET_RF_FREQUENCY, params, 4); + if (ret < 0) { + return ret; + } + + /* Set TX power and ramp time (ramp 40 us = 0x04) */ + params[0] = (uint8_t)cfg->tx_power; + params[1] = 0x04; /* Ramp time */ + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_SET_TX_PARAMS, params, 2); + if (ret < 0) { + return ret; + } + + /* + * Set modulation parameters (LoRa): + * [0] SF, [1] BW, [2] CR, [3] LDRO (low data rate optimize) + */ + params[0] = zephyr_sf_to_lr11xx(cfg->datarate); + params[1] = zephyr_bw_to_lr11xx(cfg->bandwidth); + params[2] = zephyr_cr_to_lr11xx(cfg->coding_rate); + params[3] = 0x00; /* LDRO: auto would require SF/BW calculation */ + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_SET_MODULATION_PARAMS, + params, 4); + if (ret < 0) { + return ret; + } + + /* + * Set packet parameters (LoRa): + * [0:1] preamble length, [2] header type (0=explicit), + * [3] payload length, [4] CRC mode (1=on), + * [5] IQ inversion (0=standard) + */ + uint16_t preamble = (uint16_t)cfg->preamble_len; + + params[0] = (preamble >> 8) & 0xFF; + params[1] = (preamble >> 0) & 0xFF; + params[2] = 0x00; /* explicit header */ + params[3] = 0xFF; /* max payload (will be overridden per-packet) */ + params[4] = 0x01; /* CRC on */ + params[5] = 0x00; /* standard IQ */ + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_SET_PACKET_PARAMS, + params, 6); + if (ret < 0) { + return ret; + } + + /* Set buffer base addresses (TX=0, RX=0) */ + params[0] = 0x00; + params[1] = 0x00; + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_SET_BUFFER_BASE_ADDR, + params, 2); + if (ret < 0) { + return ret; + } + + /* Configure DIO IRQ routing to EVENT/DIO9 */ + uint32_t irq_mask = LR11XX_DIO9_MASK; + + params[0] = (irq_mask >> 24) & 0xFF; + params[1] = (irq_mask >> 16) & 0xFF; + params[2] = (irq_mask >> 8) & 0xFF; + params[3] = (irq_mask >> 0) & 0xFF; + /* DIO9 enable mask = same as IRQ mask */ + params[4] = params[0]; + params[5] = params[1]; + params[6] = params[2]; + params[7] = params[3]; + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_SET_DIO_IRQ_PARAMS, + params, 8); + + return ret; +} + +/* ── Send ────────────────────────────────────────────────────────────────── */ + +static int lr11xx_send(const struct device *dev, + uint8_t *data, uint32_t data_len) +{ + struct lr11xx_data *drv = dev->data; + int ret; + uint8_t params[3]; + + /* Clear pending IRQ flags */ + uint32_t clr = LR11XX_IRQ_ALL; + + params[0] = (clr >> 24) & 0xFF; + params[1] = (clr >> 16) & 0xFF; + params[2] = (clr >> 8) & 0xFF; + params[3] = (clr >> 0) & 0xFF; + lr11xx_hal_write_cmd(dev, LR11XX_CMD_CLEAR_IRQ_STATUS, params, 4); + + /* Write payload to TX buffer at offset 0 */ + uint8_t *tx_buf = k_malloc(data_len + 1); + + if (!tx_buf) { + return -ENOMEM; + } + tx_buf[0] = 0x00; /* buffer offset */ + memcpy(&tx_buf[1], data, data_len); + + /* Use opcode 0x010A (WriteBuffer8) to write payload */ + ret = lr11xx_hal_write_cmd(dev, 0x010Au, tx_buf, data_len + 1); + k_free(tx_buf); + if (ret < 0) { + return ret; + } + + /* Update packet params with actual payload length */ + uint8_t pkt_params[6]; + + pkt_params[0] = 0x00; + pkt_params[1] = 0x08; /* preamble length high byte */ + pkt_params[2] = 0x00; /* explicit header */ + pkt_params[3] = (uint8_t)data_len; + pkt_params[4] = 0x01; /* CRC on */ + pkt_params[5] = 0x00; /* standard IQ */ + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_SET_PACKET_PARAMS, + pkt_params, 6); + if (ret < 0) { + return ret; + } + + /* Reset the event semaphore and trigger TX */ + k_sem_reset(&drv->event_sem); + + params[0] = 0x00; /* TX timeout = 0 (no timeout) */ + params[1] = 0x00; + params[2] = 0x00; + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_SET_TX, params, 3); + if (ret < 0) { + return ret; + } + + /* Wait for TX_DONE IRQ */ + ret = k_sem_take(&drv->event_sem, K_MSEC(5000)); + if (ret < 0) { + LOG_WRN("TX timeout"); + return -ETIMEDOUT; + } + + /* Read and check IRQ status */ + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_GET_IRQ_STATUS, NULL, 0); + if (ret < 0) { + return ret; + } + + uint8_t irq_buf[4]; + + ret = lr11xx_hal_read_resp(dev, irq_buf, sizeof(irq_buf)); + if (ret < 0) { + return ret; + } + + uint32_t irq = ((uint32_t)irq_buf[0] << 24) | + ((uint32_t)irq_buf[1] << 16) | + ((uint32_t)irq_buf[2] << 8) | + ((uint32_t)irq_buf[3]); + + if (!(irq & LR11XX_IRQ_TX_DONE)) { + LOG_WRN("TX: unexpected IRQ 0x%08x", irq); + return -EIO; + } + + /* Clear IRQ flags */ + params[0] = (irq >> 24) & 0xFF; + params[1] = (irq >> 16) & 0xFF; + params[2] = (irq >> 8) & 0xFF; + params[3] = (irq >> 0) & 0xFF; + lr11xx_hal_write_cmd(dev, LR11XX_CMD_CLEAR_IRQ_STATUS, params, 4); + + return 0; +} + +/* ── Receive ─────────────────────────────────────────────────────────────── */ + +static int lr11xx_recv(const struct device *dev, + uint8_t *data, uint8_t size, + k_timeout_t timeout, + int16_t *rssi, int8_t *snr) +{ + struct lr11xx_data *drv = dev->data; + int ret; + uint8_t params[3]; + + /* Clear pending IRQs */ + uint32_t clr = LR11XX_IRQ_ALL; + + params[0] = (clr >> 24) & 0xFF; + params[1] = (clr >> 16) & 0xFF; + params[2] = (clr >> 8) & 0xFF; + params[3] = (clr >> 0) & 0xFF; + lr11xx_hal_write_cmd(dev, LR11XX_CMD_CLEAR_IRQ_STATUS, params, 4); + + /* Enter continuous RX */ + k_sem_reset(&drv->event_sem); + + params[0] = 0xFF; /* RX timeout: 0xFFFFFF = single with preamble timeout */ + params[1] = 0xFF; + params[2] = 0xFF; + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_SET_RX, params, 3); + if (ret < 0) { + return ret; + } + + /* Wait for RX_DONE (or timeout) */ + ret = k_sem_take(&drv->event_sem, timeout); + if (ret < 0) { + return -ETIMEDOUT; + } + + /* Read IRQ status */ + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_GET_IRQ_STATUS, NULL, 0); + if (ret < 0) { + return ret; + } + + uint8_t irq_buf[4]; + + ret = lr11xx_hal_read_resp(dev, irq_buf, sizeof(irq_buf)); + if (ret < 0) { + return ret; + } + + uint32_t irq = ((uint32_t)irq_buf[0] << 24) | + ((uint32_t)irq_buf[1] << 16) | + ((uint32_t)irq_buf[2] << 8) | + ((uint32_t)irq_buf[3]); + + /* Clear IRQ flags */ + params[0] = (irq >> 24) & 0xFF; + params[1] = (irq >> 16) & 0xFF; + params[2] = (irq >> 8) & 0xFF; + params[3] = (irq >> 0) & 0xFF; + lr11xx_hal_write_cmd(dev, LR11XX_CMD_CLEAR_IRQ_STATUS, params, 4); + + if (irq & (LR11XX_IRQ_TIMEOUT | LR11XX_IRQ_CRC_ERROR)) { + LOG_DBG("RX error IRQ=0x%08x", irq); + return -EIO; + } + + if (!(irq & LR11XX_IRQ_RX_DONE)) { + return -EIO; + } + + /* Get RX buffer status: payload_len, rx_start_ptr */ + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_GET_RX_BUFFER_STATUS, NULL, 0); + if (ret < 0) { + return ret; + } + + uint8_t rxbuf_status[2]; + + ret = lr11xx_hal_read_resp(dev, rxbuf_status, sizeof(rxbuf_status)); + if (ret < 0) { + return ret; + } + + uint8_t payload_len = rxbuf_status[0]; + uint8_t offset = rxbuf_status[1]; + + if (payload_len > size) { + payload_len = size; + } + + /* Read payload from RX buffer */ + uint8_t read_params[2] = { offset, payload_len }; + + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_READ_BUFFER, + read_params, sizeof(read_params)); + if (ret < 0) { + return ret; + } + + ret = lr11xx_hal_read_resp(dev, data, payload_len); + if (ret < 0) { + return ret; + } + + /* Get packet status (RSSI, SNR) */ + ret = lr11xx_hal_write_cmd(dev, LR11XX_CMD_GET_PACKET_STATUS, NULL, 0); + if (ret < 0) { + goto skip_status; + } + + uint8_t pkt_status[3]; + + ret = lr11xx_hal_read_resp(dev, pkt_status, sizeof(pkt_status)); + if (ret == 0) { + /* rssi_pkt = -pkt_status[0] / 2 dBm */ + if (rssi) { + *rssi = -(int16_t)pkt_status[0] / 2; + } + /* snr_pkt = pkt_status[1] / 4 dB (signed) */ + if (snr) { + *snr = (int8_t)pkt_status[1] / 4; + } + } + +skip_status: + return (int)payload_len; +} + +/* ── Initialization ──────────────────────────────────────────────────────── */ + +static int lr11xx_init(const struct device *dev) +{ + const struct lr11xx_config *cfg = dev->config; + struct lr11xx_data *drv = dev->data; + int ret; + + if (!spi_is_ready_dt(&cfg->spi)) { + LOG_ERR("SPI bus not ready"); + return -ENODEV; + } + + ret = gpio_pin_configure_dt(&cfg->reset, GPIO_OUTPUT_ACTIVE); + if (ret < 0) { + return ret; + } + + ret = gpio_pin_configure_dt(&cfg->busy, GPIO_INPUT); + if (ret < 0) { + return ret; + } + + ret = gpio_pin_configure_dt(&cfg->event, GPIO_INPUT); + if (ret < 0) { + return ret; + } + + k_sem_init(&drv->event_sem, 0, 1); + + /* Set up interrupt on EVENT/DIO9 pin */ + ret = gpio_pin_interrupt_configure_dt(&cfg->event, + GPIO_INT_EDGE_TO_ACTIVE); + if (ret < 0) { + return ret; + } + + gpio_init_callback(&drv->event_cb, lr11xx_hal_event_handler, + BIT(cfg->event.pin)); + ret = gpio_add_callback(cfg->event.port, &drv->event_cb); + if (ret < 0) { + return ret; + } + + /* Hardware reset */ + ret = lr11xx_hal_reset(dev); + if (ret < 0) { + LOG_ERR("Reset failed: %d", ret); + return ret; + } + + LOG_INF("LR11xx initialized"); + return 0; +} + +/* ── Driver API ──────────────────────────────────────────────────────────── */ + +static const struct lora_driver_api lr11xx_api = { + .config = lr11xx_configure, + .send = lr11xx_send, + .recv = lr11xx_recv, +}; + +/* ── Device instantiation macro ─────────────────────────────────────────── */ + +#define LR11XX_DEFINE(n) \ + static struct lr11xx_data lr11xx_data_##n; \ + \ + static const struct lr11xx_config lr11xx_config_##n = { \ + .spi = SPI_DT_SPEC_INST_GET(n, \ + SPI_WORD_SET(8) | SPI_TRANSFER_MSB, 0), \ + .reset = GPIO_DT_SPEC_INST_GET(n, reset_gpios), \ + .busy = GPIO_DT_SPEC_INST_GET(n, busy_gpios), \ + .event = GPIO_DT_SPEC_INST_GET(n, event_gpios), \ + }; \ + \ + DEVICE_DT_INST_DEFINE(n, \ + lr11xx_init, NULL, \ + &lr11xx_data_##n, &lr11xx_config_##n, \ + POST_KERNEL, CONFIG_LORA_INIT_PRIORITY, \ + &lr11xx_api); + +DT_INST_FOREACH_STATUS_OKAY(LR11XX_DEFINE) diff --git a/drivers/lora/lr11xx/lr11xx.h b/drivers/lora/lr11xx/lr11xx.h new file mode 100644 index 0000000..483e705 --- /dev/null +++ b/drivers/lora/lr11xx/lr11xx.h @@ -0,0 +1,107 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Semtech LR11xx private driver header. + * Defines SPI opcodes, register constants, and internal types. + */ + +#ifndef LR11XX_H +#define LR11XX_H + +#include +#include +#include +#include +#include + +/* ── SPI opcode definitions ──────────────────────────────────────────────── */ + +/* System commands */ +#define LR11XX_CMD_RESET_STATS 0x0200u +#define LR11XX_CMD_GET_STATUS 0x0100u +#define LR11XX_CMD_SET_STANDBY 0x011Bu /* Note: opcode varies by fw */ +#define LR11XX_CMD_SET_PACKET_TYPE 0x010Au +#define LR11XX_CMD_SET_RF_FREQUENCY 0x010Eu +#define LR11XX_CMD_SET_MODULATION_PARAMS 0x0115u +#define LR11XX_CMD_SET_PACKET_PARAMS 0x0116u +#define LR11XX_CMD_SET_TX_PARAMS 0x0110u +#define LR11XX_CMD_SET_DIO_IRQ_PARAMS 0x0113u +#define LR11XX_CMD_CLEAR_IRQ_STATUS 0x0114u +#define LR11XX_CMD_GET_IRQ_STATUS 0x0112u +#define LR11XX_CMD_SET_RX 0x0107u +#define LR11XX_CMD_SET_TX 0x0108u +#define LR11XX_CMD_GET_RX_BUFFER_STATUS 0x011Cu +#define LR11XX_CMD_READ_BUFFER 0x011Eu +#define LR11XX_CMD_GET_PACKET_STATUS 0x011Du +#define LR11XX_CMD_SET_BUFFER_BASE_ADDR 0x010Fu + +/* Packet type: LoRa = 0x01 */ +#define LR11XX_PKT_TYPE_LORA 0x01u + +/* Standby mode: RC oscillator = 0x00, XOSC = 0x01 */ +#define LR11XX_STANDBY_RC 0x00u +#define LR11XX_STANDBY_XOSC 0x01u + +/* IRQ bitmasks */ +#define LR11XX_IRQ_TX_DONE BIT(2) +#define LR11XX_IRQ_RX_DONE BIT(3) +#define LR11XX_IRQ_TIMEOUT BIT(8) +#define LR11XX_IRQ_CRC_ERROR BIT(10) +#define LR11XX_IRQ_ALL 0xFFFFFFFFu + +/* DIO mask for routing IRQs to EVENT/DIO9 */ +#define LR11XX_DIO9_MASK (LR11XX_IRQ_TX_DONE | LR11XX_IRQ_RX_DONE | \ + LR11XX_IRQ_TIMEOUT | LR11XX_IRQ_CRC_ERROR) + +/* LoRa spreading factor codes */ +#define LR11XX_LORA_SF5 0x05u +#define LR11XX_LORA_SF6 0x06u +#define LR11XX_LORA_SF7 0x07u +#define LR11XX_LORA_SF8 0x08u +#define LR11XX_LORA_SF9 0x09u +#define LR11XX_LORA_SF10 0x0Au +#define LR11XX_LORA_SF11 0x0Bu +#define LR11XX_LORA_SF12 0x0Cu + +/* LoRa bandwidth codes */ +#define LR11XX_LORA_BW_125 0x04u +#define LR11XX_LORA_BW_250 0x05u +#define LR11XX_LORA_BW_500 0x06u + +/* LoRa coding rate codes */ +#define LR11XX_LORA_CR_4_5 0x01u +#define LR11XX_LORA_CR_4_6 0x02u +#define LR11XX_LORA_CR_4_7 0x03u +#define LR11XX_LORA_CR_4_8 0x04u + +/* RX timeout: 0 = continuous, 0xFFFFFF = single (timeout on preamble) */ +#define LR11XX_RX_CONTINUOUS 0x000000u + +/* ── Driver config/data structures ───────────────────────────────────────── */ + +struct lr11xx_config { + struct spi_dt_spec spi; + struct gpio_dt_spec reset; + struct gpio_dt_spec busy; + struct gpio_dt_spec event; +}; + +struct lr11xx_data { + struct gpio_callback event_cb; + struct k_sem event_sem; + uint32_t last_irq; +}; + +/* ── HAL function prototypes (implemented in lr11xx_hal.c) ───────────────── */ + +int lr11xx_hal_reset(const struct device *dev); +int lr11xx_hal_wait_busy(const struct device *dev, k_timeout_t timeout); +int lr11xx_hal_write_cmd(const struct device *dev, + uint16_t opcode, + const uint8_t *params, size_t params_len); +int lr11xx_hal_read_resp(const struct device *dev, + uint8_t *buf, size_t len); +void lr11xx_hal_event_handler(const struct device *port, + struct gpio_callback *cb, uint32_t pins); + +#endif /* LR11XX_H */ diff --git a/drivers/lora/lr11xx/lr11xx_hal.c b/drivers/lora/lr11xx/lr11xx_hal.c new file mode 100644 index 0000000..1c1cac6 --- /dev/null +++ b/drivers/lora/lr11xx/lr11xx_hal.c @@ -0,0 +1,129 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * LR11xx Zephyr HAL — SPI and GPIO low-level operations. + * + * The LR11xx SPI protocol: + * WRITE: Assert NSS → send 2-byte opcode + param bytes → deassert NSS + * READ: Assert NSS → send 2-byte opcode → deassert NSS + * Wait for BUSY low + * Assert NSS → read STAT1 + STAT2 + N response bytes → deassert NSS + * + * All transactions must wait for BUSY=low before asserting NSS. + */ + +#include "lr11xx.h" + +#include +#include +#include +#include + +LOG_MODULE_DECLARE(lr11xx, LOG_LEVEL_DBG); + +#define BUSY_POLL_INTERVAL_US 100 +#define BUSY_TIMEOUT_MS 1000 + +int lr11xx_hal_reset(const struct device *dev) +{ + const struct lr11xx_config *cfg = dev->config; + int ret; + + ret = gpio_pin_set_dt(&cfg->reset, 1); + if (ret < 0) { + return ret; + } + k_sleep(K_USEC(200)); + ret = gpio_pin_set_dt(&cfg->reset, 0); + if (ret < 0) { + return ret; + } + k_sleep(K_MSEC(10)); + + return lr11xx_hal_wait_busy(dev, K_MSEC(BUSY_TIMEOUT_MS)); +} + +int lr11xx_hal_wait_busy(const struct device *dev, k_timeout_t timeout) +{ + const struct lr11xx_config *cfg = dev->config; + uint32_t start = k_uptime_get_32(); + uint32_t timeout_ms = k_ticks_to_ms_floor32(timeout.ticks); + + while (gpio_pin_get_dt(&cfg->busy)) { + if (timeout_ms > 0 && + (k_uptime_get_32() - start) > timeout_ms) { + return -ETIMEDOUT; + } + k_busy_wait(BUSY_POLL_INTERVAL_US); + } + return 0; +} + +int lr11xx_hal_write_cmd(const struct device *dev, + uint16_t opcode, + const uint8_t *params, size_t params_len) +{ + const struct lr11xx_config *cfg = dev->config; + int ret; + + ret = lr11xx_hal_wait_busy(dev, K_MSEC(BUSY_TIMEOUT_MS)); + if (ret < 0) { + return ret; + } + + uint8_t opcode_buf[2] = { + (uint8_t)(opcode >> 8), + (uint8_t)(opcode & 0xFF), + }; + + const struct spi_buf tx_bufs[] = { + { .buf = opcode_buf, .len = sizeof(opcode_buf) }, + { .buf = (void *)params, .len = params_len }, + }; + const struct spi_buf_set tx = { + .buffers = tx_bufs, + .count = (params_len > 0) ? 2 : 1, + }; + + return spi_write_dt(&cfg->spi, &tx); +} + +int lr11xx_hal_read_resp(const struct device *dev, + uint8_t *buf, size_t len) +{ + const struct lr11xx_config *cfg = dev->config; + int ret; + + ret = lr11xx_hal_wait_busy(dev, K_MSEC(BUSY_TIMEOUT_MS)); + if (ret < 0) { + return ret; + } + + /* First two bytes are STAT1 and STAT2 — discard for now */ + uint8_t status[2]; + uint8_t dummy_opcode[2] = {0x00, 0x00}; + + const struct spi_buf tx_bufs[] = { + { .buf = dummy_opcode, .len = sizeof(dummy_opcode) }, + }; + struct spi_buf rx_bufs[] = { + { .buf = status, .len = sizeof(status) }, + { .buf = buf, .len = len }, + }; + const struct spi_buf_set tx = { .buffers = tx_bufs, .count = 1 }; + const struct spi_buf_set rx = { .buffers = rx_bufs, .count = (len > 0) ? 2 : 1 }; + + return spi_transceive_dt(&cfg->spi, &tx, &rx); +} + +void lr11xx_hal_event_handler(const struct device *port, + struct gpio_callback *cb, uint32_t pins) +{ + ARG_UNUSED(port); + ARG_UNUSED(pins); + + struct lr11xx_data *data = + CONTAINER_OF(cb, struct lr11xx_data, event_cb); + + k_sem_give(&data->event_sem); +} diff --git a/dts/bindings/lora/semtech,lr11xx.yaml b/dts/bindings/lora/semtech,lr11xx.yaml new file mode 100644 index 0000000..de4b4f4 --- /dev/null +++ b/dts/bindings/lora/semtech,lr11xx.yaml @@ -0,0 +1,46 @@ +# Copyright (c) 2024 loramodem contributors +# SPDX-License-Identifier: Apache-2.0 + +description: | + Semtech LR1110/LR1120 multi-protocol radio transceiver. + + This driver implements Zephyr's LoRa subsystem API for LoRa packet mode + only. LoRaWAN and GNSS capabilities of the LR11xx are not used. + + The LR11xx uses a 2-byte SPI opcode protocol that differs from the + SX126x family: commands consist of a 2-byte opcode followed by + parameters. Responses are read in a separate transaction after + asserting NSS, beginning with STAT1 and STAT2 status bytes. + +compatible: "semtech,lr11xx" + +include: spi-device.yaml + +properties: + reset-gpios: + type: phandle-array + required: true + description: | + GPIO connected to NRESET (active low). Drive low for >= 100 us + to reset the chip. + + busy-gpios: + type: phandle-array + required: true + description: | + GPIO connected to the BUSY pin (active high). + The host must wait for BUSY to go low before starting a new + SPI transaction. + + event-gpios: + type: phandle-array + required: true + description: | + GPIO connected to the DIO9 / EVENT pin (active high). + Used for RX packet-ready and TX-done interrupts. + + lf-clk-gpios: + type: phandle-array + description: | + Optional: GPIO to enable/disable the LF clock output from LR11xx. + Some board designs use this to drive an external 32.768 kHz crystal. diff --git a/west.yml b/west.yml new file mode 100644 index 0000000..d8fe1b6 --- /dev/null +++ b/west.yml @@ -0,0 +1,18 @@ +manifest: + self: + path: loramodem + + remotes: + - name: zephyrproject-rtos + url-base: https://github.com/zephyrproject-rtos + + projects: + - name: zephyr + remote: zephyrproject-rtos + revision: v3.7.0 + import: + name-allowlist: + - cmsis + - hal_nordic + - hal_espressif + - loramac-node diff --git a/zephyr/module.yml b/zephyr/module.yml new file mode 100644 index 0000000..ea6ec83 --- /dev/null +++ b/zephyr/module.yml @@ -0,0 +1,9 @@ +name: loramodem + +build: + cmake: . + kconfig: Kconfig + + settings: + board_root: . + dts_root: .