Worklog Super PP: RPi Zero 2W SNES

The Next Guy

Sun Dried Cases
.
Joined
Feb 28, 2017
Messages
100
Likes
161
Location
Washington State
Portables
Melted in the Sun
Over the last week, I was bored and decided to make a raspberry pi powered portable. I found some RPi Zero 2Ws for $20 a piece and thought they were perfect since they were small. I also found out that you could use SPI screens.

Using ili9341-fbcp, I was able to connect the screen after some fiddling with the compile parameters. For the buttons, I used GPIONext, which made configuration a breeze. Each button only needed a ground connection, and the connection to the IO pin. Getting sound to output to PWM and having EmulationStation properly output to it was a chore, but I found success with a custom btoverlay file setting the PWM pins. I took the PWM signal, and fed it through a low pass filter, as well as a voltage divider to manually lower the max output volume. The little 1W speakers could not handle all the current so well.

The batteries I used are 2x 1200mAh lithium polymer in parallel. The power module I have can take this and boost it to anything up to 24v with a 8W max. I set it to 5.5v. I found the total run time to be 5 hours and 5 minutes. For the test, I streamed a youtube video and played it at max volume.

To design the case, I used Fusion 360, and cura to slice up the parts. I messed around with the raw components until I had a layout that seemed like it would work well. I came up with 2 designs. One landscape, and one portrait. The Portrait had 1 speaker, and used a 2 inch ST7789 screen. The landscape had 2 speakers, and a 3.2 inch ILI9341 screen. The footprint of the portrait is much smaller, almost as small as the original pi, at 26mm thickness. The footprint of the landscape is much larger, but only 20mm thick, nice and slim in the hand. Both units have fans the pull air inside, and the Pi is placed in such a way that the air routes over it to escape the air vent. Keeping it continuously on, it never gets above warm to the touch. The reported temp doesn't go above 50C.
schematic.png

20240508_192808.jpg

20240508_192238.jpg

20240508_201217.jpg

20240508_201232.jpg

20240508_201301.jpg


If you all are interested in the ROM .img file for the setup that uses the ILI9341, I can provide it and maybe get permission to upload it here. It's compressed to about 1GB with gz.
Attached will be the models that I used in the landscape model so you can slice and print them for yourself if you are interested.
 

Attachments

Last edited:

The Next Guy

Sun Dried Cases
.
Joined
Feb 28, 2017
Messages
100
Likes
161
Location
Washington State
Portables
Melted in the Sun
I found out something about compiling the ili9341 driver. If you use the default DMA channels when compiling for the ili9341 specifically, omxplayer will crash fbcp, and attempting to restart it causes DMA conflict. To avoid this, you can add
Code:
-DDMA_TX_CHANNEL=7 -DDMA_RX_CHANNEL=5
or other channels than the default if you are using these, and omxplayer won't cause issue anymore. This is documented on the github, but the symptoms confused me at first. If you realise that your display is no longer updating after OMX player runs, it's because of this.
 

The Next Guy

Sun Dried Cases
.
Joined
Feb 28, 2017
Messages
100
Likes
161
Location
Washington State
Portables
Melted in the Sun
I finished version 2 yesterday. This one charges with Type-C and using the g_mass_storage module with the raspberry pi, I can plug it into my computer and copy roms directly to the pi. I made a 24gb img file that g_mass_storage is given access to, then gave it a mount point at /mnt/usb_share. I then created soft links from the share to the RetroPie folder. It's mounted as read only.

This version also uses I2S audio, though I couldn't really tell a difference between this and just using pins 18 and 13 for PWM audio. It does add a headphone jack though. The power switch is wired so that you can play off of battery power, or USB power. Not power pathed I know, but still better than just battery power. I also wrote a program in c++ that monitors button presses using SDL, allowing me to add volume adjust by holding select and pressing up and down on the DPad. You can restart the system by holding L+R+Start+Select for 5 seconds, or turn off the system by holding L+R+Select+X.

Currently I'm trying to find out a way to draw on top of the console and X11 if it's open for EmulationStation so that I can indicate volume as it's adjusted, as well as battery life. Once I figure that out, v3 will have a battery monitor, provided by an ATTiny85 running at 3.3v connected to the serial port. Since the console lasts 5 hours already, I didn't make it a priority for the other versions.
20240516_105407.jpg
20240516_105532.jpg
20240516_105548.jpg
 
Last edited:

The Next Guy

Sun Dried Cases
.
Joined
Feb 28, 2017
Messages
100
Likes
161
Location
Washington State
Portables
Melted in the Sun
And now, I finally have version 3 done! This one involved a lot more software than hardware but I did add a raspberry pi pico inside, and used the really nice orange squishy tact switches. The pico allows me to control the status led when the pi is off or on as well as show charging and BSOC. It also allows me to read the battery voltage, adjust fan speed, and adjust backlight. The driver I wrote is here: https://github.com/The-Next-Guy/picotroller I also modified fbcp-ili9341 to have it draw from a SMA buffer, you can find it here: https://github.com/The-Next-Guy/fbcp-nexus/. Picotroller connects to this SMA buffer to draw overlays on top of the screen. For a week I tried messing with dispmanx but to no avail. I knew that fbcp was drawing to the screen directly, so I thought it would be easier to just modify the display driver itself.

If you have your own pi project using an SPI screen and fbcp-ili9341, but want to draw on top of the current contents of the HDMI frame buffer, you can copy overlay.cpp, overlay.h, and shared_memory.h into your own project and use my version of the lib. I changed very little. Just added a class for handling and drawing from the SMA, added an overlay init line, and modified the wait loop to wait 10us instead of using the atomic wait. I've only tested this on an RPi Zero 2w, but should work fine for any multicore board still. If you just want to be able to read controller events and process them for uinput, you can copy controller.cpp and controller.h into your project and it should be straightforward to add it in. It even provides a button callback. Use PicoSketch for your pico. It requires the Arduino Pico Framework by Earle F. Philihower to be installed in your arduino IDE.

I have it set right now such that select is the hotkey button, and pressing each action button brings up a menu. A for Fan, B for Battery, X for Volume and Y for brightness. Holding the hotkey and action button, you can adjust the fan, volume or brightness with the up and down on the D Pad (will add left and right as a polishing feature). You can shut down the system by holding Select + X + R + L or reboot with Select + Start + R + L. A very basic version of picotroller was on V2 that allowed volume adjustment and power off/reboot. The reason I used the DPad and not the triggers for adjustment even though the hand placement is a bit weird, was because that's how you load states. Before in my program I had the controller monitor and the controller driver in 2 different programs, which meant that the monitor could not interrupt the driver. Now that its combined it does interrupt the driver with these combos. Now I could have it go into a sort of "control" state where you have to enter it with a specific button combo, and only exit with a specific combo, where you could adjust the console settings, but I just haven't thought of the best way to do it yet. And it is quite rare to have to press select and an action button at the same time in a game, so I haven't had the pressure to add it yet.


The best part about writing my own controller driver, was the debounce handling. It seemed that when I was using GPIONext in V1 and V2, the button would sometimes spam 2 or 3 presses. For example, if you selected a system in the menu, it might also select the first game highlighted, which was kinda annoying. There are a few ways to debounce button presses like a basic timer, but I choose to take samples as fast as possible between frames. I set a frame size to 20ms, which I found to be overly safe but stuck with it. During each frame, I check if the button state is high or low, then I increment the high count or low counter appropriately. At the end of the frame, I check to see if the high counter is greater than the low counter and set the state, and reset the counters. This leads to the smoothest controls in any portable I have ever made, just as responsive and precise as a real SNES controller.
Controller messages are sent between the pi and the pico in a pretty simple format. The pi can send requests with the format {FVVVV}CC
Code:
'{' - literal char
F - function id (1 byte)
V - parameter (4 bytes)
'}' - literal char
C - crc16 xmodem (2 bytes)
The controller sends updates with the format {XB<BD>A<AD>}CC
Code:
'{' - literal char
X - controller id (0 - 5, 1 byte) controller 0 is used for non controller data like battery voltage, fan speed or backlight brightness.
B - Button Count (0 - 32, 1 byte) how many buttons the controller has, in an integer arithmetic context, take this value, add 7 and mod 8 to get the count of button data bytes
<BD> - Button Data Bytes (0 - 255, 1 - 4 bytes) contains button data, each button is written as a single bit, Button 0 is byte 0, bit 0, button 27 is byte 3, bit 4.
A - Axis Count (0 - 4, 1 byte) how many axis this controller has, each axis is actually treated as 2, uint16_t values. So take this and shift left 2 to get the count of axis data bytes.
<AD> - Axis Data Bytes (0 - 16 bytes) contains the axis data of this controller. Byte(c) is x high byte, Byte(c+1) is x low byte, Byte(c+2) is y high byte, Byte(c+3) is y low byte.
'}' - literal char
C - crc16 xmodem (2 bytes)
The { and} provide solid searching chars for the start and end of a message. I tested speeds up to 1mbit for serial, and have never had an extraneous input, even though power lines aren't exactly far from the serial lines. I found message error to happen about 2 - 3 times a second, and crc error to happen once every few minutes. During this time I had the controller report and update for all 4 controllers every 15ms, as well as the battery, and fan. I found a baud of 115200 worked just fine for 15 to 20ms response times though so I reduced it, and also changed it to just send a 3 time repeated burst of each event. The pi program sends a battery request every 10ms, and samples it 100 times for the average that's used for displaying and calculating the LED state. In a future update, I'm gonna have let the pi control the LED through the pico while the pico sees the pi on.

The contents of controller 0 depends on the last issued command to the pico. Commands 1- 7 change values in controller 0, Command 0 just sends the current state of the controller specified mod 4. On controller 0, button 0 is if the unit is charging, bo button 1 and 2 are false for a battery reading, button 1 true and 2 false if the last issued command was for the fan, button 2 true and 1 false if the last issued command was for the backlight. Axis 0, x and y represent the value of the last command, I could have done this in many different ways but I decided to keep status messages the same as controller messages to keep decoding simpler.

Below are some pics of the console:
20240526_204410.jpg
20240526_204232.jpg
And of the menus I added:
20240526_213357.jpg
20240526_213403.jpg
20240526_213408.jpg
20240526_213413.jpg
20240526_213432.jpg
 
Top