RC2025 – Part 4 – Bit-banging a NES controller using Z80 assembly language

In my last post, I wrote about using MS-Basic on my RC2014 to bit-bang an NES controller. In this post, I want to use Z80 assembly language to do the same. I want to be able to press a button on the controller and have its name printed on the serial terminal.

The first thing we need to do is set up a few things.

The code will need to run at 0x9000, so an ORG statement will tell the assembler to build the code to run from there.

        ORG $9000

The NES controller module will be located at the Z80 IO port 1.

PORT    EQU $01

The Clock bit is 1, the Latch bit is 2, and the Data bit is 1.

CLOCK   EQU $01
LATCH   EQU $02
DATA    EQU $01

I’ll be using Stephen Cousins’ SCM on the RC2014. The SCM API provides a routine to print a string, so I’ll be using that later on. This is API call 0x06.

OUTPUT_LINE EQU $06

The first thing the code will need to do is to pulse the Latch line low, high, low. This will capture the current state of the buttons being pressed on the NES controller.

start:
        ld a, 0
        out (PORT), a
        ld a, LATCH
        out (PORT), a
        ld a, 0
        out (PORT), a

Now we need to loop 8 times to capture each of the 8 bits the NES controller is going to be sending us.

        ld b, 8 
loop:
        djnz loop

Inside the loop, we first need to read the Data line to get the current bit. We mask out all but the Data bit and then see if it is 0 or not. If it’s not zero, skip the next block of code.

        in a, (PORT)

        and DATA
        jr nz, .skip

If a button is being pressed, we need to look up the name of the button in a lookup table and print it out using the SCM API. To do this, we get the current iteration minus 1 and store it in the HL register. As this is a 16 bit register, we need to set the L register to the iteration value, and H to 0. We add this value to the address of the lookup table to get the address of the string to print. We then pass this to the SCM API.

        ld de, lookuptable  ; point to the lookup table
        ld a, b             ; put the current iteration from b into a
        dec a               ; delete 1 to make it zero based
        ld l, a             ; place the interation in l
        ld h, 0             ; zero h, hl should now be value of the iteration
        add hl, hl          ; multiply by 2 (size of address)
        add hl, de          ; add to base address of table
        ld e, (hl)          ; get low byte of string address to use
        inc hl              ; point to high byte
        ld d, (hl)          ; get high byte of string address
        ld c, OUTPUT_LINE   ; SCM output line
        push bc             ; save the bc registers to the stack
        rst $30             ; Call SCM API
        pop bc              ; restore the bc registers from the stack
lookuptable:
        dw right_txt
        dw left_txt
        dw down_txt
        dw up_txt
        dw start_txt
        dw select_txt
        dw b_txt
        dw a_txt

a_txt:      db "A",5,0
b_txt:      db "B",5,0
select_txt: db "Select",5,0
start_txt:  db "Start",5,0
up_txt:     db "Up",5,0
down_txt:   db "Down",5,0
left_txt:   db "Left",5,0
right_txt:  db "Right",5,0

Next, we pulse the CLOCK line high to low before we end the loop.

.skip:
        ld a, CLOCK
        out (PORT), a
        ld a, 0
        out (PORT), a

Finally, we loop back to the very start of the program.

        jr start

The complete Z80 assembly language program

Here’s the code as a single program that can be assembled using the sjasmplus assembler.

 ; A simple button reading program for the RC2014 Z80 computer running SCM
 ; Robert Price - 15th October 2025

        ORG $9000

; The Z80 port address to use for the controller interface.
PORT    EQU $01

; The bit masks for the controller interface lines.
CLOCK   EQU $01
LATCH   EQU $02
DATA    EQU $01

; The SCM API value to output a line.
OUTPUT_LINE EQU $06

start:
; pulse the LATCH line low to high and back to low again.
        ld a, 0
        out (PORT), a
        ld a, LATCH
        out (PORT), a
        ld a, 0
        out (PORT), a

; setup the loop counter to read 8 buttons.
        ld b, 8             ; 8 buttons to read. This will be decremented to 0.
loop:
; read the controller button states
        in a, (PORT)        ; read the DATA line

        and DATA            ; mask out all but DATA bit
        jr nz, .skip        ; skip if a button was not pressed

; print out the button pressed using a lookup table.
        ld de, lookuptable  ; point to the lookup table
        ld a, b             ; put the current iteration from b into a
        dec a               ; delete 1 to make it zero based
        ld l, a             ; place the interation in l
        ld h, 0             ; zero h, hl should now be value of the iteration
        add hl, hl          ; multiply by 2 (size of address)
        add hl, de          ; add to base address of table
        ld e, (hl)          ; get low byte of string address to use
        inc hl              ; point to high byte
        ld d, (hl)          ; get high byte of string address
        ld c, OUTPUT_LINE   ; SCM output line
        push bc             ; save the bc registers to the stack
        rst $30             ; Call SCM API
        pop bc              ; restore the bc registers from the stack

.skip:
; pulse the CLOCK line to read the next button.
        ld a, CLOCK
        out (PORT), a
        ld a, 0
        out (PORT), a

; loop 8 times to read all buttons.
        djnz loop

; forever loop to read buttons again.
        jr start



; the lookup table stores the addresses of the text strings for each button.
lookuptable:
        dw right_txt
        dw left_txt
        dw down_txt
        dw up_txt
        dw start_txt
        dw select_txt
        dw b_txt
        dw a_txt

; The text strings to print for each button.
; each string is terminated with a CR (5) and a null (0).
a_txt:      db "A",5,0
b_txt:      db "B",5,0
select_txt: db "Select",5,0
start_txt:  db "Start",5,0
up_txt:     db "Up",5,0
down_txt:   db "Down",5,0
left_txt:   db "Left",5,0
right_txt:  db "Right",5,0

Once assembled, I convert it to Intel Hex using z88dk-appmake and send it to my RC2014 running SCM. It is then executed using g 9000 .

It is very fast code, so even the slightest tap of a button will register multiple times.