Android 串口通讯全解析:从硬件到代码实现

在工业控制、智能硬件、物联网等场景中,Android 设备常需与外部硬件(如传感器、PLC、读卡器)通过串口通信。串口作为一种传统的有线通信方式,以其简单可靠的特性,至今仍是设备间数据交互的重要选择。本文将从硬件基础到代码实现,全面讲解 Android 串口通讯的开发流程,帮助开发者快速搭建串口交互功能。

一、Android 串口通讯基础

1.1 什么是串口通讯?

串口通讯(Serial Communication)是指通过串行端口(如 RS-232、RS-485)实现设备间的数据传输。与 USB、蓝牙等方式相比,串口通讯具有硬件简单、协议灵活、抗干扰能力强的特点,适合短距离、低速率的数据交换(如工业设备状态监控、传感器数据采集)。

串口通讯的核心参数(需与外部设备一致):

  • 波特率(Baud Rate):数据传输速率(如 9600、115200,单位:位 / 秒),双方必须使用相同波特率;
  • 数据位(Data Bits):每个字节的数据长度(通常为 8 位);
  • 停止位(Stop Bits):标记一个数据帧的结束(通常为 1 位);
  • 校验位(Parity):用于数据校验(如无校验、奇校验、偶校验,常用 “无校验”)。

1.2 Android 设备如何支持串口?

Android 设备本身通常不自带物理串口(手机、平板以 USB、蓝牙为主),需通过以下方式实现串口通讯:

1.USB 转串口

通过 “USB 转 TTL/RS232” 模块(如 CH340、PL2303 芯片),将 Android 设备的 USB 接口转换为串口,这是最常用的方案。

2.内置串口的定制设备

工业级 Android 设备(如工控平板、物联网网关)通常自带硬件串口(如 UART 接口),可直接使用。

3.蓝牙转串口

通过蓝牙模块(如 HC-05)将蓝牙信号转为串口数据,适合无线串口场景(但本质是蓝牙通讯,本文以有线串口为例)。

1.3 核心概念:串口设备节点

在 Android(基于 Linux)中,所有硬件设备都以 “设备节点” 形式存在于文件系统中。串口设备节点通常路径为:

  • /dev/ttyS*(如ttyS0,内置硬件串口);
  • /dev/ttyUSB*(如ttyUSB0,USB 转串口模块,需驱动支持);
  • /dev/ttyACM*(如ttyACM0,部分 USB 转串口芯片的节点)。

串口通讯的本质是对设备节点进行文件读写操作:向节点写入数据(发送),从节点读取数据(接收)。

二、开发准备:硬件与依赖

2.1 硬件准备

  • Android 设备:需支持 USB Host 模式(大部分设备默认开启);
  • USB 转串口模块:如 CH340 模块(性价比高,支持 Android 驱动);
  • 外部串口设备:如温湿度传感器、PLC 控制器(需确认其串口参数);
  • 连接线:根据模块和设备接口选择(如杜邦线、DB9 串口线)。

硬件连接示例

Android 设备 USB 口 → USB 转串口模块(CH340) → 外部串口设备(如传感器),确保模块电源正常(部分模块需外接 5V 电源)。

2.2 驱动与权限

(1)USB 转串口驱动

USB 转串口模块需要 Android 设备支持对应的驱动:

  • CH340/CH341:大部分 Android 设备已内置驱动,无需额外安装;
  • PL2303:部分设备需手动安装驱动 APK(厂商提供);
  • CP2102:主流设备支持,可通过内核配置开启。

可通过adb shell ls /dev/ttyUSB*验证:插入模块后若能看到ttyUSB0节点,说明驱动已识别。

(2)Android 权限配置

在AndroidManifest.xml中添加 USB 相关权限:











    

对于 Android 6.0+,需动态申请 USB 设备访问权限(后续代码会涉及)。

2.3 依赖库:Android SerialPort

Android 原生 SDK 未提供串口操作 API,需使用第三方库封装串口读写逻辑。推荐使用android-serialport-api(GitHub 开源项目),核心功能是封装JNI层对串口设备节点的操作。

集成方式

  1. 在app/build.gradle中添加依赖:
    dependencies {
        implementation 'com.github.licheedev:Android-SerialPort:2.1.1'
    }

  2. 若需自定义,可参考其核心逻辑:通过 JNI 调用 Linux 的open/read/write系统函数操作设备节点。

三、串口通讯核心 API 与流程

串口通讯的开发流程可概括为 “打开串口→发送数据→接收数据→关闭串口”,核心类为SerialPort(封装节点操作)和InputStream/OutputStream(数据读写流)。

3.1 打开串口(核心步骤)

打开串口需指定设备节点路径和串口参数(波特率、校验位等),并申请设备访问权限。

import android.hardware.usb.UsbManager;
import com.licheedev.hwutils.HwUtils;
import com.licheedev.serialport.SerialPort;
import java.io.File;
import java.io.IOException;

public class SerialHelper {
    private SerialPort mSerialPort;
    private OutputStream mOutputStream;
    private InputStream mInputStream;
    private ReadThread mReadThread; // 接收数据的线程

    // 打开串口
    public boolean open(String devicePath, int baudRate) {
        // 1. 检查设备节点是否存在
        File device = new File(devicePath);
        if (!device.exists()) {
            Log.e("Serial", "设备节点不存在:" + devicePath);
            return false;
        }

        // 2. 申请USB设备权限(Android 6.0+)
        if (HwUtils.hasUsbPermission(device)) {
            try {
                // 3. 打开串口(参数:设备、波特率、校验位等)
                // 第三个参数:0表示无校验,1表示奇校验,2表示偶校验
                mSerialPort = new SerialPort(device, baudRate, 0);
                // 获取读写流
                mOutputStream = mSerialPort.getOutputStream();
                mInputStream = mSerialPort.getInputStream();

                // 4. 启动接收线程(持续读取数据)
                mReadThread = new ReadThread();
                mReadThread.start();
                return true;
            } catch (IOException e) {
                Log.e("Serial", "打开串口失败:" + e.getMessage());
                return false;
            }
        } else {
            // 申请权限(需在Activity中处理权限回调)
            HwUtils.requestUsbPermission(device);
            return false;
        }
    }

    // 接收数据的线程(必须在子线程中读取,避免阻塞主线程)
    private class ReadThread extends Thread {
        @Override
        public void run() {
            super.run();
            byte[] buffer = new byte[1024]; // 缓冲区
            int bytes;

            while (!isInterrupted() && mInputStream != null) {
                try {
                    // 读取数据(阻塞操作,若无数据会等待)
                    bytes = mInputStream.read(buffer);
                    if (bytes > 0) {
                        // 拷贝有效数据(避免缓冲区多余数据)
                        byte[] data = new byte[bytes];
                        System.arraycopy(buffer, 0, data, 0, bytes);
                        // 回调给UI层处理(需切换到主线程)
                        onDataReceived(data);
                    }
                } catch (IOException e) {
                    Log.e("Serial", "读取数据失败:" + e.getMessage());
                    break;
                }
            }
        }
    }

    // 数据接收回调(需在主线程处理)
    private void onDataReceived(byte[] data) {
        // 示例:转换为十六进制字符串
        String hexData = bytesToHex(data);
        Log.d("Serial", "收到数据:" + hexData);
        // 通知UI更新(可通过Handler或接口回调)
    }

    // 字节数组转十六进制字符串(便于调试)
    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            String hex = String.format("%02X ", b);
            sb.append(hex);
        }
        return sb.toString();
    }
}

关键说明

  • 设备节点路径需根据实际模块确定(可通过adb shell ls /dev/tty*查看插入模块前后的节点变化);
  • 波特率必须与外部设备一致(如传感器默认 9600,需对应设置);
  • 接收数据需在独立线程中进行(read方法是阻塞的,避免卡死主线程)。

3.2 发送数据

通过OutputStream向串口写入字节数组,需注意数据格式(与外部设备约定,如固定长度、带校验位)。

// 发送数据(字节数组)
public void send(byte[] data) {
    if (mOutputStream == null) {
        Log.e("Serial", "串口未打开");
        return;
    }

    try {
        mOutputStream.write(data);
        mOutputStream.flush(); // 立即发送
        Log.d("Serial", "发送数据:" + bytesToHex(data));
    } catch (IOException e) {
        Log.e("Serial", "发送失败:" + e.getMessage());
    }
}

// 发送字符串(需转为字节数组,指定编码)
public void send(String text) {
    send(text.getBytes(StandardCharsets.UTF_8));
}

// 发送十六进制字符串(如"AA BB CC")
public void sendHex(String hex) {
    byte[] data = hexToBytes(hex);
    if (data != null) {
        send(data);
    }
}

