Controlling An ILI9341V TFT Display

I’ve been messing around with Grok to figure out how to control the apparently illusive ILI9341V (please note the ‘V’) TFT display via 4-wire SPI on an Arduino Mega2560, since I wasn’t having much luck in finding many, if any, successful Arduino forum threads about it in general. I was eventually able to get it working with a custom user setup in ‘TFT_eSPI’, which I then named “Setup1b_ILI9341V.h” (see attached). With that I was able to print a 240x240 16-bit RGB565 raw image file in 505ms. Hopefully this will help some folks out.

Setup1b_ILI9341V.h (1.8 KB)

see attached).

The library has the ability to set up inverted colors

I had to not only invert the screen but also invert all color presets and invoke the BGR MADCTL bit.

#define USER_SETUP_INFO "User_Setup"

//  #define TFT_RGB_ORDER TFT_RGB  // Colour order Red-Green-Blue
//  #define TFT_RGB_ORDER TFT_BGR  // Colour order Blue-Green-Red

// #define TFT_INVERSION_ON
// #define TFT_INVERSION_OFF

There was something about the way that TFT_eSPI was sending information to the ILI9341V that it did not like, which is why it had to be done in the sketch itself.

#include <TFT_eSPI.h>
#include <SPI.h>
#include <SdFat_Adafruit_Fork.h>

TFT_eSPI tft = TFT_eSPI();
SdFat SD;
File rawFile;

#define SD_CS     4
#define TFT_BL    6
#define RAW_NAME "logo565.raw"
#define RAW_SIZE 115200

#define IMG_W    240
#define IMG_H    240
#define ROW_PIXELS 240
#define ROW_BYTES  (ROW_PIXELS * 2)

// === 12 ROW BATCH — OPTIMAL & SAFE ===
#define BATCH_ROWS 12
#define BATCH_PIXELS (ROW_PIXELS * BATCH_ROWS)
#define BATCH_BYTES  (BATCH_PIXELS * 2)
static uint16_t batchBuffer[BATCH_PIXELS];  // 5760 bytes — fits in 8KB

#define ILI9341V_BLACK 0x0000
#define ILI9341V_RED   0xF800

SPISettings tftSPI(40000000, MSBFIRST, SPI_MODE0);
SPISettings sdSPI(25000000, MSBFIRST, SPI_MODE0);

// === MADCTL (CCW LANDSCAPE) ===
bool tft_mh = 0;
bool tft_bgr = 1;
bool tft_ml = 0;
bool tft_mv = 1;
bool tft_mx = 1;
bool tft_my = 1;

void writeCommand(uint8_t cmd) {
  digitalWrite(TFT_DC, LOW);
  digitalWrite(TFT_CS, LOW);
  SPI.transfer(cmd);
  digitalWrite(TFT_CS, HIGH);
}

void writeData(uint8_t data) {
  digitalWrite(TFT_DC, HIGH);
  digitalWrite(TFT_CS, LOW);
  SPI.transfer(data);
  digitalWrite(TFT_CS, HIGH);
}

void setAddrWindow(int16_t x, int16_t y, int16_t w, int16_t h) {
  uint16_t x1 = x + w - 1;
  uint16_t y1 = y + h - 1;

  writeCommand(0x2A);
  writeData(x >> 8); writeData(x & 0xFF);
  writeData(x1 >> 8); writeData(x1 & 0xFF);

  writeCommand(0x2B);
  writeData(y >> 8); writeData(y & 0xFF);
  writeData(y1 >> 8); writeData(y1 & 0xFF);

  writeCommand(0x2C);
}

void setMADCTLbits() {
  uint8_t madctl_byte = 0;
  bitWrite(madctl_byte, 2, tft_mh);
  bitWrite(madctl_byte, 3, tft_bgr);
  bitWrite(madctl_byte, 4, tft_ml);
  bitWrite(madctl_byte, 5, tft_mv);
  bitWrite(madctl_byte, 6, tft_mx);
  bitWrite(madctl_byte, 7, tft_my);
  tft.writecommand(0x36);
  tft.writedata(madctl_byte);
}

int freeSRAM() {
  extern int __heap_start, *__brkval;
  int v;
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}

bool displayLogo() {
  rawFile = SD.open(RAW_NAME);
  if (!rawFile || rawFile.size() != RAW_SIZE) return false;

  uint32_t startTime = millis();
  uint32_t fileOffset = 0;

  for (int startY = 0; startY < IMG_H; startY += BATCH_ROWS) {
    int rowsThisBatch = min(BATCH_ROWS, IMG_H - startY);
    int bytesThisBatch = rowsThisBatch * ROW_BYTES;

    // READ
    SPI.beginTransaction(sdSPI);
    digitalWrite(SD_CS, LOW);
    rawFile.seek(fileOffset);
    rawFile.read(batchBuffer, bytesThisBatch);
    digitalWrite(SD_CS, HIGH);
    SPI.endTransaction();

    // DRAW
    setAddrWindow(40, startY, IMG_W, rowsThisBatch);
    SPI.beginTransaction(tftSPI);
    digitalWrite(TFT_CS, LOW);
    digitalWrite(TFT_DC, HIGH);
    for (int i = 0; i < bytesThisBatch / 2; i++) {
      SPI.transfer(batchBuffer[i] >> 8);
      SPI.transfer(batchBuffer[i] & 0xFF);
    }
    digitalWrite(TFT_CS, HIGH);
    SPI.endTransaction();

    fileOffset += bytesThisBatch;
  }

  rawFile.close();
  Serial.print(F("DISPLAY: "));
  Serial.print(millis() - startTime);
  Serial.println(F(" ms"));
  return true;
}

void setup() {
  Serial.begin(9600);
  while (!Serial);
  Serial.println(F("12-ROW BATCH — 505 ms — 1163 BYTES FREE"));

  pinMode(TFT_BL, OUTPUT);
  analogWrite(TFT_BL, 0);
  pinMode(TFT_RST, OUTPUT);
  digitalWrite(TFT_RST, LOW); delay(120);
  digitalWrite(TFT_RST, HIGH); delay(120);

  pinMode(TFT_CS, OUTPUT);
  pinMode(TFT_DC, OUTPUT);
  digitalWrite(TFT_CS, HIGH);
  digitalWrite(TFT_DC, HIGH);

  SPI.begin();
  tft.init();

  tft_mh = 0; tft_bgr = 1; tft_ml = 0; tft_mv = 1; tft_mx = 1; tft_my = 1;
  tft.invertDisplay(true);
  tft.fillScreen(ILI9341V_BLACK);
  setMADCTLbits();

  Serial.print(F("SRAM: "));
  Serial.println(freeSRAM());

  SPI.beginTransaction(sdSPI);
  if (!SD.begin(SD_CS, SD_SCK_MHZ(25))) {
    Serial.println(F("SD FAILED"));
    analogWrite(TFT_BL, 255);
    return;
  }
  SPI.endTransaction();
  Serial.println(F("SD OK"));

  displayLogo();

  for (int i = 0; i <= 255; i += 8) {
    analogWrite(TFT_BL, i);
    delay(3);
  }
}

void loop() {}

As they say, the master is the boss )))
The configuration file can be added directly to the sketch, so each project will use its own settings.
User_Setup.h

If it helps any, here’s the exact product I’m using: Amazon.com: Hosyond 2.8 inch 240x320 IPS Capacitive Touch Screen SPI Serial ILI9341V Driver LCD Display Module for Arduino R3/Mega2560 : Electronics

It definitely won't help me. I use different display models, more than 10, and controllers bigger than ESP32. I've written this library for myself for four display models.
In AVR, the default SPI interface operates at a frequency of only 4 MHz.

Update: Like I said, I was messing around with Grok. I was able to clean out a bunch of unnecessary code. See below:

// Filename: 'Setup1b_ILI9341V.h'

#define TFT_MISO -1
#define TFT_MOSI 51
#define TFT_SCLK 52
#define TFT_CS   9
#define TFT_DC   8
#define TFT_RST  7

#define TFT_BL   6            // LED back-light
#define TFT_BACKLIGHT_ON LOW

#define ILI9341_DRIVER

#define TFT_WIDTH  240
#define TFT_HEIGHT 320

#define SPI_FREQUENCY  40000000
#define SPI_READ_FREQUENCY  20000000
#define SPI_TOUCH_FREQUENCY  2500000

#define TFT_RGB_ORDER TFT_BGR   // BGR panel

#define LOAD_GLCD   // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2  // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define LOAD_FONT4  // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
#define LOAD_FONT6  // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm
#define LOAD_FONT7  // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:.
#define LOAD_FONT8  // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.
#define LOAD_GFXFF  // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts

#define SMOOTH_FONT

In, short- You might want to observe the initialization sequence shown in the code below, but otherwise just set “tft.invertDisplay(true)” in your sketch and use “#define TFT_RGB_ORDER TFT_BGR” in your User Setup file, that way you can use TFT_eSPI’s native colors, but also makes images print in color-correct fashion.

// Filename: ILI9341_Display_Test.ino

#include <TFT_eSPI.h>
#include <SPI.h>
#include <SdFat_Adafruit_Fork.h>

TFT_eSPI tft = TFT_eSPI();
SdFat SD;
File rawFile;

#define SD_CS     4
#define TFT_BL    6
#define RAW_NAME "logo565.raw"
#define RAW_SIZE 115200

#define IMG_W    240
#define IMG_H    240

#define ROW_PIXELS 240
#define ROW_BYTES  (ROW_PIXELS * 2)
#define BATCH_ROWS 12
#define BATCH_PIXELS (ROW_PIXELS * BATCH_ROWS)
#define BATCH_BYTES  (BATCH_PIXELS * 2)

SPISettings tftSPI(40000000, MSBFIRST, SPI_MODE0);
SPISettings sdSPI(25000000, MSBFIRST, SPI_MODE0);

void writeCommand(uint8_t cmd) {
  digitalWrite(TFT_DC, LOW);
  digitalWrite(TFT_CS, LOW);
  SPI.transfer(cmd);
  digitalWrite(TFT_CS, HIGH);
}

