libmodbus 源码分析_1

unit-test-server.c

我们接着从源码的单元测试服务器(从机)libmodbus-3.1.10\tests\unit-test-server.cmain 函数开始分析源码执行流和所用的数据结构。推进学习百问网提供的全场景工业互联设备管理系统解决方案,其中有对 libmodbus 源码详细的视频课程讲解。

可以看到对于从机,初始化时的操作是类似的

use_backend = RTU;                          /* 设置传输模式为 RTU */
ip_or_device = "/dev/ttyUSB0";              /* 设置串口设备 */
ctx = modbus_new_rtu(ip_or_device, 115200, 'N', 8, 1);
modbus_set_slave(ctx, SERVER_ID);           /* 从机设置自己的地址 */
query = malloc(MODBUS_RTU_MAX_ADU_LENGTH);  /* 分配一个 ADU 最大的内存空间 */
/* Modbus_Application_Protocol_V1_1b.pdf Chapter 4 Section 1 Page 5
 * RS232 / RS485 ADU = 253 bytes + slave (1 byte) + CRC (2 bytes) = 256 bytes
 */
#define MODBUS_RTU_MAX_ADU_LENGTH 256

构建服务器(从机)端的 Modbus 数据模型内存映射

/* Allocates 4 arrays to store bits, input bits, registers and inputs
   registers. The pointers are stored in modbus_mapping structure.

   The modbus_mapping_new_start_address() function shall return the new allocated
   structure if successful. Otherwise it shall return NULL and set errno to
   ENOMEM. */
modbus_mapping_t *modbus_mapping_new_start_address(unsigned int start_bits,
                                                   unsigned int nb_bits,
                                                   unsigned int start_input_bits,
                                                   unsigned int nb_input_bits,
                                                   unsigned int start_registers,
                                                   unsigned int nb_registers,
                                                   unsigned int start_input_registers,
                                                   unsigned int nb_input_registers)

modbus_mapping_new_start_address() 函数它分配内存来映射存储 Modbus 中的数据模型(线圈,输入离散值,保持寄存器和输入寄存器)的数据结构。这些结构在 Modbus 通信过程中用于管理和映射数据。如下图所示

该函数创建并初始化一个 modbus_mapping_t 结构体

typedef struct _modbus_mapping_t {
    int nb_bits;
    int start_bits;
    int nb_input_bits;
    int start_input_bits;
    int nb_input_registers;
    int start_input_registers;
    int nb_registers;
    int start_registers;
    uint8_t *tab_bits;
    uint8_t *tab_input_bits;
    uint16_t *tab_input_registers;
    uint16_t *tab_registers;
} modbus_mapping_t;

该函数它包含了以下映射:

  1. 位 (0X):用于读取和写入输出的线圈状态。
  2. 输入位 (1X):用于读取输入离散状态。
  3. 寄存器 (4X):用于读取和写入数据的保持寄存器。
  4. 输入寄存器 (3X):用于读取输入寄存器的值。

该函数它接受以下参数来配置每个映射的内存分配:

  • start_bits:线圈的起始地址。
  • nb_bits:线圈的数量。
  • start_input_bits:输入离散值的起始地址。
  • nb_input_bits:输入离散值的数量。
  • start_registers:保持寄存器的起始地址。
  • nb_registers:保持寄存器的数量。
  • start_input_registers:输入寄存器的起始地址。
  • nb_input_registers:输入寄存器的数量。

该函数使用动态内存分配来创建这些映射,并返回指向 modbus_mapping_t 结构体的指针。

/* 首先为 modbus_mapping_t 结构体分配内存。*/
mb_mapping = (modbus_mapping_t *) malloc(sizeof(modbus_mapping_t));

接着为各种数据模型分配内存。

/* 0X (线圈) */
mb_mapping->nb_bits = nb_bits;
mb_mapping->start_bits = start_bits;
if (nb_bits == 0) {
    mb_mapping->tab_bits = NULL;  // 如果没有线圈需要映射,则不分配内存。
} else {
    // 为线圈分配内存。
    mb_mapping->tab_bits = (uint8_t *) malloc(nb_bits * sizeof(uint8_t));
    if (mb_mapping->tab_bits == NULL) {
        free(mb_mapping);  // 如果内存分配失败,释放已分配的内存。
        return NULL;
    }
    memset(mb_mapping->tab_bits, 0, nb_bits * sizeof(uint8_t));  // 初始化为 0。
}

/* 1X (输入离散值) */
mb_mapping->nb_input_bits = nb_input_bits;
mb_mapping->start_input_bits = start_input_bits;
if (nb_input_bits == 0) {
    mb_mapping->tab_input_bits = NULL;  // 如果没有输入离散值需要映射,则不分配内存。
} else {
    // 为输入离散值分配内存。
    mb_mapping->tab_input_bits = (uint8_t *) malloc(nb_input_bits * sizeof(uint8_t));
    if (mb_mapping->tab_input_bits == NULL) {
        free(mb_mapping->tab_bits);  // 释放已分配的内存。
        free(mb_mapping);
        return NULL;
    }
    memset(mb_mapping->tab_input_bits, 0, nb_input_bits * sizeof(uint8_t));  // 初始化为 0。
}

/* 4X (保持寄存器) */
mb_mapping->nb_registers = nb_registers;
mb_mapping->start_registers = start_registers;
if (nb_registers == 0) {
    mb_mapping->tab_registers = NULL;  // 如果没有保持寄存器需要映射,则不分配内存。
} else {
    // 为保持寄存器分配内存。
    mb_mapping->tab_registers = (uint16_t *) malloc(nb_registers * sizeof(uint16_t));
    if (mb_mapping->tab_registers == NULL) {
        free(mb_mapping->tab_input_bits);  // 释放已分配的内存。
        free(mb_mapping->tab_bits);
        free(mb_mapping);
        return NULL;
    }
    memset(mb_mapping->tab_registers, 0, nb_registers * sizeof(uint16_t));  // 初始化为 0。
}

/* 3X (输入寄存器) */
mb_mapping->nb_input_registers = nb_input_registers;
mb_mapping->start_input_registers = start_input_registers;
if (nb_input_registers == 0) {
    mb_mapping->tab_input_registers = NULL;  // 如果没有输入寄存器需要映射,则不分配内存。
} else {
    // 为输入寄存器分配内存。
    mb_mapping->tab_input_registers =
        (uint16_t *) malloc(nb_input_registers * sizeof(uint16_t));
    if (mb_mapping->tab_input_registers == NULL) {
        free(mb_mapping->tab_registers);  // 释放已分配的内存。
        free(mb_mapping->tab_input_bits);
        free(mb_mapping->tab_bits);
        free(mb_mapping);
        return NULL;
    }
    memset(mb_mapping->tab_input_registers, 0, nb_input_registers * sizeof(uint16_t));  // 初始化为 0。
}
```c

---

```c
return mb_mapping;  // 返回指向分配的 modbus_mapping_t 结构体的指针。

建立连接

rc = modbus_connect(ctx);
if (rc == -1) {
    fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
    modbus_free(ctx);
    return -1;
}

_modbus_rtu_receive

do {
    rc = modbus_receive(ctx, query);
    /* Filtered queries return 0 */
} while (rc == 0);

这个函数的用来接收 Modbus RTU 消息,并根据协议的需要进行处理。

static int _modbus_rtu_receive(modbus_t *ctx, uint8_t *req)
{
    int rc;
    modbus_rtu_t *ctx_rtu = ctx->backend_data;

    /* 检查是否设置了忽略下一个确认消息的标志 */
    if (ctx_rtu->confirmation_to_ignore) {
        /* 如果设置了忽略标志,接收消息作为确认(不处理)*/
        _modbus_receive_msg(ctx, req, MSG_CONFIRMATION);

        /* 在接收确认消息后,重置该标志 */
        ctx_rtu->confirmation_to_ignore = FALSE;

        rc = 0;  /* 设置返回代码为 0,表示成功处理(没有错误)*/
        
        /* 如果启用了调试模式,打印提示消息,表明确认消息被忽略 */
        if (ctx->debug) {
            printf("Confirmation to ignore\n");
        }
    } else {
        /* 如果没有设置忽略标志,继续接收常规消息(指示消息)*/
        rc = _modbus_receive_msg(ctx, req, MSG_INDICATION);

        /* 如果没有发生错误(rc == 0),则设置标志,表明下一个消息应该是需要忽略的确认消息 */
        if (rc == 0) {
            ctx_rtu->confirmation_to_ignore = TRUE;
        }
    }

    /* 返回接收操作的结果代码 */
    return rc;
}

