Monday, November 15, 2021

Long Overdue Progress

In the very first post on this project, I listed my design constraints.  The most important of these came last:

  • Working on it must make me happy
It turns out that writing VHDL testbeds does not make me happy.  And the complete ALU needed a large and complicated testbed.

After a great deal of procrastination (including writing a transpiler for a language that was much more pleasant to work in than VHDL, but never got to the point of being able to create the testbed the ALU needed), I made myself sit down and just do it.  After a lot more work, the ALU finally passed all of its tests.

The high-level structure from the previous post is still true.  There are two main inputs, A and B.  B is fed through the shifter (with its own 'shift amount' input), the inverter, and then both go into the adder.  In addition to ADD, AND, OR, and EOR, the adder can also pass the B input directly to the output.  Not shown are a collection of multiplexors to select a source for each of the inputs, and the large number of control signals to get it to perform the right operation.

There's also another block that wasn't shown on the diagram, but which turned out to be a fairly major chunk of logic.  This takes the inputs, the adder output (including carry out), shifter carry output, and a collection of control lines, and generates values for the flags.  It has to work on 8, 16, and 32 bit operations, and also handle the N and V flags for the BIT instruction.  Here it is, squeezed into as few LUTs as I could manage
The 65020's ALU flags component

With the ALU complete, all that was needed was to construct the rest of the CPU.  That was a much simpler and quicker task: it took far less than the year of procrastination that the ALU did.  I already had the structure of it worked out in the C++ simulator, so most of it was just a matter of turning those components into VHDL, with a bit of fixing up where things weren't a good fit for an FPGA.

And now... it's working.  There was a lot of debugging, of course, but it didn't take too long to get it running its first instructions.  Since that milestone it's been a pleasant process of writing software for it and working out why it doesn't work.  As the fault can be in the software, the assembler, or the CPU itself (and sometimes in all three), that's been a lot of fun.

You'll be wanting a screenshot, of course.  Here it is
The C640 computer running a variety of colourful tests

Each new test uses some instructions that the others hadn't, and these occasionally throw up new bugs to be fixed.  Fortunately these problems seem to be getting less frequent.  It's almost looking like a computer.

The next step is getting the PS/2 keyboard interface working.  It's close - the FPGA side appears to be good, but for some reason the software is sometimes dropping key presses.

Saturday, June 6, 2020

The ALU

The 65020's ALU is its most complex component.  It must be able to add and subtract in both binary and decimal modes, perform logical and, or, and exclusive or, shift and rotate values arbitrary distances, and also set, clear, toggle, and test individual bits.  All of these operations must work on 8, 16, or 32 bit values, and produce appropriate values for the flags.

To do all of this, I've broken it down into three smaller sub-components.


The A input is usually the first operand, and B is usually the second operand.  B can be optionally shifted (not shifting is the same as shifting by 0) or inverted.  The two operands go into the Add/Logic sub-component, which can do binary or BCD addition, or logical and/or/exclusive or.  Subtraction is done by inverting the second operand before adding it to the first.

So ADC A0, #123 is handled by sending the contents of A0 to input A, the constant 123 to input B, setting the shift input to 0, not inverting it, and then adding the two.

SBC A0, $1234 has A = A0, B = value from memory, shift = 0, B inverted, then adding.

The various shift and rotate instructions take their operand on the B input.  A is set to 0, and the adder/logic unit performs an OR, to pass the shifted result to the output.

LDA doesn't look like an instruction that uses the ALU, but I have it using the same microcode and passing the loaded value through the ALU.  A is set to 0, there there is no shift or invert, and the Add/Logic sub-component performs an OR.  This send the loaded value through unchanged, but allows flags to be set.

CLC also doesn't look like an ALU instruction.  But on the 65020, it's a special-case of a more general set of bit-clearing instructions.  These have the bit number encoded in the instruction, and the bit to be cleared in any register, or in memory.  The A input takes the register or memory value, B is set to 1, and the bit number goes to the shift input.  The shifter output is inverted, and then ANDed with the value.  SEC is done in a very similar way, but using OR and not inverting the shifter output.

The Inverter

The inverter is a very simple component, but there are a couple of interesting points.  If we didn't have to support decimal mode, it would be a simple array of XOR gates, with one input of each connected to the 'invert' control signal.

To support BCD subtraction, it also needs to be able to give us the 9's complement of a value: this is obtained by subtracting each nybble from 9.  At first glance, this is simple: take the input 4 bits at a time, add the 'invert' and 'decimal' control signals, and that's a perfect fit for the Spartan 6's 6-input LUTs.  A 32 bit inverter will take 32 LUTs, or 8 slices.

But we can do better than that.  Each LUT actually has two outputs, allowing two functions of 5 and 6 inputs respectively.  If we can use only five inputs, we can have two independent functions implemented in a single LUT.

input   binary  decimal 

0000    1111    1001
0001    1110    1000
0010    1101    0111
0011    1100    0110

0100    1011    0101
0101    1010    0100
0110    1001    0011
0111    1000    0010

1000    0111    0001
1001    0110    0000
1010    0101    1111
1011    0100    1110

1100    0011    1101
1101    0010    1100
1110    0001    1011
1111    0000    1010

Examining the truth table of the inverter, it is apparent that the bottom two bits of the output depend on only the bottom two bits of the input.  And the top two bits of the output depend on only the top three.  Add the two control lines, and that's 4 inputs for one pair of outputs and 5 for the other pair.  That means we can do the whole inverter in only 16 LUTs, or 4 slices.

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!

Tuesday, March 17, 2020

A New Start

One day I'll learn to listen to my own advice.  In the previous post, I had a very ugly nested-if implementation of the handful of instructions I needed to run a simple test program.  That worked, but it clearly wasn't a good way to continue.  It was obvious that I needed to take a much more hardware-oriented approach, and definitely use a microcode ROM.

So of course I didn't.  I continued with the nested-if style, implementing more and more instructions.  Synthesis was taking longer and longer.  Eventually, with fewer than half the instructions done and each iteration taking about 15 minutes, I stopped and looked at the log.  That showed 105% FPGA resource use.  I can only assume that Place-And-Route was doing some heroic optimisation to squeeze it all in.

So I need a new implementation.  It wasn't clear what architecture would be needed - what components there should be, what internal busses, and how it should all connect together.  My usual approach is to do a rough first draft, then keep tweaking it as I fill in the details.  I'm comfortable doing that in software, but I still find writing VHDL enough of an effort that I was reluctant to try it.  But I still needed to know what the implementation should look like before I could start.

So I'm back to the software simulator for a while.  I've re-worked the code a little so it now supports two separate implementations of the CPU interface.  One is the old simulator, which will serve as a reference.  The new one is written to have the same structure as the (eventual) hardware.  There's a class for each type of component, and they communicate through explicit signal variables.  It's all controlled by a two-level microcode/nanocode component.

First, there's a 256 entry microcode ROM which gives global information about each instruction - what registers it uses, the structure of the opcode extension, and so on.

Then there's a 32x8 entry nanocode ROM, which provides cycle-by-cycle control of the execution of each instruction.  Instructions can take up to 7 cycles, and there are 25 different types.  Rounding that up to powers of 2, we get 32x8 = 256 entries.

Each nanocode instruction has a conditional jump, to allow skipping of some cycles under various conditions.  That allows, for example, LDA abs,Y and ADC zp,X to use the same type.  Microcode selects the index register, and ADC zp,X can skip the cycle that fetches the high byte of the base address.

Right now, only one instruction type is implemented, and that type has only one instruction: BRK.  The original 6502 implemented its reset sequence as a variant of BRK - the usual writes of P and PC to the stack are suppressed (although their cycles still take place), and the vector is fetched from $fffc instead of $fffe.  I'm doing the same, loading $0100 into the opcode register and setting a flag that disables writes until the end of the next instruction.  The 65020's BRK instruction has a 4 bit vector selection field in its extension bits, so it can select $fffc through that instead of using extra logic.  Here's the nanocode for BRK:
AddressInputSel_SP | RegASel_PC | MemWriteDataSel_RegAOutH | WriteEnable | SPDec
AddressInputSel_SP | RegASel_PC | MemWriteDataSel_RegAOutL | WriteEnable | SPDec
AddressInputSel_SP | RegASel_P | MemWriteDataSel_RegAOutL | WriteEnable | SPDec
AddressInputSel_Vector | OperandAddrLoad
AddressInputSel_Vector | AddressInc | OperandAddrExtend
AddressInputSel_OperandAddr | PCInputSel_OperandAddr | PCLoad
AddressInputSel_PC | OpcodeLoad | PCInc | CycleCond_Always | CycleJump0
Each line represents one cycle.  The first three push P and PC to the stack: the address output selects the SP register, the register file output A selects PC or P,  the memory write data bus selects either the high or low half of the selected register, a memory write is requested, and SP is decremented.

In the next two cycles, a vector address (generated from the opcode extension) is placed on the address bus, and the data read from memory is loaded into the OperandAddr register.  This takes two cycles because the register is 32 bits wide, but the data bus is only 16.  The first cycle loads the low 16 bits of the register and sets the high 16 bits to 0.  The second cycle (OperandAddrExtend) takes the 16 bits already loaded and combines them with 16 new bits to make a 32 bit address.

Next, OperandAddr is sent to the address bus (this is probably not needed) and PC is loaded with the contents of OperandAddr.  If PC was given the same ability to load and extend as OperandAddr, this whole cycle could be removed.  That sort of refinement is the whole purpose of writing this new simulator.

On the last cycle, PC is sent to the address bus and incremented, the Opcode register is loaded from memory, and we unconditionally jump to cycle 0 to start execution of the instruction that was just loaded.

The rest of the simulator is still set up to load the Commodore 64's ROMs, and the first instruction in their reset sequence is $a2 $ff: LDX #$ff.  So that will be the next instruction.  Since each nanocode routine handles all instructions that need the same sequence of operations, that's going to end up implementing the immediate mode of all of the 'main group' of instructions: LDA, ADC, CPX, and so on.

Sunday, November 10, 2019

First Instructions


There's not much apparent difference between this and the last screenshot.  But it's an important one.  The cursor is blinking.  It's blinking under software control.  We have a working CPU!

I've now got a simple test program in ROM.  It copies the screen data from ROM (doing an ASCII to CBM conversion on the way), then sits in a loop turning the cursor on and off.  Here's the relevant part of the code:
0000e4ea:                         54 reset
0000e4ea: 01a2 03e7               55 ldx.w #999
0000e4ec: 20a9 000e               56 lda a1, #14
0000e4ee:                         57 copyScreen
0000e4ee: 00b4 e000               58 ldy initScreen,x
0000e4f0: 10a5 e3e8               59 lda asciiToCBM,y
0000e4f2: 0095 0400               60 sta $0400,x
0000e4f4: 2095 d800               61 sta a1, $d800,x
0000e4f6: 01ca                    62 dex.w
0000e4f7: 0010 00f5               63 bpl copyScreen
0000e4f9:                         64 loop
0000e4f9: 00a9 0020               65 lda #32
0000e4fb: 0085 04f0               66 sta cursor-initScreen+$400
0000e4fd: 02a2 0823 007a          67 ldx.l #555555
0000e500:                         68 delay1
0000e500: 02ca                    69 dex.l
0000e501: 00d0 00fd               70 bne delay1
0000e503: 00a9 00a0               71 lda #32+128
0000e505: 0085 04f0               72 sta cursor-initScreen+$400
0000e507: 02a2 0823 007a          73 ldx.l #555555
0000e50a:                         74 delay2
0000e50a: 02ca                    75 dex.l
0000e50b: 00d0 00fd               76 bne delay2
0000e50d: 80f0 00ea               77 bra loop
Because I'm not attempting full compatibility with the original 6502, instruction timing is a little different.  DEX is a single cycle.  Branches take two cycles, whether they're taken or not.  There's no penalty for crossing a page boundary.  Thus the delay loop for blinking the cursor takes 3 cycles per iteration, and the loop count of 555,555 gives 3 blinks in 2 seconds at the C640's 5MHz.

The VHDL is still very brute-force and very ugly.  Only a handful of opcodes are supported (the ones needed for this very basic test program), and it's done through nested case and if statements, deciding what to do on each phase of each cycle for each individual opcode.

That's clearly not going to be a sustainable way of implementing the full CPU.  A few things stand out - opcodes 85 (sta zp,0) and 95 (sta zp,x) are really the same instruction, but with different index registers.  And b4 (ldy zp,x) only differs in destination register.  A lot of the work that I'm currently cut-and-pasting between instructions could and probably should be shared.

It would be good for development to put most of the complicated parts into a microcode ROM.  That way changes can be made, and new instructions implemented, by simply building a new ROM and inserting it into the bitstream, leaving the VHDL alone.  When the time comes to develop software for the ROM, it will be a relief to avoid the increasingly lengthy VHDL synthesis whenever possible.

Monday, September 23, 2019

A Character Display

The C640 showing off its character display.  It says
*** C640 COMPUTER SYSTEM ***
2MB RAM SYSTEM  38911 BASIC BYTES FREE
That took a lot longer than I wanted it to.

Last time, we had VIC displaying the contents of RAM as a bitmap, and a dummy CPU component copying ROM into RAM.  It sort of worked, but I wasn't happy with the SRAM interface, which was writing corrupted data to memory.

A little tweaking of the memory timing - what happens on which clock phases - fixed the memory problems.  I still wasn't comfortable with it: why did it work stop working with some signals delayed 6ns, when everything was happening at half the speed that should have worked?  But never mind.  It worked, and I was eager to move on to the character display.

This required getting a number of things to work.  First up, VIC doesn't have enough memory bandwidth to read all the information it needs (well, it does in the 320x200 mode I'm using here.  It wouldn't at higher resolution).  The Commodore 64 would stop the CPU every 8 lines so VIC could fetch character pointers from screen memory at $0400 into an internal buffer.  It could then use those pointers to create addresses for bitmap data, which is read on every line.

So the C640 needs DMA.  It needs to be able to pause the CPU, allow VIC to use the CPU's half of the cycle to access memory, have an internal buffer to store it, and create addresses from it to fetch bitmap data.  There's a fairly long pipeline there, and the sequence must start early enough that the bitmap data is ready for display before the border ends.

I didn't think to take any screenshots of the early attempts.  They weren't pretty, and it took weeks of not-at-all-intensive debugging to fix all the problems.

The first one was that SRAM writes immediately broke.  Thinking that it was obviously a timing problem, and the extra logic I'd added had pushed something past its limit, I dug through Xilinx's documentation and discovered the trce tool for generating a timing report after place and route.  That's the only time that can be expected to give accurate results, as a significant part of the total delay is the time it takes to get a signal from one part of the FPGA to another.

The report revealed a large number of timing violations, mostly in the clock enable signals.

mclk, system clock phase, and some of the C640's clock enables.
This is from a later (working) version of the design, so there are only 16 clock phases
Since I didn't want the extra complexity of dealing with multiple clocks, I'm using a single clock (160MHz at this point, and called 'mclk') with enable signals to tell various parts of the design when they should pay attention to it.  Most enables are only active one in every 5MHz system cycle.  To make them active at the right times, I have a "system clock phase" counter, which says how far through the 5MHz cycle this particular 160MHz clock pulse is.  So the clock phase will be active around the rising edge of mclk (which is the edge that everything else is latched on), this counter is incremented on the falling edge of mclk.

That means there's 3.125ns to increment the counter, combine it with whatever other logic is required for the clock enable in question, and get the result to the clock enable input of the register.  The Spartan 6 is fast, but it's not that fast.  Many clock enables were arriving too late.

So it's back to an 80MHz clock, and this time the memory controller uses both edges to generate control signals for the SRAM.  Memory access is now rock solid, and the timing report has no constraint violations.

There then followed far too much fiddling around trying to get the right sequence of actions to make DMA work.  For weeks I had an almost correct display, but there was always something wrong.  The first character on each row would be duplicated, the last character would appear at the start, the first character would have bitmap data from a different character displayed on its first line, ... my poor software brain was at its limit trying to deal with a system where everything happens simultaneously, but everything must happen in exactly the right sequence.

But, as you can see, I finally got there.  There is now a working character display, and I feel that I'm starting to get the hang of this FPGA thing.

Next, it's time to start on the CPU.  I can see significant failure ahead.

Saturday, July 27, 2019

It's Back!

It's been a while.

The work last year had taken the design as far as it needed to go, and the simulator had done its job in proving that it all fitted together.  It was time to turn to hardware.  My chosen platform is the Papilio Duo from Gadget Factory - a Spartan 6 FPGA coupled with an Arduino and a 2MB static RAM.  The large SRAM was attractive (it's much easier to interface than the usual DDR), and it has a "Classic Computing Shield" add-on with all the necessary ports to turn it into a classic 1980s computer.  I'm ignoring the Arduino side of it.

With great enthusiasm I got started.  And that's when I hit a very solid brick wall.

It didn't take much work to get a basic VGA display

There's a little more going on in this photo that it might appear, but also a lot less.  The FPGA contains three main components: a clock generator, a memory controller (including character ROM), and a video generator.

The clock generator multiplies the Papilio Duo's 32MHz oscillator up to 80MHz (I have since increased this to 160MHz, for reasons that will be explained below).  Why such a high frequency?  I need a 40MHz pixel clock for 800x600 VGA (which becomes 640x400 with a border), so it makes sense to start with a multiple of that.  The pixel clock becomes 20MHz in 320 mode, and with 8 pixels per system clock (like the Commodore 64), that implies a 5MHz system clock.

In a real, and by this point rather impractical, mid 1980s computer, we would have the CPU and VIC both accessing memory every cycle.  VIC has two data busses, so each 5MHz cycle needs to support three independent 16 bit accesses.  Since the SRAM on the Papilio Duo is only 8 bits wide, that means six memory accesses per cycle, which I've rounded up to 8.  The extra slot might get used for some kind of DMA in the future.

My original plan for accessing the SRAM needed two clock cycles per access (write need /WE to be low and then high, so it can't be done in one), so that works out to 80MHz.

But this is still pretending to be an old computer, and old computers didn't do anything at 80MHz.  I didn't want to attempt a design with multiple clock domains on my first serious FPGA project, so I'm using a single clock which can be gated by the different modules.  These clock enable signals are also generated by the clock generator.  There are two for the CPU, reflecting the two phases of the clock, and two for VIC.

The next module is the memory manager.  This takes memory access requests from the CPU and VIC, and translates them into the right signals for the SRAM.  It also contains a character bitmap ROM stored in an FPGA BRAM.

Finally, there is the video generator, VIC.  At the moment it is a very simple design, just generating VGA timing signals and reading a bitmap from memory.  For this screenshot, it's configured to read from character ROM.

The next obvious step is to get SRAM working.  My plan was to build a very simple fake CPU that simply copied character ROM into RAM, then get VIC to read from RAM instead of ROM.  That's when it all fell apart.  It didn't work, and nothing I tried could change that.  Motivation drained away, I moved onto other things, and the project was stalled.


But then... I recently bought a Digilent Digital Discovery.  It does a number of things, but for me the most important function is the 32 channel logic analyser.  Being able to see the real signals on the real hardware should make debugging this thing possible.

And it did!  After a little bit of work, I discovered a number of problems.  First, a bit of re-jigging in VIC had resulted in it always displaying a blank screen no matter what data it was getting from memory.  Oops.  I had also been rather optimistic in the way I was writing to SRAM.

The original design presented address and data, then pulled /WE low for a cycle, then returned it high.  The datasheet suggested that this might work, as the relevant setup and hold times were all zero.  But changing outputs on an FPGA and receiving those as inputs on the SRAM are different things.  I couldn't guarantee that the address or data weren't changing a little bit later than /WE, and the tracks on the PCB were definitely not all the same length.

That prompted the clock doubling.  The FPGA is now running at 160MHz, giving me four clocks for each memory access.  That lets me stagger the signals in a way that has a better chance of fitting the timing.

And it almost does.  Here's what I get now

Can you spot the difference?  Those pixels in the bottom right of each character are written by the fake CPU as it copies data, so I can be sure that VIC is fetching data from RAM.  But zoom in closer and look at the right hand side of the Hs.  There are a few missing pixels there.

It's worse in real time.  Pixels flicker on and off all over the screen.  It starts out OK, and gets worse as the chips warm up.  Clearly I'm not quite meeting some timing somewhere.  I added a third phase to the CPU to test this: now it reads ROM, writes to RAM, then reads RAM and compares.  If the result is different, it turns on an error LED.  At full speed the LED is always on.  If I reduce the clock speed, there are no errors at all.

So that's where it is now.  There's a little more work to do on the memory controller, because there's no point trying to continue if I can't trust SRAM writes to work.  Then next step will be making VIC a little bit closer to the real design, using DMA to read character pointers and colours.  And then, with the ability to display proper data, I can finally start work on making a real CPU.

Tuesday, August 14, 2018

SAD Graphics

Last time we were left with a problem: too much graphics data.  The original Commodore 64 has 8 bits of bitmap data from system RAM each cycle.  The C640 doubles this by using the colour RAM interface, and doubles it again by having 16 bits per byte.  That gives us 4 bits per pixel in standard mode, and 8 bits in multicolour.

It would be nice to use the extra data to increase resolution, but I haven't been able to think of a good way of doing that.  We want to keep old behaviour when the new bits are 0, and try to avoid adding new modes.

My solution is "Store and Display" graphics.  This is inspired by the Amiga's HAM mode, and the Apple IIGS's fill mode.

In standard mode, we have four bits per pixel: ABCD.  The low bit (D) is stored in the low 8 bits of system RAM, C is in the high 8 bits of system RAM, B in the low 8 bits of colour RAM, and A in the high 8 bits of colour RAM.

CD selects the colour for this pixel, as described before.  A and B control store and display.

  • 00CD: store CD in register C0, display CD
  • 01CD: store CD in register C1, display CD
  • 10CD: display register C0
  • 11CD: display register C1
(I might change the last two to xor the contents of the colour register with CD, depending on how useful it is in practice.  I'm also tempted to add some new registers to set the initial values of C0 and C1 on each line)

If we clear the screen to 1000 with a column of 0000 down the left hand side, that gives us a screen full of the background colour 00 (register $d021).  If we set a single pixel to 0001, then it will display colour 01, and also store 01 in register C0.  Then on the rest of that line, every pixel will be displaying the colour from C0, which is now 01.  By changing a single byte, we have drawn a horizontal line.

If we didn't want to draw to the end of the line, we could write 0000 to a pixel to set C0 back to colour 00.

This gives us the ability to draw single-colour filled polygons by drawing only the pixels along the left and right edges.  But so far that's only using C0, not C1.

Now clear the screen to a checkerboard pattern of 1000 and 1100, ensuring that C0 and C1 are both set to 00 at the start of each line.  This gives us a full screen of the background colour.  If we set two adjacent pixels to 00xx and 01yy, then C0 and C1 will be set to xx and yy respectively, and the rest of the line will alternate between those two colours.  This lets us draw dithered filled polygons, as in the teaser picture from the last post:




Multicolour mode has 8 bits per pixel: ABCDEFGH.  These are stored as follows:

  • GH: system RAM low
  • EF: system RAM high
  • CD: colour RAM low
  • AB: colour RAM high
If A is 0, then BCD is used to select one of eight C registers (C0 to C7), and EFGH is both stored and displayed.  If A is 1, then the contents of the selected register will be displayed instead.  This is the same as standard resolution, but with many more colours to play with.

It probably won't be useful to store split-pixel 'colours' in the C registers, but if there is a use for it, it will work.


Currently, the simulator ignores the low bits when the top bit is 1 (when it is displaying the contents of a C register rather than storing a colour into it).  That feels a bit wasteful.  If those bits were xored with the contents of the displayed register, that would allow overlaying images onto a filled polygon without interfering with the rest of it.  This might be useful.  But it might end up too complicated to actually use.  That decision will have to wait until I've written some more test software.  And that will have to wait, as I've put the simulator aside for now, and moved development onto much more interesting things...

Sunday, July 22, 2018

A Better VIC

The 6567 "VIC II" chip in the Commodore 64 has two memory interfaces.  One has 14 address bits and 8 data bits, and is connected to system RAM.  It does a fetch of bitmap data in the first half of every cycle, and a fetch of character pointers (or colours in bitmap mode) in the second half of the cycle during DMA (usually every 8 lines).

The other interface has 10 address bits, shared with the first interface, and 4 data bits.  It is connected to the 1Kx4 colour RAM.  Both interfaces fetch data at the same times, but only the DMA data from the colour RAM is used.

If the 6567 had a few extra pins, and RAM hadn't been so expensive at the time, colour RAM could have been extended to 16Kx8.  It would need new register similar to $D018 to control the high bits of its address.  Then we can map the bitmap and DMA data to different parts of colour RAM, and use both.

For compatibility we will need another mode bit in one of the control registers, that will force bitmap reads of colour RAM to 0.  As always, we want everything to behave the same as a standard Commodore 64 if the extra bits are 0.

This gives us twice as many bits per pixel, and an extra 4 bit colour per character cell.

Standard resolution now has two bits per pixel.  The low bit comes from system RAM, the high bit from colour RAM.  Colours could be allocated as follows:

  • 00: Low 4 bits of $D021
  • 01: Low 4 bits of colour RAM DMA data
  • 10: High 4 bits of $D021
  • 11: High 4 bits of colour RAM DMA data
This is not the final allocation - there's a big change coming soon!

In multicolour mode, we now have four bits per double-width pixel.  The low two come from system RAM, the high two from colour RAM.  We could assign each of the 16 values a different colour source (two from colour RAM, and the rest from global colour registers like $D021), but in a system with only 16 colours in total that doesn't feel like a good fit.

Instead, we assign the first 10 values to various colours, and the remaining 6 to pairs of colours.  Each pixel can then choose a different colour for its left and right halves:
  • 1010 Left = $D021  Right = colour low
  • 1011 Left = $D021  Right = colour high
  • 1100 Left = colour low  Right = $D021
  • 1101 Left = colour high  Right = $D021
  • 1110 Left = colour low  Right = colour high
  • 1111 Left = colour high  Right = colour low
That gives us the best of both modes.  We have a choice of 10 colours for double-width pixels, with 3 colours for high resolution detail, all in the same graphics mode.

Similar things can be done with bitmap graphics and sprites.

But wait...

In the C640, every byte contains 16 bits.  That doubles the amount of data available again.  For the DMA data, that's easy to use.  We can extend character pointers to allow more than 256 characters, and add flags to mirror characters horizontally or vertically.  And we can have 5 bit values in colour RAM, giving a 32 colour palette, and have three available in each character cell.

But what can we do with the extra bitmap data?  Here's a clue

Saturday, July 7, 2018

Some 65020 code

Since the simulator can now handle C64 bitmap graphics, I thought it would be good to write some graphics primitives in proper 65020 code, using the new CPU features.


My little test program starts by entering graphics mode and clearing the screen
sbt #5, $d011
lda #$18
sta $d018
lda #$15
bra.l clear
That first instruction is actually one of the old ones with a new addressing mode.  The 6502 has instructions to set and clear flags within the status register.  The 65020 uses four of the extension bits to select different destination registers (for the original opcodes, which are now reinterpreted as acc mode), or an index register for an indexed addressing mode.  Three others are combined with the bit number implied in the original opcode's flag to select any of the 32 bits in the word.  SBT #5 gets assembled as a variant of SEI.

The other new feature here is the bra.l instruction.  This is BEQ, with the P bit set to select a different condition (in this case, it becomes "always").  The .l modifier tells it to push the current value of PC to the stack before branching.  This turns it into a JSR-like instruction, with relative addressing.  Branch instructions also have four bits to select a base register, if you want to branch relative to something other than PC.  We'll see a use for that later.

The clear routine is straighforward.  The only thing worth noting is that wider registers make it a lot less wordy than the original 6502
clear
ldx.w #1000
clear1
sta $0400,x
dex.w
bpl clear1
ldx.w #8000
lda #0
clear2
sta $2000,x
dex.w
bpl clear2
rts
One of the annoyances of Commodore 64 bitmap graphics is the way that the bitmap is arranged in memory.  Most hardware does this linearly, with the first N bytes representing pixels in the first line, from left to right, the next N bytes being the second line, and so on.  The Commodore 64 does things differently, as a result of re-using some of the hardware that handles the character screen.  The first byte represents the first 8 pixels in the first line (line 0).  But then the second byte is the first 8 pixels of line 1.  This continues, with byte 7 being the start of line 7.  Then byte 8 jumps back up to the second set of 8 pixels on line 0.  This continues until byte 319, which is the last 8 pixels of line 7.  The next 320 bytes repeat the same pattern 8 lines lower.

An essential function of any graphics code is going to be a routine to map pixel coordinates to the address of a byte, and a mask for the right bit within that byte.  Here's the 65020 way:
getPixelAddr
phx.l x1
; a3 = y & 7
mov.w a0, x1
and a0, #7
; x2 = y/8
lsr.w #3, x1
; addr = 320*(y/8) = 256*(y/8) + 64*(y/8)
mov.w a1, x1
asl.w #2, a1
add.w a1, x1
asl.w #3, a1
asl.w #3, a1
; addr = 40*(y/8) = 32*(y&~8) + 8*(y&~8)
; addr += low bits of y
add.w a1, a0
; a = 1<<(x&7)
mov.w a0, x0
and a0, #7
; pre-subtract low bits of x (so adding later only adds the high bits)
sub.w a1, a0
lda a0, bitmasks, a0
; addr += high bits of x
add.w a1, x0
plx.l x1
rts
bitmasks
.byte 128, 64, 32, 16, 8, 4, 2, 1
getPixelAddr takes the x coordinate in X0 and the y coordinate in X1.  It returns the offset within the bitmap in A1, and the bit mask for the pixel in A0.

There are a few things worth noting here.  The 65020 has 12 main registers (A0-A3, X0-X3, Y0-Y3).  The A and X registers are almost completely interchangeable.  The Y registers can be used as the destination of ALU instructions, but not the source.  It took a few false starts, but I've eventually settled on a convention for register use that I think might suffice for the future.  X registers are parameters, and their values are not changed by the routine.  A registers are result, and their values can be changed.  Y registers are for pointers, and I'm not sure whether they should be saved by routines or not.  They probably should be.

So getPixelAddr starts by saving the value of the one X register that it modifies, and ends by restoring it.  The rest is the usual sort of bit-fiddling that you expect to see in code like this.  I originally wrote it to calculate the mask first, then multiply y/8 by 320, then add all the components together.  A bit of re-arranging followed to make it use fewer registers, and also to work around the awkwardness of the two-operand instructions.  Being able to have a destination register different from the sources is a very useful feature if you're writing code by hand, but the 65020 just doesn't have enough opcode bits to do it.

The 2 bit constant in acc mode instructions like ASL is occasionally useful.  But you can see here I needed a shift by 6 bits, which doesn't quite fit and has to be done in two batches of 3.

getPixelAddr is a lot shorter and faster than the equivalent in 8 bit 6502 code.  But it's not the sort of thing you want to call more often than necessary.  For drawLine we'd like to call it once at the start of the line, and then use simpler modifications of the bit mask and address to move from the current pixel to one of its neighbours.  drawLine uses four helper routines to move right, left, down, and up:
drawLine_incX
rrb a0
bcc drawLine_incX_exit
add.w a1, #8
drawLine_incX_exit
rts
 
drawLine_decX
rlb a0
bcc drawLine_decX_exit
sub.w a1, #8
drawLine_decX_exit
rts
 
drawLine_incY
inx.w a1
mov a2, a1
and a2, #7
bne drawLine_incY_skip
add.w a1, #312
drawLine_incY_skip
rts
 
drawLine_decY
mov a2, a1
and a2, #7
bne drawLine_decY_skip
sub.w a1, #312
drawLine_decY_skip
dex.w a1
rts
To move right or left, I use the RRB and RLB instructions.  These are similar to the old ROR and ROL instructions, but instead of rotating a 9 bit value including the carry flag, they rotate within the 8, 16, or 32 bits selected by the instruction width (here it's 8 bit).  RRB A0 shifts the low 8 bits of A0 one bit to the right.  The old right-most bit gets shifted into bit 7, and also copied to the C flag.  Most of the time we can move right or left by just doing this rotate.  If C is clear after it, nothing more needs to be done.  If C is set, that means we've stepped outside this byte and need to move on to the next.  Because we're using RRB and RLB instead of ROR and ROL, the bit that was shifted out is already shifted in to the other end, and all we need to do is add or subtract 8 from the offset.  That's done with the ADD instruction, which is an add without carry in.  It's the old ADC instruction with the P bit of the extension set.

Now for drawLine itself.  First, a bit of set-up
drawLine
phx.l x0
phx.l x1
phx.l x2
phx.l x3
ldy.l y2, #drawLine_incX
sbx.w x2, x0 ; x2 = dx
bpl drawLine_noswap
ldy.l y2, #drawLine_decX
lda a3, #0
sub.w a3, x2
mov.w x2, a3
drawLine_noswap
bra.l getPixelAddr
ldy.l y3, #drawLine_incY
; get displacement of endpoint
sbx.w x3, x1 ; x3 = dy
bpl drawLine_ypositive
; y negative
ldy.l y3, #drawLine_decY
lda a3, #0
sub.w a3, x3
mov.w x3, a3
drawLine_ypositive
mov.w y0, x2 ; loop count = dx x major
cpx.w x2, x3
bgt drawLine_xmajor
mov.w y0, x3 ; loop count = dy y major
; swap x2, x3
mov.w a2, x2
mov.w x2, x3
mov.w x3, a2
; swap y2, y3
mov.w a2, y2
mov.w y2, y3
mov.w y3, a2
drawLine_xmajor
mov.w a3, x2 ; a3 = error
sub.w a3, x3
adx.w x2, x2 ; x2 = 2dx (dy)
adx.w x3, x3 ; x3 = 2dy (dx)

drawLine takes (x1, y1) in X0 and X1, (x2, y2) in X2 and X3.

Save the X registers that are changed, then calculate load Y2 with the address of the appropriate vertical movement helper: if y2 > y1 we increment y, otherwise we decrement.  Then we load Y3 with the helper routine for horizontal movement.  Increment x if x2 > x1, otherwise decrement.

As a side-effect of those tests, we've also calculated dx = abs(x2-x1) and dy = abs(y2-y1) and put them in X2 and X3.  If dy > dx, we need to swap dx and dy, and the pointers to the helper routines in Y2 and Y3.

Finally, we initialise a3 to the difference between dx and dy, and double X2 and Y2.  Now we're ready to draw
drawLine_loop
mov a2, a0
ora a2, $2000, a1
sta a2, $2000, a1
bra.l 0,y2
sub.w a3, x3
bpl drawLine_skip
bra.l 0,y3
add.w a3, x2
drawLine_skip
dey.w y0
bne drawLine_loop
plx.l x3
plx.l x2
plx.l x1
plx.l x0
rts
Set the current pixel, call the helper routine that Y2 points to (which will step right or left for x-major lines, down or up for y-major).  Then subtract X3 from A3 (this will be double either dx or dy).  If A3 goes negative, we need to step on the other axis and add X2.  Do this in a loop, and the whole line is drawn.  All that's left is to restore the registers we saved at the start and return.

BRA.L 0, Y2 uses a different base register to call a routine whose address is stored in Y2.  Normally, a branch instruction will add an offset to PC and jump to that address.  Here, the offset is 0, and the base is stored in Y2.  This is a very useful feature.

Branches have another extension bit, which I haven't used yet.  This enables indirection.  Instead of register+offset pointing to the branch destination directly, indirection makes it point to a memory location that contains the address of the destination.  This allows tables of function pointers, which could be used to implement virtual functions in a language like C++.  If Y0 points to an object, we might say
ldy y1, (0,y0)
bra.il 10, y1
Y0 points to the the start of the object.  Its first word (two 16-bit bytes) is a pointer to the virtual table.  LDY Y1, (0, Y0) loads this pointer into Y1.  The next instruction adds 10 to this pointer, loads the address of the routine we want to call, and calls it.

Throughout drawLine, I'm using the MOV instruction to copy values from one register to another.  This isn't a real instruction, but gets translated by the assembler into the appropriate transfer instruction.  A move from an X register to an A register, for example, will become TXA.  The existing transfer instructions cover a subset of moves between A, X, Y, and S.  Two extension bits for each of the source and destination give us A0-A3, X0-X3, Y0-Y3, and P, Z, SP, or PC.  Another extension bit for each of source and destination changes the group.  A becomes Y, Y becomes A, X becomes S, and S becomes X.  This gives a complete set of moves from any register to any other register, but the encoding is complicated enough that it's best to leave it to the assembler.

I think the same applies to the ALU instructions.  I currently have different instructions for ADD, ADX, and ADY, which do the same operation (add) on A, X, or Y registers.  Since I'm always explicitly specifying the register, it would be much nicer to just say ADD for all of them, and let the assembler choose the right opcode.

The drawLine routine has also highlighted some missing instructions.  NEG (negate) and ABS (absolute value) would be very useful.  LEA (load effective address, which loads the address specified by an addressing mode into a register, so it can be used later) would also be useful.  I also need to add variants of the shift and rotate instructions that allow a variable shift amount.  And drawLine would benefit from a SWP instruction to swap the contents of two registers (or possibly a register and a memory location).  I'm not sure about that one - it would require adding another write port to the register file, just for one instruction.  It's probably not worth it.

The image at the top was created by a simple test program.  Most of it isn't worth describing, but the pattern in the bottom left brought up one surprise
; Drawing lines in each octant
ldy y0, #0
octantLoop
ldx.w x0, octantLines,y0
cpx.w x0, #$ffff
beq octant_done
ldx.w x1, octantLines+1,y0
ldx.w x2, octantLines+2,y0
ldx.w x3, octantLines+3,y0
phy y0
bra.l drawLine
ply y0
iny #4, y0
bra octantLoop
octant_done
This tests the drawLine routine, getting it to draw a line in each octant.  The surprise wasn't in the code, but in the table of coordinates
octantLines
.byte 10, 180, 40, 190
.byte 40, 190, 50, 160
.byte 50, 160, 20, 150
.byte 20, 150, 10, 180
.byte 20, 190, 50, 180
.byte 50, 180, 40, 150
.byte 40, 150, 10, 160
.byte 10, 160, 20, 190
.byte $ffff
These are all 16 bit values (even though most of them are less than 256), but they're being assembled with .byte.  I had originally used .word, but of course that won't work.  For compatibility with existing source code, .word has to assemble values to two bytes.  Since bytes now contain 16 bits, it splits a 16 bit value into two 8 bit parts, and puts one part in the low 8 bits of each of two bytes.  That's not what you want when you're writing new 65020 code.  The .w modifier on instructions tells the CPU to use all 16 bits from the byte in question.  So .byte has to deal with 16 bit values.  .long will handle 32 bit values, putting them in two adjacent bytes.  .word will very rarely be used in new code.


Wednesday, June 27, 2018

Getting Commodore 64 BASIC to run

The plan was to use the source for the Commodore 64's ROM (https://github.com/mist64/cbmsrc) as a test for the assembler.  If I could get a 100% matching binary, then I could be reasonably confident that the assembler was working correctly, for 8-bit code at least.  I could then start optimising parts of it to get a feel for how well the extended instruction set works, which bits are useful, and what is missing.

This plan immediately ran into a problem, which in hindsight should have been obvious.

With the 65020, a byte contains 16 bits.  Every address used by the Commodore 64 ROM is (at most) 16 bit.  So instructions that need two byte operands in the original get assembled to only one byte.  It doesn't take long for the binary to get out of sync.

However, this approach was still good enough to find and fix a number of simple assembler bugs.  Feeding the result to the simulator gave this:

Commodore 64 start-up screen, "64K RAM SYSTEM  51199 BASIC BYTES FREE"
Great!  But isn't it supposed to be 38911 BASIC BYTES FREE?  It turns out that BASIC starts with a memory test.  It checks every location until it finds something that isn't RAM, and assumes it can use all of it.  My simulator didn't distinguish ROM from RAM, so it kept going until it hit I/O space at $D000.  Write-protecting the ROMs at $A000-BFFF and $E000-$FFFF fixed this.

Now we've got a fully working BASIC.  Except we don't.  It's not much use if you can't type programs in and run them.  So I added some I/O handler code to convert Windows keyboard scan codes into the Commodore 64 keyboard matrix, and return the appropriate values when $DC00 and $DC01 were accessed.

Now we can run a real test

Commodore 64 start-up screen with the program 10 PRINT"HELLO WORLD", followed by ?SYNTAX ERROR
Entering anything would give a syntax error.  That's not how it's supposed to go.

The nice thing about trying things out in a software simulator rather than jumping straight to hardware is that you can create useful debugging tools.  The simulator already had an instruction trace feature, printing out every instruction that is executed, along with the contents of some of the registers (PC, SP, P, A0, X0, Y0).  This quickly revealed the problem.

BASIC uses a small routine called CHRGET, which is copied into zero-page memory.  Here's the important part:
INITAT  INC CHRGET+7
        BNE CHDGOT
        INC CHRGET+8CHDGOT
        LDA 60000
It uses self-modifying code to increment the two bytes of the address (60000 is just the initial value in the source code, used to force absolute addressing mode.  It gets set to different values later).  But on the 65020, 60000 is a one-byte quantity.  That LDA gets assembled with zero page addressing.

Another easy fix, if a little hacky:
CHDGOT  .BYTE $AD, $00, $00
Now I can type in programs.  But it's annoying to type them in every time.  I need to be able to save programs and load them back in.  Again, this is the advantage of a software simulator.  I don't have to emulate Commodore's serial bus and floppy drive, or the tape drive, or anything like that.  I can simply replace the LOAD and SAVE kernal routines with STA LOADTRIGGER and STA SAVETRIGGER, writing to unused I/O locations.  The I/O handler traps these, reads register values from the CPU, and loads or saves chunks of memory to or from a regular file on my PC.

Now, some more tests.  PRINT 1+1 says 2.  PRINT 2*2 says 4.  PRINT 1/10 says 5.95173333E-09

What's going on there?  The floating point divide routine builds the result bit-by-bit.  It starts with A set to 1, and shifts in bits of the result one at a time.  When the 1 bit gets shifted out, the partial result is written to a temporary buffer.  Here's the code that does this:
        LDX #253-ADDPRC
        LDA #1DIVIDE        ... ; do the compare
SAVQUO  PHP
        ROL A
        BCC QSHFT
        INX
        STA RESLO,X
        BEQ LD100
RESLO is the last byte of the buffer.  X is initialised to -4, so the first byte is written to the start of the buffer.  It is incremented each time a byte is written, and when it reaches 0 the loop ends.  This relies on RESLO being in page 0, and zero-page indexed addressing wrapping if RESLO+X is greater than 255.  That's the main incompatibility between the 6502 and 65020.  On the 65020, indexing never wraps.  The solution again is a small change to the source:
        LDX.L #$FFFFFFFD-ADDPRC
        ...
        INX.L 
65020 indexing always uses all 32 bits of the register, so we must load X0 with a 32 bit version of -4, and do a 32 bit increment.

The simulator isn't meant to be a full Commodore 64 emulator, but I couldn't resist adding support for bitmap mode.  Typing in the example from the Programmers Reference Guide gives me this
A "high resolution" sine curve in black, on a cyan background
That's a good place to stop for now.  I have a collection of Commodore's public domain software as a set of .d64 files.  I plan to extract the individual programs and use them as further tests.  I want to play ARTILLERY again!  After that, I'll finally get back to the plan, and start optimising parts of the ROM using the 65020's extended features.  And then, finally, the FPGA.

Sunday, June 17, 2018

The 65020

The original idea for the 65020 came about 30 years ago, as a reaction to the 65816's mode bits.  The 65816 is a better 6502, but it's not a very satisfying processor.  I wanted to do better. 

The key idea in the 65020 is the extension of bytes to 16 bits.  That immediately opens up a lot of possibilities.  Instructions have 8 more bits to specify data width, extra registers, and other operations.  16 bit addresses become 32 bits.

The intention is that if the top 8 bits of every byte are all zero, then it will behave exactly like a 6502.  So far I've managed to do that, with a few minor exceptions: the stack doesn't wrap, and zero-page indexing doesn't wrap either.

Instruction set

Many of the gaps in the 6502's opcode map have been filled with new instructions, and new addressing modes for old instructions:



New opcodes are marked in gray.

  • ACX, ACY: Add with carry, with an X or Y register as destination
  • SCX, SCY: Subtract with carry, with an X or Y register as destination
  • ANX, ANY: Logical and, with an X or Y register as destination
  • EOX, EOY: Logical exclusive-or, with an X or Y register as destination
  • ORX, ORY: Logical or, with an X or Y register as destination
  • MUL, DIV, MOD: Multiply, divide, and mod operations
  • SQR: Square root (if floating point is ever implemented)
  • PHX, PHY: Push an X or Y register to the stack
  • PLX, PLY: Pull an X or Y register from the stack
  • SEV: Set overflow
  • CBT: Clear bit
  • SBT: Set bit


Registers

There are 16 registers:
  • 0-3: A0, A1, A2, A3
  • 4-7: X0, X1, X2, X3
  • 8-11: Y0, Y1, Y2, Y3
  • 12-15: P, Z, SP, PC
P is the processor status register.  If used as an index, a constant zero is used instead.  This explains the abs,0 and zp,0 addressing modes in the opcode table above.  They are now indexed modes, but use P as the default index register.  Selection bits in the opcode extension allow other Y0-Y3, Z, SP, or PC to be used instead.  This provides stack- and PC-relative addressing modes for most instructions.

All registers are 32 bits wide.  In most cases, writing to the low 8 or 16 bits will clear the rest of the register.  The exception is writing to the low 8 bits of SP.  For compatibility, this will set the high 24 bits to $000001.

Extension

Most of the new features come through the opcode extension.  There are a few formats for these, depending on the instruction and addressing mode
  • PRRXXXDD
    • Used by most instructions.
    • P selects the alternate operation
    • RR selects the destination register, or a small constant (1-4)
    • XXX selects the index register or source register
    • DD is the operation width. 00 for 8 bit, 01 for 16 bit, 10 for 32 bit, 11 for floating point (where that makes sense)
  • PAAARRRR
    • Used by bit-set and -clear instructions
    • AAA is combined with the base bit from the instruction to form the bit number
    • RRRR is the destination register
  • SSSRRRDD
    • Used by register-move instructions
    • SSS selects the source register
    • RRR selects the destination register
    • DD is the width
  • WNNNVVVV
    • Used by the BRK instruction
    • W enables waiting for an external interrupt
    • NNN selects the external interrupt to wait for
    • VVVV selects the interrupt vector: $0000fffe - 2*VVVV
  • CDILRRRR
    • Used by branch instructions
    • C selects a different set of conditions (BGE, BLT, BLE, BGT, BLS, BHI, BNV, BRA).  BNV is "never", BRA is "always"
    • I enables indirection.  If it is 0, the target address is base register + offset.  If it is 1, then base register + offset points to a memory location containing the target address
    • L enables subroutine calls.  If it is 1, the current PC will be pushed before the branch is taken
    • RRRR is the base register
The register selection fields in opcode extensions don't encode the register number directly.  Instead, each instruction has a default register (to give standard 6502 behavior when the extension is zero).  The register selection field is xored into the lower bits of this register number.

The 6502's flag-clearing and -setting instructions have been generalised to clear or set any bit of any register.  These general instructions as called CBT and SBT.  So CLC is CBT 0, CLI is CBT 2, and so on.  The AAA bits from the extension are shifted up two places, with the lowest bit copied to fill the new ones.  The resulting value is xored into the bit selected by the instruction.  This gives access to all 32 bits.

Similarly, the register-transfer instructions TAX, TYA, and so on, have been generalised to copy any register to any other.  By xoring the instruction's register number with the bits in the extension, all combinations of source and destination register are covered.

Increment, decrement, and the shift/roll instructions have a small constant encoded instead of a destination register.  This makes it possible to add or subtract numbers up to 4 in a single one-byte (16 bit) instruction.  For the acc mode, these instructions use the index register selection to specify the register.

Alternative operations

Many instructions use the 'P' bit in the extension to select a different operation
  • ADC, ACX, ACY -> ADD, ADX, ADY Add without carry
  • SBC, SCX, SCY -> SUB, SBX, SBY Subtract without carry
  • CMP, CPX, CPY -> CPC, CCX, CCY Compare with carry
  • AND, ANX, ANY -> BIC, BCX, BCY Bit clear (dest <- dest and ~source)
  • MUL, DIV, MOD -> MLS, DVS, MDS Signed multiply, divide, mod
  • SQR -> RSQ Reciprocal square root
  • ASL -> ESL Shift left, filling with the right-most bit
  • LSR -> ASR Shift right, filling with the left-most bit
  • ROL , ROR -> RLB, RRB Rotate without carry
  • CBT -> TBT Test bit.  The Z flag is set if the tested bit is zero, cleared if it is one
  • SBT -> XBT Toggle bit