387 lines
12 KiB
C
387 lines
12 KiB
C
/*
|
|
* This file is part of the MicroPython project, http://micropython.org/
|
|
*
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2017 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 <stdint.h>
|
|
#include <string.h>
|
|
|
|
#include "extmod/vfs_fat.h"
|
|
#include "py/gc.h"
|
|
#include "py/mperrno.h"
|
|
#include "py/runtime.h"
|
|
#include "common-hal/audiobusio/I2SOut.h"
|
|
#include "shared-bindings/audiobusio/I2SOut.h"
|
|
#include "shared-bindings/audioio/RawSample.h"
|
|
#include "shared-bindings/microcontroller/Pin.h"
|
|
#include "supervisor/shared/translate.h"
|
|
|
|
#include "atmel_start_pins.h"
|
|
#include "hal/include/hal_gpio.h"
|
|
#include "hpl/gclk/hpl_gclk_base.h"
|
|
#include "peripheral_clk_config.h"
|
|
|
|
#ifdef SAMD21
|
|
#include "hpl/pm/hpl_pm_base.h"
|
|
#endif
|
|
|
|
#include "samd/clocks.h"
|
|
#include "samd/dma.h"
|
|
#include "samd/events.h"
|
|
#include "samd/i2s.h"
|
|
#include "samd/pins.h"
|
|
#include "samd/timers.h"
|
|
|
|
#include "audio_dma.h"
|
|
|
|
#ifdef SAMD21
|
|
#define SERCTRL(name) I2S_SERCTRL_ ## name
|
|
#endif
|
|
|
|
#ifdef SAMD51
|
|
#define SERCTRL(name) I2S_TXCTRL_ ## name
|
|
#endif
|
|
|
|
void i2sout_reset(void) {
|
|
// Make sure the I2S peripheral is running so we can see if the resources we need are free.
|
|
#ifdef SAMD51
|
|
// Connect the clock units to the 2mhz clock. It can't disable without it.
|
|
connect_gclk_to_peripheral(5, I2S_GCLK_ID_0);
|
|
connect_gclk_to_peripheral(5, I2S_GCLK_ID_1);
|
|
#endif
|
|
if (I2S->CTRLA.bit.ENABLE == 1) {
|
|
I2S->CTRLA.bit.ENABLE = 0;
|
|
while (I2S->SYNCBUSY.bit.ENABLE == 1) {}
|
|
}
|
|
|
|
// Make sure the I2S peripheral is running so we can see if the resources we need are free.
|
|
#ifdef SAMD51
|
|
// Connect the clock units to the 2mhz clock by default. They can't reset without it.
|
|
disconnect_gclk_from_peripheral(5, I2S_GCLK_ID_0);
|
|
disconnect_gclk_from_peripheral(5, I2S_GCLK_ID_1);
|
|
|
|
hri_mclk_clear_APBDMASK_I2S_bit(MCLK);
|
|
#endif
|
|
|
|
#ifdef SAMD21
|
|
_pm_disable_bus_clock(PM_BUS_APBC, I2S);
|
|
#endif
|
|
}
|
|
|
|
void common_hal_audiobusio_i2sout_construct(audiobusio_i2sout_obj_t* self,
|
|
const mcu_pin_obj_t* bit_clock, const mcu_pin_obj_t* word_select,
|
|
const mcu_pin_obj_t* data, bool left_justified) {
|
|
uint8_t serializer = 0xff;
|
|
uint8_t bc_clock_unit = 0xff;
|
|
uint8_t ws_clock_unit = 0xff;
|
|
#ifdef SAMD21
|
|
if (bit_clock == &pin_PA10
|
|
#ifdef PIN_PA20
|
|
|| bit_clock == &pin_PA20
|
|
#endif
|
|
) { // I2S SCK[0]
|
|
bc_clock_unit = 0;
|
|
}
|
|
#ifdef PIN_PB11
|
|
else if (bit_clock == &pin_PB11) { // I2S SCK[1]
|
|
bc_clock_unit = 1;
|
|
}
|
|
#endif
|
|
if (word_select == &pin_PA11
|
|
#ifdef PIN_PA21
|
|
|| word_select == &pin_PA21
|
|
#endif
|
|
) { // I2S FS[0]
|
|
ws_clock_unit = 0;
|
|
}
|
|
#ifdef PIN_PB12
|
|
else if (word_select == &pin_PB12) { // I2S FS[1]
|
|
ws_clock_unit = 1;
|
|
}
|
|
#endif
|
|
|
|
if (data == &pin_PA07 || data == &pin_PA19) { // I2S SD[0]
|
|
serializer = 0;
|
|
} else if (data == &pin_PA08
|
|
#ifdef PIN_PB16
|
|
|| data == &pin_PB16
|
|
#endif
|
|
) { // I2S SD[1]
|
|
serializer = 1;
|
|
}
|
|
#endif
|
|
#ifdef SAMD51
|
|
// Only clock unit 0 can be used for transmission.
|
|
if (bit_clock == &pin_PA10 || bit_clock == &pin_PB16) { // I2S SCK[0]
|
|
bc_clock_unit = 0;
|
|
}
|
|
if (word_select == &pin_PA09 || word_select == &pin_PA20) { // I2S FS[0]
|
|
ws_clock_unit = 0;
|
|
}
|
|
if (data == &pin_PA11 || data == &pin_PA21) { // I2S SDO
|
|
serializer = 0;
|
|
}
|
|
#endif
|
|
if (bc_clock_unit == 0xff) {
|
|
mp_raise_ValueError_varg(translate("Invalid %q pin"), MP_QSTR_bit_clock);
|
|
}
|
|
if (ws_clock_unit == 0xff) {
|
|
mp_raise_ValueError_varg(translate("Invalid %q pin"), MP_QSTR_word_select);
|
|
}
|
|
if (bc_clock_unit != ws_clock_unit) {
|
|
mp_raise_ValueError(translate("Bit clock and word select must share a clock unit"));
|
|
}
|
|
if (serializer == 0xff) {
|
|
mp_raise_ValueError_varg(translate("Invalid %q pin"), MP_QSTR_data);
|
|
}
|
|
self->clock_unit = ws_clock_unit;
|
|
self->serializer = serializer;
|
|
|
|
turn_on_i2s();
|
|
|
|
if (I2S->CTRLA.bit.ENABLE == 0) {
|
|
I2S->CTRLA.bit.SWRST = 1;
|
|
while (I2S->CTRLA.bit.SWRST == 1) {}
|
|
} else {
|
|
#ifdef SAMD21
|
|
if ((I2S->CTRLA.vec.SEREN & (1 << serializer)) != 0) {
|
|
mp_raise_RuntimeError(translate("Serializer in use"));
|
|
}
|
|
#endif
|
|
#ifdef SAMD51
|
|
if (I2S->CTRLA.bit.TXEN == 1) {
|
|
mp_raise_RuntimeError(translate("Serializer in use"));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#ifdef SAMD51
|
|
#define GPIO_I2S_FUNCTION GPIO_PIN_FUNCTION_J
|
|
#endif
|
|
#ifdef SAMD21
|
|
#define GPIO_I2S_FUNCTION GPIO_PIN_FUNCTION_G
|
|
#endif
|
|
assert_pin_free(bit_clock);
|
|
assert_pin_free(word_select);
|
|
assert_pin_free(data);
|
|
|
|
self->bit_clock = bit_clock;
|
|
self->word_select = word_select;
|
|
self->data = data;
|
|
|
|
claim_pin(bit_clock);
|
|
claim_pin(word_select);
|
|
claim_pin(data);
|
|
|
|
gpio_set_pin_function(self->bit_clock->number, GPIO_I2S_FUNCTION);
|
|
gpio_set_pin_function(self->word_select->number, GPIO_I2S_FUNCTION);
|
|
gpio_set_pin_function(self->data->number, GPIO_I2S_FUNCTION);
|
|
|
|
self->left_justified = left_justified;
|
|
self->playing = false;
|
|
audio_dma_init(&self->dma);
|
|
}
|
|
|
|
bool common_hal_audiobusio_i2sout_deinited(audiobusio_i2sout_obj_t* self) {
|
|
return self->bit_clock == mp_const_none;
|
|
}
|
|
|
|
void common_hal_audiobusio_i2sout_deinit(audiobusio_i2sout_obj_t* self) {
|
|
if (common_hal_audiobusio_i2sout_deinited(self)) {
|
|
return;
|
|
}
|
|
|
|
reset_pin_number(self->bit_clock->number);
|
|
self->bit_clock = mp_const_none;
|
|
reset_pin_number(self->word_select->number);
|
|
self->word_select = mp_const_none;
|
|
reset_pin_number(self->data->number);
|
|
self->data = mp_const_none;
|
|
}
|
|
|
|
void common_hal_audiobusio_i2sout_play(audiobusio_i2sout_obj_t* self,
|
|
mp_obj_t sample, bool loop) {
|
|
if (common_hal_audiobusio_i2sout_get_playing(self)) {
|
|
common_hal_audiobusio_i2sout_stop(self);
|
|
}
|
|
#ifdef SAMD21
|
|
if ((I2S->CTRLA.vec.CKEN & (1 << self->clock_unit)) == 1) {
|
|
mp_raise_RuntimeError(translate("Clock unit in use"));
|
|
}
|
|
#endif
|
|
uint8_t bits_per_sample = audiosample_bits_per_sample(sample);
|
|
// We always output stereo so output twice as many bits.
|
|
uint16_t bits_per_sample_output = bits_per_sample * 2;
|
|
uint16_t divisor = 48000000 / (bits_per_sample_output * audiosample_sample_rate(sample));
|
|
// Find a free GCLK to generate the MCLK signal.
|
|
uint8_t gclk = find_free_gclk(divisor);
|
|
if (gclk > GCLK_GEN_NUM) {
|
|
mp_raise_RuntimeError(translate("Unable to find free GCLK"));
|
|
}
|
|
self->gclk = gclk;
|
|
|
|
uint32_t clkctrl = I2S_CLKCTRL_MCKSEL_GCLK |
|
|
I2S_CLKCTRL_NBSLOTS(1) |
|
|
I2S_CLKCTRL_FSWIDTH_HALF;
|
|
if (self->left_justified) {
|
|
clkctrl |= I2S_CLKCTRL_BITDELAY_LJ;
|
|
} else {
|
|
clkctrl |= I2S_CLKCTRL_FSOUTINV | I2S_CLKCTRL_BITDELAY_I2S;
|
|
}
|
|
uint8_t channel_count = audiosample_channel_count(sample);
|
|
if (channel_count > 2) {
|
|
mp_raise_ValueError(translate("Too many channels in sample."));
|
|
}
|
|
#ifdef SAMD21
|
|
uint32_t serctrl = (self->clock_unit << I2S_SERCTRL_CLKSEL_Pos) | SERCTRL(SERMODE_TX) | I2S_SERCTRL_TXSAME_SAME | I2S_SERCTRL_EXTEND_MSBIT | I2S_SERCTRL_TXDEFAULT_ONE | I2S_SERCTRL_SLOTADJ_LEFT;
|
|
#endif
|
|
#ifdef SAMD51
|
|
uint32_t serctrl = (self->clock_unit << I2S_RXCTRL_CLKSEL_Pos) | I2S_TXCTRL_TXSAME_SAME;
|
|
#endif
|
|
if (audiosample_channel_count(sample) == 1) {
|
|
serctrl |= SERCTRL(MONO_MONO);
|
|
} else {
|
|
serctrl |= SERCTRL(MONO_STEREO);
|
|
}
|
|
if (bits_per_sample == 8) {
|
|
serctrl |= SERCTRL(DATASIZE_8C);
|
|
clkctrl |= I2S_CLKCTRL_SLOTSIZE_8;
|
|
} else if (bits_per_sample == 16) {
|
|
serctrl |= SERCTRL(DATASIZE_16C);
|
|
clkctrl |= I2S_CLKCTRL_SLOTSIZE_16;
|
|
}
|
|
|
|
// Configure the I2S peripheral
|
|
i2s_set_enable(false);
|
|
|
|
I2S->CLKCTRL[self->clock_unit].reg = clkctrl;
|
|
#ifdef SAMD21
|
|
I2S->SERCTRL[self->serializer].reg = serctrl;
|
|
#endif
|
|
#ifdef SAMD51
|
|
I2S->TXCTRL.reg = serctrl;
|
|
#endif
|
|
|
|
// The DFLL is always a 48mhz clock
|
|
enable_clock_generator(self->gclk, CLOCK_48MHZ, divisor);
|
|
connect_gclk_to_peripheral(self->gclk, I2S_GCLK_ID_0 + self->clock_unit);
|
|
|
|
i2s_set_enable(true);
|
|
|
|
#ifdef SAMD21
|
|
uint32_t tx_register = (uint32_t) &I2S->DATA[self->serializer].reg;
|
|
uint8_t dmac_id = I2S_DMAC_ID_TX_0 + self->serializer;
|
|
#endif
|
|
#ifdef SAMD51
|
|
uint32_t tx_register = (uint32_t) &I2S->TXDATA.reg;
|
|
uint8_t dmac_id = I2S_DMAC_ID_TX_0;
|
|
#endif
|
|
audio_dma_result result = audio_dma_setup_playback(&self->dma, sample, loop, false, 0,
|
|
true /* output signed */, tx_register, dmac_id);
|
|
|
|
if (result == AUDIO_DMA_DMA_BUSY) {
|
|
common_hal_audiobusio_i2sout_stop(self);
|
|
mp_raise_RuntimeError(translate("No DMA channel found"));
|
|
} else if (result == AUDIO_DMA_MEMORY_ERROR) {
|
|
common_hal_audiobusio_i2sout_stop(self);
|
|
mp_raise_RuntimeError(translate("Unable to allocate buffers for signed conversion"));
|
|
}
|
|
|
|
I2S->INTFLAG.reg = I2S_INTFLAG_TXUR0 | I2S_INTFLAG_TXUR1;
|
|
|
|
I2S->CTRLA.vec.CKEN = 1 << self->clock_unit;
|
|
while ((I2S->SYNCBUSY.vec.CKEN & (1 << self->clock_unit)) != 0) {}
|
|
|
|
// Init the serializer after the clock. Otherwise, it will never enable because its unclocked.
|
|
#ifdef SAMD21
|
|
I2S->CTRLA.vec.SEREN = 1 << self->serializer;
|
|
while ((I2S->SYNCBUSY.vec.SEREN & (1 << self->serializer)) != 0) {}
|
|
#endif
|
|
#ifdef SAMD51
|
|
I2S->CTRLA.bit.TXEN = 1;
|
|
while (I2S->SYNCBUSY.bit.TXEN == 1) {}
|
|
#endif
|
|
|
|
self->playing = true;
|
|
}
|
|
|
|
void common_hal_audiobusio_i2sout_pause(audiobusio_i2sout_obj_t* self) {
|
|
audio_dma_pause(&self->dma);
|
|
}
|
|
|
|
void common_hal_audiobusio_i2sout_resume(audiobusio_i2sout_obj_t* self) {
|
|
// Clear any overrun/underrun errors
|
|
#ifdef SAMD21
|
|
I2S->INTFLAG.reg = I2S_INTFLAG_TXUR0 << self->serializer;
|
|
#endif
|
|
#ifdef SAMD51
|
|
I2S->INTFLAG.reg = I2S_INTFLAG_TXUR0 | I2S_INTFLAG_TXUR1;
|
|
#endif
|
|
|
|
audio_dma_resume(&self->dma);
|
|
}
|
|
|
|
bool common_hal_audiobusio_i2sout_get_paused(audiobusio_i2sout_obj_t* self) {
|
|
return audio_dma_get_paused(&self->dma);
|
|
}
|
|
|
|
void common_hal_audiobusio_i2sout_stop(audiobusio_i2sout_obj_t* self) {
|
|
audio_dma_stop(&self->dma);
|
|
|
|
#ifdef SAMD21
|
|
I2S->CTRLA.vec.SEREN &= ~(1 << self->serializer);
|
|
while ((I2S->SYNCBUSY.vec.SEREN & (1 << self->serializer)) != 0) {}
|
|
#endif
|
|
#ifdef SAMD51
|
|
I2S->CTRLA.bit.TXEN = 0;
|
|
while (I2S->SYNCBUSY.bit.TXEN == 1) {}
|
|
#endif
|
|
|
|
#ifdef SAMD21
|
|
if (self->clock_unit == 0) {
|
|
I2S->CTRLA.bit.CKEN0 = 0;
|
|
while (I2S->SYNCBUSY.bit.CKEN0 == 1) {}
|
|
} else {
|
|
I2S->CTRLA.bit.CKEN1 = 0;
|
|
while (I2S->SYNCBUSY.bit.CKEN1 == 1) {}
|
|
}
|
|
#endif
|
|
disconnect_gclk_from_peripheral(self->gclk, I2S_GCLK_ID_0 + self->clock_unit);
|
|
disable_clock_generator(self->gclk);
|
|
|
|
#ifdef SAMD51
|
|
connect_gclk_to_peripheral(5, I2S_GCLK_ID_0 + self->clock_unit);
|
|
#endif
|
|
|
|
self->playing = false;
|
|
}
|
|
|
|
bool common_hal_audiobusio_i2sout_get_playing(audiobusio_i2sout_obj_t* self) {
|
|
bool still_playing = audio_dma_get_playing(&self->dma);
|
|
if (self->playing && !still_playing) {
|
|
common_hal_audiobusio_i2sout_stop(self);
|
|
}
|
|
return still_playing;
|
|
}
|