大家好,我是txp,今天我们继续来学习从零写一个ALSA声卡驱动系列文章内容!
在本节中,我们将完成与芯片相关的构造函数、析构函数以及 PCI 项的实现。下面首先展示示例代码:
struct mychip {
struct snd_card *card;
struct pci_dev *pci;
unsigned long port;
int irq;
};
static int snd_mychip_free(struct mychip *chip)
{
/* disable hardware here if any */
.... /* (not implemented in this document) */
/* release the irq */
if (chip->irq >= 0)
free_irq(chip->irq, chip);
/* release the I/O ports & memory */
pci_release_regions(chip->pci);
/* disable the PCI entry */
pci_disable_device(chip->pci);
/* release the data */
kfree(chip);
return 0;
}
/* chip-specific constructor */
static int snd_mychip_create(struct snd_card *card,
struct pci_dev *pci,
struct mychip **rchip)
{
struct mychip *chip;
int err;
static const struct snd_device_ops ops = {
.dev_free = snd_mychip_dev_free,
};
*rchip = NULL;
/* initialize the PCI entry */
err = pci_enable_device(pci);
if (err < 0)
return err;
/* check PCI availability (28bit DMA) */
if (pci_set_dma_mask(pci, DMA_BIT_MASK(28)) < 0 ||
pci_set_consistent_dma_mask(pci, DMA_BIT_MASK(28)) < 0) {
printk(KERN_ERR "error to set 28bit mask DMA\n");
pci_disable_device(pci);
return -ENXIO;
}
chip = kzalloc(sizeof(*chip), GFP_KERNEL);
if (chip == NULL) {
pci_disable_device(pci);
return -ENOMEM;
}
/* initialize the stuff */
chip->card = card;
chip->pci = pci;
chip->irq = -1;
/* (1) PCI resource allocation */
err = pci_request_regions(pci, "My Chip");
if (err < 0) {
kfree(chip);
pci_disable_device(pci);
return err;
}
chip->port = pci_resource_start(pci, 0);
if (request_irq(pci->irq, snd_mychip_interrupt,
IRQF_SHARED, KBUILD_MODNAME, chip)) {
printk(KERN_ERR "cannot grab irq %d\n", pci->irq);
snd_mychip_free(chip);
return -EBUSY;
}
chip->irq = pci->irq;
card->sync_irq = chip->irq;
/* (2) initialization of the chip hardware */
.... /* (not implemented in this document) */
err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);
if (err < 0) {
snd_mychip_free(chip);
return err;
}
*rchip = chip;
return 0;
}
/* PCI IDs */
static struct pci_device_id snd_mychip_ids[] = {
{ PCI_VENDOR_ID_FOO, PCI_DEVICE_ID_BAR,
PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0, },
....
{ 0, }
};
MODULE_DEVICE_TABLE(pci, snd_mychip_ids);
/* pci_driver definition */
static struct pci_driver driver = {
.name = KBUILD_MODNAME,
.id_table = snd_mychip_ids,
.probe = snd_mychip_probe,
.remove = snd_mychip_remove,
};
/* module initialization */
static int __init alsa_card_mychip_init(void)
{
return pci_register_driver(&driver);
}
/* module clean up */
static void __exit alsa_card_mychip_exit(void)
{
pci_unregister_driver(&driver);
}
module_init(alsa_card_mychip_init)
module_exit(alsa_card_mychip_exit)
EXPORT_NO_SYMBOLS; /* for old kernels only */
PCI 资源的分配是在 probe 函数中完成的,通常会专门编写一个额外的 xxx_create() 函数来实现这一目的。
对于 PCI 设备,在分配资源之前,首先需要调用 pci_enable_device() 函数。同时,还需要设置合适的 PCI DMA 掩码(mask),以限制可访问的 I/O 范围。在某些情况下,还可能需要调用 pci_set_master() 函数。
假设使用 28 位的掩码,需要添加的代码如下所示:
err = pci_enable_device(pci);
if (err < 0)
return err;
if (pci_set_dma_mask(pci, DMA_BIT_MASK(28)) < 0 ||
pci_set_consistent_dma_mask(pci, DMA_BIT_MASK(28)) < 0) {
printk(KERN_ERR "error to set 28bit mask DMA\n");
pci_disable_device(pci);
return -ENXIO;
}
I/O 端口和中断(IRQ)的分配是通过标准内核函数完成的。这些资源必须在析构函数中释放(见下文)。
现在假设该 PCI 设备具有一个 8 字节的 I/O 端口和一个中断,那么 struct mychip 将包含以下字段:
struct mychip {
struct snd_card *card;
unsigned long port;
int irq;
};
对于一个 I/O 端口(以及内存区域),你需要保存其资源指针,以便进行标准的资源管理。
而对于中断(irq),你只需要保存中断号(一个整数)。但在实际分配之前,需要将该中断号初始化为 -1,因为中断号 0 是合法的。
I/O 端口地址及其资源指针可以通过 kzalloc() 自动初始化为 NULL,因此你无需手动重置它们。
I/O 端口的分配方式如下所示:
err = pci_request_regions(pci, "My Chip");
if (err < 0) {
kfree(chip);
pci_disable_device(pci);
return err;
}
chip->port = pci_resource_start(pci, 0);
它将为指定的 PCI 设备保留一个 8 字节的 I/O 端口区域。返回值 chip->res_port 是通过 request_region() 使用 kmalloc() 分配的。该指针必须通过 kfree() 释放,但这里存在一个问题,这个问题稍后会进行说明。
中断源的分配方式如下所示:
if (request_irq(pci->irq, snd_mychip_interrupt,
IRQF_SHARED, KBUILD_MODNAME, chip)) {
printk(KERN_ERR "cannot grab irq %d\n", pci->irq);
snd_mychip_free(chip);
return -EBUSY;
}
chip->irq = pci->irq;
snd_mychip_interrupt() 是稍后定义的中断处理函数。需要注意的是,只有当 request_irq() 调用成功后,才应设置 chip->irq 的值。
在 PCI 总线上,中断是可以共享的,因此在调用 request_irq() 时需要使用 IRQF_SHARED 作为中断标志。
request_irq() 的最后一个参数是传递给中断处理函数的数据指针。通常会传入与芯片相关的结构体记录,但你也可以传入其他你需要的数据。
此处不会详细介绍中断处理函数的具体内容,但至少可以说明它的大致结构。中断处理函数通常如下所示:
static irqreturn_t snd_mychip_interrupt(int irq, void *dev_id)
{
struct mychip *chip = dev_id;
....
return IRQ_HANDLED;
}
在申请中断(IRQ)之后,可以将其传递给 card->sync_irq 字段:
card->irq = chip->irq;
这样做可以让 PCM 核心在适当的时机(例如在 hw_free 之前)自动调用 synchronize_irq()。具体细节请参见后文的 sync_stop 回调章节。
现在我们来编写上面这些资源对应的析构函数。析构函数的作用很简单:禁用硬件(如果已经启用)并释放资源。
目前我们还没有涉及硬件部分,因此这里不编写禁用相关的代码。
在释放资源时,采用“检查并释放(check-and-release)”的方法更加安全。对于中断,可以按如下方式处理:
if (chip->irq >= 0)
free_irq(chip->irq, chip);
由于中断号(irq number)可能从 0 开始,因此你应该将 chip->irq 初始化为一个负值(例如 -1),这样就可以像上面那样判断中断号是否有效。
当你像本示例中那样通过 pci_request_region() 或 pci_request_regions() 申请了 I/O 端口或内存区域时,应使用对应的函数 pci_release_region() 或 pci_release_regions() 来释放资源:
pci_release_regions(chip->pci);
如果你是通过 request_region() 或 request_mem_region() 手动申请的资源,则可以通过 release_resource() 来释放。
假设你将 request_region() 返回的资源指针保存在 chip->res_port 中,那么释放过程如下所示:
release_and_free_resource(chip->res_port);
别忘了在结束前调用 pci_disable_device()。 最后,释放与芯片相关的结构体记录:
kfree(chip);
我们在上文中并未实现硬件禁用(hardware disabling)的部分。如果你需要实现这一功能,请注意:析构函数可能会在芯片初始化尚未完成之前就被调用。因此,建议设置一个标志位,用于在硬件尚未初始化时跳过硬件禁用操作。
当使用 snd_device_new() 并以 SNDRV_DEV_LOWLEVEL 类型将芯片数据(chip-data)绑定到 card 时,其析构函数会最后被调用。也就是说,可以确保此时其他组件(如 PCM 和控制接口)都已被释放。你无需显式停止 PCM 等模块,只需调用底层的硬件停止操作即可。
对内存映射区域(memory-mapped region)的管理方式几乎与 I/O 端口的管理相同。你需要如下两个字段:
struct mychip {
....
unsigned long iobase_phys;
void __iomem *iobase_virt;
};
分配操作如下所示:
err = pci_request_regions(pci, "My Chip");
if (err < 0) {
kfree(chip);
return err;
}
chip->iobase_phys = pci_resource_start(pci, 0);
chip->iobase_virt = ioremap(chip->iobase_phys,
pci_resource_len(pci, 0));
相应的释放函数如下所示:
static int snd_mychip_free(struct mychip *chip)
{
....
if (chip->iobase_virt)
iounmap(chip->iobase_virt);
....
pci_release_regions(chip->pci);
....
}
当然,使用现代的 pci_iomap() 方法也会让处理变得更简单一些:
err = pci_request_regions(pci, "My Chip");
if (err < 0) {
kfree(chip);
return err;
}
chip->iobase_virt = pci_iomap(pci, 0, 0);
它需要在析构函数中与 pci_iounmap() 配对使用。
到目前为止一切顺利。现在我们来完成剩下的 PCI 相关内容。首先,我们需要为该芯片组定义一个 struct pci_device_id 表。
这个表用于列出 PCI 的厂商/设备 ID 以及一些掩码信息。
例如:
static struct pci_device_id snd_mychip_ids[] = {
{ PCI_VENDOR_ID_FOO, PCI_DEVICE_ID_BAR,
PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0, },
....
{ 0, }
};
MODULE_DEVICE_TABLE(pci, snd_mychip_ids);
struct pci_device_id 的前两个字段分别是厂商 ID 和设备 ID。如果你没有特别的需求去过滤匹配的设备,那么其余字段可以像上面那样保持默认。
结构体中最后一个字段用于存放该条目的私有数据。你可以在这里指定任意值,例如用于为不同的设备 ID 定义特定的操作。类似的用法可以在 intel8x0 驱动中找到。
该列表的最后一项必须是终止项(terminator),也就是全为 0 的条目。
接下来,准备 struct pci_driver 结构体记录:
static struct pci_driver driver = {
.name = KBUILD_MODNAME,
.id_table = snd_mychip_ids,
.probe = snd_mychip_probe,
.remove = snd_mychip_remove,
};
probe 和 remove 函数已经在前面的章节中定义过。
name 字段是该设备的名称字符串,注意不要在该字符串中使用斜杠(“/”)。 最后是模块入口部分:
static int __init alsa_card_mychip_init(void)
{
return pci_register_driver(&driver);
}
static void __exit alsa_card_mychip_exit(void)
{
pci_unregister_driver(&driver);
}
module_init(alsa_card_mychip_init)
module_exit(alsa_card_mychip_exit)
请注意,这些模块入口函数带有 __init 和 __exit 前缀标签。就是这样!
文章学习参考:
https://kernel.org/doc/html/latest/sound/kernel-api/writing-an-alsa-driver.html#header-files