Android中的串口通信

串口通讯

在计算机之间、计算机内部各部分之间,通信可以以串行和并行的方式进行。一个并行连接通过多个通道(例如导线、印制电路布线和光纤)在同一时间内传播多个数据流;而串行在同一时间内只连接传输一个数据流

虽然串行连接单个时钟周期能够传输的数据比并行数据更少,前者传输能力看起来比后者要弱一些,实际的情况却常常是,串行通信可以比并行通信更容易提高通信时钟频率,从而提高数据的传输速率。

串口通讯和并行通讯的区别

可以从上图看到,并行通讯可以一次传输8字节的数据,而串口一次只传输一个字节。但是通常串行通信都凭借其更低廉的部署成本成为更佳的选择,尤其是在远距离传输中。许多集成电路都具有串行通信接口来减少引脚数量,从而节约成本。

串口通讯的接口标准有很多,最常见的为RS-232、RS-485和USB等,下面我们看一下RS-232的接口标准。

串口通讯示意图

在RS-232标准中字符是按byte来一个接一个串列方式传输的,所以配线简单,发送的距离远,上图就是一个和电脑通讯的串口连接方式,通常情况我们将有管脚的端叫公头,将有孔的一端叫母头。

管脚的定义如下:

1
2
3
4
5
DE-9 Male(Pin Side)                   DE-9 Female (Pin Side)
------------- -------------
\ 1 2 3 4 5 / \ 5 4 3 2 1 /
\ 6 7 8 9 / \ 9 8 7 6 /
--------- ---------
脚位简写意义说明
Pin1DCDData Carrier Detect调制解调器通知计算机有载波被侦测到。
Pin2RXDReceiver接收数据。
Pin3TXDTransmit发送数据。
Pin4DTRData Terminal Ready计算机告诉调制解调器可以进行传输。
Pin5GNDGround地线。
Pin6DSRData Set Ready调制解调器告诉计算机一切准备就绪。
Pin7RTSRequest To Send计算机要求调制解调器将数据提交。
Pin8CTSClear To Send调制解调器通知计算机可以传数据过来。
Pin9RIRing Indicator调制解调器通知计算机有电话进来。

串口通讯分为异步串行通信和同步串行通信。

异步串行通信

异步串行通信不需要发送方和接收方的时钟频率一致,这样就需要每个字符附件2~3位的起止、校验位和停止位,各个帧之间还有间隔,所以传输效率不高。

同步串行通信

同步串行通信需要发送方和接收方的时钟频率一致,有开始和结束标志,这种方式传输效率高。

Google官方源码

我们现在市面上的所有Android串口通信的源代码都是Google公司在2011年开源的Google官方源代码。

官方串口通讯源码地址:https://code.google.com/archive/p/android-serialport-api/

官方的源码是Eclipse环境的,幸运的是GitHub上已经有人帮我们做了Android Studio上面的支持,源码地址:https://github.com/lxqxsyu/Android-SerialPort-API

如何使用

第一步:开启串口。
第二步:打开输入输出流。
第三步:开启读线程。

1
2
3
4
5
6
7
8
9
10
11
try {
mSerialPort = new SerialPort(devicePath, baudrate, 0);
mInputStream = new BufferedInputStream(mSerialPort.getInputStream());
mOutputStream = new BufferedOutputStream(mSerialPort.getOutputStream());
mReadThread = new SerialReadThread();
mReadThread.start();
return mSerialPort;
} catch (Throwable tr) {
closeSerial();
return null;
}

向串口写入数据:

1
2
3
4
5
6
try {
mOutputStream.write(bytes, off, len);
mOutputStream.flush();
} catch (Exception e) {
e.printStackTrace();
}

从串口读取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
while(true){
int available = mInputStream.available();

if (available > 0) {
len = mInputStream.read(mRecvBuffer);
if (len > 0) {
getReceiver().onReceive(mRecvBuffer, 0, len);
}
} else {
// 暂停一点时间,免得一直循环造成CPU占用率过高
SystemClock.sleep(1);
}
}

过程分析

检查devicePath是否具有可读可写的权限,如果没有则通过chmod 666来更改权限, 接下来调用native方法open().

在这个open()方法中首先做了一个波特率的转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;
}
}

随后调用了linux的open()函数

1
int open(const char *pathname, int flags);

pathname是文件路径。
flags参数表示打开文件所采用的操作,我们需要注意的是:必须指定以下三个常量的一种,且只允许指定一个:

  • O_RDONLY:只读模式
  • O_WRONLY:只写模式
  • O_RDWR:可读可写

以下的常量是选用的,这些选项是用来和上面的必选项进行按位或起来作为flags参数。

  • O_APPEND 表示追加,如果原来文件里面有内容,则这次写入会写在文件的最末尾。
  • O_CREAT 表示如果指定文件不存在,则创建这个文件
  • O_EXCL 表示如果要创建的文件已存在,则出错,同时返回 -1,并且修改 errno 的值。
  • O_TRUNC 表示截断,如果文件存在,并且以只写、读写方式打开,则将其长度截断为0。
  • O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
  • O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode)
1
fd = open(path_utf, O_RDWR | flags);

然后使用通过cfgetispeed函数和cfgetospeed函数来设置输入和输出的波特率。

1
2
cfsetispeed(&cfg, speed);
cfsetospeed(&cfg, speed);

这个cfg是一个termios类型的结构体:

1
struct termios cfg;

termios结构体中,该结构体一般包括如下的成员:

成员说明
c_iflag输入模式标志,控制终端输入方式
c_oflag输出模式标志,控制终端输出方式
c_cflag控制模式标志,指定终端硬件控制信息
c_lflag本地模式标志,控制终端编辑功能

