APP下载

基于JNI的Java串口通信系统的设计与实现

2018-01-09罗尹奇刘力银

电脑知识与技术 2017年34期
关键词:串口通信

罗尹奇+刘力银

摘要:为解决在不同平台下Java串口通信问题,设计了一种通用的Java串口通信系统。该系统采用三层体系架构,包含了Java接口调用层、本地接口调用层和本地实现层;Java接口调用层负责定义串口通信的功能函数,本地接口调用层则由JNI(Java Native Interface)技术生成,定义本地代码的函数调用接口,本地实现层利用操作系统API实现串口通信功能。根据该系统设计,以Windows平台为例,实现了在Windows平台下的Java串口通信。通过功能测试实验表明该系统设计能正确地完成串口通信功能。

关键词:JNI(Java Native Interface);本地方法;串口通信;Windows

中图分类号:TP311 文献标识码:A 文章编号:1009-3044(2017)34-0051-06

Abstract: In order to solve the problem of Java serial communication in different platforms, a general Java serial communication system is designed in this paper. The system uses three layer architecture, including Java interface layer, native interface layer and a local implementation layer; Java interface layer is responsible for the function definition of serial communication, native interface layer is formed by JNI (Java Native Interface) technique and the function interface to define the local code, local implementation layer using operation API realize the serial communication function. According to the design of the system, this paper takes the Windows platform as an example to realize the Java serial communication under the Windows platform. The function test shows that the design of the system can correctly complete the serial communication function.

Key words: JNI(Java Native Interface); native method;serial communication; Windows

随着物联网技术的不断发展,串口通信技术作为一项十分重要的数据传输手段正得到越来越广泛的应用[1]。在传统的应用领域里,串口设备的访问均是基于C/C++本地代码实现的,虽然具备较高的访问性能,但其跨平台性则相对受限;而Java程序在跨平台方面具备得天独厚的优势,其体系结构无关性正受到越来越多的企业级服务的青睐[2-3]。然而Java的跨平台特性也为其带来了一定的局限性,部分与平台相关的功能无法得到良好支持[4],在对本地硬件设备访问方面(诸如串口设备等)就是受限情况之一。

JNI技术(Java Native Interface)作为Java访问C/C++本地代码的接口,可以实现对本地动态库的调用,既弥补了Java的不足,同时也兼具了跨平台的优势[3]。通过该技术可以将与平台相关的串口通信同跨平台的Java应用结合起来,实现Java程序对串口设备的访问。

1 关键技术简介

1.1 JNI(Java Native Interface)

JNI(Java Native Interface)是Java本地程序接口,属于JDK的一部分[3]。JNI允许运行在Java 虚拟机(JavaVirtual Machine,JVM)上的Java代码操作其他语言(例如C/C++)编写的应用程序和库[5]。同时本地应用程序和库也可以通过JNI来操作JVM内存中的Java对象,实现与Java应用程序共享这些对象[5]。

本地代码在平台属性使用和高性能计算方面具备明显的优势,而JNI技术则将这种优势集成到了Java之中,极大扩展了Java的功能范畴,特别是在处理Java本身不具备的平台属性和提升Java应用程序性能方面。可以说JNI技术充当了跨平台的Java代码和平台相关的本地代码的通信“桥梁”,实现了两者之間的互操作性。

1.2 串口通信

串口是计算机与外围设备之间的数据传输通道[6],数据通过串口以一位一位按顺序的方式进行传输,其优点是只需一对传输线,大大降低了数据传输的成本,特别适合远距离通信[7]。当前主流的串口标准包含了RS-232、RS-485、RS-422等,这些标准只对接口的电气特性做出规定,而不涉及接插件、电缆或协议,在此基础上用户可以建立自己的高层通信协议。

在当前主流操作系统平台上,串口设备是作为一种设备资源存在的[6],不同的操作系统对设备资源的管理方式各不相同,对串口设备的编程访问也需要依平台而定。在Windows平台下,实现串口通信的方式主要包括平台API函数、MScomm通信组件以及VS2008专门提供的串口通信类SerialPort[6]。

2 系统设计

2.1 整体架构

为了实现系统模块化设计,保证各个模块之间逻辑相对独立以方便后续改进,该系统采用了三层体系架构,即Java接口调用层、本地接口调用层和本地实现层。各层的关系如下:

1) Java接口調用层:定义串口通信的Java接口,该接口一方面提供给应用层代码调用,实现Java程序与串口设备通信;另一方面该接口通过JNI技术产生本地调用接口,实现Java调用接口与本地调用接口一一对应关系。

