- PVSM.RU - https://www.pvsm.ru -

Реализация USB-интерфейса ИБП на основе ARM-платы MC HCK

Возникла задача сделать USB-устройство, которое, будучи вставленным в NAS, воспринималось бы им как USB-линк к источнику бесперебойного питания (именно через такое USB-соединение NAS узнает от ИБП об исчезновении питания, разрядке батарей и т.д.).

1. Внутри USB

Для решения задачи важно понимать, как USB устроен и работает. Очень короткое и доходчивое введение для знающих английский язык называется USB in a NutShell [1] (upd: есть перевод [2]). Затем советую по возможности пролистать книгу "USB Complete [3]".

После этого, если потребуется, уже можно что-то уточнять в спецификациях [4], изучать классы [5], знакомиться с USB 3.0 SuperSpeed и т.д., но я уверен, что текста USB in a Nutshell и хороших примеров достаточно, чтобы сделать свое первое экспериментальное устройство.

2. USB-протокол ИБП/хост

В моем 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. Конкретно в данном случае (отсутствие массового производства, осознанная мимикрия и т.д.) ничего страшного в этом нет.

3. Железо

Итак, всей этой информации достаточно, чтобы начать программировать. В качестве платформы для реализации я решил попробовать плату MC HCK [17] на микроконтроллере Freescale Kinetis K20: я заказывал несколько прототипов MC HCK в прошлом году. Мне понравилась идея недорогой ($5-7), но достаточно мощной платы для различных экспериментов, выполненная в удобном форм-факторе.

Реализация USB интерфейса ИБП на основе ARM платы MC HCK

Кстати, нацеленная практически на эту же нишу, но гораздо более известная плата Teensy 3.1 [18] использует аналогичный МК, но с большим объемом памяти.

Описание использованного контроллера можно найти здесь [19]. Если кратко, то это очень недорогой ARM Cortex-M4 50Mhz с 32kb flash + 32kb data и различными прелестями, из которых нам наиболее актуальна аппаратная реализация USB. По минимуму для подключения процессора к USB требуется лишь несколько резисторов и конденсаторов.

4. Практическая реализация

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

  1. Компилятор; я использовал GCC ARM embedded toolchain [20]
  2. Сам SDK: MC HCK toolchain [21]
  3. dfu-util [22] для заливки прошивки

Сам SDK состоит из библиотек, облегчающих доступ к возможностям контроллера, бутлодера и примеров. Наверное, стоит сказать, что SDK еще не очень зрел (да и сам MC HCK пока в массы не вышел), но вполне может использоваться в различных проектах (например, внутри сенсоров окружающей среды [23] с низким энергопотреблением). Библиотека работы с USB отличается практически полным отсутствием документации, но код чист и понятен, а существующих примеров достаточно.

Вспомним иерархию внутри любого USB-устройства:

Реализация USB интерфейса ИБП на основе ARM платы MC HCK

Учитывая простоту нашего 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();
}

5. Проверка в реальной жизни

После сборки проекта командой make, необходимо нажать на MC HCK кнопку RESET, переводящую его на некоторое время в режим программирования, и набрать make flash для его прошивки с помощью dfu-util. Теперь плату можно вставлять в различные компьютеры и смотреть, как они на нее реагируют.

Проверим, как определяется наше устройство NAS'ом:

Реализация USB интерфейса ИБП на основе ARM платы MC HCK

Подробности покажет USB-Prober для OS X:

Реализация USB интерфейса ИБП на основе ARM платы MC HCK

Если немного подождать, то в логах 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 [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/