Сказ о том, как я эмулятор Intel 4004 на Python писал (часть 2)
Небольшой дисклеймер: перед прочтением данной статьи ознакомьтесь с первой частью, дабы вникнуть в суть происходящего. Желаю вам приятного прочтения :)
Вступление
Сидел я тут на днях и думал, как можно улучшить мой эмулятор «Intel 4004» и перечитывая комментарии под первой частью, я осознал одну очень простую вещь — моё творение на 4004-ый не очень то и похоже. Абсолютно рандомные опкоды, инструкции, которых в данном процессоре отродясь не было, например, инструкции HLT, AND и OR (HLT так вообще появилась только в Intel 4040).
После некоторых раздумий я принял следующее решение — нужно переписать эмулятор с нуля, с корректными опкодами, инструкциями и так далее;)
Как всё писалось?
Я начал активно смотреть datasheet, стал рассматривать другие проекты по теме 4004-го, особенно мне понравился эмулятор пользователя markablov, написанный на языке JavaScript (именно оттуда впоследствии были взяты необходимые опкоды).
Как и в прошлый раз, я создал класс CPU и начал с реализации памяти (насущные 256 байт), аккумулятора и счётчика команд (память в этот раз я запихал в инициализацию для удобства):
class CPU:
def __init__(self):
# 256 bytes of memory
self.memory = bytearray(256)
# accumulator
self.acc = 0
# program counter
self.pc = 0
В этот раз в эмуляторе используется всего 7 инструкций из 46 возможных (так что полноценным его назвать нельзя, скорее урезанным, в прошлый раз было также).
Список инструкций из datasheet
Вот список используемых инструкций:
Инструкция | Описание инструкции |
NOP | Без операции |
INC | Увеличение индексного регистра |
ISZ | Пропуск индексного регистра, если он равен нулю |
ADD | Добавить индексный регистр к аккумулятору с переносом |
SUB | Вычитание индексного регистра из аккумулятора с заимствованием |
LD | Загрузка индексного регистра в аккумулятор |
XCH | Обмен индексного регистра и аккумулятора |
Их реализация была выполнена путём создания функций:
# 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
Обо всём по порядку:
NOP просто увеличивает значение счётчика команд (pc) на 1, что позволяет перейти к следующей инструкции в программе.
INC увеличивает значение аккумулятора (acc) на 1, ограничивая его значением до 0–255, и затем увеличивает pc на 1.
ISZ увеличивает значение в ячейке памяти с заданным адресом на 1, снова ограничивая его до 0–255. Если значение в ячейке становится равным 0, pc увеличивается на 2, иначе увеличивается на 1.
ADD добавляет значение из ячейки памяти с заданным адресом к значению аккумулятора, ограничивает результат до 0–255 и увеличивает pc на 2.
SUB вычитает значение из ячейки памяти с заданным адресом из значения аккумулятора, ограничивает результат до 0–255 и увеличивает pc на 2.
LD загружает значение из ячейки памяти с заданным адресом в аккумулятор и увеличивает pc на 2.
XCH обменивает значение аккумулятора и значение в ячейке памяти с заданным адресом, увеличивает pc на 2.
Дальше была создана функция run, в которой были прописаны опкоды с выполнением определённой функции:
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
Как составляется программа?
Программу надо составлять прямо в коде, а конкретно в program.py. Вот пример программы, которая отнимает от числа 12 число 5 и прибавляет число 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('')
Результат программы
Заключение
На этот раз я учёл ошибки прошлого эмулятора и в этой работе постарался сделать всё максимально верным.
У меня есть следующие идеи по развитию проекта на будущее:
Добавить ещё больше инструкций 4004-го.
Используя библиотеку tkinter, создать окно, где пользователь вводит программу и ему выводится результат (дабы не приходилось устанавливать сам Python, различные IDE к нему для запуска и теста эмулятора).
Полный код можно посмотреть на моём GitHub.
С вами был Yura_FX. Спасибо, что дочитали данную статью до конца. Не забывайте делиться своим мнением в комментариях :)