Bulk pixel with Adafruit_SSD1351

My new display is doing just fine, except for the speed.
This is the relevant code I am using right now:

    #include "Adafruit_SSD1351.h"
    ...
        unsigned int screenBufferSize = SSD1351WIDTH * SSD1351HEIGHT *2;// which makes it wxh at 2 bytes (uint16_t)
        uint8_t *screenPixelBuffer;
    ...
    screenPixelBuffer = (uint8_t *)malloc(screenBufferSize);
    ...
     int16_t *pixelBuffer = (int16_t *)screenPixelBuffer;
     for(int16_t yP = 0; yP < SSD1351HEIGHT; yP++)
    {
          for(int16_t xP = 0; xP < SSD1351WIDTH; xP++)
          {
                tft.drawPixel(xP,yP, *(pixelBuffer + yP * SSD1351WIDTH + xP));
          }
    }

The complete bitmap is in memory, the format is the correct RGB565 and all chunks are arranged in the right order. However, although everything is fine, I have to itereate through my bitmap and draw pixel by pixel.
I’d like to replace the two loops with one single call.

I have looked around and it seems, as if all common examples are poking on single pixels the one way ot the other.
Is there a function call to copy a complete bitmap as a block of memory into the memory of the display?

@dial, since the Adafruit driver uses the GFX (or mfGFX) graphics library, the lowest level function is drawPixel. This avoids having a display buffer in RAM. Since you created a display buffer then you could either do a direct “bulk” SPI write to the display or perhaps even an SPI DMA write if the data in the buffer is correctly structured.

I believe you first need to set the column and row of the first pixel, then issue a WriteRAM command then drop the CS line, do the bulk SPI transfer the raise CS again. Take a look at the end of fillScreen() function. Just replace the last for look with yours which will write the entire screen buffer to the display RAM. Once you get that working, you could optimize the SPI write code so instead of toggling CS for each write you only do it before and after the bulk transfer. The last thing you can then try is the SPI DMA transfer supported by the system firmware.

Let me know how it goes! :wink:

2 Likes

Thanks a lot. I had no idea about this limitated functionality.

The ‘fill’-approach is definitely faster. I added two functions in Adafruit_SSD1351.cpp:

void Adafruit_SSD1351::displayImageOnScreen(uint16_t *imagePixelBuffer) {
    displayImageInRect(0, 0, SSD1351WIDTH, SSD1351HEIGHT, imagePixelBuffer);
}

void Adafruit_SSD1351::displayImageInRect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t *imagePixelBuffer)
{
    // Bounds check
    if ((x >= SSD1351WIDTH) || (y >= SSD1351HEIGHT))
        return;
    
    // Y bounds check
    if (y+h > SSD1351HEIGHT)
    {
        h = SSD1351HEIGHT - y - 1;
    }
    
    // X bounds check
    if (x+w > SSD1351WIDTH)
    {
        w = SSD1351WIDTH - x - 1;
    }
    
    /*
     Serial.print(x); Serial.print(", ");
     Serial.print(y); Serial.print(", ");
     Serial.print(w); Serial.print(", ");
     Serial.print(h); Serial.println(", ");
     */
    
    // set location
    writeCommand(SSD1351_CMD_SETCOLUMN);
    writeData(x);
    writeData(x+w-1);
    writeCommand(SSD1351_CMD_SETROW);
    writeData(y);
    writeData(y+h-1);
    // fill!
    writeCommand(SSD1351_CMD_WRITERAM);
    
    for (uint16_t i=0; i < w*h; i++) {
        writeData(*(imagePixelBuffer + i) >> 8);
        writeData(*(imagePixelBuffer + i));
    }
}
2 Likes

Hi Dial

I am working away on a similar project. I have been able to get to the point where the image is loading using the standard pixel by pixel write method.

I have added your 2 routines above to the Adafruit_SSD1351 .h and .cpp. Would you be kind enough to share an example of the code you are using to call these routines? It would be most helpful.

Thanks,
David

I made good progress today. Below is a copy of the working software that loads an image from the Teensy 3.6 on board SD card and displays it on a SSD1351 (128x128) OLED display. The read and the display combined takes less than 200ms vs. the more conventional code which writes pixel by pixel on the OLED, about 550ms.


/*************************************************** 
  This sketch reads in bmp files (128 x 128) and 
  then displays the image onto a SSD1351 compatible
  OLED display via hardware based SPI on a Teensy 3.6
  using the built in SD card reader and the latest 
  SDFat library.

  Why?  Using the original code example from Adafruit
  took approximate 525ms to load.  Too slow for the 
  interactive purposes that I had in mind for my project
  thus I looked for a faster method.  Having seen a demo
  of video being played via a Teensy 3.6 on a small (I belive
  to be OLED display) I knew that there had to be a 
  faster way to get an image to load on a small OLED.  Thus
  this project was born.  


  It comprises code from Adafruit as well as 
  from "Dial" 
  ------> https://community.particle.io/t/bulk-pixel-with-adafruit-ssd1351/18403
  and the latest SDFAT library by Bill Greiman which can be found at
  ------> https://github.com/greiman/SdFat-beta
 
  Additionally many thanks to" 
  o  Caffine
  o  Bill Greiman
  o  Pete (el_supremo)
  o  Robinsloan
  on the PJRC forums for their guidance and inspiration.

  You can find the thread related to this code development
  at:
  ------> https://forum.pjrc.com/threads/40871-Teensy-6-5-SDFat-BMP-file-read-fail

  I am positive this code can be improved upon and look 
  forward to see what others can do to improve the performance.

  Please utilize the forum posting above if you do make updates.

  SoCalPinPlayer
  David 
  
 ****************************************************/
/*************************************************** 
  This is a example sketch demonstrating bitmap drawing
  capabilities of the SSD1351 library for the 1.5" 
  and 1.27" 16-bit Color OLEDs with SSD1351 driver chip

  Pick one up today in the adafruit shop!
  ------> http://www.adafruit.com/products/1431
  ------> http://www.adafruit.com/products/1673
 
  If you're using a 1.27" OLED, change SSD1351HEIGHT in Adafruit_SSD1351.h
 	to 96 instead of 128

  These displays use SPI to communicate, 4 or 5 pins are required to  
  interface
  Adafruit invests time and resources providing this open source code, 
  please support Adafruit and open-source hardware by purchasing 
  products from Adafruit!

  Written by Limor Fried/Ladyada for Adafruit Industries.  
  BSD license, all text above must be included in any redistribution

  The Adafruit GFX Graphics core library is also required
  https://github.com/adafruit/Adafruit-GFX-Library
  Be sure to install it!
 ****************************************************/

#include <SPI.h>
#include <SdFat.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1351.h>

/*
// Set USE_SDIO to zero for SPI card access. 
#define USE_SDIO 1

 * SD chip select pin.  Common values are:
 *
 * Arduino Ethernet shield, pin 4.
 * SparkFun SD shield, pin 8.
 * Adafruit SD shields and modules, pin 10.
 * Default SD chip select is the SPI SS pin.
 */
// const uint8_t SD_CHIP_SELECT = SS;
/*
 * Set DISABLE_CHIP_SELECT to disable a second SPI device.
 * For example, with the Ethernet shield, set DISABLE_CHIP_SELECT
 * to 10 to disable the Ethernet controller.
 */
const int8_t DISABLE_CHIP_SELECT = -1;

SdFatSdio sd;

// serial output steam
ArduinoOutStream cout(Serial);

// OLED Display SPI PINS   -- Built in SD card on Teensy uses separate SPI
// If we are using the hardware SPI interface, these are the pins (for future ref)
#define sclk 13
#define mosi 11
#define cs   10
#define rst  6
#define dc   9

// Color definitions
#define	BLACK           0x0000
#define	BLUE            0x001F
#define	RED             0xF800
#define	GREEN           0x07E0
#define CYAN            0x07FF
#define MAGENTA         0xF81F
#define YELLOW          0xFFE0  
#define WHITE           0xFFFF

// to draw images from the SD card, we will share the hardware SPI interface
Adafruit_SSD1351 tft = Adafruit_SSD1351(cs, dc, rst);

// For Arduino Uno/Duemilanove, etc
//  connect the SD card with MOSI going to pin 11, MISO going to pin 12 and SCK going to pin 13 (standard)
//  Then pin 10 goes to CS (or whatever you have set up)
//  #define SD_CS BUILTIN_SDCARD    // Set the chip select line to whatever you use (10 doesnt conflict with the library)

// the file itself
File bmpFile;

// information we extract about the bitmap file
int bmpWidth, bmpHeight;
uint8_t bmpDepth, bmpImageoffset;


void setup(void) {
  Serial.begin(9600);
   
  pinMode(cs, OUTPUT);
  digitalWrite(cs, HIGH);

  delay(2000);
  Serial.begin(9600);

  
  // Wait for USB Serial 
  // Comment out the following if you don't want the MCU to wait
  // until the developer opens up the serial monitor on the 
  // Arduino IDE.
  while (!Serial) {
    SysCall::yield();
  }

     
  // initialize the OLED
  tft.begin();

  Serial.println("init");
  
  tft.fillScreen(BLUE);
  
  delay(500);
  Serial.print("Initializing SD card...");

  if (!sd.begin()) {
    Serial.println("failed!");
    return;
  }
  Serial.println("SD OK!");

// images need to be 128 x 128 in size

  bmpDraw("lily128.bmp", 0, 0);
  bmpDraw("img1.bmp", 0, 0);
  bmpDraw("img2.bmp", 0, 0);
  
}

void loop() {
}

// This function opens a Windows Bitmap (BMP) file and
// displays it at the given coordinates.  It's first
// reads the data into an array after converting the 
// BMP pixels to tft format.
// The array is then written to the OLED display using
// displayImageOnScreen function that has been added 
// to the Adafruit_SSD1351 .h and .cpp files.


#define BUFFPIXEL 20

