| 
				 一、引言 
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类,用来便捷地进行串口数据的收发,并给出了应用实例和测试结果。该程序具有通用性,可以用于相关的嵌入式系统设备的二次开发中。完整的源代码可以从本刊网站下载。 
  			
				 |