Em agosto de 2019 publiquei o artigo “FreeMODBUS – Apresentação & Port” onde apresentei uma breve introdução sobre o Modbus, FreeModbus e um “port” que desenvolvi para a placa NXP Freedom Board KL25Z. Porém, o protocolo Modbus para algumas aplicações, pode trazer um excesso de complexidade desnecessária. Por exemplo, aplicações simples entre mestre-escravo ponto a ponto. Como alternativa de protocolo de comunicação mais simples temos o padrão STX-ETX.
O padrão STX-ETX, trata-se de um protocolo flexível e fácil de implementação dentro do padrão de sistema de segurança presentes no mercado. É um protocolo orientado a caractere, segmentação por delimitadores de dados e comprimento de frames variáveis.
Nota I: Infelizmente na minha pesquisa para escrever esse artigo não encontrei informação específica sobre a teoria desse padrão. Por conta disso vou apenas concentrar na implementação.
Nota II: Por se tratar de protocolo flexível e amplamente utilizado, existe bastante variações. A implementações que irei apresentar neste artigo, é algoritmo que costumo utilizar em meus projetos.
Implementação do Protocolo de Comunicação STX-ETX
As mensagens do protocolo de comunicação STX-ETX são formados por frames (conjunto de byte que forma a mensagem). O frame é divido em três grupos;
Cabeçalho da mensagem (Message Header): onde é composto pelos bytes de sincronismo (SYN) e o byte STX. Esse conjunto de bytes tem como objetivo sinalizar o envio de um novo frame.
Conteúdo da mensagem (Message Content): para a implementação que desenvolvi o conteúdo da mensagem é formada por;
- MSG Byte Number: Número de byte que é compõem o MSG Data[ n ], que varia de zero a quatro bytes.
- Command / Status: é número do comando ou número registro, para as mensagens enviadas pelo master, as mensagens de retorno do Slave este byte é utilizado como status, para notificar master se recebimento da mensagem.
- MSG Data [ ]: bytes destinado ao conteúdo da informação.
- Checksum: é o código usado para verificar a integridade de dados transmitidos.
Byte ETX: é byte que sinaliza o fim do frame.
A seguir temos figura que ilustra a composição do frame da mensagem:
Implementação
A implementação desenvolvida é dívida entre Master (mestre) e Slave (escravo). O algoritmo desenvolvido para Master, é uma aplicação desktop. Essa aplicação contém o algoritmo do protocolo e a interface de interação com o usuário, essa interface é simples baseada em terminal, onde permite que o usuário construa a mensagem a ser enviada ao dispositivo Slave.
A seguir temos o código fonte do master onde contém o algoritmo que constrói a mensagem, o restante do código fonte do master pode ser consultado no Github.
/*
* stx_etx.c
*
* Created on: 8 de jan de 2020
* Author: Evandro Teixeira
*/
#include "stx_etx.h"
/**
*
*/
//uint8_t stx_etx_calculate_checksum(msg_t data);
/*
*
*/
void stx_etx_send(msg_t dt)
{
uint8_t buf[16] = {0};
uint8_t i = 0, ii = 0;
buf[i++] = VALUE_SYN;
buf[i++] = VALUE_SYN;
buf[i++] = VALUE_STX;
buf[i++] = dt.byte_number;
buf[i++] = dt.command;
for(ii=0;ii<dt.byte_number;ii++)
{
buf[i++] = dt.data[ii];
}
buf[i++] = stx_etx_calculate_checksum(dt);
buf[i++] = VALUE_ETX;
if( serial_write(buf,i) == OK)
{
printf("\n\r Dados transmitido com sucesso. ");
}
else
{
printf("\n\r Falha em transmitir dados. ");
}
}
/**
* @brief
* @note: https://blog.datek.com.br/2019/10/ccomo-calcular-checksum/
*/
uint8_t stx_etx_calculate_checksum(msg_t data)
{
uint8_t checksum = 0;
uint8_t i = 0;
checksum ^= data.byte_number;
checksum ^= data.command;
for(i=0;i<data.byte_number;i++)
{
checksum ^= data.data[i];
}
return (0xFF - checksum);
}
/*
* stx_etx.h
*
* Created on: 8 de jan de 2020
* Author: Evandro Teixeira
*/
#ifndef STX_ETX_H_
#define STX_ETX_H_
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "serial.h"
#define NUMBER_OF_ITEMS 4
#define VALUE_SYN (uint8_t)0x16
#define VALUE_STX (uint8_t)0x02
#define VALUE_ETX (uint8_t)0x03
typedef struct
{
uint8_t byte_number;
uint8_t command;
uint8_t data[NUMBER_OF_ITEMS];
}msg_t;
void stx_etx_send(msg_t dt);
uint8_t stx_etx_calculate_checksum(msg_t data);
#endif /* STX_ETX_H_ */
O algoritmo desenvolvido para Slave é uma aplicação embarcada dedicada a microcontroladores. A implementação conta com a utilização do FreeRTOS, com isso o processo foi dividido em duas Task (tarefas).
A primeira Task é “STX ETX RX” que tem como responsabilidade processar os dados proveniente da interrupção da UART. Ela reconstrói a mensagem e checa se o conteúdo está íntegro, uma vez que a mensagem é válida esse encaminhada para as camadas da aplicação.
A segunda Task é “STX ETX TX” que tem como atribuição preparar e formata a mensagem a ser enviada ao Master.
A seguir temos o código fonte do Slave onde contém o algoritmo que constrói e processa a mensagem, o restante do código fonte do Slave pode ser consultado no Github.
/*
* stx_etx.c
*
* Created on: 27/12/2019
* Author: evandro
*/
#include "../Inc/stx_etx.h"
/**
*
*/
static data_msg_t data = {0};
QueueHandle_t queue_data_rx;
QueueHandle_t queue_msg_out;
QueueHandle_t queue_msg_in;
/**
*
*/
void stx_etx_task_rx(void *pvParameters);
void stx_etx_task_tx(void *pvParameters);
uint8_t stx_etx_calculate_checksum(data_msg_t data);
status_interpreter_t stx_etx_interpreter(data_rx_t data_in/*, data_msg_t *data_out*/);
/**
*
*/
void stx_etx_init(void)
{
/* Creat Queue Data RX UART */
queue_data_rx = xQueueCreate(NUMBER_OF_ITEMS,sizeof(data_rx_t));
/* Creat Queue MSG In */
queue_msg_out = xQueueCreate(NUMBER_OF_ITEMS,sizeof(data_msg_t));
/* Creat Queue MSG Out */
queue_msg_in = xQueueCreate(NUMBER_OF_ITEMS,sizeof(data_msg_t));
/* Creat Task Rx */
if(xTaskCreate(stx_etx_task_rx,"TaskRX",configMINIMAL_STACK_SIZE * 2,NULL,(configMAX_PRIORITIES-3),NULL) != pdPASS)
{
/* Fail in creat task */
}
/* Creat Task Tx */
if(xTaskCreate(stx_etx_task_tx,"TaskTX",configMINIMAL_STACK_SIZE,NULL,(configMAX_PRIORITIES-3),NULL) != pdPASS)
{
/* Fail in creat task */
}
}
/**
*
*/
void stx_etx_task_rx(void *pvParameters)
{
data_rx_t data_rx;
while(1)
{
if(xQueueReceive(queue_data_rx,&data_rx,(TickType_t)portMAX_DELAY) == pdTRUE)
{
switch( stx_etx_interpreter(data_rx/*,&data_tx*/) )
{
case FAULT_START_BYTE:
case FAULT_TIMEOUT:
case FAULT_INCORRECT_CRC:
case FAULT_STOP_BYTE:
case FAULT_INTERNAL_ERROR:
case FAULT_NO_COMMAND:
/* Notify Master of Communication Failure */
break;
case MSG_COMPLETED:
/* Send Data of App*/
if(xQueueSend(queue_msg_in,&data,(TickType_t)portMAX_DELAY) != pdPASS)
{
/* Failed to post the message */
}
break;
case IDLE:
case PROCESSING:
default:
break;
}
}
}
}
/**
*
*/
void stx_etx_task_tx(void *pvParameters)
{
data_msg_t data_tx = {0};
uint8_t data[16] = {0};
uint8_t index = 0;
uint8_t i = 0;
while(1)
{
if(xQueueReceive(queue_msg_out,&data_tx,(TickType_t)portMAX_DELAY) == pdTRUE)
{
/* Prepares data to be transmitted by UART */
index = 0;
data[index++] = VALUE_SYN;
data[index++] = VALUE_SYN;
data[index++] = VALUE_STX;
data[index++] = data_tx.byte_number;
data[index++] = data_tx.command;
for(i=0;i<data_tx.byte_number;i++)
{
data[index++] = data_tx.data[i];
}
data[index++] = stx_etx_calculate_checksum(data_tx);
data[index++] = VALUE_ETX;
/* Transmit data by UART */
MX_USART1_UART_Transmit(data,index);
}
}
}
/**
*
*/
void stx_etx_queue_get_data(uint8_t data)
{
data_rx_t data_rx;
data_rx.data = data;
data_rx.time = HAL_GetTick();
xQueueSendFromISR(queue_data_rx,&data_rx,pdFALSE );
}
/**
*
*/
bool stx_etx_queue_receive(data_msg_t *data_out, uint32_t tick)
{
bool ret = false;
static data_msg_t data = {0};
if(xQueueReceive(queue_msg_in,&data,(TickType_t)tick) == pdTRUE)
{
*data_out = data;
ret = true;
}
return ret;
}
/**
*
*/
bool stx_etx_queue_send(data_msg_t data,uint32_t tick)
{
bool ret = false;
if(xQueueSend(queue_msg_out,&data,(TickType_t)tick) == pdPASS)
{
ret = true;
}
return ret;
}
/**
* @brief
* @note: https://blog.datek.com.br/2019/10/ccomo-calcular-checksum/
*/
uint8_t stx_etx_calculate_checksum(data_msg_t data)
{
uint8_t checksum = 0;
uint8_t i = 0;
checksum ^= data.byte_number;
checksum ^= data.command;
for(i=0;i<data.byte_number;i++)
{
checksum ^= data.data[i];
}
return (0xFF - checksum);
}
/**
*
*/
status_interpreter_t stx_etx_interpreter(data_rx_t data_in /*,data_msg_t *data_out*/)
{
static state_data_t state_data = BYTE_NUMBER;
static state_interpreter_t state_interpreter = SYN1;
static data_rx_t data_old = {0};
static uint8_t index_data = 0;
status_interpreter_t ret = IDLE;
/* Check the time delta between bytes */
if((data_in.time - data_old.time) > TIMEOUT)
{
/* returns to initial state */
state_interpreter = SYN1;
}
switch(state_interpreter)
{
case SYN1: /* Start Byte 0 */
/* Check byte value SYN */
if(data_in.data == VALUE_SYN)
{
state_interpreter = SYN2;
ret = PROCESSING;
}
else
{
/* signals failure and returns to initial state */
ret = FAULT_START_BYTE;
}
break;
case SYN2: /* Start Byte 1 */
/* Check byte value SYN */
if(data_in.data == VALUE_SYN)
{
state_interpreter = STX;
ret = PROCESSING;
}
else
{
/* signals failure and returns to initial state */
state_interpreter = SYN1;
ret = FAULT_START_BYTE;
}
break;
case STX: /* Start Byte 2 */
/* Check byte value STX */
if(data_in.data == VALUE_STX)
{
state_interpreter = DATA;
state_data = BYTE_NUMBER;
ret = PROCESSING;
}
else
{
/* signals failure and returns to initial state */
state_interpreter = SYN1;
ret = FAULT_START_BYTE;
}
break;
case DATA: /* Message assembler */
ret = PROCESSING;
switch(state_data)
{
case BYTE_NUMBER:
state_data = COMMAND;
data.byte_number = data_in.data;
break;
case COMMAND:
if(data.byte_number == 0)
{
state_data = BYTE_NUMBER;
state_interpreter = CHK_CRC;
}
else
{
state_data = DATA_ASSEMBLER;
}
data.command = data_in.data;
index_data=0;
break;
case DATA_ASSEMBLER:
//msg.data.data[index_data++] = data_in.data;
data.data[index_data++] = data_in.data;
if(index_data >= data.byte_number)
{
state_data = BYTE_NUMBER;
state_interpreter = CHK_CRC;
}
break;
case MAX_STATE_DATA:
default:
/* signals failure and returns to initial state */
state_interpreter = SYN1;
state_data = BYTE_NUMBER;
ret = FAULT_INTERNAL_ERROR;
break;
}
break;
case CHK_CRC:
/* computes the received message's CRC value
* and compares the transmitted CRC value */
if(data_in.data == stx_etx_calculate_checksum(data))
{
state_interpreter = ETX;
}
else
{
/* signals failure and returns to initial state */
state_interpreter = SYN1;
ret = FAULT_INCORRECT_CRC;
}
break;
case ETX: /* Stop Byte */
if(data_in.data == VALUE_ETX)
{
state_interpreter = SYN1;
ret = MSG_COMPLETED;
}
else
{
/* signals failure and returns to initial state */
state_interpreter = SYN1;
ret = FAULT_STOP_BYTE;
}
break;
case MAX_STATE_INTERPRETER:
default:
/* signals failure and returns to initial state */
state_interpreter = SYN1;
ret = FAULT_INTERNAL_ERROR;
break;
}
/* update variable that stores last data */
data_old = data_in;
return ret;
}
/*
* stx_etx.h
*
* Created on: 27/12/2019
* Author: Evandro Teixeira
*/
#ifndef PROTOCOLOSTXETX_INC_STX_ETX_H_
#define PROTOCOLOSTXETX_INC_STX_ETX_H_
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
#include "usart.h"
#include "crc.h"
#include <stdbool.h>
#define NUMBER_OF_ITEMS 4
#define VALUE_SYN (uint8_t)0x16
#define VALUE_STX (uint8_t)0x02
#define VALUE_ETX (uint8_t)0x03
#define TIMEOUT (uint32_t)100000
typedef enum
{
SYN1 = 0,
SYN2,
STX,
DATA,
CHK_CRC,
ETX,
MAX_STATE_INTERPRETER
}state_interpreter_t;
typedef enum
{
IDLE = 0,
PROCESSING,
MSG_COMPLETED,
FAULT_START_BYTE,
FAULT_TIMEOUT,
FAULT_INCORRECT_CRC,
FAULT_STOP_BYTE,
FAULT_NO_COMMAND,
FAULT_INTERNAL_ERROR
}status_interpreter_t;
typedef enum
{
BYTE_NUMBER = 0,
COMMAND,
DATA_ASSEMBLER,
MAX_STATE_DATA
}state_data_t;
typedef struct
{
uint8_t data;
uint32_t time;
}data_rx_t;
typedef struct
{
//uint8_t config;
uint8_t byte_number;
uint8_t command;
uint8_t data[4];
}data_msg_t;
/*typedef struct
{
uint8_t syn1;
uint8_t syn2;
uint8_t stx;
data_msg_t data;
uint8_t crc;
uint8_t etx;
}msg_t;*/
void stx_etx_init(void);
void stx_etx_queue_get_data(uint8_t data);
//uint8_t stx_etx_calculate_checksum(data_msg_t data, data_msg_t *data_out);
bool stx_etx_queue_receive(data_msg_t *data_out,uint32_t tick);
bool stx_etx_queue_send(data_msg_t data,uint32_t tick);
#endif /* PROTOCOLOSTXETX_INC_STX_ETX_H_ */
Demonstração
A aplicação de demonstração para o protocolo de comunicação STX-ETX é bem simples, consiste em:
- Master preparar e envia comandos para o Slave.
- O Slave por sua vez recebe os comandos, processa e toma as ações de acordo com comandos recebidos. Segue a tabelas com os comandos:
|
Master |
||
|
MSG |
Command |
Data |
|
Set LED Green |
1 |
Off: 0 | On: 1 |
|
Set LED Blue |
2 |
Off: 0 | On: 1 |
|
Get Status Button |
3 |
NA |
|
Slave |
||
|
MSG |
Status |
Data |
|
Set LED Green |
Ok: 1 | Fail: 0 |
NA | Cod. Error |
|
Set LED Blue |
Ok: 1 | Fail: 0 |
NA | Cod. Error |
|
Get Status Button |
Ok: 1 | Fail: 0 |
Value Button | Cod. Error |
Como dito anteriormente a aplicação master é desenvolvida para desktop. Ela interage com o usuário para montar as mensagens a ser enviada para o Slave. A seguir temos algumas imagens da interface do usuário com alguns comandos enviados.
A aplicação desenvolvida para o Slave, é um firmware para STM32F0DISCOVERY, trata-se de kit de desenvolvimento para o microcontrolador STM32F0 que por sua vez é baseado arquitetura ARM Cortex-M0. Os comandos propostos para a aplicação limitam-se em acionar os LED’s presente na STM32F0DISCOVERY e ler o status do USER Button. A seguir temos figura que ilustra a arquitetura do software presente do Slave.
O algoritmo do Slave consiste em ler os dados recebidos pelo barramento serial (UART), reconstruir a mensagem e tomar as ações de acordo com as mensagens recebidas.
/*
* app.c
*
* Created on: 03/01/2020
* Author: Evandro Teixeira
*/
#include "../Inc/app.h"
/**
*
*/
void app_task(void *pvParameters);
void app_led_green(bool st);
void app_led_blue(bool st);
bool app_status_button_get(void);
/**
*
*/
static bool status_button = false;
extern QueueHandle_t queue_msg_in;
/**
*
*/
void app_init(void)
{
/* Creat Task App */
if(xTaskCreate(app_task,"App",configMINIMAL_STACK_SIZE,NULL,(configMAX_PRIORITIES-4),NULL) != pdPASS)
{
/* Fail in creat task */
}
}
/**
*
*/
void app_task(void *pvParameters)
{
static data_msg_t data_in = {0};
static data_msg_t data_out = {0};
app_led_green(0 /* Off */);
app_led_blue(0 /* Off */);
while(1)
{
if(stx_etx_queue_receive(&data_in,portMAX_DELAY) == true)
//if(xQueueReceive(queue_msg_in,&data_in,(TickType_t)portMAX_DELAY) == pdTRUE)
{
/* State machine with the commands */
switch(data_in.command)
{
case APP_LED_GREEN:
/* Set LED Green */
app_led_green((bool)data_in.data[0]);
/* Prepare data to be transmitted */
data_out.byte_number = 0;
data_out.command = 1; // Status Ok
break;
case APP_LED_BLUE:
/* Set LED Blue */
app_led_blue((bool)data_in.data[0]);
/* Prepare data to be transmitted */
data_out.byte_number = 0;
data_out.command = 1; // Status Ok
break;
case APP_STATUS_BUTTON:
/* Get Status Button */
data_out.byte_number = 1;
data_out.command = 1; // Status Ok
data_out.data[0] = (uint8_t)(app_status_button_get());
break;
case APP_NO_COMMAND:
default:
/* Signals failure */
data_out.byte_number = 1;
data_out.command = 0; // Status Faul
data_out.data[0] = 255; // Cod. Error
break;
}
/* Send MSG */
stx_etx_queue_send(data_out,portMAX_DELAY);
}
}
}
/**
*
*/
void app_led_green(bool st)
{
HAL_GPIO_WritePin(GPIOC,LD3_Pin,(GPIO_PinState)st);
}
/**
*
*/
void app_led_blue(bool st)
{
HAL_GPIO_WritePin(GPIOC,LD4_Pin,(GPIO_PinState)st);
}
/**
*
*/
void app_status_button_set(bool data)
{
status_button = data;
}
/**
*
*/
bool app_status_button_get(void)
{
status_button = HAL_GPIO_ReadPin(USER_BUTTON_GPIO_Port,USER_BUTTON_Pin);
return status_button;
}
/*
* app.h
*
* Created on: 03/01/2020
* Author: Evandro Teixeira
*/
#ifndef APP_INC_APP_H_
#define APP_INC_APP_H_
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
#include "../Inc/stx_etx.h"
#include <stdbool.h>
typedef enum
{
APP_LED_GREEN = 1,
APP_LED_BLUE,
APP_STATUS_BUTTON,
APP_NO_COMMAND
}app_command_t;
void app_init(void);
void app_status_button_set(bool data);
#endif /* APP_INC_APP_H_ */
Conclusão
O protocolo de comunicação STX-ETX é uma boa alternativa para projetos mais simples que necessitam de uma comunicação ponto a ponto entres Master e Slave. É um padrão de fácil implementação.
O que você achou? Você trabalha ou já trabalhou com o protocolo de comunicação STX-ETX? Deixe o seu comentário a abaixo.
Saiba Mais
FreeMODBUS – Apresentação & Port
Protocolo Modbus: Fundamentos e Aplicações
Criando seu próprio shell para sistemas embarcados
Referência
https://github.com/evandro-teixeira/protocolo_stx_etx




