Wednesday, 4 November 2015

Some More Arduino Code for the ZX81 Keyboard

I've made  a fair number of changes to the ZX81 keyboard Arduino sketch. The most noticeable being that the code is now broken into multiple files / libraries; all feeling a little more C++ like. Of course real microcontroller programmers use C, but we wont discus that here.

In general the keyboard functions as earlier. The main physically tangible improvement being that the keyboard performs a whole lot better in ZX81 emulators. It's not perfect and it's definitely not ideal for playing real time games with, though It works as advertised when typing normally, ie. using the keyboard to program with.

Emulator comparability wise, the keyboard will work with  sz81 and ZEsarUX. I'd recommend using sz81, as the keyboard still functions best with that particular emulator. I've also had a go at using Fuse with the keyboard, and to the extent that a ZX81 keyboard can be used on ZX Spectrum it functions okay, though it is missing those couple of crucial extra keys for proper Spectrum emulation.

When using the keyboard in standard mode as a regular USB input device in Linux or with a Raspberry Pi there are no issues, unfortunately, as with the emulators, games that require SDL libraries suffer from the "keys not being registered when you'd like them to be" syndrome. This issue I'm afraid may be down to problems with the Arduino USB Keyboard library and beyond my immediate control.

Presented For Your Amusement, Code Caught in the Wild


Main Program (zx81usbkeyboard.ino)

As the name on the implies, here be the general program.


// **************************************************************************
// **** ZX81 USB Keyboard for Funtronics LeoStick (based on a Leonardo). ****
// **** zx81usbkeyboard.ino                                              ****
// **************************************************************************
// ** David Stephenson 2015-11-03  **
// **                              **
// ** Originally based on code by: **
// ** Dave Curran  2013-04-27      **
// ** Tony Smith 2014-02-15        **
// **********************************

 #include "Arduino.h"
 #include "zx81keyboard.h"
 #include "zx81modestate.h"
 
// ************************
// *** Global Variables ***
// ************************

// Setup Global Variables for keyboard.
ZxKeyBoard MyKeyboard;

// Setup LeoStick pins for mode switch and indicator LEDs.
ModeState MyModeState(A5,A0,A1,A2);

// Setup LeoStick pins for Keyboard rows and columns.
const byte bColPins[NUM_COLS] = {13,12,10,9,8};
const byte bRowPins[NUM_ROWS] = {7,6,5,4,3,2,1,0};


// ******************
// *** Main Setup ***
// ******************
void setup() {
  
 // Set all Keyboard pins as inputs and activate pull-ups.
 for (byte bColCount = 0 ; bColCount < NUM_COLS ; bColCount++)
 {
  pinMode(bColPins[bColCount], INPUT);
  digitalWrite(bColPins[bColCount], HIGH);
 }
 
 // Set all Keyboard pins as inputs.
 for (byte bRowCount = 0 ; bRowCount < NUM_ROWS ; bRowCount++)
 {
  pinMode(bRowPins[bRowCount], INPUT);
 }
 
 // initialize control over the keyboard.
 //Serial.begin(9600);
 Keyboard.begin();
 
}
 
// ************************
// *** Main loop        ***
// *** Lets get Busy-ah ***
//*************************
void loop() {

 bool boShifted = false;
 byte bKeyPressed = 0;
 
 // Set Keyboard Mode.
 KEYMODES eMyKeyMode = MyModeState.GetMode();
 KEYSTATES eMyKeyState = MyModeState.GetState();
 
 // Check for the Shift key being pressed.
 pinMode(bRowPins[SHIFT_ROW], OUTPUT);
 
 if (digitalRead(bColPins[SHIFT_COL]) == LOW) boShifted = true;
 
 pinMode(bRowPins[SHIFT_ROW], INPUT);
 
 for (byte bRow = 0 ; bRow < NUM_ROWS ; bRow++){
  // Run through the rows, turn them on.
  
  pinMode(bRowPins[bRow], OUTPUT);
  digitalWrite(bRowPins[bRow], LOW);
  
  for (byte bCol = 0 ; bCol < NUM_COLS ; bCol++){
   if (digitalRead(bColPins[bCol]) == LOW){
    if (boShifted && eMyKeyMode == STANDARD){
     // Select correct Keyboard layout for current state For STANDARD mode. 
     // Shift alters the Current state selection. eg. gets Red Shift symbols in Normal State. 
     switch (eMyKeyState) {
      case NORMAL:
       bKeyPressed = MyKeyboard.bKeyPress(bRow, bCol, NORMAL_SHIFTED, eMyKeyMode);
       if (bKeyPressed == KEY_RETURN) { 
        eMyKeyState = FUNCTION;
        bKeyPressed = 0;
        MyKeyboard.KeyDisable(bRow, bCol);
       }
       if (bKeyPressed == '9') {
        eMyKeyState = GRAPHICS;
        bKeyPressed = 0;
        MyKeyboard.KeyDisable(bRow, bCol);
       }
      break;
       
      case FUNCTION:
       bKeyPressed = MyKeyboard.bKeyPress(bRow, bCol, NORMAL_SHIFTED, eMyKeyMode);
       if (bKeyPressed == KEY_RETURN) { 
        eMyKeyState = NORMAL;
        bKeyPressed = 0;
        MyKeyboard.KeyDisable(bRow, bCol);
       }
       if (bKeyPressed == '9') {
        eMyKeyState = GRAPHICS;
        bKeyPressed = 0;
        MyKeyboard.KeyDisable(bRow, bCol);
       }
      break;
       
      case GRAPHICS:
       bKeyPressed = MyKeyboard.bKeyPress(bRow, bCol, GRAPHICS_SHIFTED, eMyKeyMode);
       if (bKeyPressed == KEY_RETURN) { 
        eMyKeyState = FUNCTION;
        bKeyPressed = 0;
        MyKeyboard.KeyDisable(bRow, bCol);
       }
       if (bKeyPressed == '9') {
        eMyKeyState = NORMAL;
        bKeyPressed = 0;
        MyKeyboard.KeyDisable(bRow, bCol);
       }
      break;
       
     }
    } else {
     // Emulator keyboard, the keyboard states are controlled by a ZX81 emulator.
     // Keyboard mimics unaltered PS2 Keyboard presses as expected by an emulator.
     bKeyPressed = MyKeyboard.bKeyPress(bRow, bCol, eMyKeyState, eMyKeyMode);
    }
    
    if (bKeyPressed > 0 ) {
     //Serial.write(bKeyPressed);
     if (eMyKeyMode == EMULATOR){
      if (bKeyPressed && boShifted) Keyboard.press(KEY_LEFT_SHIFT);
      Keyboard.press(bKeyPressed);
      // Many Emulators don't use standard OS keyboard routines and require a pause
      // for key to register. There is some variance, but 100ms seems to cover most.  
      delay(DEBOUNCE_DELAY/2);
     } else {
      if (eMyKeyState == GRAPHICS && bKeyPressed > 96 && bKeyPressed < 123){
       if (boShifted){
        Keyboard.press(KEY_LEFT_ALT);
       } else {
        Keyboard.press(KEY_LEFT_CTRL);
       }
      }
      Keyboard.press(bKeyPressed);
      // Some audio feedback if not in EMULATOR mode.
      // Kill the line if you hate beepy beep beeps.
      tone(11, 31, 20);
     }
     
     Keyboard.releaseAll();
    }
    
   } else {
    MyKeyboard.KeyPressReset(bRow, bCol);
   }
  }
  pinMode(bRowPins[bRow], INPUT);
 }
 
 digitalWrite(bRowPins[SHIFT_ROW], LOW);
 
 // Update LED panel and check for Mode change switch press.
 MyModeState.SetState(eMyKeyState);
 
 }

Keyboard Class (zx81keyboard.h)

Main guts of the keyboard setup and initial handling is in here.


// ***********************
// ** zx81keyboard.h    **
// ** Class Definitions **
// ** For ZX81 keyboard **
// ***********************

enum KEYMODES {EMULATOR, STANDARD};
enum KEYSTATES {NORMAL, NORMAL_SHIFTED, GRAPHICS, GRAPHICS_SHIFTED, FUNCTION};

#define NUM_ROWS 8
#define NUM_COLS 5 

#define SHIFT_COL 4
#define SHIFT_ROW 5
//#define FUNCTION_COL 4
//#define FUNCTION_ROW 6
//#define GRAPHICS_COL 3
//#define GRAPHICS_ROW 2

#define DEBOUNCE_DELAY 200 //debounce countdown value.
#define DEBOUNCE_DELAY_REPEAT 600

// Defines a single key and its 4 possible Major KEYSTATES: NORMAL, NORMAL_SHIFTED, GRAPHICS, GRAPHICS_SHIFTED
// Returns 5 KEYSTATES, adds FUNCTION: Caps Lock).
class ZxKey  {
 private:
  // Standard Keyboard Characters for Normal PC Use
  byte bNormal;   // Standard ZX keyboard only in lower case. EMULATOR KEYMODE should use bNormal only.
  // Extra Modes for using keyboard as a normal(ish) PS2 / USB device.
  byte bNormalShifted; // Red Symbols & Words (replaced by Symbols)  
  byte bGraphics;   // Standard ZX keyboard but should be used with CTL characters. Numbers = F1 - F10 etc
  byte bGraphicsShifted; // Standard ZX keyboard but should be used with ALT characters. Numbers = F11 - F12, 5 = home, 6 PGUP etc 
  
  // Monitor Keys last press / activity.
  short iDebounceCount = DEBOUNCE_DELAY;
  short iDebounceCountHighest = iDebounceCount;

  byte KeyValue(KEYSTATES eKeystate) {
   // Get Keyboard character for correct Key state.
   byte bKeyValue = 0;

   switch (eKeystate) {
    case FUNCTION:
     bKeyValue = bNormal;
     // Adjust for Capital Letters
     if (bKeyValue > 96 && bKeyValue < 123){
      bKeyValue = bKeyValue - 32;
     }
     break;
     
    case NORMAL_SHIFTED:
     bKeyValue = bNormalShifted;
     break;
     
    case GRAPHICS:
     bKeyValue = bGraphics;
     break;
     
    case GRAPHICS_SHIFTED:
     bKeyValue = bGraphicsShifted;
     break;
     
    default:
     // Standard normal and Emulator mode keyboard.
     // Shift key modifier used later to select functions etc in a ZX81 Emulator.
     bKeyValue = bNormal;
     break;
   }
   return bKeyValue;
  }
  
 public:
  ZxKey (byte, byte, byte, byte);
   
  byte KeyPressDetected(KEYSTATES eKeystate, KEYMODES eKeymode){
   byte bKeyValue = 0;
   iDebounceCount--;
   if (iDebounceCount == 0){
    switch (iDebounceCountHighest) {
     case DEBOUNCE_DELAY:
      iDebounceCount = DEBOUNCE_DELAY_REPEAT;
      iDebounceCountHighest = iDebounceCount;
     break;
     
     case DEBOUNCE_DELAY_REPEAT:
      if (eKeymode == EMULATOR){
       iDebounceCount = DEBOUNCE_DELAY;
      } else {
       iDebounceCount = DEBOUNCE_DELAY / 2;
      }
     break;
     
     default:
      iDebounceCount = DEBOUNCE_DELAY;
     break;
    }
    bKeyValue = KeyValue(eKeystate);
   }
   return bKeyValue;
  }
  
  void KeyPressReset(){
   iDebounceCount = DEBOUNCE_DELAY;
   iDebounceCountHighest = iDebounceCount;
  }
 
  void KeyDisable(){
   iDebounceCount = -1;
  }
};


// ZxKey constructor
ZxKey::ZxKey (byte Normal, byte NormalShifted, byte Graphics, byte GraphicsShifted){
 bNormal = Normal;
 bNormalShifted = NormalShifted;
 bGraphics = Graphics;
 bGraphicsShifted = GraphicsShifted;
}


// Defines entire keyboard, includes ZxKey class.
class ZxKeyBoard {
 private:
  // Setup 4 versions of keyboard states into keyboard.
  // Keyboard mapped for US, might need changing for other configurations eg. UK keyboard.
  ZxKey keyMap[NUM_ROWS][NUM_COLS] = 
  {
   {{'5',KEY_LEFT_ARROW,KEY_F5,KEY_HOME},{'4','%',KEY_F4,KEY_INSERT},{'3','#',KEY_F3,KEY_ESC},{'2','@',KEY_F2,KEY_F12},{'1','!',KEY_F1,KEY_F11}},
   {{'t','_','t','t'},{'r','&','r','r'},{'e','^','e','e'},{'w','`','w','w'},{'q','~','q','q'}},
   {{'6',KEY_DOWN_ARROW,KEY_F6,KEY_PAGE_DOWN},{'7',KEY_UP_ARROW,KEY_F7,KEY_PAGE_UP},{'8',KEY_RIGHT_ARROW,KEY_F8,KEY_END},{'9','9',KEY_F9,'9'},{'0',KEY_BACKSPACE,KEY_F10,'0'}},
   {{'g','\\','g','g'},{'f','}','f','f'},{'d','{','d','d'},{'s',']','s','s'},{'a','[','a','a'}},
   {{'y','|','y','y'},{'u','$','u','u'},{'i','(','i','i'},{'o',')','o','o'},{'p','"','p','p'}},
   {{'v','/','v','v'},{'c','?','c','c'},{'x',';','x','x'},{'z',':','z','z'},{0,0,0,0}},
   {{'h','\'','h','h'},{'j','-','j','j'},{'k','+','k','k'},{'l','=','l','l'},{KEY_RETURN,KEY_RETURN,KEY_RETURN,KEY_RETURN}},
   {{'b','*','b','b'},{'n','<','n','n'},{'m','>','m','m'},{'.',',','.','.'},{' ',KEY_TAB,'£',' '}}
  };
  
 public:
  // Return from ZxKey matching specified state.
  byte bKeyPress(byte bRow, byte bCol, KEYSTATES eKeystate, KEYMODES eKeymode){
   return keyMap[bRow][bCol].KeyPressDetected(eKeystate, eKeymode);
  }
  
  void KeyPressReset(byte bRow, byte bCol){;
   keyMap[bRow][bCol].KeyPressReset();
  }
  
  void KeyDisable(byte bRow, byte bCol){;
   keyMap[bRow][bCol].KeyDisable();
  }
};

Mode Switch & LED Class (zx81modestate.h)

This ones still a work in progress, I'm wanting to add some more functionality to the Mode selection button at some point. Works well enough for the moment.


// *****************************
// ** zx81modestate.h         **
// ** Class Definitions       **
// ** For Mode Switch  & LEDs **
// *****************************

// Defines LED / Mode switch panel and its behaviour.
// Three LEDs are configured to report on keyboard mode and states.
// 
// In STANDARD mode:
//  Left LED on = GRAPHICS state.
//  Middle LED on = FUNCTION state.
//  Right LED on = NORMAL state.
//
// In EMULATOR mode:
//  Left and Right LEDs = on.

class ModeState {
private:
 KEYMODES eKeyboardMode = STANDARD;
 KEYSTATES eKeyboardState = NORMAL;
 byte bModeSwitchPin;
 byte bGraphicsPin;
 byte bFunctionPin;
 byte bModePin;
 
 bool boDebounce = false;
 
 byte SetMode(){
  if (digitalRead(bModeSwitchPin) == HIGH && boDebounce == false) {
   //Serial.println("high ");
   if (eKeyboardMode == STANDARD){
    eKeyboardMode = EMULATOR;
   } else {
    eKeyboardMode = STANDARD;
   }
   
   delay(DEBOUNCE_DELAY_REPEAT);
   boDebounce = true;
  } else if (digitalRead(bModeSwitchPin) == LOW) {
   boDebounce = false;
  }
 }
 
public:
 ModeState (byte, byte, byte, byte);
 
 byte SetState(KEYSTATES eKeystate){
  
  eKeyboardState = eKeystate;
  
  // Check and set mode if Mode switch is pressed
  SetMode();
  
  if (eKeyboardMode != EMULATOR){
   switch (eKeyboardState) {
    
    case FUNCTION:
     digitalWrite(bGraphicsPin, LOW);
     digitalWrite(bFunctionPin, HIGH);
     digitalWrite(bModePin, LOW);
     break;
     
    case GRAPHICS:
     digitalWrite(bGraphicsPin, HIGH);
     digitalWrite(bFunctionPin, LOW);
     digitalWrite(bModePin, LOW);
     break;
     
    default:
     digitalWrite(bGraphicsPin, LOW);
     digitalWrite(bFunctionPin, LOW);
     digitalWrite(bModePin, HIGH);
     eKeyboardState = NORMAL;
     break;
   }
  } else {
   digitalWrite(bGraphicsPin, HIGH);
   digitalWrite(bFunctionPin, LOW);
   digitalWrite(bModePin, HIGH);
   eKeyboardState = NORMAL;
  }
 }
 
 KEYMODES GetMode(){
  return eKeyboardMode;
 }
 
 KEYSTATES GetState(){
  return eKeyboardState;
 }
};

// ModeState constructor.
ModeState::ModeState (byte ModeSwitchPin, byte GraphicsPin, byte FunctionPin, byte ModePin){
 
 bModeSwitchPin = ModeSwitchPin;
 bGraphicsPin = GraphicsPin;
 bFunctionPin = FunctionPin;
 bModePin = ModePin;
 
 pinMode(bModeSwitchPin, INPUT);
 pinMode(bGraphicsPin, OUTPUT);
 pinMode(bFunctionPin, OUTPUT);
 pinMode(bModePin, OUTPUT);
 
}