Path: Home => AVR-EN => Apps => LCD on an AVR   Diese Seite in Deutsch: Flag DE Logo
LCD small AVR applications

LCD on an AVR in assembler

A LCD on an AVR

lcd.inc at work The ultimate control software for a LCD on an AVR works in any case and can be configured to fit to any needs.
  1. Hardware
  2. Configuration
  3. Software
Changes:
  1. Convenient: It is now possible to define the clock separate. This is useful in cases where the controller's clock is altered by software, e.g. by writing to CLKPR. If the constant LcdClock is defined, it should hold the effective clock rate by which the LCD will work. If undefined, it sets LcdClock to the constant clock. So, older formulations need no change.
  2. A serious error with the four-bit mode occurred when using the nibble, that is not used for the LCD (direction bits go to zero). The error is corrected.
  3. Not necessary but more correct: I added delays for changes of the RS and RW control lines. The delays are clock-dependent, so the LCD works correct even with very high clock rates. All delays are packed into macros to shorten the source code.
Download those pages as one single PDF document (50 pages, 1,6 MB).

1 Hardware

1.1 LCD connections and operating modes of the LCD

LCD connections LCDs have three control input lines and eight bidirectional data bus input/output lines.

The three control inputs have the following function:
  1. E input (Enable): This input is drawn from zero to one for one microsecond and cleared after that. Each write or read cycle to/from the LCD one such pulse is necessary (in four bit mode: two).
  2. RS input (Register Select): If this input is low during read/write, a control command is wrote/read to/from the LCD. If high, a data write or read is performed.
  3. RW input (Read/Write): If high, data is read from the LCD and placed on the data bus. In that case the LCD drives the data bus. A low on this input writes data or control bytes to the LCD and the controller drives the data bus.
The data bus of the LCD can be operated in 8 bit and in 4 bit mode. In 8 bit mode each write/read operation transfers 8 bits. Switching the LCD from 8 to 4 bit mode is done by a control command named function set (see below during init). If that command has been sent, further communication between the controller and the LCD only uses the upper four bits of its data bus (D4 to D7). Each write and read operation then requires two pulses on the E input: first the upper nibble is transferred, then the lower nibble. The controller can use its upper or lower half of an I/O port.

1.2 Busy and wait mode

If the RW input is fixed to ground, no data read operations can be made. In that case the controller has to ensure with wait cycles that the LCD has enough time to perform command and data operations and that the next command and data is only send later.

The configuration, if the busy flag of the LCD shall be read or the controller shall perform wait time loops instead is done with the constant LcdWait: if set to one, wait cycles are included, if set to zero the LCD's busy flag is read and the next command is written or read when this bit is low.

The configuration of the Wait-/Busy-Mode also has influence on the LCD-RW pin, see below for more details.

1.3 Connecting a LCD to an AVR

A LCD can be connected to an AVR in six different ways. Those results from: Those variations look like that:
  1. With eight data bits and with the Read/Write control pin R/W:

    LCD 8 bit with busy For this operating mode 11 I/O pins of the AVR are required. Operation of the data bus is bidirectional. Prior to writing to the LCD the state of the busy flag is read and it is waited until this is clear.
  2. With eight data bits and without Read/Write control pin R/W:

    LCD 8 bit with wait The control pin R/W is permanently tied to GND. This operating mode requires 10 I/O pins. Data bus is permanently driven by the AVR. Following each write operation a specific time is waited until the LCD has performed the task.
  3. With four data bits and the Read/Write control pin R/W, with the Data bus on the upper nibble of the AVR I/O port:

    LCD 4 bit with busy on the upper nibble This operating mode requires 7 port pins of the AVR. Operation of the data bus is bidirectional. Prior to any write operation to the LCD the busy flag is read until this is clear.
  4. With four data bits and the Read/Write control pin R/W, the data bus is connected to the lower nibble of the AVR port:

    LCD 4 bit with busy on the lower nibble This operating mode requires 7 port pins of the AVR. Bus operation is bidirectional. Prior to write operations the busy flag of the AVR is read and waited for.
  5. With four data bits and without Read/Write control pin R/W, the data bus on the upper nibble of the AVR port:

    LCD 4 bit with wait on the upper nibble This requires 6 port pins of the AVR. Operation of the data bus is unidirectional. Following each write operation a specific wait cycle for LCD completion.
  6. With four data bits without Read/Write control pin R/W, the data bus on the lower nibble of the AVR port:

    LCD 4 bit with wait on the lower nibble For this mode of operation 6 portpins of the AVR are necessary. Operation of the data bus is unidirectional. Following each write operation to the LCD is a specific wait cycle performed.
The decision which of these options is selected can consider the following: Whatever you choose, this software fits it all.

1.4 Control- and data commands of LCDs

1.4.1 Control commands

The following commands can be send to the LCD, if during write
  1. the RS input is low,
  2. the RW input is also low, and
  3. the E input is held high for at least one microsecond and low after that.
One further hint: each data book for a LCD type names different execution times for commands and character write. That comes from the fact that different controllers and different clock settings are used.

