
在 RK3588 板卡上跑血压计 Demo 时,我们遇到过一个很迷惑的现象:点击"开始测量",日志打印了 FF C0 03 A3 A0,气泵也启动了,界面却没有持续更新气压。第一反应很容易是"串口已经通了,应该查 UI"。但发送成功、收到字节、收到有效测量帧,其实是三件事。
这个 Demo 通过 Linux 设备节点读写串口;只有接收线程拿到完整且校验通过的协议帧,Android 界面才有气压或血压值可显示。
本文以仓库中的 Java Demo 为准:应用配置为 minSdk 30、targetSdk 36,现场使用过 RK3588、Android 12 的调试板卡。/dev/ttyS1、115200 波特率和血压模块协议都属于这套硬件的约定,换板时要重新核对。
1. 先把调用链画出来
text
点击按钮
→ MainActivity.startMeasurement()
→ SerialPortManager.sendCommand()
→ /dev/ttyS1 的 TX → 血压模块
血压模块的 TX → /dev/ttyS1 的 RX
→ SerialReader 线程 → ProtocolParser.feed()
→ onPressureData() / onResultData() / onErrorData()
→ 主线程 Handler → TextView
MainActivity 创建并持有 SerialPortManager 和 ProtocolParser;读取工作由 SerialReader 线程执行;解析结果通过监听器回到 Activity,再交给主线程修改界面。这里没有 Android 系统服务替应用解释血压协议:系统负责把串口字节送到进程,帧格式和业务含义由应用自己处理。Handler.post() 的线程切换语义见 Android 官方文档。
这个地图也指出了调试顺序:先确认 TX,再确认 RX 的原始字节,最后才查协议解析和 UI。只看到"发送命令"日志,不能跳过中间几层。
2. 应用怎样打开串口
入口在 MainActivity.java:它把设备路径固定为 /dev/ttyS1,波特率固定为 115200。真正的打开逻辑在 SerialPortManager.java:先检查节点和访问权限,配置串口参数,再分别创建输入、输出流。
当前 Demo 的配置命令是:
java
String cmd = "stty -F " + devicePath + " " + baudRate
+ " cs8 -cstopb -parenb -crtscts raw min 1 time 0";
Process process = Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd});
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("stty返回码: " + exitCode);
}
这里的 cs8 -cstopb -parenb 是 8 位数据、1 位停止位、无校验;-crtscts 关闭硬件流控;raw 避免终端行处理改写二进制数据。最后的 min 1 time 0 很关键:至少收到 1 字节才结束一次读取,且没有空闲超时。Linux termios 文档给出了这两个参数的四种组合。
我们在板上实际遇到过 min=0 time=1:没有新字节时,底层 read() 约 0.1 秒后返回 0;Android 的 IoBridge 会把这次零字节读取映射为 Java 流的 -1,也就是 EOF。原先读取线程因此退出,界面还能继续发启动命令,却再也收不到结果。AOSP IoBridge.read() 源码可以核对这一行为。Demo 现在显式设置 min 1 time 0,并在读取意外终止时报告错误。
设备节点权限还有另一层:File.canRead()、File.canWrite() 只能说明文件访问检查的一部分。调试板卡处于 SELinux Permissive 时,拒绝会被记录但不执行;产品固件若处于 Enforcing,必须由板卡固件提供合适的设备节点权限和策略。不要把调试机上的 chmod 666 当作应用层的通用授权方案。AOSP 的设备策略说明对此有明确区分。stty 也依赖板卡镜像提供对应命令;正式产品更适合由受控的厂商接口或 native termios 层统一配置串口。
3. 为什么写入成功还不等于收到数据
sendCommand() 把命令写入 FileOutputStream 并打印十六进制日志。比如 FF C0 03 A3 A0 是"开始测量",FF C0 03 A4 A1 是"停止测量"。日志只能证明写入调用没有抛出异常,不能证明模块返回了应答,更不能证明板卡 RX 收到了应答。
读取线程则在 inputStream.read(buffer) 上等待。它拿到的是本次读取到的一段字节 ,不是天然完整的一帧:一帧可能被拆成两次读取,两帧也可能落在同一次读取里。所以 Demo 先复制本次字节,再交给 ProtocolParser.feed() 累积和扫描。字节读取、协议解析都在工作线程;MainActivity 用 Handler(Looper.getMainLooper()) 将 TextView 更新交回主线程。
Activity 销毁时会关闭输入输出流,让阻塞读取退出。当前实现没有自动重连;如果读线程意外退出,应用会提示错误并拒绝继续发送测量命令。这比让气泵启动而界面永远等待更容易诊断。
4. 一帧血压数据如何变成界面文字
这个模块的帧以 FF C0 开始,后面依次是长度、校验和、命令和数据。这里的长度从长度字节自身 算起,因此 LEN=05 的帧总共 7 字节。校验和是"长度 + 命令 + 数据"的低 8 位,不包含帧头和校验和字段。
| 帧例子 | 含义 | 界面动作 |
|---|---|---|
FF C0 03 A3 A0 |
启动测量命令 | 发送到模块 |
FF C0 05 A8 A0 00 03 |
气压 3 mmHg,心跳标志为否 | 更新状态文字中的气压 |
FF C0 04 B2 AD 01 |
模块错误码 1 | 显示测量失败和模块错误 |
FF C0 08 ... AC ... |
最终结果帧 | 更新收缩压、舒张压、心率 |
以气压帧为例,05 + A0 + 00 + 03 = A8,所以第四个字节应为 A8。ProtocolParser.java先找 FF C0,按长度等待完整帧,再算校验和;任何一步不成立,都不会调用气压或结果回调。
测量中的 A0 帧只提供气压和"检测到心跳"的标志位。当前协议解析代码没有测量过程中的心率数字 ;界面上的 bpm 要等最终 AC 帧到达才会填入。气压、心跳标志和最终心率不是同一个字段。
5. RK3588 上的一次故障,应该从哪层查
这次现场最有用的不是"发送命令"日志,而是接收原始字节。气泵运行时,应用大约每 0.54 秒收到一组字节,但很多是 FF FF F5 ...、FF FE ...,不满足 FF C0 帧头。直到末尾才收到两帧有效的 FF C0 05 A8 A0 00 03,随后收到 FF C0 04 B2 AD 01。这说明当时接收线程在工作,持续气压没有显示的直接原因是多数周期数据无法组成有效帧;最终也没有可供界面展示的 AC 结果帧。
定位时我会按下面的顺序取证:
bash
adb shell stty -F /dev/ttyS1 -a
adb logcat -s SerialPortManager:V ProtocolParser:V
adb shell su 0 cat /proc/tty/driver/serial
第一条看波特率、数据格式和 min/time;第二条同时看发送、原始接收字节、校验结果;第三条在这块有 su 的调试板上看 UART 的 TX/RX 计数和帧错误,普通量产设备未必能执行。另一个应用若也打开 /dev/ttyS1,可能抢读字节或改动共用串口参数,测试前应让串口只有一个所有者。
现场数据在气泵运行阶段集中损坏,提示要检查模块 TX 到板卡 RX 的整条路径 :接线、共地、电平、供电波动,以及电机工作时的干扰。这是根据日志时间关系提出的排查方向,不是仅凭日志就能确定的硬件结论。最直接的验证是同时看模块 TX 引脚和板卡 RX 引脚:如果模块输出已经损坏,查模块供电与气泵;如果模块输出完整、板卡收到的字节却变了,查接线、转换电路和板卡输入。LubanCat 的串口文档也提醒,不同 UART 复用组对应不同引脚,换板不能只照搬设备节点名称。
还有一条容易误导的日志:alsa_route 找不到 aw87xxx_profile_switch_0 属于板卡音频服务,与本 Demo 的 UART 读写不是同一条调用链。看串口问题时,应先按进程、标签和时间把日志分开。
这个 Demo 给我的实践结论很简单:**先确认原始 RX 字节是否构成有效帧,再决定改串口参数、协议解析,还是界面代码。**气泵会转只证明命令走到了模块;实时气压要靠每一帧可信的返回数据。