I wanted to build a small and compact Game of Life minidevice using an attiny85 and one of those popular small OLED displays (a 128x64 one in my case).
The idea was to build a somehow "portable" handheld Game of Life thing while learning more about the SSD1306 driver chip among the way, since my only previous experience with this screens was by using the Adafruit SSD1306 library on Arduinos and similar devices (and that's something you cannot use in the attiny85 - also, I'm not using the Arduino environment here!).
I thought it would be an "easy" project: I used some of my previous I2C bit banging code (a simple implementation I initially wrote for this NES Mini Controller USB Adapter) and started poking around with the available instructions and the datasheet configuration (also this post with some extra explanations regarding the SSD1306 modes and this SSD1306 C++ library for AVR micros) but then I realized something:
There wasn't enough space in the attiny85 SRAM to manage a dynamic full 128x64 buffer (with 1 pixel being 1 cell on the automaton)!
The main problem
For a 128x64 pixel grid, with each pixel being "on" or "off", I'll need 1024 bytes (128x64/8) of available space in the SRAM in order to dynamically populate the "cells" and, on each step, change them accordingly.
(all those images are from the SSD1306 datasheet)
But there are only 512 SRAM bytes on the attiny85! (and also I'm using a SECOND buffer in order to make the proper calculations per step and switching them when displaying, so that's even MORE space needed!).
Notice that if I used the flash memory itself to store some static data (like a single picture like the boot logo I made) I wouldn't have any problem (8k bytes of flash is enough for some cool photos!) but since I wanted to modify that grid this wasn't an option for me.
So I thought about shrinking the grid size. Or making the pixels bigger. It's kinda the same.
Bigger pixels, the solution
Instead of having a single 128x64 matrix, let's assume we have TWO different ones:
- The first one is the one from the display. A real 128x64 pixel grid that can be set by sending the proper commands and data via I2C. In my case I'm using the horizontal mode to send the different pages
- And the second one is an "internal" 32x16 grid and it's used only by the micro itself. It uses 64 bytes (32x16/8) and we have TWO of them (remember that we have TWO buffers to swap between iterations)
We handle all the "Game of Life logic" in a 32x16 matrix and update each buffer on each iteration and, when we need to send those buffers via I2C to the SSD1306 chip, we translate them so each one of our "internal pixels" is actually a package made of 4x4 "real pixels".
So, by dividing by 4 both width and height, we can manipulate up to 512 individual "big pixels"!
Sending the translated data
The SSD1306 works with pages and columns. Each page is, more or less, "an horizontal line with a thickness of 8 bits". A 128x64 display has 8 pages and 128 columns, and the way the info is sent to the screen depends on the mode (check the SSD1306 datasheet for more info about this!).
I'm using the horizontal mode, so that means I'm sending the pixels "from left to right". And since each page is "8 bits thick" that means I'm sending page after page until I reach the end.
And because I'm using an internal grid 4 times smaller than the "real one", those pages are actually "two bigger pixels thick" (instead of "8 small pixels").
With this in mind, I use the internal grid and:
- For each bit on the grid I build 4x4 equivalent pixels on a new structure for one page (that's basically putting all the bits one after the another in the proper format to be displayed as a "bigger square").
- Once I have a full 128 columns line (that's the full page) I send the proper commands to the SSD1306 and then send that single page to the driver (you don't need to send the whole buffer at once; actually...