Two in one: USB host and a USB composite device

Not so long ago, an article was published . Since the project is open, we thought it would be interesting, if we write small notes about the design process, the challenges we face and the difficulties we encounter.
The main essence of Pastille is that it is a kind of adapter between the keyboard and PC. Thus, it should be able to:
the
-
the
- to be a USB host for the keyboard that connects to it, the
- to be a PC keyboard, or to redirect messages from a real keyboard, or to be the keyboard the
- to be disk drive so that you can edit the database password in human readable form.
This function is a skeleton of our project, so the first post will be dedicated to him.
Implementing USB host
So, first I had to implement on the device USB host so it can recognize and communicate with the connected keyboard. Because at work I use a bundle Eclipse + GNU ARM Eclipse + libopencm3, I wanted to find something already prepared and preferably written using the library libopencm3. My desire was very greasy, to the latest moment did not believe that my search will be successful. However, at the end of the day, prokralis Internet to the bottom, I suddenly stumbled on a this. libusbhost? Seriously? And it was not just written based on libopencm3 usb host, he was written under STM32F4, under the same which we have chosen to use in the project. In General, the stars aligned 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 the library libusbhost I'm not collecting, just taking a much needed source code, written a driver for the keyboard and, in General, all drove! But everything in order.
From libusbhost I took the following files:
the
-
the
- usbh_device_driver.h the
- usbh_config.h the
- usbh_hubbed.[ch] the
- usbh_lld_stm32f4.[ch]
There was another file usart_helpers.[ch], it was possible for the UART to transmit to the terminal, all messages coming from the device to host and a lot of different debugging information. I functionality play, but from the draft it was removed.
By analogy with usbh_driver_hid_mouse.[ch] I've written a driver for the keyboard (usbh_driver_hid_kbd.[ch]).
It was further implemented a simple class 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;
USB_host class
{
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();
};
Here everything is transparent. The device must listen to the keyboard and wait for a set of special key combinations to switch to the selection mode login and password. This happens in the handler of interruption from the keyboard kbd_in_message_handler(uint8_t data_len, const uint8_t *data). There are two versions of events:
the
-
the
- If there is no combination, then we need to skip the message from the keyboard on the PC. To handle this event in the constructor the function _redirect_callback. the
- If the combination is pressed, we need to let the system know that we have moved into mode login and password, therefore we are no longer broadcast messages from the keyboard to the PC. Now the device itself is a keyboard, and the messages from this keyboard now interpreted as commands to the device. To handle such event, to the constructor of the function _control_interception_callback.
- Function hid_control_request need to communicate Pastille as keyboards to a host (in this case, the host is a PC). Outside the class this function is called via USB_control_callback. the
- Function hid_set_config needed to manage endpoints (endpoints) and register USB_control_callback described in the previous paragraph. Outside the class this function is called via USB_set_config_callback.
Implementation composite USB device
Then I had to make our device appear in the device Manager as a keyboard, and as a disk drive. Then all the magic in the handles=) this the document, in Chapter 9, describes in detail the USB Device Framework. This Chapter you need to read carefully and in accordance with it to describe the descriptors of the device. In my case, were as follows:
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))
{
usb_hid_descriptor struct 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, not much versed with the structure of the report, believed the document) as a whole, here are a couple of points that you need to pay special attention to:
the
usb_config_descriptor: bNumInterfaces should reflect as many interfaces as actually implemented. In our case, two: HID and MSD
usb_interface_descriptor: bInterfaceNumber denotes the interface number, but the count starts with zero, therefore, the number of the first interface — 0.
Here, from a descriptive point of view, perhaps, and all. I can not mention how well the library is described descriptors (their description is in the file usbstd.h). All clear on the documentation. I believe it has significantly simplified my task, since questions were not "How can I describe composite device?". All was immediately clear.
To work with the composite device class was written USB_composite 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));
usb_send_packet void(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
-
the
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, functions control_request and set_config must be explicitly specified for each device. However, this rule has an exception: Mass Storage Device. So, let's deal with the class constructor USB_Composite.
First, we initialize feet USB OTG FS:
the
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);
Second, we need to initialize our composite device, register USB_set_config_callback, which was discussed above, and to allow an interrupt:
the
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 ensure that in device Manager our device to be recognized:
the
-
the
- In the tab "USB Controllers": as a composite device the
- In the same tab as "USB mass Storage device", the
- In the tab "Keyboard" as "HID Keyboard". However, the "USB mass Storage device" will be marked with a warning that the device is working properly. The thing is that unlike other USB devices, Mass Storage is initialized a little differently, through the function usb_msc_init described in the file usb_msc.c library libopencm3. I mentioned above that MSD do not need to explicitly describe the functions of control_request and set_config. This is because the function usb_msc_init will do everything for us and end points set up, and all colbecki register. Thus, we need to add another constructor with one line:
the
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 the MSD, we need to give him minimal API for working with memory
the
-
the
- block_count: the number of sectors of memory the
- read_block: function read-sector the
- write_block: function to record the sector.
In Pesticide we use external flash SST25VF064C. The driver for this chip can be viewed here. Further, on the basis of this driver, the flash will be implemented as a file system. Most likely, this some write in detail, my colleague. But since I wanted to test the MSD, I wrote the germ of the file system=) Over it you can cry here.
So. Now that the class constructor USB_Composite finished, you can build the project to flash the device and see that "USB mass Storage device" is no longer marked with a warning, and in the tab "Disk drives" can be detected "ThirdPin Pastilda USB Device". And, it would seem that all is well. But no=) the Problem was more:
1. Go to drive. When you try to do this all hangs dying, the computer is very bad.
2. Detection device as a disk takes more than 2 minutes.
About these problems and how to solve them without harm for health it is written here: USB mass storage device and libopencm3.
And, Oh, miracle! No stains=) Now everything works. We have a USB host and a USB composite device. It remains only to combine their work.
the Association of the host and the composite device
Our goal:
the
-
the
- Broadcast messages from the keyboard to the PC as long as you have not pressed Ctrl + Shift + ~. the
- After pressing Ctrl + Shift + ~, Pastilla needs to take control and to send a message to PC like the keyboard, after which we return to the translation mode and again the expected combination.
The code that implements all this, 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:
-
the
- a Led to blink; the
- Flash to files on disk, create / delete; the
- Host, handing him at the same function redirect (what to do if there is no combination) and control_interception (what to do if the combination is pressed); the
- Composite device, passing it the function of read / write memory; And that's all. It's a start, the skeleton of our device is created. Very soon will be finalized the file system, by pressing Ctrl + Shift + ~, we get a one-line menu and in flash to store our encrypted database of passwords.
- Repository of the project Pastila moved here. Recently was published the 1.0 release. the
- Latest news on the project can be watch here. the
- And we finally launched project site!
I will be glad any comments and suggestions.
And, of course, a link to github.
UPD 27.06.2017:
-
the
Комментарии
Отправить комментарий