Close
0%
0%

Tiny OLED

Tiny handheld with OLED and XIAO.

Similar projects worth following

The "small game console with an OLED screen" is practically a genre already. There are dozens of such projects, with all kinds of microcontrollers on board. I decided to do one more iteration on this design, using all the experience I gained recently with power circuits for such boards. Since the microcontroller wasn't the interesting part here, I just left a Seeed XIAO footprint on the back, so you can use any XIAO board – that gives you a rich selection of microcontrollers to choose from, including some 3rd party designs like the MIAO board.

For the prototype I used an RP2040 XIAO, running CircuitPython, and since I didn't have time to write special games for it, I just ported my PewPew library to it, so that I can play all the PewPew games on it in their 8x8 pixel glory. The speaker is currently unused, but it works either with PWM or audiopwmio.

PCB_PCB_xiao-game_2024-09-11.json

EasyEDA PCB Design

JavaScript Object Notation (JSON) - 481.04 kB - 09/10/2024 at 22:45

Download

SCH_xiao-game_2024-09-11.json

EasyEDA Schematic

JavaScript Object Notation (JSON) - 71.59 kB - 09/10/2024 at 22:45

Download

Schematic_xiao-game_2024-09-10.pdf

Schematic

Adobe Portable Document Format - 83.19 kB - 09/10/2024 at 21:25

Preview

Gerber_xiao-game_PCB_xiao-game_2024-09-10.zip

Gerbers

Zip Archive - 80.62 kB - 09/10/2024 at 21:25

Download

BOM_xiao-game_2024-09-10.csv

Bill of Materials

Comma-Separated Values - 2.99 kB - 09/10/2024 at 21:24

Download

View all 6 files

  • Boulderdash

    deʃhipu09/13/2024 at 21:01 0 comments

    I while ago I was trying to make a game shield for the micro:bit, #Micro:Boy, but trying to write a simple game for it in MicroPython I quickly ran out of memory – not for the game itself, but for compiling it source code. Minifying the code helped somewhat, but not enough to get something playable,

    But this time, with CitcuitPython, I have orders of magnitude more memory,  and can even split the code into multiple files if I have to. It's also somewhat faster, using SPI instead of I2C for communication. So I ported the code, and got something like this:

    import board
    import busdisplay
    import busio
    import displayio
    import fourwire
    import keypad
    import time
    import keypad
    import supervisor
    
    
    _TICKS_PERIOD = const(1<<29)
    _TICKS_MAX = const(_TICKS_PERIOD-1)
    _TICKS_HALFPERIOD = const(_TICKS_PERIOD//2)
    
    
    class Blitty:
        UP = 0x01
        DOWN = 0x02
        LEFT = 0x04
        RIGHT = 0x08
        BUTTON_O = 0x10
        BUTTON_X = 0x20
    
        def __init__(self, delay_ms=100):
            self.buffer = bytearray(1024)
            self.dmin = bytearray(8)
            self.dmax = bytearray(128 for i in range(8))
    
            displayio.release_displays()
            self.bus = fourwire.FourWire(busio.SPI(board.SCK, board.MOSI),
                 command=board.D7, chip_select=board.D9, baudrate=10_000_000)
            self.display = busdisplay.BusDisplay(
                self.bus,
                (b"\xae\x00\xd5\x01\x80\xa8\x01\x3f\xd3\x01\x00\x40\x00\xad\x01"
                 b"\x8b\xa1\x00\xc8\x00\xda\x01\x12\x81\x01\xff\xd9\x01\x1f\xdb"
                 b"\x01\x40\x20\x01\x20\x33\x00\xa6\x00\xa4\x00\xaf\x00"),
                width=128,
                height=64,
                colstart=2,
                rowstart=0,
                color_depth=1,
                grayscale=True,
                pixels_in_byte_share_row=False,
                data_as_commands=True,
                brightness_command=0x81,
                SH1107_addressing=True,
                auto_refresh=False,
            )
            self.delay = delay_ms
            self.next_tick = (supervisor.ticks_ms() + self.delay) % _TICKS_PERIOD
            self.keypad = keypad.Keys((board.D5, board.D1, board.D2, board.D3,
                board.D4, board.D0), value_when_pressed=False, interval=0.01)
            self.last_buttons = 0
            self.event = keypad.Event(0, False)
    
        def buttons(self):
            buttons = self.last_buttons
            events = self.keypad.events
            while events:
                if events.get_into(self.event):
                    bit = 1 << self.event.key_number
                    if self.event.pressed:
                        buttons |= bit
                        self.last_buttons |= bit
                    else:
                        self.last_buttons &= ~bit
            return buttons
    
        def tick(self):
            diff = (self.next_tick - supervisor.ticks_ms()) & _TICKS_MAX
            diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD
            time.sleep(diff / 1000)
            self.next_tick = (self.next_tick + self.delay) % _TICKS_PERIOD
    
        def update(self):
            b = memoryview(self.buffer)
            addr = 0
            for page in range(8):
                dmin = self.dmin[page]
                dmax = self.dmax[page]
                if dmax:
                    self.bus.send(0xb0 | page, b'')
                    self.bus.send(0x00 | ((2 + dmin) & 0x0f), b'')
                    self.bus.send(0x10 | ((2 + dmin) >> 4) & 0x0f, b'')
                    self.bus.send(0x40, b[addr + dmin:addr + dmax])
                addr += 128
                self.dmax[page] = 0
                self.dmin[page] = 127
    
        def blit(self, x, y, data, mask=b''):
            if not 0 <= x <= 120:
                return
            b = memoryview(self.buffer)
            page = y // 8
            shift = y % 8
            if 0 <= page <= 7:
                addr = x + 128 * page
                for byte in mask:
                    b[addr] &= ~(byte << shift)
                    addr += 1
                addr = x + 128 * page
                for byte in data:
                    b[addr] ^= (byte << shift) & 0xff
                    addr += 1
                self.dmin[page] = min(self.dmin[page], x)
                self.dmax[page] = max(self.dmax[page], x + 7)
            page += 1
            if 0 <= page <= 7 and shift:
                shift = 8 - shift
                addr = x + 128 * page
                for byte in mask:
                    b[addr] &= ~(byte >> shift)
                    addr += 1
                addr = x + 128 * page
                for byte in data:
                    b[addr] ^= byte >> shift
                    addr += 1
                self.dmin[page] = min(self.dmin[page], x)
                self.dmax[page] = max(self.dmax[page], x + 7)
    
    
    MAN_MASK = bytes((
        0b00111100,
        0b11111110,
        0b11111111,
        0b11111111,
        0b11111111,
        0b11111111,
        0b11111110,
        0b00111000,
    ))
    MAN = (bytes((
        0b00000000,
        0b00010000,
        0b01111010,
        0b00111110,
        0b00111010,
        0b01111110,
        0b00100000,
        0b00000000,
    )), bytes((
        0b00000000,
        0b00100000,
        0b00011010,
        0b01111110,
        0b01111010,
        0b00111110,
        0b00010000,
        0b00000000,
    )))
    DIRT = bytes((
        0b10001010,
        0b00100000,
        0b00000101,
        0b01010000...
    Read more »

  • The Challenge

    deʃhipu09/10/2024 at 22:42 0 comments

    When the Tiny Games Challenge was announced, I really debated whether I should enter it or not. After all, I was explicitly called out in the announcement. It would be very unfair to the other participants if I entered one of my projects that I have been polishing for over five years. So I initially decided not to enter.

    But recently I have been preparing for a workshop on becoming "full stack game dev", that is, on building your own game console and then making games for it, and while I did that, I looked through some of my old prototypes, and I realized that all of them were made to either be very minimalist in terms of parts used, or to explore a certain cost-saving technique, like making as much of the device out of PCB, for example, or seeing how many capacitors I can skip in the design before it stops working. I never really built anything "proper", because initially I simply didn't have the skill to do it correctly, and later on I was chasing interesting hacks.

    So when I was ordering PCBs, I decided to quickly make another handheld game console, but this time made properly, without hacks, without requiring special batteries or having weird switch for the power, or using the minimal number of parts. Just a normal, solid design, that is not an exercise, but something to actually use as a base for further projects. And since I used a XIAO footprint for the microcontroller board, I can also use it to experiment with new microcontrollers coming out every year.

    And when the PCBs arrived today, and I assembled the prototype and got it working, I decided to enter it into the contest after all. It's not going to compete with people who have build very innovative designs, or people who really focused on coming up with a fun game. I didn't even program a special game for it — I just ported the Pew library, so that all the PewPew games, such as snake, tetris, sokoban or othello will work on it. But I think that it can be used for learning, and as a base for future projects. Things like tamagotchi clones, flipper zero wannabes, or control panels for home automation.

  • Better Power Circuit

    deʃhipu09/10/2024 at 21:41 0 comments

    I have made similar handhelds before, and if you look through hackaday.io a bit, you will find many similar consoles made by other people, all of them using the same 128x64 OLED display, six buttons, and a variety of microcontrollers. So what's interesting in doing it one more time?

    Well, I've been iterating on my handheld game consoles for a while, and I have learned some things in the process — I'm not really a professional electrical engineer, so a lot of things I use in my designs are just tricks that I have learned from somewhere (often lifted from other people's designs). This time I used some new tricks.

    First and foremost, there is a boost converter on this board, so not only will it run from a regular coin cell battery (not rechargeable), but it will run on it until there is no electron left to suck out of it. And it should even run with the weaker batteries sold in some countries.

    Second, the power switch is no longer switching between battery and USB power. There is now a proper power switching circuit, that cuts the battery power when USB is connected.

    Third, I added a reset circuit to the OLED screen, that handles the required physical reset of the display some time after powering it on. Previously I wasted a GPIO pin just for that, but adding those simple three components lets me use the pin for a speaker instead..

    Fourth, I'm back to physical tact switches, instead of touch pads, and I'm using some very nice, low-force clicky switches that I found through a lot of trial and error.

    Fifth, the display is right side up this time, with a slot in the middle of the PCB for the FPC connector. It took some work to get the spacing right for this. There are even pilot holes for soldering it more easily. I might make the slot a milimeter or so longer next time, to make it easier to get the connector through it, though (right now you have to bend it to get it through the hole).

    Sixth, I also added an optional footprint for a BH1/2AA battery holder, for those who want to have a battery that lasts a little bit longer, but still is pretty small.

    Seventh, the speaker is without amplification, just with a protecting resistor. This is because any louder sounds would rapidly drain the battery. We only want very quiet and simple sounds here.

View all 3 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates