Fix SSD1306 128x32: wrong column-start address - #707
Conversation
Fixes electro-smith#634. SSD130xDriver::Update() (and the equivalent switches in SSD1307Driver) set the page column-start command to 0x12 (column 32) for height==32 panels instead of the standard 0x10 (column 0) used by the default case and every reference SSD1306 driver. Every page write started 32 columns in: the intended content shifted 32 columns right, and the last 32 columns wrapped back around to the left edge, garbling whatever was drawn there. Confirmed independently by three reports on electro-smith#634 (including a maintainer) against SSD130x4WireSpi128x32Driver; the same class also backs the I2C 128x32 alias. Also fixes the same line in SSD1307Driver::TransferPageDma() for consistency, though no public alias currently instantiates that class at height==32.
SSD130xI2CTransport::SendData() issued a full I2C transaction (START+address+ACK+STOP) per byte -- 512 transactions for one full 128x32 frame (4 pages x 128 bytes), plus ~12 more for per-page setup commands. The SSD1306 auto-increments its column pointer for every data byte that follows a single 0x40 prefix within a transaction, so there's no need to restart the bus per byte. Batches a page's data into one transaction (page bytes + the 0x40 prefix); measured on a 128x32 panel, this cut a full-screen Update() from ~30-40ms to a few ms. Separate from the column-start fix in the previous commit -- this is a transport-layer performance change, not a correctness fix, so it's easy to drop if unwanted.
|
I was made aware that creating a PR using LLM made fixes is not the best route. I'll try and add things like this in less direct ways like raising an issue / ticket in the future. Please do close this PR if it's considered bad practice. I'm not looking for credits, just wanted to share / give back what i thought was a small fix. |
|
My initial hesitance to patch this when #634 was created was because a device (bluemchen, I believe -- a reprogrammable 4hp eurorack module with small OLED was the initial project that added the explicitly different column-start. Given that this PR ends up making the The burst write looks like a nice improvement. I don't have an I2C OLED to test with at the moment, but it looks okay to me. Re. LLM usage: I don't have any particular issue with contributors using LLMs to help them fix problems, write code, etc. |
Replaces the flat 0x10 from the previous commit, per review feedback: key the offset on width as well as height, and expose it so panels that map differently can be handled without editing the library. The 0x12 came in with electro-smith#326, whose author states the motivation was the kxmx_bluemchen's 64x32 I2C SSD1306. That is correct for that panel -- 64-column modules are wired to the middle 64 columns of the controller's 128-column RAM, so they need a 32-column offset (0x10 | 32 >> 4 == 0x12). The bug is only that the switch keys on height alone, so 128x32 panels got the offset too. Init() already distinguishes the two inside case 32: via `if(width == 64)` for the COM-pins command; the column-start line never got the same treatment. So the default is now geometry-derived -- 0x12 only for 64x32, 0x10 otherwise -- and lives in Config, overridable per instance. Config stays an aggregate under gnu++14 and every in-tree call site default-constructs then assigns, so no existing code changes. Applied to SSD1307Driver's Update() and TransferPageDma() too, keeping both drivers configured the same way, though no SSD1307 alias is 64x32 today. Tested on a 128x32 I2C panel: the default renders correctly, and forcing high_column_addr = 0x12 reproduces the 32-column shift and wrap from electro-smith#634 exactly -- which is also what confirms the override reaches the panel. No 64x32 hardware here to verify bluemchen directly, which is why the default is derived from geometry rather than flattened to 0x10. Co-Authored-By: Claude Opus 5 <[email protected]>
|
I've spent some more time with Claude going through this. Built a test to verify. Here's the summary: Looks like you were right to hesitate. I traced that line back to the commit that added it: it came in with #326, and that PR's description gives the reason:
So 0x12 is correct for bluemchen, and my first patch would have indeed broken it.
So the new commit does both things you asked for: the default is derived from geometry (0x12 only for 64x32, 0x10 otherwise) and it lives in Config, overridable per instance without editing the library.
Tested on a 128x32 I2C panel, using the stock SSD130xI2c128x32Driver:
I don't have a 64x32 , or any other OLEDs for that matter, to verify e.g. bluemchen directly, which is precisely why the default is derived from geometry rather than flattened to 0x10. The burst write is exercised on the same hardware by the firmware this came out of. Two things Claude found but I deliberately left this alone, your call on both:
No one seems to have reported it, so I'd rather not change untested behaviour on a guess rather just mention it. Re. LLM usage, appreciated, thanks for saying so. Still, I'm out of my comfort zone, and I hope I was thorough enough with the tests to substantiate the PR. |

Fixes #634.
SSD130xDriver::Update()(and the equivalent switches inSSD1307Driver) set the page column-start command to0x12(column 32) forheight==32panels instead of the standard0x10(column 0) used by thedefaultcase and every reference SSD1306 driver. Every page write started 32 columns in: the intended content shifted 32 columns right, and the last 32 columns wrapped back around to the left edge, garbling whatever was drawn there.This is the same root cause @Len42 tracked down in #634 (
SSD130x4WireSpi128x32Driveris an alias for this sameSSD130xDriverclass, just over SPI instead of I2C) and that @stephenhensley confirmed hitting independently. Also fixes the same line inSSD1307Driver::TransferPageDma()for consistency, though no public alias currently instantiates that class atheight==32.Second commit is a separate, unrelated perf change (batches SSD1306 I2C page writes into one transaction instead of one per byte) — happy to drop it if you'd rather keep this PR scoped to just the bug fix.