Перевод этой страницы:
ru

Подходы к симуляции

Для упрощения представления примем, что реальные логические (цифровые) схемы работают как потоки жидкости.

Жидкость "стекает" от высокого потенциала (depletion-mode транзисторы, питание) к низкому (земля). Вы часто можете видеть в моих стримах и на схемах то, что я обычно называю английским словом "flow". Просто таким образом я провожу анализ схем, проводя аналогию с течением жидкости :-)

Транзисторы представляют собой "вентили", которые управляются путём подачи контрольного сигнала на затвор. При подаче 1 вентиль открывается, при подаче 0 - вентиль закрыт.

Кроме логических 0 и 1 в схеме могут также присутствовать такие значения как "x" и "z".

x - означает что мы не можем сказать какое состояние у выбранного элемента. Например после включения питания - мы не можем сказать какой заряд хранится на затворах у защелок. А вдруг питание было включено, а потом сразу резко выключено и снова включено? Тогда на затворе защелки может остаться слабый заряд, который будет "дотягивать" до логической 1.

z - означает обрыв. Представьте себе транзистор, слева у которого имеется какая-то управляющая схема, а справа он подсоединен к другому устройству, которым он управляет. Так вот если транзистор будет закрыт, то никакого тока в правой цепи не будет, то есть фактически схема будет как-будто разорвана.

Если на затворе транзистора x (то есть "не пойми чего"), то то же самое будет и на выходе. А если на затворе "z" (то есть затвор "оторван" от схемы), то на выходе тоже будет "z" (через такой транзистор ток не проходит).

Статическая симуляция

По условию задачи мы имеем процессор, а также 2 вспомогательных устройства - APU и PPU. При этом тактовый сигнал для этих устройств общий - CLK.

Если мы напишем на Си отдельно симуляцию всей логики 6502, отдельно для APU и PPU, то возникает вопрос : что выполнять в первую очередь ? Хотя 6502 является black-box, однако он соединен с внешним миром шинами.

