Files
edgetx-gx12/radio/src/targets/common/arm/stm32/audio_dac_driver.cpp
T
2026-07-16 12:30:16 +10:00

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();
}