Bits with an x in the table are ignored (can be 0 or 1). Note that the line order of the table has nothing to do with the order that those command will be used during init (see 2 Configuration)!
CommandData byteAbbreviationsTime
D7D6D5D4D3D2D1D0
Clear display 00000001 - 1.52/1.64 ms
Reset 0000001x - 1.52/1.64 ms
Input mode 000001I/DS I/D: Cursor direction 0=left, 1=right
S: Shift display 0=no, 1=yes
37/40 µs
Display on/off 00001DCB D: Display 0=Off, 1=On
C: Cursor 0=Off 1=On
B: Blink cursor 0=Off 1=On
37/40 µs
Cursor shift 0001S/CR/Lxx S/C: 0=Shift cursor, 1=Shift display
R/L: 0=left, 1=right
37/40 µs
Function set 001DLNFxx DL: Data bus 0=4 bit, 1=8 bit
N: Lines 0=1 line, 1=2 or 4 lines
F: 0=Font 5x8 1=Font 5*10
37/40 µs
Set character generator RAM address 01a5a4a3a2a1a0 a5:a3 Character 0 to 7
a2:a0 Line 0 to 7
37/40 µs
Set display RAM address 1a6a5a4a3a2a1a0 a6:a0 Address, 0x80=line 1, 0xC0=line 2
0x80+columns/line=line 3, 0xC0+columns/line=line 4
37/40 µs

1.4.2 Read commands

If the RS input is low and the RW input is high, the following can be read from the data bus of the LCD:
ProcedureData byteAbbreviationsTime
D7D6D5D4D3D2D1D0
Read busy flag and
Display RAM address
BFa6a5a4a3a2a1a0 BF: Busy flag 0=ready, 1=busy
a6:a0: Display address
E: 1 µs

1.4.3 Write display and character generator data

With the RS input high and

2 Configuration of the LCD

After power is supplied to the LCD and prior to using the LCD a configuration procedure has to be absolved. Several properties have to be selected by writing control commands to the LCD (operating modes, numbver of lines, on and off, cursor settings, etc. This chapter demonstrates how this can be made in assembler.

Please note that the assembler include and the examples use .IF directives for different configurations to not having to write numerous different versions of the software. The software lcd.inc uses those directives very extensively. Those are only assembled by more modern assemblers, such as ATMEL's assembler version 2 or with gavrasm. Older assemblers throw error messages instead.

First it is described how to write control and data bytes to the LCD with an 2.1 Enable pulse) with the necessary duration. Then consultation of the 2.2 Busy flag and waiting for the LCD to be ready is demonstrated, both for an 2.2.1 8 bit data bus and for a 2.2.2 4 bit data bus. For the operation of the LCD without busy flag the necessary 2.3 Wait routines are shown. 2.4.5 4 bit operation is demonstrated. And in the chapter 2.5 Others further operations are described that can be made with an LCD.

2.1 Duration of the Enable pulse

The Enable pulse must have at least a duration of one microsecond to be compatible with the specification of the LCD.

The Enable pulse can be generated in assembler by the instructions

.equ pLcdCEO = PORTB ; Output port, where the LCD control bit E resides
.equ bLcdCEO = PORTB0 ; Output pin of the LCD control bit E
LcdPulseE:
  sbi pLcdCEO,bLcdCEO ; Set bit bLcdCEO in the LCD control port
  cbi pLcdCEO,bLcdCEO ; Clear bit bLcdCEO

If the LCD shall be read from, the following has to be performed:

LcdInE:
  sbi pLcdCEO,bLcdCEO ; Set portbit bLcdCEO in LCD control port
  nop ; Wait for one microsecond
  in R16,pLcdDI ; Read data bus to register R16
  cbi pLcdCEO,bLcdCEO ; Clear portbit bLcdCEO

This only works reliably if your clock frequency is 1 MHz or below. If you work with 4 MHz, you might have luck and your controller is faster than its specification. But if your controller works at e.g. 16 MHz this would be too fast for the LCD. At elevated clock frequencies additional wait cycles have to be added (with the frequency in the constant clock):

.equ LcdClock = 3276800 ; clock frequency
LcdPulseE:
  sbi pLcdCEO,bLcdCEO ; Set bit bLcdCEO in LCD control port
  .if LcdClock>1000000
    nop
    .endif
  .if LcdClock>2000000
    nop
    .endif
  .if LcdClock>3000000
    nop
    .endif
  ; ... etc. and on and on ...
  cbi pLcdCEO,bLcdCEO ; Clear bit bLcdCEO

This ties the Enable pulse to the clock frequency and provides correct execution delay. And it works correct at whatever frequency.

Please define LcdClock within the LCD header constants. If undefined, lcd.inc looks after a constant "clock". If also undefined an error will occur. If defined, LcdClock will take over the value of clock. If the controller's clock changes more than one time, replace the ".equ LcdClock = n", use .set instead of .equ lcd.inc.

2.2 Wait for the busy flag

If the RW pin has been tied to the AVR (and not to GND), the busy flag can be read from the LCD and further write operations can be delayed until this flag reads zero.

Waiting for the busy flag is slightly different in 2.2.1 8 bit mode and in 2.2.2 4 bit mode.

2.2.1 Busy flag in 8 bit mode

Wait for busy, 8 bit The following routine waits in 8 bit mode for a cleared busy flag. Because the routine uses PUSH and POP to save and restore register content, the stack has to be setup.

; Wait until the busy flag of the LCD is clear
;   Uses R16, but preserves its content
;   Ports and portpins of the LCD:
    .equ pLcdDI = PINA ; Read port data bus
    .equ pLcdDD = DDRA ; Direction port of the data bus
    .equ pLcdCRSO = PORTB ; Port of the LCD RS pin
    .equ bLcdCRSO = PORTB1 ; Portpin of the LCD RS pin
; 
LcdBusy8:
  push R16 ; Save R16
  clr R16 ; Data bus direction to input
  out pLcdDD,R16 ; Clear direction port
  cbi pLcdCRSO,bLcdCRSO ; RS pin to low
  sbi pLcdCRWO,bLcdCRWO ; RW pin to high
LcdBusyWait:
  rcall LcdInE ; Activate E, read data port, deactivate E
  lsl R16 ; Busy flag to carry
  brcs LcdBusyWarte ; Flag 1, wait on
  cbi pLcdCRWO,bLcdCRWO ; RW pin to low
  ldi R16,0xFF  Data bus to output
  out pLcdDD,R16 ; Direction port all high
  pop R16 ; Restore R16
  ret ; Done


2.2.2 Busy flag in 4 bit mode

Wait for busy, 4 bit In 4 bit mode the data bus can be located on the upper (Lcd4High=1) or on the lower nibble (Lcd4High=0) of the AVR. The following routine works in both cases.

LcdBusy4:
  push R16 ; Save R16
  in R16,pLcdDD ; Data bus direction port
  .if Lcd4High == 1
    andi R16,0x0F ; Clear upper nibble
    .else
    andi R16,0xF0 ; Clear lower nibble
    .endif
  out pLcdDD,R16 ; Write direction port
  cbi pLcdCRSO,bLcdCRSO ; RS pin to low
  sbi pLcdCRWO,bLcdCRWO ; RW pin to high
LcdBusyWarte:
  rcall LcdInE ; Activate E, read data port, deactivate E
  rcall LcdPulseE ; Pulse lower nibble, content ignored
  .if Lcd4High != 1 ; If connected to lower nibble
    swap R16 ; Swap lower to higher nibble
    .endif
  lsl R16 ; Shift busy flag to carry
  brcs LcdBusyWarte ; Flag 1, wait on
  cbi pLcdCRWO,bLcdCRWO ; RW pin to low
  in R16,pLcdDD ; Read data bus direction port
  .if Lcd4High == 1
    ori R16,0xF0 ; Set upper nibble
    .else
    ori R16,0x0F ; Set lower nibble
    .endif
  out pLcdDD,R16 ; Write to direction port
  pop R16 ; Restore R16
  ret ; Done

Please note: if the upper nibble data port of the LCD is connected with the lower nibble of the AVR port, the SWAP has to transfer the bit 4 to bit 7.

2.3 Wait cycles

Under all circumstances wait periods are necessary:
  1. Prior to the first access the start-up cycle of 50 ms has to be waited so that the LCD can perform its own setup.
  2. Setting the function configuration (to 8 bit or 4 bit mode, the LCD needs 5 ms for this.
When configuring the LCD without the RW pin, additional wait cycles are necessary:
  1. 1,64 ms for the Return or Home command,
  2. 40 µs for all other operations.
1 and 2 are required in any case, 3 and 4 only if the busy flag is not available.

For those wait cycles
  1. the register pair ZH:ZL is loaded with a number,
  2. is down-counted with SBIW ZL,1, and
  3. when it reaches zero, the cycle ends.
To be compatible with any clock frequency, the number Z is calculated from the clock frequency in LcdClock.

Those are the routines:

; Wait routines
;
; Wait 50 ms
;   When the clock frequency is above 3.1 MHz the maximum
;   that can be counted in 16 bits. Therefore the 5 ms routine
;   is called for 10 times
LcdWait50ms:
  push R16 ; Save R16
  ldi R16,10 ; 10 times 5 ms
  rcall LcdWait5ms
  dec R16
  brne LcdWait50ms1
  pop R16
  ret
;
LcdWait5ms:
  ; Wait 5 ms
  .equ cLcdZ5ms = (5*LcdClock/1000 - 10 + 2) / 4
  ldi ZH,High(cLcdZ5ms)
  ldi ZL,Low(cLcdZ5ms)
  rjmp LcdWaitZ
;
.if LcdWait == 1 ; Wait mode delays
  ; Wait 1.6 ms
  .equ cLcdZ1600us = (1600*LcdClock/1000000 - 10 + 2) / 4
  LcdWait1600us:
    ldi ZH,High(cLcdZ1600us)
    ldi ZL,Low(cLcdZ1600us)
    rjmp LcdWaitZ
  ;
  ; Wait 40 us
  LcdWait40:
    ldi ZH,High(cLcdZ40us)
    ldi ZL,Low(cLcdZ40us)
    rjmp LcdWaitZ
  .endif
;
; Wait routine with Z cycles
LcdWaitZ:
  sbiw ZL,1 ; Count down, 2 clock cycles
  brne LcdWaitZ ; Not zero: 2 clock cycles, Zero: 1 clock cycle
  ret ; Back, 4 clock cycles
;
  ; clock cycles = 7 * 4 + (Z - 1) + 3 + 4
  ;                7: RCALL, LDI, LDI, RJMP
  ;                    4: 4 clock cycles per count - 1
  ;                                  3: 3 clock cycles for last count
  ;                                      4: RET
  ;              = 4 * Z + 7 - 4 + 3 + 4
  ;              = 4 * Z + 10
  ; Z = (clock cycles - 10 + 2) / 4
  ;                         +2: Rounding prior to division by 4

A possible variation is to save register pair ZH:ZL prior to using it in the two resp. four wait routines and to restore it on the end of LcdWaitZ:. The four pushes and pops only change the calculation of Z slightly.

5 ms wait at 2.45768 MHz 5 ms wait at 20 MHz The derivation of Z from the clock frequency leads to exact delays independant from clock. The simulated times (with avr_sim are for completely different clock rates for 5 ms.

2.4 Initing the LCD

Init of the LCD, phase A to D Communication with the LCD starts with the configuration of the control pins (A) and the 2.4.2 data bus (B). Then the 50 ms wait cycle is performed (C) followed by switching the LCD to 8 bit mode (D), no matter which data bus will be used later on.

2.4.1 Control pins

To be configured are
  1. the E pin,
  2. the RS pin,
  3. and, if reading the busy flag is desired, the RW pin.
To be able to have those three pins located in different I/O ports (whereever there is a bit available), those I/O ports and pins are named with symbols to achieve a maximum of flexibility and understand-ability: The names consist of
  1. p for all port symbols,
  2. Lcd those connected with the LCD,
  3. C for control pins,
  4. E, RS or RW for the three control pins, and
  5. O for Output or D for direction ports.
To be consistent also the portpins get symbolic names. Those start with b for portbit and relate to the output bits PORTnb (eg. .equ bLcdCEO = PORTB2) or the direction bits DDnb, eg. .equ bLcdCED = DDB2.

The init routine for the control pins therefore looks like that:

  ; Init Control pins
  cbi pLcdCEO,bLcdCEO ; E output low
  sbi pLcdCED,bLcdCED ; E direction high
  cbi pLcdCRSO,bLcdCRSO ; RS output low
  sbi pLcdCRSD,bLcdCRSD ; RS direction high
  .ifdef bLcdCRWO ; If RW pin defined
    cbi pLcdCRWO,bLcdCRWO ; RW output low
    sbi pLcdCRWD,bLcdCRWD ; RW direction high
    .endif

Please note the following configuration issues for init. As can be seen from the above .ifdef, initializing of the port-pin bLcdCRW only takes place, if both symbols are defined. If this is not the case, the LCD's RW pin is to be connected to the GND pin and LcdWait has to be set one. If the LCD-RW-pin is connected with an AVR portpin, this pin has to be defined. This pin is then initiated as output and is low, no matter if the LcdWait mode is used or the LcdBusy mode is configured. In case of the LCDWait mode, this pin is not touched and never used, in case of LcdBusy mode this pin is used.

2.4.2 Data bus init

Init of the data bus is slightly different under an 2.4.2.1 8 bit data bus and for a 2.4.2.2 4 bit data bus.

2.4.2.1 Data bus init 8 bit operation

With an 8 bit data bus
  1. the data port outputs in PORTn (mit n=A, B, C, etc.) are written to zeros, and
  2. the direction bits in DDRn are written to ones.
The default direction is that the AVR writes to the LCD.

The port's names are symbolics to be flexible. Data bus ports are named pLcdDO and pLcdDD.

An init routine for the 8 bit data bus:

  ; Init 8 bit data bus
  clr R16 ; Port outputs to low
  out pLcdDO,R16 ; Clear outputs
  ldi R16,0xFF ; Direction ports to high
  out pLcdDD,R16 ; Port is output

2.4.2.2 Data bus init 4 bit operation

In 4 bit operation the four bits in the AVR port that are not connected to the LCD have to remain the same as before so that init does not interfere other port settings. Both ports, pLcdDO and pLcdDD, have to be preserved. Lcd4High again decides whether the lower or upper nibble have to be preserved.

The code looks like that:

  ; Init 4 bit data bus
  in R16,pLcdDO ; Read output port
  .if Lcd4High == 1
    andi R16,0x0F ; Preserve lower nibble, clear upper
    .else
    andi R16,0xF0 ; Preserve upper nibble, clear lower
    .endif
  out pLcdDO,R16 ; Write to output port
  in R16,pLcdDD ; Read direction port
  .if Lcd4High == 1
    ori R16,0xF0 ; Upper nibble high, preserve lower
    .else
    ori R16,0x0F ; Lower nibble high, preserve higher
    .endif
  out pLcdDD,R16 ; Write direction port

2.4.3 Wait for 50 ms

One has to assume that when the controller starts its work the LCD also starts. Therefore the init starts with a long pause to leave enough time for the LCD to do its own internal init work. 50 ms is sufficient, even for older LCDs.

Here the routine described in 2.3 Wait cycles can be used.

2.4.4 Switching the LCD to 8 bit operation

All LCDs, even when they are supposed to operate in 4 bit mode, have to be switched to 8 bit mode first. This ensures that from whatever state the LCD operates in a predictable mode and later switchung to 4 bit is a reliable sequence.

To switch to 8 bit mode is done by
  1. clearing the RS pin (and RW pin, if necessary),
  2. placing 0x30 on the data bus (in 4 bit mode: 0x3 on the data bus nibble),
  3. activating the E pin for one microsecond and then low again, and by
  4. waiting for 1.6 ms.
If this procedure is repeated two or three times, we are absolutely sure that the LCD is in 8 bit mode.

The code in 8 bit mode:

  ; Switching to 8 bit mode
  cbi pLcdCRSO,bLcdCRSO ; RS pin Low
  ldi R16,0x30 ; 8 bit mode
  out pLcdDO,R16 ; to data bus
  rcall LcdPulseE ; E pin high for one us
  rcall LcdWait1600us ; Wait 1.6 ms

The last two instructions shall be repeated two or three times.

With a 4 bit data bus the code looks as follows:

  ; Switching to 8 bit operation with a 4 bit data bus
  cbi pLcdCRSO,bLcdCRSO ; RS pin Low
  in R16,pLcdDO ; Read output port
  .if Lcd4High == 1
    andi R16,0x0F ; Clear uppoer nibble, preserve lower
    ori R16,0x30 ; Upper nibble to 0x3
    .else
    andi R16,0xF0 ; Clear lower nibble, preserve upper
    ori R16,0x03 ; Lower nibble to 0x3
    .endif
  out pLcdDO,R16 ; to data bus
  rcall LcdPulseE ; Activate E pin for one us
  rcall LcdWait1600us ; Wait 1.6 ms


2.4.5 Switching the LCD from 8 bit to 4 bit operation

From 8 to 4 bit mode This part is only performed if the LCD shall be operated in 4 bit mode.

The code above is repeated but ori R16,0x30 and ori R16,0x03 is replaced by 0x20 resp. 0x02. And make sure that the command is sent only once. From now on each write and read operation requires two pulses on the E pin: first the upper and second the lower nibble are wrote or read.

2.4.6 System Set of the LCD

The System Set command of the LCD shall be send following the switching to 8 bit and, if applicable the switching to 4 bit mode. It differs a bit between 2.4.6.1 System Set 8 bit and 2.4.6.2 System Set 4 bit.

2.4.6.1 System Set of the LCD in 8 bit mode

System Set 8 bit Like with all following writes to and reads from the LCD the first step is to decide if the busy mode is selected. If yes, it is waited for the busy flag.

Then the System Set command byte is send. In 8 bit mode this is The byte is send with RS=low as control command by activating the E pin for one microsecond.

If wait operation is selected, this write is followed by a 1.64 ms delay. The code looks as follows:

;
; SystemSet8 sends System Set control command
;   in 8 bit modeto the LCD
;   The constant LcdLines is set to the number of lines
;     (1..4) of the LCD
SystemSet8:
  .if LcdLines > 1
    ldi R16,0x3C ; Two or four line display
    else
    ldi R16,0x30 ; Single line display
    .endif
;
; LcdCtrl8 sends Kontrollbyte in R16
;   in 8 bit mode to the LCD
LcdCtrl8:
  .if LcdWait == 0
    rcall LcdBusy ; If busy mode wait for busy
    .endif
  cbi pLcdCRSO,bLcdCRSO ; RS bit to low
  out pLcdDO,R16 ; Place R16 to0 data bus
  rcall LcdPulseE ; Activate E
  .if LcdWait == 1
    rjmp LcdWait1640us ; Wait 1.64 ms
    .else
    ret
  .endif


2.4.6.2 System Set of the LCD from 8 bit to 4 bit operation

System Set 4 bit System Set has to be repeated once in 4 bit mode to ensure that the number of display lines is correct.

From now on the check for the busy flag prior to sending is possible, if selected in the LcdWait property (LcdWait=0). Otherwise wait cycles are inserted following command write.

In 4 bit mode the System Set is transferred in two portions: First the upper nibble with 0x2 is written with RS=Low and RW=Low, followed by the lower nibble with the lines settings.

The codes then is:

;
; Systemset4 writes System Set control byte
;   in 4 bit mode to the LCD
;   LcdLines holds the number of lines (1..4) of the LCD
SystemSet4:
  .if LcdLines > 1
    ldi R16,0x28 ; Two or four line display
    else
    ldi R16,0x2C ; Single line display
    .endif
;
; LcdCtrl writes control byte in R16
;   in 4 bit mode to the LCD
;   LcdHigh 0: LCD on lower port nibble
;           1: LCD on upper port nibble
;   LcdWait 0: busy mode
;           1: wait cycles
LcdCtrl4:
  .if LcdWait == 0
    rcall LcdBusy4
    .endif
  cbi pLcdCRSO,bLcdCRSO ; Clear RS bit
  push ZL ; Save ZL
  push R16 ; Save R16
  in ZL,pLcdDO ; Read data output port
  .if Lcd4High == 1
    andi ZL,0x0F ; Clear upper nibble of port, preserve lower
    andi R16,0xF0 ; Preserve upper nibble of input
    .else
    andi ZL,0xF0 ; Clear lower nibble of port, preserve upper
    swap R16 ; Upper to lower nibble
    andi R16,0x0F ; Preserve lower nibble, clear upper
    .endif
  or R16,ZL ; combine lower and upper nibble
  out pLcdDO,R16 ; and write to data bus
  rcall LcdPulseE ; Activate E
  pop R16 ; Restore R16
  push R16 ; and save again
  in ZL,pLcdDO ; Read data output port again
  .if Lcd4High == 1
    andi ZL,0x0F ; Preserve lower nibble, clear upper
    swap R16 ; Lower to upper nibble
    andi R16,0xF0 ; Preserve upper, clear lower
    .else
    andi ZL,0xF0 ; Preserve upper, clear lower nibble
    andi R16,0x0F ; Preserve lower nibble
    .endif
  or R16,ZL ; Combine upper and lower nibble
  out pLcdDO,R16 ; Write to data bus
  rcall LcdPulseE ; Activate E
  pop R16 ; Restore R16
  pop ZL ; Restore ZL
  .if LcdWait == 1
    rjmp LcdWait1640us ; Wait 1.64 ms
    .else
    ret
    .endif


2.4.7 Finalize init

Finalize init Following System Set further properties of the LED can be configured.

The Entry mode shall be modified. Cursor move direction is by default right to left (Japanese) and should be set to left to right by issuing 0x06 as command.

The cursor behaviour can be set to display an underscore for it and to blink the character at the cursor.

If after init the display shows any wild characters those can be deleted by clearing the display. If in wait mode, this should be followed by a pause of 1.6 ms.

With this the LCD is completely initiated and we can start working with the LCD and we can display characters. The code for displaying characters is:

; Display character A
  ldi R16,'A' ; Load ASCII character A to R16
;
; LcdChar displays character in R16 on the display
;   LcdWait determines if wait cycles are used (=1) or
;     if busy is read (=0)
;   LcdBits determines if a 4 or 8 bit data bus is used
;   Lcd4High determines, in case of a 4 bit bus, if the
;     data port is connected to the upper (=1) or lower
;     (=0) nibble of the AVR port
LcdChar:
  .if LcdWait == 0
    rcall LcdBusy ; Wait for busy clear
    .endif
  sbi pLcdCRSO,bLcdCRSO ; RS pin high
  .if LcdBits == 8
    out pLcdDD,R16 ; To data bus
    .else
    push ZL ; Save ZL
    push R16 ; Save R16
    in ZL,pLcdDO ; Read output port
    .if Lcd4High == 1
      andi ZL,0x0F ; Clear upper nibble, preserve lower
      andi R16,0xF0 ; Clear lower nibble
      .else
      andi ZL,0xF0 ; Clear lower nibble, preserve higher
      swap R16 ; Upper to lower nibble
      andi R16,0x0F ; Clear upper nibble
      .endif
    or R16,ZL ; combine
    out pLcdDO,R16 ; Write to data bus
    rcall LcdPulseE ; Activate E
    pop R16 ; Restore R16
    in ZL,pLcdDO ; Read output port again
    andi R16,0x0F ; Clear upper nibble
    .if Lcd4High == 1
      andi ZL,0x0F ; Clear upper nibble
      swap R16 ; Lower to upper nibble 
      .else
      andi ZL,0xF0 ; Clear lower nibble
      .endif
    or R16,ZL ; Combine nibbles
    out pLcdDD,R16 ; To data bus
    pop ZL ; Restore ZL
    .endif
  rcall LcdPulseE ; Activate E
  .if LcdWait == 1
    rjmp LcdWait40us ; Wait 40 us
    .else
    ret
    .endif

That sounds a little bit complicated but works in all cases. Conditional assembly is used to tailor the software to the specific case: code is solely generated if really needed.

2.5 Select an output position on the LCD

Set cursor position Very often only part of the content of the display shall be changed, either one or a few chars are to be written. In that case we need a cursor set routine. This address change is a little bit strange.

The start of the first and the second line can be addressed by issuing 0x00 and 0x40 with the seventh bit set (0x80 and 0xC0). The column - 1 is added, so the addresses of the first line are 0x80, 0x81, 0x82, etc.. On the second line the addresses to be issued are 0xC0, 0xC1, 0xC3, etc.

A bit more crazy are the lines 3 and 4, if the LCD has those. Their display RAM addresses add the number of characters per line of the LCD (N = 8, 16, 20 or 24) to the base address of line 1 (0x80 + N) or line 2 (0xC0 + N).

An assembler routine for setting the address could be:

;
; Set position To column 8 of the second line
LcdPos28:
  ldi ZH,2-1 ; Set line two
  ldi ZL,8-1 ; Set column eight
;
; LcdPos sets the LCD cursor to the position in Z
;   ZH: Line (0 to number of lines - 1)
;   ZL: Column (0 to number of columns - 1)
LcdPos:
  cpi ZH,1 ; Line = 2?
  ldi R16,0x80 ; Line 1
  .if LcdLines < 2 ; LCD has only one line
    rjmp LcdPos1
    .endif
  brcs LcdPos1 ; No, line 1
  ldi R16,0xC0 ; Line 2 address
  .if LcdLines == 2 ; LCD has only two lines
    rjmp LcdPos1
    .endif
  breq LcdPos1
  ldi R16,0x80+LcdCols ; Line 3 address
  cpi ZH,2 ; Line = 3
  breq LcdPos1
  ldi R16,0xC0+LcdCols ; Line 4 address
LcdPos1:
  add R16,ZL ; Add column
  rjmp LcdCtrl ; Write control command to LCD

2.6 To generate own characters

Addresses and data of the character generator In each LCD the first eight characters (char 0x00 to 0x07) can be altered by the user and can be used to display own characters.

The character generator RAM of the first eight characters are addressed as follows:
  1. The address has six bits: When writing the address the two bits 0b01 are added on the left and send with RS=low to the LCD.
  2. The data to be written to those addresses has five bits. Ones turn the pixel on, zeros off (to background color). The five bits have to be written with preceeding 0b000 with RS=high to the LCD. If the cursor direction I/D during initiation was set to Increase, the eight bytes data can be send in a row, without re-writing the address (Auto-Increment).
To send those special chars, those would be best written to a table. The table shall consist of one address byte and eight data bytes. To come to an even number of bytes per row, a dummy character is added (that is ignored by the software) to avoid insertion of a null byte by the assembler.
AddressDummyLines
12345678
Byte0ByteByteByteByteByteByteByteByte
Byte0ByteByteByteByteByteByteByteByte
... further characters ...
Byte0ByteByteByteByteByteByteByteByte
00(End of table)
To generate such tables a spreadsheet application can be used. Here such spreadsheets are described. The generated assembler table can be copied and pasted to the source code file.

The following code writes the table content to the LCD:

;
; LcdSpec generates special characters to the LCD
;   Z points to 2*Table address
;   Tabellenformat:
;     1.Byte: Address of character 0b01zzz000,
;             0: End of table
;     2.Byte: Dummy
;     3. to 10.Byte: Data of the lines 1 to 8
LcdSpec:
  push R0 ; R0 is counter
LcdSpec1:
  lpm R16,Z+ ; Read address of characters
  tst R16 ; End of the table?
  breq LcdSpec3 ; End of table
  rcall LcdCtrl ; Write address
  adiw ZL,1 ; Overread dummy
  ldi R16,8 ; 8 byte data per character
  mov R0,R16 ; R0 is counter
LcdSpec2:
  lpm R16,Z+ ; Read data byte
  rcall LcdData ; Write data byte
  dec R0 ; Count down
  brne LcdSpec2 ; Go on with data
  rjmp LcdSpec1 ; Next character
LcdSpec3:
  pop R0 ; Restore R0
  ldi ZH,0 ; Cursor home
  ldi ZL,0
  rjmp LcdPos
;


The following table generates arrow characters on the LCD.

;
; Table of character codes
CharCodes:
.db 64,0,0,12,6,31,6,12,0,0 ; Z = 0, arrow right
.db 72,0,0,6,12,31,12,6,0,0 ; Z = 1, arrow left
.db 80,0,4,14,31,21,4,4,4,0 ; Z = 2, arrow up
.db 88,0,4,4,4,21,31,14,4,0 ; Z = 3, arrow down
.db 96,0,0,15,3,5,9,16,0,0 ; Z = 4, arrow rigth up
.db 104,0,0,16,9,5,3,15,0,0 ; Z = 5, arrow right down
.db 112,0,0,1,18,20,24,30,0,0 ; Z = 6, arrow left down
.db 120,0,0,30,24,20,18,1,0,0 ; Z = 7, arrow left up
.db 0,0 ; End of the table
;

Arrow characters as special chars Special chars arrows
To the right is shown how those look alike on the LCD.

3 Software

The software for accessing all kind of LCDs

3.1 Source code

The assembler source code of the include file is available here for download and can be viewed here in HTML-Format in the browser. To assemble these two files an assembler has to be used that can handle .IF, .IFDEF and .ERROR directives.

3.2 Parameters

3.2.1 Constants to be defined in the main source file

To adjust all necessary parameters for the included file the template that starts from line 63 of the include file can be copied to main. All parameters have to be adjusted and the semicolon is to be removed (if appropriate).

; *************************************
;  P A R A M E T E R - T E M P L A T E
; *************************************
;
; Standard parameter set of properties/definitions
;.equ clock = 1000000 ; Clock frequency of controller in Hz
; LCD size:
  ;.equ LcdLines = 1 ; Number of lines (1, 2, 4)
  ;.equ LcdCols = 8 ; Number of characters per line (8..24)
; LCD bus interface
  ;.equ LcdBits = 4 ; Bus size (4 or 8)
  ; If 4 bit bus:
    ;.equ Lcd4High = 1 ; Bus nibble (1=Upper, 0=Lower)
  ;.equ LcdWait = 0 ; Access mode (0 with busy, 1 with delay loops)
; LCD data ports
  ;.equ pLcdDO = PORTA ; Data output port
  ;.equ pLcdDD = DDRA ; Data direction port
; LCD control ports und pins
  ;.equ pLcdCEO = PORTB ; Control E output port
  ;.equ bLcdCEO = PORTB0 ; Control E output portpin
  ;.equ pLcdCED = DDRB ; Control E direction port
  ;.equ bLcdCED = DDB0 ; Control E direction portpin
  ;.equ pLcdCRSO = PORTB ; Control RS output port
  ;.equ bLcdCRSO = PORTB1 ; Control RS output portpin
  ;.equ pLcdCRSD = DDRB ; Control RS direction port
  ;.equ bLcdCRSD = DDB1 ; Control RS direction portpin
; If LcdWait = 0:
  ;.equ pLcdDI = PINA ; Data input port
  ;.equ pLcdCRWO = PORTB ; Control RW output port
  ;.equ bLcdCRWO = PORTB2 ; Control RW output portpin
  ;.equ pLcdCRWD = DDRB ; Control RW direction port
  ;.equ bLcdCRWD = DDB2 ; Control RW direction portpin
; If you need binary to decimal conversion:
  ;.equ LcdDecimal = 1 ; If defined: include those routines
; If you need binary to hexadecimal conversion:
  ;.equ LcdHex = 1 ; If defined: include those routines
; If simulation in the SRAM is desired:
  ;.equ avr_sim = 1 ; 1=Simulate, 0 or undefined=Do not simulate
;

The three switches LcdDecimal, LcdHex and avr_sim can be undefined by leaving the semicolon where it is, if those routines are not needed.

If the port pLcdCRWO and the portpin bLvcdCRWO is defined and wait mode with LcdWait = 1 is selected, this pin is set as output and is set low, but is not used later on. In this case selection between different LcdWait modes is possible and this pin has correct signals in both cases.

During assembling all currently selected properties are checked for completeness and availability. Missing constants throw error messages and disable code generation.

3.3 Application

The functions that the include file provides are described in the header of the file. Included are

; ***********************************************
; *  L C D   I N T E R F A C E  R O U T I N E S *
; ***********************************************
;
; +-------+----------------+--------------+
; |Routine|Function        |Parameters    |
; +-------+----------------+--------------+
; |LcdInit|Inits the LCD   |Ports, pins   |
; |       |in the desired  |              |
; |       |mode            |              |
; +-------+----------------+--------------+
; |LcdText|Displays the    |Z=2*Table     |
; |       |text in flash   |  address     |
; |       |memory starting |0x0D: Next    |
; |       |with line 1     |      line    |
; |       |                |0xFF: Ignore  |
; |       |                |0xFE: Text end|
; +-------+----------------+--------------+
; |LcdSram|Display the text|Z=SRAM-Address|
; |       |in SRAM         |R16: number of|
; |       |                |    characters|
; +-------+----------------+--------------+
; |LcdChar|Display charac- |R16: Character|
; |       |ter on LCD      |              |
; +-------+----------------+--------------+
; |LcdCtrl|Output control  |R16: Control  |
; |       |byte to LCD     |     byte     |
; +-------+----------------+--------------+
; |LcdPos |Set position on |ZH: Line 0123 |
; |       |the LCD         |ZL: Col 0..   |
; +-------+----------------+--------------+
; |LcdSpec|Generate special|Z: 2*Table    |
; |       |characters      |   address    |
; +-------+----------------+--------------+
; |    S W I T C H   L C D D E C I M A L  |
; +-------+----------------+--------------+
; |LcdDec2|Convert to two  |R16: Binary   |
; |       |decimal digits  |     8 bit    |
; +-------+----------------+--------------+
; |LcdDec3|Convert to three|R16: Binary   |
; |       |decimal digits, |     8 bit    |
; |       |supp. leading 0s|              |
; +-------+----------------+--------------+
; |LcdDec5|Convert to five |Z: Binary     |
; |       |decimal digits, |   16 bit     |
; |       |supp. leading 0s|              |
; +-------+----------------+--------------+
; |      S W I T C H   L C D H E X        |
; +-------+----------------+--------------+
; |LcdHex2|Convert to two  |R16: Binary   |
; |       |digits in hex   |     8 bit    |
; +-------+----------------+--------------+
; |LcdHex4|Convert to four |Z: Binary     |
; |       |digits in hex   |   16 bit     |
; +-------+----------------+--------------+

The routines are programmed in a way that the registers ZH and ZL as well as R2, R1 and R0 are used. R2, R1 and R0 are preserved (restoring their content after use). ZH and ZL are not always preserved. R16 is also used and is not always preserved. Consult the source code if in doubt.

3.3.1 Init the LCD

With rcall LcdInit the LCD is inited. This routine is called once during init of the hardware. The routine
  1. configures the control- and data bus-output pins according to the given port and portbit information,
  2. waits 50 ms, until the LCD has done its self-initiation,
  3. switches the LCD four times to 8 bit mode,
  4. switches, if selected, to 4 bit mode,
  5. performs the Function Set (mode and line selection),
  6. switches the LCD on and the cursor and cursor blink off, and
  7. clears the LCD.

3.3.2 Display text on the LCD

With rcall LcdText the LCD can be fed with predefined text from a table in flash memory. The table

3.3.3 Displaying text from the SRAM on the LCD

With rcall LcdSram the text in SRAM, to which register pair Z points, is displayed on the LCD. The number of characters to be displayed is in R16.

3.3.4 To display a single character on the LCD

rcall LcdChar outputs the character in R16 on the current position of the LCD.

3.3.5 To send a control command to the LCD

rcall LcdCtrl sends the byte in R16 as control command to the LCD.

3.3.6 To adjust the cursor position of the LCD

rcall LcdPos adjusts the cursor position of the LCD to the line given in ZH (0...LcdLines-1) and the column given in ZL (0...LcdCols-1).

3.3.7 Generate special characters in the LCD

By rcall LcdSpec a table with special chars is send to the LCD The table consists of
  1. one address byte (0b01zz.z000), in which z is the character number (0...7),
  2. one dummy byte (eg. 0), plus
  3. eight data bytes, that hold the five bits from right to left per character line, from to below,
  4. a null byte as address byte, that ends output.
The special chars can be inserted with 0x00 to 0x07 in text output, e.g. .db "Special char 0 =",0x00,0xFE.

3.3.8 Display binaries as decimal on LCD

Those routines are included in the code and can be called with rcall LcdDec2, rcall LcdDec3 and rcall LcdDec5 if the switch LcdDecimal is defined in the source code of the main program.

LcdDec2 displays the binary in R16 with two decimal digits. If R16 is larger than 99 the 99 is displayed. A leading zero is not blanked.

LcdDec3 displays an 8 bit binary in R16 with three digits. Leading zeros are blanked.

LcdDec5 displays the 16 bit binary in Z (ZH:ZL) with five digits at the current display position. Leading zeros are blanked.

3.3.9 Display binaries hexadecimal on the LCD

Those routines are included in the code if the switch LcdHex is defined in the source code.

LcdHex2 displays the 8 bit binary in R16 with two digits in in hexadecimal form. LcdHex4 displays the 16 bit binary in Z in hexadecimal form.

3.4 Size of the program of the assembled versions

The size of the assembled program code depends from the chosen parameters. The table displays those sizes, solely resulting from the include file.
VersionSize
words
Minimal + Option
Minimal version (1 MHz, 8 bit, wait mode, w/o dec/hex)135-
Wait ==> Busy+9146
1 MHz ==> 20 MHz+50185
8 bit ==> 4 bit+45180
LcdDecimal+86221
LcdHex+14149
Maximum version
(20 MHz, 4-Bit, busy mode,
Decimal- and Hex-Conversion)
+250385
The include file is optimized by conditional assembling. The size is minimized and tailored to the application. No unnecessary code blows the code up, as is the case with C or Basic compilers.

3.5 Example programs

The use of the include software for the LCD is demonstrated with two example programs:
  1. with an 8 bit data bus and displaying decimal and hexadecimal numbers, and
  2. with a 4 bit data bus with generation and display of eight special characters.

3.5.1 Example program 8 bit

Example 8 bit program with decimal output The example program (in assembler source code format here, in HTML format here) demonstrates, with an ATmega8, the 8 bit connection and the output of decimal and hexadecimal numbers on the LCD and including the der include file lcd.inc into the source code. In this case the ATmega8 operates with an 8 MHz crystal.

The routine LcdInit initializes the LCD, with LcdText the display mask is issued as listed in the table InitText:. LcdPos positions the LCD cursor to the lines 3 and 4 and LcdDec5 display the 16 bit binary in decimal, LcdHex4 in hexadecimal form.

3.5.2 Example program 4 bit

Example program 4 bit with special chars The example program (in assembler source code format here, in HTML format here) demonstrates with an ATtiny24 the 4 bit connection and the generation of eight arrow characters on the LCD and how to integrate this the include file lcd.inc. Here, the controller works with 1 MHz clock.

The hardware has been described here.

The routine LcdInit initiates the LCD, with LcdSpec the special characters (arrows) in the table Codezeichen: are written to the LCD. With LcdText the text in the table InitText: is displayed, including the special characters.


Hardware Configuration Software


Praise, error reports, scolding and spam please via the comment page to me.

To the top of that page

©2018-2020 by http://www.avr-asm-tutorial.net