Простой пример : запись в регистр PPU. Звучит просто, но рассмотрим что происходит при этом :

  • процессор (а точнее ядро процессора 6502 встроенного в APU) начинает выполнение инструкции Store. Инструкция декодируется и в итоге значение регистра подается на шину данных APU, которая соединена с шиной данных PPU. При этом на адресную шину выставляется адрес регистра PPU.
  • Дополнительная логика на материнской плате NES определяет что выставленный адрес соответствует регистрам PPU ($2000 например). Логика включает PPU CS (chip select) (а точнее делает контакт #DBE = 0), также она подает сигнал R/W (WR=1) и выставляет контакты PPU RS0-RS2 (выбор регистра)
  • В это время логика Register Select внутри PPU запускает процесс обновления затребованного регистра. При этом все внутренние схемы PPU мгновенно реагируют на изменение регистра и соответственно изменяется логика работы всего PPU.

Какие могут возникнуть тут проблемы ? Главная проблема - это конечно же propagation delay. Или что было раньше - курица или яйцо. А ведь ситуации могут быть и похлеще, например спрайтовая DMA : когда внутренняя схема APU отключает ядро от шины и подвешивает процессор, при этом в PPU пересылается поток байт, для обновления спрайтовой памяти.

В общем случае проблема выглядит так (упрощенно) :

  • Схема A имеет выходы aaa и входы bbb.
  • Схема B имеет выходы bbb и входы aaa. (то есть схемы выдают друг на друга управляющие сигналы)
  • Что выполняется вначале - схема A или B?

Ведь при изменении aaa - схема B сразу изменится, соответственно сигналы bbb тоже поменяются и повлияют на работу A. Возникает долбаный круговорот.

То есть мы имеем более общий вариант, частным вариантом которого являются "транзисторные гонки" (конфликты шин). Только теперь в качестве устройств у нас не отдельные компоненты схемы, а сразу микросхемы целиком (в качестве конфликтующих девайсов).

Выход из этой ситуации заключается в следующем : девайсы симулируются по-очереди, в зависимости от их приоритета. Например при записи в регистр PPU вначале выполняется ядро 6502, а потом весь PPU. Вообще в APU главным является не ядро, а управляющая логика APU, которая решает что главнее в данный момент - внутренние устройства APU (звуковые каналы) или ядро 6502. Поэтому статический подход к симуляции полутакта CLK заключается в следующем :

  • симулировать все внутренние схемы APU (делитель частоты, frame counter, выбор регистра и выдача его на шину данных, звуковые схемы)
  • симулировать ядро 6502 (выполнить инструкцию, получить или выдать значение на шину данных)
  • симулировать PPU

Но опять же остается проблема : что делать если мы уже симулировали всю схему (например 6502), но последующая симуляцию другой схемы поменяла входные данные. Например : чтение регистра PPU :

  • мы уже выполнили инструкцию Load (при этом получили с шины данных какую-то фигню)
  • настала очередь симуляции PPU, мы выдаем на шину данных верное значение регистра, но ведь симуляция 6502 уже прошла!
  • жопа

Реактивное программирование

Все вы запомнили чувака http://www.youtube.com/watch?v=SyWFvn0I6m8 который выкупал про реактивное программирование :-)

Я залез в вики и почитал немного про парадигмы программирования. Оказывается всё что я до этого делал (статический подход) - называется императивным программированием :

b = 10
a = b;
b = 20;

То есть когда b меняется на 20, a остается таким же, каким после операции a = b;

А теперь взглянем на реактивный подход :

b = 10;
a <=> b;
b = 20;

В этом случае a получает реактивную связь с b, и при "реакции" b = 20, также изменяется и становится равным 20. Я пометил реактивное приравнивание как

А теперь вспомним наш пример с чтением регистра PPU и потоками жидкости. В момент чтения регистра на шине данных ничего не было (z), поэтому 6502 соединил свой внутренний регистр с "ничем". Однако после того как PPU выдал на шину данных значение регистра PPU - схемы 6502 мгновенно отреагировали и загрузили этот регистр. То есть схемы цифровой логики работают по принципам реактивного программирования. В данном случае наш пример в реактивной связке будет выглядеть так (пример инструкции LDA $2000) :

A <=> DATA_BUS;
DATA_BUS = PPUREG[0];

Таким образом проблема "что выполнить раньше" снимается, причём глобально, вплоть до уровня симуляции отдельных транзисторов.

Стабилизация схемы

С помощью реактивного подхода можно поделить схему на компоненты, при этом обозначив их входы, выходы и двунаправленные соединения (шины).

После изменения тактового сигнала - схема запускается, в зависимости от входных сигналов изменяет своё внутреннее состояние и выдаёт наружу выходы. Далее эти выходы используются как входы для другой схемы по цепочке.

А теперь рассмотрим пример когда исходная система передает своё состояние на выход другой, а выход другой системы подается на вход первой. При этом обе системы формируют выходы на основе своих внутренних состояний. В качестве примера будет выступать обыкновенный регистр :

reg.jpg

Логика работы следующая :

  • Во время CLK = 0 вход регистра отсоединен и входная защелка рефрешится старым значением
  • Во время CLK = 1 выход отрезается (транзистором /CLK), чтобы не конфликтовать, а новое значение подается на вход in, но только в том случае, если открыт транзистор ENABLE (разрешить запись)
  • Выход будет инвертирован относительно входа (так как входная защелка организована в виде инвертора с плавающим затвором)
  • Если CLK = 1, а ENABLE = 0, то на выход подается остаточный заряд с затвора защёлки (то есть старое значение)

Псевдокод в этом случае будет такой :

block reg :
    inputs : regnew, enable
    outputs : #out
    inouts : -
    regs : regvalue
    at (CLK == 1) {
         if (enable == 1) regvalue = regnew;
         #out = not (regvalue);
    }
    at (CLK == 0) {
         #out = not (regvalue);
         regvalue = not (#out);
    }
end
 
block some_circuit :
    outputs : regnew, enable
    bla bla bla do something.
end

В этом случае все входы и выходы всех компонентов схемы являются реактивными связями (почему это не пишут в обучалках по Verilog - я хз, ведь именно с этого надо вести речь). Как будет синтезироваться netlist схемы нам не интересно (это вообще отдельная тема), но вот как теперь будет проходить симуляция : вполне понятно!

При симуляции происходит следующее :

  • Выбираются все блоки at. Эти блоки являются реактивными по отношению к какому-либо сигналу. В данном случае реактивным сигналом будет являться сигнал CLK. Ядро симулятора постоянно следит за CLK, и как только он изменяет своё значение (с 0 на 1, или наоборот) - запускается соответствующий блок at. Блоки которые не имеют реактивной связи нас не интересуют (они как будто "заморожены").
  • Последовательность симуляции выбранных at-блоков не имеет значения, поскольку все входы и выходы также реактивно связаны
  • В нашем примере мы имеем непосредственно наш регистр (reg) и какую-то схему, которая выдает новое значение regnew и сигнал enable.
  • Порядок расположения схем выбран не случайно (вначале reg, потом some_circuit), чтобы показать особенность реактивного "выполнения" блоков.

Начинаем исполнение :

  • Первым на очереди стоит блок reg, его входы regnew и enable пока не определены (то есть равны "x")
  • В этом случае выход блока #out будет просто инверсией текущего значения регистра not(regvalue).
  • Это был первый прогон блока reg
  • Теперь мы выполняем блок some_circuit и выдаем наружу сигналы regnew и enable. Ядро симулятора видит что эти сигналы реактивно связаны с входами блока reg, и (это ключевой момент) они изменились. А раз они изменились надо заново прогнать схему reg, чтобы "стабилизировать" её.
  • Блок reg вызывается во второй раз, но уже на этот раз входы regnew и enable имеют определенные значения.
  • Схема стабилизировалась, значит все блоки выполнились и мы ждём изменения сигнала CLK, чтобы начать всё заново, но логика работы блоков уже поменяется (так как будут загружены блоки at для CLK = 1).

А что будет если схема не сможет стабилизироваться ? Что если regnew или enable будет всегда меняться и нам придётся выполнять блок reg снова и снова? Да ничего особенного :-) Такая ситуация называется "race condition" и обычно не возникает. В частности в процессоре 6502 для стабилизации всех схем достаточно не более 10 итераций. Для того, чтобы наша схема не начала бесконечный цикл мы просто ставим таймаут на количество итераций и если их количество стало ну скажем - больше 100, то просто выводим ошибку "Race condition!" и до свидания :-)

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

Verilog

Из имеющихся открытых реализаций Verilog самый адекватный - это Icarus Verilog (http://iverilog.icarus.com). Работает он как консольное приложение и содержит в своём составе компилятор Verilog и симулятор.

Такой вариант нам не подходит. Всё таки у нас speed-application, причём интерактивное. То есть помимо собственно симулятора у нас есть псевдо-устройства (джойстики, телевизор, колонки), которые соединяются к контактам симулируемых микросхем. Джойстики соединены с IO-портами APU, а звук и видео мы генерируем на основании контактов APU SND1/SND2 и PPU VOUT (пока мы не хрена не генерируем).

То есть нам нужно сделать так, чтобы Verilog стал скриптовым языком, а псевдо-устройства стали встроенными функциями по типу $display. При этом не каким-то тормозным интерпретатором нетлиста, а самым настоящим рекомпилятором нетлиста в X86-код, чтобы максимально эффективно "выполнять" симулируемые блоки.

Такая реализация обеспечит нам серьезный задел на будущее :

  • Симулировать можно любые системы и процессоры, просто перегнав их в Verilog
  • Система встраиваемых функций позволяет реализовать самые любые псевдо-устройства
  • Реализацию на Verilog можно будет воплотить в железном варианте
  • HardWareMan будет тоже очень рад, потому что он получит готовую реализацию PPU на Verilog :-)

Задача ясна и понятна :

  • Написать лексический анализатор Verlog-синтаксиса
  • Написать парсер синтаксиса в синтаксическое дерево
  • Оптимизировать и преобразовать синтаксическое дерево в netlist (надо изучить какие бывают популярные форматы для netlist-ов)
  • Написать ядро симулятора netlist, которое статически рекомпилирует его в X86-код и выполняет (тут надо продумать эффективную реактивную модель)
  • Написать псевдо-устройства для NES (джойпад и телек)

http://irs.nntu.ru/globals/files/bukvarev/verilog.pdf

Описание внутреннего устройства виртуальной машины - BreaksVM

sim.txt · Последние изменения: 2015/09/03 23:17 — org
 
За исключением случаев, когда указано иное, содержимое этой вики предоставляется на условиях следующей лицензии: Public Domain
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki