Небольшой дисклеймер: перед прочтением данной статьи ознакомьтесь с первой частью, дабы вникнуть в суть происходящего. Желаю вам приятного прочтения :)
Вступление
Сидел я тут на днях и думал, как можно улучшить мой эмулятор "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 возможных (так что полноценным его назвать нельзя, скорее урезанным, в прошлый раз было также).
Вот список используемых инструкций:
Инструкция |
Описание инструкции |
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. Спасибо, что дочитали данную статью до конца. Не забывайте делиться своим мнением в комментариях :)
Комментарии (6)
mc2
06.04.2024 12:21tkinter, создать окно, где пользователь вводит программу и ему выводится результат (дабы не приходилось устанавливать сам Python, различные IDE к нему для запуска и теста эмулятора).
Возьмите сразу tcl/tk :)
danilovmy
06.04.2024 12:21+1if / elif
стильно/модно/моледежно заменять на словарь:cmd_dict = { 0x0: CPU.NOP, 0x6: CPU.INC, ... } ... # Где-то в коде run() cmd = cmd_dict.get(opcode) if not cmd: raise UncnownOpCode() # или print + return в зависимости от реализации. address = memory[self.pc + 1] cmd(self, address) ...
для
CPU.NOP
надо, правда, поменять сигнатуру что бы принималaddress
или завернуть в lambda безадресные команды:cmd_dict = { 0x0: lambda *args, **kwargs: CPU.NOP, 0x6: CPU.SUB, ... }
А так спасибо @Yura_FX вспомнил детство за 8051 архитектурой. Было клево и безоблачно... ээх.
Yura_FX Автор
06.04.2024 12:21Очень рад тому, что вам интересна данная тема. Особенно радует то, что вы пытаетесь как-либо в этой теме помочь, даёте советы по улучшению кода и т.д. :-)
Tzimie
Писать интерпретатор команд на интерпретаторе...
adron_s
А как еще заставить современный процессор работать с скоростью Intel 4004? :-)
А вообще автор молодец! Единственный способ выучить новый язык программирования это практическая работа c ним. Можно прочесть кучу книг по языку и не уметь на нем программировать. Только практика реально учит пользоваться функционалом языка. Пока не наступишь на все возможные грабли, не поймешь теорию. Говоря другими словами: "Ответы не будут услышаны, пока не заданы вопросы". Вот практика как раз и формирует эти самые вопросы. Ну а ответы уже можно найти в книгах.
Да и по Питону я бы рекомендовал прочесть Марка Лутца. Причем в оригинале и на английском. За одно и английский выучите :-D
Yura_FX Автор
Рад, что вам понравилось :)