Close

Emulating the 6850 With a PSoC

A project log for 3-Chip Z80 Design

Combining a Z80 retro design with a modern PSoC CPU.

land-boardscomland-boards.com 10/20/2019 at 15:000 Comments

This should be the final piece of work to get Grant's 7-chip Z80 design to run on this hardware.

This is arguably the "hardest" part of the port since the 6850 emulator does not yet exist. I've gone through the front end details of creating Z80 peripheral chips in a lot of details for the PIO (although the series is not yet complete) so I will spare the details here. Here's the first log of that series (Z80 Peripheral Emulation Guide for the PSoC). Since the 6850 is more or less a very cut down version (not really a version) of the SIO we will start from that design, clone it, and change the functions that need changes to match. 

The significant differences between the SIO and the 8650 include:

FeatureSIO6850
Ports21
Command/Status InterfaceComplicated requiring a write then read or write cycleSimple with single control/status location
InterruptsUses Interrupt AcknowledgeInterrupts but no Interrupt Acknowledge hardware

Modify Z80_IO_Handle.c

We've already put #define conditional selection in this file to remove the SIO so we just need to clone that for the 6850 so let's start by copying that code and deleting what we don't need. Also, we need to use the proper #define USING_6850 to select the code when there's a 6850 being emulated. That should all happen inside the HandleZ80IO() function. That looks like this:

#ifdef USING_6850
        case M6850_D:
            if (ioCrtlRegVal == REGULAR_READ_CYCLE)             // regular read cycle
            {
                M6850ReadData();
                return;
            }
            else if (ioCrtlRegVal == REGULAR_WRITE_CYCLE)      // regular write cycle
            {
                M6850WriteData();
                return;
            }
            break;
        case  M6850_C:    // Control register
            if (ioCrtlRegVal == REGULAR_READ_CYCLE)             // regular read cycle
            {
                M6850ReadStatus();
                return;
            }
            if (ioCrtlRegVal == REGULAR_WRITE_CYCLE)      // regular write cycle
            {
                M6850WriteCtrl();
                return;
            }
            break;
#endif

Of course, as seen by four yellow exclamation boxes, we now broke the compilation since we've created a bunch of function calls to handle the Z80 access which don't yet exist. 

Let's go create the four routines we need to handle these conditions. If we are careful enough the functions in main which call these handlers will still work. If we need to we can add some additional conditional compilations to handle them. In all likelihood we will have to modify the calling routines given that the SIO is interrupt driven for receive and this one may be different (yet to look at the details enough to see - it may be nearly the same). At the very least, interrupt acknowledge cycles will need to be different.

Low Level 6850 Handler

To do this, create two new source files in PSOC Creator (Z80_6850_Emul.c and .h). Liberally copy the pieces from the SIO equivalent and change what is necessary. Leave as much as possible the same to minimize changes to calling routines. Generally speaking, this means to replace the SIO_A with M6850_ in most places.

There are two lines we have to pay particular attention to. These determine which bits are used for the calling routines to test receive character ready and set RTS. For the SIO, these lines are:

#define SIOA_CHAR_RDY   0x1
#define SIO_RTS 0x2

 It's a bit hard to read in the datasheet but here's the values of the registers in the 6850:

The status register bit 0 is receiver data full. That's convenient since that's exactly what we have for the SIO so we won't change that #define value. But there's no bit which directly sets the RTS and we do need hardware handshake for flow control since the Z80 is much slower than a USB block transfer. The control register in the 6850 handles the RTS in a slightly different manner:

We'll have to look closer at Grant's assembly code INTMINI for the answer on how these bits need to be set but for the moment let's just make a mask and define the bits for the four patterns. This will replace the RTS code above and may necessitate some changes in the main( ) calling code.

The top of the .c file now looks like:

#include <project.h>
#include "Z80_6850_emul.h"
#include "Z80_IO_Handle.h"

#define SIOA_CHAR_RDY               0x01
#define M6850_INT_RTS_MASK          0x60
#define M6850_RTS_LOW__INT_DIS      0x00
#define M6850_RTS_LOW__INT_EN       0x20
#define M6850_RTS_HI__INT_DIS       0x40
#define M6850_RTS_LOW__INT_DIS_BK   0x60

volatile uint8 M6850_Ctrl;
volatile uint8 M6850_Status;
volatile uint8 M6850_DataOut;
volatile uint8 M6850_DataIn;

Next, let's make stubs for the four missing routines. Since the data ready flag is in the same place the Serial receive code can be easily created by copying it from the SIO and changing the variable names. The status bits are the same.

///////////////////////////////////////////////////////////////////////////////
// M6850ReadData(void)- Z80 is reading data from Serial Port

void M6850ReadData(void)
{
    Z80_Data_In_Write(M6850_DataIn);
    M6850_Status &= 0xFE;                              // No Rx Character Available
    ackIO();
}

 The Serial output routine is similarly pretty easy.

///////////////////////////////////////////////////////////////////////////////
// void M6850WriteData(void) - Send a single byte from the Z80 to the USB

void M6850WriteDataA(void)
{
    uint8 buffer[64];
    uint16 count = 1;
    while (0u == USBUART_CDCIsReady());
    buffer[0] = Z80_Data_Out_Read();
    USBUART_PutData(buffer, count);
    ackIO();
}

The read status function is much simpler than the SIO since it does not use indirection and only returns the single status value. It no longer gets called from the routine which passes it the register offset so the passed value changes to a void. In fact, it becomes nearly trivial although it may require clearing a flag later on (probably not since reading the data register has already cleared the receive character flag).

///////////////////////////////////////////////////////////////////////////////
// void M6850ReadStatus(void) - Read the Status registers

void M6850ReadStatusA(void)
{
    Z80_Data_In_Write(M6850_Status);
    ackIO();
}

Similarly, writing to the control register is much simpler since there aren't two steps to do the write. For now, we'll just include the basic operation of setting the variable remembering that we need to make control register writes perform the operation of the control (we'll touch this later on). Here's what the routine looks like (without adding the side effects of setting control codes).

///////////////////////////////////////////////////////////////////////////////
// void M6850WriteCtrl(void) - Write to the SIO Port A control registers

void M6850WriteCtrlA(void)
{
    uint8 regNum;
    M6850_Ctrl = Z80_Data_Out_Read();
    // TBD - This is where the side effects of changing control codes are handled
    ackIO();
}

Next, let's add the function prototypes for these operations to the .h file.

void M6850ReadData(void);
void M6850WriteData(void);
void M6850ReadStatus(void);
void M6850WriteCtrl(void);

Also, add this .h file to the growing list of peripherals in the Z80_IO_Handle.c file.

#include <project.h>
#include "FrontPanel.h"
#include "Z80_IO_Handle.h"
#include "Z80_SIO_emul.h"
#include "Z80_PIO_emul.h"
#include "Z80_6850_emul.h"
#include "Hardware_Config.h"

Running the PSOC Creator build we can see that this compiles without error. That's a great start and a good point to save the current build off to GitHub with a comment that it is WIP.

Remember this won't work as it currently is because we've not yet implemented the write control register function to add the side effects.

In the next part we'll go through the remaining details of 6850 emulation and try the code to see if it runs.

Discussions