Sunday, April 30, 2023

Centurion FFC Microcode

The is one of a series of articles about the Centurion FFC board's custom CPU. In previous posts I explained how the program is stored as microcode. In this post we will take a look at what each bit in he microcode instruction does. In later posts I will dig deeper into some of these functions.

The first set of signals control the 2901 Chip Slice which I discussed in an earlier post


These signals select which 2901 register goes on the A and B inputs to the ALU.

MP.H50 - ALU A Select 0
MP.H51 - ALU A Select 1
MP.H52 - ALU A Select 2
MP.H53 - ALU A Select 3
MP.H54 - ALU B Select 0
MP.H55 - ALU B Select 1
MP.H56 - ALU B Select 2
MP.H57 - ALU B Select 3


These signals select the ALU source, function and destination.

MP.H60 - ALU Source Select 0
MP.H61 - ALU Source Select 1
MP.H62 - ALU Source Select 2
MP.H64 - ALU Function Select 0
MP.H65 - ALU Function Select 1
MP.H66 - ALU Function Select 2
MP.H70 - ALU Destination Select 0
MP.H71 - ALU Destination Select 1
MP.H72 - ALU Destination Select 2


The next set of signals selects the source for the bit that gets shifted into the 2901 Q register and RAM register during a shift operation. This bit could get shifted into either the high or low bit depending on the direction of the shift.

MP.H10 - Shift Qin Select 0
MP.H11 - Shift Qin Select 1
MP.H12 - Shift Qin Select 2

MP.H14 - Shift RAM In Select 0
MP.H15 - Shift RAM In Select 1
MP.H16 - Shift RAM In Select 2


These three lines select what will go into the carry input of the 2901's ALU which is used during addition and subtraction operations.

MP.H84 - ALU.Carry_IN Select 0
MP.H85 - ALU.Carry_IN Select 1
MP.H86 - ALU.Carry_IN Select 2


These signals are used to select the bit, for example Carry Out, that will be tested by the Microcode Sequencer to determine if a branch should be taken or not. Normally the branch selection is determined by the result of the current microcode instruction, but there is also a latch that records the state of a flag bit at the end of the previous instruction. The Flag Mux signals select which bit will be latched which can then be selected using Branch Select.

MP.H34 - Branch Select Invert
MP.H35 - Branch Select 0
MP.H36 - Branch Select 1
MP.H37 - Branch Select 2

MP.H30 - Flag Mux Invert
MP.H31 - Flag Mux 0
MP.H32 - Flag Mux 1
MP.H33 - Flag Mux 2


These signals control which microcode instruction gets executed next. I discussed this in detail in a previous post.

MP.H80 - Control Sequence Next Select  0
MP.H81 - Control Sequence Next Select  1
MP.H82 - Control Sequence Next Select  2
MP.H83 - Control Sequencer B8
MP.H87 - Control Sequencer B9


This set of signals controls reading and writing from various sources on the board. This includes RAM, the system register and various control and status signals. One of the outputs from these control muxes is always enabled, but some are unused so these can be specified in the microcode when no I/O operation is needed.

MP.H20 - Read Control 0
MP.H21 - Read Control 1
MP.H22 - Read Control 2
MP.H23 - Read Control 3
MP.H24 - Write Control 0
MP.H25 - Write Control 1
MP.H26 - Write Control 2
MP.H27 - Write Control 3


This signal disconnects the output of the ALU from the Data.Fx bus and connects that bus to the Data.INx bus. This allows data to be passed between parts of the CPU without the ALU being involved. For example, this allows a branch destination to be passed directly from the Constant ROM to the Control Sequencer. 

MP.H77 - DataRtoW - R/W Data Bridge


I will cover this signal in a later pot on how the RAM works.

MP.H63 - RAM Address Counter enable


This is an interesting signal. It simply goes to a test point on the board and doesn't actually control anything. I assume this was used for debugging during development. The released microcode ROMs never set this bit.

MP.H73 - TP4


The remaining signals directly control sections of the drive control electronics. 

MP.H17 - CRCEnable
MP.H13 - RDSSEL Sync Select
MP.H74 - CRCout
MP.H75 - CRCReset
MP.H76 - TSM_CSEL


 




Saturday, April 22, 2023

Centurion FFC Overview

In my last two posts I took a look at the two key components that form the heart of the Centurion FFC board's custom CPU. Now let's take a step back and take a look at an overview of the board. The FFC is actually composed of two boards, one is primarily for the CPU and the other for the drive control, but there is some overlap. 

This is the board with the drive control electronics.

