# SSD1306 I2C: Draw only changes works however on wrong position, how to set position (command)?

I create an OLED display class for the SSD1306 and how it works is pretty straight forward. The display is buffered (offscreen method) and at performing an update, it 'dumps' the buffer to the display via I2C, in this case a total of 512 bytes (128x32 pixels) but can be also 1024 bytes (128x64 pixels).

The offscreen method is to avoid flicker and performs faster because it only updates when I want it to update. Very usefull with animations for example.

Nothing fancy however on many updates, its an expensive operation and mostly not neccessary because of little screen changes. I'm also curious about the frame rate, with less updates needed, does it perform better.

So I want to be the class is smart enough to send only the changes, so I don't need to take care about speed/performing issues. This is partly working already (by a double buffer), to detect the changes and send only those, however, appear on wrong position. This is caused by previous method, with a simple dump, it is not required to specify each pixel pair location, it just pumping an array of values.

What kind of command/instruction of the SSD1306 do I need to set target position or am i able to skip values? I cannot figure it out using the datasheet.

Here is my code (AVR-C):

// Send start address
if (useRegisters)          // Send TWI Start
{
TWCR = _BV(TWEN) | _BV(TWEA) | _BV(TWINT) | _BV(TWSTA);
while(TOD_CHK_WAIT((TWCR & _BV(TWINT))) == 0) {};
TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);
while (TOD_CHK_WAIT((TWCR & _BV(TWINT)) == 0)) {};
TWDR = TOD_DATA_CONTINUE;
TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);
while(TOD_CHK_WAIT((TWCR & _BV(TWINT)) == 0)) {};
}
else
{
pinWaitForAck();
pinWriteByte(TOD_DATA_CONTINUE);
pinWaitForAck();
}

// Dump buffer
for (uint16_t i=0; i < cacheSize; i++)    // Send data
{
bool bUpdate = ((!doubleBuffer) || ( doubleBuffer && (displayCache[cacheSize-1+i] != displayCache[i])));

if( doubleBufferFirstTime || bUpdate )
{
if( doubleBuffer )
{ displayCache[cacheSize-1+i]=displayCache[i]; }

if( useRegisters )
{
if( !doubleBufferFirstTime && doubleBuffer )
{
// Set location must be here
// /* X */ set( TOD_SET_COLUMN_ADDR, x ); //width-1 );
// /* Y */ set( TOD_SET_PAGE_ADDR  , y ); //height-1 );
}

TWDR = displayCache[i];
TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);               // Clear TWINT to proceed
while(TOD_CHK_WAIT((TWCR & _BV(TWINT)) == 0)) {};        // Wait for TWI to be ready
}
else {
pinWriteByte(displayCache[i]);
pinWaitForAck();
}
}

}

doubleBufferFirstTime=false;

if( useRegisters )                                            // Send TWI Stop
{ TWCR = _BV(TWEN)| _BV(TWINT) | _BV(TWSTO); }              // Send STOP
else { pinSendStop(); }


I tried to use the COLUMN_ADDR (21h) and PAGE_ADDR (22h) to set the offset, the device doesn't seems to like it (lock up). What can I use to change the offset or skip a specific offset?

This is the setup of the display:

  set( TOD_SET_DISPLAY_CLOCK_DIV_RATIO  , 0x80 );
set( TOD_SET_MULTIPLEX_RATIO          , height-1 );
set( TOD_SET_DISPLAY_OFFSET           , 0x0  );

set( TOD_SET_START_LINE | 0x0 );

set( TOD_CHARGE_PUMP                  , 0x14 );

set( TOD_SET_SEGMENT_REMAP | 0x1 );
set( TOD_COM_SCAN_DIR_DEC );

set( TOD_SET_COM_PINS                 , ( height > 32 )?0x12:0x02 );
set( TOD_SET_CONTRAST_CONTROL         , 0xCF );

set( TOD_SET_PRECHARGE_PERIOD         , 0xF1 );

set( TOD_SET_VCOM_DETECT              , 0x40 );

set( TOD_DISPLAY_ALL_ON_RESUME );
set( TOD_NORMAL_DISPLAY );

set( TOD_DEACTIVATE_SCROLL );

show();


