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
- Breadboards probably introduce just as many bugs as I do
- 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
.inofiles are magic, and get a bunch of stuff added to them for free, like the correct header files (likeArduino.h) for whatever you're using. - Beware the fact that Arduino is C++ by default - if you make a
.cfile and expect to be able to callSerial.xxx(), that's not gonna work. Your code has to be.cppfiles, 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. The easiest way is to fire up a debugger session from inside of arduino-ide and check ps -aufx to find the openocd process being executed. 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. Right?
Not really.
The Arduino IDE hides some complexity in how to hook up the debugger. The openocd server serves as a bridge between gdb and the jtag interface on the ESP32, and you can't just hook gdb up to it. Even once you find the right gdb binary to connect to it (it's buried somewhere in your arduino install), there is some configuration magic buried in the arduino IDE installation that tells gdb and openocd how to talk to each other. I tried several times to make this work outside of the Arduino IDE and to be perfectly frank it was not worth the effort.
You can install the Espressif ESP32 toolchain separately, and run the debugger from that, but again you're going to have some trouble getting it to work exactly right. There's a strong change the upstream Espressif toolchain you downloaded has a different gcc version than what your Arduino IDE is running.
As long as I'm working with Arduino, I'll just use the Arduino IDE to debug the code. When I move on to STM32 after these projects, I'll drop out to my beloved command line gdb.
More breadboard bugs
My biggest frustration with these breadboards so far has been resistors. Resistors have really long legs out of the package, and I think for breadboard usage, much shorter resistor legs would be better. With their long legs, they short into other nodes, bend over, come loose, and get caught on other wires as they get moved around. I hate to cut down resistor legs since I might need them longer in other projects, but with this project, I had to start cutting them down so they would lie flush on the board.
For example I had a problem with the LCD 7 segment display where segment D (the very bottom light in a cell) and segment E (the bottom left light in a cell) would only light together. So 2, 6, 8, 0, b, and d all appeared correctly. But 3, 5, A, and F would not. I traced every wire, every connection, checked every voltage on every pin of the display and the shift register. Best I can tell, it was due to a short somewhere in the breadboard. But I made the rookie mistake of wiring everything up before starting to work on the code, so by the time I saw the error, I was retiscent to pull it apart. But I'm glad I did; cutting down the resistor legs for flush mounting solved the problem.
By Day 4 of the project the breadboard had defeated me. The LED matrix had a short of some kind very similar to what the 7 segment LCD had. Some columns didn't work, some rows didn't work, and bumping the boards and resistors caused things to behave strangely. With great reluctance, I pulled everything apart, rewired the LED matrix and 7 segment display on their own breadboards away from the main ESP32 breakout and the potentiometer, cut down all the resistor wires for flush mounting, and that resolved all the issues.
Joysticks and ADC and wierd outputs
Once I had the 7 segment and LED matrix driver finally working (able to plot arbitrary pixels on the LED matrix), I attached the joystick so the user could control the snake. Attached the joystick X and Y to pins 1 and 2, power and ground, Z to 47, and ... the shift registers driving the 7 segment and matrix would see ADC values output to them from their signal pins whenever joystick X or Y went positive.
For example, the potentiometer that I use as a difficulty selector was on pin 9. The joystick X and Y were on pins 1 and 2. The serial data signals for the shift registers were on pins 14 and 18. The Freenove ESP32-S3-WROOM pinout shows:
- 1 :
GPIO1, ADC1_CH0, T1 - 2 :
GPIO2, ADC1_CH1, LED_ON, T2 - 9 :
GPIO9, CAM_Y3, ADC1_CH8, T9 - 14 :
GPIO14, ADC2_CH3, T14 - 18 :
GPIO18, CAM_Y7, ADC2_CH7, U1RXD
Essentially this means that the shift registers were using GPIO pins that were doubled up on ADC2, and the joystick & potentiometer were using pins set up for ADC1. Why in the world would providing analog input values on pins 1 and 2 affect the outputs read on pins 2 and 9 - they are different ADC channels - let alone 14 and 18, when they are a separate ADC device entirely? I even tried moving pin 9 directly to ground - so there was never any signal coming to that pin, it was pinned to ground - and I still read values off that pin corresponding to the joystick values coming in to the ADC.
I didn't actually wind up figuring this out. I wrestled with it for a bit, checked the TRM to see if maybe I could disable ADC channels I wasn't using, tried multisampling the ADC, tried attenuating it ... Nothing helped. I considered moving the shift register serial and clock lines to different GPIO pins without ADC, but almost every pin on this thing is connected to the ADC. There are no pins that are just GPIO with nothing else. I knew there must have been SOMETHING I did wrong, but at this point I'd been struggling with the LED matrix wiring and logic for days, and I was just tapped out with figuring out hardware wierdness. I threw in the towel on the joystick, and changed my design to use a small controller that used 4 buttons instead.
... Once I got all that working, I discovered that the wire between the potentiometer and pin 10 was bad. No continuity through the wire. Had it been that way all along? I don't think so. But sure as shit it's dead now. Replaced that and the wierd analog readings on the potentiometer went away. I had already ripped out the joystick handling code, so I didn't try putting it back in. There's no turning back now.
Custom 74HC595 driver code
- Managing the clock signal
Driving 7-segment displays
These 7 segment LCD displays have an odd relationship between their pins and the segments of the LCD. You might think that you would start at pin 1 and proceed in some kind of logical fashion, clockwise or counterclockwise, around the device. Nope. The pins are all over the place. Wiring them is kind of a pain, especially when you have a lot on the breadboard.
- 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.
- High nibble
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)