RadioLibSmol/src/modules/SX126x/SX126x_LR_FHSS.cpp
Jan Gromeš dc77e6e662
[SX126x] LR-FHSS support (#1228)
* [FEC] Added Vitervi encoder

* [SX126x] Added initial LR-FHSS transmission support

* [CI] Use RPi build for CodeQL

* [SX126x] Fix signed comparison warning

* [FEC] Make input to Viterbi encoder const

* [CI] Drop SX126x examples from Arduino Uno builds

* [CI] Build SX123x for CodeQL scan

* [FEC] Fix comparison type

* [SX126x] Added configurable grid step

* [SX126x] Rename convolutional coding class

* [SX126x] Fix payload CRC

* [SX126x] ADded LR-FHSS example

* [SX126x] Make argument const
2024-10-07 20:00:19 +02:00

392 lines
14 KiB
C++

#include "SX126x.h"
#include <string.h>
#include <math.h>
#if !RADIOLIB_EXCLUDE_SX126X
/*
LR-FHSS implementation in this file is adapted from Setmech's LR-FHSS demo:
https://github.com/Lora-net/SWDM001/tree/master/lib/sx126x_driver
Its SX126x driver is distributed under the Clear BSD License,
and to comply with its terms, it is reproduced below.
The Clear BSD License
Copyright Semtech Corporation 2021. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted (subject to the limitations in the disclaimer
below) provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the Semtech corporation nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SEMTECH CORPORATION BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
// header interleaver
static const uint8_t LrFhssHeaderInterleaver[80] = {
0, 18, 36, 54, 72, 4, 22, 40,
58, 76, 8, 26, 44, 62, 12, 30,
48, 66, 16, 34, 52, 70, 1, 19,
37, 55, 73, 5, 23, 41, 59, 77,
9, 27, 45, 63, 13, 31, 49, 67,
17, 35, 53, 71, 2, 20, 38, 56,
74, 6, 24, 42, 60, 78, 10, 28,
46, 64, 14, 32, 50, 68, 3, 21,
39, 57, 75, 7, 25, 43, 61, 79,
11, 29, 47, 65, 15, 33, 51, 69,
};
int16_t SX126x::buildLRFHSSPacket(const uint8_t* in, size_t in_len, uint8_t* out, size_t* out_len, size_t* out_bits, size_t* out_hops) {
// perform payload whitening
uint8_t lfsr = 0xFF;
for(size_t i = 0; i < in_len; i++) {
uint8_t u = in[i] ^ lfsr;
// we really shouldn't reuse the caller's memory in this way ...
// but since this is a private method it should be at least controlled, if not safe
out[i] = ((u & 0x0F) << 4 ) | ((u & 0xF0) >> 4);
lfsr = (lfsr << 1) | (((lfsr & 0x80) >> 7) ^ (((lfsr & 0x20) >> 5) ^ (((lfsr & 0x10) >> 4) ^ ((lfsr & 0x08) >> 3))));
}
// calculate the CRC-16 over the whitened data, looks like something custom
RadioLibCRCInstance.size = 16;
RadioLibCRCInstance.poly = 0x755B;
RadioLibCRCInstance.init = 0xFFFF;
RadioLibCRCInstance.out = 0x0000;
uint16_t crc16 = RadioLibCRCInstance.checksum(out, in_len);
// add payload CRC
out[in_len] = (crc16 >> 8) & 0xFF;
out[in_len + 1] = crc16 & 0xFF;
out[in_len + 2] = 0;
// encode the payload with CRC using convolutional coding with 1/3 rate into a temporary buffer
uint8_t tmp[RADIOLIB_SX126X_LR_FHSS_MAX_ENC_SIZE] = { 0 };
size_t nb_bits = 0;
RadioLibConvCodeInstance.begin(3);
RadioLibConvCodeInstance.encode(out, 8 * (in_len + 2) + 6, tmp, &nb_bits);
memset(out, 0, RADIOLIB_SX126X_MAX_PACKET_LENGTH);
// for rates other than the 1/3 base, puncture the code
if(this->lrFhssCr != RADIOLIB_SX126X_LR_FHSS_CR_1_3) {
uint32_t matrix_index = 0;
uint8_t matrix[15] = { 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0 };
uint8_t matrix_len = 0;
switch(this->lrFhssCr) {
case RADIOLIB_SX126X_LR_FHSS_CR_5_6:
matrix_len = 15;
break;
case RADIOLIB_SX126X_LR_FHSS_CR_2_3:
matrix_len = 6;
break;
case RADIOLIB_SX126X_LR_FHSS_CR_1_2:
matrix_len = 3;
break;
}
uint32_t j = 0;
for(uint32_t i = 0; i < nb_bits; i++) {
if(matrix[matrix_index]) {
if(TEST_BIT_IN_ARRAY_LSB(tmp, i)) {
SET_BIT_IN_ARRAY_LSB(out, j);
} else {
CLEAR_BIT_IN_ARRAY_LSB(out, j);
}
j++;
}
if(++matrix_index == matrix_len) {
matrix_index = 0;
}
}
nb_bits = j;
memcpy(tmp, out, (nb_bits + 7) / 8);
}
// interleave the payload into output buffer
uint16_t step = 0;
while(step * step < nb_bits) {
// probably the silliest sqrt() I ever saw
step++;
}
const uint16_t step_v = step >> 1;
step <<= 1;
uint16_t pos = 0;
uint16_t st_idx = 0;
uint16_t st_idx_init = 0;
int16_t bits_left = nb_bits;
uint16_t out_row_index = RADIOLIB_SX126X_LR_FHSS_HEADER_BITS * this->lrFhssHdrCount;
while(bits_left > 0) {
int16_t in_row_width = bits_left;
if(in_row_width > RADIOLIB_SX126X_LR_FHSS_FRAG_BITS) {
in_row_width = RADIOLIB_SX126X_LR_FHSS_FRAG_BITS;
}
// guard bits
CLEAR_BIT_IN_ARRAY_LSB(out, out_row_index);
CLEAR_BIT_IN_ARRAY_LSB(out, out_row_index + 1);
for(int16_t j = 0; j < in_row_width; j++) {
// guard bit
if(TEST_BIT_IN_ARRAY_LSB(tmp, pos)) {
SET_BIT_IN_ARRAY_LSB(out, j + 2 + out_row_index);
} else {
CLEAR_BIT_IN_ARRAY_LSB(out, j + 2 + out_row_index);
}
pos += step;
if(pos >= nb_bits) {
st_idx += step_v;
if(st_idx >= step) {
st_idx_init++;
st_idx = st_idx_init;
}
pos = st_idx;
}
}
bits_left -= RADIOLIB_SX126X_LR_FHSS_FRAG_BITS;
out_row_index += 2 + in_row_width;
}
nb_bits = out_row_index - RADIOLIB_SX126X_LR_FHSS_HEADER_BITS * this->lrFhssHdrCount;
// build the header
uint8_t raw_header[RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2];
raw_header[0] = in_len;
raw_header[1] = (this->lrFhssCr << 3) | ((uint8_t)this->lrFhssGridNonFcc << 2) |
(RADIOLIB_SX126X_LR_FHSS_HOPPING_ENABLED << 1) | (this->lrFhssBw >> 3);
raw_header[2] = ((this->lrFhssBw & 0x07) << 5) | (this->lrFhssHopSeqId >> 4);
raw_header[3] = ((this->lrFhssHopSeqId & 0x000F) << 4);
// CRC-8 used seems to based on 8H2F, but without final XOR
RadioLibCRCInstance.size = 8;
RadioLibCRCInstance.poly = 0x2F;
RadioLibCRCInstance.init = 0xFF;
RadioLibCRCInstance.out = 0x00;
uint16_t header_offset = 0;
for(size_t i = 0; i < this->lrFhssHdrCount; i++) {
// insert index and calculate the header CRC
raw_header[3] = (raw_header[3] & ~0x0C) | ((this->lrFhssHdrCount - i - 1) << 2);
raw_header[4] = RadioLibCRCInstance.checksum(raw_header, (RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2 - 1));
// convolutional encode
uint8_t coded_header[RADIOLIB_SX126X_LR_FHSS_HDR_BYTES] = { 0 };
RadioLibConvCodeInstance.begin(2);
RadioLibConvCodeInstance.encode(raw_header, 8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2, coded_header);
// tail-biting seems to just do this twice ...?
RadioLibConvCodeInstance.encode(raw_header, 8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2, coded_header);
// clear guard bits
CLEAR_BIT_IN_ARRAY_LSB(out, header_offset);
CLEAR_BIT_IN_ARRAY_LSB(out, header_offset + 1);
// interleave the header directly to the physical payload buffer
for(size_t j = 0; j < (8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2); j++) {
if(TEST_BIT_IN_ARRAY_LSB(coded_header, LrFhssHeaderInterleaver[j])) {
SET_BIT_IN_ARRAY_LSB(out, header_offset + 2 + j);
} else {
CLEAR_BIT_IN_ARRAY_LSB(out, header_offset + 2 + j);
}
}
for(size_t j = 0; j < (8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2); j++) {
if(TEST_BIT_IN_ARRAY_LSB(coded_header, LrFhssHeaderInterleaver[(8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2) + j])) {
SET_BIT_IN_ARRAY_LSB(out, header_offset + 2 + (8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2) + (8*RADIOLIB_SX126X_LR_FHSS_SYNC_WORD_BYTES) + j);
} else {
CLEAR_BIT_IN_ARRAY_LSB(out, header_offset + 2 + (8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2) + (8*RADIOLIB_SX126X_LR_FHSS_SYNC_WORD_BYTES) + j);
}
}
// copy the sync word to the physical payload buffer
for(size_t j = 0; j < (8*RADIOLIB_SX126X_LR_FHSS_SYNC_WORD_BYTES); j++) {
if(TEST_BIT_IN_ARRAY_LSB(this->lrFhssSyncWord, j)) {
SET_BIT_IN_ARRAY_LSB(out, header_offset + 2 + (8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2) + j);
} else {
CLEAR_BIT_IN_ARRAY_LSB(out, header_offset + 2 + (8*RADIOLIB_SX126X_LR_FHSS_HDR_BYTES/2) + j);
}
}
header_offset += RADIOLIB_SX126X_LR_FHSS_HEADER_BITS;
}
// calculate the number of hops and total number of bits
uint16_t length_bits = (in_len + 2) * 8 + 6;
switch(this->lrFhssCr) {
case RADIOLIB_SX126X_LR_FHSS_CR_5_6:
length_bits = ( ( length_bits * 6 ) + 4 ) / 5;
break;
case RADIOLIB_SX126X_LR_FHSS_CR_2_3:
length_bits = length_bits * 3 / 2;
break;
case RADIOLIB_SX126X_LR_FHSS_CR_1_2:
length_bits = length_bits * 2;
break;
case RADIOLIB_SX126X_LR_FHSS_CR_1_3:
length_bits = length_bits * 3;
break;
}
*out_hops = (length_bits + 47) / 48 + this->lrFhssHdrCount;
// calculate total number of payload bits, after breaking into blocks
uint16_t payload_bits = length_bits / RADIOLIB_SX126X_LR_FHSS_FRAG_BITS * RADIOLIB_SX126X_LR_FHSS_BLOCK_BITS;
uint16_t last_block_bits = length_bits % RADIOLIB_SX126X_LR_FHSS_FRAG_BITS;
if(last_block_bits > 0) {
// add the 2 guard bits for the last block + the actual remaining payload bits
payload_bits += last_block_bits + 2;
}
*out_bits = (RADIOLIB_SX126X_LR_FHSS_HEADER_BITS * this->lrFhssHdrCount) + payload_bits;
*out_len = (*out_bits + 7) / 8;
return(RADIOLIB_ERR_NONE);
}
int16_t SX126x::resetLRFHSS() {
// initialize hopping configuration
const uint16_t numChan[] = { 80, 176, 280, 376, 688, 792, 1480, 1584, 3120, 3224 };
// LFSR polynomials for different ranges of lrFhssNgrid
const uint8_t lfsrPoly1[] = { 33, 45, 48, 51, 54, 57 };
const uint8_t lfsrPoly2[] = { 65, 68, 71, 72 };
const uint8_t lfsrPoly3[] = { 142, 149 };
uint32_t nb_channel_in_grid = this->lrFhssGridNonFcc ? 8 : 52;
this->lrFhssNgrid = numChan[this->lrFhssBw] / nb_channel_in_grid;
this->lrFhssLfsrState = 6;
switch(this->lrFhssNgrid) {
case 10:
case 22:
case 28:
case 30:
case 35:
case 47:
this->lrFhssPoly = lfsrPoly1[this->lrFhssHopSeqId >> 6];
this->lrFhssSeed = this->lrFhssHopSeqId & 0x3F;
if(this->lrFhssHopSeqId >= 384) {
return(RADIOLIB_ERR_INVALID_DATA_SHAPING);
}
break;
case 60:
case 62:
this->lrFhssLfsrState = 56;
this->lrFhssPoly = lfsrPoly1[this->lrFhssHopSeqId >> 6];
this->lrFhssSeed = this->lrFhssHopSeqId & 0x3F;
if(this->lrFhssHopSeqId >= 384) {
return(RADIOLIB_ERR_INVALID_DATA_SHAPING);
}
break;
case 86:
case 99:
this->lrFhssPoly = lfsrPoly2[this->lrFhssHopSeqId >> 7];
this->lrFhssSeed = this->lrFhssHopSeqId & 0x7F;
break;
case 185:
case 198:
this->lrFhssPoly = lfsrPoly3[this->lrFhssHopSeqId >> 8];
this->lrFhssSeed = this->lrFhssHopSeqId & 0xFF;
break;
case 390:
case 403:
this->lrFhssPoly = 264;
this->lrFhssSeed = this->lrFhssHopSeqId;
break;
default:
return(RADIOLIB_ERR_INVALID_DATA_SHAPING);
}
return(RADIOLIB_ERR_NONE);
}
uint16_t SX126x::stepLRFHSS() {
uint16_t hop;
do {
uint16_t lsb = this->lrFhssLfsrState & 1;
this->lrFhssLfsrState >>= 1;
if(lsb) {
this->lrFhssLfsrState ^= this->lrFhssPoly;
}
hop = this->lrFhssSeed;
if(hop != this->lrFhssLfsrState) {
hop ^= this->lrFhssLfsrState;
}
} while(hop > this->lrFhssNgrid);
return(hop);
}
int16_t SX126x::setLRFHSSHop(uint8_t index) {
if(!this->lrFhssFrameHopsRem) {
return(RADIOLIB_ERR_NONE);
}
uint16_t hop = stepLRFHSS();
int16_t freq_table = hop - 1;
if(freq_table >= (int16_t)(this->lrFhssNgrid >> 1)) {
freq_table -= this->lrFhssNgrid;
}
uint32_t nb_channel_in_grid = this->lrFhssGridNonFcc ? 8 : 52;
uint32_t grid_offset = (1 + (this->lrFhssNgrid % 2)) * (nb_channel_in_grid / 2);
uint32_t grid_in_pll_steps = this->lrFhssGridNonFcc ? 4096 : 26624;
uint32_t freq_raw = this->frf - freq_table * grid_in_pll_steps - grid_offset * 512;
if((this->lrFhssHopNum < this->lrFhssHdrCount)) {
if((((this->lrFhssHdrCount - this->lrFhssHopNum) % 2) == 0)) {
freq_raw += 256;
}
}
uint8_t frq[4] = { (uint8_t)((freq_raw >> 24) & 0xFF), (uint8_t)((freq_raw >> 16) & 0xFF), (uint8_t)((freq_raw >> 8) & 0xFF), (uint8_t)(freq_raw & 0xFF) };
int16_t state = writeRegister(RADIOLIB_SX126X_REG_LR_FHSS_FREQX_0(index), frq, sizeof(freq_raw));
RADIOLIB_ASSERT(state);
// (LR_FHSS_HEADER_BITS + pulse_shape_compensation) symbols on first sync_word, LR_FHSS_HEADER_BITS on
// next sync_words, LR_FHSS_BLOCK_BITS on payload
uint16_t numSymbols = RADIOLIB_SX126X_LR_FHSS_BLOCK_BITS;
if(index == 0) {
numSymbols = RADIOLIB_SX126X_LR_FHSS_HEADER_BITS + 1; // the +1 is "pulse_shape_compensation", but it's constant in the demo
} else if(index < this->lrFhssHdrCount) {
numSymbols = RADIOLIB_SX126X_LR_FHSS_HEADER_BITS;
} else if(this->lrFhssFrameBitsRem < RADIOLIB_SX126X_LR_FHSS_BLOCK_BITS) {
numSymbols = this->lrFhssFrameBitsRem;
}
// write hop length in symbols
uint8_t sym[2] = { (uint8_t)((numSymbols >> 8) & 0xFF), (uint8_t)(numSymbols & 0xFF) };
state = writeRegister(RADIOLIB_SX126X_REG_LR_FHSS_NUM_SYMBOLS_FREQX_MSB(index), sym, sizeof(uint16_t));
RADIOLIB_ASSERT(state);
this->lrFhssFrameBitsRem -= numSymbols;
this->lrFhssFrameHopsRem--;
this->lrFhssHopNum++;
return(RADIOLIB_ERR_NONE);
}
#endif