TA的每日心情 | 难过 2021-2-27 22:16 |
---|
签到天数: 1568 天 连续签到: 1 天 [LV.Master]伴坛终老
|
【EVB-335X-II】之基于libmodbus库的Modbus-RTU从站的C/S架构软件开发(QT多线程监听服务)
介绍了如何在将libmodbus TCP库应用在EVB-335X-II开发板上,实现了以树莓派3B为主机,EVB-335X-II为从机,由EVB-335X-II开发板为树莓派提供服务的例程。
在一些工业应用中,有些场合是不适合使用以太网的,多数是采用RS485电气接口,结合Modbus RTU通讯协议实现对底层数据采集器的联网。这篇试用报告我们将介绍如何在EVB-335X-II开发板上应用libmodbus库的RTU功能模块,实现EVB-335X-II开发板作为服务提供端,为上层应用提供底层采集到的数据。
相对于上一篇使用报告,由于我们没有采用多线程技术,在EVB-335X-II开发板上启动RTU服务后,整个GUI处于for循环中,无法接收用户的其他操作,服务次数也无法动态显示,只有在Modbus主机结束连接后,才显示出EVB-335X-II到底服务了几次。为了解决上述存在的问题,这篇报告中,我们设计一个多线程类,由该类去承载Modbus RTU服务,使得服务监听与主界面GUI并行执行,达到动态显示服务次数的目的。
1. 硬件搭建
由于树莓派3B的本身特点,蓝牙模块与串口的冲突,这次实验我采用友善之臂的NanoPi M2作为Modbus RTU主机,硬件连接原理如图所示:
树莓派UART3对应的驱动未/dev/ttyAMA2。在EVB-335X-II端,由于串口0用于调试作用,串口2-4即可用于TTL,也可用于RS232,而串口1只支持RS232,为了调试方便,我这里就直接选用串口1,串口1的驱动为:/dev/ttyO1
硬件连接如图所示:
NanoPi M2侧:
EVB-335X-II侧:
2. EVB-335X-II侧libmodbus rtu服务功能程序
1)服务代码
我们只需要将上一篇试用报告中的TCP换成RTU,并设置串口的驱动程序、波特率、数据位、停止位和奇偶校验位,以及设置RS232还是RS485模式。
代码如下:
int s = -1;
modbus_t *ctx;
modbus_mapping_t *mb_mapping;
int rc;
int i;
int use_backend;
uint8_t *query;
int header_length;
int service_count = 0;
use_backend = RTU;
if (use_backend == TCP) {
ctx = modbus_new_tcp("192.168.1.104", 1502);
query = (uint8_t *)malloc(MODBUS_TCP_MAX_ADU_LENGTH);
}else if (use_backend == TCP_PI) {
ctx = modbus_new_tcp_pi("::0", "1502");
query = (uint8_t *)malloc(MODBUS_TCP_MAX_ADU_LENGTH);
}else {
ctx = modbus_new_rtu("/dev/ttyO1",9600, 'N', 8, 1);
modbus_set_slave(ctx,SERVER_ID);
modbus_rtu_set_serial_mode(ctx,MODBUS_RTU_RS232);
query = (uint8_t*)malloc(MODBUS_RTU_MAX_ADU_LENGTH);
}
header_length= modbus_get_header_length(ctx);
modbus_set_debug(ctx, TRUE);
mb_mapping = modbus_mapping_new_start_address(
UT_BITS_ADDRESS, UT_BITS_NB,
UT_INPUT_BITS_ADDRESS, UT_INPUT_BITS_NB,
UT_REGISTERS_ADDRESS, UT_REGISTERS_NB_MAX,
UT_INPUT_REGISTERS_ADDRESS, UT_INPUT_REGISTERS_NB);
if (mb_mapping == NULL) {
fprintf(stderr, "Failed to allocate the mapping: %s\n",
modbus_strerror(errno));
modbus_free(ctx);
return; //-1;
}
/* Examples from PI_MODBUS_300.pdf.
Only the read-only input values are assigned. */
/* Initialize input values that's can be only done server side. */
modbus_set_bits_from_bytes(mb_mapping->tab_input_bits, 0,UT_INPUT_BITS_NB,
UT_INPUT_BITS_TAB);
for (i=0; i < UT_REGISTERS_NB; i++) {
mb_mapping->tab_registers = UT_REGISTERS_TAB;;
}
/* Initialize values of INPUT REGISTERS */
for (i=0; i < UT_INPUT_REGISTERS_NB; i++) {
mb_mapping->tab_input_registers = UT_INPUT_REGISTERS_TAB;
}
if (use_backend == TCP) {
s = modbus_tcp_listen(ctx, 1);
modbus_tcp_accept(ctx, &s);
}else if (use_backend == TCP_PI) {
s = modbus_tcp_pi_listen(ctx, 1);
modbus_tcp_pi_accept(ctx, &s);
}else {
rc = modbus_connect(ctx);
if (rc == -1) {
fprintf(stderr, "Unable to connect %s\n", modbus_strerror(errno));
modbus_free(ctx);
return; //-1;
}
}
for (;;) {
do {
rc = modbus_receive(ctx, query);
/* Filtered queries return 0 */
} while (rc == 0);
/* The connection is not closed on errors which require on reply such as
bad CRC in RTU. */
if (rc == -1 && errno != EMBBADCRC) {
/* Quit */
break;
}
/* Special server behavior to test client */
if (query[header_length] == 0x03) {
/* Read holding registers */
if (MODBUS_GET_INT16_FROM_INT8(query, header_length + 3)
== UT_REGISTERS_NB_SPECIAL) {
printf("Set an incorrectnumber of values\n");
MODBUS_SET_INT16_TO_INT8(query,header_length + 3,
UT_REGISTERS_NB_SPECIAL - 1);
} else if (MODBUS_GET_INT16_FROM_INT8(query, header_length + 1)
== UT_REGISTERS_ADDRESS_SPECIAL){
printf("Reply to thisspecial register address by an exception\n");
modbus_reply_exception(ctx,query,
MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY);
continue;
} else if (MODBUS_GET_INT16_FROM_INT8(query, header_length + 1)
==UT_REGISTERS_ADDRESS_INVALID_TID_OR_SLAVE) {
const int RAW_REQ_LENGTH = 5;
uint8_t raw_req[] = {
(use_backend == RTU) ? INVALID_SERVER_ID :0xFF,
0x03,
0x02, 0x00, 0x00
};
printf("Reply with aninvalid TID or slave\n");
modbus_send_raw_request(ctx,raw_req, RAW_REQ_LENGTH * sizeof(uint8_t));
continue;
} else if (MODBUS_GET_INT16_FROM_INT8(query, header_length + 1)
==UT_REGISTERS_ADDRESS_SLEEP_500_MS) {
printf("Sleep 0.5 s beforereplying\n");
usleep(500000);
} else if (MODBUS_GET_INT16_FROM_INT8(query, header_length + 1)
==UT_REGISTERS_ADDRESS_BYTE_SLEEP_5_MS) {
/* Test low level onlyavailable in TCP mode */
/* Catch the reply and sendreply byte a byte */
uint8_t req[] ="\x00\x1C\x00\x00\x00\x05\xFF\x03\x02\x00\x00";
int req_length = 11;
int w_s =modbus_get_socket(ctx);
if (w_s == -1) {
fprintf(stderr,"Unable to get a valid socket in special test\n");
continue;
}
/* Copy TID */
req[1] = query[1];
for (i=0; i < req_length;i++) {
printf("(%.2X)", req);
usleep(5000);
rc = send(w_s, (constchar*)(req + i), 1, MSG_NOSIGNAL);
if (rc == -1) {
break;
}
}
continue;
}
}
service_count++;
le->setText(QString::number(service_count));
rc = modbus_reply(ctx, query, rc, mb_mapping);
if (rc == -1) {
break;
}
}
printf("Quit the loop: %s\n", modbus_strerror(errno));
if (use_backend == TCP) {
if (s != -1) {
//close(s);
}
}
modbus_mapping_free(mb_mapping);
free(query);
/* For RTU */
modbus_close(ctx);
modbus_free(ctx);
2)QT多线程
创建一个Mythread线程类,该类继承自QThread,在类定义文件中设计一个指向QLineEdit空间的指针,创建该类时,通过构造函数使QLineEdit指针指向QWidget界面中的实际创建的QLineEdit控件,使得在新创建的现场函数中能够动态调整系统的“服务次数”。
线程类的头文件:
线程的cpp文件,我们在构造函数中设置QLineEdit控件指针,在run函数中设置Modbus服务函数。
构造函数如下所示:
run函数的代码为我们上面贴的关键代码(完整代码见附件,完成的工程文件)
3)多线变量定义与启动
我们在GUI的主窗口类中定义线程变量,在构造函数中创建线程,在启动服务的按钮处理函数中启动线程。
主界面类定义:
构造函数与线程启动函数:
4)波特率
在测试过程中,当波特率设置为115200时,通讯无法连接,可能是我的连线较长,也可能是开发板的驱动还不够稳定,在测试注意将波特率设置为19200或9600,波特率低一些对实际应用影响不大,因为通常,工业应用中,通讯距离较远,9600是最常用的速度参数。
3. NanoPi M2侧Modbus主机的程序设计
Modbus上下文对象定义,以及通讯连接函数如图所示:
Modbus读寄存器函数:
Modbus写寄存器函数:
在读写函数中的,352为寄存器的起始地址,其16进制为0x160,对于与Modbus RTU服务程序unit-test.h文件中,如图所示:
4.上电测试
1)EVB-335X-II上电
给EVB-335X-II开发板上电,执行命令:
ifconfig eth0 192.168.1.112
mount -t nfs 192.168.1.109:/nfsshare /mnt-o nolock
cd /mnt
./libmodbustest_rtu
命令执行结果如图所示:
点击服务启动按钮,启动服务。
2)NanoPi M2上电
给NanoPi M2上电,编辑代码,设置串口驱动程序,及通讯参数:
执行命令设置ttyAMA2的权限:
chmod 777 /dev/ttyAMA2
编译Modbus主机程序,运行程序:
3)测试程序
在NanoPi M2上点击读取寄存器按钮,执行结果如图所示:
说明我们成功的读取了3个数据,数据内容如图所示:
连续再执行两次。然后我们点击将数据写入寄存器按钮,执行结果如图所示:
我们再次点击读取寄存器数据,结果如图所示:
说明,我们成功的修改了服务器端寄存器中保存的数据。
我们在看看服务器端记录的服务次数:
在操作的过程中,我们可以发现,服务次数编辑框中的数字是在动态变化的,说明我们的基于QT GUI的多线程设计也是成功的。
5. 小结
通过上述的程序设计,我们实现了基于libmodbus的RTU通讯主、从功能,并且较好的利用了QThread的多线程编程模型,使得Modbus RTU服务监听功能与QT主界面线程并行执行,改善了用户QT GUI界面的操作体验。
|
|