White Dots appear on my TFT Screen (Coding Issue)

I'm currently working on a project for a smart planter, and I've run into a strange issue. I'm using an Arduino Nano Every, Micro SD Card, and a WaveShare Round 1.28-inch TFT Screen. The main background images are displaying correctly, and I haven't encountered any problems with them. However, I'm trying to display a smaller image at the top of the screen using void drawRainDrop(), and I'm seeing 8 white dots evenly spaced on the y coordinates on the left-hand side, all sharing the same x coordinates. The image itself is displayed


nicely with no distortions and proper colors. The images are all formatted correctly as 16-bit rgb565 BMP images, and the size of the image I want to display is 45x45. If anyone has experienced this issue and knows how to solve it, I would greatly appreciate some insight. Thank you very much!

Here is the code that handles the image processing for the 45x45 image:

void drawRaindrop() {
    File bmpFile = SD.open("raindrop.bmp", FILE_READ);
    if (!bmpFile) {
        Serial.println("Could not open raindrop.bmp");
        return;
    }

    uint32_t bmpDataOffset, imageWidth, imageHeight;
    if (readBMPHeader(bmpFile, bmpDataOffset, imageWidth, imageHeight) != 0) {
        Serial.println("Error reading BMP header!");
        bmpFile.close();
        return;
    }

    // Adjust startX calculation to center the image horizontally
    int startX = (tft.width() - imageWidth) / 2;
    int startY = 190; // Set the starting y-coordinate

    tft.startWrite();

    int bytesPerPixel = 2; // For 16-bit images
    int rowSizeWithPadding = (imageWidth * bytesPerPixel + 3) & (~3); // Calculate total bytes per row, including padding
    int padding = rowSizeWithPadding - imageWidth * bytesPerPixel; // Calculate padding size

    uint16_t rowBuffer[imageWidth]; // Buffer to hold a single row of pixel data

    bmpFile.seek(bmpDataOffset);

    for (int y = startY; y < startY + imageHeight; y++) {
        bmpFile.read((uint8_t*)rowBuffer, imageWidth * bytesPerPixel);
        for (int x = 0; x < imageWidth; x++) {
            tft.writePixel(x + startX, y, rowBuffer[x]);
        }
        // Skip padding bytes
        bmpFile.seek(bmpFile.position() + padding);
    }

    tft.endWrite();
    bmpFile.close();
}

I think you will find the people here will want to see the complete code and circuit diagram.

Do you have anything in the source code like tft.print(".")

Have you checked the dots are not in the BMP file?

No, I have nothing like that. I believe it is how the "packets" are being processed and then displayed

No, I edited and reloaded the BMP image multiple times, and also tried different images, which is why I believe it is an issue with how my code processes these images.

Seems like some amount work, but you could use the serial monitor and dump each line you read from the image file there before you publish it to the display, just to see what is what.

a7

Last night I stayed up for quite some time working on this problem. I decided to try and load the entire image into a "buffer" in the flash memory, load it to the TFT screen, and then delete the "buffer". This got rid of the white dots, however somewhere in my code I messed up, and there is a white "gash" going through the center of my screen. (The white line underneath the raindrop is meant to be there).

void drawRaindrop() {
    File bmpFile = SD.open("raindrop.bmp", FILE_READ);
    if (!bmpFile) {
        Serial.println("Could not open raindrop.bmp");
        return;
    }

    uint32_t bmpDataOffset, imageWidth, imageHeight;
    if (readBMPHeader(bmpFile, bmpDataOffset, imageWidth, imageHeight) != 0) {
        Serial.println("Error reading BMP header!");
        bmpFile.close();
        return;
    }

    // Adjust startX and startY positions
    int startX = 97;
    int startY = 190;

    tft.startWrite();

    uint16_t* imageDataBuffer = new uint16_t[imageWidth * imageHeight]; // Buffer to hold entire image data

    int bytesPerPixel = 2; // For 16-bit images
    int rowSizeWithPadding = (imageWidth * bytesPerPixel + 3) & (~3); // Calculate total bytes per row, including padding
    int padding = rowSizeWithPadding - imageWidth * bytesPerPixel; // Calculate padding size

    bmpFile.seek(bmpDataOffset);

    // Read entire image data into buffer
    for (int y = 0; y < imageHeight; y++) {
        bmpFile.read((uint8_t*)(imageDataBuffer + y * imageWidth), imageWidth * bytesPerPixel);
        // Skip padding bytes
        bmpFile.seek(bmpFile.position() + padding);
    }

    // Write entire image data to TFT screen
    for (int y = 0; y < imageHeight; y++) {
        for (int x = 0; x < imageWidth; x++) {
            tft.writePixel(x + startX, y + startY, imageDataBuffer[y * imageWidth + x]);
        }
    }

    tft.endWrite();
    bmpFile.close();

    // Free the dynamically allocated buffer
    delete[] imageDataBuffer;
}


OK, now you'll have to post a complete sketch. Either your real,project sketch, or a minimum sketch that shows this problem we can read, compile and test for ourseffs.

Is that in your code, for example?

a7

This is the full code. The transferBMPtoTFT works perfectly, and when I try to structure the drawRaindrop() like it it causes my image to have that weird distortion.

#include <Adafruit_GFX.h>
#include <Adafruit_GC9A01A.h>
#include <SPI.h>
#include <SD.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/FreeSans9pt7b.h>

#define SD_CS_PIN 6
#define TFT_CS_PIN 3
#define TFT_DC_PIN 4
#define TFT_RESET 7
#define TFT_BLACK 0x0000
#define CHUNK_SIZE 2048  // Adjust this value to optimize performance

// Define user-defined variables for desired width and height
uint32_t desiredWidth = 240; // Default width
uint32_t desiredHeight = 120; // Default height

#define AOUT_PINA A6
#define AOUT_PINB A5
#define LightAOUT_PIN A4
#define BUTTON_PIN 2  // Change this to an interrupt-capable pin (e.g., 2 or 3 on most Arduino boards)

Adafruit_GC9A01A tft(TFT_CS_PIN, TFT_DC_PIN, TFT_RESET);

const char* imageFilenames[] = { "image1.bmp", "image2.bmp", "image3.bmp", "image4.bmp", "image5.bmp", "image6.bmp", "image7.bmp", "image8.bmp", "image9.bmp", "image10.bmp", "raindrop.bmp" };
const int numImages = sizeof(imageFilenames) / sizeof(imageFilenames[0]);

int currentImageIndex = 0;  // Variable to keep track of the current image index

unsigned long previousMillis = 0;
const unsigned long interval = 5000;  // Change the interval to 10 seconds
float averageMoisture = 0.0;
int lightValue = 0;
const char* plantTypes[] = { "Succulent", "Grasses", "Cactus", "Fern", "Flowers", "Bonsai", "Herbs", "Vines", "Broad Leaf", "Moss" };
int plantTypeIndex = 0;
const int moistureRanges[][2] = {
  { 550, 570 },  // Succulent
  { 460, 490 },  // Native Grass
  { 550, 570 },  // Cactus
  { 340, 370 },  // Fern
  { 400, 430 },  // Orchid
  { 490, 520 },  // Bonsai
  { 460, 490 },  // Herb
  { 400, 430 },  // Vine
  { 400, 430 },  // Broad Leaf
  { 310, 340 }   // Moss
};

unsigned long lastWateredTime = 0;  // Variable to store the time when the plant was last watered
unsigned long elapsedHours = 0;     // Variable to store elapsed hours since last watering
unsigned long elapsedMinutes = 0;   // Variable to store elapsed minutes since last watering

int currentMoistureValue = 0;  // Global variable to store the current moisture value

float previousAverageMoisture = 0.0;

enum DisplayMode {
  LAST_WATERED,
  LIGHT_LEVEL,
  CUSTOM_PROMPT
};

DisplayMode currentMode = LAST_WATERED;
unsigned long animationTimer = 0;
const unsigned long animationInterval = 5000;  // 5 seconds

bool shouldDrawRaindrop = false; // Flag to track if raindrop needs to be drawn

int readBMPHeader(File& bmpFile, uint32_t& bmpDataOffset, uint32_t& actualWidth, uint32_t& actualHeight) {
    static uint16_t bmpSignature;
    static uint32_t bmpFileSize;

    // Read BMP file signature
    bmpFile.read((uint8_t*)&bmpSignature, sizeof(bmpSignature));
    if (bmpSignature != 0x4D42) {
        return 1; // Invalid BMP file signature
    }

    // Read BMP file size
    bmpFile.read((uint8_t*)&bmpFileSize, sizeof(bmpFileSize));

    // Skip over the reserved fields
    bmpFile.seek(10);

    // Read BMP data offset
    bmpFile.read((uint8_t*)&bmpDataOffset, sizeof(bmpDataOffset));

    // Read actual image width and height
    bmpFile.seek(18);
    bmpFile.read((uint8_t*)&actualWidth, sizeof(actualWidth));
    bmpFile.read((uint8_t*)&actualHeight, sizeof(actualHeight));

    // Skip past the remaining header data
    bmpFile.seek(bmpDataOffset);

    return 0;
}

void transferBMPToTFT(File& bmpFile) {
    uint32_t bmpDataOffset, actualWidth, actualHeight;

    if (readBMPHeader(bmpFile, bmpDataOffset, actualWidth, actualHeight) != 0) {
        Serial.println("Error reading BMP header!");
        bmpFile.close();
        return;
    }

    // Check if the user-defined width and height exceed the actual image dimensions
    if (desiredWidth > actualWidth || desiredHeight > actualHeight) {
        Serial.println("User-defined dimensions exceed the actual image dimensions!");
        bmpFile.close();
        return;
    }

    uint32_t rowSize = (desiredWidth * 2 + 3) & ~3; // Calculate the row size with padding
    uint16_t* buffer = new uint16_t[desiredWidth]; // Dynamically allocate buffer based on desired width

    bmpFile.seek(bmpDataOffset);
    for (int y = 0; y < desiredHeight; y++) { // Loop through all rows
        bmpFile.read((uint8_t*)buffer, desiredWidth * sizeof(uint16_t)); // Read the desired width of pixels

        // Swap the byte order of RGB565 pixel data
        for (int x = 0; x < desiredWidth; x++) {
            buffer[x] = ((buffer[x] << 8) & 0xFF00) | ((buffer[x] >> 8) & 0x00FF);
        }

        tft.startWrite();
        tft.writePixels(buffer, desiredWidth, 0, y + (120 - desiredHeight) / 2); // Center the image vertically
        tft.endWrite();

        // Skip over any padding bytes at the end of the row
        bmpFile.seek(bmpFile.position() + rowSize - desiredWidth * 2);
    }

    // Free the dynamically allocated buffer
    delete[] buffer;
}


void drawRaindrop() {
    File bmpFile = SD.open("raindrop.bmp", FILE_READ);
    if (!bmpFile) {
        Serial.println("Could not open raindrop.bmp");
        return;
    }

    uint32_t bmpDataOffset, imageWidth, imageHeight;
    if (readBMPHeader(bmpFile, bmpDataOffset, imageWidth, imageHeight) != 0) {
        Serial.println("Error reading BMP header!");
        bmpFile.close();
        return;
    }

    // Adjust startX and startY positions
    int startX = 97;
    int startY = 190;

    tft.startWrite();

    uint16_t* imageDataBuffer = new uint16_t[imageWidth * imageHeight]; // Buffer to hold entire image data

    int bytesPerPixel = 2; // For 16-bit images
    int rowSizeWithPadding = (imageWidth * bytesPerPixel + 3) & (~3); // Calculate total bytes per row, including padding
    int padding = rowSizeWithPadding - imageWidth * bytesPerPixel; // Calculate padding size

    bmpFile.seek(bmpDataOffset);

    // Read entire image data into buffer
    for (int y = 0; y < imageHeight; y++) {
        bmpFile.read((uint8_t*)(imageDataBuffer + y * imageWidth), imageWidth * bytesPerPixel);
        // Skip padding bytes
        bmpFile.seek(bmpFile.position() + padding);
    }

    // Write entire image data to TFT screen
    for (int y = 0; y < imageHeight; y++) {
        for (int x = 0; x < imageWidth; x++) {
            tft.writePixel(x + startX, y + startY, imageDataBuffer[y * imageWidth + x]);
        }
    }

    tft.endWrite();
    bmpFile.close();

    // Free the dynamically allocated buffer
    delete[] imageDataBuffer;
}




