Create the device driver

Now let's see how we can implement communication between the two bus participants. For the communication with my board, I use a Python script and the PyUSB module on the host.

Before the communication can be implemented, some preparations have to be made. First of all, Python (I'm using version 3.7) needs to be installed. The PyUSB module is then installed from the command line:

$ python -m pip install pyusb

In order for USB devices to work under PyUSB, an adapted device driver is required, which can be created using the inf-wizard. The inf-wizard is part of libusb and is located in the bin directory.

This program is started as administrator and after clicking on Next you get to a list of recognized USB devices, where the microcontroller is then selected.

The selection is confirmed with Next and the following mask is filled in if necessary. Next takes you to the next menu, but you first have to specify a storage location for the generated files. The generated driver can be installed immediately via Install Now ... . If the installation was successfully completed, the board is listed as a correctly registered USB device in the device manager:

Before the communication between host and device can be started, the end points of the device must be configured. This usually takes place when the device has been successfully initialized by the host, i.e. when the host has assigned an address and selected a configuration. The endpoints should therefore be configured using the configuration change event of the USB_DeviceCallbacks_t object:

void USB_Event_ConfigurationChanged(const uint8_t Configuration)
{
	if(Endpoint_Configure(IN_EP, ENDPOINT_TYPE_INTERRUPT, EP_SIZE, 1) && Endpoint_Configure(OUT_EP, ENDPOINT_TYPE_INTERRUPT, EP_SIZE, 1))
	{
		GPIO_Set(GET_PERIPHERAL(LED0_GREEN), GET_INDEX(LED0_GREEN));
		GPIO_Set(GET_PERIPHERAL(LED0_RED), GET_INDEX(LED0_RED));
	}
	else
	{
		USB_Event_OnError();
	}
}

When the configuration of both endpoints has been carried out successfully, the green and red LEDs are switched on and the device is ready for operation. Otherwise the device switches to error mode.

Now you can start communicating with the device. The goal should be to read the result of the analog / digital converter of the microcontroller via USB. The measured data are transmitted to the host via the IN endpoint of the microcontroller and the host transmits the requested ADC channel via the microcontroller's OUT endpoint.

In the Python script, the PyUSB module is first imported and checked whether a device with a specific vendor and product id (the id isassigned via the descriptor) is connected to the computer via USB.

import usb.core
import usb.control
Vendor = 0x0123
Product = 0x4567
if(__name__ == "__main__"):
    Devices = usb.core.show_devices()
    if(Devices is None):
        raise ValueError("[ERROR] No devices found!")
    Device = usb.core.find(idVendor = Vendor, idProduct = Product)
    if(Device is None):
        raise ValueError("[ERROR] Device not found!")

If a suitable device has been found, a corresponding configuration (in this example there is only one configuration) can be selected, set and read out:

Device.set_configuration(1)
Config = Device.get_active_configuration()[(0, 0)]

The addresses of the existing endpoints are also required for communication with the device. The endpoint descriptors should therefore also be read out.

EP_OUT = usb.util.find_descriptor(Config,
                                  custom_match = lambda e: \
                                  usb.util.endpoint_direction(e.bEndpointAddress) == \
                                  usb.util.ENDPOINT_OUT)
EP_IN = usb.util.find_descriptor(Config,
                                 custom_match = lambda e: \
                                 usb.util.endpoint_direction(e.bEndpointAddress) == \
                                 usb.util.ENDPOINT_IN)

The find_descriptor method requires the current configuration as a parameter as well as a lambda expression that describes the filter rules. Since the current device has only one IN and one OUT endpoint, it is sufficient if the end points are selected according to their transfer direction. The information read out is then output.

print("[DEBUG] Device:\n\r{}".format(Device))
print("[DEBUG] Product: {}".format(Device.product))
print("[DEBUG] Manufacturer: {}".format(Device.manufacturer))
print("[DEBUG] Serial number: {}".format(Device.serial_number))
for n, ID in enumerate(Device.langids):
    print("[DEBUG] LANG {}: 0x{:02x}".format(n, ID))
print("[DEBUG] Status: {}".format(usb.control.get_status(Device)))
print("[DEBUG] Current configuration:\n\r{}".format(Config))
print("[DEBUG] Current interface: {}".format(usb.control.get_interface(Device, 0)))

Next, the Python script expects a user input. Here the user should enter the desired ADC channel, numbered from 0 to 7. The script checks the input and then throws an exception if necessary.

while(True):
    try:
        Channel = input("[INPUT] Please enter the ADC channel: ")
        if(int(Channel) > 7):
            raise Exception

The entered channel is then written to the OUT endpoint of the target device.

Device.write(EP_OUT.bEndpointAddress, Channel)

The data packet must now be processed in the microcontroller program. A new function called USB_DeviceTask is created for this. This function is called cyclically and after each call the state machine of the USB controller is queried to check whether the device is ready for use.

int main(void)
{
	while(1) 
	{
	    USB_Poll();
		  USB_DeviceTask();
	}
}
void USB_DeviceTask(void)
{
	if(USB_GetState() != USB_STATE_CONFIGURED)
	{
		return;
	}
}

Then the OUT endpoint is selected. The software can use the RWAL bit in the UEINTX register of the endpoint to query whether the endpoint is empty and can send data (only for IN endpoints) or whether the endpoint has received data and can be read out (only for OUT endpoints).

It is not allowed to use the RWAL bit for a control end point!

When data has been received, the transmission is confirmed by the device and the data can be read out. With the data byte read out, the ADC channel is selected and an ADC measurement is started.

Endpoint_Select(OUT_EP);
if(Endpoint_IsReadWriteAllowed())
{
	Endpoint_AckOUT();
	ADC_SetChannel(Endpoint_ReadByte());
	ADC_StartConversion();
}

As soon as the measurement is finished, the ADC jumps into an interrupt and writes the measurement result to the IN end point of the microcontroller. This is done in a similar way to the OUT endpoint:

  • Chose the endpoint

  • Check RWAL

  • Check of the IN endpoint is empty. Set TXINI and FIFOCON if the endpoint is not empty.

  • Write new data

  • Transmit the data

Endpoint_Select(IN_EP);
if(Endpoint_IsReadWriteAllowed())
{
	if(Endpoint_INReady())
	{
		Endpoint_WriteByte(Result & 0xFF);
		Endpoint_WriteByte((Result >> 0x08) & 0xFF);
		Endpoint_FlushIN();
	}
	else
	{
		Endpoint_FlushIN();
	}
}

The IN endpoint can now be read out using the Python script.

Analog = Device.read(EP_IN.bEndpointAddress, 2)

The values ​​are then converted and displayed:

ConversionResult = (Analog[1] << 0x08) | Analog[0]
print(" Conversion result: {}".format(ConversionResult))
U = 2.56 / 1024 * int(ConversionResult)
if(int(Channel) == 0):
    RT = 100000.0 / (3.3 - (U)) * U
    Beta = 4250
    T0 = 298
    R0 = 100000
    T = (Beta / (math.log(RT / R0) + (Beta / T0))) - 273
    print(" Temperature: {:.2f}°C".format(T))
elif(int(Channel) == 3):
    Battery = (U * 320 / 100) + 0.7
    print(" Battery: {:.2f} V".format(Battery))
else:
    print("   Analog value: {:.2f}".format(U))

The script is now done. The microcontroller is now still being programmed and connected to the PC. Then the ADC can be read out.

Python 3.6.8 (tags/v3.6.8:3c6b436a57, Dec 24 2018, 00:16:47) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license()" for more information.
>>> 
 RESTART: USB_Example\script\USB_Example.py 
[DEBUG] Device:
DEVICE ID 0123:4567 on Bus 000 Address 001 =================
 bLength                :   0x12 (18 bytes)
 bDescriptorType        :    0x1 Device
 bcdUSB                 :   0x11 USB 0.11
 bDeviceClass           :   0xff Vendor-specific
 bDeviceSubClass        :    0x0
 bDeviceProtocol        :    0x0
 bMaxPacketSize0        :    0x8 (8 bytes)
 idVendor               : 0x0123
 idProduct              : 0x4567
 bcdDevice              :    0x1 Device 0.01
 iManufacturer          :    0x1 Daniel Kampert
 iProduct               :    0x2 AT90USB1287 USB-Example
 iSerialNumber          :    0x3 123456
 bNumConfigurations     :    0x1
  CONFIGURATION 1: 100 mA ==================================
   bLength              :    0x9 (9 bytes)
   bDescriptorType      :    0x2 Configuration
   wTotalLength         :   0x20 (32 bytes)
   bNumInterfaces       :    0x1
   bConfigurationValue  :    0x1
   iConfiguration       :    0x0 
   bmAttributes         :   0xc0 Self Powered
   bMaxPower            :   0x32 (100 mA)
    INTERFACE 0: Vendor Specific ===========================
     bLength            :    0x9 (9 bytes)
     bDescriptorType    :    0x4 Interface
     bInterfaceNumber   :    0x0
     bAlternateSetting  :    0x0
     bNumEndpoints      :    0x2
     bInterfaceClass    :   0xff Vendor Specific
     bInterfaceSubClass :    0x0
     bInterfaceProtocol :    0x0
     iInterface         :    0x0 
      ENDPOINT 0x81: Interrupt IN ==========================
       bLength          :    0x7 (7 bytes)
       bDescriptorType  :    0x5 Endpoint
       bEndpointAddress :   0x81 IN
       bmAttributes     :    0x3 Interrupt
       wMaxPacketSize   :    0x8 (8 bytes)
       bInterval        :    0xa
      ENDPOINT 0x2: Interrupt OUT ==========================
       bLength          :    0x7 (7 bytes)
       bDescriptorType  :    0x5 Endpoint
       bEndpointAddress :    0x2 OUT
       bmAttributes     :    0x3 Interrupt
       wMaxPacketSize   :    0x8 (8 bytes)
       bInterval        :    0xa
[DEBUG] Product: AT90USB1287 USB-Example
[DEBUG] Manufacturer: Daniel Kampert
[DEBUG] Serial number: 123456
[DEBUG] LANG 0: 0x409
[DEBUG] Status: 0
[DEBUG] Current configuration:
    INTERFACE 0: Vendor Specific ===========================
     bLength            :    0x9 (9 bytes)
     bDescriptorType    :    0x4 Interface
     bInterfaceNumber   :    0x0
     bAlternateSetting  :    0x0
     bNumEndpoints      :    0x2
     bInterfaceClass    :   0xff Vendor Specific
     bInterfaceSubClass :    0x0
     bInterfaceProtocol :    0x0
     iInterface         :    0x0 
      ENDPOINT 0x81: Interrupt IN ==========================
       bLength          :    0x7 (7 bytes)
       bDescriptorType  :    0x5 Endpoint
       bEndpointAddress :   0x81 IN
       bmAttributes     :    0x3 Interrupt
       wMaxPacketSize   :    0x8 (8 bytes)
       bInterval        :    0xa
      ENDPOINT 0x2: Interrupt OUT ==========================
       bLength          :    0x7 (7 bytes)
       bDescriptorType  :    0x5 Endpoint
       bEndpointAddress :    0x2 OUT
       bmAttributes     :    0x3 Interrupt
       wMaxPacketSize   :    0x8 (8 bytes)
       bInterval        :    0xa
[DEBUG] Current interface: 1
[INPUT] Please enter the ADC channel: 0
 Conversion result: 631
 Temperature: 26.85°C
[INPUT] Please enter the ADC channel: 1
 Conversion result: 477
   Analog value: 1.19
[INPUT] Please enter the ADC channel: 3
 Conversion result: 964
 Battery: 8.41 V
[INPUT] Please enter the ADC channel:

Now the first big milestone of this USB tutorial is complete. The microcontroller is communicating with the host as a device, we can access the microcontroller via USB, start a measurement and read out the measurement result. All of this is done with an unofficial device description, which has been created only very rudimentary. In addition, we are not yet able to use standard drivers such as it exist for a mouse or something else.

Therefore, we want to deal with the implementation of an existing USB device, namely a USB mouse in the next steps. This requires some additional knowledge of the different USB classes and we have to adapt the software for the microcontroller. So the device descriptors are filled in accordingly so that the microcontroller can be controlled by standard drivers. I would like to start with the so-called HID (Human Interface Device) class of the USB protocol.

Last updated