[Перевод] Союз Arduino и классического процессора

ol8ryrltwjene6e2frje_pacnti.jpeg

Ретрокомпьютерщики бывают различной степени привередливости. Одни довольствуются эмуляцией. Другие предпочитают ПЛИС, потому что тогда получается не эмуляция, а воссоздание. Наконец, третьим подавай настоящий процессор.

Но процессору для работы нужно столько всего! Снова дилемма: взять настоящие микросхемы тех же лет, или поместить всё в ПЛИС, оставив снаружи процессор? Впрочем, почему обязательно ПЛИС? Да здравствует союз Arduino и классического процессора!

Подарите своему Arduino «второй мозг» и сделайте его «умнее».

Настоящий восьмибитный микропроцессор выполняет программы, а Arduino эмулирует ПЗУ, ОЗУ и простейшую периферию.

Проектируйте виртуальную периферию в Arduino IDE, а на микропроцессоре запускайте код на ассемблере. Не нужно собирать сложные схемы и прошивать параллельные ПЗУ.

Поддерживаемые микропроцессоры: 6502, 6809 и Z80 (КР1858ВМ1), на подходе — другие.

Шилд с микропроцессором не мешает подключать другие шилды: с ЖКИ, картами памяти, и др.

Помимо самостоятельного программирования на ассемблере, можно попробовать запустить на микропроцессоре какой-нибудь классический код.

Правда, микропроцессор будет работать на очень небольшой частоте — порядка 95 кГц, точное её значение зависит от оптимизации кода эмуляции периферии.

Распределение адресного пространства задаётся программно в скетче. Микропроцессору можно выделить от 4 до 6 кБ ОЗУ из 8 кБ, имеющихся на Arduino Mega. ПЗУ можно выделить более 200 кБ из имеющихся 256.

При помощи последовательного порта Arduino Mega можно эмулировать UART.

Схемы, чертежи плат, Gerber-файлы доступны под CC-BY-SA 4.0 здесь. При этом имеется требование обязательно прикладывать файл README.md, потому что в нём содержится следующее предупреждение:

Не подключайте шилд, пока не залит скетч эмуляции периферии! Иначе возможно закорачивание выходных линий микропроцессора.

Да и в самом скетче что-нибудь переделывать нужно осторожно по той же причине.

Схема устройства на 6502:

e0n86kpozlcttklifjihnl51quo.png

Схема устройства на 6809:

qfyikx1tktdjuad0hocivlpsolw.png

Схема устройства на Z80:

ctuhqskjmmp1rvxrhs8abcoqvne.png

Уже можно запустить:

На устройстве с 6502 — Apple I, Woz Monitor + ПЗУ с Бейсиком

На устройстве с 6809 — Аналог самодельного компьютера Simon6809 того же разработчика, учебный монитор с ассемблером и дизассемблером

На устройстве с Z80 — пока только эхо-тест последовательного порта, позволяющий проверить работоспособность виртуального 8251 (КР580ВВ51А).

Прошивки для эмуляции периферии — под лицензией MIT.

Краткие описания принципа действия:

К устройству на 6502

К устройству на 6809

К устройству на Z80 — в процессе подготовки.

Разработчик пытается продавать устройства, но с доставкой только по США. Особого смысла покупать нет, поскольку схема очень простая, повторить её на куске макетки можно за час.

Запланирована разработка аналогичных плат на RCA1802, 68008, 8085 (КР1821ВМ85А), 8088 (КР1810ВМ88). Про К1801ВМ1 не сказано, но можно подкинуть автору такую идею.

Файлы:

К устройству на 6502: инструкция по сборке, шелкография, схема

К устройству на 6809: инструкция по сборке, шелкография, схема

К устройству на Z80: инструкция по сборке, шелкография, схема

Рассмотрим взаимодействие Arduino и устройства на 6502. Arduino периодически меняет уровень на входе микропроцессора, предназначенном для подачи тактовых импульсов, с нуля на единицу и обратно. На каждом такте оно проверяет, что происходит на линиях управления и шине адреса, и, в зависимости от ситуации, считывает информацию с шины данных или отправляет её туда. Arduino может также управлять линиями IRQ и NMI, вызывая прерывания. На рисунке показаны виды данных и направления их передачи:

trinjeibi9cwwrngfq628lm4yns.png

Соответствие портов Arduino и выводов микропроцессора сконфигурировано в скетче:

/* Digital Pin Assignments */
#define DATA_OUT    PORTL
#define DATA_IN     PINL
#define ADDR_H      PINC
#define ADDR_L      PINA
#define ADDR        ((unsigned int) (ADDR_H << 8 | ADDR_L))

