Close
0%
0%

SPI Master

An SPI interface for 6502 microcomputers using discrete logic gates

Similar projects worth following
If you've ever used an AVR's SPI interface, you know it's a pretty nice software model. Most 6502 implementations bitbang SPI using a 6522 but it's slow and your computer can't do anything while transmitting data. It also means your ROM is filled with basic SPI routines. At its core, the AVR SPI system is just a shift register ring and a status register, so can I build this using discrete logic gates?

This project assumes you know about or have used SPI in some form or another. This could be with an AVR, a Raspberry Pi, or bitbanging it to a peripheral using your own 6502 design like me. This project is also focused on building a circuit for use in a very specific device: the n8 Bit Special microcomputer. This means that certain design decisions will be made to gracefully interoperate in its eventual home.

SPI

As a most basic refresher, SPI is a full-duplex serial data protocol with one orchestrator device and potentially many peripheral devices sharing some common control lines but individual device enable lines. Data is moved 8 bits at a time between orchestrator and peripheral. A typical hardware example illustrates this using 2 shift registers: one in the orchestrator and one in the peripheral, forming a circular buffer:

There are also 4 modes, each of which describes when data is sampled in which device. For details on this (and more), you can check out the SPI Wikipedia page.

Bit Banging SPI

SPI is a pretty simple protocol so its often "bit-banged": using software and some simple control lines to communicate instead of dedicated hardware. This has some disadvantages though: it's pretty slow (it's going to be significantly slower than your CPU's clock. I was able to get it to about 60khz but the CPU clock was 2Mhz!) and your CPU is dedicated entirely to the task until it's finished. It may also take a significant percentage of your computer's GPIO capability, especially with more than one device.

SPI on AVRs

AVRs have solved this by building SPI support directly into the chip itself. This hardware is exposed via 3 registers: a data register, a status register, and a control register. 

The data register is a read-write register. Writing to it initiates an SPI transfer and the data received is stored in a buffer which can be read.

The control register is a read-write register for configuring various SPI modes, roles, and speeds. It looks like this:

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
SPIESPEDORDMSTRCPOLCPHASPR1SPR0

SPR0 and SPR1 form a 2 bit clock rate divider.

CPOL and CPHA form a 2 bit mode select.

MSTR is a bit to control whether the AVR is the master or a peripheral.

DORD is a bit to specify data order (MSB vs LSB).

SPE is an SPI Enable bit.

SPIE is an SPI Interrupt Enable bit.

The status register only has 3 relevant bits, and looks like:

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
SPIFWCOLxxxxxSPI_2x

SPI_2x is a bit to double the speed (it basically forms a 3 bit clock divider)

WCOL is a read-only bit if there a write collision occurs. In other words, if data is written to the data register while a transfer was already in progress.

SPIF is a read-only bit that is set when a transfer is complete and is cleared when SPIF bit and data register are read.

The AVR also has some dedicated SPI control lines: MISO, MOSI, CLK and you can use any GPIO pins as device select lines to your peripherals.

SPI Master

I want to achieve something very similar for my microcomputer. I want dedicated hardware that appears as exposed registers that perform SPI tasks and notify me when they're done. I also want to do this in discrete logic. Could I build this in an FPGA? Sure. Can I also just use an AVR and use its dedicated SPI hardware? Sure. But I don't want to :)

So, how will I achieve this? Well, first I actually shed some of the requirements. My design will not have any mode select, it's going to be Mode 0 only. I'm also going to (for now) skip things like double speed and write collision.

Next I'm going to add to the requirements. SPI master will have hardware to control up to 4 SPI devices. The 6502 doesn't have any built-in GPIO and I don't want to dedicate VIA pins to this purpose so it makes sense to build this in.

The rest of the requirements remain: I want clock speed selection, I want interrupts, I want a set it and forget it data register that initiates...