void changeImageAndPlantType() {
  // Find the next image filename that is not "raindrop.bmp"
  do {
    currentImageIndex = (currentImageIndex + 1) % (numImages - 1); // Exclude the last index (raindrop.bmp)
  } while (strcmp(imageFilenames[currentImageIndex], "raindrop.bmp") == 0);

  plantTypeIndex = (plantTypeIndex + 1) % (sizeof(plantTypes) / sizeof(plantTypes[0]));
  currentMoistureValue = (moistureRanges[plantTypeIndex][0] + moistureRanges[plantTypeIndex][1]) / 2;

  // Clear the screen before displaying the new image and text
  //tft.fillScreen(TFT_BLACK);
  tft.fillRect(0, 0, 240, 160, 0x0000);
  // Open and display the new image
  File bmpFile = SD.open(imageFilenames[currentImageIndex], FILE_READ);
  if (!bmpFile) {
    Serial.print("Could not open ");
    Serial.println(imageFilenames[currentImageIndex]);
    return;
  }

  transferBMPToTFT(bmpFile);
  bmpFile.close();

  // Draw the raindrop image after displaying the background image
  drawRaindrop();

  updateDisplay();
}

void buttonPressed() {
  static unsigned long lastInterruptTime = 0;
  unsigned long interruptTime = millis();

  // Debounce the button press
  if (interruptTime - lastInterruptTime > 200) {
    changeImageAndPlantType();
    lastInterruptTime = interruptTime;
  }
}

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonPressed, FALLING);

  readSoilMoisture();                         // Calculate the initial averageMoisture value
  previousAverageMoisture = averageMoisture;  // Store the initial value in previousAverageMoisture

  SPI.begin();
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("SD card initialization failed!");
    return;
  }
  tft.begin();
  tft.fillScreen(TFT_BLACK);  // Clear the screen initially

  Serial.println("GC9A01A Test!");
  tft.setRotation(2);  // Set rotation to 0 before displaying text
  tft.fillRoundRect(75, 50, 90, 2, .25, 0xFFFF);

  // Read soil moisture and calculate currentMoistureValue
  readSoilMoisture();
  currentMoistureValue = (moistureRanges[plantTypeIndex][0] + moistureRanges[plantTypeIndex][1]) / 2;

  tft.setTextSize(1);
  tft.setFont(&FreeSansBold9pt7b);
  tft.setTextColor(0xffff);


  // Calculate the position to center "Soil Moisture:" at (120, 60)
  const char* text = "Soil Moisture";
  int16_t x1, y1;
  uint16_t width, height;
  tft.getTextBounds(text, 0, 0, &x1, &y1, &width, &height);
  int16_t x = 120 - width / 2;
  int16_t y = 75;

  // Display "Soil Moisture:" centered at (120, 50)
  tft.setCursor(x, y);
  tft.println(text);

  tft.setFont(&FreeSans9pt7b);
  tft.setCursor(60, 120);
  tft.println(averageMoisture);

  tft.setTextColor(0xf800);
  tft.setCursor(120, 120);
  tft.print(currentMoistureValue);

  tft.setTextColor(0xffff);
  tft.setRotation(0);
}

void loop() {
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    readSoilMoisture();
    readLightLevel();

    tft.setRotation(2);
    tft.setTextSize(1);
    tft.setTextColor(0x0000);
    tft.setFont(&FreeSans9pt7b);

    tft.setCursor(60, 120);
    tft.println(previousAverageMoisture);
    tft.setRotation(0);

    noInterrupts();                             // Disable interrupts temporarily
    previousAverageMoisture = averageMoisture;  // Store the current averageMoisture value
    updateDisplay();
    updateTimeSinceWatering(currentMillis);
    interrupts();
    // Re-enable interrupts

    previousMillis = currentMillis;
  }

  if (currentMillis - animationTimer >= animationInterval) {
    animationTimer = currentMillis;
    noInterrupts();
    updateAnimationMode();
    interrupts();
  }
}

