⬆️ ⬇️

Two in one: USB host and composite USB device

image






Not so long ago, the article "Pastilde - an open hardware password manager" was published . Since this project is open, we decided that it would be interesting if we write small notes about the design process, about the tasks that we face and about the difficulties that we face.



The main essence of Pastilde is that it is a kind of adapter between the keyboard and the PC. Thus, she should be able to:



This functionality is the skeleton of our project, so the first article will be devoted to him.



')

USB host implementation



So, first, I needed to implement a host on a USB device so that it could recognize and communicate with the keyboard connected to it. Since I use the Eclipse + GNU ARM Eclipse + libopencm3 bundle in my work, I really wanted to find something ready-made and preferably written using the libopencm3 library. My desire was very bold, until the last moment did not believe that my search will be crowned with success. However, at the end of the working day, scrolling the Internet to the bottom, I suddenly stumbled upon this . libusbhost? Seriously? And it was not just a usb host based on a libopencm3 usb, it was also written under STM32F4, the one we decided to use in the project. In general, the stars came together and my joy knew no bounds. By the way, it turned out that this project was created as part of libopencm3, but it was never added to the library.



As a library, I did not compile libusbhost, I just took the sources I needed, wrote the driver for the keyboard and, in general, everything drove! But first things first.



From libusbhost I took the following files:



There was also a usart_helpers. [Ch] file, with its help it was possible to send all messages coming from the device to the host via the UART via the UART and a lot of different debug information. I played with this functionality, but removed it from the project.



By analogy with usbh_driver_hid_mouse. [Ch], I wrote a driver for the keyboard (usbh_driver_hid_kbd. [Ch]).



Next, a simple class was implemented for working with the host:



USB Host Class
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(); }; 




Everything is transparent here. The device must listen to the keyboard and wait for a special key combination to switch to the login and password selection mode. This happens in the kbd_in_message_handler keyboard interrupt handler (uint8_t data_len, const uint8_t * data). There are two options for the development of events:





Implementing a composite USB device



Next, I needed to make our device appear in the device manager both as a keyboard and as a disk drive. Here, all the magic in the descriptors =) This document, in Chapter 9, describes in detail the USB Device Framework. This chapter should be very carefully read and in accordance with it to describe the descriptors of the device. In my case, the following happened:



Composite USB Descriptors
 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 }; 




The keyboard_report_descriptor was taken from the Device Class Definition for Human Interface Devices (HID) document, Appendix E.6 Report Descriptor (Keyboard). Honestly, I didn’t understand much about the structure of the report, I believed the document) In general, here are a couple of points that need special attention:



Here, from a descriptive point of view, probably, that's all. I cannot fail to note how well the descriptors are described in the library (their description is in the usbstd.h file). Everything is clear on the documentation. I suppose this greatly simplified my task, since there were no questions like “How can I describe a composite device?”. Everything was immediately clear.



To work with the composite device, the USB_composite class has been written, presented below.



Composite USB Class
 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); }; 




Key features in this class are two functions:



Below is a variant of their implementation:



Callbacks
 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) ; } 




As a rule, the control_request and set_config functions must be explicitly described for each device. However, there is an exception to this rule: Mass Storage Device. So, let's deal with the constructor of the class USB_Composite.



First, we initialize the USB OTG FS legs:

 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); 


Secondly, we need to initialize our composite device, register USB_set_config_callback, which was discussed above, and enable the interrupt:

  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); 


This is enough for the device manager to recognize our device:



However, the “USB Mass Storage Device” will be flagged with a warning that the device is not working properly. The thing is that unlike other USB devices, Mass Storage is initialized a little differently, through the usb_msc_init function, described in the usb_msc.c file of the libopencm3 library. I mentioned earlier that for MSD there is no need to explicitly describe the control_request and set_config functions. This is because the usb_msc_init function will do everything for us: it will configure both endpoints and register all callbacks. Thus, we need to add one more line to the constructor:

  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); 


Here you can see that when MSD is initialized, we need to give it a minimal API for working with memory:



In Pastilde, we use external flash SST25VF064C. The driver for this chip can be found here . In the future, based on this driver, the file system will be implemented in the flash. Most likely, my colleague will write about this in some detail. But since I wanted to quickly test the work of MSD, I wrote the germ of the file system =) You can cry on it here .



So here. Now that the USB_Composite class constructor has been added, you can build a project, flash the device and see that the USB Mass Storage Device is no longer flagged with a warning, and you can find the ThirdPin Pastilda USB Device tab in the Disk Devices tab. And, it would seem, all is well. But no =) There are more problems:



1. It is impossible to enter the disc. When you try to do this, everything hangs, dies, the computer is very bad.

2. Recognizing a device as a disk takes more than 2 minutes.



About these problems and how to solve them without harm to health is written here: USB mass storage device and libopencm3 .



And, oh, a miracle! No stains =) Now everything works. We have a USB host and a composite USB device. It remains only to combine their work.



Combining the host and composite device



Our goal:





The code that implements all of this is as simple as a stick:



App.cpp
 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); } 




In the constructor, we initialize everything that is needed:

  1. LEDs to blink;
  2. Flash, so you can create files on the disk / delete;
  3. Host, passing it the redirect function (what to do if there is no combination) and control_interception (what to do if the combination is pressed);
  4. Composite device, passing it the function of reading / writing memory;


And so, in fact, that's all. A start, the skeleton of our device is created. Very soon the file system will be finalized, by pressing the Ctrl + Shift + ~ combination, we will get into the one-line menu, and our encrypted password database will be stored in the flash.



I will be glad to any comments and suggestions.



And, of course, the github link.



UPD Jun 27, 2017:

  1. The Pastilda project repository moved here . The release 1.0 was recently published.
  2. Latest news on the project can be viewed here .
  3. And we finally launched the project site !

Source: https://habr.com/ru/post/305602/



All Articles