简单记录Android串口的使用

adb  Android  NDK  2024年11月10日 am8:08发布5天前更新 91es.com站长
8 0 0

前言

简单记录一下Android串口的使用。记录于此,方便自己查阅。

在Android系统上无法直接使用设备的串口,因为Android平台没有提供操作串口的API。不过可以使用库文件来操作串口,本文使用github上开源的《android-serialport-api》进行打开串口,亲测有效。

小结

下面记录一下遇到的问题

串口打不开或无法通信
  1. 没有SELInux权限

    如果临时测试,可以使用adb shell setenforce 0,否则需要配置权限(这个后面会单独记录一下怎设置)

  2. 串口不存在或串口没有权限

    串口错误就找对应的问硬件或BSP

    没有权限就临时改变一下试试,进入dev目录 chmod 777 ttyAT2

  3. 无法通信,问一下波特率

如何判断串口通信成功

其实就是短路一下RX和TX,然后使用查看是否能收到自己发送的数据。一般是找硬件接线短路,短路后使用如下方法。

  1. cmd 使用命令echo 和cat 对应串口

  2. 使用UartAssist和下面的打开窗口工具进行收发数据

如果收发正常,表示窗口是通的。

正文

这里需要使用NDK,如果没有配置过,可以看一下《Android NDK配置简单记录》。

第一步

创建一个工程

工程名   :BiuSerial
工程包名 : com.biumall.serial

上面看自己定义,如果包名不一样,下面需要改。

第二步

如果对JNI不熟悉的,可以看《JNI静态注册

先下载《android-serialport-api》代码,然后把

android-serialport-api/project/jni

中代码拷贝到自己的工程

BiuSerial\jni

这里部分代码需要改动,暂时先跳过。

第三步

定义native方法,就是打开和关闭串口的接口。(根据上面JNI中的方法定义)

SerialPort.java
package com.biumall.serial.manager;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class SerialPort {
    public FileDescriptor mFd;
    private final FileInputStream mFileInputStream;
    private final FileOutputStream mFileOutputStream;

    public SerialPort(File device, int baudRate, int flags) throws SecurityException, IOException {
        mFd = open(device.getAbsolutePath(), baudRate, flags);
        if (mFd == null) {
            throw new IOException();
        }
        mFileInputStream = new FileInputStream(mFd);
        mFileOutputStream = new FileOutputStream(mFd);
    }
    
    public InputStream getInputStream() {
        return mFileInputStream;
    }

    public OutputStream getOutputStream() {
        return mFileOutputStream;
    }
    //定义native方法
    private native static FileDescriptor open(String path, int baudRate, int flags);
    public native void close();
    //加载so库,我这里改了加载SerialPort.so哈
    static {
        System.loadLibrary("SerialPort");
    }
}

注意这里的包名是com.biumall.serial.manager

根据《JNI静态注册》可以通过javac和javah进行编译出native方法在JNI的定义。(静态注册)

这里的对应方法名为:

JNIEXPORT jobject JNICALL Java_com_biumall_serial_manager_SerialPort_open
        (JNIEnv *, jclass, jstring, jint, jint);
        
JNIEXPORT void JNICALL Java_com_biumall_serial_manager_SerialPort_close
(JNIEnv *, jobject);

因为应用包名是我们自己的(当然也可以跟android-serialport-api一样,一样的话不需要修改,直接用即可)。

我们包名不一样,因此需要改变,带jni中SerialPort.h和SerialPort.c代码,然后替换上面方面名即可。

比如

JNIEXPORT jobject JNICALL Java_android_1serialport_1api_SerialPort_open
换成
JNIEXPORT jobject JNICALL Java_com_biumall_serial_manager_SerialPort_open
JNIEXPORT void JNICALL Java_android_1serialport_1api_SerialPort_close
换成
JNIEXPORT void JNICALL Java_com_biumall_serial_manager_SerialPort_close

其他的可以不变。

下面是我这边完整的,部分删减了。

