2024年3月12日发(作者:焦昭懿)
ALSA声卡驱动之一:ALSA架构简介
一. 概述
ALSA是Advanced Linux Sound Architecture 的缩写,目前已经成为了linux的主
流音频体系结构,想了解更多的关于ALSA的这一开源项目的信息和知识,请查
看以下网址:/。
在内核设备驱动层,ALSA提供了alsa-driver,同时在应用层,ALSA为我们提
供了alsa-lib,应用程序只要调用alsa-lib提供的API,即可以完成对底层音频硬
件的控制。
图 1.1 alsa的软件体系结构
由图1.1可以看出,用户空间的alsa-lib对应用程序提供统一的API接口,这样
可以隐藏了驱动层的实现细节,简化了应用程序的实现难度。内核空间中,
alsa-soc其实是对alsa-driver的进一步封装,他针对嵌入式设备提供了一些列
增强的功能。本系列博文仅对嵌入式系统中的alsa-driver和alsa-soc进行讨论。
1
二. ALSA设备文件结构
我们从alsa在linux中的设备文件结构开始我们的alsa之旅. 看看我的电脑
中的alsa驱动的设备文件结构:
$ cd /dev/snd
$ ls -l
crw-rw----+ 1 root audio 116, 8 2011-02-23 21:38 controlC0
crw-rw----+ 1 root audio 116, 4 2011-02-23 21:38 midiC0D0
crw-rw----+ 1 root audio 116, 7 2011-02-23 21:39 pcmC0D0c
crw-rw----+ 1 root audio 116, 6 2011-02-23 21:56 pcmC0D0p
crw-rw----+ 1 root audio 116, 7 2011-02-23 21:39 pcmC0D1c
crw-rw----+ 1 root audio 116, 5 2011-02-23 21:38 pcmC0D1p
crw-rw----+ 1 root audio 116, 3 2011-02-23 21:38 seq
crw-rw----+ 1 root audio 116, 2 2011-02-23 21:38 timer
$
我们可以看到以下设备文件:
controlC0 --> 用于声卡的控制,例如通道选择,混音,麦克风
的控制等
midiC0D0 --> 用于播放midi音频
pcmC0D0c --〉 用于录音的pcm设备
pcmC0D0p --〉 用于播放的pcm设备
seq --〉 音序器
timer --〉 定时器
其中,C0D0代表的是声卡0中的设备0,pcmC0D0c最后一个c代表capture,
pcmC0D0p最后一个p代表playback,这些都是alsa-driver中的命名规则。从
上面的列表可以看出,我的声卡下挂了6个设备,根据声卡的实际能力,驱动实
际上可以挂上更多种类的设备,在include/sound/core.h中,定义了以下设备类
型:
[c-sharp] view plain copy
2
1. #define SNDRV_DEV_TOPLEVEL ((__force snd_device_type_t)
0)
2. #define SNDRV_DEV_CONTROL ((__force snd_device_type_t)
1)
3. #define SNDRV_DEV_LOWLEVEL_PRE ((__force snd_device_type
_t) 2)
4. #define SNDRV_DEV_LOWLEVEL_NORMAL ((__force snd_device_ty
pe_t) 0x1000)
5. #define SNDRV_DEV_PCM ((__force snd_device_type_t)
0x1001)
6. #define SNDRV_DEV_RAWMIDI ((__force snd_device_type_t)
0x1002)
7. #define SNDRV_DEV_TIMER ((__force snd_device_type_t)
0x1003)
8. #define SNDRV_DEV_SEQUENCER ((__force snd_device_type_t)
0x1004)
9. #define SNDRV_DEV_HWDEP ((__force snd_device_type_t)
0x1005)
10. #define SNDRV_DEV_INFO ((__force snd_device_type_t)
0x1006)
11. #define SNDRV_DEV_BUS ((__force snd_device_type_t)
0x1007)
12. #define SNDRV_DEV_CODEC ((__force snd_device_type_t)
0x1008)
13. #define SNDRV_DEV_JACK ((__force snd_device_typ
e_t) 0x1009)
14. #define SNDRV_DEV_LOWLEVEL ((__force snd_device_type_t)
0x2000)
通常,我们更关心的是pcm和control这两种设备。
三. 驱动的代码文件结构
在Linux2.6代码树中,Alsa的代码文件结构如下:
sound
/core
/oss
/seq
/ioctl32
/include
3
/drivers
/i2c
/synth
/emux
/pci
/(cards)
/isa
/(cards)
/arm
/ppc
/sparc
/usb
/pcmcia /(cards)
/oss
/soc
/codecs
core 该目录包含了ALSA驱动的中间层,它是整个ALSA驱动
的核心部分
core/oss 包含模拟旧的OSS架构的PCM和Mixer模块
core/seq 有关音序器相关的代码
include ALSA驱动的公共头文件目录,该目录的头文件需要导出给
用户空间的应用程序使用,通常,驱动模块私有的头文件不应放置在这里
drivers 放置一些与CPU、BUS架构无关的公用代码
i2c ALSA自己的I2C控制代码
pci pci声卡的顶层目录,子目录包含各种pci声卡的代码
isa isa声卡的顶层目录,子目录包含各种isa声卡的代码
soc 针对system-on-chip体系的中间层代码
soc/codecs 针对soc体系的各种codec的代码,与平台无关
4
ALSA声卡驱动之二:声卡的创建
1. struct snd_card
1.1. snd_card是什么
snd_card可以说是整个ALSA音频驱动最顶层的一个结构,整个声卡的软件逻
辑结构开始于该结构,几乎所有与声音相关的逻辑设备都是在snd_card的管理
之下,声卡驱动的第一个动作通常就是创建一个snd_card结构体。正因为如此,
本节中,我们也从 struct cnd_card开始吧。
1.2. snd_card的定义
snd_card的定义位于改头文件中:include/sound/core.h
[c-sharp] view plain copy
1. /* main structure for soundcard */
2.
3. struct snd_card {
4. int number; /* number of soundcard (index to
5. snd_cards) */
6.
7. char id[16]; /* id string of this card */
8. char driver[16]; /* driver name */
9. char shortname[32]; /* short name of this soundca
rd */
10. char longname[80]; /* name of this soundcard */
11. char mixername[80]; /* mixer name */
12. char components[128]; /* card components delim
ited with
13. space */
14. struct module *module; /* top-level module */
15.
16. void *private_data; /* private data for soundcar
d */
17. void (*private_free) (struct snd_card *card); /* cal
lback for freeing of
5
18. private data */
19. struct list_head devices; /* devices */
20.
21. unsigned int last_numid; /* last used numeric ID
*/
22. struct rw_semaphore controls_rwsem; /* controls list
lock */
23. rwlock_t ctl_files_rwlock; /* ctl_files list lock *
/
24. int controls_count; /* count of all controls */
25. int user_ctl_count; /* count of all user control
s */
26. struct list_head controls; /* all controls for this
card */
27. struct list_head ctl_files; /* active control files
*/
28.
29. struct snd_info_entry *proc_root; /* root for soun
dcard specific files */
30. struct snd_info_entry *proc_id; /* the card id */
31. struct proc_dir_entry *proc_root_link; /* number li
nk to real id */
32.
33. struct list_head files_list; /* all files associa
ted to this card */
34. struct snd_shutdown_f_ops *s_f_ops; /* file operatio
ns in the shutdown
35. state */
36. spinlock_t files_lock; /* lock the files for th
is card */
37. int shutdown; /* this card is going down *
/
38. int free_on_last_close; /* free in context of fi
le_release */
39. wait_queue_head_t shutdown_sleep;
40. struct device *dev; /* device assigned to this c
ard */
41. #ifndef CONFIG_SYSFS_DEPRECATED
42. struct device *card_dev; /* cardX object for sysf
s */
43. #endif
44.
45. #ifdef CONFIG_PM
6
46. unsigned int power_state; /* power state */
47. struct mutex power_lock; /* power lock */
48. wait_queue_head_t power_sleep;
49. #endif
50.
51. #if defined(CONFIG_SND_MIXER_OSS) || defined(CONFIG_SND_
MIXER_OSS_MODULE)
52. struct snd_mixer_oss *mixer_oss;
53. int mixer_oss_change_count;
54. #endif
55. };
struct list_head devices 记录该声卡下所有逻辑设备的链表
struct list_head controls 记录该声卡下所有的控制单元的链表
void *private_data 声卡的私有数据,可以在创建声卡时通过参数
指定数据的大小
2. 声卡的建立流程
2.1.1. 第一步,创建snd_card的一个实例
[c-sharp] view plain copy
1. struct snd_card *card;
2. int err;
3. ....
4. err = snd_card_create(index, id, THIS_MODULE, 0, &card);
index 一个整数值,该声卡的编号
id 字符串,声卡的标识符
第四个参数 该参数决定在创建snd_card实例时,需要同时额外分配的
私有数据的大小,该数据的指针最终会赋值给snd_card的private_data
数据成员
card 返回所创建的snd_card实例的指针
7
2.1.2. 第二步,创建声卡的芯片专用数据
声卡的专用数据主要用于存放该声卡的一些资源信息,例如中断资源、io资源、
dma资源等。可以有两种创建方法:
通过上一步中snd_card_create()中的第四个参数,让snd_card_create
自己创建
[c-sharp] view plain copy
1. // struct mychip 用于保存专用数据
2. err = snd_card_create(index, id, THIS_MODULE,
3. sizeof(struct mychip), &card);
4. // 从private_data中取出
5. struct mychip *chip = card->private_data;
自己创建:
[c-sharp] view plain copy
1. struct mychip {
2. struct snd_card *card;
3. ....
4. };
5. struct snd_card *card;
6. struct mychip *chip;
7.
8. chip = kzalloc(sizeof(*chip), GFP_KERNEL);
9. ......
10. err = snd_card_create(index[dev], id[dev], THIS_MODULE,
0, &card);
11. // 专用数据记录snd_card实例
12. chip->card = card;
13. .....
然后,把芯片的专有数据注册为声卡的一个低阶设备:
[c-sharp] view plain copy
1. static int snd_mychip_dev_free(struct snd_device *device)
8
2. {
3. return snd_mychip_free(device->device_data);
4. }
5.
6. static struct snd_device_ops ops = {
7. .dev_free = snd_mychip_dev_free,
8. };
9. ....
10. snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);
注册为低阶设备主要是为了当声卡被注销时,芯片专用数据所占用的内存可以
被自动地释放。
2.1.3. 第三步,设置Driver的ID和名字
[c-sharp] view plain copy
1. strcpy(card->driver, "My Chip");
2. strcpy(card->shortname, "My Own Chip 123");
3. sprintf(card->longname, "%s at 0x%lx irq %i",
4. card->shortname, chip->ioport, chip->irq);
snd_card的driver字段保存着芯片的ID字符串,user空间的alsa-lib会使用到
该字符串,所以必须要保证该ID的唯一性。shortname字段更多地用于打印信
息,longname字段则会出现在/proc/asound/cards中。
2.1.4. 第四步,创建声卡的功能部件(逻辑设备),例如PCM,Mixer,MIDI
等
这时候可以创建声卡的各种功能部件了,还记得开头的snd_card结构体的
devices字段吗?每一种部件的创建最终会调用snd_device_new()来生成一个
snd_device实例,并把该实例链接到snd_card的devices链表中。
通常,alsa-driver的已经提供了一些常用的部件的创建函数,而不必直接调用
snd_device_new(),比如:
PCM ---- snd_pcm_new()
RAWMIDI -- snd_rawmidi_new()
9
CONTROL -- snd_ctl_create()
TIMER -- snd_timer_new()
INFO -- snd_card_proc_new()
JACK -- snd_jack_new()
2.1.5. 第五步,注册声卡
[c-sharp] view plain copy
1. err = snd_card_register(card);
2. if (err < 0) {
3. snd_card_free(card);
4. return err;
5. }
2.2. 一个实际的例子
我把/sound/arm/pxa2xx-ac97.c的部分代码贴上来:
[cpp] view plain copy
1. static int __devinit pxa2xx_ac97_probe(struct platform_device *dev)
2. {
3. struct snd_card *card;
4. struct snd_ac97_bus *ac97_bus;
5. struct snd_ac97_template ac97_template;
6. int ret;
7. pxa2xx_audio_ops_t *pdata = dev->rm_data;
8.
9. if (dev->id >= 0) {
10. dev_err(&dev->dev, "PXA2xx has only one AC97 port./n");
11. ret = -ENXIO;
12. goto err_dev;
13. }
14. ////(1)////
15. ret = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
16. THIS_MODULE, 0, &card);
17. if (ret < 0)
18. goto err;
19.
20. card->dev = &dev->dev;
21. ////(3)////
10
22. strncpy(card->driver, dev->->name, sizeof(card->driver));
23.
24. ////(4)////
25. ret = pxa2xx_pcm_new(card, &pxa2xx_ac97_pcm_client, &pxa2xx_ac97_pcm);
26. if (ret)
27. goto err;
28. ////(2)////
29. ret = pxa2xx_ac97_hw_probe(dev);
30. if (ret)
31. goto err;
32.
33. ////(4)////
34. ret = snd_ac97_bus(card, 0, &pxa2xx_ac97_ops, NULL, &ac97_bus);
35. if (ret)
36. goto err_remove;
37. memset(&ac97_template, 0, sizeof(ac97_template));
38. ret = snd_ac97_mixer(ac97_bus, &ac97_template, &pxa2xx_ac97_ac97);
39. if (ret)
40. goto err_remove;
41. ////(3)////
42. snprintf(card->shortname, sizeof(card->shortname),
43. "%s", snd_ac97_get_short_name(pxa2xx_ac97_ac97));
44. snprintf(card->longname, sizeof(card->longname),
45. "%s (%s)", dev->->name, card->mixername);
46.
47. if (pdata && pdata->codec_pdata[0])
48. snd_ac97_dev_add_pdata(ac97_bus->codec[0], pdata->codec_pdata[0]);
49. snd_card_set_dev(card, &dev->dev);
50. ////(5)////
51. ret = snd_card_register(card);
52. if (ret == 0) {
53. platform_set_drvdata(dev, card);
54. return 0;
55. }
56.
57. err_remove:
58. pxa2xx_ac97_hw_remove(dev);
59. err:
60. if (card)
61. snd_card_free(card);
62. err_dev:
63. return ret;
64. }
65.
11
66. static int __devexit pxa2xx_ac97_remove(struct platform_device *dev)
67. {
68. struct snd_card *card = platform_get_drvdata(dev);
69.
70. if (card) {
71. snd_card_free(card);
72. platform_set_drvdata(dev, NULL);
73. pxa2xx_ac97_hw_remove(dev);
74. }
75.
76. return 0;
77. }
78.
79. static struct platform_driver pxa2xx_ac97_driver = {
80. .probe = pxa2xx_ac97_probe,
81. .remove = __devexit_p(pxa2xx_ac97_remove),
82. .driver = {
83. .name = "pxa2xx-ac97",
84. .owner = THIS_MODULE,
85. #ifdef CONFIG_PM
86. .pm = &pxa2xx_ac97_pm_ops,
87. #endif
88. },
89. };
90.
91. static int __init pxa2xx_ac97_init(void)
92. {
93. return platform_driver_register(&pxa2xx_ac97_driver);
94. }
95.
96. static void __exit pxa2xx_ac97_exit(void)
97. {
98. platform_driver_unregister(&pxa2xx_ac97_driver);
99. }
100.
101. module_init(pxa2xx_ac97_init);
102. module_exit(pxa2xx_ac97_exit);
103.
104. MODULE_AUTHOR("Nicolas Pitre");
105. MODULE_DESCRIPTION("AC97 driver for the Intel PXA2xx chip");
驱动程序通常由probe回调函数开始,对一下2.1中的步骤,是否有相似之处?
经过以上的创建步骤之后,声卡的逻辑结构如下图所示:
12
图 2.2.1 声卡的软件逻辑结构
下面的章节里我们分别讨论一下snd_card_create()和snd_card_register()这两
个函数。
3. snd_card_create()
snd_card_create()在/sound/core/init.c中定义。
[cpp] view plain copy
1. /**
2. * snd_card_create - create and initialize a soundcard s
tructure
3. * @idx: card index (address) [0 ... (SNDRV_CARDS-1)]
4. * @xid: card identification (ASCII string)
5. * @module: top level module for locking
6. * @extra_size: allocate this extra size after the main
soundcard structure
7. * @card_ret: the pointer to store the created card inst
ance
8. *
9. * Creates and initializes a soundcard structure.
10. *
11. * The function allocates snd_card instance via kzalloc
with the given
12. * space for the driver to use freely. The allocated s
truct is stored
13. * in the given card_ret pointer.
14. *
13
15. * Returns zero if successful or a negative error code.
16. */
17. int snd_card_create(int idx, const char *xid,
18. struct module *module, int extra_size,
19. struct snd_card **card_ret)
首先,根据extra_size参数的大小分配内存,该内存区可以作为芯片的专有数据
使用(见前面的介绍):
[c-sharp] view plain copy
1. card = kzalloc(sizeof(*card) + extra_size, GFP_KERNEL);
2. if (!card)
3. return -ENOMEM;
拷贝声卡的ID字符串:
[c-sharp] view plain copy
1. if (xid)
2. strlcpy(card->id, xid, sizeof(card->id));
如果传入的声卡编号为-1,自动分配一个索引编号:
[c-sharp] view plain copy
1. if (idx < 0) {
2. for (idx2 = 0; idx2 < SNDRV_CARDS; idx2++)
3. /* idx == -1 == 0xffff means: take any free slot
*/
4. if (~snd_cards_lock & idx & 1< 5. if (module_slot_match(module, idx2)) { 6. idx = idx2; 7. break; 8. } 9. } 10. } 11. if (idx < 0) { 12. for (idx2 = 0; idx2 < SNDRV_CARDS; idx2++) 13. /* idx == -1 == 0xffff means: take any free slot */ 14. if (~snd_cards_lock & idx & 1< 15. if (!slots[idx2] || !*slots[idx2]) { 14 16. idx = idx2; 17. break; 18. } 19. } 20. } 初始化snd_card结构中必要的字段: [c-sharp] view plain copy 1. card->number = idx; 2. card->module = module; 3. INIT_LIST_HEAD(&card->devices); 4. init_rwsem(&card->controls_rwsem); 5. rwlock_init(&card->ctl_files_rwlock); 6. INIT_LIST_HEAD(&card->controls); 7. INIT_LIST_HEAD(&card->ctl_files); 8. spin_lock_init(&card->files_lock); 9. INIT_LIST_HEAD(&card->files_list); 10. init_waitqueue_head(&card->shutdown_sleep); 11. #ifdef CONFIG_PM 12. mutex_init(&card->power_lock); 13. init_waitqueue_head(&card->power_sleep); 14. #endif 建立逻辑设备:Control [c-sharp] view plain copy 1. /* the control interface cannot be accessed from the user space until */ 2. /* snd_cards_bitmask and snd_cards are set with snd_card_ register */ 3. err = snd_ctl_create(card); 建立proc文件中的info节点:通常就是/proc/asound/card0 [c-sharp] view plain copy 1. err = snd_info_card_create(card); 把第一步分配的内存指针放入private_data字段中: [c-sharp] view plain copy 15 1. if (extra_size > 0) 2. card->private_data = (char *)card + sizeof(struct snd _card); 4. snd_card_register() snd_card_create()在/sound/core/init.c中定义。 [c-sharp] view plain copy 1. /** 2. * snd_card_register - register the soundcard 3. * @card: soundcard structure 4. * 5. * This function registers all the devices assigned to t he soundcard. 6. * Until calling this, the ALSA control interface is blo cked from the 7. * external accesses. Thus, you should call this functi on at the end 8. * of the initialization of the card. 9. * 10. * Returns zero otherwise a negative error code if the registrain failed. 11. */ 12. int snd_card_register(struct snd_card *card) 首先,创建sysfs下的设备: [c-sharp] view plain copy 1. if (!card->card_dev) { 2. card->card_dev = device_create(sound_class, card->dev , 3. MKDEV(0, 0), card, 4. "card%i", card->number); 5. if (IS_ERR(card->card_dev)) 6. card->card_dev = NULL; 7. } 其中,sound_class是在/sound/sound_core.c中创建的: [c-sharp] view plain copy 16 1. static char *sound_devnode(struct device *dev, mode_t *mo de) 2. { 3. if (MAJOR(dev->devt) == SOUND_MAJOR) 4. return NULL; 5. return kasprintf(GFP_KERNEL, "snd/%s", dev_name(dev)) ; 6. } 7. static int __init init_soundcore(void) 8. { 9. int rc; 10. 11. rc = init_oss_soundcore(); 12. if (rc) 13. return rc; 14. 15. sound_class = class_create(THIS_MODULE, "sound"); 16. if (IS_ERR(sound_class)) { 17. cleanup_oss_soundcore(); 18. return PTR_ERR(sound_class); 19. } 20. 21. sound_class->devnode = sound_devnode; 22. 23. return 0; 24. } 由此可见,声卡的class将会出现在文件系统的/sys/class/sound/下面,并且, sound_devnode()也决定了相应的设备节点也将会出现在/dev/snd/下面。 接下来的步骤,通过snd_device_register_all()注册所有挂在该声卡下的逻辑设 备,snd_device_register_all()实际上是通过snd_card的devices链表,遍历所 有的snd_device,并且调用snd_device的ops->dev_register()来实现各自设备 的注册的。 [c-sharp] view plain copy 1. if ((err = snd_device_register_all(card)) < 0) 2. return err; 最后就是建立一些相应的proc和sysfs下的文件或属性节点,代码就不贴了。 17 至此,整个声卡完成了建立过程。 3. Linux ALSA声卡驱动之三:PCM设备的创建 1. PCM是什么 PCM是英文Pulse-code modulation的缩写,中文译名是脉冲编码调制。我们 知道在现实生活中,人耳听到的声音是模拟信号,PCM就是要把声音从模拟转 换成数字信号的一种技术,他的原理简单地说就是利用一个固定的频率对模拟信 号进行采样,采样后的信号在波形上看就像一串连续的幅值不一的脉冲,把这些 脉冲的幅值按一定的精度进行量化,这些量化后的数值被连续地输出、传输、处 理或记录到存储介质中,所有这些组成了数字音频的产生过程。 图1.1 模拟音频的采样、量化 PCM信号的两个重要指标是采样频率和量化精度,目前,CD音频的采样频率 通常为44100Hz,量化精度是16bit。通常,播放音乐时,应用程序从存储介质 中读取音频数据(MP3、WMA、),经过解码后,最终送到音频驱动 程序中的就是PCM数据,反过来,在录音时,音频驱动不停地把采样所得的 PCM数据送回给应用程序,由应用程序完成压缩、存储等任务。所以,音频驱 动的两大核心任务就是: 18 playback 如何把用户空间的应用程序发过来的PCM数据,转化为人耳 可以辨别的模拟音频 capture 把mic拾取到得模拟信号,经过采样、量化,转换为PCM信 号送回给用户空间的应用程序 2. alsa-driver中的PCM中间层 ALSA已经为我们实现了功能强劲的PCM中间层,自己的驱动中只要实现一些 底层的需要访问硬件的函数即可。 要访问PCM的中间层代码,你首先要包含头文件 需要访问一些与 hw_param相关的函数,可能也要包含 每个声卡最多可以包含4个pcm的实例,每个pcm实例对应一个pcm设备文 件。pcm实例数量的这种限制源于linux设备号所占用的位大小,如果以后使用 64位的设备号,我们将可以创建更多的pcm实例。不过大多数情况下,在嵌入 式设备中,一个pcm实例已经足够了。 一个pcm实例由一个playback stream和一个capture stream组成,这两个 stream又分别有一个或多个substreams组成。 19 图2.1 声卡中的pcm结构 在嵌入式系统中,通常不会像图2.1中这么复杂,大多数情况下是一个声卡,一 个pcm实例,pcm下面有一个playback和capture stream,playback和capture 下面各自有一个substream。 下面一张图列出了pcm中间层几个重要的结构,他可以让我们从uml的角度看 一看这列结构的关系,理清他们之间的关系,对我们理解pcm中间层的实现方 式。 20 图2.2 pcm中间层的几个重要的结构体的关系 图 snd_pcm是挂在snd_card下面的一个snd_device snd_pcm中的字段:streams[2],该数组中的两个元素指向两个 snd_pcm_str结构,分别代表playback stream和capture stream snd_pcm_str中的substream字段,指向snd_pcm_substream结构 snd_pcm_substream是pcm中间层的核心,绝大部分任务都是在 substream中处理,尤其是他的ops(snd_pcm_ops)字段,许多user 空间的应用程序通过alsa-lib对驱动程序的请求都是由该结构中的函数处 理。它的runtime字段则指向snd_pcm_runtime结构,snd_pcm_runtime 记录这substream的一些重要的软件和硬件运行环境和参数。 3. 新建一个pcm alsa-driver的中间层已经为我们提供了新建pcm的api: 21 int snd_pcm_new(struct snd_card *card, const char *id, int device, int playback_count, int capture_count, struct snd_pcm ** rpcm); 参数device 表示目前创建的是该声卡下的第几个pcm,第一个pcm设备从0 开始。 参数playback_count 表示该pcm将会有几个playback substream。 参数capture_count 表示该pcm将会有几个capture substream。 另一个用于设置pcm操作函数接口的api: void snd_pcm_set_ops(struct snd_pcm *pcm, int direction, struct snd_pcm_ops *ops); 新建一个pcm可以用下面一张新建pcm的调用的序列图进行描述: 图3.1 新建pcm的序列图 snd_card_create pcm是声卡下的一个设备(部件),所以第一步是 要创建一个声卡 22 snd_pcm_new 调用该api创建一个pcm,才该api中会做以下事情 o o o 如果有,建立playback stream,相应的substream也同时建立 如果有,建立capture stream,相应的substream也同时建立 调用snd_device_new()把该pcm挂到声卡中,参数ops中的 dev_register字段指向了函数snd_pcm_dev_register,这个回调 函数会在声卡的注册阶段被调用。 snd_pcm_set_ops 设置操作该pcm的控制/操作接口函数,参数中的 snd_pcm_ops结构中的函数通常就是我们驱动要实现的函数 snd_card_register 注册声卡,在这个阶段会遍历声卡下的所有逻辑设 备,并且调用各设备的注册回调函数,对于pcm,就是第二步提到的 snd_pcm_dev_register函数,该回调函数建立了和用户空间应用程序 (alsa-lib)通信所用的设备文件节点:/dev/snd/pcmCxxDxxp和 /dev/snd/pcmCxxDxxc 4. 设备文件节点的建立(dev/snd/pcmCxxDxxp、pcmCxxDxxc) 4.1 struct snd_minor 每个snd_minor结构体保存了声卡下某个逻辑设备的上下文信息,他在逻辑设 备建立阶段被填充,在逻辑设备被使用时就可以从该结构体中得到相应的信息。 pcm设备也不例外,也需要使用该结构体。该结构体在include/sound/core.h中 定义。 [c-sharp] view plain copy 1. struct snd_minor { 2. int type; /* SNDRV_DEVICE_TYPE_XXX */ 3. int card; /* card number */ 4. int device; /* device number */ 5. const struct file_operations *f_ops; /* file opera tions */ 6. void *private_data; /* private data for f_ops->op en */ 7. struct device *dev; /* device for sysfs */ 8. }; 在sound/sound.c中定义了一个snd_minor指针的全局数组: [c-sharp] view plain copy 1. static struct snd_minor *snd_minors[256]; 23 前面说过,在声卡的注册阶段(snd_card_register),会调用pcm的回调函数 snd_pcm_dev_register(),这个函数里会调用函数 snd_register_device_for_dev(): [c-sharp] view plain copy 1. static int snd_pcm_dev_register(struct snd_device *device ) 2. { 3. ...... 4. 5. /* register pcm */ 6. err = snd_register_device_for_dev(devtype, pcm->card, 7. pcm->device, 8. &snd_pcm_f_ops[cidx], 9. pcm, str, dev); 10. ...... 11. } 我们再进入snd_register_device_for_dev(): [c-sharp] view plain copy 1. int snd_register_device_for_dev(int type, struct snd_card *card, int dev, 2. const struct file_operations *f_ops, 3. void *private_data, 4. const char *name, struct device *device) 5. { 6. int minor; 7. struct snd_minor *preg; 8. 9. if (snd_BUG_ON(!name)) 10. return -EINVAL; 11. preg = kmalloc(sizeof *preg, GFP_KERNEL); 12. if (preg == NULL) 13. return -ENOMEM; 14. preg->type = type; 15. preg->card = card ? card->number : -1; 16. preg->device = dev; 17. preg->f_ops = f_ops; 18. preg->private_data = private_data; 24 19. mutex_lock(&sound_mutex); 20. #ifdef CONFIG_SND_DYNAMIC_MINORS 21. minor = snd_find_free_minor(); 22. #else 23. minor = snd_kernel_minor(type, card, dev); 24. if (minor >= 0 && snd_minors[minor]) 25. minor = -EBUSY; 26. #endif 27. if (minor < 0) { 28. mutex_unlock(&sound_mutex); 29. kfree(preg); 30. return minor; 31. } 32. snd_minors[minor] = preg; 33. preg->dev = device_create(sound_class, device, MKDEV (major, minor), 34. private_data, "%s", name); 35. if (IS_ERR(preg->dev)) { 36. snd_minors[minor] = NULL; 37. mutex_unlock(&sound_mutex); 38. minor = PTR_ERR(preg->dev); 39. kfree(preg); 40. return minor; 41. } 42. 43. mutex_unlock(&sound_mutex); 44. return 0; 45. } 首先,分配并初始化一个snd_minor结构中的各字段 o type: SNDRV_DEVICE_TYPE_PCM_PLAYBACK/SNDRV_DEVICE_ TYPE_PCM_CAPTURE o o o o card: card的编号 device:pcm实例的编号,大多数情况为0 f_ops:snd_pcm_f_ops private_data:指向该pcm的实例 根据type,card和pcm的编号,确定数组的索引值minor,minor也作 为pcm设备的此设备号 把该snd_minor结构的地址放入全局数组snd_minors[minor]中 最后,调用device_create创建设备节点 25 4.2 设备文件的建立 在4.1节的最后,设备文件已经建立,不过4.1节的重点在于snd_minors数组 的赋值过程,在本节中,我们把重点放在设备文件中。 回到pcm的回调函数snd_pcm_dev_register()中: [c-sharp] view plain copy 1. static int snd_pcm_dev_register(struct snd_device *device) 2. { 3. int cidx, err; 4. char str[16]; 5. struct snd_pcm *pcm; 6. struct device *dev; 7. 8. pcm = device->device_data; 9. ...... 10. for (cidx = 0; cidx < 2; cidx++) { 11. ...... 12. switch (cidx) { 13. case SNDRV_PCM_STREAM_PLAYBACK: 14. sprintf(str, "pcmC%iD%ip", pcm->card->number, pcm->de vice); 15. devtype = SNDRV_DEVICE_TYPE_PCM_PLAYBACK; 16. break; 17. case SNDRV_PCM_STREAM_CAPTURE: 18. sprintf(str, "pcmC%iD%ic", pcm->card->number, pcm->de vice); 19. devtype = SNDRV_DEVICE_TYPE_PCM_CAPTURE; 20. break; 21. } 22. /* device pointer to use, pcm->dev takes precedence if 23. * it is assigned, otherwise fall back to card's device 24. * if possible */ 25. dev = pcm->dev; 26. if (!dev) 27. dev = snd_card_get_device_link(pcm->card); 28. /* register pcm */ 29. err = snd_register_device_for_dev(devtype, pcm->card, 30. pcm->device, 31. &snd_pcm_f_ops[cidx], 32. pcm, str, dev); 33. ...... 26 34. } 35. ...... 36. } 以上代码我们可以看出,对于一个pcm设备,可以生成两个设备文件,一个用 于playback,一个用于capture,代码中也确定了他们的命名规则: playback -- pcmCxDxp,通常系统中只有一各声卡和一个pcm,它就 是pcmC0D0p capture -- pcmCxDxc,通常系统中只有一各声卡和一个pcm,它就是 pcmC0D0c snd_pcm_f_ops snd_pcm_f_ops是一个标准的文件系统file_operations结构数组,它的定义在 sound/core/pcm_native.c中: [c-sharp] view plain copy 1. const struct file_operations snd_pcm_f_ops[2] = { 2. { 3. .owner = THIS_MODULE, 4. .write = snd_pcm_write, 5. .aio_write = snd_pcm_aio_write, 6. .open = snd_pcm_playback_open, 7. .release = snd_pcm_release, 8. .llseek = no_llseek, 9. .poll = snd_pcm_playback_poll, 10. .unlocked_ioctl = snd_pcm_playback_ioctl, 11. .compat_ioctl = snd_pcm_ioctl_compat, 12. .mmap = snd_pcm_mmap, 13. .fasync = snd_pcm_fasync, 14. .get_unmapped_area = snd_pcm_get_unmapped_are a, 15. }, 16. { 17. .owner = THIS_MODULE, 18. .read = snd_pcm_read, 19. .aio_read = snd_pcm_aio_read, 20. .open = snd_pcm_capture_open, 21. .release = snd_pcm_release, 27 22. .llseek = no_llseek, 23. .poll = snd_pcm_capture_poll, 24. .unlocked_ioctl = snd_pcm_capture_ioctl, 25. .compat_ioctl = snd_pcm_ioctl_compat, 26. .mmap = snd_pcm_mmap, 27. .fasync = snd_pcm_fasync, 28. .get_unmapped_area = snd_pcm_get_unmapped_are a, 29. } 30. }; snd_pcm_f_ops作为snd_register_device_for_dev的参数被传入,并被记录在 snd_minors[minor]中的字段f_ops中。最后,在snd_register_device_for_dev 中创建设备节点: [c-sharp] view plain copy 1. snd_minors[minor] = preg; 2. preg->dev = device_create(sound_class, device, MKDEV(majo r, minor), 3. private_data, "%s", name); 4.3 层层深入,从应用程序到驱动层pcm 4.3.1 字符设备注册 在sound/core/sound.c中有alsa_sound_init()函数,定义如下: [c-sharp] view plain copy 1. static int __init alsa_sound_init(void) 2. { 3. snd_major = major; 4. snd_ecards_limit = cards_limit; 5. if (register_chrdev(major, "alsa", &snd_fops)) { 6. snd_printk(KERN_ERR "unable to register native ma jor device number %d/n", major); 7. return -EIO; 8. } 9. if (snd_info_init() < 0) { 10. unregister_chrdev(major, "alsa"); 11. return -ENOMEM; 12. } 13. snd_info_minor_register(); 14. return 0; 28 15. } register_chrdev中的参数major与之前创建pcm设备是device_create时的 major是同一个,这样的结果是,当应用程序open设备文件/dev/snd/pcmCxDxp 时,会进入snd_fops的open回调函数,我们将在下一节中讲述open的过程。 4.3.2 打开pcm设备 从上一节中我们得知,open一个pcm设备时,将会调用snd_fops的open回 调函数,我们先看看snd_fops的定义: [c-sharp] view plain copy 1. static const struct file_operations snd_fops = 2. { 3. .owner = THIS_MODULE, 4. .open = snd_open 5. }; 跟入snd_open函数,它首先从inode中取出此设备号,然后以次设备号为索引, 从snd_minors全局数组中取出当初注册pcm设备时填充的snd_minor结构(参 看4.1节的内容),然后从snd_minor结构中取出pcm设备的f_ops,并且把 file->f_op替换为pcm设备的f_ops,紧接着直接调用pcm设备的f_ops->open(), 然后返回。因为file->f_op已经被替换,以后,应用程序的所有read/write/ioctl 调用都会进入pcm设备自己的回调函数中,也就是4.2节中提到的 snd_pcm_f_ops结构中定义的回调。 [c-sharp] view plain copy 1. static int snd_open(struct inode *inode, struct file *fil e) 2. { 3. unsigned int minor = iminor(inode); 4. struct snd_minor *mptr = NULL; 5. const struct file_operations *old_fops; 6. int err = 0; 7. 8. if (minor >= ARRAY_SIZE(snd_minors)) 9. return -ENODEV; 10. mutex_lock(&sound_mutex); 11. mptr = snd_minors[minor]; 12. if (mptr == NULL) { 29 13. mptr = autoload_device(minor); 14. if (!mptr) { 15. mutex_unlock(&sound_mutex); 16. return -ENODEV; 17. } 18. } 19. old_fops = file->f_op; 20. file->f_op = fops_get(mptr->f_ops); 21. if (file->f_op == NULL) { 22. file->f_op = old_fops; 23. err = -ENODEV; 24. } 25. mutex_unlock(&sound_mutex); 26. if (err < 0) 27. return err; 28. 29. if (file->f_op->open) { 30. err = file->f_op->open(inode, file); 31. if (err) { 32. fops_put(file->f_op); 33. file->f_op = fops_get(old_fops); 34. } 35. } 36. fops_put(old_fops); 37. return err; 38. } 下面的序列图展示了应用程序如何最终调用到snd_pcm_f_ops结构中的回调函 数: 30 图4.3.2.1 应用程序操作pcm设备 4. Linux ALSA声卡驱动之四:Control设备的创建 Control接口 Control接口主要让用户空间的应用程序(alsa-lib)可以访问和控制音频codec 芯片中的多路开关,滑动控件等。对于Mixer(混音)来说,Control接口显得 尤为重要,从ALSA 0.9.x版本开始,所有的mixer工作都是通过control接口的 API来实现的。 ALSA已经为AC97定义了完整的控制接口模型,如果你的Codec芯片只支持 AC97接口,你可以不用关心本节的内容。 的controls,请在代码中包含该头文件。 Controls的定义 要自定义一个Control,我们首先要定义3各回调函数:info,get和put。然后, 定义一个snd_kcontrol_new结构: [c-sharp] view plain copy 31 1. static struct snd_kcontrol_new my_control __devinitdata = { 2. .iface = SNDRV_CTL_ELEM_IFACE_MIXER, 3. .name = "PCM Playback Switch", 4. .index = 0, 5. .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, 6. .private_value = 0xffff, 7. .info = my_control_info, 8. .get = my_control_get, 9. .put = my_control_put 10. }; 11. iface字段指出了control的类型,alsa定义了几种类型 (SNDDRV_CTL_ELEM_IFACE_XXX),常用的类型是MIXER,当然 也可以定义属于全局的CARD类型,也可以定义属于某类设备的类型, 例如HWDEP,PCMRAWMIDI,TIMER等,这时需要在device和 subdevice字段中指出卡的设备逻辑编号。 12. 13. name字段是该control的名字,从ALSA 0.9.x开始,control的名字是变 得比较重要,因为control的作用是按名字来归类的。ALSA已经预定义 了一些control的名字,我们再Control Name一节详细讨论。 14. 15. index字段用于保存该control的在该卡中的编号。如果声卡中有不止一 个codec,每个codec中有相同名字的control,这时我们可以通过index 来区分这些controls。当index为0时,则可以忽略这种区分策略。 16. 17. access字段包含了该control的访问类型。每一个bit代表一种访问类型, 这些访问类型可以多个“或”运算组合在一起。 18. 19. private_value字段包含了一个任意的长整数类型值。该值可以通过info, get,put这几个回调函数访问。你可以自己决定如何使用该字段,例如可 以把它拆分成多个位域,又或者是一个指针,指向某一个数据结构。 20. 21. tlv字段为该control提供元数据。 Control的名字 control的名字需要遵循一些标准,通常可以分成3部分来定义control的名字: 源--方向--功能。 32 源,可以理解为该control的输入端,alsa已经预定义了一些常用的源, 例如:Master,PCM,CD,Line等等。 方向,代表该control的数据流向,例如:Playback,Capture,Bypass, Bypass Capture等等,也可以不定义方向,这时表示该Control是双向的 (playback和capture)。 功能,根据control的功能,可以是以下字符串:Switch,Volume,Route 等等。 也有一些命名上的特例: 全局的capture和playback "Capture Source","Capture Volume", "Capture Switch",它们用于全局的capture source,switch和volume。 同理,"Playback Volume","Playback Switch",它们用于全局的输出 switch和volume。 Tone-controles 音调控制的开关和音量命名为:Tone Control - XXX, 例如,"Tone Control - Switch","Tone Control - Bass","Tone Control - Center"。 3D controls 3D控件的命名规则:,"3D Control - Switch","3D Control - Center","3D Control - Space"。 Mic boost 麦克风音量加强控件命名为:"Mic Boost"或"Mic Boost(6dB)"。 访问标志(ACCESS Flags) Access字段是一个bitmask,它保存了改control的访问类型。默认的访问类型 是:SNDDRV_CTL_ELEM_ACCESS_READWRITE,表明该control支持读和 写操作。如果access字段没有定义(.access==0),此时也认为是READWRITE 类型。 如果是一个只读control,access应该设置为: SNDDRV_CTL_ELEM_ACCESS_READ,这时,我们不必定义put回调函数。 类似地,如果是只写control,access应该设置为: SNDDRV_CTL_ELEM_ACCESS_WRITE,这时,我们不必定义get回调函数。 33 如果control的值会频繁地改变(例如:电平表),我们可以使用VOLATILE类 型,这意味着该control会在没有通知的情况下改变,应用程序应该定时地查询 该control的值。 回调函数 info回调函数 info回调函数用于获取control的详细信息。它的主要工作就是填充通过参数传 入的snd_ctl_elem_info对象,以下例子是一个具有单个元素的boolean型 control的info回调: info回调函数用于获取control的详细信息。它的主要工作就是填充通过参数传 入的snd_ctl_elem_info对象,以下例子是一个具有单个元素的boolean型 control的info回调: [c-sharp] view plain copy 1. static int snd_myctl_mono_info(struct snd_kcontrol *kcont rol, 2. struct snd_ctl_elem_info *uinfo) 3. { 4. uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN; 5. uinfo->count = 1; 6. uinfo-> = 0; 7. uinfo-> = 1; 8. return 0; 9. } type字段指出该control的值类型,值类型可以是BOOLEAN, INTEGER, ENUMERATED, BYTES,IEC958和INTEGER64之一。count字段指出了改 control中包含有多少个元素单元,比如,立体声的音量control左右两个声道的 音量值,它的count字段等于2。value字段是一个联合体(union),value的 内容和control的类型有关。其中,boolean和integer类型是相同的。 ENUMERATED类型有些特殊。它的value需要设定一个字符串和字符串的索 引,请看以下例子: 34 [c-sharp] view plain copy 1. static int snd_myctl_enum_info(struct snd_kcontrol *kcont rol, 2. struct snd_ctl_elem_info *uinfo) 3. { 4. static char *texts[4] = { 5. "First", "Second", "Third", "Fourth" 6. }; 7. uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED; 8. uinfo->count = 1; 9. uinfo-> = 4; 10. if (uinfo-> > 3) 11. uinfo-> = 3; 12. strcpy(uinfo->, 13. texts[uinfo->]); 14. return 0; 15. } alsa已经为我们实现了一些通用的info回调函数,例如: snd_ctl_boolean_mono_info(),snd_ctl_boolean_stereo_info()等等。 get回调函数 该回调函数用于读取control的当前值,并返回给用户空间的应用程序。 [c-sharp] view plain copy 1. static int snd_myctl_get(struct snd_kcontrol *kcontrol, 2. struct snd_ctl_elem_value *ucontrol) 3. { 4. struct mychip *chip = snd_kcontrol_chip(kcontrol); 5. ucontrol->[0] = get_some_value(chi p); 6. return 0; 7. } value字段的赋值依赖于control的类型(如同info回调)。很多声卡的驱动利 用它存储硬件寄存器的地址、bit-shift和bit-mask,这时,private_value字段可 以按以下例子进行设置: 35 .private_value = reg | (shift << 16) | (mask << 24); 然后,get回调函数可以这样实现: static int snd_sbmixer_get_single(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { int reg = kcontrol->private_value & 0xff; int shift = (kcontrol->private_value >> 16) & 0xff; int mask = (kcontrol->private_value >> 24) & 0xff; .... //根据以上的值读取相应寄存器的值并填入value中 } 如果control的count字段大于1,表示control有多个元素单元,get回调函数 也应该为value填充多个数值。 put回调函数 put回调函数用于把应用程序的控制值设置到control中。 [c-sharp] view plain copy 1. static int snd_myctl_put(struct snd_kcontrol *kcontrol, 2. struct snd_ctl_elem_value *ucontrol) 3. { 4. struct mychip *chip = snd_kcontrol_chip(kcontrol); 5. int changed = 0; 6. if (chip->current_value != 7. ucontrol->[0]) { 8. change_current_value(chip, 9. ucontrol->[0]); 10. changed = 1; 11. } 12. return changed; 13. } 36 如上述例子所示,当control的值被改变时,put回调必须要返回1,如果值没有 被改变,则返回0。如果发生了错误,则返回一个负数的错误号。 和get回调一样,当control的count大于1时,put回调也要处理多个control 中的元素值。 创建Controls 当把以上讨论的内容都准备好了以后,我们就可以创建我们自己的control了。 alsa-driver为我们提供了两个用于创建control的API: snd_ctl_new1() snd_ctl_add() 我们可以用以下最简单的方式创建control: [c-sharp] view plain copy 1. err = snd_ctl_add(card, snd_ctl_new1(&my_control, chip)); 2. if (err < 0) 3. return err; 在这里,my_control是一个之前定义好的snd_kcontrol_new对象,chip对象将 会被赋值在kcontrol->private_data字段,该字段可以在回调函数中访问。 snd_ctl_new1()会分配一个新的snd_kcontrol实例,并把my_control中相应的 值复制到该实例中,所以,在定义my_control时,通常我们可以加上__devinitdata 前缀。snd_ctl_add则把该control绑定到声卡对象card当中。 37 元数据(Metadata) 很多mixer control需要提供以dB为单位的信息,我们可以使用 DECLARE_TLV_xxx宏来定义一些包含这种信息的变量,然后把control的tlv.p 字段指向这些变量,最后,在access字段中加上 SNDRV_CTL_ELEM_ACCESS_TLV_READ标志,就像这样: static DECLARE_TLV_DB_SCALE(db_scale_my_control, -4050, 150, 0); static struct snd_kcontrol_new my_control __devinitdata = { ... .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | SNDRV_CTL_ELEM_ACCESS_TLV_READ, ... .tlv.p = db_scale_my_control, }; DECLARE_TLV_DB_SCALE宏定义的mixer control,它所代表的值按一个固 定的dB值的步长变化。该宏的第一个参数是要定义变量的名字,第二个参数是 最小值,以0.01dB为单位。第三个参数是变化的步长,也是以0.01dB为单位。 如果该control处于最小值时会做出mute时,需要把第四个参数设为1。 DECLARE_TLV_DB_LINEAR宏定义的mixer control,它的输出随值的变化而 线性变化。 该宏的第一个参数是要定义变量的名字,第二个参数是最小值,以 0.01dB为单位。第二个参数是最大值,以0.01dB为单位。如果该control处于 最小值时会做出mute时,需要把第二个参数设为TLV_DB_GAIN_MUTE。 这两个宏实际上就是定义一个整形数组,所谓tlv,就是Type-Lenght-Value的 意思,数组的第0各元素代表数据的类型,第1个元素代表数据的长度,第三 个元素和之后的元素保存该变量的数据。 38 Control设备的建立 Control设备和PCM设备一样,都属于声卡下的逻辑设备。用户空间的应用程 序通过alsa-lib访问该Control设备,读取或控制control的控制状态,从而达到 控制音频Codec进行各种Mixer等控制操作。 Control设备的创建过程大体上和PCM设备的创建过程相同。详细的创建过程 可以参考本博的另一篇文章:Linux音频驱动之三:PCM设备的创建。下面我 们只讨论有区别的地方。 我们需要在我们的驱动程序初始化时主动调用snd_pcm_new()函数创建pcm设 备,而control设备则在snd_card_create()内被创建,snd_card_create()通过调 用snd_ctl_create()函数创建control设备节点。所以我们无需显式地创建control 设备,只要建立声卡,control设备被自动地创建。 和pcm设备一样,control设备的名字遵循一定的规则:controlCxx,这里的xx 代表声卡的编号。我们也可以通过代码正是这一点,下面的是 snd_ctl_dev_register()函数的代码: [c-sharp] view plain copy 1. /* 2. * registration of the control device 3. */ 4. static int snd_ctl_dev_register(struct snd_device *device ) 5. { 6. struct snd_card *card = device->device_data; 7. int err, cardnum; 8. char name[16]; 9. 10. if (snd_BUG_ON(!card)) 11. return -ENXIO; 12. cardnum = card->number; 13. if (snd_BUG_ON(cardnum < 0 || cardnum >= SNDRV_CARDS )) 14. return -ENXIO; 15. /* control设备的名字 */ 16. sprintf(name, "controlC%i", cardnum); 17. if ((err = snd_register_device(SNDRV_DEVICE_TYPE_CON TROL, card, -1, 39 18. &snd_ctl_f_ops, card, name)) < 0) 19. return err; 20. return 0; 21. } snd_ctl_dev_register()函数会在snd_card_register()中,即声卡的注册阶段被调 用。注册完成后,control设备的相关信息被保存在snd_minors[]数组中,用control 设备的此设备号作索引,即可在snd_minors[]数组中找出相关的信息。注册完成 后的数据结构关系可以用下图进行表述: control设备的操作函数入口 用户程序需要打开control设备时,驱动程序通过snd_minors[]全局数组和此设 备号,可以获得snd_ctl_f_ops结构中的各个回调函数,然后通过这些回调函数 访问control中的信息和数据(最终会调用control的几个回调函数get,put,info)。 详细的代码我就不贴了,大家可以读一下代码:/sound/core/control.c。 5. Linux ALSA声卡驱动之五:移动设备中的ALSA(ASoC) 40 1. ASoC的由来 ASoC--ALSA System on Chip ,是建立在标准ALSA驱动层上,为了更好地支 持嵌入式处理器和移动设备中的音频Codec的一套软件体系。在ASoc出现之 前,内核对于SoC中的音频已经有部分的支持,不过会有一些局限性: Codec驱动与SoC CPU的底层耦合过于紧密,这种不理想会导致代码 的重复,例如,仅是wm8731的驱动,当时Linux中有分别针对4个平 台的驱动代码。 音频事件没有标准的方法来通知用户,例如耳机、麦克风的插拔和检测, 这些事件在移动设备中是非常普通的,而且通常都需要特定于机器的代码 进行重新对音频路劲进行配置。 当进行播放或录音时,驱动会让整个codec处于上电状态,这对于PC 没问题,但对于移动设备来说,这意味着浪费大量的电量。同时也不支持 通过改变过取样频率和偏置电流来达到省电的目的。 ASoC正是为了解决上述种种问题而提出的,目前已经被整合至内核的代码树 中:sound/soc。ASoC不能单独存在,他只是建立在标准ALSA驱动上的一个, 它必须和标准的ALSA驱动框架相结合才能工作。 2. 硬件架构 通常,就像软件领域里的抽象和重用一样,嵌入式设备的音频系统可以被划分为 板载硬件(Machine)、Soc(Platform)、Codec三大部分,如下图所示: 图2.1 音频系统结构 41 Machine 是指某一款机器,可以是某款设备,某款开发板,又或者是某 款智能手机,由此可以看出Machine几乎是不可重用的,每个Machine 上的硬件实现可能都不一样,CPU不一样,Codec不一样,音频的输入、 输出设备也不一样,Machine为CPU、Codec、输入输出设备提供了一 个载体。 Platform 一般是指某一个SoC平台,比如pxaxxx,s3cxxxx,omapxxx 等等,与音频相关的通常包含该SoC中的时钟、DMA、I2S、PCM等等, 只要指定了SoC,那么我们可以认为它会有一个对应的Platform,它只 与SoC相关,与Machine无关,这样我们就可以把Platform抽象出来, 使得同一款SoC不用做任何的改动,就可以用在不同的Machine中。实 际上,把Platform认为是某个SoC更好理解。 Codec 字面上的意思就是编解码器,Codec里面包含了I2S接口、D/A、 A/D、Mixer、PA(功放),通常包含多种输入(Mic、Line-in、I2S、PCM) 和多个输出(耳机、喇叭、听筒,Line-out),Codec和Platform一样, 是可重用的部件,同一个Codec可以被不同的Machine使用。嵌入式 Codec通常通过I2C对内部的寄存器进行控制。 3. 软件架构 在软件层面,ASoC也把嵌入式设备的音频系统同样分为3大部分,Machine, Platform和Codec。 Codec驱动 ASoC中的一个重要设计原则就是要求Codec驱动是平台 无关的,它包含了一些音频的控件(Controls),音频接口,DAMP(动 态音频电源管理)的定义和某些Codec IO功能。为了保证硬件无关性, 任何特定于平台和机器的代码都要移到Platform和Machine驱动中。所 有的Codec驱动都要提供以下特性: o o o o Codec DAI 和 PCM的配置信息; Codec的IO控制方式(I2C,SPI等); Mixer和其他的音频控件; Codec的ALSA音频操作接口; 必要时,也可以提供以下功能: 42 o o o DAPM描述信息; DAPM事件处理程序; DAC数字静音控制 Platform驱动 它包含了该SoC平台的音频DMA和音频接口的配置和 控制(I2S,PCM,AC97等等);它也不能包含任何与板子或机器相关 的代码。 Machine驱动 Machine驱动负责处理机器特有的一些控件和音频事件 (例如,当播放音频时,需要先行打开一个放大器);单独的Platform 和Codec驱动是不能工作的,它必须由Machine驱动把它们结合在一起 才能完成整个设备的音频处理工作。 4. 数据结构 整个ASoC是由一些列数据结构组成,要搞清楚ASoC的工作机理,必须要理 解这一系列数据结构之间的关系和作用,下面的关系图展示了ASoC中重要的 数据结构之间的关联方式: 43 图4.1 Kernel-2.6.35-ASoC中各个结构的静态关系 ASoC把声卡实现为一个Platform Device,然后利用Platform_device结构中 的dev字段:a,它实际上指向一个snd_soc_device结构。可以认为 snd_soc_device是整个ASoC数据结构的根本,由他开始,引出一系列的数据 结构用于表述音频的各种特性和功能。snd_soc_device结构引出了 snd_soc_card和soc_codec_device两个结构,然后snd_soc_card又引出了 snd_soc_platform、snd_soc_dai_link和snd_soc_codec结构。如上所述,ASoC 被划分为Machine、Platform和Codec三大部分,如果从这些数据结构看来, snd_codec_device和snd_soc_card代表着Machine驱动,snd_soc_platform 则代表着Platform驱动,snd_soc_codec和soc_codec_device则代表了Codec 驱动,而snd_soc_dai_link则负责连接Platform和Codec。 44 5. 3.0版内核对ASoC的改进 本来写这篇文章的时候参考的内核版本是2.6.35,不过有CSDN的朋友提出在 内核版本3.0版本中,ASoC做了较大的变化。故特意下载了3.0的代码,发现 确实有所变化,下面先贴出数据结构的静态关系图: 图5.1 Kernel 3.0中的ASoC数据结构 由上图我们可以看出,3.0中的数据结构更为合理和清晰,取消了 snd_soc_device结构,直接用snd_soc_card取代了它,并且强化了 snd_soc_pcm_runtime的作用,同时还增加了另外两个数据结构 snd_soc_codec_driver和snd_soc_platform_driver,用于明确代表Codec驱动 和Platform驱动。 45 后续的章节中将会逐一介绍Machine和Platform以及Codec驱动的工作细节和 关联。 1. Linux ALSA声卡驱动之六:ASoC架构中的Machine 前面一节的内容我们提到,ASoC被分为Machine、Platform和Codec三大部 分,其中的Machine驱动负责Platform和Codec之间的耦合以及部分和设备或 板子特定的代码,再次引用上一节的内容:Machine驱动负责处理机器特有的一 些控件和音频事件(例如,当播放音频时,需要先行打开一个放大器);单独的 Platform和Codec驱动是不能工作的,它必须由Machine驱动把它们结合在一 起才能完成整个设备的音频处理工作。 ASoC的一切都从Machine驱动开始,包括声卡的注册,绑定Platform和Codec 驱动等等,下面就让我们从Machine驱动开始讨论吧。 1. 注册Platform Device ASoC把声卡注册为Platform Device,我们以装配有WM8994的一款Samsung 的开发板SMDK为例子做说明,WM8994是一颗Wolfson生产的多功能Codec 芯片。 代码的位于:/sound/soc/samsung/smdk_wm8994.c,我们关注模块的初始化函 数: [cpp] view plain copy 1. static int __init smdk_audio_init(void) 2. { 3. int ret; 4. 5. smdk_snd_device = platform_device_alloc("soc-audio", -1); 6. if (!smdk_snd_device) 7. return -ENOMEM; 8. 9. platform_set_drvdata(smdk_snd_device, &smdk); 10. 11. ret = platform_device_add(smdk_snd_device); 12. if (ret) 46 13. platform_device_put(smdk_snd_device); 14. 15. return ret; 16. } 由此可见,模块初始化时,注册了一个名为soc-audio的Platform设备,同时把 smdk设到platform_device结构的a字段中,这里引出了第一个数据 结构snd_soc_card的实例smdk,他的定义如下: [cpp] view plain copy 1. static struct snd_soc_dai_link smdk_dai[] = { 2. { /* Primary DAI i/f */ 3. .name = "WM8994 AIF1", 4. .stream_name = "Pri_Dai", 5. .cpu_dai_name = "samsung-i2s.0", 6. .codec_dai_name = "wm8994-aif1", 7. .platform_name = "samsung-audio", 8. .codec_name = "wm8994-codec", 9. .init = smdk_wm8994_init_paiftx, 10. .ops = &smdk_ops, 11. }, { /* Sec_Fifo Playback i/f */ 12. .name = "Sec_FIFO TX", 13. .stream_name = "Sec_Dai", 14. .cpu_dai_name = "samsung-i2s.4", 15. .codec_dai_name = "wm8994-aif1", 16. .platform_name = "samsung-audio", 17. .codec_name = "wm8994-codec", 18. .ops = &smdk_ops, 19. }, 20. }; 21. 22. static struct snd_soc_card smdk = { 23. .name = "SMDK-I2S", 24. .owner = THIS_MODULE, 25. .dai_link = smdk_dai, 26. .num_links = ARRAY_SIZE(smdk_dai), 27. }; 通过snd_soc_card结构,又引出了Machine驱动的另外两个个数据结构: snd_soc_dai_link(实例:smdk_dai[] ) snd_soc_ops(实例:smdk_ops ) 47 其中,snd_soc_dai_link中,指定了Platform、Codec、codec_dai、cpu_dai 的名字,稍后Machine驱动将会利用这些名字去匹配已经在系统中注册的 platform,codec,dai,这些注册的部件都是在另外相应的Platform驱动和Codec 驱动的代码文件中定义的,这样看来,Machine驱动的设备初始化代码无非就是 选择合适Platform和Codec以及dai,用他们填充以上几个数据结构,然后注 册Platform设备即可。当然还要实现连接Platform和Codec的dai_link对应的 ops实现,本例就是smdk_ops,它只实现了hw_params函数: smdk_hw_params。 2. 注册Platform Driver 按照Linux的设备模型,有platform_device,就一定会有platform_driver。ASoC 的platform_driver在以下文件中定义:sound/soc/soc-core.c。 还是先从模块的入口看起: [cpp] view plain copy 1. static int __init snd_soc_init(void) 2. { 3. ...... 4. return platform_driver_register(&soc_driver); 5. } soc_driver的定义如下: [cpp] view plain copy 1. /* ASoC platform driver */ 2. static struct platform_driver soc_driver = { 3. .driver = { 4. .name = "soc-audio", 5. .owner = THIS_MODULE, 6. .pm = &soc_pm_ops, 7. }, 8. .probe = soc_probe, 9. .remove = soc_remove, 10. }; 我们看到platform_driver的name字段为soc-audio,正好与platform_device 中的名字相同,按照Linux的设备模型,platform总线会匹配这两个名字相同的 device和driver,同时会触发soc_probe的调用,它正是整个ASoC驱动初始 化的入口。 48 3. 初始化入口soc_probe() soc_probe函数本身很简单,它先从platform_device参数中取出snd_soc_card, 然后调用snd_soc_register_card,通过snd_soc_register_card,为 snd_soc_pcm_runtime数组申请内存,每一个dai_link对应 snd_soc_pcm_runtime数组的一个单元,然后把snd_soc_card中的dai_link配 置复制到相应的snd_soc_pcm_runtime中,最后,大部分的工作都在 snd_soc_instantiate_card中实现,下面就看看snd_soc_instantiate_card做了 些什么: 该函数首先利用card->instantiated来判断该卡是否已经实例化,如果已经实例 化则直接返回,否则遍历每一对dai_link,进行codec、platform、dai的绑定工 作,下只是代码的部分选节,详细的代码请直接参考完整的代码树。 [cpp] view plain copy 1. /* bind DAIs */ 2. for (i = 0; i < card->num_links; i++) 3. soc_bind_dai_link(card, i); ASoC定义了三个全局的链表头变量:codec_list、dai_list、platform_list,系统 中所有的Codec、DAI、Platform都在注册时连接到这三个全局链表上。 soc_bind_dai_link函数逐个扫描这三个链表,根据card->dai_link[]中的名称进 行匹配,匹配后把相应的codec,dai和platform实例赋值到card->rtd[]中 (snd_soc_pcm_runtime)。经过这个过程后,snd_soc_pcm_runtime: (card->rtd)中保存了本Machine中使用的Codec,DAI和Platform驱动的信 息。 snd_soc_instantiate_card接着初始化Codec的寄存器缓存,然后调用标准的 alsa函数创建声卡实例: [cpp] view plain copy 1. /* card bind complete so register a sound card */ 2. ret = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_S TR1, 3. card->owner, 0, &card->snd_card); 4. card->snd_card->dev = card->dev; 5. 6. card->_level = SND_SOC_BIAS_OFF; 7. card-> = card->dev; 8. card-> = card; 9. list_add(&card->, &card->dapm_list); 49 然后,依次调用各个子结构的probe函数: [cpp] view plain copy 1. 2. 3. 4. 5. 6. 7. 8. 9. /* initialise the sound card only once */ if (card->probe) { ret = card->probe(card); if (ret < 0) goto card_probe_error; } /* early DAI link probe */ for (order = SND_SOC_COMP_ORDER_FIRST; order <= SND_SOC_COMP_ORDE R_LAST; 10. order++) { 11. for (i = 0; i < card->num_links; i++) { 12. ret = soc_probe_dai_link(card, i, order); 13. if (ret < 0) { 14. pr_err("asoc: failed to instantiate card %s: %dn", 15. card->name, ret); 16. goto probe_dai_err; 17. } 18. } 19. } 20. 21. for (i = 0; i < card->num_aux_devs; i++) { 22. ret = soc_probe_aux_dev(card, i); 23. if (ret < 0) { 24. pr_err("asoc: failed to add auxiliary devices %s: %dn", 25. card->name, ret); 26. goto probe_aux_dev_err; 27. } 28. } 在上面的soc_probe_dai_link()函数中做了比较多的事情,把他展开继续讨论: 1. static int soc_probe_dai_link(struct snd_soc_card *card, int num, int order) 2. { 3. ...... 4. /* set default power off timeout */ 5. rtd->pmdown_time = pmdown_time; 6. 50 7. /* probe the cpu_dai */ 8. if (!cpu_dai->probed && 9. cpu_dai->driver->probe_order == order) { 10. 11. if (cpu_dai->driver->probe) { 12. ret = cpu_dai->driver->probe(cpu_dai); 13. } 14. cpu_dai->probed = 1; 15. /* mark cpu_dai as probed and add to card dai list */ 16. list_add(&cpu_dai->card_list, &card->dai_dev_list); 17. } 18. 19. /* probe the CODEC */ 20. if (!codec->probed && 21. codec->driver->probe_order == order) { 22. ret = soc_probe_codec(card, codec); 23. } 24. 25. /* probe the platform */ 26. if (!platform->probed && 27. platform->driver->probe_order == order) { 28. ret = soc_probe_platform(card, platform); 29. } 30. 31. /* probe the CODEC DAI */ 32. if (!codec_dai->probed && codec_dai->driver->probe_order == o rder) { 33. if (codec_dai->driver->probe) { 34. ret = codec_dai->driver->probe(codec_dai); 35. } 36. 37. /* mark codec_dai as probed and add to card dai list */ 38. codec_dai->probed = 1; 39. list_add(&codec_dai->card_list, &card->dai_dev_list); 40. } 41. 42. /* complete DAI probe during last probe */ 43. if (order != SND_SOC_COMP_ORDER_LAST) 44. return 0; 45. 46. ret = soc_post_component_init(card, codec, num, 0); 47. if (ret) 48. return ret; 49. ...... 51 50. /* create the pcm */ 51. ret = soc_new_pcm(rtd, num); 52. ........ 53. return 0; 54. } 该函数出了挨个调用了codec,dai和platform驱动的probe函数外,在最后还 调用了soc_new_pcm()函数用于创建标准alsa驱动的pcm逻辑设备。现在把该 函数的部分代码也贴出来: 1. /* create a new pcm */ 2. int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num) 3. { 4. ...... 5. struct snd_pcm_ops *soc_pcm_ops = &rtd->ops; 6. 7. soc_pcm_ops->open = soc_pcm_open; 8. soc_pcm_ops->close = soc_pcm_close; 9. soc_pcm_ops->hw_params = soc_pcm_hw_params; 10. soc_pcm_ops->hw_free = soc_pcm_hw_free; 11. soc_pcm_ops->prepare = soc_pcm_prepare; 12. soc_pcm_ops->trigger = soc_pcm_trigger; 13. soc_pcm_ops->pointer = soc_pcm_pointer; 14. 15. ret = snd_pcm_new(rtd->card->snd_card, new_name, 16. num, playback, capture, &pcm); 17. 18. /* DAPM dai link stream work */ 19. INIT_DELAYED_WORK(&rtd->delayed_work, close_delayed_ work); 20. 21. rtd->pcm = pcm; 22. pcm->private_data = rtd; 23. if (platform->driver->ops) { 24. soc_pcm_ops->mmap = platform->driver->ops->mmap; 25. soc_pcm_ops->pointer = platform->driver->ops->po inter; 26. soc_pcm_ops->ioctl = platform->driver->ops->ioct l; 27. soc_pcm_ops->copy = platform->driver->ops->copy; 52 28. soc_pcm_ops->silence = platform->driver->ops->si lence; 29. soc_pcm_ops->ack = platform->driver->ops->ack; 30. soc_pcm_ops->page = platform->driver->ops->page; 31. } 32. 33. if (playback) 34. snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, soc_pcm_ops); 35. 36. if (capture) 37. snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, s oc_pcm_ops); 38. 39. if (platform->driver->pcm_new) { 40. ret = platform->driver->pcm_new(rtd); 41. if (ret < 0) { 42. pr_err("asoc: platform pcm constructor faile dn"); 43. return ret; 44. } 45. } 46. 47. pcm->private_free = platform->driver->pcm_free; 48. return ret; 49. } 该函数首先初始化snd_soc_runtime中的snd_pcm_ops字段,也就是rtd->ops 中的部分成员,例如open,close,hw_params等,紧接着调用标准alsa驱动 中的创建pcm的函数snd_pcm_new()创建声卡的pcm实例,pcm的 private_data字段设置为该runtime变量rtd,然后用platform驱动中的 snd_pcm_ops替换部分pcm中的snd_pcm_ops字段,最后,调用platform驱 动的pcm_new回调,该回调实现该platform下的dma内存申请和dma初始化 等相关工作。到这里,声卡和他的pcm实例创建完成。 回到snd_soc_instantiate_card函数,完成snd_card和snd_pcm的创建后,接 着对dapm和dai支持的格式做出一些初始化合设置工作后,调用 了 card->late_probe(card)进行一些最后的初始化合设置工作,最后则是调用标 准alsa驱动的声卡注册函数对声卡进行注册: 1. if (card->late_probe) { 2. ret = card->late_probe(card); 3. if (ret < 0) { 53 4. dev_err(card->dev, "%s late_probe() failed: %dn" , 5. card->name, ret); 6. goto probe_aux_dev_err; 7. } 8. } 9. 10. snd_soc_dapm_new_widgets(&card->dapm); 11. 12. if (card->fully_routed) 13. list_for_each_entry(codec, &card->codec_dev_list, ca rd_list) 14. snd_soc_dapm_auto_nc_codec_pins(codec); 15. 16. ret = snd_card_register(card->snd_card); 17. if (ret < 0) { 18. printk(KERN_ERR "asoc: failed to register soundcard for %sn", card->name); 19. goto probe_aux_dev_err; 20. } 至此,整个Machine驱动的初始化已经完成,通过各个子结构的probe调用, 实际上,也完成了部分Platfrom驱动和Codec驱动的初始化工作,整个过程可 以用一下的序列图表示: 54 图3.1 基于3.0内核 soc_probe序列图 下面的序列图是本文章第一个版本,基于内核2.6.35,大家也可以参考一下两个 版本的差异: 55 图3.2 基于2.6.35 soc_probe序 列图 7. Linux ALSA声卡驱动之七:ASoC架构中的Codec 1. Codec简介 在移动设备中,Codec的作用可以归结为4种,分别是: 对PCM等信号进行D/A转换,把数字的音频信号转换为模拟信号 对Mic、Linein或者其他输入源的模拟信号进行A/D转换,把模拟的声音 信号转变CPU能够处理的数字信号 对音频通路进行控制,比如播放音乐,收听调频收音机,又或者接听电话 时,音频信号在codec内的流通路线是不一样的 对音频信号做出相应的处理,例如音量控制,功率放大,EQ控制等等 ASoC对Codec的这些功能都定义好了一些列相应的接口,以方便地对Codec 进行控制。ASoC对Codec驱动的一个基本要求是:驱动程序的代码必须要做 到平台无关性,以方便同一个Codec的代码不经修改即可用在不同的平台上。 以下的讨论基于wolfson的Codec芯片WM8994,kernel的版本3.3.x。 2. ASoC中对Codec的数据抽象 描述Codec的最主要的几个数据结构分别是:snd_soc_codec, snd_soc_codec_driver,snd_soc_dai,snd_soc_dai_driver,其中的snd_soc_dai 56 和snd_soc_dai_driver在ASoC的Platform驱动中也会使用到,Platform和 Codec的DAI通过snd_soc_dai_link结构,在Machine驱动中进行绑定连接。 下面我们先看看这几个结构的定义,这里我只贴出我要关注的字段,详细的定义 请参照:/include/sound/soc.h。 snd_soc_codec: 1. /* SoC Audio Codec device */ 2. struct snd_soc_codec { 3. const char *name; /* Codec的名字*/ 4. struct device *dev; /* 指向Codec设备的指针 */ 5. const struct snd_soc_codec_driver *driver; /* 指向该codec的驱 动的指针 */ 6. struct snd_soc_card *card; /* 指向Machine驱动的card实 例 */ 7. int num_dai; /* 该Codec数字接口的个数,目前越来越多的Codec带有多 个I2S或者是PCM接口 */ 8. int (*volatile_register)(...); /* 用于判定某一寄存器是否是 volatile */ 9. int (*readable_register)(...); /* 用于判定某一寄存器是否可 读 */ 10. int (*writable_register)(...); /* 用于判定某一寄存器是否可 写 */ 11. 12. /* runtime */ 13. ...... 14. /* codec IO */ 15. void *control_data; /* 该指针指向的结构用于对codec的控制,通常和 read,write字段联合使用 */ 16. enum snd_soc_control_type control_type;/* 可以是SND_SOC_SPI, SND_SOC_I2C,SND_SOC_REGMAP中的一种 */ 17. unsigned int (*read)(struct snd_soc_codec *, unsigned int); /* 读取Codec寄存器的函数 */ 18. int (*write)(struct snd_soc_codec *, unsigned int, unsigned i nt); /* 写入Codec寄存器的函数 */ 19. /* dapm */ 20. struct snd_soc_dapm_context dapm; /* 用于DAPM控件 */ 21. }; snd_soc_codec_driver: 1. /* codec driver */ 2. struct snd_soc_codec_driver { 3. /* driver ops */ 57 4. int (*probe)(struct snd_soc_codec *); /* codec驱动的probe函 数,由snd_soc_instantiate_card回调 */ 5. int (*remove)(struct snd_soc_codec *); 6. 7. 8. 9. int (*suspend)(struct snd_soc_codec *); /* 电源管理 */ int (*resume)(struct snd_soc_codec *); /* 电源管理 */ /* Default control and setup, added after probe() is run */ 10. const struct snd_kcontrol_new *controls; /* 音频控件指针 */ 11. const struct snd_soc_dapm_widget *dapm_widgets; /* dapm部件指 针 */ 12. const struct snd_soc_dapm_route *dapm_routes; /* dapm路由指 针 */ 13. 14. /* codec wide operations */ 15. int (*set_sysclk)(...); /* 时钟配置函数 */ 16. int (*set_pll)(...); /* 锁相环配置函数 */ 17. 18. /* codec IO */ 19. unsigned int (*read)(...); /* 读取codec寄存器函数 */ 20. int (*write)(...); /* 写入codec寄存器函数 */ 21. int (*volatile_register)(...); /* 用于判定某一寄存器是否是 volatile */ 22. int (*readable_register)(...); /* 用于判定某一寄存器是否可 读 */ 23. int (*writable_register)(...); /* 用于判定某一寄存器是否可 写 */ 24. 25. /* codec bias level */ 26. int (*set_bias_level)(...); /* 偏置电压配置函数 */ 27. 28. }; snd_soc_dai: 1. 2. 3. 4. 5. 6. /* * Digital Audio Interface runtime data. * * Holds runtime data for a DAI. */ struct snd_soc_dai { 7. const char *name; /* dai的名字 */ 8. struct device *dev; /* 设备指针 */ 58 9. 10. /* driver ops */ 11. struct snd_soc_dai_driver *driver; /* 指向dai驱动结构的指 针 */ 12. 13. /* DAI runtime info */ 14. unsigned int capture_active:1; /* stream is in use */ 15. unsigned int playback_active:1; /* stream is in use */ 16. 17. /* DAI DMA data */ 18. void *playback_dma_data; /* 用于管理playback dma */ 19. void *capture_dma_data; /* 用于管理capture dma */ 20. 21. /* parent platform/codec */ 22. union { 23. struct snd_soc_platform *platform; /* 如果是cpu dai,指向 所绑定的平台 */ 24. struct snd_soc_codec *codec; /* 如果是codec dai指向所绑定 的codec */ 25. }; 26. struct snd_soc_card *card; /* 指向Machine驱动中的crad实 例 */ 27. }; snd_soc_dai_driver: 1. /* 2. * Digital Audio Interface Driver. 3. * 4. * Describes the Digital Audio Interface in terms of its ALSA, DAI and AC97 5. * operations and capabilities. Codec and platform driver s will register this 6. * structure for every DAI they have. 7. * 8. * This structure covers the clocking, formating and ALSA operations for each 9. * interface. 10. */ 11. struct snd_soc_dai_driver { 12. /* DAI description */ 13. const char *name; /* dai驱动名字 */ 59 14. 15. /* DAI driver callbacks */ 16. int (*probe)(struct snd_soc_dai *dai); /* dai驱动的 probe函数,由snd_soc_instantiate_card回调 */ 17. int (*remove)(struct snd_soc_dai *dai); 18. int (*suspend)(struct snd_soc_dai *dai); /* 电源管 理 */ 19. int (*resume)(struct snd_soc_dai *dai); 20. 21. /* ops */ 22. const struct snd_soc_dai_ops *ops; /* 指向本dai的 snd_soc_dai_ops结构 */ 23. 24. /* DAI capabilities */ 25. struct snd_soc_pcm_stream capture; /* 描述capture的 能力 */ 26. struct snd_soc_pcm_stream playback; /* 描述playback 的能力 */ 27. }; snd_soc_dai_ops用于实现该dai的控制盒参数配置: 1. struct snd_soc_dai_ops { 2. /* 3. * DAI clocking configuration, all optional. 4. * Called by soc_card drivers, normally in their hw_p arams. 5. */ 6. int (*set_sysclk)(...); 7. int (*set_pll)(...); 8. int (*set_clkdiv)(...); 9. /* 10. * DAI format configuration 11. * Called by soc_card drivers, normally in their hw_ params. 12. */ 13. int (*set_fmt)(...); 14. int (*set_tdm_slot)(...); 15. int (*set_channel_map)(...); 16. int (*set_tristate)(...); 17. /* 18. * DAI digital mute - optional. 60 19. * Called by soc-core to minimise any pops. 20. */ 21. int (*digital_mute)(...); 22. /* 23. * ALSA PCM audio operations - all optional. 24. * Called by soc-core during audio PCM operations. 25. */ 26. int (*startup)(...); 27. void (*shutdown)(...); 28. int (*hw_params)(...); 29. int (*hw_free)(...); 30. int (*prepare)(...); 31. int (*trigger)(...); 32. /* 33. * For hardware based FIFO caused delay reporting. 34. * Optional. 35. */ 36. snd_pcm_sframes_t (*delay)(...); 37. }; 3. Codec的注册 因为Codec驱动的代码要做到平台无关性,要使得Machine驱动能够使用该 Codec,Codec驱动的首要任务就是确定snd_soc_codec和snd_soc_dai的实 例,并把它们注册到系统中,注册后的codec和dai才能为Machine驱动所用。 以WM8994为例,对应的代码位置:/sound/soc/codecs/wm8994.c,模块的入 口函数注册了一个platform driver: 1. static struct platform_driver wm8994_codec_driver = { 2. .driver = { 3. .name = "wm8994-codec", 4. .owner = THIS_MODULE, 5. }, 6. .probe = wm8994_probe, 7. .remove = __devexit_p(wm8994_remove), 8. }; 9. 10. module_platform_driver(wm8994_codec_driver); 有platform driver,必定会有相应的platform device,这个platform device 的来源后面再说,显然,platform driver注册后,probe回调将会被调用,这 里是wm8994_probe函数: 61 1. static int __devinit wm8994_probe(struct platform_device *pdev) 2. { 3. return snd_soc_register_codec(&pdev->dev, &soc_codec_ dev_wm8994, 4. wm8994_dai, ARRAY_SIZE(wm8994_dai)); 5. } 其中,soc_codec_dev_wm8994和wm8994_dai的定义如下(代码中定义了3 个dai,这里只列出第一个): [html] view plain copy 1. static struct snd_soc_codec_driver soc_codec_dev_wm8994 = { 2. .probe = wm8994_codec_probe, 3. .remove = wm8994_codec_remove, 4. .suspend = wm8994_suspend, 5. .resume = wm8994_resume, 6. .set_bias_level = wm8994_set_bias_level, 7. .reg_cache_size = WM8994_MAX_REGISTER, 8. .volatile_register = wm8994_soc_volatile, 9. }; [html] view plain copy 1. static struct snd_soc_dai_driver wm8994_dai[] = { 2. { 3. .name = "wm8994-aif1", 4. .id = 1, 5. .playback = { 6. .stream_name = "AIF1 Playback", 7. .channels_min = 1, 8. .channels_max = 2, 9. .rates = WM8994_RATES, 10. .formats = WM8994_FORMATS, 11. }, 12. .capture = { 13. .stream_name = "AIF1 Capture", 14. .channels_min = 1, 15. .channels_max = 2, 16. .rates = WM8994_RATES, 17. .formats = WM8994_FORMATS, 18. }, 19. .ops = &wm8994_aif1_dai_ops, 20. }, 62 21. ...... 22. } 可见,Codec驱动的第一个步骤就是定义snd_soc_codec_driver和 snd_soc_dai_driver的实例,然后调用snd_soc_register_codec函数对Codec 进行注册。进入snd_soc_register_codec函数看看: 首先,它申请了一个snd_soc_codec结构的实例: 1. codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL) ; 确定codec的名字,这个名字很重要,Machine驱动定义的snd_soc_dai_link 中会指定每个link的codec和dai的名字,进行匹配绑定时就是通过和这里的 名字比较,从而找到该Codec的! 1. /* create CODEC component name */ 2. codec->name = fmt_single_name(dev, &codec->id); 然后初始化它的各个字段,多数字段的值来自上面定义的 snd_soc_codec_driver的实例soc_codec_dev_wm8994: 1. codec->write = codec_drv->write; 2. codec->read = codec_drv->read; 3. codec->volatile_register = codec_drv->volatile_register; 4. codec->readable_register = codec_drv->readable_register; 5. codec->writable_register = codec_drv->writable_register; 6. codec->_level = SND_SOC_BIAS_OFF; 7. codec-> = dev; 8. codec-> = codec; 9. codec->_notifier = codec_drv->seq_notifier; 10. codec->_event = codec_drv->stream_event; 11. codec->dev = dev; 12. codec->driver = codec_drv; 13. codec->num_dai = num_dai; 63 在做了一些寄存器缓存的初始化和配置工作后,通过snd_soc_register_dais函 数对本Codec的dai进行注册: 1. /* register any DAIs */ 2. if (num_dai) { 3. ret = snd_soc_register_dais(dev, dai_drv, num_dai); 4. if (ret < 0) 5. goto fail; 6. } 最后,它把codec实例链接到全局链表codec_list中,并且调用 snd_soc_instantiate_cards是函数触发Machine驱动进行一次匹配绑定操作: 1. list_add(&codec->list, &codec_list); 2. snd_soc_instantiate_cards(); 上面的snd_soc_register_dais函数其实也是和snd_soc_register_codec类似, 显示为每个snd_soc_dai实例分配内存,确定dai的名字,用snd_soc_dai_driver 实例的字段对它进行必要初始化,最后把该dai链接到全局链表dai_list中,和 Codec一样,最后也会调用snd_soc_instantiate_cards函数触发一次匹配绑定 的操作。 图3.1 dai的注册 关于snd_soc_instantiate_cards函数,请参阅另一篇博文:Linux音频驱动之六: ASoC架构中的Machine。 64 4. mfd设备 前面已经提到,codec驱动把自己注册为一个platform driver,那对应的platform device在哪里定义?答案是在以下代码文件中:/drivers/mfd/wm8994-core.c。 WM8994本身具备多种功能,除了codec外,它还有作为LDO和GPIO使用, 这几种功能共享一些IO和中断资源,linux为这种设备提供了一套标准的实现方 法:mfd设备。其基本思想是为这些功能的公共部分实现一个父设备,以便共享 某些系统资源和功能,然后每个子功能实现为它的子设备,这样既共享了资源和 代码,又能实现合理的设备层次结构,主要利用到的API就是: mfd_add_devices(),mfd_remove_devices(),mfd_cell_enable(), mfd_cell_disable(),mfd_clone_cell()。 回到wm8994-core.c中,因为WM8994使用I2C进行内部寄存器的存取,它首 先注册了一个I2C驱动: [html] view plain copy 1. static struct i2c_driver wm8994_i2c_driver = { 2. .driver = { 3. .name = "wm8994", 4. .owner = THIS_MODULE, 5. .pm = &wm8994_pm_ops, 6. .of_match_table = wm8994_of_match, 7. }, 8. .probe = wm8994_i2c_probe, 9. .remove = wm8994_i2c_remove, 10. .id_table = wm8994_i2c_id, 11. }; 12. 13. static int __init wm8994_i2c_init(void) 14. { 15. int ret; 16. 17. ret = i2c_add_driver(&wm8994_i2c_driver); 18. if (ret != 0) 19. pr_err("Failed to register wm8994 I2C driver: %d n", ret); 20. 21. return ret; 22. } 23. module_init(wm8994_i2c_init); 65 进入wm8994_i2c_probe()函数,它先申请了一个wm8994结构的变量,该变量 被作为这个I2C设备的driver_data使用,上面已经讲过,codec作为它的子设 备,将会取出并使用这个driver_data。接下来,本函数利用regmap_init_i2c() 初始化并获得一个regmap结构,该结构主要用于后续基于regmap机制的寄存 器I/O,关于regmap我们留在后面再讲。最后,通过wm8994_device_init()来 添加mfd子设备: 1. static int wm8994_i2c_probe(struct i2c_client *i2c, 2. const struct i2c_device_id *id) 3. { 4. struct wm8994 *wm8994; 5. int ret; 6. wm8994 = devm_kzalloc(&i2c->dev, sizeof(struct wm8994 ), GFP_KERNEL); 7. i2c_set_clientdata(i2c, wm8994); 8. wm8994->dev = &i2c->dev; 9. wm8994->irq = i2c->irq; 10. wm8994->type = id->driver_data; 11. wm8994->regmap = regmap_init_i2c(i2c, &wm8994_base_r egmap_config); 12. 13. return wm8994_device_init(wm8994, i2c->irq); 14. } 继续进入wm8994_device_init()函数,它首先为两个LDO添加mfd子设备: 1. /* Add the on-chip regulators first for bootstrapping */ 2. ret = mfd_add_devices(wm8994->dev, -1, 3. wm8994_regulator_devs, 4. ARRAY_SIZE(wm8994_regulator_devs), 5. NULL, 0); 因为WM1811,WM8994,WM8958三个芯片功能类似,因此这三个芯片都使用了 WM8994的代码,所以wm8994_device_init()接下来根据不同的芯片型号做了一 些初始化动作,这部分的代码就不贴了。接着,从platform_data中获得部分配 置信息: 1. if (pdata) { 2. wm8994->irq_base = pdata->irq_base; 66 3. wm8994->gpio_base = pdata->gpio_base; 4. 5. /* GPIO configuration is only applied if it's non-zer o */ 6. ...... 7. } 最后,初始化irq,然后添加codec子设备和gpio子设备: 1. wm8994_irq_init(wm8994); 2. 3. ret = mfd_add_devices(wm8994->dev, -1, 4. wm8994_devs, ARRAY_SIZE(wm8994_devs), 5. NULL, 0); 经过以上这些处理后,作为父设备的I2C设备已经准备就绪,它的下面挂着4 个子设备:ldo-0,ldo-1,codec,gpio。其中,codec子设备的加入,它将会和 前面所讲codec的platform driver匹配,触发probe回调完成下面所说的codec 驱动的初始化工作。 5. Codec初始化 Machine驱动的初始化,codec和dai的注册,都会调用 snd_soc_instantiate_cards()进行一次声卡和codec,dai,platform的匹配绑定 过程,这里所说的绑定,正如Machine驱动一文中所描述,就是通过3个全局 链表,按名字进行匹配,把匹配的codec,dai和platform实例赋值给声卡每对 dai的snd_soc_pcm_runtime变量中。一旦绑定成功,将会使得codec和dai 驱动的probe回调被调用,codec的初始化工作就在该回调中完成。对于 WM8994,该回调就是wm8994_codec_probe函数: 67 图 5.1 wm8994_codec_probe 取出父设备的driver_data,其实就是上一节的wm8994结构变量,取出 其中的regmap字段,复制到codec的control_data字段中; 申请一个wm8994_priv私有数据结构,并把它设为codec设备的 driver_data; 通过snd_soc_codec_set_cache_io初始化regmap io,完成这一步后, 就可以使用API:snd_soc_read(),snd_soc_write()对codec的寄存器进 行读写了; 把父设备的driver_data(struct wm8994)和platform_data保存到私有 结构wm8994_priv中; 因为要同时支持3个芯片型号,这里要根据芯片的型号做一些特定的初始 化工作; 申请必要的几个中断; 设置合适的偏置电平; 通过snd_soc_update_bits修改某些寄存器; 根据父设备的platform_data,完成特定于平台的初始化配置; 添加必要的control,dapm部件进而dapm路由信息; 68 至此,codec驱动的初始化完成。 5. regmap-io 我们知道,要想对codec进行控制,通常都是通过读写它的内部寄存器完成的, 读写的接口通常是I2C或者是SPI接口,不过每个codec芯片寄存器的比特位 组成都有所不同,寄存器地址的比特位也有所不同。例如WM8753的寄存器地 址是7bits,数据是9bits,WM8993的寄存器地址是8bits,数据也是16bits, 而WM8994的寄存器地址是16bits,数据也是16bits。在kernel3.1版本,内核 引入了一套regmap机制和相关的API,这样就可以用统一的操作来实现对这些 多样的寄存器的控制。regmap使用起来也相对简单: 为codec定义一个regmap_config结构实例,指定codec寄存器的地址 和数据位等信息; 根据codec的控制总线类型,调用以下其中一个函数,得到一个指向 regmap结构的指针: o struct regmap *regmap_init_i2c(struct i2c_client *i2c, const struct regmap_config *config); o struct regmap *regmap_init_spi(struct spi_device *dev, const struct regmap_config *config); 把获得的regmap结构指针赋值给codec->control_data; 调用soc-io的api:snd_soc_codec_set_cache_io使得soc-io和regmap 进行关联; 完成以上步骤后,codec驱动就可以使用诸如snd_soc_read、snd_soc_write、 snd_soc_update_bits等API对codec的寄存器进行读写了。 8. Linux ALSA声卡驱动之八:ASoC架构中的Platform 1. Platform驱动在ASoC中的作用 前面几章内容已经说过,ASoC被分为Machine,Platform和Codec三大部件, Platform驱动的主要作用是完成音频数据的管理,最终通过CPU的数字音频接 口(DAI)把音频数据传送给Codec进行处理,最终由Codec输出驱动耳机或 者是喇叭的音信信号。在具体实现上,ASoC有把Platform驱动分为两个部分: snd_soc_platform_driver和snd_soc_dai_driver。其中,platform_driver负责管 理音频数据,把音频数据通过dma或其他操作传送至cpu dai中,dai_driver则 主要完成cpu一侧的dai的参数配置,同时也会通过一定的途径把必要的dma 等参数与snd_soc_platform_driver进行交互。 69 2. snd_soc_platform_driver的注册 通常,ASoC把snd_soc_platform_driver注册为一个系统的platform_driver, 不要被这两个相像的术语所迷惑,前者只是针对ASoC子系统的,后者是来自 Linux的设备驱动模型。我们要做的就是: 定义一个snd_soc_platform_driver结构的实例; 在platform_driver的probe回调中利用ASoC的API: snd_soc_register_platform()注册上面定义的实例; 实现snd_soc_platform_driver中的各个回调函数; 以kernel3.3中的/sound/soc/samsung/dma.c为例: [cpp] view plain copy 1. static struct snd_soc_platform_driver samsung_asoc_platfo rm = { 2. .ops = &dma_ops, 3. .pcm_new = dma_new, 4. .pcm_free = dma_free_dma_buffers, 5. }; 6. 7. static int __devinit samsung_asoc_platform_probe(struct p latform_device *pdev) 8. { 9. return snd_soc_register_platform(&pdev->dev, &samsung _asoc_platform); 10. } 11. 12. static int __devexit samsung_asoc_platform_remove(struct platform_device *pdev) 13. { 14. snd_soc_unregister_platform(&pdev->dev); 15. return 0; 16. } 17. 18. static struct platform_driver asoc_dma_driver = { 19. .driver = { 20. .name = "samsung-audio", 21. .owner = THIS_MODULE, 22. }, 23. 24. .probe = samsung_asoc_platform_probe, 70 25. .remove = __devexit_p(samsung_asoc_platform_remove), 26. }; 27. 28. module_platform_driver(asoc_dma_driver); snd_soc_register_platform() 该函数用于注册一个snd_soc_platform,只有注 册以后,它才可以被Machine驱动使用。它的代码已经清晰地表达了它的实现 过程: 为snd_soc_platform实例申请内存; 从platform_device中获得它的名字,用于Machine驱动的匹配工作; 初始化snd_soc_platform的字段; 把snd_soc_platform实例连接到全局链表platform_list中; 调用snd_soc_instantiate_cards,触发声卡的machine、platform、codec、 dai等的匹配工作; 3. cpu的snd_soc_dai driver驱动的注册 dai驱动通常对应cpu的一个或几个I2S/PCM接口,与snd_soc_platform一样, dai驱动也是实现为一个platform driver,实现一个dai驱动大致可以分为以下几 个步骤: 定义一个snd_soc_dai_driver结构的实例; 在对应的platform_driver中的probe回调中通过API: snd_soc_register_dai或者snd_soc_register_dais,注册snd_soc_dai 实例; 实现snd_soc_dai_driver结构中的probe、suspend等回调; 实现snd_soc_dai_driver结构中的snd_soc_dai_ops字段中的回调函数; snd_soc_register_dai 这个函数在上一篇介绍codec驱动的博文中已有介绍, 请参考:Linux ALSA声卡驱动之七:ASoC架构中的Codec。 snd_soc_dai 该结构在snd_soc_register_dai函数中通过动态内存申请获得, 简要介绍一下几个重要字段: driver 指向关联的snd_soc_dai_driver结构,由注册时通过参数传入; playback_dma_data 用于保存该dai播放stream的dma信息,例如dma 的目标地址,dma传送单元大小和通道号等; capture_dma_data 同上,用于录音stream; platform 指向关联的snd_soc_platform结构; snd_soc_dai_driver 该结构需要自己根据不同的soc芯片进行定义,关键字 段介绍如下: 71 probe、remove 回调函数,分别在声卡加载和卸载时被调用; suspend、resume 电源管理回调函数; ops 指向snd_soc_dai_ops结构,用于配置和控制该dai; playback snd_soc_pcm_stream结构,用于指出该dai支持的声道数, 码率,数据格式等能力; capture snd_soc_pcm_stream结构,用于指出该dai支持的声道数,码 率,数据格式等能力; 4. snd_soc_dai_driver中的ops字段 ops字段指向一个snd_soc_dai_ops结构,该结构实际上是一组回调函数的集 合,dai的配置和控制几乎都是通过这些回调函数来实现的,这些回调函数基本 可以分为3大类,驱动程序可以根据实际情况实现其中的一部分: 工作时钟配置函数 通常由machine驱动调用: set_sysclk 设置dai的主时钟; set_pll 设置PLL参数; set_clkdiv 设置分频系数; dai的格式配置函数 通常由machine驱动调用: set_fmt 设置dai的格式; set_tdm_slot 如果dai支持时分复用,用于设置时分复用的slot; set_channel_map 声道的时分复用映射设置; set_tristate 设置dai引脚的状态,当与其他dai并联使用同一引脚时需 要使用该回调; 标准的snd_soc_ops回调 通常由soc-core在进行PCM操作时调用: startup shutdown hw_params hw_free prepare trigger 抗pop,pop声 由soc-core调用: digital_mute 以下这些api通常被machine驱动使用,machine驱动在他的snd_pcm_ops字 段中的hw_params回调中使用这些api: snd_soc_dai_set_fmt() 实际上会调用snd_soc_dai_ops或者codec driver中的set_fmt回调; 72 snd_soc_dai_set_pll() 实际上会调用snd_soc_dai_ops或者codec driver中的set_pll回调; snd_soc_dai_set_sysclk() 实际上会调用snd_soc_dai_ops或者codec driver中的set_sysclk回调; snd_soc_dai_set_clkdiv() 实际上会调用snd_soc_dai_ops或者codec driver中的set_clkdiv回调; snd_soc_dai_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)的第二个参数 fmt在这里特别说一下,ASoC目前只是用了它的低16位,并且为它专门定义 了一些宏来方便我们使用: bit 0-3 用于设置接口的格式: 1. #define SND_SOC_DAIFMT_I2S 1 /* I2S mode */ 2. #define SND_SOC_DAIFMT_RIGHT_J 2 /* Right Justified mode */ 3. #define SND_SOC_DAIFMT_LEFT_J 3 /* Left Justified m ode */ 4. #define SND_SOC_DAIFMT_DSP_A 4 /* L data MSB after FRM LRC */ 5. #define SND_SOC_DAIFMT_DSP_B 5 /* L data MSB durin g FRM LRC */ 6. #define SND_SOC_DAIFMT_AC97 6 /* AC97 */ 7. #define SND_SOC_DAIFMT_PDM 7 /* Pulse density modula tion */ bit 4-7 用于设置接口时钟的开关特性: 1. #define SND_SOC_DAIFMT_CONT (1 << 4) /* continuous cl ock */ 2. #define SND_SOC_DAIFMT_GATED (2 << 4) /* clock is gated */ bit 8-11 用于设置接口时钟的相位: 1. #define SND_SOC_DAIFMT_NB_NF (1 << 8) /* normal bi t clock + frame */ 2. #define SND_SOC_DAIFMT_NB_IF (2 << 8) /* normal BC LK + inv FRM */ 73 3. #define SND_SOC_DAIFMT_IB_NF (3 << 8) /* invert BC LK + nor FRM */ 4. #define SND_SOC_DAIFMT_IB_IF (4 << 8) /* invert BC LK + FRM */ bit 12-15 用于设置接口主从格式: 1. #define SND_SOC_DAIFMT_CBM_CFM (1 << 12) /* codec cl k & FRM master */ 2. #define SND_SOC_DAIFMT_CBS_CFM (2 << 12) /* codec cl k slave & FRM master */ 3. #define SND_SOC_DAIFMT_CBM_CFS (3 << 12) /* codec cl k master & frame slave */ 4. #define SND_SOC_DAIFMT_CBS_CFS (4 << 12) /* codec cl k & FRM slave */ 5. snd_soc_platform_driver中的ops字段 该ops字段是一个snd_pcm_ops结构,实现该结构中的各个回调函数是soc platform驱动的主要工作,他们基本都涉及dma操作以及dma buffer的管理等 工作。下面介绍几个重要的回调函数: 当应用程序打开一个pcm设备时,该函数会被调用,通常,该函数会使用 snd_soc_set_runtime_hwparams()设置substream中的snd_pcm_runtime结构 里面的hw_params相关字段,然后为snd_pcm_runtime的private_data字段 申请一个私有结构,用于保存该平台的dma参数。 _params 驱动的hw_params阶段,该函数会被调用。通常,该函数会通过 snd_soc_dai_get_dma_data函数获得对应的dai的dma参数,获得的参数一般 都会保存在snd_pcm_runtime结构的private_data字段。然后通过 snd_pcm_set_runtime_buffer函数设置snd_pcm_runtime结构中的dma buffer 的地址和大小等参数。要注意的是,该回调可能会被多次调用,具体实现时要小 心处理多次申请资源的问题。 e 74 正式开始数据传送之前会调用该函数,该函数通常会完成dma操作的必要准备 工作。 r 数据传送的开始,暂停,恢复和停止时,该函数会被调用。 r 该函数返回传送数据的当前位置。 6. 音频数据的dma操作 soc-platform驱动的最主要功能就是要完成音频数据的传送,大多数情况下,音 频数据都是通过dma来完成的。 6.1. 申请dma buffer 因为dma的特殊性,dma buffer是一块特殊的内存,比如有的平台规定只有某 段地址范围的内存才可以进行dma操作,而多数嵌入式平台还要求dma内存的 物理地址是连续的,以方便dma控制器对内存的访问。在ASoC架构中,dma buffer的信息保存在snd_pcm_substream结构的snd_dma_buffer *buf字段中, 它的定义如下 1. struct snd_dma_buffer { 2. struct snd_dma_device dev; /* device type */ 3. unsigned char *area; /* virtual pointer */ 4. dma_addr_t addr; /* physical address */ 5. size_t bytes; /* buffer size in bytes */ 6. void *private_data; /* private for allocator; don't t ouch */ 7. }; 那么,在哪里完成了snd_dam_buffer结构的初始化赋值操作呢?答案就在 snd_soc_platform_driver的pcm_new回调函数中,还是以 /sound/soc/samsung/dma.c为例: 1. static struct snd_soc_platform_driver samsung_asoc_platfo rm = { 2. .ops = &dma_ops, 3. .pcm_new = dma_new, 75 4. .pcm_free = dma_free_dma_buffers, 5. }; 6. 7. static int __devinit samsung_asoc_platform_probe(struct p latform_device *pdev) 8. { 9. return snd_soc_register_platform(&pdev->dev, &samsung _asoc_platform); 10. } pcm_new字段指向了dma_new函数,dma_new函数进一步为playback和 capture分别调用preallocate_dma_buffer函数,我们看看 preallocate_dma_buffer函数的实现: 1. static int preallocate_dma_buffer(struct snd_pcm *pcm, in t stream) 2. { 3. struct snd_pcm_substream *substream = pcm->streams[st ream].substream; 4. struct snd_dma_buffer *buf = &substream->dma_buffer; 5. size_t size = dma__bytes_max; 6. 7. pr_debug("Entered %sn", __func__); 8. 9. buf-> = SNDRV_DMA_TYPE_DEV; 10. buf-> = pcm->card->dev; 11. buf->private_data = NULL; 12. buf->area = dma_alloc_writecombine(pcm->card->dev, s ize, 13. &buf->addr, GFP_KERNEL); 14. if (!buf->area) 15. return -ENOMEM; 16. buf->bytes = size; 17. return 0; 18. } 该函数先是获得事先定义好的buffer大小,然后通过dma_alloc_weitecombine 函数分配dma内存,然后完成substream->dma_buffer的初始化赋值工作。上 76 述的pcm_new回调会在声卡的建立阶段被调用,调用的详细的过程请参考Linux ALSAs声卡驱动之六:ASoC架构中的Machine中的图3.1。 在声卡的hw_params阶段,snd_soc_platform_driver结构的ops->hw_params 会被调用,在该回调用,通常会使用api:snd_pcm_set_runtime_buffer()把 substream->dma_buffer的数值拷贝到substream->runtime的相关字段中 (.dma_area, .dma_addr, .dma_bytes),这样以后就可以通过 substream->runtime获得这些地址和大小信息了。 dma buffer获得后,即是获得了dma操作的源地址,那么目的地址在哪里?其 实目的地址当然是在dai中,也就是前面介绍的snd_soc_dai结构的 playback_dma_data和capture_dma_data字段中,而这两个字段的值也是在 hw_params阶段,由snd_soc_dai_driver结构的ops->hw_params回调,利用 api:snd_soc_dai_set_dma_data进行设置的。紧随其后, snd_soc_platform_driver结构的ops->hw_params回调利用api: snd_soc_dai_get_dma_data获得这些dai的dma信息,其中就包括了dma的 目的地址信息。这些dma信息通常还会被保存在 substream->runtime->private_data中,以便在substream的整个生命周期中可 以随时获得这些信息,从而完成对dma的配置和操作。 6.2 dma buffer管理 播放时,应用程序把音频数据源源不断地写入dma buffer中,然后相应platform 的dma操作则不停地从该buffer中取出数据,经dai送往codec中。录音时则 正好相反,codec源源不断地把A/D转换好的音频数据经过dai送入dma buffer 中,而应用程序则不断地从该buffer中读走音频数据。 图 6.2.1 环形缓冲区 环形缓冲区正好适合用于这种情景的buffer管理,理想情况下,大小为Count 的缓冲区具备一个读指针和写指针,我们期望他们都可以闭合地做环形移动,但 77 是实际的情况确实:缓冲区通常都是一段连续的地址,他是有开始和结束两个边 界,每次移动之前都必须进行一次判断,当指针移动到末尾时就必须人为地让他 回到起始位置。在实际应用中,我们通常都会把这个大小为Count的缓冲区虚 拟成一个大小为n*Count的逻辑缓冲区,相当于理想状态下的圆形绕了n圈之 后,然后把这段总的距离拉平为一段直线,每一圈对应直线中的一段,因为n 比较大,所以大多数情况下不会出现读写指针的换位的情况(如果不对buffer 进行扩展,指针到达末端后,回到起始端时,两个指针的前后相对位置会发生互 换)。扩展后的逻辑缓冲区在计算剩余空间可条件判断是相对方便。alsa driver 也使用了该方法对dma buffer进行管理: 图6.2.2 alsa driver缓冲区管理 snd_pcm_runtime结构中,使用了四个相关的字段来完成这个逻辑缓冲区的管 理: snd_pcm__ptr_base 环形缓冲区每一圈的基地址,当读写指 针越过一圈后,它按buffer size进行移动; snd_pcm_->hw_ptr 硬件逻辑位置,播放时相当于读指针, 录音时相当于写指针; snd_pcm_l->appl_ptr 应用逻辑位置,播放时相当于写指 针,录音时相当于读指针; snd_pcm_ry 扩展后的逻辑缓冲区大小,通常是 (2^n)*size; 通过这几个字段,我们可以很容易地获得缓冲区的有效数据,剩余空间等信息, 也可以很容易地把当前逻辑位置映射回真实的dma buffer中。例如,获得播放 缓冲区的空闲空间: 78 1. static inline snd_pcm_uframes_t snd_pcm_playback_avail(st ruct snd_pcm_runtime *runtime) 2. { 3. snd_pcm_sframes_t avail = runtime->status->hw_ptr + r untime->buffer_size - runtime->control->appl_ptr; 4. if (avail < 0) 5. avail += runtime->boundary; 6. else if ((snd_pcm_uframes_t) avail >= runtime->bounda ry) 7. avail -= runtime->boundary; 8. return avail; 9. } 要想映射到真正的缓冲区位置,只要减去runtime->hw_ptr_base即可。下面的 api用于更新这几个指针的当前位置: 1. int snd_pcm_update_hw_ptr(struct snd_pcm_substream *subst ream) 所以要想通过snd_pcm_playback_avail等函数获得正确的信息前,应该先要调 用这个api更新指针位置。 以播放(playback)为例,我现在知道至少有3个途径可以完成对dma buffer的 写入: 应用程序调用alsa-lib的snd_pcm_writei、snd_pcm_writen函数; 应用程序使用ioctl:SNDRV_PCM_IOCTL_WRITEI_FRAMES或 SNDRV_PCM_IOCTL_WRITEN_FRAMES; 应用程序使用alsa-lib的snd_pcm_mmap_begin/snd_pcm_mmap_commit; 以上几种方式最终把数据写入dma buffer中,然后修改 runtime->control->appl_ptr的值。 播放过程中,通常会配置成每一个period size生成一个dma中断,中断处理函 数最重要的任务就是: 更新dma的硬件的当前位置,该数值通常保存在runtime->private_data 中; 调用snd_pcm_period_elapsed函数,该函数会进一步调用 snd_pcm_update_hw_ptr0函数更新上述所说的4个缓冲区管理字段,然 后唤醒相应的等待进程; 79 void snd_pcm_period_elapsed(struct snd_pcm_substream *sub stream) { struct snd_pcm_runtime *runtime; unsigned long flags; if (PCM_RUNTIME_CHECK(substream)) return; runtime = substream->runtime; if (runtime->transfer_ack_begin) runtime->transfer_ack_begin(substream); snd_pcm_stream_lock_irqsave(substream, flags); if (!snd_pcm_running(substream) || snd_pcm_update_hw_ptr0(substream, 1) < 0) goto _end; if (substream->timer_running) snd_timer_interrupt(substream->timer, 1); _end: snd_pcm_stream_unlock_irqrestore(substream, flags); if (runtime->transfer_ack_end) runtime->transfer_ack_end(substream); kill_fasync(&runtime->fasync, SIGIO, POLL_IN); } 如果设置了transfer_ack_begin和transfer_ack_end回调, snd_pcm_period_elapsed还会调用这两个回调函数。 7. 图说代码 最后,反正图也画了,好与不好都传上来供参考一下,以下这张图表达了 ASoC 中Platform驱动的几个重要数据结构之间的关系: 80 图 7.1 ASoC Platform驱动 一堆的private_data,很重要但也很容易搞混,下面的图不知对大家有没有帮助: 81 图7.2 private_data 82
2024年3月12日发(作者:焦昭懿)
ALSA声卡驱动之一:ALSA架构简介
一. 概述
ALSA是Advanced Linux Sound Architecture 的缩写,目前已经成为了linux的主
流音频体系结构,想了解更多的关于ALSA的这一开源项目的信息和知识,请查
看以下网址:/。
在内核设备驱动层,ALSA提供了alsa-driver,同时在应用层,ALSA为我们提
供了alsa-lib,应用程序只要调用alsa-lib提供的API,即可以完成对底层音频硬
件的控制。
图 1.1 alsa的软件体系结构
由图1.1可以看出,用户空间的alsa-lib对应用程序提供统一的API接口,这样
可以隐藏了驱动层的实现细节,简化了应用程序的实现难度。内核空间中,
alsa-soc其实是对alsa-driver的进一步封装,他针对嵌入式设备提供了一些列
增强的功能。本系列博文仅对嵌入式系统中的alsa-driver和alsa-soc进行讨论。
1
二. ALSA设备文件结构
我们从alsa在linux中的设备文件结构开始我们的alsa之旅. 看看我的电脑
中的alsa驱动的设备文件结构:
$ cd /dev/snd
$ ls -l
crw-rw----+ 1 root audio 116, 8 2011-02-23 21:38 controlC0
crw-rw----+ 1 root audio 116, 4 2011-02-23 21:38 midiC0D0
crw-rw----+ 1 root audio 116, 7 2011-02-23 21:39 pcmC0D0c
crw-rw----+ 1 root audio 116, 6 2011-02-23 21:56 pcmC0D0p
crw-rw----+ 1 root audio 116, 7 2011-02-23 21:39 pcmC0D1c
crw-rw----+ 1 root audio 116, 5 2011-02-23 21:38 pcmC0D1p
crw-rw----+ 1 root audio 116, 3 2011-02-23 21:38 seq
crw-rw----+ 1 root audio 116, 2 2011-02-23 21:38 timer
$
我们可以看到以下设备文件:
controlC0 --> 用于声卡的控制,例如通道选择,混音,麦克风
的控制等
midiC0D0 --> 用于播放midi音频
pcmC0D0c --〉 用于录音的pcm设备
pcmC0D0p --〉 用于播放的pcm设备
seq --〉 音序器
timer --〉 定时器
其中,C0D0代表的是声卡0中的设备0,pcmC0D0c最后一个c代表capture,
pcmC0D0p最后一个p代表playback,这些都是alsa-driver中的命名规则。从
上面的列表可以看出,我的声卡下挂了6个设备,根据声卡的实际能力,驱动实
际上可以挂上更多种类的设备,在include/sound/core.h中,定义了以下设备类
型:
[c-sharp] view plain copy
2
1. #define SNDRV_DEV_TOPLEVEL ((__force snd_device_type_t)
0)
2. #define SNDRV_DEV_CONTROL ((__force snd_device_type_t)
1)
3. #define SNDRV_DEV_LOWLEVEL_PRE ((__force snd_device_type
_t) 2)
4. #define SNDRV_DEV_LOWLEVEL_NORMAL ((__force snd_device_ty
pe_t) 0x1000)
5. #define SNDRV_DEV_PCM ((__force snd_device_type_t)
0x1001)
6. #define SNDRV_DEV_RAWMIDI ((__force snd_device_type_t)
0x1002)
7. #define SNDRV_DEV_TIMER ((__force snd_device_type_t)
0x1003)
8. #define SNDRV_DEV_SEQUENCER ((__force snd_device_type_t)
0x1004)
9. #define SNDRV_DEV_HWDEP ((__force snd_device_type_t)
0x1005)
10. #define SNDRV_DEV_INFO ((__force snd_device_type_t)
0x1006)
11. #define SNDRV_DEV_BUS ((__force snd_device_type_t)
0x1007)
12. #define SNDRV_DEV_CODEC ((__force snd_device_type_t)
0x1008)
13. #define SNDRV_DEV_JACK ((__force snd_device_typ
e_t) 0x1009)
14. #define SNDRV_DEV_LOWLEVEL ((__force snd_device_type_t)
0x2000)
通常,我们更关心的是pcm和control这两种设备。
三. 驱动的代码文件结构
在Linux2.6代码树中,Alsa的代码文件结构如下:
sound
/core
/oss
/seq
/ioctl32
/include
3
/drivers
/i2c
/synth
/emux
/pci
/(cards)
/isa
/(cards)
/arm
/ppc
/sparc
/usb
/pcmcia /(cards)
/oss
/soc
/codecs
core 该目录包含了ALSA驱动的中间层,它是整个ALSA驱动
的核心部分
core/oss 包含模拟旧的OSS架构的PCM和Mixer模块
core/seq 有关音序器相关的代码
include ALSA驱动的公共头文件目录,该目录的头文件需要导出给
用户空间的应用程序使用,通常,驱动模块私有的头文件不应放置在这里
drivers 放置一些与CPU、BUS架构无关的公用代码
i2c ALSA自己的I2C控制代码
pci pci声卡的顶层目录,子目录包含各种pci声卡的代码
isa isa声卡的顶层目录,子目录包含各种isa声卡的代码
soc 针对system-on-chip体系的中间层代码
soc/codecs 针对soc体系的各种codec的代码,与平台无关
4
ALSA声卡驱动之二:声卡的创建
1. struct snd_card
1.1. snd_card是什么
snd_card可以说是整个ALSA音频驱动最顶层的一个结构,整个声卡的软件逻
辑结构开始于该结构,几乎所有与声音相关的逻辑设备都是在snd_card的管理
之下,声卡驱动的第一个动作通常就是创建一个snd_card结构体。正因为如此,
本节中,我们也从 struct cnd_card开始吧。
1.2. snd_card的定义
snd_card的定义位于改头文件中:include/sound/core.h
[c-sharp] view plain copy
1. /* main structure for soundcard */
2.
3. struct snd_card {
4. int number; /* number of soundcard (index to
5. snd_cards) */
6.
7. char id[16]; /* id string of this card */
8. char driver[16]; /* driver name */
9. char shortname[32]; /* short name of this soundca
rd */
10. char longname[80]; /* name of this soundcard */
11. char mixername[80]; /* mixer name */
12. char components[128]; /* card components delim
ited with
13. space */
14. struct module *module; /* top-level module */
15.
16. void *private_data; /* private data for soundcar
d */
17. void (*private_free) (struct snd_card *card); /* cal
lback for freeing of
5
18. private data */
19. struct list_head devices; /* devices */
20.
21. unsigned int last_numid; /* last used numeric ID
*/
22. struct rw_semaphore controls_rwsem; /* controls list
lock */
23. rwlock_t ctl_files_rwlock; /* ctl_files list lock *
/
24. int controls_count; /* count of all controls */
25. int user_ctl_count; /* count of all user control
s */
26. struct list_head controls; /* all controls for this
card */
27. struct list_head ctl_files; /* active control files
*/
28.
29. struct snd_info_entry *proc_root; /* root for soun
dcard specific files */
30. struct snd_info_entry *proc_id; /* the card id */
31. struct proc_dir_entry *proc_root_link; /* number li
nk to real id */
32.
33. struct list_head files_list; /* all files associa
ted to this card */
34. struct snd_shutdown_f_ops *s_f_ops; /* file operatio
ns in the shutdown
35. state */
36. spinlock_t files_lock; /* lock the files for th
is card */
37. int shutdown; /* this card is going down *
/
38. int free_on_last_close; /* free in context of fi
le_release */
39. wait_queue_head_t shutdown_sleep;
40. struct device *dev; /* device assigned to this c
ard */
41. #ifndef CONFIG_SYSFS_DEPRECATED
42. struct device *card_dev; /* cardX object for sysf
s */
43. #endif
44.
45. #ifdef CONFIG_PM
6
46. unsigned int power_state; /* power state */
47. struct mutex power_lock; /* power lock */
48. wait_queue_head_t power_sleep;
49. #endif
50.
51. #if defined(CONFIG_SND_MIXER_OSS) || defined(CONFIG_SND_
MIXER_OSS_MODULE)
52. struct snd_mixer_oss *mixer_oss;
53. int mixer_oss_change_count;
54. #endif
55. };
struct list_head devices 记录该声卡下所有逻辑设备的链表
struct list_head controls 记录该声卡下所有的控制单元的链表
void *private_data 声卡的私有数据,可以在创建声卡时通过参数
指定数据的大小
2. 声卡的建立流程
2.1.1. 第一步,创建snd_card的一个实例
[c-sharp] view plain copy
1. struct snd_card *card;
2. int err;
3. ....
4. err = snd_card_create(index, id, THIS_MODULE, 0, &card);
index 一个整数值,该声卡的编号
id 字符串,声卡的标识符
第四个参数 该参数决定在创建snd_card实例时,需要同时额外分配的
私有数据的大小,该数据的指针最终会赋值给snd_card的private_data
数据成员
card 返回所创建的snd_card实例的指针
7
2.1.2. 第二步,创建声卡的芯片专用数据
声卡的专用数据主要用于存放该声卡的一些资源信息,例如中断资源、io资源、
dma资源等。可以有两种创建方法:
通过上一步中snd_card_create()中的第四个参数,让snd_card_create
自己创建
[c-sharp] view plain copy
1. // struct mychip 用于保存专用数据
2. err = snd_card_create(index, id, THIS_MODULE,
3. sizeof(struct mychip), &card);
4. // 从private_data中取出
5. struct mychip *chip = card->private_data;
自己创建:
[c-sharp] view plain copy
1. struct mychip {
2. struct snd_card *card;
3. ....
4. };
5. struct snd_card *card;
6. struct mychip *chip;
7.
8. chip = kzalloc(sizeof(*chip), GFP_KERNEL);
9. ......
10. err = snd_card_create(index[dev], id[dev], THIS_MODULE,
0, &card);
11. // 专用数据记录snd_card实例
12. chip->card = card;
13. .....
然后,把芯片的专有数据注册为声卡的一个低阶设备:
[c-sharp] view plain copy
1. static int snd_mychip_dev_free(struct snd_device *device)
8
2. {
3. return snd_mychip_free(device->device_data);
4. }
5.
6. static struct snd_device_ops ops = {
7. .dev_free = snd_mychip_dev_free,
8. };
9. ....
10. snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);
注册为低阶设备主要是为了当声卡被注销时,芯片专用数据所占用的内存可以
被自动地释放。
2.1.3. 第三步,设置Driver的ID和名字
[c-sharp] view plain copy
1. strcpy(card->driver, "My Chip");
2. strcpy(card->shortname, "My Own Chip 123");
3. sprintf(card->longname, "%s at 0x%lx irq %i",
4. card->shortname, chip->ioport, chip->irq);
snd_card的driver字段保存着芯片的ID字符串,user空间的alsa-lib会使用到
该字符串,所以必须要保证该ID的唯一性。shortname字段更多地用于打印信
息,longname字段则会出现在/proc/asound/cards中。
2.1.4. 第四步,创建声卡的功能部件(逻辑设备),例如PCM,Mixer,MIDI
等
这时候可以创建声卡的各种功能部件了,还记得开头的snd_card结构体的
devices字段吗?每一种部件的创建最终会调用snd_device_new()来生成一个
snd_device实例,并把该实例链接到snd_card的devices链表中。
通常,alsa-driver的已经提供了一些常用的部件的创建函数,而不必直接调用
snd_device_new(),比如:
PCM ---- snd_pcm_new()
RAWMIDI -- snd_rawmidi_new()
9
CONTROL -- snd_ctl_create()
TIMER -- snd_timer_new()
INFO -- snd_card_proc_new()
JACK -- snd_jack_new()
2.1.5. 第五步,注册声卡
[c-sharp] view plain copy
1. err = snd_card_register(card);
2. if (err < 0) {
3. snd_card_free(card);
4. return err;
5. }
2.2. 一个实际的例子
我把/sound/arm/pxa2xx-ac97.c的部分代码贴上来:
[cpp] view plain copy
1. static int __devinit pxa2xx_ac97_probe(struct platform_device *dev)
2. {
3. struct snd_card *card;
4. struct snd_ac97_bus *ac97_bus;
5. struct snd_ac97_template ac97_template;
6. int ret;
7. pxa2xx_audio_ops_t *pdata = dev->rm_data;
8.
9. if (dev->id >= 0) {
10. dev_err(&dev->dev, "PXA2xx has only one AC97 port./n");
11. ret = -ENXIO;
12. goto err_dev;
13. }
14. ////(1)////
15. ret = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,
16. THIS_MODULE, 0, &card);
17. if (ret < 0)
18. goto err;
19.
20. card->dev = &dev->dev;
21. ////(3)////
10
22. strncpy(card->driver, dev->->name, sizeof(card->driver));
23.
24. ////(4)////
25. ret = pxa2xx_pcm_new(card, &pxa2xx_ac97_pcm_client, &pxa2xx_ac97_pcm);
26. if (ret)
27. goto err;
28. ////(2)////
29. ret = pxa2xx_ac97_hw_probe(dev);
30. if (ret)
31. goto err;
32.
33. ////(4)////
34. ret = snd_ac97_bus(card, 0, &pxa2xx_ac97_ops, NULL, &ac97_bus);
35. if (ret)
36. goto err_remove;
37. memset(&ac97_template, 0, sizeof(ac97_template));
38. ret = snd_ac97_mixer(ac97_bus, &ac97_template, &pxa2xx_ac97_ac97);
39. if (ret)
40. goto err_remove;
41. ////(3)////
42. snprintf(card->shortname, sizeof(card->shortname),
43. "%s", snd_ac97_get_short_name(pxa2xx_ac97_ac97));
44. snprintf(card->longname, sizeof(card->longname),
45. "%s (%s)", dev->->name, card->mixername);
46.
47. if (pdata && pdata->codec_pdata[0])
48. snd_ac97_dev_add_pdata(ac97_bus->codec[0], pdata->codec_pdata[0]);
49. snd_card_set_dev(card, &dev->dev);
50. ////(5)////
51. ret = snd_card_register(card);
52. if (ret == 0) {
53. platform_set_drvdata(dev, card);
54. return 0;
55. }
56.
57. err_remove:
58. pxa2xx_ac97_hw_remove(dev);
59. err:
60. if (card)
61. snd_card_free(card);
62. err_dev:
63. return ret;
64. }
65.
11
66. static int __devexit pxa2xx_ac97_remove(struct platform_device *dev)
67. {
68. struct snd_card *card = platform_get_drvdata(dev);
69.
70. if (card) {
71. snd_card_free(card);
72. platform_set_drvdata(dev, NULL);
73. pxa2xx_ac97_hw_remove(dev);
74. }
75.
76. return 0;
77. }
78.
79. static struct platform_driver pxa2xx_ac97_driver = {
80. .probe = pxa2xx_ac97_probe,
81. .remove = __devexit_p(pxa2xx_ac97_remove),
82. .driver = {
83. .name = "pxa2xx-ac97",
84. .owner = THIS_MODULE,
85. #ifdef CONFIG_PM
86. .pm = &pxa2xx_ac97_pm_ops,
87. #endif
88. },
89. };
90.
91. static int __init pxa2xx_ac97_init(void)
92. {
93. return platform_driver_register(&pxa2xx_ac97_driver);
94. }
95.
96. static void __exit pxa2xx_ac97_exit(void)
97. {
98. platform_driver_unregister(&pxa2xx_ac97_driver);
99. }
100.
101. module_init(pxa2xx_ac97_init);
102. module_exit(pxa2xx_ac97_exit);
103.
104. MODULE_AUTHOR("Nicolas Pitre");
105. MODULE_DESCRIPTION("AC97 driver for the Intel PXA2xx chip");
驱动程序通常由probe回调函数开始,对一下2.1中的步骤,是否有相似之处?
经过以上的创建步骤之后,声卡的逻辑结构如下图所示:
12
图 2.2.1 声卡的软件逻辑结构
下面的章节里我们分别讨论一下snd_card_create()和snd_card_register()这两
个函数。
3. snd_card_create()
snd_card_create()在/sound/core/init.c中定义。
[cpp] view plain copy
1. /**
2. * snd_card_create - create and initialize a soundcard s
tructure
3. * @idx: card index (address) [0 ... (SNDRV_CARDS-1)]
4. * @xid: card identification (ASCII string)
5. * @module: top level module for locking
6. * @extra_size: allocate this extra size after the main
soundcard structure
7. * @card_ret: the pointer to store the created card inst
ance
8. *
9. * Creates and initializes a soundcard structure.
10. *
11. * The function allocates snd_card instance via kzalloc
with the given
12. * space for the driver to use freely. The allocated s
truct is stored
13. * in the given card_ret pointer.
14. *
13
15. * Returns zero if successful or a negative error code.
16. */
17. int snd_card_create(int idx, const char *xid,
18. struct module *module, int extra_size,
19. struct snd_card **card_ret)
首先,根据extra_size参数的大小分配内存,该内存区可以作为芯片的专有数据
使用(见前面的介绍):
[c-sharp] view plain copy
1. card = kzalloc(sizeof(*card) + extra_size, GFP_KERNEL);
2. if (!card)
3. return -ENOMEM;
拷贝声卡的ID字符串:
[c-sharp] view plain copy
1. if (xid)
2. strlcpy(card->id, xid, sizeof(card->id));
如果传入的声卡编号为-1,自动分配一个索引编号:
[c-sharp] view plain copy
1. if (idx < 0) {
2. for (idx2 = 0; idx2 < SNDRV_CARDS; idx2++)
3. /* idx == -1 == 0xffff means: take any free slot
*/
4. if (~snd_cards_lock & idx & 1< 5. if (module_slot_match(module, idx2)) { 6. idx = idx2; 7. break; 8. } 9. } 10. } 11. if (idx < 0) { 12. for (idx2 = 0; idx2 < SNDRV_CARDS; idx2++) 13. /* idx == -1 == 0xffff means: take any free slot */ 14. if (~snd_cards_lock & idx & 1< 15. if (!slots[idx2] || !*slots[idx2]) { 14 16. idx = idx2; 17. break; 18. } 19. } 20. } 初始化snd_card结构中必要的字段: [c-sharp] view plain copy 1. card->number = idx; 2. card->module = module; 3. INIT_LIST_HEAD(&card->devices); 4. init_rwsem(&card->controls_rwsem); 5. rwlock_init(&card->ctl_files_rwlock); 6. INIT_LIST_HEAD(&card->controls); 7. INIT_LIST_HEAD(&card->ctl_files); 8. spin_lock_init(&card->files_lock); 9. INIT_LIST_HEAD(&card->files_list); 10. init_waitqueue_head(&card->shutdown_sleep); 11. #ifdef CONFIG_PM 12. mutex_init(&card->power_lock); 13. init_waitqueue_head(&card->power_sleep); 14. #endif 建立逻辑设备:Control [c-sharp] view plain copy 1. /* the control interface cannot be accessed from the user space until */ 2. /* snd_cards_bitmask and snd_cards are set with snd_card_ register */ 3. err = snd_ctl_create(card); 建立proc文件中的info节点:通常就是/proc/asound/card0 [c-sharp] view plain copy 1. err = snd_info_card_create(card); 把第一步分配的内存指针放入private_data字段中: [c-sharp] view plain copy 15 1. if (extra_size > 0) 2. card->private_data = (char *)card + sizeof(struct snd _card); 4. snd_card_register() snd_card_create()在/sound/core/init.c中定义。 [c-sharp] view plain copy 1. /** 2. * snd_card_register - register the soundcard 3. * @card: soundcard structure 4. * 5. * This function registers all the devices assigned to t he soundcard. 6. * Until calling this, the ALSA control interface is blo cked from the 7. * external accesses. Thus, you should call this functi on at the end 8. * of the initialization of the card. 9. * 10. * Returns zero otherwise a negative error code if the registrain failed. 11. */ 12. int snd_card_register(struct snd_card *card) 首先,创建sysfs下的设备: [c-sharp] view plain copy 1. if (!card->card_dev) { 2. card->card_dev = device_create(sound_class, card->dev , 3. MKDEV(0, 0), card, 4. "card%i", card->number); 5. if (IS_ERR(card->card_dev)) 6. card->card_dev = NULL; 7. } 其中,sound_class是在/sound/sound_core.c中创建的: [c-sharp] view plain copy 16 1. static char *sound_devnode(struct device *dev, mode_t *mo de) 2. { 3. if (MAJOR(dev->devt) == SOUND_MAJOR) 4. return NULL; 5. return kasprintf(GFP_KERNEL, "snd/%s", dev_name(dev)) ; 6. } 7. static int __init init_soundcore(void) 8. { 9. int rc; 10. 11. rc = init_oss_soundcore(); 12. if (rc) 13. return rc; 14. 15. sound_class = class_create(THIS_MODULE, "sound"); 16. if (IS_ERR(sound_class)) { 17. cleanup_oss_soundcore(); 18. return PTR_ERR(sound_class); 19. } 20. 21. sound_class->devnode = sound_devnode; 22. 23. return 0; 24. } 由此可见,声卡的class将会出现在文件系统的/sys/class/sound/下面,并且, sound_devnode()也决定了相应的设备节点也将会出现在/dev/snd/下面。 接下来的步骤,通过snd_device_register_all()注册所有挂在该声卡下的逻辑设 备,snd_device_register_all()实际上是通过snd_card的devices链表,遍历所 有的snd_device,并且调用snd_device的ops->dev_register()来实现各自设备 的注册的。 [c-sharp] view plain copy 1. if ((err = snd_device_register_all(card)) < 0) 2. return err; 最后就是建立一些相应的proc和sysfs下的文件或属性节点,代码就不贴了。 17 至此,整个声卡完成了建立过程。 3. Linux ALSA声卡驱动之三:PCM设备的创建 1. PCM是什么 PCM是英文Pulse-code modulation的缩写,中文译名是脉冲编码调制。我们 知道在现实生活中,人耳听到的声音是模拟信号,PCM就是要把声音从模拟转 换成数字信号的一种技术,他的原理简单地说就是利用一个固定的频率对模拟信 号进行采样,采样后的信号在波形上看就像一串连续的幅值不一的脉冲,把这些 脉冲的幅值按一定的精度进行量化,这些量化后的数值被连续地输出、传输、处 理或记录到存储介质中,所有这些组成了数字音频的产生过程。 图1.1 模拟音频的采样、量化 PCM信号的两个重要指标是采样频率和量化精度,目前,CD音频的采样频率 通常为44100Hz,量化精度是16bit。通常,播放音乐时,应用程序从存储介质 中读取音频数据(MP3、WMA、),经过解码后,最终送到音频驱动 程序中的就是PCM数据,反过来,在录音时,音频驱动不停地把采样所得的 PCM数据送回给应用程序,由应用程序完成压缩、存储等任务。所以,音频驱 动的两大核心任务就是: 18 playback 如何把用户空间的应用程序发过来的PCM数据,转化为人耳 可以辨别的模拟音频 capture 把mic拾取到得模拟信号,经过采样、量化,转换为PCM信 号送回给用户空间的应用程序 2. alsa-driver中的PCM中间层 ALSA已经为我们实现了功能强劲的PCM中间层,自己的驱动中只要实现一些 底层的需要访问硬件的函数即可。 要访问PCM的中间层代码,你首先要包含头文件 需要访问一些与 hw_param相关的函数,可能也要包含 每个声卡最多可以包含4个pcm的实例,每个pcm实例对应一个pcm设备文 件。pcm实例数量的这种限制源于linux设备号所占用的位大小,如果以后使用 64位的设备号,我们将可以创建更多的pcm实例。不过大多数情况下,在嵌入 式设备中,一个pcm实例已经足够了。 一个pcm实例由一个playback stream和一个capture stream组成,这两个 stream又分别有一个或多个substreams组成。 19 图2.1 声卡中的pcm结构 在嵌入式系统中,通常不会像图2.1中这么复杂,大多数情况下是一个声卡,一 个pcm实例,pcm下面有一个playback和capture stream,playback和capture 下面各自有一个substream。 下面一张图列出了pcm中间层几个重要的结构,他可以让我们从uml的角度看 一看这列结构的关系,理清他们之间的关系,对我们理解pcm中间层的实现方 式。 20 图2.2 pcm中间层的几个重要的结构体的关系 图 snd_pcm是挂在snd_card下面的一个snd_device snd_pcm中的字段:streams[2],该数组中的两个元素指向两个 snd_pcm_str结构,分别代表playback stream和capture stream snd_pcm_str中的substream字段,指向snd_pcm_substream结构 snd_pcm_substream是pcm中间层的核心,绝大部分任务都是在 substream中处理,尤其是他的ops(snd_pcm_ops)字段,许多user 空间的应用程序通过alsa-lib对驱动程序的请求都是由该结构中的函数处 理。它的runtime字段则指向snd_pcm_runtime结构,snd_pcm_runtime 记录这substream的一些重要的软件和硬件运行环境和参数。 3. 新建一个pcm alsa-driver的中间层已经为我们提供了新建pcm的api: 21 int snd_pcm_new(struct snd_card *card, const char *id, int device, int playback_count, int capture_count, struct snd_pcm ** rpcm); 参数device 表示目前创建的是该声卡下的第几个pcm,第一个pcm设备从0 开始。 参数playback_count 表示该pcm将会有几个playback substream。 参数capture_count 表示该pcm将会有几个capture substream。 另一个用于设置pcm操作函数接口的api: void snd_pcm_set_ops(struct snd_pcm *pcm, int direction, struct snd_pcm_ops *ops); 新建一个pcm可以用下面一张新建pcm的调用的序列图进行描述: 图3.1 新建pcm的序列图 snd_card_create pcm是声卡下的一个设备(部件),所以第一步是 要创建一个声卡 22 snd_pcm_new 调用该api创建一个pcm,才该api中会做以下事情 o o o 如果有,建立playback stream,相应的substream也同时建立 如果有,建立capture stream,相应的substream也同时建立 调用snd_device_new()把该pcm挂到声卡中,参数ops中的 dev_register字段指向了函数snd_pcm_dev_register,这个回调 函数会在声卡的注册阶段被调用。 snd_pcm_set_ops 设置操作该pcm的控制/操作接口函数,参数中的 snd_pcm_ops结构中的函数通常就是我们驱动要实现的函数 snd_card_register 注册声卡,在这个阶段会遍历声卡下的所有逻辑设 备,并且调用各设备的注册回调函数,对于pcm,就是第二步提到的 snd_pcm_dev_register函数,该回调函数建立了和用户空间应用程序 (alsa-lib)通信所用的设备文件节点:/dev/snd/pcmCxxDxxp和 /dev/snd/pcmCxxDxxc 4. 设备文件节点的建立(dev/snd/pcmCxxDxxp、pcmCxxDxxc) 4.1 struct snd_minor 每个snd_minor结构体保存了声卡下某个逻辑设备的上下文信息,他在逻辑设 备建立阶段被填充,在逻辑设备被使用时就可以从该结构体中得到相应的信息。 pcm设备也不例外,也需要使用该结构体。该结构体在include/sound/core.h中 定义。 [c-sharp] view plain copy 1. struct snd_minor { 2. int type; /* SNDRV_DEVICE_TYPE_XXX */ 3. int card; /* card number */ 4. int device; /* device number */ 5. const struct file_operations *f_ops; /* file opera tions */ 6. void *private_data; /* private data for f_ops->op en */ 7. struct device *dev; /* device for sysfs */ 8. }; 在sound/sound.c中定义了一个snd_minor指针的全局数组: [c-sharp] view plain copy 1. static struct snd_minor *snd_minors[256]; 23 前面说过,在声卡的注册阶段(snd_card_register),会调用pcm的回调函数 snd_pcm_dev_register(),这个函数里会调用函数 snd_register_device_for_dev(): [c-sharp] view plain copy 1. static int snd_pcm_dev_register(struct snd_device *device ) 2. { 3. ...... 4. 5. /* register pcm */ 6. err = snd_register_device_for_dev(devtype, pcm->card, 7. pcm->device, 8. &snd_pcm_f_ops[cidx], 9. pcm, str, dev); 10. ...... 11. } 我们再进入snd_register_device_for_dev(): [c-sharp] view plain copy 1. int snd_register_device_for_dev(int type, struct snd_card *card, int dev, 2. const struct file_operations *f_ops, 3. void *private_data, 4. const char *name, struct device *device) 5. { 6. int minor; 7. struct snd_minor *preg; 8. 9. if (snd_BUG_ON(!name)) 10. return -EINVAL; 11. preg = kmalloc(sizeof *preg, GFP_KERNEL); 12. if (preg == NULL) 13. return -ENOMEM; 14. preg->type = type; 15. preg->card = card ? card->number : -1; 16. preg->device = dev; 17. preg->f_ops = f_ops; 18. preg->private_data = private_data; 24 19. mutex_lock(&sound_mutex); 20. #ifdef CONFIG_SND_DYNAMIC_MINORS 21. minor = snd_find_free_minor(); 22. #else 23. minor = snd_kernel_minor(type, card, dev); 24. if (minor >= 0 && snd_minors[minor]) 25. minor = -EBUSY; 26. #endif 27. if (minor < 0) { 28. mutex_unlock(&sound_mutex); 29. kfree(preg); 30. return minor; 31. } 32. snd_minors[minor] = preg; 33. preg->dev = device_create(sound_class, device, MKDEV (major, minor), 34. private_data, "%s", name); 35. if (IS_ERR(preg->dev)) { 36. snd_minors[minor] = NULL; 37. mutex_unlock(&sound_mutex); 38. minor = PTR_ERR(preg->dev); 39. kfree(preg); 40. return minor; 41. } 42. 43. mutex_unlock(&sound_mutex); 44. return 0; 45. } 首先,分配并初始化一个snd_minor结构中的各字段 o type: SNDRV_DEVICE_TYPE_PCM_PLAYBACK/SNDRV_DEVICE_ TYPE_PCM_CAPTURE o o o o card: card的编号 device:pcm实例的编号,大多数情况为0 f_ops:snd_pcm_f_ops private_data:指向该pcm的实例 根据type,card和pcm的编号,确定数组的索引值minor,minor也作 为pcm设备的此设备号 把该snd_minor结构的地址放入全局数组snd_minors[minor]中 最后,调用device_create创建设备节点 25 4.2 设备文件的建立 在4.1节的最后,设备文件已经建立,不过4.1节的重点在于snd_minors数组 的赋值过程,在本节中,我们把重点放在设备文件中。 回到pcm的回调函数snd_pcm_dev_register()中: [c-sharp] view plain copy 1. static int snd_pcm_dev_register(struct snd_device *device) 2. { 3. int cidx, err; 4. char str[16]; 5. struct snd_pcm *pcm; 6. struct device *dev; 7. 8. pcm = device->device_data; 9. ...... 10. for (cidx = 0; cidx < 2; cidx++) { 11. ...... 12. switch (cidx) { 13. case SNDRV_PCM_STREAM_PLAYBACK: 14. sprintf(str, "pcmC%iD%ip", pcm->card->number, pcm->de vice); 15. devtype = SNDRV_DEVICE_TYPE_PCM_PLAYBACK; 16. break; 17. case SNDRV_PCM_STREAM_CAPTURE: 18. sprintf(str, "pcmC%iD%ic", pcm->card->number, pcm->de vice); 19. devtype = SNDRV_DEVICE_TYPE_PCM_CAPTURE; 20. break; 21. } 22. /* device pointer to use, pcm->dev takes precedence if 23. * it is assigned, otherwise fall back to card's device 24. * if possible */ 25. dev = pcm->dev; 26. if (!dev) 27. dev = snd_card_get_device_link(pcm->card); 28. /* register pcm */ 29. err = snd_register_device_for_dev(devtype, pcm->card, 30. pcm->device, 31. &snd_pcm_f_ops[cidx], 32. pcm, str, dev); 33. ...... 26 34. } 35. ...... 36. } 以上代码我们可以看出,对于一个pcm设备,可以生成两个设备文件,一个用 于playback,一个用于capture,代码中也确定了他们的命名规则: playback -- pcmCxDxp,通常系统中只有一各声卡和一个pcm,它就 是pcmC0D0p capture -- pcmCxDxc,通常系统中只有一各声卡和一个pcm,它就是 pcmC0D0c snd_pcm_f_ops snd_pcm_f_ops是一个标准的文件系统file_operations结构数组,它的定义在 sound/core/pcm_native.c中: [c-sharp] view plain copy 1. const struct file_operations snd_pcm_f_ops[2] = { 2. { 3. .owner = THIS_MODULE, 4. .write = snd_pcm_write, 5. .aio_write = snd_pcm_aio_write, 6. .open = snd_pcm_playback_open, 7. .release = snd_pcm_release, 8. .llseek = no_llseek, 9. .poll = snd_pcm_playback_poll, 10. .unlocked_ioctl = snd_pcm_playback_ioctl, 11. .compat_ioctl = snd_pcm_ioctl_compat, 12. .mmap = snd_pcm_mmap, 13. .fasync = snd_pcm_fasync, 14. .get_unmapped_area = snd_pcm_get_unmapped_are a, 15. }, 16. { 17. .owner = THIS_MODULE, 18. .read = snd_pcm_read, 19. .aio_read = snd_pcm_aio_read, 20. .open = snd_pcm_capture_open, 21. .release = snd_pcm_release, 27 22. .llseek = no_llseek, 23. .poll = snd_pcm_capture_poll, 24. .unlocked_ioctl = snd_pcm_capture_ioctl, 25. .compat_ioctl = snd_pcm_ioctl_compat, 26. .mmap = snd_pcm_mmap, 27. .fasync = snd_pcm_fasync, 28. .get_unmapped_area = snd_pcm_get_unmapped_are a, 29. } 30. }; snd_pcm_f_ops作为snd_register_device_for_dev的参数被传入,并被记录在 snd_minors[minor]中的字段f_ops中。最后,在snd_register_device_for_dev 中创建设备节点: [c-sharp] view plain copy 1. snd_minors[minor] = preg; 2. preg->dev = device_create(sound_class, device, MKDEV(majo r, minor), 3. private_data, "%s", name); 4.3 层层深入,从应用程序到驱动层pcm 4.3.1 字符设备注册 在sound/core/sound.c中有alsa_sound_init()函数,定义如下: [c-sharp] view plain copy 1. static int __init alsa_sound_init(void) 2. { 3. snd_major = major; 4. snd_ecards_limit = cards_limit; 5. if (register_chrdev(major, "alsa", &snd_fops)) { 6. snd_printk(KERN_ERR "unable to register native ma jor device number %d/n", major); 7. return -EIO; 8. } 9. if (snd_info_init() < 0) { 10. unregister_chrdev(major, "alsa"); 11. return -ENOMEM; 12. } 13. snd_info_minor_register(); 14. return 0; 28 15. } register_chrdev中的参数major与之前创建pcm设备是device_create时的 major是同一个,这样的结果是,当应用程序open设备文件/dev/snd/pcmCxDxp 时,会进入snd_fops的open回调函数,我们将在下一节中讲述open的过程。 4.3.2 打开pcm设备 从上一节中我们得知,open一个pcm设备时,将会调用snd_fops的open回 调函数,我们先看看snd_fops的定义: [c-sharp] view plain copy 1. static const struct file_operations snd_fops = 2. { 3. .owner = THIS_MODULE, 4. .open = snd_open 5. }; 跟入snd_open函数,它首先从inode中取出此设备号,然后以次设备号为索引, 从snd_minors全局数组中取出当初注册pcm设备时填充的snd_minor结构(参 看4.1节的内容),然后从snd_minor结构中取出pcm设备的f_ops,并且把 file->f_op替换为pcm设备的f_ops,紧接着直接调用pcm设备的f_ops->open(), 然后返回。因为file->f_op已经被替换,以后,应用程序的所有read/write/ioctl 调用都会进入pcm设备自己的回调函数中,也就是4.2节中提到的 snd_pcm_f_ops结构中定义的回调。 [c-sharp] view plain copy 1. static int snd_open(struct inode *inode, struct file *fil e) 2. { 3. unsigned int minor = iminor(inode); 4. struct snd_minor *mptr = NULL; 5. const struct file_operations *old_fops; 6. int err = 0; 7. 8. if (minor >= ARRAY_SIZE(snd_minors)) 9. return -ENODEV; 10. mutex_lock(&sound_mutex); 11. mptr = snd_minors[minor]; 12. if (mptr == NULL) { 29 13. mptr = autoload_device(minor); 14. if (!mptr) { 15. mutex_unlock(&sound_mutex); 16. return -ENODEV; 17. } 18. } 19. old_fops = file->f_op; 20. file->f_op = fops_get(mptr->f_ops); 21. if (file->f_op == NULL) { 22. file->f_op = old_fops; 23. err = -ENODEV; 24. } 25. mutex_unlock(&sound_mutex); 26. if (err < 0) 27. return err; 28. 29. if (file->f_op->open) { 30. err = file->f_op->open(inode, file); 31. if (err) { 32. fops_put(file->f_op); 33. file->f_op = fops_get(old_fops); 34. } 35. } 36. fops_put(old_fops); 37. return err; 38. } 下面的序列图展示了应用程序如何最终调用到snd_pcm_f_ops结构中的回调函 数: 30 图4.3.2.1 应用程序操作pcm设备 4. Linux ALSA声卡驱动之四:Control设备的创建 Control接口 Control接口主要让用户空间的应用程序(alsa-lib)可以访问和控制音频codec 芯片中的多路开关,滑动控件等。对于Mixer(混音)来说,Control接口显得 尤为重要,从ALSA 0.9.x版本开始,所有的mixer工作都是通过control接口的 API来实现的。 ALSA已经为AC97定义了完整的控制接口模型,如果你的Codec芯片只支持 AC97接口,你可以不用关心本节的内容。 的controls,请在代码中包含该头文件。 Controls的定义 要自定义一个Control,我们首先要定义3各回调函数:info,get和put。然后, 定义一个snd_kcontrol_new结构: [c-sharp] view plain copy 31 1. static struct snd_kcontrol_new my_control __devinitdata = { 2. .iface = SNDRV_CTL_ELEM_IFACE_MIXER, 3. .name = "PCM Playback Switch", 4. .index = 0, 5. .access = SNDRV_CTL_ELEM_ACCESS_READWRITE, 6. .private_value = 0xffff, 7. .info = my_control_info, 8. .get = my_control_get, 9. .put = my_control_put 10. }; 11. iface字段指出了control的类型,alsa定义了几种类型 (SNDDRV_CTL_ELEM_IFACE_XXX),常用的类型是MIXER,当然 也可以定义属于全局的CARD类型,也可以定义属于某类设备的类型, 例如HWDEP,PCMRAWMIDI,TIMER等,这时需要在device和 subdevice字段中指出卡的设备逻辑编号。 12. 13. name字段是该control的名字,从ALSA 0.9.x开始,control的名字是变 得比较重要,因为control的作用是按名字来归类的。ALSA已经预定义 了一些control的名字,我们再Control Name一节详细讨论。 14. 15. index字段用于保存该control的在该卡中的编号。如果声卡中有不止一 个codec,每个codec中有相同名字的control,这时我们可以通过index 来区分这些controls。当index为0时,则可以忽略这种区分策略。 16. 17. access字段包含了该control的访问类型。每一个bit代表一种访问类型, 这些访问类型可以多个“或”运算组合在一起。 18. 19. private_value字段包含了一个任意的长整数类型值。该值可以通过info, get,put这几个回调函数访问。你可以自己决定如何使用该字段,例如可 以把它拆分成多个位域,又或者是一个指针,指向某一个数据结构。 20. 21. tlv字段为该control提供元数据。 Control的名字 control的名字需要遵循一些标准,通常可以分成3部分来定义control的名字: 源--方向--功能。 32 源,可以理解为该control的输入端,alsa已经预定义了一些常用的源, 例如:Master,PCM,CD,Line等等。 方向,代表该control的数据流向,例如:Playback,Capture,Bypass, Bypass Capture等等,也可以不定义方向,这时表示该Control是双向的 (playback和capture)。 功能,根据control的功能,可以是以下字符串:Switch,Volume,Route 等等。 也有一些命名上的特例: 全局的capture和playback "Capture Source","Capture Volume", "Capture Switch",它们用于全局的capture source,switch和volume。 同理,"Playback Volume","Playback Switch",它们用于全局的输出 switch和volume。 Tone-controles 音调控制的开关和音量命名为:Tone Control - XXX, 例如,"Tone Control - Switch","Tone Control - Bass","Tone Control - Center"。 3D controls 3D控件的命名规则:,"3D Control - Switch","3D Control - Center","3D Control - Space"。 Mic boost 麦克风音量加强控件命名为:"Mic Boost"或"Mic Boost(6dB)"。 访问标志(ACCESS Flags) Access字段是一个bitmask,它保存了改control的访问类型。默认的访问类型 是:SNDDRV_CTL_ELEM_ACCESS_READWRITE,表明该control支持读和 写操作。如果access字段没有定义(.access==0),此时也认为是READWRITE 类型。 如果是一个只读control,access应该设置为: SNDDRV_CTL_ELEM_ACCESS_READ,这时,我们不必定义put回调函数。 类似地,如果是只写control,access应该设置为: SNDDRV_CTL_ELEM_ACCESS_WRITE,这时,我们不必定义get回调函数。 33 如果control的值会频繁地改变(例如:电平表),我们可以使用VOLATILE类 型,这意味着该control会在没有通知的情况下改变,应用程序应该定时地查询 该control的值。 回调函数 info回调函数 info回调函数用于获取control的详细信息。它的主要工作就是填充通过参数传 入的snd_ctl_elem_info对象,以下例子是一个具有单个元素的boolean型 control的info回调: info回调函数用于获取control的详细信息。它的主要工作就是填充通过参数传 入的snd_ctl_elem_info对象,以下例子是一个具有单个元素的boolean型 control的info回调: [c-sharp] view plain copy 1. static int snd_myctl_mono_info(struct snd_kcontrol *kcont rol, 2. struct snd_ctl_elem_info *uinfo) 3. { 4. uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN; 5. uinfo->count = 1; 6. uinfo-> = 0; 7. uinfo-> = 1; 8. return 0; 9. } type字段指出该control的值类型,值类型可以是BOOLEAN, INTEGER, ENUMERATED, BYTES,IEC958和INTEGER64之一。count字段指出了改 control中包含有多少个元素单元,比如,立体声的音量control左右两个声道的 音量值,它的count字段等于2。value字段是一个联合体(union),value的 内容和control的类型有关。其中,boolean和integer类型是相同的。 ENUMERATED类型有些特殊。它的value需要设定一个字符串和字符串的索 引,请看以下例子: 34 [c-sharp] view plain copy 1. static int snd_myctl_enum_info(struct snd_kcontrol *kcont rol, 2. struct snd_ctl_elem_info *uinfo) 3. { 4. static char *texts[4] = { 5. "First", "Second", "Third", "Fourth" 6. }; 7. uinfo->type = SNDRV_CTL_ELEM_TYPE_ENUMERATED; 8. uinfo->count = 1; 9. uinfo-> = 4; 10. if (uinfo-> > 3) 11. uinfo-> = 3; 12. strcpy(uinfo->, 13. texts[uinfo->]); 14. return 0; 15. } alsa已经为我们实现了一些通用的info回调函数,例如: snd_ctl_boolean_mono_info(),snd_ctl_boolean_stereo_info()等等。 get回调函数 该回调函数用于读取control的当前值,并返回给用户空间的应用程序。 [c-sharp] view plain copy 1. static int snd_myctl_get(struct snd_kcontrol *kcontrol, 2. struct snd_ctl_elem_value *ucontrol) 3. { 4. struct mychip *chip = snd_kcontrol_chip(kcontrol); 5. ucontrol->[0] = get_some_value(chi p); 6. return 0; 7. } value字段的赋值依赖于control的类型(如同info回调)。很多声卡的驱动利 用它存储硬件寄存器的地址、bit-shift和bit-mask,这时,private_value字段可 以按以下例子进行设置: 35 .private_value = reg | (shift << 16) | (mask << 24); 然后,get回调函数可以这样实现: static int snd_sbmixer_get_single(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { int reg = kcontrol->private_value & 0xff; int shift = (kcontrol->private_value >> 16) & 0xff; int mask = (kcontrol->private_value >> 24) & 0xff; .... //根据以上的值读取相应寄存器的值并填入value中 } 如果control的count字段大于1,表示control有多个元素单元,get回调函数 也应该为value填充多个数值。 put回调函数 put回调函数用于把应用程序的控制值设置到control中。 [c-sharp] view plain copy 1. static int snd_myctl_put(struct snd_kcontrol *kcontrol, 2. struct snd_ctl_elem_value *ucontrol) 3. { 4. struct mychip *chip = snd_kcontrol_chip(kcontrol); 5. int changed = 0; 6. if (chip->current_value != 7. ucontrol->[0]) { 8. change_current_value(chip, 9. ucontrol->[0]); 10. changed = 1; 11. } 12. return changed; 13. } 36 如上述例子所示,当control的值被改变时,put回调必须要返回1,如果值没有 被改变,则返回0。如果发生了错误,则返回一个负数的错误号。 和get回调一样,当control的count大于1时,put回调也要处理多个control 中的元素值。 创建Controls 当把以上讨论的内容都准备好了以后,我们就可以创建我们自己的control了。 alsa-driver为我们提供了两个用于创建control的API: snd_ctl_new1() snd_ctl_add() 我们可以用以下最简单的方式创建control: [c-sharp] view plain copy 1. err = snd_ctl_add(card, snd_ctl_new1(&my_control, chip)); 2. if (err < 0) 3. return err; 在这里,my_control是一个之前定义好的snd_kcontrol_new对象,chip对象将 会被赋值在kcontrol->private_data字段,该字段可以在回调函数中访问。 snd_ctl_new1()会分配一个新的snd_kcontrol实例,并把my_control中相应的 值复制到该实例中,所以,在定义my_control时,通常我们可以加上__devinitdata 前缀。snd_ctl_add则把该control绑定到声卡对象card当中。 37 元数据(Metadata) 很多mixer control需要提供以dB为单位的信息,我们可以使用 DECLARE_TLV_xxx宏来定义一些包含这种信息的变量,然后把control的tlv.p 字段指向这些变量,最后,在access字段中加上 SNDRV_CTL_ELEM_ACCESS_TLV_READ标志,就像这样: static DECLARE_TLV_DB_SCALE(db_scale_my_control, -4050, 150, 0); static struct snd_kcontrol_new my_control __devinitdata = { ... .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | SNDRV_CTL_ELEM_ACCESS_TLV_READ, ... .tlv.p = db_scale_my_control, }; DECLARE_TLV_DB_SCALE宏定义的mixer control,它所代表的值按一个固 定的dB值的步长变化。该宏的第一个参数是要定义变量的名字,第二个参数是 最小值,以0.01dB为单位。第三个参数是变化的步长,也是以0.01dB为单位。 如果该control处于最小值时会做出mute时,需要把第四个参数设为1。 DECLARE_TLV_DB_LINEAR宏定义的mixer control,它的输出随值的变化而 线性变化。 该宏的第一个参数是要定义变量的名字,第二个参数是最小值,以 0.01dB为单位。第二个参数是最大值,以0.01dB为单位。如果该control处于 最小值时会做出mute时,需要把第二个参数设为TLV_DB_GAIN_MUTE。 这两个宏实际上就是定义一个整形数组,所谓tlv,就是Type-Lenght-Value的 意思,数组的第0各元素代表数据的类型,第1个元素代表数据的长度,第三 个元素和之后的元素保存该变量的数据。 38 Control设备的建立 Control设备和PCM设备一样,都属于声卡下的逻辑设备。用户空间的应用程 序通过alsa-lib访问该Control设备,读取或控制control的控制状态,从而达到 控制音频Codec进行各种Mixer等控制操作。 Control设备的创建过程大体上和PCM设备的创建过程相同。详细的创建过程 可以参考本博的另一篇文章:Linux音频驱动之三:PCM设备的创建。下面我 们只讨论有区别的地方。 我们需要在我们的驱动程序初始化时主动调用snd_pcm_new()函数创建pcm设 备,而control设备则在snd_card_create()内被创建,snd_card_create()通过调 用snd_ctl_create()函数创建control设备节点。所以我们无需显式地创建control 设备,只要建立声卡,control设备被自动地创建。 和pcm设备一样,control设备的名字遵循一定的规则:controlCxx,这里的xx 代表声卡的编号。我们也可以通过代码正是这一点,下面的是 snd_ctl_dev_register()函数的代码: [c-sharp] view plain copy 1. /* 2. * registration of the control device 3. */ 4. static int snd_ctl_dev_register(struct snd_device *device ) 5. { 6. struct snd_card *card = device->device_data; 7. int err, cardnum; 8. char name[16]; 9. 10. if (snd_BUG_ON(!card)) 11. return -ENXIO; 12. cardnum = card->number; 13. if (snd_BUG_ON(cardnum < 0 || cardnum >= SNDRV_CARDS )) 14. return -ENXIO; 15. /* control设备的名字 */ 16. sprintf(name, "controlC%i", cardnum); 17. if ((err = snd_register_device(SNDRV_DEVICE_TYPE_CON TROL, card, -1, 39 18. &snd_ctl_f_ops, card, name)) < 0) 19. return err; 20. return 0; 21. } snd_ctl_dev_register()函数会在snd_card_register()中,即声卡的注册阶段被调 用。注册完成后,control设备的相关信息被保存在snd_minors[]数组中,用control 设备的此设备号作索引,即可在snd_minors[]数组中找出相关的信息。注册完成 后的数据结构关系可以用下图进行表述: control设备的操作函数入口 用户程序需要打开control设备时,驱动程序通过snd_minors[]全局数组和此设 备号,可以获得snd_ctl_f_ops结构中的各个回调函数,然后通过这些回调函数 访问control中的信息和数据(最终会调用control的几个回调函数get,put,info)。 详细的代码我就不贴了,大家可以读一下代码:/sound/core/control.c。 5. Linux ALSA声卡驱动之五:移动设备中的ALSA(ASoC) 40 1. ASoC的由来 ASoC--ALSA System on Chip ,是建立在标准ALSA驱动层上,为了更好地支 持嵌入式处理器和移动设备中的音频Codec的一套软件体系。在ASoc出现之 前,内核对于SoC中的音频已经有部分的支持,不过会有一些局限性: Codec驱动与SoC CPU的底层耦合过于紧密,这种不理想会导致代码 的重复,例如,仅是wm8731的驱动,当时Linux中有分别针对4个平 台的驱动代码。 音频事件没有标准的方法来通知用户,例如耳机、麦克风的插拔和检测, 这些事件在移动设备中是非常普通的,而且通常都需要特定于机器的代码 进行重新对音频路劲进行配置。 当进行播放或录音时,驱动会让整个codec处于上电状态,这对于PC 没问题,但对于移动设备来说,这意味着浪费大量的电量。同时也不支持 通过改变过取样频率和偏置电流来达到省电的目的。 ASoC正是为了解决上述种种问题而提出的,目前已经被整合至内核的代码树 中:sound/soc。ASoC不能单独存在,他只是建立在标准ALSA驱动上的一个, 它必须和标准的ALSA驱动框架相结合才能工作。 2. 硬件架构 通常,就像软件领域里的抽象和重用一样,嵌入式设备的音频系统可以被划分为 板载硬件(Machine)、Soc(Platform)、Codec三大部分,如下图所示: 图2.1 音频系统结构 41 Machine 是指某一款机器,可以是某款设备,某款开发板,又或者是某 款智能手机,由此可以看出Machine几乎是不可重用的,每个Machine 上的硬件实现可能都不一样,CPU不一样,Codec不一样,音频的输入、 输出设备也不一样,Machine为CPU、Codec、输入输出设备提供了一 个载体。 Platform 一般是指某一个SoC平台,比如pxaxxx,s3cxxxx,omapxxx 等等,与音频相关的通常包含该SoC中的时钟、DMA、I2S、PCM等等, 只要指定了SoC,那么我们可以认为它会有一个对应的Platform,它只 与SoC相关,与Machine无关,这样我们就可以把Platform抽象出来, 使得同一款SoC不用做任何的改动,就可以用在不同的Machine中。实 际上,把Platform认为是某个SoC更好理解。 Codec 字面上的意思就是编解码器,Codec里面包含了I2S接口、D/A、 A/D、Mixer、PA(功放),通常包含多种输入(Mic、Line-in、I2S、PCM) 和多个输出(耳机、喇叭、听筒,Line-out),Codec和Platform一样, 是可重用的部件,同一个Codec可以被不同的Machine使用。嵌入式 Codec通常通过I2C对内部的寄存器进行控制。 3. 软件架构 在软件层面,ASoC也把嵌入式设备的音频系统同样分为3大部分,Machine, Platform和Codec。 Codec驱动 ASoC中的一个重要设计原则就是要求Codec驱动是平台 无关的,它包含了一些音频的控件(Controls),音频接口,DAMP(动 态音频电源管理)的定义和某些Codec IO功能。为了保证硬件无关性, 任何特定于平台和机器的代码都要移到Platform和Machine驱动中。所 有的Codec驱动都要提供以下特性: o o o o Codec DAI 和 PCM的配置信息; Codec的IO控制方式(I2C,SPI等); Mixer和其他的音频控件; Codec的ALSA音频操作接口; 必要时,也可以提供以下功能: 42 o o o DAPM描述信息; DAPM事件处理程序; DAC数字静音控制 Platform驱动 它包含了该SoC平台的音频DMA和音频接口的配置和 控制(I2S,PCM,AC97等等);它也不能包含任何与板子或机器相关 的代码。 Machine驱动 Machine驱动负责处理机器特有的一些控件和音频事件 (例如,当播放音频时,需要先行打开一个放大器);单独的Platform 和Codec驱动是不能工作的,它必须由Machine驱动把它们结合在一起 才能完成整个设备的音频处理工作。 4. 数据结构 整个ASoC是由一些列数据结构组成,要搞清楚ASoC的工作机理,必须要理 解这一系列数据结构之间的关系和作用,下面的关系图展示了ASoC中重要的 数据结构之间的关联方式: 43 图4.1 Kernel-2.6.35-ASoC中各个结构的静态关系 ASoC把声卡实现为一个Platform Device,然后利用Platform_device结构中 的dev字段:a,它实际上指向一个snd_soc_device结构。可以认为 snd_soc_device是整个ASoC数据结构的根本,由他开始,引出一系列的数据 结构用于表述音频的各种特性和功能。snd_soc_device结构引出了 snd_soc_card和soc_codec_device两个结构,然后snd_soc_card又引出了 snd_soc_platform、snd_soc_dai_link和snd_soc_codec结构。如上所述,ASoC 被划分为Machine、Platform和Codec三大部分,如果从这些数据结构看来, snd_codec_device和snd_soc_card代表着Machine驱动,snd_soc_platform 则代表着Platform驱动,snd_soc_codec和soc_codec_device则代表了Codec 驱动,而snd_soc_dai_link则负责连接Platform和Codec。 44 5. 3.0版内核对ASoC的改进 本来写这篇文章的时候参考的内核版本是2.6.35,不过有CSDN的朋友提出在 内核版本3.0版本中,ASoC做了较大的变化。故特意下载了3.0的代码,发现 确实有所变化,下面先贴出数据结构的静态关系图: 图5.1 Kernel 3.0中的ASoC数据结构 由上图我们可以看出,3.0中的数据结构更为合理和清晰,取消了 snd_soc_device结构,直接用snd_soc_card取代了它,并且强化了 snd_soc_pcm_runtime的作用,同时还增加了另外两个数据结构 snd_soc_codec_driver和snd_soc_platform_driver,用于明确代表Codec驱动 和Platform驱动。 45 后续的章节中将会逐一介绍Machine和Platform以及Codec驱动的工作细节和 关联。 1. Linux ALSA声卡驱动之六:ASoC架构中的Machine 前面一节的内容我们提到,ASoC被分为Machine、Platform和Codec三大部 分,其中的Machine驱动负责Platform和Codec之间的耦合以及部分和设备或 板子特定的代码,再次引用上一节的内容:Machine驱动负责处理机器特有的一 些控件和音频事件(例如,当播放音频时,需要先行打开一个放大器);单独的 Platform和Codec驱动是不能工作的,它必须由Machine驱动把它们结合在一 起才能完成整个设备的音频处理工作。 ASoC的一切都从Machine驱动开始,包括声卡的注册,绑定Platform和Codec 驱动等等,下面就让我们从Machine驱动开始讨论吧。 1. 注册Platform Device ASoC把声卡注册为Platform Device,我们以装配有WM8994的一款Samsung 的开发板SMDK为例子做说明,WM8994是一颗Wolfson生产的多功能Codec 芯片。 代码的位于:/sound/soc/samsung/smdk_wm8994.c,我们关注模块的初始化函 数: [cpp] view plain copy 1. static int __init smdk_audio_init(void) 2. { 3. int ret; 4. 5. smdk_snd_device = platform_device_alloc("soc-audio", -1); 6. if (!smdk_snd_device) 7. return -ENOMEM; 8. 9. platform_set_drvdata(smdk_snd_device, &smdk); 10. 11. ret = platform_device_add(smdk_snd_device); 12. if (ret) 46 13. platform_device_put(smdk_snd_device); 14. 15. return ret; 16. } 由此可见,模块初始化时,注册了一个名为soc-audio的Platform设备,同时把 smdk设到platform_device结构的a字段中,这里引出了第一个数据 结构snd_soc_card的实例smdk,他的定义如下: [cpp] view plain copy 1. static struct snd_soc_dai_link smdk_dai[] = { 2. { /* Primary DAI i/f */ 3. .name = "WM8994 AIF1", 4. .stream_name = "Pri_Dai", 5. .cpu_dai_name = "samsung-i2s.0", 6. .codec_dai_name = "wm8994-aif1", 7. .platform_name = "samsung-audio", 8. .codec_name = "wm8994-codec", 9. .init = smdk_wm8994_init_paiftx, 10. .ops = &smdk_ops, 11. }, { /* Sec_Fifo Playback i/f */ 12. .name = "Sec_FIFO TX", 13. .stream_name = "Sec_Dai", 14. .cpu_dai_name = "samsung-i2s.4", 15. .codec_dai_name = "wm8994-aif1", 16. .platform_name = "samsung-audio", 17. .codec_name = "wm8994-codec", 18. .ops = &smdk_ops, 19. }, 20. }; 21. 22. static struct snd_soc_card smdk = { 23. .name = "SMDK-I2S", 24. .owner = THIS_MODULE, 25. .dai_link = smdk_dai, 26. .num_links = ARRAY_SIZE(smdk_dai), 27. }; 通过snd_soc_card结构,又引出了Machine驱动的另外两个个数据结构: snd_soc_dai_link(实例:smdk_dai[] ) snd_soc_ops(实例:smdk_ops ) 47 其中,snd_soc_dai_link中,指定了Platform、Codec、codec_dai、cpu_dai 的名字,稍后Machine驱动将会利用这些名字去匹配已经在系统中注册的 platform,codec,dai,这些注册的部件都是在另外相应的Platform驱动和Codec 驱动的代码文件中定义的,这样看来,Machine驱动的设备初始化代码无非就是 选择合适Platform和Codec以及dai,用他们填充以上几个数据结构,然后注 册Platform设备即可。当然还要实现连接Platform和Codec的dai_link对应的 ops实现,本例就是smdk_ops,它只实现了hw_params函数: smdk_hw_params。 2. 注册Platform Driver 按照Linux的设备模型,有platform_device,就一定会有platform_driver。ASoC 的platform_driver在以下文件中定义:sound/soc/soc-core.c。 还是先从模块的入口看起: [cpp] view plain copy 1. static int __init snd_soc_init(void) 2. { 3. ...... 4. return platform_driver_register(&soc_driver); 5. } soc_driver的定义如下: [cpp] view plain copy 1. /* ASoC platform driver */ 2. static struct platform_driver soc_driver = { 3. .driver = { 4. .name = "soc-audio", 5. .owner = THIS_MODULE, 6. .pm = &soc_pm_ops, 7. }, 8. .probe = soc_probe, 9. .remove = soc_remove, 10. }; 我们看到platform_driver的name字段为soc-audio,正好与platform_device 中的名字相同,按照Linux的设备模型,platform总线会匹配这两个名字相同的 device和driver,同时会触发soc_probe的调用,它正是整个ASoC驱动初始 化的入口。 48 3. 初始化入口soc_probe() soc_probe函数本身很简单,它先从platform_device参数中取出snd_soc_card, 然后调用snd_soc_register_card,通过snd_soc_register_card,为 snd_soc_pcm_runtime数组申请内存,每一个dai_link对应 snd_soc_pcm_runtime数组的一个单元,然后把snd_soc_card中的dai_link配 置复制到相应的snd_soc_pcm_runtime中,最后,大部分的工作都在 snd_soc_instantiate_card中实现,下面就看看snd_soc_instantiate_card做了 些什么: 该函数首先利用card->instantiated来判断该卡是否已经实例化,如果已经实例 化则直接返回,否则遍历每一对dai_link,进行codec、platform、dai的绑定工 作,下只是代码的部分选节,详细的代码请直接参考完整的代码树。 [cpp] view plain copy 1. /* bind DAIs */ 2. for (i = 0; i < card->num_links; i++) 3. soc_bind_dai_link(card, i); ASoC定义了三个全局的链表头变量:codec_list、dai_list、platform_list,系统 中所有的Codec、DAI、Platform都在注册时连接到这三个全局链表上。 soc_bind_dai_link函数逐个扫描这三个链表,根据card->dai_link[]中的名称进 行匹配,匹配后把相应的codec,dai和platform实例赋值到card->rtd[]中 (snd_soc_pcm_runtime)。经过这个过程后,snd_soc_pcm_runtime: (card->rtd)中保存了本Machine中使用的Codec,DAI和Platform驱动的信 息。 snd_soc_instantiate_card接着初始化Codec的寄存器缓存,然后调用标准的 alsa函数创建声卡实例: [cpp] view plain copy 1. /* card bind complete so register a sound card */ 2. ret = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_S TR1, 3. card->owner, 0, &card->snd_card); 4. card->snd_card->dev = card->dev; 5. 6. card->_level = SND_SOC_BIAS_OFF; 7. card-> = card->dev; 8. card-> = card; 9. list_add(&card->, &card->dapm_list); 49 然后,依次调用各个子结构的probe函数: [cpp] view plain copy 1. 2. 3. 4. 5. 6. 7. 8. 9. /* initialise the sound card only once */ if (card->probe) { ret = card->probe(card); if (ret < 0) goto card_probe_error; } /* early DAI link probe */ for (order = SND_SOC_COMP_ORDER_FIRST; order <= SND_SOC_COMP_ORDE R_LAST; 10. order++) { 11. for (i = 0; i < card->num_links; i++) { 12. ret = soc_probe_dai_link(card, i, order); 13. if (ret < 0) { 14. pr_err("asoc: failed to instantiate card %s: %dn", 15. card->name, ret); 16. goto probe_dai_err; 17. } 18. } 19. } 20. 21. for (i = 0; i < card->num_aux_devs; i++) { 22. ret = soc_probe_aux_dev(card, i); 23. if (ret < 0) { 24. pr_err("asoc: failed to add auxiliary devices %s: %dn", 25. card->name, ret); 26. goto probe_aux_dev_err; 27. } 28. } 在上面的soc_probe_dai_link()函数中做了比较多的事情,把他展开继续讨论: 1. static int soc_probe_dai_link(struct snd_soc_card *card, int num, int order) 2. { 3. ...... 4. /* set default power off timeout */ 5. rtd->pmdown_time = pmdown_time; 6. 50 7. /* probe the cpu_dai */ 8. if (!cpu_dai->probed && 9. cpu_dai->driver->probe_order == order) { 10. 11. if (cpu_dai->driver->probe) { 12. ret = cpu_dai->driver->probe(cpu_dai); 13. } 14. cpu_dai->probed = 1; 15. /* mark cpu_dai as probed and add to card dai list */ 16. list_add(&cpu_dai->card_list, &card->dai_dev_list); 17. } 18. 19. /* probe the CODEC */ 20. if (!codec->probed && 21. codec->driver->probe_order == order) { 22. ret = soc_probe_codec(card, codec); 23. } 24. 25. /* probe the platform */ 26. if (!platform->probed && 27. platform->driver->probe_order == order) { 28. ret = soc_probe_platform(card, platform); 29. } 30. 31. /* probe the CODEC DAI */ 32. if (!codec_dai->probed && codec_dai->driver->probe_order == o rder) { 33. if (codec_dai->driver->probe) { 34. ret = codec_dai->driver->probe(codec_dai); 35. } 36. 37. /* mark codec_dai as probed and add to card dai list */ 38. codec_dai->probed = 1; 39. list_add(&codec_dai->card_list, &card->dai_dev_list); 40. } 41. 42. /* complete DAI probe during last probe */ 43. if (order != SND_SOC_COMP_ORDER_LAST) 44. return 0; 45. 46. ret = soc_post_component_init(card, codec, num, 0); 47. if (ret) 48. return ret; 49. ...... 51 50. /* create the pcm */ 51. ret = soc_new_pcm(rtd, num); 52. ........ 53. return 0; 54. } 该函数出了挨个调用了codec,dai和platform驱动的probe函数外,在最后还 调用了soc_new_pcm()函数用于创建标准alsa驱动的pcm逻辑设备。现在把该 函数的部分代码也贴出来: 1. /* create a new pcm */ 2. int soc_new_pcm(struct snd_soc_pcm_runtime *rtd, int num) 3. { 4. ...... 5. struct snd_pcm_ops *soc_pcm_ops = &rtd->ops; 6. 7. soc_pcm_ops->open = soc_pcm_open; 8. soc_pcm_ops->close = soc_pcm_close; 9. soc_pcm_ops->hw_params = soc_pcm_hw_params; 10. soc_pcm_ops->hw_free = soc_pcm_hw_free; 11. soc_pcm_ops->prepare = soc_pcm_prepare; 12. soc_pcm_ops->trigger = soc_pcm_trigger; 13. soc_pcm_ops->pointer = soc_pcm_pointer; 14. 15. ret = snd_pcm_new(rtd->card->snd_card, new_name, 16. num, playback, capture, &pcm); 17. 18. /* DAPM dai link stream work */ 19. INIT_DELAYED_WORK(&rtd->delayed_work, close_delayed_ work); 20. 21. rtd->pcm = pcm; 22. pcm->private_data = rtd; 23. if (platform->driver->ops) { 24. soc_pcm_ops->mmap = platform->driver->ops->mmap; 25. soc_pcm_ops->pointer = platform->driver->ops->po inter; 26. soc_pcm_ops->ioctl = platform->driver->ops->ioct l; 27. soc_pcm_ops->copy = platform->driver->ops->copy; 52 28. soc_pcm_ops->silence = platform->driver->ops->si lence; 29. soc_pcm_ops->ack = platform->driver->ops->ack; 30. soc_pcm_ops->page = platform->driver->ops->page; 31. } 32. 33. if (playback) 34. snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, soc_pcm_ops); 35. 36. if (capture) 37. snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, s oc_pcm_ops); 38. 39. if (platform->driver->pcm_new) { 40. ret = platform->driver->pcm_new(rtd); 41. if (ret < 0) { 42. pr_err("asoc: platform pcm constructor faile dn"); 43. return ret; 44. } 45. } 46. 47. pcm->private_free = platform->driver->pcm_free; 48. return ret; 49. } 该函数首先初始化snd_soc_runtime中的snd_pcm_ops字段,也就是rtd->ops 中的部分成员,例如open,close,hw_params等,紧接着调用标准alsa驱动 中的创建pcm的函数snd_pcm_new()创建声卡的pcm实例,pcm的 private_data字段设置为该runtime变量rtd,然后用platform驱动中的 snd_pcm_ops替换部分pcm中的snd_pcm_ops字段,最后,调用platform驱 动的pcm_new回调,该回调实现该platform下的dma内存申请和dma初始化 等相关工作。到这里,声卡和他的pcm实例创建完成。 回到snd_soc_instantiate_card函数,完成snd_card和snd_pcm的创建后,接 着对dapm和dai支持的格式做出一些初始化合设置工作后,调用 了 card->late_probe(card)进行一些最后的初始化合设置工作,最后则是调用标 准alsa驱动的声卡注册函数对声卡进行注册: 1. if (card->late_probe) { 2. ret = card->late_probe(card); 3. if (ret < 0) { 53 4. dev_err(card->dev, "%s late_probe() failed: %dn" , 5. card->name, ret); 6. goto probe_aux_dev_err; 7. } 8. } 9. 10. snd_soc_dapm_new_widgets(&card->dapm); 11. 12. if (card->fully_routed) 13. list_for_each_entry(codec, &card->codec_dev_list, ca rd_list) 14. snd_soc_dapm_auto_nc_codec_pins(codec); 15. 16. ret = snd_card_register(card->snd_card); 17. if (ret < 0) { 18. printk(KERN_ERR "asoc: failed to register soundcard for %sn", card->name); 19. goto probe_aux_dev_err; 20. } 至此,整个Machine驱动的初始化已经完成,通过各个子结构的probe调用, 实际上,也完成了部分Platfrom驱动和Codec驱动的初始化工作,整个过程可 以用一下的序列图表示: 54 图3.1 基于3.0内核 soc_probe序列图 下面的序列图是本文章第一个版本,基于内核2.6.35,大家也可以参考一下两个 版本的差异: 55 图3.2 基于2.6.35 soc_probe序 列图 7. Linux ALSA声卡驱动之七:ASoC架构中的Codec 1. Codec简介 在移动设备中,Codec的作用可以归结为4种,分别是: 对PCM等信号进行D/A转换,把数字的音频信号转换为模拟信号 对Mic、Linein或者其他输入源的模拟信号进行A/D转换,把模拟的声音 信号转变CPU能够处理的数字信号 对音频通路进行控制,比如播放音乐,收听调频收音机,又或者接听电话 时,音频信号在codec内的流通路线是不一样的 对音频信号做出相应的处理,例如音量控制,功率放大,EQ控制等等 ASoC对Codec的这些功能都定义好了一些列相应的接口,以方便地对Codec 进行控制。ASoC对Codec驱动的一个基本要求是:驱动程序的代码必须要做 到平台无关性,以方便同一个Codec的代码不经修改即可用在不同的平台上。 以下的讨论基于wolfson的Codec芯片WM8994,kernel的版本3.3.x。 2. ASoC中对Codec的数据抽象 描述Codec的最主要的几个数据结构分别是:snd_soc_codec, snd_soc_codec_driver,snd_soc_dai,snd_soc_dai_driver,其中的snd_soc_dai 56 和snd_soc_dai_driver在ASoC的Platform驱动中也会使用到,Platform和 Codec的DAI通过snd_soc_dai_link结构,在Machine驱动中进行绑定连接。 下面我们先看看这几个结构的定义,这里我只贴出我要关注的字段,详细的定义 请参照:/include/sound/soc.h。 snd_soc_codec: 1. /* SoC Audio Codec device */ 2. struct snd_soc_codec { 3. const char *name; /* Codec的名字*/ 4. struct device *dev; /* 指向Codec设备的指针 */ 5. const struct snd_soc_codec_driver *driver; /* 指向该codec的驱 动的指针 */ 6. struct snd_soc_card *card; /* 指向Machine驱动的card实 例 */ 7. int num_dai; /* 该Codec数字接口的个数,目前越来越多的Codec带有多 个I2S或者是PCM接口 */ 8. int (*volatile_register)(...); /* 用于判定某一寄存器是否是 volatile */ 9. int (*readable_register)(...); /* 用于判定某一寄存器是否可 读 */ 10. int (*writable_register)(...); /* 用于判定某一寄存器是否可 写 */ 11. 12. /* runtime */ 13. ...... 14. /* codec IO */ 15. void *control_data; /* 该指针指向的结构用于对codec的控制,通常和 read,write字段联合使用 */ 16. enum snd_soc_control_type control_type;/* 可以是SND_SOC_SPI, SND_SOC_I2C,SND_SOC_REGMAP中的一种 */ 17. unsigned int (*read)(struct snd_soc_codec *, unsigned int); /* 读取Codec寄存器的函数 */ 18. int (*write)(struct snd_soc_codec *, unsigned int, unsigned i nt); /* 写入Codec寄存器的函数 */ 19. /* dapm */ 20. struct snd_soc_dapm_context dapm; /* 用于DAPM控件 */ 21. }; snd_soc_codec_driver: 1. /* codec driver */ 2. struct snd_soc_codec_driver { 3. /* driver ops */ 57 4. int (*probe)(struct snd_soc_codec *); /* codec驱动的probe函 数,由snd_soc_instantiate_card回调 */ 5. int (*remove)(struct snd_soc_codec *); 6. 7. 8. 9. int (*suspend)(struct snd_soc_codec *); /* 电源管理 */ int (*resume)(struct snd_soc_codec *); /* 电源管理 */ /* Default control and setup, added after probe() is run */ 10. const struct snd_kcontrol_new *controls; /* 音频控件指针 */ 11. const struct snd_soc_dapm_widget *dapm_widgets; /* dapm部件指 针 */ 12. const struct snd_soc_dapm_route *dapm_routes; /* dapm路由指 针 */ 13. 14. /* codec wide operations */ 15. int (*set_sysclk)(...); /* 时钟配置函数 */ 16. int (*set_pll)(...); /* 锁相环配置函数 */ 17. 18. /* codec IO */ 19. unsigned int (*read)(...); /* 读取codec寄存器函数 */ 20. int (*write)(...); /* 写入codec寄存器函数 */ 21. int (*volatile_register)(...); /* 用于判定某一寄存器是否是 volatile */ 22. int (*readable_register)(...); /* 用于判定某一寄存器是否可 读 */ 23. int (*writable_register)(...); /* 用于判定某一寄存器是否可 写 */ 24. 25. /* codec bias level */ 26. int (*set_bias_level)(...); /* 偏置电压配置函数 */ 27. 28. }; snd_soc_dai: 1. 2. 3. 4. 5. 6. /* * Digital Audio Interface runtime data. * * Holds runtime data for a DAI. */ struct snd_soc_dai { 7. const char *name; /* dai的名字 */ 8. struct device *dev; /* 设备指针 */ 58 9. 10. /* driver ops */ 11. struct snd_soc_dai_driver *driver; /* 指向dai驱动结构的指 针 */ 12. 13. /* DAI runtime info */ 14. unsigned int capture_active:1; /* stream is in use */ 15. unsigned int playback_active:1; /* stream is in use */ 16. 17. /* DAI DMA data */ 18. void *playback_dma_data; /* 用于管理playback dma */ 19. void *capture_dma_data; /* 用于管理capture dma */ 20. 21. /* parent platform/codec */ 22. union { 23. struct snd_soc_platform *platform; /* 如果是cpu dai,指向 所绑定的平台 */ 24. struct snd_soc_codec *codec; /* 如果是codec dai指向所绑定 的codec */ 25. }; 26. struct snd_soc_card *card; /* 指向Machine驱动中的crad实 例 */ 27. }; snd_soc_dai_driver: 1. /* 2. * Digital Audio Interface Driver. 3. * 4. * Describes the Digital Audio Interface in terms of its ALSA, DAI and AC97 5. * operations and capabilities. Codec and platform driver s will register this 6. * structure for every DAI they have. 7. * 8. * This structure covers the clocking, formating and ALSA operations for each 9. * interface. 10. */ 11. struct snd_soc_dai_driver { 12. /* DAI description */ 13. const char *name; /* dai驱动名字 */ 59 14. 15. /* DAI driver callbacks */ 16. int (*probe)(struct snd_soc_dai *dai); /* dai驱动的 probe函数,由snd_soc_instantiate_card回调 */ 17. int (*remove)(struct snd_soc_dai *dai); 18. int (*suspend)(struct snd_soc_dai *dai); /* 电源管 理 */ 19. int (*resume)(struct snd_soc_dai *dai); 20. 21. /* ops */ 22. const struct snd_soc_dai_ops *ops; /* 指向本dai的 snd_soc_dai_ops结构 */ 23. 24. /* DAI capabilities */ 25. struct snd_soc_pcm_stream capture; /* 描述capture的 能力 */ 26. struct snd_soc_pcm_stream playback; /* 描述playback 的能力 */ 27. }; snd_soc_dai_ops用于实现该dai的控制盒参数配置: 1. struct snd_soc_dai_ops { 2. /* 3. * DAI clocking configuration, all optional. 4. * Called by soc_card drivers, normally in their hw_p arams. 5. */ 6. int (*set_sysclk)(...); 7. int (*set_pll)(...); 8. int (*set_clkdiv)(...); 9. /* 10. * DAI format configuration 11. * Called by soc_card drivers, normally in their hw_ params. 12. */ 13. int (*set_fmt)(...); 14. int (*set_tdm_slot)(...); 15. int (*set_channel_map)(...); 16. int (*set_tristate)(...); 17. /* 18. * DAI digital mute - optional. 60 19. * Called by soc-core to minimise any pops. 20. */ 21. int (*digital_mute)(...); 22. /* 23. * ALSA PCM audio operations - all optional. 24. * Called by soc-core during audio PCM operations. 25. */ 26. int (*startup)(...); 27. void (*shutdown)(...); 28. int (*hw_params)(...); 29. int (*hw_free)(...); 30. int (*prepare)(...); 31. int (*trigger)(...); 32. /* 33. * For hardware based FIFO caused delay reporting. 34. * Optional. 35. */ 36. snd_pcm_sframes_t (*delay)(...); 37. }; 3. Codec的注册 因为Codec驱动的代码要做到平台无关性,要使得Machine驱动能够使用该 Codec,Codec驱动的首要任务就是确定snd_soc_codec和snd_soc_dai的实 例,并把它们注册到系统中,注册后的codec和dai才能为Machine驱动所用。 以WM8994为例,对应的代码位置:/sound/soc/codecs/wm8994.c,模块的入 口函数注册了一个platform driver: 1. static struct platform_driver wm8994_codec_driver = { 2. .driver = { 3. .name = "wm8994-codec", 4. .owner = THIS_MODULE, 5. }, 6. .probe = wm8994_probe, 7. .remove = __devexit_p(wm8994_remove), 8. }; 9. 10. module_platform_driver(wm8994_codec_driver); 有platform driver,必定会有相应的platform device,这个platform device 的来源后面再说,显然,platform driver注册后,probe回调将会被调用,这 里是wm8994_probe函数: 61 1. static int __devinit wm8994_probe(struct platform_device *pdev) 2. { 3. return snd_soc_register_codec(&pdev->dev, &soc_codec_ dev_wm8994, 4. wm8994_dai, ARRAY_SIZE(wm8994_dai)); 5. } 其中,soc_codec_dev_wm8994和wm8994_dai的定义如下(代码中定义了3 个dai,这里只列出第一个): [html] view plain copy 1. static struct snd_soc_codec_driver soc_codec_dev_wm8994 = { 2. .probe = wm8994_codec_probe, 3. .remove = wm8994_codec_remove, 4. .suspend = wm8994_suspend, 5. .resume = wm8994_resume, 6. .set_bias_level = wm8994_set_bias_level, 7. .reg_cache_size = WM8994_MAX_REGISTER, 8. .volatile_register = wm8994_soc_volatile, 9. }; [html] view plain copy 1. static struct snd_soc_dai_driver wm8994_dai[] = { 2. { 3. .name = "wm8994-aif1", 4. .id = 1, 5. .playback = { 6. .stream_name = "AIF1 Playback", 7. .channels_min = 1, 8. .channels_max = 2, 9. .rates = WM8994_RATES, 10. .formats = WM8994_FORMATS, 11. }, 12. .capture = { 13. .stream_name = "AIF1 Capture", 14. .channels_min = 1, 15. .channels_max = 2, 16. .rates = WM8994_RATES, 17. .formats = WM8994_FORMATS, 18. }, 19. .ops = &wm8994_aif1_dai_ops, 20. }, 62 21. ...... 22. } 可见,Codec驱动的第一个步骤就是定义snd_soc_codec_driver和 snd_soc_dai_driver的实例,然后调用snd_soc_register_codec函数对Codec 进行注册。进入snd_soc_register_codec函数看看: 首先,它申请了一个snd_soc_codec结构的实例: 1. codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL) ; 确定codec的名字,这个名字很重要,Machine驱动定义的snd_soc_dai_link 中会指定每个link的codec和dai的名字,进行匹配绑定时就是通过和这里的 名字比较,从而找到该Codec的! 1. /* create CODEC component name */ 2. codec->name = fmt_single_name(dev, &codec->id); 然后初始化它的各个字段,多数字段的值来自上面定义的 snd_soc_codec_driver的实例soc_codec_dev_wm8994: 1. codec->write = codec_drv->write; 2. codec->read = codec_drv->read; 3. codec->volatile_register = codec_drv->volatile_register; 4. codec->readable_register = codec_drv->readable_register; 5. codec->writable_register = codec_drv->writable_register; 6. codec->_level = SND_SOC_BIAS_OFF; 7. codec-> = dev; 8. codec-> = codec; 9. codec->_notifier = codec_drv->seq_notifier; 10. codec->_event = codec_drv->stream_event; 11. codec->dev = dev; 12. codec->driver = codec_drv; 13. codec->num_dai = num_dai; 63 在做了一些寄存器缓存的初始化和配置工作后,通过snd_soc_register_dais函 数对本Codec的dai进行注册: 1. /* register any DAIs */ 2. if (num_dai) { 3. ret = snd_soc_register_dais(dev, dai_drv, num_dai); 4. if (ret < 0) 5. goto fail; 6. } 最后,它把codec实例链接到全局链表codec_list中,并且调用 snd_soc_instantiate_cards是函数触发Machine驱动进行一次匹配绑定操作: 1. list_add(&codec->list, &codec_list); 2. snd_soc_instantiate_cards(); 上面的snd_soc_register_dais函数其实也是和snd_soc_register_codec类似, 显示为每个snd_soc_dai实例分配内存,确定dai的名字,用snd_soc_dai_driver 实例的字段对它进行必要初始化,最后把该dai链接到全局链表dai_list中,和 Codec一样,最后也会调用snd_soc_instantiate_cards函数触发一次匹配绑定 的操作。 图3.1 dai的注册 关于snd_soc_instantiate_cards函数,请参阅另一篇博文:Linux音频驱动之六: ASoC架构中的Machine。 64 4. mfd设备 前面已经提到,codec驱动把自己注册为一个platform driver,那对应的platform device在哪里定义?答案是在以下代码文件中:/drivers/mfd/wm8994-core.c。 WM8994本身具备多种功能,除了codec外,它还有作为LDO和GPIO使用, 这几种功能共享一些IO和中断资源,linux为这种设备提供了一套标准的实现方 法:mfd设备。其基本思想是为这些功能的公共部分实现一个父设备,以便共享 某些系统资源和功能,然后每个子功能实现为它的子设备,这样既共享了资源和 代码,又能实现合理的设备层次结构,主要利用到的API就是: mfd_add_devices(),mfd_remove_devices(),mfd_cell_enable(), mfd_cell_disable(),mfd_clone_cell()。 回到wm8994-core.c中,因为WM8994使用I2C进行内部寄存器的存取,它首 先注册了一个I2C驱动: [html] view plain copy 1. static struct i2c_driver wm8994_i2c_driver = { 2. .driver = { 3. .name = "wm8994", 4. .owner = THIS_MODULE, 5. .pm = &wm8994_pm_ops, 6. .of_match_table = wm8994_of_match, 7. }, 8. .probe = wm8994_i2c_probe, 9. .remove = wm8994_i2c_remove, 10. .id_table = wm8994_i2c_id, 11. }; 12. 13. static int __init wm8994_i2c_init(void) 14. { 15. int ret; 16. 17. ret = i2c_add_driver(&wm8994_i2c_driver); 18. if (ret != 0) 19. pr_err("Failed to register wm8994 I2C driver: %d n", ret); 20. 21. return ret; 22. } 23. module_init(wm8994_i2c_init); 65 进入wm8994_i2c_probe()函数,它先申请了一个wm8994结构的变量,该变量 被作为这个I2C设备的driver_data使用,上面已经讲过,codec作为它的子设 备,将会取出并使用这个driver_data。接下来,本函数利用regmap_init_i2c() 初始化并获得一个regmap结构,该结构主要用于后续基于regmap机制的寄存 器I/O,关于regmap我们留在后面再讲。最后,通过wm8994_device_init()来 添加mfd子设备: 1. static int wm8994_i2c_probe(struct i2c_client *i2c, 2. const struct i2c_device_id *id) 3. { 4. struct wm8994 *wm8994; 5. int ret; 6. wm8994 = devm_kzalloc(&i2c->dev, sizeof(struct wm8994 ), GFP_KERNEL); 7. i2c_set_clientdata(i2c, wm8994); 8. wm8994->dev = &i2c->dev; 9. wm8994->irq = i2c->irq; 10. wm8994->type = id->driver_data; 11. wm8994->regmap = regmap_init_i2c(i2c, &wm8994_base_r egmap_config); 12. 13. return wm8994_device_init(wm8994, i2c->irq); 14. } 继续进入wm8994_device_init()函数,它首先为两个LDO添加mfd子设备: 1. /* Add the on-chip regulators first for bootstrapping */ 2. ret = mfd_add_devices(wm8994->dev, -1, 3. wm8994_regulator_devs, 4. ARRAY_SIZE(wm8994_regulator_devs), 5. NULL, 0); 因为WM1811,WM8994,WM8958三个芯片功能类似,因此这三个芯片都使用了 WM8994的代码,所以wm8994_device_init()接下来根据不同的芯片型号做了一 些初始化动作,这部分的代码就不贴了。接着,从platform_data中获得部分配 置信息: 1. if (pdata) { 2. wm8994->irq_base = pdata->irq_base; 66 3. wm8994->gpio_base = pdata->gpio_base; 4. 5. /* GPIO configuration is only applied if it's non-zer o */ 6. ...... 7. } 最后,初始化irq,然后添加codec子设备和gpio子设备: 1. wm8994_irq_init(wm8994); 2. 3. ret = mfd_add_devices(wm8994->dev, -1, 4. wm8994_devs, ARRAY_SIZE(wm8994_devs), 5. NULL, 0); 经过以上这些处理后,作为父设备的I2C设备已经准备就绪,它的下面挂着4 个子设备:ldo-0,ldo-1,codec,gpio。其中,codec子设备的加入,它将会和 前面所讲codec的platform driver匹配,触发probe回调完成下面所说的codec 驱动的初始化工作。 5. Codec初始化 Machine驱动的初始化,codec和dai的注册,都会调用 snd_soc_instantiate_cards()进行一次声卡和codec,dai,platform的匹配绑定 过程,这里所说的绑定,正如Machine驱动一文中所描述,就是通过3个全局 链表,按名字进行匹配,把匹配的codec,dai和platform实例赋值给声卡每对 dai的snd_soc_pcm_runtime变量中。一旦绑定成功,将会使得codec和dai 驱动的probe回调被调用,codec的初始化工作就在该回调中完成。对于 WM8994,该回调就是wm8994_codec_probe函数: 67 图 5.1 wm8994_codec_probe 取出父设备的driver_data,其实就是上一节的wm8994结构变量,取出 其中的regmap字段,复制到codec的control_data字段中; 申请一个wm8994_priv私有数据结构,并把它设为codec设备的 driver_data; 通过snd_soc_codec_set_cache_io初始化regmap io,完成这一步后, 就可以使用API:snd_soc_read(),snd_soc_write()对codec的寄存器进 行读写了; 把父设备的driver_data(struct wm8994)和platform_data保存到私有 结构wm8994_priv中; 因为要同时支持3个芯片型号,这里要根据芯片的型号做一些特定的初始 化工作; 申请必要的几个中断; 设置合适的偏置电平; 通过snd_soc_update_bits修改某些寄存器; 根据父设备的platform_data,完成特定于平台的初始化配置; 添加必要的control,dapm部件进而dapm路由信息; 68 至此,codec驱动的初始化完成。 5. regmap-io 我们知道,要想对codec进行控制,通常都是通过读写它的内部寄存器完成的, 读写的接口通常是I2C或者是SPI接口,不过每个codec芯片寄存器的比特位 组成都有所不同,寄存器地址的比特位也有所不同。例如WM8753的寄存器地 址是7bits,数据是9bits,WM8993的寄存器地址是8bits,数据也是16bits, 而WM8994的寄存器地址是16bits,数据也是16bits。在kernel3.1版本,内核 引入了一套regmap机制和相关的API,这样就可以用统一的操作来实现对这些 多样的寄存器的控制。regmap使用起来也相对简单: 为codec定义一个regmap_config结构实例,指定codec寄存器的地址 和数据位等信息; 根据codec的控制总线类型,调用以下其中一个函数,得到一个指向 regmap结构的指针: o struct regmap *regmap_init_i2c(struct i2c_client *i2c, const struct regmap_config *config); o struct regmap *regmap_init_spi(struct spi_device *dev, const struct regmap_config *config); 把获得的regmap结构指针赋值给codec->control_data; 调用soc-io的api:snd_soc_codec_set_cache_io使得soc-io和regmap 进行关联; 完成以上步骤后,codec驱动就可以使用诸如snd_soc_read、snd_soc_write、 snd_soc_update_bits等API对codec的寄存器进行读写了。 8. Linux ALSA声卡驱动之八:ASoC架构中的Platform 1. Platform驱动在ASoC中的作用 前面几章内容已经说过,ASoC被分为Machine,Platform和Codec三大部件, Platform驱动的主要作用是完成音频数据的管理,最终通过CPU的数字音频接 口(DAI)把音频数据传送给Codec进行处理,最终由Codec输出驱动耳机或 者是喇叭的音信信号。在具体实现上,ASoC有把Platform驱动分为两个部分: snd_soc_platform_driver和snd_soc_dai_driver。其中,platform_driver负责管 理音频数据,把音频数据通过dma或其他操作传送至cpu dai中,dai_driver则 主要完成cpu一侧的dai的参数配置,同时也会通过一定的途径把必要的dma 等参数与snd_soc_platform_driver进行交互。 69 2. snd_soc_platform_driver的注册 通常,ASoC把snd_soc_platform_driver注册为一个系统的platform_driver, 不要被这两个相像的术语所迷惑,前者只是针对ASoC子系统的,后者是来自 Linux的设备驱动模型。我们要做的就是: 定义一个snd_soc_platform_driver结构的实例; 在platform_driver的probe回调中利用ASoC的API: snd_soc_register_platform()注册上面定义的实例; 实现snd_soc_platform_driver中的各个回调函数; 以kernel3.3中的/sound/soc/samsung/dma.c为例: [cpp] view plain copy 1. static struct snd_soc_platform_driver samsung_asoc_platfo rm = { 2. .ops = &dma_ops, 3. .pcm_new = dma_new, 4. .pcm_free = dma_free_dma_buffers, 5. }; 6. 7. static int __devinit samsung_asoc_platform_probe(struct p latform_device *pdev) 8. { 9. return snd_soc_register_platform(&pdev->dev, &samsung _asoc_platform); 10. } 11. 12. static int __devexit samsung_asoc_platform_remove(struct platform_device *pdev) 13. { 14. snd_soc_unregister_platform(&pdev->dev); 15. return 0; 16. } 17. 18. static struct platform_driver asoc_dma_driver = { 19. .driver = { 20. .name = "samsung-audio", 21. .owner = THIS_MODULE, 22. }, 23. 24. .probe = samsung_asoc_platform_probe, 70 25. .remove = __devexit_p(samsung_asoc_platform_remove), 26. }; 27. 28. module_platform_driver(asoc_dma_driver); snd_soc_register_platform() 该函数用于注册一个snd_soc_platform,只有注 册以后,它才可以被Machine驱动使用。它的代码已经清晰地表达了它的实现 过程: 为snd_soc_platform实例申请内存; 从platform_device中获得它的名字,用于Machine驱动的匹配工作; 初始化snd_soc_platform的字段; 把snd_soc_platform实例连接到全局链表platform_list中; 调用snd_soc_instantiate_cards,触发声卡的machine、platform、codec、 dai等的匹配工作; 3. cpu的snd_soc_dai driver驱动的注册 dai驱动通常对应cpu的一个或几个I2S/PCM接口,与snd_soc_platform一样, dai驱动也是实现为一个platform driver,实现一个dai驱动大致可以分为以下几 个步骤: 定义一个snd_soc_dai_driver结构的实例; 在对应的platform_driver中的probe回调中通过API: snd_soc_register_dai或者snd_soc_register_dais,注册snd_soc_dai 实例; 实现snd_soc_dai_driver结构中的probe、suspend等回调; 实现snd_soc_dai_driver结构中的snd_soc_dai_ops字段中的回调函数; snd_soc_register_dai 这个函数在上一篇介绍codec驱动的博文中已有介绍, 请参考:Linux ALSA声卡驱动之七:ASoC架构中的Codec。 snd_soc_dai 该结构在snd_soc_register_dai函数中通过动态内存申请获得, 简要介绍一下几个重要字段: driver 指向关联的snd_soc_dai_driver结构,由注册时通过参数传入; playback_dma_data 用于保存该dai播放stream的dma信息,例如dma 的目标地址,dma传送单元大小和通道号等; capture_dma_data 同上,用于录音stream; platform 指向关联的snd_soc_platform结构; snd_soc_dai_driver 该结构需要自己根据不同的soc芯片进行定义,关键字 段介绍如下: 71 probe、remove 回调函数,分别在声卡加载和卸载时被调用; suspend、resume 电源管理回调函数; ops 指向snd_soc_dai_ops结构,用于配置和控制该dai; playback snd_soc_pcm_stream结构,用于指出该dai支持的声道数, 码率,数据格式等能力; capture snd_soc_pcm_stream结构,用于指出该dai支持的声道数,码 率,数据格式等能力; 4. snd_soc_dai_driver中的ops字段 ops字段指向一个snd_soc_dai_ops结构,该结构实际上是一组回调函数的集 合,dai的配置和控制几乎都是通过这些回调函数来实现的,这些回调函数基本 可以分为3大类,驱动程序可以根据实际情况实现其中的一部分: 工作时钟配置函数 通常由machine驱动调用: set_sysclk 设置dai的主时钟; set_pll 设置PLL参数; set_clkdiv 设置分频系数; dai的格式配置函数 通常由machine驱动调用: set_fmt 设置dai的格式; set_tdm_slot 如果dai支持时分复用,用于设置时分复用的slot; set_channel_map 声道的时分复用映射设置; set_tristate 设置dai引脚的状态,当与其他dai并联使用同一引脚时需 要使用该回调; 标准的snd_soc_ops回调 通常由soc-core在进行PCM操作时调用: startup shutdown hw_params hw_free prepare trigger 抗pop,pop声 由soc-core调用: digital_mute 以下这些api通常被machine驱动使用,machine驱动在他的snd_pcm_ops字 段中的hw_params回调中使用这些api: snd_soc_dai_set_fmt() 实际上会调用snd_soc_dai_ops或者codec driver中的set_fmt回调; 72 snd_soc_dai_set_pll() 实际上会调用snd_soc_dai_ops或者codec driver中的set_pll回调; snd_soc_dai_set_sysclk() 实际上会调用snd_soc_dai_ops或者codec driver中的set_sysclk回调; snd_soc_dai_set_clkdiv() 实际上会调用snd_soc_dai_ops或者codec driver中的set_clkdiv回调; snd_soc_dai_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)的第二个参数 fmt在这里特别说一下,ASoC目前只是用了它的低16位,并且为它专门定义 了一些宏来方便我们使用: bit 0-3 用于设置接口的格式: 1. #define SND_SOC_DAIFMT_I2S 1 /* I2S mode */ 2. #define SND_SOC_DAIFMT_RIGHT_J 2 /* Right Justified mode */ 3. #define SND_SOC_DAIFMT_LEFT_J 3 /* Left Justified m ode */ 4. #define SND_SOC_DAIFMT_DSP_A 4 /* L data MSB after FRM LRC */ 5. #define SND_SOC_DAIFMT_DSP_B 5 /* L data MSB durin g FRM LRC */ 6. #define SND_SOC_DAIFMT_AC97 6 /* AC97 */ 7. #define SND_SOC_DAIFMT_PDM 7 /* Pulse density modula tion */ bit 4-7 用于设置接口时钟的开关特性: 1. #define SND_SOC_DAIFMT_CONT (1 << 4) /* continuous cl ock */ 2. #define SND_SOC_DAIFMT_GATED (2 << 4) /* clock is gated */ bit 8-11 用于设置接口时钟的相位: 1. #define SND_SOC_DAIFMT_NB_NF (1 << 8) /* normal bi t clock + frame */ 2. #define SND_SOC_DAIFMT_NB_IF (2 << 8) /* normal BC LK + inv FRM */ 73 3. #define SND_SOC_DAIFMT_IB_NF (3 << 8) /* invert BC LK + nor FRM */ 4. #define SND_SOC_DAIFMT_IB_IF (4 << 8) /* invert BC LK + FRM */ bit 12-15 用于设置接口主从格式: 1. #define SND_SOC_DAIFMT_CBM_CFM (1 << 12) /* codec cl k & FRM master */ 2. #define SND_SOC_DAIFMT_CBS_CFM (2 << 12) /* codec cl k slave & FRM master */ 3. #define SND_SOC_DAIFMT_CBM_CFS (3 << 12) /* codec cl k master & frame slave */ 4. #define SND_SOC_DAIFMT_CBS_CFS (4 << 12) /* codec cl k & FRM slave */ 5. snd_soc_platform_driver中的ops字段 该ops字段是一个snd_pcm_ops结构,实现该结构中的各个回调函数是soc platform驱动的主要工作,他们基本都涉及dma操作以及dma buffer的管理等 工作。下面介绍几个重要的回调函数: 当应用程序打开一个pcm设备时,该函数会被调用,通常,该函数会使用 snd_soc_set_runtime_hwparams()设置substream中的snd_pcm_runtime结构 里面的hw_params相关字段,然后为snd_pcm_runtime的private_data字段 申请一个私有结构,用于保存该平台的dma参数。 _params 驱动的hw_params阶段,该函数会被调用。通常,该函数会通过 snd_soc_dai_get_dma_data函数获得对应的dai的dma参数,获得的参数一般 都会保存在snd_pcm_runtime结构的private_data字段。然后通过 snd_pcm_set_runtime_buffer函数设置snd_pcm_runtime结构中的dma buffer 的地址和大小等参数。要注意的是,该回调可能会被多次调用,具体实现时要小 心处理多次申请资源的问题。 e 74 正式开始数据传送之前会调用该函数,该函数通常会完成dma操作的必要准备 工作。 r 数据传送的开始,暂停,恢复和停止时,该函数会被调用。 r 该函数返回传送数据的当前位置。 6. 音频数据的dma操作 soc-platform驱动的最主要功能就是要完成音频数据的传送,大多数情况下,音 频数据都是通过dma来完成的。 6.1. 申请dma buffer 因为dma的特殊性,dma buffer是一块特殊的内存,比如有的平台规定只有某 段地址范围的内存才可以进行dma操作,而多数嵌入式平台还要求dma内存的 物理地址是连续的,以方便dma控制器对内存的访问。在ASoC架构中,dma buffer的信息保存在snd_pcm_substream结构的snd_dma_buffer *buf字段中, 它的定义如下 1. struct snd_dma_buffer { 2. struct snd_dma_device dev; /* device type */ 3. unsigned char *area; /* virtual pointer */ 4. dma_addr_t addr; /* physical address */ 5. size_t bytes; /* buffer size in bytes */ 6. void *private_data; /* private for allocator; don't t ouch */ 7. }; 那么,在哪里完成了snd_dam_buffer结构的初始化赋值操作呢?答案就在 snd_soc_platform_driver的pcm_new回调函数中,还是以 /sound/soc/samsung/dma.c为例: 1. static struct snd_soc_platform_driver samsung_asoc_platfo rm = { 2. .ops = &dma_ops, 3. .pcm_new = dma_new, 75 4. .pcm_free = dma_free_dma_buffers, 5. }; 6. 7. static int __devinit samsung_asoc_platform_probe(struct p latform_device *pdev) 8. { 9. return snd_soc_register_platform(&pdev->dev, &samsung _asoc_platform); 10. } pcm_new字段指向了dma_new函数,dma_new函数进一步为playback和 capture分别调用preallocate_dma_buffer函数,我们看看 preallocate_dma_buffer函数的实现: 1. static int preallocate_dma_buffer(struct snd_pcm *pcm, in t stream) 2. { 3. struct snd_pcm_substream *substream = pcm->streams[st ream].substream; 4. struct snd_dma_buffer *buf = &substream->dma_buffer; 5. size_t size = dma__bytes_max; 6. 7. pr_debug("Entered %sn", __func__); 8. 9. buf-> = SNDRV_DMA_TYPE_DEV; 10. buf-> = pcm->card->dev; 11. buf->private_data = NULL; 12. buf->area = dma_alloc_writecombine(pcm->card->dev, s ize, 13. &buf->addr, GFP_KERNEL); 14. if (!buf->area) 15. return -ENOMEM; 16. buf->bytes = size; 17. return 0; 18. } 该函数先是获得事先定义好的buffer大小,然后通过dma_alloc_weitecombine 函数分配dma内存,然后完成substream->dma_buffer的初始化赋值工作。上 76 述的pcm_new回调会在声卡的建立阶段被调用,调用的详细的过程请参考Linux ALSAs声卡驱动之六:ASoC架构中的Machine中的图3.1。 在声卡的hw_params阶段,snd_soc_platform_driver结构的ops->hw_params 会被调用,在该回调用,通常会使用api:snd_pcm_set_runtime_buffer()把 substream->dma_buffer的数值拷贝到substream->runtime的相关字段中 (.dma_area, .dma_addr, .dma_bytes),这样以后就可以通过 substream->runtime获得这些地址和大小信息了。 dma buffer获得后,即是获得了dma操作的源地址,那么目的地址在哪里?其 实目的地址当然是在dai中,也就是前面介绍的snd_soc_dai结构的 playback_dma_data和capture_dma_data字段中,而这两个字段的值也是在 hw_params阶段,由snd_soc_dai_driver结构的ops->hw_params回调,利用 api:snd_soc_dai_set_dma_data进行设置的。紧随其后, snd_soc_platform_driver结构的ops->hw_params回调利用api: snd_soc_dai_get_dma_data获得这些dai的dma信息,其中就包括了dma的 目的地址信息。这些dma信息通常还会被保存在 substream->runtime->private_data中,以便在substream的整个生命周期中可 以随时获得这些信息,从而完成对dma的配置和操作。 6.2 dma buffer管理 播放时,应用程序把音频数据源源不断地写入dma buffer中,然后相应platform 的dma操作则不停地从该buffer中取出数据,经dai送往codec中。录音时则 正好相反,codec源源不断地把A/D转换好的音频数据经过dai送入dma buffer 中,而应用程序则不断地从该buffer中读走音频数据。 图 6.2.1 环形缓冲区 环形缓冲区正好适合用于这种情景的buffer管理,理想情况下,大小为Count 的缓冲区具备一个读指针和写指针,我们期望他们都可以闭合地做环形移动,但 77 是实际的情况确实:缓冲区通常都是一段连续的地址,他是有开始和结束两个边 界,每次移动之前都必须进行一次判断,当指针移动到末尾时就必须人为地让他 回到起始位置。在实际应用中,我们通常都会把这个大小为Count的缓冲区虚 拟成一个大小为n*Count的逻辑缓冲区,相当于理想状态下的圆形绕了n圈之 后,然后把这段总的距离拉平为一段直线,每一圈对应直线中的一段,因为n 比较大,所以大多数情况下不会出现读写指针的换位的情况(如果不对buffer 进行扩展,指针到达末端后,回到起始端时,两个指针的前后相对位置会发生互 换)。扩展后的逻辑缓冲区在计算剩余空间可条件判断是相对方便。alsa driver 也使用了该方法对dma buffer进行管理: 图6.2.2 alsa driver缓冲区管理 snd_pcm_runtime结构中,使用了四个相关的字段来完成这个逻辑缓冲区的管 理: snd_pcm__ptr_base 环形缓冲区每一圈的基地址,当读写指 针越过一圈后,它按buffer size进行移动; snd_pcm_->hw_ptr 硬件逻辑位置,播放时相当于读指针, 录音时相当于写指针; snd_pcm_l->appl_ptr 应用逻辑位置,播放时相当于写指 针,录音时相当于读指针; snd_pcm_ry 扩展后的逻辑缓冲区大小,通常是 (2^n)*size; 通过这几个字段,我们可以很容易地获得缓冲区的有效数据,剩余空间等信息, 也可以很容易地把当前逻辑位置映射回真实的dma buffer中。例如,获得播放 缓冲区的空闲空间: 78 1. static inline snd_pcm_uframes_t snd_pcm_playback_avail(st ruct snd_pcm_runtime *runtime) 2. { 3. snd_pcm_sframes_t avail = runtime->status->hw_ptr + r untime->buffer_size - runtime->control->appl_ptr; 4. if (avail < 0) 5. avail += runtime->boundary; 6. else if ((snd_pcm_uframes_t) avail >= runtime->bounda ry) 7. avail -= runtime->boundary; 8. return avail; 9. } 要想映射到真正的缓冲区位置,只要减去runtime->hw_ptr_base即可。下面的 api用于更新这几个指针的当前位置: 1. int snd_pcm_update_hw_ptr(struct snd_pcm_substream *subst ream) 所以要想通过snd_pcm_playback_avail等函数获得正确的信息前,应该先要调 用这个api更新指针位置。 以播放(playback)为例,我现在知道至少有3个途径可以完成对dma buffer的 写入: 应用程序调用alsa-lib的snd_pcm_writei、snd_pcm_writen函数; 应用程序使用ioctl:SNDRV_PCM_IOCTL_WRITEI_FRAMES或 SNDRV_PCM_IOCTL_WRITEN_FRAMES; 应用程序使用alsa-lib的snd_pcm_mmap_begin/snd_pcm_mmap_commit; 以上几种方式最终把数据写入dma buffer中,然后修改 runtime->control->appl_ptr的值。 播放过程中,通常会配置成每一个period size生成一个dma中断,中断处理函 数最重要的任务就是: 更新dma的硬件的当前位置,该数值通常保存在runtime->private_data 中; 调用snd_pcm_period_elapsed函数,该函数会进一步调用 snd_pcm_update_hw_ptr0函数更新上述所说的4个缓冲区管理字段,然 后唤醒相应的等待进程; 79 void snd_pcm_period_elapsed(struct snd_pcm_substream *sub stream) { struct snd_pcm_runtime *runtime; unsigned long flags; if (PCM_RUNTIME_CHECK(substream)) return; runtime = substream->runtime; if (runtime->transfer_ack_begin) runtime->transfer_ack_begin(substream); snd_pcm_stream_lock_irqsave(substream, flags); if (!snd_pcm_running(substream) || snd_pcm_update_hw_ptr0(substream, 1) < 0) goto _end; if (substream->timer_running) snd_timer_interrupt(substream->timer, 1); _end: snd_pcm_stream_unlock_irqrestore(substream, flags); if (runtime->transfer_ack_end) runtime->transfer_ack_end(substream); kill_fasync(&runtime->fasync, SIGIO, POLL_IN); } 如果设置了transfer_ack_begin和transfer_ack_end回调, snd_pcm_period_elapsed还会调用这两个回调函数。 7. 图说代码 最后,反正图也画了,好与不好都传上来供参考一下,以下这张图表达了 ASoC 中Platform驱动的几个重要数据结构之间的关系: 80 图 7.1 ASoC Platform驱动 一堆的private_data,很重要但也很容易搞混,下面的图不知对大家有没有帮助: 81 图7.2 private_data 82