This is the board that contains the drive control CPU which is the one I am going to focus on in these posts. 



Here is a block diagram I created of the CPU section. Each thick colored line is a different bus in the circuit. This may look complicated by we can look at it piece by piece to more easily understand it. 



As discussed in my last post, the 8x02 Control Sequencer provides the program counter for the microcode program and also controls the jumps and branches within the code. It outputs the MP.Ax bus which is used to address the Microcode ROM which controls the functions of the CPU, the Constant ROM which provides a single data byte for each microcode instruction and the Sequence Control ROM which controls how the sequencer determines what the next instruction should be. 

Coming out of the Microcode ROMs is the MP.Hxx bus which has the control signals that control the rest of the CPU. We will go into more details on these in later posts. 

There are two data buses in the processor, one that goes into the 2901 ALU and another that comes out of it. The one going into the ALU is shown in red and is called Data.INx. The data on this bus can come from RAM, the Constant ROM, the System Data Register, or input devices. The one going out of the ALU is in orange and called Data.Fx. The data on this bus can go to the RAM, to output devices, System Data Register, or to the Control Sequencer to allow for jumps or branches to a specific address in the microcode. 

The two buses can also be linked together through the Data Bridge. When the bridge is enabled, the output of the 2901 is disabled. This allows, for example, the constant ROM to be fed directly to the Sequence Controller to provide branch addresses. 
 
In my next post we will look at the what each bit of the microcode does. 

Sunday, April 9, 2023

Centurion FFC Microcode Sequencer

In my last post I described the AMD 2901 Chip Slice which forms the core the Centurion floppy disk controller. The 2901 provides the two fundamental components of a CPU, registers and an Arithmetic Logic Unit (ALU). The question is, how do you turn a 2901 into something that can execute code? The key to this is microcode.

You can think of microcode as machine language instructions, but each bit of the microcode instruction controls a single signal within the CPU. For example the the 2901 has eight inputs which control the ALU function, each of these inputs would be a single bit in the microcode instruction. It is not uncommon for microprocessors to use microcode under the hood, where it is hidden from the programmer. The programmer works with machine language opcodes each of which executes one or more microcode instructions. The Centurion FFC on the other hand uses microcode directly. 

The Centurion FFC has seven microcode ROMs making for a 56 bit wide microcode instruction, and each is 1K. There is also an eighth ROM that store immediate data instead of control signals which can be fed into the 2901 and other parts of the CPU. Before we can look at what the microcode bits do, we need to see how microcode is addressed. For that, the board uses the Signetics 8X02A Control Store Sequencer. Here is the block diagram of that chip:


The chip contains a program counter which drives the nine address bits that go to the microcode ROMs. The next address is controlled by the AC0..AC2 inputs which comes from bits 0..2 of microcode ROM H8, so each microcode instruction also determines which instruction get's executed next. Here are the function of those controls lines, they are numerically out of order but I think they make more sense this way...

7 = Reset. This is the first value that needs to be applied to chip. It resets the program counter to 0. 

1 = Increment. This is the simplest and most common function, it just increments the program counter by one.

0 = Test and Skip. This is the simplest flow control function. If the Test input to the chip is high, the program counter in incremented by two, thus skipping an instruction. If the input is low, the program counter is incremented by one. This makes it easy to implement a branch without a need to specify an explicit destination. 

5 = Push for Looping. This function pushes the current address onto the chips internal stack, and then increments the program counter. This is used to start a loop.

2 = Branch to Loop. If the Test input is high, the top value is popped off the internal stack into the program counter, otherwise the address is incremented and the top value popped and discarded. This used to test for the end of a conditional loop started with the Push for Looping function. 

4 = Branch to Subroutine.  If the Test input is high the next address is pushed onto the chip's internal stack and then the program counter is loaded from the B0-B9 inputs to the chip, we will look at where those come from below. If the Test input is zero the counter increments to the next instruction. This function works as a conditional subroutine call, and since one of the conditions can be a static 1, it can also function a simple jump to subroutine. 

3 = Pop Stock. This pops the top value off the stack and puts it in the program counter. This allows the program to return from subroutines called from Branch to Subroutine. 

6 = Branch if Test Is True. If the Test input is high, the program counter is loaded from the B0-B9 inputs, otherwise the program counter is incremented. This works as a branch to a specific address.

The Branch to Subroutine and Branch if Test functions branch to the address on the B0-B9 inputs. This data can come from two places on the FFC, either from the constant ROM or from the output of the ALU. 



Monday, April 3, 2023

AMD 2901 Chip Slice

