Android Studio的串口通讯开发
前言
软件代码写久了,总会对嵌入式开发感兴趣,因为软件的东西写来写去看不见摸不着,而嵌入式硬件开发,可以捣鼓一些机械设备玩,电子感应灯,遥控车啥的,这也就是传说中的创客啊!后面索性买了一套单片机教程《手把手教你学51单片机》和板子学习,作为入门书来说,确实挺不错,配合上官方的板子,把单片机的基础原理都将的挺透彻,其中还包括了C语言的讲解,为嵌入式开发打下基础。
共花了2个月把单片机学了一遍,最后觉得也没有想象中的难,因为其实到头来还是像在使用工具,只是知识体系会有些不一样,主要就是基于C/汇编语言通过单片机对寄存器、定时器、每个引脚的高低电平的使用,然后参考硬件手册,对每个硬件进行操作,以达到想要实现的效果。而如果想要实现自己的最小系统,或者弄出一些好玩的东西,还要自己根据物力电气原理选原件、画PCB板子,这就是硬件工程师要干的事情了。
刚好公司项目里基于Android板子开发了一些串口通讯的应用,得益于之前单片机的学习,没有遇到太多困难,记录分享一下。
RS232标准接口
也就是PC电脑上所说的COM口,RS232是负逻辑电平,它定义+5~+12V为低电平,而-12~-5V为高电平。
正常情况下,PC台式主机机箱都会有一个RS232的通讯接口(别和VGI的口搞错啦!),而目前笔记本几乎不会带有了,所以都是用USB转接口。
UART
及Universal Asynchronous Receiver Transmitter:通用异步收发器,通常ARM嵌入式板子都会集成此接口
UART有4个pin(VCC, GND, RX, TX), 用的TTL电平, 低电平为0(0V),高电平为1(3.3V或以上)。
RS232与UART转接
通常嵌入式里所说的串口,是指UART口,但硬件众多,大多数都是基于RS232和UART,有时候这两种口之间需要通讯,最主要不同的其实也就是电平不一样,所以需要转接口,某宝上MAX3232种类繁多。

下载 NDK 和构建工具
由于是使用JNI直接进行串口设备的读写,所以需要下载工具。android studio版本务必要2.2以上
运行SDK Manager,下载3个工具,CMake、LLDB、NDK
PS:如果下载进度很慢,请使用国内镜像

创建支持 C/C++ 的新项目
在 Customize C++ Support 选项卡中。你有下面几种方式来自定义你的项目:
- C++ Standard:点击下拉框,可以选择标准 C++,或者选择默认 CMake 设置的 Toolchain Default 选项。
- Exceptions Support:如果你想使用有关 C++ 异常处理的支持,就勾选它。勾选之后,Android Studio 会在 module 层的 build.gradle 文件中的 cppFlags 中添加 -fexcetions 标志。
- Runtime Type Information Support:如果你想支持 RTTI,那么就勾选它。勾选之后,Android Studio 会在 module 层的 build.gradle 文件中的 cppFlags 中添加 -frtti 标志。
新建成功之后,相对于以前的项目目录,多了几个地方。
- cpp 目录存放你所有 native code 的地方,包括源码,头文件,预编译项目等。对于新项目,Android Studio 创建了一个 C++ 模板文件:native-lib.cpp,并且将该文件放到了你的 app 模块的 src/main/cpp/ 目录下。这份模板代码提供了一个简答的 C++ 函数:stringFromJNI(),该函数返回一个字符串:”Hello from C++”。
- External Build Files 目录是存放 CMake 或 ndk-build 构建脚本的地方。有点类似于 build.gradle 文件告诉 Gradle 如何编译你的 APP 一样,CMake 和 ndk-build 也需要一个脚本来告知如何编译你的 native library。对于一个新的项目,Android Studio 创建了一个 CMake 脚本:CMakeLists.txt,并且将其放到了你的 module 的根目录下。
- gradle的脚本下也加入了externalNativeBuild字段来显示指定CMake。
PS:如果现有项目需要添加C/C++ 代码,则参考项目模板添加相应文件即可。
编译C/C++代码
CMakeLists.txt
# Sets the minimum version of CMake required to build the native# library. You should either keep the default value or only pass a# value of 3.4.0 or lower.cmake_minimum_required(VERSION 3.4.1)# Creates and names a library, sets it as either STATIC# or SHARED, and provides the relative paths to its source code.# You can define multiple libraries, and CMake builds it for you.# Gradle automatically packages shared libraries with your APK.add_library( # Sets the name of the library.serial_port# Sets the library as a shared library.SHARED# Provides a relative path to your source file(s).# Associated headers in the same location as their source# file are automatically included.src/main/cpp/SerialPort.c )# Searches for a specified prebuilt library and stores the path as a# variable. Because system libraries are included in the search path by# default, you only need to specify the name of the public NDK library# you want to add. CMake verifies that the library exists before# completing its build.find_library( # Sets the name of the path variable.log-lib# Specifies the name of the NDK library that# you want CMake to locate.log )# Specifies libraries CMake should link to your target library. You# can link multiple libraries, such as libraries you define in the# build script, prebuilt third-party libraries, or system libraries.target_link_libraries( # Specifies the target library.serial_port# Links the target library to the log library# included in the NDK.${log-lib} )
SerialPort.h
/* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h>/* Header for class android_serialport_api_SerialPort */#ifndef _Included_android_serialport_api_SerialPort#define _Included_android_serialport_api_SerialPort#ifdef __cplusplusextern "C" {#endif/** Class: android_serialport_api_SerialPort* Method: open* Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;*/JNIEXPORT jobject JNICALL Java_android_1serialport_1api_SerialPort_open(JNIEnv *, jclass, jstring, jint, jint);/** Class: android_serialport_api_SerialPort* Method: close* Signature: ()V*/JNIEXPORT void JNICALL Java_android_1serialport_1api_SerialPort_close(JNIEnv *, jobject);#ifdef __cplusplus}#endif#endif
SerialPort.c
/** Copyright 2009-2011 Cedric Priscal** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/#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="serial_port";#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: android_serialport_SerialPort* Method: open* Signature: (Ljava/lang/String;II)Ljava/io/FileDescriptor;*/JNIEXPORT jobject JNICALL Java_android_1serialport_1api_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) {/* TODO: throw an exception */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){/* Throw an exception */LOGE("Cannot open port");/* TODO: throw an exception */return NULL;}}/* Configure device */{struct termios cfg;LOGD("Configuring serial port");if (tcgetattr(fd, &cfg)){LOGE("tcgetattr() failed");close(fd);/* TODO: throw an exception */return NULL;}cfmakeraw(&cfg);cfsetispeed(&cfg, speed);cfsetospeed(&cfg, speed);//此处设置校验位//cfg.c_cflag……if (tcsetattr(fd, TCSANOW, &cfg)){LOGE("tcsetattr() failed");close(fd);/* TODO: throw an exception */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: cedric_serial_SerialPort* Method: close* Signature: ()V*/JNIEXPORT void JNICALL Java_android_1serialport_1api_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);}
最后java在需要的地方调用
// JNIprivate native static FileDescriptor open(String path, int baudrate, int flags);public native void close();static {Log.i(TAG, "loadLibrary..............");System.loadLibrary("serial_port");}
串口通讯原理
使用到了linux系统函数,是相对底层的开发,所以一大堆标志位,位运算,看着有些晕,函数的具体使用方法需要查询手册
1、打开串口
fd = open(path_utf, O_RDWR | flags);
其中
O_RDWR 读写方式打开
O_NOCTTY 不允许进程管理串口
O_NDELAY 非阻塞
2、写串口
n = write(fd, "ATZ ", 4);
n实际写入个数
3、设置串口为非阻塞方式
fcntl(fd, F_SETFL, FNDELAY);
4、设置串口为阻塞方式:
fcntl(fd, F_SETFL, 0);
5、读串口:
res = read(fd,buf,len);
6、关闭串口
Close(fd);
注:示例代码中通过JAVA的FileDescriptor类包装了数据流,所以读写操作是在上层完成的。
/* 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;
关于校验位
示例代码默认是没有添加任何校验位的,一般情况设备的基础通讯协议都是
波特率:9600
校验位:无
数据位:8
停止位:1位
至于具体的指令协议,根据设备提供商文档自行写控制代码即可。
校验位设置代码如下:
No parity (8N1):
cfg.c_cflag &= ~PARENBcfg.c_cflag &= ~CSTOPBcfg.c_cflag &= ~CSIZE;cfg.c_cflag |= CS8;
Even parity (7E1):
cfg.c_cflag |= PARENBcfg.c_cflag &= ~PARODDcfg.c_cflag &= ~CSTOPBcfg.c_cflag &= ~CSIZE;cfg.c_cflag |= CS7;
Odd parity (7O1):
cfg.c_cflag |= PARENBcfg.c_cflag |= PARODDcfg.c_cflag &= ~CSTOPBcfg.c_cflag &= ~CSIZE;cfg.c_cflag |= CS7;
Space parity is setup the same as no parity (7S1):
cfg.c_cflag &= ~PARENBcfg.c_cflag &= ~CSTOPBcfg.c_cflag &= ~CSIZE;cfg.c_cflag |= CS8;//cfg.c_cflag |= PARENB | CS8 | CMSPAR;
Mark parity is simulated by using 2 stop bits (7M1):
cfg.c_cflag &= ~PARENB;cfg.c_cflag |= CSTOPB;cfg.c_cflag &= ~CSIZE;cfg.c_cflag |= CS7;//cfg.c_cflag |= PARENB | CS8 | CMSPAR |PARODD;
1.even 每个字节传送整个过程中bit为1的个数是偶数个(校验位调整个数)
2.odd 每个字节穿送整个过程中bit为1的个数是奇数个(校验位调整个数)
3.noparity没有校验位
4.space 校验位总为0
5.mark 校验位总为1;
HexString与Bytes的转换
默认情况下串口读出来的数据是byte[],有时候需要转换16进制的字符串进行指令识别
/*** Created by lixin(178078114@qq.com) on 2016/12/17.*/public class HexUtils {/*** 把字节数组转换成16进制字符串** @param bArray* @return*/public static String bytesToHexString(byte[] src) {StringBuilder stringBuilder = new StringBuilder("");if (src == null || src.length <= 0) {return null;}for (int i = 0; i < src.length; i++) {int v = src[i] & 0xFF;String hv = Integer.toHexString(v);if (hv.length() < 2) {stringBuilder.append(0);}stringBuilder.append(hv);}return stringBuilder.toString().toUpperCase();}/*** 把16进制字符串转换成字节数组** @param hex* @return*/public static byte[] hexStringToByte(String hex) {int len = (hex.length() / 2);byte[] result = new byte[len];char[] achar = hex.toCharArray();for (int i = 0; i < len; i++) {int pos = i * 2;result[i] = (byte) (toByte(achar[pos]) << 4 | toByte(achar[pos + 1]));}return result;}private static byte toByte(char c) {byte b = (byte) "0123456789ABCDEF".indexOf(c);return b;}}
最终的demo如图,选择对了参数和串口设备,即可调试了!

参考
http://gqdy365.iteye.com/blog/2188906
http://wl9739.github.io/2016/09/21/%E5%9C%A8-Android-Studio-2-2-%E4%B8%AD%E6%84%89%E5%BF%AB%E5%9C%B0%E4%BD%BF%E7%94%A8-C-C-md/
demo示例:
链接:http://pan.baidu.com/s/1pLc1JVt 密码:h1zt
浙公网安备 33010602011771号