8-900-374-94-44
[email protected]
Slide Image
Меню

Драйвер stm32 usb – STM32 и USB. Часть 2. Немного о драйверах и софте. / STM32 / Сообщество EasyElectronics.ru

Микроконтроллер STM32 и USB. | MicroTechnics

Продолжаем работать с интерфейсом 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)

Как вы помните из предыдущей статьи, транзакции

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, на этом только начинаются!

microtechnics.ru

STM32 и USB. Часть 1. Проект для Keil. / STM32 / Сообщество EasyElectronics.ru

Связанные статьи:
STM32 и USB. Часть 2. Немного о драйверах и софте.

Введение

После того как я вдоволь наковырялся с STM32 и USB, решил что было бы неплохо поделитсья опытом с окружающими. Тем более, что все делалось аж под три разные платы и две разные линейки процессоров: High-Density (STM32F103RET6, STM32F103VET6) и Connectivity-Line (STM32F107VCT6).

Платы у меня в руках оказались следующие:

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-ов, которые бывают следующих типов:

  • Control. Endpoint такого типа, с номером 0, обязательно должен присутствовать в любом USB-устройстве.
  • Interrupt. Название, в принципе, говорит само за себя. Более подробно читайте в статье.
  • Isochronous. Гарантированные передачи через равные промежутки времени. Обычно используется для передачи аудио и видео.
  • Bulk. Самый простой для реализации вариант. Применяется широко. Подробнее в статье. С ним мы и будем работать.

Проект для 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_GPIOx

4) Проверяем, есть ли на контроллере пин, отвечающий за программный коннект/дисконнект 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)

Слово, начинающееся с $ — ключевое для поиска. При помощи поиска по всем файлам можно найти важные куски кода, которые надо проверить, при внесении в проект изменений.
$BOARDSPECIFIC — код, который зависит от того, какая плата выбрана
$USBCONFIG — код, который нужно проверить поправить для изменения конфигурации USB-устройства.

Проект собран так, что USB-устройство, помимо нулевой контрольной, содержит 4 оконечных точки типа bulk, попарно на прием и передачу.
В проекте используются первые две, по которым при помощи несложного протокола передаются команды управления светодиодами на плате.
Краткое описание протокола можно найти в Doc\protocol.txt

И что дальше?

Ну а дальше — куда приведет фантазия. Ковырять USB рекомендую начинать с файла дескрипторов usb_desc.c, потом поиграться с ендпоинтами.
Если есть желание — можно попробовать реализовать один из стандартных классов USB-устройств, или не париться и сделать свой протокол под свои задачи.

На этом пока все. Если эта статья покажется кому-то интересной и полезной, во второй части немного напишу о драйверах и софте.

Файлы, используемые в статье собраны тут

P.S.: Хоть это и первый блин, конструктивная критика, естественно, принимается.

we.easyelectronics.ru

Использование USB в STM32 на примере Virtual COM port — Open. Hardware.

#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-стека в микроконтроллерах STM32 и TivaC / Habr

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

Постановка задачи


Итак, мы хотим сделать устройство, которое обменивается с компьютером сообщениями произвольной длины через USB порт. Самый простой способ сделать это — воспользоваться USB классом символьных устройств (CDC), известным также под названием ‘виртуальный последовательный порт’. Тогда на хост-системе, к которой вы подключите ваше устройство, автоматически будет создан последовательный порт, через который вы сможете обмениваться данными с устройством, работая с ним как с обычным файлом. На практике, однако, выясняется, что некоторые необходимые для этого функции в USB-стеке производителя либо не реализованы вовсе, либо реализованы с ошибками. Мы начнем с рассмотрения микроконтроллеров STM32 (первый случай) и закончим другим популярным семейством — Texas Instruments Tiva C (второй случай). Оба семейства имеют архитектуру ARM Cortex M4.

STM32 — просто добавь кода


Микроконтроллеры STM обычно имеют богатый функционал при весьма демократичной цене. Производитель поставляет широкий спектр библиотек на все случаи жизни. Среди них есть и библиотеки для поддержки USB, и библиотека для работы с прочей периферией, имеющейся на кристалле. В последнее время все эти библиотеки были объединены в один мега-пакет под названием STM32Cube. При этом, однако, о совместимости особо не заботились и поменяли все, что только смогли поменять, включая названия полей в структурах, описывающих конфигурацию портов ввода-вывода, при том, что само название структуры осталось прежним. Интресно, что есть еще и третий вариант примеров и библиотек, который можно найти на сайте stm32f4-discovery.com. Однако, автор этого варианта очень любит переименовывать файлы, позаимствованные у STM, дабы увековечить свои инициалы, что тоже не добавляет совместимости со всем остальным кодом. Учитывая все вышеизложенное, я решил взять за основу последний до-кубический вариант библиотек, поставляемых STM. Сейчас их можно найти в комплекте поставки компиляторов (я использую IAR). Чтобы потом долго не искать, библиотеки включены в состав проекта, который вы можете взять из гита по ссылке внизу. Для экспериментов я использовал плату STM32F4DISCOVERY www.st.com/web/catalog/tools/FM116/SC959/SS1532/PF252419. Если у вас другая плата и код сразу не заработал, дело скорее всего в частоте внешнего кварцевого генератора. Хотя библиотеки изобилуют всяческими макроопределениями, и в последней версии библиотек среди них появился и макрос для внешней тактовой частоты, в коде этот параметр по-прежнему прописан в виде числа без всяких комментариев, видимо, чтобы разработчики не теряли форму и не забывали читать мануал. Вы можете найти это число — тактовую частоту в мегагерцах — в файле system_stm32f4xx.c в определении макроса PLL_M.

Итак, берем за основу готовый пример, который перекладывает данные из USB в последовательный порт микроконтроллера и обратно. Последовательный порт нам не понадобится, а данные мы будем просто перекладывать из входного потока в выходной, то есть реализуем эхо. С помощью PuTTY убеждаемся, что оно работает. Но этого недостаточно. Для обмена данными с устройством нам понадобится слать много больше одного символа за раз. Пишем тестовую программу на питоне, которая шлет посылки случайной длины и вычитывает ответ. И тут нас ждет сюрприз. Тест работает, но недолго, после чего очередная попытка чтения либо зависает навсегда, либо завершается по таймауту, если он выставлен. Исследование проблемы с помощью отладчика показывает, что МК таки отослал все полученные данные, причем последняя посылка имела длину 64 байта. Что же произошло?

USB-стек на хост-системе имеет многослойную структуру. На уровне драйвера данные получены, но остались у него в кэше. Драйвер передает закэшированные данные приложению тогда, когда приходят новые данные и вытесняют старые, либо когда драйвер узнает, что новых данных пока ожидать не следует. Откуда же он может получить это знание? USB шина передает данные пакетами. Максимальный размер пакета в нашем случае как раз 64 байта. Если в очередном пакете данных пришло меньше, значит новых данных пока можно не ждать, и это является сигналом для того, чтобы передать приложению все полученные данные. А если данных пришло ровно 64 байта? На этот случай в протоколе предусмотрена посылка пакета нулевой длины (ZLP), который и является сигналом прерывания потока. Получив его, драйвер понимает, что новых данных пока ожидать не следует. В нашем случае он его не получил потому, что разработчики USB стека для STM32 про ZLP просто ничего не знали.

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

Tiva C — слоеный пирог с багами


Для экспериментов была взята плата EK-TM4C123GXL www.ti.com/tool/ek-tm4c123gxl. Для компиляции необходим пакет библиотек TivaWare www.ti.com/tool/sw-ek-tm4c123gxl. Изучение библиотек показывает, что разработчики не обошли вниманием ни ZLP ни проблему буферизации — во входном и выходном канале имеются готовые к использованию кольцевые буфера. Однако автоматический тест дает все тот же результат — обмен данными внезапно прекращается. С помощью отладчика выясняется, что на этот раз данные застряли в кольцевом буфере передачи, причем с размером последнего пакета, а значит и с ZLP, проблема не связана никак.

Выявить проблему удается только путем тщательного изучения исходников библиотек. Оказывается, что для посылки ZLP необходимо выставить специальный флажок, который по умолчанию не выставлен. Возможно, это обстоятельство и подтолкнуло других разработчиков к тому, чтобы добавить код, посылающий ZLP еще в одном месте — на более низком уровне USB-стека, и уже без флажка. Это изменение и внесло баг, приводящий к остановке передачи. Проблема возникает следующим образом. Передатчик получает следующий пакет, когда заканчивается передача предыдущего, либо если предыдущего не было, а приложение добавило данные в буфер передачи. Код, который инициирует передачу, получает нотификацию о завершении передачи предыдущего пакета от нижнего уровня USB-стека. Проблема в том, что если нижний уровень стека инициировал передачу ZLP, то нотификацию о завершении он не присылает, т.к. инициировал передачу он сам. Верхний уровень не начинает передачу данных, пока передатчик занят передачей ZLP пакета, и не начинает передачу после ее завершения, поскольку не получает нотификации — процесс передачи останавливается. Исправить проблему очень просто — нужно убрать код нижнего уровня, посылающий ZLP, и предоставить это верхнему уровню стека. Вторая проблема, требующая решения, связана с тем, что процедура, начинающая передачу, может быть вызвана как из контекста обработчика прерывания (по завершении передачи), так и из контекста приложения по добавлении данных в буфер передачи. Чтобы сериализовать вызовы этой процедуры из разных контекстов, нужно запрещать прерывания на время ее исполнения.

Исходный код


Лежит тут github.com/olegv142/stm32tivc_usb_cdc.
В папках stm и ti лежат по 2 тестовых проекта — usb_cdc_echo и usb_cdc_api. Первый просто посылает все полученные данные обратно, второй реализует пакетный протокол, который вы можете легко адаптировать под свои нужды. В папке tools — тестовые скрипты на питоне.

habr.com

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *