Sunday, 22 March 2020

Redirecting keystrokes from Linux to the C64 module: Part 2

Foreword

In the previous post we have started the process of capturing keystrokes in Linux and rediecting them to out C64 module.

We ended with a Kernel driver and a very simple user program and left the PC scancode mapping to C64 scancode mapping for this post.

For the scancode mapping we could have probably hack together something quickly, with a long case statement just doing a mapping of the essentials keys.

I did indeed followed a similar minimalistic approach with previous emulators that I wrote, and it worked just fine.

However, when entering programs with such a minimal mapping, it becomes a guessing game when you need to enter a shift key sequence. On a PC keyboard, for instance, hitting Shift and '2', gives you the @-sign. On a C64 keyboard, the same key sequence gives you double quotes (").

In this regard the Vice Commodore emulator do a very convenient key mapping. If you enter a Shift+2 sequence on a PC keyboard, a @ will be displayed on the C64 screen.  Thus, no need to have a photo of a C64 handy when typing Shift sequences!

In this post we will look into how keymapping works in the Vice Commodore emulator, and see how we can apply a similar keymapping within our C64 implementation.

An Overview of Vice key mapping

Let us do a quick overview on how key mapping works in the Vice emulator.

The easiest way to approach this exercise is to have a look at a key mapping file within the Vice source.

Start by downloading the source of Vice as a tarball, untarring it, and opening the file data/C64/sdl_sym.vkm.

This is a text file and apart from configuration information, it contains lots of comments.

One of the useful comments is the format of every line:

# - normal line has 'keysym/scancode row column shiftflag'

Each line start with a scan code for a particular key on a source keyboard.

Quickly looking at these scan codes, it appears to be the ASCII representation of applicable key. The key 'A', for instance will have the scancode 97 in the file (e.g. lowecase a). Similarly, the '1' key will have the code 49.

The next two values, row and column, represents the row and column values from the C64 keyboard for the associated key.

The final value on a row, shiftflag, tells whether a shiftkey is applicable for this key mapping. This will become clear in a moment.

Further on in the file we have a couple of comments giving more information about shiftflag:

# Shiftflag can have the values:
# 0      key is not shifted for this keysym/scancode
# 1      key is shifted for this keysym/scancode
# 2      left shift
# 4      right shift
# 8      key can be shifted or not with this keysym/scancode
# 16     deshift key for this keysym/scancode
# 32     another definition for this keysym/scancode follows
# 64     shift lock
# 256    key is used for an alternative keyboard mapping

Let us try to understand these shiftflags by looking at some examples.

We start with a simple example:

49 7 0 8               /*            1 -> 1            */

Here the shiftflag is 8, which, according to the table, can be either shifted or not. In short, this means for that particular key, we can blindly pass shift key presses on the PC keyboard to the C64 core. This is because on both a PC keyboard and C64 keyboard a Shift+1 corresponds to an exclamation mark (!).

Let us look at a more complex example:

50 7 3 32              /*            2 -> 2            */
50 5 6 16              /*            @ -> @            */

The first row have a shiftcode of 32, which, according to the table: another definition for this keysym/scancode follows.

This means that when you use this key with a shift, don't simply pass on the shift key to the C64 core. In such a case you need to consider the definition in the next row.

In this, the definition in the next row give us a new C64 scan code for the PC Shift+2 combination. Also, the shift code for this row is 16, meaning that we should not send a shift key to the C64 core for this key combination. So, on a PC Shift+2 corresponds to @. On a C64 you can access the @-key without a shift.

Let us look at another example:

55 3 0 32              /*            7 -> 7            */
55 2 3 1               /*            & -> &            */

Here the Shift+7 key has the shiftcode 1. This means that not only does Shift+7 maps to a different C64 scancode, but in addition we need to send a shift key to the C64 core.

Let us end off this section with another interesting example:

39 3 0 33              /*            ' -> '            */
39 7 3 1               /*            " -> "            */

In the first row we have two shift set simultaneously. This firstly means that for Shift+', use the definition in the  next row.

It also means that if we use (') without the shift, we need to also pass a shift to the C64 core. This is because to type a (') on the C64 we need to use the key combination Shift+7.

ASCII like scan codes

I mentioned previously that the source scan codes in sdl_sym.vkm corresponds more or less to the ASCII code of the relevant keys.

However, the scan codes we get from Linux, doesn't have any relation to a equivalent ASCII code whatsoever.

So, we need a conversion table between Linux scan codes to the scan code we require. This table will look more or less like the following:

struct keyboard {
           char code;
           char desc[20];
};
struct keyboard temp[256] ={
  {},
  {'\x1b',"ESCAPE"}, //1
  {'1',"Key1"}, //2
  {'2',"Key2"}, //3
  {'3',"Key3"}, //4
  {'4',"Key4"}, //5
  {'5',"Key5"}, //6
  {'6',"Key6"}, //7
  {'7',"Key7"}, //8
  {'8',"Key8"}, //9
  {'9',"Key9"}, //a
  {'0',"Key0"}, //b
...
  {'q',"KeyQ"}, //10
  {'w',"KeyW"}, //11
  {'e',"KeyE"}, //12
  {'r',"KeyR"}, //13
  {'t',"KeyT"}, //14
  {'y',"KeyY"}, //15
  {'u',"KeyU"}, //16
  {'i',"KeyI"}, //17
  {'o',"KeyO"}, //18
  {'p',"KeyP"}, //19
  {'[',"Key["}, //1a
...
};

So, for instance if someone press the key '1', we will get the scan code 2 from Linux. If we look at position 2 in this lookup table, we will find char value '1'.

The question might arise what code we can use as a scan code for modifier keys (e.g. shift, control) that doesn't really map to any ASCII code.

For these keys we could either use Capitals or ASCII codes after 128.

Parsing sdl_sym.vkm

Earlier on we discussed the structure of a vkm file.

The question at his point is: How do we parse such a file and store in a structure that we can easily transform a PC scancode to a C64 scancode?

For this purpose we can create a lookup table where we use the PC scan code as an index to retrieve the appropriate row that contains the resulting C64 scan code.

That is easy enough, but how do we cater for the shifted version of a scancode?

We can cater the shifted version by having two elements in each row of the lookup table, with the second element in a row been the shifted version of a scancode.

You might also remember from our discussion on the vkm file structure, that the resulting C64 scancode might optionally have an shift key assosiated with it. To cater for this, our lookup table need to have 4 elements per row.

Let us do some coding and start by defining this lookup table and initialising it:

...
int key_map[256][4];
...
void init_table() {
  for (int i = 0; i < 256; i++) {
    key_map[i][0] = -1;
    key_map[i][1] = -1;
    key_map[i][2] = -1;
    key_map[i][3] = -1;
  }
...
}

We want to keep the parsing of the vkm file simple, so we will need to modify this file a bit. We will be removing all comment lines at the beginning. We will also remove all the negative scancodes towards the end of the file.

Speaking of removing comments. You will see that each mapping line ends with a comment. It is not necessary for a vigorous exercise to remove these comments as well. Instead, when reading each line, we will read the first four items on every line that we need and skip straight to the next line.

Let us see if we can implement this functionality in code:

void init_table() {
...
  FILE *fp;
  fp = fopen("sym.txt", "r");
  char input[80];
  int num1, num2, num3, num4;
  while (1) {
    int status = fscanf(fp,"%d", &num1);
    fscanf(fp,"%d", &num2);
    fscanf(fp,"%d", &num3);
    fscanf(fp,"%d", &num4);
    fscanf(fp,"%[^\n]",input);
    if (status == EOF)
      break;
  }
...
}

sym.txt is our modified vkm as described earlier.

For every line we read our four values, and skip to the next line with [^\n].

We can now use num1, num2, num3 and num4 on each line to populate the lookup table.

Let us start with the simple case for rows having the shiftcode 8:

void init_table() {
  for (int i = 0; i < 256; i++) {
    key_map[i][0] = -1;
    key_map[i][1] = -1;
    key_map[i][2] = -1;
    key_map[i][3] = -1;
  }
  FILE *fp;
  fp = fopen("sym.txt", "r");
  char input[80];
  int num1, num2, num3, num4;
  while (1) {
    int status = fscanf(fp,"%d", &num1);
    fscanf(fp,"%d", &num2);
    fscanf(fp,"%d", &num3);
    fscanf(fp,"%d", &num4);
    fscanf(fp,"%[^\n]",input);
    if (status == EOF)
      break;
    if (num4 == 8) {
      key_map[num1][0] = (num2 << 3) | num3;
      key_map[num1][1] = 0;
      key_map[num1][2] = (num2 << 3) | num3;
      key_map[num1][3] = 1;
    }
  }
}


Here is number of things going on here, so let us unpack it a bit.

First, we are combining the row and column value into a single number, by putting row value at bits 5-3 and the column value into bits 2-0.

Also element 0 and 1 on the row, is for the unshifted version of the PC scancode, and element 2 and 3 for the shifted version.

Element 1 and 3 indicates whether we should pass a shift key to our C64 core with the resolved scancode.

Next, let us cater for rows having shiftcode as 1:

void init_table() {
  for (int i = 0; i < 256; i++) {
    key_map[i][0] = -1;
    key_map[i][1] = -1;
    key_map[i][2] = -1;
    key_map[i][3] = -1;
  }
  FILE *fp;
  fp = fopen("sym.txt", "r");
  char input[80];
  int num1, num2, num3, num4;
  while (1) {
    int status = fscanf(fp,"%d", &num1);
    fscanf(fp,"%d", &num2);
    fscanf(fp,"%d", &num3);
    fscanf(fp,"%d", &num4);
    fscanf(fp,"%[^\n]",input);
    if (status == EOF)
      break;
    if (num4 == 8) {
      key_map[num1][0] = (num2 << 3) | num3;
      key_map[num1][1] = 0;
      key_map[num1][2] = (num2 << 3) | num3;
      key_map[num1][3] = 1;
    } else if (num4 == 1) {
      key_map[num1][0] = (num2 << 3) | num3;
      key_map[num1][1] = 1;
    } 
  }
}

For rows having this shift code we don't have a shifted version for these scancodes.

Finally let us cater for rows with shiftcode 32. As seen in a previous section we can have cases where shiftcode 32 and 1 can be enabled simultaneously. So, when checking for flag 32, we need to mask off all other bits:

void init_table() {
  for (int i = 0; i < 256; i++) {
    key_map[i][0] = -1;
    key_map[i][1] = -1;
    key_map[i][2] = -1;
    key_map[i][3] = -1;
  }
  FILE *fp;
  fp = fopen("sym.txt", "r");
  char input[80];
  int num1, num2, num3, num4;
  while (1) {
    int status = fscanf(fp,"%d", &num1);
    fscanf(fp,"%d", &num2);
    fscanf(fp,"%d", &num3);
    fscanf(fp,"%d", &num4);
    fscanf(fp,"%[^\n]",input);
    if (status == EOF)
      break;
    if (num4 == 8) {
      key_map[num1][0] = (num2 << 3) | num3;
      key_map[num1][1] = 0;
      key_map[num1][2] = (num2 << 3) | num3;
      key_map[num1][3] = 1;
    } else if (num4 == 1) {
      key_map[num1][0] = (num2 << 3) | num3;
      key_map[num1][1] = 1;
    } else if (num4 & 32) {
      key_map[num1][0] = (num2 << 3) | num3;
      key_map[num1][1] = num4 & 1;
      fscanf(fp,"%d",&num1);
      fscanf(fp,"%d",&num2);
      fscanf(fp,"%d",&num3);
      fscanf(fp,"%d",&num4);
      fscanf(fp,"%[^\n]",input);
      key_map[num1][2] = (num2 << 3) | num3;
      key_map[num1][3] = num4 & 1;
    }
  }
}


When we reach a row with shiftflag 32, we also need to read the next line to get details about the shifted version of the scancode.

Transforming to C64 scancodes

Let us bring all code we have developed in this post together. The following outline shows the changes we need to make to our main method so that we convert PC scancodes to C64 scancodes:

int main(){
...
  init_table();
...
  while(1) {
...
    char keys_to_process[4];
...
    readKeyboard();

    for (int i = 0; i < 6; i++) {

      char ps_2_code = temp[keys[i]].code;
      if ((ps_2_code == 'C') || (ps_2_code == 'B')) {
        shifted = 1;
        continue;
      }
      keys_to_process[num_keys_to_process] = ps_2_code;
      num_keys_to_process++;
    }

    keyToProcess.word1 = 0;
    keyToProcess.word2 = 0;

    for (int i = 0; i < num_keys_to_process; i++) {
      int offset = shifted << 1;
      int c64_scan_code = key_map[keys_to_process[i]][offset];
      if (c64_scan_code == -1)
        continue;
      if (c64_scan_code < 32) {
        keyToProcess.word1 = keyToProcess.word1 | (1 << c64_scan_code);
      } else {
        c64_scan_code = c64_scan_code - 32;        
        keyToProcess.word2 = keyToProcess.word2 | (1 << c64_scan_code);
      }
      if (key_map[keys_to_process[i]][offset+1]) {
        keyToProcess.word1 = keyToProcess.word1 | (1<<15);
      }
    }
    ret = write(fd, &keyToProcess, 8); // Send the string to the LKM

  }
   return 0;
}


We start by calling init_table, which we developed earlier on.

We then take the captured keys produced by readKeyboard(), and convert them to ASCII like scancodes as they appear in the VKM file. The translated keys are stored in the array keys_to_process.

During this loop we also determine if any shift key is been held done, and set the variable shifted as such. I would also like to mention here that in this transaltion process I have decided to assign the left- and right-shiftkey to the ASCII values 'C' and 'B' respectively.

Once we have translated all keys, we loop though them and translate it to C64 scancodes. Here we make use of the shifted variable to decide if we are going to either use elements 0/1, or elements 2/3.

We finally enable the shiftKey in the C64 keyboard matrix if either element 1 or 3 are set.

Mapping the cursor keys

Up to this point the mapping of PC keys to C64 scancodes has been fairly straightforward. Our mapping model, however, falls a bit on its face when we try to map the cursor keys.

When we press one of the cursor keys, Linux returns us a break code preceding the actual scan code.

For instance, if we press cursor up, we will receive the following bytes:

E0 48

This is a bit problematic for our process that maps to ASCII like scancodes, which only works with single byte values.

We could condense these two bytes into a single one with the fact that these scan codes usually only make use of values 0 to 127. For scancodes preceded by E0, we can just set bit 7, and then set the lower 7 bits with our actual scan codes.

On thing we should aware of, though, is that when we release a key, we get a scan code of which bit 7 is also set.

With all this in mind, let us write some code for condensing a scan code with a break code, into a single byte.

We start with our readKeyboard() method. Previously this method read a single byte from stdin and acted accordingly.

Here we already have problems if we get a scancode with a breakcode. At this point we can already reduce our problems by reducing a scancode with a breakcode into a single number.

I will, however, reduce this number to a 9-bit number instead of a 8-number, so we can preserve information about whether the scancode is for a key down or a key up:

int getKeyCode(int *code) {
  char buf[1];
  int res;
  res = read(0, &buf[0], 1);
  if (res == -1)
    return -1;
  if ((buf[0] & 0xff) != 0xe0) {
    *code = buf[0] & 0xff;
    return res;
  }
  res = read(0, &buf[0], 1);
  if (buf[0] == 1) {
    restoreKeyboard();
    exit(0);
  }
  *code = (buf[0] & 0xff) | 0x100;
  return res;
}

void readKeyboard()
{
    int res;
    int code;
    res = getKeyCode(&code);
    processKey(code);
    while (res >= 0) {
        res = getKeyCode(&code);
        processKey(code);
    }
}


So, if we encounter a scancode with a breakcode, we just set bit 8 to a 1.

Next, we should change processKey so that it can interpret bit 8 accordingly:

void processKey(int scanCode) {
  if ((scanCode & 0x80)) {
    //do key release
    scanCode = (scanCode & 0x100) ? ((scanCode & 0x7f) | 0x80) : (scanCode & 0x7f);
    doKeyUp(scanCode);
  } else {
    //do key down
    scanCode = (scanCode & 0x100) ? ((scanCode & 0x7f) | 0x80) : scanCode;
    doKeyDown(scanCode);
  }
}


Once we have determined whether the scancode is a keydown or a keyup, we can reduce the scancode to 8-bits.

In Summary

In this post we have implemented scan code mapping from PC scancodes to C64 scancodes. For this purpose we have used a key mapping file from the Vice Commodore emulator.

In the next post we will be modifying our Test program so that it can accept a tape image filename as a parameter. The test program will then transfer the Tape Image from the Linux file system to the C64 core.

Till next time!

No comments:

Post a Comment