Saturday, July 6, 2013

Event-driven LCD Keypad handler

The LCD Keypad Shield is equipped with a 16X2 character HD44780 LCD controller and six buttons. To reduce the number of pins required to handle the buttons they are connected in a resistor net to an analog pin. The different buttons, when pressed, give different analog readings.

Fig.1: LCD Keypad Shield with demo sketch CosaKeypad
The programming challenge is to reduce the complexity of handling the buttons so that application programmers only needs to deal with the logic of the buttons. And at the same time avoiding busy-wait or continuously reading the analog pin in the loop.

The Cosa approach is to use an object-oriented, event-driven, method where all the logic of reading the analog pin, mapping to key and checking for change is hidden. The resulting application support is a simple definition of two virtual method, on_key_down() and on_key_up(), which are called when a key is pushed and released. Below is a snippet from the Cosa example sketch, CosaKeypad.ino.

#include "Cosa/Keypad.hh"
#include "Cosa/IOstream.hh"
...
class KeypadTrace : public LCDKeypad {
private:
  IOStream m_out;
public:
  KeypadTrace(IOStream::Device* dev) : LCDKeypad(), m_out(dev) {}

  void trace(const char* msg, uint8_t nr);
  virtual void on_key_down(uint8_t nr) { trace(PSTR("down"), nr); }

  virtual void on_key_up(uint8_t nr) { trace(PSTR("up"), nr); }
};

The KeypadTrace implements the on_key_down/up() methods and will be called when a change is detected. The setup() and loop() needs to execute the event handler. Below is a snippet from these functions.

void setup()
{
   Watchdog::begin(16, SLEEP_MODE_IDLE, Watchdog::push_timeout_events);
   ...
}

void loop()
{
  Event event;
  Event::queue.await(&event);
  event.dispatch();
}


The Watchdog is setup to wakeup every 16 ms from sleep mode and check if there are any timeouts. The Cosa Keypad class (inherited by LCDKeypad) acts as a periodic function that issues analog sample requests. The processor will go back to sleep while waiting for the Analog-Digital Converter (ADC) to complete (See previous blog post).

When a conversion is completed the ADC interrupt handler will push an event and the next step is performed, again dispatched by the event loop(). The value from the analog pin is mapped to a key index and if a change is detected the on_key() method is called. The processor will sleep while waiting for the Watchdog or Analog-Digital Conversion Competed interrupt.

The Cosa Keypad class may be used to implement other keypads based on resistor nets. The configuration is simply the analog thresholds for the resistor net. Below is the definition of the LCDKeypad. It defines the mapping (m_map) and the key codes.

class LCDKeypad : public Keypad {
private:
  // Analog reading to key index map
  static const uint16_t m_map[] PROGMEM;

public:
  // Key index
  enum {
    NO_KEY = 0,
    SELECT_KEY,
    LEFT_KEY,
    DOWN_KEY,
    UP_KEY,
    RIGHT_KEY
  } __attribute__((packed));
  LCDKeypad() : Keypad(Board::A0, m_map) {}
};

...
const uint16_t LCDKeypad::m_map[] PROGMEM = {
  1000, 700, 400, 300, 100, 0
};
 

More details on the example sketch, CosaKeypad. The KeypadTrace class will print the key index and name to the LCD. As the LCD is an IOStream::Device it may be used for IOStream output. Note that the parameter to the KeypadTrace constructor could be any other IOStream::Device such as the UART. Below is a snippet that binds and initiates the LCD.
 
#include "Cosa/IOStream.hh"
#include "Cosa/LCD/Driver/HD44780.hh"
...
HD44780::Port port;
HD44780 lcd(&port);
KeypadTrace keypad(&lcd);

...
void setup()
{
   ...
   lcd.begin();
   lcd.puts_P(PSTR("CosaKeypad: started"));
}


Last, the implementation of trace method where the key name, action and index are written to the IOStream/LCD. The key codes are defined in the class LCDKeypad.

void
KeypadTrace::trace(const char* msg, uint8_t nr)
{
  m_out << clear;
  switch (nr) {
  case NO_KEY:
    m_out << PSTR("NO_KEY");
    break;
  case SELECT_KEY:
    m_out << PSTR("SELECT_KEY");
    break;
  case LEFT_KEY:
    m_out << PSTR("LEFT_KEY");
    break;
  case DOWN_KEY:
    m_out << PSTR("DOWN_KEY");
    break;
  case UP_KEY:
    m_out << PSTR("UP_KEY");
    break;
  case RIGHT_KEY:
    m_out << PSTR("RIGHT_KEY");
    break;
  }
  
  m_out << ' ' << msg << endl;
  m_out << PSTR("key = ") << nr;

}