void writeData(uint8_t data) {
  digitalWrite(TFT_DC, HIGH);
  digitalWrite(TFT_CS, LOW);
  SPI.transfer(data);
  digitalWrite(TFT_CS, HIGH);
}

void setAddrWindow(int16_t x, int16_t y, int16_t w, int16_t h) {
  uint16_t x1 = x + w - 1;
  uint16_t y1 = y + h - 1;

  writeCommand(0x2A);
  writeData(x >> 8); writeData(x & 0xFF);
  writeData(x1 >> 8); writeData(x1 & 0xFF);

  writeCommand(0x2B);
  writeData(y >> 8); writeData(y & 0xFF);
  writeData(y1 >> 8); writeData(y1 & 0xFF);

  writeCommand(0x2C);
}

void pushColors(uint16_t *data, int len) {
  digitalWrite(TFT_DC, HIGH);
  digitalWrite(TFT_CS, LOW);
  for (int i = 0; i < len; i++) {
    SPI.transfer(data[i] >> 8);
    SPI.transfer(data[i] & 0xFF);
  }
  digitalWrite(TFT_CS, HIGH);
}

bool displayLogoCentered() {
  Serial.print(F("Opening "));
  Serial.println(RAW_NAME);
  rawFile = SD.open(RAW_NAME);
  if (!rawFile || rawFile.size() != RAW_SIZE) {
    Serial.println(F("ERROR: File not found or wrong size"));
    return false;
  }

  Serial.print(F("File OK. Buffering "));
  Serial.print(BATCH_ROWS);
  Serial.println(F(" rows per batch..."));

  uint32_t startTime = millis();

  uint16_t batchBuffer[BATCH_PIXELS];
  uint32_t fileOffset = 0;

  for (int startY = 0; startY < IMG_H; startY += BATCH_ROWS) {
    int rowsThisBatch = min(BATCH_ROWS, IMG_H - startY);
    int pixelsThisBatch = rowsThisBatch * ROW_PIXELS;
    int bytesThisBatch = pixelsThisBatch * 2;

    SPI.beginTransaction(sdSPI);
    digitalWrite(SD_CS, LOW);
    rawFile.seek(fileOffset);
    int read = rawFile.read(batchBuffer, bytesThisBatch);
    digitalWrite(SD_CS, HIGH);
    SPI.endTransaction();

    if (read != bytesThisBatch) {
      Serial.print(F("ERROR: Read failed at offset "));
      Serial.println(fileOffset);
      rawFile.close();
      return false;
    }

    int drawY = startY;
    setAddrWindow(40, drawY, IMG_W, rowsThisBatch);  // ← (40, 0) = centered
    pushColors(batchBuffer, pixelsThisBatch);

    fileOffset += bytesThisBatch;
  }

  rawFile.close();
  uint32_t elapsed = millis() - startTime;
  Serial.print(F("Logo displayed in "));
  Serial.print(elapsed);
  Serial.println(F(" ms"));
  return true;
}

#ifdef __arm__
extern "C" char* sbrk(int incr);
#else
extern char *__brkval;
#endif

int freeMemory() {
  char top;
#ifdef __arm__
  return &top - reinterpret_cast<char*>(sbrk(0));
#else
  return __brkval ? &top - __brkval : &top - __malloc_heap_start;
#endif
}

void setup() {
  Serial.begin(9600);
  while (!Serial);
  Serial.println("=== v40.7.1 — FINAL VICTORY ===");

  pinMode(TFT_BL, OUTPUT);

  pinMode(TFT_RST, OUTPUT);
  digitalWrite(TFT_RST, LOW);
  delay(100);
  digitalWrite(TFT_RST, HIGH);
  delay(100);

  pinMode(TFT_CS, OUTPUT);
  pinMode(TFT_DC, OUTPUT);
  digitalWrite(TFT_CS, HIGH);
  digitalWrite(TFT_DC, HIGH);

  SPI.begin();
  
  tft.init();
  tft.invertDisplay(true);
  tft.setRotation(3);
  tft.fillScreen(TFT_BLACK);

  Serial.print(F("Free RAM: "));
  Serial.println(freeMemory());

  Serial.println(F("SD init..."));
  SPI.beginTransaction(sdSPI);
  if (!SD.begin(SD_CS, SD_SCK_MHZ(25))) {
    Serial.println(F("SD FAILED"));
    analogWrite(TFT_BL, 255);
    return;
  }
  SPI.endTransaction();
  Serial.println(F("SD OK"));

  if (!displayLogoCentered()) {
    tft.fillRect(20, 100, 200, 40, TFT_RED);
  }

  for (int i = 0; i <= 255; i += 5) {
    analogWrite(TFT_BL, i);
    delay(50);
  }
  Serial.println(F("DISPLAY READY — LOGO CENTERED IN LANDSCAPE"));

  delay(1000);
  for (int i = 255; i >= 0; i -= 5) {
    analogWrite(TFT_BL, i);
    delay(10);
  }
  tft.fillScreen(TFT_BLACK);
}

void loop() {}