- PVSM.RU - https://www.pvsm.ru -
Возникла задача сделать USB-устройство, которое, будучи вставленным в NAS, воспринималось бы им как USB-линк к источнику бесперебойного питания (именно через такое USB-соединение NAS узнает от ИБП об исчезновении питания, разрядке батарей и т.д.).
Для решения задачи важно понимать, как USB устроен и работает. Очень короткое и доходчивое введение для знающих английский язык называется USB in a NutShell [1] (upd: есть перевод [2]). Затем советую по возможности пролистать книгу "USB Complete [3]".
После этого, если потребуется, уже можно что-то уточнять в спецификациях [4], изучать классы [5], знакомиться с USB 3.0 SuperSpeed и т.д., но я уверен, что текста USB in a Nutshell и хороших примеров достаточно, чтобы сделать свое первое экспериментальное устройство.
В моем NAS операционная система [6] основана на Linux и для общения с ИБП использует Network UPS Tools [7] (NUT).
Выбирем в исходных текстах NUT [8] самый простой драйвер; на всякий случай проверим, что он есть в списке [9] ИБП, поддерживаемых NAS.
Самым простым и коротким показался drivers/richcomm_usb.c [10] для устройств какого-то китайского производителя. Если сравнить его со скелетом [11], то становится ясно, что протокол у китайцев максимально примитивен: это «сухие контакты» без каких-либо подробностей; даже не 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 [12]). Устройство отвечает 6 байтами, которые отправляются в endpoint 1 (0x81; см. описание Endpoint Address [13]).
Значения отсылаемых байт можно посмотреть в функции query_ups(), а смысл принятых байт — в функции upsdrv_updateinfo(). Если кратко, то мы отсылаем вместе с control message массив { 0x01, 0x00, 0x00, 0x30 }, а в принятом массиве смотрим на пару бит в нужном байте: они и сообщают статус питания (от сети/от батарей) и состояние батареи (заряжена/почти разряжена).
Отдельно отмечу: в качестве Vendor ID китайцы решили использовать 0x925 — число, которое они напрямую скопировали из примеров к упоминавшейся выше книге «USB Complete» Яна Аксельсона. Естественно, это плохое решение, потому что данный Vendor ID выдан Lakeview Research, компании Яна Аксельсона, и использовать его в своих проектах как минимум некорректно. Китайцы могли бы хотя бы почитать FAQ [14] по проблеме USB VendorID/Product ID или послушать интересный доклад на эту же тему [15] на Open Hardware [16] Summit 2012.
Для того, чтобы наше устройство определилось драйверами Network UPS Tools, нам тоже придется использовать чужой Vendor ID/Product ID. Конкретно в данном случае (отсутствие массового производства, осознанная мимикрия и т.д.) ничего страшного в этом нет.
Итак, всей этой информации достаточно, чтобы начать программировать. В качестве платформы для реализации я решил попробовать плату MC HCK [17] на микроконтроллере Freescale Kinetis K20: я заказывал несколько прототипов MC HCK в прошлом году. Мне понравилась идея недорогой ($5-7), но достаточно мощной платы для различных экспериментов, выполненная в удобном форм-факторе.
Кстати, нацеленная практически на эту же нишу, но гораздо более известная плата Teensy 3.1 [18] использует аналогичный МК, но с большим объемом памяти.
Описание использованного контроллера можно найти здесь [19]. Если кратко, то это очень недорогой ARM Cortex-M4 50Mhz с 32kb flash + 32kb data и различными прелестями, из которых нам наиболее актуальна аппаратная реализация USB. По минимуму для подключения процессора к USB требуется лишь несколько резисторов и конденсаторов.
Для разработки необходимо установить:
Сам SDK состоит из библиотек, облегчающих доступ к возможностям контроллера, бутлодера и примеров. Наверное, стоит сказать, что SDK еще не очень зрел (да и сам MC HCK пока в массы не вышел), но вполне может использоваться в различных проектах (например, внутри сенсоров окружающей среды [23] с низким энергопотреблением). Библиотека работы с 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 [24] и нам останется отработать лишь конкретный запрос:
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();
}
После сборки проекта командой 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.
Казалось бы, пора подключать к пинам MC HCK что-то реальное — то есть то, для чего устройство предназначалось… но к этому моменту я уже потерял интерес.
Откровенно говоря, я просто хотел сэкономить на ИБП для NAS, взяв что-нибудь за максимально смешную сумму и добавив к нему USB-линк самостоятельно. Судя по отзывам о таких ИБП, их качестве и их батареях, это была дурацкая идея. Так что я купил недорогой APC, вмешиваться в работу которого не потребовалось: он и так поддерживает USB HID power device class.
Тем не менее, этот пример может оказаться кому-то полезен хотя бы для того, чтобы понять, как всё несложно. Полный исходный текст выложен на GitHub [25].
Автор: vk2
Источник [26]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/diy/67535
Ссылки в тексте:
[1] USB in a NutShell: http://www.beyondlogic.org/usbnutshell/usb1.shtml
[2] перевод: http://microsin.ru/content/view/1107/44/
[3] USB Complete: http://www.amazon.com/USB-Complete-Fourth-Edition-Developers/dp/1931448086
[4] спецификациях: http://www.usb.org/developers/docs/usb20_docs/
[5] классы: http://www.usb.org/developers/docs/devclass_docs/
[6] операционная система: http://www.synology.com/en-global/dsm/
[7] Network UPS Tools: http://www.networkupstools.org
[8] исходных текстах NUT: https://github.com/networkupstools/nut/
[9] списке: http://www.synology.com/en-global/support/faq/300
[10] drivers/richcomm_usb.c: https://github.com/networkupstools/nut/blob/master/drivers/richcomm_usb.c
[11] скелетом: https://github.com/networkupstools/nut/blob/master/drivers/skel.c
[12] USB request: http://wiki.osdev.org/Universal_Serial_Bus#USB_Device_Requests
[13] Endpoint Address: http://wiki.osdev.org/Universal_Serial_Bus#ENDPOINT
[14] FAQ: http://www.oshwa.org/2013/11/19/new-faq-on-usb-vendor-id-and-product-id/
[15] интересный доклад на эту же тему: http://vimeo.com/63986266
[16] Open Hardware: http://oshwa.org
[17] MC HCK: https://mchck.org/about/
[18] Teensy 3.1: https://www.pjrc.com/teensy/index.html
[19] здесь: https://github.com/mchck/mchck/wiki/MCU-Features---MK20DX32VLF5
[20] GCC ARM embedded toolchain: https://launchpad.net/gcc-arm-embedded/+download
[21] MC HCK toolchain: http://github.com/mchck/mchck
[22] dfu-util: https://gitorious.org/dfu-util
[23] сенсоров окружающей среды: http://publiclab.org/wiki/open-water
[24] SDK: https://github.com/mchck/mchck/blob/master/toolchain/lib/usb/usb.c
[25] GitHub: http://github.com/kolontsov/richcomm-ups-mchck/
[26] Источник: http://habrahabr.ru/post/233391/
Нажмите здесь для печати.