Close

Z80 SIO Emulation (part 2)

A project log for 3-Chip Z80 Design

Combining a Z80 retro design with a modern PSoC CPU.

land-boardscomland-boards.com 10/11/2019 at 19:510 Comments

SPoC Code Side

The PSoC will need to poll the IO_Stat_Reg and wait until it sees IORQ* asserted along with either CPURD* or CPUWR*. It should then read the AdrLowIn status register. Using this information, the PSoC should then perform the IO operation. 

The main( ) code is actually not idle. It's essentially polling the USB interface. I think the USB is interrupt driven so the code probably just interacts with the handshake to the interrupt. It is currently configured to loop back USB data from the receive to the transmit. I can see this on the PC when I connect to the card using PuTTY. Whatever I type gets echoed back. This will need to be intercepted and routed to the Z80 via the SIO interface.

Data, Status and Ports

There are four pins provided for an external UART, but I'm going to just toss out the SIO data from the second part for the moment. Using the PSoC debugger to single step, the first write is to the SIO A Control Register with a value of 0. This matches the ASM code.

0403   01A6 3E 00               LD    A,$00
0404   01A8 D3 02               OUT    (SIOA_C),A
0405   01AA 3E 18               LD    A,$18
0406   01AC D3 02               OUT    (SIOA_C),A

SIO Data Sheet

 This is when looking at the SIO data sheet comes into play. What does a write to the control register with a value of 0x0 do in the SIO?

The SIO is described in the document Z8420 Peripheral User Manual um0081 starting on p207. There's a lot of information in there about all of the modes that the SIO can do.

Grant's schematic connects the interrupt line from the SIO to the Z80 but that, by itself doesn't mean it is interrupt driven. The Monitor Code has the following:

0139   0062             ;------------------------------------------------------------------------------
0140   0062             ; Serial interrupt handlers
0141   0062             ; Same interrupt called if either of the inputs receives a character
0142   0062             ; so need to check the status of each SIO input.
0143   0062             ;------------------------------------------------------------------------------
0144   0062 F5          serialInt:    PUSH    AF
0145   0063 E5                  PUSH    HL

So, it looks like the hardware and code support interrupts. It looks like there may only be a receive buffer so the interrupts may only be on receive characters. I will have to dig deeper to see if that is the case. It does make sense to have interrupts on receive and wait on output since the buffer could just back up on the transmit side anyway if the host is not ready.

If it's the case that there's only input interrupts then I should be able to get outputs running without worrying about interrupt handling. The console output routine also seems to be polled and waits until the transmit is ready.

0287   0124             ;------------------------------------------------------------------------------
0288   0124             ; Console output routine
0289   0124             ; Use the "primaryIO" flag to determine which output port to send a character.
0290   0124             ;------------------------------------------------------------------------------
0291   0124 F5          conout:        PUSH    AF        ; Store character
0292   0125 3A 8A 40            LD    A,(primaryIO)
0293   0128 FE 00               CP    0
0294   012A 20 0D               JR    NZ,conoutB1
0295   012C 18 01               JR    conoutA1
0296   012E             conoutA:
0297   012E F5                  PUSH    AF
0298   012F             
0299   012F CD 42 01    conoutA1:    CALL    CKSIOA        ; See if SIO channel A is finished transmitting
0300   0132 28 FB               JR    Z,conoutA1    ; Loop until SIO flag signals ready
0301   0134 F1                  POP    AF        ; RETrieve character
0302   0135 D3 00               OUT    (SIOA_D),A    ; OUTput the character
0303   0137 C9                  RET

...

0314   0142             ;------------------------------------------------------------------------------
0315   0142             ; I/O status check routine
0316   0142             ; Use the "primaryIO" flag to determine which port to check.
0317   0142             ;------------------------------------------------------------------------------
0318   0142             CKSIOA
0319   0142 97                  SUB    A
0320   0143 D3 02               OUT     (SIOA_C),A
0321   0145 DB 02               IN       A,(SIOA_C)    ; Status byte D2=TX Buff Empty, D0=RX char ready    
0322   0147 0F                  RRCA            ; Rotates RX status into Carry Flag,    
0323   0148 CB 4F               BIT      1,A        ; Set Zero flag if still transmitting character    
0324   014A C9                      RET

The PSoC USB interface ought to be significantly faster than a Serial UART.

Some things, like bits per character, stop bits, parity don't matter for the USB-Serial interface since it's automatically taken care of by the USB transport so writes to those control values can be safely ignored (at least for the USB-Serial port). They will probably need to be taken into account for the 2nd UART interface, but a lot of that is easily handled by the PSoC UART code.

As a side note: M1* will matter when it comes to handling interrupts (from the SIO datasheet)

M1 Machine Cycle One (input from Z80 CPU, active Low). When M1 is
active and RD is also active, the Z80 CPU is fetching an instruction from
memory; when M1 is active while IORQ is active, the Z80 SIO accepts
M1 and IORQ as an interrupt acknowledge if the Z80 SIO is the highest
priority device that has interrupted the Z80 CPU.

 Hardware handshake is also not an issue for the USB-Serial port since it's also taken care of by the USB protocol.

The SIO datasheet shows that there are multiple registers which apparently are selected by a write to the first location.

These registers can be represented in C by variables.

WR0

WR1:

Re-writing Grant's SIO initialization code as psuedo-code:

; SIO Initialization code in Grant's monitor
; Set up Port A
SIOA_C = $00        ; WR0
SIOA_C = $18        ; Channel A reset

SIOA_C = $04        ; Select WR4 
SIOA_C = $C4        ; X64 clock rate, 1 stop bit, no parity

SIOA_C = $01        ; Select WR1
SIOA_C = $18        ; Rx int on all rx chars, no Tx int enable

SIOA_C = $03        ; Select WR3
SIOA_C = $E1        ; 8 bits/char, auto enable CTS, Rx Enable

SIOA_C = $05        ; Select WR5
SIOA_C = RTS_LOW    ; RTS = LOW

; Set up Port B
SIOB_C = $00        ; Select WR0
SIOB_C = $18        ; Channel A reset

SIOB_C = $04        ; Select WR4
SIOB_C = $C4        ; X64 clock rate, 1 stop bit, no parity

SIOB_C = $01        ; Select WR1
SIOB_C = $18        ; Rx int on all rx chars, no Tx int enable

SIOB_C = $02        ; Select WR2 is Interrupt Vector Register
SIOB_C = $60        ; INTERRUPT VECTOR ADDRESS Value

SIOB_C = $03        ; Select WR3
SIOB_C = $E1        ; 8 bits/char, auto enable CTS, Rx Enable

SIOB_C = $05        ; Select WR5
SIOB_C = RTS_LOW    ; RTS = LOW

I think I could simply ignore any writes to the control registers since I know what they do in Grant's code. This isn't a general purpose emulator, but I don't need to support many of the SCC modes. A more general purpose emulator would need to deal with changes to the registers. All I really need to know is:

  1. Receive characters are interrupt driven
  2. Transmit characters are directly written by polling status and then writing

The one thing I do need is the WR2 value since it provides the interrupt vector address. This get supplied back to the Z80 in response to an interrupt acknowledge cycle.

Another possibility is to hard code the fact that these accesses have to come in pairs. The first access is the offset to the register in the second access. I could stuff away the register values based on the second access and hard code this as pairs in the SIO emulator. This seems like the best choice.

Discussions