在前面执行的 modbus_new_rtu 函数时,函数内部 modbus-rtu.c 中第 #1285 行初始化 ctx_rtu->confirmation_to_ignore = FALSE
服务器端(从机)在开始建立连接并接收请求时会直接执行 _modbus_receive_msg 函数。

简单说下 _modbus_receive_msg 函数内部执行流程。

while (length_to_read != 0) {
    // 1. 使用 select 函数等待数据可读
    rc = ctx->backend->select(ctx, &rset, p_tv, length_to_read);

    // ... (此处错误处理)

    // 2. 接收数据并将其存储在消息缓冲区中
    rc = ctx->backend->recv(ctx, msg + msg_length, length_to_read);

    // ... (此处错误处理)

    // 3. 如果调试模式开启,打印接收到的每个字符的十六进制值
    if (ctx->debug) {
        int i;
        for (i = 0; i < rc; i++)
            printf("<%.2X>", msg[msg_length + i]);
    }

    // 4. 更新已接收的字节数
    msg_length += rc;
    // 5. 计算剩余的待读取字节数
    length_to_read -= rc;

    // 6. 如果当前步骤的字节已经全部读取完毕,进入下一步骤
    if (length_to_read == 0) {
        switch (step) {
        case _STEP_FUNCTION:
            // 6.1 计算功能码后的元数据长度
            length_to_read = compute_meta_length_after_function(
                msg[ctx->backend->header_length], msg_type);
            if (length_to_read != 0) {
                step = _STEP_META;  // 进入元数据读取步骤
                break;
            } // 如果元数据长度为0,直接进入下一步
        case _STEP_META:
            // 6.2 计算元数据后的数据长度
            length_to_read = compute_data_length_after_meta(ctx, msg, msg_type);
            if ((msg_length + length_to_read) > ctx->backend->max_adu_length) {
                // 如果数据长度超过最大允许长度,返回错误
                errno = EMBBADDATA;
                _error_print(ctx, "too many data");
                return -1;
            }
            step = _STEP_DATA;  // 进入数据读取步骤
            break;
        default:
            break;
        }
    }

    // 7. 如果还有待读取的字节,并且设置了字节间超时,则更新超时时间
    if (length_to_read > 0 &&
        (ctx->byte_timeout.tv_sec > 0 || ctx->byte_timeout.tv_usec > 0)) {
        // 7.1 设置字节间超时时间
        tv.tv_sec = ctx->byte_timeout.tv_sec;
        tv.tv_usec = ctx->byte_timeout.tv_usec;
        p_tv = &tv;
    }
    // 8. 如果没有设置字节间超时,则必须在响应超时之前读取完整响应(仅用于确认)
}

以上代码都是在 Linux 环境中实现的,需要改成我们的裸机版本和FreeRTOS版本。

  1. select 函数

    • 用于等待数据可读。select 是一个常见的 I/O 多路复用函数,可以监视多个文件描述符的状态变化。
    • ctx->backend->select 是一个抽象的后端函数,用于检查是否有数据可读。
    • p_tv 是超时时间,如果在指定时间内没有数据可读,select 会返回。
  2. recv 函数

    • 用于从连接中接收数据。ctx->backend->recv 是一个抽象的后端函数,用于接收数据。
    • 接收到的数据存储在 msg 缓冲区中,从 msg_length 位置开始存储。

modbus_send_raw_request modbus_reply

通过 modbus_send_raw_request 发送原始请求作为回应或者 modbus_reply 给客户端(主机)发送回复。

modbus_mapping_free modbus_close modbus_free

通过 modbus_mapping_free 释放数据模型内存
通过 modbus_close 关闭连接
通过 modbus_free 释放上下文对象