For the last few days, I have been playing with some small displays we have lying around in Labitat. We have ninety-odd of them from an old donation, and I thought it would be cool to be able to use them for some fun projects.
The displays are ET024002DMU with a built-in ST7787 controller. They really are quite nice displays. 240-by-320 resolution in a 42mm-by-60mm form factor. 18-bit colour and a nice and clear display at a high resolution. The protocol is reasonably well designed and described, and they have some nice extra features that should make them suitable for most tasks, even demanding ones.
The picture to the right shows a display soldered to a simple breakout board and connected to an old STM32F405 (ARM Cortex-M4 board) of mine. This is using the display’s 16-bit parallel bus, which allows for really fast update. It should even be possible to connect the display to the memory controller of the STM32 and utilise DMA to update the display without tying up the CPU (though my test just used bit-banging on a GPIO port). Other nice features include the ability to sync with or control the vertical refresh (to avoid visible glitches/tearing during frame updates) and hardware scrolling. The display is spec’ed for 2.8V, which is somewhat inconvenient when most of my stuff is otherwise running 3.3V logic, but so far just running the display at 3.3V seems to work fine.
Driving the display over a 16-bit bus from and ARM MUC is cool for high-end stuff. Since we have so many of these displays lying around, it would also be cool to have something that is really easy, and really cheap, to use. So I extended the TFT_eSPI library for the ESP8266 to support the displays. The ESP8266 is very cheap and conveniently runs 3.3V logic (as opposed to the Arduino’s normally 5V). The ESPs have become very popular, so since I have not used them before, this was also a good chance to learn a bit more about them.
The TFT_eSPI library is done by Bodmer, derived from some Adafruit libraries. It uses the serial interface mode of the display. This greatly reduces pin count (the ESP8266 seems quite starved for GPIOs), but also greatly reduces performance, of course. But the TFT_eSPI is well optimised, and should be adequate for most purposes. I think I managed to fully implement the ST7787 controller for the library, all of the demos seem to run well. And the performance should be on par with the other supported display controllers. Here is a video showing the display running some demos from the library:
While implementing the support in the library, I discovered a couple interesting details about the display that I wanted to write up. Generally, these kind of displays all seem to share a protocol, and work much the same way. However, the ST7787 only supports a 3-wire serial mode, while the other controllers supported by the library are using a 4-wire serial mode. The difference is that the 4-wire mode uses a dedicated pin DC to distinguish between command (DC=0) and data (DC=1) bytes. In the 3-wire mode, the DC pin is not available, and the data/command bit is instead included as a 9th bit with every byte. Here are diagrams showing this, taken from the datasheet of another display controller, the ILI9341:
The main work in extending the library to support the ST7787 was to re-write all the data transfers to handle the 3-wire mode, bit-stuffing the D/C bit into the SPI data stream instead of pulsing the D/C GPIO. The bit-stuffing probably introduces some overhead; on the other hand, now multiple commands can be given to the ESP8266’s SPI peripheral at once, without the need to manually pulse D/C in-between transfers. So I expect the performance to be roughly equivalent; it will be interesting to run some benchmark comparisons against other displays.
I did discover one notable limitation of the 3-wire mode, though. This is when reading pixel data back out of the display’s frame buffer memory. There is a command RAMRD to do this, and in the other modes, it allows to read out all pixels in a previously defined read window at once in a single command, or a subset of pixels. But this did not appear to work in the 3-wire mode. It turns out that the display controller takes its dataline into high-Z mode as soon as one pixel has been transfered, no matter the size of the defined read window. This means that frame buffer reads will be a lot slower on the ST7787, due to the overhead of reading out pixels one by one (fortunately, framebuffer read is not a common operation).
I am thinking that this limitation comes from not having the D/C pin available to distinguish data from commands and thereby terminating a transfer. Apparently, the read window size is not available to the SPI interface, or forcing the host to transfer the full window size was deemed undesirable. Thus, only single pixel transfers are supported. The data sheet is not very clear on this, to say the least.
A related limitation is seen in the RAMWR command, which writes a sequence of pixels to the display. The RAMWR does support writing multiple pixels, which is fortunate as this is one of the most used and most performance-critical operations. But what I found was that when writing less that the defined window size, some glitches can occur. It appears that the controller’s SPI interface is buffering up to 4 pixels before writing them to the display memory. When writing less than 4 pixels, the pixels would not be written to framebuffer memory immediately. Instead, they would be included in a later write, appearing in the wrong location!
This problem only occurs when writing less than the defined window size. So once understood, it was easy to work around by always setting the right window size before writing pixels – which only affected the line drawing algorithm. Again, this seems to be a limitation of the 3-wire protocol due to missing D/C input. Apparently, it is not able to utilise the D/C bit that is available to the controller in write mode, instead relying on the frame buffer handler to notify when the write window is full. Or maybe it is just a controller bug.
It was btw. interesting to see how similar these different controllers are. This is quite apparent also from similarities between the data sheets. There has clearly been a lot of copy-paste between them. For example, I found this gem in the ST7787 datasheet (in the description of the WRX pin):
-In 4-line serial interface, this pin is used as D/CX (data/ command select)
The ST7787 though does not have any 4-line serial interface (there is no other mention of this mode anywhere in the datasheet). So this is clearly a left-over from a copy-paste from another controller…
Otherwise the implementation of ST7787 support in the library was reasonably straight-forward. The library uses a lot of direct register access to the SPI hardware to optimise the transfer speed, so some care is needed to get the bit-stuffing right in all cases. One thing to be aware of is that the RAMRD command for reading a pixel needs 9 dummy clock pulses between sending the command and reading back the pixel data. A very careful reading of the datasheet might be said to hint at this, though it really is not very clear.
Another thing to be aware of is that the ST7787 uses a single shared data line for its communication; it does not have separate MOSI/MISO lines. This is not a problem in practice, as the controller never transmits and receives data at the same time. So the D0 pin of the ST7787 can simply be connected to both MOSI and MISO on the ESP8266 microcontroller. But the code then needs to temporarily disable the MOSI pin output while reading, to avoid contention on the data line.
So with the library working, it should be easy to control the displays from an ESP8266. The library has a lot of nice functionality, including really good text support with a lot of available fonts. The next step should be to make a couple nice PCBs with connector for the display and for an ESP8266, some power supply, handy pin breakouts, etc. Hopefully some cool projects will come of it in the coming months.