You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1089 lines
32 KiB
1089 lines
32 KiB
/*
|
|
* Copyright (C) 2025 by Jonathan Naylor G4KLX and contributors
|
|
* SX1255 support: Karel Kadlec (OK1LBC) <xkk@post.cz>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
#include "SX1255Controller.h"
|
|
#include "DStarDefines.h"
|
|
#include "Logger.h"
|
|
|
|
#include <alsa/asoundlib.h>
|
|
#include <linux/spi/spidev.h>
|
|
|
|
#include <sys/ioctl.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#include <dlfcn.h>
|
|
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <chrono>
|
|
#include <thread>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SX1255 RF constants
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// RF I/Q sample rate exposed by the genericstereoaudiocodec overlay.
|
|
static const unsigned int RF_SR = 125000U;
|
|
|
|
// The SX1255 I/Q stream is two interleaved channels (I, Q).
|
|
static const unsigned int RF_CH = 2U;
|
|
|
|
// Decimation factor applied to the RX I/Q stream before FM demodulation.
|
|
static const unsigned int RX_DECIM = 5U;
|
|
|
|
// RX IF sample rate after decimation (125 kHz / 5).
|
|
static const unsigned int IF_SR = RF_SR / RX_DECIM;
|
|
|
|
// Number of taps in the RX decimation low-pass filter.
|
|
static const unsigned int IF_TAPS = 61U;
|
|
|
|
// ALSA period sizes (in frames).
|
|
static const unsigned int RX_PERIOD = 2048U;
|
|
static const unsigned int RX_BUF_PERIODS = 4U;
|
|
static const unsigned int TX_PERIOD = 1024U;
|
|
static const unsigned int TX_BUF_PERIODS = 4U;
|
|
|
|
// Baseband block size handed to the TX path (20 ms at 48 kHz).
|
|
static const unsigned int TX_BASE_BLOCK = DSTAR_RADIO_BLOCK_SIZE;
|
|
|
|
// Peak D-Star GMSK deviation for a unit-magnitude baseband sample (+-0.5
|
|
// represents the peak deviation). h = 0.5 at 4800 baud => 1200 Hz.
|
|
static const float GMSK_PEAK_DEVIATION_HZ = 1200.0F;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SX1255 SPI registers (see the reference sx1255-spi.c from the
|
|
// sx1255-fm-repeater project).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static const uint8_t REG_MODE = 0x00U;
|
|
static const uint8_t REG_RX_FREQ_MSB = 0x01U;
|
|
static const uint8_t REG_RX_FREQ_MID = 0x02U;
|
|
static const uint8_t REG_RX_FREQ_LSB = 0x03U;
|
|
static const uint8_t REG_TX_FREQ_MSB = 0x04U;
|
|
static const uint8_t REG_TX_FREQ_MID = 0x05U;
|
|
static const uint8_t REG_TX_FREQ_LSB = 0x06U;
|
|
static const uint8_t REG_VERSION = 0x07U;
|
|
static const uint8_t REG_TX_DAC = 0x08U;
|
|
static const uint8_t REG_TX_PLL = 0x0AU;
|
|
static const uint8_t REG_TX_DAC_TAPS = 0x0BU;
|
|
static const uint8_t REG_RX_GAIN = 0x0CU;
|
|
static const uint8_t REG_RX_BW = 0x0DU;
|
|
static const uint8_t REG_RX_PLL = 0x0EU;
|
|
static const uint8_t REG_I2S_CFG = 0x12U;
|
|
static const uint8_t REG_BITRATE = 0x13U;
|
|
|
|
// Mode register bit definitions (bit 3 is the shared PLL/front-end enable).
|
|
static const uint8_t MODE_REF = 0x01U;
|
|
static const uint8_t MODE_RX = 0x0BU; // REF + RX + enable
|
|
static const uint8_t MODE_TX = 0x0DU; // REF + TX + enable
|
|
static const uint8_t MODE_DUP = 0x0FU; // REF + RX + TX + enable
|
|
static const uint8_t MODE_OFF = 0x00U;
|
|
|
|
static const float SX1255_CLK_HZ = 32.0e6F;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DSP helper structures (complex samples and the RX/TX processing chains)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
typedef struct { float re, im; } cf32;
|
|
|
|
typedef struct { cf32 x_prev, y_prev; float r; } dc_block_cc_t;
|
|
|
|
typedef struct { float phase, phase_inc; } rotator_t;
|
|
|
|
typedef struct { float phase, phase_inc; } fm_mod_t;
|
|
|
|
typedef struct {
|
|
float taps[IF_TAPS];
|
|
cf32 delay[IF_TAPS];
|
|
int idx;
|
|
} fir_decim_cc_t;
|
|
|
|
typedef struct { cf32 prev; bool has_prev; } nbfm_demod_t;
|
|
|
|
static void design_lowpass(float* taps, int ntaps, float fs, float fc)
|
|
{
|
|
const float norm_cut = fc / fs;
|
|
const int m = ntaps - 1;
|
|
float sum = 0.0F;
|
|
|
|
for (int n = 0; n < ntaps; n++) {
|
|
float x = (float)n - (float)m / 2.0F;
|
|
float h = fabsf(x) < 1e-6F
|
|
? 2.0F * norm_cut
|
|
: sinf(2.0F * (float)M_PI * norm_cut * x) / ((float)M_PI * x);
|
|
float w = 0.54F - 0.46F * cosf(2.0F * (float)M_PI * n / (float)m);
|
|
taps[n] = h * w;
|
|
sum += taps[n];
|
|
}
|
|
|
|
for (int n = 0; n < ntaps; n++)
|
|
taps[n] /= sum;
|
|
}
|
|
|
|
static void dc_block_cc_init(dc_block_cc_t* d, float r)
|
|
{
|
|
::memset(d, 0, sizeof(*d));
|
|
d->r = r;
|
|
}
|
|
|
|
static inline cf32 dc_block_cc_process(dc_block_cc_t* d, cf32 x)
|
|
{
|
|
cf32 y;
|
|
y.re = x.re - d->x_prev.re + d->r * d->y_prev.re;
|
|
y.im = x.im - d->x_prev.im + d->r * d->y_prev.im;
|
|
d->x_prev = x;
|
|
d->y_prev = y;
|
|
return y;
|
|
}
|
|
|
|
static void rotator_init(rotator_t* r, float fs, float shiftHz)
|
|
{
|
|
r->phase = 0.0F;
|
|
r->phase_inc = 2.0F * (float)M_PI * shiftHz / fs;
|
|
}
|
|
|
|
static inline cf32 rotator_process(rotator_t* r, cf32 x)
|
|
{
|
|
float c = cosf(r->phase);
|
|
float s = sinf(r->phase);
|
|
cf32 y;
|
|
y.re = x.re * c - x.im * s;
|
|
y.im = x.re * s + x.im * c;
|
|
r->phase += r->phase_inc;
|
|
if (r->phase > (float)M_PI)
|
|
r->phase -= 2.0F * (float)M_PI;
|
|
else if (r->phase < -(float)M_PI)
|
|
r->phase += 2.0F * (float)M_PI;
|
|
return y;
|
|
}
|
|
|
|
static void fir_decim_cc_init(fir_decim_cc_t* f, float fs, float fc)
|
|
{
|
|
::memset(f, 0, sizeof(*f));
|
|
design_lowpass(f->taps, (int)IF_TAPS, fs, fc);
|
|
}
|
|
|
|
static inline void fir_decim_cc_push(fir_decim_cc_t* f, cf32 x)
|
|
{
|
|
f->delay[f->idx++] = x;
|
|
if (f->idx >= (int)IF_TAPS)
|
|
f->idx = 0;
|
|
}
|
|
|
|
static inline cf32 fir_decim_cc_compute(const fir_decim_cc_t* f)
|
|
{
|
|
cf32 y = {0.0F, 0.0F};
|
|
int idx = f->idx;
|
|
for (int n = 0; n < (int)IF_TAPS; n++) {
|
|
if (--idx < 0)
|
|
idx = (int)IF_TAPS - 1;
|
|
y.re += f->taps[n] * f->delay[idx].re;
|
|
y.im += f->taps[n] * f->delay[idx].im;
|
|
}
|
|
return y;
|
|
}
|
|
|
|
static void nbfm_demod_init(nbfm_demod_t* d)
|
|
{
|
|
::memset(d, 0, sizeof(*d));
|
|
}
|
|
|
|
static inline float nbfm_demod(nbfm_demod_t* d, cf32 x)
|
|
{
|
|
if (!d->has_prev) {
|
|
d->prev = x;
|
|
d->has_prev = true;
|
|
return 0.0F;
|
|
}
|
|
float re = x.re * d->prev.re + x.im * d->prev.im;
|
|
float im = x.im * d->prev.re - x.re * d->prev.im;
|
|
d->prev = x;
|
|
return atan2f(im, re);
|
|
}
|
|
|
|
static void fm_mod_init(fm_mod_t* m, float sensitivity)
|
|
{
|
|
m->phase = 0.0F;
|
|
m->phase_inc = sensitivity;
|
|
}
|
|
|
|
static inline cf32 fm_mod_process(fm_mod_t* m, float x)
|
|
{
|
|
m->phase += m->phase_inc * x;
|
|
if (m->phase > (float)M_PI)
|
|
m->phase -= 2.0F * (float)M_PI;
|
|
else if (m->phase < -(float)M_PI)
|
|
m->phase += 2.0F * (float)M_PI;
|
|
cf32 y;
|
|
y.re = cosf(m->phase);
|
|
y.im = sinf(m->phase);
|
|
return y;
|
|
}
|
|
|
|
static inline float clampf(float x, float lo, float hi)
|
|
{
|
|
if (x < lo) return lo;
|
|
if (x > hi) return hi;
|
|
return x;
|
|
}
|
|
|
|
static inline int32_t f_to_s32(float x)
|
|
{
|
|
x = clampf(x, -1.0F, 1.0F);
|
|
return (int32_t)lrintf(x * 2147483000.0F);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// libgpiod (v2) support
|
|
//
|
|
// libgpiod is loaded dynamically so the build does not require the libgpiod
|
|
// development headers and the binary still runs on systems without libgpiod.
|
|
// It is used both for the SX1255 hardware reset and for the optional PA PTT
|
|
// output. The legacy sysfs GPIO path remains available as a reset fallback.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
struct gpiod_chip;
|
|
struct gpiod_line_settings;
|
|
struct gpiod_line_config;
|
|
struct gpiod_request_config;
|
|
struct gpiod_line_request;
|
|
|
|
typedef gpiod_chip* (*gpiod_chip_open_fn)(const char*);
|
|
typedef void (*gpiod_chip_close_fn)(gpiod_chip*);
|
|
typedef gpiod_line_settings* (*gpiod_settings_new_fn)();
|
|
typedef void (*gpiod_settings_free_fn)(gpiod_line_settings*);
|
|
typedef int (*gpiod_settings_set_direction_fn)(gpiod_line_settings*, int);
|
|
typedef gpiod_line_config* (*gpiod_config_new_fn)();
|
|
typedef void (*gpiod_config_free_fn)(gpiod_line_config*);
|
|
typedef int (*gpiod_config_add_settings_fn)(gpiod_line_config*, const unsigned int*, size_t, gpiod_line_settings*);
|
|
typedef gpiod_request_config* (*gpiod_req_new_fn)();
|
|
typedef void (*gpiod_req_free_fn)(gpiod_request_config*);
|
|
typedef void (*gpiod_req_set_consumer_fn)(gpiod_request_config*, const char*);
|
|
typedef gpiod_line_request* (*gpiod_request_lines_fn)(gpiod_chip*, gpiod_request_config*, gpiod_line_config*);
|
|
typedef void (*gpiod_release_fn)(gpiod_line_request*);
|
|
typedef int (*gpiod_set_value_fn)(gpiod_line_request*, unsigned int, int);
|
|
|
|
struct GpiodApi {
|
|
bool attempted = false;
|
|
bool loaded = false;
|
|
void* handle = nullptr;
|
|
|
|
gpiod_chip_open_fn chip_open = nullptr;
|
|
gpiod_chip_close_fn chip_close = nullptr;
|
|
gpiod_settings_new_fn settings_new = nullptr;
|
|
gpiod_settings_free_fn settings_free = nullptr;
|
|
gpiod_settings_set_direction_fn settings_set_direction = nullptr;
|
|
gpiod_config_new_fn config_new = nullptr;
|
|
gpiod_config_free_fn config_free = nullptr;
|
|
gpiod_config_add_settings_fn config_add_settings = nullptr;
|
|
gpiod_req_new_fn req_new = nullptr;
|
|
gpiod_req_free_fn req_free = nullptr;
|
|
gpiod_req_set_consumer_fn req_set_consumer = nullptr;
|
|
gpiod_request_lines_fn request_lines = nullptr;
|
|
gpiod_release_fn release = nullptr;
|
|
gpiod_set_value_fn set_value = nullptr;
|
|
|
|
bool load()
|
|
{
|
|
if (loaded)
|
|
return true;
|
|
if (attempted)
|
|
return false;
|
|
attempted = true;
|
|
|
|
handle = ::dlopen("libgpiod.so.3", RTLD_NOW | RTLD_LOCAL);
|
|
if (handle == nullptr) {
|
|
wxLogWarning("SX1255: cannot load libgpiod.so.3 (%s)", ::dlerror());
|
|
return false;
|
|
}
|
|
|
|
chip_open = (gpiod_chip_open_fn)::dlsym(handle, "gpiod_chip_open");
|
|
chip_close = (gpiod_chip_close_fn)::dlsym(handle, "gpiod_chip_close");
|
|
settings_new = (gpiod_settings_new_fn)::dlsym(handle, "gpiod_line_settings_new");
|
|
settings_free = (gpiod_settings_free_fn)::dlsym(handle, "gpiod_line_settings_free");
|
|
settings_set_direction = (gpiod_settings_set_direction_fn)::dlsym(handle, "gpiod_line_settings_set_direction");
|
|
config_new = (gpiod_config_new_fn)::dlsym(handle, "gpiod_line_config_new");
|
|
config_free = (gpiod_config_free_fn)::dlsym(handle, "gpiod_line_config_free");
|
|
config_add_settings = (gpiod_config_add_settings_fn)::dlsym(handle, "gpiod_line_config_add_line_settings");
|
|
req_new = (gpiod_req_new_fn)::dlsym(handle, "gpiod_request_config_new");
|
|
req_free = (gpiod_req_free_fn)::dlsym(handle, "gpiod_request_config_free");
|
|
req_set_consumer = (gpiod_req_set_consumer_fn)::dlsym(handle, "gpiod_request_config_set_consumer");
|
|
request_lines = (gpiod_request_lines_fn)::dlsym(handle, "gpiod_chip_request_lines");
|
|
release = (gpiod_release_fn)::dlsym(handle, "gpiod_line_request_release");
|
|
set_value = (gpiod_set_value_fn)::dlsym(handle, "gpiod_line_request_set_value");
|
|
|
|
if (chip_open == nullptr || chip_close == nullptr ||
|
|
settings_new == nullptr || settings_free == nullptr ||
|
|
settings_set_direction == nullptr ||
|
|
config_new == nullptr || config_free == nullptr ||
|
|
config_add_settings == nullptr ||
|
|
req_new == nullptr || req_free == nullptr ||
|
|
request_lines == nullptr || release == nullptr ||
|
|
set_value == nullptr) {
|
|
wxLogWarning("SX1255: libgpiod.so.3 is missing required symbols");
|
|
::dlclose(handle);
|
|
handle = nullptr;
|
|
return false;
|
|
}
|
|
|
|
loaded = true;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
static GpiodApi g_gpiod;
|
|
|
|
static bool gpiodRequestOutput(const std::string& chip, unsigned int line, const char* consumer,
|
|
gpiod_chip*& outChip, gpiod_line_request*& outRequest)
|
|
{
|
|
outChip = nullptr;
|
|
outRequest = nullptr;
|
|
|
|
if (!g_gpiod.load())
|
|
return false;
|
|
|
|
gpiod_chip* c = g_gpiod.chip_open(chip.c_str());
|
|
if (c == nullptr) {
|
|
int savedErrno = errno;
|
|
wxLogWarning("SX1255: cannot open %s (%s)", chip.c_str(), ::strerror(savedErrno));
|
|
return false;
|
|
}
|
|
|
|
gpiod_line_settings* ls = g_gpiod.settings_new();
|
|
gpiod_line_config* lc = g_gpiod.config_new();
|
|
gpiod_request_config* rc = g_gpiod.req_new();
|
|
gpiod_line_request* req = nullptr;
|
|
|
|
if (ls != nullptr && lc != nullptr && rc != nullptr) {
|
|
if (g_gpiod.req_set_consumer != nullptr)
|
|
g_gpiod.req_set_consumer(rc, consumer);
|
|
|
|
unsigned int offsets[1] = { line };
|
|
|
|
// 3 = GPIOD_LINE_DIRECTION_OUTPUT (libgpiod v2 enum).
|
|
if (g_gpiod.settings_set_direction(ls, 3) == 0 &&
|
|
g_gpiod.config_add_settings(lc, offsets, 1U, ls) == 0)
|
|
req = g_gpiod.request_lines(c, rc, lc);
|
|
}
|
|
|
|
if (req == nullptr) {
|
|
int savedErrno = errno;
|
|
wxLogWarning("SX1255: cannot request %s line %u as output (%s)",
|
|
chip.c_str(), line, ::strerror(savedErrno));
|
|
}
|
|
|
|
if (ls != nullptr)
|
|
g_gpiod.settings_free(ls);
|
|
if (lc != nullptr)
|
|
g_gpiod.config_free(lc);
|
|
if (rc != nullptr)
|
|
g_gpiod.req_free(rc);
|
|
|
|
if (req == nullptr) {
|
|
g_gpiod.chip_close(c);
|
|
return false;
|
|
}
|
|
|
|
outChip = c;
|
|
outRequest = req;
|
|
return true;
|
|
}
|
|
|
|
static bool gpiodReset(const std::string& chip, unsigned int line)
|
|
{
|
|
gpiod_chip* c = nullptr;
|
|
gpiod_line_request* req = nullptr;
|
|
if (!gpiodRequestOutput(chip, line, "dstarrepeaterd", c, req))
|
|
return false;
|
|
|
|
// Replicate the reference reset sequence from sx1255-spi.c:
|
|
// drive the pin high, then low.
|
|
g_gpiod.set_value(req, line, 1);
|
|
::usleep(100000U);
|
|
g_gpiod.set_value(req, line, 0);
|
|
::usleep(250000U);
|
|
g_gpiod.release(req);
|
|
g_gpiod.chip_close(c);
|
|
return true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Construction / destruction
|
|
// ---------------------------------------------------------------------------
|
|
|
|
CSX1255Controller::CSX1255Controller(const std::string& rxDevice, const std::string& txDevice,
|
|
const std::string& spiDevice,
|
|
unsigned int rxFrequency, unsigned int txFrequency,
|
|
float rxShiftHz, float txDeviationHz,
|
|
bool rxInvert, bool txInvert,
|
|
float rxLevel, float txLevel,
|
|
unsigned int txDelay, unsigned int txTail,
|
|
unsigned int spiSpeed, int resetGpio,
|
|
const std::string& resetChip, unsigned int resetLine,
|
|
const std::string& pttChip, unsigned int pttLine, bool pttInvert) :
|
|
CSoundCardController(rxDevice, txDevice, rxInvert, txInvert, rxLevel, txLevel, txDelay, txTail),
|
|
m_rxDevice(rxDevice),
|
|
m_txDevice(txDevice),
|
|
m_spiDevice(spiDevice),
|
|
m_rxFrequency(rxFrequency),
|
|
m_txFrequency(txFrequency),
|
|
m_rxShiftHz(rxShiftHz),
|
|
m_txDeviationHz(txDeviationHz > 0.0F ? txDeviationHz : GMSK_PEAK_DEVIATION_HZ),
|
|
m_spiSpeed(spiSpeed),
|
|
m_resetGpio(resetGpio),
|
|
m_resetChip(resetChip),
|
|
m_resetLine(resetLine),
|
|
m_pttChip(pttChip),
|
|
m_pttLine(pttLine),
|
|
m_pttInvert(pttInvert),
|
|
m_spiFd(-1),
|
|
m_pcmRx(nullptr),
|
|
m_pcmTx(nullptr),
|
|
m_rxThread(),
|
|
m_txThread(),
|
|
m_txEnabled(false),
|
|
m_txActive(false),
|
|
m_pttChipHandle(nullptr),
|
|
m_pttRequest(nullptr),
|
|
m_pttAsserted(false)
|
|
{
|
|
}
|
|
|
|
CSX1255Controller::~CSX1255Controller()
|
|
{
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SPI control
|
|
// ---------------------------------------------------------------------------
|
|
|
|
bool CSX1255Controller::spiOpen()
|
|
{
|
|
if (m_spiFd >= 0)
|
|
return true;
|
|
|
|
int fd = ::open(m_spiDevice.c_str(), O_RDWR);
|
|
if (fd < 0) {
|
|
wxLogError("SX1255: cannot open SPI device %s (%s)", m_spiDevice.c_str(), ::strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
uint8_t mode = 0U;
|
|
uint8_t bits = 8U;
|
|
if (::ioctl(fd, SPI_IOC_WR_MODE, &mode) < 0 ||
|
|
::ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0 ||
|
|
::ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &m_spiSpeed) < 0) {
|
|
wxLogError("SX1255: cannot configure SPI device %s (%s)", m_spiDevice.c_str(), ::strerror(errno));
|
|
::close(fd);
|
|
return false;
|
|
}
|
|
|
|
m_spiFd = fd;
|
|
return true;
|
|
}
|
|
|
|
void CSX1255Controller::spiClose()
|
|
{
|
|
if (m_spiFd >= 0) {
|
|
::close(m_spiFd);
|
|
m_spiFd = -1;
|
|
}
|
|
}
|
|
|
|
bool CSX1255Controller::spiReset()
|
|
{
|
|
// Preferred path: reset through libgpiod using an explicit gpiochip and
|
|
// line offset. This is the reliable interface on Raspberry Pi 5 where the
|
|
// legacy sysfs GPIO numbers differ from the classic BCM2835 numbering.
|
|
if (!m_resetChip.empty()) {
|
|
if (gpiodReset(m_resetChip, m_resetLine)) {
|
|
wxLogInfo("SX1255: reset via libgpiod on %s line %u", m_resetChip.c_str(), m_resetLine);
|
|
return true;
|
|
}
|
|
wxLogWarning("SX1255: libgpiod reset on %s line %u failed, falling back to sysfs", m_resetChip.c_str(), m_resetLine);
|
|
}
|
|
|
|
// Legacy fallback: sysfs GPIO export by global GPIO number.
|
|
if (m_resetGpio <= 0) {
|
|
wxLogWarning("SX1255: no reset pin configured (set ResetChip/ResetLine or ResetGPIO)");
|
|
return true;
|
|
}
|
|
|
|
char path[128];
|
|
::snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", m_resetGpio);
|
|
|
|
if (::access(path, W_OK) != 0) {
|
|
// The pin has not been exported yet; try the legacy sysfs export API.
|
|
FILE* fp = ::fopen("/sys/class/gpio/export", "wb");
|
|
if (fp != nullptr) {
|
|
::fprintf(fp, "%d", m_resetGpio);
|
|
::fclose(fp);
|
|
::usleep(250000U);
|
|
}
|
|
|
|
::snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/direction", m_resetGpio);
|
|
fp = ::fopen(path, "wb");
|
|
if (fp != nullptr) {
|
|
::fwrite("out", 1U, 3U, fp);
|
|
::fclose(fp);
|
|
}
|
|
}
|
|
|
|
::snprintf(path, sizeof(path), "/sys/class/gpio/gpio%d/value", m_resetGpio);
|
|
if (::access(path, W_OK) != 0) {
|
|
wxLogWarning("SX1255: reset GPIO %d is not available, assuming the chip is already initialised", m_resetGpio);
|
|
return true;
|
|
}
|
|
|
|
FILE* fp = ::fopen(path, "wb");
|
|
if (fp == nullptr)
|
|
return true;
|
|
::fwrite("1", 1U, 1U, fp);
|
|
::fclose(fp);
|
|
::usleep(100000U);
|
|
fp = ::fopen(path, "wb");
|
|
if (fp != nullptr) {
|
|
::fwrite("0", 1U, 1U, fp);
|
|
::fclose(fp);
|
|
}
|
|
::usleep(250000U);
|
|
return true;
|
|
}
|
|
|
|
bool CSX1255Controller::spiSetFrequency(bool tx, unsigned int freqHz)
|
|
{
|
|
if (freqHz < 410000000U || freqHz > 510000000U) {
|
|
wxLogError("SX1255: frequency %u Hz is outside the 410-510 MHz range", freqHz);
|
|
return false;
|
|
}
|
|
|
|
uint32_t val = (uint32_t)lroundf((float)freqHz * 1048576.0F / SX1255_CLK_HZ);
|
|
uint8_t msb = (uint8_t)((val >> 16) & 0xFFU);
|
|
uint8_t mid = (uint8_t)((val >> 8) & 0xFFU);
|
|
uint8_t lsb = (uint8_t)(val & 0xFFU);
|
|
|
|
uint8_t regMsb = tx ? REG_TX_FREQ_MSB : REG_RX_FREQ_MSB;
|
|
uint8_t regMid = tx ? REG_TX_FREQ_MID : REG_RX_FREQ_MID;
|
|
uint8_t regLsb = tx ? REG_TX_FREQ_LSB : REG_RX_FREQ_LSB;
|
|
|
|
uint8_t txb[2];
|
|
uint8_t rxb[2] = {0U, 0U};
|
|
struct spi_ioc_transfer tr;
|
|
::memset(&tr, 0, sizeof(tr));
|
|
tr.tx_buf = (unsigned long)txb;
|
|
tr.rx_buf = (unsigned long)rxb;
|
|
tr.len = 2U;
|
|
tr.speed_hz = m_spiSpeed;
|
|
tr.bits_per_word = 8U;
|
|
|
|
txb[0] = (uint8_t)(0x80U | regMsb); txb[1] = msb;
|
|
if (::ioctl(m_spiFd, SPI_IOC_MESSAGE(1), &tr) < 0) return false;
|
|
txb[0] = (uint8_t)(0x80U | regMid); txb[1] = mid;
|
|
if (::ioctl(m_spiFd, SPI_IOC_MESSAGE(1), &tr) < 0) return false;
|
|
txb[0] = (uint8_t)(0x80U | regLsb); txb[1] = lsb;
|
|
if (::ioctl(m_spiFd, SPI_IOC_MESSAGE(1), &tr) < 0) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CSX1255Controller::spiSetMode(unsigned char mode)
|
|
{
|
|
uint8_t txb[2] = { (uint8_t)(0x80U | REG_MODE), mode };
|
|
uint8_t rxb[2] = {0U, 0U};
|
|
struct spi_ioc_transfer tr;
|
|
::memset(&tr, 0, sizeof(tr));
|
|
tr.tx_buf = (unsigned long)txb;
|
|
tr.rx_buf = (unsigned long)rxb;
|
|
tr.len = 2U;
|
|
tr.speed_hz = m_spiSpeed;
|
|
tr.bits_per_word = 8U;
|
|
|
|
if (::ioctl(m_spiFd, SPI_IOC_MESSAGE(1), &tr) < 0) {
|
|
wxLogError("SX1255: cannot write mode register (%s)", ::strerror(errno));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CSX1255Controller::spiInitChip()
|
|
{
|
|
uint8_t txb[2];
|
|
uint8_t rxb[2] = {0U, 0U};
|
|
struct spi_ioc_transfer tr;
|
|
::memset(&tr, 0, sizeof(tr));
|
|
tr.tx_buf = (unsigned long)txb;
|
|
tr.rx_buf = (unsigned long)rxb;
|
|
tr.len = 2U;
|
|
tr.speed_hz = m_spiSpeed;
|
|
tr.bits_per_word = 8U;
|
|
|
|
// Verify the silicon version register.
|
|
txb[0] = REG_VERSION; txb[1] = 0U;
|
|
if (::ioctl(m_spiFd, SPI_IOC_MESSAGE(1), &tr) < 0) {
|
|
wxLogError("SX1255: cannot read version register (%s)", ::strerror(errno));
|
|
return false;
|
|
}
|
|
if (rxb[1] != 0x11U) {
|
|
wxLogWarning("SX1255: unexpected version 0x%02X (expected 0x11)", rxb[1]);
|
|
}
|
|
|
|
struct { uint8_t reg; uint8_t val; } initRegs[] = {
|
|
{ REG_BITRATE, (uint8_t)((0U << 7) | (0U << 6) | (5U << 3) | (1U << 2)) }, // 0x2C
|
|
{ REG_I2S_CFG, (uint8_t)((2U << 4) | 2U) }, // 0x22
|
|
{ REG_RX_BW, (uint8_t)((0x01U << 5) | (0x05U << 2) | 0x03U) }, // 0x37
|
|
{ REG_RX_PLL, 0x00U },
|
|
{ REG_RX_GAIN, (uint8_t)((0x01U << 5) | (0x0FU << 1) | 0x00U) }, // 0x3E
|
|
{ REG_TX_DAC_TAPS, 5U },
|
|
{ REG_TX_PLL, 0x00U },
|
|
{ REG_TX_DAC, (uint8_t)((1U << 4) | 0x0EU) }, // 0x1E
|
|
};
|
|
|
|
for (unsigned int i = 0U; i < sizeof(initRegs) / sizeof(initRegs[0]); i++) {
|
|
txb[0] = (uint8_t)(0x80U | initRegs[i].reg);
|
|
txb[1] = initRegs[i].val;
|
|
if (::ioctl(m_spiFd, SPI_IOC_MESSAGE(1), &tr) < 0) {
|
|
wxLogError("SX1255: cannot initialise register 0x%02X (%s)", initRegs[i].reg, ::strerror(errno));
|
|
return false;
|
|
}
|
|
::usleep(10000U);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CSX1255Controller::pttOpen()
|
|
{
|
|
// An empty chip means the PTT output is not used.
|
|
if (m_pttChip.empty())
|
|
return true;
|
|
|
|
gpiod_chip* c = nullptr;
|
|
gpiod_line_request* req = nullptr;
|
|
if (!gpiodRequestOutput(m_pttChip, m_pttLine, "dstarrepeaterd-ptt", c, req))
|
|
return false;
|
|
|
|
m_pttChipHandle = c;
|
|
m_pttRequest = req;
|
|
pttSet(false);
|
|
wxLogInfo("SX1255: PA PTT on %s line %u (%s)", m_pttChip.c_str(), m_pttLine,
|
|
m_pttInvert ? "active low" : "active high");
|
|
return true;
|
|
}
|
|
|
|
void CSX1255Controller::pttClose()
|
|
{
|
|
if (m_pttRequest != nullptr && g_gpiod.loaded) {
|
|
pttSet(false);
|
|
g_gpiod.release((gpiod_line_request*)m_pttRequest);
|
|
m_pttRequest = nullptr;
|
|
}
|
|
if (m_pttChipHandle != nullptr && g_gpiod.loaded) {
|
|
g_gpiod.chip_close((gpiod_chip*)m_pttChipHandle);
|
|
m_pttChipHandle = nullptr;
|
|
}
|
|
m_pttAsserted = false;
|
|
}
|
|
|
|
void CSX1255Controller::pttSet(bool active)
|
|
{
|
|
if (m_pttRequest == nullptr || !g_gpiod.loaded)
|
|
return;
|
|
|
|
int value = active ? (m_pttInvert ? 0 : 1) : (m_pttInvert ? 1 : 0);
|
|
g_gpiod.set_value((gpiod_line_request*)m_pttRequest, m_pttLine, value);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ALSA I/Q transport
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static snd_pcm_t* openPcm(const std::string& device, snd_pcm_stream_t stream,
|
|
unsigned int period, unsigned int bufPeriods)
|
|
{
|
|
snd_pcm_t* pcm = nullptr;
|
|
if (::snd_pcm_open(&pcm, device.c_str(), stream, SND_PCM_NONBLOCK) < 0)
|
|
return nullptr;
|
|
|
|
snd_pcm_hw_params_t* hw = nullptr;
|
|
snd_pcm_hw_params_alloca(&hw);
|
|
::snd_pcm_hw_params_any(pcm, hw);
|
|
::snd_pcm_hw_params_set_access(pcm, hw, SND_PCM_ACCESS_RW_INTERLEAVED);
|
|
::snd_pcm_hw_params_set_format(pcm, hw, SND_PCM_FORMAT_S32_LE);
|
|
::snd_pcm_hw_params_set_channels(pcm, hw, RF_CH);
|
|
|
|
unsigned int rate = RF_SR;
|
|
::snd_pcm_hw_params_set_rate_near(pcm, hw, &rate, nullptr);
|
|
|
|
snd_pcm_uframes_t p = (snd_pcm_uframes_t)period;
|
|
::snd_pcm_hw_params_set_period_size_near(pcm, hw, &p, nullptr);
|
|
snd_pcm_uframes_t b = p * (snd_pcm_uframes_t)bufPeriods;
|
|
::snd_pcm_hw_params_set_buffer_size_near(pcm, hw, &b);
|
|
|
|
if (::snd_pcm_hw_params(pcm, hw) < 0) {
|
|
::snd_pcm_close(pcm);
|
|
return nullptr;
|
|
}
|
|
|
|
::snd_pcm_prepare(pcm);
|
|
return pcm;
|
|
}
|
|
|
|
bool CSX1255Controller::alsaOpen()
|
|
{
|
|
m_pcmRx = openPcm(m_rxDevice, SND_PCM_STREAM_CAPTURE, RX_PERIOD, RX_BUF_PERIODS);
|
|
if (m_pcmRx == nullptr) {
|
|
wxLogError("SX1255: cannot open RX I/Q capture device %s", m_rxDevice.c_str());
|
|
return false;
|
|
}
|
|
|
|
m_pcmTx = openPcm(m_txDevice, SND_PCM_STREAM_PLAYBACK, TX_PERIOD, TX_BUF_PERIODS);
|
|
if (m_pcmTx == nullptr) {
|
|
wxLogError("SX1255: cannot open TX I/Q playback device %s", m_txDevice.c_str());
|
|
::snd_pcm_close(m_pcmRx);
|
|
m_pcmRx = nullptr;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void CSX1255Controller::alsaClose()
|
|
{
|
|
if (m_pcmRx != nullptr) {
|
|
::snd_pcm_close(m_pcmRx);
|
|
m_pcmRx = nullptr;
|
|
}
|
|
if (m_pcmTx != nullptr) {
|
|
::snd_pcm_close(m_pcmTx);
|
|
m_pcmTx = nullptr;
|
|
}
|
|
}
|
|
|
|
static snd_pcm_sframes_t readFrames(snd_pcm_t* pcm, int32_t* buf, snd_pcm_uframes_t frames)
|
|
{
|
|
for (;;) {
|
|
snd_pcm_sframes_t r = ::snd_pcm_readi(pcm, buf, frames);
|
|
if (r == -EAGAIN)
|
|
return 0;
|
|
if (r == -EPIPE) {
|
|
::snd_pcm_prepare(pcm);
|
|
continue;
|
|
}
|
|
if (r < 0)
|
|
return r;
|
|
return r;
|
|
}
|
|
}
|
|
|
|
static int writeFrames(snd_pcm_t* pcm, const int32_t* buf, snd_pcm_uframes_t frames)
|
|
{
|
|
const int32_t* p = buf;
|
|
snd_pcm_uframes_t left = frames;
|
|
while (left > 0U) {
|
|
snd_pcm_sframes_t w = ::snd_pcm_writei(pcm, p, left);
|
|
if (w == -EAGAIN) {
|
|
::usleep(1000U);
|
|
continue;
|
|
}
|
|
if (w == -EPIPE) {
|
|
::snd_pcm_prepare(pcm);
|
|
continue;
|
|
}
|
|
if (w < 0)
|
|
return (int)w;
|
|
left -= (snd_pcm_uframes_t)w;
|
|
p += (size_t)w * RF_CH;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Start / stop
|
|
// ---------------------------------------------------------------------------
|
|
|
|
bool CSX1255Controller::start()
|
|
{
|
|
if (!spiOpen())
|
|
return false;
|
|
|
|
spiReset();
|
|
|
|
if (!spiInitChip()) {
|
|
spiClose();
|
|
return false;
|
|
}
|
|
|
|
// Tune the RX synthesiser low by RXShift so the wanted carrier ends up at
|
|
// DC after the software mixer, avoiding the LO DC spike.
|
|
int rxLo = (int)m_rxFrequency + (int)m_rxShiftHz;
|
|
if (!spiSetFrequency(false, (unsigned int)rxLo)) {
|
|
spiClose();
|
|
return false;
|
|
}
|
|
if (!spiSetFrequency(true, m_txFrequency)) {
|
|
spiClose();
|
|
return false;
|
|
}
|
|
|
|
// Start receive-only: RX front-end on, TX front-end off.
|
|
if (!spiSetMode(MODE_RX)) {
|
|
spiClose();
|
|
return false;
|
|
}
|
|
m_txEnabled = false;
|
|
|
|
if (!alsaOpen()) {
|
|
spiSetMode(MODE_OFF);
|
|
spiClose();
|
|
return false;
|
|
}
|
|
|
|
// Optional PA PTT output. Failure is non-fatal: the modem still works,
|
|
// but the external PA is not keyed.
|
|
if (!pttOpen())
|
|
wxLogError("SX1255: failed to open PA PTT output");
|
|
|
|
// Demodulator thread (reuses CSoundCardController::entry()).
|
|
m_thread = std::thread([this]() { entry(); });
|
|
m_rxThread = std::thread(&CSX1255Controller::rxThread, this);
|
|
m_txThread = std::thread(&CSX1255Controller::txThread, this);
|
|
|
|
wxLogInfo("SX1255 modem started: RX=%u Hz (LO=%d Hz), TX=%u Hz, shift=%.1f Hz",
|
|
m_rxFrequency, rxLo, m_txFrequency, m_rxShiftHz);
|
|
return true;
|
|
}
|
|
|
|
void CSX1255Controller::stop()
|
|
{
|
|
m_stopped = true;
|
|
|
|
if (m_pcmRx != nullptr)
|
|
::snd_pcm_abort(m_pcmRx);
|
|
if (m_pcmTx != nullptr)
|
|
::snd_pcm_abort(m_pcmTx);
|
|
|
|
if (m_rxThread.joinable())
|
|
m_rxThread.join();
|
|
if (m_txThread.joinable())
|
|
m_txThread.join();
|
|
if (m_thread.joinable())
|
|
m_thread.join();
|
|
|
|
alsaClose();
|
|
|
|
pttClose();
|
|
|
|
if (m_spiFd >= 0)
|
|
spiSetMode(MODE_OFF);
|
|
spiClose();
|
|
|
|
wxLogInfo("SX1255 modem stopped");
|
|
}
|
|
|
|
bool CSX1255Controller::isTX()
|
|
{
|
|
return m_txActive.load();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RX thread: SX1255 I/Q -> D-Star baseband
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void CSX1255Controller::rxThread()
|
|
{
|
|
const unsigned int block = RX_PERIOD;
|
|
|
|
int32_t* iq = new int32_t[(size_t)block * RF_CH];
|
|
float* out = new float[block];
|
|
|
|
dc_block_cc_t dc;
|
|
rotator_t rot;
|
|
fir_decim_cc_t lpf;
|
|
nbfm_demod_t demod;
|
|
|
|
dc_block_cc_init(&dc, 0.999F);
|
|
rotator_init(&rot, (float)RF_SR, m_rxShiftHz);
|
|
fir_decim_cc_init(&lpf, (float)RF_SR, 7500.0F);
|
|
nbfm_demod_init(&demod);
|
|
|
|
// Scale the discriminator output so +-GMSK_PEAK_DEVIATION_HZ maps to +-1.0.
|
|
float demodScale = (float)IF_SR / (2.0F * (float)M_PI * GMSK_PEAK_DEVIATION_HZ);
|
|
|
|
double resampStep = (double)IF_SR / (double)DSTAR_RADIO_SAMPLE_RATE;
|
|
double nextOutT = 0.0;
|
|
double inT = 0.0;
|
|
float prevIf = 0.0F;
|
|
bool havePrev = false;
|
|
unsigned int outPos = 0U;
|
|
int decPhase = 0;
|
|
|
|
wxLogInfo("SX1255 RX thread started");
|
|
|
|
while (!m_stopped) {
|
|
snd_pcm_sframes_t got = readFrames(m_pcmRx, iq, (snd_pcm_uframes_t)block);
|
|
if (got == 0) {
|
|
::usleep(1000U);
|
|
continue;
|
|
}
|
|
if (got < 0)
|
|
break;
|
|
|
|
for (snd_pcm_sframes_t i = 0; i < got; i++) {
|
|
cf32 x;
|
|
x.re = (float)iq[i * RF_CH + 0U] / 2147483648.0F;
|
|
x.im = (float)iq[i * RF_CH + 1U] / 2147483648.0F;
|
|
|
|
x = dc_block_cc_process(&dc, x);
|
|
x = rotator_process(&rot, x);
|
|
|
|
fir_decim_cc_push(&lpf, x);
|
|
if (++decPhase < (int)RX_DECIM)
|
|
continue;
|
|
decPhase = 0;
|
|
|
|
cf32 xif = fir_decim_cc_compute(&lpf);
|
|
float af = nbfm_demod(&demod, xif) * demodScale;
|
|
|
|
if (!havePrev) {
|
|
prevIf = af;
|
|
havePrev = true;
|
|
continue;
|
|
}
|
|
|
|
inT += 1.0;
|
|
while (nextOutT <= inT) {
|
|
double frac = nextOutT - (inT - 1.0);
|
|
float a = prevIf + (float)frac * (af - prevIf);
|
|
out[outPos++] = a;
|
|
if (outPos >= block) {
|
|
m_rxAudio.addData(out, outPos);
|
|
outPos = 0U;
|
|
}
|
|
nextOutT += resampStep;
|
|
}
|
|
prevIf = af;
|
|
}
|
|
}
|
|
|
|
if (outPos > 0U)
|
|
m_rxAudio.addData(out, outPos);
|
|
|
|
delete[] iq;
|
|
delete[] out;
|
|
|
|
wxLogInfo("SX1255 RX thread stopped");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TX thread: D-Star baseband -> SX1255 I/Q
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void CSX1255Controller::txThread()
|
|
{
|
|
float* base = new float[TX_BASE_BLOCK];
|
|
|
|
// Sample-and-hold upsample from 48 kHz to 125 kHz, FM-modulating directly
|
|
// at the RF rate. sensitivity maps the +-0.5 GMSK baseband to +-peak
|
|
// deviation.
|
|
float sensitivity = 2.0F * (float)M_PI * (m_txDeviationHz / 0.5F) / (float)RF_SR;
|
|
fm_mod_t mod;
|
|
fm_mod_init(&mod, sensitivity);
|
|
|
|
const unsigned int maxRf = (TX_BASE_BLOCK * RF_SR + DSTAR_RADIO_SAMPLE_RATE - 1U) / DSTAR_RADIO_SAMPLE_RATE + 8U;
|
|
int32_t* iq = new int32_t[(size_t)maxRf * RF_CH];
|
|
int32_t* silence = new int32_t[(size_t)TX_PERIOD * RF_CH];
|
|
::memset(silence, 0, sizeof(int32_t) * (size_t)TX_PERIOD * RF_CH);
|
|
|
|
typedef std::chrono::steady_clock clock;
|
|
clock::time_point lastData = clock::now();
|
|
const int hangMs = 50;
|
|
|
|
wxLogInfo("SX1255 TX thread started");
|
|
|
|
while (!m_stopped) {
|
|
unsigned int n = m_txAudio.getData(base, TX_BASE_BLOCK);
|
|
|
|
if (n == 0U) {
|
|
if (m_txEnabled.load()) {
|
|
int elapsed = (int)std::chrono::duration_cast<std::chrono::milliseconds>(clock::now() - lastData).count();
|
|
if (elapsed >= hangMs) {
|
|
spiSetMode(MODE_RX);
|
|
m_txEnabled = false;
|
|
if (m_pttAsserted) {
|
|
pttSet(false);
|
|
m_pttAsserted = false;
|
|
}
|
|
m_txActive = false;
|
|
}
|
|
}
|
|
writeFrames(m_pcmTx, silence, TX_PERIOD);
|
|
continue;
|
|
}
|
|
|
|
m_txActive = true;
|
|
lastData = clock::now();
|
|
if (!m_txEnabled.load()) {
|
|
// Key the PA before enabling the SX1255 TX front-end so the
|
|
// external amplifier is already up when RF appears.
|
|
if (!m_pttAsserted) {
|
|
pttSet(true);
|
|
m_pttAsserted = true;
|
|
}
|
|
spiSetMode(MODE_DUP);
|
|
m_txEnabled = true;
|
|
}
|
|
|
|
int rfFrames = 0;
|
|
int upAcc = 0;
|
|
for (unsigned int i = 0U; i < n; i++) {
|
|
float x = clampf(base[i], -1.0F, 1.0F);
|
|
upAcc += (int)RF_SR;
|
|
while (upAcc >= (int)DSTAR_RADIO_SAMPLE_RATE && rfFrames < (int)maxRf) {
|
|
upAcc -= (int)DSTAR_RADIO_SAMPLE_RATE;
|
|
cf32 s = fm_mod_process(&mod, x);
|
|
iq[(size_t)rfFrames * RF_CH + 0U] = f_to_s32(s.re);
|
|
iq[(size_t)rfFrames * RF_CH + 1U] = f_to_s32(s.im);
|
|
rfFrames++;
|
|
}
|
|
}
|
|
|
|
if (rfFrames > 0)
|
|
writeFrames(m_pcmTx, iq, (snd_pcm_uframes_t)rfFrames);
|
|
}
|
|
|
|
if (m_txEnabled.load()) {
|
|
spiSetMode(MODE_RX);
|
|
m_txEnabled = false;
|
|
}
|
|
if (m_pttAsserted) {
|
|
pttSet(false);
|
|
m_pttAsserted = false;
|
|
}
|
|
m_txActive = false;
|
|
|
|
delete[] base;
|
|
delete[] iq;
|
|
delete[] silence;
|
|
|
|
wxLogInfo("SX1255 TX thread stopped");
|
|
}
|