Greyscale for arduboy

Looks like this isn’t an option either :frowning:


Arrrgh, thanks for looking that up. Is that the datasheet for the actual display panel used by the Arduboy as opposed to the generic controller datasheet? Could you please share a link to that doc? Thank you!

The datasheet I’ve been looking at is on Adafruit.

Judging from this thread, it looks like this is actual package used in the Arduboy. And no luck there either:

Should voltage be a factor? Arduboy FX unit used:

402 — 443 (Battery normal)
416 — 457 (Battery depleted)
400 — 440 (On USB)

463 — 510 (Battery normal)
479 — 525 (Battery depleted)
460 — 508 (On USB)

GRAYSCALE_OPTION 2 (looks the best IMHO)
461 — 509 (Battery normal)
477 — 529 (Battery depleted)
460 — 507 (On USB)

I think I’m quite sensitive to the strobing. Although it’s on the edge of perception, modes 0 and 1 bothered me. I’d accept just one grey tone to avoid any weird effects.

Updated: Battery depleted to minimum (only lasts 30sec- just enough to test). All testing with ‘stock’ gist, with display start line of 0x7F.

Good find with the battery/USB difference! I’ll bet there is also a difference depending on the battery charge as well. What value of display start line are you using?

Currently my preference is 0, 2, 1. For me the only strobing on mode 0 happens only at the boundary between the two different gray shades because they must be rendered on mutually exclusive frames in a 2-frame cycle. My hope is that it would become unnoticeable for more detailed images or if the art style minimizes the times that those two shades neighbor each other.

Don’t be surprised if you find that ambient temperature has an effect, as well.

Oh smart intentionally blanking part of the screen to catch the refresh cycles! That’s a significant chunk of ram though!

Current gist value of 0x7F. Updated original post with depleted battery readings. Hope that helps, .

1 Like

Thank you. Yep, looks like that’s the one. sigh

OK, updated the gist.

  • Biggest change: sacrificed more rows to enlarge timing window. Usable space is 128x48, my interval (both USB and full battery) is now [387, 477]. I’m fairly confident there will be some value common to all Arduboys and all battery levels and temperatures now.
  • At frame_counter = 415, option 1 has four levels with no artifacts – it’s my new favorite.
  • Pre-charge setting is the same for all three options so they should all share the same timing window.
  • Displays test image of a girl borrowed from thumby code, in addition to old solid gray bars.
  • Grayscale option is displayed and can be cycled at runtime with A/B.
  • Hold left/right to roughly adjust frame_counter and press up/down to increment/decrement.

I have a thought… since option 1 now works well enough at a three-frame cycle, perhaps we could use the full screen now, where the top 48 rows are grayscale in some two-frame style (option 0 or 2) and the third frame in the cycle is used for monochrome rendering in the bottom 16 rows, drawn by setting the COM scan direction to scan bottom-to-top. The contrast could be cranked up real high for the third frame to account for the fact that it’s only drawn for one out of every three frames.

I think this could enable some neat games incorporating clean grayscale while using the full screen. Like a grayscale gameplay/action area in the top 48 rows with monochrome status info displayed in the bottom 16.

1 Like

Minor issue with rev 13: when cycling through option 0 the contrast setting is altered and never reset to the default value when switching to the next mode so the screen looks dark.

It sounds ambitious! I have been going in a different direction: I am thinking of a calibration app that saves settings to a known EEPROM location and focussing on “option 0” only. I have reworked the main loop and timer code from an earlier version of your gist to get 128x63 lines. The range of acceptable frame durations is narrow but it otherwise seems to works well.

I have decoupled pushing bits to the display memory from the frame-sync hack. CPU time is not spent in delays or refreshing the display in the interrupt handler. The duration of the gap between setting 0xA8, 0x00 and 0xA8, 0x63 is all that matters for locking. The longer the gap, the wider the range of accceptable values and the taller the black bar at the bottom.

I can publish it if you are ok with that. I would add a comment to point out it was derived from your gist.

1 Like

Oops, thanks! Rev 14 now.

I like this! Though I worry about ensuring the range is large enough to cover varying battery charge and/or temperature – @acedent noted the timings changed quite a bit as his battery discharged. One way to help this is to make sure calibration is always done on USB power and sets the timing at the top end of the interval (slowest allowable frame speed) to make sure it keeps working as Fosc slows with voltage droop…

