Compare commits

..

2 Commits

Author SHA1 Message Date
69c0846a27 WIP 2026-06-07 22:02:09 -04:00
5e7713f08b Add the WS2812 code 2026-06-06 23:50:04 -04:00
4 changed files with 105 additions and 14 deletions

View File

@@ -1,3 +1,5 @@
#include "Freenove_WS2812_Lib_for_ESP32.h"
/*********************************************************************************/ /*********************************************************************************/
// Constants // Constants
@@ -29,7 +31,7 @@
//#define PIN_JOY_Z 12 //#define PIN_JOY_Z 12
#define BUTTON_STABLETIME 100 #define BUTTON_STABLETIME 100
#define JOYSTICK_STABLETIME 1000 #define JOYSTICK_STABLETIME 3000
// Button states // Button states
#define BUTTON_STATE_BOUNCING (1 << 0) #define BUTTON_STATE_BOUNCING (1 << 0)
@@ -44,14 +46,14 @@
#define JOYSTICK_STATE_READY (1 << 3) #define JOYSTICK_STATE_READY (1 << 3)
// These aren't pins, they're just defines that help us identify the lights on the 2812 unit // These aren't pins, they're just defines that help us identify the lights on the 2812 unit
#define LED_2812_LEFT 1 // 2812 LEDs are ordered from connection point at left #define LED_2812_LEFT 0 // 2812 LEDs are ordered from connection point at left
#define LED_2812_TOPLEFT 2 // and go clockwise #define LED_2812_TOPLEFT 1 // and go clockwise
#define LED_2812_TOP 3 #define LED_2812_TOP 2
#define LED_2812_TOPRIGHT 4 #define LED_2812_TOPRIGHT 3
#define LED_2812_RIGHT 5 #define LED_2812_RIGHT 4
#define LED_2812_BOTTOMRIGHT 6 #define LED_2812_BOTTOMRIGHT 5
#define LED_2812_BOTTOM 7 #define LED_2812_BOTTOM 6
#define LED_2812_BOTTOMLEFT 8 #define LED_2812_BOTTOMLEFT 7
// Error flags // Error flags
@@ -97,7 +99,7 @@ typedef struct JoystickAxis {
uint8_t pin; uint8_t pin;
// Calibration data // Calibration data
Calibration calibration; Calibration calibration;
// Position as of the last read time // Position as of the last read time.
uint32_t position; uint32_t position;
} JoystickAxis; } JoystickAxis;
@@ -118,6 +120,7 @@ typedef struct Joystick {
int errno; int errno;
Button calibration_button; Button calibration_button;
Joystick js; Joystick js;
Freenove_ESP32_WS2812 *strip;
/******************************************************************************/ /******************************************************************************/
// Initialization Functions // Initialization Functions
@@ -126,8 +129,15 @@ void initSerial()
Serial.begin(115200); Serial.begin(115200);
} }
int init2812() int init2812(Freenove_ESP32_WS2812 **strip)
{ {
Freenove_ESP32_WS2812 *obj = new Freenove_ESP32_WS2812(8, PIN_2812, 0, TYPE_GRB);
if ( obj == NULL ) {
ERROR(ERRNO_NULLPOINTER);
}
obj->setBrightness(10);
obj->begin();
*strip = obj;
return ERRNO_SUCCESS; return ERRNO_SUCCESS;
} }
@@ -273,10 +283,16 @@ int readJoystick(Joystick *js)
adcvalue = analogRead(js->x.pin); adcvalue = analogRead(js->x.pin);
if ( adcvalue < js->x.calibration.low || adcvalue > js->x.calibration.high ) { if ( adcvalue < js->x.calibration.low || adcvalue > js->x.calibration.high ) {
js->x.position = adcvalue; js->x.position = adcvalue;
} else {
// Snap to center
js->x.position = 2048;
} }
adcvalue = analogRead(js->y.pin); adcvalue = analogRead(js->y.pin);
if ( adcvalue < js->y.calibration.low || adcvalue > js->y.calibration.high ) { if ( adcvalue < js->y.calibration.low || adcvalue > js->y.calibration.high ) {
js->y.position = adcvalue; js->y.position = adcvalue;
} else {
// Snap to center
js->y.position = 2048;
} }
//Serial.printf("Joystick x %d y %d\n", js->x.position, js->y.position); //Serial.printf("Joystick x %d y %d\n", js->x.position, js->y.position);
@@ -317,15 +333,43 @@ int displayLEDs(Joystick *js)
return ERRNO_SUCCESS; return ERRNO_SUCCESS;
} }
int display2812(Joystick *js) int display2812(Joystick *js, Freenove_ESP32_WS2812 *strip)
{ {
if ( js == NULL ) { if ( js == NULL ) {
ERROR(ERRNO_NULLPOINTER); ERROR(ERRNO_NULLPOINTER);
} }
strip->setLedColorData(LED_2812_LEFT, 0, 0, 0);
strip->setLedColorData(LED_2812_TOPLEFT, 0, 0, 0);
strip->setLedColorData(LED_2812_TOP, 0, 0, 0);
strip->setLedColorData(LED_2812_TOPRIGHT, 0, 0, 0);
strip->setLedColorData(LED_2812_RIGHT, 0, 0, 0);
strip->setLedColorData(LED_2812_BOTTOMRIGHT, 0, 0, 0);
strip->setLedColorData(LED_2812_BOTTOM, 0, 0, 0);
strip->setLedColorData(LED_2812_BOTTOMLEFT, 0, 0, 0);
// Is the joystick fully ready? Display direction on the 2812 // Is the joystick fully ready? Display direction on the 2812
if ( ( js->state & JOYSTICK_STATE_READY ) == JOYSTICK_STATE_READY ) { if ( ( js->state & JOYSTICK_STATE_READY ) == JOYSTICK_STATE_READY ) {
// Light up the correct LED_2812_* depending on the value of the X/Y axes on the joystick // Light up the correct LED_2812_* depending on the value of the X/Y axes on the joystick
if ( js->x.position < 1024 && (js->y.position < 3072 && js->y.position > 1024) ) {
strip->setLedColorData(LED_2812_LEFT, 128, 128, 128);
} else if ( js->x.position < 1024 && js->y.position < 1024 ) {
strip->setLedColorData(LED_2812_TOPLEFT, 128, 128, 128);
} else if ( ( js->x.position > 1024 && js->x.position < 3072 ) && js->y.position < 1024 ) {
strip->setLedColorData(LED_2812_TOP, 128, 128, 128);
} else if ( js->x.position > 3072 && js->y.position < 1024 ) {
strip->setLedColorData(LED_2812_TOPRIGHT, 128, 128, 128);
} else if ( js->x.position > 3072 && ( js->y.position < 3072 && js->y.position > 1024) ) {
strip->setLedColorData(LED_2812_RIGHT, 128, 128, 128);
} else if ( js->x.position > 3072 && js->y.position > 3072 ) {
strip->setLedColorData(LED_2812_BOTTOMRIGHT, 128, 128, 128);
} else if ( ( js->x.position > 1024 && js->x.position < 3072 ) && js->y.position > 3072 ) {
strip->setLedColorData(LED_2812_BOTTOM, 128, 128, 128);
} else if ( js->x.position < 1024 && js->y.position > 3072 ) {
strip->setLedColorData(LED_2812_BOTTOMLEFT, 128, 128, 128);
}
} }
strip->show();
return ERRNO_SUCCESS; return ERRNO_SUCCESS;
} }
@@ -342,7 +386,7 @@ void setup() {
calibration_button.pressedvalue = LOW; calibration_button.pressedvalue = LOW;
initSerial(); initSerial();
if ( init2812() != ERRNO_SUCCESS ) Serial.printf("Failed to initialize 2812 LED array : %d\n", errno); if ( init2812(&strip) != ERRNO_SUCCESS ) Serial.printf("Failed to initialize 2812 LED array : %d\n", errno);
if ( initLEDs() != ERRNO_SUCCESS ) Serial.printf("Failed to initialize LED GPIO pins : %d\n", errno); if ( initLEDs() != ERRNO_SUCCESS ) Serial.printf("Failed to initialize LED GPIO pins : %d\n", errno);
if ( initJoystick(&js) != ERRNO_SUCCESS ) Serial.printf("Failed to initialized Joystick datastructure : %d\n", errno); if ( initJoystick(&js) != ERRNO_SUCCESS ) Serial.printf("Failed to initialized Joystick datastructure : %d\n", errno);
if ( initButton(&calibration_button) != ERRNO_SUCCESS ) Serial.printf("Failed to initialize Calibration button and GPIO pin : %d\n", errno); if ( initButton(&calibration_button) != ERRNO_SUCCESS ) Serial.printf("Failed to initialize Calibration button and GPIO pin : %d\n", errno);
@@ -369,7 +413,7 @@ void loop() {
if ( displayLEDs(&js) != ERRNO_SUCCESS ) { if ( displayLEDs(&js) != ERRNO_SUCCESS ) {
Serial.printf("Failed to display LEDs : %d\n", errno); Serial.printf("Failed to display LEDs : %d\n", errno);
} }
if ( display2812(&js) != ERRNO_SUCCESS ) { if ( display2812(&js, strip) != ERRNO_SUCCESS ) {
Serial.printf("Failed to display 2812 LEDs : %d\n", errno); Serial.printf("Failed to display 2812 LEDs : %d\n", errno);
} }
} }

