Sunday 1 March 2020

Redirecting keystrokes from Linux to the C64 module: Part 1

Foreword

In the previous post we implemented functionality within our C64 design allowing us to toggle between Linux console output and C64 video output.

We performed this toggling between the two video outputs with the help of a toggle button present on the Zybo board. Later on we will be moving the control of this video mode toggling to software.

In this post we will be focusing on redirecting keystrokes from Linux to our C64 module.

In order to do this we need to develop a Kernel Driver and a user program in userspace.

This is quite a lot to cover in one post, so in this post we will not be developing a complex PC key to C64 mapping mechanism. Instead, as a proof of concept, we will just be capturing two keystrokes from a keyboard to display on the C64 screen.

For this reason I have decided to split the whole keystroke redirection functionality into two posts. In the next post we will be tackling advanced PC key -> C64 key mapping.

Surfacing Screen mode to software

As mentioned in the forward, we need to work towards the goal of switching screen mode in software.

In order to achieve this, we need to surface this mode bit within a register in our Slave AXI block.

For this purpose we can just use Slave register 2, since we don't utilise all the bits of this register. Bits 8 to 4 gets utilised by the joystick bits, so we can use bit 9 for screen mode. For this we make the following changes to our user logic:

 // Add user logic here
    assign slave_reg_0 = slv_reg0;
    assign slave_reg_1 = slv_reg1;
    assign restart = slv_reg2[1];
    assign tape_button = slv_reg2[2];
    assign joybits = slv_reg2[8:4];
    assign c64_mode = slv_reg2[9];
 // User logic ends


We need to ensure that this c64_mode gets surfaced in our Slave AXI block:
This port we will connect to our VGA block, effectively replacing the connection from the push button on the Zybo board.

We will now be able to control the screen mode in software by just writing to bit 9 of address 0x43c0_0008.

As mentioned in a previous post, it is not so easy to access a physical address in Linux, especially in Userspace.

So, this is one of the reasons we will be developing a Kernel driver in this post.

Into Kernel drivers

Let us get our fingers dirty with writing a Kernel driver. For beginners there is a nice resource, Linux Device Drivers (third edition), available here.

Before we start writing a Kernel device driver, let us first focus on what we want to achieve.

To send one or more keystrokes to our C64 module, we need to set one or more bits in the registers located at addresses 0x43c0_0000 and 0x43c0_0004. Together these two registers contains 64 bits, which corresponds to the 64 keys you find on a C64 keyboard.

The idea is that we open this Kernel device driver as a file and we write two 32-bits at a time to this 'file'. Our kernel driver in turn will write these values to address 0x43c0_0000 and 0x43c0_0004 respectively.

To send both register values as a unit, we can make use of a struct with the following definition:
struct keyboard 
{
           u32 word1;
           u32 word2;
};


The details will become clear later.

To help us to get started quickly, it will help if we can find a minimalistic example on the Internet that is similar to what we want to achieve. Derek Molloy's comes to the rescue here: http://derekmolloy.ie/writing-a-linux-kernel-module-part-2-a-character-device/

Derek gives the source of this tutorial on his Github site. In particular, we are interested in the following two files:


In ebbchar.c there is all the necessary code for a fully fletched character driver. The example provided open the device driver as a file, write a string to it and then reads it back.

When I tried out this example, it crashed when i tried writing to the driver. At first I could figure out why this was happening. However, when I had a look at the read and write method together, I discovered something:

static ssize_t dev_read(struct file *filep, char *buffer, size_t len, loff_t *offset){
   int error_count = 0;
   // copy_to_user has the format ( * to, *from, size) and returns 0 on success
   error_count = copy_to_user(buffer, message, size_of_message);

   if (error_count==0){            // if true then have success
      printk(KERN_INFO "EBBChar: Sent %d characters to the user\n", size_of_message);
      return (size_of_message=0);  // clear the position to the start and return 0
   }
   else {
      printk(KERN_INFO "EBBChar: Failed to send %d characters to the user\n", error_count);
      return -EFAULT;              // Failed -- return a bad address message (i.e. -14)
   }
}

static ssize_t dev_write(struct file *filep, const char *buffer, size_t len, loff_t *offset){
   sprintf(message, "%s(%zu letters)", buffer, len);   // appending received string with its length
   size_of_message = strlen(message);                 // store the length of the stored message
   printk(KERN_INFO "EBBChar: Received %zu characters from the user\n", len);
   return len;
}

In dev_read there is a call to copy_to_user, but not a similar call within dev_write. When passing a pointer from user space to kernel space, functions like copy_to_user and copy_from_user is necessary to move the information  between the two spaces.

Writing the C64 Keyboard driver

In the previous section we had a look at Derek Molloy's example Kernel driver. With the minimum amount of tweaks to this example driver, we can easily create our C64 Keyboard driver.

