Android 智能家居开发:串口是什么,为什么android版本都比较低?粘包半包的原因以及处理思路,缓冲区处理,以及超时清空缓冲区....

目录

  1. android设备
  2. 串口是什么
  3. 为什么Android设备的系统一般版本比较低?
  4. 停止位
  5. 粘包、半包如何处理
    1. 默认不处理
    2. 包头包尾标识
    3. 长度字段
    4. 固定长度
    5. 代码实战: 包头包尾标识+缓冲区管理+超时处理
  6. 流控是什么
  7. 校验和

前言

串口是什么?只知道拿来进行使用,只能使用别人封装好的,导致如果下位机更换了,就不知道如何去对接了,出现拆包,粘包,就不知道如何去使用了。所以这篇文章,就来认识一下串口究竟是什么。


一、android 设备

我们会发现,一些智能家居,门禁,人脸考勤,自动售卖机等等,都会使用android设备来实现控制硬件来动作。

这里我们所说的android设备,和我们所使用的手机,虽然都是android系统,但android设备的系统是经过定制化才得出来的。比如厂商主动开放Root权限,我们才能进行设备驱动,才能进行串口开发,进行数据的传输,从而实现软件控制硬件的联动,并且这些android设备,往往会提供更多的接口去和硬件通讯,比如串口,a和b,tx和rx。


二、串口是什么?

在Linux/Android系统中,​​一切皆文件​​!我们经常使用串口,就会看到有输入流,输出入流,其实,无论是硬盘里的文档、摄像头、还是串口,都被看作"文件"。

  • ​打开文件​open("/dev/ttyS0") → 就像双击打开一个文档。
  • ​读取数据​read() → 像从文档里读文字。
  • ​写入数据​write() → 像往文档里打字。
  • ​关闭文件​close() → 关闭文档。

Android基于Linux内核,所以串口也是/dev/tty*文件。但普通APP无法直接访问,需要以下方法:

  1. ​Root权限​​:

    • 直接操作/dev/ttyS0,但会破坏系统安全。

      adb shell "su -c 'chmod 666 /dev/ttyS0'" # 强制修改文件权限

  2. ​使用android设备,厂商开放接口​​:

    • 通过Android的​硬件抽象层(HAL)​​JNI​调用底层驱动。

    // Java通过JNI调用C代码操作串口
    public native int openSerialPort(String path, int baudRate);


三、一般android设备的系统,不会使用太高的版本

一般android 设备,会使用android4,android5,或者android7。为什么呢?

一方面是因为智能家居设备通常采用 ​​低成本硬件方案​​(如全志A33、瑞芯微RK3308等低端芯片),其配置远低于主流手机。并且旧版本AOSP(Android开源项目)代码量更小。例如:

  • Android 4.4代码库约40GB,编译后系统镜像≤800MB
  • Android 11代码库超100GB,系统镜像≥2GB

并且,做android智能家居,追求的是稳定性 ,智能家居设备需 ​​7×24小时运行​​,旧版本Android经过多年工业场景验证:

  • ​Android 4.4​:2013年发布,已修复大量稳定性问题(如内存泄漏、死锁)。

所以,智能家居设备选择旧版Android的原因是:​​在有限的硬件资源下,通过成熟稳定的系统版本实现功能、成本与维护效率的最优平衡​​。


四、停止位

​1. 停止位是什么?​

停止位是数据包末尾的​​标志信号​​,表示当前数据包传输结束。

  • ​简单理解​ :停止位就像说话时的​句号​,告诉对方"我说完了,该你回应了"。

​2. 为什么需要停止位?​

  • ​同步复位​:接收方检测到停止位后,复位内部计时器,准备接收下一个数据包。

3. 停止位的作用时机​

  • ​数据包结束时生效​:发送方发送停止位,接收方检测到后确认当前数据包接收完成。

五、粘包、半包如何处理呢?

出现粘包的原因,有很多种情况:

  1. 发送方连续发送多个数据包,接收方处理速度不足,导致缓冲区堆积。
  2. 若协议未定义数据包长度,接收方无法判断何时结束。
  3. 读取数据的频率控制不了,也无法干涉,有几率出现拿不到完整的数据,可能是因为发送方阻塞,或者发一半的时候被取走了。

4种处理方式:

  1. 固定长度
  2. 包头包尾标识
  3. 长度字段
  4. 默认不处理

5.1 默认不处理【测试时使用】

一般生产环境中不使用这种模式。

建议使用场景:

1. 对数据完整性要求低的简单调试

