chebe: (Default)
chebe ([personal profile] chebe) wrote2022-10-19 08:05 pm
Entry tags:

Adafruit HalloWing M0 displaying images from SD card through Arduino

I went to an event at the weekend. Which meant I needed an event badge. Time to dust off my HalloWing badge. I finished the last post saying I would update once I got the badge reading images from the SD card shield. Well, dear reader, I was pleasantly surprised to discover that at some point I had gotten it working! At least to the point of auto-rotating through all the images on the card.

I don't really remember what I did to get the SD card working, but from the code the main bits are;
#include <SPI.h>
#include <SD.h>

const int chipSelect = 10;
File root;

void setup(void) {
  if (!SD.begin(chipSelect)) {
    Serial.println("initialization failed!");
    while (1);
  }
  Serial.println("initialization done.");

  // List files on the SD card
  root = SD.open("/");
  printDirectory(root, 0);
  root.close();
}

printDirectory(root, 0) uses dir.openNextFile(); which sorts through your files alphabetically. bmpDraw(...) (see code below) is what what reads the bmp file and translates it into what the tft wants. Nice.

But I wanted more. First thing you need to know is that Adafruit have moved on and really want us to use the Arcada library to work with the badges in Arduino. But if I wanted a simple life I would have gone with the CircuitPython option. I travelled the frustrating path instead.

I set up two of the buttons to 'scroll' left/right through the images. The images have overlays that display matching usernames. One of the buttons toggles on/off the tft backlight, and the other button toggles on/off the on-board NeoPixel. For a while things weren't working, and after a lot of digging I discovered that Adafruit had made breaking changes, including deprecating a function they had used in the examples I was working from.

setAddrWindow(...) was changed, from (start_x, start_y, end_x, end_y) to (start_x, start_y, width, height). So the change was from;
tft.setAddrWindow(x, y, x+w-1, y+h-1);
to;
tft.startWrite();
tft.setAddrWindow(x, y, w, h);
tft.endWrite();


pushColor(...) is deprecated. After experimentation I discovered that;
tft.pushColor(tft.color565(r,g,b));
needs to be replaced with;
tft.startWrite();
tft.writePixel(col, row, tft.color565(r,g,b));
tft.endWrite();


For the buttons a library is required;
#include <Adafruit_FreeTouch.h>
Then you need to create each of the four buttons (A2, A3, A4, A5);
Adafruit_FreeTouch qt_1 = Adafruit_FreeTouch(A2, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);
Each of which you qt_1.begin(); in setup(void), and then read with qt_1.measure() as in;
if (qt_1.measure() > 700) { /* do stuff */ }

The backlight and NeoPixel parts were straight-forward. And the only other thing I did was add an extender to the battery JST connector so it's easier to reach. I wrapped the leads around the shield, and tucked the battery between the shield and board.




Back of the badge, with SD card shield in place, and battery tucked between
Photo by [personal profile] chebe






Front of the badge, displaying an image with overlay, and NeoPixel backlit
Photo by [personal profile] chebe






// Adafruit Hallowing M0 SAMD21
// Switch on to detect over port
// This is using the datalogger wing (no rtc, just sd card)
// Device does have battery charging functionality

#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7735.h> // Hardware-specific library for ST7735
// include the SD library:
#include <SPI.h>
#include <SD.h>
// buttons
#include <Adafruit_FreeTouch.h>
#include <Adafruit_NeoPixel.h>

#define TFT_CS     39
#define TFT_RST    37
#define TFT_DC     38
#define TFT_BACKLIGHT 7

Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS,  TFT_DC, TFT_RST);
bool backlightState = LOW;

Adafruit_FreeTouch qt_1 = Adafruit_FreeTouch(A2, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);
Adafruit_FreeTouch qt_2 = Adafruit_FreeTouch(A3, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);
Adafruit_FreeTouch qt_3 = Adafruit_FreeTouch(A4, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);
Adafruit_FreeTouch qt_4 = Adafruit_FreeTouch(A5, OVERSAMPLE_4, RESISTOR_50K, FREQ_MODE_NONE);

// set up variables using the SD utility library functions:
File root;
File myFile;
bool updateImage = false;

const int NumSocialFileNames = 4;
String socialFileNames[NumSocialFileNames] = {"00.BMP", "01.BMP", "02.BMP", "03.BMP"};
const char* socialUserNames[NumSocialFileNames] = {"username1", "username2", "username3", "username4"};

int currentFileIndex = 0;

// red LED on D13
// NeoPixel on D8
Adafruit_NeoPixel onBoard = Adafruit_NeoPixel(1, 8, NEO_GRB + NEO_KHZ800);
bool neopixelState = false;
uint8_t j = 0;

const int chipSelect = 10;

void setup(void) {
  // Start TFT and fill black
  tft.initR(INITR_144GREENTAB);
  tft.setRotation(2);
  tft.fillScreen(ST77XX_BLACK);

  // Turn on backlight
  pinMode(TFT_BACKLIGHT, OUTPUT);
  backlightState = HIGH;
  digitalWrite(TFT_BACKLIGHT, backlightState);
  
  tft.setCursor(0, 0);
  tft.setTextColor(ST77XX_GREEN);
  tft.setTextWrap(true);

  if (!SD.begin(chipSelect)) {
    Serial.println("initialization failed!");
    while (1);
  }
  //Serial.println("initialization done.");

  // List files on the SD card
  root = SD.open("/");
  printDirectory(root, 0);
  root.close();

  // Set up buttons
  if (! qt_1.begin())  
    Serial.println("Failed to begin qt on pin A2");
  if (! qt_2.begin())  
    Serial.println("Failed to begin qt on pin A3");
  if (! qt_3.begin())  
    Serial.println("Failed to begin qt on pin A4");
  if (! qt_4.begin())  
    Serial.println("Failed to begin qt on pin A5");

  // Set up NeoPixel
  onBoard.begin();
  onBoard.setBrightness(50);
  onBoard.show(); // Initialize all pixels to 'off'

  updateImage = true;
}

void loop() {
  if (updateImage) {
    myFile = SD.open(socialFileNames[currentFileIndex]);
    if (myFile) {
      bmpDraw(myFile.name(), 0, 0);
    }
    myFile.close();
    addContactInfo();
    updateImage = false;
  }

  // 4: left-most tooth, right-most while wearing
  if (qt_4.measure() > 700) {
    safelyMoveLeft();
    updateImage = true;
  }
  if (qt_3.measure() > 700) {
    toggleBacklight();
  }
  if (qt_2.measure() > 700) {
    neopixelState = !neopixelState;
  }
  if (qt_1.measure() > 700) {
    safelyMoveRight();
    updateImage = true;
  }

  delay(150);
  
  for(int32_t i=0; i< onBoard.numPixels(); i++) {
    if (neopixelState) {
      onBoard.setPixelColor(i, Wheel(((i * 256 / onBoard.numPixels()) + j*5) & 255));
    }
    else {
      onBoard.setPixelColor(i, onBoard.Color(0, 0, 0));
    }
    j++;
    onBoard.show();
  }
}

void toggleBacklight() {
  backlightState = !backlightState;
  digitalWrite(TFT_BACKLIGHT, backlightState);
}

void blankScreen() {
  tft.startWrite();
  tft.fillRect(0, 0, 128, 128, ST77XX_BLACK);
  tft.endWrite();
}

void addContactInfo() {
  const char* username = socialUserNames[currentFileIndex];
  int usernameWidth = strlen(username) * 6;
  int horizontalPosition = (128 - (usernameWidth)) / 2;

  tft.fillRect(horizontalPosition - 1, 120 - 1, usernameWidth + 1, 8 + 1, ST77XX_BLACK);
  tft.setCursor(horizontalPosition, 120);
  tft.setTextColor(ST77XX_GREEN);
  tft.println(username);
}


void safelyMoveRight() {
  if (currentFileIndex + 1 >= NumSocialFileNames) {
    currentFileIndex = 0;
  }
  else {
    currentFileIndex = currentFileIndex + 1;
  }
}

void safelyMoveLeft() {
  if (currentFileIndex - 1 < 0) {
    currentFileIndex = NumSocialFileNames - 1;
  }
  else {
    currentFileIndex = currentFileIndex -1;
  }
}

// This scrolls through images by itself
void browseImages() {
  File entry = root.openNextFile();
  if (entry) {
    if (!entry.isDirectory()) {
      bmpDraw(entry.name(), 0, 0);
      delay(5000);
    }
  }
  else
  {
    root.rewindDirectory();
  }
  entry.close();
}

// 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 8

void bmpDraw(char *filename, uint8_t x, uint16_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(F("Loading image '"));
  Serial.print(filename);
  Serial.println('\'');

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

  // 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(read16(bmpFile) == 1) { // # planes -- must be '1'
      bmpDepth = read16(bmpFile); // bits per pixel
      if((bmpDepth == 24) && (read32(bmpFile) == 0)) { // 0 = uncompressed

        goodBmp = true; // Supported BMP format -- proceed!

        // 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;
        // wrap around
        if((x+w-1) >= tft.width())  w = tft.width()  - x;
        if((y+h-1) >= tft.height()) h = tft.height() - y;

        tft.startWrite();
        // Set TFT address window to clipped image bounds
        tft.setAddrWindow(x, y, w, h);
        tft.endWrite();

        for (row=0; row<h; row++) { // For each scanline...
          // Serial.print(F("row: ")); Serial.println(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
          }

          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.startWrite();
            tft.writePixel(col, row, tft.color565(r,g,b));
            tft.endWrite();
          } // end pixel
        } // end scanline
      } // end goodBmp
    }
  }

  bmpFile.close();
  if(!goodBmp) Serial.println(F("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;
}

void printDirectory(File dir, int numTabs) {
  while (true) {
    File entry =  dir.openNextFile();
    if (! entry) {
      // no more files
      break;
    }
    for (uint8_t i = 0; i < numTabs; i++) {
      Serial.print('\t');
    }
    Serial.print(entry.name());
    if (entry.isDirectory()) {
      Serial.println("/");
      printDirectory(entry, numTabs + 1);
    } else {
      // files have sizes, directories do not
      Serial.print("\t\t");
      Serial.println(entry.size(), DEC);
    }
    entry.close();
  }
}

// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
  if(WheelPos < 85) {
    return onBoard.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
  } else if(WheelPos < 170) {
    WheelPos -= 85;
    return onBoard.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  } else {
    WheelPos -= 170;
    return onBoard.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
}


Post a comment in response:

(will be screened)
(will be screened if not validated)
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

If you are unable to use this captcha for any reason, please contact us by email at support@dreamwidth.org