Разработка эмулятора процессора i8080

advertisement
Разработка эмулятора процессора 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() и проследить за его выполнением.
Download