WM8960 audio codec

cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

WM8960 audio codec

WM8960 audio codec

pastedImage_10.png

Introduction

This is a sharing of my experience about porting the audio codec WM8960 in Linux BSP. I know this driver is not the perfect one.  If you find any place is not good in the driver, please let me know.

This driver is modified base on the wm8960.c in L3.0.35 Linux BSP. This document is talking about how to modify the codec driver. The Audio Codec driver is located in linux/sound/soc/codec/wm8960.c.

ALSA

The Audio Codec driver is based on ALSA to setup up all the things. For details, please see :

AlsaProject

Advanced Linux Sound Architecture - Wikipedia, the free encyclopedia.

kcontrols are defined in linux/include/sound/soc.h and soc-dapm.h.

Audio controls and path in WM8960

Left and Right Input signal path

pastedImage_11.pngpastedImage_12.png

Output signal path

pastedImage_17.png

Base on the input and output signal diagrams, we can setup all the controls that we want in the driver. Such as switches, volume controls, PGA controls and so on. All the controls below can be used in the alsamixer.

static const struct snd_kcontrol_new wm8960_snd_controls[] = {

SOC_DOUBLE_R_TLV("PCM DAC Playback Volume", WM8960_LDAC, WM8960_RDAC, 0, 255, 0, dac_tlv), //LDACVOL , RDACVOL

SOC_DOUBLE_R_TLV("PCM ADC Capture Volume", WM8960_LADC, WM8960_RADC, 0, 255, 0, adc_tlv), //LADCVOL, RADCVOL

SOC_DOUBLE_R_TLV("Headphone Volume", WM8960_LOUT1, WM8960_ROUT1, 0, 127, 0, out_tlv),

SOC_DOUBLE_R("Headphone ZC Switch", WM8960_LOUT1, WM8960_ROUT1,    7, 1, 0),

SOC_DOUBLE_R_TLV("Speaker Volume", WM8960_LOUT2, WM8960_ROUT2, 0, 127, 0, out_tlv),

SOC_DOUBLE_R("Speaker ZC Switch", WM8960_LOUT2, WM8960_ROUT2, 7, 1, 0),

SOC_DOUBLE_R("Capture Volume ZC Switch", WM8960_LINVOL, WM8960_RINVOL, 6, 1, 0),

SOC_SINGLE_TLV("Input Volume of LINPUT1", WM8960_LINVOL, 0, 63, 0, in_tlv),  //LINVOL

SOC_SINGLE_TLV("Input Volume of RINPUT1", WM8960_RINVOL, 0, 63, 0, in_tlv),  //RINVOL

SOC_SINGLE_TLV("Input Boost Volume LINPUT3", WM8960_INBMIX1, 4, 7, 0, boost_tlv),    //RIN3BOOST

SOC_SINGLE_TLV("Input Boost Volume LINPUT2", WM8960_INBMIX1, 1, 7, 0, boost_tlv),    //RIN2BOOST

SOC_SINGLE_TLV("Input Boost Volume RINPUT3", WM8960_INBMIX2, 4, 7, 0, boost_tlv),    //LIN3BOOST

SOC_SINGLE_TLV("Input Boost Volume RINPUT2", WM8960_INBMIX2, 1, 7, 0, boost_tlv),    //LIN2BOOST

SOC_SINGLE_TLV("PGA LB2LOVOL-Bypass from Left Boost", WM8960_BYPASS1, 4, 7, 1, bypass_tlv),    //LB2LOVOL

SOC_SINGLE_TLV("PGA LI2LOVOL-Bypass from LINPUT3", WM8960_LOUTMIX, 4, 7, 1, bypass_tlv),    //LI2LOVOL

SOC_SINGLE_TLV("PGA RB2ROVOL-Bypass from Right Boost", WM8960_BYPASS2, 4, 7, 1, bypass_tlv),    //RB2ROVOL

SOC_SINGLE_TLV("PGA RI2ROVOL-Bypass from RINPUT3", WM8960_ROUTMIX, 4, 7, 1, bypass_tlv),    //RI2ROVOL

SOC_SINGLE("Capture Mute (Left)", WM8960_LINVOL, 7, 1, 0), // LINMUTE

SOC_SINGLE("Capture Mute (Right)", WM8960_RINVOL, 7, 1, 0), // RINMUTE

SOC_SINGLE("PCM Playback -6dB Switch", WM8960_DACCTL1, 7, 1, 0),

SOC_SINGLE("Speaker DC gain", WM8960_CLASSD3, 3, 5, 0),

SOC_SINGLE("Speaker AC gain", WM8960_CLASSD3, 0, 5, 0),

SOC_ENUM("ADC Polarity", wm8960_enum[0]),

SOC_SINGLE("ADC High Pass Filter Switch", WM8960_DACCTL1, 0, 1, 0),

SOC_ENUM("DAC Polarity", wm8960_enum[2]),

SOC_SINGLE_BOOL_EXT("DAC Deemphasis Switch", 0, wm8960_get_deemph, wm8960_put_deemph),

SOC_ENUM("3D Filter Upper Cut-Off", wm8960_enum[2]),

SOC_ENUM("3D Filter Lower Cut-Off", wm8960_enum[3]),

SOC_SINGLE("3D Depth", WM8960_3D, 1, 15, 0),

SOC_SINGLE("3D", WM8960_3D, 0, 1, 0),

SOC_ENUM("ALC Function", wm8960_enum[4]),

SOC_SINGLE("ALC Max Gain", WM8960_ALC1, 4, 7, 0),

SOC_SINGLE("ALC Target", WM8960_ALC1, 0, 15, 1),

SOC_SINGLE("ALC Min Gain", WM8960_ALC2, 4, 7, 0),

SOC_SINGLE("ALC Hold Time", WM8960_ALC2, 0, 15, 0),

SOC_ENUM("ALC Mode", wm8960_enum[5]),

SOC_SINGLE("ALC Decay", WM8960_ALC3, 4, 15, 0),

SOC_SINGLE("ALC Attack", WM8960_ALC3, 0, 15, 0),

SOC_SINGLE("Noise Gate Threshold", WM8960_NOISEG, 3, 31, 0),

SOC_SINGLE("Noise Gate Switch", WM8960_NOISEG, 0, 1, 0),

SOC_ENUM("Capture Left Boost", wm8960_enum[6]), //LMICBOOST

SOC_ENUM("Capture Right Boost", wm8960_enum[7]), //RMICBOOT

};

1. SOC_SINGLE(xname, reg, shift, max, invert)

To setup a simple switch, we can use SOC_SINGLE.

pastedImage_64.png

e.g

SOC_SINGLE("PCM Playback -6dB Switch", WM8960_DACCTL1, 7, 1, 0),

- The name of this control is “PCM Playback -6dB Switch”.

- The register in WM8960 is WM8960_DACCTL1 . (the register address is 0x5, defined in wm8960.h)

- ‘7’ : The 7th bit in DACCTL1 register is used to enable/disable the DAC 6dB Attenuate.

- ‘1’ : Only one enable or disable option.

- ‘0’ : the value you set is not inverted.

2. SOC_SINGLE_TLV(xname, reg, shift, max, invert, tlv_array)

To setup a switch with levels, we can use SOC_SINGLE_TLV.

e.g.

pastedImage_27.png

In this example, the left input volume control is from 000000(-17.25dB) to 111111(+30dB). Each step is 0.75dB. Total is 63 steps.

SOC_SINGLE_TLV("Input Volume of LINPUT1", WM8960_LINVOL, 0, 63, 0, in_tlv),

The scale of in_tlv declare like this:

static const DECLARE_TLV_DB_SCALE(in_tlv, -1725, 75, 0);

in_tlv : the name of the scale.

-1725 : start from -17.25dB

75: each step is 0.75dB

0: the step is start from 0. For some volume control case the first step is "mute", then the step is start from 1 so change this number to 1.

for example: The 0000 0000 of the DAC volume control is digital mute.

pastedImage_62.png

static const DECLARE_TLV_DB_SCALE(dac_tlv, -12700, 50, 1);

3. SOC_DOUBLE_R(xname, reg_left, reg_right, xshift, xmax, xinvert)

SOC_DOUBLE_R is a stereo version of SOC_SINGLE. You can control the left and right channel at the same time.

e.g.

SOC_DOUBLE_R("Headphone ZC Switch", WM8960_LOUT1, WM8960_ROUT1, 7, 1, 0),

4. SOC_DOUBLE_R_TLV(xname, reg_left, reg_right, xshift, xmax, xinvert, tlv_array)

SOC_DOUBLE_R_TLV is the stereo version of SOC_SINGLE_TLV.

e.g.

SOC_DOUBLE_R_TLV("PCM DAC Playback Volume", WM8960_LDAC, WM8960_RDAC, 0, 255, 0, dac_tlv),

5. SOC_ENUM_SINGLE(xreg, xshift, xmax, xtexts)

When the control option are some texts, we can use SOC_ENUM to enum the options.

e.g. MIC boost

pastedImage_63.png

5.1. setup the array for the texts.

static const char *wm8960_micboost[] = {"0dB","+13dB","+20dB","+29dB"};

5.2. use the SOC_ENUM_SINGLE.

static const struct soc_enum wm8960_enum[] = {

     SOC_ENUM_SINGLE(WM8960_DACCTL1, 5, 4, wm8960_polarity),

     SOC_ENUM_SINGLE(WM8960_DACCTL2, 5, 4, wm8960_polarity),

     SOC_ENUM_SINGLE(WM8960_3D, 6, 2, wm8960_3d_upper_cutoff),

     SOC_ENUM_SINGLE(WM8960_3D, 5, 2, wm8960_3d_lower_cutoff),

     SOC_ENUM_SINGLE(WM8960_ALC1, 7, 4, wm8960_alcfunc),

     SOC_ENUM_SINGLE(WM8960_ALC3, 8, 2, wm8960_alcmode),

     SOC_ENUM_SINGLE(WM8960_LINPATH, 4, 4, wm8960_micboost),

     SOC_ENUM_SINGLE(WM8960_RINPATH, 4, 4, wm8960_micboost),

};

5.3.  use SOC_ENUM to add the controls for MIC boost.

SOC_ENUM("Capture Left Boost", wm8960_enum[6]),

SOC_ENUM("Capture Right Boost", wm8960_enum[7]),

After created all the controls, we can start to create the switches.

The following switches created base on the input and output diagrams. I used the same name from datasheet of each switch. It will more easy to find out the proper switch in alsamixer.

static const struct snd_kcontrol_new wm8960_lin[] = {

SOC_DAPM_SINGLE("<- LMP2", WM8960_LINPATH, 6, 1, 0), //LMP2

SOC_DAPM_SINGLE("<- LMP3", WM8960_LINPATH, 7, 1, 0), //LMP3

SOC_DAPM_SINGLE("<- LMN1", WM8960_LINPATH, 8, 1, 0), //LMN1

};

static const struct snd_kcontrol_new wm8960_lin_boost[] = {

SOC_DAPM_SINGLE("<- LMIC2B", WM8960_LINPATH, 3, 1, 0), //LMIC2B

};

static const struct snd_kcontrol_new wm8960_rin[] = {

SOC_DAPM_SINGLE("<- RMP2", WM8960_RINPATH, 6, 1, 0), //RMP2

SOC_DAPM_SINGLE("<- RMP3", WM8960_RINPATH, 7, 1, 0), //RMP3

SOC_DAPM_SINGLE("<- RMN1", WM8960_RINPATH, 8, 1, 0), //RMN1

};

static const struct snd_kcontrol_new wm8960_rin_boost[] = {

SOC_DAPM_SINGLE("<- RMIC2B", WM8960_RINPATH, 3, 1, 0), //RMIC2B

};

static const struct snd_kcontrol_new wm8960_loutput_mixer[] = {

SOC_DAPM_SINGLE("<- LD2LO", WM8960_LOUTMIX, 8, 1, 0), //LD2LO

SOC_DAPM_SINGLE("<- LI2LO", WM8960_LOUTMIX, 7, 1, 0), //LI2LO

SOC_DAPM_SINGLE("<- LB2LO", WM8960_BYPASS1, 7, 1, 0), //LB2LO

};

static const struct snd_kcontrol_new wm8960_routput_mixer[] = {

SOC_DAPM_SINGLE("<- RD2RO", WM8960_ROUTMIX, 8, 1, 0), //RD2RO

SOC_DAPM_SINGLE("<- RI2RO", WM8960_ROUTMIX, 7, 1, 0), //RI2RO

SOC_DAPM_SINGLE("<- RB2RO", WM8960_BYPASS2, 7, 1, 0), //RB2RO

};

static const struct snd_kcontrol_new wm8960_mono_out[] = {

SOC_DAPM_SINGLE("<- L2MO", WM8960_MONOMIX1, 7, 1, 0), //L2MO

SOC_DAPM_SINGLE("<- R2MO", WM8960_MONOMIX2, 7, 1, 0), //R2MO

};

Then, create the inputs, ADC, DAC, mixers, PGA and outputs.

static const struct snd_soc_dapm_widget wm8960_dapm_widgets[] = {

SND_SOC_DAPM_INPUT("LINPUT1"),

SND_SOC_DAPM_INPUT("RINPUT1"),

SND_SOC_DAPM_INPUT("LINPUT2"),

SND_SOC_DAPM_INPUT("RINPUT2"),

SND_SOC_DAPM_INPUT("LINPUT3"),

SND_SOC_DAPM_INPUT("RINPUT3"),

SND_SOC_DAPM_MICBIAS("MICB", WM8960_POWER1, 1, 0),

SND_SOC_DAPM_MIXER("Left Boost Mixer", WM8960_POWER1, 5, 0, wm8960_lin_boost, ARRAY_SIZE(wm8960_lin_boost)),

SND_SOC_DAPM_MIXER("Right Boost Mixer", WM8960_POWER1, 4, 0, wm8960_rin_boost, ARRAY_SIZE(wm8960_rin_boost)),

SND_SOC_DAPM_MIXER("Left Input PGA", WM8960_POWER3, 5, 0, wm8960_lin, ARRAY_SIZE(wm8960_lin)),

SND_SOC_DAPM_MIXER("Right Input PGA", WM8960_POWER3, 4, 0, wm8960_rin, ARRAY_SIZE(wm8960_rin)),

SND_SOC_DAPM_ADC("Left ADC", "Capture", WM8960_POWER1, 3, 0),

SND_SOC_DAPM_ADC("Right ADC", "Capture", WM8960_POWER1, 2, 0),

SND_SOC_DAPM_DAC("Left DAC", "Playback", WM8960_POWER2, 8, 0),

SND_SOC_DAPM_DAC("Right DAC", "Playback", WM8960_POWER2, 7, 0),

SND_SOC_DAPM_MIXER("Left Output Mixer", WM8960_POWER3, 3, 0, wm8960_loutput_mixer, ARRAY_SIZE(wm8960_loutput_mixer)),

SND_SOC_DAPM_MIXER("Right Output Mixer", WM8960_POWER3, 2, 0, wm8960_routput_mixer, ARRAY_SIZE(wm8960_routput_mixer)),

SND_SOC_DAPM_PGA("Left HP PGA", WM8960_POWER2, 6, 0, NULL, 0),

SND_SOC_DAPM_PGA("Right HP PGA", WM8960_POWER2, 5, 0, NULL, 0),

SND_SOC_DAPM_PGA("Left Speaker PGA", WM8960_POWER2, 4, 0, NULL, 0),

SND_SOC_DAPM_PGA("Right Speaker PGA", WM8960_POWER2, 3, 0, NULL, 0),

SND_SOC_DAPM_PGA("Right Speaker Output", WM8960_CLASSD1, 7, 0, NULL, 0), //SPK_OP_EN

SND_SOC_DAPM_PGA("Left Speaker Output", WM8960_CLASSD1, 6, 0, NULL, 0),

SND_SOC_DAPM_OUTPUT("SPK_LP"),

SND_SOC_DAPM_OUTPUT("SPK_LN"),

SND_SOC_DAPM_OUTPUT("HP_L"),

SND_SOC_DAPM_OUTPUT("HP_R"),

SND_SOC_DAPM_OUTPUT("SPK_RP"),

SND_SOC_DAPM_OUTPUT("SPK_RN"),

SND_SOC_DAPM_OUTPUT("OUT3"),

};

