Monday, August 25, 2014

Font issues with BeagleBone Black and ILI9341 TFT display

In my continuing quest to build a really cool digital speedometer for my car I have been experimenting with an Adafruit 2.2" color TFT display. This past weekend I loaded up Ubuntu 14.04 on my BeagleBone Black and wired up the TFT display to it. Adafruit has a python library that works on both the BeagleBone Black and a Raspberry Pi. After trying out the example code I decided I wanted to try using a nicer font than the default one in the example code. The first font I tried seemed to look fine but the second font I tried had the bottom third of the characters not displayed. To figure out which fonts were affected I wrote a python script that cycled through displaying a bunch of fonts on the screen. Here is a video of the results:


As you can see some fonts are affected more than others. A few have over half the line cut off. I started digging into the code that displays the text. I figured out the code is determining the height and width of the text and then turning the text into an image to be displayed on the screen. This is done so text can easily be rotated on the display.

Line number 17 in this snippet of code is where the height and width is determined before making the image.
import Image
import ImageDraw
import ImageFont
import Adafruit_ILI9341 as TFT
import Adafruit_GPIO as GPIO
import Adafruit_GPIO.SPI as SPI
font = ImageFont.truetype('Minecraftia.ttf', 16)
# Define a function to create rotated text. Unfortunately PIL doesn't have good
# native support for rotated fonts, but this function can be used to make a
# text image and rotate it so it's easy to paste in the buffer.
def draw_rotated_text(image, text, position, angle, font, fill=(255,255,255)):
# Get rendered font width and height.
draw = ImageDraw.Draw(image)
width, height = draw.textsize(text, font=font)
# Create a new image with transparent background to store the text.
textimage = Image.new('RGBA', (width, height), (0,0,0,0))
# Render the text.
textdraw = ImageDraw.Draw(textimage)
textdraw.text((0,0), text, font=font, fill=fill)
# Rotate the text image.
rotated = textimage.rotate(angle, expand=1)
# Paste the text into the image, using it as a mask for transparency.
image.paste(rotated, position, rotated)
# Write two lines of white text on the buffer, rotated 90 degrees counter clockwise.
draw_rotated_text(disp.buffer, 'Hello World!', (150, 120), 90, font, fill=(255,255,255))
draw_rotated_text(disp.buffer, 'This is a line of text.', (170, 90), 90, font, fill=(255,255,255))
# Write buffer to display hardware, must be called to make things visible on the
# display!
disp.display()
The Adafruit library is using PIL (Python Image Library) to create an image from the text. Ubuntu 14.04 actually uses a fork of PIL called Pillow. I did some google searches and discovered that the textsize function has a bug that does not account for the font offsets which causes the clipping on some fonts. The Ubuntu 14.04 I installed on my BBB came with Pillow 2.3.0 which was broken. I updated it to latest available package which was Pillow 2.5.3 and it was still broken. I looked at the bug fix on the master branch of Pillow and it was just a small change to one file, PIL/ImageFont.py, so I decided to apply that change to my 2.5.3 install of Pillow.

Here is how I fixed it.

cd /usr/local/lib/python2.7/dist-packages/PIL
sudo vi ImageFont.py

At about line 142 look for the getsize function. Here is what it looked like before the change.




And here it is after the change.


Save the file and then you need to compile it into python byte-code.
sudo pycompile ImageFont.py

This creates an ImageFont.pyc file. Now to test it again.



All fixed! I'm sure that fix to getsize will be pushed out soon so this won't be a problem in the future but until then this will let me continue my experimentation.



Thursday, August 7, 2014

Arduino - TFT LCD display refresh rate part 2

Since January I have been thinking about how to reduce the flashing effect of redrawing characters on the Adafruit 2.2" TFT display running off of an Arduino. Initially I thought I was doing something wrong but then I saw that other projects experience the same slow screen redraw performance. This post isn't about rewriting/optimizing the ILI9340 library that talks to the display. I'm not a good enough programmer to even think about digging into that code. Also I don't think it really is a problem with the library or the display itself. I've seen videos with this display hooked up to a Raspberry Pi smoothly playing video files. I suspect the limitation is the CPU and/or RAM of the Arduino. For my speedometer project I wanted to use an Arduino because it starts up almost instantly and I don't have to worry about shutting it down which is not the case with a Raspberry Pi.

So my goal was to optimize the refresh rate by reducing the amount of screen redraw to the smallest amount possible. To do this I first draw the static elements on the screen (the red box and the mph text). These are never redrawn. Redrawing static items causes a flashing effect. Then the speed value is only updated if it changes. When it does change only digits that have changed are redrawn. For example say the speed is 35 mph and then on the next loop the speed is 36 mph. The 5 would be drawn over with black and then the 6 is drawn in white. This performs the least amount of screen drawing possible.

Here is a video of the progress so far. The code is below the video.



Here is the code


