sparkfun / SparkFun_Micro_OLED_Arduino_Library

Arduino library for the SparkFun Micro OLED - a breakout board for a monochrome, 0.66", 64x48 OLED display.
Other
83 stars 60 forks source link

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

Closed mleibman closed 3 years ago

mleibman commented 6 years ago

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 commented 3 years ago

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