前言

在之前的两篇文章中,咱们解说了串口的基础知识和在安卓中运用串口通讯的办法,假如还没看过之前文章的同学们,建议先看一遍,否则可能会不了解这篇文章讲的某些内容。

事实上,在实践应用中,咱们很少会直接运用串口通讯,一般都会运用到 Modbus。

由于正如我上篇文章所说,假如直接运用串口通讯的话,需求咱们自界说数据层协议,或许爽性就直接发送一个 byte 的数字进行通讯,这明显是不便利的,也不安全的。

例如我上篇提到过的一个问题,我所运用的驱动版厂商界说的协议中没有界说数据长度(或许在数据中附上数据长度),也没有界说中止符号,这会导致呈现“沾包”或“分包”情况时欠好区别数据。

而且自界说协议还需求自己去解析并处理数据,运用起来不是那么便利。

所以,我司在尝试过直接运用串口通讯后,终究仍是决定抛弃直接运用串口通讯,而是改用 Modbus 通讯。

本篇文章归于系列文章的扩展篇,咱们将解说 Modbus 的基础知识以及怎么在安卓中运用 Modbus。

本文中部分图表来自文末标注的参考资料

Modbs 基础

简介

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 是应用层协议,即界说了上述物理层传输过来的数据应该以什么样的格式去解析。

Modbus RTU

运用串口作为物理层协议时,一般采用的是 RS485 。

而咱们在第一篇文章就说过,RS485 支撑一主多从多个设备一起衔接,所以运用 RS485 的 Modbus 相同支撑多个设备衔接。在规范负载情况下,支撑一个主机衔接最多32个从机。而且在衔接设备时,只能运用菊花链衔接,不能运用星型网络:

安卓与串口通信-modbus篇

另外,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篇

功用码

在上一节咱们介绍了储存区数据模型,那么咱们要怎么去读取不同的数据模型数据呢?或许说,在 Modbus 中是怎样区别不同的数据模型?

此刻,就要用到 功用码。

在 Modbus 中界说了三种类型的功用码:

  • 公共功用码 : Modbus 组织界说的规范的揭露的通用的功用码,包含已界说的和保存的功用码
  • 用户自界说功用码 : 用户能够自界说自己需求的功用码,规模在 65-72 和 100-110(都是十进制)之间。
  • 保存功用码 : 一些公司的传统设备中运用的功用码,对公共功用码无效。

安卓与串口通信-modbus篇

公共功用码界说了如下几种:

安卓与串口通信-modbus篇

而咱们一般会运用到的有以下几种:

安卓与串口通信-modbus篇

能够看到,咱们常用的有 8 个功用码,其实仔细一看就能看出不过是读一切数据模型;以及可写数据模型和写单个/写多个的排列组合。

读取数据时一切数据模型均支撑只读取单个和一起读取多个数据,而且运用的都是同一个功用码。

写入数据相同支撑只写入单个数据和一起写入多个数据,可是写入单个和写入多个的功用码是分开的。

可能有细心的读者发现了,为什么表中的一切 寄存器地址 都是相同的啊,这是由于上表中的 PLC 地址运用的是肯定地址,一般用于文档中或程序中。

而实践设备的寄存器地址则运用的是相对地址。由于咱们现已通过功用码区别开了不同的数据区块,所以为了节约传输时的字节占用,直接运用相对地址即可(假如运用肯定地址,那么现在的字节数不够表明一切地址)。

主/从站

上文中提到过,运用串口的 Modbus 是主-从协议。即,在同一时刻,只要一个主节点和一个或多个子节点衔接在同一个串行总线上。

Modbus 的通讯总是由主节点建议,子节点呼应。而且子节点之间不会相互通讯。

在 Modbus 中,主节点没有地址,每个子节点都有自己仅有的地址(1-247),一般称为从站地址。

主节点有两种办法宣布恳求:单播形式与播送形式。

安卓与串口通信-modbus篇

在单播形式中,主站(主节点)发送一个带有从站(子节点)地址的恳求给当时衔接的一切设备,可是只要从站地址符合的从站会呼应该恳求,并返回数据。其他设备不会呼应也不会履行任何操作(读取到地址不符合后直接抛弃这个恳求报文)。在这个形式中会发生两个报文:主站的恳求报文和从站的呼应报文。

在播送形式中一切从站都不会发送呼应报文给主站,可是会履行恳求的操作,而且主站的恳求会发送给一切从站。播送形式一般用于写数据。此刻主站发送的恳求报文中的从站地址为 0 ,表明播送。

数据帧

一个 Modbus RTU 的报文帧由 4 个部分组成:

8位从站地址+8位功用码+最大252*8位数据+16位过失校验

安卓与串口通信-modbus篇

在 RTU 中一般运用的过错校验办法是 CRC 校验(眼熟吗?CRC 又呈现了)

不知道你们有没有发现,这儿的功用码运用了 2 byte ,可是上面介绍功用码时明明最大才到 127 ,那么剩下的一半去哪儿呢?

在 Modbus 界说中,从机假如能够正确处理主机的恳求,则返回报文中的功用码将和主机恳求的功用码相同,假如呈现过错,无法正确的处理恳求,则从机返回报文的功用码将是最高位为 1 的功用码,即 128-255 。

数据位在不同的功用码以及主机恳求还有从机呼应都有不同的数据内容和长度,例如恳求读取线圈则数据位的内容为:2字节数据表明读取线圈开端地址+2字节数据表明要读取的线圈数量。

此刻从机将会依照恳求读取的线圈数量返回数据,数据格式为:1字节表明数据的字节数+N字节表明读取到线圈状况数据。假如读取到的线圈状况数据不是 8 位的整数,则会在后面填充 0 使其满足 8 位的倍数。

安卓与串口通信-modbus篇

数据位在某些情况下,能够为空。

下面举一个数据帧的完好比如(比如来自参考资料 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 呢?假如你了解了 Modbus 的基础,而且前面的两篇文章也大致了解了,那么这就不是问题了。

核心思路便是通过上篇文章介绍的运用 android-serialport-api 或运用 USB Host 的办法翻开串口,并获取到输入输出流,然后在发送和接纳数据时依照 Modbus 协议规范封装或解析即可。

其间怎么翻开串口以及获取输入输出流现已在上篇文章介绍,因而现在需求解决的是怎么封装/解析数据。

当然,你能够依照 Modbus 规范文档自己动手写一个。

或许,你也能够不必重复造轮子,直接运用现成的第三方库。

这儿咱们能够运用 modbus4j,可是,从它的姓名就能够看出来,这是一个 java 库,好在咱们只需求运用它的解析和封装的功用,所以在安卓中依旧能够运用。

modbus4j

老规矩,运用 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 的话也得自己封装一个了,其实封装起来也不算难,完全能够基于这个库自己改一改就好了。

Modbus4Android

运用这个库的第一步,依旧是导入依靠:

// 增加远程库房
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篇

总结

咱们在这篇文章中介绍了在安卓中运用串口通讯时大概率会接触到的一种应用层协议 — Modbus,并解说了怎么在安卓中运用 Modbus ,另外介绍了几个个人认为比较好用的第三方库。

自此,关于安卓上的串口通讯内容就讲的差不多了。

下一篇看情况写一写各个校验办法的原理和算法结束或许是上文中挖的运用 USB Host 结束 Modbus 的坑,也可能这个系列就此结束吧,哈哈。

参考资料

  1. 这节课带你吃透Modbus通讯协议
  2. 6分钟快速了解Modbus通讯协议!
  3. Modbus
  4. modbus_proto_cn.pdf

本文正在参加「金石方案 . 分割6万现金大奖」