PocKonso!
pocKonso is an Arduino-based "pocket console" for simple retro gaming.
Its small size and rechargeable Li-Po battery allow it to slip easily into any pocket for fun on the go.
The Li-Po battery gives 7+ hours of continuous play time; the integrated battery charger with a standard Micro USB port make recharging easy. A slide switch allows the device to be turned off when not in use. A USB-C port is used to upload games and other sketches to the pocKonso.
The rigid aluminium case affords protection from damage and gives it a unique look.
It even beeps and boops through a tiny speaker. This can be turned off should you tire of beeps and boops.
pocKonso can also be played while recharging, and if unplugged suddenly, will continue to function without interruption assuming it has recharged sufficiently.
Supplies
Electronics:
- Seeeduino Xiao (SAMD21). Note that an Adafruit QT Py (and the Adafruit QT Py R2040) are pin-for-pin drop-in alternatives BUT cannot be receive current directly from a battery. A separate module, such as the Adafruit Micro-Lipo, would have to integrated into the design.
- TP4056 lithium battery charger and protection module with R3 regulator resistor swapped [this modification is detailed below]
- 220 mAh Li-Po battery
- 128*64 monochrome OLED I2C display
- mini-speaker (Specs? I pulled this from a Samsung clamshell phone)
- on-off slide-switch
- on-off (x2) DIP switch mounted flush to external face
- wire
- 6.8K ohm resistor
Solder, flux, shrinktubes, electrical tape... the usual.
Aluminium case was sourced from scrap piece of closet rail. Tools and materials used were:
- Hacksaw
- hobby razor saws
- hand files
- emery boards (sanding sticks for fingernails)
- cyanoacrylate glue
- rubber cement glue
- 1 nail
patience and creativity :-)
Breadboarding
I did not take any photographs of the device's components on the breadboard.
Please refer to the Schematic step, below, for an understanding of how the components are connected.
Project Box... Sometimes Murphy's Law Works for Me!
When I began the project, my personal challenge was to make it as small as possible and I made a simple wood mock-up for the ergonomics and to play with how the components might fit. My original intention was to make the case out of hobby plywood, a material I am familiar with and for which I have the appropriate tools.
Unfortunately, as I drilled holes into the wood of the mock-up block, to test the position of the buttons, the wood split in two along the grain. As I rummaged around looking for a block of replacement wood, I happened across a scrap piece of aluminium: a "closed U" bracket from a closet door repair. It was a bit narrower but looked like it would do the job.
Life is serendipitous sometimes.
In the first photograph, the green block is the original wood piece, wrapped in painter's masking tape. The aluminium bracket has had the momentary switch holes drilled out, and I've outlined in red where the display might go, but the final cuts have not been made.
A Tight, Portable Rechargeable Power Package! Also, How to Mod the TP4056 Regulator Resistor for Your Battery
As can be seen in the photograph, the microcontroller is very small.
Don't be fooled by its size, it is quite capable (far superior to a UNO) and has a few tricks up its sleeve; notably, it has a power regulator with two contact pads on the bottom of the board. This means that power can go directly from the battery (full charge of 4.2v; nominal 3.7v ) to the board without magic smoke being created. Also, because it runs at 3.3v I don't need to use level shifters for the display which requires that current... bonus space savings.
A lithium charging module with overcharge and over-discharge protection is required to recharge the battery.
I used a TP4056 lithium charging module (with overcharge and over-discharge protection).
Because the TP4056 is built at the factory with a regulator resistor for an Amp (or greater) battery, and this project uses a small 220mAh battery, I had to replace the resistor with a 6.8k resistor as per the tp4056 datasheet.
I replaced the SMD resistor with a through-hole resistor; the through-hole resistor can be seen at the bottom of the module in the second photograph. A brief explanation of how this is done:
1- if your battery is less than 1A, refer to the datasheet to determine which regulator resistor value is appropriate. Specifically, refer to the Rprog Current table on the third page. TP4056 datasheet
2- if replacing with a through-hole resistor, pre-bend the wires so that they will touch the contacts. See photograph #2. I only bent the wires to the approximate shape and did not cut them.
3- dab a dollop of flux on the SMD resistor at R3;
4- with your properly tinned soldering iron in your dominant hand, use tweezers in the other hand to grip the SMD resistor.
5- apply the soldering iron to the SMD resistor and apply gentle pressure to the SMD resistor; it will come off the pads easily.
6- use soldering wick to remove excess solder;
7- using a "helping hands" to hold the module, solder one wire of the resistor into place. In order to get a tight fit, I bent the wire more precisely with tweezers once the SMD was removed.
8- once the first wire is properly positioned, it is a simple matter of bending the resistor and second wire to the proper position prior to soldering it in place.
9- verify there is no bridging or contact with other components; cut excess wire.
The power wiring is as follows: + and - leads from the Li-Po battery to the TP4056, + and - leads from TP4056 soldered directly to the Seeeduino Xiao power pads. I placed a simple on-off slide-switch on the + line so as to be able to turn it off.
The third photograph demonstrates how the components might fit inside the aluminium case - this was not, in fact, the final orientation of the parts.
The Display: a Small SSD1306 OLED
The display is a 128*64 OLED display with an SSD1306 chip.
It is two-colour monochrome (not an oxymoron?) 25% is yellow and 75% is blue. It uses the I2C wires to receive data.
The I2C hardware pins of each MCU is different, check the pin-out of your device to identify the SDA and SCL IO pins. You can also, for a processing cost, use software I2C pin assignments. This is sometimes referred to as "bit-banging". A display using ISP could also have been used, but I2C uses only 4 wires.
I'm using the Adafruit SSD1136 library. Props to Adafruit for making this and so many other libraries available to the public.
This display draws little power (20.7mA) and is quite bright, and is a good choice for this type of project.
I was unable to de-solder and remove the pins from the display. This was a little inconvenient. I decided to mount the display "upside down"; the orientation issue was easily corrected with software.
Wiring is straightforward. Again, because the XIAO operates at 3.3v, no level shifting was required.
3.3v pin to Vcc
GND to GND
SDA to SDA
SCL to SCL.
Because of space constraints and the use of an aluminium case, I placed shrink tube insulation around each solder joint to ensure that no shorting with other wires or the aluminium case would occur.
Project Box 2; Discovering the Idiosyncrasies of Al
I've never worked with aluminium before.
I like it, but it is idiosyncratic: it's strong and light, but quite soft and easily scratched. It can be bent... But only a little. Also, it conducts electricity quite well... So watch out for shorts. Also, it doesn't like glue if the mating surface is too smooth.
I used a hacksaw for the rough cuts, followed by handheld hobby razor saws for more precise cuts. Except for cutting out the window for the display, I did not use the Dremel. The Dremel was simply too difficult to control with any degree of precision. I may not have been using the correct bits or technique. Final cleanup of the edges was done with hand files. The holes for the micro USB, USB-C and micro switches, were made by drilling holes and using hand files, testing frequently to check the size.
Although the use of hand tools was extensive, the soft character of Al means that cutting time was not unreasonably long and I soon learned how many more strokes of the razor saw would be required for a certain depth of cut.
For the Aluminium end caps, I used cyanoacrylate (AKA Krazy Glue) on the speaker resonance chamber and end cap, and ShoeGoo- a rubber cement with more give on the other end cap with the ports. I used the ShoeGoo because while strong, it will be relatively easy to remove if I need to.
Standard advice for this step:
measure twice (three times, four times) cut once.
Cut a smaller hole than required: you can remove material relatively easily (with the use of the hand files), but it is difficult to add material.
In the end, the case has a solid feel in one's hand. It has no flex at all and, unless it falls directly on the display, I doubt it will ever break.
Wiring It All In... and Feature Creep.
What with the switch, the MCU, the TP4056, the display and the momentary buttons it was all getting a bit tight in the case.
Then the customer (my 9-year-old son) asked me if it would be able to play Friday Night Funkin', a popular rhythm game which involves pressing one of four buttons in time to music. After thinking about it, I decided it probably could... but a speaker would be necessary for sound.
Could it fit a speaker?
It just so happened that I had a tiny speaker from the teardown of an ancient Samsung clamshell phone which fit.
At this point I had already permanently installed the switch and soldered the MCU+battery pack to the display and buttons. Could I still wire the speaker?
Yes, if I snipped and peeled back the electrical tape I could expose a pin on the MCU. Sweet! Beep and boop tones() could be had.
But there was no space to add an amplifier, so I opted to create a small resonance box and drill a hole for the sound to escape. It works, though it is not very loud of course.
Wiring a speaker (or piezo) is straightforward: the red wire can be wired to any pin, and the ground goes to ground. I added a switch, as described in the next step, so as to be able to turn off the sound.
Feature Creep 2 and Closing It All Up.
Having a monotonal speaker is awesome but can get a little annoying, especially when the same tune is on a loop. I needed a switch to turn it off, but I didn't want another bump ruining the clean lines of the pocKonso.
Edit: video added to hear pocKonso play a classic :-)
Fortunately, I happened to have a very small switch (technically two single throw, single pole DIP switches) in my parts box. Moreover if I place it just so, flush to the endcap, it makes a cute little robot face!
Though I have no need for it, I was able to wire a second pin to the second switch as well as the one for the speaker. If I ever want to add another feature, I have a wired line controlled by a mechanical on-off switch ready to go. It's nice when one can leave the possibility of upgrading open.
Being flush with the endcap face, the DIP switches require a small, hard point to change positions. I use a nail which fits neatly in a groove in the bottom of the case. The nail is held by a piece of adhesive tape which also holds a flat piece of aluminium in place over the access trench on the bottom. Tape is not the most elegant solution; this is definitely an area for future improvement.
Schematic
MCU Seeeduino Xiao:
Pin 0 to speaker via DIP switch, speaker ground (GND) to ground bus.
Pin 1 via DIP switch to... nothing, the wire is insulated, ready for future use.
Pins 2&3 not wired
Pin 4 is hardware SDA wire for I2C to the SSD1306 display. Using wire.h and Adafruit libraries.
Pin 5 is hardware SCL wire for display.
Pin 6 is down button (if in landscape - right if in portrait).
Pin 7 is left/down.
Pin 8 is right/up.
Pin 9 is up/left.
Pin 10 is not wired.
3.3v out to power the display.
Ground to ground bus.
Power from battery to MCU goes from battery to TP4056, from TP4056 the + and - are soldered to pad on the back of the MCU, which incorporates a voltage regulator.
Power with Pong running on a loop: over 7 hours.
Software at Present and Future Plans
At the moment, the venerable Pong is fully functional. Credit to KonstantinRU whose code (from arduino.cc) I used and modified to suit my hardware. My modified version of Pong is provided in the next step.
I'm very close to getting Space Trash by Pi BOTS MakerHub working.... but there's something about the coding regarding the switches that I haven't yet figured out.
I hope to also adapt Tetris (turning the unit on its side and playing in portrait mode). But I haven't found suitable code to adapt yet, and I don't have the knowledge (yet) to code it from the ground up.
However, I intend to try my hand at coding a version of Friday Night Funkin' (FNF) a rhythm game, which requires pushing the correct four buttons when an arrow representing a musical note being played crosses a line. This should be easier to code than Tetris since arrow graphics move up, but do not rotate. The FNF video shown above only consists of a splash screen, and the three characters who will feature in the game: Boyfriend, Girlfriend, and C-Virus who will challenge Boyfriend to a rap battle; none of the game code has been written. I will likely create an Instructable demonstrating the process, once it is finished.
The original FNF can be played via web browser at: friday-night-funkin
Once I have two or more games working, I intend to create a menu, optimize them and squeeze as many as I can fit into my pocket, er... pocKonso, at once.
Arduino Pong Sketch and Space Invaders
Link to pocKonsoPONG on github
Link to Space Invaders on pocKonso [ported from XTronical.com]
If you want the code; get it from the link above.
DO NOT use the code below- the formatting is off and libraries are missing.
I have left the code below simply for ease of reference. Have fun!
ANGUS
/* I've commented the code fairly extensively but if you have any questions just write a comment and we'll look into it together :-)
A simple Pong game for pocKonso.
Thank you KonstantinRU [arduino.cc forums] for the base code! Thank you to AdaFruit for the libraries and community support! Note: pitches.h not necessary. */
//#include // uncomment if using a display using the SPI protocol
#include // pocKonso uses an SSD1306 with I2c so Wire.h is called
#include
#include
//#include "pitches.h" // ANGUS: uncomment if you want to use pitches.h, there will still be sound if commented
//const int SW_pin = 4; // ANGUS: this is KonstantinRU's code; digital pin # for potentiometer
//const int Y_pin = 1; // ANGUS: KonstatinRU's code; analog pin # connected to Y output
#define UP_BUTTON 9 // pocKonso pin; change pin numbers as required
#define DOWN_BUTTON 6 // pocKonso pin; change pin numbers as required
const unsigned long PADDLE_RATE = 33; // paddle movement speed higher is slower const unsigned long BALL_RATE = 5; // ball movement speed higher value is slower
const uint8_t PADDLE_HEIGHT = 12; // value is the number of pixels the paddle is comprised of; lower is harder //for human - below 6 is impossible for AI.
int playerScore = 0; // start score. Don't cheat ;-)
int aiScore = 0;
int maxScore = 8; // want to play best out of a thousand? go for it! ;-)
int BEEPER = 0; // this is the pin for the sound; on pocKonso it is the first pin which is GPIO 0 bool
resetBall = false; // false
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
//#define RESET_BUTTON 3 // This is KonstantinRU original code reset pin designation
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
void drawCourt();
void drawScore();
uint8_t ball_x = 64, ball_y = 32;
uint8_t ball_dir_x = 1, ball_dir_y = 1;
unsigned long ball_update;
unsigned long paddle_update;
const uint8_t CPU_X = 22;
uint8_t cpu_y = 26;
const uint8_t PLAYER_X = 105;
uint8_t player_y = 6;
void setup() {
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.setRotation(2); // ANGUS: because I installed the pocKonso display upside down, I had to change this // // value to rotate the display 180 degrees.
display.display();
unsigned long start = millis();
pinMode(BEEPER, OUTPUT);
pinMode(UP_BUTTON, INPUT); // ANGUS code
pinMode(DOWN_BUTTON, INPUT); //ANGUS code
digitalWrite(UP_BUTTON,1); // ANGUS code
digitalWrite(DOWN_BUTTON,1); //ANGUS code
//pinMode(RESET_BUTTON, INPUT_PULLUP); //KonstantinRU code commented out
// digitalWrite(SW_pin, HIGH); // KonstantinRU
display.clearDisplay();
drawCourt();
drawScore();
while(millis() - start < 2000);
display.display();
ball_update = millis();
paddle_update = ball_update;
}
void loop() {
bool update = false;
unsigned long time = millis();
static bool up_state = false;
static bool down_state = false;
up_state |= (digitalRead(UP_BUTTON) == LOW); //ANGUS code added for momentary buttons down_state |= (digitalRead(DOWN_BUTTON) == LOW); // ANGUS code
if(resetBall) {
if(playerScore == maxScore || aiScore == maxScore)
{
gameOver();
}
else{
display.fillScreen(BLACK);
drawScore();
drawCourt();
ball_x = random(45,50);
ball_y = random(23,33);
do
{
ball_dir_x = random(-1,2); //ANGUS: parameters of ball direction still need a bit of tweaking
}while(ball_dir_x==0);
do
{
ball_dir_y = random(-1,2);
}while(ball_dir_y==0);
resetBall=false;
}
}
//up_state |= (digitalRead(UP_BUTTON) == LOW);
// down_state |= (digitalRead(DOWN_BUTTON) == LOW);
if(time > ball_update) {
uint8_t new_x = ball_x + ball_dir_x;
uint8_t new_y = ball_y + ball_dir_y;
// Check if we hit the vertical walls
if(new_x == 0 || new_x == 127) {
if(new_x == 0){
playerScore+=1;
display.fillScreen(BLACK);
soundPoint();
resetBall = true;
}
else if(new_x == 127){
aiScore+=1;
display.fillScreen(BLACK);
soundPoint();
resetBall = true;
}
ball_dir_x = -ball_dir_x;
new_x += ball_dir_x + ball_dir_x;
}
// Check if we hit the horizontal walls.
if(new_y == 0 || new_y == 63) {
soundBounce();
ball_dir_y = -ball_dir_y;
new_y += ball_dir_y + ball_dir_y;
}
// Check if we hit the CPU paddle
if(new_x == CPU_X && new_y >= cpu_y && new_y <= cpu_y + PADDLE_HEIGHT) {
ball_dir_x = -ball_dir_x;
new_x += ball_dir_x + ball_dir_x;
}
// Check if we hit the player paddle
if(new_x == PLAYER_X
&& new_y >= player_y
&& new_y <= player_y + PADDLE_HEIGHT)
{
ball_dir_x = -ball_dir_x;
new_x += ball_dir_x + ball_dir_x;
}
display.drawPixel(ball_x, ball_y, BLACK);
display.drawPixel(new_x, new_y, WHITE);
ball_x = new_x;
ball_y = new_y;
ball_update += BALL_RATE;
update = true;
}
if(time > paddle_update) {
paddle_update += PADDLE_RATE;
// CPU paddle
display.drawFastVLine(CPU_X, cpu_y, PADDLE_HEIGHT, BLACK);
const uint8_t half_paddle = PADDLE_HEIGHT >> 1;
if(cpu_y + half_paddle > ball_y) {
cpu_y -= 1;
}
if(cpu_y + half_paddle < ball_y) {
cpu_y += 1;
}
if(cpu_y < 1) cpu_y = 1;
if(cpu_y + PADDLE_HEIGHT > 63) cpu_y = 63 - PADDLE_HEIGHT; display.drawFastVLine(CPU_X, cpu_y, PADDLE_HEIGHT, WHITE);
// Player paddle // This is entire section is KonstantinRU's code for the potentiometer control. Commented //out since I've added my own code for pocKonso's switches
/* display.drawFastVLine(PLAYER_X, player_y, PADDLE_HEIGHT, BLACK);
if(analogRead(Y_pin) < 480) {
player_y -= 1;
}
if(analogRead(Y_pin) > 510) {
player_y += 1;
}
up_state = down_state = false;
if(player_y < 1) player_y = 1;
if(player_y + PADDLE_HEIGHT > 63) player_y = 63 - PADDLE_HEIGHT; display.drawFastVLine(PLAYER_X, player_y, PADDLE_HEIGHT, WHITE); } update = true;
*/
display.drawFastVLine(PLAYER_X, player_y, PADDLE_HEIGHT, BLACK); //new
//const uint8_t half_paddle = PADDLE_HEIGHT >> 1; //new
if(up_state) {
player_y -= 1;
}
if(down_state) {
player_y += 1;
}
up_state = down_state = false;
if(player_y < 1) player_y = 1;
if(player_y + PADDLE_HEIGHT > 63) player_y = 63 - PADDLE_HEIGHT; display.drawFastVLine(PLAYER_X, player_y, PADDLE_HEIGHT, WHITE);
}
update = true;
if(update){
drawScore();
display.display();
if (digitalRead(UP_BUTTON) == 0) //(SW_pin)
if (digitalRead(DOWN_BUTTON) == 0) // New line Angus
{
gameOver();
}
}
}
void drawCourt() {
display.drawRect(0, 0, 128, 64, WHITE);
}
void drawScore() {
// draw AI and player scores
display.setTextSize(2);
display.setTextColor(WHITE);
display.setCursor(45, 0);
display.println(aiScore);
display.setCursor(75, 0);
display.println(playerScore);
}
void gameOver(){
display.fillScreen(BLACK);
if(playerScore>aiScore)
{
display.setCursor(20,4);
display.setTextColor(WHITE);
display.setTextSize(2);
display.print("Good game!"); // ANGUS: I changed KonstantinRU's bland (sorry) text to "You lost. Again" to //give pocKonso some attitude. Then I decided to make him/she/it somewhat more gracious.
}
else
{
display.setCursor(45,4);
display.setTextColor(WHITE);
display.setTextSize(2);
display.print("You played well !"); // ANGUS: I changed this too... initially pocKonso said "CHEATER!" //when he/she/it lost.
}
delay(200);
display.display();
delay(2000)
aiScore = playerScore = 0;
unsigned long start = millis();
while(millis() - start < 2000);
ball_update = millis();
paddle_update = ball_update;
resetBall=true;
}
void soundBounce() { tone(BEEPER, 500, 50); // ANGUS: the first value is the frequency of the tone; the //second is its length.
}
void soundPoint() { tone(BEEPER, 250, 50); // ANGUS: same as above.
}