Protocol Analysis and Libusb Linux Driver for the Uni-T UT61B Multimeter
Nov, 2014
I possess the Uni-T UT61B digital multimeter which has the possibility to readout its sensor data via an optically coupled USB cable at a frequency of f ≈ 2Hz. As so often, such hardware is not officially supported by linux so I decided to write my own driver. Searching the internet, I found out that there are several versions of the UT61 series (models A-E), which differ slightly by their features and also in their readout protocol. The B model uses the Fortune Semiconductor FS9922-DMM3 chip, the C and D model the Fortune Semiconductor FS9922-DMM4 chip and the E model the Cyrustek ES51922 chip. All the following relates to the UT61B, which is the model I own. The code should probably work for the C and D model with the similar chip as well. The chip of the E model is slightly different, however, there exist linux drivers from other people (see References).
The multimeter encodes its data with the serial protocol RS-232. This serial output from the multimeter is optically coupled and packed into the USB protocol via the attached UTD04 data cable and the WCH CH9325 chip. This is shown schematically in the following figure:
For the serial RS-232 protocol, I found a datasheet on the vendor page. Yet, I was not able to find out how the WCH CH9325 chip packs the serial data frames into the USB protocol, so I had to reverse engineer the USB protocol. To write a driver, I did the following:
- Understanding of the RS-232 protocol of the FS9922-DMM3 chip using the datasheet.
- Reverse engineering the USB protocol of the WCH CH9325 chip.
- Making use of the libusb library to write the actual driver.
Understanding the RS-232 protocol
The datasheet with the RS-232 serial protocol specification of the FS9922-DMM3 chip is available at the vendor page. Each data frame consists of 14 bytes. In the following table, the purpose of each byte is summarized and explained:
Byte | Purpose and values | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | Sign, "+" (0x2b ) or "-" (0x2d ) |
||||||||||||||||||
1 | First digit, "0" (0x30 ) - "9" (0x39 ) or "?" (0x3f ) for overload |
||||||||||||||||||
2 | Second digit, "0" (0x30 ) - "9" (0x39 ) |
||||||||||||||||||
3 | Third digit, "0" (0x30 ) - "9" (0x39 ) |
||||||||||||||||||
4 | Fourth digit, "0" (0x30 ) - "9" (0x39 ) |
||||||||||||||||||
5 | Unused, space " " (0x20 ) |
||||||||||||||||||
6 | Decimal point position
|
||||||||||||||||||
7 | Status flags, bit mask
|
||||||||||||||||||
8 | Status flags, bit mask
|
||||||||||||||||||
9 | Status flags, bit mask
|
||||||||||||||||||
10 | Unit
|
||||||||||||||||||
11 | Bargraph
|
||||||||||||||||||
12 | End of data frame, CR (0x0d ) |
||||||||||||||||||
13 | End of data frame, LF (0x0a ) |
An example data frame could look like
2b 32 36 39 37 20 34 31 00 40 80 1a 0d 0a
which has the following meaning:
- 2b plus sign +
- 32 digit 2
- 36 digit 6
- 39 digit 9
- 37 digit 7
- 20 space
- 34 decimal point position 000.0
- 31 (00110001) flags AUTO, DC, BPN
- 00 (00000000) no flags
- 40 (01000000) flag milli
- 80 (10000000) flag Volt
- 1a (0 0011010) bit 0 = positive, binary 0011010 = 26
- 0d end byte
- 0a end byte
All in all this corresponds to
- a measured value +269,7 mV DC
- autorange enabled
- bargraph visible with a value of +26
Reverse engineering the serial-to-USB protocol mapping
The serial output of the multimeter is optically coupled and translated into the the USB protocol via the attached UTD04 data cable. There seems to be two different versions of this adapter available, an older one, which bases on the Hoitek HE2325U chip and a new one, which bases on the WCH CH9325 chip. I possess the later one, but unfortunately I couldn't find an official datasheet on the internet. In the following, I describe how I reverse engineered the USB protocol.
First, let us see which information we can obtain from the hardware itself. When the adapter is plugged in, a lsusb as root yields the following information for the device, which is identified by the vender:product id 1a86:e008.
$ lsusb -d 1a86:e008 -v Bus 006 Device 002: ID 1a86:e008 QinHeng Electronics HID-based serial adapater Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 1.00 bDeviceClass 0 bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 8 idVendor 0x1a86 QinHeng Electronics idProduct 0xe008 HID-based serial adapater bcdDevice 12.00 iManufacturer 1 WCH.CN iProduct 2 USB to Serial iSerial 0 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 41 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 4 (error) bmAttributes 0x80 (Bus Powered) MaxPower 100mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 2 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 bInterfaceProtocol 0 iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.00 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 37 Report Descriptors: ** UNAVAILABLE ** Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x82 EP 2 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 5 Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x02 EP 2 OUT bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 5 can't get debug descriptor: Resource temporarily unavailable Device Status: 0x0000 (Bus Powered)
One can see that the device shows up as a Human Interface Device with an IN and OUT endpoint. However, the OUT endpoint cannot be used as the adapter does not allow for sending of data to the multimeter. The information of the IN endpoint shows that data in the size of 8 bytes can be retrieved via an interrupt transfer.
Sniff the USB protocol
Now, lets sniff the protocol to understand how we can instruct the multimeter to send us the data and to see how the serial protocol of the FS9922-DMM3 chip in the table above is packet into the USB data frames. To do so, I read out the multimeter with the official program under windows in a virtual machine, while I use wireshark with the kernel module usbmon under the linux host to capture the traffic. Of course, I could also have used an USB sniffer directly on windows. To understand the following USB protocol, I found this short introduction to the USB protocol specification and the official Human interface device class specification quite helpful.
First, one has to install the wireshark package and load the kernel module via modprobe usbmon. With lsusb one can determine the bus where the multimeter is attached to. In the output of lsusb one can also see, whether different devices are attached to the same bus. If this is the case one can try different USB ports until one finds a port where no other device is attached. This has the advantage that the captured log file will not be floated with unnecessary packages from different devices. Otherwise one has to filter the log to show only messages from the desired device.
Then, one can open wireshark and choose the usbmon interface
corresponding to the respective USB bus and start capturing.
According to the upper output of lsusb
the bus in my case is Bus 006,
therefore I have to choose the usbmon6
interface.
In the virtual machine, I installed the
official program
and enabled the USB device of the serial adapter in the virtual machine.
If the program is opened,
the wireshark log shows the captured packages.
The log reveals the following information. In the beginning, the program sends an initial package via a control transfer to the device. Afterwards, the data are received via interrupt transfers. Let us study the two parts in more detail, starting with the control transfer package which initializes the transfer of data. A screenshot of the wireshark log is shown below:
The interesting part is the selected setup package, here with the hex value 21 09 00 03 00 00 05 00 and the data 60 09 00 00 03. The meaning of the individual parts according to the Human interface device class specification is the following:
Name | Value | Meaning | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
bm |
00100001 | Properties of the request
|
||||||||
bRequest | 0x09 | Request number, corresponds to SET_REPORT request according to HID class definition. This request sets some features of the device. | ||||||||
wValue | 00 03 | Report Type (03 = Feature) and Report ID (00 = not used) | ||||||||
wIndex | 0x0000 | Interface number | ||||||||
wLength | 0x05 | Length of the report data (= 5 bytes) | ||||||||
data | 60 09 00 00 03 | Data of the report request
|
The only purpose of this report request is quite simple: It sets the baudrate to 2400.
Afterwards the device starts sending the data via interrupt data packages about every 12 ms. The packages differ dependent on whether data is currently sent or not. An example of data packages is shown in the following image, where on the top a package with no data and on the bottom a package with data is shown:
If no data is available, the 8 byte package contains the data f0 00 00 00 00 00 00 00 (upper part of the image). Otherwise the 14 byte package of the previously discussed RS-232 specification is split into 14 interrupt data packages, encoded as f1 XX 00 00 00 00 00 00, where XX is one byte of the package.
Writing the linux driver
With the above analysis, the protocol is completely understood and a driver for linux can be written. To summarize the procedure, one has to implement the following:
- Check for an USB device with vendor:product id of 1a86:e008.
- Open the device for data transfer.
- Send a SET_REPORT request with a control transfer package containing data 60 09 00 00 03 to set the baudrate and start the data transfer.
- Wait for interrupt packages.
- Check arriving 8 byte large data of interrupt packages whether first byte is f1. If yes, start buffering second byte. There should be in total 14 consecutive packages, containing the data of the RS-232 data frame. The last two bytes of the 14 byte long sequence must be 0d 0a.
- Parse buffered data frame according to the RS-232 specification and save or further process data.
To access USB devices under linux, I used the C library libusb.
A simple implementation of the driver in c++ and a commandline program can be found at
GitHub.
My implementation consists of a WCH_CH9325
class to handle the access to the USB device with libusb
and a FS9922_DMM3
class for parsing the data frame.
The main program uses both classes to provide a commandline interface for starting a data capture.
The code itself is documented in detail.
Further references
- Product page of the UT61 series
- Official driver and software for the UT61 series
- Datasheet and RS232 protocol specification of the FS9922-DMM3 chip
- DMM.exe - A third party windows program
- ultradmm - A third party windows program
- he2325u - A linux driver for the UT61E
- ut61e_python - A linux driver for the UT61E
- Perl script to readout the UT61B via serial port
- USB Human interface device class specification
- Introduction into the USB protocol
- libusb API documentation
- sigrok library and the device page of the UT61B