Monday 17 December 2018

Redirecting USB keystrokes to C64

Foreword

In the previous post we managed to catch the scan codes of keys pressed on a USB keyboard.

In this post we will be redirecting these keystrokes to our C64 module so we can have some meaningful interaction with our C64 module.

The Plan of Action

Let us start by refreshing our minds a bit.

A couple of posts ago we implemented two slave registers which we mapped into memory space at locations 43c0_0000 and 43c0_0004.

Combining these two slave registers we have 64 bits in which each of these bits represents a key on the C64 keyboards. The ARM can toggle the bits in these registers and in effect simulate key presses within our C64 module.

All it will take from us is to take the USB keyboard scan codes we receive from the keyboard, and converting it to C64 key scan codes and we have a working implementation.

Starting simple

Let us start by implementing a mapping between USB and C64 keyboard for just four keys: A, B, C, D.

The USB scan codes for these keys are as follows:


  • A -> 4
  • B -> 5
  • C -> 6
  • D -> 7
The corresponding scan codes for these keys on a C64 is as follows:

  • A -> 0xa
  • B -> 0x1c
  • C -> 0x14
  • D -> 0x12
We can create a quick mapping function for these keys:

u32 mapUsbToC64(int usbCode) {
 if (usbCode == 0x4) {
  return 0xa;
 } else if (usbCode == 0x5) {
  return 0x1c;
 } else if (usbCode == 0x6) {
  return 0x14;
 } else if (usbCode == 0x7) {
  return 0x12;
 }
}


We will invoke this method within our state_machine method where are printing the USB scancodes to the console:

...
  if (!(Xil_In32(qTDAddressCheck + 8) & 0x80)) {
   u32 word0 = Xil_In32(0x305000);
   u32 word1 = Xil_In32(0x305004);
   if (word0 == 0)
    Xil_Out32(0x43c00000, 0);
   else {
    u32 bit = mapUsbToC64((word0 >> 16) & 0xff);
    bit = 1 << bit;
    Xil_Out32(0x43c00000, bit);
   }
...

Here we set the corresponding bit in the slave register according to the returned c64 scan code.


Here is a demonstration of our code in action:



Our mapUsbToC64 can now just be extended to cover the other keys.

As it stands, our current implementation only support the first 32 c64 scancodes. So let us quickly do some changes to cover the full 64 scancodes:

...
  if (!(Xil_In32(qTDAddressCheck + 8) & 0x80)) {
   u32 word0 = Xil_In32(0x305000);
   u32 word1 = Xil_In32(0x305004);
   if (word0 == 0) {
    Xil_Out32(0x43c00000, 0);
    Xil_Out32(0x43c00004, 0);
   } else {
    u32 bit = mapUsbToC64((word0 >> 16) & 0xff);
    //bit = 1 << bit;
    u32 c64Word0 = 0;
    u32 c64Word1 = 0;
    if (bit < 32) {
     c64Word0 = 1 << bit;
    } else {
     c64Word1 = 1 << (bit - 32);
    }

    Xil_Out32(0x43c00000, c64Word0);
    Xil_Out32(0x43c00004, c64Word1);
   }
   printf("%x %x\n",word0, word1);
...

So, if the scancode is less than 32 we set the appropriate bit at address 0x43c0_0000. For scancodes bigger than 32 we set the appropriate bit at address 0x43c0_0004.

Implementing simultaneous key presses

Up to this point in time we are only able to deal with one key press at a time. This becomes an issue when we want to type double quotes (") on the C64, which require pressing the shift and the 2 key simultaneously.

In this section we will deal with simultaneous key presses.

Luckily from the USB side we are provided with enough information to determine if more than one key is pressed simultaneously. Each byte from the 8 bytes returned in the USB report descriptor represent a key that is currently been pressed. The exception to the rule is modifier keys, like Shift and Control. The status of all the modifier keys is contained within a single byte, where is bit corresponds to a modifier key.

We start off by creating a method we are sending the 8 USB bytes and returning the values we need to assign to addresses 0x43c0_0000 and 0x43c0_0004 respectively:

void getC64Words(u32 usbWord0, u32 usbWord1, u32 *c64Word0, u32 *c64Word1) {

}

We implement two loops for looping through both USB words:

void getC64Words(u32 usbWord0, u32 usbWord1, u32 *c64Word0, u32 *c64Word1) {
  *c64Word0 = 0;
  *c64Word1 = 0;

  usbWord0 = usbWord0 >> 16;

  for (int i = 0; i < 2; i++) {
   int current = usbWord0 & 0xff;
   if (current != 0) {
     int scanCode = mapUsbToC64(current);
        if (scanCode < 32) {
     *c64Word0 = *c64Word0 | (1 << scanCode);
     } else {
     *c64Word1 = *c64Word1 | (1 << (scanCode - 32));
     }

   }

   usbWord0 = usbWord0 >> 8;
  }

  for (int i = 0; i < 4; i++) {
   int current = usbWord1 & 0xff;
   if (current != 0) {
     int scanCode = mapUsbToC64(current);
        if (scanCode < 32) {
     *c64Word0 = *c64Word0 | (1 << scanCode);
     } else {
     *c64Word1 = *c64Word1 | (1 << (scanCode - 32));
     }

   }

   usbWord1 = usbWord1 >> 8;
  }

}


You will see that for the first USB word we are discarding the first two bytes. This is because the first byte is the byte mask for the modifier keys and the second byte is reserved.

Talking about the modifier keys. It would be nice to implement the shift key in order to type the double quotation (") in our C64 module. So let us do that quickly:

void getC64Words(u32 usbWord0, u32 usbWord1, u32 *c64Word0, u32 *c64Word1) {
  *c64Word0 = 0;
  *c64Word1 = 0;

  if (usbWord0 & 2) {
   *c64Word0 = 0x8000;
  }
...
}

In the USB report the left shift key is bit 2 of the modifier byte. So this is why we are masking off this bit.

Let us now do a test run. In the following video I write a very simple basic program and run it:


This conclude this post.

In Summary

In this post we integrated the USB keyboard with our C64 module.

We then tested everything by writing a very simple basic program and running it.

Till next time!

Tuesday 4 December 2018

Catching keystrokes from a USB keyboard

Foreword

In the previous post we managed to read a couple of descriptors from a USB keyboard and identified which endpoint to use for capturing the keystrokes from the keyboard.

In this post we will develop some code for actually retrieving the keystrokes from the keyboard.

Moving to the configured state

For the majority of the previous post we lingered within the default state. Just to refresh our minds again of the other states for a USB device, have a look a gain at a USB state diagram.

As you can see, after the Default state there is still two states, Address and Configured, we need to go through until we can do something useful with the USB device.

Let us start by having a look at the Address state. In this state we assign an address to our USB device so so that it stops listening at the default address (e.g. 0).

To set the address, we need to make a bit of changes to our state_machine method:

void state_machine() {
 //bit 24 bit 18
 u32 in2 = Xil_In32(0xE0002144) | (1<<24) | (1<<18);
 Xil_Out32(0xE0002144, in2); //clear

 if (status == 0) {
  set_port_reset_state(1);
  scheduleTimer(12000);
  status = 1;
  return;
 } else if (status == 1) {
  set_port_reset_state(0);
  status = 2;

  //set address
  Xil_Out32(0x301000, 0x00030500);
  Xil_Out32(0x301004, 0x00000000);
  schedTransfer(1,0,0x0, 0x300000);
  return;
 } else if (status == 2) {
  scheduleTimer(3000);
  status = 3;

  return;

 }
}

Here we are setting up a request type 5, which is SET_ADDRESS, and we are setting the device to address 3. We end by waiting 3 milliseconds just make sure everything settled down before we continue.

Let us now configure the device. For his we implement an extra status in our if-else block:

else if (status == 3) {
  in2 = Xil_In32(0x300004) | 3;
  Xil_Out32(0x300004, in2);

  //set configuration
  Xil_Out32(0x301000, 0x00010900);
  Xil_Out32(0x301004, 0x00000000);
  schedTransfer(1,0, 0, 0x300000);
  status = 4;
        return;
 } 

You will see that we adjust the device address in our first address to 3, because the address was changed in a previous state.

Next we select the appropriate configuration. In the previous post we determined that we should select configuration number #1.

At this stage our USB device is fully configured and ready to use.

A brief pause at periodic schedules

With our USB keyboard ready to use, the next thing obviously for us is to read the keystrokes.

My first take on reading these keystrokes was to also implement an asynchronous schedule. However, with this approach I didn't had any luck at all. Things worked better for me using Periodic schedules.

So, in this section let us spend some time discussing in more detail how periodic schedules work.

Firstly, let us look again at the diagram of how periodic schedules work. Have a look again at the diagram on page 446 of the Zynq TRM.

From the diagram we see that everything is driven of a periodic frame list been referenced in part by a 
FrameIndex, which is updated at the end of each USB frame.

A USB Frame is basically a time period of 1 millisecond.

If you look into further detail on when the FrameIndex gets updated, you will see that strictly speaking the frameindex isn't updated every millisecond, but every 1/8 millisecond. Furthermore you will see that the bottom 3 bits of the FrameIndex is not used to index the Periodic frame list, but rather from bit 3 upwards of the frameindex.

At this point you my be wondering why the frameindex get incremented every 1/8 of a millsecond if the rest of the system only work in increments of 1 millisecond.

The answer is to maintain a bit of compatibility between USB 1.1 and USB 2.0. USB 1.1 always had frames of 1 millisecond in duration. USB 2.0 introduced the concept of microframes, breaking a framedown into even smaller durations of 1/8 milliseconds.

But, despite my explanation, how can you access 1/8 millisecond frames if the frame index, for all basic reasons, only gets incremented every 1 milliseconds? The key to this questions lies in the lower 8 bits of word 2 in a QH.

From the EHCI spec these 8 bits is referred to as the Interrupt schedule mask. Every bit in this byte correspond to a specific microframe within the frame. A one in any particular position means that the transaction will take place within the particular microframe.

If only one bit is set within the Interrupts schedule mask, only one transaction will execute within the frame. Similarly, if more than one bit is set, more than one transaction will trigger within the frame.

Let us now talk a bit about the data structures a Period Frame List points to. A Periodic Frame List also points to QH/qtd structures as an Asynchronous List does.

In fact, it is very convenient to think of each element in a Periodic Frame List as an Asynchronous list on its own. In this analogy, each element of the Periodic Frame List can be thought of as an ASYNCLISTADDR-register on its own.

There is , however, a small flaw in this analogy. In an Asynchronous schedule the ASYNCLISTADDR-register gets updated during traversal to always point to the next QH in the list. In a periodic schedule, however, each element in the Periodic Frame List always point to the first QH element in the list.

As such, within a periodic schedule a circular QH list doesn't make sense. 

Configuring the Periodic Schedule

Let us now write some code for scheduling the periodic schedule.

Firstly we need to specify the number of elements of our Periodic Frame List. We want to poll once every 16 milliseconds. Since each element have duration of 1 millisecond, it makes sense to have sixteen elements with only one of these elements pointing to a valid QH.

To set the frame list size we make use of three bits of register 0xe0002140: 15, 3 & 2.  These three bits gets grouped together as [15][3][2] and has the following meaning:

  • 000: List size is 1024 elements
  • 001: List size is 512 elements
  • 010: List size is 256 elements
  • 011: List size is 128 elements
  • 100: List size is 64 elements
  • 101: List size is 32 elements
  • 110: List size is 16 elements
  • 111: List size is 8 elements
From the above list we should use the value 110 which corresponds to the following code:

void setup_periodic() {
  u32 in2 = Xil_In32(0xE0002140) | (1<<15) | 8;
  Xil_Out32(0xE0002140, in2);

}

Our List will reside at address 0x304000, so we initialise this area and set the Periodic address base register:

void setup_periodic() {
...
 Xil_Out32(0x304000, 1);
 Xil_Out32(0x304004, 1);
 Xil_Out32(0x304008, 1);
 Xil_Out32(0x30400c, 1);
 Xil_Out32(0x304010, 1);
 Xil_Out32(0x304014, 1);
 Xil_Out32(0x304018, 1);
 Xil_Out32(0x30401c, 1);
 Xil_Out32(0x304020, 1);
 Xil_Out32(0x304024, 1);
 Xil_Out32(0x304028, 1);
 Xil_Out32(0x30402c, 1);
 Xil_Out32(0x304030, 1);
 Xil_Out32(0x304034, 1);
 Xil_Out32(0x304038, 1);
 Xil_Out32(0x30403c, 1);

 Xil_Out32(0xE0002154, 0x304000);
}

We start off the initialisation by setting all pointers to invalid pointers. We then set the one of these pointers to a valid one:

void setup_periodic() {
...
 struct QStruct *qh;
 qh = 0x204040;
 qh->word0 = 0x304082;
 qh->word1 = 0;
 qh->word2 = 0;
 qh->word3 = 0;
 qh->word4 = 1;
 qh->word5 = 1;

 qh = 0x204080;
 qh->word0 = 1;
 qh->word1 = 0x00085103; 
 qh->word2 = 0x40000001;
 qh->word3 = 0;
 qh->word4 = 0x204100;
 qh->word5 = 1;

 struct QStruct *qTD;
 qTD = 0x204100;
 qTD->word0 = 1; 
 qTD->word1 = 1; 
 qTD->word2 = 0x00080180; 
 qTD->word3 = 0x305000;

 //set first frame to qh
 Xil_Out32(0x304000, 0x304042);
}

You might find it a bit strange that we start with a QH that doesn't contain any qTD's at all, followed by a QH that does have them. I will explain the reasoning behind this a bit later on.

You will also see that the NAK count reload field for the second QH is zero. You might recall that for our asynchronous Schedule this was always 15. Why the difference?

To answer this question let us first look at what a NAK packet is.

When a USB host request data from a USB device and the device doesn't have any data available it will respond with a NAK packet. Sometimes you would like to throw an error if a certain number of NAK packets is received in a row. This is the purpose of the NAK reload field.

In our case we just would like to ignore these packets all together, so we set the RL field to zero. In our schedule when a NAK packet is encountered the slot will just be ignored and be moved on to the next slot.

What is left to be done is to enable the periodic schedule by adding another state within state_machine:

 } else if (status == 5) {
  //enable periodic scheduling
  setup_periodic();
  in2 = Xil_In32(0xE0002140) | 16;
  Xil_Out32(0xE0002140, in2);
  status = 6;
  scheduleTimer(10000);
  return;
 }

As can be seen, we schedule a wait of 10 milliseconds before we transition the next state.

Reading the actual keystrokes

Let us now write some code for capturing keystrokes from the USB keyboard.

The basic idea is to display the keycode each time a key is pressed or released.

In the previous post we have set up the periodic scheduled with a scheduled qTD transfer in one slot.

We should poll this qTD datastructure till the transfer is finished, which happens when bit 7 of word 2 change to 0. We implement this functionality with an extra state:

 } else if (status == 6) {
  if (!(Xil_In32(0x304100 + 8) & 0x80)) {
   u32 word0 = Xil_In32(0x305000);
   u32 word1 = Xil_In32(0x305004);
   printf("%x %x\n",word0, word1);
  }

  scheduleTimer(10000);
  return;
 }

We are polling the qTD datastructure at every 10 milliseconds. Once transfer is finished we will get the keystroke information at the first eight bytes at location 0x305000.

Once the scheduled transfer is finished, a new transfer would not be automatically scheduled. It is up to you to schedule a new one.

One could probably just reset the values in the qTD to restart a new transfer. Doing this we may end off with a potential cache coherency issue. 

Changes you made to QH and qTD structures are written to System Memory. The DMA block within the USB controller reads these reads these changes from Main Memory from time to time into internal Dual-Port RAM.

One cannot tell at which stage the USB Controller is reading from System memory and half baked qTD datastructures might end up into the Dual port RAM.

The solution to this issue is to not modify these structures but to create new structures:

 } else if (status == 6) {
  u32 qTDAddress = currentTD ? 0x304100 : 0x304120;
  u32 qTDAddressCheck = currentTD ? 0x304120 : 0x304100;

  if (!(Xil_In32(qTDAddressCheck + 8) & 0x80)) {
   u32 word0 = Xil_In32(0x305000);
   u32 word1 = Xil_In32(0x305004);
   printf("%x %x\n",word0, word1);
   struct QStruct *qh;
   qh = 0x304040;
   qh->word0 = 1;

   struct QStruct *qh2;
   qh2 = currentTD ? 0x304080 : 0x3040c0;

   qh2->word0 = 1;
   qh2->word1 = 0x00085103; 
   qh2->word2 = 0x40000001;
   qh2->word3 = 0;
   qh2->word4 = qTDAddress;
   qh2->word5 = 1;

   struct QStruct *qTD;
   qTD = qTDAddress;
   qTD->word0 = 1; 
   qTD->word1 = 1; 
   qTD->word2 = 0x00080180;
   qTD->word3 = 0x305000;
            u32 temp = qh2;
            temp = temp | 2;
   qh->word0 = temp;

   currentTD = ~currentTD;
  }

  scheduleTimer(10000);
  return;
 }

As you see, this is where our QH comes in which contains no qTD's. Once we have created a new QH and qTD we just change the next pointer of the first mentioned QH.

One thing to also keep in mind when a transfer is complete, is to preserve the Data toggle bit and apply it to the new qTD. This is done as follows:

...
  u32 toggle = Xil_In32(qTDAddressCheck+8) & 0x80000000;
  if (!(Xil_In32(qTDAddressCheck + 8) & 0x80)) {
...
   qTD->word2 = 0x00080180 | toggle;
...
                }
...

All the developed code should now be sufficient to capturing the keystrokes continuously and outputting to the console.

The meaning of USB keycodes

As mentioned in the previous section, each key press will result in 8 bytes been populated at address 0x305000. However only the last 6 bytes is significant to us.

Each value of these six bytes represent a keycode of a key that is currently pressed. This means that up to 6 keys can be pressed simultaneously.

USB key scan codes is a bit different than your convential PS/2 codes in that it is more predictable. For instance have a look at the USB scan codes for the first couple of alphabet letters:


  • Key A: scancode 4
  • Key B: scancode 5
  • Key C: scancode 6
  • Key D: scancode 7
  • Key D: scan code 8
  • Key E: scancode 9
etc.

In summary

In this post we implemented some code for catching keystrokes from the USB keyboard.

In the next post we will integrate the USB keyboard with our C64 module.

Till next time!