Snake works! Scoreboard is fucky.

This commit is contained in:
2026-06-16 22:12:30 -04:00
parent 8303ea9d10
commit 414f0684ab
15 changed files with 648 additions and 325 deletions

View File

@@ -1,21 +0,0 @@
# 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

@@ -1,74 +1,348 @@
#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
#include "IC74HC595.h"
#include "7Segment.h"
#include "matrix.h"
#define PIN_BUZZER 21
#define PIN_DIFFICULTY 9
#define PWM_FREQUENCY 1000
#define PWM_BITWIDTH 12
#define PWM_BUZZER_CHANNEL 0
Joystick js;
#define ENTITY_ALIVE 1 << 0
#define ENTITY_FOOD 1 << 1
#define ENTITY_SNAKE 1 << 2
#define ENTITY_HIGH_MASK 240 // 11110000
#define ENTITY_LOW_MASK 15 // 00001111
int8_t player_x;
int8_t player_y;
int8_t delta_x;
int8_t delta_y;
// Entities represent a lit pixel on the LED matrix. Each entity is an 8 bit mask that consists of:
//
// 8 0
// 0 0 0 0 0 0 0 0
// ^ ^ ^ ^ ^ ^
// | | | | | `-- Alive or dead. When 1, display this entity.
// | | | | `---- Food (1)
// | | | `------ Snake (1)
// | | `-------- Reserved
// `-----+---------- Lifetime. How many game cycles should this entity remain for? 0-128.
// The lifetime gets counted down each cycle!
uint8_t entities[64] = {0};
int found_food = 0;
int8_t tail_length = 2;
uint32_t gamespeed;
uint32_t lastupdate;
uint32_t difficulty = 0;
uint8_t score = 0;
uint8_t open_indexes[64] = {0};
Button btn_left = {PIN_BUTTON_LEFT, 0, LOW, BUTTON_STABLETIME, 0};
Button btn_up = {PIN_BUTTON_UP, 0, LOW, BUTTON_STABLETIME, 0};
Button btn_down = {PIN_BUTTON_DOWN, 0, LOW, BUTTON_STABLETIME, 0};
Button btn_right = {PIN_BUTTON_RIGHT, 0, LOW, BUTTON_STABLETIME, 0};
void initSerial()
{
Serial.begin(115200);
Serial.printf("\nSERIAL READY\n");
}
void initBuzzer() {
ledcAttachChannel(PIN_BUZZER, 1000, 10, PWM_BUZZER_CHANNEL);
ledcWriteTone(PIN_BUZZER, 2000);
delay(1000);
ledcWriteTone(PIN_BUZZER, 0);
}
void move_player()
{
player_x += delta_x;
player_y += delta_y;
if ( player_x > 7) {
player_x = 0;
} else if ( player_x < 0 ) {
player_x = 7;
}
if ( player_y > 7 ) {
player_y = 0;
} else if ( player_y < 0 ) {
player_y = 7;
}
}
void collide_player()
{
uint8_t entity;
entity = entities[(player_x * 8) + player_y];
// Is there an entity where we're going?
if ( entity > 0 ) {
// We die if we hit our own body
if ( (entity & ENTITY_SNAKE) == ENTITY_SNAKE ) {
Serial.printf("Hit entity %d (%d) and died!", (player_x * 8) + player_y, entity);
ledcWriteTone(PIN_BUZZER, 2000);
delay(1000);
init_game();
} else if ( (entity & ENTITY_FOOD) == ENTITY_FOOD ) {
Serial.printf("Found food!\n");
found_food = 1;
}
}
}
void draw_entities()
{
uint8_t entity;
resetMatrix();
// Poke matrix pixels for every living entity
for ( int i = 0 ; i < 64; i++ ) {
entity = entities[i];
if ( entity & ENTITY_ALIVE == ENTITY_ALIVE ) {
if ( pokeMatrixPixel((i / 8), (i % 8)) != ERRNO_SUCCESS ) {
Serial.printf("Failed to poke pixel for entity %d : %d\n", i, errno);
return;
}
}
}
displayMatrix();
}
void update_matrix_entities()
{
uint8_t entity;
int8_t elife = 0;
// Poke matrix pixels for every living entity
for ( int i = 0 ; i < 64; i++ ) {
entity = entities[i];
if ( entity & ENTITY_ALIVE == ENTITY_ALIVE && i != ((player_x * 8) + player_y) ) {
// Snake parts age (except the head)
if ( entity & ENTITY_SNAKE == ENTITY_SNAKE ) {
elife = (entity & ENTITY_HIGH_MASK) >> 4;
Serial.printf("Entity %d (%d) has lifetime %d\n", i, entity, elife);
if ( found_food == 0 ) {
// Count down their lifetime
if ( elife == 0 ) {
Serial.printf("Entity %d died\n", i);
entity = 0;
} else {
elife -= 1;
entity = (elife << 4) | (entity & ENTITY_LOW_MASK);
}
entities[i] = entity;
} /*else {
elife += 1;
entity = (elife << 4) | (entity & ENTITY_LOW_MASK);
entities[i] = entity;
}*/
}
}
}
}
void wingame()
{
Serial.printf("The player won the game!\n");
}
void place_random_entity(int8_t eflags, int8_t set_player)
{
uint8_t index = 0;
uint8_t entity = 0;
uint8_t open_positions = 0;
memset((void *)&open_indexes, 0x00, 64);
// Count how many positions are open on the board
for ( int i = 0 ; i < 64; i++ ) {
entity = entities[i];
if ( entity == 0) {
Serial.printf("Entity %d is available (%d)\n", i, entity);
open_positions += 1;
open_indexes[open_positions] = i;
} else {
Serial.printf("Entity %d is alive (%d)\n", i, entity);
}
}
Serial.printf("There are %d positions open on the board\n", open_positions);
if ( open_positions == 0 ) {
// This is a win condition!
wingame();
return;
}
index = random(0, open_positions-1);
if ( index > 63 ) {
Serial.printf("Index out of bounds!\n");
return;
}
index = open_indexes[index];
// Place a random piece of food on the board
entities[index] = (eflags);
if ( set_player == 1 ) {
player_x = (index / 8);
player_y = (index % 8);
}
Serial.printf("Placed entity at %d (%d)\n", (index / 8) + (index % 8), entities[index]);
Serial.printf("Done\n");
}
void init_game()
{
tail_length = 2;
memset((void *)&entities, 0x00, 64);
gamespeed = 1000;
lastupdate = millis();
place_random_entity(ENTITY_HIGH_MASK | ENTITY_ALIVE | ENTITY_FOOD, 0);
// Place the player away from the food
Serial.printf("Placing player\n");
place_random_entity(((tail_length << 4) | ENTITY_ALIVE | ENTITY_SNAKE), 1);
}
void read_controls()
{
difficulty = readDifficulty();
if ( readButton(&btn_up) != ERRNO_SUCCESS ||
readButton(&btn_left) != ERRNO_SUCCESS ||
readButton(&btn_right) != ERRNO_SUCCESS ||
readButton(&btn_down) != ERRNO_SUCCESS ) {
Serial.printf("Failed to read buttons\n");
return;
}
// We don't let the player reverse direction back on themselves.
if ( (delta_x == 0) && (btn_up.state & BUTTON_STATE_DOWN) == BUTTON_STATE_DOWN ) {
delta_x = 1;
delta_y = 0;
}
if ( (delta_x == 0) && (btn_down.state & BUTTON_STATE_DOWN) == BUTTON_STATE_DOWN ) {
delta_x = -1;
delta_y = 0;
}
if ( (delta_y == 0) && (btn_right.state & BUTTON_STATE_DOWN) == BUTTON_STATE_DOWN ) {
delta_y = -1;
delta_x = 0;
}
if ( (delta_y == 0) && (btn_left.state & BUTTON_STATE_DOWN) == BUTTON_STATE_DOWN ) {
delta_y = 1;
delta_x = 0;
}
}
void display_score()
{
uint8_t integerValues[] = {
CHAR_7SEG_0,
CHAR_7SEG_1,
CHAR_7SEG_2,
CHAR_7SEG_3,
CHAR_7SEG_4,
CHAR_7SEG_5,
CHAR_7SEG_6,
CHAR_7SEG_7,
CHAR_7SEG_8,
CHAR_7SEG_9,
};
uint8_t score_cur = 0;
int rc = 0;
if ( difficulty > 0 ) {
gamespeed = 1000 / difficulty;
} else {
gamespeed = 1000;
}
score_cur = (score % 10);
select7SegDisplay(PIN_7SEGMENT_DIGIT1);
rc = writeIC74HC595Char(&scoreboard, integerValues[score_cur], MSBFIRST);
if ( rc != ERRNO_SUCCESS ) {
Serial.printf("Failed to write scoreboard: %d", errno);
return;
}
score_cur = (score / 10);
select7SegDisplay(PIN_7SEGMENT_DIGIT2);
rc = writeIC74HC595Char(&scoreboard, integerValues[score_cur], MSBFIRST);
if ( rc != ERRNO_SUCCESS ) {
Serial.printf("Failed to write scoreboard: %d", errno);
return;
}
select7SegDisplay(PIN_7SEGMENT_DIGIT3);
rc = writeIC74HC595Char(&scoreboard, 0x00, MSBFIRST);
if ( rc != ERRNO_SUCCESS ) {
Serial.printf("Failed to write scoreboard: %d", errno);
return;
}
select7SegDisplay(PIN_7SEGMENT_DIGIT4);
rc = writeIC74HC595Char(&scoreboard, integerValues[difficulty], MSBFIRST);
if ( rc != ERRNO_SUCCESS ) {
Serial.printf("Failed to write scoreboard: %d", errno);
return;
}
}
void gamelogic()
{
uint32_t updatetime = millis();
found_food = 0;
if ( (updatetime - lastupdate) > gamespeed ) {
Serial.printf("Updating game logic. Difficulty %d (%d elapsed / gamespeed %d). Player <%d, %d>.\n", difficulty, (updatetime - lastupdate), gamespeed, player_x, player_y);
lastupdate = updatetime;
// Skip all of this if we have no delta (the player isn't playing yet)
if ( (delta_x + delta_y) != 0 ) {
move_player();
collide_player();
// Add a snake entity to the new location
if ( found_food == 1 ) {
tail_length += 1;
place_random_entity(ENTITY_HIGH_MASK | ENTITY_ALIVE | ENTITY_FOOD, 0);
}
entities[(player_x * 8) + player_y] = ((tail_length << 4) | ENTITY_ALIVE | ENTITY_SNAKE );
}
update_matrix_entities();
found_food = 0;
}
}
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);
}
}
if ( init74HC595(&matrixcols) != ERRNO_SUCCESS ) {
Serial.printf("failed to initialize matrix series 74HC595");
}
if ( initButton(&btn_up) != ERRNO_SUCCESS ||
initButton(&btn_left) != ERRNO_SUCCESS ||
initButton(&btn_right) != ERRNO_SUCCESS ||
initButton(&btn_down) != ERRNO_SUCCESS ) {
Serial.printf("Failed to initialize buttons\n");
}
if ( init74HC595(&scoreboard) != ERRNO_SUCCESS ) {
Serial.printf("Failed to initialize scoreboard 74HC595");
}
if ( init7Segment() != ERRNO_SUCCESS ) {
Serial.printf("Failed to initialize 7 segment display");
}
initBuzzer();
init_game();
Serial.printf("\nSETUP READY\n");
}
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);
}
}
int rc = 0;
read_controls();
display_score();
gamelogic();
draw_entities();
}

View File

@@ -0,0 +1,42 @@
#include <Arduino.h>
#include "7Segment.h"
#include "IC74HC595.h"
#include "error.h"
int init7Segment() {
pinMode(PIN_7SEGMENT_DIGIT1, OUTPUT);
pinMode(PIN_7SEGMENT_DIGIT2, OUTPUT);
pinMode(PIN_7SEGMENT_DIGIT3, OUTPUT);
pinMode(PIN_7SEGMENT_DIGIT4, OUTPUT);
return ERRNO_SUCCESS;
}
void test7Segment()
{
uint8_t segments[] = {
PIN_7SEGMENT_DIGIT1,
PIN_7SEGMENT_DIGIT2,
PIN_7SEGMENT_DIGIT3,
PIN_7SEGMENT_DIGIT4
};
for ( int j = 0; j < 4 ; j++ ) {
select7SegDisplay(segments[j]);
for ( int i = 0 ; i < 8; i++ ) {
if ( i > 0 ) {
writeIC74HC595Char(&scoreboard, ((uint8_t) ~(SEG_DP | (1 << i))), MSBFIRST);
} else {
writeIC74HC595Char(&scoreboard, ((uint8_t) ~(SEG_DP | 1)), MSBFIRST);
}
delay(1000);
}
}
}
int select7SegDisplay(uint8_t pin)
{
digitalWrite(PIN_7SEGMENT_DIGIT1, LOW);
digitalWrite(PIN_7SEGMENT_DIGIT2, LOW);
digitalWrite(PIN_7SEGMENT_DIGIT3, LOW);
digitalWrite(PIN_7SEGMENT_DIGIT4, LOW);
digitalWrite(pin, HIGH);
return ERRNO_SUCCESS;
}

