Greyscale for arduboy

Really impressive demo. After manual calibration the flicker was greatly reduced. Wild to see gray work so well on Arduboy :smiley:


I just had a sorta-crazy idea. A lot of time has been spend trying to “dial in” the delay so it’s “just right”… but we know that’s impossible without a FR pin… and some games have done well just picking a static timer value and running with it (and accepting some flicker).

I wonder what it would look like if dialed it in but then ran with a random timer offset… so if the “perfect” value was say 16ms… then you render between 15.75 and 16.26ms, randomly… or non-randomly in some repeating pattern… I wonder if that would produce a “nicer” flicker pattern.

Instead of the “scan line” flying by (quickly or slowly) this would have the effect of randomizing it’s appearance - for better or worse.

I think I made a suggestion to this effect, but my solution was to allow the user to fiddle with the vsync until they were happy with it. Randomizing it would theoretically give a better “perception” of the tear, I think. Give it a try and let us know! :slight_smile:

Probably won’t find the time, but if someone else did I’d be happy to review the code and help think about if it was actually doing what they thought it was or not.

Yeah, the idea is you’d still let them “fiddle” but once you got it tuned in you’d switch to “exactly what you tuned +/- random offset” mode.

It might not really look that different from “slightly off tuned” (which is how Sensitive looked to me).

It seems Thumby users have come up with a (better?) method to keep the SSD1306 in sync. Technical details are interesting…



The Thumby uses the SSD1306 display driver chip. According to some people in the Arduboy community, some versions of this chip have a “hidden pin” called FR, that allows for perfect synchronisation.

We have reached the stage where the Arduboy forum is citing a Thumby project that cites the Arduboy forum. The circle is complete!


I do not believe the technique used by Thumby is applicable to the Arduboy :smiling_face_with_tear: (an explanation follows), nevertheless I am thinking of trying it to confirm.

The technique relies on setting up unused (off-screen) lines above and below the on-screen lines. This is possible on Thumby because only 40 of the 64 lines are used while the Arduboy uses all 64 lines. So I would expect the Arduboy to:

  1. End up with a mostly solid always-on first line (or last line, if the controller is setup to refresh bottom-up instead of top-down). This happens because the line used to lock and sync the refresh is visible on the Arduboy.
  2. Maybe show more flickering than Thumby at the top or bottom of the screen. This may happen because the command used to “lock” the refresh line and re-sync has to happen in a much narrower time-window on the Arduboy. i.e. during T(front porch) + T(back porch) instead of T(front porch) + T(back porch) + T(offscreen lines).

EDIT: On second thoughts brightness (or contrast) could be set to zero while locking the scanline or the vertical timing settings could be updated so that the locked line is in a region where the controller doesn’t allow it to drive the display. So many things to try … :crazy_face:


They seemed to dig deep into the control codes… hopefully there’s something of value there? Their source code is well commented.

No dice. I am not 100% done with testing but I can confirm the appearence of (the expected) artifacts on screen. One of the artifacts presents itself as a very bright bottom row on the display which can likely damage that specific row of OLEDs. This approach (on the Arduboy’s full size screen) is IMO worse than the existing solutions, both visually and in terms of CPU usage.

@dxb Picking your brain here because you understand this method much better than I:

Could a grayscale picture be achieved using this method with 8px tall black “anamorphic bars” on the top and bottom? Like using +8px vertical scroll and the bottom 16ish rows for the timing variation zone with all zeros written to the controller buffer for those rows. Then usable grayscale screen is 128x48.

Alternatively, 16px wide bars on the sides with +16px horizontal scroll and vertical addressing mode, with a 96x64 usable space.

1 Like

Hi, short answer is no. The reason is that in order to avoid artifacts the artifacts have to occur off-screen. Always-off bars would not be enough because those lines are still driven by the controller. The Thumby has a smaller screen so the lines where the glitches occur are not visible because they are physically not connected to the display.

If you are ok with half-screen grayscale (top, center, bottom or streched) then the method implemented in this demo works fairly well and is easy on the CPU.

1 Like

I feel we are quitting too easily… we cannot ‘lose’ to the new kids! :sweat_smile:

lol You can swap out the arduboy’s screen with a smaller one and it will work! :stuck_out_tongue_winking_eye:

1 Like

If the manufacturer would only bring out the pin, it’s on the controller and there is an available pin on the fpc. I asked, for whatever reason they can’t be bothered.

I’ve speculated before that they do this on purpose to just sell more grayscale displays.

After some experimenting – as long as controller RAM bits for the would-be off-screen pixels are kept cleared (sacrificing some screen space), I think we can adapt this approach!

Here’s an attempt.

Three variations selected by the macro GRAYSCALE_OPTION:

  • 0: 4-level in 2 frames using contrast (slight noise at border between 01 and 10)
  • 1: 4-level in 3 frames (slight flicker due to lower refresh rate)
  • 2: 3-level in 2 frames (no visual artifacts for me but only 3 levels)

It currently uses a timer for precise frame timing but I don’t think this is necessary. The usable area is cut to 128x55 pixels (the last page and the first row have to be kept zeroed out) and requires 1792 bytes to buffer, an increase of 768 bytes. If starved for RAM you could probably pre-render the appropriate plane each frame.

I was hoping to be able to sacrifice columns instead of rows to have a nicer aspect ratio left to work with. Unfortunately I don’t think this can work… I’ve learned the controller only drives entire rows so this approach needs extra rows to sacrifice for the timing variation.


Great work. I’m glad you proved me wrong. My test code isn’t using a timer and the result is far cry from yours.


FYI the acceptable range for BLAH on my Arduboy is [406, 443] using GRAYSCALE_OPTION 0 and the default Fosc setting (see 0xD5 command). Outside that range flickering, or other undesirable artifacts appear.

Hopefully there is a subset of values for BLAH that works OK for most Arduboys out there.

It is possible that setting Fosc to the maximum allowed value using 0xD5, 0xF0 will result in less dotclock jitter.


Adding 0x7C (see ‘10.1.6 Set Display Start Line’ in the SSD1306 datasheet) to SETUP_CMDS and disabling wiping of the first scan-line centers the usable area vertically in the screen and makes the first line of the frame buffer available for use (usable area: 128x56 px).

static uint8_t const SETUP_CMDS[] PROGMEM =
        0x22, 0, FBR - 1,
        0x91, 24,
        // slight boost to phases 1,2 improves flickering at 01,10 neighboring pixels
        0xD9, 0x33,


On my Arduboy, using both your original gist and one with my vertical alignment change, I am observing an anomalous burst (1 or more subsequent frames) every so often (approx. every 1 minute or so). The glitch looks like a black (partial?) frame to the naked eye. I wish I had a camera with a high enough frame rate to see what is going on exactly.


Out of curiosity: is the external clock input pin brought out?

The Thumby technique implemented by @brow1067 could be combined with a display clock signal we can count (or control) using a combination of PWM and timers. This way the frame duration would be known and we’d likely be able to use the entire 128x64 screen.

Thanks a lot for the display start line command! I’m still learning my way around the controller.

I see the occasional missed frame too. I think it’s the millis/micros ISR occasionally causing the frame ISR to miss its timing window, because disabling the timer0 overflow ISR seems to fix it.

I’ve updated the gist to incorporate display start line and disable the timer0 ISR. This means that none of millis/micros/delay can be used anymore but I think this is OK since the update/render loop is tied to the frame ISR to avoid tearing, which negates the need for Arduboy2::nextFrame or similar. Are there any other vital uses for timer0?

Nice! I went looking for the current implementation of the relevant TIMER0_OVF_vect interrupt handler. I also see that millis/micros calls disable interrupts albeit for a short time.

I saw you used a value of 0x7E (2+6 lines split) instead of 0x7C (4+4 lines split). Any specific reason to prefer one or the other?

Ack! Just a brain fart. Fixed!