Organize projects in chronological order

This commit is contained in:
2026-05-30 09:34:40 -04:00
parent daebe3bd49
commit 124a37a79c
22 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
"version": "0.2.0",
"configurations": [
]
}

View File

@@ -0,0 +1,138 @@
# 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 not great. On simpler microcontrollers, `delay()` burns CPU cycles (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). But on the ESP32-S3 in particular, the solution package as a whole isn't losing those cycles - it runs Arduino on top of an RTOS, so `delay()` is actually yielding back to the RTOS scheduler so that things like bluetooth and wifi stacks can continue. But *our* program is fully blocked here. If I want to scan additional buttons, or perform some other task, we're locked in here. So 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 the waves on the sea. 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, we can use a digital value V with a precision of P bits, to express that for a given percentage of the period (known as the "duty cycle"), the circuit should be ON (usually HIGH), while for the remainder of the duty cycle, it should be OFF (normally LOW). For example:
* Circuit produces 5v when HIGH
* Over a period T of 1000ms
* Using a 10 bit precision (maximum value `1023`)
... We can send a value of `512` to turn the signal HIGH for 50% duty cycle (50% of the period, approximately 500ms). Sending `256` would be approximately 25%, `768` approximately 75%, and `1023` 100%. While the output pin is only ever sending two values (ON at 5v or OFF at 0v), most circuits and devices actually average the voltage on the circuit. This makes our PWM output 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.
Time period T is also related to frequency, F. The time period in milliseconds has an inverse relationship to the frequency in hertz.
$$ f = \frac{1}{T} $$
As T increases positive from zero, F decreases. So longer time results in lower frequencies. This will become more important in future tutorials where we start using PWM for audio devices like buzzers.
![pulse width modulation](pwm.png)
*image source Freenove tutorials*
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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

View File

@@ -0,0 +1,73 @@
#define LED_COUNT 8
#define UP 1
#define DOWN 2
#define PWM_PRECISION 8
#define PWM_VALUE_MAX 1 << PWM_PRECISION
#define PIN_BUTTON 13
#define BUTTON_UP 1
#define BUTTON_BOUNCING 2
#define BUTTON_DOWN 3
#define BUTTON_HELD 4
const byte ledPins[] = {21, 47, 38, 39, 40, 41, 42, 2}; //define led pins
const byte chns[] = {0, 1, 2, 3, 4, 5, 6, 7}; //define the pwm channels
int pwmvalues[] = {1, 1 << 1, 1 << 2, 1 << 3, 1 << 4, 1 << 5, 1 << 6, 1 << 7};
char velocities[] = {DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN};
int delayTimes = 100; //flowing speed ,the smaller, the faster
unsigned long button_bounce_millis = 0;
char buttonState = BUTTON_UP;
void setup() {
for (int i = 0; i < LED_COUNT; i++) { //setup the pwm channels
ledcAttachChannel(ledPins[i], 1000, PWM_PRECISION, chns[i]);
}
pinMode(PIN_BUTTON, INPUT);
}
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;
}
}
void loop() {
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;
}
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]);
}
delay(delayTimes);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB