Skip to content

Instantly share code, notes, and snippets.

@dxxb
Last active August 31, 2022 08:47
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/989ae92ab8e7637c22a639ef98c8f9e6 to your computer and use it in GitHub Desktop.
Save dxxb/989ae92ab8e7637c22a639ef98c8f9e6 to your computer and use it in GitHub Desktop.
/*
* 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