18
In a recent project I needed to show a progress bar on a 2 line LCD module. Some time ago I had created a horizontal bar graph (described here) but this version needed to be ‘prettier’. Luckily, using more than one LCD custom character was also not an issue.
The progress bar is enclosed by a border and grows from the left towards the right side of the allocated display space. The implementation uses 5 user defined LCD characters, with three of these redefined each time the graph is drawn (more below).
Software Implementation
The code for the bar graph is implemented as a class and uses the LiquidCrystal (or functional equivalent) library to manage the LCD display. The entire Arduino sketch is attached at the end of this article.
The progress bar is defined by a range, a starting point and a length in characters. The class has 3 public methods:
- begin() to define the custom LCD characters.
- setRange() to set the full range value for the bar graph. Default is 0 to 100.
- show() to display the value specified as a progress bar.
The graph is built up as a string of characters and written to the display all at once. Each character in the string is one of the 5 user defined LCD characters:
- Start character. This is always at the start of the display string. Every time a value is displayed, the LCD character map is redefined to include the right number of progress bars.
- Full block character. After the start character there can be zero or more full block characters. This is a predefined constant character where the block is fully filled-in.
- Transition character. After the full block characters there may be transition character lies between the full blocks and the rest of the progress bar – either empty blocks or the end character. The transition block is redefined on the fly to include the correct number of transition columns.
- Empty block character. This is the opposite of the full block character, a predefined constant character where the progress bar is empty.
- End character. This is always at the end of the display string. The end character is the mirror of the start character. Like the start character, the end character is redefined each time to include the right number of progress.
The sketch below uses an analog potentiometer to test drive display. It displays the progress bar and the value it represents, producing the displays shown above.
// Draw a horizontal progress bar on a LCD module
// (LCD Keypad Shield)
//
// Varying input provided by through the analog input ANALOG_IN,
// connected to a pot for testing purposes
#define DEBUG 0
#ifdef DEBUG
#define PRINTS(s) do { Serial.print(F(s)); } while (false)
#define PRINT(s,v) do { Serial.print(F(s)); Serial.print(v); } while (false)
#define PRINTX(s,v) do { Serial.print(F(s)); Serial.print(F("0x")); Serial.print(v, HEX); } while (false)
#else
#define PRINTS(s)
#define PRINT(s,v)
#define PRINTX(s,v)
#endif
#include <LiquidCrystal.h>
// Analog input
const uint8_t ANALOG_IN = A5;
// LCD display definitions
const uint8_t LCD_ROWS = 2;
const uint8_t LCD_COLS = 16;
const uint8_t LCD_RS = 8;
const uint8_t LCD_ENA = 9;
const uint8_t LCD_D4 = 4;
const uint8_t LCD_D5 = LCD_D4+1;
const uint8_t LCD_D6 = LCD_D4+2;
const uint8_t LCD_D7 = LCD_D4+3;
static LiquidCrystal lcd(LCD_RS, LCD_ENA, LCD_D4, LCD_D5, LCD_D6, LCD_D7);
class BarGraph
{
public:
BarGraph(LiquidCrystal *lcd, uint8_t row, uint8_t colStart, uint8_t colLen) :
_lcd(lcd), _row(row), _colStart(colStart), _colLen(colLen)
{
setRange(0, 100);
};
void setRange(uint32_t valueMin, uint32_t valueMax) { _valueMin = valueMin; _valueMax = valueMax; };
void begin(void)
{
uint8_t c[ROW_PER_CHAR];
loadChar(c, BG_START, ROW_PER_CHAR);
lcd.createChar(LCD_START_CHAR, c);
loadChar(c, BG_MID_FULL, ROW_PER_CHAR);
lcd.createChar(LCD_MID_FULL_CHAR, c);
loadChar(c, BG_MID_PART, ROW_PER_CHAR);
lcd.createChar(LCD_MID_PART_CHAR, c);
loadChar(c, BG_MID_EMPTY, ROW_PER_CHAR);
lcd.createChar(LCD_MID_EMPTY_CHAR, c);
loadChar(c, BG_END, ROW_PER_CHAR);
lcd.createChar(LCD_END_CHAR, c);
}
bool show(uint32_t value)
// Build a string with the graph characters and then display the string.
// The graph is always displayed growing from L to R.
// Return true if the graph was updated.
{
char *szGraph = (char *) malloc((_colLen+1) * sizeof(char));
const uint16_t lenChart = (_colLen * COL_PER_CHAR) - (2 * CHAR_END_UNUSED); // total of the graph in pixel columns
int16_t barCount; // displayed number of pixels
// Can't do much if we couldn't get RAM
if (szGraph == NULL)
{
PRINTS("\nNo RAM allocated");
return(false);
}
// work out what value means in the display
if (value > _valueMax) barCount = lenChart;
else if (value < _valueMin) barCount = 0;
else barCount = map(value, _valueMin, _valueMax, 0, lenChart);
PRINT("\n----------\nProcessing value = ", value);
// create the bar graph string
{
uint8_t idx = 0; // index of string character being processed
uint8_t c[ROW_PER_CHAR];
uint8_t mask;
// ** Handle the first character
// This only contains up to COL_PER_CHAR - CHAR_END_UNUSED vertical bars
// Code redefines the start character to have right number of
// columns filled in.
PRINT("\n- First Char barCount = ", barCount);
szGraph[idx++] = LCD_START_CHAR;
loadChar(c, BG_START, ROW_PER_CHAR);
mask = 0;
for (uint8_t count = 0; count < COL_PER_CHAR - CHAR_END_UNUSED; count++)
{
if (barCount > 0)
{
mask |= (1 << (COL_PER_CHAR - CHAR_END_UNUSED - 1 - count));
barCount--;
}
}
PRINTX(" mask = ", mask);
for (uint8_t row = MASK_START_ROW; row <= MASK_END_ROW; row++)
c[row] |= mask;
lcd.createChar(LCD_START_CHAR, c); // new definition includes right number of columns
// ** Handle the full blocks in the chart
// While we have more that the number of columns per char left to display,
// this block will be a full block
PRINT("\n- Full Char barCount = ", barCount);
while (barCount >= COL_PER_CHAR)
{
PRINT(" ", idx);
szGraph[idx++] = LCD_MID_FULL_CHAR;
barCount -= COL_PER_CHAR;
}
// ** Handle the transition from full to empty
// This can contain up to COL_PER_CHAR vertical bars
// Code redefines the LCD_MID_PART character to have right number of
// columns filled in.
if (idx != _colLen - 1) // not the last column (handled separately)
{
PRINT("\n- Transition at ", idx);
PRINT(" barCount = ", barCount);
szGraph[idx++] = LCD_MID_PART_CHAR;
loadChar(c, BG_MID_PART, ROW_PER_CHAR);
mask = 0;
for (uint8_t count = 0; count < COL_PER_CHAR; count++)
{
if (barCount > 0)
{
mask |= (1 << (COL_PER_CHAR - count));
barCount--;
}
}
PRINTX(" Mask = ", mask);
for (uint8_t row = MASK_START_ROW; row <= MASK_END_ROW; row++)
c[row] |= mask;
lcd.createChar(LCD_MID_PART_CHAR, c); // new definition includes right number of columns
}
// ** Handle the empty blocks to the end
PRINT("\n- Empty Char barCount = ", barCount);
PRINT(" idx = ", idx);
while (idx < _colLen - 1)
{
PRINT(" ", idx);
szGraph[idx++] = LCD_MID_EMPTY_CHAR;
}
// ** Handle the last block
PRINT("\n- Last Char barCount = ", barCount);
szGraph[_colLen - 1] = LCD_END_CHAR;
loadChar(c, BG_END, ROW_PER_CHAR);
mask = 0;
for (uint8_t count = 0; count < COL_PER_CHAR - CHAR_END_UNUSED; count++)
{
if (barCount > 0)
{
mask |= (1 << (COL_PER_CHAR - 1 - count));
barCount--;
}
}
PRINTX(" Mask = ", mask);
for (uint8_t row = MASK_START_ROW; row <= MASK_END_ROW; row++)
c[row] |= mask;
lcd.createChar(LCD_END_CHAR, c); // new definition includes right number of columns
}
// now display the chart string and release the string memory
_lcd->setCursor(_colStart, _row);
for (uint8_t i = 0; i < _colLen; i++)
_lcd->write(szGraph[i]);
free(szGraph);
return(true);
}
protected:
LiquidCrystal *_lcd;
uint8_t _row, _colStart; // LCD position for start
uint8_t _colLen; // in characters
uint32_t _valueMin, _valueMax; // range for the bar chart
// Define LCD character constants
// The LCD characters are defined in OPRIGMEM outside of the class.
static const uint8_t COL_PER_CHAR = 5;
static const uint8_t ROW_PER_CHAR = 8;
static const uint8_t CHAR_END_UNUSED = 2; // number of columns unused at ends (static component)
static const uint8_t MASK_START_ROW = 2;
static const uint8_t MASK_END_ROW = 5;
static const uint8_t LCD_START_CHAR = 1;
static const uint8_t BG_START[ROW_PER_CHAR];
static const uint8_t LCD_MID_FULL_CHAR = 2;
static const uint8_t BG_MID_FULL[ROW_PER_CHAR];
static const uint8_t LCD_MID_PART_CHAR = 3;
static const uint8_t BG_MID_PART[ROW_PER_CHAR];
static const uint8_t LCD_MID_EMPTY_CHAR = 4;
static const uint8_t BG_MID_EMPTY[ROW_PER_CHAR];
static const uint8_t LCD_END_CHAR = 5;
static const uint8_t BG_END[ROW_PER_CHAR];
// methods
inline void loadChar(uint8_t *c, const uint8_t *src, uint8_t size)
{
memcpy_P(c, src, size * sizeof(uint8_t));
}
};
// LCD user defined character data
const uint8_t PROGMEM BarGraph::BG_START[ROW_PER_CHAR] = { 0x0f, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x0f };
const uint8_t PROGMEM BarGraph::BG_MID_FULL[ROW_PER_CHAR] = { 0x1f, 0x00, 0x1f, 0x1f, 0x1f, 0x1f, 0x00, 0x1f };
const uint8_t PROGMEM BarGraph::BG_MID_PART[ROW_PER_CHAR] = { 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f };
const uint8_t PROGMEM BarGraph::BG_MID_EMPTY[ROW_PER_CHAR] = { 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f };
const uint8_t PROGMEM BarGraph::BG_END[ROW_PER_CHAR] = { 0x1e, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x1e };
BarGraph bg(&lcd, 1, 0, LCD_COLS);
void setup()
{
#if DEBUG
Serial.begin(57600);
PRINTS("\n[BarGraph2]");
#endif
// initialize hardware pins
pinMode(ANALOG_IN, INPUT);
// initialize LCD display
lcd.begin(LCD_COLS, LCD_ROWS);
lcd.clear();
lcd.noAutoscroll();
lcd.noCursor();
// initialize BarGraph parameters
bg.begin();
bg.setRange(0, 102);
}
void loop()
{
static uint16_t lastValue = 1024;
uint16_t v = 0;
if ((v = analogRead(ANALOG_IN)/10) != lastValue)
{
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(v);
bg.show(v);
lastValue = v;
}
}