Teensy 6.5 SDFat BMP file read fail

Status
Not open for further replies.
Hi All

I am attempting to use a Teensy 3.6 to display images (bmp files) on a small OLED display (128x128) SSD13xx. I am able to read the SD card parameters successfully and I am able to control the OLED display using lines, etc. But I am not able to get the Adafruit sample sketch that uses lily128.bmp to load in the image.

The error I am getting is that the number of planes is 19778 not 1. This is the original bmp file loaded directly from Adafruit so it should be good and I am able to show it on my laptop.

ReadBMP.JPG

I am using SDfat (I believe the latest version).

Here is the source code:

Code:
/*************************************************** 
  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!
 ****************************************************/
/*
 * This program attempts to initialize an SD card and analyze its structure.
 */
#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 
  while (!Serial) {
    SysCall::yield();
  }


/*
  // use uppercase in hex and use 0X base prefix
  cout << uppercase << showbase << endl;

  // F stores strings in flash to save RAM
  cout << F("SdFat version: ") << SD_FAT_VERSION << endl;
#if !USE_SDIO  
  if (DISABLE_CHIP_SELECT < 0) {
    cout << F(
           "\nAssuming the SD is the only SPI device.\n"
           "Edit DISABLE_CHIP_SELECT to disable another device.\n");
  } else {
    cout << F("\nDisabling SPI device on pin ");
    cout << int(DISABLE_CHIP_SELECT) << endl;
    pinMode(DISABLE_CHIP_SELECT, OUTPUT);
    digitalWrite(DISABLE_CHIP_SELECT, HIGH);
  }
  cout << F("\nAssuming the SD chip select pin is: ") <<int(SD_CHIP_SELECT);
  cout << F("\nEdit SD_CHIP_SELECT to change the SD chip select pin.\n");
#endif  // !USE_SDIO 
*/


     
  // 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!");

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

void loop() {
}

// This function opens a Windows Bitmap (BMP) file and
// displays it at the given coordinates.  It's sped up
// by reading many pixels worth of data at a time
// (rather than pixel by pixel).  Increasing the buffer
// size takes more of the Arduino's precious RAM but
// makes loading a little faster.  20 pixels seems a
// good balance.

#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();

  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);
    Serial.print("No. of Planes: "); Serial.println(read16(bmpFile));
    if(read16(bmpFile) == 1) { // # planes -- must be '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;

        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++];

            tft.drawPixel(x+col, y+row, tft.Color565(r,g,b));
            // optimized!
            //tft.pushColor(tft.Color565(r,g,b));
          } // end pixel
        } // end scanline
        Serial.print("Loaded 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;
}


Any help is greatly appreciated. My goal is to be able to load a few small 128x128 images very quickly onto the OLED display (making it interactive when the user pushes a button). If you have recommendations of another way to go or examples of working Teensy 3.x using DMA, etc. please point me to them.

Thank you
 
I know what is happening but not why. Converting 1798851906 to hex gives 6B384D42. The 4D42 is the BMP signature and the rest is part of the file size. But when the code tries to read further through the file, with read32, it keeps reading the same thing from the beginning so each read32 returns 1798851906. The read16 reads 19778 which in hex is 4D42 (the BMP signature again). This number should be the number of planes, which is always 1, and the code gives up.

Pete
 
The file position is maintained in the File object. Your call makes a copy of the File object so the file position is not updated.

In SD.h this will work since SD.h is a wrapper for SdFat and has a pointer to the true file object.

Code:
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;
}

Either change to call by reference:
Code:
uint32_t read32(File& f);

or use a pointer"
Code:
uint32_t read32(File* f);
// must use f->read()

In general, it is dangerous to use call by value for complex objects with state.
 
I should add that SD.h only works due to a bug that also can cause problems. SD.hwould not work if it had a proper destructor, copy constructor, and assignment operator.

Currently SD.h can have memory leaks since the SdFat object is in dynamic memory but can be lost if an assignment is made to an open file object.
 
Amazing. Thank you Bill and Pete. I have made the changes and it now works.

It is loading a picture in 557ms. I am wondering if there is any way to make the data transfer/load faster?

Is there some DMA method I should use or another library which would increase the speed? Basically you can see the screen getting refreshed (filling in the pixels from top to bottom) vs. the image "just showing up".

Thank you.

David
 
Hey David:

You have two things to optimize -- the transfer from the card and the drawing to the display.

For the transfer, you might try making BUFFPIXEL larger. (And yes definitely ensure you are using SDFat-beta with support for the 3.6's built-in SD reader => https://github.com/greiman/SdFat-beta)

For the drawing, the code posted at the bottom of this thread looks like it might be helpful to you: https://community.particle.io/t/bulk-pixel-with-adafruit-ssd1351/18403

I hope either or both of those are at least mildly helpful!

R
 
Last edited:
Your code (which looks like the example code in the ili9341_t3 library) reads individual lines of the BMP off the SD card and then writes them out to the display.

I modified the example code to read a number of rows at once and write them out as a block. On my Teensy 3.5 I can read a 320x240 BMP and draw it to my ILI9341 screen in around 75ms. (I also have support for scaling the image by an integer amount, by skipping lines, so ignore that if you'd like)

Code:
bool bmpDrawScale(const char *filename, uint8_t x, uint16_t y, int scale, int maxheight) {

	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*ROWSPERDRAW]; // pixel buffer (R+G+B per pixel)
	uint32_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      row, col;
	uint8_t  r, g, b;
	uint32_t pos = 0, startTime = millis();

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

	Serial.println();
	Serial.print("Filename: ");
	Serial.println(filename);

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

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

				goodBmp = true; // Supported BMP format -- proceed!
								//Serial.print(F("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;
				}

				for (row = 0; row < maxheight; row += ROWSPERDRAW) { // For each scanline...

																	 // 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 - ROWSPERDRAW - 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
					}
					for (int i = ROWSPERDRAW; i > 0; i--)
					{
						int index = 0;

						for (col = 0; col < (bmpWidth); 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
							}

							b = sdbuffer[buffidx++];
							g = sdbuffer[buffidx++];
							r = sdbuffer[buffidx++];
							// Convert pixel from BMP to TFT format, push to display
							if (i%scale == 0)
							{
								if (index++%scale == 0)
								{
									awColors[(index / scale) + (((i / scale) - 1)*bmpWidth / scale)] = tft.color565(r, g, b);
								}
							} // end pixel
						}
					}
					tft.writeRect(x, y + (row / scale), bmpWidth / scale, ROWSPERDRAW / scale, awColors);
				} // end scanline
				Serial.print(F("Loaded in ")); Serial.print(millis() - startTime); Serial.println(" ms");
			} // end goodBmp
		}
	}
	else
	{
		return false;
	}
	bmpFile.close();
	if (!goodBmp) Serial.println(F("BMP format not recognized."));
	return true;
}
 
Hi Caffine:

This really looks promising. I have been able to almost get this to compile. Turns out that the Adafruit_SSD1351.H (.cpp) does not include a "writeRect" function. So I am looking at adding it in based upon the "writeRect" in the ILI9341_t3 but am rather new to working at this level of code. Here is the writeRect from the ILI9341 ... any help in how to port this to the Adafruit_1351 would be greatly appreciated.


void ILI9341_t3::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color)
{
// rudimentary clipping (drawChar w/big text requires this)
if((x >= _width) || (y >= _height)) return;
if(x < 0) { w += x; x = 0; }
if(y < 0) { h += y; y = 0; }
if((x + w - 1) >= _width) w = _width - x;
if((y + h - 1) >= _height) h = _height - y;

// TODO: this can result in a very long transaction time
// should break this into multiple transactions, even though
// it'll cost more overhead, so we don't stall other SPI libs
SPI.beginTransaction(SPISettings(SPICLOCK, MSBFIRST, SPI_MODE0));
setAddr(x, y, x+w-1, y+h-1);
writecommand_cont(ILI9341_RAMWR);
for(y=h; y>0; y--) {
for(x=w; x>1; x--) {
writedata16_cont(color);
}
writedata16_last(color);
if (y > 1 && (y & 1)) {
SPI.endTransaction();
SPI.beginTransaction(SPISettings(SPICLOCK, MSBFIRST, SPI_MODE0));
}
}
SPI.endTransaction();
}

Than you,

David
 
I believe so. I found this posting, bulk-pixel-load https://community.particle.io/t/bulk-pixel-with-adafruit-ssd1351/18403/2, that seems to indicate a bulk load is possible.

I successfully was able to compile the added functions in the Addafruit_SSD1351 .h and .cpp files.

I have spent the last 2 hours attempting to get the new functions called from my example program.

But no luck. It seems that I need to read in the full bmp (image portion) into a buffer (or file) and then call the function with (I believe) a pointer to the buffer, in this case sdbuffer? It seems that the bulk load code I found at the link above does not do any of the typical bmp to tft pixel conversion etc. Perhaps this is done elsewhere in separate code that reads in from the sd card and places it into a buffer?

Your thoughts please. If I can get this working I believe it would help a lot of people out there looking to get images to load on these small OLEDs much faster.

Thank you,
David
 
I load a subset of the full bmp into an array and then write that out to the screen. If you have more ram, or a smaller bmp, you may be able to load the whole thing into an array and then write it out.
 
Hi All

Trying to give back to this great community in any way I can.

I was able to get the code working (see below). I can now load in and display a 128x128 image in less than 200ms which is great given that originally I was only able to achieve the same at 525ms.

As I state in the header, I am positive that this can even be improved upon by others.

Thank you for the help and inspiration to press forward with this project.

Code:
/*************************************************** 
  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;
}
 
Nice one!

What's the breakdown of loading vs displaying the BMP? I.e. How much of your 200ms is spent reading the sd card?

You may get a performance boost by increasing BUFFPIXEL so that each read is larger, but less frequent (at the expect of increased ram usage)
 
It looks to be about 93ms to load from the SD card and 102ms to display on the OLED. Here is a print out for the 3 test images (all color photos).

Code:
init
Initializing SD card...SD OK!

Loading image 'lily128.bmp'
File size: 49206
Image Offset: 54
Header size: 40
No. of Planes: 1
Bit Depth: 24
Image size: 128x128
Imaged Loaded into array in 93 ms
Imaged Loaded into OLED in 102 ms

Loading image 'img1.bmp'
File size: 49206
Image Offset: 54
Header size: 40
No. of Planes: 1
Bit Depth: 24
Image size: 128x128
Imaged Loaded into array in 95 ms
Imaged Loaded into OLED in 102 ms

Loading image 'img2.bmp'
File size: 49206
Image Offset: 54
Header size: 40
No. of Planes: 1
Bit Depth: 24
Image size: 128x128
Imaged Loaded into array in 95 ms
Imaged Loaded into OLED in 103 ms
 
Regarding changing BUFFPIXEL, well rather strange behavior. If I change BUFFPIXEL from 20 to differing values with really no change in load time from the SD card. When I take it up to 80 the load slows down to 160ms and if I take it to 100 I get vertical lines on the display but an 8ms load from the SD card.

Not sure what to make of this.
 
Size of BUFFPIXEL

Regarding changing BUFFPIXEL, well rather strange behavior. If I change BUFFPIXEL from 20 to differing values with really no change in load time from the SD card. When I take it up to 80 the load slows down to 160ms and if I take it to 100 I get vertical lines on the display but an 8ms load from the SD card.

I faced the same problem. One has to increase the size of the variable 'buffidx' to uint16_t:
// uint8_t buffidx = sizeof(sdbuffer); // Original definition
uint16_t buffidx = sizeof(sdbuffer); // Current position in sdbuffer, this works

uint8_t is limited to 0...255, and the array sdbuffer is initiated as "uint8_t sdbuffer[3*BUFFPIXEL];". So the statement "buffidx = sizeof(sdbuffer);" fails if the number of buffered pixels is >85. Strange is, that the total display times increase in the original code for large values of BUFFPIXEL, e.g. 500.
 
Thanks guys, even though it's going on 3 years old this information helped me solve my problems encountered with displaying bitmaps via SDfat library-driven SD card + ST7789 240x240 TFT display. I was having the exact same issue, as I began to realize when I noticed a similar impossibly large filesize as I saw in my example. I added the Serial.print to display # of planes and found it was an identical 19778 - frankly I don't know what a plane even is in the context of the BMP file format - but it clued me in that there must be a similar issue in the way the file was being (incorrectly) read.

If you find yourself in a similar situation reading bitmaps from SD and these numbers show up in your serial monitor, take heed...
 
Was there was a finial fix to this problem, I have implimented the above changes but still have a problem reading in BMP files on a 3.6 with a 7735 display? I still get the following error, with a modified code from the 7735 example file.


Screenshot 2019-09-16 at 09.24.39.png


Code:
#include <Adafruit_GFX.h>    // Core graphics library
#include <ST7735_t3.h> // Hardware-specific library
#include <SPI.h>
#include <SdFat.h>

// This Teensy3 native optimized version requires specific pins
//
#define TFT_SCLK 13  // SCLK can also use pin 14
#define TFT_MOSI 11  // MOSI can also use pin 7
#define TFT_CS   10  // CS & DC can use pins 2, 6, 9, 10, 15, 20, 21, 22, 23
#define TFT_DC    9  //  but certain pairs must NOT be used: 2+10, 6+9, 20+23, 21+22
#define TFT_RST   8  // RST can use any pin
//#define SD_CS     BUILTIN_SDCARD  // CS for SD card, can use any pin

SdFatSdio sd;

ST7735_t3 tft = ST7735_t3(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST);

void setup(void) {
//  pinMode(SD_CS, INPUT_PULLUP);  // keep SD CS high when not using SD card
  Serial.begin(9600);

  tft.initR(INITR_144GREENTAB);

  Serial.print("Initializing SD card...");

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

//  bmpDraw("Ford_002.bmp", 0, 0);
}

void loop() {
// uncomment these lines to draw bitmaps in different locations/rotations!

  tft.fillScreen(ST7735_BLACK); // Clear display
  for (int i=0; i<4; i++)       // Draw 4 parrots
    bmpDraw("Ford_002.bmp", tft.width() / 4 * i, tft.height() / 4 * i);
  delay(1000);
  tft.setRotation(tft.getRotation() + 1); // Inc rotation 90 degrees

}

#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;
}
 
Try replacing SdFatSdio sd; with -
SdFatSdioEX SD;
(beware that if you type that with a capital SD, you will have to change references to sd later in your code to SD, it's case sensitive)

That is the only difference I see between yours and my graphics test sketch in terms of initial setup. Here is the drawBmp function I'm using, it's a little different from what's included I believe, but I don't know how all of this plays out with your ST7735 vs my ST7789 - compare it to yours side by side:

Code:
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();

  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;
        // Set TFT address window to clipped image bounds
        tft.setAddrWindow(x, y, x+w-1, y+h-1);

        for (row=0; row<h; row++) { // For each scanline...

          // 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...  //DIFF
            // 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
            // below was original configuration, red and blue were swapped -
            //b = sdbuffer[buffidx++];
            //g = sdbuffer[buffidx++];
            //r = sdbuffer[buffidx++];
            // corrected -
            r = sdbuffer[buffidx++];
            g = sdbuffer[buffidx++];
            b = sdbuffer[buffidx++];
            tft.pushColor(tft.Color565(r,g,b));
            //pixPos++;
          } // end pixel
        } // end scanline
        Serial.print("Imaged Loaded in ");
        Serial.print(millis() - startTime);
        Serial.println(" ms");
        
      } // end goodBmp
    }
  }

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

Also, check on your libraries, make sure you are using the versions you expected to be.
 
A quick glance of your function vs mine shows a number of differences, and also notice there is a section in mine I needed to change because by default the red and blue were swapped once I got it working!

Like I said I don't know if some of this may be irrelevant/incorrect for you because I'm using an ST7789, so I'd suggest testing changes line by line. I'm sorry I don't understand this display code deeply enough to be precise, it took me some playing around.

The changes in mine that finally worked, were originally adapted from someone's (in another thread here) SSD1306 version of bmpDraw() - a very different display, and by comparison the ST7735 and ST7789 are very similar. So I think the changes that were made when I altered mine were less about the kind of display and more about how the function handles the file and that's what you need - IF that simple change to SdFatSdio first mentioned doesn't magically fix this (which in retrospect could be irrelevant to you! I have a much larger project using the SdFatSdioEX version of the SD object that I'm incorporating the display into, so I wanted compatibility! Try them both if in doubt...)
 
Was there was a finial fix to this problem, I have implimented the above changes but still have a problem reading in BMP files on a 3.6 with a 7735 display? I still get the following error, with a modified code from the 7735 example file.

Brendon, did you figure it out?
I'd like to know if any of the extra info I posted was helpful or if it was something else, and to be honest I've got a few ST7735's I gave up on before moving to the ST7789 (a move made both out of having decided the ST7789 was better for my application, *and* trying to eliminate the batch of cheap ST7735's I've bought on eBay as a source of trouble)! I might go back and play with the ST7735's for another project someday :)
 
I found out the problem, it was the picture conversion removing all the layers from the bitmap it was odd as I saved it all from Photoshop reduced the picture size and it removed the layers. So all the code worked without any problems.

Many thanks for your all your help and support
 
Status
Not open for further replies.
Back
Top