Connecting a graphical LCD via a I2C using a 16-bit port expander

lcd.fillRect (20, 20, 50, 50, 1); <== thats 900 pixels

  • Without cache: 4.686 seconds
  • With cache: 2.586 seconds

Are those really seconds? or should that be millis?

No, seconds. Let me run through how long it takes to set a pixel:

  • Take EN high, "set page", take EN low: 0.5 ms - that sets the Y address
  • Take EN high, "set address", take EN low: 0.5 ms - that sets the X address
  • Set the data lines to inputs, so we can read the pixel: 0.3 ms
  • Take EN, DATA, READ, CS1 high: 0.3 ms
  • Take EN low, keep DATA high: 0.3 ms (this tells the LCD to latch the value)
  • Take EN, DATA, READ, CS1 high: 0.3 ms
  • Take EN low, keep DATA high: 0.3 ms (this tells the LCD to read the value)
  • Actually read the value from the LCD: 0.2 ms
  • Switch the data lines back to outputs, so we can change the pixel: 0.3 ms

(reading advanced the address, so we have to put the LCD register back)

  • Take EN high, "set page", take EN low: 0.5 ms - that sets the Y address
  • Take EN high, "set address", take EN low: 0.5 ms - that sets the X address
  • Take EN high, write the new data byte, take EN low: 0.5 ms

Total time to change one pixel: 4.5 ms

Multiply that by 900 pixels: 4.5 * 900 = 4.05 seconds

The observed time was a bit longer as I didn't measure all the gaps between the actions above, plus there are loops and computations in the C code.

The fact is that commanding display hardware to do things, one pixel at a time, is notoriously slow. The display has 128 * 64 pixels (8192), so anything that has to be done 8192 times, plus stuff like setting up address registers, is going to be slow.

However if you make efficiency compromises, like writing a byte at a time destructively, you can make major speed improvements. As I said before, drawing a bar where you send a byte at a time reduces the time by a factor of 8, and not caring what was there before saves the time taken to query the display for the previous value.

Like I said above, if you stick to rendering headings once, and then smallish amounts of text (like temperature, humidity etc.) more often, the speed is perfectly acceptable.

If you want to have a display that shows 8 lines of text, perhaps some lines between them to divide sections, and a few bar graphs, it will work fine.

For speed, probably the ultimate efficiency would be to pre-render everything into processor RAM, detect which bytes have changed (a second buffer perhaps?) and then just push through changes, a byte at a time, in the optimal order to reduce the need to re-setup the LCD's address registers. But apart from the cost of RAM that would entail, I was aiming for a simple system that anyone could plug in, with minimal overheads both in wiring connections the the Arduino, and memory used by the library.

Some of those timings do seem a bit lengthy.
Like setting control lines. 300 uS seems like a long time
to get control lines set.

There are several optimizations that can be done to reduce
the overheads by eliminating many of the i/o accesses to the glcd.
(I did all the low level code for the ks0108 now glcd library).

You actually don't need to fully render everything in ram to move to
doing full byte/page accesses when possible.
All you have to do is have some clever
code (that is actually quite complex).
Turns out that you only need about 3 bytes and several local
status variables to ensure full byte accesses when possible.
It does get quite hairy/tricky to do any sized font rendering on
any boundary and paint the glyphs horizontally rather than
vertically.
Things like fills are not too bad.

Another thing that really helps is to keep track of the page
and address for each chip. That way you can take advange
of the auto increment to advance the address for you avoiding
that command when doing multiple horizontal operations (writes) such
as fill operations. Also if you track the page you only have to set it
when changing rows.

The code can also be made smart enough to detect
the boundaries of things like a fill operation such that
it can combine multiple pixels to build up a page/byte before it
is tossed to the physical display. It can also detect page boundaries
and full page fills and then do full byte writes (with no reads) when possible rather than doing 8 individual pixel operations.

So you only have to do reads when the updating data is not a full page.

The rule of thumb is always push pages to the lcd horizontally.
First handle the fraction pages at the top of a fill
then do full pages, then do the fractional pages along the bottom of the fill.

So in your example of
lcd.fillRect(20,20,50,50,1);

You would read the page 3 (pixels 16-23) then update/set pixels
20-23 and do that starting at address 20 out to address 50.
You would only set the page and address once and auto increment would
take care of the rest for that row.
Yes you have to read each page 3 as you go across but
it turns into a read operation followed by updating the byte
locally in ram and then a write. (if caching, then the read is eliminated)
Each byte written is updated with the same mask value.
Then for the next 3 rows below that, it would reduce
to nothing but full writes of the mask value (0xff or 00 depending on color)
No reads, no set pages, no set addresses just writes.
Yes there is an initial set page and set address to start the row
but then the next 30 pages (from pixel 20 to 50) would be written as full pages.
Then the pixels from 48-50 have to handled just like pixels at 16-23 since
they are not a full page.

