Nie takie USB straszne jak je malują, cz.4 - własna biblioteka

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.4 - własna biblioteka

Post autor: elvis » 06 lip 2019, 12:30

Poprzednio opisałem jak wyglądają rejestry sprzętowe modułu USB w mikrokontrolerze STM32L053 teraz opiszę bardzo prostą bibliotekę, która pozwoli na ukrycie pewnej niewygody w dostępie do nich.
Cube HAL zawiera odpowiedniki tych funkcji, ja postanowiłem napisać je samemu, żeby lepiej zrozumieć działanie układu - ale zachęcam do używania HAL-a, żeby nie było na mnie jak coś będzie źle.

Po pierwsze postanowiłem nieco uprościć konfigurację. Endpointy mogą mieć właściwie dowolne wielkości w obrębie dostępnej pamięci oczywiści. Ja ustaliłem wielkość wszystkich na 64 bajty. Ustawiam również numer endpointa (pole EA) na równe indeksowi rejestru.

Moja biblioteka ma następujący interfejs (plik endpoint.h):

Kod: Zaznacz cały

#ifndef ENDPOINT_H_
#define ENDPOINT_H_

#include <stdint.h>
#include <stdbool.h>

#define	ENDPOINT_SIZE					64

void endpoint_init(uint8_t ep, uint16_t rx_offset, uint16_t tx_offset);
void endpoint_set_rx_status(uint8_t ep, uint16_t status);
void endpoint_set_tx_status(uint8_t ep, uint16_t status);
void endpoint_use_rx_data0(uint8_t ep);
void endpoint_use_tx_data0(uint8_t ep);
void endpoint_set_type(uint8_t ep, uint16_t type);
void endpoint_clear_rx(uint8_t ep);
void endpoint_clear_tx(uint8_t ep);
bool endpoint_is_setup_req(uint8_t ep);
void endpoint_copy_from(uint8_t ep, void *data, uint16_t length);
void endpoint_copy_to(uint8_t ep, const void *data, uint16_t length);
uint16_t endpoint_get_rx_length(uint8_t ep);

#endif /* ENDPOINT_H_ */
Stała ENDPOINT_SIZE określa wspomniany rozmiar endpointów, poniżej krótki opis zdefiniowanych funkcji:
  • endpoint_init - wypełnia wpis w "spisie treści" buforów, przypisuje bitom EA wartość równą numerowi endpointu
  • endpoint_set_rx_status - ustawia status endpointa odbiorczego (DIABLED, STALL, NAK, VALID)
  • endpoint_set_tx_status - ustawia status endpointa nadawczego
  • endpoint_use_rx_data0 - wymusza użycie pakietu DATA0 podczas następnego odbioru danych
  • endpoint_use_tx_data0 - wymusza użycie pakietu DATA0 podczas następnego wysyłania danych
  • endpoint_set_type - ustawia typ endpointu (CONTROL, INTERRUPT, BULK lub ISOCHRONOUS)
  • endpoint_clear_rx - kasuje flagę przerwania po odebraniu danych
  • endpoint_clear_tx - kasuje flagę przerwania po wysłaniu danych
  • endpoint_is_setup_req - zwraca true jeśli otrzymano żądanie SETUP
  • endpoint_copy_from - kopiuje dane z endpointa
  • endpoint_copy_to - kopiuje dane do endpointa
  • endpoint_get_rx_length - zwraca liczbę odebranych bajtów
Program nie jest może piękny, ale jeśli się komuś przyda zamieszczam go poniżej (plik endpoint.c):

Kod: Zaznacz cały

#include "stm32l053xx.h"
#include "endpoint.h"

#define EP_REG(n)			(*(volatile uint16_t *)((uint32_t)&USB->EP0R + (n) * 4))

struct endpoint_mem_desc {
  __IO uint16_t ADDR_TX;
  __IO uint16_t COUNT_TX;
  __IO uint16_t ADDR_RX;
  __IO uint16_t COUNT_RX;
};