Read more »

  • Part 3: Device Select

    Nate Rivard07/13/2022 at 11:17 0 comments

    Overview

    In keeping with our theme of understanding the control register, we will look at the device selection and device port subsystem. 

    In SPI systems, the orchestrator and many peripherals share some common lines (MOSI, MISO, CLK) so there needs to be some mechanism to signal which peripheral is the intended target of communication. In our control register, we have 2 bits dedicated to selecting 1 of 4 devices, SEL0 and SEL1:

    SEL1...SEL0Selected Device
    000
    011
    102
    113

    We also have one bit in our control register to turn off all devices. When DEN is HIGH, the device targeted in SEL1 and SEL0 is asserted and when DEN is LOW all devices are de-asserted. In other words, no peripheral should be listening anymore.

    Design Considerations

    This subsystem is fairly straightforward, but we still need it to:

    • select 1 of 4 possible devices via software
    • be able to completely de-assert all the devices or assert a single device via software
    • have some mechanism to connect devices to this circuit

    Design

    We will start by selecting 1 of 4 possible devices. We have already looked at using a 2-to-4 mux in our clock select circuit. This device takes 4 input lines and selects 1 of those lines as output via 2 select lines. Our device select logic, though, should actually be the opposite: we have a single "input" (we will discuss this later) that we want output on a single line out of a possible 4 lines. To accomplish this, we will use one half of a 74HC139 Dual 2-to-4 line decoder/demultiplexer (don't worry that the diagram states a 74LS139, it's being used here because it has the same footprint and function).

    You may have noticed the deliberate usage of "assert" and "de-assert" when referring to turning on or off a device. This is because SPI is active-low to assert a device, not active-high. That means for our demux, our input would actually be a constant LOW signal and that LOW would flow out to the selected device. One nice feature of the '139 is that "input" is already assumed.

    We also need a way of de-asserting all devices. In other words, we want all of the outputs to be HIGH. Another feature of the '139 is that it has an active-low ENABLE pin. When E is HIGH, all the outputs are HIGH. When E is LOW, the line selected via A0 and A1 is LOW.

    The intended method of controlling this is to use DEN. One consideration here is that on bootup, RES will go low and we want our control register to be reset. The 74HC273 we're using will zero the itself out, which means a LOW signal will be the default for DEN.

    If we connect that line straight to E on our '139, it means a device (in this case Device 0 bc SEL0 and SEL1 will also be reset as LOW) will be asserted! This could be very bad as we don't know what will be on some of our other lines like CLK, MOSI or MOSI. So ideally, we want a LOW on DEN to mean de-asserted and a HIGH to mean asserted.

    To do this, we will use one-sixth of a 74HC04 Hex-inverter. So our final design for the device selection logic is:

    Lastly, we need a way to route our signals to actual devices. A simple and flexible way to do this is to provide a user port on the board. We haven't talked about some of the signals referenced in this circuit, but here is our port:

    It provides 5V power and shared GND (only GND is truly necessary here. Devices can be self powered if they want to), MOSI, MISO, and SPI_CLK (these will be discussed when we talk about the data register) and our 4 device select lines. 

    One interesting and perhaps nice change would be to provide 4 almost identical ports:

    Here our shared lines are in identical spots across two ports but they each have a separate device select line routed. This would allow us to standardize connectors for our devices and provide power and ground to each device. 

    But for now, we will go with a single 1x9 port!

  • Part 2: Clock Select

    Nate Rivard07/01/2022 at 10:28 0 comments

    Overview

    The next subsystem we're going to examine is the clock select logic. SPI orchestrators and peripherals all share the same clock, generated by the orchestrator, to stay in-sync. Peripherals may only accept specific ranges of frequencies so the orchestrator needs the ability to generate a wide range of frequencies for better compatibility.

    Remember that in our control register we dedicated 2 bits (so 4 possible values) to clock selection: DIV0 and DIV1 (Note: the target system for this device, the n8 Bit Special microcomputer, has a main clock of 3.6MHz):

    DIV1...DIV0ResultValue @3.6Mhz
    00CLK / 21800 kHz
    01CLK / 4900 kHz
    10CLK / 8450 kHz
    11CLK / 16225 kHz

    One side effect of this is that it is impossible to run the device at the full speed of the main clock. 

    Design Considerations

    As previously stated, SPI peripherals may only accept specific ranges of clock frequencies and these limits are typically at higher clock speeds. SD cards, for example, can only be initialized in SPI mode under 400khz. Afterwards, they operate just fine in the Mhz range. With that in mind,  this clock selector should be able to:

    • operate under 400khz
    • change clock speed via software

    Design

    We will start with the circuit to generate our 4 possible clock values: the main clock divided by 2, 4, 8, and 16. You may see a very useful pattern emerge if you stare at those values long enough. Each value is half of the previous! 

    We will use this property to efficiently generate our divided clock signals using one half of a 74HC393 4-bit binary ripple counter. We will connect our main system clock to the clock input of the counter and each of the 4 outputs will be our 4 clock signals:

    Note that we tied MR line to ground (this reset line is assert high!) so this counter will run continuously and cannot be reset. This has the potential side effect of the first pulse of the divided clock being too quick after changing the clock speed but this doesn't seem dangerous.

    This device is also negative-edge triggered rather than positive-edge triggered like many of the other devices in this design. On integration, we will see if that is going to be a problem or if we will have to invert the incoming clock signal to better fit.

    Now we need a way to programatically select which of these signals is our divided clock, using the 2 lowest bits in our control register. For this we will use one half of a 74HC153 4-input multiplexer. In essence, this allows 2-lines to select a single line from our 4 inputs, exactly what we need. The other half of the device is just completely disabled so there is no interference. So our final design is:

  • Part 1: Control Register

    Nate Rivard06/29/2022 at 15:48 0 comments

    Overview

    We will start our discussion of this design with the control register because virtually every other subsystem depends on it in some way. It is the primary interface for controlling the behavior of the device. To recap, the control register is an 8-bit read/write register and this is how its bits are laid out:

    Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
    ITCxIENDENSEL1SEL0DIV1DIV0
    • DIV0 and DIV1 are a 2-bit clock divider select
    • SEL0 and SEL1 are a 2-bit device select
    • DEN is a device enable flag
    • IEN is an interrupt enable flag
    • Bit 6 is unused
    • ITC is a transfer complete flag (it is also an interrupt acknowledge flag if IEN is enabled and a transfer has completed, thus triggering an interrupt)

    Design Considerations

    There are a few things to remember when designing this control register:

    • it needs to fit in well in a 6502 memory-mapped environment
    • it must be safe to read and write
    • it must be resettable
    • other subsystems are controlled via flag values so these have to be accessible to the rest of the design without driving the shared data bus
    • on a completed transfer, the ITC flag (discussed in a future log entry) must be automatically set. This requirement causes much of the complexity of this design

    Design

    In discussing the design, we will add complexity layer-by-layer to address each of the design considerations.

    To start, we need a read/write register that fits well in a 6502 memory-mapped environment. For this basic task, I chose a 74HC273 Octal D-type positive-edge triggered flip-flop. This IC satisfies the first 2 considerations pretty well. It features:

    • 8-bit input/output
    • dedicated reset line
    • data latch on a positive-edge trigger to its clock line

    Unfortunately, this device by itself does not fit well in a 6502 memory-mapped environment. When SPI Master is not being addressed, it should be completely disconnected from the shared data bus. This particular chip doesn't have that capability.

    One option is to use a 74HC373. This device has built-in tri-state buffers that can prevent the IC from driving the data bus, but this would break the requirement that other subsystems be able to read flag values when the device isn't asserted.

    The solution I've chosen is to use a 74HC245 Octal bus transciever. The '245 is configured to permanently flow in one direction, A -> B, where A are inputs and B are outputs. Next, the '273 outputs are connected to the '245 inputs, and the '273 outputs are connected to the shared data bus.

    Other devices can connect to the lines between the two ICs, thus satisfying that design requirement. 

    Only one requirement remains, which is the ability to set/reset the ITC bit independently. This requirement is the most complex because in many cases, to the system, it should just look like a regular 8-bit register. You write values to it, you can read that value back. But in certain scenarios, when a transfer is complete (and potentially when a transfer is started), the ITC bit should be set (or cleared, respectively) by the device itself.

    To satisfy this design requirement, I've chosen to break the top bit out into its own device, one half of a 74HC74 Dual D-type positive-edge triggered flip-flop. This flip-flop has separate set and reset lines and is triggered at the same time (when their clocks are connected to the same signal) as the '273.

    On startup, the RESET line is driven low and the value is 0. When a transfer is complete, TX_COMPLETE is driven low and the value is set to 1. Otherwise, the user can write a value to it on D7 as part of a memory write or can read the value on D7 as part of a memory read.

    This could bring a potential failure point (or quirk at least) because you can write a 1 to this bit and tell the device "transfer is complete". This would kick off an interrupt if enabled, etc. One solution to this is to disconnect D7 from the input side, but you would need a way to acknowledge an interrupt and clear ITC from the programmer's perspective. The venerable...

    Read more »

View all 3 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates