Наш магазин на eBay Наш магазин на AliExpress Наш канал в telegram

Программирование ARM-контроллеров STM32 на ядре Cortex-M3. Часть 16. Создание устройства USB HID в Keil uVision при помощи библиотечного компонента USB

Библиотечный компонент USB Device

Модуль USB, реализующий физику, нижний уровень логики и расчёт CRC, встроен во многие контроллеры STM32. Но как этот модуль применять? Как сделать на его основе собственное USB-устройство? При работе в среде разработки Keil uVision самый быстрый и простой вариант — использовать готовый компонент «USB Device», входящий в стандартные библиотеки кейла. Ниже я пошагово расскажу как с помощью этого компонента создать собственное USB устройство класса HID и приведу пример создания такого устройства на микроконтроллере STM32F103C8T6.

Физику в рамках данной статьи рассматривать не будем, про неё можно почитать, например, вот здесь (фактически, всё, что нужно для создания full-speed устройства с точки зрения физики — это подтянуть через резистор к питанию линию D+).

В чём преимущества использования стандартного библиотечного компонента USB Device? На самом деле их много, но из самых главных я бы выделил следующие:

  • Весь процесс «создания» сводится к выбору необходимых компонентов и настройке через визарды их основных свойств: тип устройства, дескрипторы, vid/pid и так далее
  • Для написания пользовательского кода предоставляются шаблоны с готовыми callback-функциями, автоматически вызывающимися при приёме или отправке через USB-модуль репортов того или иного типа. Всё! Вам остаётся только описать в этих шаблонах, что делать с принимаемыми или отправляемыми данными. Весь системный код отделён от пользовательского, добавляется в проект автоматически и фактически вам его трогать никак не нужно.
  • Ну и конечно же нельзя не отметить наличие документации на компонент USB Device (только на английском) а также примеров его использования. Ссылка на официальный хелп

Настройка и конфигурирование библиотечного компонента USB Device для создания USB HID устройства