2) 本地接口调用层:由JNI技术产生与Java接口对应的本地代码(C/C++)接口,该接口规定了本地实现串口通信功能时所必须遵照的调用协议[2]。

3) 本地实现层:利用操作系统API实现串口号获取、打开串口、配置参数、读写串口、关闭串口等通信功能,并对本地调用接口进行封装。

根据各层的关系,该系统的整体架构设计如下:

2.2 Java接口调用层

Java接口调用层实现对串口对象的抽象,将实际物理串口设备抽象成逻辑串口对象,并提供给应用层代码调用;同时为了和本地代码通信,该层定义了调用接口,描述了本地接口应该遵照的调用协议形式。由于Java语言本身是面向对象的,而本地方法(例如C语言)是面向过程的,所以调用接口是按照基本数据传递方式进行设计。Java接口调用层设计如图2所示。

1) SerialPort接口描述了串口对象的基本功能,屏蔽了底层串口通信细节,应用层可以调用该接口实现与串口通信通信;在该接口中定义了串口的打开和关闭、配置串口以及读写串口功能;

2) SerialPortImpl类为SerialPort接口的具体实现,该类中包含了串口对象的波特率、数据位、停止位和校验位属性,同时由于串口设备只能采用独占式打开,因此该类采用了单例设计模式,保证逻辑串口对象和物理串口设备的统一;

3) PortHelper类为调用接口,可以通过JNI技术生成本地调用接口,该接口中的方法均为静态native方法,以便和本地代码(例如C语言)匹配。

2.3 本地接口调用层

本地接口调用层由JNI技术自动生成,该层的函数与上述PortHelper类中的方法形成一一对应关系,从而实现Java虚拟机通过装载动态连接库[4],经JNI间接完成本地代码调用的过程。由于该层接口是对Java调用接口的本地描述,因此当Java调用接口发生改变时,该层需重新生成以保证调用逻辑正确对应。

本地调用层在实现与Java接口调用层通信的同时,还需要实现同本地实现代码之间的调用。为了保持模块的相对独立性,本地调用层不包含具体的串口通信代码,其主要功能是实现基本的数据提取和安全检查,并将处理后的数据传递给本地实现层函数。本地接口调用层设计如下:

1) 本地接口依赖于JNI技术自动生成,是一组本地代码的函数声明,不包含具体的逻辑功能,与PortHelper类中的方法对应。由于采用了基本数据类型传递数据,因此生成的本地接口中的数据类型可以和C语言中的基本数据类型进行关联,并可以通过相应的方法完成数据转换;

2) Java中的数据类型与C语言中的数据类型存在差异,无法在本地代码中直接使用Java数据类型,因此需要对JNI传递而来的数据做适当的提取与格式转化之后才能在本地代码中使用;

3) 在JVM内存模型中,JVM能够实现对堆内存进行自动垃圾回收,但在本地内存上则无法实现,因此在处理数据的过程中,还需对数据的内存进行管理,以保证不会发生JVM崩溃问题。

2.4 本地实现层

本地实现层负责处理具体的串口逻辑功能。由于串口设备的驱动方式和通信方式与具体的操作系统平台相关,因此本地实现层需要借助具体的操作系统平台API来实现对串口设备的操作。串口设备作为一种独占式访问设备,要求用户程序有且仅有一个实例来获取串口句柄,因此在本地实现层里提供了一个全局串口变量来抽象该设备,通过该变量的使用状态来达到访问互斥的效果。本地实现层设计如下:

在现代操作系统中,物理设备被抽象成各种文件,因此对设备的访问也相应的抽象成了文件访问。而在不同操作系统中,文件模型以及读写文件的IO模型各不一样,故本地实现层的代码需视具体的操作系统平台而定。全局串口变量存在于本地内存中,为了保证其内存运行正确,不会出现溢出崩溃现象,故设计了一组内存管理函数New_Port和Free_Port,用于对全局串口变量的内存空间开辟和释放的管理。

3 代码实现

3.1 Java接口实现

在Java接口调用层中,PortHelper类为调用接口,也是通过JNI技术生成本地调用接口的模板,因此对该接口的实现成为了系统调用的关键。根据Java调用接口层设计,接口中的方法均为静态native方法,并且传递的参数均为基本数据类型,不涉及复杂对象的传递。同时Java语言本身具备良好的跨平台性,该接口的定义在任意平台下均适用。调用接口的代码如下:

各方法的功能及参数的说明如表2所示。

3.2 本地接口生成

本地接口是基于JNI技术自动生成的,在生成时需要使用不同平台下的JNI[3]。本文选择了Windows平台作为实验平台,因此需使用该平台下的JNI(%JAVA_HOME%\bin\javah.exe)。通过调用javah命令将上述PortHelper类生成对应的本地接口文件test_tools_PortHelper.h[2]。该头文件中定义的本地接口与PortHelper类中的静态方法形成了一一对应关系,具体代码如下(省略条件编译代码):

其中jni.h头文件(以及内部包含的jni_md.h)由本地JDK提供,定义了一系列Java与本地代码通信的库函数和数据结构,通过引入该头文件可以实现Java数据类型与本地数据类型之间相互转化,以及JVM的本地内存管理。

3.3 本地接口调用实现

生成的本地接口不包含任何逻辑,为了实现本地接口对本地实现层的代码调用,还需对本地接口的调用过程进行实现。为了保证调用过程的相对独立,降低各个接口之间的耦合度,本地接口与本地实现层的调用关系也为一一对应的,具体对应关系如下:

在调用过程中,数据的传递是双向的,一方面Java代码中的数据通过JNI传递到本地代码中,这需要保证在本地代码中能够正确的提取出所传递的数据;另一方面本地代码产生的结果也需要通过JNI传递到Java代码中,这需要保证JVM本地内存和本地内存正确,避免潜在的内存泄漏引发程序崩溃。因此在本地调用时,需要做以下两点处理:

1) 利用虚拟机内存指针对传递的数据进行处理,将其转化为本地代码能够识别的数据类型;

2) 利用虚拟机内存指针和本地释放函数对数据进行回收,避免内存泄漏。

由于在调用过程中所有的模块均需要遵循上述处理要求,因此现以读取串口为例,展示从JNI中利用虚拟机内存指针获取数据、处理数据、调用本地代码和回收数据整个过程(其他模块处理过程类似,在这里不再复述)。

根据Java调用接口定义,读取串口方法中包含了缓冲区参数以及返回实际读取的字节数,由于Java数据类型与本地代码数据类型存在差异,因此首先需要在本地代码中利用虚拟机内存指针获取JNI层传递的数组对象;其次通过调用本地实现层将数据从串口设备中读取出来;最后对读取结果进行封装并回收内存。具体的调用代码如下:

其中需要注意以下关键步骤:

1) 在步骤①中,由于Java数组对象与本地代码数组存在着差异,无法直接对该数组对象直接操作,因此通过虚拟机内存指针以拷贝的形式将JNI传递的数组对象获取出来。需要注意的是该获取过程采用了复制数组的形式,因此必须要有对应的回收数据的过程,避免虚拟机内存溢出;

2) 在步骤②中,通过直接调用本地实现层代码,完成了从串口设备中读取数据的功能,其读取的字节保存在缓冲区中;

3) 在步骤③中,将缓冲区中的字节数据复制到数组对象中,但需要注意的是,因为该数组对象是复制产生的(步骤①),并未对实际数组对象发生任何修改,所以还需通过虚拟机内存指针对原数组对象进行覆盖;

4) 步骤④实现了虚拟机内存指针对原数组对象进行覆盖的功能,从而将本地数据通过JNI传递到了Java代码中;

5) 步驟⑤与步骤①对应,将复制的数组对象进行释放,回收内存空间,防止虚拟机内存溢出。

3.4 本地实现层

3.4.1 基本数据结构

本地实现层负责利用平台API完成实际的串口通信。在Windows平台下串口设备被抽象成了文件,因此对串口设备的访问转化为对文件的访问,而文件的访问均需要通过对应的句柄才能完成,故为了方便数据管理,进一步提高抽象层次,基本数据结构中封装了串口句柄和相应参数,并定义了一组常量和内存管理函数。基本数据结构核心代码如下:

其中Port结构体封装了与串口有关的数据,包括串口句柄、波特率、数据位、停止位和校验位。本地实现层定义了全局变量g_port,能够保存串口对象以便在后续访问中实现对串口设备的读写功能。New_Port函数负责从内存中创建全局变量并完成初始化,free_Port函数则负责关闭串口句柄并回收内存,确保本地内存不会泄漏。

3.4.2 串口号获取

在Windows平台下,为了获取主机安装的所有串口设备,需要借助Windows API对注册表进行访问。在获取所有串口过程中没有打开串口的操作,故不需要使用全局变量g_port。串口号获取核心代码如下:

其中核心步骤主要有以下四步:

1) 步骤①利用了Windows平台提供的操作注册表函数实现打开注册表,将注册表句柄赋值给hKey变量,并从参数中返回;

2) 在步骤②中,循环的从注册表句柄里不断的获取串口名称,当没有串口名时才从循环中退出。获取到的串口名被封装进commName变量里;

