ttwrplus: add support for SA868

Add support for initializing SA868 and querying its firmware version,
created a new instance of the AT1846S class that leverages the SA8x8
serial connection as an i2c implementation.
Rx works! In the sense that the RSSI bar behaves as expected, still no
audio.

This commit was contributed by edgetriggered.

TG-553
This commit is contained in:
Niccolò Izzo 2023-08-07 09:02:43 +02:00 committed by Silvano Seva
parent e05d09f0fe
commit c60f580396
7 changed files with 585 additions and 3 deletions

View File

@ -0,0 +1,238 @@
/***************************************************************************
* Copyright (C) 2021 - 2023 by Federico Amedeo Izzo IU2NUO, *
* Niccolò Izzo IU2KIN *
* Frederik Saraci IU2NRO *
* Silvano Seva IU2KWO *
* *
* 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 3 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, see <http://www.gnu.org/licenses/> *
**************************************************************************/
#include <zephyr/kernel.h>
#include <cstring>
#include <stdio.h>
#include <interfaces/delays.h>
#include "AT1846S.h"
#define SA8X8_MSG_SIZE 32
char rx_buf[SA8X8_MSG_SIZE] = { 0 };
void AT1846S::init()
{
i2c_writeReg16(0x30, 0x0001); // Soft reset
delayMs(50);
i2c_writeReg16(0x30, 0x0004); // Chip enable
i2c_writeReg16(0x04, 0x0FD0); // 26MHz crystal frequency
i2c_writeReg16(0x1F, 0x1000); // Gpio6 squelch output
i2c_writeReg16(0x09, 0x03AC);
i2c_writeReg16(0x24, 0x0001);
i2c_writeReg16(0x31, 0x0031);
i2c_writeReg16(0x33, 0x45F5); // AGC number
i2c_writeReg16(0x34, 0x2B89); // RX digital gain
i2c_writeReg16(0x3F, 0x3263); // RSSI 3 threshold
i2c_writeReg16(0x41, 0x470F); // Tx digital gain
i2c_writeReg16(0x42, 0x1036);
i2c_writeReg16(0x43, 0x00BB);
i2c_writeReg16(0x44, 0x06FF); // Tx digital gain
i2c_writeReg16(0x47, 0x7F2F); // Soft mute
i2c_writeReg16(0x4E, 0x0082);
i2c_writeReg16(0x4F, 0x2C62);
i2c_writeReg16(0x53, 0x0094);
i2c_writeReg16(0x54, 0x2A3C);
i2c_writeReg16(0x55, 0x0081);
i2c_writeReg16(0x56, 0x0B02);
i2c_writeReg16(0x57, 0x1C00); // Bypass RSSI low-pass
i2c_writeReg16(0x5A, 0x4935); // SQ detection time
i2c_writeReg16(0x58, 0xBCCD);
i2c_writeReg16(0x62, 0x3263); // Modulation detect tresh
i2c_writeReg16(0x4E, 0x2082);
i2c_writeReg16(0x63, 0x16AD);
i2c_writeReg16(0x30, 0x40A4);
delayMs(50);
i2c_writeReg16(0x30, 0x40A6); // Start calibration
delayMs(100);
i2c_writeReg16(0x30, 0x4006); // Stop calibration
delayMs(100);
i2c_writeReg16(0x58, 0xBCED);
i2c_writeReg16(0x0A, 0x7BA0); // PGA gain
i2c_writeReg16(0x41, 0x4731); // Tx digital gain
i2c_writeReg16(0x44, 0x05FF); // Tx digital gain
i2c_writeReg16(0x59, 0x09D2); // Mixer gain
i2c_writeReg16(0x44, 0x05CF); // Tx digital gain
i2c_writeReg16(0x44, 0x05CC); // Tx digital gain
i2c_writeReg16(0x48, 0x1A32); // Noise 1 threshold
i2c_writeReg16(0x60, 0x1A32); // Noise 2 threshold
i2c_writeReg16(0x3F, 0x29D1); // RSSI 3 threshold
i2c_writeReg16(0x0A, 0x7BA0); // PGA gain
i2c_writeReg16(0x49, 0x0C96); // RSSI SQL thresholds
i2c_writeReg16(0x33, 0x45F5); // AGC number
i2c_writeReg16(0x41, 0x470F); // Tx digital gain
i2c_writeReg16(0x42, 0x1036);
i2c_writeReg16(0x43, 0x00BB);
}
void AT1846S::setBandwidth(const AT1846S_BW band)
{
if(band == AT1846S_BW::_25)
{
// 25kHz bandwidth
i2c_writeReg16(0x15, 0x1F00); // Tuning bit
i2c_writeReg16(0x32, 0x7564); // AGC target power
i2c_writeReg16(0x3A, 0x44C3); // Modulation detect sel
i2c_writeReg16(0x3F, 0x29D2); // RSSI 3 threshold
i2c_writeReg16(0x3C, 0x0E1C); // Peak detect threshold
i2c_writeReg16(0x48, 0x1E38); // Noise 1 threshold
i2c_writeReg16(0x62, 0x3767); // Modulation detect tresh
i2c_writeReg16(0x65, 0x248A);
i2c_writeReg16(0x66, 0xFF2E); // RSSI comp and AFC range
i2c_writeReg16(0x7F, 0x0001); // Switch to page 1
i2c_writeReg16(0x06, 0x0024); // AGC gain table
i2c_writeReg16(0x07, 0x0214);
i2c_writeReg16(0x08, 0x0224);
i2c_writeReg16(0x09, 0x0314);
i2c_writeReg16(0x0A, 0x0324);
i2c_writeReg16(0x0B, 0x0344);
i2c_writeReg16(0x0D, 0x1384);
i2c_writeReg16(0x0E, 0x1B84);
i2c_writeReg16(0x0F, 0x3F84);
i2c_writeReg16(0x12, 0xE0EB);
i2c_writeReg16(0x7F, 0x0000); // Back to page 0
maskSetRegister(0x30, 0x3000, 0x3000);
}
else
{
// 12.5kHz bandwidth
i2c_writeReg16(0x15, 0x1100); // Tuning bit
i2c_writeReg16(0x32, 0x4495); // AGC target power
i2c_writeReg16(0x3A, 0x40C3); // Modulation detect sel
i2c_writeReg16(0x3F, 0x28D0); // RSSI 3 threshold
i2c_writeReg16(0x3C, 0x0F1E); // Peak detect threshold
i2c_writeReg16(0x48, 0x1DB6); // Noise 1 threshold
i2c_writeReg16(0x62, 0x1425); // Modulation detect tresh
i2c_writeReg16(0x65, 0x2494);
i2c_writeReg16(0x66, 0xEB2E); // RSSI comp and AFC range
i2c_writeReg16(0x7F, 0x0001); // Switch to page 1
i2c_writeReg16(0x06, 0x0014); // AGC gain table
i2c_writeReg16(0x07, 0x020C);
i2c_writeReg16(0x08, 0x0214);
i2c_writeReg16(0x09, 0x030C);
i2c_writeReg16(0x0A, 0x0314);
i2c_writeReg16(0x0B, 0x0324);
i2c_writeReg16(0x0C, 0x0344);
i2c_writeReg16(0x0D, 0x1344);
i2c_writeReg16(0x0E, 0x1B44);
i2c_writeReg16(0x0F, 0x3F44);
i2c_writeReg16(0x12, 0xE0EB); // Back to page 0
i2c_writeReg16(0x7F, 0x0000);
maskSetRegister(0x30, 0x3000, 0x0000);
}
reloadConfig();
}
void AT1846S::setOpMode(const AT1846S_OpMode mode)
{
if(mode == AT1846S_OpMode::DMR)
{
// DMR mode
i2c_writeReg16(0x3A, 0x00C2);
i2c_writeReg16(0x33, 0x45F5);
i2c_writeReg16(0x41, 0x4731);
i2c_writeReg16(0x42, 0x1036);
i2c_writeReg16(0x43, 0x00BB);
i2c_writeReg16(0x58, 0xBCFD); // Bit 0 = 1: CTCSS LPF bandwidth to 250Hz
// Bit 3 = 1: bypass CTCSS HPF
// Bit 4 = 1: bypass CTCSS LPF
// Bit 5 = 1: bypass voice LPF
// Bit 6 = 1: bypass voice HPF
// Bit 7 = 1: bypass pre/de-emphasis
// Bit 11 = 1: bypass VOX HPF
// Bit 12 = 1: bypass VOX LPF
// Bit 13 = 1: bypass RSSI LPF
i2c_writeReg16(0x44, 0x06CC);
i2c_writeReg16(0x40, 0x0031);
}
else
{
// FM mode
i2c_writeReg16(0x33, 0x44A5);
i2c_writeReg16(0x41, 0x4431);
i2c_writeReg16(0x42, 0x10F0);
i2c_writeReg16(0x43, 0x00A9);
i2c_writeReg16(0x58, 0xBC05); // Bit 0 = 1: CTCSS LPF badwidth to 250Hz
// Bit 3 = 0: enable CTCSS HPF
// Bit 4 = 0: enable CTCSS LPF
// Bit 5 = 0: enable voice LPF
// Bit 6 = 0: enable voice HPF
// Bit 7 = 0: enable pre/de-emphasis
// Bit 11 = 1: bypass VOX HPF
// Bit 12 = 1: bypass VOX LPF
// Bit 13 = 1: bypass RSSI LPF
i2c_writeReg16(0x44, 0x06FF);
i2c_writeReg16(0x40, 0x0030);
maskSetRegister(0x57, 0x0001, 0x00); // Audio feedback off
maskSetRegister(0x3A, 0x7000, 0x4000); // Select voice channel
}
reloadConfig();
}
/*
* Implementation of AT1846S I2C interface through SA8x8
*/
static constexpr uint8_t devAddr = 0xE2;
void AT1846S::i2c_init()
{
// I2C already init'd by platform support package
}
/*
* These callbacks needs to be implemented by the platform, providing functions
* to read and write from the serial port
*/
extern void radio_uartPrint(const char *fmt, ...);
extern void radio_uartScan(char *buf);
void AT1846S::i2c_writeReg16(uint8_t reg, uint16_t value)
{
/*
* SA8x8 with sa8x8_fw uses PEEK and POKE AT commands to write to AT1846S
*/
radio_uartPrint("AT+POKE=%d,%d\r\n", reg, value);
radio_uartScan(rx_buf);
// Check that response is "OK\r"
if (strncmp(rx_buf, "OK\r", 3))
printk("SA8x8 Error: %d <- %d\n", reg, value);
}
uint16_t AT1846S::i2c_readReg16(uint8_t reg)
{
/*
* SA8x8 with sa8x8_fw uses PEEK and POKE AT commands to write to AT1846S
*/
int32_t value = 0;
radio_uartPrint("AT+PEEK=%d\r\n", reg);
radio_uartScan(rx_buf);
sscanf(rx_buf, "%d\r", &value);
return value;
}

