Arduino Graphics Using Web Tricks

line.ctrl
5 min readOct 4, 2017

--

This is a post about the tiny Arduino-based text terminal emulator project. Please see the Hackaday project page for more background.

Working on the Arduino terminal emulator implementation required me to get very comfortable with string parsing and low-level graphics output code.

A TTY has to accept and process regular text and special character commands over serial, and show the results on screen (a 1x1 inch TFT driven by the ILI9163C chipset) using low-level display chip instructions. Both tasks turned out to be non-trivial. And, frankly, neither one was something I wanted to implement myself when I started the project.

In a hardware project, code is a chore, and at first I was feeling pretty lazy about having to do all this stuff from the ground up. But, laziness can be a good motivator for cleverness. And cleverness is something I am always ready to indulge in as a fun goal in itself!

Display code turned out to be a good area to get clever with. And my web-dev experience, of all things, helped a lot.

Complications

It is not that hard to push out pixels and text to screen in Arduino-land. Kind folks have written screen controller driver code for my display’s chipset and there are also awesome libraries like Adafruit GFX to help out. And, ostensibly, the text terminal program state is not that complicated — just receive characters over serial port and dump ’em out onto the screen based on a pixel font. The Adafruit library even has a drawChar command that does exactly that.

// character display using Adafruit_GFX
tft->drawChar(cursor_x, cursor_y, 'A', 0xFFFF, 0x0000, 1);
// cursor bar
tft->fillRect(cursor_x, cursor_y + CHAR_HEIGHT - 1, CHAR_WIDTH, 1, 0xFFFF);

But that is just the start of it.

The blinking text cursor has to be actually “blinked” (filled and cleared) continuously in code using an animation timer tracking its on-off state; the entire screen needs to scroll as expected when the cursor goes past the bottom row; there are some interesting line clearing commands to support, too. All in all, the terminal emulator display state is actually not that simple.

On top of that, the 4kb 8-bit Arduino Uno is not exactly a performance monster, and the tiny TFT screen I am using was probably meant for copiers or, I dunno, smart toasters. A complete full-screen redraw takes a really long time, despite there being only 32x16 characters to show. This necessitates optimizations and screen-chip-specific hacks (like the register-based view scroll) to apply, but they all increase code complexity.

Beside all those limitations, I also wanted room to play with gnarly techniques like sub-pixel rendering to make better use of screen real-estate. And I still needed to be able to expand the program with more features like supporting extended TTY commands. Finally, part of my plan was to keep display-specific code separate so that the code can be easily ported to work with other displays and even other MCUs.

I knew this would result in spaghetti code and bugs, so I wanted a solid plan from the get-go.

Reactive Rendering

My answer to those challenges was inspired by something that many Web developers rely on daily: reactive rendering. This is how frameworks like React, Angular and Vue update webpage content really fast while keeping the application code maintainable and sane.

This is the simplified pseudo-code for the main loop of the emulator:

struct term_state state;while (1) {
if (data_available()) {
// parse TTY command
// update term_state fields
}
render(&state);
}

The render function is just called continuously, even when there is no new data. Simple! But how does the emulator stay performant? We cannot redraw the entire screen on every cycle, after all.

The answer is to do what every functional reactive Web framework does — cache the crap out of everything and use change detection to re-render just the needed bits.

Let’s consider the following sample terminal state that tracks cursor position:

struct term_state {
int16_t cursor_col, cursor_row;
// ... etc, etc
};

The renderer maintains its own “shadow state” that represents the last displayed state:

struct rendered_state {
int16_t cursor_col, cursor_row;
};

Every render invocation then simply compares the current terminal state with the last rendered version and only does slow operations like cursor bar redraw when there is a difference between the two:

// detect change in cursor state
if (
rendered_state.cursor_col != term_state.cursor_col ||
rendered_state.cursor_row != term_state.cursor_row
) {
// clear cursor rectangle
clear_rectangle(rendered_state.cursor_col, rendered_state.cursor_row);
// show new cursor rectangle
draw_rectangle(term_state.cursor_col, term_state.cursor_row);
// save new rendered state
rendered_state.cursor_col = term_state.cursor_col;
rendered_state.cursor_row = term_state.cursor_row;
}

The above may not look like much, but this is exactly how Angular’s digest loop and React’s virtual DOM reconciliation work. Examine new state, compare with last state and draw only the changes.

Of course there is no such thing as DOM inside a running Arduino processor and in general this code is a lot simpler, but the concept is the same. Because state comparisons are “cheap”, they can run all the time, and control when “expensive” redraw operation needs to happen. And even then, only the parts that need fresh paint are updated.

Laziness Prevails

What this allows is to keep the renderer “contract” extremely simple: it’s just that one render() function that can be called at any point in the sketch. E.g. when parsing complex commands the terminal emulator calls the same render function from many different spots in the call tree.

In addition, any persistent state that helps the renderer optimize display is nicely encapsulated and separated from the rest of the code. Porting the renderer to a different display chip is much easier than hunting for draw commands interspersed throughout the data parser. Also, chip-specific optimization hacks are still possible but do not influence code structure outside of the display routines.

Hello world!

I am really happy with how this approach has been working out so far: with very little fuss, it has allowed me to implement basic cursor animation, screen scrolling and even to replace the character rendering routine (the stock drawChar method implementation was still not fast enough). It is neat to be able to apply a technique to Arduino hobby work from an area as distant Web development.

Check out more details and pictures on the Hackaday project page!

--

--

line.ctrl

Nick Matantsev: independent web developer by day, building electronics/graphics/art projects in my spare time