5

I’m trying to communicate with my Z80 computer (RC2014) using a wireless radio. The easy way to do this is to use something like an ESP8266 (and there’s even an RC2014 module for this), but where’s the fun in that?

I have tried to do this using a simple 433MHz receiver and software decoder (written by me, based on an Arduino ASK decoder). I thought that I might be able to accomplish this with a simple IO port (SC129), but speed and timing seem to be a limiting factor. I could certainly look for a more optimal hardware solution to decode the signal from the 433 receiver, but I thought perhaps I could look into using something like LoRa.

My next thought was, can you use a LoRa module such as the RFM95 on a Z80? It uses SPI. I asked a related question on EE SE. I’m aware that SPI has been around for a while, but is this something you can do with a Z80? Or would an older technology [than LoRa] be much more practical?

I want to turn a simple switch on/off, and read back simple sensor data.

2
  • Using the 433 MHz transmitter / receiver (hardware decoding) ought to be straightforward (for example, sending 9600 baud asynchronous serial communication through that system (or slower)). The receiver has the crucial AGC built-in. Jul 10, 2022 at 23:33
  • @PeterMortensen Any tips on hardware decoding for 433 MHz transmitter / receiver? I tried software encoding/decoding, but at 12 milliseconds per iteration, it seems impractical or maybe impossible with software. Jul 24, 2022 at 17:01

2 Answers 2

13

SPI is eventually the most simple interface to implement. It was designed especially to work with low end processors. I tend to use SPI for all my micro controller designs (*1).

Unless one intends to use some dedicated hardware, like a shift register (useful if speed is of concern), all it needs are 3 port bits for the data interface, plus one port bit per device connected (*2) for it's basic signals:

  • MOSI - Out - Master Out Slave In, the data the CPU sends out
  • MISO - In - Master In Slave Out, the data returned by external hardware
  • SCLK - Out - Serial Clock, a signal to be toggled once per bit

and

  • SS/CS - Out - Slave Select, also called Chip Select, selecting the external interface.

The great advantage is that SPI does not require any specific timing (*3) as long as the basic sequence is followed. It can be handled as slow as a bit per day. Any interface able to provide three output lines and one input line can be used. Including a classic PC parallel port. Well, or a Z80 PIO.


Let's say we have a Z80-PIO connected to a Z80-CPU at I/O address 20h (*4) and use port A with its first 4 lines assigned as (*4):

  • A0 - MISO - In
  • A1 - MOSI - Out
  • A2 - SCLK - Out
  • A3 - SS/CS - Out

We also assume the device to be operated in Mode 0 (CPOL=0, CPHA=0) as the RFM95 does. For other modes this may need to be adapted.

A subroutine to initialize this setup may look like this:

(All references relate to the Z80 Family CPU Peripherals User Manual UM008101-0601)

[Caveat, this has been just hacked down, so no guarantee for this code being bug free or working at all - not at least due to actual Beer-Level :)]

PIO1   EQU  20h     ; First PIO
PIO1AD EQU  PIO1+0  ; Data Register
PIO1AC EQU  PIO1+2  ; Command Register

SPI_INIT:
; Initialized SPI transfer on port A
; A destroyed

       LD   A,011001111b  ; Set Mode 3, Control (see p.189)
       OUT  (PIO1AC),A
       LD   A,000001110b  ; Line 1/2/3 as output all other input (see p.190) (*5)
       OUT  (PIO1AC),A
       LD   A,000000111b  ; Interrupt control, no interrupt (see p.191)
       OUT  (PIO1AC),A
       LD   A,000001111b  ; Set CS high to deselect
       OUT  (PIO1AD),A
       RET

By calling SPI_INIT the interface will be put into operating condition. Since sending and receiving is interleaved a single routine can be used:

SPI_CLK EQU  4
SPI_CS  EQU  8 

SPI_SENDREC:
; Starts or continues an SPI transaction by SENDing a byte
; while RECeiving the peripherals response at the same time
; Called with byte to send in A
; Returns with byte received in A
       PUSH DE
       PUSH BC
       LD   D,A           ; Byte to send in D
       LD   A,000000000b  ; CS Low, Clock Low
       OUT  (PIO1AD),A
       LD   B,8           ; 8 bits per byte
BITLOOP:
       LD   A,000000000   ; Clock Low, Data Low
       RL   D             ; Isolate bit to send
       JR   NC,BCLR       ; Is it a zero bit?
       LD   A,000000100   ; No? Then set data high