We start off by mapping our Slave AXI registers into Kernel Space:

...
static void __iomem *c64_reg_base;
static void __iomem *c64_reg_screen_mode;
static void __iomem *c64_reg_keyboard_0;
static void __iomem *c64_reg_keyboard_1;
...
static int __init ebbchar_init(void){
...
   c64_reg_base = ioremap(0x43c00000, 16384);
   c64_reg_screen_mode = c64_reg + 8;
   c64_reg_keyboard_0 = c64_reg;
   c64_reg_keyboard_1 = c64_reg + 4;
...
   return 0;
}
...

The key here is the call to ioremap, which maps maps a 16KB region, starting at the first address of our Slave AXI regsiters, into virtual memory.

We then define some more pointers in which we can access the keyboard bits and C64 screen mode directly.

I was thinking for some time what kind of interface we could use for switching between two screen modes. This ended off not to be a problem at all. We can just switch to C64 screen moe when we open the driver, and switching back to Linux Console mode when we close the driver again:

...
static int dev_open(struct inode *inodep, struct file *filep){
   numberOpens++;
   printk(KERN_INFO "EBBChar: Device has been opened %d time(s)\n", numberOpens);
   iowrite32(0x200, c64_reg_screen_mode);
   return 0;
}
...
static int dev_release(struct inode *inodep, struct file *filep){
   printk(KERN_INFO "EBBChar: Device successfully closed\n");
   iowrite32(0x0, c64_reg_screen_mode);
   return 0;
}
...

What we still need to do is to take writes to our kernel driver and sending this information to the physical registers:

static ssize_t dev_write(struct file *filep, const char * keys, size_t len, loff_t *offset){
   struct keyboard temp[1];
   copy_from_user(temp, keys, 8);
   iowrite32(temp[0].word1, c64_reg_keyboard_0);
   iowrite32(temp[0].word2, c64_reg_keyboard_1);
   return 8;
}


Our dev_write accepts the keys as a pointer of char. This is to conform to the interface when creating a character file driver. Here we cheat a bit, however. The actual data we will be sending will not be a an array of char, but a struct of keyboard.

Internally we will copy this data an actual keyboard structure. Lastly we will write the actual data to the actual registers.

With Linux running on our Zybo board, to load this driver is a two step process.

Firstly, similarly as we done with our Linux Framebuffer driver, we need to issue a insmod command, for loading the kernel driver so it can be used by the Linux Kernel driver.

When this particular driver loads, it will output the major number it is registered as. Make a note of this number, as you will need it to in order to add it as a node under /dev.

To add a node under /dev for this device, issue the following command:

mknod /dev/ebbchar c 244 0

In my case the major device number was 244. Also, the c indicates that we are about to add a character driver.

Writing the user program

Our test program is kind of a merge, where we take a take program in a previous post where captured keystrokes in Linux, together with Derek Molloy's test program.

Let us start the discussion by looking at the final main() method:

int main(){
   int ret, fd;
   char stringToSend[BUFFER_LENGTH];
   printf("Starting device test code example...\n");
   fd = open("/dev/ebbchar", O_RDWR);             // Open the device with read/write access
   if (fd < 0){
      perror("Failed to open the device...");
      return errno;
   }

  setupKeyboard();
  struct keyboard keyToProcess;

  while(1) {
    usleep(20000);
    readKeyboard();
    keyToProcess.word1 = 0;
    keyToProcess.word2 = 0;
    for (int i = 0; i < 6; i++) {
      if (keys[i] == 0)
        continue;
      int translated = getC64ScanCode(keys[i]);
      keyToProcess.word1 = (translated < 32 ) ? (keyToProcess.word1 | (1 << translated)) : keyToProcess.word1;
      if (translated > 31) {
        translated = translated - 32;
        keyToProcess.word2 = keyToProcess.word2 | (1 << translated);
      } 
    }
    ret = write(fd, &keyToProcess, 8); // Send the string to the LKM
    if (ret < 0){
       perror("Failed to write the message to the device.");
       return errno;
    }

  }
   return 0;
}

We start by opening a file handle to our device driver. In a main loop we capture key up/down events from the keyboard, convert it to a C64 scancode and send to our device driver as a keyboard struct.

You will remember from a previous post that we maintain a global keys array, which indicates which keys are currently been held down. This array caters for up to 6 elements and if an element is not in use, it will simply hold the value zero.

We will be implenting the method getC64ScanCode in the next post.

In Summary

In this post we have created a Linux Kernel device driver that will accept C64 scan codes from a user program and forward it to our C64 module.

In the next post we will be functionality where we will map PC scancodes to C64 scancodes. In this process I will try to utilise the keyboard mapping functionality present in the Vice C64 emulator.

Till next time!

No comments:

Post a Comment