嵌入式Linux驅動筆記(十七)------詳解V4L2框架(UVC驅動)
你好!這裡是風箏的部落格,
歡迎和我一起交流。
Video for Linux 2,簡稱V4l2,是Linux核心中關於視訊裝置的核心驅動框架,為上層的訪問底層的視訊裝置提供了統一的介面。凡是核心中的子系統都有抽象底層硬體的差異,為上層提供統一的介面和提取出公共程式碼避免程式碼冗餘等好處。
首先來看看所有的v4l2驅動都必須要有的幾個組成部分:
– 用來描述每一個v4l2裝置例項狀態的結構(struct v4l2_device)。
– 用來初始化和控制子裝置的方法(struct v4l2_subdev)。
– 要能建立裝置節點(/dev/videoX、/dev/vbiX 和 /dev/radioX)並且能夠對該節點所持有的資料進行跟蹤(structvideo_device)。
– 為每一個被開啟的節點維護一個檔案控制程式碼(structv4l2_fh)。
– 視訊緩衝區的處理(videobuf或者videobuf2 framework)。
用一個比較粗糙的圖來表現他們之間的關係,大致為:
裝置例項(v4l2_device)
|__子裝置例項(v4l2_subdev)
|__視訊裝置節點(video_device)
|__檔案訪問控制(v4l2_fh)
|__視訊緩衝的處理(videobuf/videobuf2)
許多驅動需要與子裝置通訊。這些裝置可以完成各種任務,但通常他們負責 音視訊複用和編解碼。如網路攝像頭的子裝置通常是感測器和攝像頭控制器。
這些一般為 I2C 介面裝置,但並不一定都是。為了給驅動提供呼叫子裝置的 統一介面,v4l2_subdev 結構體(v4l2-subdev.h)產生了。
在別的文章看到的圖,覺得還不錯,貼一下:
可以看出:
每個子裝置驅動都必須有一個 v4l2_subdev 結構體(實際的硬體裝置都被抽象為v4l2_subdev),代表一個簡單的子裝置,也可以嵌入到一個更大的結構體中,與更多裝置狀態 資訊儲存在一起。
v4l2_device在v4l2框架中充當所有v4l2_subdev的父裝置,管理著註冊在其下的子裝置。
因為子裝置千差萬別,所以v4l2-device又向上層提供一個標準的介面。所以可以認為v4l2-device就是一箇中間層。
在說v4l2之前,先說下uvc吧:
USB video class(又稱為USB video device class or UVC)就是USB device class視訊產品在不需要安裝任何的驅動程式下即插即用,包括攝像頭、數字攝影機、模擬視訊轉換器、電視卡及靜態視訊相機。
V4L2就是用來管理UVC裝置的並且能夠提供視訊相關的一些API
我們以Linux kernel 4.8.17為例,分析下實現過程:
drivers\media\usb\uvc\uvc_driver.c檔案:
struct uvc_driver uvc_driver = {
.driver = {
.name = "uvcvideo",
.probe = uvc_probe,//支援的video裝置插入就會進入
.disconnect = uvc_disconnect,
.suspend = uvc_suspend,
.resume = uvc_resume,
.reset_resume = uvc_reset_resume,
.id_table = uvc_ids,
.supports_autosuspend = 1,
},
};
當特定的usb裝置被插入時,就會觸發probe函式:
static int uvc_probe(struct usb_interface *intf,
const struct usb_device_id *id)
{
struct usb_device *udev = interface_to_usbdev(intf);
struct uvc_device *dev;
int ret;
/*省略部分內容*/
if ((dev = kzalloc(sizeof *dev, GFP_KERNEL)) == NULL)//【1】
return -ENOMEM;
/*省略部分內容*/
dev->udev = usb_get_dev(udev);//【2】
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;
/*省略部分內容*/
/* Parse the Video Class control descriptor. */
if (uvc_parse_control(dev) < 0) {//【3】
uvc_trace(UVC_TRACE_PROBE, "Unable to parse UVC "
"descriptors.\n");
goto error;
}
/*省略部分內容*/
if (v4l2_device_register(&intf->dev, &dev->vdev) < 0)//【4】
goto error;
/* Initialize controls. */
if (uvc_ctrl_init_device(dev) < 0)//【5】
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)//【6】
goto error;
/*省略部分內容*/
/* Initialize the interrupt URB. */
if ((ret = uvc_status_init(dev)) < 0) {//【7】uvc狀態的處理由中斷端點來控制處理
/*省略部分內容*/
return 0;
error:
uvc_unregister_video(dev);
return -ENODEV;
}
函式太長了,省略了部分內容,但是可以看出,主要的就是做幾件事情:
【1】分配一個dev
【2】給dev設定各種引數,如dev->udevudev
【3】呼叫uvc_parse_control函式分析裝置的控制描述符
【4】呼叫v4l2_device_register函式初始化v4l2_dev
【5】呼叫uvc_ctrl_init_device函式初始化uvc控制裝置
【6】呼叫uvc_register_chains函式註冊所有通道
【7】呼叫uvc_status_init函式初始化uvc狀態
我們來一個個分析下:
【3】:呼叫uvc_parse_control函式
看下呼叫關係:
uvc_parse_control(dev)
uvc_parse_standard_control(dev, buffer, buflen)
uvc_parse_streaming(dev, intf)
跟蹤下uvc_parse_streaming函式:
static int uvc_parse_streaming(struct uvc_device *dev,
struct usb_interface *intf)
{
/*以下大部分內容省略,只顯示重要的*/
struct uvc_streaming *streaming = NULL;
struct uvc_format *format;
struct uvc_frame *frame;
streaming = kzalloc(sizeof *streaming, GFP_KERNEL);
size = nformats * sizeof *format + nframes * sizeof *frame
+ nintervals * sizeof *interval;
format = kzalloc(size, GFP_KERNEL);//申請format陣列存放格式
streaming->format = format;//設定格式
streaming->nformats = nformats;//最多支援nformats種格式
ret = uvc_parse_format(dev, streaming, format,
&interval, buffer, buflen);//分析格式
list_add_tail(&streaming->list, &dev->streams);
return 0;
}
這裡面申請了streaming和format記憶體
streaming是uvc_streaming 結構體,視訊流,很重要,大部分引數都是存在裡面。這函式裡申請了之後進行了很多設定,不過現在我省略了寫。
format記憶體存放的是視訊的格式,frame存放的是如解析度
這裡面都把他設定到了streaming裡面(streaming->format = format;streaming->nformats = nformats;)
最後呼叫uvc_parse_format函式分析格式:
static int uvc_parse_format()
{
fmtdesc = uvc_format_by_guid(&buffer[5]);//通過GUID找到格式format
/*裡面還會對frame進行各種分析和設定,
*如設定format->nframes得出最多有多少種解析度選擇
*暫時忽略*/
}
裡面uvc_format_by_guid函式會從uvc_fmts陣列中通過匹配guid找到格式:
static struct uvc_format_desc uvc_fmts[] = {
{
.name = "YUV 4:2:2 (YUYV)",
.guid = UVC_GUID_FORMAT_YUY2,
.fcc = V4L2_PIX_FMT_YUYV,
},
{
.name = "YUV 4:2:2 (YUYV)",
.guid = UVC_GUID_FORMAT_YUY2_ISIGHT,
.fcc = V4L2_PIX_FMT_YUYV,
},
{
.name = "YUV 4:2:0 (NV12)",
.guid = UVC_GUID_FORMAT_NV12,
.fcc = V4L2_PIX_FMT_NV12,
},
{
.name = "MJPEG",
.guid = UVC_GUID_FORMAT_MJPEG,
.fcc = V4L2_PIX_FMT_MJPEG,
},
/*後面省略......*/
}
.
這樣【3】的工作就完成了,我們來看下【4】的:
int v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev)
{
INIT_LIST_HEAD(&v4l2_dev->subdevs);//用來管理v4l2_device 下的subdevs例項
spin_lock_init(&v4l2_dev->lock);
v4l2_prio_init(&v4l2_dev->prio);
kref_init(&v4l2_dev->ref);
get_device(dev);
v4l2_dev->dev = dev;
if (!v4l2_dev->name[0])
snprintf(v4l2_dev->name, sizeof(v4l2_dev->name), "%s %s",
dev->driver->name, dev_name(dev));
if (!dev_get_drvdata(dev))//dev->driver_data 域 為 NULL
dev_set_drvdata(dev, v4l2_dev);//就將其指向 v4l2_dev
return 0;
}
簡單,沒啥好講的,就是初始化v4l2_dev->subdevs子裝置例項的連結串列,然後設定名字和設定dev->driver_data
看下【5】呼叫uvc_ctrl_init_device
int uvc_ctrl_init_device(struct uvc_device *dev)
{
/*省略了部分內容*/
list_for_each_entry(entity, &dev->entities, list) {
bmControls = entity->extension.bmControls;//控制點陣圖
bControlSize = entity->extension.bControlSize;//控制位域大小
entity->controls = kcalloc(ncontrols, sizeof(*ctrl),
GFP_KERNEL);//分配ncontrols個uvc控制記憶體
if (entity->controls == NULL)
return -ENOMEM;
entity->ncontrols = ncontrols;//設定uvc控制個數
/* Initialize all supported controls */
ctrl = entity->controls;//指向uvc控制陣列
for (i = 0; i < bControlSize * 8; ++i) {
if (uvc_test_bit(bmControls, i) == 0)//跳過控制位域設定0的
continue;
ctrl->entity = entity;
ctrl->index = i;//設定控制位域索引
uvc_ctrl_init_ctrl(dev, ctrl);//初始化uvc控制元件
ctrl++;//uvc控制 指向下一個uvc控制陣列項
}
}
}
uvc_ctrl_init_device主要就是初始化控制引數,裡面就會遍歷uvc裝置實體entities連結串列,然後設定點陣圖和位域大小
最後還會呼叫uvc_ctrl_init_ctrl函式設定背光,色溫等等
接下來繼續看【6】呼叫uvc_register_chains函式:
呼叫關係:
uvc_register_chains
uvc_register_terms(dev, chain)
uvc_stream_by_id
uvc_register_video
uvc_mc_register_entities(chain)
uvc_stream_by_id函式會通過函式傳入的id和dev->streams連結串列的header.bTerminalLink匹配,尋找到stream
這不是重點,我們的重點是uvc_register_video函式,找到stream會就要註冊:
static int uvc_register_video(struct uvc_device *dev,
struct uvc_streaming *stream)
{
/*部分內容省略......*/
struct video_device *vdev = &stream->vdev;
ret = uvc_queue_init(&stream->queue, stream->type, !uvc_no_drop_param);//初始化佇列
ret = uvc_video_init(stream);//初始化
uvc_debugfs_init_stream(stream);
vdev->v4l2_dev = &dev->vdev;
vdev->fops = &uvc_fops;//v4l2操作函式集
vdev->ioctl_ops = &uvc_ioctl_ops;//設定真正的ioctl操作集
vdev->release = uvc_release;//釋放方法
vdev->prio = &stream->chain->prio;
strlcpy(vdev->name, dev->name, sizeof vdev->name);
video_set_drvdata(vdev, stream);//將uvc視訊流作為v4l2裝置的驅動資料
ret = video_register_device(vdev, VFL_TYPE_GRABBER, -1);//註冊
return 0;
}
這是非常重要的函式,我們來一點一點分析:
看下uvc_queue_init函式,佇列初始化,佇列這東西,我們視訊傳輸時會呼叫到,在ioctl裡操作:
static struct vb2_ops uvc_queue_qops = {
.queue_setup = uvc_queue_setup,
.buf_prepare = uvc_buffer_prepare,
.buf_queue = uvc_buffer_queue,
.buf_finish = uvc_buffer_finish,
.wait_prepare = vb2_ops_wait_prepare,
.wait_finish = vb2_ops_wait_finish,
.start_streaming = uvc_start_streaming,
.stop_streaming = uvc_stop_streaming,
};
int uvc_queue_init(struct uvc_video_queue *queue, enum v4l2_buf_type type,
int drop_corrupted)
{
queue->queue.type = type;
queue->queue.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
queue->queue.drv_priv = queue;
queue->queue.buf_struct_size = sizeof(struct uvc_buffer);
queue->queue.ops = &uvc_queue_qops;//stream->queue->queue.ops
queue->queue.mem_ops = &vb2_vmalloc_memops;
queue->queue.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC
| V4L2_BUF_FLAG_TSTAMP_SRC_SOE;
queue->queue.lock = &queue->mutex;
ret = vb2_queue_init(&queue->queue);//初始化queue
mutex_init(&queue->mutex);
spin_lock_init(&queue->irqlock);
INIT_LIST_HEAD(&queue->irqqueue);//初始化stream->queue->irqqueue
queue->flags = drop_corrupted ? UVC_QUEUE_DROP_CORRUPTED : 0;
return 0;
}
裡面先對佇列進行初始化設定,如設定type和ops。
這裡queue->queue.ops = &uvc_queue_qops非常重要,之後我們呼叫vidioc_streamon回撥函式時就是呼叫到這裡的uvc_queue_qops結構體裡的.start_streaming函式
這函式裡對各種佇列進行了初始化:
vb2_queue_init(&queue->queue)
q->buf_ops = &v4l2_buf_ops;
vb2_core_queue_init(struct vb2_queue *q)
INIT_LIST_HEAD(&q->queued_list);//stream->queue->queue->queued_list
INIT_LIST_HEAD(&q->done_list);//stream->queue->done_list
INIT_LIST_HEAD(&queue->irqqueue);//初始化stream->queue->irqqueue
我們繼續看回uvc_register_video函式,裡面接著呼叫了uvc_video_init函式初始化UVC視訊裝置:
int uvc_video_init(struct uvc_streaming *stream)
{
/*省略部分內容*/
struct uvc_streaming_control *probe = &stream->ctrl;//獲取uvc資料流的uvs資料流控制物件
if (uvc_get_video_ctrl(stream, probe, 1, UVC_GET_DEF) == 0)//先得到定義的控制引數
uvc_set_video_ctrl(stream, probe, 1);//再設定uvc視訊控制
ret = uvc_get_video_ctrl(stream, probe, 1, UVC_GET_CUR);//最後在get一次
for (i = stream->nformats; i > 0; --i) {
format = &stream->format[i-1];//獲取對應的uvc格式
if (format->index == probe->bFormatIndex)
break;
}
probe->bFormatIndex = format->index;//設定uvc視訊流控制的格式索引為uvc格式的索引
probe->bFrameIndex = frame->bFrameIndex;//設定uvc視訊流控制的解析度索引為uvc解析度的索引
stream->def_format = format;
stream->cur_format = format;//設定uvc格式為uvc資料流的cur_format成員
stream->cur_frame = frame;//設定uvc幀為uvc資料流的cur_frame成員
if (stream->type == V4L2_BUF_TYPE_VIDEO_CAPTURE) {//視訊採集
if (stream->dev->quirks & UVC_QUIRK_BUILTIN_ISIGHT)
stream->decode = uvc_video_decode_isight;
else if (stream->intf->num_altsetting > 1)
stream->decode = uvc_video_decode_isoc;//同步方式
else
stream->decode = uvc_video_decode_bulk;//bluk方式
}
return 0;
}
這裡面內容就比較多了,先得到,然後設定uvc的控制引數,裡面會操作urb發出usb資料。
然後通過probe->bFormatIndex索引找到使用的format格式和通過probe->bFrameIndex找到對應的frame解析度,然後設定到stream裡。
最後選擇解碼方式,如同步方式或者bluk方式,解碼方式會在資料完成時被回撥函式complete裡呼叫。
再次回到uvc_register_video函式,沒辦法,這個函式太重要了:
裡面繼續:
vdev->fops = &uvc_fops;//v4l2操作函式集
vdev->ioctl_ops = &uvc_ioctl_ops;//設定真正的ioctl操作集
vdev->release = uvc_release;//釋放方法
ret = video_register_device(vdev, VFL_TYPE_GRABBER, -1);
裡面就是vdev->v4l2_dev = &dev->vdev;這樣v4l2_device就與video_device關聯起來,也就是我們文章一開始那個圖看到的。
然後設定fops操作函式vdev->fops = &uvc_fops,雖然這不是給使用者空間使用的open、read、write函式,但是最後vdev->cdev->ops還是最呼叫到這個uvc_fops的,所以使用者空間實際上的pen、read、write函式還是會在這呼叫。 然後ioctl操作函式最終是會呼叫到vdev->ioctl_ops = &uvc_ioctl_ops。可以說,V4L2最重要的就是各種形式的ioctl了,這裡先不講,下一節在分析看看。
然後最終就是我們的註冊函式了:video_register_device裡呼叫到__video_register_device函式:
int __video_register_device(struct video_device *vdev, int type, int nr,
int warn_if_nr_in_use, struct module *owner)
{
/*省略部分函式*/
vdev->minor = -1;//-1表明這個video device從未被註冊過
switch (type) {//根據type選擇裝置名稱
case VFL_TYPE_GRABBER:
name_base = "video";
break;
case VFL_TYPE_VBI:
name_base = "vbi";
break;
case VFL_TYPE_RADIO:
name_base = "radio";
break;
case VFL_TYPE_SUBDEV:
name_base = "v4l-subdev";
break;
case VFL_TYPE_SDR:
name_base = "swradio";
break;
default:
printk(KERN_ERR "%s called with unknown type: %d\n", __func__, type);
return -EINVAL;
}
switch (type) {//選擇得到次裝置號偏移值
case VFL_TYPE_GRABBER://用於視訊輸入/輸出裝置的 videoX
minor_offset = 0;
minor_cnt = 64;
break;
case VFL_TYPE_RADIO://用於廣播調諧器的 radioX
minor_offset = 64;
minor_cnt = 64;
break;
case VFL_TYPE_VBI://用於垂直消隱資料的 vbiX (例如,隱藏式字幕,圖文電視)
minor_offset = 224;
minor_cnt = 32;
break;
default:
minor_offset = 128;
minor_cnt = 64;
break;
}
nr = devnode_find(vdev, 0, minor_cnt);//獲取一個沒有被使用的裝置節點序號
for (i = 0; i < VIDEO_NUM_DEVICES; i++)
if (video_device[i] == NULL)//從video_device[]陣列中選擇一個空缺項,這個空缺項的索引值放到i中
break;
vdev->minor = i + minor_offset;//裝置的次裝置號
video_device[vdev->minor] = vdev;//注意:將設定好的video_device放入到video_device[]
vdev->cdev->ops = &v4l2_fops;//操作使用者空間操作函式集
ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);//新增字元裝置到系統
ret = device_register(&vdev->dev);//裝置註冊
set_bit(V4L2_FL_REGISTERED, &vdev->flags);//將flags第0為設定為1,表示這個video_device是註冊過的了
return 0;
}
我們梳理一下里面做的事情:
1.確定裝置名稱,也就是我們在/dev/下生成的video啊,radio之類的
2.得到次裝置的偏移值
3.找到一個空的video_device陣列,把vdev存進去
4.設定vdev->cdev,這裡就設定了vdev->cdev->ops = &v4l2_fops;裡面就是真正的使用者空間操作集合
5.註冊video_device裝置
6.就是標誌此video_device以註冊
最後【6】呼叫uvc_register_chains函式裡還會呼叫一個uvc_mc_register_entities函式,裡面繼續呼叫uvc_mc_init_entity函式,這就是v4l2_device_register_subdev函式,進行註冊v4l2_subdev,同時初始化然後連線到v4l2_dev->subdevs管理。
好了,【6】呼叫uvc_register_chains函式:就分析完了,我們最後剩一個了:
【7】呼叫uvc_status_init函式
int uvc_status_init(struct uvc_device *dev)
{
/*省略部分函式*/
struct usb_host_endpoint *ep = dev->int_ep;//獲取usb_host_endpoint
uvc_input_init(dev);//初始化uvc輸入裝置,裡面註冊input裝置
dev->status = kzalloc(UVC_MAX_STATUS_SIZE, GFP_KERNEL);//分配urb裝置狀態記憶體
dev->int_urb = usb_alloc_urb(0, GFP_KERNEL);//分配urb
pipe = usb_rcvintpipe(dev->udev, ep->desc.bEndpointAddress);//中斷輸入端點
usb_fill_int_urb(dev->int_urb, dev->udev, pipe,
dev->status, UVC_MAX_STATUS_SIZE, uvc_status_complete,
dev, interval);//填充中斷urb
return 0;
}
裡面就是關於urb的一些東西了,看看就好。
最後,我們使用者空間怎麼才操作的?
看看__video_register_device函式裡的:vdev->cdev->ops = &v4l2_fops;
static const struct file_operations v4l2_fops = {
.owner = THIS_MODULE,
.read = v4l2_read,
.write = v4l2_write,
.open = v4l2_open,
.get_unmapped_area = v4l2_get_unmapped_area,
.mmap = v4l2_mmap,
.unlocked_ioctl = v4l2_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = v4l2_compat_ioctl32,
#endif
.release = v4l2_release,
.poll = v4l2_poll,
.llseek = no_llseek,
};
static int v4l2_open(struct inode *inode, struct file *filp)
{
/*省略部分函式*/
struct video_device *vdev;
vdev = video_devdata(filp);//根據次裝置號從video_devices[]陣列中得到video_device
if (vdev->fops->open) {
if (video_is_registered(vdev))
ret = vdev->fops->open(filp);//實際就是vdev->fops
else
ret = -ENODEV;
}
}
記得我們之前把video_device放入到video_device[]嗎?就是這裡取了出來
然後呼叫vdev->fops->open(filp)
vdev->fops就是我們在uvc_register_video函式裡設定的:
vdev->fops = &uvc_fops
const struct v4l2_file_operations uvc_fops = {//實際的使用者操作
.owner = THIS_MODULE,
.open = uvc_v4l2_open,
.release = uvc_v4l2_release,
.unlocked_ioctl = video_ioctl2,
#ifdef CONFIG_COMPAT
.compat_ioctl32 = uvc_v4l2_compat_ioctl32,
#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
};
至於這個uvc_fops 裡的回撥函式,特別是ioctl,這是V4L2的重頭,就在下一章試著分析吧,我對這個也是比較模糊……
相關文章
- 嵌入式Linux驅動筆記(十八)------淺析V4L2框架之ioctlLinux筆記框架
- 嵌入式Linux驅動筆記(十六)------裝置驅動模型(kobject、kset、ktype)Linux筆記模型Object
- 嵌入式Linux驅動筆記(十二)------通俗易懂式分析瞭解spi框架Linux筆記框架
- 嵌入式Linux驅動筆記(十四)------詳解clock時鐘(CCF)框架及clk_get函式Linux筆記框架函式
- 嵌入式Linux驅動學習筆記(十六)------裝置驅動模型(kobject、kset、ktype)Linux筆記模型Object
- 嵌入式Linux驅動筆記(十一)------i2c裝置之mpu6050驅動Linux筆記
- 嵌入式Linux驅動筆記(十三)------spi裝置之RFID-rc522驅動Linux筆記
- 嵌入式Linux驅動筆記(十)------通俗易懂式分析瞭解i2c框架Linux筆記框架
- 新字元驅動框架驅動LED字元框架
- 嵌入式Linux驅動筆記(九)------dts裝置樹在2440使用Linux筆記
- Linux驅動開發筆記(一):helloworld驅動原始碼編寫、makefile編寫以及驅動編譯Linux筆記原始碼編譯
- Binder 驅動詳解(下)
- Binder 驅動詳解(上)
- 詳解資料驅動
- linux驅動之LED驅動Linux
- 【linux】驅動-5-驅動框架分層分離&實戰Linux框架
- 字元驅動框架字元框架
- 字元裝置驅動 —— 字元裝置驅動框架字元框架
- Windows驅動程式框架Windows框架
- SPI驅動框架一框架
- SPI轉can晶片CSM300詳解以及Linux驅動移植除錯筆記晶片Linux除錯筆記
- ArmSoM系列板卡 嵌入式Linux驅動開發實戰指南 之 字元裝置驅動Linux字元
- RealSence 驅動及ROS包配置筆記ROS筆記
- 【原創】Linux PCI驅動框架分析(一)Linux框架
- 【原創】Linux PCI驅動框架分析(二)Linux框架
- 學Linux驅動: 應該先了解驅動模型Linux模型
- 【linux】驅動-7-平臺裝置驅動Linux
- linux核心匯流排驅動模型-驅動篇Linux模型
- Linux裝置驅動之字元裝置驅動Linux字元
- Linux驅動開發筆記(四):裝置驅動介紹、熟悉雜項裝置驅動和ubuntu開發雜項裝置DemoLinux筆記Ubuntu
- 詳解kernel中watchdog 驅動程式
- 嵌入式Linux驅動筆記(十五)------編譯使用tslib支援LCD觸控式螢幕Linux筆記編譯
- 【linux】驅動-6-匯流排-裝置-驅動Linux
- Linux驅動開發筆記(三):基於ubuntu的驅動、makefile編寫以及編譯載入流程Linux筆記Ubuntu編譯
- 基於Linux的tty架構及UART驅動詳解Linux架構
- 分享一個LCD驅動框架框架
- Linux裝置驅動探究第1天----spi驅動(1)Linux
- 《Linux裝置驅動開發詳解(第2版)》——第1章Linux裝置驅動概述及開發環境構建1.1裝置驅動的作用Linux開發環境