从零写一个ALSA声卡驱动学习(8)之ALSA如何和procfs进行绑定?

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)操作的流程如下:

  1. 获取声卡和芯片相关的数据;

  2. 调用 snd_power_change_state() 并传入 SNDRV_CTL_POWER_D3hot,以切换电源状态;

  3. 如果使用了 AC97 编解码器,则需要对每个编解码器调用 snd_ac97_suspend();

  4. 如有必要,保存寄存器的值;

  5. 如有必要,停止硬件。

典型的代码如下所示:

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)操作的流程如下:

  1. 获取声卡和芯片相关的数据;

  2. 重新初始化芯片;

  3. 如有必要,恢复先前保存的寄存器值;

  4. 恢复混音器,例如通过调用 snd_ac97_resume();

  5. 重新启动硬件(如果有);

  6. 调用 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. 驱动由单个源文件组成;

  2. 驱动由多个源文件组成。

仅包含一个源文件的驱动程序

  • 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) 的警告信息,通常还会伴随堆栈回溯信息。

无论是否打印警告,它都会返回该条件表达式的结果值。

解析这个函数,尤其是传入的参数int snd_card_new(struct device *parent, int idx, const char *xid, 202 struct module *module, int extra_size, 203 struct snd_card **card_ret) 204 { 205 struct snd_card *card; 206 int err; 207 208 if (snd_BUG_ON(!card_ret)) 209 return -EINVAL; 210 *card_ret = NULL; 211 212 if (extra_size < 0) 213 extra_size = 0; 214 card = kzalloc(sizeof(*card) + extra_size, GFP_KERNEL); 215 if (!card) 216 return -ENOMEM; 217 if (extra_size > 0) 218 card->private_data = (char *)card + sizeof(struct snd_card); 219 if (xid) 220 strlcpy(card->id, xid, sizeof(card->id)); 221 err = 0; 222 mutex_lock(&snd_card_mutex); 223 if (idx < 0) /* first check the matching module-name slot */ 224 idx = get_slot_from_bitmask(idx, module_slot_match, module); 225 if (idx < 0) /* if not matched, assign an empty slot */ 226 idx = get_slot_from_bitmask(idx, check_empty_slot, module); 227 if (idx < 0) 228 err = -ENODEV; 229 else if (idx < snd_ecards_limit) { 230 if (test_bit(idx, snd_cards_lock)) 231 err = -EBUSY; /* invalid */ 232 } else if (idx >= SNDRV_CARDS) 233 err = -ENODEV; 234 if (err < 0) { 235 mutex_unlock(&snd_card_mutex); 236 dev_err(parent, "cannot find the slot for index %d (range 0-%i), error: %d\n", 237 idx, snd_ecards_limit - 1, err); 238 kfree(card); 239 return err; 240 } 241 set_bit(idx, snd_cards_lock); /* lock it */ 242 if (idx >= snd_ecards_limit) 243 snd_ecards_limit = idx + 1; /* increase the limit */ 244 mutex_unlock(&snd_card_mutex); 245 card->dev = parent; 246 card->number = idx; 247 card->module = module; 248 INIT_LIST_HEAD(&card->devices); 249 init_rwsem(&card->controls_rwsem); 250 rwlock_init(&card->ctl_files_rwlock); 251 INIT_LIST_HEAD(&card->controls); 252 INIT_LIST_HEAD(&card->ctl_files); 253 spin_lock_init(&card->files_lock); 254 INIT_LIST_HEAD(&card->files_list); 255 #ifdef CONFIG_PM 256 init_waitqueue_head(&card->power_sleep); 257 #endif 258 259 device_initialize(&card->card_dev); 260 card->card_dev.parent = parent; 261 card->card_dev.class = sound_class; 262 card->card_dev.release = release_card_device; 263 card->card_dev.groups = card->dev_groups; 264 card->dev_groups[0] = &card_dev_attr_group; 265 err = kobject_set_name(&card->card_dev.kobj, "card%d", idx); 266 if (err < 0) 267 goto __error; 268 269 snprintf(card->irq_descr, sizeof(card->irq_descr), "%s:%s", 270 dev_driver_string(card->dev), dev_name(&card->card_dev)); 271 272 /* the control interface cannot be accessed from the user space until */ 273 /* snd_cards_bitmask and snd_cards are set with snd_card_register */ 274 err = snd_ctl_create(card); 275 if (err < 0) { 276 dev_err(parent, "unable to register control minors\n"); 277 goto __error; 278 } 279 err = snd_info_card_create(card); 280 if (err < 0) { 281 dev_err(parent, "unable to create card info\n"); 282 goto __error_ctl; 283 } 284 *card_ret = card; 285 return 0; 286 287 __error_ctl: 288 snd_device_free_all(card); 289 __error: 290 put_device(&card->card_dev); 291 return err; 292 } 293 EXPORT_SYMBOL(snd_card_new);
最新发布
07-17
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值