Programming the Hart Communication Protocol at a PC Com Port

A C Language Software Example Presented by Borst Automation

Back to Borst Automation

Sometimes I am being asked: "How can I do a Hart Protocol implementation myself?". Usually the my answer is: "Use the tools of Borst Automation.". Just kidding. Sometimes it is the case that students would like to use Hart and they don't have the budget to effort our tools. For all these people I provided the example which is described here. The example, which is written in Visual Studio 6.0 can be downloaded from the following url: http://www.hart-profibus.com/HartCexample/ReadTagName.zip. It is free of charge!

If you are interested into the physics and low level basics of the Hart Communication Protocol I recommend that you visit http://www.analogservices.com/about_part1.htm. It is a very nice, professional and informative page which contains more than you will need.

However this example is only a very rudimentary small implementation which does not solve the following problems:

  • Addressing by Tag Name

  • Burst Mode

  • Multi Master

  • Windows Drivers with Defects

  • Multiplexer Support

  • Monitor Interface

  • Encoding and Decoding

If you have problems, need further information or have proposals please send email to info@hart-profibus.com.

Example Program Main

The example is based on a very simple console program, which is reading the unique identifier of the hart device and then is reading the tag name.

   /****/
int main(int argc, char* argv[])
/****/
  printf("Opening "); printf(&m_acComPort[0]); printf("\n");
  if(openComPort(&m_acComPort[0]))
  { /* Connect to device with address 0 */
    if(connectToDevice(0))
    {
      printf("Connected to a device with Mfr Id: %d", m_ucRxBuffer[7]);
      printf(" and Dev Id: %d\n", m_ucRxBuffer[8]);
      printf("Reading tag name...\n");
      if(readTagName())
      {
        printf("The tag name of the device is ");
        printf((const char *)&m_ucTagName[0]);
        printf(".\n");
      }
      else
      {
        printf("Failed to read tag name of the device!\n");
      }
    }
    else
    {
      printf("Connection failed! No device response.\n");
    }
  }
  else
  {
    printf("Could not open com port!\n");
  }
  printf("Press return to exit!\n");
  getchar();
  if(m_hComFile != INVALID_HANDLE_VALUE)
  {
    closeComPort(m_hComFile);
  }
  return 0;
}

Opening a Com Port

The CreateFile function of the Windows API is used to open a com port:

           /***********/
