Setting Pixels for ST7920 based 128x64 LCD

Started by Kevin Townsend November 25, 2008
I've been working on a driver for a ST7920 LCD I received today,
since most of the drivers I found on the web tend to be for
KS0107/0108 LCDs. I'd like to post the driver here when I'm finished
so that other people don't have to pull their own hair out, but I was
wondering if anyone would be able to offer some help on setting
individual pixels.

I've got everything working with the basic command set, and can
display text, initialise, etc. I can't figure out how to get the
extended command set working, though, specifically the ability to set
pixels individually.

I've noticed that you need to change the GDRam address to a position
in an 8x4 grid (see: st7920_setGDRamAddress), and then you can set
pixels in a 16x16 block, but how to set those 16x16 pixels is where
I'm stuck.

Does anyone here have any experience with this LCD controller that
could perhaps fill in the "st7920_setPixel(U8 x, U8 y)" method
appropriately?

http://www.sitronix.com.tw/sitronix/product.nsf/Doc/ST7920

I'll post the code in the another message

Kevin.

An Engineer's Guide to the LPC2100 Series

ST7920.H
=============================================================
#ifndef ST7920_H
#define ST7920_H

#include "sysdefs.h"

/* Common Aliases */
#ifndef PIN
#define PIN(n) (U32)(1 << (n))
#endif

/* Pin Registers */
#define ST7920_BUSPIN0 16 // Parallel Bus
(P0.16..23)
#define ST7920_DATA ((U32)0xff< #define ST7920_BUSALL 0x00FF0000 // Parallel Bus Mask
(P0.16..23)
#define ST7920_PSB PIN(28) // Parallel Select (0 Serial Mode, 1 = 8/4-Bit Parallel Bus Mode) (P0.28)
#define ST7920_RW PIN(29) // RW (P0.29)
#define ST7920_DI PIN(6) // RS (P0.6)
#define ST7920_E PIN(30) // Enable (P0.30)
#define ST7920_RST PIN(25) // Reset (P0.25)

/* General Definitions */
#define ST7920_BUSIN 0 // Parallel Bus as Input
#define ST7920_BUSOUT 1 // Parallel Bus as Output
#define ST7920_BUSY 0x80 // Busy Flag

/* Basic ST7920 Commands */
#define ST7920_CMD_CLEAR 0x01 // Clear Display
#define ST7920_CMD_HOME 0x02 // Move Cursor Home
#define ST7920_CMD_EM 0x04 // Entry Mode Base
#define ST7920_CMD_EM_INCRR 0x02 // Increment Cursor Right
#define ST7920_CMD_EM_INCRL 0x00 // Increment Cursor Left
#define ST7920_CMD_EM_SHFTR 0x03 // Shift Display Right
#define ST7920_CMD_EM_SHFTL 0x01 // Shift Display Left
#define ST7920_CMD_DC 0x08 // Display Control
#define ST7920_CMD_DC_DISPON 0x04 // Display On
#define ST7920_CMD_DC_CURON 0x02 // Cursor On
#define ST7920_CMD_DC_BLNKON 0x01 // Blink On
#define ST7920_CMD_FNC 0x20 // Function Set
#define ST7920_CMD_FNC_DL8 0x10 // 8-Bit Interface
#define ST7920_CMD_FNC_DL4 0x00 // 4-Bit Interface
#define ST7920_CMD_FNC_EXTINS 0x04 // Extended Instruction Set
#define ST7920_CMD_FNC_BASINS 0x00 // Basic Instruction Set
#define ST7920_CMD_CGRAM_ADDR 0x40 // Set CGRAM Address
#define ST7920_CMD_DDRAM_ADDR 0x80 // Set DDRAM Address

/* Extended ST7920 Commands */
#define ST7920_ECMD_GFXDISPON 0x36 // Ext. Display Control (8-
bit, Extended Instructions, GFX Display On)

// Method Prototypes
void st7920_init(void);
void st7920_clear(void);
void st7920_printf(U8 *text);
void st7920_setPixel(U8 x, U8 y);

#endif

ST7920.C
=============================================================
#include "lpc214x.h"
#include "st7920.h"
#include "delay.h"

void st7920_strobeEnable(void)
{
GPIO0_IOSET = ST7920_E;
DelayMS(15);
GPIO0_IOCLR = ST7920_E;
}

void st7920_setBusDir(U8 dir)
{
if (dir==ST7920_BUSOUT)
{
GPIO0_IODIR |= (ST7920_BUSALL);
}
else
{
GPIO0_IODIR &= ~(ST7920_BUSALL);
}
}

void st7920_waitWhileBusy()
{
U8 rdata;

st7920_setBusDir(ST7920_BUSIN);

// Read busy flag
GPIO0_IOCLR = ST7920_DI; /* Clear RS */
GPIO0_IOSET = ST7920_RW; /* Set Read Mode */

st7920_strobeEnable();

// Loop until no longer busy
while ((rdata & 0x7F) == ST7920_BUSY)
{
rdata = (unsigned char)(GPIO0_IOPIN >> ST7920_BUSPIN0);
}

st7920_setBusDir(ST7920_BUSOUT);
GPIO0_IOCLR = (U32)(ST7920_BUSALL); /* Set all pins low */
GPIO0_IOCLR = ST7920_DI;
GPIO0_IOCLR = ST7920_RW; /* Read mode */
}

void st7920_data(U8 data)
{
st7920_setBusDir(ST7920_BUSOUT);

GPIO0_IOSET = ST7920_DI;
GPIO0_IOCLR = ST7920_RW;

GPIO0_IOSET = ((U32)data< st7920_strobeEnable();

GPIO0_IOSET = ST7920_RW;
GPIO0_IOCLR = (U32)(ST7920_BUSALL);
GPIO0_IOCLR = ST7920_DI;

DelayMS(2);
}

void st7920_command(U8 command)
{
st7920_setBusDir(ST7920_BUSOUT);

st7920_waitWhileBusy();
GPIO0_IOCLR = ST7920_DI;
GPIO0_IOCLR = ST7920_RW;
GPIO0_IOSET = ((U32)command< st7920_strobeEnable();

GPIO0_IOSET = ST7920_RW;
GPIO0_IOCLR = (U32)(ST7920_BUSALL);
GPIO0_IOCLR = ST7920_DI;

DelayMS(2);
}

// 0x80 0x81 0x82 0x83 0x84 0x85 0x86 0x87
// 0x90 0x91 0x92 0x93 0x94 0x95 0x96 0x97
// 0x88 0x89 0x8a 0x8b 0x8c 0x8d 0x8e 0x8f
// 0x98 0x99 0x9a 0x9b 0x9c 0x9d 0x9e 0x9f
void st7920_setGDRamAddress(U8 addr)
{
st7920_command(addr);
}

/* Public Methods
********************************************************** */
void st7920_init(void)
{
// Set pin to default state
U32 mask = (U32)(ST7920_RST | ST7920_PSB | ST7920_RW | ST7920_DI |
ST7920_E);
GPIO0_IOCLR = mask; // Clear all pins
GPIO0_IODIR |= mask; // Set all pins for output
GPIO0_IOSET = ST7920_PSB; // Set PSB High (0 = Serial Mode,
1 = 8/4-Bit Parallel Bus Mode)

// Wait until the LCD is not busy
st7920_waitWhileBusy();

// Toggle Reset
GPIO0_IOSET = ST7920_RST;
DelayMS(21);
GPIO0_IOCLR = ST7920_RST;
DelayMS(20);
GPIO0_IOSET = ST7920_RST;
DelayMS(20);

// Instantiate intialisation commands (Assigned here for debug
purposes. Remove variables in a production environment.)
U8 functionSetBas = (ST7920_CMD_FNC | ST7920_CMD_FNC_DL8 |
ST7920_CMD_FNC_BASINS); // (8-bit, Basic Instruction Set)
U8 functionSetExt = (ST7920_CMD_FNC | ST7920_CMD_FNC_DL8 |
ST7920_CMD_FNC_EXTINS); // (8-bit, Extended Instruction Set)
U8 displayCmd = (ST7920_CMD_DC | ST7920_CMD_DC_DISPON |
ST7920_CMD_DC_BLNKON); // (Display On, Blink On, Cursor OFF)
U8 entryMode = (ST7920_CMD_EM |
ST7920_CMD_EM_INCRR); // (Increment cursor
right, no shift)

// Send initialisation command sequence
st7920_command(functionSetBas); // Basic Function Set
st7920_command(functionSetBas); // Repeat Function Set
st7920_command(displayCmd); // Display
st7920_command(ST7920_CMD_CLEAR); // Clear Display
st7920_command(entryMode); // Set Entry Mode
st7920_command(functionSetExt); // Extended Function Set
st7920_command(ST7920_ECMD_GFXDISPON); // Graphic Display On

st7920_setPixel(1,1);
}

void st7920_clear(void)
{
st7920_command(ST7920_CMD_CLEAR); // Clear Display
}

void st7920_printf(U8 *text)
{
while(*text != 0)
{
st7920_data(*text);
text++;
}
}

void st7920_setPixel(U8 x, U8 y)
{
st7920_setGDRamAddress(0x80);

// ToDo: How to set individual pixels?
}

l... napisa(a):
ST7920.H

[...]
// ToDo: How to set individual pixels?
}

Try:
void st7920_setPixel(U8 x, U8 y)
{
U16 val;

val = 0x8000 >> (x & 0xF);
st7920_command(ST7920_CMD_DDRAM_ADDR + y);
st7920_command(ST7920_CMD_DDRAM_ADDR + (x >> 4));
st7920_command(val >> 8);
st7920_command(val & 0xFF);
}
Albert

Albert:

Thanks for the reply. I tried the supplied code, but don't seem to have any luck with it.
Whenever I send 'val >> 8' the screen clears as if I was in basic (text-only) mode, even
though I'm in extended mode. I've come up with the following code to at least put move
to the correct 16x16 block by means of the GDRAM Address ... I'm just missing what
command I need to send to set, say, pixel '8, 3' in the current 16x16 block. The code can
be heavily optimised later ... for the moment I just want to get it working, and understand
how it works:

void st7920_drawPixel(U8 x, U8 y, U8 color)
{
U8 xBlock, yBlock;
U8 xPixel, yPixel;

// Set horizontal GDRam position (0-7)
if (x > 15) { xBlock = x / 16; }
else { xBlock = 0; }

// Set vertical GDRam position (0-3)
if (y > 15) { yBlock = y / 16; }
else { yBlock = 0; }

// Move to appropriate GDRam block
st7920_setGDRamAddress(xBlock, yBlock);

// Determine 16x16 block address of the requested pixel
if (x < 16) { xPixel = x; }
else { xPixel = x - (xBlock * 16); }
if (y < 16) { yPixel = y; }
else { yPixel = y - (yBlock * 16); }

// ToDo: How to set individual pixels using xPixel and yPixel 16x16 position?
}

Dnia 2008-11-25, wto o godzinie 13:43 +0000, Kevin Townsend pisze:
> Albert:
>
> Thanks for the reply. I tried the supplied code, but don't seem to
> have any luck with it.
> Whenever I send 'val >> 8' the screen clears as if I was in basic
Yes, my mistake.
Correct is:
...
st7920_data(val >> 8);
st7920_data(val & 0xFF);
...

See also:
http://www.crystalfontz.com/forum/showthread.php?tV05

Albert

Albert:

That's a lot closer to what I was hoping for, thanks. However, whenever I set one 'column'
of pixels, it seems to clear all the previous ones in the same 16x16 block. For example, I
would expect the code in 'main()' below to product a solid black block 128x8 pixels in size
along the top of the screen. Instead, I have 15 blank pixels, 1 black pixel, 15 blank, 1
black, repeated for each 16x16 block, 8 pixels high:

................................................ (etc.)
................................................ (etc.)
................................................ (etc.)
................................................ (etc.)
................................................ (etc.)
................................................ (etc.)
................................................ (etc.)
................................................ (etc.)

I see that the pixels are all set as the program executes, but they mysteriously get cleared
as pixels on the following column are set. Any idea why this might be?

int main(void)
{
st7920_init();
for (int x = -1; x < 127; x++)
{
for (int y = -1; y < 7; y++)
{
st7920_setPixel(x,y);
}
}

while (1);
}

--------------

void st7920_drawPixel(U8 x, U8 y)
{
U16 val;

val = 0x8000 >> (x & 0xF);
st7920_command(ST7920_CMD_DDRAM_ADDR + y);
st7920_command(ST7920_CMD_DDRAM_ADDR + (x >> 4));
st7920_data(val >> 8);
st7920_data(val & 0xFF);
}

In any case, thanks for your help. It's already a big lead in the right direction. I have really
been scratching my head with the datasheet trying to figure out what I was missing.

Kevin.

Am Mittwoch, 26. November 2008 schrieb Kevin Townsend:
> Albert:
>
> That's a lot closer to what I was hoping for, thanks. However, whenever I
> set one 'column' of pixels, it seems to clear all the previous ones in the
> same 16x16 block. For example, I would expect the code in 'main()' below to
> product a solid black block 128x8 pixels in size along the top of the
> screen. Instead, I have 15 blank pixels, 1 black pixel, 15 blank, 1 black,
> repeated for each 16x16 block, 8 pixels high:
>
> ................................................ (etc.)
> ................................................ (etc.)
> ................................................ (etc.)
> ................................................ (etc.)
> ................................................ (etc.)
> ................................................ (etc.)
> ................................................ (etc.)
> ................................................ (etc.)
>
> I see that the pixels are all set as the program executes, but they
> mysteriously get cleared as pixels on the following column are set. Any
> idea why this might be?
>

