Небольшой дисклеймер: перед прочтением данной статьи ознакомьтесь с первой частью, дабы вникнуть в суть происходящего. Желаю вам приятного прочтения :)

Вступление

Сидел я тут на днях и думал, как можно улучшить мой эмулятор "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
Список инструкций из 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('')
Результат программы
Результат программы

Заключение

На этот раз я учёл ошибки прошлого эмулятора и в этой работе постарался сделать всё максимально верным.

У меня есть следующие идеи по развитию проекта на будущее:

  1. Добавить ещё больше инструкций 4004-го.

  2. Используя библиотеку tkinter, создать окно, где пользователь вводит программу и ему выводится результат (дабы не приходилось устанавливать сам Python, различные IDE к нему для запуска и теста эмулятора).

Полный код можно посмотреть на моём GitHub.

С вами был Yura_FX. Спасибо, что дочитали данную статью до конца. Не забывайте делиться своим мнением в комментариях :)

Комментарии (6)


  1. Tzimie
    06.04.2024 12:21
    +1

    Писать интерпретатор команд на интерпретаторе...


    1. adron_s
      06.04.2024 12:21
      +3

      А как еще заставить современный процессор работать с скоростью Intel 4004? :-)

      А вообще автор молодец! Единственный способ выучить новый язык программирования это практическая работа c ним. Можно прочесть кучу книг по языку и не уметь на нем программировать. Только практика реально учит пользоваться функционалом языка. Пока не наступишь на все возможные грабли, не поймешь теорию. Говоря другими словами: "Ответы не будут услышаны, пока не заданы вопросы". Вот практика как раз и формирует эти самые вопросы. Ну а ответы уже можно найти в книгах.

      Да и по Питону я бы рекомендовал прочесть Марка Лутца. Причем в оригинале и на английском. За одно и английский выучите :-D


      1. Yura_FX Автор
        06.04.2024 12:21

        Рад, что вам понравилось :)


  1. mc2
    06.04.2024 12:21

    tkinter, создать окно, где пользователь вводит программу и ему выводится результат (дабы не приходилось устанавливать сам Python, различные IDE к нему для запуска и теста эмулятора).

    Возьмите сразу tcl/tk :)


  1. danilovmy
    06.04.2024 12:21
    +1

    if / 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 архитектурой. Было клево и безоблачно... ээх.


    1. Yura_FX Автор
      06.04.2024 12:21

      Очень рад тому, что вам интересна данная тема. Особенно радует то, что вы пытаетесь как-либо в этой теме помочь, даёте советы по улучшению кода и т.д. :-)