ED64 - HOW TO WRITE A
COMMODORE 64 EMULATOR
Copyright 2006 ir. Marc Dendooven
Chapter 2 : Emulating the 6510
instruction set
Introduction
In Chapter 1 we created a simple emulator model an implemented it.
Now we have to add the instruction set of the processor.
Processor instructions
Introduction
A machine code programmer can use three user registers in the processor: A (the accumulator), X and Y (the index registers)
Let’s take two common used machine code instructions: LDA and STA.
LDA stands for Load Accumulator. It loads A with a value from memory. STA stands for STore Accumulator. It stores the value of A in memory.
We can implement a machine code instruction as a pascal procedure:
procedure lda (address : word);
begin
A := peek (address)
end;
procedure sta
(address : word);
begin
poke (address, A)
end;
The address used depends on the memory mode of the instruction.
memory modes
immediate mode
LDA# $12 means load the accumulator with the value $12
The corresponding machine code is A912
The value to be loaded in A comes immediately after the opcode. This is the place where PC points to after that the opcode has been fetched in IR in the main processor loop.
If we implement a memory mode as a pascal function this gives:
function imm : word;
begin
imm := PC;
inc(PC)
end;
PC is incremented so it points at the next byte in memory.
Remark: Altering toplevel variables in a function is called a ‘side effect’ and is considered as bad programming practice. How ever in a low level programming project like in an emulator it is often necessary to use constructs like these.
The case entry for LDA in immediate mode becomes then:
$A9 : lda(imm);
It will be clear that STA in immediate mode makes no sense. It does not existe.
absolute mode
LDA $1234 means load the accumulator with the content of memory address $1234
The corresponding machine code is AD3412
Remark : Little endian.
When a two byte address is stored in two consecutive bytes we speak of ‘big endian’ when the high byte is stored first. On the c64 the low byte is stored first. This is called little endian. We will create a function to read little endian addresses.
function peek2(address : word) :
word;
begin
peek2 := peek(address) +
peek(address+1)*256
end;
using this function we can implement absolute mode:
function abs : word;
begin
abs := peek2(PC);
inc(PC,2)
end;
Remark that instructions in absolute mode are three bytes long. So PC should by incremented by 2.
in the case we can now write :
$AD : lda(abs);
$8D : sta(abs);
absolute, indexed by X
This memory mode is equivalent to absolute mode, but the address is incemented by the X register.
function absx : word;
begin
absx := peek2(PC) + X;
inc(PC,2)
end;
absolute, indexed by Y
Analogously:
function absy : word;
begin
absy := peek2(PC) + Y;
inc(PC,2)
end;
absolute indirect mode
in this mode, the address after the opcode points to a location containing the address to be used.
function ind : word;
begin
ind := peek2(peek2(PC));
inc(PC,2)
end;
zero page mode
The processor uses the first 256 bytes of memory (the zero page) as a special storage area: these bytes can be referenced by a one byte address (the high byte is zero).
Using 2 byte addresses takes more memory and processor time.
function zp : byte;
begin
zp := peek(PC);
inc(PC)
end;
zero page, indexed by X
function zpx : byte;
begin
zpx := peek(PC)+X;
inc(PC)
end;
remark that the the address MUST be in the zero page! The sum of peek(PC) and X could be greater than 255. This is enforced by using byte as the returntype of the function instead of word.
zeropage, indexed by Y
function zpy : byte;
begin
zpy := peek(PC)+Y;
inc(PC)
end;
indexed indirect
function indx : word;
begin
indx :=
peek2(byte(peek(PC)+X));
inc(PC)
end;
Remark the explicit byte conversion to stay in the zeropage.
indirect indexed
function indy : word;
begin
indy := peek2(peek(PC)) +
Y;
inc(PC)
end;
The entries in the case become then:
$A9 : lda(imm);
$A5 : lda(zp);
$B5 : lda(zpx);
$AD : lda(abs);
$BD : lda(absx);
$B9 : lda(absy);
$A1 : lda(indx);
$B1 : lda(indy);
$85 : sta(zp);
$95 : sta(zpx);
$8D : sta(abs);
$9D : sta(absx);
$99 : sta(absy);
$81 : sta(indx);
$91 : sta(indy);
State register
The implementation of STA is ok, since it doesn’t change the state of the processor. LDA on the other hand alters the value of the accumulator, and this should be represented in the state register.
The state register (P) is a byte register containing different flags (state indicators) represented by individual bits. These are NV.BDIZC (bit 3 is not used). The reference guide explains the meaning of this flags. We should provide methods to read and write individual bits in P.
If we define the following constants:
const
C = $01; //0000 0001
Z = $02; //0000 0010
I = $04; //0000 0100
D = $08; //0000 1000
B = $10; //0001 0000
V = $40; //0100 0000
N = $80; //1000 0000
We can read an individual flag by:
function flagset (flag : byte) :
boolean;
begin
flagset := boolean(P and
flag)
end;
and set a flag by:
procedure setflag (flag : byte;
status : boolean);
begin
if status then P :=
P or flag
else P :=
P and not flag
end;
remark: two procedures ‘setflag’ and ‘clearflag’ would be more efficient, but would produce lesser clear code later on.
LDA can now be altered to represent the correct state:
procedure lda (address : word);
begin
A := peek (address);
setflag(Z,A=0);
setflag(N,A>=$80)
end;
Remark: the reference guides state what flags are changed for each instruction.
Some machine code instructions simple set or clear a flag. They can be implemented directly in the case, with setflag used as a generic procedure.
e.g. SEC (SEt Carry) and CLC (Clear Carry) can be implemented by:
$38 : setflag(C,true); //sec
$18 : setflag(C,false); //clc
Simular instructions
Most of the instructions are similar to LDA and STA. We will not treat them here in detail. Look in ed64.pas. You will find the implementation quiet obvious.
Stack operations
An other type of instructions are stack instructions. Some registers can be pushed on or pulled of the stack.
We will implement two generics named push and pull.
The stack is physically placed in memory page 1. (from $100 to $1FF) and is referenced by the stack pointer (S). Since S is a byte register, it points to the stack by adding $100 to S.
procedure push (b : byte);
begin
poke ($100+S,b);
dec(S)
end;
function pull : byte;
begin
inc(S);
pull := peek($100+S)
end;
Jumping instructions
Jumping instructions alter the flow of a machine code program. A jump to an address works by putting this address in the PC.
procedure jmp (address : word);
begin
PC := address;
end;
Branching instructions
Branching is a relative jump. The (single) databyte is considered as a two’s complement (a value that can be positive or negative). A positive value jumps forward, a negative value jumps backward.
All branches are conditional (depending on a flag). So we will implement two generic procedures bfs (Branch on Flag Set) and bfc (Branch on Flag Clear):
procedure bfs (flag :
byte);
begin
if flagset(flag)
then PC := PC +
shortint(peek(PC)) + 1
else inc(PC)
end;
procedure bfc (flag :
byte);
begin
if flagset(flag)
then inc(PC)
else PC := PC +
shortint(peek(PC)) + 1
end;
remark: the value is treated as a two’s complement by typecasting to shortint.
Subroutines
JSR (Jump SubRoutine) and RTS (ReTurn from Subroutine) are a combination of jumping and stack operation:
procedure jsr (address : word);
begin
dec(PC);
push(hi(PC));
push(lo(PC));
PC := address
end;
procedure rts;
begin
PC := pull;
PC := PC + pull*256 + 1
end;
Decimal mode
Decimal mode is not implemented. An error is generated if the D flag is set by SED. If the D flag should be set by any other method (e.g. PLP) this is not detected !
Interrupts
Interrupts are not implemented. All instructions regarding interrupts are implemented (but since there are no methods for generating them, they will not occur)
Undocumented instructions
There are some instructions in the 6510 that are not documented. These are not implemented at the moment.
Start address
The 6510 processor looks at address $FFFC to find where to start the execution of a program:
PC := peek2($FFFC);
Conclusion
Look at the listing to see the complete implementation of the 6510 instruction set.
Debugging
When executing the emulator at this point, nothing will seems to happen since the program works only on internal data. We will add some debug lines in order to follow what is happening.
var trace : boolean;
…
procedure dump1;
begin
write('PC=',hexstr(PC,4),'
IR=',hexstr(IR,2));
end;
procedure dump2;
begin
writeln('
A=',hexstr(A,2),' X=',hexstr(X,2),
'
Y=',hexstr(Y,2),' S=',hexstr(S,2),
'
P=',binstr(P,8));
end;
…
trace := true;
…
while true do
begin
IR := peek(PC);
if trace then dump1;
inc(PC);
case IR of
…
else
error ('unknown
instruction ')
end;
if trace then dump2
end
If trace is true PC and IR are displayed before, and the other registers after executing the machine code (so that we can see what the instruction has done with them).
It will be clear that this trace will slow down the emulator. Even when trace is false a check will be done each loop. So the trace code should be removed in the definitive version.
Executing the program
Executing the program will not have much sense since no machine code is present in the memory.
We will test it by adding a machine code program: The following program will count from 5 downto 0. We will exit the program with an unknown instruction.
Assembler
Machine
code
LDX
#$05 A205
LOOP DEX CA
BNE LOOP D0FD
(EXIT) FF
To store the program at address $C000 we should add the following lines in the emulator program:
poke($C000,$A2); poke($C001,$05);
poke($C002,$CA);
poke($C003,$D0); poke($C004,$FD);
poke($C005,$FF);
and to set the starting address:
poke($FFFC,$00); poke($FFFD,$C0);
Of course this poke’s should be removed from the real program later on. They are just included for this demonstration.
The complete code can be found in ED64.pas
After compiling memio.pas and ED64.pas we can execute the program. The execution of the machine code program can be monitored in our trace. Remark that X is decremented and that the zero flag is set in order to exit the loop.
--------------------------------------
Welcome to EL DENDO's c64
emulator
(c) 2006 ir. Marc Dendooven
--------------------------------------
PC=C000 IR=A2 A=00 X=05 Y=00 S=00
P=00000000
PC=C002 IR=CA A=00 X=04 Y=00 S=00
P=00000000
PC=C003 IR=D0 A=00 X=04 Y=00 S=00
P=00000000
PC=C002 IR=CA A=00 X=03 Y=00 S=00
P=00000000
PC=C003 IR=D0 A=00 X=03 Y=00 S=00
P=00000000
PC=C002 IR=CA A=00 X=02 Y=00 S=00
P=00000000
PC=C003 IR=D0 A=00 X=02 Y=00 S=00
P=00000000
PC=C002 IR=CA A=00 X=01 Y=00 S=00
P=00000000
PC=C003 IR=D0 A=00 X=01 Y=00 S=00
P=00000000
PC=C002 IR=CA A=00 X=00 Y=00 S=00
P=00000010
PC=C003 IR=D0 A=00 X=00 Y=00 S=00
P=00000010
PC=C005 IR=FF
--------------------------------------
emulator error: unknown
instruction
PC=C005 IR=FF
Execution has been ended
push return to exit
--------------------------------------
Conclusion
The emulator is now capable of emulating machine code programs for the 6510 processor.
In the following chapters we will load the C64 kernel and basic ROM’s in the emulator, and emulate some minimal input an output hardware to obtain a real working C64 emulator.