Edit 21 okt 2017

• Anyone here? Or question too difficult? – Codebeat Oct 19 '17 at 18:33
• I'm afraid I'm not familiar with the AVR code. But I have used an SSD1306 successfully in the past. The setup looks very similar to what I had. However I cannot see the code where you set the memory address. "COLUMN_ADDR (21h) and PAGE_ADDR (22h)" are very strange. The page address is set with a value in the range 0xb0 to 0xb7. And setting the column address requires two bytes, one in the range 0x00 to 0x0f, and one in the range 0x10 to 0x1f. Can you point me to the code where you set the address and show the code for the SSD1306 specific functions used in that code? – Codo Oct 20 '17 at 8:58

I assume you understand how to create the I2C byte sequence for the SSD1306 but I'll repeat it anyway: The SSD1306 distinguishes between commands (incl. command parameters) and data (pixel data). With SPI, it uses a dedicated input pin to distinguish commands and data.

With I2C, 0x80 needs to be prepended to each command byte. 0x40 switch to data mode. The data mode continues until the end of the I2C transaction (indicated by a STOP condition).

To update a part of the screen, the start address of the top left corner has to be set and then the data can be sent. A valid byte sequence for starting at the coordinates (20, 16) for x and y looks like this:

0x80, 0xb1,  // page start address: 0xb0 | (y >> 3)
0x80, 0x04,  // lower nibble of column: 0x00 | (x & 0x0f)
0x80, 0x11,  // upper nibble of column: 0x10 | ((x >> 4) & 0x0f)
0x40, // switch to data mode
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, ... // pixel data


The memory is divided into pages. Each page covers 8 pixel rows. So you can only update stripes of 8 vertical pixels and the stripes must be aligned to multiples of 8. As you can see in the first line of the byte sequence, the lower 3 bits of y are simply discarded.

The horizontal start position is provided in two parts: the upper and lower nibble. A nibble is four bits, i.e. half a byte. See lines 2 and 3 above.

The remaining two lines switch to data mode and send the pixel data. With each byte, the address advances by 1, i.e. it advances horizontally from left to right and each byte written affects a vertical piece of 8 pixels.

With the different addressing modes (command 0x20 to 0x22), you can determine how the address advances at the end of page, at the end of your update area etc. The simplest approach is to write to each page separately and to explicitly set the address the beginning of each page.

Note that there are clones of the SSD1306 chips that do not support the different addressing modes.

• Hi, thanks for your nice answer. The buffer works the same as the internal memory, i know about the bytes and bits, pages and columns. So if you set a pixel, you manipulate a byte with 8 bits, 8 pixels. Because this question is a few days old, I change the code already and it's working however the device increments the memory pointer all the time also when you set the page size and column size. I get the idea from this resource: ccsinfo.com/forum/viewtopic.php?t=54453 . It is working and draws only a few bytes but in reverse order so I need to figure out to solve this. – Codebeat Oct 20 '17 at 18:07
• Do you know if there is a way to reset the memory pointer because it increases automatically after each write and doesn't reset when you change the page column setting to position the bytes target. – Codebeat Oct 20 '17 at 18:11
• The memory pointer is set with the six first bytes in my answer. I called it address. But it's the same As to the automatically increasing pointer: that's what the address modes and the commands 0x20 to 0x22 are for. Show your code or at least the generated byte sequence. Then I can tell more. – Codo Oct 21 '17 at 15:28
• Hi @Codo, thanks for the explanation, figured out already, see my posted answer. – Codebeat Oct 21 '17 at 15:44

The number of bytes to transfer (1KB) is so low this mean to update whole screen is much easier. You don't need spending time which area you want to refresh. Just keep framebuffer in SRAM and update only this and then send whole buffer to screen.

Personally I use STM32F0 (yes is much faster than AVR and I also used DMA for transfer) but first try was transfer with bitbanging and the speed was still very fast. I got total framerate with this configuration over SPI more than 1000fps in case if CPU spend time only by sending data to the screen (one transfer of whole buffer took less than 1ms).

So my proposal is to keep program simple and always update whole screen, because is too small.

