Two in one: USB host and composite USB device

Published on July 20, 2016

Two in one: USB host and composite USB device

    image



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


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


    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), (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 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:
    1. LEDs to blink;
    2. Flash, so that you can create / delete files on the disk;
    3. 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);
    4. 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:
    1. Pastilda project repository moved here . Recently release 1.0 was published.
    2. The latest news on the project can be viewed here .
    3. And we finally launched the project site !