SerialPort.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
#ifndef SerialPort_H
#define SerialPort_H
#ifdef __cplusplus
extern "C" {
#endif

/*
 * Class:     com_biumall_serial_manager_SerialPort
 * Method:    open
 * Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;
 */
JNIEXPORT jobject JNICALL Java_com_biumall_serial_manager_SerialPort_open
        (JNIEnv *, jclass, jstring, jint, jint);

/*
 * Class:     com_biumall_serial_manager_SerialPort
 * Method:    close
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_biumall_serial_manager_SerialPort_close
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
SerialPort.c
#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <jni.h>
#include "SerialPort.h"
#include "android/log.h"
static const char *TAG="SerialApp_native_v2_";
#define LOGI(fmt, args...) __android_log_print(ANDROID_LOG_INFO,  TAG, fmt, ##args)
#define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, TAG, fmt, ##args)
#define LOGE(fmt, args...) __android_log_print(ANDROID_LOG_ERROR, TAG, fmt, ##args)

static speed_t getBaudRate(jint baudRate)
{
	switch(baudRate) {
	case 0: return B0;
	case 50: return B50;
	case 75: return B75;
	case 110: return B110;
	case 134: return B134;
	case 150: return B150;
	case 200: return B200;
	case 300: return B300;
	case 600: return B600;
	case 1200: return B1200;
	case 1800: return B1800;
	case 2400: return B2400;
	case 4800: return B4800;
	case 9600: return B9600;
	case 19200: return B19200;
	case 38400: return B38400;
	case 57600: return B57600;
	case 115200: return B115200;
	case 230400: return B230400;
	case 460800: return B460800;
	case 500000: return B500000;
	case 576000: return B576000;
	case 921600: return B921600;
	case 1000000: return B1000000;
	case 1152000: return B1152000;
	case 1500000: return B1500000;
	case 2000000: return B2000000;
	case 2500000: return B2500000;
	case 3000000: return B3000000;
	case 3500000: return B3500000;
	case 4000000: return B4000000;
	default: return -1;
	}
}

/*
 * Class:     com_biumall_serial_manager_SerialPort
 * Method:    open
 * Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;
 */
JNIEXPORT jobject JNICALL Java_com_biumall_serial_manager_SerialPort_open
  (JNIEnv *env, jclass thiz, jstring path, jint baudRate, jint flags)
{
	int fd;
	speed_t speed;
	jobject mFileDescriptor;
	/* Check arguments */
	{
		speed = getBaudRate(baudRate);
		if (speed == -1) {
			LOGE("Invalid baudRate");
			return NULL;
		}
	}

	/* Opening device */
	{
		jboolean iscopy;
		const char *path_utf = (*env)->GetStringUTFChars(env, path, &iscopy);
		LOGD("Opening serial port %s with flags 0x%x", path_utf, O_RDWR | flags);
		fd = open(path_utf, O_RDWR | flags);
		LOGD("open() fd = %d", fd);
		(*env)->ReleaseStringUTFChars(env, path, path_utf);
		if (fd == -1)
		{
			LOGE("Cannot open port");
			return NULL;
		}
	}

	/* Configure device */
	{
		struct termios cfg;
		LOGD("Configuring serial port");
		if (tcgetattr(fd, &cfg))
		{
			LOGE("tcgetattr() failed");
			close(fd);
			return NULL;
		}

		cfmakeraw(&cfg);
		cfsetispeed(&cfg, speed);
		cfsetospeed(&cfg, speed);

		if (tcsetattr(fd, TCSANOW, &cfg))
		{
			LOGE("tcsetattr() failed");
			close(fd);
			return NULL;
		}
	}

	/* Create a corresponding file descriptor */
	{
		jclass cFileDescriptor = (*env)->FindClass(env, "java/io/FileDescriptor");
		jmethodID iFileDescriptor = (*env)->GetMethodID(env, cFileDescriptor, "<init>", "()V");
		jfieldID descriptorID = (*env)->GetFieldID(env, cFileDescriptor, "descriptor", "I");
		mFileDescriptor = (*env)->NewObject(env, cFileDescriptor, iFileDescriptor);
		(*env)->SetIntField(env, mFileDescriptor, descriptorID, (jint)fd);
	}

	return mFileDescriptor;
}

/*
 * Class:     com_biumall_serial_manager_SerialPort
 * Method:    close
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_biumall_serial_manager_SerialPort_close
(JNIEnv *env, jobject thiz)
{
	jclass SerialPortClass = (*env)->GetObjectClass(env, thiz);
	jclass FileDescriptorClass = (*env)->FindClass(env, "java/io/FileDescriptor");
	jfieldID mFdID = (*env)->GetFieldID(env, SerialPortClass, "mFd", "Ljava/io/FileDescriptor;");
	jfieldID descriptorID = (*env)->GetFieldID(env, FileDescriptorClass, "descriptor", "I");
	jobject mFd = (*env)->GetObjectField(env, thiz, mFdID);
	jint descriptor = (*env)->GetIntField(env, mFd, descriptorID);
	LOGD("close(fd = %d)", descriptor);
	close(descriptor);
}

第四步

修改Android.mk和Application.mk

Android.mk
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

TARGET_PLATFORM := android-27
LOCAL_MODULE    := SerialPort
LOCAL_SRC_FILES := SerialPort.c
LOCAL_LDLIBS    := -llog

include $(BUILD_SHARED_LIBRARY)

编译的so库名为SerialPort,也就是编译后有SerialPort.so文件。这个名字跟上面加载的要一致。

Application.mk
APP_PLATFORM := android-27
APP_ABI := all

我这里是编译所有架构的平台,因此生成的有如下对应的so库。

├─arm64-v8a
├─armeabi-v7a
├─riscv64
├─x86
└─x86_64

第五步

就是打开串口和收发数据了。

ISerialListener.java

串口状态监听

package com.biumall.serial.utils;

public interface ISerialListener {
    void onSerialStatus(boolean open);
}
IDataListener.java

数据接收回调

package com.biumall.serial.utils;

public interface IDataListener {
    void onData(final byte[] buffer, final int size);
}
SerialManager.java
package com.biumall.serial.manager;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.biumall.serial.utils.SerialUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;

public class SerialManager {
    private final String TAG = "SerialApp_java_v2_SerialPortUtils";
    private boolean mSuccess;
    private SerialPort mSerialPort = null;
    private InputStream mInputStream = null;
    private OutputStream mOutputStream = null;
    private String mCurrentPath = null;

    public void openSerialPort(Context context, String devPath, int baudRate) {
        Log.d(TAG, "openSerialPort devPath : " + devPath + " , baudRate : " + baudRate + " , mSuccess : " + mSuccess);
        if (mSuccess) {
            Log.d(TAG, "openSerialPort : The serial port [" + mCurrentPath + "] has been opened. Please close it and then open it again.");
            Toast.makeText(context, "已经打开过串口[" + mCurrentPath + "],请先关闭再打开", Toast.LENGTH_SHORT).show();
            return;
        }
        if (TextUtils.isEmpty(devPath)) {
            Log.d(TAG, "openSerialPort : There is an error in the serial port");
            Toast.makeText(context, "串口存在错误", Toast.LENGTH_SHORT).show();
            return;
        }
        try {
            mSerialPort = new SerialPort(new File(devPath), baudRate, 0);
            mInputStream = mSerialPort.getInputStream();
            mOutputStream = mSerialPort.getOutputStream();
            mSuccess = true;
            mCurrentPath = devPath;
            Log.d(TAG, "openSerialPort success ");
            Toast.makeText(context, "打开串口成功", Toast.LENGTH_SHORT).show();
            if (null != SerialUtils.getISerialListener()) {
                SerialUtils.getISerialListener().onSerialStatus(true);
            }
            new PortReadThread().start();
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, "openSerialPort e : " + e);
            Toast.makeText(context, "打开串口失败", Toast.LENGTH_SHORT).show();
        }
    }

    public void closeSerialPort(Context context) {
        Log.d(TAG, "closeSerialPort mSuccess : " + mSuccess);
        try {
            if (null != mInputStream) {
                mInputStream.close();
            }
            if (null != mOutputStream) {
                mOutputStream.close();
            }
            if (null != mSerialPort) {
                mSerialPort.close();
            }
            if (mSuccess) {
                Log.d(TAG, "openSerialPort : Close serial port successfully");
                mSuccess = false;
                Toast.makeText(context, "关闭串口成功", Toast.LENGTH_SHORT).show();
                if (null != SerialUtils.getISerialListener()) {
                    SerialUtils.getISerialListener().onSerialStatus(false);
                }
            } else {
                Log.d(TAG, "openSerialPort : The serial port is not currently open.");
            }
        } catch (Exception e) {
            Log.e(TAG, "closeSerialPort e : " + e);
            Toast.makeText(context, "关闭串口失败", Toast.LENGTH_SHORT).show();
        }
    }

    public void sendSerialPort(Context context, byte[] buffer) {
        Log.d(TAG, "sendSerialPort : " + Arrays.toString(buffer) + " ,  mSuccess : " + mSuccess);
        if (!mSuccess) {
            Toast.makeText(context, "请先打开串口", Toast.LENGTH_SHORT).show();
            Log.d(TAG, "sendSerialPort fail: ");
            return;
        }
        try {
            if (null != mOutputStream && buffer.length > 0) {
                Log.d(TAG, "sendSerialPort success : "+ Arrays.toString(buffer));
                mOutputStream.write(buffer);
                mOutputStream.flush();
            }
        } catch (IOException e) {
            Toast.makeText(context, "发送数据失败", Toast.LENGTH_SHORT).show();
            Log.d(TAG, "sendSerialPort fail : " + e);
        }
    }

    /**
     * 因为这里是阻塞的,所以单开一线程来读数据
     */
    private class PortReadThread extends Thread {
        @Override
        public void run() {
            super.run();
            while (mSuccess && !isInterrupted()) {
                //16 64 1024
                byte[] buffer = new byte[16];
                int size;
                try {
                    if (mInputStream == null) {
                        return;
                    }
                    size = mInputStream.read(buffer);
                    Log.d(TAG, "run size : " + size);
                    if (size > 0) {
                        if (null != SerialUtils.getIDataListener()) {
                            SerialUtils.getIDataListener().onData(buffer, size);
                        }
                    }
                } catch (IOException e) {
                    Log.d(TAG, "run read buffer fail , e : " + e);
                }
            }

        }
    }
}

