
为什么需要自己写驱动
在很多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)已经封装了设备状态感知能力,不要自己造轮子。
环境说明
text
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:基于本机开发的HarmonyOS NEXT真机设备(建议使用有物理USB接口的平板或开发板)
DriverExtensionAbility 生命周期
DDK驱动扩展的核心是DriverExtensionAbility。它是HarmonyOS扩展机制的一部分,不是普通组件。生命周期只有三个函数。
typescript
// DriverExtensionIndex.ts
import { DriverExtensionAbility, driver } from '@kit.DriverKit';
export default class MyUsbDriverExtension extends DriverExtensionAbility {
onInit(want: Want) {
// 驱动初始化,这里不能做耗时操作
console.log('DriverExtension onInit called');
}
onRelease() {
// 驱动释放,清理资源
console.log('DriverExtension onRelease called');
}
onConnect(want: Want) {
// 当有应用连接到本驱动时触发
// 返回一个IBinder用于应用层通信
return new MyDriverBinder();
}
}
这里的关键点:
- onInit:只在驱动进程第一次创建时调用一次,适合做资源预分配,但不要在这里注册设备。因为此时设备可能还没连上。
- onConnect :这是返回给应用层的通信通道,应用层通过
driver.connectDriverExtension拿到这个IBinder,然后才能进行数据收发。 - onRelease:进程销毁前调用,必须在这里释放所有设备资源。否则下次驱动加载时设备会处于脏状态。
标准外设之一:HID键盘驱动(模拟输入)
HID协议的核心是报告描述符(Report Descriptor)。它告诉系统:这个设备能做什么,数据格式是什么。
模拟一个键盘,我们需要构造的报告描述符大致描述:这是一个键盘设备,有8个按键同时按下能力,按键值使用标准USB HID键码。
c
// 构造的HID报告描述符数据
static const uint8_t hidReportDescriptor[] = {
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
};
在驱动注册时,把这份描述符注册进去:
typescript
// 假设在onConnect里面进行设备绑定
onConnect(want: Want): rpc.RemoteObject {
// 获取USB设备端点
let usbDevice = /* 从want参数中解析 */;
let interface = usbDevice.interfaces[0];
let inEndpoint = interface.endpoints[0]; // 输入端点
let outEndpoint = interface.endpoints[1]; // 输出端点(键盘不需要输出)
// 1. 声明当前驱动可以处理的USB设备(VID/PID匹配)
let deviceDescriptor = new driver.DeviceDescriptor();
deviceDescriptor.vendorId = 0x1234; // 假设的设备VID
deviceDescriptor.productId = 0x5678;
deviceDescriptor.probingMode = 0; // 自动匹配
// 2. 为设备创建HID适配器
let hidAdapter = new driver.HidAdapter(usbDevice);
// 注册报告描述符
hidAdapter.registerHidReportDesc(hidReportDescriptor);
// 设置设备通信超时
hidAdapter.setTimeout(1000);
// 返回用于应用通信的Binder
return new DriverBinderImpl(hidAdapter);
}
这一段的要点:HID报告描述符是整个驱动的心脏。如果描述符写错,系统要么识别不出设备,要么报告数据解析完全错乱。官方文档也提到了标准HID描述符的结构,但实际在ArkTS中构造字节数组变量比较繁琐,建议参考USB-IF官方HID规范。
标准外设之二:SCSI设备的CDB命令
对于U盘、读卡器这类SCSI设备,驱动开发的核心是CDB命令。比如要读取设备信息,需要发送INQUIRY命令。
typescript
// 发送SCSI INQUIRY命令
function sendInquiryCommand(scsiAdapter: driver.ScsiAdapter): Uint8Array {
// 构造CDB命令块
let cdb = new Uint8Array(6); // 6字节CDB
cdb[0] = 0x12; // INQUIRY操作码
cdb[1] = 0x00; // obsolete
cdb[2] = 0x00; // page code
cdb[3] = 0x00; // allocation length high byte
cdb[4] = 0x24; // allocation length low byte (36 bytes)
cdb[5] = 0x00; // control
let dataBuffer = new ArrayBuffer(36);
// 发送命令,读取返回数据
let result = scsiAdapter.sendCommand(cdb, dataBuffer);
if (result !== 0) {
console.error('SCSI command failed');
return new Uint8Array(0);
}
return new Uint8Array(dataBuffer);
}
SCSI驱动相比HID更严格:命令顺序不能乱。在正式读取数据之前,必须有INQUIRY→READ CAPACITY→READ(10)这样的顺序。如果不按这个顺序,设备会返回check condition错误。
核心实战:非标USB串口设备驱动
标准外设都有现成的协议和适配器,HID和SCSI都有专有类。但自定义USB串口设备(通常基于CP2102、FT232、CH34X等芯片)就不一样了。它们通常实现为标准CDC ACM设备,或者直接走Bulk端点传输。
驱动注册
假设我们有一个自定义串口设备,它的连接方式是:从应用层收到数据包(格式自己定),然后通过USB Bulk端点发给设备。驱动代码核心在onConnect中注册设备并返回Binder。
typescript
// CustomSerialDriverExtension.ts
import { DriverExtensionAbility, driver, common } from '@kit.DriverKit';
import { rpc } from '@kit.IPCKit';
// 自定义Binder实现
class CustomSerialBinder extends rpc.RemoteObject {
private driverExt: 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);
return true;
} else if (code === 2) {
// 写数据,数据从data中读取
let buffer = data.readByteArray();
let ret = this.driverExt.serialWrite(buffer);
reply.writeInt(ret);
return true;
} else if (code === 3) {
// 读数据
let result = this.driverExt.serialRead();
reply.writeByteArray(result);
return true;
}
return false;
}
}
export default class CustomSerialDriverExtension extends DriverExtensionAbility {
private usbDevice: driver.UsbDevice | null = null;
private bulkInEndpoint: driver.UsbEndpoint | null = null;
private bulkOutEndpoint: driver.UsbEndpoint | null = null;
private usbIo: driver.UsbIo | null = null;
onInit(want: Want) {
// 参数校验
if (!want.parameters) {
return;
}
// 从want中提取USB设备信息
let deviceHandle = want.parameters['usb-device-handle'];
// 详细获取UsbDevice过程略
}
onConnect(want: Want): rpc.RemoteObject {
// 假设已经拿到了usbDevice对象
// 这里简单演示如何声明端点
this.usbDevice = /*...*/;
// 选取第一个接口的Bulk端点
let iface = this.usbDevice.interfaces[0];
for (let ep of iface.endpoints) {
if (ep.type === 2) { // Bulk类型
if (ep.direction === 0) {
this.bulkInEndpoint = ep;
} else {
this.bulkOutEndpoint = ep;
}
}
}
// 初始化USB IO
this.usbIo = new driver.UsbIo(this.usbDevice);
// 声明独占使用权
this.usbIo.claimInterface(0, true);
console.log('Serial driver connected');
return new CustomSerialBinder(this);
}
openDevice(): void {
// 发送握手包或初始化命令
let handshake = new Uint8Array([0xAA, 0x01, 0x00, 0x00, 0x55]);
this.bulkWrite(handshake);
}
serialWrite(data: Uint8Array): number {
if (!this.usbIo || !this.bulkOutEndpoint) return -1;
// 通过Bulk端点发送
return this.usbIo.bulkTransfer(this.bulkOutEndpoint, data.buffer, 1000);
}
serialRead(): Uint8Array {
if (!this.usbIo || !this.bulkInEndpoint) return new Uint8Array();
let length = 64; // 假设每次读64字节
let buffer = new ArrayBuffer(length);
let ret = this.usbIo.bulkTransfer(this.bulkInEndpoint, buffer, 1000);
if (ret > 0) {
return new Uint8Array(buffer.slice(0, ret));
}
return new Uint8Array(0);
}
private bulkWrite(buffer: Uint8Array): number {
return this.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中添加配置:
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调用驱动函数:
typescript
import { driver, common } from '@kit.DriverKit';
import { rpc } from '@kit.IPCKit';
async function testSerialDriver() {
try {
// 连接驱动扩展
const remoteObj: rpc.IRemoteObject = await driver.connectDriverExtension(
'com.example.myapp/CustomSerialDriver'
);
const option = new rpc.MessageOption();
const data = rpc.MessageParcel.create();
const reply = rpc.MessageParcel.create();
// 打开设备
remoteObj.sendMessageRequest(1, data, reply, option);
console.log('Open result: ' + reply.readInt());
// 写数据
data.writeByteArray(new Uint8Array([0x01, 0x02, 0x03, 0x04]));
remoteObj.sendMessageRequest(2, data, reply, option);
console.log('Write size: ' + reply.readInt());
// 读数据
remoteObj.sendMessageRequest(3, data, reply, option);
let buffer = 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中声明了vendorId和productId,但插入设备后系统默认驱动(如通用HID驱动)还是先加载了,导致自定义驱动不生效。
原因:HarmonyOS的USB驱动匹配机制基于优先级。系统内置驱动的优先级高于用户扩展。需要修改配置使优先级高于默认值。
解决:在deviceDescriptor.probingMode中设置优先级,或者在设备插入前动态声明驱动。
typescript
deviceDescriptor.probingMode = 1; // 高于默认驱动
如果还是不行,需要在系统侧预先过滤默认驱动,这涉及系统级配置。
问题3:多次插拔设备后驱动不响应
现象:设备第一次插入正常工作,拔出再插入后就无法连接。
原因:驱动进程销毁后,USB设备节点没有完全释放。再次插拔时系统认为设备还被人占用。
解决:在onRelease中除了releaseInterface,还需要调用driver.removeDriverExtension彻底清理。同时,应用层最好监听设备插拔事件,在设备拔出时主动断开与驱动的连接。
最佳实践
- 不要在onInit中做设备注册。onInit只做进程级初始化,设备相关的逻辑应该放在onConnect中,因为此时应用层才开始与驱动交互。
- Binder请求尽量异步化。串口读写如果是长帧数据,可能会阻塞Binder线程。建议在驱动内部使用异步队列,然后通过回调通知应用层。
- 设备匹配使用精确的VID/PID 。如果在
module.json5中填写的vendorId过于宽泛(如0x0001),会匹配到太多设备,导致驱动加载冲突。对于非标设备,尽量使用特定VID和PID组合。
Demo 入口
typescript
@Entry
@Component
struct 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驱动竞争问题比较多,建议先在小范围设备上验证匹配逻辑,再推广到全量设备。