Разработка или выбор управляющего контроллера для встраиваемой системы на ПЛИС –актуальная и не всегда тривиальная задача. Часто выбор падает в пользу широкораспространенных 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).
