Files
esp32-learning/08-74HC595-Snake/README.md
2026-06-12 12:40:08 -04:00

9.4 KiB

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
This is where we would run the debugger, if we had one

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)