kopia lustrzana https://github.com/mobilinkd/tnc3-firmware
344 wiersze
9.3 KiB
C++
344 wiersze
9.3 KiB
C++
// Copyright 2020 Rob Riggs <rob@mobilinkd.com>
|
|
// All rights reserved.
|
|
|
|
#pragma once
|
|
|
|
#include "AudioInput.hpp"
|
|
#include "Modulator.hpp"
|
|
#include "HdlcFrame.hpp"
|
|
#include "M17.h"
|
|
|
|
#include <arm_math.h>
|
|
|
|
#include <array>
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
|
|
namespace mobilinkd { namespace tnc {
|
|
|
|
/**
|
|
* M17 modulator. Collects 16 symbols of data, upsamples by 10x using
|
|
* an interpolating FIR filter, which is sent out via the DAC using
|
|
* DMA.
|
|
*/
|
|
struct M17Modulator : Modulator
|
|
{
|
|
// Six buffers per M17 frame, or 12 half-buffer interrupts.
|
|
static constexpr uint8_t UPSAMPLE = 10;
|
|
static constexpr uint32_t BLOCKSIZE = 4;
|
|
static constexpr uint32_t STATE_SIZE = (m17::FILTER_TAP_NUM_9 / UPSAMPLE) + BLOCKSIZE - 1;
|
|
static constexpr int16_t DAC_BUFFER_LEN = 80; // 8 symbols, 16 bits, 2 bytes.
|
|
static constexpr int16_t TRANSFER_LEN = DAC_BUFFER_LEN / 2; // 4 symbols, 8 bits, 1 byte.
|
|
static constexpr uint16_t VREF = 4095;
|
|
enum class State { STOPPED, STARTING, RUNNING, STOPPING };
|
|
|
|
arm_fir_interpolate_instance_f32 fir_interpolator;
|
|
std::array<float, STATE_SIZE> fir_state;
|
|
std::array<int16_t, DAC_BUFFER_LEN> buffer_;
|
|
std::array<float, 4> symbols;
|
|
osMessageQId dacOutputQueueHandle_{0};
|
|
PTT* ptt_{nullptr};
|
|
uint16_t volume_{4096};
|
|
volatile uint16_t delay_count = 0; // TX Delay
|
|
volatile uint16_t stop_count = 0; // Flush the RRC matched filter.
|
|
State state{State::STOPPED};
|
|
float tmp[TRANSFER_LEN];
|
|
|
|
M17Modulator(osMessageQId queue, PTT* ptt)
|
|
: dacOutputQueueHandle_(queue), ptt_(ptt)
|
|
{
|
|
arm_fir_interpolate_init_f32(
|
|
&fir_interpolator, UPSAMPLE, m17::FILTER_TAP_NUM_9,
|
|
(float32_t*) m17::rrc_taps_f9.data(), fir_state.data(), BLOCKSIZE);
|
|
}
|
|
|
|
~M17Modulator() override {}
|
|
|
|
void start_loopback() override
|
|
{
|
|
}
|
|
|
|
void stop_loopback() override
|
|
{
|
|
}
|
|
|
|
void loopback(const void* input) override
|
|
{
|
|
}
|
|
|
|
void init(const kiss::Hardware& hw) override;
|
|
|
|
void deinit() override
|
|
{
|
|
state = State::STOPPED;
|
|
HAL_DAC_Stop(&hdac1, DAC_CHANNEL_1);
|
|
HAL_TIM_Base_Stop(&htim7);
|
|
ptt_->off();
|
|
}
|
|
|
|
void set_gain(uint16_t level) override
|
|
{
|
|
auto v = std::max<uint16_t>(256, level);
|
|
v = std::min<uint16_t>(4096, v);
|
|
volume_ = v;
|
|
}
|
|
|
|
void set_ptt(PTT* ptt) override
|
|
{
|
|
if (state != State::STOPPED)
|
|
{
|
|
ERROR("PTT change while not stopped");
|
|
CxxErrorHandler();
|
|
}
|
|
ptt_ = ptt;
|
|
ptt_->off();
|
|
}
|
|
|
|
void send(uint8_t bits) override
|
|
{
|
|
uint16_t txdelay = 0;
|
|
|
|
switch (state)
|
|
{
|
|
case State::STOPPING:
|
|
case State::STOPPED:
|
|
#if defined(KISS_LOGGING) && !defined(NUCLEOTNC)
|
|
HAL_RCCEx_DisableLSCO();
|
|
#endif
|
|
delay_count = 0;
|
|
txdelay = kiss::settings().txdelay * 12 - 5;
|
|
fill_empty(buffer_.data());
|
|
fill_empty(buffer_.data() + TRANSFER_LEN);
|
|
state = State::STARTING;
|
|
[[fallthrough]];
|
|
case State::STARTING:
|
|
osMessagePut(audioInputQueueHandle, tnc::audio::IDLE,
|
|
osWaitForever);
|
|
start_conversion();
|
|
ptt_->on();
|
|
while (delay_count < txdelay) osThreadYield();
|
|
stop_count = 4; // 16 symbols to flush the RRC filter.
|
|
osMessagePut(dacOutputQueueHandle_, bits, osWaitForever);
|
|
state = State::RUNNING;
|
|
break;
|
|
case State::RUNNING:
|
|
osMessagePut(dacOutputQueueHandle_, bits, osWaitForever);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// DAC DMA interrupt functions.
|
|
[[gnu::noinline]]
|
|
void fill_first(uint8_t bits) override
|
|
{
|
|
fill(buffer_.data(), bits);
|
|
}
|
|
|
|
[[gnu::noinline]]
|
|
void fill_last(uint8_t bits) override
|
|
{
|
|
fill(buffer_.data() + TRANSFER_LEN, bits);
|
|
}
|
|
|
|
/*
|
|
* DAC queue is empty when STARTING. It is filled with '0' symbols
|
|
* for TX delay duration (using delay_count). It then transitions
|
|
* to the running state in send() after filling the DAC queue.
|
|
*
|
|
* When no more symbols are available, the DAC queue is empty in the
|
|
* running state. The FIR filter is flushed of the remaining data
|
|
* using '0' symbols.
|
|
*/
|
|
void empty_first() override
|
|
{
|
|
switch (state)
|
|
{
|
|
case State::STARTING:
|
|
fill_empty(buffer_.data());
|
|
delay_count += 1;
|
|
break;
|
|
case State::RUNNING:
|
|
fill_empty(buffer_.data());
|
|
state = State::STOPPING;
|
|
break;
|
|
case State::STOPPING:
|
|
fill_empty(buffer_.data());
|
|
if (--stop_count == 0) state = State::STOPPED;
|
|
break;
|
|
case State::STOPPED:
|
|
stop_conversion();
|
|
ptt_->off();
|
|
#if defined(KISS_LOGGING) && !defined(NUCLEOTNC)
|
|
HAL_RCCEx_EnableLSCO(RCC_LSCOSOURCE_LSE);
|
|
#endif
|
|
osMessagePut(audioInputQueueHandle, tnc::audio::DEMODULATOR,
|
|
osWaitForever);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* DAC queue is empty when STARTING. It is filled with '0' symbols
|
|
* for TX delay duration (using delay_count). It then transitions
|
|
* to the running state in send() after filling the DAC queue.
|
|
*
|
|
* When no more symbols are available, the DAC queue is empty in the
|
|
* running state. The FIR filter is flushed of the remaining data
|
|
* using '0' symbols.
|
|
*/
|
|
void empty_last() override
|
|
{
|
|
switch (state)
|
|
{
|
|
case State::STARTING:
|
|
fill_empty(buffer_.data() + TRANSFER_LEN);
|
|
delay_count += 1;
|
|
break;
|
|
case State::RUNNING:
|
|
fill_empty(buffer_.data() + TRANSFER_LEN);
|
|
state = State::STOPPING;
|
|
break;
|
|
case State::STOPPING:
|
|
fill_empty(buffer_.data() + TRANSFER_LEN);
|
|
if (--stop_count == 0) state = State::STOPPED;
|
|
break;
|
|
case State::STOPPED:
|
|
stop_conversion();
|
|
ptt_->off();
|
|
#if defined(KISS_LOGGING) && !defined(NUCLEOTNC)
|
|
HAL_RCCEx_EnableLSCO(RCC_LSCOSOURCE_LSE);
|
|
#endif
|
|
osMessagePut(audioInputQueueHandle, tnc::audio::DEMODULATOR,
|
|
osWaitForever);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void abort() override
|
|
{
|
|
state = State::STOPPED;
|
|
stop_conversion();
|
|
ptt_->off();
|
|
#if defined(KISS_LOGGING) && !defined(NUCLEOTNC)
|
|
HAL_RCCEx_EnableLSCO(RCC_LSCOSOURCE_LSE);
|
|
#endif
|
|
// Drain the queue.
|
|
while (osMessageGet(dacOutputQueueHandle_, 0).status == osEventMessage);
|
|
}
|
|
|
|
float bits_per_ms() const override
|
|
{
|
|
return 9.6f;
|
|
}
|
|
|
|
private:
|
|
|
|
/**
|
|
* Configure the DAC for timer-based DMA conversion, start the timer,
|
|
* and start DMA to DAC.
|
|
*/
|
|
void start_conversion()
|
|
{
|
|
DAC_ChannelConfTypeDef sConfig;
|
|
|
|
sConfig.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE;
|
|
sConfig.DAC_Trigger = DAC_TRIGGER_T7_TRGO;
|
|
sConfig.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE;
|
|
sConfig.DAC_ConnectOnChipPeripheral = DAC_CHIPCONNECT_ENABLE;
|
|
sConfig.DAC_UserTrimming = DAC_TRIMMING_FACTORY;
|
|
if (HAL_DAC_ConfigChannel(&hdac1, &sConfig, DAC_CHANNEL_1) != HAL_OK)
|
|
{
|
|
CxxErrorHandler();
|
|
}
|
|
|
|
HAL_TIM_Base_Start(&htim7);
|
|
HAL_DAC_Start_DMA(
|
|
&hdac1, DAC_CHANNEL_1,
|
|
reinterpret_cast<uint32_t*>(buffer_.data()), buffer_.size(),
|
|
DAC_ALIGN_12B_R);
|
|
}
|
|
|
|
uint16_t adjust_level(float sample) const
|
|
{
|
|
sample *= volume_;
|
|
sample /= 8;
|
|
sample += 2048;
|
|
if (sample > 4095) sample = 4095;
|
|
else if (sample < 0) sample = 0;
|
|
return sample;
|
|
}
|
|
|
|
constexpr int8_t bits_to_symbol(uint8_t bits)
|
|
{
|
|
switch (bits)
|
|
{
|
|
case 0: return 1;
|
|
case 1: return 3;
|
|
case 2: return -1;
|
|
case 3: return -3;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
constexpr int16_t symbol_skew(int32_t symbol)
|
|
{
|
|
const size_t shift = 8;
|
|
if (symbol == 1 || symbol == -1)
|
|
{
|
|
int32_t offset = ((symbol * (kiss::settings().tx_twist - 50)) << shift) / 50;
|
|
return (symbol << shift) - offset;
|
|
}
|
|
else
|
|
{
|
|
return symbol << shift;
|
|
}
|
|
}
|
|
|
|
[[gnu::noinline]]
|
|
void fill(int16_t* buffer, uint8_t bits)
|
|
{
|
|
int16_t polarity = kiss::settings().tx_rev_polarity() ? -1 : 1;
|
|
|
|
for (size_t i = 0; i != 4; ++i)
|
|
{
|
|
symbols[i] = bits_to_symbol(bits >> 6) * polarity;
|
|
bits <<= 2;
|
|
}
|
|
|
|
arm_fir_interpolate_f32(
|
|
&fir_interpolator, symbols.data(), tmp, BLOCKSIZE);
|
|
|
|
for (size_t i = 0; i != TRANSFER_LEN; ++i)
|
|
{
|
|
buffer[i] = adjust_level(tmp[i]);
|
|
}
|
|
}
|
|
#if 0
|
|
[[gnu::noinline]]
|
|
void fill_empty(int16_t* buffer)
|
|
{
|
|
symbols.fill(0);
|
|
|
|
arm_fir_interpolate_f32(
|
|
&fir_interpolator, symbols.data(), tmp, BLOCKSIZE);
|
|
|
|
for (size_t i = 0; i != TRANSFER_LEN; ++i)
|
|
{
|
|
buffer[i] = adjust_level(tmp[i]);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
[[gnu::noinline]]
|
|
void fill_empty(int16_t* buffer)
|
|
{
|
|
for (size_t i = 0; i != TRANSFER_LEN; ++i)
|
|
{
|
|
buffer[i] = 2048;
|
|
}
|
|
}
|
|
};
|
|
|
|
}} // mobilinkd::tnc
|