Разработка или выбор управляющего контроллера для встраиваемой системы на ПЛИС –актуальная и не всегда тривиальная задача. Часто выбор падает в пользу широкораспространенных IP-ядер, обладающих развитой программно-аппаратной структурой – поддержка высокопроизводительных шин, периферийный устройств, прикладное программное обеспечение и, в ряде случаев, операционных систем (в основном Linux, Free-RTOS). Одними из причин данного выбора являются желание обеспечить достаточную производительность и иметь под рукой готовый инструментарий для разработки программного обеспечения.
В том случае, если применяемая в проекте ПЛИС не содержит аппаратных процессорных ядер, реализация полноценного процессорного ядра может быть избыточной, или вести к усложнению программного его обеспечения, а следовательно приведет к увеличению затрат на его разработку. Кроме того, универсальное софт-ядро будет, так или иначе, занимать дефицитные ресурсы программируемой логики. Специализированный софт-процессор будет более оптимальным решением в свете экономии ресурсов логики – за счет адаптированной системы команд, небольшого количества регистров, разрядности данных (вплоть до некратной 8битам). Согласование с периферийными устройствами – проблема в основном согласования шин и протоколов. Заменой сложной системы обработки прерываний может служить многопоточная архитектура процессора.
У стековых процессоров есть интересное свойство –это небольшой размер контекста потока. Поскольку роль регистров выполняет стек, при переключении на другой поток необязательно иметь полный комплект регистров общего назначения - достаточно переключить указатель стека [1]. В простейшем случае контекст потока можно ограничить небольшим набором указателей - стека, стека возвратов и счетчик команд потока. При наличии нескольких определяемых спецификой задач потоков вычислений компактность многопотоковой схемы будет важнее пиковой производительности, а задача реализации процессорной системы в ПЛИС минимального объема является актуальной в свете, например, нового семейства Spartan-7 число ячеек в устройствах которого невелико.
Идеи и методика реализации многопоточного софт-процессора изложены в работе [1]. Развитие работ в этом направлении привело к появлению свободного компилятора языка Python для процессоров стековой архитектуры [2]. Это решает проблему разработки системного программного обеспечения для софт-процессора. Более того, данный язык благодаря простому синтаксису и поддержке различных парадигм программирования является популярным и удобен для начинающих программистов.
Потенциально, совместное применение инструментариев MyHDL и Uzh позволит вести разработку софт-процессора и компилятора языка высокого уровня для него на одном языке, что позволит снизить уровень сложности задачи. В частности это будет полезно в проектах, связанных с начальным обучением программированию, разработки цифровой техники, систем управления. На данный момент предлагаемые подходы к начальному обучению работе с программируемой логикой находятся на стадии поиска эффективных решений и не всегда методически выдержаны [5-7]. Единый стиль описания может помочь сгладить сложные момент в процессе освоения ПЛИС, к тому же некоторые популярные конструкторы и наборы с программируемыми блоками используют Python для задания рабочей программы.
Комбинационные логические схемы и последовательные автоматы описываются и симулируются достаточно не сложно [8,9], откуда можно сделать вывод, что, придерживаясь определенной концепции описать прототип многопоточного софт-процессора на поведенческом уровне достаточно просто.
Однотактовые и конвейерные пути решения для начальной реализации не очевидны, и с учетом того, что для софт-процессора не требуется сверхвысокой производительности, командный цикл процессора можно сделать многотактным. В первом приближении достаточно четырех стадий выполнения: чтение контекста потока, выборка операндов в кеширующие регистры, выполнение команды, переключение на следующий поток.
Разрядность памяти программ, данных, размеры стеков и количество потоков задается параметрически при помощи ряда переменных:
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)
Библиографический список
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/
В том случае, если применяемая в проекте ПЛИС не содержит аппаратных процессорных ядер, реализация полноценного процессорного ядра может быть избыточной, или вести к усложнению программного его обеспечения, а следовательно приведет к увеличению затрат на его разработку. Кроме того, универсальное софт-ядро будет, так или иначе, занимать дефицитные ресурсы программируемой логики. Специализированный софт-процессор будет более оптимальным решением в свете экономии ресурсов логики – за счет адаптированной системы команд, небольшого количества регистров, разрядности данных (вплоть до некратной 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/
RAD для софт-процессоров и немного «сферических коней в вакууме»
Разработка или выбор управляющего контроллера для встраиваемой системы на ПЛИС –актуальная и не всегда тривиальная задача. Часто выбор падает в пользу широкораспространенных IP-ядер, обладающих...
habr.com