Разработка эмулятора процессора i8080 Этап 1. Основа Для создания основных классов эмулятора можно использовать шаблоны, содержащиеся в файлах memory.py, cpu.py и instructions.py. В файле cputest.py находятся тесты для проверки основных методов класса CPU, сделанные с использованием библиотеки py.test. Класс Memory Реализуйте класс Memory, представляющий память эмулятора. Пока память это массив байтов, доступный для чтения и записи, адресуемый от 0 до 0xFFFF (= 6553510). Начальное значение всех ячеек памяти равно 0. Для хранения данных можно использовать bytearray. Доступ к значениям через квадратные скобки: m = Memory() m[100] = 123 print(m[200]) # Записать значение 123 по адресу 100 # Распечатать значение из памяти по адресу 200 Класс Register Реализуйте класс Register, представляющий 8-битный регистр процессора. Регистр хранит одно число, начальное значение 0. Доступ к значению регистра должен осуществляться через свойство value. При установке значения отбрасывайте биты, выходящие за пределы одного байта. Класс RegisterPair Реализуйте класс RegisterPair, обеспечивающий доступ к двум 8 битным регистрам как к единому целому. В конструктор класса RegisterPair передаются два регистра (класс Register), старший и младший. Эти регистры следует сохранить в переменных объекта. Доступ к значению регистровой пары (как к одному 16-битному числу) обеспечивается через свойство value. Пример использования классов Register и RegisterPair: r1 = Register() r2 = Register() pair = RegisterPair(r1, r2) r1.value = 0x12 r2.value = 0xFF pair.value = pair.value+1 print(hex(r1.value), hex(r2.value)) # Напечатает 0x13 0x0 Класс Instruction Класс Instruction представляет базовый класс для классов, реализующих инструкции процессора. Инструкция должна уметь: выполняться (менять состояние процессора); сообщать свой размер и число тактов выполнения; уметь преобразовываться в строку с текстовым представлением. В классе Instruction будут объявлены все методы и переменные, присущие всем инструкциям и определены их простейшие реализации. В случае необходимости эти реализации могут быть заменены в дочерних классах на более сложные. В объектах класса Instruction следует хранить следующие переменные: _сpu – процессор (класс CPU); _byte_len - размер в байтах; _mnemonic – имя команды в текстовом виде; _argument – аргумент[ы] или Nonе если аргументов нет; _cycles – время выполнения инструкции в циклах. В этом классе должны быть реализованы следующие методы: конструктор __init__(self, cpu, len, cycles) - запоминает cpu (процессор), len (размер в байтах) и cycles (время выполнения), задаёт значения по умолчанию для остальных переменных; instruction_logic – реализация логики инструкции; этот метод должен быть переопределён в дочерних классах, которые будут выполнять там необходимые действия, у базового класса Instruction этот метод ничего не делает; byte_len() – возвращает размер инструкции в байтах (значение _byte_len); cycle_count() – возвращает время выполнения инструкции (значение _cycles); mnemonic – возвращает текстовое описание инструкции (строку из self._mnemonic и self._argument); execute() – вызывает instruction_logic и печатает в консоль мнемонику выполненной инструкции. Обратите внимание, что при использовании класса Instruction при реализации класса CPU следует вызывать метод cycle_count() только после метода execute(), так как время выполнения инструкции может зависеть от условий, окончательно выясняющихся только в процессе выполнения инструкции. Как используется класс Instruction Для создания инструкций с базовым классом Instruction необходимо сделать следующее: 1. Создайте свой класс, унаследованный от Instruction. Рекомендуется использовать имя вида InstructionXXX, где XXX – мнемоника команды. 2. Определите по справочнику размер инструкции и время её выполнения. 3. Конструктор нового класса должен получать следующие аргументы: cpu – процессор; xx, yyy, zzz – значения битовых полей кода операции. 4. В конструкторе нового класса: вызовите конструктор Instruction, передайте ему cpu, размер инструкции, время выполнения (если оно не равно 4 тактам); задайте значения переменных _mnemonic и _argument (если аргументы есть); если необходимо, запомните значения из битовых полей кода операции или прочитайте значения непосредственных аргументов из памяти, используя cpu.pc. 5. Определите в новом классе метод instruction_logic(). Реализуйте в этом методе логику работы инструкции. 6. Если время выполнения инструкции зависит от каких-то параметров, eто в конструкторе задайте минимальное время выполнения, а в instruction_logic скорректируйте значение self._cycles, если необходимо. Класс InstructionNotImplemented Первый класс инструкции, который следует реализовать – класс для инструкции с неизвестным кодом. Объекты этого класса будут возвращаться в случае, если код является неправильным или инструкция пока не реализована. При попытке выполнения этот класс должен завершить работу эмулятора с выводом сообщения об ошибке. Реализуйте класс InstructionNotImplemented, унаследованый от Instruction. В этом классе должно быть реализовано два метода: конструктор __init__(cpu, xx, yyy, zzzz): 1) вызывает конструктор базового класса, передав ему размер инструкции = 1; 2) восстанавливает из xx yyy zzz код операции и запоминает его; 3) устанавливает значение переменной self._mnemonic равное коду операции; instruction_logic() – выбрасывает исключение, указав в качестве аргумента код операции. Класс CPU Для представления процессора в эмуляторе будут использоваться объекты класса CPU. Объекты класса CPU должны хранить регистры, память и счётчик тактов процессора. Для хранения значения счётчика команд используйте обычную переменную объекта с именем pc. Остальные регистры и регистровые пары реализуйте с помощью объектов классов Register и RegisterPair. Определите в классе CPU (вне методов1) следующие константы: 1 ALL_REGISTERS = "BCDEHLFASP" - имена всех регистров процессора; REGISTER_NAMES = "BCDEHLMA" - имена регистров, используемых в командах с регистровыми аргументами; PAIR_NAMES_1 = ['BC', 'DE', 'HL', 'SP'] - имена регистров, использующихся в командах работы с парами, первый тип; PAIR_NAMES_2 = ['BC', 'DE', 'HL', 'AF'] - имена регистров, использующихся в командах работы с парами, второй тип. Переменные, объявленные непосредственно в классе часто называют статическими переменными класса. Они существуют в единственном экземпляре для всех объектов класса. Доступ к ним можно получить как через объект, так и используя имя класса: cpu = CPU() print(cpu.ALL_REGISTERS) # доступ через объект, print(CPU.ALL_REGISTERS) # доступ через класс. Обратите внимание, что порядок регистров в переменных соответствует индексам регистров, используемых в битовых полях кодов операций. Поэтому, если нужно будет по букве определить индекс регистра, то это можно сделать вызовом CPU.REGISTER_NAMES.index(“A”). И наоборот, имея индекс регистра r можно определить его имя используя CPU.REGISTER_NAMES[r]. Класс CPU также должен хранить счётчик выполненных тактов. Он потребуется для корректной эмуляции работы внешних устройств. Заведите для этого счётчика переменную. В классе CPU на первом этапе следует реализовать следующие методы: 1. Конструктор __init__(self, memory) - создаёт все структуры данных, начальное значение всех регистров должно быть равно 0. Аргументом конструктора является память (объект класса Memory), его следует запомнить в переменной объекта memory. 2. step() – выполняет одну инструкцию: 1. Прочитайте инструкцию из памяти по адресу pc. 2. Декодируйте инструкцию, используя метод decode(opcode). 3. Увеличьте pc на размер инструкции (используйте метод Instruction.byte_len()). 4. Выполните инструкцию, вызвав её метод execute(). 5. Увеличьте счётчик циклов на время выполнения инструкции (метод Instruction.cycle_count()). 3. run() – выполняет step() в бесконечном цикле. 4. split_opcode(opcode) – возвращает тройку (XX, YYY, ZZZ) групп битов опкода 5. decode(opcode) – возвращает инструкцию по коду операции. Этот метод получает на вход код операции (число) и должен вернуть соответствующий объект, унаследованный от класса Instruction. Исходно decode() возвращает InstructionNotImplemented для всех кодов. По мере реализации инструкций в него надо будет добавлять проверку их кодов. Для анализа кодов удобно использовать результаты работы функции split_opcode(). 6. get_pair1(DD) – возвращает значение регистровой пары, заданной кодом DD (первого типа). 7. get_register(rrr) – возвращает значение регистра с индеком rrr. Если индекс равен 0b110 следует вернуть значение из ячейки памяти с адресом, записанном в паре HL. 8. set_register(rrr, value) – записывает значение value в регистр с индеком rrr. Если индекс равен 0b110 следует записать значение в память по адресу, записанному в паре HL. 9. __str__ - возвращает описание процессора в текстовом виде. Желательно вывести значения всех регистров. Создание регистрового файла Используя определённые в классе CPU текстовые константы можно легко создать соответствующие им наборы регистров и регистровых пар. Например, следующий код создаёт список регистров: registers = [Register() for r in CPU.ALL_REGISTERS] Создать список регистровых пар для первого типа индексов можно следующим образом: pairs_1 = [] for p in CPU.PAIR_NAMES_1: r1 = CPU.ALL_REGISTERS.index(p[0]) r2 = CPU.ALL_REGISTERS.index(p[1]) pairs_1.append(r1, r2) Этот код можно выделить в функцию, параметром которой будет список имён пар и использовать для создания обоих списков регистровых пар. Тестирование Обязательно тщательно протестируйте все методы класса CPU. Для реализации тестов следует использовать одну из библиотек модульного тестирования, например py.test. В качестве примера можно использовать тесты из файла cputest.py. Можно протестировать работу эмуляции процессора в целом. Выполните метод step() – это должно привести к исключению, сгенерированному внутри InstructionNotImplemented(). Инструкция NOP Простейшей инструкцией является инструкция NOP – «ничего не делай», имеющая код 0. Для реализации этой инструкции создайте класс InstructionNOP, унаследованный от Instruction. В этом классе необходимо реализовать только конструктор. В нём: 1) вызовите конструктор Instruction, передав ему длину инструкции 1; 2) установите переменную self._mnemonic = NOP. Проверьте работу этой инструкции. Так как по умолчанию вся память заполнена нулями, а ноль это код инструкции NOP, то достаточно просто создать память и процессор, вызвать метод процессора step() и проследить за его выполнением.