• Hi Thanks for the answer. I know 1KB is not much however use the bus and MCU for more tasks. It is part of a controller with MANY functions (27K already used by code) and every improvement is welcome. I don't want to complicate this stuff but want also learn from it, that's why I wrote the class myself instead of using a fully baked library. It is not only for size, speed or only for this little display, maybe I can use this technique with other displays too. I also want to understand what I am doing and why. I can detect the changes and the difference is huge, exam 54 bytes instead 512 bytes. – Codebeat Oct 20 '17 at 17:56

Finally got it, this site/article gave me some insights to do it right: https://www.ccsinfo.com/forum/viewtopic.php?t=54453 It is at the gotoxy feature. I think the calculation at this function is not correct in the example but ok, i figured out myself the correct way.

My column/page adressing was wrong and correct it (change allot actually in a few days). Changed also the way to update, it is now in a recursive manner.

It works actually pretty good (and fast), for example when a little icon changes, only about 12 bytes/updates will be send instead of the full 512 bytes. I need to calculate the send bus bytes to see if it is really effective. It is a nice experiment but I need to do some test to see what it will do with the framerate for example.

Thanks everyone for the comments. Here is my new code, I think you can't use it without edits/changes, it is only here as example and to show you it is possible and give some idea how to do it.

struct TODPoint
{
uint8_t x;
uint8_t y;
};

uint8_t TOLEDdisplay::getMemPageMax()
{ return (height > 16)?((height > 32)?7:3):1; }

uint8_t TOLEDdisplay::getMemPageCount()
{ return getMemPageMax()+1; }

uint8_t TOLEDdisplay::getMemColumnMax()
{
uint16_t iSize     = (uint16_t)width*(uint16_t)height;
uint16_t iPageSize = (uint16_t)getMemPageCount()*8;
if( iSize >= iPageSize )
{
iSize/=iPageSize;
return iSize-1;
}
return 0x7F;
}

uint8_t TOLEDdisplay::getMemColumnCount()
{ return getMemColumnMax()+1; }

void TOLEDdisplay::gotoMemXY( uint8_t memX, uint8_t memY )
{
uint8_t iMaxX = getMemColumnCount();
uint8_t iMaxY = getMemPageCount();

// gotoXY routine to move the memory pointers to column, page.
if( memX < iMaxX && memY < iMaxY )
{
TOD_DISABLE_INTERRUPTS();

if( !cycleTimeout )
{ set( TOD_SET_PAGE_ADDR, memY, iMaxY-1 ); }

TOD_ENABLE_INTERRUPTS();
}
}

{
TODPoint xy;
xy.x = getMemColumnCount();
return xy;
}

{
{
gotoMemXY( memXY.x, memXY.y );
}
}

bool TOLEDdisplay::isCacheAddressChanged( uint16_t iAddress, bool bUpdateWhenChanged /* default = false */  )
{
bool bFound = ( doubleBuffer && !doubleBufferFirstTime && ( iAddress < cacheSize ) );
if( bFound )
{

if( bUpdateWhenChanged )
}

return bFound;
}

uint8_t TOLEDdisplay::getMemPageMax()
{ return (height > 16)?((height > 32)?7:3):1; }

uint8_t TOLEDdisplay::getMemPageCount()
{ return getMemPageMax()+1; }

uint8_t TOLEDdisplay::getMemColumnMax()
{
uint16_t iSize     = (uint16_t)width*(uint16_t)height;
uint16_t iPageSize = (uint16_t)getMemPageCount()*8;
if( iSize >= iPageSize )
{
iSize/=iPageSize;
return iSize-1;
}
return 0x7F;
}

uint8_t TOLEDdisplay::getMemColumnCount()
{ return getMemColumnMax()+1; }

TODMinax TOLEDdisplay::getCacheChangeRange()  // get min max boundary
{
TODMinax result;
bool bFound = ( doubleBuffer && !doubleBufferFirstTime );

if( bFound )
{
result.min  = 0;
result.max  = cacheSize;
bFound = false;

// Don't count zero, will be drawed when needed
while( !bFound && (++result.min < result.max ) )
{ bFound = displayCache[result.max+result.min]!=displayCache[result.min]; }

if( bFound )
{
bFound=false;
while( !bFound && (--result.max > result.min) )
{ bFound = displayCache[cacheSize-1+result.max]!=displayCache[result.max]; }
}
}

if( !bFound )
{ result.min = result.max = 0; }

return result;
}

