Kampis Elektroecke

Den Gerätetreiber erstellen

Im letzten Teil des USB-Tutorials haben wir es (endlich) geschafft das sich der Mikrocontroller erfolgreich als Device beim Host anmeldet. Nun wollen wir mal schauen, wie wir eine Kommunikation zwischen beiden Busteilnehmern umsetzen können. Für die Kommunikation mit meinem Board verwende ich auf dem Host ein Python-Skript und das Modul PyUSB.

Bevor die Kommunikation implementiert werden kann müssen noch einige Vorbereitungen getroffen werden. Zu allererst muss Python (ich verwende Version 3.7) installiert werden. Das Modul PyUSB wird anschließend über die Kommandozeile installiert:

$ python -m pip install pyusb

Damit USB-Geräte unter PyUSB funktionieren wird ein angepasster Gerätetreiber benötigt, der mit Hilfe des inf-wizards erstellt werden kann. Der inf-wizard ist Teil der libusb, und sich im bin-Verzeichnis befindet. Dieses Programm wird als Administrator gestartet und nach einem Klick auf Next gelangt man zu einer Liste mit erkannten USB-Geräten, wo dann der Mikrocontroller ausgewählt wird.

Die Auswahl wird mit Next bestätigt und die nachfolgende Maske wird bei Bedarf ausgefüllt. Über Next gelangt man ins nächste Menü, wobei man aber erst einen Speicherort für die erzeugten Dateien angeben muss. Über Install Now… kann der erzeugte Treiber umgehend installiert werden. Wenn die Installation erfolgreich abgeschlossen wurde, wird das Board als korrekt angemeldetes USB-Gerät im Geräte-Manager aufgelistet:

Bevor mit der Kommunikation zwischen Host und Device begonnen werden kann, müssen die Endpunkte des Gerätes konfiguriert werden. Dies erfolgt i. d. R. dann, wenn das Gerät erfolgreich vom Host initialisiert worden ist, sprich wenn der Host eine Adresse vergeben und eine Konfiguration ausgewählt hat. Die Konfiguration der Endpunkte soll daher über erstellte ConfigurationChanged-Event des USB_DeviceCallbacks_t-Objektes erfolgen:

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();
	}
}

Wenn die Konfiguration beider Endpunkte erfolgreich durchgeführt worden ist, wird die grüne und die rote LED eingeschaltet und das Gerät ist betriebsbereit. Andernfalls wechselt das Gerät in den Fehlermodus.

Jetzt kann auch schon damit begonnen werden mit dem Device zu kommunizeren. Das Ziel soll es sein, das Ergebnis des Analog/Digital Konverters des Mikrocontroller per USB auszulesen. Die gemessenen Daten werden über den IN-Endpunkt des Mikrocontrollers zum Host übertragen und der Host übermittelt den angefragten ADC-Kanal über den OUT-Endpunkt des Mikrocontrollers.

In dem Python-Skript wird zuerst das Modul PyUSB importiert und geprüft ob ein Gerät mit einer bestimmten Vendor- und Product-ID (die IDs, die über den Deskriptor vergeben wurden) per USB mit dem Computer verbunden ist.

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!")

Wenn ein passendes Gerät gefunden wurde, kann eine entsprechende Konfiguration (in diesem Beispiel gibt es nur eine Konfiguration) ausgewählt, gesetzt und ausgelesen werden:

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

Für eine Kommunikation mit dem Gerät werden zudem noch die Adressen der vorhandenen Endpunkte benötigt. Daher sollen auch die Endpunktdeskriptoren ausgelesen werden.

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)

Die Methode find_descriptor benötigt als Parameter die aktuelle Konfiguration, sowie einen Lambda-Ausdruck, der die Filterregeln beschreibt. Da das aktuelle Gerät nur einen IN– und einen OUT-Endpunkt besitzt, reicht es aus, wenn die Endpunkte entsprechend ihrer Transferrichtung ausgewählt werden. Die ausgelesenen Informationen werden anschließend ausgegeben.

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)))

Als nächstes erwartet das Python-Skript eine Eingabe vom Nutzer. Hier soll der Nutzer den gewünschten ADC-Kanal, nummeriert von 0 bis 7, eingeben. Das Skript überprüft die Eingabe und wirft dann ggf. eine Exception.

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

Der eingegebene Kanal wird dann in den OUT-Endpunkt des Zielgerätes geschrieben.

Device.write(EP_OUT.bEndpointAddress, Channel)

In dem Mikrocontrollerprogramm muss das Datenpaket nun bearbeitet werden. Hierfür wird eine neue Funktion namens USB_DeviceTask angelegt. Diese Funktion wird zyklisch aufgerufen und nach jedem Aufruf wird der Zustandsautomat des USB-Controllers abgefragt, um zu überprüfen ob das Gerät einsatzbereit ist.

int main(void)
{
	while(1) 
	{
	    	USB_Poll();
		USB_DeviceTask();
	}
}

void USB_DeviceTask(void)
{
	if(USB_GetState() != USB_STATE_CONFIGURED)
	{
		return;
	}
}

Anschließend wird der OUT-Endpunkt ausgewählt. Über das RWAL-Bit im UEINTX-Register des Endpunktes kann die Software abfragen ob der Endpunkt leer ist und Daten versenden kann (nur für IN-Endpunkte) oder ob der Endpunkt Daten empfangen hat und ausgelesen werden kann (nur für OUT-Endpunkte).


Achtung:

Das RWAL-Bit darf nicht für einen Kontrollendpunkt verwendet werden!


Wenn Daten empfangen worden sind, wird die Übertragung vom Device bestätigt und die Daten können ausgelesen werden. Mit dem ausgelesenen Datenbyte wird der ADC-Kanal ausgewählt und eine ADC-Messung gestartet.

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

Sobald die Messung beendet ist, springt der ADC in einen Interrupt und schreibt das Messergebnis in den IN-Endpunkt des Mikrocontrollers. Dies erfolgt auf ähnliche Weise wie beim OUT-Endpunkt:

  • Auswählen des Endpunktes
  • Prüfen ob das RWAL-Bit gesetzt ist
  • Überprüfen ob der IN-Endpunt leer ist. Wenn nicht, das TXINI– und das FIFOCON-Bit setzen um eine Übertragung zu starten (wird nur bei IN-Endpunkten benötigt)
  • Neue Daten in den Endpunkt schreiben
  • Daten senden
Endpoint_Select(IN_EP);
if(Endpoint_IsReadWriteAllowed())
{
	if(Endpoint_INReady())
	{
		Endpoint_WriteByte(Result & 0xFF);
		Endpoint_WriteByte((Result >> 0x08) & 0xFF);
		Endpoint_FlushIN();
	}
	else
	{
		Endpoint_FlushIN();
	}
}

Der IN-Endpunkt kann nun über das Python-Skript ausgelesen werden.

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

Die Werte werden abschließend umgerechnet und angezeigt:

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))

Damit ist das Skript auch schon fertig. Der Mikrocontroller wird jetzt noch programmiert und mit dem PC verbunden. Anschließend kann der ADC ausgelesen werden.

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:

Damit ist der erste große Schritt des USB-Tutorials abgeschlossen. Der Mikrocontroller meldet sich als Gerät beim Host an, wir können per USB auf den Mikrocontroller zugreifen, eine Messung starten und das Messergebnis auslesen. All das erfolgt mit einer nicht offiziellen Gerätebeschreibung, die nur sehr rudimentär erstellt worden ist. Zudem sind wir noch nicht in der Lage Standardtreiber, wie sie z. B. für eine Maus oder eine Tastatur existieren, zu nutzen.

Daher wollen wir uns als nächstes mit der Implementierung eines bereits vorhandenen USB-Gerätes, nämlich einer USB-Maus, beschäftigen. Dazu wird etwas zusätzliches Wissen über die verschiedenen USB-Klasse benötigt und wir müssen die Software für den Mikrocontroller anpassen. So müssen z. B. die Gerätedeskriptoren entsprechend ausgefüllt werden, damit der Mikrocontroller von Standardtreibern gesteuert werden kann. Beginnen möchte ich mit der sogenannten HID (Human Interface Device)-Klasse des USB-Protokolls.

Das komplette Projekt könnt ihr bei GitLab downloaden.

Zurück

Last Updated on

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.