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).
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
Was this helpful?