:::: MENU ::::

STM32L, USB, CDC, виртуальный COM-порт, начало

Попробуем разобраться с реализацией USB на STM32L. Библиотека от ST просто ужасна. Нет, она прекрасно работает, но на ее примере разобраться с USB навряд ли получится. Она написана очень сложно. А вот на примере от keil (у них есть свой пример для USB) разобраться можно. Вот на таком немного переработанном примере я и постараюсь объяснить (хотя бы для самого себя) как можно реализовать USB на микроконтроллере.

В примере реализуется CDC устройство.

USB на ПК определяет подключение к шине какого-то типа устройства по появлению подтяжки на DM или DP.

  • pull-up на D- на стороне девайса — девайс говорит, что он Low-speed;
  • pull-up на D+ на стороне девайса — девайс говорит, что он Full-speed или High-speed (уточняется в дальнейшем диалоге с хостом);
  • оба пина без подтяжек на стороне девайса — он отключен от шины, и хост с ним не работает;

Для подключения резистора к DP нам понадобится модуль SYSCFG и регистр PMC(кроме того, в библиотеки периферии от ST в файле stm32l1xx_syscfg.c есть функция SYSCFG_USBPuCmd, которая поможет вам записать в этот регистр нужные значение — вкл/выкл подтяжку. Я ей буду пользоваться, поэтому ее надо подключить к проекту).

Инициализацию USB, для нашего микроконтроллера, разделим на 3 части.

  • Настройка частоты тактирования микроконтроллера и вкл. тактирования модулей периферии;
  • Настройка прерываний от USB;
  • Инициализация самого USB модуля (настраиваем через соответствующие регистры);

В самом начале надо настроить тактирование самого микроконтроллера — для этого я обычно изменяю сам файл system_stm32l1xx.c (из стандартного проекта keil дляSTM32). А точнее, функцию SetSysClock. Для USB нам необходимо, чтобы после PLLMUL была частота 96 МГц.

rcc_usb

Если используем HSI 16 МГц, то надо выбрать коэффициент умножения 6. А после, для того, что-бы тактировать микроконтроллер от PLL выбрать, коэффициент деления 3. Тогда на выходе PLL, частота будет 32 МГц. При такой частоте, не забываем настроить пропуск такта при чтении FLASH (она может работать максимум на частоте 24 МГц, и если пропуск не настроить программа будет просто виснуть).

static void SetSysClock(void)
{
    //Turn ON HSI
    RCC->CR |= RCC_CR_HSION;

    //Wait until it's stable
    while (!(RCC->CR & RCC_CR_HSIRDY));

    // input for PLL is HSI
    RCC->CFGR |= RCC_CFGR_PLLSRC_HSI;

    //Switch to HSI as SYSCLK
    //PLL input = HSI
    //PLL division factor = 3
    //PLL multiplication factor = 6
    RCC->CFGR |= RCC_CFGR_PLLDIV2 | RCC_CFGR_PLLMUL3;

    //Turn PLL on
    RCC->CR |= RCC_CR_PLLON;

    //Wait PLL to stabilize
    while (!(RCC->CR & RCC_CR_PLLRDY));

    //Setting up flash for high speed
    FLASH->ACR = FLASH_ACR_ACC64;
    FLASH->ACR |= FLASH_ACR_LATENCY;
    FLASH->ACR |= FLASH_ACR_PRFTEN;

    //Set PLL as SYSCLK
    RCC->CFGR |= RCC_CFGR_SW_PLL;

    //Turn off MSI
    RCC->CR&=~RCC_CR_MSION;
}

Теперь включаем тактирование USB периферии и SYSCFG (для включения подтягивающего резистора — см. выше).

void usb_clock_enable(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB, ENABLE);
}

Теперь переходим к настройке прерываний. В нашей программе мы будем использовать только вектор прерывания USB_LP_IRQHandler.

Он был использован в примере, и если честно, с другими векторами USB прерываний я не работал и не знаю как работать. В Reference Manual про них написано:

  • USB low-priority interrupt (USB_LP_IRQHandler) — страбатывает на всех USBсобытиях (корректная передача, USB reset и т.д.)
  • USB high-priority interrupt (USB_HP_IRQHandler) — срабатывает только на событии корректной передачи для isochronous and double-buffer bulk и используется для достижения наибольшей возможной скорости обмена.
  • USB wakeup interrupt (USB_FS_WKUP_IRQHandler) — срабатывает по просыпанию из Suspend mode.

Настраиваем прерывание:

void USB_Interrupts_Config(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;

  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

  NVIC_InitStructure.NVIC_IRQChannel = USB_LP_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);  
}

Теперь перейдем к самой последовательности инициализации USB периферии. Объяснение действий — см. комментарии в коде. Все регистры для USB и значения, которые в них пишутся задефайнены в файле usb.h, можете посмотреть его скачав проект по ссылке ниже.

void usb_on(void)
{
    // включение подтягивающего резистора
    SYSCFG_USBPuCmd(ENABLE);

    // PowerDown = 0 (включает usb периферию)
    // FORCE USB RESET = 1 (вызывает reset usb прерывание, при сбросе бита из 1 в 0)
    // (остальные биты - отключены другие прерывания)

    // при включении FRES и PWDN установлен в 1, 
    // дополнительно выставляем CNTR_FRES как в примере от ST
    CNTR = CNTR_FRES;

    // сбрасываем FRES - генерируем прерывание 
    // при дальнейшем включении прерывания произойдет вход в него
    CNTR = 0; 

    // сброс всех флагов прерывания
    ISTR = 0;

    // включаем прерывание по USB RESET
    // Suspend mode (3 мс нет трафика)
    // Wakeup - выход из Suspend Mode
    wInterrupt_Mask = CNTR_RESETM | CNTR_SUSPM | CNTR_WKUPM;
    CNTR = wInterrupt_Mask;     
}

Дальнейшая настройка будет происходить в прерывании USB_LP_IRQHandler.

void USB_LP_IRQHandler(void)
{
    uint32_t istr, val, num;

    istr = ISTR;

    // прерывание по событию reset usb
    if(istr & USB_ISTR_RESET)
    {
        // сброс регистра статуса прерывания
        ISTR = 0;

        // включаем прерывание по правильной передаче (USB_CNTR_CTRM)
        // reset последовательности (USB_CNTR_RESETM)
        CNTR = USB_CNTR_CTRM | USB_CNTR_RESETM;

        // нулевая конечная точка расположена в памяти после коненчных точек для виртуального COM порта

        FreeBufAddr = EP_BUF_ADDR; // размер 3 конечные точки
        // регистр btable=0 - адрес где расположены endpoints в packet memory
        // начало packet memory (USB_PMA_ADDR) 0x40006000
        BTABLE = 0;
        // pBUF_DSCR - структура конечной точки (ADDR_TX; COUNT_TX; ADDR_RX; COUNT_RX;)
        pBUF_DSCR->ADDR_TX = FreeBufAddr;
        FreeBufAddr = FreeBufAddr + USB_MAX_PACKET0;
        pBUF_DSCR->ADDR_RX = FreeBufAddr;
        FreeBufAddr = FreeBufAddr + USB_MAX_PACKET0;        

        // устновка размера буфера для приема - см. REFERENCE MANUAL 
        if(USB_MAX_PACKET0 > 62) 
        {           
            pBUF_DSCR->COUNT_RX = 0;
            pBUF_DSCR->COUNT_RX |= BLSIZE;                      
            pBUF_DSCR->COUNT_RX |= NUM_BLOCK_1_BYTE; 
        }
        else
        {
            pBUF_DSCR->COUNT_RX = 0;
            pBUF_DSCR->COUNT_RX |= NUM_BLOCK_3_BYTE;
        }

        // тип  конечной точки control
        // статус приема - правильный прием
        EPxREG(0) = EP_CONTROL | EP_RX_VALID;

        // включаем нулевую конечную точку
        // кроме того daddr содержит уникальный адрес устройства
        DADDR = USB_DADDR_EF | 0;       
    }

    // возниклло прерывания и передача завершена
    while ((istr = ISTR) & USB_ISTR_CTR) 
    {
        // сброс флага прерывания "передача завершена"
        ISTR = ~USB_ISTR_CTR;

        // определяем конечную точку по которой произошло прерывание
        // и какой запрос - прерывание запрос/передача
        num = istr & USB_ISTR_EP_ID;
        val = EPxREG(num);

        if(val & USB_EP0R_CTR_RX)
        {
            EPxREG(num) = val & ~USB_EP0R_CTR_RX & EP_MASK;         
            if(val & EP_SETUP) 
            {
                if(num == 0) USB_EndPoint0(USB_EVT_SETUP);                  
                if(num == 1)
                {
                    USB_EndPoint1(USB_EVT_SETUP);
                }                   
                if(num == 2) USB_EndPoint2(USB_EVT_SETUP);
                if(num == 3) USB_EndPoint3(USB_EVT_SETUP);                  
            }

            else
            {
                if(num == 0) USB_EndPoint0(USB_EVT_OUT);                    
                if(num == 1)
                {
                    USB_EndPoint1(USB_EVT_OUT);
                }                   
                if(num == 2) USB_EndPoint2(USB_EVT_OUT);
                if(num == 3) USB_EndPoint3(USB_EVT_OUT);                    
            }       
        }

        if(val & USB_EP0R_CTR_TX)
        {
            EPxREG(num) = val & ~USB_EP0R_CTR_TX & EP_MASK;
            if(num == 0) USB_EndPoint0(USB_EVT_IN);                     
            if(num == 1) USB_EndPoint1(USB_EVT_IN);
            if(num == 2) USB_EndPoint2(USB_EVT_IN);
            if(num == 3) USB_EndPoint3(USB_EVT_IN);                         
        }               
    }
}

