Разработка конвейерной версии
Продолжает развиваться конвейерная версия ядра «Эверест». Текущий статус легче всего показать на следующем примере.
Надо сказать что пример надуманный и при старте на реальном железе он должен попасть в бесконечный цикл. В случае симуляции этого кода подразумевается что он выполняется после включения питания или сброса устройства. Если вам интересно почему так произойдёт, смотрите пояснение в статье «Горячий возврат из подпрограммы».
Ядро использует трёхстадийный конвейер со стадиями чтения, декодирования и исполнения инструкций. Конвейер может быть временно остановлен устройством чтения при выполнении некоторых запросов.
На рисунке выше показан пример старта устройства с первого по десятый такт. Показанным сигналам соответствует выполнение следующего фрагмента кода:
Небольшое пояснение сигналов на второй картинке. Сигнал CLK это тактовый сигнал, подающийся на вход ядра. Любые другие сигналы привязаны к тактовому сигналу и обработка этих сигналов синхронизируется только тактовым сигналом.
Входной сигнал RST это сигнал сброса — активный уровень входного сигнала сбрасывает устройство в состояние «холодного старта».
Выходной сигнал на шине REQ определяет состояние выходных шин ядра и тип активного запроса. Например, значение REQ = 0 определяет что ядро ожидает появление инструкции на шине IDA.
Входной сигнал ACK сигнализирует ядру о том, что активный запрос выполнен. Поле Counter считает количество тактов с момента старта симуляции. На четвёртом такте устройство выборки отреагировало на сигнал сброса и выставило на шину IDA инструкции, расположенные по физическом адресу 0xff000000, который отображается на ПЗУ с моделируемым кодом. Число 0x0250002100 это выбранные инструкции. Несложно заметить что они соответствуют первым пяти байтам из дампа на предыдущей картинке и соответствуют следующим инструкциям:
- старшие восемь бит (код 0x02) это инструкция CLC (сброс флага переноса),
- следующие шестнадцать бит (0x5000) это инструкция LOAD8 r0, 0x00 (загрузка восьми битной константы 0x0 в регистр R0),
- младшие шестнадцать бит (0x2100) это инструкция XOR (исключающее ИЛИ регистра R0 c самим собой).
Сигнал сброса активировал устройство выборки и сразу после отмены сброса (такт 5) устройство выборки выдало на шину IDA три инструкции. Устройство выборки можно рассматривать как первую стадию конвейера (pipeline).
Мы не ставили цель выполнить несколько инструкций за такт, поэтому команды выполняются последовательно. Для упрощения аппаратной логики часть функций сброса сделана программно. Сигналы ->carry и carry-> описывают состояние флага переноса на стадии декодирования инструкции и на стадии выполнения соответственно. С учётом конвейерной реализации это означает сигнал ->carry влияет на выполнение операции, а сигнал carry-> является результатом операции. Только лишь некоторые инструкции используют или изменяют флаги.
На пятом такте декодируется инструкция загрузки константы в регистр. Непосредственно запись производится в шестом такте, о чём свидетельствуют сигналы wrem (строб записи), idx_W (номер записываемого регистра), Write_val (записываемое значение). Эта инструкция не меняет состояние флагов.
Для сброса флага нуля выполняется инструкция XOR. Пока предыдущая инструкция обновляет регистры, текущая XOR уже подхватила эта значения и в такте 7 повторно записала 0 в регистр r0 одновременно со сбросом флага нуля. На реальном железе этой инструкции было бы достаточно чтобы сбросить флаг нуля и регистр, но на этапе моделирование любые операции с неизвестными регистрами приводят к неизвестным результатам, поэтому предыдущая инструкция загрузки константы убирает неопределённость при моделировании.
В седьмом такте происходит декодировании инструкции 0xedfeeeeeed выполняющей загрузку (код операции LOAD) 32-х битной константы 0xfeeeeeed в регистр R13. Это сделано не спроста — поскольку в текущей версии регистровый файл упрощён и не поддерживает запрет чтения, то в любой момент на выходах регистрового файла находятся какие-то значения. Некоторые инструкции не используют регистровый файл, поэтому при их выполнении выбирается регистр R13, а записываемое значение позволяет упростить идентификацию таких инструкций в момент моделирования. В общем — это слегка упрощает отладку.
В следующем, восьмом, такте, пока обновляется регистр R13, происходит выборка следующей инструкции — NOTCH (префикс «засечка»). Этот префикс изменяет поведение следующей за ним инструкции, что можно увидеть на следующем такте где показан активный сигнал Notched.
На девятом такте декодируется инструкция перехода JMP16 (инструкция безусловного перехода со 16-ти битным знаковым смещением). На первый взгляд инструкция, которая просто переходит на следующую, совершенно бесполезна. Но активизированная на предыдущем такте засечка превращает инструкцию перехода в инструкцию вызова подпрограммы.
На десятом такте виден результат выполнения префикса и инструкции перехода — вместе с переходом на адрес 0xff00000e совершается запись «адреса возврата» в регистр R15. Таким образом пока никакая инструкция не обновит содержимое регистра R15, выполняемая позже инструкция выхода из подпрограммы (RETURN) совершит переход по адресу, заданному регистром R15. Что происходит на десятом такте показано на следующем рисунке.
Этому фрагменту диаграммы сигналов соответствует следующий код
Код операция 0x202f на десятом такте соответствует инструкции MOV R2, R15 — она совершает копирование из регистра R15 в регистр R2. Непосредственно копирование происходит на следующем такте, что видно по состоянию сигналов на шинах wren, idx_W и Write_val. Так же на одиннадцатом такте выбирается засечка, которая меняет логику работы следующей инструкции.
На двенадцатом такте на шине IDA декодируется инструкция 0x90000f, которая соответствует инструкции LOAD16 r0, 0x000f Она показана на предыдущей картинке, но вы не найдёте её в ассемблерном файле, показанном на первой картинке. В пару инструкций NOTCH и LOAD выродилась макрокоманда LEA R0, $ok_str C помощью этого макроса можно загрузить абсолютный адрес области памяти, содержащей данные. В данном случае константа, присутствующая в коде инструкции LOAD, является знаковым смещением относительно счётчика адреса до области данных, на которую будет указывать регистр после выполнения команд NOTCH и LOAD. Таким образом на тринадцатом такте в регистр R0 записывается адрес 0xff000020, который указывается на данные, помеченные в исходном коде меткой $ok_str.
В тринадцатом такте производится уже известная нам операция 0x5104, которая соответствует ассемблерному коду LOAD R1, 4 Запись непосредственно в регистр происходит на четырнадцатом такте.
В четырнадцатом такте впервые встречается инструкция работы с памятью — 0x6d20, которая соответствует следующей мнемонике — ADDC R2, (R0), которая означает выборку данных из памяти по адресу, заданному значением регистра R0 и сложение выбранных данных с регистром R2. С учётом флага переноса и записью результата обратно в регистр R2. Собственно это момент, когда в связи с необходимостью обратиться к данных возникает необходимость остановить конвейер инструкций. В момент декодирования этой инструкции мы выбираем регистр с помощью idx_B и уже на следующем, пятнадцатом, такте мы используем в качестве адреса.
На пятнадцатом такте изменяется значение шины REQ — оно становится равным 2, что означает что ядро запрашивает данные. Поскольку в отличие от инструкций, мы не можем предсказать адрес, по которому программа будет читать данные, устройству выборке необходимо некоторое время для обращения к данным, поэтому устройство выборки сбрасывает сигнал готовности ACK и производит обращение к данным.
Подтверждение чтения данных происходит на семнадцатом такте, которое определяется активным сигналом ACK и прочитанных из памяти значением на шине IDA. Как видно из диаграммы, в семнадцатом такте происходит сложение прочитанного их регистра R2 числа 0xff00000e с прочитанными данными 0x77777777 с сохранением результата 0x76777785 в регистр R2.
В восемнадцатом такте после завершения предыдущей инструкции сигнал на шине REQ снова переходит в состояние выборки инструкций и устройство выборки возвращает код 0x3603, который соответствует инструкции INC R0, 4 — увеличение значение регистра на 4.
В девятнадцатом такте происходи запись увеличенного на четыре регистра и выбирается следующая инструкция, выполнение которой показано на следующей диаграмме.
Диаграмма описывает циклическое исполнение следующего фрагмента кода
В девятнадцатом такте на шину IDA приходит код 0x3710, который соответствует инструкции DEC R1, вычитающей единицу из регистра R1 с записью результата и установкой флагов на следующем такте.
В двадцатом такте выполняется декодирование инструкция условного перехода. Поскольку на момент декодирования инструкции известно состояние флагов, то в это же время формируется выходной сигнал, формирующий «подсказку» для устройства выборки инструкций.
В двадцать первом такте переход уже совершился и устройство выборки выдаёт очередную команду, полученую в результате перехода. Цикл повторяется до обнуления регистра R1.
Наконец, завершение симуляции и точка останова, поставленная на инструкции RETURN, чтобы проверить корректность завершения цикла.
Объём этой статьи как бы намекает почему так долго не было новостей от проекта. Ближайшей целью проекта является тестирование и запуск кода, проверенного на предыдущей версии ядра, не поддерживающей конвейер. Затем запуск конвейерной версии на плате Марсоход-2.
Оставить комментарий