The keypad instance will connect to the Watchdog timeout queue and a callback will occur every 64 milli-seconds, which will give an effective debouncing of the button.

Fig.2: Pushing a button and executing the on_key callback.
For more details see the on-line documentation. Below is the full example sketch:

#include "Cosa/Types.h"
#include "Cosa/Keypad.hh"
#include "Cosa/IOStream.hh"
#include "Cosa/LCD/Driver/HD44780.hh"

class KeypadTrace : public LCDKeypad {
private:
  IOStream m_out;
public:
  KeypadTrace(IOStream::Device* dev) : LCDKeypad(), m_out(dev) {}
  virtual void on_key_down(uint8_t nr) { trace(PSTR("down"), nr); }
  virtual void on_key_up(uint8_t nr) { trace(PSTR("up"), nr); }
  void trace(const char* msg, uint8_t nr);
};

void
KeypadTrace::trace(const char* msg, uint8_t nr)
{
  m_out << clear;
  switch (nr) {
  case NO_KEY:
    m_out << PSTR("NO_KEY");
    break;
  case SELECT_KEY:
    m_out << PSTR("SELECT_KEY");
    break;
  case LEFT_KEY:
    m_out << PSTR("LEFT_KEY");
    break;
  case DOWN_KEY:
    m_out << PSTR("DOWN_KEY");
    break;
  case UP_KEY:
    m_out << PSTR("UP_KEY");
    break;
  case RIGHT_KEY:
    m_out << PSTR("RIGHT_KEY");
    break;
  }
  m_out << ' ' << msg << endl;
  m_out << PSTR("key = ") << nr;
}

HD44780::Port port;
HD44780 lcd(&port);
KeypadTrace keypad(&lcd);

void setup()
{
   Watchdog::begin(16, SLEEP_MODE_IDLE, Watchdog::push_timeout_events);
   lcd.begin();
   lcd.puts_P(PSTR("CosaKeypad: started"));
}

void loop()
{
  Event event;
  Event::queue.await(&event);
  event.dispatch();
}

5 comments:

  1. I tried using the example on my newly arrived LCD Shield and the LCD worked as intended but the key mappings were screwed up. Turns out that my board has a different resistor map than the one assumed in Keypad.cpp. Have you considered making the mapping changeable (apart from modifying Keypad.cpp) to accomodate boards with slightly different mappings?

    ReplyDelete
  2. The design of Keypad is extendable with any mapping. That is the nature of C++ classes. The LCDKeymap https://github.com/mikaelpatel/Cosa/blob/master/cores/cosa/Cosa/Keypad.hh#L123 is more or less just an example. You can create your own Keypad sub-class. Or are you thinking in the line of passing the mapping as a parameter to the class. This could also be done but then the key mapping is also needed and encapsulation goes out the window. Do you have the schematics/voltage levels for your required mapping? What LCD keypad are you using?

    ReplyDelete
    Replies
    1. My comment was not aimed at Keypad in general, just the LCDKeypad class which is of course just an example. But there seems to be several different LCD Shields with keypads that all look similar but differ on the electrical level (at least in the resistor mappings). So to make it easier for newbies like me one could, as you suggest, for instance provide the mapping explicitly as a parameter (with a well chosen default value).

      My shield is from DF Robots (http://www.dfrobot.com/index.php?route=product/product&product_id=51#.Ur9RnGRDv7c). I didn't order it from there, but seems to match by description. Mine matches the levels for the 1.0 version of this board. Have a look at the example code they have at their web page for details.

      The reason I wrote the comment was that I would prefer not be forced to "tweak" the stock Cosa code for my own projects. I would prefer if I could stick to the provided classes and just build my own on top.

      Delete
    2. I understand the problem and it would be nice to have support for as much as possible *<:-) Anyway to help you out I have pushed an update to the example sketch so that you can check the values from your keypad. See https://github.com/mikaelpatel/Cosa/commit/67b65c61c72db5f451a6d1af21ffc678c41cb621. Some cheaper shields have problem with the buttons. If you can run the sketch I might be able to tweak the mapping vector towards a common threshold.

      God Fortsättning!

      Delete
    3. Thanks for the update, I get the following readings:

      NONE = 1023
      Select = 639
      Left = 409
      Down = 256
      Up = 100
      Right = 0

      Nice change, but I would have loved to have a nice hw debugger at hand. That would make understanding and debugging code a whole lot easier. Do you have any suggestions for affordable debuggers (that one can get in Sweden preferably)?

      God fortsättning till dig med!

      Delete