View File

@@ -0,0 +1,67 @@
#ifndef _7SEGMENT_H_
#define _7SEGMENT_H_
#define SEG_A 1
#define SEG_B 1 << 1
#define SEG_C 1 << 2
#define SEG_D 1 << 3
#define SEG_E 1 << 4
#define SEG_F 1 << 5
#define SEG_G 1 << 6
#define SEG_DP 1 << 7
// 3, 5, A, F don't properly handle segment D (bottom). Not sure why.
// Other digits that include the bottom segment (like 2, 0, 8) and that don't include it
// (like 7, 9, 4) work fine.
#define CHAR_7SEG_0 (uint8_t) ~(SEG_DP | SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F)
#define CHAR_7SEG_1 (uint8_t) ~(SEG_DP | SEG_B | SEG_C)
#define CHAR_7SEG_2 (uint8_t) ~(SEG_DP | SEG_A | SEG_B | SEG_G | SEG_E | SEG_D)
#define CHAR_7SEG_3 (uint8_t) ~(SEG_DP | SEG_A | SEG_B | SEG_C | SEG_D | SEG_G )
#define CHAR_7SEG_4 (uint8_t) ~(SEG_DP | SEG_F | SEG_G | SEG_B | SEG_C)
#define CHAR_7SEG_5 (uint8_t) ~(SEG_DP | SEG_A | SEG_F | SEG_G | SEG_C | SEG_D)
#define CHAR_7SEG_6 (uint8_t) ~(SEG_DP | SEG_A | SEG_F | SEG_E | SEG_D | SEG_C | SEG_G)
#define CHAR_7SEG_7 (uint8_t) ~(SEG_DP | SEG_A | SEG_B | SEG_C)
#define CHAR_7SEG_8 (uint8_t) ~(SEG_DP | SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G)
#define CHAR_7SEG_9 (uint8_t) ~(SEG_DP | SEG_A | SEG_B | SEG_C | SEG_F | SEG_G)
#define CHAR_7SEG_A (uint8_t) ~(SEG_DP | SEG_A | SEG_B | SEG_C | SEG_E | SEG_F | SEG_G)
#define CHAR_7SEG_B (uint8_t) ~(SEG_DP | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G)
#define CHAR_7SEG_C (uint8_t) ~(SEG_DP | SEG_A | SEG_D | SEG_E | SEG_F)
#define CHAR_7SEG_D (uint8_t) ~(SEG_DP | SEG_C | SEG_D | SEG_E | SEG_B | SEG_G)
#define CHAR_7SEG_E (uint8_t) ~(SEG_DP | SEG_A | SEG_D | SEG_E | SEG_F | SEG_G)
#define CHAR_7SEG_F (uint8_t) ~(SEG_DP | SEG_A | SEG_E | SEG_F | SEG_G)
#define CHAR_7SEG_L (uint8_t) ~(SEG_D | SEG_E | SEG_F)
#define CHAR_7SEG_DP (uint8_t) ~SEG_DP
/* #define CHAR_7SEG_0 0xC0 // 0b11000000 */
/* #define CHAR_7SEG_1 0xF9 // 0b11111001 */
/* #define CHAR_7SEG_2 0xA4 // 0b10100100 */
/* #define CHAR_7SEG_3 0xB0 // 0b10110000 */
/* #define CHAR_7SEG_4 0x99 // 0b10011001 */
/* #define CHAR_7SEG_5 0x92 // 0b10010010 */
/* #define CHAR_7SEG_6 0x82 // 0b10000010 */
/* #define CHAR_7SEG_7 0xF8 // 0b11111000 */
/* #define CHAR_7SEG_8 0x80 // 0b10000000 */
/* #define CHAR_7SEG_9 0x90 // 0b10010000 */
/* #define CHAR_7SEG_A 0x88 // 0b10001000 */
/* #define CHAR_7SEG_B 0x83 // 0b10000011 */
/* #define CHAR_7SEG_C 0xC6 // 0b11000110 */
/* #define CHAR_7SEG_D 0xA1 // 0b10100001 */
/* #define CHAR_7SEG_E 0x86 // 0b10000110 */
/* #define CHAR_7SEG_F 0x8E // 0b10001110 */
/* #define CHAR_7SEG_DP 0x80 // 0b10000000 */
/* #define CHAR_7SEG_OFF 0x00 // 0b00000000 */
#define PIN_7SEGMENT_DIGIT1 7
#define PIN_7SEGMENT_DIGIT2 6
#define PIN_7SEGMENT_DIGIT3 5
#define PIN_7SEGMENT_DIGIT4 4
int init7Segment(void);
void test7Segment(void);
int select7SegDisplay(uint8_t num);
#endif // _7SEGMENT_H_

