uvc摄像头驱动uvc设备的注册分析

简介: uvc摄像头驱动uvc设备的注册分析

uvc_init

/driver/media/usb/uvc/uvc_driver.c

/*
 * UVC 驱动结构体
 */
struct uvc_driver uvc_driver = {
    .driver = {
        .name        = "uvcvideo", // 驱动名
        .probe        = uvc_probe, // 探测函数
        .disconnect    = uvc_disconnect, // 断开连接函数
        .suspend    = uvc_suspend, // 挂起函数
        .resume        = uvc_resume, // 恢复函数
        .reset_resume    = uvc_reset_resume, // 重置恢复函数
        .id_table    = uvc_ids, // 设备 ID 表
        .supports_autosuspend = 1, // 支持自动挂起
    },
};

该结构体的作用是初始化UVC(USB Video Class)驱动。主要包括以下步骤:

  1. 调用uvc_debugfs_init()函数初始化调试文件系统,用于在调试过程中提供驱动相关的调试信息。
  2. 调用usb_register()函数注册USB设备,将UVC驱动与USB设备进行关联。此处使用的是uvc_driver结构体中定义的USB驱动。
  3. 如果USB设备注册失败(ret小于0),则调用uvc_debugfs_cleanup()函数清理调试文件系统,释放相关资源,并返回错误码。
  4. 如果USB设备注册成功,使用printk()函数打印驱动的描述信息(DRIVER_DESC)和版本号(DRIVER_VERSION)。
  5. 返回0,表示驱动初始化成功。
    总体来说,该函数用于初始化UVC驱动,并将其注册到USB子系统中,以便与相应的USB设备进行匹配和通信。同时,它还处理了调试文件系统的初始化和清理,以及打印驱动信息的功能。
/*
 * uvc_init - UVC 驱动初始化函数
 */
static int __init uvc_init(void)
{
    int ret;
    /*
     * 初始化调试文件系统
     */
    uvc_debugfs_init();
    /*
     * 注册 USB 设备
     */
    ret = usb_register(&uvc_driver.driver);
    if (ret < 0) {
        /*
         * 注册失败,清理调试文件系统
         */
        uvc_debugfs_cleanup();
        return ret;
    }
    /*
     * 打印驱动信息
     */
    printk(KERN_INFO DRIVER_DESC " (" DRIVER_VERSION ")\n");
    return 0;
}

uvc_init->usb_register->

pr_info("%s: registered new interface driver %s\n",
            usbcore_name, new_driver->name);

接入uvc驱动支持的摄像头,并匹配成功之后调用uvc_probe函数

uvc_probe

/driver/media/usb/uvc/uvc_driver.c

该函数是UVC(USB Video Class)驱动中的设备探测函数,用于处理USB设备与UVC驱动的匹配和初始化工作。一般情况下,它会在USB设备与UVC驱动匹配成功时被调用。

作用:

  1. 确定USB设备是否与当前的UVC驱动匹配,通过检查设备的VID(Vendor ID)和PID(Product ID)等信息。
  2. 分配并初始化UVC设备的数据结构,如uvc_device、uvc_streaming等。
  3. 配置和注册UVC设备,包括分配和初始化视频流的端点、请求USB设备资源等。
  4. 注册视频设备,创建V4L2(Video4Linux2)设备节点,以便用户空间可以访问和操作视频流。
  5. 启动视频流的传输和捕获,包括设置视频格式、帧率等参数,开启视频流传输通道。
  6. 设置V4L2设备的相关属性和回调函数,以响应用户空间的操作和请求。
    通常,当插入支持UVC协议的USB摄像头或视频设备时,USB子系统会检测到设备并调用uvc_probe()函数,以便与UVC驱动进行匹配和初始化。
static int uvc_probe(struct usb_interface *intf,
             const struct usb_device_id *id)
{
    // 获取 USB 设备
    struct usb_device *udev = interface_to_usbdev(intf);
    // 定义 UVC 设备
    struct uvc_device *dev;
    int ret;
    // 如果 idVendor 和 idProduct 都存在,则打印已知 UVC 设备的信息
    if (id->idVendor && id->idProduct)
        uvc_trace(UVC_TRACE_PROBE, "Probing known UVC device %s "
                "(%04x:%04x)\n", udev->devpath, id->idVendor,
                id->idProduct);
    // 否则打印通用 UVC 设备的信息
    else
        uvc_trace(UVC_TRACE_PROBE, "Probing generic UVC device %s\n",
                udev->devpath);
    // 为设备分配内存并初始化
    if ((dev = kzalloc(sizeof *dev, GFP_KERNEL)) == NULL)
        return -ENOMEM;
// 初始化设备的各个链表
    INIT_LIST_HEAD(&dev->entities);
    INIT_LIST_HEAD(&dev->chains);
    INIT_LIST_HEAD(&dev->streams);
    atomic_set(&dev->nstreams, 0);
    atomic_set(&dev->nmappings, 0);
    mutex_init(&dev->lock);
    // 获取 USB 设备
    dev->udev = usb_get_dev(udev);
    dev->intf = usb_get_intf(intf);
    dev->intfnum = intf->cur_altsetting->desc.bInterfaceNumber;
    dev->quirks = (uvc_quirks_param == -1)
            ? id->driver_info : uvc_quirks_param;
    // 如果 USB 设备有产品名称,则使用该名称,否则使用默认名称
    if (udev->product != NULL)
        strlcpy(dev->name, udev->product, sizeof dev->name);
    else
        snprintf(dev->name, sizeof dev->name,
            "UVC Camera (%04x:%04x)",
            le16_to_cpu(udev->descriptor.idVendor),
            le16_to_cpu(udev->descriptor.idProduct));
    // 解析 Video Class 控制描述符
    if (uvc_parse_control(dev) < 0) {
        uvc_trace(UVC_TRACE_PROBE, "Unable to parse UVC "
            "descriptors.\n");
        goto error;
    }
    // 打印 UVC 设备信息
    uvc_printk(KERN_INFO, "Found UVC %u.%02x device %s (%04x:%04x)\n",
        dev->uvc_version >> 8, dev->uvc_version & 0xff,
        udev->product ? udev->product : "<unnamed>",
        le16_to_cpu(udev->descriptor.idVendor),
        le16_to_cpu(udev->descriptor.idProduct));
if (dev->quirks != id->driver_info) {
        // 如果设备 quirks 不等于驱动程序信息,则打印信息
        uvc_printk(KERN_INFO, "Forcing device quirks to 0x%x by module "
            "parameter for testing purpose.\n", dev->quirks);
        // 打印信息
        uvc_printk(KERN_INFO, "Please report required quirks to the "
            "linux-uvc-devel mailing list.\n");
    }
    /* Register the media and V4L2 devices. */
#ifdef CONFIG_MEDIA_CONTROLLER
    // 设置 media 设备的信息
    dev->mdev.dev = &intf->dev;
    strlcpy(dev->mdev.model, dev->name, sizeof(dev->mdev.model));
    if (udev->serial)
        strlcpy(dev->mdev.serial, udev->serial,
            sizeof(dev->mdev.serial));
    strcpy(dev->mdev.bus_info, udev->devpath);
    dev->mdev.hw_revision = le16_to_cpu(udev->descriptor.bcdDevice);
    dev->mdev.driver_version = LINUX_VERSION_CODE;
    // 注册 media 设备
    if (media_device_register(&dev->mdev) < 0)
        goto error;
    // 设置 v4l2 设备的信息
    dev->vdev.mdev = &dev->mdev;
#endif
// 注册 v4l2 设备
    if (v4l2_device_register(&intf->dev, &dev->vdev) < 0)
        goto error;
    /* Initialize controls. */
    // 初始化控制器
    if (uvc_ctrl_init_device(dev) < 0)
        goto error;
    /* Scan the device for video chains. */
    // 扫描设备的视频链
    if (uvc_scan_device(dev) < 0)
        goto error;
    /* Register video device nodes. */
    // 注册视频设备节点
    if (uvc_register_chains(dev) < 0)
        goto error;
    /* Save our data pointer in the interface data. */
    // 在接口数据中保存数据指针
    usb_set_intfdata(intf, dev);
    /* Initialize the interrupt URB. */
    // 初始化中断 URB
    if ((ret = uvc_status_init(dev)) < 0) {
        uvc_printk(KERN_INFO, "Unable to initialize the status "
            "endpoint (%d), status interrupt will not be "
            "supported.\n", ret);
    }
    // 打印 UVC 设备初始化信息
    uvc_trace(UVC_TRACE_PROBE, "UVC device initialized.\n");
    // 启用 USB 自动挂起
    usb_enable_autosuspend(udev);
    return 0;
error:
    // 注销视频设备
    uvc_unregister_video(dev);
    return -ENODEV;
}

该函数是UVC(USB Video Class)驱动中的设备探测函数,用于初始化和注册UVC设备,并进行设备的配置和初始化工作。

概括如下:

  1. 获取USB设备的相关信息,并创建并初始化UVC设备结构体。
  2. 初始化设备的链表、计数器和互斥锁等。
  3. 解析USB设备的描述符,包括Video Class控制描述符。
  4. 打印设备信息和版本号。
  5. 注册媒体设备(如果启用了CONFIG_MEDIA_CONTROLLER)和V4L2设备。
  6. 初始化控制器和控制设备。
  7. 扫描设备以获取视频链。
  8. 注册视频设备节点。
  9. 在接口数据中保存设备指针。
  10. 初始化中断URB(USB Request Block)。
  11. 打印设备初始化信息。
  12. 启用USB自动挂起功能。
  13. 返回初始化成功或失败的状态。
    通常情况下,当插入支持UVC协议的USB摄像头或视频设备时,USB子系统会调用uvc_probe()函数,以便进行UVC设备的初始化、配置和注册。
// 打印 UVC 设备信息
uvc_printk(KERN_INFO, "Found UVC %u.%02x device %s (%04x:%04x)\n",
    dev->uvc_version >> 8, dev->uvc_version & 0xff,
    udev->product ? udev->product : "<unnamed>",
    le16_to_cpu(udev->descriptor.idVendor),
    le16_to_cpu(udev->descriptor.idProduct));

uvc_register_video

uvc_probe->uvc_register_chains->uvc_register_terms->uvc_register_video

uvc_register_chains

该函数用于注册 UVC 设备中的所有视频链和相关的终端,如果支持 Media Controller,则还会注册相应的实体。这样可以将 UVC 设备在系统中进行识别和管理,并建立视频链与终端之间的关联关系。

/*
 * 注册所有的视频链
 */
static int uvc_register_chains(struct uvc_device *dev)
{
    struct uvc_video_chain *chain;
    int ret;
    // 遍历所有的视频链
    list_for_each_entry(chain, &dev->chains, list) {
        // 注册视频链中的所有终端
        ret = uvc_register_terms(dev, chain);
        if (ret < 0)
            return ret;
#ifdef CONFIG_MEDIA_CONTROLLER
        // 如果支持 media controller,则注册实体
        ret = uvc_mc_register_entities(chain);
        if (ret < 0) {
            uvc_printk(KERN_INFO, "Failed to register entites "
                "(%d).\n", ret);
        }
#endif
    }
    return 0;
}
  1. 遍历所有的视频链。
  2. 对于每个视频链,调用 uvc_register_terms() 函数注册视频链中的所有终端。
  3. 如果系统配置支持 Media Controller(CONFIG_MEDIA_CONTROLLER 宏定义),则调用 uvc_mc_register_entities() 函数注册视频链的实体。
  4. 返回成功。

uvc_register_terms

该函数的作用是将视频链中的流式传输终端与相应的流式传输结构体关联起来,并注册视频设备。通过这样的关联,可以在 UVC 设备中建立起视频链、终端和流式传输之间的关联关系,以便在数据传输和控制过程中进行正确的处理和管理。

static int uvc_register_terms(struct uvc_device *dev,
    struct uvc_video_chain *chain)
{
    struct uvc_streaming *stream;
    struct uvc_entity *term;
    int ret;
    // 遍历视频链中的所有实体
    list_for_each_entry(term, &chain->entities, chain) {
        // 如果实体不是流式传输终端,则跳过
        if (UVC_ENTITY_TYPE(term) != UVC_TT_STREAMING)
            continue;
        // 根据终端 ID 获取流式传输结构体
        stream = uvc_stream_by_id(dev, term->id);
        if (stream == NULL) {
            uvc_printk(KERN_INFO, "No streaming interface found "
                   "for terminal %u.", term->id);
            continue;
        }
        // 将流式传输结构体与视频链关联
        stream->chain = chain;
        // 注册视频设备
        ret = uvc_register_video(dev, stream);
        if (ret < 0)
            return ret;
        // 将视频设备与终端关联
        term->vdev = &stream->vdev;
    }
    return 0;
}
  1. 遍历给定视频链 chain 中的所有实体。
  2. 对于每个实体,如果它不是流式传输终端(UVC_ENTITY_TYPE(term) != UVC_TT_STREAMING),则跳过。
  3. 根据终端的 ID,在设备 dev 中查找对应的流式传输结构体 stream。
  4. 如果找到了流式传输结构体,将其与视频链关联(stream->chain = chain)。
  5. 调用 uvc_register_video() 函数注册视频设备。
  6. 将视频设备与终端关联(term->vdev = &stream->vdev)。
  7. 返回成功。

uvc_register_video

该函数负责初始化和注册视频设备,使其能够在V4L2框架中正常工作,并与特定的流式传输相关联。

static int uvc_register_video(struct uvc_device *dev,
        struct uvc_streaming *stream)
{
    // 初始化视频缓冲区队列
    int ret = uvc_queue_init(&stream->queue, stream->type, !uvc_no_drop_param);
    if (ret)
        return ret;
    // 使用默认的流式传输参数初始化流式传输接口
    ret = uvc_video_init(stream);
    if (ret < 0) {
        uvc_printk(KERN_ERR, "Failed to initialize the device "
            "(%d).\n", ret);
        return ret;
    }
    // 初始化调试文件系统
    uvc_debugfs_init_stream(stream);
    // 在 V4L 中注册设备
    // 我们已经持有对 dev->udev 的引用。在引用被释放之前,视频设备将被注销,因此我们不需要获取另一个引用。
    struct video_device *vdev = &stream->vdev;
    vdev->v4l2_dev = &dev->vdev;
    vdev->fops = &uvc_fops;
    vdev->ioctl_ops = &uvc_ioctl_ops;
    vdev->release = uvc_release;
    vdev->prio = &stream->chain->prio;
    if (stream->type == V4L2_BUF_TYPE_VIDEO_OUTPUT)
        vdev->vfl_dir = VFL_DIR_TX;
    strlcpy(vdev->name, dev->name, sizeof vdev->name);
    // 在调用 video_register_device 之前设置驱动程序数据,否则 uvc_v4l2_open 可能会与我们竞争。
    video_set_drvdata(vdev, stream);
    ret = video_register_device(vdev, VFL_TYPE_GRABBER, -1);
    if (ret < 0) {
        uvc_printk(KERN_ERR, "Failed to register video device (%d).\n",
               ret);
        return ret;
    }
    if (stream->type == V4L2_BUF_TYPE_VIDEO_CAPTURE)
        stream->chain->caps |= V4L2_CAP_VIDEO_CAPTURE;
    else
        stream->chain->caps |= V4L2_CAP_VIDEO_OUTPUT;
    atomic_inc(&dev->nstreams);
    return 0;
}

这个函数在Linux内核uvc_driver驱动中的作用是注册视频设备。它执行以下操作:

  1. 初始化视频缓冲区队列,使用指定的流类型和参数初始化视频队列。
  2. 使用默认的流式传输参数初始化流式传输接口,包括分配和配置相关的数据结构和资源。
  3. 初始化调试文件系统,用于调试和监控该视频流。
  4. 在V4L2框架中注册视频设备。它设置视频设备的各种属性,如设备名称、文件操作函数、IO控制操作函数等。
  5. 设置视频设备的驱动程序数据,以便将其与流式传输相关联。
  6. 调用video_register_device函数,在V4L2框架中注册视频设备。
  7. 更新视频链的功能标志,根据流的类型设置相应的功能标志。
  8. 增加视频设备数量的计数器。

uvc_ioctl_ops

// 定义V4L2的ioctl操作函数集合
const struct v4l2_ioctl_ops uvc_ioctl_ops = {
    .vidioc_querycap = uvc_ioctl_querycap, // 查询设备的能力
    .vidioc_enum_fmt_vid_cap = uvc_ioctl_enum_fmt_vid_cap, // 枚举视频捕获格式
    .vidioc_enum_fmt_vid_out = uvc_ioctl_enum_fmt_vid_out, // 枚举视频输出格式
    .vidioc_g_fmt_vid_cap = uvc_ioctl_g_fmt_vid_cap, // 获取视频捕获格式
    .vidioc_g_fmt_vid_out = uvc_ioctl_g_fmt_vid_out, // 获取视频输出格式
    .vidioc_s_fmt_vid_cap = uvc_ioctl_s_fmt_vid_cap, // 设置视频捕获格式
    .vidioc_s_fmt_vid_out = uvc_ioctl_s_fmt_vid_out, // 设置视频输出格式
    .vidioc_try_fmt_vid_cap = uvc_ioctl_try_fmt_vid_cap, // 尝试设置视频捕获格式
    .vidioc_try_fmt_vid_out = uvc_ioctl_try_fmt_vid_out, // 尝试设置视频输出格式
    .vidioc_reqbufs = uvc_ioctl_reqbufs, // 请求缓冲区
    .vidioc_querybuf = uvc_ioctl_querybuf, // 查询缓冲区
    .vidioc_qbuf = uvc_ioctl_qbuf, // 将缓冲区放入队列
    .vidioc_dqbuf = uvc_ioctl_dqbuf, // 从队列中取出缓冲区
    .vidioc_create_bufs = uvc_ioctl_create_bufs, // 创建缓冲区
    .vidioc_streamon = uvc_ioctl_streamon, // 开始视频流
    .vidioc_streamoff = uvc_ioctl_streamoff, // 停止视频流
    .vidioc_enum_input = uvc_ioctl_enum_input, // 枚举输入
    .vidioc_g_input = uvc_ioctl_g_input, // 获取输入
    .vidioc_s_input = uvc_ioctl_s_input, // 设置输入
    .vidioc_queryctrl = uvc_ioctl_queryctrl, // 查询控制器
    .vidioc_query_ext_ctrl = uvc_ioctl_query_ext_ctrl, // 查询扩展控制器
    .vidioc_g_ctrl = uvc_ioctl_g_ctrl, // 获取控制器
    .vidioc_s_ctrl = uvc_ioctl_s_ctrl, // 设置控制器
    .vidioc_g_ext_ctrls = uvc_ioctl_g_ext_ctrls, // 获取扩展控制器
    .vidioc_s_ext_ctrls = uvc_ioctl_s_ext_ctrls, // 设置扩展控制器
    .vidioc_try_ext_ctrls = uvc_ioctl_try_ext_ctrls, // 尝试设置扩展控制器
    .vidioc_querymenu = uvc_ioctl_querymenu, // 查询菜单
    .vidioc_g_selection = uvc_ioctl_g_selection, // 获取选择
    .vidioc_g_parm = uvc_ioctl_g_parm, // 获取参数
    .vidioc_s_parm = uvc_ioctl_s_parm, // 设置参数
    .vidioc_enum_framesizes = uvc_ioctl_enum_framesizes, // 枚举帧大小
    .vidioc_enum_frameintervals = uvc_ioctl_enum_frameintervals, // 枚举帧间隔
    .vidioc_subscribe_event = uvc_ioctl_subscribe_event, // 订阅事件
    .vidioc_unsubscribe_event = v4l2_event_unsubscribe, // 取消订阅事件
    .vidioc_default = uvc_ioctl_default, // 默认操作
};

结构体 const struct v4l2_ioctl_ops uvc_ioctl_ops 定义了与视频设备 IOCTL 请求相关的函数指针,它的作用是为 UVC(USB Video Class)驱动程序提供对视频设备的控制接口。

该结构体包含了以下函数指针成员:

vidioc_querycap:用于查询设备的能力和属性的回调函数。

vidioc_enum_fmt_vid_cap:用于枚举支持的视频捕获格式的回调函数。

vidioc_g_fmt_vid_cap:用于获取当前视频捕获格式的回调函数。

vidioc_s_fmt_vid_cap:用于设置视频捕获格式的回调函数。

vidioc_reqbufs:用于请求视频缓冲区的回调函数。

vidioc_querybuf:用于查询视频缓冲区信息的回调函数。

vidioc_qbuf:用于将已填充数据的视频缓冲区排队到驱动程序的回调函数。

vidioc_dqbuf:用于从驱动程序中取出已处理数据的视频缓冲区的回调函数。

vidioc_streamon:用于启动视频数据流的回调函数。

vidioc_streamoff:用于停止视频数据流的回调函数。

这些回调函数实现了对视频设备的控制和数据流操作,包括查询设备属性、设置和获取视频格式、请求和查询视频缓冲区信息、数据缓冲的排队和取出,以及启动和停止视频数据流等。通过指定这些函数指针,驱动程序可以处理相应的 IOCTL 请求,与用户空间的应用程序进行交互,并控制视频设备的行为。

uvc_fops

const struct v4l2_file_operations uvc_fops = {
    .owner      = THIS_MODULE, // 指向拥有这个结构体的模块的指针
    .open       = uvc_v4l2_open, // 打开设备文件时调用的函数
    .release    = uvc_v4l2_release, // 关闭设备文件时调用的函数
    .unlocked_ioctl = video_ioctl2, // 处理ioctl命令的函数
#ifdef CONFIG_COMPAT
    .compat_ioctl32 = uvc_v4l2_compat_ioctl32, // 处理32位ioctl命令的函数
#endif
    .read       = uvc_v4l2_read, // 读取设备文件时调用的函数
    .mmap       = uvc_v4l2_mmap, // 映射设备文件到用户空间时调用的函数
    .poll       = uvc_v4l2_poll, // 等待设备文件可读或可写时调用的函数
#ifndef CONFIG_MMU
    .get_unmapped_area = uvc_v4l2_get_unmapped_area, // 获取未映射的内存区域时调用的函数
#endif
};

结构体 const struct v4l2_file_operations uvc_fops 定义了与视频设备文件操作相关的函数指针,它的作用是为 UVC(USB Video Class)驱动程序提供视频设备文件的操作接口。

该结构体包含了以下函数指针成员:

open:用于打开视频设备文件的回调函数。

release:用于关闭视频设备文件的回调函数。

unlocked_ioctl:用于处理视频设备文件的 IOCTL 请求的回调函数。

poll:用于轮询视频设备文件的回调函数,检查文件是否可读或可写。

mmap:用于实现内存映射的回调函数,将视频设备的内存映射到用户空间。

read:用于从视频设备文件中读取数据的回调函数。

write:用于向视频设备文件中写入数据的回调函数。

这些回调函数实现了对视频设备文件的常见操作,例如打开、关闭、读取、写入、控制和轮询等。通过指定这些函数指针,驱动程序可以与用户空间的应用程序进行交互,并实现相应的操作逻辑。

如果文章对您有帮助,点赞👍支持,感谢🤝


目录
相关文章
|
6月前
|
存储 Linux 开发工具
Rockchip系列之浅度分析UART接口系列(1)
Rockchip系列之浅度分析UART接口系列(1)
334 1
|
4月前
|
传感器 编解码 API
【STM32开发入门】温湿度监测系统实战:SPI LCD显示、HAL库应用、GPIO配置、UART中断接收、ADC采集与串口通信全解析
SPI(Serial Peripheral Interface)是一种同步串行通信接口,常用于微控制器与外围设备间的数据传输。SPI LCD是指使用SPI接口与微控制器通信的液晶显示屏。这类LCD通常具有较少的引脚(通常4个:MISO、MOSI、SCK和SS),因此在引脚资源有限的系统中非常有用。通过SPI协议,微控制器可以向LCD发送命令和数据,控制显示内容和模式。
143 0
|
存储 传感器 物联网
STM32(HAL)驱动RFID模块(ATS522)
STM32(HAL)驱动RFID模块(ATS522)
|
存储 缓存 索引
uvc驱动ioctl分析下
uvc驱动ioctl分析下
222 0
|
存储 缓存 流计算
uvc驱动ioctl分析上
uvc驱动ioctl分析上
153 0
|
存储 Linux
uvc驱动中的v4l2
uvc驱动中的v4l2
129 0
UART子系统(十)UART驱动情景分析_read
UART子系统(十)UART驱动情景分析_read
193 2
UART子系统(十)UART驱动情景分析_read
UART子系统(八)UART驱动情景分析_注册
UART子系统(八)UART驱动情景分析_注册
74 1
UART子系统(八)UART驱动情景分析_注册
|
Ubuntu Linux Shell
UART子系统(三) UART属于TTY体系之一
UART子系统(三) UART属于TTY体系之一
236 1
UART子系统(三) UART属于TTY体系之一
UART子系统(九)UART驱动情景分析_open
UART子系统(九)UART驱动情景分析_open
140 0
UART子系统(九)UART驱动情景分析_open