Programming the
Hart Communication Protocol at a PC Com Port
A C Language Software Example
Presented by 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:
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.
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
|