libmodbus:写一个modbusTCP服务

初级代码游戏的专栏介绍与文章目录-CSDN博客

我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。

这些代码大部分以Linux为目标但部分代码是纯C++的,可以在任何平台上使用。

源码指引:github源码指引_初级代码游戏的博客-CSDN博客


libmodbus很好用,不过多是写客户端。为了测试客户端,一般会用物理设备或模拟程序,不过既然libmodbus支持写服务端,为什么不直接写一个服务端用来测试呢?(串口当然可能受数量限制,TCP就没有任何限制了)

目录

一、主要过程

[1.1 创建上下文对象,设定参数](#1.1 创建上下文对象,设定参数)

[1.1.1 坑:Ubuntu上无法打开低端口](#1.1.1 坑:Ubuntu上无法打开低端口)

[1.2 数据映射](#1.2 数据映射)

[1.3 启动服务](#1.3 启动服务)

[1.4 接受连接](#1.4 接受连接)

[1.5 接收请求](#1.5 接收请求)

[1.6 返回应答](#1.6 返回应答)

[1.7 清理](#1.7 清理)

二、完整代码

三、处理多个连接


一、主要过程

1.1 创建上下文对象,设定参数

cpp 复制代码
MODBUS_API modbus_t* modbus_new_tcp(const char *ip_address, int port);

非常简单,指定地址端口就可以了。地址NULL则使用任何地址,标准端口是502。

1.1.1 坑:Ubuntu上无法打开低端口

这个坑好大,我试了好久程序都不正确,在后面modbus_receive的时候挂了,开始以为是内存错误,后来老老实实每步检查返回值才发现是modbus_tcp_listen这一步就失败了,提示"无权操作",用了su也不行,于是想到会不会是低端口保护,改成高端口就正常了(比如10502)。

低端口0-1023由国际组织分配,Ubuntu限制应用程序不能使用是可以理解的。

1.2 数据映射

cpp 复制代码
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;

MODBUS_API modbus_mapping_t* modbus_mapping_new(int nb_bits, int nb_input_bits,
                                                int nb_registers, int nb_input_registers);

根据给定的四种数据的数量创建存储结构,返回的结构里面对每种数据都包含三个值:

  1. 数据个数,最大数据量
  2. 起始modbus地址,数据对应的modbus地址可以不从0开始,比如只提供【100-120】
  3. 数据指针,存储实际数据,可以根据需要直接修改每个数据的值(但是不要动这个指针,这是内部创建的,用另一个函数释放)

1.3 启动服务

cpp 复制代码
MODBUS_API int modbus_tcp_listen(modbus_t *ctx, int nb_connection);

这会根据之前设置的参数来启动服务,nb_connection是一般TCP编程里面的等待连接队列长度。

返回值是服务socket的值,如果成功返回值应该大于0。服务端口要自行用close来关闭。

1.4 接受连接

cpp 复制代码
MODBUS_API int modbus_tcp_accept(modbus_t *ctx, int *s);

这一步的参数s就是前一步的返回值,也就是服务socket。

返回值是新socket,同时新socket也会存储在上下文中,后续收发操作使用上下文中存储的socket。

1.5 接收请求

cpp 复制代码
MODBUS_API int modbus_receive(modbus_t *ctx, uint8_t *req);

这个函数接收一个请求并存储在req里面,返回值是数据长度:

  • 大于0 有效的请求
  • 等于0 忽略的请求,比如从站号不匹配(本例程并未设置从站号)
  • -1 出错

循环调用此函数接受请求,并可以在接收之后进行一些处理,然后再发送应答。

1.6 返回应答

cpp 复制代码
MODBUS_API int modbus_reply(modbus_t *ctx, const uint8_t *req,
                            int req_length, modbus_mapping_t *mb_mapping);

如果没什么别的要求,直接调用这个函数返回应答就可以了。调用之前可以修改数据映射的数据。

1.7 清理

cpp 复制代码
				if (s != -1)
				{
					close(s);
				}

			modbus_mapping_free(mb_mapping);
			modbus_close(ctx);
			modbus_free(ctx);

服务端口需要关闭,数据映射和上下文需要释放。

二、完整代码

cpp 复制代码
			modbus_t * ctx = modbus_new_tcp(NULL, 10503);//ubuntu上开启低端口会报权限不足,su也不行
			modbus_mapping_t * mb_mapping = modbus_mapping_new(100, 100, 100, 100);
			if (mb_mapping == NULL)
			{
				fprintf(stderr, "Failed to allocate the mapping: %s\n", modbus_strerror(errno));
				modbus_free(ctx);
				return -1;
			}
			//设置初值
			{
				mb_mapping->start_registers = 0;
				for (int i = 0; i < 10; ++i)
				{
					mb_mapping->tab_registers[mb_mapping->start_registers + i] = i;
				}
			}
			while (CMyProcess::isProcessLive(parent_pid))
			{
				int s = modbus_tcp_listen(ctx, 5);
				if (s < 0)
				{
					thelog << "modbus_tcp_listen error : " << modbus_strerror(errno) << endi;
					SleepSeconds(1);
					continue;
				}
				modbus_tcp_accept(ctx, &s); 
				thelog << "s:" << s << endi;
				while (CMyProcess::isProcessLive(parent_pid))
				{
					uint8_t query[512];
					int rc = modbus_receive(ctx, query);
					if (rc > 0)
					{
						modbus_reply(ctx, query, rc, mb_mapping);
					}
					else if (rc == -1)
					{
						break;
					}
					//改变数据
					for (int i = 0; i < 10; ++i)
					{
						++mb_mapping->tab_registers[mb_mapping->start_registers + i];
					}
				}
				thelog << "对方断开或出错 " << modbus_strerror(errno) << endi;
				if (s != -1)
				{
					close(s);
				}
			}
			modbus_mapping_free(mb_mapping);
			modbus_close(ctx);
			modbus_free(ctx);

CMyProcess::isProcessLive(parent_pid)判断父进程是否存在,换成死循环就可以了。

专门对保持寄存器的前十个值做了设置,因为测试只用了这几个值。

一次只能处理一个连接,这个连接断开才会处理下一个连接。因为客户socket是存储在上下文的,所以并行处理多个连接不方便。实际上写这个代码的目的是程序连接到自身来进行回归测试的。

三、处理多个连接

额外有个函数modbus_set_socket用来改变上下文中保存的客户连接,可以接受多个连接,然后用select来选择可以操作的连接,然后先设置modbus_set_socket再modbus_receive。

因为我没有试,所以没有示例代码。


(这里是文档结束)

相关推荐
serene13126 天前
Modbus新手教程
物联网·mqtt·教程·modbus·工业网关·iec104·iec101
韭菜钟10 天前
康耐视智能相机(Insight)通过ModbusTCP发送字符串到倍福(BECKHOFF)PLC中
modbus·智能相机·康耐视
雨雪飘零1 个月前
Linux系统制作Java JAR程序为系统服务
java·linux·jar·开机启动·服务·daemon
Amd7941 个月前
Nuxt.js 应用中的 schema:written 事件钩子详解
生命周期·vite·配置·日志·nuxt·服务·钩子
橘色的喵1 个月前
工业通信协议对比:OPC-UA、Modbus、MQTT、HTTP
mqtt·网络协议·http·modbus·opc-ua·工业协议
ggtc2 个月前
通过串口与ModBus硬件设备通信
modbus·硬件
小李飞刀李寻欢2 个月前
vm.max_map_count 表示啥意思啊?通俗易懂点,有单位么?262144表示啥意思?
jvm·elasticsearch·内存·map·es·服务
老菜鸟的每一天2 个月前
Qt Modbus 2 通信实现
qt·modbus·rtu
萧技电创EIIA2 个月前
Modbus TCP 西门子PLC指令以太口地址配置以及 Poll Slave调试软件地址配置
服务器·网络·modbus
serene942 个月前
第二章:信息建模:对象和变量的类型2
物联网·mqtt·modbus·opc·opc ua·iec 104