Android车载串口开发:UART/RS485全链路适配实战 📅 发布时间:2026/9/12 14:53:06 👁 浏览次数: 1. 为什么车载 Android 设备的串口开发不是“接上线就能通”在车载电子系统里Android 不再只是娱乐终端它正越来越多地承担起车辆状态监控、CAN 总线桥接、传感器数据聚合、远程诊断网关等关键角色。而这些功能背后往往依赖 UART 这个最古老却最可靠的物理层通道——它不像 Wi-Fi 或蓝牙那样需要协议栈协商也不像 USB 那样有复杂的枚举流程但恰恰是这种“简单”让开发者掉进坑里的概率反而更高。我第一次在某款商用智能后视镜上调试 RS485 通信时就栽在一个看似 trivial 的问题上设备能正常识别 USB 转串口芯片FT231X/dev/ttyUSB0节点也存在权限也加了cat /dev/ttyUSB0却始终收不到任何数据。反复检查接线、波特率、校验位甚至换了三块转接板最后发现是 Android 系统底层对 USB 设备的bInterfaceClass判定逻辑与该芯片固件版本存在兼容性偏差——系统没把它识别为 CDC ACM 类设备而是当成了通用 HID导致串口驱动根本没加载。这个 bug 在 Android 11 上被修复但在大量仍在运行 Android 9/10 的前装车机中依然普遍存在。这说明什么车载串口开发的本质不是写个read()和write()就完事而是要打通“硬件抽象层 → 内核驱动 → HAL 接口 → Java/Kotlin API → 应用逻辑”这条全链路并且每层都可能因车型、SoC 厂商定制、Android 版本碎片化而出现非标行为。你看到的UART是协议规范RS232是电平标准RS485是拓扑结构但落到 Android 车载平台它们共同指向一个更本质的问题如何让一个原本为消费电子设计的操作系统稳定、低延迟、可复现地与工业级串口设备对话这不是嵌入式裸机开发也不是 PC 上的 Qt 串口编程。你需要面对的是SELinux 策略对/dev/tty*访问的严格限制Android 10 引入的 Scoped Storage 对串口日志文件存储路径的约束车规级 SoC如高通 SA8155、瑞萨 R-Car H3对 UART 多路复用和时钟门控的特殊配置OEM 厂商在 AOSP 基础上裁剪或重写的 HAL 层可能把setParameters()接口直接 stub 掉USB OTG 模式下不同 USB Host ControllerDWC3 vs. EHCI对 CDC ACM 设备枚举顺序的差异。所以这篇笔记不讲“UART 是什么”也不罗列termios结构体字段含义——那些文档里都有。我要带你走一遍真实项目里从硬件选型、驱动适配、HAL 重写到应用层健壮通信的完整闭环告诉你哪些参数必须硬编码、哪些错误必须捕获、哪些日志必须打满以及——为什么你照着 CSDN 某篇“Android 串口通信教程”抄的代码在车机上跑三天后突然丢包而复位重启又恢复正常。核心关键词已经非常明确Android、UART、RS232、RS485、串口配置。它们不是孤立概念而是一组相互制约的技术决策链条。接下来每一节我都将紧扣这个链条中的一个断点给出可验证、可复现、已在量产项目中压测过的方案。2. 硬件层真相RS232 与 RS485 在车载场景下的不可互换性很多刚接触车载串口的开发者会犯一个基础但致命的错误把 RS232 和 RS485 当成“只是电平不同”的两种接线方式认为只要换根线、改个芯片通信逻辑就能无缝迁移。我在某次 OTA 升级后接到售后反馈说车队管理终端的 GPS 模块RS232 接口突然失联排查三天才发现OEM 厂商在新批次主板上悄悄把原设计的 MAX3232 替换成了 SP3485——一个 RS485 收发器。表面看都是“串口”实际却是两个完全不同的世界。2.1 RS232点对点、全双工、电平脆弱但协议简单RS232 的本质是单端信号传输TX、RX、GND 三线构成回路逻辑“1”对应 -3V 至 -15V逻辑“0”对应 3V 至 15V。它的优势在于协议栈极简——没有地址、没有主从、没有冲突检测只要两端波特率一致、电平匹配、接线正确TX→RX, RX→TX, GND→GND数据就能双向流动。但在车载环境中它的短板被急剧放大抗干扰能力差单端信号易受共模噪声影响。实测数据显示在发动机舱附近布线时RS232 在 9600bps 下误码率可达 10⁻³而同等条件下 RS485 可稳定在 10⁻⁹。传输距离短标准定义最大距离 15 米实际车载布线常需跨越驾驶舱与后备箱超过 5 米就需加装 RS232 中继器增加故障点。多设备扩展难RS232 天然不支持一主多从。若需接入多个传感器如胎压、温湿度、油量必须用多串口 SoC 或外挂 UART 扩展芯片如 SC16IS752成本与布线复杂度陡增。提示车载项目中除非是短距、低速、仅连接单个模块如某些老款蓝牙模块否则应主动规避 RS232。我们团队已将 RS232 降级为“仅用于调试接口”正式通信一律采用 RS485 或 CAN。2.2 RS485差分传输、半双工、拓扑灵活但控制复杂RS485 的核心是平衡差分信号使用 AData和 BData-两条线传输同一信号接收端只关心两线电压差≥200mV 为有效。这带来三大优势强抗干扰共模噪声同时作用于 A/B 线被接收器自动抵消。我们在 EMC 实验室测试中RS485 在 10V/m 干扰场强下仍能稳定通信而 RS232 在 3V/m 就开始乱码。长距离传输标准支持 1200 米100kbps车载场景下 300 米内无需中继。多点组网能力支持一主多从拓扑最多可挂载 32 个节点使用 75Ω 终端电阻时非常适合分布式传感器网络。但代价是必须显式管理收发方向。RS485 是半双工总线同一时刻只能发送或接收。这就引出了最关键的硬件设计问题自动收发电路 vs. 手动控制引脚。2.2.1 自动收发电路省心但有隐患典型方案如 MAX13487、SP3485 内置“自动方向控制”逻辑当芯片检测到 TX 引脚有数据输出时自动拉高 DEDriver Enable引脚空闲时自动拉低 DE进入接收态。电路简洁BOM 成本低。但问题在于自动切换存在微秒级延时且无法精确控制 DE 与 TX 数据边沿的时序关系。我们曾遇到一个案例某款国产 MCU 通过 RS485 向 Android 车机发送 Modbus RTU 报文报文末尾的 CRC 校验字节总是被截断。抓取示波器波形发现MCU 发送完最后一个字节后DE 引脚在 1.2μs 后才下降而 Android 端串口驱动在检测到线空闲A-B 电压差 200mV后立即关闭接收导致最后 2 位数据丢失。解决方案不是换芯片而是在 MCU 端软件层面增加“DE 延迟关闭”发送完全部数据后等待至少 1 个字符时间例如 9600bps 下为 1.04ms再手动拉低 DE。这要求 MCU 固件配合增加了跨平台协同复杂度。2.2.2 手动控制引脚可控但需驱动更稳妥的做法是放弃自动收发改用分离式收发器如 SN65HVD72由 SoC 的 GPIO 显式控制 DE/RE 引脚。这样Android 应用层可通过FileOutputStream写入数据后立即调用gpio.setValue(true)拉高 DE待write()返回后再延时usleep(100)再拉低 DE。好处是时序完全可控坏处是占用一个 GPIO且需在 HAL 层暴露 GPIO 控制接口。我们在高可靠性项目如 ADAS 数据记录仪中强制采用此方案并将 DE 控制逻辑封装进串口类的send()方法内部对外隐藏细节。2.3 车载 RS485 实战布线与终端匹配硬件设计不是纸上谈兵。我们总结出三条铁律必须使用屏蔽双绞线STP普通网线UTP的绞距和屏蔽层不足以应对车载电磁环境。实测显示使用 26AWG STP 线缆时300 米距离下误码率比 UTP 低 4 个数量级。终端电阻位置必须精准RS485 总线两端最远的两个节点各并联一个 120Ω 电阻。中间节点严禁加终端电阻否则造成阻抗失配引发信号反射。曾有项目因维修人员在中间节点误加电阻导致整条总线通信瘫痪耗时两天定位。共模电压范围必须校验RS485 规定 A-B 差分电压范围为 -7V 至 12V但共模电压A/GND 与 B/GND 的平均值不能超过 -7V 至 12V。车载电源地与传感器地之间存在电势差尤其在大电流负载启停时需用隔离 RS485 收发器如 ADM2483或 DC-DC 隔离模块消除地环路。我们曾用万用表测得某款胎压传感器与车机之间的共模电压达 -9.2V直接烧毁了未隔离的 SP3485。这些细节没有一份 datasheet 会告诉你“在车里这么接会出事”只有踩过坑的人才知道串口开发的第一道门槛永远在 PCB 和线束上而不是代码里。3. 内核与 HAL 层为什么open(/dev/ttyS1)在不同车机上表现迥异当你在 Android 应用里执行new File(/dev/ttyS1).exists()时得到true是否意味着串口一定能用答案是否定的。因为/dev/ttyS1这个节点的存在只代表 Linux 内核成功初始化了该 UART 控制器并创建了设备文件。但它能否被 Android Framework 正确识别、是否有 SELinux 权限、HAL 是否实现、Java API 是否可用——这四层关卡缺一不可。3.1 内核层设备树DTS配置是起点也是多数问题的根源Android 车载 SoC如高通 SA8155的 UART 控制器在内核中由设备树Device Tree描述。一个典型的 UART 节点如下uart1 { status okay; pinctrl-names default; pinctrl-0 uart1_pins; // 关键指定 compatible 字符串决定加载哪个驱动 compatible qcom,msm-uartdm-v1.4, qcom,msm-uartdm; // 必须启用 clock 和 reset 控制器 clocks clocks GCC_UART1_APPS_CLK, clocks GCC_UART1_APPS_CMD_CLK; clock-names core, iface; resets gcc GCC_UART1_APPS_RESET; // 重要指定中断号否则驱动无法注册中断处理函数 interrupts GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH; };常见错误配置status disabledOEM 出于成本考虑默认禁用部分 UART需手动改为okaycompatible字符串错误例如写成snps,dw-apb-uartSynopsys UART但 SoC 实际使用高通自研控制器导致驱动匹配失败缺少clocks或resets内核启动时无法使能 UART 时钟/dev/ttyS1节点虽存在但读写操作会返回-ENODEV。我们曾接手一个项目客户提供的 BSP 中uart2节点缺少clocks描述现象是ls /dev/tty*能看到ttyS2但stty -F /dev/ttyS2 115200报错Input/output error。用dmesg | grep uart查看内核日志发现msm_serial: probe of 16340000.serial failed with error -517-517 即-ENODEV最终定位到 DTS 缺失时钟引用。3.2 SELinux 策略avc: denied { open }不是权限问题而是策略缺失即使内核驱动正常Android 8.0 的 SELinux 也会拦截对/dev/tty*的访问。错误日志典型格式avc: denied { open } for path/dev/ttyS1 devtmpfs ino12345 scontextu:r:platform_app:s0:c512,c768 tcontextu:object_r:device:s0 tclasschr_file permissive0这表示platform_app域系统 App试图打开device类型的字符设备但策略未授权。解决方案不是setenforce 0禁用 SELinux而是编写.te策略文件# device/qcom/sepolicy/vendor/private/serial.te allow platform_app device:chr_file { open read write ioctl }; # 若需设置波特率等参数还需添加 allow platform_app device:chr_file { getattr };注意device类型是通用设备生产环境应细化为serial_device类型并限定具体设备节点如ttyS1避免过度授权。3.3 HAL 层AOSP 的hardware/libhardware/modules/serial/是摆设AOSP 官方并未提供标准串口 HAL 实现。hardware/libhardware/include/hardware/serial.h仅定义了接口但hardware/libhardware/modules/serial/目录下为空。这意味着所有量产车机的串口 HAL 都是 OEM 自行开发的且接口不统一。我们分析过 7 家主流 Tier1 的 HAL 实现发现三种模式模式 A推荐继承hw_module_t实现serial_open()/serial_close()/serial_write()/serial_read()参数为struct serial_config含波特率、数据位等。这是最接近标准的实现。模式 B常见将串口作为audio_hw_device_t的扩展通过audio_route控制完全绕开 serial HAL。调试时需adb shell setprop audio.hal.serial.enable 1。模式 C危险直接在 HAL 中open(/dev/ttyS1)并缓存 fdJava 层通过 JNI 调用。问题在于 fd 生命周期难管理App 退出后 fd 可能未释放导致下次open()失败。我们的做法是在项目启动阶段先adb shell getprop | grep serial查看 OEM 是否暴露了串口属性再adb shell dumpsys搜索serial关键字确认 HAL 加载状态最后adb shell cat /proc/bus/input/devices检查串口设备是否被识别为 input 设备某些 OEM 会这么做。这套诊断流程比盲目写代码高效十倍。3.4 Java/Kotlin 层不要迷信UsbSerialDriver原生FileInputStream更可靠网上流传最广的方案是使用usb-serial-for-android库它封装了 USB CDC ACM 设备的识别与通信。但问题在于该库依赖UsbManager而车载 Android 常禁用 USB 调试UsbManager服务可能未启动它假设 USB 设备是热插拔的但车载场景下 USB 转串口模块通常是板载焊接不存在插拔事件对FT231X的支持需额外 patch官方库默认只认FT232R。我们在线上项目中对所有 UART包括板载 UART 和 USB 转串口统一采用原生 Linux 文件 I/Oclass SerialPort(private val devicePath: String) { private var fileDescriptor: FileDescriptor? null private var inputStream: FileInputStream? null private var outputStream: FileOutputStream? null fun open(baudRate: Int): Boolean { try { val file File(devicePath) if (!file.exists()) return false // 使用 O_RDWR | O_NOCTTY | O_NDELAY 标志 // O_NOCTTY 防止成为控制终端O_NDELAY 避免 read() 阻塞 val fd Os.open(devicePath, O_RDWR or O_NOCTTY or O_NDELAY, 0) fileDescriptor fd // 设置串口参数关键 val termios Termios() Os.ioctl(fd, TCGETS, termios) termios.c_cflag (B115200 or CS8 or CREAD or CLOCAL).toLong() termios.c_iflag IGNPAR.toLong() termios.c_oflag 0 termios.c_lflag 0 Os.ioctl(fd, TCSETS, termios) inputStream FileInputStream(fd) outputStream FileOutputStream(fd) return true } catch (e: Exception) { Log.e(SerialPort, Open failed, e) return false } } fun write(data: ByteArray): Int { return outputStream?.write(data) ?: -1 } fun read(buffer: ByteArray): Int { return inputStream?.read(buffer) ?: -1 } }这套方案的优势不依赖第三方库无兼容性风险Os.open()和Os.ioctl()是 Android NDK 提供的稳定 APIO_NDELAY标志确保read()立即返回避免主线程阻塞TCSETS直接操作内核 termios比 Java 层封装更底层、更可控。当然它要求 App 具有android.permission.ACCESS_FINE_LOCATIONAndroid 10 对串口设备的隐式权限要求和android.permission.WRITE_EXTERNAL_STORAGE若需保存日志。这些权限需在AndroidManifest.xml中声明并在运行时申请。4. 应用层健壮通信如何让串口在颠簸、断电、EMC 测试中不死写一个能收发字符串的 Demo 很容易但让串口在真实车载环境中连续运行 30 天不丢包、不假死、不内存泄漏这才是难点。我们团队沉淀出一套“五层防护”机制覆盖从数据帧解析到异常恢复的全链路。4.1 第一层帧结构设计——拒绝裸奔的 ASCII 协议很多车载设备仍使用简单 ASCII 协议如ATREAD?\r\n响应READ:123\r\nOK\r\n。这种协议在实验室没问题但在车上极易崩溃\r\n可能被噪声干扰成\r\x00导致解析器卡死响应中若包含\r\n字符如固件版本号v1.2.3\r\n会被误判为帧结束无校验单比特翻转无法检测。我们的标准帧格式基于 Modbus RTU 优化字段长度说明Header2 字节固定0x55 0xAA避免与数据混淆Length1 字节后续字段总长度不含 Header 和 CRCCMD1 字节命令码如0x01读寄存器0x02写寄存器PayloadN 字节命令参数长度由 Length 字段指定CRC162 字节Modbus CRC-160xA001校验覆盖 Header 到 Payload关键设计点Header 使用0x55 0xAA而非0x00 0xFF因其在二进制流中出现概率极低统计学上随机字节序列中连续两个特定字节出现概率为 1/65536Length 字段独立于 Payload避免因 Payload 中包含0x55导致 Header 误识别CRC 计算包含 Header防止 Header 被篡改后仍通过校验。解析器伪代码fun parseFrame(buffer: ByteArray): ByteArray? { var i 0 while (i buffer.size - 1) { // 查找 Header if (buffer[i] 0x55.toByte() buffer[i 1] 0xAA.toByte()) { if (i 4 buffer.size) break // 至少需要 Header Length CMD CRC val length buffer[i 2].toInt() and 0xFF val frameEnd i 4 length 2 // Header(2) Length(1) CMD(1) Payload(length) CRC(2) if (frameEnd buffer.size) break // 校验 CRC val crc calculateCRC(buffer, i, frameEnd - 2) val crcInFrame (buffer[frameEnd - 2].toInt() and 0xFF) or ((buffer[frameEnd - 1].toInt() and 0xFF) shl 8) if (crc crcInFrame) { // 提取 Payload val payload ByteArray(length) System.arraycopy(buffer, i 4, payload, 0, length) return payload } } i } return null }4.2 第二层超时与重传——别让一次丢包拖垮整个系统串口通信没有 TCP 的 ACK 机制必须自己实现可靠性。我们采用指数退避重传 最大重试次数fun sendWithRetry(cmd: ByteArray, maxRetry: Int 3): Boolean { var retry 0 while (retry maxRetry) { try { serialPort.write(cmd) // 启动超时计时器Handler.postDelayed val timeoutRunnable Runnable { if (!responseReceived) { retry if (retry maxRetry) { // 指数退避100ms, 200ms, 400ms Handler(Looper.getMainLooper()).postDelayed({ sendWithRetry(cmd, maxRetry) }, (100L shl retry)) } else { Log.e(Serial, Command timeout after $maxRetry retries) } } } handler.postDelayed(timeoutRunnable, 1000) // 1秒超时 // 等待响应在另一个线程中 waitForResponse() } catch (e: Exception) { retry Thread.sleep((100L shl retry)) // 退避 } } return false }为什么不用Thread.sleep()阻塞因为车载 App 需响应系统事件如来电、导航语音阻塞主线程会导致 ANR。所有串口操作必须异步超时用Handler管理。4.3 第三层缓冲区管理——防止OutOfMemoryError的隐形杀手FileInputStream.read()返回实际读取字节数可能小于缓冲区长度。若每次read()都新建ByteArray高频通信下会触发频繁 GC最终 OOM。我们的解决方案是预分配固定大小环形缓冲区RingBufferclass RingBuffer(private val capacity: Int) { private val buffer ByteArray(capacity) private var head 0 private var tail 0 private var size 0 fun write(data: ByteArray) { for (b in data) { buffer[tail] b tail (tail 1) % capacity if (size capacity) size else head (head 1) % capacity } } fun readInto(dest: ByteArray, offset: Int, length: Int): Int { val actualLength minOf(length, size) for (i in 0 until actualLength) { dest[offset i] buffer[head] head (head 1) % capacity } size - actualLength return actualLength } }缓冲区大小根据通信频率设定低速传感器1Hz设为 1024 字节高速日志100Hz设为 65536 字节。实测表明相比每次new ByteArray(1024)环形缓冲区将 GC 次数降低 92%。4.4 第四层异常隔离——让串口故障不影响主业务串口通信失败不应导致整个 App 崩溃。我们采用独立 Binder Service CrashGuard创建SerialService运行在独立进程android:process:serialApp 通过 AIDL 与之通信SerialService内部持有SerialPort实例SerialService启动时用try-catch包裹所有串口操作并在onBind()中返回代理对象若SerialService崩溃系统会自动重启它App 侧只需监听ServiceConnection.onServiceDisconnected()事件重新绑定即可。这样即使串口芯片因静电损坏导致IOException频发也只是SerialService进程重启主界面和导航功能完全不受影响。4.5 第五层日志与诊断——没有日志的串口等于黑盒车载系统无法像手机 App 那样随时adb logcat。我们必须将关键日志持久化到安全位置实时日志写入/data/data/com.yourpackage/files/serial_debug.log按大小轮转1MB/个保留最近 5 个关键事件日志写入/data/misc/serial/SELinux 允许 system_server 写入包含open success,baudrate set to 115200,frame received,crc error count: 3原始数据快照当检测到连续 3 次 CRC 错误时自动保存前 1024 字节原始数据到/data/misc/serial/crash_dump_20231001_123456.bin供售后工程师用串口分析仪回放。日志格式强制包含时间戳SystemClock.elapsedRealtime()、线程 ID、错误码errno例如[123456.789][TID:1234][SERIAL] ERROR: write() failed, errno5 (EIO), device/dev/ttyS1 [123457.890][TID:1235][SERIAL] FRAME: recv len12, header0x55AA, cmd0x01, crc0x1234, expected0x5678这套日志体系让我们在一次批量召回事件中仅用 2 小时就定位到是某批次 RS485 收发器在 -30℃ 下 DE 引脚响应延迟超标而非软件 Bug。5. 实战排错从java.io.IOException: Device or resource busy到量产交付的完整链路最后分享一个真实案例某款新能源汽车的电池管理系统BMS通过 RS485 向车机上报 SOC剩余电量但 OTA 升级后SOC 显示卡在 87% 不动且logcat持续刷java.io.IOException: Device or resource busy。5.1 现象还原与初步定位adb shell ls -l /dev/ttyS2显示crw-rw---- 1 root dialout 204, 2 2023-09-01 10:00 ttyS2权限正常adb shell stty -F /dev/ttyS2返回speed 115200 baud; ...说明串口参数已设置adb shell cat /dev/ttyS2无输出但echo test /dev/ttyS2无报错证明写入通道通畅dmesg | grep ttyS2无异常cat /proc/tty/drivers显示msm_serial正常加载。此时Device or resource busy的字面意思——设备忙——显然不成立因为cat和echo都能操作。问题一定出在更高层。5.2 深入内核fuser与lsof揭露真相执行adb shell su -c fuser -v /dev/ttyS2输出USER PID ACCESS COMMAND /dev/ttyS2: root 1234 f.... seriald root 1235 f.... bms_service两个进程都在占用/dev/ttyS2进一步adb shell su -c ps -p 1234,1235 -o pid,tid,comm,argsPID TID COMM ARGS 1234 1234 seriald /system/bin/seriald -d /dev/ttyS2 1235 1235 bms_ser... /system/bin/bms_service原来OEM 在 OTA 包中新增了一个名为seriald的守护进程它在系统启动时就open(/dev/ttyS2)并保持 fd 打开用于向另一路设备如空调控制器发送指令。而我们的 BMS 服务也尝试open(/dev/ttyS2)Linux 内核返回EBUSY资源忙Java 层抛出IOException。5.3 根本原因串口复用冲突与 OEM 的“隐藏功能”查阅seriald的源码OEM 提供的符号表发现它使用O_EXCL标志打开设备int fd open(/dev/ttyS2, O_RDWR | O_EXCL);O_EXCL表示“独占打开”若设备已被其他进程打开则open()失败。而我们的 App 使用O_RDWR未加O_EXCL按理说应能共享。但问题在于seriald在open()后调用了ioctl(fd, TIOCEXCL)该 ioctl 会为该 fd 设置独占锁后续任何open()无论是否带O_EXCL都会失败。这是 Linux tty 子系统的特性TIOCEXCL锁是 fd 级别的且不可解除只能等seriald进程退出或close(fd)。5.4 解决方案协商、绕行与妥协我们提出三个方案最终选择第三个要求 OEM 修改seriald移除TIOCEXCL或改为O_NONBLOCK。被否决因seriald是 OTA 升级包的一部分修改需全量重刷成本过高申请新增 UART 通道OEM 提供ttyS3给 BMS 使用。被否决因硬件 PCB 未预留ttyS3的引脚需改板复用seriald的 IPC 通道与seriald开发者对接约定一个统一的 IPC 协议如 Unix Domain SocketBMS 服务不再直接操作/dev/ttyS2而是向seriald发送 JSON 指令由seriald统一调度串口资源。我们实现了方案 3seriald新增 socket 服务/dev/socket/serialdBMS 服务通过LocalSocket连接发送{cmd:read_bms,addr:0x01}seriald解析后构造 RS485 帧通过/dev/ttyS2发送并将响应通过 socket 回传。虽然增加了 IPC 开销约 0.5ms 延迟但彻底解决了资源冲突且符合 OEM 的“集中管控”架构理念。该方案已随 OTA 推送到 50 万辆车。5.5 经验总结车载串口开发