Excelente artigo e ideias. Já implementei comunicação serial assíncrona com 1 Master e diversos Slaves e as ideias apresentadas aqui servirão de ajuda nos próximos projetos.
Olá Edemilso, fico feliz em saber que você gostou do artigo.
Recentemente implementei também implementei com um Master e vários Slaves, sobre o protocolo RS485, funcionou muito bem. Tive que adicionar mais camada de software no Master para o gerenciamento dos dispositivos. Ficou bem legal! 😉
Olá Evandro Teixeira, gostei muito do seu trabalho, parabéns. Eu estou trabalhando em um projeto e preciso enviar alguns dados através do protocolo de comunicação STX ETX, poderia me dar alguma dica?
Bruno de Lima fico feliz que você gostou do artigo.
É claro que posso te ajudar.
Facil e digo que até intuitivo, comecei a desenvolver um protocolo para uma arquitetura master/slave não tinha conhecimento dessa, entretanto, o que fiz é muito parecido com o que foi descrito:
STX | ID | VALUE | CRC | ETX
Obrigado por compartilhar.
Francis David fico feliz em saber que você gostou do artigo.
Conforme o Jeu Tonete apontou em seu comentário, existe alguns pontos a serem melhorados no algoritmo.
Ótimo artigo, parabéns. Utilizo um protocolo semelhante a esse há algum tempo. O que difere é o que não utilizo o STX-ETX no header e final do frame. Funciona muito bem em comunicações digitais assíncronas, inclusive uso na comunicação entre módulo Bluetooth e microcontrolador. Vale ressaltar duas coisas: 1- O tamanho máximo de dados que pode ser trafegado nesse protocolo é 255 (máximo valor do MSG byte number) e precisa ser filtrado quando recebido para não estourar os vetores de recepção. 2- É válido comentar sobre o timeout na recepção que você implementou. Sem ele é muito fácil perder o… Leia mais »
Jeu Tonete fico feliz que você gostou do artigo,
Muito obrigado pelas dicas, vou implementar no algoritmo do projeto e atualizar o Github.