As I said in the
project objectives, I want to equip the box with a color display driven user interface that can be operated using a
rotary encoder with push button.
An 1.8 inch TFT display based on the ST7735 chip with a resolution of 160x128 pixels and an SPI interface can be had for USD 20 from Adafruit [1] (or for less from your favorite Hong Kong supplier, but consider your karma). Adafruit has also developed and maintains an Arduino library to interface it [2]. They also ship and support larger and higher resolution displays, such as 2.8 inch or 3.5 inch ones, but I want to keep cost and computational load as low as possible.
Interfacing the ST7735 display is dead easy and was an instant success for me: On the reverse side, VCC and LED pins connect to +3.3V, GND connects to ground, CS, RST, and DC are assigned to Arduino PIO pins (e.g. pins 2 to 4), SCL connects to the SPI header's SCK pin, and SDA connects to the SPI header's MOSI pin. Like for most things, YouTube has more than one tutorial, e.g. [3].
Once the Adafruit library is told the pin numbers of CS, RST, and DC, it takes care of the initialization procedure. The library itself can then be used to draw a graphics primitives such as pixels, lines, and rectangles. In this post, I would like to talk about two things:
- Design and implementation of a text buffer class for the ST7735 built on top of Adafruit's class. This is done so I can easily design a user interface based on color characters instead of graphics primitives.
- Replacing Adafruits bit-banging SPI interface by a DMA transfer from memory to SPI for speed and to parallelize the rendering of the text buffer (by the Arduino CPU) and transferral of the rendered buffer to the display (by the Arduino's DMA controller)
Text framebuffer
My class
TextFrameBuffer realizes a text mode frame buffer that contains a two-dimensional array of character-attribute pairs. Old-schoolers among you will remember VGA's mode 3 frame buffer at B800:0000 - this is what this class recreates.
The frame buffer's size ST7735_SCRWIDTH x ST7735_SCRHEIGHT depends on the size of the font's characters. I'm currently using a 6x8 pixel font and hence get 26x16 characters. Each frame buffer cell in
m_buf stores an ASCII character byte in [0], and an attribute byte in [1]. The attribute is composed from a 4-bit foreground and a 4-bit background palette index, enabling 16 colors that are predefined in
s_palette.
The list of text functions should be self-explanatory and will be extended as the need arises. The job of
render is to convert the current text frame buffer to a pixel frame buffer and transfer that one via SPI to the display. In order to keep the data transfer minimal, a
dirty_update keeps track of a dirty rectangle, the frame buffer region that needs to be converted and transferred.
If the dirty rectangle is non-empty, the function prepares the ST7735 address window, initiates the SPI data transfer, and then loops over all character rows (
y), all vertical scanlines of a character (
jj) and all horizontal pixels of a scanline (
x). Then, the attribute at (y,x) is converted into a pair of ST7735 foreground and background colors using
s_palette. A horizontal pixel line is fetched from the
font, depending on the ASCII character code in the text frame buffer at (y,x). Then, the bits in that line are read and either foreground or background color are written to the scanline buffer.
We'll discuss what to do in place of the TODO in a second.
Faster bit-banging transfer to SPI
The first thing to notice is that Adafruit's display and library are somewhat sluggish, to the point of being clearly noticeable on the display, for two reasons: The SPI clock is set to 4MHz (CLK divider 21) only, and a bit-banging SPI access is used and transfers only one pixel color at a time.
As for the first point, my display tolerates an SPI clock rate of up to 42 MHz (CLK divider 2), which is a huge step forward. This apparently depends on the particular batch your display comes from, so your mileage may vary.
As for the second point, a lot can be gained by extending the SPI class used by Adafruit's ST7735 class by an additional function that transfers an entire buffer of bytes via SPI, instead of repeatedly calling transfer() for each byte in the buffer.
DMA transfer to SPI
Even better, however, is the idea to offload the job of transferring a scanline buffer via SPI. This has recently been demonstrated for displays based on the IL9341 chip, e.g. [4,5]. We're going to do the same for the ST7735 (something I couldn't find out there) and free the Arduino's CPU of the entire SPI communication burden, which will taken on by the Arduino's DMA controller. In doing so, the CPU can assemble the next scanline already while the current one is being transferred.
In order to do so, the SPI initialization code needs to be changed significantly: First, the DMA controller needs to be enabled. Then, we'll fix the SPI pin used, and we'll also fix the SPI chip select (CS) number to that particular pin such that DMA buffer transfers end up at the proper device.
I'm sure I'm doing some things twice here by calling
SPI_Configure first. 84/SPI_CLK_DIVIDER is the SPI clock in MHz; I'm using SPI_CLK_DIVIDER=3 for 28MHz without any issues.
Below is the code for initiating a DMA buffer transfer. The function
sendBufferDMA accepts a data pointer and a buffer length in bytes. Internally,
SPIClass has a new member
dma that indicates the number of an 8-bit DMA channel to be used; I'm using channel 0.
After
sendBufferDMA returns, the CPU can proceed doing other things (e.g. assemble the next scanline) while the buffer is being transferred. Before initiating the next DMA buffer transfer, we need to see the current one out:
In place of the TODO in the code above, I can now call:
I use two
scanline[] buffers indexed by a variable
nbuf that flips between 0 and 1, such that there's always a buffer being assembled and a buffer being transferred.
Using
waitForDMA and
sendBufferDMA, the burden of text buffer rendering is barely noticeable on my Arduino Due.
As always, the modified
SPIClass,
Adafruit_ST7735 class, and
TextFrameBuffer class are available for download from the
project's github repository.
References
- [1] https://www.adafruit.com/products/358
- [2] https://github.com/adafruit/Adafruit-ST7735-Library
- [3] https://www.youtube.com/watch?v=boagCpb6DgY
- [4] https://www.youtube.com/watch?v=vnEwzN14BsU
- [5] http://marekburiak.github.io/ILI9341_due/