void TOLEDdisplay::update( bool bForce /* default = false */, uint16_t iAddress /* default = 0 */ )
{
if( !isInit )
{ return; }

if( !bForce )
{
if( !enabled )
{ return; }

if( cycleTimeout )
{
reset();
if( cycleTimeout )
{ return; }
}
}

/*
// Prints cache to mem position table
{
Serial.print( ": Column " );
Serial.print( memXY.x );
Serial.print( " - Page " );
Serial.println( memXY.y );
}

return;
*/

// When doublebuffer is enabled, we check individual parts of the
// cache are changed, to only draw the changes. When doublebuffer is
// disabled or an update is forced, we send the whole cache to the
// display.
if( !bForce && doubleBuffer && !doubleBufferFirstTime && (iAddress == 0) )
{
//Serial.println( "Check changes" );

// Check for changes, get min and max range of cache changes.
// It is possible that not all entries between this range are changed,
// this just to limit the loop to check for changes, see code below.
// NOTICE: The function getCacheChangeRange() skips element 0 in
//         the cache array, it needs to be checked manually, see also
//         code below.
TODMinax changeRange = getCacheChangeRange();

//Serial.println( "-----" );
//Serial.println( changeRange.min );
//Serial.println( changeRange.max );

// Something changed?
if( changeRange.min == changeRange.max )
{
// Check if address 0 has been changed and reset change

// Function sets ranges to zero when no change is found,
// when value > 0 then doublecheck entry has been changed
if( changeRange.min != 0 && isCacheAddressChanged( changeRange.min, true ) )
{
// Recursive call to this function with different address
update( false, changeRange.min );
}

// Cache element 0 changed?
{
// No, exit function
return;
}

// At this point proceed normal, draw changes detected at element 0
// In this case, loop max iMaxAddress is set to 1
}
else {
// Changed range found
TOD_DISABLE_INTERRUPTS();

while( changeRange.min <= changeRange.max )
{
if( isCacheAddressChanged( changeRange.min, true ) )
{
// Recursive call to this function with different address
update( false, changeRange.min );
}

++changeRange.min;
}

//Serial.println( "-----------" );
//Serial.print( "bytes updated : " );

// Check if address 0 has been changed
{
TOD_ENABLE_INTERRUPTS();
return;
}

// otherwise proceed, draw iAddress 0
}
}

if( cycleTimeout )
{
doubleBufferFirstTime=true;
return;
}

{
// Update all unless iMaxAddress has been changed to 1,
// If this the case, we draw only address 0
doubleBufferFirstTime=false;
gotoMemXY(0,0);
}
else {
//gotoMemXY(0,0);
}

if( cycleTimeout )
{
doubleBufferFirstTime=true;
return;
}

{
TOD_DISABLE_INTERRUPTS();
}

if (useRegisters)          // Send TWI Start
{
TWCR = _BV(TWEN) | _BV(TWEA) | _BV(TWINT) | _BV(TWSTA);
while(TOD_CHK_WAIT((TWCR & _BV(TWINT))) == 0) {};
TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);
while (TOD_CHK_WAIT((TWCR & _BV(TWINT)) == 0)) {};
TWDR = TOD_DATA_CONTINUE;
//TWDR = TOD_DATA;
TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);
while(TOD_CHK_WAIT((TWCR & _BV(TWINT)) == 0)) {};
}
else
{
pinWaitForAck();
pinWriteByte(TOD_DATA_CONTINUE);
pinWaitForAck();
}

{
if( useRegisters )
{
TWDR = displayCache[i];
TWCR = _BV(TWEN) | _BV(TWINT) | _BV(TWEA);               // Clear TWINT to proceed
while(TOD_CHK_WAIT((TWCR & _BV(TWINT)) == 0)) {};        // Wait for TWI to be ready
}
else {
pinWriteByte(displayCache[i]);
pinWaitForAck();
}
}

if( useRegisters )                                            // Send TWI Stop
{ TWCR = _BV(TWEN)| _BV(TWINT) | _BV(TWSTO); }              // Send STOP
else { pinSendStop(); }