Dennis Chen edited this page Dec 16, 2015 · 9 revisions

Introduction: Who is This Guide For?

This guide is for people who want to write a really rudimentary emulator for the 6502 CPU. While much of this guide is related to the CPU and programming language we chose to implement our emulator in, there are a lot of computer architecture and emulation concepts that you might find interesting. If you’re comfortable with a programming language and you’ve heard some computer architecture terms before, this guide should be interesting to you! We’ll walk you through the process we went through in writing a primitive 6502 emulator, and discuss some computer architecture concepts along the way. This is only a surface level treatment of the topic, and this guide does not cover things like cycle timing emulation. We hope that this will be a useful resource for someone trying to get started writing a small emulator on their own. Please feel free to email us if you spot errors!

What’s Our Goal?

“Emulating the 6502” is a vague sounding statement that doesn’t tell us much about where to start. For our purposes, the goal was to build software that can act like a 6502 CPU. What does the 6502 do? At a high level, it should take 6502 assembly code, execute it, and output the results of running the code. So we’d like our emulator to do all of this too!

Breaking Down the 6502

6502 assembly code that we write looks like this:

 LDX #00
 
 LDY #00

This assembly program loads the value of zero into the X and Y registers of the 6502 CPU. If you aren’t familiar with registers, think of them as the scratch space that a CPU uses to do its calculations. The 6502 instruction set, or all of the possible programming instructions you can give the CPU (LDX and LDY are two such instructions) has instructions for reading from memory into the registers or scratch space, performing manipulations on values in the scratch space, and writing values from this scratch space back out to memory.

You may wonder how the CPU ‘knows’ what to do when it gets a command like ‘LDX #$00’. In fact, the CPU doesn’t. An assembler translates ‘LDX #$00’ to ‘a2 00’ in hexadecimal, or base 16, before the CPU ever sees it. If you are unfamiliar with hexadecimal or binary, the wiki page here would be a good read. The 6502 CPU expects everything it sees to be stored in bytes, eight bits at a time. The byte ‘1010 0010’, or ‘a2’ in hexadecimal notation, represents the LDX instruction in the immediate addressing mode. The CPU understands when it sees ‘a2’ that it needs to perform the LDX instruction using an immediate. What is an immediate? It’s a byte value that is specified directly by the user in the program, and in assembly we denote immediate values with a ‘#$’ before them. When the CPU sees a2, it thinks ‘Oh! I need to get an immediate byte value to load into register X. I know that I can find this immediate value directly after the LDX instruction. So I’ll look at the byte following a2 for that value.’ The CPU then loads the byte ‘00’ into the X register.

Don’t worry if this doesn’t quite all make sense yet. The high level takeaway here is that the 6502 will interpret a byte as a specific instruction it needs to execute with a specific addressing mode. This point can be confusing- the LDX in immediate mode is represented by a different byte than LDX in zero page (zero page is another kind of addressing mode). LDX immediate is ‘a2’, while LDX zero page is ‘a6’. We need to distinguish between the two because the CPU behavior changes based on the addressing mode. If it helps, you can think of LDX immediate and LDX zero as distinct commands you are giving the CPU that result in different things. The commands just happen to share some things in common, so they are both considered LDX instructions.

One thing we haven’t mentioned yet is the program counter (PC). The program counter is a special CPU register that has one very important job: keep track of which instruction the CPU should run next. Two important things to note about the program counter for the 6502: It is an unusual register because it holds 16 bits, not 8 bits, and it starts off with the value greater than or equal to 0x0200 when the 6502 is powered on (0x denotes that the numbers following it are hex values). The program counter changes with every instruction.

Why does the program counter have 16 bits when every other register has 8 bits? It needs 16 bits to keep track of which instruction to run because the 6502 has memory that needs to be addressed with 16 bits. Anything that needs to be stored for the long term goes into memory. So that includes the program the CPU is running, something called the stack (we’ll talk about that later), and any values that we want to keep around for the duration of the program. Note that registers I mentioned earlier are entirely separate from this memory! How much memory does the 6502 have? Well, it has 65,536 or 2^16 bytes of memory. A strangely specific number, huh? Well, the memory is this size exactly so that we can address it with 16 bits! For our CPU to keep track of where something is within memory, it needs 16 bits, where 0x0000 refers to the zeroth (or the first, sequentially) byte of memory, and 0xFFFF refers to the 2^16 (or last) byte of memory.

Now why does the program counter start with a value of 0x0200 or greater? And what’s this about it constantly changing? Well, on the 6502, the first 0x01FF bytes of memory are reserved for zero-page memory and the stack, something we’ll discuss later. So, when we choose to load our programs into memory, we have to place it after 0x1FF. Exactly where depends on how a manufacturer chooses to use the chip. On the NES, which uses a 6502 chip, for instance, programs are loaded into memory starting at 0x0600. It’s just important to pick a value and stick with it, setting our PC to that value when execution of our program starts. Why does the program counter constantly change? Well, because we want to go through our program sequentially! After each instruction is run, our program counter is automatically incremented by some amount so that the next instruction can run. How much the program counter increments depends on the instruction length. Some instructions are one byte long, but others can be two, three, or four bytes. We’ll have to think about this later when we code the CPU. Additionally, there are some instructions that directly change the value of the program counter in order to jump execution elsewhere in the code. This simple seeming concept of being able to change the program counter value gives us all of our control flow statements in programming, like if statements, for loops, and while loops.

What Does Our Emulator Need?

Now that we’ve discussed some aspects of the CPU, we actually have enough to start thinking about how we plan on implementing it. Let’s break our program down into smaller parts. We’ll need:

  • An assembler that can take 6502 assembly and turn it into the corresponding hex code. In other words, it transforms something that looks like ‘LDX #$00’ into something like ‘a2 00’. This is technically not part of the emulator itself, but it’s something we’ll need if we want to actually run assembly code.
  • Something that takes hex code generated by the assembler and loads it into our representation of the system’s memory.
  • The emulator itself- something that fetches instructions from memory based on the program counter value and executes fetched instructions, changing the state of the registers and memory appropriately.

The third part is the most complex, so we’ll start by walking through what it takes to model the hardware state of the 6502 and to emulate execution of instructions.

Modeling the 6502

First, we need to figure out what we need to represent the hardware state of the 6502 in order to emulate it. Based on our discussion so far, we need to represent the registers and the memory. So, we can start with a simple program that declares all of these things and write a getCPU function that returns a struct representing the hardware state of our emulated 6502. The example code below is in C.

typedef struct cpu {
    uint16_t PC; //PC stands for program counter
    //8 bit registers
    int8_t status;
    int8_t stack; 
    int8_t x;
    int8_t y;
    int8_t accum;
    //hardware memory
    int8_t *memory;
} CPU; 

CPU * getCPU(){
    CPU *c = malloc(sizeof(CPU));
    c->PC = 0x600; //set program counter to start at 0x600 in memory
    c->stack = 0xFF; //initialize stack pointer to 0xFF (we’ll discuss this later)
    c->status = 0b00100000 //set unused bit to 1 (we’ll discuss this later)
    //initialize address space
    int MEMORY_SIZE = 65536;
    int8_t *m = calloc(MEMORY_SIZE,sizeof(int8_t));
    c-> memory = m;
    return c;
}

Note that our CPU has five eight bit registers. The five registers are the X, Y, ACCUMULATOR, STATUS, and STACK registers. There’s lots of great documentation about these registers and their functionality online already, so we’d suggest taking a look at this amazingly thorough guide to understand what specific registers are used for. As a quick summary, the X,Y, and ACCUMULATOR registers are general purpose registers where math operations take place. The stack register keeps track of where the top of the runtime stack is in memory. The status register is a register that is used to keep track of specific flags, or boolean conditions. These flags are often changed by or used for operations. For example, the Arithmetic Shift Left instruction with the Accumulator addressing mode shifts the byte in the ACCUMULATOR register to the left by one bit and then sets the CARRY flag of the STATUS register, based on the value that was shifted out. For example, if the status register initially looks like this:

NVUBDIZC
00000000 

where the letter above each bit denotes what flag that bit represents (In this case, C represents Carry flag), and the ACCUMULATOR register looks like this:

0b11111111 //equivalent to 0xFF

performing ASL in the accumulator addressing mode changes the state of the ACCUMULATOR to

0b11111110

and changes the status register to

NVUBDIZC
00000001 

since the leftmost bit of the accumulator that got shifted out was a 1.

Now we have a struct that represents the hardware state of the CPU. Since we created a getCPU function, we should create a corresponding freeCPU function to make memory management easy. This function deallocates the CPU from memory, essentially destroying it.

void freeCPU(CPU *c){
    free(c->addressSpace);
    free(c);
}

Next, we’ll need a function to print out the CPU hardware state so that we’ll be able to reason out what’s going on with our program.

/* prints state of CPU registers */
void print(CPU *c){
    printf("PC: ");
    printf("%x\n", c->PC);
    printf("SVUBDIZC\n");
    printf("%s\n",getStatus(c));
    printf("STACK REG: ");
    uint8_t stackVal = 0xFF & c->stack;
    printf("%x\n", stackVal);
    printf("ACCUM REG: ");
    uint8_t accumVal = 0xFF & c->accum;
    printf("%x\n", accumVal);
    printf("IND_X REG: ");
    uint8_t xVal = 0xFF & c->x;
    printf("%x\n", xVal);
    printf("IND_Y REG: ");
    uint8_t yVal = 0xFF & c->y;
    printf("%x\n", yVal);
}

The getStatus function, which is omitted here for the sake of keeping things short, is a function that reads the status register and returns a binary string representing its value. We print out the status register in this special way because we care about the value of each individual bit or flag within it.

Great, so where are we now? We have a struct that represents the entire hardware state of our CPU. We can get, free, and print this CPU struct to examine it’s contents. If you’re following along, see if you can initialize and print out the object representing the hardware state of your emulator as the first step.

Manipulating Hardware State

Before we begin implementing opcodes or looking at how we’ll get the emulator to execute instructions, we’ll need to build a few tools that will allow us to easily manipulate the hardware state. This includes:

  • A function to set and clear specific flags in the status register
  • A function to read from and write to memory
  • A function to push to and pull (a.k.a pop) from the stack
  • Functions to read from and write to different registers

Some of these functions are simple one liners and seem redundant when you could directly manipulate the CPU struct itself, but these functions are valuable because they’ll provide encapsulation, cut down on repetition, and can have input validation to protect you from making mistakes. You should really follow this step if you’re working in a dynamically typed language like Python, where it’d be possible to set values to the wrong type at run time.

Setting and getting register values, flags, and memory values should be straightforward so we won’t include the full functions in the write-up here, but there are some gotchas you might come across when you write the push to and pull from stack functions. On the 6502, the stack is the part of memory from 0x100 to 0x1FF. The stack starts at 0x1FF and grows towards 0x100. The CPU keeps track of where the address of the top of the stack is in the eight bit stack register. Wait, hold up! We know that addresses are represented by sixteen bits. How is it possible for us to keep track of a sixteen bit value in an eight bit register? Well, since the top of the stack’s address is always between 0x0100 and 0x01FF, we only need the last eight bits to keep track of the top of the stack! We keep those last eight bits in the stack register, and know to interpret those bits as the ones that we add to 0x0100 in order to get the true stack pointer location. One other caveat: when I say ‘the top of the stack’, I really mean the ‘next memory location that new data will be pushed to’.

The PUSH function should get the stack location, OR it with 0x0100 in order to get the full 16 bit address, write the byte we intend to push to that address location, and finally update the stack location in the stack register, decrementing it by one.

void PUSH(CPU *c, int8_t operand){
    uint8_t stackVal = getStackReg(c);
    uint16_t address = 0x0100 | stackVal;
    write(c, address, operand);
    uint8_t newStackVal = stackVal - 1;
    setStackReg(c, newStackVal);
} 

The PULL function does the opposite of the PUSH function, as you might have guessed. First we fetch the address of the top of the stack, increment it by one to shrink the stack (remember, the stack starts at 0x1FF and grows towards 0x100, so incrementing the stack pointer shrinks it and decrementing the stack pointer expands it!), OR it with 0x0100 in order to get the full address to read from, and then return whatever we read from that address.

int8_t PULL(CPU *c){
    uint8_t stackVal = getStackReg(c);
    uint8_t newStackVal = stackVal + 1;
    setStackReg(c, newStackVal);
    uint16_t address = 0x0100 | newStackVal;
    return read(c, address);
}

In the two functions above there are a few functions we haven’t mentioned- getStackReg, setStackReg, and read. The first function takes a CPU struct and returns the byte value of the stack register in that struct. The second takes a CPU struct and a byte value, and then sets the stack register within the CPU struct. The last function reads from memory using a specified sixteen bit memory address, and the returns the byte read. These are some of the ‘tool’ functions we mentioned earlier. They’re straightforward, so we’ll leave it to you to figure out the implementation details!

Instruction Set Implementation

Here comes the fun part! It’s time to implement the instruction set for our CPU. Walking through the implementation of each instruction would take forever, and you will pick up quickly on what to do once you implement a few instructions. For those reasons, we’ll present this section in FAQ form, with the questions we think you’ll find yourself asking as you go along.

Q: Where do I start?

A: Start by determining what subset of instructions you’ll implement initially! We started by implementing all the instructions present in Pedro Franceschi’s 6502 program that calculates the 7th fibonacci number. It’s a good idea to do this because you’ll have a clear goal and you can test the functionality of your CPU as a whole without implementing all the instructions first. Besides, it’d get pretty boring to just try and implement every single instruction all in one go! Make it fun for yourself and build your understanding by implementing enough of your emulator to run a simple program, and then a more complex one, and so on.

Picked the instructions you plan on implementing? Good! Now head on over to this excellent resource and read about the first instruction you plan on implementing. We found ourselves referring to this guide constantly throughout the process. After understanding what you need to implement, have at it! Write a function for your instruction that will change the CPU hardware state in the way the guide describes. Then write a test for your implementation, where you initialize a hardware state, run an instruction to change that state, and check that the hardware state changed in the way you expected. Writing tests for each instruction is critical for making development easier and faster! We don’t think this kind of project should be done without writing tests.

Q: How should I structure the instruction functions I write?

A: There are many ways to do it! If you’re curious about how we chose to do it, we copied the code structure of fogleman’s implementation of a 6502 emulator in go. Our function declarations for each instruction function look like this:

void instructionName(CPU *c, OP_CODE_INFO *o);

Since C doesn’t easily support object oriented programming, we pass around a reference to our CPU struct to instruction functions and have them directly modify the emulated hardware state. What is the second argument, o? It’s a pointer to a OP_CODE_INFO struct that looks like this:

typedef struct op_code_info {
    //holds information that instructions need to execute
    int8_t operand;
    //address that operand was fetched from
    uint16_t address;
    //enum describing addressing mode (immediate, zero indexed, etc.)
    MODE mode;
} OP_CODE_INFO;

Each instruction function examines the OP_CODE_INFO struct for the information it needs to do its job. An instruction function does not always need all the info in the struct. It often uses only the “operand” field. Let’s take the LDX instruction as an example. It takes a byte value and loads it into the X register. It does not need to know the address that this byte value was fetched from, nor does it do anything addressing mode specific once the operand has been fetched.

void ldx(CPU *c, OP_CODE_INFO *o){
    setSign(c, o->operand);
    setZero(c, o->operand);
    setRegByte(c, IND_X, o->operand);
}

The reason for structuring the code this way is to move code that deals with addressing modes out of instruction implementations. Without using this OP_CODE_INFO struct pattern, we would have to write a bajillion functions that are small variations of the same instruction, like ldxImmediate, ldxZeroPage, and so on. The only reason that the addressing mode is available in the OP_CODE_INFO struct at all (as a MODE enum) is that this separation of concerns breaks down with a few odd instructions like LSR and ASL. These instructions have addressing mode dependent behavior even after the operand has been fetched.

If this doesn’t entirely make sense at this point, don’t worry! Just know that if you follow along with this scheme, you should ignore addressing mode dependent behavior for now and trust that some other part of the program will use the addressing mode to load the correct values into the ‘operand’ and ‘address’ in the OP_CODE_INFO struct. Don’t use the mode information at all except in the ASL, LSR, ROL, or ROR instructions!

Program Execution

We have a struct object that represents the hardware state of our emulator, and instruction implementations that change the hardware state. What do we still need? An assembler to turn assembly code into hex code, and a loader to put that hex code into the memory of our emulated CPU. You can find an assembler online instead of writing your own for now, and there is a section below on writing a loader if you need help on getting started with it. The rest of this section is written with the assumption that you have those two components figured out.

Now we want to emulate execution of a program on the 6502. This means that we’ll need our program to do the following in a loop:

  1. Fetch a hex value from the memory location specified by the program counter
  2. Figure out which addressing mode and instruction are represented by that hex value
  3. Based on the addressing mode, load ‘address’ and ‘operand’ into the OP_CODE_INFO struct
  4. Increment the program counter by the appropriate amount
  5. Call the function that corresponds to the instruction, passing it the OP_CODE_INFO

Fetching the hex value from memory is straightforward, especially if we’ve implemented a function to read from memory.

uint8_t hexInstruction = read(c, c->PC);

We can figure out the instruction and addressing mode represented by this hexInstruction by using lookup tables, which are arrays we index into using the hexInstruction value. The arrays contain the addressing mode or instruction represented by the hex value. The values that should go into the lookup tables can be found in fogleman’s implementation of a 6502 emulator in go, so you can skip the tedious step of filling out the lookup tables by hand.

uint8_t addressingModes[256] = {
    6, 7, 6, 7, 11, 11, 11, 11, 6, 5, 4, 5, 1, 1, 1, 1,
    ….
    //the addressingModes lookup table. Values here are enums that map to modes. 
    //For example, 1 represents modeAbsolute and 5 represents modeImmediate.
};

void (*hexToFunction[256])(CPU *c, OP_CODE_INFO *o) = {
    brk, ora, fut, fut, fut, ora, asl, fut,
    ….
   //lookup table of function pointers. maps from hex val to the
   //function that will execute the instruction represented by the
   //hex val
};

The hexToFunction lookup table has some weird looking syntax and is more difficult to understand. We chose to put function pointers in it that would point to the correct function to call to execute an instruction. This seems strange at first, but it saves us the effort of having to do it the roundabout way. WIthout function pointers, we’d have to return enums from the lookup table, and do a giant case switch statement on the return value that then calls the appropriate functions.

So far so good! Now we want to create a OP_CODE_INFO struct that we will pass to our instruction functions when called. Again, the OP_CODE_INFO struct looks like this:

typedef struct op_code_info {
    //holds information that instructions need to execute
    int8_t operand;
    //address that operand was fetched from
    uint16_t address;
    //enum describing addressing mode (immediate, zero indexed, etc.)
    MODE mode;
} OP_CODE_INFO;

To fill in the operand and address, we set up a switch case structure based on the addressing mode, and put the operand and address into the struct according to the addressing mode descriptions which can be found here. Here’s a short example which includes the Immediate addressing mode.

switch(mode){
        …. 
        case modeImmediate:
            address = c->PC + 1;
            operand = c->addressSpace[address];
            o = getOP_CODE_INFO(operand, address, mode);
            break;
         …
}

After creating the OP_CODE_INFO struct, the PC should be incremented at this point. The reason we choose to do this before executing the instruction is that there are some instructions which change the PC, like jmp. We don’t want to mess this up by changing the PC after an instruction has already changed it! So, it’s easiest to increment the PC at this point in the program. Note that instructions have different sizes so it’s not as straightforward as incrementing the PC by 1. Instead, we’ll need a lookup table that tells us how much we should increment PC, depending on the ‘hexInstruction’ value.

c->PC += instructionSizes[hexInstruction]; 
//instructionSizes is a lookup table

Finally, we call the function to execute the relevant instruction and change the state of our CPU struct, passing it the CPU and OP_CODE_INFO structs. We get this function using the function pointer lookup table mentioned up above.

hexToFunction[hexVal](c,o);
//c is a pointer to the CPU struct and o is a pointer to the
//OP_CODE_INFO struct

And that’s about it! You’ll probably want to develop all of this incrementally, only implementing some of the addressing modes initially and then adding more as you go. You may also have wondered about when 6502 program execution stops and how to have your 6502 program end. Right now it just seems like it would go on forever, or the PC address would increment until it went out of bounds and your emulator crashed. A simple method that works fine for our purposes is to halt execution of our emulator when the PC is equal to or greater than the end of our 6502 program in memory.

In the next two sections we’ll describe writing the assembler and program loader in greater detail, since we glossed over it before.

Writing the Assembler

As we said before, we want to convert 6502 assembly, which is meant to be human-readable, into 1-byte hex values, which is what the CPU actually reads. Our assembler uses text files as both input and output. This means it doesn’t need to be part of or run at the same time as the CPU - ours is actually a separate Python script. Depending on your implementation, you could also make the assembler part of the CPU or pipe its output directly into the memory loader.

Our assembler must be able to:

  • Ignore whitespace and comments: While we generally only have one “thing” (command, label, or variable declaration) per line, 6502 assembly is NOT whitespace-sensitive. It allows for any number of tabs or spacing in one line. Lines can also end with a comment, which always starts with a semicolon. We must be able to find the relevant commands in the whitespace.

  • Remember labels and variables: Instead of having the human programmer memorize memory addresses, just have the computer do it instead! A label is simply a word followed by a colon, typically on its own line. Generally, a label is a jump or branch target, and you can treat them like a function declaration in a higher level language. The syntax for defining a variable is:

      define variableName $memoryAddress ; $memory address is the usual one or two bytes (00 to FFFF)
    
  • Determine the Addressing Mode: While it’s not hard to read the opcode, each opcode can have multiple possible addressing modes, each with a different hex value. The CPU determines which addressing mode to use solely by reading the hex value, so it’s important we get this right. Thankfully, each addressing mode for an opcode has a slightly different address format that we can use to distinguish them.

** The Assembling Process**

Our implementation of the assembler runs through the source assembly code twice. In both loops, we trim whitespace from each line, split it into its different words (example: “define” “variable” “$1234”), and keep track of the byte position, which is how many bytes the line is from the beginning of the program.

In the first loop, we specifically look for variables and labels. We keep a dictionary/hash table of variables and their associated values and a dictionary/hash table of labels and their byte position. We need the byte position so we can determine how far back or forward a jump or branch target is.

In the second loop, we ignore labels and variables, only parsing instructions. Whenever we come across an opcode with a label or variable as its target address, we replace the address with our stored value for variables and the difference between our current location and the stored target location for labels. For each instruction, we determine the addressing mode by looking at the address format, then look up the correct hex value to use for that opcode-addressing mode combination.

The end result is a one-byte hex value for the opcode, followed by one or two bytes for the address. If we have a two-byte value for the address like $1234, we divide it up into one-byte pieces and reverse the order, giving us 34 12. The output hex is what we can load into memory and run on our CPU.

Writing the Loader

The loader takes in a hex dump file, which is basically the binary bits that the CPU will actually run but written in a text file with 2 digit hexadecimal values separated by spaces. For example, here is Fibonacci assembled from 6502 assembly:

a2 01 86 00 38 a0 07 98 e9 03 a8 18 a9 02 85 01 a6 01 65 00 85 01 86 00 88 d0 f5

This is all read as a character array. Each non-whitespace character is converted from the hexadecimal value as a character to the corresponding integer. This is done in the get_hex_from_char function:

int get_hex_from_char(char c) {
    int hex_val;
    if (c-'0' >= 0 && c-'0' <= 9) {
        hex_val = c-'0';
    } else if (c-'a' >= 0 && c-'f' <= 0) {
        hex_val = c-'a' + 10;
    } else {
        printf("Invalid hex symbol\n");
        return 0;
    }
    return hex_val;
}

This function just converts a single character to its integer value assuming that the character is in ASCII. This function only works for lowercase hexadecimal characters, just for simplicity. There is a problem though, the hex dump file uses 2 digit hexadecimal values. That’s addressed using the get_hex_from_chars function:

int get_hex_from_chars(char *c) {
    int hex_val = 0, length;
    int i;
    for (i = 0; c[i] != '\0' && c[i] != ' ' && c[i] != '\n'; i++)
        hex_val = get_hex_from_char(c[i]) + (hex_val << 4);
    return hex_val;
}

This function will take in a character array of hexadecimal values separated by spaces and calculate the value of the first hex string that does not contain a whitespace. Each hex character is read and converted to an integer value. The preceding hex value is then bit shifted right by 4, which is the same as multiplying by 16, and then added to the hex value from the current character, until the loop runs into a whitespace character.

With these two functions, it is possible to simply loop through all the characters in a hex dump file, take the returned integer value of each hex substring and put it into the memory space of the 6502 CPU. This rest of the code occurs in the load_program function of load_prog.c.

First, the hex dump file is opened by taking in the second command-line input string as the file path:

FILE *hex;

if ( (hex = fopen(argv[1], "r")) == NULL) {
    printf("Error: unable to open file %s\n", argv[1]);
    return start;
}

This is something that you can dig into much much further. The emulator described here is

int16_t load_program(int argc, char **argv, int8_t *mem, int16_t start) {
    int16_t pc = start;
    int8_t hex_val;

    unsigned int bufc = 0;
    char scan_buf[6], val;

    while ((val = getc(hex)) != EOF) {
        while (val != '\n' && val != ' ' && val != EOF) {
            scan_buf[bufc++] = val;
            val = getc(hex);
        }
        scan_buf[bufc] = '\0';
        if (bufc && scan_buf[bufc-1] != ':')
            mem[pc++] = (int8_t) get_hex_from_chars(scan_buf);
        bufc = 0;
    }

    fclose(hex);
    return pc;
}

In the nested while loop, the outer loop gets the next character in the file until it hits the end of the file. The inner loop scans for non-whitespace characters and puts the characters in a small character buffer variable called scan_buf. After the inner loop condition fails-because it encountered a whitespace character- a null character is appended to the character buffer. Some hex dump file generators, like this one by skilldrick, actually output the program counter at the beginning of each line. The program counters all end with a colon. Thus, all character buffers ending in a colon are ignored with the if statement. The character buffers that do not end with a colon are then converted to an integer value and put into the memory at the start location specified by the function parameter. load_program also returns the end of the loaded program in memory as the index into the memory, so that when the function is called, the end of the program in memory is also known.

Final Notes

Thanks for thanking the time to read our guide! We hope it had enough helpful pointers to get you started, and that it made 6502 emulation concepts clearer for you. If you enjoyed the project, it’s something that you can dig into much much further. The emulator described here is rudimentary so it doesn’t detect page crossings, emulate timing correctly, and a whole slew of other things. Have fun and keep exploring!

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.