Micro GUI on Arduino (Mega / Uno)
by Gilissen Erno in Circuits > Arduino
413 Views, 2 Favorites, 0 Comments
Micro GUI on Arduino (Mega / Uno)
The idea was to create a micro GUI that was still able to run on Arduino Uno (for very small applications, since this controller has very limit resources: FLASH (32Kbyte) and RAM (2Kbyte) and less than 20 IO pins.
Targets:
- Use as few resources as possible (including memory, CPU time).
- Easy to define screen objects (by type, position, width, height, text).
- Touch-able objects:
- Slider bars (horizontal & vertical) with uni- or bipolar % reading.
- Sliding keys with uni- or bipolar % reading.
- Buttons with text where possible automatic located in the button..
- Checkbox.
- Relative fast response (taken above limits into account).
The video gives a first impression how it works on a UNO. On a standard MEGA it's about 50% slower, because the Mega code splits the used pins over multiple IO ports, requiring more software interaction to write a single pixel to the display. This GUI is in no way compare-able to most (semi-) commercial solutions like LvGL or TouchGFX on STM32. The main purpose is to foresee a basic easy-to-configure GUI that has minimal requirements and uses low power. In the example it even runs in your loop function, so you have to make sure it's run frequently enough to still react reasonable to user actions.
Downloads
Supplies
Required:
- Arduino UNO or Mega. This source code targets AVR processors.
- Preferably a Parallel (8bit) connected display that has positive coordinates (X, Y = 0, 0 on one edge and ex. 479, 319 at the other edge).
- On the UNO a parallel display severely brings down remaining I/Opins for your application. Using SPI or I2C reduces the Display IO pin requirements but will noticeably slow down the user experience. However, on the UNO you only have a few IO pins left when connecting the display on a parallel bus (ex. D0 and D1 are required for Serial functions / not used by the display hardware).
- Arduino IDE functional on your computer.
- The display dependencies (Adafruit GFX, MCUFriend stuff etc).
Getting Started:
- Attach the display to your UNO or Mega board. I only used MCUFriend displays so far; basically everything compatible to Arduino should be useful, but how to get your display up & running might be different vs. the supplier.
- If required, calibrate the Touch screen. Keep the calibration values (see notes).
- Download the the source files and store in a directory (folder) on your computer.
- Make sure the directory and INO file have the same name.
- Open the INO file from Arduino IDE.
- Select UNO or Mega as board and select the COM port.
- Modify the touch parameters MINPRESSURE, MAXPRESSURE, XP, YP,... TS_LEFT, ... TS_BOTTOM in the GUIuser.h file with the parameters obtained from your Display calibration. Note this file has also a huge read-me section to start from. If you use another display brand, you probably have to adapt the display initialization and how the calibration must be performed / implemented in the code.
Notes.
- The major disadvantage of the used MCUFriend display is a shared resource for touch and digital control line for the display. It could have been handled gracefully in their driver, but unfortunately we have to do it. For this reason, the code contains a specific MCUFriend display initialize function.
- It should be possible to use an unused Arduino timer to interrupt your running application and execute touch_scan from there. Example how to use interrupts can be found in some of my stepper motor instructables. In such case, it's recommended to re-enable interrupts so that other tasks (like Serial.print) can resume while display touch is handled. But since AVR is a single 8bit 16MHz core, don't expect miracles in terms of speed if you consider this option.
- Cheap touch screens as used typically lack EEPROM. By assembly, the touch matrix is not matching the display pixel coordinates. In case of MCUFriend, display calibration is not more than just obtaining the touch coordinates from a known set of pixels (at the edges of the screen). Since the pixel position is known, touch coordinates are computed and printed for the border pixels. In the source code you will find a map function call. This computes the touch coordinate vs. the calibrated points and as such, map it with the pixel X, Y that is closest to the touch X, Y position.
- The display used in the demo video has 480x320 pixels in RGB565 (16bit per pixel). So the video RAM is 307200 bytes. If you connect a display by SPI, consider a lower resolution (ex. 320x240). If you reduce the amount of pixels to be written, the response seems faster.
Demo Explained
The source code has an example (it's a mixture of screen objects that have most of the time no correlation - except for the keypad - but it's showing what the GUI can do).
As you may notice, the source exists out of an INO file and a few CPP and H files (I don't use Arduino often and I'm more familiar with CPP and H than INO files so I stick to what I know). For the people not used to it and want to modify this source for own use: the callback functions you need are listed in the .h (header) file. This is called the prototype. The function itself is repeated with the same arguments in the cpp file. There you use it in the same way as you do with INO files.
Notes
- I have a version of this code that uses these structs in RAM (not published at this time; if there's interest contact me). Using RAM has the advantage the GUI parameters can be programmatically changed, but it will most likely never be able to run on a UNO: its RAM will be insufficient. In such case, the structs should not be defined const. A possibility under consideration is to duplicate the active struct array from Flash to RAM (allocate only the size so the largest struct will fit). That would keep RAM usage at minimal and provide this flexibility.
- The GUI computes values at the smallest possible value. The AVR core inside Arduino UNO or Mega is an 8bit architecture. If you use more bits, the core must perform more operations. For instance, the slider % output is an 8bit signed (ranging from -128 to +127). When your graphical slider is mapped back to this % output, it only uses 16bit values. This goes fastest and requires smallest code, but it comes with at a cost. To avoid rounding errors and make everything fit, your display is not allowed to have more than 655 pixels in any direction. But since most small displays don't exceed 480 pixels, this is acceptable.
The next steps explain each object available in the GUI.
Screen Data in SCR_DATA Structure
Each object that must be displayed on the display is formatted as a SCR_DATA structure. This structure defines
- Touch range: top left is (tX0, tY0); bottom right is (tX1, tY1).
- Object coordinate (oX0, oX1).
- Object Width (oW) and height (oH).
- Extra object information (oE).
- Object Color (oC).
- Label (or Text) position (lX0, lY0).
- Char-Pointer to Label (or Text). Don't worry to much if you're not familiar to pointers, the demo is considered sufficient understand-able to modify what's there without to much hassle (see notes).
- Font Size (fS) and Label Color (lC).
- A Callback function (referenced as onClick). Similar to Pointer: the demo shows simplified example.
- Low and High Percentages (oLoPct, oHiPct); mainly used on sliding objects.
Each screen is simply an array of such objects. The demo code should help you getting there.
Available objects are listed in the next slides. You can place as many (even identical) objects on the same screen as long as these objects don't overlap. If you place - for instance - 4 slider bars, this driver takes care of all. There's no multi-touch support by hardware, but each individual slider change is handled by a single function.
Notes:
- If an object should not respond to touch, set the touch area (tX?, tY?) all 0 or all 0xFFFF. Any value outside the touch range is OK.
- The source code is free to use and distribute.
- You may trash or comment out options you don't use (to further free up FLASH for your own application).
- A pointer to an array of chars (aka C-string) is not the same as a pointer to a single char. The C notation requires that pointers to arrays of chars end with a nul-terminated char As a result, the string length is what you can read + 1 zero byte. In C(PP) this last byte is often shown as '\0'. In the source code, you can find these char-arrays as const char IDENTIFIER[] PROGMEM = "text"; (see the GUIuser.h file). This works as follows:
- The text (between double quotes) is stored at some address in FLASH memory. The const and PROGMEM ensure the data is in non-volatile FLASH memory (in fact, PROGMEM prevents on AVR it's duplicated as const to RAM by the startup code. This is required to overcome AVR - a Harvard architecture - that is not able to handle data in FLASH as easy as Von Neumann architecture).
- Because of the double quotes, you indicate it's an array of chars (C-String), not a single char. It's a sequence of one or more char where the last char byte is a null ('\0'). This is inserted automatically when you use double quotes in your source code (whereas single quote references to a single char). Even a single char between double quotes requires 2 bytes of storage.
- IDENTIFIER is your pointer. This is the one you use to reference the text in SCR_DATA.
- Many people struggle with pointers because they never programmed in assembler. Therefore I explain it usually as: if you write in your code int n = 5, you store somewhere in RAM a value 5. The int indicates integer and is specific for your architecture (Arduino AVR: 16bit signed). When you reference to n, you can imagine your compiler uses n as a pointer to the memory location where the 5 was stored. What is the major difference: if you use "int n = 5" the pointer "n" doesn't feel like a pointer but in the compiled code, it often still is to some extent a hard-coded pointer. When you write "int* np", np becomes a pointer to an integer. Such pointer is often stored in RAM. That means you can change it: a property we need to point to the series of SCR_DATA structures in the source code as pointers allow arithmetic operations.
- If you look in the code, you may wonder why I typecast the struct pointer to a uint8_t* pointer. The regular struct operations (like pointing by dot or "->" don't work in FLASH stored structs. To overcome this, I typecast the struct pointer to uint8_t*. From this moment, I know pointer arithmetic applies on byte size. Using the indexof in the struct allows me to address the correct member. The only thing the code does is to read the parameter in the exact type as defined in the structure. Ex. if a struct member is a a 2byte signed parameter (int16_t), this gets read by FLws macro, because FL indicates FLash, w = word and s = signed.
- The callback function is declared to pass an 8bit integer. If you use a sliding object, this 8bit integer is the computed % of the slide position. For all other objects, it's the oLoPct value read from the struct. Because some functions may not require the data at all, I've declared the 8bit parameter optional by the unused directive. This prevents the compiler complaining about it (supress the warning). A tip: if you use a free editor like Notepad++, you can search through all the files in a directory. That gives a much better overview how things in mulitple files interact.
BACKGROUND
- Must be the first object in an array of SCR_DATA (he background is set by filling the entire screen with this color (what would wipe away previous objects))
- Ignored if not the first object in the screen array to prevent previous objects are erased.
- The SCR_DATA oC parameter determines the background color. If no BACKGROUND is defined in the array, default color is BLACK. But you can change in the screen_draw function.
BUTTON_RECT, BUTTON_RECT_RND & BUTTON_ROUND
The properties of BUTTON_RECT, BUTTON_RECT_RND and BUTTON_ROUND are mainly the same.
- Touch range is always rectangular from (tX0, tY0) to (tX1, tY1) in which the object is considered "pressed".
- The button top left is on coordinate oX0, oX1 (therefore BUTTON_ROUND adds 50% diameter to oX0).
- Rectangular objects have Width (oW) and Height (oH), but ROUND have a diameter equal to oW.
- BUTTON_RECT uses the object Extra (oE) information to allow a 3D effect when the button is pressed. The oE value defines how much pixels of this button are used for the 3D effect (relative to bottom and right side). This oE value must always be less than the Width oW or Height oH parameters. The color is automatically picked by dividing the oC by 4 (do red becomes darker red, and so on).
- BUTTON_RECT_RND uses oE to determine the button rounding, but does not support 3D effect.
- BUTTON_ROUND only uses the oW parameter, not the height and draws a button that has this diameter.
- If (lX0, lX1) are zero, the driver will automatic place the text relative to the absolute coordinates.
- The text pointer (if not NULL) is used to determine the desired text. The example code shows how.
- Only if a text is defined (no NULL pointer), the color (lC) and font scaling (fS) are applied to the text.
- oLoPct can be set to any value ranging -128 to 127 and is passed to the onClick callback function when the object is pressed.
These buttons are like momentary switches: only active when pressed.
CHECKBOX
If you compile the code with #define TOUCH_HIGHLIGHT RED enabled in GUIuser.h file, you will see the touch area for CHECKBOX is larger than just the square alone. This feature allows users to press the text rather than the checkbox, since this is intended for small displays. The image shows how checkbox touch area is larger than the indicator.
The checkbox is very similar to BUTTON_RECT, the difference are:
- The object size is intended for only the checkbox, not surrounding the text label next to it.
- A CHECKBOX is a latching switch. So it toggles the status on pressing. The active button is either:
- oE != 1: full square
- oE == 1: a cross.
- The status is indicated in the object range from oX0, oY0 with given Width oW and Height oH.
LABEL
Label is very similar to BUTTON_RECT. The demo code shows some options how labels might be used to allocate display areas for output, but you may also draw text directly on the display and remove this option.
Missing in the demo code is a function to read back the state of a given checkbox, but can be implemented by reading the variable and mask it with the CHECKBOX0...7 to obtain the specific value.
LINE, LINE_V & LINE_H
These objects just pass LINE,LINE_V (vertical Line) and LINE_H parameters to the Adafruit library. Vertical lines only use the oH parameter (discard width) while horizontal only use Width (oW) and discard Height. Only LINE can draw lines under an angle. May also be removed and replaced by direct line draws -> up to the user.
Reason why these are in is possible bypassing of required Adafruit Graphic library to further downscale the code size.
RECTANGLE & RECTANGLE_FILL
Very similar to BUTTON_RECT, but without FILL the background of the area remains in the background.
BUTTON_SLH
A button you can slide horizontal (compare it to a horizontal volume slider).
- Similar to most other objects, the touch area must cover the entire visible object.
- The visible sliding button is defined by oX0, oY1 as start position with a Width (oW) and Height (oH).
- oH must be even, otherwise the line will be erased.
- After being drawn, the visible slider must remain in range of the entire touch area.
- Either tapping, dragging or by calling a function with a % value sets the position. So an existing value position can be scaled on the slider in various ways.
- The bar has a left minimal value (oLoPct) and right maximal value (oHiPct). If the range does not cause overflow - info is in the source code, the callback function will receive this value as long as the touch area is active.
- The active range is from tX0 to (tX1 - oW) where the oLoPct and oHiPCT value are mapped upon (similar to the Arduino map function) but speed optimized for this core using only 16bit or less ranging parameters.
- The span (oHiPct - oLoPct) must be as large as possible to prevent overflows (ex. -100 to +100, 0 to +100. Unbalanced scale is allowed (ex. -50 to +100%).
- The lowest value must be larger than the highest value. If your application requires the highest value on the left hand side, you have to invert sign in the callback function but keep it lowest in the GUI.
- You don't have to worry about where the active drawing range exactly is. For the leftmost position the function returns oLoPct. For the rightmost position it returns oHiPct. Any position in between gets mapped according to the slider position.
BUTTON_SLV
BUTTON_SLH rotated by 90 degree. As a result, some requirements must be adapted but functional it's very similar to BUTTON_SLH excepts that it slides vertical instead of horizontal:
- Width (oW) must be even (instead of Height oH).
- The sliding range is from tY0 to (tY1 - oH) where the lowest position of the slider equals oLoPct and the higher position equals oHiPct.
Notes:
- Active range may be changed from (tY0 + oH) up to (tY1).
- The user does not need to be concerned what the active range of the slider is. On the bottom position, the slider returns oLoPct, on the top position it returns oHiPct.
SLIDERBAR_H
SLIDERBAR_H has very similar restrictions as BUTTON_SLH. Differences:
- The active area is reflected by a bar. The zero position of the bar is dynamically computed from oLoPct and HiPct values. If oLoPct is negative, the Zero position of the bar is scaled. In the last step I show some screen objects and how it correlates to the data in the SCR_DATA array.
- It's possible to use the object Extra (oE) parameter to add a certain amount of markers above the bar.
- Similar to BUTTON_SLH, the callback function receives the actual percentage when the area is touched.
SLIDERBAR_V
This is identical to SLIDERBAR_H but with vertical functionality. That vertical functionality is similar to the difference from BUTTON_SLV to BUTTON_SL:H
(If I have somewhat more time free I might provide more information; but I hope the info in the source file, combined with the example code helps you to start using the code).
Source Code for FLASH Based Operation
Below source code is required to run the GUI. It has been tested on Arduino IDE 2.6.3 using both Arduino UNO and Arduino Mega 2560. As mentioned earlier, the code is approx. 50% slower on the Mega, because more IO ports are involved to write to the same IO pins (basically the same problem you can find in my Instructable how to make a traffic light in a few lines of code).
On Arduino 2.3.6, the compiled code uses 487 bytes of RAM (23%) and 30144 bytes of FLASH (93%). Taken into account you can erase some of the example code and only require 1 up to 3 screens, there is still some left for your application (but not much).
Planned:
- Modify the MCUFriend library to use a single port on Mini Arduino Mega 2560 (this is expected to be even faster than the UNO display since the UNO still uses 2 different IO ports for the display whereas a Mega Mini can be modified to use only one IO port.).
- TBD: dropping the Adafruit GFX Lib to free up more Flash for UNO applications. This would have the disadvantage other GFX functions are no longer available.
Comments / requests: leave a message :-)
Overview
This shows how several SCR_DATA objects are configured and how it displays on the TFT. There are a few restrictions and helpers to allow you building up the screen:
- To keep the code as light as possible, error check is also minimal.
- Overlap touch areas in the present implementation executes sequentially callbacks of overlapping objects (in the sequence how these objects are defined).
- To make sure your touch area matches with the displayed objects, it's recommended to build the code at least once using the option #define TOUCH_HIGHLIGHT RED (where you can select another color than red to ensure your highlight color is not the same as the background).