// 十六进制字符串转字节数组(工具方法)
private byte[] hexToBytes(String hex) {
    hex = hex.replaceAll(" ", ""); // 去除空格
    int len = hex.length();
    if (len % 2 != 0) {
        Log.e("Serial", "十六进制字符串长度应为偶数");
        return null;
    }

    byte[] bytes = new byte[len / 2];
    for (int i = 0; i < len; i += 2) {
        bytes[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
    }
    return bytes;
}

3.3 关闭串口(释放资源)

使用完毕后需关闭串口,停止接收线程,避免资源泄漏。

// 关闭串口
public void close() {
    // 停止接收线程
    if (mReadThread != null) {
        mReadThread.interrupt();
        mReadThread = null;
    }

    // 关闭流
    try {
        if (mInputStream != null) {
            mInputStream.close();
            mInputStream = null;
        }
        if (mOutputStream != null) {
            mOutputStream.close();
            mOutputStream = null;
        }
    } catch (IOException e) {
        Log.e("Serial", "关闭流失败:" + e.getMessage());
    }

    // 关闭串口
    if (mSerialPort != null) {
        mSerialPort.close();
        mSerialPort = null;
    }
}

3.4 权限申请处理

在 Activity 中处理 USB 权限申请回调,确保能访问串口设备:

public class SerialActivity extends AppCompatActivity {
    private SerialHelper mSerialHelper;
    private static final String ACTION_USB_PERMISSION = "com.example.serial.USB_PERMISSION";
    private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (ACTION_USB_PERMISSION.equals(action)) {
                synchronized (this) {
                    UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                        if (device != null) {
                            // 权限已获取,打开串口
                            mSerialHelper.open("/dev/ttyUSB0", 9600);
                        }
                    } else {
                        Log.e("Serial", "用户拒绝了USB权限");
                    }
                }
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_serial);

        mSerialHelper = new SerialHelper();

        // 注册USB权限广播
        IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
        registerReceiver(mUsbReceiver, filter);

        // 初始化时尝试打开串口
        findViewById(R.id.btn_open).setOnClickListener(v -> {
            // 检查是否有USB权限
            File device = new File("/dev/ttyUSB0");
            if (!HwUtils.hasUsbPermission(device)) {
                // 申请权限
                PendingIntent permissionIntent = PendingIntent.getBroadcast(
                    this, 0, new Intent(ACTION_USB_PERMISSION),
                    PendingIntent.FLAG_IMMUTABLE
                );
                UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
                usbManager.requestPermission(HwUtils.getUsbDevice(device), permissionIntent);
            } else {
                // 已有权限,直接打开
                mSerialHelper.open("/dev/ttyUSB0", 9600);
            }
        });

        // 发送数据按钮
        findViewById(R.id.btn_send).setOnClickListener(v -> {
            String text = ((EditText) findViewById(R.id.et_send)).getText().toString();
            mSerialHelper.send(text);
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 注销广播,关闭串口
        unregisterReceiver(mUsbReceiver);
        mSerialHelper.close();
    }
}

四、实战案例:与串口设备通信

以 “Android 设备与温湿度传感器通过串口通信” 为例,说明完整流程。

4.1 场景需求

  • 传感器通过串口输出温湿度数据(格式:AA 01 02 03 BB,其中 02、03 分别为温度、湿度值);
  • Android 设备发送指令AA 00 BB请求数据;
  • 接收数据后解析温度和湿度,显示在 UI 上。

4.2 代码实现(核心逻辑)

// 在SerialHelper的onDataReceived中解析数据
private void onDataReceived(byte[] data) {
    String hexData = bytesToHex(data);
    Log.d("Serial", "收到原始数据:" + hexData);

    // 按协议解析(示例:AA 01 02 03 BB)
    if (data.length == 5 
        && data[0] == (byte) 0xAA 
        && data[4] == (byte) 0xBB) {
        
        int temp = data[2] & 0xFF; // 温度值(转为无符号整数)
        int humi = data[3] & 0xFF; // 湿度值
        
        // 切换到主线程更新UI
        runOnUiThread(() -> {
            tvTemp.setText("温度:" + temp + "℃");
            tvHumi.setText("湿度:" + humi + "%");
        });
    } else {
        Log.w("Serial", "数据格式不符");
    }
}

// 发送请求指令(在Activity中调用)
findViewById(R.id.btn_request).setOnClickListener(v -> {
    // 发送指令:AA 00 BB
    mSerialHelper.sendHex("AA 00 BB");
});

五、常见问题及解决方案

5.1 串口无法打开(Permission denied)

  • 原因:设备节点无读写权限,或未获取 USB 权限。
  • 解决
  1. 检查/dev/ttyUSB0的权限(adb shell ls -l /dev/ttyUSB0),确保有rw权限;
  2. 确认已申请 USB 权限(在onCreate中主动请求);
  3. 对于 root 设备,可通过命令修改权限:adb shell chmod 666 /dev/ttyUSB0。

5.2 数据接收乱码或不完整

  • 原因:串口参数不匹配(波特率错误),或接收线程读取不及时。
  • 解决
  1. 核对波特率、数据位、校验位(必须与外部设备完全一致);
  2. 增大接收缓冲区(如byte[] buffer = new byte[4096]);
  3. 确保接收线程不被阻塞(避免在run方法中做耗时操作)。

5.3 发送数据无响应

  • 原因:硬件连接错误,或外部设备未正确识别指令。
  • 解决
  1. 检查接线(TX 接 RX,RX 接 TX,共地,避免交叉);
  2. 用串口调试助手(如 Windows 的 SSCOM)测试外部设备是否正常;
  3. 确认指令格式正确(如是否需要校验位、结束符)。

5.4 USB 转串口模块不识别

  • 原因:驱动不支持或模块供电不足。
  • 解决
  1. 更换 CH340 等主流芯片的模块(兼容性更好);
  2. 检查模块是否需要外接电源(部分模块仅靠 USB 供电不足);
  3. 在设备/proc/tty/drivers中查看是否有usbserial驱动。

六、优化与扩展

6.1 数据校验机制

为确保数据可靠性,可在协议中添加校验位(如 CRC 校验):

// 计算CRC16校验(示例)
public static int crc16(byte[] data) {
    int crc = 0xFFFF;
    for (byte b : data) {
        crc ^= (int) b & 0xFF;
        for (int i = 0; i < 8; i++) {
            if ((crc & 1) != 0) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

// 发送数据时添加CRC
byte[] data = "Hello".getBytes();
int crc = crc16(data);
byte[] crcBytes = new byte[2];
crcBytes[0] = (byte) (crc & 0xFF);
crcBytes[1] = (byte) (crc >> 8);
// 拼接数据和CRC后发送
byte[] sendData = ArrayUtils.addAll(data, crcBytes);

6.2 断线重连机制

监测串口连接状态,断开时自动重连:

// 在SerialHelper中添加状态监测
private void checkConnectStatus() {
    new Handler(Looper.getMainLooper()).postDelayed(() -> {
        if (mSerialPort == null || !isReading()) { // 未打开或读取中断
            Log.w("Serial", "尝试重连串口");
            open("/dev/ttyUSB0", 9600);
        }
        checkConnectStatus(); // 循环监测
    }, 3000); // 每3秒检查一次
}

6.3 多串口管理

对于需要连接多个串口设备的场景(如工业控制),可封装SerialManager管理多个SerialHelper实例:

public class SerialManager {
    private Map mSerialHelpers = new HashMap<>();

    // 获取指定串口的助手
    public SerialHelper getSerial(String devicePath) {
        if (!mSerialHelpers.containsKey(devicePath)) {
            mSerialHelpers.put(devicePath, new SerialHelper());
        }
        return mSerialHelpers.get(devicePath);
    }

    // 关闭所有串口
    public void closeAll() {
        for (SerialHelper helper : mSerialHelpers.values()) {
            helper.close();
        }
        mSerialHelpers.clear();
    }
}

七、总结

Android 串口通讯的核心是 “通过设备节点进行文件读写”,开发流程遵循 “打开→发送→接收→关闭” 的逻辑。关键在于:

  • 硬件连接与驱动支持(USB 转串口模块需正确识别);
  • 串口参数匹配(波特率、校验位必须与外部设备一致);
  • 数据解析与线程管理(接收需异步,避免主线程阻塞)。

实际开发中,需根据外部设备的通讯协议(数据格式、指令集)定制解析逻辑,并做好异常处理(断线重连、数据校验)。通过本文的步骤,可快速搭建稳定的串口通讯功能,满足工业控制、智能硬件等场景的需求。

你可能感兴趣的:(android,串口通讯,java)