Оглавление
§ Этапы выполнения
Данная реализация процессора предполагает, что вся инструкция, а также ее операнды будут считываться и записываться в память побайтно, что отличает все остальные правильные реализации процессоров от моей, "неправильной" реализации. Такой подход очень сильно упрощает код процессора и его понимание, поэтому такой метод считывания выбран для учебных целей.
Когда процессор считывает новую инструкцию, он сохраняет в определенные временные регистры ее параметры: какие были префиксы, байт опкода, modrm и sib, сами операнды. Другими словами, читая инструкцию, необходимо выделить некоторые внутренние регистры для сохранения параметрической информации.
В собственном процессоре я выделил 8 этапов исполнения инструкции (подпрограмм):
0 Исполнение инструкции: считывание опкода (или префикса), выполнение микрооперации – так как инструкция всегда состоит из серии небольших операции, которые работают с АЛУ, читают или пишут что-то в память, выполняют преобразования данных
1 Считывание операндов и/или эффективного адреса
2 Запись результата в память или регистр
3 Запись в стек
4 Чтение из стека
5 Деление
6 Прерывание
7 Неизвестная инструкция
Этот набор этапов по сути достаточно произвольный, потому что некоторые инструкции могут не использовать запись или чтение из стека через вызов отдельного этапа. Это зависит от необходимости в вызове таких подпрограмм. Самой большой подпрограммой, она же основная стадия исполнения инструкции является 0-й этап "исполнение инструкции".
По умолчанию, процессор в самом начале после сброса переходит именно в стадию исполнения инструкции, и по мере ее исполнения вызываются необходимые подпрограммы вроде считывания операндов и получение эффективного адреса из modrm+sib, запись результата в память или регистры, запись или чтение из стека, и другие подпрограммы.
§ Считывание кода операции
Обозначим регистры, которые будут хранить номер исполняемой процедуры, а также номер "линии" в этой процедуре.
reg [ 2:0] t;
reg [ 3:0] m;
reg [ 7:0] opcache;
Помимо указанных регистров будут и другие регистры статуса, которые я буду добавлять по мере изложения материала.
В регистре t хранится номер текущей исполняемой процедуры на данный момент, которая выбирается при помощи конструкции case в верилоге, а в m – конкретный номер линии из процедуры именно исполнения микрооперации, поскольку для других процедур будут собственные номера линии, для каждого – своя линия, и это сделано по той причине, что процедуры могут вызывать друг друга, но при этом нельзя терять номер исполняемой линии. Если бы мы сделали общую линию для всех, то при вызове одной процедуры из другой предыдущее состояние линии бы затиралось и было бы невозможно корректно вернуться обратно.
В регистре же opcache будет сохранено значение кода операции, что крайне важно, потому что на входе i (входящие данные) всегда будет некоторое новое значение – либо это участок инструкции, либо часть данных, а код операции должен будет сохраняться на каждом этапе декодирования и исполнения одной инструкции.
Для начала назначу константы основным процедурам.
localparam
RUN = 0,
MODRM = 1,
WB = 2,
PUSH = 3,
POP = 4,
DIV = 5,
INTERRUPT = 6,
UNDEF = 7;
Это – номера процедур в плечах альтернативы case:
case (t)
RUN: begin end
MODRM: begin end
...
endcase
Теперь важно вот что понять. Каждый новый такт будет выполнять тот или иной участок кода, а если точнее – с помощью мультиплексирования присваивать регистрам новое значение в соответствии с теми условиями, которые назначены. Например, если t = MODRM, то на следующем такте все что не касается плеча MODRM в выбранном case (t) будет отброшено. За один такт будет "выполнен" код, который находится в begin .. end именно для процедуры MODRM и никак иначе.
А где же та самая одновременность исполнения? А она есть! То что якобы "выполнение" кода происходит из одного плеча не значит, что остальные не будут вычислены. Они будут вычислены: просто они не будут выбраны (результаты исполнения отброшены), так что будет казаться словно на верилоге мы исполняем инструкцию одну за другой. Это не так. Но в то же время, это и так, в каком-то смысле.
Перейдем к тому чтобы начать читать инструкцию. Пока что это сделать невозможно по той причине, что мы не знаем, откуда читать – нет указателя на память. Он вычисляется просто, и, поскольку сейчас мы находимся в режиме реальных адресов, то для получения физического адреса в памяти из линейного адреса, необходимо назначить выходящему пину адреса следующее выражение:
Тем самым способом однозначно определяя указатель на инструкцию. Но только лишь на инструкцию, потому что в будущем необходимо читать не только код, но и данные. Об этом позже.
Сейчас для начала работы кода есть всё необходимое. Дополним код сброса процессора следующим присвоением новым регистрам начального значения:
Это важный этап, поскольку при сбросе процессор должен обязательно переустановить все указатели на процедуру, на начальный адрес в памяти и все внутренние регистры исполнения должны установиться в изначальный статус, чтобы начать читать инструкцию.
Теперь ситуация следующая: адрес установлен и ссылается на начало инструкции, все внутренние регистры установлены в начальное значение. Это значит, что на входе i у нас если не опкод, то хотя бы префикс. Его надо сохранить в регистр opcache и убрать "мусор" от выполнения предыдущей инструкции.
w <= 0;
if (t == RUN && m == 0) begin
opcache <= i;
m <= 1;
ip <= ip + 1;
end
В коде я добавил важную деталь: помимо всего, на каждом такте процессора будет всегда вначале обнуляться (w=0), запись в память. Почему это необходимо? Иногда инструкции будут писать в память, и чтобы не загромождать код лишним выключением записи в память, я его выключаю вначале всегда. Когда необходимо будет включить такт на запись, это будет происходить там, где только это необходимо. Запись в память является гораздо более редкой операцией, чем чтение оттуда.
В коде выше, кроме сохранения текущего опкода в кеш, также производятся некоторые обычные действия:
- Передвигается указатель текущей строки выполнения
m на +1
- Так как мы читаем опкод, то передвигаем также и указатель
ip на один байт вперед
В этом же коде будет сброс много чего, что пока у нас не реализовано, так что сюда будем периодически возвращаться раз за разом чтобы добавить новые присваивания.