linux 驅動 - v4l2 驅動框架

文章目錄

  • 一、/dev/videoX
    • 1. 查詢設備能力
    • 2. 獲取當前視頻格式
    • 3. 設置視頻格式
    • 4. 申請緩沖區
      • 1) mmap 方式
      • 2) user 分配
    • 5. 查詢緩沖區信息
    • 6. 將緩沖區放入隊
      • 1) fill_vb2_buffer
      • 2) buf_prepare
      • 3) get_userptr
      • 4) buf_init
      • 5) attach_dmabuf 和 map_dmabuf
      • 6) start_streaming
      • 7) 總結
    • 7. 啟動視頻流
      • 1) buf_queue
      • 2) vidioc_streamon
      • 3. vb2_buffer_done
    • 8. 從隊列中取出緩沖
    • 9. mmap 映射內存
    • 10. 停止視頻流
    • 11. 總結
  • 二、實現一個虛擬攝像頭
    • 1. 最簡單的虛擬攝像頭
    • 2. 支持查詢設備能力
    • 3. 支持申請 buffer 的能力
    • 4. 支持查詢緩沖區信息
    • 5. 支持 mmap 功能
    • 6. 支持將隊列放入緩沖區
    • 7. 添加啟動視頻流功能
    • 8. 從隊列中取出緩沖
  • 三、/dev/subdevX
    • 1. subdev 的接口
    • 2. 實現簡單的 subdev 驅動
      • 1) 注冊三個 subdev
      • 2) 注冊 isp 對應的 video
  • 四、media controller
    • 1. media controller 的核心組件
      • 1) media_device
      • 2) entity 組件
      • 3) pad 組件
      • 4) media_link 組件
    • 2. 添加 media controller
      • 1) 給 isp 添加 media entity
      • 2) 給 subdev 添加 media entity
      • 3) 應用程序
      • 4) 驗證結果
  • 五、設備樹 ports 方法
    • 1. 常見接口
      • 1) fwnode_graph_get_port_parent
      • 2) fwnode_graph_get_remote_endpoint
      • 3) fwnode_graph_parse_endpoint
      • 4) fwnode_graph_get_remote_port
      • 5) fwnode_graph_get_port_parent
    • 2. 編程驗證
      • 1) 設備 A 的驗證程序
      • 2) 設備 B 的驗證程序
    • 3. 總結

作者: baron
博客: baron-z.cn

????V4L2(Video for Linux 2) 是 Linux 操作系統中的視頻驅動框架, 它為視頻設備提供了統一的處理接口. 本文基于內核版本 Linux 4.4.94+ 分析 linux 視頻驅動框架. 它包括以下內容.

  • /dev/videoX 視頻流處理框架
  • /dev/v4l-subdevX 的子設備框架
  • Media Controller 框架
  • 設備樹 ports 方法

????/dev/videoX 節點對應的設備后續會稱為 video 設備和 video 框架. /dev/v4l-subdevX 節點對應的設備則稱為 subdev 設備和 subdev 框架. 無論是 video 設備還是 subdev 設備, 都是通過 v4l2_device 進行統一管理. video 設備subdev 設備 的整體框架如下所示.

在這里插入圖片描述

注意: video 指代的是 /dev/videoX 節點對應的設備, 而不是內核中的 video_deivce 數據結構. 這個數據結構無論是 video 設備還是 subdev 設備都用到了.

一、/dev/videoX

????/dev/videoX 設備節點用于攝像頭視頻流的管理, 視頻流數據傳輸, 攝像頭到內存的 dma 傳輸, 視頻流的控制, 啟動停止, 內存分配, 自定義操作等. 它的整體框架如圖所示.

在這里插入圖片描述

????可以看出 video 設備本質就是字符設備. 主要操作就是一堆 ioctl, 主要的宏如下.

ioctl功能含義
VIDIOC_QUERYCAP查詢設備能力(如是否支持視頻輸入/輸出、驅動信息等)
VIDIOC_G_FMT獲取當前視頻格式(分辨率、像素格式等)
VIDIOC_S_FMT設置視頻格式(分辨率、像素格式等)
VIDIOC_REQBUFS申請緩沖區(用于流式 I/O 傳輸)
VIDIOC_QUERYBUF查詢緩沖區信息(如內存地址、大小、狀態等)
VIDIOC_QBUF將緩沖區放入隊列(用于數據采集)
VIDIOC_DQBUF從隊列中取出緩沖區(獲取已采集的數據)
VIDIOC_STREAMON啟動視頻流(開始采集數據)
VIDIOC_STREAMOFF停止視頻流(停止采集數據)
VIDIOC_DEFAULT處理未定義的 ioctl 請求(默認操作)

????對于以上的操作, 對應驅動需要提供三個操作函數 v4l2_ioctl_ops , vb2_ops, 以及 vb2_mem_ops.

1. 查詢設備能力

應用需要返回的數據結構如下.

struct v4l2_capability {__u8    driver[16];    // 驅動模塊的名稱__u8    card[32];      // 視頻設備描述信息, 廠商型號等__u8    bus_info[32];  // 總線信息__u32   version;       // 內核版本號, 通過 KERNEL_VERSION 宏獲取__u32   capabilities;  // 設備的整體功能__u32   device_caps;   // 設備節點的訪問能力__u32   reserved[3];   // 保留
};

對應的 ioctl 宏為 VIDIOC_QUERYCAP

#define VIDEO_DEVICE "/dev/video0"int main() {int fd = open(VIDEO_DEVICE, O_RDWR);// 1. 查詢設備能力struct v4l2_capability cap;ioctl(fd, VIDIOC_QUERYCAP, &cap)...... // 省略部分代碼close(fd);
};

調用流程如下圖

在這里插入圖片描述

對于該接口, 驅動需要實現 vidioc_querycap 接口. 主要用于提供 cap->driver, cap->card, cap->bus_info

const struct v4l2_ioctl_ops xxx_ioctl_ops = {.vidioc_querycap = xxx_querycap,
};

2. 獲取當前視頻格式

應用需要返回的數據結構如下.

/** 使用時,根據 `type` 字段的值來選擇合適的 `fmt` 字段格式:* - 如果 `type` 是 `V4L2_BUF_TYPE_VIDEO_CAPTURE`,則使用 `pix`* - 如果 `type` 是 `V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE`,則使用 `pix_mp`* - 如果 `type` 是 `V4L2_BUF_TYPE_VIDEO_OVERLAY`,則使用 `win`* - 如果 `type` 是 `V4L2_BUF_TYPE_VBI_CAPTURE`,則使用 `vbi`* - 如果 `type` 是 `V4L2_BUF_TYPE_SLICED_VBI_CAPTURE`,則使用 `sliced`* - 如果 `type` 是 `V4L2_BUF_TYPE_SDR_CAPTURE`,則使用 `sdr`* - 如果 `type` 是 `V4L2_BUF_TYPE_META_CAPTURE`,則使用 `meta`* - 如果需要自定義格式,可以使用 `raw_data`*/
struct v4l2_format {__u32    type;union {// 單平面視頻圖像格式(適用于大多數普通攝像頭)struct v4l2_pix_format        pix;// 多平面視頻圖像格式(適用于 YUV420 等多平面格式,如現代攝像頭或視頻編解碼器)struct v4l2_pix_format_mplane pix_mp;// 視頻疊加層格式(定義視頻疊加窗口的位置、混合方式等)struct v4l2_window            win;// 原始 VBI (Vertical Blanking Interval) 格式// 用于捕獲模擬視頻信號中的垂直消隱期數據(如圖文電視)struct v4l2_vbi_format        vbi;// 分片 VBI 格式(結構化的 VBI 數據,按行分片)struct v4l2_sliced_vbi_format sliced;// 軟件定義無線電 (SDR) 格式// 用于定義 SDR 設備的采樣率、調制方式等參數       struct v4l2_sdr_format        sdr;// 原始數據緩沖區(保留字段,用于未來擴展或自定義格式)struct v4l2_meta_format       meta; __u8    raw_data[200];} fmt;
};

對于普通的攝像頭是會用到 v4l2_pix_format. 這里只以這個為例進行說明

// 描述單平面視頻幀的格式(適用于傳統攝像頭)
struct v4l2_pix_format {// 圖像寬度(像素),例如 1920__u32           width;// 圖像高度(像素),例如 1080__u32           height;// 像素格式(四字符編碼,如 V4L2_PIX_FMT_YUYV 表示 YUV422 格式)__u32           pixelformat;// 掃描模式(枚舉 v4l2_field)// V4L2_FIELD_NONE       = 逐行掃描// V4L2_FIELD_INTERLACED = 隔行掃描__u32           field;// 每行像素數據的字節數(包含內存對齊填充)// 計算公式:width * 每像素字節數 + 填充字節// 若為 0 表示無填充(連續存儲)__u32           bytesperline;// 整個圖像數據的總大小(單位:字節)// 典型計算:bytesperline * height__u32           sizeimage;// 顏色空間(枚舉 v4l2_colorspace)// 例如:V4L2_COLORSPACE_SRGB(sRGB 顏色空間)__u32           colorspace;// 私有數據,含義取決于 pixelformat(通常保留使用)__u32           priv;// 格式標志位(V4L2_PIX_FMT_FLAG_* 的位掩碼組合)// 例如:V4L2_PIX_FMT_FLAG_PREMUL_ALPHA(預乘 Alpha 通道)__u32           flags;// 顏色編碼聯合體(根據場景選擇成員)union {// Y'CbCr 編碼標準(如 ITU-R BT.601/709/2020)__u32 ycbcr_enc;// HSV 編碼方式(極少使用場景)__u32 hsv_enc;};// 量化范圍(枚舉 v4l2_quantization)// V4L2_QUANTIZATION_FULL_RANGE = 0-255(PC 范圍)// V4L2_QUANTIZATION_LIM_RANGE  = 16-235(TV 范圍,YUV 默認)__u32           quantization;// 傳輸函數(枚舉 v4l2_xfer_func)// 定義光信號到電信號的轉換曲線(如 sRGB 伽馬曲線)__u32           xfer_func;
};

同樣的應用調用如下.

...... // 省略無關代碼struct v4l2_format fmt;fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;ioctl(fd, VIDIOC_G_FMT, &fmt)
......

調用到驅動的調用鏈如下

在這里插入圖片描述

也就是驅動要實現一個 vidioc_g_fmt_vid_cap 用于填充 v4l2_pix_format 數據結構.

const struct v4l2_ioctl_ops xxx_ioctl_ops = {.vidioc_g_fmt_vid_cap = xxx_fmt_vid_cap,
};

3. 設置視頻格式

    struct v4l2_format new_fmt;new_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;new_fmt.fmt.pix.width = 640;new_fmt.fmt.pix.height = 480;new_fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;new_fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;ioctl(fd, VIDIOC_S_FMT, &new_fmt)

調用流程如下

在這里插入圖片描述

驅動需要實現 vidioc_s_fmt_vid_cap 函數, 用于設置攝像頭格式.

const struct v4l2_ioctl_ops xxx_ioctl_ops = {.vidioc_s_fmt_vid_cap = xxx_fmt_vid_cap,
};

4. 申請緩沖區

????v4l2 支持 4 種分配內存的方式 mmap, user, overlay, dma-buf. 本文只講前兩種常用的, 在內核中使用 vb2_queue 來管理 buffer. 在內核中如下圖所示

在這里插入圖片描述

  • 每一個 vb2_buffer 用于描述一幀圖像
  • 一幀圖像可以由一個或者多個 vb2_plane 組成, (例如可以將 YUV 數據分別存儲于三個 vb2_plane), 大多數情況一個 vb2_plane 里面存放一幀數據
  • 每一個 vb2_plane 有一個 mem_priv 指向一個數據結構, 該結構用于保存實際分配的內存, videobuf2 為我們提供了一個數據結構 vb2_vmalloc_buf.
  • 由于每一幀圖像的數據量是確定的, 分辨率一般是不會變的, 所以為了減少代碼冗余, 使用 plane_sizes 數組用于保存每個 vb2_plane 的數據大小

1) mmap 方式

????用于空間需要用到 v4l2_requestbuffers 用來要分配的內存類型.

struct v4l2_requestbuffers {__u32           count;  // 緩沖區的數量__u32           type;   // 緩沖區的類型, 由 v4l2_buf_type 進行描述__u32           memory; // 內存分配的方式__u32           capabilities;__u32           reserved[1];
};// 可用的 type , 常用的就兩個
enum v4l2_buf_type {V4L2_BUF_TYPE_VIDEO_CAPTURE        = 1,  // 捕獲視頻流數據V4L2_BUF_TYPE_VIDEO_OUTPUT         = 2,  // 輸出視頻流數據.....// 省略/* Deprecated, do not use */V4L2_BUF_TYPE_PRIVATE              = 0x80,
};// memory 的類型有四種
enum v4l2_memory {V4L2_MEMORY_MMAP             = 1, // mmap 內核空間分配V4L2_MEMORY_USERPTR          = 2, // user 用戶空間分配V4L2_MEMORY_OVERLAY          = 3, // 覆蓋模式, 將數據直接映射到顯存V4L2_MEMORY_DMABUF           = 4, // dma-buf 
};

應用的調用方式如下所示

    struct v4l2_requestbuffers req;req.count = 4;req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;req.memory = V4L2_MEMORY_MMAP;ioctl(fd, VIDIOC_REQBUFS, &req);

驅動的調用流程如下.

在這里插入圖片描述

????從調用流程可以知道驅動要實現的接口有 queue_setup

static const struct vb2_ops uvc_queue_qops = {.queue_setup = uvc_queue_setup,......
};

????傳入的 count 被轉換為 num_buffers, 但具體的數量由驅動決定, 以及每一個 vb2_buffer 的數量也是由驅動決定的. 因此 uvc_queue_setup 包含以下內容.

  • 根據傳入的 num_buffers 重新計算 vb2_buffer 的數量.
  • 指定 vb2_plane 的數量, 大部分情況為 1
  • 根據 vb2_plane 的數量, 將每個 vb2_plane 的大小填充到 vb2_queue->plane_sizes 數組中

????可以參考 uvc 驅動的實現 drivers/usb/gadget/function/uvc_queue.c, 這里就不搬代碼了.從上面的圖可以看出 __vb2_queue_alloc 函數也是非常重要的函數我這里就直接附上源碼.

static int __vb2_queue_alloc(struct vb2_queue *q, enum vb2_memory memory,unsigned int num_buffers, unsigned int num_planes)
{unsigned int buffer;struct vb2_buffer *vb;int ret;// 分配 num_buffers 個 vb2_bufferfor (buffer = 0; buffer < num_buffers; ++buffer) {vb = kzalloc(q->buf_struct_size, GFP_KERNEL); // 分配 vb2_bufferif (!vb) {dprintk(1, "memory alloc for buffer struct failed\n");break;}// 設置 buffer 的 state 為已出隊狀態// VB2_BUF_STATE_DEQUEUED: 已出隊, 代表緩沖區已被用戶空間取出,當前由用戶應用程序控制// VB2_BUF_STATE_PREPARING: 準備中, 緩沖還未完全初始化// VB2_BUF_STATE_PREPARED: videobuf 和驅動程序已完成緩沖區的準備,緩沖區可用于硬件操作// VB2_BUF_STATE_QUEUED: 緩沖區已加入 videobuf 隊列,但尚未送至驅動程序處理// VB2_BUF_STATE_REQUEUEING: 緩沖區正在重新提交給驅動程序進行處理// VB2_BUF_STATE_ACTIVE: 緩沖區已經被驅動程序接收,并可能正在用于硬件操作(如視頻采集、編碼等)// VB2_BUF_STATE_DONE: 硬件或驅動已處理完緩沖區,并返回到 videobuf,但尚未被用戶空間取出, 操作完成,等待用戶獲取// VB2_BUF_STATE_ERROR: 緩沖區處理過程中發生錯誤vb->state = VB2_BUF_STATE_DEQUEUED;vb->vb2_queue = q; // 設置所屬的 vb2_queuevb->num_planes = num_planes; // 設置 vb2_plane 的數量vb->index = q->num_buffers + buffer; // 設置 index , 該 index 也是數組標vb->type = q->type;                  // 設置 typevb->memory = memory;                 // 設置內存分配方式if (memory == VB2_MEMORY_MMAP) {// 遍歷 vb2_buffer 的 vb2_plane 為每一個 vb2_plane 分配內存// 分配的內存保存在 mem_priv 數據結構中, 具體內存的分配方式由b->vb2_queue->mem_ops->alloc 提供, vb2_plane 的大小更新到 length 成員變量ret = __vb2_buf_mem_alloc(vb);if (ret) {dprintk(1, "failed allocating memory for ""buffer %d\n", buffer);kfree(vb);break;}// 如果 ops 設置了 buf_init 則調用該函數對 buffer 進一步初始化.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);kfree(vb);break;}}// 將 vb2_buffer 保存到 vb2_queue->bufs[]數組中 中q->bufs[q->num_buffers + buffer] = vb;}// 更新 vb2_buffer 中 vb2_plane 的大小__setup_lengths(q, buffer);// 更新新分配的 vb2_plane.m.offset 的值, 它等于之前分配的 plane 的數據長度加上自己的數據長度. if (memory == VB2_MEMORY_MMAP)__setup_offsets(q, buffer);dprintk(1, "allocated %d buffers, %d plane(s) each\n",buffer, num_planes);return buffer;
}

這個函數的功能如下

  • 分配 num_buffers 個 vb2_buffer , 設置所屬的 vb2_queue, 更新 vb2_plane 的數量 num_planes, 設置數組索引 index, 設置內存分配方式 memory.
  • 如果內存分配方式是 VB2_MEMORY_MMAP, 則遍歷 vb2_buffer 的 vb2_plane 為每一個 vb2_plane 分配內存, 配的內存保存在 mem_priv 數據結構中, 具體內存的分配方式由 b->vb2_queue->mem_ops->alloc 提供
  • 更新 vb2_buffer 中 vb2_plane 的大小即 length 成員變量.
  • 如果內存分配的方式是 VB2_MEMORY_MMAP , 更新新分配的 vb2_plane.m.offset 的值, 它等于之前分配的 vb2_plane 的數據長度加上自己的數據長度. 我們只需要知道 vb2_plane.m.offset 每一個 vb2_plane 都是唯一的, 它也是作為 plane 的索引.

其中驅動需要提供 alloc 函數, 用于分配實現內存分配接口.

const struct vb2_mem_ops vb2_vmalloc_memops = {.alloc      = vb2_vmalloc_alloc,......
}

對于 vb2_mem_ops 我們一般使用默認的 vb2_buffer 提供的默認接口, 這里 alloc 則對應 vb2_vmalloc_alloc.

static void *vb2_vmalloc_alloc(void *alloc_ctx, 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 NULL;buf->size = size; // 設置 buffer 的大小buf->vaddr = vmalloc_user(buf->size); // 分配虛擬內存buf->dma_dir = dma_dir;    // 設置 dma_dirbuf->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 NULL;}atomic_inc(&buf->refcount);return buf;
}

????這個函數就是用于分配內存的, vb2 提供了一個默認的數據結構. 這個數據結構被保存到了 vb2_buffer->planes[plane].mem_priv.

struct vb2_vmalloc_buf {void                  *vaddr; // 用于保存分配的內存的虛擬地址.struct frame_vector   *vec;enum dma_data_direction dma_dir;unsigned long           size;refcount_t              refcount;struct vb2_vmarea_handler   handler;struct dma_buf          *dbuf; // 如果是 dma_buf 則放到這里
};

總結

  1. 驅動需要實現 vb2_ops->queue_setup 用于提供 vb2_buffer 的數量, vb2_plane 的數量和大小.
  2. 驅動需要還需要實現另一個接口 vb2_mem_ops->alloc 用于分配實際的內存空間, 大多數情況, 使用 videobuf2-vmalloc.c 中提供的 vb2_vmalloc_alloc 函數, 當然也可以根據需求自己實現.

2) user 分配

????user 分配和 mmap 分配完全一樣只是不會調用 __vb2_buf_mem_alloc 分配觸發真正的內存分配而已. 因為真正的內存由用戶空間分配, 在 VIDIOC_QBUF 提交 buffer 到內核操作的時候會分配該 __vb2_buf_mem_alloc, 并將轉換后的虛擬地址填充到該結構.

在這里插入圖片描述

5. 查詢緩沖區信息

????查詢緩沖區的作用就是返回前面分配的內存信息, 即返回內核里面的 v4l2_buffer.

......struct v4l2_buffer buf;buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;buf.index = 0;ioctl(fd, VIDIOC_QUERYBUF, &buf);
......

調用流程如下所示

在這里插入圖片描述

????根據 index 返回分配的對應的 v4l2_buffer , 不同內存分配的方式也會返回不同的信息.

  • mmap 分配的內存則會返回 b->m.offset 這個則是 mmap 的索引
  • user 方式則返回 b.m->userptr 指向用戶空間分配的內存
  • dma-buf 方式則返回對應 b.m.fd

6. 將緩沖區放入隊

    struct v4l2_buffer buf;buf.index = 0;buf.length = dev->mem[i].length;buf.m.userptr = (unsigned long)dev->mem[i].start;buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {perror("將緩沖區放入隊列失敗");close(fd);return 1;}

調用流程如下所示

在這里插入圖片描述

????從調用鏈可以看出, 對于 VIDIOC_QBUF 操作就是一些回調函數的實現, 主要需要實現的函數如下所示.

1) fill_vb2_buffer

非必須實現, 該接口用于更新用戶傳入的 vb2_buffer , 即用內核里面的 vb2_buffer 更新用戶空間傳入的 vb2_buffer. 內核為我們提供了一個默認的實現 __fill_vb2_buffer. 當調用內核的 vb2_queue_init 初始化 vb2_queue 時設置.

//drivers/media/common/videobuf2/videobuf2-v4l2.c
static const struct vb2_buf_ops v4l2_buf_ops = {.fill_vb2_buffer    = __fill_vb2_buffer,
};int vb2_queue_init(struct vb2_queue *q)
{......q->buf_ops = &v4l2_buf_ops;......
};

2) buf_prepare

該接口用于驅動對 buffer 在放入鏈表 vb2_queue->queued_lis 前作處理.

static struct vb2_ops uvc_queue_qops = {...... .buf_prepare = uvc_buffer_prepare,......
};static int uvc_buffer_prepare(struct vb2_buffer *vb)
{struct uvc_video_queue *queue = vb2_get_drv_priv(vb->vb2_queue);struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);struct uvc_buffer *buf = container_of(vbuf, struct uvc_buffer, buf);.... // 更新 buffer 狀態buf->state = UVC_BUF_STATE_QUEUED;buf->mem = vb2_plane_vaddr(vb, 0);    // 獲取 buffer 的虛擬地址buf->length = vb2_plane_size(vb, 0);  // 獲取 buffer 的大小if (vb->type == V4L2_BUF_TYPE_VIDEO_CAPTURE)buf->bytesused = 0;elsebuf->bytesused = vb2_get_plane_payload(vb, 0);return 0;
}

????對于 uvc 來說就是更新私有 uvc_buffer 的虛擬地址和 buffer 的大小 buf_prepare 會對 vb2_buffer 做預處理. 它和后面 buf_queue 是兩個相互配合的接口. uvc 和一些常規的驅動, 會在這里拿到 vb2_buffer 的虛擬地址. 還有一些例如 rk 的驅動會在 buf_queue 中拿走虛擬地址. 就是 camer 控制器的圖像傳輸用到的內存地址是從 buf_prepare或者 buf_queue 獲取的. 具體取決于驅動的選擇.

3) get_userptr

????如果需要支持用戶空間分配內存必須實現, 該接口用于將用戶空間的分配的內存地址, 轉換為內核空間的虛擬地址. 內核也提供了默認的實現 vb2_vmalloc_get_userptr, 一般情況初始化 vb2_queue 時設置, 參考 uvc

const struct vb2_mem_ops vb2_vmalloc_memops = {.......get_userptr    = vb2_vmalloc_get_userptr,......
};int uvcg_queue_init(struct uvc_video_queue *queue, enum v4l2_buf_type type,struct mutex *lock)
{int ret;// ......  省略部分代碼queue->queue.ops = &uvc_queue_qops; // 初始化 vb2_ops//......queue->queue.mem_ops = &vb2_vmalloc_memops; // 初始化 vb2_mem_ops//......ret = vb2_queue_init(&queue->queue);// ......return 0;
}

????vb2_vmalloc_get_userptr 會創建 vb2_vmalloc_buf 用來保存內存的內核虛擬地址, 如果是 mmap 則在申請內存操作 VIDIOC_REQBUFS 時分配, 用戶分配則挪到了這里.

static void *vb2_vmalloc_get_userptr(struct device *dev, unsigned long vaddr,unsigned long size,enum dma_data_direction dma_dir)
{struct vb2_vmalloc_buf *buf;struct frame_vector *vec;int n_pages, offset, i;int ret = -ENOMEM;buf = kzalloc(sizeof(*buf), GFP_KERNEL);if (!buf)return ERR_PTR(-ENOMEM);buf->dma_dir = dma_dir;offset = vaddr & ~PAGE_MASK;buf->size = size;// ...... 省略部分代碼if (frame_vector_to_pages(vec) < 0) {buf->vaddr = (__force void *)ioremap_nocache(__pfn_to_phys(nums[0]), size + offset);} else {buf->vaddr = vm_map_ram(frame_vector_pages(vec), n_pages, -1, PAGE_KERNEL);}buf->vaddr += offset;return buf;
}

4) buf_init

????如果一直沒有分配 mem_priv 則會嘗試調用 buf_init, 沒看到有啥驅動用到, 不做分析.

5) attach_dmabuf 和 map_dmabuf

????dma-buf 中用獲取 dma-buf 內存的回調接口. 這里也不詳細描述. 后面可能會補充.

6) start_streaming

????嘗試開始取流, 如果 q->streaming 設置了且檢測到 queued_count 小于 min_buffers_needed, 則嘗試觸發取流操作.

7) 總結

  • VIDIOC_QBUF 參數用于將緩沖區放入隊列, 分三種情況 mmap、 user 和 dma-buf. 分別對應三種內存分配的方式. 不同的方式需要實現的回調不同.
  • 用戶空間只需要提供內存的分配方式, 以及 buf.index. 緩沖區的分配要通過 VIDIOC_REQBUFS 來實現. 簡單的操作就是分配之后用 VIDIOC_QUERYBUF 拿到分配的 vb2_buffer. 用這個 buffer 來實現 VIDIOC_QBUF 操作
  • 內核會更新用戶傳入的 vb2_buffer 信息, 同步為內核的 vb2_buffer.
  • vb->queued_entry 鏈接到 &q->queued_list, 同時增加 q->queued_count 的引用計數. 鏈表 q->queued_list 就是等待處理 buffer 的鏈表.

7. 啟動視頻流

    if (ioctl(fd, VIDIOC_STREAMON, &buf.type) == -1) {perror("啟動視頻流失敗");close(fd);return 1;}

調用流程如下.

在這里插入圖片描述

1) buf_queue

????在啟動視頻流前遍歷需要處理的 buffer, 做啟動前的預處理. 部分驅動在這里獲取 vb2_buffer 的地址給到 carme 控制器.

static struct vb2_ops rkcif_vb2_ops = {// .......wait_prepare = vb2_ops_wait_prepare,// ......
};static void rkcif_buf_queue(struct vb2_buffer *vb)
{// ......for (i = 0; i < fmt->mplanes; i++) {void *addr = vb2_plane_vaddr(vb, i);  // 獲取 vb2_buffer 的虛擬地址if (hw_dev->iommu_en) {struct sg_table *sgt = vb2_dma_sg_plane_desc(vb, i);// 填充到 carmera 控制器的 buffercifbuf->buff_addr[i] = sg_dma_address(sgt->sgl);} else {cifbuf->buff_addr[i] = vb2_dma_contig_plane_dma_addr(vb, i);}}// ......

2) vidioc_streamon

????啟動視頻流的回調接口, 可以直接實現啟動功能, 參考 uvc 驅動的實現

const struct v4l2_ioctl_ops uvc_v4l2_ioctl_ops = {//....vidioc_streamon = uvc_v4l2_streamon,// ...
};

????或者這里使用內核的默認實現 vb2_ioctl_streamon, 然后在實現 start_streaming 作為驅動真正的流啟動接口. 以 vimc 為例, 如下所示.

static const struct v4l2_ioctl_ops vimc_cap_ioctl_ops = {.......vidioc_streamon = vb2_ioctl_streamon,......
};static const struct vb2_ops vimc_cap_qops = {// .......start_streaming    = vimc_cap_start_streaming,// ......
};

????這里可以寫寄存器啟動 sensor 也可以通過 v4l2_subdev_call 接口調用 senor 的 subdev 對應的啟動接口啟動.

3. vb2_buffer_done

????如果 buffer 已經處理完了, 則喚醒 done_wq 將 buffer 返回給用戶空間.

8. 從隊列中取出緩沖

    if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1) {perror("從隊列中取出緩沖區失敗");close(fd);return 1;}

調用流程如下所示.

在這里插入圖片描述

從緩沖區沖取出隊列的關鍵函數是 __vb2_wait_for_done_vb, 他的功能如下

static int __vb2_wait_for_done_vb(struct vb2_queue *q, int nonblocking)
{for (;;) {int ret;if (q->waiting_in_dqbuf) { // 當前有其他進程在等待緩沖區, 則直接返回return -EBUSY;}if (!q->streaming) { // 視頻流停止了也直接返回return -EINVAL;}if (q->error) { // 隊列中有錯誤也直接返回return -EIO;}if (q->last_buffer_dequeued) { // 最后一個緩沖區出隊返回return -EPIPE;}if (!list_empty(&q->done_list)) { // 如果已經有完成的緩沖區退出break;}if (nonblocking) { // 沒有緩沖區直接返回return -EAGAIN;}q->waiting_in_dqbuf = 1; // 設置占用該 vb2_queuecall_void_qop(q, wait_prepare, q); // 回調 vb2_queue->ops->wait_prepare 加鎖// 進入等待, 等待緩沖區準備好.dprintk(3, "will sleep waiting for buffers\n");ret = wait_event_interruptible(q->done_wq,!list_empty(&q->done_list) || !q->streaming ||q->error);call_void_qop(q, wait_finish, q); // 回調 vb2_queue->ops->wait_finish 解鎖q->waiting_in_dqbuf = 0; // 釋放占用}return 0;
}
  • 當前有其他進程在等待緩沖區 q->waiting_in_dqbuf視頻流停止了 !q->streaming隊列中有錯誤q->error最后一個緩沖區已經出隊 q->last_buffer_dequeued都立即返回.
  • 調用 wait_prepare 默認實現為 vb2_ops_wait_prepare 為 vb2_queue 加鎖. 設置 q->waiting_in_dqbuf 表示當前進程占用該 vb2_queue.
  • wait_event_interruptible 進入等待, 當 buffer 準備好會觸發中斷, 在中斷中調用 vb2_buffer_done 喚醒 q->done_wq 隊列返回.
  • 調用 wait_finish 默認實現為 vb2_ops_wait_finish 為 vb2_queue 解鎖. 清除 q->waiting_in_dqbuf 釋放前進程占用 vb2_queue.

9. mmap 映射內存

void *buffer_start;
buffer_start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);

????映射內存就是通過 buf.m.offset 找到對應的 vb2_plane , 然后映射其內存到用戶空間.內核為我們提供了默認實現, 調用流程如下所示.

在這里插入圖片描述

這里貼一下關鍵代碼, 這部分內存主要是內存映射想關, 不再這篇文章過多贅述.

vb2_vmalloc_mmap() -->remap_vmalloc_range() -->  // 這里就把內核的虛擬內存映射到用戶空間了vma->vm_start = (unsigned long)(addr + (pgoff << PAGE_SHIFT));vma->vm_end = vma->vm_start + size;

10. 停止視頻流

if (ioctl(fd, VIDIOC_STREAMOFF, &buf.type) == -1) {perror("停止視頻流失敗");close(fd);return 1;
}

調用流程如下.

在這里插入圖片描述

和啟動流相同, 可以在 ops->vidioc_streamoff 中就直接實現停止操作, 例如 uvc

const struct v4l2_ioctl_ops uvc_v4l2_ioctl_ops = {// .......vidioc_streamoff = uvc_v4l2_streamoff,// ......
};

也可以調用通用的接口 vb2_ioctl_streamoff, 然后在實現 vb2_queue->ops->stop_streaming 接口用于真正的停止操作. 如下所示

static const struct v4l2_ioctl_ops vpif_ioctl_ops = {// .......vidioc_streamoff       = vb2_ioctl_streamoff, // 調通通用接口// ......
};static const struct vb2_ops video_qops = {// .......stop_streaming     = vpif_stop_streaming, // 真正的停止接口// ......
};

11. 總結

????完整的整理整個 /dev/videox 的操作流程可以發現, 其實就是對 vb2_buffer 的操作. 它聚焦于視頻的流處理.下圖完整的展示了一幀圖像的處理流程(這圖對應 mmap). 的這些步驟已近是很精簡的步驟了.

在這里插入圖片描述

知識回顧: 每一幀圖像用一個 vb2_buffer 描述, 一幀圖像可以由一個或者多個 vb2_plane 組成, 每一個 vb2_plane 有一個 mem_priv 指向一個數據結構, 該結構用于保存實際分配的內存, videobuf2 為我們提供了一個數據結構 vb2_vmalloc_buf

驅動需要實現的接口如下:

接口說明
vb2_queue->ops->queue_setup用于確認 vb2_plane 的數量和大小。
vb2_queue->mem_ops->alloc用于分配一幀圖像傳輸需要的內存。
vb2_queue->buf_ops->fill_user_buffer用于將 vb2_buffer 返回給用戶空間。
vb2_queue->ops->buf_prepare獲取前面分配的 vb2_buffer 的 vb2_plane 中虛擬地址,
把它傳給 camera 控制器,或者對 plane 做預處理。
它和 vb2_queue->ops->buf_queue 是二選其一
如果在 buf_queue 中獲取,這里就不用獲取。
vb2_queue->ops->buf_queue獲取前面分配的 vb2_buffer 的 vb2_plane 中虛擬地址,
把它傳給 camera 控制器。
它和 vb2_queue->ops->buf_prepare 是二選其一
如果在 buf_prepare 中獲取,這里就不用獲取。
vb2_queue->ops->start_streaming啟動攝像頭傳輸。如果這里使用 vb2_ioctl_streamon
系統接口則需要實現 vb2_queue->ops->start_streaming
vb2_queue->ops->start_streaming如果前面的 vb2_queue->ops->start_streaming
使用了 vb2_ioctl_streamon 接口則需要實現這個接口。
vb2_queue->mem_ops->mmap用于映射內核虛擬地址到用戶空間
一般使用默認實現 vb2_vmalloc_mmap

二、實現一個虛擬攝像頭

????有了前面的知識, 我們來逐步實現一個支持 mmap 的虛擬攝像頭.為了減少代碼的冗余, 除了第一個程序外, 后續的修改將以 patch 的形式給出. 這樣也方便觀察修改了哪些地方.

1. 最簡單的虛擬攝像頭

????首先是實現一個簡單的攝像頭驅動, 它只需要具備 /dev/videox 節點就行了.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/videodev2.h>
#include <media/v4l2-device.h>static struct v4l2_device v4l2_dev;
static struct video_device vcam_vdev;static int vcam_open(struct file *file) {printk(KERN_INFO "simple_vcam: device opened\n");return 0;
}static int vcam_release(struct file *file) {printk(KERN_INFO "simple_vcam: device closed\n");return 0;
}static const struct v4l2_file_operations vcam_fops = {.owner = THIS_MODULE,.open = vcam_open,.release = vcam_release,
};static int __init vcam_init(void) {int ret;printk(KERN_INFO "simple_vcam: initializing\n");// 當第一個參數為 NULL 時必須設置 namesnprintf(v4l2_dev.name, sizeof(v4l2_dev.name), "%s", "vcam v4l2 dev");ret = v4l2_device_register(NULL, &v4l2_dev);if (ret) {printk(KERN_ERR "simple_vcam: v4l2_device_register failed\n");return ret;}// 初始化  vcam_vdevstrscpy(vcam_vdev.name, "Simple Virtual Camera", sizeof(vcam_vdev.name));vcam_vdev.v4l2_dev = &v4l2_dev; // 必須設置所屬 v4l2_devvcam_vdev.fops = &vcam_fops;    // 設置 fopsvcam_vdev.release = video_device_release_empty; // 必須設置 relase 接口vcam_vdev.vfl_dir = VFL_DIR_RX;vcam_vdev.device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_READWRITE;// 注冊 video device.ret = video_register_device(&vcam_vdev, VFL_TYPE_GRABBER, -1);if (ret) {printk(KERN_ERR "simple_vcam: video_register_device failed\n");v4l2_device_unregister(&v4l2_dev);return ret;}printk(KERN_INFO "simple_vcam: registered video device /dev/video%d\n", vcam_vdev.minor);return 0;
}static void __exit vcam_exit(void) {printk(KERN_INFO "simple_vcam: exiting\n");video_unregister_device(&vcam_vdev);v4l2_device_unregister(&v4l2_dev);
}module_init(vcam_init);
module_exit(vcam_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("baron");
MODULE_DESCRIPTION("Simple virtual camera driver example");

驗證程序

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main() {int fd;// 打開設備 /dev/video9(根據實際情況可能是 /dev/videoX)fd = open("/dev/video9", O_RDWR);if (fd == -1) {perror("打開設備失敗");return -1;}printf("設備已打開成功!\n");// 關閉設備close(fd);printf("設備已關閉。\n");return 0;
}

驗證結果:

console:/cache # ./mytest
[ 1777.977139] simple_vcam: device opened
camera open
camera close
console:/cache # [ 1777.977359] simple_vcam: device closed

2. 支持查詢設備能力

驅動修改如下

Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -6,6 +6,7 @@#include <linux/uaccess.h>#include <linux/videodev2.h>#include <media/v4l2-device.h>
+#include <media/v4l2-ioctl.h>static struct v4l2_device v4l2_dev;static struct video_device vcam_vdev;
@@ -20,8 +21,34 @@ static int vcam_release(struct file *filreturn 0;}+// 驅動需要實現 querycap 用于返回 video 信息
+// cap->driver:   設備的驅動信息
+// cap->card:     設備的名稱
+// cap->bus_info: 設備的總線信息
+static int vcam_v4l2_querycap(struct file *file, void *fh, struct v4l2_capability *cap)
+{
+    struct video_device *vdev = video_devdata(file);
+
+    strlcpy(cap->driver, "virtual_vcam", sizeof(cap->driver));
+    strlcpy(cap->card, "Virtual Camera", sizeof(cap->card));
+    strlcpy(cap->bus_info, vdev->name, sizeof(vdev->name));
+
+    cap->device_caps = V4L2_CAP_VIDEO_OUTPUT | V4L2_CAP_STREAMING;
+    cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
+
+    return 0;
+}
+
+const struct v4l2_ioctl_ops vcam_v4l2_ioctl_ops = {
+    .vidioc_querycap = vcam_v4l2_querycap, // 實現 vidioc_querycap 用于支持 VIDIOC_QUERYCAP
+};
+static const struct v4l2_file_operations vcam_fops = {.owner = THIS_MODULE,
+    .unlocked_ioctl = video_ioctl2, // 增加 ioctl 的支持
+#ifdef CONFIG_COMPAT
+    .compat_ioctl32 = video_ioctl2, // 增加 ioctl 的支持
+#endif.open = vcam_open,.release = vcam_release,};
@@ -43,6 +70,7 @@ static int __init vcam_init(void) {strscpy(vcam_vdev.name, "Simple Virtual Camera", sizeof(vcam_vdev.name));vcam_vdev.v4l2_dev = &v4l2_dev; // 必須設置所屬 v4l2_devvcam_vdev.fops = &vcam_fops;    // 設置 fops
+    vcam_vdev.ioctl_ops = &vcam_v4l2_ioctl_ops;vcam_vdev.release = video_device_release_empty; // 必須設置 relase 接口vcam_vdev.vfl_dir = VFL_DIR_RX;vcam_vdev.device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_READWRITE;

應用修改如下:

Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -18,6 +18,17 @@ int main() {printf("camera open\n");+    // 1. 查詢設備能力
+    struct v4l2_capability cap;
+    if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) {
+        perror("查詢設備能力失敗");
+        close(fd);
+        return 1;
+    }
+    printf("card: %s\n", cap.card);
+    printf("driver: %s\n", cap.driver);
+    printf("bus_info: %s\n", cap.bus_info);
+// 關閉設備close(fd);printf("camera close\n");

驗證結果:

console:/cache # ./mytest
[  888.990438] simple_vcam: device opened
camera open
card: Virtual Camera // 打印card 信息
driver: virtual_vcam // 打印 drvier 信息
bus_info: Simple Virtual Camera // 打印總線信息
camera close
console:/cache # [  888.996537] simple_vcam: device closed

3. 支持申請 buffer 的能力

????從前面的文章可以知道 camera 的 buffer 通過 vb2_queue 來管理, 因此我們需要添加對它的支持.

Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -7,9 +7,15 @@#include <linux/videodev2.h>#include <media/v4l2-device.h>#include <media/v4l2-ioctl.h>
+#include <linux/spinlock.h>
+#include <media/videobuf2-core.h>
+#include <media/videobuf2-v4l2.h>
+#include <media/videobuf2-vmalloc.h>static struct v4l2_device v4l2_dev;static struct video_device vcam_vdev;
+static struct vb2_queue vcam_queue;
+static struct mutex vcam_mutex;static int vcam_open(struct file *file) {printk(KERN_INFO "simple_vcam: device opened\n");
@@ -21,7 +27,7 @@ static int vcam_release(struct file *filreturn 0;}-// 驅動需要實現 querycap 用于返回 video 信息
+//  用于支持查詢設備的能力// cap->driver:   設備的驅動信息// cap->card:     設備的名稱// cap->bus_info: 設備的總線信息
@@ -39,8 +45,41 @@ static int vcam_v4l2_querycap(struct filreturn 0;}+// 分配 buffer 時驅動的回調接口, 這里使用通用的 vb2_reqbufs 實現
+// 虛擬攝像頭這里就隨便填寫了.
+static int vcam_v4l2_reqbufs(struct file *file, void *fh, struct v4l2_requestbuffers *b)
+{
+    int ret;
+
+    if (b->type != vcam_queue.type)
+        return -EINVAL;
+
+    printk("%s\n", __func__);
+
+    ret = vb2_reqbufs(&vcam_queue, b);
+
+    return ret ? ret : b->count;
+}
+
+// V4L2_BUF_TYPE_VIDEO_CAPTURE 必須實現這個接口用于返回支持的視頻格式.
+static int vcam_v4l2_g_fmt_vid_cap(struct file *file, void *fh, struct v4l2_format *f)
+{
+    f->fmt.pix.width        = 400;
+    f->fmt.pix.height       = 800;
+    f->fmt.pix.pixelformat  = V4L2_PIX_FMT_YUYV;
+    f->fmt.pix.field        = V4L2_FIELD_NONE;
+    f->fmt.pix.bytesperline = f->fmt.pix.width * 2;
+    f->fmt.pix.sizeimage    = f->fmt.pix.height * f->fmt.pix.bytesperline;
+    f->fmt.pix.colorspace   = V4L2_COLORSPACE_SRGB;
+    f->fmt.pix.priv         = 0;
+
+    return 0;
+}
+const struct v4l2_ioctl_ops vcam_v4l2_ioctl_ops = {
-    .vidioc_querycap = vcam_v4l2_querycap, // 實現 vidioc_querycap 用于支持 VIDIOC_QUERYCAP
+    .vidioc_querycap = vcam_v4l2_querycap,           // 用于返回設備信息
+    .vidioc_reqbufs = vcam_v4l2_reqbufs,             // 用于支持分配 vb2_buffer
+    .vidioc_g_fmt_vid_cap = vcam_v4l2_g_fmt_vid_cap, // 用于支持返回驅動支持的視頻格式};static const struct v4l2_file_operations vcam_fops = {
@@ -53,6 +92,48 @@ static const struct v4l2_file_operations.release = vcam_release,};+// 用于設置 vb2_plane 的數量和大小
+static int vcam_queue_setup(struct vb2_queue *vq,
+               unsigned int *nbuffers, unsigned int *nplanes,
+               unsigned int sizes[], struct device *alloc_devs[])
+{
+    printk("%s\n", __func__);
+    *nplanes = 1;   // 設置數量
+    sizes[0] = 800*400; // 設置大小
+    return 0;
+}
+
+static void vcam_buffer_queue(struct vb2_buffer *vb)
+{
+    printk("%s\n", __func__);
+}
+
+static struct vb2_ops vcam_queue_qops = {
+    .queue_setup = vcam_queue_setup, // 用于設置 vb2_plane 的數量和大小
+    .buf_queue = vcam_buffer_queue,  // 必須實現
+};
+
+// 初始化 vb2_queue
+int vcam_queue_init(struct vb2_queue *queue, enum v4l2_buf_type type,
+            struct mutex *lock)
+{
+    int ret;
+
+    queue->type = type;
+    queue->io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
+    queue->ops = &vcam_queue_qops;
+    queue->lock = lock;
+    queue->mem_ops = &vb2_vmalloc_memops;
+    queue->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC
+                     | V4L2_BUF_FLAG_TSTAMP_SRC_EOF;
+
+    ret = vb2_queue_init(queue);
+    if (ret)
+        return ret;
+
+    return 0;
+}
+static int __init vcam_init(void) {int ret;@@ -83,6 +164,9 @@ static int __init vcam_init(void) {return ret;}+    // 初始化 vb2_queue
+    vcam_queue_init(&vcam_queue, V4L2_BUF_TYPE_VIDEO_CAPTURE, &vcam_mutex);
+printk(KERN_INFO "simple_vcam: registered video device /dev/video%d\n", vcam_vdev.minor);return 0;}

應用程序修改.

Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -29,6 +29,19 @@ int main() {printf("driver: %s\n", cap.driver);printf("bus_info: %s\n", cap.bus_info);+   // 2. 申請緩沖區
+    struct v4l2_requestbuffers req;
+    req.count = 3;
+    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    req.memory = V4L2_MEMORY_MMAP;
+    if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
+        perror("申請緩沖區失敗");
+        close(fd);
+        return 1;
+    }
+
+   printf("alloc 3 buffer\n");
+// 關閉設備close(fd);printf("camera close\n");

驗證結果:

console:/cache # ./mytest
[  785.535608] simple_vcam: device opened
camera open
card: Virtual Camera
driver: virtual_vcam
bus_info: Simple Virtual Camera
alloc 3 buffer
camera close
[  785.541709] vcam_v4l2_reqbufs
[  785.541882] vcam_queue_setup
[  785.543306] simple_vcam: device closed

4. 支持查詢緩沖區信息

????這個驅動需要添加對 vidioc_querybuf 的支持.

Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -76,10 +76,17 @@ static int vcam_v4l2_g_fmt_vid_cap(strucreturn 0;}+// 添加驅動用于支持查詢緩沖區信息的功能
+static int vcam_v4l2_querybuf(struct file *file, void *fh, struct v4l2_buffer *b)
+{
+    return vb2_querybuf(&vcam_queue, b);
+}
+const struct v4l2_ioctl_ops vcam_v4l2_ioctl_ops = {.vidioc_querycap = vcam_v4l2_querycap,           // 用于返回設備信息.vidioc_reqbufs = vcam_v4l2_reqbufs,             // 用于支持分配 vb2_buffer.vidioc_g_fmt_vid_cap = vcam_v4l2_g_fmt_vid_cap, // 用于支持返回驅動支持的視頻格式
+   .vidioc_querybuf = vcam_v4l2_querybuf,           // 添加驅動用于支持查詢緩沖區信息的功能};static const struct v4l2_file_operations vcam_fops = {

應用程序

Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -42,6 +42,18 @@ int main() {printf("alloc 3 buffer\n");+    // 3. 查詢緩沖區信息
+    struct v4l2_buffer buf;
+    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    buf.memory = V4L2_MEMORY_MMAP;
+    buf.index = 0;
+    if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) {
+        perror("查詢緩沖區信息失敗");
+        close(fd);
+        return 1;
+    }
+    printf("緩沖區大小: %u bytes\n", buf.length);
+// 關閉設備close(fd);printf("camera close\n");

驗證結果

console:/cache # ./mytest
[  182.823177] simple_vcam: device opened
camera open
card: Virtual Camera
driver: virtual_vcam
bus_info: Simple Virtual Camera
緩沖區大小: 320000 bytes // 計算一下 800x400 = 320000
alloc 3 buffer
camera close
[  182.829223] vcam_v4l2_reqbufs
[  182.829240] vcam_queue_setup
[  182.833253] simple_vcam: device closed

5. 支持 mmap 功能

Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -89,13 +89,20 @@ const struct v4l2_ioctl_ops vcam_v4l2_io.vidioc_querybuf = vcam_v4l2_querybuf,           // 添加驅動用于支持查詢緩沖區信息的功能};+// 添加對 mmap 的支持
+static int vcam_v4l2_mmap(struct file *file, struct vm_area_struct *vma)
+{
+    return vb2_mmap(&vcam_queue, vma);
+}
+static const struct v4l2_file_operations vcam_fops = {.owner = THIS_MODULE,
+    .open = vcam_open,.unlocked_ioctl = video_ioctl2, // 增加 ioctl 的支持#ifdef CONFIG_COMPAT.compat_ioctl32 = video_ioctl2, // 增加 ioctl 的支持#endif
-    .open = vcam_open,
+    .mmap = vcam_v4l2_mmap,         // 添加對 mmap 的支持.release = vcam_release,};

應用程序:

Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -5,6 +5,7 @@#include <sys/ioctl.h>#include <linux/videodev2.h>#include <errno.h>
+#include <sys/mman.h>int main() {int fd;
@@ -54,6 +55,16 @@ int main() {}printf("緩沖區大小: %u bytes\n", buf.length);+    // 4. mmap 映射內存
+    void *buffer_start;
+    buffer_start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
+    if (buffer_start == MAP_FAILED) {
+        perror("mmap");
+        exit(EXIT_FAILURE);
+    }
+
+    printf("mmap camera bufer %p\n", buffer_start);
+// 關閉設備close(fd);printf("camera close\n");

驗證結果:

console:/cache # ./mytest
[   55.868859] simple_vcam: device opened
camera open
card: Virtual Camera
driver: virtual_vcam
bus_info: Simple Virtual Camera
alloc 3 buffer
緩沖區大小: 320000 bytes
mmap camera bufer 0x78780ac000  // 映射內存
camera close
[   55.874932] vcam_v4l2_reqbufs
[   55.874953] vcam_queue_setup
[   55.884138] simple_vcam: device closed

6. 支持將隊列放入緩沖區

????我們在將 buffer 放入緩沖區前, 先給他寫入 “hello vcam this is user” 然后在內核中打印出我們的修改.

Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -16,6 +16,8 @@ static struct v4l2_device v4l2_dev;static struct video_device vcam_vdev;static struct vb2_queue vcam_queue;static struct mutex vcam_mutex;
+static void *vcam_mem;
+static unsigned int length;static int vcam_open(struct file *file) {printk(KERN_INFO "simple_vcam: device opened\n");
@@ -87,6 +89,7 @@ const struct v4l2_ioctl_ops vcam_v4l2_io.vidioc_reqbufs = vcam_v4l2_reqbufs,             // 用于支持分配 vb2_buffer.vidioc_g_fmt_vid_cap = vcam_v4l2_g_fmt_vid_cap, // 用于支持返回驅動支持的視頻格式.vidioc_querybuf = vcam_v4l2_querybuf,           // 添加驅動用于支持查詢緩沖區信息的功能
+    .vidioc_qbuf = vb2_ioctl_qbuf,                   // 用于支持將緩沖區放入隊列};// 添加對 mmap 的支持
@@ -122,9 +125,23 @@ static void vcam_buffer_queue(struct vb2printk("%s\n", __func__);}+
+// 獲取前面分配的 buffer 的虛擬地址
+// 打印出用戶的修改, 這里應該能打印 "hello vcam this is user"
+static int vcam_buffer_prepare(struct vb2_buffer *vb)
+{
+    vcam_mem = vb2_plane_vaddr(vb, 0);
+    length = vb2_plane_size(vb, 0);
+
+    printk("vcam_mem: %s, length:%d\n", (char*)vcam_mem, length);
+
+   return 0;
+}
+static struct vb2_ops vcam_queue_qops = {.queue_setup = vcam_queue_setup, // 用于設置 vb2_plane 的數量和大小.buf_queue = vcam_buffer_queue,  // 必須實現
+    .buf_prepare = vcam_buffer_prepare, // 用于獲取前面分配的 buffer 的虛擬地址};// 初始化 vb2_queue
@@ -179,6 +196,7 @@ static int __init vcam_init(void) {}// 初始化 vb2_queue
+   vcam_vdev.queue = &vcam_queue; // vb2_ioctl_qbuf 需要把 queue 綁定到 video_devicevcam_queue_init(&vcam_queue, V4L2_BUF_TYPE_VIDEO_CAPTURE, &vcam_mutex);printk(KERN_INFO "simple_vcam: registered video device /dev/video%d\n", vcam_vdev.minor);

應用程序修改

Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -6,6 +6,7 @@#include <linux/videodev2.h>#include <errno.h>#include <sys/mman.h>
+#include <string.h>int main() {int fd;
@@ -65,6 +66,18 @@ int main() {printf("mmap camera bufer %p\n", buffer_start);+    // 往映射內存中寫入數據
+    const char *msg = "hello vcam this is user";
+    memcpy(buffer_start, msg, strlen(msg) + 1);
+
+   printf("write %s to kernel\n", (char*)buffer_start);
+
+   if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
+       perror("將緩沖區放入隊列失敗");
+       close(fd);
+       return 1;
+   }
+// 關閉設備close(fd);printf("camera close\n")

驗證結果:

console:/cache # ./mytest
[   33.442391] simple_vcam: decvaimceer ao poepneendcard: Virtual Camera
driver: virtual_vcam
bus_info: Simple Virtual Camera
alloc 3 buffer
緩沖區大小: 320000 bytes
mmap camera bufer 0x7ef2c21000
write hello vcam this is user to kernel // 應用打印
camera close
[   33.448609] vcam_queue_setup
[   33.458952] vcam_mem: hello vcam this is user, length:320000 // 在內核中打印出應用寫入的數據.
[   33.461500] simple_vcam: device closed

7. 添加啟動視頻流功能

????添加啟動視頻流的功能, 并且在啟動的時候, 模擬 sensor 處理完視頻并提交這幀. 模擬的方式很簡單就是往 mem 中寫入 “hello vcam this is kernel”

Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -90,6 +90,7 @@ const struct v4l2_ioctl_ops vcam_v4l2_io.vidioc_g_fmt_vid_cap = vcam_v4l2_g_fmt_vid_cap, // 用于支持返回驅動支持的視頻格式.vidioc_querybuf = vcam_v4l2_querybuf,           // 添加驅動用于支持查詢緩沖區信息的功能.vidioc_qbuf = vb2_ioctl_qbuf,                   // 用于支持將緩沖區放入隊列
+   .vidioc_streamon = vb2_ioctl_streamon,                             // 使用標準的 vb2_ioctl_streamon 接口};// 添加對 mmap 的支持
@@ -138,10 +139,36 @@ static int vcam_buffer_prepare(struct vbreturn 0;}+// 直接在 steram on 中 模擬數據采集完成
+static int vcam_start_streaming(struct vb2_queue *vq, unsigned int count)
+{
+   struct vb2_buffer *vb;
+   void *mem;
+
+    if (list_empty(&vq->queued_list)) {
+        printk("vcam: queued_list is empty, cannot start streaming\n");
+        return 0;
+    }
+
+   vb = list_first_entry(&vq->queued_list, struct vb2_buffer, queued_entry);
+
+    // 模擬采集過程, 這里直接修改 buffer 內容
+    mem = vb2_plane_vaddr(vb, 0);
+   memcpy(mem, "hello vcam this is kernel", strlen("hello vcam this is kernel") + 1);
+
+   // 采集完成將 buffer 放到 vb2_queue->done_list 鏈表
+    vb2_buffer_done(vb, VB2_BUF_STATE_DONE); // 將 buffer 放到完成鏈表
+
+   printk("%s change mem to %s\n", __func__, (char*)mem);
+
+   return 0;
+}
+static struct vb2_ops vcam_queue_qops = {.queue_setup = vcam_queue_setup, // 用于設置 vb2_plane 的數量和大小.buf_queue = vcam_buffer_queue,  // 必須實現.buf_prepare = vcam_buffer_prepare, // 用于獲取前面分配的 buffer 的虛擬地址
+   .start_streaming = vcam_start_streaming, // vb2_ioctl_streamon 的后續接口, 這個是真正的 stream on 接口};

應用修改:

Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -72,12 +72,22 @@ int main() {printf("write %s to kernel\n", (char*)buffer_start);+   // 5. 將緩沖區放入隊列if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {perror("將緩沖區放入隊列失敗");close(fd);return 1;}+    // 6. 啟動視頻流
+    if (ioctl(fd, VIDIOC_STREAMON, &buf.type) == -1) {
+        perror("啟動視頻流失敗");
+        close(fd);
+        return 1;
+    }
+
+    printf("vcam stream on\n");
+// 關閉設備close(fd);printf("camera close\n");

驗證結果:

console:/cache # ./mytest
[   51.017063] simple_vcam: device opened
camera open
card: Virtual Camera
driver: virtual_vcam
bus_info: Simple Virtual Camera
alloc 3 buffer
緩沖區大小: 320000 bytes
mmap camera bufer 0x7a81864000
write hello vcam this is user to kernel
vcam stream on // 啟動攝像頭
camera close
[   51.023179] vcam_queue_setup
[   51.033537] vcam_mem: hello vcam this is user, length:320000
[   51.033591] vcam_start_streaming change mem to hello vcam this is kernel // 將 mem 的內容該成  hello vcam this is kernel
[   51.037391] simple_vcam: device closed

8. 從隊列中取出緩沖

Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -91,6 +91,7 @@ const struct v4l2_ioctl_ops vcam_v4l2_io.vidioc_querybuf = vcam_v4l2_querybuf,           // 添加驅動用于支持查詢緩沖區信息的功能.vidioc_qbuf = vb2_ioctl_qbuf,                   // 用于支持將緩沖區放入隊列.vidioc_streamon = vb2_ioctl_streamon,                             // 使用標準的 vb2_ioctl_streamon 接口
+   .vidioc_dqbuf = vb2_ioctl_dqbuf,             // 使用標準的 vb2_ioctl_dqbuf  接口};// 添加對 mmap 的支持
@@ -169,6 +170,8 @@ static struct vb2_ops vcam_queue_qops =.buf_queue = vcam_buffer_queue,  // 必須實現.buf_prepare = vcam_buffer_prepare, // 用于獲取前面分配的 buffer 的虛擬地址.start_streaming = vcam_start_streaming, // vb2_ioctl_streamon 的后續接口, 這個是真正的 stream on 接口
+   .wait_prepare = vb2_ops_wait_prepare, // 使用標準接口加鎖
+   .wait_finish = vb2_ops_wait_finish,   // 使用標準接口解鎖};// 初始化 vb2_queue

應用修改

Index: test/test.c
===================================================================
--- test.orig/test.c
+++ test/test.c
@@ -88,6 +88,14 @@ int main() {printf("vcam stream on\n");+    if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
+        perror("從隊列中取出緩沖區失敗");
+        close(fd);
+        return 1;
+    }
+
+   printf("%s\n", (char*)buffer_start);
+// 關閉設備close(fd);printf("camera close\n");

驗證結果:

console:/cache # ./mytest
[   99.390015] simple_vcam: device opened
camera open
card: Virtual Camera
driver: virtual_vcam
bus_info: Simple Virtual Camera
alloc 3 buffer
緩沖區大小: 320000 bytes
mmap camera bufer 0x741ca9f000
write hello vcam this is user to kernel
vcam stream on
hello vcam this is kernel // 內核讀出處理過后的 buffer
camera close
[   99.396111] vcam_queue_setup
[   99.406462] vcam_mem: hello vcam this is user, length:320000
[   99.406512] vcam_start_streaming change mem to hello vcam this is kernel // 修改 buffer
[   99.412631] simple_vcam: device closed

三、/dev/subdevX

????/dev/v4l-subd1evX 設備, 用于管理和配資子設備, sensor 的寄存器初始化等操作、 isp (Image Signal Processor)、視頻編碼器/解碼器、橋接芯片(bridge). 他和 video 設備配合使用.

// 可以是一對一的
攝像頭設備(/dev/videoX)│└── subdevX (sensor)// 也可以是一對多的
攝像頭設備(/dev/videoX)│├── subdev0 (Sensor傳感器)├── subdev1 (ISP處理芯片)└── subdev2 (鏡頭控制器,VCM模塊)

????/dev/v4l-subd1evX 設備本質上也是通過一個字符設備的一系列 ioctl 來控制的. 為什么要單獨抽取出來, 而不直接放到流處理部分中呢? 這是為了方便驅動的移植, 因為 /dev/video 流處理部分是通用的, 所有攝像頭都會用到. 為減少代碼冗余, 就將與攝像頭控制器強耦合的部分單獨抽取出來, 這部分由芯片廠家實現. 而 sensor 部分則通過 subdev 獨立實現, 這樣移植新 sensor 時, 只需更換對應的 subdev 即可. 整體框架如下圖所示:

在這里插入圖片描述

從上圖中也可以看出 /dev/videox/dev/subdevxv4l2_device 統一管理.

注: 圖中只畫了一個 subdev 是因為圖片大小限制, 一個vdieo 右邊可以掛多個subdev.

1. subdev 的接口

????注冊的接口有兩個. 第一個接口 v4l2_device_register_subdev 接口很簡單, 用于將 subdev->list 鏈接到所屬的 v4l2_dev->subdevs. 第二個接口 v4l2_device_register_subdev_nodes 接口的功能如下.

  • 遍歷 v4l2_dev->subdevs 上的 subdev 為每一個沒有注冊的設備創建一個 video_device 并注冊進內核.
  • 設置對應的 video_device 的 fops 為 v4l2_subdev_fops, ioctl 的不同就是 /dev/subdev 設備/dev/video 設備的核心差異點.
const struct v4l2_file_operations v4l2_subdev_fops = {.owner = THIS_MODULE,.open = subdev_open,.unlocked_ioctl = subdev_ioctl, // 對于 video 設備這里是 video_ioctl2
#ifdef CONFIG_COMPAT.compat_ioctl32 = subdev_compat_ioctl32,
#endif.release = subdev_close,.poll = subdev_poll,
};
EXPORT_SYMBOL_GPL(v4l2_subdev_fops);

????如下所示為 gc5035 攝像頭的驅動實現.

在這里插入圖片描述

????到這里我們就可以發現, subdev 的核心就是一堆回調函數.

struct v4l2_subdev_ops {const struct v4l2_subdev_core_ops   *core;   // 核心接口上下電等const struct v4l2_subdev_tuner_ops  *tuner;const struct v4l2_subdev_audio_ops  *audio;const struct v4l2_subdev_video_ops  *video;  // 視頻相關的接口, 啟動停止視頻流等const struct v4l2_subdev_vbi_ops    *vbi;const struct v4l2_subdev_ir_ops     *ir;const struct v4l2_subdev_sensor_ops *sensor;const struct v4l2_subdev_pad_ops    *pad;   // pad 相關接口支持的分變率等.
};

????sensor 支持什么就添加什么接口, 然后通過 v4l2_subdev_call 調用該接口. 驅動可以直接通過 v4l2_subdev_call 調用 subdev 支持的接口.

#define v4l2_subdev_call(sd, o, f, args...)             \({                              \int __result;                       \if (!(sd))                      \__result = -ENODEV;             \else if (!((sd)->ops->o && (sd)->ops->o->f))        \__result = -ENOIOCTLCMD;            \else                            \__result = (sd)->ops->o->f((sd), ##args);   \__result;                       \})

例如我們想要調用上圖中的 gc5035_s_stream 啟動視頻流接口. 我們先看驅動的實現.

static const struct v4l2_subdev_video_ops gc5035_video_ops = {.s_stream = gc5035_s_stream, // 對應的 s_stream 接口
};static const struct v4l2_subdev_ops gc5035_subdev_ops = {.vido   = &gc5035_core_ops, // 對應的 video 接口
};

于是我們就可以使用以下接口調用 gc5035_s_stream

v4l2_subdev_call(gc5035_subdev, video, s_stream, 1); // 傳入 1 啟動視頻流

subdev 總體上來說比較簡單

  • 首先通過 v4l2_device_register_subdev 添加到所屬的 v4l2_dev
  • 然后使用 v4l2_device_register_subdev_nodes 注冊 v4l2_dev 上的所有 subdev.
  • 最后通過 v4l2_subdev_call 調用 subdev 提供的接口.

2. 實現簡單的 subdev 驅動

????假設我們有以下攝像頭硬件鏈路, 有兩個sensor, sensor_Asensor_B. 數據通過 csi 控制器后輸入到 isp.

在這里插入圖片描述

????如果我們就需要實現三個 subdev 驅動, 分別用于描述 sensor_AsensorBcsi 控制器.

1) 注冊三個 subdev

????注冊 sensor_AsensorBcsi 控制器. 對應的 subdev, 本文的目的只是為了展示原理, 代碼做錯誤處理, 以及只保留一個最簡單的功能. 正常情況下分別對應三個驅動程序, 這里我就放到一起了.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/videodev2.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <linux/spinlock.h>
#include <media/videobuf2-core.h>
#include <media/videobuf2-v4l2.h>
#include <media/videobuf2-vmalloc.h>
#include <media/media-entity.h>extern struct v4l2_device my_v4l2_dev;static int sensor_s_stream(struct v4l2_subdev *sd, int on)
{printk("%s: %s stream %s\n", __func__, sd->name, on ? "on" : "off");return 0;
}static const struct v4l2_subdev_video_ops sensor_video_ops = {.s_stream = sensor_s_stream,
};static struct v4l2_subdev_ops sensor_ops = {.video = &sensor_video_ops,
};struct v4l2_subdev* my_sensor_register(const char *name)
{struct v4l2_subdev *sd;int ret;sd = kzalloc(sizeof(*sd), GFP_KERNEL);v4l2_subdev_init(sd, &sensor_ops);sd->owner = THIS_MODULE;sd->flags = V4L2_SUBDEV_FL_HAS_DEVNODE;strscpy(sd->name, name, sizeof(sd->name));ret = v4l2_device_register_subdev(&my_v4l2_dev, sd);ret = v4l2_device_register_subdev_nodes(&my_v4l2_dev);return sd;
}static int __init sensor_module_init(void)
{struct v4l2_subdev *sensor_A;struct v4l2_subdev *sensor_B;struct v4l2_subdev *csi_control;sensor_A = my_sensor_register("sensor_A");sensor_B = my_sensor_register("sensor_B");csi_control = my_sensor_register("csi_control");return 0;
}module_init(sensor_module_init);MODULE_LICENSE("GPL");
MODULE_AUTHOR("baron");
MODULE_DESCRIPTION("V4L2 subdev example");

2) 注冊 isp 對應的 video

????同樣代碼只做展示, 這這里我只保留了 stream_on 的功能.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/videodev2.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <linux/spinlock.h>
#include <media/videobuf2-core.h>
#include <media/videobuf2-v4l2.h>
#include <media/videobuf2-vmalloc.h>struct v4l2_device my_v4l2_dev;
EXPORT_SYMBOL_GPL(my_v4l2_dev);static struct media_device media_dev;struct video_device isp_vdev;
EXPORT_SYMBOL_GPL(isp_vdev);struct v4l2_subdev *get_remote_sensor(struct media_pad *local)
{struct media_pad *remote;remote = media_entity_remote_pad(local);return media_entity_to_v4l2_subdev(remote->entity);
}
EXPORT_SYMBOL_GPL(get_remote_sensor);static int isp_open(struct file *file)
{printk("simple_isp: device opened\n");return 0;
}static int isp_release(struct file *file)
{printk("simple_isp: device closed\n");return 0;
}static int isp_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
{struct video_device *vdev = video_devdata(file);struct v4l2_subdev * remote_sub;printk("%s\n", __func__);remote_sub = get_remote_sensor(&vdev->entity.pads[0]);v4l2_subdev_call(remote_sub, video, s_stream, 1);return 0;
}const struct v4l2_ioctl_ops isp_v4l2_ioctl_ops = {.vidioc_streamon = isp_v4l2_streamon,
};static const struct v4l2_file_operations isp_fops = {.owner = THIS_MODULE,.open = isp_open,.unlocked_ioctl = video_ioctl2,
#ifdef CONFIG_COMPAT.compat_ioctl32 = video_ioctl2,
#endif.release = isp_release,
};static struct media_pad isp_pads[1] = {{.flags = MEDIA_PAD_FL_SINK,},
};static int __init isp_init(void) {int ret;printk("gsimple_isp: initializing\n");// 當第一個參數為 NULL 時必須設置 namesnprintf(my_v4l2_dev.name, sizeof(my_v4l2_dev.name), "%s", "isp v4l2 dev");ret = v4l2_device_register(NULL, &my_v4l2_dev);if (ret) {printk("simple_isp: v4l2_device_register failed\n");return ret;}// 初始化  isp_vdevstrscpy(isp_vdev.name, "Simple Virtual Camera", sizeof(isp_vdev.name));isp_vdev.v4l2_dev = &my_v4l2_dev; // 必須設置所屬 v4l2_devisp_vdev.fops = &isp_fops;    // 設置 fopsisp_vdev.ioctl_ops = &isp_v4l2_ioctl_ops;isp_vdev.release = video_device_release_empty; // 必須設置 relase 接口isp_vdev.vfl_dir = VFL_DIR_RX;isp_vdev.device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_READWRITE;isp_vdev.entity.function = MEDIA_ENT_F_CAM_SENSOR;ret = media_entity_pads_init(&isp_vdev.entity, 1, isp_pads);my_v4l2_dev.mdev = &media_dev;// 這個要在 video_register_device 前面snprintf(media_dev.model, sizeof(media_dev.model), "My Virtual Media Device");snprintf(media_dev.driver_name, sizeof(media_dev.driver_name), "My Virtual driver");snprintf(media_dev.serial, sizeof(media_dev.serial), "My Virtual serial");snprintf(media_dev.bus_info, sizeof(media_dev.bus_info), "My Virtual bus_info");media_device_init(&media_dev);ret = media_device_register(&media_dev);if (ret) {pr_err("media_device_register failed\n");return ret;}// 注冊 video device.ret = video_register_device(&isp_vdev, VFL_TYPE_GRABBER, -1);if (ret) {printk("simple_isp: video_register_device failed\n");return ret;}printk("gsimple_isp: registered video device /dev/video%d\n", isp_vdev.minor);return 0;
}module_init(isp_init);MODULE_LICENSE("GPL");
MODULE_AUTHOR("baron");
MODULE_DESCRIPTION("Simple virtual camera driver example");

注冊完之后可以在系統中找到這些設備.

console:/ # cat /sys/class/video4linux/v4l-subdev0/name
sensor_A  // sensor_A 設備
console:/ # cat /sys/class/video4linux/v4l-subdev1/name
sensor_B  // sensor_B 設備
console:/ # cat /sys/class/video4linux/v4l-subdev2/name
isp conrtol // sensor_C 設備
console:/ # cat /sys/class/video4linux/video9/name
Simple Virtual Camera

四、media controller

????前面我們模擬了一個簡單的攝像頭鏈路, 由sensor_AsensorBcsi 控制器 以及 isp video. 組成, 我們為他們創建了對應的設備. 現在引入一個問題: 只有一個 csi 控制器, 但是有兩個 sensor 輸入, 我們希望組成如下圖的鏈路狀態我們應該怎么實現.

在這里插入圖片描述

????當然我們可以在驅動寫死, 如果我們一開始就知道硬件鏈接的話. 然而實際芯片的鏈路往往更加復雜, 可能存在多個csi控制器, isp 處理后有多個輸出. 有沒有一種方法可以動態的鏈接這些鏈路. 于是 v4l2 中的 media controller(MC) 框架閃亮登場. media controller(MC) 允許用戶空間獲取 v4l2_device 下的硬件設備信息, 以及對他們進行動態的管理.

1. media controller 的核心組件

????media controller(MC)的核心組件有三個 entitypad 以及 link 組成.這些組件由 media_deivce 進行統一的管理, 如下圖所示.

在這里插入圖片描述

????media controller(MC) 的本質也是一個字符設備, 提供了一組 ioctl 對 entity 等組件進行管理.

1) media_device

????media_device 向提供一個 cdev 字符設備, 用于用戶空間與內核空間的 entitypadlink 組件進行交互和管理. 它包括流媒體設備的整體描述, 以下描述字段是需要提供的.

字段作用
media_devce.model[32]標識設備的硬件型號名稱 (如"USB Camera Model X")
media_devce.driver_name[32]表示控制該設備的內核驅動名稱(如"uvcvideo")
media_devce.serial[40]存儲設備的唯一序列號,用于區分同型號的不同設備。
media_devce.bus_info[32]描述設備的總線連接信息(如"usb-3-2.4"),指明設備的物理連接位置

????media_device 通過 media_device_init 接口初始化然后通過 media_device_register 接口進行注冊. 下面偽代碼展示了注冊流程, 注意 media_device 由 v4l2_dev 進行管理必須設置所屬的 v4l2_dev.

    v4l2_dev.mdev = &media_dev; // media device 由 v4l2_dev 進行管理必須設置所屬的 v4l2_dev// 設置 media device 的基本信息snprintf(media_dev.model, sizeof(media_dev.model), "My Virtual Media Device");snprintf(media_dev.driver_name, sizeof(media_dev.driver_name), "My Virtual driver");snprintf(media_dev.serial, sizeof(media_dev.serial), "My Virtual serial");snprintf(media_dev.bus_info, sizeof(media_dev.bus_info), "My Virtual bus_info");media_device_init(&media_dev); // 初始化ret = media_device_register(&media_dev); // 注冊

2) entity 組件

????需要鏈接的對象,比如我們的sensor_AsensorBcsi 控制器以及 isp vido 模塊.當我們需要它作為鏈接對象時, 我們只需要內嵌 entity 就行了. subdev 和 video_device 已經幫我們嵌入了這個結構. 我們只需要在 kernel 中配置 CONFIG_MEDIA_CONTROLLER 這個宏就行了.

struct v4l2_subdev {
#if defined(CONFIG_MEDIA_CONTROLLER)struct media_entity entity;  // 內嵌 entity 用于支持 media controller
#endif
// ......
}struct video_device {
#if defined(CONFIG_MEDIA_CONTROLLER)struct media_entity entity; // 內嵌 entity 用于支持 media controllerstruct media_intf_devnode *intf_devnode;struct media_pipeline pipe;
#endif
// ......

????通過接口 media_device_register_entity 進行注冊, 同樣的對于 subdev 和 video_device 內核已經提供了對它的支持.

// subdev 的支持
v4l2_device_register_subdev() -->#if defined(CONFIG_MEDIA_CONTROLLER) // 通過宏 CONFIG_MEDIA_CONTROLLER 控制media_device_register_entity()-->#endif// video device 的支持
__video_register_device() -->video_register_media_controller() -->#if defined(CONFIG_MEDIA_CONTROLLER) // 通過宏 CONFIG_MEDIA_CONTROLLER 控制ret = media_device_register_entity(vdev->v4l2_dev->mdev, &vdev->entity);#endif

entity 通過 function 用于描述不同的 entity 的功能, 對于攝像頭內核提供以下的 function

#define MEDIA_ENT_F_CAM_SENSOR      (MEDIA_ENT_F_OLD_SUBDEV_BASE + 1)
#define MEDIA_ENT_F_FLASH           (MEDIA_ENT_F_OLD_SUBDEV_BASE + 2)
#define MEDIA_ENT_F_LENS            (MEDIA_ENT_F_OLD_SUBDEV_BASE + 3)
#define MEDIA_ENT_F_TUNER           (MEDIA_ENT_F_OLD_SUBDEV_BASE + 5)

在注冊前我們需要設置 function, 如下面偽代碼

static struct v4l2_subdev sensorA_sd;
int xxx_init(void)
{// 設置 MEDIA_ENT_F_CAM_SENSOR 攝像頭常用的 functionsensorA_sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;// 注冊 v4l2_subdev 的時候就幫我們注冊了 v4l2_devret = v4l2_device_register_subdev(&v4l2_dev, &sensorA_sd);
}

3) pad 組件

????entity 所描述的實體設備上面的端口. 每一個 entity 上面有一個到多個端口(pad), 端口的數量是固定好的存放與 entity->pads 數組中.數據結構如下所示

struct media_pad {struct media_gobj graph_obj; // 用于管理 padstruct media_entity *entity; // 所屬 entityu16 index;                   // entity->pads 中對應的數組下標unsigned long flags;         // pad 狀態標志
};

????pad 中 index 表示所在的 entity->pads[index] 中的數組下標. pad 有三種標志 sink 接收端口source 發送端口must connect 必須連接.

#define MEDIA_PAD_FL_SINK           (1 << 0)  // sink 接收端口
#define MEDIA_PAD_FL_SOURCE         (1 << 1)  // source 發送端口
#define MEDIA_PAD_FL_MUST_CONNECT   (1 << 2)  // connect 端口必須是連接狀態

????通過 media_entity_pads_init 函數將 pads 添加到所屬的 entity. 下面偽代碼展示了添加過程.

static struct v4l2_subdev sensorA_sd;
// sensorA 只有 1 個 pad, 并且為發送端口.
static struct media_pad sensorA_pads[1] = {{.flags = MEDIA_PAD_FL_SOURCE,},
};int xxx_init(void)
{// ......ret = media_entity_pads_init(&sensorA_sd.entity, sizeof(sensorA_pads), sensorA_pads);// ......
}

4) media_link 組件

????media_link 用于連接 pad 和 pad. 如下圖所示, link 有兩個一個 link 一個 linkback, 設計兩個 link 是為了方便鏈表的遍歷.

在這里插入圖片描述

????link 也是驅動提前注冊的, 有哪些可以 link 在一起, 驅動需要提前規劃好并且使用 media_create_pad_link 接口創建好這些 link. 用戶空間則對這些 link 進行連接還是斷開的控制.有了 link 之后就可以通過 pad 返回對面的設備. 下面代碼展示了獲取流程.

struct v4l2_subdev *get_remote_sensor(struct media_pad *local)
{struct media_pad *remote;remote = media_entity_remote_pad(local);return media_entity_to_v4l2_subdev(remote->entity);
}
EXPORT_SYMBOL_GPL(get_remote_sensor);

????有了這個技巧, 我們就能在當前設備的接口里面通過 v4l2_subdev_call 調用遠端設備的接口, 從而實現動態調用的功能.

static int isp_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type)
{struct video_device *vdev = video_devdata(file);struct v4l2_subdev * remote_sub;remote_sub = get_remote_sensor(&vdev->entity.pads[0]);v4l2_subdev_call(remote_sub, video, s_stream, 1);return 0;
}

????這樣我們只需要在應用切換 link 就能實現動態切換了.

2. 添加 media controller

????有了這些知識, 給我們的 sensor_AsensorBcsi 控制器以及 isp vido 模塊 添加 media controller 吧. 這里的修改也是基于前面的 subdev 驅動修改的, 這里也只給出 patch.

1) 給 isp 添加 media entity

????給我們的 /dev/video9 描述的 isp 添加 media entity, 按照前面的關系圖, 我們需要添加一個 pad(sink) 用于接收 csi 的數據.

Index: kernel/drivers/media/my_camera.c
===================================================================
--- kernel.orig/drivers/media/my_camera.c
+++ kernel/drivers/media/my_camera.c
@@ -16,9 +16,21 @@struct v4l2_device my_v4l2_dev;EXPORT_SYMBOL_GPL(my_v4l2_dev);+static struct media_device media_dev;
+struct video_device isp_vdev;EXPORT_SYMBOL_GPL(isp_vdev);+struct v4l2_subdev *get_remote_sensor(struct media_pad *local)
+{
+   struct media_pad *remote;
+
+   remote = media_entity_remote_pad(local);
+
+   return media_entity_to_v4l2_subdev(remote->entity);
+}
+EXPORT_SYMBOL_GPL(get_remote_sensor);
+static int isp_open(struct file *file){printk("simple_isp: device opened\n");
@@ -33,8 +45,15 @@ static int isp_release(struct file *filestatic int isp_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type){
+   struct video_device *vdev = video_devdata(file);
+   struct v4l2_subdev * remote_sub;
+printk("%s\n", __func__);+   remote_sub = get_remote_sensor(&vdev->entity.pads[0]);
+
+   v4l2_subdev_call(remote_sub, video, s_stream, 1);
+return 0;}@@ -52,6 +71,12 @@ static const struct v4l2_file_operations.release = isp_release,};+static struct media_pad isp_pads[1] = {
+   {
+       .flags = MEDIA_PAD_FL_SINK,
+   },
+};
+static int __init isp_init(void) {int ret;@@ -74,6 +99,22 @@ static int __init isp_init(void) {isp_vdev.vfl_dir = VFL_DIR_RX;isp_vdev.device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_READWRITE;+   isp_vdev.entity.function = MEDIA_ENT_F_CAM_SENSOR;
+    ret = media_entity_pads_init(&isp_vdev.entity, 1, isp_pads);
+
+   my_v4l2_dev.mdev = &media_dev;// 這個要在 video_register_device 前面
+   snprintf(media_dev.model, sizeof(media_dev.model), "My Virtual Media Device");
+   snprintf(media_dev.driver_name, sizeof(media_dev.driver_name), "My Virtual driver");
+   snprintf(media_dev.serial, sizeof(media_dev.serial), "My Virtual serial");
+   snprintf(media_dev.bus_info, sizeof(media_dev.bus_info), "My Virtual bus_info");
+   media_device_init(&media_dev);
+
+   ret = media_device_register(&media_dev);
+    if (ret) {
+        pr_err("media_device_register failed\n");
+        return ret;
+    }
+// 注冊 video device.ret = video_register_device(&isp_vdev, VFL_TYPE_GRABBER, -1);if (ret) {

2) 給 subdev 添加 media entity

????給我們的 sensor_AsensorB 分別添加一個 pad(source) 用于傳送數據到 csi. 給 csi 控制器 添加兩個 pad(source/link) 分別用于接收 sensor 的數據和發送數據給 isp. 最后給他們添加 link.

ndex: kernel/drivers/media/my_sensorB.c
===================================================================
--- kernel.orig/drivers/media/my_sensorB.c
+++ kernel/drivers/media/my_sensorB.c
@@ -15,11 +15,21 @@extern struct v4l2_device my_v4l2_dev;+extern struct v4l2_subdev *get_remote_sensor(struct media_pad *local);
+static int sensor_s_stream(struct v4l2_subdev *sd, int on){
+
+   struct v4l2_subdev * remote_sub;
+printk("%s: %s stream %s\n", __func__, sd->name, on ? "on" : "off");
-
-   return 0;
+
+   if(strcmp(sd->name, "csi_control") == 0){
+       remote_sub = get_remote_sensor(&sd->entity.pads[0]);
+       v4l2_subdev_call(remote_sub, video, s_stream, 1);
+   }
+
+    return 0;}static const struct v4l2_subdev_video_ops sensor_video_ops = {
@@ -30,7 +40,28 @@ static struct v4l2_subdev_ops sensor_ops.video = &sensor_video_ops,};-struct v4l2_subdev* my_sensor_register(const char *name)
+static struct media_pad sensorA_pads[1] = {
+   {
+       .flags = MEDIA_PAD_FL_SOURCE,
+   },
+};
+
+static struct media_pad sensorB_pads[1] = {
+   {
+       .flags = MEDIA_PAD_FL_SOURCE,
+   },
+};
+
+static struct media_pad sensor_csi_pads[2] = {
+   {
+       .flags = MEDIA_PAD_FL_SINK,
+   },
+   {
+       .flags = MEDIA_PAD_FL_SOURCE,
+   },
+};
+
+struct v4l2_subdev* my_sensor_register(const char *name, unsigned int pad_num, struct media_pad *pads){struct v4l2_subdev *sd;int ret;
@@ -42,6 +73,9 @@ struct v4l2_subdev* my_sensor_register(csd->flags = V4L2_SUBDEV_FL_HAS_DEVNODE;strscpy(sd->name, name, sizeof(sd->name));+   sd->entity.function = MEDIA_ENT_F_CAM_SENSOR;
+    ret = media_entity_pads_init(&sd->entity, pad_num, pads);
+ret = v4l2_device_register_subdev(&my_v4l2_dev, sd);ret = v4l2_device_register_subdev_nodes(&my_v4l2_dev);
@@ -49,15 +83,22 @@ struct v4l2_subdev* my_sensor_register(creturn sd;}+extern struct video_device isp_vdev;
+static int __init sensor_module_init(void){
+   int ret;struct v4l2_subdev *sensor_A;struct v4l2_subdev *sensor_B;struct v4l2_subdev *csi_control;-    sensor_A = my_sensor_register("sensor_A");
-    sensor_B = my_sensor_register("sensor_B");
-    csi_control = my_sensor_register("csi_control");
+    sensor_A = my_sensor_register("sensor_A", 1, sensorA_pads);
+    sensor_B = my_sensor_register("sensor_B", 1, sensorB_pads);
+    csi_control = my_sensor_register("csi_control", 2, sensor_csi_pads);
+
+   ret = media_create_pad_link(&sensor_A->entity, 0, &csi_control->entity, 0, MEDIA_LNK_FL_ENABLED);
+   ret = media_create_pad_link(&sensor_B->entity, 0, &csi_control->entity, 0, 0);
+   ret = media_create_pad_link(&csi_control->entity, 1, &isp_vdev.entity, 0, MEDIA_LNK_FL_ENABLED);return 0;}

3) 應用程序

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
#include <errno.h>
#include <sys/mman.h>
#include <string.h>int main() {int fd;// 打開設備 /dev/video9fd = open("/dev/video9", O_RDWR);if (fd == -1) {perror("打開設備失敗");return -1;}printf("camera open\n");struct v4l2_buffer buf;buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;buf.index = 0;// 6. 啟動視頻流if (ioctl(fd, VIDIOC_STREAMON, &buf.type) == -1) {perror("啟動視頻流失敗");close(fd);return 1;}printf("vcam stream on\n");// 關閉設備close(fd);printf("camera close\n");return 0;
}

4) 驗證結果

????驗證需要用 media-ctl 工具, 命令 media-ctl -d /dev/media1 -p 用于打印基本信息. 如圖所示默認

在這里插入圖片描述

????有兩個關鍵的信息 entity idpad id , 創建鏈接需要用到這兩個 id. 驅動在注冊的時候創建了一個默認的連接圖中也可以看出來 sensor_A -> csi 控制器 -> isp vido 模塊. 我們來驗證一下這個鏈接.

console:/cache # ./mytest
[ 1224.326341] simple_isp: device opened
camera open
vcam stream on
camera close
console:/cache # [ 1224.326580] isp_v4l2_streamon
[ 1224.326607] sensor_s_stream: csi_control stream on
[ 1224.326626] sensor_s_stream: sensor_A stream on // 通過 isp 的 open 調用到 sensor_A
[ 1224.327022] simple_isp: device closed

????接下來我們用 media-ctl 修改連接為 sensorB -> csi 控制器 -> isp vido 模塊.

130|console:/cache # media-ctl -d /dev/media1 --reset // 先清除原來的鏈接
// 前面的 9:0 就是 sensorB 的 entity id 和 pad id
130|console:/cache # media-ctl -d /dev/media1 -l "9:0 -> 13:0 [1]" // 創建 sensorB --> csi 控制器的連接
130|console:/cache # media-ctl -d /dev/media1 -l "13:1 -> 1:0 [1]" // 創建 csi 控制器 --> isp vido 模塊的連接

????修改之后如圖

在這里插入圖片描述

????再來編程驗證, 發現 sensor 已經動態切換成 sensor_B 了

console:/cache # ./mytest
[ 1728.598123] simple_isp: device opened
camera open
vcam stream on
camera close
console:/cache # [ 1728.598343] isp_v4l2_streamon
[ 1728.598368] sensor_s_stream: csi_control stream on
[ 1728.598387] sensor_s_stream: sensor_B stream on // 通過 isp 的 open 調用到 sensor_B
[ 1728.598794] simple_isp: device closed

五、設備樹 ports 方法

????linux 設備樹中使用 port(端口)endpoint(端點) 用來描述設備之間的連接關系. 這種連接關系主要用于多媒體, 音頻, 視頻等場景. 其實就是通過 port 和 endpoint 來找到 dts 描述的對方設備.

在這里插入圖片描述

????為了兼容 acpi linux 又整了一套 fwnode 接口封裝了設備樹相關的操作接口, 有兩個關鍵的數據結構 fwnode_handlefwnode_endpoint, 前者用于描述設備節點, 后者則用于記錄端點信息.

struct fwnode_endpoint {unsigned int port; // 端口號 idunsigned int id;   // 端點 id, 用于區分同一端口下的多個端點const struct fwnode_handle *local_fwnode;
};

????他們的接口實現在 drivers/base/property.cdrivers/of/property.c 中, 下面給出常見接口.

1. 常見接口

1) fwnode_graph_get_port_parent

????獲取設備節點寫的 fwnode 的 prev 指向的端點的下一個端點, 如果 prev 為空則表示找第一個端點.

  • 注意這里的 fwnode 必現為設備節點 device_node
  • 當 prev 為 NULL 時 device_node --> ports --> port --> endpiont
  • 當 prev 不為空時 prev --> parent_node --> endpoint
struct fwnode_handle *fwnode_graph_get_next_endpoint(const struct fwnode_handle *fwnode, struct fwnode_handle *prev);

2) fwnode_graph_get_remote_endpoint

????獲取 fwnode 描述的端點的遠端端點, endpoint --> remote_endpoint --> endpoint.

struct fwnode_handle *fwnode_graph_get_remote_endpoint(const struct fwnode_handle *fwnode);

3) fwnode_graph_parse_endpoint

????讀取 fwnode 描述的 endpoint 的端點信息和端點信息.

  • endpoint->port 設備樹 port 的 reg 屬性
  • endpoint->id 設備樹中的 endpoint 的 reg 屬性
int fwnode_graph_parse_endpoint(const struct fwnode_handle *fwnode, struct fwnode_endpoint *endpoint);

4) fwnode_graph_get_remote_port

????獲取 fwnode 描述的 endpoint 的遠程 endpoint 端點的 port 節點.

struct fwnode_handle *fwnode_graph_get_remote_port_parent(const struct fwnode_handle *fwnode);

5) fwnode_graph_get_port_parent

????獲取 fwnode 描述的 endpoint 的遠程 endpoint 端點的 device 設備節點.

struct fwnode_handle *fwnode_graph_get_endpoint_by_id(const struct fwnode_handle *fwnode, u32 port, u32 endpoint, unsigned long flags);

2. 編程驗證

????設備樹的編寫如下, 設計了兩個設備, 然后通過上面的接口, 獲取對方設備的信息.

/{......device_A { // 設備 Acompatible = "device_A";status = "okay";ports {#address-cells = <1>;#size-cells = <0>;port@11 {#address-cells = <1>;#size-cells = <0>;reg = <11>;    // 設備 A 的端口號device_A_out: endpoint@0 {reg = <0>; // 設備 A 的端點號remote-endpoint = <&device_B_in>;};};};};device_B { // 設備 Bcompatible = "device_B";status = "okay";ports {#address-cells = <1>;#size-cells = <0>;port@12 {#address-cells = <1>; #size-cells = <0>;reg = <12>;    // 設備 B 的端口號device_B_in: endpoint@1 {reg = <1>; // 設備 B 的端點號remote-endpoint = <&device_A_out>;};};};};......
};

1) 設備 A 的驗證程序

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/firmware.h>
#include <linux/fwnode.h>
#include <linux/property.h>
#include <linux/mod_devicetable.h>
#include <linux/of.h>static int device_A_probe(struct platform_device *pdev)
{struct fwnode_handle *fwnode = of_fwnode_handle(pdev->dev.of_node);struct fwnode_handle *endpoint;struct fwnode_handle *remote_endpoint;struct fwnode_endpoint ep;struct fwnode_handle *remote_port;struct fwnode_handle *remote_device;int ret;printk("Device A: probe begin\n");// 獲取當前設備的端點endpoint = fwnode_graph_get_next_endpoint(fwnode, NULL);if (endpoint){printk("endpiont name:%s\n", ((to_of_node(endpoint))->full_name));}// 解析端點信息ret = fwnode_graph_parse_endpoint(endpoint, &ep);if (!ret){// 打印端口號和端點 IDprintk("device A Port ID: %u\n", ep.port);printk("device A Endpoint ID: %u\n", ep.id);}// 獲取當前端點的遠程端點remote_endpoint = fwnode_graph_get_remote_endpoint(endpoint);if (remote_endpoint) {printk("remote_endpiont name:%s\n", ((to_of_node(remote_endpoint))->full_name));}// 解析端點信息ret = fwnode_graph_parse_endpoint(remote_endpoint, &ep);if (!ret){// 打印遠端端口和端點 IDprintk("device B Port ID: %u\n", ep.port);printk("device B Endpoint ID: %u\n", ep.id);}// 獲取遠端 device b 的 port 節點remote_port = fwnode_graph_get_remote_port(endpoint);if (remote_port) {printk("remote port name:%s\n", ((to_of_node(remote_port))->full_name));}// 獲取遠端 device b 的設備節點remote_device = fwnode_graph_get_port_parent(endpoint);if (remote_device){printk("remote device name:%s\n", ((to_of_node(remote_device))->full_name));}printk("Device A: probe end\n");return 0;
}static const struct of_device_id device_A_of_match[] = {{ .compatible = "device_A"},{},
};struct platform_driver device_A_driver = {.probe = device_A_probe,.driver = {.name = "device_A_driver",.of_match_table = device_A_of_match,},
};static int __init device_A_init(void)
{return platform_driver_register(&device_A_driver);
}static void __exit device_A_exit(void)
{platform_driver_unregister(&device_A_driver);
}module_init(device_A_init);
module_exit(device_A_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("baron");
MODULE_DESCRIPTION("Device A Driver");

驗證結果:

[    1.206735] Device A: probe begin
[    1.206754] endpiont name:endpoint@0         // 打印設備 A 的端點名
[    1.206767] device A Port ID: 11             // 打印設備 A 的端口號
[    1.206777] device A Endpoint ID: 0          // 打印設備 A 的端點號
[    1.206791] remote_endpiont name:endpoint@1  // 打印遠程設備 B 的端點名
[    1.206804] device B Port ID: 12             // 打印遠程設備 B 的端口號   
[    1.206813] device B Endpoint ID: 1          // 打印遠程設備 B 的端點號
[    1.206820] remote port name:port@12         // 打印遠程設備 B 的端口名
[    1.206830] remote device name:device_A      // 打印遠程設備名
[    1.206840] Device A: probe end

2) 設備 B 的驗證程序

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/firmware.h>
#include <linux/fwnode.h>
#include <linux/property.h>
#include <linux/mod_devicetable.h>
#include <linux/of.h>static int device_B_probe(struct platform_device *pdev)
{struct fwnode_handle *fwnode = of_fwnode_handle(pdev->dev.of_node);struct fwnode_handle *endpoint;struct fwnode_handle *remote_endpoint;struct fwnode_endpoint ep;struct fwnode_handle *remote_port;struct fwnode_handle *remote_device;int ret;printk("Device B: probe begin\n");// 獲取當前設備的端點endpoint = fwnode_graph_get_next_endpoint(fwnode, NULL);if (endpoint){printk("endpiont name:%s\n", ((to_of_node(endpoint))->full_name));}// 解析端點信息ret = fwnode_graph_parse_endpoint(endpoint, &ep);if (!ret){// 打印端口號和端點 IDprintk("device B Port ID: %u\n", ep.port);printk("device B Endpoint ID: %u\n", ep.id);}// 獲取當前端點的遠程端點remote_endpoint = fwnode_graph_get_remote_endpoint(endpoint);if (remote_endpoint) {printk("remote_endpiont name:%s\n", ((to_of_node(remote_endpoint))->full_name));}// 解析端點信息ret = fwnode_graph_parse_endpoint(remote_endpoint, &ep);if (!ret){// 打印遠端端口和端點 IDprintk("device A Port ID: %u\n", ep.port);printk("device A Endpoint ID: %u\n", ep.id);}// 獲取遠端 device a 的 port 節點remote_port = fwnode_graph_get_remote_port(endpoint);if (remote_port) {printk("remote port name:%s\n", ((to_of_node(remote_port))->full_name));}// 獲取遠端 device a 的設備節點remote_device = fwnode_graph_get_port_parent(endpoint);if (remote_device){printk("remote device name:%s\n", ((to_of_node(remote_device))->full_name));}printk("Device B: probe end\n");return 0;
}static const struct of_device_id device_B_of_match[] = {{ .compatible = "device_B"},{},
};static struct platform_driver device_B_driver = {.probe = device_B_probe,.driver = {.name = "device_B_driver",.of_match_table = device_B_of_match,},
};static int __init device_B_init(void)
{return platform_driver_register(&device_B_driver);
}static void __exit device_B_exit(void)
{platform_driver_unregister(&device_B_driver);
}module_init(device_B_init);
module_exit(device_B_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("baron");
MODULE_DESCRIPTION("Device B Driver");

驗證結果:

[    1.207246] Device B: probe begin         
[    1.207263] endpiont name:endpoint@1        // 打印設備 B 的端點名
[    1.207278] device B Port ID: 12            // 打印設備 B 的端口號
[    1.207287] device B Endpoint ID: 1         // 打印設備 B 的端點號
[    1.207303] remote_endpiont name:endpoint@0 // 打印遠程設備 A 的端點名
[    1.207314] device A Port ID: 11            // 打印遠程設備 A 的端號
[    1.207323] device A Endpoint ID: 0         // 打印遠程設備 A 的端點號
[    1.207336] remote port name:port@11        // 打印遠程設備 A 的端口名
[    1.207346] remote device name:device_B     // 打印遠程設備名
[    1.207356] Device B: probe end

3. 總結

????通過這樣的方式, 我們就能夠通過設備樹找到對方設備, 從而形成數據流, 因此也約定俗成數據要發給誰對應的 remote-endpoint 就設置為 xxx_in. 例如有兩個攝像頭 camera0 和 camera1, 兩個攝像頭都接到了 csi2_dphy 這個攝像頭控制器, 然后通過 csi2_dphy 輸出到 isp 處理. 就可以通過 dts 來實現設備的級聯.

在這里插入圖片描述

????這樣對于攝像頭驅動來說, 只需要在 dts 節點里面創建一個 ports 然后把 remote-endpoint 指向我們需要輸入的攝像頭控制器節點的 endpoint 就可以完成模塊的對接, 因為在攝像頭控制器加載的時候, 會遍歷設備樹中 ports 節點上的攝像頭設備, 這樣就知道了接的是哪個攝像頭了. 不需要的時候設置為 disable 或者直接注釋掉就行了. 十分方便.

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/915681.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/915681.shtml
英文地址,請注明出處:http://en.pswp.cn/news/915681.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

windows內核研究(驅動開發-0環與3環的通信)

驅動開發0環與3環的通信 設備對象 在之前開發窗口程序時&#xff0c;消息都是被封裝成一個結構體&#xff08;MSG&#xff09;&#xff0c;在內核開發時&#xff0c;消息被封裝成另外一個結構體&#xff1a;IRP&#xff08;I/O Request Package&#xff09; 在窗口程序中&#…

ASP.NET Core Web API 內存緩存(IMemoryCache)入門指南

在 Web API 開發中&#xff0c;頻繁訪問數據庫或第三方服務可能會帶來性能瓶頸。為了提升接口響應速度并減輕后端壓力&#xff0c;使用緩存是非常有效的優化手段。本文將帶你快速上手 ASP.NET Core 提供的內存緩存&#xff08;IMemoryCache&#xff09;&#xff0c;無需安裝額外…

Axios Token 設置示例

以下是一個完整的 Axios Token 設置示例&#xff0c;涵蓋全局配置、請求攔截器和單次請求設置三種方式&#xff1a;1. 基礎配置&#xff08;推薦方案&#xff09;javascript復制代碼import axios from axios;// 創建 Axios 實例 const apiClient axios.create({baseURL: https…

Excel數據合并工具:零門檻快速整理

軟件介紹 在數據處理工作中&#xff0c;合并Excel同類數據是一項常見但繁瑣的任務。今天為大家推薦一款專為簡化此類操作設計的工具&#xff0c;它能快速完成工作表內多行同類數據的合并整理&#xff0c;大幅提升數據處理效率。 零門檻操作體驗 相比Excel自帶的數據透視…

深度學習 -- 梯度計算及上下文控制

深度學習 – 梯度計算及上下文控制 文章目錄深度學習 -- 梯度計算及上下文控制一&#xff0c;自動微分1.1 基礎概念1.2 計算梯度1.2.1 計算標量梯度1.2.2 計算向量梯度1.2.3 多標量梯度計算1.2.4 多向量梯度計算二&#xff0c;梯度上下文控制2.1 控制梯度計算2.2 累計梯度2.3 梯…

Redisson RLocalCachedMap 核心參數詳解

&#x1f9d1; 博主簡介&#xff1a;CSDN博客專家&#xff0c;歷代文學網&#xff08;PC端可以訪問&#xff1a;https://literature.sinhy.com/#/?__c1000&#xff0c;移動端可微信小程序搜索“歷代文學”&#xff09;總架構師&#xff0c;15年工作經驗&#xff0c;精通Java編…

【Unity3D實例-功能-移動】角色移動-通過WSAD(Rigidbody方式)

你是否曾夢想在虛擬世界中自由翱翔&#xff0c;像海豚一樣在海洋自由穿梭&#xff0c;或者像宇航員一樣在宇宙中盡情探索&#xff1f;今天&#xff0c;我們就來聊聊如何在Unity中使用Rigidbody來實現角色移動。 廢話不多說&#xff0c;走&#xff0c;讓我們馬上來一探究竟&…

Vue接口平臺學習十一——業務流測試

效果圖及簡單說明 與之前的用例列表相似布局&#xff0c;也分左右&#xff0c;左邊用于顯示測試流程的名稱&#xff0c;右邊用于顯示流程相關信息。 左側點擊添加&#xff0c;直接增加一個新的業務流。 右側是點擊的業務流詳情&#xff0c;展示名稱&#xff0c;名稱的編輯保存&…

碳化硅缺陷分類與原因

01一、碳化硅晶體材料中的缺陷到底是什么&#xff1f;碳化硅晶體材料中的缺陷是指在晶體生長、加工或使用過程中出現的不完美結構。這些缺陷可能表現為晶體內部的裂紋、表面的凹坑、原子排列的錯誤等。雖然缺陷看起來微不足道&#xff0c;但它們卻可能對晶體的電學、熱學和機械…

Jenkins 實現項目的構建和發布

作者&#xff1a;小凱 沉淀、分享、成長&#xff0c;讓自己和他人都能有所收獲&#xff01; 本文的宗旨在于通過簡單干凈實踐的方式教會讀者&#xff0c;如何在 Docker 中部署 Jenkins&#xff0c;并通過 Jenkins 完成對項目的打包構建并在 Docker 容器中部署。 Jenkins 的主要…

Django接口自動化平臺實現(三)

3.2 后臺 admin 添加數據 1&#xff09;注冊模型類到 admin&#xff1a; 1 from django.contrib import admin2 from . import models3 4 5 class ProjectAdmin(admin.ModelAdmin):6 list_display ("id", "name", "proj_owner", "tes…

CentOS 7 配置環境變量常見的4種方式

?博客主頁&#xff1a; https://blog.csdn.net/m0_63815035?typeblog &#x1f497;《博客內容》&#xff1a;.NET、Java.測試開發、Python、Android、Go、Node、Android前端小程序等相關領域知識 &#x1f4e2;博客專欄&#xff1a; https://blog.csdn.net/m0_63815035/cat…

k8s:手動創建PV,解決postgis數據庫本地永久存儲

1.離線環境CPU:Hygon C86 7285 32-core Processor 操作系統&#xff1a;麒麟操作系統 containerd&#xff1a;1.7.27 Kubernetes:1.26.12 KubeSphere:4.1.2 kubekey&#xff1a;3.1.10 Harbor:2.13.1 Postgis:17-3.52創建StorageClass2.1創建 apiVersion: storage.k8s.io/v1kin…

谷歌瀏覽器Chrome的多用戶配置文件功能

谷歌瀏覽器Chrome的多用戶配置文件功能允許在同一設備上創建多個獨立賬戶,每個賬戶擁有完全隔離的瀏覽數據(如書簽、歷史記錄、擴展、Cookies等),非常適合工作/生活賬戶分離、家庭共享或臨時多賬號登錄場景。 如何使用Chrome的多用戶配置文件功能? 一、創建與切換用戶 1.…

傲軟錄屏 專業高清錄屏軟件 ApowerREC Pro 下載與保姆級安裝教程!!

小編今天分享一款強大的電腦屏幕錄像軟件 傲軟錄屏 ApowerREC&#xff0c;能夠幫助用戶錄制中電腦桌面屏幕上的所有內容&#xff0c;包括畫面和聲音&#xff0c;支持全屏錄制、區域錄制、畫中畫以及攝像頭錄制等多種視頻錄制模式&#xff0c;此外&#xff0c;還支持計劃任務錄制…

【計算機網絡】MAC地址與IP地址:網絡通信的雙重身份標識

在計算機網絡領域&#xff0c;MAC地址與IP地址是兩個核心概念&#xff0c;它們共同構成了數據傳輸的基礎。理解二者的區別與聯系&#xff0c;對于網絡配置、故障排查及安全管理至關重要。 一、基本概念 1. MAC地址&#xff08;物理地址&#xff09; 定義&#xff1a;固化在網絡…

如何用keepAlive實現標簽頁緩存

什么是KeepAlive首先&#xff0c;要明確所說的是TCP的 KeepAlive 還是HTTP的 Keep-Alive。TCP的KeepAlive和HTTP的Keep-Alive是完全不同的概念&#xff0c;不能混為一談。實際上HTTP的KeepAlive寫法是Keep-Alive&#xff0c;跟TCP的KeepAlive寫法上也有不同。TCP的KeepAliveTCP…

數據庫隔離級別

隔離級別決定了事務之間的可見性規則&#xff0c;直接影響數據庫的并發性能和數據一致性。SQL 標準定義了 4 種隔離級別&#xff0c;從低到高依次為&#xff1a;讀未提交→讀已提交→可重復讀→串行化。隔離級別越高&#xff0c;對并發問題的解決能力越強&#xff0c;但對性能的…

基于Python flask的電影數據分析及可視化系統的設計與實現,可視化內容很豐富

摘要&#xff1a;基于Python的電影數據分析及可視化系統是一個應用于電影市場的數據分析平臺&#xff0c;旨在為廣大電影愛好者提供更準確、更詳細、更實用的電影數據。數據分析部分主要是對來自貓眼電影網站上的數據進行清洗、分類處理、存儲等步驟&#xff0c;數據可視化則是…

TCP通訊開發注意事項及常見問題解析

文章目錄一、TCP協議特性與開發挑戰二、粘包與拆包問題深度解析1. 成因原理2. 典型場景與實例驗證3. 系統化解決方案接收方每次讀取10字節2. 丟包檢測與驗證工具3. 工程化解決方案四、連接管理關鍵實踐1. 超時機制設計2. TIME_WAIT狀態優化3. 異常處理最佳實踐五、高性能TCP開發…