void bmpDraw(char *filename, uint8_t x, uint8_t y) {

  File     bmpFile;
  int      bmpWidth, bmpHeight;   // W+H in pixels
  uint8_t  bmpDepth;              // Bit depth (currently must be 24)
  uint32_t bmpImageoffset;        // Start of image data in file
  uint32_t rowSize;               // Not always = bmpWidth; may have padding
  uint8_t  sdbuffer[3*BUFFPIXEL]; // pixel buffer (R+G+B per pixel)
  uint8_t  buffidx = sizeof(sdbuffer); // Current position in sdbuffer
  boolean  goodBmp = false;       // Set to true on valid header parse
  boolean  flip    = true;        // BMP is stored bottom-to-top
  int      w, h, row, col;
  uint8_t  r, g, b;
  uint32_t pos = 0, startTime = millis();
  uint16_t imgArry[16348];            // array that holds all pixel converted into tft format hard coded for 128 x 128 display
  uint32_t pixPos = 0;                // position in the image array 



  if((x >= tft.width()) || (y >= tft.height())) return;

  Serial.println();
  Serial.print("Loading image '");
  Serial.print(filename);
  Serial.println('\'');

  // Open requested file on SD card
  if ((bmpFile = sd.open(filename)) == NULL) {
    Serial.print("File not found");
    return;
  }

  // Parse BMP header
  if(read16(bmpFile) == 0x4D42) { // BMP signature
    Serial.print("File size: "); Serial.println(read32(bmpFile));
    (void)read32(bmpFile); // Read & ignore creator bytes
    bmpImageoffset = read32(bmpFile); // Start of image data
    Serial.print("Image Offset: "); Serial.println(bmpImageoffset, DEC);
    // Read DIB header
    Serial.print("Header size: "); Serial.println(read32(bmpFile));
    bmpWidth  = read32(bmpFile);
    bmpHeight = read32(bmpFile);
    if(read16(bmpFile) == 1) { // # planes -- must be '1'
      Serial.println("No. of Planes: 1");
      bmpDepth = read16(bmpFile); // bits per pixel
      Serial.print("Bit Depth: "); Serial.println(bmpDepth);
      if((bmpDepth == 24) && (read32(bmpFile) == 0)) { // 0 = uncompressed

        goodBmp = true; // Supported BMP format -- proceed!
        Serial.print("Image size: ");
        Serial.print(bmpWidth);
        Serial.print('x');
        Serial.println(bmpHeight);

        // BMP rows are padded (if needed) to 4-byte boundary
        rowSize = (bmpWidth * 3 + 3) & ~3;

        // If bmpHeight is negative, image is in top-down order.
        // This is not canon but has been observed in the wild.
        if(bmpHeight < 0) {
          bmpHeight = -bmpHeight;
          flip      = false;
        }

        // Crop area to be loaded
        w = bmpWidth;
        h = bmpHeight;
        if((x+w-1) >= tft.width())  w = tft.width()  - x;
        if((y+h-1) >= tft.height()) h = tft.height() - y;

//        tft.displayImageOnScreen(sdbuffer);

        //========= Read in the BMP image put into imgArry ======== 

        for (row=0; row<h; row++) { // For each scanline...
          // tft.goTo(x, y+row);

          // Seek to start of scan line.  It might seem labor-
          // intensive to be doing this on every line, but this
          // method covers a lot of gritty details like cropping
          // and scanline padding.  Also, the seek only takes
          // place if the file position actually needs to change
          // (avoids a lot of cluster math in SD library).
          if(flip) // Bitmap is stored bottom-to-top order (normal BMP)
            pos = bmpImageoffset + (bmpHeight - 1 - row) * rowSize;
          else     // Bitmap is stored top-to-bottom
            pos = bmpImageoffset + row * rowSize;
          if(bmpFile.position() != pos) { // Need seek?
            bmpFile.seek(pos);
            buffidx = sizeof(sdbuffer); // Force buffer reload
          }

          // optimize by setting pins now
          for (col=0; col<w; col++) { // For each pixel...
            // Time to read more pixel data?
            if (buffidx >= sizeof(sdbuffer)) { // Indeed
              bmpFile.read(sdbuffer, sizeof(sdbuffer));
              buffidx = 0; // Set index to beginning
            }

            // Convert pixel from BMP to TFT format, push to display
            b = sdbuffer[buffidx++];
            g = sdbuffer[buffidx++];
            r = sdbuffer[buffidx++];

            imgArry[pixPos] = tft.Color565(r,g,b); 
            pixPos++;
          } // end pixel
        } // end scanline


        Serial.print("Imaged Loaded into array in ");
        Serial.print(millis() - startTime);
        Serial.println(" ms");

        // --- lets write the array out to the OLED screen

        startTime = millis();
        
        tft.displayImageOnScreen(imgArry);

        Serial.print("Imaged Loaded into OLED in ");
        Serial.print(millis() - startTime);
        Serial.println(" ms");
        
      } // end goodBmp
    }
  }

  bmpFile.close();
  
  if(!goodBmp) Serial.println("BMP format not recognized.");
}


// These read 16- and 32-bit types from the SD card file.
// BMP data is stored little-endian, Arduino is little-endian too.
// May need to reverse subscript order if porting elsewhere.

uint16_t read16(File& f) {
  uint16_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read(); // MSB
  return result;
}

uint32_t read32(File& f) {
  uint32_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read();
  ((uint8_t *)&result)[2] = f.read();
  ((uint8_t *)&result)[3] = f.read(); // MSB
  return result;
}

I am confident that this can be improved upon but will have to leave that to others.

David