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;
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.
to;
needs to be replaced with;
For the buttons a library is required;
Then you need to create each of the four buttons (A2, A3, A4, A5);
Each of which you
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.
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 chebe
Front of the badge, displaying an image with overlay, and NeoPixel backlit
Photo by 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);
}
}