文章目录
- 一、modbus_new_rtu
- **1. 函数声明:**
- **2. 参数检查:**
- **3. 内存分配:**
- **4. 设备字符串分配与初始化:**
- **5. 配置 RTU 参数:**
- **6. 其他 RTU 配置:**
- **7. 返回 `modbus_t` 上下文:**
- **总结:**
- 二、modbus_set_slave
- 函数声明:
- 代码分析:
- **总结:**
- 三、modbus_connect
- `modbus_rtu_connect` 函数
- `modbus_rtu_is_connected` 函数
- 总结:
- 四、modbus_write_bit
- 1. **`modbus_write_bit` 函数:**
- 2. **`write_single` 函数:**
- 3. **`send_msg` 函数:**
- 4. **`_modbus_receive_msg` 和 `check_confirmation` 函数:**
- 总结:
一、modbus_new_rtu
1. 函数声明:
modbus_t *modbus_new_rtu(const char *device, int baud, char parity, int data_bit, int stop_bit)
- 返回类型:
modbus_t *
,返回一个指向modbus_t
类型的指针,表示新创建的 Modbus RTU 上下文。 - 参数:
device
:设备名称(通常是串口设备路径,如/dev/ttyS0
)。baud
:波特率,表示通信速度(例如 9600,19200 等)。parity
:奇偶校验位(‘N’ 表示无校验,‘E’ 表示偶校验,‘O’ 表示奇校验)。data_bit
:数据位数,通常为 8 位。stop_bit
:停止位,通常为 1 或 2。
2. 参数检查:
if (device == NULL || *device == 0) {
fprintf(stderr, "The device string is empty\n");
errno = EINVAL;
return NULL;
}
- 如果设备字符串为空或无效(
device
为 NULL 或空字符串),打印错误消息并返回NULL
,同时设置errno
为EINVAL
(无效参数错误)。
if (baud == 0) {
fprintf(stderr, "The baud rate value must not be zero\n");
errno = EINVAL;
return NULL;
}
- 检查波特率是否为 0,如果是,则返回
NULL
,并设置errno
为EINVAL
。
3. 内存分配:
ctx = (modbus_t *) malloc(sizeof(modbus_t));
if (ctx == NULL) {
return NULL;
}
- 为
modbus_t
类型的结构体分配内存。modbus_t
是 Modbus 通信的上下文结构。
_modbus_init_common(ctx);
ctx->backend = &_modbus_rtu_backend;
- 调用
_modbus_init_common
初始化modbus_t
结构中的公共部分。 - 设置
ctx->backend
为 RTU 后端的结构指针(_modbus_rtu_backend
)。
ctx->backend_data = (modbus_rtu_t *) malloc(sizeof(modbus_rtu_t));
if (ctx->backend_data == NULL) {
modbus_free(ctx);
errno = ENOMEM;
return NULL;
}
- 为
modbus_rtu_t
类型的结构体分配内存,modbus_rtu_t
是与 RTU 相关的具体实现数据结构。 - 如果分配失败,则释放已经分配的
ctx
内存,并返回NULL
。
4. 设备字符串分配与初始化:
ctx_rtu = (modbus_rtu_t *) ctx->backend_data;
ctx_rtu->device = (char *) malloc((strlen(device) + 1) * sizeof(char));
if (ctx_rtu->device == NULL) {
modbus_free(ctx);
errno = ENOMEM;
return NULL;
}
- 为设备名(如串口设备路径)分配内存,并检查是否分配成功。
#if defined(_WIN32)
strcpy_s(ctx_rtu->device, strlen(device) + 1, device);
#else
strcpy(ctx_rtu->device, device);
#endif
- 根据平台(Windows 或其他平台),使用不同的方式复制设备名字符串。
5. 配置 RTU 参数:
ctx_rtu->baud = baud;
- 设置波特率。
if (parity == 'N' || parity == 'E' || parity == 'O') {
ctx_rtu->parity = parity;
} else {
modbus_free(ctx);
errno = EINVAL;
return NULL;
}
- 检查传入的奇偶校验位是否合法。如果不合法(不是
'N'
、'E'
或'O'
),则释放内存并返回NULL
。
ctx_rtu->data_bit = data_bit;
ctx_rtu->stop_bit = stop_bit;
- 设置数据位和停止位。
6. 其他 RTU 配置:
#if HAVE_DECL_TIOCSRS485
ctx_rtu->serial_mode = MODBUS_RTU_RS232;
#endif
- 如果支持 RS485,设置串口模式。默认情况下使用 RS232 模式。
#if HAVE_DECL_TIOCM_RTS
ctx_rtu->rts = MODBUS_RTU_RTS_NONE;
ctx_rtu->onebyte_time =
1000000 * (1 + data_bit + (parity == 'N' ? 0 : 1) + stop_bit) / baud;
ctx_rtu->set_rts = _modbus_rtu_ioctl_rts;
ctx_rtu->rts_delay = ctx_rtu->onebyte_time;
#endif
- 配置 RTS(请求发送)信号的行为。
- 计算每个字节传输所需的时间(以微秒为单位)。
- 配置用于设置 RTS 的内部函数和延迟。
ctx_rtu->confirmation_to_ignore = FALSE;
- 设置确认消息的忽略标志。
7. 返回 modbus_t
上下文:
return ctx;
- 返回创建并初始化完成的
modbus_t
上下文指针。
总结:
该函数 modbus_new_rtu
创建并初始化了一个 Modbus RTU 的上下文结构,主要执行以下操作:
- 验证输入参数。
- 动态分配内存给 Modbus RTU 相关的结构体。
- 初始化 RTU 的设备名称、波特率、奇偶校验、数据位、停止位等参数。
- 配置 RS485 和 RTS 信号控制(如果适用)。
- 返回指向
modbus_t
结构的指针,以供后续 Modbus RTU 通信使用。
这种设计使得 modbus_new_rtu
函数能够创建一个专用于 Modbus RTU 的上下文对象,并将必要的硬件和通信参数封装到其中,为后续的 Modbus 通信操作做好准备。
二、modbus_set_slave
modbus_set_slave
函数的作用是设置 Modbus 通信中的从设备(Slave)的地址。它接收一个 modbus_t
类型的上下文指针(ctx
)和一个整数类型的从设备地址(slave
),然后调用具体 Modbus 后端的 set_slave
方法来设置从设备地址。下面我们逐步分析代码:
函数声明:
int modbus_set_slave(modbus_t *ctx, int slave)
- 返回类型:
int
,表示操作是否成功,通常0
表示成功,-1
表示失败。 - 参数:
ctx
:指向 Modbus 上下文的指针,表示当前的 Modbus 会话。slave
:要设置的从设备地址,通常是一个整数(例如 1 到 247),用于标识 Modbus 网络中的从设备。
代码分析:
if (ctx == NULL) {
errno = EINVAL;
return -1;
}
- 检查
ctx
是否为NULL
:- 如果
ctx
为NULL
,说明传入的 Modbus 上下文无效,无法进行后续的操作。 - 使用
errno = EINVAL
设置错误码为EINVAL
(无效的参数错误),并返回-1
表示函数失败。
- 如果
return ctx->backend->set_slave(ctx, slave);
- 如果
ctx
不为NULL
,则调用ctx->backend->set_slave
方法来设置从设备地址。ctx->backend
是一个指向 Modbus 后端实现结构的指针。后端可能是 RTU 或 TCP 等不同的 Modbus 实现,它包含了不同的实现细节。set_slave
是一个在后端实现中定义的函数,用来设置从设备的地址。通过ctx->backend->set_slave
调用该函数,传入ctx
和slave
参数,设置从设备地址。
总结:
- 功能:
modbus_set_slave
函数的主要作用是设置 Modbus 协议中的从设备地址,通常用于后续的通信。 - 步骤:
- 函数首先检查传入的
ctx
是否为NULL
,如果是,则返回错误。 - 如果
ctx
是有效的,调用 Modbus 后端的set_slave
函数来设置从设备地址。
- 函数首先检查传入的
- 扩展:具体的
set_slave
实现会依赖于 Modbus 后端类型(例如 RTU 或 TCP)。后端的set_slave
函数通常负责存储或处理从设备地址的设置。
三、modbus_connect
modbus_rtu_connect
函数
static int _modbus_rtu_connect(modbus_t *ctx)
{
struct termios tios;
int flags;
speed_t speed;
modbus_rtu_t *ctx_rtu = ctx->backend_data;
- 变量定义:
struct termios tios
:用于保存和设置串行端口的配置参数。int flags
:用于保存打开串行端口时的标志。speed_t speed
:用于保存串行通信的波特率。modbus_rtu_t *ctx_rtu
:指向 Modbus RTU 后端数据结构的指针,包含与 RTU 设备通信相关的参数。
if (ctx->debug) {
printf("Opening %s at %d bauds (%c, %d, %d)\n",
ctx_rtu->device,
ctx_rtu->baud,
ctx_rtu->parity,
ctx_rtu->data_bit,
ctx_rtu->stop_bit);
}
- 如果
ctx->debug
为true
,打印串口打开的调试信息,包括设备名、波特率、校验类型、数据位数和停止位。
flags = O_RDWR | O_NOCTTY | O_NDELAY | O_EXCL;
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
- 串口配置标志:
O_RDWR
:以读写模式打开文件。O_NOCTTY
:不将程序设置为控制终端。O_NDELAY
:打开时不等待连接建立,允许继续执行。O_EXCL
:以排他模式打开设备,确保不会被其他进程使用。O_CLOEXEC
:在执行exec
时关闭文件描述符(在支持的平台上)。
ctx->s = open(ctx_rtu->device, flags);
if (ctx->s < 0) {
if (ctx->debug) {
fprintf(stderr,
"ERROR Can't open the device %s (%s)\n",
ctx_rtu->device,
strerror(errno));
}
return -1;
}
- 打开串口设备:使用
open
系统调用打开设备文件。如果打开失败,输出错误信息并返回-1
。
tcgetattr(ctx->s, &ctx_rtu->old_tios);
memset(&tios, 0, sizeof(struct termios));
- 保存原始的串口配置:使用
tcgetattr
获取当前串口的设置,保存在ctx_rtu->old_tios
中。 - 清空新的串口配置:将
tios
结构体初始化为全零,准备设置新的串口参数。
if (9600 == B9600) {
speed = ctx_rtu->baud;
} else {
speed = _get_termios_speed(ctx_rtu->baud, ctx->debug);
}
- 设置波特率:根据
ctx_rtu->baud
设置串口的波特率,如果是在 9600 波特率的情况下,直接使用B9600
,否则调用_get_termios_speed
函数获取对应的波特率常量。
if ((cfsetispeed(&tios, speed) < 0) || (cfsetospeed(&tios, speed) < 0)) {
close(ctx->s);
ctx->s = -1;
return -1;
}
- 设置输入和输出的波特率。如果设置失败,关闭串口并返回
-1
。
tios.c_cflag |= (CREAD | CLOCAL);
tios.c_cflag &= ~CSIZE;
- 设置控制标志:
CREAD
:使能接收功能。CLOCAL
:本地连接,不让系统将串口设置为控制终端。CSIZE
:掩码,表示数据位数。
switch (ctx_rtu->data_bit) {
case 5:
tios.c_cflag |= CS5;
break;
case 6:
tios.c_cflag |= CS6;
break;
case 7:
tios.c_cflag |= CS7;
break;
case 8:
default:
tios.c_cflag |= CS8;
break;
}
- 设置数据位(5、6、7 或 8 位),根据
ctx_rtu->data_bit
选择合适的设置。
if (ctx_rtu->stop_bit == 1)
tios.c_cflag &= ~CSTOPB;
else /* 2 */
tios.c_cflag |= CSTOPB;
- 设置停止位(1 或 2 位)。
if (ctx_rtu->parity == 'N') {
tios.c_cflag &= ~PARENB;
} else if (ctx_rtu->parity == 'E') {
tios.c_cflag |= PARENB;
tios.c_cflag &= ~PARODD;
} else {
tios.c_cflag |= PARENB;
tios.c_cflag |= PARODD;
}
- 设置校验位:
'N'
:无校验,PARENB
被清除。'E'
:偶校验,PARENB
设置,PARODD
清除。'O'
:奇校验,PARENB
和PARODD
都设置。
tios.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
- 设置输入模式:禁用规范输入模式(
ICANON
)、回显(ECHO
)、和信号处理(ISIG
)。
if (ctx_rtu->parity == 'N') {
tios.c_iflag &= ~INPCK;
} else {
tios.c_iflag |= INPCK;
}
- 设置输入校验:
- 无校验时,
INPCK
清除。 - 有校验时,
INPCK
设置。
- 无校验时,
tios.c_iflag &= ~(IXON | IXOFF | IXANY);
tios.c_oflag &= ~OPOST;
- 禁用软件流控制(
IXON
,IXOFF
,IXANY
)并设置原始输出模式(OPOST
)。
tios.c_cc[VMIN] = 0;
tios.c_cc[VTIME] = 0;
- 设置最小读取字符数为
0
,并禁用读取超时(VMIN
和VTIME
)。
if (tcsetattr(ctx->s, TCSANOW, &tios) < 0) {
close(ctx->s);
ctx->s = -1;
return -1;
}
- 使用
tcsetattr
设置串口配置。如果设置失败,关闭串口并返回-1
。
return 0;
}
- 如果一切设置成功,返回
0
,表示连接成功。
modbus_rtu_is_connected
函数
static unsigned int _modbus_rtu_is_connected(modbus_t *ctx)
{
#if defined(_WIN32)
modbus_rtu_t *ctx_rtu = ctx->backend_data;
return ctx_rtu->w_ser.fd != INVALID_HANDLE_VALUE;
#else
return ctx->s >= 0;
#endif
}
- 功能:检查 RTU 连接是否成功。
- 在 Windows 上,检查文件描述符是否有效(
fd != INVALID_HANDLE_VALUE
)。 - 在其他系统(如 Linux),检查串口文件描述符
ctx->s
是否大于或等于0
(表示打开成功)。
- 在 Windows 上,检查文件描述符是否有效(
总结:
modbus_rtu_connect
函数设置并打开 Modbus RTU 的串口设备,配置波特率、数据位、停止位、校验等参数,确保通信能够正常进行。modbus_rtu_is_connected
函数用于检查 RTU 连接是否成功。
四、modbus_write_bit
modbus_write_bit
函数用于将单个线圈(coil)位(0 或 1)写入 Modbus 从设备。我们一步步分析这个函数以及它的调用关系:
1. modbus_write_bit
函数:
-
函数的三个参数:
ctx
:指向modbus_t
结构体的指针,表示 Modbus 上下文(包括连接等信息)。addr
:目标线圈的地址(即要写入的寄存器地址)。status
:要写入的状态,0 或 1。
-
函数内部:
- 首先检查
ctx
是否为NULL
。如果是NULL
,则设置errno
为EINVAL
(无效参数),并返回-1
,表示出错。 - 否则,调用
write_single
函数来实际执行写入操作。
- 首先检查
2. write_single
函数:
-
write_single
是实现写操作的核心函数,它接受以下参数:ctx
:Modbus 上下文。function
:Modbus 功能码,这里是写单个线圈的功能码MODBUS_FC_WRITE_SINGLE_COIL
。addr
:寄存器地址。value
:要写入的值,这里是status ? 0xFF00 : 0
。如果status
为 1,则写入0xFF00
,否则写入0x00
。
-
函数内部:
- 调用
ctx->backend->build_request_basis
构建请求报文,req
数组存储构建的请求消息,req_length
存储消息长度。 - 调用
send_msg
函数发送请求消息,并返回响应。如果发送成功,继续接收并处理响应消息。 - 如果接收到的响应消息有效,则调用
check_confirmation
验证确认消息。 - 最终返回执行结果。
- 调用
3. send_msg
函数:
send_msg
负责将构建的消息通过串口或其他传输方式发送出去。该函数执行如下步骤:- 先通过
ctx->backend->send_msg_pre
对消息进行预处理(这可能与 Modbus 的协议实现或底层操作系统的设置有关)。 - 如果启用了调试(
ctx->debug
),则打印发送的消息。 - 然后,进入一个循环,调用
ctx->backend->send
发送消息。如果发送失败,根据错误类型决定是否进行错误恢复操作(如重连或清理)。 - 如果消息发送成功,并且实际发送的字节数与期望的字节数不匹配,则设置
errno
为EMBBADDATA
,表示数据错误。
- 先通过
4. _modbus_receive_msg
和 check_confirmation
函数:
- 如果消息发送成功,接下来需要接收响应。
_modbus_receive_msg
用于接收来自从设备的响应。 - 接收到响应后,
check_confirmation
函数用于检查收到的响应是否有效,并与请求进行比较,确保通信正确。
总结:
modbus_write_bit
函数调用了write_single
,后者负责构建请求并发送给从设备。- 在发送消息后,系统会等待并验证从设备的确认响应。
- 如果发生错误(如设备断开连接),
send_msg
会尝试进行重连或错误恢复。
整个流程实现了对 Modbus 从设备的单个线圈写操作。通过发送和接收 Modbus 消息,确保请求成功执行。