| /* |
| * 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; |
| } |