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).
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('')
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:
Add even more 4004 instructions.
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 🙂