fix(sim): SSD1306 page-addressing mode (Tiny4kOLED/U8g2 OLED garbled)

SSD1306Core only handled horizontal/vertical addressing (0x20/0x21/0x22) and
defaulted memMode to horizontal. Page-mode drivers (Tiny4kOLED on ATtiny85,
U8g2 page buffer, classic SSD1306 libs) position the cursor with the single-byte
commands 0xB0-0xB7 (page) and 0x00-0x0F / 0x10-0x1F (column nibbles) and rely on
the SSD1306 power-on default of PAGE addressing — they never send 0x20. velxio
ignored those cursor commands and advanced in horizontal mode, so every setCursor
was a no-op and the hatching/border/text piled onto wrong rows -> garbled display.
Fix: default memMode=2 (datasheet power-on) and handle the page/column-set
commands. Adafruit_SSD1306 still works (it sends 0x20,0x00 + 0x21/0x22 explicitly).
Verified: decoded the real ATTinyCore Tiny4kOLED I2C stream renders a clean
border + '128x64'. Adds a page-addressing render test.
This commit is contained in:
David Montero 2026-06-19 18:08:01 +02:00
parent 37b3fac978
commit 8c32b16589
2 changed files with 49 additions and 2 deletions

View File

@ -169,6 +169,36 @@ describe('SSD1306 — ImageData rendering (syncElement fix)', () => {
expect(px[idxOff + 2]).toBe(0);
});
it('page addressing (Tiny4kOLED): 0xB0+page / 0x00-0x1F col, no 0x20, cursor persists across data streams', () => {
const el = makeOLEDElement();
const sim = makeSim();
PartSimulationRegistry.get('ssd1306')!.attachEvents!(el, sim as any, () => null);
const device = sim._devices[0];
// Page-addressing setCursor: page 1, column 8 (col high nibble = 0x10,
// col low nibble = 0x08). No 0x20 — relies on the power-on page-mode
// default that Tiny4kOLED / U8g2-page / classic SSD1306 drivers assume.
sendCommandStream(device, [0xb1, 0x10, 0x08]);
// TinyWireM flushes its small buffer as distinct 16-byte I2C transactions,
// so the column pointer MUST persist across separate data streams.
sendDataStream(device, [0xff, 0x00]); // col 8, 9
sendDataStream(device, [0x00, 0xff]); // col 10, 11 — cursor continued
const px = el.imageData.data;
const lit = (row: number, col: number) => {
const i = (row * 128 + col) * 4;
return px[i] + px[i + 1] + px[i + 2] > 0;
};
// page 1 → rows 8..15; 0xff lights the whole 8-pixel column.
expect(lit(8, 8)).toBe(true);
expect(lit(15, 8)).toBe(true);
expect(lit(8, 9)).toBe(false); // 0x00
expect(lit(8, 10)).toBe(false); // 0x00 (start of 2nd stream)
// col 11 lit proves the cursor advanced across the STOP/new transaction.
expect(lit(8, 11)).toBe(true);
expect(lit(15, 11)).toBe(true);
});
it('fills all 1024 GDDRAM bytes via horizontal addressing', () => {
const el = makeOLEDElement();
const sim = makeSim();

View File

@ -64,7 +64,12 @@ class SSD1306Core {
private colEnd = 127;
private pageStart = 0;
private pageEnd = 7;
private memMode = 0; // 0=horizontal, 1=vertical, 2=page
// 0=horizontal, 1=vertical, 2=page. SSD1306 power-on default is PAGE
// addressing (datasheet 10b). Adafruit_SSD1306 overrides it to horizontal
// via 0x20,0x00; page-mode drivers (Tiny4kOLED, U8g2 page buffer) rely on
// this default and never send 0x20 — so the default MUST be 2 or their
// setCursor (0xB0-0xB7 + 0x00-0x1F) renders garbled.
private memMode = 2;
// Multi-byte command accumulation
private cmdBuf: number[] = [];
@ -125,7 +130,19 @@ class SSD1306Core {
this.page = this.pageStart;
break;
default:
if (cmd >= 0x40 && cmd <= 0x7f) {
// Page-addressing-mode cursor commands (single-byte). Used by
// Tiny4kOLED / U8g2 page buffer / classic SSD1306 drivers whose
// setCursor() does NOT use the 0x21/0x22 column/page-range commands.
if (cmd >= 0xb0 && cmd <= 0xb7) {
// set page start address (B0..B7 → page 0..7)
this.page = cmd & 0x07;
} else if (cmd <= 0x0f) {
// set lower column nibble (0x00..0x0F)
this.col = (this.col & 0xf0) | (cmd & 0x0f);
} else if (cmd >= 0x10 && cmd <= 0x1f) {
// set higher column nibble (0x10..0x1F)
this.col = (this.col & 0x0f) | ((cmd & 0x0f) << 4);
} else if (cmd >= 0x40 && cmd <= 0x7f) {
/* display start line — visual, skip */
}
break;