当前位置: 首页 > news >正文

《HarmonyOS技术精讲》四:驱动开发入门 ── 标准外设与非标USB串口

为什么需要自己写驱动

在很多HarmonyOS NEXT的应用场景中,我们不只是开发一个App,而是要跟硬件打交道。比如做个工业巡检助手,需要连接一个自定义的红外测温仪;或者做一个智能家居中枢,需要控制非标准的USB灯控设备。

这时候你会发现,系统自带的HID、SCSI驱动只管得了键鼠、U盘这种标准设备。一旦遇到非标USB串口设备,或者需要精细控制HID协议(比如模拟键盘输入),就不得不进入驱动开发这个领域。

HarmonyOS的Driver Development Kit(DDK)就是为这个场景设计的。它不像Linux内核驱动那么底层难啃,但又有别于应用层API的简单调用。这篇文章会侧重讲清楚DriverExtensionAbility的生命周期、设备驱动的注册流程,以及HID和SCSI两种标准协议的结构,最后带出一段非标USB串口驱动的读写实战代码。

DDK 解决的核心问题

DDK归根结底要做的事情只有一件:让用户态程序能够直接管理一个外设设备

传统HarmonyOS应用开发中,你只能通过系统提供的API去操作外设,能做什么、不能做什么,全看系统封装了多少。但DDK让你能编写一个"驱动扩展"(DriverExtensionAbility),加载到系统里,直接和内核态的硬件设备通信。

这个能力主要面向:

场景说明适用协议
标准HID外设如自定义键盘、游戏手柄,需要控制报告描述符HID
标准大容量存储如特殊格式的U盘、读卡器,要控制命令集SCSI
非标USB串口设备如工业传感器、自定义USB到串口转换器自定义

不推荐用DDK的场景:如果系统自带的API(如@ohos.multimodalAwareness.kit)已经封装了设备状态感知能力,不要自己造轮子。

环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上 目标设备:基于本机开发的HarmonyOS NEXT真机设备(建议使用有物理USB接口的平板或开发板)

DriverExtensionAbility 生命周期

DDK驱动扩展的核心是DriverExtensionAbility。它是HarmonyOS扩展机制的一部分,不是普通组件。生命周期只有三个函数。

// DriverExtensionIndex.tsimport{DriverExtensionAbility,driver}from'@kit.DriverKit';exportdefaultclassMyUsbDriverExtensionextendsDriverExtensionAbility{onInit(want:Want){// 驱动初始化,这里不能做耗时操作console.log('DriverExtension onInit called');}onRelease(){// 驱动释放,清理资源console.log('DriverExtension onRelease called');}onConnect(want:Want){// 当有应用连接到本驱动时触发// 返回一个IBinder用于应用层通信returnnewMyDriverBinder();}}

这里的关键点:

  • onInit:只在驱动进程第一次创建时调用一次,适合做资源预分配,但不要在这里注册设备。因为此时设备可能还没连上。
  • onConnect:这是返回给应用层的通信通道,应用层通过driver.connectDriverExtension拿到这个IBinder,然后才能进行数据收发。
  • onRelease:进程销毁前调用,必须在这里释放所有设备资源。否则下次驱动加载时设备会处于脏状态。

标准外设之一:HID键盘驱动(模拟输入)

HID协议的核心是报告描述符(Report Descriptor)。它告诉系统:这个设备能做什么,数据格式是什么。

模拟一个键盘,我们需要构造的报告描述符大致描述:这是一个键盘设备,有8个按键同时按下能力,按键值使用标准USB HID键码。

// 构造的HID报告描述符数据staticconstuint8_thidReportDescriptor[]={0x05,0x01,// Usage Page (Generic Desktop)0x09,0x06,// Usage (Keyboard)0xA1,0x01,// Collection (Application)0x05,0x07,// Usage Page (Keyboard/Keypad)0x19,0xE0,// Usage Minimum (224)0x29,0xE7,// Usage Maximum (231)0x15,0x00,// Logical Minimum (0)0x25,0x01,// Logical Maximum (1)0x75,0x01,// Report Size (1)0x95,0x08,// Report Count (8)0x81,0x02,// Input (Data,Var,Abs)0x95,0x01,// Report Count (1)0x75,0x08,// Report Size (8)0x81,0x01,// Input (Const,Array,Abs)// ... 省略按键数组部分,完整版约50字节0xC0// End Collection};

在驱动注册时,把这份描述符注册进去:

// 假设在onConnect里面进行设备绑定onConnect(want:Want):rpc.RemoteObject{// 获取USB设备端点letusbDevice=/* 从want参数中解析 */;letinterface=usbDevice.interfaces[0];letinEndpoint=interface.endpoints[0];// 输入端点letoutEndpoint=interface.endpoints[1];// 输出端点(键盘不需要输出)// 1. 声明当前驱动可以处理的USB设备(VID/PID匹配)letdeviceDescriptor=newdriver.DeviceDescriptor();deviceDescriptor.vendorId=0x1234;// 假设的设备VIDdeviceDescriptor.productId=0x5678;deviceDescriptor.probingMode=0;// 自动匹配// 2. 为设备创建HID适配器lethidAdapter=newdriver.HidAdapter(usbDevice);// 注册报告描述符hidAdapter.registerHidReportDesc(hidReportDescriptor);// 设置设备通信超时hidAdapter.setTimeout(1000);// 返回用于应用通信的BinderreturnnewDriverBinderImpl(hidAdapter);}

这一段的要点:HID报告描述符是整个驱动的心脏。如果描述符写错,系统要么识别不出设备,要么报告数据解析完全错乱。官方文档也提到了标准HID描述符的结构,但实际在ArkTS中构造字节数组变量比较繁琐,建议参考USB-IF官方HID规范。

标准外设之二:SCSI设备的CDB命令

对于U盘、读卡器这类SCSI设备,驱动开发的核心是CDB命令。比如要读取设备信息,需要发送INQUIRY命令。

// 发送SCSI INQUIRY命令functionsendInquiryCommand(scsiAdapter:driver.ScsiAdapter):Uint8Array{// 构造CDB命令块letcdb=newUint8Array(6);// 6字节CDBcdb[0]=0x12;// INQUIRY操作码cdb[1]=0x00;// obsoletecdb[2]=0x00;// page codecdb[3]=0x00;// allocation length high bytecdb[4]=0x24;// allocation length low byte (36 bytes)cdb[5]=0x00;// controlletdataBuffer=newArrayBuffer(36);// 发送命令,读取返回数据letresult=scsiAdapter.sendCommand(cdb,dataBuffer);if(result!==0){console.error('SCSI command failed');returnnewUint8Array(0);}returnnewUint8Array(dataBuffer);}

SCSI驱动相比HID更严格:命令顺序不能乱。在正式读取数据之前,必须有INQUIRY→READ CAPACITY→READ(10)这样的顺序。如果不按这个顺序,设备会返回check condition错误。

核心实战:非标USB串口设备驱动

标准外设都有现成的协议和适配器,HID和SCSI都有专有类。但自定义USB串口设备(通常基于CP2102、FT232、CH34X等芯片)就不一样了。它们通常实现为标准CDC ACM设备,或者直接走Bulk端点传输。

驱动注册

假设我们有一个自定义串口设备,它的连接方式是:从应用层收到数据包(格式自己定),然后通过USB Bulk端点发给设备。驱动代码核心在onConnect中注册设备并返回Binder。

// CustomSerialDriverExtension.tsimport{DriverExtensionAbility,driver,common}from'@kit.DriverKit';import{rpc}from'@kit.IPCKit';// 自定义Binder实现classCustomSerialBinderextendsrpc.RemoteObject{privatedriverExt:CustomSerialDriverExtension;constructor(ext:CustomSerialDriverExtension){super('CustomSerialBinder');this.driverExt=ext;}onRemoteRequest(code:number,data:rpc.MessageParcel,reply:rpc.MessageParcel,option:rpc.IRemoteObject):boolean{if(code===1){// 打开设备this.driverExt.openDevice();reply.writeInt(0);returntrue;}elseif(code===2){// 写数据,数据从data中读取letbuffer=data.readByteArray();letret=this.driverExt.serialWrite(buffer);reply.writeInt(ret);returntrue;}elseif(code===3){// 读数据letresult=this.driverExt.serialRead();reply.writeByteArray(result);returntrue;}returnfalse;}}exportdefaultclassCustomSerialDriverExtensionextendsDriverExtensionAbility{privateusbDevice:driver.UsbDevice|null=null;privatebulkInEndpoint:driver.UsbEndpoint|null=null;privatebulkOutEndpoint:driver.UsbEndpoint|null=null;privateusbIo:driver.UsbIo|null=null;onInit(want:Want){// 参数校验if(!want.parameters){return;}// 从want中提取USB设备信息letdeviceHandle=want.parameters['usb-device-handle'];// 详细获取UsbDevice过程略}onConnect(want:Want):rpc.RemoteObject{// 假设已经拿到了usbDevice对象// 这里简单演示如何声明端点this.usbDevice=/*...*/;// 选取第一个接口的Bulk端点letiface=this.usbDevice.interfaces[0];for(letepofiface.endpoints){if(ep.type===2){// Bulk类型if(ep.direction===0){this.bulkInEndpoint=ep;}else{this.bulkOutEndpoint=ep;}}}// 初始化USB IOthis.usbIo=newdriver.UsbIo(this.usbDevice);// 声明独占使用权this.usbIo.claimInterface(0,true);console.log('Serial driver connected');returnnewCustomSerialBinder(this);}openDevice():void{// 发送握手包或初始化命令lethandshake=newUint8Array([0xAA,0x01,0x00,0x00,0x55]);this.bulkWrite(handshake);}serialWrite(data:Uint8Array):number{if(!this.usbIo||!this.bulkOutEndpoint)return-1;// 通过Bulk端点发送returnthis.usbIo.bulkTransfer(this.bulkOutEndpoint,data.buffer,1000);}serialRead():Uint8Array{if(!this.usbIo||!this.bulkInEndpoint)returnnewUint8Array();letlength=64;// 假设每次读64字节letbuffer=newArrayBuffer(length);letret=this.usbIo.bulkTransfer(this.bulkInEndpoint,buffer,1000);if(ret>0){returnnewUint8Array(buffer.slice(0,ret));}returnnewUint8Array(0);}privatebulkWrite(buffer:Uint8Array):number{returnthis.usbIo.bulkTransfer(this.bulkOutEndpoint,buffer.buffer,1000);}onRelease(){// 释放USB接口if(this.usbIo){this.usbIo.releaseInterface(0);this.usbIo=null;}console.log('Serial driver released');}}

