Возникла задача сделать USB-устройство, которое, будучи вставленным в NAS, воспринималось бы им как USB-линк к источнику бесперебойного питания (именно через такое USB-соединение NAS узнает от ИБП об исчезновении питания, разрядке батарей и т.д.).
1. Внутри USB
Для решения задачи важно понимать, как USB устроен и работает. Очень короткое и доходчивое введение для знающих английский язык называется USB in a NutShell (upd: есть перевод). Затем советую по возможности пролистать книгу "USB Complete".
После этого, если потребуется, уже можно что-то уточнять в спецификациях, изучать классы, знакомиться с USB 3.0 SuperSpeed и т.д., но я уверен, что текста USB in a Nutshell и хороших примеров достаточно, чтобы сделать свое первое экспериментальное устройство.
2. USB-протокол ИБП/хост
В моем NAS операционная система основана на Linux и для общения с ИБП использует Network UPS Tools (NUT).
Выбирем в исходных текстах NUT самый простой драйвер; на всякий случай проверим, что он есть в списке ИБП, поддерживаемых NAS.
Самым простым и коротким показался drivers/richcomm_usb.c для устройств какого-то китайского производителя. Если сравнить его со скелетом, то становится ясно, что протокол у китайцев максимально примитивен: это «сухие контакты» без каких-либо подробностей; даже не HID-устройство. Но нас это вполне устраивает.
Рассмотрим основную функцию общения с ИБП:
#define STATUS_REQUESTTYPE 0x21
#define REPLY_REQUESTTYPE 0x81
#define QUERY_PACKETSIZE 4
#define REPLY_PACKETSIZE 6
static int execute_and_retrieve_query(char *query, char *reply)
{
. . .
usb_control_msg(udev, STATUS_REQUESTTYPE, REQUEST_VALUE, MESSAGE_VALUE,
INDEX_VALUE, query, QUERY_PACKETSIZE, 1000);
. . .
usb_interrupt_read(udev, REPLY_REQUESTTYPE, reply, REPLY_PACKETSIZE, 1000);
}
Видно, что при совершении запроса хост посылает устройству управляющий пакет плюс 4 байта, адресуя все это интерфейсу/классу (0x21; см. описание полей USB request). Устройство отвечает 6 байтами, которые отправляются в endpoint 1 (0x81; см. описание Endpoint Address).
Значения отсылаемых байт можно посмотреть в функции query_ups(), а смысл принятых байт — в функции upsdrv_updateinfo(). Если кратко, то мы отсылаем вместе с control message массив { 0x01, 0x00, 0x00, 0x30 }, а в принятом массиве смотрим на пару бит в нужном байте: они и сообщают статус питания (от сети/от батарей) и состояние батареи (заряжена/почти разряжена).
Отдельно отмечу: в качестве Vendor ID китайцы решили использовать 0x925 — число, которое они напрямую скопировали из примеров к упоминавшейся выше книге «USB Complete» Яна Аксельсона. Естественно, это плохое решение, потому что данный Vendor ID выдан Lakeview Research, компании Яна Аксельсона, и использовать его в своих проектах как минимум некорректно. Китайцы могли бы хотя бы почитать FAQ по проблеме USB VendorID/Product ID или послушать интересный доклад на эту же тему на Open Hardware Summit 2012.
Для того, чтобы наше устройство определилось драйверами Network UPS Tools, нам тоже придется использовать чужой Vendor ID/Product ID. Конкретно в данном случае (отсутствие массового производства, осознанная мимикрия и т.д.) ничего страшного в этом нет.
3. Железо
Итак, всей этой информации достаточно, чтобы начать программировать. В качестве платформы для реализации я решил попробовать плату MC HCK на микроконтроллере Freescale Kinetis K20: я заказывал несколько прототипов MC HCK в прошлом году. Мне понравилась идея недорогой ($5-7), но достаточно мощной платы для различных экспериментов, выполненная в удобном форм-факторе.
Кстати, нацеленная практически на эту же нишу, но гораздо более известная плата Teensy 3.1 использует аналогичный МК, но с большим объемом памяти.
Описание использованного контроллера можно найти здесь. Если кратко, то это очень недорогой ARM Cortex-M4 50Mhz с 32kb flash + 32kb data и различными прелестями, из которых нам наиболее актуальна аппаратная реализация USB. По минимуму для подключения процессора к USB требуется лишь несколько резисторов и конденсаторов.
4. Практическая реализация
Для разработки необходимо установить:
- Компилятор; я использовал GCC ARM embedded toolchain
- Сам SDK: MC HCK toolchain
- dfu-util для заливки прошивки
Сам SDK состоит из библиотек, облегчающих доступ к возможностям контроллера, бутлодера и примеров. Наверное, стоит сказать, что SDK еще не очень зрел (да и сам MC HCK пока в массы не вышел), но вполне может использоваться в различных проектах (например, внутри сенсоров окружающей среды с низким энергопотреблением). Библиотека работы с USB отличается практически полным отсутствием документации, но код чист и понятен, а существующих примеров достаточно.
Вспомним иерархию внутри любого USB-устройства:
Учитывая простоту нашего USB-протокола, основной объем исходного кода занимают дескрипторы USB-устройства, одной конфигурации, одного интерфейса и одного endpoint'a («одного» — потому что endpoint для управляющих пакетов создается по умолчанию и не зависит от нас). Названия полей выступают в роли комментариев.
static const struct usb_desc_dev_t device_dev_desc = {
.bLength = sizeof(struct usb_desc_dev_t),
.bDescriptorType = USB_DESC_DEV,
.bcdUSB = { .maj = 2 },
.bDeviceClass = USB_DEV_CLASS_SEE_IFACE,
.bDeviceSubClass = USB_DEV_SUBCLASS_SEE_IFACE,
.bDeviceProtocol = USB_DEV_PROTO_SEE_IFACE,
.bMaxPacketSize0 = EP0_BUFSIZE,
.idVendor = RCM_VENDOR,
.idProduct = RCM_PRODUCT,
.bcdDevice = { .sub = 1 },
.iManufacturer = 1,
.iProduct = 2,
.iSerialNumber = 3,
.bNumConfigurations = 1,
}
static const struct usb_config_1 usb_config_1 = {
.config = {
.bLength = sizeof(struct usb_desc_config_t),
.bDescriptorType = USB_DESC_CONFIG,
.wTotalLength = sizeof(struct usb_config_1),
.bNumInterfaces = 1,
.bConfigurationValue = 1,
.iConfiguration = 0,
.one = 1,
.bMaxPower = 10
},
.usb_function_0 = {
.iface = {
.bLength = sizeof(struct usb_desc_iface_t),
.bDescriptorType = USB_DESC_IFACE,
.bInterfaceNumber = 0,
.bAlternateSetting = 0,
.bNumEndpoints = 1,
.iInterface = 0,
.bInterfaceClass = RCM_CLASS,
.bInterfaceSubClass = RCM_SUBCLASS,
.bInterfaceProtocol = RCM_PROTOCOL,
.iInterface = 0
},
.int_in_ep = {
.bLength = sizeof(struct usb_desc_ep_t),
.bDescriptorType = USB_DESC_EP,
.bEndpointAddress = UPS_REPLY_EP,
.type = USB_EP_INTR,
.wMaxPacketSize = UPS_REPLY_EP_SIZE,
.bInterval = 0xFF
}
},
};
В коллбэк-функции, которая вызывается при успешной инициализации, мы зададим коллбэк rcm_handle_control() для обработки управляющих запросов и структуру tx_pipe для отсылки ответов:
static const struct usbd_function usbd_function = {
.control = rcm_handle_control,
.interface_count = 1
};
usb_attach_function(&usbd_function, &usbd_ctx);
tx_pipe = usb_init_ep(&usbd_ctx, 1, USB_EP_TX, UPS_REPLY_EP_SIZE);
Стандартные USB-запросы вроде Get Descriptor или Set Configuration возьмет на себя SDK и нам останется отработать лишь конкретный запрос:
static int rcm_handle_control(struct usb_ctrl_req_t *req, void *data)
{
static unsigned char buf[UPS_REQUESTSIZE];
if (req->recp == USB_CTRL_REQ_IFACE &&
req->type == USB_CTRL_REQ_CLASS &&
req->bRequest == UPS_REQUESTVALUE &&
req->wValue == UPS_MESSAGEVALUE &&
req->wIndex == UPS_INDEXVALUE &&
req->wLength == UPS_REQUESTSIZE)
{
usb_ep0_rx(buf, req->wLength, rcm_handle_data, NULL);
return (1);
}
return 0;
}
Видно, что при совпадении всех полей SETUP-пакета, мы собираемся прочитать от хоста оставшиеся данные и устанавливаем для этого коллбэк rcm_handle_data(). Сам коллбэк мигает светодиодом и отсылает хосту в endpoint 1 текущий статус питания и заряда батареи:
static void rcm_handle_data(void *buf, ssize_t len, void *data)
{
// Demonstration
static int counter = 0;
switch (counter++) {
case 0: ups_online(1); ups_batterystatus(1); break;
case 30: ups_online(0); break;
case 40: ups_online(1); break;
case 50: ups_online(0); break;
case 60: ups_batterystatus(0); break;
}
onboard_led(ONBOARD_LED_TOGGLE);
// Send ACK for this request
usb_handle_control_status(0);
usb_tx(tx_pipe, ups_reply, UPS_REPLYSIZE, UPS_REPLY_EP_SIZE, NULL, NULL);
}
В общем-то, это всё…
void main()
{
usb_init(&rcm_device);
// Wait for interrupts
sys_yield_for_frogs();
}
5. Проверка в реальной жизни
После сборки проекта командой make, необходимо нажать на MC HCK кнопку RESET, переводящую его на некоторое время в режим программирования, и набрать make flash для его прошивки с помощью dfu-util. Теперь плату можно вставлять в различные компьютеры и смотреть, как они на нее реагируют.
Проверим, как определяется наше устройство NAS'ом:
Подробности покажет USB-Prober для OS X:
Если немного подождать, то в логах Synology DSM можно увидеть, что наше устройство работает корректно:
info 2014/08/09 16:23:12 SYSTEM: Local UPS was plugged in.
info 2014/08/09 16:23:13 SYSTEM: The UPS was connected.
warning 2014/08/09 16:25:14 SYSTEM: Server is on battery.
info 2014/08/09 16:26:04 SYSTEM: Server back online.
warning 2014/08/09 16:26:54 SYSTEM: Server is on battery.
warning 2014/08/09 16:27:45 SYSTEM: Server going to Safe Shutdown.
6. Заключение
Казалось бы, пора подключать к пинам MC HCK что-то реальное — то есть то, для чего устройство предназначалось… но к этому моменту я уже потерял интерес.
Откровенно говоря, я просто хотел сэкономить на ИБП для NAS, взяв что-нибудь за максимально смешную сумму и добавив к нему USB-линк самостоятельно. Судя по отзывам о таких ИБП, их качестве и их батареях, это была дурацкая идея. Так что я купил недорогой APC, вмешиваться в работу которого не потребовалось: он и так поддерживает USB HID power device class.
Тем не менее, этот пример может оказаться кому-то полезен хотя бы для того, чтобы понять, как всё несложно. Полный исходный текст выложен на GitHub.
Автор: vk2
какой report Descriptor использован в примере?