Насколько быстр Intel 8080? Используем чипсет на FPGA чтоб проверить

от автора

Введение

Я люблю вызовы — например, написать код в условиях ограниченных ресурсов: медленный процессор, странный набор инструкций, крохи памяти. У меня уже было несколько проектов такого рода — я запускал тяжелую вычислительную задачу на процессорах, которые уже разменяли пол-века: 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. Именно здесь происходит вся жара. Выбор ПЛИС диктовал среду разработки, поэтому пришлось адаптироваться. Итеративно я составил такую схему компонента верхнего уровня.

Чудесная диаграма из Libero SoC

Чудесная диаграма из Libero SoC

Голубенький прямоугольник это как раз подсистема встроенной 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 попугаев. К сожалению, никто не портировал этот бенчмарк на самые слабые процессоры, так что скорее всего это худший результат в истории.

Заключение и ссылки

А как же моё любимое число π? Конечно же, я опять написал код для его вычисления! В этот раз с использованием алгоритма Чудновского и «быстрым» вычислением квадратного корня для длинной арифметики (не, это не метод Ньютона). Но это тема для другой статьи — более «математической».

Весь исходный код разбит на три репозитория:

  1. https://github.com/quasiengineer/i8080-sbc — схема платы, gerber-файлы, прошивки для stm32/fpga и управляющая программа для ПК.

  2. https://github.com/quasiengineer/i8080-emulator — эмулятор для 8080

  3. https://github.com/quasiengineer/i8080-benchmarks — набор программ, написанных для 8080


ссылка на оригинал статьи https://habr.com/ru/articles/917156/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *