Compare commits

1 Commits

Author SHA1 Message Date
696edc206a WIP - initial working version 2026-06-05 18:41:42 -04:00
2 changed files with 377 additions and 2 deletions

View File

@@ -34,9 +34,9 @@ The water analogy from many simpler electronics tutorials lead me to use words l
1. At what point downstream from that bend does the river become the ocean? Or, for our circuits, at what point does the circuit cease to be supply voltage and become ground reference voltage? 1. At what point downstream from that bend does the river become the ocean? Or, for our circuits, at what point does the circuit cease to be supply voltage and become ground reference voltage?
At some point after the bend in the river, the river will eventually reach the ocean. The ocean and the river are made up of different types of water. The river is fresh water; the ocean is salt water. There is an interface between the two at which point the water is neither river nor ocean, it is some mix of both. You go just a little upstream from that interface, it is the freshwater river. You go a little more downstream, it is the saltwater ocean. Electrical circuits do not behave this way. For an electrical circuit, it would be more like, as soon as the topography of the bend is complete, the freshwater river immediately ceases to be detectable and it is instantly the ocean. Even if the real body of the ocean is hundreds of miles away, at the instant where the pathway leaves the topographical bend connected to the upstream freshwater source, it becomes the river. This is because, in an electrical circuit, the concept of the hundreds of miles of riverbed between the bend and the ocean are irrelevant; the only thing that matters is the potential difference between point connection points. As soon as the downstream leg of our resistor R1 becomes attached to the rest of the circuit that eventally leads (unimpeded by another components) to ground, that downstream leg of R1 **becomes ground itself**. At some point after the bend in the river, the river will eventually reach the ocean. The ocean and the river are made up of different types of water. The river is fresh water; the ocean is salt water. There is an interface between the two at which point the water is neither river nor ocean, it is some mix of both. You go just a little upstream from that interface, it is the freshwater river. You go a little more downstream, it is the saltwater ocean. Electrical circuits do not behave this way. For an electrical circuit, it would be more like, as soon as the topography of the bend is complete, the freshwater river immediately ceases to be detectable and it is instantly the ocean. Even if the real body of the ocean is hundreds of miles away, at the instant where the pathway leaves the topographical bend connected to the upstream freshwater source, it becomes the saltwater ocean. This is because, in an electrical circuit, the concept of the hundreds of miles of riverbed between the bend and the ocean are irrelevant; the only thing that matters is the potential difference between point connection points. As soon as the downstream leg of our resistor R1 becomes attached to the rest of the circuit that eventally leads (unimpeded by another components) to ground, that downstream leg of R1 **becomes ground itself**.
The water analogy further breaks down here when we begin thinking about current. In the river analogy, even when the river meets the ocean, there is still current. The ocean is constantly moving. So the individual water molecules never stop moving. Rivers flow because of gravity; you call the difference in altitude between a river's origin and its exit to the sea as the *potential difference*. Because of the external force of gravity, the river flows down the terrain towards lower elevations, until it eventually reaches sea level. At that point, there is no longer a *potential difference* in the river's trajectory, if there were no more external forces acting on the water, it would have no more reason to flow. There would be no current. (Let us exclude for a moment the idea of all the various forces that can act upon a still body of water to induce currents in an otherwise level body of water at sea level). The water analogy further breaks down here when we begin thinking about current. In the river analogy, even when the river meets the ocean, there is still current. The ocean is constantly moving. So the individual water molecules never stop moving. Rivers flow because of gravity; you may call the difference in altitude between a river's origin and its exit to the sea as the *potential difference*. Because of the external force of gravity, the river flows down the terrain towards lower elevations, until it eventually reaches sea level. At that point, there is no longer a *potential difference* in the river's trajectory, if there were no more external forces acting on the water, it would have no more reason to flow. There would be no current. (Let us exclude for a moment the idea of all the various forces that can act upon a still body of water to induce currents in an otherwise level body of water at sea level).
Circuits behave similarly to the river in the analogy, but not exactly, because they don't have the same external forces working on them in the same way. Current only flows in an electrical circuit because there is a potential difference between one end of the circuit and the other. There is a positive and a negative; a source and a return; a supply and a ground. The difference in charge between these two points is expressed as a *potential difference*, otherwise known as a *voltage*. This difference in charge between two points is what causes current to flow, like the elevation difference in the river analogy. Whenever a circuit has no potential difference - meaning that the voltage between point A and point B is essentially 0 volts - there is no current flow. The water is perfectly still. And this brings us to the second interesting question. Circuits behave similarly to the river in the analogy, but not exactly, because they don't have the same external forces working on them in the same way. Current only flows in an electrical circuit because there is a potential difference between one end of the circuit and the other. There is a positive and a negative; a source and a return; a supply and a ground. The difference in charge between these two points is expressed as a *potential difference*, otherwise known as a *voltage*. This difference in charge between two points is what causes current to flow, like the elevation difference in the river analogy. Whenever a circuit has no potential difference - meaning that the voltage between point A and point B is essentially 0 volts - there is no current flow. The water is perfectly still. And this brings us to the second interesting question.

375
06-joystick/06-joystick.ino Normal file
View File

@@ -0,0 +1,375 @@
/*********************************************************************************/
// Constants
// 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)
#define PWM_FREQUENCY 1000
#define PWM_BITWIDTH 12
// 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
// But Pin 12 is where the tutorial has it
//#define PIN_JOY_Z 12
#define BUTTON_STABLETIME 100
#define JOYSTICK_STABLETIME 1000
// Button states
#define BUTTON_STATE_BOUNCING (1 << 0)
#define BUTTON_STATE_DOWN (1 << 1)
#define BUTTON_STATE_UP (1 << 2)
#define BUTTON_STATE_HELD (1 << 3)
// Joystick states
#define JOYSTICK_STATE_CALIBRATING (1 << 0)
#define JOYSTICK_STATE_CALIBRATED (1 << 1)
#define JOYSTICK_STATE_READING (1 << 2)
#define JOYSTICK_STATE_READY (1 << 3)
// 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_TOPLEFT 2 // and go clockwise
#define LED_2812_TOP 3
#define LED_2812_TOPRIGHT 4
#define LED_2812_RIGHT 5
#define LED_2812_BOTTOMRIGHT 6
#define LED_2812_BOTTOM 7
#define LED_2812_BOTTOMLEFT 8
// Error flags
#define ERRNO_SUCCESS 0
#define ERRNO_NULLPOINTER 1
#define ERRNO_IO 2
#define ERRNO_TIMEOUT 3
#define ERRNO_CALIBRATION 4
#define ERROR(x) errno=x; return x;
// Uses the Freenove WS2812 Lib for ESP32@2.0.1
/**********************************************************************************/
// Interface structures
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 Button {
// What pin is this button on
uint8_t pin;
// What is the state of this button (bitmask of BUTTON_STATE_*)
uint8_t state;
// Is this button pressed when it is LOW or HIGH?
uint32_t pressedvalue;
// How long do we wait when debouncing this button
uint32_t debouncetime;
// When did we start debouncing
uint32_t debounce_start_time;
} Button;
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;
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;
/*******************************************************************************/
// Global Variables
int errno;
Button calibration_button;
Joystick js;
/******************************************************************************/
// Initialization Functions
void initSerial()
{
Serial.begin(115200);
}
int init2812()
{
return ERRNO_SUCCESS;
}
int initJoystick(Joystick *js)
{
if ( js == NULL ) {
ERROR(ERRNO_NULLPOINTER);
}
js->button.pin = PIN_JOY_Z;
js->button.debouncetime = BUTTON_STABLETIME;
js->button.pressedvalue = LOW;
pinMode(js->button.pin, INPUT_PULLUP);
js->x.pin = PIN_JOY_X;
js->x.calibration.stable_time = JOYSTICK_STABLETIME;
js->y.pin = PIN_JOY_Y;
js->y.calibration.stable_time = JOYSTICK_STABLETIME;
return ERRNO_SUCCESS;
}
int initButton(Button *button)
{
pinMode(button->pin, INPUT);
return ERRNO_SUCCESS;
}
int initLEDs()
{
// Attach LEDC to the joystick status LED pins
ledcAttach(PIN_LED_X, PWM_FREQUENCY, PWM_BITWIDTH);
ledcAttach(PIN_LED_Y, PWM_FREQUENCY, PWM_BITWIDTH);
pinMode(PIN_LED_Z, OUTPUT);
return ERRNO_SUCCESS;
}
/****************************************************************************************/
// IO Routines
// 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;
}
// Read the state of a given button
int readButton(Button *button)
{
// We could improve this method further. It doesn't *really* debounce. It just introduces a delay between the
// point of first contact and the time when the value is read. A true debouncing would wait until the state was
// *stable* for a given period of time. I may come back and change it to work this way later.
uint32_t pinvalue;
uint32_t curmillis;
uint32_t buttonvalue;
if ( button == NULL ) {
ERROR(ERRNO_NULLPOINTER);
}
curmillis = millis();
buttonvalue = digitalRead(button->pin);
if ( (button->state & BUTTON_STATE_BOUNCING) == BUTTON_STATE_BOUNCING ) {
if ( (curmillis - button->debounce_start_time) >= button->debouncetime ) {
if ( buttonvalue == button->pressedvalue ) {
button->state = BUTTON_STATE_DOWN;
} else {
button->state = BUTTON_STATE_UP;
}
Serial.printf("Button on pin %d has debounced state %d\n", button->pin, button->state);
button->debounce_start_time = 0;
return ERRNO_SUCCESS;
}
return ERRNO_SUCCESS;
}
if ( buttonvalue != button->pressedvalue && (
(button->state & BUTTON_STATE_DOWN) == BUTTON_STATE_DOWN ||
(button->state & BUTTON_STATE_HELD) == BUTTON_STATE_HELD)){
// We are beginning to release. Debounce.
Serial.printf("Debouncing button on pin %d for release\n", button->pin);
button->state = button->state | BUTTON_STATE_BOUNCING;
button->debounce_start_time = curmillis;
}
if ( buttonvalue == button->pressedvalue && (
(button->state & BUTTON_STATE_DOWN) != BUTTON_STATE_DOWN &&
(button->state & BUTTON_STATE_HELD) != BUTTON_STATE_HELD)){
// We are beginning to press. Debounce.
Serial.printf("Debouncing button on pin %d for press\n", button->pin);
button->state = button->state | BUTTON_STATE_BOUNCING;
button->debounce_start_time = curmillis;
}
return ERRNO_SUCCESS;
}
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;
}
adcvalue = analogRead(js->y.pin);
if ( adcvalue < js->y.calibration.low || adcvalue > js->y.calibration.high ) {
js->y.position = adcvalue;
}
//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;
}
int displayLEDs(Joystick *js)
{
if ( js == NULL ) {
ERROR(ERRNO_NULLPOINTER);
}
// Is the joystick fully ready? Display state on the LEDs
if ( ( js->state & JOYSTICK_STATE_READY ) == JOYSTICK_STATE_READY ) {
ledcWrite(PIN_LED_X, js->x.position);
ledcWrite(PIN_LED_Y, js->y.position);
if ( ( js->button.state & BUTTON_STATE_DOWN ) == BUTTON_STATE_DOWN ) {
digitalWrite(PIN_LED_Z, HIGH);
} else {
digitalWrite(PIN_LED_Z, LOW);
}
return ERRNO_SUCCESS;
}
// Is the joystick calibrating? Turn all the LEDs off
if ( js->state == JOYSTICK_STATE_CALIBRATING ) {
ledcWrite(PIN_LED_X, 1);
ledcWrite(PIN_LED_Y, 1);
digitalWrite(PIN_LED_Z, LOW);
}
return ERRNO_SUCCESS;
}
int display2812(Joystick *js)
{
if ( js == NULL ) {
ERROR(ERRNO_NULLPOINTER);
}
// Is the joystick fully ready? Display direction on the 2812
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
}
return ERRNO_SUCCESS;
}
/**********************************************************************************************/
// Setup and Loop
void setup() {
errno = ERRNO_SUCCESS;
memset(&js, 0x00, sizeof(Joystick));
memset(&calibration_button, 0x00, sizeof(Button));
calibration_button.pin = PIN_BUTTON;
calibration_button.debouncetime = BUTTON_STABLETIME;
calibration_button.pressedvalue = LOW;
initSerial();
if ( init2812() != 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 ( 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);
}
void loop() {
if ( readButton(&calibration_button) != ERRNO_SUCCESS ) Serial.printf("Failed to read calibration button : %d\n", errno);
if ( calibration_button.state == BUTTON_STATE_DOWN && js.state != JOYSTICK_STATE_CALIBRATING ) {
// A calibrating joystick cannot be in any other state, so don't mask
Serial.printf("Calibrating joystick\n");
calibration_button.state = BUTTON_STATE_HELD;
js.state = JOYSTICK_STATE_CALIBRATING;
}
if ( js.state == JOYSTICK_STATE_CALIBRATING ) {
if ( calibrateJoystick(&js) != ERRNO_SUCCESS ) {
Serial.printf("Failed to calibrate joystick : %d\n", errno);
}
}
if ( ( js.state & JOYSTICK_STATE_CALIBRATED) == JOYSTICK_STATE_CALIBRATED ) {
if ( readJoystick(&js) != ERRNO_SUCCESS ) {
Serial.printf("Failed to read joystick state : %d\n", errno);
}
}
if ( displayLEDs(&js) != ERRNO_SUCCESS ) {
Serial.printf("Failed to display LEDs : %d\n", errno);
}
if ( display2812(&js) != ERRNO_SUCCESS ) {
Serial.printf("Failed to display 2812 LEDs : %d\n", errno);
}
}