Lukas Schwarz

Protocol Analysis and Libusb Linux Driver for the Uni-T UT61B Multimeter

GitHub GitHub

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:

  1. Understanding of the RS-232 protocol of the FS9922-DMM3 chip using the datasheet.
  2. Reverse engineering the USB protocol of the WCH CH9325 chip.
  3. 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
Value Purpose
0x30 no decimal point (0000)
0x31 decimal point at first position (0.000)
0x32 decimal point at second position (00.00)
0x34 decimal point at third position (000.0)
7 Status flags, bit mask
Bit Purpose
0 unused
1 unused
2 autorange (AUTO)
3 DC mode
4 AC mode
5 REL button pressed
6 HOLD button pressed
7 bargraph visible (BPN)
8 Status flags, bit mask
Bit Purpose
0 user defined symbol (Z1)
1 user defined symbol (Z2)
2 MAX button pressed
3 MIN button pressed
4 auto power off (APO)
5 low battery (BAT)
6 SI-prefix nano (n)
7 user defined symbol (Z3)
9 Status flags, bit mask
Bit Purpose
0 SI-prefix micro (µ)
1 SI-prefix milli (m)
2 SI-prefix kilo (k)
3 SI-prefix Mega (M)
4 beep
5 diode
6 percent (%)
7 user defined symbol (Z4)
10 Unit
Bit Purpose
0 Volt (V)
1 Ampere (A)
2 Ohm (Ω)
3 hFE
4 Hertz (Hz)
5 Farad (F)
6 degree Celcius (°C)
7 degree Fahrenheit (°F)
11 Bargraph
Bit Purpose
0 positive (0), negative (1)
1-7 value of the bargraph as 7bit unsigned integer
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
bmRequestType 00100001 Properties of the request
Bit Meaning
0 Direction is host to device
1-2 Type of request is class specific. Bit 3-7 say the request recipent is an interface. According to the output of lsusb the interface class is HID. This means the request is a standard request defined for the HID device class.
3-7 recipent of request is interface
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
Data Meaning
60 09 baudrate = 2400
00 00 03 unkown

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:

  1. Check for an USB device with vendor:product id of 1a86:e008.
  2. Open the device for data transfer.
  3. 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.
  4. Wait for interrupt packages.
  5. 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.
  6. 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