Wednesday 29 April 2020

Initialising the Sound System

Foreword

In the previous post we added joystick support to our C64 FPGA from the Linux operating system. In addition we expanded the system to be able to use either Joystick Port #1 or Joystick Port #2.

In this post we will I show how to initialise the sound chip on the Zybo board from Linux, so that we can hear the sounds from the SID module.

Speaking of the sound system. A couple of posts ago I mentioned that I have updated my Github repo with the recent changes of our C64 core module, excluding the sound system.

I am pleased to announce that my Github repo now also contains the addition of the sound system.

Rendering SID samples to a speaker

Some time ago, I wote two posts, here and here, where I explained how to incorporate Thomas Kindler's SID core into our C64 design.

My discussion in these two posts basically stopped at the point where the SID samples was serialised over the I2S bus. I haven't explained the supporting processes at all that needs to happen so that the sounds finally gets rendered on a speaker.

Having an overview of these processes is necessary to understand the steps required for initialising the Sound System. So, let's get started!

Sound is produced and captured on the Zybo board by means of the SSM2603 chip from ANALOG Devices.

This chip receives sound samples via the I2S bus and is configured via a I2C bus. As previously mentioned we have already implemented the I2S bus for sending Audio Samples.

We haven't, however, discussed the implementation to interface with the I2C bus. Luckily one doesn't need to implement a I2C module from scratch, since the Zynq contains two I2C onchip peripherals.

In the next section I will briefly highlight what is required to use one of these onchip I2C peripherals to initialise the SSM2603.

The question is, however, how do we use a onchip I2C pheriperhal in our design? We will cover this in the next section.

Using a onchip I2C peripheral

To surface a I2C peripheral in our design, we need to configure our Zynq block. So, start by double clikcing on the Zynq Block, selecting the MIO Configuration section and opening I/O peripherals.

There is a couple of things you should do here. First you need to select the option I2C 1. With this option selected a drop down will appear next to this option in the IO column.

In this dropdown in the IO column you need to select to which external pins on the ZYNQ this I2C peripheral should be attached to. The first couple of options are MIO pins. If we select any of these we will not see the I2C pins in our block design at all. The only option that will surface the pins in our design is EMIO:



With this option selected the I2C pins will now appear within our design in the Zynq block:

You will see that I have already linked up these pins to the rest of the diagram. the iobuf block is a custom block I have created have pin as a birectional pin. The direction of this pin is controlled by the tristate input.

This is all there is to wiring up these pins. Next, let us see what software changes is required to drive these pins and to ultimately initialising the SSM2603.

Initialising SSM2603 from Linux

When I originally developed the SID functionality in the C64 core, I initialised the SSM2603 in a Bare-Metal application. Here is the source for the Bare-Metal application: https://github.com/ovalcode/c64fpga/blob/master/SDK.src/standalone/c64.c

There is quite a bit going on in this application and apart from initialising the SSM2603, we are also reading from a USB keyboard, as explained in a previous post. The key method to look for is init_sound().

If you follow the code in init_sound(), you will see that we are directly writing to the registers associated with the I2C 1 controller. Since we are working in Linux, at the moment, it is probably better practice to see if one can open I2C 1 as a device file and then manipulate the SSM2602 with this file.

In approaching this problem with accessing I2C 1 as a device file, I found myself trying to fiddle with the device tree to try and enable the I2C driver.  This ended up been quite a mission, so I reverted, at least for now, to copy the register writes/read from the standalone application and just modifying it a bit, so it can work in Linux. The whole SSM2603 init sequence we will also make part of the Kernel driver we have been developing in the last couple of posts.

As we have seen in previous posts, when you want to access physical memory locations in Linux, you first need to map it to a virtual address space. So, let us do it for the I2C 1 registers:

static void __iomem *i2c_reg;

static int __init ebbchar_init(void){
...
c64_reg = ioremap(0x43c00000,
           16000);
c64_reg = c64_reg;
c64_reg_screen_mode = c64_reg + 8;
c64_reg_keyboard_0 = c64_reg;
c64_reg_keyboard_1 = c64_reg + 4;
tape_mem_area =  ioremap(0x1f500000,
           2000000);

i2c_reg = ioremap(0xE0005000, 128);
...
}

The address returned by ioremap is an address in virtual address space. From this point onward when you want to access of the I2C registers, you need to use i2c_reg as your base and then add your register offset.

