воодушевившись примером "горыныча", конечно же захотелось заиметь своего многоглавого змея...
для определенности под "горынычем" буду иметь ввиду многопоточный процессор стековой архитектуры - как верно тут отмечали - "несколько голов на одном теле" (потоков на общем пространстве кода/данных).
В ходе работ по связке процессора whiteTiger с питоновским компилятором Uzh мимоходом попалась надстройка/модуль MyHDL, позволяющая описывать аппаратные модули/узлы на Python (3. Начинаем FPGA на Python _ Хабр //
https://m.habr.com/ru/post/439638/ ). Для профи такое скорее всего и ни к чему, но для нубов вроде меня - почему б и не попробовать
Синтаксис немного проще VHDL, да и можно логически симулировать не привлекая к этому тяжеловесные системы типа Vivado, Quartus+ModelSim.
Простенькие логические схемки и последовательные автоматы вроде почти несложно описывать/симулировать/транслировать в HDL (доводилось до трансляции в VHDL и скармливанию Quartus-у с последующей заливкой в ПЛИС).
Ну и попутно зрела мысль описать "горыныча" на Python....Змей на змеином языке....да еще с прицелом на змеиный компилятор для него...ну прям - ну красота же! все в едином стиле))) (да, пробивается у меня иногда юношеский максимализм из серии - Форт должен быть написан на Форте ( и исполняться на форт-процессоре, как вы уже догадались
))
В итоге немного набив руки на костылях в myhdl (хз, как в ней правильно описывать иерархические решения, ибо, не все что компилируется без ошибок потом можно заставить симулироваться или транслироваться в HDL - а поэтому - наиболее устойчивые решения - "одноуровневые" решения, т.е. все в одном длинном проекте
) перешел к описанию своего "змееныша" семейства "горынычей"
что удобно - параметризация - как говорится - изи:
Код:
bits=16
Nthread=8
RAMsize=256
ROMsize=2048
STACKsize=8
RS_BASE=64
DS_BASE=8
не придумав ничего умнее - мой Горыныч будет о четырех ногах, а именно каждый поток будет состоять из четырех стадий выполнения (как в известном мультике - "пока твой конь четырьмя ногами - раааз-дваа-трии-четыыре, мальчишка ногами - раз-два-раз-два")...ну так вот - мой Горыныч - именно как тот конь)
Код:
#thread states
task_sw = 0 #переключение контекста
get_data = 1 #чтение операндов
execute = 2 #выполнение
save_task = 3 #переключение на следующий поток/нить
Вход процессора - тактовый сигнал и сигнал сброса, плюс шины данных и адреса для внешних устройств (доп.сигналы - потом при желании и необходимости, сейчас всё просто попробовать и проверить).
Внутри - переключатель состояний процессора, номер текущего потока, наборы указателей стеков и счетчика команд для каждого из потоков, регистры текущего контекста, память данных и программ.
Код:
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(clk.posedge)
def st_swt():
if reset==0:
state.next = task_sw
else:
state.next = state + 1
Переключение контекста - считываем нужные рабочие регистры из наборов для текущего потока
Код:
@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_comb
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
.......
.......
else: #reset state
for i in range(Nthread):
th_sp[i].next=DS_BASE+STACKsize*i
th_rp[i].next=RS_BASE+STACKsize*i
# а тут небольшая тестовая программка ( 5 2 + ) - сложение с пятеркой, нестареющая универовская классика)))
C_ROM[0].next= lit0
C_ROM[1].next= 5+0x100
C_ROM[2].next= lit0
C_ROM[3].next= 2+0x100
C_ROM[4].next= add
C_ROM[5].next= nop
Финальный шаг Змееныша - инкремент счетчика потоков
Код:
@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
потом прописывается местная MyHDL-ская магия:
Код:
return st_swt, st_sw, st_get, st_ex, st_sv
и по сути - модуль готов.
Магия симуляции - определяется тестовая функция, генеруется тактовая последовательность, подается сбросовый сигнал.
Результат симуляции автоматически выгружается в файл .vcd (в данном случае - test.vcd), который потом скармливается GTKWave-у
Код:
def test():
reset = Signal(bool(0))
clk = Signal(bool(0))
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)
simulate(4096)
При желании и необходимости может быть транслирован в HDL файл, который потом скармливается любимой среде разработки под выбранное семейство ПЛИС
Код:
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)
#toVerilog(gor, reset, clk, data, prt)
convert()