Proc Interface
ALSA 为 procfs(进程文件系统)提供了一个简便的接口。proc 文件在调试时非常有用。如果你正在编写一个驱动程序,并希望查看运行状态或寄存器转储,我建议你设置 proc 文件。相关的 API 定义在 <sound/info.h> 中。
要创建一个 proc 文件,可以调用 snd_card_proc_new() 函数:
struct snd_info_entry *entry;
int err = snd_card_proc_new(card, "my-file", &entry);
第二个参数用于指定要创建的 proc 文件的名称。上述示例将会在声卡目录下创建一个名为 my-file 的文件,例如:/proc/asound/card0/my-file。
与其他组件类似,通过 snd_card_proc_new() 创建的 proc 条目会在声卡注册和释放时自动注册和释放。
当创建成功后,该函数会将新创建的实例存储到第三个参数所指向的指针中。这个实例会被初始化为一个只读的文本类型 proc 文件。
如果你希望将该 proc 文件直接作为只读文本文件使用,可以通过 snd_info_set_text_ops() 设置其 read 回调函数以及私有数据:
snd_info_set_text_ops(entry, chip, my_proc_read);
其中,第二个参数(chip)是要在回调函数中使用的私有数据;第三个参数指定读取缓冲区的大小;第四个参数(my_proc_read)是回调函数,其定义方式如下:
static void my_proc_read(struct snd_info_entry *entry,
struct snd_info_buffer *buffer);
在 read 回调函数中,可以使用 snd_iprintf() 来输出字符串,它的用法与普通的 printf() 类似。例如:
static void my_proc_read(struct snd_info_entry *entry,
struct snd_info_buffer *buffer)
{
struct my_chip *chip = entry->private_data;
snd_iprintf(buffer, "This is my chip!\n");
snd_iprintf(buffer, "Port = %ld\n", chip->port);
}
文件权限可以在创建之后进行修改。默认情况下,所有用户对该文件都只有读取权限。如果你希望为用户(默认是 root)添加写权限,可以按如下方式操作:
entry->mode = S_IFREG | S_IRUGO | S_IWUSR;
并设置写入缓冲区的大小以及回调函数:
entry->c.text.write = my_proc_write;
在 write 回调函数中,你可以使用 snd_info_get_line() 获取一行文本,使用 snd_info_get_str() 从该行中提取字符串。一些示例可以参考 core/oss/mixer_oss.c 和 core/oss/pcm_oss.c。
对于原始数据类型的 proc 文件(raw-data proc-file),可以按如下方式设置属性:
static const struct snd_info_entry_ops my_file_io_ops = {
.read = my_file_io_read,
};
entry->content = SNDRV_INFO_CONTENT_DATA;
entry->private_data = chip;
entry->c.ops = &my_file_io_ops;
entry->size = 4096;
entry->mode = S_IFREG | S_IRUGO;
对于原始数据,必须正确设置 size 字段,它用于指定对 proc 文件访问的最大大小。原始模式(raw mode)的读/写回调比文本模式更加底层,你需要使用诸如 copy_from_user() 和 copy_to_user() 这类底层 I/O 函数来传输数据:
static ssize_t my_file_io_read(struct snd_info_entry *entry,
void *file_private_data,
struct file *file,
char *buf,
size_t count,
loff_t pos)
{
if (copy_to_user(buf, local_data + pos, count))
return -EFAULT;
return count;
}
如果信息条目的 size 字段已被正确设置,那么 count 和 pos 的值将保证在 0 到该大小范围之内。除非有其他特殊需求,否则在回调函数中无需对其范围进行检查。
Power Management
如果芯片需要支持挂起/恢复(suspend/resume)功能,那么你需要在驱动中添加电源管理(power-management)相关的代码。电源管理的附加代码应通过 #ifdef CONFIG_PM 包裹,或者使用 __maybe_unused 属性进行标注,否则编译器会报错。
如果驱动完全支持挂起/恢复,也就是说设备在恢复后能够正确还原到挂起前的状态,那么你可以在 PCM 的信息字段中设置 SNDRV_PCM_INFO_RESUME 标志。通常当芯片的寄存器可以安全地保存在 RAM 中并在恢复时还原时,就可以实现这一功能。如果设置了该标志,在恢复回调执行完成后,会调用 trigger 回调函数,并传入 SNDRV_PCM_TRIGGER_RESUME 参数。
即使驱动并不完全支持电源管理,但如果仍然支持部分挂起/恢复,实现 suspend/resume 回调函数仍然是有意义的。在这种情况下,应用程序可以通过调用 snd_pcm_prepare() 来重置状态,并适当地重新启动音频流。因此你可以定义 suspend/resume 回调函数,但不要为 PCM 设置 SNDRV_PCM_INFO_RESUME 标志。
需要注意的是:当调用 snd_pcm_suspend_all() 时,即使没有设置 SNDRV_PCM_INFO_RESUME 标志,也始终会触发带有 SUSPEND 的 trigger 回调。而 RESUME 标志仅影响 snd_pcm_resume() 的行为。(因此,理论上如果没有设置 SNDRV_PCM_INFO_RESUME 标志,在 trigger 回调中并不需要处理 SNDRV_PCM_TRIGGER_RESUME,但为了兼容性,最好仍然保留相关处理。)
驱动需要根据设备所连接的总线类型来定义对应的 suspend/resume 钩子函数。对于 PCI 驱动来说,回调函数的形式如下:
static int __maybe_unused snd_my_suspend(struct device *dev)
{
.... /* do things for suspend */
return 0;
}
static int __maybe_unused snd_my_resume(struct device *dev)
{
.... /* do things for suspend */
return 0;
}
实际挂起(suspend)操作的流程如下:
-
获取声卡和芯片相关的数据;
-
调用 snd_power_change_state() 并传入 SNDRV_CTL_POWER_D3hot,以切换电源状态;
-
如果使用了 AC97 编解码器,则需要对每个编解码器调用 snd_ac97_suspend();
-
如有必要,保存寄存器的值;
-
如有必要,停止硬件。
典型的代码如下所示:
static int __maybe_unused mychip_suspend(struct device *dev)
{
/* (1) */
struct snd_card *card = dev_get_drvdata(dev);
struct mychip *chip = card->private_data;
/* (2) */
snd_power_change_state(card, SNDRV_CTL_POWER_D3hot);
/* (3) */
snd_ac97_suspend(chip->ac97);
/* (4) */
snd_mychip_save_registers(chip);
/* (5) */
snd_mychip_stop_hardware(chip);
return 0;
}
实际恢复(resume)操作的流程如下:
-
获取声卡和芯片相关的数据;
-
重新初始化芯片;
-
如有必要,恢复先前保存的寄存器值;
-
恢复混音器,例如通过调用 snd_ac97_resume();
-
重新启动硬件(如果有);
-
调用 snd_power_change_state() 并传入 SNDRV_CTL_POWER_D0,以通知相关进程设备已恢复。
典型的代码如下所示:
static int __maybe_unused mychip_resume(struct pci_dev *pci)
{
/* (1) */
struct snd_card *card = dev_get_drvdata(dev);
struct mychip *chip = card->private_data;
/* (2) */
snd_mychip_reinit_chip(chip);
/* (3) */
snd_mychip_restore_registers(chip);
/* (4) */
snd_ac97_resume(chip->ac97);
/* (5) */
snd_mychip_restart_chip(chip);
/* (6) */
snd_power_change_state(card, SNDRV_CTL_POWER_D0);
return 0;
}
请注意,在该回调函数被调用时,PCM 流已经通过其自身的电源管理(PM)操作内部调用 snd_pcm_suspend_all() 被挂起了。
好了,现在我们已经准备好了所有的回调函数。接下来我们需要进行设置:在声卡初始化过程中,请确保你可以从 card 实例中获取芯片数据,通常是通过 private_data 字段,特别是在你是单独创建芯片数据的情况下:
static int snd_mychip_probe(struct pci_dev *pci,
const struct pci_device_id *pci_id)
{
....
struct snd_card *card;
struct mychip *chip;
int err;
....
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,
0, &card);
....
chip = kzalloc(sizeof(*chip), GFP_KERNEL);
....
card->private_data = chip;
....
}
当你使用 snd_card_new() 创建芯片数据时,它无论如何都可以通过 private_data 字段访问 :
static int snd_mychip_probe(struct pci_dev *pci,
const struct pci_device_id *pci_id)
{
....
struct snd_card *card;
struct mychip *chip;
int err;
....
err = snd_card_new(&pci->dev, index[dev], id[dev], THIS_MODULE,
sizeof(struct mychip), &card);
....
chip = card->private_data;
....
}
如果你需要空间来保存寄存器,也应该在这里分配相应的缓冲区,因为在挂起(suspend)阶段无法分配内存将会是致命的。分配的缓冲区应在对应的析构函数中释放。
接下来,将 suspend/resume 回调函数设置到 pci_driver 结构中:
static DEFINE_SIMPLE_DEV_PM_OPS(snd_my_pm_ops, mychip_suspend, mychip_resume);
static struct pci_driver driver = {
.name = KBUILD_MODNAME,
.id_table = snd_my_ids,
.probe = snd_my_probe,
.remove = snd_my_remove,
.driver = {
.pm = &snd_my_pm_ops,
},
};
Module Parameters:
ALSA 提供了标准的模块参数选项。每个模块至少应包含以下选项:index、id 和 enable。
如果模块支持多个声卡(通常最多支持 8 个,即 SNDRV_CARDS 个),这些选项应定义为数组形式。为了简化编程,默认的初始值已经作为常量进行了定义:
static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
static int enable[SNDRV_CARDS] = SNDRV_DEFAULT_ENABLE_PNP;
如果模块只支持单个声卡,那么这些参数也可以定义为单个变量。在这种情况下,enable 选项并不是必需的,但为了兼容性,最好还是添加一个占位选项(dummy option)。
模块参数必须使用标准的宏来声明:module_param()、module_param_array() 和 MODULE_PARM_DESC()。
典型的代码如下所示:
#define CARD_NAME "My Chip"
module_param_array(index, int, NULL, 0444);
MODULE_PARM_DESC(index, "Index value for " CARD_NAME " soundcard.");
module_param_array(id, charp, NULL, 0444);
MODULE_PARM_DESC(id, "ID string for " CARD_NAME " soundcard.");
module_param_array(enable, bool, NULL, 0444);
MODULE_PARM_DESC(enable, "Enable " CARD_NAME " soundcard.");
另外,不要忘记定义模块的描述信息和许可证。尤其是现在的 modprobe 要求模块必须声明许可证为 GPL 等,否则系统将被标记为“tainted”(受污染) :
MODULE_DESCRIPTION("Sound driver for My Chip");
MODULE_LICENSE("GPL");
Device-Managed Resources:
在前面的示例中,所有资源的分配和释放都是手动完成的。但人类本性是懒惰的,尤其是开发者更懒。因此,有一些方法可以自动释放资源,这就是所谓的 设备托管资源(device-managed resources),也称为 devres 或 devm 系列接口。
例如,通过 devm_kmalloc() 分配的对象,会在设备解绑时自动释放。
ALSA 内核核心层也提供了设备托管的辅助函数,比如 snd_devm_card_new() 用于创建声卡对象。你可以使用这个函数来代替普通的 snd_card_new(),这样就无需再显式调用 snd_card_free(),因为在发生错误或设备移除时它会被自动调用。
需要注意的一点是:snd_card_free() 的调用会在你调用 snd_card_register() 之后,才被加入释放链的最前面。
此外,private_free 回调函数在释放声卡时总是会被调用,所以请务必将硬件清理操作放在 private_free 回调中。要注意的是,它可能会在初始化流程中尚未设置完成时就被调用(比如在早期发生错误的路径中)。为避免这种无效的初始化问题,可以在 snd_card_register() 成功之后再设置 private_free 回调函数。
还有一点值得注意的是:一旦你选择使用设备托管方式管理声卡,就应尽可能为每个组件都使用对应的设备托管辅助函数。托管资源与普通资源混用可能会导致释放顺序混乱的问题。
How To Put Your Driver Into ALSA Tree:
到目前为止,你已经学习了如何编写驱动代码。此时你可能会有一个疑问:如何将我自己的驱动集成到 ALSA 的驱动树中?下面(终于 :) 简要介绍了标准的操作流程。 假设你为一块名为 “xyz” 的声卡创建了一个新的 PCI 驱动。那么该模块的名称应为 snd-xyz。新的驱动通常会被放入 ALSA 驱动源码树中的 sound/pci 目录(对于 PCI 声卡而言)。
在接下来的章节中,我们假设你的驱动代码将被放入 Linux 内核源码树中。我们将涵盖两种情况:
-
驱动由单个源文件组成;
-
驱动由多个源文件组成。
仅包含一个源文件的驱动程序
-
1、修改 sound/pci/Makefile 假设你有一个名为 xyz.c 的源文件,添加如下两行代码:
snd-xyz-y := xyz.o
obj-$(CONFIG_SND_XYZ) += snd-xyz.o
-
2、创建 Kconfig 条目
为你的 xyz 驱动添加一个新的 Kconfig 条目:
config SND_XYZ
tristate "Foobar XYZ"
depends on SND
select SND_PCM
help
Say Y here to include support for Foobar XYZ soundcard.
To compile this driver as a module, choose M here:
the module will be called snd-xyz.
select SND_PCM 这一行表示 xyz 驱动支持 PCM 功能。除了 SND_PCM,select 命令还支持以下组件:SND_RAWMIDI、SND_TIMER、SND_HWDEP、SND_MPU401_UART、SND_OPL3_LIB、SND_OPL4_LIB、SND_VX_LIB、SND_AC97_CODEC。你需要根据驱动所支持的功能,为每个组件添加对应的 select 命令。 请注意,有些选择项会隐式包含底层依赖项。例如:
-
PCM 会包含 TIMER;
-
MPU401_UART 会包含 RAWMIDI;
-
AC97_CODEC 会包含 PCM;
-
OPL3_LIB 会包含 HWDEP。
因此,不需要重复写出这些底层依赖项的 select。
关于 Kconfig 脚本的更多细节,请参考 kbuild 的相关文档。
包含多个源文件的驱动程序
假设驱动 snd-xyz 包含多个源文件,这些文件被放置在新的子目录 sound/pci/xyz 中。
-
1、在 sound/pci/Makefile 中添加如下内容,以引入新的目录 sound/pci/xyz:
obj-$(CONFIG_SND) += sound/pci/xyz/
-
2、 在目录 sound/pci/xyz 下创建一个 Makefile:
snd-xyz-y := xyz.o abc.o def.o
obj-$(CONFIG_SND_XYZ) += snd-xyz.o
-
创建 Kconfig 条目
这个步骤与上一节中的操作相同。
Useful Functions
snd_BUG()
它会在出错的位置显示 BUG? 消息和堆栈回溯信息,功能类似于 snd_BUG_ON()。当发生严重错误时,这对于定位问题非常有用。
当没有设置调试标志时,此宏会被忽略。
snd_BUG_ON()
snd_BUG_ON() 宏的作用类似于 WARN_ON() 宏。例如:
snd_BUG_ON(!pointer);
或者可以用作条件表达式:
if (snd_BUG_ON(non_zero_is_bug)) return -EINVAL;
该宏接收一个条件表达式作为参数进行判断。当配置了 CONFIG_SND_DEBUG 时,如果该表达式的值为非零,就会显示一条类似 BUG?(xxx) 的警告信息,通常还会伴随堆栈回溯信息。
无论是否打印警告,它都会返回该条件表达式的结果值。
1815

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



