circuitpython/ports/atmel-samd/audio_dma.c

438 lines
15 KiB
C

/*
* This file is part of the MicroPython project, http://micropython.org/
*
* The MIT License (MIT)
*
* Copyright (c) 2018 Scott Shawcroft for Adafruit Industries
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include <string.h>
#include "audio_dma.h"
#include "samd/clocks.h"
#include "samd/events.h"
#include "samd/dma.h"
#include "shared-bindings/audiocore/RawSample.h"
#include "shared-bindings/audiocore/WaveFile.h"
#include "shared-bindings/microcontroller/__init__.h"
#include "supervisor/background_callback.h"
#include "py/mpstate.h"
#include "py/runtime.h"
#if CIRCUITPY_AUDIOIO || CIRCUITPY_AUDIOBUSIO
// Flag value for dma->buffer_to_load, indicating there is nothing to do.
// Otherwise dma->buffer_to_load is 0 or 1.
#define NO_BUFFER_TO_LOAD 0xff
static audio_dma_t *audio_dma_state[AUDIO_DMA_CHANNEL_COUNT];
// This cannot be in audio_dma_state because it's volatile.
static volatile bool audio_dma_pending[AUDIO_DMA_CHANNEL_COUNT];
static bool audio_dma_allocated[AUDIO_DMA_CHANNEL_COUNT];
uint8_t find_sync_event_channel_raise() {
uint8_t event_channel = find_sync_event_channel();
if (event_channel >= EVSYS_SYNCH_NUM) {
mp_raise_RuntimeError(translate("All sync event channels in use"));
}
return event_channel;
}
uint8_t dma_allocate_channel(void) {
uint8_t channel;
for (channel = 0; channel < AUDIO_DMA_CHANNEL_COUNT; channel++) {
if (!audio_dma_allocated[channel]) {
audio_dma_allocated[channel] = true;
return channel;
}
}
return channel; // i.e., return failure
}
void dma_free_channel(uint8_t channel) {
assert(channel < AUDIO_DMA_CHANNEL_COUNT);
assert(audio_dma_allocated[channel]);
audio_dma_disable_channel(channel);
audio_dma_allocated[channel] = false;
}
void audio_dma_disable_channel(uint8_t channel) {
if (channel >= AUDIO_DMA_CHANNEL_COUNT) {
return;
}
dma_disable_channel(channel);
}
void audio_dma_enable_channel(uint8_t channel) {
if (channel >= AUDIO_DMA_CHANNEL_COUNT) {
return;
}
dma_enable_channel(channel);
}
static void audio_dma_convert_samples(
audio_dma_t *dma,
uint8_t *input, uint32_t input_length,
uint8_t *available_output_buffer, uint32_t available_output_buffer_length,
uint8_t **output, uint32_t *output_length,
uint8_t *output_spacing) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-align"
if (dma->signed_to_unsigned || dma->unsigned_to_signed) {
// Must convert.
// Write the conversion into the passed-in output buffer
*output = available_output_buffer;
*output_length = input_length / dma->spacing;
*output_spacing = 1;
if (*output_length > available_output_buffer_length) {
mp_raise_RuntimeError(translate("Internal audio buffer too small"));
}
uint32_t out_i = 0;
if (dma->bytes_per_sample == 1) {
for (uint32_t i = 0; i < input_length; i += dma->spacing) {
if (dma->signed_to_unsigned) {
((uint8_t *)*output)[out_i] = ((int8_t *)input)[i] + 0x80;
} else {
((int8_t *)*output)[out_i] = ((uint8_t *)input)[i] - 0x80;
}
out_i += 1;
}
} else if (dma->bytes_per_sample == 2) {
for (uint32_t i = 0; i < input_length / 2; i += dma->spacing) {
if (dma->signed_to_unsigned) {
((uint16_t *)*output)[out_i] = ((int16_t *)input)[i] + 0x8000;
} else {
((int16_t *)*output)[out_i] = ((uint16_t *)input)[i] - 0x8000;
}
out_i += 1;
}
}
} else {
*output = input;
*output_length = input_length;
*output_spacing = dma->spacing;
}
#pragma GCC diagnostic pop
}
static void audio_dma_load_next_block(audio_dma_t *dma, size_t buffer_idx) {
uint8_t *sample_buffer;
uint32_t sample_buffer_length;
audioio_get_buffer_result_t get_buffer_result =
audiosample_get_buffer(dma->sample, dma->single_channel_output, dma->audio_channel,
&sample_buffer, &sample_buffer_length);
DmacDescriptor *descriptor = dma->descriptor[buffer_idx];
if (get_buffer_result == GET_BUFFER_ERROR) {
audio_dma_stop(dma);
return;
}
// Use one of the allocated buffers for conversion. But if there's no conversion,
// this will be set to buffer in audio_dma_convert_samples() to avoid any copying.
uint8_t *output_buffer;
uint32_t output_buffer_length;
uint8_t output_spacing;
audio_dma_convert_samples(dma, sample_buffer, sample_buffer_length,
// Available output buffer: may be used or not.
dma->buffer[buffer_idx], dma->buffer_length[buffer_idx],
// Buffer where output was placed.
&output_buffer, &output_buffer_length,
&output_spacing);
descriptor->BTCNT.reg = output_buffer_length / dma->beat_size / output_spacing;
descriptor->SRCADDR.reg = ((uint32_t)output_buffer) + output_buffer_length;
if (get_buffer_result == GET_BUFFER_DONE) {
if (dma->loop) {
audiosample_reset_buffer(dma->sample, dma->single_channel_output, dma->audio_channel);
} else {
if (output_buffer_length == 0) {
// Nothing further to read and previous buffer is finished.
audio_dma_stop(dma);
return;
} else {
// Break descriptor chain.
descriptor->DESCADDR.reg = 0;
}
}
}
descriptor->BTCTRL.bit.VALID = true;
}
static void setup_audio_descriptor(DmacDescriptor *descriptor, uint8_t beat_size,
uint8_t spacing, uint32_t output_register_address) {
uint32_t beat_size_reg = DMAC_BTCTRL_BEATSIZE_BYTE;
if (beat_size == 2) {
beat_size_reg = DMAC_BTCTRL_BEATSIZE_HWORD;
} else if (beat_size == 4) {
beat_size_reg = DMAC_BTCTRL_BEATSIZE_WORD;
}
descriptor->BTCTRL.reg = beat_size_reg |
DMAC_BTCTRL_SRCINC |
DMAC_BTCTRL_EVOSEL_BLOCK |
DMAC_BTCTRL_STEPSIZE(spacing - 1) |
DMAC_BTCTRL_STEPSEL_SRC;
descriptor->DSTADDR.reg = output_register_address;
}
// Playback should be shutdown before calling this.
audio_dma_result audio_dma_setup_playback(audio_dma_t *dma,
mp_obj_t sample,
bool loop,
bool single_channel_output,
uint8_t audio_channel,
bool output_signed,
uint32_t output_register_address,
uint8_t dma_trigger_source) {
uint8_t dma_channel = dma_allocate_channel();
if (dma_channel >= AUDIO_DMA_CHANNEL_COUNT) {
return AUDIO_DMA_DMA_BUSY;
}
dma->sample = sample;
dma->loop = loop;
dma->single_channel_output = single_channel_output;
dma->audio_channel = audio_channel;
dma->dma_channel = dma_channel;
dma->signed_to_unsigned = false;
dma->unsigned_to_signed = false;
dma->spacing = 1;
audiosample_reset_buffer(sample, single_channel_output, audio_channel);
dma->buffer_to_load = NO_BUFFER_TO_LOAD;
dma->descriptor[0] = dma_descriptor(dma_channel);
dma->descriptor[1] = &dma->second_descriptor;
bool samples_signed;
uint32_t max_buffer_length;
audiosample_get_buffer_structure(sample, single_channel_output, &dma->single_buffer, &samples_signed,
&max_buffer_length, &dma->spacing);
uint8_t output_spacing = dma->spacing;
if (output_signed != samples_signed) {
output_spacing = 1;
max_buffer_length /= dma->spacing;
}
dma->buffer[0] = (uint8_t *)m_realloc(dma->buffer[0], max_buffer_length);
dma->buffer_length[0] = max_buffer_length;
if (dma->buffer[0] == NULL) {
return AUDIO_DMA_MEMORY_ERROR;
}
if (!dma->single_buffer) {
dma->buffer[1] = (uint8_t *)m_realloc(dma->buffer[1], max_buffer_length);
dma->buffer_length[1] = max_buffer_length;
if (dma->buffer[1] == NULL) {
return AUDIO_DMA_MEMORY_ERROR;
}
}
dma->signed_to_unsigned = !output_signed && samples_signed;
dma->unsigned_to_signed = output_signed && !samples_signed;
dma->event_channel = 0xff;
if (!dma->single_buffer) {
// We're likely double buffering so set up the block interrupts.
turn_on_event_system();
dma->event_channel = find_sync_event_channel_raise();
init_event_channel_interrupt(dma->event_channel, CORE_GCLK, EVSYS_ID_GEN_DMAC_CH_0 + dma_channel);
// We keep the audio_dma_t for internal use and the sample as a root pointer because it
// contains the audiodma structure.
audio_dma_state[dma->dma_channel] = dma;
MP_STATE_PORT(playing_audio)[dma->dma_channel] = dma->sample;
}
if (audiosample_bits_per_sample(sample) == 16) {
dma->beat_size = 2;
dma->bytes_per_sample = 2;
} else {
dma->beat_size = 1;
dma->bytes_per_sample = 1;
if (single_channel_output) {
output_register_address += 1;
}
}
// Transfer both channels at once.
if (!single_channel_output && audiosample_channel_count(sample) == 2) {
dma->beat_size *= 2;
}
#ifdef SAM_D5X_E5X
int irq = dma->event_channel < 4 ? EVSYS_0_IRQn + dma->event_channel : EVSYS_4_IRQn;
// Only disable and clear on SAMD51 because the SAMD21 shares EVSYS with ticks.
NVIC_DisableIRQ(irq);
NVIC_ClearPendingIRQ(irq);
#else
int irq = EVSYS_IRQn;
#endif
setup_audio_descriptor(dma->descriptor[0], dma->beat_size, output_spacing, output_register_address);
if (dma->single_buffer) {
dma->descriptor[0]->DESCADDR.reg = 0;
if (dma->loop) {
// The descriptor chains to itself.
dma->descriptor[0]->DESCADDR.reg = (uint32_t)dma->descriptor[0];
}
} else {
// Set up the two descriptors to chain to each other.
dma->descriptor[0]->DESCADDR.reg = (uint32_t)dma->descriptor[1];
setup_audio_descriptor(dma->descriptor[1], dma->beat_size, output_spacing, output_register_address);
dma->descriptor[1]->DESCADDR.reg = (uint32_t)dma->descriptor[0];
}
// Load the first two blocks up front.
audio_dma_load_next_block(dma, 0);
if (!dma->single_buffer) {
audio_dma_load_next_block(dma, 1);
}
dma->playing_in_progress = true;
dma_configure(dma_channel, dma_trigger_source, true);
audio_dma_enable_channel(dma_channel);
NVIC_EnableIRQ(irq);
return AUDIO_DMA_OK;
}
void audio_dma_stop(audio_dma_t *dma) {
uint8_t channel = dma->dma_channel;
if (channel < AUDIO_DMA_CHANNEL_COUNT) {
audio_dma_disable_channel(channel);
disable_event_channel(dma->event_channel);
MP_STATE_PORT(playing_audio)[channel] = NULL;
audio_dma_state[channel] = NULL;
dma_free_channel(dma->dma_channel);
}
dma->dma_channel = AUDIO_DMA_CHANNEL_COUNT;
dma->playing_in_progress = false;
}
void audio_dma_pause(audio_dma_t *dma) {
dma_suspend_channel(dma->dma_channel);
}
void audio_dma_resume(audio_dma_t *dma) {
dma_resume_channel(dma->dma_channel);
}
bool audio_dma_get_paused(audio_dma_t *dma) {
if (dma->dma_channel >= AUDIO_DMA_CHANNEL_COUNT) {
return false;
}
uint32_t status = dma_transfer_status(dma->dma_channel);
return (status & DMAC_CHINTFLAG_SUSP) != 0;
}
void audio_dma_init(audio_dma_t *dma) {
dma->dma_channel = AUDIO_DMA_CHANNEL_COUNT;
}
void audio_dma_reset(void) {
for (uint8_t i = 0; i < AUDIO_DMA_CHANNEL_COUNT; i++) {
audio_dma_state[i] = NULL;
audio_dma_pending[i] = false;
audio_dma_allocated[i] = false;
audio_dma_disable_channel(i);
dma_descriptor(i)->BTCTRL.bit.VALID = false;
MP_STATE_PORT(playing_audio)[i] = NULL;
}
}
bool audio_dma_get_playing(audio_dma_t *dma) {
if (dma->dma_channel >= AUDIO_DMA_CHANNEL_COUNT) {
return false;
}
return dma->playing_in_progress;
}
// WARN(tannewt): DO NOT print from here, or anything it calls. Printing calls
// background tasks such as this and causes a stack overflow.
STATIC void dma_callback_fun(void *arg) {
audio_dma_t *dma = arg;
if (dma == NULL) {
return;
}
common_hal_mcu_disable_interrupts();
uint8_t buffer_to_load = dma->buffer_to_load;
dma->buffer_to_load = NO_BUFFER_TO_LOAD;
common_hal_mcu_enable_interrupts();
if (buffer_to_load == NO_BUFFER_TO_LOAD) {
audio_dma_stop(dma);
} else {
audio_dma_load_next_block(dma, buffer_to_load);
}
}
void audio_dma_evsys_handler(void) {
for (uint8_t i = 0; i < AUDIO_DMA_CHANNEL_COUNT; i++) {
audio_dma_t *dma = audio_dma_state[i];
if (dma == NULL) {
continue;
}
bool block_done = event_interrupt_active(dma->event_channel);
if (!block_done) {
continue;
}
// By the time we get here, the write-back descriptor has been set to the
// current running descriptor. Fill the buffer that the next chained descriptor
// will play.
//
// The state of the write-back descriptor was determined empirically,
// The datasheet appears to imply that the descriptor that just finished would
// be in the write-back descriptor. But the VALID bit is set in the write-back descriptor,
// and reversing which buffer to fill produces crackly output. So the choice
// of which buffer to fill here appears correct.
DmacDescriptor *next_descriptor =
(DmacDescriptor *)dma_write_back_descriptor(dma->dma_channel)->DESCADDR.reg;
if (next_descriptor == dma->descriptor[0]) {
dma->buffer_to_load = 0;
} else if (next_descriptor == dma->descriptor[1]) {
dma->buffer_to_load = 1;
} else if (next_descriptor == NULL) {
dma->buffer_to_load = NO_BUFFER_TO_LOAD;
} else {
continue;
}
background_callback_add(&dma->callback, dma_callback_fun, (void *)dma);
}
}
MP_REGISTER_ROOT_POINTER(mp_obj_t playing_audio[AUDIO_DMA_CHANNEL_COUNT]);
#endif