HDLC-Like data link via RS485

Communication between microcontrollers is always interesting topic. I was looking for simple, efficient and reliable protocol between several microcontrollers. First I have to choose the physical layer. One of the most commonly used multipoint buses is RS485. Due to simplicity I choose half-duplex RS485. Only two wires is all it needs to establish link between two devices:

rs485

Next problem was finding good data link on top of RS485 physical layer. High-Level Data Link Control or HDLC is data link layer protocol developed by the International Organization for Standardization (ISO) with several standards for frame structure and protocol procedures. Since I am not developing HDLC-compliant hardware I decided to take only part of it (frame structure) and write ultra-simplified “mini” HDLC-like protocol or mHDLC.

The contents of an  mHDLC frame are shown in the following table:

Flag Source address Destination address Control Information FCS Flag
8 bits 8 bits 8 bits 8 bits Variable length, n * 8 bits 16  bits 8 bits
  • The frame boundary octet is 01111110, (0x7E in hexadecimal notation).
  • Since RS485 is limited with the number of the devices on same bus it is enough to use 8-bit addressing. There are two addresses: Source and destination. The sender send first it’s own address followed by address of the addressed device.
  • Control octet is taken from HDLC frame structure. Currently onlu unnumbered information frame is implemented in my code. I put this in the frame structure for future expansion.
  • Information is the sequence of one or more bytes
  • The frame check sequence (FCS) is a 16-bit CRC-CCITT

 

Escape sequences

A “control escape octet”, has the bit sequence ‘01111101’, (7D hexadecimal).
If either frame boundary octet or escape octet appears in the transmitted data, an escape octet is sent, followed by the original data octet with bit 5 inverted. For example, the data sequence “01111110” (7E hex) would be transmitted as “01111101 01011110” (“7D 5E” hex).

 

UART

RS485 is basically asynchronous serial port, which is present in almost every microcontroller. The only difference is that it requires transciever for balanced lines and when bus half-duplex it means the two wires are shared between all devices on the bus. The consequence is that only one device can “talk” at once. This requires additional signal line for driving the data to the bus. The operation is shown in this RS485 Half Duplex mode oscillogram:

scope_0

The implementation of the driver enable and  initialiyation of the UART peripheral device is vendor- and family- specific for each microcontroller.

 

CODE
Code for the mHDLC has two functions which are not implemented and should be provided by application:

void uart_putchar(char ch);
int16_t payload_processor(hdlc_t *payload);

First function is straightforward. Second is callback when information is received for specific device. The example is at the end of this page.  When the mHDLC is started, the function hdlc_init() initializes the whole machinery. And finally, when byte is received via UART/RS485 it should be passed to the function hdlc_process_rx_byte().

 

hdlc.c

/**
  ******************************************************************************
  * File Name          : hdlc.c
  * Description        : HDLC frame parser
  ******************************************************************************
  *
  * Copyright (c) 2016 S54MTB
  * Licensed under Apache License 2.0 
  * http://www.apache.org/licenses/LICENSE-2.0.html
  *
  ******************************************************************************
  */
/* Includes ------------------------------------------------------------------*/
#include "hdlc.h"                                // header for this HDLC implementation
#include <string.h>                            // memcpy()
 
 
// Basic setup constants, define for debugging and skip CRC checking, too...
#include "setup.h"
 
 
__weak void uart_putchar(char ch)
{
    // implement function to send character via UART
}
 
//extern void uart_putchar(char ch);
__weak int16_t payload_processor(hdlc_t *hdlc)
{
    // implement function to do something with the received payload
    // when this function return > 0 ... the HDLC frame processor will 
    // initiate sending of the payload back to the device with source address
    return 0;
}
 
static hdlc_t        hdlc;
 
// Static buffer allocations
static uint8_t  _hdlc_rx_frame[HDLC_MRU];   // rx frame buffer allocation
static uint8_t  _hdlc_tx_frame[HDLC_MRU];   // tx frame buffer allocation
static uint8_t  _hdlc_payload[HDLC_MRU];    // payload buffer allocation
 
 
/** Private functions to send bytes via UART */
/* Send a byte via uart_putchar() function */
static void hdlc_tx_byte(uint8_t byte)
{
  uart_putchar((char)byte);
}
 