BCLR:
       OUT  (PIO1AD),A    ; Set data value
       MOV  C,A           ; Save value with clock low (*6)
       OR   A,SPI_CLK     ; Set clock
       OUT  (PIO1AD),A    ; Data and Clock
       IN   A,(PIO1AD)    ; Read input data
       RRCA               ; Isolate input bit to carry
       RRCA
       MOV  A,C           ; Restore Data with clock cleared (*7)
       OUT  (PIO1AD),A    ; Clear Clock (*6)
       RL   E             ; Insert bit from Carry into E
       DJNZ BITLOOP       ; Next bit?

       LD   A,E           ; Return value in A
       POP  BC
       POP  DE
       RET

The important part is, as mentioned, to stay with the sequence:

  1. Activate CS
  2. Apply MOSI
  3. Raise Clock
  4. Read MISO
  5. Lower Clock
  6. Repeat step 2 thru 5 for all bits/bytes to be transferred
  7. Deactivate CS

Note that the routine does not perform step 7, it leaves chip select active. This is the base for multi-byte transfer, important for the RFM95, as each of it's transfers always starts with an address followed by one or more data bytes. CS has to stay active during such a transfer over all bytes to be written (and read), acting as transaction marker.

The SENDREC function is written in a way that multiple bytes can be chained by calling it in sequence as often as needed. Of course we now need a another function to end a transfer:

SPI_DONE:
; Ends an SPI transaction
; A destroyed
       LD   A,000001111b  ; Set CS high to deselect
       OUT  (PIO1AD),A
       RET

Let's for example put the RFM95 into LoRa mode. This requires to set the high bit of the Operation Mode Register at address 01h needs (See p.102 of the manual). To do so we need to send one byte with the registers address, followed by the new value, all in in one transaction handled by CS:

       ...
       LD   A,01h         ; Address of Operation Mode Register
       CALL SPI_SENDREC   ; Start transaction and send address
       LD   A,010000000b  ; Set LoRa mode + Sleep
       CALL SPI_SENDREC   ; Continue transaction and send data
       CALL SPI_DONE      ; End transaction
       ...

Now the RFM95 is in LoRa mode (*7) - Easy, isn't it?

So while this function may not be super speed (*8,*9), it might be fast enough for 'some switching and sensor read out'.


Bottom Line: Z80, or any other classic CPU is quite capable of doing SPI (within reason).


P.S.: The very same code could ofc be used with the 245/374 based RC2014 I/O. Addresses need to be adjusted and initialization could be reduced to setting a default state. In fact, by using the same bit (2^0) for MOSI and MISO, one or two instructions could be saved.

And yes, it can be done in Pascal using the port[] array (*10). Of course, a bit slower, like 2-5 times. Then again, packaging above assembly into functions would eliminate most of that.


P.P.S.: This made me smile:

is this something you can do with a Z80? Or would an older technology be much more practical?

A Z80 is about as old as it gets (1978), not to mention the TTL used by the 2014's digital I/O


*1 - Foremost to add MMC/SDC support, as the basic interface/protocol spoken even with modern terabyte sized cards is still SPI. But also any other interface or other controllers.

*2 - Using a decoder can reduce this when more than 2-3 devices are connected to a single interface.

*3 - Note that most devices have an upper speed limit, usually in the 5..40 MHz range, so not even remotely relevant (*9).

*4 - Doesn't really matter, just assumed for simplicity of the example code.

*5 - Unused lines should always be set as input.

*6 - With strict Mode 0, this could be deleted, as data is only valid during raising clock.

*7 - Well, in real use you may want to do this twice, just in case that the device was not in Sleep mode first, but that's another story.

*8 - I bet there is still room for improvement - this is, as said, just a quick writeup how it could work, made for the fun of it. Not much thought put into.

*9 - A rough estimate is 7-800 clocks per byte transferred, which gives a maximum transfer speed of around 50 KiB/s on a 4 MHz Z80. Which is about the range one can expect from a Z80 system anyway.

If that is too low, some external shift register and a some glue logic might be the way to go.

*10 - Using the RC2014 digital I/O at address 00h this might look a bit like this:

program SetLoRa;
{$U+}

const
  InPort:  Integer = 0;
  OutPort: Integer = 0;
  MOSI:    Byte    = 1; /* Bit value for output data */
  MISO:    Byte    = 1; /* Bit value for input data */
  CLOCK:   Byte    = 2; /* Bit value for clock handling */
  CS:      Byte    = 8; /* Bit value for chip select */

function SPI_SendRec(Value: Byte):Byte;
  var
    i:     Byte;
    bit:   Byte;
    rdval: Byte;