那个这个cfg的数据是从哪来的呢?其实是通过tcgetattr(fd, &cfg)函数获得的,这个函数可以获得与终端相关的参数,参数保存在cfg中。

1
2
3
4
5
6
7
8
9
10
11
12
13
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);

tcgetattr()可以得到波特率、字符大小、数据位、停止位、奇偶校验位和硬件流控制等。

最后创建了一个java.io.FileDescriptor对象并返回。

1
2
3
4
5
6
7
8
/* 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);
}

事实上我们的读写流也是通过FileDescriptor对象来获得的。

1
2
3
4
5
6
7
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);

哈哈,这个时候你是不是觉得这个FileDescriptor很神奇,它到底是什么。

FileDescriptor

FileDescriptor 是在UNIX系统里对文件描述的一个提法。在Window系统里,称为file handle。是指代文件的一种抽象表示法。

操作系统使用文件描述符来指代一个打开的文件,对文件的读写操作,都需要文件描述符作为参数。Java虽然在设计上使用了抽象程度更高的流来作为文件操作的模型,但是底层依然要使用文件描述符与操作系统交互,而Java世界里文件描述符的对应类就是FileDescriptor。

Java文件操作的三个类:FileIntputStream,FileOutputStream,RandomAccessFile,打开这些类的源码可以看到都有一个FileDescriptor成员变量。

操作系统中的文件描述符本质上是一个非负整数,其中0,1,2固定为标准输入,标准输出,标准错误输出,程序接下来打开的文件使用当前进程中最小的可用的文件描述符号码,比如3。

文件描述符本身就是一个整数,所以FileDescriptor的核心职责就是保存这个数字:

1
2
3
public final class FileDescriptor {
private int fd;
}

标准输入,标准输出,标准错误输出是所有操作系统都支持的,对于一个进程来说,文件描述符0,1,2固定是标准输入,标准输出,标准错误输出。

Java对标准输入,标准输出,标准错误输出的支持也是通过FileDescriptor实现的,FileDescriptor中定义了in,out,err这三个静态变量:

1
2
3
public static final FileDescriptor in = new FileDescriptor(0);
public static final FileDescriptor out = new FileDescriptor(1);
public static final FileDescriptor err = new FileDescriptor(2);

评论

Ajax Android AndroidStudio Animation Anroid Studio AppBarLayout Banner Buffer Bulma ByteBuffer C++ C11 C89 C99 CDN CMYK COM1 COM2 CSS Camera Raw, 直方图 Chrome ContentProvider CoordinatorLayout C语言 DML DOM Dagger Dagger2 Darktable Demo Document DownloadManage Element Error Exception Extensions File FileProvider Fresco GCC Git GitHub GitLab Gradle Groovy HTML5 Handler HandlerThread Hexo Hybrid I/O IDEA IO ImageMagick IntelliJ Intellij Interpolator JCenter JNI JS Java JavaScript JsBridge Kotlin Lab Lambda Lifecycle Lint Linux Looper MQTT MVC MVP Maven MessageQueue Modbus Momentum MySQL NDK NIO NexT Next Nodejs ObjectAnimator Oracle VM Permission PhotoShop Physics Python RGB RS-232 RTU Remote-SSH Retrofit Runnable RxAndroid RxJava SE0 SSH Spring SpringBoot Statubar Task Theme Thread Tkinter UI UIKit UML VM virtualBox VS Code ValueAnimator ViewPropertyAnimator Web Web前端 Workbench api apk bookmark by关键字 compileOnly css c语言 databases demo hexo hotfix html iOS icarus implementation init jQuery javascript launchModel logo merge mvp offset photos pug query rxjava2 scss servlet shell svg tkinter tomcat transition unicode utf-8 vector virtual box vscode 七牛 下载 中介者模式 串口 临潼石榴 主题 书签 事件 享元模式 仓库 代理模式 位运算 依赖注入 修改,tables 光和色 内存 内核 内部分享 函数 函数式编程 分支 分析 创建 删除 动画 单例模式 压缩图片 发布 可空性 合并 同向性 后期 启动模式 命令 命令模式 响应式 响应式编程 图层 图床 图片压缩 图片处理 图片轮播 地球 域名 基础 增加 备忘录模式 外观模式 多线程 大爆炸 天气APP 太白山 头文件 奇点 字符串 字符集 存储引擎 宇宙 宏定义 实践 属性 属性动画 岐山擀面皮 岐山肉臊子 岐山香醋 工具 工厂模式 年终总结 开发技巧 异常 弱引用 恒星 打包 技巧 指针 插件 摄影 操作系统 攻略 故事 数据库 数据类型 数组 文件 新功能 旅行 旋转木马 时序图 时空 时间简史 曲线 杂谈 权限 枚举 架构 查询 标准库 标签选择器 样式 核心 框架 案例 桥接模式 检测工具 模块化 模板引擎 模板方法模式 油泼辣子 泛型 洛川苹果 浅色状态栏 源码 瀑布流 热修复 版本 版本控制 状态栏 状态模式 生活 留言板 相册 相对论 眉县猕猴桃 知识点 码云 磁盘 科学 笔记 策略模式 类图 系统,发行版, GNU 索引 组件 组合模式 结构 结构体 编码 网易云信 网格布局 网站广播 网站通知 网络 美化 联合 膨胀的宇宙 自定义 自定义View 自定义插件 蒙版 虚拟 虚拟机 补码 补齐 表单 表达式 装饰模式 西安 观察者模式 规范 视图 视频 解耦器模式 设计 设计原则 设计模式 访问者模式 语法 责任链模式 贪吃蛇 转换 软件工程 软引用 运算符 迭代子模式 适配器模式 选择器 通信 通道 配置 链表 锐化 错误 键盘 闭包 降噪 陕西地方特产 面向对象 项目优化 项目构建 黑洞
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×