Gerrit Niezen
Published © MIT

Adding USB Host Support to Espruino

Connect USB devices to your Espruino using the MAX3421E chip.

AdvancedWork in progress2 hours1,580
Adding USB Host Support to Espruino

Things used in this project

Hardware components

Espruino Pixl.js
Espruino Pixl.js
×1
SparkFun USB Host Shield
×1
SparkFun Arduino Stackable Header Kit - R3
×1

Software apps and online services

Espruino Web IDE

Story

Read more

Code

Espruino MAX3421E USB stack with CP2102 driver

JavaScript
Copy into the Espruino Web IDE and edit the line that begins with "cp2102.send" to the command that you want to send to the device.
const SS = D10;

const USB_NAK_LIMIT = 100;
const USB_RETRY_LIMIT = 3;

const STATE = {
  DETACHED: 1,
  ATTACHED: 2,
};

const REGISTERS = {
  RCVFIFO: 0x08,
  SNDFIFO: 0x10,
  SUDFIFO: 0x20,
  RCVBC: 0x30,
  SNDBC: 0x38,
  USB_CONTROL: 0x78,
  CPU_CONTROL: 0x80,
  PIN_CONTROL: 0x88,
  USB_IRQ: 0x68,
  IRQ: 0xc8,
  MODE: 0xd8,
  HIEN: 0xd0,
  PERADDR: 0xe0,
  HCTL: 0xe8,
  HXFR: 0xf0,
  HRSL: 0xf8,
};

const COMMANDS = {
  CHIP_RESET: 0x20,
};

const OPTIONS = {
  FULL_DUPLEX: 0x10,
  LEVEL_INTERRUPT: 0x08,
};

const REQUEST_TYPE =  {
  vendor: 0x40,
  standard: 0x00,
  class: 0xa1
};

const TOKENS = {
  SETUP: 0x10,
  IN: 0x00,
  OUT: 0x20,
  INHS: 0x80,
  OUTHS: 0xA0,
  ISOIN: 0x40,
  ISOOUT: 0x60
};

const HCTL = {
  BUS_RESET: 0x01,
  FRMRST: 0x02,
  SAMPLE_BUS: 0x04,
  SIGRSM: 0x08,
  RCVTOG0: 0x10,
  RCVTOG1: 0x20,
  SNDTOG0: 0x40,
  SNDTOG1: 0x80
};

const HRSL = {
  SUCCESS: 0x00,
  BUSY: 0x01,
  BADREQ: 0x02,
  UNDEF: 0x03,
  NAK: 0x04,
  STALL: 0x05,
  TOGERR: 0x06,
  WRONGPID: 0x07,
  BADBC: 0x08,
  PIDERR: 0x09,
  PKTERR: 0x0A,
  CRCERR: 0x0B,
  KERR: 0x0C,
  JERR: 0x0D,
  TIMEOUT: 0x0E,
  BABBLE: 0x0F,
};

const IRQS = {
  BUS_EVENT: 0x01,
  RWU: 0x02,
  RECEIVED_DATA_AVAILABLE: 0x04,
  SEND_BUFFER_AVAILABLE: 0x08,
  SUS_DONE: 0x10,
  CONNECTION_DETECT: 0x20,
  FRAME: 0x40,
  HXFR_DONE: 0x80
};

let currentAddress = 0;

const storeShort = function(v, b, st) {
    b[st] = v & 0xFF;
    b[st + 1] = (v >> 8) & 0xFF;
};

const extractShort = function(b, st) {
    return ((b[st+1] << 8) + b[st]);
};

const bytes2hex = function(bytes, noGaps) {
  var message = '';
  for(var i in bytes) {
    var hex = bytes[i].toString(16).toUpperCase();
    if(hex.length === 1) {
      message += '0';
    }
    message += hex;
    if(!noGaps) {
      message += ' ';
    }
  }
  return message;
};

class USB {
  constructor() {
    Terminal.println('Setting up SPI');
    SPI1.setup({mosi:D11, miso:D12, sck:D13, order:'msb', baud: 26000000});
    Terminal.println('Revision ' + this.readRegister(0x90));

    // TODO: get endpoints from config descriptor
    this.endpoints = {
      0 : { // control endpoint
        receiveToggle: HCTL.RCVTOG1,
        sendToggle: HCTL.SNDTOG0
      },
      1 : {
        receiveToggle: HCTL.RCVTOG0,
        sendToggle: HCTL.SNDTOG0
      }
    };
  }

  setRegister(register, command) {
    SPI1.write([register | 0x02, command], SS);
  }

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

  readBytes(register, length) {
    let bytesToRead = new Uint8Array(length);
    const result = SPI1.send([register, bytesToRead], SS);
    console.log('Got:', bytes2hex(result));
    return result.slice(1);
  }

  readRegister(register) {
    const result = SPI1.send([register, 0x00], SS);
    return result[1];
  }

  init(cb) {
    console.log('Setting full-duplex');
    this.setRegister(REGISTERS.PIN_CONTROL, OPTIONS.FULL_DUPLEX | OPTIONS.LEVEL_INTERRUPT);

    if (this.reset() === 0)
      console.log('Oscillator did not settle in time');
    console.log('Setting host mode and pulldown resistors');
    this.setRegister(REGISTERS.MODE, 0xc1); // set host mode and pulldown resistors
    this.setRegister(REGISTERS.HIEN, IRQS.CONNECTION_DETECT | IRQS.FRAME);

    // check if device is connected

    this.setRegister(REGISTERS.HCTL, HCTL.SAMPLE_BUS);
    let sampled = 0;
    while(!sampled) {
      // wait for USB sample to finish
      const res = this.readRegister(REGISTERS.HCTL);
      sampled = res & HCTL.SAMPLE_BUS;
    }

    this.busprobe();

    // clear interrupt for connection detect
    this.setRegister(REGISTERS.IRQ, IRQS.CONNECTION_DETECT);
    this.setRegister(REGISTERS.CPU_CONTROL, 0x01); // enable interrupt pin

    // wait for device to settle and then reset
    setTimeout( () => {
      console.log('Resetting device bus');
      return this.resetDevice(cb);
    }, 200);
  }

