Initial import
This commit is contained in:
25
app/CMakeLists.txt
Normal file
25
app/CMakeLists.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
cmake_minimum_required(VERSION 3.20.0)
|
||||
|
||||
# Set BOARD_ROOT and DTS_ROOT to the repo root so that boards/<vendor>/<board>/
|
||||
# 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)
|
||||
71
app/Kconfig
Normal file
71
app/Kconfig
Normal file
@@ -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
|
||||
28
app/prj.conf
Normal file
28
app/prj.conf
Normal file
@@ -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
|
||||
109
app/src/kiss.c
Normal file
109
app/src/kiss.c
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* KISS protocol encoder and decoder implementation.
|
||||
*/
|
||||
|
||||
#include "kiss.h"
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
125
app/src/kiss.h
Normal file
125
app/src/kiss.h
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* KISS (Keep It Simple Stupid) protocol encoder/decoder.
|
||||
*
|
||||
* Frame format:
|
||||
* FEND | CMD | <data with escaping> | FEND
|
||||
*
|
||||
* Escape sequences (inside frame data only):
|
||||
* 0xC0 → 0xDB 0xDC (FESC TFEND)
|
||||
* 0xDB → 0xDB 0xDD (FESC TFESC)
|
||||
*/
|
||||
#ifndef KISS_H
|
||||
#define KISS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* 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 */
|
||||
218
app/src/lora_modem.c
Normal file
218
app/src/lora_modem.c
Normal file
@@ -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 <zephyr/drivers/lora.h>
|
||||
#include <zephyr/devicetree.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
92
app/src/lora_modem.h
Normal file
92
app/src/lora_modem.h
Normal file
@@ -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 <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <zephyr/kernel.h>
|
||||
|
||||
/**
|
||||
* @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 */
|
||||
362
app/src/main.c
Normal file
362
app/src/main.c
Normal file
@@ -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 | <sub-cmd> [params...] | FEND
|
||||
* Device → host: FEND | 0x06 | <sub-cmd> [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 <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user