第六步

MainActivity和布局,只是简单测试。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/darker_gray"
    android:gravity="center"
    android:orientation="vertical">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="100dp">

        <TextView
            android:id="@+id/main_tv_receiver"
            android:background="@android:color/white"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/serial_tips"
            android:padding="10dp"
            android:textColor="@android:color/holo_red_dark"
            android:textSize="22sp" />
    </ScrollView>

    <Button
        android:id="@+id/main_bt_open"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="20dp"
        android:text="@string/serial_open"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="22sp" />

    <Button
        android:id="@+id/main_bt_close"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="20dp"
        android:text="@string/serial_close"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="22sp" />

    <EditText
        android:id="@+id/main_et_send"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:numeric="integer"
        android:background="@android:color/white"
        android:padding="10dp"
        android:singleLine="true"
        android:text="@string/serial_data"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="22sp" />

    <Button
        android:id="@+id/main_bt_send"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="20dp"
        android:text="@string/serial_send"
        android:textColor="@android:color/holo_red_dark"
        android:textSize="22sp" />

</LinearLayout>
MainActivity.java
public class MainActivity extends Activity implements View.OnClickListener, IDataListener, ISerialListener {
    private final String TAG = "SerialApp_java_v2_MainActivity";
    private SerialManager mSerialManager;
    private EditText mEtSend;
    private TextView mTvReceiver;
    private Button mBtSend, mBtClose, mBtOpen;
	//串口号,不同平台路径不一样
    private String mDevPath = "/dev/ttyAT2";
	//波特率
    private int mBaudRate = 38400;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG, "onCreate: ");
        initUI();
        initData();
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume: ");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "onPause: ");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (null != mSerialManager) {
            mSerialManager.closeSerialPort(MainActivity.this);
        }
        SerialUtils.setIDataListener(null);
        SerialUtils.setISerialListener(null);
        Log.d(TAG, "onDestroy: ");
    }

    private void initData() {
        Log.d(TAG, "initData: ");
        mSerialManager = new SerialManager();
        SerialUtils.setIDataListener(this);
        SerialUtils.setISerialListener(this);
    }

    private void initUI() {
        Log.d(TAG, "initUI: ");
        mEtSend = findViewById(R.id.main_et_send);
        mTvReceiver = findViewById(R.id.main_tv_receiver);
        mBtOpen = findViewById(R.id.main_bt_open);
        mBtOpen.setOnClickListener(this);
        mBtOpen.setEnabled(true);
        mBtClose = findViewById(R.id.main_bt_close);
        mBtClose.setOnClickListener(this);
        mBtClose.setEnabled(false);
        mBtSend = findViewById(R.id.main_bt_send);
        mBtSend.setOnClickListener(this);
        mBtSend.setEnabled(false);
    }

    @SuppressLint("NonConstantResourceId")
    @Override
    public void onClick(View v) {
        Log.d(TAG, "onClick v : " + v);
        switch (v.getId()) {
            case R.id.main_bt_close:
                mSerialManager.closeSerialPort(MainActivity.this);
                break;
            case R.id.main_bt_open:
                mSerialManager.openSerialPort(MainActivity.this, mDevPath, mBaudRate);
                break;
            case R.id.main_bt_send:
                CharSequence charSequence = mEtSend.getText();
                Log.d(TAG, "onClick charSequence : " + charSequence);
                mSerialManager.sendSerialPort(MainActivity.this, charSequence.toString().getBytes());
                break;
        }
    }

    @Override
    public void onData(byte[] buffer, int size) {
        if (size > 0) {
            Log.d(TAG, "run byteArrayToHexString buffer : " + Arrays.toString(buffer));
            runOnUiThread(() -> {
                mTvReceiver.append("\n");
                mTvReceiver.append(Arrays.toString(buffer));
            });
        }
    }

    @Override
    public void onSerialStatus(boolean open) {
        runOnUiThread(() -> {
            Log.d(TAG, "onSerialStatus open : " + open);
            mBtSend.setEnabled(open);
            mBtOpen.setEnabled(!open);
            mBtClose.setEnabled(open);
        });
    }

}

参考文章

  1. android-serialport-api

  2. Android NDK配置简单记录

  3. JNI静态注册

 历史上的今天

  1. 2022: [代码片段]MediaCode 播放Video中的音频(0条评论)
  2. 2021: Android监听Home键和Back键的简介(0条评论)
  3. 2019: 余华:麦田里(0条评论)
版权声明 1、 本站名称: 91易搜
2、 本站网址: 91es.com
3、 本站内容: 部分来源于网络,仅供学习和参考,若侵权请留言
3、 本站申明: 个人流水账日记,内容并不保证有效

暂无评论

暂无评论...

随机推荐

Ubuntu18编译FFmpeg笔记

前言系统版本 :Ubuntu 18FFmpeg版本 : ffmpeg version N-102948-g79ebdbb9b9(下载最新的,这是编译后的版本信息)最近有看就整理了一下自己编译ffmpeg的编译过程,方便自己查阅。正文下面就记录编译的过程用的编译脚本以及所遇到的问题。ND...

朱自清:看花

生长在大江北岸一个城市里,那儿的园林本是著名的,但却很少,似乎自幼就不曾听见过“我们今天看花去”一类话,可见花事是不盛的。有些爱花的人,大都只是将花栽在盆里,一盆盆搁在架上,架子横放在院子里。院子照例是小小的,只够放下一个架子,架上至多搁二十多盆花罢了。有时院子里依墙筑起一座“花台”,台上种一株开花...

[摘]Android GC日志简单分析

前言测试反馈的日志中有大量的dalvikvm,如下:D/dalvikvm( 723): GC_CONCURRENT freed 658K, 38% free 1168K/1876K, paused 1ms+1ms, total 11msD/dalvikvm( 526): GC_FOR_A...

JNI之数组简单操作

前言简单记录一下JNI中数组操作。正文public class Hello {   static {       System.loadLibrary("Hello");   }   // JNI中对数组排序   public static native int[] so...

Beyond Compare4 30天试用到期的解决办法

网上有很多关于Beyond Compare4破解方法,国内还是没有习惯付费软件,也有偶尔跟着潮流。这个软件功能太强大了,有时候不得不佩服开发者,我只是偶尔用一下,下面是摘抄网络的内容,记录于此,方便自己查阅。隐藏内容!密码验证后才能查看!提交PS: 上面只是个人观点,也就偶尔使用此工具,如果是...

顾城:树枝的疏忽

我喜古诗,不因文学史,不因人们的仰望,而在它的美丽,文字清简明润,如玉如天,在于它显示出的中国哲思,那一无言就在眼前,若张九龄句:海上生明月,天涯共此时。诗如禅,如顿悟——骤然风动云散,黑暗退隐,你看见万物万象,明媚自如。“红豆生南国,春来发几枝”,气象柔和空阔;红豆生于南国,红豆生出南国,色空互...