Basic display modules may consume most of the MCU's interface pins leaving no room for sensors or actuators. This is because they operate in parallel mode. Let's adapt them to work in serial mode instead and save some interface pins.
LCD screens are widely used in embedded systems built around MCUs. You can find character display modules based on LCD and 7-segment technologies with a variety of line and character configurations. You can also find graphic LCD modules with touch screens and different pixel densities. The main factor that matters for an MCU, the driver of your chosen display, is how you are going to wire them together. The displays come with two modes of operation that dictate how to interface them with an MCU: parallel, and serial.
In parallel mode, you will need 4 or 8 interface pins for the data to be displayed, and a few pins for controlling the display operation, counting for around 13 pins out of the MCU. So, using a parallel display with a development board such as the Arduino UNO would consume 8 out of its 14 digital GPIOs and 4 out of 6 of its analog IO pins, leaving not enough room for any other breakout board to plug in. In technical terms, using parallel display modules may exclude the use of I2C and SPI communication of the MCU as well as driving any digital and analog peripherals, e.g. sensors and actuators. For some popular MCU such as the ESP8266 and ESP32, using parallel displays is out of the question as their exposed digital and analog IO pins are very limited to barely cope with their designated application.
Converting a parallel display to work in serial mode, using I2C or SPI communication, is readily available in the market, but unfortunately limited to character-based displays. The I2C Interface PCF8574, MCP23012, MCP230084, and the HC595 are examples of such serialization based modules.
These images illustrate the wiring of the PCF8574 module to 16 interface pins of a HD44780 16x2 dot matrix LCD screen, resulting in just 4 I2C pins to be connected to the MCU. MCP23S084 and MCP23S172 on the other hand provide serial SPI interfacing to parallel 8 and 16-bit devices.
The broad overview
In this article, I am presenting a way out for parallel graphic displays by converting them to serial mode of operation using a dedicated secondary MCU linked to the main one of your project. The proposed display MCU would drive the parallel interface of the display but will keep room for communicating with the main MCU via I2C, SPI, or UART facility. I chose Arduino Pro Mini5 for its abundance of digital and analog interfaces and cheap price to do the job. It has 14 digital GPIO and 8 analog IO pins in contrast to the core MCU board which happens to be the popular WeMos D1 Mini6 that has a limited number of usable interface pins. The board is based on ESP8266 MCU and has only 11 digital GPIO and one analog input interface pin.
The two board images shown here illustrate the multi-function pin assignments of both MCU boards. In the rest of the article, I shall refer to the Arduino Pro Mini as just Arduino, and the WeMos D1 Mini as WeMos to cut short the wording.
Note: The Arduino board operates at 5.0V logic level whilst the WeMos works at 3.3V. This requires the use of a digital logic level converter to interface the I2C signal between them and to avoid browning-up the WeMos board.
The DISPLAY
I used the TFT 2.4" touch display module from MCUFRIEND7,8 for its readiness to plug into Arduino UNO as a shield board.
The front and rear views of this screen show the shield showing its numerous data and control pins.
The module requires 8 digital input data lines and 5 analog input control lines for the LCD screen operation.
The module combines an SD Card slot for auxiliary storage to the Arduino, and adopts SPI interface for accessing the slot. So, additional 4 pins are required out of the Arduino to work out SPI interface and access images stored in inset SD Card.
In total, we shall need all 12 digital GPIO pins of the Arduino in addition to 5 out of its 7 analog pins. A problem arises here since the default analog control pins declared in the graphics libraries include A4 which has a double role and is required for I2C communication.
Luckily this pin handles the reset pin of the LCD module and it can be hardwired to logic one using a pull-up resistor to the Vcc, thus enabling the LCD screen operation.
As you can see on the Fritzing diagram elsewhere in this project, the display module is wired to 8 digital GPIO and 4 analog pins of the Arduino, leaving the SPI, I2C and UART serial communication pins intact and available for our use. I chose serial UART communication to link the Arduino to the WeMos for its simple 2-wire connection and programming.
Now, we move to explain the coding parts of both MCUs. I implemented the key LCD screen functions which counted to 18 as depicted in the following table to demonstrate the concept of communicating parsed commands between the MCUs. The reader may follow suit and extend the implemented display functions as appropriate.
Parsing the TFT Display Command
Here is a tricky part in which I set up a display function header file of my own that would intentionally alter the implementation of the LCD's regular display functions to just construct the relevant parsed command to be passed on to the Arduino.
Upon receiving a command, the Arduino would apply the relevant display function implementation using the standard library. So, the reader needs to follow suit when setting up the WeMos sketch and use the included header and code files in this project, rather than the standard ones, in order to make the display serialization as transparent as possible.
The parsed command is constructed from 7 parts:
- The command code representing a display function as an integer ranging from 0 to 20;
- 5 possible integer fields for the coordinates within the LCD panel and/or colour codes;
- one string attribute for user text or bitmap file name.
The parsed command exchanged between MCUs will have the following format with blanks in place of irrelevant fields to any display function:
CMD_CODE, INT#1, INT#2, INT#3, INT#4, INT#5, STRING
Table 1 summarises the implemented regular/standard MCUFRIEND display functions alongside their parsed relevant interpretation for the exchanged commands between the MCUs. I listed the corresponding standard display function the user may use in his sketch as depicted in MCUFRIEND's LCD library headers UTFTGLUE.h (Table 3) and colour representations in MCUFRIENF_kbv.h file (Table 4).
Note that there are two versions of colour setting commands and I implemented both in this project.
In Table 2, I present two bonus bitmap functions of my own: slideshow() for displaying a stream of bitmap images loaded in the root directory of an SD Card, and bmpDraw() for displaying a specific bitmap image from that stream. The latter function is different from the standard drawBitmap() function which displays a bitmap from the internal Arduino's EEPROM. The reader should note that all bitmap images should be sized to fit the 320x240 pixel TFT display. I included some interesting images from Egyptian culture for testing those functions.
The following data shows the serial monitor output of the WeMos while in action sending a stream of parsed display functions to the Arduino.
Parsed Command from WeMos & Arduino's OK:
0, OK received!
4,255,127,127, OK received!
19,1000 OK received!
0, OK received!
5,128,0,128, OK received!
6,0,128,0, OK received!
8,1,15,318,224, OK received!
Parsed Display Function:
clrScr
fillScr
slideshow
clrScr
setColor
setBackColor
fillRect
The WeMos activity is governed by the Arduino's OK confirmation thus avoiding commands overrun at the Arduino.
SPHINX.BMP
Loading image 'SPHINX.BMP'
File size: 220854
Image Offset: 54
Header size: 40
Bit Depth: 24
Image size: 320x230
Loaded in 2707 ms
The above code shows the Arduino in action displaying a bitmap image from the SD Card, where its serial monitor prints image details while being viewed on the TFT display.
The Build:
Parts Required: | Jaycar | |
---|---|---|
1 x Arduino Pro Mini | - | |
1 x WeMos D1 Mini Standard (built-in antenna) | XC3802 | |
1 x Logic Level Converter | - | |
1 x 2.4" 320x240 Touchscreen LCD Shield | - | |
1 x 220Ω Resistor* | RR0556 | |
1 x LM1117-3.3 Voltage Regulator | Element14: 3122029 | |
1 x 12V DC Power Adaptor | MP3011 |
The Arduino Code
Our Arduino has tiny program and dynamic memory (SRAM) storage areas. It is built around the ATmega328P MCU which has 30KB of flash memory for storing code, and 2KB of SRAM for manipulating variables.
Such limitation on storage dictated configuring the Arduino to work in either one of two modes:
- 1. Displaying bitmaps pre-stored in program memory with disabled SD Card access, or
- 2. Displaying bitmaps pre-stored in SD Card without any bitmaps in program memory. Preparing bitmaps that will be fitted in mode#1 can be accomplished using
Img2code utility9.
Such modes helped me in restricting the storage space required for the included and loaded libraries.
I switch between modes by using the C++ compiler directives #define / #ifndef / #ifdef which forces the compiler to include or exclude specific chunks of the code as appropriate to the enabled mode. If the SDCARD keyword is defined, the Arduino is set to the second mode; otherwise, it works in the first mode. The user needs to add or remove the #define SDCARD statement as appropriate to enforce his selected mode, then reboots the Arduino.
In both modes, I was conscious about optimising my sketches by limiting global variable usage and debug serial monitor messaging. I used the F() macro to inform the compiler to keep fixed printouts to the serial port in program memory, as depicted in the following sample of Snippet 1.
Serial.print(F("***OK***"));
// confirm command execution to host MCU
Then when it is time to access it, one byte of the data is copied to RAM at a time.
There’s a small performance overhead for this extra work. However, printing strings over Serial or to an LCD is really a slow process by nature, so a few extra clock cycles really won’t matter.
As for the tiny bitmaps, I used the PROGMEM directive as shown in the example of Snippet 2 (below) to inform the compiler to keep them in program memory as well during sketch run, thus saving SRAM space.
Conditional compiler directives were also used to control the modes of operation throughout the sketch code to eliminate irrelevant code segments to the selected mode of operation.
const unsigned char car [] PROGMEM ={ 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xf, 0xff, 0xff, 0xff . . . . . .
The following is an example of the first mode in which the program memory is almost fully utilised with only five small mono-colour bitmaps, while the SRAM is half-full to accommodate any additional bitmaps to be loaded interactively.
Sketch uses 27918 bytes (90%) of program storage space. Maximum is 30720 bytes.
Global variables use 515 bytes (25%) of dynamic memory, leaving 1533 bytes for local variables. Maximum is 2048 bytes.
The following depicts memory allocations when using the
SD Card storage, and demonstrates the effect of adding more variables and code to the sketch where the SRAM utilisation doubles.
It also shows that program memory becomes 20% less occupied as no bitmaps are stored there even though more libraries are added to the sketch for manipulating the SD Card.
Sketch uses 22880 bytes (74%) of program storage space. Maximum is 30720 bytes.
Global variables use 1169 bytes (57%) of dynamic memory, leaving 879 bytes for local variables. Maximum is 2048 bytes.
Note: I experienced a symptom in this project: when I2C is started as either a master or a slave, the SD Card access using SPI is disrupted. In this project, only BMP files in the root directory of the SD Card will be accounted for.
Note: There has been a great improvement in Arduino's IDE V2.1, which I used for flashing the sketches of this project, compared to V 1.18, since it produces more compact and optimised code. Without V2.1, the Arduino sketch would not have fitted the Pro Mini. I suppressed debugging messages for SD Slot, BMP displays and CMD parsing at will using conditional macros for Serial.print()/Serial.println()functions to even shrink the final product memory size allocation. Snippet 3 (below) shows an example of how to use Arduino's macros to control debugging messages in this project.
#ifdef DEBUG_CMD
#define CMD_PRINT(x) Serial.print(x)
#define CMD_PRINTLN(x) Serial.println(x)
#define CMD_PRINTDEC(x,DEC) Serial.println(x,DEC)
#else
#define CMD_PRINT(x)
#define CMD_PRINTLN(x)
#define CMD_PRINTDEC(x,DEC)
#endif
The Arduino sketch has been prepared with best coding practices in mind as well. Rather than having a single code file encompassing 970 statements, I split the sketch into 4 code files, each has a specific role, as shown in the table shown here.
Sketch Files |
Description |
Stmt Cnt |
Arduino-Serial-Driver |
Main sketch code file, selects mode |
130 |
bmp-handler |
Declares tiny bitmaps, handles image display & slideshow |
650 |
cmd-handler |
Parses received commands from the WeMos back into standard LCD function calls |
150 |
sd-handler |
SD Card utilities, enumerating detected bitmaps |
40 |
This segmentation of the code enables the reader to easily follow up the workflow of the project, and to be able to reuse specialised code segments in other projects.
The WeMos Code
The sketch of the WeMos is lightweight compared to that of the Arduino as depicted in Figure 8. This is meant to be in order to demonstrate the feasibility and practicality of this project. The sketch is also segmented, into 3 code files and an overloaded TFT display library of MCUFRIEND, the UTFTGLUE, as depicted in the following table.
Sketch Files |
Description |
Stmt Cnt |
TFT-Serial-Driver |
Main sketch code file |
80 |
get-cmd |
Declares tiny bitmaps, handles image display & slideshow |
40 |
cmd-demo |
Parses received commands from the WeMos back into standard LCD function calls |
150 |
UTFTGLUE.h |
UTFTGLUE display library overloaded header |
30 |
UTFTGLUE.cpp |
UTFTGLUE display library overloaded class implementation |
80 |
I have customised the UTFTGLUE library to just parse the standard display commands listed in the 2nd column of Table 1 and translate them to the WeMos-Arduino commands formatted out of columns: the 1st and the 3rd to the 8th. On the Arduino side, the standard UTFTGLUE library is used to execute the original display command implementation. The last column of that table presents examples of the parsed commands that the user can input himself from the serial monitor to test the Arduino operation.
Now, since my manipulation of the UTFTGLUE library marks the main contribution and novelty of this project, let me elaborate more on what has been done to the library.
Snippet 3 shows the modified GLUE class of the TFT display in which I included only a single template of some of the key standard display functions for POC purposes. The reader may extend these declarations to cover all function templates if he chooses to.
Snippet 4 shows the implementation of one of the display functions, that is screen inversion. The reader would see the Serial Printing function which sends the parsed command via the UART to the Arduino, following the format structure depicted in Table 1. Note that I designed the getOK() function used here to force the WeMos to wait for a confirmation from the Arduino after executing the parsed command, and not to attempt flooding it with new commands.
class GLUE {
public:
GLUE(bool exist);
static void clrScr();
static void invertDisplay(int inv);
static void vertScroll(int scrl);
static void setRotation(int rot);
static void fillScr(int r, int g, int b);
static void setColor(int r, int g, int b);
static void setBackColor(int r, int g, int b);
static void drawRect(int x1, int y1, int x2, int y2);
static void fillRect(int x1, int y1, int x2, int y2);
static void drawRoundRect(int x1, int y1, int x2, int y2);
static void fillRoundRect(int x1, int y1, int x2, int y2);
static void drawCircle(int x, int y, int rad);
static void fillCircle(int x, int y, int rad);
static void drawLine(int x1, int y1, int x2, int y2);
static void drawPixel(int x, int y);
static void setTextSize(int sz);
static void setCursor(int x, int y);
static void print(char * txt, int al, int x);
static void drawBitmap(int x1, int y1, int x2,int y2, int rgb, int seq);
static void slideshow(int showdelay);
static void bmpDraw();
private:
bool mcufriend_on;
};
Note: The reader should note the speed difference between the MCUs involved in this project which may lead to a racing condition that would eventually cause the Arduino to drop many parsed commands as a result. The getOK() avoids this situation.
void GLUE::invertDisplay(int inv) {
//Serial.print("1,inv, , , , , ");
Serial.print("1,inv, ");
getOK();
}
Conclusion
This article presents the implementation of overloading classes in C++ for the purpose of customising class methods implementations. I demonstrated how to parse standard display library into user-defined commands on one MCU and reverse such action on another MCU to execute the standard display function. The cheap and humble Arduino Pro Mini volunteered to drive a parallel graphic TFT display for the sake of saving WeMos Mini the burden of providing the sheer count of GPIOs to drive the display. Software development best practices have been employed to help the reader comprehend the tricks of command parsing and code optimisation and reusability.