Skip to content

Commit

Permalink
Replace external SPIRAM lib w/internal optimized (#250)
Browse files Browse the repository at this point in the history
* Replace external SPIRAM lib w/internal optimized

Instead of requiring everyone to download and install another Arduino
library to use this lib, include an optimized, self-contained one from
the 8266 VM PR: esp8266/Arduino#6994

Does change the API as it now requires a HW chip select, so that
parameter is removed from the SpiRAM buffer object.

* Redo the FIFO to try and be faster

* Fix the SPI interface, buffering.

SPI FIFO needs special care so do all work in a RAM copy and the copy it
over manually w/32b writes.

Ensure the SPI command is done before touching the FIFO.

Fix buffering organization.

Make the buffer report status as a #### bar.

* Fix infinite wait on EOF

* Add SW chip select support

* Update URL to one that's live today

* Clean up metadata printout

* Clean up readme refs to obsolete lib

* Slow down the DIO->SIO reset sequence

* Update readme to remove old SPIRam library ref

Also add the resistor to the 1T circuit to avoid transistor overheating.
  • Loading branch information
earlephilhower committed Apr 14, 2020
1 parent be0ee82 commit 0fd2d04
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 136 deletions.
32 changes: 10 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,16 @@ A neat MQTT-driven ESP8266 light-and-sound device (alarm? toy? who can say!) was
A very interesting "linear clock" with a stepper motor, NTP time keeping, and configurable recorded chimes with schematics, 3D printer plans, and source code, is now available http://home.kpn.nl/bderogee1980/projects/linear_clock/linear_clock.html

## Prerequisites
First, make sure you are running the 2.4 or GIT head version of the Arduino libraries for ESP8266, or the latest ESP32 SDK from Espressif.
First, make sure you are running the 2.6.3/later or GIT head version of the Arduino libraries for ESP8266, or the latest ESP32 SDK from Espressif.

You can use GIT to pull right from GitHub: see [this README](https://github.com/esp8266/Arduino/blob/master/README.md#using-git-version) for detailed instructions.

## ESP-32 SPIFFS Errors
The latest official release of the ESP32-Arduino seems to have broken SPIFFS, but a patch has just been committed to git head. If you want to run SPIFFS, please follow the directions below, courtesy of @rfestag:
```sh
cd ~/Arduino/hardware/espressif/esp32 # Or wherever you have it installed
git pull # Update to the latest
cd tools
python get.py # On my system, I have python3 installed by default, so I had to run python2.7 get.py
# Re-upload files using the new mkspiffs that is installed
# Then reload your sketch
```
Be sure to use the [ESP32 SPIFFS](https://github.com/me-no-dev/arduino-esp32fs-plugin) upload plugin before running your sketch to upload the data files once the fixed IDE is set up.

## Installation
Install the library and the SPI driver library in your ~/Arduino/libraries
Install the library in your ~/Arduino/libraries
```sh
mkdir -p ~/Arduino/libraries
cd ~/Arduino/libraries
git clone https://github.com/earlephilhower/ESP8266Audio
git clone https://github.com/Gianbacchio/ESP8266_Spiram
```

When in the IDE please select the following options on the ESP8266:
Expand Down Expand Up @@ -185,7 +172,7 @@ There are many other variants out there, and they should all work reasonably wel
## Software I2S Delta-Sigma DAC (i.e. playing music with a single transistor and speaker)
For the best fidelity, and stereo to boot, spend the money on a real I2S DAC. Adafruit makes a great mono one with amplifier, and you can find stereo unamplified ones on eBay or elsewhere quite cheaply. However, thanks to the software delta-sigma DAC with 32x oversampling (up to 128x if the audio rate is low enough) you can still have pretty good sound!
Use the AudioOutputI2S*No*DAC object instead of the AudioOutputI2S in your code, and the following schematic to drive a 2-3W speaker using a single $0.05 NPN 2N3904 transistor:
Use the `AudioOutputI2S*No*DAC` object instead of the `AudioOutputI2S` in your code, and the following schematic to drive a 2-3W speaker using a single $0.05 NPN 2N3904 transistor and ~1K resistor:
```
2N3904 (NPN)
Expand All @@ -197,7 +184,7 @@ Use the AudioOutputI2S*No*DAC object instead of the AudioOutputI2S in your code,
| | | A|
ESP8266-GND ------------------+ | +------+ K|
| | | E|
ESP8266-I2SOUT (Rx) -------------+ | \ R|
ESP8266-I2SOUT (Rx) -----/\/\/\--+ | \ R|
| +-|
USB 5V -----------------------------+

Expand All @@ -207,15 +194,15 @@ If you don't have a 5V source available on your ESP model, you can use the 5V fr
Connections are as a follows:
```
ESP8266-RX(I2S tx) -- 2N3904 Base
ESP8266-RX(I2S tx) -- Resistor (~1K ohm, not critical) -- 2N3904 Base
ESP8266-GND -- 2N3904 Emitter
USB-5V -- Speaker + Terminal
2N3904-Collector -- Speaker - Terminal
```
Basically the transistor acts as a switch and requires only a drive of 1/beta (~1/1000 for the transistor specified) times the speaker current. As shown you've got a max current of (5-0.7)/8=540mA and a power of 0.54^2 * 8 = ~2.3W into the speaker.
*NOTE*: A prior version of this schematic had a direct connection from the ESP8266 to the base of the transistor. While this does provide the maximum amplitude, it also can draw more current from the 8266 than is safe, and can also cause the transistor to overheat.
When using the software delta-sigma DAC, even though our playback circuit is not using the LRCLK or BCLK pins, the ESP8266 internal hardware *will* be driving them. So these pins cannot be used as outputs in your application. However, you can use the LRCLK and BCLK pins as *inputs*. Simply start playback, then use the standard pinMode(xxx, INPUT/INPUT_PULLUP) Arduino commands and you can, for example, use those two pins to read a button or sensor.
As of the latest ESP8266Audio release, with the software delta-sigma DAC the LRCLK and BCLK pins *can* be used by an application. Simply use normal `pinMode` and `dicitalWrite` or `digitalRead` as desired.
### High pitched buzzing with the 1-T circuit
The 1-T amp can _NOT_ drive any sort of amplified speaker. If there is a power or USB input to the speaker, or it has lights or Bluetooth or a battery, it can _NOT_ be used with this circuit.
Expand Down Expand Up @@ -251,8 +238,9 @@ Ground ---------------------+
For ESP8266 with red LED (~1.9Vf drop) you need minimum 150Ohm resistor (12mA max per pin), and output pin is fixed (GPIO3/RX0).On ESP32 it is confgurable with `AudioOutputSPDIF(gpio_num)`.
## Using external SPI RAM to increase buffer
A class allows you to use a 23lc1024 SPI RAM from Microchip as input buffer. This chip connects to ESP8266 HSPI port and uses an [external SPI RAM library](https://github.com/Gianbacchio/ESP8266_Spiram).
You need to choose another pin than GPIO15 for Cs as this pin is already used by the I2S port. Here is an example with the Cs pin plugged to GPIO00 on NodeMCU board.
A class allows you to use a 23lc1024 SPI RAM from Microchip as input buffer. This chip connects to ESP8266 HSPI port and provides a large buffer to help avoid hiccus in playback of web streams.
The current version allows for using the standard hardware CS (GPIO15) or any other pin via software at slightly less performance. The following schematic shows one example:
![Example of SPIRAM Schematic](examples/StreamMP3FromHTTP_SPIRAM/Schema_Spiram.png)
Expand Down
27 changes: 13 additions & 14 deletions examples/StreamMP3FromHTTP_SPIRAM/StreamMP3FromHTTP_SPIRAM.ino
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const char *SSID = ".....";
const char *PASSWORD = ".....";

// Randomly picked URL
const char *URL="http://streaming.shoutcast.com/80sPlanet?lang=en-US";
const char *URL="http://kvbstreams.dyndns.org:8000/wkvi-am";

AudioGeneratorMP3 *mp3;
AudioFileSourceICYStream *file;
Expand All @@ -29,25 +29,24 @@ void MDCallback(void *cbData, const char *type, bool isUnicode, const char *stri
const char *ptr = reinterpret_cast<const char *>(cbData);
(void) isUnicode; // Punt this ball for now
// Note that the type and string may be in PROGMEM, so copy them to RAM for printf
char s1[32], s2[64];
strncpy_P(s1, type, sizeof(s1));
s1[sizeof(s1)-1]=0;
strncpy_P(s2, string, sizeof(s2));
s2[sizeof(s2)-1]=0;
Serial.printf("METADATA(%s) '%s' = '%s'\n", ptr, s1, s2);
Serial.printf_P(PSTR("METADATA(%s) '%s' = '%s'\n"), ptr, type, string);
Serial.flush();
}


// Called when there's a warning or error (like a buffer underflow or decode hiccup)
void StatusCallback(void *cbData, int code, const char *string)
{
const char *ptr = reinterpret_cast<const char *>(cbData);
// Note that the string may be in PROGMEM, so copy it to RAM for printf
char s1[64];
strncpy_P(s1, string, sizeof(s1));
s1[sizeof(s1)-1]=0;
Serial.printf("STATUS(%s) '%d' = '%s'\n", ptr, code, s1);
Serial.flush();
static uint32_t lastTime = 0;
static int lastCode = -99999;
uint32_t now = millis();
if ((lastCode != code) || (now - lastTime > 1000)) {
Serial.printf_P(PSTR("STATUS(%s) '%d' = '%s'\n"), ptr, code, string);
Serial.flush();
lastTime = now;
lastCode = code;
}
}

void setup()
Expand All @@ -73,7 +72,7 @@ void setup()
file = new AudioFileSourceICYStream(URL);
file->RegisterMetadataCB(MDCallback, (void*)"ICY");
// Initialize 23LC1024 SPI RAM buffer with chip select ion GPIO4 and ram size of 128KByte
buff = new AudioFileSourceSPIRAMBuffer(file, 4, 131072);
buff = new AudioFileSourceSPIRAMBuffer(file, 4, 128*1024);
buff->RegisterStatusCB(StatusCallback, (void*)"buffer");
out = new AudioOutputI2SNoDAC();
mp3 = new AudioGeneratorMP3();
Expand Down
191 changes: 101 additions & 90 deletions src/AudioFileSourceSPIRAMBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
Copyright (C) 2017 Sebastien Decourriere
Based on AudioFileSourceBuffer class from Earle F. Philhower, III
Copyright (C) 2020 Earle F. Philhower, III
Rewritten for speed and functionality
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
Expand All @@ -26,131 +29,139 @@

AudioFileSourceSPIRAMBuffer::AudioFileSourceSPIRAMBuffer(AudioFileSource *source, uint8_t csPin, uint32_t buffSizeBytes)
{
Spiram = new ESP8266Spiram(csPin, 40e6);
Spiram->begin();
Spiram->setSeqMode();
ramSize = buffSizeBytes;
writePtr = 0;
readPtr = 0;
src = source;
length = 0;
filled = false;
bytesAvailable = 0;
audioLogger->printf_P(PSTR("SPI RAM buffer size: %u Bytes\n"), ramSize);
ram.begin(40, csPin);
ramSize = buffSizeBytes;
writePtr = 0;
readPtr = 0;
filled = false;
src = source;
audioLogger->printf_P(PSTR("SPI RAM buffer size: %u Bytes\n"), ramSize);
}

AudioFileSourceSPIRAMBuffer::~AudioFileSourceSPIRAMBuffer()
{
ram.end();
}

bool AudioFileSourceSPIRAMBuffer::seek(int32_t pos, int dir)
{
// Invalidate
readPtr = 0;
writePtr = 0;
length = 0;
return src->seek(pos, dir);
// Invalidate
readPtr = 0;
writePtr = 0;
filled = false;
return src->seek(pos, dir);
}

bool AudioFileSourceSPIRAMBuffer::close()
{
return src->close();
return src->close();
}

bool AudioFileSourceSPIRAMBuffer::isOpen()
{
return src->isOpen();
return src->isOpen();
}

uint32_t AudioFileSourceSPIRAMBuffer::getSize()
{
return src->getSize();
return src->getSize();
}

uint32_t AudioFileSourceSPIRAMBuffer::getPos()
{
return src->getPos();
return src->getPos() - (writePtr - readPtr);
}

uint32_t AudioFileSourceSPIRAMBuffer::read(void *data, uint32_t len)
{
uint32_t bytes = 0;
// Check if the buffer isn't empty, otherwise we try to fill completely
if (!filled) {
uint8_t buffer[256];
writePtr = readPtr = 0;
uint16_t toRead = sizeof(buffer);
// Fill up completely before returning any data at all
audioLogger->printf_P(PSTR("Buffering...\n"));
while (bytesAvailable!=ramSize) {
length = src->read(buffer, toRead);
if(length>0) {
Spiram->write(writePtr, buffer, length);
bytesAvailable+=length;
writePtr = bytesAvailable % ramSize;
if ((ramSize-bytesAvailable)<toRead) {
toRead=ramSize-bytesAvailable;
}
} else {
// EOF, break out of read loop
break;
}
}
writePtr = bytesAvailable % ramSize;
filled = true;
audioLogger->printf_P(PSTR("Filling Done !\n"));
}

// audioLogger->printf("Buffer: %u%\n", bytesAvailable*100/ramSize);

uint8_t *ptr = reinterpret_cast<uint8_t*>(data);
uint32_t toReadFromBuffer = (len < bytesAvailable) ? len : bytesAvailable;
if (toReadFromBuffer>0) {
// Pull from buffer until we've got none left or we've satisfied the request
Spiram->read(readPtr, ptr, toReadFromBuffer);
readPtr = (readPtr+toReadFromBuffer) % ramSize;
bytes = toReadFromBuffer;
bytesAvailable-=toReadFromBuffer;
len-=toReadFromBuffer;
ptr += toReadFromBuffer;
}

// If len>O there is no data left in buffer and we try to read more directly from source.
// Then, we trigger a complete buffer refill
if (len) {
bytes += src->read(ptr, len);
bytesAvailable = 0;
filled = false;
}
return bytes;
uint32_t bytes = 0;

// Check if the buffer isn't empty, otherwise we try to fill completely
if (!filled) {
cb.st(999, PSTR("Filling buffer..."));
uint8_t buffer[256];
writePtr = 0;
readPtr = 0;
// Fill up completely before returning any data at all
do {
int toRead = std::min(ramSize - (writePtr - readPtr), sizeof(buffer));
int length = src->read(buffer, toRead);
if (length > 0) {
#ifdef FAKERAM
for (size_t i=0; i<length; i++) fakeRAM[(i+writePtr)%ramSize] = buffer[i];
#else
ram.writeBytes(writePtr % ramSize, buffer, length);
#endif
writePtr += length;
} else {
// EOF, break out of read loop
break;
}
} while ((writePtr - readPtr) < ramSize);
filled = true;
cb.st(999, PSTR("Buffer filled..."));
}

// Read up to the entire buffer from RAM
uint32_t toReadFromBuffer = std::min(len, writePtr - readPtr);
uint8_t *ptr = reinterpret_cast<uint8_t*>(data);
if (toReadFromBuffer > 0) {
#ifdef FAKERAM
for (size_t i=0; i<toReadFromBuffer; i++) ptr[i] = fakeRAM[(i+readPtr)%ramSize];
#else
ram.readBytes(readPtr % ramSize, ptr, toReadFromBuffer);
#endif
readPtr += toReadFromBuffer;
ptr += toReadFromBuffer;
bytes += toReadFromBuffer;
len -= toReadFromBuffer;
}

// If len>0 there is no data left in buffer and we try to read more directly from source.
// Then, we trigger a complete buffer refill
if (len) {
bytes += src->read(data, len);
filled = false;
}
return bytes;
}

void AudioFileSourceSPIRAMBuffer::fill()
{
// Make sure the buffer is pre-filled before make partial fill.
if (!filled) return;

// Now trying to refill SPI RAM Buffer
uint8_t buffer[128];
// Make sure there is at least buffer size free in RAM
if ((ramSize - bytesAvailable)<sizeof(buffer)) {
return;
}
uint16_t cnt = src->readNonBlock(buffer, sizeof(buffer));
if (cnt) {
Spiram->write(writePtr, buffer, cnt);
bytesAvailable+=cnt;
writePtr = (writePtr + cnt) % ramSize;
#ifdef SPIBUF_DEBUG
audioLogger->printf_P(PSTR("SockRead: %u | RamAvail: %u\n"), cnt, bytesAvailable);
// Make sure the buffer is pre-filled before make partial fill.
if (!filled) return;

for (auto i=0; i<5; i++) {
// Make sure there is at least buffer size free in RAM
uint8_t buffer[128];
if ((ramSize - (writePtr - readPtr)) < sizeof(buffer)) {
return;
}

int cnt = src->readNonBlock(buffer, sizeof(buffer));
if (cnt) {
#ifdef FAKERAM
for (size_t i=0; i<cnt; i++) fakeRAM[(i+writePtr)%ramSize] = buffer[i];
#else
ram.writeBytes(writePtr % ramSize, buffer, cnt);
#endif
}
return;
writePtr += cnt;
}
}
}

bool AudioFileSourceSPIRAMBuffer::loop()
{
if (!src->loop()) return false;
fill();
return true;
static uint32_t last = 0;
if (!src->loop()) return false;
fill();
if ((ESP.getCycleCount() - last) > microsecondsToClockCycles(1000000)) {
last = ESP.getCycleCount();
char str[65];
memset(str, '#', 64);
str[64] = 0;
str[((writePtr - readPtr) * 64)/ramSize] = 0;
cb.st(((writePtr - readPtr) * 100)/ramSize, str);
}
return true;
}
Loading

0 comments on commit 0fd2d04

Please sign in to comment.