文章目录
更新:2023年7月6日:因为面试问到bar基地址,结合开源代码+《精通linux设备驱动程序》,重新学习并整理
代码整理思路:书上demo–》开源代码—》项目代码(受限,无法公开)
12 PCI接口
PCI:外围设备互联
12.1 PCI寻址
PCI外设:域,总线号,设备号,功能号标识
PCI域:占用多达256个总线,,每个总线占用32个设备,每个设备最多8个功能,可被16位地址或key标识
2
16
=
256
∗
32
∗
8
2^{16}=256*32*8
216=256∗32∗8
借助数据结构pci_dev在设备上操作
(1)XX:YY:Z
查询设备列表:lspci
开头的xx:yy:z
xx:表示总线号,一个PCI域能容纳256个总线
YY:是PCI设备编号,每个总线可以支持32个PCI设备
Z:表示设备功能,每个PCI设备可以容纳8个PCI功能
PCI系统布局(lspci -t)
PCI系统的整体布局为树形,其中每个总线连接到上一级总线,直到树根的0号总线。
Host/PCI桥:用于连接CPU与PCI根总线,在PC中,内存控制器也通常被集成到Host/PCI桥设备芯片,Host/PCI桥通常被称 为“北桥芯片组”。
PCI/ISA桥:用于连接旧的ISA总线。PCI/ISA桥也被称为“南桥芯片组”。
PCI-to-PCI桥:用于连接PCI主总线与次总线。
桥:连接两个总线的特殊PCI外设
简图:
显示硬件地址时:2个值:8位总线号 + 8 位 设备和功能号
3个值:(bus (8:256)+ device(5:32) + function(3:8))
4个值:(domain + bus + device +function)
表示:常用16进制显示
12.1.1 PCI寻址
PCI设备包括3个寻址空间:配置空间、IO端口和设备内存。
12.1.2 配置空间
PCI总线规范定义的配置空间总长度为256字节
前64字节是标准的,配置头主要用来识别设备、定义主机访问PCI卡的方式(I/O访问或者存储器访问,还有中断信息)
其余的192个字节称为本地配置空间,主要定义卡上局部总线的特性、本地空间基地址及范围等。
PCI寄存器为小端模式(低对低,高对高)
常用:只读寄存器:verdorID ,deviceID,和class code驱动借助它们来查找设备。
subsystem verdorID和subsystem deviceID 被供应商设置来进一步区分类似的设备。
寄存器细节
verdorID:16位寄存器,标识硬件制造商 ,例如:intel 0x8086
deviceID:16位寄存器,和供应商ID,组成唯一的32位标识符。设备驱动依靠签名来标识它的设备。可在硬件手册中找到对于目标设备要寻找的值。
class code:类寄存器,16位:高8位标识“基类”;例如“ethernet和token ring 是2个类,都属于network群
一些驱动可支持类似的设备,每个设备都有一个不同的签名,但是都属于同样的类。驱动依赖类寄存器标识他们的外设。
pci驱动可告知内核它支持的设备
示例
lspci -x 查看网卡配置区的详细情况
注意:PCI寄存器是小端字节序(如:厂家ID:0x177D)
配置空间访问
读/写函数
#include<linux/pci.h>
参数:
struct pci_dev是PCI设备结构体,
where是访问配置空间的字节位置(offset)
val:对于读函数来说,是数据缓冲区的指针(将where的数据读取到val);对写函数来说,是准备写的数据(将val的数据写入where)
示例:
unsigned char irq;
pci_read_config_word(pdev,PCI_INTERRUPT_LINE,&irq);
宏-偏移地址
配置空间的偏移地址都在文件include/linux/pci_regs.h中有详细定义,所以要用PCI_INTERRUPT_LINE而不是60来定义
#include<linux/pci_regs.h>
示例
基地址寄存器的偏移量:
程序定义
bar地址读取:
12.2 I/O和内存空间
PCI设备包括 3 个寻址空间:
-
配置空间
-
I/O 端口:
-
设备内存:映射缓冲,存放数据,不是所有卡都有可寻址的内存区域
PCI有6个I/O或内存区域。 IO区域包含寄存器,内存区域存放数据。例如:视频卡的I/O区域包含控制寄存器,内存区域映射帧缓冲。并不是所有的卡都有课寻址的内存区域。
I/O寄存器具有边际效应(side effect)。I/O寄存器不应该由CPU缓存。
12.2.1 访问I/O区域
(1)I/O基址
(1)从配置区相应的基地址寄存器里得到I/O区域的基址
这里假定设备控制寄存器被映射到由变量bar关联的I/O区域,bar值变化范围为0-5 (PCI有6个I/O或内存区域。 )
// 从配置区相应寄存器得到I/O区域的基址
unsigned long pci_resource_start(struct pci_dev *dev, int bar);// Bar值的范围为0-5
// 或者
pci_read_config_dword(pci_dev, PCI_BASE_ADDRESS_0,pvalue);
(2)获取I/O区域,并标明设备
(2)用内核的request_region()常规机制获取这个I/O区域,并标明她对应的设备;或者pci_request_region()
request_region(io_base,length,"my_driver");
/* 申请起始于first的n个端口,参数name是设备名称
* 返回值:成功:非NULL;失败:NULL
*/
struct resource *request_region(unsigned long first, unsigned long n, const char *name);
void release_region(unsigned long start, unsigned long n);// 卸载
/* Reserve PCI I/O and memory resource
* pdev:要预留的设备
* bar:要预留的总线
* res_name :设备名称
*/
int pci_request_region(struct pci_dev *pdev, int bar, const char *res_name);
(3)基址+偏移
用寄存器手册上的偏移地址加上第一步得到的基址,用inb()和outb()函数访问这些寄存器
//read:
register_data = inl(io_base + REGISTER_OFFSET);
// write
outl(register_data,io_base + REGISTER_OFFSET);
12.2.2 访问内存
为了能操作PCI的内存区域,如前面提到的PCI视频卡上的帧缓冲,按下面步骤进行
(1)获得基址、内存区域长度以及内存相关标志
这里假定设备内存区域被映射到基址寄存器bar
unsigned long mmio_base = pci_resource_start(pdev,bar);
unsigned long mmio_length = pci_resource_length(pdev,bar);
unsigned long mmio_flags = pci_resource_flags(pdev,bar);
(2)标记owner
request_mem_region(mmio_base,mmio_length,"my_driver");
// 用前文提到的封装函数pci_request_region()也可以
(3)CPU访问
让CPU访问第一步获得的设备内存。为防止发生意外,某些内存空间(如包含寄存器的)设计成了不让CPU预取或不可启用高速缓存。
对于其他内存,CPU可以启用高速缓存。
根据内存区域的访问标志,使用合适的函数可以得到与映射区域相对应的内核虚拟地址。
为安全期间,并避免执行前述检查。使用lib/iomap.c定义的函数pci_iomap代替
buffer = pci_iomap(pdev,bar,mmio_length);
void __iomem *buffer;
if(flag & IORESOURCE_CACHEABEL)
{
buffer = ioremap(mmio_base,mmio_length);
}
else
{
buffer = ioremap_nocache(mmio_base,mmio_length);
}
12.2.3 相关函数原型
/* Reserve PCI I/O and memory resource
* pdev:要预留的设备
* bar:要预留的总线
* res_name :设备名称
*/
// PCI设备的I/O区域已经被集成到通用资源管理。因此我们无需访问配置变量来了解设备被映射到内存或I/O空间的何处。但需要调用pci_request_region()来验证没有设备已经在使用相同的地址资源。
int pci_request_region(struct pci_dev *pdev, int bar, const char *res_name);
// 从配置区相应寄存器得到I/O区域的基址
unsigned long pci_resource_start(struct pci_dev *dev, int bar);// Bar值的范围为0-5
// 从配置区相应寄存器得到I/O区域的内存区域长度:
unsigned long pci_resource_length(struct pci_dev *dev, int bar);
// 返回该资源相关联的标志
unsigned long pci_resource_flags(struct pci_dev *dev, int bar);
// 返回第bar个区域的尾地址
// 是最后一个可用的地址,而不是下一个区域的首地址
unsigned long pci_resource_end(struct pci_dev *dev, int bar);// Bar值的范围为0-5
细节差异:SI检索的是宏,书中是函数
分配I/O内存之后,我们还需要保证该I/O内存对于内核而言是可访问的,必须首先建立映射。映射的建立有ioremap完成。
ioremap:将一个IO地址空间映射到内核的虚拟地址空间上去。
返回值:虚拟地址,用来访问指定的物理内存区域
应用:映射PCI缓冲区地址到内核空间。
iounmap:释放虚拟地址
注意:
- 由ioremap返回的地址不应当做指向内存的指针直接访问,而应该使用其他I/O函数
- 会修改页表
12.2.4 访问内存-示例代码
/**
* \brief maps a PCI BAR
* @param oct Pointer to Octeon device
* @param baridx bar index
* @param max_map_len maximum length of mapped memory
*/
static inline int octeon_map_pci_barx(struct octeon_device *oct,
int baridx, int max_map_len)
{
u32 mapped_len = 0;
// 预留I/O和内存资源,标记拥有者DRV_NAME
// PCI设备的I/O区域已经被集成到通用资源管理。因此我们无需访问配置变量来了解设备被映射到内存或I/O空间的何处。但需要调用pci_request_region()来验证没有设备已经在使用相同的地址资源。
if (pci_request_region(oct->pci_dev, baridx * 2, DRV_NAME)) {
lio_dev_err(oct, "pci_request_region failed for bar %d\n",
baridx);
return 1;
}
// 获取基地址首地址
oct->mmio[baridx].start = pci_resource_start(oct->pci_dev, baridx * 2);
// 获取映射空间的长度
oct->mmio[baridx].len = pci_resource_len(oct->pci_dev, baridx * 2);
mapped_len = oct->mmio[baridx].len;
if (!mapped_len)
goto err_release_region;
// 校验:如果需要指定内存大小,且有足够的内存资源,即内存长度>所需最大值
// 此处:max_map_len = 0
if (max_map_len && (mapped_len > max_map_len))
mapped_len = max_map_len;// 修改为指定的大小
// I/O地址映射到内核空间,内核可借助生成的虚拟地址访问物理内存,也可以使用pci_iomap(),低版本中ioremap调用了ioremap_nocache()
oct->mmio[baridx].hw_addr =
ioremap(oct->mmio[baridx].start, mapped_len);
oct->mmio[baridx].mapped_len = mapped_len;
// debug
lio_dev_dbg(oct, "BAR%d start: 0x%llx mapped %u of %u bytes\n",
baridx, oct->mmio[baridx].start, mapped_len,
oct->mmio[baridx].len);
// 空值校验
if (!oct->mmio[baridx].hw_addr) {
lio_dev_err(oct, "error ioremap for bar %d\n",
baridx);
goto err_release_region;
}
// 映射成功的标签
oct->mmio[baridx].done = 1;
return 0;
err_release_region:
pci_release_region(oct->pci_dev, baridx * 2);
return 1;
}
12.3 DMA
外围设备直接传送数据到内存,不经过CPU的干预,DMA可以成倍的提高外围设备的性能。
DMA由DMA控制器初始化。DMA控制器位于PC主板的南桥上( PCI/ISA桥:用于连接旧的ISA总线。PCI/ISA桥也被称为“南桥芯片组”)。南桥负责管理I/O总线,并启动DMA从外围设备直接读/写数据。
12.3.1 DMA的数据传送:同步和异步
异步:CPU和网卡直接传送数据帧
12.3.2 dma_set_mask
DMA缓冲区是DMA传送时用作源地址或者目的地址的内存区。DMA缓冲区大小收到总线接口能访问的地址限制。比如24位总线(ISA)的DMA缓冲区只占系统内存底部的16MB,称为ZONE_DMA
PCI总线默认32位,在32位平台下不会受到限制。可以用下列函数告诉内核系统中可用作DMA缓冲区的地址的特殊要求:DMA缓冲区的地址掩码
dma_set_mask(struct device *dev,u64 mask);
如果函数执行成功,就可以在mask指定的地址范围内进行DMA操作
12.3.3 总线地址:dma_addr_t
I/O设备需要的是DMA缓冲区的总线地址(文档:Dynamic DMA mapping Guide.txt)。DMA服务例程将DMA缓冲区的内核虚拟地址映射到总线地址上,以便设备和CPU都能访问DMA缓冲区。
总线地址类型:dma_addr_t ,在文件include/asm-x86(架构)/types.h
12.3.4 回弹缓冲区
回弹缓冲区驻留在可作为DMA缓冲区的内存区域里,在DMA请求的地址没有DMA功能的内存区域时,可以作为临时内存区存放数据。
比如,用DMA方式从32位PCI外设向地址高于4GB的目标传送数据时,如果没有IOMMU单元时,就需要回弹缓冲区。
12.3.5 分散/聚集
当需要的数据分布在不连续的内存上时,分散/聚集能对这些不连续缓冲区的数据进行一次性发送,反过来,DMA也能把数据从卡正确的传送到地址不连续的缓冲区中。
分散/聚集通过减少重复的DMA传送请求来提高效率。
12.3.6 PCI DMA API
内核为PCI驱动程序提供两类DMA服务。
(1)一致性DMA访问方法
DMA传输存在高速缓存一致性问题。为了提高处理器性能,在处理器和主存之间增加了高速缓存。在DMA过程中,数据在外围设备和主存之间直接传输,因此不会经过高速缓存。这样有可能导致数据的不一致性,因为处理器可能会使用高速缓存中过时的数据。
一致性DMA访问保证DMA传送数据的一致性。如果PCI设备和CPU都有可能干预DMA缓冲区,保证数据的一致性很重要。
/* pdev:PCI设备结构体
* size:DMA缓冲区的字节大小
* dma_handle:指向总线地址的指针
*/
void *pci_alloc_consistent(struct pci_dev *pdev,size_t size,dma_addr_t *dma_handle);
void *pci_free_consistent(struct pci_dev *pdev,size_t size,dma_addr_t *dma_handle);
这个函数分配一个DMA缓冲区,生成它的总线地址,并返回相关的内核虚拟地址.
(2)流式DMA访问
单一DMA
映射、去映射以及同步一个预先分配的单一DMA缓冲区。
dma_addr_t pci_map_single(struct pci_dev *pdev,void *ptr,size_t size,int direction);
pci_unmap_single();
pci_unmap_dma_sync();
前面3个参数分别是PCI设备结构体、预先分配的DMA缓冲区虚拟内核地址,缓冲区字节数
第4个参数:
- PCI_DMA_BIDIRRECTION:不建议
- PCI_DMA_TODEVICE
- PCI_DMA_FROMDEVICE
- PCI_DMA_NONE:调试时使用
分散/聚集DMA缓冲区链
映射、去映射并同步一个分散/聚集的DMA缓冲区链。
int pci_map_sg(struct pci_dev *pdev,struct scatterlist *sgl,int num_entries,int direction)
sgl表示分散内存的链表
num_entries表示分散内存的链表中入口数。
ret:返回被映射的入口数
示例
num_mapped = pci_map_sg(pdev,sgl,num_entries,PCI_DMA_TODEVICE);
for(i=0; i< num_mapped;i++)
{
sg_dma_address(&sgl[i]);//返回当前entry的总线地址
sg_dma_len(&sgl[i]);// 获取当前entry的长度
}
(3)总结
- 每次DMA都操作同一个缓冲区,数据需要同步:一致性映射
- 每次DMA操作的缓冲区都不一样,异步操作:流映射(例如:网络驱动程序,其中存放传输数据包的缓冲区被快速的映射和去映射)
- DMA描述符:一致性映射
(4)DMA描述符
DMA描述符包含DMA缓冲区的元数据,如缓冲区的地址和字节长度。
12.4 初始化和探测
驱动程序通常需要执行以下初始化:
- 启用设备
- 请求MMIO / IOP资源
- 设置DMA掩码大小(用于一致性DMA和流式DMA)
- 分配和初始化共享控制数据(pci_allocate_coherent())
- 访问设备配置空间(如果需要)
- 注册IRQ处理程序(request_irq())
- 初始化non-PCI(即LAN/SCSI/等芯片部分)
- 启用DMA /处理引擎
12.4.1 pci_device_id
12.4.2 pci_register_driver
在初始化阶段,驱动程序在PCI子系统里先注册pci_driver结构体,为实现注册,驱动程序调用pci_register_driver()。
/* register a new pci driver;Adds the driver structure to the list of registered drivers.*/
static inline int pci_register_driver(struct pci_driver *drv)
{
return 0;
}
从内核注销pci_driver
void pci_unregister_driver(struct pci_driver *drv)
{
driver_unregister(&drv->driver);
pci_free_dynids(drv);
}
12.4.2 struct pci_driver
下面列出了该结构体中PCI驱动程序必须注意的字段:
(1)name
/* 驱动程序的名字。在内核的所有PCI驱动程序中它必须是唯一的,通常被设置为和驱动程序的模块名相同。当驱动程序运行在内核时,它会出现在sysfs的/sys/bus/pci/drivers/下面 */
const char *name;
实例:
查询结果:
(2)id_table
/* ID向量,内核用于把一些设备关联到此驱动程序,指向struct pci_device_id表 */
const struct pci_device_id *id_table;
实例:
(3)probe
/* 指针,指向PCI驱动程序中的探测函数
* 当PCI层发现它正在搜寻驱动程序的设备ID与前面id_table匹配,就会调用此函数
* 此函数应该开启硬件、分配net_device结构,初始化并注册新设备,分配正确工作所需的所有数据结构(如传输或接收时所用的缓冲区)
* 成功确认pci_dev 则返回0,失败返回负值*/
int (*probe) (struct pci_dev *dev, const struct pci_device_id *id); /* New device inserted */
探测方式有两种:
- 静态:给定设备PCI ID,内核就能根据id_table向量查询出正确的PCI驱动程序
- 动态:根据用户手动配置的ID,动态指系统管理员可以新增ID的能力
(4)remove
/* 指针,指向移除函数
*当pci_dev被从系统移除,或从内核中卸载PCI驱动程序时,调用,probe配对函数
* 释放已分配的I/O端口,I/O内存,为设备除名,释放net_device以及其他由设备驱动程序在probe函数内分配的辅助数据结构
*/
void (*remove) (struct pci_dev *dev); /* Device removed (NULL if not a hot-plug capable driver) */
(5)suspend
/* 可选,指向挂起函数,挂起状态用state传递
* 主要停止设备出口队列,使得该设备无法再传输
*/
int (*suspend) (struct pci_dev *dev, pm_message_t state); /* Device suspended */
(6)resume
/* 可选,指向恢复函数 ,重启出口队列*/
int (*resume) (struct pci_dev *dev); /* Device woken up */
只有当内核支持电源管理时,才会对suspend和resume做初始化。
实例:
12.5 probe
probe()函数启用PCI设备,发现I/O基地址和中断号资源等,给这个设备分配一个网络数据结构,还可以在内核网络层注册这个结构。
pci子系统调用probe()函数时传递了两个参数:
(1)一个指向pci_dev设备结构体的指针,每个PCI设备在内核中都被抽象成这个数据结构。该结构在文件include/linux/pci.h里有定义,每个PCI设备对应一个这样的结构体,这些结构体由PCI子系统负责管理
(2)一个pci_device_id类型的指针,指向驱动程序的pci_device_id表,该表的数据和从设备配置区获得的设备信息要一致。
12.5.1参数
(1) pci_dev
这个设备由内核使用,以引用一个PCI设备,如同网络设备都会被分派net_device实例一样。详细清单见附录
主要成员介绍:
devfn
unsigned int devfn;// lspci 中的YY:Z
PCI设备的设备功能号,也称为PCI逻辑设备号(0-255).其中bit[7:3]是物理设备号(取值范围0-31)。bit[2:0]是功能号(取值范围0-7)
YY:是PCI设备编号,每个总线可以支持32个PCI设备
Z:表示设备功能,每个PCI设备可以容纳8个PCI功能
示例:表示为0000 0000 与0000 0001
dev
struct device dev; /* Generic device interface */
(2) pci_device_id
ID表是一个以全零结尾的struct pci_device_id条目数组。通常首选使用静态const定义。
(1)成员清单
struct pci_device_id 结构体用于定义该驱动程序支持的不同类型的PCI设备列表。该结构体包含下列字段
最后一个成员driver_data是设备驱动程序的私有数据,当驱动程序支持多张卡的时候,经常用它来表示驱动程序相关的配置信息
(2)创建:PCI_DEVICE
PCI_DEVICE(vendor,device)
创建一个struct pci_device_id ,只匹配特定的供应商和设备ID,子供应商和子设备成员为PCI_ANY_ID
(3)PCI_DEVICE_CLASS
(4) MODULE_DEVICE_TABLE
struct pci_device_id结构体需要被导出到用户空间,使热拔插和模块装载系统知道什么模块针对什么硬件设备。
宏MODULE_DEVICE_TABLE完成这个工作。
/* 该语句创建一个名为__mod_pci_device_table 的局部变量,指向struct pci_device_table数组。
* 在稍后的内核构建过程中,depmod程序在所有的模块中搜索符号__mod_pci_device_table。
* 如果找到该符号,它把数据从该模块中抽出,添加到文件/lib/modules/KERNEL_VERSION/modules.pcimap中。
* 当depmod结束后,内核模块支持的所有PCI设备,连同它们的模块名都在该文件中被列出。当内核告知热插拔系统一个新的PCI设备已
* 经被发现时,热插拔系统使用modules.pcimap文件来寻找要装载的恰当的驱动程序
*/
MODULE_DEVICE_TABLE(pci, liquidio_pci_tbl);
注意:实际的系统目录下,没有检索到modules.pcimap,只在modules.networking 下检索到了驱动模块名
示例:
当PCI的热插拔层检测到卡的属性和驱动程序的pci_device_id表里存储的ID信息一致时,该层将激发驱动程序的probe()函数。
12.5.2 驱动注册引导过程
系统主要做了以下几项工作:
(1)相应的驱动程序初始化例程想系统注册好所支持的卡的pci_device_id表和probe()函数
(2)PCI热插拔层检测卡的插入,从卡的配置空间读取厂家ID和设备ID,并找到包含这个ID的驱动程序
(3)因为从PCI配置空间读出来的ID和所注册的驱动程序包含的ID一致,所以该驱动程序就是为这个PCI设备服务的,与设备对应的probe()函数被激活。
(4)probe()方法为以太网驱动程序分配好资源,生成网络接口设备文件,用户可以通过这两个文件读写数据了。

