Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Peter Feerick <5500713+pfeerick@users.noreply.github.com>
441 lines
11 KiB
C++
441 lines
11 KiB
C++
/*
|
|
* Copyright (C) EdgeTX
|
|
*
|
|
* Based on code named
|
|
* opentx - https://github.com/opentx/opentx
|
|
* th9x - http://code.google.com/p/th9x
|
|
* er9x - http://code.google.com/p/er9x
|
|
* gruvin9x - http://code.google.com/p/gruvin9x
|
|
*
|
|
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
#include "hal/gpio.h"
|
|
#include "hal/audio_driver.h"
|
|
|
|
#include "os/sleep.h"
|
|
#include "stm32_gpio.h"
|
|
#include "stm32_timer.h"
|
|
#include "stm32_dma.h"
|
|
|
|
#include "edgetx.h"
|
|
|
|
#if defined(AUDIO_MUTE_GPIO)
|
|
|
|
static inline void set_mute_pin(bool enabled)
|
|
{
|
|
#if defined(INVERTED_MUTE_PIN)
|
|
enabled = !enabled;
|
|
#endif
|
|
gpio_write(AUDIO_MUTE_GPIO, enabled);
|
|
}
|
|
|
|
static inline bool get_mute_pin(void)
|
|
{
|
|
bool enabled = gpio_read(AUDIO_MUTE_GPIO) ? 1 : 0;
|
|
#if defined(INVERTED_MUTE_PIN)
|
|
enabled = !enabled;
|
|
#endif
|
|
return enabled;
|
|
}
|
|
|
|
void audioMute()
|
|
{
|
|
if (!g_eeGeneral.audioMuteEnable) return;
|
|
#if defined(AUDIO_UNMUTE_DELAY)
|
|
tmr10ms_t now = get_tmr10ms();
|
|
if (!audioQueue.lastAudioPlayTime) {
|
|
// we start the mute delay now
|
|
audioQueue.lastAudioPlayTime = now;
|
|
}
|
|
else if (now - audioQueue.lastAudioPlayTime > AUDIO_MUTE_DELAY / 10) {
|
|
// delay expired, we may mute
|
|
set_mute_pin(true);
|
|
}
|
|
#else
|
|
// mute
|
|
set_mute_pin(true);
|
|
#endif
|
|
}
|
|
|
|
void audioUnmute()
|
|
{
|
|
if (isFunctionActive(FUNCTION_DISABLE_AUDIO_AMP)) {
|
|
set_mute_pin(true);
|
|
return;
|
|
}
|
|
|
|
#if defined(AUDIO_UNMUTE_DELAY)
|
|
// if muted
|
|
if (get_mute_pin()) {
|
|
// ..un-mute
|
|
set_mute_pin(false);
|
|
sleep_ms(AUDIO_UNMUTE_DELAY);
|
|
}
|
|
// reset the mute delay
|
|
audioQueue.lastAudioPlayTime = 0;
|
|
#else
|
|
set_mute_pin(false);
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
#if defined(STM32H5) || defined(STM32H7) || defined(STM32H7RS)
|
|
|
|
#define DAC_TRIGGER LL_DAC_TRIG_EXT_TIM6_TRGO
|
|
|
|
// 16 bit, 1 channels
|
|
#define DMA_BUFFER_HALF_LEN AUDIO_BUFFER_SIZE
|
|
|
|
// 2 buffers
|
|
#define DMA_BUFFER_LEN (DMA_BUFFER_HALF_LEN * 2)
|
|
|
|
static uint16_t _dma_buffer[DMA_BUFFER_LEN] __DMA_NO_CACHE;
|
|
|
|
static volatile uint32_t _dma_buffer_offset = 0;
|
|
static volatile uint8_t _empty_dma_halves = 0;
|
|
|
|
// Require sustained silence before stopping DMA to avoid start/stop thrashing
|
|
// when the producer briefly lags behind the consumer.
|
|
constexpr uint8_t DMA_EMPTY_HALVES_STOP_THRESHOLD = 6;
|
|
|
|
static inline uint32_t _calc_offset(uint8_t tc)
|
|
{
|
|
return tc * DMA_BUFFER_HALF_LEN;
|
|
}
|
|
|
|
// return true if no more data available
|
|
static bool audio_update_dma_buffer(uint8_t tc)
|
|
{
|
|
_dma_buffer_offset = _calc_offset(tc);
|
|
|
|
auto buffer = audioQueue.buffersFifo.getNextFilledBuffer();
|
|
if (!buffer) {
|
|
unsigned idx = 0;
|
|
for (; idx < DMA_BUFFER_HALF_LEN; idx++) {
|
|
uint32_t offset = _dma_buffer_offset + idx;
|
|
_dma_buffer[offset] = AUDIO_DATA_SILENCE;
|
|
}
|
|
return true;
|
|
} else {
|
|
unsigned idx = 0;
|
|
for (; idx < buffer->size; idx++) {
|
|
uint32_t offset = _dma_buffer_offset + idx;
|
|
_dma_buffer[offset] = (uint16_t)buffer->data[idx];
|
|
}
|
|
for (; idx < DMA_BUFFER_HALF_LEN; idx++) {
|
|
uint32_t offset = _dma_buffer_offset + idx;
|
|
_dma_buffer[offset] = AUDIO_DATA_SILENCE;
|
|
}
|
|
audioQueue.buffersFifo.freeNextFilledBuffer();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void dac_dma_init()
|
|
{
|
|
stm32_dma_enable_clock(AUDIO_DMA);
|
|
|
|
// _dma_buffer lives in a NOLOAD section, so it holds random SRAM contents at
|
|
// power-on. Prime it with silence so the DAC never clocks out garbage (heard
|
|
// as a crackle) on the very first transfer.
|
|
for (unsigned i = 0; i < DMA_BUFFER_LEN; i++)
|
|
_dma_buffer[i] = AUDIO_DATA_SILENCE;
|
|
|
|
LL_DMA_DeInit(AUDIO_DMA, AUDIO_DMA_Stream);
|
|
|
|
LL_DMA_InitTypeDef dmaInit;
|
|
LL_DMA_StructInit(&dmaInit);
|
|
|
|
#if defined(STM32H7)
|
|
dmaInit.Mode = LL_DMA_MODE_CIRCULAR;
|
|
dmaInit.PeriphRequest = AUDIO_DMA_Channel;
|
|
dmaInit.Direction = LL_DMA_DIRECTION_MEMORY_TO_PERIPH;
|
|
dmaInit.Priority = LL_DMA_PRIORITY_VERYHIGH;
|
|
dmaInit.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT;
|
|
dmaInit.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_HALFWORD;
|
|
dmaInit.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_HALFWORD;
|
|
dmaInit.PeriphOrM2MSrcAddress = LL_DAC_DMA_GetRegAddr(
|
|
DAC1, LL_DAC_CHANNEL_1, LL_DAC_DMA_REG_DATA_12BITS_LEFT_ALIGNED);
|
|
dmaInit.MemoryOrM2MDstAddress = (uintptr_t)_dma_buffer;
|
|
dmaInit.NbData = DMA_BUFFER_LEN;
|
|
#elif defined(STM32H7RS)
|
|
#error not implemented
|
|
#endif
|
|
LL_DMA_Init(AUDIO_DMA, AUDIO_DMA_Stream, &dmaInit);
|
|
|
|
NVIC_EnableIRQ(AUDIO_DMA_Stream_IRQn);
|
|
NVIC_SetPriority(AUDIO_DMA_Stream_IRQn, 7);
|
|
}
|
|
|
|
static inline void dac_clear_dma_flags()
|
|
{
|
|
// Drain stale half/complete flags so a new transfer starts from a clean state.
|
|
stm32_dma_check_ht_flag(AUDIO_DMA, AUDIO_DMA_Stream);
|
|
stm32_dma_check_tc_flag(AUDIO_DMA, AUDIO_DMA_Stream);
|
|
}
|
|
|
|
static void dac_close_dma_xfer()
|
|
{
|
|
LL_DMA_DisableIT_TC(AUDIO_DMA, AUDIO_DMA_Stream);
|
|
LL_DMA_DisableIT_HT(AUDIO_DMA, AUDIO_DMA_Stream);
|
|
LL_DMA_DisableStream(AUDIO_DMA, AUDIO_DMA_Stream);
|
|
|
|
// Wait until DMA EN bit is actually cleared by hardware.
|
|
uint32_t timeout = 1000;
|
|
while (LL_DMA_IsEnabledStream(AUDIO_DMA, AUDIO_DMA_Stream) && timeout--) {
|
|
__NOP();
|
|
}
|
|
|
|
dac_clear_dma_flags();
|
|
}
|
|
|
|
static void dac_start_dma()
|
|
{
|
|
// re-arm from the start of the buffer: a mid-transfer stop leaves NDTR and the
|
|
// memory address partway, which desyncs the HT/TC half tracking
|
|
LL_DMA_DisableStream(AUDIO_DMA, AUDIO_DMA_Stream);
|
|
LL_DMA_SetMemoryAddress(AUDIO_DMA, AUDIO_DMA_Stream, (uintptr_t)_dma_buffer);
|
|
LL_DMA_SetDataLength(AUDIO_DMA, AUDIO_DMA_Stream, DMA_BUFFER_LEN);
|
|
|
|
dac_clear_dma_flags();
|
|
|
|
// enable DMA stream and transfer complete interrupt
|
|
LL_DMA_EnableIT_HT(AUDIO_DMA, AUDIO_DMA_Stream);
|
|
LL_DMA_EnableIT_TC(AUDIO_DMA, AUDIO_DMA_Stream);
|
|
LL_DMA_EnableStream(AUDIO_DMA, AUDIO_DMA_Stream);
|
|
|
|
// clear underrun flag
|
|
LL_DAC_ClearFlag_DMAUDR1(AUDIO_DAC);
|
|
|
|
// enable DAC
|
|
AUDIO_DAC->CR |= DAC_CR_EN1 | DAC_CR_DMAEN1;
|
|
}
|
|
|
|
void audioConsumeCurrentBuffer()
|
|
{
|
|
if (!LL_DMA_IsEnabledStream(AUDIO_DMA, AUDIO_DMA_Stream)) {
|
|
if (!audio_update_dma_buffer(0)) {
|
|
_empty_dma_halves = 0;
|
|
// prime the second half as well so the first full DMA cycle plays valid
|
|
// data and the half tracking starts aligned (ignore the result: if no
|
|
// more data is available it is filled with silence)
|
|
audio_update_dma_buffer(1);
|
|
#if defined(AUDIO_MUTE_GPIO)
|
|
audioUnmute();
|
|
#endif
|
|
dac_start_dma();
|
|
} else {
|
|
#if defined(AUDIO_MUTE_GPIO)
|
|
audioMute();
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
extern "C" void AUDIO_DMA_Stream_IRQHandler()
|
|
{
|
|
bool hasData = false;
|
|
if(stm32_dma_check_ht_flag(AUDIO_DMA, AUDIO_DMA_Stream)) {
|
|
if (audio_update_dma_buffer(0)) {
|
|
if (_empty_dma_halves < 0xFF) _empty_dma_halves++;
|
|
} else {
|
|
hasData = true;
|
|
}
|
|
}
|
|
|
|
if(stm32_dma_check_tc_flag(AUDIO_DMA, AUDIO_DMA_Stream)) {
|
|
if (audio_update_dma_buffer(1)) {
|
|
if (_empty_dma_halves < 0xFF) _empty_dma_halves++;
|
|
} else {
|
|
hasData = true;
|
|
}
|
|
}
|
|
|
|
if (hasData) {
|
|
_empty_dma_halves = 0;
|
|
}
|
|
|
|
if(_empty_dma_halves >= DMA_EMPTY_HALVES_STOP_THRESHOLD) {
|
|
_empty_dma_halves = 0;
|
|
dac_close_dma_xfer();
|
|
}
|
|
}
|
|
|
|
#else // STM32H5 || STM32H7 || STM32H7RS
|
|
|
|
#define AUDIO_DAC DAC
|
|
#define DAC_TRIGGER 0 // TIM6
|
|
|
|
const AudioBuffer * nextBuffer = nullptr;
|
|
|
|
static void dac_dma_init()
|
|
{
|
|
stm32_dma_enable_clock(AUDIO_DMA);
|
|
|
|
// Disable DMA stream
|
|
AUDIO_DMA_Stream->CR &= ~DMA_SxCR_EN;
|
|
|
|
// clear DMA stream flags
|
|
AUDIO_DMA->HIFCR = DMA_HIFCR_CTCIF5 | DMA_HIFCR_CHTIF5 | DMA_HIFCR_CTEIF5 |
|
|
DMA_HIFCR_CDMEIF5 | DMA_HIFCR_CFEIF5;
|
|
|
|
// Chan 7, 16-bit wide, Medium priority, memory increments
|
|
AUDIO_DMA_Stream->CR = DMA_SxCR_CHSEL_0 | DMA_SxCR_CHSEL_1 |
|
|
DMA_SxCR_CHSEL_2 | DMA_SxCR_PL_0 | DMA_SxCR_MSIZE_0 |
|
|
DMA_SxCR_PSIZE_0 | DMA_SxCR_MINC | DMA_SxCR_DIR_0;
|
|
|
|
// write to DAC channel 1 (12 bits, left-aligned)
|
|
AUDIO_DMA_Stream->PAR = CONVERT_PTR_UINT(&AUDIO_DAC->DHR12L1);
|
|
|
|
// disable direct mode and set FIFO threshold to half
|
|
AUDIO_DMA_Stream->FCR = DMA_SxFCR_DMDIS | DMA_SxFCR_FTH_0;
|
|
|
|
NVIC_EnableIRQ(AUDIO_DMA_Stream_IRQn);
|
|
NVIC_SetPriority(AUDIO_DMA_Stream_IRQn, 7);
|
|
}
|
|
|
|
static void dac_close_dma_xfer()
|
|
{
|
|
// disable transfer complete interrupt
|
|
AUDIO_DMA_Stream->CR &= ~DMA_SxCR_TCIE;
|
|
|
|
// reset DMA flags
|
|
AUDIO_DMA->HIFCR = DMA_HIFCR_CTCIF5 | DMA_HIFCR_CHTIF5 | DMA_HIFCR_CTEIF5 |
|
|
DMA_HIFCR_CDMEIF5 | DMA_HIFCR_CFEIF5;
|
|
|
|
// Disable DMA stream
|
|
AUDIO_DMA_Stream->CR &= ~DMA_SxCR_EN;
|
|
}
|
|
|
|
static void dac_start_dma_request(const audio_data_t* data, uint16_t length)
|
|
{
|
|
// set DMA buffer
|
|
AUDIO_DMA_Stream->M0AR = CONVERT_PTR_UINT(data);
|
|
AUDIO_DMA_Stream->NDTR = length;
|
|
|
|
// enable DMA stream and transfer complete interrupt
|
|
AUDIO_DMA_Stream->CR |= DMA_SxCR_EN | DMA_SxCR_TCIE;
|
|
|
|
// clear underrun flag
|
|
AUDIO_DAC->SR = DAC_SR_DMAUDR1;
|
|
|
|
// enable DAC
|
|
AUDIO_DAC->CR |= DAC_CR_EN1 | DAC_CR_DMAEN1;
|
|
}
|
|
|
|
void audioConsumeCurrentBuffer()
|
|
{
|
|
if (!nextBuffer) {
|
|
nextBuffer = audioQueue.buffersFifo.getNextFilledBuffer();
|
|
if (nextBuffer) {
|
|
|
|
#if defined(AUDIO_MUTE_GPIO)
|
|
audioUnmute();
|
|
#endif
|
|
dac_close_dma_xfer();
|
|
dac_start_dma_request(nextBuffer->data, nextBuffer->size);
|
|
} else {
|
|
#if defined(AUDIO_MUTE_GPIO)
|
|
audioMute();
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
extern "C" void AUDIO_DMA_Stream_IRQHandler()
|
|
{
|
|
dac_close_dma_xfer();
|
|
|
|
// get next buffer if any
|
|
if (nextBuffer) audioQueue.buffersFifo.freeNextFilledBuffer();
|
|
nextBuffer = audioQueue.buffersFifo.getNextFilledBuffer();
|
|
|
|
if (nextBuffer) {
|
|
dac_start_dma_request(nextBuffer->data, nextBuffer->size);
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
// Init timer that triggers the DAC conversion
|
|
// at sampling frequency
|
|
static void dac_trigger_init()
|
|
{
|
|
stm32_timer_enable_clock(AUDIO_TIMER);
|
|
|
|
// set timer to: 10 uS, 100 kHz
|
|
AUDIO_TIMER->PSC = 0;
|
|
AUDIO_TIMER->ARR = (PERI1_FREQUENCY * TIMER_MULT_APB1) / 100000 - 1;
|
|
|
|
// reset counter
|
|
AUDIO_TIMER->CNT = 0;
|
|
|
|
// set expiry to sample period (1 / sample_rate)
|
|
AUDIO_TIMER->ARR =
|
|
(PERI1_FREQUENCY * TIMER_MULT_APB1) / AUDIO_SAMPLE_RATE - 1;
|
|
|
|
// Master mode selection
|
|
//
|
|
// 000: Reset
|
|
AUDIO_TIMER->CR2 = 0;
|
|
// 010: The update event is selected as trigger output (TRGO)
|
|
// (used to trigger DAC conversion)
|
|
AUDIO_TIMER->CR2 = TIM_CR2_MMS_1;
|
|
|
|
// enable timer
|
|
AUDIO_TIMER->CR1 |= TIM_CR1_CEN;
|
|
}
|
|
|
|
static void dac_periph_init()
|
|
{
|
|
#if defined(LL_APB1_GRP1_PERIPH_DAC12)
|
|
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_DAC12);
|
|
#else
|
|
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_DAC1);
|
|
#endif
|
|
|
|
gpio_init_analog(AUDIO_OUTPUT_GPIO);
|
|
|
|
// set data registre to silence
|
|
AUDIO_DAC->DHR12L1 = AUDIO_DATA_SILENCE;
|
|
|
|
// clear underrun flag
|
|
AUDIO_DAC->SR = DAC_SR_DMAUDR1;
|
|
|
|
// use TIM6 TRGO as trigger
|
|
// enable DAC & channel 1 trigger
|
|
AUDIO_DAC->CR = DAC_CR_TEN1 | DAC_CR_EN1 | DAC_TRIGGER;
|
|
}
|
|
|
|
// Configure DAC0
|
|
// Not sure why PB14 has not be allocated to the DAC, although it is an EXTRA function
|
|
// So maybe it is automatically done
|
|
void dacInit()
|
|
{
|
|
#if defined(AUDIO_MUTE_GPIO)
|
|
// Mute before init anything
|
|
gpio_init(AUDIO_MUTE_GPIO, GPIO_OUT, GPIO_PIN_SPEED_LOW);
|
|
set_mute_pin(true);
|
|
#endif
|
|
|
|
dac_trigger_init();
|
|
dac_dma_init();
|
|
dac_periph_init();
|
|
}
|
|
|
|
// Sound routines
|
|
void audioInit()
|
|
{
|
|
dacInit();
|
|
}
|