#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9340.h"
#include <SD.h>
#if defined(__SAM3X8E__)
#undef __FlashStringHelper::F(string_literal)
#define F(string_literal) string_literal
#endif
// These are the pins used for the Mega
// for Due/Uno/Leonardo use the hardware SPI pins (which are different)
#define _sclk 52
#define _miso 50
#define _mosi 51
#define _cs 53
#define _rst 7
#define _dc 6
#define SD_CS 5
Adafruit_ILI9340 tft = Adafruit_ILI9340(_cs, _dc, _rst);
int potPin=0;
int count=0;
int prevCount=1;
int countdigits[] = {0, 0, 0};
int prevdigits[] = {0, 0, 0};
int digitpos[] = {30, 90, 150};
int x=0;
void setup() {
Serial.begin(9600);
//Init the SD Card
Serial.print("Initializing SD card...");
if (!SD.begin(SD_CS)) {
Serial.println("failed!");
return;
}
Serial.println("OK!");
//Start the TFT screen and paint it black
tft.begin();
tft.setRotation(1);
tft.fillScreen(ILI9340_BLACK);
//Draw TRD Logo before turing on backlight
bmpDraw("trdlogo.bmp", 40, 60);
//fade in back lighting
for(int b=0; b < 230; b++){
analogWrite(2, b);
delay(20);
}
//Sleep a while to display the logo
delay(2000);
tft.fillScreen(ILI9340_BLACK);
tft.drawFastHLine(0, 180, 320, ILI9340_RED);
//Draw static screen elements
bmpDraw("trdsml24.bmp", 110, 195);
tft.setTextColor(ILI9340_WHITE);
tft.setTextSize(4);
tft.setCursor(230, 90);
tft.print("mph");
}
void loop(void) {
//This simulates input of a VSS
//This will be replaced with pulse counting of the VSS sensor.
count = analogRead(potPin) / 8;
//Split each digit of the speed into an array
//This will allow printing of just numbers that have changed.
//The speed of a car can exceed a two digit number, certantly
//in kph and occasionally in mph. The splitting is done by
//taking the modulo, dividing or a combination of both.
//Grab the last digit of the speed
countdigits[2] = count % 10;
//How to handle the middle digit depends on if the
//the speed is a two or three digit number
if(count > 99){
countdigits[1] = (count / 10) % 10;
}else{
countdigits[1] = count / 10;
}
//Grab the first digit
countdigits[0] = count / 100;
//Split out the digits of the previous speed
prevdigits[2] = prevCount % 10;
if(prevCount > 99){
prevdigits[1] = (prevCount / 10) % 10;
}else{
prevdigits[1] = prevCount / 10;
}
prevdigits[0] = prevCount / 100;
//Now print the digits on the TFT screen.
//Only execute this block if the speed has changed.
if(count != prevCount){
tft.setTextSize(10);
//Compare each digit to the value from the previous loop.
//The digit will only be redrawn if it has changed.
for(x=0; x < 3; x++){
if(countdigits[x] != prevdigits[x]){
//black out old value first.
//Draw digit in black over the top of white digit
tft.setCursor(digitpos[x], 70);
tft.setTextColor(ILI9340_BLACK);
tft.print(prevdigits[x]);
//print new value in white
if((x == 0) and (count > 99) and (countdigits[x] > 0)){
tft.setCursor(digitpos[x], 70);
tft.setTextColor(ILI9340_WHITE);
tft.print(countdigits[x]);
}
if((x == 1) and (count >= 99)){
tft.setCursor(digitpos[x], 70);
tft.setTextColor(ILI9340_WHITE);
tft.print(countdigits[x]);
}else if((x == 1) and (count < 99) and (countdigits[x] > 0)){
tft.setCursor(digitpos[x], 70);
tft.setTextColor(ILI9340_WHITE);
tft.print(countdigits[x]);
}
if(x == 2){
tft.setCursor(digitpos[x], 70);
tft.setTextColor(ILI9340_WHITE);
tft.print(countdigits[x]);
}
}
}
}
delay(999); //Delay screen updates to simulate 1 second pulse counting.
prevCount = count; //Store current speed for comparison on the next loop.
}
//--------------------------------------------------------------------------------
//I didn't write anything below here. The code below is from the
//Adafruit library and it is used to load images from the SD card.
//--------------------------------------------------------------------------------
// 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 50
void bmpDraw(char *filename, uint8_t x, uint8_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("Loading image '");
Serial.print(filename);
Serial.println('\'');
// Open requested file on SD card
if ((bmpFile = SD.open(filename)) == NULL) {
Serial.print("File not found");
return;
}
// Parse BMP header
if(read16(bmpFile) == 0x4D42) { // BMP signature
Serial.print("File size: "); Serial.println(read32(bmpFile));
(void)read32(bmpFile); // Read & ignore creator bytes
bmpImageoffset = read32(bmpFile); // Start of image data
Serial.print("Image Offset: "); Serial.println(bmpImageoffset, DEC);
// Read DIB header
Serial.print("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
Serial.print("Bit Depth: "); Serial.println(bmpDepth);
if((bmpDepth == 24) && (read32(bmpFile) == 0)) { // 0 = uncompressed
goodBmp = true; // Supported BMP format -- proceed!
Serial.print("Image size: ");
Serial.print(bmpWidth);
Serial.print('x');
Serial.println(bmpHeight);
// 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;
if((x+w-1) >= tft.width()) w = tft.width() - x;
if((y+h-1) >= tft.height()) h = tft.height() - y;
// Set TFT address window to clipped image bounds
tft.setAddrWindow(x, y, x+w-1, y+h-1);
for (row=0; row<h; row++) { // For each scanline...
// 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.pushColor(tft.Color565(r,g,b));
} // end pixel
} // end scanline
Serial.print("Loaded in ");
Serial.print(millis() - startTime);
Serial.println(" ms");
} // end goodBmp
}
}
bmpFile.close();
if(!goodBmp) Serial.println("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;
}