/* Check and send a byte with hdlc ESC sequence via UART */
static void hdlc_esc_tx_byte(uint8_t byte)
{
    if((byte == HDLC_CONTROL_ESCAPE) || (byte == HDLC_FLAG_SOF))
    {
        hdlc_tx_byte(HDLC_CONTROL_ESCAPE);
        byte ^= HDLC_ESCAPE_BIT;
        hdlc_tx_byte(byte);
    }
    else 
        hdlc_tx_byte(byte);
}
 
 
/* initialiyatiuon of the HDLC state machine, buffer pointers and status variables */
void hdlc_init(void)
{
  hdlc.rx_frame_index = 0;
  hdlc.rx_frame_fcs   = HDLC_CRC_INIT_VAL;
    hdlc.p_rx_frame       = _hdlc_rx_frame;
    memset(hdlc.p_rx_frame, 0, HDLC_MRU);
    hdlc.p_tx_frame       = _hdlc_tx_frame;
    memset(hdlc.p_tx_frame, 0, HDLC_MRU);
    hdlc.p_payload         = _hdlc_payload;
    memset(hdlc.p_payload, 0, HDLC_MRU);
    hdlc.state                    = HDLC_SOF_WAIT;
    hdlc.own_addr                = SETUP_OWNADDRESS;
}
 
 
/* This function should be called when new character is received via UART */
void hdlc_process_rx_byte(uint8_t rx_byte)
{
    switch (hdlc.state)
    {
        case HDLC_SOF_WAIT:   /// Waiting for SOF flag
            if (rx_byte == HDLC_FLAG_SOF) 
            {
                hdlc_init();
                hdlc.state = HDLC_DATARX;
            }
        break;
            
        case HDLC_DATARX:     /// Data reception process running
            if (rx_byte == HDLC_CONTROL_ESCAPE)  // is esc received ?
            {
                hdlc.state = HDLC_PROC_ESC;  // handle ESCaped byte
                break;
            } 
            // not ESC, check for next sof
            if (rx_byte == HDLC_FLAG_SOF) // sof received ... process frame
            {
                if (hdlc.rx_frame_index == 0) // sof after sof ... drop and continue
                    break;
                if (hdlc.rx_frame_index > 5) // at least addresses + crc
                  hdlc_process_rx_frame(hdlc.p_rx_frame, hdlc.rx_frame_index);
                hdlc_init();
                hdlc.state = HDLC_DATARX;
            } else // "normal" - not ESCaped byte
            {
                if (hdlc.rx_frame_index < HDLC_MRU) 
                {
                    hdlc.p_rx_frame[hdlc.rx_frame_index] = rx_byte;
                    hdlc.rx_frame_index++;                    
                } else // frame overrun
                {
                    hdlc_init();   // drop frame and start over
                }
            }
        break;
            
        case HDLC_PROC_ESC:  /// process ESCaped byte
            hdlc.state = HDLC_DATARX; // return to normal reception after this
          if (hdlc.rx_frame_index < HDLC_MRU) // check for overrun
            {
                rx_byte ^= HDLC_ESCAPE_BIT;  // XOR with ESC bit
                hdlc.p_rx_frame[hdlc.rx_frame_index] = rx_byte;  
                hdlc.rx_frame_index++;
            } else // frame overrun
            {
                hdlc_init();  // drop frame and start over
            }
        break;
            
            
    }
}
 
/** Process received frame buf with length len
  Frame structure:
      [Source Address]                // Address of the data source
        [Destination address]        // Address of the data destination
        [HDLC Ctrl byte]                // only UI - Unnumbered Information with payload are processed
        [payload]                                // 1 or more bytes of payload data
        [crc16-H]                                // MSB of CRC16
        [crc16-L]                                // LSB of CRC16
*/
void hdlc_process_rx_frame(uint8_t *buf, uint16_t len)
{
    if (len>=5)               // 5 bytes overhead (2xaddr+ctrl+crc)
    {
        hdlc.src_addr = buf[0];        // source address --- nedded for sending reply
        hdlc.dest_addr = buf[1];  // destination address --- check for match with own address
        hdlc.ctrl = buf[2];                // HDLC Ctrl byte
        
        // Is the received packet for this device and has proper ctrl ?
        if ((hdlc.dest_addr == SETUP_OWNADDRESS) & (hdlc.ctrl == (HDLC_UI_CMD | HDLC_POLL_FLAG)))
        {
          // process only frame where destination address matches own address
            hdlc.rx_frame_fcs = (uint16_t)(buf[len-2]<<8) | (uint16_t)(buf[len-1]);
            if (len>5)
            {  // copy payload
                memcpy(hdlc.p_payload,hdlc.p_rx_frame+3,len-5);
            }
            #ifndef __SKIPCRC__
            if (crc16(buf, len-2) == hdlc.rx_frame_fcs)
            #endif
            {
                // process received payload
                len = payload_processor(&hdlc);
                if (len > 0)
                {
                    hdlc_tx_frame(hdlc.p_payload, len);
                }
            }
        }        
    }
}
 
// calculate crc16 CCITT
uint16_t crc16(const uint8_t *data_p, uint8_t length)
{
    uint8_t x;
    uint16_t crc = 0xFFFF;
 
    while (length--){
            x = crc >> 8 ^ *data_p++;
            x ^= x>>4;
            crc = (crc << 8) ^ ((uint16_t)(x << 12)) ^ ((uint16_t)(x <<5)) ^ ((uint16_t)x);
    }
    return crc;
}
 
 
// Transmit HDLC UI frame 
void hdlc_tx_frame(const uint8_t *txbuffer, uint8_t len)
{
    //uint8_t  byte;
    uint16_t crc, i;
 
    // Prepare Tx buffer
    hdlc.p_tx_frame[0] = hdlc.own_addr;
    hdlc.p_tx_frame[1] = hdlc.src_addr;
    hdlc.p_tx_frame[2] = HDLC_UI_CMD | HDLC_FINAL_FLAG;
    
    for (i=0; i<len; i++)
    {
        hdlc.p_tx_frame[3+i] = *txbuffer++;
    }
    
    // Calculate CRC
    crc = crc16(hdlc.p_tx_frame, len+3);
    
    // Send/escaped buffer
    for (i=0; i<len+3; i++)
    {
        hdlc_esc_tx_byte(hdlc.p_tx_frame[i]);        // Send byte with esc checking
    }
        
    hdlc_esc_tx_byte((uint8_t)((crc>>8)&0xff));  // Send CRC MSB with esc check
    hdlc_esc_tx_byte((uint8_t)(crc&0xff));      // Send CRC LSB with esc check
    hdlc_tx_byte(HDLC_FLAG_SOF);         // Send flag - stop frame
}
 
 
// Transmit "RAW" HDLC UI frame --- just added SOF and CRC
void hdlc_tx_raw_frame(const uint8_t *txbuffer, uint8_t len)
{
    uint8_t  byte;
    uint16_t crc = crc16(txbuffer, len);
    
    hdlc_tx_byte(HDLC_FLAG_SOF);             // Send flag - indicate start of frame
    while(len)
    {
        byte = *txbuffer++;     // Get next byte from buffer
        hdlc_esc_tx_byte(byte);        // Send byte with esc checking
        len--;
    }
    
    hdlc_esc_tx_byte((uint8_t)((crc>>8)&0xff));  // Send CRC MSB with esc check
    hdlc_esc_tx_byte((uint8_t)(crc&0xff));      // Send CRC LSB with esc check
    hdlc_tx_byte(HDLC_FLAG_SOF);         // Send flag - stop frame
}
 
/* Copyright (c) 2016 S54MTB            ********* End Of File   ********/

 

hdlc.h

 

/**
  ******************************************************************************
  * File Name          : hdlc.h
  * Description        : HDLC implementation
  ******************************************************************************
  *
  * Copyright (c) 2016 S54MTB
  * Licensed under Apache License 2.0 
  * http://www.apache.org/licenses/LICENSE-2.0.html
  *
  ******************************************************************************
  */
/* Includes ------------------------------------------------------------------*/
#ifndef __HDLC_H__
#define __HDLC_H__
 