In the early days of computers, processors were build from discrete components starting with relays, vacuum tubes, transistors and then small scale integrated circuit chips. In the early 1970's the first microprocessors were introduced which made it easy to construct personal computers but they lacked the processing power needed for mini and mainframe computers. Companies like AMD provided a middle ground between these two options in the form of large scale integrated circuit chips that provided the basic building blocks of a processor. The key component of the AMD family was the 2901 chip slice.

I first encountered this chip when I was working on the MAME driver for that Atari arcade game I-Robot. I-Robot was the first arcade game to used solid filled 3d polygons. The 2901 was used as the core of a custom processor that did all the 3D calculations. 

More recently I started watching the Usagi Electric YouTube channel where he has been working on a Centurion mini-computer. You can see the playlist of videos about this project here:

https://www.youtube.com/watch?v=DJ1HwuYBuss&list=PLnw98JPyObn0wJFdbcRDP7LMz8Aw2T97V

The main CPU as well as some of the controller cards in this system were also based on the 2901.  I am going to take an in depth look at the FFC floppy controller board. You can find a lot of information about this board in this Github repository. We will start by looking at the 2901 Chip Slice Processor.

Here is the block diagram of he 2901. 


The chip has two major parts, the RAM block which is used to create 16 internal CPU registers, and the Arithmetic Logic Unit (ALU)  which does addition, subtraction and logical operations. The RAM and ALU are each 4 bits wide, but you can combine multiple chips to create as wide a data path as is needed. This is why it is referred to as a chip "slice". 

Now let's look at the inputs of the chip to better understand how it works. First there is the A and B address inputs. These are both 4 bits and select which location in memory is put on the A and B bus. To be clear there is only one block of memory, so if A and B are the same, then they are putting the same data on both busses. 

The next input is 3 bits for the source selection, this will select what goes into the two inputs of the ALU as follows:


A and B are the data selected by the A and B address inputs. Q is a temporary holding register separate from the RAM registers. D is the direct data inputs to the chip. Finally 0 makes all four bits are zero.

Next are the three function selection bits. These select the function the ALU will perform and include addition, subtraction and logical operators. 



The final control inputs, called the destination, are a little more complicated since they control destination, output and shifting. 


Lets start with the first four since they do not involve shifting. 

0 -  Transfers the output of the ALU, called F, to the Q register and also puts that output on the chip's data output, called Y. 

1- This is called no-op and just puts the ALU output on the Y output but does not store it anywhere else. 

2 - This passes the output of the ALU to the RAM location selected by the B input. The RAM location addressed by A is put on the data output. 

3 - This passes the output of the ALU to the RAM location selected by the B input and also puts on the data output.

The next four destinations do shift operations before storing the results in RAM or the Q register. Both shifts have external inputs/outputs on either end, so if a value is shifted down the low bit is available on and output and a new high bit comes in from an input. This setup allows the shift operations to work across multiple 2901s.

4 - Loads the ALU Output/2 into the B memory, Q/2 into Q and outputs the unshifted ALU output. 

5. Loads the ALU Output/2 into the B memory and outputs the unshifted ALU output. 

6. Loads the ALU Output*2 into the B memory, Q*2 into Q and outputs the unshifted ALU output. 

7. Loads the ALU Output*2 into the B memory, and outputs the unshifted ALU output.

You will notice that there is no way to get external data directly into one of the RAM registers. To do this we would use the DZ source which is external data and zero and then do a function like ADD and finally do a destination of RAMF. This will take the input, add 0 and then store it in the RAM registers pointed to by B.

Now that we have looked at the control pins, let look at the status pins. 

F=0 - This pin is used to indicate that all four outputs of the ALU are zero. This is open collector so if there are multiple 2901s, these pins will be tied together on all the chips. If any one indicates non-zero it pull the line low,. If all the chips indicate zero and external resistor pull the line high. 

OVR - This pin indicates an arithmetic two's complement operation overflowed into the sign bit. If using multiple 2901s this would only be relevant on the last (highest bits) chips. 

Cn, Cn+4 - Carry in and out

~P,~G - These are used to do a fast carry between multiple 2901s. This can be done with the Cn,Cn+4 pins but the fast carry pins allow the processor to run faster, but requires some extra hardware.

F3 - The most significant bit of the ALU output. On the last 2901 this could be used as a sign bit. 


The final two signals are for general control of the chip.

Most operations in the chip are asynchronous, but there are a couple things controlled by the CP clock input. It controls the latching of the Q registers and the RAM register outputs and also latches the data into the RAM registers. 

The final input is is Output Enable (OE) which enables the data outputs from the chip.