Два в одном: USB хост и составное USB устройство
Не так давно, была опубликована статья «Пастильда — открытый аппаратный менеджер паролей». Так как данный проект является открытым, то мы решили, что будет интересно, если мы будем писать небольшие заметки о процессе проектирования, о задачах, которые перед нами стоят и о трудностях, с которыми мы сталкиваемся.
Основная суть Пастильды заключается в том, что она является своеобразным переходником между клавиатурой и ПК. Таким образом, она должна уметь:
- быть USB хостом для клавиатуры, которая к ней подключается,
- быть клавиатурой для ПК, чтобы либо перенаправлять сообщения от реальной клавиатуры, либо самой быть клавиатурой,
- быть дисковым накопителем, чтобы можно было редактировать базу данных паролей в удобном для человека виде.
Данный функционал является скелетом нашего проекта, поэтому первая заметка будет посвящена именно ему.
Реализация USB хоста
Итак, во-первых мне нужно было реализовать на устройстве USB хост, чтобы оно могло распознавать и общаться с подключенной к нему клавиатурой. Так как в работе я использую связку Eclipse + GNU ARM Eclipse + libopencm3, то очень хотелось найти уже что-то готовое и желательно написанное с использованием библиотеки libopencm3. Желание мое было очень жирным, до последнего момента не верила, что мои поиски увенчаются успехом. Однако под конец рабочего дня, проскролив интернет до самого дна, я вдруг наткнулась вот на это. libusbhost? Серьезно? И это был не просто написанный на основе libopencm3 usb хост, он еще и был написан под STM32F4, под тот самый, который мы решили использовать в проекте. В общем, звезды сошлись и радости моей не было предела. Кстати, оказалось, что этот проект создавался как часть libopencm3, однако его так и не добавили в библиотеку.
Как библиотеку, libusbhost я не собирала, просто взяла необходимые мне исходники, написала драйвер для клавиатуры и, в общем-то все, погнали! Но обо всем по-порядку.
Из libusbhost я взяла следующие файлы:
- usbh_device_driver.h
- usbh_config.h
- usbh_hubbed.[ch]
- usbh_lld_stm32f4.[ch]
Там был еще файл usart_helpers.[ch], с его помощью можно было по UART передавать в терминал все сообщения, приходящие от устройства в хост и еще много различной отладочной информации. Я с этим функционалом поигралась, но из проекта его убрала.
По аналогии с usbh_driver_hid_mouse.[ch], я написала драйвер для клавиатуры (usbh_driver_hid_kbd.[ch]).
Далее был реализован простенький класс, для работы с хостом:
constexpr uint8_t USB_HOST_TIMER_NUMBER = 6;
constexpr uint16_t USB_HOST_TIMER_PRESCALER = (8400 - 1);
constexpr uint16_t USB_HOST_TIMER_PERIOD = (65535);
typedef void (*redirect)(uint8_t *data, uint8_t len);
typedef void (*control_interception)();
static redirect redirect_callback;
static control_interception control_interception_callback;
class USB_host
{
public:
USB_host(redirect redirect_callback, control_interception control_interception_callback);
void poll();
static void kbd_in_message_handler(uint8_t data_len, const uint8_t *data);
static constexpr hid_kbd_config_t kbd_config = { &kbd_in_message_handler };
static constexpr usbh_dev_driver_t *device_drivers[] =
{
(usbh_dev_driver_t *)&usbh_hid_kbd_driver
};
private:
TIMER_ext *_timer;
void timer_setup();
uint32_t get_time_us();
void oth_hs_setup();
};
Здесь все прозрачно. Устройство должно слушать клавиатуру и ждать набора специальной комбинации клавиш, для перехода в режим выбора логина и пароля. Это происходит в обработчике прерывания от клавиатуры kbd_in_message_handler (uint8_t data_len, const uint8_t *data). Тут есть два варианта развития событий:
- Если комбинации нет, то нам нужно пропустить сообщение от клавиатуры дальше в ПК. Для обработки данного события, в конструктор передана функция _redirect_callback.
- Если комбинация нажата, то нам нужно оповестить систему о том, что мы перешли в режим выбора логина и пароля, следовательно, мы больше не транслируем сообщения от клавиатуры в ПК. Теперь само устройство является клавиатурой, а сообщения от настоящей клавиатуры теперь интерпретируются как команды устройству. Для обработки такого события, в конструктор передана функция _control_interception_callback.
Реализация составного USB устройства
Далее мне нужно было сделать так, чтобы наше устройство отображалось в диспетчере устройств и как клавиатура, и как дисковый накопитель. Тут вся магия в дескрипторах=) В этом документе, в главе 9, подробно описан USB Device Framework. Эту главу нужно очень внимательно прочитать и в соответствии с ней описать дескрипторы устройства. В моем случае получилось следующее:
static constexpr uint8_t keyboard_report_descriptor[] =
{
0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01,
0x75, 0x01, 0x95, 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x03, 0x75, 0x01,
0x05, 0x08, 0x19, 0x01, 0x29, 0x03, 0x91, 0x02, 0x95, 0x05, 0x75, 0x01, 0x91, 0x01, 0x95, 0x06,
0x75, 0x08, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x05, 0x07, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x81, 0x00,
0xC0
};
static constexpr char usb_strings[][30] =
{
"Third Pin",
"Composite Device",
"Pastilda"
};
static constexpr struct usb_device_descriptor dev =
{
USB_DT_DEVICE_SIZE, //bLength
USB_DT_DEVICE, //bDescriptorType
0x0110, //bcdUSB
0x0, //bDeviceClass
0x00, //bDeviceSubClass
0x00, //bDeviceProtocol
64, //bMaxPacketSize0
0x0483, //idVendor
0x5741, //idProduct
0x0200, //bcdDevice
1, //iManufacturer
2, //iProduct
3, //iSerialNumber
1 //bNumConfigurations
};
typedef struct __attribute__((packed))
{
struct usb_hid_descriptor hid_descriptor;
struct
{
uint8_t bReportDescriptorType;
uint16_t wDescriptorLength;
} __attribute__((packed)) hid_report;
} type_hid_function;
static constexpr type_hid_function keyboard_hid_function =
{
{
9, //bLength
USB_DT_HID, //bDescriptorType
0x0111, //bcdHID
0, //bCountryCode
1 //bNumDescriptors
},
{
USB_DT_REPORT,
sizeof(keyboard_report_descriptor)
}
};
static constexpr struct usb_endpoint_descriptor hid_endpoint =
{
USB_DT_ENDPOINT_SIZE, //bLength
USB_DT_ENDPOINT, //bDescriptorType
Endpoint::E_KEYBOARD, //bEndpointAddress
USB_ENDPOINT_ATTR_INTERRUPT, //bmAttributes
64, //wMaxPacketSize
0x20 //bInterval
};
static constexpr struct usb_endpoint_descriptor msc_endpoint[] =
{
{
USB_DT_ENDPOINT_SIZE, //bLength
USB_DT_ENDPOINT, //bDescriptorType
Endpoint::E_MASS_STORAGE_IN, //bEndpointAddress
USB_ENDPOINT_ATTR_BULK, //bmAttributes
64, //wMaxPacketSize
0 //bInterval
},
{
USB_DT_ENDPOINT_SIZE, //bLength
USB_DT_ENDPOINT, //bDescriptorType
Endpoint::E_MASS_STORAGE_OUT, //bEndpointAddress
USB_ENDPOINT_ATTR_BULK, //bmAttributes
64, //wMaxPacketSize
0 //bInterval
}
};
static constexpr struct usb_interface_descriptor iface[] =
{
{
USB_DT_INTERFACE_SIZE, //bLength
USB_DT_INTERFACE, //bDescriptorType
Interface::I_KEYBOARD, //bInterfaceNumber
0, //bAlternateSetting
1, //bNumEndpoints
USB_CLASS_HID, //bInterfaceClass
1, //bInterfaceSubClass
1, //bInterfaceProtocol
0, //iInterface
&hid_endpoint, &keyboard_hid_function,
sizeof(keyboard_hid_function)
},
{
USB_DT_INTERFACE_SIZE, //bLength
USB_DT_INTERFACE, //bDescriptorType
Interface::I_MASS_STORAGE, //bInterfaceNumber
0, //bAlternateSetting
2, //bNumEndpoints
USB_CLASS_MSC, //bInterfaceClass
USB_MSC_SUBCLASS_SCSI, //bInterfaceSubClass
USB_MSC_PROTOCOL_BBB, //bInterfaceProtocol
0x00, //iInterface
msc_endpoint, 0, 0
},
};
static constexpr struct usb_config_descriptor::usb_interface ifaces[]
{
{
(uint8_t *)0, //cur_altsetting
1, //num_altsetting
(usb_iface_assoc_descriptor*)0, //iface_assoc
&iface[Interface::I_KEYBOARD] //altsetting
},
{
(uint8_t *)0, //cur_altsetting
1, //num_altsetting
(usb_iface_assoc_descriptor*)0, //iface_assoc
&iface[Interface::I_MASS_STORAGE] //altsetting
},
};
static constexpr struct usb_config_descriptor config_descr =
{
USB_DT_CONFIGURATION_SIZE, //bLength
USB_DT_CONFIGURATION, //bDescriptorType
0, //wTotalLength
2, //bNumInterfaces
1, //bConfigurationValue
0, //iConfiguration
0x80, //bmAttributes
0x50, //bMaxPower
ifaces
};
keyboard_report_descriptor был взят из документа Device Class Definition for Human Interface Devices (HID) , Appendix E.6 Report Descriptor (Keyboard). Честно, сильно не разбиралась со структурой отчета, поверила документу) В целом, вот пара моментов, на которые нужно обратить особое внимание:
- usb_config_descriptor: поле bNumInterfaces должно отражать столько интерфейсов, сколько реально реализовано. В нашем случае два: HID и MSD
- usb_interface_descriptor: поле bInterfaceNumber обозначает номер интерфейса, но отсчет начинается с нуля, следовательно, номер первого интерфейса — 0.
Вот, с описательной точки зрения, наверно, и все. Не могу не отметить, как грамотно в библиотеке описаны дескрипторы (их описание находится в файле usbstd.h). Все четко по документации. Полагаю, это значительно упростило мне задачу, так как не возникало вопросов «Как же мне описать составное устройство?». Все сразу было понятно.
Для работы с составным устройством был написан класс USB_composite, представленный ниже.
extern "C" void USB_OTG_IRQ();
int USB_control_callback(usbd_device *usbd_dev, struct usb_setup_data *req,
uint8_t **buf, uint16_t *len, usbd_control_complete_callback *complete);
void USB_set_config_callback(usbd_device *usbd_dev, uint16_t wValue);
static uint8_t keyboard_protocol = 1;
static uint8_t keyboard_idle = 0;
static uint8_t keyboard_leds = 0;
class USB_composite
{
public:
uint8_t usbd_control_buffer[500];
UsbCompositeDescriptors *descriptors;
uint8_t usb_ready = 0;
usbd_device *my_usb_device;
USB_composite(const uint32_t block_count,
int (*read_block)(uint32_t lba, uint8_t *copy_to),
int (*write_block)(uint32_t lba, const uint8_t *copy_from));
void usb_send_packet(const void *buf, int len);
int hid_control_request(usbd_device *usbd_dev, struct usb_setup_data *req, uint8_t **buf, uint16_t *len,
void (**complete)(usbd_device *usbd_dev, struct usb_setup_data *req));
void hid_set_config(usbd_device *usbd_dev, uint16_t wValue);
};
Ключевыми в этом классе являются две функции:
- Функция hid_control_request нужна для общения Пастильды как клавиатуры с хостом (в данном случае, хост — это ПК). Вне класса данная функция вызывается через USB_control_callback.
- Функция hid_set_config нужна для того, чтобы настроить конечные точки (endpoints) и зарегистрировать USB_control_callback, описанный в предыдущем пункте. Вне класса данная функция вызывается через USB_set_config_callback.
Ниже представлен вариант их реализации:
int USB_composite::hid_control_request(usbd_device *usbd_dev, struct usb_setup_data *req, uint8_t **buf, uint16_t *len,
void (**complete)(usbd_device *usbd_dev, struct usb_setup_data *req))
{
(void)complete;
(void)usbd_dev;
if ((req->bmRequestType & USB_REQ_TYPE_DIRECTION) == USB_REQ_TYPE_IN)
{
if ((req->bmRequestType & USB_REQ_TYPE_TYPE) == USB_REQ_TYPE_STANDARD)
{
if (req->bRequest == USB_REQ_GET_DESCRIPTOR)
{
if (req->wValue == 0x2200)
{
*buf = (uint8_t *)descriptors->keyboard_report_descriptor;
*len = sizeof(descriptors->keyboard_report_descriptor);
return (USBD_REQ_HANDLED);
}
else if (req->wValue == 0x2100)
{
*buf = (uint8_t *)&descriptors->keyboard_hid_function;
*len = sizeof(descriptors->keyboard_hid_function);
return (USBD_REQ_HANDLED);
}
return (USBD_REQ_NOTSUPP);
}
}
else if ((req->bmRequestType & USB_REQ_TYPE_TYPE) == USB_REQ_TYPE_CLASS)
{
if (req->bRequest == HidRequest::GET_REPORT)
{
*buf = (uint8_t*)&boot_key_report;
*len = sizeof(boot_key_report);
return (USBD_REQ_HANDLED);
}
else if (req->bRequest == HidRequest::GET_IDLE)
{
*buf = &keyboard_idle;
*len = sizeof(keyboard_idle);
return (USBD_REQ_HANDLED);
}
else if (req->bRequest == HidRequest::GET_PROTOCOL)
{
*buf = &keyboard_protocol;
*len = sizeof(keyboard_protocol);
return (USBD_REQ_HANDLED);
}
return (USBD_REQ_NOTSUPP);
}
}
else
{
if ((req->bmRequestType & USB_REQ_TYPE_TYPE) == USB_REQ_TYPE_CLASS)
{
if (req->bRequest == HidRequest::SET_REPORT)
{
if (*len == 1)
{
keyboard_leds = (*buf)[0];
}
return (USBD_REQ_HANDLED);
}
else if (req->bRequest == HidRequest::SET_IDLE)
{
keyboard_idle = req->wValue >> 8;
return (USBD_REQ_HANDLED);
}
else if (req->bRequest == HidRequest::SET_PROTOCOL)
{
keyboard_protocol = req->wValue;
return (USBD_REQ_HANDLED);
}
}
return (USBD_REQ_NOTSUPP);
}
return (USBD_REQ_NEXT_CALLBACK);
}
int USB_control_callback(usbd_device *usbd_dev,
struct usb_setup_data *req, uint8_t **buf, uint16_t *len,
usbd_control_complete_callback *complete)
{
return(usb_pointer->hid_control_request(usbd_dev, req, buf, len, complete));
}
void USB_composite::hid_set_config(usbd_device *usbd_dev, uint16_t wValue)
{
(void)wValue;
(void)usbd_dev;
usbd_ep_setup(usbd_dev, Endpoint::E_KEYBOARD, USB_ENDPOINT_ATTR_INTERRUPT, 8, 0);
usbd_register_control_callback(usbd_dev, USB_REQ_TYPE_INTERFACE, USB_REQ_TYPE_RECIPIENT, USB_control_callback );
}
void USB_set_config_callback(usbd_device *usbd_dev, uint16_t wValue)
{
usb_pointer->hid_set_config(usbd_dev, wValue) ;
}
Как правило, функции control_request и set_config должны быть явно описаны для каждого устройства. Однако из этого правила есть исключение: Mass Storage Device. Итак, разберемся с конструктором класса USB_Composite.
Во-первых, мы инициализируем ноги USB OTG FS:
GPIO_ext uf_p(PA11);
GPIO_ext uf_m(PA12);
uf_p.mode_setup(Mode::ALTERNATE_FUNCTION, PullMode::NO_PULL);
uf_m.mode_setup(Mode::ALTERNATE_FUNCTION, PullMode::NO_PULL);
uf_p.set_af(AF_Number::AF10);
uf_m.set_af(AF_Number::AF10);
Во-вторых, нам нужно проинициализировать наше составное устройство, зарегистрировать USB_set_config_callback, о котором шла речь выше, и разрешить прерывание:
my_usb_device = usbd_init(&otgfs_usb_driver, &(UsbCompositeDescriptors::dev),
&(UsbCompositeDescriptors::config_descr), (const char**)UsbCompositeDescriptors::usb_strings, 3,
usbd_control_buffer, sizeof(usbd_control_buffer));
usbd_register_set_config_callback(my_usb_device, USB_set_config_callback);
nvic_enable_irq(NVIC_OTG_FS_IRQ);
Этого достаточно для того, чтобы в диспетчере устройств наше устройство распознавалось:
- Во вкладке «Контроллеры USB»: как составное устройство,
- В этой же вкладке, как «Запоминающее устройство для USB»,
- Во вкладке «Клавиатуры», как «Клавиатура HID».
Однако «Запоминающее устройство для USB» будет помечено предупреждением о том, что устройство работает неправильно. Все дело в том, что в отличие от других USB устройств, Mass Storage инициализируется немного иначе, через функцию usb_msc_init, описанную в файле usb_msc.c библиотеки libopencm3. Выше я уже упоминала о том, что для MSD нет необходимости явно описывать функции control_request и set_config. Это потому, что функция usb_msc_init все сделает за нас: и конечные точки настроит, и все колбэки зарегистрирует. Таким образом, нам нужно дополнить конструктор еще одной строчкой:
usb_msc_init(my_usb_device, Endpoint::E_MASS_STORAGE_IN, 64, Endpoint::E_MASS_STORAGE_OUT, 64,
"ThirdPin", "Pastilda", "0.00", block_count, read_block, write_block);
Тут можно заметить, что при инициализации MSD, нам нужно передать ему минимальное API для работы с памятью:
- block_count: количество секторов памяти,
- read_block: функция для чтения сектора,
- write_block: функция для записи сектора.
В Пастильде мы используем внешний флеш SST25VF064C. Драйвер для этой микросхемы можно посмотреть здесь. В дальнейшем, на основе этого драйвера, во флеше будет реализована файловая система. Скорее всего, об этом как-нибудь подробно напишет мой коллега. Но так как я хотела поскорее протестировать работу MSD, я написала зародыш файловой системы=) Над ним можно поплакать здесь.
Так вот. Теперь, когда конструктор класса USB_Composite дописан, можно собрать проект, прошить устройство и увидеть, что «Запоминающее устройство для USB» больше не помечено предупреждением, а во вкладке «Дисковые устройства» можно обнаружить «ThirdPin Pastilda USB Device». И, казалось бы, все хорошо. Но нет=) Проблем стало больше:
1. Зайти на диск невозможно. При попытке сделать это все виснет, умирает, компьютеру очень плохо.
2. Распознавание устройства как дискового занимает более 2-х минут.
Об этих проблемах и о том, как их решить без вреда для здоровья написано здесь: USB mass storage device и libopencm3.
И, о, чудо! Никаких пятен=) Теперь все работает. У нас есть USB хост и составное USB устройство. Осталось только объединить их работу.
Объединение хоста и составного устройства
Наша цель:
- Транслировать сообщения от клавиатуры в ПК до тех пор, пока не нажата комбинация Ctrl + Shift + ~.
- После нажатия комбинации Ctrl + Shift + ~, Пастильда должна перехватить управление и отправить сообщение в ПК как клавиатура, после чего мы возвращаемся в режим трансляции и снова ожидаем комбинацию.
Код, реализующий все это, простой как палка:
App *app_pointer;
App::App()
{
app_pointer = this;
clock_setup();
systick_init();
_leds_api = new LEDS_api();
_flash = new FlashMemory();
usb_host = new USB_host(redirect, control_interception);
usb_composite = new USB_composite(_flash->flash_blocks(), _flash->flash_read, _flash->flash_write);
}
void App::process()
{
_leds_api->toggle();
usb_host->poll();
}
void App::redirect(uint8_t *data, uint8_t len)
{
app_pointer->usb_composite->usb_send_packet(data, len);
}
void App::control_interception()
{
memset(app_pointer->key, 0, 8);
app_pointer->key[2] = KEY_W;
app_pointer->key[3] = KEY_O;
app_pointer->key[4] = KEY_N;
app_pointer->key[5] = KEY_D;
app_pointer->key[6] = KEY_E;
app_pointer->key[7] = KEY_R;
app_pointer->usb_composite->usb_send_packet(app_pointer->key, 8);
app_pointer->key[2] = 0;
app_pointer->key[3] = 0;
app_pointer->key[4] = 0;
app_pointer->key[5] = 0;
app_pointer->key[6] = 0;
app_pointer->key[7] = 0;
app_pointer->usb_composite->usb_send_packet(app_pointer->key, 8);
app_pointer->key[2] = KEY_SPACEBAR;
app_pointer->key[3] = KEY_W;
app_pointer->key[4] = KEY_O;
app_pointer->key[5] = KEY_M;
app_pointer->key[6] = KEY_A;
app_pointer->key[7] = KEY_N;
app_pointer->usb_composite->usb_send_packet(app_pointer->key, 8);
app_pointer->key[2] = 0;
app_pointer->key[3] = 0;
app_pointer->key[4] = 0;
app_pointer->key[5] = 0;
app_pointer->key[6] = 0;
app_pointer->key[7] = 0;
app_pointer->usb_composite->usb_send_packet(app_pointer->key, 8);
}
В конструкторе мы инициализируем все, что необходимо:
- Светодиоды, чтобы моргали;
- Флеш, чтобы можно было файлы на диске создавать / удалять;
- Хост, передав ему при этом функцию redirect (что делать, если комбинации нет) и control_interception (что делать, если комбинация нажата);
- Составное устройство, передав ему функции чтения / записи памяти;
И вот, собственно, и все. Начало положено, скелет нашего устройства создан. Совсем скоро будет доработана файловая система, по нажатию комбинации Ctrl + Shift + ~, мы будем попадать в однострочное меню, а во флеше будет храниться наша зашифрованная база данных паролей.
Буду рада любым комментариям и пожеланиям.
И, конечно же, ссылка на github.
Комментарии (4)
20 июля 2016 в 17:48 (комментарий был изменён)
0↑
↓
Ехали антипаттерны через антипаттерны:memset(app_pointer->key, 0, 8);
app_pointer->key[2] = KEY_W;
app_pointer->key[3] = KEY_O;
app_pointer->key[4] = KEY_N;
app_pointer->key[5] = KEY_D;
app_pointer->key[6] = KEY_E;
app_pointer->key[7] = KEY_R;
app_pointer->usb_composite->usb_send_packet(app_pointer->key, 8);app_pointer->key[2] = 0;
app_pointer->key[3] = 0;
app_pointer->key[4] = 0;
app_pointer->key[5] = 0;
app_pointer->key[6] = 0;
app_pointer->key[7] = 0;
1. Магическая константа 8. Стоит поменять размер массива — имеем жуткий головняк с отловом константы.
2. Как следствие первого — первый и второй аргументы memset между собой не связаны. Компилятор никак не сможет предупредить нас, если что-то пошло не так. Напоминаю, что у вас C++, а значит есть шаблоны. Можно сделать все проверки очень красиво и в compile time.
3. Обнуление — классный кусок «китайского кода». Вам работу построчно оплачивают? Это раздутый кирпич кода, весь семантический смысл которого — memset. Если вы думаете, что цикл будет работать дольше — заставьте компилятор его раскрутить.20 июля 2016 в 18:31
0↑
↓
На этом месте в дальнейшем будет меню, здесь нужно было просто вывести что-нибудь, что бы понять, работает USB HID или нет.
20 июля 2016 в 18:24
0↑
↓
Что поражает в «экосистеме» разработки под микроконтроллеры — жуткий зоопарк не совместимых друг с другом и с немного отличающися железом, нет, не библиотек, а огрызков исходного кода. Каких-либо вменяемых абстракций просто нет, народ из своей бизнес-логики пишет напрямую в регистры и занимается прочим непотребством. В ChibiOS хотя бы более-менее работающий HAL сделали и понятия блочных устройств и потоков ввели, но это не сильно спасает, когда в руки попадает какой-нибудь экранчик, пример к которому написан ещё до появления STMCube и использует старый и вручную поправленый cmsis, а иной документации просто нет.20 июля 2016 в 18:31
0↑
↓
Отмечу, что «пишет напрямую в регистры» в большинстве попросту необходимо. Довольно часто невозможно спроектировать программную абстракцию. Или такая абстракция будет неоправданно «тяжелой».