2. 传输固定长度数据包的场景

3. 需快速验证基础通信功能的场景

java 复制代码
 @Override
    public byte[] execute(InputStream is) {
        try {
            // 获取当前可读取的字节数(不保证后续实际读取数量)
            int available = is.available();
            
            if (available > 0) {
                // 创建与可用字节数相等的缓冲区
                byte[] buffer = new byte[available];
                
                // 尝试读取数据(实际读取数可能<=available)
                int size = is.read(buffer);
                
                if (size > 0) {
                    // 返回实际读取的字节(可能包含不完整数据包)
                    return buffer;
                }
            } else {
                // 无可用数据时暂停,避免CPU空转(可能引入延迟)
                SystemClock.sleep(50);
            }
            
        } catch (IOException e) {
            // 异常处理(建议记录日志而非仅打印堆栈)
            e.printStackTrace();
        }
        // 无数据或异常时返回null
        return null;
    }

5.2 包头包尾标识【使用较多】

适用场景:协议明确规定了数据包的开始和结束标记.

在数据包头部和尾部添加特殊标记(如0xAA 0x55)。

复制代码
// 示例:数据包格式
[0xAA][0x55][数据][CRC校验][0x55][0xAA]

拿到数据以后,就解析头和尾,取出数据,如果尾没有,可以判断为是半包,需要先把前面的数据存储起来,继续从串口里面拿数据,进行拼接。

需要结合缓冲区。

5.3 长度字段【使用较多】

包头中明确声明数据长度,接收方按长度读取。

java 复制代码
// 示例:包头包含长度信息
struct PacketHeader {
    uint8_t start_marker;  // 0xAA
    uint8_t length;        // 数据部分长度
    uint8_t data[256];     // 数据
    uint8_t crc;           // 校验码
};

有了长度字段以后,我们获取数据的时候,就会根据字段的长度来拿到指定的数据。如果长度不符号,那么数据就是出现了问题。

5.4 固定长度

需要和下位机,约定每个数据包固定长度(如64字节),不足部分填充空值。

复制代码
// 示例:温度数据固定为8字节(含填充)
uint8_t packet[8] = {'T', '2', '5', 0x00, 0x00, 0x00, 0x00, 0x00};

数据固定长度,那么就更加好办了,需要严格按照这个长度要求,如果超出,那么就会出现问题。比较严格,不利于扩展。

5.5 下面我们使用头尾标识+缓冲区+超时写一个处理半包,粘包问题的代码。

  1. 这里我们主要看读线程,所以先粘贴一个不做任何处理粘包问题的代码。
