Arduino ZX Spectrum USB Keyboard - znoxx/ARCAdaptor GitHub Wiki

Скетч для Arduino, позволяющий подключить стандартную клавиатуру ZX-Spectrum (40 клавиш) к USB-порту с последующим использованием в эмуляторах, в частности Unreal.

Стандартная клавиатура ZX-Spectrum представляет собой матрицу кнопок 8x5.

Источник картинки - Издание "Абзац"

8 сигналов идет с шины адреса A8-A15 через диоды для исключения перегрузок при замыкании нескольких кнопок. На один из 8 адресов подается 0, на остальные 1 и считывается 5 битов данных (пол-ряда из 10 кнопок). Затем 0 подается на следующий адрес, на остальные 1. Считывается состояние следующих 5 кнопок. Так сканируют все 8 полурядов.

ARCAdaptor обладает нужным количеством контактов, так что будем подключать. Внутренние резисторы-"подтяжки" есть в самой ATMeg8(a), равно, как и защитные диоды.

Для начала изучим наш экземпляр клавиатуры, так как маркировка на ней со стороны контактов частично не читаема. Для удобства будем именовать "шину данных" D0-D4, а "шину адреса" - A0-A7.

Вот, что получилось после "прозвонки" клавиатуры:

Можно писать "скетч" для Arduino, тем более, что библиотека работы с USB-клавиатурой доступна доступна.

Часть первая - объявляем библиотеки, помимо той, что работает с USB потребуется еще определения контактов ARCAdaptor (GitHub) и Debounce - библиотека для устранения дребезга контактов.

#include "UsbKeyboard.h"
#include "ARCAdaptor.h"
#include "Bounce2.h" /*http://playground.arduino.cc/code/bounce*/

#define DATA0 X1_1
#define DATA1 X1_3
#define DATA2 X1_5
#define DATA3 X1_7
#define DATA4 X1_9

#define ADDRESS0 X2_1
#define ADDRESS1 X2_3
#define ADDRESS2 X2_5
#define ADDRESS3 X2_7
#define ADDRESS4 X2_9
#define ADDRESS5 X2_11
#define ADDRESS6 X2_13
#define ADDRESS7 X2_15

byte addressBus[]={
  ADDRESS0,
  ADDRESS1,
  ADDRESS2,
  ADDRESS3,
  ADDRESS4,
  ADDRESS5,
  ADDRESS6,
  ADDRESS7
     };
byte dataBus[]={
  DATA0,
  DATA1,
  DATA2,
  DATA3,
  DATA4
};

Часть вторая - определяем массивы буффера USB, опроса клавиатуры и устанавливаем пины в нужное состояние. "Карта" клавиатуры лежит в массиве keybmatrix - фактически туда записаны HID коды. Особое внимание на Caps Shift и Symbol Shift - в Unreal Speccy это Shift и Alt.

//defining keyboard
#define SYMBOL_SHIFT 1
#define CAPS_SHIFT 35
#define KEY_ALT 0xE2
#define KEY_SHIFT 0xE1

//for UNREAL Speccy Portable
//sym shift -> to ALT
//caps shift -> to SHIFT

byte keybmatrix[]={
  KEY_SPACE,KEY_ALT,KEY_M,KEY_N,KEY_B,
  KEY_ENTER,KEY_L,KEY_K,KEY_J,KEY_H,
  KEY_P,KEY_O,KEY_I,KEY_U,KEY_Y,
  KEY_0,KEY_9,KEY_8,KEY_7,KEY_6,
  KEY_1,KEY_2,KEY_3,KEY_4,KEY_5,
  KEY_Q,KEY_W,KEY_E,KEY_R,KEY_T,
  KEY_A,KEY_S,KEY_D,KEY_F,KEY_G,
  KEY_SHIFT,KEY_Z,KEY_X,KEY_C,KEY_V
};
#define KBUFSIZE 40
#define USBBUFFERSIZE BUFFER_SIZE /* UsbKeyboard.h */
#define DEBOUNCER 5

byte usbBuffer[USBBUFFERSIZE];

byte keyBuffer[KBUFSIZE];

bool bIdle=true;

// Instantiate a Bounce object
Bounce debouncer = Bounce(); 

void **setup**() {
//setting analog pins to digital
  // and setting up data bus
  
  pinMode(DATA0,INPUT);
  digitalWrite(DATA0,HIGH);
  pinMode(DATA1,INPUT);
  digitalWrite(DATA1,HIGH);
  pinMode(DATA2,INPUT);
  digitalWrite(DATA2,HIGH);
  pinMode(DATA3,INPUT);
  digitalWrite(DATA3,HIGH);
  pinMode(DATA4,INPUT);
  digitalWrite(DATA4,HIGH);
  //address bus
  pinMode(ADDRESS0, INPUT);
  digitalWrite(ADDRESS0,HIGH);
  pinMode(ADDRESS1, INPUT);
  digitalWrite(ADDRESS1,HIGH);
  pinMode(ADDRESS2, INPUT);
  digitalWrite(ADDRESS2,HIGH);
  pinMode(ADDRESS3, INPUT);
  digitalWrite(ADDRESS3,HIGH);
  pinMode(ADDRESS4, INPUT);
  digitalWrite(ADDRESS4,HIGH);
  pinMode(ADDRESS5, INPUT);
  digitalWrite(ADDRESS5,HIGH);
  pinMode(ADDRESS6, INPUT);
  digitalWrite(ADDRESS6,HIGH);
  pinMode(ADDRESS7, INPUT);
  digitalWrite(ADDRESS7,HIGH);
}

