ASoC: codecs: wsa884x: Implement temperature reading and hwmon
authorKrzysztof Kozlowski <krzysztof.kozlowski@linaro.org>
Fri, 9 Aug 2024 11:01:22 +0000 (13:01 +0200)
committerMark Brown <broonie@kernel.org>
Thu, 29 Aug 2024 16:04:46 +0000 (17:04 +0100)
Read temperature of the speaker and expose it via hwmon interface, which
will be later used during calibration of speaker protection algorithms.

Signed-off-by: Krzysztof Kozlowski <krzysztof.kozlowski@linaro.org>
Link: https://patch.msgid.link/20240809110122.137761-1-krzysztof.kozlowski@linaro.org
Signed-off-by: Mark Brown <broonie@kernel.org>
sound/soc/codecs/wsa884x.c

index 8db1380d1f107a28e47c96089c5634bcd312de6b..86df5152c547bc5abd005a37f0418b8bef76cf59 100644 (file)
@@ -5,11 +5,14 @@
  */
 
 #include <linux/bitfield.h>
+#include <linux/cleanup.h>
 #include <linux/device.h>
 #include <linux/gpio/consumer.h>
+#include <linux/hwmon.h>
 #include <linux/init.h>
 #include <linux/kernel.h>
 #include <linux/module.h>
+#include <linux/mutex.h>
 #include <linux/pm_runtime.h>
 #include <linux/regmap.h>
 #include <linux/regulator/consumer.h>
 #define WSA884X_PA_FSM_MSK1            (WSA884X_DIG_CTRL0_BASE + 0x3b)
 #define WSA884X_PA_FSM_BYP_CTL         (WSA884X_DIG_CTRL0_BASE + 0x3c)
 #define WSA884X_PA_FSM_BYP0            (WSA884X_DIG_CTRL0_BASE + 0x3d)
+#define WSA884X_PA_FSM_BYP0_DC_CAL_EN_MASK             0x01
+#define WSA884X_PA_FSM_BYP0_DC_CAL_EN_SHIFT            0
+#define WSA884X_PA_FSM_BYP0_CLK_WD_EN_MASK             0x02
+#define WSA884X_PA_FSM_BYP0_CLK_WD_EN_SHIFT            1
+#define WSA884X_PA_FSM_BYP0_BG_EN_MASK                 0x04
+#define WSA884X_PA_FSM_BYP0_BG_EN_SHIFT                        2
+#define WSA884X_PA_FSM_BYP0_BOOST_EN_MASK              0x08
+#define WSA884X_PA_FSM_BYP0_BOOST_EN_SHIFT             3
+#define WSA884X_PA_FSM_BYP0_PA_EN_MASK                 0x10
+#define WSA884X_PA_FSM_BYP0_PA_EN_SHIFT                        4
+#define WSA884X_PA_FSM_BYP0_D_UNMUTE_MASK              0x20
+#define WSA884X_PA_FSM_BYP0_D_UNMUTE_SHIFT             5
+#define WSA884X_PA_FSM_BYP0_SPKR_PROT_EN_MASK          0x40
+#define WSA884X_PA_FSM_BYP0_SPKR_PROT_EN_SHIFT         6
+#define WSA884X_PA_FSM_BYP0_TSADC_EN_MASK              0x80
+#define WSA884X_PA_FSM_BYP0_TSADC_EN_SHIFT             7
 #define WSA884X_PA_FSM_BYP1            (WSA884X_DIG_CTRL0_BASE + 0x3e)
 #define WSA884X_TADC_VALUE_CTL         (WSA884X_DIG_CTRL0_BASE + 0x50)
+#define WSA884X_TADC_VALUE_CTL_TEMP_VALUE_RD_EN_MASK   0x01
+#define WSA884X_TADC_VALUE_CTL_TEMP_VALUE_RD_EN_SHIFT  0
+#define WSA884X_TADC_VALUE_CTL_VBAT_VALUE_RD_EN_MASK   0x02
+#define WSA884X_TADC_VALUE_CTL_VBAT_VALUE_RD_EN_SHIFT  1
 #define WSA884X_TEMP_DETECT_CTL                (WSA884X_DIG_CTRL0_BASE + 0x51)
 #define WSA884X_TEMP_DIN_MSB           (WSA884X_DIG_CTRL0_BASE + 0x52)
 #define WSA884X_TEMP_DIN_LSB           (WSA884X_DIG_CTRL0_BASE + 0x53)
                SNDRV_PCM_FMTBIT_S24_LE |\
                SNDRV_PCM_FMTBIT_S24_3LE | SNDRV_PCM_FMTBIT_S32_LE)
 
+/* Two-point trimming for temperature calibration */
+#define WSA884X_T1_TEMP                        -10L
+#define WSA884X_T2_TEMP                        150L
+
+/*
+ * Device will report senseless data in many cases, so discard any measurements
+ * outside of valid range.
+ */
+#define WSA884X_LOW_TEMP_THRESHOLD     5
+#define WSA884X_HIGH_TEMP_THRESHOLD    45
+
 struct wsa884x_priv {
        struct regmap *regmap;
        struct device *dev;
@@ -706,6 +740,13 @@ struct wsa884x_priv {
        int active_ports;
        int dev_mode;
        bool hw_init;
+       /*
+        * Protects temperature reading code (related to speaker protection) and
+        * fields: temperature and pa_on.
+        */
+       struct mutex sp_lock;
+       unsigned int temperature;
+       bool pa_on;
 };
 
 enum {
@@ -1660,6 +1701,10 @@ static int wsa884x_spkr_event(struct snd_soc_dapm_widget *w,
 
        switch (event) {
        case SND_SOC_DAPM_POST_PMU:
+               mutex_lock(&wsa884x->sp_lock);
+               wsa884x->pa_on = true;
+               mutex_unlock(&wsa884x->sp_lock);
+
                wsa884x_spkr_post_pmu(component, wsa884x);
 
                snd_soc_component_write_field(component, WSA884X_PDM_WD_CTL,
@@ -1671,6 +1716,10 @@ static int wsa884x_spkr_event(struct snd_soc_dapm_widget *w,
                snd_soc_component_write_field(component, WSA884X_PDM_WD_CTL,
                                              WSA884X_PDM_WD_CTL_PDM_WD_EN_MASK,
                                              0x0);
+
+               mutex_lock(&wsa884x->sp_lock);
+               wsa884x->pa_on = false;
+               mutex_unlock(&wsa884x->sp_lock);
                break;
        }
 
@@ -1810,6 +1859,144 @@ static struct snd_soc_dai_driver wsa884x_dais[] = {
        },
 };
 
+static int wsa884x_get_temp(struct wsa884x_priv *wsa884x, long *temp)
+{
+       unsigned int d1_msb = 0, d1_lsb = 0, d2_msb = 0, d2_lsb = 0;
+       unsigned int dmeas_msb = 0, dmeas_lsb = 0;
+       int d1, d2, dmeas;
+       unsigned int mask;
+       long val;
+       int ret;
+
+       guard(mutex)(&wsa884x->sp_lock);
+
+       if (wsa884x->pa_on) {
+               /*
+                * Reading temperature is possible only when Power Amplifier is
+                * off. Report last cached data.
+                */
+               *temp = wsa884x->temperature;
+               return 0;
+       }
+
+       ret = pm_runtime_resume_and_get(wsa884x->dev);
+       if (ret < 0)
+               return ret;
+
+       mask = WSA884X_PA_FSM_BYP0_DC_CAL_EN_MASK |
+              WSA884X_PA_FSM_BYP0_CLK_WD_EN_MASK |
+              WSA884X_PA_FSM_BYP0_BG_EN_MASK |
+              WSA884X_PA_FSM_BYP0_D_UNMUTE_MASK |
+              WSA884X_PA_FSM_BYP0_SPKR_PROT_EN_MASK |
+              WSA884X_PA_FSM_BYP0_TSADC_EN_MASK;
+       /*
+        * Here and further do not care about read or update failures.
+        * For example, before turning on Power Amplifier for the first
+        * time, reading WSA884X_TEMP_DIN_MSB will always return 0.
+        * Instead, check if returned value is within reasonable
+        * thresholds.
+        */
+       regmap_update_bits(wsa884x->regmap, WSA884X_PA_FSM_BYP0, mask, mask);
+
+       regmap_update_bits(wsa884x->regmap, WSA884X_TADC_VALUE_CTL,
+                          WSA884X_TADC_VALUE_CTL_TEMP_VALUE_RD_EN_MASK,
+                          FIELD_PREP(WSA884X_TADC_VALUE_CTL_TEMP_VALUE_RD_EN_MASK, 0x0));
+
+       regmap_read(wsa884x->regmap, WSA884X_TEMP_DIN_MSB, &dmeas_msb);
+       regmap_read(wsa884x->regmap, WSA884X_TEMP_DIN_LSB, &dmeas_lsb);
+
+       regmap_update_bits(wsa884x->regmap, WSA884X_TADC_VALUE_CTL,
+                          WSA884X_TADC_VALUE_CTL_TEMP_VALUE_RD_EN_MASK,
+                          FIELD_PREP(WSA884X_TADC_VALUE_CTL_TEMP_VALUE_RD_EN_MASK, 0x1));
+
+       regmap_read(wsa884x->regmap, WSA884X_OTP_REG_1, &d1_msb);
+       regmap_read(wsa884x->regmap, WSA884X_OTP_REG_2, &d1_lsb);
+       regmap_read(wsa884x->regmap, WSA884X_OTP_REG_3, &d2_msb);
+       regmap_read(wsa884x->regmap, WSA884X_OTP_REG_4, &d2_lsb);
+
+       regmap_update_bits(wsa884x->regmap, WSA884X_PA_FSM_BYP0, mask, 0x0);
+
+       dmeas = (((dmeas_msb & 0xff) << 0x8) | (dmeas_lsb & 0xff)) >> 0x6;
+       d1 = (((d1_msb & 0xff) << 0x8) | (d1_lsb & 0xff)) >> 0x6;
+       d2 = (((d2_msb & 0xff) << 0x8) | (d2_lsb & 0xff)) >> 0x6;
+
+       if (d1 == d2) {
+               /* Incorrect data in OTP? */
+               ret = -EINVAL;
+               goto out;
+       }
+
+       val = WSA884X_T1_TEMP + (((dmeas - d1) * (WSA884X_T2_TEMP - WSA884X_T1_TEMP))/(d2 - d1));
+
+       dev_dbg(wsa884x->dev, "Measured temp %ld (dmeas=%d, d1=%d, d2=%d)\n",
+               val, dmeas, d1, d2);
+
+       if ((val > WSA884X_LOW_TEMP_THRESHOLD) &&
+           (val < WSA884X_HIGH_TEMP_THRESHOLD)) {
+               wsa884x->temperature = val;
+               *temp = val;
+               ret = 0;
+       } else {
+               ret = -EAGAIN;
+       }
+
+out:
+       pm_runtime_mark_last_busy(wsa884x->dev);
+       pm_runtime_put_autosuspend(wsa884x->dev);
+
+       return ret;
+}
+
+static umode_t wsa884x_hwmon_is_visible(const void *data,
+                                       enum hwmon_sensor_types type, u32 attr,
+                                       int channel)
+{
+       if (type != hwmon_temp)
+               return 0;
+
+       switch (attr) {
+       case hwmon_temp_input:
+               return 0444;
+       default:
+               break;
+       }
+
+       return 0;
+}
+
+static int wsa884x_hwmon_read(struct device *dev,
+                             enum hwmon_sensor_types type,
+                             u32 attr, int channel, long *temp)
+{
+       int ret;
+
+       switch (attr) {
+       case hwmon_temp_input:
+               ret = wsa884x_get_temp(dev_get_drvdata(dev), temp);
+               break;
+       default:
+               ret = -EOPNOTSUPP;
+               break;
+       }
+
+       return ret;
+}
+
+static const struct hwmon_channel_info *const wsa884x_hwmon_info[] = {
+       HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT),
+       NULL
+};
+
+static const struct hwmon_ops wsa884x_hwmon_ops = {
+       .is_visible     = wsa884x_hwmon_is_visible,
+       .read           = wsa884x_hwmon_read,
+};
+
+static const struct hwmon_chip_info wsa884x_hwmon_chip_info = {
+       .ops    = &wsa884x_hwmon_ops,
+       .info   = wsa884x_hwmon_info,
+};
+
 static void wsa884x_reset_powerdown(void *data)
 {
        struct wsa884x_priv *wsa884x = data;
@@ -1866,6 +2053,8 @@ static int wsa884x_probe(struct sdw_slave *pdev,
        if (!wsa884x)
                return -ENOMEM;
 
+       mutex_init(&wsa884x->sp_lock);
+
        for (i = 0; i < WSA884X_SUPPLIES_NUM; i++)
                wsa884x->supplies[i].supply = wsa884x_supply_name[i];
 
@@ -1923,6 +2112,18 @@ static int wsa884x_probe(struct sdw_slave *pdev,
        regcache_cache_only(wsa884x->regmap, true);
        wsa884x->hw_init = true;
 
+       if (IS_REACHABLE(CONFIG_HWMON)) {
+               struct device *hwmon;
+
+               hwmon = devm_hwmon_device_register_with_info(dev, "wsa884x",
+                                                            wsa884x,
+                                                            &wsa884x_hwmon_chip_info,
+                                                            NULL);
+               if (IS_ERR(hwmon))
+                       return dev_err_probe(dev, PTR_ERR(hwmon),
+                                            "Failed to register hwmon sensor\n");
+       }
+
        pm_runtime_set_autosuspend_delay(dev, 3000);
        pm_runtime_use_autosuspend(dev);
        pm_runtime_mark_last_busy(dev);