Linux v4l2架构学习总链接

vivi代码

v4l2测试代码

step 5 : 设置缓存

1,申请缓存

struct v4l2_requestbuffers req;req.count = nr_bufs; //缓存数量
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0)

这里会调用到v4l_reqbufs

static int v4l_reqbufs(const struct v4l2_ioctl_ops *ops,struct file *file, void *fh, void *arg)
{struct v4l2_requestbuffers *p = arg;int ret = check_fmt(file, p->type);if (ret)return ret;CLEAR_AFTER_FIELD(p, memory);return ops->vidioc_reqbufs(file, fh, p);
}

这里的 ops->vidioc_reqbufs对应vivi驱动的 vidioc_requbufs

static int vidioc_reqbufs(struct file *file, void *priv,struct v4l2_requestbuffers *p)
{struct vivi_dev *dev = video_drvdata(file);return vb2_reqbufs(&dev->vb_vidq, p);
}

这里就开始分析如何申请缓存了,下面的可能大部分都是贴代码,比较枯燥。但是希望可以从枯燥中学到东西,这样就值得了。

int vb2_reqbufs(struct vb2_queue *q, struct v4l2_requestbuffers *req)
{/** vb2_verify_memory_type* 1. 检测memory的值,需要是下面3种类型才可以*    VB2_MEMORY_MMAP  *    VB2_MEMORY_USERPTR*    VB2_MEMORY_DMABUF*    应用代码中传入的是MMAP,没有问题** 2. type值要等于q->type*    这里需要明白一个问题参数 struct vb2_queue *q是哪里来的?*    根据函数调用知道 q = &dev->vb_vidq*    vivi驱动vivi_create_instance函数中,有这样的代码*            q = &dev->vb_vidq;*            memset(q, 0, sizeof(dev->vb_vidq));*            q->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;*            q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ;*            q->drv_priv = dev;*            q->buf_struct_size = sizeof(struct vivi_buffer);*            q->ops = &vivi_video_qops;*            q->mem_ops = &vb2_vmalloc_memops;*    可以看到 type等于q->type** 3. 对于当前使用MMAP方式*    q->mem_ops成员 alloc put mmap不能为空*    mem_ops 对应 vb2_vmalloc_memops 不为空,满足条件* * 4. q->fileio值要为空*    这里暂时还没有看到赋值,分析的时候默认为空*/        int ret = vb2_verify_memory_type(q, req->memory, req->type);/** 接着分析vb2_core_reqbufs*/return ret ? ret : vb2_core_reqbufs(q, req->memory, &req->count);
}

接着分析vb2_core_reqbufs

int vb2_core_reqbufs(struct vb2_queue *q, enum vb2_memory memory,unsigned int *count)
{unsigned int num_buffers, allocated_buffers, num_planes = 0;unsigned plane_sizes[VB2_MAX_PLANES] = { };int ret;/** streaming表示数据流的状态,分析的时候默认为flase* 对于这个值什么时候true,后面遇到再分析* 对于waiting_in_dqbuf也同样,遇到再分析*/if (q->streaming) {dprintk(1, "streaming active\n");return -EBUSY;}if (q->waiting_in_dqbuf && *count) {dprintk(1, "another dup()ped fd is waiting for a buffer\n");return -EBUSY;}/** if条件满足,只要符合以下3种就可以* 1. 申请的缓冲区个数为0,有人会这么写吗?写0的意义是什么?申请之前申请的buffer???* 2. 有之前申请且现在没有释放的缓冲区* 3. memory类型发生了变化,比如mmap变成了dmabuf*/if (*count == 0 || q->num_buffers != 0 ||(q->memory != VB2_MEMORY_UNKNOWN && q->memory != memory)) {/** We already have buffers allocated, so first check if they* are not in use and can be freed.*/mutex_lock(&q->mmap_lock);/** 对于之前是mmap的方式,需要判断之前申请的buffer是不是在使用中* 对于判断方式后面分析*/                if (q->memory == VB2_MEMORY_MMAP && __buffers_in_use(q)) {mutex_unlock(&q->mmap_lock);dprintk(1, "memory in use, cannot free\n");return -EBUSY;}/** Call queue_cancel to clean up any buffers in the PREPARED or* QUEUED state which is possible if buffers were prepared or* queued without ever calling STREAMON.*//** 释放之前申请的所有的buffer* 释放方法同样后面分析,这里主要分析申请* 主意这里会free所有的buffer* q->num_buffers值会变成0*/__vb2_queue_cancel(q);ret = __vb2_queue_free(q, q->num_buffers);mutex_unlock(&q->mmap_lock);if (ret)return ret;/** In case of REQBUFS(0) return immediately without calling* driver's queue_setup() callback and allocating resources.*//** 申请0个buffer,走到这里好像释放了之前的buffer....*/if (*count == 0)return 0;}/** 这里我们可以骗自己,认为是正常的申请* 比如首次申请,申请buffer也不为0* 给自己减少一些额外的因素*//** Make sure the requested values and current defaults are sane.*//** VB2_MAX_FRAME 值 32* min_buffers_needed vivi驱动中并没有设置,所以为0*/WARN_ON(q->min_buffers_needed > VB2_MAX_FRAME);num_buffers = max_t(unsigned int, *count, q->min_buffers_needed);num_buffers = min_t(unsigned int, num_buffers, VB2_MAX_FRAME);memset(q->alloc_devs, 0, sizeof(q->alloc_devs));/** 记录本次申请的memory类型*/q->memory = memory;/** Ask the driver how many buffers and planes per buffer it requires.* Driver also sets the size and allocator context for each plane.*//** q->ops->queue_setup* 对应vivi_video_qops.queue_setup 也就是queue_setup*/ret = call_qop(q, queue_setup, q, &num_buffers, &num_planes,plane_sizes, q->alloc_devs);if (ret)return ret;}

新的一天,继续写,,,

分析queue_setup

static int queue_setup(struct vb2_queue *vq, unsigned int *nbuffers,unsigned int *nplanes, unsigned int sizes[], struct device* alloc_devs[])
{struct vivi_dev *dev = vb2_get_drv_priv(vq);unsigned long size;size = dev->width * dev->height * 2;/* * *nbuffers 有机会等于0吗?* 应该在调用之前就返回了吧*/if (0 == *nbuffers)*nbuffers = 32;/** vid_limit值为16* vid_limit * 1024 * 1024 = 16Mbytes* 这里加了一个条件,会影响最终可以申请到底buffers的个数* 所以说并不是申请多少就一定返回多少的*/while (size * *nbuffers > vid_limit * 1024 * 1024)(*nbuffers)--;/** 对于这个nplanes 字面意思是平面的个数* 个人的理解* 比如rgb,rgb是连续排放,所以是不是就是1个平面?* 对于YUV422P,Y分量放完了,接着U分量,然后接着V分量,这样的话是3个平面?* 对于YUV422SP,Y分量放完了,UV分量交错放,这样的话是2个平面?* YUV相关的可以看这篇文章 https://www.jianshu.com/p/3e44c2262775* 以上只是猜测,待验证*/*nplanes = 1;sizes[0] = size;/** videobuf2-vmalloc allocator is context-less so no need to set* alloc_ctxs array.*/dprintk(dev, 1, "%s, count=%d, size=%ld\n", __func__,*nbuffers, size);return 0;
}

回到vb2_core_reqbufs接着分析

/** buffer个数,平面个数,平面大小都已经知道了*//* Finally, allocate buffers and video memory */allocated_buffers =__vb2_queue_alloc(q, memory, num_buffers, num_planes, plane_sizes);if (allocated_buffers == 0) {dprintk(1, "memory allocation failed\n");return -ENOMEM;}

跟进 分析__vb2_queue_alloc

static int __vb2_queue_alloc(struct vb2_queue *q, enum vb2_memory memory,unsigned int num_buffers, unsigned int num_planes,const unsigned plane_sizes[VB2_MAX_PLANES])
{unsigned int buffer, plane;struct vb2_buffer *vb;int ret;/** 之前申请的buffer个数加上现在要申请的,总数不能超过VB2_MAX_FRAME* 对于VIDIOC_REQBUFS ioctl 走到这里q->num_buffers值是0* 这里为什么还要有个减q->num_buffers的操作呢?* 其实这是为VIDIOC_CREATE_BUFS ioctl准备的,扩充buffer*//* Ensure that q->num_buffers+num_buffers is below VB2_MAX_FRAME */num_buffers = min_t(unsigned int, num_buffers,VB2_MAX_FRAME - q->num_buffers);for (buffer = 0; buffer < num_buffers; ++buffer) {/** 为每个buffer申请一个struct vb2_buffer结构体,用于管理buffer*//* Allocate videobuf buffer structures */vb = kzalloc(q->buf_struct_size, GFP_KERNEL);if (!vb) {dprintk(1, "memory alloc for buffer struct failed\n");break;}/** VB2_BUF_STATE_DEQUEUED,表示buffer在用户空间控制* 猜测是用户空间发起申请的意思* 填充vb的其他成员信息* index: 每个buffer都有自己的编号*/vb->state = VB2_BUF_STATE_DEQUEUED;vb->vb2_queue = q;vb->num_planes = num_planes;vb->index = q->num_buffers + buffer;vb->type = q->type;vb->memory = memory;/** 这里明显的可以看到,每个buffer还有平面的结构* 平面个数越多,后面申请的内存空间越大*/for (plane = 0; plane < num_planes; ++plane) {vb->planes[plane].length = plane_sizes[plane];vb->planes[plane].min_length = plane_sizes[plane];}/** bufs记录每个vb*/q->bufs[vb->index] = vb;/** MMAP是我们这次分析的重点*//* Allocate video buffer memory for the MMAP type */if (memory == VB2_MEMORY_MMAP) {ret = __vb2_buf_mem_alloc(vb);if (ret) {dprintk(1, "failed allocating memory for buffer %d\n",buffer);q->bufs[vb->index] = NULL;kfree(vb);break;}....

这里要跟进分析 __vb2_buf_mem_alloc

static int __vb2_buf_mem_alloc(struct vb2_buffer *vb)
{struct vb2_queue *q = vb->vb2_queue;void *mem_priv;int plane;int ret = -ENOMEM;/** 针对每一个平面去操作*//** Allocate memory for all planes in this buffer* NOTE: mmapped areas should be page aligned*/for (plane = 0; plane < vb->num_planes; ++plane) {/** 主意这里是PAGE_SIZE对齐的* 比如申请5000byte,最后对齐后大小则是8192byte* 所以说最终申请到的内存大于等于想要申请的内存*/unsigned long size = PAGE_ALIGN(vb->planes[plane].length);/* Did it wrap around? */if (size < vb->planes[plane].length)goto free;mem_priv = call_ptr_memop(vb, alloc,q->alloc_devs[plane] ? : q->dev,q->dma_attrs, size, q->dma_dir, q->gfp_flags);if (IS_ERR_OR_NULL(mem_priv)) {if (mem_priv)ret = PTR_ERR(mem_priv);goto free;}/** 这里mem_priv的类型是struct vb2_vmalloc_buf* 记录每个mem_priv*//* Associate allocator private data with this plane */vb->planes[plane].mem_priv = mem_priv;}return 0;
free:/* Free already allocated memory if one of the allocations failed */for (; plane > 0; --plane) {call_void_memop(vb, put, vb->planes[plane - 1].mem_priv);vb->planes[plane - 1].mem_priv = NULL;}return ret;
}

call_ptr_memop最后解析出来是 vb->vb2_queue->mem_ops->alloc

对应代码 vb2_vmalloc_alloc

static void *vb2_vmalloc_alloc(struct device *dev, unsigned long attrs,unsigned long size, enum dma_data_direction dma_dir,gfp_t gfp_flags)
{struct vb2_vmalloc_buf *buf;buf = kzalloc(sizeof(*buf), GFP_KERNEL | gfp_flags);if (!buf)return ERR_PTR(-ENOMEM);buf->size = size;/** 对于vmalloc_user具体的实现我也不明白* 这里贴一下网上的信息,关键信息如下* 1. 申请一段虚拟地址连续的内存给user space使用* 2. 添加VM_USERMAP的flag,防止将kernel space的数据泄露到user space* 摘自博客:https://blog.csdn.net/tiantao2012/article/details/79285721* 总的来说,这里申请了一个平面大小需要的空间(虚拟地址连续)*/buf->vaddr = vmalloc_user(buf->size);buf->dma_dir = dma_dir;buf->handler.refcount = &buf->refcount;buf->handler.put = vb2_vmalloc_put;buf->handler.arg = buf;if (!buf->vaddr) {pr_debug("vmalloc of size %ld failed\n", buf->size);kfree(buf);return ERR_PTR(-ENOMEM);}/** 设置buffer的引用计数*/refcount_set(&buf->refcount, 1);return buf;
}

这里画个图,记录一下buffer的层次结构

回到__vb2_queue_alloc继续分析

            if (memory == VB2_MEMORY_MMAP) {/** __vb2_buf_mem_alloc上面已经分析过*/ret = __vb2_buf_mem_alloc(vb);if (ret) {dprintk(1, "failed allocating memory for buffer %d\n",buffer);q->bufs[vb->index] = NULL;kfree(vb);break;}/* * 这里主要分析__setup_offset*/__setup_offsets(vb);/** Call the driver-provided buffer initialization* callback, if given. An error in initialization* results in queue setup failure.*/ret = call_vb_qop(vb, buf_init, vb);if (ret) {dprintk(1, "buffer %d %p initialization failed\n",buffer, vb);__vb2_buf_mem_free(vb);q->bufs[vb->index] = NULL;kfree(vb);break;}}

跟进分析 __setup_offset

static void __setup_offsets(struct vb2_buffer *vb)
{struct vb2_queue *q = vb->vb2_queue;unsigned int plane;unsigned long off = 0;/** 对于第一个vb2_buffer,这里不需要处理* 其off值就是为0* 对于index非0的情况,下看完下面的for循环再接着看这个*/if (vb->index) {/** 之前分析过 vb2_buffer是用来管理buffer的,保存在q->bufs* 获取上一个编号的vb2_buffer* prev->num_planes - 1 这里是为了获取上一个vb2_buffer的最后* 一个plane的信息* 再次更新off的值,这个off点值就是当前的vb2_buffer的* planes[0].m.offset的值* 下面的for循环中就是这么直接赋值的*/struct vb2_buffer *prev = q->bufs[vb->index - 1];struct vb2_plane *p = &prev->planes[prev->num_planes - 1];off = PAGE_ALIGN(p->m.offset + p->length);}/** 对于index为0的情况,假设num_planes值为2,也就是2个平面* plane[0].m.offset值为0* 那么plane[1].m.offset应该就是* plane[0].m.offset + PAGE_ALIGN(plane[0].length)* 其实也就是下面的操作,只是先加了,后PAGE_ALIGN** 对于index非0的情况* off的值就是当前buffer的planes[plane].m.offset的值*/for (plane = 0; plane < vb->num_planes; ++plane) {vb->planes[plane].m.offset = off;dprintk(3, "buffer %d, plane %d offset 0x%08lx\n",vb->index, plane, off);off += vb->planes[plane].length;off = PAGE_ALIGN(off);}
}

那么这个offset的作用是什么???暂时没有看出来具体的作用。

回到__vb2_queue_alloc继续分析

            if (memory == VB2_MEMORY_MMAP) {/** __vb2_buf_mem_alloc上面已经分析过*/ret = __vb2_buf_mem_alloc(vb);if (ret) {dprintk(1, "failed allocating memory for buffer %d\n",buffer);q->bufs[vb->index] = NULL;kfree(vb);break;}/* * 上面已经分析*/__setup_offsets(vb);/** Call the driver-provided buffer initialization* callback, if given. An error in initialization* results in queue setup failure.*//** 对应vb->vb2_queue->ops->buf_init* 就是vivi_video_qops.buf_init*/ret = call_vb_qop(vb, buf_init, vb);if (ret) {dprintk(1, "buffer %d %p initialization failed\n",buffer, vb);__vb2_buf_mem_free(vb);q->bufs[vb->index] = NULL;kfree(vb);break;}}

vivi_video_qops.buf_init

static int buffer_init(struct vb2_buffer *vb)
{struct vivi_dev *dev = vb2_get_drv_priv(vb->vb2_queue);BUG_ON(NULL == dev->fmt);return 0;
}

并没有做什么实质性的动作。

到这里分析完了 __vb2_queue_alloc

回到 vb2_core_reqbufs中

int vb2_core_reqbufs(struct vb2_queue *q, enum vb2_memory memory,unsigned int *count)
{...allocated_buffers =__vb2_queue_alloc(q, memory, num_buffers, num_planes, plane_sizes);if (allocated_buffers == 0) {dprintk(1, "memory allocation failed\n");return -ENOMEM;}/** 既然能走到这里说明allocated_buffers这个值不为0* 这里会判断allocated_buffers是不是小于驱动中要求的最小min_buffers_needed* 如果小于的话 将ret给赋值,在这之前ret的值为0*/if (allocated_buffers < q->min_buffers_needed)ret = -ENOMEM;/** 这里的if需要2个条件* 申请到的buffer个数大于驱动中要求的最小min_buffers_needed* 申请到的buffer个数小于用户空间期望的buffer* 这2个要同时满足才会进去* 这里我们可以这样思考,应用期望申请8个buffer,但是由于某些原因只申请到4个* 用户空间对于这4个buffer是否满足不需要驱动关心,应用不满意,本次ioctl后* 直接close就行了* 这就是驱动的事情了,驱动对未申请到足够的buffer个数是否满意*//** Check if driver can handle the allocated number of buffers.*/if (!ret && allocated_buffers < num_buffers) {num_buffers = allocated_buffers;/** num_planes is set by the previous queue_setup(), but since it* signals to queue_setup() whether it is called from create_bufs()* vs reqbufs() we zero it here to signal that queue_setup() is* called for the reqbufs() case.*/num_planes = 0;/** 对实际申请的buffer个数是否可以接受* 接受就返回0,不接受返回非0*/ret = call_qop(q, queue_setup, q, &num_buffers,&num_planes, plane_sizes, q->alloc_devs);if (!ret && allocated_buffers < num_buffers)ret = -ENOMEM;/** Either the driver has accepted a smaller number of buffers,* or .queue_setup() returned an error*/}mutex_lock(&q->mmap_lock);//总感觉这里不对,待验证后确认/** 对于这里为什么不是 q->num_buffers += allocated_buffers;* 我专门提了个patch* 作者回复我VIDIC_REQBUFS ioct会清空所有的buffer* 在这里q->num_buffers原来的值就是0*/q->num_buffers = allocated_buffers;if (ret < 0) {/** Note: __vb2_queue_free() will subtract 'allocated_buffers'* from q->num_buffers.*/__vb2_queue_free(q, allocated_buffers);mutex_unlock(&q->mmap_lock);return ret;}mutex_unlock(&q->mmap_lock);/** Return the number of successfully allocated buffers* to the userspace.*//** 将实际申请到的buffer的个数赋值,用于返回到用户空间*/*count = allocated_buffers;/** is_output的值为0,所以这里变量waiting_for_buffers为为true*/q->waiting_for_buffers = !q->is_output;return 0;}

到此这个ioctl过程分析完了,这个过程中,主要明白了buffer中plane的内存的申请到过程。

遗留的一个问题就是offset的作用是什么???

这部分的应用测试代码如下:

        struct v4l2_requestbuffers req;req.count = 5;req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;req.memory = V4L2_MEMORY_MMAP;if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {printf("Reqbufs fail\n");goto err;}printf("buffer number: %d\n", req.count);

打印信息:

buffer number: 4

为什么是4呢?

vivi驱动代码queue_setup中

 size = dev->width * dev->height * 2;if (0 == *nbuffers)*nbuffers = 32;while (size * *nbuffers > vid_limit * 1024 * 1024)(*nbuffers)--;

之前设置的分辨率是 1920*1080,vid_limit = 16

所以值为 16*1024*1024 / (1920*1080*2) = 4.xxx ,所以最后得到的buffer个数是4

从应用调用vivi驱动分析v4l2 -- 申请缓存(VIDIOC_REQBUFS)相关推荐

  1. v4l2框架—申请缓存(VIDIOC_REQBUFS)

    1.前言 本文对学习V4L2框架缓存管理做一个笔记. 2.v4l2中关于缓存管理的结构体 在v4l2中,使用vb2_queue结构体作为缓存管理的结构体 struct vb2_queue {enum ...

  2. 深入学习Linux摄像头(三)虚拟摄像头驱动分析

    深入学习Linux摄像头系列 深入学习Linux摄像头(一)v4l2应用编程 深入学习Linux摄像头(二)v4l2驱动框架 深入学习Linux摄像头(三)虚拟摄像头驱动分析 深入学习Linux摄像头 ...

  3. 基于全志A64平台v4l2驱动分析

    纪念再一次使用这里,刚好开通好博客,写下近年来的第一篇. 最近要做一个全志A64平台的vfe驱动培训,组织了下v4l2与vfe驱动分析.这里记录下. 全志A64芯片csi部份不自带isp(其实是有带一 ...

  4. linux 串口驱动 atmel_set_mctrl何时调用,linux uart serial使用驱动分析

    uart tty serial 驱动分析 内核版本3.14.23 以atmel为例: 起点: static int __init atmel_serial_init(void) { int ret; ...

  5. 虚拟视频驱动vivi.c分析(linux-3.4.2版本)

    参考韦东山老师视频: 虚拟视频驱动vivi.c分析(linux-3.4.2版本): vivi_init      vivi_create_instance(i); { struct vivi_dev ...

  6. zz-linux-i2c驱动分析am335x框架调用150103d

    //zz//####################################################################### zz-linux-i2c驱动分析am335x ...

  7. I2C驱动分析,好文!!

    登录 | 注册 小雷的学习空间 用硬件包围软件 最终实现软硬通吃 目录视图 摘要视图 订阅 Linux I2C驱动完全分析(二) 标签: clinuxstructalgorithmtable 2011 ...

  8. linux串口驱动分析

    linux串口驱动分析 硬件资源及描写叙述 s3c2440A 通用异步接收器和发送器(UART)提供了三个独立的异步串行 I/O(SIO)port,每一个port都能够在中断模式或 DMA 模式下操作 ...

  9. Android10.0 Binder通信原理(五)-Binder驱动分析

    摘要:本节主要来讲解Android10.0 Binder的驱动层分析 阅读本文大约需要花费35分钟. 文章首发微信公众号:IngresGe 专注于Android系统级源码分析,Android的平台设计 ...

最新文章

  1. Java中实现接口与继承的区别
  2. 2019年首期“医工结合科研创新支持计划”项目申报获批:医工携手 强校强国...
  3. HashMap中ConcurrentModificationException异常解读
  4. mysql自动分区partition_Mysql分区表及自动创建分区Partition
  5. android 自定义打包,android 自定义打包后的app名称
  6. java ipv6校验_JS及java验证 IPV6,IPV4地址的 正则表达式 | 学步园
  7. ios 不同sdk4.3 6.0版本号,关于方法的兼容性的通用方法
  8. 《嵌入式系统开发之道——菜鸟成长日志与项目经理的私房菜》——02-04项目范围(Scope)管理...
  9. Makefile的学习
  10. 一起来学SpringBoot | 第四篇:整合Thymeleaf模板
  11. linux服务器进虚拟机,初次登录 Linux 服务器马上要做的 9 件事|Linux 中国
  12. 深度学习自学(四):NCNN配置openmp-CMAKELIST
  13. python分析视频文件_FLV视频文件格式分析
  14. ad转3d视图快捷键_AD工具快捷键
  15. ARCore之路-环境准备
  16. web前端开发面试题(七)
  17. paip 刮刮卡砸金蛋抽奖概率算法跟核心流程
  18. 电脑连接打印机出现的问题(打印机显示未指定)解决方法
  19. 涨姿势!用深度学习LSTM炒股:对冲基金案例分析
  20. 智能通断器Homekit

热门文章

  1. jQ选择器与常用的方法归纳
  2. 海康威视错误代码0xf_调用海康威视sdk获取车牌号
  3. linux上传图片后无权限访问解决方案
  4. AVR单片机开发6——AVR单片机串口Proteus调试注意事项
  5. 虚拟机怎么装服务器系统教程视频,云服务器装虚拟机教程视频
  6. MFC制作Windows画图程序(二)
  7. DANet(双重注意力融合网络)与CBAM
  8. android手机设置固定dns,手机dns怎么设置 简单几步就搞定
  9. 西门子触摸屏和计算机网络,西门子触摸屏与计算机连接不上
  10. STK。如何规定“视线”的范围