Having convinced myself to use a Sharp Memory Display for the Bramley, I put an order in at Pimoroni for the new 400x240 module. A few years ago, I'd bought a 96x96 version (photo above), and I decided to dig that out too so I could make an early start on the code.
Unlike the new display, the 'black' pixels on the older board actually have a mirror-like finish. It's striking, but depending on what it reflects it can be hard to read. Both displays use the same interface, though, so it will make good target practice until my new screen arrives.
I gathered my resources and prepared to write a Rust library.
- Datasheet for the 96x96 display
- Datasheet for the 400x240 display
- CircuitPython library by Adafruit
- C++ library by Adafruit
- C++ library by MakerDyne (MakerDyne manufactures a different breakout board, might be instructive)
Writing to the display
The Memory Display is driven using SPI, but unlike most SPI devices, it expects the chip select pin to be high when writing to the display, rather than low. And, unfortunately, I couldn't persuade the Raspberry Pi to use active high.
Eventually, I gave up and wired a generic GPIO pin as my chip select that I manually set high or low as required. Other libraries do the same, so perhaps it's a known limitation of the Pi. Anyway, with my workaround in place, I was ready to write some pixels.
Oh, the new screen arrived by the way! And good news - it works on both screens. There's just one problem: the refresh rate is too slow - especially on the large display - it can take 160ms to redraw the screen. Even if I did nothing but draw, that's only 6 frames per second!
If you want to know how painful 160ms of latency is, try it for yourself (alternative). If I'm going to use this screen, it will need to update faster. So I checked the datasheet to see how fast I could push the SPI clock.
I had started with the 'typical' value of 1MHz. By switching to the 'maximum' 2MHz I could refresh the screen in 80ms. Much better.
I also spent some time tweaking the SPI buffer size and using chunked writes, but the gains were small. After that, it seemed like a good point to benchmark against Adafruit's own Python library - just to check I wasn't doing anything stupid.
The Python performance wasn't great, but in the process of inspecting their code, I noticed they ran the clock at 8MHz - four times the maximum! With a little apprehension, I set my code to 8MHz too and ran it.
The screen updated in 37ms. But was it safe? That was a few weeks ago now, and since then - after 11 months at 8MHz - Adafruit have slowed their library to the recommended 2MHz. Perhaps they'd run into problems? I asked Sharp for their opinion.
To my surprise, someone responded. The memory display has a mean time between failures of 50KHrs at 25°C, and they explained that, at a higher speed, I'd increase the internal temperatures and lower its functional life. It might be OK - depending on product variation and my control over the operating temperature - but they understandably wouldn't warrant values outside the spec.
So, at least to begin with, I'm going to stick to 2MHz and hope 80ms is fast enough for a full screen refresh. I wouldn't want to damage anyone elses screen if they ran my code - even if it works OK on mine. But, before I finish, this screen has one more trick up its sleeve.
Each screen update looks something like this:
COMMAND BYTE (PADDING) LINE ADDRESS 1 ROW OF PIXEL DATA 1 (PADDING) LINE ADDRESS 2 ROW OF PIXEL DATA 2 (PADDING) ... (PADDING)
Basically, a byte to tell the display we're writing to it, followed by each line's address and pixel data.
This is where the 'Memory' in 'Memory Display' comes in. Once updated, those lines will display the same pixels continually while powered. So, in theory, I could skip any lines that remain the same between updates and send only those that changed.
I modified my library to remember the pixels from each screen update and diff them against the next. Now, if a row hasn't changed, I skip it.
It's a big improvement, and the update times now vary between 0ms - 80ms, depending on how many rows were updated. Updating horizontal text, for example, would only modify a few rows, which is great because it gives low latency for typing.
The varying update times are a quirk, for sure - but I think this might be good enough.
My Rust library for the Sharp Memory Display (source code)
Next time, I'll look at reading input from the six buttons on the back of the device.
Next entry: Bramley: 5. Buttons