JAVA串口通信开发

JAVA串口通信开发提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 JAVA 串口通信开发前言一 项目背景二 实际开发 1 引入库 2 串口通信工具类 3 数据解析总结前言最近几个月一直在接触串口 与硬件打交道 还是学到了不少之前没听过的东西 特此记录一下 其中不免有语焉不详或一知半解的地方 欢迎各位指教 提示 以下是本篇文章正文内容 下面案例可供参考一 项目背景首先说串口是什么 百度上说串行接口简称串口 也称串行通信接口或串行通讯接口 通常指 COM 接口 是采用串行通信方式的扩展接口 串行接口


前言

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



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

一、项目背景

首先说串口是什么,百度上说串行接口简称串口,也称串行通信接口或串行通讯接口(通常指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(); } 

总结

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/229411.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月16日 下午4:46
下一篇 2026年3月16日 下午4:46


相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号