I am planning on using an LCD screen as part of this year’s Retro Challenge. I want to use my new Rotary Encoder Module to be able to scroll the content on the screen.
I have bought the official RC2014 LCD Driver Module, and I have a 4*20 screen attached to it. This gives me 4 lines of 20 characters.
Spencer provides example BASIC code to control the screen. I want to control it using Z80 assembly language. Spencer does warn that this will not be as straight forward as the LCD will not respond fast enough. He does point us towards Mike Sutton’s blog for a solution. This involves us reading the status register on the LCD to see if it is ready for the next instruction.
I created two routines to send data to the LCD screen in Z80 assembly language. One to send a command byte and return when completed. The other to send a data byte and return when completed.
LCD_R EQU 218
LCD_D EQU 219
; Sends a command byte to the LCD.
; A - Command in
send_command:
out (LCD_R),a
.lcd_busy:
in a,(LCD_R)
rlca
jr c,.lcd_busy
ret
; Sends a data byte to the LCD
; A - Byte in
send_data:
out (LCD_D),a
.lcd_busy:
in a,(LCD_R)
rlca
jr c,.lcd_busy
ret
Another issue is the layout of the LCD screen. The first 20 characters are the first line, the next 20 are the third line, the next 20 are the second line, and the last 20 are the last line. This means we can’t simply loop through all characters in order to display them. Instead, we must be aware that every 20 characters we need to pick the next batch from elsewhere in memory.
In our case, the first line is at start of our data block. The second line is 40 bytes from the start. The third line is 20 bytes from the start. The fourth line is 60 bytes from the the start. This gives us 80 bytes in total.
This is how I solved this in Z80 assembly language.
; Display 4 lines of consecutive text on the LCD
; HL - address of text to display on the LCD
show_four_lines:
ld b,20
.line1loop:
ld a,(hl)
inc hl
call send_data
djnz .line1loop
ld de,20
add hl,de
ld b,20
.line2loop:
ld a,(hl)
inc hl
call send_data
djnz .line2loop
ld de,40
sub hl,de
ld b,20
.line3loop:
ld a,(hl)
inc hl
call send_data
djnz .line3loop
ld de,20
add hl,de
ld b,20
.line4loop:
ld a,(hl)
inc hl
call send_data
djnz .line4loop
ret
puff:
db "Retro Challenge 2024"
dc " "
db " LCD RC2014 Test "
db " Robert Price "
An alternative could be to store the lines in screen order. This would be easier to iterate, but would make the text harder for a human to read in the source code.
My final code looks like this. I have to send some commands to setup the LCD before I can display my text.
OUTPUT LCD.z80
ORG $9000
LCD_R EQU 218
LCD_D EQU 219
ld a,56 ; Function 8 bit, 2 lines, 5x8 dot font
call send_command
ld a,12 ; Display on, cursor off, no blink
call send_command
ld a,1 ; clear the display
call send_command
ld hl,puff ; the address of the text
call show_four_lines
ret
; Sends a command byte to the LCD.
; A - Command in
; A, C registers used.
send_command:
out (LCD_R),a
.lcd_busy:
in a,(LCD_R)
rlca
jr c,.lcd_busy
ret
; Sends a data byte to the LCD
; A - Byte in
; A, C registers used.
send_data:
out (LCD_D),a
.lcd_busy:
in a,(LCD_R)
rlca
jr c,.lcd_busy
ret
; Display 4 lines of consecutive text on the LCD
; HL - address of text to display on the LCD
; A, B, C, D, E, H, L registers used.
show_four_lines:
ld b,20
.line1loop:
ld a,(hl)
inc hl
call send_data
djnz .line1loop
ld de,20
add hl,de
ld b,20
.line2loop:
ld a,(hl)
inc hl
call send_data
djnz .line2loop
ld de,40
sub hl,de
ld b,20
.line3loop:
ld a,(hl)
inc hl
call send_data
djnz .line3loop
ld de,20
add hl,de
ld b,20
.line4loop:
ld a,(hl)
inc hl
call send_data
djnz .line4loop
ret
puff:
db "Retro Challenge 2024"
dc " "
db " LCD RC2014 Test "
db " Robert Price "