47
06-joystick/README.md Normal file
View File

@@ -0,0 +1,47 @@
# Joystick demo
# Lessons Learned
* I still don't trust C++
* How to drive an WS2812 RGB LED strip from the ESP32 using the Freenove library
* How the RMT peripheral on the ESP32-S3 works and why it is involved when driving WS2812 LED strs
* How to select pin assignments on the ESP32 without relying on the tutorial
* How to calibrate joysticks
## Not trusting C++
To be fair, I give C++ more hate than it deserves. But the way some people write it really does the language no favors.
The WS2812 tutorial(s) use a global variable with an initializer.
I chose to use a heap allocation rather than use a global scope class initializer. Calling code outside of a function in a C-derived language just feels wrong to me. The objects live in the data segment, whcih is nice, but the compiler will generate some magic unspecified initializer function wherein it will call the constructors on the object, before it calls `main()`. But the order of those initializers is not guaranteed, so if A depends on B, you may be shit out of luck. And I especially don't trust global initializers that seem to be touching hardware. The whole thing just feels *wrong*.
<center><img alt="it's wrong" width="320" src="cartmanitswrong.gif"/></center>
Still, I could have done better. I was lazy and used `new` to create a new object on the heap. This is fine for the simple demo but for a real product I would have statically allocated the memory in the data segment and then pointed the `new()` operator at it somewhere in `setup()`.
```arduino
```
## Selecting pin assignments: read the datasheets
Up until now I've been using whatever pins the tutorial chose because, well, I had enough to figure out while getting my fundamental electronics lessons. But ...
<center><img alt="read the datasheet" width="320" src="readthedatasheet.jpg"/></center>
## WS2812s
The WS2812 is a series of LEDs on a strip, which can be controlled by sending a compact stream of data to the strip, and each LED takes its own data before forwarding on the rest. This allows you to control each LED individually in a theoretically endless string of LEDs with very little hardware overhead. In the particular example here, the WS2812 strip is arranged as an octagon of LEDs on a square PCB package with one `V+` line, one `GND` line, and one `S`(ignal) line.
Oh, and you control it using the **infrared remote control module** on the ESP32.
<center><img alt="wot" width="320" src="../05-photoresistors/wot.jpeg"/></center>
The WS2812 is controlled with a wierd single line protocol that's really efficient but very sensitive to timing. The whole thing works off of PWM signals on the wire, and there is no clock synchronization. You could do it yourself on a GPIO pin manually sending the PWM signals but it is very easy to get wrong. So the RMT module, which was originally designed to convert data to pulses and back again for sending over infrared, is abused for this purpose instead. We can just send the data and the CPU goes along its merry way.
## Calibrating joysticks
I implemented calibration logic in my code, but it was a bit lazy, and to be honest, it doesn't do what real calibration typically does. But by the time I got here, I was just ready to move on to the next project. When you press the calibration button, the calibration routine starts which continually samples over a period of time. It creates a deadzone according to the lowest and highest values seen on each axes during this time. When future joystick reads occur, anything within this deadzone are counted as being at the center of the axis. Really I did this to account for potential wobble in the joystick I hooked to the arduino, but that was unnecessary, it was remarkably stable in the center.
A real calibration routine is more advanced and allows the center to actually move. As it samples, it figures out where the center of the sample average range is, and future joystick reads return the axis movement relative to that center location. My calibration routine here can't do that. I considered changing this, but again, that was outside of the original design goal for the calibration routine.

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB