框架"/>
OpenHarmony的USB框架
作者:鸿湖万联 许文龙
1、 前言
写这篇文档主要目的是想弄清楚OpenHarmony的usb设备、驱动以及设备结点的加载过程,弄清楚usb分别在内核驱动、HDF、ueventd都做了什么,是什么关系。顺便,学习一下HDF的设计思路,它是如何与内核态的驱动交互的。同时也能窥探一下内核的驱动框架。
最后,就是要基于自己的理解,解决usb设备结点为什么有的没有被创建。比如插入打印机,看不到结点。是系统bug还是有意为之。
2、通讯机制
2.1 字符设备驱动
内核提供了三种设备文件,字符设备、块设备、网络设备。
属性开头c标志的都是字符设备。
字符设备驱动机制
在用户态,往字符设备文件写数据,就相当于向设备写数据。
-
usb 设备驱动初始化时,会注册设备驱动。然后生成主设备号,绑定操作函数。并没有生成设备结点。
const struct file_operations usbdev_file_operations = {.owner = THIS_MODULE,.llseek = no_seek_end_llseek, ----> 对应Linux 标准接口seek.read = usbdev_read,----> 对应Linux 标准接口read.poll = usbdev_poll,----> 对应Linux 标准接口poll.unlocked_ioctl = usbdev_ioctl, ----> 对应Linux 标准接口ioctlpat_ioctl = compat_ptr_ioctl,.mmap = usbdev_mmap, ----> 对应Linux 标准接口mmap.open = usbdev_open, ----> 对应Linux 标准接口open.release = usbdev_release, };
-
设备插入时,生成次设备号,并探测到对应的驱动,关联具体的操作函数。状态上报给用户空间,用户守护进程(Openharmony 里是Ueventd)创建设备结点。
-
在用户空间操作设备,就相当于调用了驱动的操作函数。比如,打开设备文件后,用户调用ioctl, 就相当于调用驱动的ioctl函数。
设备号
字符设备的路径是可以改的,主要的是主设备号、次设备号。主设备号对应bus上的设备驱动,主设备号是固定的,比如usb的主设备号是180,任何设备上都是固定的。而是次设备号是动态申请的。
下面是主设备号:
# cat /proc/devices
Character devices:1 mem4 tty4 ttyS5 /dev/tty5 /dev/console10 misc13 input29 fb81 video4linux89 i2c
153 spi
180 usb
188 ttyUSB
189 usb_device
usb 可能比较特别,他跟其他驱动不同,他有两个主设备号。180和189, 这主要是因为usb 还包括鼠标、键盘这样的设备以及一些复杂的数据传输。
-
usb_device 189 是usb设备文件系统(usbfs)
# ls -al /dev/bus/usb/003 crw-rw---- 1 root root 189, 256 2017-08-05 09:00 001 crw-rw-r-- 1 root root 189, 259 2017-08-08 07:01 004
-
usb 180 是给用户开发使用的。可以直接读写对应的设备结点即可读写设备。
# ls /dev/usb/ -al crw------- 1 root root 180, 0 2017-08-09 05:43 lp0
用180和189应该都是可以通讯的,只是使用189会复杂些,需要处理许多细节,比如发送数据要自己构造urb的结构体,而180则简单很多,直接按文件读写就可以了。
HDF DDK,libusb等都是使用189, 因为他们需要实现许多复杂的需求,而普通应用程序只需要收发数据,直接使用180结点就行了。这也解释了为什么用libusb只需要知道bus 和dev 编号就能通讯。
实现原理
字符设备也是有个驱动的,这里没有详细了解。只是弄清楚机制和用法。因为,在Openharmony里很多代码用到这个东西,比如token_id也是这样的。
# ls -al /dev/access_token_id
crw-rw-rw- 1 access_token access_token 10, 56 2017-08-05 09:00 /dev/access_token_id
这就是设备结点的一个基本的机制。
以usb_device 为例:
int usb_devio_init()
{// 1、 注册设备号, 这里是189, 即usb_device. 三个参数分别是主设备号(189), 设备可分配最大个数,名称retval = register_chrdev_region(USB_DEVICE_DEV, USB_DEVICE_MAX,"usb_device"); // 在/proc/devices中添加: 189 usb_deviceif (retval) {printk(KERN_ERR "Unable to register minors for usb_device\n");goto out;}// 2、 注册设备号, 关联对应的操作函数cdev_init(&usb_device_cdev, &usbdev_file_operations);// 3、 添加到usb 的bus上。retval = cdev_add(&usb_device_cdev, USB_DEVICE_DEV, USB_DEVICE_MAX);if (retval) {printk(KERN_ERR "Unable to get usb_device major %d\n",USB_DEVICE_MAJOR);goto error_cdev;}usb_register_notify(&usbdev_nb);
}const struct file_operations usbdev_file_operations = {.owner = THIS_MODULE,.llseek = no_seek_end_llseek, ----> 对应Linux 标准接口seek.read = usbdev_read,----> 对应Linux 标准接口read.poll = usbdev_poll,----> 对应Linux 标准接口poll.unlocked_ioctl = usbdev_ioctl, ----> 对应Linux 标准接口ioctlpat_ioctl = compat_ptr_ioctl,.mmap = usbdev_mmap, ----> 对应Linux 标准接口mmap.open = usbdev_open, ----> 对应Linux 标准接口open.release = usbdev_release,
};static long usbdev_do_ioctl(struct file *file, unsigned int cmd,void __user *p)
{struct usb_dev_state *ps = file->private_data;struct inode *inode = file_inode(file);struct usb_device *dev = ps->dev;int ret = -ENOTTY;switch (cmd) {case USBDEVFS_BULK:snoop(&dev->dev, "%s: BULK\n", __func__);ret = proc_bulk(ps, p);if (ret >= 0)inode->i_mtime = current_time(inode);break;case USBDEVFS_GET_CAPABILITIES:ret = proc_get_capabilities(ps, p);break;。。。 省略}
次设备号是在设备插拔时自动生成的,是动态的。主设备号是静态申请的,固定的。
在HDF 的用户态是这样调用的:
// 对应的设备结点/dev/bus/usb/ 下的设备结点,主设备号是189char path[64];sprintf_s(path, sizeof(path), USB_DEV_FS_PATH "/%03u/%03u", dev->busNum, dev->devAddr);fd = open(path,0666);// 获取设备接口描述符ret = ioctl(fd, USBDEVFS_GET_CAPABILITIES, &devHandle->caps);// 传输数据ret = ioctl(fd, USBDEVFS_SUBMITURB, urb);
这就是字符设备的一个机制。
2.2 HdfSBuf
HdfSBuf 类似组件使用的Binder机制,使用比较相似,只是底层机制不同,Binder使用的是共享内存。
HdfSBuf 底层也是使用字符设备实现的。底层细节没有深入。
crw-rw---- 1 root root 234, 0 2017-08-05 09:00 dev_mgr
crw-rw---- 1 root root 234, 1 2017-08-05 09:00 devsvc_mgr
crw-rw---- 1 root root 234, 7 2017-08-05 09:00 hdf_audio_capture
crw-rw---- 1 root root 234, 8 2017-08-05 09:00 hdf_audio_render
crw-rw---- 1 root root 234, 11 2017-08-05 09:00 hdf_usb_pnp_notify_service
HdfSBuf 不仅可以在内核空间和用户空间通讯,也可以转变成Binder的对象,使用c++开发。
int32_t SbufToParcel(struct HdfSBuf *sbuf, OHOS::MessageParcel **parcel);
下面看一个例子:
服务端:
static int32_t UsbPnpNotifyDispatch(struct HdfDeviceIoClient *client, int32_t cmd, struct HdfSBuf *data, struct HdfSBuf *reply)
{int32_t ret = HDF_SUCCESS;struct UsbPnpAddRemoveInfo *usbPnpInfo = NULL;uint32_t infoSize;HdfSbufReadBuffer(data, (const void **)(&usbPnpInfo), &infoSize);// 根据cmd做相应处理,代码省略OsalMutexUnlock(&g_usbSendEventLock);if (!HdfSbufWriteInt32(reply, INT32_MAX)) {HDF_LOGE("%s: reply int32 fail", __func__);}return ret;
}static int32_t UsbPnpNotifyBind(struct HdfDeviceObject *device)
{static struct IDeviceIoService pnpNotifyService = {.Dispatch = UsbPnpNotifyDispatch,};device->service = &pnpNotifyService;return HDF_SUCCESS;
}struct HdfDriverEntry g_usbPnpNotifyEntry = {.moduleVersion = 1,.Bind = UsbPnpNotifyBind,.Init = UsbPnpNotifyInit,.Release = UsbPnpNotifyRelease,.moduleName = "HDF_USB_PNP_NOTIFY",
};HDF_INIT(g_usbPnpNotifyEntry);
客户端:
struct HdfSBuf *data = HdfObtainDefaultSize();
struct HdfSBuf *reply = HdfObainDefaultSize();
struct UsbPnpAddRemoveInfo usbPnpInfo;
uint32_t ret = 0;HdfSbufWriteBuffer(data, &usbPnpInfo, sizeof(usbPnpInfo));
// 阻塞的
server->dispatcher->Dispatch(&server->object, cmd, data, reply);HdfSbufReadInt32(reply, &ret);HdfSBufRecycle(data);
HdfSBufRecycle(reply);
return ret;
2.3 Uevent
uevent 主要是用于usb设备插拔监控的。
socket的NETLINK协议族有多种协议,包括网络通讯、进程间通信、路由查询,ueventd等。ueventd是内核空间向用户空间主动上报事件的一种协议。内核驱动只是作为socket客户端,广播消息。谁接收由用户进程决定。
在OpenHarmony中,用户进程的热插拔处理是有一个Ueventd的进程。它是基于init的fd代持技术实现条件触发、按需启动。
-
条件就是收到socket的NETLINK_KOBJECT_UEVENT的消息,ueventd进程就会被唤醒
-
按需启动就是出现热插拔事件。
通过调试也能看到这一点:
插拔设备之前,一直是停止运行的,
# param dump | grep ueventdparam: startup.service.ctl.ueventd=stopped插入设备之后,很快就被唤醒了。
# param dump | grep ueventdparam: startup.service.ctl.ueventd=running
#
Ueventd使用的是NETLINK_KOBJECT_UEVENT协议,获取驱动发来的uevent消息。
struct sockaddr_nl addr;addr.nl_family = AF_NETLINK;addr.nl_pid = getpid();addr.nl_groups = 0xffffffff;int sockfd = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, NETLINK_KOBJECT_UEVENT);setsockopt(sockfd, SOL_SOCKET, SO_RCVBUFFORCE, &buffSize, sizeof(buffSize));setsockopt(sockfd, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on));if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {}
3、 USB 通讯过程
3.1 usb 内核驱动
内核usb主要就是加载驱动。并将状态通过uevent报文上报给用户空间。
大致了解一下。
1、注册debugfs, 注册电源等高级配置。 /sys/kernel/debug/usb/ 可以看各种调试信息
2、注册usb总线,并加载到系统驱动总线. 会创建 /sys/bus/usb目录,关联相关的函数操作,包括uevent,设备match
3、分配主设备号,180。 /sys/bus/usb/drviers/usb, 关联ioctl,read,write等函数
4、分配usbfs设备号,189, /sys/bus/usb/drivers/usbfs, 关联ioctl,open, read等函数
5、注册hub
6、注册一般设备驱动(usb分为设备驱动和接口驱动,接口驱动可以理解为逻辑设备,一个设备可以有多个逻辑设备)
7、接口驱动是单独注册的,在/sys/bus/usb/drivers 这里可以看到所有已经加载的驱动。
# ls /sys/bus/usb/drivers/usblp/ -al
total 0
drwxr-xr-x 2 root root 0 2017-08-05 09:26 .
drwxr-xr-x 33 root root 0 2017-08-05 09:00 ..
lrwxrwxrwx 1 root root 0 2017-08-06 01:11 3-1:1.0 -> ../../../../devices/platform/fd840000.usb/usb3/3-1/3-1:1.0
--w------- 1 root root 4096 2017-08-06 01:11 bind
lrwxrwxrwx 1 root root 0 2017-08-06 01:11 module -> ../../../../module/usblp
-rw-r--r-- 1 root root 4096 2017-08-06 01:11 new_id
-rw-r--r-- 1 root root 4096 2017-08-06 01:11 remove_id
--w------- 1 root root 4096 2017-08-06 01:11 uevent
--w------- 1 root root 4096 2017-08-06 01:11 unbind
驱动框架:
这里的设备是逻辑设备,对于usb来说就是接口驱动。
状态通知:
数据通讯:
3.2 Ueventd进程
在Openharmony里,用户空间使用的是Ueventd作为守护进程。前面已经说过,它是条件触发、按需启动。这里做一个简单梳理:
1、 解析设备结点的权限配置文件
/etc/ueventd.config
/vendor/etc/ueventd.config
// 摘取部分
/dev/block/sdd19 0660 6666 6666
/dev/watchdog 0660 watchdog watchdog
/dev/hdf_input_event* 0660 3029 3029
/dev/HDF* 0666 0 0
/dev/ttyS* 0666 0 0
/dev/ttyACM* 0666 0 0
/dev/ttyUSB* 0666 0 0
2、基于init的fd代持技术,获取socket句柄。
如果init进程socket不存在,就自己按默认方式创建socket服务端。
使用的是NETLINK_KOBJECT_UEVENT
3、解析ueventd报文
就是一般的文本格式。
# cat /sys/class/usbmisc/lp0/uevent
MAJOR=180
MINOR=0
DEVNAME=usb/lp0
4、创建设备结点
两种方式:
-
静态创建设备结点
扫描这些目录下的ueventd文件,并创建设备结点。
Trigger("/sys/block", sockFd, devices, num); Trigger("/sys/class", sockFd, devices, num); Trigger("/sys/devices", sockFd, devices, num);
就是系统启动时,在ueventd里已经配置好的。一般可能是上次系统关闭之前设备并未拔出,系统重启后就生成的。
/sys/class/usbmisc/lp0/uevent # cat /sys/class/usbmisc/lp0/uevent MAJOR=180 MINOR=0 DEVNAME=usb/lp0
-
动态创建设备结点
是收到netlink uevent消息时触发创建的。
下面是usb的设备结点路径处理的代码。
void HandleOtherDeviceEvent(const struct Uevent *uevent)
{char deviceNode[DEVICE_FILE_SIZE] = {};char sysPath[SYSPATH_SIZE] = {};const char *devName = GetDeviceName(sysPath, uevent->deviceName);const char *devPath = GetDeviceBasePath(uevent->subsystem);if (STRINGEQUAL(uevent->subsystem, "usb")) {// 189 的设备结点, /dev/bus/usb/003/001if (uevent->deviceName != NULL) {if (snprintf_s(deviceNode, DEVICE_FILE_SIZE, DEVICE_FILE_SIZE - 1, "/dev/%s", uevent->deviceName) == -1) {INIT_LOGE("Make device file for device [%d : %d]", uevent->major, uevent->minor);return;}} } else if (STARTSWITH(uevent->subsystem, "usb")) {//180的设备结点,走这条线路// Other usb devies, do not handle it.return;} else {}HandleDeviceNode(uevent, deviceNode, false);// 最终调用mknode(path, mode, 主设备号<<8|次设备号); //mode是权限,默认0666
}
5、设备结点参数化
在OpenHarmony release 3.2版本将设备节点参数化。
startup.uevent.xxx = "added"/"remove"
这样,就可以通过参数服务提供的接口,将ueventd的热插拔事件通知到任何用户进程。
不过这部分代码,好像有问题。
3.3 HDF usb
hdf 服务分为内核态(khdf)和用户态(uhdf)服务。 这个可以根据hcs 的配置文件所在的目录可以知道。
vendor/isoftstone/tecsun/hdf_config
├── khdf
│ ├── audio
│ ├── device_info
│ ├── hdf.hcs
│ ├── input
│ └── wifi
└── uhdf├── camera├── device_info.hcs├── hdf.hcs├── media_codec├── usb_ecm_acm.hcs└── usb_pnp_device.hcs
在编译时,hdf目录的framework,khdf 部分的代码是链接到内核一起编译的。
wlxuz@swanlink02:~/code/SwanlinkOS$ ls -al out/kernel/src_tmp/linux-5.10/drivers/hdf/
total 24
drwxr-xr-x 4 wlxuz deve 4096 May 6 17:43 .
drwxr-xr-x 146 wlxuz deve 4096 May 6 17:43 ..
drwxr-xr-x 2 wlxuz deve 4096 May 6 17:43 evdev
lrwxrwxrwx 1 wlxuz deve 58 May 6 17:43 framework -> //SwanlinkOS/drivers/hdf_core/framework
lrwxrwxrwx 1 wlxuz deve 67 May 6 17:43 khdf -> //SwanlinkOS/drivers/hdf_core/adapter/khdf/linux
-rw-r--r-- 1 wlxuz deve 74 May 6 17:43 Makefile
drwxr-xr-x 3 wlxuz deve 4096 May 6 17:43 wifi
hcs 配置文件里, 每个host是一个独立的进程。
UsbPnpNotify服务是创建在内核态的服务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6cXuXmhm-1683769479080)(figures/4b535758-af46-11ec-aa7f-dac502259ad0.png)]
它通过调用下面的内核接口获取内核空间的usb驱动的状态:
usb_register_notify(&g_usbPnpNotifyNb); // 将监听接口注册到usb的通知链。
usb_for_each_dev((void *)client, funcs); // 遍历设备列表
通知链是usb总线的一个通知链表,当事件触发时,所有注册到这个链表的监听函数,会被依次通知。HDF 利用这种机制将内核的状态变化通知到hdf。
这样就实现了本来只能在内核态开发的usb驱动迁移到了用户态。
4、 问题解决
4.1 设备结点未创建的问题
主要问题是插入usb设备,设备结点没有被创建。主要分析ueventd的log基本就知道问题所在。
查看log 可以用下面命令都可以看log
# cat /proc/kmsg
# hilog -t kmsg
# dmesg | grep usb
先用 dmesg|grep usb 查看插拔时产生的log,
[252064.106002] [I/USB_PNP_NOTIFY] UsbPnpNotifyHdfSendEvent:387 report one device information, 4 usbDevAddr=18446743524337473536, devNum=3, busNum=3, infoTable=1-0x483-0x7540
!\x0d
[252066.381377] usb 3-1: new full-speed USB device number 4 using ohci-platform
[252066.614071] usb 3-1: New USB device found, idVendor=0483, idProduct=7540, bcdDevice= 2.00
[252066.614185] usb 3-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[252066.614227] usb 3-1: Product: ICOD_Thermal_Printer
[252066.614259] usb 3-1: Manufacturer: ICOD
[252066.614291] usb 3-1: SerialNumber: 000000000002af5360
[252066.630719] usblp 3-1:1.0: usblp0: USB Bidirectional printer dev 4 if 0 alt 0 proto 2 vid 0x0483 pid 0x7540
[252066.634401] [I/USB_PNP_NOTIFY] UsbPnpNotifyHdfSendEvent:387 report one device information, 3 usbDevAddr=18446743524337473536, devNum=4, busNum=3, infoTable=1-0x483-0x7540
!\x0d
可以看到,UsbPnpNotify 服务首先获取到了新插入的设备,并将信息上报.
下面是ueventd打印的log,usb打印机的subsystem 为usbmisc, 路径为lp0.
[2017-8-8 7:1:8][pid=5353][<6>][INFO] [ueventd_device_handler.c:464)] subsystem = usb, syspath = /devices/platform/fd840000.usb/usb3/3-1
[2017-8-8 7:1:8][pid=5353][<6>][INFO] [ueventd_device_handler.c:483)] HandleOtherDeviceEvent, devPath = /dev, devName = 004
[2017-8-8 7:1:8][pid=5353][<6>][INFO] [ueventd_device_handler.c:464)] subsystem = usb, syspath = /devices/platform/fd840000.usb/usb3/3-1/3-1:1.0
[2017-8-8 7:1:8][pid=5353][<6>][INFO] [ueventd_device_handler.c:464)] subsystem = class, syspath = /class/usbmisc
[2017-8-8 7:1:8][pid=5353][<6>][INFO] [ueventd_device_handler.c:464)] subsystem = usbmisc, syspath = /devices/platform/fd840000.usb/usb3/3-1/3-1:1.0/usbmisc/lp0
[2017-8-8 7:1:8][pid=5353][<6>][INFO] [ueventd_device_handler.c:483)] HandleOtherDeviceEvent, devPath = /dev, devName = lp0
所以,只需要在HandleOtherDeviceEvent函数拼接路径即可。
void HandleOtherDeviceEvent(const struct Uevent *uevent)
{// 其他代码省略 。。。if (STRINGEQUAL(uevent->subsystem, "usb")) {。。。 省略, 这里是/dev/bus/usb/ 的设备结点} else if (STARTSWITH(uevent->subsystem, "usb")) {// Other usb devies, do not handle it.// 添加的代码if (!STRINGEQUAL(uevent->subsystem, "usbmisc")) {return;}if (devPath == NULL) {return;}// printer usb devices ,need create device node: /dev/usb/lp%dif (snprintf_s(deviceNode, DEVICE_FILE_SIZE, DEVICE_FILE_SIZE - 1, "%s/usb/%s", devPath, devName) == -1) {INIT_LOGE("Make device file for device [%d : %d]", uevent->major, uevent->minor);return;}}
}
通过运行, 设备结点确实创建了,而且可用。
# ls -al /dev/usb/
total 0
drwxr-xr-x 2 root root 60 2017-08-10 06:30 .
drwxr-xr-x 21 root root 4340 2017-08-05 09:00 ..
crw------- 1 root root 180, 0 2017-08-10 06:30 lp0
发现权限太高了,配置权限:
在/etc/ueventd.config 添加一行:
/dev/usb/* 0666 1005 1005
1005是samgr的uid,gid
重启系统,权限就变了。
# ls -al /dev/usb/
total 0
drwxr-xr-x 2 root root 60 2017-08-09 14:09 .
drwxr-xr-x 21 root root 4340 2017-08-09 14:09 ..
crwxrwxrwx 1 samgr samgr 180, 0 2017-08-09 14:09 lp0
执行打印命令,验证确实可用。
echo "test print" > /dev/usb/lp0
4. 总结
这里的设备是逻辑设备,对于usb来说就是接口驱动。
状态通知:
数据通讯:
更多推荐
OpenHarmony的USB框架
发布评论