Wyvern Semiconductors Expert in Digital IP Solutions  
 
 


User Manual
for the cpu6502
Instruction Set Simulator



Simon Southwell

January 2017






Contents

Introduction

 

This package comprises an instruction set simulator, modelling the MOS 6502A 8 bit CPU and variants, with a C++ compatible API. The source is free-software, released under the terms of the GNU licence (see LICENCE included in the package).

Features

 

Included Features:

·         Support for 65C02 instructions

·         Support for Rockwell and WDC instructions, including WAI and STP

·         All supported core instructions for NMOS 6502A

·         NMI and up to 16 wire-or'ed  IRQs

·         Configurable internal memory (compile option)

-       Supports up to 64K bytes

·         Cycle count functionality for accurate timing

·         Run-time disassembly

·         Extensibility via callbacks

-       Intercept memory accesses via register of call back functions

·         Compile options for Linux and Cygwin(makefile) and windows (MS Visual C++ express 2010)

-       Coverage compile support in makefile (lcov/gcov)

·         Can read binary, Intel Hex format or Motorola S-Record program files (standalone compile).

 

The code is a simple exercise in modelling a popular 8 bit CPU, with the aim of being clear and instructive, and provide examples of integration in broader system models. It comes with absolutely no warranties for accuracy, or fitness for any given purpose, and is provided 'as-is'. Hopefully it is useful for someone, and feel free to extend and enhance the model, and maybe let me know how it's going.

 

Simon Southwell (simon@anita-simulator.org.uk)

Cambridge, January 2017

Source files

 

Listed and described here are those source files that make up the cpu6502 ISS. These are the source files needed for integration into other C or C++ environments, or to be compiled as a standalone executable.

 

The main header files comprise those listed below:

 

·         src/cpu6502_api.h

·         src/cpu6502.h

·         src/read_ihx.h

 

For integrating the model with external programs only cpu6502_api.h needs be included in source code that references the API. The cpu6502.h header is only used by the internal source files, and includes all the definitions and types needed by this code. The read_ihx.h is also an internal header, used for the program loading code.

 

The following listed files define the methods that belong to the class cpu6502, along with the above headers specific to those methods. The class methods are split over two files, but all belong to the single cpu6502 class.

 

·         src/cpu6502.cpp

·         src/read_ihx.cpp

 

The entry point methods and program flow methods are all defined in cpu6502.cpp, along with the instruction methods themselves. There is almost a one-to-one mapping of 6502A instructions and the instruction methods, but a couple of methods double up for multiple instructions. Code disassembly is handled by internal methods, also defined in cpu6502.cpp, control of which is handled indirectly by the external API method execute(). The processing of the program files are handled in methods defined in read_ihx.cpp, with its header file as read_ihx.h.

Building code

 

Included in the package is a makefile to build the code under Linux (and also, Cygwin), and support is also provided for MSVC 2010. By default (i.e. simply typing 'make', or building under visual C++) it will build the cpu6502 (or cpu6502.exe) standalone target, and also linkable libraries. For windows lib6502.dll is built, along with the .lib and .exp support files for linking with other programs. Under Linux, a shared object libcpu6502.so and static library libcpu6502.a are built. The standalone build is an executable for running simple programs, particularly the self-test programs provided in the package-see Testing section below.

 

The makefile also, by default, builds the code with optimisations for fast execution, but these can be overridden by defining COPTS (e.g. make COPTS=-g for debug compilation). Additional user options can be added (such as turning on addition warnings, etc.) using USROPTS.

 

A target of 'test' can be specified, which compiles the test images (test/test.as65 and test/test_65c02.as65), and runs the tests on the compiled model. This requires the as65 assembler to be available (see Testing section below).

 

The executable can be compiled to have gcov and lcov support compiled in, by defining DOCOV, and building for a 'coverage' target (e.g. make DOCOV=1 coverage). When building for coverage, COPTS is overridden to force as '-g', and additional test options are used.

API

 

The API to the model is a C++ interface that consists of a single object (of class cpu6502, as defined in cpu6502_api.h) that has a set of methods for configuring the model, setting control of program flow, and running executable code. Definitions are provided in cpu6502_api.h needed to communicate with some of these methods, and set their parameters. This is all described in the sections to follow. In summary, the methods are:

 

                       cpu6502 ();

 

    void               reset              (void);

 

    void               nmi_interrupt      (void);

    void               activate_irq       (const uin16_t  id = 0);

    void               deactivate_irq     (const uin16_t  id = 0);

 

    wy65_exec_status_t execute            (const uint32_t icount       = 0,

                                           const uint32_t start_count  = 0xffffffff,

                                           const uint32_t stop_count   = 0xffffffff,

                                           const bool     en_jmp_mrks  = true);

 

    void               register_mem_funcs (wy65_p_writemem_t p_wfunc,

                                           wy65_p_readmem_t  p_rfunc);

 

     int               read_prog          (const char       *filename,

                                           const prog_type_e type       = HEX,

                                           const uint16_t    start_addr = 0);

 

     int               save_state         (FILE* fp);

     int               restore_state      (FILE* fp);

     int               save_mem           (FILE* fp);

     int               restore_mem        (FILE* fp);

 

Initialisation

 

The model object is created by instantiating a variable of type cpu6502 class, or creating via 'new'. The constructor, cpu6502() does some internal initialisation (but mode of the 6502 CPU state itself), but requires no arguments externally. The CPU itself is initialised with the reset() method, which must be called before execution of a program, but can also be called subsequently to emulate a hardware reset.

 

The reset() method normally also requires no arguments, and this configures the model for running as an NMOS 6502A. However, the model can be configured to add additional instructions for later variants. To add 65C02 instructions, an argument of C02 may be given. To additionally add the instructions supported by Rockwell and WDC (BBR, BBS, RMB, SMB), an argument of WRK may be given. The WDC unique instructions (WAI and STP) can be added on top of all the others with an argument of WDC. At each reset the model can be reconfigured with one of these arguments. To return to an NMOS 6502A model, for instance, an argument of BASE can be given. If a reset does not need to change the support mode it can be called without an argument, or with an argument of DEFAULT.

Loading a Program

 

The cpu6502 model supports three different file formats for loading a program; raw binary, Intel Hex and Motorola S-Records. Three methods are provided for reading these files:

 

·         read_prog()

 

The method requires a filename string, which can be an absolute path, or a path relative to the execution directory. There are a couple of optional arguments; one for specifying the program file type (with an enumerated type prog_type_e), and the other for a load address.

 

In addition, if the file type is BIN, for a binary file, the read_prog() method requires a load address argument, if this is not the default of 0, as this address is not contained within the program file itself. The default type is HEX, for Intel hex format files whereas S-Record files need an argument of SREC. The load address is ignored for Intel hex and Motorola S-Record files.

Execution

 

Once a model object is created, a program can be run via the execute() method. At its simplest, it is called without any arguments to execute a single instruction, and update its internal state. Optional arguments are available to control run-time disassemble output:

 

icount

type

const uint32_t

valid values

Any valid uint32_t number

default value

0

description

An input with the number of instructions that have currently been executed since reset

 

 

 

start_count

type

const uint32_t

valid values

Any valid uint32_t number less than, or equal to stop_count

default value

0xFFFFFFFF

description

The value that icount must have reached before disassembly is enabled

 

 

 

stop_count

type

const uint32_t

valid values

Any valid uint32_t number greater than, or equal to stop_count

default value

0xFFFFFFFF

description

The value that icount must not exceed after which disassembly is disabled

 

 

 

 

en_jmp_mrks

type

bool

valid values

true / false

default value

true

description

Boolean to enable or disable 'jump' marks in the disassembled output. See Disassembled Output section.

 

 

Return Value

  

The execute() method returns a structure of type wy65_exec_status_t, which has the following fields:

 

·         uint16_t pc

·         uint8_t  flags

·         uint32_t cycles

 

The pc and flags fields are direct copies of the state of the 6502A model's program counter and status flags registers after the instruction that was just executed. The cycles field is the number of cycles taken to execute that instruction.

Interrupts

 

The 6502 processor has both an NMI interrupt (an active low edge triggered interrupt) and maskable IRQ interrupt (active low level sensitive interrupt). The IRQ is often connected as a wired-or of several sources of interrupt, and the model provided support for this.

 

To emulate the event of the NMI input going low, the method nmi_interrupt() is provided. This takes no arguments, and will cause the model to jump to the NMI vector on the next call of execute().

 

For IRQ interrupts, the activate_irq() and deactivate_irq() methods are used. An optional argument id (with a default of 0) can be given to control up to 16 wired-or lines (0 to 15). An ID value outside of a 0 to 15 range means the call is ignored. The methods activate or deactivates these lines, and their state is checked as the program executes. If any are active and the model's interrupt bit in the status register is clear, then the PC will jump to the IRQ vector address before executing from there.

 

If the system in which the model is integrated only has a single source of IRQ, or maintains wired-or state externally, then the methods may be called without an argument.

Wait and Stop

When the model is configured to support the WDC unique instructions WAI and STP, execution of these instructions has an effect on the interrupt and execution behaviour. In this model, if either are executed then, assuming no calls to reset or active interrupts, subsequent calls to the execute() method will return a PC continually pointing to the WAI or STP instruction location. On the first call that executes the instruction, a cycle count is returned for the execution of that instruction. Subsequent calls return a count of 0.

 

With the STP instruction only a call to reset() will terminate this condition, and the model will start executing from the reset vector's indicated location. With the WAI instruction, an NMI or an IRQ when not disabled (I bit clear) will make the model jump to the appropriate interrupt vector. An IRQ when the I bit is set will clear the wait status, and execution continues with the next instruction after the WAI instruction.

Callbacks

 

The ISS is a model of a processor core, and its main usage is as a component in a larger system level model. It has an internal memory model for convenience and to aid stand alone testing, but it is via the callbacks that the model can be extended or integrated into a system model of arbitrary complexity. The model supports two user defined callbacks that can be registered with the model. These are for calling at each memory read or write access that the CPU performs.

 

The main use of this extension is to map peripherals (including more memory, if desired) into the memory space via the external memory callback functions, trapping accesses to addresses with memory mapped peripheral registers and implementing the functionality.

 

The callback registration function is described below:

 

register_mem_funcs (wy65_p_writemem_t p_wfunc, wy65_p_readmem_t p_rfunc)

description

The caller must provide as the parameter inputs, firstly a pointer to a function of type wy65_p_writemem_t, e.g.: 

 

void w_func(int addr, unsigned char* data);

 

secondly a pointer to a function of type wy65_p_readmem_t, e.g.: 

 

int r_func(int addr);

 

If the integrating system's own read and write memory function do not fit this model exactly, then wrapper functions should be added, and all memory accesses available from the CPU must be routed through these.

 

 

When this method is used to register external memory access, the model's internal; memory is effectively disabled. In addition, if the method is to be used, it must be called before any other method, including reset().

 

Save and Restore

 

Four methods are provided in the API to save and restore internal model state to a file. The CPU state is separated from internal memory with the methods as internal memory need not be used if external functions have been registered with register_mem_funcs(), and hence does not need to be saved with the CPU state. All four methods take a single argument of a pointer to a FILE. The file is assumed to have been opened for writing successfully prior to calling these methods, and the flushing and closing operation also taken care of externally.

 

The methods for saving and restoring CPU state to a file are, respectively, save_state(fp) and restore_state(fp). If the file pointer argument is NULL, the functions will simply return the size of the CPU state to be saved or restored, in bytes, without modifying any files or values. This is useful in extracting a length value of the data without any knowledge of data structure or types. When the fp argument is a valid FILE pointer, the returned value is the actual number of bytes written to, or read from, the file. If this is less than the value returned with a NULL pointer, then an error occurred.

 

The methods for memory saving (save_mem(fp) and restore_mem(fp)) work in exactly the same way as for the CPU state, only for the internal memory.

Disassembled Output

 

The model can output (to 'cpu6502.log') fully disassembled output during run-time, showing program flow during a normal execution of code on the model. When disassembly is enabled the output looks something like the example fragment shown below:

 

331b   C6 0C       DEC   $0C       a=80 x=0e y=ff sp=ff flags=f1 (sp)=33

331d   E6 0D       INC   $0D       a=80 x=0e y=ff sp=ff flags=73 (sp)=33

331f   D0 E0       BNE   $E0       a=80 x=0e y=ff sp=ff flags=f1 (sp)=33

            *

3301   18          CLC             a=80 x=0e y=ff sp=ff flags=f1 (sp)=33

3302   20 86 35    JSR   $3586     a=80 x=0e y=ff sp=ff flags=f0 (sp)=33

            *

3586   A5 11       LDA   $11       a=80 x=0e y=ff sp=fd flags=f0 (sp)=f1

3588   29 83       AND   #$83      a=80 x=0e y=ff sp=fd flags=f0 (sp)=f1

358a   48          PHA             a=80 x=0e y=ff sp=fd flags=f0 (sp)=f1

358b   A5 0D       LDA   $0D       a=80 x=0e y=ff sp=fc flags=f0 (sp)=b0

358d   45 0E       EOR   $0E       a=e2 x=0e y=ff sp=fc flags=f0 (sp)=b0

358f   30 0A       BMI   $0A       a=e2 x=0e y=ff sp=fc flags=f0 (sp)=b0

 

The default on calling execute is that no output is given. If the optional arguments are set such that it is enabled for a portion of the execution, then the above is the default display. Each line is a single instruction execution, with the 16 bit hex PC address followed by the raw bytes of the instruction (the opcode followed by zero to two operand bytes). The disassembled instruction follows. Any unrecognised instructions will be disassembled as XXX, though an attempt at the size of the instruction is made (i.e. number of operands), based on its opcode. The final set of data is the main internal state of the CPU registers, A, X, Y, SP and flags (PSW). The contents of the value pointed to by the stack pointer is also shown ('(sp)'). Note that the values shown are as set before the instruction is executed.

 

The lines containing '*' are used to indicate where flow is dis-contiguous, due to branches jumps, interrupts etc. If these are not desired, then the execute() method can set the en_jmp_mrk argument to false.

 

Timing Model

 

As has been mentioned before (see Execution section), the execute() method returns status that includes the cycles taken for the executed instruction. This value returned is cycle accurate for the instructions, including extra cycles for branching or page crossing etc. It does not include any extra delays for accessing memory etc.-for example, wait states via the RDY input. These must be added to the returned value to maintain accuracy, if emulating such systems.

Testing

Test Platform

 

As has been mentioned above, an executable environment, cpu6502, is constructed that instantiates the 6502 model, and provides sufficient control and facilities to allow the model to be fully tested. This includes a command line control interface for configuring the model and testing the instructions, as well as interrupts etc.

 

Detailed discussion of the code is not undertaken here-the code is not complicated, and inspection of the source should be sufficient-but a brief description of the program's usage is given. The usage message for cpu6502 is as follows:

 

Usage: cpu6502 [[-f | -I | -M] <filename>][-l <addr>>][-s <addr>]

        [-S <count>][-E <count>][-c][-D]

 

    -f Binary program file name            (default test.bin)

    -I Intel Hex program file name

    -M Motorola S-Record program file name

    -l Load start address of binary image  (default 0x000a)

    -s Start address of program execution  (default 0x0400)

    -S Disassemble start instruction count (default 0xffffffff)

    -E Disassemble end instruction count   (default 0xffffffff)

    -c Enable 65C02 features               (default off)

    -D Disable testing and just run prog   (default off)

 

The first three options (-f, -I and -M) are used to select the input program file for loading in to memory. By default the program expect a binary file named test.bin, but this can be overridden using the -f option. When a binary file is to be loaded, then a load start address must be supplied using the -l option, if this is not the default address (of 0x000a), as this information is not contained within the raw data. If either an Intel hex file format or a Motorola S-record file format is available, then the -I and -M options (respectively) are used to specify the filename. These formats contain load address information in their records, and so do not need the -l option, which is ignored if specified.

 

If the program does not contain a record to load the reset vector (at 0xfffc/0xfffd), then the execution start address must be specified using the -s option. Any valid address may be given, from 0x0000 to 0xffff.

 

The next two options (-d and -i) don't do anything for normal execution. These are present as debug devices if running the code in a debugger (e.g. MS Visual C++ or Eclipse). In the main test loop, in function main(), is a line 'prev_pc = prev_pc' which is only reached if the instruction count is greater than or equal to that specified with the -i options and the PC has reached the break address specified with the -d option. If a breakpoint is set on this source code line then one has control over when the simulation will stop for debugging. Since this may be a long way into a simulation, and a particular address may be hit multiple times before an error occurs, these two controls ease homing in on bugs. Indeed, these were originally just for development of the model, but proved so useful, in conjunction with the disassembled output, they have been left in.

 

Disassembly is controlled with -S and -E. These specify the start and end instruction counts that disassembled output is active. By default the values are set to disable disassembled output. If -S is used to specify a start count of 0, the disassembly is continuously enabled. However the log file can fill very quickly, and the model runs very slowly, so a better practice is to specify a start and end count which brackets the suspected error.

 

The -c option enables all the 65C02 additional instructions, including the Rockwell and WDC instructions. When this option is not specified all unmapped opcodes are treated like NOPS, though of varying sizes their would be instruction format.

 

The -D option disables the testing part of the code, and simply runs the loaded program until a termination condition. This allows the use of the standalone executable to be used to run arbitrary code that does not report test pass/fail status.

 

Test Code

 

The testing falls into two categories: testing of instructions, and testing of interrupts. A piece of luck enabled the first of these to be covered with little effort. A 6502 assembly code test suite was written by Klaus Dormann and made available on github under terms of the GPL licence. There are two test programs we are interested in, which test the base instructions in one program, and the extended instructions (except WAI and STP) in the other. These were modified in a trivial way to write a value 0x900d at location 0xfff8/0xfff9 (initialised to 0x0bad) if the test passed. The code does a 'jump to this location' when complete (either good or bad)-i.e. it deliberately hangs. This can be detected externally by seeing a PC value the same on subsequent returns from execute(), and the test terminated. The code can be compiled by the as65 assembler of Frank Cross, and loaded with the command line options.

 

The testing of interrupts is done explicitly in a function (interrupt_test()) called from main(), by 'poking' values and opcodes into memory, and calling the interrupt API methods. The subsequent execute() PC and flags statuses are checked to validate correct operation. Similarly, testing of WAI and STP instructions is effected with a call to a function wait_stop_tests(), if extended instructions are enabled with the -c option.

 

Executing Tests

 

The tests are run by simply executing cpu6502 -I test/test.hex, for the base instruction tests, or cpu6502 -I test/test_65c02.hex, for the extended instruction tests, from the main directory (the defaults match the compiled test code), assuming that the test code has been compiled for Intel hex format with as65. E.g., from within the test/ directory, execute the command:

 

as65 -s2 test.a65

 

Under Linux, the make file can be used to compile the model, test code and run it, by specifying a build of test (i.e. make test). The output should be something like that shown below:

 

Executing ./test/test.hex from address 0x0400 ...

 

********

* PASS *

********

 

Executed 30.65 million instructions (32.6 MIPS)

 

Executing ./test/test_65c02.hex from address 0x0400 ...

 

********

* PASS *

********

 

Executed 21.99 million instructions (33.5 MIPS)

 

The make file test build compiles the model with optimisations for fast execution (g++ with an option of -Ofast), and compiles the test code for Intel hex format. A debug build can be specified by overriding the COPTS definition (e.g. make COPTS=-g test)

Coverage

 

Code coverage for the self-tests (for Linux only) was performed using gcov and lcov, with support within the makefile to build and execute to generate coverage data. Excluded from the coverage was any disassembler or debug output code as, although this can be covered to a level of 100%, it cannot be verified in an automatic self-test, and it is does not affect the accuracy of the model.

 

The diagram below shows the LCOV report generated by executing the following commands:

 

make clean

make DOCOV=1 coverage

 

The 'make clean' is important as the coverage is accumulative, and any old results must be deleted, unless the intention is to merge results. The report generated is created in the directory cov_html/src, and accessed via index.html.





In order to obtain a goal of 100% coverage, some waivers on lines of code were needed on unreachable lines of code, (e.g. failed to open a file for reading) and the disassembler code, as mentioned above.

 

Source Code Architecture

It is not the intention to go into minute detail for the internal architecture of the model here, but a brief overview of the main program flow, internal state, and major structures is in order, to allow anyone wishing to understand or modify the code enough of a handle, that they can explore the details on their own.

Main Execution Flow

 

The main entry point for the model is the execute() method, which advances by one instruction. Below is shown some pseudo code of the structure of this method.

 

// --------------------------

// Execute next instruction

execute()

  irq()

 

  opcode = rd_mem(pc++)           // Fetch instruction

  curr_instr = instr_tbl[opcode]  // Lookup table

 

  if disassembly enable...

    disassemble()

  endif

 

  num_cycles = curr_instr()        // Execute instruction function

 

  cycles +=  num_cycles            // Update cycles with returned count

  return {num_cycles, pc, flags }  // Return status

 end execute

 

The method starts by calling the irq() function to check for outstanding interrupts (see below). The opcode is fetched from the PC memory location, and a lookup is performed on the instruction table, which was initialised during cpu6502's construction. The table consists of a 256 entry array, where each entry is of a structure type, cpu6502::tbl_t.

 

// Instruction table entry type.

typedef struct

{

    const char*       op_str;

    pInstrFunc_t      pFunc;

    uint32_t          exec_cycles;

    addr_mode_e       addr_mode;

 

} tbl_t;

 

Each entry has a pointer to a string for the instruction (e.g. "ASL"), a pointer to the instruction function, the base number of cycles for the instruction and the address mode for the particular variant of the instruction. The function pointer is for a function that takes a single argument of type cpu6502::opt_t, and returns an integer (with the final cycle count for the instruction, including run-time extras, such as page crossing, or branching).

 

The table lookup is stored locally in curr_instr, and the method pointed to by pFunc is executed, with the other information from the lookup passed in. The returned cycle count (including extras) is added to the total count, and some status is returned in a structure (wy65_exec_status_t) which includes the executed cycles, PC and flags.

 

The instruction methods vary in function in their detail, but all have a general structure that is similar to all of them. Below is shown some pseudo code for a generic instruction function.

 

// --------------------------

// Generic instruction

<curr_instr>()

  addr = calc_addr()               // Get address of operand (and update PC)

 

  <do opcode function>             // Perform instruction's function

 

  update flags

 

  return opcode cycles             // Minumum #cycles

            [+ page crossed cycle] // page cross on relevant instructions

            [+ branch taken cycle] // Branch take cycle (branches only)

 

 end <curr_inst>

 

Firstly the method fetches the address in memory where the operand data can be found via the calc_addr() method. This method is called in all cases, even for those opcodes that have no address such as those with implied addressing or accumulator operations, and these functions ignore addr. On entry the PC will be pointing passed the opcode itself to any operand bytes, and on exit the PC is pointing to the next instruction's opcode.

 

The particular operation of the function is then performed (ANDing, subtracting, memory read etc.), before the flags are updated as necessary (not all instructions alter the flags). The number of cycles is then returned. The calc_addr() method returns a flag if a page was crossed which adds a cycle, and branch instructions add a cycle if the branch is taken.

 

The calc_addr() method selects the address mode, and generates a final location where the operand data can be found, incrementing the PC (where appropriate) in fetching the instruction argument bytes. Pseudo code for the function is shown below.

  

// --------------------------

// Calculate address

calc_addr()

  case address mode of ...

    IND : addr = rd_mem16(rd_mem16(pc));   pc += 2;

    IDX : addr = rd_mem16(rd_mem(pc) + x); pc += 1;

    IDY : addr = rd_mem16(rd_mem(pc)) + y; pc += 1;

    ABS : addr = rd_mem16(pc);             pc += 2;

    ABX : addr = rd_mem16(pc) + x;         pc += 2;

    ABY : addr = rd_mem16(pc) + y;         pc += 2;

    ZPG : addr = rd_mem(pc);               pc += 1;

    ZPX : addr = rd_mem(pc) + x;           pc += 1;

    ZPY : addr = rd_mem(pc) + y;           pc += 1;

    REL : addr = rd_mem(pc) + pc + 1;      pc += 1;

    IMM : addr = pc;                       pc += 1;

    IAX : addr = rd_mem16(rd_mem16(pc+x)); pc += 2;

    ZPR : addr = rd_mem16(pc+1) + pc + 2;  pc += 0; // Leave PC at ZP operand

    IDZ : addr = rd_mem16(rd_mem(pc));     pc += 1;

    ACC : NON: addr = don't care; // Implied, so no operands

  end case

 

  return addr, pc, page crossed flag

end calc_addr

 

The method is a simple case statement for all the supported addressing modes. The argument bytes following the location of the opcode (if any) are read, and any follow-up read for indirected modes, to create a final location which is returned, along with the updated PC and a page crossing flag (details not shown for clarity). In the case of accumulator and implied addressing modes, where there are no argument bytes, the method does nothing, but the address returned is INVALID_ADDR, so that calling routines can detect this condition.

 

The interrupt method (irq()) checks for outstanding interrupts and, if the I bit of the flags is clear, updates the PC with the interrupt vector after pushing the PC on to the stack, along with the flags. The flags then have the I bit set to mask further interrupts. The NMI is similar, except that there is no check, and the NMI is always actioned. Pseudo-code for these two functions is shown below:

 

 

// --------------------------

// Interrupt check and update

irq()

  if active IRQ and flags' I bit clear...

    push(pc high byte)

    push(pc low byte)

    push(flags & ~BRK_MASK)

    

    set flags I bit

    pc = mem[] 16 bit value at 0xfffe and 0xffff

    cycles += IRQ_CYCLES

  end if

end irq




// --------------------------

// NMI Interrupt

nmi()

  push(pc high byte)

  push(pc low byte)

  push(flags)

    

  set flags I bit

  pc = mem[] 16 bit value at 0xfffa and 0xfffb

  cycles += NMI_CYCLES

end nmi

Compile Options

 

By default, when cpu6502 is compiled, it has the behaviour as described in the previous sections. However, it can be compiled with various definitions in order to modify its behaviour. There are, presently, three conditional compile definitions that can be set:

 

  • WY65_STANDALONE : When defined a main function is included that has the testing facilities as defined above, and the default make and MSVC solution have this defined. If not defined, the code will compile without a main() function, so as to be included as part of another environment.

  • WY65_EN_PRINT_CYCLES : Defining this enables the printing of cycle counts in the disassembled output.

  • WY65_MEM_SIZE : The internal memory model of the ISS has a size of 65536 (the maximum address space of the 6502). This can be altered by defining this to a different value. If the register_mem_funcs() method is used to set external memory accesses in place of the internal memory it is useful to set this to 1 (though not necessary for functionality) to reduce the memory footprint of the unused array.

 

Case Study: Integration into 'BeebEm'

 

In order to demonstrate the use of the model within a system context, the ISS was integrated into the BeebEm application that emulates the BBC micro and variants. The heart of the BBC microcomputer was a 6502 processor, and the BeebEm program has a model of the processor within it. This will be replaced with the cpu6502 model as a study and example of integration, and for further testing of the model. Once integrated the BeebEm executable runs just as before, and is capable of running BBC micro software, but now using the cpu6502 model.

 

 



The BeebEm application is a sophisticated model, supporting many variants of the BBC Micro. Beyond the emulation of the Basic BBC Model B, variants of the 6502 are modelled that have additional opcodes, both documented and undocumented. The cpu6502 models all of these and works for all configurations.

Integration Details

 

Integration of the cpu6502 model into BeebEm is straightforward. To integrate the cpu6502 model into the BeebEm model, the instruction execution code and the code for the two interrupts (NMI/IRQ) must be replaced. In addition, the cpu6502 model must have access to the local memory system, and two BeebEm memory access routines (BeebWriteMem() and BeebReadMem()) need to be registered as callback functions with the model, via the register_mem_funcs() method. Fortunately, this can all be done with some minor changes to a single file-6502core.cpp-found in the src/ directory of BeebEm package, version 4.14 for windows, which can be downloaded from here:

 

  http://www.mkw.me.uk/beebem/BeebEm414.zip

 

This package comes with a Visual C++ solution file (BeebEm.sln) for use with Microsoft Visual C++ 2008, but the testing done by the author is with 2010 express, which will convert this solution for use in that newer version. It does not support Linux (though cpu6502 does), but the original BeebEm, from which the windows version is derived, can be found at:

 

  http://beebem-unix.bbcmicro.com/download/beebem06-1.tar.gz

 

The 6502core.cc file of this version is not dissimilar to the windows derivative, and might easily be modified to use the cpu6502 model in a similar manner to the details given below, but this has not (yet) been tried.

 

The modified code of the windows version of BeebEm is not included in the package for cpu6502, but below is detailed the few changes required to update the source to use the cpu6502 model. The modifications are such that the default behaviour is to run with the BeebEm processor model. In order to use the cpu6502 model, definition below must be set at compilation (C/C++ Pre-processor configuration, added to the Pre-processor Definitions field):

 

·         TEST_CPU6502

 

The details of the changes are given below, and the code may be cut and pasted directly into the file. No original code is altered, only new, compile dependant code added. The line numbers refer to the original, unmodified 6502core.cpp file for version 4.14.

 

·         The cpu6502 model must be added to the BeebEm file. So after the system include file references, insert the following code between lines 32 and 33:

 

#ifdef TEST_CPU6502

#include "cpu6502_api.h"

cpu6502 cpu6502;

#endif

 

·         The model will need access to the BeebEm's internal memory access functions, and then be reset whenever BeebEm is initialised or reset. The two BeebEm memory functions are registered with the cpu6502 mode via the register_mem_funcs() method, and then the reset() method called. To implement this change, insert between lines 1066 and 1067, after line containing 'NMILock=0;', in the BeebEm function Init6502core():

 

#ifdef TEST_CPU6502

  // Register the local memory access functions with the cpu6502 model

  // to allow use of BeebEm local memory.

  cpu6502.register_mem_funcs (BeebWriteMem, BeebReadMem);

 

  // Reset the cpu6502 model

  cpu6502.reset();

#endif

 

·         The BeebEm main executions functions is called Exec6502Instruction(), and contains its 6502 functionality, both for instruction executions and NMI/IRQ interrupts. The instruction execution code is ifdef'ed out, and replaced with cpu6502 calls. Insert between lines 1202 and 1203, after the call to 'AdvanceCyclesForMemRead();' in Exec6502Instruction():

 

#ifdef TEST_CPU6502

                // Execute a single instruction in the model

                wy65_exec_status_t status = cpu6502.execute();

    

                // Update local variables with returned status

                Cycles                   += status.cycles;

                ProgramCounter            = status.pc;

                PSR                       = status.flags;

 

                // Clear any active maskable interrupt

                cpu6502.deactivate_irq();

#else

 

·         To end the #ifdef/#else of the above code, insert between lines 2250 and 2251, just before 'PollVIAs(Cycles - ViaCycles);', in function Exec6502Instruction():

 

#endif

 

·         For the IRQ code, the DoInterrupt() function is ifdef'ed out and replaced with cpu6502 calls. In particular, a call to the activate_irq() method is made. The equivalent call is made to deactivate_irq() after the call to execute() (see above). Insert between lines 2260 and 2261, before 'DoInterrupt();':

 

#ifdef TEST_CPU6502

                        cpu6502.activate_irq();

                        IRQCycles = 7;

#else

 

·         To finish the #ifdef/#else pair, insert between lines 2261 and 2262, after  the function 'DoInterrupt();':

 

#endif

 

·         For the NMI code, the DoNMI() function is ifdef'ed out and replaced with cpu6502 a call to the model's nmi_interrupt() method. Insert between lines 2270 and 2271, before 'DoNMI()':

 

#ifdef TEST_CPU6502

                        cpu6502.nmi_interrupt();

                        IRQCycles = 7;

#else

 

·         To finish the #ifdef/#else pair, insert between lines 2271 and 2272, after 'DoNMI();':

 

#endif

 

The above additions are all that is required to integrate the cpu6502 model into BeebEm. When compiled with TEST_CPU6502 defined, the resultant executable will be using the model in place of its own 6502 ISS.

 

Performance

 

Running the standalone tests executes some 30 million instructions, and the program contains code for measuring the time it takes to execute the main instructions' test loop. The code is measured with optimised compilations (i.e. Release mode for MSCV, and -Ofast for gcc). The platform used was an Intel® i7 920 CPU, running at 2.67GHz, with a system having 6GB RAM on an ASUS P6T SE Motherboard.

 

The results are summarised in the table below:

 

OS

Compiler

Perfomance

Windows 10

MSVC Express 2010

32.5 MIPS

Ubuntu 16.04 LTS

gcc v5.4.0 (-m32)

31.7 MIPS

gcc v5.4.0 (-m64)

34.7 MIPS

Cygwin

gcc v5.4.0 (-m64)

34.3 MIPS

 

It is interesting to note that a model of the 8 bit processor, introduced in 1975, runs at up to 17 times faster than that actual hardware used on, say, the Acorn BBC Micro, on what is a fairly modest desktop computer, whose particular processor was launched in 2008, and illustrates the rapid advancement in the intervening, though relatively short, time period.

 

The performances measured far exceed anything that might be needed for any real-time application using the model (such as BeebEm, for example), and no optimisation of the code has been done to increase the performance of the code. I'm sure that if some effort were put into optimising the code further speed increases would be possible—this might be useful if integrating in a larger system model, where the processor model musn't be the limiting factor in order to achieve real-time simulation, or if the platform is a much lower spec. machine. In reality, if using the model to simulate a system in real time, some synchronisation between the model's cycle counts, simulated target frequency, and a system clock needs to be done, just as is done with the BeebEm simulator.

 

Downloads


The model is released under version 3 of the GPL, and comes with no warranties whatsoever. A copy of the license is included. The cpu6502 package is available for download from github. As well as all the source code, make files and MSVC 2010 file, the package contains all the test assembly code, and means to run them.

 

Further Reading

 

[1]  MCS6501 - MCS6505 Microprocessors Datasheet, MOS Technology Inc., August 1975

[2]  R650X and R651X Microprocessors (CPU), Rev 8, Rockwell, June 1987

[3]  SY6500/MCS6500 Microcomputer Family Programming Manual, Synertek Inc, August 1976

[4]  BeebEm website, http://www.mkw.me.uk/beebem/ , retrieved Jan 2017.

[5]  BeebEm for UNIX website, http://beebem-unix.bbcmicro.com/, retrieved Jan 2017

[6]  6502_65C02_functional_tests git hub page, https://github.com/Klaus2m5/6502_65C02_functional_tests/ , Klaus Dormann, retrieved Jan  2017

[7]  Frank Kingswood's as65 Cross Assembler, http://www.kingswood-consulting.co.uk/assemblers/ , retrieved Jan  2017