Driving an LCD module from an FPGA

Some time ago I bought a Digilent Nexys 2 FPGA development board and a Digilent PmodCLS LCD module. I spent some time implementing a more or less trivial CPU in the FPGA, and various bits and pieces to aid with debugging such as a driver for the 4-digit 7-segment LED display on the board (ideal for displaying the contents of 16-bit registers) and debug LEDs and switches to select what to display. I’ve been using this primarily as an exercise in learning Verilog.

I used SPI to send bytes to the LCD module, and hooked up this hardware to the address decoder such that I could simply issue writes from the CPU to send strings to the LCD module. It’s kinda a test project, the hardware equivalent of a hello world program.

The CPU is a very simple 16-bit RISC CPU with 4 orthogonal registers. It’s nothing special, not pipelined, takes quite a few clock cycles per instruction, and has a pretty rubbish instruction set. Conspicuously lacking are indirect addressing modes, which is a shame as it makes writing software quite hard.

The test program I used for this demo was rather complicated by the need for self-modifying code to write meaningful loops for string handling. That made debugging quite interesting. I wrote an assembler in perl, as assembling this kind of code by hand is rather too time-consuming.

The source code for the software running on the CPU is reproduced below. It’s not the model of great code, but it was quite a lot of fun to write and debug.

There’s also a video, running through one cycle.

Here’s the code. Execution starts at the beginning, at address zero.

        ld r2 compare
        st r2 comparestash
        ld r2 testend
        st r2 compare
        ld r2 textloop
        st r2 textloopstash

writetext:
        ld r3 one

textloop:
        ld r0 text
compare:
        b testend        ; mov r0 r0
        beq loopend
        st r0 lcdreg
        ld r1 textloop
        add r1 r1 r3
        st r1 textloop
        b textloop

loopend:
        ld r1 textloop
        add r1 r1 r3
        st r1 textloop
        ld r3 comparestash
        st r3 compare
        b textloop

testend:
        mov r0 r0
        beq gobacktostart
        ld r2 testend
        st r2 compare
        b delay

gobacktostart:
        ld r2 textloopstash
        st r2 textloop
delay:
        ld r0 count1
        ld r1 count2
        ld r2 zero
        ld r3 one
delayloop:
        sub r0 r0 r3
        sbc r1 r1 r2
        beq delaycontinue
        b delayloop
delaycontinue:
        mov r0 r0
        beq goroundagain
        b delayloop
; ready to go round again

goroundagain:
        ld r2 testend
        st r2 compare
        b writetext

stop:
        b stop
        ds #0
        ds #0
        b stop

zero:   ds #0
one:    ds #1

comparestash: ds #0
textloopstash: ds #0

count1: ds #0xffff
count2: ds #0x002f

