RAD для софт-процессоров и немного «сферических коней в вакууме»
Разработка или выбор управляющего контроллера для встраиваемой системы на ПЛИС –актуальная и не всегда тривиальная задача. Часто выбор падает в пользу широкораспространенных IP-ядер, обладающих развитой программно-аппаратной структурой — поддержка высокопроизводительных шин, периферийный устройств, прикладное программное обеспечение и, в ряде случаев, операционных систем (в основном Linux, Free-RTOS). Одними из причин данного выбора являются желание обеспечить достаточную производительность и иметь под рукой готовый инструментарий для разработки программного обеспечения.
В том случае, если применяемая в проекте ПЛИС не содержит аппаратных процессорных ядер, реализация полноценного процессорного ядра может быть избыточной, или вести к усложнению программного его обеспечения, а следовательно приведет к увеличению затрат на его разработку. Кроме того, универсальное софт-ядро будет, так или иначе, занимать дефицитные ресурсы программируемой логики. Специализированный софт-процессор будет более оптимальным решением в свете экономии ресурсов логики — за счет адаптированной системы команд, небольшого количества регистров, разрядности данных (вплоть до некратной 8 битам). Согласование с периферийными устройствами — проблема в основном согласования шин и протоколов. Заменой сложной системы обработки прерываний может служить многопоточная архитектура процессора.
Стековые софт-процессоры и контекст потока
Обычно многопоточные процессоры имеют одно АЛУ и несколько наборов регистров (иногда называемых «теневыми» регистрами) для хранения контекста потока, следовательно, чем больше требуется потоков, тем будут больше накладные расходы логики и памяти. Среди разнообразия архитектур софт-процессорных ядер следует выделить стековую архитектуру. Такие процессоры часто называют еще Форт-процессорами, так как чаще всего их ассемблер естественным образом поддерживает подмножество команд языка Форт.
У стековых процессоров есть интересное свойство –это небольшой размер контекста потока. Поскольку роль регистров выполняет стек, при переключении на другой поток необязательно иметь полный комплект регистров общего назначения — достаточно переключить указатель стека [1]. В простейшем случае контекст потока можно ограничить небольшим набором указателей — стека, стека возвратов и счетчик команд потока. При наличии нескольких определяемых спецификой задач потоков вычислений компактность многопотоковой схемы будет важнее пиковой производительности, а задача реализации процессорной системы в ПЛИС минимального объема является актуальной в свете, например, нового семейства Spartan-7 число ячеек в устройствах которого невелико.
Идеи и методика реализации многопоточного софт-процессора изложены в работе [1]. Развитие работ в этом направлении привело к появлению свободного компилятора языка Python для процессоров стековой архитектуры [2]. Это решает проблему разработки системного программного обеспечения для софт-процессора. Более того, данный язык благодаря простому синтаксису и поддержке различных парадигм программирования является популярным и удобен для начинающих программистов.
Инструментальные средства для упрощенного проектирования IP-ядер
Известен также инструментарий MyHDL [3], позволяющий описывать аппаратные модули и узлы на языке Python. Синтаксис описания проще VHDL, сам инструментарий менее требователен к ресурсам системы, чем фирменные среды разработки. Помимо самого описания MyHDL позволяет описывать тестовые последовательности и логически симулировать работу модулей [4].
Потенциально, совместное применение инструментариев MyHDL и Uzh позволит вести разработку софт-процессора и компилятора языка высокого уровня для него на одном языке, что позволит снизить уровень сложности задачи. В частности это будет полезно в проектах, связанных с начальным обучением программированию, разработки цифровой техники, систем управления. На данный момент предлагаемые подходы к начальному обучению работе с программируемой логикой находятся на стадии поиска эффективных решений и не всегда методически выдержаны [5–7]. Единый стиль описания может помочь сгладить сложные момент в процессе освоения ПЛИС, к тому же некоторые популярные конструкторы и наборы с программируемыми блоками используют Python для задания рабочей программы.
Комбинационные логические схемы и последовательные автоматы описываются и симулируются достаточно не сложно [8,9], откуда можно сделать вывод, что, придерживаясь определенной концепции описать прототип многопоточного софт-процессора на поведенческом уровне достаточно просто.
Реализация простого многопоточного процессора на MyHDL
Учитывая идеи и базовую архитектуру процессора, предложенные в [1], разрабатываемый процессор будет иметь следующие особенности: Гарвардская архитектура с раздельными памятями программ и данных, стеки физически отображаются на память данных, для хранения контекста потока предусмотрены наборы теневых регистров.
Однотактовые и конвейерные пути решения для начальной реализации не очевидны, и с учетом того, что для софт-процессора не требуется сверхвысокой производительности, командный цикл процессора можно сделать многотактным. В первом приближении достаточно четырех стадий выполнения: чтение контекста потока, выборка операндов в кеширующие регистры, выполнение команды, переключение на следующий поток.
Разрядность памяти программ, данных, размеры стеков и количество потоков задается параметрически при помощи ряда переменных:
bits=16
Nthread=8
RAMsize=256
ROMsize=2048
STACKsize=8
RS_BASE=64
DS_BASE=8
Вход процессора — тактовый сигнал и сигнал сброса, плюс шины данных и адреса для внешних устройств (дополнительные сигналы — опционально). В ядро процессора будут входить переключатель состояний процессора, счетчик текущего потока, наборы указателей стеков и счетчика команд для каждого из потоков, регистры текущего контекста, память данных и программ.
Сама микроархитектура ядра реализуется через ряд функций, отвечающих за генерацию последовательных схем. Счетчик состояний процессора реализуется просто — установка в ноль при сигнале сброса, иначе — инкремент на каждый такт.
Логика работы узлов процессора на каждом из этапов выполнения описывается в отдельной функции:
def gor(reset, clk, dat, prt):
state=Signal(modbv(0, min=0, max=4))
thread=Signal(modbv(0, min=0, max=Nthread))
th_sp = [Signal(intbv(0)[bits:]) for i in range(Nthread)]
th_rp = [Signal(intbv(0)[bits:]) for i in range(Nthread)]
th_ip = [Signal(intbv(0)[bits:]) for i in range(Nthread)]
sp, rp, ipreg, rt = [Signal(intbv(0)[bits:]) for i in range(4)]
cmd = Signal(intbv(0)[9:])
tos, sos, tdata = [Signal(intbv(0)[bits:]) for i in range(3)]
D_RAM = [Signal(intbv(0)[bits:]) for i in range(RAMsize)]
C_ROM = [Signal(intbv(0)[9:]) for i in range(ROMsize)]
Первый этап — переключение контекста — считываются нужные рабочие регистры из наборов для текущего потока:
@always_comb def st_sw():
if reset==1:
if state==task_sw:
sp.next=th_sp[thread]
rp.next=th_rp[thread]
ipreg.next=th_ip[thread]
Чтение операндов — считываются из памяти верхние элементы стеков, текущая команда потока и текущий внешний вход:
@always(clk.posedge) def st_get():
if reset==1:
if state==get_data:
tos.next=D_RAM[sp]
sos.next=D_RAM[sp+1]
rt.next=D_RAM[rp]
cmd.next=C_ROM[ipreg]
tdata.next=dat
Третий этап — выполнение команд.
@always(clk.posedge) def st_ex():
if reset==1:
if state==execute:
# unary
if cmd == nop:
D_RAM[sp].next= tos
th_ip[thread].next=ipreg+1
elif cmd == noti:
D_RAM[sp].next= ~tos
th_ip[thread].next=ipreg+1
#alu
elif cmd == add:
D_RAM[sp+1].next=tos+sos
th_sp[thread].next=sp+1
th_ip[thread].next=ipreg+1
elif cmd == andi:
D_RAM[sp+1].next=tos&sos
th_sp[thread].next=sp+1
th_ip[thread].next=ipreg+1
и т.д.
Состав команд может быть оптимизирован для каждой конкретной задачи, единственное требование — желательна поддержка команд абстрактной стековой машины [1] для построения более эффективного кода при компиляции с высокоуровнего языка. Некоторые идеи по реализации работы с константами взяты из работы [10].
Финальный этап — инкремент счетчика потоков:
@always(clk.posedge)
def st_sv(): # task_sw = 3
if reset==1:
# get_data = 1
if state== save_task:
thread.next=thread+1
else:
thread.next = 0
Все описанные блоки возвращаются при завершении описании основной функции процессорного ядра:
return st_swt, st_sw, st_get, st_ex, st_sv
Полученное ядро можно логически протестировать — определяется тестовая функция, генеруется тактовая последовательность, подается сбросовый сигнал:
def test (): reset = Signal (bool (0)) clk = Signal (bool (0)) #C_ROM = (noti, dup, drop, nop, nop, nop, nop, nop, nop) #C_ROM = (lit0, 5×0x100, lit0, 2×0x100, add, nop, nop, nop, nop, nop, nop) dat, prt = [Signal (intbv (0)[bits:]) for i in range (2)] test = gor (reset, clk, dat, prt)
@always(delay(10))
def gen():
clk.next = not clk
@always(delay(50))
def go():
reset.next = 1
return test, gen, go
def simulate(timesteps):
tb = traceSignals(test)
sim = Simulation(tb)
sim.run(timesteps)
Результат симуляции автоматически выгружается в файл .vcd (в данном случае — test.vcd), который потом можно будет открыть в любом просмотрщике временных диаграмм.
На рисунке представлены временные диаграмы работы сгенерированного восьмипоточного 16-битного процессора. Все потоки начинают работу с одного адреса, но их стеки данных и возвратов находятся по разным адресам. Можно отследить изменения счетчиков команд потоков, загрузку констант, манипуляции с данными на стеках.
Рис. Результат симуляции работы софт-процессора.
При необходимости MyHDL код может быть транслирован в код налюбом из HDL языков — в Verilog или в VHDL. Полученные таким образом файлы в дальнейшем могут быть использованы в проектах сред разработки под выбранное семейство ПЛИС:
def convert():
reset = Signal(bool(0))
clk = Signal(bool(0))
dat, prt = [Signal(intbv(0)[bits:]) for i in range(2)]
toVHDL(gor, reset, clk, dat, prt)
Заключение
Был продемонстрирован простой маршрут быстрого прототипирования софт-процессора, позволяющий при незначительных затратах времени и вычислительных ресурсов проверить концептуальную идею на работоспособность. Полученные результаты — транслированные HDL-файлы могут служить основой для дальнейшего развития и оптимизации проекта уже с учетом особенностей текущей серии ПЛИС. В частности, для представленного примера финальный VHDL код будет генерировать память на дефицитных для ПЛИС регистрах, а более предпостительным является задействование ресурсов встроенных блоков памяти.
Библиографический список
1. Советов П.Н., Тарасов И.Е. Разработка многопоточного софт-процессора со стековой архитектурой на основе совместной оптимизации программной модели и системной архитектуры. Многоядерные процессоры, параллельное программирование, плис, системы обработки сигналов, вып.7. 2017, стр. 8–19.
2. GitHub — true-grue_uzh_ Uzh compiler // https://github.com/true-grue/uzh.
3. MyHDL // http://www.myhdl.org.
4. Начинаем FPGA на Python _ Хабр // https://m.habr.com/ru/post/439638/
5. Юрий Панчул. Следущие шаги в черной магии процессоростроения после того, как вы освоили Харрис & Харрис // https://panchul.livejournal.com/578909.html .
6. Жельнио Станислав. Школа по основам цифровой схемотехники_ Новосибирск — Ок, Красноярск — приготовиться _ Хабр // https://habr.com/ru/users/sparf/posts/ .
7. Жельнио Станислав. Процессорное ядро SchoolMIPS и его использование для обучения основам микроархитектуры процессора // http://www.silicon-russia.com/public_materials/2017_10_08_msu_rountable/20171007_Zhelnio_SchoolMIPS%20for%20Education.pdf
8. MyHDL examples // http://www.myhdl.org/docs/examples/
9. Felton Christopher. Developing FPGA-DSP IP with Python // https://www.fpgarelated.com/showarticle/7.php
10. Forth-процессор на VHDL // https://m.habr.com/ru/post/149686/