Electronic Business Card Part 3: Animations and Artwork

Referenced in this blog:

Last week I taught you about the physics of I2C and the low-level drivers it takes to draw single pixels to the SSD1306. This week there will be no scary circuit diagrams or physics talk: just fun pictures that move! …and maybe some scary code, but we’ll get through it together I promise.

Drawing Sprites to the SSD1306

So far I have the ability to draw individual pixels on the SSD1306. And while that is technically all it takes to run some version DOOM, I think the old-school route of pixelated sprite based animations would be more manageable than a true bitmapped mode. Plus, pixelated art styles have become super popular in indie video games, and what’s more indie than making your whole console from scratch?

I’ve decided that sprites in my project will be 8 pixels by 8 pixels. This is partially because I love powers of two (I am an Electrical Engineer after all), but mostly because the pages in the SSD1306 display RAM are 8 bits tall. If I have my sprite data, I can make use of the Horizontal Addressing Mode to write to several pages at once. The SSD1306 Datasheet illustrates how the pointer moves in this mode:

Another great thing about an 8×8 sprite is that it can be represented by single bytes (type uint8_t ) in memory. With that in mind, it’s pretty easy for me to define my sprite bitmap format as an 8 element array of bytes. Each element in the array will correspond to one column of the 8×8 sprite, which we might as well call pages like the SSD1306 does. Remember that writing a 1 to a pixel turns it on, so I can think of my sprites like this:

Now that I have my sprite format defined, I can write ssd1306_drawSprite() to draw any sprite anywhere:

/* * ! Draw an 8x8 sprite at location (x,y) * ! * ! \param x: x coordinate of bottom left pixel of sprite [0:SSD1306_COLUMNS-1] * ! \param y: y coordinate of bottom left pixel of sprite [0:SSD1306_ROWS-1] * ! \param *sprite: pointer to 8x8 sprite data (const uint8_t[8]) * ! * ! \return 0: successful, else error: * ! 1: x value out of range * ! 2: y value out of range * ! 3: I2C error during configuration * ! 4: I2C error during data transmission * ! * ! Draws an 8x8 sprite to on-chip VRAM, then the updated VRAM region to SSD1306 */ uint16_t ssd1306_drawSprite(uint16_t x, uint16_t y, const uint8_t *const sprite) { // ensure pixel location is valid if (x >= SSD1306_COLUMNS) return 1; if (y >= SSD1306_ROWS) return 2; // determine column range: [x:x+7] uint8_t colStop = x + 7; if (colStop >= SSD1306_COLUMNS) { colStop = SSD1306_COLUMNS - 1; } // determine page range uint8_t pageStart = y >> 3, // y / 8 = starting page pageStop = y >> 3; // y / 8 = stopping page, unless.. if (y & 0x07) // if y is not an integer multiple of 8, pageStop++; // two pages must be updated // update VRAM unsigned int i = colStop - x + 1, // counter for iterating pageOffset = y & 0x07; // offset from bottom of page while (i!=0) { i--; // decrement counter uint8_t lowerPage = sprite[i] << pageOffset; // move sprite 'up' ssd1306_vram[pageStart][x+i] |= lowerPage; // OR into VRAM if (pageStop < SSD1306_ROWS / 8) { // only update second page if valid uint8_t upperPage = sprite[i] >> (8-pageOffset); // move sprite 'down' ssd1306_vram[pageStop][x+i] |= upperPage; // OR into VRAM } } // send configuration message const uint8_t configMsg[] = { SSD1306_CMD_START, // start commands SSD1306_SETPAGERANGE, // set page range: pageStart, // y / 8 pageStop, // SSD1306_SETCOLRANGE, // set column range: x, // x colStop // min(x+7, 127) }; if (i2c_tx(SSD1306_I2C_ADDRESS, configMsg, sizeof configMsg)) return 3; // draw updated VRAM to screen uint8_t dataMsg[9] = { // message can be a max of 9 bytes SSD1306_DATA_START // start data }; i = pageStart; while (i != pageStop + 1) { // loop over pages if (i < SSD1306_ROWS / 8) { // only if valid page unsigned int j = colStop - x + 1; // local counter to while (j != 0) { // copy VRAM into dataMsg j--; dataMsg[j+1] = ssd1306_vram[i][x+j]; } if (i2c_tx(SSD1306_I2C_ADDRESS, dataMsg, colStop-x+2)) return 4; } i++; } // return successful return 0; }

This function is based on the ssd1306_drawPixel() function I wrote last week with a few modifications to handle page and column ranges. First, these ranges will need to be calculated and checked for edge cases before I can draw to VRAM: never forget to check for edge cases. Next is the hardest part: drawing a sprite that might span two pages in VRAM. Luckily, I can left or right shift the sprite data to move the image up or down on the display when drawing to VRAM. Here’s a visualization of how shifting a sprite can make it span two pages:

I can test my function with a simple checkerboard sprite. I should be able to draw the sprite anywhere I want, including at the edges of the screen. Here’s a picture of my test screen showing the sprites at page boundaries, spanning pages, and spanning both the x and y edges of the screen:

Generating Sprite Artwork with a Custom Python GUI

Drawing sprites to the screen is very exciting, but figuring out hexadecimal values to make fun pictures is not. There are lots of great tools out there for generating sprite artwork, but none that natively support my own made-up SSD1306 sprite format. So, I figured I’d write my own Python GUI to do just that! Don’t worry, I won’t bore you with the details of the source code, but you can find it in the PythonScripts folder of the GitHub repository for this project.

If you’re brave enough to follow along with me, I’m going to assume you aren’t afraid of the command line interface. But, just in case you are, all you need to do to run my Sprite-Maker GUI is to navigate to the folder where the file is stored and type python SpriteMaker.py in your terminal application. If you want to adjust the sprite width or display options, you can add the -h option to see the command line options.

With my custom Sprite-Maker GUI I can make (and even animate!) sprites, then just copy and paste the output to use it right away. This might seem like a lot of work to generate a few character arrays, but it’ll be immensely helpful when I want to animate different characters for my game. I’m shooting for a top-down space shooter (pun intended), so here’s a simple spaceship I animated in my Sprite-Maker GUI:

Look at those engines go!

Animations on the SSD1306

Before I can go and put that beautiful little spaceship on my SSD1306 display, I need to stop and consider how I want to handle animations. It’s tempting to just make an infinite loop and draw a different frame every loop, but that will turn out wrong: the different sprite frames will be drawn on top of each other and become a huge mess!

The solution here is to break drawing each frame into a few steps:

Clear the entire VRAM. Draw all the sprites to VRAM. Draw the final VRAM image to the SSD1306.

That probably sounds like a lot of unnecessary work: because it is. For any given frame, most of the display won’t change. Because the SSD1306 is a simple monochromatic display, there won’t be any fancy animated backgrounds or anything like that. In fact, all I’m planning to have is a few space ships and lasers flying around, which will probably mean 16 or fewer sprites on-screen at a time. So, in a worst-case scenario with all 16 sprites spread out, I would only be drawing to ¼ of the screen!

Luckily, there are lots of different ways to handle animation efficiently. One technique to to do this is called Dirty Rectangle Animation. It works by keeping track of what regions (rectangles) of the screen changed on the last frame and what regions change on the current frame. It has two basic steps on every frame:

Clear every region of the screen that you drew to last frame. For the first frame, this list of rectangles would be empty. Draw all of the sprites for the current frame and keep a list of rectangles that defines their location to be cleared on the next frame.

And that’s all it takes! I can even simplify things by only keeping track of (x,y) coordinates, because all of my sprites are the same size: 8×8. Now I can start coding my own version of Dirty Rectangle Animation!

Dirty Rectangle Circular Buffer

First, I’ll need a data structure to keep the list of coordinates and functions for adding and removing entries. I’m a big fan of the Circular Buffer because it’s very small and efficient, especially if your buffer size is a power of two! I won’t need any functionalities besides read() and write() functions, because I can make them return a value that tells me if the list is empty or full at the same time. One more thing to keep in mind is that the order in which I clear the rectangles from the last frame doesn’t matter, so I don’t need to worry about whether my list is LIFO or FIFO. Here’s a visualization of how a simple 8-element circular buffer works:

And here’s the code to implement my circular Dirty Rectangle buffer:

/***************************** * Dirty Rectangle Animation * *****************************/ #define DIRTY_RECT_BUFFER_POWER 4 //length of buffer will be 2^POWER // struct definition typedef struct DirtyRectangleBuffer_S { uint16_t readIndex; uint16_t writeIndex; uint16_t length; uint8_t coordinates[2][1 << DIRTY_RECT_BUFFER_POWER]; } DirtyRectangleBuffer; // static global static DirtyRectangleBuffer dirtyRectBuff; /* * ! write an (x,y) coordinate pair to the Dirty Rectangle buffer * ! * ! \param x: x coordinate to write to buffer * ! \param y: y coordinate to write to buffer * ! * ! \return 0 if successful, 1 if buffer was already full */ uint16_t dirtyRect_write(uint8_t x, uint8_t y) { // check if buffer is full if (dirtyRectBuff.length == (1 << DIRTY_RECT_BUFFER_POWER)) return 1; // else write values, increment writeIndex & length dirtyRectBuff.coordinates[0][dirtyRectBuff.writeIndex] = x; dirtyRectBuff.coordinates[1][dirtyRectBuff.writeIndex] = y; dirtyRectBuff.writeIndex = (dirtyRectBuff.writeIndex + 1) & ((1 << DIRTY_RECT_BUFFER_POWER) - 1); dirtyRectBuff.length++; return 0; } /* * ! read an (x,y) coordinate pair from the Dirty Rectangle buffer * ! * ! \param *x: reference to x value to store Dirty Rectangle buffer value * ! \param *y: reference to y value to store Dirty Rectangle buffer value * ! * ! \return 0 if successful, 1 if buffer was already empty */ uint16_t dirtyRect_read(uint8_t *x, uint8_t *y) { // check if buffer is empty if (!dirtyRectBuff.length) return 1; //else read values, increment readIndex, decrement length *x = dirtyRectBuff.coordinates[0][dirtyRectBuff.readIndex]; *y = dirtyRectBuff.coordinates[1][dirtyRectBuff.readIndex]; dirtyRectBuff.readIndex = (dirtyRectBuff.readIndex + 1) & ((1 << DIRTY_RECT_BUFFER_POWER) - 1); dirtyRectBuff.length--; return 0; }

I tried to do this in the most barebones way I could. I have a simple struct to keep everything organized: two index variables, the current length of the list, and the array to store (x,y) coordinates in. All of the shifted #define values just ensure that the list is a power of two in length so that I can use a quick bitwise AND instead of a slow modulo operation. I wrote the functions to return a 1 if the list was full/empty, which should be helpful with debugging later on. Once again, I’m using a static global variable to keep my dirtyRectBuffer struct from being accessed outside of the SSD1306 files.

Dirty Rectangle Animation

And now the moment we’ve all been waiting for: animating that little spaceship! The two steps I listed above are very simple to implement now thanks to my circular buffer. First, I’ll write a function called dirtyRect_clearLastFrame() that does exactly what you’d think: clear the last frame! It looks almost exactly like the ssd1306_drawSprite() function I wrote earlier, but this time I’m clearing VRAM instead of drawing a sprite to it:

/* * clear all (x,y) sprite locations in VRAM that are listed in the Dirty Rectangle Buffer */ void dirtyRect_clearLastFrame(void) { // iterate over all (x,y) coordinate in buffer uint8_t x=0, y=0; while(!dirtyRect_read(&x, &y)) { // determine column range uint8_t colStop = x + 7; if (colStop >= SSD1306_COLUMNS) { colStop = SSD1306_COLUMNS - 1; } // determine page range uint8_t pageStart = y >> 3, // y / 8 = starting page pageStop = y >> 3; // y / 8 = stopping page, unless.. if (y & 0x07) // if y is not an integer multiple of 8, pageStop++; // two pages must be updated // update VRAM unsigned int i = colStop - x + 1, // counter for iterating pageOffset = y & 0x07; // offset from bottom of page while (i!=0) { i--; // decrement counter uint8_t lowerPage = 0xFF << pageOffset; // move box 'up' ssd1306_vram[pageStart][x+i] &= ~lowerPage; // clear from VRAM if (pageStop < (SSD1306_ROWS / 8)) { // only update second page if valid uint8_t upperPage = 0xFF >> (8-pageOffset); // move box 'down' ssd1306_vram[pageStop][x+i] &= ~upperPage; // clear from VRAM } } } }

This has the added bonus of clearing the list of dirty rectangles as it reads it! Now I can modify my ssd1306_drawSprite() function by inserting one line of code (with a new error code in case the buffer is full) at line 20:

// add (x,y) coordinate to dirty rectangle animation buffer if (dirtyRect_write(x, y)) return 5;

And just like that, I’m ready to animate my ship! Here’s what I have to add to the infinite loop in my main program file to see something moving:

// test spaceship animation dirtyRect_clearLastFrame(); ssd1306_drawSprite(x, 10, ship[frame]); frame++; frame &= 0x03; x += 2; x &= 127;

Hmm, I don’t remember adding a contrail to the animation… The problem here is that I’ve performed Dirty Rectangle animation on the VRAM, not on the SSD1306 itself. When I clear the VRAM regions from last frame, I’m not also clearing the regions on the screen! But I don’t just want to send the blank regions to the SSD1306 when I clear them in VRAM, because that would make my sprites flicker in place. Instead, I’ll need a clever method to update regions of the screen at once.

Merging Dirty Rectangles

Programming interviewers, try to contain your excitement: here’s a perfect example of an interview question! I need to take the list of dirty rectangles and merge any intersecting rectangles into larger rectangles to draw. If I update these larger rectangles on the SSD1306 after my sprites are drawn, then I can update everything in one fell swoop! Here’s my strategy for only updating the pages and columns of the SSD1306 that change in a given frame:

Keep a list of all pages and columns that sprites are drawn to / cleared from. This is almost the same as my list of rectangles for Dirty Animation, but it includes the regions I clear from last frame and the regions I draw to in this frame. Also, if a sprite straddles two pages in VRAM then I will log this as two separate entries. This means the worst case size of the list will be 4 times the number of sprites I allow, or 64 entries. Sort the list of (page, column) coordinates by column and then by page. This will put all regions of a given page that need to be updated together and in ascending order by column. I’m an Electrical Engineer, so I never took the ‘3,000 Sorting Algorithms’ course in college that all Software Engineers seem to have taken. With that in mind, I’ll likely use a simple Cocktail Sort to keep things easy on me (it’s Bubble Sort’s cooler older brother). Remember that I’ll be sorting a maximum of 64 elements here, so I’m not worried about O(n**2) complexity. Draw the list in the newly sorted order, merging regions on the same page that are close enough to overlap. Because I know that all sprites are 8 pixels, or columns, wide I can merge any consecutive regions that are less than 8 columns apart. This will be easy because all regions of a given page that need to be drawn to will already be together and in ascending order.

I won’t make you look through the new code, because it’s really just the sprite drawing and dirty rectangle animation functions taken apart and put together with the sorting method I mentioned to make a new set of display functions. If you want to look through it for yourself, you can go to the GitHub repository and play with it for yourself, but I promise it looks almost the same. I now have three functions that will handle all of my sprite-based animations:

display_frameStart() – clears the regions that were drawn to last frame in VRAM according to the Dirty Rectangle method we talked about and adds these regions to the list of regions to update on the SSD1306. This needs to be called at the beginning of a frame to clear the previous frame from VRAM. display_drawSprite(x, y, *sprite) – draws the chosen sprite at the chosen coordinates and adds these coordinates to both the Dirty Rectangle list and the list of regions to update on the SSD1306. This can be called up to 16 times in one frame to draw 16 sprites; that limit can always be adjusted later! display_drawFrame() – sorts the list of SSD1306 screen regions by column and page, merges overlapping regions, and updates them on the SSD1306 from VRAM. This needs to be called at the end of a frame to render everything.

Phew, that was a lot of work! But it all paid off in the end, because I can finally watch my spaceship flit about the screen:

Next Time: Player Input

Believe it or not, the worst is behind us now! All of the work I’ve done in the last two weeks have culminated in three simple functions that will handle all of the graphics for me. Next time I’ll work on setting up reliable ways to read player input and tackle the dreaded issue of button bouncing!