一、引言
Java程序具有一次编译、到处运行的跨平台特性,随着Java在嵌入式系统中的广泛应用,研究Java的串行通信程序具有日益重要的意义。文献[1]实现了基于Java事件驱动的串行通信,在该文献及Sun公司中国技术社区的蒋清野的基础之上,设计了通用的串行通信类,跟文献[2]相比,这些类更简洁、健壮,而且可以方便地处理任何字节,并可以将一批数据作为一个整体来提交,方便用户的下一步处理。这里的通用串口类由两个Java类组成,OperateCOM类用来初始化串口,并启动数据接收进程;ReadCOM类用来读取串口数据,并将一批数据作为整体提交。SerialExample类利用OperateCOM和ReadCOM类进行串行通信的测试,并利用文献[3]中的工具进行各种格式的显示。
二、OperateCOM类
OperateCOM与ReadCOM类的包名均为SerialPort。OperateCOM类主要完成的工作是:取得串口ID、打开串口、获取输入输出流、设置串口参数、启动串口数据读取进程。然后,就是常用的读取串口数据、从串口发送数据,以及关闭串口。
1.public OperateCOM (int PortID, int nLen )
这是OperateCOM类的构造方法,PortID表示需要操作的串口号,“1”表示“COM1”,“2”表示“COM2”……以此类推;nLen表示串口的输入缓冲区大小,最小值为1。构造方法的源代码如下:
public OperateCOM(int PortID, int nLen) {
PortName = "COM"+PortID;
nMaxLength = nLen;
if (nLen < 1) nMaxLength = 1;
}
2.public int GetPortID()
该方法通过串口名字,如COM1,取得串口的ID,如果正确则返回Serial_Success(常数1);如果错误,则抛出没有此串口的异常NoSuchPortException,并返回Serial_Error(常数-1),其源代码如下:
public int GetPortID(){
try {
portID = CommPortIdentifier.getPortIdentifier(PortName);
} catch (NoSuchPortException e) {
return Serial_Error;
}
return Serial_Success;
}
3.public int Open(String AppName, int nTime)
该方法通过获取的串口portID打开串口,输入参数AppName是程序的名称,nTime表示延迟的毫秒数。如果该串口正在被使用,则会抛出PortInUseException异常;如果打开串口成功,则根据serialPort继续获取串口的输入输出流,其源代码如下:
public int Open(String AppName, int nTime){
try {
serialPort = (SerialPort)portID.open(AppName, nTime);
} catch (PortInUseException e) {
return Serial_Error;
}
try {
in = serialPort.getInputStream();
out = serialPort.getOutputStream();
} catch (IOException e) {
return Serial_Error;
}
return Serial_Success;
}
4.public int SetParams(int baudrate, int dataBits, int stopBits, int parity)
该方法用来设置串口参数。输入参数分别对应波特率、数据位、停止位和校验方式。在Java Communications API的Javadoc中,有相应的常数代号,例如,DATABITS_8表示整数8,STOPBITS_1表示整数1等。源代码如下:
public int SetParams(int baudrate, int dataBits, int stopBits, int parity){
try {
serialPort.setSerialPortParams(baudrate, dataBits, stopBits, parity);
} catch (UnsupportedCommOperationException e) {
return Serial_Error;
}
return Serial_Success;
}
5.public void StartCom(int nDelay)
该方法启动数据接收进程,nDelay是延迟的毫秒数,表示凡是时间间隔在nDelay毫秒之内的数据,都作为一个整体来处理。从文献[1]中的测试效果可知,串行通信的数据确实是不连续的,需要进行累加处理。ReadCOM是Thread类的子类,本方法根据给定的参数,生成一个多线程的实例,并启动多线程。OperateCOM类直接控制ReadCOM类的实例的生成、多线程的销毁与数据的读取等。StartCom的源代码如下:
public void StartCom(int nDelay){
readThread = new ReadCOM(in, nDelay, nMaxLength);
readThread.start();
}
6.public byte[] ReadPort()
读取串口数据,并且根据要求的时间片将数据作为整体处理,是串行通信中的难点。ReadCOM类很好地实现了这个功能,其方法GetComBuffer()可以取得以字节数组形式的完整的数据包。ReadPort()的源代码如下:
public byte[] ReadPort(){
return readThread.GetComBuffer();
}
7.public void WritePort(byte bData[], int off, int len)
该方法用来从串口发送数据,其参数包括需要发送的字节数组、数组的偏移量与发送的长度,其源代码如下。
public void WritePort(byte bData[], int off, int len){
try {
out.write(bData, off, len);
} catch (IOException e) {
System.out.println("IOException:"+e);
}
}
8.public void ClosePort()
OperateCOM类的Open方法打开串口,ClosePort则用于关闭串口。由于读取串口数据的多线程是一个死循环,关闭串口前,需要首先关闭多线程。在多线程中,有一个布尔变量,可以通过公共方法DestroyReadThread调用,用来设置为true,从而使多线程退出。纯粹给多线程实例readThread赋值null,并不能迫使多线程退出,在NetBeans 5.0 的调试环境下可以发现这一点。另外,Thread的Destroy方法已经抛弃不用了。关闭多线程后,再关闭串口即可。ClosePort方法的源代码如下:
public void ClosePort(){
readThread.DestroyReadThread();
readThread = null;
serialPort.close();
}
三、ReadCOM类
ReadCOM类派生于Thread类,其主要方法为构造方法及run方法,其他方法被run方法所调用。
1.public ReadCOM(InputStream Port, int steps, int nLen)
该方法是ReadCOM类的构造方法,需要传入输入流Port;需要等待的节拍数(即毫秒数)steps,在此时间片内的数据将被当作一个整体来处理;nLen则是输入缓冲区的大小,根据该数据调用ByteBuffer类的allocate方法分配缓冲区。构造方法的源代码如下:
public ReadCOM(InputStream Port, int steps, int nLen) {
ComPort = Port;
TimeToWait = steps;
nPackageLen = nLen;
ComBuffer = ByteBuffer.allocate(nLen);
}
2.public void run()
该方法是一个多线程方法,是ReadCOM类中的核心方法,本类中的其他方法几乎都是为该方法服务的。run方法是一个while循环,不停地检测串口的输入流有无数据。这个循环由3组并列的条件语句组成:
l bDestroy为true,则退出循环,该变量的设置通过公共方法DestroyReadThread完成;
l 如果输入流中有数据,即ComPort.available()>0,则读取当前字节,lStart中记录当前字节到达的时间。第一个字节到达后,通过方法SetAvailable(false)将当前数据包设置为不可用,因为数据还没有接收组装完成;
l 如果当前字节到达的时间lStart大于0,则计算当前时间与lStart之间的时间间隔,如果大于规定的数值TimeToWait(在构造方法中设置),则认为数据接收结束,通过方法SetAvailable(true)将当前数据包设置为可用。run方法的源代码如下:
public void run(){
byte bIn; //存放读取的当前字节
try {
while(true){
if(bDestroy == true) return;
if (ComPort.available()>0){
bIn = (byte)ComPort.read();
if (lStart == 0) SetAvailable(false); //清空缓冲区
PutByte(bIn); //保存数据
sTime = Calendar.getInstance();
lStart = sTime.getTimeInMillis(); //当前读取数据的时间
}
if (lStart>0){
sTime = Calendar.getInstance();
lInterval = sTime.getTimeInMillis() - lStart;
if (lInterval >= TimeToWait) {
SetAvailable(true);
}
}
}
} catch (IOException e) {
System.out.println("IOException Error"+e);
}
}
3.private synchronized void PutByte(byte bIn)
该方法是私有方法,被run方法调用,将当前从串口输入流ComPort中读取的字节存入串口输入缓冲区ComBuffer中,如果缓冲区的最后一个字节的位置已经大于最大值nPackageLen(在构造方法中设置),则提示缓冲区溢出,同时,将缓冲区指针复位。ComBuffer.put((byte)bIn)语句将字节bIn存入缓冲区,这将使得缓冲区的指针(位置)后移一个字节,源代码如下:
private synchronized void PutByte(byte bIn){
if (ComBuffer.position() >= nPackageLen){
System.out.println("ComBuffer overflow!");
ComBuffer.rewind(); //指针复位
}
ComBuffer.put((byte)bIn);
}
4.private void SetAvailable(boolean bGet)
该方法是私有方法,被run方法和GetComBuffer方法调用。如果设置为false,则串口缓冲区ComBuffer中的内容被清除,指针(位置)复位,在首次收到数据包的头部数据时,执行该动作;如果在run方法中等待的时间片大于TimeToWait,则表示一个数据包接收结束,设置为true,同时,令lStart为0,表示如果收到新的数据,则认为是下一个数据包中的内容。SetAvailable方法的源代码如下:
private void SetAvailable(boolean bGet){
bAvailable = bGet;
if(bGet == false) {
ComBuffer.clear();
ComBuffer.rewind();
}
if(bGet == true) lStart = 0;
}
5.public void DestroyReadThread()
该公有方法用来将bDestroy的值设置为true,从而让run方法从while循环中退出,达到杀死进程的目的。其源代码如下:
public void DestroyReadThread(){
bDestroy = true;
}
6.public byte[] GetComBuffer()
该方法是公有方法,用来返回完整的数据包。输入缓冲区ComBuffer的指针(位置)即为数据包的长度。如果数据接收完成,则bAvailable为true,就先取得数据的长度,然后,将ComBuffer缓冲区的指针(位置)复位,这样,就可以从位置0处开始读取给定的字节。数据读取完毕,就通过SetAvailable(false)方法销毁数据,以免数据被重复读取,如此模仿Visual Basic 6.0 中的MSComm控件的动作。如果没有数据或者数据没有准备好,就返回null。
public byte[] GetComBuffer(){
int bLen;
byte[] bReceive;
if (bAvailable == true){ //数据接收结束
bLen = ComBuffer.position();
bReceive = new byte[bLen];
ComBuffer.rewind();
ComBuffer.get(bReceive, 0, bLen);
SetAvailable(false); //销毁数据
return bReceive;
}
else return null; //没有数据或数据没有准备好
}
四、串口类的发布
在NetBeans 5.0环境下完成代码编写后,可以单击项目名称,然后,选择生成项目,即可在项目的build\classes\SerialPort目录下,看到OperateCOM.class和ReadCOM.class。在C盘建立一个目录,如C:\JarPackage,将包含类文件的SerialPort文件夹复制到该文件夹。在DOS环境下进入JarPackage目录,输入如下命令(下划线所示):
C:\JarPackage> jar cvf SerialPort.jar *
即可得到将以上两个类打包后的jar文件。该命令的“c”表示创建新的文档,“v”表示生成详细信息到标准输出上,“f”表示制定存档文件名。
可以将该串口包复制到jre目录下的lib中,并在CLASSPATH系统环境变量中包含该包的绝对路径,即可通过“import SerialPort.*;”来使用串口类,并在DOS环境下利用javac命令对java源代码进行编译。如果在NetBeans 5.0环境下使用串口包,则需要添加库,并指出库的绝对路径。
五、串口类的测试
测试源代码除了需要引用上文生成的SerialPort包外,还要使用文献[3]中的ComputerMonitor包,用来灵活地处理数据,并以需要的形式进行显示。测试程序首先初始化串口,然后,在while循环中读取数据,以各种形式显示,如果收到的数据包的第一个字节为0x21(即字符”!”)则退出程序。测试类SerialExample的源代码如下:
import SerialPort.*;
import ComputerMonitor.*;
/** Creates a new instance of SerialExample */
public static void main(String[] args) {
OperateCOM SB = new OperateCOM(1, 1024);
// open COM1, the max length of package is 1024 bytes
if (SB.GetPortID() == -1) {
System.out.println("No such port!");
System.exit(1);
}
if (SB.Open("SerialExample", 100) == -1) {
System.out.println("Port in use now!");
System.exit(1);
}
if (SB.SetParams(9600, 8, 1, 0) == -1){
System.out.println("Unsupported operation!");
System.exit(1);
}
SB.StartCom(150); //延迟150毫秒
while(true){
byte[] bData = SB.ReadPort();
if (bData != null){
System.out.println("Receive and send back: ");
System.out.println("UTF-8: " +
ByteProcess.BytesToEnString(bData));
System.out.println("UTF-16BE: " +
ByteProcess.UnicodeToString(bData));
System.out.println("Bytes: " +
ByteProcess.InsertSpaceToHexChars(
ByteProcess.BytesToHexChars(bData)));
SB.WritePort(bData,0,bData.length);
if(bData[0] == 0x21){ //The first char is "!", so stop now!
SB.ClosePort();
return;
}
}
}
}
Java例程在NetBeans 5.0 环境下的调试方式运行,利用串口测试工具TestPort分别发送字节序列 FF 00 66 25和 21 4F 4B,结果如下图所示。在串行通信中,字节最高位为1,可能由于字符集或系统环境原因,导致最高位为0;而字节00通常作为高级语言中字符串的结束标志,这意味着字节00后面的数据将被截去。从下图中可以看出,所有数据均被完整接收,并可以实现原样发送返回。在UTF-8的表示中,0xFF和0对应的字符不可见,0x66对应字符“f”,0x25对应字符“%”;在UTF-16BE的表示中,两个字节表示一个汉字,0xFF00是不可见汉字(或没有此汉字),0x6625表示汉字“春”。由于第二个数据包的第一个字节是0x21,程序退出,与设计效果完全一样。
六、结语
本文利用Java Communications API函数,设计了OperateCOM与ReadCOM类,用来便捷地进行串口数据的收发,并给出了应用实例和测试结果。该程序具有通用性,可以用于相关的嵌入式系统设备的二次开发中。完整的源代码可以从本刊网站下载。
|