1 前言和环境
之前其实写过两篇,一篇是讲ALSA,一篇是I2S。
ALSA架构学习1(框架)_alsa框架学习-CSDN博客
总线学习5--I2S_max98357接喇叭教程-CSDN博客
在ALSA那篇的结尾,也提了几个小练习。比如:
### 4. **定制音频驱动程序**
- **目标**: 开发一个简单的 ALSA 驱动程序,用于控制一个虚拟或简单的音频硬件设备。
- **技术点**: 学习如何编写一个基本的 PCM 驱动程序,理解 ALSA 内核 API(如 `snd_pcm_new`、`snd_pcm_ops` 等),处理音频数据的 DMA 传输。
- **扩展**: 支持更复杂的硬件设备,如具有多个 PCM 子设备的音频控制器,实现高级功能,如音频格式转换、硬件加速等。### 5. **基于 I2S 的嵌入式音频项目**
- **目标**: 在嵌入式设备(如 Raspberry Pi 或 BeagleBone)上,使用 I2S 接口与外部 DAC(数模转换器)或 ADC(模数转换器)通讯,实现音频输入和输出功能。
- **技术点**: 配置设备树中的 I2S 接口,编写或修改 ALSA 驱动以支持 I2S,处理音频数据的采集与播放。
- **扩展**: 开发一个简单的 DSP(数字信号处理)功能,如均衡器或混响效果。
正好这次就使用I2S的MAX98357A,试一试在树莓派3B上面驱动起来。
2 操作流程
2.1 alsa驱动
首先看一下kernel的make menuconfig
在这里可以看到,MAX98357A已经作为module集成在kernel了。
直接就可以加载
sudo modprobe snd-soc-max98357a
此时可以看到,已经加载成功了。
2.2 设备树
哦,天啊,dtbo居然也是现成的。
2.3 系统修改
看来只用改系统配置就可以了。
在/boot/fireware/config.txt中增加配置
dtparam=i2s=on
dtoverlay=max98357a
重启之后就可以看到已经加载上了。是card1
查看声卡:
tom@raspberrypi:~ $ cat /proc/asound/cards
0 [Headphones ]: bcm2835_headpho - bcm2835 Headphones
bcm2835 Headphones
1 [MAX98357A ]: simple-card - MAX98357A
MAX98357A
2 [vc4hdmi ]: vc4-hdmi - vc4-hdmi
vc4-hdmi
此时在系统中也已经可以看到。
2.4 硬件连接
根据引脚说明连接即可。
详细连接如下:
MAX98357A Pin | Raspberry Pi 3B Pin | 描述 |
---|---|---|
DIN | GPIO21 / Pin 40 | I²S 数据输出 |
BCLK | GPIO18 / Pin 12 | I²S 时钟 |
LRCLK | GPIO19 / Pin 35 | I²S 帧时钟 |
SD_MODE | GPIO4 / Pin 7(可选) | 控制 DAC 启动(如果驱动中有) |
GND | Pin 6 或任意 GND | 地 |
VDD | Pin 1 (3.3V) 或 Pin 2 (5V) | 推荐 5V 供电,声音更大 |
GAIN0/1 | 接地或浮空(按需要) | 控制输出增益 |
OUTP/OUTN | 连接喇叭 | 差分输出(无须耳放) |
直接播放card1就可以听到声音了。(card0依然是3.5mm耳机口)
也可以安装mplayer播放mp3,此时要指定是card1。
这里还有一个问题,MAX98357是一个 “数字音频功放”(DAC+AMP),本身没有可控音量寄存器。所以没法在系统中控制音量,比如用alsamixer这些工具。只能在播放的时候增加参数-volume 30来控制。
3 代码学习
3.1 设备树
在./arch/arm/boot/dts/overlays/max98357a-overlay.dts
tom@raspberrypi:~/linux $ cat ./arch/arm/boot/dts/overlays/max98357a-overlay.dts
// Overlay for Maxim MAX98357A audio DAC
// dtparams:
// no-sdmode - SD_MODE pin not managed by driver.
// sdmode-pin - Specify GPIO pin to which SD_MODE is connected (default 4).
/dts-v1/;
/plugin/;
/ {
compatible = "brcm,bcm2835";
/* Enable I2S */
fragment@0 {
target = <&i2s_clk_producer>;
__overlay__ {
status = "okay";
};
};
/* DAC whose SD_MODE pin is managed by driver (via GPIO pin) */
fragment@1 {
target-path = "/";
__overlay__ {
max98357a_dac: max98357a {
compatible = "maxim,max98357a";
#sound-dai-cells = <0>;
sdmode-gpios = <&gpio 4 0>; /* 2nd word overwritten by sdmode-pin parameter */
status = "okay";
};
};
};
/* DAC whose SD_MODE pin is not managed by driver */
fragment@2 {
target-path = "/";
__dormant__ {
max98357a_nsd: max98357a {
compatible = "maxim,max98357a";
#sound-dai-cells = <0>;
status = "okay";
};
};
};
/* Soundcard connecting I2S to DAC with SD_MODE */
fragment@3 {
target = <&sound>;
__overlay__ {
compatible = "simple-audio-card";
simple-audio-card,format = "i2s";
simple-audio-card,name = "MAX98357A";
status = "okay";
simple-audio-card,cpu {
sound-dai = <&i2s_clk_producer>;
};
simple-audio-card,codec {
sound-dai = <&max98357a_dac>;
};
};
};
/* Soundcard connecting I2S to DAC without SD_MODE */
fragment@4 {
target = <&sound>;
__dormant__ {
compatible = "simple-audio-card";
simple-audio-card,format = "i2s";
simple-audio-card,name = "MAX98357A";
status = "okay";
simple-audio-card,cpu {
sound-dai = <&i2s_clk_producer>;
};
simple-audio-card,codec {
sound-dai = <&max98357a_nsd>;
};
};
};
__overrides__ {
no-sdmode = <0>,"-1+2-3+4";
sdmode-pin = <&max98357a_dac>,"sdmode-gpios:4";
};
};
首先是打开I2S接口。i2s_clk_producer
fragment@0 {
target = <&i2s_clk_producer>;
__overlay__ {
status = "okay";
};
};
里面有4个fragment,通过参数来确定使用哪个。没有参数就是第一个。这个时候带了SD_MODE。多用了一个GPIO去控制器件开关,感觉主要是用于低功耗。
3.2 驱动代码
驱动代码是max98357a.c
可以看出,这个代码还是比较祖传,15年的,差不多10年了。
tom@raspberrypi:~/linux $ cat ./sound/soc/codecs/max98357a.c
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright (c) 2010-2011,2013-2015 The Linux Foundation. All rights reserved.
*
* max98357a.c -- MAX98357A ALSA SoC Codec driver
*/
#include <linux/acpi.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/gpio/consumer.h>
#include <linux/kernel.h>
#include <linux/mod_devicetable.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <sound/pcm.h>
#include <sound/soc.h>
#include <sound/soc-dai.h>
#include <sound/soc-dapm.h>
struct max98357a_priv {
struct gpio_desc *sdmode;
unsigned int sdmode_delay;
int sdmode_switch;
};
static int max98357a_daiops_trigger(struct snd_pcm_substream *substream,
int cmd, struct snd_soc_dai *dai)
{
struct snd_soc_component *component = dai->component;
struct max98357a_priv *max98357a =
snd_soc_component_get_drvdata(component);
if (!max98357a->sdmode)
return 0;
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_RESUME:
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
mdelay(max98357a->sdmode_delay);
if (max98357a->sdmode_switch) {
gpiod_set_value(max98357a->sdmode, 1);
dev_dbg(component->dev, "set sdmode to 1");
}
break;
case SNDRV_PCM_TRIGGER_STOP:
case SNDRV_PCM_TRIGGER_SUSPEND:
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
gpiod_set_value(max98357a->sdmode, 0);
dev_dbg(component->dev, "set sdmode to 0");
break;
}
return 0;
}
static int max98357a_sdmode_event(struct snd_soc_dapm_widget *w,
struct snd_kcontrol *kcontrol, int event)
{
struct snd_soc_component *component =
snd_soc_dapm_to_component(w->dapm);
struct max98357a_priv *max98357a =
snd_soc_component_get_drvdata(component);
if (event & SND_SOC_DAPM_POST_PMU)
max98357a->sdmode_switch = 1;
else if (event & SND_SOC_DAPM_POST_PMD)
max98357a->sdmode_switch = 0;
return 0;
}
static const struct snd_soc_dapm_widget max98357a_dapm_widgets[] = {
SND_SOC_DAPM_OUTPUT("Speaker"),
SND_SOC_DAPM_OUT_DRV_E("SD_MODE", SND_SOC_NOPM, 0, 0, NULL, 0,
max98357a_sdmode_event,
SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_POST_PMD),
};
static const struct snd_soc_dapm_route max98357a_dapm_routes[] = {
{"SD_MODE", NULL, "HiFi Playback"},
{"Speaker", NULL, "SD_MODE"},
};
static const struct snd_soc_component_driver max98357a_component_driver = {
.dapm_widgets = max98357a_dapm_widgets,
.num_dapm_widgets = ARRAY_SIZE(max98357a_dapm_widgets),
.dapm_routes = max98357a_dapm_routes,
.num_dapm_routes = ARRAY_SIZE(max98357a_dapm_routes),
.idle_bias_on = 1,
.use_pmdown_time = 1,
.endianness = 1,
};
static const struct snd_soc_dai_ops max98357a_dai_ops = {
.trigger = max98357a_daiops_trigger,
};
static struct snd_soc_dai_driver max98357a_dai_driver = {
.name = "HiFi",
.playback = {
.stream_name = "HiFi Playback",
.formats = SNDRV_PCM_FMTBIT_S16 |
SNDRV_PCM_FMTBIT_S24 |
SNDRV_PCM_FMTBIT_S32,
.rates = SNDRV_PCM_RATE_8000 |
SNDRV_PCM_RATE_16000 |
SNDRV_PCM_RATE_32000 |
SNDRV_PCM_RATE_44100 |
SNDRV_PCM_RATE_48000 |
SNDRV_PCM_RATE_88200 |
SNDRV_PCM_RATE_96000,
.rate_min = 8000,
.rate_max = 96000,
.channels_min = 1,
.channels_max = 2,
},
.ops = &max98357a_dai_ops,
};
static int max98357a_platform_probe(struct platform_device *pdev)
{
struct max98357a_priv *max98357a;
int ret;
max98357a = devm_kzalloc(&pdev->dev, sizeof(*max98357a), GFP_KERNEL);
if (!max98357a)
return -ENOMEM;
max98357a->sdmode = devm_gpiod_get_optional(&pdev->dev,
"sdmode", GPIOD_OUT_LOW);
if (IS_ERR(max98357a->sdmode))
return PTR_ERR(max98357a->sdmode);
ret = device_property_read_u32(&pdev->dev, "sdmode-delay",
&max98357a->sdmode_delay);
if (ret) {
max98357a->sdmode_delay = 0;
dev_dbg(&pdev->dev,
"no optional property 'sdmode-delay' found, "
"default: no delay\n");
}
dev_set_drvdata(&pdev->dev, max98357a);
return devm_snd_soc_register_component(&pdev->dev,
&max98357a_component_driver,
&max98357a_dai_driver, 1);
}
#ifdef CONFIG_OF
static const struct of_device_id max98357a_device_id[] = {
{ .compatible = "maxim,max98357a" },
{ .compatible = "maxim,max98360a" },
{}
};
MODULE_DEVICE_TABLE(of, max98357a_device_id);
#endif
#ifdef CONFIG_ACPI
static const struct acpi_device_id max98357a_acpi_match[] = {
{ "MX98357A", 0 },
{ "MX98360A", 0 },
{},
};
MODULE_DEVICE_TABLE(acpi, max98357a_acpi_match);
#endif
static struct platform_driver max98357a_platform_driver = {
.driver = {
.name = "max98357a",
.of_match_table = of_match_ptr(max98357a_device_id),
.acpi_match_table = ACPI_PTR(max98357a_acpi_match),
},
.probe = max98357a_platform_probe,
};
module_platform_driver(max98357a_platform_driver);
MODULE_DESCRIPTION("Maxim MAX98357A Codec Driver");
MODULE_LICENSE("GPL v2");
这里首先设置了codec的参数
.playback = {
.formats = S16/S24/S32, // 支持的位深
.rates = 8k ~ 96kHz, // 采样率范围
.channels_min/max = 1~2 // 单声道或双声道
}
然后主要还是围绕着SD_MODE来操作的。播放时才给芯片上电,一旦停止播放,则直接断电。
tom@raspberrypi:~/alsa $ echo function | sudo tee /sys/kernel/debug/tracing/current_tracer
tom@raspberrypi:~/alsa $ echo max98357a_daiops_trigger | sudo tee /sys/kernel/debug/tracing/set_ftrace_filter
tom@raspberrypi:~/alsa $ sudo bash -c 'echo 1 > /sys/kernel/debug/tracing/tracing_on'
tom@raspberrypi:~/alsa $ sudo cat /sys/kernel/debug/tracing/trace | grep max98
mplayer-2146 [003] d..1. 3792.358634: max98357a_daiops_trigger <-snd_soc_pcm_dai_trigger
mplayer-2146 [001] d..1. 3815.900668: max98357a_daiops_trigger <-snd_soc_pcm_dai_trigger
看来就是在播放时拉高SD_MODE也就是GPIO4,停止时拉低。
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_RESUME:
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
mdelay(max98357a->sdmode_delay);
if (max98357a->sdmode_switch) {
gpiod_set_value(max98357a->sdmode, 1);
dev_dbg(component->dev, "set sdmode to 1");
}
break;
case SNDRV_PCM_TRIGGER_STOP:
case SNDRV_PCM_TRIGGER_SUSPEND:
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
gpiod_set_value(max98357a->sdmode, 0);
dev_dbg(component->dev, "set sdmode to 0");
break;
}
4 后记
好了,总算写完了。其实可以对比一下之前esp32使用i2s(总线学习5--I2S_max98357接喇叭教程-CSDN博客)。 之前使用python播放wav,连同读取解析wav,转码,相当于干了很多播放器的活,代码也没有超过40行。在linux上使用ALSA真的是复杂了非常非常多。有时候真的怀疑这些是不是过度封装。很多时候代码的存在,到底是一个技术问题,还是一个管理问题,真的要打个问号。。。
不过linux的好处就是有大量的现存代码,如果对linux的机制熟悉,可以简单配置一下就可以发声。这也算勉强方便的地方吧。
看了一下驱动,其实干的事真不多,核心的I2S也没有涉及,后面可能针对ALSA的这部分再单独看看吧。