Начинаем основной цикл программы - опрашиваем клавиатуру. При опросе бит шины данных устанавливается в "0" и режим ввода, так что если есть замыкание - он будет выставлен в "1". Этим мы и воспользуемся, не забывая после опроса вернуть линию данных в исходное состояние. А также записываем результат в буфер клавиатуры. В процессе опроса избавляемся от дребезга контактов через объект debouncer.

void **loop**() {
  
  UsbKeyboard.update();

    char addressPointer=0;
  //reading keyboard
  char bufferPtr=0;
  memset(keyBuffer,0,sizeof(keyBuffer));
  memset(usbBuffer,0,sizeof(usbBuffer));
  for(char i=0;ipinMode(addressBus[i], OUTPUT);
    digitalWrite(addressBus[i],LOW); //preparing data
    for (char j=0;jattach(dataBus[j]);
       debouncer.interval(DEBOUNCER);
       debouncer.update();
      char key = debouncer.read();
      
      if (!key)
      {
        keyBuffer[bufferPtr]=keybmatrix[i*5+j];        
      }
      bufferPtr++;
    } //end data bus
   pinMode(addressBus[i], INPUT);
   digitalWrite(addressBus[i],HIGH); //get back
   
  } //end address bus

Обрабатываем результаты опроса клавиатуры - вычисляем модификаторы. Некоторые клавиатуры "умеют" слать скан-коды Alt, Ctrl, Shift и и прочих. В нашем случае мы будем из "занулять" и использовать байт-модификатор.

  bool bPressed=false;
  //calculating modifiers
  bool bCapsShift=false;
  bool bSymbolShift = false;
  byte kModifier = 0;
  if (keyBuffer[SYMBOL_SHIFT])
  {
    bSymbolShift = true;
    kModifier = kModifier | (MOD_ALT_LEFT);
    keyBuffer[SYMBOL_SHIFT]=0;   
  }
  if (keyBuffer[CAPS_SHIFT])
  {
    bCapsShift = true;
    kModifier = kModifier | (MOD_SHIFT_LEFT);
    keyBuffer[CAPS_SHIFT]=0;  
  }

Дальше нужно переслать нажатия в буфер USB-клавиатуры. В нашем случае из-за библиотеки USBKeyboard он "всего лишь" 4 байта - 1 байт модификатора и 3 байта данных. То есть можно обработать 3 одновременных нажатия + одновременное нажатие Caps Shift и Symbol Shift (SHIFT и ALT).

Поэтому мы посылаем только "нажатые кнопки" из первичного буфера, тем более что модификаторы у нас вычислены на предыдущем этапе.

  #define DATA_PTR 1 //starting from second byte

  byte bytePtr = DATA_PTR;

  for(char k=0;k

Но вполне может быть, что нажаты только модификаторы, а в сам буфер ничего не посылается. Обработаем эту ситуацию.

  if (bytePtr == DATA_PTR ) //only modifiers left
  {
     memset(usbBuffer,0,sizeof(usbBuffer));
     if(bCapsShift)
     {
       usbBuffer[1] = KEY_SHIFT;
       bPressed=true;
     }
     if(bSymbolShift)
     {
       usbBuffer[2] = KEY_ALT;
       bPressed=true;
     }
     
     
  }

Ну и в конечном итоге, решаем, что делать. Если ничего не было нажато и в прошлый раз - "пропускаем ход". В другом случае посылаем новые данные в буффер USB. Об остальном позаботится библиотека.

  usbBuffer[0]=kModifier;
  
   
 if (bPressed)
  {
    bIdle = false;
  }
     
  if(!bPressed)
  {
    memset(usbBuffer,0,sizeof(usbBuffer));
    
  }
  
  if (!bIdle)
  {
    UsbKeyboard.sendBuffer(usbBuffer);
    if (!bPressed)
    {
      bIdle = true;
    }
  }

 
}

Код готов для загрузки в устройство (после перекомпиляции).

Исходный код полностью доступен на GitHub.

Необходимо отметить, что библиотека [V-USB для Arduino](Библиотека для работы с V-USB), точнее её "клавиатурный" раздел был изменен - в него был добавлен метод sendBuffer, который посылает не "нажатие" кнопки, а пересылает весь буфер. Это нужно для правильной обработки клавиатуры - имеющийся метод sendKey был не пригоден, ибо сразу за нажатием посылал "отжатие" кнопки, что, фактически, блокировало игровой функционал.