WIP
This commit is contained in:
@@ -1,12 +1,25 @@
|
|||||||
# Joystick demo
|
# Joystick demo
|
||||||
|
|
||||||
|
This demo shows using a joystick peripheral to control the position of an LED light on a WS2812 light strip, as well as how to calibrate a joystick, while using PWM driven LEDs to show the relative position of a joystick on its axis and the status of the momentary joystick pushbutton.
|
||||||
|
|
||||||
|
I won't cover the entirety of the code here, but you are welcome to peruse it for yourself: [06-joystick.ino](06-joystick.ino)
|
||||||
|
|
||||||
# Lessons Learned
|
# Lessons Learned
|
||||||
|
|
||||||
|
* Fritzing is time consuming but neat
|
||||||
* I still don't trust C++
|
* I still don't trust C++
|
||||||
* How the RMT peripheral on the ESP32-S3 works and why it is involved when driving WS2812 LED strs
|
* 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 select pin assignments on the ESP32 without relying on the tutorial
|
||||||
* How to calibrate joysticks
|
* How to calibrate joysticks
|
||||||
|
|
||||||
|
## Fritzing
|
||||||
|
|
||||||
|
I've been using a breadboard software named [Fritzing](https://fritzing.org/) to do breadboard diagrams for these projects. If the breadboard is unchanged from a tutorial, I'll just copy and paste from the tutorial, but whenever I modify a breadboard, I make up the model in Fritzing. The Freenove folks were cool enough to send the Fritzing components for the devboard I'm using so making the breadboards is relatively easy. Still, translating the breadboard from reality to Fritzing *feels* slow. That's probably just in my head, but I have tended to dread it, kind of like writing documentation. It does have some amount of simulation ability, which I haven't explored yet, but you can apparently do some amount of arduino simulation from within Fritzing, which is neat.
|
||||||
|
|
||||||
|
I also haven't gotten the hang of Fritzing's schematic functionality. It can generate schematics from breadboards, which is pretty cool, but I'll be honest the schematics are soup. You have to do a ton of cleanup to make them readable, and I haven't devoted the time yet to figuring out the best way to manage that workflow.
|
||||||
|
|
||||||
|
But if you're getting into this hobby, Fritzing is definitely worth the (small) money.
|
||||||
|
|
||||||
## I still don't trust C++
|
## I still don't trust 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.
|
To be fair, I give C++ more hate than it deserves. But the way some people write it really does the language no favors.
|
||||||
@@ -78,7 +91,12 @@ It's also worth pointing out that, in order to get the most complete picture of
|
|||||||
* The Espressif ESP32-S3 WROOM datasheet (the specific ESP32-S3 package my dev board is using, which has some differences vs a regular ESP32-S3)
|
* The Espressif ESP32-S3 WROOM datasheet (the specific ESP32-S3 package my dev board is using, which has some differences vs a regular ESP32-S3)
|
||||||
* The Freenove devboard documentation (such as it is)
|
* The Freenove devboard documentation (such as it is)
|
||||||
|
|
||||||
Using this information, I was able to make pin selections for my code, while noting some incompatibilities if we were to turn on other functions.
|
Using this information, I was able to make pin selections for my code, while noting some incompatibilities if we were to turn on other functions. I knew I was going to need:
|
||||||
|
|
||||||
|
* 2 input pins on an ADC controller for the joystick axes
|
||||||
|
* 2 output pins on a PWM controller for the joystick axes LED lights
|
||||||
|
* Something for the WS2812 signal
|
||||||
|
* 2 GPIOs, one for the calibration button, one for the joystick button
|
||||||
|
|
||||||
```
|
```
|
||||||
// Pin 21 is GPIO only
|
// Pin 21 is GPIO only
|
||||||
@@ -116,10 +134,179 @@ 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>
|
<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.
|
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 signal wire, and there is no clock synchronization. Basically every LED receives a 24 bit color value (in RGB or GRB or whatever format is appropriate), then there's a pause, then the next value is sent for the next LED. If you've got 8 LEDs in series, you get 8 24 bit values, in order.
|
||||||
|
|
||||||
|
```
|
||||||
|
<-- more LED values, or a
|
||||||
|
LED8val LED7val LED6val LED5val LED4val LED3val LED2val LED1val
|
||||||
|
```
|
||||||
|
|
||||||
|
The 24 bits for each LED RGB/GBR value are sent in reverse order (bit 23 is sent first, down to bit 0). What's really wierd is that the bits in the value are sent using pulse width modulation. Each bit in a value consists of some portion of a full duty cycle being high, and some other portion being low. If the duty cycle is empty, then there is no bit on the wire during that time.
|
||||||
|
|
||||||
|
```
|
||||||
|
0 bit:
|
||||||
|
┌─┐
|
||||||
|
│ │
|
||||||
|
│ └─────────
|
||||||
|
└───────────
|
||||||
|
0.4µs 0.85µs
|
||||||
|
|
||||||
|
1 bit:
|
||||||
|
┌──────┐
|
||||||
|
│ │
|
||||||
|
│ └────
|
||||||
|
└───────────
|
||||||
|
0.8µs 0.45µs
|
||||||
|
```
|
||||||
|
|
||||||
|
Apparently this kind of task is what the remote control peripheral was made for. It's a pulse encoder, and what you do is you fill a buffer in RAM with the values you want sent, tell it to go, and it will take care of performing all the pulse width encoding of those values for you. (Or, in the reverse: it receives and decodes pulses, and puts them in memory for you.) You could do it yourself on a GPIO pin manually sending the PWM signals but it is very easy to get wrong, as you can see with the timing data; and even if your code was perfect, if you're running on a multitasking environment on the ESP32, you're basically screwed. Instead we can just send the data to the independent hardware peripheral, and the CPU goes along its merry way doing other things.
|
||||||
|
|
||||||
## Calibrating joysticks
|
## 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.
|
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.
|
||||||
|
|
||||||
|
The calibration data lives on the JoystickAxis datastructure
|
||||||
|
|
||||||
|
```arduino
|
||||||
|
typedef struct Calibration {
|
||||||
|
// When did calibration start
|
||||||
|
uint32_t start_time;
|
||||||
|
// Low calibration range
|
||||||
|
uint32_t low;
|
||||||
|
// High calibration range
|
||||||
|
uint32_t high;
|
||||||
|
// How long we should sample values on this joystick
|
||||||
|
uint32_t stable_time;
|
||||||
|
} Calibration;
|
||||||
|
|
||||||
|
typedef struct JoystickAxis {
|
||||||
|
// What ADC pin is this joystick axis connected to
|
||||||
|
uint8_t pin;
|
||||||
|
// Calibration data
|
||||||
|
Calibration calibration;
|
||||||
|
// Position as of the last read time.
|
||||||
|
uint32_t position;
|
||||||
|
} JoystickAxis;
|
||||||
|
```
|
||||||
|
|
||||||
|
A Joystick itself is represented as a collection of axis measurements and a button
|
||||||
|
|
||||||
|
```arduino
|
||||||
|
typedef struct Joystick {
|
||||||
|
// X axis data
|
||||||
|
JoystickAxis x;
|
||||||
|
// Y axis data
|
||||||
|
JoystickAxis y;
|
||||||
|
// This is the Z axis switch on the joystick control
|
||||||
|
Button button;
|
||||||
|
// What is the state of this joystick (bitmask of JOYSTICK_STATE_*)
|
||||||
|
uint8_t state;
|
||||||
|
} Joystick;
|
||||||
|
```
|
||||||
|
|
||||||
|
The calibration method manages the state of the joystick in question, performing calibration over a given period of time
|
||||||
|
|
||||||
|
```arduino
|
||||||
|
// Calibrate a given joystick
|
||||||
|
int calibrateJoystick(Joystick *js)
|
||||||
|
{
|
||||||
|
uint32_t x = 0;
|
||||||
|
uint32_t y = 0;
|
||||||
|
uint32_t curmillis = 0;
|
||||||
|
if ( js == NULL ) {
|
||||||
|
ERROR(ERRNO_NULLPOINTER);
|
||||||
|
}
|
||||||
|
curmillis = millis();
|
||||||
|
if ( js->state == JOYSTICK_STATE_CALIBRATING ) {
|
||||||
|
if ( js->x.calibration.start_time == 0 || js->y.calibration.start_time == 0 ) {
|
||||||
|
// Starting a new calibration cycle
|
||||||
|
Serial.printf("Starting a new calibration cycle\n");
|
||||||
|
js->x.calibration.low = 65535;
|
||||||
|
js->x.calibration.high = 0;
|
||||||
|
js->y.calibration.low = 65535;
|
||||||
|
js->y.calibration.high = 0;
|
||||||
|
js->x.calibration.start_time = curmillis;
|
||||||
|
js->y.calibration.start_time = curmillis;
|
||||||
|
return ERRNO_SUCCESS;
|
||||||
|
}
|
||||||
|
if ( (curmillis - js->x.calibration.start_time) >= js->x.calibration.stable_time ) {
|
||||||
|
js->state = JOYSTICK_STATE_CALIBRATED;
|
||||||
|
js->x.calibration.start_time = 0;
|
||||||
|
js->y.calibration.start_time = 0;
|
||||||
|
Serial.printf("Calibrated joystick to x: <%d, %d> y <%d, %d>\n",
|
||||||
|
js->x.calibration.low,
|
||||||
|
js->x.calibration.high,
|
||||||
|
js->y.calibration.low,
|
||||||
|
js->y.calibration.high);
|
||||||
|
return ERRNO_SUCCESS;
|
||||||
|
}
|
||||||
|
x = analogRead(js->x.pin);
|
||||||
|
if ( x < js->x.calibration.low ) {
|
||||||
|
js->x.calibration.low = x;
|
||||||
|
} else if ( x > js->x.calibration.high ) {
|
||||||
|
js->x.calibration.high = x;
|
||||||
|
}
|
||||||
|
y = analogRead(js->y.pin);
|
||||||
|
if ( y < js->y.calibration.low ) {
|
||||||
|
js->y.calibration.low = y;
|
||||||
|
} else if ( y > js->y.calibration.high ) {
|
||||||
|
js->y.calibration.high = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ERRNO_SUCCESS;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The method for reading joystick axes and buttons is only called when the joystick has a `CALIBRATED` status
|
||||||
|
|
||||||
|
```arduino
|
||||||
|
if ( ( js.state & JOYSTICK_STATE_CALIBRATED) == JOYSTICK_STATE_CALIBRATED ) {
|
||||||
|
if ( readJoystick(&js) != ERRNO_SUCCESS ) {
|
||||||
|
Serial.printf("Failed to read joystick state : %d\n", errno);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
... and the joystick read function performs the primitive snap-to-center lazy calibration:
|
||||||
|
|
||||||
|
```arduino
|
||||||
|
int readJoystick(Joystick *js)
|
||||||
|
{
|
||||||
|
uint32_t adcvalue = 0;
|
||||||
|
if ( js == NULL ) {
|
||||||
|
ERROR(ERRNO_NULLPOINTER);
|
||||||
|
}
|
||||||
|
js->state = js->state | JOYSTICK_STATE_READING;
|
||||||
|
|
||||||
|
adcvalue = analogRead(js->x.pin);
|
||||||
|
if ( adcvalue < js->x.calibration.low || adcvalue > js->x.calibration.high ) {
|
||||||
|
js->x.position = adcvalue;
|
||||||
|
} else {
|
||||||
|
// Snap to center
|
||||||
|
js->x.position = 2048;
|
||||||
|
}
|
||||||
|
adcvalue = analogRead(js->y.pin);
|
||||||
|
if ( adcvalue < js->y.calibration.low || adcvalue > js->y.calibration.high ) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
readButton(&js->button);
|
||||||
|
js->state = js->state ^ JOYSTICK_STATE_READING;
|
||||||
|
if ( errno != ERRNO_SUCCESS ) {
|
||||||
|
Serial.printf("Failed to read joystick button : %d\n", errno);
|
||||||
|
return errno;
|
||||||
|
}
|
||||||
|
js->state = js->state | JOYSTICK_STATE_READY;
|
||||||
|
return ERRNO_SUCCESS;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
## Closing Thoughts
|
||||||
|
|
||||||
|
This project was fun. It was the first one I designed fully independently of tutorials (except for the PIN 48 assignment and library syntax for the WS2812), and I got a lot of reusable code out of the deal. The entire joystick and button subsystem can be reused in future projects. And I think one of my next projects is going to be connecting the joystick to a Linux PC as a human interface device, and seeing if I can control some [`libakgl`](https://source.starfort.tech/andrew/libakgl) demos with it.
|
||||||
|
|||||||
Reference in New Issue
Block a user