static struct endpoint_mem_desc *USB_EP = (struct endpoint_mem_desc*)USB_PMAADDR;

void endpoint_init(uint8_t ep, uint16_t rx_offset, uint16_t tx_offset)
{
	uint16_t epr = EP_REG(ep);
	EP_REG(ep) = (epr & USB_EPREG_MASK) | ep | USB_EP_CTR_RX | USB_EP_CTR_TX;

	USB_EP[ep].ADDR_RX = rx_offset;
	USB_EP[ep].COUNT_RX = 0x8800;
	USB_EP[ep].ADDR_TX = tx_offset;
	USB_EP[ep].COUNT_TX = 0;
}

void endpoint_set_rx_status(uint8_t ep, uint16_t status)
{
	uint16_t epr = EP_REG(ep) & USB_EPRX_DTOGMASK;

	if (status == USB_EP_RX_VALID)
		USB_EP[ep].COUNT_RX = 0x8800;

	if (status & USB_EPRX_DTOG1)
		epr ^= USB_EPRX_DTOG1;
	if (status & USB_EPRX_DTOG2)
		epr ^= USB_EPRX_DTOG2;

	EP_REG(ep) = epr | USB_EP_CTR_RX | USB_EP_CTR_TX;

}

void endpoint_set_tx_status(uint8_t ep, uint16_t status)
{
	uint16_t epr = EP_REG(ep) & USB_EPTX_DTOGMASK;

	if (status & USB_EPTX_DTOG1)
		epr ^= USB_EPTX_DTOG1;
	if (status & USB_EPTX_DTOG2)
		epr ^= USB_EPTX_DTOG2;

	EP_REG(ep) = epr | USB_EP_CTR_RX | USB_EP_CTR_TX;
}

void endpoint_use_rx_data0(uint8_t ep)
{
	uint16_t epr = EP_REG(ep);
	if (epr & USB_EP_DTOG_RX)
		EP_REG(ep) = (epr & USB_EPREG_MASK) | USB_EP_DTOG_RX | USB_EP_CTR_RX | USB_EP_CTR_TX;
}

void endpoint_use_tx_data0(uint8_t ep)
{
	uint16_t epr = EP_REG(ep);
	if (epr & USB_EP_DTOG_TX)
		EP_REG(ep) = (epr & USB_EPREG_MASK) | USB_EP_DTOG_TX | USB_EP_CTR_RX | USB_EP_CTR_TX;
}

void endpoint_set_type(uint8_t ep, uint16_t type)
{
	uint16_t epr = EP_REG(ep);
	EP_REG(ep) = (epr & USB_EP_T_MASK) | type | USB_EP_CTR_RX | USB_EP_CTR_TX;
}

void endpoint_clear_rx(uint8_t ep)
{
	uint16_t epr = EP_REG(ep) & USB_EPREG_MASK;
	EP_REG(ep) = (epr | USB_EP_CTR_TX) & ~USB_EP_CTR_RX;
}

void endpoint_clear_tx(uint8_t ep)
{
	uint16_t epr = EP_REG(ep) & USB_EPREG_MASK;
	EP_REG(ep) = (epr | USB_EP_CTR_RX) & ~USB_EP_CTR_TX;
}

bool endpoint_is_setup_req(uint8_t ep)
{
	uint16_t epr = EP_REG(ep) & USB_EPREG_MASK;
	if (epr & USB_EP_SETUP)
		return true;
	else
		return false;
}

void endpoint_copy_from(uint8_t ep, void *data, uint16_t length)
{
	volatile uint16_t *p = (volatile uint16_t*)(USB_PMAADDR + USB_EP[ep].ADDR_RX);
	uint8_t *pdata = data;

	for (int i = 0; i < length / 2; i++) {
		uint16_t tmp = *p++;
		*pdata++ = tmp;
		*pdata++ = tmp >> 8;
	}
	if (length & 1) {
		uint16_t tmp = *p++;
		*pdata++ = tmp;
	}
}

void endpoint_copy_to(uint8_t ep, const void *data, uint16_t length)
{
	volatile uint16_t *p = (volatile uint16_t*)(USB_PMAADDR + USB_EP[ep].ADDR_TX);
	const uint8_t *pdata = data;

	for (int i = 0; i < length / 2; i++) {
		uint16_t tmp = pdata[i * 2] + ((pdata[i * 2 + 1]) << 8);
		*p++ = tmp;
	}
	if (length & 1) {
		uint16_t tmp = pdata[length - 1];
		*p++ = tmp;
	}
	USB_EP[ep].COUNT_TX = length;
}

uint16_t endpoint_get_rx_length(uint8_t ep)
{
	volatile uint16_t rx = USB_EP[ep].COUNT_RX;
	return rx & 0x3ff;
}
Taka bardzo uproszczona biblioteka pozwala na przesyłanie danych o wielkości co najwyżej 64 bajtów, ale można napisać pierwszy program i spróbować go uruchomić:

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

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

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));
				// TODO: handle SETUP request
				asm volatile ("bkpt #1");

			} else {
				// transaction OUT (RX)
				// TODO:
			}

		} else {
			// transaction IN (TX)
			// TODO:
		}
	}
}

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)  {
	}
}
Pomijając długą i nudną inicjalizację sprzętu najważniejsza jest procedura obsługi przerwania, czyli USB_IRQHandler:

Kod: Zaznacz cały

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));
				// TODO: handle SETUP request
				asm volatile ("bkpt #1");

			} else {
				// transaction OUT (RX)
				// TODO:
			}

		} else {
			// transaction IN (TX)
			// TODO:
		}
	}
}
Po odebranu pierwszego pakiety czyli żądania SETUP program się zatrzymuje:

Obrazek

W zmiennej req znajdziemy odebrane dane, o których napiszę za chwilę. Dla upewnienia się że wszystko działa jeszcze zrzut ekranu analizatora:

Obrazek

Jak widać to co pokazuje debuger odpowiada danym z analizatora - więc pierwsze dane udało się odebrać. Czas przeanalizować otrzymane dane i odpowiedzieć hostowi.

Dodano po 19 minutach 48 sekundach:
Format żądań hosta zdefiniowany jest przez standard USB, więcej szczegółów znajdziemy na stronie https://usb.org. Pakiet SETUP ma rozmiar 8 bajtów do których dekodowania zadeklarowałem strukturę setup_request:

Kod: Zaznacz cały

struct setup_request {
	uint8_t bmRequestType;
	uint8_t bRequest;
	uint16_t wValue;
	uint16_t wIndex;
	uint16_t wLength;
};
Pierwszy bajt czyli bmRequestType ma następujące znaczenie:

Obrazek

Otrzymana wartość to 0x80, czyli zapytanie (Device-to-host), adresowane do urządzenia (Device), a samo zapytanie to standardowe zapytanie USB. Możliwe są jeszcze zapytania zdefinowane dla klasy (u nas HID, inne ma np. CDC). Możliwe są też zapytania specyficzne dla producenta (Vendor).
Standardowe zapytania znajdziemy w dokumentacji protokołu USB:

Obrazek

Otrzymane przez nas zapytanie znajdziemy w polu bRequest. Jego wartość to 0x06, czyli GET_DESCRIPTOR. W pierwszej części widzieliśmy jak przebiega komunikacja z działającym stosem - na początku host pyta o deskryptor podłączonego urządzenia. Właśnie takie zapytanie teraz otrzymaliśmy.
Powinniśmy w odpowiedzi odesłać odpowiedni deskryptor. Musimy go jednak najpierw przygotować oraz nieco udoskonalić program.
Za chwilę pokażę przykładowy program, który pozwoli odesłać żądanie do hosta.

ODPOWIEDZ