static bool openComPort(const char * pcComPort)
{          /***********/
  COMMTIMEOUTS strCommTimeOuts;
  DCB                      dcb;

  m_hComFile =
  CreateFile(&m_acComPort[0],
             GENERIC_READ | GENERIC_WRITE,
             0, //exclusive access
             NULL, //no security attribs
             OPEN_EXISTING,
             FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
             NULL
            );
  if(m_hComFile == INVALID_HANDLE_VALUE)
  {
    return FALSE;
  }

The com port name is passed as the first parameter to the function:

static const char m_acComPort[] = { 'C','O','M','1',0x00 };

The next API call is done to clear all buffers:

PurgeComm(m_hComFile,
          PURGE_TXABORT | PURGE_RXABORT |
          PURGE_TXCLEAR | PURGE_RXCLEAR
         );

Now we have to configure the port:

GetCommState(m_hComFile,&dcb) ;
dcb.BaudRate = CBR_1200;
dcb.ByteSize = DATABITS_8;
dcb.Parity = ODDPARITY;
dcb.StopBits = ONESTOPBIT;
dcb.fBinary = TRUE;
dcb.fParity = TRUE;
dcb.fErrorChar = FALSE;
dcb.ErrorChar = (char) 0;
dcb.fAbortOnError = FALSE;
dcb.fDtrControl = DTR_CONTROL_DISABLE;
dcb.fRtsControl = RTS_CONTROL_DISABLE;
if(SetCommState(m_hComFile,&dcb)==FALSE)
{
  CloseHandle(m_hComFile);
  return FALSE;
}

Usually Hart is working with a modem. Most of the modems on the market use the handshake signals RTS and DTR for switching the carrier on and off. Therefore we have to put these signals in the 'off state'.

EscapeCommFunction(m_hComFile,CLRRTS);
EscapeCommFunction(m_hComFile,SETDTR);
Sleep(10);
return TRUE;

Connecting to the Hart Device

All Hart Commands are working with a five byte unique address called the unique identifier. Only command 0 has the option to use the short address. Command 0 is used to read the unique identifier.

Firstly we have to prepare a frame for sending command 0.

// Send: FF FF FF FF FF|02|80|000|000||82
memcpy(&m_ucTxBuffer[0], "\xff\xff\xff\xff\xff\x02\x80\x00\x00",9);

Insert the address.

m_ucTxBuffer[6] |= ucAddr;

And build the bcb.

m_ucTxBuffer[9] = m_ucTxBuffer[5] ^ m_ucTxBuffer[6] ^
                  m_ucTxBuffer[7] ^ m_ucTxBuffer[8];

The next is to switch the carrier on, allow some time to settle, send the frame, wait for a certain time to send and switch off the carrier again.

// Switch on carrier
EscapeCommFunction(m_hComFile,SETRTS);
EscapeCommFunction(m_hComFile,CLRDTR);
// Wait until carrier is stable
Sleep(2);
// Send the data
ClearCommError(m_hComFile, &dwErrorFlags, &strComStat);
WriteFile(m_hComFile, &m_ucTxBuffer[0], 10, &dwWritten, &osWrite);
// Wait until Tx is completed
// 10 Bytes * 11 Bits * 1/1200 + 10 ms safety margin

Sleep(102);
// Switch off carrier
ClearCommError(m_hComFile, &dwErrorFlags, &strComStat);
EscapeCommFunction(m_hComFile,CLRRTS);
EscapeCommFunction(m_hComFile,SETDTR);

Then we call waitResponse in order to get the response from the Hart device. Without the preamble the length of the response we are expecting is 19 bytes.

 if(waitForResponse(19))

Finally we have to get the unique identifier from the response and store it on another place.

m_ucUniqueId[0] = m_ucRxBuffer[7];
m_ucUniqueId[1] = m_ucRxBuffer[8];
m_ucUniqueId[2] = m_ucRxBuffer[15];
m_ucUniqueId[3] = m_ucRxBuffer[16];
m_ucUniqueId[4] = m_ucRxBuffer[17];

Read the Tag Name

For reading the tag name we use command 13. The request frame is longer now carrying the unique id which was previously sent by the device.

// Send: FF FF FF FF FF|82|80|00|00|00|00|13|00|BCB
memcpy(&m_ucTxBuffer[0], "\xff\xff\xff\xff\xff\x82\x80\x00\x00\x00\x00\x0d\x00",13);

Insert the unique identifier.

m_ucTxBuffer[6] |= m_ucUniqueId[0] & 0x3f;
m_ucTxBuffer[7] = m_ucUniqueId[1];
m_ucTxBuffer[8] = m_ucUniqueId[2];
m_ucTxBuffer[9] = m_ucUniqueId[3];
m_ucTxBuffer[10] = m_ucUniqueId[4];

Note that in the first byte of the unique only the 6 low order bits are used.

Build and insert the bcb.

m_ucTxBuffer[13] = m_ucTxBuffer[5] ^ m_ucTxBuffer[6] ^
                   m_ucTxBuffer[7] ^ m_ucTxBuffer[8] ^
                   m_ucTxBuffer[9] ^ m_ucTxBuffer[10] ^
                   m_ucTxBuffer[11] ^ m_ucTxBuffer[12];

Sending the frame and waiting for the response is done like in the example above. The only difference is that there is a loop which repeats the command if the device answers with the response code busy(32).

do
{ bRepeat = FALSE;
  // Send the data
  //...
  //Wait for the response of 32 bytes length

  if(waitForResponse(32))
  {
    // Decode Tag Name
    //...

    return TRUE;
  }
  if((m_ucRcvLen == 0x0b) && (m_ucRxBuffer[8] == 32))
  { // Repeat service if device is busy
    bRepeat = TRUE;
    printf("Device Busy detected!\n");
  }
  else
  {
    return FALSE;
  }
}
while(bRepeat);
return FALSE; /* The program will never arrive here. */

Note that the length of the received frame is 0x0b in case of a busy frame. The busy frame contains only the two response codes in the Hart Protocol data field.

Finally the tag name can be decoded and stored locally. The packed ASCII format is only using 6 bits for the coding of characters. In this way 4 characters of packed ASCII fits into 3 bytes.

PA ASC CHR PA ASC CHR PA ASC CHR PA ASC CHR
 0  64  @  16  80  P  32  32 SPC 48  48  0
 1  65  A  17  81  Q  33  33  !  49  49  1
 2  66  B  18  82  R  34  34  "  50  50  2
 3  67  C  19  83  S  35  35  #  51  51  3
 4  68  D  20  84  T  36  36  $  52  52  4
 5  69  E  21  85  U  37  37  %  53  53  5
 6  70  F  22  86  V  38  38  &  54  54  6
 7  71  G  23  87  W  39  39  '  55  55  7
 8  72  H  24  88  X  40  40  (  56  56  8
 9  73  I  25  89  Y  41  41  )  57  57  9
10  74  J  26  90  Z  42  42  *  58  58  :
11  75  K  27  91  [  43  43  +  59  59  ;
12  76  L  28  92  \  44  44  ,  60  60  <
13  77  M  29  93  ]  45  45  -  61  61  =
14  78  N  30  94  ^  46  46  .  62  62  >
15  79  O  31  95  _  47  47  /  63  63  ?

The decoder is implemented straight foreward.

// Decode Hart Tag Name
for(e = 10; e < 16; e += 3)
{ uc = ((m_ucRxBuffer[e ] >> 2) & 0x3F);
  if(uc < 0x20)
    uc += 0x40;
  m_ucTagName[ucIdx++] = uc;
  uc = ((m_ucRxBuffer[e ] << 4) & 0x3F) |
       ((m_ucRxBuffer[e + 1] >> 4) & 0x3F);
  if(uc < 0x20)
    uc += 0x40;
  m_ucTagName[ucIdx++] = uc;
  uc = ((m_ucRxBuffer[e + 1] << 2) & 0x3F) |
       ((m_ucRxBuffer[e + 2] >> 6) & 0x3F);
  if(uc < 0x20)
    uc += 0x40;
  m_ucTagName[ucIdx++] = uc;
  uc = m_ucRxBuffer[e + 2] & 0x3F;
  if(uc < 0x20)
    uc += 0x40;
  m_ucTagName[ucIdx++] = uc;
}

Receiving the response

The response frame can have two formats.

Hart response with short address:

PA PA PA PA PA DE AD CO LE R1 R2 Data BC
|  |  |  |  |  |  |  |  |  |  |   |   +- Byte Check, xor all bytes
|  |  |  |  |  |  |  |  |  |  |   +----- Data Bytes
|  |  |  |  |  |  |  |  |  |  +--------- Response 2 (part of data)
|  |  |  |  |  |  |  |  |  +------------ Response 1 (part of data)
|  |  |  |  |  |  |  |  +--------------- Data Length 2..255
|  |  |  |  |  |  |  +------------------ Command
|  |  |  |  |  |  +--------------------- Control + Address(4 Bit)
|  |  |  |  |  +------------------------ Delimiter 0x06
+--+--+--+--+--------------------------- 5..20 Preamble bytes 0xFF

Hart response frame with long address:

PA PA PA PA PA DE U1 U2 U3 U4 U5 CO LE R1 R2 Data BC
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |   |   +- Byte Check, xor all bytes
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |   +----- Data Bytes
|  |  |  |  |  |  |  |  |  |  |  |  |  |  +--------- Response 2 (part of data)
|  |  |  |  |  |  |  |  |  |  |  |  |  +------------ Response 1 (part of data)
|  |  |  |  |  |  |  |  |  |  |  |  +--------------- Data Length 2..255
|  |  |  |  |  |  |  |  |  |  |  +------------------ Command
|  |  |  |  |  |  |  +--+--+--+--------------------- Unique Id Bytes 2..5
|  |  |  |  |  |  +--------------------------------- Control + Unique Id 1 (6 Bit)
|  |  |  |  |  +------------------------------------ Delimiter 0x86
+--+--+--+--+--------------------------------------- 5..20 Preamble bytes 0xFF

The receiver loop in the function waitForResponse has the following style

static bool waitForResponse(unsigned char ucExpectedDataLen)
{ // Initialize the receiver state machine
  //...
  while((m_ucRcvLen < ucExpectedDataLen) && (ucTimeOut))
  {
    // Get received data
    // ...
    if(dwLength)
    {
      // Data was received
      // Run the receiver state machine for each character
      // ...

    }
    ucTimeOut--;
    Sleep(10);
  }
  if(m_ucRcvLen < ucExpectedDataLen)
  {
    return FALSE;
  }
  return TRUE;
}

As it can be seen easily the loop is waiting until enough data is received or until a timer is exspired. The call Sleep(10) is very important. It allows other threads to run and thus the loop is not blocking windows or at least the process which it is running in.

The initialization of the receiver state machine is setting the time out timer, sets the receiver data length zero and puts the state machine into the initial state WAIT_PREAMBLE.

ucTimeOut = (unsigned char)
            ((ucExpectedDataLen * 9.16 + 300.0f) / 10.0f);
m_ucRcvLen = 0;
ucRcvStatus = WAIT_PREAMBLE;

The data is picked from the com port using the following three statements:

ClearCommError(m_hComFile, &dwErrorFlags, &strComStat);
dwLength = min(255, strComStat.cbInQue);
ReadFile(m_hComFile, &ucBuffer[0], dwLength, &dwLength, &osRead);

ClearCommError is returning the status of the com port in the structure strComStat. strComStat.cbInQue holds the count of received data bytes waiting in the file's queue. With ReadFile the bytes are picked from the queue and copied into the local array ucBuffer.

The receiver state machine is accessed for each single byte:

for(e = 0; e < dwLength; e++)
{
  switch(ucRcvStatus)
  {
    case WAIT_PREAMBLE:
      if(ucBuffer[e] == 0xff)
      { // Preamble byte
        ucRcvStatus = WAIT_DELIMITER;
      }
      break;
    case WAIT_DELIMITER:
      if(ucBuffer[e] == 0xff)
      { // Preamble byte do nothing
      }
      else
      {
        if((ucBuffer[e] &0x3f) == 0x06)
        { //Delimiter begin recording data
          m_ucRxBuffer[0] = ucBuffer[e];
          m_ucRcvLen++;
          ucRcvStatus = READING_DATA;
        }
        else
        { // Garbidge wait for next preamble
          ucRcvStatus = WAIT_PREAMBLE;
        }
      }
      break;
    case READING_DATA:
      m_ucRxBuffer[m_ucRcvLen++] = ucBuffer[e];
      break;
  }
}

The receiver state machine is waiting for a sequence consisting of a preamble followed by the start delimiter. It stores the received frame beginning with the start delimiter.

Back to Top

Last updated: 08.09.2008