Итак, давайте рассмотрим по шагам «создание» USB устройства класса HID:

  • Запускаем Keil и создаём новый проект: Project -> New uVision Project. В окне настройки выбираем нужный чип (в моём случае это STM32F103C8)
    картинка

    выбор микроконтроллера

    [свернуть]
  • Далее в открывшемся окне Manage Run-Time Environment выбираем компоненты, которые нужно подключить в наш проект. Для «создания» USB-устройства нам понадобится выбрать компонент USB CORE, а также ниже, во вкладке Device, прописать один компонент класса HID:
    картинка

    выбор подключаемых USB-компонентов

    [свернуть]

    Как можно видеть на скриншоте, при помощи стандартных библиотек можно создать не только HID-устройство, но также устройство класса Mass Storage, Communication, Audio или Custom. Более того, при наличии в вашем контроллере нескольких USB-модулей можно создать композитное устройство (включающее несколько устройств одного и того же или разных классов). У меня в контроллере USB-модуль только один, поэтому «создавать» я буду одно единственное устройство класса HID.
  • После выбора нужных USB компонентов, Keil на лету проанализирует наш выбор и в нижней части этого же окна, на вкладке Validation Output, напишет каких ещё компонентов не хватает в нашем проекте для нормальной работы того, что мы уже выбрали. Для того, чтобы автоматически добавить в проект недостающие компоненты — нужно всего лишь нажать кнопку «Resolve» (внизу, слева). На скриншоте ниже показано какие дополнительные компоненты после этого будут добавлены в проект:
    картинка

    автоматическое добавление недостающих компонентов

    [свернуть]

    Как видите, для работы USB-устройства потребовалось добавить в проект ядро CMSIS, операционную систему реального времени Keil RTX, CMSIS драйвер для USB Device, стартовый код и драйвер GPIO. Это всё, что нужно для работы USB.
    Не стоит пугаться ОСРВ Keil RTX, — это вполне простая и понятная система (но мы её рамках этой статьи рассматривать не будем) и по ней тоже есть неплохая документация на официальном сайте Keil.
  • Кроме компонентов необходимых для работы USB, вы можете добавить к проекту любые другие компоненты по своему желанию. Например, давайте дополнительно добавим в проект стандартные драйвера для GPIO и USART (с которым, после нажатия на кнопку «Resolve», добавятся также компоненты «Framework» и «RCC»), чтобы далее в нашем тестовом проекте можно было пересылать данные из USB в USART и обратно:
    картинка

    дополнительные компоненты для работы с USART

    [свернуть]
  • После того, как вы выбрали все необходимые компоненты и нажали кнопку «Ok» в левой части окна, в менеджере проекта можно видеть все подключенные файлы. Теперь необходимо настроить эти файлы под наше будущее USB-устройство (на картинке ниже файлы, которые нам понадобятся для настройки подчёркнуты красным цветом).
    картинка

    структура подключенных к проекту файлов

    [свернуть]
  • Как я уже писал ранее в статье про CMSIS, — среда Keil позволяет выполнить настройку через специальные визарды (и это удобнее всего). Первым делом, находим в менеджере проекта файл sturtup_stm32f10x_md.s (находится в секции Device), кликаем по нему два раза и у открывшегося окна внизу выбираем вкладку Configuration Wizard. В этом файле настраивается всего два параметра — размеры стека и кучи. У меня по-умолчанию стоят значения 0x200 (512) байт для кучи и 0x400 (1024) байта для стека.
    Требования к памяти описаны в хелпе к компоненту USB Device, в секции Resource Requirements. В соответствии с этим документом в файле sturtup_stm32f103x_md.s нужно выделить 512 байт стека на ядро USB + ещё 512 байт на USB Device Driver. То есть всего нам в этом файле нужно прописать 1024 байта стека (у нас как раз так и есть). Размер кучи зависит только от вашего кода (от того, сколько вы будете динамически памяти выделять), так что тут тоже можно оставить значение по-умолчанию.
    картинка

    настройка файла startup_stm32f103x_md.s

    [свернуть]
  • Теперь нужно сконфигурировать сам чип, настройки которого хранятся в файле RTE_Device.h (находится в секции Device). Конфигурирование выполняется также через визард. Здесь важны следующие вещи:
    • Clock configuration — частоты работы различных шин и модулей. Оставляем всё по-умолчанию.
    • USART1 — ставим галочку. Он нам понадобится, поскольку мы договорились для теста сделать пересылку данных между USART и USB. Мы будем использовать только линии Tx/Rx (напротив них вместо not used выбираем PA9, PA10). Остальные линии использовать не будем, DMA тоже пока оставим без внимания, ремап нам не понадобится.
    • USB Device Full-speed — ага, ставим галочку. А вот напротив CON On/Off Pin галочку снимаем. Эта опция указывает на пин, который используется для управления подтяжкой линии D+ через резистор к питанию (чтобы сообщить хосту о подключении full-speed устройства). Я в тестах управлять подтяжкой не буду (резистор от D+ к питанию будет жёстко подключен), поэтому опцию убираем. В реальных устройствах лучше такое управление делать, поскольку если устройство долго загружается, оно может не успеть ответить хосту после того, как он обнаружит подключение и начнёт его опрашивать. Хост в этом случае может подумать, что устройство на этом порту неисправно и перестать с ним общаться. При наличии управления подтяжкой мы можем включить её (тем самым сообщив хосту о подключении устройства) только тогда, когда полностью загрузимся и будем готовы к общению. Плюс это позволит при необходимости имитировать переподключение устройства.

    картинка

    настройка чипа через RTE_Device.h

    [свернуть]
  • Далее настраиваем через Wizard ОСРВ RTX. Настройки хранятся в файле RTX_Conf_CM.c (находится в секции CMSIS). Нам важно следующее:
    • Меняем значение Main Thread stack size [bytes] на 512. По-умолчанию стоит 200.
    • Меняем значение Number of threads with user-provided stack size на 2 (1 для USB Core и 1 для USB Device). По-умолчанию стоит 0.
    • Меняем значение Total stack size [bytes] for threads with user-provided stack size на 1024 (дока говорит, что надо по 512 на процесс). По-умолчанию стоит 0.
    • На вкладке RTX Kernel Timer Tick Configuration нужно установить RTOS Kernel Timer input clock frequency [Hz] в значение 72000000 (по-умолчанию 12000000). Этот параметр определяет значение константы OS_CLOCK, которое должно совпадать со значением SystemCoreClock (его мы задавали в файле RTE_Device.h).
    • Остальные параметры оставляем по-умолчанию. Значение параметра RTX Timer tick interval value [us] нельзя устанавливать больше 1000, поскольку цикл опроса USB не должен быть больше 1 мс.

    картинка

    настройка ОСРВ RTX через RTX_Conf_CM.c

    [свернуть]
  • Теперь нужно настроить через визард ядро нашего будущего USB-устройства (наконец-то и до настроек самого USB добрались). Настройки хранятся в файле USBD_Config_0.c (находится в секции USB). Нас интересуют следующие из них:
    • Vendor ID / Product ID, — идентификаторы производителя и продукта (те самые VID/PID). Сюда нужно вписать свои значения, поскольку значения по-умолчанию (0xC251/0x0000) принадлежат Keil и не разрешены для использования в пользовательских устройствах. «Свои» значения можно легко купить у USB-IF всего за каких-то 6000$. Список уже занятых значений можно посмотреть вот здесь.
      А нельзя ли кх-м… эм… получить их бесплатно? Можно! Некоторые вендоры, прекратившие выпуск продукции, раздают для open source проектов бесплатные пары VID/PID. Например, вот ссылка на правила, по которым можно получить бесплатную пару VID/PID от вендора 0x1209. Я в тестах буду для красоты использовать пару 0x0011/0x0000 (linux-usb говорит, что такой VID относится к вендору Unknown) и да простят меня всесильные корпорации-члены из USB-IF.
    • Строковые дескрипторы, описывающие производителя и продукт. В Manufacturer String пропишем Radiohlam (по умолчанию прописано Keil Software), а в Product String, скажем USB HID Test Device (по-умолчанию в этой строке указан Keil USB Device). Теперь наше устройство будет определяться в системе как Radiohlam USB Test Device. Ну и до кучи пропишем нашему тестовому девайсу S/N, для чего присвом Serial Number String значение 000000000001 (по-умолчанию было 0001A0000000).

    Остальное оставляем по-умолчанию.

    картинка

    настройка ядра USB через USBD_Config_0.c

    [свернуть]
  • Ну и наконец последнее, что мы можем сконфигурировать через визард — это настройки, специфичные именно для HID-устройства. Они хранятся в файле USBD_Config_HID_0.h (находится в секции USB). Здесь задаются номера конечных точек, желаемое время опроса, размеры репортов и прочая специфическая для класса HID информация. По-идее тут можно всё оставить без изменений, давайте только для того, чтоб было интереснее изменим размеры репортов. Скажем, пусть репорты Input и Output будут по 2 байта, а Feature репорт — 3 байта (по умолчанию они все по 1 байту).
    Важно! Не нужно путать типы репортов (Input, Output, Feature…) и типы маркирующих транзакции пакетов (IN, OUT, SETUP…) — это абсолютно разные вещи (с разных уровней абстракции модели OSI).
    картинка

    конфигурирование специфичных настроек HID через USBD_Config_HID_0.c

    [свернуть]

Написание пользовательского кода USB HID устройства, созданного с использованием библиотечного компонента USB Device

Следующий этап разработки — написание программного кода. Здесь нам очень помогут предоставляемые Keil готовые шаблоны. Итак:

Кликаем в менеджере проектов правой кнопкой на папке Source Group1 и выбираем во всплывающем меню пункт Add New Item to Group ‘Source Group 1’…

картинка

добавление в проект нового файла с исходным кодом

[свернуть]

В открывшемся диалоговом окне слева выбираем тип добавляемого в проект файла (нам нужен User Code Template), а справа выбираем шаблон для компонента RTOS:Keil RTX, который называется CMSIS-RTOS ‘main’ function.

картинка

выбор шаблона, содержащего функцию main

[свернуть]

В этом файле реализован шаблон исходного кода, содержащий главную пользовательскую функцию программы — main. Эта функция получает управление сразу после выполнения стартового кода иниициализации, прописанного в файле startup_stm32f10x_md.s. Пустой шаблон выглядит так:

код

/*----------------------------------------------------------------------------
 * CMSIS-RTOS 'main' function template
 *---------------------------------------------------------------------------*/

#define osObjectsPublic                     // define objects in main module
#include "osObjects.h"                      // RTOS object definitions

/*
 * main: initialize and start the system
 */
int main (void) {
	osKernelInitialize ();                    // initialize CMSIS-RTOS

	// initialize peripherals here

	// create 'thread' functions that start executing,
	// example: tid_name = osThreadCreate (osThread(name), NULL);

	osKernelStart ();                       // start thread execution
	while (1)				// loop forever
	{
	}
}

[свернуть]

В этом шаблоне вызываются две функции: osKernelInitialize и osKernelStart, а между ними нам предлагается вписать инициализацию периферии и создание дополнительных потоков. По-идее планировщик CMSIS-RTOS будет запущен при входе в функцию main и эта функция станет первым активным потоком. Вызов osKernelInitialize остановит работу ОС по переключению задач планировщика до тех пор, пока мы не перезапустим RTOS вызовом функции osKernelStart. Между этими двумя функциями нам предлагают вписать код, инициализирующий нужную нам периферию и создающий если нужно дополнительные потоки.

Вообще говоря, нам функция main кроме как при запуске и не понадобится, так что вызовы функций osKernelInitialize и osKernelStart можно было бы вообще стереть, поток main после выполнения всех настроек и инициализации всех дополнительных потоков завершить и больше о нём не вспоминать, — в примерах от Keil так и сделано. Но я всё это оставлю, так будет правильнее.

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

код

/*----------------------------------------------------------------------------
 * CMSIS-RTOS 'main' function template
 *---------------------------------------------------------------------------*/

#define osObjectsPublic				// define objects in main module
#include "osObjects.h"				// RTOS object definitions
#include "rl_usb.h"				// подключаем хидеры для usb


/*
 * main: initialize and start the system
 */
int main (void) {
	osKernelInitialize ();			// initialize CMSIS-RTOS

	// инициализируем и включаем USB
	USBD_Initialize (0);			// USB Device 0 Initialization
	USBD_Connect (0);			// USB Device 0 Connect

	osKernelStart ();			// start thread execution 
}

[свернуть]

В общем-то всё, на этом месте уже можно скомпилировать программу, залить прошивку в контроллер и попытаться подключить устройство к компьютеру. Оно должно без проблем опознаваться компьютером как USB HID Test Device. Единственная проблема в том, что пока наше устройство абсолютно ничего не делает. Но это не беда, это мы сейчас исправим.

Давайте дополнительно пропишем функции инициализации USART, включим прерывание от приёмника USART, а также выделим глобальный циклический буфер, в который будем складывать принятые данные (для всего этого в шапку придётся добавить ссылку на хидер с описаниями стандартных адресов и имён). После этого наш код будет выглядеть вот так:

код

/*----------------------------------------------------------------------------
 * CMSIS-RTOS 'main' function template
 *---------------------------------------------------------------------------*/

#define osObjectsPublic			// define objects in main module
#include "osObjects.h"			// RTOS object definitions
#include "rl_usb.h"			// подключаем хидеры для usb
#include "stm32f10x.h"			// стандартные хидеры

GPIO_InitTypeDef	PORT_Structure;		// структура для настройки линий порта
USART_InitTypeDef	USART_Structure;	// структура для инициализации USART
NVIC_InitTypeDef	NVIC_Structure;		// структура для настройки NVIC

uint8_t			Data[2];		// буфер для данных, полученных по UART
uint8_t			Recieve_Counter;	// счётчик полученных по UART данных

/* Прототипы функций */
void GPIO_Configuration(void);			// функция настройки GPIO
void USART_Configuration(void);			// функция настройки USART
void NVIC_Configuration(void);			// функция настройки NVIC (здесь нужно разрешить прерывание от USART)

int main (void)
{	osKernelInitialize ();			// initialize CMSIS-RTOS

	Recieve_Counter = 0;			// обнуляем счётчик принятых байт
	
	/* конфигурируем периферию */
	/* включаем тактирование USART1, GPIOA, AFIO */
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
	GPIO_Configuration();			// настраиваем GPIO
	USART_Configuration();			// настраиваем USART
	NVIC_Configuration();			// настраиваем NVIC

	/* инициализируем USB */
	USBD_Initialize (0);			// USB Device 0 Initialization
	USBD_Connect (0);			// USB Device 0 Connect

	osKernelStart ();			// start thread execution 
	while (1)				// loop forever
	{
	}
}

/* Настройка GPIO */
void GPIO_Configuration(void)
{	/* Configure USART1 Tx (PA.09) as alternate function push-pull */
	PORT_Structure.GPIO_Pin = GPIO_Pin_9;			// пин 9, сюда подключена линия Tx модуля USART
	PORT_Structure.GPIO_Mode = GPIO_Mode_AF_PP;		// альтернативная функция push-pull
	PORT_Structure.GPIO_Speed = GPIO_Speed_50MHz;		// максимальная скорость 50 МГц
	GPIO_Init(GPIOA, &PORT_Structure);
 
	/* Configure USART1 Rx (PA.10) as input floating */
	PORT_Structure.GPIO_Pin = GPIO_Pin_10;			// пин 10, сюда подключена линия Rx модуля USART
	PORT_Structure.GPIO_Mode = GPIO_Mode_IN_FLOATING;	// аналоговый вход
	/* максимальная скорость не меняется, так что заново этот элемент структуры можно не записывать */
	GPIO_Init(GPIOA, &PORT_Structure);
}
/* Настройка USART1 */
void USART_Configuration(void)
{	/* инициализируем USART */
	USART_Structure.USART_BaudRate = 115200;					// BaudRate = 115200
	USART_Structure.USART_WordLength = USART_WordLength_8b;				// Word Length = 8 Bits
	USART_Structure.USART_StopBits = USART_StopBits_1;				// One Stop Bit
	USART_Structure.USART_Parity = USART_Parity_No;					// No parity
	USART_Structure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;	// Hardware flow control disabled
	USART_Structure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;			// Receive and transmit enabled
	USART_Init(USART1, &USART_Structure);
 
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);		// включаем прерывание по приёму очередного байта
	USART_Cmd(USART1, ENABLE);				// включаем USART
}
/* настройка NVIC (включаем прерывание для USART1) */
void NVIC_Configuration(void)
{	NVIC_Structure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_Structure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_Structure.NVIC_IRQChannelSubPriority = 0;
	NVIC_Structure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_Structure);
}
/*****************************************************************************/
/*** IRQ ROUTINES ************************************************************/
/*****************************************************************************/
/* обработчик прерывания от USART1 */
void USART1_IRQHandler(void)
{	/* у нас для UART1 включено прерывание только по одному событию, поэтому
	можно не выяснять что конкретно случилось, а сразу приступать к чтению.
	В дальнейшем, однако, всё равно нужно обязательно сделать проверку ошибок передачи */
	/* бит RXNE сбросится автоматически при чтении байта */
	Data[Recieve_Counter] = USART_ReceiveData(USART1);	// сохраняем принятый байт в буфер
	/* делаем наш буфер циклическим */
	if(Recieve_Counter < 1)	Recieve_Counter += 1;
	else			Recieve_Counter = 0;
}

[свернуть]

Теперь давайте добавим к нашему проекту ещё один важный файл. Снова кликаем правой кнопкой на папке Source Group1 и выбираем во всплывающем меню пункт Add New Item to Group 'Source Group 1'... В открывшемся диалоговом окне слева, также, как и в прошлый раз, выбираем тип User Code Template, а справа на этот раз выбираем шаблон, который называется USB Device HID (Human Interface Device):

картинка

выбор шаблона, содержащего callback-функции по обработке репортов

[свернуть]

В итоге в менеджер проекта добавится файл USBD_User_HID_0.c, в котором содержатся готовые callback-функции, срабатывающие по некоторым связанным с USB событиям. Полные описания этих функций можно найти в документации на компонент USB Component, ссылку на которую я давал выше, но часть из них мы рассмотрим ниже:

  • Функция USBD_HID0_GetReport срабатывает как callback при запросе хостом данных или после отправки данных хосту. В этой функции можно написать код, который будет выполняться при запросе хостом данных через разные типы передач и репортов. Тип репорта (Input, Feature) определяется по значению переменной rtype, а тип передачи (Control, Interrupt) - по значению переменной req.
    Передачи Interrupt используются хостом для периодического опроса устройства, если хост не установил для этого устройства значение Idle period равным Infinite (бесконечность).
    Во входных параметрах функции нам, помимо прочего, передают указатель на буфер (buf), в который нужно положить данные, которые мы хотим отправить. При выходе из функции нужно вернуть вызывающему коду количество байт, которые мы хотим отправить.
  • Функция USBD_HID0_SetReport срабатывает как callback когда мы получаем от хоста какие-то данные. Опять же, по значению переменной rtype мы можем определить какой тип репорта использовался для передачи данных (Output, Feature), а по значению переменной req - понять тип передачи (Control, Interrupt). Во входных параметрах функции нам, помимо прочего, передают указатель на буфер, в котором хранятся полученные данные (buf), а также количество байт принятых данных (len).

Помимо этого есть специальная функция - USBD_HID_GetReportTrigger, - позволяющая асинхронно подготовить данные для отправки хосту. Она может быть вызвана из любой части программы.

Как видите, всё достаточно просто. Ниже давайте попробуем этими функциями воспользоваться, придумав для этого какой-нибудь простенький алгоритм. Скажем, пусть при получении через UART двух байт - они подготавливаются для отправки хосту через Input-репорты. Если хост хочет получить данные через Feature-репорт - будем отправлять ему содержимое циклического буфера UART плюс результат операции XOR над байтами нашего буфера (как вы помните, мы при конфигурировании указали размер буфера и размер репорта Input - 2 байта, а размер репорта Feature - 3 байта). При получении Output-репорта или Feature-репорта с данными будем, наоборот, отправлять полученные через USB байты в UART (только давайте при получении данных через Feature-репорт будем отправлять их в UART инвертированными).
Чтобы выполнить всё задуманное, - код обработчика прерывания от UART в файле main.c и коды callback-функций в файле USBD_User_HID_0.c (не забудьте прописать в этот файл стандартные хидеры) нужно изменить следующим образом:

код обработчика прерывания от UART

/* обработчик прерывания от USART1 */
void USART1_IRQHandler(void)
{	/* у нас для UART1 включено прерывание только по одному событию, поэтому
	можно не выяснять что конкретно случилось, а сразу приступать к чтению.
	В дальнейшем, однако, всё равно нужно обязательно сделать проверку ошибок передачи */
	/* бит RXNE сбросится автоматически при чтении байта */
	Data[Recieve_Counter] = USART_ReceiveData(USART1);	// сохраняем принятый байт в буфер
	/* делаем наш буфер циклическим */
	if(Recieve_Counter < 1)	Recieve_Counter += 1;
	else
	{	Recieve_Counter = 0;
		USBD_HID_GetReportTrigger(0, 0, &Data[0], 2);	// асинхронно готовим данные к отправке по USB
	}
}

[свернуть]
содержимое файла USBD_User_HID_0.c

#include "rl_usbd.h"
#include "usb_hid.h"
#include "stm32f10x.h"		// стандартные хидеры


// Called during USBD_Initialize to initialize the USB Device class.
void USBD_HID0_Initialize (void) {
  // Add code for initialization
}


// Called during USBD_Uninitialize to de-initialize the USB Device class.
void USBD_HID0_Uninitialize (void) {
  // Add code for de-initialization
}


// \brief Prepare HID Report data to send.
// \param[in]   rtype   report type:
//                - HID_REPORT_INPUT           = input report requested
//                - HID_REPORT_FEATURE         = feature report requested
// \param[in]   req     request type:
//                - USBD_HID_REQ_EP_CTRL       = control endpoint request
//                - USBD_HID_REQ_PERIOD_UPDATE = idle period expiration request
//                - USBD_HID_REQ_EP_INT        = previously sent report on interrupt endpoint request
// \param[in]   rid     report ID (0 if only one report exists).
// \param[out]  buf     buffer containing report data to send.
// \return              number of report data bytes prepared to send or invalid report requested.
//              - value >= 0: number of report data bytes prepared to send
//              - value = -1: invalid report requested
int32_t USBD_HID0_GetReport (uint8_t rtype, uint8_t req, uint8_t rid, uint8_t *buf) {	
	extern uint8_t Data[2];		// сообщаем компилятору, что буфер UART у нас объявлен в другом файле
	
  switch (rtype) {
    case HID_REPORT_INPUT:
      switch (rid) {
        case 0:
          switch (req) {
            case USBD_HID_REQ_EP_CTRL:        // Explicit USB Host request via Control OUT Endpoint
            case USBD_HID_REQ_PERIOD_UPDATE:  // Periodic USB Host request via Interrupt OUT Endpoint
              break;
            
            case USBD_HID_REQ_EP_INT:         // Called after USBD_HID_GetReportTrigger to signal data obtained
              break;
          }
          break;
      }
      break;
      
    case HID_REPORT_FEATURE:
		buf[0] = Data[0];
		buf[1] = Data[1];
		buf[2] = Data[0] ^ Data[1];	// выполняем XOR
		return (3);
	// break;	// эту команду можно закоментить, она всё равно никогда не выполнится
  }
  return (0);
}


// \brief Process received HID Report data.
// \param[in]   rtype   report type:
//                - HID_REPORT_OUTPUT    = output report received
//                - HID_REPORT_FEATURE   = feature report received
// \param[in]   req     request type:
//                - USBD_HID_REQ_EP_CTRL = report received on control endpoint
//                - USBD_HID_REQ_EP_INT  = report received on interrupt endpoint
// \param[in]   rid     report ID (0 if only one report exists).
// \param[in]   buf     buffer that receives report data.
// \param[in]   len     length of received report data.
// \return      true    received report data processed.
// \return      false   received report data not processed or request not supported.
bool USBD_HID0_SetReport (uint8_t rtype, uint8_t req, uint8_t rid, const uint8_t *buf, int32_t len) {
	uint8_t			InvByte;	// здесь будем хранить инвертированный байт
	uint8_t			i;			// счётчик отправленных байт
  
	switch (rtype){
    case HID_REPORT_OUTPUT:
		for(i=0; i < len; i++)			// перебираем все принятые байты
		{	// ждём возможности отправить данные
			while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET)
			{
			}
			USART_SendData(USART1, buf[i]);		// отправляем очередной байт
		}
		break;
    case HID_REPORT_FEATURE:
		for(i=0; i < len; i++)			// перебираем все принятые байты
		{	// ждём возможности отправить данные
			while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET)
			{
			}
			InvByte = ~buf[i];			// инвертируем байт из буфера
			USART_SendData(USART1, InvByte);	// отправляем очередной байт
		}
      break;
  }
  return true;
}

[свернуть]

Теперь осталось только скомпилировать проект, залить его в контроллер и можно тестировать.

Тестирование и проверка

Ниже показаны скриншоты того, как определяется наше устройство в диспетчере устройств винды, а также программ, которые я использовал для тестов передачи данных между USB и UART:

наше USB-устройство в диспетчере устройств Windows

созданное USB HID устройство в диспетчере устройств Windows

[свернуть]
Тест передачи данных между USB HID устройством и UART

тест передачи данных между USB HID устройством и UART

[свернуть]

На последнем скриншоте, слева - программа RH_Com - уже известная многим терминалка для COM-порта с сайта radiohlam.ru (есть hex-режим, можно скачать с исходниками из раздела "Полезные программы для ПК"). Программа на этом же скриншоте, справа - по сути такая же терминалка, только для USB (позволяет принимать / отправлять данные различными способами USB HID-устройству). Эта терминалка также самописная, поскольку никакой готовой терминалки для тестов USB нагуглить не удалось (на сайт пока не выложил, но видимо скоро выложу, заодно сразу со статьёй о том, как в винде с USB работать).

Самые внимательные обратят внимание на то, что размеры репортов Input, Output и Feature на правой картинке на единицу больше, чем те, которые мы устанавливали при конфигурировании. Это связано с тем, что при настройке в Keil мы указываем только количество информационных байт в репорте, а в винде, при определении размера репорта, его значение возвращается с учётом байта, определяющего Report ID (идентификатор репорта), который формально тоже считается байтом данных.

Update
Выложил на сайт самодельную терминалку для работы с HID-устройствами под Windows (с исходниками), - искать в этой статье или в разделе Полезные программы для ПК

  1. Часть 1. Установка MDK, создание проекта, основы Keil uVision
  2. Часть 2. Команды и директивы ассемблера, структура и синтаксис программы. Первая программа для STM32
  3. Часть 3. Карта памяти контроллеров STM32, методы работы с памятью
  4. Часть 4. Регистры, старт и режимы работы контроллеров STM32
  5. Часть 5. Как залить прошивку в контроллер
  6. Часть 6. Настройка системы тактирования
  7. Часть 7. Работа с портами ввода-вывода
  8. Часть 8. Процедуры на ассемблере для STM32
  9. Часть 9. Система прерываний
  10. Часть 10. CMSIS, использование стандартных библиотек и функций
  11. Часть 11. Подключение и использование драйверов из пакета StdPeriph
  12. Часть 12. Работа с модулями USART и UART.
  13. Часть 13. Работа с модулями ADC
  14. Часть 14. Использование DMA
  15. Часть 15. Таймеры. Глава 1 - Введение. Простейшие таймеры
  16. Часть 15. Таймеры. Глава 2 - Таймеры общего назначения TIM9 - TIM14
  17. Часть 15. Таймеры. Глава 3 - Таймеры общего назначения TIM2 - TIM5
  18. Часть 15. Таймеры. Глава 4 - Продвинутые таймеры TIM1, TIM8
  19. Часть 16. Создание устройства USB HID в Keil uVision при помощи библиотечного компонента USB
  20. Приложение 1. Набор инструкций THUMB-2 и особенности их использования
  21. Приложение 2. Таблица векторов прерываний для семейств STM32F101, STM32F102, STM32F103
  22. Приложение 3. Драйвера и функции библиотеки StdPeriph

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