Maker of open-source software and hardware.

USB host shield on Espruino: Part 3

Feature image

Now that we can connect to a device using the USB controller chip with Espruino, we can start reading the USB configuration and device descriptors. These descriptors give us more information about the device, for example its USB vendor ID and product ID. We need to follow the following series of steps before we can start sending and receiving data:

  1. According to the USB spec we first need to wait 200ms after connecting to the device and then reset the device bus.
  2. We then wait until the device is reset and start the Start-Of-Frame (SOF) marker. This is a timing reference that is sent at 1 ms intervals at full speed.
  3. According to the USB spec we then need to wait 20 ms after the device is reset and check if the SOF marker was received.
setTimeout( () => {
  console.log('Resetting device bus');
  this.setRegister(REGISTERS.HCTL, HCTL.BUS_RESET);
  while((this.readRegister(REGISTERS.HCTL) & HCTL.BUS_RESET) !== 0) { }

  console.log('Start SOF');
  const result = this.readRegister(REGISTERS.MODE) | HCTL.SOFKAENAB;
  this.setRegister(REGISTERS.MODE, result);

  setTimeout( () => {
    if (this.readRegister(REGISTERS.IRQ) & IRQS.FRAME) {
      // first SOF received
      console.log('SOF received');

      // get descriptors here

    }
  }, 20); // USB spec says wait 20ms after reset
}, 200);

As soon as we have the first SOF marker, we can start reading the descriptors using a control transfer. To send and receive data, everything in USB land happens in terms of transfers, of which there are four types:

To do a control transfer, we need to set the address register and create an eight-byte setup packet containing the following:

In the following code example we also introduce two other functions. sendBytes(register, bytes) is used to send multiple bytes, which we place into the USB chip's SUDFIFO register. After that dispatchPacket(token, ep) sends the packet on its way.

sendBytes(register, bytes) {
 SPI1.write(E.toUint8Array(register | 0x02, bytes), SS);
}

dispatchPacket(token, ep) {
 let nakCount = 0;
 let retryCount = 0;

 const abortTimer = setTimeout( () => {
   console.log('Timeout error');
   return(new Error('Timeout error'));
 }, 5000);

 this.setRegister(REGISTERS.HXFR, token | ep);
 while (this.readRegister(REGISTERS.IRQ) & IRQS.HXFR_DONE === 0) { }

 // clear interrupt
 clearTimeout(abortTimer);
 this.setRegister(REGISTERS.IRQ, IRQS.HXFR_DONE);

 const transferResult = this.readRegister(REGISTERS.HRSL) & 0x0f;

 if (transferResult === HRSL.NAK) {
   console.log('NAK');
   nakCount++;
   if (nakCount > USB_NAK_LIMIT) {
     return(new Error('NAK error'));
   }
 } else if (transferResult === HRSL.TIMEOUT) {
   console.log('Timeout, retrying..');
   retryCount++;
   if (retryCount > USB_RETRY_LIMIT) {
     return(new Error('Timeout error'));
   }
 }
 return transferResult;
}

controlTransferIn(setup, length) {
 const addr = 0;

 this.setRegister(REGISTERS.PERADDR, addr);

 let setupPacket = new Uint8Array(8);
 setupPacket[0] = REQUEST_TYPE[setup.requestType];
 setupPacket[1] = setup.request;
 storeShort(setup.value, setupPacket, 2);
 storeShort(setup.index, setupPacket, 4);
 setupPacket[6] = length;
 console.log('Setup packet:', bytes2hex(setupPacket));

 this.sendBytes(REGISTERS.SUDFIFO, setupPacket);
 let err = this.dispatchPacket(TOKENS.SETUP, 0);
 if (err) {
   console.log('Setup packet error:', err);
 }
}

Now that we have the control transfer set up and sent to the device, we need to transfer the data bytes in. This is done in the transferIn(ep, length) function. We first set the receive toggle , which is used for USB signaling. We then dispatch the IN packet to indicate that we want to read the data bytes. We then check that the data is available by checking the IRQ register for IRQS.RECEIVED_DATA_AVAILABLE. We then ready the packet size from the RCVBC register, and finally read the data bytes themselves from the RCVFIFO register. We clear the interrupt and continue reading until all the data bytes are read

transferIn(ep, length) {
  let transferLength = 0;
  let data = new Uint8Array(length);

  this.setRegister(REGISTERS.HCTL, receiveToggle);
  while (1) {
    const err = this.dispatchPacket(TOKENS.IN, ep);
    if (err) {
      console.log('Error:', err);
      return(err);
    }
    if (this.readRegister(REGISTERS.IRQ) & IRQS.RECEIVED_DATA_AVAILABLE === 0) {
      return(new Error('Receive error'));
    }
    const packetSize = this.readRegister(REGISTERS.RCVBC);
    console.log('Packet size:', packetSize);
    data.set(this.readBytes(REGISTERS.RCVFIFO, packetSize), transferLength);
    this.setRegister(REGISTERS.IRQ, IRQS.RECEIVED_DATA_AVAILABLE); // clear interrupt
    transferLength += packetSize;

    if ((packetSize < 8) || (transferLength >= length)) {
      return { data: data };
    }
  }
}

OK, now that we have defined all the functions we'll need, we can specify the setup packet to get the device descriptor and perform our control transfer:

const getDeviceDescriptor = {
    requestType: 'vendor',
    recipient: 'device',
    request: 0x06, // USB_REQUEST_GET_DESCRIPTOR
    value: 0x0100, // USB_DESCRIPTOR_DEVICE = 0x01
    index: 0x0000
};

usb.controlTransferIn(getDeviceDescriptor, 18);
let results = usb.transferIn(0, 18);
console.log('Get device descriptor:', results.data);

Note that we already know that the device descriptor is 18 bytes long. The configuration descriptor can be of variable length, so we start with reading the first four bytes, which contain the length, and then do another control transfer to retrieve all the data bytes:

const getConfigDescriptor = {
  requestType: 'vendor',
  recipient: 'device',
  request: 0x06, // USB_REQUEST_GET_DESCRIPTOR
  value: 0x0202, // USB_DESCRIPTOR_CONFIGURATION = 0x02, conf = 0x02
  index: 0x0000
};

// Get config descriptor length
usb.controlTransferIn(getConfigDescriptor, 4);
results = usb.transferIn(0, 4);
const configDescriptorLength = extractShort(results.data, 2);
console.log('Config descriptor length:', configDescriptorLength);

usb.controlTransferIn(getConfigDescriptor, configDescriptorLength);
results = usb.transferIn(0, configDescriptorLength);
console.log('Data:', results.data);

Wow, that was quite a bit of code! Well, at least we will be able to re-use quite a few of the functions when we need to perform the other USB transfers. If you'd like to read more about USB, I can definitely recommend Jan Axelson's book USB Complete.

#Electronics #Tidepool