为什么需要自己写驱动在很多HarmonyOS NEXT的应用场景中我们不只是开发一个App而是要跟硬件打交道。比如做个工业巡检助手需要连接一个自定义的红外测温仪或者做一个智能家居中枢需要控制非标准的USB灯控设备。这时候你会发现系统自带的HID、SCSI驱动只管得了键鼠、U盘这种标准设备。一旦遇到非标USB串口设备或者需要精细控制HID协议比如模拟键盘输入就不得不进入驱动开发这个领域。HarmonyOS的Driver Development KitDDK就是为这个场景设计的。它不像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}fromkit.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参数中解析 */;letinterfaceusbDevice.interfaces[0];letinEndpointinterface.endpoints[0];// 输入端点letoutEndpointinterface.endpoints[1];// 输出端点键盘不需要输出// 1. 声明当前驱动可以处理的USB设备VID/PID匹配letdeviceDescriptornewdriver.DeviceDescriptor();deviceDescriptor.vendorId0x1234;// 假设的设备VIDdeviceDescriptor.productId0x5678;deviceDescriptor.probingMode0;// 自动匹配// 2. 为设备创建HID适配器lethidAdapternewdriver.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命令块letcdbnewUint8Array(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;// controlletdataBuffernewArrayBuffer(36);// 发送命令读取返回数据letresultscsiAdapter.sendCommand(cdb,dataBuffer);if(result!0){console.error(SCSI command failed);returnnewUint8Array(0);}returnnewUint8Array(dataBuffer);}SCSI驱动相比HID更严格命令顺序不能乱。在正式读取数据之前必须有INQUIRY→READ CAPACITY→READ10这样的顺序。如果不按这个顺序设备会返回check condition错误。核心实战非标USB串口设备驱动标准外设都有现成的协议和适配器HID和SCSI都有专有类。但自定义USB串口设备通常基于CP2102、FT232、CH34X等芯片就不一样了。它们通常实现为标准CDC ACM设备或者直接走Bulk端点传输。驱动注册假设我们有一个自定义串口设备它的连接方式是从应用层收到数据包格式自己定然后通过USB Bulk端点发给设备。驱动代码核心在onConnect中注册设备并返回Binder。// CustomSerialDriverExtension.tsimport{DriverExtensionAbility,driver,common}fromkit.DriverKit;import{rpc}fromkit.IPCKit;// 自定义Binder实现classCustomSerialBinderextendsrpc.RemoteObject{privatedriverExt:CustomSerialDriverExtension;constructor(ext:CustomSerialDriverExtension){super(CustomSerialBinder);this.driverExtext;}onRemoteRequest(code:number,data:rpc.MessageParcel,reply:rpc.MessageParcel,option:rpc.IRemoteObject):boolean{if(code1){// 打开设备this.driverExt.openDevice();reply.writeInt(0);returntrue;}elseif(code2){// 写数据数据从data中读取letbufferdata.readByteArray();letretthis.driverExt.serialWrite(buffer);reply.writeInt(ret);returntrue;}elseif(code3){// 读数据letresultthis.driverExt.serialRead();reply.writeByteArray(result);returntrue;}returnfalse;}}exportdefaultclassCustomSerialDriverExtensionextendsDriverExtensionAbility{privateusbDevice:driver.UsbDevice|nullnull;privatebulkInEndpoint:driver.UsbEndpoint|nullnull;privatebulkOutEndpoint:driver.UsbEndpoint|nullnull;privateusbIo:driver.UsbIo|nullnull;onInit(want:Want){// 参数校验if(!want.parameters){return;}// 从want中提取USB设备信息letdeviceHandlewant.parameters[usb-device-handle];// 详细获取UsbDevice过程略}onConnect(want:Want):rpc.RemoteObject{// 假设已经拿到了usbDevice对象// 这里简单演示如何声明端点this.usbDevice/*...*/;// 选取第一个接口的Bulk端点letifacethis.usbDevice.interfaces[0];for(letepofiface.endpoints){if(ep.type2){// Bulk类型if(ep.direction0){this.bulkInEndpointep;}else{this.bulkOutEndpointep;}}}// 初始化USB IOthis.usbIonewdriver.UsbIo(this.usbDevice);// 声明独占使用权this.usbIo.claimInterface(0,true);console.log(Serial driver connected);returnnewCustomSerialBinder(this);}openDevice():void{// 发送握手包或初始化命令lethandshakenewUint8Array([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();letlength64;// 假设每次读64字节letbuffernewArrayBuffer(length);letretthis.usbIo.bulkTransfer(this.bulkInEndpoint,buffer,1000);if(ret0){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.usbIonull;}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}fromkit.DriverKit;import{rpc}fromkit.IPCKit;asyncfunctiontestSerialDriver(){try{// 连接驱动扩展constremoteObj:rpc.IRemoteObjectawaitdriver.connectDriverExtension(com.example.myapp/CustomSerialDriver);constoptionnewrpc.MessageOption();constdatarpc.MessageParcel.create();constreplyrpc.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);letbufferreply.readByteArray();console.log(Read data: buffer);data.reclaim();reply.reclaim();}catch(err){console.error(connect driver failed: JSON.stringify(err));}}常见问题问题1onConnect返回Binder后应用层无法调用现象应用层connectDriverExtension返回了对象但发送消息请求时崩溃。原因Binder的onRemoteRequest中如果代码code从0开始会被系统保留。应用层发送请求的code必须从1开始。这是一个HarmonyOS IPC的隐藏约束。解决所有自定义请求代码从1开始编号。问题2驱动加载后无法屏蔽系统默认驱动现象在module.json5中声明了vendorId和productId但插入设备后系统默认驱动如通用HID驱动还是先加载了导致自定义驱动不生效。原因HarmonyOS的USB驱动匹配机制基于优先级。系统内置驱动的优先级高于用户扩展。需要修改配置使优先级高于默认值。解决在deviceDescriptor.probingMode中设置优先级或者在设备插入前动态声明驱动。deviceDescriptor.probingMode1;// 高于默认驱动如果还是不行需要在系统侧预先过滤默认驱动这涉及系统级配置。问题3多次插拔设备后驱动不响应现象设备第一次插入正常工作拔出再插入后就无法连接。原因驱动进程销毁后USB设备节点没有完全释放。再次插拔时系统认为设备还被人占用。解决在onRelease中除了releaseInterface还需要调用driver.removeDriverExtension彻底清理。同时应用层最好监听设备插拔事件在设备拔出时主动断开与驱动的连接。最佳实践不要在onInit中做设备注册。onInit只做进程级初始化设备相关的逻辑应该放在onConnect中因为此时应用层才开始与驱动交互。Binder请求尽量异步化。串口读写如果是长帧数据可能会阻塞Binder线程。建议在驱动内部使用异步队列然后通过回调通知应用层。设备匹配使用精确的VID/PID。如果在module.json5中填写的vendorId过于宽泛如0x0001会匹配到太多设备导致驱动加载冲突。对于非标设备尽量使用特定VID和PID组合。Demo 入口EntryComponentstruct DriverToolHome{build(){Column(){Button(连接串口驱动的Binder).onClick((){testSerialDriver();})}.width(100%).height(100%)}}FAQQ为什么HID驱动要构造报告描述符而SCSI驱动只需要发命令AHID协议要求设备端主动描述自己系统根据描述符解析输入数据。SCSI协议则是主从模式主机主动发命令设备被动响应。二者协议架构不同。Q自定义USB串口驱动可以不写module.json5配置直接从代码中创建设备吗A不行。驱动扩展必须在模块配置中声明否则系统不会把你的DriverExtensionAbility视为合法驱动扩展。module.json5中的metadata是驱动匹配的依据。QBinder通信效率如何适合高频读写吗ABinder本身是原子化IPC单次调用有一定的开销约0.1ms。对于工业传感器频率几十Hz完全够用。如果需要更高速率如视频流可以考虑使用共享内存或者mmap。但DDK当前版本不太建议高频读写建议走厂商提供的独立驱动。如果你也在做HarmonyOS外设驱动重点检查驱动扩展的生命周期管理和USB接口正确释放。系统级USB驱动竞争问题比较多建议先在小范围设备上验证匹配逻辑再推广到全量设备。