写出稳定的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, ) == 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 ;
} /**
* @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 = ;
}
return ;
}
/**
* @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 ;
} /**
* @brief modbus callbcak function.
* @param value to write.
* @retval 1=Success, 0=fail.
*/
uint8_t MdTimeOut_R(uint16_t *value)
{
....
return ;
} /**
* @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 ;
}
// 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, , , , &HoldReg.up, LockUp_R},
{0x03, , , , &HoldReg.dowm, LockDown_R}, {0x06, , , , &HoldReg.up, LockUp_W},
{0x06, , , , &HoldReg.dowm, LockDown_W}, {0x04, , , , &InputReg.md_addr, pNone},
{0x04, , , 0xff, &InputReg.lock_stats, pNone},
{0x04, , , , &InputReg.soc, pNone},
{0x04, , , , &InputReg.rssi, pNone},
{0x04, , , , &InputReg.search, pNone}, {0x04, , , , &InputReg.reserve[], pNone},
{0x04, , , , &InputReg.reserve[], pNone},
{0x04, , , , &InputReg.reserve[], pNone}, {0x04, , , 0xffff, &InputReg.MAC_BLE[], pNone},
{0x04, , , 0xffff, &InputReg.MAC_BLE[], pNone},
{0x04, , , 0xffff, &InputReg.MAC_BLE[], pNone},
{0x04, , , 0xffff, &InputReg.MAC_BLE[], pNone},
{0x04, , , 0xffff, &InputReg.MAC_BLE[], pNone},
{0x04, , , 0xffff, &InputReg.MAC_BLE[], pNone}, {0x04, , , 0xffff, &InputReg.MAC_LOCK[], pNone},
{0x04, , , 0xffff, &InputReg.MAC_LOCK[], pNone},
{0x04, , , 0xffff, &InputReg.MAC_LOCK[], pNone},
{0x04, , , 0xffff, &InputReg.MAC_LOCK[], pNone},
{0x04, , , 0xffff, &InputReg.MAC_LOCK[], pNone},
{0x04, , , 0xffff, &InputReg.MAC_LOCK[], pNone},
};
写出稳定的Modbus代码之点滴经验的更多相关文章
- 如何写出没有BUG的代码
1947年9月9日,美国海军准将 Grace Hopper 在哈佛学院计算机实验室里使用 Mark II 和 Mark III 计算机进行研究工作.她的团队跟踪到 Mark II 上的一个错误,操作人 ...
- 如何写出没有 bug 的代码?
来源:www.cnblogs.com/sherrywasp/p/9262877.html 1947年9月9日,美国海军准将 Grace Hopper 在哈佛学院计算机实验室里使用 Mark II 和 ...
- 如何用java写出无副作用的代码
搞java的同学们可能对无副作用这个概念比较陌生,这是函数式编程中的一个概念,无副作用的意思就是: 一个函数(java里是方法)的多次调用中,只要输入参数的值相同,输出结果的值也必然相同,并且在这个函 ...
- 如何写出优雅的CSS代码 ?(转)
对于同样的项目或者是一个网页,尽管最终每个前端开发工程师都可以实现相同的效果,但是他们所写的代码一定是不同的.有的优雅,看起来清晰易懂,代码具有可拓展性,这样的代码有利于团队合作和后期的维护:而有的混 ...
- fir.im Weekly - 如何写出零 bug 的代码
神兽护体,代码无bug.经常看到代码注释的各种形状,这是一种程序员情怀.那么,如何能写出零 Bug 的代码呢,来看看@码农翻身 的这篇手册--零Bug的代码是怎么炼成的. 写零 Bug 一定少不了代码 ...
- 如何写出优雅的css代码 ?
如何写出优雅的css代码 ? 对于同样的项目或者是一个网页,尽管最终每个前端开发工程师都可以实现相同的效果,但是他们所写的代码一定是不同的.有的优雅,看起来清晰易懂,代码具有可拓展性,这样的代码有利于 ...
- 用6个字符写出任意的Javascript代码
博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:用6个字符写出任意的Javascript代码.
- 请阐述调用Activity有哪几种方法,并写出相关的Java代码
请阐述调用Activity有哪几种方法,并写出相关的Java代码. 答案:可以采用两种方式调用Activity:显示调用和隐式调用.显示调用直接指定了Activity,代码如下: Intent int ...
- JAVA语言之怎样写出高性能的Java代码?
本文主要向大家介绍了JAVA语言之怎样写出高性能的 Java 代码?通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助. 在这篇文章中,我们将讨论几个有助于提升Java应用程序性能的方法.我 ...
随机推荐
- Android通过Apk插件调起微信支付
App对接微信调起微信支付需要在微信平台注册,鉴别的标识就是App的包名,所以将申请的包名单独打包成一个Apk文件,则在其他的App调起此Apk的时候同样可以起到调用微信支付的功能.这样就实现了调起微 ...
- Java显式锁学习总结之六:Condition源码分析
概述 先来回顾一下java中的等待/通知机制 我们有时会遇到这样的场景:线程A执行到某个点的时候,因为某个条件condition不满足,需要线程A暂停:等到线程B修改了条件condition,使con ...
- 模拟做饭系统(java+线程中的join方法)
(一)项目框架分析 妈妈要去做饭,发现没有酱油,让儿子去买酱油,然后回来做饭. 根据面向对象的思想,有两个对象,妈妈和儿子 主要有两个方法: (一)没有线程控制(即儿子没有买酱油回来妈妈就做好饭了)+ ...
- angular 1.26 版本 window.history.back() 自动去顶部
在1.26版本,在url ("www.example.com#xx"),接着按back,会自动调到顶部,这是因为angular的默认设置 只要在config注入$AnchorScr ...
- LINUX RHEL6.5字符界面安装图形化桌面
安装RHEL 6.5 系统,也是一波三折.好不容易把系统装上去了,发现没装图形化界面.重装倒是学会了,不过觉得太麻烦,于是有了今天. 查了很多帖子,然后自己一一尝试,发现都是说简单,只要 yum gr ...
- Hibernate一级缓存(基于查询分析)
首先我们应该弄清什么是hibernate缓存:hibernate缓存是指为了降低应用程序对物理数据源的访问频次,从而提高应用程序的运行性能的一种策略.我们要将这个跟计算机内存或者cpu的缓存区分开. ...
- 基于Asp.Net Core Mvc和EntityFramework Core 的实战入门教程系列-3
来个目录吧: 第一章 第二章 第三章 暂时就这么多.后面路线更新吧 创建.查询.更新.删除 这章主要讲解使用EF完成 增删改查的功能. 自定义"详情信息"页面 我们通过基架生成的代 ...
- java基础:修改数组对应下标的数据
- KoaHub.js是基于 Koa.js 平台的 Node.js web 快速开发框架
koahubjs KoaHub.js -- 基于 Koa.js 平台的 Node.js web 快速开发框架.可以直接在项目里使用 ES6/7(Generator Function, Class, A ...
- 3223: Tyvj 1729 文艺平衡树
3223: Tyvj 1729 文艺平衡树 Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 1347 Solved: 724[Submit][Stat ...