前言
简单记录一下Android串口的使用。记录于此,方便自己查阅。
在Android系统上无法直接使用设备的串口,因为Android平台没有提供操作串口的API。不过可以使用库文件来操作串口,本文使用github上开源的《》进行打开串口,亲测有效。
小结
下面记录一下遇到的问题
串口打不开或无法通信
-
没有SELInux权限
如果临时测试,可以使用adb shell 0,否则需要配置权限(这个后面会单独记录一下怎设置)
-
串口不存在或串口没有权限
没有权限就临时改变一下试试,进入dev目录 chmod 777 ttyAT2
-
无法通信,问一下波特率。
如何判断串口通信成功
其实就是短路一下RX和TX,然后使用查看是否能收到自己发送的数据。一般是找硬件接线短路,短路后使用如下方法。
-
cmd 使用命令echo 和cat 对应串口
-
使用UartAssist和下面的打开窗口工具进行收发数据
如果收发正常,表示窗口是通的。
正文
这里需要使用NDK,如果没有配置过,可以看一下《》。
第一步
创建一个工程
工程名 :BiuSerial 工程包名 : com.biumall.serial
上面看自己定义,如果包名不一样,下面需要改。
第二步
如果对JNI不熟悉的,可以看《》
先下载《》代码,然后把
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
根据《》可以通过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); }); } }
参考文章
-
《》
-
《》
-
《