О, приятно видеть единомышленника. Я тоже изумился косоте, запутанности и размеру стандартной USB-библиотеки для STM32 и начал писать свою реализацию. Кстати, в качестве модельного примера взял именно USB Audio Class.
Сейчас я застрял на том, что прерывание USB RESET я отлавливаю, но вот нулевая конечная точка почему-то не хочет принимать данные. Пишу под STM32L152, потому что у меня есть отладка с ним.
Итак, чтобы завести USB, нам надо:
1. четко представлять себе, как работает собственно USB;
2. научиться взаимодействовать с железкой конкретного МК.
Говорю сразу, что без знания английского разобраться невозможно. Все адекватные источники написаны именно на этом языке.
Для достижения цели №1 можно действовать так:
1.1. почитать
USB made simple и/или
USB in a nutshell. Первое мне понравилось больше.
1.2 Почитать стандарт на какой-нибудь класс. Например, на
USB Audio Device Class.
1.4 Опционально заглянуть в главу 9
общего описания USB (архив, в нем файл usb_20.pdf).
После этого приходит понимание, что сам по себе USB не так страшен, как о нем рассказывают, за одним исключением: если пытаться написать
универсально конфигурируемый движок USB, выйдет неудобоваримое чудовище, что и вышло у ST.
Правда, на то, чтобы понять, что USB не так страшен, у меня ушло два полных дня (~20 часов) непрерывного осознания мануалов (включая reference manual на чип) и стандартов (с перерывами только на поесть и сходить в туалет), приведенных выше (именно осознания, с чтением английского проблем нет).
После этого можно переходить к пункту 2 - настройке железа. Что дает блок USB? Он обеспечивает прием и передачу пакетов. То есть, он сам разбирается со всякими SOF, ACK/NAK и прочим, сам считает и проверят CRC и т.д. То есть, когда хост совершает транзакцию OUT, в идеале должно вызваться прерывание, в котором мы увидим, что конечная точка N приняла данные, заберем готовые, уже положенные в буфер, данные и обработаем их. При совершении транзакции IN блок USB сам заберет подготовленные данные из буфера и отдаст их хосту.
Чтобы разобраться с этим, надо читать reference manual на используемый чип. После этого можно начинать писать код с оглядкой на уже прочитанное.
На этом этапе ко мне пришло понимание, что разобраться с тем, что наворотили ST в своем блоке USB, сложнее, чем понять собственно стандарт USB. Самый главный вынос мозга - USB packet memory. Я так до конца пока и не понял, как она организована. То ли так:
[два байта настоящей памяти USB][два байта выравнивания][два байта настоящей памяти USB][два байта выравнивания][два байта настоящей памяти USB][два байта выравнивания]...
то ли так:
[байт памяти USB][три байта выравнивания][байт памяти USB][три байта выравнивания][байт памяти USB][три байта выравнивания]...
Сейчас я склонился к первому варианту, поскольку он больше похож на то, что следует из формул, приведенных в RM. У меня есть смутные подозрения, что моя конечная точка не работает именно потому, что при заполнении таблицы адресов буферов я все же промахнулся... Вот что им стоило
нарисовать memory map, а?..
Сейчас сижу и медитирую над мануалами.
Если интересно, можете взглянуть на то, что я написал. Разумеется, это просто некий экспериментальный проект, до решения ему еще далеко... Пишу в IAR 6.30.
Как я уже говорил, сейчас проблема в том, что я
не получаю прерывания по корректному приему. Есть подозрения, что это из-за неверной конфигурации таблицы адресов, произошедшей из-за неверного доступа к памяти USB. Понять бы, как в нее писать/читать корректно...
Всю эту мороку мужики из ST запилили из благих побуждений - для большей гибкости. Если бы они сделали четко выделенные буферы для каждой конечной точки, проблем бы не было.
Спойлер
Код: Выделить всё
#include "stm32l1xx.h"
/* USB registers definitions */
#define USB_REG_BASE_ADDR 0x40005C00UL
#define USB_MEM_BASE_ADDR 0x40006000UL
#define USB_MEM_SIZE_B 512UL
__no_init __root uint32_t USB_EP0R @ (USB_REG_BASE_ADDR + 0x00);
__no_init __root uint32_t USB_EP1R @ (USB_REG_BASE_ADDR + 0x04);
__no_init __root uint32_t USB_EP2R @ (USB_REG_BASE_ADDR + 0x08);
__no_init __root uint32_t USB_EP3R @ (USB_REG_BASE_ADDR + 0x0C);
__no_init __root uint32_t USB_EP4R @ (USB_REG_BASE_ADDR + 0x10);
__no_init __root uint32_t USB_EP5R @ (USB_REG_BASE_ADDR + 0x14);
__no_init __root uint32_t USB_EP6R @ (USB_REG_BASE_ADDR + 0x18);
__no_init __root uint32_t USB_EP7R @ (USB_REG_BASE_ADDR + 0x1C);
__no_init __root uint32_t USB_CNTR @ (USB_REG_BASE_ADDR + 0x40);
__no_init __root uint32_t USB_ISTR @ (USB_REG_BASE_ADDR + 0x44);
__no_init __root uint32_t USB_FNR @ (USB_REG_BASE_ADDR + 0x48);
__no_init __root uint32_t USB_DADDR @ (USB_REG_BASE_ADDR + 0x4C);
__no_init __root uint32_t USB_BTABLE @ (USB_REG_BASE_ADDR + 0x50);
/* Endpoint buffer description pseudo-register table definitions */
//Add other definitions as needed
#define USB_BTABLE_LOC 0UL
#define USBMEM_OFFSET_RECALC(REC_NUM,REG_NUM) (USB_MEM_BASE_ADDR + USB_BTABLE_LOC + (REC_NUM)*16UL + (REG_NUM)*4UL)
//The length of a table below; adjust as needed according to actual record count
#define USBMEM_TABLE_LENGTH_BYTES 32UL
//Endpoint 0 description record
__no_init __root uint32_t USB_ADDR0_TX @ (USBMEM_OFFSET_RECALC(0,0));
__no_init __root uint32_t USB_COUNT0_TX @ (USBMEM_OFFSET_RECALC(0,1));
__no_init __root uint32_t USB_ADDR0_RX @ (USBMEM_OFFSET_RECALC(0,2));
__no_init __root uint32_t USB_COUNT0_RX @ (USBMEM_OFFSET_RECALC(0,3));
//Endpoint 1 description record
__no_init __root uint32_t USB_ADDR1_TX @ (USBMEM_OFFSET_RECALC(1,0));
__no_init __root uint32_t USB_COUNT1_TX @ (USBMEM_OFFSET_RECALC(1,1));
__no_init __root uint32_t USB_ADDR1_RX @ (USBMEM_OFFSET_RECALC(1,2));
__no_init __root uint32_t USB_COUNT1_RX @ (USBMEM_OFFSET_RECALC(1,3));
//Endpoint 2 description record
__no_init __root uint32_t USB_ADDR2_TX @ (USBMEM_OFFSET_RECALC(2,0));
__no_init __root uint32_t USB_COUNT2_TX @ (USBMEM_OFFSET_RECALC(2,1));
__no_init __root uint32_t USB_ADDR2_RX @ (USBMEM_OFFSET_RECALC(2,2));
__no_init __root uint32_t USB_COUNT2_RX @ (USBMEM_OFFSET_RECALC(2,3));
//Endpoint 3 description record
__no_init __root uint32_t USB_ADDR3_TX @ (USBMEM_OFFSET_RECALC(3,0));
__no_init __root uint32_t USB_COUNT3_TX @ (USBMEM_OFFSET_RECALC(3,1));
__no_init __root uint32_t USB_ADDR3_RX @ (USBMEM_OFFSET_RECALC(3,2));
__no_init __root uint32_t USB_COUNT3_RX @ (USBMEM_OFFSET_RECALC(3,3));
//Add more (up to 8 total) endpoint records as needed
#define ENDP_BLOCKSIZE_32B (16UL<<10UL)
#define ENDP0_TXBUFFER_SIZE_BYTES 32
#define ENDP0_TXBUFFER_START_LOC (USBMEM_TABLE_LENGTH_BYTES)
#define ENDP0_TXBUFFER_START_ABS (USB_MEM_BASE_ADDR + USBMEM_TABLE_LENGTH_BYTES*2UL)
#define ENDP0_RXBUFFER_SIZE_BYTES 32
#define ENDP0_RXBUFFER_START_LOC (USBMEM_TABLE_LENGTH_BYTES + ENDP0_TXBUFFER_SIZE_BYTES)
#define ENDP0_RXBUFFER_START_ABS (USB_MEM_BASE_ADDR + (USBMEM_TABLE_LENGTH_BYTES + ENDP0_BUFFER_SIZE_BYTES)*2UL)
volatile uint8_t ENDP0_RAM_RX_buffer[32];
volatile uint8_t ENDP0_RAM_TX_buffer[32];
uint8_t DEVICE_DESCRIPTOR[] = {0x12,0x01,0x01,0x00,0x00,0x00,0x00,0x20,0x12,0x34,0x56,0x78,0x01,0x00,0x00,0x00,0x00,0x01};
/* Copy data from hardware USB buffer to RAM */
void USB_copy_from_RX_memory(uint32_t *HW_buffer,uint16_t size_bytes,uint8_t *RAM_buffer)
{
uint16_t i,k;
k=0;
for (i=0; i<(size_bytes/2); i++)
{
RAM_buffer[k]=(HW_buffer[i] & 0x000000FF);
RAM_buffer[k+1]=(HW_buffer[i] & 0x0000FF00) >> 8;
k=+2;
}
}
/* Copy data to hardware USB buffer from RAM */
void USB_copy_to_TX_memory(uint32_t *HW_buffer,uint16_t size_bytes,uint8_t *RAM_buffer)
{
uint16_t i,k;
k=0;
for (i=0; i<(size_bytes/2); i++)
{
HW_buffer[i]=RAM_buffer[k] | (RAM_buffer[k+1]<<8);
k+=2;
}
}
void USB_clock_setup(void)
{
/* System clock setup with USB module in mind */
/* Switch to HSI to configure PLL */
//Turn HSI on
RCC->CR|=RCC_CR_HSION;
while (!(RCC->CR & RCC_CR_HSIRDY))
{
//Make sure HSI is active
}
//Switch to HSI for core clocks
RCC->CFGR=RCC_CFGR_SW_0;
//Turn PLL off
RCC->CR&=~RCC_CR_PLLON;
while (RCC->CR & RCC_CR_PLLRDY)
{
//Make sure PLL is fully stopped
}
//Set PLL input to HSE, multiplication by 12 (96/2 = 48 MHz for USB), division to 4 (96/4 = 24 MHz SYSCLK)
//All prescalers are set to 1
RCC->CFGR|=RCC_CFGR_PLLDIV_1 | RCC_CFGR_PLLDIV_0 | RCC_CFGR_PLLMUL_2 | RCC_CFGR_PLLSRC;
//Configure memory to work with 1 wait state (core clock above 16 MHz)
//Enable 64-bit access
FLASH->ACR |= FLASH_ACR_ACC64;
//Enable Prefetch Buffer
FLASH->ACR |= FLASH_ACR_PRFTEN;
//Flash 1 wait state
FLASH->ACR |= FLASH_ACR_LATENCY;
//Turn PLL on
RCC->CR|=RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY))
{
//Make sure PLL is ready
}
/* Switch system clock to PLL */
RCC->CFGR|=RCC_CFGR_SW_0 | RCC_CFGR_SW_1;
//Turn on USB clock and port A clock (SYSCFG is used for USB pullup)
RCC->AHBENR=RCC_AHBENR_GPIOAEN;
RCC->APB1ENR=RCC_APB1ENR_USBEN;
RCC->APB2ENR=RCC_APB2ENR_SYSCFGEN;
//Now USB should have proper clock
}
void USB_hw_setup(void)
{
volatile uint32_t i;
/* General USB module setup */
//Switching ON the analog part
USB_CNTR&=~USB_CNTR_PDWN;
for (i=0; i<500UL; i++)
{
//Wait for analog part to start-up
}
//Remove reset, initialize register
USB_CNTR=0;
//Enable interrupts: correct transfer, buffer error, any error, bus reset, interrupt on SOF,
USB_CNTR|=USB_CNTR_CTRM | USB_CNTR_RESETM | USB_CNTR_SOFM | USB_CNTR_PMAOVRM | USB_CNTR_ERRM;
//Clear possible spurious interrupts
USB_ISTR=0;
//Enable low prioruty USB interrupt
NVIC_EnableIRQ(USB_LP_IRQn);
}
void USB_reset_setup(void)
{
/* Endpoint 0 memory setup */
//Set relative position of the buffer table
USB_BTABLE=USB_BTABLE_LOC;
//Endpoint 0 buffer configuration
USB_ADDR0_TX=ENDP0_TXBUFFER_START_LOC;
USB_ADDR0_RX=ENDP0_RXBUFFER_START_LOC;
USB_COUNT0_TX=ENDP0_TXBUFFER_SIZE_BYTES;
USB_COUNT0_RX=ENDP_BLOCKSIZE_32B; //Allocate 1 block of 32 bytes
//Endpoint 0 is a control endpoint
USB_EP0R=USB_EP0R_EP_TYPE_0 | USB_EP0R_STAT_TX_0 | USB_EP0R_STAT_TX_1;
USB_copy_to_TX_memory((uint32_t*)ENDP0_TXBUFFER_START_ABS,18,DEVICE_DESCRIPTOR);
//Set USB address to 0
USB_DADDR=USB_DADDR_EF;
//Allow USB pullup
SYSCFG->PMC|=SYSCFG_PMC_USB_PU;
}
void USB_LP_IRQHandler(void)
{
if (USB_ISTR & USB_ISTR_RESET)
{
/* Master issued RESET */
//Re-initiaze HW
USB_reset_setup();
GPIOB->ODR|=GPIO_ODR_ODR_6;
}
if (USB_ISTR & USB_ISTR_CTR)
{
/* Some endpoint has received data */
GPIOB->ODR|=GPIO_ODR_ODR_7;
//Refresh endpoint state
USB_EP0R=USB_EP0R_EP_TYPE_0 | USB_EP0R_STAT_TX;
}
if (USB_ISTR & USB_ISTR_SOF)
{
/* SOF sync detected */
}
if (USB_ISTR & USB_ISTR_PMAOVR)
{
/* Packet memory area buffer overrun/underrun */
}
if (USB_ISTR & USB_ISTR_ERR)
{
/* General reception error */
}
//All interrupt sources serviced
USB_ISTR=0;
}
void main(void)
{
volatile uint32_t i;
USB_clock_setup();
USB_hw_setup();
USB_reset_setup();
//Test LEDs
RCC->AHBENR=RCC_AHBENR_GPIOBEN;
//PB6 and PB7 are outputs
GPIOB->MODER=GPIO_MODER_MODER7_0 | GPIO_MODER_MODER6_0;
//All outputs push-pull
GPIOB->OTYPER=0;
//High speed mode
GPIOB->OSPEEDR=GPIO_OSPEEDER_OSPEEDR7_0 | GPIO_OSPEEDER_OSPEEDR7_1 | GPIO_OSPEEDER_OSPEEDR6_0 | GPIO_OSPEEDER_OSPEEDR6_1;
//Init to 0
GPIOB->ODR=0;
__enable_interrupt();
while (1)
{
for (i=0; i<200000UL; i++)
{
}
GPIOB->ODR=0;
}
}
Если же обмен пакетами налажен, то дальше вся работа сводится к тому, чтобы на этапе настройки передавать нужные дескрипторы в ответ на соответствующие запросы, а после этого - просто писать и читать нужные буферы конечных точек. Дескрипторы тоже не так страшны - это write-only вещь. Сначала можно потратить часок на их формирование соответственно стандарту, после чего забыть про все и воспринимать их как предопределенные двоичные блоки.