一、在了解pic启动开发前,作为开发人员需了解以下内核结构体:
struct pci_device_id {
__u32 vendor, device;/* Vendor and device ID or PCI_ANY_ID*/
__u32 subvendor, subdevice;/* Subsystem ID's or PCI_ANY_ID */
__u32 class, class_mask;/* (class,subclass,prog-if) triplet */
kernel_ulong_t driver_data;/* Data private to the driver */
};
vendor:标识硬件制造商,是一个16位的寄存器。
device:设备ID,由制造商选择,也是一个16位的寄存器。一般与厂商ID配对生成一个唯一的32位硬件设备标识符。
class:每个外部设备属于某个类(class),也是一个16位的寄存器。当某个驱动程序可支持多个相似的设备,
每个具有不同的签名,但都属于同一个类,这时,就可以用class类对它们的外设进行识别。
该结构用来区分各个设备。(即该驱动程序控制的设备)
每个驱动程序可控制多个设备,这时用数组:
staticstruct pci_device_id example_pci_tbl [] __initdata ={
{PCI_VENDOR_ID_EXAMPLE, PCI_DEVICE_ID_EXAMPLE, PCI_ANY_ID, PCI_ANY_ID,0,0, EXAMPLE},
{0,}
};
注意:不管你这里匹配了多少设备,记得最后一个都是{0,}。
介绍下面两个宏
PCI_DEVICE(vendor, device)
创建一个仅和特定厂商及设备ID相匹配的struct pci_device_id。它把结构体的subvendor和subdevice设为PCI_ANY_ID。PCI_ANY_ID定义如下:
#define PCI_ANY_ID (~0)
PCI_DEVICE_CLASS(device_class, device_class_mask)
创建一个和特定PCI类相匹配的struct pci_device_id。
struct pci_driver {
struct list_head node;
char*name;
conststruct pci_device_id *id_table;/* must be non-NULL for probe to be called */
int(*probe)(struct pci_dev *dev,conststruct pci_device_id *id);/* New device inserted */
void(*remove)(struct pci_dev *dev);/* Device removed (NULL if not a hot-plug capable driver) */
int(*suspend)(struct pci_dev *dev,pm_message_t state);/* Device suspended */
int(*suspend_late)(struct pci_dev *dev,pm_message_t state);
int(*resume_early)(struct pci_dev *dev);
int(*resume)(struct pci_dev *dev);/* Device woken up */
void(*shutdown)(struct pci_dev *dev);
struct pci_error_handlers *err_handler;
struct device_driver driver;
struct pci_dynids dynids;
};
可以认为pci_driver结构体对象就表示一个驱动,重点说明的成员如下:
id_table: 表示该驱动程序控制的所有设备
probe: 当驱动程序与设备匹配后,就会调用probe指向的函数。(通常在函数里激活设备,占用I/O端口和内存)
remove: 指向卸载设备函数
在pci_driver结构体里有包含struct device_driver,下面有说明。
在驱动程序开发里,device_driver里包含总线信息
struct pci_dev {
struct list_head bus_list;/* node in per-bus list */
struct pci_bus *bus;/* bus this device is on */
struct pci_bus *subordinate;/* bus this device bridges to */
void*sysdata;/* hook for sys-specific extension */
struct proc_dir_entry *procent;/* device entry in /proc/bus/pci */
struct pci_slot *slot;/* Physical slot this device is in */
unsignedint devfn;/* encoded device & function index */
unsignedshort vendor;
unsignedshort device;
unsignedshort subsystem_vendor;
unsignedshort subsystem_device;
unsignedintclass;/* 3 bytes: (base,sub,prog-if) */
u8 revision;/* PCI revision, low byte of class word */
u8 hdr_type;/* PCI header type (`multi' flag masked out) */
u8 pcie_cap;/* PCI-E capability offset */
u8 pcie_type;/* PCI-E device/port type */
u8 rom_base_reg;/* which config register controls the ROM */
u8 pin;/* which interrupt pin this device uses */
struct pci_driver *driver;/* which driver has allocated this device */
u64 dma_mask;/* Mask of the bits of bus address this
device implements. Normally this is
0xffffffff. You only need to change
this if your device has broken DMA
or supports 64-bit transfers. */
struct device_dma_parameters dma_parms;
pci_power_t current_state;/* Current operating state. In ACPI-speak,
this is D0-D3, D0 being fully functional,
and D3 being off. */
int pm_cap;/* PM capability offset in the
configuration space */
unsignedint pme_support:5;/* Bitmask of states from which PME#
can be generated */
unsignedint pme_interrupt:1;
unsignedint d1_support:1;/* Low power state D1 is supported */
unsignedint d2_support:1;/* Low power state D2 is supported */
unsignedint no_d1d2:1;/* Only allow D0 and D3 */
unsignedint mmio_always_on:1;/* disallow turning off io/mem
decoding during bar sizing */
unsignedint wakeup_prepared:1;
unsignedint d3_delay;/* D3->D0 transition time in ms */
#ifdef CONFIG_PCIEASPM
struct pcie_link_state *link_state;/* ASPM link state. */
#endif
pci_channel_state_t error_state;/* current connectivity state */
struct device dev;/* Generic device interface */
int cfg_size;/* Size of configuration space */
/*
* Instead of touching interrupt line and base address registers
* directly, use the values stored here. They might be different!
*/
unsignedint irq;
struct resource resource[DEVICE_COUNT_RESOURCE];/* I/O and memory regions + expansion ROMs */
resource_size_t fw_addr[DEVICE_COUNT_RESOURCE];/* FW-assigned addr */
/* These fields are used by common fixups */
unsignedint transparent:1;/* Transparent PCI bridge */
unsignedint multifunction:1;/* Part of multi-function device */
/* keep track of device state */
unsignedint is_added:1;
unsignedint is_busmaster:1;/* device is busmaster */
unsignedint no_msi:1;/* device may not use msi */
unsignedint block_ucfg_access:1;/* userspace config space access is blocked */
unsignedint broken_parity_status:1;/* Device generates false positive parity */
unsignedint irq_reroute_variant:2;/* device needs IRQ rerouting variant */
unsignedint msi_enabled:1;
unsignedint msix_enabled:1;
unsignedint ari_enabled:1;/* ARI forwarding */
unsignedint is_managed:1;
unsignedint is_pcie:1;/* Obsolete. Will be removed.
Use pci_is_pcie() instead */
unsignedint needs_freset:1;/* Dev requires fundamental reset */
unsignedint state_saved:1;
unsignedint is_physfn:1;
unsignedint is_virtfn:1;
unsignedint reset_fn:1;
unsignedint is_hotplug_bridge:1;
unsignedint __aer_firmware_first_valid:1;
unsignedint __aer_firmware_first:1;
pci_dev_flags_t dev_flags;
atomic_t enable_cnt;/* pci_enable_device has been called */
u32 saved_config_space[16];/* config space saved at suspend time */
struct hlist_head saved_cap_space;
struct bin_attribute *rom_attr;/* attribute descriptor for sysfs ROM entry */
int rom_attr_enabled;/* has display of the rom attribute been enabled? */
struct bin_attribute *res_attr[DEVICE_COUNT_RESOURCE];/* sysfs file for resources */
struct bin_attribute *res_attr_wc[DEVICE_COUNT_RESOURCE];/* sysfs file for WC mapping of resources */
#ifdef CONFIG_PCI_MSI
struct list_head msi_list;
#endif
struct pci_vpd *vpd;
#ifdef CONFIG_PCI_IOV
union{
struct pci_sriov *sriov;/* SR-IOV capability related */
struct pci_dev *physfn;/* the PF this VF is associated with */
};
struct pci_ats *ats;/* Address Translation Service */
#endif
};
可以看到该结构体包含总线、驱动、device和一个PCI设备几乎所有的硬件信息,包括厂商ID、设备ID、各种资源等:
struct device 就不多说了:它用于描述设备相关的信息设备之间的层次关系,以及设备与总线、驱动的关系。
二、理解设备和驱动加载先后顺序
Linux驱动先注册总线,总线上可以先挂device,也可以先挂driver,那么究竟怎么控制先后的顺序呢。
Linux关于总线、设备、驱动的注册顺序
设备挂接到总线上时,与总线上的所有驱动进行匹配(用bus_type.match进行匹配),
如果匹配成功,则调用bus_type.probe或者driver.probe初始化该设备,挂接到总线上
如果匹配失败,则只是将该设备挂接到总线上。
驱动挂接到总线上时,与总线上的所有设备进行匹配(用bus_type.match进行匹配),
如果匹配成功,则调用bus_type.probe或者driver.probe初始化该设备;挂接到总线上
如果匹配失败,则只是将该驱动挂接到总线上。
需要重点关注的是总线的匹配函数match(),驱动的初始化函数probe()
1. platform_bus_type--总线先被kenrel注册。
2. 系统初始化过程中调用platform_add_devices或者platform_device_register,将平台设备(platform devices)注册到平台总线中(platform_bus_type)
3. 平台驱动(platform driver)与平台设备(platform device)的关联是在platform_driver_register或者driver_register中实现,一般这个函数在驱动的初始化过程调用。
通过这三步,就将平台总线,设备,驱动关联起来。
先看看pci_register_driver注册驱动过程
1.首先会调用int _pci_register_driver()函数,在这个函数里面,首先会把pci_driver存储的信息复制到device_driver中,
因为内核最终要注册的是一个device的driver信息,而不单单指pci设备的driver信息。在复制完成之后,内核会调用driver_register()函数注册device_driver的信息。
2.driver_register()函数,driver_register首先会检查struct kset drivers链表中有没有对应名称的dirver: driver_find(drv->name, drv->bus); 如果已经有了,
则重新把对应的driver加载到struct kset drivers中,如果没有,则会执行bus_add_drivers函数,把当前的驱动加载到struct kset drivers中。
3.bus_add_drivers函数,这个函数中重要的一个操作就是driver_attach操作,在driver_attach函数中会调用dirver_match_deivce函数来判断总线上是否有设备与当前
driver相匹配,实现的方式应该就是比较vendor_id和device_id。如果当前bus中有设备和当前driver相匹配,那么就会执行driver_probe_device函数,对当前deivce进行
初始化,这个probe函数里最终就是指向的驱动程序中编写的probe函数。driver和deivce就绑定完毕,而且device也已经初始化完毕。
Linux中总线、设备、驱动是如何关联的?
总线、设备、驱动,也就是bus、device、driver,在内核里都会有它们自己专属的结构,在include/linux/device.h 里定义。
首先是总线,bus_type.
struct bus_type {
const char * name;
struct subsystem subsys;//代表自身
struct kset drivers; //当前总线的设备驱动集合
struct kset devices; //所有设备集合
struct klist klist_devices;
struct klist klist_drivers;
struct bus_attribute * bus_attrs;//总线属性
struct device_attribute * dev_attrs;//设备属性
struct driver_attribute * drv_attrs;
int (*match)(struct device * dev, struct device_driver * drv);//设备驱动匹配函数
int (*uevent)(struct device *dev, char **envp,
int num_envp, char *buffer, int buffer_size);//热拔插事件
int (*probe)(struct device * dev);
int (*remove)(struct device * dev);
void (*shutdown)(struct device * dev);
int (*suspend)(struct device * dev, pm_message_t state);
int (*resume)(struct device * dev);
};
下面是设备device的定义:
struct device {
struct device * parent; //父设备,一般一个bus也对应一个设备。
struct kobject kobj;//代表自身
char bus_id[BUS_ID_SIZE];
struct bus_type * bus; /* 所属的总线 */
struct device_driver *driver; /* 匹配的驱动*/
void *driver_data; /* data private to the driver 指向驱动 */
void *platform_data; /* Platform specific data,由驱动定义并使用*/
///更多字段忽略了
};
下面是设备驱动定义:
struct device_driver {
const char * name; //该driver的名称
struct bus_type * bus;//所属总线
struct completion unloaded;
struct kobject kobj;//代表自身
struct klist klist_devices;//设备列表
struct klist_node knode_bus;
struct module * owner;
int (*probe) (struct device * dev);
int (*remove) (struct device * dev);
void (*shutdown) (struct device * dev);
int (*suspend) (struct device * dev, pm_message_t state);
int (*resume) (struct device * dev);
const struct dev_pm_ops *pm;
struct driver_private *p;
};
我们会发现,struct bus_type中有成员struct kset drivers和struct kset devices,同时struct device中有两个成员struct bus_type *bus和struct device_driver *driver
, struct device_driver中有两个成员struct bus_type *bus和struct klist klist_devices。struct device中的bus表示这个设备连到哪个总线上,driver表示这个设备的驱动
是什么,struct device_driver中的bus表示这个驱动属于哪个总线,klist_devices表示这个驱动都支持哪些设备,因为这里device是复数,又是list,更因为一个驱动可以支持多个设备,
而一个设备只能绑定一个驱动。当然,struct bus_type中的drivers和devices分别表示了这个总线拥有哪些设备和哪些驱动。
还有上面device 和driver结构里出现的kobject 结构是什么?kobject 和kset 都是Linux 设备模型中最基本的元素。一般来说应该这么理解,整个Linux 的设备模型是一个OO
的体系结构,总线、设备和驱动都是其中鲜活存在的对象,kobject 是它们的基类,所实现的只是一些公共的接口,kset 是同种类型kobject 对象的集合,也可以说是对象的容器。
那么总线、设备和驱动之间是如何关联的呢?
先说说总线中的那两条链表是怎么形成的。内核要求每次出现一个设备就要向总线汇报,或者说注册,每次出现一个驱动,也要向总线汇报,或者说注册。比如系统初始化的
时候,会扫描连接了哪些设备,并为每一个设备建立起一个struct device 的变量,每一次有一个驱动程序,就要准备一个truct device_driver 结构的变量。把这些变量统
统加入相应的链表,device插入devices链表,driver 插入drivers 链表。这样通过总线就能找到每一个设备,每一个驱动。
设备和驱动又是如何联系?
原来是把每一个要用的设备在计算机启动之前就已经插好了,插放在它应该在的位置上,然后计算机启动,然后操作系统开始初始化,总线开始扫描设备,每找到一个设备,
就为其申请一个struct device 结构,并且挂入总线中的devices 链表中来,然后每一个驱动程序开始初始化,开始注册其struct device_driver 结构,然后它去总线的
devices 链表中去寻找(遍历),去寻找每一个还没有绑定驱动的设备,struct device 中的struct device_driver 指针仍为空的设备,然后它会去观察这种设备的特征,
看是否是他所支持的设备,如果是,那么调用一个叫做device_bind_driver 的函数,然后他们就结为了秦晋之好。换句话说,把struct device 中的
struct device_driver driver指向这个驱动,而struct device_driver driver 把struct device 加入他的那struct klist klist_devices链表中来。就这样,bus、device
和driver,这三者之间或者说他们中的两两之间,就给联系上了。知道其中之一,就能找到另外两个。
但现在情况变了,出现了一种新的名词,叫热插拔。设备可以在计算机启动以后在插入或者拔出计算机了。设备可以在任何时刻出现,而驱动也可以在任何时刻被加载,所以,
出现的情况就是,每当一个struct device 诞生,它就会去bus 的drivers链表中寻找自己的另一半,反之,每当一个struct device_driver 诞生,它就去bus的devices
链表中寻找它的那些设备。如果找到了合适的,那么OK,和之前那种情况一下,调device_bind_driver 绑定好。如果找不到,没有关系,等待吧!
三、开始pic驱动开发基本步骤
// 在这里指定PCI设备ID,PCI_VDEVICE会组装厂家ID和PCI设备ID
static const struct pci_device_id misc_pci_tbl[] = {
{ PCI_VDEVICE(INTEL, 0x0f1c), 0 },
{ PCI_VDEVICE(INTEL, 0x0f12), 1 }, // SMBus
{0,},
};
MODULE_DEVICE_TABLE(pci, misc_pci_tbl);
static struct pci_driver hamachi_driver = {
.name = DRV_NAME,
.id_table = hamachi_pci_tbl,
.probe = hamachi_init_one,
.remove = __devexit_p(hamachi_remove_one),
};
static int hamachi_init_one(struct pci_dev *dev, const struct pci_device_id *id)
{
/*
此函数里的dev,在PCI总线里已经被初始化了。接着我们该实现如何对该设备进行控制。
无非,就是读写操作
*/
1. pci_enable_device(dev);//启用设备I/O,内存
2.分配I/O端口空间或内存空间(如果是分配I/O内存,则需要把外设备寄存器地址映射到内存中才能访问设备),供驱动程序对设备进行访问。
3.注册主设备号(内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 register_chrdev_region()、alloc_chrdev_region() 和 register_chrdev())
4.cdev_init和cdev_add初始化和注册字符设备文件
在cdev_init时会提供访问设备文件的方法(函数)
说明:struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
打开文件设备,读写就可以访问设备了。
}
static int __init hamachi_init (void)
{
return pci_register_driver(&hamachi_driver);
}
static void __exit hamachi_exit (void)
{
pci_unregister_driver(&hamachi_driver);
}
module_init(hamachi_init);
module_exit(hamachi_exit);
说明:在加载模块时使用宏module_init将hamachi_init函数注册到内核,hamachi_init函数里注册驱动程序,并挂在PCI总线上,当匹配到PCI总线上的device时,
就会调用probe函数.device在总线扫描到设备时就申请device结构,初始化挂在总线上。
probe函数里一般实现,1.激活设备 2.分配I/O端口或I/O内存 3.分配设备号;2.提供操作设备文件的方法。
每个PCI设备包括三个寻址空间:配置空间,I/O端口空间,内存空间,其中I/0和内存空间用来与外设备进行通讯。
1. 获取物理地址或I/O端口
函数pci_resource_start(struct pci_dev *dev, int bar)
说明:用来获取设备的物理地址或设备的I/O端口(每个PCI设备有0-5一共6个地址空间,我们通常只使用前两个bar的范围为0-5)
pio_start = pci_resource_start (pdev, 0);
pio_end = pci_resource_end (pdev, 0);
pio_flags = pci_resource_flags (pdev, 0);
pio_len = pci_resource_len (pdev, 0);
mmio_start = pci_resource_start (pdev, 1);
mmio_end = pci_resource_end (pdev, 1);
mmio_flags = pci_resource_flags (pdev, 1);
mmio_len = pci_resource_len (pdev, 1);
这样看来0号bar就是提供的io映射,1号bar提供内存映射。
pci_resource_start 取得的物理地址是PCI设备上内存的地址(I/O端口).
IOREMAP可以转换这个地址.因为转换的过程就是给这个物理地址建立一个页表,分配一个相应的虚拟地址
得到内存长度
pci_resource_length(struct pci_dev *dev, int bar) Bar值的范围为0-5。
从配置区相应寄存器得到I/O区域的内存的相关标志:
pci_resource_flags(struct pci_dev *dev, int bar) Bar值的范围为0-5。
2. 分配I/O端口空间
void request_region(unsigned long from,
unsigned long num, const char *name)
参数1:io端口的基地址。
参数2:io端口占用的范围。
参数3:使用这段io地址的设备名。
这个函数分配的是I/O端口空间,可以直接使用inb(),outb()等I/O端口函数直接访问(或ioport_map端口映射,方便访问)。告诉内核from开始num个数端口被占用
分配I/O内存空间
void request_mem_region(unsigned long from,
unsigned long num, const char *name)
参数1:io端口的基地址。
参数2:io端口占用的范围。
参数3:使用这段io地址的设备名。
request_mem_region函数并没有做实际性的映射工作,只是告诉内核要使用一块内存地址,标志占有。重要的还是ioremap函数,ioremap()则是映射地址了。
pci_request_regions (pdev, DRV_NAME);
通知内核该设备对应的IO端口和内存资源已经使用,其他的PCI设备不要再使用这个区域
1.使用Linux内核 提供了如下一些访问I/O端口的内联函数:
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
2.void *ioport_map( unsigned long port, unsigned int count );
通过这个函数,可以把port开始的count个连续端口重映射为一段“内存空间”。然后就可以在其返回的地址上象访问I/O内存一样访问这几个I/O端口。
当不需要这种映射时,需要调用撤消函数: void iport_unmap(void *addr);
3.I/O内存映射,确保I/O内存对内核是可以访问的。使用函数void *ioremap(unsigned long phys_addr, unsigned long size);
phys_addr:要映射的内存地址
size:内存大小
pci驱动为什么要ioremap?
根据计算机体系和总线不同,I/O 内存可分为可以或者不可以通过页表来存取。若通过页表存取,内核必须先重新编排物理地址,使其对驱动程序可见,
这就意味着在进行任何I/O操作之前,你必须调用ioremap;如果不需要页表,I/O内存区域就类似于I/O端口,你可以直接使用适当的I/O函数读写它们。
由于边际效应的缘故,不管是否需要 ioremap,都不鼓励直接使用I/O内存指针,而应使用专门的I/O内存操作函数。这些I/O内存操作函数不仅在所有平台上是安全,
而且对直接使用指针操作 I/O 内存的情况进行了优化.
那什么叫边际效应呢? 我查了查 这个答案比较靠谱
(个人理解就是副作用):读取某个地址时可能导致这个地址的内容发生变化,比如很多中断寄存器的值一经读取,便自动清零
附加:copy_to_user和copy_from_user就是在进行驱动相关程序设计的时候,要经常遇到的两个函数。由于内核空间与用户空间的内存不能直接互访,因此借助函数copy_to_user()完成内核空间到用户空间的复制,函数copy_from_user()完成用户空间到内核空间的复制。
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
第一个参数to是用户空间的数据源地址指针,
第二个参数from是内核空间的数据目标地址指针,
第三个参数n是数据的长度。
unsigned long copy_from_user(void * to, const void __user * from, unsigned long n)
第一个参数to是内核空间的数据目标地址指针,
第二个参数from是用户空间的数据源地址指针,
第三个参数n是数据的长度。
内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 register_chrdev_region()、alloc_chrdev_region() 和 register_chrdev()。
(1)register_chrdev 比较老的内核注册的形式 早期的驱动
(2)register_chrdev_region/alloc_chrdev_region + cdev 新的驱动形式
区别:register_chrdev()函数是老版本里面的设备号注册函数,可以实现静态和动态注册两种方法,主要是通过给定的主设备号是否为0来进行区别,为0的时候为动态注册。register_chrdev_region以及alloc_chrdev_region就是将上述函数的静态和动态注册设备号进行了拆分的强化。
register_chrdev_region(dev_t first,unsigned int count,char *name)
First :要分配的设备编号范围的初始值, 这组连续设备号的起始设备号, 相当于register_chrdev()中主设备号
Count:连续编号范围. 是这组设备号的大小(也是次设备号的个数)
Name:编号相关联的设备名称. (/proc/devices); 本组设备的驱动名称
alloc_chrdev_region函数,来让内核自动给我们分配设备号
(1)register_chrdev_region是在事先知道要使用的主、次设备号时使用的;要先查看cat /proc/devices去查看没有使用的。
(2)更简便、更智能的方法是让内核给我们自动分配一个主设备号,使用alloc_chrdev_region就可以自动分配了。
(3)自动分配的设备号,我们必须去知道他的主次设备号,否则后面没法去mknod创建他对应的设备文件。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
1:这个函数的第一个参数,是输出型参数,获得一个分配到的设备号。可以用MAJOR宏和MINOR宏,将主设备号和次设备号,提取打印出来,看是自动分配的是多少,方便我们在mknod创建设备文件时用到主设备号和次设备号。 mknod /dev/xxx c 主设备号 次设备号
2:第二个参数:次设备号的基准,从第几个次设备号开始分配。
3:第三个参数:次设备号的个数。
4:第四个参数:驱动的名字。
5:返回值:小于0,则错误,自动分配设备号错误。否则分配得到的设备号就被第一个参数带出来。
cdev介绍
cdev是一个结构体,里面的成员来共同帮助我们注册驱动到内核中,表达字符设备的,将这个struct cdev结构体进行填充,主要填充的内容就是
struct cdev { struct kobject kobj; struct module *owner;//填充时,值要为 THIS_MODULE,表示模块 const struct file_operations *ops;//这个file_operations结构体,注册驱动的关键,要填充成这个结构体变量 struct list_head list; dev_t dev;//设备号,主设备号+次设备号 unsigned int count;//次设备号个数 };
file_operations这个结构体变量,让cdev中的ops成员的值为file_operations结构体变量的值。这个结构体会被cdev_add函数想内核注册
cdev结构体,可以用很多函数来操作他。
如:
cdev_alloc:让内核为这个结构体分配内存的
cdev_init:将struct cdev类型的结构体变量和file_operations结构体进行绑定的
void cdev_init(struct cdev *, const struct file_operations *);
cdev_add:向内核里面添加一个驱动,注册驱动
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
该函数向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经可以使用了。
当然这里还需提供两个参数:
(1)第一个设备号 dev,
(2)和该设备关联的设备编号的数量。
这两个参数直接赋值给struct cdev 的dev成员和count成员
cdev_del:从内核中注销掉一个驱动。注销驱动
设备号
(1)dev_t类型(包括了主设备号和次设备号 不同的内核中定义不一样有的是16位次设备号和16位主设备号构成 有的是20为次设备号12位主设备号 )
(2)MKDEV、MAJOR、MINOR三个宏
MKDEV: 是用来将主设备号和次设备号,转换成一个主次设备号的。(设备号)
MAJOR: 从设备号里面提取出来主设备号的。
MINOR宏:从设备号中提取出来次设备号的。