284 lines
7.9 KiB
C++
284 lines
7.9 KiB
C++
/*
|
|
SinglePinI2SDriver
|
|
ESP8266Audio I2S Minimal driver
|
|
|
|
Most of this code is taken and reworked from ESP8266 Arduino core,
|
|
which itself is reworked from Espessif's I2S examples.
|
|
Original code is licensed under LGPL 2.1 or above
|
|
|
|
Reasons for rewrite:
|
|
- Non-configurable size of DMA buffers
|
|
- We only need GPIO3 connected to I2SO_DATA
|
|
- No API to queue data from ISR. Callbacks are unusable
|
|
as i2s_write_* functions are not in IRAM and may re-enable
|
|
SLC interrupt before returning
|
|
- ISR overhead is not needed
|
|
|
|
Copyright (C) 2020 Ivan Kostoski
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2.1 of the License, or (at your option) any later version.
|
|
|
|
This library is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
|
|
#if defined(ESP8266)
|
|
|
|
#include "SinglePinI2SDriver.h"
|
|
|
|
SinglePinI2SDriver::~SinglePinI2SDriver()
|
|
{
|
|
stopI2S();
|
|
freeBuffers(bufCount);
|
|
}
|
|
|
|
bool SinglePinI2SDriver::begin(uint8_t bufCount, uint16_t bufSize)
|
|
{
|
|
if (bufCount < 4) return false;
|
|
if ((bufSize < 32) || (bufSize > 1020) || ((bufSize % 4) != 0)) return false;
|
|
|
|
stopSLC();
|
|
|
|
if ((bufCount != this->bufCount) || (bufSize != this->bufSize)) freeBuffers(this->bufCount);
|
|
this->bufCount = bufCount;
|
|
this->bufSize = bufSize;
|
|
if (!allocateBuffers()) return false;
|
|
|
|
zeroBuffers();
|
|
configureSLC();
|
|
startI2S();
|
|
|
|
// Send out at least one buffer, so we have valid address of
|
|
// finished descriptor in SLCRXEDA
|
|
head = &descriptors[0];
|
|
head->owner = 1;
|
|
startSLC();
|
|
while (!lastSentDescriptor()) delayMicroseconds(1);
|
|
// By here, SLC should be stalled (next descriptor owner is 0)
|
|
head->owner = 0;
|
|
head = head->next_link_ptr;
|
|
headPos = 0;
|
|
underflowCount = -1;
|
|
return true;
|
|
};
|
|
|
|
void SinglePinI2SDriver::stop()
|
|
{
|
|
stopI2S();
|
|
}
|
|
|
|
bool SinglePinI2SDriver::allocateBuffers()
|
|
{
|
|
// Allocate output (RXLINK) descriptors and bufferss
|
|
if (descriptors) return true;
|
|
descriptors = (SLCDecriptor*) calloc(bufCount, sizeof(SLCDecriptor));
|
|
if (!descriptors) return false;
|
|
int allocated;
|
|
for (allocated = 0; allocated < bufCount; allocated++) {
|
|
uint32_t* buffer = (uint32_t*)malloc(bufSize * sizeof(uint32_t));
|
|
if (!buffer) break;
|
|
auto descriptor = &descriptors[allocated];
|
|
descriptor->eof = 1; // Needed for SLC to update SLCRXEDA
|
|
descriptor->datalen = bufSize * sizeof(uint32_t);
|
|
descriptor->blocksize = bufSize * sizeof(uint32_t);
|
|
descriptor->buf_ptr = buffer;
|
|
descriptor->next_link_ptr = &descriptors[(allocated+1) % bufCount];
|
|
}
|
|
// Release memory if not all buffers were allocated
|
|
if (allocated < bufCount) {
|
|
freeBuffers(allocated);
|
|
return false;
|
|
}
|
|
zeroBuffers();
|
|
return true;
|
|
}
|
|
|
|
void SinglePinI2SDriver::freeBuffers(int allocated)
|
|
{
|
|
if (!descriptors) return;
|
|
while (--allocated >= 0) {
|
|
if (descriptors[allocated].buf_ptr) {
|
|
free(descriptors[allocated].buf_ptr);
|
|
descriptors[allocated].buf_ptr = NULL;
|
|
}
|
|
}
|
|
free(descriptors);
|
|
descriptors = NULL;
|
|
}
|
|
|
|
void SinglePinI2SDriver::zeroBuffers()
|
|
{
|
|
for(int i = 0; i < bufCount; i++) {
|
|
auto descriptor = &descriptors[i];
|
|
ets_memset((void *)descriptor->buf_ptr, 0, bufSize * sizeof(uint32_t));
|
|
}
|
|
}
|
|
|
|
inline void SinglePinI2SDriver::advanceHead(const SLCDecriptor *lastSent)
|
|
{
|
|
if (headPos >= bufSize) {
|
|
head->owner = 1; // Owned by SLC
|
|
head = head->next_link_ptr;
|
|
head->owner = 0; // Owned by CPU
|
|
headPos = 0;
|
|
// If SLC stopped, fill-up the buffers before (re)starting
|
|
if (isSLCStopped() && (head->next_link_ptr == lastSent)) {
|
|
underflowCount++;
|
|
startSLC();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SinglePinI2SDriver::write(uint32_t sample)
|
|
{
|
|
auto lastSent = lastSentDescriptor();
|
|
if (isSLCRunning() && (lastSent->next_link_ptr == head)) return false;
|
|
head->buf_ptr[headPos++] = sample;
|
|
advanceHead(lastSent);
|
|
return true;
|
|
};
|
|
|
|
bool SinglePinI2SDriver::writeInterleaved(uint32_t samples[4])
|
|
{
|
|
auto lastSent = lastSentDescriptor();
|
|
if (isSLCRunning() && (lastSent->next_link_ptr == head)) return false;
|
|
uint32_t* buf = head->buf_ptr;
|
|
buf[headPos++] = samples[1];
|
|
buf[headPos++] = samples[0];
|
|
buf[headPos++] = samples[3];
|
|
buf[headPos++] = samples[2];
|
|
advanceHead(lastSent);
|
|
return true;
|
|
};
|
|
|
|
void SinglePinI2SDriver::configureSLC()
|
|
{
|
|
ETS_SLC_INTR_DISABLE();
|
|
// Reset SLC, clear interrupts
|
|
SLCC0 |= SLCRXLR | SLCTXLR;
|
|
SLCC0 &= ~(SLCRXLR | SLCTXLR);
|
|
SLCIC = 0xFFFFFFFF;
|
|
// Configure SLC DMA in mode 1
|
|
SLCC0 &= ~(SLCMM << SLCM);
|
|
SLCC0 |= (1 << SLCM);
|
|
SLCRXDC |= SLCBINR | SLCBTNR;
|
|
SLCRXDC &= ~(SLCBRXFE | SLCBRXEM | SLCBRXFM);
|
|
// RXLINK is output DMA, but TXLINK must be configured with valid descriptr
|
|
SLCTXL &= ~(SLCTXLAM << SLCTXLA);
|
|
SLCRXL &= ~(SLCRXLAM << SLCRXLA);
|
|
SLCTXL |= (uint32)&descriptors[0] << SLCTXLA;
|
|
SLCRXL |= (uint32)&descriptors[0] << SLCRXLA;
|
|
// Not setting up interrupts. Not starting DMA here
|
|
}
|
|
|
|
void SinglePinI2SDriver::stopSLC()
|
|
{
|
|
// Stop SLC, reset descriptors and clear interrupts
|
|
ETS_SLC_INTR_DISABLE();
|
|
SLCTXL |= SLCTXLE;
|
|
SLCRXL |= SLCRXLE;
|
|
SLCTXL &= ~(SLCTXLAM << SLCTXLA);
|
|
SLCRXL &= ~(SLCRXLAM << SLCRXLA);
|
|
SLCIC = 0xFFFFFFFF;
|
|
}
|
|
|
|
void SinglePinI2SDriver::setDividers(uint8_t div1, uint8_t div2)
|
|
{
|
|
// Ensure dividers fit in bit fields
|
|
div1 &= I2SBDM;
|
|
div2 &= I2SCDM;
|
|
// trans master(active low), recv master(active_low), !bits mod(==16 bits/channel), clear clock dividers
|
|
I2SC &= ~(I2STSM | I2SRSM | (I2SBMM << I2SBM) | (I2SBDM << I2SBD) | (I2SCDM << I2SCD));
|
|
// I2SRF = Send/recv right channel first
|
|
// I2SMR = MSB recv/xmit first
|
|
// I2SRMS, I2STMS = We don't really care about WS delay here
|
|
// div1, div2 = Set I2S WS clock frequency. BCLK seems to be generated from 32x this
|
|
I2SC |= I2SRF | I2SMR | I2SRMS | I2STMS | (div1 << I2SBD) | (div2 << I2SCD);
|
|
}
|
|
|
|
void SinglePinI2SDriver::setRate(const uint32_t rateHz)
|
|
{
|
|
if (rateHz == currentRate) return;
|
|
currentRate = rateHz;
|
|
|
|
uint32_t scaledBaseFreq = I2SBASEFREQ / 32;
|
|
float bestDelta = scaledBaseFreq;
|
|
uint8_t sbdDivBest=1;
|
|
uint8_t scdDivBest=1;
|
|
for (uint8_t i = 1; i < 64; i++) {
|
|
for (uint8_t j = i; j < 64; j++) {
|
|
float delta = fabs(((float)scaledBaseFreq/i/j) - rateHz);
|
|
if (delta < bestDelta){
|
|
bestDelta = delta;
|
|
sbdDivBest = i;
|
|
scdDivBest = j;
|
|
}
|
|
}
|
|
}
|
|
|
|
setDividers(sbdDivBest, scdDivBest);
|
|
}
|
|
|
|
float SinglePinI2SDriver::getActualRate()
|
|
{
|
|
return (float)I2SBASEFREQ/32/((I2SC>>I2SBD) & I2SBDM)/((I2SC >> I2SCD) & I2SCDM);
|
|
}
|
|
|
|
void SinglePinI2SDriver::startI2S()
|
|
{
|
|
// Setup only I2S SD pin for output
|
|
pinMode(I2SO_DATA, FUNCTION_1);
|
|
I2S_CLK_ENABLE();
|
|
// Reset I2S
|
|
I2SIC = 0x3F;
|
|
I2SIE = 0;
|
|
I2SC &= ~(I2SRST);
|
|
I2SC |= I2SRST;
|
|
I2SC &= ~(I2SRST);
|
|
// I2STXFMM, I2SRXFMM=0 => 16-bit, dual channel data
|
|
I2SFC &= ~(I2SDE | (I2STXFMM << I2STXFM) | (I2SRXFMM << I2SRXFM));
|
|
I2SFC |= I2SDE; // Enable DMA
|
|
// I2STXCMM, I2SRXCMM=0 => Dual channel mode, RX/TX CHAN_MOD=0
|
|
I2SCC &= ~((I2STXCMM << I2STXCM) | (I2SRXCMM << I2SRXCM));
|
|
// Set dividers to something reasonable
|
|
currentRate = 0;
|
|
setRate(44100);
|
|
// Start I2S peripheral
|
|
I2SC |= I2STXS;
|
|
}
|
|
|
|
void SinglePinI2SDriver::stopI2S()
|
|
{
|
|
// Disable any I2S send or receive
|
|
I2SC &= ~(I2STXS | I2SRXS);
|
|
// Reset I2S
|
|
I2SC &= ~(I2SRST);
|
|
I2SC |= I2SRST;
|
|
I2SC &= ~(I2SRST);
|
|
|
|
stopSLC();
|
|
// Reconnect RX0 ?
|
|
//pinMode(I2SO_DATA, SPECIAL);
|
|
}
|
|
|
|
int SinglePinI2SDriver::getUnderflowCount()
|
|
{
|
|
int count = underflowCount;
|
|
underflowCount = 0;
|
|
return count;
|
|
}
|
|
|
|
// Global instance
|
|
SinglePinI2SDriver I2SDriver;
|
|
|
|
#endif // defined(ESP8266)
|