126 lines
11 KiB
Markdown
126 lines
11 KiB
Markdown
# Joystick demo
|
|
|
|
# Lessons Learned
|
|
|
|
* 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 to select pin assignments on the ESP32 without relying on the tutorial
|
|
* How to calibrate joysticks
|
|
|
|
## 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.
|
|
|
|
The WS2812 tutorial(s) use a global variable with an initializer that is called outside of the scope of any function. This is something that you can do in C++ that you really can't do in C, and I tend to think C is right where C++ is wrong.
|
|
|
|
```arduino
|
|
Freenove_ESP32_WS2812 strip = Freenove_ESP32_WS2812(8, PIN_2812, 0, TYPE_GRB);
|
|
```
|
|
|
|
In C the code wouldn't compile, because you can't execute code (beyond setting initializer values with builtin types) outside of a function. In C++, this is valid for constructors. The compiler puts the objects in the data segment, which is nice, as the alternative might be to call `new()` on it and get storage from the heap, which is nasty. But the compiler will generate some magic unspecified initializer function wherein it will call the constructors on the object, before it calls `main()`. And 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>
|
|
|
|
I chose instead to force the initializer for the global variable to happen during the `init2812` routine, which is called during `setup`.
|
|
|
|
```arduino
|
|
alignas(Freenove_ESP32_WS2812) uint8_t strip_storage[sizeof(Freenove_ESP32_WS2812)];
|
|
Freenove_ESP32_WS2812 *strip;
|
|
|
|
int init2812(Freenove_ESP32_WS2812 **strip)
|
|
{
|
|
Freenove_ESP32_WS2812 *obj = new(strip_storage) 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;
|
|
}
|
|
```
|
|
|
|
With this pattern, I get the global variable, that lives in the data segment, that is initialized exactly when I want it to be initialized. The downside (some might say) is that I'm stuck with using a pointer to reference the object in memory. Someone who fears pointers could use `std::optional` and `emplace()` to do the same thing, assuming that their toolchain supports it. But I feel like that's adding complexity in layers of abstraction. It doesn't get much simpler than saying "This object lives in memory *right over there* and here is a pointer to it".
|
|
|
|
## 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>
|
|
|
|
When you're following tutorials it's easy to assume "Pin X does Y thing because that's what the tutorial connected me to", and it's especially easy to think "Pin X **only** does Y thing", or "You **always** do Y thing with Pin X". This is almost never true. Each microcontroller has a datasheet from the manufacturer that tells you what all the different pins on the MCU do. The particular dev board you're using should also have a datasheet (or at least a pinout) that shows how the dev board maps all of the microcontroller pins to the devboard pins. Using these references will show you that, in fact, you can just lots of pins for lots of different functions, and that depending on how the chip is set up, a given pin may be overloaded with several different features - and that these usually can't be used together at the same time.
|
|
|
|
For example, here is the Freenove ESP32-S3 WROOM pinout from Freenove, the manufacturer of the dev board I'm using
|
|
|
|
<center><img alt="Freenove ESP32-S3 WROOM pinout" src="wroompinout.png" width="800px" /></center>
|
|
|
|
And here is some of the pin reference documentation from Espressif on the ESP32-S3 chip itself that lives on the devboard (click each image for higher resolution - this can alos be found in chapter 2 of the datasheet from Espressif). This should tell us that the pins on our devboard aren't on the same place as the ones on the ESP32-S3 itself, they may not even be numbered the same, and the board vendor may not actually choose to expose the entire package. At least the vendor and the chip manufacturer should be using the same *names* for the pins (`GPIOxx` for example) so we can cross reference them.
|
|
|
|
<img alt="Espressif ESP32-S3 pinout" src="esp32s3pinout.png" width="20%" />
|
|
<img alt="Espressif ESP32-S3 pin names" src="esp32s3pinnames.png" width="30%" />
|
|
<img alt="Espressif ESP32-S3 pin functions" src="esp32s3pinfunctions.png" width="40%" />
|
|
<br/>
|
|
<img alt="Espressif ESP32-S3 GPIO functions" src="esp32s3gpiofunctions.png" width="100%" />
|
|
|
|
... The datasheet goes on to explain that not only might these pins have multiple different functions, but that those functions:
|
|
|
|
* Might be input only, output only, or both
|
|
* May or may not be high impedance
|
|
* What the input signal of unused functions on a given pin will read when checkeed
|
|
* What is the default state of the pin after reset
|
|
* What is the drive strength of the signal on the pin (how many milliamps)
|
|
* What glitches occur on which pins at powerup (high level, low level, pull up, pull down) and how long does that glitch last
|
|
* Which pins have mappings to which in-package Flash/SRAM pins, which makes these pins unsuitable for other usage
|
|
|
|
It's also worth pointing out that, in order to get the most complete picture of the pin configuration, I actually had to consult THREE different datasheets:
|
|
|
|
* The Espressif ESP32-S3 datasheet
|
|
* 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)
|
|
|
|
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.
|
|
|
|
```
|
|
// Pin 21 is GPIO only
|
|
#define PIN_BUTTON 21 // Calibration button
|
|
|
|
// Apparently there is a dedicated 2812 controller in the ESP32-S3 that lives on this pin
|
|
// and this pin only.
|
|
#define PIN_2812 48 // Data bus for the 2812 LED
|
|
|
|
// These pins are shared with the second ADC unit. We're not using that unit in this project
|
|
// but for future projects as my boards become more dense I need to remember that each pin
|
|
// choice is a tradeoff. At a certain point we run out of available pins for adding stuff on,
|
|
// and have to start getting creative about how we use the pins we have left.
|
|
|
|
#define PIN_LED_X 16 // Red X axis LED
|
|
#define PIN_LED_Y 18 // Green Y axis LED
|
|
#define PIN_LED_Z 15 // Yellow Z axis LED (button)
|
|
|
|
// Pins 1 and 2 are on ADC channel 0 and 1.
|
|
#define PIN_JOY_X 1
|
|
#define PIN_JOY_Y 2
|
|
// Pin 47 is GPIO only
|
|
#define PIN_JOY_Z 47
|
|
```
|
|
|
|
Interstingly enough, the Freenove documentation labels pin 48 on their dev board as featuring some kind of dedicated WS2812 controller. However the text of their C tutorial says that the WS2812 is driven through the RMT controller (more on that in a minute). The ESP32-S3 WROOM MCU on this devboard doesn't even list 48 pins on the datasheet (though the parent ESP32-S3 datasheet does, so I guess the parent datasheet is authoritative? Maybe the missing pins are in the WROOM's "keep out zone"). When you're designing a board, you may need to ask questions like, "how will this dev board map to the real world when my code is driving the MCU on a different PCB? Will my assumptions about pins be correct? Will the peripherals be in the same place and work the same way? *Will this peripheral I'm using on the dev board even be there*?"
|
|
|
|
Hopefully the documentation makes all of that clear. Sometimes it does not, and you have to go spelunking, or ask the vendor questions directly. In the case of the WS2812 mystery pin 48, we can see in the ESP32-S3 datasheet chapter 3 (peripheral pin configurations) that the remote control peripheral (RMT) can be assigned to "any GPIO pin", depending on how it is initialized. So it looks like the Freenove WS2812 driver library is simply forcing the assignment of one of the RMT channels to pin 48, and that's why the tutorials and datasheets list pin 48 as the home for WS2812.
|
|
|
|
## WS2812s and the RMT
|
|
|
|
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.
|