Add README

This commit is contained in:
2026-05-28 08:04:29 -04:00
parent 1d1702c4ae
commit 61913ae3cc
4 changed files with 131 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
# Flowing LED bar light with button
![Breadboard diagram](breadboard.png)
This program sends a cascading light effect through the LED bar that fades on either side of a strong central point. It normally flows in one direction, but if you press the button, it changes direction.
## Lessons Learned
I learned several things from this project:
- How to use the Arduino IDE to deploy code to the ESP32-S3 WROOM
- How to use the Arduino IDE debugger to step through code running on the device
- How to configure GPIO pins on the ESP32-S3 as input or output
- How to read and write data to and from those GPIO pins
- Using a physical button to control program flow in the microcontroller
- Debouncing mechanical buttons
- What is pulse width modulation and how do we control it on the ESP32-S3
- Using pulse width modulation with an LED to create a smooth visual effect simulating an analog curve
## Debouncing buttons
![Illustration of bounce](bounce.png)
*image from Freenove tutorials*
Buttons and other physical contacts have jitter, being a time where they are becoming connected but not yet fully stable. This means we may read the input and get many different readings over the course of the first few milliseconds. The tutorial I was following had a very simplistic way to manage this:
```arduino
if (digitalRead(PIN_BUTTON) == LOW) {
delay(20);
// do something with the pin
```
... This is really wasteful. I paid good money for those CPU cycles, and on embedded hardware we don't have a lot of them to go around. (The ESP32-S3 has an Xtensa dual-core 32-bit LX7 running up to 240Mhz - stunning when you think about the price and size of the package, but still, not enough to be wasteful.) I improved that by using a timer in a function that yields time back to the CPU while it's waiting to verify that the button is debounced.
```arduino
void checkButtonPressed(void) {
unsigned long curmillis = millis();
if ( digitalRead(PIN_BUTTON) == LOW ) {
if ( buttonState == BUTTON_UP ) {
button_bounce_millis = millis();
buttonState = BUTTON_BOUNCING;
return;
} else if ( buttonState == BUTTON_BOUNCING && curmillis - button_bounce_millis > 20 ) {
if ( digitalRead(PIN_BUTTON) == LOW ) {
buttonState = BUTTON_DOWN;
}
}
} else if ( digitalRead(PIN_BUTTON) == HIGH ) {
buttonState = BUTTON_UP;
}
}
```
## Pulse Width Modulation
Pulse Width Modulation (PWM) is essentially a way for digital circuits to pretend to be analog. Essentially, analog circuits can produce signals that have gentle sweeping curves, elegant waves, like God creating the feminine. Digital circuits can only produce on or off values - hard square shapes with sharp edges, like man chiseling facts into stone.
![analog vs digital signals](analog-digital.png)
*image source [Klipsch Blog](https://www.klipsch.com/blog/digital-vs-analog-audio)*
Basically pulse width modulation allows us to say that, over a given period of time T ("duty cycle"), we can use a digital value V with a precision of P bits, to express that for a given percentage of the duty cycle, the circuit should be ON (usually HIGH), while for the remainder of the duty cycle, it shouuld be OFF (normally LOW). For example:
* Circuit produces 5v when HIGH
* Over a duty cycle T of 1000ms
* Using a 10 bit precision
... We can send a value of `(1 << 5)` to turn the signal HIGH for 50% of the duty cycle (approximately 500ms). Sending `(1 << 2)` would be approximately 25%, `(1 << 7)` approximately 75%, and `(1 << 9)` approximately 100%. This effectively varies the output voltage on the circuit according to a time series pattern that can make it seem like a wave, when in reality, we are just providing the same voltage for more or less of the time that the circuit is on.
![pulse width modulation](pwm.png)
*image source Freenove tutorials*
It's not *really* analog, because the voltage stays the same the entire time - an analog signal will oscilate from 0v to the height, then back down, and may even go negative to complete all 4 phases of the wave. But in many controls, it has a similar effect, and *feels* like analog.
In the case of an LED, by controlling the voltage with PWM, we can create a glowing or fading effect. As the duty cycle increases, the LED gets brighter; as it decreases, the LED gets dimmer.
The code connects several GPIO pins of the ESP32 to the input pins of an LED bar array to drive their power. In code, it uses an array, `pwmvalues`, to track the PWM value of each output pin. It initializes them in a ladder - each one being higher than the next. This creates the initial appearance of the waterfall/fade effect.
```arduino
const byte ledPins[] = {21, 47, 38, 39, 40, 41, 42, 2};
int pwmvalues[] = {1, 1 << 1, 1 << 2, 1 << 3, 1 << 4, 1 << 5, 1 << 6, 1 << 7};
```
The code also tracks the velocity (UP or DOWN) for each pin. This tells the code if it should be increasing or decreasing the value on that pin. (*I should have named this 'direction' rather than 'velocity' but whatever.*)
```arduino
#define UP 1
#define DOWN 2
char velocities[] = {DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN};
```
During the main program loop, it modifies the values of the pins according to their velocities (bouncing them UP at 0 and DOWN at the maximum value), and then outputs the value to the given GPIO pin.
```arduino
for ( int i = 0; i < LED_COUNT ; i++ ) {
if ( pwmvalues[i] == 0 ) {
velocities[i] = UP;
pwmvalues[i] = 1;
} else if ( pwmvalues[i] == PWM_VALUE_MAX ) {
velocities[i] = DOWN;
}
if ( velocities[i] == UP ) {
pwmvalues[i] = pwmvalues[i] << 1;
} else if ( velocities[i] == DOWN ) {
pwmvalues[i] = pwmvalues[i] >> 1;
}
ledcWrite(ledPins[i], pwmvalues[i]);
}
```
## Changing direction with the button
Now that we have working visual flow on the LEDs and the button debounce mechanic works, implementing the button is simple. When the user presses the buttons, all we have to do is invert the velocities for each pin, to reverse the visual effect.
```arduino
checkButtonPressed();
if ( buttonState == BUTTON_DOWN ) {
for ( int i = 0; i < LED_COUNT ; i++ ) {
if ( velocities[i] == UP) {
velocities[i] = DOWN;
} else {
velocities[i] = UP;
}
}
buttonState = BUTTON_HELD;
}
```
... We set `buttonState` to `BUTTON_HELD` after doing this, so that we only change direction once for each button press. If we didn't do this, then as long as the user held down the button, we would continually change direction on the bar. This usually had the appearance of a PAUSE button, which was neat, but not what I was going for.
[Here is a video of it in action](https://x.com/AKLabsDotNet/status/2058655798591476058)