分享

写出稳定的Modbus代码之点滴经验

 guitarhua 2018-11-21

1.引言

  Modbus是工业领域重要的协议,物理层有常见的RS485双绞线和TCP,所以又常说Modbus 485开发和Modbus TCP开发。

  前者就是串口通信,比较简单。后者涉及到网络协议,复杂度高出好几个层次。

  但是如果有稳定的TCP通信做铺垫,这两种Modbus的区别就不大了,都是数据包的解析而已,能共用大部分代码。

  本文不讨论Modbus协议如何读写一个Register或Coil之类的,这些东西看看文档或者网上搜下博客教程就知道了。

  本文目标是讨论如何写一个稳定的Modbus通信驱动,由于Modbus TCP对Modbus的操作除了有个特殊的7字节包头和无CRC外,其他部分和Modbus 485没有区别,因此本文对Modbus TCP同样有参考意义。

 

2. RS485通信

  部分应用场景下并没有使用Modbus协议,而是简单的串口通信,然后加上和校验等检查,传输层使用RS485,这类应用可以归为RS485通信的范畴。

  其实个人感觉,都做到这个份上,还不如直接用Modbus来做,复杂度高不出多少,好处却有很多。比如可以借助Modbus大量的流行测试工具做系统测试。可以把系统做成一个标准品,也利于客户使用和测试。另外有现成的通用协议,总比自己定的协议稳定度高。

  比如我曾见到过的几个公司的产品:

  这是一个电机控制器,使用RS485通信,但本质就是串口通信,加上了头0x4A,和结束符0D,0A,以及和校验。

  这是一家BMS产商的说明书,RS485通信,和上面一样,就是基于RS485的串口通信而已。

 

  这类驱动程序,需要从稳定性和易读性来考虑,如果稳定性较差会造成系统控制故障,如果易读性差就会造成难以维护,这些控制指令之间差别很小,如果一个一个单独写命令,非常容易出错。对应举措如下:

  1)避免每个指令写一部分代码,需要统一处理,比如校验函数,发送函数,接收函数等。

  通信协议已知,从中可以知道通信的实际数据长度(不包含包头包尾和校验的部分),所以可以控制读写多少个字节,并且可以知道什么时候启动校验,那些数据参与校验计算。

  2)为指令建立指令列表,这样后来需要添加功能,就可以直接把指令字加入列表即可。

  3)适当抽象。为每个指令的动作写回调函数,这样就可以在应用层,用一句简单的回调函数指针直接操作具体的动作函数,而不是应用逻辑层操作具体的驱动层面的接口。

  4)超时,必须有超时机制。通信失败怎么处理?通信了一般线断了怎么处理?不能让系统死等后面的几个字节发过来。

  5)新手和一些小公司经常会不注意的出现一个问题——不关闭通信端口。

  如果通信都是由你发起的,那么无论此次通信是完成还是超时或失败,都应该关闭通信端口。

  因为从合理的逻辑上来说,此刻之后都不应该再有数据过来打扰系统的工作。

复制代码
Motor_Send_Cmd(cmd_type);    // send command    
if(xQueueReceive(MotorQueue, &motorMsg, 200) == pdPASS)  // wait response
{
    check = Motor_Rcv_Check(&motorMsg, &motor_cmd_struct);
    if(check)
    {
    .....
    }
    else // check failed
    {
       Motor_RS485_Mode(UART_OFF);
    }
}
else // timeout
{
     Motor_RS485_Mode(UART_OFF);
}
复制代码

3. Modbus 驱动

  有了上面高质量的485串口通信的驱动,进行Modbus通信协议的改造就非常简单了。但是Modbus TCP接收部分没有帧间隔超时,因为都是由TCP协议来保证了。

  一个比较完善的Modbus驱动要注意以下几点:

  1)接收超时机制,不能依靠数传输的字节个数来停止接收来和区分帧间隔,因为可能通信就是断掉了,所以要按照协议,串口通信情况下3~5个bit空闲就认为一帧结束。

  2)响应超时机制,Modbus是主从问答式通信,那么主机就需要知道到底多久从机才会应答,主机等待从机应答的最长时间就是从机的最大回复间隔,超过这个时间后从机即使已经完成计算也不能回复,因为此时主机可能已经开始给其他从机发送数据了。

  3)Modbus地址可能很多,那么就需要一个table来管理,不能写成一个一个的 if--else if--else if来处理某个地址的操作。

  4)如果table管理了上千个地址,那么地址的搜索就需要一个高效的算法,顺序搜索肯定是太low了,最好使用二分查找。

  5)有些写寄存器可能需要对机器设定,比如在线改波特率,如果此时波特率和新设定的波特率一样,那么就不需要执行串口初始化代码,所以在每个地址table行,需要特定的回调函数,搜索到某一个地址后,就可以操作这个回调函数,执行一些动作。

  6)如果某个资源多个任务访问,需要读写互斥。

  7)如果某个资源读写不是原子性的,那么就需要加锁。免得改了一半,被其他任务来读出,结果读了一半新值一半旧值。

  8)模块化,读写寄存器接口需要包装起来,对外暴露3个参数,function,addr,*value即可。

