IndustrialDeviceController/Software/HighLevelApp/drivers/modbus/modbus_transport_rtu.c (357 lines of code) (raw):
/* Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License. */
#include <applibs/gpio.h>
#include <applibs/application.h>
#include <math.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#include <init/device_hal.h>
#include <init/globals.h>
#include <init/ipc.h>
#include <iot/diag.h>
#include <utils/llog.h>
#include <utils/timer.h>
#include <utils/uart.h>
#include <utils/utils.h>
#include <driver/modbus.h>
#include <safeclib/safe_lib.h>
#include "modbus_transport.h"
#include "modbus_transport_rtu.h"
// Note: Modbus RTU only support one outgoing transcation, so everything
// is serialized, for Sphere to talking to multiple modbus rtu device (e.g. two
// device A, B, the sequence will be
// req1A, resp1A, req2A, resp2A, req1B, resp1B, req2B, resp2B.
typedef struct modbus_transport_rtu_t modbus_transport_rtu_t;
struct modbus_transport_rtu_t {
modbus_transport_t base; // must be first
int rtcore_socket_fd;
int uart_fd;
int uart_port;
int uart_tx_enable_fd;
int t35_ms;
int t35_adjust_times;
UART_Config uart_config;
};
static int rtu_ensure_idle(modbus_transport_rtu_t* ctx, int32_t timeout)
{
struct timespec poll_sw;
timer_stopwatch_start(&poll_sw);
struct pollfd fds[1];
fds[0].fd = ctx->rtcore_socket_fd;
fds[0].events = POLLIN;
bool quit = false;
uint8_t garbage[MB_RTU_MAX_ADU_SIZE];
while (!quit) {
int32_t elapse_ms = timer_stopwatch_stop(&poll_sw);
if (elapse_ms >= timeout) {
quit = true;
continue;
}
int nevents = poll(fds, 1, ctx->t35_ms);
if (nevents == 0) {
// no data on rtu
return 0;
} else if ((nevents == 1) && (fds[0].revents & POLLIN)) {
// Read garbage data in uart buffer up to one frame of 256 bytes.
int count = UART_read(ctx->uart_fd, garbage, MB_RTU_MAX_ADU_SIZE);
LOGD("Consume garbage data: read %d byes of garbage on RTU", count);
LOGD("Garbage data <--%s", hex(garbage, count));
} else {
LOGE("Uart poll error in rtu_ensure_idle");
quit = true;
}
}
return -1;
}
static uint16_t crc16(const uint8_t *buffer, int32_t len)
{
uint16_t temp, flag;
temp = 0xFFFF;
for (int32_t i = 0; i < len; i++) {
temp = temp ^ buffer[i];
for (int j = 1; j <= 8; j++) {
flag = temp & 0x0001;
temp >>= 1;
if (flag) {
temp ^= 0xA001;
}
}
}
return temp;
}
/// <summary>
/// try to write RTU frame over uart connection, timeout in MB_RTU_WRITE_TIMEOUT_MS
/// </summary>
/// <param name="ctx">RTU transportion instance</param>
/// <param name="buf">buffer to send</param>
/// <param name="count">count of bytes to send</param>
/// <param name="timeout">the value of timer in ms for this operation</param>
/// <returns>error code</returns>
static err_code rtu_write_frame(modbus_transport_rtu_t *ctx, uint8_t *buf, int32_t count, int32_t timeout)
{
err_code result = ipc_execute_command(ctx->rtcore_socket_fd, IPC_WRITE_UART, buf, count);
if (DEVICE_OK != result) {
LOGE("ERROR: Could not write bytes to UART on the real-time core with erro code: %d", result);
}
return result;
}
/// <summary>
/// try to find pdu len from first two bytes of pdu
/// </summary>
/// <param name="pdu">pdu</param>
/// <returns>pdu length or 0 if unknown</returns>
static int32_t modbus_find_pdu_len(uint8_t *pdu)
{
// it's error response, pdu is two bytes
if (pdu[0] & 0x80) {
return 2;
}
int32_t length = 0;
switch (pdu[0]) {
case FC_READ_COILS:
case FC_READ_DISCRETE_INPUTS:
case FC_READ_HOLDING_REGISTERS:
case FC_READ_INPUT_REGISTERS:
// 1 byte function code + 1 byte byte count + n bytes values
length = 2 + pdu[1];
break;
case FC_WRITE_COILS:
case FC_WRITE_HOLDING_REGISTERS:
case FC_WRITE_SINGLE_COIL:
case FC_WRITE_SINGLE_REGISTER:
// 1 byte function code + 2 bytes start addr, 2 byte quantity
length = 5;
break;
case FC_READ_EXCEPTION_STATUS:
// 1 byte function code + 1 byte output data
length = 2;
break;
case FC_DIAGNOSTICS:
// 1 byte function code, 2 byte sub-function , 2 bytes data
length = 5;
break;
case FC_GET_COMM_EVENT_COUNTER:
// 1 byte function code, 2 byte status, 2 bytes event count
length = 5;
break;
case FC_GET_COMM_EVENT_LOG:
length = 2 + pdu[1];
break;
case FC_REPORT_SERVER_ID:
length = 5;
break;
case FC_READ_FILE_RECORD:
case FC_WRITE_FILE_RECORD:
length = 2 + pdu[1];
break;
case FC_MASK_WRITE_REGISTER:
length = 7;
break;
case FC_READ_WRITE_REGISTERS:
length = 2 + pdu[1];
break;
case FC_READ_FIFO_QUEUE:
length = 2 + pdu[1];
break;
case FC_MEI:
// FC_MEI is not supported now as the pdu lenth cann't be found
// in pdu and not fixed.
default:
length = -1;
break;
}
return length;
}
/// <summary>
/// try to receied count bytes over uart connection, timeout in MB_RTU_IO_TIMEOUT_MS
/// </summary>
/// <param name="ctx">RTU transportion instance</param>
/// <param name="buf">buffer to receive data</param>
/// <param name="count">count of bytes to receive</param>
/// <param name="timeout">the value of timer in ms for this operation</param>
/// <returns>error code</returns>
static err_code rtu_read_bytes(modbus_transport_rtu_t *ctx, uint8_t *buf, int32_t length, int32_t *count, int32_t timeout)
{
err_code err = DEVICE_OK;
int total = 0;
struct timespec poll_sw;
struct pollfd fds[1];
fds[0].fd = ctx->rtcore_socket_fd;
fds[0].events = POLLIN;
timer_stopwatch_start(&poll_sw);
while (total < *count) {
int32_t elapse_ms = timer_stopwatch_stop(&poll_sw);
if (elapse_ms >= timeout) {
err = DEVICE_E_TIMEOUT;
break;
}
int nevents = poll(fds, 1, timeout - elapse_ms);
if (nevents < 0) {
LOGE("uart poll in error");
err = DEVICE_E_IO;
break;
} else if (nevents == 0) {
err = DEVICE_E_TIMEOUT;
break;
} else {
if (fds[0].revents & POLLHUP) {
LOGE("uart connection broken");
err = DEVICE_E_BROKEN;
break;
} else if (fds[0].revents & POLLERR) {
LOGE("uart poll error");
err = DEVICE_E_IO;
break;
} else if (fds[0].revents & POLLIN) {
int nread = recv(fds[0].fd, buf + total, length - total, 0);
if (nread < 0) {
LOGE("uart read error");
err = DEVICE_E_IO;
break;
}
total += nread;
}
}
}
*count = total;
return err;
}
/// <summary>
/// try to receie one RTU frame over uart connection
/// </summary>
/// <param name="ctx">RTU transportion instance</param>
/// <param name="buf">buffer to receive one frame of MB_RTU_MAX_ADU_SIZE bytes</param>
/// <param name="fbytes">bytes of received frame</param>
/// <param name="timeout">the value of timer in ms for this operation</param>
/// <returns>error code</returns>
static err_code rtu_read_frame(modbus_transport_rtu_t *ctx, uint8_t *buf, int32_t length, int32_t *frame_bytes, int32_t timeout)
{
// Find pdu len is tricky as we need to parse pdu to figure out
// read first three bytes of adu
struct timespec poll_sw;
timer_stopwatch_start(&poll_sw);
int32_t received_bytes = MB_RTU_HEADER_SIZE;
err_code err = rtu_read_bytes(ctx, buf, length, &received_bytes, timeout);
if (err) {
return err;
}
LOGD("PDU header <--%s", hex(buf, MB_RTU_HEADER_SIZE));
int32_t pdu_len = modbus_find_pdu_len(buf + 1);
if ((pdu_len <= 0) || (pdu_len > MB_RTU_MAX_ADU_SIZE - 3)) {
LOGE("Invalid pdu len %d", pdu_len);
return DEVICE_E_PROTOCOL;
}
// 1 byte slave_id + pdu + 2 bytes crc
*frame_bytes = 1 + pdu_len + 2;
int32_t elapse_ms = timer_stopwatch_stop(&poll_sw);
if (elapse_ms >= timeout) {
return DEVICE_E_TIMEOUT;
}
if (*frame_bytes == received_bytes) {
return DEVICE_OK;
} else {
pdu_len = *frame_bytes - received_bytes;
return rtu_read_bytes(ctx, buf + received_bytes, length - received_bytes, &pdu_len, timeout - elapse_ms);
}
}
/// <summary>
/// Adjust t35 timer if fail to get modbus response as time out.
/// </summary>
/// <param name="ctx">RTU transportion instance</param>
/// <param name="code">error code</param>
static void rtu_adjust_t35(modbus_transport_rtu_t* ctx, err_code code)
{
// only adjust t35 if time out
if (DEVICE_E_TIMEOUT == code && ctx->t35_adjust_times > 0) {
ctx->t35_ms += MODBUS_T35_ADJUST_STEP;
ctx->t35_adjust_times -= 1;
LOGD("Try T3.5=%dms for next request", ctx->t35_ms);
}
}
// ------------------------ public interface --------------------------------
/// <summary>
/// open uart connection
/// </summary>
err_code rtu_open(modbus_transport_t *instance, int32_t timeout_ms)
{
modbus_transport_rtu_t *ctx = (modbus_transport_rtu_t *)instance;
// don't try to open again
if (ctx->rtcore_socket_fd >= 0) {
LOGW("rtore socket already opened");
return DEVICE_OK;
}
LOGD("IPC open");
ctx->rtcore_socket_fd = Application_Connect(RT_APP_COMPONENT_ID);
if (ctx->rtcore_socket_fd == -1) {
LOGW("ERROR: Unable to create socket: %d (%s)\n", errno, strerror(errno));
return DEVICE_E_IO;
}
// Set timeout, to handle case where real-time capable application does not respond.
struct timeval rt_timeout = {.tv_sec = 0, .tv_usec = timeout_ms * 1000 / 4};
setsockopt(ctx->rtcore_socket_fd, SOL_SOCKET, SO_SNDTIMEO, &rt_timeout, sizeof(rt_timeout));
setsockopt(ctx->rtcore_socket_fd, SOL_SOCKET, SO_RCVTIMEO, &rt_timeout, sizeof(rt_timeout));
uint8_t data[6];
int32_t size = sizeof(data);
// serialize uart baudrate, parity and stopbits into message data.
serialize_uint32(data, ctx->uart_config.baudRate);
data[4] = ctx->uart_config.parity;
data[5] = ctx->uart_config.stopBits;
err_code result = ipc_execute_command(ctx->rtcore_socket_fd, IPC_OPEN_UART, data, size);
if (DEVICE_OK != result) {
LOGE("ERROR: Could not open UART on the real-time core with error code: %d", result);
}
return result;
}
/// <summary>
/// close uart connection
/// </summary>
/// <returns>0 on success, or -1 on failure</returns>
err_code rtu_close(modbus_transport_t *instance)
{
LOGD("rtu close");
modbus_transport_rtu_t *ctx = (modbus_transport_rtu_t *)instance;
if (ctx->uart_fd >= 0) {
UART_close(ctx->uart_fd);
ctx->uart_fd = -1;
}
if (ctx->rtcore_socket_fd >= 0) {
ipc_execute_command(ctx->rtcore_socket_fd, IPC_CLOSE_UART, NULL, 0);
close(ctx->rtcore_socket_fd);
ctx->rtcore_socket_fd = -1;
}
return DEVICE_OK;
}
/// <summary>
/// send request pdu
/// </summary>
/// <param name="pdu_len">pdu length to send</param>
/// <returns>error code</returns>
err_code rtu_send_request(modbus_transport_t *instance, uint8_t slave_id, const uint8_t *pdu, int32_t pdu_len,
int32_t timeout)
{
modbus_transport_rtu_t *ctx = (modbus_transport_rtu_t *)instance;
uint8_t adu[MB_RTU_MAX_ADU_SIZE];
adu[0] = slave_id;
memcpy_s(adu + 1, MB_RTU_MAX_ADU_SIZE - 1, pdu, pdu_len);
// crc for the entrie adu
uint16_t crc = crc16(adu, pdu_len + 1);
adu[1 + pdu_len] = crc & 0xFF;
adu[1 + pdu_len + 1] = (crc >> 8) & 0xFF;
int32_t adu_len = pdu_len + 3; // 1 byte slave id + pdu + 2 bytes crc
struct timespec poll_sw;
timer_stopwatch_start(&poll_sw);
if (rtu_ensure_idle(ctx, timeout) != 0) {
return DEVICE_E_BUSY;
}
int32_t elapse_ms = timer_stopwatch_stop(&poll_sw);
err_code err = rtu_write_frame(ctx, adu, adu_len, timeout - elapse_ms);
if (err) {
LOGE("Failed to write request:%s", err_str(err));
return err;
}
LOGV("ADU-->%s", hex(adu, adu_len));
return DEVICE_OK;
}
/// <summary>
/// recv response for previously sent request
/// </summary>
/// <param name="pdu">pdu buffer</param>
/// <param name="ppdu_len">pointer to variable to hold pdu len</param>
/// <param name="timeout">the value of timer in ms for this operation</param>
/// <returns>error code</returns>
err_code rtu_recv_response(modbus_transport_t *instance, uint8_t slave_id, uint8_t *pdu, int32_t *ppdu_len,
int32_t timeout)
{
modbus_transport_rtu_t *ctx = (modbus_transport_rtu_t *)instance;
uint8_t adu[MB_RTU_MAX_ADU_SIZE];
int32_t adu_len = 0;
// adu must have enough space to receive MB_RTU_MAX_ADU_SIZE bytes
int err = rtu_read_frame(ctx, adu, MB_RTU_MAX_ADU_SIZE, &adu_len, timeout);
diag_log_value(MODBUS_T35_DATAPOINT, ctx->t35_ms);
if (err != 0) {
LOGE("Failed to read adu:%s", err_str(err));
rtu_adjust_t35(ctx, err);
return err;
}
LOGV("ADU<--%s", hex(adu, adu_len));
if (adu[0] != slave_id) {
LOGE("Discard unexpected frame from slave %d, expected %d", adu[0], slave_id);
return DEVICE_E_PROTOCOL;
}
uint16_t crc1 = (adu[adu_len - 1] << 8) + adu[adu_len - 2];
uint16_t crc2 = crc16(adu, adu_len - 2);
if (crc1 != crc2) {
LOGE("CRC error, recv=%x calc=%x", crc1, crc2);
return DEVICE_E_PROTOCOL;
}
// 1 byte slave_id + pdu + 2 bytes crc
int32_t pdu_len = adu_len - 3;
// Assume that the caller passes in the buffer with size of MODBUS_MAX_PDU_SIZE
memcpy_s(pdu, MODBUS_MAX_PDU_SIZE, adu + 1, pdu_len);
*ppdu_len = pdu_len;
return DEVICE_OK;
}
/// <summary>
/// destroy rtu transport instance when no more reference
/// </summary>
/// <param name="id"></param>
/// <returns>0 on success, or -1 on failure</returns>
void modbus_transport_rtu_destroy(modbus_transport_t *instance)
{
ASSERT(instance);
LOGD("rtu destory");
rtu_close(instance);
FREE(instance);
}
/// <summary>
/// create modbus rtu transport instance, if connection configuration
/// is the same, then reuse old connection, increase reference count
/// </summary>
/// <param name="connection">connection config json value</param>
/// <param name="ptransport">pointer to variable hold transportion instance</param>
/// <returns>0 on success, or error code</returns>
modbus_transport_t *modbus_transport_rtu_create(const char *conn_str)
{
ASSERT(conn_str);
UART_Config config;
if (parse_uart_config_string(conn_str, &(config)) != 0) {
LOGE("modbus rtu invalid uart config");
return NULL;
}
LOGD("rtu create");
modbus_transport_rtu_t *rtu = (modbus_transport_rtu_t *)CALLOC(1, sizeof(modbus_transport_rtu_t));
rtu->base.transport_open = rtu_open;
rtu->base.transport_close = rtu_close;
rtu->base.send_request = rtu_send_request;
rtu->base.recv_response = rtu_recv_response;
rtu->uart_port = BOARD_UART;
memcpy_s(&(rtu->uart_config), sizeof(UART_Config), &(config), sizeof(UART_Config));
rtu->uart_fd = -1;
rtu->rtcore_socket_fd = -1;
rtu->uart_tx_enable_fd = -1;
if (rtu->uart_config.baudRate >= 19200) {
rtu->t35_ms = MODBUS_T35_MS;
} else {
// 1 start bit + dataBits + parity + stopBits
uint32_t bits_per_byte = 1 + rtu->uart_config.dataBits
+ (rtu->uart_config.parity == UART_Parity_None ? 0 : 1)
+ rtu->uart_config.stopBits;
float bytes_per_second = (float)(rtu->uart_config.baudRate) / bits_per_byte;
rtu->t35_ms = ceil(1000 * 3.5 / bytes_per_second);
}
rtu->t35_adjust_times = MODBUS_T35_MAXIMUM_RETRY;
return (modbus_transport_t *)rtu;
}