Two in one: USB host and composite USB device

Not so long ago, the article "Pastilda - 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 the difficulties that we face.
The main essence of Pastilda is that it is a kind of adapter between the keyboard and the PC. Thus, she should be able to:
- be the USB host for the keyboard that connects to it,
- be a keyboard for a PC to either redirect messages from a real keyboard, or be a keyboard itself,
- be a disk drive so that you can edit the password database in a human-friendly form.
This functionality is the skeleton of our project, so the first note will be dedicated to him.
Implementing a USB host
So, firstly, I needed to implement a USB host on the device so that it could recognize and communicate with the keyboard connected to it. Since in my work I use the Eclipse + GNU ARM Eclipse + libopencm3 bundle, I really wanted to find something ready-made and preferably written using the libopencm3 library. My desire was very fat, until the last moment I did not believe that my searches would succeed. 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 host written on the basis of libopencm3 usb, it was also written under STM32F4, under the one we decided to use in the project. In general, the stars converged 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 a driver for the keyboard and, in general, drove them all! But first things first.
From libusbhost I took the following files:
- usbh_device_driver.h
- usbh_config.h
- usbh_hubbed. [ch]
- usbh_lld_stm32f4. [ch]
There was also a usart_helpers. [Ch] file, with its help it was possible to send via UART to the terminal all messages coming from the device to the host and a lot of different debugging information. I played with this functionality, but removed it from the project.
Similar to usbh_driver_hid_mouse. [Ch], I wrote a driver for the keyboard (usbh_driver_hid_kbd. [Ch]).
Next, a simple class was implemented to work with the host:
USB Host Class
constexpruint8_t USB_HOST_TIMER_NUMBER = 6;
constexpruint16_t USB_HOST_TIMER_PRESCALER = (8400 - 1);
constexpruint16_t USB_HOST_TIMER_PERIOD = (65535);
typedefvoid(*redirect)(uint8_t *data, uint8_t len);
typedefvoid(*control_interception)();
static redirect redirect_callback;
static control_interception control_interception_callback;
classUSB_host
{public:
USB_host(redirect redirect_callback, control_interception control_interception_callback);
voidpoll();
staticvoidkbd_in_message_handler(uint8_t data_len, constuint8_t *data);
staticconstexprhid_kbd_config_t kbd_config = { &kbd_in_message_handler };
staticconstexprusbh_dev_driver_t *device_drivers[] =
{
(usbh_dev_driver_t *)&usbh_hid_kbd_driver
};
private:
TIMER_ext *_timer;
voidtimer_setup();
uint32_t get_time_us();
voidoth_hs_setup();
};
Everything is transparent here. The device must listen to the keyboard and wait for a set of special key combinations to enter the login and password selection mode. This happens in the keyboard interrupt handler kbd_in_message_handler (uint8_t data_len, const uint8_t * data). There are two options for the development of events:
- If there is no combination, then we need to skip the message from the keyboard further to the PC. To handle this event, the _redirect_callback function was passed to the constructor.
- If the combination is pressed, then we need to notify the system that we have switched to the login and password selection mode, therefore, we no longer broadcast messages from the keyboard to the PC. Now the device itself is a keyboard, and messages from a real keyboard are now interpreted as commands to the device. To handle such an event, the function _control_interception_callback was passed to the constructor.
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 =) In this document, in Chapter 9, the USB Device Framework is described in detail. This chapter must be read very carefully and described in accordance with it the device descriptors. In my case, the following happened:
Composite USB Descriptors
staticconstexpruint8_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
};
staticconstexprchar usb_strings[][30] =
{
"Third Pin",
"Composite Device",
"Pastilda"
};
staticconstexprstructusb_device_descriptordev =
{
USB_DT_DEVICE_SIZE, //bLength
USB_DT_DEVICE, //bDescriptorType0x0110, //bcdUSB0x0, //bDeviceClass0x00, //bDeviceSubClass0x00, //bDeviceProtocol64, //bMaxPacketSize00x0483, //idVendor0x5741, //idProduct0x0200, //bcdDevice1, //iManufacturer2, //iProduct3, //iSerialNumber1//bNumConfigurations
};
typedefstruct __attribute__((packed))
{structusb_hid_descriptorhid_descriptor;struct
{uint8_t bReportDescriptorType;
uint16_t wDescriptorLength;
} __attribute__((packed)) hid_report;
} type_hid_function;
staticconstexpr type_hid_function keyboard_hid_function =
{
{
9, //bLength
USB_DT_HID, //bDescriptorType0x0111, //bcdHID0, //bCountryCode1//bNumDescriptors
},
{
USB_DT_REPORT,
sizeof(keyboard_report_descriptor)
}
};
staticconstexprstructusb_endpoint_descriptorhid_endpoint =
{
USB_DT_ENDPOINT_SIZE, //bLength
USB_DT_ENDPOINT, //bDescriptorType
Endpoint::E_KEYBOARD, //bEndpointAddress
USB_ENDPOINT_ATTR_INTERRUPT, //bmAttributes64, //wMaxPacketSize0x20//bInterval
};
staticconstexprstructusb_endpoint_descriptormsc_endpoint[] =
{
{
USB_DT_ENDPOINT_SIZE, //bLength
USB_DT_ENDPOINT, //bDescriptorType
Endpoint::E_MASS_STORAGE_IN, //bEndpointAddress
USB_ENDPOINT_ATTR_BULK, //bmAttributes64, //wMaxPacketSize0//bInterval
},
{
USB_DT_ENDPOINT_SIZE, //bLength
USB_DT_ENDPOINT, //bDescriptorType
Endpoint::E_MASS_STORAGE_OUT, //bEndpointAddress
USB_ENDPOINT_ATTR_BULK, //bmAttributes64, //wMaxPacketSize0//bInterval
}
};
staticconstexprstructusb_interface_descriptoriface[] =
{
{
USB_DT_INTERFACE_SIZE, //bLength
USB_DT_INTERFACE, //bDescriptorType
Interface::I_KEYBOARD, //bInterfaceNumber0, //bAlternateSetting1, //bNumEndpoints
USB_CLASS_HID, //bInterfaceClass1, //bInterfaceSubClass1, //bInterfaceProtocol0, //iInterface
&hid_endpoint, &keyboard_hid_function,
sizeof(keyboard_hid_function)
},
{
USB_DT_INTERFACE_SIZE, //bLength
USB_DT_INTERFACE, //bDescriptorType
Interface::I_MASS_STORAGE, //bInterfaceNumber0, //bAlternateSetting2, //bNumEndpoints
USB_CLASS_MSC, //bInterfaceClass
USB_MSC_SUBCLASS_SCSI, //bInterfaceSubClass
USB_MSC_PROTOCOL_BBB, //bInterfaceProtocol0x00, //iInterface
msc_endpoint, 0, 0
},
};
staticconstexprstructusb_config_descriptor::usb_interface ifaces[]
{
{
(uint8_t *)0, //cur_altsetting1, //num_altsetting
(usb_iface_assoc_descriptor*)0, //iface_assoc
&iface[Interface::I_KEYBOARD] //altsetting
},
{
(uint8_t *)0, //cur_altsetting1, //num_altsetting
(usb_iface_assoc_descriptor*)0, //iface_assoc
&iface[Interface::I_MASS_STORAGE] //altsetting
},
};
staticconstexprstructusb_config_descriptorconfig_descr =
{
USB_DT_CONFIGURATION_SIZE, //bLength
USB_DT_CONFIGURATION, //bDescriptorType0, //wTotalLength2, //bNumInterfaces1, //bConfigurationValue0, //iConfiguration0x80, //bmAttributes0x50, //bMaxPower
ifaces
};
keyboard_report_descriptor was taken from Device Class Definition for Human Interface Devices (HID) , Appendix E.6 Report Descriptor (Keyboard). Honestly, I did not understand much about the structure of the report, I believed the document) In general, here are a couple of points that you need to pay special attention to:
- usb_config_descriptor : the bNumInterfaces field should reflect as many interfaces as it is actually implemented. In our case, two: HID and MSD
- usb_interface_descriptor : the bInterfaceNumber field indicates the number of the interface, but the count starts from zero, therefore, the number of the first interface is 0.
That, from a descriptive point of view, is probably all. I cannot but note how well the descriptors are described in the library (their description is in the usbstd.h file). Everything is clear from 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 a composite device, the USB_composite class was written below.
Composite USB Class
extern"C"voidUSB_OTG_IRQ();
intUSB_control_callback(usbd_device *usbd_dev, struct usb_setup_data *req,
uint8_t **buf, uint16_t *len, usbd_control_complete_callback *complete);
voidUSB_set_config_callback(usbd_device *usbd_dev, uint16_t wValue);
staticuint8_t keyboard_protocol = 1;
staticuint8_t keyboard_idle = 0;
staticuint8_t keyboard_leds = 0;
classUSB_composite
{public:
uint8_t usbd_control_buffer[500];
UsbCompositeDescriptors *descriptors;
uint8_t usb_ready = 0;
usbd_device *my_usb_device;
USB_composite(constuint32_t block_count,
int (*read_block)(uint32_t lba, uint8_t *copy_to),
int (*write_block)(uint32_t lba, constuint8_t *copy_from));
voidusb_send_packet(constvoid *buf, int len);
inthid_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));
voidhid_set_config(usbd_device *usbd_dev, uint16_t wValue);
};
The key in this class are two functions:
- The hid_control_request function is needed for Pastilda to communicate as a keyboard with the host (in this case, the host is a PC). Outside the class, this function is called via USB_control_callback.
- The hid_set_config function is needed in order to configure endpoints and register the USB_control_callback described in the previous paragraph. Outside the class, this function is called via USB_set_config_callback.
Below is an option for 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);
}
elseif (req->wValue == 0x2100)
{
*buf = (uint8_t *)&descriptors->keyboard_hid_function;
*len = sizeof(descriptors->keyboard_hid_function);
return (USBD_REQ_HANDLED);
}
return (USBD_REQ_NOTSUPP);
}
}
elseif ((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);
}
elseif (req->bRequest == HidRequest::GET_IDLE)
{
*buf = &keyboard_idle;
*len = sizeof(keyboard_idle);
return (USBD_REQ_HANDLED);
}
elseif (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);
}
elseif (req->bRequest == HidRequest::SET_IDLE)
{
keyboard_idle = req->wValue >> 8;
return (USBD_REQ_HANDLED);
}
elseif (req->bRequest == HidRequest::SET_PROTOCOL)
{
keyboard_protocol = req->wValue;
return (USBD_REQ_HANDLED);
}
}
return (USBD_REQ_NOTSUPP);
}
return (USBD_REQ_NEXT_CALLBACK);
}
intUSB_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 );
}
voidUSB_set_config_callback(usbd_device *usbd_dev, uint16_t wValue){
usb_pointer->hid_set_config(usbd_dev, wValue) ;
}
Typically, the control_request and set_config functions should be explicitly described for each device. However, there is an exception to this rule: Mass Storage Device. So, we will deal with the constructor of the USB_Composite class.
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 the USB_set_config_callback mentioned above, and enable the interrupt:
my_usb_device = usbd_init(&otgfs_usb_driver, &(UsbCompositeDescriptors::dev),
&(UsbCompositeDescriptors::config_descr), (constchar**)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 to recognize our device in the device manager:
- In the tab "USB controllers": as a composite device,
- In the same tab as “USB Mass Storage Device”,
- In the “Keyboards” tab, like “Keyboard HID”.
However, “USB Mass Storage Device” will be marked 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 functions control_request and set_config. This is because the usb_msc_init function will do everything for us: it will configure endpoints and register all callbacks. Thus, we need to supplement the constructor with one more line:
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 initializing MSD, we need to pass it the minimum API for working with memory:
- block_count: number of memory sectors
- read_block: function for reading a sector,
- write_block: function for writing a sector.
In Pastilda, we use the 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 in detail. But since I wanted to quickly test the work of MSD, I wrote the germ of the file system =) You can cry over it here .
So here. Now that the constructor for the USB_Composite class has been added, you can assemble the project, flash the device and see that the “USB storage device” is no longer marked with a warning, and in the “Disk devices” tab you can find the “ThirdPin Pastilda USB Device”. And, it would seem, all is well. But no =) There are more problems:
1. It is impossible to access the disk. 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 spots =) 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:
- Broadcast messages from the keyboard to the PC until the combination Ctrl + Shift + ~ is pressed.
- After pressing the combination Ctrl + Shift + ~, Pastilda should take control and send the message to the PC as a keyboard, after which we return to the broadcast mode and again wait for the combination.
Code that implements all 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:
- LEDs to blink;
- Flash, so that you can create / delete files on the disk;
- The 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);
- A composite device, passing it the functions of reading / writing memory;
And that's all. A start has been made, the skeleton of our device has been created. The file system will be finalized very soon, by pressing the combination Ctrl + Shift + ~, we will get to the single-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, a link to github .
UPD 06/27/2017:
- Pastilda project repository moved here . Recently release 1.0 was published.
- The latest news on the project can be viewed here .
- And we finally launched the project site !