3) 步骤③则将本轮循环得到的commName变量(即串口名)赋值给结果变量;

4) 步骤④则与步骤①相对,将打开的注册表关闭并释放句柄,保证操作注册表的逻辑正确。

3.4.3 打开串口

在Windows平台下串口设备被抽象成了文件,可以借助文件操作函数CreateFile实现串口打开。但需要注意的是,在Windows平台上文件的访问有同步访问和重叠访问,而串口设备不允许采用重叠访问的方式打开,因此在串口设备打开时需要选择同步模式。打开串口的核心代码如下:

由于串口设备必须是以独占的形式访问,因此全局变量g_port必须是在为NULL的时候才能进行串口打开操作,而一旦打开成功时全局变量g_port非空,这样防止了重复打开串口引发错误,同时也提高了代码访问效率,降低了对系统函数访问的频率。

3.4.4 配置串口

在打开串口成功之后,此时的串口运行参数是系统默认值,需进一步对串口的通信参数进行配置。串口的通信参数主要包括了串口的波特率、数据位、停止位和校验位。在一般情况下,配置串口过程是通过从系统中获取默认的串口参数信息,修改该参数信息并重新设置来实现的。配置串口的核心代码如下:

步骤①实现了获取系统默认的参数信息,而通过步骤②对默认的参数信息进行修改,使用实际参数值进行覆盖,在步骤③中则用修改后的参数信息对系统信息重新设置,并刷新串口缓冲区。

3.4.5 串口读写

串口读写的过程与一般文件读写过程类似。在成功获取了串口句柄和配置串口的基础上,通过平台提供的文件操作函数ReadFile和WriteFile实现对串口的读写。在读取的过程中,从串口中获取的字节信息将会保存到缓冲区中,并返回给本地调用层;而向串口写入数据时则直接将缓冲区中的字节数据写出。串口读写核心代码如下:

3.4.6 关闭串口

在操作结束时需要将串口设备关闭。关闭的过程一方面需要使用平台提供的句柄操作函数CloseHandle函数来关闭串口句柄,另一方面需要回收全局变量g_port,并将其恢复成初始化状态,以便下一次访问串口设备。关闭串口核心代码如下:

4 实验与结果

为了测试Java代码能否通过JNI技术实现本地串口通信,本文设计了一组功能实验,具体的运行环境如下:

基于上述测试环境,实验得到的结果如图5所示。

根据上图实验结果,图(a)实验软件显示了获取平台的全部串口信息,同时对串口COM1实现了打开并配置。在配置成功的基础上,实验软件向串口COM1写入了测试数据,并开启了循环读取串口数据功能,结果显示实验软件能够持续的从串口中读出数据。图(b)验证软件打开了串口COM2,由于串口COM1与串口COM2实现了关联,因此可以将串口COM1的数据从串口COM2中读出,或向串口COM2写入数据由串口COM1读出。验证软件运行结果显示,其能够正确接收到实验软件发送的测试数据,并且其周期性写入的数据在实验软件中也实现了正确读出。

5 结束语

实验结果表明,在Windows平台下Java代码能够通过JNI技术实现对本地串口设备的访问,运行结果符合预期,显示了该串口通信系统的设计方案是较为合理的,可以基于该设计方案在其他操作系统平台下做进一步验证。

然而该系统设计并未考虑性能因素,未将其与同类型软件在性能上做出横向对比,在后续的研究工作中还需深入对该设计方案的性能做出评估。

参考文献:

[1] 陈传波, 杜娟, 张智杰. WIN32下基于RS232C协议的串口通信方法及应用研究[J]. 南昌大学学报:工科版, 2005(3):71-75.

[2] 李亚东, 夏雨佳, 席裕庚. 基于JNI的跨平台软件设计[J]. 计算机工程, 2000(9):87-88,154.

[3] 任俊伟, 林东岱. JNI技术实现跨平台开发的研究[J]. 计算机应用研究, 2005(7):180-184.

[4] 張华平, 玄光哲, 于贵平, 等. 基于JNI技术应用框架的分析和实现[J].吉林大学学报:信息科版, 2003(2):188-191.

[5] 沙嘉祥, 宁书年, 林捷. 利用JNI实现企业Java程序与传统应用程序的集成[J]. 计算机与现代化, 2004(2):20-25.

[6] 龚新文. 串口通信在VS2008中的实现与应用[J]. 电脑与电信, 2011(3):47-48,51.

[7] 黄晖, 柴剑勇, 严兴, 等. 串口通信技术[J]. 科技创新导报, 2010(27):20-21.

猜你喜欢

串口通信
基于Qt和Flash的嵌入式Linux软件架构设计