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.

// wait for device to settle and then reset
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:

  • Control transfers — send a defined request to the device, for example to read information about a device or select configuration settings
  • Interrupt transfers — low latency data, for example keypresses or mouse movements; also the the only way for low-speed devices to send data
  • Bulk transfers — data where a delay can be tolerated, but the fastest way to send data if the bus is idle
  • Isochronous transfers — guaranteed delivery time, but no error correction, so useful for streaming audio and video

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

  • The request type, i.e, 'vendor', 'standard' or 'class'
  • The request itself, for example 0x06 is USB_REQUEST_GET_DESCRIPTOR
  • A value field with two bytes containing request-specific information
  • A length field specifying the number of data bytes

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.