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

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},
};
时间: 2024-11-05 11:38:31

写出稳定的Modbus代码之点滴经验的相关文章

如何写出没有BUG的代码?

1947年9月9日,美国海军准将 Grace Hopper 在哈佛学院计算机实验室里使用 Mark II 和 Mark III 计算机进行研究工作.她的团队跟踪到 Mark II 上的一个错误,操作人员发现是由于一只飞蛾钻到了 Mark II 的继电器里导致的.团队清除了这只飞蛾,一切恢复正常.当时的工作人员记录了这样一句日志:" First actual case of bug being found. "  这次著名的事件,犹如潘多拉打开了魔盒,从此,程序员的世界里,bug 满天飞

关于单元测试,如何写出可测试的代码?

单元测试在一个完整的软件开发流程中是必不可少的.非常重要的一个环节.通常写单元测试并不难,但有的时候,有的代码和功能难以测试,导致写起测试来困难重重.因此,写出良好的可测试的(testable)代码是非常重要的.接下来,我们简要地讨论一下什么样的代码是难以测试的,我们应该如何避免写出难以测试的代码,以及要写出可测试性强的代码的一些最佳实践. 什么是单元测试(unit test)? 在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进

如何写出优雅的CSS代码 ?(转)

对于同样的项目或者是一个网页,尽管最终每个前端开发工程师都可以实现相同的效果,但是他们所写的代码一定是不同的.有的优雅,看起来清晰易懂,代码具有可拓展性,这样的代码有利于团队合作和后期的维护:而有的混乱,虽然表达出了最终的效果,然而却晦涩难懂,显然团队成员在读这样的代码时就显得无从下手,更不利于后期的维护了.那么如何写出优雅的代码呢?下面我将以一个很小的项目就以下几个方面简单的表达一下自己的看法,如有不妥,望批评指正. 如何整理一个项目. 如何写出清晰易懂的HTML代码. 如何写出优雅的css代

【整洁之道】如何写出更整洁的代码(上)

如何写出更整洁的代码 代码整洁之道不是银弹,不会立竿见影的带来收益. 没有任何犀利的武功招式,只有一些我个人异常推崇的代码整洁之道的内功心法.它不会直接有效的提高你写代码的能力与速度,但是对于程序员的整个职业生涯必然会带来意想不到的好处. 如果你还是一个在校学生,或者是刚工作没多久的"菜鸟",那么很有必要接触一些这方面的知识的.很显然,它会帮助你更快的适应企业级开发的要求. 1. 为什么需要代码更整洁? 在考虑代码整洁的时候,我们需要明确的一个前提是,这里不讨论代码的对错. 关于什么是

幸福村站——成都传智播客程序员写出你的烧烤代码

又是一个阳光明媚,风和日丽之天,如果作为程序员的你还在键盘上苦苦的想着下一串代码该怎么写的话,那你就弱爆了.俗语说得好,学习要劳逸结合,写代码更是需要清晰的思维,在传智播客Java基础班开班一个月后,班主任决定带着这群"猿猴们"去传说中的"幸福村"放松放松,我们也跟着一起去感受程序员们的烧烤代码的幸福吧! 带着好奇的心理走进了"幸福梅林站",一个又一个的农家乐园开始浮现在我们眼前,那里朴素的民风和美丽的风景让我们暂时忘却了学习上的烦恼和城市里的喧

【知识点】如何写出优雅的CSS代码 ?

对于同样的项目或者是一个网页,尽管最终每个前端开发工程师都可以实现相同的效果,但是他们所写的代码一定是不同的.有的优雅,看起来清晰易懂,代码具有可拓展性,这样的代码有利于团队合作和后期的维护:而有的混乱,虽然表达出了最终的效果,然而却晦涩难懂,显然团队成员在读这样的代码时就显得无从下手,更不利于后期的维护了.那么如何写出优雅的代码呢?下面我将以一个很小的项目就以下几个方面简单的表达一下自己的看法,如有不妥,望批评指正. 如何整理一个项目. 如何写出清晰易懂的HTML代码. 如何写出优雅的css代

怎样写出工业级的C代码 (2)

怎样写出工业级的C代码 1. 层次分明:结构清晰 ,模块划分 2.重要tips a.从词法的角度来看, 熟悉C运算符号的优先级,特别是不能混淆=/&&/+-和移位等操作符之间的顺序: 常见容易混淆的顺序包括: 算术加减和移位操作:a>>b+1表示a>>(b+1),而非(a>>b) + 1: 自加/减和->/.运算符号:a->b.c; ++a->b 理解编译器对符号解析处理的过程:比如a+++b,到底是a++ +b还是a+ ++b: if

用6个字符写出任意的Javascript代码

博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:用6个字符写出任意的Javascript代码.

利用|,&,^,~,<<,>>>写出高效艺术的代码

简介: 大家在阅读源码的时候经常会看到一些比如下面这样特别难理解的代码. cancelEvent.setAction(MotionEvent.ACTION_CANCEL | (motionEvent.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); order = ((order) >> (INDEX_OFFSET -1) + 1) << INDEX_OFFSET; 类似与这种"高大上&