-
-
Save dxxb/cdb7395c834f2fdfe8c7fcd94c5ced95 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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