Libmodbus关于从站地址的问题

在嵌入式LINUX开发板上,基于libmodbus第三方库编程实现土壤水分及温度的读取,传感器采用大连祺峰科技有限公司的土壤水分温度传感器(型号:SMTS-II-485)。在程序运行过程中,遇到了一些问题,记录如下:

查看土壤水分温度传感器手册,若要读取水分温度,则需要发送如下RTU帧:FE 03 00 00 00 02 D0 04,这里FE是传感器modbus站地址,这是传感器出厂默认地址。在libmodbus程序中,程序运行到modbus_set_slave(ctx,0xFE)时,程序报错并终止运行,为了查清原因,查看libmodbus源码中modbus_set_slave()函数的实现。

在Modbus.c文件中,可以看到modbus_set_slave()函数的实现如下:
 
  1. /* Define the slave number */
  2. int modbus_set_slave(modbus_t *ctx, int slave)
  3. {
  4.     return ctx->backend->set_slave(ctx, slave);
  5. }
这个函数执行的是 ctx - > backend - > set_slave ( ctx ,  slave ),ctx是一个modbus_t类型的结构体,定义在Modbus.h中
 
  1. typedef struct _modbus modbus_t;
在Modbus-Private.h中有_modbus的具体定义
 
  1. struct _modbus {
  2.     /* Slave address */
  3.     int slave;
  4.     /* Socket or file descriptor */
  5.     int s;
  6.     int debug;
  7.     int error_recovery;
  8.     struct timeval response_timeout;
  9.     struct timeval byte_timeout;
  10.     const modbus_backend_t *backend;
  11.     void *backend_data;
  12. };
可以看到成员backend是一个modbus_backend_t,继续追踪modbus_backend_t,定义在Modbus-Private.h中
 
  1. typedef struct _modbus_backend {
  2.     unsigned int backend_type;
  3.     unsigned int header_length;
  4.     unsigned int checksum_length;
  5.     unsigned int max_adu_length;
  6.     int (*set_slave) (modbus_t *ctx, int slave);
  7.     int (*build_request_basis) (modbus_t *ctx, int function, int addr,
  8.                                 int nb, uint8_t *req);
  9.     int (*build_response_basis) (sft_t *sft, uint8_t *rsp);
  10.     int (*prepare_response_tid) (const uint8_t *req, int *req_length);
  11.     int (*send_msg_pre) (uint8_t *req, int req_length);
  12.     ssize_t (*send) (modbus_t *ctx, const uint8_t *req, int req_length);
  13.     ssize_t (*recv) (modbus_t *ctx, uint8_t *rsp, int rsp_length);
  14.     int (*check_integrity) (modbus_t *ctx, uint8_t *msg,
  15.                             const int msg_length);
  16.     int (*pre_check_confirmation) (modbus_t *ctx, const uint8_t *req,
  17.                                    const uint8_t *rsp, int rsp_length);
  18.     int (*connect) (modbus_t *ctx);
  19.     void (*close) (modbus_t *ctx);
  20.     int (*flush) (modbus_t *ctx);
  21.     int (*select) (modbus_t *ctx, fd_set *rfds, struct timeval *tv, int msg_length);
  22.     int (*filter_request) (modbus_t *ctx, int slave);
  23. } modbus_backend_t;
这个结构体中除了前面几个变量成员外,剩下的都是一些函数指针,这些函数指针用来对ctx进行操作。其中看到第一个函数指针 int   ( * set_slave )   ( modbus_t  * ctx ,   int  slave ) ;就是用来设置从站地址的。那么这个函数的具体实现在哪里?

