Stage, a Tile and Sprite Engine

A library for MicroPython for drawing tiles and sprites on a RGB SPI screen.

Similar projects worth following

This is a MicroPython library that provides the means for drawing sprites with tiled background on 16-bit SPI-based displays. It consists of a small part written in C that needs to be compiled into the firmware, and the rest written in Python, usually also included in the firmware as frozen modules.

This library was originally created in CircuitPython for #µGame, but has since been ported onto other platforms.

  • Tinypico Play Shield

    deʃhipu12/18/2020 at 17:33 6 comments

    After watching Unexpected Maker's stream on which he builds his Play Shield for Tinypico, I decided to try and see if the Stage library would run on it.

    I asked him about which display he uses and how it is connected, and made this "driver" for it:

    import ustruct
    import utime
    class Display(object):  # ST7739
        _BUF = bytearray(4)
        width = 240
        height = 240
        def __init__(self, spi, dc, cs=None, rst=None):
            self.spi = spi
            self.dc = dc
            self.cs = cs or (lambda x: x)
            self.rst = rst or (lambda x: x)
        def reset(self):
            for command, data in (
    #            (b'\x01', None), # reset
                (b'\x11', None), # wake
                (b'\x3a', b'\x55'),  # format
                (b'\x36', b'\xc8'),  # mad
                (b'\x21', None), # invert
                (b'\x13', None), # no partial
                (b'\x29', None), # on
                self.write(command, data)
        def write(self, command=None, data=None):
            if command is not None:
            if data:
        def block(self, x0, y0, x1, y1):
            y0 += 80
            y1 += 80
            ustruct.pack_into('>HH', self._BUF, 0, x0, x1)
            self.write(b'\x2a', self._BUF)
            ustruct.pack_into('>HH', self._BUF, 0, y0, y1)
            self.write(b'\x2b', self._BUF)
        def clear(self, color=0x00):
            self.block(0, 0, self.width, self.height)
            chunks, rest = divmod(self.width * self.height, 512)
            pixel = ustruct.pack('>H', color)
            if chunks:
                data = pixel * 512
                for count in range(chunks):
            if rest:
                self.spi.write(pixel * rest)
        def __enter__(self):
            return self
        def __exit__(self, exc_type, exc_val, exc_tb):

    One surprising thing that took me a while to figure out is that to get correct colors, you have to put the display in inverted color mode — a bit weird, but I guess it's a question of how the actual LCD is connected to the chip inside. A quick test confirms that it works:

    Next I needed to handle the buttons. The Play Shield uses MPR121 chip to handle them, so I just added this button-handling class:

    class Buttons: # mpr121
        def __init__(self, i2c, address=0x5a):
            self._i2c = i2c
            self._address = address
            for register, value in (
                (0x80, b'\x63'), # reset
                (0x53, b'\x00'), # stop mode, reset config
                (0x2b, b'\x01\x01\x0e\x00\x01\x05\x01\x00\x00\x00\x00'),
                (0x5b, b'\x00\x10\x20'), # debounce, config1, config2
                (0x53, b'\x8f'), # exit stop mode
                self._i2c.writeto_mem(self._address, register, value)
        def _get_pressed(self):
            return int.from_bytes(
                self._i2c.readfrom_mem(self._address, 0x00, 2), 'big')

    Not having an MPR121 chip at hand, I couldn't test it, but once I sent the compiled binaries and some example code to Unexpected Maker to test on the actual shield, it ran correctly, as he shows on Twitter:

    Well, OK, that demo doesn't actually use the buttons, but it initializes the chip, and that seems to have worked.

    I can do further refining once I get my hands on the actual shield.

    The full code and compiling instructions are at

  • Dynamically Loadable Modules, Continued

    deʃhipu03/08/2020 at 22:31 0 comments

    I got some help with that, and did a little bit of progress, though it still doesn't work the way it should.

    People who are smarter than me started to figure things out, and there is a pull request with more documention with some additional discussion that has cleared some stuff for me.

    Basically you can only call functions that have been explicitly exposed to the native module mechanism, and if you need some additional functions, they need to be added to a list. Sounds simple enough, and I managed to get that to work, but there is a small catch: it will only work with the firmware that has those functions added as well, so it kind of defeats the whole purpose.

    Fortunately, there seems to be some ongoing work on solving this:

    So hopefully in a couple of months it will be possible.

  • Dynamically Loadable Module

    deʃhipu12/25/2019 at 23:04 0 comments

    MicroPython 1.12 is released, and the new release includes an interesting feature: the .mpy modules can now contain native code compiled with C or any other language that produces standard object files. Of course such .mpy files are then architecture-specific. This is great news for the stage library, because it means that you won't need to compile it into the firmware anymore — you should be able to just copy the .mpy file to the filesystem and import it as any other Python module.

    But how to actually do this? There is very brief documentation at and there are some examples at — not much to go by, but I will try anyways.

    The first, naive, try failed badly. Just removing the module initialization code and replacing it with mpy_init with equivalent functions is maybe enough for the factorial example, but not for my library — in particular, the dicts that act as namespaces for the two classes that I defined fail to compile, because they would normally go to fixed code, and that is not supported with dynamic loading. So I started to dig through the examples, and figured out from the framebuf and regexp modules how to dynamically declare those classes. So far so good. Now the script crashes. Great. After naively fixing the script, the compilation still fails, because it can't locate the mp_obj_get_array function. Which is super-weird, because it has no problem locating other functions from the py/obj.c file.

    My code is available if anybody wants to give it their try.

    At this point I've given up. I might try it again after the next release, maybe this feature will be more ready for actual use.

  • M5Stack Support

    deʃhipu08/01/2019 at 20:02 0 comments

    I added support for the buttons on the game face of M5Stack, (it's very similar to my #D1 Mini X-Pad Shield, I basically just had to change the numbers for the button maping, the I²C address, and negate the output). Together with the automatic 2× scaling, it means you can play the µGame games on it now (and, what is more important, easily make your own).

    What I don't like is the noise from the speakers — for some reason that happens only when the face is connected, and not on USB — perhaps grounding issues? There is no sound from the game, because so far MicroPython doesn't support it (though technically it's possible, someone just needs to write the code).

  • Scaling

    deʃhipu08/01/2019 at 14:00 0 comments

    One problem with supporting devices other than #µGame is that they use different displays. The 160×128 version of ST7735 is not that bad — there is just a strip of 32 unused pixels — but the ILI9341 is almost twice as large, with 320×240 pixels. So you either play the game on a tiny postage stamp, or you adapt it to the platform — but the 16×16 tiles and sprites are still too small.

    That's why I just added a "scale" optional parameter to the Stage object in the library, that allows you to scale the display 2× or more, giving you back the chunky pixels we so love.

  • Proper Repository

    deʃhipu08/01/2019 at 13:53 1 comment

    While the CircuitPython port of this library is being used both on the #µGame and a number of Adafruit boards (see #PyBadge Hacking), and is included in the official repository, the MicroPython port is not as well supported. In fact, if you wanted to use it on MicroPython, you were up for a bit of a challenge, figuring out what files to modify in order to have the C portion of the library compiled in your firmware. But that is no more.

    Recent releases of MicroPython have a primitive but workable way for adding third-party modules, and I finally took the time to follow that guide and make a proper repository for the MicroPython version of Stage, available now at The documentation is still lacking, and the only fully working example is for a D1 Mini with the #D1 Mini X-Pad Shield and an ST7735 display, with some partial support for M5Stack, but I plan to extend that to a couple of third-party devices, including OdroidGo, Meowbit, and maybe even Pokitto.

  • Build for ESP8266

    deʃhipu01/30/2019 at 23:17 0 comments

    If you want to try playing with this, but don't feel like compiling your own MicroPython firmware, I prepared a build for the ESP8266 with the #D1 Mini X-Pad Shield and an ST7735 display module:

    I also updated and rebased the "stage" branch, here:

  • MicroPython Branch

    deʃhipu01/08/2019 at 15:05 0 comments

    I just realized that while this project page links to the repository with the plain python portion of the library, the portion that is written in C is harder to find. So here it is:

    The branch is a little bit dated, since I didn't upgrade it in a while, but if you look at the three last commits, you will see all the required code. It's not much, just enough to make the plain python version fast enough. Enjoy!

  • This Just Arrived

    deʃhipu06/29/2018 at 11:55 0 comments

    The ODROID GO is here:

    For now I just tested the default firmware, which contains all the emulators. It works as advertised, though I have to say that the buttons feel rather cheap — much worse than on the #µGame. The sound is super-loud and kinda annoying, but you can mute it.

  • More Players

    deʃhipu06/20/2018 at 17:37 0 comments

    Looks like my decision to abandon #µGame Turbo and instead focus on improving this library and making it work better on the original #µGame might have been right. New devices are popping up left and right. Adafruit has recently leaked some information about the Arcade FeatherWing they are working on, and now @Jarrett alerted me about a new device made by Odroid:

    From the point of view of hardware, it uses the same stuff as the M5Stack, so this library should already work on it. I ordered one ($48 with shipping for me), and of course will attempt to get my games to run on it. Still a bit more than the $11 I paid for the TTGO 1.0, but this one has the button and a case already. We will see how good those buttons are...

View all 12 project logs

Enjoy this project?



Makerfabs wrote 03/12/2018 at 03:48 point

An 3D printer enclosure makes the projects friendly to none-hardware engineers...

  Are you sure? yes | no

deʃhipu wrote 03/12/2018 at 09:50 point

Definitely! Also safer to put in your pocket, etc. 

  Are you sure? yes | no

Ted Yapo wrote 03/10/2018 at 23:43 point

How fast is it?  I guess I could figure it out by the number of bits and SPI speed, but it's easier if you tell me :-)

I assume you update just the dirty areas of the display?

  Are you sure? yes | no

deʃhipu wrote 03/11/2018 at 00:03 point

ESP8266 could do the same speed as the MCU clock, so 80MHz without overclocking, and 160MHz with. The ESP32 has a 240MHz clock, but I'm not sure what the SPI clock limits are. Well, HSPI is limited to 26MHz in full-duplex mode, but VSPI doesn't seem to be.

Yes, I only update the dirty areas, that's how I managed to make it work with 24MHz SPI on the µGame. Of course that mans no scrolling, but the ILI9341 has hardware scrolling in one direction, so I might be able to make use of that.

  Are you sure? yes | no

Josh Lloyd wrote 05/01/2019 at 00:03 point

I believe the SPI Baudrate limit on the ESP32 is 80MHz if you're lucky, I've had the chip fail to perform at 80MHz, and getting 40MHz instead. This applies to the QSPI also that the Flash and PSRAM are on. 

  Are you sure? yes | no

deʃhipu wrote 05/01/2019 at 19:13 point

Before you hit the limitation of the ESP8266, you hit the limitation of the display itself. I'm driving it at 24MHz SPI, but that is already outside of its official limit of 20MHz for writes.

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

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