Продолжаем работать с интерфейсом USB, и сегодня пришло время практики! Как вы помните, теоретические аспекты мы уже рассмотрели (вот), так что сегодня возьмем в руки микроконтроллер от ST и напишем небольшой примерчик 😉 Сразу скажу, что я решил поэкспериментировать с контроллером STM32F303 и, соответственно, с платой STM32F3Discovery.
На плате уже есть два USB разъема, один под ST-Link и второй для пользовательских задач, то есть как раз то, что нам надо )
С платой разобрались, теперь по поводу софта. STMicroelectronics любезно предоставили библиотеки для работы с USB для различных семейств микроконтроллеров, а кроме того, выпустили кучу примеров под разные отладочные платы. Но плат Discovery в этом списке нет, поэтому не станем вносить свои изменения в уже готовые проекты от ST, а лучше создадим свой новый проект, взяв из примеров и библиотек только то, что нам реально понадобится.
Задача будет такая – по приему байта данных по 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) |
Как вы помните из предыдущей статьи, транзакции
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, на этом только начинаются!
microtechnics.ru
Платы у меня в руках оказались следующие:
1) STM32 Development Board MINI (512K Flash 64K SRAM) 2.4-inch QVGA TFT module
(ссылка 1) (ссылка 2)
На ней стоит микроконтроллер STM32F103VET6
2) Embest EM-STM32C (EM-STM3210C)
(ссылка)
На ней стоит микроконтроллер STM32F107VCT6 — Connectivity Line
3) Встраиваемый модуль TE-STM32F103 — Махаон, от фирмы Terraelectronica.
(ссылка)
Соответственно, на ней стоит контроллер STM32F103RET6
Запустить проект из примеров, который использует USB, на любой из этих плат, задача не такая уж и сложная.
Куда сложнее встроить эти примеры в свои проекты, так как часто они бывают очень запутанно завязаны на конкретных платах. Еще сложнее собрать проект с нуля, используя библиотеки драйверов от STM — все равно без примеров обойтись сложно.
Поэтому я поставил перед собой задачу сделать универсальный проект, работающий со всеми имеющимися у меня в наличии платами, и, при необходимости, легко подстраиваемый под другие платы.
Между первой и третьей платой отличий мало: похожие контроллеры, отличающиеся лишь числом ног, у обоих выведен USART1. А вот второй отличается сильно: это контроллер Connectivity Line, с поддержкой USB On-The-Go, из-за чего работа с USB построена по-другому, а также вместо USART1 выведен USART2, да еще и с ремапом пинов на другие, отличные от дефолтных, ноги.
На всех платах есть светодиоды в разном количестве: 1, 4 и 3 соответственно.
Поэтому было принято решение сделать банальную вещь — устройтво, светодиоды которого управляются с компьютера по USB.
Прежде чем продолжать, рекомендую вкратце ознакомиться с тем, что же из себя представляет USB.
Самая лучшая, на мой взгляд, статья по этому вопросу — «USB in a NutShell». Ее перевод можно найти тут.
Если совсем упрощенно, то каждое USB-устройство имеет некоторое количество оконечных точек — Endpoint-ов, которые бывают следующих типов:
Проект для Keil
В результате некоторых ковыряний и копипасты с примеров, редактирования, кодинга и прочих мучений, получилось следующее:
USB_SampleSomeDevice_src.rar (зеркало 1)
Структура файлов такая же, как и во многих примерах:
\Libraries\ — папка с библиотеками (CMSIS, Standart Peripheral Driver, USB OTG Full speed Device Driver)
\Project\ — папка для проектов. Их может быть много и все они могут использовать одни и те же библиотеки. Но у нас проект один.
\Project\SampleSomeDevice\ — папка с проектом
\Project\SampleSomeDevice\Doc\ — краткие описания
\Project\SampleSomeDevice\driver\ — драйвер устройства для Windows (подробнее о драйверах и софте в ч.2, когда ее напишу)
\Project\SampleSomeDevice\inc\ — заголовки .h
\Project\SampleSomeDevice\src\ — файлы исходников .c
\Project\SampleSomeDevice\RVMDK\ — файлы проекта и выходные файлы
Распаковываем проект и открываем.
Смотрим на вкладку Project, видим там несколько групп:
User — Основные исходники проекта.
User_headers — Заголовочные файлы. Вынес в отдельную группу для быстрого и удобного доступа к ним.
USB-FS-Device_Driver — файлы библиотеки USB.
StdPeriph_Driver — файлы библиотеки стандарной периферии.
RVMDK — startup-файлы для каждой линейки контроллеров. Обратите внимание, что только один, соответствующий вашему контроллеру должен компилиться.
Doc — Краткие описания.
По умолчанию проект сконфигурирован под плату TE-STM32F103.
Конфигурируем проект
под другой контроллер и плату.
1) Надо знать название контроллера и его линейку. Поддерживаются практически все контроллеры 103 серии (кроме XL-density), а также 105 и 107 серия — Connectivity Line.
Даташиты, предварительно скачанные с сайта ST:
STM32F103x4x6.pdf (зеркало 1) — STM32 Low-density performance line (краткое обозначение LD)
STM32F103x8xB.pdf (зеркало 1) — STM32 Medium-density performance line (краткое обозначение MD)
STM32F103xCxDxE.pdf (зеркало 1) — STM32 High-density performance line (краткое обозначение HD)
STM32F105_F107.pdf (зеркало 1) — STM32 Connectivity line (краткое обозначение CL)
Все, что связано с линейкой контроллера, содержит в себе краткое обозначение.
Например, startup-файл для Medium-density performance line будет называться startup_stm32f10x_md.s
Или глобальный define для Connectivity line — STM32F10X_CL
В Keil правым кликом по Target заходим в опции, выбираем вкладку Device и ищем там свой контроллер.
Далее, открываем файл Doc\sample_global_defines.txt и, в зависимости от линейки контроллера, платы и необходимости вывода отладочных сообщений, выбираем нужную строку и копируем.
Если ни одна из этих плат не используется, просто копируем любую строку, исправив define линейки процессора и выкинув define, содержащий _BOARD.
Вставляем строку в опции, во вкладку С/С++
Следующий шаг — выбираем используемый JTAG для прошивки и отладки.
Я использую TE-ARM-LINK, отечественный клон J-LINK.
Последний шаг в данном пункте — выбрать нужный startup-файл в группе RVMDK, соответствующий линейке контроллера, включить его в сборку проекта, отключив при этом все остальные:
Следующие пункты — настройка платы.
открываем файл platform_config.h, ищем кусок кода:
#else// Дефолтная конфигурация - сделано под TE-STM32F103_BOARD
#define USB_DISCONNECT GPIOB
#define USB_DISCONNECT_PIN GPIO_Pin_5
#define USB_DISCONNECT_LOG1 DISABLE
#define RCC_APB2Periph_GPIO_DISCONNECT RCC_APB2Periph_GPIOB
#define LED1 0
#define LED1_GPIO_PORT GPIOA
#define LED1_GPIO_CLK RCC_APB2Periph_GPIOA
#define LED1_GPIO_PIN GPIO_Pin_4
#define LED2 1
#define LED2_GPIO_PORT GPIOA
#define LED2_GPIO_CLK RCC_APB2Periph_GPIOA
#define LED2_GPIO_PIN GPIO_Pin_5
#define LED3 2
#define LED3_GPIO_PORT GPIOA
#define LED3_GPIO_CLK RCC_APB2Periph_GPIOA
#define LED3_GPIO_PIN GPIO_Pin_6
#define USE_USART1_DEFAULT_PA9_PA10
#endif
2) Узнаем куда выведен USART. Для общего развития полезно также знать, используется ли при этом ремап пинов. При включенном дефайне _DEBUG_ на него выводится различная информация, которая может быть полезна.
Открываем схему платы и смотрим. Предположим, выяснили, что выведен USART2, TX — PB5, RX — PB6.
Смотрим комментарии вначале файла platform_config.h:
// Варианты конфигурации USART
// USE_USART1_DEFAULT_PA9_PA10
// USE_USART1_REMAP_PB6_PB7
// USE_USART2_DEFAULT_PA2_PA3
// USE_USART2_REMAP_PD5_PD6
// USE_USART3_DEFAULT_PB10_PB11
// USE_USART3_REMAP1_PC10_PC11
// USE_USART3_REMAP2_PD8_PD9
Выбираем подходящий и заменяем в последнем дефайне. В данном случае это будет так:
#define USE_USART2_REMAP_PD5_PD6
USART сконфигурирован на скорость 115200, 8 бит, 1 стоп, No Parity.
3) Смотрим сколько на плате светодиодов и куда они подключены. Правим соответствующие дефайны. Число светодиодов должно быть не более 4х, начинаться с
#define LED1 0
и идти по порядку. Также не забываем вместе с правкой порта, поправить и RCC_APB2Periph_GPIOx4) Проверяем, есть ли на контроллере пин, отвечающий за программный коннект/дисконнект USB и где он расположен. Схема может выглядеть так:
Если пина нету, просто удаляем дефайны, отвечающие за него.
Вот, в принципе и все. Осталось залить прошивку в контроллер. Если есть JTAG — это не проблема.
Если оного нету, не все потеряно:
USB_DfuSe.part1.rar (зеркало 1) — Софт для прошивки STM32 Connectivity line по USB. Часть 1
USB_DfuSe.part2.rar (зеркало 1) — Часть 2
COM_FlashLoader.zip (зеркало 1) — Софт для прошивки STM32 (103 серия) по UART
Не забудьте перед прошивкой этим способом перевести девайс в DFU-Mode, корректно выставив джамперы BOOT0 — BOOT1.
О софте и драйверах я напишу в другой раз, однако уже можно скачать программу, которая общается с любым количеством подключенных девайсов с данной прошивкой:
SomeUsbDev_1.0.0_src.rar (зеркало 1) — Проект для Visual Studio 2010
SomeUsbDev_1.0.0_bin.rar (зеркало 1) — Исполняемые файлы
.NET Framework 4.0
Ковыряемся в проекте
Поскольку каждую строчку кода расписывать долго, да и исходники полны в том числе и моих комментариев, приведу здесь список основных файлов проекта и их назначение.
User:
— main.c — очевидно.
— hw_config.c — конфигурация контроллера (периферия, прерывания, клоки и так далее)
— stm32f10x_it.c — обработчики прерываний
— usb_???.c — конфигурация и работа USB посредством драйвера.
— user_usb.c — пользовательская работа с USB — разбор пакетов с данными и обработка команд.
— led.c — работа со светодиодами. Включение, выключение, непрерывное мигание.
User_headers:
— platform_config.h — конфигурация платы.
Для более удобного поиска я добавил в код комментарии следующего вида:
#define BTABLE_ADDRESS (0x00)
// $USBCONFIG - при изменении числа ендпойнтов, нужно подправить эту таблицу
// и задать верные адреса для буферов с учетом максимального размера пакета.
/* EP0 */
/* rx/tx buffer base address */
//#define ENDP0_RXADDR (0x18)
#define ENDP0_RXADDR (0x40)
#define ENDP0_TXADDR (0x80)
Проект собран так, что USB-устройство, помимо нулевой контрольной, содержит 4 оконечных точки типа bulk, попарно на прием и передачу.
В проекте используются первые две, по которым при помощи несложного протокола передаются команды управления светодиодами на плате.
Краткое описание протокола можно найти в Doc\protocol.txt
И что дальше?
Ну а дальше — куда приведет фантазия. Ковырять USB рекомендую начинать с файла дескрипторов usb_desc.c, потом поиграться с ендпоинтами.
Если есть желание — можно попробовать реализовать один из стандартных классов USB-устройств, или не париться и сделать свой протокол под свои задачи.
На этом пока все. Если эта статья покажется кому-то интересной и полезной, во второй части немного напишу о драйверах и софте.
Файлы, используемые в статье собраны тут
P.S.: Хоть это и первый блин, конструктивная критика, естественно, принимается.
we.easyelectronics.ru
#include «usbd_cdc_if.h»
extern tmStruct *RAMStruct;
#define APP_RX_DATA_SIZE 64
#define APP_TX_DATA_SIZE 64
uint8_t UserRxBufferFS[APP_RX_DATA_SIZE];
uint8_t UserTxBufferFS[APP_TX_DATA_SIZE];
USBD_HandleTypeDef *hUsbDevice_0;
extern USBD_HandleTypeDef hUsbDeviceFS;
static int8_t CDC_Init_FS(void);
static int8_t CDC_DeInit_FS(void);
static int8_t CDC_Control_FS(uint8_t cmd, uint8_t *pbuf, uint16_t length);
static int8_t CDC_Receive_FS(uint8_t *pbuf, uint32_t *Len);
const uint16_t fcstab[] = {
0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf,
0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7,
0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e,
0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876,
0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd,
0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5,
0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c,
0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974,
0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb,
0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3,
0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a,
0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72,
0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9,
0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1,
0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738,
0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70,
0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7,
0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff,
0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036,
0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e,
0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5,
0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd,
0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134,
0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c,
0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3,
0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb,
0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232,
0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a,
0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1,
0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9,
0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330,
0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78
};
extern char MainStruct[];
extern uint16_t MainStructIndex;
uint16_t PacketSize = 0;
extern bool TimeToSaveFlashPack;
extern __root const uint8_t FlashPack[MAINSTRUCTLENGTH] @ PAGE1;
char WRITEREQUEST[] = «HardwareWriteRequest»;
char WRITERESPONSE[] = «HardwareWriteResponseOK»;
uint16_t WRITERESPONSELENGTH = sizeof(WRITERESPONSE) — 1;
char READREQUEST[] = «HardwareReadRequest»;
char READRESPONSE[] = «HardwareReadResponseOK»;
uint16_t READRESPONSELENGTH = sizeof(READRESPONSE) — 1;
extern uint8_t DataTransferState;
static uint16_t ComputeCRC16(char data[], uint16_t start, uint16_t length)
{
uint16_t fcs = 0xFFFF;
uint16_t end = start + length;
for (uint16_t i = start; i < end; i++)
fcs = (uint16_t)(((uint16_t)(fcs >> 8)) ^ fcstab[(fcs ^ data[i]) & 0xFF]);
return (uint16_t)(~fcs);
}
USBD_CDC_ItfTypeDef USBD_Interface_fops_FS = { CDC_Init_FS, CDC_DeInit_FS,
CDC_Control_FS, CDC_Receive_FS };
static int8_t CDC_Receive_FS(uint8_t *Buf, uint32_t *Len) {
for (int i = 0; i < *Len; i++) {
UserRxBufferFS[i] = Buf[i];
if (DataTransferState == DATAWRITEPROCESS) // Идет запись пакета
{
HAL_GPIO_WritePin(GPIOC, Red_LED_Pin, GPIO_PIN_SET);
MainStruct[MainStructIndex] = Buf[i];
if (MainStructIndex == 1) // В первых двух байтах должна была прийти общая длина пакета
PacketSize = *(uint16_t *)MainStruct;
if (MainStructIndex == PacketSize — 1) // Пришел последний байт пакета
{
uint16_t checksum = ComputeCRC16(MainStruct, 0, PacketSize — 2); // При расчете контрольной суммы не учитываем два последних байта, содержащих собственно контрольную сумму
uint16_t receivedchecksum = *(uint16_t *)(MainStruct + MainStructIndex — 1);
if (checksum == receivedchecksum) // Правильная контрольная сумма
TimeToSaveFlashPack = true;
DataTransferState = DATAREQUESTNOPE;
HAL_GPIO_WritePin(GPIOC, Red_LED_Pin, GPIO_PIN_RESET);
}
MainStructIndex++;
}
}
if (DataTransferState == DATAREADPROCESS) // Отдаем пакет
{
if (!TimeToSaveFlashPack) // Пришедший пакет был уже переписан в ROM или его вообще не было
{
uint16_t PackLength = *(uint16_t *)FlashPack; // В первых двух байтах лежит общая длина пакета
if ((PackLength >= 128) && (PackLength <= MAINSTRUCTLENGTH)) // Разумные размеры пакета
{
for (uint16_t i = 0; i < PackLength — 2; i++) // Переливаем из ROM в RAM
MainStruct[i] = FlashPack[i];
RAMStruct = (tmStruct *)MainStruct;
uint16_t checksum = ComputeCRC16(MainStruct, 0, PackLength — 2); // При расчете контрольной суммы не учитываем два последних байта, содержащих собственно контрольную сумму
MainStruct[PackLength — 2] = checksum & 0x00ff; // Записываем контрольную сумму в конец передаваемого пакета
MainStruct[PackLength — 1] = (checksum & 0xff00) >> 8;
CDC_Transmit_FS(MainStruct, PackLength);
}
}
DataTransferState = DATAREQUESTNOPE;
}
if (*Len > 0)
if (strstr(UserRxBufferFS, WRITEREQUEST) != NULL)
DataTransferState = DATAWRITEREQUEST; // Запрос на запись данных с хоста
else if (strstr(UserRxBufferFS, READREQUEST) != NULL)
DataTransferState = DATAREADREQUEST;
if (*Len > 0)
if (DataTransferState == DATAWRITEREQUEST) { // Если хост хочет записать пакет данных, отвечаем согласием
CDC_Transmit_FS(WRITERESPONSE, WRITERESPONSELENGTH);
DataTransferState = DATAWRITEPROCESS; // и переходим в режим записи
MainStructIndex = 0;
}
else if (DataTransferState == DATAREADREQUEST) {
CDC_Transmit_FS(READRESPONSE, READRESPONSELENGTH);
DataTransferState = DATAREADPROCESS; // и переходим в режим чтения
MainStructIndex = 0;
}
USBD_CDC_ReceivePacket(hUsbDevice_0);
return (USBD_OK);
}
static int8_t CDC_Init_FS(void) {
hUsbDevice_0 = &hUsbDeviceFS;
USBD_CDC_SetTxBuffer(hUsbDevice_0, UserTxBufferFS, 0);
USBD_CDC_SetRxBuffer(hUsbDevice_0, UserRxBufferFS);
return (USBD_OK);
}
static int8_t CDC_DeInit_FS(void) {
return (USBD_OK);
}
static int8_t CDC_Control_FS(uint8_t cmd, uint8_t *pbuf, uint16_t length) {
switch (cmd) {
case CDC_SEND_ENCAPSULATED_COMMAND:
break;
case CDC_GET_ENCAPSULATED_RESPONSE:
break;
case CDC_SET_COMM_FEATURE:
break;
case CDC_GET_COMM_FEATURE:
break;
case CDC_CLEAR_COMM_FEATURE:
break;
case CDC_SET_LINE_CODING:
break;
case CDC_GET_LINE_CODING:
break;
case CDC_SET_CONTROL_LINE_STATE:
break;
case CDC_SEND_BREAK:
break;
default:
break;
}
return (USBD_OK);
}
uint8_t CDC_Transmit_FS(uint8_t *Buf, uint16_t Len) {
uint8_t result = USBD_OK;
USBD_CDC_SetTxBuffer(hUsbDevice_0, Buf, Len);
result = USBD_CDC_TransmitPacket(hUsbDevice_0);
return result;
}
wiredlogic.io
Итак, берем за основу готовый пример, который перекладывает данные из USB в последовательный порт микроконтроллера и обратно. Последовательный порт нам не понадобится, а данные мы будем просто перекладывать из входного потока в выходной, то есть реализуем эхо. С помощью PuTTY убеждаемся, что оно работает. Но этого недостаточно. Для обмена данными с устройством нам понадобится слать много больше одного символа за раз. Пишем тестовую программу на питоне, которая шлет посылки случайной длины и вычитывает ответ. И тут нас ждет сюрприз. Тест работает, но недолго, после чего очередная попытка чтения либо зависает навсегда, либо завершается по таймауту, если он выставлен. Исследование проблемы с помощью отладчика показывает, что МК таки отослал все полученные данные, причем последняя посылка имела длину 64 байта. Что же произошло?
USB-стек на хост-системе имеет многослойную структуру. На уровне драйвера данные получены, но остались у него в кэше. Драйвер передает закэшированные данные приложению тогда, когда приходят новые данные и вытесняют старые, либо когда драйвер узнает, что новых данных пока ожидать не следует. Откуда же он может получить это знание? USB шина передает данные пакетами. Максимальный размер пакета в нашем случае как раз 64 байта. Если в очередном пакете данных пришло меньше, значит новых данных пока можно не ждать, и это является сигналом для того, чтобы передать приложению все полученные данные. А если данных пришло ровно 64 байта? На этот случай в протоколе предусмотрена посылка пакета нулевой длины (ZLP), который и является сигналом прерывания потока. Получив его, драйвер понимает, что новых данных пока ожидать не следует. В нашем случае он его не получил потому, что разработчики USB стека для STM32 про ZLP просто ничего не знали.
Вторая проблема, которую разработчики USB-стека незаслуженно обошли вниманием — что делать с данными, которые были получены по USB, если их некуда девать, т.к. входной буфер переполнен. По большому счету, их вообще не волновала проблема входного буфера — они предполагали, что все полученные данные немедленно обрабатываются, что, конечно-же, не всегда может быть выполнено. В USB протоколе на случай, если данные не могут быть получены, предусмотрен ответ NAK — отрицательное подтверждение. После такого ответа хост просто посылает данные еще раз. Если мы хотим избежать переполнения входного буфера, нам нужно в случае, если в нем нет места для полной посылки (64 байта), переводить канал в состояние NAK, что обеспечивает автоматический ответ NAK на все входящие пакеты.
Выявить проблему удается только путем тщательного изучения исходников библиотек. Оказывается, что для посылки ZLP необходимо выставить специальный флажок, который по умолчанию не выставлен. Возможно, это обстоятельство и подтолкнуло других разработчиков к тому, чтобы добавить код, посылающий ZLP еще в одном месте — на более низком уровне USB-стека, и уже без флажка. Это изменение и внесло баг, приводящий к остановке передачи. Проблема возникает следующим образом. Передатчик получает следующий пакет, когда заканчивается передача предыдущего, либо если предыдущего не было, а приложение добавило данные в буфер передачи. Код, который инициирует передачу, получает нотификацию о завершении передачи предыдущего пакета от нижнего уровня USB-стека. Проблема в том, что если нижний уровень стека инициировал передачу ZLP, то нотификацию о завершении он не присылает, т.к. инициировал передачу он сам. Верхний уровень не начинает передачу данных, пока передатчик занят передачей ZLP пакета, и не начинает передачу после ее завершения, поскольку не получает нотификации — процесс передачи останавливается. Исправить проблему очень просто — нужно убрать код нижнего уровня, посылающий ZLP, и предоставить это верхнему уровню стека. Вторая проблема, требующая решения, связана с тем, что процедура, начинающая передачу, может быть вызвана как из контекста обработчика прерывания (по завершении передачи), так и из контекста приложения по добавлении данных в буфер передачи. Чтобы сериализовать вызовы этой процедуры из разных контекстов, нужно запрещать прерывания на время ее исполнения.
habr.com