在之前的两篇文章中,我们讲解了串口的基础知识和在安卓中使用串口通信的方法,如果还没看过之前文章的同学们,建议先看一遍,不然可能会不理解这篇文章讲的某些内容。
事实上,在实际应用中,我们很少会直接使用串口通信,一般都会使用到 Modbus。
因为正如我上篇文章所说,如果直接使用串口通信的话,需要我们自定义数据层协议,或者干脆就直接发送一个 byte 的数字进行通信,这显然是不方便的,也不安全的。
例如我上篇提到过的一个问题,我所使用的驱动版厂商定义的协议中没有定义数据长度(或者在数据中附上数据长度),也没有定义停止符号,这会导致出现“沾包”或“分包”情况时不好区分数据。
并且自定义协议还需要自己去解析并处理数据,使用起来不是那么方便。
所以,我司在尝试过直接使用串口通信后,最终还是决定放弃直接使用串口通信,而是改用 Modbus 通信。
本篇文章属于系列文章的扩展篇,我们将讲解 Modbus 的基础知识以及如何在安卓中使用 Modbus。
本文中部分图表来自文末标注的参考资料
Modbus 是一种应用层报文传输协议,由 Modicon 公司在 1979 年发布,是为了解决 PLC 通信而研发的协议。
因为 Modbus 是开源的且无著作权要求、易于部署维护、可靠性强的特性,所以 Modbus 已经成为工业领域通信协议事实上的业界标准,并且现在是工业电子设备之间常用的连接方式。
由于 Modbus 定义的只是应用层的报文协议,所以它可以使用串口(RS232、RS485)、以太网作为物理层接口。
Modbus 分为三种传输模式:RTU、ASII、TCP。
在使用 Modbus 时,所有设备的传输模式必须相同。
RTU 使用二进制数据传输、ASCII 使用 ASCII 字符传输。
使用串口连接时支持 RTU 和 ASCII 模式。
使用以太网连接时支持 TCP 模式。
因为本系列文章的重点在于讲解串口通信,所以我们不过多讲解 TCP 模式,同时,由于 ASCII 模式在目前实际应用中比较少,我们一般都是使用的 RTU 模式。故,我们会重点讲解 Modbus RTU。如果对其他传输模式感兴趣的可以阅读参考资料 4 的文档。
额外说明一下,Modbus 和 RS232、RS485 的区别。
RS232、RS485定义的是物理层标准,即接线方式,电平高低,数据传输方式等。
而 Modbus 是应用层协议,即定义了上述物理层传输过来的数据应该以什么样的格式去解析。
使用串口作为物理层协议时,通常采用的是 RS485 。
而我们在第一篇文章就说过,RS485 支持一主多从多个设备同时连接,所以使用 RS485 的 Modbus 同样支持多个设备连接。在标准负载情况下,支持一个主机连接最多32个从机。并且在连接设备时,只能使用菊花链连接,不能使用星型网络:
另外,Modbus 是一种请求/应答协议,即只能通过主站(主机)发送请求给从站后,从站响应数据给主站,而不能从站直接主动发送数据给主站。
在 Modbus 中定义了4种不同的数据模型,具体如下:
名称 | 数据类型 | 访问类型 | 说明 |
---|---|---|---|
离散量输入 | 单个比特(bit) | 只读 | I/O系统提供 |
线圈 | 单个比特(bit) | 读写 | 可通过应用程序改写 |
输入寄存器 | 字(word,16bit) | 只读 | I/O系统提供 |
保持寄存器 | 字(word,16bit) | 读写 | 可通过应用程序改写 |
其中 线圈 和 离散量输入 又可以称为 输出线圈 和 输入线圈。
它们的数据长度都是一个 bit,即只能表示 1 或 0,表现在程序中就是一个 Boolean 类型的数据。对于安卓程序员来说,可能会疑惑啥是线圈,其实这两个模型之所以叫做线圈是因为 Modbus 是为了 PLC 通信而编写的协议,而在 PLC 中一些物理设备(例如继电器)只有两种状态:断开与接通(即 0 或 1 ,或者 Boolean 的 false 与 true ),这些物理设备的状态切换一般都是依赖于线圈的通/断电来实现,所以在 Modbus 中就将这种类型的数据称为 线圈。
而 输入寄存器 和 保持寄存器 又可以称为 输入寄存器 和 输出寄存器。
它们的数据长度是一个 word,即 16 bit,2 byte,表现在程序中可以看成一个 Int 类型。
显然,在同一个设备中不同的数据模型肯定不止一个可用的数据区块,理论上来说,每种数据模型最大可以定义 65536 个数据区块。
因此,每种数据模型的地址定义为如下:
数据模型 | 地址范围 |
---|---|
线圈 | 00001-09999 |
离散输入 | 10001-19999 |
输入寄存器 | 30001-39999 |
保持寄存器 | 40001-49999 |
可以看到,虽然我们上面说每种模型理论上支持 65536 个数据区块,但是实际使用中每种数据模型一般都只会定义最大 10000 个数据区块。
Modbus 允许将四种不同的数据模型存放在不同的数据区块,这样使用不同的功能码(下面会说什么是功能码)读到的是不同的数据:
同时,Modbus 也可以将不同的数据模型映射到同一个数据区块中,这样一来,不同的功能码读取到的可能是相同的数据:
在上一节我们介绍了储存区数据模型,那么我们要如何去读取不同的数据模型数据呢?或者说,在 Modbus 中是怎么区分不同的数据模型?
此时,就要用到 功能码。
在 Modbus 中定义了三种类型的功能码:
公共功能码定义了如下几种:
而我们一般会使用到的有以下几种:
可以看到,我们常用的有 8 个功能码,其实仔细一看就能看出不过是读所有数据模型;以及可写数据模型和写单个/写多个的排列组合。
读取数据时所有数据模型均支持只读取单个和同时读取多个数据,并且使用的都是同一个功能码。
写入数据同样支持只写入单个数据和同时写入多个数据,但是写入单个和写入多个的功能码是分开的。
可能有细心的读者发现了,为什么表中的所有 寄存器地址 都是一样的啊,这是因为上表中的 PLC 地址使用的是绝对地址,一般用于文档中或程序中。
而实际设备的寄存器地址则使用的是相对地址。由于我们已经通过功能码区分开了不同的数据区块,所以为了节约传输时的字节占用,直接使用相对地址即可(如果使用绝对地址,那么现在的字节数不够表示所有地址)。
上文中提到过,使用串口的 Modbus 是主-从协议。即,在同一时刻,只有一个主节点和一个或多个子节点连接在同一个串行总线上。
Modbus 的通信总是由主节点发起,子节点响应。并且子节点之间不会相互通信。
在 Modbus 中,主节点没有地址,每个子节点都有自己唯一的地址(1-247),通常称为从站地址。
主节点有两种方式发出请求:单播模式与广播模式。
在单播模式中,主站(主节点)发送一个带有从站(子节点)地址的请求给当前连接的所有设备,但是只有从站地址符合的从站会响应该请求,并返回数据。其他设备不会响应也不会执行任何操作(读取到地址不符合后直接抛弃这个请求报文)。在这个模式中会产生两个报文:主站的请求报文和从站的响应报文。
在广播模式中所有从站都不会发送响应报文给主站,但是会执行请求的操作,并且主站的请求会发送给所有从站。广播模式一般用于写数据。此时主站发送的请求报文中的从站地址为 0 ,表示广播。
一个 Modbus RTU 的报文帧由 4 个部分组成:
8位从站地址+8位功能码+最大252*8位数据+16位差错校验
在 RTU 中通常使用的错误校验方式是 CRC 校验(眼熟吗?CRC 又出现了)
不知道你们有没有发现,这里的功能码使用了 2 byte ,但是上面介绍功能码时明明最大才到 127 ,那么剩下的一半去哪儿呢?
在 Modbus 定义中,从机如果能够正确处理主机的请求,则返回报文中的功能码将和主机请求的功能码一样,如果出现错误,无法正确的处理请求,则从机返回报文的功能码将是最高位为 1 的功能码,即 128-255 。
数据位在不同的功能码以及主机请求还有从机响应都有不同的数据内容和长度,例如请求读取线圈则数据位的内容为:2字节数据表示读取线圈起始地址+2字节数据表示要读取的线圈数量。
此时从机将会按照请求读取的线圈数量返回数据,数据格式为:1字节表示数据的字节数+N字节表示读取到线圈状态数据。如果读取到的线圈状态数据不是 8 位的整数,则会在后面填充 0 使其满足 8 位的倍数。
数据位在某些情况下,可以为空。
下面举一个数据帧的完整例子(例子来自参考资料 1)。
我们有一个从站是温湿度传感器,从站地址为 1,它会将采集到的湿度写入保持寄存器的 40001 区块中;温度写入保持寄存器的 40002 区块中。此时我们发送读取保持寄存器请求去获取它的温湿度信息。
则,主机的请求报文为:
0103040146013B5A59
分别拆解这个数据帧为:
01 :从站地址
03 :功能码,读保持寄存器
00 00 :读取的起始寄存器地址(对应 40001 的相对地址)
00 02 :读取的寄存器长度(这里表示连续读取两个寄存器)
C4 0B :CRC校验码
从机在接收到请求后,响应报文为:
0103040146013B5A59
拆解数据:
01:从站地址
03: 功能码,读保持寄存器
04 :读取到的数据的字节长度(这里表示4字节)
01 46 01 3B :读取到的数据,前两个字节为湿度(换算成十进制为 326 ,即 32.6% ),后两个字节为温度(十进制为 315,即 31.5 摄氏度)
5A 59 :CRC校验码
这里提一句,别纠结为啥读取到的温湿度的值要除以 10 才是实际值,因为这是温湿度传感器厂家定义的。
经过上面的介绍,相信大家已经对于 Modbus 有了一个大致的了解。
那么,如何在安卓中使用 Modbus 呢?如果你理解了 Modbus 的基础,并且前面的两篇文章也大致理解了,那么这就不是问题了。
核心思路就是通过上篇文章介绍的使用 android-serialport-api 或使用 USB Host 的方法打开串口,并获取到输入输出流,然后在发送和接收数据时按照 Modbus 协议标准封装或解析即可。
其中如何打开串口以及获取输入输出流已经在上篇文章介绍,因此现在需要解决的是如何封装/解析数据。
当然,你可以按照 Modbus 标准文档自己动手写一个。
或者,你也可以不用重复造轮子,直接使用现成的第三方库。
这里我们可以使用 modbus4j,但是,从它的名字就可以看出来,这是一个 java 库,好在我们只需要使用它的解析和封装的功能,所以在安卓中依旧可以使用。
老规矩,使用 modbus4j 前需要先引入依赖:
// 添加仓库地址
repositories {
...
maven { url 'https://jitpack.io' }
}
……
// 添加依赖
implementation 'com.github.MangoAutomation:modbus4j:3.1.0'
然后在正式使用之前,我们需要新建一个类继承自 SerialPortWrapper
,用于实现在安卓上的串口功能:
class AndroidWrapper : SerialPortWrapper {
// 关闭串口
override fun close() {
TODO("Not yet implemented")
}
// 打开串口
override fun open() {
TODO("Not yet implemented")
}
// 获取输入流
override fun getInputStream(): InputStream {
TODO("Not yet implemented")
}
// 获取输出流
override fun getOutputStream(): OutputStream {
TODO("Not yet implemented")
}
// 获取波特率
override fun getBaudRate(): Int {
TODO("Not yet implemented")
}
// 获取数据位
override fun getDataBits(): Int {
TODO("Not yet implemented")
}
// 获取停止位
override fun getStopBits(): Int {
TODO("Not yet implemented")
}
// 获取校验位
override fun getParity(): Int {
TODO("Not yet implemented")
}
}
在我们新建的这个类中重写上述几个方法,用于提供串口通信所需要的几个参数即可。
然后,初始化 modbus4j 并发送消息:
val modbusFactory = ModbusFactory()
val wrapper: SerialPortWrapper = AndroidWrapper()
// 创建管理对象
val master = modbusFactory.createRtuMaster(wrapper)
// 发送消息
val request = ……
val response = master.send(request) // requst 为要发送的数据,response 为接收到的响应数据
上面就是 modbus4j 的简单使用方法,如果同学们甚至都不想自己去完成串口通信的话,还可以用这个库 Modbus4Android ,这个库基于 android-serialport-api 和 上面的 modbus4j 封装了一个安卓上到手即用的 Modbus 库。
不过它使用的是 android-serialport-api 实现串口通信,如果需要使用 USB Host 的话可能还是需要自己去封装一个库了。(等我找到合适的测试设备后抽空我也封装一个)
并且,这个库使用了 RxJava 如果不喜欢 RxJava 的话也得自己封装一个了,其实封装起来也不算难,完全可以基于这个库自己改一改就好了。
使用这个库的第一步,依旧是导入依赖:
// 添加远程仓库
repositories {
maven { url 'https://jitpack.io' }
}
……
// 添加依赖
dependencies {
implementation 'com.github.licheedev:Modbus4Android:2.0.2'
}
接下来,为了方便使用,同时为了避免重复初始化,我们可以创建一个全局单例实例 ModbusManager
:
class ModbusManager : ModbusWorker() {
/**
* 释放整个ModbusManager,单例会被置null
*/
@Synchronized
override fun release() {
super.release()
sInstance = null
}
companion object {
@Volatile
private var sInstance: ModbusManager? = null
fun getInstance(): ModbusManager {
var manager = sInstance
if (manager == null) {
synchronized(ModbusManager::class.java) {
manager = sInstance
if (manager == null) {
manager = ModbusManager()
sInstance = manager
}
}
}
return manager!!
}
}
}
复制代码
然后初始化串口连接:
private fun initConnect(): Boolean {
Log.i(TAG, "initConnect: 开始初始化连接 Modbus\nconfig=$config")
val param = SerialParam
.create(config.serialPath, config.serialRate) // 串口地址和波特率
.setDataBits(config.serialDataBits) // 数据位
.setParity(config.serialParity) // 校验位
.setStopBits(config.serialStopBits) // 停止位
.setTimeout(config.serialTimeout) //超时时间
.setRetries(config.serialRetries) // 重试次数
try {
// 初始化前先关闭,避免串口已经被打开过
ModbusManager.getInstance().closeModbusMaster()
val modbusMaster = ModbusManager.getInstance().syncInit(param)
return true
// 初始化(打开串口)成功
} catch (e: ModbusInitException) {
Log.e(TAG, "initConnect: 初始化modbus出错!", e)
} catch (e: InterruptedException) {
Log.e(TAG, "initConnect: 初始化modbus出错!", e)
} catch (e: ExecutionException) {
Log.e(TAG, "initConnect: 初始化modbus出错!", e)
} catch (e: ModbusTransportException) {
Log.e(TAG, "initConnect: 初始化modbus出错!", e)
} catch (e: ModbusRespException) {
Log.e(TAG, "initConnect: 初始化modbus出错!", e)
}
return false
}
完成上述步骤后,我们就可以开始发送请求并接收数据了。
这里依旧以读取线圈数据为例,我们可以使用同步请求:
val slaveId = 1 // 从站地址
val start = 00001 // 读取的起始位置
val len = 1 // 需要读取的长度
val response = ModbusManager.getInstance().syncReadCoil(slaveId, start, len)
其中的 response
即为响应数据信息。
另外,我们也可以使用异步读取的方式:
ModbusManager.getInstance().readCoil(slaveId, start, len, object : ModbusCallback<ReadCoilsResponse> {
override fun onSuccess(response: ReadCoilsResponse?) {
// 请求成功,收到回复为 response
}
override fun onFailure(tr: Throwable?) {
// 请求失败
}
override fun onFinally() {
// 请求完成
}
})
该库支持的所有读取方法如下:
所有写数据方法如下:
我们在这篇文章中介绍了在安卓中使用串口通信时大概率会接触到的一种应用层协议 -- Modbus,并讲解了如何在安卓中使用 Modbus ,另外介绍了几个个人认为比较好用的第三方库。
自此,关于安卓上的串口通信内容就讲的差不多了。
下一篇看情况写一写各个校验方法的原理和算法实现或者是上文中挖的使用 USB Host 实现 Modbus 的坑,也可能这个系列就此完结吧,哈哈。
本文由哈喽比特于1年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/JhCSdtvZ8kxVU9U-Axd1Ag
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。