begin

  port[OutPort] := 0;       /* Clear Clock, activate CS */
  rdval :=0
  for i:= 1 to 8 do
  begin
    bit := (Value and 128) shl 7; /* isolate next data bit */
    Value := Value shl 1;         /* prepare follow up bit */
    port[OutPort] = bit;          /* set MOSI to data bit */
    port[OutPort] = bit+ CLOCK;   /* Raise CLOCK */
    rdval := rdval shl 1;         /* Make room for input data */
    rdval := rdval or (port[InPort] and 1); /* Move input bit */
    port[OutPort] = bit;          /* Clear Clock but keep data (*5) */
  end;
  SPI_SendRec := rdval;     /* Retrun what has been read */

end;

procedure SPI_Done;
/* Procedure to end a transaction */
/* or first time port initialisation */
begin

  port[OutPort] := CS + CLOCK + MOSI; /* disable operation */

end;

  var Dummy: Byte;

begin
/* Program to set RFM95 into LoRa Mode */

  SPI_Done;                   /* Initialize Port */

  Dummy := SPI_SendRec(01);   /* Register address */
  Dummy := SPI_SendRec(128);  /* Value to be set  */
  SPI_Done;                   /* End transaction */

end.

(And yes, I was bored :))

21
  • 1
    SPI was not designed to work on low end processors. Devices/boards/chips that used synchronous serial communications already existed before SPI was invented. And processors communicated with them in software. Just like many MCUs had UART/USART peripherals in them to ease asynchronous communication, Motorola decided to design in a peripheral that allowed configurable byte oriented synchronous communications and decided to call it SPI. It just off-loaded the software bit-banging to hardware module inside the MCU.
    – Justme
    Jul 9, 2022 at 22:13
  • 1
    SPI was designed to allow use of simple standardized peripherals with minimal effort. SPI does not need any 'hardware module'. These are developments done way after SPI was established - much as you already not, to offload some tasks.
    – Raffzahn
    Jul 9, 2022 at 22:29
  • 1
    No, it's the exact same thing than with UART. Asynchronous serial comms existed before UARTs existed and when UARTs existed they became the de facto devices to implement async serial comms, and they got integrated to MCUs. Synchronous serial comms also existed before SPI and was not standardized. When first MCU with SPI peripheral came to market (maybe MC6805 in 1983?) then you could use MCU SPI module to communicate with the same old non-standard logic chips but it also enabled comms between two MCUs with SPI modules in a non-standard way. Which resulted into boards with SPI interface.
    – Justme
    Jul 9, 2022 at 22:46
  • 1
    @Justme But there is not need for an 'SPI module' same way as there is no need for a UART. exactly as you argue. And it works exactly as that. SPI is a protocol (like RS232), not a device.
    – Raffzahn
    Jul 9, 2022 at 23:17
  • 3
    @Justme " use the UART or SPI" Erm, I guess that is where it goes wrong: A UART is a device implementing a serial protocol. SPI is not a device. SPI is an interface and protocol. What you talk about is an SPI-Module within an MCU/SoC which implements that interface (like an UART). These are two different items that need to be kept apart when talking about.
    – Raffzahn
    Jul 10, 2022 at 0:10
6

Of course you can interface a Z80 with an SPI device, just like you can with any CPU or MCU that has no built-in SPI. All you need is a GPIO port of some sorts and control the pins in software.

This is how CPUs and MCUs interfaced with synchronous serial devices before Motorola decided to integrate a configurable peripheral to interface with synchronous serial devices into their MCUs and call it SPI.

It is also possible to build a hardware card/board/module that sits on the CPU bus and implements a synchronous byte oriented serial interface for the Z80.

3
  • I just have the need to add that original Z80 had no GPIO so you need to add address decoder + LATCH register (at least 4 or 3 bits) and implement SPI in SW ... still easily doable however with low Z80 clock and pure SW implementation the speed will be slow ...
    – Spektre
    Jul 10, 2022 at 3:44
  • @Spektre The assumption is that there is some IO on the bus to connect to and that's a non-issue. The RC2014 computer platform does have I/O modules that can be used. What's even more important is that the RC2014 platform uses 5V levels, and the LoRa module uses 3.3V levels, so there must be some form of voltage level conversion between the 5V host and 3.3V LoRa module so they are compatible and don't get damaged.
    – Justme
    Jul 10, 2022 at 11:29
  • @Spektre: If one has a single-bit input port for MISO, and a single-bit output port device select, an I/O address decode that's active for two addresses could be used as CLK, and an address bit that was ignored by the address decoder could be used for MOSI.
    – supercat
    Jul 10, 2022 at 13:27

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .