From db1cb43a9dc1227a8be0c09c2277058908f0e84c Mon Sep 17 00:00:00 2001 From: Vit Stanicek Date: Thu, 11 Jul 2024 12:53:41 +0200 Subject: [PATCH] drivers: audio: Add the wm8904 driver Add driver for the Wolfson WM8904 audio codec. Signed-off-by: Hake Huang Signed-off-by: Yves Vandervennet Signed-off-by: Vit Stanicek --- drivers/audio/CMakeLists.txt | 1 + drivers/audio/Kconfig | 1 + drivers/audio/Kconfig.wm8904 | 9 + drivers/audio/wm8904.c | 681 +++++++++++++++++++++++++ drivers/audio/wm8904.h | 154 ++++++ dts/bindings/audio/wolfson,wm8904.yaml | 25 + 6 files changed, 871 insertions(+) create mode 100644 drivers/audio/Kconfig.wm8904 create mode 100644 drivers/audio/wm8904.c create mode 100644 drivers/audio/wm8904.h create mode 100644 dts/bindings/audio/wolfson,wm8904.yaml diff --git a/drivers/audio/CMakeLists.txt b/drivers/audio/CMakeLists.txt index c38c35c8807..0396ae23e48 100644 --- a/drivers/audio/CMakeLists.txt +++ b/drivers/audio/CMakeLists.txt @@ -9,3 +9,4 @@ zephyr_library_sources_ifdef(CONFIG_AUDIO_DMIC_NRFX_PDM dmic_nrfx_pdm.c) zephyr_library_sources_ifdef(CONFIG_AUDIO_TAS6422DAC tas6422dac.c) zephyr_library_sources_ifdef(CONFIG_AUDIO_CODEC_SHELL codec_shell.c) zephyr_library_sources_ifdef(CONFIG_AUDIO_DMIC_MCUX dmic_mcux.c) +zephyr_library_sources_ifdef(CONFIG_AUDIO_CODEC_WM8904 wm8904.c) diff --git a/drivers/audio/Kconfig b/drivers/audio/Kconfig index 500e36e7e44..70d02b5320a 100644 --- a/drivers/audio/Kconfig +++ b/drivers/audio/Kconfig @@ -37,6 +37,7 @@ source "subsys/logging/Kconfig.template.log_config" source "drivers/audio/Kconfig.tas6422dac" source "drivers/audio/Kconfig.tlv320dac" +source "drivers/audio/Kconfig.wm8904" endif # AUDIO_CODEC diff --git a/drivers/audio/Kconfig.wm8904 b/drivers/audio/Kconfig.wm8904 new file mode 100644 index 00000000000..35c7cd394b5 --- /dev/null +++ b/drivers/audio/Kconfig.wm8904 @@ -0,0 +1,9 @@ +# Copyright 2024 NXP +# SPDX-License-Identifier: Apache-2.0 + +config AUDIO_CODEC_WM8904 + bool "Wolfson WM8904 codec support" + select I2C + depends on DT_HAS_WOLFSON_WM8904_ENABLED + help + Enable support for the Wolfson WM8904 codec diff --git a/drivers/audio/wm8904.c b/drivers/audio/wm8904.c new file mode 100644 index 00000000000..8cfe9db900a --- /dev/null +++ b/drivers/audio/wm8904.c @@ -0,0 +1,681 @@ +/* + * Copyright 2024 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include + +#include +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(wolfson_wm8904); + +#include "wm8904.h" + +#define DT_DRV_COMPAT wolfson_wm8904 + +struct wm8904_driver_config { + struct i2c_dt_spec i2c; + int clock_source; + const struct device *mclk_dev; + clock_control_subsys_t mclk_name; +}; + +#define DEV_CFG(dev) ((const struct wm8904_driver_config *const)dev->config) + +static void wm8904_write_reg(const struct device *dev, uint8_t reg, uint16_t val); +static void wm8904_read_reg(const struct device *dev, uint8_t reg, uint16_t *val); +static void wm8904_update_reg(const struct device *dev, uint8_t reg, uint16_t mask, uint16_t val); +static void wm8904_soft_reset(const struct device *dev); + +static void wm8904_configure_output(const struct device *dev); + +static void wm8904_configure_input(const struct device *dev); + +static int wm8904_protocol_config(const struct device *dev, audio_dai_type_t dai_type) +{ + wm8904_protocol_t proto; + + switch (dai_type) { + case AUDIO_DAI_TYPE_I2S: + proto = kWM8904_ProtocolI2S; + break; + case AUDIO_DAI_TYPE_LEFT_JUSTIFIED: + proto = kWM8904_ProtocolLeftJustified; + break; + case AUDIO_DAI_TYPE_RIGHT_JUSTIFIED: + proto = kWM8904_ProtocolRightJustified; + break; + case AUDIO_DAI_TYPE_PCMA: + proto = kWM8904_ProtocolPCMA; + break; + case AUDIO_DAI_TYPE_PCMB: + proto = kWM8904_ProtocolPCMB; + break; + default: + return -EINVAL; + } + + wm8904_update_reg(dev, WM8904_REG_AUDIO_IF_1, (0x0003U | (1U << 4U)), (uint16_t)proto); + + LOG_DBG("Codec protocol: %#x", proto); + return 0; +} + +static int wm8904_audio_fmt_config(const struct device *dev, audio_dai_cfg_t *cfg, uint32_t mclk) +{ + wm8904_sample_rate_t wm_sample_rate; + uint32_t fs; + uint16_t wmfs_ratio; + uint16_t mclkDiv; + uint16_t word_size = cfg->i2s.word_size; + + switch (cfg->i2s.frame_clk_freq) { + case 8000: + wm_sample_rate = kWM8904_SampleRate8kHz; + break; + case 11025: + wm_sample_rate = kWM8904_SampleRate11025Hz; + break; + case 12000: + wm_sample_rate = kWM8904_SampleRate12kHz; + break; + case 16000: + wm_sample_rate = kWM8904_SampleRate16kHz; + break; + case 22050: + wm_sample_rate = kWM8904_SampleRate22050Hz; + break; + case 24000: + wm_sample_rate = kWM8904_SampleRate24kHz; + break; + case 32000: + wm_sample_rate = kWM8904_SampleRate32kHz; + break; + case 44100: + wm_sample_rate = kWM8904_SampleRate44100Hz; + break; + case 48000: + wm_sample_rate = kWM8904_SampleRate48kHz; + break; + default: + LOG_WRN("Invalid codec sample rate: %d", cfg->i2s.frame_clk_freq); + return -EINVAL; + } + + wm8904_read_reg(dev, WM8904_REG_CLK_RATES_0, &mclkDiv); + fs = (mclk >> (mclkDiv & 0x1U)) / cfg->i2s.frame_clk_freq; + + switch (fs) { + case 64: + wmfs_ratio = kWM8904_FsRatio64X; + break; + case 128: + wmfs_ratio = kWM8904_FsRatio128X; + break; + case 192: + wmfs_ratio = kWM8904_FsRatio192X; + break; + case 256: + wmfs_ratio = kWM8904_FsRatio256X; + break; + case 384: + wmfs_ratio = kWM8904_FsRatio384X; + break; + case 512: + wmfs_ratio = kWM8904_FsRatio512X; + break; + case 768: + wmfs_ratio = kWM8904_FsRatio768X; + break; + case 1024: + wmfs_ratio = kWM8904_FsRatio1024X; + break; + case 1408: + wmfs_ratio = kWM8904_FsRatio1408X; + break; + case 1536: + wmfs_ratio = kWM8904_FsRatio1536X; + break; + default: + LOG_WRN("Invalid Fs ratio: %d", fs); + return -EINVAL; + } + + /* Disable SYSCLK */ + wm8904_write_reg(dev, WM8904_REG_CLK_RATES_2, 0x00); + + /* Set Clock ratio and sample rate */ + wm8904_write_reg(dev, WM8904_REG_CLK_RATES_1, + ((wmfs_ratio) << 10U) | (uint16_t)(wm_sample_rate)); + + switch (cfg->i2s.word_size) { + case 16: + word_size = 0; + break; + case 20: + word_size = 1; + break; + case 24: + word_size = 2; + break; + case 32: + word_size = 3; + break; + default: + LOG_ERR("Word size %d bits not supported; falling back to 16 bits", + cfg->i2s.word_size); + word_size = 0; + break; + } + /* Set bit resolution */ + wm8904_update_reg(dev, WM8904_REG_AUDIO_IF_1, (0x000CU), ((uint16_t)(word_size) << 2U)); + + /* Enable SYSCLK */ + wm8904_write_reg(dev, WM8904_REG_CLK_RATES_2, 0x1007); + return 0; +} + +static int wm8904_out_update( + const struct device *dev, + audio_channel_t channel, + uint16_t val, + uint16_t mask +) +{ + switch (channel) { + case AUDIO_CHANNEL_FRONT_LEFT: + wm8904_update_reg(dev, WM8904_REG_ANALOG_OUT2_LEFT, mask, val); + return 0; + + case AUDIO_CHANNEL_FRONT_RIGHT: + wm8904_update_reg(dev, WM8904_REG_ANALOG_OUT2_RIGHT, mask, val); + return 0; + + case AUDIO_CHANNEL_HEADPHONE_LEFT: + wm8904_update_reg(dev, WM8904_REG_ANALOG_OUT1_LEFT, mask, val); + return 0; + + case AUDIO_CHANNEL_HEADPHONE_RIGHT: + wm8904_update_reg(dev, WM8904_REG_ANALOG_OUT1_RIGHT, mask, val); + return 0; + + case AUDIO_CHANNEL_ALL: + wm8904_update_reg(dev, WM8904_REG_ANALOG_OUT1_LEFT, mask, val); + wm8904_update_reg(dev, WM8904_REG_ANALOG_OUT1_RIGHT, mask, val); + wm8904_update_reg(dev, WM8904_REG_ANALOG_OUT2_LEFT, mask, val); + wm8904_update_reg(dev, WM8904_REG_ANALOG_OUT2_RIGHT, mask, val); + return 0; + + default: + return -EINVAL; + } +} + +static int wm8904_out_volume_config(const struct device *dev, audio_channel_t channel, int volume) +{ + /* Set volume values with VU = 0 */ + const uint16_t val = WM8904_REGVAL_OUT_VOL(0, 0, 1, volume); + const uint16_t mask = WM8904_REGMASK_OUT_VU + | WM8904_REGMASK_OUT_ZC + | WM8904_REGMASK_OUT_VOL; + + return wm8904_out_update(dev, channel, val, mask); +} + +static int wm8904_out_mute_config(const struct device *dev, audio_channel_t channel, bool mute) +{ + const uint16_t val = WM8904_REGVAL_OUT_VOL(mute, 0, 0, 0); + const uint16_t mask = WM8904_REGMASK_OUT_MUTE; + + return wm8904_out_update(dev, channel, val, mask); +} + +static int wm8904_in_update( + const struct device *dev, + audio_channel_t channel, + uint16_t mask, + uint16_t val +) +{ + switch (channel) { + case AUDIO_CHANNEL_FRONT_LEFT: + wm8904_update_reg(dev, WM8904_REG_ANALOG_LEFT_IN_0, mask, val); + return 0; + + case AUDIO_CHANNEL_FRONT_RIGHT: + wm8904_update_reg(dev, WM8904_REG_ANALOG_RIGHT_IN_0, mask, val); + return 0; + + case AUDIO_CHANNEL_ALL: + wm8904_update_reg(dev, WM8904_REG_ANALOG_LEFT_IN_0, mask, val); + wm8904_update_reg(dev, WM8904_REG_ANALOG_RIGHT_IN_0, mask, val); + return 0; + + default: + return -EINVAL; + } +} + +static int wm8904_in_volume_config(const struct device *dev, audio_channel_t channel, int volume) +{ + const uint16_t val = WM8904_REGVAL_IN_VOL(0, volume); + const uint16_t mask = WM8904_REGMASK_IN_MUTE; + + return wm8904_in_update(dev, channel, val, mask); +} + +static int wm8904_in_mute_config(const struct device *dev, audio_channel_t channel, bool mute) +{ + const uint16_t val = WM8904_REGVAL_IN_VOL(mute, 0); + const uint16_t mask = WM8904_REGMASK_IN_MUTE; + + return wm8904_in_update(dev, channel, val, mask); +} + +static int wm8904_route_input(const struct device *dev, audio_channel_t channel, uint32_t input) +{ + if (input < 1 || input > 3) { + return -EINVAL; + } + + uint8_t val = WM8904_REGVAL_INSEL(0, input - 1, input - 1, 0); + uint8_t mask = WM8904_REGMASK_INSEL_CMENA + | WM8904_REGMASK_INSEL_IP_SEL_P + | WM8904_REGMASK_INSEL_IP_SEL_N + | WM8904_REGMASK_INSEL_MODE; + uint8_t reg; + + switch (channel) { + case AUDIO_CHANNEL_FRONT_LEFT: + reg = WM8904_REG_ANALOG_LEFT_IN_1; + break; + + case AUDIO_CHANNEL_FRONT_RIGHT: + reg = WM8904_REG_ANALOG_RIGHT_IN_1; + break; + + default: + return -EINVAL; + } + + wm8904_update_reg(dev, reg, mask, val); + return 0; +} + +static void wm8904_set_master_clock(const struct device *dev, audio_dai_cfg_t *cfg, uint32_t sysclk) +{ + uint32_t sampleRate = cfg->i2s.frame_clk_freq; + uint32_t bitWidth = cfg->i2s.word_size; + uint32_t bclk = sampleRate * bitWidth * 2U; + uint32_t bclkDiv = 0U; + uint16_t audioInterface = 0U; + uint16_t sysclkDiv = 0U; + + wm8904_read_reg(dev, WM8904_REG_CLK_RATES_0, &sysclkDiv); + sysclk = sysclk >> (sysclkDiv & 0x1U); + LOG_DBG("Codec sysclk: %d", sysclk); + + if ((sysclk / bclk > 48U) || (bclk / sampleRate > 2047U) || (bclk / sampleRate < 8U)) { + LOG_ERR("Invalid BCLK clock divider configured."); + return; + } + + wm8904_read_reg(dev, WM8904_REG_AUDIO_IF_2, &audioInterface); + + audioInterface &= ~(uint16_t)0x1FU; + bclkDiv = (sysclk * 10U) / bclk; + LOG_INF("blk %d", bclk); + + switch (bclkDiv) { + case 10: + audioInterface |= 0U; + break; + case 15: + audioInterface |= 1U; + break; + case 20: + /* Avoid MISRA 16.4 violation */ + break; + case 30: + /* Avoid MISRA 16.4 violation */ + break; + case 40: + /* Avoid MISRA 16.4 violation */ + break; + case 50: + audioInterface |= (uint16_t)bclkDiv / 10U; + break; + case 55: + audioInterface |= 6U; + break; + case 60: + audioInterface |= 7U; + break; + case 80: + audioInterface |= 8U; + break; + case 100: + /* Avoid MISRA 16.4 violation */ + break; + case 110: + /* Avoid MISRA 16.4 violation */ + break; + case 120: + audioInterface |= (uint16_t)bclkDiv / 10U - 1U; + break; + case 160: + audioInterface |= 12U; + break; + case 200: + audioInterface |= 13U; + break; + case 220: + audioInterface |= 14U; + break; + case 240: + audioInterface |= 15U; + break; + case 250: + audioInterface |= 16U; + break; + case 300: + audioInterface |= 17U; + break; + case 320: + audioInterface |= 18U; + break; + case 440: + audioInterface |= 19U; + break; + case 480: + audioInterface |= 20U; + break; + default: + LOG_ERR("invalid audio interface for wm8904 %d", bclkDiv); + return; + } + + /* bclk divider */ + wm8904_write_reg(dev, WM8904_REG_AUDIO_IF_2, audioInterface); + /* bclk direction output */ + wm8904_update_reg(dev, WM8904_REG_AUDIO_IF_1, 1U << 6U, 1U << 6U); + + wm8904_update_reg(dev, WM8904_REG_GPIO_CONTROL_4, 0x8FU, 1U); + /* LRCLK direction and divider */ + audioInterface = (uint16_t)((1UL << 11U) | (bclk / sampleRate)); + wm8904_update_reg(dev, WM8904_REG_AUDIO_IF_3, 0xFFFU, audioInterface); +} + +static int wm8904_configure(const struct device *dev, struct audio_codec_cfg *cfg) +{ + uint16_t value; + const struct wm8904_driver_config *const dev_cfg = DEV_CFG(dev); + + if (cfg->dai_type >= AUDIO_DAI_TYPE_INVALID) { + LOG_ERR("dai_type not supported"); + return -EINVAL; + } + + wm8904_soft_reset(dev); + + if (cfg->dai_route == AUDIO_ROUTE_BYPASS) { + return 0; + } + + /* MCLK_INV=0, SYSCLK_SRC=0, TOCLK_RATE=0, OPCLK_ENA=1, + * CLK_SYS_ENA=1, CLK_DSP_ENA=1, TOCLK_ENA=1 + */ + wm8904_write_reg(dev, WM8904_REG_CLK_RATES_2, 0x000F); + + /* WSEQ_ENA=1, WSEQ_WRITE_INDEX=0_0000 */ + wm8904_write_reg(dev, WM8904_REG_WRT_SEQUENCER_0, 0x0100); + + /* WSEQ_ABORT=0, WSEQ_START=1, WSEQ_START_INDEX=00_0000 */ + wm8904_write_reg(dev, WM8904_REG_WRT_SEQUENCER_3, 0x0100); + + do { + wm8904_read_reg(dev, WM8904_REG_WRT_SEQUENCER_4, &value); + } while (((value & 1U) != 0U)); + + /* TOCLK_RATE_DIV16=0, TOCLK_RATE_x4=1, SR_MODE=0, MCLK_DIV=1 + * (Required for MMCs: SGY, KRT see erratum CE000546) + */ + wm8904_write_reg(dev, WM8904_REG_CLK_RATES_0, 0xA45F); + + /* INL_ENA=1, INR ENA=1 */ + wm8904_write_reg(dev, WM8904_REG_POWER_MGMT_0, 0x0003); + + /* HPL_PGA_ENA=1, HPR_PGA_ENA=1 */ + wm8904_write_reg(dev, WM8904_REG_POWER_MGMT_2, 0x0003); + + /* DACL_ENA=1, DACR_ENA=1, ADCL_ENA=1, ADCR_ENA=1 */ + wm8904_write_reg(dev, WM8904_REG_POWER_MGMT_6, 0x000F); + + /* ADC_OSR128=1 */ + wm8904_write_reg(dev, WM8904_REG_ANALOG_ADC_0, 0x0001); + + /* DACL_DATINV=0, DACR_DATINV=0, DAC_BOOST=00, LOOPBACK=0, AIFADCL_SRC=0, + * AIFADCR_SRC=1, AIFDACL_SRC=0, AIFDACR_SRC=1, ADC_COMP=0, ADC_COMPMODE=0, + * DAC_COMP=0, DAC_COMPMODE=0 + */ + wm8904_write_reg(dev, WM8904_REG_AUDIO_IF_0, 0x0050); + + /* DAC_MONO=0, DAC_SB_FILT-0, DAC_MUTERATE=0, DAC_UNMUTE RAMP=0, + * DAC_OSR128=1, DAC_MUTE=0, DEEMPH=0 (none) + */ + wm8904_write_reg(dev, WM8904_REG_DAC_DIG_1, 0x0040); + + /* Enable DC servos for headphone out */ + wm8904_write_reg(dev, WM8904_REG_DC_SERVO_0, 0x0003); + + /* HPL_RMV_SHORT=1, HPL_ENA_OUTP=1, HPL_ENA_DLY=1, HPL_ENA=1, + * HPR_RMV_SHORT=1, HPR_ENA_OUTP=1, HPR_ENA_DLY=1, HPR_ENA=1 + */ + wm8904_write_reg(dev, WM8904_REG_ANALOG_HP_0, 0x00FF); + + /* CP_DYN_PWR=1 */ + wm8904_write_reg(dev, WM8904_REG_CLS_W_0, 0x0001); + + /* CP_ENA=1 */ + wm8904_write_reg(dev, WM8904_REG_CHRG_PUMP_0, 0x0001); + + wm8904_protocol_config(dev, cfg->dai_type); + wm8904_update_reg(dev, WM8904_REG_CLK_RATES_2, (uint16_t)(1UL << 14U), + (uint16_t)(dev_cfg->clock_source)); + + if (dev_cfg->clock_source == 0) { + int err = clock_control_on(dev_cfg->mclk_dev, dev_cfg->mclk_name); + + if (err < 0) { + LOG_ERR("MCLK clock source enable fail: %d", err); + } + + err = clock_control_get_rate(dev_cfg->mclk_dev, dev_cfg->mclk_name, + &cfg->mclk_freq); + if (err < 0) { + LOG_ERR("MCLK clock source freq acquire fail: %d", err); + } + } + + wm8904_audio_fmt_config(dev, &cfg->dai_cfg, cfg->mclk_freq); + + if ((cfg->dai_cfg.i2s.options & I2S_OPT_FRAME_CLK_MASTER) == I2S_OPT_FRAME_CLK_MASTER) { + wm8904_set_master_clock(dev, &cfg->dai_cfg, cfg->mclk_freq); + } else { + /* BCLK/LRCLK default direction input */ + wm8904_update_reg(dev, WM8904_REG_AUDIO_IF_1, 1U << 6U, 0U); + wm8904_update_reg(dev, WM8904_REG_AUDIO_IF_3, (uint16_t)(1UL << 11U), 0U); + } + + switch (cfg->dai_route) { + case AUDIO_ROUTE_PLAYBACK: + wm8904_configure_output(dev); + break; + + case AUDIO_ROUTE_CAPTURE: + wm8904_configure_input(dev); + break; + + case AUDIO_ROUTE_PLAYBACK_CAPTURE: + wm8904_configure_output(dev); + wm8904_configure_input(dev); + break; + + default: + break; + } + + return 0; +} + +static void wm8904_start_output(const struct device *dev) +{ +} + +static void wm8904_stop_output(const struct device *dev) +{ +} + +static int wm8904_set_property(const struct device *dev, audio_property_t property, + audio_channel_t channel, audio_property_value_t val) +{ + switch (property) { + case AUDIO_PROPERTY_OUTPUT_VOLUME: + return wm8904_out_volume_config(dev, channel, val.vol); + + case AUDIO_PROPERTY_OUTPUT_MUTE: + return wm8904_out_mute_config(dev, channel, val.mute); + + case AUDIO_PROPERTY_INPUT_VOLUME: + return wm8904_in_volume_config(dev, channel, val.vol); + + case AUDIO_PROPERTY_INPUT_MUTE: + return wm8904_in_mute_config(dev, channel, val.mute); + } + + return -EINVAL; +} + +static int wm8904_apply_properties(const struct device *dev) +{ + /** + * Set VU = 1 for all output channels, VU takes effect for the whole + * channel pair. + */ + wm8904_update_reg( + dev, + WM8904_REG_ANALOG_OUT1_LEFT, + WM8904_REGVAL_OUT_VOL(0, 1, 0, 0), + WM8904_REGMASK_OUT_MUTE + ); + wm8904_update_reg(dev, + WM8904_REG_ANALOG_OUT2_LEFT, + WM8904_REGVAL_OUT_VOL(0, 1, 0, 0), + WM8904_REGMASK_OUT_MUTE + ); + + return 0; +} + +static void wm8904_write_reg(const struct device *dev, uint8_t reg, uint16_t val) +{ + const struct wm8904_driver_config *const dev_cfg = DEV_CFG(dev); + uint8_t data[3]; + int ret; + + /* data is reversed */ + data[0] = reg; + data[1] = (val >> 8) & 0xff; + data[2] = val & 0xff; + + ret = i2c_write(dev_cfg->i2c.bus, data, 3, dev_cfg->i2c.addr); + + if (ret != 0) { + LOG_ERR("i2c write to codec error %d", ret); + } + + LOG_DBG("REG:%02u VAL:%#02x", reg, val); +} + +static void wm8904_read_reg(const struct device *dev, uint8_t reg, uint16_t *val) +{ + const struct wm8904_driver_config *const dev_cfg = DEV_CFG(dev); + uint16_t value; + int ret; + + ret = i2c_write_read(dev_cfg->i2c.bus, dev_cfg->i2c.addr, ®, sizeof(reg), &value, + sizeof(value)); + if (ret == 0) { + *val = (value >> 8) & 0xff; + *val += ((value & 0xff) << 8); + /* update cache*/ + LOG_DBG("REG:%02u VAL:%#02x", reg, *val); + } +} + +static void wm8904_update_reg(const struct device *dev, uint8_t reg, uint16_t mask, uint16_t val) +{ + uint16_t reg_val = 0; + uint16_t new_value = 0; + + if (reg == 0x19) { + LOG_DBG("try write mask %#x val %#x", mask, val); + } + wm8904_read_reg(dev, reg, ®_val); + if (reg == 0x19) { + LOG_DBG("read %#x = %x", reg, reg_val); + } + new_value = (reg_val & ~mask) | (val & mask); + if (reg == 0x19) { + LOG_DBG("write %#x = %x", reg, new_value); + } + wm8904_write_reg(dev, reg, new_value); +} + +static void wm8904_soft_reset(const struct device *dev) +{ + wm8904_write_reg(dev, WM8904_REG_RESET, 0x00); +} + +static void wm8904_configure_output(const struct device *dev) +{ + wm8904_out_volume_config(dev, AUDIO_CHANNEL_ALL, WM8904_OUTPUT_VOLUME_DEFAULT); + wm8904_out_mute_config(dev, AUDIO_CHANNEL_ALL, false); + + wm8904_apply_properties(dev); +} + +static void wm8904_configure_input(const struct device *dev) +{ + wm8904_route_input(dev, AUDIO_CHANNEL_FRONT_LEFT, 2); + wm8904_route_input(dev, AUDIO_CHANNEL_FRONT_RIGHT, 2); + + wm8904_in_volume_config(dev, AUDIO_CHANNEL_ALL, WM8904_INPUT_VOLUME_DEFAULT); + wm8904_in_mute_config(dev, AUDIO_CHANNEL_ALL, false); +} + +static const struct audio_codec_api wm8904_driver_api = { + .configure = wm8904_configure, + .start_output = wm8904_start_output, + .stop_output = wm8904_stop_output, + .set_property = wm8904_set_property, + .apply_properties = wm8904_apply_properties, + .route_input = wm8904_route_input +}; + +#define WM8904_INIT(n) \ + static const struct wm8904_driver_config wm8904_device_config_##n = { \ + .i2c = I2C_DT_SPEC_INST_GET(n), \ + .clock_source = DT_INST_PROP_OR(n, clk_source, 0), \ + .mclk_dev = DEVICE_DT_GET(DT_INST_CLOCKS_CTLR_BY_NAME(n, mclk)), \ + .mclk_name = (clock_control_subsys_t)DT_INST_CLOCKS_CELL_BY_NAME(n, mclk, name)}; \ + \ + DEVICE_DT_INST_DEFINE(n, NULL, NULL, NULL, &wm8904_device_config_##n, \ + POST_KERNEL, CONFIG_AUDIO_CODEC_INIT_PRIORITY, &wm8904_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(WM8904_INIT) diff --git a/drivers/audio/wm8904.h b/drivers/audio/wm8904.h new file mode 100644 index 00000000000..da01fb00df1 --- /dev/null +++ b/drivers/audio/wm8904.h @@ -0,0 +1,154 @@ +/* + * Copyright 2024 NXP + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_CODEC_WM8904_H_ +#define ZEPHYR_INCLUDE_CODEC_WM8904_H_ + +/******************************************************************************* + * Definitions + ******************************************************************************/ + +#define WM8904_REG_RESET (0x00) +#define WM8904_REG_ANALOG_ADC_0 (0x0A) +#define WM8904_REG_POWER_MGMT_0 (0x0C) +#define WM8904_REG_POWER_MGMT_2 (0x0E) +#define WM8904_REG_POWER_MGMT_3 (0x0F) +#define WM8904_REG_POWER_MGMT_6 (0x12) +#define WM8904_REG_CLK_RATES_0 (0x14) +#define WM8904_REG_CLK_RATES_1 (0x15) +#define WM8904_REG_CLK_RATES_2 (0x16) +#define WM8904_REG_AUDIO_IF_0 (0x18) +#define WM8904_REG_AUDIO_IF_1 (0x19) +#define WM8904_REG_AUDIO_IF_2 (0x1A) +#define WM8904_REG_AUDIO_IF_3 (0x1B) +#define WM8904_REG_DAC_DIG_1 (0x21) +#define WM8904_REG_DAC_DIG_0 (0x27) +#define WM8904_REG_ANALOG_LEFT_IN_0 (0x2C) +#define WM8904_REG_ANALOG_RIGHT_IN_0 (0x2D) +#define WM8904_REG_ANALOG_LEFT_IN_1 (0x2E) +#define WM8904_REG_ANALOG_RIGHT_IN_1 (0x2F) +#define WM8904_REG_ANALOG_OUT1_LEFT (0x39) +#define WM8904_REG_ANALOG_OUT1_RIGHT (0x3A) +#define WM8904_REG_ANALOG_OUT12_ZC (0x3D) +#define WM8904_REG_DC_SERVO_0 (0x43) +#define WM8904_REG_ANALOG_HP_0 (0x5A) +#define WM8904_REG_CHRG_PUMP_0 (0x62) +#define WM8904_REG_CLS_W_0 (0x68) +#define WM8904_REG_WRT_SEQUENCER_0 (0x6C) +#define WM8904_REG_WRT_SEQUENCER_3 (0x6F) +#define WM8904_REG_WRT_SEQUENCER_4 (0x70) +#define WM8904_REG_DAC_DIGITAL_VOLUME_LEFT (0x1E) +#define WM8904_REG_DAC_DIGITAL_VOLUME_RIGHT (0x1F) +#define WM8904_REG_ADC_DIGITAL_VOLUME_LEFT (0x24) +#define WM8904_REG_ADC_DIGITAL_VOLUME_RIGHT (0x25) +#define WM8904_REG_ANALOG_OUT2_LEFT (0x3B) +#define WM8904_REG_ANALOG_OUT2_RIGHT (0x3C) +#define WM8904_REG_GPIO_CONTROL_4 (0x7C) +/* FLL control register */ +#define WM8904_REG_FLL_CONTROL_1 (0x74) +#define WM8904_REG_FLL_CONTROL_2 (0x75) +#define WM8904_REG_FLL_CONTROL_3 (0x76) +#define WM8904_REG_FLL_CONTROL_4 (0x77) +#define WM8904_REG_FLL_CONTROL_5 (0x78) +/* GPIO control register */ +#define WM8904_REG_GPIO_CONTROL_1 (0x79) +#define WM8904_REG_GPIO_CONTROL_2 (0x7A) +#define WM8904_REG_GPIO_CONTROL_3 (0x7B) +#define WM8904_REG_GPIO_CONTROL_4 (0x7C) +/* fll nco */ +#define WM8904_REG_FLL_NCO_TEST_0 (0xF7U) +#define WM8904_REG_FLL_NCO_TEST_1 (0xF8U) + +#define WM8904_OUTPUT_VOLUME_MIN (0b000000) +#define WM8904_OUTPUT_VOLUME_MAX (0b111111) +#define WM8904_OUTPUT_VOLUME_DEFAULT (0b101101) +#define WM8904_INPUT_VOLUME_MIN (0b00000) +#define WM8904_INPUT_VOLUME_MAX (0b11111) +#define WM8904_INPUT_VOLUME_DEFAULT (0b00101) + +/** + * WM8904_ANALOG_OUT1_LEFT, WM8904_ANALOG_OUT1_RIGHT (headphone outs), + * WM8904_ANALOG_OUT2_LEFT, WM8904_ANALOG_OUT2_RIGHT (line outs): + * [8] - MUTE: Output mute + * [7] - VU: Volume update, works for entire channel pair + * [6] - ZC: Zero-crossing enable + * [5:0] - VOL: 6-bit volume value + */ +#define WM8904_REGVAL_OUT_VOL(mute, vu, zc, vol) \ + (((mute & 0b1) << 8) | (vu & 0b1) << 7 | (zc & 0b1) << 6 | (vol & 0b000111111)) +#define WM8904_REGMASK_OUT_MUTE 0b100000000 +#define WM8904_REGMASK_OUT_VU 0b010000000 +#define WM8904_REGMASK_OUT_ZC 0b001000000 +#define WM8904_REGMASK_OUT_VOL 0b000111111 + +/** + * WM8904_ANALOG_LEFT_IN_0, WM8904_ANALOG_RIGHT_IN_0: + * [7] - MUTE: Input mute + * [4:0] - VOL: 5 bit volume value + */ +#define WM8904_REGVAL_IN_VOL(mute, vol) \ + ((mute & 0b1) << 7 | (vol & 0b00011111)) +#define WM8904_REGMASK_IN_MUTE 0b10000000 +#define WM8904_REGMASK_IN_VOLUME 0b00011111 + +/** + * WM8904_ANALOG_LEFT_IN_1, WM8904_ANALOG_RIGHT_IN_1: + * [6] - INx_CM_ENA: Common-mode rejection enable (N/A for single-mode) + * [5:4] - x_IP_SEL_N: Inverting input selection + * [3:2] - x_IP_SEL_P: Non-inverting input selection + * [1:0] - x_MODE: Input mode + */ +#define WM8904_REGVAL_INSEL(cm, nin, pin, mode) \ + (((cm) & 0b1) << 6) | (((nin) & 0b11) << 4) +#define WM8904_REGMASK_INSEL_CMENA 0b01000000 +#define WM8904_REGMASK_INSEL_IP_SEL_N 0b00110000 +#define WM8904_REGMASK_INSEL_IP_SEL_P 0b00001100 +#define WM8904_REGMASK_INSEL_MODE 0b00000011 + + +/*!@brief WM8904 maximum volume */ +#define WM8904_MAP_HEADPHONE_LINEOUT_MAX_VOLUME 0x3FU +#define WM8904_DAC_MAX_VOLUME 0xC0U + + +/*! @brief The audio data transfer protocol. */ +typedef enum _wm8904_protocol { + kWM8904_ProtocolI2S = 0x2, /*!< I2S type */ + kWM8904_ProtocolLeftJustified = 0x1, /*!< Left justified mode */ + kWM8904_ProtocolRightJustified = 0x0, /*!< Right justified mode */ + kWM8904_ProtocolPCMA = 0x3, /*!< PCM A mode */ + kWM8904_ProtocolPCMB = 0x3 | (1 << 4), /*!< PCM B mode */ +} wm8904_protocol_t; + +/*! @brief The SYSCLK / fs ratio. */ +typedef enum _wm8904_fs_ratio { + kWM8904_FsRatio64X = 0x0, /*!< SYSCLK is 64 * sample rate * frame width */ + kWM8904_FsRatio128X = 0x1, /*!< SYSCLK is 128 * sample rate * frame width */ + kWM8904_FsRatio192X = 0x2, /*!< SYSCLK is 192 * sample rate * frame width */ + kWM8904_FsRatio256X = 0x3, /*!< SYSCLK is 256 * sample rate * frame width */ + kWM8904_FsRatio384X = 0x4, /*!< SYSCLK is 384 * sample rate * frame width */ + kWM8904_FsRatio512X = 0x5, /*!< SYSCLK is 512 * sample rate * frame width */ + kWM8904_FsRatio768X = 0x6, /*!< SYSCLK is 768 * sample rate * frame width */ + kWM8904_FsRatio1024X = 0x7, /*!< SYSCLK is 1024 * sample rate * frame width */ + kWM8904_FsRatio1408X = 0x8, /*!< SYSCLK is 1408 * sample rate * frame width */ + kWM8904_FsRatio1536X = 0x9 /*!< SYSCLK is 1536 * sample rate * frame width */ +} wm8904_fs_ratio_t; + + +/*! @brief Sample rate. */ +typedef enum _wm8904_sample_rate { + kWM8904_SampleRate8kHz = 0x0, /*!< 8 kHz */ + kWM8904_SampleRate12kHz = 0x1, /*!< 12kHz */ + kWM8904_SampleRate16kHz = 0x2, /*!< 16kHz */ + kWM8904_SampleRate24kHz = 0x3, /*!< 24kHz */ + kWM8904_SampleRate32kHz = 0x4, /*!< 32kHz */ + kWM8904_SampleRate48kHz = 0x5, /*!< 48kHz */ + kWM8904_SampleRate11025Hz = 0x6, /*!< 11.025kHz */ + kWM8904_SampleRate22050Hz = 0x7, /*!< 22.05kHz */ + kWM8904_SampleRate44100Hz = 0x8 /*!< 44.1kHz */ +} wm8904_sample_rate_t; + +#endif /* ZEPHYR_INCLUDE_CODEC_WM8904_H_ */ diff --git a/dts/bindings/audio/wolfson,wm8904.yaml b/dts/bindings/audio/wolfson,wm8904.yaml new file mode 100644 index 00000000000..5bbd9c0e507 --- /dev/null +++ b/dts/bindings/audio/wolfson,wm8904.yaml @@ -0,0 +1,25 @@ +# Copyright 2024 NXP +# SPDX-License-Identifier: Apache-2.0 + +description: WM8904 audio codec + +include: [i2c-device.yaml] + +compatible: "wolfson,wm8904" + +properties: + reg: + required: true + clock-source: + type: string + default: "MCLK" + description: | + Codec's internal clock signal (SYSCLK) source selection. These options + are available: + - "MCLK": WM8904's MCLK pin (supplied by the host) + - "FLL": WM8904's FLL facility, can be free-running + The "MCLK" option is default, as this clock signal is usually supplied + by the host. + enum: + - "MCLK" + - "FLL"