Undocumented grayscale mode on the ssd1306

this is how it looks with just the register:

there is also one more mode where it is 1/3 height, i guess it is 4 colors, but zoom does not work…(but i have not made many experiments, i found that register 24h ago)

Yeah so I mean I guess it would be more accurate that the controller is skipping every other pixel during the data it reads in.

these are the commands used to init the mode.
(unfortunatly the example is my whole graphics engine and i was implementing a vfd-emulator, so there is a lot of test code lying arround).
TestCmd(1); <—turns the mode on and off

the mode is stable for every brightness setting, also the charge pump settings and voltages do not influence the apperance. (at least on my ssd1306, and i got one report now of it working on another one)

You have that contact!? Please can you ask for the latest technical document covering the driving of the SSD1306. They ignored my request! Literally the whole interweb relies on an ancient PDF that Adafruit put out there.

There’s a lot of folklore around these hidden commands. Hoping a new PDF might illuminate them! (Or at least clarify some gaps in the old doc).

i have a newer datasheet, but do not know where i got it from.
SSD1306_neues_datasheet.pdf (1.8 MB)

but it does not contain anything usefull, except for these useless fading commands.

1 Like

I just used their contact form. I had a response back from them years ago when they told me that I needed to order a million units to schedule a meeting with them.

1 Like

…nice!! Will check it out. Please could you also add the pdf to your github repo?

That alone doesn’t produce greyscale, it merely stretches the pixels.

I don’t have a suitable camera on me to take a picture, so here’s some minimal code:

I’ve ported this and all it appears to do is to stretch the pixels vertically.

Not that it’ll work in the emulator, but for anyone who doesn’t want to compile the code, here’s a .hex:
GoergGrey.hex (17.9 KB)

If I include these, all I get is some incredibly bright pixels on the last line of the screen.

For anyone else who wants to try, here’s a version with those functions ported, though only the first one is being called.

(I also left some comments to specify what the commands are supposed to be according to the datasheet.)

@GoergPflug, just to check, are those leading 0s in the arrays in your functions part of the screen instruction sequence or are they something to do with I2C or the os_i2c_write function?

1 Like

the i2c ssd1306 needs a dummy zero in front of the commands. (i do not know for the spi version)

1 Like

my set brightness function is also different, it adjusts voltage and precharge to get a larger range.

I just tried removing them and it worked without them, so I think that’s definitely just an I2 thing. SPI doesn’t need the leading zeroes.

After looking at that 2010 datasheet with the extra instructions, I noticed that 0xD6 is the zoom opcode and suspected that’s what was causing the extra-long characters. Sure enough, it was.

So I’m presuming 0x9A is the ‘undocumented’ opcode that’s supposed to activate the ‘grey mode’?

If so, I can confirm that it doesn’t work on my Arduboy unit.
Running that command and an operand of 2 or 0 does not activate a grey mode, it just nudges the pixels to the right.

The minimal code for anyone with an Arduboy who wants to test:

#include <Arduboy2.h>

Arduboy2 arduboy;

void setMode(bool enabled)

	arduboy.SPItransfer(enabled ? 2 : 0);


void setup()

void loop()




	arduboy.println(F("Hello World"));


Pressing A moves the frame a few pixels to the right,
and pressing B moves it back to where it was at the start.


the effect of the command allone is the screen height halfing and the pixels summing.

the zoom command makes it fill the screen again.

i got one report from the wokwi community that it was working (i2c)

That’s what I’m calling ‘grey mode’. I’m considering that to be a different effect to ‘zoom’.

Either way, zoom works but ‘grey mode’ doesn’t (or doesn’t appear to).

That said, the command does actually do something, which suggests that the SPI version might have undocumented instructions… (Assuming it’s not just an alias for an existing function. I don’t exactly have all the SSD opcodes memorised.)

In which case, there’s a few possibilities:

  • The ‘grey mode’ instruction does exist on the Arduboy’s screen version, but it’s assigned a different opcode.
  • The setup commands that Arduboy2 runs in arduboy.begin() might somehow affect the behaviour.
  • The ‘grey mode’ instruction is I2C only.

Even if there’s no grey mode, there’s now a case for trying to unearth more undocumented functions.

(Also, it’s nice to know that the zoom function works.)

or its a question of the hardware revision of the ssd1306… i do not know… i found that stuff yesterday…and hope to know more soon as people are testing.

Are there any other differences between this code and @GoergPflug’s? Addressing / page mode?

As I mentioned before, there’s different SSD1306 revisions out there- which affected Thumby. (Setting brightness to the minimum 0 now switches off some newer modules). It also affected how greyscale can be done. Basically revisions in the undocumented areas of the controller.

i have one of these modules that switch off with brightness zero, the one i used before did not do that, but i sprayed to much resine on it and it died.

It would be of interest if you test with an ‘old’ and ‘new’ revision module (determined by 0 brightness). These seemed to come into the supply chain late 2022.

i will dig what i find…

the relevant functions are the init:

void os_init_ssd1306 (void)
  // brigher screen init:
  const u8 init1306[]={
   0,0xe4,0xAE,0xD5, 0x80, 0xA8, 0x3F,0xD3, 0x0,0x40,0x8D, 0x14, 0x20, 0x01, 0xA1, 0xC8, 0xDA, 0x12, 0x81, 0x7F, 0xD9, 0xF1, 0xDB, 0x40,  0xA4,0xA6,0xAF
  // darker screen init:
  const u8 init1306[ ] = {
    0x0,0x20, 1 /*vertical mode*/,0xB0,0xC8,0x00,0x10,0x40,0x81,0x0,0xA1,0xA6,0xA8,0x3F,0xA4,0xD3,0x00,0xD5,0xF0,0xD9,0x22,0xDA,0x12,0xDB,0x20,0x8D, 0x14,0xAF
  os_i2c_write(init1306, sizeof(init1306));

and the display transfer

  os_i2c_write_byte(0xAF);  // Display On
  os_i2c_write_byte(0x21);  // define x range 

if vsync-mode is enabled i send a sleep command after spinning in mux=1, but it works without the vsync mode.

1 Like

It could be. I don’t know how old your I2C screen is, but my Arduboy unit is a quite few years old. At least 3-5.

It could also be a difference between manufacturers, assuming there’s more than one manufacturer.

By the way, the forum uses Markdown, so you can use `s to wrap your code. E.g.

int main()


int main()

You can also use old-fashioned BB code. E.g.

int main()

Quite possibly. That’s one of the reasons I was hoping for a ‘minimal program that works’ rather than something with sine functions and unrelated functionality. The closer we get to something that’s ‘bare minimum’ and reasonably hardware agnostic, the easier this’ll be to disect and test.

I also refer you to my above remark:

I definitely won’t be digging through that tonight, but someone else is welcome to if they want to.


Someone should really set up a repo to document this stuff.

In software land we don’t call them ‘undocumented areas’, we call that the implementation and chastise people for depending on the implementation instead of the interface/abstraction because the implementation is expected to change…

1 Like