Posts | Comments

Planet Arduino

Archive for the ‘lcd display’ Category

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;
  }
}

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;
  }
}

If you’re like us, you probably spend more time browsing Reddit than you’d like to admit to your friends/family/boss/therapist. A seemingly endless supply of knowledge, wisdom, and memes; getting stuck on Reddit is not unlike looking something up on Wikipedia and somehow managing to spend the next couple hours just clicking through to new pages. But we’re willing to bet that none of us love browsing Reddit quite as much as [Saad] does.

He writes in to tell us about the handheld device he constructed which lets him view random posts from the popular /r/showerthoughts sub. Each press of the big red button delivers another slice of indispensable Internet wisdom, making it a perfect desk toy to fiddle with when you need a little extra push to get you through the day. Like one of those “Word a Day” calendars, but one that you’ll actually read.

For those curious as to how [Saad] is scraping Reddit with an Arduino, the short answer is that he isn’t. Posts are pulled from Reddit using an online tool created for the project by his wife (/r/relationshipgoals/), and dumped into a text file that can be placed on the device’s SD card. With 1500 of the all-time highest rated posts from /r/showerthoughts onboard, he should be good on content for awhile.

[Saad] has done an excellent job documenting the hardware side of this build, providing plenty of pictures as well as a list of the parts he used and a few tips to help make assembly easier. Overall it’s not that complex a project, but his documentation is a big help for those who might not live and breathe this kind of thing.

For the high-level summary: it uses an Arduino Pro Mini, a ILI9341 screen, and a 3.3 V regulator to step down 5 V USB instead of using batteries. A bit of perfboard, a 3D printed case, and a suitably irresistible big red button pulls the whole thing together.

We’ve seen a similar concept done in a picture frame a couple of years back, but if that’s not interactive enough you could always build yourself a Reddit “controller”.

There are cheap LCDs available from China, and when plugged into an Arduino, these displays serve as useful interfaces or even shinier baubles for your latest project. [Michael] picked up a few of these displays in the hope of putting a few animated .GIFs on them. This is an impossible task with an ATMega microcontroller – the Arduino does not have the RAM or the processing power to play full-screen animations. It is possible to display 3D vector graphics, with an updated graphics library [Michael] wrote.

The display in question uses the ILI9341 LCD driver, found in the Adafruit library, and an optimized 3D graphics driver. Both of these drivers have noticeable flicker when the animation updates, caused by the delay between erasing a previous frame and when a new frame is drawn.

With 16-bit color and a resolution of 320×240 pixels, there simply isn’t enough memory or the processing power on an ATMega microcontroller to render anything in the time it takes to display a single frame. There isn’t enough memory to render off-screen, either. To solve this problem, [Michael] built his render library to only render pixels that are different from the previous frame.

Rendering in 3D presents its own problems, with convex surfaces that can overlap themselves. To fix this, [Michael]’s library renders objects from front to back – if the pixel doesn’t change, it doesn’t need to be rendered. This automatically handles occlusions.

In a demo application, [Michael]’s LCD and Arduino can display the Stanford bunny, a low-poly 3D face, and geometric object. It’s not a video game yet, but [Michael] thinks he can port the classic game Spectre to this platform and have it run at a decent frame rate.

Video of the demo below.


Filed under: Arduino Hacks


  • Newsletter

    Sign up for the PlanetArduino Newsletter, which delivers the most popular articles via e-mail to your inbox every week. Just fill in the information below and submit.

  • Like Us on Facebook