欢迎来到 安卓源码空间!
安卓源码空间

                                    JAVA串口通信开发



JAVA串口通信开发





前言


最近几个月一直在接触串口,与硬件打交道,还是学到了不少之前没听过的东西,特此记录一下,其中不免有语焉不详或一知半解的地方,欢迎各位指教。



提示:以下是本篇文章正文内容,下面案例可供参考


一、项目背景


首先说串口是什么,百度上说串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。串行接口 (Serial Interface)是指数据一位一位地顺序传送。实际上就是传输数据用的物理接口,一般可以按照接线方式分为RS-232和RS-485,对于程序开发来说,这两者并没有什么不同。
之后说一下实际使用的项目背景,首先会有一台计算机,计算机上有一排物理串口,串口上接的是232的控制器,控制器连接实际的机械设备。而我们的目前是使用程序向232控制器发生指令来操控机械设备实现不同动作,程序最终会以HTTP接口的方式对外暴露。


二、实际开发


1.引入库


对于JAVA的串口通信开发,一般能查到的都是使用RXTXcomm.jar,同时需要rxtxParallel.dll和rxtxSerial.dll两个dll文件。最开始我也是使用了这种方式,但是后连在实际测试中发现了一个非常致命的问题,因为我的程序最后是一组HTTP接口,所以避免不了会同时对多个串口操作,而一旦发生同时或短时间内操作多个串口时,程序会崩溃,类似这样。


在这里插入图片描述

这个问题我实在没有搞清楚产生的原因,我怀疑可能使用的RXTXcomm并不支持同时操作,另外可能与JDK版本有关,建议1.8.0_144。
因为产生了这个问题目前又无法解决,所以最终我决定换一个驱动,不再采用RXTXcomm,而是选用了purejavacomm。purejavacomm使用的是JNA,并不需要额外引用DLL,使用方式与RXTXcomm相同,还是比较便捷的,我个人觉得绝对是JAVA串口开发最好的驱动了,可以直接用pom引用。



	<dependency> <groupId>com.github.purejavacomm</groupId> <artifactId>purejavacomm</artifactId> <version>1.0.1.RELEASE</version> </dependency>  

2.串口通信工具类


废话不多说,代码如下:


package com.water.api.util; 
  import java.io.IOException; 
  import java.io.InputStream; 
  import java.io.OutputStream; 
  import java.util.ArrayList;  
 import java.util.Enumeration;  
 import java.util.TooManyListenersException;  
 import org.slf4j.Logger; 
  import org.slf4j.LoggerFactory;  
 import org.springframework.stereotype.Component;  
 import purejavacomm.CommPort;  
 import purejavacomm.CommPortIdentifier;  
 import purejavacomm.NoSuchPortException;  
 import purejavacomm.PortInUseException;  
 import purejavacomm.SerialPort; 
  import purejavacomm.SerialPortEventListener; 
  import purejavacomm.UnsupportedCommOperationException; 
  @Component