text:
        ds #27  ; ESC
        ds #ord('[')
        ds #ord('j') ; j (clear screen and home cursor)
        ds #27  ; ESC
        ds #ord('[')
        ds #ord('0') ; wrap line at 16 characters
        ds #ord('h') ; set display mode
        ds #27  ; ESC
        ds #ord('[')
        ds #ord('3') ; 3 (display on, backlight on)
        ds #ord('e') ; e (control backlight)
        ds #27  ; ESC
        ds #ord('[')
        ds #ord('0') ; cursor off
        ds #ord('c') ; set cursor mode
        ds #32       ; SPACE
        ds #ord('H')
        ds #ord('e')
        ds #ord('l')
        ds #ord('l')
        ds #ord('o')
        ds #ord(',')
        ds #32       ; SPACE
        ds #ord('W')
        ds #ord('o')
        ds #ord('r')
        ds #ord('l')
        ds #ord('d')
        ds #ord('!')

        ds #0

        ds #27  ; ESC
        ds #ord('[')
        ds #ord('1') ; row 1
        ds #59       ; semicolon
        ds #ord('0') ; column 0
        ds #ord('H') ; set display mode

        ds #32       ; SPACE
        ds #ord('.')
        ds #ord('.')
        ds #ord('o')
        ds #ord('o')
        ds #ord('O')
        ds #ord('O')
        ds #ord('(')
        ds #ord(')')
        ds #ord('O')
        ds #ord('O')
        ds #ord('o')
        ds #ord('o')
        ds #ord('.')
        ds #ord('.')

        ds #0

        ds #27  ; ESC
        ds #ord('[')
        ds #ord('0') ; row 1
        ds #59       ; semicolon
        ds #ord('0') ; column 0
        ds #ord('H') ; set display mode

        ds #ord('T')
        ds #ord('h')
        ds #ord('i')
        ds #ord('s')
        ds #32       ; SPACE
        ds #ord('i')
        ds #ord('s')
        ds #32       ; SPACE
        ds #ord('a')
        ds #ord('n')
        ds #32       ; SPACE
        ds #ord('F')
        ds #ord('P')
        ds #ord('G')
        ds #ord('A')

        ds #0

        ds #27  ; ESC
        ds #ord('[')
        ds #ord('1') ; row 1
        ds #59       ; semicolon
        ds #ord('0') ; column 0
        ds #ord('H') ; set display mode

        ds #ord('a')
        ds #ord('n')
        ds #ord('d')
        ds #32       ; SPACE
        ds #ord('a')
        ds #32       ; SPACE
        ds #ord('C')
        ds #ord('P')
        ds #ord('U')
        ds #32       ; SPACE
        ds #ord('i')
        ds #ord('n')
        ds #ord('s')
        ds #ord('i')
        ds #ord('d')
        ds #ord('e')

        ds #0

        ds #27  ; ESC
        ds #ord('[')
        ds #ord('0') ; row 1
        ds #59       ; semicolon
        ds #ord('0') ; column 0
        ds #ord('H') ; set display mode

        ds #ord('[')
        ds #ord('^')
        ds #ord('^')
        ds #ord(']')
        ds #ord('N')
        ds #ord('e')
        ds #ord('x')
        ds #ord('y')
        ds #ord('s')
        ds #32       ; SPACE
        ds #ord('2')
        ds #ord('[')
        ds #ord('^')
        ds #ord('^')
        ds #ord(']')
        ds #32       ; SPACE
        ds #ord('[')
        ds #ord('v')
        ds #ord('v')
        ds #ord(']')
        ds #ord('P')
        ds #ord('m')
        ds #ord('o')
        ds #ord('d')
        ds #ord('C')
        ds #ord('L')
        ds #ord('S')
        ds #ord('[')
        ds #ord('v')
        ds #ord('v')
        ds #ord(']')
        ds #32       ; SPACE

        ds #0

        ds #27  ; ESC
        ds #ord('[')
        ds #ord('0') ; row 1
        ds #59       ; semicolon
        ds #ord('0') ; column 0
        ds #ord('H') ; set display mode

        ds #ord('v')
        ds #ord('e')
        ds #ord('r')
        ds #ord('i')
        ds #ord('l')
        ds #ord('o')
        ds #ord('g')
        ds #ord('-')
        ds #ord('|')
        ds #ord('-')
        ds #ord('x')
        ds #ord('i')
        ds #ord('l')
        ds #ord('i')
        ds #ord('n')
        ds #ord('x')

        ds #ord('-')
        ds #ord('-')
        ds #ord('-')
        ds #ord('a')
        ds #ord('s')
        ds #ord('m')
        ds #ord('-')
        ds #ord('-')
        ds #ord('|')
        ds #ord('-')
        ds #ord('-')
        ds #ord('p')
        ds #ord('e')
        ds #ord('r')
        ds #ord('l')

        ds #0

        ds #27  ; ESC
        ds #ord('[')
        ds #ord('0') ; row 1
        ds #59       ; semicolon
        ds #ord('0') ; column 0
        ds #ord('H') ; set display mode

        ds #ord('C')
        ds #ord('P')
        ds #ord('U')
        ds #ord('<')
        ds #ord('-')
        ds #ord('-')
        ds #ord('-')
        ds #ord('S')
        ds #ord('P')
        ds #ord('I')
        ds #ord('-')
        ds #ord('-')
        ds #ord('>')
        ds #ord('L')
        ds #ord('C')
        ds #ord('D')

        ds #ord('C')
        ds #ord('P')
        ds #ord('U')
        ds #ord('<')
        ds #ord('-')
        ds #ord('>')
        ds #ord('P')
        ds #ord('S')
        ds #ord('R')
        ds #ord('A')
        ds #ord('M')
        ds #ord('.')
        ds #ord('c')
        ds #ord('o')
        ds #ord('d')
        ds #ord('e')

        ds #0

        ds #27  ; ESC
        ds #ord('[')
        ds #ord('0') ; row 1
        ds #59       ; semicolon
        ds #ord('0') ; column 0
        ds #ord('H') ; set display mode

        ds #ord('>')
        ds #ord('>')
        ds #ord('L')
        ds #ord('E')
        ds #ord('D')
        ds #ord('s')
        ds #32       ; SPACE
        ds #ord('f')
        ds #ord('o')
        ds #ord('r')
        ds #ord('e')
        ds #ord('v')
        ds #ord('e')
        ds #ord('r')
        ds #ord('<')
        ds #ord('<')

        ds #ord('>')
        ds #ord('>')
        ds #ord('.')
        ds #32       ; SPACE
        ds #ord('.')
        ds #32       ; SPACE
        ds #ord('.')
        ds #32       ; SPACE
        ds #32       ; SPACE
        ds #ord('.')
        ds #32       ; SPACE
        ds #ord('.')
        ds #32       ; SPACE
        ds #ord('.')
        ds #ord('<')
        ds #ord('<')

        ds #0

        ds #27  ; ESC
        ds #ord('[')
        ds #ord('1') ; row 1
        ds #59       ; semicolon
        ds #ord('0') ; column 0
        ds #ord('H') ; set display mode

        ds #ord('>')
        ds #ord('>')
        ds #ord('o')
        ds #32       ; SPACE
        ds #ord('o')
        ds #32       ; SPACE
        ds #ord('o')
        ds #32       ; SPACE
        ds #32       ; SPACE
        ds #ord('o')
        ds #32       ; SPACE
        ds #ord('o')
        ds #32       ; SPACE
        ds #ord('o')
        ds #ord('<')
        ds #ord('<')

        ds #0

        ds #27  ; ESC
        ds #ord('[')
        ds #ord('1') ; row 1
        ds #59       ; semicolon
        ds #ord('0') ; column 0
        ds #ord('H') ; set display mode

        ds #ord('>')
        ds #ord('>')
        ds #ord('.')
        ds #32       ; SPACE
        ds #ord('.')
        ds #32       ; SPACE
        ds #ord('.')
        ds #32       ; SPACE
        ds #32       ; SPACE
        ds #ord('.')
        ds #32       ; SPACE
        ds #ord('.')
        ds #32       ; SPACE
        ds #ord('.')
        ds #ord('<')
        ds #ord('<')

        ds #0
        ds #0

lcdreg: equ #0x7ff
This entry was posted in Electronics. Bookmark the permalink.

2 Responses to Driving an LCD module from an FPGA

Comments are closed.