Greyscale for arduboy

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

OK… this took a long time to get working, but I think I’m getting closer to a good method now. It can drive all 64 rows of grayscale with a generous /64 counter interval: [1560, 1999] for me.

The key discovery was that the Arduboy’s screen is upside down, and the default configuration is to remap both segments and COMs to rotate the image. This is an issue: changing the mux ratio when COMs are remapped also changes the vertical shift! This is why I couldn’t get 63 rows and why you found that the timing window is so tight: changing the mux ratio from 1 to 64 to 1 shifts the display by one row each time so you have be so quick that the controller doesn’t have time to drive that neighboring row (as you found) or keep the neighboring row cleared as well (as I did).

The new approach reverts the orientation to normal and instead transmits the image upside down. It was barely possible to do this while maintaining the 18-cycle cadence of the paintScreen loop from the Arduboy2 library. I had to spend 256 bytes on a bit-reversal LUT.

It’s worth it, though: being able to change the mux ratio arbitrarily without resulting in vertical shift allows for the trick used to drive all 64 rows.

Updated gist. I cleaned it up, added attributions and a detailed description of the method, and changed to your timer setup (/64 fast PWM). The display commands/data can be sent either entirely in the ISR or in the loop method (I still want the former to remain available because in ArduChess it allows the AI to think “in the background” while still rendering and responding to input).


Hi @brow1067 , this sounds great! I am going to try it out as soon as I can.

I haven’t looked closely at what your code does yet, but I think inverting DORD (bit 5) of the SPCR register (see "17.2.1 SPI Control Register – SPCR) will reverse bits for you.

EDIT 1: So to recap: set COM to normal to avoid row shift (need to send bits out in reverse order to compensate) and zero out rows at the right time so the bright line at the bottom does not occur. Is this correct?

EDIT 2: I am not 100% sure I understand all the implications but I think we should be able to interrupt just twice per frame and overwrite a single page only once with a mask to avoid the bright line.

1 Like

Perfect, I didn’t know about this at all! Makes things much easier – gist is updated.

The procedure is to do a short “dummy frame” (that doesn’t display anything) with mux ratio 8, followed by the real frame of 64 rows. The three interrupts are:

  1. Start of frame: set mux ratio to 8 to kick off dummy frame.
  2. Write rows 0-3 when the controller reaches rows 4-7 in the dummy frame.
  3. Write rows 0-7 when the controller reaches rows 0-3 in the real frame (and set mux ratio to 1 to allow it to continue on).

I found that the timing gap between frames was fairly short, possibly less than a row time, because delaying 4 rows between steps 2 and 3 was enough. (EDIT: if I were to guess, there is actually no gap at all, and the FR signal is high while driving the last row of a frame)

At any rate, I do think/hope this can be improved on further. Currently, this approach negatively impacts frame time by driving 72 rows instead of 64 each frame. It is possible to go down to only 4 extra rows (by writing 2 rows in step 2 instead of 4) but this narrows the window for the timer counter considerably.

I just added music back into Sensitive but it plays very slowly. I assume the timer fr the screen is interfearing with the ATMlib timer?

I also gave this teatment a try on my Tiny Blocks game, similar results to the first Sensitive test, low contrast and super bright line at the bottom.

1 Like

I think so, yes. ATMlib fires off its ISR every 256 cycles, resulting in its ISR being called 62,500 times per second. Most times I think its ISR does nothing but track timing and then occasionally it calls another method to advance the next bit of the song. Because of this, if there’s other ISRs taking a long time causing ATMlib to skip a bunch of those 62.5 kHz interrupts, it’ll track timing more slowly, and the song progresses more slowly.

This is especially noticeable because in my older code the frame ISR does all the display work inside itself. As @dxb did, you can instead make that work happen in the main loop and use the ISR only for timing, and then I imagine the song speed would improve.

My latest gist also supports updating the display outside the ISR now, but I want to work on a more user-friendly implementation that’s easier to plug in to a project.

@brow1067, I have forked your gist and modified it to require a single interrupt per frame. I also updated the computation of the frame duration to be based on minimum supported frame rate.

Instead of racing around the update row I am slowing down and lengthening line duration while disabling the charge pump to minimize “bright pixels” on the first line.

The first line (at the bottom) doesn’t always look perfect but IMHO the slight imperfection is an acceptable trade-off.

I had tried this before with remapped COM lines and didn’t succeed. I will probably give it a go again to see if I can obtain the same result as this one.

1 Like