public class SerialTool {  
 private static Logger logger = LoggerFactory.getLogger(SerialTool.class);  
 public static final ArrayList<String> findPorts() {  
 // 获得当前所有可用串口 
  Enumeration<CommPortIdentifier> portList = CommPortIdentifier.getPortIdentifiers(); 
  ArrayList<String> portNameList = new ArrayList<String>(); 
  // 将可用串口名添加到List并返回该List  
 while (portList.hasMoreElements()) {  
 String portName = portList.nextElement().getName(); 
  portNameList.add(portName); } 
  return portNameList;  
 } 
  /**
	 * 打开串口
	 * 
	 * @param portName
	 *            端口名称
	 * @param baudrate
	 *            波特率
	 * @return 串口对象
	 * @throws Exception
	 * @throws SerialPortParameterFailure
	 *             设置串口参数失败
	 * @throws NotASerialPort
	 *             端口指向设备不是串口类型
	 * @throws NoSuchPort
	 *             没有该端口对应的串口设备
	 * @throws PortInUse
	 *             端口已被占用
	 */ public static SerialPort openPort(String portName, Integer baudrate, Integer dataBits, Integer stopBits, Integer parity) 
  throws Exception { try {  
 // 通过端口名识别端口 CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName); 
  // 打开端口,并给端口名字和一个timeout(打开操作的超时时间) CommPort commPort = portIdentifier.open(portName, 2000); 
  // 判断是不是串口 if (commPort instanceof SerialPort) { SerialPort serialPort = (SerialPort) commPort; 
  try { // 设置一下串口的波特率等参数 serialPort.setSerialPortParams(baudrate, dataBits, stopBits, parity); 
  logger.info("串口" + portName + "打开成功"); } 
  catch (UnsupportedCommOperationException e) { 
  logger.error("设置串口" + portName + "参数失败:" + e.getMessage()); throw e; }  
 return serialPort; } else { logger.error("不是串口" + portName); // 不是串口 throw new Exception(); } } 
  catch (NoSuchPortException e1) { logger.error("无此串口" + portName); throw e1; }  
 catch (PortInUseException e2) { logger.error("串口使用中" + portName); throw e2; }  
 catch (Exception e) { throw e; } } 
  public static byte[] HexString2Bytes(String src)  
 { if (null == src || 0 == src.length()) { return null; } byte[] ret = new byte[src.length() / 2]; 
  byte[] tmp = src.getBytes(); 
  for (int i = 0; i < (tmp.length / 2); i++) { 
  ret[i] = uniteBytes(tmp[i * 2], tmp[i * 2 + 1]); } return ret; }  
 // byte类型数据,转成十六进制形式;  
 public static byte uniteBytes(byte src0, byte src1) { byte _b0 = Byte.decode("0x" + new String(new byte[] {  
 src0 })).byteValue();  
 _b0 = (byte) (_b0 << 4); 
  byte _b1 = Byte.decode("0x" + new String(new byte[]  
 { src1 })).byteValue(); 
  byte ret = (byte) (_b0 ^ _b1); 
  return ret; } /**
	 * 关闭串口
	 * 
	 * @throws IOException
	 */  
 public static synchronized void closePort(SerialPort serialPort)  
  throws IOException { 
  if (serialPort != null) { 
  serialPort.close();  
 logger.info("串口" + serialPort.getName() + "已关闭");  
 }  
 }  
 /**
	 * 往串口发送数据
	 * 
	 * @param order
	 *            待发送数据
	 * @throws SendDataToSerialPortFailure
	 *             向串口发送数据失败
	 * @throws SerialPortOutputStreamCloseFailure
	 *             关闭串口对象的输出流出错
	 */ 
  public static void sendToPort(byte[] order, SerialPort serialPort) throws IOException { 
 

  OutputStream out = null; 
  try {  
 out = serialPort.getOutputStream(); out.write(order); out.flush(); 
  logger.info("发送数据成功" + serialPort.getName()); } catch (IOException e) { logger.error("发送数据失败" + serialPort.getName()); throw e; } 
  finally { try { if (out != null) { out.close(); out = null; } } catch (IOException e) { logger.error("关闭串口对象的输出流出错"); throw e; } } } 
  /**
	 * 从串口读取数据
	 * 
	 * @param serialPort
	 *            当前已建立连接的SerialPort对象
	 * @return 读取到的数据
	 * @throws ReadDataFromSerialPortFailure
	 *             从串口读取数据时出错
	 * @throws SerialPortInputStreamCloseFailure
	 *             关闭串口对象输入流出错
	 */ 
  public static byte[] readFromPort(SerialPort serialPort) throws Exception {  
 InputStream in = null; byte[] bytes = null; 
  try {  
 if (serialPort != null) { in = serialPort.getInputStream(); } else { return null; } int bufflenth = in.available();  
 // 获取buffer里的数据长度  
 while (bufflenth != 0) { bytes = new byte[bufflenth];  
 // 初始化byte数组为buffer中数据的长度  
 in.read(bytes); bufflenth = in.available(); } }  
 catch (Exception e) { throw e; } 
  finally {  
 try { 
  if (in != null) { in.close(); in = null; } }  
 catch (IOException e) { throw e; } } 
  return bytes; } 
  /**
	 * 添加监听器
	 * 
	 * @param port
	 *            串口对象
	 * @param listener
	 *            串口监听器
	 * @throws TooManyListeners
	 *             监听类对象过多
	 */  
 public static void addListener(SerialPortEventListener listener, SerialPort serialPort) throws TooManyListenersException { 
  try {  
 // 给串口添加监听器 
  serialPort.addEventListener(listener);  
 // 设置当有数据到达时唤醒监听接收线程 
  serialPort.notifyOnDataAvailable(true); 
  // 设置当通信中断时唤醒中断线程 
  serialPort.notifyOnBreakInterrupt(true); }  
 catch (TooManyListenersException e) { throw e; } } }  
 


这部分其实没什么好说的,网上一搜一堆,唯一需要注意的是,需要考虑在程序里每一个串口的生命周期。对于一个串口,系统全局应当只有一个实例,频繁的开关串口并不是一个好的选择。


上面的代码只是说明了如何向串口发送数据,从串口中读数据需要添加监听,当有数据返回时会把数据推送到监听里。当然这种方式使用起来非常不爽,因为发送和接收是异步的,换句话说,发送指令是一个线程,而接收数据又是一个线程,实际使用中,很多时候需要得到返回值,然后来判断接下来发送的指令,这样代码写起来非常复杂,作为一个使用者肯定希望在同一位置完成收发,所以最后我封装了一个操作类来完成收发。


 public class SerialResquest {  
 private static Logger logger = LoggerFactory.getLogger(SerialResquest.class); 
  public static void resquest(String portName, Integer baudrate, Integer dataBits, Integer stopBits, Integer parity,byte[] data) throws Exception {  
 SerialPort serialPort; if (!GlobalCache.smap.containsKey(portName)) {  
 GlobalCache.bmap.put(portName, false); 
  serialPort = SerialTool.openPort(portName, baudrate, dataBits, stopBits, parity); 
  GlobalCache.smap.put(portName, serialPort);  
 SerialTool.addListener(new SerialPortEventListener() { @Override
				public void serialEvent(SerialPortEvent event) { try { Thread.sleep(50); }  
 catch (InterruptedException e1) { logger.error("SerialResquest 监听异常!"+e1); } 
  switch (event.getEventType()) {  
 case SerialPortEvent.DATA_AVAILABLE: byte[] readBuffer = null; int availableBytes = 0;  
 try { availableBytes = serialPort.getInputStream().available(); if (availableBytes > 0) { 
  try { readBuffer = SerialTool.readFromPort(serialPort); GlobalCache.bmap.put(portName, true); 
  GlobalCache.dmap.put(portName, readBuffer); } catch (Exception e) { logger.error("读取推送信息异常!"+e); } } }  
 catch (IOException e) { logger.error("读取流信息异常!"+e); } } } }, serialPort); }else  
 { serialPort = GlobalCache.smap.get(portName); } SerialTool.sendToPort(data, serialPort); } 
  public static byte[] response(String portName) throws InterruptedException { /*if (!GlobalCache.dmap.containsKey(portName)) { 
  return null;
		}*/ Thread.sleep(100); int i =0; while (!GlobalCache.bmap.get(portName))  
 { Thread.sleep(100); if (i++>30) { return new byte[0]; } } GlobalCache.bmap.put(portName, false);  
 return GlobalCache.dmap.get(portName); } public static void close(String portName) throws IOException  
 { SerialTool.closePort(GlobalCache.smap.get(portName)); GlobalCache.smap.remove(portName); } }  
 


对于上面的代码,可以调用resquest方法来完成发送指令,如果系统没有当前串口实例,会生成一个并添加监听,如果有,则直接使用实例发送指令,通过response方法来接收返回值。需要注意的是,在监听中做了一次Thread.sleep(50),这是因为实际使用时发现返回值是断断续续的,例如发送一条指令A,理论上应该立即返回一条结果如AABBCCDD,但是实际会多次返回不同的部分,如先返回AA,然后返回BBC,每次返回的不完整,可能的原因是程序调用的是CPU资源,串口返回值是走串口连接线,速度上有差异,sleep后可以得到完整的返回值。如果实际串口连接线比较长,可以适当增大sleep时间。


3.数据解析


上面说过,与程序通过串口通信的实际上是控制器,当然有些设备也可以直接连接串口通信。与这些硬件通信的时候,避免不了数据交互,有些设备可能返回的数据比较友好,可以直观的看到数据值,但大部分返回的都需要解析。
数据的解析方式需要依据设备厂商提供的文档,但是原理大同小异,一般来说,返回的数据格式为short或float居多。而我们从程序读到的都是字节数组,我们需要做的就是把字节转成short或float。对于short,我们知道它占两字节,也就是16bit,那么我们只需要知道字节组中哪两位代表了一个short就可以解析出这个值,方法如下:

	public static short toShort(byte b1, byte b2) { return (short) (b1 << 8 | b2 & 0xFF); }  

对于float,占四个字节,也就是32bit,同样知道字节组中哪四位代表了一个float就可以解析出这个值,方法如下:

private float bytes2Float(byte[] bytes) { String BinaryStr = bytes2BinaryStr(bytes);  
 // 符号位S  
 Long s = Long.parseLong(BinaryStr.substring(0, 1)); 
  // 指数位E  
 Long e = Long.parseLong(BinaryStr.substring(1, 9), 2);  
 // 位数M  
 String M = BinaryStr.substring(9); 
  float m = 0, a, b; for (int i = 0; i < M.length(); i++) { a = Integer.valueOf(M.charAt(i) + ""); 
  b = (float) Math.pow(2, i + 1); m = m + (a / b); } 
  Float f = (float) ((Math.pow(-1, s)) * (1 + m) * (Math.pow(2, (e - 127))));  
 return f; }  
 private static String bytes2BinaryStr(byte[] bytes) { 
  StringBuffer binaryStr = new StringBuffer(); 
  for (int i = 0; i < bytes.length; i++)  
 { 
  String str = Integer.toBinaryString((bytes[i] & 0xFF) + 0x100).substring(1); 
  binaryStr.append(str); } return binaryStr.toString();  
 } 
 


总结


说了半天可能有些东西还是没说明白,或者我自己也没有理解。如果您有类似的困惑,欢迎与我联系,我们可以一起探讨。



copyright@ 2020-2028  安卓源码空间网版权所有   

备案号:豫ICP备2023034476号-1号