Hello Kevin,

you access the display's memory directly, so it does know of any "setpixel" or
whatever commands. Therefore, it does not know how to combine the data in the
memory already, with the data you just send. So it just overwrites the data.

To set an individual pixel anywhere, without affecting already set pixels, you
need to read the display ram first. So, set the address, read the old content
from the display, combine with the new data, write back to the display. Many
displays offer a special mode where a read does not advance the display's
address-counter, but a write will do. That would save you an "set address"
command inbetween.

Lets assume a stripe (pixel row) is byte-wide, and the display is 128x64
pixels. A simple function to read-modify-write the display could look like
this "pseudo-code":

#define WIDTH 128
#define HEIGHT 64

setPixel(pos_x, pos_y, colour)
{
// row_data holds a stripe, pix_pos is the position of the bit in the
stripe.
unsigned char row_data, pix_pos;

setDisplayAddress( (x >> 3) + (y * (WIDTH >> 3)) ); // >>3 equals /8

row_data = readDisplayRam(); // read stripe from the display

pix_pos = x % 8; //calculate the new pixel position inside a stripe

if(colour) // if != 0, draw pixel
{
row_data |= (0x80 >> pix_pos); // combine old data and new pixel using OR
}
else
{
row_data &= ~(0x80 >> pix_pos); // combine using AND with the inverted
new-pixel-stripe
}

setDisplayAddress( (x >> 3) + (y * (WIDTH >> 3)) ); // set address again

writeDisplayData(row_data); // write out the new stripe
}
Here i assume that inside a stripe pixel 0 is on the left. If it is different,
just do a "0x01 << pix_pos" instead. If you want to invert a pixel on the
display, combine using XOR "row_data ^= (0x80 >> pix_pos);", etc, etc...

The pseudo-code is not meant to be functional, just an example to show how it
should be done. It sets the display address twice, that could be avoided
depending on the display.

An alternative method would be to have a display-buffer in local RAM and work
on that, if you can afford it. Then just send the modified bytes from the
internal buffer to the display. That could allow you a flicker-free update of
the whole display, at the expense using more RAM resources on the chip. In
any case, the method would be the same when working on an internal buffer:
read-modify-write.

Greetings,

Chris

Kevin Townsend wrote:
> st7920_data(val >> 8);
> st7920_data(val & 0xFF);

This doesn't only set pixels on, it *writes* them 16 pixels at once. So
when value of val is 0x0001, then you get 15 leftmost pixels cleared and
one rightmost pixel set on.

When your code writes 16 times into the same address, the effect of the
last write remains. And that happens to be always 0x0001 which you see
on your display.

I glanced over the datasheet and didn't see any method to touch only one
pixel at a time.

--

Timo

Chris:

Thanks for the lengthy reply. After a bit of thought, that's what I assummed was happening,
but it's nice to have a clear explanation of how to resolve it. I'll adjust the code accordingly.
Hopefully, once I can add some support for fonts, etc., I can optimise the code a bit and
upload it here so other's can avoid some of the same problems.

Kevin.
Am Mittwoch, 26. November 2008 schrieb Kevin Townsend:
> Chris:
>
> Thanks for the lengthy reply. After a bit of thought, that's what I
> assummed was happening, but it's nice to have a clear explanation of how to
> resolve it. I'll adjust the code accordingly. Hopefully, once I can add
> some support for fonts, etc., I can optimise the code a bit and upload it
> here so other's can avoid some of the same problems.
>
> Kevin.

Hello Kevin,

quite some while ago i did some code for an LPC2220 based remote-control. It
also contains LCD routines for drawing, and the display also need's the
read-copy-modify method. If you want you can take a look at it. You can find
the code is svn, at svn://svn.mamalala.org/boop id you use a svn client, or
by web-interface at http://svn.mamalala.org/ and then the "boop" project.
There you will find a trunk/display/ folder which contains the code.

It has functions to draw lines, circles, rectangles, fonts, ....

It's far from perfect, but works for me. Maybe that will give you some ideas
for your stuff. Font-Handling is a bit uncommon in that code. If you need
help with that stuff, drop me a note. i'm not that good at documenting code,
as you can see ;)

Greetings,

Chris