void readSoilMoisture() {
  int valueA = analogRead(AOUT_PINA);
  int valueB = analogRead(AOUT_PINB);
  int sum = valueA + valueB;
  averageMoisture = sum / 2.0;

  //Serial.print("Moisture: ");
  //Serial.println(averageMoisture);

  if (averageMoisture < 310) {
    updateLastWateredTime();
    elapsedHours = 0;    // Reset elapsed hours
    elapsedMinutes = 0;  // Reset elapsed minutes
  }
}

void updateLastWateredTime() {
  lastWateredTime = millis();
}

void readLightLevel() {
  lightValue = analogRead(LightAOUT_PIN);
}

void updateDisplay() {
  tft.setRotation(2);
  tft.setTextSize(1);
  tft.setTextColor(0xFFFF);
  tft.setFont(&FreeSansBold9pt7b);

  // Calculate the position to center "Soil Moisture:" at (120, 60)
  const char* text = "Soil Moisture";
  int16_t x1, y1;
  uint16_t width, height;
  tft.getTextBounds(text, 0, 0, &x1, &y1, &width, &height);
  int16_t x = 120 - width / 2;
  int16_t y = 75;

  // Display "Soil Moisture:" centered at (120, 50)
  tft.setCursor(x, y);
  tft.println(text);

  tft.setFont(&FreeSans9pt7b);

  tft.setCursor(60, 120);
  tft.println(averageMoisture);

  tft.setTextColor(0xf800);
  tft.setCursor(120, 120);
  tft.print(currentMoistureValue);
  tft.setRotation(0);
}

void updateTimeSinceWatering(unsigned long currentTime) {
  unsigned long elapsedTime = currentTime - lastWateredTime;
  unsigned long elapsedSeconds = elapsedTime / 1000;
  elapsedMinutes = elapsedSeconds / 60;
  elapsedHours = elapsedMinutes / 60;

  elapsedMinutes %= 60;
}

void updateAnimationMode() {
  /* tft.setTextColor(0xFFFF);

void updateAnimationMode() {
  /* tft.setTextColor(0xFFFF);
  tft.setCursor(60, 165);
  tft.setTextSize(1);

  tft.setRotation(2);

  switch (currentMode) {
    case LAST_WATERED:
      tft.setCursor(60, 145);
      tft.print("Last Watered: ");
      tft.setCursor(60, 170);
      tft.print(elapsedHours);
      tft.print("h ");
      tft.print(elapsedMinutes);
      tft.print("m ");
      currentMode = LIGHT_LEVEL;
      break;
    case LIGHT_LEVEL:
      tft.setCursor(60, 145);
      tft.print("Light Level: ");
      tft.setCursor(60, 170);
      tft.print(lightValue);
      currentMode = CUSTOM_PROMPT;
      break;
    case CUSTOM_PROMPT:
      tft.setCursor(60, 145);
      tft.print("Smart Tip: ");
      tft.setCursor(60, 170);

      // Check if the plant needs to be watered
      int targetMoistureValue = (moistureRanges[plantTypeIndex][0] + moistureRanges[plantTypeIndex][1]) / 2;
      unsigned long timeSinceLastWatered = (millis() - lastWateredTime) / 60000;  // Time in minutes

      if (averageMoisture > moistureRanges[plantTypeIndex][0] + 75 && timeSinceLastWatered >= 5) {
        tft.print("Needs Water");
      } else if (averageMoisture < moistureRanges[plantTypeIndex][1] - 75) {
        tft.print("Too Wet");
      } else {
        tft.print("No Action");
      }

      currentMode = LAST_WATERED;
      break;
  }
  tft.setRotation(0); */
}

I found the issue. The white "gash" was appearing because I wrote tft.startWrite() in the wrong spot. I simply moved it down which fixed the issue. Thank you for your help!

void drawRaindrop() {
    File bmpFile = SD.open("raindrop.bmp", FILE_READ);
    if (!bmpFile) {
        Serial.println("Could not open raindrop.bmp");
        return;
    }

    uint32_t bmpDataOffset, imageWidth, imageHeight;
    if (readBMPHeader(bmpFile, bmpDataOffset, imageWidth, imageHeight) != 0) {
        Serial.println("Error reading BMP header!");
        bmpFile.close();
        return;
    }

    // Adjust startX and startY positions
    int startX = 97;
    int startY = 190;



    uint16_t* imageDataBuffer = new uint16_t[imageWidth * imageHeight]; // Buffer to hold entire image data

    int bytesPerPixel = 2; // For 16-bit images
    int rowSizeWithPadding = (imageWidth * bytesPerPixel + 3) & (~3); // Calculate total bytes per row, including padding
    int padding = rowSizeWithPadding - imageWidth * bytesPerPixel; // Calculate padding size

    bmpFile.seek(bmpDataOffset);

    // Read entire image data into buffer
    for (int y = 0; y < imageHeight; y++) {
        bmpFile.read((uint8_t*)(imageDataBuffer + y * imageWidth), imageWidth * bytesPerPixel);
        // Skip padding bytes
        bmpFile.seek(bmpFile.position() + padding);
    }

    tft.startWrite();
    // Write entire image data to TFT screen
    for (int y = 0; y < imageHeight; y++) {
        for (int x = 0; x < imageWidth; x++) {
            tft.writePixel(x + startX, y + startY, imageDataBuffer[y * imageWidth + x]);
        }
    }

    tft.endWrite();
    bmpFile.close();

    // Free the dynamically allocated buffer
    delete[] imageDataBuffer; 
}

Do you know why that fixed anything?

Possibly a variation of this caveat:

  1. If your are using tft.startWrite() in your code make sure you use tft.endWrite() before you access any other SPI devices. The red stripes are likely to be cause by messages that are destined for the RF522 are also going to the TFT.

On that logic, I would also move

    bmpFile.close();

up in the code s far as possible. It might never be an issue, on the other hand weird things that don't seem like they should be happening do, so why not?

It reads better, too.

a7

I do, I originally put the command before reading the image into the "buffer"

    tft.startWrite();
    // Read entire image data into buffer
    for (int y = 0; y < imageHeight; y++) {
        bmpFile.read((uint8_t*)(imageDataBuffer + y * imageWidth), imageWidth * bytesPerPixel);
        // Skip padding bytes
        bmpFile.seek(bmpFile.position() + padding);
    }
    
    // Write entire image data to TFT screen
    for (int y = 0; y < imageHeight; y++) {
        for (int x = 0; x < imageWidth; x++) {
            tft.writePixel(x + startX, y + startY, imageDataBuffer[y * imageWidth + x]);
        }
    }

    tft.endWrite();
    bmpFile.close();

This caused som internal error, which caused the Arduino to try and write the image data to the screen twice. The first time would be the raw image data stored into the buffer, without dimensions and constraints. The second time it would follow the proper format and display the image correctly. The white gash was a result of the first image rendering. So the fix was moving tft.startWrite(); as far forwards as I could. This would stop any unwanted data from being displayed.


    // Read entire image data into buffer
    for (int y = 0; y < imageHeight; y++) {
        bmpFile.read((uint8_t*)(imageDataBuffer + y * imageWidth), imageWidth * bytesPerPixel);
        // Skip padding bytes
        bmpFile.seek(bmpFile.position() + padding);
    }
    
    tft.startWrite();
    // Write entire image data to TFT screen
    for (int y = 0; y < imageHeight; y++) {
        for (int x = 0; x < imageWidth; x++) {
            tft.writePixel(x + startX, y + startY, imageDataBuffer[y * imageWidth + x]);
        }
    }

    tft.endWrite();
    bmpFile.close();

Again, thank you for your time and help!