Embedded Programming
Two weeks ago I already programmed a binary number calculator with my 1-button 1-LED board. (You can read about it here or watch the demo video). But there's only so much fun you can have with only one button and one LED, so this week I wanted to sextuple the fun by making a new board with 6 buttons and 6 LED's!
I programmed it to be an “arcade” game board on which you can play 6 different mini-games. You can scroll to the (near) bottom of the page to watch the video demos (also linked here: 0,1,2,3,4,5,6). You can read the manual below about how to play (click for PDF):
The full code (for Arduino IDE) can be downloaded here or viewed below:
/*******************************************************
* SAMD21 ARCADE BOARD - 6 GAMES IN 1 *
* Lingdong Huang 2021 *
*******************************************************/
#define N 6
const int LEDS[N] = {7, 6, 5, 4, 3, 2 };
const int BTNS[N] = {10,11,16,17,18,19};
int BTN_CURR[N] = {HIGH,HIGH,HIGH,HIGH,HIGH,HIGH};
int BTN_PREV[N] = {HIGH,HIGH,HIGH,HIGH,HIGH,HIGH};
const int DANCE_SEQ_L = 29;
const bool DANCE_SEQ[DANCE_SEQ_L*N] = {
0,0,0,0,1,1,
0,0,0,1,1,0,
0,0,1,1,0,0,
0,1,1,0,0,0,
1,1,0,0,0,0,
0,1,1,0,0,0,
0,0,1,1,0,0,
0,0,0,1,1,0,
0,0,0,0,1,1,
0,0,0,1,1,0,
0,0,1,1,0,0,
0,1,0,0,1,0,
1,0,0,0,0,1,
0,1,0,0,1,0,
0,0,1,1,0,0,
0,1,1,1,1,0,
1,1,1,1,1,1,
1,0,1,0,1,0,
0,1,0,1,0,1,
1,0,1,0,1,0,
0,1,0,1,0,1,
1,0,1,0,1,0,
1,0,0,0,0,1,
0,1,0,0,1,0,
0,0,1,1,0,0,
0,1,0,0,1,0,
1,0,0,0,0,1,
1,1,0,0,1,1,
1,1,1,1,1,1
};
int mode = -1;
/*******************************************************
* Utils *
*******************************************************/
void dance(){
for (int i = 0; i < DANCE_SEQ_L; i++){
for (int j = 0; j < N; j++){
digitalWrite(LEDS[j], DANCE_SEQ[i*N+j] ? HIGH: LOW);
}
delay(100);
}
}
void flash_screen(){
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
delay(60);
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],HIGH);
delay(60);
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
delay(60);
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],HIGH);
delay(60);
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
delay(60);
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],HIGH);
delay(60);
}
void reveal_screen(int idx){
for (int i = 0; i < N; i++){
digitalWrite(LEDS[i],i == idx ? HIGH : LOW);
}
delay(50);
for (int i = 1; i < N; i++){
int a = idx-i;
int b = idx+i;
if (0 <= a && a < N) digitalWrite(LEDS[a],HIGH);
if (0 <= b && b < N) digitalWrite(LEDS[b],HIGH);
delay(50);
}
}
void blink_led(int i){
digitalWrite(LEDS[i],LOW);
delay(60);
digitalWrite(LEDS[i],HIGH);
delay(60);
digitalWrite(LEDS[i],LOW);
delay(60);
digitalWrite(LEDS[i],HIGH);
delay(60);
digitalWrite(LEDS[i],LOW);
delay(60);
digitalWrite(LEDS[i],HIGH);
delay(60);
}
void game_over(){
flash_screen();
mode = -1;
delay(500);
gsel_init();
}
void btn_flip(bool dir, int idx){
if (mode == -1){
gsel_input(dir,idx);
}else if (mode == 0){
pong_input(dir,idx);
}else if (mode == 1){
whac_input(dir,idx);
}else if (mode == 2){
simo_input(dir,idx);
}else if (mode == 3){
croc_input(dir,idx);
}else if (mode == 4){
echo_input(dir,idx);
}else if (mode == 5){
prgm_input(dir,idx);
}
}
void btn_update(){
for (int i = 0; i < N; i++){
BTN_CURR[i] = digitalRead(BTNS[i]);
if (BTN_CURR[i] == LOW && BTN_PREV[i] == HIGH){
btn_flip(true,i);
}
if (BTN_CURR[i] == HIGH && BTN_PREV[i] == LOW){
btn_flip(false,i);
}
BTN_PREV[i] = BTN_CURR[i];
}
}
/*******************************************************
* Game Selection *
*******************************************************/
void gsel_init(){
}
void gsel_update(){
for (int i = 0; i < N; i++){
digitalWrite(LEDS[i],HIGH);
}
}
void gsel_input(bool dir, int idx){
if (dir){
for (int i = 0; i < N; i++){
digitalWrite(LEDS[i],i == idx ? HIGH : LOW);
}
delay(100);
// flash_screen();
reveal_screen(idx);
delay(500);
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
mode = idx;
if (mode == 0){
pong_init();
}else if (mode == 1){
whac_init();
}else if (mode == 2){
simo_init();
}else if (mode == 3){
croc_init();
}else if (mode == 4){
echo_init();
}else if (mode == 5){
prgm_init();
}else{
mode = -1;
}
}
}
/*******************************************************
* 1D-Pong *
*******************************************************/
float ball_x;
float ball_v;
const float ball_a = -1.05;
int ball_xi;
int pad_a;
int pad_b;
const int pad_max_hold = 50;
const float ball_max_v = 1.0;
void pong_init(){
ball_x = N-2;
ball_v = -0.01;
pad_a = 0;
pad_b = 0;
}
void pong_update(){
ball_x += ball_v;
if (ball_x < 1.5 && ball_v < 0 && pad_a){
ball_v *= ball_a;
}else if (ball_x > N-2.5 && ball_v > 0 && pad_b){
ball_v *= ball_a;
}
if (abs(ball_v) > ball_max_v){
ball_v = copysign(ball_max_v,ball_v);
}
ball_xi = roundf(ball_x);
if (ball_xi < 0 || ball_xi >= N){
game_over();
}
for (int i = 0; i < N; i++){
if (i == ball_xi){
digitalWrite(LEDS[i],HIGH);
}else{
digitalWrite(LEDS[i],LOW);
}
}
if (ball_xi != 0) digitalWrite(LEDS[0 ],pad_a?HIGH:LOW);
if (ball_xi != N-1) digitalWrite(LEDS[N-1],pad_b?HIGH:LOW);
if (pad_a) pad_a ++;
if (pad_b) pad_b ++;
if (pad_a > pad_max_hold) pad_a = 0;
if (pad_b > pad_max_hold) pad_b = 0;
delay(10);
}
void pong_input(bool dir, int idx){
if (idx == 0){
pad_a = dir ? 1 : 0;
}else if (idx == N-1){
pad_b = dir ? 1 : 0;
}
}
/*******************************************************
* Whac-an-LED *
*******************************************************/
int mole_i;
int timeout;
int timeout_max;
const int timeout_min = 10;
void whac_init(){
mole_i = rand()%N;
timeout_max = 200;
timeout = timeout_max;
}
void whac_update(){
for (int i = 0; i < N; i++){
if (i == mole_i){
digitalWrite(LEDS[i],HIGH);
}else{
digitalWrite(LEDS[i],LOW);
}
}
timeout --;
if (timeout == 0){
game_over();
}
delay(10);
}
void whac_input(bool dir, int idx){
if (dir){
if (idx == mole_i){
int i;
do{
i = rand()%N;
}while (i == mole_i);
mole_i = i;
timeout_max -= 2;
if (timeout_max < timeout_min) timeout_max = timeout_min;
timeout = timeout_max;
}else{
game_over();
}
}
}
/*******************************************************
* Simon *
*******************************************************/
const int pattern_max_l = 64;
int pattern[pattern_max_l] = {0};
int pattern_l;
int simo_state;
int simo_idx;
unsigned long last_press_time;
int last_press_btn;
void simo_init(){
for (int i = 0; i < pattern_max_l; i++){
pattern[i]= rand()%N;
}
pattern_l = 1;
simo_idx = 0;
simo_state = 0;
last_press_time = millis();
last_press_btn = -1;
}
void simo_update(){
if (simo_state == 0){
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
for (int i = 0; i < pattern_l; i++){
delay(400);
digitalWrite(LEDS[pattern[i]], HIGH);
delay(500);
digitalWrite(LEDS[pattern[i]], LOW);
}
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
simo_state = 1;
}else{
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],BTN_CURR[i] ? LOW : HIGH);
}
}
void simo_input(bool dir, int idx){
if (simo_state == 0){
return;
}
if (last_press_time - millis() < 200 && last_press_btn == idx){
return;
}
last_press_time = millis();
if (dir){
if (idx != pattern[simo_idx]){
blink_led(pattern[simo_idx]);
game_over();
}
simo_idx ++;
if (simo_idx >= pattern_l){
reveal_screen(idx);
delay(500);
simo_idx = 0;
pattern_l ++;
if (pattern_l > pattern_max_l) pattern_l = pattern_max_l;
simo_state = 0;
}
}
}
/*******************************************************
* Croc Dentist *
*******************************************************/
int tooth;
int teeth[N] = {0};
void croc_init(){
tooth = rand()%N;
for (int i = 0; i < N; i++){
digitalWrite(LEDS[i],HIGH);
teeth[i] = 0;
}
}
void croc_update(){
for (int i = 0; i < N; i++){
digitalWrite(LEDS[i], teeth[i] ? LOW : HIGH);
}
}
void croc_input(int dir, int idx){
if (dir){
if (idx == tooth){
flash_screen();
game_over();
}else{
teeth[idx] = 1;
}
}
}
/*******************************************************
* Echo *
*******************************************************/
typedef struct _b_event_t{
char idx;
bool dir;
unsigned int t;
} b_event_t;
const int max_events = 1024;
int n_events;
b_event_t events[max_events] = {0};
int echo_mode;
unsigned int echo_start_t;
int playhead;
int first_press_time;
void echo_init(){
n_events = 0;
echo_mode = 0;
playhead = 0;
last_press_time = millis();
first_press_time = -1;
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
}
void echo_update(){
if (echo_mode == 0){
for (int i = 0; i < N; i++){
digitalWrite(LEDS[i], BTN_CURR[i] ? LOW : HIGH);
}
if (millis() - last_press_time > 3000 && n_events){
flash_screen();
flash_screen();
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
echo_mode = 1;
echo_start_t = millis();
}
}else{
unsigned int t = millis() - echo_start_t;
while (events[playhead].t < t){
digitalWrite(LEDS[events[playhead].idx], events[playhead].dir ? HIGH : LOW);
playhead ++;
if (playhead >= n_events){
playhead = 0;
echo_start_t = millis();
break;
}
}
}
delay(10);
}
void echo_input(int dir, int idx){
if (echo_mode == 1){
game_over();
return;
}
if (n_events >= max_events){
return;
}
if (first_press_time < 0){
first_press_time = millis();
}
last_press_time = millis();
events[n_events].idx = idx;
events[n_events].dir = dir;
events[n_events].t = millis()-first_press_time;
n_events++;
}
/*******************************************************
* Interpreter (Brainfuck variant) *
*******************************************************/
const int max_prgm_l = 4096;
const int max_mem = 256;
char prgm[max_prgm_l] = {0};
char pmem[max_mem] = {0};
int prgm_mode;
int prgm_len = 0;
int code_ptr;
int cell_ptr;
int shft_key;
#define CMD_NEXT 0
#define CMD_PREV 1
#define CMD_INCR 2
#define CMD_DECR 3
#define CMD_WRIT 4 //4+0
#define CMD_READ 5 //4+1
#define CMD_JMPL 6 //4+2
#define CMD_JMPR 7 //4+3
// run/exit 5
void prgm_init(){
prgm_mode = 0;
code_ptr = 0;
cell_ptr = 0;
prgm_len = 0;
shft_key = 0;
for (int i = 0; i < max_mem; i++) pmem[i] = 0;
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
}
void prgm_update(){
if (prgm_mode == 0){
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],BTN_CURR[i] ? LOW : HIGH);
}else{
if (prgm[code_ptr] == CMD_NEXT){
cell_ptr ++;
code_ptr ++;
}else if (prgm[code_ptr] == CMD_PREV){
cell_ptr --;
code_ptr ++;
}else if (prgm[code_ptr] == CMD_INCR){
pmem[cell_ptr] ++;
code_ptr ++;
}else if (prgm[code_ptr] == CMD_DECR){
pmem[cell_ptr] --;
code_ptr ++;
}else if (prgm[code_ptr] == CMD_WRIT){
for (int i = 0; i < N; i++){
digitalWrite( LEDS[i], ((pmem[cell_ptr]>>i) & 1 ) ? HIGH : LOW );
}
code_ptr ++;
}else if (prgm[code_ptr] == CMD_READ){
int x = 0;
for (int i = N-1; i>=0; i--){
x = (x << 1) | (BTN_CURR[i] == HIGH ? 0 : 1);
}
pmem[cell_ptr] = x;
code_ptr ++;
}else if (prgm[code_ptr] == CMD_JMPL){
if (pmem[cell_ptr]){
code_ptr++;
}else{
code_ptr ++;
int lvl = 0;
while (1){
if (prgm[code_ptr] == CMD_JMPL){
lvl++;
}
if (prgm[code_ptr] == CMD_JMPR){
if (lvl == 0){
break;
}
lvl--;
}
code_ptr ++;
}
code_ptr ++;
}
}else if (prgm[code_ptr] == CMD_JMPR){
if (pmem[cell_ptr]){
int lvl = 0;
code_ptr --;
while (1){
if (prgm[code_ptr] == CMD_JMPR){
lvl++;
}
if (prgm[code_ptr] == CMD_JMPL){
if (lvl == 0){
break;
}
lvl--;
}
code_ptr --;
}
code_ptr ++;
}else{
code_ptr ++;
}
}
if (code_ptr >= prgm_len){
delay(4000);
game_over();
}
}
delay(10);
}
void prgm_input(int dir, int idx){
if (dir){
if (prgm_mode == 0){
if (idx == 5){
flash_screen();
prgm_mode = 1;
return;
}else if (idx == 4){
shft_key = 1;
}else{
prgm[prgm_len]=(char)(idx + shft_key * 4);
prgm_len ++;
shft_key = 0;
}
}else{
if (idx == 5){
game_over();
}
}
}
}
/*******************************************************
* Main *
*******************************************************/
void setup() {
for (int i = 0; i < N; i++) pinMode(LEDS[i], OUTPUT);
for (int i = 0; i < N; i++) pinMode(BTNS[i], INPUT);
dance();
gsel_init();
}
void loop() {
btn_update();
if (mode == -1){
gsel_update();
}else if (mode == 0){
pong_update();
}else if (mode == 1){
whac_update();
}else if (mode == 2){
simo_update();
}else if (mode == 3){
croc_update();
}else if (mode == 4){
echo_update();
}else if (mode == 5){
prgm_update();
}
}
Design
Last time I used the SAMD11C microcontroller; It was very nice and straightforward, but didn't have too much storage or too many pins. I heard my classmates were having a lot of troubles with SAMD21E, so I thought I should upgrade and join the “fun”.
I referenced our TA Jake's helpful EAGLE tutorial when placing my schematics in KiCAD. There were a couple confusing moments since the KiCAD library has less detailed labels for the pins than EAGLE's, but now that I'm quite comfortable with the software I had everything figured out in no time.
Below is the first version of my schematics. You can see that I made a silly error. I wired all the LED's as if they're buttons, as I was distracted while working on them.
Last time I had to restart from scratch three times to come up a good layout. This time, the situation was almost sixfold more complicated but it only took me one attempt to get a near-perfect layout. I obviously became better through practice, but the main reason was that I used a trick: I first delibrately forgot about any design rules, and set the trace thickness to very skinny. I went crazy with throwing everything around. I didn't care if things were aligned or nicely packed -- I just wanted to solve the topological puzzle first. You can see below how disorderly I was doing it, but the puzzle was solved! I only need one single 0-ohm resistor.
Then I made a screenshot of the solution and deleted everything. Now in the second pass, I brought aesthetics into consideration, and actually try to produce a nice layout:
If you look closely near the microcontroller you can see I had to use another “cheat”. The pads of SAMD21E are very skinny and they're tightly spaced. If I were to use the recommended design rule (0.5mm trace, 0.5mm clearance), KiCAD wouldn't even let me connect any trace to the pads. What I did was I first changed the thickness and clearance to a smaller number, and drew a couple traces that “extend” the pads into open area around the microcontroller. Now that they are more spread out, I changed the design rule back to normal values, and connect the regular thicker traces to the pads extensions instead of the pads.
While admiring my routing “masterpiece” I suddenly realized my mistake with the LED's. I went back to the schematic and fixed it:
Luckily it was trivial to update the layout. However, it now looked less “cool”. I was proud of the cryptic way I initially ran traces beneath the current-limiting resistors for the LED's. Now this was no longer necessary.
Still, I felt pretty happy with the layout. Likely, in the eyes of professionals, this board is merely child's play, but I had a great sense of achievement since it was only my second.
I exported the traces and the outlines. I further edited the traces in photoshop, to make the pads of the microcontroller even skinnier, since I estimated that the 1/64 endmill would have a hard time going between them otherwise.
Fabrication
I designed the board on Wednesday night and went to the shop on Thursday to fabricate it.
It initially appeared that we were almost out of unused single-sided copper boards (I would later learn that there was a whole box of them right next to the drawer). So at the time I tried to be really thrifty and save boards for others, by squeezing my design onto a used board.
I failed the first time because I underestimated the largeness of my design: the top boarder was carved off-board and I had to redo the job. The second time I did more careful calculations and got the space jigsawing to work:
It took the Roland SRM-20 just over an hour to mill.
All the traces were articulated beautifully (probably thanks to my special treatment of the microcontroller pads), except for one small place where underneath the microcontroller, where two traces turn 45°, they touched adjacent traces because the turn was just a little bit too sharp. I took a razor and cleaned it up in a matter of seconds.
Then I went on to solder the SAMD21. This was when the nightmare began. The SAMD21's pins are so tiny, that even under the microscope, they look skinnier than mosquito legs, and in comparison, the solder and the iron look as thick as liverwurst sausages. There was no way I could accurately spread liverwurst onto mosquito legs!
If my hand shook even slightly, I would spread solder all over some three or four adjacent pins. And I have very shaky hands!
I asked Reina, who've accomplished the feat before for advice. She showed that instead of the usual way of feeding in the solder, one could flux the pads a lot, dip the tip of the iron in solder, and scrape it onto the pads in the fashion of reflowing.
That helped, but it still took me forever, and I was so miserable.
I managed to solder three of the four sides, when the worst moment happened. I accidentally smeared solder into the elevated section of two adjacent pins, and there was no way to get it out. The solder seeped into the narrow crevice. The iron was way too thick; the braid could not suck it out. I then tried using a razor the chop it with brute force. The razor blade barely fit in the crevice (you would think the razor is so sharp, but the separation between the pins is even narrower!). It finally worked, after a lot of struggling.
Then, only seconds later, the same accident happened to another two adjacent pins. When trying to fix it, I made even a larger mess, and now all four pins are glued together in a large blob of solder. I tried to use the razor blade again, but this time I screwed up too badly. After trying real hard, the pins started to become shaky, and it appeared that they're going to fall off!
As I was starting to lose my sanity, I turned to Reina for help. Using the same braid I had attempted to use, she quickly cleaned up the mess, and the pins were as good as new. Observing this miracle, I suddenly had the cleverest idea. I asked if she could kindly help me solder the last row of pins, for if I were to do it myself, I would most likely re-commit the blunder a third time. She did it all neat and shiny, and in comparison, the three other rows I soldered myself looked like gross dog barf. I have a lot yet to learn in the art of soldering!
Soldering the other parts were pieces of cakes -- I finished stuffing the rest of the board in less than half an hour after struggling for some two hours with the SAMD21. I wonder why they made this microcontroller such a pain to solder.
Debugging
I got my board bootloaded after a couple trials with different orientations of the cable. However, the strange part was that, neither lsusb
or Arduino IDE, acknowledged my board afterwards. To them, my board simply didn't exist. (But it was bootloaded just fine!)
My classmates also helped me take a look; we stabbed the board with the multimeter; we stared at it really hard; we clicked around in Arduino; we plugged the board into different computers; but none helped.
Then I realized that I might need to start using my brain for a bit. The fact that the board bootloaded successfully meant that there must be no problem with the wiring between the header and the microcontroller. The wiring for the LED's and buttons shouldn't matter at this point. There were only 4 traces going from the USB to the microcontroller. The board bootloaded, so it must had power, meaning that the 5V and the ground traces must be working. Therefore, the problem must be with the D+ and D- traces.
Putting the board under the microscope again, I checked the soldering for those pins very carefully. It turned out that the D- pin, when looked at from above, seemed soldered perfectly; but when inspected sideways, one would realize that there was a “staircase” between the solder on the pin and the solder on the pad, where the two just failed to touch each other by a hairline.
Amusingly, one would imagine that my gross “vomit-style” soldering was the problem; but the D- pin was in fact one of those beautifully soldered by Reina. So I heated up the iron again, and gave D- an overhaul with my signature solder vomit. That fixed the board!
I promptly uploaded a simple test program to turn on/off the LED's using the buttons. It seemed that the happiness associated with the ability to light an LED numbs very quickly: for two weeks ago I was feeling the ecstasy of my life when I turned on my first LED; This time I was only mildly entertained by the accomplishment. I expect that in the future I will be like “meh” or “yeah of course” when my new boards light up the first time.
Programming
The main task of this week, embedded programming, turned out to be the easiest part for me.
There was only one catch. The process of uploading a sketch from Arduino IDE to my board via USB only had about 60% success rate. The rest of the time it would either freeze, or tell me something like:
SAM-BA operation failed
Then ensued varying degrees of disasters:
- I can no longer upload any sketches
- The board disappears as a device, apparently “bricked”
Each time I had to do one or more of the following to get it working again:
- Bash the “hard reset” button on my board a couple times
- Disconnect and reconnect the USB hub
- Disconnect USB, trick Arduino into uploading the sketch into thin air (and failing), reconnect USB, upload sketch again
- Panic, and retry in a minute
I have yet to pinpoint the issue. I never ran into such problems in my previous experience with SAMD11C, so my theory is that it must be one of the following:
- The SAMD21 bootloader is janky
- My board is soldered badly
- I damaged my microcontroller when trying so hard to solder it (remember the episode with the razor)
- $6 USB hub from microcenter
And I am pretty sure it's not a USB connectivity issue, for I've put solder blobs on the front, and business card on the back, and the fit is as tight as it can be.
This made testing my code super annoying. I guess it's just that fate wants to give me some extra challenge to keep me entertained.
Now I know friends who learned programming the “academic” way: They can fully reason about how a program should behave, on paper, whiteboard, or even in their minds, and when they type it out, run it once, everything works exactly as they've planned. I'm unfortunately not one of those types, I build my program incrementally: I have some idea, type out a couple lines of code, run it, see how it differs from my imagination, modify a bit, add some more lines, run it again, edit, repeat.
The riskiness of the uploading process (and even when it works, it's kinda slow) forced me to think in a different way. It was actually an interesting practice.
In the end, even though there were a couple of close calls, I never unrevocably bricked my board, and managed to finish the programming.
The Games
I was quite into game/interaction design previously, but haven't touched the area for a while. Programming the board also brought back the memory of coding on a TI-84 calculator in my high school years. With only a 6 pixel, 1 bit, 1D display device at my disposal, I had to think minimalistically, to boil down ideas into their simplest forms.
After considering what game I would make, I soon decided that I wanted to make six instead of one: There're 6 buttons (not counting hard reset), so they can act as a menu from which the player can select games to play.
Startup Tune
I scripted a LED “dance” sequence for when the board is powered on, the idea is similar to how OS's play a little tune when you startup the computer. It's just a cool little indicator that tells you the board is working!
#define N 6
const int LEDS[N] = {7, 6, 5, 4, 3, 2 };
const int BTNS[N] = {10,11,16,17,18,19};
const int DANCE_SEQ_L = 29;
const bool DANCE_SEQ[DANCE_SEQ_L*N] = {
0,0,0,0,1,1,
0,0,0,1,1,0,
0,0,1,1,0,0,
0,1,1,0,0,0,
1,1,0,0,0,0,
0,1,1,0,0,0,
0,0,1,1,0,0,
0,0,0,1,1,0,
0,0,0,0,1,1,
0,0,0,1,1,0,
0,0,1,1,0,0,
0,1,0,0,1,0,
1,0,0,0,0,1,
0,1,0,0,1,0,
0,0,1,1,0,0,
0,1,1,1,1,0,
1,1,1,1,1,1,
1,0,1,0,1,0,
0,1,0,1,0,1,
1,0,1,0,1,0,
0,1,0,1,0,1,
1,0,1,0,1,0,
1,0,0,0,0,1,
0,1,0,0,1,0,
0,0,1,1,0,0,
0,1,0,0,1,0,
1,0,0,0,0,1,
1,1,0,0,1,1,
1,1,1,1,1,1
};
void dance(){
for (int i = 0; i < DANCE_SEQ_L; i++){
for (int j = 0; j < N; j++){
digitalWrite(LEDS[j], DANCE_SEQ[i*N+j] ? HIGH: LOW);
}
delay(100);
}
}
1D Pong
The first game idea that came to my mind was Pong in 1D. Instead of moving the pads, the players can only control the “existence” of the pads, by pressing buttons at either end of the board. To prevent a player from holding the button forever and never having to miss, the pad disappears after being held down for half a second. This way the player needs to hit the ball right at the time when it reaches them.
The velocity of the ball increases a tiny bit every time it is hit.
The code for Pong is extracted below (you'll need the supporting code from the full program to run it):
/*******************************************************
* 1D-Pong *
*******************************************************/
float ball_x;
float ball_v;
const float ball_a = -1.05;
int ball_xi;
int pad_a;
int pad_b;
const int pad_max_hold = 50;
const float ball_max_v = 1.0;
void pong_init(){
ball_x = N-2;
ball_v = -0.01;
pad_a = 0;
pad_b = 0;
}
void pong_update(){
ball_x += ball_v;
if (ball_x < 1.5 && ball_v < 0 && pad_a){
ball_v *= ball_a;
}else if (ball_x > N-2.5 && ball_v > 0 && pad_b){
ball_v *= ball_a;
}
if (abs(ball_v) > ball_max_v){
ball_v = copysign(ball_max_v,ball_v);
}
ball_xi = roundf(ball_x);
if (ball_xi < 0 || ball_xi >= N){
game_over();
}
for (int i = 0; i < N; i++){
if (i == ball_xi){
digitalWrite(LEDS[i],HIGH);
}else{
digitalWrite(LEDS[i],LOW);
}
}
if (ball_xi != 0) digitalWrite(LEDS[0 ],pad_a?HIGH:LOW);
if (ball_xi != N-1) digitalWrite(LEDS[N-1],pad_b?HIGH:LOW);
if (pad_a) pad_a ++;
if (pad_b) pad_b ++;
if (pad_a > pad_max_hold) pad_a = 0;
if (pad_b > pad_max_hold) pad_b = 0;
delay(10);
}
void pong_input(bool dir, int idx){
if (idx == 0){
pad_a = dir ? 1 : 0;
}else if (idx == N-1){
pad_b = dir ? 1 : 0;
}
}
Whac-a-Mole
Whac-a-mole seemed like an easy enough idea to think of when you have several lights corresponding to several buttons. It is equally easy to program:
When an LED lights up, the player needs to bash the button next to it; then another LED lights up, and so on. Each time the player has less time to react. If they bash the wrong button, or when time is out, they lose.
/*******************************************************
* Whac-an-LED *
*******************************************************/
int mole_i;
int timeout;
int timeout_max;
const int timeout_min = 10;
void whac_init(){
mole_i = rand()%N;
timeout_max = 200;
timeout = timeout_max;
}
void whac_update(){
for (int i = 0; i < N; i++){
if (i == mole_i){
digitalWrite(LEDS[i],HIGH);
}else{
digitalWrite(LEDS[i],LOW);
}
}
timeout --;
if (timeout == 0){
game_over();
}
delay(10);
}
void whac_input(bool dir, int idx){
if (dir){
if (idx == mole_i){
int i;
do{
i = rand()%N;
}while (i == mole_i);
mole_i = i;
timeout_max -= 2;
if (timeout_max < timeout_min) timeout_max = timeout_min;
timeout = timeout_max;
}else{
game_over();
}
}
}
Simon
I heard that Simon is a must-have for electronics-hello-worlders. The LED's play a sequence, which you must reproduce after it's finished. Every turn, the sequence gets a bit longer, until you can no longer remember it.
Initially I had some trouble debugging it, for even though I thought I entered the correct sequence, I lost; Soon I discovered that I'm just bad at remembering stuff and my code was in fact correct.
/*******************************************************
* Simon *
*******************************************************/
const int pattern_max_l = 64;
int pattern[pattern_max_l] = {0};
int pattern_l;
int simo_state;
int simo_idx;
unsigned long last_press_time;
int last_press_btn;
void simo_init(){
for (int i = 0; i < pattern_max_l; i++){
pattern[i]= rand()%N;
}
pattern_l = 1;
simo_idx = 0;
simo_state = 0;
last_press_time = millis();
last_press_btn = -1;
}
void simo_update(){
if (simo_state == 0){
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
for (int i = 0; i < pattern_l; i++){
delay(400);
digitalWrite(LEDS[pattern[i]], HIGH);
delay(500);
digitalWrite(LEDS[pattern[i]], LOW);
}
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
simo_state = 1;
}else{
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],BTN_CURR[i] ? LOW : HIGH);
}
}
void simo_input(bool dir, int idx){
if (simo_state == 0){
return;
}
if (last_press_time - millis() < 200 && last_press_btn == idx){
return;
}
last_press_time = millis();
if (dir){
if (idx != pattern[simo_idx]){
blink_led(pattern[simo_idx]);
game_over();
}
simo_idx ++;
if (simo_idx >= pattern_l){
reveal_screen(idx);
delay(500);
simo_idx = 0;
pattern_l ++;
if (pattern_l > pattern_max_l) pattern_l = pattern_max_l;
simo_state = 0;
}
}
}
Russian Roulette
I initially thought of making a 1D minesweeper. But it would be difficult to hint the player about number of mines in adjacent cells. I guess I could blink the light in a N times in quick succession to mean N mines nearby, but I wanted to keep things simple and intuitive.
Then I realized that minesweeper without hints is basically Russian roulette. But actually not quite, because the players of Russian roulette usually don't make a choice about which round to fire after initially spinning the cylinder. The closest game is in fact crocodile dentist. It's a common toy where the crocodile has a bunch of teeth, which the players take turn to depress; if a bad teeth is pressed, the crocodile shuts its mouth thereby hurting the player.
In terms of probability, the two games are the same, but one seems to give the user a more sense of “agency”, while the other that of “fate”.
I wouldn't have otherwise realized that hint-less minesweeper, crocodile dentist, and Russian roulette are in fact the same game, if I didn't have to boil them down to their minimal forms. I wonder if there's a school of “game taxonomy” that studies this sort of thing.
The code is laughably simple:
/*******************************************************
* Croc Dentist *
*******************************************************/
int tooth;
int teeth[N] = {0};
void croc_init(){
tooth = rand()%N;
for (int i = 0; i < N; i++){
digitalWrite(LEDS[i],HIGH);
teeth[i] = 0;
}
}
void croc_update(){
for (int i = 0; i < N; i++){
digitalWrite(LEDS[i], teeth[i] ? LOW : HIGH);
}
}
void croc_input(int dir, int idx){
if (dir){
if (idx == tooth){
flash_screen();
game_over();
}else{
teeth[idx] = 1;
}
}
}
Echo
Despite the same name, this is NOT the hello world from this course where the board repeats what you type through serial communication. Instead, you play a pattern or a sequence with the buttons; the board remembers it, and replays it in a loop via the LED's. Even though there's no sound involved, the gameplay is strangely like improvising on a musical instrument.
The implementation is also inspired by music technology. I used to loathe the MIDI format: why are there separate note on and note off events? Why couldn't you tell just me how long the note is right away? Though I did in fact understand the reason, it was not until now, having to program a similar system myself, that I started to appreciate it.
/*******************************************************
* Echo *
*******************************************************/
typedef struct _b_event_t{
char idx;
bool dir;
unsigned int t;
} b_event_t;
const int max_events = 1024;
int n_events;
b_event_t events[max_events] = {0};
int echo_mode;
unsigned int echo_start_t;
int playhead;
int first_press_time;
void echo_init(){
n_events = 0;
echo_mode = 0;
playhead = 0;
last_press_time = millis();
first_press_time = -1;
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
}
void echo_update(){
if (echo_mode == 0){
for (int i = 0; i < N; i++){
digitalWrite(LEDS[i], BTN_CURR[i] ? LOW : HIGH);
}
if (millis() - last_press_time > 3000 && n_events){
flash_screen();
flash_screen();
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
echo_mode = 1;
echo_start_t = millis();
}
}else{
unsigned int t = millis() - echo_start_t;
while (events[playhead].t < t){
digitalWrite(LEDS[events[playhead].idx], events[playhead].dir ? HIGH : LOW);
playhead ++;
if (playhead >= n_events){
playhead = 0;
echo_start_t = millis();
break;
}
}
}
delay(10);
}
void echo_input(int dir, int idx){
if (echo_mode == 1){
game_over();
return;
}
if (n_events >= max_events){
return;
}
if (first_press_time < 0){
first_press_time = millis();
}
last_press_time = millis();
events[n_events].idx = idx;
events[n_events].dir = dir;
events[n_events].t = millis()-first_press_time;
n_events++;
}
To me the most surprising element is that: because the program records the exact timing of button down and button up, it captures all the imperfections of the player -- slight stagger when pressing two buttons at the same time, small falter between notes, etc. And through replaying the human performance, the inanimate crappy little board suddenly feels very human and very alive.
Brainfuck Interpreter
From the beginning I knew I wanted to add some sort of interpreter so the player can type in the code for their own games. But brainfuck has 8 commands and my board only has 6 buttons: I thought for a bit about coming up with a brainfuck variant with 6 or less commands. I couldn't find a neat way to do it without drastically changing the language: It looked like all 8 are quite essential.
So I gave up and added a “dice” game instead: All the LED's flash quickly and randomly, and when you press a button, the lights freeze and you can read the result of your “dice roll”.
Then I regretted giving up. I introduced a simple mechanism, much like the shift/alt key on our keyboards, to accommodate the 8 commands of Brainfuck. 4 of the 6 buttons are for commands, and when the 5th is pressed first, the meaning of the ensuing button press changes, giving 2x4=8 possible commands. The 6th button is the “run” and “stop” button. So:
>
: first button; move pointer right<
: second button; move pointer left+
: third button; increment number under pointer-
: fourth button; decrement number under pointer.
: fifth button, then first button; output number to LED's (little endian),
: fifth button, then second button; read number from button states (little endian)[
: fifth button, then thrid button; jump forward if zero]
: fifth button, then fourth button; jump backward if nonzero
In the video below, you can see I pressed 220222142302143040
, or ++>+++<[->+<].
in brainfuck.
- The first part
++>+++<
increments the first cell by 2, and the second cell by 3, returning to initial position. - The second part
[->+<]
is an adder, which in a loop, adds the content of the second cell into the first. - The final part
.
outputs the result to the LED's.
And as you can see in the video, the readout from the LED's is ON, OFF, ON, OFF, OFF, OFF
, following little endian order, we get 0b101
which is 5, the correct result intended from 2+3.
The implementation is below, but I haven't tested it extensively, for (as you might have realized) it's quite a pain to program in brainfuck, let alone typing it in via little buttons (there's no undo!).
/*******************************************************
* Interpreter (Brainfuck variant) *
*******************************************************/
const int max_prgm_l = 4096;
const int max_mem = 256;
char prgm[max_prgm_l] = {0};
char pmem[max_mem] = {0};
int prgm_mode;
int prgm_len = 0;
int code_ptr;
int cell_ptr;
int shft_key;
#define CMD_NEXT 0
#define CMD_PREV 1
#define CMD_INCR 2
#define CMD_DECR 3
#define CMD_WRIT 4 //4+0
#define CMD_READ 5 //4+1
#define CMD_JMPL 6 //4+2
#define CMD_JMPR 7 //4+3
// run/exit 5
void prgm_init(){
prgm_mode = 0;
code_ptr = 0;
cell_ptr = 0;
prgm_len = 0;
shft_key = 0;
for (int i = 0; i < max_mem; i++) pmem[i] = 0;
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],LOW);
}
void prgm_update(){
if (prgm_mode == 0){
for (int i = 0; i < N; i++) digitalWrite(LEDS[i],BTN_CURR[i] ? LOW : HIGH);
}else{
if (prgm[code_ptr] == CMD_NEXT){
cell_ptr ++;
code_ptr ++;
}else if (prgm[code_ptr] == CMD_PREV){
cell_ptr --;
code_ptr ++;
}else if (prgm[code_ptr] == CMD_INCR){
pmem[cell_ptr] ++;
code_ptr ++;
}else if (prgm[code_ptr] == CMD_DECR){
pmem[cell_ptr] --;
code_ptr ++;
}else if (prgm[code_ptr] == CMD_WRIT){
for (int i = 0; i < N; i++){
digitalWrite( LEDS[i], ((pmem[cell_ptr]>>i) & 1 ) ? HIGH : LOW );
}
code_ptr ++;
}else if (prgm[code_ptr] == CMD_READ){
int x = 0;
for (int i = N-1; i>=0; i--){
x = (x << 1) | (BTN_CURR[i] == HIGH ? 0 : 1);
}
pmem[cell_ptr] = x;
code_ptr ++;
}else if (prgm[code_ptr] == CMD_JMPL){
if (pmem[cell_ptr]){
code_ptr++;
}else{
code_ptr ++;
int lvl = 0;
while (1){
if (prgm[code_ptr] == CMD_JMPL){
lvl++;
}
if (prgm[code_ptr] == CMD_JMPR){
if (lvl == 0){
break;
}
lvl--;
}
code_ptr ++;
}
code_ptr ++;
}
}else if (prgm[code_ptr] == CMD_JMPR){
if (pmem[cell_ptr]){
int lvl = 0;
code_ptr --;
while (1){
if (prgm[code_ptr] == CMD_JMPR){
lvl++;
}
if (prgm[code_ptr] == CMD_JMPL){
if (lvl == 0){
break;
}
lvl--;
}
code_ptr --;
}
code_ptr ++;
}else{
code_ptr ++;
}
}
if (code_ptr >= prgm_len){
delay(4000);
game_over();
}
}
delay(10);
}
void prgm_input(int dir, int idx){
if (dir){
if (prgm_mode == 0){
if (idx == 5){
flash_screen();
prgm_mode = 1;
return;
}else if (idx == 4){
shft_key = 1;
}else{
prgm[prgm_len]=(char)(idx + shft_key * 4);
prgm_len ++;
shft_key = 0;
}
}else{
if (idx == 5){
game_over();
}
}
}
}
Downloads
You can download my design files below: