The story of how I wrote an Intel 4004 emulator in Python (part 2)

Small disclaimer: Before reading this article, read the first part in order to understand the essence of what is happening. I wish you a pleasant reading 🙂

Introduction

I was sitting here the other day and thinking about how I could improve my emulator”Intel 4004“and re-reading the comments under the first part, I realized one very simple thing – my creation is not very similar to the 4004th.. Absolutely random opcodes, instructions that were never present in this processor, for example, HLT, AND and OR instructions (HLT only appeared in Intel 4040).

After some thought, I made the following decision – I need to rewrite the emulator from scratch, with the correct opcodes, instructions, and so on 😉

How was everything written?

I began to actively look at the datasheet, began to consider other projects on the topic of 4004, I especially liked Markablov user emulatorwritten in JavaScript (it was from there that the necessary opcodes were subsequently taken).

Like last time, I created a CPU class and started with the implementation of memory (essential 256 bytes), accumulator and program counter (this time I stuffed the memory into initialization for convenience):

class CPU:
    def __init__(self):

        # 256 bytes of memory
        self.memory = bytearray(256)

        # accumulator
        self.acc = 0

        # program counter
        self.pc = 0

This time, the emulator uses only 7 instructions out of 46 possible (so it cannot be called full-fledged, rather cut down, it was the same last time).

List of instructions from datasheet

List of instructions from datasheet

Here is a list of instructions used:

Instructions

Description of instructions

NOP

Without surgery

INC

Increase index register

ISZ

Skip index register if it is zero

ADD

Add index register to accumulator with carry

SUB

Subtracting an index register from an accumulator with borrowing

LD

Loading the index register into the accumulator

XCH

Exchange index register and accumulator

Their implementation was done by creating functions:

    # NOP instruction (No Operation)
    def NOP(self):
        self.pc += 1

    # INC instruction (Increment index register)
    def INC(self):
        self.acc = (self.acc + 1) % 256
        self.pc += 1

    # ISZ instruction (Increment index register skip if zero)
    def ISZ(self, address):
        self.memory[address] = (self.memory[address] + 1) % 256

        if self.memory[address] == 0:
            self.pc += 2
        else:
            self.pc += 1

    # ADD instruction (Add index register to accumulator with carry)
    def ADD(self, address):
        self.acc = (self.acc + self.memory[address]) % 256
        self.pc += 2

    # SUB instruction (Subtract index register to accumulator with borrow)
    def SUB(self, address):
        self.acc = (self.acc - self.memory[address]) % 256
        self.pc += 2

    # LD instruction (Load index register to Accumulator)
    def LD(self, address):
        self.acc = self.memory[address]
        self.pc += 2

    # XCH instruction (Exchange index register and accumulator)
    def XCH(self, address):
        temp = self.acc
        self.acc = self.memory[address]
        self.memory[address] = temp
        self.pc += 2

First things first:

  • NOP simply increments the program counter (pc) by 1, allowing it to advance to the next instruction in the program.

  • INC increases the accumulator value (acc) by 1, limiting it to a value of 0-255, and then increases pc by 1.

  • ISZ increments the value in the memory location at the given address by 1, again limiting it to 0-255. If the value in the cell becomes 0, pc is increased by 2, otherwise it is increased by 1.

  • ADD adds the value from the memory location with the given address to the accumulator value, limits the result to 0-255 and increases pc by 2.

  • SUB subtracts the value from the memory location at the given address from the accumulator value, limits the result to 0-255, and increments pc by 2.

  • LD loads the value from the memory cell with the given address into the accumulator and increments pc by 2.

  • XCH exchanges the value of the accumulator and the value in the memory cell with the given address, increases pc by 2.

Next, the run function was created, in which opcodes were written to perform a specific function:

    def run(self):
        while self.pc < len(self.memory):
            opcode = self.memory[self.pc]

            # NOP instruction opcode
            if opcode == 0x0:
                self.NOP()

            # INC instruction opcode
            elif opcode == 0x6:
                self.INC()

            # ISZ instruction opcode
            elif opcode == 0x7:
                self.ISZ(self.memory[self.pc + 1])

            # ADD instruction opcode
            elif opcode == 0x8:
                self.ADD(self.memory[self.pc + 1])

            # SUB instruction opcode
            elif opcode == 0x9:
                self.SUB(self.memory[self.pc + 1])

            # LD instruction opcode
            elif opcode == 0xA:
                self.LD(self.memory[self.pc + 1])

            # XCH instruction opcode
            elif opcode == 0xB:
                self.XCH(self.memory[self.pc + 1])

            else:
                print('Unknown opcode!!!')
                return

            self.pc += 1

How is the program compiled?

The program must be written directly in code, specifically in program.py. Here is an example of a program that subtracts the number 5 from the number 12 and adds the number 2:

from cpu import CPU

cpu = CPU()

# Write the numbers 12, 5 and 2 to memory at arbitrary addresses (e.g. 0x10, 0x11 and 0x12)
cpu.memory[0x10] = 12
cpu.memory[0x11] = 5
cpu.memory[0x12] = 2

# Execute the commands to subtract the numbers 12 and 5, and then add the number 2 to the resulting number
cpu.LD(0x10)
cpu.SUB(0x11)
cpu.ADD(0x12)
cpu.NOP()

# The result of the program will be stored in the accumulator
print('')
print(f'  Result: {cpu.acc}')
print('')
Program result

Program result

Conclusion

This time I took into account the mistakes of the previous emulator and in this work I tried to make everything as correct as possible.

I have the following ideas for developing the project for the future:

  1. Add even more 4004 instructions.

  2. Using the tkinter library, create a window where the user enters the program and the result is displayed to him (so that he does not have to install Python itself, various IDEs for it to run and test the emulator).

The full code can be viewed on my GitHub.

Yura_FX was with you. Thank you for reading this article to the end. Don't forget to share your opinion in the comments 🙂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *