Pages: [1] 2 3   Go Down
Author Topic: Connecting a graphical LCD via a I2C using a 16-bit port expander  (Read 11930 times)
0 Members and 1 Guest are viewing this topic.
Global Moderator
Offline Offline
Brattain Member
*****
Karma: 452
Posts: 18694
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

I recently purchased a 128x64 bit graphical LCD from Adafruit, and decided to avoid using up most of the free ports on the Arduino, by connecting it via I2C, using a MCP23017 16-bit I/O expander chip.

This is a photo of the results:



Note that only four wires go from the LCD screen to the Arduino.

The cost is pretty minor - the expander chip sells for around $US 1.20 to $US 2.

More details, how to wire it up, and library code is available here:

http://www.gammon.com.au/forum/?id=10940

The library has fairly basic features, however you can draw text, clear rectangles to black or white, draw boxes, and "blit" in predefined images.

The nice thing about using I2C is that almost all of your pinouts on the Arduino are still free for connecting up to whatever it is you want to show on the LCD screen. And indeed since I2C can be shared, you could also use the I2C for some other device as well.

I'm no expert on making circuit boards, but it seems to me that this would be a useful project for a "backpack" board. It would basically only need the expander chip, a row of 20 holes for connecting to the LCD, and the contrast pot on it somewhere. I used a couple of resistors to pull the reset lines high, but they could probably just be straight wires. Any advice on how to go about getting such a board made would be welcome.
Logged

Sydney, Australia
Offline Offline
Full Member
***
Karma: 3
Posts: 230
Arduino rocks
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

Nice! I bought the same screen but with the serial backpack. The library for that is a bag of nails, so I think I might just remove the backpack and use your setup instead!


Cheers,
G.
Logged

Is life really that serious...??!

Dallas, TX USA
Offline Offline
Edison Member
*
Karma: 47
Posts: 2334
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

One thing you might want to look at for dramatically enhancing the performance.
is to create a write-through cache.
I have experimented with this in the latest ks0108 glcd library and it makes
a huge difference for typical usage cases.
For a 128x64 display it will consume 1k of RAM which granted is a half of what is
on a 328 but for some applications it is worth it and other AVRs have additional
memory so it isn't that bad.

In the glcd library it was less than 10 lines of code to do it
(around 13 lines including ifdefs to turn it on/off)

To do this, you create a memory buffer in AVR memory that mirrors the glcd display memory.
So every time you write to the glcd you also write to the AVR memory buffer.
For reads you never read the glcd hardware but instead read the data from the AVR memory buffer.

The code to do this is quite small (just a few lines).

I really like that chip, so I may also look at adding support for it
into the glcd library. It wouldn't be that difficult to add.

--- bill

Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 452
Posts: 18694
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Thanks for the comments! Much appreciated.

I tried adding the write-through cache, which I had initially resisted as it was gobbling up a lot of RAM, but admittedly you might have it free.

My measured figures certainly showed a big performance improvement, particularly for large pixel-based operations (eg. filling a large box). It was approximately twice as fast (I'm not sure I would call it "huge" but maybe that's a matter of opinion).

For example, executing this line:

Code:
  lcd.fillRect (20, 20, 50, 50, 1);

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

Maybe I didn't do it as efficiently as possible. I understand caching, but since I was allowing for multiple displays the cache was a member variable of the lcd class, so accessing it was a couple of dereferences.

I would be more excited if the time went from 4 seconds to 0.4 seconds. :-)

In terms of speed, the original is really quite fast for something like showing a bar graph of volume, temperature, etc.

For example, this test code here:

Code:
int sensorPin = A0;    // select the input pin for the potentiometer
char buf [20];

void loop ()
{
  // read the value from the sensor:
  int sensorValue = analogRead(sensorPin);   

  // draw bar
  lcd.clear (0, 16, sensorValue / 10, 23, 0xFF); 
  lcd.clear (sensorValue / 10 + 1, 16, 127, 23, 0); 
  lcd.gotoxy (0, 32);
  lcd.clear (0, 32, 127, 39);
  sprintf (buf, "Value: %i", sensorValue);
  lcd.string (buf); 
 
  delay (100);
}  // end of loop

This read (random noise) from A0 and displayed a bar using the (fast) clear routine. It also showed the value as a number. This ran so fast it flickered annoyingly, hence the 100 ms loop to slow it down a bit.

So I think a bit of careful screen layout, allowing for the more efficient use of boxes aligned on vertical 8-pixel boundaries, is what really speeds things up. Basically you reduce having to do 8 writes to the LCD screen down to one write, which is the big time-saver.

I didn't really emphasise it before, but with the I2C approach you could easily enough have multiple LCD screens, all connected to the same 2 pins on the Arduino. So for a project that needed to show a lot of data, that could be ideal. Of course, they are sharing the same data bus so throughput would be down a bit, but if the important thing to you is to show a lot of data, rather than updating it really quickly, that could be a nice solution.
Logged

Global Moderator
Netherlands
Online Online
Shannon Member
*****
Karma: 169
Posts: 12441
In theory there is no difference between theory and practice, however in practice there are many...
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

Quote
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?
Logged

Rob Tillaart

Nederlandse sectie - http://arduino.cc/forum/index.php/board,77.0.html -
(Please do not PM for private consultancy)

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 452
Posts: 18694
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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.
Logged

Dallas, TX USA
Offline Offline
Edison Member
*
Karma: 47
Posts: 2334
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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
Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 452
Posts: 18694
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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.

Quote
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. smiley 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. smiley

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:

http://www.gammon.com.au/forum/?id=10893

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?
Logged

Dallas, TX USA
Offline Offline
Edison Member
*
Karma: 47
Posts: 2334
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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
Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 452
Posts: 18694
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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.
Logged

Dallas, TX USA
Offline Offline
Edison Member
*
Karma: 47
Posts: 2334
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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
« Last Edit: February 18, 2011, 03:40:24 pm by bperrybap » Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 452
Posts: 18694
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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.
Logged

SF Bay Area
Offline Offline
Edison Member
*
Karma: 10
Posts: 1235
Arduino Ninja
View Profile
WWW
 Bigger Bigger  Smaller Smaller  Reset Reset

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

Unique RGB LED Modules and Arduino shields: http://www.macetech.com/store

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 452
Posts: 18694
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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.
« Last Edit: February 22, 2011, 01:23:02 am by Nick Gammon » Logged

Global Moderator
Offline Offline
Brattain Member
*****
Karma: 452
Posts: 18694
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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.
« Last Edit: February 27, 2011, 11:02:58 pm by Nick Gammon » Logged

Pages: [1] 2 3   Go Up
Jump to: