Thursday, 6 February 2020

Capturing KeyUp/KeyDown events in Linux

Foreword

In the previous post we finally got to a point where we can view a Linux console from a Zybo board on a VGA screen, as well as interacting with it via a USB keyboard.

For all practical reasons, Linux can now now run completely on its own a Zybo board, without the need of been connected to a PC.

Having manged to run Linux on the Zybo board, the next step would be to able for this running instance of Linux to be able to interact with our C64 FPGA module. This interaction will involve the following:


  • Redirecting keystrokes to the C64 FPGA module
  • Switching between C64 video output and the Linux Console
  • Loading .TAP images from the Linux file system and transferring it to the C64 FPGA module
This is quite a lot of functionality and we will definitely not be able tackle it all in one post, but as usual, we will tackle it like eating an elephant: bit by bit, or in our case, post by post :-)

In this particular post will start to tackle the redirection of keystrokes to the C64 FPGA module. This by itself needs two pieces of functionality:

  • Writing a user program in C running under Linux, carefully monitoring when the user pressed a key or released a key.
  • Writing a Kernel Driver driver that receives the key events from the user program mentioned in the previous point, and forwarding it to our C64 FPGA module.
One might wonder why it would be necessary to write a Kernel driver for forwarding the keystrokes to our C64 FPGA module. Why couldn't the mentioned user program take care of this as well?

The big reason for this is is because because interaction between our C64 FPGA module and Linux will happen via specific physical locations in memory space. When working with physical locations in memory it is always better to delegate these actions to a Kernel Driver.

In this post we will only be dealing with creating the user program for capturing the KeyUp/KeyDown events and in a later post we will be developing the associated Kernel Driver.

Watching the keyboard like a Hawk


When one writes an emulator for the Commodore 64, one of the first things one realise is that at any instant in time, you need to exactly know the state of the keyboard: Which keys are currently been held down and the moment one of them is released.

Knowing this kind if information just makes your emulator appear more in sync with your keyboard.

If one will be getting these keyboard changes from the Linux console, as we will be doing with our Zybo board, one will be facing a couple of frustrations. One of these frustrations is that by default you will not receive any key release events, no matter what you try.

This issue has to do with the default mode the Linux console is in, which is called cooked mode.

Let us analyse this problem by first looking at what cooked mode is.

Cooked mode actually provides a lot of functionality in the background when requesting a line of text. In cooked all edits from the user will be performed in the background and the final result will be send when you hit enter.

However, when we want to catch both keyup and keydown events, we need to switch the console from cooked mode to raw mode.

To set the right options to switch to raw mode takes quite a bit of fiddling.

Luckily someone on the Internet did a nice write-up on how to switch to raw mode: 

http://www.gcat.org.uk/tech/?p=70

In this blog post, the following method takes care of setting the console into raw mode:

#include "unistd.h"
#include "linux/kd.h"
#include "termios.h"
#include "fcntl.h"
#include "sys/ioctl.h"

static struct termios tty_attr_old;
static int old_keyboard_mode;

int setupKeyboard()
{
    struct termios tty_attr;
    int flags;

    /* make stdin non-blocking */
    flags = fcntl(0, F_GETFL);
    flags |= O_NONBLOCK;
    fcntl(0, F_SETFL, flags);

    /* save old keyboard mode */
    if (ioctl(0, KDGKBMODE, &old_keyboard_mode) < 0) {
 return 0;
    }

    tcgetattr(0, &tty_attr_old);

    /* turn off buffering, echo and key processing */
    tty_attr = tty_attr_old;
    tty_attr.c_lflag &= ~(ICANON | ECHO | ISIG);
    tty_attr.c_iflag &= ~(ISTRIP | INLCR | ICRNL | IGNCR | IXON | IXOFF);
    tcsetattr(0, TCSANOW, &tty_attr);

    ioctl(0, KDSKBMODE, K_RAW);
    return 1;
}

If one wants to set the console back to cooked mode, the following method will do the trick:

void restoreKeyboard()
{
    tcsetattr(0, TCSAFLUSH, &tty_attr_old);
    ioctl(0, KDSKBMODE, old_keyboard_mode);
}

This is quite a bit of code to get into raw mode and it might be easier just to use the SDL library that makes it much easier to capture keyup and keydown events.

However, to get the SDL library on the Zybo board, one would need to jump through a couple of cross compile hoops.

So, for now the setupKeyboard method will do it for me.

The previous mentioned post also provides a method for reading the keystrokes when we are in raw mode:

void readKeyboard()
{
    char buf[1];
    int res;

    /* read scan code from stdin */
    res = read(0, &buf[0], 1);
    /* keep reading til there's no more*/
    while (res >= 0) {
 switch (buf[0]) {
 case 0x01:
            /* escape was pressed */
            break;
        case 0x81:
            /* escape was released */
            break;
        /* process more scan code possibilities here! */
 }
 res = read(0, &buf[0], 1);
    }
}

Putting everything together

The mentioned Blog post in the previous section didn't provide a main method, so let us quickly create one:

int main(int argc, char * argv[])
{
   //code
  setupKeyboard();
  while(1) {
    usleep(20000);
    readKeyboard();
  }

}

Here we are in an endless loop, waiting 20milliseconds at each cycle before we read the keyboard.

Currently the readKeyboard method doesn't give any visual feedback on the keystrokes it gets. For this one could probably do a printf after each read. This would, however, clutter the display with repeated scancodes at a rate of whatever the keyboard repeat rate is been set at.

It would be so much nicer if we could just print output if the state of a key changes.

Let us achieve this by keeping at hand an array of keys that is currently been held down together with an applicable utility method:

...
int keys[6];
...
int getCodeInList(int code) {
  if (code == 0xff) {
    return -1;
  }
  code = code & 0x7f;

  for (int i = 0; i < 6; i++) {
    if (keys[i] == code)
      return i;
  }
  
  return -1;
}
...

With this code we can have up to 6 keys that are simultaneously been held down. You will also realise that we are masking off the most siginicant bit of the scancode to test. This is because both a press and release will yield the same value in lower 7 bits, but a release will have bit 7 set to one.

Next, let us write a method for when the key pressed, we insert the code into the array:

void doKeyDown(int scanCode) {
  int index = getCodeInList(scanCode);
  if (index != -1)
    return;
  for (int i = 0; i < 6; i++) {
    if (keys[i] == 0) {
      keys[i] = scanCode;
      if (scanCode == 0x1e) {
        printf("A pressed\n");
      } else if (scanCode == 0x1f) {
        printf("S pressed\n");
      }
      break;
    }
  }
}


Here we only print something to the console if the scan code wasn't in the key array.

Similarly, the following method will remove an element from the array:

void doKeyUp(int scanCode) {
  for (int i = 0; i < 6; i++) {
    if (keys[i] == scanCode) {
      keys[i] = 0;
      if (scanCode == 0x1e) {
        printf("A released\n");
      } else if (scanCode == 0x1f) {
        printf("S released\n");
      }
      break;
    }
  }
}


Once again, we only print something if this code were previously in the array.

Next, let us write another method that will call either doKeyDown or doKeyUp, depending on the scanCode given:

void processKey(int scanCode) {
  if (scanCode == 0xff) {
    return;
  }

  if ((scanCode & 0x80)) {
    //do key release
    doKeyUp(scanCode & 0x7f);
  } else {
    //do key down
    doKeyDown(scanCode);
  }
}


Finally, we need to modify the readKeyboard method as follows:

void readKeyboard()
{
    char buf[1];
    int res;

    /* read scan code from stdin */
    res = read(0, &buf[0], 1);
    if(buf[0] == 1) {
      restoreKeyboard();
      exit(0);
    }
    processKey(buf[0]);
    /* keep reading til there's no more*/
    while (res >= 0) {
 res = read(0, &buf[0], 1);
        if (buf[0] == 1) {
          restoreKeyboard();
          exit(0);
        }
        processKey(buf[0]);
    }
}

You can now test the whole program. This program, however, will only work if you use a virtual console. If you do it over an SSH channel or terminal window in an X-Windows session, it will not work.

As you keep pressing and releasing the A ans S keys, you will see statements  like A pressed and A released.

In Summary

In this post we have implemented functionality to capture KeyUp and KeyDown events.

In the next post we will be implementing functionality within our C64 FPGA module for switching between the video output of the C64 module and the Linux console.

Till next time!

No comments:

Post a Comment