View File

@@ -0,0 +1,21 @@
#include <Arduino.h>
#include "IC74HC595.h"
#include "error.h"
IC74HC595 matrixcols = {PIN_74HC595_MATRIX_UPDATE, PIN_74HC595_MATRIX_DATA, PIN_74HC595_MATRIX_CLOCK};
IC74HC595 scoreboard = {PIN_74HC595_SCOREBOARD_UPDATE, PIN_74HC595_SCOREBOARD_DATA, PIN_74HC595_SCOREBOARD_CLOCK};
int init74HC595(IC74HC595 *sr) {
pinMode(sr->updatepin, OUTPUT);
pinMode(sr->clockpin, OUTPUT);
pinMode(sr->datapin, OUTPUT);
return ERRNO_SUCCESS;
}
int writeIC74HC595Char(IC74HC595 *sr, uint8_t value, uint8_t order)
{
digitalWrite(sr->updatepin, LOW);
shiftOut(sr->datapin, sr->clockpin, order, value);
digitalWrite(sr->updatepin, HIGH);
return ERRNO_SUCCESS;
}

View File

@@ -0,0 +1,28 @@
#ifndef _IC74HC595_H_
#define _IC74HC595_H_
// 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_SCOREBOARD_CLOCK 11
#define PIN_74HC595_MATRIX_CLOCK 12
#define PIN_74HC595_MATRIX_UPDATE 8
#define PIN_74HC595_SCOREBOARD_UPDATE 17
#define PIN_74HC595_MATRIX_DATA 18
#define PIN_74HC595_SCOREBOARD_DATA 14
typedef struct IC74HC595 {
uint8_t updatepin;
uint8_t datapin;
uint8_t clockpin;
} IC74HC595;
extern IC74HC595 matrixcols;
extern IC74HC595 scoreboard;
int init74HC595(IC74HC595 *sr);
int writeIC74HC595Char(IC74HC595 *sr, uint8_t value, uint8_t order);
#endif // _IC74HC595_H_

