Drawing on a LCD
Table of Contents
Introduction
This project is about drawing (somewhat) freely on a 16x2 LCD. This is not a native feature of the LCD screen since the LCD is usually meant to display text. As such you can tell it to lcd.print("Hello World") but lcd.setPixel(1, atX, atY) is not built in. The only way to do something similar is to use the eight custom characters that we can set:
byte i = 0 // 0 to 8
byte apple[8] = {
0b00000,
0b00000,
0b11010,
0b00100,
0b01110,
0b11111,
0b11111,
0b01010,
};
// register the custom character in slot i
lcd.createChar(i, apple);
// set the cursor to the correct position
lcd.setCursor(col, row);
// print the custom character previously
// stored in slot i to the position of the character
lcd.write(i);Unless we attempt to display too much stuff thats spread too far apart we can use these eight chars to draw whatever we want.
If you are thinking about drawing on the whole display by using the eight custom chars to flicker the whole screen it does not look good. Essentially, the idea with flickering would be that for a 16x2 screen we can draw everything in four times eight characters. If we use four frames per image and refresh really fast it should look like the whole screen is drawn to, the problem with this is the slow refresh rate of the LCD.
Connecting the LCD to the arduino is done as detailed in the docs
Idea
Assume you have a bitmap (i.e. an array of width-bit integers of length height) we need a function, that for a given offset copies the relevant parts of the bitmap to the 8 custom characters and then displays them at the right positions.
We can see the 16x2 screen here separated into its 5x8 segments with a 7x6 bitmap at a given position. The steps are as follows:
- go over each segment of the display
- find the overlap the bitmap has with that display
- register a custom character containing the overlapping part
I chose to implement the gaps between the segments as if there are pixels there that are just always off.
The overlap is calculated by treating the pixels as a coordinate system, where the top left is the (0, 0) pixel, then we can calculate each segments top left pixel as (5 * col + col, 8 * row + row) or (5 * col, 8 * row) depending on if we want the gaps to be actual space or not. Then we need to write the overlap in the buffer with fitInto, which also returns true if we have written anything. The function gets all relevant input like the bitmap position offsetX, offsetY and the position of the current segment. i is the custom character counter, which we will later limit such that we can control how many custom characters each bitmap controls for now it just uses every custom character:
byte buffer[8][8] = {0};
int i = 0;
int i_max = 8
for (int col = 0; col < _segmentCols && i < i_max; col++) {
for (int row = 0; row < _segmentRows && i < i_max; row++) {
bool isOverlaps = fitInto(
offsetX,
offsetY,
col * _segmentWidth + col,
row * _segmentHeight + row,
buffer[i])
if (!isOverlaps)
continue;
lcd.createChar(i, buffer[i]);
lcd.setCursor(col, row);
lcd.write(i);
i++;
}
}The fitInto function is the actually interesting part. First, we go to coordinates relative to the segment with oX and oY. This moves the coordinate system to the top left of the segment.
bool fitInto(
int offsetX, int offsetY,
int customCharX, int customCharY,
byte* customChar) {
bool hasDrawn = false;
int oX = offsetX - customCharX;
int oY = offsetY - customCharY;Next, we need to calculate what part of the bitmap will be visible. The bitmap is fully visible if oX and oY are both positive, otherwise the part of the bitmap after -oX or -oY is surely visible. This means rectTop and rectLeft are in bitmap coordinates.
For the width and height, we have the same cases either we have the full bitmap or the bitmap goes over the segments border, in that case we take less of the bitmap.
Finally, we check if this rectangle is a valid part of the bitmap.
int rectTop = max(0, -oY);
int rectHeight = min(_height, _segmentHeight - oY);
int rectBottom = rectTop + rectHeight;
int rectLeft = max(0, -oX);
int rectWidth = min(_width, _segmentWidth - oX);
if (rectLeft >= _width
|| rectTop >= _height
|| rectHeight < 0
|| rectWidth < 0)
return false;I think this code is unnecessary since customChar is an element of buffer and the array is initialized as all zero, however, I encountered some problems that looked like non zero values in customChar that where fixed by adding this loop.
for (int i = 0; i < _segmentHeight; i++) {
customChar[i] = 0;
}Lastly, we actually crop out the bitmap and then copy it into the customChar, for this we shift the content to the left so that everything thats not visible in the current segment on the right of the bitmap is deleted (croppedRight), then we apply a mask such that everything on the left of the bitmap thats not visible is deleted as well (croppedLeft). This is superior to just applying a mask that deletes everything but the visible part, since now we just need to shift the bitmap to the left and we do not need to check if we need to make additional shifts to the right since the content is shifted all the way to the right already. Now, we just need to shift it to the left again into position (positioned). _mask is equal to (1 << _segmentWidth) - 1, I do not think this mask is necessary, its just an additional safeguard.
If value is not zero we have visible content, so we set hasDraw to true so that we know we have used one of the available custom characters. i is in bitmap coordinates so we add the offset oY to transform it to the segment coordinates and then write the value. I use bitwise or to write so technically we could store the buffer statically and then have overlapping bitmaps. As the code is written now this is not possible but extending it that way would not be difficult.
for (int i = rectTop; i < min(rectBottom + 1, _height); i++) {
int croppedRight = _content[i] >> _width - rectWidth;
int croppedLeft = croppedRight & ((1 << (rectWidth - rectLeft)) - 1);
int positioned = croppedLeft << _segmentWidth - (rectWidth - rectLeft) - (oX + rectLeft);
int value = positioned & _mask;
if (value != 0 && i + oY >= 0 && i + oY < _segmentHeight) {
customChar[i + oY] |= value;
hasDrawn = true;
}
}
return hasDrawn;
}Implementation of LcdBitmap
LcdBitmap.h
#pragma once
#include <LiquidCrystal.h>
#include <Arduino.h>
class LcdBitmap {
private:
int _customCharStorageSlotOffset = 0;
int _customCharStorageSize = 8;
int _width, _height;
int* _content;
const int _segmentWidth = 5, _segmentHeight = 8, _segmentRows = 2, _segmentCols = 16;
const int _mask = (1 << _segmentWidth) - 1;
bool fitInto(int offsetX, int offsetY, int customCharX, int customCharY, byte* customChar);
public:
LcdBitmap(int width, int height, int* content, int customCharStorageSlotOffset = 0, int customCharStorageSize = 8);
void show(LiquidCrystal& lcd, int offsetX, int offsetY);
};
LcdBitmap.cpp
#include "LcdBitmap.h"
LcdBitmap::LcdBitmap(int width, int height, int* content, int customCharStorageSlotOffset, int customCharStorageSize)
: _width(width), _height(height), _content(content), _customCharStorageSlotOffset(customCharStorageSlotOffset), _customCharStorageSize(customCharStorageSize) {}
bool fitInto(
int offsetX, int offsetY,
int customCharX, int customCharY,
byte* customChar) {
bool hasDrawn = false;
int oX = offsetX - customCharX;
int oY = offsetY - customCharY;
int rectTop = max(0, -oY);
int rectHeight = min(_height, _segmentHeight - oY);
int rectBottom = rectTop + rectHeight;
int rectLeft = max(0, -oX);
int rectWidth = min(_width, _segmentWidth - oX);
if (rectLeft >= _width
|| rectTop >= _height
|| rectHeight < 0
|| rectWidth < 0)
return false;
for (int i = 0; i < _segmentHeight; i++) {
customChar[i] = 0;
}
for (int i = rectTop; i < min(rectBottom + 1, _height); i++) {
int croppedRight = _content[i] >> _width - rectWidth;
int croppedLeft = croppedRight & ((1 << (rectWidth - rectLeft)) - 1);
int positioned = croppedLeft << _segmentWidth - (rectWidth - rectLeft) - (oX + rectLeft);
int value = positioned & _mask;
if (value != 0 && i + oY >= 0 && i + oY < _segmentHeight) {
customChar[i + oY] |= value;
hasDrawn = true;
}
}
return hasDrawn;
}
void LcdBitmap::show(LiquidCrystal& lcd, int offsetX, int offsetY) {
byte buffer[8][8] = {0};
int i = _customCharStorageSlotOffset;
for (int col = 0; col < _segmentCols && i < _customCharStorageSlotOffset + _customCharStorageSize; col++) {
for (int row = 0; row < _segmentRows && i < _customCharStorageSlotOffset + _customCharStorageSize; row++) {
if (fitInto(offsetX, offsetY, col * _segmentWidth, row * _segmentHeight, buffer[i])) {
lcd.createChar(i, buffer[i]);
lcd.setCursor(col, row);
lcd.write(i);
i++;
}
}
}
}Example Program
I decided to make a simple program of a cat, that moves towards an apple whenever it is hungry, for this I added an animation class LcdAnimation so that the cat moves its legs when it moves and idles when it waits to get hungry again:
cat.ino
I also added a physical dial to control the fps, as this is based on the code found in the arduino docs for the lcd display.
#include <LiquidCrystal.h>
#include "Cat.h"
const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
auto lcd = LiquidCrystal(rs, en, d4, d5, d6, d7);
auto cat = Cat();
void setup() {
lcd.begin(16, 2);
}
int foodX = -1;
void loop() {
lcd.clear();
if (foodX < 0 || !cat.stepTo(lcd, foodX)) {
foodX = random(10, 15 * 5);
cat.eat();
}
int sensorReading = analogRead(A0);
int delayTime = 1000 / map(sensorReading, 0, 1023, 1, 60);
delay(delayTime);
}
LcdAnimation.h
#pragma once
#include "LcdBitmap.h"
#include <Arduino.h>
class LcdAnimation {
private:
LcdBitmap* _frames;
size_t _frameCount;
size_t _currentFrame = 0;
public:
LcdAnimation(LcdBitmap* frames, size_t frameCount);
void proceed(LiquidCrystal& lcd, int offsetX, int offsetY);
void show(LiquidCrystal& lcd, int offsetX, int offsetY);
void setFrame(int currentFrame);
};LcdAnimation.cpp
#include "LcdAnimation.h"
LcdAnimation::LcdAnimation(LcdBitmap* frames, size_t frameCount) : _frames(frames), _frameCount(frameCount) {}
void LcdAnimation::proceed(LiquidCrystal& lcd, int offsetX, int offsetY) {
_frames[_currentFrame].show(lcd, offsetX, offsetY);
_currentFrame = (_currentFrame + 1) % _frameCount;
}
void LcdAnimation::show(LiquidCrystal& lcd, int offsetX, int offsetY) {
_frames[_currentFrame].show(lcd, offsetX, offsetY);
}
void LcdAnimation::setFrame(int currentFrame) {
_currentFrame = (currentFrame + 1) % _frameCount;
}Cat.h
#pragma once
#include "LcdAnimation.h"
#include <Arduino.h>
class Cat {
private:
int hunger = 6;
int posX = 0, posY = 8;
public:
Cat();
bool stepTo(LiquidCrystal& lcd, int x);
void eat();
private:
static const int apple[8];
static const LcdBitmap appleBitmap;
static const int heart0[8];
static const int heart1[8];
static const int heart2[8];
static const int heart3[8];
static const int heart4[8];
static const int heart5[8];
static const LcdBitmap heartFrames[6];
static const LcdAnimation heartAnimation;
static const int catDefault[8];
static const int catIdle0[8];
static const int catIdle1[8];
static const int catIdle2[8];
static const int catIdle3[8];
static const int catWalkRight0[8];
static const int catWalkRight1[8];
static const int catWalkLeft0[8];
static const int catWalkLeft1[8];
static const LcdBitmap catIdleFrames[4];
static const LcdBitmap catWalkRightFrames[2];
static const LcdBitmap catWalkLeftFrames[2];
static const LcdAnimation catWalkRightAnimation;
static const LcdAnimation catWalkLeftAnimation;
static const LcdAnimation catIdleAnimation;
};Cat.cpp
#include "Cat.h"
Cat::Cat() {}
bool Cat::stepTo(LiquidCrystal& lcd, int x) {
if ((random(100) < 5)) {
hunger -= (hunger > 1);
}
heartAnimation.setFrame(min(5 - hunger, 5));
heartAnimation.show(lcd, 0, 0);
appleBitmap.show(lcd, x, 0);
if (hunger >= 3 || posX == x) {
catIdleAnimation.proceed(lcd, posX, posY);
return posX != x;
}
if (posX < x) {
posX += 1;
catWalkRightAnimation.proceed(lcd, posX, posY);
} else {
posX -= 1;
catWalkLeftAnimation.proceed(lcd, posX, posY);
}
return true;
}
void Cat::eat() {
hunger = 6;
}
const int Cat::apple[8] = {
0b00000,
0b00000,
0b11010,
0b00100,
0b01110,
0b11111,
0b11111,
0b01010,
};
const LcdBitmap Cat::appleBitmap = LcdBitmap(5, 8, const_cast<int*>(Cat::apple), 5, 2);
const int Cat::heart0[8] = {
0b01010,
0b11111,
0b01110,
0b00100,
0b00000,
0b00100,
0b00100,
0b00100,
};
const int Cat::heart1[8] = {
0b01010,
0b11111,
0b01110,
0b00100,
0b00000,
0b10100,
0b10100,
0b10100,
};
const int Cat::heart2[8] = {
0b01010,
0b11111,
0b01110,
0b00100,
0b00000,
0b10101,
0b10101,
0b10101,
};
const int Cat::heart3[8] = {
0b01010,
0b11111,
0b01110,
0b00100,
0b00000,
0b10101,
0b10101,
0b10010,
};
const int Cat::heart4[8] = {
0b01010,
0b11111,
0b01110,
0b00100,
0b00000,
0b01010,
0b01010,
0b00100,
};
const int Cat::heart5[8] = {
0b01010,
0b11111,
0b01110,
0b00100,
0b00000,
0b10101,
0b10101,
0b01001,
};
const LcdBitmap Cat::heartFrames[6] = {
LcdBitmap(5, 8, const_cast<int*>(Cat::heart5), 4, 1),
LcdBitmap(5, 8, const_cast<int*>(Cat::heart4), 4, 1),
LcdBitmap(5, 8, const_cast<int*>(Cat::heart3), 4, 1),
LcdBitmap(5, 8, const_cast<int*>(Cat::heart2), 4, 1),
LcdBitmap(5, 8, const_cast<int*>(Cat::heart1), 4, 1),
LcdBitmap(5, 8, const_cast<int*>(Cat::heart0), 4, 1),
};
const LcdAnimation Cat::heartAnimation = LcdAnimation(Cat::heartFrames, 6);
const int Cat::catDefault[8] = {
0b01000000010,
0b01100000110,
0b01111111110,
0b01101110110,
0b11111011111,
0b00011111000,
0b10011111000,
0b01101010000,
};
const int Cat::catIdle0[8] = {
0b01000000010,
0b01100000110,
0b01111111110,
0b01101110110,
0b11111011111,
0b00011111000,
0b00011111000,
0b11101010000,
};
const int Cat::catIdle1[8] = {
0b01000000010,
0b01100000110,
0b01111111110,
0b01101110110,
0b11111011111,
0b00011111000,
0b10011111000,
0b01101010000,
};
const int Cat::catIdle2[8] = {
0b01000000100,
0b01100000110,
0b01111111110,
0b01011101110,
0b11110111111,
0b00011111000,
0b10011111000,
0b01101010000,
};
const int Cat::catIdle3[8] = {
0b00100000010,
0b01100000110,
0b01111111110,
0b01110111010,
0b11111101111,
0b00011111000,
0b10011111000,
0b01101010000,
};
const int Cat::catWalkRight0[8] = {
0b00100000010,
0b01100000110,
0b01111111110,
0b01110111010,
0b11111101111,
0b00011111000,
0b00011111000,
0b11101010000,
};
const int Cat::catWalkRight1[8] = {
0b00100000010,
0b01100000110,
0b01111111110,
0b01110111010,
0b11111101111,
0b00011111100,
0b00011111100,
0b11101000100,
};
const int Cat::catWalkLeft0[8] = {
0b01000000100,
0b01100000110,
0b01111111110,
0b01011101110,
0b11110111111,
0b00011111000,
0b00011111000,
0b00001010111,
};
const int Cat::catWalkLeft1[8] = {
0b01000000100,
0b01100000110,
0b01111111110,
0b01011101110,
0b11110111111,
0b00011111000,
0b00011111000,
0b00010010111,
};
const LcdBitmap Cat::catIdleFrames[4] = {
LcdBitmap(11, 8, const_cast<int*>(Cat::catIdle0), 0, 4),
LcdBitmap(11, 8, const_cast<int*>(Cat::catIdle1), 0, 4),
LcdBitmap(11, 8, const_cast<int*>(Cat::catIdle2), 0, 4),
LcdBitmap(11, 8, const_cast<int*>(Cat::catIdle3), 0, 4)
};
const LcdBitmap Cat::catWalkRightFrames[2] = {
LcdBitmap(11, 8, const_cast<int*>(Cat::catWalkRight0), 0, 4),
LcdBitmap(11, 8, const_cast<int*>(Cat::catWalkRight1), 0, 4)
};
const LcdBitmap Cat::catWalkLeftFrames[2] = {
LcdBitmap(11, 8, const_cast<int*>(Cat::catWalkLeft0), 0, 4),
LcdBitmap(11, 8, const_cast<int*>(Cat::catWalkLeft1), 0, 4)
};
const LcdAnimation Cat::catIdleAnimation = LcdAnimation(Cat::catIdleFrames, 4);
const LcdAnimation Cat::catWalkRightAnimation = LcdAnimation(Cat::catWalkRightFrames, 2);
const LcdAnimation Cat::catWalkLeftAnimation = LcdAnimation(Cat::catWalkLeftFrames, 2);