Now, we can start to route the audio path.

The path is from right to left , like : { “destination”, “switch”, “source” }

So, lets take the LINPUT1 to ADC as an example:

pastedImage_59.png

{ "Left Input PGA", "<- LMN1", "LINPUT1" },

{ "Left Boost Mixer", "<- LMIC2B", "Left Input PGA" },

{ "Left ADC", NULL, "Left Boost Mixer" },

Another example is DAC to Headphone.

pastedImage_61.png

                { "Left Output Mixer", "<- LD2LO", "Left DAC" },

                { "Right Output Mixer", "<- RD2RO", "Right DAC" },

                { "Left HP PGA", NULL, "Left Output Mixer" },

                { "Right HP PGA", NULL, "Right Output Mixer" },

                { "HP_L", NULL, "Left HP PGA" },

                { "HP_R", NULL, "Right HP PGA" },

In linux, you can run "alsamixer" to turn on/off the switches and adjust the volumes.

pastedImage_66.png

(this picture is an example of alsamixer of other codec, not for wm8960)

In alsamixer, use 'M' to turn the switch on/off,  use arrow keys to control the volumes.

wm8960_dai_ops is another important part in the driver.

Here is the ops of the wm8960_dai.

static struct snd_soc_dai_ops wm8960_dai_ops = {

                .hw_params = wm8960_hw_params,

                .digital_mute = wm8960_mute,

                .set_fmt = wm8960_set_dai_fmt,

                .set_clkdiv = wm8960_set_dai_clkdiv,

                .set_pll = wm8960_set_dai_pll,

};

wm8960_hw_params : used to set the PCM format (16bit/24bit), set the deemph, alc_rates and etc.

wm8960_mute:  used to mute the output

wm8960_set_dai_fmt : used to set the Master/Slave mode, set the interface format (I2S, DSP, Left justified and Right justified) and set the clock inversion.

wm8960_set_dai_clkdiv: used to set the CLK divider such as DACDIV, ADCDIV, BCLKDIV and so on.

wm8960_set_dai_pll: used to calculate the proper PLL values.

In the wm8960_set_dai_pll, we need to calculate the proper PLL values.

pastedImage_68.png

Base on the table, if the MCLK >14.4, the sysclk prescale divider is 2. So, set the sysclk pre-divider to 2 before finding pll_factors.

if (freq_in > 15000000 ) {

                                /* update sysclk div */

                                reg = snd_soc_read(codec, WM8960_CLOCK1) & 0x1f9;

                                snd_soc_write(codec, WM8960_CLOCK1, reg | 0x4);

                                clk_in = clk_in/2;

                                }

                if (freq_in && freq_out) {

                                ret = pll_factors(clk_in, freq_out, &pll_div);

                                if (ret != 0)

                                                return ret;

                }

In the driver, there are two names are important. One is the name of codec dai. The name is “wm8960”. Make sure this codec dai name is the same codec dai name used in the imx-wm8960.c.

static struct snd_soc_dai_driver wm8960_dai = {

                .name = "wm8960",

                .playback = {

                                .stream_name = "Playback",

                                .channels_min = 1,

                                .channels_max = 2,

                                .rates = WM8960_RATES,

                                .formats = WM8960_FORMATS,},

                .capture = {

                                .stream_name = "Capture",

                                .channels_min = 1,

                                .channels_max = 2,

                                .rates = WM8960_RATES,

                                .formats = WM8960_FORMATS,},

                .ops = &wm8960_dai_ops,

                .symmetric_rates = 1,

};

Another name is the I2C device id. Make sure the I2C name is same as the name used in your_board.c file.

static const struct i2c_device_id wm8960_i2c_id[] = {

                { "wm8960", 0 },

                { }

};

MODULE_DEVICE_TABLE(i2c, wm8960_i2c_id);

static struct i2c_driver wm8960_i2c_driver = {

                .driver = {

                                .name = "wm8960",

                                .owner = THIS_MODULE,

                },

                .probe =    wm8960_i2c_probe,

                .remove =   __devexit_p(wm8960_i2c_remove),

                .id_table = wm8960_i2c_id,

};

Here is the name used in your_board.c

static struct i2c_board_info mxc_i2c0_board_info[] __initdata = {

    {

        I2C_BOARD_INFO("wm8960", 0x1a),

    },

}

Machine driver imx-wm8960.c

Basically, the machine driver is the connection between wm8960.c and the i.MX.

It is modified base on the imx-wm8962.c. I didn't add the HP and MIC detection in this driver. If you need the HP and MIC detection, please take the imx-wm8962.c for reference.

Here is an example of my_board.c. The following platform data pass to the machine driver from my board.

static struct platform_device audio_wm8960_device = {

    .name = "imx-wm8960",

};

static struct mxc_audio_platform_data wm8960_pdata;

static int wm8960_clk_enable(int enable)

{

    if (enable)

        clk_enable(clko);

    else

        clk_disable(clko);

    return 0;

}

static int mxc_wm8960_init(void)

{

    int rate;

    clko = clk_get(NULL, "clko_clk");

    if (IS_ERR(clko)) {

        pr_err("can't get CLKO clock.\n");

        return PTR_ERR(clko);

    }

    /* both audio codec and comera use CLKO clk*/

    rate = clk_round_rate(clko, 24000000);

    clk_set_rate(clko, rate);

    wm8960_pdata.sysclk = rate;

    return 0;

}

static struct mxc_audio_platform_data wm8960_pdata = {

    .ssi_num = 1,

    .src_port = 2,

    .ext_port = 3,

    .init = mxc_wm8960_init,

    .clock_enable = wm8960_clk_enable,

};

I attach the driver and the machine driver here. I hope this document is useful for you.

Tags (2)
Attachments
Comments

Hi Jimmy,

Very good document. But for a external reference I´d rather point to a different link like the Linux Documentation or maybe AlsaProject

Very good point. I added your suggestion to my document. thanks.

Thanks jimmychan,

    Worthy Document.Really useful for Developers.

Hi all!

Thanks jimmychan​ for this document!

Please tell me, can I route audio paths in this codec accordingly to this scheme:

Mic1 (LINPUT1/2) directly to OUT3,

Line In (LINPUT3) directly to SPK_R,

Mic2 (RINPUT1/2) to CPU,

CPU to SPK_L.

"directly" means that sound passes through the codec with no help of CPU.

Is this configuration feasible?

Now I am able only to capture Mic1/2 with CPU and play captured sound.

No ratings
Version history
Last update:
‎07-12-2015 08:17 PM
Updated by: