Skip to content

Instantly share code, notes, and snippets.

@dxxb
Forked from tiberiusbrown/grayscale.ino
Last active August 31, 2022 21:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dxxb/cdb7395c834f2fdfe8c7fcd94c5ced95 to your computer and use it in GitHub Desktop.
Save dxxb/cdb7395c834f2fdfe8c7fcd94c5ced95 to your computer and use it in GitHub Desktop.
/*
Arduboy 128x64 grayscale proof of concept
Inspired by Thumby method of setting mux ratio to 1 to sync frames:
https://github.com/Timendus/thumby-grayscale
Relevant Arduboy forum discussion:
https://community.arduboy.com/t/greyscale-for-arduboy/3032/40
Thanks to @dxb for extremely helpful discussion and hints:
https://gist.github.com/dxxb/989ae92ab8e7637c22a639ef98c8f9e6
Rendering Modes
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 but 3 levels instead of 4
Controls
A/B cycle grayscale drawing method 0/1/2
Left/Right decrease/increase frame time interrupt counter
Up/Down increment/decrement frame time interrupt counter
Method Description
At the start of each frame, the controller is parked at row 0,
page 0 is cleared, and pages 1-7 have the previous frame's data.
--> .............................. page 0 rows 0-3
.............................. page 0 rows 4-7
?????????????????????????????? page 1
?????????????????????????????? page 2
?????????????????????????????? page 3
?????????????????????????????? page 4
?????????????????????????????? page 5
?????????????????????????????? page 6
?????????????????????????????? page 7
Set the mux ratio to 8 and wait for the controller to advance
by 4 rows. Then write rows 0-3.
Note: you can do a shorter loop here instead of 8 rows,
but the timing window quickly becomes hard to hit and
the acceptable frame time interval narrows.
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 0 rows 0-3
--> .............................. page 0 rows 4-7
?????????????????????????????? page 1
?????????????????????????????? page 2
?????????????????????????????? page 3
?????????????????????????????? page 4
?????????????????????????????? page 5
?????????????????????????????? page 6
?????????????????????????????? page 7
Wait for another 4 rows. By this time, the controller has wrapped
around to rows 0-3. Write rows 0-7 now -- the rows the controller
is currently driving do not change and rows 4-7 will be ready.
Then, set the mux ratio to 1. After the previous write, the
controller will have advanced beyond row 0, so this is safe.
--> XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 0 rows 0-3
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 0 rows 4-7
?????????????????????????????? page 1
?????????????????????????????? page 2
?????????????????????????????? page 3
?????????????????????????????? page 4
?????????????????????????????? page 5
?????????????????????????????? page 6
?????????????????????????????? page 7
Write rows 8-63.
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 0 rows 0-3
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 0 rows 4-7
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 1
--> XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 2
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 3
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 4
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 5
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 6
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 7
Zero rows 0-7. After the previous write, the controller will have
advanced beyond row 7, so this is safe.
.............................. page 0 rows 0-3
.............................. page 0 rows 4-7
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 1
--> XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 2
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 3
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 4
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 5
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 6
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX page 7
Wait for the frame to complete and the controller to park at row 0.
During this time you can do game logic and render the next frame.
Notes
This approach requires being able to change the mux ratio as the
controller is actively driving the display. This is normally fine,
but if the display is configured with remapped COM, then changing
the mux ratio results in an immediate vertical shift! (Table 10-2
in the datasheet) So the COM must be configured as normal.
Unfortunately, the Arduboy's display is upside-down, so the image
data needs to be sent upside-down for it to display properly.
Additionally, the first page transfer needs to be masked to only
send rows 0-3.
This is accomplished with a modification of the Arduboy2 library's
paintScreen method, which uses a tight assembly loop at an 18-cycle
cadence to avoid having to poll SPSR. The new loop iterates in the
reverse direction, masks the bits in each byte, and reverses the
bit order using the DORD bit of SPCR.
There remains a small artifact: the park position (bottom row) is
ever so slightly dimmer than the other rows. I think this may be
due to it being driven to black many times while the controller is
parked between frames.
*/
#include <Arduboy2.h>
// 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 but 3 levels instead of 4)
static uint8_t grayscale_option = 1;
// whether to send display commands/updates from interrupt handler
#define DO_PHASE_WORK_IN_INTERRUPT 1
static constexpr uint8_t FBW = 128;
static constexpr uint8_t FBH = 64;
// Support a minimum panel refresh rate of 120Hz
// This also effectively limits the refresh rate to 120Hz because the controller is not allowed to start
// a new frame faster than the period we set.
static uint16_t timer_counter = ((F_CPU/64)/(135-1));
static void send_cmds(uint8_t const* d, uint8_t n)
{
Arduboy2::LCDCommandMode();
while(n-- != 0)
Arduboy2::SPItransfer(*d++);
Arduboy2::LCDDataMode();
}
#define SEND_CMDS(...) \
do { \
uint8_t const CMDS_[] = { __VA_ARGS__ }; \
send_cmds(CMDS_, sizeof(CMDS_)); \
} while(0)
static void send_cmds_prog(uint8_t const* d, uint8_t n)
{
Arduboy2::LCDCommandMode();
while(n-- != 0)
Arduboy2::SPItransfer(pgm_read_byte(d++));
Arduboy2::LCDDataMode();
}
#define SEND_CMDS_PROG(...) \
do { \
static uint8_t const CMDS_[] PROGMEM = { __VA_ARGS__ }; \
send_cmds_prog(CMDS_, sizeof(CMDS_)); \
} while(0)
static uint8_t bufs[2][FBH * FBW / 8];
static void paint(uint8_t* image, bool clear, uint8_t pages, uint8_t mask)
{
uint16_t count = pages * FBW;
#if 1
// rotated orientation
SPCR = _BV(SPE) | _BV(MSTR) | _BV(DORD); // MSB-to-LSB
image += count;
asm volatile (
"1: ld __tmp_reg__, -%a[ptr] ;2 \n\t" //tmp = *(--image)
" cpse %[clear], __zero_reg__ ;1/2 \n\t" //if (clear) tmp = 0;
" mov __tmp_reg__, __zero_reg__ ;1 \n\t"
" st %a[ptr], __tmp_reg__ ;2 \n\t" //*(image) = tmp
" sbiw %a[count], 0 ;2 \n\t" //[delay]
" sbiw %a[count], 0 ;2 \n\t" //[delay]
" sbiw %a[count], 0 ;2 \n\t" //[delay]
" and __tmp_reg__, %[mask] ;1 \n\t" //tmp &= mask
" out %[spdr], __tmp_reg__ ;1 \n\t" //SPDR = tmp
" sbiw %a[count], 1 ;2 \n\t" //len--
" brne 1b ;1/2 :18 \n\t" //len > 0
" in __tmp_reg__, %[spsr] \n\t" //read SPSR to clear SPIF
" sbiw %a[count], 0 \n\t"
" sbiw %a[count], 0 \n\t" // delay before resetting DORD
" sbiw %a[count], 0 \n\t" // below so it doesn't mess up
" sbiw %a[count], 0 \n\t" // the last transfer
" sbiw %a[count], 0 \n\t"
: [ptr] "+&e" (image),
[count] "+&w" (count)
: [spdr] "I" (_SFR_IO_ADDR(SPDR)),
[spsr] "I" (_SFR_IO_ADDR(SPSR)),
[clear] "r" (clear),
[mask] "r" (mask)
);
SPCR = _BV(SPE) | _BV(MSTR); // LSB-to-MSB
#else
// normal orientation
asm volatile (
"1: ld __tmp_reg__, %a[ptr] ;2 \n\t" //tmp = *(image)
" cpse %[clear], __zero_reg__ ;1/2 \n\t" //if (clear) tmp = 0;
" mov __tmp_reg__, __zero_reg__ ;1 \n\t"
" st %a[ptr]+, __tmp_reg__ ;2 \n\t" //*(image++) = tmp
" sbiw %a[count], 0 ;2 \n\t" //[delay]
" sbiw %a[count], 0 ;2 \n\t" //[delay]
" sbiw %a[count], 0 ;2 \n\t" //[delay]
" and __tmp_reg__, %[mask] ;1 \n\t" //tmp &= mask
" out %[spdr], __tmp_reg__ ;1 \n\t" //SPDR = tmp
" sbiw %a[count], 1 ;2 \n\t" //len--
" brne 1b ;1/2 :18 \n\t" //len > 0
" in __tmp_reg__, %[spsr] \n\t" //read SPSR to clear SPIF
: [ptr] "+&e" (image),
[count] "+&w" (count)
: [spdr] "I" (_SFR_IO_ADDR(SPDR)),
[spsr] "I" (_SFR_IO_ADDR(SPSR)),
[clear] "r" (clear),
[mask] "r" (mask)
);
#endif
}
void setup()
{
Arduboy2::boot();
paint(&bufs[0][0], true, 8, 0x00); // clear controller RAM
Arduboy2::flashlight();
Arduboy2::waitNoButtons();
SEND_CMDS_PROG(
0xC0, 0xA0, // reset to normal orientation
0xD9, 0x31, // 1-cycle discharge, 3-cycle charge
0xA8, 0, // park at row 0
);
// disable timer0 overflow ISR (cannot use micros/millis/delay)
bitWrite(TIMSK0, TOIE0, 0);
OCR3A = timer_counter;
TCCR3A = 0; // CTC mode, no output pin toggling
TCCR3B = _BV(WGM32) | _BV(CS31) | _BV(CS30); // CTC mode, prescaler /64
TCNT3 = 0; // Reset the counter manually to avoid waitiing for overflow
bitWrite(TIMSK3, OCIE3A, 1);
}
static void draw_digit(uint8_t x, uint8_t p, uint8_t d)
{
static uint8_t const DIGITS[10*3] PROGMEM =
{
0x0e, 0x11, 0x0e,
0x12, 0x1f, 0x10,
0x19, 0x15, 0x12,
0x11, 0x15, 0x0a,
0x07, 0x04, 0x1f,
0x17, 0x15, 0x09,
0x0e, 0x15, 0x08,
0x01, 0x19, 0x07,
0x0a, 0x15, 0x0a,
0x02, 0x15, 0x0e,
};
uint16_t j = FBW * p + x;
for(uint8_t i = 0; i < 3; ++i)
{
uint8_t t = pgm_read_byte(&DIGITS[d * 3 + i]);
bufs[0][j + i] = bufs[1][j + i] = t;
}
}
// this method needs to be as fast as possible (less than ~5-6ms)
static void game_render()
{
memset(&bufs[0][0], 0, sizeof(bufs));
// render
for(uint8_t r = 0; r < FBH / 8; ++r)
{
uint8_t pat = 0xff;
for(uint8_t c = 0; c < 12; ++c)
{
bufs[0][c + FBW * r] = pat;
bufs[1][c + FBW * r] = pat;
bufs[0][c + FBW * r + 12] = pat;
if(grayscale_option != 2)
bufs[1][c + FBW * r + 24] = pat;
}
uint8_t x = (grayscale_option == 2 ? 26 : 38);
bufs[0][FBW * r + x + 0] = bufs[1][FBW * r + x + 0] = 0x55;
bufs[0][FBW * r + x + 2] = bufs[1][FBW * r + x + 2] = 0x11;
bufs[0][FBW * r + x + 4] = bufs[1][FBW * r + x + 4] = 0x01;
draw_digit(x + 7, r, r);
}
draw_digit(70, 0, grayscale_option);
uint16_t t = timer_counter;
for(uint8_t i = 0, x = 100; i < 4 && t != 0; ++i, x -= 4)
{
uint8_t d = t % 10;
t /= 10;
draw_digit(x, 0, d);
}
// thumby girl sprite
static uint8_t const GIRL0[] PROGMEM = {
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,68,21,106,149,74,41,2,120,254,254,255,255,255,255,127,127,127,126,126,120,48,64,0,0,0,0,0,0,0,0,2,1,4,18,9,38,217,36,84,160,80,128,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,5,0,128,218,248,230,255,127,191,252,254,244,250,249,194,227,224,240,168,176,40,0,0,0,0,0,0,0,0,0,0,0,0,3,20,43,68,186,69,40,160,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,224,240,119,31,31,63,59,57,54,121,63,127,191,255,31,239,23,73,130,0,0,0,0,0,0,0,64,0,0,0,0,40,128,0,0,0,0,2,1,22,9,36,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,240,248,248,252,254,254,255,255,255,235,0,224,248,254,78,0,1,28,13,15,2,11,2,1,0,0,0,128,192,64,0,0,0,88,2,0,0,0,0,0,55,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,128,128,128,252,255,255,255,255,255,255,255,127,255,127,62,63,31,7,0,0,0,128,192,224,224,240,240,240,240,248,246,255,250,253,60,252,28,253,30,160,0,0,0,0,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
static uint8_t const GIRL1[] PROGMEM = {
128,4,160,8,130,32,8,162,0,40,130,8,160,10,64,39,53,187,234,149,106,181,214,253,135,1,81,44,210,40,74,180,192,40,129,1,135,207,191,254,232,218,144,96,1,131,7,13,30,59,237,246,217,38,219,171,94,173,106,225,148,64,8,130,32,8,162,0,40,2,144,4,
0,84,0,18,64,136,2,168,2,80,10,64,10,160,5,168,1,85,135,30,189,122,127,103,37,134,17,0,133,64,3,1,11,141,6,181,20,31,15,87,79,215,255,255,216,32,193,160,112,208,248,243,239,159,124,235,212,187,69,186,215,94,248,200,130,32,10,160,5,80,4,0,
0,149,32,133,80,8,66,40,2,169,4,81,8,162,8,146,36,129,40,130,104,213,74,136,224,224,192,68,198,201,134,192,137,64,2,224,16,232,182,125,255,255,255,255,107,206,63,191,222,127,71,241,215,127,244,0,3,15,61,126,233,246,219,255,16,74,0,74,32,5,168,1,
2,80,10,160,10,81,4,145,34,136,33,74,208,138,32,198,122,212,173,116,170,5,20,143,16,167,1,177,87,62,99,114,48,125,180,189,254,223,255,255,127,63,187,254,255,232,167,253,87,217,255,255,255,200,190,232,0,162,0,81,4,147,35,136,37,144,69,16,37,128,42,64,
160,21,64,20,66,17,136,34,200,146,196,145,195,215,168,127,138,117,10,37,138,20,128,197,195,32,88,151,33,212,201,226,200,245,234,226,255,254,167,73,0,5,2,195,11,243,2,225,95,223,191,255,127,255,250,255,122,136,66,41,4,80,10,32,138,32,74,17,68,18,64,21
};
for(uint8_t r = 0; r < 5; ++r)
{
for(uint8_t c = 0; c < 72; ++c)
{
bufs[0][FBW * 3 + 56 + c + FBW * r] = pgm_read_byte(&GIRL0[r*72+c]);
bufs[1][FBW * 3 + 56 + c + FBW * r] = pgm_read_byte(&GIRL1[r*72+c]);
}
}
if(grayscale_option == 2)
{
// change 0,1 frames to 1,0 frames
for(uint16_t i = 0; i < FBW * FBH / 8; ++i)
{
uint8_t b0 = bufs[0][i];
uint8_t b1 = bufs[1][i];
bufs[0][i] = b0 & b1;
bufs[1][i] = b0 | b1;
}
}
}
static void game_update()
{
static uint8_t pb = 0;
uint8_t b = Arduboy2::buttonsState();
uint8_t pr = b & ~pb;
if((pr & UP_BUTTON) | (b & RIGHT_BUTTON))
++timer_counter;
if((pr & DOWN_BUTTON) | (b & LEFT_BUTTON))
--timer_counter;
if((pr & (A_BUTTON | B_BUTTON)) && ++grayscale_option > 2)
grayscale_option = 0;
pb = b;
}
static bool do_work = false;
static uint8_t plane = 0;
static void do_phase_work()
{
uint8_t* b = bufs[plane & 1];
if(grayscale_option == 0)
SEND_CMDS(0x81, (plane & 1) ? 0x70 : 0xf0);
if(grayscale_option == 1)
SEND_CMDS_PROG(0x81, 0xf0);
{
uint8_t oldSREG = SREG;
cli();
// 1. Run the dot clock into the ground
// 2. Disable the charge pump
// 3. Make phase 1 and 2 very large
SEND_CMDS_PROG(0x22, 0, 7, 0x8D, 0x0, 0xD5, 0x0F, 0xD9, 0xFF);
paint(&b[FBW * 7], false, 1, 0xFF);
SEND_CMDS_PROG(0xA8, 63, 0x8D, 0x14, 0xD9, 0x31, 0xD5, 0xF0);
SREG = oldSREG;
}
SEND_CMDS_PROG(0x22, 1, 7);
paint(&b[FBW * 0], false, 7, 0xFF);
SEND_CMDS_PROG(0xA8, 0, 0x22, 0, 7);
paint(&b[FBW * 7], false, 1, 0x00);
if(plane == 0 && grayscale_option == 1)
{
// prepare bufs for second and third planes
for(uint16_t i = 0; i < FBW * FBH / 8; ++i)
{
uint8_t b0 = bufs[0][i];
uint8_t b1 = bufs[1][i];
bufs[0][i] = b0 & b1;
bufs[1][i] = b0 | b1;
}
}
if(grayscale_option == 1)
{
// 3-plane cycle
if(plane == 1) game_update();
if(plane == 2) game_render();
if(++plane >= 3) plane = 0;
}
else
{
// 2-plane cycle
if(plane == 0) game_update();
if(plane == 1) game_render();
plane = !plane;
}
}
void loop()
{
if (WDTCSR & _BV(WDE))
{
// disable ints and set magic key
cli();
*(volatile uint8_t*)0x800 = 0x77;
*(volatile uint8_t*)0x801 = 0x77;
for(;;);
}
// timer3 will wake CPU
Arduboy2::idle();
#if !DO_PHASE_WORK_IN_INTERRUPT
if(do_work)
{
do_work = false;
do_phase_work();
}
#endif
}
ISR(TIMER3_COMPA_vect)
{
OCR3A = timer_counter;
#if DO_PHASE_WORK_IN_INTERRUPT
do_phase_work();
#else
do_work = true;
#endif
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment