Path:
Home =>
AVR-EN =>
Apps => LCD on an AVR
Diese Seite in Deutsch:
 |
AVR applications
LCD on an AVR in assembler |
A LCD on an AVR
The ultimate control software for a LCD on an AVR works in any
case and can be configured to fit to any needs.
- Hardware
- Configuration
- Software
Changes:
- 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.
- 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.
- 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.1 LCD connections and operating modes of the LCD
LCDs have three control input lines and eight bidirectional data bus
input/output lines.
The three control inputs have the following function:
- 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).
- 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.
- 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:
- 8 or 4 bit data bus,
- with RW bit (bidirectional, with busy flag read) or
without (unidirectional, with wait loops), and
-
- if a 4 bit data bus is used: communication with the
upper or lower nibble of the I/O port of the controller.
Those variations look like that:
- With eight data bits and with the Read/Write control pin
R/W:
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.
- With eight data bits and without Read/Write control pin R/W:
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.
- 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:
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.
- 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:
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.
- With four data bits and without Read/Write control pin R/W,
the data bus on the upper nibble of the AVR port:
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.
- With four data bits without Read/Write control pin R/W, the
data bus on the lower nibble of the AVR port:
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:
- availability of portpins,
- accessability of the portpins (e.g. if the 7th bit is not
available in that port),
- simplicity/complexity of the software (an 8 bit write is
simpler and faster than two four bit writes, and
- speed requirements (to avoid unnecessary wait cycles).
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
- the RS input is low,
- the RW input is also low, and
- 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)!
Command | Data byte | Abbreviations | Time |
D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
Clear display |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
- |
1.52/1.64 ms |
Reset |
0 | 0 | 0 | 0 | 0 | 0 | 1 | x |
- |
1.52/1.64 ms |
Input mode |
0 | 0 | 0 | 0 | 0 | 1 | I/D | S |
I/D: Cursor direction 0=left, 1=right S: Shift display 0=no, 1=yes |
37/40 µs |
Display on/off |
0 | 0 | 0 | 0 | 1 | D | C | B |
D: Display 0=Off, 1=On C: Cursor 0=Off 1=On B: Blink cursor 0=Off 1=On |
37/40 µs |
Cursor shift |
0 | 0 | 0 | 1 | S/C | R/L | x | x |
S/C: 0=Shift cursor, 1=Shift display R/L: 0=left, 1=right |
37/40 µs |
Function set |
0 | 0 | 1 | DL | N | F | x | x |
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 |
0 | 1 | a5 | a4 | a3 | a2 | a1 | a0 |
a5:a3 Character 0 to 7 a2:a0 Line 0 to 7 |
37/40 µs |
Set display RAM address |
1 | a6 | a5 | a4 | a3 | a2 | a1 | a0 |
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:
Procedure | Data byte | Abbreviations | Time |
D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
Read busy flag and Display RAM address |
BF | a6 | a5 | a4 | a3 | a2 | a1 | a0 |
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
- the RW input is low data is written to the character generator
or the display RAM, depending from the previous display address
setting. Current address and cursor position are advanced as
configured with the direction configuration (input mode),
- the RW input high data from the character generator or the
display RAM at the current address is read.
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.
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.
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.
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
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.
Under all circumstances wait periods are necessary:
- 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.
- 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,64 ms for the Return or Home command,
- 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
- the register pair ZH:ZL is loaded with a number,
- is down-counted with SBIW ZL,1, and
- 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.
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.
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.
To be configured are
- the E pin,
- the RS pin,
- 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:
- pLcdCEO Output port E pin,
- pLcdCRSO Output port RS pin,
- pLcdCRWO Output port RW pin (if in busy mode),
- pLcdCED Direction port E pin,
- pLcdCRSD Direction port RS pin,
- pLcdCRWD Direction port RW pin (if busy).
The names consist of
- p for all port symbols,
- Lcd those connected with the LCD,
- C for control pins,
- E, RS or RW for the three control pins, and
- 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.
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.
With an 8 bit data bus
- the data port outputs in PORTn (mit n=A, B, C, etc.)
are written to zeros, and
- 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
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
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.
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
- clearing the RS pin (and RW pin, if necessary),
- placing 0x30 on the data bus (in 4 bit mode: 0x3 on the
data bus nibble),
- activating the E pin for one microsecond and then low
again, and by
- 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
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.
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.
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
- 0x30 with a single line display, or
- 0x38 in case of a two- or four-line display.
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
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
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.
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
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:
- The address has six bits:
- the upper three bits address the number of the
character,
- the three lower bits address the lines of the
character, from top down.
When writing the address the two bits 0b01 are added
on the left and send with RS=low to the LCD.
- 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.
Address | Dummy | Lines |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Byte | 0 | Byte | Byte | Byte | Byte | Byte | Byte | Byte | Byte |
Byte | 0 | Byte | Byte | Byte | Byte | Byte | Byte | Byte | Byte |
... further characters ... |
Byte | 0 | Byte | Byte | Byte | Byte | Byte | Byte | Byte | Byte |
0 | 0 | (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
;
To the right is shown how those look alike on the LCD.
The software for accessing all kind of LCDs
- is completely written in assembler,
- can be added to the source code file with the
.include "lcd.inc" directive, and
- is configured in the main source code with constants
and so is adjusted to the specific application.
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.
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.
The functions that the include file provides are described
in the header of the file. Included are
- the name, that can be called with RCALL (name)
from the main source,
- the funktion, that is provided by the routine,
and
- the parameters (registers, content, etc.), that the
routine expects.
; ***********************************************
; * 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.
With rcall LcdInit the LCD is inited. This routine is called
once during init of the hardware. The routine
- configures the control- and data bus-output pins according
to the given port and portbit information,
- waits 50 ms, until the LCD has done its self-initiation,
- switches the LCD four times to 8 bit mode,
- switches, if selected, to 4 bit mode,
- performs the Function Set (mode and line selection),
- switches the LCD on and the cursor and cursor blink off, and
- clears the LCD.
With rcall LcdText the LCD can be fed with predefined text
from a table in flash memory. The table
- address has to be set, with its doubled value, to the
register pair Z,
- should consist of .db directives and should have an even
number of bytes per line, if not: add a 0xFF that is ignored in
the text output routine,
- does not need to write complete lines (unless you want to
override previous content) but can be, with a 0x0D byte, moved
to the beginning of the next line (if the LCD has multiple
lines),
- ends with 0xFE.
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.
rcall LcdChar outputs the character in R16 on the current
position of the LCD.
rcall LcdCtrl sends the byte in R16 as control command to
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).
By rcall LcdSpec a table with special chars is send to the LCD
The table consists of
- one address byte (0b01zz.z000), in which z is the character
number (0...7),
- one dummy byte (eg. 0), plus
- eight data bytes, that hold the five bits from right to left
per character line, from to below,
- 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.
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.
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.
The size of the assembled program code depends from the chosen
parameters. The table displays those sizes, solely resulting
from the include file.
Version | Size words | Minimal + Option |
Minimal version (1 MHz, 8 bit, wait mode, w/o dec/hex) | 135 | - |
Wait ==> Busy | +9 | 146 |
1 MHz ==> 20 MHz | +50 | 185 |
8 bit ==> 4 bit | +45 | 180 |
LcdDecimal | +86 | 221 |
LcdHex | +14 | 149 |
Maximum version (20 MHz, 4-Bit, busy mode, Decimal- and Hex-Conversion) |
+250 | 385 |
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.
The use of the include software for the LCD is demonstrated with
two example programs:
- with an 8 bit data bus
and displaying decimal and hexadecimal numbers, and
- with a 4 bit data bus with
generation and display of eight special characters.
3.5.1 Example program 8 bit
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
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.
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