  resetDevice(cb) {
    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');
          return cb();
        }
      }, 20); // USB spec says wait 20ms after reset
  }

  reset() {
    this.setRegister(REGISTERS.USB_CONTROL, COMMANDS.CHIP_RESET);
    this.setRegister(REGISTERS.USB_CONTROL, 0x00);
    let i = 0;
    while((this.readRegister(REGISTERS.USB_IRQ) & 0x01) === 0) {
      i++;
    }
    console.log(i, 'attempts to settle');
    return i;
  }

  busprobe() {
    const JSTATUS = 0x80;
    const KSTATUS = 0x40;
    const LOW_SPEED = 0x02;

    let busSample = this.readRegister(REGISTERS.HRSL);
    busSample &= (JSTATUS | KSTATUS);

    if ((busSample === KSTATUS) || (busSample === JSTATUS)) {
      const isFullSpeed = (this.readRegister(REGISTERS.MODE) & LOW_SPEED) === 0;

      if (isFullSpeed) {
        console.log('Full-speed host');
        this.setRegister(REGISTERS.MODE, 0xC9);
      } else {
        console.log('Low-speed host');
        this.setRegister(REGISTERS.MODE, 0xcb);
      }
    } else if(busSample === 0xc0) {
      console.log('Illegal state');
    } else if (busSample === 0) {
      console.log('Disconnected');
    }
  }

  controlTransfer(direction, setup, length) {
    let ep = 0;

    let setupPacket = new Uint8Array(8);

    if (setup.recipient === 'endpoint') {
      setupPacket[0] = 0x02;
    } else if (direction === 'in' && setup.requestType === 'standard') {
      setupPacket[0] = 0x80;
    } else {
      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, ep);
    if (err) {
      console.log('Setup packet error:', err);
      return err;
    }

    return 'ok';
  }

  controlTransferIn(setup, length) {
    this.controlTransfer('in', setup, length);
    const res = this.transferIn(0, length);

    err = this.dispatchPacket(TOKENS.OUTHS, 0);
    if (err) {
      console.log('IN handshake error:', err);
    }

    return res;
  }

  controlTransferOut(setup, data, cb) {
    let res;
    let err;

    if (data) {
      console.log('Data length:', data.byteLength);
      this.controlTransfer('out', setup, data.byteLength);
      this.transferOut(0, data, (res) => {
        return cb(res);
      });
    } else {
      res = this.controlTransfer('out', setup, 0);
      this.transferIn(0, 0);
      return res;
    }
  }

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

    while (1) {
      this.setRegister(REGISTERS.HCTL, this.endpoints[ep].receiveToggle);
      console.log('Dispatching packet');
      let err = this.dispatchPacket(TOKENS.IN, ep);
      if (err) {
        console.log('Error:', err);
        return(err);
      }
      console.log('Waiting for data..');

      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;

      // TODO: use MAX_PACKET_SIZE
      if ((packetSize < 8) || (transferLength >= length)) {
        this.endpoints[ep].receiveToggle = this.readRegister(REGISTERS.HRSL) & RCVTOGRD ? HCTL.RCVTOG0 : HCTL.RCVTOG1;

        return { data: data };
      }
    }
  }

  transferOut(ep, bytes, cb) {
    const SNDTOGRD = 0x20;
    let packetSize = 8; // TODO: use max packet size from descriptor
    let bytesLeft = bytes.byteLength;
    let bytesWritten = 0;
    console.log('Sending..');

    setTimeout( () => {

      // Wait until send buffer is available
      while (this.readRegister(REGISTERS.IRQ) & IRQS.SEND_BUFFER_AVAILABLE === 0) { }

      while (bytesLeft) {
        this.setRegister(REGISTERS.HCTL, this.endpoints[ep].sendToggle);

        const nrBytes = (bytesLeft >= packetSize) ? packetSize : bytesLeft;
        const toSend = bytes.slice(bytesWritten, bytesWritten + nrBytes);
        this.sendBytes(REGISTERS.SNDFIFO, toSend);
        console.log('Sending', nrBytes, 'bytes:', bytes2hex(toSend));
        this.setRegister(REGISTERS.SNDBC, nrBytes); // set number of bytes
        const err = this.dispatchPacket(TOKENS.OUT, ep);
        if (err) {
          console.log('Transfer out error:', err); // TODO: enumerate errors
          return cb({status: err, bytesWritten: bytesWritten});
        }
        bytesLeft -= nrBytes;
        bytesWritten = bytes.byteLength - bytesLeft;

        this.endpoints[ep].sendToggle = this.readRegister(REGISTERS.HRSL) & SNDTOGRD ? HCTL.SNDTOG0 : HCTL.SNDTOG1;
      }

      return cb({status: 'ok', bytesWritten: bytesWritten});
    }, 300); // wait at least 200ms according to USB spec
  }

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

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

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

      this.setRegister(REGISTERS.IRQ, IRQS.HXFR_DONE);

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

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

  selectConfiguration(configuration) {
    const conf = {
      requestType: 'standard',
      recipient: 'device', 
      request: 0x09, // USB_REQUEST_SET_CONFIGURATION
      value: configuration,
      index: 0x0000
    };

    let results = usb.controlTransferOut(conf);
    console.log('select configuration:', results);
   }

  getConfiguration() {
    const conf = {
      requestType: 'standard',
      recipient: 'device', 
      request: 0x08, // USB_REQUEST_GET_CONFIGURATION
      value: 0x0000,
      index: 0x0000
    };

    let results = usb.controlTransferIn(conf, 1);
    console.log('get configuration:', bytes2hex(results.data));
    return results.data;
   }

  setAddress(address) {
    const setAddress = {
      requestType: 'standard',
      recipient: 'device',
      request: 0x05, // USB_REQUEST_SET_ADDRESS
      value: address,
      index: 0x0000
    };

    let results = usb.controlTransferOut(setAddress);
    console.log('set address:', results);
  }

  claimInterface(number) {
    const conf = {
      requestType: 'standard',
      recipient: 'device', 
      request: 0x0B, // USB_REQUEST_SET_INTERFACE
      value: 0x0000,
      index: number
    };

    let results = usb.controlTransferOut(conf);
    console.log('set interface:', results);
   }

  clearHalt(endpoint) {
    const conf = {
      requestType: 'standard',
      recipient: 'endpoint', 
      request: 0x01, // USB_REQUEST_CLEAR_FEATURE
      value: 0x0000, // endpoint halt
      index: endpoint
    };

    let results = usb.controlTransferOut(conf);
    console.log('clear halt', endpoint, ':', results);
   }
}

class CP2102 {

  constructor() {
    this.setPortConfig = {
      requestType: 'vendor',
      recipient: 'device',
      request: 0x05,
      value: 0x00,
      index: 0x03
    };

    this.openPort = {
      requestType: 'vendor',
      recipient: 'device',
      request: 0x06,
      value: 0x80,
      index: 0x03
    };

    this.startPort = {
      requestType: 'vendor',
      recipient: 'device',
      request: 0x08,
      value: 0x00,
      index: 0x03
    };

    this.closePort = {
      requestType: 'vendor',
      recipient: 'device',
      request: 0x07,
      value: 0x00,
      index: 0x03
    };

    this.timeoutID = null;
    this.intervalTimer = null;
  }

  init() {

    console.log('Initialising CP2102');

    usb.controlTransferOut({
      requestType: 'vendor',
      recipient: 'device',
      request: 0x00,
      index: 0x00,
      value: 0x01,
    });

    usb.controlTransferOut({
      requestType: 'vendor',
      recipient: 'device',
      request: 0x07,
      index: 0x00,
      value: 0x03 | 0x0100 | 0x0200,
    });

    usb.controlTransferOut({
      requestType: 'vendor',
      recipient: 'device',
      request: 0x01,
      index: 0x00,
      value: 0x384000 / 38400, // TODO: change baud rate here
    });

    console.log('Baud rate set to 38400');

  }

  close() {
    //TODO: release interface and close device
  }

  send(data, cb) {
    usb.transferOut(0x01, new Uint8Array(data), (result) => {
      console.log('send result:', result);
      return cb();
    });

    this.timeoutID = setTimeout(() => {
      console.log('Device not connected');

      if(this.intervalTimer) {
        clearInterval(this.intervalTimer);
      }
      this.close();
    }, 1000);
  }

  receive(length, end, cb) {

    console.log('Receiving...');
    //this.intervalTimer = setInterval(() => {
      let allData = [];
      let incoming = usb.transferIn(0x01, length);

      if (incoming.data && incoming.data.byteLength > 0) {
        const data = E.toString(incoming.data);
        if (this.timeoutID) {
          clearTimeout(this.timeoutID);
          this.timeoutID = null;
        }
        console.log('Received:', data);
        allData.concat(data);
        if (data.indexOf(end) !== -1) {
          clearInterval(intervalTimer);
          return cb(allData);
        }
      }
    //}, 50);
  }
}

const usb = new USB();
usb.init(() => {

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

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

  Terminal.println('Vendor ID: 0x' + bytes2hex([results.data[9], results.data[8]], true));
  Terminal.println('Product ID: 0x' + bytes2hex([results.data[11], results.data[10]], true));

  console.log('getConfiguration');
  if (usb.getConfiguration() === 0) {
    console.log('selectConfiguration');
    usb.selectConfiguration(1);
  }

  usb.claimInterface(0);

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

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

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

  const cp2102 = new CP2102();
  cp2102.init();
  cp2102.send([0x02, 0x08, 0x00, 0x04, 0x06, 0x03, 0x78, 0xC1], () => {
    console.log('Sent message');
    cp2102.receive(34, 'END', (data) => {
      cp2102.close();
    });
  });
});

Credits

Gerrit Niezen

Gerrit Niezen

4 projects • 9 followers
Maker of open-source hardware and software

Comments