Thursday 2 April 2020

Loading tape images from Linux file system

Foreword

In the previous post we wrote some code for mapping PC scancodes to C64 scancodes, and sending the result to our C64 FPGA core.

In this post we will be writing some code for reading tape images from the Linux file system and sending them to our C64 core.

Overview

Let us start this post by familiarising ourselves again on how tape image loading  currently works in our C64 core.

Our C64 core uses an AXI master port to read tape image data from the SDRAM on the Zybo from a predefined location.

So, when triggering a tape load in our C64 core by typing LOAD<ENTER>, it is up to you to ensure that this predefined memory area is populated with a valid tape image.

Apart from the memory area that should contain the tape image, our C64 core also have a peripheral register mapped into the Zynq memory space that controls tape operation. This peripheral register is located at address 0x43c0_0008.

In this register there is two bits of importance for controlling tape loading:


  • Bit 2: Tape Button: This bit corresponds to the Play Button of an original Datasette unit. 
  • Bit 1: Reset tape data pointer: When you have populated the tape image area in memory with a new image, you should briefly set this bit to 1 and then back to zero. This ensure that when you trigger a LOAD from the C64 core, the reading will start at the beginning of the memory area.
So, our application running in Linux should perform a couple of things to cater for tape loading.

Firstly when the application starts, it should the requested tape image into the tape area in memory. The tape image to load should be supplied as a command line parameter .

From previous posts, you might remember that with user applications Linux, you work in Virtual Memory space, and don't have access to physical memory addresses. In our case we will need access to physical memory addresses in order populate the area in memory with the required tape image so that our C64 core can access it.

Our Linux Kernel driver in this regard will also need to act as mediator for moving the tape image to required memory area.

Using IOCTL's

In one the previous two posts we have developed Kernel driver that served as an interface between a user application and our C64 FPGA core.

Currently when opening this driver as a file, you can only write C64 scan codes to it.

Can you utilise this driver to control more aspects of the C64 core? For instance, if you not only want to send C64 scancodes to this driver, but also tape images?

One can indeed, with the help of IOCTL's. According to Wikipedia:
ioctl (an abbreviation of input/output control) is a system call for device-specific input/output operations and other operations which cannot be expressed by regular system calls.
This sounds exactly what we want to achieve with our device driver. IOCTL provides you a way of serving multiple with a single file handle.

We encountered ioctl's in a previous post where I explained how to capture keyup/keydown events in Linux within the setupKeyboard event:

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

Here we used an ioctl to get the current keyboard mode from stdin (e.g. file handle zero). By simply doing a read() call from stdin, you will just receive key events from the keyboard and you simply wouldn't be able to get the keyboard mode at all.

IOCTL gives us some helping out for this need. IOCTL is almost like a read() function call, but it provides you with an additional important parameter: A command parameter. In the previous example we provided the command parameter KDGKBMODE.

So, how does one define an ioctl call within your device driver? For starters, you need to define a function in your driver having the following signature:

int (*ioctl) (struct inode *inode, struct file *filp,
unsigned int cmd, unsigned long arg);

In our case, we won't worry about the first two parameters. Our key parameters will only be the last two parameters, cmd and arg.

The arg parameter in our case will be a pointer, and we will need to cast it as such in our ioctl method.

Let us end this section, by defining a skeleton ioctl method, on which we will expand in coming sections:

...
static struct file_operations fops =
{
   .open = dev_open,
   .read = dev_read,
   .write = dev_write,
   .unlocked_ioctl = c64_ioctl,
   .release = dev_release,
};
...
static long c64_ioctl (struct file *filp,
                   unsigned int cmd, unsigned long arg) {

}
...

I have also added an extra member to out fops struct. This method instructs Linux on which method to call when we do an ioctl on our C64 driver.

Changes to our driver

Let us tart with the necessary changes to our C64 kernel driver.

The first thing that comes to mind is that currently, when we open the driver, we switch to the C64 screen right away.

So, in effect the copying of the tape data will take place while the C64 screen is already active. Somehow, this scenario doesn't seem so clean. We would want to only switch to the C64 screen once all background initialisation has been completed.

So let us do the following modifiction to the open method:

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;
}

No switching to C64 screen on open anymore!!

Next, let us work on the actual tape loading mechanism. Let us start with by defining a memory mapping for a physical address range storing the tape image that our C64 core will retrieve:

 ...
static void __iomem *tape_mem_area;
...
static int __init ebbchar_init(void){
...
  tape_mem_area =  ioremap(0x1f500000,
           2000000);
...
   return 0;
}

I have used start address 0x1f50_0000, which is also above the 500MB mark, out of available Kernel space.

Next, let us define an IOCTL call for copying tape data from userspace to out tape area:

...
static long c64_ioctl (struct file *filp,
                   unsigned int cmd, unsigned long arg) {
...
  if (cmd == 0) {
    unsigned char * user = (unsigned char *) arg;
    copy_from_user(tape_data, user, 8192);
    for (i = 0; i < 8192; i++) {
      iowrite8(tape_data[i], i + tape_mem_area + tape_pointer);
    }
    tape_pointer = tape_pointer + 8192;
  } 
...
  return 0;
}
...

So, zero is the command code to send chunks of data from our userspace program to the Kernel.

This call assumes a chunk size of 8KB per call. So, you need to always pass a pointer to an array of char with 8192 elements. This simplify code somewhat. If we are about to send the last portion of the tape file, the chunk size might be less than 8192 bytes. The remaining garbage in the array is not an issue since our C64 core only reads what it needs.

The tape_pointer variable keeps track of where we are currently with the copying of the tape image.

Once we are finished copying the tape image, we need a IOCTL call to actually switch to C64 and to inform the C64 core to start reading the tape image from the beginning:

static long c64_ioctl (struct file *filp,
                   unsigned int cmd, unsigned long arg) {
  int i;
  if (cmd == 0) {
    unsigned char * user = (unsigned char *) arg;
    printk("after cast\n");
    copy_from_user(tape_data, user, 8192);
    for (i = 0; i < 8192; i++) {
      iowrite8(tape_data[i], i + tape_mem_area + tape_pointer);
    }
    tape_pointer = tape_pointer + 8192;
  } else if (cmd == 1) {
    iowrite32(0x206, c64_reg_screen_mode);
    msleep(1000);
    iowrite32(0x204, c64_reg_screen_mode);
  } 

  return 0;
}

The reset function currently only resets the C64core's pointer to the beginning of the tape area. However, the FPGA can be altered to also reset the 6502 CPU.

We finally need one last IOCTL call to simulate the press of the Play button:

static long c64_ioctl (struct file *filp,
                   unsigned int cmd, unsigned long arg) {
  int i;
  if (cmd == 0) {
    unsigned char * user = (unsigned char *) arg;
    printk("after cast\n");
    copy_from_user(tape_data, user, 8192);
    for (i = 0; i < 8192; i++) {
      iowrite8(tape_data[i], i + tape_mem_area + tape_pointer);
    }
    tape_pointer = tape_pointer + 8192;
  } else if (cmd == 1) {
    iowrite32(0x206, c64_reg_screen_mode);
    msleep(1000);
    iowrite32(0x204, c64_reg_screen_mode);
  } else if (cmd == 3) {
    iowrite32(0x200, c64_reg_screen_mode);
  }

  return 0;
}

Changes to our userspace program

Let us see what changes we need for our userspace program.

We start with the following changes:

int main(int argc, char *argv[]){
...
   unsigned char data[8192];
   tapefile = fopen(argv[1],"r");
   int num_read;
   do {
     num_read = fread(data, 1, 8192, tapefile);
     ioctl(fd,0,&data);
   } while (num_read == 8192);

   ioctl(fd,1);
...
}


We receive the filename of the required as a parameter on the commandline. We then open this file and send chunks of 8KB to the driver.

Finally we call command #1 on the driver, which enables the C64 screen and reset the C64core so that it reads tape data from the beginning.

We finally need to cater for allowing the user to enable the Play, when required to do so. I have decided to allocate the Function key F11 for this purpose.

I will checking for this key in the loop that process 'ASCII'-scancodes:

    for (int i = 0; i < num_keys_to_process; i++) {
      int offset = shifted << 1;
      if (keys_to_process[i] == 0x18) {
        ioctl(fd,3);
        continue;
      }
      int c64_scan_code = key_map[keys_to_process[i] & 0xff][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] & 0xff][offset+1]) {
        keyToProcess.word1 = keyToProcess.word1 | (1<<15);
      }
    }

In my mapping file where I map Linux scan codes to ASCII-scancodes I have mapped the resulting code for F11 to 0x18. When we encounter this key, we make IOCTL call to enable the play button, and skip further processing of this key.

In Summary

In this post we have developed some functionality for loading tape images stored on a Linux File system, and copying it to an area of memory which our C64 core can access to trigger a tape load.

In the next post we will be mapping keys for a joystick, so we can play the game we have load from the tape image on the Linux File System!

Till next time!

No comments:

Post a Comment