В итоге мы его успешно ускорили до 25 МГц, после чего доработали компилятор, чтобы он автоматически обнаруживал так называемые Hazard'ы (конфликт соседних команд, из-за того, что они на самом деле выполняются "параллельно", одна записывает данные, а другая считывает "устаревшие") и исправлял их. Кроме того, доработали программу алгоритма захвата ("аффинного алгоритма"), чтобы она хорошо работала на этом процессоре. Как результат, она сократилась в размере (благодаря возможности пересылок "память-память" и упрощения кода в нескольких местах) и ускорилась в 5,5 раз.
Дальше я зачем-то попробовал поставить двунаправленную шину, и оно получилось ещё лучше (в теории. Для такого варианта надо было бы опять всю логику конвейера перекраивать, и Hazard'ы впридачу, поэтому провести симуляцию и получить конкретные результаты - это очень муторно), но сейчас я решил: остановлюсь на том, что есть. В дальнейшем, будет смысл ещё сильнее ускориться, до 50..80 МГц, для того, чтобы максимально быстро получать картинку с фотоприёмной матрицы 1205ХВ014. В этом один-единственный смысл: убрать эффект Rolling Shutter, насколько это вообще возможно. А "укороченный конвейер" на 25 МГц - это в любом случае полумера, нам главное, чтобы в принципе на 25 МГц работало, а выигрыш в несколько микросекунд за счёт более совершенной архитектуры совсем неинтересен (хотя всё равно червь грызёт, это уже психиатрическое).
Так что пока "фиксируем" имеющуюся архитектуру. Мы испытали "голое" ядро, теперь нужно присоединить к нему периферию: UART, SPI и контроллер ЖК-экранчика. Их нужно встроить в нашу "конвейерную логику", а именно, правильно прицепить к ним сигналы SrcDiscard, SrcStall, DestStall, SrcStallReq и DestStallReq...
А первым делом всё-таки оформим "ядро" QuatCore в отдельный модуль:

Все входы нужны: тактовая частота, сброс, входы для внешней статической памяти и для остальной периферии, и входы "запроса на остановку конвейера". Если активна команда на получение данных откуда-то "издалека" (UART, SPI), и ей требуется более одного такта, она должна подать единичку на DestStallReq (запрос на остановку и "получателя данных", и конвейера в целом). Если же активна команда на передачу данных, и она также пока "застряла", то она должна подать единичку на SrcStallReq. Тогда, если она использовала источником данных, к примеру, [--SP], то всё то время ожидания, пока нем не дали передать данные, на шине данных будет лежать именно [SP-1], и только ОДИН РАЗ будет вычитаться единица.
Мы пока оставили два отладочных выхода: PC (Program Counter) и MemAddr, они никуда подключаться не должны, просто чтобы на симуляции понимать, что там вообще происходит.
А все остальные очень важны. SrcAddr и DestAddr задают текущую команду "на запись" и "на чтение". Они должны поступать на каждый внешний модуль. DataBus - "выход" шины данных, после мультиплексирования и задержки на один такт. Это те данные, которые мы можем захотеть куда-то отправить, будь то статическая память, UART, SPI или ЖК-экранчик (и что там ещё добавится в процессе).
DestStall - сигнал для временного отключения всех команд "на запись", либо из-за того, что был произведён прыжок, а в конвейер "по инерции" попала команда, которую исполнять не надо, либо потому что команда "на чтение" выполняется больше одного такта, и можно наломать дров, если наша команда "на запись" хоть чуточку сложнее простой записи в регистр/в память. Т.е инкременты i++,j++,k++, [SP++], [--SP], а также арифметические действия нужно выполнить РОВНО ОДИН РАЗ, иначе жди беды.
SrcStall и SrcDiscard - то же самое для команд "на чтение", но мы как всегда жадные. SrcStall означает - "именно эту команду и надо исполнить, но не торопись". SrcDiscard означает - "это вообще неправильная команда, сделай вид, что её нет". Разница у нас возникла при чтении из памяти: увы, один такт уходит на формирование эффективного адреса и его отправки в блок внутренней памяти, и только к следующему такту мы получаем результат. И если совместно с этой командой выполняется команда умножения (к примеру), то если бы мы выдавали SrcDiscard, то сначала пришлось бы ждать окончания умножения, а потом ещё ждать, пока сформируется эффективный адрес и мы получим правильное значение из памяти, т.к всё время выполнения умножения наш модуль памяти "страдал фигнёй". А так он уже запросит нужный адрес, и будет готов выдать его "мгновенно", но если это был адрес [SP++], то с прибавлением единички он повременит.
Также у нас "выведено наружу" множество параметров: ширина адреса ПЗУ/ROM (где хранится программа), ОЗУ/RAM (где хранятся текущие данные, но при включении ПЛИС они также инициализируются "как пожелаем", что позволяет элементарно хранить там строки и константы, не придумывая хитрючие "загрузчики" или целую операционку). Можно поиграться с шириной аккумулятора - от 19 до 32 бит, чем больше - тем точнее, но занимает много ЛЭ. ijkEnabled - позволяет включить или отключить многострадальную команду ijk (как на запись, так и на чтение), которая собирает в 16 бит регистры i,j,k (каждый по 5 бит) и Inv (1 бит), чтобы можно было их дружно инициализировать, а также одним махом сохранить в стек и извлечь из стека. Казалось, что хорошая идея, но выходной мультиплексор QuatCorePC от этого неплохо "раздувается".
И наконец, EnableIO и EnableSRAM позволяет чуть-чуть упростить "ядро" процессора, мультиплексор шины данных, если известно, что один из входов будет незадействован.
Теперь приделаем хотя бы UART "на передачу", увидеть сообщение Hello, World! на компьютере. Но сразу с перспективой подключить всё остальное. Первым делом размещаем модуль QuatCoreIOselector, слегка доработанный, чисто "из жадности":

А вот код этого модуля:
//DestAddr==000x_xxxx : 'OUT' //DestAddr==001x_xxxx : select output device //xxxx_xxxx_xxxx_xx00 - UART //xxxx_xxxx_xxxx_xx01 - LCD //xxxx_xxxx_xxxx_xx1x - SPI //also selecting SPI device //xxxx_xxxx_xxxx_0xxx - Ethernet //xxxx_xxxx_xxxx_10xx - SD //xxxx_xxxx_xxxx_11xx - ADC //IN command is //SrcAddr == 1001_0xxx //(ALU Src was 100x_xxxx, now it is 1000_xxxx) module QuatCoreIOselector ( input clk, input [7:0] SrcAddr, input [7:0] DestAddr, input [15:0] DataBus, input SPI_busy, output UARTtxEN, output LCD_EN, output SPItxEN, output UARTrxEN, output SPIrxEN, output reg [1:0] SPIdevice = 2'b0); parameter enableLCD = 1'b0; parameter enableUART = 1'b1; parameter enableSPI = 1'b0; localparam HasChoice = enableSPI | ((enableLCD + enableUART + enableSPI) > 1); wire isSelection = (~DestAddr[7]) & (~DestAddr[6]) & DestAddr[5] & HasChoice; wire isIO_out = (~DestAddr[7])&(~DestAddr[6])&((~DestAddr[5]) | (~HasChoice)); wire isIO_in = (SrcAddr[7:3] == 5'b1001_0); reg [1:0] sel = enableUART? 2'b00: enableLCD? 2'b01: 2'b10; reg [1:0] shadowSPI = 2'b0; always @(posedge clk) if (isSelection) begin sel <= DataBus[1:0]; shadowSPI <= DataBus[3:2]; end always @(posedge clk) if (~SPI_busy) SPIdevice <= shadowSPI; assign UARTtxEN = (sel==2'b00) & isIO_out & enableUART; assign LCD_EN = (sel==2'b01) & isIO_out & enableLCD; assign SPItxEN = sel[1] & isIO_out & enableSPI; assign UARTrxEN = (sel==2'b00) & isIO_in & enableUART; assign SPIrxEN = sel[1] & isIO_in & enableSPI; endmodule
Если помните, мы решили (как всегда, это решение не единственно возможное и наверняка не лучшее), что будут команды IN и OUT, которые будут принимать данные с устройства ввода-вывода или выдавать данные на него, а устройство будет выбираться с помощью команды SIO (Select I/O), и при текущей реализации QuatCoreIOSelector может быть выбрано UART, либо ЖК-экранчик, либо SPI, и дополнительно можно выбрать одно из 3 устройств, подключённых по SPI: медленный АЦП (присутствует на отладочной плате для 5576ХС4Т), Ethernet-контроллер (также присутствует, и является одним из двух "генераторов тактовой частоты", причём его тактовую частоту можно настроить) и SD-карточка (её я туда подпаял и учился с ней работать по SPI). Позже часть устройств может стать не нужна, а другие добавятся, навроде приёмопередатчика МКО (он же МКИО, он же ГОСТ Р 52070-2003, он же MIL-STD 1553).
А очередная жадность состоит в том, что теперь этот QuatCoreIOSelector не просто заведомо отключит те модули ввода-вывода, которые мы заявим как ненужные (с помощью параметров EnableUART, EnableSPI, EnableLCD), но и в целом "упразднит" команду SIO и свои регистры, на неё завязанные, если окажется, что устройство ввода-вывода всего одно. А именно, в этом случае локальный параметр HasChoice станет равен нулю, и тогда isSelection тоже заведомо будет нулевым, и в этом случае регистры sel и ShadowSPI никогда не будут обновляться, и синтезатор их выкинет, заменив начальными значениями. В итоге, если нам нужен только UART, то размер QuatCoreIOselector уменьшится с 15 ЛЭ до 3 ЛЭ, не считая того, что и незадействованные модули автоматически "самоустранятся".
И теперь подключим передатчик UART, вот его код:
`include "math.v" module QuatCoreUARTtx (input clk, input st, input [15:0] DataBus, output busy, output txd); parameter CLKfreq = 4_000_000; parameter BAUDrate = 1_000_000; localparam sIdle = 4'b0000; localparam sStart = 4'b0110; localparam sB1 = 4'b0111; localparam sB2 = 4'b1000; localparam sB3 = 4'b1001; localparam sB4 = 4'b1010; localparam sB5 = 4'b1011; localparam sB6 = 4'b1100; localparam sB7 = 4'b1101; localparam sB8 = 4'b1110; localparam sStop = 4'b1111; wire [3:0] State; wire isStopState; wire isIdle = (~State[3]) & (~State[2]); //shortcut as not all states are used wire DoStart = st & isIdle; wire [7:0] Data = DataBus[7:0]; wire ce; //count enable reg r_ce = 1'b0; reg r_set = 1'b0; always @(posedge clk) begin r_ce <= ce; r_set <= ce | DoStart | isIdle; end lpm_counter StateMachine ( .clock (clk), .cnt_en (r_ce), .sset (DoStart), .q (State), .cout (isStopState) ); defparam StateMachine.lpm_direction = "UP", StateMachine.lpm_port_updown = "PORT_UNUSED", StateMachine.lpm_type = "LPM_COUNTER", StateMachine.lpm_width = 4, StateMachine.lpm_svalue = sStart; localparam DividerBits = `CLOG2(((CLKfreq + BAUDrate / 2) / BAUDrate - 1)); localparam Limit = (CLKfreq + BAUDrate / 2) / BAUDrate - 2; lpm_counter Divider ( .clock (clk), .sset (r_set), .cout (ce) ); defparam Divider.lpm_direction = "DOWN", Divider.lpm_port_updown = "PORT_UNUSED", Divider.lpm_type = "LPM_COUNTER", Divider.lpm_width = DividerBits, Divider.lpm_svalue = Limit; reg [8:0] ShiftReg = 9'b1111_1111_1; assign txd = ShiftReg[0]; assign busy = st & (~isIdle); always @(posedge clk) if (DoStart | r_ce) ShiftReg <= DoStart? {Data, 1'b0} : {1'b1, ShiftReg[8:1]}; endmodule
И теперь чуть-чуть вспомним, что такое выход busy этого модуля. Когда мы просим его передать байт данных (st=1), busy=0 на этом такте. А затем, пока этот байт передаётся, получится busy=1, если снова st=1, т.е мы уже запросили передачу ещё одного байта, хотя и этот передать не успели! Это хорошее решение, т.к модуль UART может работать вообще без перерывов: мы отдали ему первый байт на передачу, затем процессор двинулся на байт вперёд, проверил условия окончания цикла, перешёл в начало цикла, отдал следующий байт на передачу - и вот только сейчас остановился, чтобы дождаться, пока предыдущий байт будет отправлен. Получается какое-никакое распараллеливание работ: модуль UART работает сам по себе, процессор работает сам по себе, а если он пытается "перегрузить" UART, то "автоматически" останавливается - очень приятный подход.
Подключим этот выход busy к входу SrcStallReq: тем самым мы добьёмся остановки конвейера, пока мы не закончили передавать предыдущий байт, и остановим "источник данных", который готовит значение для следующей команды. Получается пока вот так:

Увы, "философия" busy этого модуля отличается от того, что мы делали с АЛУ и другими устройствами "ядра". Если помните, при обращении к АЛУ мы всегда могли защёлкнуть в него новые данные СРАЗУ ЖЕ, и только после этого он нам останавливал выполнение, пока всё не сделает. Так уж "исторически сложилось", хотя правильнее было бы сделать так же, как с UART: пусть мы даём АЛУ команду "перемножить" - и переходим к следующим командам, а АЛУ перемножает себе потихоньку. И только если мы запросим результат этого умножения, будь то непосредственно значение Acc / UAC, или делая условный переход по JO/JNO, JL/JGE, или если мы дадим очередную арифметическую операцию - лишь тогда мы остановимся и подождём, пока АЛУ завершит свои дела. Как ни странно, один шаг "навстречу" нам уже пришлось сделать - добавить интерлок (блокировку), которая дожидается полного окончания работы, прежде чем отдать значение Acc / UAC.
Разница заключается в том, что для АЛУ и вообще для всех модулей "ядра" до сих пор хватало "защёлки" на мультиплексоре, которая всегда задерживает поступление данных на шину НА ОДИН ТАКТ. Первый такт выполнения новой команды - данные там лежат. Ещё такт спустя - туда приходит что-то другое, так что "хватать" надо очень быстро. Но UART не может по первому требованию загрузить новый байт - у него сдвиговый регистр, который активно в работе по передаче предыдущего байта! Так что давайте в кои-то веки "продлим" PipeStall и до мультиплексора. Его код становится таким:
module QuatCoreSrcMux (input [15:0] SRAM, input [15:0] MEM, input [15:0] ALU, input [15:0] IMM, input [15:0] PC, input [15:0] IO, input [7:0] SrcAddr, input clk, input stall, output [15:0] Q); parameter DoLatch = 1'b1; parameter EnableIO = 1'b0; parameter EnableSRAM = 1'b0; reg [15:0] rQ; wire [15:0] combQ; assign combQ = (~SrcAddr[7])? IMM : SrcAddr[6]? MEM : SrcAddr[5]? PC : (~SrcAddr[4])|(~(EnableIO|EnableSRAM))? ALU : (SrcAddr[3]&EnableSRAM)|(~EnableIO)? SRAM : IO; always @(posedge clk) if (~stall) rQ <= combQ; assign Q = DoLatch? rQ : combQ; endmodule
Присоединяем вход stall к проводу PipeStall:

И теперь всё это вместе вполне успешно синтезируется в 516 ЛЭ, fitter срабатывает на удивление быстро (наверное, я пока не присоединил отладочных пинов, а 3 пина, clk, reset и UART_TX - это суперхалява!), тайминги выдерживаются, 25,71 МГц.
Самое время сдуть пыль с программы HelloWorld.asm и попробовать запустить, сначала на симуляторе, а потом и "в железе"...