So, for instance, if you want to write the value 0x1f to the register 0x1c, you will do the following:

iowrite32(0x1f, i2c_reg+0x1c);

Similarly, if you want to read a register, you will do something like the following:

status = ioread32(i2c_reg+0x10) & 1;

You will see that in the standalone code, we are using two different operators for doing a read and write from a register: Xil_In32 and Xil_Out32. So when using this code in Linux remember to convert it to ioread32 and iowrite32 respectively. Also, remember that the parameter order of iowrite32 is different than that Xil_Out32: In Linux it is value followed by address and Xil_Out32  starts with the address, followed by the value.

If you want find the final source for the Linux Kernel driver, just go here: https://github.com/ovalcode/c64fpga/tree/master/SDK.src/linux

Another fine difference between the standalone code and the Linux code, is the operator we use for introducing a delay. In the standalone code I use usleep, where the value should be microseconds. Also, in Linux I am using msleep, where the value should be in milliseconds.

This is about all the changes required to initialise the SSM2603 from Linux.

When equivalent isn't exactly equivalent

In the previous post I have basically copy and pasted the SSM2603 initialisation code to our Linux Kernel driver, with minor changes.  So, in theory, this code should just worked in our Linux Driver.

However, when I tried this code for the first time, the SSM2603 didn't initialise at all.

After some investigation, I found that the driver got stuck in the following loop within the method writeReg:

         do {
          status = ioread32(i2c_reg+0x10) & 1;
         } while (!status);

This loop checks one of the status bits of I2C 1, which is set as soon as the I2C have shifted out a chunk of information.

This really confused me, because the equivalent code on the Bare-Metal application worked perfectly.  I tried thinking of a couple of possibilities that was causing this.

Firstly I wanted to know if the pins from I2C really got routed to EMIO instead of one or other MIO pin.

So armed, with the XSCT console, I inspected the memory locations for MIO routing and comparing the same registers to when Linux was running.

Maybe I should just take a step back and give an some background to the checks I was doing.

The MIO registers provides you with some options to route the I2C pins to different external pins. As an example, please have a look on page 1643 of the Zynq Technical Reference manual.

This register gives you the option to route the Serial Clock pin of the I2C 1 to pin 12. Similar registers exists, like 0xF8000734 and 0xF8000740, which allows you to route I2C pins to other MIO pins.

When I inspect these registers when running the Bare-Metal application, I found that no MIO pin is configured for surfacing any I2C 1 pin. This is exactly what I expect.

When inspecting the same registers when running Linux, I got the same result.

This inspection let to a bit of a dead-end, but there is still a question remaining: How do enable pins to get routed through EMIO?

At first sight I couldn't find any information about this in the Zynq TRM. However, the diagram on page 49 provided some subtle information for me.

As seen on that diagram, all peripherals go into the MIO, which performs the necessary multiplexing as describe earlier.

Some of these peripherals are also connected to the EMIO. From the diagram it looks like the connections to EMIO are direct, so in theory these pins should always be available to the FPGA.

Once again I reached a dead-end. What else could be the reason why the the SSM2603 doesn't initialise in Linux?

I did a further search in the Zynq TRM PDF to see if I could locate any other registers that is related to the I2C peripheral, and eventually I came across the register APER_CLK_CTRL on page 1586. Specifically, my eyes ctached the following phrase:

Please note that these clocks must be enabled if you want to read from the peripheral register space.
This could be indeed the problem, we are trying to read the status, but we are not getting any sensible data back, because the AMBA clock is not enabled for I2C 1. For clocking to be enabled for I2C 1, bit 19 should be set for register APER_CLK_CTRL.

I could confirm that this bit was set when the Bare-Metal application was running, and, as a relief, I could confirm that bit wasn't set when running Linux!

This is luckily an easy fix!

...
static void __iomem *clk_reg;
...
static int __init ebbchar_init(void){
...
clk_reg = ioremap(0xF8000000, 0x200);
...
iowrite32(0x01EC044D, clk_reg+0x12c);
msleep(2);
init_sound();
...
}
...

I introduced a small delay after setting the bit in the register APER_CLK_CTRL.

After the fix the SSM2603 initialisation worked in Linux. One can quickly detect that the initialisation is working by hearing the speaker attached to Zybo board turning on.

In Summary

In this post we looked into how to initialise the SSM2603 for sound in Linux.

This set of blog posts on how to create a C64 on a Zybo is quickly running to an end, since I have achieved more or less all my goals with it.

That been said, there is so much more you can use the Zybo board for, some of these of which I also want to write Blog posts about.

I also want to write some posts about using the C64 outside of a FPGA context, like writing a Chess Engine.

So, in coming posts I will introduce some variety, that might not be related to implementing a C64 on on FPGA.

I might, however, revist the topic of C64 on an FPGA from time to time, for instance implementing a 1541 running in parallel with the C64 in the FPGA.

Till next time!

Tuesday 14 April 2020

Mapping joystick bits from Linux

Foreword

Previously we managed to load a tape image from the Linux File system, and got our C64 core to load it.

In this post we will add some more key mappings for a joystick, so we can play the game that loads.

Making our C64 design accept two joystick inputs

Currently our C64 FPGA design have only implemented Joystick Port #2. Having implemented functionality to rapidly switch between different game tape images, it actually becomes more of a requiremnt to implement Joystick Port #1 as well.

So, let us start with this requirement by looking into the C64 FPGA design.

Our starting point is our IP block that contains both an Master and Slave AXI port. This block we need to modify so that it can get an additional output port for Joystick Port #1:


To wire up this port, we need to make the following adjustments to the user logic of our AXI Slave block:

 // 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];
    assign joybits2 = slv_reg2[14:10];

 // User logic ends

The bits of the two joystick ports are not consecutive to each other, due to the c64_mode in between. This will be a source of interesting bit manipulation in our driver, as will be seen later.

On our C64 core IP, we need to add an extra input port to accept this extra Joystick input. In our C64 core we supply the bits of the second joystick port to port B of CIA#1.

Port B of CIA#1, however, is also used as an from our keyboard. Thus we need to merge the lower four bits of the keyboard input bits with the bits from our second joystick port.

We do this as follows:

...
    assign key_joy_merged = ~(~keyboard_result[4:0] | ~joybits2);
...     
    cia cia_1(
          .port_a_out(keyboard_control),
          .port_a_in({3'b111, joybits}),
          .port_b_in({keyboard_result[7:5],key_joy_merged}),
          .addr(addr[3:0]),
          .we(we & io_enabled & (addr[15:8] == 8'hdc)),
          .clk(clk_1_mhz),
          .chip_select(addr[15:8] == 8'hdc & io_enabled),
          .data_in(ram_in),
          .data_out(cia_1_data_out),
          .flag1(flag1 & !flag1_delayed),
          .irq(irq)
            );
...

This is all the changes we require for the second joystick port for our FPGA design

Changes to the driver

Let us see what changes is required to our driver to accommodate two joystick ports.

Firstly we need to modify our record structure for input events as follows:

struct keyboard 
{
           u32 word1;
           u32 word2;
           u32 joybits;
};


Joybits will store the bits for both joystick ports.

Next, I will make a couple of changes to our write method:

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, 12);
   iowrite32(temp[0].word1, c64_reg_keyboard_0);
   iowrite32(temp[0].word2, c64_reg_keyboard_1);
   unsigned int tempjoy = temp[0].joybits & 0x3ff;
   tempjoy = ~tempjoy & 0x3ff;
   unsigned int joy_high = (tempjoy << 1) & 0x7c0;
   unsigned int joy_low = tempjoy & 0x1f;
   joy_high = joy_high | joy_low;
   joy_high = joy_high << 4;
   unsigned int screenread = ioread32(c64_reg_screen_mode) & 0xffff820f;
   screenread = screenread | joy_high;
   iowrite32(screenread, c64_reg_screen_mode);
   
   return 12;
}


So, we isolate the bits of both ports and shift them to the correct position. We also need to shift the data of the two joystick ports one bit apart, because they are separated by the c64_mode bit.

Changes to the application

In our application, we make use of a file called sym.txt to indicate the resulting c64 scancode for each mapped key.

The real question here is: How do we indicate that a set of mapped keys is purposed for a Joystick? For this purpose, I am going to use the value -3:

84 -3 0 0                                            
17 -3 1 0                                              
87 -3 2 0                                              
89 -3 3 0                                              
19 -3 4 0                                              

The third value indicates the relevant bit that should be set for the joystick.

You might also remember that we use the file sym.txt to populate an array called key_map, containing c64 scancode, where the row and column values are reduced to a single scan code value between the range 0 to 63.

For the joystick, we can just extend the range, so scancode 64 can be joystick bit 0, 65 joystick bit 1 and so on.

With this in mind, we can change the method init_table as follows:

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 (num2 == -3) {
      key_map[num1][0] = num3 + 64;
      key_map[num1][1] = 0;
    } else 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;
    }
  }
}


