Kampis Elektroecke

Anmeldung beim Host – Teil 1

Im letzten Teil des USB-Tutorials haben wir gelernt, wie die unterschiedlichen Deskriptoren für ein beispielhaftes USB-Gerät aussehen können und wie diese in per Software implementiert werden. Am Ende dieses Teils wird der Mikrocontroller in der Lage sein sich beim Host anzumelden, sodass wir uns im nächsten Teil darum kümmern können mit dem Mikrocontroller über USB zu kommunizieren.

Für die spätere Applikation sind bestimmte Ereignisse, die während der Verwendung des USB auftreten können (wie z. B. eine erfolgreiche Enumeration oder bei einem Fehler), wichtig. Daher soll als im ersten Schritt eine Struktur definiert werden, die die Callbacks für bestimmte Events bereithält:

 typedef struct
 {
    void (*ConfigurationChanged)(const uint8_t Configuration);
    void (*Error)();
    void (*EndOfReset)();
} USB_DeviceCallbacks_t;

Diese Callbacks werden nun im Hauptprogramm deklariert und die Struktur erstellt:

void USB_Event_OnError(void);
void USB_Event_ConfigurationChanged(const uint8_t Configuration);

const USB_DeviceCallbacks_t Events_USB =
{
    .EndOfReset = USB_Event_EndOfReset,
    .Error = USB_Event_OnError,
    .ConfigurationChanged = USB_Event_ConfigurationChanged,
};

void USB_Event_EndOfReset(void)
{
    GPIO_Set(GET_PERIPHERAL(LED0_GREEN), GET_INDEX(LED0_GREEN));
    GPIO_Clear(GET_PERIPHERAL(LED0_RED), GET_INDEX(LED0_RED));
}

void USB_Event_OnError(void)
{
    GPIO_Clear(GET_PERIPHERAL(LED0_GREEN), GET_INDEX(LED0_GREEN));
    GPIO_Set(GET_PERIPHERAL(LED0_RED), GET_INDEX(LED0_RED));
}

Als nächstes wird die USB_Init-Funktion angepasst werden um die Callbacks zu speichern:

extern USB_DeviceCallbacks_t _USBEvents;

void USB_Init(const USB_DeviceCallbacks_t* Events)
{
	USBController_Init(USB_MODE_DEVICE, USB_SPEED_LOW);

	_USBEvents = *Events;
	_DeviceState = USB_STATE_RESET;
}

Und dann wird der USB-Interrupt angepasst um den Reset-Callback, bzw. im Fehlerfall den Error-Callback aufzurufen.

ISR(USB_GEN_vect)
{
    ...

    if(USB_Controller_CheckForInterrupt(USB_EOR_INTERRUPT))
    {
        USB_Controller_ClearInterruptFlag(USB_EOR_INTERRUPT);

        if(!Endpoint_Configure(ENDPOINT_CONTROL_ADDRESS, ENDPOINT_TYPE_CONTROL, ENDPOINT_CONTROL_SIZE, 0x00))
        {
            if(_USBEvents.Error != NULL)
            {
                _USBEvents.Error();
            }
        }
        else
        {
            if(_USBEvents.Error != NULL)
            {
                _USBEvents.Error();
            }
        }

        _DeviceState = USB_STATE_RESET;
    }
}

Hinweis:

Die Variable _USBEvents ist durch den Zusatz extern für andere Source-Files verfügbar, sodass die Callbacks an jeder Stelle im Treibercode verwendet werden können.


Beim USB wird jede Kommunikation Host vom initiiert, sprich USB-Devices sind nicht in der Lage selbstständig Daten zu senden. Der USB-Host muss somit die einzelnen Busteilnehmer periodisch nach neuen Daten abfragen (dieses Verfahren wird auch Pollen genannt) oder Interrupts verwenden (nicht in diesem Beispiel verwendet).

Das Pollen wird in der main-Funktion der Applikation durchgeführt:

int main(void)
{
	USB_Init(&Events_USB);

	sei();

	while(1) 
	{
	    USB_Poll();
	}
}

Die Konfiguration, bzw. die Parametrierung des USB-Devices durch den Host, erfolgt mit Hilfe von sogenannten Standard-Request, die über SETUP-Pakete versendet werden. Über diese Requests kann der Host bestimmte Informationen (wie z. B. die Deskriptoren) vom Device abfragen oder bestimmte Konfigurationen am Device vornehmen.

Die Funktion USB_Poll überprüft zuerst ob der Mikrocontroller noch mit dem USB verbunden ist. Dazu wird der aktuelle Zustand des Zustandsautomaten abgefragt.

void USB_Poll(void)
{
	if(_DeviceState == USB_STATE_UNATTACHED)
	{
		return;
	}

	uint8_t CurrEndpoint = UENUM;
	UENUM = ENDPOINT_CONTROL_ADDRESS;
	if(UEINTX & (0x01 << RXSTPI))
	{
		USB_Device_ControlRequest();
	}
	UENUM = CurrEndpoint;
}

Wenn der Mikrocontroller mit dem Bus verbunden ist, wird der zur Zeit aktive Endpunkt aus dem UENUM-Register ausgelesen und gespeichert, auf den Kontrollendpunkt gewechselt und überprüft ob ein SETUP-Paket empfangen wurde, indem das RXSTPI-Bit im UEINTX-Register des Endpunktes abgefragt wird. Wenn kein SETUP-Paket empfangen wurde, wechselt die Software wieder zurück auf den vorherigen Endpunkt und verlässt die Funktion.

Andernfalls springt die Software in die Funktion USB_Device_ControlRequest, in der das empfangene SETUP-Paket eingelesen und weiterverarbeitet wird. In dieser Funktion werden die empfangenen Requests ausgewertet. Der Einfachheit halber beschränke ich die Anzahl der ausgewerteten Requests in diesem einfachen Beispiel auf die mindestens benötigten Requests:

  • SET_ADDRESS – Wird benötigt um die Adresse, die der Host während der Enumeration vergibt, zu setzen
  • GET_DESCRIPTOR – Wird benötigt um dem Host die unterschiedlichen Deskriptoren zu senden
  • SET_CONFIGURATION – Wird benötigt um das Ende der Enumeration zu erkennen

Die komplette Kommunikation findet über den IN- und den OUT-Kontrollendpunkt statt. Es werden somit Daten gelesen (z. B. die Adresse, die der Host zum Device gesendet hat) und Daten geschrieben (z. B. die Deskriptoren, die das Device zum Host sendet). Der Ablauf eines Schreib- bzw. Lesevorgangs ist im Datenblatt des USB Devicecontrollers beschrieben.

Zu Beginn eines jeden Controlrequests wird das RXSTPI-Bit durch den USB-Controller gesetzt. Dies signalisiert der CPU, dass ein neues SETUP-Paket eingetroffen ist und ausgelesen werden kann:

uint8_t* RequestHeader = (uint8_t*)& _ControlRequest;

for(uint8_t i = 0x00; i < sizeof(USB_SetupPacket_t); i++)
{
	*(RequestHeader++) = UEDATX;
}

Dazu wird ein Zeiger auf die statische Variable _ControlRequest erstellt und anschließend werden die einzelnen Datenbytes aus dem Kontrollendpunkt ausgelesen und in den Speicher der Variable _ControlRequest geschrieben.


Hinweis:

Damit die Struktur korrekt befüllt wird, muss sie mit dem Attribut __attribute__((packed)) deklariert werden. Andernfalls fügt der Compiler u. U. Paddingbytes ein, wodurch bei der Iteration über den Speicherbereich mittels Zeiger auf die Struktur die Daten nicht korrekt abgespeichert werden.


Nachdem die Daten gelesen wurden muss das RXSTPI-Bit gelöscht werden:

UEINTX &= ~(0x01 << RXSTPI);

Das Löschen des RXSTPI-Bits signalisiert das Ende der SETUP-Stage und es wird zur nächsten Stage gewechselt. Je nach Art des Requests ist die nächste Stage die DATA– oder die STATUS-Stage. Die Art des Requests lässt sich über das Feld bRequest der Struktur _ControlRequest ermitteln und über eine Switch-Anweisung abfragen:

void USB_Device_ControlRequest(void)
{
	uint8_t* RequestHeader = (uint8_t*)& _ControlRequest;

	for(uint8_t i = 0x00; i < sizeof(USB_SetupPacket_t); i++)
	{
		*(RequestHeader++) = UEDATX;
	}

	UEINTX &= ~(0x01 << RXSTPI);

	switch(_ControlRequest.bRequest)
	{
		case REQUEST_SET_ADDRESS:
		{
			break;
		}
		case REQUEST_GET_DESCRIPTOR:
		{
			break;
		}
		case REQUEST_SET_CONFIGURATION:
		{
			break;	
		}
	}
}

Zu Anfang sendet der USB-Host einen GET_DESCRIPTOR-Request an das angeschlossene Device, damit der Host die unterschiedlichen Deskriptoren einlesen kann um u. a. Informationen über die Größe des Kontrollendpunktes zu erhalten.


Hinweis:

Es ist sinnvoll das Feld bRequestType des Request auszuwerten, dazusätzlich zu den Standardrequests noch klassenspezifische Requests verwendet werden können, wodurch die unterschiedlichen USB-Klassen die Funktion eines Requests mit klassenspezifischen Funktionen erweitern (z. B. die HID-Klasse).


Im nächsten Schritt wird das Feld bRequestType ausgewertet werden, welches bei einem Standardrequest GET_DESCRIPTOR-Request mit dem 100000002 gefüllt ist. Aus diesem Wert ergeben sich die folgenden Informationen:

  • Transferrichtung: Device to Host (also ein IN-Transfer)
  • Art des Request: Standard
  • Empfänger: Device
if(_ControlRequest.bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_DEVICE))
{
	...
}

Und für die anderen Felder des Request sind die folgende Funktionen festgelegt:

Anfragefeld Beschreibung
wValue Art und Index des Deskriptors
wIndex Null oder Sprach-ID (bei einem Stringdeskriptor)
wLength Größe des Deskriptors
Data Der Deskriptor

Jetzt wird noch eine Funktion benötigt um, basierend auf den Parametern wValue und wIndex, den entsprechenden Deskriptor aus dem Programmspeicher zu laden. Im zweiten Schritt müssen wLength Datenbytes des Deskriptors an den Host übermittelt werden. Die Funktion zum Laden der Deskriptoren kann z. B. folgendermaßen aussehen:

const void* USB_GetDescriptor(const uint16_t wValue, const uint16_t wIndex, uint16_t* Size)
{
	uint8_t DescriptorType = (wValue >> 0x08);
	uint8_t DescriptorNumber = (wValue & 0xFF);

	switch(DescriptorType)
	{
		case DESCRIPTOR_TYPE_DEVICE:
		{
			*Size = sizeof(USB_DeviceDescriptor_t);
			return &DeviceDescriptor;
		}
		case DESCRIPTOR_TYPE_CONFIGURATION:
		{
			*Size = sizeof(USB_Configuration_t);
			return &ConfigurationDescriptor;
		}
		case DESCRIPTOR_TYPE_STRING:
		{
			switch(DescriptorNumber)
			{
				case STRING_ID_LANGUAGE:
				{
					*Size = pgm_read_byte(&LANGID.bLength);
					return &LANGID;
				}
				case STRING_ID_MANUFACTURER:
				{
					*Size = pgm_read_byte(&ManufacturerString.bLength);
					return &ManufacturerString;
				}
				case STRING_ID_PRODUCT:
				{
					*Size = pgm_read_byte(&ProductString.bLength);
					return &ProductString;
				}
				case STRING_ID_SERIAL:
				{
					*Size = pgm_read_byte(&SerialString.bLength);
					return &SerialString;
				}
			}
		}
	}

	*Size = 0x00;
	return NULL;
}