java 复制代码
    @Override
    public byte[] execute(InputStream is) {
        try {
            int available = is.available();
            if (available > 0) {
                byte[] buffer = new byte[available];
                int size = is.read(buffer);
                if (size > 0) {
                    return buffer;
                }
            } else {
                SystemClock.sleep(50);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
  1. 我们创建一个缓冲区出来,将收到的数据存储起来
java 复制代码
private final ByteArrayOutputStream receiveBuffer = new ByteArrayOutputStream();

将收到的数据,放到缓冲区里面

java 复制代码
    @Override
    public byte[] execute(InputStream is) {
        try {
            int available = is.available();
            if (available > 0) {
                byte[] buffer = new byte[available];
                int size = is.read(buffer);
                if (size > 0) {
                    synchronized (receiveBuffer) {
                        // 将新数据追加到缓冲区 追加到 receiveBuffer(ByteArrayOutputStream)的尾部。
                        receiveBuffer.write(received, 0, size);
                        // 尝试解析缓冲区
                        processBuffer();
                    }
                }
            } else {
                SystemClock.sleep(50);
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

下面我们看看如何解析缓冲区的

java 复制代码
// 解析缓冲区核心逻辑
private void processBuffer() {

    // 1. 将缓冲区数据转为字节数组
    byte[] bufferData = receiveBuffer.toByteArray();
    int bufferSize = bufferData.length;
    
    // 2. 查找起始符位置(如:0xAA 0xBB)
    int startIndex = findStartIndex(bufferData);
    if (startIndex == -1) {
     // 情况1:未找到起始符,清空无效数据(避免内存溢出)
        resetBuffer(bufferSize); 
        return;
    }
    
     // 3. 在起始符之后查找结束符位置(如:0x0D 0x0A)
    int endIndex = findEndIndex(bufferData, startIndex);
    if (endIndex == -1) {
         // 情况2:找到起始符但未找到结束符,保留起始符之后的未处理数据
        retainUnprocessedData(startIndex); 
        return;
    }
    
    // 4.提取完整数据帧(排除起始符和结束符)
    int dataStart = startIndex + START_FLAG.length;
    int dataEnd = endIndex - END_FLAG.length + 1;
    if (dataEnd >= dataStart) {
        byte[] packet = Arrays.copyOfRange(bufferData, dataStart, dataEnd);
        //将完整的数据报传递给业务层处理
        onPacketReceived(packet); 
    }
    
     // 5. 保留结束符之后的数据,用于下次处理【处理粘包】
    retainRemainingData(endIndex + 1, bufferSize);
}

逻辑步骤:

复制代码
1. 将缓冲区数据转为字节数组
2. 查找起始符位置(如:0xAA 0xBB)
3. 在起始符之后查找结束符位置(如:0x0D 0x0A)
(1)找到起始符但未找到结束符,保留起始符之后的未处理数据【解决半包的问题】
4. 提取有效数据(排除起始符和结束符)
5. 保留结束符之后的数据,用于下次处理【解决粘包的问题】

retainRemainingData、retainUnprocessedData方法的代码

java 复制代码
/**
 * 保留剩余数据(当完整包处理完成后)
 * @param endIndex 当前数据包结束位置
 * @param totalSize 缓冲区总大小
 */
private void retainRemainingData(int endIndex, int totalSize) {
    if (endIndex < totalSize) {
        byte[] remaining = Arrays.copyOfRange(
            receiveBuffer.toByteArray(), 
            endIndex, 
            totalSize
        );
        receiveBuffer.reset();
        receiveBuffer.write(remaining, 0, remaining.length);
    } else {
        receiveBuffer.reset();
    }
}


/**
 * 保留未处理数据(当找到起始符但未找到结束符时)
 * @param startIndex 起始符的起始位置
 */
private void retainUnprocessedData(int startIndex) {
    byte[] remaining = Arrays.copyOfRange(
        receiveBuffer.toByteArray(), 
        startIndex, 
        receiveBuffer.size()
    );
    receiveBuffer.reset();
    receiveBuffer.write(remaining, 0, remaining.length);
}

假如说,一直都没有尾呢?所以我们需要定期清理缓冲区的内容,避免出现内存问题。

java 复制代码
private static final long TIMEOUT_MS = 1000; // 超时时间1秒
public void init() {
    timeoutTask = new Runnable() {
        @Override
        public void run() {
            synchronized (receiveBuffer) {
                if (receiveBuffer.size() > 0) {
                    receiveBuffer.reset();
                }
            }
        }
    };
}

注意,得处理多线程的问题。

缓冲区是什么?

• 实时解析串口输入的字节流,动态分割为完整数据包

• 解决粘包(多个包粘连)和半包(一个包分多次到达)问题。


六、流控

流控,简单理解,就是控制数据收发的频率。比如你处理数据不过来的时候,让其先不发。

  1. 硬件流控(可选)​

    • 启用 RTS/CTS 硬件流控,防止缓冲区溢出导致丢数据。
    • 适合高速传输场景(如波特率 ≥ 115200)。

七、校验和

  • ​累加和校验​​(简单):

    • 将数据所有字节相加,取低8位作为校验码。
    • 示例:数据 0x01 0x02 → 校验码 = 0x03。
  • ​CRC 校验​​(更可靠):

    • 通过复杂算法生成校验码(如 CRC16),可检测多位错误。
    • 工具:在线 CRC 计算工具生成代码。
相关推荐
大G哥19 分钟前
PHP标签+注释+html混写+变量
android·开发语言·前端·html·php
CYRUS_STUDIO1 小时前
使用 Dex2C 加壳保护 Android APK 代码
android·安全·逆向
alexhilton2 小时前
理解Jetpack Compose中副作用函数的内部原理
android·kotlin·android jetpack
恋猫de小郭6 小时前
腾讯 Kuikly 正式开源,了解一下这个基于 Kotlin 的全平台框架
android·前端·ios
贫道绝缘子6 小时前
【Android】四大组件之Activity
android
人生游戏牛马NPC1号7 小时前
学习Android(四)
android·kotlin
_祝你今天愉快7 小时前
安卓触摸事件分发机制分析
android
fyr897577 小时前
Ubuntu 下编译goldfish内核并使用模拟器运行
android·linux
心之所向,自强不息8 小时前
关于Android Studio的Gradle各项配置
android·ide·gradle·android studio
隐-梵8 小时前
Android studio学习之路(八)---Fragment碎片化页面的使用
android·学习·android studio