create snapshot of 3.5" TFT and save to file in bitmap format

In case anyone might find this useful…

// Creates a bitmap (BMP) snapshot of the Adafruit 3.5" screen with HX8357D controller
// The BMP file has 2 bytes per pixel, RGB555 format
//
// The readPixel function was written by David Evans, June 2016
//
// The BMP writing function is a mashup of code from here:
// http://forum.arduino.cc/index.php?topic=177361.0
// http://forum.arduino.cc/index.php?topic=112733.0


/*  Used for testing:
 *   
 *  Magenta: 11111 000000 11111
 *  DkGreen: 00011 011111 00000
 *  Yellow:  11111 111111 00000
 *  Grey:    11011 111000 11100
 *  Red:     11111 000000 00000
 *  LtRed:   11111 110011 11001
 *  Black:   00000 000000 00000
 *  White:   11111 111111 11111
 *  
 *  const unsigned int GREY = 0xDF1C;
 *  const unsigned int LTRED = 0xFE79;
 *  const unsigned int DKGREEN = 0x1BE0;
 */

#include <SPI.h>
#include <SdFat.h>
#include "Adafruit_GFX.h"
#include "Adafruit_HX8357.h"

const byte TFT_DC = 9;
const byte TFT_CS = 10;
const byte TFT_CLK = 13;

const byte SD_CS = 4;

const int w = 480;     // image width in pixels
const int h = 320;     // height

char str[] = "TEST11.BMP";

const unsigned int GREY = 0xDF1C;
const unsigned int LTRED = 0xFE79;
const unsigned int DKGREEN = 0x1BE0;

SdFat SD;
File outFile;

Adafruit_HX8357 tft = Adafruit_HX8357(TFT_CS, TFT_DC);

void setup()
{
  tft.begin(HX8357D);

  // print some objects on TFT to be captured in BMP
  tft.setRotation(3);
  tft.setTextSize(3);
  
  tft.fillScreen(HX8357_BLUE);
  
  tft.drawPixel(13,3,HX8357_WHITE);
  tft.drawPixel(14,3,HX8357_WHITE);
  tft.drawPixel(15,3,HX8357_WHITE);
  
  tft.drawLine(0,1,100,200,HX8357_YELLOW);
  tft.drawLine(0,2,100,202,HX8357_YELLOW);
  tft.drawLine(0,3,100,203,HX8357_YELLOW);
  
  tft.fillRect(150, 150, 30, 30, GREY);
  tft.fillRect(30, 150, 30, 30, LTRED);
  tft.fillRect(150, 30, 30, 30, DKGREEN);
  
  tft.setCursor(50, 50);
  tft.print("hello world");
  // end test print to TFT
  
  Serial.begin(115200);
  Serial.println("starting");
  
  //init SD Card
  if (!SD.begin(SD_CS))
  {
    Serial.println("err strtng SD");
    while (1);    //If failed, stop here
  }

  Serial.println("working");
  GrabImage(str);
  Serial.println("done");
  
  tft.setCursor(100, 100);
  tft.print("Done");
}

