写在前面:本文所用的硬件平台是天嵌的E9开发板,烧的是安卓6.0.1系统,E9平台的如何烧录镜像等操作这里不作讲解,单纯当做一个有串口接口的Android设备来使用。当然一般的Android手机的硬件设备都可以用来操作,自己的手机具体有哪些硬件设备可以操作可以在系统的/dev目录下查看,串口、显示屏和触摸屏等都在目录中可以找到(截图只有一部分),这里默认读者已经熟悉Android系统和应用开发、了解C语言基本文件操作。
然后一直下一步直到Finish:
工程新建好后在目录结构中比一般的Android工程多几个文件,其中如下是最为重要的:
如上新建的工程可以直接编译运行到手机里面,只在中间显示了一串符串:
这一串字符来自native-lib.cpp文件:
这个文件对函数名做一个简单介绍:
extern "C"
JNIEXPORT jstring JNICALL
Java_com_test_ndk_serialdemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
对C、C++比较熟悉的应该知道第一行的作用。
第二行说明这个函数的返回值是string。
把函数名字分割成如下4个部分:
1: 每个函数最前面固定写一个Java
2: 将包名中的“.”换成“_”
3: 要调用c++文件的类名,c++文件会被编译成.so库,java通过加载库来调用c++函数,这里这个cpp文件会被编译成libnative–lib.so,这个名字也是由3部分组成,lib+native-lib+so,中间那个才是库名。
4: 最后一部分就是函数名字。
这个函数的作用就是返回字符串"Hello from C++"
9~11行:加载有cpp文件编译成的native-lib库。
25行:用关键字native修饰,说明库里面有一个名为stringFromJNI的函数。
函数的调用就在19行,把返回值显示在TextView上面。
这个文件有英文注释,描述比较清楚也就不再翻译,主要是表述了cpp文件名、编译过后生成的库名和最后需要链接的库名字。
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <jni.h>
#include <android/log.h>
static const char *TAG = "seril";
#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 (speed_t) -1;
}
}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_test_ndk_ndkdemo_SerialPort_open(JNIEnv *env, jclass type, jstring path_, jint baudrate, jint flags) {
int fd;
speed_t speed;
jobject mFileDescriptor;
const char *path = env->GetStringUTFChars(path_, 0);
// Check arguments
{
speed = getBaudrate(baudrate);
if (speed == -1) {
LOGD("Invalid baudrate");
return NULL;
}
}
// Opening device
{
LOGD("Opening serial port %s with flags 0x%x", path, O_RDWR | flags);
fd = open(path, O_RDWR | flags);
LOGD("open() fd = %d", fd);
env->ReleaseStringUTFChars(path_, path);
if (fd == -1) {
// Throw an exception
LOGD("Cannot open port");
return NULL;
}
}
// Configure device
{
struct termios cfg;
LOGD("Configuring serial port");
if (tcgetattr(fd, &cfg)) {
LOGD("tcgetattr() failed");
close(fd);
return NULL;
}
cfmakeraw(&cfg);
cfsetispeed(&cfg, speed);
cfsetospeed(&cfg, speed);
if (tcsetattr(fd, TCSANOW, &cfg)) {
LOGD("tcsetattr() failed");
close(fd);
return NULL;
}
//获得串口指向配置结构的指针
cfmakeraw(&cfg);
// 设置串口数据位-----------------------------------------------
//屏蔽其他标志
cfg.c_cflag&=~CSIZE;
//将数据位修改为8bit
cfg.c_cflag |=CS8;
//将修改后的termios数据设置到串口中
if (tcsetattr(fd, TCSANOW, &cfg)) {
close(fd);
return env->NewStringUTF("uart data num set error");
}
//获得串口指向配置结构的指针
cfmakeraw(&cfg);
// 设置串口校验位-----------------------------------------------
cfg.c_cflag &= ~PARENB;
//将修改后的termios数据设置到串口中
if (tcsetattr(fd, TCSANOW, &cfg)) {
close(fd);
return env->NewStringUTF("uart cheack set error");
}
//获得串口指向配置结构的指针
cfmakeraw(&cfg);
// 设置串口流控-----------------------------------------------
cfg.c_cflag &= ~CRTSCTS;
//将修改后的termios数据设置到串口中
if (tcsetattr(fd, TCSANOW, &cfg)) {
close(fd);
return env->NewStringUTF("Data bit setting failed");
}
}
// Create a corresponding file descriptor
{
jclass cFileDescriptor = env->FindClass("java/io/FileDescriptor");
jmethodID iFileDescriptor = env->GetMethodID(cFileDescriptor, "" , "()V");
jfieldID descriptorID = env->GetFieldID(cFileDescriptor, "descriptor", "I");
mFileDescriptor = env->NewObject(cFileDescriptor, iFileDescriptor);
env->SetIntField(mFileDescriptor, descriptorID, (jint) fd);
}
return mFileDescriptor;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_test_ndk_ndkdemo_SerialPort_close(JNIEnv *env, jobject instance) {
jclass SerialPortClass = env->GetObjectClass(instance);
jclass FileDescriptorClass = env->FindClass("java/io/FileDescriptor");
jfieldID mFdID = env->GetFieldID(SerialPortClass, "mFd", "Ljava/io/FileDescriptor;");
jfieldID descriptorID = env->GetFieldID(FileDescriptorClass, "descriptor", "I");
jobject mFd = env->GetObjectField(instance, mFdID);
jint descriptor = env->GetIntField(mFd, descriptorID);
LOGD("close(fd = %d)", descriptor);
close(descriptor);
}
上面代码中关于串口的配置参数如下:
cfmakeraw(&cfg);
// 设置串口数据位-----------------------------------------------
//屏蔽其他标志
cfg.c_cflag&=~CSIZE;
//将数据位修改为8bit
cfg.c_cflag |=CS8;
//将修改后的termios数据设置到串口
if (tcsetattr(fd, TCSANOW, &cfg)) {
close(fd);
//提示设置错误
return env->NewStringUTF("Data bit setting failed");
}
设 置 | 代 码 |
---|---|
无校验 | cfg.c_cflag &= ~PARENB; |
奇校验 | cfg.c_cflag |= (PARODD | PARENB); |
偶校验 | cfg.c_cflag &= ~ PARENB; cfg.c_cflag &= ~PARODD; |
空格 | cfg.c_cflag &= ~PARENB; cfg.c_cflag &= ~CSTOPB; |
设 置 | 代 码 |
---|---|
1位 | cfg.c_cflag &= ~CSTOPB; |
2位 | cfg.c_cflag |= CSTOPB; |
设 置 | 代 码 |
---|---|
无流控 | cfg.c_cflag &= ~CRTSCTS |
奇校验 | cfg.c_cflag |= CRTSCTS |
偶校验 | cfg.c_cflag |= IXON | IXOFF | IXANY |
package com.test.ndk.ndkdemo;
import android.util.Log;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class SerialPort {
static {
System.loadLibrary("uart");
}
private static final String TAG = "SerialPort";
private FileInputStream mFileInputStream;
private FileOutputStream mFileOutputStream;
private FileDescriptor mFd;
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"); // 切换root用户
String cmd = "chmod 777 " + 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();
}
}
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); // 获取串口输出流
}
// Getters and setters
public FileInputStream getInputStream() {
return mFileInputStream;
}
public FileOutputStream getOutputStream() {
return mFileOutputStream;
}
// cpp文件中的两个方法
private native static FileDescriptor open(String path, int baudrate, int flags);
public native void close();
}
顺便说一句,linux(Android也是linux)里面所有设备都是以文件形式呈现,操作文件即操作硬件接口。
在MainActivity创建SerialPort对象,并获取SerialPort中的输入输出stream即可实现串口读写。界面中有一个Button,一个TextView。
MainActivity代码如下
package com.test.ndk.ndkdemo;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
private SerialPort mSerialPort;
private FileInputStream mInputStream;
private FileOutputStream mOutputStream;
private Button send;
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.sample_text);
send = findViewById(R.id.send);
// 点击按钮发送2个字节数据
send.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
byte[] data = new byte[2];
data[0] = 5;
data[1] = 6;
try {
mOutputStream.write(data);
mOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
});
// 打开串口设备并获取输入输出流
try {
// 不同的设备串口的设备名字不一样,比如A8开发板的串口文件名字是s3c2410_serial2...,E9开发板提供了四个串口接口,串口1到串口4,串口1的文件名为ttySAC0,用于debug不开放.
// ttySAC1到ttySAC3对应串口2~4,每个串口的硬件位置查看硬件原理图就可以知道,位置都在“/dev”下。
mSerialPort = new SerialPort(new File("/dev/ttySAC3"), 115200, 0); // 打开连接协调器串口,波特率115200,无奇偶校验位
mInputStream = mSerialPort.getInputStream(); // 获取串口输入流,用于读取串口数据
mOutputStream = mSerialPort.getOutputStream(); // 获取串口输出流,用于通过串口发送数据
} catch (IOException e) {
System.out.println("无法打开串口,ttySAC3");
e.printStackTrace();
}
new Thread(new ReadThread()).start();
}
/**
* 数据读取线程
*/
private class ReadThread extends Thread {
private int size;
private byte[] buffer = new byte[512];
@Override
public void run() {
while (true) {
if (mInputStream == null) {
try {
// A8串口:s3c2410_serial2...要用什么对应原理图
// E9串口: ttySAC0(调试接口),ttySAC1、ttySAC2、ttySAC3具体位置看原理图
mSerialPort = new SerialPort(new File("/dev/ttySAC3"), 115200, 0); // 打开连接协调器串口,波特率115200,无奇偶校验位
mInputStream = mSerialPort.getInputStream(); // 获取串口输入流,用于读取串口数据
mOutputStream = mSerialPort.getOutputStream(); // 获取串口输出流,用于通过串口发送数据
} catch (Exception e) {
System.out.println("无法打开串口");
SystemClock.sleep(3000);
}
}
else
{
try { // buffer数组就是读取到的内容,size就是读取到的数量
size = mInputStream.read(buffer, 0, buffer.length); // 从协调器串口读取数据
if (size > 0) {
Log.d("seril",size+"");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
}
E9开发版拓展接口图。如果读者用的A8或者2440等设备需要查看自己板子的原理图,并在“/dev”路径下找到相应的文件
最后将demo上传提供参考。