LCD 16x2 PushWheel Effect - DevBhoomi Robots

Home Top Ad

Responsive Ads Here

Post Top Ad

Your Ad Spot

Tuesday, September 18, 2018

LCD 16x2 PushWheel Effect

Pushwheel

A question on the Arduino forum and the thread that followed prompted me to try and emulate a mechanical push- or thumb-wheel display update on an LCD module. The technique uses the LCD programmable characters and could be extended to other applications for simple LCD module animations.

 

 

Mechanical Thumbwheels

A mechanical thumbwheel is small thumb-operated wheel on a mechanical or electronic device (shown in the picture above). They were (and still are) a user interface for entering numbers into electronic equipment. The type shown has ‘+’ and ‘-‘ switches to increment or decrement each digit, usually using your thumb. Other types have a notched rotating wheel beside the number pushed around with a thumb or finger.
When a digit is changed on the device, the number on the display cylinder scrolls around to the next/previous digit. This ‘analog’ transition is what the LCD animation emulates. The effect is shown in the video below

LCD Module Displays

Click To Enlarge

 

Display modules based on the Hitachi HD44780 LCD controller are an alphanumeric dot matrix liquid crystal display that can display ASCII characters, non-ASCII characters (usually Japanese Kana characters), and some symbols from a built in character generator ROM for the standard Kana ROM version available from online suppliers is shown at right. All characters fit in an 8×5 grid. The devices are very inexpensive and in widespread use in the hobby community. The fact an Arduino library – LiquidCrystal – is part of the official software suite also helps to make them ubiquitous.
In addition to the ‘standard’ ROM character set, the modules allow the user to define up to 8 additional characters (ASCII 0 through 7) at run time in the module’s volatile memory. These are set up by transmitting a bitmap made up of 8 bytes (one per row). The lower 5 bits of each byte represent the bit pattern to be displayed across each row. The LiquidCrystal library makes this process straightforward by providing the createChar() method for this purpose.
These custom characters can be rewritten ay time during execution and will immediately update the display with the new bitmap. This property is often a disadvantage, but in this application it is exploited to our advantage.

Implementing the Animation

The full sketch implementing what is discussed below can be found at my code site.
The first thing to realise is that, much like the physical pushwheel, the animated version needs to operate on individual digits. So any number must be split into its constituent parts at some point.
Also, as we only have 8 custom characters we can, at most, have an 8 digit animated display. So, in the code we group all this information into a structure to track the information on a per-digit basis
typedef struct
{
 uint32_t timeLastFrame;     // time the last frame started animating
 uint8_t prev, curr;         // numeric value for the digit
 uint8_t index;              // animation progression index
 uint8_t charMap[CHAR_ROWS]; // generated custom char bitmap
} digitData_t;
The animation sequence will depend on the current number and the one that replaces it. Once we have established the start and end points for the animation (the full digit on display), we need to define the intermediate frames for each animation. Each frame will consist of part of the old character scrolling off the display and part of the new character appearing onto the display. When seen end to end, as in the video, the numbers appear to rotate on and off.
To build the intermediate stages, the code starts with the ROM bitmap definitions and appropriately combines them in the charMAP element of digitData_t. The index is to keep track of which frame is being displayed in the transition. As each character is 8 pixels high, and the smallest difference between frames is one pixel, we have 8 frames for each transition.
These animation sequences are easiest to implement as Finite State Machines. The displayValue() function implements the FSM for the animation.  Some thought breaks the task up into initialisation (ST_INIT), waiting for a change (ST_WAIT) and animation (ST_ANIM) phases.
case ST_INIT: // Initialise the display - done once only on first call
  for (uint8_t i = 0; i < MAX_DIGITS; i++)
  {
    // separate digits
     digits[i].prev = digits[i].curr = value % 10;
     value = value / 10;
  }
  // Display the starting number
  for (uint8_t i = 0; i < MAX_DIGITS; i++)
    memcpy(digits[i].charMap, digitsMap[digits[i].curr], ARRAY_SIZE(digits[i].charMap));
  updateDisplay(DISP_R, DISP_C, true);
  // Now we just wait for a change
  state = ST_WAIT;
break;
During initialisation, we split the number into separate digits, saving them to both the current and previous values, copy the bitmap for the currently displayed value into the animation character map and the update the display, making sure that it is initialised (see below). This once-only task is completed and we transition to the WAIT state.
case ST_WAIT: // not animating - save new value digits and check if we need to animate
  if (valueLast != value)
  {
    state = ST_ANIM; // a change has been found - we will be animating something
 
    for (int8_t i = 0; i < MAX_DIGITS; i++)
    {
      // separate digits
      digits[i].curr = value % 10;
      value = value / 10;
 
      // initialise animation parameters for this digit
      digits[i].index = 0;
      digits[i].timeLastFrame = 0;
    }
  }
  if (state == ST_WAIT) // no changes - keep waiting
    break;
  // else fall through as we need to animate from now
While waiting for a change, we check if the current value is different from the last value (a change!), in which case we split the new number into constituent digits, and set up the parameters for the animation to follow.
case ST_ANIM: // currently animating a change
  // work out the new intermediate bitmap for each character
  for (uint8_t i = 0; i < MAX_DIGITS; i++)
  {
    if ((digits[i].prev != digits[i].curr) && // values are different ...
        (millis() - digits[i].timeLastFrame >= ANIMATION_FRAME_TIME)) // ... and timer has expired
    {
      if (value > valueLast)
      {
        // scroll up
        // copy the bottom of the old digit from the index position and then the
        // top of new digit for the rest of the character
        for (int8_t p = 0; p < CHAR_ROWS; p++)
        {
          if (p < CHAR_ROWS - digits[i].index)
            digits[i].charMap[p] = digitsMap[digits[i].prev][p + digits[i].index];
          else
            digits[i].charMap[p] = digitsMap[digits[i].curr][p - CHAR_ROWS + digits[i].index];
        }
        bUpdate = true;
      }
      else
      {
         // scroll down
         // copy the bottom of new digit up to the index position and then from
         // the start of the old digit for the rest of the character
         for (uint8_t p = 0; p < CHAR_ROWS; p++)
         {
           if (p < digits[i].index)
             digits[i].charMap[p] = digitsMap[digits[i].curr][p + CHAR_ROWS - digits[i].index];
           else
             digits[i].charMap[p] = digitsMap[digits[i].prev][p - digits[i].index];
         }
         bUpdate = true;
       }

       // set new parameters for next animation and check if we are done
       digits[i].index++;
       digits[i].timeLastFrame = millis();
       if (digits[i].index > CHAR_ROWS)
       digits[i].prev = digits[i].curr; // done animating
     }
  } 

  if (bUpdate) updateDisplay(DISP_R, DISP_C);

  // are we done animating?
  {
    boolean allDone = true;

    for (uint8_t i = 0; allDone && (i < MAX_DIGITS); i++)
      allDone = allDone && (digits[i].prev == digits[i].curr);

    if (allDone)
    {
      valueLast = value;
      state = ST_WAIT;
    }
  }
  break;
The animation state is the most complex, but it basically needs to make some simple decisions for each digit on the display:
  1. Is it time to animate (ie, has enough time passed and are the curr and prev values different)?
  2. What does the intermediate frame need to look like?
  3. Is the animation completed?
The only interesting part is the second question. How we combine the two character maps depends on whether the number (value) is increasing or decreasing. In one case we would scroll up and in the other we scroll down. The specific direction is arbitrary, but they should be opposite to provide the right visual feedback.
The code then creates the appropriate animation frame from a combination of the two digits to be displayed and the animation index.
Finally, the display needs to be updated. The first time we do this we need to actually write the ASCII value 0 to 7 on the display. Subsequently, we can just update the createChar() definition for each character as an update. This work is done in the updateDisplay() function.
void updateDisplay(uint8_t r, uint8_t c, bool init = false)
// do the necessary to display current number scrolling anchored
// on the LHS at LCD (r, c) coordinates.
{
 // for each digit position, create the lcd custom character, and
 // if told to, display the custom characters left to right.
 for (uint8_t i = 0; i < MAX_DIGITS; i++)
 {
  lcd.createChar(i, digits[MAX_DIGITS - i - 1].charMap);
  if (init)
  {
   lcd.setCursor(c + i, r);
   lcd.write((uint8_t)i);
  }
 }
}


Post Top Ad

Your Ad Spot