I forgot to mention, I found this too by accident and I don’t know why this is the case. The gist inserts calls to delayMicroseconds after updating display memory. Can you help my intuition for what’s going on during the gap and why that delay is important?

My prior intuition was that setting the mux ratio to 1 immediately interrupts the display refresh and causes the display to drive row 0 over and over, so I thought you set mux ratio to 1, then copy to controller RAM, then allow the display to drive all rows again by setting the mux ratio back to 64. Then you precisely time the refresh interrupts to occur somewhere between the last used row and row 63 before it wraps around. But this has to be the wrong intuition because adding extra delay after parking the row driver at row 0 wouldn’t alter this strategy.

Yes, please! No attribution needed.

1 Like

Gist here. I will proceed to cleaning up the code and add the girl image from your gist (and probably make it scroll vertically with wrapping).

I tried to answer your question and document what I found at the top of the gist. let me know if it makes sense.

1 Like

Wow! Your comments gave me a really important insight – that the mux ratio doesn’t take effect until the end of the current frame. From testing, what I think the controller is doing is doing an equality test on the mux ratio register after driving each row, not a “greater than or equal to” test. Only if that row index is equal to the mux ratio (taking into account display start line / display offset or whatever), then it starts a new frame. This means we can set the mux ratio to 1 any time after the first row has been driven and before the frame ends (a huge window, very easy to hit), and it won’t take effect until the beginning of the next frame!

This means instead of this:

  1. Set mux ratio to 1
  2. Copy image to controller memory
  3. Delay for a precise-ish amount of time
  4. Set mux ratio to 64

We can do this:

  1. Copy image to controller memory.
  2. Set mux ratio to 64
  3. Delay for at least the amount of time for the controller to drive one row
  4. Set mux ratio to 1

I’ve found you only need to delay at least 100us for the controller to finish driving the first row. So I delay 150us to be safe. I haven’t implemented that delay by changing timer counters on the fly like you did, but that definitely could be done too to save power.

Also, this means the frame_counter interval is one-sided: mine is now [386, inf). The only upper limit is when strobing becomes visible.

The result is a 128x62 grayscale image. :smiley: I gave up trying to get 128x63 – it seems it should be possible, but I kept getting pixel bleeding one way or the other and gave up. Hopefully we can get that last row out later.

Updated gist.


How difficult would this be to implement into existing games, like [GAME] Sensitive for example?

1 Like

It shouldn’t be too difficult.

It would be great to see 'Sensitive` ported to this new method! I think it’d be a nice test/ demo for the approach :slight_smile:
Update: Having just replayed the game, I think the strobing could be greatly improved… is the source in a Git repository somewhere?

1 Like


Yes, that’s a great insight! It is both simple and explains some apparent inconsistencies I have observed when testing.

I have updated my gist to use your finding. Mine works with 128x63 and no artifacts.

What I am doing is interrupt 3 times per frame:

VBLANK -> set mux ratio to 1
Line 1
Line 2
[... controller keeps drawing lines ... ]
Line 64-K -> start pushing GDDRAM update to the controller (K = 8 right now)
Line 64 -> refresh is locked by the controller
[... time elapses while locked on line 64 "absorbing" any extra frame time ...]
Line 64 -> set mux ratio to 64

The reason for starting display memory update a bit before line 64 is so that it completes while the controller is spinning on line 64. This fixed some update artifacts appearing on the first (or first few lines) on top of the screen.

I found the interval between setting mux to 63 and then back to 1 to be critical and short. I think I am going to try updating my gist to interrupt just twice and send NOPs to the display controller between MUX commands in order to control the interval between them.

Also code and docs updates are still pending. I think adding the animation will be interesting because we’ll find out how it looks when things are moving. My feeling from previous attempts at this is that we’ll need to up the display Fosc to the max and probably stick to the 2-frames option.

EDIT 1: I have also increased the timer clock speed by setting the prescaler to 64 instead of 256.

1 Like

I had a try at getting GitHub - spinalcode/arduboy_sensitive to work with this method. There is no flickering, which is great! but the screen is not as bright, perhaps I messed something?

The only anomaly on the screen, is that a line a couple of lines up from the bottom is far brighter than the others, I am more than happy to edit the font to fix that, as I only use that part of the screen for text.


See my PR

As for brightness I tried increasing contrast which may help. Increasing the compile time frame rate will make the image more stable and probably improve contrast but may not work on some Arduboys under some conditions. Once we figure out a safe minimum value that works with most/all Arduboys we can update the source code.

1 Like