Die Funktion USB_GetDescriptor erwartet als Übergabeparameter die Werte von wValue und wIndex aus dem Controlrequest des Hostes, sowie einen Zeiger auf einen Speicherbereich für die Länge des Deskriptors und gibt einen Zeiger auf den Speicherbereich des Deskriptors zurück. Dazu wird zuerst die Art und der Index des Deskriptors aus dem Parameter wValue extrahiert und anschließend wird mittels Switch-Anweisung die Adresse und die Größe des Deskriptors ermittelt.


Achtung:

Bei dem Geräte- und dem Konfigurationsdeskriptor kann die Länge des Deskriptors über den sizeof-Operator und den Aufbau der Deskriptorstruktur ermittelt werden. Bei dem String-Deskriptor funktioniert dies nicht, weil sizeof bei einem Array nur die Größe des Zeigers auf das erste Element zurückgibt. Daher muss in diesem Fall die Größe des Deskriptors mittels pgm_read_byte aus dem Feld bLength des Deskriptors im Programmspeicher gelesen werden.


Die erstellte Funktion wird bei der Bearbeitung des GET_DESCRIPTOR-Request aufgerufen und die empfangenen Parameter für wValue und wIndex übergeben:

if(_ControlRequest.bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_DEVICE))
{
	const void* Descriptor;
	uint16_t DescriptorSize;

	Descriptor = USB_GetDescriptor(_ControlRequest.wValue, _ControlRequest.wIndex, &DescriptorSize);
	if((DescriptorSize == 0x00) && (_USBEvents.Error != NULL))
	{
		_USBEvents.Error();
	}
}

Jetzt muss der Deskriptor noch an den Host übermittelt werden. Hierbei muss beachtet werden, dass der Host über den Parameter wLength vorgibt, wie viele Datenbytes er vom Deskriptor einlesen möchte. Die Funktion zum Senden der Daten sieht folgendermaßen aus:

Endpoint_CS_State_t USB_DeviceStream_ControlIN(const void* Buffer, const uint16_t Length, const uint16_t RequestedLength)
{
	uint8_t* Buffer_Temp = (uint8_t*)Buffer;
	uint16_t Length_Temp = Length;
	uint8_t LastPacketFull = 0x00;

	if(Length > RequestedLength)
	{
		Length_Temp = RequestedLength;
	}

	while(Length_Temp)
	{
		Endpoint_CS_State_t State = USB_DeviceStream_GetControlEndpointState();
		if(State != ENDPOINT_CS_NO_ERROR)
		{
			return State;
		}
		else if(UEINTX & (0x01 << RXOUTI))
		{
			break;
		}

		if(UEINTX & (0x01 << TXINI))
		{
			while(Length_Temp && (UEBCX < Endpoint_ControlSize))
			{
				UEDATX = pgm_read_byte(Buffer_Temp++);
				Length_Temp--;
			}

			UEINTX &= ~((0x01 << TXINI) | (0x01 << FIFOCON));
		}
	}

	while(!(UEINTX & (0x01 << TXINI)));
	UEINTX &= ~((0x01 << TXINI) | (0x01 << FIFOCON));

	while(!(UEINTX & (0x01 << RXOUTI)))
	{
		Endpoint_CS_State_t State = USBStream_GetControlEndpointState();
		if(State != ENDPOINT_CS_NO_ERROR)
		{
			return State;
		}
	}

	UEINTX &= ~((0x01 << RXOUTI) | (0x01 << FIFOCON));

	return ENDPOINT_CS_NO_ERROR;
}

Zu Beginn überprüft die Funktion ob die zu übertragende Nachricht länger ist als die Anzahl der Datenbytes, die der Host angefragt hat. In der nachfolgenden While-Schleife werden die Daten anschließend übertragen. Dazu wird als erstes der Status des Kontrollendpunktes überprüft:

static Endpoint_CS_State_t USB_DeviceStream_GetControlEndpointState(void)
{
	if(_DeviceState == USB_STATE_UNATTACHED)
	{
		return ENDPOINT_CS_DISCONNECT;
	}
	else if(_DeviceState == USB_STATE_SUSPEND)
	{
		return ENDPOINT_CS_SUSPEND;
	}
	else if(UEINTX & (0x01 << RXSTPI))
	{
		return ENDPOINT_CS_ABORT_FROM_HOST;
	}
	
	return ENDPOINT_CS_NO_ERROR;
}

...

Endpoint_CS_State_t State = USB_DeviceStream_GetControlEndpointState();
if(State != ENDPOINT_CS_NO_ERROR)
{
	return State;
}
else if(UEINTX & (0x01 << RXOUTI))
{
	break;
}

Wenn sich der Zustand des USB-Devices ändert, oder der Host ein neues SETUP– oder Daten-Paket in den OUT-Endpunkt geschrieben hat, bricht die Übertragung ab und es wird ein entsprechender Fehler zurückgegeben.

Anschließend wird die Datenübertragung entsprechend des gezeigten Ablaufdiagramms umgesetzt. Zuerst wird über das TXINI-Bit geprüft ob der IN-Endpunkt bereit eine neue IN-Transaktion erhalten, sprich ob die DATA-Stage, begonnen hat.

Wurde eine entsprechende IN-Transaktion erkannt, wird der Endpunkt mit Daten gefüllt. Sobald der Endpunkt voll ist, wird eine Übertragung in Richtung Host initiiert und der Endpunkt geleert und auf das Ende der Übertragung gewartet:

if(UEINTX & (0x01 << TXINI))
{
	while(Length_Temp && (UEBCX < ENDPOINT_CONTROL_SIZE))
	{
		UEDATX = pgm_read_byte(Buffer_Temp++);
		Length_Temp--;
	}

	UEINTX &= ~((0x01 << TXINI) | (0x01 << FIFOCON));
	while(!(UEINTX & (0x01 << TXINI));
}

Sobald sämtliche Daten übertragen wurden beginnt die STATUS-Stage. Das erste Kommando der STATUS-Stage wird vom USB-Controller immer mit einem NAK quittiert. Das Datenblatt gibt vier Schritte vor, die in diesem Fall bearbeitet werden müssen:

  • Transmit Ready setzen
  • Auf Transmit Complete oder Receive Complete warten
  • Wenn Receive Complete: Flag löschen und beenden
  • Wenn Transmit Complete: Weitermachen

Dieses Vorgehen muss nun programmtechnisch umgesetzt werden, wobei auf die Auswertung vom Transmit Complete in diesem Fall verzichtet werden kann:

UEINTX &= ~((0x01 << TXINI) | (0x01 << FIFOCON));
while(!(UEINTX & (0x01 << RXOUTI)))
{
	Endpoint_CS_State_t State = USB_DeviceStream_GetControlEndpointState();
	if(State != ENDPOINT_CS_NO_ERROR)
	{
		return State;
	}
}

UEINTX &= ~((0x01 << RXOUTI) | (0x01 << FIFOCON));

Während die Software auf den Receive Complete wartet, wird permanent der Status des Endpunktes überprüft und ggf. ein Fehler zurückgegeben. Die fertige Funktion kann nun in den Abarbeitung des GET_DESCRIPTOR-Request eingebaut werden:

case REQUEST_GET_DESCRIPTOR:
{
	if(_ControlRequest.bmRequestType == (REQUEST_DIRECTION_DEVICE_TO_HOST | REQUEST_TYPE_STANDARD | REQUEST_RECIPIENT_DEVICE))
	{
		const void* Descriptor;
		uint16_t DescriptorSize;

		Descriptor = USB_GetDescriptor(_ControlRequest.wValue, _ControlRequest.wIndex, &DescriptorSize);
		if((DescriptorSize == 0x00) && (_USBEvents.Error != NULL))
		{
			_USBEvents.Error();
		}

		USB_DeviceStream_ControlIN(Descriptor, DescriptorSize, _ControlRequest.wLength);
	}

	break;
}

Damit wird der erste Request erfolgreich abgearbeitet. Die beiden anderen benötigten Requests folgen dann im nächsten Teil.

Zurück

Schreibe einen Kommentar

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