Compare commits

2 Commits

Author SHA1 Message Date
8303ea9d10 WIP 2026-06-12 12:40:08 -04:00
200156c432 Add the 3-bit Flash ADC project 2026-06-11 17:01:47 -04:00
23 changed files with 707 additions and 0 deletions

Binary file not shown.

140
07-LM339N-ADC/README.md Normal file
View File

@@ -0,0 +1,140 @@
# Breadboard
![breadboard](breadboard.png)
This project uses an LM339N comparator chip, a voltage ladder, a potentiometer and 4 LEDs to create a Flash Analog to Digital Converter.
[Here is a video of it working](https://x.com/AKLabsDotNet/status/2064431239013048425?s=20)
It's worth pointing out that this ADC outputs thermometer code (unary code), not binary. I would have needed to use an additional priority encoder to get it converted down to binary. I didn't have a priority encoder on hand (though I could've built one with quite a few other chips I *did* have on hand), so I just left it at unary output.
# Lessons Learned
* What is a voltage ladder
* How pull up and pull down resistors work
* Breadboards are small finnicky things
* How USB power delivery dummies work
## Voltage ladder
Not a lot to say here. A voltage ladder is a series of connected voltage dividers. Each junction between resistors in the series is a step on the "ladder".
```
Supply
|
R1 220
|
>----- Step 1
|
R2 220
|
>----- Step 2
|
R3 220
|
>----- Step 3
|
R4 220
|
>----- Step 4
|
R5 220
|
GROUND
```
... and so on. The voltage is (assuming an equal dispersion of resistor values) equally divided at every step on the ladder, allowing you to take different voltages at different parts of the ladder. Varying the resistance values in between steps of the ladder will give you different divisions along the ladder.
For this project, I used 220 ohm resistors all the way through, for an equal 25% division at each step.
## Pull Up and Pull Down Resistors
This circuit uses LM339N chips to perform the comparison operations on the input voltage vs the reference voltage at various points along the voltage ladder. The LM339N output pins require a pull up resistor to do their job correctly.
[From the LM339N datasheet](https://www.ti.com/lit/ds/symlink/lm339-n.pdf?ts=1781077876487):
> The output is HIGH when the voltage on the non-inverting (+IN) input is greater than the inverting (-IN) input. The output is LOW when the voltage on the noninverting (+IN) input is less than the inverting (-IN) input
The voltage ladder is going to `-IN` and the potentiometer is going to `+IN`. So when the potentiometer is greater than a given step of the voltage ladder, the output for that step is supposed to be `LOW`; otherwise it should be `HIGH`. However, if we just wire the LEDs up directly to the output pins, we get unpredictable readings on the output pin - I got `0.25v` when I was expecting `5v`.
<center><img alt="Okay but why" src="butwhy.webp" width="320px"/></center>
When working with electronics, you might see something called a "pull up" or "pull down" resistor. For example,
* "... We connect this pin to a pull up resistor which pulls the voltage up to 5v"
* "... We connect this pin to a pull down resistor which pulls the voltage down to 0v"
Electricity, like water, always wants to take the path of least resistance possible. So if there is a resistor of any value in a circuit path, the electrons will prefer to go a different way until the path of least resistance is found. Pull up and pull down resistors use this property of electricity, in cooperation with [transistors](../02-buzzers_with_transistors#transistors), to control the flow of voltage between a supply and a ground.
In lesson 2 I talked about [how transistors work](../02-buzzers_with_transistors#transistors)), having a collector, an emitter, and a base. They work by a charge coming into the base and changing the amount of charge carriers that can pass through depletion zone in the `P/N` materials at the `BC` and `BE` junction. Crudely speaking, the base connector is used like a switch to turn the flow from one side of the transistor to the other (depending on how the transistor is configured) on or off.
If we look at the block diagram for the LM339N [from the datasheet](https://www.ti.com/lit/ds/symlink/lm339-n.pdf?ts=1781077876487), we can see that the integrated circuit is made up of (among other things) a number of transistors, especially transistors that sit right at the output pins:
<center><img alt="TI LM339N block diagram" src="lm339nblock.png"/></center>
This tells us that the output pin is actually one leg of a transistor, and that the transistor is set up as a low side switch. Makes sense - we have comparator that will be HIGH or LOW depending on the operation, so we have an output that will be turned on or off depending on the output of that comparison. The LM339N datasheet further clarifies this:
> The output of the LMx39-N series is the uncommitted collector of a grounded-emitter NPN output transistor.
What they're saying here is that the collector is floating, but the transistor is setup as a low-side switch. *The collector is actually the output pin*; the emitter is going to ground. Remember what I said about low side switching:
> ... with low side switching, the load is always connected to +V and the load just floats when it's turned off.
This is why we see `0.25v` without the pull up resistor. The output is just floating, so when the base goes high, there's no voltage there, because the LM339N isn't actually providing any voltage of its own. It essentially produces unpredictable noise, because the charge from base was allowing charge carriers to move but there were no charge carriers around and they had nowhere to go anyway. I got `0.25v` you might get something else, but either way it's not usable voltage.
So how do we solve this? How do we get 5v out of our pin like we expect? We have to **supply the 5v to the output pin ourself**, and use a `pull up resistor` to make the 5v supply current flow on the output path whenever that transistor is activated.
```
+---------+
| |
| | OUTPUT
| |-----+--> 220R ---> LED
| LM339N | ^
| | |
| | |
| | +--- 10kR <--- +5v
+---------+
```
What this does is weakly connects 5v onto the output pin. When the comparator returns a false result, the transistor provides a strong connection to ground that provides an easier path than for the +5v to travel through the resistor out to the output, and the transistor sinks the `+5v` supply voltage coming from the output pin directly to ground. So we get a `+5v` when the comparator says "your potentiometer is above this step on the voltage ladder", and we get ground reference voltage (`0v`) in the opposite case. However when the comparator returns a true result, the transistor ensures the easiest path for the `+5v` supply voltage is out to the LED rather than back towards ground.
A pull down resistor works in the opposite way.
```
+---------+
| |
| | OUTPUT
| |------+--> 220R ---> LED
+5v --> | DEVICE | |
| | |
| | |
| | +--- 10kR ---> GROUND
+---------+
```
Instead of weakly connecting the output pin to supply voltage, it weakly connects the output pin to ground. When the transistor is open, the pull down resistor keeps the output (which would otherwise float) down at ground reference voltage (`0v`). When the transistor is closed, the `10k` resistor allows only a very small amount of current to flow, which keeps the output at supply voltage (depending on the device).
<center><img alt="That's just on and off with extra steps" src="extrasteps.jpeg" /></center>
Part of the confusion here is not understanding how the LM339N in particular works. The LM339 never outputs a logic 1. It only outputs a logic 0 or "nothing." The pull-up resistor converts that "nothing" state into a valid logic 1 by gently connecting the output to the supply voltage. The datasheet explains this, but if you're like me, that point might WHOOSH past you.
Open collector outputs like you see on the LM339N do have some uses. For example:
* You can combine multiple outputs onto a single pullup resistor to make a `wired-AND` or `wired-OR` (depending on the logic of your particular chip). This way, if any of the comparator outputs go low, they all go low.
* You can pretty easily set different outputs to different voltage levels. For example you can supply the LM339N with 5v but drop the output pins to 3.3v.
And I'm sure there are others besides this.
## Breadboards are small and finicky things
Once you start getting into integrated circuits, a breadboard starts feeling really small really fast. I've seen some folks who have breadboarding setups that are essentially dozens of breadboards glued together, and I'm definitely seeing why. Initially I had planned to use several of these LM339N chips to make a 4-bit ADC, but after seeing how hairy the wiring got on a single one, I quickly ditched that plan.
Also they are not the best at forming strong connections, especially if your components stand proud of the board. If your resistors have long legs, for example, they may bend and move and momentarily lose contact with the breadboard. You may need to poke things to make them work. On simple circuits that's not a huge issue, but even at this level of complexity, there were several times I thought I had a bug in my design only to find I needed to touch a loose resistor or an LED to make things work again.
## How USB power delivery dummies work
This was my first exposure to a USB-C power delivery dummy device. Basically it's just a little USB plug with an IC, a few resistors, a `VCC` and `GND` connection, and (on some models like mine) a switch that tells the IC what output voltage you want.
The device acts by pretending to be a USB device, and asks for the voltage you've requested. If the power delivery device can provide the granted voltage, that's what you get. Otherwise you will get 5 volts. Some devices, no matter what you ask for or what they agree to, will always provide 5 volts and you just have to get bent if you don't like that. But these things were a really neat way to power this board, and I'm sure I'll be using them again soon.
They can be had very cheaply - I'm sure there are cheaper options, but [these are the ones I ordered](https://www.amazon.com/dp/B0FXL6DLB9).

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

BIN
07-LM339N-ADC/butwhy.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

21
08-74HC595-Snake/.gdbinit Normal file
View File

@@ -0,0 +1,21 @@
# 1. Connect to the running OpenOCD server
target remote :3333
# 2. Configure GDB for Espressif-specific environments
set remote hardware-watchpoint-limit 2
set remote hardware-breakpoint-limit 2
set mem inaccessible-by-default off
# 3. Handle FreeRTOS multi-threading without crashing GDB
handle SIGTRAP noprint nostop pass
# 4. Define a custom macro to safely reset the ESP32-S3
define reset
monitor reset halt
flushregs
end
# 5. Execute a fresh reset and set initial breakpoint
reset
thbreak setup
continue

View File

@@ -0,0 +1,74 @@
#include <Arduino.h>
#include "controls.h"
#include "error.h"
/*
Need 11 GPIO pins
- 74HC95 for the 8 segment
- 74HC595 for the rows of the 8x8
- 74HC9 for the cols of the 8x8
- 1x for the Joystick button
- 1x for the buzzer
Need 3 ADC pins
- Joy X
- Joy Y
- Difficulty potentiometer
*/
// 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
// We save 4 pins by updating all 3 shift registers at once
// with one clock and one update signal. Each shift register
// only needs its own data line.
#define PIN_74HC595_CLOCK 11
#define PIN_74HC595_UPDATE 12
#define PIN_74HC595_LEDROWS_DATA 18
#define PIN_74HC595_LEDCOLS_DATA 13
#define PIN_74HC595_SCOREBOARD_DATA 14
#define PIN_BUZZER 21
#define PIN_DIFFICULTY 9
#define PWM_FREQUENCY 1000
#define PWM_BITWIDTH 12
Joystick js;
void initSerial()
{
Serial.begin(115200);
}
void setup() {
memset(&js, 0x00, sizeof(Joystick));
initSerial();
if ( initJoystick(&js, PIN_JOY_X, PIN_JOY_Y, PIN_JOY_Z) != ERRNO_SUCCESS ) {
Serial.printf("Failed to initialized Joystick datastructure : %d\n", errno);
}
}
void loop() {
if ( readButton(&js.button) != ERRNO_SUCCESS ) Serial.printf("Failed to read calibration button : %d\n", errno);
if ( js.button.state == BUTTON_STATE_DOWN && js.state != JOYSTICK_STATE_CALIBRATING ) {
js.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);
}
}
}

211
08-74HC595-Snake/README.md Normal file
View File

@@ -0,0 +1,211 @@
# Breadboard
This project uses 3 74HC595 serial to parallel shift registers, a 7-segment display multi unit, an 8x8 LED matrix, an active buzzer, an NPN transistor, a joystick, a handful of resistors and a potentiometer to make a playable Snake game.
# Lessons Learned
* Compiling, uploading, and debugging Arduino code from the CLI
* Managing mutexes in Arduino to handle thread locking
* How to write data to a 74HC595 shift register without using the Arduino HAL
* How to drive a 7-segment display
* How to drive an 8x8 LED matrix
* You don't need lots of pixels, or even colors, to make it fun
## Compiling, uploading, and debugging from the CLI
The Arduino IDE is ... not great. Its editor is crummy; I edit my code in Emacs and just use the IDE to upload, monitor and debug it. And frankly its debugger is not that great - I find most GUI debuggers to be kind of clunky, and this one especially. It's great for finding and adding libraries and things like that, but I don't like being constrained inside of it. Luckily the IDE is mostly just calling `arduino-cli` behind the scenes so everything the IDE does, we can do from the CLI. Setting up the arduino-cli is reasonably simple, but it quickly falls apart when you want to start debugging.
To set up the ESP32S3 dev board:
```
$ arduino-cli core update-index \
--additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
$ arduino-cli core install esp32:esp32 \
--additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
$ arduino-cli board list
Port Protocol Type Board Name FQBN Core
/dev/ttyACM0 serial Serial Port (USB) Unknown
```
To attach to the dev board when it's connected over USB Serial
```
# Get the board FQBN from the board list
$ arduino-cli board list all esp32
$ arduino-cli board attach -b esp32:esp32:esp32s3 -p serial:///dev/ttyACM0 .
```
To compile a project in the current directory
```
arduino-cli compile -b esp32:esp32:esp32s3 .
```
.. Then watch lots and lots of errors because the Arduino IDE has hidden the knowledge of header files and compiler flags from you.
* Apparently `.ino` files are magic, and get a bunch of stuff added to them for free, like the correct header files (like `Arduino.h`) for whatever you're using.
* Beware the fact that Arduino is C++ by default - if you make a `.c` file and expect to be able to call `Serial.xxx()`, that's not gonna work. Your code has to be `.cpp` files, like it or not.
* Your board has hidden compile options that you're used to setting in the Arduino IDE. For example USB CDC on boot options. If you don't set this at all, you'll get mysterious errors about Serial missing, etc. You need to inspect the board parameters and pass them to your build command appropriately.
* You don't get to control the link order on the output files.
* If you don't specify the output directory, it goes to some cache directory off in the middle of nowhere
```
$ mkdir -p build
$ arduino-cli compile --fqbn esp32:esp32:esp32s3:CDCOnBoot=cdc --output-dir build/ .
Sketch uses 320680 bytes (24%) of program storage space. Maximum is 1310720 bytes.
Global variables use 22504 bytes (6%) of dynamic memory, leaving 305176 bytes for local variables. Maximum is 327680 bytes.
$ ls build/
08-74HC595-Snake.ino.bin 08-74HC595-Snake.ino.elf 08-74HC595-Snake.ino.merged.bin esp32.esp32.esp32s3
08-74HC595-Snake.ino.bootloader.bin 08-74HC595-Snake.ino.map 08-74HC595-Snake.ino.partitions.bin
```
Sweet. Now to upload it
```
$ arduino-cli upload -p /dev/ttyACM0 -b esp32:esp32:esp32s3:CDCOnBoot=cdc --build-path build/
esptool v5.3.0
Serial port /dev/ttyACM0:
Connecting....
Connected to ESP32-S3 on /dev/ttyACM0:
Chip type: ESP32-S3 (QFN56) (revision v0.2)
Features: Wi-Fi, BT 5 (LE), Dual Core + LP Core, 240MHz, Embedded PSRAM 8MB (AP_3v3)
Crystal frequency: 40MHz
MAC: 1c:db:d4:59:f8:c4
Uploading stub flasher...
.... snip ...
Verifying written data...
Hash of data verified.
Hard resetting via RTS pin...
New upload port: /dev/ttyACM0 (serial)
```
Now let's monitor the serial:
```
$ arduino-cli monitor -p /dev/ttyACM0
Using default monitor configuration for board: esp32:esp32:esp32s3
Monitor port settings:
baudrate=9600
bits=8
dtr=on
parity=none
rts=on
stop_bits=1
Connecting to /dev/ttyACM0. Press CTRL-C to exit.
```
Let's debug it. The process requires spinning up OpenOCD and connecting GDB to it. Figuring out where OpenOCD got installed is not necessarily straightforward ... it depends on your toolchain, version, etc ... There is some googling involved. But once you find it:
```
cd ~/.arduino15/packages/esp32/tools/openocd-esp32/v0.12.0-esp32-20251215/share/openocd/scripts/
../../../bin/openocd -f interface/esp_usb_jtag.cfg -f target/esp32s3.cfg
```
Now you can connect gdb to it. I use a `.gdbinit` file to help set up GDB for monitoring this specific hardware environment, doing things like mapping a custom reset function:
```
$ cat .gdbinit
# 1. Connect to the running OpenOCD server
target remote :3333
# 2. Configure GDB for Espressif-specific environments
set remote hardware-watchpoint-limit 2
set remote hardware-breakpoint-limit 2
set mem inaccessible-by-default off
# 3. Handle FreeRTOS multi-threading without crashing GDB
handle SIGTRAP noprint nostop pass
# 4. Define a custom macro to safely reset the ESP32-S3
define reset
monitor reset halt
flushregs
end
# 5. Execute a fresh reset and set initial breakpoint
reset
thbreak setup
continue
```
<center><img alt="This is where we would run the debugger, if we had one" src="nodebugger.jpeg" width="320px"/></center>
This is where it gets quite silly. Apparently the esp32 toolchain installed by `arduino-cli` doesn't include the debugger. (Who would want a silly thing like that?) So you have to actually download and install the esp32 toolchain from Espressif directly, make sure that the gcc version matches, and then just drop in the debugger from that toolchain.
I'm sure there won't ever be any problems at all from mixing and matching like this.
So I had to ...
1. Install the Espressif EIM tool directly from Espressif via Apt
2. Install the ESP-IDF via the Espressif EIM tool
3. ... wait ... watch my disk space get consumed by duplicate dev tools ...
4. Source the ESP-IDF environment variables into my terminal `source "/home/andrew/.espressif/tools/activate_idf_v6.0.1.sh"`
5. Run the debugger against openocd
... And it works
```
Hardware assisted breakpoint 1 at 0x42001f3f: file /home/andrew/source/source.starfort.tech/andrew/esp32-learning/08-74HC595-Snake/08-74HC595-Snake.ino, line 49.
Info : [esp32s3.cpu0] Target halted, PC=0x42008890, debug_reason=00000000
[esp32s3.cpu0] Target halted, PC=0x42008890, debug_reason=00000000Info : Set GDB target to 'esp32s3.cpu0'
Set GDB target to 'esp32s3.cpu0'
Info : [esp32s3.cpu1] Target halted, PC=0x42001F3F, debug_reason=00000001
[esp32s3.cpu1] Target halted, PC=0x42001F3F, debug_reason=00000001
Info : Detected FreeRTOS version: (10.5.1)
[New Thread 1070529176]
[Remote target exited]
[New Thread 1070202636]
[New Thread 1070204132]
[New Thread 1070185320]
[New Thread 1070207644]
[New Thread 1070194980]
[New Thread 1070183888]
[Switching to Thread 1070529176]
Thread 2 "loopTask" hit Temporary breakpoint 1, setup ()
at /home/andrew/source/source.starfort.tech/andrew/esp32-learning/08-74HC595-Snake/08-74HC595-Snake.ino:49
49 memset(&js, 0x00, sizeof(Joystick));
(gdb)
```
Nevermind that the arduino-cli is compiling with a 14.x version of the esp32 gcc and this toolchain I just installed is using 16.x. .... You know what it's fine. Don't worry about it.
This kind of garbage is apparently just part and parcel of working in the embedded world. When I get into STM32 and PIC microcontrollers here in a bit, I'll wind up with even more toolchains strung about and connected together with other various glue. I'm not entirely unfamiliar with this - back when I was at Nintendo, rigging up software for the devkits involved spaghetti library version and application management of the highest order. It wasn't until right about the time tht I left that we finally got with SDSG and got a working tool to manage all of that mess in a somewhat reasonable manner.
This just means that I'll continue using the Arduino IDE to debug my code; I can do everything else safely from the command line.
## Custom 74HC595 driver code
* Getting the Arduino code to compile the additional .c file and include the header
* Managing the clock signal
## Driving 7-segment displays
* Single units
* Multiple units
## Driving an 8x8 LED matrix
* Treat it like a tiny framebuffer. 64 (8*8) bytes, each containing a packed bitfield.
* High nibble
* On / Off
* Is it a pellet or a snake part
* Low nibble
* How many game cycles should this stay lit. For snake parts, this is a counter. For pellets this is ignored.
## Fun within constraints
* Only 1 color
* Only 64 possible pixels
* Four 7 segment readouts
* Only 1 sound
* Only 1 button
* One player
* 1 difficulty adjuster (controls the speed of the game clock)

View File

@@ -0,0 +1,4 @@
#!/bin/bash
arduino-cli compile --fqbn esp32:esp32:esp32s3:CDCOnBoot=cdc --output-dir build/ .

View File

@@ -0,0 +1,154 @@
#include <Arduino.h>
#include <stddef.h>
#include <stdint.h>
#include "controls.h"
#include "error.h"
// For initializing B103-348 style joysticks
int initJoystick(Joystick *js, uint8_t pin_x, uint8_t pin_y, uint8_t pin_z)
{
if ( js == NULL ) {
ERROR(ERRNO_NULLPOINTER);
}
js->button.pin = pin_z;
js->button.debouncetime = BUTTON_STABLETIME;
js->button.pressedvalue = LOW;
pinMode(js->button.pin, INPUT_PULLUP);
js->x.pin = pin_x;
js->x.calibration.stable_time = JOYSTICK_STABLETIME;
js->y.pin = pin_y;
js->y.calibration.stable_time = JOYSTICK_STABLETIME;
return ERRNO_SUCCESS;
}
// For initializing simple buttons
int initButton(Button *button)
{
pinMode(button->pin, INPUT);
return ERRNO_SUCCESS;
}
// 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->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)
{
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;
} 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;
}

View File

@@ -0,0 +1,72 @@
#ifndef _JOYSTICK_H_
#define _JOYSTICK_H_
#include <Arduino.h>
#include <stdint.h>
#define BUTTON_STABLETIME 100
#define JOYSTICK_STABLETIME 3000
// 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)
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;
int initJoystick(Joystick *js, uint8_t pin_x, uint8_t pin_y, uint8_t pin_z);
int initButton(Button *button);
int calibrateJoystick(Joystick *js);
int readButton(Button *button);
int readJoystick(Joystick *js);
#endif // _JOYSTICK_H_

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
#!/bin/bash
(cd ~/.arduino15/packages/esp32/tools/openocd-esp32/v0.12.0-esp32-20251215/share/openocd/scripts/ ; ../../../bin/openocd -f interface/esp_usb_jtag.cfg -f target/esp32s3.cfg) &
/home/andrew/.espressif/tools/xtensa-esp-elf-gdb/16.3_20250913/xtensa-esp-elf-gdb/bin/xtensa-esp32-elf-gdb -x .gdbinit ./build/08-74HC595-Snake.ino.elf
#/home/andrew/.arduino15/packages/esp32/tools/esp-x32/2601/share/licenses/binutils/gdb -x .gdbinit ./build/08-74HC595-Snake.ino.elf

View File

@@ -0,0 +1 @@
#include "error.h"

14
08-74HC595-Snake/error.h Normal file
View File

@@ -0,0 +1,14 @@
#ifndef _ERROR_H_
#define _ERROR_H_
#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;
extern int errno;
#endif // _ERROR_H_

View File

@@ -0,0 +1,3 @@
#!/bin/bash
arduino-cli monitor -p /dev/ttyACM0

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -0,0 +1,2 @@
default_fqbn: esp32:esp32:esp32s3
default_port: serial:///dev/ttyACM0

View File

@@ -0,0 +1,3 @@
#!/bin/bash
arduino-cli upload -p /dev/ttyACM0 -b esp32:esp32:esp32s3:CDCOnBoot=cdc --build-path build/