При общении с той или иной конечной точкой, вызывается соответствующая функция — USB_EndPoint0, USB_EndPoint1, USB_EndPoint2, USB_EndPoint3.

Именно в нулевой конечной точки реализованы все стандартные запросы USB, запросы специфические для CDC устройства (вирт. COM-порта). При запросе к нулевой точки, отдается дескриптор устройства (который необходимо заранее прописать).

Вторая и третья конечная точка вызываются при приеме и передачи данных соответственно. Точнее на передачу конечная точка вызывается постоянно с какой-то периодичностью, просто, грубо говоря, передается ноль байт (можете посмотреть в отладчике — постоянное попадание в функцию USB_EndPoint3). А в конечную точку на прием попадаем, лишь когда пришли данные с ПК.

Первая конечная точка для CDC устройства используется для настроек параметров самого COM-порта (хотя не уверен, я уже забыл — разбирался давно).

Дескрипторы для CDC устройства, с комментариями:

// device descriptor
const uint8_t Virtual_Com_Port_DeviceDescriptor[] =
{
    0x12, // размер данного дескриптора
    0x01, // тип данного дескриптора - device descriptor 
    0x00, 0x02, // 2 байта - версия usb 2.0
    0x02, // класс устройства cdc
    0x00, // подкласс
    0x00, // протокол
    USB_MAX_PACKET0, // max размер пакета для нулевой конечной точки
    0x83, 0x04, // 2 байта - VID
    0x40,   0x57,// 2 байта - PID
    0x00, 0x02,// 2 байта - версия (ревизия) устройства
    0x01, // индекс строки с названием производителя
    0x02, // индекс строки с названием устройства
    0x03, // индекс строки с серийным номером устройства
    0x01 // количество поддерживаемых конфигураций
};

// configuration descriptor
const uint8_t Virtual_Com_Port_ConfigDescriptor[] =
{
    /* ============== CONFIGURATION 1 =========== */
    /* Configuration 1 descriptor */
    0x09, // размер дескриптора конфигурации
    0x02, // тип дескриптора - configuration
    0x43, // 2 байта - полный размер дескриптора, включая др. дескрипторы 
    0x00, // (CwTotalLength 2 EP + Control)
    0x02, // количество интерфейсов (2 интерфейса для CDC - data и config)
    0x01, // номер данной конфигурации (SET_CONFIGURATION)
    0x00, // индекс строки описывающий данную конфигурацию
    0xC0, // битовое поле, характеризующее конфигурацию
        /*
        Распределение бит:
        D7 – зарезервировано (установлено в 1);
        D6 – признак наличия собственного источника питания;
        D5 – признак разрешения сообщения хосту о выходе
        устройства из режима «сна»;
        D4...D0 – зарезервированы (сброшены в 0)
        */
    0x00, // max потребляемый ток от шины (половина от реального (50 = 100мА))

        /* Communication Class Interface Descriptor Requirement */
        0x09, // длина дескриптора
        0x04, // тип дескриптора - интерфейс
        0x00, // номер данного интерфейса
        0x00, // номер альтернативной установки для интерфейса
        0x01, // количество точек для данной альтернативной установки в данном интерфейсе
        0x02, // код класса(USB-IF)
        0x02, // код подкласса(USB-IF)
        0x00, // код протокола(USB-IF)
        0x01, // индекс строки, описывающей данную альтернативную установку данного интерфейса

    /* Header Functional Descriptor */
    0x05, // bFunction Length
    0x24, // bDescriptor type: CS_INTERFACE
    0x00, // bDescriptor subtype: Header Func Desc
    0x10, // bcdCDC:1.1
    0x01,

    /*Call Management Functional Descriptor */
    0x05, /* bFunctionLength */
    0x24, /* bDescriptorType: CS_INTERFACE */
    0x01, /* bDescriptorSubtype: Call Management Func Desc */
    0x00, /* bmCapabilities: device handles call management */ //!!!!!!!!!!!!!!!!!!1
    0x01, /* bDataInterface: CDC data IF ID */

        /* ACM Functional Descriptor */
        0x04, // bFunctionLength
        0x24, // bDescriptor Type: CS_INTERFACE
        0x02, // bDescriptor Subtype: ACM Func Desc
        0x02, // bmCapabilities

    /* Union Functional Descriptor */
    0x05, // bFunctionLength
    0x24, // bDescriptorType: CS_INTERFACE
    0x06, // bDescriptor Subtype: Union Func Desc
    0x00, // bMasterInterface: Communication Class Interface
    0x01, // bSlaveInterface0: Data Class Interface

        /* Endpoint 1 descriptor */
        0x07, // размер дескриптора
        0x05, // тип дескриптора - endpoint
        0x81, // битовое поле адреса точки // IN
            /*
            D7 – направление передачи данных точкой (1 – IN, 0 – OUT);
            D6...D4 – зарезервированы (сброшены в 0);
            D3...D0 – адрес точки
            */
            0x03, // битовое поле, характеризующее точку
            /*
            D7, D6 – зарезервированы (сброшены в 0);
            D5, D4 – функция, выполняемая точкой:
            00 – точка данных;
            01 – точка обратной связи;
            10 – точка данных с неявной обратной связью;
            11 – зарезервировано
            D3, D2 – тип синхронизации хоста и точки:
            00 – без синхронизации;
            01 – асинхронный;
            10 – адаптивный;
            11 – синхронный
            D1, D0 – тип обмена данными:
            00 – контрольный;
            01 – изохронный;
            10 – bulk;
            11 – interrupt
            */

        64, // 2 байта - битовое поле  
            // характеризующее размер пакета передаваемых данных

        0x00,   /*
            D15…D13 – зарезервированы (сброшены в 0);
            D12, D11 – количество дополнительных передач:
            00 – нет дополнительных передач;
            01 – 1 дополнительная передача (всего 2 передачи),
            10 – 2 дополнительные передачи (всего 3 передачи);
            11 – зарезервировано.
            D10...D0 – размер пакета в байтах
            */
        0xFF, // интервал готовности точки к обмену данными

    /* Data Class Interface Descriptor Requirement */
    0x09, // bLength
    0x04, // bDescriptorType
    0x01, // bInterfaceNumber
    0x00, // bAlternateSetting
    0x02, // bNumEndpoints
    0x0A, // bInterfaceClass
    0x00, // bInterfaceSubclass
    0x00, // bInterfaceProtocol
    0x04, // iInterface


    /* Endpoint 2 descriptor */
    0x07, // bLength
    0x05, // bDescriptorType
    0x02, // bEndpointAddress, Endpoint 01 - OUT
    0x02, // bmAttributes BULK
    64, // wMaxPacketSize
    0x00,
    0x00, // bInterval

    /* Endpoint 3 descriptor */
    0x07, // bLength
    0x05, // bDescriptorType
    0x83, // bEndpointAddress, Endpoint 02 - IN
    0x02, // bmAttributes BULK
    64, // wMaxPacketSize
    0x00,
    0x00 // bInterval
};

/* USB String Descriptors */
const uint8_t Virtual_Com_Port_StringLangID[] =
{
    0x04, // длина дескриптора
    0x03, // тип дескриптора - string desc
    0x09, // N байт индетификатор языка
    0x04 /* LangID = 0x0409: U.S. English */
};

const uint8_t Virtual_Com_Port_StringVendor[] =
{
    10, // длина дескриптра
    0x03, // тип дескриптора - string desc
    /* имя */
    'T', 0, 
    'E', 0, 
    'S', 0, 
    'T', 0
};

const uint8_t Virtual_Com_Port_StringProduct[] =
{
    10, // длина дескриптра
    0x03, // тип дескриптора - string desc
    /* имя */
    'T', 0, 
    'E', 0, 
    'S', 0, 
    'T', 0
};

const uint8_t Virtual_Com_Port_StringSerial[] =
{
    10, // длина дескриптра
    0x03, // тип дескриптора - string desc
    /* имя */
    'T', 0, 
    'E', 0, 
    'S', 0, 
    'T', 0
};

Если руки дойдут, то может когда-нибудь постараюсь поподробнее расписать алгоритм работы с конечными точками. А сейчас, я уже и сам забыл, нужно освежать знания — читать книги Агурова про USB.

Проект —  USB_CDC — для скачивания (проект сделан в keil, заархивирован 7Zip). Драйвер для ПК я брал от ST, его можно скачать или на сайте ST или скачать — stsw-stm32102.

P.S.: Дополнительно прикладываю USB CDC проект, собранный с использованием библиотек от STUSB_CDC_ST.