So now instead of 900 set pixel operations,
which is set page, set address, read, set address, write

you have 60 reads, (30 at top and 30 at bottom) and 150 writes.
And the number of set page and set address operations would also
be dramatically reduced as you would only need one per row
to start off the row.
(actually there is 1 set address for every read as well to back up the address
after a read to prepare for the write)

Turn on caching and now the operations to the glcd would be:
(repeat 5 times) set page, set address, 30 writes

So instead of 900 reads (actaully 1800), 1800 set page, 1800 set address,
and 900 writes,
you do 5 set page, 5+60 set address, 150 writes.


This is how the glcd library works. It optimizes the operations
to full pages as much as it can.
Now I'm curious, how well the glcd library would work
sitting on top of a port expander because it already has all the logic
to strip out all the unneeded set page/address commands and collapses
everything down to full page accesses when possible to absolutely
minimize the traffic to the glcd module.

--- bill

You are quite right of course.

My font drawing does in fact make use of the auto increment of the hardware by noting the new X address, so it only has to set the page/address when it crosses the 64-pixel boundary. Similarly for clearing in blocks of 8 pixels.

And you are right that filled rectangles could be optimized into the parts that fall onto 8-pixel vertical boundaries, and the edge cases. Although as you note it gets quite complex.

An interesting compromise would be to keep the library simple, but for the implementer, if they really really needed filled rectangles, and couldn't design them to fit onto 8-pixel boundaries, to combine the operations themselves. For example, do the fast fill and then do a couple of line draws to extend the rectangle vertically as required.

My only concern was that, without knowing the end application, to have a library that was quite complex, when it might not be needed. Although if the linker optimizes that out perhaps no real harm is done.

Anyway, it has been a good learning exercise - I find that if I am forced to find how the hardware works I use it more efficiently.

Now I'm curious, how well the glcd library would work sitting on top of a port expander ...

Well that would be an interesting exercise. :slight_smile: One of the reasons I wrote my own code was because the existing libraries seemed to be heavily reliant on toggling individual pins, whereas using the port expander you had to take bit more of a "batch" approach. Plus I learnt more about port expanders. And LCD modules. And graphics. :slight_smile:

Even without much in the way of fancy graphics I am leaning towards upgrading my mini-Adventure game to use the graphics LCD because I get 8 lines of text rather than 4. To see the earlier version:

With 8 lines of text, and maybe a mini-map in the corner, it could be quite cool. Well, as a teaching exercise if nothing else.

Just as a quick hardware question - I initially connected the backlight straight to the +5V, but then read somewhere that maybe a series resistor was warranted. Adding in a 220 ohm resistor cuts the backlight down from "quite bright" to "nice and subtle". Do you know if the backlight is intended to be directly connected to the +5V, or do they assume you put a resistor in, like you usually do for LEDs?

With respect to the resistors on the backlight. It depends on the module.
Some do need them and some don't. Some will actually burn out instantly with
no resistor. So I use a resistor all the time just to be safe.
Measure the current and see if falls within the spec
(if you can really believe the spec).
For several of mine I inserted a larger than necessary resistor because I power my circuits from USB and to keep the power under the USB spec
the display needs to be limited more than usual.
(most USB ports can supply way more than the spec - which is how
all these small external USB hard drives work - they use more than
double the spec when powering up)

In my experiments for using limiting resistors, I learned that the human
does not see brightness linearly. What that means is that it sees a change
in brightness (light energy level) easier when the level is low rather the bright
and that once you achieve a certain level of brightness, the eye really
can't detect it any more. So at least for my eyes, I can limit the current
a bit and not tell any noticeable difference.

Also some of my circuits are battery powered which benefit from the reduced
current. I noticed that often you can power it with half or less than the spec
and still see things quite well when indoors. - Outdoors, well that is
another matter.

And I'll agree with you that the only real way to learn about this stuff is to really
dig down into it by writing a real project.

I'm impressed with your i/o expander. Neat project.


