Last active
August 31, 2022 08:47
-
-
Save dxxb/989ae92ab8e7637c22a639ef98c8f9e6 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
/* | |
* Technique based on <https://github.com/Timendus/thumby-grayscale/blob/main/lib/thumbyGrayscale.py> | |
* Implementation derived from <https://gist.github.com/tiberiusbrown/6da1a02d06f263efb796103154e6b0f6> | |
* | |
*/ | |
/* | |
The technique used by the Thumby to achieve grayscale on SSD1306 without FR pin is based on | |
temporarily setting the mux ration to 1 (0xA8, 0x00). The assumption seems to be that reducing the | |
mux ratio to 1 will lock the screen refresh at the beginning of the frame until released by setting | |
the mux ratio to actual number of lines to be refreshed. | |
I believe the assumption above does not hold in general, and that the locking occurs only if the controller | |
enters VBLANK while the mux ratio is set to 1. | |
The timer interrupt handler in this example was used to test various combinations of timing and settings | |
to try and approximate a model the SSD1306 behaviour in relation to this mux ratio hack. What I found so far: | |
1. Mux ratio takes affect on line change. | |
2. Setting a mux ratio of 1 does not make the line counter jump to the first/last or VBLANK per-se. | |
3. There is a pause between frames probably corresponding to FR pin assert i.e. VBLANK | |
(see 8.4 FR synchronization in the SD1306 datasheet). The VBLANK gap duration may be equivalent | |
to 1 page (i.e. 8 rows) worth of display refresh time. | |
4. Mux ratio gate [...] | |
Note 1: the timer is setup in Fast PWM mode so the COMPA register is double buffered. This means the timer interval | |
set at any given step takes effect after the current timer period expires. | |
*/ | |
#include <Arduboy2.h> | |
// 0: 4-level in 2 frames using contrast (slight noise at border between 01 and 10) | |
static constexpr uint8_t FBW = 128; | |
static constexpr uint8_t FBH = 64; | |
static constexpr uint8_t FBR = FBH / 8; | |
// 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)/(120-1)); | |
static uint16_t line_duration; | |
#define SCREEN_UPLOAD_DURATION ((F_CPU/64)/((F_CPU/2)/(128*8*8))) | |
static void send_cmds(uint8_t const* d, uint8_t n) | |
{ | |
Arduboy2::LCDCommandMode(); | |
while(n-- != 0) | |
Arduboy2::SPItransfer(*d++); | |
Arduboy2::LCDDataMode(); | |
} | |
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(); | |
} | |
static uint8_t bufs[2][FBH * FBW / 8]; | |
void paint(uint8_t image[], bool clear) | |
{ | |
uint16_t count; | |
asm volatile ( | |
" ldi %A[count], %[len_lsb] \n\t" //for (len = WIDTH * HEIGHT / 8) | |
" ldi %B[count], %[len_msb] \n\t" | |
"1: ld __tmp_reg__, %a[ptr] ;2 \n\t" //tmp = *(image) | |
" out %[spdr], __tmp_reg__ ;1 \n\t" //SPDR = tmp | |
" cpse %[clear], __zero_reg__ ;1/2 \n\t" //if (clear) tmp = 0; | |
" mov __tmp_reg__, __zero_reg__ ;1 \n\t" | |
"2: sbiw %A[count], 1 ;2 \n\t" //len -- | |
" sbrc %A[count], 0 ;1/2 \n\t" //loop twice for cheap delay | |
" rjmp 2b ;2 \n\t" | |
" st %a[ptr]+, __tmp_reg__ ;2 \n\t" //*(image++) = tmp | |
" 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)), | |
[len_msb] "M" ((FBR * FBW * 2) >> 8), // 8: pixels per byte | |
[len_lsb] "M" ((FBR * FBW * 2) & 0xFF), // 2: for delay loop multiplier | |
[clear] "r" (clear) | |
); | |
} | |
void setup() | |
{ | |
Arduboy2::boot(); | |
Arduboy2::paintScreen(&bufs[0][0], false); // clear controller RAM | |
Arduboy2::flashlight(); | |
Arduboy2::waitNoButtons(); | |
static uint8_t const SETUP_CMDS[] PROGMEM = | |
{ | |
0xA8, 0x00, | |
0x7F, | |
//0xD3, 01, | |
//0xC0, 0xA0, | |
0xD9, 0x22, | |
0xD5, 0xF0, | |
}; | |
send_cmds_prog(SETUP_CMDS, sizeof(SETUP_CMDS)); | |
// disable timer0 overflow ISR (cannot use micros/millis/delay) | |
bitWrite(TIMSK0, TOIE0, 0); | |
// Setup frame timer | |
#if 0 | |
OCR3A = (timer_counter >> 4) + 1; | |
TCCR3A = _BV(WGM31) | _BV(WGM30); // Fast PWM mode, no output pin toggling | |
TCCR3B = _BV(WGM33) | _BV(WGM32) | _BV(CS31) | _BV(CS30); // Fast PWM mode, prescaler /64 | |
#else | |
OCR3A = timer_counter; | |
TCCR3A = 0; // CTC mode, no output pin toggling | |
TCCR3B = _BV(WGM32) | _BV(CS31) | _BV(CS30); // CTC mode, prescaler /64 | |
#endif | |
TCNT3 = 0; // Reset the counter manually to avoid waitiing for overflow | |
bitWrite(TIMSK3, OCIE3A, 1); | |
line_duration = (timer_counter >> 6) + 1; | |
// Render and push something | |
game_render(); | |
paint(bufs[1], false); | |
} | |
// this method needs to be as fast as possible (less than ~7ms) | |
static void game_render() | |
{ | |
memset(&bufs[0][0], 0, sizeof(bufs)); | |
// render | |
for(uint8_t r = 0; r < (FBR-1); ++r) | |
{ | |
for(uint8_t c = 0; c < 32; ++c) | |
{ | |
bufs[0][c + FBW * r] = 0xff; | |
bufs[1][c + FBW * r] = 0xff; | |
bufs[0][c + FBW * r + 32] = 0xff; | |
bufs[1][c + FBW * r + 64] = 0xff; | |
} | |
} | |
for(uint8_t c = 0; c < 32; ++c) { | |
bufs[0][c + FBW * 7] = 0x7F; | |
bufs[1][c + FBW * 7] = 0x7F; | |
bufs[0][c + FBW * 7 + 32] = 0x7F; | |
bufs[1][c + FBW * 7 + 64] = 0x7F; | |
} | |
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, | |
}; | |
for(uint8_t r = 0, c = 100; r < 8; ++r) { | |
for(uint8_t j = 0; j < 3; ++j) { | |
bufs[0][FBW * r + 100 + j] = pgm_read_byte(&DIGITS[r * 3 + j]); | |
bufs[1][FBW * r + 100 + j] = pgm_read_byte(&DIGITS[r * 3 + j]); | |
} | |
} | |
uint16_t t = timer_counter; | |
for(uint8_t i = 0, x = 123; i < 4 && t != 0; ++i, x -= 4) | |
{ | |
uint8_t d = t % 10; | |
t /= 10; | |
for(uint8_t j = 0; j < 3; ++j) | |
bufs[0][FBW + x + j] = bufs[1][FBW + x + j] = pgm_read_byte(&DIGITS[d * 3 + j]); | |
} | |
} | |
static void game_update() | |
{ | |
uint8_t b = Arduboy2::buttonsState(); | |
if(b & (UP_BUTTON)) | |
++timer_counter; | |
if(b & (DOWN_BUTTON)) | |
--timer_counter; | |
line_duration = (timer_counter >> 6) + 1; | |
} | |
static uint8_t n = 0; | |
static uint8_t rf = 0; | |
void loop() | |
{ | |
while(true) { | |
if (WDTCSR & _BV(WDE)) | |
{ | |
// disable ints and set magic key | |
cli(); | |
*(volatile uint8_t*)0x800 = 0x77; | |
*(volatile uint8_t*)0x801 = 0x77; | |
for(;;); | |
} | |
// timer3 will still wake CPU | |
Arduboy2::idle(); | |
if (rf) { | |
rf = 0; | |
paint(bufs[n & 1], false); | |
if((n & 3) == 3) | |
game_update(); | |
if((n & 3) == 0) | |
game_render(); | |
} | |
} | |
} | |
void timer_irq_handler_3phases(void) | |
{ | |
static int8_t ph = 0; | |
static uint8_t const PRE_CMDS[] PROGMEM = { 0xA8, 0}; | |
static uint8_t const POST_CMDS0[] PROGMEM = { 0x81, 0xf0, 0xA8, 63, }; | |
static uint8_t const POST_CMDS1[] PROGMEM = { 0x81, 0x70, 0xA8, 63, }; | |
if (ph == 0) { | |
OCR3A = 25; | |
++n; | |
rf = 1; | |
} else if (ph == 1) { | |
OCR3A = timer_counter-SCREEN_UPLOAD_DURATION; | |
if (n & 1) | |
send_cmds_prog(POST_CMDS1, sizeof(POST_CMDS1)); | |
else | |
send_cmds_prog(POST_CMDS0, sizeof(POST_CMDS0)); | |
} else { | |
OCR3A = SCREEN_UPLOAD_DURATION; | |
send_cmds_prog(PRE_CMDS, sizeof(PRE_CMDS)); | |
ph = -1; | |
} | |
++ph; | |
} | |
void timer_irq_handler_2phases(void) | |
{ | |
static uint8_t ph = 0; | |
static uint8_t const PRE_CMDS[] PROGMEM = { 0xA8, 0}; | |
static uint8_t const POST_CMDS0[] PROGMEM = { 0x81, 0xFF, 0xA8, 63, }; | |
static uint8_t const POST_CMDS1[] PROGMEM = { 0x81, 0x80, 0xA8, 63, }; | |
TCCR3B = 0; // Stop Timer | |
TCNT3 = 0; // Reset the counter | |
if ((ph & 1) == 0) { | |
OCR3A = 25; | |
++n; | |
rf = 1; | |
if (n & 1) | |
send_cmds_prog(POST_CMDS1, sizeof(POST_CMDS1)); | |
else | |
send_cmds_prog(POST_CMDS0, sizeof(POST_CMDS0)); | |
} else { | |
send_cmds_prog(PRE_CMDS, sizeof(PRE_CMDS)); | |
OCR3A = timer_counter; | |
} | |
TCCR3B = _BV(WGM32) | _BV(CS31) | _BV(CS30); // restart timer, prescaler /64 | |
++ph; | |
} | |
void timer_irq_handler_1phase(void) | |
{ | |
static uint8_t const CONTRAST_CMDS0[] PROGMEM = { 0x81, 0xFF, }; | |
static uint8_t const CONTRAST_CMDS1[] PROGMEM = { 0x81, 0x80, }; | |
static uint8_t const LOCK_CMDS0[] PROGMEM = { 0xA8, 63, | |
0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, | |
0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, | |
0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, | |
0xE3, 0xE3, 0xE3, 0xE3, 0xE3, | |
0xE3, 0xE3, | |
0xA8, 0}; | |
TCCR3B = 0; // Stop Timer | |
TCNT3 = 0; // Reset the counter | |
OCR3A = timer_counter; | |
++n; | |
rf = 1; | |
if (n & 1) | |
send_cmds_prog(CONTRAST_CMDS1, sizeof(CONTRAST_CMDS1)); | |
else | |
send_cmds_prog(CONTRAST_CMDS0, sizeof(CONTRAST_CMDS0)); | |
uint8_t oldSREG = SREG; | |
cli(); | |
send_cmds_prog(LOCK_CMDS0, sizeof(LOCK_CMDS0)); | |
SREG = oldSREG; | |
TCCR3B = _BV(WGM32) | _BV(CS31) | _BV(CS30); // restart timer, prescaler /64 | |
} | |
ISR(TIMER3_COMPA_vect) | |
{ | |
timer_irq_handler_1phase(); | |
//timer_irq_handler_2phases(); | |
//timer_irq_handler_3phases(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment