Compare commits
4 Commits
07-LM339N-
...
8e95e8d94f
| Author | SHA1 | Date | |
|---|---|---|---|
|
8e95e8d94f
|
|||
|
414f0684ab
|
|||
|
8303ea9d10
|
|||
|
200156c432
|
BIN
06-joystick/joystickdemo.fzz
Normal file
BIN
06-joystick/joystickdemo.fzz
Normal file
Binary file not shown.
140
07-LM339N-ADC/README.md
Normal file
140
07-LM339N-ADC/README.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Breadboard
|
||||
|
||||

|
||||
|
||||
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).
|
||||
BIN
07-LM339N-ADC/breadboard.png
Normal file
BIN
07-LM339N-ADC/breadboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 589 KiB |
BIN
07-LM339N-ADC/butwhy.webp
Normal file
BIN
07-LM339N-ADC/butwhy.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
07-LM339N-ADC/extrasteps.jpeg
Normal file
BIN
07-LM339N-ADC/extrasteps.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
BIN
07-LM339N-ADC/lm339nblock.png
Normal file
BIN
07-LM339N-ADC/lm339nblock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
349
08-74HC595-Snake/08-74HC595-Snake.ino
Normal file
349
08-74HC595-Snake/08-74HC595-Snake.ino
Normal file
@@ -0,0 +1,349 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "controls.h"
|
||||
#include "error.h"
|
||||
#include "IC74HC595.h"
|
||||
#include "7Segment.h"
|
||||
#include "matrix.h"
|
||||
|
||||
#define PIN_BUZZER 21
|
||||
|
||||
#define PWM_FREQUENCY 1000
|
||||
#define PWM_BITWIDTH 12
|
||||
#define PWM_BUZZER_CHANNEL 0
|
||||
|
||||
#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;
|
||||
score += 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() {
|
||||
initSerial();
|
||||
|
||||
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() {
|
||||
int rc = 0;
|
||||
read_controls();
|
||||
display_score();
|
||||
gamelogic();
|
||||
draw_entities();
|
||||
}
|
||||
42
08-74HC595-Snake/7Segment.cpp
Normal file
42
08-74HC595-Snake/7Segment.cpp
Normal 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;
|
||||
}
|
||||
67
08-74HC595-Snake/7Segment.h
Normal file
67
08-74HC595-Snake/7Segment.h
Normal 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_
|
||||
21
08-74HC595-Snake/IC74HC595.cpp
Normal file
21
08-74HC595-Snake/IC74HC595.cpp
Normal 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;
|
||||
}
|
||||
28
08-74HC595-Snake/IC74HC595.h
Normal file
28
08-74HC595-Snake/IC74HC595.h
Normal 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_
|
||||
185
08-74HC595-Snake/README.md
Normal file
185
08-74HC595-Snake/README.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# 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 (later replaced with 4 buttons), a handful of resistors and a potentiometer to make a playable Snake game.
|
||||
|
||||
This was easily the most challenging project so far, taking me well over a week to get it working right.
|
||||
|
||||
# Lessons Learned
|
||||
|
||||
* The Arduino IDE debugger really sucks
|
||||
* Compiling, uploading, and debugging Arduino code from the CLI
|
||||
* Breadboards probably introduce just as many bugs as I do
|
||||
* How to drive a 7-segment display
|
||||
* How to drive an 8x8 LED matrix
|
||||
* Embedded platforms have undocumented limits that may surprise you
|
||||
* 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. 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?
|
||||
|
||||
<center><img alt="This is where we would run the debugger, if we had one" src="nodebugger.jpeg" width="320px"/></center>
|
||||
|
||||
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.
|
||||
|
||||
<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.
|
||||
|
||||
## Arduino IDE debugger woes and ESP32 limits
|
||||
|
||||
I spent about an hour struggling with a `Stack smashing protection` fault. Try as I might, I couldn't get the Arduino IDE debugger to show me where the actual crash was in my code. All I was getting was a fault with some addresses in memory for a backtrace. Sure, I could objdump the binary, figure out the addresses, and figure it out that way. But if I'm gonna do that, WTF is the point of having a GUI debugger? There are some GUI tools for decoding ESP32 stacktraces, but the Arduino IDE integration for them hasn't worked in at least one whole major version, and the newer versions want to run in some IDE I don't want to use.
|
||||
|
||||
I wound up figuring it out essentially through trial and error. The `place_random_entity()` function uses an array of 64 bytes to store references to entries on the LED matrix where there are currently no actors. I had originally allocated that inside of the function itself - on the stack. Apparently this was the *wrong* place to put it. Moving this to a global variable resolved my problem. I have to assume that the default stack size was something ridiculously small, even though the scant documentation I could find said it would be something like 8Kb.
|
||||
|
||||
I'm sure there was a more elegant solution than just moving it in to a global variable. But at this point, 9 days in to the project, lol bitch please, you're getting a global variable and we're making this damn thing run.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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)
|
||||
4
08-74HC595-Snake/build.sh
Normal file
4
08-74HC595-Snake/build.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
arduino-cli compile --clean --optimize-for-debug --fqbn esp32:esp32:esp32s3:CDCOnBoot=cdc --output-dir build/ .
|
||||
|
||||
64
08-74HC595-Snake/controls.cpp
Normal file
64
08-74HC595-Snake/controls.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#include <Arduino.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "controls.h"
|
||||
#include "error.h"
|
||||
|
||||
// For initializing simple buttons
|
||||
int initButton(Button *button)
|
||||
{
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
uint32_t readDifficulty()
|
||||
{
|
||||
analogRead(PIN_DIFFICULTY);
|
||||
delay(1);
|
||||
uint32_t setting = analogRead(PIN_DIFFICULTY);
|
||||
uint32_t divisor = 4096/9;
|
||||
return setting/divisor;
|
||||
}
|
||||
40
08-74HC595-Snake/controls.h
Normal file
40
08-74HC595-Snake/controls.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#ifndef _JOYSTICK_H_
|
||||
#define _JOYSTICK_H_
|
||||
|
||||
#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
|
||||
|
||||
// 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)
|
||||
|
||||
|
||||
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;
|
||||
|
||||
int initButton(Button *button);
|
||||
int readButton(Button *button);
|
||||
uint32_t readDifficulty();
|
||||
|
||||
#endif // _JOYSTICK_H_
|
||||
BIN
08-74HC595-Snake/datasheets/3461BS.pdf
Normal file
BIN
08-74HC595-Snake/datasheets/3461BS.pdf
Normal file
Binary file not shown.
BIN
08-74HC595-Snake/datasheets/5161bs.pdf
Normal file
BIN
08-74HC595-Snake/datasheets/5161bs.pdf
Normal file
Binary file not shown.
BIN
08-74HC595-Snake/datasheets/sn74hc595.pdf
Normal file
BIN
08-74HC595-Snake/datasheets/sn74hc595.pdf
Normal file
Binary file not shown.
Binary file not shown.
5
08-74HC595-Snake/debug.sh
Normal file
5
08-74HC595-Snake/debug.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
(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
|
||||
1
08-74HC595-Snake/error.cpp
Normal file
1
08-74HC595-Snake/error.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "error.h"
|
||||
15
08-74HC595-Snake/error.h
Normal file
15
08-74HC595-Snake/error.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#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 ERRNO_OUTOFBOUNDS 5
|
||||
|
||||
#define ERROR(x) errno=x; return x;
|
||||
|
||||
extern int errno;
|
||||
|
||||
#endif // _ERROR_H_
|
||||
57
08-74HC595-Snake/matrix.cpp
Normal file
57
08-74HC595-Snake/matrix.cpp
Normal 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
12
08-74HC595-Snake/matrix.h
Normal 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_
|
||||
3
08-74HC595-Snake/monitor.sh
Normal file
3
08-74HC595-Snake/monitor.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
arduino-cli monitor -p /dev/ttyACM0
|
||||
BIN
08-74HC595-Snake/nodebugger.jpeg
Normal file
BIN
08-74HC595-Snake/nodebugger.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
2
08-74HC595-Snake/sketch.yaml
Normal file
2
08-74HC595-Snake/sketch.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
default_fqbn: esp32:esp32:esp32s3
|
||||
default_port: serial:///dev/ttyACM0
|
||||
BIN
08-74HC595-Snake/somethingsfucky.jpg
Normal file
BIN
08-74HC595-Snake/somethingsfucky.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
3
08-74HC595-Snake/upload.sh
Normal file
3
08-74HC595-Snake/upload.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
arduino-cli upload -p /dev/ttyACM0 -b esp32:esp32:esp32s3:CDCOnBoot=cdc --build-path build/
|
||||
Reference in New Issue
Block a user