驱动配置文件

注册驱动扩展需要在module.json5中添加配置:

{ "module": { // ... "extensionAbilities": [ { "name": "CustomSerialDriver", "srcEntry": "./ets/DriverExtension/CustomSerialDriverExtension.ts", "description": "Custom USB Serial Driver", "type": "driver", "exported": true, "metadata": [ { "name": "driver-usb-config", "value": "{\"vendorId\":\"1234\",\"productId\":\"5678\"}" } ] } ] } }

应用层调用

应用层通过driver.connectDriverExtension拿到Binder,然后通过IPC调用驱动函数:

import{driver,common}from'@kit.DriverKit';import{rpc}from'@kit.IPCKit';asyncfunctiontestSerialDriver(){try{// 连接驱动扩展constremoteObj:rpc.IRemoteObject=awaitdriver.connectDriverExtension('com.example.myapp/CustomSerialDriver');constoption=newrpc.MessageOption();constdata=rpc.MessageParcel.create();constreply=rpc.MessageParcel.create();// 打开设备remoteObj.sendMessageRequest(1,data,reply,option);console.log('Open result: '+reply.readInt());// 写数据data.writeByteArray(newUint8Array([0x01,0x02,0x03,0x04]));remoteObj.sendMessageRequest(2,data,reply,option);console.log('Write size: '+reply.readInt());// 读数据remoteObj.sendMessageRequest(3,data,reply,option);letbuffer=reply.readByteArray();console.log('Read data: '+buffer);data.reclaim();reply.reclaim();}catch(err){console.error('connect driver failed: '+JSON.stringify(err));}}


常见问题

问题1:onConnect返回Binder后,应用层无法调用

现象:应用层connectDriverExtension返回了对象,但发送消息请求时崩溃。

原因:Binder的onRemoteRequest中,如果代码(code)从0开始,会被系统保留。应用层发送请求的code必须从1开始。这是一个HarmonyOS IPC的隐藏约束。

解决:所有自定义请求代码从1开始编号。

问题2:驱动加载后无法屏蔽系统默认驱动

现象:在module.json5中声明了vendorIdproductId,但插入设备后系统默认驱动(如通用HID驱动)还是先加载了,导致自定义驱动不生效。

原因:HarmonyOS的USB驱动匹配机制基于优先级。系统内置驱动的优先级高于用户扩展。需要修改配置使优先级高于默认值。

解决:在deviceDescriptor.probingMode中设置优先级,或者在设备插入前动态声明驱动。

deviceDescriptor.probingMode=1;// 高于默认驱动

如果还是不行,需要在系统侧预先过滤默认驱动,这涉及系统级配置。

问题3:多次插拔设备后驱动不响应

现象:设备第一次插入正常工作,拔出再插入后就无法连接。

原因:驱动进程销毁后,USB设备节点没有完全释放。再次插拔时系统认为设备还被人占用。

解决:在onRelease中除了releaseInterface,还需要调用driver.removeDriverExtension彻底清理。同时,应用层最好监听设备插拔事件,在设备拔出时主动断开与驱动的连接。

最佳实践

  1. 不要在onInit中做设备注册。onInit只做进程级初始化,设备相关的逻辑应该放在onConnect中,因为此时应用层才开始与驱动交互。
  2. Binder请求尽量异步化。串口读写如果是长帧数据,可能会阻塞Binder线程。建议在驱动内部使用异步队列,然后通过回调通知应用层。
  3. 设备匹配使用精确的VID/PID。如果在module.json5中填写的vendorId过于宽泛(如0x0001),会匹配到太多设备,导致驱动加载冲突。对于非标设备,尽量使用特定VID和PID组合。

Demo 入口

@Entry@Componentstruct DriverToolHome{build(){Column(){Button('连接串口驱动的Binder').onClick(()=>{testSerialDriver();})}.width('100%').height('100%')}}

FAQ

Q:为什么HID驱动要构造报告描述符,而SCSI驱动只需要发命令?

A:HID协议要求设备端主动描述自己,系统根据描述符解析输入数据。SCSI协议则是主从模式,主机主动发命令,设备被动响应。二者协议架构不同。

Q:自定义USB串口驱动可以不写module.json5配置,直接从代码中创建设备吗?

A:不行。驱动扩展必须在模块配置中声明,否则系统不会把你的DriverExtensionAbility视为合法驱动扩展。module.json5中的metadata是驱动匹配的依据。

Q:Binder通信效率如何?适合高频读写吗?

A:Binder本身是原子化IPC,单次调用有一定的开销(约0.1ms)。对于工业传感器(频率几十Hz)完全够用。如果需要更高速率(如视频流),可以考虑使用共享内存或者mmap。但DDK当前版本不太建议高频读写,建议走厂商提供的独立驱动。

如果你也在做HarmonyOS外设驱动,重点检查驱动扩展的生命周期管理和USB接口正确释放。系统级USB驱动竞争问题比较多,建议先在小范围设备上验证匹配逻辑,再推广到全量设备。

http://www.jsqmd.com/news/927615/

相关文章:

  • [特殊字符]️ Agent零信任:Anthropic给企业AI安全画了一张新地图(设计测试 + 最小代理 + Agentic SOAR)
  • 从SEO到AIO:泉州本地企业如何应对生成式搜索带来的流量重构
  • 我花了6年写了14000行Go代码,给电工兄弟做了一个Modbus RTU数据采集工具
  • 7.3.2 Other Technologies, Rambus in Particular
  • 保姆级教程:在VMware里给openEuler虚拟机扩容磁盘,不重启搞定LVM分区
  • 从GMM-HMM到端到端:ASR技术演进、核心挑战与工程实践全解析
  • ICML 2024投稿倒计时24天:手把手教你用Overleaf+Git搞定论文格式与协作(附Latex模板)
  • 理性看待AI热潮:技术边界、应用场景与可持续实践
  • 2023年AR技术趋势:从空间计算、WebAR到产业融合的深度解析
  • 项目介绍 MATLAB实现基于双向门控循环单元(BiGRU))进行锂离子电池健康状态(SOH)的准确估计和剩余使用寿命(RUL)预测(含模型描述及部分示例代码)专栏近期有大量优惠 还请多多点一下关注
  • 从源码到接口:手把手教你用CMake和VS2019为Gmsh生成专属C++开发包
  • 《HarmonyOS技术精讲》五:实战项目 ── 智能支架助手
  • AnchorRefine框架:两阶段残差优化提升机器人操作精度
  • 保姆级教程!互联网用户行为日志数据加工全流程(解析 + 结构化 + 聚合分析,附完整代码 + 踩坑)
  • STM32 FOC实战:手把手教你配置ADC采样点,避开电流采样三大坑(基于R3.2库)
  • 从被动到主动:构建智能Slack机器人的架构演进与实践
  • 用鲸鱼算法自动调SVM参数的Python完整实现(带数据+可视化)
  • 基于检索-重排-抽取流水线的科学文献精准信息抽取系统实践
  • STM32开发环境搭建避坑指南:Clion 2024配置OpenOCD与Arm Toolchain常见问题解析
  • 从DDR到DDR5:内存BANK交错技术(Interleaving)的演进与实战调优(以AMD平台为例)
  • DINO检测器深度解读:对比去噪、混合查询与‘向前看两次’如何联手解决DETR的老大难问题
  • 发起投票小程序怎么弄,云帆投票零门槛上手 - 投票小程序
  • Nat Med发表SPARK智能体框架,可以自主思考、提出假设、设计实验并验证结果,让AI也能主动发现肿瘤生物学规律
  • 基于文本补偿与原型增强的增量学习任务路由机制
  • 从保温杯到电路板:聊聊‘导热系数’这个参数,以及我们怎么在实验室里测它
  • 别再只算准确率了!用Python手撸DCG/IDCG/nDCG,给你的推荐系统做个‘CT检查’
  • C语言指针精讲(三)∶数组名与指针访问,传参与冒泡排序
  • 监控画面总有雪花噪点?深入拆解海思/安霸芯片里的3D降噪技术到底是怎么工作的
  • 【视频资料】NBA总决赛原版视频 (1991-2021)【中英解说】珍藏版
  • 实战指南:如何在不重写数据的情况下,优雅演进你的Iceberg表分区策略