View File

@@ -5,7 +5,7 @@ This project uses 3 74HC595 serial to parallel shift registers, a 7-segment disp
# Lessons Learned
* Compiling, uploading, and debugging Arduino code from the CLI
* Managing mutexes in Arduino to handle thread locking
* 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
@@ -100,94 +100,61 @@ Monitor port settings:
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:
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. 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
```
Now you can connect gdb to it. Right?
<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.
Not really.
I'm sure there won't ever be any problems at all from mixing and matching like this.
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.
So I had to ...
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.
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
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.
... And it works
## More breadboard bugs
```
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'
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.
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]
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.
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)
```
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.
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.
## Joysticks and ADC and wierd outputs
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.
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.
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.
<center><img alt="Something's fucky" src="somethingsfucky.jpg" width="320px" /></center>
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
* Getting the Arduino code to compile the additional .c file and include the header
* 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

View File

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

View File

@@ -4,151 +4,61 @@
#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;
pinMode(button->pin, INPUT);
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);
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;
}
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;
}
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 readDifficulty()
{
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;
analogRead(PIN_DIFFICULTY);
delay(1);
uint32_t setting = analogRead(PIN_DIFFICULTY);
uint32_t divisor = 4096/9;
return setting/divisor;
}

View File

@@ -4,8 +4,14 @@
#include <Arduino.h>
#include <stdint.h>
#define PIN_BUTTON_LEFT 1 // Red
#define PIN_BUTTON_UP 2 // Green
#define PIN_BUTTON_DOWN 47 // Yellow
#define PIN_BUTTON_RIGHT 13 // Blue
#define PIN_DIFFICULTY 10
#define BUTTON_STABLETIME 100
#define JOYSTICK_STABLETIME 3000
// Button states
#define BUTTON_STATE_BOUNCING (1 << 0)
@@ -13,60 +19,22 @@
#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;
// 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);
uint32_t readDifficulty();
#endif // _JOYSTICK_H_

View File

@@ -1,8 +1,5 @@
#!/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) &
(cd ~/.arduino15/packages/esp32/tools/openocd-esp32/v0.12.0-esp32-20251215/share/openocd/scripts/ ; ../../../bin/openocd -f board/esp32s3-builtin.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

@@ -6,6 +6,7 @@
#define ERRNO_IO 2
#define ERRNO_TIMEOUT 3
#define ERRNO_CALIBRATION 4
#define ERRNO_OUTOFBOUNDS 5
#define ERROR(x) errno=x; return x;

View File

@@ -0,0 +1,57 @@
#include <Arduino.h>
#include <stdint.h>
#include "error.h"
#include "IC74HC595.h"
#include "matrix.h"
uint8_t matrixcolumns[8] = {0};
int pokeMatrixPixel(uint8_t x, uint8_t y)
{
if (x > 7 || y > 7) {
ERROR(ERRNO_OUTOFBOUNDS);
}
//Serial.printf("Poking (%d, %d)\n", x, y);
// Because of the alignment of the screen in our demo, we treat columns as the X axis
matrixcolumns[x] = matrixcolumns[x] | (1 << y);
return ERRNO_SUCCESS;
}
void resetMatrix()
{
matrixcolumns[0] = 0;
matrixcolumns[1] = 0;
matrixcolumns[2] = 0;
matrixcolumns[3] = 0;
matrixcolumns[4] = 0;
matrixcolumns[5] = 0;
matrixcolumns[6] = 0;
matrixcolumns[7] = 0;
}
void displayMatrix()
{
for (int i = 0; i < 8; i++) {
writeLEDMatrix(matrixcolumns[i], ~(1 << i));
}
}
void writeLEDMatrix(uint8_t row, uint8_t col)
{
digitalWrite(matrixcols.updatepin, LOW);
shiftOut(matrixcols.datapin, matrixcols.clockpin, LSBFIRST, row);
shiftOut(matrixcols.datapin, matrixcols.clockpin, MSBFIRST, col);
digitalWrite(matrixcols.updatepin, HIGH);
}
void testMatrix()
{
uint8_t row = 0;
uint8_t col = 0;
for ( int i = 0; i < 8; i++ ) {
row = (1 << i);
col = ~(1 << i);
Serial.printf("%d = (%d, %d)\n", i, row, col);
writeLEDMatrix(row, col);
}
};

12
08-74HC595-Snake/matrix.h Normal file
View File

@@ -0,0 +1,12 @@
#ifndef _MATRIX_H_
#define _MATRIX_H_
#include <stdint.h>
int pokeMatrixPixel(uint8_t x, uint8_t y);
void resetMatrix();
void displayMatrix();
void writeLEDMatrix(uint8_t row, uint8_t col);
void testMatrix();
#endif // _MATRIX_H_

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB