Nie takie USB straszne jak je malują, cz.5 - pierwszy kompletny transfer

Wszystko co dotyczy płytek z rodziny Discovery firmy STM
ODPOWIEDZ
Awatar użytkownika
elvis
Użytkownik
Posty: 56
Rejestracja: 30 lis 2018, 17:50

Nie takie USB straszne jak je malują, cz.5 - pierwszy kompletny transfer

Post autor: elvis » 06 lip 2019, 13:33

W poprzedniej części udało się odebrać pakiet SETUP z zapytaniem hosta. Jednak żeby poprawnie komunikować się przez USB trzeba nieco skomplikować przykładowy program.
Na początek małe wyjaśnienie odnośnie komunikacji. W przypadku endpointów kontrolnych, czyli tego czym się na początek musimy zająć, komunikacja odbywa się w kilku etapach.
Host najpierw wysyła żądanie w znanym już 8 bajtowym pakiecie SETUP. Teraz są dwie możliwości - jeśli jest to zapytanie np. GET_DESCRIPTOR, urządzenie powinno odesłać zamawiane dane, następnie host potwierdzi odbiór wysyłają pusty pakiet (który musimy odebrać i zignorować).
Możliwe są też pakiety bez danych, wówczas urządzenie odpowiada pustym pakietem. Możliwe byłoby również przesłanie przez host dodatkowych danych, wtedy transmisja wyglądałaby następująco: 1) host wysyła pakiet SETUP, 2) host przesyła dodatkowe dane 3) urządzenie potwierdza odbiór pustym pakietem.
Komplikacja bierze się ze stanowego sposobu działania - program otrzymuje przerwanie od USB i musi podjąć decyzję co zrobić dalej. Najwygodniej będzie więc dodać trochę zmiennych, które będą przechowywały stan komunikacji.
Ja zdefiniowałem następującą strukturę:

Kod: Zaznacz cały

typedef void (*comm_callback)(uint8_t);

struct {
	const void *tx_buffer;
	int32_t tx_length;
	comm_callback tx_handler;
	void *rx_buffer;
	int32_t rx_length;
	comm_callback rx_handler;
}endpoint[8];
Pola tx_buffer i tx_length przechowują informację ile jeszcze danych ma być wysłanych przez endpoint. Podobnie rx_buffer i rx_length odpowiadają za dane do odebrania. Dodatkowo używam callback-ów tx_handler i rx_handler które będą wywołane po zakończeniu transmisji.

Zamiast więc bezpośrednio używać niskopoziomowej biblioteki mam teraz dwie nowe funkcje:

Kod: Zaznacz cały

void endpoint_send(uint8_t ep, const void *data, uint16_t length, comm_callback next)
{
	endpoint[ep].tx_handler = next;
	if (length < ENDPOINT_SIZE) {
		endpoint[ep].tx_buffer = NULL;
		endpoint[ep].tx_length = -1;
	} else {
		endpoint[ep].tx_length = length - ENDPOINT_SIZE;
		endpoint[ep].tx_buffer = data + ENDPOINT_SIZE;
		length = ENDPOINT_SIZE;
	}

	endpoint_copy_to(ep, data, length);
	endpoint_set_tx_status(ep, USB_EP_TX_VALID);
}

void endpoint_receive(uint8_t ep, void *data, uint16_t length, comm_callback next)
{
	endpoint[ep].rx_handler = next;
	endpoint[ep].rx_buffer = data;
	endpoint[ep].rx_length = length;

	endpoint_set_rx_status(ep, USB_EP_RX_VALID);
}
endpoint_send rozpoczyna nadawanie i sama troszczy się o podzielenie danych, które nie mieszczą się do jednego endpointa. Magiczna wartość -1 oznacza że nic już nie bedzie wysyłane. USB nie przesyła długości danych, zamiast tego jeśli host lub urządzenie otrzyma mniej niż wielkość endpointa, to wie że to koniec danych. Natomiast jeśli dostanie 64 bajty (bo tyle mają u mnie endpointy), to musi jeszcze raz rozpocząć transmisje żeby obsłużyć kolejne dane. Dlatego przesłanie pustego pakietu ma inne znaczenie niż nie przesłanie go wcale...

Sztuczkę z callbackiem podpatrzyłem w języku JavaScript - tam w ten sposób realizowane są czasochłonne operacje. Program zamiast czekać na ich zakończenie, robi swoją robotę a na koniec otrzymuje wywołanie zwrotne.
Teraz czas na rozbudowanie procedury obsługi przerwania:

Kod: Zaznacz cały

static void use_callback(uint8_t ep, comm_callback *handler)
{
	comm_callback callback = *handler;
	if (callback == NULL)
		return;

	*handler = NULL;
	callback(ep);
}

