mirror of
https://github.com/torvalds/linux.git
synced 2026-07-27 09:36:22 +02:00
The initial merged patch mixed up the bits for temp reg with LDG report,
now fixing to the right bits according to TRM (SLOU589A).
Fixes: 133c81f844 ("ASoC: codecs: Add TAS67524 quad-channel audio amplifier driver")
Signed-off-by: Sen Wang <sen@ti.com>
Link: https://patch.msgid.link/20260630183126.2588322-3-sen@ti.com
Signed-off-by: Mark Brown <broonie@kernel.org>
2195 lines
70 KiB
C
2195 lines
70 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* ALSA SoC Texas Instruments TAS67524 Quad-Channel Audio Amplifier
|
|
*
|
|
* Copyright (C) 2026 Texas Instruments Incorporated - https://www.ti.com/
|
|
* Author: Sen Wang <sen@ti.com>
|
|
*/
|
|
|
|
#include <linux/bitfield.h>
|
|
#include <linux/module.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/i2c.h>
|
|
#include <linux/regmap.h>
|
|
#include <linux/gpio/consumer.h>
|
|
#include <linux/regulator/consumer.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/property.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/workqueue.h>
|
|
#include <linux/pm_runtime.h>
|
|
#include <linux/iopoll.h>
|
|
#include <sound/soc.h>
|
|
#include <sound/tlv.h>
|
|
#include <sound/pcm_params.h>
|
|
|
|
#include "tas675x.h"
|
|
|
|
#define TAS675X_FAULT_CHECK_INTERVAL_MS 200
|
|
|
|
enum tas675x_type {
|
|
TAS67524,
|
|
};
|
|
|
|
struct tas675x_reg_param {
|
|
u8 page;
|
|
u8 reg;
|
|
u32 val;
|
|
};
|
|
|
|
struct tas675x_priv {
|
|
struct device *dev;
|
|
struct regmap *regmap;
|
|
enum tas675x_type dev_type;
|
|
/* Custom regmap lock; protects writes across books */
|
|
struct mutex io_lock;
|
|
|
|
struct gpio_desc *pd_gpio;
|
|
struct gpio_desc *stby_gpio;
|
|
struct regulator_bulk_data supplies[2];
|
|
struct regulator *vbat;
|
|
bool fast_boot;
|
|
|
|
int audio_slot;
|
|
int llp_slot;
|
|
int vpredict_slot;
|
|
int isense_slot;
|
|
int bclk_offset;
|
|
int slot_width;
|
|
unsigned int tx_mask;
|
|
|
|
int gpio1_func;
|
|
int gpio2_func;
|
|
|
|
unsigned long active_playback_dais;
|
|
unsigned long active_capture_dais;
|
|
unsigned int rate;
|
|
unsigned int saved_rtldg_en;
|
|
#define TAS675X_DSP_PARAM_NUM 2
|
|
struct tas675x_reg_param dsp_params[TAS675X_DSP_PARAM_NUM];
|
|
|
|
/* Fault monitor, disabled when Fault IRQ is used */
|
|
struct delayed_work fault_check_work;
|
|
#define TAS675X_FAULT_REGS_NUM 9
|
|
unsigned int last_status[TAS675X_FAULT_REGS_NUM];
|
|
};
|
|
|
|
static const char * const tas675x_supply_names[] = {
|
|
"dvdd", /* Digital power supply */
|
|
"pvdd", /* Output powerstage supply */
|
|
};
|
|
|
|
/* Page 1 setup initialization defaults */
|
|
static const struct reg_sequence tas675x_page1_init[] = {
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0xC8), 0x20), /* Charge pump clock */
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0x2F), 0x90), /* VBAT idle */
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0x29), 0x40), /* OC/CBC threshold */
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0x2E), 0x0C), /* OC/CBC config */
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0xC5), 0x02), /* OC/CBC config */
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0xC6), 0x10), /* OC/CBC config */
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0x1F), 0x20), /* OC/CBC config */
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0x16), 0x01), /* OC/CBC config */
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0x1E), 0x04), /* OC/CBC config */
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0xC1), 0x00), /* CH1 DC fault */
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0xC2), 0x04), /* CH2 DC fault */
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0xC3), 0x00), /* CH3 DC fault */
|
|
REG_SEQ0(TAS675X_PAGE_REG(1, 0xC4), 0x00), /* CH4 DC fault */
|
|
};
|
|
|
|
static inline const char *tas675x_state_name(unsigned int state)
|
|
{
|
|
switch (state & 0x0F) {
|
|
case TAS675X_STATE_DEEPSLEEP: return "DEEPSLEEP";
|
|
case TAS675X_STATE_LOAD_DIAG: return "LOAD_DIAG";
|
|
case TAS675X_STATE_SLEEP: return "SLEEP";
|
|
case TAS675X_STATE_HIZ: return "HIZ";
|
|
case TAS675X_STATE_PLAY: return "PLAY";
|
|
case TAS675X_STATE_FAULT: return "FAULT";
|
|
case TAS675X_STATE_AUTOREC: return "AUTOREC";
|
|
default: return "UNKNOWN";
|
|
}
|
|
}
|
|
|
|
static inline int tas675x_set_state_all(struct tas675x_priv *tas, u8 state)
|
|
{
|
|
const struct reg_sequence seq[] = {
|
|
REG_SEQ0(TAS675X_STATE_CTRL_CH1_CH2_REG, state),
|
|
REG_SEQ0(TAS675X_STATE_CTRL_CH3_CH4_REG, state),
|
|
};
|
|
|
|
return regmap_multi_reg_write(tas->regmap, seq, ARRAY_SIZE(seq));
|
|
}
|
|
|
|
static inline int tas675x_select_book(struct regmap *regmap, u8 book)
|
|
{
|
|
int ret;
|
|
|
|
/* Reset page to 0 before switching books */
|
|
ret = regmap_write(regmap, TAS675X_PAGE_CTRL_REG, 0x00);
|
|
if (!ret)
|
|
ret = regmap_write(regmap, TAS675X_BOOK_CTRL_REG, book);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* Raw I2C version of tas675x_select_book, must be called with io_lock held */
|
|
static inline int __tas675x_select_book(struct tas675x_priv *tas, u8 book)
|
|
{
|
|
struct i2c_client *client = to_i2c_client(tas->dev);
|
|
int ret;
|
|
|
|
/* Reset page to 0 before switching books */
|
|
ret = i2c_smbus_write_byte_data(client, TAS675X_PAGE_CTRL_REG, 0x00);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return i2c_smbus_write_byte_data(client, TAS675X_BOOK_CTRL_REG, book);
|
|
}
|
|
|
|
static int tas675x_dsp_mem_write(struct tas675x_priv *tas, u8 page, u8 reg, u32 val)
|
|
{
|
|
struct i2c_client *client = to_i2c_client(tas->dev);
|
|
u8 buf[4];
|
|
int ret;
|
|
|
|
/* DSP registers are 32 bit big-endian */
|
|
buf[0] = (val >> 24) & 0xFF;
|
|
buf[1] = (val >> 16) & 0xFF;
|
|
buf[2] = (val >> 8) & 0xFF;
|
|
buf[3] = val & 0xFF;
|
|
|
|
/*
|
|
* DSP regs in a different book, therefore block
|
|
* regmap access before completion.
|
|
*/
|
|
mutex_lock(&tas->io_lock);
|
|
|
|
ret = __tas675x_select_book(tas, TAS675X_BOOK_DSP);
|
|
if (ret)
|
|
goto out;
|
|
|
|
ret = i2c_smbus_write_byte_data(client, TAS675X_PAGE_CTRL_REG, page);
|
|
if (ret)
|
|
goto out;
|
|
|
|
ret = i2c_smbus_write_i2c_block_data(client, reg, sizeof(buf), buf);
|
|
|
|
out:
|
|
__tas675x_select_book(tas, TAS675X_BOOK_DEFAULT);
|
|
mutex_unlock(&tas->io_lock);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int tas675x_dsp_mem_read(struct tas675x_priv *tas, u8 page, u8 reg, u32 *val)
|
|
{
|
|
struct i2c_client *client = to_i2c_client(tas->dev);
|
|
u8 buf[4];
|
|
int ret;
|
|
|
|
/*
|
|
* DSP regs in a different book, therefore block
|
|
* regmap access before completion.
|
|
*/
|
|
mutex_lock(&tas->io_lock);
|
|
|
|
ret = __tas675x_select_book(tas, TAS675X_BOOK_DSP);
|
|
if (ret)
|
|
goto out;
|
|
|
|
ret = i2c_smbus_write_byte_data(client, TAS675X_PAGE_CTRL_REG, page);
|
|
if (ret)
|
|
goto out;
|
|
|
|
ret = i2c_smbus_read_i2c_block_data(client, reg, sizeof(buf), buf);
|
|
if (ret == sizeof(buf)) {
|
|
*val = (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
|
|
ret = 0;
|
|
} else if (ret >= 0) {
|
|
ret = -EIO;
|
|
}
|
|
|
|
out:
|
|
__tas675x_select_book(tas, TAS675X_BOOK_DEFAULT);
|
|
mutex_unlock(&tas->io_lock);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static const struct {
|
|
const char *name;
|
|
int val;
|
|
} tas675x_gpio_func_map[] = {
|
|
/* Output functions */
|
|
{ "low", TAS675X_GPIO_SEL_LOW },
|
|
{ "auto-mute", TAS675X_GPIO_SEL_AUTO_MUTE_ALL },
|
|
{ "auto-mute-ch4", TAS675X_GPIO_SEL_AUTO_MUTE_CH4 },
|
|
{ "auto-mute-ch3", TAS675X_GPIO_SEL_AUTO_MUTE_CH3 },
|
|
{ "auto-mute-ch2", TAS675X_GPIO_SEL_AUTO_MUTE_CH2 },
|
|
{ "auto-mute-ch1", TAS675X_GPIO_SEL_AUTO_MUTE_CH1 },
|
|
{ "sdout2", TAS675X_GPIO_SEL_SDOUT2 },
|
|
{ "sdout1", TAS675X_GPIO_SEL_SDOUT1 },
|
|
{ "warn", TAS675X_GPIO_SEL_WARN },
|
|
{ "fault", TAS675X_GPIO_SEL_FAULT },
|
|
{ "clock-sync", TAS675X_GPIO_SEL_CLOCK_SYNC },
|
|
{ "invalid-clock", TAS675X_GPIO_SEL_INVALID_CLK },
|
|
{ "high", TAS675X_GPIO_SEL_HIGH },
|
|
/* Input functions */
|
|
{ "mute", TAS675X_GPIO_IN_MUTE },
|
|
{ "phase-sync", TAS675X_GPIO_IN_PHASE_SYNC },
|
|
{ "sdin2", TAS675X_GPIO_IN_SDIN2 },
|
|
{ "deep-sleep", TAS675X_GPIO_IN_DEEP_SLEEP },
|
|
{ "hiz", TAS675X_GPIO_IN_HIZ },
|
|
{ "play", TAS675X_GPIO_IN_PLAY },
|
|
{ "sleep", TAS675X_GPIO_IN_SLEEP },
|
|
};
|
|
|
|
static int tas675x_gpio_func_parse(struct device *dev, const char *propname)
|
|
{
|
|
const char *str;
|
|
int i, ret;
|
|
|
|
ret = device_property_read_string(dev, propname, &str);
|
|
if (ret)
|
|
return -1;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(tas675x_gpio_func_map); i++) {
|
|
if (!strcmp(str, tas675x_gpio_func_map[i].name))
|
|
return tas675x_gpio_func_map[i].val;
|
|
}
|
|
|
|
dev_warn(dev, "Invalid %s value '%s'\n", propname, str);
|
|
return -1;
|
|
}
|
|
|
|
static const struct {
|
|
unsigned int reg;
|
|
unsigned int mask;
|
|
} tas675x_gpio_input_table[TAS675X_GPIO_IN_NUM] = {
|
|
[TAS675X_GPIO_IN_ID_MUTE] = {
|
|
TAS675X_GPIO_INPUT_MUTE_REG, TAS675X_GPIO_IN_MUTE_MASK },
|
|
[TAS675X_GPIO_IN_ID_PHASE_SYNC] = {
|
|
TAS675X_GPIO_INPUT_SYNC_REG, TAS675X_GPIO_IN_SYNC_MASK },
|
|
[TAS675X_GPIO_IN_ID_SDIN2] = {
|
|
TAS675X_GPIO_INPUT_SDIN2_REG, TAS675X_GPIO_IN_SDIN2_MASK },
|
|
[TAS675X_GPIO_IN_ID_DEEP_SLEEP] = {
|
|
TAS675X_GPIO_INPUT_SLEEP_HIZ_REG, TAS675X_GPIO_IN_DEEP_SLEEP_MASK },
|
|
[TAS675X_GPIO_IN_ID_HIZ] = {
|
|
TAS675X_GPIO_INPUT_SLEEP_HIZ_REG, TAS675X_GPIO_IN_HIZ_MASK },
|
|
[TAS675X_GPIO_IN_ID_PLAY] = {
|
|
TAS675X_GPIO_INPUT_PLAY_SLEEP_REG, TAS675X_GPIO_IN_PLAY_MASK },
|
|
[TAS675X_GPIO_IN_ID_SLEEP] = {
|
|
TAS675X_GPIO_INPUT_PLAY_SLEEP_REG, TAS675X_GPIO_IN_SLEEP_MASK },
|
|
};
|
|
|
|
static void tas675x_config_gpio_pin(struct regmap *regmap, int func_id,
|
|
unsigned int out_sel_reg,
|
|
unsigned int pin_idx,
|
|
unsigned int *gpio_ctrl)
|
|
{
|
|
int id;
|
|
|
|
if (func_id < 0)
|
|
return;
|
|
|
|
if (func_id & TAS675X_GPIO_FUNC_INPUT) {
|
|
/* 3-bit mux: 0 = disabled, 0b1 = GPIO1, 0b10 = GPIO2 */
|
|
id = func_id & ~TAS675X_GPIO_FUNC_INPUT;
|
|
regmap_update_bits(regmap,
|
|
tas675x_gpio_input_table[id].reg,
|
|
tas675x_gpio_input_table[id].mask,
|
|
(pin_idx + 1) << __ffs(tas675x_gpio_input_table[id].mask));
|
|
} else {
|
|
/* Output GPIO, update selection register and enable bit */
|
|
regmap_write(regmap, out_sel_reg, func_id);
|
|
*gpio_ctrl |= pin_idx ? TAS675X_GPIO2_OUTPUT_EN : TAS675X_GPIO1_OUTPUT_EN;
|
|
}
|
|
}
|
|
|
|
static int tas675x_rtldg_thresh_info(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_info *uinfo)
|
|
{
|
|
uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
|
|
uinfo->count = 1;
|
|
uinfo->value.integer.min = 0;
|
|
/* threshold reg ranges up to 24bit */
|
|
uinfo->value.integer.max = 0x00FFFFFF;
|
|
return 0;
|
|
}
|
|
|
|
static int tas675x_set_rtldg_thresh(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_value *ucontrol)
|
|
{
|
|
struct snd_soc_component *comp = snd_kcontrol_chip(kcontrol);
|
|
struct tas675x_priv *tas = snd_soc_component_get_drvdata(comp);
|
|
const struct tas675x_reg_param *t =
|
|
(const struct tas675x_reg_param *)kcontrol->private_value;
|
|
u32 val = ucontrol->value.integer.value[0];
|
|
int ret;
|
|
|
|
ret = tas675x_dsp_mem_write(tas, t->page, t->reg, val);
|
|
|
|
/* Cache the value */
|
|
if (!ret) {
|
|
int i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(tas->dsp_params); i++) {
|
|
if (tas->dsp_params[i].page == t->page &&
|
|
tas->dsp_params[i].reg == t->reg) {
|
|
tas->dsp_params[i].val = val;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Return 1 to notify change, or propagate error */
|
|
return ret ? ret : 1;
|
|
}
|
|
|
|
static int tas675x_get_rtldg_thresh(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_value *ucontrol)
|
|
{
|
|
struct snd_soc_component *comp = snd_kcontrol_chip(kcontrol);
|
|
struct tas675x_priv *tas = snd_soc_component_get_drvdata(comp);
|
|
const struct tas675x_reg_param *t =
|
|
(const struct tas675x_reg_param *)kcontrol->private_value;
|
|
u32 val = 0;
|
|
int ret;
|
|
|
|
ret = tas675x_dsp_mem_read(tas, t->page, t->reg, &val);
|
|
if (!ret)
|
|
ucontrol->value.integer.value[0] = val;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static const struct tas675x_reg_param tas675x_dsp_defaults[] = {
|
|
[TAS675X_DSP_PARAM_ID_OL_THRESH] = {
|
|
TAS675X_DSP_PAGE_RTLDG, TAS675X_DSP_RTLDG_OL_THRESH_REG },
|
|
[TAS675X_DSP_PARAM_ID_SL_THRESH] = {
|
|
TAS675X_DSP_PAGE_RTLDG, TAS675X_DSP_RTLDG_SL_THRESH_REG },
|
|
};
|
|
|
|
static_assert(ARRAY_SIZE(tas675x_dsp_defaults) == TAS675X_DSP_PARAM_NUM);
|
|
|
|
static int tas675x_set_dcldg_trigger(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_value *ucontrol)
|
|
{
|
|
struct snd_soc_component *comp = snd_kcontrol_chip(kcontrol);
|
|
struct tas675x_priv *tas = snd_soc_component_get_drvdata(comp);
|
|
unsigned int state, state34;
|
|
int ret;
|
|
|
|
if (!ucontrol->value.integer.value[0])
|
|
return 0;
|
|
|
|
if (snd_soc_component_active(comp))
|
|
return -EBUSY;
|
|
|
|
ret = pm_runtime_resume_and_get(tas->dev);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/*
|
|
* Abort automatic DC LDG retry loops (startup or init-after-fault)
|
|
* and clear faults before manual diagnostics.
|
|
*/
|
|
regmap_update_bits(tas->regmap, TAS675X_DC_LDG_CTRL_REG,
|
|
TAS675X_LDG_ABORT_BIT | TAS675X_LDG_BYPASS_BIT,
|
|
TAS675X_LDG_ABORT_BIT | TAS675X_LDG_BYPASS_BIT);
|
|
regmap_write(tas->regmap, TAS675X_RESET_REG, TAS675X_FAULT_CLEAR);
|
|
|
|
/* Wait for LOAD_DIAG to exit */
|
|
ret = regmap_read_poll_timeout(tas->regmap, TAS675X_STATE_REPORT_CH1_CH2_REG,
|
|
state, (state & 0x0F) != TAS675X_STATE_LOAD_DIAG &&
|
|
(state >> 4) != TAS675X_STATE_LOAD_DIAG,
|
|
TAS675X_POLL_INTERVAL_US,
|
|
TAS675X_STATE_TRANSITION_TIMEOUT_US);
|
|
ret |= regmap_read_poll_timeout(tas->regmap, TAS675X_STATE_REPORT_CH3_CH4_REG,
|
|
state34, (state34 & 0x0F) != TAS675X_STATE_LOAD_DIAG &&
|
|
(state34 >> 4) != TAS675X_STATE_LOAD_DIAG,
|
|
TAS675X_POLL_INTERVAL_US,
|
|
TAS675X_STATE_TRANSITION_TIMEOUT_US);
|
|
if (ret) {
|
|
dev_err(tas->dev,
|
|
"DC LDG: abort timeout (CH1/2=0x%02x [%s/%s], CH3/4=0x%02x [%s/%s])\n",
|
|
state, tas675x_state_name(state), tas675x_state_name(state >> 4),
|
|
state34, tas675x_state_name(state34), tas675x_state_name(state34 >> 4));
|
|
goto out_restore_ldg_ctrl;
|
|
}
|
|
|
|
/* Transition to HIZ state */
|
|
ret = tas675x_set_state_all(tas, TAS675X_STATE_HIZ_BOTH);
|
|
if (ret)
|
|
goto out_restore_ldg_ctrl;
|
|
|
|
/* Set LOAD_DIAG state for manual DC LDG */
|
|
ret = tas675x_set_state_all(tas, TAS675X_STATE_LOAD_DIAG_BOTH);
|
|
if (ret)
|
|
goto out_restore_ldg_ctrl;
|
|
|
|
/* Wait for device to transition to LOAD_DIAG state */
|
|
ret = regmap_read_poll_timeout(tas->regmap, TAS675X_STATE_REPORT_CH1_CH2_REG,
|
|
state, state == TAS675X_STATE_LOAD_DIAG_BOTH,
|
|
TAS675X_POLL_INTERVAL_US,
|
|
TAS675X_STATE_TRANSITION_TIMEOUT_US);
|
|
ret |= regmap_read_poll_timeout(tas->regmap, TAS675X_STATE_REPORT_CH3_CH4_REG,
|
|
state34, state34 == TAS675X_STATE_LOAD_DIAG_BOTH,
|
|
TAS675X_POLL_INTERVAL_US,
|
|
TAS675X_STATE_TRANSITION_TIMEOUT_US);
|
|
if (ret) {
|
|
dev_err(tas->dev,
|
|
"DC LDG: LOAD_DIAG timeout (CH1/2=0x%02x [%s/%s], CH3/4=0x%02x [%s/%s])\n",
|
|
state, tas675x_state_name(state), tas675x_state_name(state >> 4),
|
|
state34, tas675x_state_name(state34), tas675x_state_name(state34 >> 4));
|
|
goto out_restore_hiz;
|
|
}
|
|
|
|
/* Clear ABORT and BYPASS bits to enable manual DC LDG */
|
|
ret = regmap_update_bits(tas->regmap, TAS675X_DC_LDG_CTRL_REG,
|
|
TAS675X_LDG_ABORT_BIT | TAS675X_LDG_BYPASS_BIT,
|
|
0);
|
|
if (ret)
|
|
goto out_restore_hiz;
|
|
|
|
dev_dbg(tas->dev, "DC LDG: Started\n");
|
|
|
|
/* Poll all channels for SLEEP state */
|
|
ret = regmap_read_poll_timeout(tas->regmap, TAS675X_STATE_REPORT_CH1_CH2_REG,
|
|
state, state == TAS675X_STATE_SLEEP_BOTH,
|
|
TAS675X_POLL_INTERVAL_US,
|
|
TAS675X_DC_LDG_TIMEOUT_US);
|
|
ret |= regmap_read_poll_timeout(tas->regmap, TAS675X_STATE_REPORT_CH3_CH4_REG,
|
|
state34, state34 == TAS675X_STATE_SLEEP_BOTH,
|
|
TAS675X_POLL_INTERVAL_US,
|
|
TAS675X_DC_LDG_TIMEOUT_US);
|
|
if (ret) {
|
|
dev_err(tas->dev,
|
|
"DC LDG: SLEEP timeout (CH1/2=0x%02x [%s/%s], CH3/4=0x%02x [%s/%s])\n",
|
|
state, tas675x_state_name(state), tas675x_state_name(state >> 4),
|
|
state34, tas675x_state_name(state34), tas675x_state_name(state34 >> 4));
|
|
goto out_restore_hiz;
|
|
}
|
|
|
|
dev_dbg(tas->dev, "DC LDG: Completed successfully (CH1/2=0x%02x, CH3/4=0x%02x)\n",
|
|
state, state34);
|
|
|
|
out_restore_hiz:
|
|
tas675x_set_state_all(tas, TAS675X_STATE_HIZ_BOTH);
|
|
|
|
out_restore_ldg_ctrl:
|
|
regmap_update_bits(tas->regmap, TAS675X_DC_LDG_CTRL_REG,
|
|
TAS675X_LDG_ABORT_BIT | TAS675X_LDG_BYPASS_BIT,
|
|
0);
|
|
|
|
pm_runtime_mark_last_busy(tas->dev);
|
|
pm_runtime_put_autosuspend(tas->dev);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int tas675x_set_acldg_trigger(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_value *ucontrol)
|
|
{
|
|
struct snd_soc_component *comp = snd_kcontrol_chip(kcontrol);
|
|
struct tas675x_priv *tas = snd_soc_component_get_drvdata(comp);
|
|
unsigned int state, state34;
|
|
int ret;
|
|
|
|
if (!ucontrol->value.integer.value[0])
|
|
return 0;
|
|
|
|
if (snd_soc_component_active(comp))
|
|
return -EBUSY;
|
|
|
|
ret = pm_runtime_resume_and_get(tas->dev);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/* AC Load Diagnostics requires SLEEP state */
|
|
ret = tas675x_set_state_all(tas, TAS675X_STATE_SLEEP_BOTH);
|
|
if (ret) {
|
|
dev_err(tas->dev, "AC LDG: Failed to set SLEEP state: %d\n", ret);
|
|
goto out;
|
|
}
|
|
|
|
/* Start AC LDG on all 4 channels (0x0F) */
|
|
ret = regmap_write(tas->regmap, TAS675X_AC_LDG_CTRL_REG, 0x0F);
|
|
if (ret) {
|
|
dev_err(tas->dev, "AC LDG: Failed to start: %d\n", ret);
|
|
goto out;
|
|
}
|
|
|
|
dev_dbg(tas->dev, "AC LDG: Started\n");
|
|
|
|
/* Poll all channels for SLEEP state */
|
|
ret = regmap_read_poll_timeout(tas->regmap, TAS675X_STATE_REPORT_CH1_CH2_REG,
|
|
state, (state == TAS675X_STATE_SLEEP_BOTH),
|
|
TAS675X_POLL_INTERVAL_US,
|
|
TAS675X_AC_LDG_TIMEOUT_US);
|
|
if (ret) {
|
|
dev_err(tas->dev,
|
|
"AC LDG: CH1/CH2 timeout: %d (state=0x%02x [%s/%s])\n",
|
|
ret, state, tas675x_state_name(state),
|
|
tas675x_state_name(state >> 4));
|
|
regmap_write(tas->regmap, TAS675X_AC_LDG_CTRL_REG, 0x00);
|
|
goto out;
|
|
}
|
|
|
|
ret = regmap_read_poll_timeout(tas->regmap, TAS675X_STATE_REPORT_CH3_CH4_REG,
|
|
state34, (state34 == TAS675X_STATE_SLEEP_BOTH),
|
|
TAS675X_POLL_INTERVAL_US,
|
|
TAS675X_AC_LDG_TIMEOUT_US);
|
|
if (ret) {
|
|
dev_err(tas->dev,
|
|
"AC LDG: CH3/CH4 timeout: %d (state=0x%02x [%s/%s])\n",
|
|
ret, state34, tas675x_state_name(state34),
|
|
tas675x_state_name(state34 >> 4));
|
|
regmap_write(tas->regmap, TAS675X_AC_LDG_CTRL_REG, 0x00);
|
|
goto out;
|
|
}
|
|
|
|
dev_dbg(tas->dev, "AC LDG: Completed successfully (CH1/2=0x%02x, CH3/4=0x%02x)\n",
|
|
state, state34);
|
|
regmap_write(tas->regmap, TAS675X_AC_LDG_CTRL_REG, 0x00);
|
|
|
|
out:
|
|
pm_runtime_mark_last_busy(tas->dev);
|
|
pm_runtime_put_autosuspend(tas->dev);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int tas675x_rtldg_impedance_info(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_info *uinfo)
|
|
{
|
|
uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
|
|
uinfo->count = 1;
|
|
uinfo->value.integer.min = 0;
|
|
uinfo->value.integer.max = 0xFFFF;
|
|
return 0;
|
|
}
|
|
|
|
static int tas675x_get_rtldg_impedance(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_value *ucontrol)
|
|
{
|
|
struct snd_soc_component *comp = snd_kcontrol_chip(kcontrol);
|
|
struct tas675x_priv *tas = snd_soc_component_get_drvdata(comp);
|
|
unsigned int msb_reg = (unsigned int)kcontrol->private_value;
|
|
u8 buf[2];
|
|
int ret;
|
|
|
|
ret = regmap_bulk_read(tas->regmap, msb_reg, buf, 2);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ucontrol->value.integer.value[0] = (buf[0] << 8) | buf[1];
|
|
return 0;
|
|
}
|
|
|
|
static int tas675x_dc_resistance_info(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_info *uinfo)
|
|
{
|
|
/* 10-bit: 2-bit MSB + 8-bit LSB, 0.1 ohm/code, 0-102.3 ohm */
|
|
uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
|
|
uinfo->count = 1;
|
|
uinfo->value.integer.min = 0;
|
|
uinfo->value.integer.max = 1023;
|
|
return 0;
|
|
}
|
|
|
|
static int tas675x_get_dc_resistance(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_value *ucontrol)
|
|
{
|
|
struct snd_soc_component *comp = snd_kcontrol_chip(kcontrol);
|
|
struct tas675x_priv *tas = snd_soc_component_get_drvdata(comp);
|
|
unsigned int lsb_reg = (unsigned int)kcontrol->private_value;
|
|
unsigned int msb, lsb, shift;
|
|
int ret;
|
|
|
|
ret = regmap_read(tas->regmap, TAS675X_DC_LDG_DCR_MSB_REG, &msb);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = regmap_read(tas->regmap, lsb_reg, &lsb);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* 2-bit MSB: CH1=[7:6], CH2=[5:4], CH3=[3:2], CH4=[1:0] */
|
|
shift = 6 - (lsb_reg - TAS675X_CH1_DC_LDG_DCR_LSB_REG) * 2;
|
|
msb = (msb >> shift) & 0x3;
|
|
|
|
ucontrol->value.integer.value[0] = (msb << 8) | lsb;
|
|
return 0;
|
|
}
|
|
|
|
/* Counterparts with read-only access */
|
|
#define SOC_SINGLE_RO(xname, xreg, xshift, xmax) \
|
|
{ .iface = SNDRV_CTL_ELEM_IFACE_MIXER, \
|
|
.name = xname, \
|
|
.access = SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE, \
|
|
.info = snd_soc_info_volsw, \
|
|
.get = snd_soc_get_volsw, \
|
|
.private_value = SOC_SINGLE_VALUE(xreg, xshift, 0, xmax, 0, 0) }
|
|
#define SOC_DC_RESIST_RO(xname, xlsb_reg) \
|
|
{ .name = xname, \
|
|
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, \
|
|
.access = SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE, \
|
|
.info = tas675x_dc_resistance_info, \
|
|
.get = tas675x_get_dc_resistance, \
|
|
.private_value = (xlsb_reg) }
|
|
#define SOC_RTLDG_IMP_RO(xname, xreg) \
|
|
{ .name = xname, \
|
|
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, \
|
|
.access = SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE, \
|
|
.info = tas675x_rtldg_impedance_info, \
|
|
.get = tas675x_get_rtldg_impedance, \
|
|
.private_value = (xreg) }
|
|
|
|
#define SOC_DSP_THRESH_EXT(xname, xthresh) \
|
|
{ .name = xname, \
|
|
.iface = SNDRV_CTL_ELEM_IFACE_MIXER, \
|
|
.info = tas675x_rtldg_thresh_info, \
|
|
.get = tas675x_get_rtldg_thresh, \
|
|
.put = tas675x_set_rtldg_thresh, \
|
|
.private_value = (unsigned long)&(xthresh) }
|
|
|
|
/*
|
|
* DAC digital volumes. From -103 to 0 dB in 0.5 dB steps, -103.5 dB means mute.
|
|
* DAC analog gain. From -15.5 to 0 dB in 0.5 dB steps, no mute.
|
|
*/
|
|
static const DECLARE_TLV_DB_SCALE(tas675x_dig_vol_tlv, -10350, 50, 1);
|
|
static const DECLARE_TLV_DB_SCALE(tas675x_ana_gain_tlv, -1550, 50, 0);
|
|
|
|
static const char * const tas675x_ss_texts[] = {
|
|
"Disabled", "Triangle", "Random", "Triangle and Random"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ss_enum, TAS675X_SS_CTRL_REG, 0, tas675x_ss_texts);
|
|
|
|
static const char * const tas675x_ss_tri_range_texts[] = {
|
|
"6.5%", "13.5%", "5%", "10%"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ss_tri_range_enum,
|
|
TAS675X_SS_RANGE_CTRL_REG, 0,
|
|
tas675x_ss_tri_range_texts);
|
|
|
|
static const char * const tas675x_ss_rdm_range_texts[] = {
|
|
"0.83%", "2.50%", "5.83%", "12.50%", "25.83%"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ss_rdm_range_enum,
|
|
TAS675X_SS_RANGE_CTRL_REG, 4,
|
|
tas675x_ss_rdm_range_texts);
|
|
|
|
static const char * const tas675x_ss_rdm_dwell_texts[] = {
|
|
"1/FSS to 2/FSS", "1/FSS to 4/FSS", "1/FSS to 8/FSS", "1/FSS to 15/FSS"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ss_rdm_dwell_enum,
|
|
TAS675X_SS_RANGE_CTRL_REG, 2,
|
|
tas675x_ss_rdm_dwell_texts);
|
|
|
|
static const char * const tas675x_oc_limit_texts[] = {
|
|
"Level 4", "Level 3", "Level 2", "Level 1"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_oc_limit_enum, TAS675X_CURRENT_LIMIT_CTRL_REG,
|
|
0, tas675x_oc_limit_texts);
|
|
|
|
static const char * const tas675x_otw_texts[] = {
|
|
"Disabled", ">95C", ">110C", ">125C", ">135C", ">145C", ">155C", ">165C"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ch1_otw_enum,
|
|
TAS675X_OTW_CTRL_CH1_CH2_REG, 4,
|
|
tas675x_otw_texts);
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ch2_otw_enum,
|
|
TAS675X_OTW_CTRL_CH1_CH2_REG, 0,
|
|
tas675x_otw_texts);
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ch3_otw_enum,
|
|
TAS675X_OTW_CTRL_CH3_CH4_REG, 4,
|
|
tas675x_otw_texts);
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ch4_otw_enum,
|
|
TAS675X_OTW_CTRL_CH3_CH4_REG, 0,
|
|
tas675x_otw_texts);
|
|
|
|
static const char * const tas675x_dc_ldg_sl_texts[] = {
|
|
"0.5 Ohm", "1 Ohm", "1.5 Ohm", "2 Ohm", "2.5 Ohm",
|
|
"3 Ohm", "3.5 Ohm", "4 Ohm", "4.5 Ohm", "5 Ohm"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ch1_dc_ldg_sl_enum,
|
|
TAS675X_DC_LDG_SL_CH1_CH2_CTRL_REG, 4,
|
|
tas675x_dc_ldg_sl_texts);
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ch2_dc_ldg_sl_enum,
|
|
TAS675X_DC_LDG_SL_CH1_CH2_CTRL_REG, 0,
|
|
tas675x_dc_ldg_sl_texts);
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ch3_dc_ldg_sl_enum,
|
|
TAS675X_DC_LDG_SL_CH3_CH4_CTRL_REG, 4,
|
|
tas675x_dc_ldg_sl_texts);
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ch4_dc_ldg_sl_enum,
|
|
TAS675X_DC_LDG_SL_CH3_CH4_CTRL_REG, 0,
|
|
tas675x_dc_ldg_sl_texts);
|
|
|
|
static const char * const tas675x_dc_slol_ramp_texts[] = {
|
|
"15 ms", "30 ms", "10 ms", "20 ms"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_dc_slol_ramp_enum,
|
|
TAS675X_DC_LDG_TIME_CTRL_REG, 6,
|
|
tas675x_dc_slol_ramp_texts);
|
|
|
|
static const char * const tas675x_dc_slol_settling_texts[] = {
|
|
"10 ms", "5 ms", "20 ms", "15 ms"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_dc_slol_settling_enum,
|
|
TAS675X_DC_LDG_TIME_CTRL_REG, 4,
|
|
tas675x_dc_slol_settling_texts);
|
|
|
|
static const char * const tas675x_dc_s2pg_ramp_texts[] = {
|
|
"5 ms", "2.5 ms", "10 ms", "15 ms"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_dc_s2pg_ramp_enum,
|
|
TAS675X_DC_LDG_TIME_CTRL_REG, 2,
|
|
tas675x_dc_s2pg_ramp_texts);
|
|
|
|
static const char * const tas675x_dc_s2pg_settling_texts[] = {
|
|
"10 ms", "5 ms", "20 ms", "30 ms"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_dc_s2pg_settling_enum,
|
|
TAS675X_DC_LDG_TIME_CTRL_REG, 0,
|
|
tas675x_dc_s2pg_settling_texts);
|
|
|
|
static const char * const tas675x_dsp_mode_texts[] = {
|
|
"Normal", "LLP", "FFLP"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_dsp_mode_enum,
|
|
TAS675X_LL_EN_REG, 0,
|
|
tas675x_dsp_mode_texts);
|
|
|
|
static const char * const tas675x_ana_ramp_texts[] = {
|
|
"15us", "60us", "200us", "400us"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ana_ramp_enum,
|
|
TAS675X_ANALOG_GAIN_RAMP_CTRL_REG, 2,
|
|
tas675x_ana_ramp_texts);
|
|
|
|
static const char * const tas675x_ramp_rate_texts[] = {
|
|
"4 FS", "16 FS", "32 FS", "Instant"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ramp_down_rate_enum,
|
|
TAS675X_DIG_VOL_RAMP_CTRL_REG, 6,
|
|
tas675x_ramp_rate_texts);
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ramp_up_rate_enum,
|
|
TAS675X_DIG_VOL_RAMP_CTRL_REG, 2,
|
|
tas675x_ramp_rate_texts);
|
|
|
|
static const char * const tas675x_ramp_step_texts[] = {
|
|
"4dB", "2dB", "1dB", "0.5dB"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ramp_down_step_enum,
|
|
TAS675X_DIG_VOL_RAMP_CTRL_REG, 4,
|
|
tas675x_ramp_step_texts);
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ramp_up_step_enum,
|
|
TAS675X_DIG_VOL_RAMP_CTRL_REG, 0,
|
|
tas675x_ramp_step_texts);
|
|
|
|
static const char * const tas675x_vol_combine_ch12_texts[] = {
|
|
"Independent", "CH2 follows CH1", "CH1 follows CH2"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_vol_combine_ch12_enum,
|
|
TAS675X_DIG_VOL_COMBINE_CTRL_REG, 0,
|
|
tas675x_vol_combine_ch12_texts);
|
|
|
|
static const char * const tas675x_vol_combine_ch34_texts[] = {
|
|
"Independent", "CH4 follows CH3", "CH3 follows CH4"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_vol_combine_ch34_enum,
|
|
TAS675X_DIG_VOL_COMBINE_CTRL_REG, 2,
|
|
tas675x_vol_combine_ch34_texts);
|
|
|
|
static const char * const tas675x_auto_mute_time_texts[] = {
|
|
"11.5ms", "53ms", "106.5ms", "266.5ms",
|
|
"535ms", "1065ms", "2665ms", "5330ms"
|
|
};
|
|
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ch1_mute_time_enum,
|
|
TAS675X_AUTO_MUTE_TIMING_CH1_CH2_REG, 4,
|
|
tas675x_auto_mute_time_texts);
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ch2_mute_time_enum,
|
|
TAS675X_AUTO_MUTE_TIMING_CH1_CH2_REG, 0,
|
|
tas675x_auto_mute_time_texts);
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ch3_mute_time_enum,
|
|
TAS675X_AUTO_MUTE_TIMING_CH3_CH4_REG, 4,
|
|
tas675x_auto_mute_time_texts);
|
|
static SOC_ENUM_SINGLE_DECL(tas675x_ch4_mute_time_enum,
|
|
TAS675X_AUTO_MUTE_TIMING_CH3_CH4_REG, 0,
|
|
tas675x_auto_mute_time_texts);
|
|
|
|
/*
|
|
* ALSA Mixer Controls
|
|
*
|
|
* For detailed documentation of each control see:
|
|
* Documentation/sound/codecs/tas675x.rst
|
|
*/
|
|
static const struct snd_kcontrol_new tas675x_snd_controls[] = {
|
|
/* Volume & Gain Control */
|
|
SOC_DOUBLE_R_TLV("Analog Playback Volume", TAS675X_ANALOG_GAIN_CH1_CH2_REG,
|
|
TAS675X_ANALOG_GAIN_CH3_CH4_REG, 1, 0x1F, 1, tas675x_ana_gain_tlv),
|
|
SOC_ENUM("Analog Gain Ramp Step", tas675x_ana_ramp_enum),
|
|
SOC_SINGLE_RANGE_TLV("CH1 Digital Playback Volume",
|
|
TAS675X_DIG_VOL_CH1_REG, 0, 0x30, 0xFF, 1,
|
|
tas675x_dig_vol_tlv),
|
|
SOC_SINGLE_RANGE_TLV("CH2 Digital Playback Volume",
|
|
TAS675X_DIG_VOL_CH2_REG, 0, 0x30, 0xFF, 1,
|
|
tas675x_dig_vol_tlv),
|
|
SOC_SINGLE_RANGE_TLV("CH3 Digital Playback Volume",
|
|
TAS675X_DIG_VOL_CH3_REG, 0, 0x30, 0xFF, 1,
|
|
tas675x_dig_vol_tlv),
|
|
SOC_SINGLE_RANGE_TLV("CH4 Digital Playback Volume",
|
|
TAS675X_DIG_VOL_CH4_REG, 0, 0x30, 0xFF, 1,
|
|
tas675x_dig_vol_tlv),
|
|
SOC_ENUM("Volume Ramp Down Rate", tas675x_ramp_down_rate_enum),
|
|
SOC_ENUM("Volume Ramp Down Step", tas675x_ramp_down_step_enum),
|
|
SOC_ENUM("Volume Ramp Up Rate", tas675x_ramp_up_rate_enum),
|
|
SOC_ENUM("Volume Ramp Up Step", tas675x_ramp_up_step_enum),
|
|
SOC_ENUM("CH1/2 Volume Combine", tas675x_vol_combine_ch12_enum),
|
|
SOC_ENUM("CH3/4 Volume Combine", tas675x_vol_combine_ch34_enum),
|
|
|
|
/* Auto Mute & Silence Detection */
|
|
SOC_SINGLE("CH1 Auto Mute Switch", TAS675X_AUTO_MUTE_EN_REG, 0, 1, 0),
|
|
SOC_SINGLE("CH2 Auto Mute Switch", TAS675X_AUTO_MUTE_EN_REG, 1, 1, 0),
|
|
SOC_SINGLE("CH3 Auto Mute Switch", TAS675X_AUTO_MUTE_EN_REG, 2, 1, 0),
|
|
SOC_SINGLE("CH4 Auto Mute Switch", TAS675X_AUTO_MUTE_EN_REG, 3, 1, 0),
|
|
SOC_SINGLE("Auto Mute Combine Switch", TAS675X_AUTO_MUTE_EN_REG, 4, 1, 0),
|
|
SOC_ENUM("CH1 Auto Mute Time", tas675x_ch1_mute_time_enum),
|
|
SOC_ENUM("CH2 Auto Mute Time", tas675x_ch2_mute_time_enum),
|
|
SOC_ENUM("CH3 Auto Mute Time", tas675x_ch3_mute_time_enum),
|
|
SOC_ENUM("CH4 Auto Mute Time", tas675x_ch4_mute_time_enum),
|
|
|
|
/* Clock & EMI Management */
|
|
SOC_ENUM("Spread Spectrum Mode", tas675x_ss_enum),
|
|
SOC_ENUM("SS Triangle Range", tas675x_ss_tri_range_enum),
|
|
SOC_ENUM("SS Random Range", tas675x_ss_rdm_range_enum),
|
|
SOC_ENUM("SS Random Dwell Range", tas675x_ss_rdm_dwell_enum),
|
|
SOC_SINGLE("SS Triangle Dwell Min", TAS675X_SS_DWELL_CTRL_REG, 4, 15, 0),
|
|
SOC_SINGLE("SS Triangle Dwell Max", TAS675X_SS_DWELL_CTRL_REG, 0, 15, 0),
|
|
|
|
/* Hardware Protection */
|
|
SOC_SINGLE("OTSD Auto Recovery Switch", TAS675X_OTSD_RECOVERY_EN_REG, 1, 1, 0),
|
|
SOC_ENUM("Overcurrent Limit Level", tas675x_oc_limit_enum),
|
|
SOC_ENUM("CH1 OTW Threshold", tas675x_ch1_otw_enum),
|
|
SOC_ENUM("CH2 OTW Threshold", tas675x_ch2_otw_enum),
|
|
SOC_ENUM("CH3 OTW Threshold", tas675x_ch3_otw_enum),
|
|
SOC_ENUM("CH4 OTW Threshold", tas675x_ch4_otw_enum),
|
|
|
|
/* DSP Signal Path & Mode */
|
|
SOC_ENUM("DSP Signal Path Mode", tas675x_dsp_mode_enum),
|
|
|
|
/* DC Load Diagnostics */
|
|
{
|
|
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
|
|
.name = "DC LDG Trigger",
|
|
.access = SNDRV_CTL_ELEM_ACCESS_WRITE,
|
|
.info = snd_ctl_boolean_mono_info,
|
|
.put = tas675x_set_dcldg_trigger,
|
|
},
|
|
SOC_SINGLE("DC LDG Auto Diagnostics Switch", TAS675X_DC_LDG_CTRL_REG, 0, 1, 1),
|
|
SOC_SINGLE("CH1 LO LDG Switch", TAS675X_DC_LDG_LO_CTRL_REG, 3, 1, 0),
|
|
SOC_SINGLE("CH2 LO LDG Switch", TAS675X_DC_LDG_LO_CTRL_REG, 2, 1, 0),
|
|
SOC_SINGLE("CH3 LO LDG Switch", TAS675X_DC_LDG_LO_CTRL_REG, 1, 1, 0),
|
|
SOC_SINGLE("CH4 LO LDG Switch", TAS675X_DC_LDG_LO_CTRL_REG, 0, 1, 0),
|
|
SOC_ENUM("DC LDG SLOL Ramp Time", tas675x_dc_slol_ramp_enum),
|
|
SOC_ENUM("DC LDG SLOL Settling Time", tas675x_dc_slol_settling_enum),
|
|
SOC_ENUM("DC LDG S2PG Ramp Time", tas675x_dc_s2pg_ramp_enum),
|
|
SOC_ENUM("DC LDG S2PG Settling Time", tas675x_dc_s2pg_settling_enum),
|
|
SOC_ENUM("CH1 DC LDG SL Threshold", tas675x_ch1_dc_ldg_sl_enum),
|
|
SOC_ENUM("CH2 DC LDG SL Threshold", tas675x_ch2_dc_ldg_sl_enum),
|
|
SOC_ENUM("CH3 DC LDG SL Threshold", tas675x_ch3_dc_ldg_sl_enum),
|
|
SOC_ENUM("CH4 DC LDG SL Threshold", tas675x_ch4_dc_ldg_sl_enum),
|
|
SOC_SINGLE_RO("DC LDG Result", TAS675X_DC_LDG_RESULT_REG, 0, 0xFF),
|
|
SOC_SINGLE_RO("CH1 DC LDG Report", TAS675X_DC_LDG_REPORT_CH1_CH2_REG, 4, 0x0F),
|
|
SOC_SINGLE_RO("CH2 DC LDG Report", TAS675X_DC_LDG_REPORT_CH1_CH2_REG, 0, 0x0F),
|
|
SOC_SINGLE_RO("CH3 DC LDG Report", TAS675X_DC_LDG_REPORT_CH3_CH4_REG, 4, 0x0F),
|
|
SOC_SINGLE_RO("CH4 DC LDG Report", TAS675X_DC_LDG_REPORT_CH3_CH4_REG, 0, 0x0F),
|
|
SOC_SINGLE_RO("CH1 LO LDG Report", TAS675X_DC_LDG_RESULT_REG, 7, 1),
|
|
SOC_SINGLE_RO("CH2 LO LDG Report", TAS675X_DC_LDG_RESULT_REG, 6, 1),
|
|
SOC_SINGLE_RO("CH3 LO LDG Report", TAS675X_DC_LDG_RESULT_REG, 5, 1),
|
|
SOC_SINGLE_RO("CH4 LO LDG Report", TAS675X_DC_LDG_RESULT_REG, 4, 1),
|
|
SOC_DC_RESIST_RO("CH1 DC Resistance", TAS675X_CH1_DC_LDG_DCR_LSB_REG),
|
|
SOC_DC_RESIST_RO("CH2 DC Resistance", TAS675X_CH2_DC_LDG_DCR_LSB_REG),
|
|
SOC_DC_RESIST_RO("CH3 DC Resistance", TAS675X_CH3_DC_LDG_DCR_LSB_REG),
|
|
SOC_DC_RESIST_RO("CH4 DC Resistance", TAS675X_CH4_DC_LDG_DCR_LSB_REG),
|
|
|
|
/* AC Load Diagnostics */
|
|
{
|
|
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
|
|
.name = "AC LDG Trigger",
|
|
.access = SNDRV_CTL_ELEM_ACCESS_WRITE,
|
|
.info = snd_ctl_boolean_mono_info,
|
|
.put = tas675x_set_acldg_trigger,
|
|
},
|
|
SOC_SINGLE("AC LDG Gain", TAS675X_AC_LDG_CTRL_REG, 4, 1, 0),
|
|
SOC_SINGLE("AC LDG Test Frequency", TAS675X_AC_LDG_FREQ_CTRL_REG, 0, 0xFF, 0),
|
|
SOC_SINGLE_RO("CH1 AC LDG Real", TAS675X_AC_LDG_REPORT_CH1_R_REG, 0, 0xFF),
|
|
SOC_SINGLE_RO("CH1 AC LDG Imag", TAS675X_AC_LDG_REPORT_CH1_I_REG, 0, 0xFF),
|
|
SOC_SINGLE_RO("CH2 AC LDG Real", TAS675X_AC_LDG_REPORT_CH2_R_REG, 0, 0xFF),
|
|
SOC_SINGLE_RO("CH2 AC LDG Imag", TAS675X_AC_LDG_REPORT_CH2_I_REG, 0, 0xFF),
|
|
SOC_SINGLE_RO("CH3 AC LDG Real", TAS675X_AC_LDG_REPORT_CH3_R_REG, 0, 0xFF),
|
|
SOC_SINGLE_RO("CH3 AC LDG Imag", TAS675X_AC_LDG_REPORT_CH3_I_REG, 0, 0xFF),
|
|
SOC_SINGLE_RO("CH4 AC LDG Real", TAS675X_AC_LDG_REPORT_CH4_R_REG, 0, 0xFF),
|
|
SOC_SINGLE_RO("CH4 AC LDG Imag", TAS675X_AC_LDG_REPORT_CH4_I_REG, 0, 0xFF),
|
|
|
|
/* Temperature and Voltage Monitoring */
|
|
SOC_SINGLE_RO("PVDD Sense", TAS675X_PVDD_SENSE_REG, 0, 0xFF),
|
|
SOC_SINGLE_RO("Global Temperature", TAS675X_TEMP_GLOBAL_REG, 0, 0xFF),
|
|
SOC_SINGLE_RO("CH1 Temperature Range", TAS675X_TEMP_CH1_CH2_REG, 0, 7),
|
|
SOC_SINGLE_RO("CH2 Temperature Range", TAS675X_TEMP_CH1_CH2_REG, 3, 7),
|
|
SOC_SINGLE_RO("CH3 Temperature Range", TAS675X_TEMP_CH3_CH4_REG, 0, 7),
|
|
SOC_SINGLE_RO("CH4 Temperature Range", TAS675X_TEMP_CH3_CH4_REG, 3, 7),
|
|
|
|
/* Speaker Protection & Detection */
|
|
SOC_SINGLE("Tweeter Detection Switch", TAS675X_TWEETER_DETECT_CTRL_REG, 0, 1, 1),
|
|
SOC_SINGLE("Tweeter Detect Threshold", TAS675X_TWEETER_DETECT_THRESH_REG, 0, 0xFF, 0),
|
|
SOC_SINGLE_RO("CH1 Tweeter Detect Report", TAS675X_TWEETER_REPORT_REG, 3, 1),
|
|
SOC_SINGLE_RO("CH2 Tweeter Detect Report", TAS675X_TWEETER_REPORT_REG, 2, 1),
|
|
SOC_SINGLE_RO("CH3 Tweeter Detect Report", TAS675X_TWEETER_REPORT_REG, 1, 1),
|
|
SOC_SINGLE_RO("CH4 Tweeter Detect Report", TAS675X_TWEETER_REPORT_REG, 0, 1),
|
|
|
|
/*
|
|
* Unavailable in LLP, available in Normal & FFLP
|
|
*/
|
|
SOC_SINGLE("Thermal Foldback Switch", TAS675X_DSP_CTRL_REG, 0, 1, 0),
|
|
SOC_SINGLE("PVDD Foldback Switch", TAS675X_DSP_CTRL_REG, 4, 1, 0),
|
|
SOC_SINGLE("DC Blocker Bypass Switch", TAS675X_DC_BLOCK_BYP_REG, 0, 1, 0),
|
|
SOC_SINGLE("Clip Detect Switch", TAS675X_CLIP_DETECT_CTRL_REG, 6, 1, 0),
|
|
SOC_SINGLE("Audio SDOUT Switch", TAS675X_DSP_CTRL_REG, 5, 1, 0),
|
|
|
|
/*
|
|
* Unavailable in both FFLP and LLP, Normal mode only
|
|
*/
|
|
/* Real-Time Load Diagnostics */
|
|
SOC_SINGLE("CH1 RTLDG Switch", TAS675X_RTLDG_EN_REG, 3, 1, 0),
|
|
SOC_SINGLE("CH2 RTLDG Switch", TAS675X_RTLDG_EN_REG, 2, 1, 0),
|
|
SOC_SINGLE("CH3 RTLDG Switch", TAS675X_RTLDG_EN_REG, 1, 1, 0),
|
|
SOC_SINGLE("CH4 RTLDG Switch", TAS675X_RTLDG_EN_REG, 0, 1, 0),
|
|
SOC_SINGLE("RTLDG Clip Mask Switch", TAS675X_RTLDG_EN_REG, 4, 1, 0),
|
|
SOC_SINGLE("ISENSE Calibration Switch", TAS675X_ISENSE_CAL_REG, 3, 1, 0),
|
|
SOC_DSP_THRESH_EXT("RTLDG Open Load Threshold",
|
|
tas675x_dsp_defaults[TAS675X_DSP_PARAM_ID_OL_THRESH]),
|
|
SOC_DSP_THRESH_EXT("RTLDG Short Load Threshold",
|
|
tas675x_dsp_defaults[TAS675X_DSP_PARAM_ID_SL_THRESH]),
|
|
SOC_RTLDG_IMP_RO("CH1 RTLDG Impedance", TAS675X_CH1_RTLDG_IMP_MSB_REG),
|
|
SOC_RTLDG_IMP_RO("CH2 RTLDG Impedance", TAS675X_CH2_RTLDG_IMP_MSB_REG),
|
|
SOC_RTLDG_IMP_RO("CH3 RTLDG Impedance", TAS675X_CH3_RTLDG_IMP_MSB_REG),
|
|
SOC_RTLDG_IMP_RO("CH4 RTLDG Impedance", TAS675X_CH4_RTLDG_IMP_MSB_REG),
|
|
};
|
|
|
|
static const struct snd_kcontrol_new tas675x_audio_path_switch =
|
|
SOC_DAPM_SINGLE("Switch", SND_SOC_NOPM, 0, 1, 1);
|
|
|
|
static const struct snd_kcontrol_new tas675x_anc_path_switch =
|
|
SOC_DAPM_SINGLE("Switch", SND_SOC_NOPM, 0, 1, 1);
|
|
|
|
static const struct snd_soc_dapm_widget tas675x_dapm_widgets[] = {
|
|
SND_SOC_DAPM_SUPPLY("Analog Core", SND_SOC_NOPM, 0, 0, NULL, 0),
|
|
SND_SOC_DAPM_SUPPLY("SDOUT Vpredict", SND_SOC_NOPM, 0, 0, NULL, 0),
|
|
SND_SOC_DAPM_SUPPLY("SDOUT Isense", SND_SOC_NOPM, 0, 0, NULL, 0),
|
|
|
|
SND_SOC_DAPM_DAC("Audio DAC", "Playback", SND_SOC_NOPM, 0, 0),
|
|
SND_SOC_DAPM_DAC("ANC DAC", "ANC Playback", SND_SOC_NOPM, 0, 0),
|
|
SND_SOC_DAPM_ADC("Feedback ADC", "Feedback Capture", SND_SOC_NOPM, 0, 0),
|
|
|
|
SND_SOC_DAPM_SWITCH("Audio Path", SND_SOC_NOPM, 0, 0,
|
|
&tas675x_audio_path_switch),
|
|
SND_SOC_DAPM_SWITCH("ANC Path", SND_SOC_NOPM, 0, 0,
|
|
&tas675x_anc_path_switch),
|
|
|
|
/*
|
|
* Even though all channels are coupled in terms of power control,
|
|
* use logical outputs for each channel to allow independent routing
|
|
* and DAPM controls if needed.
|
|
*/
|
|
SND_SOC_DAPM_OUTPUT("OUT_CH1"),
|
|
SND_SOC_DAPM_OUTPUT("OUT_CH2"),
|
|
SND_SOC_DAPM_OUTPUT("OUT_CH3"),
|
|
SND_SOC_DAPM_OUTPUT("OUT_CH4"),
|
|
SND_SOC_DAPM_INPUT("SPEAKER_LOAD"),
|
|
};
|
|
|
|
static const struct snd_soc_dapm_route tas675x_dapm_routes[] = {
|
|
{ "Audio DAC", NULL, "Analog Core" },
|
|
{ "Audio Path", "Switch", "Audio DAC" },
|
|
{ "OUT_CH1", NULL, "Audio Path" },
|
|
{ "OUT_CH2", NULL, "Audio Path" },
|
|
{ "OUT_CH3", NULL, "Audio Path" },
|
|
{ "OUT_CH4", NULL, "Audio Path" },
|
|
|
|
{ "ANC DAC", NULL, "Analog Core" },
|
|
{ "ANC Path", "Switch", "ANC DAC" },
|
|
{ "OUT_CH1", NULL, "ANC Path" },
|
|
{ "OUT_CH2", NULL, "ANC Path" },
|
|
{ "OUT_CH3", NULL, "ANC Path" },
|
|
{ "OUT_CH4", NULL, "ANC Path" },
|
|
|
|
{ "Feedback ADC", NULL, "Analog Core" },
|
|
{ "Feedback ADC", NULL, "SDOUT Vpredict" },
|
|
{ "Feedback ADC", NULL, "SDOUT Isense" },
|
|
{ "Feedback ADC", NULL, "SPEAKER_LOAD" },
|
|
};
|
|
|
|
static void tas675x_program_slot_offsets(struct tas675x_priv *tas,
|
|
int dai_id, int slot_width)
|
|
{
|
|
int offset = 0;
|
|
|
|
switch (dai_id) {
|
|
case 0:
|
|
/* Standard Audio on SDIN */
|
|
if (tas->audio_slot >= 0)
|
|
offset = tas->audio_slot * slot_width;
|
|
else if (tas->tx_mask)
|
|
offset = __ffs(tas->tx_mask) * slot_width;
|
|
else
|
|
return;
|
|
offset += tas->bclk_offset;
|
|
regmap_update_bits(tas->regmap, TAS675X_SDIN_OFFSET_MSB_REG,
|
|
TAS675X_SDIN_AUDIO_OFF_MSB_MASK,
|
|
FIELD_PREP(TAS675X_SDIN_AUDIO_OFF_MSB_MASK, offset >> 8));
|
|
regmap_write(tas->regmap, TAS675X_SDIN_AUDIO_OFFSET_REG,
|
|
offset & 0xFF);
|
|
break;
|
|
case 1:
|
|
/*
|
|
* Low-Latency Playback on SDIN, **only** enabled in LLP mode
|
|
* and to be mixed with main audio before output amplification
|
|
* to achieve ANC/RNC.
|
|
*/
|
|
if (tas->llp_slot >= 0)
|
|
offset = tas->llp_slot * slot_width;
|
|
else if (tas->tx_mask)
|
|
offset = __ffs(tas->tx_mask) * slot_width;
|
|
else
|
|
return;
|
|
offset += tas->bclk_offset;
|
|
regmap_update_bits(tas->regmap, TAS675X_SDIN_OFFSET_MSB_REG,
|
|
TAS675X_SDIN_LL_OFF_MSB_MASK,
|
|
FIELD_PREP(TAS675X_SDIN_LL_OFF_MSB_MASK, offset >> 8));
|
|
regmap_write(tas->regmap, TAS675X_SDIN_LL_OFFSET_REG,
|
|
offset & 0xFF);
|
|
break;
|
|
case 2:
|
|
/* SDOUT Data Output (Vpredict + Isense feedback) */
|
|
if (!tas->slot_width)
|
|
break;
|
|
if (tas->vpredict_slot >= 0) {
|
|
offset = tas->vpredict_slot * slot_width;
|
|
offset += tas->bclk_offset;
|
|
regmap_update_bits(tas->regmap, TAS675X_SDOUT_OFFSET_MSB_REG,
|
|
TAS675X_SDOUT_VP_OFF_MSB_MASK,
|
|
FIELD_PREP(TAS675X_SDOUT_VP_OFF_MSB_MASK, offset >> 8));
|
|
regmap_write(tas->regmap, TAS675X_VPREDICT_OFFSET_REG,
|
|
offset & 0xFF);
|
|
}
|
|
if (tas->isense_slot >= 0) {
|
|
offset = tas->isense_slot * slot_width;
|
|
offset += tas->bclk_offset;
|
|
regmap_update_bits(tas->regmap, TAS675X_SDOUT_OFFSET_MSB_REG,
|
|
TAS675X_SDOUT_IS_OFF_MSB_MASK,
|
|
FIELD_PREP(TAS675X_SDOUT_IS_OFF_MSB_MASK, offset >> 8));
|
|
regmap_write(tas->regmap, TAS675X_ISENSE_OFFSET_REG,
|
|
offset & 0xFF);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (offset > 511)
|
|
dev_warn(tas->dev,
|
|
"DAI %d slot offset %d exceeds 511 SCLK limit\n",
|
|
dai_id, offset);
|
|
}
|
|
|
|
static int tas675x_hw_params(struct snd_pcm_substream *substream,
|
|
struct snd_pcm_hw_params *params,
|
|
struct snd_soc_dai *dai)
|
|
{
|
|
struct snd_soc_component *component = dai->component;
|
|
struct tas675x_priv *tas = snd_soc_component_get_drvdata(component);
|
|
unsigned int rate = params_rate(params);
|
|
u8 word_length;
|
|
|
|
/*
|
|
* Single clock domain: SDIN and SDOUT share one SCLK/FSYNC pair,
|
|
* so all active DAIs must use the same sample rate.
|
|
*/
|
|
if ((READ_ONCE(tas->active_playback_dais) || READ_ONCE(tas->active_capture_dais)) &&
|
|
tas->rate && tas->rate != rate) {
|
|
dev_err(component->dev,
|
|
"Rate %u conflicts with active rate %u\n",
|
|
rate, tas->rate);
|
|
return -EINVAL;
|
|
}
|
|
|
|
switch (params_width(params)) {
|
|
case 16:
|
|
word_length = TAS675X_WL_16BIT;
|
|
break;
|
|
case 20:
|
|
word_length = TAS675X_WL_20BIT;
|
|
break;
|
|
case 24:
|
|
word_length = TAS675X_WL_24BIT;
|
|
break;
|
|
case 32:
|
|
word_length = TAS675X_WL_32BIT;
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
|
|
/*
|
|
* RTLDG is not supported above 96kHz. Auto-disable to
|
|
* prevent DSP overload and restore when rate drops back.
|
|
*/
|
|
if (rate > 96000) {
|
|
unsigned int val;
|
|
|
|
regmap_read(component->regmap, TAS675X_RTLDG_EN_REG,
|
|
&val);
|
|
if (val & TAS675X_RTLDG_CH_EN_MASK) {
|
|
tas->saved_rtldg_en = val;
|
|
dev_dbg(component->dev,
|
|
"Sample rate %dHz > 96kHz: Auto-disabling RTLDG\n",
|
|
rate);
|
|
regmap_update_bits(component->regmap,
|
|
TAS675X_RTLDG_EN_REG,
|
|
TAS675X_RTLDG_CH_EN_MASK,
|
|
0x00);
|
|
}
|
|
} else if (tas->saved_rtldg_en) {
|
|
unsigned int cur;
|
|
|
|
/*
|
|
* Respect overrides and only restore if RTLDG is still auto-disabled
|
|
*/
|
|
regmap_read(component->regmap, TAS675X_RTLDG_EN_REG,
|
|
&cur);
|
|
if (!(cur & TAS675X_RTLDG_CH_EN_MASK)) {
|
|
dev_dbg(component->dev,
|
|
"Restoring RTLDG config after high-rate stream\n");
|
|
regmap_update_bits(component->regmap,
|
|
TAS675X_RTLDG_EN_REG,
|
|
TAS675X_RTLDG_CH_EN_MASK,
|
|
TAS675X_RTLDG_CH_EN_MASK &
|
|
tas->saved_rtldg_en);
|
|
}
|
|
tas->saved_rtldg_en = 0;
|
|
}
|
|
|
|
/* Set SDIN word length (audio path + low-latency path) */
|
|
regmap_update_bits(component->regmap, TAS675X_SDIN_CTRL_REG,
|
|
TAS675X_SDIN_WL_MASK,
|
|
FIELD_PREP(TAS675X_SDIN_AUDIO_WL_MASK, word_length) |
|
|
FIELD_PREP(TAS675X_SDIN_LL_WL_MASK, word_length));
|
|
} else {
|
|
/* Set SDOUT word length (VPREDICT + ISENSE) for capture */
|
|
regmap_update_bits(component->regmap, TAS675X_SDOUT_CTRL_REG,
|
|
TAS675X_SDOUT_WL_MASK,
|
|
FIELD_PREP(TAS675X_SDOUT_VP_WL_MASK, word_length) |
|
|
FIELD_PREP(TAS675X_SDOUT_IS_WL_MASK, word_length));
|
|
}
|
|
|
|
tas675x_program_slot_offsets(tas, dai->id,
|
|
tas->slot_width ?: params_width(params));
|
|
|
|
tas->rate = rate;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tas675x_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
|
|
{
|
|
struct snd_soc_component *component = dai->component;
|
|
struct tas675x_priv *tas = snd_soc_component_get_drvdata(component);
|
|
bool tdm_mode = false, i2s_mode = false;
|
|
|
|
/* Enforce Clocking Direction (Codec is strictly a consumer) */
|
|
switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) {
|
|
case SND_SOC_DAIFMT_BC_FC:
|
|
break;
|
|
default:
|
|
dev_err(component->dev, "Unsupported clock provider format\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* SCLK polarity: NB_NF or IB_NF only (no FSYNC inversion support) */
|
|
switch (fmt & SND_SOC_DAIFMT_INV_MASK) {
|
|
case SND_SOC_DAIFMT_NB_NF:
|
|
regmap_update_bits(component->regmap, TAS675X_SCLK_INV_CTRL_REG,
|
|
TAS675X_SCLK_INV_MASK, 0x00);
|
|
break;
|
|
case SND_SOC_DAIFMT_IB_NF:
|
|
regmap_update_bits(component->regmap, TAS675X_SCLK_INV_CTRL_REG,
|
|
TAS675X_SCLK_INV_MASK, TAS675X_SCLK_INV_MASK);
|
|
break;
|
|
default:
|
|
dev_err(component->dev, "Unsupported clock inversion\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Configure Audio Format and TDM Enable */
|
|
switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
|
|
case SND_SOC_DAIFMT_I2S:
|
|
i2s_mode = true;
|
|
tas->bclk_offset = 0;
|
|
regmap_update_bits(component->regmap, TAS675X_AUDIO_IF_CTRL_REG,
|
|
TAS675X_TDM_EN_BIT | TAS675X_SAP_FMT_MASK |
|
|
TAS675X_FS_PULSE_MASK,
|
|
TAS675X_SAP_FMT_I2S);
|
|
regmap_update_bits(component->regmap, TAS675X_SDOUT_CTRL_REG,
|
|
TAS675X_SDOUT_SELECT_MASK,
|
|
TAS675X_SDOUT_SELECT_NON_TDM);
|
|
break;
|
|
case SND_SOC_DAIFMT_RIGHT_J:
|
|
tas->bclk_offset = 0;
|
|
regmap_update_bits(component->regmap, TAS675X_AUDIO_IF_CTRL_REG,
|
|
TAS675X_TDM_EN_BIT | TAS675X_SAP_FMT_MASK |
|
|
TAS675X_FS_PULSE_MASK,
|
|
TAS675X_SAP_FMT_RIGHT_J);
|
|
regmap_update_bits(component->regmap, TAS675X_SDOUT_CTRL_REG,
|
|
TAS675X_SDOUT_SELECT_MASK,
|
|
TAS675X_SDOUT_SELECT_NON_TDM);
|
|
break;
|
|
case SND_SOC_DAIFMT_LEFT_J:
|
|
tas->bclk_offset = 0;
|
|
regmap_update_bits(component->regmap, TAS675X_AUDIO_IF_CTRL_REG,
|
|
TAS675X_TDM_EN_BIT | TAS675X_SAP_FMT_MASK |
|
|
TAS675X_FS_PULSE_MASK,
|
|
TAS675X_SAP_FMT_LEFT_J);
|
|
regmap_update_bits(component->regmap, TAS675X_SDOUT_CTRL_REG,
|
|
TAS675X_SDOUT_SELECT_MASK,
|
|
TAS675X_SDOUT_SELECT_NON_TDM);
|
|
break;
|
|
case SND_SOC_DAIFMT_DSP_A:
|
|
tdm_mode = true;
|
|
tas->bclk_offset = 1;
|
|
regmap_update_bits(component->regmap, TAS675X_AUDIO_IF_CTRL_REG,
|
|
TAS675X_TDM_EN_BIT | TAS675X_SAP_FMT_MASK |
|
|
TAS675X_FS_PULSE_MASK,
|
|
TAS675X_TDM_EN_BIT | TAS675X_SAP_FMT_TDM |
|
|
TAS675X_FS_PULSE_SHORT);
|
|
regmap_update_bits(component->regmap, TAS675X_SDOUT_CTRL_REG,
|
|
TAS675X_SDOUT_SELECT_MASK,
|
|
TAS675X_SDOUT_SELECT_TDM_SDOUT1);
|
|
break;
|
|
case SND_SOC_DAIFMT_DSP_B:
|
|
tdm_mode = true;
|
|
tas->bclk_offset = 0;
|
|
regmap_update_bits(component->regmap, TAS675X_AUDIO_IF_CTRL_REG,
|
|
TAS675X_TDM_EN_BIT | TAS675X_SAP_FMT_MASK |
|
|
TAS675X_FS_PULSE_MASK,
|
|
TAS675X_TDM_EN_BIT | TAS675X_SAP_FMT_TDM |
|
|
TAS675X_FS_PULSE_SHORT);
|
|
regmap_update_bits(component->regmap, TAS675X_SDOUT_CTRL_REG,
|
|
TAS675X_SDOUT_SELECT_MASK,
|
|
TAS675X_SDOUT_SELECT_TDM_SDOUT1);
|
|
break;
|
|
default:
|
|
dev_err(component->dev, "Unsupported DAI format\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Setup Vpredict and Isense outputs */
|
|
if (dai->id == 2) {
|
|
unsigned int sdout_en = 0;
|
|
|
|
if (tdm_mode) {
|
|
/* TDM: Vpredict and Isense may coexist on separate slots */
|
|
if (tas->vpredict_slot >= 0)
|
|
sdout_en |= TAS675X_SDOUT_EN_VPREDICT;
|
|
if (tas->isense_slot >= 0)
|
|
sdout_en |= TAS675X_SDOUT_EN_ISENSE;
|
|
regmap_update_bits(component->regmap,
|
|
TAS675X_SDOUT_EN_REG,
|
|
TAS675X_SDOUT_EN_VPREDICT |
|
|
TAS675X_SDOUT_EN_ISENSE,
|
|
sdout_en);
|
|
if (tas->vpredict_slot >= 0 && tas->isense_slot >= 0 &&
|
|
abs(tas->vpredict_slot - tas->isense_slot) < 4)
|
|
dev_warn(component->dev,
|
|
"ti,vpredict-slot-no and ti,isense-slot-no overlaps (each occupies 4 consecutive slots)\n");
|
|
} else if (i2s_mode) {
|
|
/* I2S: only one source at a time; Vpredict takes priority */
|
|
if (tas->vpredict_slot >= 0)
|
|
sdout_en = TAS675X_SDOUT_NON_TDM_SEL_VPREDICT |
|
|
TAS675X_SDOUT_EN_NON_TDM_ALL;
|
|
else if (tas->isense_slot >= 0)
|
|
sdout_en = TAS675X_SDOUT_NON_TDM_SEL_ISENSE |
|
|
TAS675X_SDOUT_EN_NON_TDM_ALL;
|
|
regmap_update_bits(component->regmap,
|
|
TAS675X_SDOUT_EN_REG,
|
|
TAS675X_SDOUT_NON_TDM_SEL_MASK |
|
|
TAS675X_SDOUT_EN_NON_TDM_ALL,
|
|
sdout_en);
|
|
if (sdout_en &&
|
|
tas->gpio1_func != TAS675X_GPIO_SEL_SDOUT2 &&
|
|
tas->gpio2_func != TAS675X_GPIO_SEL_SDOUT2)
|
|
dev_warn(component->dev,
|
|
"sdout enabled in I2S mode but no GPIO configured as SDOUT2; Ch3/Ch4 will be absent\n");
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tas675x_set_tdm_slot(struct snd_soc_dai *dai, unsigned int tx_mask,
|
|
unsigned int rx_mask, int slots, int slot_width)
|
|
{
|
|
struct tas675x_priv *tas = snd_soc_component_get_drvdata(dai->component);
|
|
|
|
if (slots == 0) {
|
|
tas->slot_width = 0;
|
|
tas->tx_mask = 0;
|
|
return 0;
|
|
}
|
|
|
|
/* No rx_mask as hardware does not support channel muxing for capture */
|
|
tas->slot_width = slot_width;
|
|
tas->tx_mask = tx_mask;
|
|
return 0;
|
|
}
|
|
|
|
static int tas675x_mute_stream(struct snd_soc_dai *dai, int mute, int direction)
|
|
{
|
|
struct snd_soc_component *component = dai->component;
|
|
struct tas675x_priv *tas = snd_soc_component_get_drvdata(component);
|
|
unsigned int discard;
|
|
int ret;
|
|
|
|
if (direction == SNDRV_PCM_STREAM_CAPTURE) {
|
|
if (mute)
|
|
clear_bit(dai->id, &tas->active_capture_dais);
|
|
else
|
|
set_bit(dai->id, &tas->active_capture_dais);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Track which playback DAIs are active.
|
|
* The TAS675x has two playback DAIs (main audio and LLP).
|
|
* Only transition to SLEEP when ALL are muted.
|
|
*/
|
|
if (mute)
|
|
clear_bit(dai->id, &tas->active_playback_dais);
|
|
else
|
|
set_bit(dai->id, &tas->active_playback_dais);
|
|
|
|
/* Last playback stream */
|
|
if (mute && !READ_ONCE(tas->active_playback_dais)) {
|
|
ret = tas675x_set_state_all(tas, TAS675X_STATE_SLEEP_BOTH);
|
|
regmap_read(tas->regmap, TAS675X_CLK_FAULT_LATCHED_REG, &discard);
|
|
return ret;
|
|
}
|
|
|
|
return tas675x_set_state_all(tas,
|
|
READ_ONCE(tas->active_playback_dais) ?
|
|
TAS675X_STATE_PLAY_BOTH :
|
|
TAS675X_STATE_SLEEP_BOTH);
|
|
}
|
|
|
|
static const struct snd_soc_dai_ops tas675x_dai_ops = {
|
|
.hw_params = tas675x_hw_params,
|
|
.set_fmt = tas675x_set_fmt,
|
|
.set_tdm_slot = tas675x_set_tdm_slot,
|
|
.mute_stream = tas675x_mute_stream,
|
|
};
|
|
|
|
static struct snd_soc_dai_driver tas675x_dais[] = {
|
|
{
|
|
.name = "tas675x-audio",
|
|
.id = 0,
|
|
.playback = {
|
|
.stream_name = "Playback",
|
|
.channels_min = 2,
|
|
.channels_max = 4,
|
|
.rates = SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000 |
|
|
SNDRV_PCM_RATE_96000 | SNDRV_PCM_RATE_192000,
|
|
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_LE |
|
|
SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
|
|
},
|
|
.ops = &tas675x_dai_ops,
|
|
},
|
|
/* Only available when Low Latency Path (LLP) is enabled */
|
|
{
|
|
.name = "tas675x-anc",
|
|
.id = 1,
|
|
.playback = {
|
|
.stream_name = "ANC Playback",
|
|
.channels_min = 2,
|
|
.channels_max = 4,
|
|
.rates = SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_96000,
|
|
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_LE |
|
|
SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
|
|
},
|
|
.ops = &tas675x_dai_ops,
|
|
},
|
|
{
|
|
.name = "tas675x-feedback",
|
|
.id = 2,
|
|
.capture = {
|
|
.stream_name = "Feedback Capture",
|
|
.channels_min = 2,
|
|
.channels_max = 8,
|
|
.rates = SNDRV_PCM_RATE_48000,
|
|
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_LE |
|
|
SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
|
|
},
|
|
.ops = &tas675x_dai_ops,
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Enable regulators and release hardware reset GPIOs.
|
|
* The device is not I2C-accessible until this returns.
|
|
*/
|
|
static int tas675x_hw_enable(struct tas675x_priv *tas)
|
|
{
|
|
int ret;
|
|
|
|
ret = regulator_bulk_enable(ARRAY_SIZE(tas->supplies), tas->supplies);
|
|
if (ret) {
|
|
dev_err(tas->dev, "Failed to enable regulators: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
if (!IS_ERR(tas->vbat)) {
|
|
ret = regulator_enable(tas->vbat);
|
|
if (ret) {
|
|
dev_err(tas->dev, "Failed to enable vbat: %d\n", ret);
|
|
regulator_bulk_disable(ARRAY_SIZE(tas->supplies), tas->supplies);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
if (tas->pd_gpio && tas->stby_gpio) {
|
|
/*
|
|
* Independent Pin Control
|
|
* Deassert PD first to boot digital, then STBY for analog.
|
|
*/
|
|
/* Min 4ms digital boot wait */
|
|
gpiod_set_value_cansleep(tas->pd_gpio, 0);
|
|
usleep_range(4000, 5000);
|
|
|
|
/* ~2ms analog stabilization */
|
|
gpiod_set_value_cansleep(tas->stby_gpio, 0);
|
|
usleep_range(2000, 3000);
|
|
} else if (tas->pd_gpio) {
|
|
/*
|
|
* Simultaneous Pin Release
|
|
* STBY tied to PD or hardwired HIGH.
|
|
*/
|
|
/* 6ms wait for simultaneous release transition */
|
|
gpiod_set_value_cansleep(tas->pd_gpio, 0);
|
|
usleep_range(6000, 7000);
|
|
} else {
|
|
/*
|
|
* PD hardwired, device in DEEP_SLEEP.
|
|
* Digital core already booted, I2C active. Deassert STBY
|
|
* to bring up the analog output stage.
|
|
*/
|
|
/* ~2ms analog stabilization */
|
|
gpiod_set_value_cansleep(tas->stby_gpio, 0);
|
|
usleep_range(2000, 3000);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void tas675x_hw_disable(struct tas675x_priv *tas)
|
|
{
|
|
if (tas->stby_gpio)
|
|
gpiod_set_value_cansleep(tas->stby_gpio, 1);
|
|
|
|
if (tas->pd_gpio)
|
|
gpiod_set_value_cansleep(tas->pd_gpio, 1);
|
|
|
|
/*
|
|
* Hold PD/STBY asserted for at least 10ms
|
|
* before removing PVDD, VBAT or DVDD.
|
|
*/
|
|
usleep_range(10000, 11000);
|
|
|
|
if (!IS_ERR(tas->vbat))
|
|
regulator_disable(tas->vbat);
|
|
|
|
regulator_bulk_disable(ARRAY_SIZE(tas->supplies), tas->supplies);
|
|
}
|
|
|
|
/*
|
|
* Write device start-up defaults.
|
|
* Must be called after tas675x_hw_enable() and after regcache is enabled.
|
|
*/
|
|
static int tas675x_init_device(struct tas675x_priv *tas)
|
|
{
|
|
struct regmap *regmap = tas->regmap;
|
|
unsigned int val;
|
|
int ret, i;
|
|
|
|
/* Clear POR fault flag to prevent IRQ storm */
|
|
regmap_read(regmap, TAS675X_POWER_FAULT_LATCHED_REG, &val);
|
|
|
|
/* Bypass DC Load Diagnostics for fast boot */
|
|
if (tas->fast_boot)
|
|
regmap_update_bits(regmap, TAS675X_DC_LDG_CTRL_REG,
|
|
TAS675X_LDG_ABORT_BIT | TAS675X_LDG_BYPASS_BIT,
|
|
TAS675X_LDG_ABORT_BIT | TAS675X_LDG_BYPASS_BIT);
|
|
|
|
tas675x_select_book(regmap, TAS675X_BOOK_DEFAULT);
|
|
|
|
/* Enter setup mode */
|
|
ret = regmap_write(regmap, TAS675X_SETUP_REG1, TAS675X_SETUP_ENTER_VAL1);
|
|
if (ret)
|
|
goto err;
|
|
ret = regmap_write(regmap, TAS675X_SETUP_REG2, TAS675X_SETUP_ENTER_VAL2);
|
|
if (ret)
|
|
goto err;
|
|
|
|
/* Set all channels to Sleep (required before Page 1 config) */
|
|
tas675x_set_state_all(tas, TAS675X_STATE_SLEEP_BOTH);
|
|
|
|
/* Set DAC clock per TRM startup script */
|
|
regmap_write(regmap, TAS675X_DAC_CLK_REG, 0x00);
|
|
|
|
/*
|
|
* Switch to Page 1 for safety-critical OC/CBC configuration,
|
|
* while bypassing regcache. (Page 1 not accessible post setup)
|
|
*/
|
|
regcache_cache_bypass(regmap, true);
|
|
ret = regmap_multi_reg_write(regmap, tas675x_page1_init,
|
|
ARRAY_SIZE(tas675x_page1_init));
|
|
regcache_cache_bypass(regmap, false);
|
|
if (ret)
|
|
goto err_setup;
|
|
|
|
/* Resync regmap's cached page selector */
|
|
regmap_write(regmap, TAS675X_PAGE_CTRL_REG, 0x00);
|
|
|
|
/* Exit setup mode */
|
|
regmap_write(regmap, TAS675X_SETUP_REG1, TAS675X_SETUP_EXIT_VAL);
|
|
regmap_write(regmap, TAS675X_SETUP_REG2, TAS675X_SETUP_EXIT_VAL);
|
|
|
|
/* Write DSP parameters if cached */
|
|
for (i = 0; i < ARRAY_SIZE(tas->dsp_params); i++) {
|
|
if (tas->dsp_params[i].val)
|
|
tas675x_dsp_mem_write(tas,
|
|
tas->dsp_params[i].page,
|
|
tas->dsp_params[i].reg,
|
|
tas->dsp_params[i].val);
|
|
}
|
|
|
|
/*
|
|
* Configure fault and warning event routing:
|
|
*
|
|
* ROUTING_1: CP fault/UVLO latch, OUTM soft short latch
|
|
* ROUTING_2: CBC latch, OTSD latch, OTSD, power fault
|
|
* ROUTING_3: CBC latch, OTSD latch, power latch, DC LDG,
|
|
* OTSD, power warnings
|
|
* ROUTING_4: OC latch, DC latch, protection shutdown
|
|
* OTW latch, OTW, clip latch
|
|
* ROUTING_5: clock latch+non-latch, RTLDG latch
|
|
* CBC warning, clip warning
|
|
*/
|
|
regmap_write(regmap, TAS675X_REPORT_ROUTING_1_REG, 0x70);
|
|
regmap_write(regmap, TAS675X_REPORT_ROUTING_2_REG, 0xA3);
|
|
regmap_write(regmap, TAS675X_REPORT_ROUTING_3_REG, 0xBB);
|
|
regmap_write(regmap, TAS675X_REPORT_ROUTING_4_REG, 0x7E);
|
|
regmap_write(regmap, TAS675X_REPORT_ROUTING_5_REG, 0xF3);
|
|
|
|
/* Configure GPIO pins if specified in DT */
|
|
if (tas->gpio1_func >= 0 || tas->gpio2_func >= 0) {
|
|
unsigned int gpio_ctrl = TAS675X_GPIO_CTRL_RSTVAL;
|
|
|
|
tas675x_config_gpio_pin(regmap, tas->gpio1_func,
|
|
TAS675X_GPIO1_OUTPUT_SEL_REG,
|
|
0, &gpio_ctrl);
|
|
tas675x_config_gpio_pin(regmap, tas->gpio2_func,
|
|
TAS675X_GPIO2_OUTPUT_SEL_REG,
|
|
1, &gpio_ctrl);
|
|
regmap_write(regmap, TAS675X_GPIO_CTRL_REG, gpio_ctrl);
|
|
}
|
|
|
|
/* Clear fast boot bits */
|
|
if (tas->fast_boot)
|
|
regmap_update_bits(regmap, TAS675X_DC_LDG_CTRL_REG,
|
|
TAS675X_LDG_ABORT_BIT | TAS675X_LDG_BYPASS_BIT,
|
|
0);
|
|
|
|
/* Clear any stale faults from the boot sequence */
|
|
regmap_read(regmap, TAS675X_POWER_FAULT_STATUS_1_REG, &val);
|
|
regmap_read(regmap, TAS675X_POWER_FAULT_LATCHED_REG, &val);
|
|
regmap_read(regmap, TAS675X_CLK_FAULT_LATCHED_REG, &val);
|
|
regmap_write(regmap, TAS675X_RESET_REG, TAS675X_FAULT_CLEAR);
|
|
|
|
return 0;
|
|
|
|
err_setup:
|
|
regmap_write(regmap, TAS675X_SETUP_REG1, TAS675X_SETUP_EXIT_VAL);
|
|
regmap_write(regmap, TAS675X_SETUP_REG2, TAS675X_SETUP_EXIT_VAL);
|
|
err:
|
|
dev_err(tas->dev, "Init device failed: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
static void tas675x_power_off(struct tas675x_priv *tas)
|
|
{
|
|
regcache_cache_only(tas->regmap, true);
|
|
regcache_mark_dirty(tas->regmap);
|
|
tas675x_hw_disable(tas);
|
|
}
|
|
|
|
static int tas675x_power_on(struct tas675x_priv *tas)
|
|
{
|
|
int ret;
|
|
|
|
ret = tas675x_hw_enable(tas);
|
|
if (ret)
|
|
return ret;
|
|
|
|
regcache_cache_only(tas->regmap, false);
|
|
regcache_mark_dirty(tas->regmap);
|
|
|
|
ret = tas675x_init_device(tas);
|
|
if (ret)
|
|
goto err_disable;
|
|
|
|
ret = regcache_sync(tas->regmap);
|
|
if (ret) {
|
|
dev_err(tas->dev, "Failed to sync regcache: %d\n", ret);
|
|
goto err_disable;
|
|
}
|
|
|
|
/* Reset fault tracking */
|
|
memset(tas->last_status, 0, sizeof(tas->last_status));
|
|
|
|
return 0;
|
|
|
|
err_disable:
|
|
tas675x_power_off(tas);
|
|
return ret;
|
|
}
|
|
|
|
static int tas675x_runtime_suspend(struct device *dev)
|
|
{
|
|
struct tas675x_priv *tas = dev_get_drvdata(dev);
|
|
|
|
disable_delayed_work_sync(&tas->fault_check_work);
|
|
tas675x_set_state_all(tas, TAS675X_STATE_SLEEP_BOTH);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tas675x_runtime_resume(struct device *dev)
|
|
{
|
|
struct tas675x_priv *tas = dev_get_drvdata(dev);
|
|
|
|
tas675x_set_state_all(tas, TAS675X_STATE_SLEEP_BOTH);
|
|
|
|
if (!to_i2c_client(dev)->irq) {
|
|
enable_delayed_work(&tas->fault_check_work);
|
|
schedule_delayed_work(&tas->fault_check_work,
|
|
msecs_to_jiffies(TAS675X_FAULT_CHECK_INTERVAL_MS));
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int tas675x_system_suspend(struct device *dev)
|
|
{
|
|
struct tas675x_priv *tas = dev_get_drvdata(dev);
|
|
int ret;
|
|
|
|
ret = tas675x_runtime_suspend(dev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (to_i2c_client(dev)->irq)
|
|
disable_irq(to_i2c_client(dev)->irq);
|
|
|
|
tas675x_power_off(tas);
|
|
return 0;
|
|
}
|
|
|
|
static int tas675x_system_resume(struct device *dev)
|
|
{
|
|
struct tas675x_priv *tas = dev_get_drvdata(dev);
|
|
int ret;
|
|
|
|
ret = tas675x_power_on(tas);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (to_i2c_client(dev)->irq)
|
|
enable_irq(to_i2c_client(dev)->irq);
|
|
|
|
return tas675x_runtime_resume(dev);
|
|
}
|
|
|
|
static const struct snd_soc_component_driver soc_codec_dev_tas675x = {
|
|
.controls = tas675x_snd_controls,
|
|
.num_controls = ARRAY_SIZE(tas675x_snd_controls),
|
|
.dapm_widgets = tas675x_dapm_widgets,
|
|
.num_dapm_widgets = ARRAY_SIZE(tas675x_dapm_widgets),
|
|
.dapm_routes = tas675x_dapm_routes,
|
|
.num_dapm_routes = ARRAY_SIZE(tas675x_dapm_routes),
|
|
.endianness = 1,
|
|
};
|
|
|
|
/* Fault register flags */
|
|
#define TAS675X_FAULT_CRITICAL BIT(0) /* causes FAULT state, FAULT_CLEAR required */
|
|
#define TAS675X_FAULT_TRACK BIT(1) /* track last value, only log on change */
|
|
#define TAS675X_FAULT_ACTIVE BIT(2) /* skip when no stream is active */
|
|
|
|
struct tas675x_fault_reg {
|
|
unsigned int reg;
|
|
unsigned int flags;
|
|
const char *name;
|
|
};
|
|
|
|
static const struct tas675x_fault_reg tas675x_fault_table[] = {
|
|
/* Critical */
|
|
{ TAS675X_OTSD_LATCHED_REG, TAS675X_FAULT_CRITICAL | TAS675X_FAULT_TRACK,
|
|
"Overtemperature Shutdown" },
|
|
{ TAS675X_OC_DC_FAULT_LATCHED_REG, TAS675X_FAULT_CRITICAL | TAS675X_FAULT_TRACK,
|
|
"Overcurrent / DC Fault" },
|
|
{ TAS675X_RTLDG_OL_SL_FAULT_LATCHED_REG, TAS675X_FAULT_CRITICAL | TAS675X_FAULT_TRACK,
|
|
"Real-Time Load Diagnostic Fault" },
|
|
{ TAS675X_CBC_FAULT_WARN_LATCHED_REG, TAS675X_FAULT_CRITICAL | TAS675X_FAULT_TRACK,
|
|
"CBC Fault/Warning" },
|
|
/* Warning */
|
|
{ TAS675X_POWER_FAULT_STATUS_1_REG, TAS675X_FAULT_TRACK,
|
|
"CP / OUTM Fault" },
|
|
{ TAS675X_POWER_FAULT_LATCHED_REG, TAS675X_FAULT_TRACK,
|
|
"Power Fault" },
|
|
{ TAS675X_CLK_FAULT_LATCHED_REG, TAS675X_FAULT_TRACK | TAS675X_FAULT_ACTIVE,
|
|
"Clock Fault" },
|
|
{ TAS675X_OTW_LATCHED_REG, TAS675X_FAULT_TRACK,
|
|
"Overtemperature Warning" },
|
|
{ TAS675X_CLIP_WARN_LATCHED_REG, TAS675X_FAULT_ACTIVE,
|
|
"Clip Warning" },
|
|
};
|
|
|
|
static_assert(ARRAY_SIZE(tas675x_fault_table) == TAS675X_FAULT_REGS_NUM);
|
|
|
|
/*
|
|
* Read and log all latched fault registers.
|
|
* Shared by both the polled fault_check_work and IRQ handler paths
|
|
* (which are mutually exclusive, only one is active per device).
|
|
* Returns true if any fault register needs to be cleared.
|
|
*
|
|
* For deciphering fault messages, see "Fault Monitoring" in
|
|
* Documentation/sound/codecs/tas675x.rst
|
|
*/
|
|
static bool tas675x_check_faults(struct tas675x_priv *tas)
|
|
{
|
|
struct device *dev = tas->dev;
|
|
bool needs_clear = false;
|
|
unsigned int reg;
|
|
int i, ret;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(tas675x_fault_table); i++) {
|
|
const struct tas675x_fault_reg *f = &tas675x_fault_table[i];
|
|
|
|
ret = regmap_read(tas->regmap, f->reg, ®);
|
|
if (ret) {
|
|
if (f->flags & TAS675X_FAULT_CRITICAL) {
|
|
dev_err(dev, "failed to read %s: %d\n", f->name, ret);
|
|
return needs_clear;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (reg)
|
|
needs_clear = true;
|
|
|
|
/* Skip logging stream-dependent events when no stream is active */
|
|
if ((f->flags & TAS675X_FAULT_ACTIVE) &&
|
|
!READ_ONCE(tas->active_playback_dais) &&
|
|
!READ_ONCE(tas->active_capture_dais))
|
|
continue;
|
|
|
|
/* Log on change or on every non-zero read */
|
|
if (reg && (!(f->flags & TAS675X_FAULT_TRACK) ||
|
|
reg != tas->last_status[i])) {
|
|
if (f->flags & TAS675X_FAULT_CRITICAL)
|
|
dev_crit(dev, "%s Latched: 0x%02x\n", f->name, reg);
|
|
else
|
|
dev_warn(dev, "%s Latched: 0x%02x\n", f->name, reg);
|
|
}
|
|
|
|
if (f->flags & TAS675X_FAULT_TRACK)
|
|
tas->last_status[i] = reg;
|
|
}
|
|
|
|
return needs_clear;
|
|
}
|
|
|
|
static void tas675x_fault_check_work(struct work_struct *work)
|
|
{
|
|
struct tas675x_priv *tas = container_of(work, struct tas675x_priv,
|
|
fault_check_work.work);
|
|
|
|
if (tas675x_check_faults(tas))
|
|
regmap_write(tas->regmap, TAS675X_RESET_REG, TAS675X_FAULT_CLEAR);
|
|
|
|
schedule_delayed_work(&tas->fault_check_work,
|
|
msecs_to_jiffies(TAS675X_FAULT_CHECK_INTERVAL_MS));
|
|
}
|
|
|
|
static irqreturn_t tas675x_irq_handler(int irq, void *data)
|
|
{
|
|
struct tas675x_priv *tas = data;
|
|
irqreturn_t ret = IRQ_NONE;
|
|
|
|
if (pm_runtime_resume_and_get(tas->dev) < 0)
|
|
return IRQ_NONE;
|
|
|
|
if (tas675x_check_faults(tas)) {
|
|
regmap_write(tas->regmap, TAS675X_RESET_REG, TAS675X_FAULT_CLEAR);
|
|
ret = IRQ_HANDLED;
|
|
}
|
|
|
|
pm_runtime_mark_last_busy(tas->dev);
|
|
pm_runtime_put_autosuspend(tas->dev);
|
|
return ret;
|
|
}
|
|
|
|
static const struct reg_default tas675x_reg_defaults[] = {
|
|
{ TAS675X_PAGE_CTRL_REG, 0x00 },
|
|
{ TAS675X_OUTPUT_CTRL_REG, 0x00 },
|
|
{ TAS675X_STATE_CTRL_CH1_CH2_REG, TAS675X_STATE_SLEEP_BOTH },
|
|
{ TAS675X_STATE_CTRL_CH3_CH4_REG, TAS675X_STATE_SLEEP_BOTH },
|
|
{ TAS675X_ISENSE_CTRL_REG, 0x0F },
|
|
{ TAS675X_DC_DETECT_CTRL_REG, 0x00 },
|
|
{ TAS675X_SCLK_INV_CTRL_REG, 0x00 },
|
|
{ TAS675X_AUDIO_IF_CTRL_REG, 0x00 },
|
|
{ TAS675X_SDIN_CTRL_REG, 0x0A },
|
|
{ TAS675X_SDOUT_CTRL_REG, 0x1A },
|
|
{ TAS675X_SDIN_OFFSET_MSB_REG, 0x00 },
|
|
{ TAS675X_SDIN_AUDIO_OFFSET_REG, 0x00 },
|
|
{ TAS675X_SDIN_LL_OFFSET_REG, 0x60 },
|
|
{ TAS675X_SDIN_CH_SWAP_REG, 0x00 },
|
|
{ TAS675X_SDOUT_OFFSET_MSB_REG, 0xCF },
|
|
{ TAS675X_VPREDICT_OFFSET_REG, 0xFF },
|
|
{ TAS675X_ISENSE_OFFSET_REG, 0x00 },
|
|
{ TAS675X_SDOUT_EN_REG, 0x00 },
|
|
{ TAS675X_LL_EN_REG, 0x00 },
|
|
{ TAS675X_RTLDG_EN_REG, 0x10 },
|
|
{ TAS675X_DC_BLOCK_BYP_REG, 0x00 },
|
|
{ TAS675X_DSP_CTRL_REG, 0x00 },
|
|
{ TAS675X_PAGE_AUTO_INC_REG, 0x00 },
|
|
{ TAS675X_DIG_VOL_CH1_REG, 0x30 },
|
|
{ TAS675X_DIG_VOL_CH2_REG, 0x30 },
|
|
{ TAS675X_DIG_VOL_CH3_REG, 0x30 },
|
|
{ TAS675X_DIG_VOL_CH4_REG, 0x30 },
|
|
{ TAS675X_DIG_VOL_RAMP_CTRL_REG, 0x77 },
|
|
{ TAS675X_DIG_VOL_COMBINE_CTRL_REG, 0x00 },
|
|
{ TAS675X_AUTO_MUTE_EN_REG, 0x00 },
|
|
{ TAS675X_AUTO_MUTE_TIMING_CH1_CH2_REG, 0x00 },
|
|
{ TAS675X_AUTO_MUTE_TIMING_CH3_CH4_REG, 0x00 },
|
|
{ TAS675X_ANALOG_GAIN_CH1_CH2_REG, 0x00 },
|
|
{ TAS675X_ANALOG_GAIN_CH3_CH4_REG, 0x00 },
|
|
{ TAS675X_ANALOG_GAIN_RAMP_CTRL_REG, 0x00 },
|
|
{ TAS675X_PULSE_INJECTION_EN_REG, 0x03 },
|
|
{ TAS675X_CBC_CTRL_REG, 0x07 },
|
|
{ TAS675X_CURRENT_LIMIT_CTRL_REG, 0x00 },
|
|
{ TAS675X_ISENSE_CAL_REG, 0x00 },
|
|
{ TAS675X_PWM_PHASE_CTRL_REG, 0x00 },
|
|
{ TAS675X_SS_CTRL_REG, 0x00 },
|
|
{ TAS675X_SS_RANGE_CTRL_REG, 0x00 },
|
|
{ TAS675X_SS_DWELL_CTRL_REG, 0x00 },
|
|
{ TAS675X_RAMP_PHASE_CTRL_GPO_REG, 0x00 },
|
|
{ TAS675X_PWM_PHASE_M_CTRL_CH1_REG, 0x00 },
|
|
{ TAS675X_PWM_PHASE_M_CTRL_CH2_REG, 0x00 },
|
|
{ TAS675X_PWM_PHASE_M_CTRL_CH3_REG, 0x00 },
|
|
{ TAS675X_PWM_PHASE_M_CTRL_CH4_REG, 0x00 },
|
|
{ TAS675X_DC_LDG_CTRL_REG, 0x00 },
|
|
{ TAS675X_DC_LDG_LO_CTRL_REG, 0x00 },
|
|
{ TAS675X_DC_LDG_TIME_CTRL_REG, 0x00 },
|
|
{ TAS675X_DC_LDG_SL_CH1_CH2_CTRL_REG, 0x11 },
|
|
{ TAS675X_DC_LDG_SL_CH3_CH4_CTRL_REG, 0x11 },
|
|
{ TAS675X_AC_LDG_CTRL_REG, 0x10 },
|
|
{ TAS675X_TWEETER_DETECT_CTRL_REG, 0x08 },
|
|
{ TAS675X_TWEETER_DETECT_THRESH_REG, 0x00 },
|
|
{ TAS675X_AC_LDG_FREQ_CTRL_REG, 0xC8 },
|
|
{ TAS675X_REPORT_ROUTING_1_REG, 0x00 },
|
|
{ TAS675X_OTSD_RECOVERY_EN_REG, 0x00 },
|
|
{ TAS675X_REPORT_ROUTING_2_REG, 0xA2 },
|
|
{ TAS675X_REPORT_ROUTING_3_REG, 0x00 },
|
|
{ TAS675X_REPORT_ROUTING_4_REG, 0x06 },
|
|
{ TAS675X_CLIP_DETECT_CTRL_REG, 0x00 },
|
|
{ TAS675X_REPORT_ROUTING_5_REG, 0x00 },
|
|
{ TAS675X_GPIO1_OUTPUT_SEL_REG, 0x00 },
|
|
{ TAS675X_GPIO2_OUTPUT_SEL_REG, 0x00 },
|
|
{ TAS675X_GPIO_CTRL_REG, TAS675X_GPIO_CTRL_RSTVAL },
|
|
{ TAS675X_OTW_CTRL_CH1_CH2_REG, 0x11 },
|
|
{ TAS675X_OTW_CTRL_CH3_CH4_REG, 0x11 },
|
|
};
|
|
|
|
static bool tas675x_is_readable_register(struct device *dev, unsigned int reg)
|
|
{
|
|
switch (reg) {
|
|
case TAS675X_RESET_REG:
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
static bool tas675x_is_volatile_register(struct device *dev, unsigned int reg)
|
|
{
|
|
switch (reg) {
|
|
case TAS675X_RESET_REG:
|
|
case TAS675X_BOOK_CTRL_REG:
|
|
case TAS675X_AUTO_MUTE_STATUS_REG:
|
|
case TAS675X_STATE_REPORT_CH1_CH2_REG:
|
|
case TAS675X_STATE_REPORT_CH3_CH4_REG:
|
|
case TAS675X_PVDD_SENSE_REG:
|
|
case TAS675X_TEMP_GLOBAL_REG:
|
|
case TAS675X_TEMP_CH1_CH2_REG:
|
|
case TAS675X_TEMP_CH3_CH4_REG:
|
|
case TAS675X_FS_MON_REG:
|
|
case TAS675X_SCLK_MON_REG:
|
|
case TAS675X_POWER_FAULT_STATUS_1_REG:
|
|
case TAS675X_POWER_FAULT_STATUS_2_REG:
|
|
case TAS675X_OT_FAULT_REG:
|
|
case TAS675X_OTW_STATUS_REG:
|
|
case TAS675X_CLIP_WARN_STATUS_REG:
|
|
case TAS675X_CBC_WARNING_STATUS_REG:
|
|
case TAS675X_POWER_FAULT_LATCHED_REG:
|
|
case TAS675X_OTSD_LATCHED_REG:
|
|
case TAS675X_OTW_LATCHED_REG:
|
|
case TAS675X_CLIP_WARN_LATCHED_REG:
|
|
case TAS675X_CLK_FAULT_LATCHED_REG:
|
|
case TAS675X_RTLDG_OL_SL_FAULT_LATCHED_REG:
|
|
case TAS675X_CBC_FAULT_WARN_LATCHED_REG:
|
|
case TAS675X_OC_DC_FAULT_LATCHED_REG:
|
|
case TAS675X_WARN_OT_MAX_FLAG_REG:
|
|
case TAS675X_DC_LDG_REPORT_CH1_CH2_REG ... TAS675X_TWEETER_REPORT_REG:
|
|
case TAS675X_CH1_RTLDG_IMP_MSB_REG ... TAS675X_CH4_DC_LDG_DCR_LSB_REG:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static const struct regmap_range_cfg tas675x_ranges[] = {
|
|
{
|
|
.name = "Pages",
|
|
.range_min = 0,
|
|
.range_max = TAS675X_PAGE_SIZE * TAS675X_PAGE_SIZE - 1,
|
|
.selector_reg = TAS675X_PAGE_CTRL_REG,
|
|
.selector_mask = 0xff,
|
|
.selector_shift = 0,
|
|
.window_start = 0,
|
|
.window_len = TAS675X_PAGE_SIZE,
|
|
},
|
|
};
|
|
|
|
static void tas675x_regmap_lock(void *lock_arg)
|
|
{
|
|
struct tas675x_priv *tas = lock_arg;
|
|
|
|
mutex_lock(&tas->io_lock);
|
|
}
|
|
|
|
static void tas675x_regmap_unlock(void *lock_arg)
|
|
{
|
|
struct tas675x_priv *tas = lock_arg;
|
|
|
|
mutex_unlock(&tas->io_lock);
|
|
}
|
|
|
|
static const struct regmap_config tas675x_regmap_config = {
|
|
.reg_bits = 8,
|
|
.val_bits = 8,
|
|
.max_register = TAS675X_PAGE_SIZE * TAS675X_PAGE_SIZE - 1,
|
|
.ranges = tas675x_ranges,
|
|
.num_ranges = ARRAY_SIZE(tas675x_ranges),
|
|
.cache_type = REGCACHE_MAPLE,
|
|
.reg_defaults = tas675x_reg_defaults,
|
|
.num_reg_defaults = ARRAY_SIZE(tas675x_reg_defaults),
|
|
.readable_reg = tas675x_is_readable_register,
|
|
.volatile_reg = tas675x_is_volatile_register,
|
|
};
|
|
|
|
static int tas675x_i2c_probe(struct i2c_client *client)
|
|
{
|
|
struct regmap_config cfg = tas675x_regmap_config;
|
|
struct tas675x_priv *tas;
|
|
u32 val;
|
|
int i, ret;
|
|
|
|
tas = devm_kzalloc(&client->dev, sizeof(*tas), GFP_KERNEL);
|
|
if (!tas)
|
|
return -ENOMEM;
|
|
|
|
tas->dev = &client->dev;
|
|
i2c_set_clientdata(client, tas);
|
|
|
|
mutex_init(&tas->io_lock);
|
|
cfg.lock = tas675x_regmap_lock;
|
|
cfg.unlock = tas675x_regmap_unlock;
|
|
cfg.lock_arg = tas;
|
|
|
|
memcpy(tas->dsp_params, tas675x_dsp_defaults, sizeof(tas->dsp_params));
|
|
INIT_DELAYED_WORK(&tas->fault_check_work, tas675x_fault_check_work);
|
|
|
|
tas->regmap = devm_regmap_init_i2c(client, &cfg);
|
|
if (IS_ERR(tas->regmap))
|
|
return PTR_ERR(tas->regmap);
|
|
|
|
/* Keep regmap cache-only until hardware is powered on */
|
|
regcache_cache_only(tas->regmap, true);
|
|
|
|
tas->dev_type = (enum tas675x_type)(unsigned long)device_get_match_data(tas->dev);
|
|
tas->fast_boot = device_property_read_bool(tas->dev, "ti,fast-boot");
|
|
|
|
tas->audio_slot = -1;
|
|
tas->llp_slot = -1;
|
|
tas->vpredict_slot = -1;
|
|
tas->isense_slot = -1;
|
|
if (!device_property_read_u32(tas->dev, "ti,audio-slot-no", &val))
|
|
tas->audio_slot = val;
|
|
if (!device_property_read_u32(tas->dev, "ti,llp-slot-no", &val))
|
|
tas->llp_slot = val;
|
|
if (!device_property_read_u32(tas->dev, "ti,vpredict-slot-no", &val))
|
|
tas->vpredict_slot = val;
|
|
if (!device_property_read_u32(tas->dev, "ti,isense-slot-no", &val))
|
|
tas->isense_slot = val;
|
|
|
|
tas->gpio1_func = tas675x_gpio_func_parse(tas->dev, "ti,gpio1-function");
|
|
tas->gpio2_func = tas675x_gpio_func_parse(tas->dev, "ti,gpio2-function");
|
|
|
|
for (i = 0; i < ARRAY_SIZE(tas675x_supply_names); i++)
|
|
tas->supplies[i].supply = tas675x_supply_names[i];
|
|
|
|
ret = devm_regulator_bulk_get(tas->dev, ARRAY_SIZE(tas->supplies), tas->supplies);
|
|
if (ret)
|
|
return dev_err_probe(tas->dev, ret, "Failed to request supplies\n");
|
|
|
|
tas->vbat = devm_regulator_get_optional(tas->dev, "vbat");
|
|
if (IS_ERR(tas->vbat) && PTR_ERR(tas->vbat) != -ENODEV)
|
|
return dev_err_probe(tas->dev, PTR_ERR(tas->vbat),
|
|
"Failed to get vbat supply\n");
|
|
|
|
tas->pd_gpio = devm_gpiod_get_optional(tas->dev, "powerdown", GPIOD_OUT_HIGH);
|
|
if (IS_ERR(tas->pd_gpio))
|
|
return dev_err_probe(tas->dev, PTR_ERR(tas->pd_gpio), "Failed powerdown-gpios\n");
|
|
|
|
tas->stby_gpio = devm_gpiod_get_optional(tas->dev, "standby", GPIOD_OUT_HIGH);
|
|
if (IS_ERR(tas->stby_gpio))
|
|
return dev_err_probe(tas->dev, PTR_ERR(tas->stby_gpio), "Failed standby-gpios\n");
|
|
|
|
if (!tas->pd_gpio && !tas->stby_gpio)
|
|
return dev_err_probe(tas->dev, -EINVAL,
|
|
"At least one of powerdown-gpios or standby-gpios is required\n");
|
|
|
|
ret = tas675x_power_on(tas);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (client->irq) {
|
|
ret = devm_request_threaded_irq(tas->dev, client->irq, NULL,
|
|
tas675x_irq_handler,
|
|
IRQF_ONESHOT | IRQF_TRIGGER_FALLING,
|
|
"tas675x-fault", tas);
|
|
if (ret) {
|
|
tas675x_power_off(tas);
|
|
return dev_err_probe(tas->dev, ret, "Failed to request IRQ\n");
|
|
}
|
|
} else {
|
|
/* Schedule delayed work for fault checking at probe and runtime resume */
|
|
schedule_delayed_work(&tas->fault_check_work,
|
|
msecs_to_jiffies(TAS675X_FAULT_CHECK_INTERVAL_MS));
|
|
}
|
|
|
|
/* Enable runtime PM with 2s autosuspend */
|
|
pm_runtime_set_autosuspend_delay(tas->dev, 2000);
|
|
pm_runtime_use_autosuspend(tas->dev);
|
|
pm_runtime_set_active(tas->dev);
|
|
pm_runtime_mark_last_busy(tas->dev);
|
|
pm_runtime_enable(tas->dev);
|
|
|
|
ret = devm_snd_soc_register_component(tas->dev, &soc_codec_dev_tas675x,
|
|
tas675x_dais, ARRAY_SIZE(tas675x_dais));
|
|
if (ret)
|
|
goto err_pm_disable;
|
|
|
|
return 0;
|
|
|
|
err_pm_disable:
|
|
pm_runtime_force_suspend(tas->dev);
|
|
pm_runtime_disable(tas->dev);
|
|
tas675x_power_off(tas);
|
|
return ret;
|
|
}
|
|
|
|
static void tas675x_i2c_remove(struct i2c_client *client)
|
|
{
|
|
struct tas675x_priv *tas = dev_get_drvdata(&client->dev);
|
|
|
|
disable_delayed_work_sync(&tas->fault_check_work);
|
|
if (client->irq)
|
|
disable_irq(client->irq);
|
|
|
|
pm_runtime_force_suspend(&client->dev);
|
|
pm_runtime_disable(&client->dev);
|
|
tas675x_power_off(tas);
|
|
}
|
|
|
|
static const struct dev_pm_ops tas675x_pm_ops = {
|
|
SYSTEM_SLEEP_PM_OPS(tas675x_system_suspend, tas675x_system_resume)
|
|
RUNTIME_PM_OPS(tas675x_runtime_suspend, tas675x_runtime_resume, NULL)
|
|
};
|
|
|
|
static const struct of_device_id tas675x_of_match[] = {
|
|
{ .compatible = "ti,tas67524", .data = (void *)TAS67524 },
|
|
{ }
|
|
};
|
|
MODULE_DEVICE_TABLE(of, tas675x_of_match);
|
|
|
|
static const struct i2c_device_id tas675x_i2c_id[] = {
|
|
{ "tas67524", TAS67524 },
|
|
{ }
|
|
};
|
|
MODULE_DEVICE_TABLE(i2c, tas675x_i2c_id);
|
|
|
|
static struct i2c_driver tas675x_i2c_driver = {
|
|
.driver = {
|
|
.name = "tas675x",
|
|
.of_match_table = tas675x_of_match,
|
|
.pm = pm_ptr(&tas675x_pm_ops),
|
|
},
|
|
.probe = tas675x_i2c_probe,
|
|
.remove = tas675x_i2c_remove,
|
|
.id_table = tas675x_i2c_id,
|
|
};
|
|
|
|
module_i2c_driver(tas675x_i2c_driver);
|
|
|
|
MODULE_AUTHOR("Sen Wang <sen@ti.com>");
|
|
MODULE_DESCRIPTION("ASoC TAS675x Audio Amplifier Driver");
|
|
MODULE_LICENSE("GPL");
|