We handle these codes in the key processing loop as follows:

    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 > 63) {
        keyToProcess.joybits = keyToProcess.joybits | (1 << (c64_scan_code - 64));
      } else 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);
      }
    }

How do we switch between different joystick ports? A very simple mechanism would be to specify an extra parameter on the commandline. We can then just test for the number of arguments:

    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 > 63) {
        if (argc == 3)
          c64_scan_code = c64_scan_code + 5;
        keyToProcess.joybits = keyToProcess.joybits | (1 << (c64_scan_code - 64));
      } else 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);
      }
    }

This conclude the code changes we should make to implement joystick ports.

In Summary

In this post we have implemented the necessary code changes for mapping keys to joystick ports in Linux.

In the next post we will write some extra code for our driver to initialise the sound system  so we can hear sound generated by our C64 core!

Till next time!

Friday 10 April 2020

Blog Series Update: Source on Github Repo

Foreword

Good day all! In this post, I will not be doing a technical discussion, but rather a bit of an update on this Blog Series.

From almost the beginning of this Blog series, I maintained a Github Repository of the source code presented throughout the series.

I must confess that I haven't updated this repo with source code presented for quite a long time (more than a year!).

The reason for this is mainly because when working with Block Designs in Vivado, you end off with quite a bit of auto generated files during Synthesis, which can differ quite a bit from one synthesis run to the next.

This just made the whole exercise of maintaining changes on the Github repo quite a nightmare.

Also, what made matters worse, was that when you cloned the repo and try to synthesise the design, one always end off with some errors on the AXI blocks. Eventually you just end off deleting these AXI blocks, re-adding them in the block design, followed by wiring them up to the rest of the design.

In recent months I received quite a number queries from asking if I can update the source on my Github Repo.

So, in this lockdown period I am having in my country, and I belief many other countries around the world, I set forth to update update the Github Repo for this Blog series.

I also gave some thought into how to simplify the build process, which I will explain in this post.

Gearing up

Let us start this post by just giving the address again for the Github Repo that hosts the source code for this Blog Series:

https://github.com/ovalcode/c64fpga

This Repo also contains a readme telling you how to get the source code and how to build the project.

In this Readme I am also explaining the issue with AXI blocks as I explained in the Foreword.

It is here I found a way to improve the build process a bit. You will also start by running the tcl file to generate the project files, after which you will open the generated project.

Next you will start off the Synthesis of the project. As previously you will see quite a number of errors appearing, but just wait till the whole process completes.

Afterwards you IDE will something like the following:


Notice that at this point I have the Design Runs tab open showing that all Out-of-Context Runs that have failed.

This window showing the errors, will also give you the solution to fixing all these issues.

Now, right click on the first error node and select Open elaborated Design. The IDE will be busy for a minute or two, after which this node will change from an error icon to a check icon.

Next, one needs to following the same set of steps for the remaining items. There is about 13 of these items, so it will feel a bit cumbersome, taking about half an hour.

However, this only needs to be done once after a clone.

After finished doing the Open Eloborate design on all items, synthesis and Generate BitStream should work without an issue.

What the source code contains

At this point the Github Repo contains all the necessary sources for generating the FPGA bitstream you can use in the previous set of posts, that will give you a VGA output of the Linux console and optionally switching to the C64 screen.

If you would rather go directly to the C64 screen on power up, you can add a Constant block to the block design, supplying the value '1' to the C64_screen_mode port on the VGA block.

Currently, this Github doesn't provide source code that will generate the necessary ARM code for booting into Linux or a baremetal design. In the future, however, I might add the necessary source code for this to the GIT repo.

What is also missing from the source code is the IP block that send the generated SID audio to the sound chip on the Zybo board.

In Summary

In this post I have given an update on the source code for this Blog series on the Github Repo.

See you in the next post, and stay safe!
 

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!