View File

@ -0,0 +1,313 @@
/***************************************************************************
* Copyright (C) 2021 - 2023 by Federico Amedeo Izzo IU2NUO, *
* Niccolò Izzo IU2KIN *
* Frederik Saraci IU2NRO *
* Silvano Seva IU2KWO *
* *
* 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 3 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, see <http://www.gnu.org/licenses/> *
***************************************************************************/
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/kernel.h>
#include <interfaces/delays.h>
#include <interfaces/radio.h>
#include <rtx.h>
#include <ui.h>
#include <algorithm>
#include <string.h>
#include <stdlib.h>
#include "AT1846S.h"
#include "radioUtils.h"
/*
* Define radio node to control the SA868
*/
#if DT_NODE_HAS_STATUS(DT_ALIAS(radio), okay)
#define UART_RADIO_DEV_NODE DT_ALIAS(radio)
#else
#error "Please select the correct radio UART device"
#endif
#define SA8X8_MSG_SIZE 32
K_MSGQ_DEFINE(uart_msgq, SA8X8_MSG_SIZE, 10, 4);
/* receive buffer used in UART ISR callback */
static char rx_buf[SA8X8_MSG_SIZE];
static uint16_t rx_buf_pos;
static const struct device *const radio_dev = DEVICE_DT_GET(UART_RADIO_DEV_NODE);
#define RADIO_PDN_NODE DT_ALIAS(radio_pdn)
static const struct gpio_dt_spec radio_pdn = GPIO_DT_SPEC_GET(RADIO_PDN_NODE, gpios);
const rtxStatus_t *config; // Pointer to data structure with radio configuration
Band currRxBand = BND_NONE; // Current band for RX
Band currTxBand = BND_NONE; // Current band for TX
enum opstatus radioStatus; // Current operating status
AT1846S& at1846s = AT1846S::instance(); // AT1846S driver
void radio_serialCb(const struct device *dev, void *user_data)
{
uint8_t c;
if (!uart_irq_update(radio_dev)) {
return;
}
if (!uart_irq_rx_ready(radio_dev)) {
return;
}
/* read until FIFO empty */
while (uart_fifo_read(radio_dev, &c, 1) == 1) {
if (c == '\n' && rx_buf_pos > 0) {
/* terminate string */
rx_buf[rx_buf_pos] = '\0';
/* if queue is full, message is silently dropped */
k_msgq_put(&uart_msgq, &rx_buf, K_NO_WAIT);
/* reset the buffer (it was copied to the msgq) */
rx_buf_pos = 0;
} else if (rx_buf_pos < (sizeof(rx_buf) - 1)) {
rx_buf[rx_buf_pos++] = c;
}
/* else: characters beyond buffer size are dropped */
}
}
void radio_uartPrint(const char *fmt, ...)
{
char buf[SA8X8_MSG_SIZE] = { 0 };
va_list args;
va_start(args, fmt);
vsnprintk(buf, SA8X8_MSG_SIZE, fmt, args);
int msg_len = strnlen(buf, SA8X8_MSG_SIZE);
for (uint16_t i = 0; i < msg_len; i++) {
uart_poll_out(radio_dev, buf[i]);
}
va_end(args);
}
void radio_uartScan(char *buf)
{
k_msgq_get(&uart_msgq, buf, K_FOREVER);
}
char *radio_getFwVersion()
{
char *tx_buf = (char *) malloc(sizeof(char) * SA8X8_MSG_SIZE);
radio_uartPrint("AT+VERSION\r\n");
k_msgq_get(&uart_msgq, tx_buf, K_FOREVER);
return tx_buf;
}
void radio_init(const rtxStatus_t *rtxState)
{
config = rtxState;
radioStatus = OFF;
int ret;
// Initialize GPIO for SA868S power down
if (!gpio_is_ready_dt(&radio_pdn)) {
printk("Error: radio device %s is not ready\n",
radio_pdn.port->name);
}
ret = gpio_pin_configure_dt(&radio_pdn, GPIO_OUTPUT);
if (ret != 0) {
printk("Error %d: failed to configure %s pin %d\n", ret, radio_pdn.port->name, radio_pdn.pin);
}
if (!device_is_ready(radio_dev)) {
printk("UART device not found!\n");
return;
}
ret = uart_irq_callback_user_data_set(radio_dev, radio_serialCb, NULL);
if (ret < 0) {
if (ret == -ENOTSUP) {
printk("Interrupt-driven UART support not enabled\n");
} else if (ret == -ENOSYS) {
printk("UART device does not support interrupt-driven API\n");
} else {
printk("Error setting UART callback: %d\n", ret);
}
return;
}
uart_irq_rx_enable(radio_dev);
ret = gpio_pin_toggle_dt(&radio_pdn);
if (ret != 0) {
printk("Failed to toggle radio power down");
return;
}
// Add SA8x8 FW version to Info menu
ui_registerInfoExtraEntry("Radio", radio_getFwVersion);
// TODO: Implement audio paths configuration
/*
* Configure AT1846S, keep AF output disabled at power on.
*/
at1846s.init();
}
void radio_disableRtx()
{
at1846s.disableCtcss();
at1846s.setFuncMode(AT1846S_FuncMode::OFF);
radioStatus = OFF;
}
void radio_terminate()
{
radio_disableRtx();
at1846s.terminate();
}
void radio_tuneVcxo(const int16_t vhfOffset, const int16_t uhfOffset)
{
//TODO: this part will be implemented in the future, when proved to be
// necessary.
(void) vhfOffset;
(void) uhfOffset;
}
void radio_setOpmode(const enum opmode mode)
{
switch(mode)
{
case OPMODE_FM:
at1846s.setOpMode(AT1846S_OpMode::FM); // AT1846S in FM mode
break;
case OPMODE_DMR:
at1846s.setOpMode(AT1846S_OpMode::DMR);
at1846s.setBandwidth(AT1846S_BW::_12P5);
break;
case OPMODE_M17:
at1846s.setOpMode(AT1846S_OpMode::DMR); // AT1846S in DMR mode, disables RX filter
at1846s.setBandwidth(AT1846S_BW::_25); // Set bandwidth to 25kHz for proper deviation
break;
default:
break;
}
}
bool radio_checkRxDigitalSquelch()
{
return at1846s.rxCtcssDetected();
}
void radio_enableAfOutput()
{
;
}
void radio_disableAfOutput()
{
;
}
void radio_enableRx()
{
if(currRxBand == BND_NONE) return;
at1846s.setFrequency(config->rxFrequency);
at1846s.setFuncMode(AT1846S_FuncMode::RX);
if(config->rxToneEn)
{
at1846s.enableRxCtcss(config->rxTone);
}
radioStatus = RX;
}
void radio_enableTx()
{
// TODO; Do not enable Tx until proven to be safe
return;
if(config->txDisable == 1) return;
at1846s.setFrequency(config->txFrequency);
at1846s.setFuncMode(AT1846S_FuncMode::TX);
if(config->txToneEn)
{
at1846s.enableTxCtcss(config->txTone);
}
radioStatus = TX;
}
void radio_updateConfiguration()
{
currRxBand = getBandFromFrequency(config->rxFrequency);
currTxBand = getBandFromFrequency(config->txFrequency);
if((currRxBand == BND_NONE) || (currTxBand == BND_NONE)) return;
// Set bandwidth, only for analog FM mode
if(config->opMode == OPMODE_FM)
{
switch(config->bandwidth)
{
case BW_12_5:
at1846s.setBandwidth(AT1846S_BW::_12P5);
break;
case BW_20:
case BW_25:
at1846s.setBandwidth(AT1846S_BW::_25);
break;
default:
break;
}
}
/*
* Update VCO frequency and tuning parameters if current operating status
* is different from OFF.
* This is done by calling again the corresponding functions, which is safe
* to do and avoids code duplication.
*/
if(radioStatus == RX) radio_enableRx();
if(radioStatus == TX) radio_enableTx();
}
float radio_getRssi()
{
return static_cast< float > (at1846s.readRSSI());
}
enum opstatus radio_getStatus()
{
return radioStatus;
}

View File

@ -4,6 +4,7 @@ CONFIG_CPP=y
CONFIG_REQUIRES_FULL_LIBCPP=y
CONFIG_STD_CPP14=y
CONFIG_PRINTK=y
CONFIG_PRINTK_SYNC=y
CONFIG_LOG=y
CONFIG_LOG_PRINTK=y
CONFIG_LOG_MODE_IMMEDIATE=y

View File

@ -14,11 +14,12 @@ target_sources(app
${OPENRTX_ROOT}/platform/drivers/display/SH1106_ttwrplus.c
${OPENRTX_ROOT}/platform/drivers/keyboard/keyboard_ttwrplus.c
${OPENRTX_ROOT}/platform/drivers/baseband/radio_ttwrplus.cpp
${OPENRTX_ROOT}/platform/drivers/baseband/AT1846S_SA8x8.cpp
${OPENRTX_ROOT}/platform/drivers/stubs/audio_stub.c
${OPENRTX_ROOT}/platform/drivers/stubs/cps_io_stub.c
${OPENRTX_ROOT}/platform/drivers/stubs/nvmem_stub.c
${OPENRTX_ROOT}/platform/drivers/stubs/radio_stub.c
${OPENRTX_ROOT}/subprojects/XPowersLib/src/XPowersLibInterface.cpp
)

