Насколько быстр Intel 8080? Используем чипсет на FPGA чтоб проверить
- суббота, 14 июня 2025 г. в 00:00:06
Я люблю вызовы - например, написать код в условиях ограниченных ресурсов: медленный процессор, странный набор инструкций, крохи памяти. У меня уже было несколько проектов такого рода - я запускал тяжелую вычислительную задачу на процессорах, которые уже разменяли пол-века: Intel 4004, Intel 4040 и Intel 8008. Очевидно, что на очереди Intel 8080!
В этой статье я опишу детали проекта по созданию системной платы с чипсетом на основе FPGA, на которой будет запущен Intel 8080A-1 на частоте выше 3Мгц. А также расскажу о том, как писать программы для этого процессора на C, и в финале покажу результаты бенчмарков - Dhrystone и CoreMark.
Телеграм-канал не веду, но более развернутое описание проекта (данная статья содержит примерно 30% материала) и косяков, которые я допустил, есть на YouTube:
Очевидно, что схему проектируем, опираясь на спецификацию процессора:
В качестве питающего напряжения используются три уровня: +5V, -5V и +12V. Соответственно и управляющие сигналы тоже бывают +5V и +12V.
Целевая частота, на которой планируем запускать процессор - 3Мгц
Хотим предоставить максимальный объем памяти, который может быть адресован напрямую - 64КиБ
Для максимальной производительности мы не должны блокировать шину данных, когда читаем/пишем в ОЗУ. То есть, такие операции должны выполняться за 1 такт или быстрее.
С питанием всё понятно - рандомный буст-контроллер для +12V и ICL7660 для инвертирования +5V в -5V.
В качестве сердца системы я рассматривал вариант использования мощного микроконтроллера, а-ля stm32h7. Его производительности должно было бы хватить, но у меня есть в планах проекты плат для еще более высоких частот, и поэтому я выбрал FPGA как платформу для контроллера памяти. Хотя я не писал какой-либо осмысленный код под FPGA уже лет 20, но настало время освежить навыки.
Существует куча вариантов FPGA, но так как мне нужно было что-то попроще, чтоб не сильно накосячить, то требования к чипу были соответствующими:
Достаточно LUT'ов. Основной челлендж это код для 8080, а не эффективный синтез.
Flash/RAM должны быть интегрированы. Меньше микросхем на плате - меньше шанс ошибиться в разводке относительно высокоскоростных сигналов. Очевидно, что нужно RAM в достаточном объеме для 8080 (64КиБ).
Никаких BGA. Я до сих пор паяю вручную (и с помощью фена), поэтому BGA увеличивает сложность, особенно с учётом того, что и так весьма много мест, где система позволяет мне сделать что-то неправильное.
Можно залить bitstream с помощью подручных средств, без необходимости покупать программаторы от вендора за сотни денег, особенно с учётом того, что не факт, что я и дальше буду использовать то же семейство ПЛИС.
После изучения предложений на рынке, я остановился на Microchip IGLOO2. Не самый оптимальный выбор, но второй пункт про интегрированные Flash/RAM сильно ограничивал перечень возможных вариантов. Особенно настораживала странная среда разработки (Libero SoC). Но в итоге всё срослось как надо.
GPIO со стороны FPGA поддерживают напряжение в +3.3V. Значит нам нужно конвертировать их в +5V и +12V. Если с +5V проблем нет - существует много как однонаправленных, так и двунаправленных конверторов, то с +12V всё немного хитрее. Особенно если учесть, что на этом уровне 8080 ожидает увидеть тактовый сигнал (а его частота 3Мгц), то есть нам нужно весьма быстро переключать +3.3V в +12V. Я решил что с этой задачей неплохо справится высокоскоростной драйвер затвора (без доп транзистора) - токи небольшие. Однако изначально выбранный драйвер (2EDN752) перегревался через 10 секунд работы - видимо, 3Мгц было слишком круто для него. Пришлось поменять на пару UCC2751.
Нам нужно как-то инциализировать память для 8080. Хранить образ памяти в самой прошивке FPGA не хотелось, поэтому решил использовать USB интерфейс (тем более нам нужно питание) и передавать образ с ПК. Мало кто реализует USB устройство на FPGA, обычно ставят какой-то мост, который упаковывает данные с USB в более простой протокол - UART/SPI/I2C. В качестве такого моста, я использовал (сюрприз!) stm32. Почему? Хотел иметь запас прочности - если вдруг возникнет непредвиденная проблема, то возможно получится решить её программно с помощью микроконтроллера. Почти не пригодилось.
Оттрассировал в меру своих способностей (2 слоя, конечно, не айс, но вроде сильных шумов на осцилоскопе не видел).
Конечно, не обошлось без промашек (здесь их показывать я не буду), но фатальных недостатков не было (за исключением замены драйвера). Так что перезаказывать плату и перепаивать компоненты на новую ревизию я не хочу.
Программирование FPGA и микроконтроллера происходит через JTAG - отдельные коннекторы, ибо это домашняя плата и возня с цепочками JTAG смысла не имела. Я и так в какой-то момент погрузился слишком глубоко в SWD протокол, когда отлаживал причину отсутствия ответа от blue pill, которую я использовал для прошивки ПЛИС.
Кстати, через OpenOCD прошить Microchip IGLOO2 у меня не вышло, но зато получилось найти код от Microchip, который позволяет общаться с FPGA через JTAG. На его основе я написал простой софт для ПК, который через ft232h-адаптер заливал bitstream в flash-память. Единственно, это занимало 45 минут, и, когда уже мне надоело ждать по часу между итерациями, я перенёс код на blue pill, которая валялась рядом. И время заливки уменьшилось до 20 секунд, что вообще ни о чём, особенно учитывая скорость синтеза Verilog кода.
После устранения всех очевидных проблем на уровне железа, настало время писать код. Причём разнообразный - управляющий код на ПК, который будет высылать образ RAM; код для stm32, соединяющий ПК и FPGA; основной Verilog-код для взимодействия с 8080; и сами программки для 8080, конечно же.
Прежде чем показывать куски кода, имеет смысл рассказать о нашем процессоре по-подробнее. В первую очередь, стоит отметить, что 16-битное пространство памяти не разделено на память команд и память данных. В системах на 8080, это разделение между RAM и ROM было исполнено на аппаратном уровне - какие-то адреса маршрутизировались в чипы ПЗУ, какие-то в ОЗУ. Поэтому нам достаточно просто предоставить 64 кибибайтный блоб, а софт под 8080 будет решать какую часть использовать под данные. Я добавил только небольшой штрих - аппаратный регистр, размещенный в памяти, который содержит текущий счетчик тактов. Да, 8080 имеет инструкции для взаимодействия с подсистемой ввода/вывода, но мне показалось удобнее сделать так, как сделал я.
Помимо очевидных шин адреса и данных, нам нужно работать еще с парочкой сигналов со стороны 8080.
Нас не особо интересует внутреннее устройство 8080, поэтому мы ориентируемся только на внешние сигналы. И сигнал SYNC означает начало цикла работы с шиной данных - чтение/запись/ввод/вывод. Спустя какое-то время после поднятия SYNC (я ориентируюсь на задний фронт второго тактового сигнала), мы можем прочитать адрес с шины адреса и тип операции с шины данных.
Сами данные на шине появляются позже - если это операция чтения или ввода с внешнего устройства, то процессор устанавливает DBIN в высокий уровень и ожидает данные на шине. Противоположная операция записи/вывода работает аналогично, но уже сигнал WR ставится в низкий уровень и на шине данных выставлятся байт для отправки в память или внешнему устройству.
В целом, это всё что нужно знать. Конечно, сигналов несколько больше, но мы не реализуем прерывания, сигналы останова и сигналы готовности подсистемы памяти. Наша память всегда готова!
Многого в плане управления нам не нужно - только отправить образ памяти и ребутнуть процессор. Дополнительно, я добавил парочку служебных команд дабы проверить работу встроенной eSRAM в ПЛИС.
const InCommandType = Object.freeze({ CmdAck: 0x01, CmdResult: 0x02, CmdPrintTime: 0x05 });
const OutCommandType = Object.freeze({ CmdWriteDump: 0x01, CmdWriteByte: 0x02, CmdReadByte: 0x03, CmdReset: 0x04 });
const sendCommand = (port, opcode, data) => new Promise((resolve, reject) => {
const result = [];
const writeResult = ({ resultByte }) => result.push(resultByte);
eventBus.on('result', writeResult);
eventBus.once('ack', () => {
eventBus.off('result', writeResult);
resolve(result);
});
port.write(Buffer.from([opcode, ...(data ?? [])]), (err) => err && reject(err));
});
const processInputCommand = (buf, offset, len) => {
switch (buf[offset]) {
case InCommandType.CmdAck:
eventBus.emit('ack');
return 1;
case InCommandType.CmdResult:
if (len - offset < 2) {
return 0;
}
eventBus.emit('result', { resultByte: buf[offset + 1] });
return 2;
case InCommandType.CmdPrintTime:
console.log(`\nCurrent time: ${Date.now()}ms\n`);
return 1;
default:
process.stdout.write(String.fromCharCode(buf[offset]));
return 1;
}
};
FPGA так же пробрасывает данные, которые выплёвывает 8080 в порт вывода. Здесь пришлось использовать небольшой хак: вместо того, чтобы завести отдельный код команды вывода байта на терминал, я просто вывожу всё, что не подпадает под известные опкоды.
Зачем? Потому что у меня была программа, которая хотела вывести на консоль много данных, и делала это весьма шустро (каждые 450 тиков = 150мкс). А передача 2х байт по UART между FPGA и stm32 занимала 175мкс на скорости интерфейса в 115200. Буферы, конечно, имелись, но их размер был не бесконечный. Скорость увеличивать было страшно - UART заработал не сразу (показывает мой уровень, хех), и поэтому трогать его не хотелось. Тем более выводил я только печатные ascii-символы, которые не пересекаются с кодами команд, так что хак рабочий.
Возможно возникнет еще вопрос о предназначении CmdPrintTime
. Ответ заключается в том, что целевая частота не ровна максимальным 3.125Мгц, а несколько меньше, и немного плавает. Поэтому я использовал время на хост-машине и количество выполненных тактов для вычисления реальной тактовой частоты.
Микроконтроллер просто обеспечивал конвертацию данных USB интерфейса в UART до ПЛИС.
typedef struct {
uint8_t buffer[TRANSFER_SIZE];
volatile uint16_t writePtr;
volatile uint16_t readPtr;
} RingBuffer;
static RingBuffer transferQueue = { .writePtr = 0, .readPtr = 0 };
void addDataToQueue(uint8_t data) {
__disable_irq();
transferQueue.buffer[transferQueue.writePtr] = data;
transferQueue.writePtr = (transferQueue.writePtr + 1) % TRANSFER_SIZE;
__enable_irq();
}
void flushData() {
uint8_t transferChunk[TRANSFER_CHUNK_SIZE];
// we don't want to disable IRQs for a long time, so at first we just copying chunk of queue into small buffer
__disable_irq();
uint16_t writePtr = transferQueue.writePtr, readPtr = transferQueue.readPtr;
uint16_t transferSize = (writePtr >= readPtr) ? (writePtr - readPtr) : (TRANSFER_SIZE - readPtr);
uint8_t chunkSize = (transferSize > TRANSFER_CHUNK_SIZE) ? TRANSFER_CHUNK_SIZE : transferSize;
for (uint8_t i = 0; i < chunkSize; ++i) {
transferChunk[i] = transferQueue.buffer[readPtr];
readPtr = (readPtr + 1) % TRANSFER_SIZE;
}
transferQueue.readPtr = readPtr;
__enable_irq();
if (!chunkSize) {
return;
}
while (CDC_Transmit_FS(transferChunk, chunkSize) != USBD_OK);
}
Обычный кольцевой буфер. Единственный нюанс в том, что передача данных на хост через USB выполнялась в главном цикле и не хотелось дергать CDC_Transmit_FS
с отключенными прерываниями, поэтому копируем кусок кольцевого буфера во временную переменную, включаем прерывания и отправляем этот кусок на ПК.
Самая интересная часть это, конечно же, код для FPGA. Именно здесь происходит вся жара. Выбор ПЛИС диктовал среду разработки, поэтому пришлось адаптироваться. Итеративно я составил такую схему компонента верхнего уровня.
Голубенький прямоугольник это как раз подсистема встроенной RAM-памяти. Работает через AHB-шину, и лучше на сниженной тактовой частоте. Поэтому перед ней есть AHB-мастер и мой мультиплексер/контроллер, который согласовывает разные тактовые домены - 180Мгц основной частоты и 50Мгц для eSRAM.
Из служебных модулей можно еще упомянуть приснопамятный UART. Кроме самих модулей TX/RX я добавил мультиплексер (чтоб отправлять байтики с разных мест) и простенькую FIFO-очередь.
Обработкой команд с UART'a занимается нехитрая FSM. Ради примера покажу как исполняется команда на загрузку дампа памяти:
// WRITE_DUMP 0x01 size1 size0 data[N] data[N-1] .... data[1] data[0]
if (in_cmd_opcode == CMD_IN_WRITE_DUMP && in_byte_count == 2) begin
case (in_cmd_state)
CMD_WRITE_DUMP_STATE_READ_DATA: begin
if (uart_rx_valid) begin
in_cmd_value <= uart_rx_data;
in_cmd_addr <= in_cmd_addr - 1;
in_cmd_state <= CMD_WRITE_DUMP_STATE_WRITE_MEM;
end
end
CMD_WRITE_DUMP_STATE_WRITE_MEM: begin
sram_data_reg <= in_cmd_value;
sram_write <= 1;
sram_read <= 0;
sram_addr_reg <= in_cmd_addr;
if (sram_busy == 0) begin
in_cmd_state <= CMD_WRITE_DUMP_STATE_WRITTEN;
end
end
CMD_WRITE_DUMP_STATE_WRITTEN: begin
sram_write <= 0;
if (in_cmd_addr == 0)
in_state <= CMD_STATE_FINISHED;
else
in_cmd_state <= CMD_WRITE_DUMP_STATE_READ_DATA;
end
endcase
end
Взимодействие с 8080 начинается с генерации тактовых сигналов (их, кстати, два):
module i8080_clock(
input wire clk, // expects 184.333 Mhz, one tick ~ 5.43ns
input wire rst,
// to i8080
output reg CLK1,
output reg CLK2,
output reg READY
);
reg [5:0] counter;
// 0 .. 59 = 320ns period = 3.125Mhz
always @(posedge clk) begin
if (rst)
counter <= 0;
else if (counter == 6'd59)
counter <= 0;
else
counter <= counter + 1;
end
// phi1 high at [0, 50ns] clock interval
always @(posedge clk) begin
if (rst)
CLK1 <= 1;
else
CLK1 <= (counter < 6'd9); // ~49ns
end
// phi2 high at [60ns, 210ns] clock interval
always @(posedge clk) begin
if (rst)
CLK2 <= 1;
else
CLK2 <= ((counter >= 6'd11) && (counter < 6'd39)); // ~59ns ... ~206ns
end
assign READY = 1;
endmodule
Не получилось абсолютно точно передать форму тактового сигнала для частоты в 3.125Мгц, но вышло достаточно близко. Всё равное выше 3Мгц. Приемлемо.
Наконец-то код самого контроллера памяти. Так как сигналы - внешние, то сначала запихиваем их в наш тактовый домен, через две триггера:
// synchronization of external signals from i8080 via two flip-flops
reg i8080_sync_1tick_before, i8080_sync_2tick_before;
reg i8080_dbin_sync0, i8080_dbin_sync1;
reg i8080_wr_sync0, i8080_wr_sync1;
always @(posedge clk or posedge rst) begin
if (rst) begin
i8080_sync_1tick_before <= 0;
i8080_sync_2tick_before <= 0;
i8080_dbin_sync0 <= 0;
i8080_dbin_sync1 <= 0;
i8080_wr_sync0 <= 0;
i8080_wr_sync1 <= 0;
end else begin
i8080_sync_1tick_before <= i8080_sync;
i8080_sync_2tick_before <= i8080_sync_1tick_before;
i8080_dbin_sync0 <= i8080_dbin;
i8080_dbin_sync1 <= i8080_dbin_sync0;
i8080_wr_sync0 <= i8080_wr;
i8080_wr_sync1 <= i8080_wr_sync0;
end
end
wire i8080_sync_rise = i8080_sync_1tick_before && !i8080_sync_2tick_before;
wire i8080_clk2_rise = i8080_clk2 && !clk2_sync;
Ловим поднятие SYNC сигнала, ждём когда сможем защёлкнуть статусный байт и адрес, и решаем что делать дальше - читать из памяти, писать в память или отправлять байт на ПК, чтоб вывести его на терминал:
STATE_IDLE: begin
if (i8080_sync_rise)
state <= STATE_WAIT_STATUS;
end
STATE_WAIT_STATUS: begin
if (!clk2_sync) begin
i8080_status_latched <= i8080_data;
i8080_addr_latched <= i8080_addr;
state <= STATE_CHECK_STATUS;
end
end
STATE_CHECK_STATUS: begin
if (i8080_status_latched[3] == 1) // HLTA
state <= STATE_IDLE;
else if (i8080_status_latched[7] == 1) // memory read
state <= STATE_READ_SRAM;
else if (i8080_status_latched[1] == 0) // memory write or output
state <= STATE_LATCH_DATA_TO_WRITE;
else
state <= STATE_IDLE;
end
Чтение тривиально, за исключением эмуляции регистра счетчика тактов, который должен лежать по определенному адресу:
STATE_READ_SRAM: begin
if (i8080_addr_latched == 16'hF880 || i8080_addr_latched == 16'hF881 || i8080_addr_latched == 16'hF882 || i8080_addr_latched == 16'hF883 || i8080_addr_latched == 16'hF884) begin
state <= STATE_LATCH_SRAM_DATA;
end else if (sram_busy == 0) begin
sram_read <= 1;
sram_req <= 1;
sram_write <= 0;
state <= STATE_LATCH_SRAM_DATA;
end
end
STATE_LATCH_SRAM_DATA: begin
case (i8080_addr_latched)
16'hF880: begin
sram_data_latched <= clocks[7:0];
state <= STATE_SEND_DATA_TO_CPU;
end
16'hF881: begin
sram_data_latched <= clocks[15:8];
state <= STATE_SEND_DATA_TO_CPU;
end
16'hF882: begin
sram_data_latched <= clocks[23:16];
state <= STATE_SEND_DATA_TO_CPU;
end
16'hF883: begin
sram_data_latched <= clocks[31:24];
state <= STATE_SEND_DATA_TO_CPU;
end
16'hF884: begin
sram_data_latched <= clocks[39:32];
state <= STATE_SEND_DATA_TO_CPU;
end
default:
if (sram_busy == 0 && sram_valid == 1) begin
sram_read <= 0;
sram_req <= 0;
sram_data_latched <= sram_datain;
state <= STATE_SEND_DATA_TO_CPU;
end
endcase
end
STATE_SEND_DATA_TO_CPU: begin
if (i8080_dbin_sync1) begin
data_output_enable <= 1;
state <= STATE_FREE_DATA_BUS;
end
end
STATE_FREE_DATA_BUS: begin
if (!i8080_dbin_sync1) begin
data_output_enable <= 0;
state <= STATE_IDLE;
end
end
И, в принципе, это всё (код для записи или отправки байта приводить не стал, там ничего интересного). Получился весьма простой Verilog-код, дай бог на пару тысяч строк. Я считаю, что это отлично - меньше кода, значит лучше.
Прошлые разы я писал всё на ассемблере, но в этот раз я воспользовался C и тулкитом z88dk, который поддерживает 8080 в качестве целевой архитектуры.
Нужно просто добавить пару конфигов и можно компилировать сишный код!
#
# Target configuration file for z88dk, should be placed at z88dk\lib\config\
#
CRT0 DESTDIR\lib\target\8080\classic\8080_crt.asm
OPTIONS -m -O2 -SO2 -M --list -subtype=default -clib=8080 -D__8080__
CLIB 8080 -Cc-standard-escape-chars -m8080 -l8080_opt -lndos -l8080_clib -startuplib=8080_crt0 -LDESTDIR\lib\clibs\8080
SUBTYPE default -Cz+hex
; CRT for i8080-sbc, should be placed at z88dk\lib\target\8080\classic\8080_crt.asm
module i8080_crt0
defc crt0 = 1
INCLUDE "zcc_opt.def"
EXTERN _main ;main() is always external to crt0 code
EXTERN asm_im1_handler
PUBLIC cleanup ;jp'd to by exit()
IFNDEF CLIB_FGETC_CONS_DELAY
defc CLIB_FGETC_CONS_DELAY = 150
ENDIF
defc TAR__clib_exit_stack_size = 4
defc TAR__register_sp = 0x0000
defc CRT_KEY_DEL = 12
defc __CPU_CLOCK = 3125000
defc CONSOLE_COLUMNS = 64
defc CONSOLE_ROWS = 32
INCLUDE "crt/classic/crt_rules.inc"
defc CRT_ORG_CODE = 0x0000
org CRT_ORG_CODE
if (ASMPC <> $0000)
defs CODE_ALIGNMENT_ERROR
endif
jp program
INCLUDE "crt/classic/crt_z80_rsts.asm"
program:
INCLUDE "crt/classic/crt_init_sp.asm"
INCLUDE "crt/classic/crt_init_atexit.asm"
call crt0_init_bss
ld hl,0
add hl,sp
ld (exitsp), hl
; Optional definition for auto MALLOC init
; it assumes we have free space between the end of
; the compiled program and the stack pointer
IF DEFINED_USING_amalloc
INCLUDE "crt/classic/crt_init_amalloc.asm"
ENDIF
push bc ;argv
push bc ;argc
call _main
pop bc
pop bc
cleanup:
call crt0_exit
INCLUDE "crt/classic/crt_terminate.inc"
INCLUDE "crt/classic/crt_runtime_selection.asm"
INCLUDE "crt/classic/crt_section.asm"
Файл для инциализации CRT содержит пару важных магических констант: CRT_ORG_CODE
задаёт адрес начала кода (у нас программа стартует с 0x0000) и TAR__register_sp
которая содержит начальное значение указателя на стек. Не смущайтесь значению 0x0000 - процессор сначала декрементирует регистр, а только потом пишет в память. Поэтому первый push будет выполнен по адресу 0xFFFF, а затем стек будет расти вниз. Всё как надо.
Кроме того, нам нужен простейший модуль для ввода/вывода на экран (хотя бы чтоб printf работал):
fputc_cons_native:
_fputc_cons_native:
pop bc ;return address
pop hl ;character to print in l
push hl
push bc
ld a,l
out (1),a
ret
fgetc_cons:
_fgetc_cons:
ret
Просто отсылаем байт на устройство 1 с помощью инструкции out
. FPGA определит что 8080 хочет отправить байт в подсистему ввода/вывода и перенаправит его через UART на ПК. А мы его уже покажем в нашей консоли.
Несмотря на то, что компуктер у нас 8-битный (если говорим об ALU), z88dk предоставляет математическую библиотеку для работы с 16/32/64-битными числами (числа с плавающей запятой не пробовал, но вроде как тоже есть поддержка):
uint32_t carry = 0;
uint16_t denominator = len - 1, numerator = (2 * len - 1), idx = 0;
while (denominator > 0) {
uint32_t x = ((uint32_t)A[idx]) * 10L + carry;
carry = denominator * (x / numerator);
A[idx] = x % numerator;
denominator--;
numerator -= 2;
idx++;
}
Однако важно следить за корректным приведением типов - лучше всегда явно приводить к нужной размерности, а иначе можно получить внезапную потерю разрядов.
А вот пример того, как можно вывести текущее количество тактов:
void storeTime(uint8_t * dst) {
*dst = *(uint8_t *)0xF880;
*(dst + 1) = *(uint8_t *)0xF881;
*(dst + 2) = *(uint8_t *)0xF882;
*(dst + 3) = *(uint8_t *)0xF883;
*(dst + 4) = *(uint8_t *)0xF884;
}
static char hex2char[16] = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
static printHex(uint8_t val) {
fputc_cons(hex2char[val >> 4]);
fputc_cons(hex2char[val & 0xF]);
}
void printTime(char * prefix, uint8_t * tm) {
fputs(prefix, stdout);
fputs(": ", stdout);
printHex(*(tm + 4));
printHex(*(tm + 3));
printHex(*(tm + 2));
printHex(*(tm + 1));
printHex(*tm);
fputs(" ticks\n", stdout);
}
Обычно я предпочитаю разрабатывать эмулятор целевой системы самостоятельно дабы лучше понять архитектуру и взаимодействие компонентов на сигнальном уровне, но в данном случае 8080 достаточно прост, поэтому я воспользовался готовым проектом.
Пришлось внести некоторые небольшие модификации - в частности, добавить эмуляцию регистра счетчика тактов. На основе этого проекта я реализовал профайлер и простой отладчик.
После отладки тестовых программ, а-ля различных вариантов hello world, я наконец-то запустил реальный код на реальном 8080. Это весьма знаменитый в прошом бенчмарк - Dhrystone.
Если перевести такты в секунды (и учесть известную среднюю тактовую частоту процессора), то получится ~0.064 DMIPS, что не очень много. Для сравнения Raspberry Pi 3 выдаёт около 3500 DMIPS. Но из винтажных систем удалось опередить Apple IIe и крайне популярный в США Commondore 64. Что ж, хоть что-то.
Следующий на очереди более современный бенчмарк, который и сейчас используется для различных тестов процессоров/микроконтроллеров/ядер.
После нехитрых вычислений получаем 0.027 попугаев. К сожалению, никто не портировал этот бенчмарк на самые слабые процессоры, так что скорее всего это худший результат в истории.
А как же моё любимое число π? Конечно же, я опять написал код для его вычисления! В этот раз с использованием алгоритма Чудновского и "быстрым" вычислением квадратного корня для длинной арифметики (не, это не метод Ньютона). Но это тема для другой статьи - более "математической".
Весь исходный код разбит на три репозитория:
https://github.com/quasiengineer/i8080-sbc - схема платы, gerber-файлы, прошивки для stm32/fpga и управляющая программа для ПК.
https://github.com/quasiengineer/i8080-emulator - эмулятор для 8080
https://github.com/quasiengineer/i8080-benchmarks - набор программ, написанных для 8080