当前位置: 首页 > 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.gsyq.cn/news/1438946.html

相关文章:

  • 7.3.2 Other Technologies, Rambus in Particular
  • 从GMM-HMM到端到端:ASR技术演进、核心挑战与工程实践全解析
  • 理性看待AI热潮:技术边界、应用场景与可持续实践
  • ICML 2024投稿倒计时24天:手把手教你用Overleaf+Git搞定论文格式与协作(附Latex模板)
  • 2023年AR技术趋势:从空间计算、WebAR到产业融合的深度解析
  • 《HarmonyOS技术精讲》五:实战项目 ── 智能支架助手
  • STM32 FOC实战:手把手教你配置ADC采样点,避开电流采样三大坑(基于R3.2库)
  • STM32开发环境搭建避坑指南:Clion 2024配置OpenOCD与Arm Toolchain常见问题解析
  • 基于检索-重排-抽取流水线的科学文献精准信息抽取系统实践
  • DINO检测器深度解读:对比去噪、混合查询与‘向前看两次’如何联手解决DETR的老大难问题
  • 发起投票小程序怎么弄,云帆投票零门槛上手 - 投票小程序
  • 实战指南:如何在不重写数据的情况下,优雅演进你的Iceberg表分区策略
  • HPC容器化部署的性能优化与跨平台兼容性挑战
  • 机器学习完全指南:从理论基石到前沿实践的系统化解析
  • 2026年武威市黄金回收靠谱门店推荐 黄金+K金+白银+铂金回收门店TOP5排行榜+联系方式 - 盛世金银回收
  • Multisim仿真避坑指南:差分放大电路偏移计算,你的结果为啥总对不上?
  • DIY一个高精度非接触测温仪:基于Arduino与MLX90614的完整项目教程
  • C语言指针精讲(二)∶加深对指针使用,理解传址调用
  • 用C#和MQTTnet在WinForm里搞个物联网消息中心,附完整源码
  • AI驱动的网络安全攻防:从算法战场到认知完整性战争
  • 手把手教你用MIPSsim模拟器调试MIPS汇编:单步、断点与寄存器观察全攻略
  • Castkit:基于Rust的CLI演示视频自动化生成工具
  • yolov26改进 | Conv/卷积篇 | 轻量化多尺度异构卷积(MSHC)优化YOLOv26精度(附独家网络结构图)
  • 【鸿蒙原生应用开发--ArkUI--015】File-manager 文件管理器应用开发教程
  • 别再傻傻分不清!用Python实战演示标准差、标准误和置信区间的区别(附代码)
  • 小爱音箱语音播放不下载音乐?一招解锁智能下载功能终极指南
  • AI内容如何通过E-E-A-T框架提升SEO效果:策略与实战指南
  • 用YOLOv8和RealSense D415给篮球拍个3D‘X光’:手把手教你提取目标点云
  • WebUncertainty框架:用不确定性建模提升AI智能体在动态网页任务中的鲁棒性
  • Qt桌面应用数据层实战:基于QxOrm封装一个可复用的Model类