Разработка или выбор управляющего контроллера для встраиваемой системы на ПЛИС –актуальная и не всегда тривиальная задача. Часто выбор падает в пользу широкораспространенных 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 код будет генерировать память на дефицитных для ПЛИС регистрах, а более предпочтительным является задействование ресурсов встроенных блоков памяти.
Библиографический список
Советов П.Н., Тарасов И.Е. Разработка многопоточного софт-процессора со стековой архитектурой на основе совместной оптимизации программной модели и системной архитектуры. Многоядерные процессоры, параллельное программирование, плис, системы обработки сигналов, вып.7. 2017, стр.8-19.
Начинаем FPGA на Python (статья на Habr).
Жельнио Станислав. Школа по основам цифровой схемотехники_ Новосибирск — Ок, Красноярск — приготовиться (статья на Habr).
Forth-процессор на VHDL (статья на Habr).