PCI设备驱动与NVMEM框架技术解析
1. PCI与直接内存访问(DMA)
在PCI设备驱动中,为了加速数据传输并减轻CPU负担,可配置控制器和设备进行直接内存访问(DMA),即数据在设备和主机间交换时无需CPU参与。PCI地址空间可能是32位或64位,系统中用于DMA传输的源或目标内存区域称为DMA缓冲区,其内存范围与总线地址大小有关。
经典PCI总线支持32位寻址,PCIe则扩展到64位,因此有32位和64位两种地址格式。要使用DMA API,驱动需包含
#include <linux/dma-mapping.h>
。为告知内核DMA缓冲区的特殊需求(指定总线宽度),可使用
dma_set_mask()
函数:
dma_set_mask(struct device *dev, u64 mask);
以下是32位(或64位)系统的示例:
int err = 0;
err = pci_set_dma_mask(pci_dev, DMA_BIT_MASK(32));
/*
* OR the below on a 64 bits system:
* err = pci_set_dma_mask(dev, DMA_BIT_MASK(64));
*/
if (err) {
dev_err(&pci_dev->dev,
" Required dma mask not supported, \
failed to initialize device\n" );
goto err_disable_pci_dev;
}
DMA传输需要合适的内存映射,包括分配DMA缓冲区并为每个缓冲区生成总线地址(
dma_addr_t
类型)。I/O设备通过总线控制器和I/O内存管理单元(IOMMU)查看DMA缓冲区,生成的总线地址会告知设备DMA缓冲区的位置。同时,内存映射还会生成虚拟地址,DMA服务例程会将DMA缓冲区的内核虚拟地址映射到总线地址。
PCI DMA映射有两种类型:连贯映射和流式映射,内核为这两种映射提供了完善的API,屏蔽了许多与DMA控制器交互的内部细节。
1.1 PCI连贯(一致)映射
连贯映射会为设备分配无缓存(连贯)和无缓冲的内存以进行DMA操作。由于设备或CPU的写入操作可立即被对方读取,无需担心缓存一致性问题,因此这种映射是同步的。虽然大多数设备需要这种映射,但它对系统来说成本较高,不过在代码实现上较为简单。
设置连贯映射的函数如下:
void * pci_alloc_consistent(struct pci_dev *hwdev, size_t size, dma_addr_t *dma_handle)
该函数保证分配的内存物理上连续,
size
是需要分配的区域长度,返回两个值:CPU可用于访问的虚拟地址和
dma_handle
(输出参数,对应分配区域的总线地址)。
需要注意的是,
pci_alloc_consistent()
实际上是
dma_alloc_coherent()
的简单封装,设置了
GFP_ATOMIC
标志,意味着分配操作不会睡眠,可在原子上下文中安全调用。如果想更改分配标志,例如使用
GFP_KERNEL
代替
GFP_ATOMIC
,建议直接使用
dma_alloc_coherent()
。
映射成本较高,最小分配单位是一个页面,实际分配的页面数是2的幂次方,页面顺序可通过
int order = get_order(size)
获取。这种映射适用于设备生命周期内持续使用的缓冲区。
要解除映射并释放DMA区域,可调用
pci_free_consistent()
:
pci_free_consistent(dev, size, cpu_addr, dma_handle);
这里的
cpu_addr
和
dma_handle
对应
pci_alloc_consistent()
返回的内核虚拟地址和总线地址。虽然映射函数可在原子上下文中调用,但此函数不能在原子上下文中调用。实际上,
pci_free_consistent()
是
dma_free_coherent()
的简单封装。
以下是一个进行DMA映射并将总线地址发送给设备的示例代码:
#define DMA_ADDR_OFFSET 0x14
#define DMA_REG_SIZE_OFFSET 0x32
[...]
int do_pci_dma (struct pci_dev *pci_dev, int direction, size_t count)
{
dma_addr_t dma_pa;
char *dma_va;
void iomem *dma_io;
/* should check errors */
dma_io = pci_iomap(dev, 2, 0);
dma_va = pci_alloc_consistent(&pci_dev->dev, count, &dma_pa);
if (!dma_va)
return -ENOMEM;
/* may need to clear allocated region */
memset(dma_va, 0, count);
/* set up the device */
iowrite8(CMD_DISABLE_DMA, dma_io + REG_CMD_OFFSET);
iowrite8(direction ? CMD_WR : CMD_RD);
/* Send bus address to the device */
iowrite32(dma_pa, dma_io + DMA_ADDR_OFFSET);
/* Send size to the device */
iowrite32(count, dma_io + DMA_REG_SIZE_OFFSET);
/* Start the operation */
iowrite8(CMD_ENABLE_DMA, dma_io + REG_CMD_OFFSET);
return 0;
}
1.2 流式DMA映射
流式映射在代码实现上有更多限制。首先,这种映射需要使用已分配的缓冲区,且映射后的缓冲区归设备所有,CPU在使用前需先解除映射以解决可能的缓存问题。
若要发起写事务(CPU到设备),驱动应在映射前将数据放入缓冲区,并指定数据移动方向,且数据只能按此方向使用。
缓冲区在被CPU访问前必须解除映射,原因在于CPU映射是可缓存的。用于流式映射的
dma_map_*()
系列函数(实际由
pci_map_*()
函数封装)会先清理/使与缓冲区相关的缓存无效,并依赖CPU在对应的
dma_unmap_*()
(由
pci_unmap_*()
函数封装)操作前不访问这些缓冲区。解除映射时,若有必要会再次使缓存无效,以处理期间的推测性预取,之后CPU才能读取设备写入内存的数据。
流式映射有两种形式:
-
单缓冲区映射
:允许单页映射,适用于偶尔的映射操作。可使用
pci_map_single()
函数设置单缓冲区映射:
dma_addr_t pci_map_single(struct pci_dev *hwdev, void *ptr, size_t size, int direction)
direction
可以是
PCI_DMA_BIDIRECTION
、
PCI_DMA_TODEVICE
、
PCI_DMA_FROMDEVICE
或
PCI_DMA_NONE
,
ptr
是缓冲区的内核虚拟地址,返回的总线地址可发送给设备。使用时应确保
direction
与数据实际移动方向匹配,而不是总是使用
DMA_BIDIRECTIONAL
。
pci_map_single()
是
dma_map_single()
的简单封装。
使用以下函数释放映射:
Void pci_unmap_single(struct pci_dev *hwdev, dma_addr_t dma_addr, size_t size, int direction)
这是
dma_unmap_single()
的封装,
dma_addr
应与
pci_map_single()
返回的地址相同,
direction
和
size
应与映射时指定的一致。
以下是简化的流式映射(单缓冲区)示例:
int do_pci_dma (struct pci_dev *pci_dev, int direction, void *buffer, size_t count)
{
dma_addr_t dma_pa;
/* bus address */
void iomem *dma_io;
/* should check errors */
dma_io = pci_iomap(dev, 2, 0);
dma_dir = (write ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
dma_pa = pci_map_single(pci_dev, buffer, count, dma_dir);
if (!dma_va)
return -ENOMEM;
/* may need to clear allocated region */
memset(dma_va, 0, count);
/* set up the device */
iowrite8(CMD_DISABLE_DMA, dma_io + REG_CMD_OFFSET);
iowrite8(direction ? CMD_WR : CMD_RD);
/* Send bus address to the device */
iowrite32(dma_pa, dma_io + DMA_ADDR_OFFSET);
/* Send size to the device */
iowrite32(count, dma_io + DMA_REG_SIZE_OFFSET);
/* Start the operation */
iowrite8(CMD_ENABLE_DMA, dma_io + REG_CMD_OFFSET);
return 0;
}
以下是作为DMA事务中断处理程序的示例,展示了如何从CPU端处理缓冲区:
void pci_dma_interrupt(int irq, void *dev_id)
{
struct private_struct *priv = (struct private_struct *) dev_id;
/* Unmap the DMA buffer */
pci_unmap_single(priv->pci_dev, priv->dma_addr,
priv->dma_size, priv->dma_dir);
/* now it is safe to access the buffer */
[...]
}
-
分散/聚集映射
:可一次性传输多个(不必物理连续)的缓冲区区域,而无需逐个映射和传输每个缓冲区。设置分散列表映射的步骤如下:
1. 分配分散的缓冲区,除最后一个缓冲区外,其他缓冲区必须是页面大小。
2. 分配一个scatterlist数组,并使用sg_set_buf()函数将之前分配的缓冲区填充到数组中。
3. 调用dma_map_sg()函数对scatterlist数组进行映射。
4. DMA操作完成后,调用dma_unmap_sg()函数解除映射。
以下是示例代码:
u32 *wbuf1, *wbuf2, *wbuf3;
struct scatterlist sgl[3];
int num_mapped;
wbuf1 = kzalloc(PAGE_SIZE, GFP_DMA);
wbuf2 = kzalloc(PAGE_SIZE, GFP_DMA);
/* size may be different for the last entry */
wbuf3 = kzalloc(CUSTOM_SIZE, GFP_DMA);
sg_init_table(sg, 3);
sg_set_buf(&sgl[0], wbuf1, PAGE_SIZE);
sg_set_buf(&sgl[1], wbuf2, PAGE_SIZE);
sg_set_buf(&sgl[2], wbuf3, CUSTOM_SIZE);
num_mapped = pci_map_sg(NULL, sgl, 3, PCI_DMA_BIDIRECTIONAL);
pci_map_sg()
是
dma_map_sg()
的简单封装。为使代码具有可移植性,可使用以下两个宏:
dma_addr_t sg_dma_address(struct scatterlist *sg);
unsigned int sg_dma_len(struct scatterlist *sg);
dma_map_sg()
和
dma_unmap_sg()
会处理缓存一致性问题。若在DMA传输之间需要访问(读写)数据,应使用
dma_sync_sg_for_cpu()
或
dma_sync_sg_for_device()
函数在每次传输之间以适当方式同步缓冲区,单区域映射的类似函数是
dma_sync_single_for_cpu()
和
dma_sync_single_for_device()
。
2. NVMEM框架概述
NVMEM(非易失性内存)框架是内核层用于处理非易失性存储(如EEPROM、eFuse等)的机制。过去,这些设备的驱动程序存储在
drivers/misc/
目录下,每个驱动程序通常需要实现自己的API来处理相同的功能,缺乏抽象代码,且内核中对这些设备的支持增加导致了大量代码重复。
引入NVMEM框架旨在解决上述问题,并引入设备树(DT)表示法,使消费设备能够从NVMEM获取所需数据(如MAC地址、SoC/修订ID、部件编号等)。
2.1 NVMEM数据结构和API
NVMEM基于生产者/消费者模式,NVMEM设备驱动程序必须包含
<linux/nvmem-provider.h>
,消费者则需包含
<linux/nvmem-consumer.h>
。该框架有几个重要的数据结构:
-
struct nvmem_device:抽象了实际的NVMEM硬件,在设备注册时由框架创建和填充,其字段实际上是struct nvmem_config字段的完整副本。
struct nvmem_device {
const char *name;
struct module *owner;
struct device dev;
int stride;
int word_size;
int id;
int users;
size_t size;
bool read_only;
int flags;
nvmem_reg_read_t reg_read;
nvmem_reg_write_t reg_write;
void *priv;
[...]
};
-
struct nvmem_config:是NVMEM设备的运行时配置,提供设备信息或访问其数据单元的辅助函数。设备注册时,其大部分字段用于填充新创建的nvmem_device结构。
struct nvmem_config {
struct device *dev;
const char *name;
int id;
struct module *owner;
const struct nvmem_cell_info *cells;
int ncells;
bool read_only;
bool root_only;
nvmem_reg_read_t reg_read;
nvmem_reg_write_t reg_write;
int size;
int word_size;
int stride;
void *priv;
[...]
};
各字段含义如下:
| 字段 | 含义 |
| ---- | ---- |
|
dev
| 父设备 |
|
name
| NVMEM设备的可选名称,与
id
一起构成完整设备名,建议在名称中添加
-
,如
<name>-<id>
,若省略则默认使用
nvmem<id>
|
|
id
| NVMEM设备的可选ID,若
name
为
NULL
则忽略,若设置为
-1
,内核会为设备提供唯一ID |
|
owner
| 拥有此NVMEM设备的模块 |
|
cells
| 预定义NVMEM单元的数组,可选 |
|
ncells
|
cells
数组中的元素数量 |
|
read_only
| 将设备标记为只读 |
|
root_only
| 指示设备是否仅对root用户可访问 |
|
reg_read
和
reg_write
| 框架用于读写数据的底层回调函数 |
|
size
| 设备的大小 |
|
word_size
| 设备的最小读写访问粒度 |
|
stride
| 最小读写访问步长 |
|
priv
| 传递给读写回调函数的上下文数据 |
数据单元(data cell)表示NVMEM设备中的内存区域,可分配给消费驱动程序。框架使用两种不同的数据结构来维护这些内存区域,分别用于提供者和消费者:
-
struct nvmem_cell_info
(提供者)
struct nvmem_cell_info {
const char *name;
unsigned int offset;
unsigned int bytes;
unsigned int bit_offset;
unsigned int nbits;
};
-
struct nvmem_cell(消费者)
struct nvmem_cell {
const char *name;
int offset;
int bytes;
int bit_offset;
int nbits;
struct nvmem_device *nvmem;
struct list_head node;
};
各字段含义如下:
| 字段 | 含义 |
| ---- | ---- |
|
name
| 单元的名称 |
|
offset
| 单元在整个硬件数据寄存器中的偏移量(起始位置) |
|
bytes
| 从
offset
开始的数据单元大小(字节) |
|
bit_offset
| 单元内的位偏移量,用于位级粒度的单元 |
|
nbits
| 感兴趣区域的位大小 |
|
nvmem
| 此单元所属的NVMEM设备 |
|
node
| 用于系统范围内跟踪单元,最终存储在
nvmem_cells
列表中 |
为了更清晰地说明,以下是一个单元配置示例:
static struct nvmem_cellinfo mycell = {
.offset = 0xc,
.bytes = 0x1,
[...]
};
若
.nbits
和
.bit_offset
都为0,表示我们对单元的整个数据区域感兴趣,在本例中为1字节大小。若只对第2到4位(实际为3位)感兴趣,结构如下:
staic struct nvmem_cellinfo mycell = {
.offset = 0xc,
.bytes = 0x1,
.bit_offset = 2,
[...]
};
综上所述,连贯映射代码实现简单但使用成本高,流式映射则相反。流式映射适用于I/O设备长时间拥有缓冲区的情况,如网络驱动程序中,每个
skbuf
数据都在运行时进行映射和解除映射。但具体使用哪种方法,设备可能有最终决定权。如果有选择,应尽可能使用流式映射,必要时使用连贯映射。
2.2 NVMEM提供者驱动
NVMEM提供者驱动的主要任务是将NVMEM内存区域暴露给消费者。以下是编写NVMEM提供者驱动的关键步骤:
-
定义
struct nvmem_config:根据设备的具体情况,填充struct nvmem_config结构,设置设备的相关信息和回调函数。
static struct nvmem_config my_nvmem_config = {
.dev = &my_device->dev,
.name = "my_nvmem_device",
.id = 0,
.owner = THIS_MODULE,
.cells = my_nvmem_cells,
.ncells = ARRAY_SIZE(my_nvmem_cells),
.read_only = false,
.root_only = false,
.reg_read = my_nvmem_read,
.reg_write = my_nvmem_write,
.size = MY_NVMEM_SIZE,
.word_size = MY_NVMEM_WORD_SIZE,
.stride = MY_NVMEM_STRIDE,
.priv = my_private_data,
};
-
实现读写回调函数
:
reg_read和reg_write是用于读写NVMEM数据的回调函数,需要根据实际硬件的读写操作进行实现。
static int my_nvmem_read(void *priv, unsigned int offset, void *val, size_t bytes) {
// 实现读取NVMEM数据的逻辑
// ...
return 0;
}
static int my_nvmem_write(void *priv, unsigned int offset, void *val, size_t bytes) {
// 实现写入NVMEM数据的逻辑
// ...
return 0;
}
-
注册NVMEM设备
:使用
nvmem_register函数注册NVMEM设备。
static int __init my_nvmem_driver_init(void) {
struct nvmem_device *nvmem;
nvmem = nvmem_register(&my_nvmem_config);
if (IS_ERR(nvmem)) {
dev_err(&my_device->dev, "Failed to register NVMEM device\n");
return PTR_ERR(nvmem);
}
return 0;
}
-
注销NVMEM设备
:在驱动退出时,使用
nvmem_unregister函数注销NVMEM设备。
static void __exit my_nvmem_driver_exit(void) {
nvmem_unregister(my_nvmem_device);
}
2.3 NVMEM消费者驱动API
NVMEM消费者驱动可以使用NVMEM提供者暴露的内存区域。以下是一些常用的NVMEM消费者驱动API:
-
nvmem_cell_get:获取指定名称的NVMEM单元。
struct nvmem_cell *nvmem_cell_get(struct device *dev, const char *name);
-
nvmem_cell_read:从NVMEM单元中读取数据。
int nvmem_cell_read(struct nvmem_cell *cell, void *val, size_t bytes);
-
nvmem_cell_write:向NVMEM单元中写入数据。
int nvmem_cell_write(struct nvmem_cell *cell, const void *val, size_t bytes);
-
nvmem_cell_put:释放NVMEM单元。
void nvmem_cell_put(struct nvmem_cell *cell);
以下是一个简单的NVMEM消费者驱动示例:
static int my_nvmem_consumer_probe(struct platform_device *pdev) {
struct nvmem_cell *cell;
u8 data[MY_NVMEM_CELL_SIZE];
int ret;
cell = nvmem_cell_get(&pdev->dev, "my_nvmem_cell");
if (IS_ERR(cell)) {
dev_err(&pdev->dev, "Failed to get NVMEM cell\n");
return PTR_ERR(cell);
}
ret = nvmem_cell_read(cell, data, sizeof(data));
if (ret) {
dev_err(&pdev->dev, "Failed to read NVMEM cell\n");
goto err_put_cell;
}
// 处理读取到的数据
// ...
ret = nvmem_cell_write(cell, data, sizeof(data));
if (ret) {
dev_err(&pdev->dev, "Failed to write NVMEM cell\n");
goto err_put_cell;
}
err_put_cell:
nvmem_cell_put(cell);
return ret;
}
总结
本文详细介绍了PCI设备驱动中的直接内存访问(DMA)技术和NVMEM框架。在PCI设备驱动中,DMA技术通过直接在设备和主机间交换数据,减轻了CPU的负担,提高了数据传输效率。DMA映射分为连贯映射和流式映射,连贯映射代码实现简单但使用成本高,流式映射则适用于I/O设备长时间拥有缓冲区的情况。
NVMEM框架是内核层用于处理非易失性存储的机制,解决了传统驱动程序缺乏抽象代码和代码重复的问题。通过定义数据结构和实现读写回调函数,NVMEM提供者驱动可以将NVMEM内存区域暴露给消费者,消费者驱动则可以使用提供的API读取和写入NVMEM数据。
流程图
graph TD;
A[PCI设备驱动] --> B[直接内存访问(DMA)];
B --> C[连贯映射];
B --> D[流式映射];
D --> E[单缓冲区映射];
D --> F[分散/聚集映射];
A --> G[NVMEM框架];
G --> H[NVMEM提供者驱动];
G --> I[NVMEM消费者驱动];
关键知识点总结
| 技术领域 | 关键知识点 |
|---|---|
| PCI DMA | 连贯映射、流式映射(单缓冲区映射、分散/聚集映射)、缓存一致性处理 |
| NVMEM框架 |
struct nvmem_device
、
struct nvmem_config
、提供者驱动编写、消费者驱动API
|
超级会员免费看
1484

被折叠的 条评论
为什么被折叠?



