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.