Browse over 10,000 Electronics Projects

USB HID device development on the STM32 F042

USB HID device development on the STM32 F042

Configuring your device

The main template, UsbCustomHid is parameterised with a type that contains some important constants that the template will refer to. Let’s see them:

struct MyHidConfiguration {

  enum {

    /*
     * USB Vendor and Product ID. Unfortunately commercial users will probably have to pay
     * the license fee to get an official VID and 64K PIDs with it. For testing and hacking
     * you can just do some research to find an unused VID and use it as you wish.
     */

    VID = 0xF055,
    PID = 0x7201,

    /*
     * IN and OUT are always with respect to the host. You as a device transmit on an IN
     * endpoint and receive on an OUT endpoint. Define how big your reports are here. 64-bytes
     * is the maximum allowed.
     *
     * Report id #1 is for reports TO the host (IN direction)
     * Report id #2 is for reports FROM the host (OUT direction)
     */

    IN_ENDPOINT_MAX_PACKET_SIZE = 12,   // 1 byte report id + 11-byte report
    OUT_ENDPOINT_MAX_PACKET_SIZE = 10,  // 1 byte report id + 9-byte report

    /*
     * The number of milliamps that our device will use. The maximum you can specify is 510.
     */

    MILLIAMPS = 100,

    /*
     * Additional configuration flags for the device. The available options that can be
     * or'd together are UsbConfigurationFlags::SELF_POWERED and
     * UsbConfigurationFlags::REMOTE_WAKEUP.
     */

    CONFIGURATION_FLAGS = 0,      // we want power from the bus

    /*
     * The language identifier for our strings
     */

    LANGUAGE_ID = 0x0809    // United Kingdom English.
  };

  /*
   * USB devices support a number of Unicode strings that are used to show information
   * about the device such as the manufacturer, product, serial number and some other
   * stuff that's not usually as visible to the user. You need to define all 5 of them
   * here with the correct byte length. Look ahead to where these are defined to see
   * what the byte lengths will be and then come back here and set them accordingly.
   */

  static const uint8_t ManufacturerString[32];
  static const uint8_t ProductString[22];
  static const uint8_t SerialString[12];
  static const uint8_t ConfigurationString[8];
  static const uint8_t InterfaceString[8];
};

The constants can be changed to suit your application, but remember the overall 64-byte report size limit. I won’t dwell on the politics of the VID/PID assignment policy here. If you want to know more then google will help you find the many articles already written on that subject. This hackaday article is a good place to start.

The official language identifers PDF can be found here. For example English (US) is 0x0409.

We can now declare a USB HID device class instance.

/*
 * Declare the USB custom HID object. This will initialise pins but won't
 * power up the device yet.
 */

UsbCustomHid<MyHidConfiguration> usb;

As the comment says, the constructor will attach PA11 and PA12 to the USB peripheral and do nothing else. Before we start the peripheral we need to subscribe to the events that will be raised during your device’s lifecycle.

Communication and status event handlers

stm32plus does all its callbacks using strongly typed event handlers implemented under the hood using Don Clugston’s famous fasted possible C++ delegates code. From the point of view of the user this gives an elegant, type-safe and scoped subscribe/unsubscribe programming model.



Advertisement1


/*
 * Subscribe to all the events
 */

usb.UsbRxEventSender.insertSubscriber(UsbRxEventSourceSlot::bind(this,&UsbDeviceCustomHid::onReceive));
usb.UsbTxCompleteEventSender.insertSubscriber(UsbTxCompleteEventSourceSlot::bind(this,&UsbDeviceCustomHid::onTransmitComplete));
usb.UsbStatusEventSender.insertSubscriber(UsbStatusEventSourceSlot::bind(this,&UsbDeviceCustomHid::onStatusChange));

The UsbCustomHid class raises three event types that can be subscribed to. The TX and RX complete events are important so that you can schedule your communication with the host. When the host has sent you a report and it’s all been received then you’ll get the RX complete event.

When you send a report to the host it is done asynchronously with a non-blocking call and when that transmission completes then you’ll get a TX complete event.

The status event is used to notify you whenever there’s been a change to the status of the connection such as would happen during device insertion and removal. In particular, the move into and away from the CONFIGURED state is critical because you can only communicate with the host during the CONFIGURED state. At the very least, you will need to look for a change into and away from that state.

In the example code my implementation of the onReceive event handler looks like this.

/*
 * Data received from the host
 */

void onReceive(uint8_t endpointIndex,const uint16_t *data,uint16_t size) {

  // note that the report data is always prefixed with the report id, which is
  // 0x02 in the stm32plus custom HID implementation for reports OUT from the host

  if(endpointIndex==1 && size==10 && memcmp(data,"x02stm32plus",size)==0)
    _receivedReportTime=MillisecondTimer::millis();
}

I check that the endpoint index is the one that I expect and that the report has the expected size and content. I then set a flag for the main loop to pick up on and flash a LED to let the user know the report was received.

The onReceive handler is called within an IRQ context so it’s important that you keep the CPU cycles to a minimum and be aware of the data sychronization issues with non-IRQ code that can occur.

To keep the performance at the highest level possible data reception is done with zero-copy semantics which means that the data pointer points directly into the USB peripheral FIFO. If you need to process the data beyond the scope of the event handler then it must be copied out of the address pointed to bydata.

In my example I don’t really need to know when data transmission is complete because I’m sending a report every 1000ms which is way more than the report transmission time. I implement a skeleton event handler anyway just so that you can see what it looks like:

/*
 * Finished sending data to the host
 */

void onTransmitComplete(uint8_t /* endpointIndex */,uint16_t /* size */) {
  // ACK received from the host
}

Again, this event handler is called within an IRQ context so you must be sure to do as little as possible before returning.

The implementation of the onStatusChange handler is more involved.

/*
 * Device status change event
 */

void onStatusChange(UsbStatusType newStatus) {

  switch(newStatus) {

    case UsbStatusType::STATE_CONFIGURED:
    _deviceConfigured=true;
    _lastTransmitTime=MillisecondTimer::millis()+5000;    // 5 second delay before starting to send
    break;

    case UsbStatusType::STATE_DEFAULT:
    case UsbStatusType::STATE_ADDRESSED:
    case UsbStatusType::STATE_SUSPENDED:
      _deviceConfigured=false;
      break;

    default:     // keep the compiler quiet
      break;
  }
}

This implementation looks for state changes in and out of CONFIGURED. Any other state means that the host is not ready to talk to us. As with the TX/RX event handlers this is called within the context of an IRQ so minimal work must be done here before returning.

With those three handlers implemented you are almost good to go with your USB device but last of all you need to implement the strings that identify your device.

Pages: 1 2 3 4 5 6 7 8 9

 


Top