A software level introduction to the HART protocol

HART is a widely used industrial grade communication protocol. One or many field devices can be queried or configured remotely via the use of HART commands. The goal of this writing is to provide a simple introduction to the software level of the protocol by short example codes.

While HART claims to be an open protocol, I found the concrete specifications really hard to acquire, since they aren't public and are locked behind paywalls. With the few documentations available somewhere on the internet, one can set up a working communication channel, but this isn't an easy task. This article covers

Also this article is not a documentation nor it aims to be one. The most important concepts are covered alongside with tips or warnings about my experience with these commands.

Setting up with command zero

Assuming you have your field device wired in properly, and you have connected your HART modem to both the field device and your PC (USB-serial or serial; it doesn't matter), we are ready to write the first program. Our language of choice will be python, due to its simplicity, and relatively clean syntax, but you can use whatever language you prefer, as long as you can set up serial communication within it.

With python, opening a serial port can be done in one line. Also the keyword arguments present nicely how the serial port should be set up: baudrate, parity, etc.

import serial

s = serial.Serial("/dev/ttyUSB0",
                    baudrate=1200,
                    bytesize=serial.EIGHTBITS,
                    parity=serial.PARITY_ODD,
                    stopbits=serial.STOPBITS_ONE,
                    timeout=2)

We are ready to send our first command; command zero. I should mention now, that we are going to use the hexadecimal notation, so unless stated otherwise, a value of 67 means the hexadecimal value of 0x67.

s.write(bytes.fromhex("FFFFFFFFFFFFFFFFFFFF0280000082"))

Most likely it was unclear what this command exactly does, so let's break it down into parts:

FF FF FF FF FF FF FF FF FF FF 02 80 00 00 82

After a request we await for the response. We usually don't know the response size beforehand, so the easiest way is to allocate a buffer large enough to store any answer.

response = s.read(128)
print(response.hex())

FF FF FF FF FF 06 80 00 0E 00 80 FE 26 3B 06 05 02 01 20 00 2A BC 31 6C

Breaking it down:

Read dynamic variables with command 3

We are now going to take a look at long addressing mode and dealing with floating point numbers.

s.write(bytes.fromhex("FFFFFFFFFFFF82A63B2ABC310300BB"))

Again breaking down to parts:

FF FF FF FF FF FF 82 A6 3B 2A BC 31 03 00 6C

And the response:

response = s.read(128)
print(response.hex())

FF FF FF FF FF 86 A6 3B 2A BC 31 03 1A 00 80 41 AE 00 00

20 46 1C 3F F6 24 7F A0 00 00 24 7F A0 00 00 24 7F A0 00 00 82

The line break has no special meaning, it is only there to stop the text from overflowing. Also to reduce visual clutter, only the data bytes are highlighted now.

Read message with command 12 (0x0C)

As now almost every data type is covered except the packed strings. As a final example we are going to read the message written into our field device.

s.write(bytes.fromhex("FFFFFFFFFFFF82A63B2ABC310C00B4"))

The command structure shouldn't require any explaination now.

FF FF FF FF FF FF 82 A6 3B 2A BC 31 0C 00 B4

And the response is:

response = s.read(128)
print(response.hex())

FF FF FF FF FF 86 A6 3B 2A BC 31 0C 1A 00 80

64 54 E0 25 48 17 3D 22 D3 82 08 20 82 08 20 82 08 20 82 08 20 82 08 20 63

All the returned data is one 32 characters long string. Each character is stored on 6 bits using some base64-like encoding, so the whole byte sequence is 24 bytes long. That also means that we can decode this stream 3 bytes at a time. Take a look at the first bunch:

The full string says: "YES IT WORKS" followed by a buch of padding spaces. The encoding of the 6 bit characters is based on the ASCII table, but with some tweaks:

Endnote

There are three main command groups defined in HART. Universal commands must be implemented in each device, common practice commands should be implemented, and device specific commands are specific to manufacturer and device type as the name suggests. Our three example commands were universal commands, but common practice ones work in a similar manner. Device specific command on the other hand usually require DD libraries, where request and response commands with their data types are defined. Sadly the format of these libraries are even more obscure than the regular HART command formats, so I have no information about them.