在Modubs-rtu.c文件中,有函数modbus_new_rtu()的具体实现,这个函数用来创建一个modbus RTU通信的context(可以理解为一个标识符),这个函数的源码如下:
 
  1. modbus_t* modbus_new_rtu(const char *device,
  2.                          int baud, char parity, int data_bit,
  3.                          int stop_bit)
  4. {
  5.     modbus_t *ctx;
  6.     modbus_rtu_t *ctx_rtu;
  7.     size_t dest_size;
  8.     size_t ret_size;
  9.  
  10.     ctx = (modbus_t *) malloc(sizeof(modbus_t));
  11.     _modbus_init_common(ctx);
  12.  
  13.     ctx->backend = &_modbus_rtu_backend;
  14.     ctx->backend_data = (modbus_rtu_t *) malloc(sizeof(modbus_rtu_t));
  15.     ctx_rtu = (modbus_rtu_t *)ctx->backend_data;
  16.  
  17.     dest_size = sizeof(ctx_rtu->device);
  18.     ret_size = strlcpy(ctx_rtu->device, device, dest_size);
  19.     if (ret_size == 0) {
  20.         fprintf(stderr, "The device string is empty\n");
  21.         modbus_free(ctx);
  22.         errno = EINVAL;
  23.         return NULL;
  24.     }
  25.  
  26.     if (ret_size >= dest_size) {
  27.         fprintf(stderr, "The device string has been truncated\n");
  28.         modbus_free(ctx);
  29.         errno = EINVAL;
  30.         return NULL;
  31.     }
  32.  
  33.     ctx_rtu->baud = baud;
  34.     if (parity == 'N' || parity == 'E' || parity == 'O') {
  35.         ctx_rtu->parity = parity;
  36.     } else {
  37.         modbus_free(ctx);
  38.         errno = EINVAL;
  39.         return NULL;
  40.     }
  41.     ctx_rtu->data_bit = data_bit;
  42.     ctx_rtu->stop_bit = stop_bit;
  43.  
  44.     return ctx;
  45. }
这个函数中有很重要的一条语句:ctx->backend = &_modbus_rtu_backend;条语这条语句实际上就使用_modbus_rtu_backend对ctx中的backend成员进行初始化,因此当调用modbus_new_rtu()函数时,除了完成对串口的初始化之外,也完成了对ctx结构体中的成员进行初始化。那么_modbus_rtu_backend这个变量是在哪里定义的?追踪发下,在Modubs-rtu.c文件中有对_modbus_rtu_backend的定义:
 
  1. const modbus_backend_t _modbus_rtu_backend = {
  2.     _MODBUS_BACKEND_TYPE_RTU,
  3.     _MODBUS_RTU_HEADER_LENGTH,
  4.     _MODBUS_RTU_CHECKSUM_LENGTH,
  5.     MODBUS_RTU_MAX_ADU_LENGTH,
  6.     _modbus_set_slave,
  7.     _modbus_rtu_build_request_basis,
  8.     _modbus_rtu_build_response_basis,
  9.     _modbus_rtu_prepare_response_tid,
  10.     _modbus_rtu_send_msg_pre,
  11.     _modbus_rtu_send,
  12.     _modbus_rtu_recv,
  13.     _modbus_rtu_check_integrity,
  14.     NULL,
  15.     _modbus_rtu_connect,
  16.     _modbus_rtu_close,
  17.     _modbus_rtu_flush,
  18.     _modbus_rtu_select,
  19.     _modbus_rtu_filter_request
  20. }
这段代码对 modbus_backend_t类型的变量“_modbus_rtu_backend”进行了初始化。其中可以看到,modbus_set_slave()函数真正调用的是_modbus_set_slave这个函数。在Modbus-rtu.c文件中可以找到_modbus_set_slave()的函数定义:
 
  1. /* Define the slave ID of the remote device to talk in master mode or set the
  2.  * internal slave ID in slave mode */
  3. static int _modbus_set_slave(modbus_t *ctx, int slave)
  4. {
  5.     /* Broadcast address is 0 (MODBUS_BROADCAST_ADDRESS) */
  6.     if (slave >= 0 && slave <= 247) {
  7.         ctx->slave = slave;
  8.     } else {
  9.         errno = EINVAL;
  10.         return -1;
  11.     }
  12.  
  13.     return 0;
  14. }
阅读这个函数源码可以知道,modbus_set_slave(modbus_t *ctx, int slave)这个函数的作用是把ctx结构体中的slave成员设置为参数slave的值。该函数中规定,libmodbus中站地址的有效范围是0到247,其中0是广播地址。而所用传感器的默认出厂站地址是0xFE(254),超出了libmodbus中站地址的有效范围,因此导致对ctx的slave成员设置不成功。

实际上当使用modbus_new_rtu()函数创建ctx时,对ctx的各成员已经初始化了默认值。在modbus_new_rtu()函数中调用了函数_modbus_init_common(ctx);该函数用来对ctx成员进行初始化默认值。这个函数的实现在Modbus.c文件中:
 
  1. void _modbus_init_common(modbus_t *ctx)
  2. {
  3.     /* Slave and socket are initialized to -1 */
  4.     ctx->slave = -1;
  5.     ctx->s = -1;
  6.  
  7.     ctx->debug = FALSE;
  8.     ctx->error_recovery = MODBUS_ERROR_RECOVERY_NONE;
  9.  
  10.     ctx->response_timeout.tv_sec = 0;
  11.     ctx->response_timeout.tv_usec = _RESPONSE_TIMEOUT;
  12.  
  13.     ctx->byte_timeout.tv_sec = 0;
  14.     ctx->byte_timeout.tv_usec = _BYTE_TIMEOUT;
  15. }
这里可以看到ctx->slave = -1;也就是说ctx中slave默认值是-1。

那么执行modbus_set_slave(ctx,0xFE)函数产生的错误信息是哪里来的呢?在Modbus-rtu.c文件中有一个函数:
 
  1. /* Builds a RTU request header */
  2. static int _modbus_rtu_build_request_basis(modbus_t *ctx, int function,
  3.                                            int addr, int nb,
  4.                                            uint8_t *req)
  5. {
  6.     assert(ctx->slave != -1);
  7.     req[0] = ctx->slave;
  8.     req[1] = function;
  9.     req[2] = addr >> 8;
  10.     req[3] = addr & 0x00ff;
  11.     req[4] = nb >> 8;
  12.     req[5] = nb & 0x00ff;
  13.  
  14.     return _MODBUS_RTU_PRESET_REQ_LENGTH;
  15. }
这个函数用来构造modbus-RTU帧头(RTU帧的前6个字节,包括地址、功能码、寄存器地址、个数等信息),这个函数中调用了assert(ctx->slave != -1);LINUX下assert函数的用法是assert( int expression );其作用是先计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行

当执行modbus_set_slave(ctx,0xFE)函数时,由于0xFE不在libmodbus有效的站地址范围内(0~247),因此ctx结构体中的slave成员还是保持原来用_modbus_init_common(ctx)初始化的值,即-1,在构造modbus-RTU帧头的时候,运行到assert(ctx->slave != -1)就会报错,并终止程序运行。

解决方法就是把传感器的站地址修改为02(在0~247之内),再对传感器进行读取,成功。

附记:对大连祺峰科技有限公司的土壤水分温度传感器(型号:SMTS-II-485)修改站地址流程如下:
该传感器有5根线,其中有一个为屏蔽线(或SET线),若要对传感器执行写操作,那么必须把SET线接高电平。当SET线置高时,传感器站地址自动变为0xFF,查看传感器手册,站地址寄存器地址为0x200,因此需往传感器发送如下RTU指令:FF 06 02 00 00 02 1C 6D,传感器返回FF 06 02 00 00 02 1C 6D,说明地址修改成功,这里是把传感器地址修改为02。把SET重新接回到低电平,用02地址访问传感器,成功。

另外,在SMTS-II-485手册中提到,当SET接高电平时,传感器内部通讯参数自动变为为9600,N,8,2(波特率9600,无校验,8个数据位,2个停止位),实验发现并非如此。SET接高电平时,通信参数还是和读取时一样,即9600,N,8,1(波特率9600,无校验,8个数据位,1个停止位)。






 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值