Implementation of the USB interface of the UPS based on the MC HCK ARM-board
- Tutorial
1. Inside USB
To solve the problem, it is important to understand how USB is arranged and works. A very short and intelligible introduction for those who know English is called USB in a NutShell (upd: there is a translation ). Then I advise you to scroll through the book " USB Complete " whenever possible .
After that, if necessary, you can already clarify something in the specifications , study classes , get acquainted with USB 3.0 SuperSpeed, etc., but I'm sure that USB text in a Nutshell and good examples are enough to make your first experimental device .
2. USB protocol of UPS / host
In my NAS, the operating system is based on Linux and uses Network UPS Tools (NUT) to communicate with the UPS .
Choose the simplest driver in the NUT source code ; just in case, check that it is on the list of UPSs supported by NAS.
The simplest and shortest seemed drivers / richcomm_usb.c for devices of some Chinese manufacturer. If you compare it with the skeleton , it becomes clear that the Chinese protocol is as primitive as possible: these are “dry contacts” without any details; not even a HID device. But we are quite happy with this.
Consider the main function of communication with the UPS:
#define STATUS_REQUESTTYPE 0x21#define REPLY_REQUESTTYPE 0x81#define QUERY_PACKETSIZE 4#define REPLY_PACKETSIZE 6staticintexecute_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);
}
It is seen that when making a request, the host sends a control packet plus 4 bytes to the device, addressing all this to the interface / class (0x21; see the description of the USB request fields ). The device responds with 6 bytes that are sent to endpoint 1 (0x81; see the description of Endpoint Address ).
Sending byte values can be found in the function query_ups () , and the meaning of bytes - in function upsdrv_updateinfo () . In short, we send an array {0x01, 0x00, 0x00, 0x30} together with the control message, and in the received array we look at a couple of bits in the desired byte: they report the power status (from mains / battery) and battery status (charged / almost discharged).
Separately, I note: as the Vendor ID, the Chinese decided to use 0x925 - the number that they directly copied from the examples to the above-mentioned book "USB Complete" by Jan Axelson. Naturally, this is a bad decision, because this Vendor ID was issued by Lakeview Research, a company of Jan Axelson, and at least it is incorrect to use it in your projects. The Chinese could at least read the FAQ on USB VendorID / Product ID or listen to an interesting report on the same topic at the Open Hardware Summit 2012.
In order for our device to be determined by Network UPS Tools drivers, we also have to use someone else's Vendor ID / Product ID. Specifically, in this case (lack of mass production, deliberate mimicry, etc.) there is nothing wrong with that.
3. Iron
So, all this information is enough to start programming. As a platform for implementation, I decided to try the MC HCK board on the Freescale Kinetis K20 microcontroller: I ordered several MC HCK prototypes last year. I liked the idea of an inexpensive ($ 5-7), but powerful enough board for various experiments, made in a convenient form factor.

By the way, aimed at almost the same niche, but the much more famous Teensy 3.1 board uses a similar MK, but with a large amount of memory.
Description of the used controller can be found here.. In short, this is a very inexpensive ARM Cortex-M4 50Mhz with 32kb flash + 32kb data and various charms, of which the USB hardware implementation is most relevant to us. At a minimum, only a few resistors and capacitors are required to connect the processor to USB.
4. Practical implementation
For development, you must install:
- Compiler; I used the GCC ARM embedded toolchain
- SDK itself: MC HCK toolchain
- dfu-util for uploading firmware
The SDK itself consists of libraries that facilitate access to the capabilities of the controller, bootloader and examples. Perhaps it’s worth saying that the SDK is not very mature yet (and the HCK MC itself hasn’t reached the masses yet), but it can be used in various projects (for example, inside low-power environment sensors ). The USB library is characterized by an almost complete lack of documentation, but the code is clean and clear, and there are enough existing examples.
Recall the hierarchy inside any USB device:

Given the simplicity of our USB protocol, the bulk of the source code is occupied by the descriptors of the USB device, one configuration, one interface and one endpoint (“one” - because endpoint for control packages is created by default and does not depend on us). Field names act as comments.
staticconststructusb_desc_dev_tdevice_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,
}
staticconst 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
}
},
};
In the callback function, which is called upon successful initialization, we define the rcm_handle_control () callback for processing control requests and the tx_pipe structure for sending responses:
staticconststructusbd_functionusbd_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);
Standard USB-requests like Get Descriptor or Set Configuration will take over the SDK and we will only have to work out a specific request:
staticintrcm_handle_control(struct usb_ctrl_req_t *req, void *data){
staticunsignedchar 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);
}
return0;
}
It can be seen that if all the fields of the SETUP package coincide, we are going to read the remaining data from the host and set the rcm_handle_data () callback for this . The callback itself blinks an LED and sends the current power and battery status to the endpoint 1 host:
staticvoidrcm_handle_data(void *buf, ssize_t len, void *data){
// Demonstrationstaticint counter = 0;
switch (counter++) {
case0: ups_online(1); ups_batterystatus(1); break;
case30: ups_online(0); break;
case40: ups_online(1); break;
case50: ups_online(0); break;
case60: 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);
}In general, that's all ...
voidmain(){
usb_init(&rcm_device);
// Wait for interrupts
sys_yield_for_frogs();
}
5. Real-life verification
After building the project with the make command, you need to press the RESET button on the MC HCK, which puts it into programming mode for a while, and type make flash to flash it using dfu-util. Now the board can be inserted into various computers and watch how they react to it.
Let's check how our device is detected by NAS: The

USB-Prober for OS X will show details:

If you wait a bit, you can see in the Synology DSM logs that our device is working correctly:
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. Conclusion
It would seem that it's time to connect something real to the MC HCK pins - that is, what the device was intended for ... but by this moment I had already lost interest.
Frankly, I just wanted to save on UPS for NAS, taking something for the most ridiculous amount and adding a USB link to it myself. Judging by the reviews of such UPSs, their quality and their batteries, it was a stupid idea. So I bought an inexpensive APC, which I didn’t need to interfere with: it already supports USB HID power device class.
Nevertheless, this example may be useful to someone, if only to understand how simple it is. The full source code is posted on GitHub .