View File

@ -23,6 +23,7 @@
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/drivers/uart.h>
#include "pmu.h"
#define BUTTON_PTT_NODE DT_NODELABEL(button_ptt)
@ -33,10 +34,12 @@ static const struct device *const qdec_dev = DEVICE_DT_GET(DT_ALIAS(qdec0));
static const hwInfo_t hwInfo =
{
.name = "ttwrplus",
.hw_version = 0,
.uhf_band = 1,
.vhf_band = 0,
.uhf_maxFreq = 430,
.uhf_minFreq = 440,
.uhf_band = 1,
.name = "ttwrplus"
};
void platform_init()

View File

@ -18,6 +18,9 @@
i2c-0 = &i2c0;
watchdog0 = &wdt0;
radio = &uart0;
radio-pwr = &radio_pwr;
radio-pdn = &radio_pdn;
radio-ptt = &radio_ptt;
qdec0 = &pcnt;
};
@ -29,6 +32,25 @@
zephyr,display = &ssd1306;
};
leds: leds {
compatible = "gpio-leds";
radio_pwr: radio_pwr {
gpios = <&gpio1 6 GPIO_ACTIVE_HIGH>;
label = "SA868S H/L on IO38";
};
radio_pdn: radio_pdn {
gpios = <&gpio1 8 GPIO_ACTIVE_LOW>;
label = "SA868S PDN on IO40";
};
radio_ptt: radio_ptt {
gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>;
label = "SA868S PTT on IO41";
};
};
buttons: buttons {
compatible = "zephyr,gpio-keys";

View File

@ -10,5 +10,9 @@ CONFIG_DYNAMIC_THREAD_PREFER_POOL=y
CONFIG_DYNAMIC_THREAD_POOL_SIZE=4
CONFIG_COMMON_LIBC_MALLOC=y
CONFIG_COMMON_LIBC_MALLOC_ARENA_SIZE=131072
CONFIG_NEWLIB_LIBC_MIN_REQUIRED_HEAP_SIZE=0
CONFIG_INPUT=y
CONFIG_SENSOR=y
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_ASSERT=y