复制代码
/**
  * @brief  modbus callbcak function. cmd like:MAC>UP\r\n
  * @param  value to read or write.
  * @retval 1=Success, 0=fail.
  */
uint8_t LockUp_W(uint16_t *value)
{
    ....//具体的执行部分
return 1; } /** * @brief modbus callbcak function. * @param value to read or write. * @retval 1=Success, 0=fail. */ uint8_t LockDown_R(uint16_t *value) { if(*value) { *value = 0; } return 1; } /** * @brief modbus callbcak function. cmd like:MAC>DOWN\r\n * @param value to read or write. * @retval 1=Success, 0=fail. */ uint8_t LockDown_W(uint16_t *value) { .....
return 1; } /** * @brief modbus callbcak function. * @param value to write. * @retval 1=Success, 0=fail. */ uint8_t MdTimeOut_R(uint16_t *value) { ....
return 1; } /** * @brief modbus callbcak function. * @param value to read. * @retval 1=Success, 0=fail. */ uint8_t MdTimeOut_W(uint16_t *value) { SystemTickLimitCfg(TMOUT_MB, *value); return 1; }
// table的数据结构,除了基本的地址外,还可以包含变量的范围,倍率,回调函数等
typedef struct
{
    uint8_t func;
    uint16_t addr;
    uint16_t min;
    uint16_t max;
    uint16_t *value;
    uint8_t (*pFunc)(uint16_t *value);
}MB_Reg_Struct;

/** * @brief modbus table. */ MB_Reg_Struct MBReg[]={ /*func, addr, min, max, &value, callBack()*/ {0x03, 1, 0, 1, &HoldReg.up, LockUp_R}, {0x03, 2, 0, 1, &HoldReg.dowm, LockDown_R}, {0x06, 1, 0, 1, &HoldReg.up, LockUp_W}, {0x06, 2, 0, 1, &HoldReg.dowm, LockDown_W}, {0x04, 1, 1, 63, &InputReg.md_addr, pNone}, {0x04, 2, 0, 0xff, &InputReg.lock_stats, pNone}, {0x04, 3, 0, 100, &InputReg.soc, pNone}, {0x04, 4, 0, 127, &InputReg.rssi, pNone}, {0x04, 5, 0, 1, &InputReg.search, pNone}, {0x04, 6, 0, 0, &InputReg.reserve[0], pNone}, {0x04, 7, 0, 0, &InputReg.reserve[1], pNone}, {0x04, 8, 0, 0, &InputReg.reserve[2], pNone}, {0x04, 9, 0, 0xffff, &InputReg.MAC_BLE[0], pNone}, {0x04, 10, 0, 0xffff, &InputReg.MAC_BLE[1], pNone}, {0x04, 11, 0, 0xffff, &InputReg.MAC_BLE[2], pNone}, {0x04, 12, 0, 0xffff, &InputReg.MAC_BLE[3], pNone}, {0x04, 13, 0, 0xffff, &InputReg.MAC_BLE[4], pNone}, {0x04, 14, 0, 0xffff, &InputReg.MAC_BLE[5], pNone}, {0x04, 15, 0, 0xffff, &InputReg.MAC_LOCK[0], pNone}, {0x04, 16, 0, 0xffff, &InputReg.MAC_LOCK[1], pNone}, {0x04, 17, 0, 0xffff, &InputReg.MAC_LOCK[2], pNone}, {0x04, 18, 0, 0xffff, &InputReg.MAC_LOCK[3], pNone}, {0x04, 19, 0, 0xffff, &InputReg.MAC_LOCK[4], pNone}, {0x04, 20, 0, 0xffff, &InputReg.MAC_LOCK[5], pNone}, };

 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多