Два в одному: USB хост і складене USB пристрій

image

Не так давно, була опублікована стаття «Пастильда — відкритий апаратний менеджер паролів». Так як даний проект є відкритим, то ми вирішили, що буде цікаво, якщо ми будемо писати невеликі замітки про процесі проектування, завдання, які перед нами стоять і про труднощі, з якими ми стикаємося.

Основна суть Пастильды полягає в тому, що вона є своєрідним перехідником між клавіатурою і ПК. Таким чином, вона повинна вміти:
  • 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]).

Далі був реалізований простенький клас, для роботи з хостом:

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


Тут все прозоро. Пристрій має слухати клавіатуру і чекати набору спеціальної комбінації клавіш, для переходу в режим вибору логіна і пароля. Це відбувається в обробника переривання від клавіатури kbd_in_message_handler(uint8_t data_len, const uint8_t *data). Тут є два варіанти розвитку подій:
  • Якщо комбінації ні, то нам потрібно пропустити повідомлення від клавіатури далі в ПК. Для обробки цієї події, конструктор передана функція _redirect_callback.
  • Якщо комбінація натиснута, то нам потрібно інформувати систему про те, що ми перейшли в режим вибору логіна і пароля, отже, ми більше не транслюємо повідомлення від клавіатури ПК. Тепер сам пристрій є клавіатурою, а повідомлення від справжньої клавіатури тепер інтерпретуються як команди пристрою. Для обробки такого події, у конструктор передана функція _control_interception_callback.


Реалізація складеного USB пристрою

Далі мені треба було зробити так, щоб наше пристрій відображалося в диспетчері пристроїв, як клавіатура, і як дисковий накопичувач. Тут вся магія в дескрипторах=) В це документі, в розділі 9, докладно описаний USB Device Framework. Цю главу потрібно дуже уважно прочитати і відповідно до неї описати дескриптори пристрою. В моєму випадку вийшло наступне:

Composite USB is invalid

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, представлений нижче.

Composite USB Class
extern "С" 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 *is invalid;
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.
Нижче представлений варіант реалізації:

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 *)is invalid->keyboard_report_descriptor;
*len = sizeof(is invalid->keyboard_report_descriptor);
return (USBD_REQ_HANDLED);
}
else if (req->wValue == 0x2100)
{
*buf = (uint8_t *)&is invalid->keyboard_hid_function;
*len = sizeof(is invalid->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.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);
}


У конструкторі ми ініціалізуємо все, що необхідно:
  1. Світлодіоди, щоб моргали;
  2. Флеш, щоб можна було файли на диску створювати / видаляти;
  3. Хост, передавши йому при цьому функцію redirect (що робити, якщо комбінації немає) і control_interception (що робити, якщо комбінація натиснута);
  4. Складений пристрій, передавши йому функції читання / запису пам'яті;
І ось, власне, і все. Початок покладено, кістяк нашого пристрою створений. Зовсім скоро буде доопрацьована файлова система, за допомогою натискання комбінації клавіш Ctrl + Shift + ~, ми будемо потрапляти в однорядкове меню, а під флеш буде зберігатися наша зашифрована база даних паролів.

Буду рада будь-яких коментарів та побажань.

І, звичайно ж, посилання на github.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.