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:
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:
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.