Change default to I2S driver, explain how to use RMT if you really want to

This commit is contained in:
Brian Bulkowski
2020-09-15 18:22:39 -07:00
parent 8f0da6ff4a
commit 89a419b1c9
5 changed files with 261 additions and 55 deletions

140
README.md
View File

@@ -5,9 +5,9 @@
This port of FastLED 3.3 runs under the 4.x ESP-IDF development environment. Enjoy.
Updates: Aug, Sept 2020:
- I2S hardware working and now default.
- RMT interface well tested.
- WS2812FX library ported and working.
- I2S hardware working ( see below )
There are some new tunables, and if you're also fighting glitches, you need to read `components/FastLED-idf/ESP-IDF.md`.
@@ -58,10 +58,8 @@ At first, I focused the port on RMT, as it seemed the hardware everyone talked a
below, the RMT interface even has a Espressif provided example! Thus the journey of getting the MEM_BUFS
code working and soak up the interrupt latency.
But, the I2S hardware is almost certainly cooler than RMT. It has more parallelism, and less code.
Right now, the code is still checked in with default RMT simply because I haven't tested I2S as much,
but please see the I2S section below on enabling it.
But, the I2S hardware is almost certainly better than RMT. It has more parallelism, and less code,
and seems enormously resistant to glitches. The default is I2S, see below.
# TL;DR about this repo
@@ -79,6 +77,8 @@ For master, either use the standard `sdkconfig` or build your own. Remove this o
and `idf.py menuconfig`, and set whatever paramenters you need. There is
nothing in this library that's specific to any particular version.
I tend to set the compiler into -O2 mode.
# a note about level shifting
I've now read a lot of reddit posts about people saying "but I hook LEDs up to an Arudino and it works,
@@ -90,8 +90,66 @@ and using a 4-pin package isn't so cool as an 8 pin package, since an ESP32 can
8 channels. That's SN74HCT245N , and they're to be had from Digikey at $0.60. Read the datasheet
carefully and get the direction and the enable pins right - they can't float.
# Use of ESP32 I2S hardware for 3 wire LEDs
## this is really good
Mid 2019, The following post showed up on reddit:
https://www.reddit.com/r/FastLED/comments/bjq0sm/new_24way_parallel_driver_for_esp32/
which announced using I2S hardware instead of RMT hardware that had been used in the past.
I2S hardware is aimed to generate sound, but it can also be configured (as it is in this case)
to provide a parallel bus interface. It is best understood by reading the Espressif documentation.
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/i2s.html
What you'll see is there's 12 parallel pins that can be supported with each hardware module,
and frankly if you need more than 12 way parallelism I worry about you. That should be enough
for a module of this size.
## Which to use?
From Sam's post: WARNINGS and LIMITATIONS
-- **All strips must use the same clockless chip (e.g., WS2812)**. Due to the way the I2S peripheral works, it would be much more complicated to drive strips that have different timing parameters, so we punted on it. If you need to use multiple strips with different chips, use the default RMT-base driver.
-- Yves has written some mad code to compute the various clock dividers, so that the output is timing-accurate. If you see timing problems, however, let us know.
-- This is new software. We tested it on our machines. If you find bugs or other issues, please let us know!
Of course, new in 2019 is not so new now :-).
I have set the default to I2S, because it ran all of my torture tests immediately and flawlessly.
I am somewhat unclear why it's so good. It's hard to suss out how deep the DMA queue is at a glance in time,
and it seems that the interupt handler might be running at a different priority. I don't know
if I can argue that it's doing less work. Since it's written for audio, there might be more
attention paid to the driver.
Obviously, if you need the I2S hardware for something else, you'll want RMT, but there are two I2S components
on an ESP32 and one on a ESP32-S2. The other uses of I2S include ( according to the documentation ) LCD
master transmitting mode, camera slave receiving mode, and ADC / DAC mode ( feed directly to the DAC for output ).
You can still fall back to RMT, see below.
## the code is a little inscrutable
The I2S code uses the underlying hardware interface ( `ll` ) fast and furious. For this reason, nothing
you see in the official documentation about I2S matches what the code is written to. Just to beware.
# Use of ESP32 RMT hardware for 3 wire LEDs
## Cookbook - enable it
There's a `#define` at the top of fastled.h which chooses I2S or RMT. Switch to RMT by commenting out.
Comment back in `"platforms/esp/32/clockless_rmt_esp32.cpp"` from `components\FastLED-idf\CMakeLists.txt`
To compile the default I2S, you need to remove the RMT file from the compile because you don't want to waste RAM,
and there are colliding symbols. You can't use both because there's no current way to define which strings
use which hardware. If there was a new interface, you'd fix the colliding symbols and enable both.
## background
The ESP32 has an interesting module, called RMT. It's a module that's
meant to make arbitrary waveforms on pins, without having to bang each pin at
the right time. While it was made for IR Remote Control devices,
@@ -117,29 +175,9 @@ The FastLED ESP32 RMT use has two modes: one which uses the "driver", and
one which doesn't, and claims to be more efficient due to when it's converting
between LED RGB and not.
Whether you can use the "direct" mode or not depends on whether you have other
users of the RMT driver within ESP-IDF.
The ESP-IDF driver does support a `translate` mode which would be the same as what
the internal mode does.
Essentially, if you have the Driver turned on, you shouldn't use the direct mode,
and if you want to use the direct mode, you should turn off the driver.
No extra commands in `menuconfig` seem necessary.
# Use of ESP32 I2S hardware for 3 wire LEDs
## TL;DR enable it
There's a `#define` at the top of fastled.h which chooses I2S or RMT. Switch to I2S.
Comment out `"platforms/esp/32/clockless_rmt_esp32.cpp"` from `components\FastLED-idf\CMakeLists.txt`
You need to remove the RMT file from the compile because you don't want to waste RAM, and there
are colliding symbols. You can't use both because there's no current way to define which strings
use which hardware.
## Which to use?
Not sure yet. Going to play with it on different builds and see what works.
# Four wire LEDs ( APA102 and similar )
@@ -148,8 +186,11 @@ the clock and data lines have to be controled together ( duh ),
and the RMT interface doesn't do that. What does do that is the SPI
interface, and I don't think I've wired that up.
There are two hardware SPI ports in the system, so that should be
able to be enabled. Work to do.
The I2S interface, on the other hand, appears synchronized. I think you could
write an APA102 driver using I2S and it would be fast and happy.
However, simply doing hardware SPI using the single SPI driver should be good enough.
Work to do.
Since hardware banging is used ( that's the big ugly warning ),
these LEDs are very likely far slower, and probably do not respond to parallelism
@@ -210,15 +251,6 @@ Within menuconfig, I tend to use the following settings. Minimum required silico
are speed workarounds at Rev0 that get compiled in. I change the compiler options to -O2 instead of -Os or -Og.
Default flash size to 4M, because all the devices I have are 4G.
Second of all, there be some dragons in the different versions.
The FastLed RMT code runs in two different modes - it either uses an onboard driver, or it uses the ESP-IDF RMT driver.
I've found that if you use the driver that was originally written for FastLED, it works great with ESP-IDF 4.0,
but crashes horribly in 4.2 and master. These are interesting branches because the new S2 version of the ESP32
chip requires those branches.
It's quite possible that more and more changes will creep in, or that someone will find the flaw that's causing 4.2
to crash.
# A short plug for Microsoft's WSL
@@ -251,17 +283,21 @@ There is an example of LED control, using the RMT interface directly.
If you just want to it like that, without all the cool stuff in FastLED,
be everyone's guest!
Interestingly, the LED library uses RMT, has glitches just like the older versions of RMT
that FastLED had, and don't support I2S which is far more stable.
I did reach out to Espressif. I think they should include or have a FastLED port.
In their forums, they said they don't intend to do anything like that.
## SamGuyer's fork
According to a reddit post, someone named Sam Guyer fixed the issues with FastLED. There are
a number of fundamental problems that Sam has been gnawing on, notably the access to flash
that happens when you 'read files'. His fork was my starting point.
When I started on this journey, I read a series of Reddit posts saying that Sam's
FastLED port was the best for ESP32. It used RMT by default, and it still glitched
a lot, which put me down the path of doing major work on the RMT code
and finding the fundamental issue which he backported.
However, I found that the primarily issue with visual artifacts in RMT had to do with
the amount of ESP-IDF jitter.
The code for the I2S driver, however, just came over peachy, and once I enabled it,
the entire system was far more stable.
Kudos to Sam for getting the code to a better point, where it was amenable to the optimizations
I did.
@@ -275,6 +311,9 @@ is a function which will take pixel bytes to RMT buffers. This is the structure
code already, so the entire ISR can be removed. This functionality is not in the Arduino
implementation of RMT, which is why its not used here yet.
However, now that one has the I2S code, I would think improving the RMT code is
a low priority.
# Updating
This package basically depends on three packages: two that you pull in,
@@ -314,7 +353,7 @@ and still, potentially, have the ability to do some extra work, because you've g
I have not determined if this is using the RMT interface, which would mean we could
use an async internal interface. The timing, however, is very solid.
# Use notes
# Use(age) notes
What I like about using FreeRTOS and a more interesting development environment is
you should be able to use more of the CPU for other things. Essentially, you should
@@ -339,7 +378,9 @@ that would be under MIT license.
I am honestly not sure what happens to LGPL in this case. It's a component in an
embedded system, which is morally a library, but it is clearly very statically
linked.
linked and in the "application", which I think would generate a copy-left. OTOH,
that would also mean that ESP-IDF is all GPL and copy-left, and Espressif
doesn't seem to think so ... whatever.
I don't intend to make any money off this, don't charge people, and do not intend
the use for commercial art projects, so the use is safe for me. But don't say
@@ -482,10 +523,6 @@ Note: what's up with registering the driver? I should, right? Make sure that's d
menuconfig? Doen't seem to be. There are no settings anywhere in the menuconfig regarding
gpio. Should probably look at the examples to see if I have to enable the ISR or something.
## using RMT vs I2S
Note to self: There is a define in the platforms area that lets a person choose.
## GCC 8 memcpy and memmove into complex structures
```
@@ -559,9 +596,8 @@ gpio.c out of the hal compile.
The code in ESP-IDF seems to really like using the pattern of having a macro for a
default constructor. In the case of initializing the RMT structure, if I did it "by hand"
and used the -O2 compile option, I got instability. When I switched to using their
constructure, which seems the same to me generally, I get a stable build. I
now wonder if this was some of the issue around not using the IRQ....
and used the -O2 compile option, I got instability. The code is now written to
do different things in different versions.
## issues regarding ESP-IDF 4.1 and private interrupt handler

View File

@@ -14,7 +14,7 @@ set(srcs
"hal/esp32-hal-misc.c"
"hal/esp32-hal-gpio.c"
# remove the following if you want I2S instead of RMT hardware, just put a pound in front
"platforms/esp/32/clockless_rmt_esp32.cpp"
# "platforms/esp/32/clockless_rmt_esp32.cpp"
)
# everything needs the ESP32 flag, not sure why this won't work

View File

@@ -10,7 +10,7 @@
// prefer I2S? Comment this in.
// Not the default because haven't tried it as much, does work
// #define FASTLED_ESP32_I2S
#define FASTLED_ESP32_I2S
#include "esp32-hal.h"

View File

@@ -519,7 +519,9 @@ protected:
// -- Allocate i2s interrupt
SET_PERI_REG_BITS(I2S_INT_ENA_REG(I2S_DEVICE), I2S_OUT_EOF_INT_ENA_V, 1, I2S_OUT_EOF_INT_ENA_S);
ESP_ERROR_CHECK(
esp_intr_alloc(interruptSource, 0 /* ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_LEVEL3*/,
// this seems to work great with the default 0 flag, but everything is in IRAM
// so why not raise it a little? Because you'll get a panic, and I'm not sure why.
esp_intr_alloc(interruptSource, 0 /* ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL2 */,
&interruptHandler, 0, &gI2S_intr_handle)
);

View File

@@ -0,0 +1,168 @@
#pragma once
#pragma message "ESP32 Hardware SPI support added"
FASTLED_NAMESPACE_BEGIN
/*
* ESP32 Hardware SPI Driver
*
* Copyright (c) 2020 Nick Wallace
* Derived from code for ESP8266 hardware SPI by Benoit Anastay.
*
* This hardware SPI implementation can drive clocked LEDs from either the
* VSPI or HSPI bus (aka SPI2 & SPI3). No support is provided for SPI1, because it is
* shared among devices and the cache for data (code) in the Flash as well as the PSRAM.
*
* To enable the hardware SPI driver, add the following line *before* including
* FastLED.h:
*
* #define FASTLED_ALL_PINS_HARDWARE_SPI
*
* This driver uses the VSPI bus by default (GPIO 18, 19, 23, & 5). To use the
* HSPI bus (GPIO 14, 12, 13, & 15) add the following line *before* including
* FastLED.h:
*
* #define FASTLED_ESP32_SPI_BUS HSPI
*
*/
/*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef FASTLED_ESP32_SPI_BUS
#define FASTLED_ESP32_SPI_BUS VSPI
#endif
SPIClass ledSPI(FASTLED_ESP32_SPI_BUS);
#if FASTLED_ESP32_SPI_BUS == VSPI
static uint8_t spiClk = 18;
static uint8_t spiMiso = 19;
static uint8_t spiMosi = 23;
static uint8_t spiCs = 5;
#elif FASTLED_ESP32_SPI_BUS == HSPI
static uint8_t spiClk = 14;
static uint8_t spiMiso = 12;
static uint8_t spiMosi = 13;
static uint8_t spiCs = 15;
#endif
template <uint8_t DATA_PIN, uint8_t CLOCK_PIN, uint32_t SPI_SPEED>
class ESP32SPIOutput {
Selectable *m_pSelect;
public:
ESP32SPIOutput() { m_pSelect = NULL; }
ESP32SPIOutput(Selectable *pSelect) { m_pSelect = pSelect; }
void setSelect(Selectable *pSelect) { m_pSelect = pSelect; }
void init() {
// set the pins to output and make sure the select is released (which apparently means hi? This is a bit
// confusing to me)
ledSPI.begin(spiClk, spiMiso, spiMosi, spiCs);
release();
}
// stop the SPI output. Pretty much a NOP with software, as there's no registers to kick
static void stop() { }
// wait until the SPI subsystem is ready for more data to write. A NOP when bitbanging
static void wait() __attribute__((always_inline)) { }
static void waitFully() __attribute__((always_inline)) { wait(); }
static void writeByteNoWait(uint8_t b) __attribute__((always_inline)) { writeByte(b); }
static void writeBytePostWait(uint8_t b) __attribute__((always_inline)) { writeByte(b); wait(); }
static void writeWord(uint16_t w) __attribute__((always_inline)) { writeByte(w>>8); writeByte(w&0xFF); }
// naive writeByte implelentation, simply calls writeBit on the 8 bits in the byte.
static void writeByte(uint8_t b) {
ledSPI.transfer(b);
}
public:
// select the SPI output (TODO: research whether this really means hi or lo. Alt TODO: move select responsibility out of the SPI classes
// entirely, make it up to the caller to remember to lock/select the line?)
void select() {
ledSPI.beginTransaction(SPISettings(SPI_SPEED, MSBFIRST, SPI_MODE0));
if(m_pSelect != NULL) { m_pSelect->select(); }
}
// release the SPI line
void release() {
if(m_pSelect != NULL) { m_pSelect->release(); }
ledSPI.endTransaction();
}
// Write out len bytes of the given value out over ledSPI. Useful for quickly flushing, say, a line of 0's down the line.
void writeBytesValue(uint8_t value, int len) {
select();
writeBytesValueRaw(value, len);
release();
}
static void writeBytesValueRaw(uint8_t value, int len) {
while(len--) {
ledSPI.transfer(value);
}
}
// write a block of len uint8_ts out. Need to type this better so that explicit casts into the call aren't required.
// note that this template version takes a class parameter for a per-byte modifier to the data.
template <class D> void writeBytes(register uint8_t *data, int len) {
select();
uint8_t *end = data + len;
while(data != end) {
writeByte(D::adjust(*data++));
}
D::postBlock(len);
release();
}
// default version of writing a block of data out to the SPI port, with no data modifications being made
void writeBytes(register uint8_t *data, int len) { writeBytes<DATA_NOP>(data, len); }
// write a single bit out, which bit from the passed in byte is determined by template parameter
template <uint8_t BIT> inline void writeBit(uint8_t b) {
ledSPI.transfer(b);
}
// write a block of uint8_ts out in groups of three. len is the total number of uint8_ts to write out. The template
// parameters indicate how many uint8_ts to skip at the beginning of each grouping, as well as a class specifying a per
// byte of data modification to be made. (See DATA_NOP above)
template <uint8_t FLAGS, class D, EOrder RGB_ORDER> __attribute__((noinline)) void writePixels(PixelController<RGB_ORDER> pixels) {
select();
int len = pixels.mLen;
while(pixels.has(1)) {
if(FLAGS & FLAG_START_BIT) {
writeBit<0>(1);
}
writeByte(D::adjust(pixels.loadAndScale0()));
writeByte(D::adjust(pixels.loadAndScale1()));
writeByte(D::adjust(pixels.loadAndScale2()));
pixels.advanceData();
pixels.stepDithering();
}
D::postBlock(len);
release();
}
};
FASTLED_NAMESPACE_END