working audio
This commit is contained in:
@@ -7,19 +7,21 @@ Features: Circular Buffer with DMA IRQ (half full, full), Master Clock enabled
|
||||
This example uses the SPI3 port tested on STM32F407VET "black" board. On other boards please define LED0_BUILTIN and LED1_BUILTIN
|
||||
*/
|
||||
#define I2S_BUFFER_SIZE 64
|
||||
I2S_HandleTypeDef hi2s3;
|
||||
DMA_HandleTypeDef hdma_spi3_tx;
|
||||
I2S_HandleTypeDef hi2s4;
|
||||
DMA_HandleTypeDef hdma_spi4_tx;
|
||||
uint32_t dma_buffer[I2S_BUFFER_SIZE];
|
||||
//int16_t sine[WAV_SIZE] = {0};
|
||||
// sinus oszillator
|
||||
float osc_phi = 0;
|
||||
float osc_phi_inc = 440.0f / 44100.0f; // generating 440HZ
|
||||
|
||||
float scale[] = {C4_HZ, D4_HZ, E4_HZ, F4_HZ, G4_HZ, A4_HZ, B4_HZ, A4_HZ, G4_HZ, F4_HZ, E4_HZ, D4_HZ, C4_HZ};
|
||||
|
||||
void MX_DMA_Init(void);
|
||||
|
||||
extern "C" void DMA1_Stream5_IRQHandler(void) // this function must be included to avoid DMA to crash!
|
||||
{
|
||||
HAL_DMA_IRQHandler(&hdma_spi3_tx);
|
||||
HAL_DMA_IRQHandler(&hdma_spi4_tx);
|
||||
}
|
||||
|
||||
void Error_Handler2(byte errorcode)
|
||||
@@ -88,17 +90,16 @@ extern "C" void HAL_I2S_TxHalfCpltCallback(I2S_HandleTypeDef *hi2s)
|
||||
// setting up I2S
|
||||
extern "C" void MX_I2S3_Init(void)
|
||||
{
|
||||
hi2s3.Instance = SPI3;
|
||||
hi2s3.Init.Mode = I2S_MODE_MASTER_TX;
|
||||
hi2s3.Init.Standard = I2S_STANDARD_PHILIPS;
|
||||
hi2s3.Init.DataFormat = I2S_DATAFORMAT_16B;
|
||||
//hi2s3.Init.MCLKOutput = I2S_MCLKOUTPUT_DISABLE;
|
||||
hi2s3.Init.MCLKOutput = I2S_MCLKOUTPUT_ENABLE;
|
||||
hi2s3.Init.AudioFreq = I2S_AUDIOFREQ_44K;
|
||||
hi2s3.Init.CPOL = I2S_CPOL_LOW;
|
||||
hi2s3.Init.ClockSource = I2S_CLOCK_PLL;
|
||||
hi2s3.Init.FullDuplexMode = I2S_FULLDUPLEXMODE_DISABLE;
|
||||
if (HAL_I2S_Init(&hi2s3) != HAL_OK)
|
||||
hi2s4.Instance = SPI4;
|
||||
hi2s4.Init.Mode = I2S_MODE_MASTER_TX;
|
||||
hi2s4.Init.Standard = I2S_STANDARD_PHILIPS;
|
||||
hi2s4.Init.DataFormat = I2S_DATAFORMAT_16B;
|
||||
hi2s4.Init.MCLKOutput = I2S_MCLKOUTPUT_DISABLE;
|
||||
hi2s4.Init.AudioFreq = I2S_AUDIOFREQ_44K;
|
||||
hi2s4.Init.CPOL = I2S_CPOL_LOW;
|
||||
hi2s4.Init.ClockSource = I2S_CLOCK_PLL;
|
||||
hi2s4.Init.FullDuplexMode = I2S_FULLDUPLEXMODE_DISABLE;
|
||||
if (HAL_I2S_Init(&hi2s4) != HAL_OK)
|
||||
{
|
||||
Error_Handler2(1); // on error: one blink
|
||||
}
|
||||
@@ -134,45 +135,51 @@ extern "C" void HAL_I2S_MspInit(I2S_HandleTypeDef *hi2s)
|
||||
PC7 MCK
|
||||
*/
|
||||
//I2S3 used GPIO configuration in this example:
|
||||
// PB5 DIN / SD
|
||||
// PA4 LRC /WD
|
||||
// PB3 SCLK /CK
|
||||
// PB5 DIN / SD ==> PA1
|
||||
// PA4 LRC /WD ==> PB12
|
||||
// PB3 SCLK /CK ==> PB13
|
||||
// PC7 MCK
|
||||
GPIO_InitStruct.Pin = GPIO_PIN_4;
|
||||
|
||||
//LRCLL
|
||||
GPIO_InitStruct.Pin = GPIO_PIN_12;
|
||||
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
|
||||
GPIO_InitStruct.Pull = GPIO_NOPULL;
|
||||
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
|
||||
GPIO_InitStruct.Alternate = GPIO_AF6_SPI3;
|
||||
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
|
||||
GPIO_InitStruct.Pin = GPIO_PIN_3 | GPIO_PIN_5;
|
||||
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
|
||||
GPIO_InitStruct.Pull = GPIO_NOPULL;
|
||||
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
|
||||
GPIO_InitStruct.Alternate = GPIO_AF6_SPI3;
|
||||
GPIO_InitStruct.Alternate = GPIO_AF6_SPI4;
|
||||
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
|
||||
// master clock
|
||||
GPIO_InitStruct.Pin = GPIO_PIN_7;
|
||||
|
||||
//DIN
|
||||
GPIO_InitStruct.Pin = GPIO_PIN_1;
|
||||
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
|
||||
GPIO_InitStruct.Pull = GPIO_NOPULL;
|
||||
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
|
||||
GPIO_InitStruct.Alternate = GPIO_AF6_SPI3;
|
||||
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
|
||||
GPIO_InitStruct.Alternate = GPIO_AF5_SPI4;
|
||||
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
|
||||
|
||||
// clock
|
||||
GPIO_InitStruct.Pin = GPIO_PIN_13;
|
||||
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
|
||||
GPIO_InitStruct.Pull = GPIO_NOPULL;
|
||||
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
|
||||
GPIO_InitStruct.Alternate = GPIO_AF6_SPI4;
|
||||
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
|
||||
|
||||
// Peripheral DMA init
|
||||
hdma_spi3_tx.Instance = DMA1_Stream5;
|
||||
hdma_spi3_tx.Init.Channel = DMA_CHANNEL_0;
|
||||
hdma_spi3_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
|
||||
hdma_spi3_tx.Init.PeriphInc = DMA_PINC_DISABLE;
|
||||
hdma_spi3_tx.Init.MemInc = DMA_MINC_ENABLE;
|
||||
hdma_spi3_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
|
||||
hdma_spi3_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
|
||||
hdma_spi3_tx.Init.Mode = DMA_CIRCULAR;
|
||||
hdma_spi3_tx.Init.Priority = DMA_PRIORITY_LOW;
|
||||
hdma_spi3_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
|
||||
if (HAL_DMA_Init(&hdma_spi3_tx) != HAL_OK)
|
||||
hdma_spi4_tx.Instance = DMA1_Stream5;
|
||||
hdma_spi4_tx.Init.Channel = DMA_CHANNEL_0;
|
||||
hdma_spi4_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
|
||||
hdma_spi4_tx.Init.PeriphInc = DMA_PINC_DISABLE;
|
||||
hdma_spi4_tx.Init.MemInc = DMA_MINC_ENABLE;
|
||||
hdma_spi4_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
|
||||
hdma_spi4_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
|
||||
hdma_spi4_tx.Init.Mode = DMA_CIRCULAR;
|
||||
hdma_spi4_tx.Init.Priority = DMA_PRIORITY_LOW;
|
||||
hdma_spi4_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
|
||||
if (HAL_DMA_Init(&hdma_spi4_tx) != HAL_OK)
|
||||
{
|
||||
Error_Handler2(5); // on error: five blinks
|
||||
}
|
||||
__HAL_LINKDMA(hi2s, hdmatx, hdma_spi3_tx);
|
||||
__HAL_LINKDMA(hi2s, hdmatx, hdma_spi4_tx);
|
||||
}
|
||||
extern "C" void HAL_MspInit(void) // maybe useful, not included in this example
|
||||
{
|
||||
@@ -194,21 +201,17 @@ extern "C" void HAL_MspInit(void) // maybe useful, not included in this example
|
||||
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void initAudio()
|
||||
{
|
||||
// HAL_MspInit(); // not important by default
|
||||
HAL_I2S_MspInit(&hi2s3); // setting up pins and clocks; routing SPI3 to DMA
|
||||
HAL_I2S_MspInit(&hi2s4); // setting up pins and clocks; routing SPI3 to DMA
|
||||
MX_DMA_Init();
|
||||
MX_I2S3_Init();
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
pinMode(PIN_I2S_SDMODE, OUTPUT);
|
||||
digitalWrite(PIN_I2S_SDMODE, HIGH);
|
||||
digitalWrite(LED_BUILTIN, 0);
|
||||
StartAudioBuffers(&hi2s3);
|
||||
StartAudioBuffers(&hi2s4);
|
||||
digitalWrite(LED_BUILTIN, 1);
|
||||
}
|
||||
|
||||
@@ -220,28 +223,34 @@ void handleAudio()
|
||||
// just a dummy code in loop, audio out is generated by ISR
|
||||
digitalWrite(LED_BUILTIN, 1);
|
||||
delay(250);
|
||||
//HAL_I2S_Transmit(&hi2s3,&sine,WAV_SIZE,1000);
|
||||
//HAL_I2S_Transmit(&hi2s4,&sine,WAV_SIZE,1000);
|
||||
HAL_StatusTypeDef res;
|
||||
int16_t signal[46876];
|
||||
int16_t signal[4096];
|
||||
int nsamples = sizeof(signal) / sizeof(signal[0]);
|
||||
|
||||
int i = 0;
|
||||
while(i < nsamples) {
|
||||
double t = ((double)i/2.0)/((double)nsamples);
|
||||
signal[i] = 32767*sin(100.0 * TAU * t); // left
|
||||
signal[i+1] = signal[i]; // right
|
||||
i += 2;
|
||||
while (i < nsamples)
|
||||
{
|
||||
double t = ((double)i / 2.0) / ((double)nsamples);
|
||||
signal[i] = 32767 * sin(100.0 * TAU * t); // left
|
||||
signal[i + 1] = signal[i]; // right
|
||||
i += 2;
|
||||
}
|
||||
|
||||
while(1) {
|
||||
res = HAL_I2S_Transmit(&hi2s3, (uint16_t*)signal, nsamples, HAL_MAX_DELAY);
|
||||
if(res != HAL_OK) {
|
||||
Serial.printf("I2S - ERROR, res = %d!\r\n", res);
|
||||
break;
|
||||
}
|
||||
while (1)
|
||||
{
|
||||
res = HAL_I2S_Transmit(&hi2s4, (uint16_t *)signal, nsamples, HAL_MAX_DELAY);
|
||||
if (res != HAL_OK)
|
||||
{
|
||||
Serial.printf("I2S - ERROR, res = %d!\r\n", res);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
Serial.printf("I2S - HAL_OK\r\n");
|
||||
}
|
||||
}
|
||||
digitalWrite(LED_BUILTIN, 0);
|
||||
delay(250);
|
||||
|
||||
|
||||
}
|
||||
@@ -26,8 +26,6 @@
|
||||
#define B4_HZ 493.88
|
||||
|
||||
// Define a C-major scale to play all the notes up and down.
|
||||
float scale[] = { C4_HZ, D4_HZ, E4_HZ, F4_HZ, G4_HZ, A4_HZ, B4_HZ, A4_HZ, G4_HZ, F4_HZ, E4_HZ, D4_HZ, C4_HZ };
|
||||
|
||||
|
||||
void initAudio(void);
|
||||
void handleAudio(void);
|
||||
|
||||
@@ -0,0 +1,707 @@
|
||||
/*!
|
||||
* @file Adafruit_ZeroDMA.cpp
|
||||
*
|
||||
* @mainpage Adafruit DMA Arduino library for SAMD microcontrollers.
|
||||
*
|
||||
* @section intro_sec Introduction
|
||||
*
|
||||
* This is the documentation for Adafruit's DMA library for SAMD
|
||||
* microcontrollers on the Arduino platform. SAMD21 and SAMD51 lines
|
||||
* are supported.
|
||||
*
|
||||
* Adafruit invests time and resources providing this open source code,
|
||||
* please support Adafruit and open-source hardware by purchasing
|
||||
* products from Adafruit!
|
||||
*
|
||||
* @section dependencies Dependencies
|
||||
*
|
||||
* @section author Author
|
||||
*
|
||||
* Written by Phil "PaintYourDragon" Burgess for Adafruit Industries,
|
||||
* based partly on DMA insights from Atmel ASFCORE 3.
|
||||
*
|
||||
* @section license License
|
||||
*
|
||||
* MIT license, all text here must be included in any redistribution.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "DMA.h"
|
||||
#include <malloc.h> // memalign() function
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifdef USE_TINYUSB
|
||||
// For Serial when selecting TinyUSB
|
||||
#include <Adafruit_TinyUSB.h>
|
||||
#endif
|
||||
|
||||
#ifdef DMAC_RESERVED_CHANNELS // SAMD core > 1.2.1
|
||||
#include <dma.h> // _descriptor[] and _writeback[] are extern'd here
|
||||
static volatile uint32_t _channelMask = DMAC_RESERVED_CHANNELS;
|
||||
#else
|
||||
#include "utility/dma.h"
|
||||
static volatile uint32_t _channelMask = 0; // Bitmask of allocated channels
|
||||
|
||||
// DMA descriptor list entry point (and writeback buffer) per channel
|
||||
__attribute__((__aligned__(16))) static DmacDescriptor ///< 128 bit alignment
|
||||
_descriptor[DMAC_CH_NUM] SECTION_DMAC_DESCRIPTOR, ///< Descriptor table
|
||||
_writeback[DMAC_CH_NUM] SECTION_DMAC_DESCRIPTOR; ///< Writeback table
|
||||
#endif
|
||||
|
||||
// Pointer to ZeroDMA object for each channel is needed for the
|
||||
// ISR (in C, outside of class context) to access callbacks.
|
||||
static DMA_class *_dmaPtr[DMAC_CH_NUM] = {0}; // Init to NULL
|
||||
|
||||
// Adapted from ASF3 interrupt_sam_nvic.c:
|
||||
|
||||
static volatile unsigned long cpu_irq_critical_section_counter = 0;
|
||||
static volatile unsigned char cpu_irq_prev_interrupt_state = 0;
|
||||
|
||||
static void cpu_irq_enter_critical(void) {
|
||||
if (!cpu_irq_critical_section_counter) {
|
||||
if (__get_PRIMASK() == 0) { // IRQ enabled?
|
||||
__disable_irq(); // Disable it
|
||||
__DMB();
|
||||
cpu_irq_prev_interrupt_state = 1;
|
||||
} else {
|
||||
// Make sure the to save the prev state as false
|
||||
cpu_irq_prev_interrupt_state = 0;
|
||||
}
|
||||
}
|
||||
|
||||
cpu_irq_critical_section_counter++;
|
||||
}
|
||||
|
||||
static void cpu_irq_leave_critical(void) {
|
||||
// Check if the user is trying to leave a critical section
|
||||
// when not in a critical section
|
||||
if (cpu_irq_critical_section_counter > 0) {
|
||||
cpu_irq_critical_section_counter--;
|
||||
|
||||
// Only enable global interrupts when the counter
|
||||
// reaches 0 and the state of the global interrupt flag
|
||||
// was enabled when entering critical state */
|
||||
if ((!cpu_irq_critical_section_counter) && cpu_irq_prev_interrupt_state) {
|
||||
__DMB();
|
||||
__enable_irq();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CONSTRUCTOR -------------------------------------------------------------
|
||||
|
||||
// Constructor initializes Adafruit_ZeroDMA basics but does NOT allocate a
|
||||
// DMA channel (that's done in allocate()) or start a job (that's done in
|
||||
// startJob()). This is because constructors in a global context are called
|
||||
// before a sketch's setup() function, which may have some other hardware
|
||||
// initialization of its own, don't want it clobbering us.
|
||||
DMA_class::DMA_class(void) {
|
||||
channel = 0xFF; // Channel not yet allocated
|
||||
jobStatus = DMA_STATUS_OK;
|
||||
hasDescriptors = false; // No descriptors allocated yet
|
||||
loopFlag = false;
|
||||
peripheralTrigger = 0; // Software trigger only by default
|
||||
triggerAction = DMA_TRIGGER_ACTON_TRANSACTION;
|
||||
memset(callback, 0, sizeof(callback));
|
||||
}
|
||||
|
||||
// TODO: add destructor? Should stop job, delete descriptors, free channel.
|
||||
|
||||
// INTERRUPT SERVICE ROUTINE -----------------------------------------------
|
||||
|
||||
/*!
|
||||
@brief This is a C function that exists outside the Adafruit_ZeroDMA
|
||||
context. DMA channel number is determined from the INTPEND
|
||||
register, from this we get a ZeroDMA object pointer through the
|
||||
_dmaPtr[] array. (It's done this way because jobStatus and
|
||||
callback[] are protected elements in the ZeroDMA object -- we
|
||||
can't touch them in C, but the next function after this, being
|
||||
part of the ZeroDMA class, can.)
|
||||
*/
|
||||
#ifdef __SAMD51__
|
||||
void DMAC_0_Handler(void) {
|
||||
#else
|
||||
void DMAC_Handler(void) {
|
||||
#endif
|
||||
cpu_irq_enter_critical();
|
||||
|
||||
uint8_t channel = DMAC->INTPEND.bit.ID; // Channel # causing interrupt
|
||||
if (channel < DMAC_CH_NUM) {
|
||||
DMA_class *dma;
|
||||
if ((dma = _dmaPtr[channel])) { // -> Channel's ZeroDMA object
|
||||
#ifdef __SAMD51__
|
||||
// Call IRQ handler with channel #
|
||||
dma->_IRQhandler(channel);
|
||||
#else
|
||||
DMAC->CHID.bit.ID = channel;
|
||||
// Call IRQ handler with interrupt flag(s)
|
||||
dma->_IRQhandler(DMAC->CHINTFLAG.reg);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
cpu_irq_leave_critical();
|
||||
}
|
||||
|
||||
#ifdef __SAMD51__
|
||||
void DMAC_1_Handler(void) __attribute__((weak, alias("DMAC_0_Handler")));
|
||||
void DMAC_2_Handler(void) __attribute__((weak, alias("DMAC_0_Handler")));
|
||||
void DMAC_3_Handler(void) __attribute__((weak, alias("DMAC_0_Handler")));
|
||||
void DMAC_4_Handler(void) __attribute__((weak, alias("DMAC_0_Handler")));
|
||||
#endif
|
||||
|
||||
void DMA_class::_IRQhandler(uint8_t flags) {
|
||||
#ifdef __SAMD51__
|
||||
// 'flags' is initially passed in as channel number,
|
||||
// from which we look up the actual interrupt flags...
|
||||
flags = DMAC->Channel[flags].CHINTFLAG.reg;
|
||||
#endif
|
||||
if (flags & DMAC_CHINTENCLR_TERR) {
|
||||
// Clear error flag
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHINTFLAG.reg = DMAC_CHINTENCLR_TERR;
|
||||
#else
|
||||
DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_TERR;
|
||||
#endif
|
||||
jobStatus = DMA_STATUS_ERR_IO;
|
||||
if (callback[DMA_CALLBACK_TRANSFER_ERROR])
|
||||
callback[DMA_CALLBACK_TRANSFER_ERROR](this);
|
||||
} else if (flags & DMAC_CHINTENCLR_TCMPL) {
|
||||
// Clear transfer complete flag
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHINTFLAG.reg = DMAC_CHINTENCLR_TCMPL;
|
||||
#else
|
||||
DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_TCMPL;
|
||||
#endif
|
||||
jobStatus = DMA_STATUS_OK;
|
||||
if (callback[DMA_CALLBACK_TRANSFER_DONE])
|
||||
callback[DMA_CALLBACK_TRANSFER_DONE](this);
|
||||
} else if (flags & DMAC_CHINTENCLR_SUSP) {
|
||||
// Clear channel suspend flag
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHINTFLAG.reg = DMAC_CHINTENCLR_SUSP;
|
||||
#else
|
||||
DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_SUSP;
|
||||
#endif
|
||||
jobStatus = DMA_STATUS_SUSPEND;
|
||||
if (callback[DMA_CALLBACK_CHANNEL_SUSPEND])
|
||||
callback[DMA_CALLBACK_CHANNEL_SUSPEND](this);
|
||||
}
|
||||
}
|
||||
|
||||
// DMA CHANNEL FUNCTIONS ---------------------------------------------------
|
||||
|
||||
// Allocates channel for ZeroDMA object
|
||||
DMAstatus DMA_class::allocate(void) {
|
||||
|
||||
if (channel < DMAC_CH_NUM)
|
||||
return DMA_STATUS_OK; // Already alloc'd!
|
||||
|
||||
// Find index of first free DMA channel. As currently written,
|
||||
// this "does not play well with others" as it assumes _channelMask
|
||||
// is the final arbiter of channels in use (this is true only within
|
||||
// this library -- but other DMA-driven code may have allocated its
|
||||
// own channel(s) elsewhere, sometimes with an equally broken
|
||||
// approach). A possible alternate approach, I haven't tested this
|
||||
// yet, might be to loop through each channel, set DMAC->CHID.bit.ID
|
||||
// and then test whether CHCTRLA.bit.ENABLE is set? But for now...
|
||||
for (channel = 0; (channel < DMAC_CH_NUM) && (_channelMask & (1 << channel));
|
||||
channel++)
|
||||
;
|
||||
// Doesn't help that code later does a software reset of the DMA
|
||||
// controller, which would blow out other DMA-using libraries
|
||||
// anyway (or they're just as likely to blow out this one).
|
||||
// I think it's just an all-or-nothing affair...use one library
|
||||
// for DMA everything, never mix and match.
|
||||
|
||||
if (channel >= DMAC_CH_NUM) // No free channel!
|
||||
return DMA_STATUS_ERR_NOT_FOUND;
|
||||
|
||||
cpu_irq_enter_critical();
|
||||
|
||||
if (!_channelMask) { // No channels allocated yet; initialize DMA!
|
||||
#if !defined(DMAC_RESERVED_CHANNELS)
|
||||
#if (SAML21) || (SAML22) || (SAMC20) || (SAMC21)
|
||||
PM->AHBMASK.bit.DMAC_ = 1;
|
||||
#elif defined(__SAMD51__)
|
||||
MCLK->AHBMASK.bit.DMAC_ = 1; // Initialize DMA clocks
|
||||
#else
|
||||
PM->AHBMASK.bit.DMAC_ = 1; // Initialize DMA clocks
|
||||
PM->APBBMASK.bit.DMAC_ = 1;
|
||||
#endif
|
||||
DMAC->CTRL.bit.DMAENABLE = 0; // Disable DMA controller
|
||||
DMAC->CTRL.bit.SWRST = 1; // Perform software reset
|
||||
|
||||
// Initialize descriptor list addresses
|
||||
DMAC->BASEADDR.bit.BASEADDR = (uint32_t)_descriptor;
|
||||
DMAC->WRBADDR.bit.WRBADDR = (uint32_t)_writeback;
|
||||
memset(_descriptor, 0, sizeof(_descriptor));
|
||||
memset(_writeback, 0, sizeof(_writeback));
|
||||
|
||||
// Re-enable DMA controller with all priority levels
|
||||
DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xF);
|
||||
#endif
|
||||
|
||||
// Enable DMA interrupt at lowest priority
|
||||
#ifdef __SAMD51__
|
||||
IRQn_Type irqs[] = {DMAC_0_IRQn, DMAC_1_IRQn, DMAC_2_IRQn, DMAC_3_IRQn,
|
||||
DMAC_4_IRQn};
|
||||
for (uint8_t i = 0; i < (sizeof irqs / sizeof irqs[0]); i++) {
|
||||
NVIC_EnableIRQ(irqs[i]);
|
||||
NVIC_SetPriority(irqs[i], (1 << __NVIC_PRIO_BITS) - 1);
|
||||
}
|
||||
#else
|
||||
NVIC_EnableIRQ(DMAC_IRQn);
|
||||
NVIC_SetPriority(DMAC_IRQn, (1 << __NVIC_PRIO_BITS) - 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
_channelMask |= 1 << channel; // Mark channel as allocated
|
||||
_dmaPtr[channel] = this; // Channel-index-to-object pointer
|
||||
|
||||
// Reset the allocated channel
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHCTRLA.bit.ENABLE = 0;
|
||||
DMAC->Channel[channel].CHCTRLA.bit.SWRST = 1;
|
||||
#else
|
||||
DMAC->CHID.bit.ID = channel;
|
||||
DMAC->CHCTRLA.bit.ENABLE = 0;
|
||||
DMAC->CHCTRLA.bit.SWRST = 1;
|
||||
#endif
|
||||
|
||||
// Clear software trigger
|
||||
DMAC->SWTRIGCTRL.reg &= ~(1 << channel);
|
||||
|
||||
// Configure default behaviors
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHPRILVL.bit.PRILVL = 0;
|
||||
DMAC->Channel[channel].CHCTRLA.bit.TRIGSRC = peripheralTrigger;
|
||||
DMAC->Channel[channel].CHCTRLA.bit.TRIGACT = triggerAction;
|
||||
DMAC->Channel[channel].CHCTRLA.bit.BURSTLEN =
|
||||
DMAC_CHCTRLA_BURSTLEN_SINGLE_Val; // Single-beat burst length
|
||||
#else
|
||||
DMAC->CHCTRLB.bit.LVL = 0;
|
||||
DMAC->CHCTRLB.bit.TRIGSRC = peripheralTrigger;
|
||||
DMAC->CHCTRLB.bit.TRIGACT = triggerAction;
|
||||
#endif
|
||||
|
||||
cpu_irq_leave_critical();
|
||||
|
||||
return DMA_STATUS_OK;
|
||||
}
|
||||
|
||||
void DMA_class::setPriority(dma_priority pri) {
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHPRILVL.bit.PRILVL = pri;
|
||||
#else
|
||||
DMAC->CHCTRLB.bit.LVL = pri;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Deallocate DMA channel
|
||||
// TODO: should this delete/deallocate the descriptor list?
|
||||
DMAstatus DMA_class::free(void) {
|
||||
|
||||
DMAstatus status = DMA_STATUS_OK;
|
||||
|
||||
cpu_irq_enter_critical(); // jobStatus is volatile
|
||||
|
||||
if (jobStatus == DMA_STATUS_BUSY) {
|
||||
status = DMA_STATUS_BUSY; // Can't leave when busy
|
||||
} else if ((channel < DMAC_CH_NUM) && (_channelMask & (1 << channel))) {
|
||||
// Valid in-use channel; release it
|
||||
_channelMask &= ~(1 << channel); // Clear bit
|
||||
if (!_channelMask) { // No more channels in use?
|
||||
#ifdef __SAMD51__
|
||||
NVIC_DisableIRQ(DMAC_0_IRQn); // Disable DMA interrupt
|
||||
DMAC->CTRL.bit.DMAENABLE = 0; // Disable DMA
|
||||
MCLK->AHBMASK.bit.DMAC_ = 0; // Disable DMA clock
|
||||
#else
|
||||
NVIC_DisableIRQ(DMAC_IRQn); // Disable DMA interrupt
|
||||
DMAC->CTRL.bit.DMAENABLE = 0; // Disable DMA
|
||||
PM->APBBMASK.bit.DMAC_ = 0; // Disable DMA clocks
|
||||
PM->AHBMASK.bit.DMAC_ = 0;
|
||||
#endif
|
||||
}
|
||||
_dmaPtr[channel] = NULL;
|
||||
channel = 0xFF;
|
||||
} else {
|
||||
status = DMA_STATUS_ERR_NOT_INITIALIZED; // Channel not in use
|
||||
}
|
||||
|
||||
cpu_irq_leave_critical();
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
// Start DMA transfer job. Channel and descriptors should be allocated
|
||||
// before calling this.
|
||||
DMAstatus DMA_class::startJob(void) {
|
||||
DMAstatus status = DMA_STATUS_OK;
|
||||
|
||||
cpu_irq_enter_critical(); // Job status is volatile
|
||||
|
||||
if (jobStatus == DMA_STATUS_BUSY) {
|
||||
status = DMA_STATUS_BUSY; // Resource is busy
|
||||
} else if (channel >= DMAC_CH_NUM) {
|
||||
status = DMA_STATUS_ERR_NOT_INITIALIZED; // Channel not in use
|
||||
} else if (!hasDescriptors || (_descriptor[channel].BTCNT.reg <= 0)) {
|
||||
status = DMA_STATUS_ERR_INVALID_ARG; // Bad transfer size
|
||||
} else {
|
||||
uint8_t i, interruptMask = 0;
|
||||
for (i = 0; i < DMA_CALLBACK_N; i++)
|
||||
if (callback[i])
|
||||
interruptMask |= (1 << i);
|
||||
jobStatus = DMA_STATUS_BUSY;
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHINTENSET.reg =
|
||||
DMAC_CHINTENSET_MASK & interruptMask;
|
||||
DMAC->Channel[channel].CHINTENCLR.reg =
|
||||
DMAC_CHINTENCLR_MASK & ~interruptMask;
|
||||
DMAC->Channel[channel].CHCTRLA.bit.ENABLE = 1;
|
||||
#else
|
||||
DMAC->CHID.bit.ID = channel;
|
||||
DMAC->CHINTENSET.reg = DMAC_CHINTENSET_MASK & interruptMask;
|
||||
DMAC->CHINTENCLR.reg = DMAC_CHINTENCLR_MASK & ~interruptMask;
|
||||
DMAC->CHCTRLA.bit.ENABLE = 1; // Enable the transfer channel
|
||||
#endif
|
||||
}
|
||||
|
||||
cpu_irq_leave_critical();
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
// Set and enable callback function for ZeroDMA object. This can be called
|
||||
// before or after channel and/or descriptors are allocated, but needs
|
||||
// to be called before job is started.
|
||||
void DMA_class::setCallback(void (*cb)(DMA_class *),
|
||||
dma_callback_type type) {
|
||||
callback[type] = cb;
|
||||
}
|
||||
|
||||
// Suspend/resume don't quite do what I thought -- avoid using for now.
|
||||
void DMA_class::suspend(void) {
|
||||
cpu_irq_enter_critical();
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHCTRLB.reg |= DMAC_CHCTRLB_CMD_SUSPEND;
|
||||
#else
|
||||
DMAC->CHID.bit.ID = channel;
|
||||
DMAC->CHCTRLB.reg |= DMAC_CHCTRLB_CMD_SUSPEND;
|
||||
#endif
|
||||
cpu_irq_leave_critical();
|
||||
}
|
||||
|
||||
#define MAX_JOB_RESUME_COUNT 10000 ///< Loop iteration threshold for timeout
|
||||
void DMA_class::resume(void) {
|
||||
cpu_irq_enter_critical(); // jobStatus is volatile
|
||||
if (jobStatus == DMA_STATUS_SUSPEND) {
|
||||
int count;
|
||||
uint32_t bitMask = 1 << channel;
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHCTRLB.reg |= DMAC_CHCTRLB_CMD_RESUME;
|
||||
#else
|
||||
DMAC->CHID.bit.ID = channel;
|
||||
DMAC->CHCTRLB.reg |= DMAC_CHCTRLB_CMD_RESUME;
|
||||
#endif
|
||||
|
||||
for (count = 0;
|
||||
(count < MAX_JOB_RESUME_COUNT) && !(DMAC->BUSYCH.reg & bitMask);
|
||||
count++)
|
||||
;
|
||||
|
||||
jobStatus = (count < MAX_JOB_RESUME_COUNT) ? DMA_STATUS_BUSY
|
||||
: DMA_STATUS_ERR_TIMEOUT;
|
||||
}
|
||||
cpu_irq_leave_critical();
|
||||
}
|
||||
|
||||
// Abort is OK though.
|
||||
void DMA_class::abort(void) {
|
||||
if (channel <= DMAC_CH_NUM) {
|
||||
cpu_irq_enter_critical();
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHCTRLA.bit.ENABLE = 0; // Disable channel
|
||||
#else
|
||||
DMAC->CHID.bit.ID = channel; // Select channel
|
||||
DMAC->CHCTRLA.reg = 0; // Disable
|
||||
#endif
|
||||
jobStatus = DMA_STATUS_ABORTED;
|
||||
cpu_irq_leave_critical();
|
||||
}
|
||||
}
|
||||
|
||||
// Set DMA peripheral trigger.
|
||||
// This can be done before or after channel is allocated.
|
||||
void DMA_class::setTrigger(uint8_t trigger) {
|
||||
peripheralTrigger = trigger; // Save value for allocate()
|
||||
|
||||
// If channel already allocated, configure peripheral trigger
|
||||
// (old lib required configure before alloc -- either way OK now)
|
||||
if (channel < DMAC_CH_NUM) {
|
||||
cpu_irq_enter_critical();
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHCTRLA.bit.TRIGSRC = trigger;
|
||||
#else
|
||||
DMAC->CHID.bit.ID = channel;
|
||||
DMAC->CHCTRLB.bit.TRIGSRC = trigger;
|
||||
#endif
|
||||
cpu_irq_leave_critical();
|
||||
}
|
||||
}
|
||||
|
||||
// Set DMA trigger action.
|
||||
// This can be done before or after channel is allocated.
|
||||
void DMA_class::setAction(dma_transfer_trigger_action action) {
|
||||
triggerAction = action; // Save value for allocate()
|
||||
|
||||
// If channel already allocated, configure trigger action
|
||||
// (old lib required configure before alloc -- either way OK now)
|
||||
if (channel < DMAC_CH_NUM) {
|
||||
cpu_irq_enter_critical();
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHCTRLA.bit.TRIGACT = action;
|
||||
#else
|
||||
DMAC->CHID.bit.ID = channel;
|
||||
DMAC->CHCTRLB.bit.TRIGACT = action;
|
||||
#endif
|
||||
cpu_irq_leave_critical();
|
||||
}
|
||||
}
|
||||
|
||||
// Issue software trigger. Channel must be allocated & descriptors added!
|
||||
void DMA_class::trigger(void) {
|
||||
if ((channel <= DMAC_CH_NUM) & hasDescriptors)
|
||||
DMAC->SWTRIGCTRL.reg |= (1 << channel);
|
||||
}
|
||||
|
||||
uint8_t DMA_class::getChannel(void) { return channel; }
|
||||
|
||||
// DMA DESCRIPTOR FUNCTIONS ------------------------------------------------
|
||||
|
||||
// Allocates a new DMA descriptor (if needed) and appends it to the
|
||||
// channel's descriptor list. Returns pointer to DmacDescriptor,
|
||||
// or NULL on various errors. You'll want to keep the pointer for
|
||||
// later if you need to modify or free the descriptor.
|
||||
// Channel must be allocated first!
|
||||
DmacDescriptor *DMA_class::addDescriptor(void *src, void *dst,
|
||||
uint32_t count,
|
||||
dma_beat_size size, bool srcInc,
|
||||
bool dstInc, uint32_t stepSize,
|
||||
bool stepSel) {
|
||||
|
||||
// Channel must be allocated first
|
||||
if (channel >= DMAC_CH_NUM)
|
||||
return NULL;
|
||||
|
||||
// Can't do while job's busy
|
||||
if (jobStatus == DMA_STATUS_BUSY)
|
||||
return NULL;
|
||||
|
||||
DmacDescriptor *desc;
|
||||
|
||||
// Scan descriptor list to find last entry. If an entry's
|
||||
// DESCADDR value is 0, that's the end of the list and it's
|
||||
// currently un-looped. If the DESCADDR value is the same
|
||||
// as the first entry, that's the end of the list and it's
|
||||
// looped. Either way, set the last entry's DESCADDR value
|
||||
// to the new descriptor, and the descriptor's own DESCADDR
|
||||
// will be set later either to 0 or the list head.
|
||||
if (hasDescriptors) {
|
||||
// DMA descriptors must be 128-bit (16 byte) aligned.
|
||||
// memalign() is considered 'obsolete' but it's replacements
|
||||
// (aligned_alloc() or posix_memalign()) are not currently
|
||||
// available in the version of ARM GCC in use, but this is,
|
||||
// so here we are.
|
||||
if (!(desc = (DmacDescriptor *)memalign(16, sizeof(DmacDescriptor))))
|
||||
return NULL;
|
||||
DmacDescriptor *prev = &_descriptor[channel];
|
||||
while (prev->DESCADDR.reg &&
|
||||
(prev->DESCADDR.reg != (uint32_t)&_descriptor[channel])) {
|
||||
prev = (DmacDescriptor *)prev->DESCADDR.reg;
|
||||
}
|
||||
prev->DESCADDR.reg = (uint32_t)desc;
|
||||
} else {
|
||||
desc = &_descriptor[channel];
|
||||
}
|
||||
hasDescriptors = true;
|
||||
|
||||
uint8_t bytesPerBeat; // Beat transfer size IN BYTES
|
||||
switch (size) {
|
||||
default:
|
||||
bytesPerBeat = 1;
|
||||
break;
|
||||
case DMA_BEAT_SIZE_HWORD:
|
||||
bytesPerBeat = 2;
|
||||
break;
|
||||
case DMA_BEAT_SIZE_WORD:
|
||||
bytesPerBeat = 4;
|
||||
break;
|
||||
}
|
||||
|
||||
desc->BTCTRL.bit.VALID = true;
|
||||
desc->BTCTRL.bit.EVOSEL = DMA_EVENT_OUTPUT_DISABLE;
|
||||
desc->BTCTRL.bit.BLOCKACT = DMA_BLOCK_ACTION_NOACT;
|
||||
desc->BTCTRL.bit.BEATSIZE = size;
|
||||
desc->BTCTRL.bit.SRCINC = srcInc;
|
||||
desc->BTCTRL.bit.DSTINC = dstInc;
|
||||
desc->BTCTRL.bit.STEPSEL = stepSel;
|
||||
desc->BTCTRL.bit.STEPSIZE = stepSize;
|
||||
desc->BTCNT.reg = count;
|
||||
desc->SRCADDR.reg = (uint32_t)src;
|
||||
|
||||
if (srcInc) {
|
||||
if (stepSel) {
|
||||
desc->SRCADDR.reg += bytesPerBeat * count * (1 << stepSize);
|
||||
} else {
|
||||
desc->SRCADDR.reg += bytesPerBeat * count;
|
||||
}
|
||||
}
|
||||
|
||||
desc->DSTADDR.reg = (uint32_t)dst;
|
||||
|
||||
if (dstInc) {
|
||||
if (!stepSel) {
|
||||
desc->DSTADDR.reg += bytesPerBeat * count * (1 << stepSize);
|
||||
} else {
|
||||
desc->DSTADDR.reg += bytesPerBeat * count;
|
||||
}
|
||||
}
|
||||
|
||||
desc->DESCADDR.reg = loopFlag ? (uint32_t)&_descriptor[channel] : 0;
|
||||
|
||||
return desc;
|
||||
}
|
||||
|
||||
// Modify DMA descriptor with a new source address, destination address &
|
||||
// block transfer count. All other attributes (including increment enables,
|
||||
// etc.) are unchanged. Mostly for changing the data being pushed to a
|
||||
// peripheral (DAC, SPI, whatev.)
|
||||
void DMA_class::changeDescriptor(DmacDescriptor *desc, void *src,
|
||||
void *dst, uint32_t count) {
|
||||
|
||||
uint8_t bytesPerBeat; // Beat transfer size IN BYTES
|
||||
switch (desc->BTCTRL.bit.BEATSIZE) {
|
||||
default:
|
||||
bytesPerBeat = 1;
|
||||
break;
|
||||
case DMA_BEAT_SIZE_HWORD:
|
||||
bytesPerBeat = 2;
|
||||
break;
|
||||
case DMA_BEAT_SIZE_WORD:
|
||||
bytesPerBeat = 4;
|
||||
break;
|
||||
}
|
||||
|
||||
if (count)
|
||||
desc->BTCNT.reg = count;
|
||||
|
||||
if (src) {
|
||||
desc->SRCADDR.reg = (uint32_t)src;
|
||||
if (desc->BTCTRL.bit.SRCINC) {
|
||||
if (desc->BTCTRL.bit.STEPSEL) {
|
||||
desc->SRCADDR.reg +=
|
||||
desc->BTCNT.reg * bytesPerBeat * (1 << desc->BTCTRL.bit.STEPSIZE);
|
||||
} else {
|
||||
desc->SRCADDR.reg += desc->BTCNT.reg * bytesPerBeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dst) {
|
||||
desc->DSTADDR.reg = (uint32_t)dst;
|
||||
if (desc->BTCTRL.bit.DSTINC) {
|
||||
if (!desc->BTCTRL.bit.STEPSEL) {
|
||||
desc->DSTADDR.reg +=
|
||||
desc->BTCNT.reg * bytesPerBeat * (1 << desc->BTCTRL.bit.STEPSIZE);
|
||||
} else {
|
||||
desc->DSTADDR.reg += desc->BTCNT.reg * bytesPerBeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// I think this code is here by accident -- disabling for now.
|
||||
#if 0
|
||||
cpu_irq_enter_critical();
|
||||
jobStatus = DMA_STATUS_OK;
|
||||
#ifdef __SAMD51__
|
||||
DMAC->Channel[channel].CHCTRLA.bit.ENABLE = 1;
|
||||
#else
|
||||
DMAC->CHID.bit.ID = channel;
|
||||
DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;
|
||||
#endif
|
||||
cpu_irq_leave_critical();
|
||||
#endif
|
||||
}
|
||||
|
||||
// TODO: delete descriptor, delete whole descriptor chain
|
||||
|
||||
// Select whether channel's descriptor list should repeat or not.
|
||||
// This can be done before or after channel & any descriptors are allocated.
|
||||
void DMA_class::loop(boolean flag) {
|
||||
// The loop selection is 'sticky' -- that is, you can enable or
|
||||
// disable looping before a descriptor list is built, or after
|
||||
// the fact. This requires some extra steps in the library code
|
||||
// but avoids a must-do-in-X-order constraint on user.
|
||||
loopFlag = flag;
|
||||
|
||||
if (hasDescriptors) { // Descriptor list already started?
|
||||
// Scan descriptor list to find last entry. If an entry's
|
||||
// DESCADDR value is 0, that's the end of the list and it's
|
||||
// currently un-looped. If the DESCADDR value is the same
|
||||
// as the first entry, that's the end of the list and it's
|
||||
// already looped.
|
||||
DmacDescriptor *desc = &_descriptor[channel];
|
||||
while (desc->DESCADDR.reg &&
|
||||
(desc->DESCADDR.reg != (uint32_t)&_descriptor[channel])) {
|
||||
desc = (DmacDescriptor *)desc->DESCADDR.reg;
|
||||
}
|
||||
// Loop or unloop descriptor list as appropriate
|
||||
desc->DESCADDR.reg = loopFlag ? (uint32_t)&_descriptor[channel] : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// MISCELLANY --------------------------------------------------------------
|
||||
|
||||
void DMA_class::printStatus(DMAstatus s) {
|
||||
if (s == DMA_STATUS_JOBSTATUS)
|
||||
s = jobStatus;
|
||||
Serial.print("Status: ");
|
||||
switch (s) {
|
||||
case DMA_STATUS_OK:
|
||||
Serial.println("OK");
|
||||
break;
|
||||
case DMA_STATUS_ERR_NOT_FOUND:
|
||||
Serial.println("NOT FOUND");
|
||||
break;
|
||||
case DMA_STATUS_ERR_NOT_INITIALIZED:
|
||||
Serial.println("NOT INITIALIZED");
|
||||
break;
|
||||
case DMA_STATUS_ERR_INVALID_ARG:
|
||||
Serial.println("INVALID ARGUMENT");
|
||||
break;
|
||||
case DMA_STATUS_ERR_IO:
|
||||
Serial.println("IO ERROR");
|
||||
break;
|
||||
case DMA_STATUS_ERR_TIMEOUT:
|
||||
Serial.println("TIMEOUT");
|
||||
break;
|
||||
case DMA_STATUS_BUSY:
|
||||
Serial.println("BUSY");
|
||||
break;
|
||||
case DMA_STATUS_SUSPEND:
|
||||
Serial.println("SUSPENDED");
|
||||
break;
|
||||
case DMA_STATUS_ABORTED:
|
||||
Serial.println("ABORTED");
|
||||
break;
|
||||
default:
|
||||
Serial.print("Unknown 0x");
|
||||
Serial.println((int)s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool DMA_class::isActive() {
|
||||
return _writeback[channel].BTCTRL.bit.VALID;
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*!
|
||||
* @file Adafruit_ZeroDMA.h
|
||||
*
|
||||
* This is part of Adafruit's DMA library for SAMD microcontrollers on
|
||||
* the Arduino platform. SAMD21 and SAMD51 lines are supported.
|
||||
*
|
||||
* Adafruit invests time and resources providing this open source code,
|
||||
* please support Adafruit and open-source hardware by purchasing
|
||||
* products from Adafruit!
|
||||
*
|
||||
* Written by Phil "PaintYourDragon" Burgess for Adafruit Industries,
|
||||
* based partly on DMA insights from Atmel ASFCORE 3.
|
||||
*
|
||||
* MIT license, all text here must be included in any redistribution.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _ADAFRUIT_ZERODMA_H_
|
||||
#define _ADAFRUIT_ZERODMA_H_
|
||||
|
||||
#include "Arduino.h"
|
||||
#ifdef DMAC_RESERVED_CHANNELS // SAMD core > 1.2.1
|
||||
#include <dma.h>
|
||||
#else
|
||||
#include "utility/dma.h"
|
||||
#endif
|
||||
|
||||
/** Status codes returned by some DMA functions and/or held in
|
||||
a channel's jobStatus variable. */
|
||||
enum DMAstatus {
|
||||
DMA_STATUS_OK = 0,
|
||||
DMA_STATUS_ERR_NOT_FOUND,
|
||||
DMA_STATUS_ERR_NOT_INITIALIZED,
|
||||
DMA_STATUS_ERR_INVALID_ARG,
|
||||
DMA_STATUS_ERR_IO,
|
||||
DMA_STATUS_ERR_TIMEOUT,
|
||||
DMA_STATUS_BUSY,
|
||||
DMA_STATUS_SUSPEND,
|
||||
DMA_STATUS_ABORTED,
|
||||
DMA_STATUS_JOBSTATUS = -1 // For printStatus() function
|
||||
};
|
||||
|
||||
/*!
|
||||
@brief Class encapsulating DMA jobs and descriptors.
|
||||
*/
|
||||
class DMA_class {
|
||||
public:
|
||||
DMA_class(void);
|
||||
|
||||
// DMA channel functions
|
||||
|
||||
/*!
|
||||
@brief Allocate channel for ZeroDMA object. This allocates a CHANNEL,
|
||||
not a DESCRIPTOR.
|
||||
@return ZeroDMAstatus type:
|
||||
DMA_STATUS_OK on success.
|
||||
DMA_STATUS_ERR_NOT_FOUND if no DMA channels are free.
|
||||
|
||||
*/
|
||||
DMAstatus allocate(void);
|
||||
|
||||
/*!
|
||||
@brief Start a previously allocated-and-configured DMA job.
|
||||
@return ZeroDMAstatus type:
|
||||
DMA_STATUS_OK on success.
|
||||
DMA_STATUS_BUSY if resource is busy.
|
||||
DMA_STATUS_ERR_NOT_INITIALIZED if attempting to start job
|
||||
on a channel that failed to allocate.
|
||||
DMA_STATUS_ERR_INVALID_ARG if a bad transfer size was specified.
|
||||
*/
|
||||
DMAstatus startJob(void);
|
||||
|
||||
/*!
|
||||
@brief Deallocates a previously-allocated DMA channel. This deallocates
|
||||
the CHANNEL, not any associated DESCRIPTORS.
|
||||
|
||||
@return ZeroDMAstatus type:
|
||||
DMA_STATUS_OK on success.
|
||||
DMA_STATUS_BUSY if channel is busy (can't deallocate while in use).
|
||||
DMA_STATUS_ERR_NOT_INITIALIZED if channel isn't in use.
|
||||
*/
|
||||
DMAstatus free(void);
|
||||
|
||||
/*!
|
||||
@brief Activate a previously allocated-and-configured DMA channel's
|
||||
software trigger.
|
||||
*/
|
||||
void trigger(void);
|
||||
|
||||
/*!
|
||||
@brief Set DMA peripheral trigger. This can be done before or after
|
||||
channel is allocated.
|
||||
@param trigger A device-specific DMA peripheral trigger ID, typically
|
||||
defined in a header file associated with that chip.
|
||||
Example triffer IDs might include SERCOM2_DMAC_ID_TX
|
||||
(SERCOM transfer complete) ADC_DMAC_ID_RESRDY (ADC
|
||||
results ready).
|
||||
*/
|
||||
void setTrigger(uint8_t trigger);
|
||||
|
||||
/*!
|
||||
@brief Set DMA trigger action. This can be done before or after
|
||||
channel is allocated.
|
||||
@param action One of DMA_TRIGGER_ACTON_BLOCK, DMA_TRIGGER_ACTON_BEAT or
|
||||
DMA_TRIGGER_ACTON_TRANSACTION for desired behavior.
|
||||
*/
|
||||
void setAction(dma_transfer_trigger_action action);
|
||||
|
||||
/*!
|
||||
@brief Set and enable callback function for ZeroDMA object. This can be
|
||||
called before or after channel and/or descriptors are allocated,
|
||||
but needs to be called before job is started.
|
||||
@param callback Pointer to callback function which accepts a pointer
|
||||
to a ZeroDMA object (or NULL to disable callback).
|
||||
@param type Which DMA operation to attach this function to (a
|
||||
channel can have multiple callbacks assigned if needed,
|
||||
one for each of these situations):
|
||||
DMA_CALLBACK_DONE on successful completion of a DMA
|
||||
transfer.
|
||||
DMA_CALLBACK_TRANSFER_ERR if a bus error is detected
|
||||
during an AHB access or when the DMAC fetches an
|
||||
invalid descriptor.
|
||||
DMA_CALLBACK_CHANNEL_SUSPEND when a channel is suspended.
|
||||
*/
|
||||
void setCallback(void (*callback)(DMA_class *) = NULL,
|
||||
dma_callback_type type = DMA_CALLBACK_TRANSFER_DONE);
|
||||
|
||||
/*!
|
||||
@brief Select whether a channel's descriptor list should repeat or not.
|
||||
This can be called before or after channel and any descriptors
|
||||
are allocated.
|
||||
@param flag 'true' if DMA descriptor list should repeat indefinitely,
|
||||
'false' if DMA transfer stops at end of descriptor list.
|
||||
*/
|
||||
void loop(boolean flag);
|
||||
|
||||
/*!
|
||||
@brief Suspend a DMA channel. AVOID USING FOR NOW.
|
||||
*/
|
||||
void suspend(void);
|
||||
|
||||
/*!
|
||||
@brief Resume a previously-suspended DMA channel. AVOID USING FOR NOW.
|
||||
*/
|
||||
void resume(void);
|
||||
|
||||
/*!
|
||||
@brief Cancel a DMA transfer operation.
|
||||
*/
|
||||
void abort(void);
|
||||
|
||||
/*!
|
||||
@brief Set DMA channel level priority.
|
||||
@param pri DMA_PRIORITY_0 (lowest priority) through DMA_PRIORITY_3
|
||||
(highest priority).
|
||||
*/
|
||||
void setPriority(dma_priority pri);
|
||||
|
||||
/*!
|
||||
@brief Print (to Serial console) a string corresponding to a DMA
|
||||
job status value.
|
||||
@param s Job status as might be returned by allocate(), startJob(),
|
||||
etc., e.g. DMA_STATUS_OK, DMA_STATUS_ERR_NOT_FOUND, ...
|
||||
*/
|
||||
void printStatus(DMAstatus s = DMA_STATUS_JOBSTATUS);
|
||||
|
||||
/*!
|
||||
@brief Get the DMA channel index associated with a ZeroDMA object.
|
||||
@return uint8_t Channel index (0 to DMAC_CH_NUM-1, or 0xFF).
|
||||
*/
|
||||
uint8_t getChannel(void);
|
||||
|
||||
// DMA descriptor functions
|
||||
|
||||
/*!
|
||||
@brief Allocate and append a DMA descriptor to a channel's descriptor
|
||||
list. Channel must be allocated first.
|
||||
@param src Source address.
|
||||
@param dst Destination address.
|
||||
@param count Transfer count.
|
||||
@param size Per-count transfer size (DMA_BEAT_SIZE_BYTE,
|
||||
DMA_BEAT_SIZE_HWORD or DMA_BEAT_SIZE_WORD for 8, 16,
|
||||
32 bits respectively).
|
||||
@param srcInc If true, increment the source address following each
|
||||
count.
|
||||
@param dstInc If true, increment the destination address following
|
||||
each count.
|
||||
@param stepSize If source/dest address increment in use, this indicates
|
||||
the 'step size' (allowing it to skip over elements).
|
||||
DMA_ADDRESS_INCREMENT_STEP_SIZE_1 for a contiguous
|
||||
transfer, "_SIZE_2 for alternate items, "_SIZE_4
|
||||
8, 16, 32, 64 or 128 for other skip ranges.
|
||||
@param stepSel DMA_STEPSEL_SRC or DMA_STEPSEL_DST depending which
|
||||
pointer the step size should apply to (can't be used
|
||||
on both simultaneously).
|
||||
@return DmacDescriptor* Pointer to DmacDescriptor structure, or NULL
|
||||
on various errors. Calling code should keep the
|
||||
pointer for later if it needs to change or free
|
||||
the descriptor.
|
||||
*/
|
||||
DmacDescriptor *
|
||||
addDescriptor(void *src, void *dst, uint32_t count = 0,
|
||||
dma_beat_size size = DMA_BEAT_SIZE_BYTE, bool srcInc = true,
|
||||
bool dstInc = true,
|
||||
uint32_t stepSize = DMA_ADDRESS_INCREMENT_STEP_SIZE_1,
|
||||
bool stepSel = DMA_STEPSEL_DST);
|
||||
|
||||
/*!
|
||||
@brief Change a previously-allocated DMA descriptor. Only the most
|
||||
common settings (source, dest, count) are available here. For
|
||||
anything more esoteric, you'll need to modify the descriptor
|
||||
structure yourself.
|
||||
@param d Pointer to descriptor structure (as returned by
|
||||
addDescriptor()).
|
||||
@param src New source address.
|
||||
@param dst New destination address.
|
||||
@param count New transfer count.
|
||||
*/
|
||||
void changeDescriptor(DmacDescriptor *d, void *src = NULL, void *dst = NULL,
|
||||
uint32_t count = 0);
|
||||
|
||||
/*!
|
||||
@brief Interrupt handler function, used internally by the library,
|
||||
DO NOT TOUCH!
|
||||
@param flags Channel number, actually.
|
||||
*/
|
||||
void _IRQhandler(uint8_t flags);
|
||||
|
||||
/*!
|
||||
@brief Test if DMA transfer is in-progress. Might be better to use
|
||||
callback and flag, unsure.
|
||||
@return 'true' if DMA channel is busy, 'false' otherwise.
|
||||
*/
|
||||
bool isActive();
|
||||
|
||||
protected:
|
||||
uint8_t channel; ///< DMA channel index (0 to DMAC_CH_NUM-1, or 0xFF)
|
||||
volatile enum DMAstatus jobStatus; ///< Last known DMA job status
|
||||
bool hasDescriptors; ///< 'true' if one or more descriptors assigned
|
||||
bool loopFlag; ///< 'true' if descriptor chain loops back to start
|
||||
uint8_t peripheralTrigger; ///< Value set by setTrigger()
|
||||
dma_transfer_trigger_action triggerAction; ///< Value set by setAction()
|
||||
void (*callback[DMA_CALLBACK_N])(DMA_class *); ///< Callback func *s
|
||||
};
|
||||
|
||||
#endif // _ADAFRUIT_ZERODMA_H_
|
||||
@@ -0,0 +1,577 @@
|
||||
/*!
|
||||
* @file Adafruit_ZeroI2S.cpp
|
||||
*
|
||||
* @mainpage Adafruit I2S peripheral driver for SAMD21 and SAMD51 chips
|
||||
*
|
||||
* @section intro_sec Introduction
|
||||
*
|
||||
* I2S peripheral driver for SAMD21 and SAMD51 chips
|
||||
*
|
||||
*
|
||||
* Adafruit invests time and resources providing this open source code,
|
||||
* please support Adafruit and open-source hardware by purchasing
|
||||
* products from Adafruit!
|
||||
*
|
||||
* @section author Author
|
||||
*
|
||||
* Written by Dean Miller for Adafruit Industries.
|
||||
*
|
||||
* @section license License
|
||||
*
|
||||
* BSD license, all text here must be included in any redistribution.
|
||||
*
|
||||
*/
|
||||
|
||||
#include "I2S.h"
|
||||
#include "wiring_private.h"
|
||||
|
||||
#ifndef DEBUG_PRINTLN
|
||||
#define DEBUG_PRINTLN Serial.println ///< where to print the debug output
|
||||
#endif
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Class Constructor
|
||||
@param FS_PIN frame sync pin
|
||||
@param SCK_PIN bit clock pin
|
||||
@param TX_PIN data output pin
|
||||
@param RX_PIN data input pin
|
||||
*/
|
||||
/**************************************************************************/
|
||||
I2S_class::I2S_class(uint8_t FS_PIN, uint8_t SCK_PIN,
|
||||
uint8_t TX_PIN, uint8_t RX_PIN)
|
||||
: _fs(FS_PIN), _sck(SCK_PIN), _tx(TX_PIN), _rx(RX_PIN) {}
|
||||
|
||||
#if defined(PIN_I2S_SDI) && defined(PIN_I2S_SDO)
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Class Constructor with defaults
|
||||
*/
|
||||
/**************************************************************************/
|
||||
I2S_class::I2S_class()
|
||||
: _fs(PIN_I2S_FS), _sck(PIN_I2S_SCK), _tx(PIN_I2S_SDO), _rx(PIN_I2S_SDI) {}
|
||||
#else
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Class Constructor with defaults
|
||||
*/
|
||||
/**************************************************************************/
|
||||
Adafruit_ZeroI2S::Adafruit_ZeroI2S()
|
||||
: _fs(PIN_I2S_FS), _sck(PIN_I2S_SCK), _tx(PIN_I2S_SD) {
|
||||
_rx = -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief start up the I2S peripheral
|
||||
@param width the width of each I2S frame
|
||||
@param fs_freq the frame sync frequency (a.k.a. sample rate)
|
||||
@param mck_mult master clock output will be fs_freq * mck_mult for chips
|
||||
that have a mclk.
|
||||
@returns true on success, false on any error
|
||||
*/
|
||||
/**************************************************************************/
|
||||
bool I2S_class::begin(I2SSlotSize width, int fs_freq, int mck_mult) {
|
||||
#if defined(__SAMD51__)
|
||||
|
||||
pinPeripheral(_fs, PIO_I2S);
|
||||
pinPeripheral(_sck, PIO_I2S);
|
||||
pinPeripheral(_rx, PIO_I2S);
|
||||
pinPeripheral(_tx, PIO_I2S);
|
||||
|
||||
I2S->CTRLA.bit.ENABLE = 0;
|
||||
|
||||
// initialize clock control
|
||||
MCLK->APBDMASK.reg |= MCLK_APBDMASK_I2S;
|
||||
|
||||
uint32_t mckFreq = (fs_freq * mck_mult);
|
||||
uint32_t sckFreq = fs_freq * I2S_NUM_SLOTS * ((width + 1) << 3);
|
||||
|
||||
uint32_t gclkval = GCLK_PCHCTRL_GEN_GCLK1_Val;
|
||||
uint32_t gclkFreq = VARIANT_GCLK1_FREQ;
|
||||
uint8_t mckoutdiv = min((gclkFreq / mckFreq) - 1U, 63U);
|
||||
uint8_t mckdiv = min((gclkFreq / sckFreq) - 1U, 63U);
|
||||
|
||||
if (((VARIANT_GCLK1_FREQ / mckFreq) - 1) > 63) {
|
||||
gclkval = GCLK_PCHCTRL_GEN_GCLK4_Val;
|
||||
gclkFreq = 12000000;
|
||||
}
|
||||
|
||||
GCLK->PCHCTRL[I2S_GCLK_ID_0].reg = gclkval | (1 << GCLK_PCHCTRL_CHEN_Pos);
|
||||
GCLK->PCHCTRL[I2S_GCLK_ID_1].reg = gclkval | (1 << GCLK_PCHCTRL_CHEN_Pos);
|
||||
|
||||
// software reset
|
||||
I2S->CTRLA.bit.SWRST = 1;
|
||||
while (I2S->SYNCBUSY.bit.SWRST || I2S->SYNCBUSY.bit.ENABLE)
|
||||
; // wait for sync
|
||||
|
||||
// CLKCTRL[0] is used for the tx channel
|
||||
I2S->CLKCTRL[0].reg =
|
||||
I2S_CLKCTRL_MCKSEL_GCLK | I2S_CLKCTRL_MCKOUTDIV(mckoutdiv) |
|
||||
I2S_CLKCTRL_MCKDIV(mckdiv) | I2S_CLKCTRL_SCKSEL_MCKDIV |
|
||||
I2S_CLKCTRL_MCKEN | I2S_CLKCTRL_FSSEL_SCKDIV | I2S_CLKCTRL_BITDELAY_I2S |
|
||||
I2S_CLKCTRL_FSWIDTH_HALF | I2S_CLKCTRL_NBSLOTS(I2S_NUM_SLOTS - 1) |
|
||||
I2S_CLKCTRL_SLOTSIZE(width);
|
||||
|
||||
uint8_t wordSize=0;
|
||||
|
||||
switch (width) {
|
||||
case I2S_8_BIT:
|
||||
wordSize = I2S_TXCTRL_DATASIZE_8_Val;
|
||||
break;
|
||||
case I2S_16_BIT:
|
||||
wordSize = I2S_TXCTRL_DATASIZE_16_Val;
|
||||
break;
|
||||
case I2S_24_BIT:
|
||||
wordSize = I2S_TXCTRL_DATASIZE_24_Val;
|
||||
break;
|
||||
case I2S_32_BIT:
|
||||
wordSize = I2S_TXCTRL_DATASIZE_32_Val;
|
||||
break;
|
||||
}
|
||||
|
||||
I2S->TXCTRL.reg = I2S_TXCTRL_DMA_SINGLE | I2S_TXCTRL_MONO_STEREO |
|
||||
I2S_TXCTRL_BITREV_MSBIT | I2S_TXCTRL_EXTEND_ZERO |
|
||||
I2S_TXCTRL_WORDADJ_RIGHT | I2S_TXCTRL_DATASIZE(wordSize) |
|
||||
I2S_TXCTRL_TXSAME_ZERO | I2S_TXCTRL_TXDEFAULT_ZERO;
|
||||
|
||||
I2S->RXCTRL.reg = I2S_RXCTRL_DMA_SINGLE | I2S_RXCTRL_MONO_STEREO |
|
||||
I2S_RXCTRL_BITREV_MSBIT | I2S_RXCTRL_EXTEND_ZERO |
|
||||
I2S_RXCTRL_WORDADJ_RIGHT | I2S_RXCTRL_DATASIZE(wordSize) |
|
||||
I2S_RXCTRL_SLOTADJ_RIGHT | I2S_RXCTRL_CLKSEL_CLK0 |
|
||||
I2S_RXCTRL_SERMODE_RX;
|
||||
|
||||
while (I2S->SYNCBUSY.bit.ENABLE)
|
||||
; // wait for sync
|
||||
I2S->CTRLA.bit.ENABLE = 1;
|
||||
|
||||
return true;
|
||||
|
||||
#else // SAMD21
|
||||
_i2sserializer = -1;
|
||||
_i2sclock = -1;
|
||||
uint32_t _clk_pin, _clk_mux, _data_pin, _data_mux, _fs_pin, _fs_mux;
|
||||
|
||||
// Clock pin, can only be one of 3 options
|
||||
uint32_t clockport = g_APinDescription[_sck].ulPort;
|
||||
uint32_t clockpin = g_APinDescription[_sck].ulPin;
|
||||
if ((clockport == 0) && (clockpin == 10)) {
|
||||
// PA10
|
||||
_i2sclock = 0;
|
||||
_clk_pin = PIN_PA10G_I2S_SCK0;
|
||||
_clk_mux = MUX_PA10G_I2S_SCK0;
|
||||
#if defined(PIN_PB11G_I2S_SCK1)
|
||||
} else if ((clockport == 1) && (clockpin == 11)) {
|
||||
// PB11
|
||||
_i2sclock = 1;
|
||||
_clk_pin = PIN_PB11G_I2S_SCK1;
|
||||
_clk_mux = MUX_PB11G_I2S_SCK1;
|
||||
#endif
|
||||
#if defined(PIN_PA20G_I2S_SCK0)
|
||||
} else if ((clockport == 0) && (clockpin == 20)) {
|
||||
// PA20
|
||||
_i2sclock = 0;
|
||||
_clk_pin = PIN_PA20G_I2S_SCK0;
|
||||
_clk_mux = MUX_PA20G_I2S_SCK0;
|
||||
#endif
|
||||
} else {
|
||||
DEBUG_PRINTLN("Clock isnt on a valid pin");
|
||||
return false;
|
||||
}
|
||||
pinPeripheral(_sck, (EPioType)_clk_mux);
|
||||
|
||||
// FS pin, can only be one of 2 options
|
||||
uint32_t fsport = g_APinDescription[_fs].ulPort;
|
||||
uint32_t fspin = g_APinDescription[_fs].ulPin;
|
||||
if ((fsport == 0) && (fspin == 11)) {
|
||||
// PA11
|
||||
_fs_pin = PIN_PA11G_I2S_FS0;
|
||||
_fs_mux = MUX_PA11G_I2S_FS0;
|
||||
#if defined(PIN_PA21G_I2S_FS0)
|
||||
} else if ((fsport == 0) && (fspin == 21)) {
|
||||
// PA21
|
||||
_fs_pin = PIN_PA21G_I2S_FS0;
|
||||
_fs_mux = MUX_PA21G_I2S_FS0;
|
||||
#endif
|
||||
} else {
|
||||
DEBUG_PRINTLN("FS isnt on a valid pin");
|
||||
return false;
|
||||
}
|
||||
pinPeripheral(_fs, (EPioType)_fs_mux);
|
||||
|
||||
uint32_t i2sGCLK;
|
||||
if (_i2sclock == 0)
|
||||
i2sGCLK = I2S_GCLK_ID_0;
|
||||
else
|
||||
i2sGCLK = I2S_GCLK_ID_1;
|
||||
|
||||
uint32_t divider = fs_freq * 2 * (width + 1) * 8;
|
||||
// configure the clock divider
|
||||
while (GCLK->STATUS.bit.SYNCBUSY)
|
||||
;
|
||||
GCLK->GENDIV.bit.ID = I2S_CLOCK_GENERATOR;
|
||||
GCLK->GENDIV.bit.DIV = SystemCoreClock / divider;
|
||||
|
||||
// use the DFLL as the source
|
||||
while (GCLK->STATUS.bit.SYNCBUSY)
|
||||
;
|
||||
GCLK->GENCTRL.bit.ID = I2S_CLOCK_GENERATOR;
|
||||
GCLK->GENCTRL.bit.SRC = GCLK_GENCTRL_SRC_DFLL48M_Val;
|
||||
GCLK->GENCTRL.bit.IDC = 1;
|
||||
GCLK->GENCTRL.bit.GENEN = 1;
|
||||
|
||||
// enable
|
||||
while (GCLK->STATUS.bit.SYNCBUSY)
|
||||
;
|
||||
GCLK->CLKCTRL.bit.ID = i2sGCLK;
|
||||
GCLK->CLKCTRL.bit.GEN = I2S_CLOCK_GENERATOR;
|
||||
GCLK->CLKCTRL.bit.CLKEN = 1;
|
||||
|
||||
while (GCLK->STATUS.bit.SYNCBUSY)
|
||||
;
|
||||
|
||||
// Data pin, can only be one of 3 options
|
||||
uint32_t datapin = g_APinDescription[_tx].ulPin;
|
||||
uint32_t dataport = g_APinDescription[_tx].ulPort;
|
||||
if ((dataport == 0) && (datapin == 7)) {
|
||||
// PA07
|
||||
_i2sserializer = 0;
|
||||
_data_pin = PIN_PA07G_I2S_SD0;
|
||||
_data_mux = MUX_PA07G_I2S_SD0;
|
||||
} else if ((dataport == 0) && (datapin == 8)) {
|
||||
// PA08
|
||||
_i2sserializer = 1;
|
||||
_data_pin = PIN_PA08G_I2S_SD1;
|
||||
_data_mux = MUX_PA08G_I2S_SD1;
|
||||
} else if ((dataport == 0) && (datapin == 19)) {
|
||||
// PA19
|
||||
_i2sserializer = 0;
|
||||
_data_pin = PIN_PA19G_I2S_SD0;
|
||||
_data_mux = MUX_PA19G_I2S_SD0;
|
||||
} else {
|
||||
DEBUG_PRINTLN("Data isnt on a valid pin");
|
||||
return false;
|
||||
}
|
||||
pinPeripheral(_tx, (EPioType)_data_mux);
|
||||
|
||||
PM->APBCMASK.reg |= PM_APBCMASK_I2S;
|
||||
|
||||
I2S->CTRLA.bit.ENABLE = 0;
|
||||
while (I2S->SYNCBUSY.bit.ENABLE)
|
||||
;
|
||||
|
||||
if (_i2sclock == 0)
|
||||
I2S->CTRLA.bit.CKEN0 = 0;
|
||||
else
|
||||
I2S->CTRLA.bit.CKEN1 = 0;
|
||||
while (I2S->SYNCBUSY.bit.CKEN0 || I2S->SYNCBUSY.bit.CKEN1)
|
||||
;
|
||||
|
||||
I2S->CLKCTRL[_i2sclock].reg =
|
||||
I2S_CLKCTRL_MCKSEL_GCLK | I2S_CLKCTRL_SCKSEL_MCKDIV |
|
||||
I2S_CLKCTRL_FSSEL_SCKDIV | I2S_CLKCTRL_BITDELAY_I2S |
|
||||
I2S_CLKCTRL_NBSLOTS(I2S_NUM_SLOTS - 1) | I2S_CLKCTRL_SLOTSIZE(width);
|
||||
|
||||
uint8_t wordSize;
|
||||
switch (width) {
|
||||
case I2S_8_BIT:
|
||||
wordSize = I2S_SERCTRL_DATASIZE_8_Val;
|
||||
break;
|
||||
case I2S_16_BIT:
|
||||
wordSize = I2S_SERCTRL_DATASIZE_16_Val;
|
||||
break;
|
||||
case I2S_24_BIT:
|
||||
wordSize = I2S_SERCTRL_DATASIZE_24_Val;
|
||||
break;
|
||||
case I2S_32_BIT:
|
||||
wordSize = I2S_SERCTRL_DATASIZE_32_Val;
|
||||
break;
|
||||
default:
|
||||
DEBUG_PRINTLN("invalid width!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_i2sserializer == 0)
|
||||
I2S->CTRLA.bit.SEREN0 = 0;
|
||||
else
|
||||
I2S->CTRLA.bit.SEREN1 = 0;
|
||||
while (I2S->SYNCBUSY.bit.SEREN0 || I2S->SYNCBUSY.bit.SEREN1)
|
||||
;
|
||||
|
||||
I2S->SERCTRL[_i2sserializer].reg =
|
||||
I2S_SERCTRL_DMA_SINGLE | I2S_SERCTRL_MONO_STEREO |
|
||||
I2S_SERCTRL_BITREV_MSBIT | I2S_SERCTRL_EXTEND_ZERO |
|
||||
I2S_SERCTRL_WORDADJ_RIGHT | I2S_SERCTRL_DATASIZE(wordSize) |
|
||||
I2S_SERCTRL_SLOTADJ_RIGHT |
|
||||
((uint32_t)_i2sclock << I2S_SERCTRL_CLKSEL_Pos);
|
||||
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief enable data output. Note that on SAMD21 chips either rx or tx can be
|
||||
enabled on an Adafruit_ZeroI2S instance, while on SAMD51 the same
|
||||
Adafruit_ZeroI2S instance can have both rx and tx channels enabled.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
void I2S_class::enableTx() {
|
||||
#if defined(__SAMD51__)
|
||||
I2S->CTRLA.bit.CKEN0 = 1;
|
||||
while (I2S->SYNCBUSY.bit.CKEN0)
|
||||
;
|
||||
|
||||
I2S->CTRLA.bit.TXEN = 1;
|
||||
while (I2S->SYNCBUSY.bit.TXEN)
|
||||
;
|
||||
#else
|
||||
if (_i2sserializer > -1 && _i2sclock > -1) {
|
||||
I2S->CTRLA.bit.ENABLE = 0;
|
||||
while (I2S->SYNCBUSY.bit.ENABLE)
|
||||
;
|
||||
|
||||
I2S->SERCTRL[_i2sserializer].bit.SERMODE = I2S_SERCTRL_SERMODE_TX;
|
||||
|
||||
if (_i2sserializer == 0)
|
||||
I2S->CTRLA.bit.SEREN0 = 1;
|
||||
else
|
||||
I2S->CTRLA.bit.SEREN1 = 1;
|
||||
|
||||
if (_i2sclock == 0)
|
||||
I2S->CTRLA.bit.CKEN0 = 1;
|
||||
else
|
||||
I2S->CTRLA.bit.CKEN1 = 1;
|
||||
|
||||
I2S->CTRLA.bit.ENABLE = 1;
|
||||
while (I2S->SYNCBUSY.bit.ENABLE || I2S->SYNCBUSY.bit.CKEN0 ||
|
||||
I2S->SYNCBUSY.bit.CKEN1 || I2S->SYNCBUSY.bit.SEREN0 ||
|
||||
I2S->SYNCBUSY.bit.SEREN1)
|
||||
;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief disable data output
|
||||
*/
|
||||
/**************************************************************************/
|
||||
void I2S_class::disableTx() {
|
||||
#if defined(__SAMD51__)
|
||||
I2S->CTRLA.bit.TXEN = 0;
|
||||
while (I2S->SYNCBUSY.bit.TXEN)
|
||||
;
|
||||
#else
|
||||
#endif
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief enable data input. Note that on SAMD21 chips either rx or tx can be
|
||||
enabled on an Adafruit_ZeroI2S instance, while on SAMD51 the same
|
||||
Adafruit_ZeroI2S instance can have both rx and tx channels enabled.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
void I2S_class::enableRx() {
|
||||
#if defined(__SAMD51__)
|
||||
I2S->CTRLA.bit.CKEN0 = 1;
|
||||
while (I2S->SYNCBUSY.bit.CKEN0)
|
||||
;
|
||||
|
||||
I2S->CTRLA.bit.RXEN = 1;
|
||||
while (I2S->SYNCBUSY.bit.RXEN)
|
||||
;
|
||||
#else
|
||||
if (_i2sserializer > -1 && _i2sclock > -1) {
|
||||
I2S->CTRLA.bit.ENABLE = 0;
|
||||
while (I2S->SYNCBUSY.bit.ENABLE)
|
||||
;
|
||||
|
||||
I2S->SERCTRL[_i2sserializer].bit.SERMODE = I2S_SERCTRL_SERMODE_RX;
|
||||
|
||||
if (_i2sserializer == 0)
|
||||
I2S->CTRLA.bit.SEREN0 = 1;
|
||||
else
|
||||
I2S->CTRLA.bit.SEREN1 = 1;
|
||||
|
||||
if (_i2sclock == 0)
|
||||
I2S->CTRLA.bit.CKEN0 = 1;
|
||||
else
|
||||
I2S->CTRLA.bit.CKEN1 = 1;
|
||||
|
||||
I2S->CTRLA.bit.ENABLE = 1;
|
||||
while (I2S->SYNCBUSY.bit.ENABLE || I2S->SYNCBUSY.bit.CKEN0 ||
|
||||
I2S->SYNCBUSY.bit.CKEN1 || I2S->SYNCBUSY.bit.SEREN0 ||
|
||||
I2S->SYNCBUSY.bit.SEREN1)
|
||||
;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief disable data input
|
||||
*/
|
||||
/**************************************************************************/
|
||||
void I2S_class::disableRx() {
|
||||
#if defined(__SAMD51__)
|
||||
I2S->CTRLA.bit.RXEN = 0;
|
||||
while (I2S->SYNCBUSY.bit.RXEN)
|
||||
;
|
||||
#else
|
||||
#endif
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief enable master clock output on devices that have a master clock
|
||||
output.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
void I2S_class::enableMCLK() {
|
||||
#ifdef PIN_I2S_MCK
|
||||
pinPeripheral(PIN_I2S_MCK, PIO_I2S);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief disable master clock output on devices that have a master clock
|
||||
output.
|
||||
*/
|
||||
/**************************************************************************/
|
||||
void I2S_class::disableMCLK() {
|
||||
#ifdef PIN_I2S_MCK
|
||||
pinMode(PIN_I2S_MCK, INPUT);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief check if data can be written to the TX data register
|
||||
@returns true if data can be written, false otherwise
|
||||
*/
|
||||
/**************************************************************************/
|
||||
bool I2S_class::txReady() {
|
||||
#if defined(__SAMD51__)
|
||||
return !((!I2S->INTFLAG.bit.TXRDY0) || I2S->SYNCBUSY.bit.TXDATA);
|
||||
#else
|
||||
if (_i2sserializer > -1) {
|
||||
if (_i2sserializer == 0) {
|
||||
return !((!I2S->INTFLAG.bit.TXRDY0) || I2S->SYNCBUSY.bit.DATA0);
|
||||
} else {
|
||||
return !((!I2S->INTFLAG.bit.TXRDY1) || I2S->SYNCBUSY.bit.DATA1);
|
||||
}
|
||||
} else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief check if data is available to be read from the RX data register
|
||||
@returns true if data is available, false otherwise
|
||||
*/
|
||||
/**************************************************************************/
|
||||
bool I2S_class::rxReady() {
|
||||
#if defined(__SAMD51__)
|
||||
return !((!I2S->INTFLAG.bit.RXRDY0) || I2S->SYNCBUSY.bit.RXDATA);
|
||||
#else
|
||||
if (_i2sserializer > -1) {
|
||||
if (_i2sserializer == 0) {
|
||||
return !((!I2S->INTFLAG.bit.RXRDY0) || I2S->SYNCBUSY.bit.DATA0);
|
||||
} else {
|
||||
return !((!I2S->INTFLAG.bit.RXRDY1) || I2S->SYNCBUSY.bit.DATA1);
|
||||
}
|
||||
} else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief perform a blocking write to the I2S peripheral. This function will
|
||||
only return once all data has been sent.
|
||||
@param left the left channel data
|
||||
@param right the right channel data
|
||||
*/
|
||||
/**************************************************************************/
|
||||
void I2S_class::write(int32_t left, int32_t right) {
|
||||
#if defined(__SAMD51__)
|
||||
while ((!I2S->INTFLAG.bit.TXRDY0) || I2S->SYNCBUSY.bit.TXDATA)
|
||||
;
|
||||
I2S->INTFLAG.bit.TXUR0 = 1;
|
||||
I2S->TXDATA.reg = left;
|
||||
|
||||
while ((!I2S->INTFLAG.bit.TXRDY0) || I2S->SYNCBUSY.bit.TXDATA)
|
||||
;
|
||||
I2S->INTFLAG.bit.TXUR0 = 1;
|
||||
I2S->TXDATA.reg = right;
|
||||
#else
|
||||
if (_i2sserializer > -1) {
|
||||
if (_i2sserializer == 0) {
|
||||
while ((!I2S->INTFLAG.bit.TXRDY0) || I2S->SYNCBUSY.bit.DATA0)
|
||||
;
|
||||
I2S->INTFLAG.bit.TXUR0 = 1;
|
||||
I2S->DATA[0].reg = left;
|
||||
|
||||
while ((!I2S->INTFLAG.bit.TXRDY0) || I2S->SYNCBUSY.bit.DATA0)
|
||||
;
|
||||
I2S->INTFLAG.bit.TXUR0 = 1;
|
||||
I2S->DATA[0].reg = right;
|
||||
} else {
|
||||
while ((!I2S->INTFLAG.bit.TXRDY1) || I2S->SYNCBUSY.bit.DATA1)
|
||||
;
|
||||
I2S->INTFLAG.bit.TXUR1 = 1;
|
||||
I2S->DATA[1].reg = left;
|
||||
|
||||
while ((!I2S->INTFLAG.bit.TXRDY1) || I2S->SYNCBUSY.bit.DATA1)
|
||||
;
|
||||
I2S->INTFLAG.bit.TXUR1 = 1;
|
||||
I2S->DATA[1].reg = right;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief perform a blocking read to the I2S peripheral. This function will
|
||||
only return once all data has been read.
|
||||
@param left pointer to where the left channel data will be written
|
||||
@param right pointer to where the right channel data will be written
|
||||
*/
|
||||
/**************************************************************************/
|
||||
void I2S_class::read(int32_t *left, int32_t *right) {
|
||||
#if defined(__SAMD51__)
|
||||
while ((!I2S->INTFLAG.bit.RXRDY0) || I2S->SYNCBUSY.bit.RXDATA)
|
||||
;
|
||||
*left = I2S->RXDATA.reg;
|
||||
|
||||
while ((!I2S->INTFLAG.bit.RXRDY0) || I2S->SYNCBUSY.bit.RXDATA)
|
||||
;
|
||||
*right = I2S->RXDATA.reg;
|
||||
#else
|
||||
if (_i2sserializer > -1) {
|
||||
if (_i2sserializer == 0) {
|
||||
while ((!I2S->INTFLAG.bit.RXRDY0) || I2S->SYNCBUSY.bit.DATA0)
|
||||
;
|
||||
*left = I2S->DATA[0].reg;
|
||||
|
||||
while ((!I2S->INTFLAG.bit.RXRDY0) || I2S->SYNCBUSY.bit.DATA0)
|
||||
;
|
||||
*right = I2S->DATA[0].reg;
|
||||
} else {
|
||||
while ((!I2S->INTFLAG.bit.RXRDY1) || I2S->SYNCBUSY.bit.DATA1)
|
||||
;
|
||||
*left = I2S->DATA[1].reg;
|
||||
|
||||
while ((!I2S->INTFLAG.bit.RXRDY1) || I2S->SYNCBUSY.bit.DATA1)
|
||||
;
|
||||
*right = I2S->DATA[1].reg;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
/*!
|
||||
* @file Adafruit_ZeroI2S.h
|
||||
*
|
||||
* This is a library for the I2S peripheral on SAMD21 and SAMD51 devices
|
||||
*
|
||||
* Adafruit invests time and resources providing this open source code,
|
||||
* please support Adafruit and open-source hardware by purchasing
|
||||
* products from Adafruit!
|
||||
*
|
||||
* Written by Dean Miller for Adafruit Industries.
|
||||
*
|
||||
* BSD license, all text here must be included in any redistribution.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef ADAFRUIT_ZEROI2S_H
|
||||
#define ADAFRUIT_ZEROI2S_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief available I2S slot sizes
|
||||
*/
|
||||
/**************************************************************************/
|
||||
typedef enum _I2SSlotSize {
|
||||
I2S_8_BIT = 0,
|
||||
I2S_16_BIT,
|
||||
I2S_24_BIT,
|
||||
I2S_32_BIT
|
||||
} I2SSlotSize;
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief number of I2S slots to use (stereo)
|
||||
*/
|
||||
/**************************************************************************/
|
||||
#define I2S_NUM_SLOTS 2
|
||||
|
||||
/**************************************************************************/
|
||||
/*!
|
||||
@brief Class that stores state and functions for interacting with I2S
|
||||
peripheral on SAMD21 and SAMD51 devices
|
||||
*/
|
||||
/**************************************************************************/
|
||||
class I2S_class {
|
||||
public:
|
||||
I2S_class(uint8_t FS_PIN, uint8_t SCK_PIN, uint8_t TX_PIN,
|
||||
uint8_t RX_PIN);
|
||||
I2S_class();
|
||||
~I2S_class() {}
|
||||
|
||||
bool begin(I2SSlotSize width, int fs_freq, int mck_mult = 256);
|
||||
|
||||
void enableTx();
|
||||
void disableTx();
|
||||
void enableRx();
|
||||
void disableRx();
|
||||
void enableMCLK();
|
||||
void disableMCLK();
|
||||
|
||||
bool txReady();
|
||||
bool rxReady();
|
||||
void write(int32_t left, int32_t right);
|
||||
void read(int32_t *left, int32_t *right);
|
||||
|
||||
private:
|
||||
int8_t _fs, _sck, _tx, _rx;
|
||||
#ifndef __SAMD51__
|
||||
int8_t _i2sserializer, _i2sclock;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,106 @@
|
||||
// Arduino Zero / Feather M0 I2S audio tone generation example.
|
||||
// Author: Tony DiCola
|
||||
//
|
||||
// Connect an I2S DAC or amp (like the UDA1334A) to the Arduino Zero
|
||||
// and play back simple sine, sawtooth, triangle, and square waves.
|
||||
// Makes your Zero sound like a NES!
|
||||
//
|
||||
// NOTE: The I2S signal generated by the Zero does NOT have a MCLK /
|
||||
// master clock signal. You must use an I2S receiver that can operate
|
||||
// without a MCLK signal (like the UDA1334A).
|
||||
//
|
||||
// For an Arduino Zero / Feather M0 connect it to you I2S hardware as follows:
|
||||
// - Digital 0 -> I2S LRCLK / FS (left/right / frame select clock)
|
||||
// - Digital 1 -> I2S BCLK / SCLK (bit / serial clock)
|
||||
// - Digital 9 -> I2S DIN / SD (data output)
|
||||
// - Ground
|
||||
//
|
||||
// Released under a MIT license: https://opensource.org/licenses/MIT
|
||||
#include "audio.h"
|
||||
|
||||
#include <I2S.h>
|
||||
#include <DMA.h>
|
||||
#include "utility/dma.h"
|
||||
#include <math.h>
|
||||
|
||||
#include "storage.h"
|
||||
|
||||
/* max volume for 32 bit data */
|
||||
#define VOLUME ( (1UL << 31) - 1)
|
||||
|
||||
/* create a buffer for both the left and right channel data */
|
||||
#define BUFSIZE 256
|
||||
int data[BUFSIZE];
|
||||
|
||||
DMA_class myDMA;
|
||||
DMAstatus stat; // DMA status codes returned by some functions
|
||||
|
||||
I2S_class i2s;
|
||||
|
||||
bool transfer_is_done = false;
|
||||
bool playbackIsDone = true;
|
||||
|
||||
void dma_callback(DMA_class *dma) {
|
||||
/* we don't need to do anything here */
|
||||
transfer_is_done = true;
|
||||
}
|
||||
|
||||
void initAudio(void)
|
||||
{
|
||||
Serial.begin(115200);
|
||||
//while(!Serial); // Wait for Serial monitor before continuing
|
||||
|
||||
Serial.println("I2S output via DMA");
|
||||
|
||||
int *ptr = data;
|
||||
|
||||
/*the I2S module will be expecting data interleaved LRLR*/
|
||||
for(int i=0; i<BUFSIZE/2; i++){
|
||||
/* create a sine wave on the left channel */
|
||||
*ptr++ = sin( (2*PI / (BUFSIZE/2) ) * i) * VOLUME;
|
||||
|
||||
/* create a cosine wave on the right channel */
|
||||
*ptr++ = cos( (2*PI / (BUFSIZE/2) ) * i) * VOLUME;
|
||||
}
|
||||
|
||||
Serial.println("Configuring DMA trigger");
|
||||
myDMA.setTrigger(I2S_DMAC_ID_TX_0);
|
||||
myDMA.setAction(DMA_TRIGGER_ACTON_BEAT);
|
||||
|
||||
Serial.print("Allocating DMA channel...");
|
||||
stat = myDMA.allocate();
|
||||
myDMA.printStatus(stat);
|
||||
|
||||
Serial.println("Setting up transfer");
|
||||
myDMA.addDescriptor(
|
||||
data, // move data from here
|
||||
#if defined(__SAMD51__)
|
||||
(void *)(&I2S->TXDATA.reg), // to here (M4)
|
||||
#else
|
||||
(void *)(&I2S->DATA[0].reg), // to here (M0+)
|
||||
#endif
|
||||
BUFSIZE, // this many...
|
||||
DMA_BEAT_SIZE_WORD, // bytes/hword/words
|
||||
true, // increment source addr?
|
||||
false);
|
||||
myDMA.loop(true);
|
||||
Serial.println("Adding callback");
|
||||
myDMA.setCallback(dma_callback);
|
||||
|
||||
/* begin I2S on the default pins. 24 bit depth at
|
||||
* 44100 samples per second
|
||||
*/
|
||||
i2s.begin(I2S_32_BIT, 44100);
|
||||
i2s.enableTx();
|
||||
|
||||
stat = myDMA.startJob();
|
||||
}
|
||||
|
||||
uint32_t filendex = 0;
|
||||
uint32_t filesize = 0;
|
||||
File musicFileHandle;
|
||||
|
||||
void handleAudio(void)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "arduino.h"
|
||||
|
||||
|
||||
void initAudio(void);
|
||||
void handleAudio(void);
|
||||
@@ -0,0 +1,14 @@
|
||||
#include "audio.h"
|
||||
#include "storage.h"
|
||||
|
||||
void setup()
|
||||
{
|
||||
initAudio();
|
||||
initStorage();
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
handleAudio();
|
||||
handleStorage();
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "storage.h"
|
||||
|
||||
File root;
|
||||
|
||||
|
||||
void printDirectory(File dir, int numTabs) {
|
||||
// Begin at the start of the directory
|
||||
dir.rewindDirectory();
|
||||
|
||||
while(true) {
|
||||
File entry = dir.openNextFile();
|
||||
if (! entry) {
|
||||
// no more files
|
||||
//Serial.println("**nomorefiles**");
|
||||
break;
|
||||
}
|
||||
for (uint8_t i=0; i<numTabs; i++) {
|
||||
Serial.print('\t'); // we'll have a nice indentation
|
||||
}
|
||||
// Print the 8.3 name
|
||||
Serial.print(entry.name());
|
||||
// Recurse for directories, otherwise print the file size
|
||||
if (entry.isDirectory()) {
|
||||
Serial.println("/");
|
||||
printDirectory(entry, numTabs+1);
|
||||
} else {
|
||||
// files have sizes, directories do not
|
||||
Serial.print("\t\t");
|
||||
Serial.println(entry.size(), DEC);
|
||||
}
|
||||
entry.close();
|
||||
}
|
||||
}
|
||||
|
||||
void initStorage(void)
|
||||
{
|
||||
if(!SD.begin(A5))
|
||||
{
|
||||
Serial.println("Failed to init SDcard");
|
||||
}
|
||||
|
||||
root = SD.open("/");
|
||||
}
|
||||
|
||||
void handleStorage(void)
|
||||
{
|
||||
//printDirectory(root, 0);
|
||||
|
||||
}
|
||||
|
||||
File getmusicFile(void)
|
||||
{
|
||||
if(SD.exists("/003.WAV"))
|
||||
{
|
||||
return SD.open("/003.WAV");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "SPI.h"
|
||||
#include "SD.h"
|
||||
|
||||
void initStorage(void);
|
||||
void handleStorage(void);
|
||||
|
||||
File getmusicFile(void);
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user