375 lines
9.2 KiB
C
375 lines
9.2 KiB
C
/*
|
|
* Copyright (c) 2019 Manivannan Sadhasivam
|
|
* Copyright (c) 2020 Grinn
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <zephyr/drivers/gpio.h>
|
|
#include <zephyr/drivers/lora.h>
|
|
#include <zephyr/logging/log.h>
|
|
#include <zephyr/sys/atomic.h>
|
|
#include <zephyr/kernel.h>
|
|
|
|
/* LoRaMac-node specific includes */
|
|
#include <radio.h>
|
|
|
|
#include "sx12xx_common.h"
|
|
|
|
#define STATE_FREE 0
|
|
#define STATE_BUSY 1
|
|
#define STATE_CLEANUP 2
|
|
|
|
LOG_MODULE_REGISTER(sx12xx_common, CONFIG_LORA_LOG_LEVEL);
|
|
|
|
struct sx12xx_rx_params {
|
|
uint8_t *buf;
|
|
uint8_t *size;
|
|
int16_t *rssi;
|
|
int8_t *snr;
|
|
};
|
|
|
|
static struct sx12xx_data {
|
|
const struct device *dev;
|
|
struct k_poll_signal *operation_done;
|
|
lora_recv_cb async_rx_cb;
|
|
RadioEvents_t events;
|
|
struct lora_modem_config tx_cfg;
|
|
atomic_t modem_usage;
|
|
struct sx12xx_rx_params rx_params;
|
|
} dev_data;
|
|
|
|
int __sx12xx_configure_pin(const struct gpio_dt_spec *gpio, gpio_flags_t flags)
|
|
{
|
|
int err;
|
|
|
|
if (!device_is_ready(gpio->port)) {
|
|
LOG_ERR("GPIO device not ready %s", gpio->port->name);
|
|
return -ENODEV;
|
|
}
|
|
|
|
err = gpio_pin_configure_dt(gpio, flags);
|
|
if (err) {
|
|
LOG_ERR("Cannot configure gpio %s %d: %d", gpio->port->name,
|
|
gpio->pin, err);
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Attempt to acquire the modem for operations
|
|
*
|
|
* @param data common sx12xx data struct
|
|
*
|
|
* @retval true if modem was acquired
|
|
* @retval false otherwise
|
|
*/
|
|
static inline bool modem_acquire(struct sx12xx_data *data)
|
|
{
|
|
return atomic_cas(&data->modem_usage, STATE_FREE, STATE_BUSY);
|
|
}
|
|
|
|
/**
|
|
* @brief Safely release the modem from any context
|
|
*
|
|
* This function can be called from any context and guarantees that the
|
|
* release operations will only be run once.
|
|
*
|
|
* @param data common sx12xx data struct
|
|
*
|
|
* @retval true if modem was released by this function
|
|
* @retval false otherwise
|
|
*/
|
|
static bool modem_release(struct sx12xx_data *data)
|
|
{
|
|
/* Increment atomic so both acquire and release will fail */
|
|
if (!atomic_cas(&data->modem_usage, STATE_BUSY, STATE_CLEANUP)) {
|
|
return false;
|
|
}
|
|
/* Put radio back into sleep mode */
|
|
Radio.Sleep();
|
|
/* Completely release modem */
|
|
data->operation_done = NULL;
|
|
atomic_clear(&data->modem_usage);
|
|
return true;
|
|
}
|
|
|
|
static void sx12xx_ev_rx_done(uint8_t *payload, uint16_t size, int16_t rssi,
|
|
int8_t snr)
|
|
{
|
|
struct k_poll_signal *sig = dev_data.operation_done;
|
|
|
|
/* Receiving in asynchronous mode */
|
|
if (dev_data.async_rx_cb) {
|
|
/* Start receiving again */
|
|
Radio.Rx(0);
|
|
/* Run the callback */
|
|
dev_data.async_rx_cb(dev_data.dev, payload, size, rssi, snr);
|
|
/* Don't run the synchronous code */
|
|
return;
|
|
}
|
|
|
|
/* Manually release the modem instead of just calling modem_release
|
|
* as we need to perform cleanup operations while still ensuring
|
|
* others can't use the modem.
|
|
*/
|
|
if (!atomic_cas(&dev_data.modem_usage, STATE_BUSY, STATE_CLEANUP)) {
|
|
return;
|
|
}
|
|
/* We can make two observations here:
|
|
* 1. lora_recv hasn't already exited due to a timeout.
|
|
* (modem_release would have been successfully called)
|
|
* 2. If the k_poll in lora_recv times out before we raise the signal,
|
|
* but while this code is running, it will block on the
|
|
* signal again.
|
|
* This lets us guarantee that the operation_done signal and pointers
|
|
* in rx_params are always valid in this function.
|
|
*/
|
|
|
|
/* Store actual size */
|
|
if (size < *dev_data.rx_params.size) {
|
|
*dev_data.rx_params.size = size;
|
|
}
|
|
/* Copy received data to output buffer */
|
|
memcpy(dev_data.rx_params.buf, payload,
|
|
*dev_data.rx_params.size);
|
|
/* Output RSSI and SNR */
|
|
if (dev_data.rx_params.rssi) {
|
|
*dev_data.rx_params.rssi = rssi;
|
|
}
|
|
if (dev_data.rx_params.snr) {
|
|
*dev_data.rx_params.snr = snr;
|
|
}
|
|
/* Put radio back into sleep mode */
|
|
Radio.Sleep();
|
|
/* Completely release modem */
|
|
dev_data.operation_done = NULL;
|
|
atomic_clear(&dev_data.modem_usage);
|
|
/* Notify caller RX is complete */
|
|
k_poll_signal_raise(sig, 0);
|
|
}
|
|
|
|
static void sx12xx_ev_tx_done(void)
|
|
{
|
|
struct k_poll_signal *sig = dev_data.operation_done;
|
|
|
|
if (modem_release(&dev_data)) {
|
|
/* Raise signal if provided */
|
|
if (sig) {
|
|
k_poll_signal_raise(sig, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void sx12xx_ev_tx_timed_out(void)
|
|
{
|
|
/* Just release the modem */
|
|
modem_release(&dev_data);
|
|
}
|
|
|
|
int sx12xx_lora_send(const struct device *dev, uint8_t *data,
|
|
uint32_t data_len)
|
|
{
|
|
struct k_poll_signal done = K_POLL_SIGNAL_INITIALIZER(done);
|
|
struct k_poll_event evt = K_POLL_EVENT_INITIALIZER(
|
|
K_POLL_TYPE_SIGNAL,
|
|
K_POLL_MODE_NOTIFY_ONLY,
|
|
&done);
|
|
uint32_t air_time;
|
|
int ret;
|
|
|
|
/* Validate that we have a TX configuration */
|
|
if (!dev_data.tx_cfg.frequency) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
ret = sx12xx_lora_send_async(dev, data, data_len, &done);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
/* Calculate expected airtime of the packet */
|
|
air_time = Radio.TimeOnAir(MODEM_LORA,
|
|
dev_data.tx_cfg.bandwidth,
|
|
dev_data.tx_cfg.datarate,
|
|
dev_data.tx_cfg.coding_rate,
|
|
dev_data.tx_cfg.preamble_len,
|
|
0, data_len, true);
|
|
LOG_DBG("Expected air time of %d bytes = %dms", data_len, air_time);
|
|
|
|
/* Wait for the packet to finish transmitting.
|
|
* Use twice the tx duration to ensure that we are actually detecting
|
|
* a failed transmission, and not some minor timing variation between
|
|
* modem and driver.
|
|
*/
|
|
ret = k_poll(&evt, 1, K_MSEC(2 * air_time));
|
|
if (ret < 0) {
|
|
LOG_ERR("Packet transmission failed!");
|
|
if (!modem_release(&dev_data)) {
|
|
/* TX done interrupt is currently running */
|
|
k_poll(&evt, 1, K_FOREVER);
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
int sx12xx_lora_send_async(const struct device *dev, uint8_t *data,
|
|
uint32_t data_len, struct k_poll_signal *async)
|
|
{
|
|
/* Ensure available, freed by sx12xx_ev_tx_done */
|
|
if (!modem_acquire(&dev_data)) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
/* Store signal */
|
|
dev_data.operation_done = async;
|
|
|
|
Radio.SetMaxPayloadLength(MODEM_LORA, data_len);
|
|
|
|
Radio.Send(data, data_len);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int sx12xx_lora_recv(const struct device *dev, uint8_t *data, uint8_t size,
|
|
k_timeout_t timeout, int16_t *rssi, int8_t *snr)
|
|
{
|
|
struct k_poll_signal done = K_POLL_SIGNAL_INITIALIZER(done);
|
|
struct k_poll_event evt = K_POLL_EVENT_INITIALIZER(
|
|
K_POLL_TYPE_SIGNAL,
|
|
K_POLL_MODE_NOTIFY_ONLY,
|
|
&done);
|
|
int ret;
|
|
|
|
/* Ensure available, decremented by sx12xx_ev_rx_done or on timeout */
|
|
if (!modem_acquire(&dev_data)) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
dev_data.async_rx_cb = NULL;
|
|
/* Store operation signal */
|
|
dev_data.operation_done = &done;
|
|
/* Set data output location */
|
|
dev_data.rx_params.buf = data;
|
|
dev_data.rx_params.size = &size;
|
|
dev_data.rx_params.rssi = rssi;
|
|
dev_data.rx_params.snr = snr;
|
|
|
|
Radio.SetMaxPayloadLength(MODEM_LORA, 255);
|
|
Radio.Rx(0);
|
|
|
|
ret = k_poll(&evt, 1, timeout);
|
|
if (ret < 0) {
|
|
if (!modem_release(&dev_data)) {
|
|
/* Releasing the modem failed, which means that
|
|
* the RX callback is currently running. Wait until
|
|
* the RX callback finishes and we get our packet.
|
|
*/
|
|
k_poll(&evt, 1, K_FOREVER);
|
|
|
|
/* We did receive a packet */
|
|
return size;
|
|
}
|
|
LOG_INF("Receive timeout");
|
|
return ret;
|
|
}
|
|
|
|
return size;
|
|
}
|
|
|
|
int sx12xx_lora_recv_async(const struct device *dev, lora_recv_cb cb)
|
|
{
|
|
/* Cancel ongoing reception */
|
|
if (cb == NULL) {
|
|
if (!modem_release(&dev_data)) {
|
|
/* Not receiving or already being stopped */
|
|
return -EINVAL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Ensure available */
|
|
if (!modem_acquire(&dev_data)) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
/* Store parameters */
|
|
dev_data.async_rx_cb = cb;
|
|
|
|
/* Start reception */
|
|
Radio.SetMaxPayloadLength(MODEM_LORA, 255);
|
|
Radio.Rx(0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
int sx12xx_lora_config(const struct device *dev,
|
|
struct lora_modem_config *config)
|
|
{
|
|
/* Ensure available, decremented after configuration */
|
|
if (!modem_acquire(&dev_data)) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
Radio.SetChannel(config->frequency);
|
|
|
|
if (config->tx) {
|
|
/* Store TX config locally for airtime calculations */
|
|
memcpy(&dev_data.tx_cfg, config, sizeof(dev_data.tx_cfg));
|
|
/* Configure radio driver */
|
|
Radio.SetTxConfig(MODEM_LORA, config->tx_power, 0,
|
|
config->bandwidth, config->datarate,
|
|
config->coding_rate, config->preamble_len,
|
|
false, true, 0, 0, config->iq_inverted, 4000);
|
|
} else {
|
|
/* TODO: Get symbol timeout value from config parameters */
|
|
Radio.SetRxConfig(MODEM_LORA, config->bandwidth,
|
|
config->datarate, config->coding_rate,
|
|
0, config->preamble_len, 10, false, 0,
|
|
false, 0, 0, config->iq_inverted, true);
|
|
}
|
|
|
|
Radio.SetPublicNetwork(config->public_network);
|
|
|
|
modem_release(&dev_data);
|
|
return 0;
|
|
}
|
|
|
|
int sx12xx_lora_test_cw(const struct device *dev, uint32_t frequency,
|
|
int8_t tx_power,
|
|
uint16_t duration)
|
|
{
|
|
/* Ensure available, freed in sx12xx_ev_tx_done */
|
|
if (!modem_acquire(&dev_data)) {
|
|
return -EBUSY;
|
|
}
|
|
|
|
Radio.SetTxContinuousWave(frequency, tx_power, duration);
|
|
return 0;
|
|
}
|
|
|
|
int sx12xx_init(const struct device *dev)
|
|
{
|
|
atomic_set(&dev_data.modem_usage, 0);
|
|
|
|
dev_data.dev = dev;
|
|
dev_data.events.TxDone = sx12xx_ev_tx_done;
|
|
dev_data.events.RxDone = sx12xx_ev_rx_done;
|
|
/* TX timeout event raises at the end of the test CW transmission */
|
|
dev_data.events.TxTimeout = sx12xx_ev_tx_timed_out;
|
|
Radio.Init(&dev_data.events);
|
|
|
|
/*
|
|
* Automatically place the radio into sleep mode upon boot.
|
|
* The required `lora_config` call before transmission or reception
|
|
* will bring the radio out of sleep mode before it is used. The radio
|
|
* is automatically placed back into sleep mode upon TX or RX
|
|
* completion.
|
|
*/
|
|
Radio.Sleep();
|
|
|
|
return 0;
|
|
}
|