#define uP_RESET_N  38
#define uP_RW_N     40
#define uP_RDY      39
#define uP_SO_N     41
#define uP_IRQ_N    50
#define uP_NMI_N    51
#define uP_E        52
#define uP_GPIO     53

Разобьём каждый такт на следующие события:

CLK меняет состояние с единицы на нуль (спад)
CLK находится в состоянии нуля
CLK меняет состояние с единицы на нуль (нарастание)
CLK находится в состоянии единицы
CLK снова меняет состояние с единицы на нуль…

Что происходит в моменты смены состояний?

6502 получает тактовые импульсы по входу CLK0, буферизует их и отправляет на два выхода: CLK1 и CLK2. Хотя в микропроцессоре все события привязаны к CLK1, будем считать, что задержка невелика, и они привязаны к CLK0 — той линии, по которой здесь микропроцессор получает тактовые импульсы из Arduino. И называть сигнал просто CLK.

9ikqhcytwd4sjg6rwjnvmohgkvi.png

1. CLK меняет состояние с единицы на нуль.

2. Микропроцессор выводит на шину адреса новый адрес, а на выход R/W — сигнал переключения между чтением и записью. Но он ещё не готов к обмену данными.

3. CLK переходит в состояние единицы, и это означает, что обмен данными начался. Если это операция чтения, микропроцессор переводит выводы шины данных в состояние входов и принимает по рим данные, а если операция записи — переводит их в состояние выходов и отправляет данные. А сигнал R/W переключает внешнее устройство в режим записи или чтения, противоположный соответствующему состоянию микропроцессора.

4. CLK переходит в состояние нуля. Теперь на шину даннных ничего не выводят ни микропроцессор, ни устройства ввода-вывода. Микропроцессор может установить в новое состояние линии шины данных и вывод R/W.

Простое объяснение, понятное и ребёнку. Который никогда и не задумается об этих «закулисных интригах», если будет программировать только микроконтроллеры. Даже на ассемблере.

Если нужно подключить своё периферийное устройство, оно должно успевать подготавливать данные до того, как на линии CLK появится единица (время подготовки), а пока там единица — не менять их. Если периферийное устройство не успеет подготовить данные, пока на CLK нуль, или поменяет их, когда там единица, вы будете долго недоумевать, почему ваш код не работает. Поскольку тактовая частота микропроцессора здесь в десять-пятнадцать раз ниже номинальной, соблюсти это требование просто. Но нужно обязательно.

Итак, нужно «научить» Arduino генерировать тактовые импульсы, непрерывно проверяя, что при этом происходит на шине адреса и линии R/W, и соответствующим образом взаимодействуя с шиной данных. Для этого в скетче задействовано прерывание по таймеру timer1, вырабатывающему импульсы с частотой в 95 кГц. Arduino работает значительно быстрее микропроцессора, и потому между его тактами успевает всё и считывать, и подготавливать. Важно проследить, чтобы после модификации скетча это условие продолжало соблюдаться.

Вот выдержка из скетча, по которой понятно, как CLK переходит из нуля в единицу, и что происходит далее:

////////////////////////////////////////////////////////////////////
// Processor Control Loop
////////////////////////////////////////////////////////////////////
// This is where the action is.
// it reads processor control signals and acts accordingly.
//
ISR(TIMER1_COMPA_vect)
{ 
  // Drive CLK high
  CLK_E_HIGH;
  
  // Let's capture the ADDR bus
  uP_ADDR = ADDR;
    
  if (STATE_RW_N)     
  //////////////////////////////////////////////////////////////////
  // HIGH = READ transaction
  {
    // uP wants to read so Arduino to drive databus to uP:
    DATA_DIR = DIR_OUT;

    // Check what device uP_ADDR corresponds to:

    // ROM?
    if ( (ROM_START <= uP_ADDR) && (uP_ADDR <= ROM_END) )
      DATA_OUT = pgm_read_byte_near(rom_bin + (uP_ADDR - ROM_START));
    else
    if ( (BASIC_START <= uP_ADDR) && (uP_ADDR <= BASIC_END) )
      DATA_OUT = pgm_read_byte_near(basic_bin + (uP_ADDR - BASIC_START));
    else
    // RAM?
    if ( (uP_ADDR <= RAM_END) && (RAM_START <= uP_ADDR) )
      DATA_OUT = RAM[uP_ADDR - RAM_START];
    else
    // 6821?
    if ( KBD <=uP_ADDR && uP_ADDR <= DSPCR )   
    {      
      // KBD?
      if (uP_ADDR == KBD)
      {
        ... // handle KBD register
      }
      else
      // KBDCR?
      if (uP_ADDR == KBDCR)
      {
        ... // handle KBDCR register
      }
      else
      // DSP?
      if (uP_ADDR == DSP)
      {
        ... // handle DSP register
      }
      else
      // DSPCR?
      if (uP_ADDR == DSPCR)
      {
        ... // handle DSPCR register
      }   
      
    }
  } 
  else 
  //////////////////////////////////////////////////////////////////
  // R/W = LOW = WRITE
  {
    // RAM?
    if ( (uP_ADDR <= RAM_END) && (RAM_START <= uP_ADDR) )
      RAM[uP_ADDR - RAM_START] = DATA_IN;
    else
    // 6821?
    if ( KBD <=uP_ADDR && uP_ADDR <= DSPCR )
    {
      // KBD?
      if (uP_ADDR == KBD)
      {
        ... // handle KBD register
      }
      else
      // KBDCR?
      if (uP_ADDR == KBDCR)
      {
        ... // handle KBDCR register
      }
      else
      // DSP?
      if (uP_ADDR == DSP)
      {
        ... // handle DSP register
      }
      else
      // DSPCR?
      if (uP_ADDR == DSPCR)
      {
        ... // handle DSPCR register
      }
    }
  }

  ////////////////////////////////////////////////////////////////
  // We are done with this cycle.

  // one full cycle complete
  clock_cycle_count ++;

  // start next cycle
  CLK_E_LOW;

  // If Arduino was driving the bus, no need anymore.
  // natural delay for DATA Hold time after CLK goes low (t_HR)
  DATA_DIR = DIR_IN;  
}

Распределение адресного пространства можно сделать каким угодно, в немодифицированном скетче оно такое же, как в Apple 1 с 256 байтами ПЗУ, 8 килобайтами ПЗУ для Бейсика, 4 килобайтами ОЗУ и устройством ввода-вывода 6821.

// MEMORY LAYOUT
// 4K MEMORY
#define RAM_START   0x0000
#define RAM_END     0x0FFF
byte    RAM[RAM_END-RAM_START+1];

// ROMs (Monitor + Basic)
#define ROM_START   0xFF00
#define ROM_END     0xFFFF
#define BASIC_START 0xE000
#define BASIC_END   0xEFFF

////////////////////////////////////////////////////////////////////
// Woz Monitor Code
////////////////////////////////////////////////////////////////////
//
PROGMEM const unsigned char rom_bin[] = {
    0xd8, 0x58, 0xa0, 0x7f, 0x8c, 0x12, 0xd0, 0xa9, 0xa7, 0x8d, 0x11, 0xd0, 
    ...
    0x00, 0xff, 0x00, 0x00
};


// BASIC ROM starts at E000
PROGMEM const unsigned char basic_bin[] = { 
    0x4C, 0xB0, 0xE2, 0xAD, 0x11, 0xD0, 0x10, 0xFB,
    ...
    0xE0, 0x80, 0xD0, 0x01, 0x88, 0x4C, 0x0C, 0xE0
};

ОЗУ эмулируется массивом byte RAM[RAM_END-RAM_START+1]. Два ключевых слова PROGMEM нужны, чтобы содержимое эмулируемых ПЗУ хранилось во флеш-памяти микроконтроллера.

6821 эмулирован в достаточной мере, чтобы через «терминалку» работали виртуальные клавиатура и дисплей. Woz Monitor и Бейсик работают, чего и добивался автор.

Чтобы эмулировать любое периферийное устройство, нужно внимательно ознакомиться с его даташитом и выяснить, какие у него есть регистры, и для чего они предназначены. Удобство эмуляции — в гибкости, с которой можно делать программные аналоги периферии.

Устройства ввода-вывода находятся в адресном пространестве микропроцессора, обращение к ним происходит так же, как к ячейкам памяти. Чтобы использовать «железную» периферию, такую, как ЖК-дисплей, карту памяти, выход звука, нужно выделить им в адресном пространстве место.

Ссылки:

www.6502.org
www.callapple.org/soft/ap1/emul.html
skilldrick.github.io/easy6502
searle.hostei.com/grant/6502/Simple6502.html
wilsonminesco.com/6502primer
SB-Assembler: www.sbprojects.net/sbasm

Переходим к 6809, в нём имеются:

Два восьмибитных аккумулятора A и B, которые могут быть объединены в один шестрадцатиразрядный аккумулятор
Два 16-битных индексных указателя стека
Адресация относительно счётчика команд
Автоматическое прибавление или вычитание числа 1 или 2
Перемножение двух восьмиразрядных чисел без знака
16-битная арифметика
Перенос и обмен данными между всеми регистрами
Запись и чтение всех регистров и любого их сочетания