void USB_IRQHandler(void)
{
	volatile uint32_t status = USB->ISTR;
	struct setup_request req;

	if (status & USB_ISTR_RESET) {
		USB->ISTR = 0;
		usb_reset();
	} else if (status & USB_ISTR_CTR) {
		uint8_t ep = status & 0xf;

		if (status & USB_ISTR_DIR) {
			endpoint_clear_rx(ep);

			if (endpoint_is_setup_req(ep)) {
				// transaction SETUP (RX)
				endpoint_copy_from(0, &req, sizeof(req));
				handle_setup(&req);
			} else {
				// transaction OUT (RX)
				uint16_t length = endpoint_get_rx_length(ep);
				if (length > endpoint[ep].rx_length)
					length = endpoint[ep].rx_length;
				endpoint_copy_from(ep, endpoint[ep].rx_buffer, length);
				endpoint[ep].rx_buffer += length;
				endpoint[ep].rx_length -= length;
				if (length == ENDPOINT_SIZE)
					endpoint_receive(ep, endpoint[ep].rx_buffer, endpoint[ep].rx_length, endpoint[ep].rx_handler);
				else
					use_callback(ep, &endpoint[ep].rx_handler);
			}

		} else {
			// transaction IN (TX)
			endpoint_clear_tx(ep);

			if (endpoint[ep].tx_length >= 0)
				endpoint_send(ep, endpoint[ep].tx_buffer, endpoint[ep].tx_length, endpoint[ep].tx_handler);
			else
				use_callback(ep, &endpoint[ep].tx_handler);
		}
	}
}
Funkcja pomocnicza use_callback wywołuje funkcję po zakończeniu transmisji, reszta kodu może nie jest piękna - ale wystarczy już do pełnej obsługi HID, a nawet CDC.
Dodałem też nową funkcję handle_setup w której będzie dekodowane otrzymane zgłoszenie.

Cały nowy program wygląda następująco:

Kod: Zaznacz cały

#include <stdio.h>
#include <string.h>
#include "stm32l053xx.h"
#include "endpoint.h"
#include "usb.h"
#include "usb_desc.h"

struct setup_request {
	uint8_t bmRequestType;
	uint8_t bRequest;
	uint16_t wValue;
	uint16_t wIndex;
	uint16_t wLength;
};

typedef void (*comm_callback)(uint8_t);

struct {
	const void *tx_buffer;
	int32_t tx_length;
	comm_callback tx_handler;
	void *rx_buffer;
	int32_t rx_length;
	comm_callback rx_handler;
}endpoint[8];

void endpoint_send(uint8_t ep, const void *data, uint16_t length, comm_callback next)
{
	endpoint[ep].tx_handler = next;
	if (length < ENDPOINT_SIZE) {
		endpoint[ep].tx_buffer = NULL;
		endpoint[ep].tx_length = -1;
	} else {
		endpoint[ep].tx_length = length - ENDPOINT_SIZE;
		endpoint[ep].tx_buffer = data + ENDPOINT_SIZE;
		length = ENDPOINT_SIZE;
	}

	endpoint_copy_to(ep, data, length);
	endpoint_set_tx_status(ep, USB_EP_TX_VALID);
}

void endpoint_receive(uint8_t ep, void *data, uint16_t length, comm_callback next)
{
	endpoint[ep].rx_handler = next;
	endpoint[ep].rx_buffer = data;
	endpoint[ep].rx_length = length;

	endpoint_set_rx_status(ep, USB_EP_RX_VALID);
}

static void usb_reset(void)
{
	endpoint_init(0, 0x040, 0x080);
	endpoint_init(1, 0x0c0, 0x100);

	USB->DADDR = USB_DADDR_EF;

	endpoint_use_rx_data0(0);
	endpoint_set_type(0, USB_EP_CONTROL);
	endpoint_set_rx_status(0, USB_EP_RX_VALID);
	endpoint_set_rx_status(1, USB_EP_RX_DIS);
	endpoint_set_tx_status(1, USB_EP_TX_DIS);
}

static void ack_received_handler(uint8_t ep)
{
	endpoint_set_rx_status(0, USB_EP_RX_VALID);
}

static void data_sent_handler(uint8_t ep)
{
	endpoint_receive(ep, NULL, 0, ack_received_handler);
}

static void get_descriptor(const struct setup_request *req)
{
	uint16_t len;
	const uint8_t *pbuf;

	switch (req->wValue >> 8) {
	case USB_DESC_TYPE_DEVICE:
		pbuf = device_desc;
		len = sizeof(device_desc);
		break;
	case USB_DESC_TYPE_CONFIGURATION:
		pbuf = configuration_desc;
		len = sizeof(configuration_desc);
		break;
	case HID_REPORT_DESC:
		pbuf = mouse_report_desc;
		len = sizeof(mouse_report_desc);
		break;
	default:
		//send_error();
		return;
	}

	if (len > req->wLength)
		len = req->wLength;
	endpoint_send(0, pbuf, len, data_sent_handler);
}

static void handle_setup(const struct setup_request *req)
{
	get_descriptor(req);
}

static void use_callback(uint8_t ep, comm_callback *handler)
{
	comm_callback callback = *handler;
	if (callback == NULL)
		return;

	*handler = NULL;
	callback(ep);
}

void USB_IRQHandler(void)
{
	volatile uint32_t status = USB->ISTR;
	struct setup_request req;

	if (status & USB_ISTR_RESET) {
		USB->ISTR = 0;
		usb_reset();
	} else if (status & USB_ISTR_CTR) {
		uint8_t ep = status & 0xf;

		if (status & USB_ISTR_DIR) {
			endpoint_clear_rx(ep);

			if (endpoint_is_setup_req(ep)) {
				// transaction SETUP (RX)
				endpoint_copy_from(0, &req, sizeof(req));
				handle_setup(&req);
			} else {
				// transaction OUT (RX)
				uint16_t length = endpoint_get_rx_length(ep);
				if (length > endpoint[ep].rx_length)
					length = endpoint[ep].rx_length;
				endpoint_copy_from(ep, endpoint[ep].rx_buffer, length);
				endpoint[ep].rx_buffer += length;
				endpoint[ep].rx_length -= length;
				if (length == ENDPOINT_SIZE)
					endpoint_receive(ep, endpoint[ep].rx_buffer, endpoint[ep].rx_length, endpoint[ep].rx_handler);
				else
					use_callback(ep, &endpoint[ep].rx_handler);
			}

		} else {
			// transaction IN (TX)
			endpoint_clear_tx(ep);

			if (endpoint[ep].tx_length >= 0)
				endpoint_send(ep, endpoint[ep].tx_buffer, endpoint[ep].tx_length, endpoint[ep].tx_handler);
			else
				use_callback(ep, &endpoint[ep].tx_handler);
		}
	}
}

static void clocks_config(void)
{
	RCC->APB1ENR |= RCC_APB1ENR_PWREN;
	RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;

	PWR->CR = (PWR->CR & ~PWR_CR_VOS_Msk) | PWR_CR_VOS_0;

	RCC->CR |= RCC_CR_HSEBYP | RCC_CR_HSEON;
	while ((RCC->CR & RCC_CR_HSERDY) == 0) {}

    RCC->CFGR = (RCC->CFGR & ~(RCC_CFGR_PLLSRC_Msk | RCC_CFGR_PLLMUL_Msk | RCC_CFGR_PLLDIV_Msk))
    		| RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMUL12 | RCC_CFGR_PLLDIV3;

    RCC->CR |= RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY) == 0) {}

    RCC->CFGR = (RCC->CFGR & ~(RCC_CFGR_HPRE_Msk | RCC_CFGR_PPRE1_Msk | RCC_CFGR_PPRE2_Msk))
    		| RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE1_DIV1 | RCC_CFGR_PPRE2_DIV1 | RCC_CFGR_SW_PLL;

	RCC->CRRCR |= RCC_CRRCR_HSI48ON;
	RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
	SYSCFG->CFGR3 |= SYSCFG_CFGR3_ENREF_HSI48;

	while ((RCC->CRRCR & RCC_CRRCR_HSI48RDY) == 0) {}

	RCC->CCIPR |= RCC_CCIPR_HSI48MSEL;

	RCC->IOPENR |= RCC_IOPENR_GPIOAEN | RCC_IOPENR_GPIOBEN | RCC_IOPENR_GPIOCEN | RCC_IOPENR_GPIODEN | RCC_IOPENR_GPIOHEN;
	RCC->APB1ENR |= RCC_APB1ENR_USBEN;
}

static void usb_init(void)
{
	USB->CNTR = 0;
	USB->CNTR = USB_CNTR_FRES;
	USB->ISTR = 0;
	USB->CNTR = USB_CNTR_CTRM | USB_CNTR_RESETM;

	USB->BTABLE = 0;
	USB->BCDR |= USB_BCDR_DPPU;

	usb_reset();

	NVIC_SetPriority(USB_IRQn, 0);
	NVIC_EnableIRQ(USB_IRQn);
}

int main(void)
{
	clocks_config();

	usb_init();

	while (1)  {
	}
}
Na razie nie ma dekodowanai żądania, więc zawsze odsyłany jest po prostu deskryptor urządzenia. Niby to niewiele, ale możemy zobaczyć, że pierwsze zapytanie działa już w pełni poprawnie:

Obrazek

Mamy już prawie gotowy program. W następnej części dodamy dekodowanie zapytań oraz nieco więcej deskryptorów i będziemy mieli w pełni działający stos USB.

ODPOWIEDZ