On the graphics and speed optimization yet keeping things simple,
one thing that I've also thought about doing is to make a very slimmed
down "lean and mean" library that uses the CP437 font.
(if you haven't seen this google it).
With CP437, you could do many graphics like capabilities
(horizontal and vertical lines) without actually doing graphics.
There are even characters/glyphs that allow doing horizontal and vertical bars
for bar graphs on pixel boundaries.
And if you force the font to be 8x8 and always
land on row boundaries, you can slam the characters out and remove
the reads, even when doing graphic like line operations.
With CP437 and a 8x8 font you will not insert any pixel padding
between characters (it isn't needed for this type of font).
Sure the graphics are limited to some straight line functions, but
you can do quite a bit using only the CP437 font.
And it is much faster than plotting lines.
Also, it is easy to support a "wide/bold" mode and a "tall" mode.
Wide and tall modes simple double each pixel of the font.
Wide is really easy as you simply double each page/byte as
you go.
Tall is a little bit more complicated as you have to stretch the byte
into two bytes and interleave the bits.
Its kind of like was don't back in the late 70's and early 80's before
we really had the ability to set individual pixels.

What would be great would be a library that sat on top of the
cp437 font to provide the "graphic" functions.
Things to draw rectangles/boarders, horizontal and vertical lines,
and bar graphs etc...

For many applications cp437 would work fine.
I'm surprised that it isn't more widely
available on the character only lcds.

--- bill

bperrybap:
On the graphics and speed optimization yet keeping things simple, one thing that I've also thought about doing is to make a very slimmed down "lean and mean" library that uses the CP437 font. (if you haven't seen this google it).

Ah, that font! That brings back fond memories of when I used to work with Turbo Pascal. We used to make quite nice menus with boxes, bars, double lines, tick marks, etc. And the gray boxes could be put to good use too.

Indeed there is absolutely nothing stopping you adding that in, because in the case of my library I had a "bit blit" function that copies characters in groups of 8 pixels to the screen, so each glyph in an 8x8 pixel font would simply be 8 bytes. To build that into the library would take 1024 bytes (8 * 128), which is memory you may or may not want to spare. Perhaps with a define around it?

I wonder if anyone has the CP437 with the pixels written down as bytes? That way at least it could be offered as an option, and it is tedious doing it yourself. So far most of the links I have found have shown what the font looks like, or the Unicode equivalent, but not the pixels as a group of 8 bytes.

I've got it if you want it.
(Couple of different sizes but 8x8 is easiest to work with)
And the format of the data matches the bit order of the glcd pages.
I played around with it on ks0108s for a while.
PM me if interested.
--- bill

Thanks Bill. I got the file and incorporated it into the posting on the link I gave above. There is also a graphic showing which glyphs you get for each character.

I made it an optional include, being conscious that with only 32 Kb of memory, you may not want to spend 1 Kb on a font you never use.

How fast is it if you put "TWBR = 12;" after "Wire.begin();" in your library? (sets I2C speed to 400KHz instead of 100KHz)

Good question!

Without the speed change:

  • Clear screen: 588 ms
  • Draw 96 characters of text: 347 ms
  • Frame a rectangle: 278 ms
  • Fill a rectangle: 4687 ms

With the suggested change:

  • Clear screen: 229 ms
  • Draw 96 characters of text: 148 ms
  • Frame a rectangle: 120 ms
  • Fill a rectangle: 1753 ms

With the suggested change and write-through caching enabled:

  • Clear screen: 230 ms
  • Draw 96 characters of text: 147 ms
  • Frame a rectangle: 78 ms
  • Fill a rectangle: 976 ms

The activities which don't involve reading back were within 1 ms of before, but the ones which involved pixel drawing were somewhat faster.

Personally I think that writing all 96 characters (over half a 8x21 line screen) in just over 1/7 of a second is pretty reasonable.

Today I received some MCP23S17 chips, which are the SPI equivalent of the MCP23017. With some fairly minor circuit changes (the pins are almost identical), and some code changes, the library is now much faster (at the expense of having to run two more wires):

  • Clear screen: 62 ms
  • Draw 96 characters of text: 54 ms
  • Frame a rectangle: 58 ms
  • Fill a rectangle: 622 ms

That is without write-through caching enabled. With it enabled the relevant times dropped to:

  • Frame a rectangle: 48 ms
  • Fill a rectangle: 432 ms

With the exception of big block fills, it now fills the screen very quickly indeed.

Are you using software SPI or the hardware SPI transceiver for this?

At the Arduino end? The hardware SPI. Why would you not use it? It clocks out bytes at about 3 microseconds each. You can connect multiple SPI devices as long as each one has its own SS line. The library I wrote lets you choose the SS line you have dedicated to the LCD (it might not be D10) so you can also connect Ethernet shields, etc.

hey can use this library if i use 2 pcf8574p as port expander.

Not unchanged. You are addressing two port expanders rather than one. The datasheet I found seems a bit light on information about the protocol, but it is probably different. I'm sure it can be done, but you will need to modify things here and there.

i need to modify library or just hardware part

I'm not sure what you mean by that. You are replacing one chip by two chips? Surely that involves a hardware change.

So you need to modify both.

thank you sir

is there any other option for mcp23017 ic. it is not easily available in india1

Mail? I get most of my stuff from overseas.