Микропроцессору 6809E (external) нужен внешний тактовый генератор, у 6809 он внутренний. У Hitachi они называются, соответственно, 6309E и 6309, от обычных они отличаются тем, что внутри операции у них выполняются в 32-разрядном виде, но возможно переключение в режим совместимости с классическим вариантом.

Собственно, весь проект RetroShield потому и начался, что автор хотел модернизировать свой же самодельный компьютер Simon6809 и назвать результат Simon6809 Turbo. Но оказалось, что микросхем стандартной логики для всего, что он хотел там реализовать, потребовалось бы очень много. Поэтому идею RetroShield автор впервые сформулировал именно применительно к 6809, и лишь затем подумал: «а что если и с другими процессорами то же проделать?».

В устройстве, разумеется, применён 6809E, требующий внешнего тактового генератора, чтобы можно было синхронизировать его работу извне. Линии E и Q у обоих процессоров называются одинаково, только у 6809 это выходы, а у 6809E — входы.

С 6809 Arduino взаимодействует так же, как с 6502, но входов тактовой частоты у него два: E и Q, а входов для прерываний — три: IRQ, FIRQ и NMI.

0iukdn25vkrhbsvd8spjlhr-s3q.png

В этот раз соответствие портов Arduino и выводов микропроцессора сконфигурировано так:

/* Digital Pin Assignments */
#define DATA_OUT PORTL
#define DATA_IN  PINL
#define ADDR_H   PINC
#define ADDR_L   PINA
#define ADDR     ((unsigned int) (ADDR_H << 8 | ADDR_L))

#define uP_RESET_N  38
#define uP_E        52
#define uP_Q        53
#define uP_RW_N     40
#define uP_FIRQ_N   41
#define uP_IRQ_N    50
#define uP_NMI_N    51
#define uP_GPIO     39

Как видно из графиков, сигнал Q сдвинут относительно E на четверть периода:

Обращать внимание на Q мы почти не будем, так как все события привязаны к E. А происходит всё так:

aohjqvkk89mbouy7_njtjqbz3ku.png

1. E переключается в нуль. Процессор выставляет на шину адреса новый адрес и меняет состояние линии R/W.

2. E переключается в единицу, процессор становится готов к обмену данными.

3. Неважно, что происходит с шиной данных, пока на E единица, главное, чтобы требуемые данные там присутствовали в момент перехода E обратно в нуль.

4. При чтении данных устройство ввода-вывод должно подать на шину данных требуемые данные до перехода линии E из единицы в нуль (минимальная задержка показана числом 17 в кружке).

5. При записи устройство ввода-вывода должно зафиксировать данные у себя в каком-нибудь регистре в том виде, в каком они были в момент перехода E из единицы в нуль. Процессор же подаст эти данные на шинв даже раньше — в момент перехода Q в единицу (число 20 в кружке).

6. После перехода E в нуль всё повторяется.

Всё сказанное выше про 6502 о необходимости выработки периферийным устройством (в т.ч. виртуальным) всех сигналов вовремя касается и 6809.

Генерация сигналов E и Q, как и в случае с 6502, с той лишь разницей, что сигналов два, и переключать их надо в соответствии с графиками. И точно так же подпрограмма, вызываемая по прерыванию, выполняет в требуемые моменты ввод или вывод данных.

Адресное пространство в немодифицированном скетче распределено так же, как в самодельном компьютере Simon6809:

// MEMORY
#define RAM_START   0x0000
#define RAM_END     0x0FFF
#define ROM_START   0xE000
#define ROM_END     0xFFFF
byte RAM[RAM_END-RAM_START+1];


////////////////////////////////////////////////////////////////////
// Monitor Code
////////////////////////////////////////////////////////////////////
// static const unsigned char 
PROGMEM const unsigned char simon09_bin[] = {
    0x1a, 0xff, 0x4f, 0x1f, 0x8b, 0x0f, 0x36, 0x7f, 0x01, 0xa5, 0x10, 0xce, 
    ...
    0x00, 0x09, 0x00, 0x0c, 0x00, 0x0f, 0xe0, 0x00
};

ОЗУ и ПЗУ хранятся в массивах так же как в варианте на 6502, с той лишь разницей, что здесь массив с данными ПЗУ — один.

Устройствам ввода-вывода здесь также выделены участки адресного пространства, и они могут быть как виртуальными, так и реальными. Так как Simon6809 — современная машина на винтажной элементной базе, с ПК, на котором запущена «терминалка», она обменивается данными через FTDI. Здесь эмулировано и это.

Ссылки:

Много информации по 6809 на Странице Arto
Статья в Википедии о 6809
SWTPc 6809 systems
Статья в Википедии об операционной системе FLEX

© Habrahabr.ru