#define HDLC_MRU    256
// HDLC constants --- RFC 1662 
#define HDLC_FLAG_SOF				  0x7e   // Flag
#define HDLC_CONTROL_ESCAPE 	0x7d   // Control Escape octet
#define HDLC_ESCAPE_BIT     	0x20   // Transparency modifier octet (XOR bit)
#define HDLC_CRC_INIT_VAL   	0xffff
#define HDLC_CRC_MAGIC_VAL   	0xf0b8
#define HDLC_CRC_POLYNOMIAL  	0x8408
#define HDLC_UI_CMD						0x03     // Unnumbered Information with payload
#define HDLC_FINAL_FLAG       0x10     // F flag
#define HDLC_POLL_FLAG       0x10      // P flag
 
typedef enum
{
	HDLC_SOF_WAIT,
	HDLC_DATARX,
	HDLC_PROC_ESC,
} hdlc_state_t;
 
typedef struct 
{
	uint8_t				own_addr;
	uint8_t				src_addr;
	uint8_t  			dest_addr;
	uint8_t 			ctrl;
	uint8_t				*p_tx_frame;			// tx frame buffer
	uint8_t 			*p_rx_frame;			// rx frame buffer
	uint8_t				*p_payload;				// payload pointer
	uint16_t   		rx_frame_index;
	uint16_t			rx_frame_fcs;
	hdlc_state_t	state;
} hdlc_t;
 
void hdlc_init(void);
void hdlc_process_rx_byte(uint8_t rx_byte);
void hdlc_process_rx_frame(uint8_t *buf, uint16_t len);
void hdlc_tx_frame(const uint8_t *txbuffer, uint8_t len);
uint16_t hdlc_crc_update(uint16_t crc, uint8_t dat);
uint16_t crc16(const uint8_t *data_p, uint8_t length);
void hdlc_tx_raw_frame(const uint8_t *txbuffer, uint8_t len);
#endif

 

example of payload processor:

 

#include "payload_processor.h"
#include "si7013.h"
#include "setup.h"
#include "hdlc.h"
 
enum
{
	CMD_Temperature = 0x30,
	CMD_Humidity,
	CMD_Thermistor,
	CMD_ID,
};
 
 
extern I2C_HandleTypeDef hi2c1;
extern si7013_userReg_t si7013_userRegs;
extern UART_HandleTypeDef huart2;
 
 
int16_t payload_processor(hdlc_t *hdlc/*uint8_t *payload*/)
{	
	uint8_t response[6],i;
  int16_t len=0;
	uint32_t uid = UNIQUE_ID;
 
	switch (hdlc->p_payload[0])
	{
		case CMD_Temperature :
			si7013_measure_intemperature(&hi2c1,(int32_t *)response);
			len=5;
		break;
 
		case CMD_Humidity :
			si7013_measure_humidity(&hi2c1,(int32_t *)response);
			len=5;			
		break;
 
		case CMD_Thermistor:
			si7013_userRegs.reg2.b.vout = 1; // connect the thermistor to vdd
			si7013_userRegs.reg2.b.vrefp = 1; // connect vref to vdd	
			si7013_write_regs(&hi2c1, &si7013_userRegs);
			si7013_measure_thermistor(&hi2c1, (int16_t *)response);
		  len = 3;
		break;
 
		case CMD_ID:
			si7013_get_device_id(&hi2c1,response);
		  memcpy(&response[1],&uid,4);
			len=6;						
		break;		
	}
 
	for (i=0; i<len; i++) hdlc->p_payload[i+1]=response[i];
 
	return len;
}

Here is example of the testing setup…

The sensor (slave) is Humidity/Temperature sensor , the master is WEB server based on FRDM K64 which requires RS485 transciever, there is also “sniffer” for RS485 based on this RS485/USB design.

debugiranje0

1 – web server, 2 – RS485 for FRDM KE64 board, 3 – Humidity and temperature sensor, 4 – USB/RS485 interface.

 

debugiranje

Debug session (left:web server/top and sensor/bot) with RS485 sniffer at work and final result, web page with the measurements read via mHDLC.

Leave a Reply