Author Topic: Using a Character LCD in the uTasker Project  (Read 8664 times)

Offline mark

  • Global Moderator
  • Hero Member
  • *****
  • Posts: 3236
    • View Profile
    • uTasker
Using a Character LCD in the uTasker Project
« on: October 10, 2007, 12:53:12 PM »
Hi All

Often some form of simple text interface is require (coffee machines, credit card terminals, process control equipment) which can be fulfilled using a charcacter LCD. The uTasker project support such an interface and also enables complete testing in the uTasker simulator.

This post discusses the use of the module.


Using a character LCD in the uTasker project

The uTasker project supports character displays which conform to the HD44780 standard interface (which is probably just about all). The display can be from the smallest type I know (1 x 8 ) up to 4 x 40 and its interfaces using either an 8 bit or 4 bit interface. The 8 bit interface is a bit faster since all read/writes to/from the display can be made in one access cycle, whereas the 4 bit interface saves bus/port lines since the data interface is less wide - accesses require two cycles in this mode and so it is a bit slower. Generally the access speed is not a big issue when interfacing to this type of LCD so 4 bit mode is often preferred.

The uTasker simulator aids in developments since it includes a configurable simulated LCD. The demo project shows an example of initialising and controlling the LCD. This may not be the best method for a particular application but is quite easy to use and reliable so represents a good starting point.

Connecting an LCD to a processor

The LCDs have an interface which follows the following pattern. The actual connector type can vary widely, being a single or double row of pins. I will describe one which I often use which is easily translated to and other type as well.

