Monday, April 6, 2020

A New Simulator

It's taken a while, but that was worth doing.  The new hardware-style simulator has clarified the structure needed for the FPGA version, and as a bonus given me the contents of the microcode ROM.

The final list of components:

  • Registers
  • ALU
  • MulDivMod
  • PCReg
  • SPReg
  • FlagsReg
  • OpcodeReg
  • InReset
  • Cycle
  • BranchCycle
  • MicrocodeROM
  • NanocodeROM
  • Address
  • Operand
  • OperandAddr
  • MemoryInterface

InReset stores a single bit indicating that the CPU is in reset.  If set, BRK's writes to memory as it pushes flags and PC are disabled.

PC, SP, and Flags are registers, but since they have special functions and can be written and read outside the standard register access, they get implemented separately.

Cycle is a 3 bit counter which stores the number of the cycle within execution of each instruction.  It normally increments on each cycle, but nanocode can request a conditional jump to a different  cycle.  This allows a single nanocode routine to implement instructions with one or two byte addresses (requiring one or two cycles), and operands of one or two bytes.  If an instruction only needs a one byte address, for example, then its microcode includes a 'BaseAddr16' flag which signals to the nanocode to skip the cycle which fetches the second address byte.

Branch instructions are too complex for this simple system.  For example, a simple branch with a one byte offset will go directly from the fetch of the first offset byte to fetching the next opcode (from either the next instruction or from the destination address, depending on the branch condition).  If the branch has the 'link' bit set, then it must go from the offset fetch to the cycles that push the current PC.  If 'indirect' is set instead, then it goes to the cycles that fetch the destination address from memory.

To handle all this complexity, BranchCycle is a 128x3 ROM.  The address is made from bits from the opcode (Link, Indirect, OffsetWidth), the branch condition, and the current cycle number.  The output is the next cycle.  If the low 5 bits of the opcode are 10000 (a branch instruction), then this ROM overrides the usual cycle selection.

The microcode ROM has 512 entries: one for each opcode and their alternates (instructions with bit 15 of the opcode set).  The outputs are

  • ALUCIn: Selects the source of the ALU's C input (carry in).  0 and 1 select constants (0 for 'add without carry', 1 for 'CMP', for example).  C selects the carry flag (for 'add with carry').  Ext and Rot select one end of the shifted value or the other, and are used for shifts and rotates.
  • ALUInvB: If set, inverts the B input of the ALU.  This is used to implement subtraction and the BIC (bit-clear) instruction.
  • ALUOp: Selects the ALU operation.  It can be Add, And, Eor, InB (output the B input unmodified), Neg, Or, ShiftL, ShiftR.  InB allows the nanocode routine that handles ADC, EOR, and so on to also implement LDA, LDX, and LDY.  ShiftL and ShiftR implement all of the shift and rotate instructions through the choice of the C input.
  • BaseAddr16: If set, signals to the nanocode to skip fetching of the second address byte.
  • BitNum: Selects which bit instructions like CLC and SED operate on.  It can be 0, 2, 3, or 6, and is combined with bits from the opcode extension to select any of the 32 bits.
  • DataWidthSel: Either '32' to signal that this instruction always works with 32 bit data, or '8_9' to use bits 8 and 9 of the opcode to select the data width.
  • DefaultReg: Selects the main register that the instruction uses.  It can be A0, X0, Y0, P, or SP.
  • RegMod: The choice of main register can be modified by bits from the opcode instruction, and this field determines which ones are used.  It can be None (don't modify), MOV (special modification for the MOV instructions), 8_11, 10_12, 10_13, 11_14, 13_14, or 13_15.
  • DefaultIndex: Selects the second, or index, register.  It can be A0, X0, Y0, P, SP, or PC.  Instructions with indexed addressing modes use this as the index register.
  • IndexMod: Which opcode bits modify the choice of index register.  It can be None, MOV (again, special handling for MOV instructions), 8_11, or 10_12.
  • FlagWrite: Four separate flags to enable writing to the C, Z, V, and N flags.
  • MulDivOp: Selects which of the MUL, DIV, and MOD instructions this is.
  • NoRegWrite: If set, disables the usual write to the destination register.  Instructions like CMP behave almost identically to other ALU instructions like SBC.  This allows them to use the same nanocode.
  • NSel: Some instructions have a small constant encoded in the opcode.  This field selects where it is.  It can be 1 (the constant is 1) or 13_14 (the constant is encoded in bits 13 and 14).

Nanocode fields are

  • AluASel: Select the source for the A input to the ALU.  This can be from Operand or from the A output of Registers.
  • AluBSel: Select the source for the B input to the ALU.  This can be Operand, the B output of Registers, OperandOrReg (the choice between the two is made by bit 14 of the opcode, for read-modify-write instructions like LSR, which can use an immediate operand either directly as a shift amount, or as the number of a register containing the shift amount), N (for instructions with a small constant encoded in the opcode), or BitNum (the bit selected by the BitNum field of the microcode).
  • CycleCond: The condition for jumps to other nanocode instructions.  This can be Always, BaseAddr16, Data16, Branch, or MulDivRunning (it is anticipated that MUL, DIV, and MOD will take more cycles than Cycle can handle.  This lets us repeat a single nanocode instruction until the MulDiv unit has finished)
  • CycleJump: The destination for Cycle to jump to if the condition specified by CycleCond is true. 
  • ExitReset: Clear the InReset flag, starting normal operation.
  • AddressInputSel: Which value to send to the address of the memory interface.  It can be OperandAddr, PC, SP, or Vector (the address is determined by bits from the opcode, for the BRK instruction)
  • AddressInc: Add 1 to the address.  This is used for accessing two-byte values.
  • MemWriteDataSel: Selects the source of the data to be written to memory.  It can be ALUOutL, ALUOutH, RegAOutL, RegAOutH, selecting the low or high bytes of either the ALU output or the A output of Registers.
  • MemWriteDataWidth: The size of data to write to memory.  16, 32, or D.  32 bit writes are implemented as two separate write cycles, with (for example) RegAOutL and RegAOutH selecting which half two write.  But the odd layout of 32 bit data required for compatibility with the 6502 means different parts of the value are written depending on whether a cycle is a 16 bit write or the first half of a 32 bit write.  D means 'take the size from bits 8 and 9 of the opcode).
  • WriteEnable: If set, this cycle writes to memory.
  • OpcodeLoad: If set, load the memory read data into the Opcode register.
  • OperandLoad: If set, load the memory read data into the Operand register.
  • OperandExtend: If set, combine the memory read data with the current contents of the Operand register to make a 32 bit value.
  • OperandAddrLoad: Load the OperandAddr register.
  • OperandAddrExtend: Extend the OperandAddr register.
  • OpernadAddrExtendFromOperand: This combines the memory read data with the contents of Operand, but writes the result to OperandAddr.  This lets us load a two byte address into OperandAddr from a location given by OperandAddr, using Operand as temporary storage for the first byte.
  • PCInc: Increment PC.
  • PCLoad: Copy OperandAddr into PC.
  • SPDec: Decrement SP.
  • SPInc: Increment SP.
  • RegASel: Which register to select for the A port of Registers: P, PC, or the register given by the microcode DefaultReg and RegMod fields.
  • RegBSel: Which register to select for the B port of Registers: Index (use DefaultIndex and IndexMod from microcode), Operand (the register number is in the low 4 bits of Operand, used for the immediate mode of read-modify-write instructions that store a register number in immediate data), or Zero (used by branch instructions, but I can't remember why)
  • RegBIsIndex: If set, the B port of Registers is used as an index register.  It is automatically added to the memory address, and if the flags register P is selected, 0 is used instead.
  • RegWriteSel: Selects the source of data to be written to a register.  It can be ALU (ALU output), Data (memory read data), or MulDiv (MulDiv unit output).
  • RegWriteEnable: If set, and the microcode hasn't selected NoRegWrite, writes a value to the register selected by DefaultReg and RegMod.
  • FlagsWriteEnable: Enables writing to flags.  The flags that get written are chosen by microcode.
  • RunMulDiv: Starts the MulDiv unit.
  • SetB: Clear the B flag if an interrupt it being handled, Set the B flag if it isn't.  Only used by the BRK instruction.  Interrupts are handled by loading a BRK instruction into the Opcode register.
  • SetI: Sets the I flag.  This happens in BRK.

The simulator is now running both CPU simulations in parallel, comparing all register values after each instruction.  Commodore 64 BASIC and my simple graphics tests run fine, with no differences between the simulations.  It's ready for the FPGA!