mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 10:55:06 +00:00
842 lines
27 KiB
C++
842 lines
27 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
#include "cc1101_radio.h"
|
|
|
|
#include "cc1101_radiolib_hal.h"
|
|
|
|
#include <tactility/concurrent/event_group.h>
|
|
#include <tactility/delay.h>
|
|
#include <tactility/drivers/gpio_controller.h>
|
|
#include <tactility/log.h>
|
|
|
|
#include <cmath>
|
|
#include <initializer_list>
|
|
|
|
#include <RadioLib.h>
|
|
|
|
#include <esp_random.h>
|
|
|
|
#define TAG "cc1101"
|
|
|
|
namespace {
|
|
|
|
// TX-done wait is derived from the packet's time-on-air. The margin covers calibration,
|
|
// PA ramp and polling latency.
|
|
constexpr auto CC1101_TX_TIMEOUT_MARGIN_MILLIS = 500;
|
|
constexpr auto CC1101_TX_POLL_INTERVAL_MILLIS = 1;
|
|
constexpr uint32_t CC1101_INTERRUPT_BIT = (1 << 0);
|
|
constexpr uint32_t CC1101_GDO0_EVENT_BIT = (1 << 1);
|
|
constexpr uint32_t CC1101_QUEUED_TX_BIT = (1 << 2);
|
|
constexpr uint32_t CC1101_REQUEST_BIT = (1 << 3);
|
|
constexpr uint32_t CC1101_RESPONSE_BIT = (1 << 0);
|
|
constexpr auto CC1101_REQUEST_TIMEOUT_MILLIS = 1000;
|
|
constexpr auto CC1101_LBT_MAX_ATTEMPTS = 5;
|
|
constexpr auto CC1101_LBT_BACKOFF_MIN_MILLIS = 20;
|
|
constexpr auto CC1101_LBT_BACKOFF_MAX_MILLIS = 200;
|
|
// PKTSTATUS bit 3: sync word received, cleared at the end of the packet
|
|
constexpr uint8_t CC1101_PKTSTATUS_SFD = (1 << 3);
|
|
constexpr size_t CC1101_THREAD_STACK_SIZE = 8192;
|
|
// Sync word (16 bits) plus the length byte and CRC (16 bits)
|
|
constexpr uint32_t CC1101_PACKET_OVERHEAD_BITS = 16 + 8 + 16;
|
|
|
|
const char* toString(enum RadioState state) {
|
|
switch (state) {
|
|
case RADIO_STATE_OFF:
|
|
return "off";
|
|
case RADIO_STATE_ON_PENDING:
|
|
return "on-pending";
|
|
case RADIO_STATE_ON:
|
|
return "on";
|
|
case RADIO_STATE_OFF_PENDING:
|
|
return "off-pending";
|
|
case RADIO_STATE_ERROR:
|
|
return "error";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
const char* toString(enum RadioModulation modulation) {
|
|
switch (modulation) {
|
|
case RADIO_MODULATION_NONE:
|
|
return "none";
|
|
case RADIO_MODULATION_FSK:
|
|
return "FSK";
|
|
case RADIO_MODULATION_FSK4:
|
|
return "4-FSK";
|
|
case RADIO_MODULATION_OOK:
|
|
return "OOK";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
constexpr error_t checkLimitsAndApply(T& target, const int32_t value, const int32_t lower, const int32_t upper) {
|
|
if ((value >= lower) && (value <= upper)) {
|
|
target = static_cast<T>(value);
|
|
return ERROR_NONE;
|
|
}
|
|
return ERROR_OUT_OF_RANGE;
|
|
}
|
|
|
|
template<typename T>
|
|
constexpr error_t checkValuesAndApply(T& target, const int32_t value, std::initializer_list<int32_t> valids) {
|
|
for (int32_t valid : valids) {
|
|
if (value == valid) {
|
|
target = static_cast<T>(value);
|
|
return ERROR_NONE;
|
|
}
|
|
}
|
|
return ERROR_OUT_OF_RANGE;
|
|
}
|
|
|
|
bool isInLowBand(int32_t frequency) { return (frequency >= 300000000) && (frequency <= 348000000); }
|
|
bool isInMidBand(int32_t frequency) { return (frequency >= 387000000) && (frequency <= 464000000); }
|
|
bool isInHighBand(int32_t frequency) { return (frequency >= 779000000) && (frequency <= 928000000); }
|
|
|
|
/** Exposes the protected register access that RadioLib keeps behind RADIOLIB_LOW_LEVEL */
|
|
class Cc1101Chip final : public CC1101 {
|
|
public:
|
|
explicit Cc1101Chip(::Module* module) : CC1101(module) {}
|
|
|
|
uint8_t getMarcState() {
|
|
return static_cast<uint8_t>(SPIgetRegValue(RADIOLIB_CC1101_REG_MARCSTATE, 4, 0));
|
|
}
|
|
|
|
/** Instantaneous RSSI in dBm, valid while in RX */
|
|
float getInstantRssi() {
|
|
const auto raw = static_cast<uint8_t>(SPIgetRegValue(RADIOLIB_CC1101_REG_RSSI));
|
|
const float half = (raw >= 128) ? (static_cast<float>(raw) - 256.0f) / 2.0f : static_cast<float>(raw) / 2.0f;
|
|
return half - 74.0f;
|
|
}
|
|
|
|
uint8_t getPacketStatus() {
|
|
return static_cast<uint8_t>(SPIgetRegValue(RADIOLIB_CC1101_REG_PKTSTATUS));
|
|
}
|
|
|
|
/** Read the VERSION register without running begin() */
|
|
int16_t readVersion() {
|
|
auto* module = getMod();
|
|
module->spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_READ] = RADIOLIB_CC1101_CMD_READ;
|
|
module->spiConfig.cmds[RADIOLIB_MODULE_SPI_COMMAND_WRITE] = RADIOLIB_CC1101_CMD_WRITE;
|
|
module->init();
|
|
const int16_t version = getChipVersion();
|
|
module->term();
|
|
return version;
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
static bool isKnownVersion(int16_t version) {
|
|
return (version == RADIOLIB_CC1101_VERSION_CURRENT) ||
|
|
(version == RADIOLIB_CC1101_VERSION_LEGACY) ||
|
|
(version == RADIOLIB_CC1101_VERSION_CLONE);
|
|
}
|
|
|
|
struct Cc1101Radio::RadioParts {
|
|
Cc1101RadiolibHal hal;
|
|
::Module radioModule;
|
|
Cc1101Chip radio;
|
|
|
|
explicit RadioParts(const Settings& settings)
|
|
: hal(settings.spi_host, settings.spi_frequency_hz, settings.spi_controller)
|
|
, radioModule(&hal, settings.pin_cs, settings.pin_gdo0, RADIOLIB_NC, RADIOLIB_NC)
|
|
, radio(&radioModule) {}
|
|
};
|
|
|
|
Cc1101Radio::Cc1101Radio(const Settings& settings)
|
|
: settings(settings) {
|
|
recursive_mutex_construct(&mutex);
|
|
mutex_construct(&requestMutex);
|
|
event_group_construct(&events);
|
|
event_group_construct(&responses);
|
|
parts = new RadioParts(settings);
|
|
}
|
|
|
|
Cc1101Radio::~Cc1101Radio() {
|
|
setEnabled(false);
|
|
delete parts;
|
|
event_group_destruct(&responses);
|
|
event_group_destruct(&events);
|
|
mutex_destruct(&requestMutex);
|
|
recursive_mutex_destruct(&mutex);
|
|
}
|
|
|
|
error_t Cc1101Radio::probe() {
|
|
const int16_t version = parts->radio.readVersion();
|
|
if (!isKnownVersion(version)) {
|
|
LOG_E(TAG, "Probe failed: unexpected VERSION register value 0x%02X (module absent or unpowered?)", (unsigned)version);
|
|
return ERROR_RESOURCE;
|
|
}
|
|
|
|
LOG_I(TAG, "Probe OK: CC1101 version 0x%02X", (unsigned)version);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
// region Thread lifecycle
|
|
|
|
void Cc1101Radio::gdo0Isr(void* context) {
|
|
auto* self = static_cast<Cc1101Radio*>(context);
|
|
// GDO0 is armed as a HIGH_LEVEL interrupt (edge types are unreliable on the
|
|
// ESP32 per erratum 3.11). A level interrupt re-fires for as long as the line
|
|
// is asserted, so mask it here and let the radio thread re-arm it after the
|
|
// next startReceive().
|
|
gpio_descriptor_disable_interrupt(self->settings.gdo0);
|
|
event_group_set(self->events, CC1101_GDO0_EVENT_BIT);
|
|
}
|
|
|
|
int32_t Cc1101Radio::threadMainStatic(void* context) {
|
|
return static_cast<Cc1101Radio*>(context)->threadMain();
|
|
}
|
|
|
|
bool Cc1101Radio::isThreadInterrupted() const {
|
|
lock();
|
|
const bool interrupted = threadInterrupted;
|
|
unlock();
|
|
return interrupted;
|
|
}
|
|
|
|
int32_t Cc1101Radio::threadMain() {
|
|
int rc = doBegin(getModulation());
|
|
if (rc != 0) {
|
|
return rc;
|
|
}
|
|
setState(RADIO_STATE_ON);
|
|
|
|
while (!isThreadInterrupted()) {
|
|
const bool hasRx = doListen();
|
|
|
|
if (isThreadInterrupted()) {
|
|
break;
|
|
}
|
|
|
|
// Service a received packet before transmitting: an RX-done and a queued TX
|
|
// can coincide in the same iteration.
|
|
if (hasRx) {
|
|
doReceive();
|
|
}
|
|
if (getTxQueueSize() > 0) {
|
|
doTransmit();
|
|
}
|
|
}
|
|
|
|
doEnd();
|
|
return 0;
|
|
}
|
|
|
|
error_t Cc1101Radio::setEnabled(bool enabled) {
|
|
lock();
|
|
|
|
if (enabled) {
|
|
if ((thread != nullptr) && (thread_get_state(thread) != THREAD_STATE_STOPPED)) {
|
|
LOG_W(TAG, "Already started");
|
|
unlock();
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
if (modulation == RADIO_MODULATION_NONE) {
|
|
LOG_E(TAG, "Cannot enable without a modulation set");
|
|
unlock();
|
|
return ERROR_INVALID_STATE;
|
|
}
|
|
|
|
if (thread != nullptr) {
|
|
thread_free(thread);
|
|
thread = nullptr;
|
|
}
|
|
|
|
threadInterrupted = false;
|
|
setState(RADIO_STATE_ON_PENDING);
|
|
|
|
thread = thread_alloc_full("CC1101", CC1101_THREAD_STACK_SIZE, threadMainStatic, this, tskNO_AFFINITY);
|
|
if (thread == nullptr) {
|
|
setState(RADIO_STATE_ERROR);
|
|
unlock();
|
|
return ERROR_OUT_OF_MEMORY;
|
|
}
|
|
thread_set_priority(thread, THREAD_PRIORITY_HIGH);
|
|
|
|
if (thread_start(thread) != ERROR_NONE) {
|
|
thread_free(thread);
|
|
thread = nullptr;
|
|
setState(RADIO_STATE_ERROR);
|
|
unlock();
|
|
return ERROR_UNDEFINED;
|
|
}
|
|
|
|
unlock();
|
|
return ERROR_NONE;
|
|
} else {
|
|
setState(RADIO_STATE_OFF_PENDING);
|
|
|
|
if (thread != nullptr) {
|
|
threadInterrupted = true;
|
|
event_group_set(events, CC1101_INTERRUPT_BIT);
|
|
|
|
Thread* oldThread = thread;
|
|
thread = nullptr;
|
|
|
|
if (thread_get_state(oldThread) != THREAD_STATE_STOPPED) {
|
|
// Unlock so the thread can lock
|
|
unlock();
|
|
thread_join(oldThread, portMAX_DELAY, pdMS_TO_TICKS(10));
|
|
lock();
|
|
}
|
|
|
|
thread_free(oldThread);
|
|
}
|
|
|
|
setState(RADIO_STATE_OFF);
|
|
unlock();
|
|
return ERROR_NONE;
|
|
}
|
|
}
|
|
|
|
// endregion
|
|
|
|
// region State and modulation
|
|
|
|
enum RadioState Cc1101Radio::getState() const {
|
|
lock();
|
|
const auto result = state;
|
|
unlock();
|
|
return result;
|
|
}
|
|
|
|
void Cc1101Radio::setState(enum RadioState newState) {
|
|
lock();
|
|
if (state == newState) {
|
|
unlock();
|
|
return;
|
|
}
|
|
LOG_I(TAG, "State: %s -> %s", toString(state), toString(newState));
|
|
state = newState;
|
|
unlock();
|
|
|
|
radio_state_event_emit(settings.device, newState);
|
|
}
|
|
|
|
error_t Cc1101Radio::setModulation(enum RadioModulation newModulation) {
|
|
const auto currentState = getState();
|
|
if ((currentState == RADIO_STATE_ON_PENDING) || (currentState == RADIO_STATE_ON)) {
|
|
return ERROR_INVALID_STATE;
|
|
}
|
|
|
|
if (!((newModulation == RADIO_MODULATION_NONE) || canTransmit(newModulation) || canReceive(newModulation))) {
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
|
|
lock();
|
|
LOG_I(TAG, "Modulation set to %s", toString(newModulation));
|
|
modulation = newModulation;
|
|
unlock();
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
enum RadioModulation Cc1101Radio::getModulation() const {
|
|
lock();
|
|
const auto result = modulation;
|
|
unlock();
|
|
return result;
|
|
}
|
|
|
|
// endregion
|
|
|
|
// region TX queue
|
|
|
|
size_t Cc1101Radio::getTxQueueSize() const {
|
|
lock();
|
|
const auto size = txQueue.size();
|
|
unlock();
|
|
return size;
|
|
}
|
|
|
|
Cc1101Radio::TxItem Cc1101Radio::popNextQueuedTx() {
|
|
lock();
|
|
auto tx = std::move(txQueue.front());
|
|
txQueue.pop_front();
|
|
unlock();
|
|
return tx;
|
|
}
|
|
|
|
error_t Cc1101Radio::transmit(const uint8_t* data, size_t length, RadioTxId* id) {
|
|
if ((length == 0) || (length > CC1101_MAX_PACKET_LENGTH)) {
|
|
return ERROR_OUT_OF_RANGE;
|
|
}
|
|
|
|
lock();
|
|
const auto txId = lastTxId;
|
|
lastTxId++;
|
|
txQueue.push_back(TxItem {.id = txId, .data = std::vector<uint8_t>(data, data + length)});
|
|
LOG_D(TAG, "TX id=%d queued: %u bytes (queue depth %u)", (int)txId, (unsigned)length, (unsigned)txQueue.size());
|
|
unlock();
|
|
|
|
radio_tx_event_emit(settings.device, txId, RADIO_TRANSMISSION_STATE_QUEUED);
|
|
event_group_set(events, CC1101_QUEUED_TX_BIT);
|
|
|
|
if (id != nullptr) {
|
|
*id = txId;
|
|
}
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
// endregion
|
|
|
|
// region Parameters
|
|
|
|
error_t Cc1101Radio::setParameter(enum RadioParameter parameter, int32_t value) {
|
|
lock();
|
|
|
|
error_t result;
|
|
switch (parameter) {
|
|
case RADIO_PARAMETER_POWER:
|
|
// The PA table only holds these output levels
|
|
result = checkValuesAndApply(power, value, {-30, -20, -15, -10, 0, 5, 7, 10});
|
|
break;
|
|
case RADIO_PARAMETER_FREQUENCY:
|
|
if (isInLowBand(value) || isInMidBand(value) || isInHighBand(value)) {
|
|
frequency = value;
|
|
result = ERROR_NONE;
|
|
} else {
|
|
result = ERROR_OUT_OF_RANGE;
|
|
}
|
|
break;
|
|
case RADIO_PARAMETER_BANDWIDTH:
|
|
// RX channel filter bandwidths, rounded to whole kHz
|
|
result = checkValuesAndApply(bandwidth, value, {58000, 68000, 81000, 102000, 116000, 135000, 162000, 203000, 232000, 270000, 325000, 406000, 464000, 541000, 650000, 812000});
|
|
break;
|
|
case RADIO_PARAMETER_PREAMBLE_LENGTH:
|
|
result = checkValuesAndApply(preambleLength, value, {16, 24, 32, 48, 64, 96, 128, 192});
|
|
break;
|
|
case RADIO_PARAMETER_SYNC_WORD:
|
|
// 16 bits, and the chip rejects a zero byte in either half
|
|
if ((value >= 0x0101) && (value <= 0xFFFF) && ((value & 0xFF) != 0) && ((value >> 8) != 0)) {
|
|
syncWord = static_cast<uint16_t>(value);
|
|
result = ERROR_NONE;
|
|
} else {
|
|
result = ERROR_OUT_OF_RANGE;
|
|
}
|
|
break;
|
|
case RADIO_PARAMETER_DATA_RATE:
|
|
switch (modulation) {
|
|
case RADIO_MODULATION_FSK4:
|
|
result = checkLimitsAndApply(bitRate, value, 600, 300000);
|
|
break;
|
|
case RADIO_MODULATION_OOK:
|
|
result = checkLimitsAndApply(bitRate, value, 600, 250000);
|
|
break;
|
|
default:
|
|
result = checkLimitsAndApply(bitRate, value, 600, 500000);
|
|
break;
|
|
}
|
|
break;
|
|
case RADIO_PARAMETER_FREQUENCY_DEVIATION:
|
|
if (modulation == RADIO_MODULATION_OOK) {
|
|
result = ERROR_NOT_SUPPORTED;
|
|
} else {
|
|
result = checkLimitsAndApply(frequencyDeviation, value, 1587, 380800);
|
|
}
|
|
break;
|
|
case RADIO_PARAMETER_LBT:
|
|
result = checkLimitsAndApply(lbt, value, 0, 1);
|
|
break;
|
|
case RADIO_PARAMETER_LBT_RSSI_THRESHOLD:
|
|
result = checkLimitsAndApply(lbtRssiThreshold, value, -140, 0);
|
|
break;
|
|
default:
|
|
result = ERROR_NOT_SUPPORTED;
|
|
break;
|
|
}
|
|
|
|
if (result == ERROR_NONE) {
|
|
LOG_D(TAG, "Parameter %d = %d", (int)parameter, (int)value);
|
|
}
|
|
|
|
unlock();
|
|
return result;
|
|
}
|
|
|
|
error_t Cc1101Radio::getParameter(enum RadioParameter parameter, int32_t* value) const {
|
|
lock();
|
|
|
|
error_t result = ERROR_NONE;
|
|
switch (parameter) {
|
|
case RADIO_PARAMETER_POWER:
|
|
*value = power;
|
|
break;
|
|
case RADIO_PARAMETER_FREQUENCY:
|
|
*value = frequency;
|
|
break;
|
|
case RADIO_PARAMETER_BANDWIDTH:
|
|
*value = bandwidth;
|
|
break;
|
|
case RADIO_PARAMETER_PREAMBLE_LENGTH:
|
|
*value = preambleLength;
|
|
break;
|
|
case RADIO_PARAMETER_SYNC_WORD:
|
|
*value = syncWord;
|
|
break;
|
|
case RADIO_PARAMETER_DATA_RATE:
|
|
*value = bitRate;
|
|
break;
|
|
case RADIO_PARAMETER_FREQUENCY_DEVIATION:
|
|
if (modulation == RADIO_MODULATION_OOK) {
|
|
result = ERROR_NOT_SUPPORTED;
|
|
} else {
|
|
*value = frequencyDeviation;
|
|
}
|
|
break;
|
|
case RADIO_PARAMETER_LBT:
|
|
*value = lbt;
|
|
break;
|
|
case RADIO_PARAMETER_LBT_RSSI_THRESHOLD:
|
|
*value = lbtRssiThreshold;
|
|
break;
|
|
default:
|
|
result = ERROR_NOT_SUPPORTED;
|
|
break;
|
|
}
|
|
|
|
unlock();
|
|
return result;
|
|
}
|
|
|
|
// endregion
|
|
|
|
// region Radio operations (radio thread only)
|
|
|
|
// GDO0 uses the GPIO descriptor callback API in HIGH_LEVEL mode. RadioLib leaves GDO0
|
|
// in high impedance outside of reception, so the interrupt is only enabled after
|
|
// startReceive() has mapped GDO0 to "RX FIFO full or end of packet", and the pull-down
|
|
// keeps the floating line low in between.
|
|
void Cc1101Radio::registerGdo0Isr() {
|
|
gpio_flags_t flags = GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_PULL_DOWN;
|
|
flags = GPIO_FLAG_INTERRUPT_TO_OPTIONS(flags, GPIO_INTERRUPT_HIGH_LEVEL);
|
|
if (gpio_descriptor_set_flags(settings.gdo0, flags) != ERROR_NONE ||
|
|
gpio_descriptor_add_callback(settings.gdo0, gdo0Isr, this) != ERROR_NONE) {
|
|
LOG_E(TAG, "Failed to install GDO0 interrupt");
|
|
}
|
|
gpio_descriptor_disable_interrupt(settings.gdo0);
|
|
}
|
|
|
|
void Cc1101Radio::unregisterGdo0Isr() {
|
|
gpio_descriptor_disable_interrupt(settings.gdo0);
|
|
gpio_descriptor_remove_callback(settings.gdo0);
|
|
}
|
|
|
|
void Cc1101Radio::applyBandSwitch() {
|
|
uint8_t value;
|
|
if (isInLowBand(frequency)) {
|
|
value = settings.band_switch_low;
|
|
} else if (isInMidBand(frequency)) {
|
|
value = settings.band_switch_mid;
|
|
} else {
|
|
value = settings.band_switch_high;
|
|
}
|
|
|
|
if (settings.band_switch_0 != nullptr) {
|
|
gpio_descriptor_set_level(settings.band_switch_0, (value & 0b01) != 0);
|
|
}
|
|
if (settings.band_switch_1 != nullptr) {
|
|
gpio_descriptor_set_level(settings.band_switch_1, (value & 0b10) != 0);
|
|
}
|
|
}
|
|
|
|
uint32_t Cc1101Radio::getTimeOnAirMillis(size_t length) const {
|
|
const uint32_t bits = preambleLength + CC1101_PACKET_OVERHEAD_BITS + (length * 8);
|
|
// 4-FSK carries 2 bits per symbol, but the data rate parameter is in bit/s
|
|
return (bits * 1000) / bitRate + 1;
|
|
}
|
|
|
|
int Cc1101Radio::doBegin(enum RadioModulation beginModulation) {
|
|
auto& radio = parts->radio;
|
|
|
|
applyBandSwitch();
|
|
|
|
// RadioLib takes MHz/kHz/kbps floats; the driver stores Hz/bit/s integers.
|
|
ConfigFSK_t config;
|
|
config.frequency = static_cast<float>(frequency) / 1000000.0f;
|
|
config.bitRate = static_cast<float>(bitRate) / 1000.0f;
|
|
config.frequencyDeviation = static_cast<float>(frequencyDeviation) / 1000.0f;
|
|
config.receiverBandwidth = static_cast<float>(bandwidth) / 1000.0f;
|
|
config.power = power;
|
|
config.preambleLength = preambleLength;
|
|
|
|
LOG_I(
|
|
TAG,
|
|
"Starting %s: %.3f MHz, %.2f kbps, deviation %.1f kHz, BW %.0f kHz, preamble %u, sync 0x%04X, %d dBm",
|
|
toString(beginModulation),
|
|
config.frequency,
|
|
config.bitRate,
|
|
config.frequencyDeviation,
|
|
config.receiverBandwidth,
|
|
preambleLength,
|
|
syncWord,
|
|
power
|
|
);
|
|
|
|
int16_t rc;
|
|
if (beginModulation == RADIO_MODULATION_FSK4) {
|
|
rc = radio.beginFSK4(config);
|
|
} else if ((beginModulation == RADIO_MODULATION_FSK) || (beginModulation == RADIO_MODULATION_OOK)) {
|
|
rc = radio.begin(config);
|
|
if ((rc == RADIOLIB_ERR_NONE) && (beginModulation == RADIO_MODULATION_OOK)) {
|
|
rc = radio.setOOK(true);
|
|
}
|
|
} else {
|
|
LOG_E(TAG, "CC1101 not capable of modulation \"%s\"", toString(beginModulation));
|
|
setState(RADIO_STATE_ERROR);
|
|
return -1;
|
|
}
|
|
|
|
if (rc != RADIOLIB_ERR_NONE) {
|
|
LOG_E(TAG, "RadioLib initialization failed with code %hi", rc);
|
|
setState(RADIO_STATE_ERROR);
|
|
return -1;
|
|
}
|
|
|
|
rc = radio.setSyncWord(static_cast<uint8_t>(syncWord >> 8), static_cast<uint8_t>(syncWord & 0xFF));
|
|
if (rc != RADIOLIB_ERR_NONE) {
|
|
LOG_E(TAG, "Setting sync word failed with code %hi", rc);
|
|
setState(RADIO_STATE_ERROR);
|
|
return -1;
|
|
}
|
|
|
|
// Longer packets are discarded by the chip, so a received packet always fits the FIFO
|
|
rc = radio.variablePacketLengthMode(CC1101_MAX_PACKET_LENGTH);
|
|
if (rc != RADIOLIB_ERR_NONE) {
|
|
LOG_E(TAG, "Setting packet length mode failed with code %hi", rc);
|
|
setState(RADIO_STATE_ERROR);
|
|
return -1;
|
|
}
|
|
|
|
LOG_I(TAG, "Modem initialized");
|
|
registerGdo0Isr();
|
|
return 0;
|
|
}
|
|
|
|
void Cc1101Radio::doEnd() {
|
|
unregisterGdo0Isr();
|
|
// Leave the modem in its lowest-power state. The next enable runs a full begin().
|
|
const int16_t rc = parts->radio.sleep();
|
|
if (rc != RADIOLIB_ERR_NONE) {
|
|
LOG_W(TAG, "Putting modem to sleep failed with code %hi", rc);
|
|
} else {
|
|
LOG_I(TAG, "Modem put to sleep");
|
|
}
|
|
}
|
|
|
|
void Cc1101Radio::doTransmit() {
|
|
auto tx = popNextQueuedTx();
|
|
auto& radio = parts->radio;
|
|
|
|
// The chip is still in RX from doListen(), so the channel can be sampled before transmitting
|
|
if (!waitForClearChannel()) {
|
|
if (isThreadInterrupted()) {
|
|
radio_tx_event_emit(settings.device, tx.id, RADIO_TRANSMISSION_STATE_ERROR);
|
|
} else {
|
|
LOG_W(TAG, "TX id=%d: channel busy", (int)tx.id);
|
|
radio_tx_event_emit(settings.device, tx.id, RADIO_TRANSMISSION_STATE_CHANNEL_BUSY);
|
|
}
|
|
return;
|
|
}
|
|
|
|
LOG_I(TAG, "TX id=%d: %u bytes (%u more queued)", (int)tx.id, (unsigned)tx.data.size(), (unsigned)getTxQueueSize());
|
|
int16_t rc = radio.startTransmit(tx.data.data(), tx.data.size());
|
|
if (rc != RADIOLIB_ERR_NONE) {
|
|
LOG_E(TAG, "Error transmitting id=%d, rc=%hi", (int)tx.id, rc);
|
|
radio.standby();
|
|
radio_tx_event_emit(settings.device, tx.id, RADIO_TRANSMISSION_STATE_ERROR);
|
|
return;
|
|
}
|
|
|
|
radio_tx_event_emit(settings.device, tx.id, RADIO_TRANSMISSION_STATE_TRANSMIT_PENDING);
|
|
|
|
// The chip returns to IDLE once the packet is out (TXOFF_MODE = IDLE).
|
|
// Polling MARCSTATE avoids depending on a wired GDO2 line.
|
|
const uint32_t txTimeoutMillis = getTimeOnAirMillis(tx.data.size()) + CC1101_TX_TIMEOUT_MARGIN_MILLIS;
|
|
uint32_t elapsedMillis = 0;
|
|
bool done = false;
|
|
while (elapsedMillis < txTimeoutMillis) {
|
|
if (radio.getMarcState() == RADIOLIB_CC1101_MARC_STATE_IDLE) {
|
|
done = true;
|
|
break;
|
|
}
|
|
if (event_group_wait(events, CC1101_INTERRUPT_BIT, false, false, nullptr, pdMS_TO_TICKS(CC1101_TX_POLL_INTERVAL_MILLIS)) == ERROR_NONE) {
|
|
break;
|
|
}
|
|
elapsedMillis += CC1101_TX_POLL_INTERVAL_MILLIS;
|
|
}
|
|
|
|
if (done) {
|
|
radio.finishTransmit();
|
|
} else {
|
|
radio.standby();
|
|
}
|
|
|
|
if (isThreadInterrupted()) {
|
|
radio_tx_event_emit(settings.device, tx.id, RADIO_TRANSMISSION_STATE_ERROR);
|
|
} else if (done) {
|
|
LOG_I(TAG, "TX id=%d: done", (int)tx.id);
|
|
radio_tx_event_emit(settings.device, tx.id, RADIO_TRANSMISSION_STATE_TRANSMITTED);
|
|
} else {
|
|
LOG_W(TAG, "TX id=%d: not finished within %u ms", (int)tx.id, (unsigned)txTimeoutMillis);
|
|
radio_tx_event_emit(settings.device, tx.id, RADIO_TRANSMISSION_STATE_TIMEOUT);
|
|
}
|
|
}
|
|
|
|
bool Cc1101Radio::doListen() {
|
|
auto& radio = parts->radio;
|
|
|
|
const int16_t rc = radio.startReceive();
|
|
if (rc != RADIOLIB_ERR_NONE) {
|
|
LOG_E(TAG, "Error starting RX, RadioLib returned %hi", rc);
|
|
// Still wait, so a queued TX or a stop request is serviced instead of spinning
|
|
event_group_wait(events, CC1101_INTERRUPT_BIT | CC1101_QUEUED_TX_BIT, false, true, nullptr, portMAX_DELAY);
|
|
return false;
|
|
}
|
|
|
|
// GDO0 now signals end of packet, so the interrupt can be armed
|
|
gpio_descriptor_enable_interrupt(settings.gdo0);
|
|
|
|
while (true) {
|
|
uint32_t flags = 0;
|
|
event_group_wait(
|
|
events,
|
|
CC1101_INTERRUPT_BIT | CC1101_GDO0_EVENT_BIT | CC1101_QUEUED_TX_BIT | CC1101_REQUEST_BIT,
|
|
false,
|
|
false,
|
|
&flags,
|
|
portMAX_DELAY
|
|
);
|
|
// Requests are served without leaving RX, so a packet in flight isn't lost
|
|
if ((flags & (CC1101_INTERRUPT_BIT | CC1101_GDO0_EVENT_BIT | CC1101_QUEUED_TX_BIT)) == 0) {
|
|
event_group_clear(events, CC1101_REQUEST_BIT);
|
|
handleRequest();
|
|
continue;
|
|
}
|
|
event_group_clear(events, flags & (CC1101_INTERRUPT_BIT | CC1101_GDO0_EVENT_BIT | CC1101_QUEUED_TX_BIT));
|
|
gpio_descriptor_disable_interrupt(settings.gdo0);
|
|
// A packet that arrived after the wake is received before a transmission can discard it
|
|
if ((event_group_get(events) & CC1101_GDO0_EVENT_BIT) != 0) {
|
|
event_group_clear(events, CC1101_GDO0_EVENT_BIT);
|
|
flags |= CC1101_GDO0_EVENT_BIT;
|
|
}
|
|
return (flags & CC1101_GDO0_EVENT_BIT) != 0;
|
|
}
|
|
}
|
|
|
|
void Cc1101Radio::doReceive() {
|
|
auto& radio = parts->radio;
|
|
|
|
const size_t rxSize = radio.getPacketLength(true);
|
|
if ((rxSize == 0) || (rxSize > CC1101_MAX_PACKET_LENGTH)) {
|
|
// Spurious IRQ or corrupt length byte
|
|
radio.finishReceive();
|
|
return;
|
|
}
|
|
|
|
std::vector<uint8_t> data(rxSize);
|
|
const int16_t rc = radio.readData(data.data(), rxSize);
|
|
if (rc == RADIOLIB_ERR_CRC_MISMATCH) {
|
|
LOG_D(TAG, "RX: dropped packet with CRC mismatch");
|
|
} else if (rc != RADIOLIB_ERR_NONE) {
|
|
LOG_E(TAG, "Error receiving data, RadioLib returned %hi", rc);
|
|
} else {
|
|
const float rssi = radio.getRSSI();
|
|
LOG_I(TAG, "RX: %u bytes, RSSI %.1f dBm, LQI %u", (unsigned)rxSize, rssi, (unsigned)radio.getLQI());
|
|
radio_rx_event_emit(settings.device, data.data(), rxSize, rssi, NAN);
|
|
}
|
|
}
|
|
|
|
bool Cc1101Radio::isChannelBusy() {
|
|
auto& radio = parts->radio;
|
|
return ((radio.getPacketStatus() & CC1101_PKTSTATUS_SFD) != 0) ||
|
|
(radio.getInstantRssi() >= static_cast<float>(lbtRssiThreshold));
|
|
}
|
|
|
|
bool Cc1101Radio::waitForClearChannel() {
|
|
lock();
|
|
const bool enabled = lbt;
|
|
unlock();
|
|
if (!enabled) {
|
|
return true;
|
|
}
|
|
|
|
// RSSI is only valid in RX, which a preceding reception may have ended
|
|
auto& radio = parts->radio;
|
|
if (radio.getMarcState() != RADIOLIB_CC1101_MARC_STATE_RX) {
|
|
radio.startReceive();
|
|
delay_millis(1);
|
|
}
|
|
|
|
for (int attempt = 0; attempt < CC1101_LBT_MAX_ATTEMPTS; attempt++) {
|
|
if (!isChannelBusy()) {
|
|
return true;
|
|
}
|
|
const uint32_t backoffMillis = CC1101_LBT_BACKOFF_MIN_MILLIS + (esp_random() % (CC1101_LBT_BACKOFF_MAX_MILLIS - CC1101_LBT_BACKOFF_MIN_MILLIS));
|
|
LOG_D(TAG, "Channel busy, backing off %u ms", (unsigned)backoffMillis);
|
|
// Returns early when the radio is being turned off
|
|
if (event_group_wait(events, CC1101_INTERRUPT_BIT, false, false, nullptr, pdMS_TO_TICKS(backoffMillis)) == ERROR_NONE) {
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Cc1101Radio::handleRequest() {
|
|
auto& radio = parts->radio;
|
|
requestResult = ERROR_NONE;
|
|
if (pendingRequest == Request::Present) {
|
|
const int16_t version = radio.getChipVersion();
|
|
requestResult = isKnownVersion(version) ? ERROR_NONE : ERROR_NOT_FOUND;
|
|
} else if (pendingRequest == Request::Rssi) {
|
|
requestRssi = radio.getInstantRssi();
|
|
} else if (pendingRequest == Request::Receiving) {
|
|
requestReceiving = (radio.getPacketStatus() & CC1101_PKTSTATUS_SFD) != 0;
|
|
}
|
|
event_group_set(responses, CC1101_RESPONSE_BIT);
|
|
}
|
|
|
|
error_t Cc1101Radio::performRequest(Request request) {
|
|
if (getState() != RADIO_STATE_ON) {
|
|
return ERROR_INVALID_STATE;
|
|
}
|
|
|
|
mutex_lock(&requestMutex);
|
|
pendingRequest = request;
|
|
event_group_clear(responses, CC1101_RESPONSE_BIT);
|
|
event_group_set(events, CC1101_REQUEST_BIT);
|
|
const error_t waitResult = event_group_wait(responses, CC1101_RESPONSE_BIT, false, true, nullptr, pdMS_TO_TICKS(CC1101_REQUEST_TIMEOUT_MILLIS));
|
|
const error_t result = (waitResult == ERROR_NONE) ? requestResult : ERROR_TIMEOUT;
|
|
mutex_unlock(&requestMutex);
|
|
return result;
|
|
}
|
|
|
|
error_t Cc1101Radio::getRssi(float* rssi) {
|
|
const error_t result = performRequest(Request::Rssi);
|
|
if (result == ERROR_NONE) {
|
|
*rssi = requestRssi;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
error_t Cc1101Radio::checkPresent() {
|
|
if (getState() != RADIO_STATE_ON) {
|
|
return ERROR_NONE;
|
|
}
|
|
// A busy or changing radio counts as present
|
|
return (performRequest(Request::Present) == ERROR_NOT_FOUND) ? ERROR_NOT_FOUND : ERROR_NONE;
|
|
}
|
|
|
|
error_t Cc1101Radio::isReceiving(bool* receiving) {
|
|
const error_t result = performRequest(Request::Receiving);
|
|
if (result == ERROR_NONE) {
|
|
*receiving = requestReceiving;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// endregion
|