Pin 1  Vss  (Ground for the power supply)
Pin 2  VDD  (+ve voltage power supply, eg. 5V or 3V3)
Pin 3  VO   (Contrast control voltage typically 0..1.5V when powered by 5V or a slightly negative value when powered by 3V3)
Pin 4  RS   (Data/Instruction select)
Pin 5  R/W  (Read/Write)
Pin 6  E    (Enable signal)
Pin 7  DB0  (Data Bus 0 - don't connect for 4 bit mode)
Pin 8  DB1  (Data Bus 1 - don't connect for 4 bit mode)
Pin 9  DB2  (Data Bus 2 - don't connect for 4 bit mode)
Pin 10 DB3  (Data Bus 3 - don't connect for 4 bit mode)
Pin 11 DB4  (Data Bus 4 - always connected)
Pin 12 DB5  (Data Bus 5 - always connected)
Pin 13 DB6  (Data Bus 6 - always connected)
Pin 14 DB7  (Data Bus 7 - always connected)
Pin 15 LEDA (Backlight LED - if available - Anode - connect positive backlight drive voltage to this - 5V with current limiting resistor recommended)
Pin 16 LEDK (Backlight LED - if available - Kathode - connect negative backlight drive voltage to this)

If connecting a 5V LCD to a 3V3 processor it may be necessary to use a level shifter. Note also that the processor not only drives the data bus but can also read back data. In particular DB7 is read back to check whether the LCD is busy so this should also be supported. If your processor is 5V tolerant this is of course no problem at all.


Trying the LCD out in the uTasker simulator.
If you want to test drive the LCD without actually having to connect one to a target the simulator comes in very handly. It can also be used for developing applications which require an LCD to display text (and a limited amount of graphics).
In config.h the LCD demo can be activated and the LCD type configured:

Code: [Select]
#define SUPPORT_LCD                                                      // enable a task for interfacing to an LCD
#ifdef SUPPORT_LCD
    #define LCD_LINES              2                                     // use 2 x 16 LCD
    #define LCD_CHARACTERS         16                                    // Options are 1:8 / 1:16 / 1:20 / 1:24 / 1:40 / 2:x / 4:x
    #define LCD_ON_COLOUR          (COLORREF)RGB(60,220,60)              // RGB colour of LCD when backlight is on
    #define LCD_OFF_COLOUR         (COLORREF)RGB(70,160,0)               // RGB colour of LCD when backlight is off
    #define LCD_PARTNER_TASK       TASK_APPLICATION
#endif

Various LCDs sizes can be selected and the text and background colour configured. Real LCDs have limited colour choice so best choose realistic colour settings to avoid disappointment..! The following description is valid when USE_TIME_SERVER has not also been activated (otherwise the LCD will display the present time).

The result is an LCD with the defined characteristics displayed when the simulator is running, showing "Hi uTasker!" continuously moving left and right.

Setting the SW interface to the target
In order to actually control an LCD the processor must be connected to it via the control lines as specified above. Each target in the uTasker project has an LCD setup to allow it to operate with a standard demo board and to easily be adapted to almost any other board. The set up is defined in the header file app_hw_xxx.h (wher xxx is the target processor in question). Here is an example for the M5223X project.

Code: [Select]
// LCD interface: Backlight control Port AS bit 0 : Data bus (4 Bit) Port TA 0..3 : RS Port AS bit 1, RW Port AS bit 2, E Port AS bit 3
//
typedef unsigned char LCD_BUS_PORT_SIZE;                                 // we use 8 bit ports
typedef unsigned char LCD_CONTROL_PORT_SIZE;
//#define LCD_BUS_8BIT                                                   // data bus in 8 bit mode
#define LCD_BUS_4BIT                                                     // data bus in 4 bit mode

#ifdef LCD_BUS_8BIT
    #define LCD_BUS_MASK         0xff
    #define DATA_SHIFT_RIGHT     0                                       // no shift required to bring data into position
    #define DATA_SHIFT_LEFT      0
#else
    #define LCD_BUS_MASK         0x0f
    #define DATA_SHIFT_RIGHT     4                                       // nibble shift down required to bring data into position
    #define DATA_SHIFT_LEFT      0
#endif

#define O_CONTROL_RS             PORT_AS_BIT1
#define O_WRITE_READ             PORT_AS_BIT2
#define O_CONTROL_EN             PORT_AS_BIT3
#define O_LCD_BACKLIGHT          PORT_AS_BIT0

#define O_CONTROL_LINES          (O_CONTROL_RS | O_WRITE_READ | O_CONTROL_EN)
#define IO_BUS_PORT_DDR          DDRTA
#define O_CONTROL_PORT_DDR       DDRAS
#define IO_BUS_PORT_DAT          PORTTA
#define IO_BUS_PORT_DAT_IN       PORTIN_SETTA
#define O_CONTROL_PORT_DAT       PORTAS

// Drive the control lines R/W + LCD Backlight '1', RS + E '0'  and the data lines with all high impedance at start up
#define INITIALISE_LCD_CONTROL_LINES()       IO_BUS_PORT_DDR = 0; IO_BUS_PORT_DDR = 0; \
                                             O_CONTROL_PORT_DDR &= ~(O_CONTROL_LINES | O_LCD_BACKLIGHT); O_CONTROL_PORT_DAT &= ~(O_CONTROL_LINES | O_LCD_BACKLIGHT); O_CONTROL_PORT_DAT |= (O_LCD_BACKLIGHT | O_WRITE_READ); \
                                             O_CONTROL_PORT_DDR |= (O_CONTROL_LINES | O_LCD_BACKLIGHT);


#define LCD_DRIVE_DATA()          IO_BUS_PORT_DDR |= LCD_BUS_MASK;    IO_BUS_PORT_DDR |= LCD_BUS_MASK;
                                  // ensure data bus outputs (delay) by repetitions according to processor speed

#define CLOCK_EN_HIGH()           O_CONTROL_PORT_DAT |= (O_CONTROL_EN); O_CONTROL_PORT_DAT |= (O_CONTROL_EN);
                                  // clock EN to high state - repeat to slow down (delay)

#define DELAY_ENABLE_CLOCK_HIGH() O_CONTROL_PORT_DAT &= ~(O_CONTROL_EN);


I must admit that this is not the simplest thing to configure but with a little understanding it should be possible to adapt to almost any other configuration, as long as a few rules are adhered to.
First of all, the mapping of the LCD control lines to real ports has to be defined. The above example is set to operate in 4 bit bus mode and controls the LCD via port AS. The bus interface is required to be on a single port and the bits are assumed to be next to each other in an accending row. In the example port TA is used and the 4 data bits are situated from TA0..TA3.
The control lines are assumed to be all on a single port but their position on the port is not important.
The routines actually controlling accesses do so using macros (like LCD_DRIVE_DATA()) where the exact method of achieving this for a particular processor is defined above.

Since modern processors are quite fast (although port bit banging can may not necessarily be as fast as expected) the actual routines may be too fast for the LCD interface. Depending on the processor and its speed setting, it is often necessary to slow down accesses by introducing 'wait-states' - this can be performed by repeating instructions in the macros as follows:

Code: [Select]
#define CLOCK_EN_HIGH()           O_CONTROL_PORT_DAT |= (O_CONTROL_EN); O_CONTROL_PORT_DAT |= (O_CONTROL_EN); \
                                  O_CONTROL_PORT_DAT |= (O_CONTROL_EN); \
                                  O_CONTROL_PORT_DAT |= (O_CONTROL_EN); \
                                  O_CONTROL_PORT_DAT |= (O_CONTROL_EN);

It may be a good idea to once tune the accesses by looking at the accesses with an oscilloscope (or logic analyser) to ensure that the accesses are not too fast (check the times in the data chip for the LCD used) but also not unnecessarily slow due to too many wait-states.
As a reference, the M5223X running at 60MHz doesn't need any wait states to operate with a display in 8 bit mode. In 4 bit mode the inter-cycle gap (when it does two back-to-back nibble reads) is at the limit so and so DELAY_ENABLE_CLOCK_HIGH() is used to ensure that this can not become an issue.


Initialisation procedure
When the define SUPPORT_LCD is activated in the uTasker demo project an LCD task is added to the system (see lcd.c). On start up this task configures the ports used by the LCD interface using the macro INITIALISE_LCD_CONTROL_LINES().

It then works through an LCD initialisation sequence:
1. The task is started by the application task afer a delay of 100ms to ensure that the LCD is ready to respond to data.
2. The first command sent to the LCD is the INIT_FUNCTION_SET command (always in 4 bit mode after power up).
3. After a delay of at least 4.1ms (one system tick is used) the command is repeated again.
4. After a delay of at least 100us (one system tick is used) the command is repeated again before finally issuing the INIT_FUNCTION_SET_MODE. The task is set to polling mode where it ready the busy bit of the LCD to see when the next command can be performed.
5. The INIT_FUNCTION_SET_MODE is repeated with final configuration settings.
6. Still operating in polling mode (meaning that the last instruction is being polled to identify when it has completed), the next command is DISPLAY_OFF_NO_CURSOR - to ensure that no cursor is being displayed.
7. Followed by CLEAR_DISPLAY, to ensure nothing is visible and the (invisible) cursor is at the top left.
8. Followed by DISPLAY_ON_NO_CURSOR so that any text written will actually be seen.

Inter-task details
The demo solution involves communication between the application task and the display task. The application task determines what is displayed and the LCD task just does what it is told. It starts by informing the application task that the initialisation has completed by sending an event E_LCD_INITIALISED.
The application task then sends a welcome text to the LCD task which writes it to the display.
When the LCD task writes to the display it does so from a text/command queue. It operates in polling mode until the commands in the queue have been completely written. It sets itself to the sleep state (to avoid unnecessary polling) and informs the application task (using event E_LCD_READY) that the previous command has completed. The application task can then send the next text or commands.
This solution this requires the application task to wait for an acknowledge from the previous commands before continuing. Individual commands/text sequences are queued in the LCD task itself.

A closer look at the hardware set up
As long as the control lines are on one single port the configuration involves simply masking out the port bits as required. Should they have to be spread over different ports the standard routine is no longer compatible and so will have to be adapted to suit. Therefore try to plan these on the same port to save any extra work.
The data lines are very simple if working in 8 bit mode on an 8 bit port.
    #define LCD_BUS_MASK         0xff
    #define DATA_SHIFT_RIGHT     0                                       // no shift required to bring data into position
    #define DATA_SHIFT_LEFT      0
This means that bytes to be written are passed to the write routine and require no manipulation to be written to the bus (PORT = byte_of_data). Also when reading it is just as simple (read_byte = PORT).
If the port width is not 8 bit bit 16 or 32, it is equally simple as long as the data bus is aligned with the lowest 8 bits.
Should the port width be 32 for example and the bus be connected to port bits 5..12 then the set up will be
    #define LCD_BUS_MASK         0x3fc0
    #define DATA_SHIFT_RIGHT     5                                       // no shift required to bring data into position
    #define DATA_SHIFT_LEFT      0

This causes the correct port bits to be masked and then the read value to be shiften into position. When reading form the bus, the inverted process takes place.

Working in 4 bit mode is a little tricker. It should be remembered that it is the higher 4 bit nibble which is important, so if the lower 4 bits of a port are connected to the LCD lines 7..4, the following setting is required:

    #define LCD_BUS_MASK         0x0f
    #define DATA_SHIFT_RIGHT     4                                       // nibble shift down required to bring data into position
    #define DATA_SHIFT_LEFT      0

When writing, the byte is passed as two nibbles (each positioned to the higher nibble position). This explains why a right shift of 4 bits is required to position it correctly in the byte to be physically written. When reading, the nibble is read at the lower nibble position and so the opposite (left shift) is performed to reconstruct the complete byte (two read cycles).
It is assumed that the data bus lines are all next to each other on a port and that the sequence is the same as the LCS (0,1,2,3 /0,1,2,3 and not 4,3,2,1...). If this is not the case, more manipulation in the code is required to get it right, so a simpel physical wiring will also keep the coding simple.

Note that reading from the LCD is important to ensure that following write cycles are not performed while the LCD is still busy. If this is not respected, text and commands will probably not all be correctly interpreted which results in either the display not operating or doing so only unreliably.





Comments are welcome. Give it a try if you would like to control an LCD in your project.

Regards

Mark