Linux device driver for Zedboard audio (2/2)
Precaution
This article is not an official documentation of Linux sound system architecture. If you need more details, or official documentation, please refer to the following link. Or, download Linux kernel source code, and generate documents using Doxygen.
Advanced Linux Sound Architecture (ALSA) project homepage
Summary
This is Linux device driver development for Zedboard audio system part 2/2. The target hardware is described in the articles below;
Zedboard Audio Hardware Design (1/2)
Zedboard Audio Hardware Design (2/2)
In last article, I explained how to configure Linux kernel to include all necessary drivers for this hardware.
Linux device driver for Zedboard audio (1/2)
But, since Xilinx PL sound card driver doesn’t know which audio CODEC we are using, it doesn’t process audio.
In this article, I’ll talk a bit about ALSA (Advanced Linux Sound Architecture), and explain how to make this hardware working.
ALSA (Advanced Linux Sound Architecture)
ALSA can be divided into 3 different parts.
- Kernel driver layer
- Library layer (asound.so)
- User applications (aplay, arecord, speaker-test, alsamixer, and so on)
The first one is in the kernel, and the rest is in userland. To make our device working, we need to understand kernel driver layer.
In PC, sound components are provided as “sound card”, which has everything necessary to process audio. But in embedded system, more specifically, in SoC (System on Chip), we might have multiple components connected to the CPU via different buses. For example, in our device, Audio Formatter (including DMA), I2S transmitter, and I2S receiver are on chip, meaning that, those are memory mapped devices, and CODEC is connected via I2S bus, and I2C bus (I2S for audio signal, I2C for control signal). Also, in different configuration, there might be multiple CODECs on board, directly connecting to each other (only control paths are connected to CPU).
ALSA has driver component for SoC system. This is called ASoC (ALSA SoC). ASoC driver glues all those audio components (and drivers) together, and instantiates it as “sound card”. So the kernel can treat it as if it is a sound card.
DAI link components
In ASoC driver, sound components are called DAI (Digital Audio Interface) link components. There are 3 different types of DAI link components in ALSA driver.
- CPU DAI
- CODEC DAI
- Platform DAI
CPU DAI manages physical audio links, and audio protocols (I2S, SPDIF, PCM, AC97, and so on).
CODEC DAI handles control signals like volume, mixing, routing, clocking and so on.
Platform DAI is basically audio DMA. But it could be different things.
In our hardware,
- I2S transmitter and I2S receiver … CPU DAI
- ADAU1761 Audio CODEC … CODEC DAI
- Audio formatter … Platform DAI.
Each DAI link component must have its own driver. In our device, we use drivers listed below;
- Xilinx I2S driver … I2S transmitter and I2S receiver
- Analog Devices AU1761 CODEC driver … ADAU1761 CODEC
- Xilinx Audio Formatter driver … Audio Formatter
In sound card driver (this is called “machine driver”), we need to instantiate DAI link component structure for each device. One important thing to note is that, those DAI link component structures must have one of “name” or “phandle” to device node. It can’t have both. My recommendation is to use “phandle”, because name has to be like “adau1761.0–0038”, which includes bus number and address in this case.
DAI link
There’s a structure called “DAI link”, which makes connections between those DAI link components. One DAI link can point to multiple CPUs, CODECs, and Platforms. This structure has instances of all DAI link components.
Machine driver
Sound card device driver is also called machine driver. In machine driver, we need to instantiate DAI link structure, which must have all the instances of each DAI link components in it.
In previous article, we used “Xilinx PL Sound Card” driver (sound/soc/xilinx/xlnx_pl_snd_card.c), but there are problems listed below;
- CODEC DAI is pointing to “snd_soc_dummy”, not the actual device
- In hw_param() callback, it doesn’t configure CODEC’s clock frequencies
- It doesn’t have “widget” structure and “route” structure
So I created another folder “zedboard” under “sound/soc/”, then, copied “sound/soc/xilinx/xlnx_pl_snd_card.c”, renaming it to “zed_pl_snd_card.c”.
First one is here. This macro is instantiating 3 DAI link components.
SND_SOC_DAILINK_DEFS(xlnx_i2s_capture,
DAILINK_COMP_ARRAY(COMP_CPU("xlnx_i2s_capture")),
DAILINK_COMP_ARRAY(COMP_DUMMY()),
DAILINK_COMP_ARRAY(COMP_PLATFORM(NULL)));SND_SOC_DAILINK_DEFS(xlnx_i2s_playback,
DAILINK_COMP_ARRAY(COMP_CPU("xlnx_i2s_playback")),
DAILINK_COMP_ARRAY(COMP_DUMMY()),
DAILINK_COMP_ARRAY(COMP_PLATFORM(NULL)));
This macro looks complicated, but this is instantiating DAI link structure like below (showing only one of 2);
static struct snd_soc_dai_link_component xlnx_i2s_playback_cpus[] = {
{ .name = "xlnx_i2s_playback_cpu",},
};
static struct snd_soc_dai_link_component xlnx_i2s_playback_codecs[] = {
{ .name = "snd-soc-dummy", .dai_name = "snd-soc-dummy" },
};
static struct snd_soc_dai_link_component xlnx_i2s_playback_platforms[] = {
{ .name = NULL,},
};
We need to change “COMP_DUMMY()” in second DAILINK_COMP_ARRAY(), which is instantiating CODEC DAI. We will use “phandle” to ADAU1761, so the name will be set to NULL.
SND_SOC_DAILINK_DEFS(xlnx_i2s_capture,
DAILINK_COMP_ARRAY(COMP_CPU("xlnx_i2s_capture")),
DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "adau-hifi")),
DAILINK_COMP_ARRAY(COMP_PLATFORM(NULL)));SND_SOC_DAILINK_DEFS(xlnx_i2s_playback,
DAILINK_COMP_ARRAY(COMP_CPU("xlnx_i2s_playback")),
DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "adau-hifi")),
DAILINK_COMP_ARRAY(COMP_PLATFORM(NULL)));
Also, by default, Xilinx PL sound card driver can instantiate multiple DAI links for various devices (SDI, HDMI and so on), but we just use I2S. I changed 2-dimensional array to linear array.
static struct snd_soc_dai_link zed_snd_dai[XLNX_MAX_PATHS] = {
{
.name = "xilinx-i2s_playback",
.stream_name = "xilinx-i2s",
SND_SOC_DAILINK_REG(xlnx_i2s_playback),
.ops = &zed_snd_card_ops,
},
{
.name = "xilinx-i2s_capture",
.stream_name = "xilinx-i2s",
SND_SOC_DAILINK_REG(xlnx_i2s_capture),
.ops = &zed_snd_card_ops,
},
};
We don’t have to change, but the macro “SND_SOC_DAILINK_REG()” will be expanded into lines below.
.cpus = xlnx_i2s_playback_cpus,
.num_cpus = ARRAY_SIZE(xlnx_i2s_playback_cpus),
.codecs = xlnx_i2s_playback_codecs,
.num_codecs = ARRAY_SIZE(xlnx_i2s_playback_codecs),
.platforms = xlnx_i2s_playback_platforms,
.num_platforms = ARRAY_SIZE(xlnx_i2s_playback_platforms),
So the DAI link structure now knows which DAI link components we are using.
Next, adding widgets and routes are easy. Put this in somewhere in driver.
static const struct snd_soc_dapm_widget zed_snd_widgets[] = {
SND_SOC_DAPM_SPK(“Line Out”, NULL),
SND_SOC_DAPM_HP(“Headphone Out”, NULL),
SND_SOC_DAPM_MIC(“Mic In”, NULL),
SND_SOC_DAPM_MIC(“Line In”, NULL),
};static const struct snd_soc_dapm_route zed_snd_routes[] = {
{ “Line Out”, NULL, “LOUT” },
{ “Line Out”, NULL, “ROUT” },
{ “Headphone Out”, NULL, “LHP” },
{ “Headphone Out”, NULL, “RHP” },
{ “Mic In”, NULL, “MICBIAS” },
{ “LINN”, NULL, “Mic In” },
{ “RINN”, NULL, “Mic In” },
{ “LAUX”, NULL, “Line In” },
{ “RAUX”, NULL, “Line In” },
};
Then, pass these variables in probe() function before registering the card.
// Widgets and routes
card->dapm_widgets = zed_snd_widgets;
card->num_dapm_widgets = ARRAY_SIZE(zed_snd_widgets);
card->dapm_routes = zed_snd_routes;
card->num_dapm_routes = ARRAY_SIZE(zed_snd_routes);
card->fully_routed = true;
Also, we need to get phandle to Audio CODEC. I added the field “audio-codec” in device tree by adding those lines in this for loop in probe().
for (i = XLNX_PLAYBACK; i < XLNX_MAX_PATHS; i++) {
struct device_node *pnode = of_parse_phandle(node[i], "xlnx,snd-pcm", 0);
// This is for CODEC (ADAU1761)
// New field is added to device tree
struct device_node *pcodec = of_parse_phandle(node[i], "audio-codec", 0);
Lastly, we need to modify hw_params() callback. Originally, this function assumes that, the audio_mclk is configurable. So every time hw_params() is called, it tries to change the audio_mclk clock frequency. This is not desirable since our clocking wizard outputs fixed frequency. Also, I tried to use I2S transmitter as clock master, but it didn’t work well (when the driver tries to set frequency divider, this driver hangs). So I changed Audio CODEC as bit clock and frame clock master. To do this, we need to set format for CODEC DAI.
fmt = SND_SOC_DAIFMT_CBM_CFM | SND_SOC_DAIFMT_I2S;
ret = snd_soc_dai_set_fmt(codec_dai, fmt);
This will set Audio CODEC as bit clock and frame clock master. Also, clock divider for I2S transmitter/receiver will be modified since the clock frequency calculation has been changed. Entire hw_params() function is like below (modified part is indicated as bold).
static int zed_snd_card_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
int ret, clk_div;
u32 ch, data_width, sample_rate;
unsigned int pll_rate;
struct pl_card_data *prv; unsigned int fmt; struct snd_soc_pcm_runtime *rtd = substream->private_data;
struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
struct snd_soc_dai *codec_dai = rtd->codec_dai; ch = params_channels(params);
data_width = params_width(params);
sample_rate = params_rate(params); prv = snd_soc_card_get_drvdata(rtd->card); // This is for CODEC DAI
// Set DAI format
// ADAU1761 will be the master, since I2S transmitter
// doesn't work with fixed frequency audio mclk
fmt = SND_SOC_DAIFMT_CBM_CFM | SND_SOC_DAIFMT_I2S;
ret = snd_soc_dai_set_fmt(codec_dai, fmt);
if (ret) {
return ret;
}
switch (sample_rate) {
case 48000:
case 8000:
case 12000:
case 16000:
case 24000:
case 32000:
case 96000:
pll_rate = 48000 * 1024;
break;
case 44100:
case 7350:
case 11025:
case 14700:
case 22050:
case 29400:
case 88200:
pll_rate = 44100 * 1024;
break;
default:
return -EINVAL;
} ret = snd_soc_dai_set_pll(codec_dai, ADAU17X1_PLL,
ADAU17X1_PLL_SRC_MCLK, clk_get_rate(prv->mclk), pll_rate);
if (ret) {
return ret;
} ret = snd_soc_dai_set_sysclk(codec_dai, ADAU17X1_CLK_SRC_PLL_AUTO, pll_rate,
SND_SOC_CLOCK_OUT);
if (ret) {
return ret;
} // This is for CPU DAI
/* only 2 channels supported */
if (ch != 2)
return -EINVAL; prv->mclk_val = clk_get_rate(prv->mclk);
clk_div = DIV_ROUND_UP(clk_get_rate(prv->mclk), 2 * ch * data_width * sample_rate);
prv->mclk_ratio = DIV_ROUND_UP(clk_get_rate(prv->mclk), sample_rate);
ret = snd_soc_dai_set_clkdiv(cpu_dai, 0, clk_div);return ret;
}
Entire driver source code is shown below.
https://gist.github.com/yuhei1horibe/ef86308cdc4dce4007789c3d32c767cc
Add Kconfig and Makefile in “zedboard” folder. Here’s my repo.
https://github.com/yuhei1horibe/Zedboard_audio_driver
To compile and link our driver, we need to modify Kconfig and Makefile in “sound/soc”.
Kconfig: Add the line below.
source "sound/soc/zedboard/Kconfig"
Makefile: Add the line below.
obj-$(CONFIG_SND_SOC) += zedboard/
Don’t forget to configure the kernel with our new driver.
Device tree
Since we added new field to I2S transmitter and receiver, we need to modify the device tree for these devices. Add some lines shown below to I2S transmitter and receiver.
i2s_receiver_0: i2s_receiver@43c20000 {
aud_mclk = <24000000>;
clock-names = "s_axi_ctrl_aclk", "aud_mclk", "m_axis_aud_aclk";
clocks = <&misc_clk_0>, <&misc_clk_1>, <&misc_clk_0>;
compatible = "xlnx,i2s-receiver-1.0", "xlnx,i2s-receiver-1.0";
interrupt-names = "irq";
interrupt-parent = <&intc>;
interrupts = <0 33 4>;
reg = <0x43c20000 0x10000>;
xlnx,depth = <0x80>;
xlnx,dwidth = <0x10>;
xlnx,num-channels = <0x1>;
xlnx,snd-pcm = <&audio_formatter_0>;
audio-codec = <&adau1761>;
};
i2s_transmitter_0: i2s_transmitter@43c10000 {
aud_mclk = <24000000>;
clock-names = "s_axi_ctrl_aclk", "aud_mclk", "s_axis_aud_aclk";
clocks = <&misc_clk_0>, <&misc_clk_1>, <&misc_clk_0>;
compatible = "xlnx,i2s-transmitter-1.0", "xlnx,i2s-transmitter-1.0";
interrupt-names = "irq";
interrupt-parent = <&intc>;
interrupts = <0 32 4>;
reg = <0x43c10000 0x10000>;
xlnx,depth = <0x80>;
xlnx,dwidth = <0x10>;
xlnx,num-channels = <0x1>;
xlnx,snd-pcm = <&audio_formatter_0>;
audio-codec = <&adau1761>;
};
That’s it!! If it works well, kernel will recognize our device, and we can play audio using “aplay” or “speaker-test”. One thing I noticed was, for some reason, aplay didn’t allow me to use “S24_LE” format. So I had to configure I2S transmitter and receiver to use 16 bits. Also, re-sampling didn’t work well for specifically 44.1kHz sampled audio file (88.2kHz wokred for some reason…). Other sample rates were working fine.