Skip to content

I2C interface is really slow (w/ solution for ESP8266) #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mleibman opened this issue May 28, 2018 · 1 comment
Closed

I2C interface is really slow (w/ solution for ESP8266) #18

mleibman opened this issue May 28, 2018 · 1 comment
Assignees

Comments

@mleibman
Copy link

mleibman commented May 28, 2018

I've tried using this library with a Wemos D1 mini and a Wemos OLED Shield since this is one of the major libraries out there that handles the 64x48 resolution displays, but I have found it to be way too slow.

On a 160Mhz ESP8266 MCU, the display update time (just the .display() call) took 118ms.
As noted in issue #11 , the library uses the default 100KHz I2C bus speed. Changing it to 700+KHz decreased the update time to 17ms. Not bad, considering the ESP8266 has only a software I2C (more on that later), but still too slow, especially considering the tiny screen size.

It is only when I looked deeper into how the data was sent to the display that I noticed that it was sending the screen data one byte at a time, each with the overhead of the I2c address + control & command byte, so for each byte of the screen data, it was sending 3 bytes on the wire. In addition to that, it was sending 12 (6*2) extra messages to set the page and column address for the page of data being sent, so that extra time on the wire.

void MicroOLED::display(void) {
  uint8_t i, j;

  for (i=0; i<6; i++) {
    setPageAddress(i);
    setColumnAddress(0);
    for (j=0;j<0x40;j++) {
      data(screenmemory[i*0x40+j]);
    }
  }
}

All of this seems completely unnecessary, especially considering that SSD1306 supports plain horizontal addressing mode that matches the memory buffer structure in screenmemory and auto-increments the current address pointer with rollover to the beginning.
We can easily set this mode in the begin() method:

// Set horizontal addressing mode
command(0x20);
command(0x00);

// Set column address (64 columns)
command(0x21);
command(32 + 0);
command(32 + 63);

// Set page address (6 pages, 8 each for 48 rows)
command(0x22);
command(0);

Then we could just write the entire block in pretty much one go. Well, technically two, since the protocol requires the first byte to be ack'ed:

void MicroOLED::display(void) {
  uint8_t buffer[2];

  // Store the first byte so we can reuse the screenmemory and not have to
  // copy it into a separate buffer.
  uint8_t firstByte = screenmemory[0];

  // Write the first byte.
  buffer[0] = 0x40;
  buffer[1] = firstByte;
  twi_writeTo(i2c_address, buffer, 2, false);

  // Write the rest.
  screenmemory[0] = 0x40;
  twi_writeTo(i2c_address, screenmemory, 384, true); 
  screenmemory[0] = firstByte;
}

Doing this speeds things up quite a bit, bringing it down to 5ms.
As mentioned earlier, ESP8266 doesn't have a hardware I2C. Using a faster software I2C implementation (https://github.com/pasko-zh/brzo_i2c), the time went down to 3-4ms, but I didn't want to bring in yet another library, so I decided to stick with the above implementation.

Looking at the Arduino's Wire API (https://www.arduino.cc/en/Reference/Wire), they mention that the default implementation only has a 32-byte buffer, and if we were to send more in one transmission, it would be discarded. So I suppose that may have served the reason for this implementation. Still, Adafruit's SSD1306 library (https://github.com/adafruit/Adafruit_SSD1306) does at least some batching and sends data 16 bytes at a time, so the overhead is much smaller. Plus, it uses the horizontal mode addressing, so the extra transmissions to set the page and column sizes are not needed.

@PaulZC
Copy link
Contributor

PaulZC commented Dec 10, 2020

Hi @mleibman,
Version 1.2.10 includes the increased I2C transmission size upgrade you requested. We've added a new function called i2cWriteMultiple to do that. It defaults to using a transfer size of 32 bytes, but you can increase that (if your hardware will allow it) by calling setI2CTransactionSize.
At 400kHz on an ATmega328, a full drawBitmap now takes around 16ms.
Best wishes,
Paul

@PaulZC PaulZC closed this as completed Dec 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants