RC2026 – Part 1 – Using a SPI LED Matrix from a RC2014

In this post I show how to use the SPI protocol from Z80 assembly language so an RC2014 computer can control a MAX7219 LED matrix.

MAX7219 connected to an RC2014

For the 2026 RetroChallenge I am exploring using SPI devices from a Z80 based RC2014 computer.

The first device I want to use is an 8×8 LED matrix (Amazon affilate link). Shelia Dixon has previously developed an 8×8 LED matrix display module for the RC2014, however this is directly interfaced to the RC2014 using two IO ports. I will be using a single port and communicating with it by bit banging the SPI protocol.

MAX7219 LED Matrix

The 8×8 LED matrix I’m using is a MAX7219 to handle the SPI communication and control the LEDs. It uses 5 wires from the RC2014

  • VCC – +5V
  • GND
  • CS – Chip Select – This is active low
  • CLK – Clock – Data is loaded when the clock goes from low to high
  • DIN – Data In – 1 bit of data

I’m using the SC129 Digital IO module from Small Computing Central on the RC2014 for the digital output. This provides pins suitable for dupont connectors. You could use the official RC2014 Digital IO module, but you would have to modify it to add connections. In this case, it’s simplier to use the SC129 module. The SC129 is connected to the LED matrix module as follows…

SC129MAX7219 LED Matrix module
+5VVCC
GNDGND
OUT D0DIN
OUT D1CLK
OUT D2CS

The MAX7219 gives us this iming diagram to follow when we want to send it data.

So to send data, we need to first take the CS line low. This tells activates the MAX7219 and it knows to listen for incoming data. Next we set DIN and take CLK high. This will load the single bit of data on DIN into the MAX7219. Now we take CLK low in preparation for the next bit to be sent. We repeat this 8 times in total to send a byte of data.

The MAX7219 expects two bytes of data to be sent each message. The first byte is an address, the second byte is the data value.

MX7219 address and data

Sending SPI data using the Z80

So how do we do this on the Z80? Well first we’re going to define some constants to help.

The SC129 is using IO Port 1.

OUTPUT_PORT     EQU $01

As we said earlier, the Data bit is D0, the Clock is D1, and the Select line is D2. We call the Data bit MOSI, which stands for Master Out Slave In.

MOSI            EQU %00000001
CLK             EQU %00000010
SELECT          EQU %00000100

As you can see, we’re using binary to define the location. When we want to set the bit in one of these positions we can use an OR statement, and when we want to clear the bit we can AND the Compliment (the inverse).

So to enable the MAX7219 we want the SELECT line to be low, the CLK line to be low, and we will take MOSI high. We can use the following Z80 subroutine to do this.

select_max7219:
    ld a, 0 & ~SELECT & ~CLK | MOSI
    out (OUTPUT_PORT),a
    ret

To disable the MAX7219 we want the SELECT line to be high, the CLK line to be low, and we will take MOSI high.

deselect_max7219:
    ld a, 0 | SELECT & ~CLK | MOSI
    out (OUTPUT_PORT),a
    ret

To send a byte, need to send 8 bits of data. We pass the byte we want to send in the d register. To send a bit of data we know we need to take SELECT and CLK low. We can prepare this and save it to the e register to reuse each bit. We are going to be shifting data into the MOSI bit, so before we save it to e, we shift it to the right. Now for each bit we want to send, first load the a register with the value of the cached e restier. We then pick up the next bit from the d register by shifting d to the left. This pushes the bit we want to send into the carry flag. We retrieve it by shifting the a register to the left so it sets the value of the carry flag into the MOSI bit. We send this, before ORing the CLK bit to take it high, and sending that.

That was quite a wordy explaination, but here’s the code so hopefully it makes more sense.

transmit_max7219_byte:
    push bc
    ld b, 8
    ld a, 0 &~SELECT &~CLK | MOSI
    rra
    ld e, a
.transmit_max7219_bit:
    ld a, e
    rl d
    rla  
    out (OUTPUT_PORT), a
    or a, CLK
    out (OUTPUT_PORT), a
    djnz .transmit_max7219_bit
    pop bc
    ret

To send a two byte instruction to the MAX7219 is expecting we need combine the subroutines we’ve built. We need to select the MAX7219, send two bytes, then deselect it. We’ll pass the two bytes in registers b and c.

max7219_write:
    call select_max7219
    call transmit_max7219
    call deselect_max7219
    ret

transmit_max7219:
    ld d, b
    call transmit_max7219_byte
    ld d, c
    call transmit_max7219_byte
    ret

Controlling the MAX7219 using SPI

To tell the MAX7219 to turn on or off a LED we send a column address in one byte, and the column data in the next. So 8 columns of bits. However, we also need to send some additional command codes so the MAX7219 knows how to behave.

A full list of commands can be found in the MAX7219 datasheet.

The MAX7219 can interface with 7 segment displays, but we don’t want any of this functionality, so we need to tell it not to decode any incoming display commands. We also want to make sure it is running on a low duty cycle to reduce power usage.

These are the commands I use to initalise the display.

    ld bc, 0x0C00       ; shutdown mode, this ensures the LEDs are off.      
    call max7219_write
    ld bc, 0x0900       ; no decoding
    call max7219_write
    ld bc, 0x0A01       ; 3/32 intensity
    call max7219_write
    ld bc, 0x0B07       ; scan all 7 columns
    call max7219_write
    ld bc, 0x0F00       ; No display test
    call max7219_write

Once setup we can send the image. We send the column number in register b, and the column data in register c. So to send a cross I can use the following.

    ld hl, cross
    ld b, 8
.send_image:
    ld c, (hl)
    inc hl
    call max7219_write
    djnz .send_image
cross:
    dc %00000000
    dc %01000010
    dc %00100100
    dc %00011000
    dc %00011000
    dc %00100100
    dc %01000010
    dc %00000000

Finally I turn the display on.

    ld bc, 0x0C01       ; normal operation - turn on the display         
    call max7219_write

Building, deploying, and running on the RC2014

To assemble the code I’m using the SjASMPlus Z80 assembler on my Macbook Pro. The RC2014 is running SCM, so to transfer the assembled code, I need to convert it into Intel hex before sending it across. I use z88dk-appmake to do this. Once across, I run the application using the “g” command in SCM.

So to assemble my matrix.s source code to address $9000 and send it to my RC2014 using the serial device at /dev/tty.usbmodem03591, I would use the following. First, I would make sure I am connected to the RC2014 over serial using the following…

% minicom -c on --device /dev/tty.usbmodem03591

Next I would open another Terminal shell, and run the following…

% sjasmplus matrix.s --raw=matrix.z80 
% z88dk-appmake +hex --org 0x9000 -b matrix.z80
% cat matrix.ihx > /dev/tty.usbmodem03591

Going back to minicom, I execute the code using the following…

g 9000

This will run the transmitted code, and my image will appear on the LED matrix.