Re: [PATCH 4/6] thermal: qcom: tsens: add hardware-bin trip-point filtering
From: Gaurav Kohli
Date: Fri Oct 23 2026 - 06:06:00 EST
On 10/22/2026 2:39 PM, Konrad Dybcio wrote:
On 10/2/26 12:46 PM, Gaurav Kohli wrote:
From: Amit Kucheria <amit.kucheria@xxxxxxxxxxxxxxxx>
Some Qualcomm SoCs are binned at manufacturing time and expose the bin
in qfprom fuses. The thermal core's supported_hw_bin mechanism filters
trip points by hardware version; wire tsens up to feed it.
[...]
+ /* Hardware-bin values for thermal-hw-bin matching in DT. */
+ const u32 hw_bin_std = 0x1;
+ const u32 hw_bin_elevated = 0x2;
+
+ /* Higher thermal profile identification for qcm6490/sc7280 bins. */
+ const u32 tsens_chip_id0 = 0x197;
+ const u32 tsens_chip_id1 = 0x198;
+ const u32 tsens_chip_id2 = 0x20e;
+ const u32 tsens_chip_id3 = 0x20f;
+ const u8 tsens_feat_id2 = 0x2;
+ const u8 tsens_feat_id3 = 0x3;
+ const u8 tsens_feat_id4 = 0x4;
Isn't that (also) exposed over SMEM?
Thanks Konrad, for review.
SMEM has related socinfo data, but there is no exported helper for the
exact fields needed here, such as feat-id, which is also used as the bin
value.
This jtag-id/feat-id decoding is Kodiak/QCM6490-specific, and the
thermal-bin mapping needs both values. Other/future targets are expected
to use only the generic i-temp fuse bit, like Hamoa.
Konrad
+
+ if (nfuse < 2)
+ return -EINVAL;
+
+ tsens_jtag = fuse[0] & GENMASK(19, 0);
+ tsens_feat_id = fuse[1] & GENMASK(7, 0);
+
+ elevate_trip =
+ (tsens_jtag == tsens_chip_id0 && tsens_feat_id == tsens_feat_id3) ||
+ (tsens_jtag == tsens_chip_id1 && tsens_feat_id == tsens_feat_id4) ||
+ (tsens_jtag == tsens_chip_id2 && tsens_feat_id == tsens_feat_id3) ||
+ (tsens_jtag == tsens_chip_id3 && tsens_feat_id == tsens_feat_id2);
+
+ hw[0] = elevate_trip ? hw_bin_elevated : hw_bin_std;
+ *count = 1;
+
+ dev_dbg(dev,
+ "qcm6490 hw-bin from jtag-id=0x%x feat-id=0x%x => 0x%x\n",
+ tsens_jtag, tsens_feat_id, hw[0]);
+
+ return 0;
+}
+
+static int tsens_compute_hw_version_i_temp(struct device *dev,
+ const u32 *fuse, unsigned int nfuse,
+ u32 *hw, unsigned int *count)
+{
+ if (nfuse < 1)
+ return -EINVAL;
+
+ /* Single bit: set bit 1 for high-temp bin, bit 0 for standard bin */
+ hw[0] = (fuse[0] & BIT(0)) ? 0x2u : 0x1u;
+ *count = 1;
+
+ dev_dbg(dev, "i-temp hw-bin from i-temp=0x%x => 0x%x\n",
+ fuse[0], hw[0]);
+
+ return 0;
+}
+
+static const char * const tsens_cells_qcm6490[] = { "jtag-id", "feat-id" };
+static const char * const tsens_cells_i_temp[] = { "i-temp" };
+
+const struct tsens_hw_bin_desc tsens_hw_bin_desc_qcm6490 = {
+ .cell_names = tsens_cells_qcm6490,
+ .ncells = ARRAY_SIZE(tsens_cells_qcm6490),
+ .compute_hw_version = tsens_compute_hw_version_qcm6490,
+};
+
+const struct tsens_hw_bin_desc tsens_hw_bin_desc_i_temp = {
+ .cell_names = tsens_cells_i_temp,
+ .ncells = ARRAY_SIZE(tsens_cells_i_temp),
+ .compute_hw_version = tsens_compute_hw_version_i_temp,
+};
+
+/**
+ * tsens_populate_hw_bin_info - read fuse cells and populate priv->hw_bin_info
+ * @priv: tsens private data
+ * @data: platform data containing the optional hw_bin_desc pointer
+ *
+ * If data->hw_bin_desc is NULL, this is a no-op and returns 0. Otherwise,
+ * reads the nvmem cells named in hw_bin_desc->cell_names, calls the
+ * compute_hw_version() callback, and stores the result in priv->hw_bin_info
+ * for later use by tsens_register().
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int tsens_populate_hw_bin_info(struct tsens_priv *priv,
+ const struct tsens_plat_data *data)
+{
+ const struct tsens_hw_bin_desc *desc;
+ u32 fuse_vals[TSENS_HW_BIN_MAX_CELLS];
+ u32 hw_versions[THERMAL_HW_BIN_MAX_LEVELS] = { 0 };
+ unsigned int hw_count = 0;
+ int i, ret;
+
+ if (!data->hw_bin_desc)
+ return 0;
+
+ desc = data->hw_bin_desc;
+
+ if (!desc->ncells || desc->ncells > TSENS_HW_BIN_MAX_CELLS) {
+ dev_err(priv->dev, "hw_bin_desc ncells %u out of range\n",
+ desc->ncells);
+ return -EINVAL;
+ }
+
+ for (i = 0; i < (int)desc->ncells; i++) {
+ ret = nvmem_cell_read_variable_le_u32(priv->dev,
+ desc->cell_names[i],
+ &fuse_vals[i]);
+ if (ret < 0) {
+ dev_err(priv->dev,
+ "failed to read nvmem cell '%s': %d\n",
+ desc->cell_names[i], ret);
+ return ret;
+ }
+ }
+
+ ret = desc->compute_hw_version(priv->dev, fuse_vals, desc->ncells,
+ hw_versions, &hw_count);
+ if (ret) {
+ dev_err(priv->dev, "compute_hw_version failed: %d\n", ret);
+ return ret;
+ }
+
+ if (!hw_count || hw_count > THERMAL_HW_BIN_MAX_LEVELS) {
+ dev_err(priv->dev,
+ "compute_hw_version returned invalid hw_count %u\n",
+ hw_count);
+ return -EINVAL;
+ }
+
+ /*
+ * Store a copy of the hw-version array in priv->hw_bin_info. We can't
+ * call thermal_zone_set_supported_hw_bin() here because the thermal zones
+ * haven't been registered yet; tsens_register() will pass &priv->hw_bin_info
+ * directly to devm_thermal_of_zone_register_with_bin() instead.
+ *
+ * Use devm_kmemdup so the copy is freed when the device is unbound.
+ */
+ priv->hw_bin_info.supported_hw_bin = devm_kmemdup(priv->dev, hw_versions,
+ hw_count * sizeof(u32),
+ GFP_KERNEL);
+ if (!priv->hw_bin_info.supported_hw_bin)
+ return -ENOMEM;
+
+ priv->hw_bin_info.supported_hw_bin_count = hw_count;
+
+ return 0;
+}
+
int tsens_read_calibration(struct tsens_priv *priv, int shift, u32 *p1, u32 *p2, bool backup)
{
u32 mode;
@@ -1206,6 +1368,12 @@ static const struct of_device_id tsens_table[] = {
}, {
.compatible = "qcom,msm8996-tsens",
.data = &data_8996,
+ }, {
+ .compatible = "qcom,qcm6490-tsens",
+ .data = &data_sc7280,
+ }, {
+ .compatible = "qcom,sc7280-tsens",
+ .data = &data_sc7280,
}, {
.compatible = "qcom,tsens-v1",
.data = &data_tsens_v1,
@@ -1218,6 +1386,9 @@ static const struct of_device_id tsens_table[] = {
}, {
.compatible = "qcom,sa8255p-tsens",
.data = &data_automotive_v2,
+ }, {
+ .compatible = "qcom,x1e80100-tsens",
+ .data = &data_x1e80100,
},
{}
};
@@ -1327,12 +1498,18 @@ static int tsens_register(struct tsens_priv *priv)
{
int i, ret;
struct thermal_zone_device *tzd;
+ const struct thermal_hw_bin_info *bin_info;
+
+ bin_info = priv->hw_bin_info.supported_hw_bin_count ?
+ &priv->hw_bin_info : NULL;
for (i = 0; i < priv->num_sensors; i++) {
priv->sensor[i].priv = priv;
- tzd = devm_thermal_of_zone_register(priv->dev, priv->sensor[i].hw_id,
- &priv->sensor[i],
- &tsens_of_ops);
+ tzd = devm_thermal_of_zone_register_with_bin(priv->dev,
+ priv->sensor[i].hw_id,
+ &priv->sensor[i],
+ &tsens_of_ops,
+ bin_info);
if (IS_ERR(tzd))
continue;
priv->sensor[i].tzd = tzd;
@@ -1425,6 +1602,12 @@ static int tsens_probe(struct platform_device *pdev)
priv->feat = data->feat;
priv->fields = data->fields;
+ ret = tsens_populate_hw_bin_info(priv, data);
+ if (ret)
+ dev_warn(&pdev->dev,
+ "thermal-bin failed to read FUSE data (%d): trips with temperature-bin will use first entry\n",
+ ret);
+
platform_set_drvdata(pdev, priv);
device_init_wakeup(dev, !data->no_irq_wake);
diff --git a/drivers/thermal/qcom/tsens.h b/drivers/thermal/qcom/tsens.h
index e8376accdff3..496ae6572bb8 100644
--- a/drivers/thermal/qcom/tsens.h
+++ b/drivers/thermal/qcom/tsens.h
@@ -40,6 +40,29 @@ enum tsens_ver {
VER_2_X_NO_RPM,
};
+/**
+ * struct tsens_hw_bin_desc - per-SoC descriptor for hardware-bin fuse reading
+ * @cell_names: NULL-terminated array of nvmem cell names to read
+ * @ncells: number of entries in @cell_names
+ * @compute_hw_version: callback that converts raw fuse values to hw-version
+ * bitfields suitable for thermal_zone_set_supported_hw_bin()
+ *
+ * Embed a pointer to this struct in tsens_plat_data.hw_bin_desc to enable
+ * hardware-bin trip filtering for a given SoC. Set to NULL to disable.
+ */
+struct tsens_hw_bin_desc {
+ const char * const *cell_names;
+ unsigned int ncells;
+ int (*compute_hw_version)(struct device *dev,
+ const u32 *fuse, unsigned int nfuse,
+ u32 *hw, unsigned int *count);
+};
+
+#define TSENS_HW_BIN_MAX_CELLS 8
+
+extern const struct tsens_hw_bin_desc tsens_hw_bin_desc_qcm6490;
+extern const struct tsens_hw_bin_desc tsens_hw_bin_desc_i_temp;
+
enum tsens_irq_type {
LOWER,
UPPER,
@@ -533,6 +556,7 @@ struct tsens_features {
* @feat: features of the IP
* @fields: bitfield locations
* @no_irq_wake: if set, TSENS interrupts will not be configured as wakeup sources
+ * @hw_bin_desc: optional hardware-bin fuse descriptor for trip filtering
*/
struct tsens_plat_data {
const u32 num_sensors;
@@ -541,6 +565,7 @@ struct tsens_plat_data {
struct tsens_features *feat;
const struct reg_field *fields;
bool no_irq_wake;
+ const struct tsens_hw_bin_desc *hw_bin_desc;
};
/**
@@ -568,6 +593,7 @@ struct tsens_context {
* @feat: features of the IP
* @fields: bitfield locations
* @ops: pointer to list of callbacks supported by this device
+ * @hw_bin_info: hardware binning info read from SoC fuse (populated at probe)
* @debug_root: pointer to debugfs dentry for all tsens
* @debug: pointer to debugfs dentry for tsens controller
* @uplow_irq: IRQ number for uplow (upper/lower) threshold interrupts
@@ -591,6 +617,8 @@ struct tsens_priv {
const struct reg_field *fields;
const struct tsens_ops *ops;
+ struct thermal_hw_bin_info hw_bin_info;
+
struct dentry *debug_root;
struct dentry *debug;
@@ -677,6 +705,7 @@ extern const struct tsens_plat_data data_ipq5018;
/* TSENS v2 targets */
extern struct tsens_plat_data data_8996, data_ipq8074, data_tsens_v2;
extern const struct tsens_plat_data data_ipq5332, data_ipq5424;
+extern const struct tsens_plat_data data_x1e80100, data_sc7280;
/* TSENS automotive targets */
extern struct tsens_plat_data data_automotive_v2;