Продолжаем работать с интерфейсом USB, и сегодня пришло время практики. Как вы помните, теоретические аспекты мы уже рассмотрели (вот), так что сегодня возьмем в руки STM32 и напишем небольшой примерчик. Сразу скажу, что я решил поэкспериментировать с контроллером STM32F303 и, соответственно, с платой STM32F3Discovery.
На плате уже есть два USB разъема, один под ST-Link и второй для пользовательских задач, то есть как раз то, что нам надо.
С платой разобрались, теперь по поводу софта. STMicroelectronics любезно предоставили библиотеки для работы с USB для различных семейств микроконтроллеров, а кроме того, выпустили кучу примеров под разные отладочные платы. Но плат Discovery в этом списке нет, поэтому не станем вносить свои изменения в уже готовые проекты от ST, а лучше создадим свой новый проект, взяв из примеров и библиотек только то, что нам реально понадобится.
Время традиционной вставки: поскольку компания STMicroelectronics прекратила поддержку библиотеки SPL, которая использовалась в этом курсе, я создал новый, посвященный работе уже с новыми инструментами, так что буду рад видеть вас там - STM32CubeMx. Кроме того, вот глобальная рубрика по STM32, а также несколько статей на смежную тему из нового курса:
Задача будет такая - по приему байта данных по USB, контроллер зажигает определенное количество светодиодов. Если пришел байт 0х01 - светится один диод, 0х02 - два, 0х03 - три, ну и так далее. Для того, чтобы реализовать отправку данных с компьютера, поставим драйвер виртуального ком-порта и будем общаться с платой через обычный терминал (я использую Advanced Serial Port Monitor).
Нужный драйвер без проблем можно скачать на официальном сайте STMicroelectronics. Устанавливается тоже без проблем и в итоге в диспетчере устройств появляется следующее:
С этим разобрались, давайте теперь откроем какой-нибудь примерчик от ST и посмотрим как же вообще в их библиотеках устроен обмен данными по USB. Вот архив со всеми библиотеками и примерами - ST USB Library.
Заходим в папку с примерами и выбираем Virtual Com Port, там без труда находим нужную нам папку с проектом для Keil'а и запускаем его. Давайте сразу же посмотрим на файл main.c:
int main(void) { Set_System(); Set_USBClock(); USB_Interrupts_Config(); USB_Init(); while(1) { } }
Видим, что в теле цикла while(1) пусто, соответственно вся работа происходит в прерываниях. В функции main() всего лишь вызываются функции инициализации. Все эти функции реализованы в файле hw_config.c, его мы поправим под себя чуть позже. Для приема и передачи данных по USB в файле usb_endp.c предусмотрены соответствующие обработчики:
void EP1_IN_Callback (void) void EP3_OUT_Callback(void)
Как вы помните из предыдущей статьи, транзакции IN нужны для передачи данных хосту (то есть ПК), а транзакции OUT для приема данных от хоста. Соответственно, для отправки данных используется конечная точка 1 (End Point 1), а для приема - End Point 3. Как программа попадает в эти обработчики? Сейчас разберемся! В файле stm32_it.c есть обработчик прерывания:
void USB_LP_CAN1_RX0_IRQHandler(void)
В его теле вызывается всего лишь одна функция - USB_Istr(), которая описана в файле usb_istr.c. Идем в этот файл и изучаем функцию... А там все в принципе просто, программа выясняет какое именно событие вызвало прерывание и в соответствии с этим происходит дальнейшая работа. Соответственно, при транзакциях IN или OUT вызываются именно те функции которые мы уже рассмотрели выше, а именно:
void EP1_IN_Callback (void) void EP3_OUT_Callback(void)
Этот проект вообще по умолчанию адаптирован для контроллеров STM32F10x, да и файлов очень много лишних, так что давайте-ка создадим свой новый пустой проект и в нем уже будем работать. Напоминаю, что я буду работать с STM32F3Discovery, поэтому проект создаю для контроллера STM32F303VC. Забираем из папки с библиотеками и примерами все файлы, которые нам понадобятся. Вот их полный список:
В папку SPL я просто запихал все файлы из Standard Peripheral Library для STM32F303. Не забываем в настройках проекта указать все пути к файлам и прописать подключение SPL ( в общем, все как тут - ссылка). Проект создан, файлы все на месте, давайте писать код. И начинаем с функций инициализации, расположенных в файле hw_config.c:
void Set_System(void) { GPIO_InitTypeDef GPIO_InitStructure; // Включаем тактирование нужной периферии RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOE, ENABLE); // Настройка пинов GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_12; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_8 | GPIO_Pin_10 | GPIO_Pin_15 | GPIO_Pin_11 | GPIO_Pin_14 | GPIO_Pin_12 | GPIO_Pin_13; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_Init(GPIOE, &GPIO_InitStructure); GPIO_PinAFConfig(GPIOA, GPIO_PinSource11, GPIO_AF_14); GPIO_PinAFConfig(GPIOA, GPIO_PinSource12, GPIO_AF_14); // Внешнее прерывание, которое внутри контроллера подключено к // функциям USB EXTI_ClearITPendingBit(EXTI_Line18); EXTI_InitStructure.EXTI_Line = EXTI_Line18; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); }
Что за пины мы настраиваем тут? А вот:
С PA11 и PA12 разобрались, а все остальные ножки - это светодиоды, которые есть на плате.
Идем дальше... Из функций для работы с USART'ом я просто все удалил, поскольку мы приемопередатчиком пользоваться не будем. Настраиваем прерывания:
void USB_Interrupts_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = USBWakeUp_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_Init(&NVIC_InitStructure); }
И еще я добавил небольшую функцию в этот же файл. Она просто гасит все светодиоды:
void GPIO_ResetLeds() { GPIO_ResetBits(GPIOE, GPIO_Pin_8); GPIO_ResetBits(GPIOE, GPIO_Pin_9); GPIO_ResetBits(GPIOE, GPIO_Pin_10); GPIO_ResetBits(GPIOE, GPIO_Pin_11); GPIO_ResetBits(GPIOE, GPIO_Pin_12); GPIO_ResetBits(GPIOE, GPIO_Pin_13); GPIO_ResetBits(GPIOE, GPIO_Pin_14); GPIO_ResetBits(GPIOE, GPIO_Pin_15); }
Не забываем в файл hw_config.h дописать прототип для этой функции:
void GPIO_ResetLeds(void);
С инициализацией вроде бы все. Открываем файл usb_endp.c. Мы будем только анализировать принятые от хоста данные, поэтому обработчик транзакций IN нам не понадобится:
void EP1_IN_Callback (void) { }
В обработчике транзакций OUT принимаем данные и в зависимости от того, какой байт принят зажигаем определенное количество светодиодов, которые у нас висят на GPIOE:
void EP3_OUT_Callback(void) { uint16_t USB_Rx_Cnt; // Принимаем данные USB_Rx_Cnt = USB_SIL_Read(EP3_OUT, USB_Rx_Buffer); // Анализируем принятый байт switch(USB_Rx_Buffer[0]) { case 0x01: GPIO_ResetLeds(); GPIO_Write(GPIOE, 0x0100); break; case 0x02: GPIO_ResetLeds(); GPIO_Write(GPIOE, 0x0300); break; case 0x03: GPIO_ResetLeds(); GPIO_Write(GPIOE, 0x0700); break; case 0x04: GPIO_ResetLeds(); GPIO_Write(GPIOE, 0x0F00); break; case 0x05: GPIO_ResetLeds(); GPIO_Write(GPIOE, 0x1F00); break; case 0x06: GPIO_ResetLeds(); GPIO_Write(GPIOE, 0x3F00); break; case 0x07: GPIO_ResetLeds(); GPIO_Write(GPIOE, 0x7F00); break; case 0x08: GPIO_ResetLeds(); GPIO_Write(GPIOE, 0xFF00); break; } // Включаем прием данных для конечной точки 3 SetEPRxValid(ENDP3); }
Так... Ну вроде бы на этом все. Вот полный проект: Проект для USB
Прошиваем микроконтроллер и тестируем! Вот, что получилось:
Послали байт 0х04 - загорелось 4 светодиода, аналогично работает и для любого другого количества светодиодов. Так что, на сегодня, пожалуй все, но опыты с USB, на этом только начинаются!