12.5.3 练习(非完整)
#include <linux/init.h>
#include <linux/module.h>
#include <kernel.h>
#include <pci.h>
struct pci_device_id myliq_pci_dev_id[] ={
{
PCI_DEVICE(PCI_VENDOR_ID_CAVIUM,0x9702)
},
{
0
},
};
struct pci_driver myliq_pci_driver = {
.name = "myliq",
.id_table = myliq_pci_dev_id,
.probe = myliq_probe,
.remove = myliq_remove,
};
// static int __devinit mtliq_probe(struct pci_dev *pdev, const struct pci_device_id *pdev_id UNUSED)
static int __devinit mtliq_probe(struct pci_dev *pdev, const struct pci_device_id *pdev_id)
{
//
}
static int __init my_liquidio_init(void);
{
return pci_register_driver(&myliq_pci_driver);
}
static int __exit my_liquidio_exit(void);
{
return pci_unregister_driver(&myliq_pci_driver);
}
module_init(my_liquidio_init);
module_exit(my_liquidio_exit);
涉及的函数
pci_enable_device
/* 初始化设备。使能IO位和memory,如果设备处于挂起状态,则唤醒。该函数可能执行失败
* Initialize device before it's used by a driver. Ask low-level code
* to enable I/O and memory. Wake up the device if it was suspended.
* Beware, this function can fail.
*/
int pci_enable_device(struct pci_dev *dev)
{
return pci_enable_device_flags(dev, IORESOURCE_MEM | IORESOURCE_IO);
}
pci_set_master
在mask指定的地址范围内进行DMA操作
12.6 示例代码解析(部分还不完全懂)
代码清单10-3(手敲强化记忆为了对比示例代码2,顺序有调整,)
#include <linux/pci.h>
unsigned long ioaddr;
/* probe method */
static int __devinit // 初始化等级,之后补上笔记,暂不扩展
net_driver_probe(struct pci_dev *pdev,const struct pci_device_id *id)
{
// 申请网络设备,并填充数据(如缓存,锁等)
struct net_device *net_dev;
net_dev = alloc_etherdev(sizeof(struct prv_data));
net_dev->hard_start_xmit = &mydevice_xmit; // 10.6
pci_enable_device(pdev);// 设备使能
pci_set_master(pdev);// 在mask指定的地址范围内进行DMA操作
// 获取配置区基地址
ioaddr = pci_resource_start(pdev,0);
requet_region(ioaddr,128,"ntwrk");
// 设置网络设备net_dev
net_dev->base_addr = ioaddr;
net_dev ->irq = pdev->irq;
dma_descriptor_setup(pdev); // 10.5 DMA描述符
register_netdev(net_dev);//向网络层注册设备
}
12.7 开源代码(删减版)
删减:还没学会的内容,函数的错误处理
侧重:处理逻辑
目的:了解处理逻辑,处理核心,之后对比项目代码,深刻了解功能上的变动,加深底层认知
疑问记录:
(1) PCI window configuration?
(2)pdev的初始化过程
// UNUSED :ent不使用
static int liquidio_probe(struct pci_dev *pdev,const struct pci_device_id *ent UNUSED)
{
struct octeon_device *oct_dev = NULL;
// 分配内存给octeon设备,并填充设备:主要是申请下标,更新oct设备总数,初始化内存访问锁,以及pci_win_lock
oct_dev = octeon_allocate_device(pdev->device,sizeof(struct octeon_device_priv));
/* 网络设备与pdev绑定 */
pci_set_drvdata(pdev, oct_dev);
/* set linux specific device pointer */
oct_dev->pci_dev = (void *)pdev;
// 设备初始化
octeon_device_init(oct_dev);
}
octeon_device_init
static int octeon_device_init(struct octeon_device *octeon_dev)
{
octeon_pci_os_setup(octeon_dev); // 设置DMA地址掩码
octeon_chip_specific_setup(); // 映射pci bar
CAVIUM_INIT_DELAYED_WORK(&octeon_dev->nic_poll_work.work, nic_starter);// 向网络层注册
if (octeon_setup_interrupt(octeon_dev, octeon_dev->sriov_info.num_pf_rings))// 设置中断
return 1;
}
12.7.1 DMA掩码
static int octeon_pci_os_setup(struct octeon_device *oct)
{
/* setup PCI stuff first 见后文*/
if (pci_enable_device(oct->pci_dev)) {
lio_dev_err(oct, "pci_enable_device failed\n");
return 1;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 13, 0)
#define PCI_DMA_64BIT 0xffffffffffffffffULL
if (pci_set_dma_mask(oct->pci_dev, PCI_DMA_64BIT)) {
lio_dev_err(oct, "pci_set_dma_mask(64bit) failed\n");
pci_disable_device(oct->pci_dev);
return 1;
}
if (pci_set_consistent_dma_mask(oct->pci_dev, PCI_DMA_64BIT)) {
lio_dev_err(oct, "pci_set_coherent_mask(64bit) failed\n");
return 1;
}
#else
// 高内核版本:一致性内存映射
if (dma_set_mask_and_coherent(&oct->pci_dev->dev, DMA_BIT_MASK(64))) {
lio_dev_err(oct, "Unexpected DMA device capability\n");
pci_disable_device(oct->pci_dev);
return 1;
}
#endif
/* Enable PCI DMA Master. */
pci_set_master(oct->pci_dev);//
return 0;
}
12.7.2 映射PCI BAR
为了下一步设置环形队列droq :每个oq在bar0上占用16字节, 第oq个队列的偏移量:oq * 16
octeon_chip_specific_setup-》setup_cn23xx_octeon_pf_device()->octeon_map_pci_barx()
static inline int octeon_map_pci_barx(struct octeon_device *oct,int baridx, int max_map_len)
{
u32 mapped_len = 0;
// 不懂为啥在前
if (pci_request_region(oct->pci_dev, baridx * 2, DRV_NAME)) {
lio_dev_err(oct, "pci_request_region failed for bar %d\n",
baridx);
return 1;
}
// 获取基地址
oct->mmio[baridx].start = pci_resource_start(oct->pci_dev, baridx * 2);
// 获取长度
oct->mmio[baridx].len = pci_resource_len(oct->pci_dev, baridx * 2);
mapped_len = oct->mmio[baridx].len;
oct->mmio[baridx].hw_addr = ioremap(oct->mmio[baridx].start, mapped_len);
oct->mmio[baridx].mapped_len = mapped_len;
oct->mmio[baridx].done = 1;
#if 0 // 错误处理
err_release_region:
pci_release_region(oct->pci_dev, baridx * 2);
return 1;
#endif
}
12.7.3 向网络层注册
nic_starter()------> liquidio_init_nic_module()—>setup_nic_devices-----》register_netdev
涉及的函数
pci_set_drvdata
pci_set_dma_mask
pci_set_consistent_dma_mask
dma_set_mask_and_coherent
pdev的填充过程
卸载模块
当使用设备完成时,可能需要卸载模块,驱动程序需要采取以下步骤:
- 禁止设备产生irq
- 释放IRQ
- 停止所有DMA活动
- 释放DMA缓存区(包括流式DMA和一致性DMA)
- 从其他子系统注销(例如scsi或netdev)
- 释放MMIO/ IOP资源
- 禁用该设备
附录
边际效应
I/O寄存器具有边际效应(side effect)
边际效应:读取某个地址时可能导致该地址内容发生变化。比如很多设备的中断状态寄存器只要一读取,便自动清零
常规内存操作:内存写操作的唯一结果就是在指定位置存储一个数据;内存读操作仅仅是返回指定位置最后一次写入的数据。
CPU优化/编译器优化:编译器能够缓存数据值到CPU寄存器而不写回内存,并且即使数据值已经存储到内存,读和写操作都能够在缓冲内存中进行,而不是直接接触物理RAM。此外,指令重编排可能在编译器级别或者在硬件级别发生。很多情况下,如果一个指令以不同于在程序文本中出现的顺序来执行(例如,为避免在RISC流水线中的互锁),它能够执行得更快
对于传统的内存(单处理器系统)来说,这些优化是透明和有益的。驱动直接存取I/O寄存器主要目的是能提高CPU性能。然后,这些优化对正确的I/O操作可能是致命的。处理器无法预见这种情形,一些其他的操作(在一个独立处理器上运行,或者发生在一个I/O控制器的事情)依赖内存存取的顺序。编译器或CPU可能只尽力胜过你并且重编排你请求的操作,结果可能是奇怪的错误而且非常难以调试。
因此,一个驱动程序必须确保没有进行缓冲并且在存取寄存器时没有发生读或写的重编排。
数据结构
- struct pci_dev :描述PCI设备
- struct pci_device_id :PCI设备标识符
- struct pci_driver :定义PCI层和设备驱动程序之间的接口。主要由函数指针组成。
pci_device_id成员清单
#include<linux/mod_devicetable.h>
struct pci_device_id {
/* Vendor and device ID or PCI_ANY_ID 指定设备的PCI厂商和设备ID。
* 如果驱动程序可以处理任何厂商或者设备ID,这些字段应该使用值PCI_ANY_ID
*/
__u32 vendor, device;
/* Subsystem ID's or PCI_ANY_ID 指定设备的PCI子系统厂商和子系统设备ID。
* 如果驱动程序可以处理任何类型的子系统ID,这些字段应该使用值PCI_ANY_ID
*/
__u32 subvendor, subdevice;
/* (class,subclass,prog-if) triplet 指定驱动支持一种PCI类(class)设备 。
* PCI规范中描述了不同类的PCI设备。
* 如果驱动程序可以处理任何类型的子系统ID,这些字段应该使用值PCI_ANY_ID
*/
__u32 class, class_mask;
/* Data private to the driver
* 不是用来和设备相匹配的,而是用来保存PCI驱动程序用于区分不同设备的信息
*/
kernel_ulong_t driver_data;
};
pci_driver成员清单
struct pci_driver {
struct list_head node;
const char *name;
const struct pci_device_id *id_table; /* must be non-NULL for probe to be called */
int (*probe) (struct pci_dev *dev, const struct 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);
int (*sriov_configure) (struct pci_dev *dev, int num_vfs); /* PF pdev */
const struct pci_error_handlers *err_handler;
struct device_driver driver;
struct pci_dynids dynids;
};
pci_dev成员清单
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 */
unsigned int devfn; /* encoded device & function index */
unsigned short vendor;
unsigned short device;
unsigned short subsystem_vendor;
unsigned short subsystem_device;
unsigned int class; /* 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 msi_cap; /* MSI capability offset */
u8 msix_cap; /* MSI-X capability offset */
u8 pcie_mpss:3; /* PCI-E Max Payload Size Supported */
u8 rom_base_reg; /* which config register controls the ROM */
u8 pin; /* which interrupt pin this device uses */
u16 pcie_flags_reg; /* cached PCI-E Capabilities Register */
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. */
u8 pm_cap; /* PM capability offset */
unsigned int pme_support:5; /* Bitmask of states from which PME#
can be generated */
unsigned int pme_interrupt:1;
unsigned int pme_poll:1; /* Poll device's PME status bit */
unsigned int d1_support:1; /* Low power state D1 is supported */
unsigned int d2_support:1; /* Low power state D2 is supported */
unsigned int no_d1d2:1; /* D1 and D2 are forbidden */
unsigned int no_d3cold:1; /* D3cold is forbidden */
unsigned int d3cold_allowed:1; /* D3cold is allowed by user */
unsigned int mmio_always_on:1; /* disallow turning off io/mem
decoding during bar sizing */
unsigned int wakeup_prepared:1;
unsigned int runtime_d3cold:1; /* whether go through runtime
D3cold, not set for devices
powered on/off by the
corresponding bridge */
unsigned int d3_delay; /* D3->D0 transition time in ms */
unsigned int d3cold_delay; /* D3cold->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!
*/
unsigned int irq;
struct resource resource[DEVICE_COUNT_RESOURCE]; /* I/O and memory regions + expansion ROMs */
bool match_driver; /* Skip attaching driver */
/* These fields are used by common fixups */
unsigned int transparent:1; /* Transparent PCI bridge */
unsigned int multifunction:1;/* Part of multi-function device */
/* keep track of device state */
unsigned int is_added:1;
unsigned int is_busmaster:1; /* device is busmaster */
unsigned int no_msi:1; /* device may not use msi */
unsigned int no_64bit_msi:1; /* device may only use 32-bit MSIs */
unsigned int block_cfg_access:1; /* config space access is blocked */
unsigned int broken_parity_status:1; /* Device generates false positive parity */
unsigned int irq_reroute_variant:2; /* device needs IRQ rerouting variant */
unsigned int msi_enabled:1;
unsigned int msix_enabled:1;
unsigned int ari_enabled:1; /* ARI forwarding */
unsigned int is_managed:1;
unsigned int is_pcie:1; /* Obsolete. Will be removed.
Use pci_is_pcie() instead */
unsigned int needs_freset:1; /* Dev requires fundamental reset */
unsigned int state_saved:1;
unsigned int is_physfn:1;
unsigned int is_virtfn:1;
unsigned int reset_fn:1;
unsigned int is_hotplug_bridge:1;
unsigned int __aer_firmware_first_valid:1;
unsigned int __aer_firmware_first:1;
unsigned int broken_intx_masking:1;
unsigned int io_window_1k:1; /* Intel P2P bridge 1K I/O windows */
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;
const struct attribute_group **msi_irq_groups;
#endif
struct pci_vpd *vpd;
#ifdef CONFIG_PCI_ATS
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
phys_addr_t rom; /* Physical address of ROM if it's not from the BAR */
size_t romlen; /* Length of ROM if it's not from the BAR */
};