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
19 changed files with 569 additions and 0 deletions

Binary file not shown.

View File

@@ -6,6 +6,8 @@ This project uses an LM339N comparator chip, a voltage ladder, a potentiometer a
[Here is a video of it working](https://x.com/AKLabsDotNet/status/2064431239013048425?s=20) [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 # Lessons Learned
* What is a voltage ladder * What is a voltage ladder

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/