1. 为什么要在Android上用Qt搞串口一个真实开发者的困境如果你和我一样是个经常用Qt做跨平台应用开发的“老手”肯定对QSerialPort这个类不陌生。在Windows、Linux甚至macOS上用它来操作串口设备比如连接个单片机、读个传感器数据那叫一个方便。几行代码配置好波特率、数据位打开端口就能读写Qt把底层的脏活累活都干了。但当你信心满满地把这套代码搬到Android平台上准备在手机或平板上连接一个USB转串口模块比如最常见的CH340、CP2102这些时噩梦就开始了。你会发现在低版本的Android比如API 23以下上可能还能凑合用但一旦你的应用目标API版本设高一点比如现在Google Play要求的最低API 24或者你想用上Android 10、11的新特性QSerialPort直接就给你摆个臭脸——Permission denied。我当初就踩了这个大坑。项目急着要一个Android端的调试工具用来通过USB转串口和我们的硬件设备通信。我心想Qt不是跨平台吗直接把桌面端的串口代码搬过来不就完了结果编译安装到手机上一点“打开串口”应用就直接闪退日志里赫然写着权限拒绝。查了一圈才发现问题根源在于Android系统的权限模型收紧。对于USB设备的访问特别是像串口这种需要直接操作底层端口的从某个高API版本开始不再允许应用通过简单的文件路径比如/dev/ttyUSB0直接访问而是必须通过Android系统提供的USB Host API并且要动态申请用户授权。这就尴尬了。Qt for Android自带的QSerialPort后端在实现上可能没有完全跟上Android这些高版本的安全策略变化导致它在高API环境下直接“罢工”。那怎么办难道要放弃Qt转头去写原生Android Java代码吗那之前用C和Qt写的核心业务逻辑怎么办重写成本太高了。这时候我的思路转向了寻找一个更底层的、能绕过这个限制的解决方案。最终我找到了libusb这个老牌、强大的跨平台USB库并把它和Qt for Android结合成功驱动了CH340x芯片实现了稳定高效的串口通信。这条路走通了而且我发现它比想象中更灵活、更强大。下面我就把整个“踩坑”和“填坑”的过程以及优化心得详细分享给你。2. 方案核心为什么是libusbQt面对Android高版本的权限墙我们有几个潜在的突围方向。第一种是老老实实走Android官方路线用Java写USB Host的代码然后通过JNI和我们的Qt C核心交互。这个方法绝对合规但缺点也很明显你需要维护Java和C两套代码JNI的调用既繁琐又容易出错调试起来更是头疼。第二种就是寻找一个纯C/C的USB库它能直接操作USB设备并且最好在Android上也能用。这样我们就能在Qt的C环境里一气呵成不用跟JNI打太多交道。libusb就是这个“最好”的选择。它是一个用C语言编写的、用户态的、跨平台的USB设备访问库。在Linux上它大名鼎鼎很多USB驱动都基于它。关键是它也被成功移植到了Android平台。这意味着我们可以在Android的NDK环境下编译和使用libusb。它的工作原理是在获取了Android系统授予的USB设备访问权限和文件描述符fd之后直接通过这个fd来初始化libusb后续所有的USB控制传输、批量传输等操作都由libusb这个纯C库来完成完全在Native层运行效率非常高。那么Qt在这里扮演什么角色呢Qt是我们的应用框架和UI框架。我们用它来构建漂亮的界面、处理用户交互、管理业务逻辑。而底层的USB通信这个“苦力活”则交给libusb。Qt和libusb的结合点就在于那个关键的“文件描述符fd”。我们需要用一点点的JNI代码或者Qt的JNI封装如QJniObject从Android系统那里拿到USB设备的连接UsbDeviceConnection和对应的fd然后把这个fd“喂”给libusb。一旦libusb初始化成功后面的事情就全是C的世界了非常清爽。对于CH340x这个系列芯片CH340, CH341等它是国内非常常见且廉价的USB转串口芯片大量出现在Arduino、各种开发板、调试器中。它的通信协议是公开的厂商提供了文档本质上就是通过USB的控制传输Control Transfer和批量传输Bulk Transfer来模拟串口的各项操作设置波特率、数据位、停止位、流控以及收发数据。因此用libusb来实现对CH340x的驱动是完全可行的我们只需要按照它的协议文档发送正确的控制请求即可。3. 手把手搭建开发环境理论说再多不如动手搭环境。这里我假设你已经有一个可以正常编译Qt for Android应用的基础环境了包括Qt Creator、Android SDK、NDK、JDK。如果没有你需要先去Qt官网看看配置教程那部分内容比较常规这里就不展开了。我们聚焦在让libusb跑起来所需的额外步骤。3.1 获取并编译Android版的libusb首先你需要libusb的源代码。推荐直接从官方GitHub仓库获取https://github.com/libusb/libusb。我们需要为Android交叉编译它。下载源码你可以直接下载zip包或者用git克隆。准备编译脚本libusb官方并不直接提供Android的CMake或Makefile。最可靠的方式是使用Android NDK的ndk-build系统。在libusb源码的android目录下通常会有个jni目录和对应的Android.mk文件。如果没有你可能需要自己编写一个或者从网上找一份验证过的。这里我提供一个最简化的思路在你的Qt项目同级目录创建一个libusb文件夹把源码放进去。在libusb文件夹内创建jni文件夹里面放入Android.mk和Application.mk。Android.mk负责告诉NDK如何编译libusb的C文件。Application.mk则指定目标平台架构比如APP_ABI : armeabi-v7a arm64-v8a x86 x86_64和使用的C标准库。一个极其简化的Android.mk示例你需要根据实际源码路径调整LOCAL_PATH : $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE : libusb LOCAL_SRC_FILES : ../libusb/core.c ../libusb/descriptor.c ../libusb/hotplug.c ../libusb/io.c ../libusb/strerror.c ../libusb/sync.c ../libusb/os/linux_usbfs.c # 列举所有需要的源文件 LOCAL_C_INCLUDES : $(LOCAL_PATH)/../libusb $(LOCAL_PATH)/../libusb/os LOCAL_CFLAGS : -DHAVE_CONFIG_H -DLIBUSB_API_VERSION0x01000100 include $(BUILD_STATIC_LIBRARY) # 或者 BUILD_SHARED_LIBRARY我更推荐编译成静态库BUILD_STATIC_LIBRARY这样最后链接进你的Qt SO库时比较方便不需要单独部署libusb的so文件。执行编译打开终端进入libusb/jni目录执行你的NDK路径下的ndk-build命令例如/path/to/your/ndk/ndk-build。编译成功后你会在libs目录下找到对应架构的libusb.a静态库文件。3.2 将libusb集成到Qt项目中编译好libusb的静态库后下一步就是让Qt项目能找到并使用它。组织项目目录在你的Qt项目目录下我习惯创建一个thirdparty文件夹里面再建一个libusb文件夹。把上一步编译好的libusb.a库文件以及libusb的所有头文件主要是libusb.h和os目录下的平台相关头文件拷贝到这个thirdparty/libusb目录下。结构类似这样你的Qt项目/ ├── yourproject.pro ├── ... (其他源码文件) └── thirdparty/ └── libusb/ ├── include/ │ ├── libusb.h │ └── ... (其他头文件) └── lib/ ├── armeabi-v7a/ │ └── libusb.a ├── arm64-v8a/ │ └── libusb.a └── ... (其他架构)配置Qt项目文件.pro这是关键一步。你需要修改你的.pro文件告诉Qt编译器去哪里找头文件和库文件并且链接libusb库。# 根据目标Android架构选择对应的库路径 ANDROID_ABIS armeabi-v7a arm64-v8a # 包含libusb头文件 INCLUDEPATH $$PWD/thirdparty/libusb/include # 为每种ABI指定库文件路径 for(abi, ANDROID_ABIS) { contains(ANDROID_TARGET_ARCH, $$abi) { LIBS -L$$PWD/thirdparty/libusb/lib/$$abi break } } # 链接libusb静态库 LIBS -lusb # 如果你的libusb编译时用了某些特定的宏可能还需要全局定义 # DEFINES HAVE_CONFIG_H注意ANDROID_TARGET_ARCH是Qt构建时根据你的Kit设置自动定义的变量。上面的for循环确保了在构建不同架构的APK时能链接到对应架构的libusb.a。验证包含在你的C代码中尝试包含#include libusb.h如果编译不报错说明头文件路径设置正确。链接阶段如果没有出现undefined reference to libusb_xxx这类错误说明库链接也成功了。环境搭建至此完成。4. 核心代码实现与逐行解析环境搞定我们来啃最硬的骨头——代码。我会结合原始文章中的代码进行更详细的解释和补充让你不仅知道怎么写更明白为什么这么写。4.1 封装CH340x设备类首先我们需要一个C类来封装对CH340x设备的操作。这个类负责用libusb与设备对话。原始文章给出了一个很好的骨架我们在此基础上丰富。头文件 (ch340x.h)的设计思路 这个头文件定义了类的接口。它包含了libusb头文件并定义了一系列枚举数据位、停止位、校验位等这些枚举和Qt的QSerialPort里的概念是对应的方便我们理解和使用。类的核心私有成员是一个libusb_device_handle *这是libusb操作设备的句柄。构造函数接受一个文件描述符fd析构函数负责释放资源。isValid()方法用来判断设备是否初始化成功。最核心的公共方法可能就是send()用于发送数据当然我们还可以扩展出read()。其他所有controlOut,setBaudRate,setParameters等方法都是私有的是驱动芯片的内部协议实现。重点解析构造函数和初始化 (ch340x.cpp中的init_ch34x) 这是整个驱动成功与否的关键。它接收一个从AndroidUsbDeviceConnection.getFileDescriptor()获得的整型文件描述符。初始化libusb上下文libusb_init(ctx)。但这里有个Android上的特殊操作libusb_set_option(NULL, LIBUSB_OPTION_NO_DEVICE_DISCOVERY, NULL)。这个调用非常关键在标准的Linux桌面系统上libusb会自己扫描USB总线枚举设备。但在Android上设备枚举和权限管理是由Android系统牢牢掌控的。我们应用只能访问已经被系统授权、并且已经通过openDevice打开的设备。这个选项告诉libusb“别自己去枚举设备了我直接给你一个已经打开的设备的句柄。” 如果不设置这个选项libusb可能会在初始化时尝试扫描总线导致权限问题或崩溃。包装文件描述符libusb_wrap_sys_device(ctx, (intptr_t)fd, devh)。这是libusb 1.0.26及以上版本提供的一个超级重要的函数。它直接将一个系统级的文件描述符fd“包装”成libusb能识别的设备句柄devh。这样我们就跳过了libusb自己寻找设备的过程直接复用Android系统已经建立好的连接。这是整个方案能行的技术基石。声明接口libusb_claim_interface(devh, 0)。CH340x这类设备通常只有一个接口Interface 0。这个调用告诉系统“这个接口我占用了别的程序别来抢。”对于串口设备这是必要的。发送芯片初始化序列后面一连串的controlOut和checkState调用就是根据CH340x芯片的厂商文档发送特定的控制请求Vendor Specific Control Transfer来配置芯片。例如controlOut(0xa1, 0, 0)setBaudRate(DEFAULT_BAUD_RATE)controlOut(0x9a, 0x2518, lcr)等。这些魔术数字0xa1, 0x9a, 0x2518是芯片规定的命令和寄存器地址用来复位芯片、设置波特率发生器、设置线路控制参数数据位、停止位、校验位等。这部分代码最需要参考芯片的官方文档或稳定的开源实现比如Linux内核驱动或安卓驱动不能自己想当然。设置控制线setControlLines()函数用于设置DTR、RTS这类硬件流控信号线。虽然很多简单场景用不上但一个完整的驱动应该支持。数据发送send方法 初始化成功后发送数据就很简单了。直接调用libusb_bulk_transfer(devh, EP_DATA_OUT, src, length, realSendLength, timeout)。这里EP_DATA_OUT是批量输出端点地址对于CH340x通常是0x02。这个函数是同步的会阻塞直到发送完成或超时。realSendLength会返回实际发送的字节数。4.2 在Qt中获取Android USB权限和设备fd光有C的驱动类还不够我们得在Qt或者说Android环境下拿到进入USB世界的“门票”——设备权限和文件描述符。这部分代码写在Qt的C里但大量使用了Qt对JNI的封装QJniObject使得调用Android Java API不那么痛苦。列出所有USB设备(getSerialPorts函数) 这个函数的目标是模仿QSerialPortInfo::availablePorts()的行为返回一个可用的“串口”设备名列表。在Android上所谓的“串口名”其实就是USB设备的设备名UsbDevice.getDeviceName()或者我们可以自己组合VID/PID。通过QNativeInterface::QAndroidApplication::context()获取Android的Context活动上下文。调用getSystemService(“usb”)获取UsbManager对象。调用UsbManager.getDeviceList()这会返回一个HashMapString, UsbDevice包含了当前连接到设备的所有USB设备。遍历这个HashMap取出每个UsbDevice获取其厂商IDVID、产品IDPID和设备名。这里我们可以根据VID/PID比如CH340x的VID是0x1a86来过滤出我们关心的设备并把设备名加入返回列表。请求设备权限和打开连接(test函数的一部分) 用户从UI下拉框选中一个设备名后我们需要操作它。请求权限在Android上即使设备已连接应用也需要显式请求用户授权才能访问。这里原始文章调用了一个Java静态方法requestUSBPermission。这个方法的内部实现就是创建一个UsbManager.requestPermission(device, pendingIntent)的调用。这是一个必须的步骤用户会在屏幕上看到一个系统弹窗询问是否允许应用访问该USB设备。只有用户点击允许后续操作才能进行。这个权限请求最好用Android的Activity或Dialog来触发原始文章将其封装在Java类USBListActivity中通过JNI调用是合理的做法。打开设备权限授予后调用UsbManager.openDevice(usbDevice)得到一个UsbDeviceConnection对象。这个对象代表了你的应用和USB设备之间的一条开放连接。获取文件描述符调用UsbDeviceConnection.getFileDescriptor()得到一个整型的文件描述符fd。这个fd就是通往设备的核心钥匙传递给驱动类将这个fd传递给我们的CH340X类的构造函数。构造函数内部的init_ch34x函数会利用这个fd通过libusb_wrap_sys_device将其转化为libusb句柄并完成芯片初始化。至此从Qt UI选择设备到获取权限再到底层libusb驱动初始化整个链条就打通了。你可以看到除了权限请求那一步需要一点Java/JNI桥接外其他所有逻辑都在C/Qt中完成保持了代码的纯粹性和高性能。5. 性能优化与实战调试技巧代码跑通只是第一步要让它稳定、高效地在实际项目中使用还需要一些优化和调试技巧。5.1 异步操作与避免UI阻塞libusb_bulk_transfer默认是同步阻塞的。如果你在主线程UI线程中发送一个很长的数据包或者等待接收数据界面就会卡住用户体验极差在Android上甚至可能触发ANR应用无响应。解决方案是使用异步传输和单独的工作线程。使用libusb的异步APIlibusb提供了一组libusb_submit_transfer,libusb_handle_events等异步函数。你可以创建一个libusb_transfer结构体填充好数据缓冲区和回调函数然后提交它。提交后函数立即返回传输在后台进行完成后会调用你设置的回调函数。这需要你管理好传输的生命周期和线程安全。更Qt风格的做法使用QThread和Qt信号槽。我更喜欢这种方式因为它和Qt框架集成得更好。创建一个继承自QObject的UsbWorker类。将CH340X对象移到这个UsbWorker中。在UsbWorker内部使用一个QTimer或者在一个循环中定期调用libusb_handle_events_timeout_completed来处理异步传输完成事件如果你用异步API或者直接在一个循环里进行阻塞式的libusb_bulk_transfer读操作。这个UsbWorker对象通过moveToThread方法移到一个专门的QThread线程中运行。当UI线程需要发送数据时通过信号槽机制发射一个信号到UsbWorker所在的线程由工作线程执行发送操作发送完成后可以发射另一个信号通知UI线程。当工作线程收到数据时也通过信号将数据传递回UI线程进行显示。这样所有耗时的USB操作都在后台线程UI线程始终保持流畅响应。5.2 错误处理与设备热插拔USB设备可能会被意外拔出或者通信出错。健壮的代码必须处理这些情况。检查返回值每一个libusb函数调用libusb_init,libusb_wrap_sys_device,libusb_bulk_transfer等都必须检查返回值。libusb定义了丰富的错误码LIBUSB_SUCCESS,LIBUSB_ERROR_IO,LIBUSB_ERROR_NO_DEVICE等。根据不同的错误码进行相应的处理比如重试、通知用户、或者清理资源。设备断开检测在异步操作模式下传输完成的回调函数里会收到状态。如果状态是LIBUSB_TRANSFER_NO_DEVICE基本可以断定设备已断开。在同步模式下libusb_bulk_transfer会返回LIBUSB_ERROR_NO_DEVICE。一旦检测到设备断开应该立即关闭libusb句柄并更新UI状态。资源清理在析构函数或设备断开处理中务必按顺序正确释放资源先取消可能未完成的异步传输 (libusb_cancel_transfer)然后释放传输结构体 (libusb_free_transfer)接着释放接口 (libusb_release_interface)关闭设备句柄 (libusb_close)最后退出libusb上下文 (libusb_exit)。顺序错了可能导致内存泄漏或崩溃。5.3 调试与日志输出在Android上调试Native代码有点麻烦但打好日志能解决大部分问题。使用qDebug()和qCritical()在C代码的关键步骤初始化成功/失败、发送/接收数据前后、错误发生时大量使用Qt的qDebug()输出信息。这些日志会在Android的logcat中打印出来。你可以通过Android Studio的Logcat窗口或者adb logcat命令查看。区分日志标签在qDebug()时可以带上特定的标签方便过滤。例如qDebug(“[CH340X] Init failed with libusb error: %d”, r);。然后在adb logcat中使用adb logcat -s “CH340X”来只看你的驱动日志。检查Android权限除了USB设备权限别忘了在AndroidManifest.xml文件中添加必要的特性声明uses-feature android:name”android.hardware.usb.host” /。如果你的应用需要兼容没有USB Host功能的设备比如一些老手机可以加上android:required”false”。同时如果你在Java代码中请求权限也需要在Manifest里声明uses-permission android:name”android.permission.USB_PERMISSION” /。6. 超越CH340x方案的通用性思考我们花了很大篇幅讨论CH340x但这个方案的价值远不止于此。libusb Qt for Android是一个通用的、强大的USB设备访问框架。支持其他USB转串口芯片CP2102、FT232、PL2303等常见芯片虽然它们的控制协议Vendor Command和CH340x不同但架构完全一样。你只需要把ch340x.cpp中那些芯片特定的初始化序列、波特率设置命令setBaudRate、参数设置命令setParameters替换成对应芯片的协议即可。libusb提供控制传输和批量传输的通用接口足以应对所有这些芯片。访问自定义的USB设备如果你的硬件不是简单的串口而是一个具有自定义功能的USB设备比如特定的HID设备、大容量存储设备、或者完全自定义协议的设备这个方案同样适用。你不需要拘泥于“串口”的思维。你可以用libusb直接与设备的各个端点Endpoint通信实现任何你想要的USB协议。Qt负责提供漂亮的配置界面和数据处理逻辑libusb负责底层通信。性能优势相比于通过Android Java层进行USB通信纯Native的libusb调用减少了JNI的上下文切换开销对于需要高速、实时数据交换的应用比如高速数据采集、视频传输有显著的性能优势。所以当你成功实现了CH340x的驱动后你手里握着的实际上是一把打开Android平台USB外设开发大门的钥匙。你可以用同样的技术栈去驱动各种各样的USB设备将手机或平板变成一个功能强大的便携式工业控制终端、数据监控仪或者调试工具。这种将复杂底层操作封装在优雅的Qt C框架内的体验既能保证性能又能提升开发效率正是这个方案最吸引我的地方。在实际项目中我根据这个核心思路已经成功对接了不止三种不同的USB设备稳定性都经过了长时间运行的考验。