《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)已经封装了设备状态感知能力,不要自己造轮子。

环境说明

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中声明了vendorIdproductId,但插入设备后系统默认驱动(如通用HID驱动)还是先加载了,导致自定义驱动不生效。

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

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

typescript 复制代码
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 入口

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驱动竞争问题比较多,建议先在小范围设备上验证匹配逻辑,再推广到全量设备。

相关推荐
不羁的木木2 小时前
《HarmonyOS底部页签-沉浸光感组件实战》高级定制:图标出血与分割线
华为·harmonyos
Goway_Hui4 小时前
【鸿蒙原生应用开发--ArkUI--015】File-manager 文件管理器应用开发教程
华为·harmonyos
不羁的木木6 小时前
《HarmonyOS底部页签-沉浸光感组件实战》基础入门:认识HdsTabs容器与核心配置
华为·harmonyos
不羁的木木6 小时前
《HarmonyOS技术精讲》三:记忆链接 ── 跨场景数据融合
pytorch·华为·harmonyos
2501_919749036 小时前
鸿蒙 Flutter 实战:image_crop 0.4.1 适配 3.27-ohos 全流程
flutter·华为·harmonyos
祭曦念6 小时前
鸿蒙应用的生命周期与页面跳转:从入门到实战
华为·harmonyos
轻口味7 小时前
HarmonyOS 6.1.1 全栈实战录 - 88 实战 Ability Kit 启动生命周期预热与快照恢复机
华为·harmonyos·鸿蒙
Goway_Hui7 小时前
【鸿蒙原生应用开发--ArkUI--013】Exercise-tracker 运动记录应用开发教程
华为·harmonyos
想你依然心痛8 小时前
HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与HMAF的“图谱智脑“——PC端AI智能体沉浸式知识图谱构建工作台
人工智能·ar·知识图谱·harmonyos·智能体