void GrabImage(char* str)
{
  byte VH, VL;
  int i, j = 0;

  //Create the File
  outFile = SD.open(str, FILE_WRITE);
  if (! outFile) {
    Serial.println("err opng file");
    return;
  };

  unsigned char bmFlHdr[14] = {
    'B', 'M', 0, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0
  };
  // 54 = std total "old" Windows BMP file header size = 14 + 40
  
  unsigned char bmInHdr[40] = {
    40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 16, 0
  };   
  // 40 = info header size
  //  1 = num of color planes
  // 16 = bits per pixel
  // all other header info = 0, including RI_RGB (no compr), DPI resolution

  unsigned long fileSize = 2ul * h * w + 54; // pix data + 54 byte hdr
  
  bmFlHdr[ 2] = (unsigned char)(fileSize      ); // all ints stored little-endian
  bmFlHdr[ 3] = (unsigned char)(fileSize >>  8); // i.e., LSB first
  bmFlHdr[ 4] = (unsigned char)(fileSize >> 16);
  bmFlHdr[ 5] = (unsigned char)(fileSize >> 24);

  bmInHdr[ 4] = (unsigned char)(       w      );
  bmInHdr[ 5] = (unsigned char)(       w >>  8);
  bmInHdr[ 6] = (unsigned char)(       w >> 16);
  bmInHdr[ 7] = (unsigned char)(       w >> 24);
  bmInHdr[ 8] = (unsigned char)(       h      );
  bmInHdr[ 9] = (unsigned char)(       h >>  8);
  bmInHdr[10] = (unsigned char)(       h >> 16);
  bmInHdr[11] = (unsigned char)(       h >> 24);

  outFile.write(bmFlHdr, sizeof(bmFlHdr));
  outFile.write(bmInHdr, sizeof(bmInHdr));

  for (i = h; i > 0; i--) {
    for (j = 0; j < w; j++) {

      uint16_t rgb = readPixA(j,i); // get pix color in rgb565 format
      
      VH = (rgb & 0xFF00) >> 8; // High Byte
      VL = rgb & 0x00FF;        // Low Byte
      
      //RGB565 to RGB555 conversion... 555 is default for uncompressed BMP
      //this conversion is from ...topic=177361.0 and has not been verified
      VL = (VH << 7) | ((VL & 0xC0) >> 1) | (VL & 0x1f);
      VH = VH >> 1;
      
      //Write image data to file, low byte first
      outFile.write(VL);
      outFile.write(VH);
    }
  }
  //Close the file
  outFile.close();
}

void loop()
{
}

uint16_t readPixA(int x, int y) { // get pixel color code in rgb565 format

  tft.setAddrWindow(x,y,x,y);

  digitalWrite(TFT_DC, LOW);
  digitalWrite(TFT_CLK, LOW);
  digitalWrite(TFT_CS, LOW);
  tft.spiwrite(0x2E); // memory read command

  digitalWrite(TFT_DC, HIGH);

  uint16_t r = 0;
  r = tft.spiread(); // discard dummy read
  r = tft.spiread() >> 3; // red: use 5 highest bits (discard three LSB)
  r = (r << 6) | tft.spiread() >> 2; // green: use 6 highest bits (discard two LSB)
  r = (r << 5) | tft.spiread() >> 3; // blue: use 5 highest bits (discard three LSB)

  digitalWrite(TFT_CS, HIGH);

  return r;
}

The attachment is the test image…oops…can’t attach BMP…oh well.

I suggest that you write a readGRAM() function that reads a block of memory. You can always choose to do it one pixel at a time. i.e. readPixel()

Modern MIPI controllers will increment the read address. Old controllers like ILI9325 do not increment (on reading). Hence can only read one pixel at a time.

Note that most controllers are considerably slower on a read cycle. This is probably not noticeable with AVR SPI. An ARM can write faster.

David.

Thank you for the suggestion.

While working on this I noticed that the “memory read” command documentation said the command auto-increments the column and page register, and I tried to get that to work. My first attempt failed so I bailed and just wrote the readPixel() function.

It takes 33 seconds to read and write all 480 x 320 pixels to the file; the file is about 306 kB. That amount of time is ok for my needs, which is an end-of-day snapshot of my graph.

P1020165s.jpg

Hello,

Sorry to reopen a really old thread - but your code snippet is the one I've been able to almost get to work.

I'm using an Adafruit #2050 3.5" 320x480 TFT display (HXD8357) and am trying to get a screenshot saved to the SD card. I'm using the snippet above but I had to add the following so that the SD card would initialize:

pinMode(SD_CS, OUTPUT); digitalWrite(SD_CS, HIGH);

The sketch runs and a BMP file is successfully written to the SD card but the it is all white. After the BMP header the same value, 'FF7F' is repeated for the remainder of the file.

Any suggestions or help would be greatly appreciated.

Thanks,

From the sketch in #0 it looks fairly good. It should create a legal BMP file on SD disk. View the BMP file on your PC.

Note that most Arduino BMP programs will not handle this format. They expect 24-bit pixels.

If you really want to create BMP files, please post links to the actual screen and quote library version. If you just want to dump fixed size images, you can read 16-bit pixels and dump as a RAW file of uint16_t values.

Personally, I prefer a standard format like BMP that is understood by different devices all over world.

David.

Here is the display I’m using:

Library: Adafruit HX8357 Version 1.1.10

A valid BMP is created on the SD card and when viewing on a PC it just shows all white instead of the test graphics from the sketch/display. Here is a hex view of the beginning of the BMP file:

42 4D 36 B0 04 00 00 00 00 00 36 00 00 00 28 00 00 00 E0 01 00 00 40 01 00 00 01 00 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F

The FF7F repeats to the end of the file.

I was wondering if anyone else had seen this issue or had an idea of what the problem could be.

Thanks.

The sketch in #0 would have been fine in 2016.

However Adafruit has made massive changes to its libraries in the intervening years.

setAddrWindow(x, y, w, h) has replaced setAddrWindow(x, y, x1, y1)
several methods require a specific startWrite() and endWrite() wrapper
spiwrite() is now called spiWrite()

the Library Manager encourages you to install/update several unrelated classes.
this guarantees that Adafruit_GFX will not compile on several targets
nothing to do with GFX. all to do with the “unrelated classes”

I don’t have an SPI HX8357. I can only test on Adafruit_ILI9341

Do you really want a 16-bit BMP file ?

David.

uint16_t readPixA(int x, int y) { // get pixel color code in rgb565 format

    tft.startWrite();    //needed for low-level methods.  CS active
    tft.setAddrWindow(x, y, 1, 1);
    tft.writeCommand(0x2E); // memory read command.  sets DC

    uint8_t r, g, b;
    r = tft.spiRead(); // discard dummy read
    r = tft.spiRead();
    g = tft.spiRead();
    b = tft.spiRead();
    tft.endWrite();    //needed for low-level methods.  CS idle

    return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3);
}

Thank you David - your updated readPixA routine fixed the issue for me.

david_prentice:
Do you really want a 16-bit BMP file ?

My goal was to get a snapshot of a custom UI for use on a website. I didn’t have a strong preference on output format, just needed something that was usable (or could be converted to something usable).

Thanks again,

A PC can display all legal BMP formats. So 16-biit wil be fine.

Note that the example uses width = 480 pixels. So no need for padding the row to 32-bit boundary. A general purpose BMP create should cope with coordinates, geometry, format, padding, ...

Be wary of Adafruit changing class methods and dependencies at some point in the future.

David.

For reference, another data point.

I also have this display:

https://www.adafruit.com/product/2478

using this library:

Adafruit ILI9341 Version 1.5.5

With the modifications the sketch also works successfully with that display/library combination.

In the old days, Adafruit would provide a spitftbmp.ino example with each library e.g. Adafruit_ST7735, Adafruit_ILI9341, Adafruit_HX8357, …

They seem to have removed this example from everything except Adafruit_HX8357
You should be able to simply edit the sketch header and costructor to suit your hardware screen. e.g.

#include <Adafruit_ILI9341.h>
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);

Note that Adafruit_ST7735 has a different begin() syntax called initR()

Since the library examples expect 24-bit, I recommend that you edit the sketch in #0 to write 24-bit format in the first place.

You can create in 16-bit. Then get IrfanView to save in 24-bit.

Most Arduino library examples only handle 24-bit format.
PC programs default to 24-bit format.

Note that “old” Adafruit examples have a different setAddrWindow() style.

David.