So, what should this device even do… Given set of components used, I was thinking it could:

  • display characters from the alphanumeric keypad on the 7-segment display
  • turn on LEDs in the 8×8 matrix when keys are pressed on the 4×4 keypad (no particular pattern, can be random)
  • display some patterns using the 4-color LED bar
  • use the four tactile buttons to switch modes of operation for joystick and encoder:
    • control the LED bar
    • control the RGB LED’s color
    • control LEDs on the 8×8 matrix

That’s for Arduino A. Since Arduino B is connected only to the LCD TFT display, it would run some sort of a sketchbook/paint application.

  • There are very few custom hardware components here, so most of the software would be handled by Arduino libraries. Libraries used:
  • Encoder — for rotary encoder
  • Adafruit MCP23017 — for GPIO expanders
  • TM1637 for 7-segment and LED matrix modules
  • Keypad — for all kinds of keypads (library is versatile enough to even correctly handle four tactile buttons used here)
  • Adafruit PCF8591 — for ADC module
  • MCUFRIEND_kbv — for controlling LCD TFT display, including touch

One caveat here is that TM1637 library assumes it is working with a common cathode/anode 7-segment digit display, so when applied to a LED matrix, all diodes across matrix’s diagonal are lit. This should be easily fixable via software update and use of a proper library for a LED matrix.

I did add some custom wrappers for some of these libraries so let’s go through the wrappers, first.

RGB LED wrapper

This wrapper is used to provide high level interface for controlling the RGB LED, including conversion of cartesian, XY coordinates, to red-green-blue values. Library uses structure and helper methods for rgb color definition from ESPHome project.

class RgbLed
        /* data */
        uint8_t pin_red;
        uint8_t pin_green;
        uint8_t pin_blue;
        Color color;
        void setRGB(void);
        uint32_t hsv2rgb(uint16_t hue, float saturation, float value);
        RgbLed(uint8_t pin_red, uint8_t pin_green, uint8_t pin_blue);
        void setColor(rgbled::Color color);
        void lighten(uint8_t amount);
        void darken(uint8_t amount);
        void changeBrightness(int delta);
        Color getColor(void);
        uint32_t rectToRGB(float x, float y);

setColor method is a primary one, used to set a specific color. Colors are defined as a structure of three float values, in range 0..1. lighten(), darken(), changeBrightness() are used to perform basic operations on the color being currently set.

Last method rectToRGB(x,y) — is a bit more complicated. It is meant to convert (x,y) coordinates — e.g. coming from the joystick — to RGB values. Idea is to be able to change RGB LED color by turning a fully tilted joystick around — from red, through green, to blue, and back.

Let’s get on with some trigonometry then. rectToRGB(x,y) method is based on an atan2(y,x) function which calculates an angle — θ — between OX axis and given coordinates. Values of angle returned by atan2 function are in range −π

Using atan2 allows for converting (x,y) coordinates to an angle, but what about radius? Radius calculation is pretty straightforward, but for sake of simplicity I’ll be just sticking to angles. Code snippet below shows application of Arduino’s atan2 function. Calculation of the angle is done only when joystick is tilted in either direction (coordinates from joystick are floats in -1..1 range; more on this later).

if (abs(x) > 0.1 or abs(y) > 0.1)
{ theta = atan2(y, x) + PI; } float ro = sqrt(x * x + y * y);

Easiest approach to converting angle to (r,g,b) value would be to devise a set of piecewise linear functions, in which each of the color coordinates is a function of angle. This would, however, produce color changes that are too sharp. Piecewise sin() function would create a smoother transition. Sinus function is great for describing cyclical phenomena; since goal here is to convert a cyclical movement of joystick to color coordinates, it’s a perfect match.

To make the transition of colors smooth, each of the color coordinates have to overlap a little — meaning that as joystick moves around, values of one of the color coordinates have to fade to 0, while other slowly raises to 1. To achieve this, each of the color coordinates is calculated with different phase shift applied to the sin() function.

Here’s the lambda function responsible for converting angle to a color coordinate:

auto cval = [](float theta, float ro, float phase, float neg_phase) {
float val = sin(0.666 * theta - phase); if (val

To calculate each of the color coordinates, this lambda is called with different phase shift, and calculated coordinates are used to create the color:

float r = cval(theta, ro, -PI / 2, PI);
float g = cval(theta, ro, 0, 3 * PI / 2); float b = cval(theta, ro, PI / 2, 5 * PI / 2); Color c = Color(r, g, b);

Display wrapper

Display wrapper class maintains buffers which convert raw characters obtained from keypads to raw bit values needed by display's libraries to actually show the character.

const uint8_t digitToSegment[16] = {
//XGFEDCBA 0b00111111, // 0 0b00110000, // 1 0b01011011, // 2 ...

Each character ('0', '1', 'A', etc.) is mapped to sequence of bits which, when passed to actual display library, will light segments needed to show the character. Internal buffer has size that matches number of digits (or rows, in case of LED matrix) in the display.

Keypad wrapper

This is a very simple subclass of parent Keypad class from respective library. It overrides pin_mode(), pin_read(), and pin_write() methods from Keypad class (and the fact that these methods are declared as virtual in Keypad class is beyond cool). This is object-oriented programming model at it's best - original pin_mode/pin_read/pin_write methods were nothing more than wrappers around native Arduino functions.

In this project, however, all keypads are connected via GPIO expanders, which precludes from using native functions. Overloading pin_mode(), pin_read() and pin_write() methods in subclass allows for re-defining their behavior to use GPIO expanders rather than native Arduino functions, without having to modify any of the behavior in the super (parent) class.

PanelKeypad(Adafruit_MCP23017 *gpio, char *userKeymap, byte *row, byte *col, byte numRows, byte numCols)
: Keypad(userKeymap, row, col, numRows, numCols) { PanelKeypad::gpio = gpio; } void pin_mode(byte pinNum, byte mode); void pin_write(byte pinNum, boolean level) { if (PanelKeypad::gpio != NULL) { PanelKeypad::gpio->digitalWrite(pinNum, level); } } int pin_read(byte pinNum) { if (PanelKeypad::gpio != NULL) { return PanelKeypad::gpio->digitalRead(pinNum); } else { return -1; } }

LED bar wrapper

This class wraps operation on Arduino pins connected to individual LEDs in the bar into a few handy methods, allowing for:

  • turning individual color on/off
  • animating leds to create a kind of a Knight Rider effect
void move_bar(int direction)
{ // make sure that direction is either 1 or -1 if (direction != 0) { direction = direction / abs(direction); LedBar::leds[led_ptr] = !LedBar::leds[led_ptr]; LedBar::color(led_ptr, LedBar::leds[led_ptr]); if (direction > 0) { ++led_ptr %= numColors; } else if (direction

Depending on a direction (positive/negative integer passed as parameter) next set of LEDs in line get's toggled on or off.

Joystick wrapper

This class wraps all logic operations on joystick while also hiding the fact that joystick is connected to an external analog-digital converter.

    class Joystick
{ public: Joystick(Adafruit_PCF8591 *adc, uint8_t pinx, uint8_t piny); Joystick(uint8_t pinx, uint8_t piny); float getX(bool update_coords = true); float getY(bool update_coords = true); bool isTilted(float threshold = TILT_THRESHOLD, bool update_coords = true); int getDirection(float axis_coords, bool update_coords = true); protected: Coordinates coords = {0}; void read(); private: uint8_t pinx = 0; uint8_t piny = 0; Adafruit_PCF8591 *adc = NULL; float read_pin(uint8_t pin); };

Joystick module is basically a two-axis potentiometer. X and Y pins of the joystick module output a voltage scaled from GND (0) to VCC (+5V, in this case), where 0 and +5V correspond to max tilt in either axis. Because of that, a joystick in its idle position outputs 0.5 * VCC on both X and Y pins.

Joystick::read() method performs read of voltage from both X and Y pins, and converts the result to a float in -1..1 range:

auto convert = [](int value, uint16_t size) {
float x = (((float)value / size) - 0.5) * 2; return x; };

It also handles communication with external ADC module via respective library.

Once tilt values from joystick are known, additional processing can be done. Joystick::isTilted() method returns a boolean true value if joystick is tilted above defined threshold (threshold can also be passed as parameter) in either direction. Joystick::getDirection() will output an integer depending on direction of the tilt.

Arduino sketch

The Arduino sketch itself is the usual initialization of required objects, with their configuration done in the setup() function. Loop continuously reads all the inputs (keypads, joystick, encoder), updates global variable which holds encoder position, calculates its movement direction, and calls handlers for screens, LEDs, etc.

Joystick and encoder behave differently, depending on selected mode of operation (mode selected via tactile buttons next to the LCD screen):

if (current_mode == RGB_LED_MODE)
{ #ifdef RGB_LED_CONNECTED rgb_led_handler(joy_sw, delta); #endif led_bar.move_bar(1); } else if (current_mode == LED_BAR_MODE) { led_bar.move_bar(delta); } else if (current_mode == LED_MATRIX_MODE) { led_matrix_handler(); } else if (current_mode == LCD_MODE) { }

In RGB_LED_MODE joystick's output is used to rotate through colors of RGB LED. LED Bar mode uses encoder to change state of LEDs in the LED bar. LED Matrix mode uses joystick to light up individual diodes in the 8x8 matrix. LCD mode is not functional, for now, as LCD is handled by the other Arduino.

The other Arduino

I did try to use a single Arduino Uno to handle all inputs/lights and the LCD TFT shield, but that turned out to be somewhat difficult, mostly due to limited amount of available pins. LCD TFT shield requires both 5-wire SPI interface (touch) and 8 wire parallel data interface (display). All those pins would have to be shared with other input devices and create interference. While random pixels being shown when an encoder's knob is turned might be an interesting effect, that was not one I was going for. So, LCD shield is handled by a second Arduino Uno.

The other Arduino runs a tftpaint application from MCUFRIEND_kbv library, unmodified. It's a basic paint app which allows for drawing points using few predefined colors.

The Code

Latest code for this project can be found on github:

Leave a Reply