Введение
Я люблю вызовы - например, написать код в условиях ограниченных ресурсов: медленный процессор, странный набор инструкций, крохи памяти. У меня уже было несколько проектов такого рода - я запускал тяжелую вычислительную задачу на процессорах, которые уже разменяли пол-века: 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 кода.
Gateware / Firmware / Software
После устранения всех очевидных проблем на уровне железа, настало время писать код. Причём разнообразный - управляющий код на ПК, который будет высылать образ RAM; код для stm32, соединяющий ПК и FPGA; основной Verilog-код для взимодействия с 8080; и сами программки для 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Мгц, а несколько меньше, и немного плавает. Поэтому я использовал время на хост-машине и количество выполненных тактов для вычисления реальной тактовой частоты.
Firmware для stm32
Микроконтроллер просто обеспечивал конвертацию данных 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
с отключенными прерываниями, поэтому копируем кусок кольцевого буфера во временную переменную, включаем прерывания и отправляем этот кусок на ПК.
Gateware для FPGA
Самая интересная часть это, конечно же, код для 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-код, дай бог на пару тысяч строк. Я считаю, что это отлично - меньше кода, значит лучше.
Разработка для 8080
Компилятор
Прошлые разы я писал всё на ассемблере, но в этот раз я воспользовался 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 достаточно прост, поэтому я воспользовался готовым проектом.
Пришлось внести некоторые небольшие модификации - в частности, добавить эмуляцию регистра счетчика тактов. На основе этого проекта я реализовал профайлер и простой отладчик.
Dhrystone бенчмарк
После отладки тестовых программ, а-ля различных вариантов hello world, я наконец-то запустил реальный код на реальном 8080. Это весьма знаменитый в прошом бенчмарк - Dhrystone.

Если перевести такты в секунды (и учесть известную среднюю тактовую частоту процессора), то получится ~0.064 DMIPS, что не очень много. Для сравнения Raspberry Pi 3 выдаёт около 3500 DMIPS. Но из винтажных систем удалось опередить Apple IIe и крайне популярный в США Commondore 64. Что ж, хоть что-то.
CoreMark
Следующий на очереди более современный бенчмарк, который и сейчас используется для различных тестов процессоров/микроконтроллеров/ядер.

После нехитрых вычислений получаем 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
Комментарии (8)
Zara6502
10.06.2025 10:15Для меня странно что i8080 обошёл 6502/6510, спросил у ИИ (можно перепроверить конечно), но по моим ощущением он ближе к правде:
| Система | Процессор | Такт.частота | Dhrystones/s | |-------------|-----------------|--------------|--------------| | Apple II | MOS Tech 6502 | 1 МГц | 1500 | | Commodore 64| MOS Tech 6510 | 1 МГц | 1700 | | Atari 130XE | MOS Tech 6502C | 1,79 МГц | 2000 | | Intel 8080 | Intel 8080 | 2 МГц | 400 |
mark_ablov Автор
10.06.2025 10:15Не знаю, откуда данные у ChatGPT, но вот данные, или можно даже глянуть в сами исходники Dhrystone: https://github.com/ARM-software/workload-automation/blob/master/wa/workloads/dhrystone/src/dhrystone.c
* MACHINE MICROPROCESSOR OPERATING COMPILER DHRYSTONES/SEC. * TYPE SYSTEM NO REG REGS * -------------------------- ------------ ----------- --------------- * Apple IIe 65C02-1.02Mhz DOS 3.3 Aztec CII v1.05i 37 37 * - Z80-2.5Mhz CPM-80 v2.2 Aztec CII v1.05g 91 91 * - 8086-8Mhz RMX86 V6 Intel C-86 V2.0 197 203LM??
37 dhrystones/sec для 65C02 ;)
Нюанс может быть в том, что информация по 6502/6510 представлен для Dhrystone v1.1, а я запускал v2.1, но обычно очки в v2.1 ниже, то есть разрыв с 8080 может быть еще больше.
Скорее всего дело в быстрой памяти. Я отдаю данные в тот же такт, а обычно в системах проц ждёт память.
checkpoint
10.06.2025 10:1537 dhrystones/sec для 65C02 ;)
Не верно ты, дядь Фёдор, бутерброд ешь! :)
В приведенной Вами таблице указаны результаты тестов на системе, а не "голого" процессора. Известно, что у Apple II видеоподсистема (как и у C64, или у Atari 800) останавливает центральный процессор при обновлении кадра, что занимает почти половину времени. Еще один момент состоит в том, что в Apple II использовалась динамическая память, которая требует циклов регенерации в процессе выполнения которых она недоступна и процессор простаивает в ожидании. Вы же проводите замеры чистого процессора без внешних раздражителей на статической памяти.
Известно, что i8080 в среднем выполняет 0.125 млн операций в секунду на частоте 1 МГц. Для MOS 6502 этот показатель равен 0.45 млн, т.е. в три с половиной раза больше. Однако у i8080 больше регистров, но у 6502 первая страница памяти (256 байт) может использоваться как таблица указателей, что существенно ускоряет вычисления. В итоге выиграет тот бенчмарк, который более тщательно адаптирован под специфику процессора и конкретной вычислительной системы. :-)
mark_ablov Автор
10.06.2025 10:15В итоге выиграет тот бенчмарк, который более тщательно адаптирован под специфику процессора и конкретной вычислительной системы. :-)
Скорее дело в компиляторе. Не зря CoreMark требует указывать компилятор и его флаги, если захочешь запостить результаты.
Вы же проводите замеры чистого процессора без внешних раздражителей на статической памяти.
Ага, указал на этот момент в родительском моменте. На i8080 системах проц тоже обычно ждёт память, что съедает такты.
Не верно ты, дядь Фёдор, бутерброд ешь! :)
Та понятно что это не совсем правильный подход к бенчмаркам (тема сама по себе весьма сложная). Цель не в сравнении попугаями, а больше в том, чтоб собрать систему и иметь запускать возможность код.
spiteman
10.06.2025 10:15Можно было и не тестировать, так как 8080 был близок (немного отставал) от Z80, а его мы знаем по многим проектам, но только ZX Spectrum.
Flammmable
Почему не MAX-10 от Altera или не MachXO2 от Lattice?
mark_ablov Автор
У MachXO2 слишком мало памяти (нужно 64Кб, а не 64 кбит). MAX-10 тоже смотрел - вполне укладывался в критерии, но не заметил особой разницы с Microsemi/Microchip, поэтому взял подошевле (подходящий вариант MAX10 что-то порядка 50 баксов, вместо 15 за Igloo2).
Flammmable
Взвешенный подход и хорошая статья :)
Спасибо!