Android串口通信:抱歉,学会它真的可以为所欲为

Android串口

Posted by 霜刃西瓜 on October 28, 2021

引言:

之所以写这篇文章,一方面是最近工作中对 Android 串口通信方面学习的总结。另外一方面也希望能够帮助到大家,能够简单的去理解串口通信方面的知识。

为什么学习 Android 串口通信:

  • 距离 2008 年发布第一款 Android 手机已经过去了 10 多年的时光了。现在 Android 的发展是百花齐放,尤其是对于很多公司而言,Android 主板与各种传感器和智能设备之间通信是很常见的事情了,那么对于开发人员而言,学习串口通信是必要的事情了。
  • 学习串口通信,能够提前了解 JNI 和 NDK 的知识,算是一个入门预习吧
  • Google 出品,必属精品。我们现在市面上的所有 Android 串口通信的源代码都是 Google 公司在 2011 年开源的Google 官方源代码,学习它也不妨是学习 Google 的设计思维。

集成串口通信:

导入.so 文件

什么是.so 文件:

  • .so 文件是 Unix 的动态连接库,本身是二进制文件,是由 C/C++编译而来的。
  • Android 调用.so 文件的过程也就是所谓的 JNI 了。在 Android 中想要调用 C/C++中的 API 的话,也就是调用.so 文件了。

一 、 复制图上所示的三个.so 文件的文件夹,到 Project –>app –>libs(没有就自己新建 libs)

.so文件 二 、 配置 Gradle 文件:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.1"
    defaultConfig {
        applicationId "cn.humiao.myserialport"
        minSdkVersion 14
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    //这里是配置JNI的引用地址,也就是引用.so文件
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

总结:就这样的几步,配置 OK 了,就能愉快的在 Java 里面直接调用 C/C++了。啦啦啦啦 ~

就是这么简单

Google 串口代码分析:

SerialPort

/**
 * Google官方代码
 *
 * 此类的作用为,JNI的调用,用来加载.so文件的
 *
 * 获取串口输入输出流
 */

public class SerialPort {

    private static final String TAG = "SerialPort";

    /*
   * Do not remove or rename the field mFd: it is used
   * close();
   */
    private FileDescriptor mFd;
    private FileInputStream mFileInputStream;
    private FileOutputStream mFileOutputStream;

    public SerialPort(File device, int baudrate, int flags)
            throws SecurityException, IOException {

    /* Check access permission */
        if (!device.canRead() || !device.canWrite()) {
            try {
                /* Missing read/write permission, trying to chmod the file */
                Process su;
                su = Runtime.getRuntime().exec("/system/bin/su");
                String cmd = "chmod 666 " + device.getAbsolutePath() + "\n"
                        + "exit\n";
                su.getOutputStream().write(cmd.getBytes());
                if ((su.waitFor() != 0) || !device.canRead()
                        || !device.canWrite()) {
                    throw new SecurityException();
                }
            } catch (Exception e) {
                e.printStackTrace();
                throw new SecurityException();
            }
        }

        System.out.println(device.getAbsolutePath() + "==============================");
        //开启串口,传入物理地址、波特率、flags值
        mFd = open(device.getAbsolutePath(), baudrate, flags);
        if (mFd == null) {
            Log.e(TAG, "native open returns null");
            throw new IOException();
        }
        mFileInputStream = new FileInputStream(mFd);
        mFileOutputStream = new FileOutputStream(mFd);
    }

     //获取串口的输入流
    public InputStream getInputStream() {
        return mFileInputStream;
    }

    //获取串口的输出流
    public OutputStream getOutputStream() {
        return mFileOutputStream;
    }

    // JNI调用,开启串口
    private native static FileDescriptor open(String path, int baudrate, int flags);
    //关闭串口
    public native void close();
    static {
        System.out.println("==============================");
        //加载库文件.so文件
        System.loadLibrary("serial_port");
        System.out.println("********************************");
    }
}

一:类作用及介绍

通过打开 JNI 的调用,打开串口。获取串口通信中的输入输出流,通过操作 IO 流,达到能够利用串口接收数据和发送数据的目的

二:类方法介绍

A : 开启串口的方法:private native static FileDescriptor open(String path, int baudrate,int flags)

  • path:

    为串口的物理地址,一般硬件工程师都会告诉你的例如 ttyS0、ttyS1 等,或者通过 SerialPortFinder 类去寻找得到可用的串口地址。

  • baudrate:

    波特率,与外接设备一致

  • flags:

    设置为 0,原因较复杂,见文章最底下。

B : IO 流,如果你不是很明白输入输出流的话,可以看看这篇文章史上最简单的 IO 流解释

mFileInputStream = new FileInputStream(mFd);// 输入流,也就是获取从单片机或者传感器,通过串口传入到Android主板的IO数据(使用的时候,执行Read方法)
mFileOutputStream = new FileOutputStream(mFd);//输出流,Android将需要传输的数据发送到单片机或者传感器(使用的时候,执行Write方法)

获取了输入输出流以后就可以对流进行操作了。 我爱学习

顺便科普一下我所理解的 IO 流:

  • 将 InputStream 和 OutputStream 的流当做一个管道,所有的 byte 流(也可以说是二进制流,因为 byte 里面存储的都是 bit)都是流动在这个管道里面的。管道通道两边的一边是内存,一边是外部存储。
  • 所谓的写(Write),也就是将内存的数据写到外部存储。反之,读(Read)也就是将外部存储的数据读取到内存里面。
  • 对于 OutputStream 与 Write 结合使用:
  • 示例代码,不能直接运行,我只是讲一下我的思路:
byte[] sendData = DataUtils.HexToByteArr(data);
outputStream.write(sendData);
outputStream.flush();

首先是获取 Byte 数组,然后通过 wite()方法将 Byte 数组放到管道 OutputStream 中,然后 wirte 出去,写出到外部存储。

  • 对于 InputStream 与 Read 结合使用:

示例代码,不能直接运行,我只是讲一下我的思路:

byte[] readData = new byte[1024];
int size = inputStream.read(readData);
String readString = DataUtils.ByteArrToHex(readData, 0, size);

首先,new 一个 byte 数组,也就是在内存里面开辟一个空间用来存储 byte 字节。然后管道 InputStream 中的外部数据通过 read 方法,读取到内存里面,也就是 byte[]中,返回值是读取的大小。然后再将 byte[]转换为 String。

  • OutputStream 和 InputStream 两个管道。输出流的管道里面是没有数据的,需要将数据写入;输入流的管道里是有数据的,需要读出来。

    学习啊

SerialPortFinder

  • 这个类是用来获取串口物理地址的,其实一般是用不到这个类的,因为硬件设备上串口的物理地址,在硬件上都是有具体标识的,你直接使用就可以了。Google 既然有这个帮助类的话,我们就具体分析一波。
  • 具体是这样的:
  1. 在这个方法中:Vector< Driver > getDrivers() throws IOException 读取 /proc/tty/drivers 里面的带有 serial,也就是标识串口的地址,然后保存在一个集合里面,例如在我的 Android 设备中: drivers的物理地址 如图示,有八个不同类型的串口驱动地址。对我而言我需要的是/dev/ttyS

  2. Vector< File > getDevices()方法中,读取/dev 下的地址。但是如图示,地址有很多,我们需要的是串口,那么就将 drivers 中读取的地址,与/dev 中的地址匹配,成功的则存储到集合中。于是就成功获取到了所有串口地址了。

dev地址

  1. 上面是具体的思路,我们一般使用的都是下面这个方法:
public String[] getAllDevicesPath() {
    Vector<String> devices = new Vector<String>();
    // Parse each driver
    Iterator<Driver> itdriv;
    try {
      itdriv = getDrivers().iterator();
      while(itdriv.hasNext()) {
        Driver driver = itdriv.next();
        Iterator<File> itdev = driver.getDevices().iterator();
        while(itdev.hasNext()) {
          String device = itdev.next().getAbsolutePath();
          devices.add(device);
        }
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
    return devices.toArray(new String[devices.size()]);
  }
  //获取在设备目录下的,所有串口的具体物理地址,并且存入到数组里面。
  • 能够获取所有的串口的具体地址,然后进行选择你需要的物理地址就行了。一般来说的话,串口地址为: /dev/ttyS2、/dev/ttyS1、/dev/ttyS0

SerialPortUtil

public class SerialPortUtil {

    private SerialPort serialPort = null;
    private InputStream inputStream = null;
    private OutputStream outputStream = null;
    private ReceiveThread mReceiveThread = null;
    private boolean isStart = false;

    /**
     * 打开串口,接收数据
     * 通过串口,接收单片机发送来的数据
     */
    public void openSerialPort() {
        try {
            serialPort = new SerialPort(new File("/dev/ttyS0"), 9600, 0);
            //调用对象SerialPort方法,获取串口中"读和写"的数据流
            inputStream = serialPort.getInputStream();
            outputStream = serialPort.getOutputStream();
            isStart = true;

        } catch (IOException e) {
            e.printStackTrace();
        }
        getSerialPort();
    }

    /**
     * 关闭串口
     * 关闭串口中的输入输出流
     */
    public void closeSerialPort() {
        Log.i("test", "关闭串口");
        try {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
            isStart = false;
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 发送数据
     * 通过串口,发送数据到单片机
     *
     * @param data 要发送的数据
     */
    public void sendSerialPort(String data) {
        try {
            byte[] sendData = DataUtils.HexToByteArr(data);
            outputStream.write(sendData);
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void getSerialPort() {
        if (mReceiveThread == null) {

            mReceiveThread = new ReceiveThread();
        }
        mReceiveThread.start();
    }

    /**
     * 接收串口数据的线程
     */

    private class ReceiveThread extends Thread {
        @Override
        public void run() {
            super.run();
            while (isStart) {
                if (inputStream == null) {
                    return;
                }
                byte[] readData = new byte[1024];
                try {
                    int size = inputStream.read(readData);
                    if (size > 0) {
                        String readString = DataUtils.ByteArrToHex(readData, 0, size);
                        EventBus.getDefault().post(readString);
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }

}

一、类的作用:

  • 实例化类 SerialPort,传入地址和波特率和 flags 值,获取串口的 IO 流,然后对 IO 流进行操作。
  • 注意一点:我这里因为与 Android 串口连接的设备都是需要的 16 进制的指令。所以我利用封装的工具类,将字符"010100000000FFFF"转换为 16 进制的<font color=#red> 010100000000FFFF</font>,也就是转 Hex 字符串为字节数组。

二、发送数据:

byte[] sendData = DataUtils.HexToByteArr(data);//转Hex字符串转字节数组
outputStream.write(sendData);//写入数组到输出流
outputStream.flush();//刷新

三 获取数据: 因为是读取流,所以我专门开一个线程去读取流:

private class ReceiveThread extends Thread {
        @Override
        public void run() {
            super.run();
            //是否开启串口
            while (isStart) {
                if (inputStream == null) {
                    return;
                }
                //new一个Byte数组
                byte[] readData = new byte[1024];
                try {
                //将流中数据读到Byte数组中,返回读的Size大小
                    int size = inputStream.read(readData);
                    if (size > 0) {
                    //将Byte数组转换了String
                        String readString = DataUtils.ByteArrToHex(readData, 0, size);
                        //跨线程通信,利用EventBus将数据传输到主线程,也可使用Handler
                        EventBus.getDefault().post(readString);
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }
  • 别的都没什么可讲的了,看注释就行。其中重点是"布尔值:isStart"。因为开启的线程是一直接收串口的数据的,如果不设置 isStart 的话,那么接收数据就只会执行一次,因为开启串口的时候才会 new 线程或者复用线程。而开启串口肯定只会执行一次,当然接收线程只会一次了。当时我没有写这个就被坑了半天时间! 于是进行条件设置用 While 函数,那么只要是isStart == True,那么线程就会一直执行下去!这就是条件判断的魅力! 学习一万年

一句话总结

Android 串口通信:抱歉,学会它真的可以为所欲为

—————————————————————————————————-

对于串口的 Flag 为 0 的理解 :

  1. 打开串口的时候,一直很奇怪,为什么会存在 flag 为 0。查询了很多资料,有的人说是因为这个是一个校验位Android 串口通信

  2. 有的人说这个并没有任何的作用和意义。

  3. 我从 Google 的本身的代码。去研究发现,其 flags 在 C++里面的代码是: 底层C++代码

    关键的代码是:
fd = open(path_utf, O_RDWR | flags) ;

open()函数:

其中 O_RDWR 表示可以读也可以写 ,为 Linux 下的 Open 函数里面的值。 根据open 各个参数含义,我们可以知道,这是常用的一种用法,fd 是设备描述符,linux 在操作硬件设备时,屏蔽了硬件的基本细节,只把硬件当做文件来进行操作,而所有的操作都是以 open 函数来开始,它用来获取 fd,然后后期的其他操作全部控制 fd 来完成对硬件设备的实际操作。

读写的 Hex 数含义:

对于 O_RDWR 的 16 进制的值是:02H , 读写参数的 Hex 数值

  • #define O_RDONLY 00
  • #define O_WRONLY 01
  • #define O_RDWR 02

分析这段代码:fd = open(path_utf, O_RDWR | flags) ;

一 、 在 Open 函数里面,传入 flags 值为 0,进行按位或运算,得到的结果还是 O_RDWR(02H),没有任何区别。所以说,其 flags 的值没有啥意义,也可以是这样理解的。

二 、 但是,我想 Google 之所以这样子去设计,肯定是有意义的,和公司的硬件大佬 CJ 大佬讨论以后。有以下的几点思考:

  • 对于 Open 函数本身而言

    会根据不同的值,进行不同的操作,如可读写(O_RDWR),非阻塞(O_NONBLOCK)等等。因为 Open 函数本身不仅仅是操作,还可以操作各种文件。所以需要很多的设定。

  • 在 Open 函数中

    对于串口的操作,肯定是必须可读可写,所以有 O_RDWR。而与 flags 按位或运算。其根本目是,按位或的运算后的结果最后一位必须为 2,例如 xx 02 或者 xx x2 。因为对于底层代码而言,最后一位是判断你是否可以读写的根本。所以 Google 的意思是最后一位必须固定不变,为"2",所以只需要保证最后一位是 2 就好,我试了试传 10H 也就是 flags 为 16(十进制),得到的结果为 12H,最后一位为 2。发现完全是可以正常通信的。bingo ~ 结论正确

你随意哦

结论:我倾向于理解为 flags 的值有意义的。

意义:必须保证串口的可读可写性,也就是其最后一位为 2 的条件下,进行拓展功能,如传入的 flags 值为 04000H(O_NONBLOCK 非阻塞),进行按位或得到 04002,那么最后串口的 open 操作既能读写,又是非阻塞的。不过一般你使用直接设置为"0"即可 ~ ~ ~嘻嘻 :)

—————————————————————————————————-

源代码

—————————————————————————————————-

参考

Android 串口通信

android 串口通信——android-serialport-api

Android Studio 下的串口程序开发实战