[PATCH v3 2/4] thermal: renesas: rzg3s: Add thermal driver for the Renesas RZ/G3S SoC

Niklas Söderlund niklas.soderlund at ragnatech.se
Sat Jul 5 05:06:17 PDT 2025


Hi Claudiu,

Thanks for your work. 

Sorry for late review, Geert only alerted me to the series a few days 
ago.

On 2025-03-24 15:56:59 +0200, Claudiu wrote:
> From: Claudiu Beznea <claudiu.beznea.uj at bp.renesas.com>
> 
> The Renesas RZ/G3S SoC features a Thermal Sensor Unit (TSU) that reports
> the junction temperature. The temperature is reported through a dedicated
> ADC channel. Add a driver for the Renesas RZ/G3S TSU.
> 
> Signed-off-by: Claudiu Beznea <claudiu.beznea.uj at bp.renesas.com>
> ---
> 
> Changes in v3:
> - drop the runtime resume/suspend from rzg3s_thermal_get_temp(); this
>   is not needed as the temperature is read with ADC
> - opened the devres group id in rzg3s_thermal_probe() and rename
>   previsouly rzg3s_thermal_probe() to rzg3s_thermal_probe_helper(), to
>   have simpler code; this approach was suggested by Jonathan in [1];
>   as there is no positive feedback for the generic solution [2] this
>   looks currently the best approach
> 
> [1] https://lore.kernel.org/all/20250224120608.1769039-2-claudiu.beznea.uj@bp.renesas.com
> [2] https://lore.kernel.org/all/20250215130849.227812-1-claudiu.beznea.uj@bp.renesas.com
> 
> 
> Changes in v2:
> - use a devres group for the devm resources obtained though this
>   driver to avoid issue described in [1]; with this dropped the
>   following calls:
> -- thermal_add_hwmon_sysfs(priv->tz);
> -- thermal_of_zone_register(priv->tz);
> -- pm_runtime_enable(priv->dev);
>   and use devm variants
> - used signed variables for temperature computation
> - convert to mili degree Celsius before applying the computation formula
>   to avoid losing precision
> 
> [1] https://lore.kernel.org/all/20250215130849.227812-1-claudiu.beznea.uj@bp.renesas.com/
> 
>  MAINTAINERS                             |   7 +
>  drivers/thermal/renesas/Kconfig         |   8 +
>  drivers/thermal/renesas/Makefile        |   1 +
>  drivers/thermal/renesas/rzg3s_thermal.c | 313 ++++++++++++++++++++++++
>  4 files changed, 329 insertions(+)
>  create mode 100644 drivers/thermal/renesas/rzg3s_thermal.c
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 7a9b8fa5f032..f3795fbcdcba 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -20594,6 +20594,13 @@ S:	Maintained
>  F:	Documentation/devicetree/bindings/iio/potentiometer/renesas,x9250.yaml
>  F:	drivers/iio/potentiometer/x9250.c
>  
> +RENESAS RZ/G3S THERMAL SENSOR UNIT DRIVER
> +M:	Claudiu Beznea <claudiu.beznea.uj at bp.renesas.com>
> +L:	linux-pm at vger.kernel.org
> +S:	Maintained
> +F:	Documentation/devicetree/bindings/thermal/renesas,r9a08g045-tsu.yaml
> +F:	drivers/thermal/renesas/rzg3s_thermal.c
> +
>  RESET CONTROLLER FRAMEWORK
>  M:	Philipp Zabel <p.zabel at pengutronix.de>
>  S:	Maintained
> diff --git a/drivers/thermal/renesas/Kconfig b/drivers/thermal/renesas/Kconfig
> index dcf5fc5ae08e..566478797095 100644
> --- a/drivers/thermal/renesas/Kconfig
> +++ b/drivers/thermal/renesas/Kconfig
> @@ -26,3 +26,11 @@ config RZG2L_THERMAL
>  	help
>  	  Enable this to plug the RZ/G2L thermal sensor driver into the Linux
>  	  thermal framework.
> +
> +config RZG3S_THERMAL
> +	tristate "Renesas RZ/G3S thermal driver"
> +	depends on ARCH_R9A08G045 || COMPILE_TEST
> +	depends on OF && IIO && RZG2L_ADC
> +	help
> +	  Enable this to plug the RZ/G3S thermal sensor driver into the Linux
> +	  thermal framework.
> diff --git a/drivers/thermal/renesas/Makefile b/drivers/thermal/renesas/Makefile
> index bf9cb3cb94d6..1feb5ab78827 100644
> --- a/drivers/thermal/renesas/Makefile
> +++ b/drivers/thermal/renesas/Makefile
> @@ -3,3 +3,4 @@
>  obj-$(CONFIG_RCAR_GEN3_THERMAL)	+= rcar_gen3_thermal.o
>  obj-$(CONFIG_RCAR_THERMAL)	+= rcar_thermal.o
>  obj-$(CONFIG_RZG2L_THERMAL)	+= rzg2l_thermal.o
> +obj-$(CONFIG_RZG3S_THERMAL)	+= rzg3s_thermal.o
> diff --git a/drivers/thermal/renesas/rzg3s_thermal.c b/drivers/thermal/renesas/rzg3s_thermal.c
> new file mode 100644
> index 000000000000..e0bc51943875
> --- /dev/null
> +++ b/drivers/thermal/renesas/rzg3s_thermal.c
> @@ -0,0 +1,313 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Renesas RZ/G3S TSU Thermal Sensor Driver
> + *
> + * Copyright (C) 2024 Renesas Electronics Corporation
> + */
> +
> +#include <linux/bitfield.h>
> +#include <linux/delay.h>
> +#include <linux/iio/consumer.h>
> +#include <linux/io.h>
> +#include <linux/module.h>
> +#include <linux/platform_device.h>
> +#include <linux/pm_runtime.h>
> +#include <linux/reset.h>
> +#include <linux/thermal.h>
> +#include <linux/units.h>
> +
> +#include "../thermal_hwmon.h"
> +
> +#define TSU_SM			0x0
> +#define TSU_SM_EN		BIT(0)
> +#define TSU_SM_OE		BIT(1)
> +#define OTPTSUTRIM_REG(n)	(0x18 + (n) * 0x4)
> +#define OTPTSUTRIM_EN_MASK	BIT(31)
> +#define OTPTSUTRIM_MASK		GENMASK(11, 0)
> +
> +#define TSU_READ_STEPS		8
> +
> +/* Default calibration values, if FUSE values are missing. */
> +#define SW_CALIB0_VAL		1297
> +#define SW_CALIB1_VAL		751
> +
> +#define MCELSIUS(temp)		((temp) * MILLIDEGREE_PER_DEGREE)
> +
> +/**
> + * struct rzg3s_thermal_priv - RZ/G3S thermal private data structure
> + * @base: TSU base address
> + * @dev: device pointer
> + * @tz: thermal zone pointer
> + * @rstc: reset control
> + * @channel: IIO channel to read the TSU
> + * @devres_group_id: devres group for the driver devres resources
> + *		      obtained in probe
> + * @mode: current device mode
> + * @calib0: calibration value
> + * @calib1: calibration value
> + */
> +struct rzg3s_thermal_priv {
> +	void __iomem *base;
> +	struct device *dev;
> +	struct thermal_zone_device *tz;
> +	struct reset_control *rstc;
> +	struct iio_channel *channel;
> +	void *devres_group_id;
> +	enum thermal_device_mode mode;
> +	u16 calib0;
> +	u16 calib1;
> +};
> +
> +static int rzg3s_thermal_get_temp(struct thermal_zone_device *tz, int *temp)
> +{
> +	struct rzg3s_thermal_priv *priv = thermal_zone_device_priv(tz);
> +	int ts_code_ave = 0;
> +	int ret, val;
> +
> +	if (priv->mode != THERMAL_DEVICE_ENABLED)
> +		return -EAGAIN;
> +
> +	for (u8 i = 0; i < TSU_READ_STEPS; i++) {
> +		ret = iio_read_channel_raw(priv->channel, &val);
> +		if (ret < 0)
> +			return ret;
> +
> +		ts_code_ave += val;
> +		/*
> +		 * According to the HW manual (section 40.4.4 Procedure for Measuring the
> +		 * Temperature) we need to wait here at leat 3us.
> +		 */
> +		usleep_range(5, 10);
> +	}
> +
> +	ret = 0;
> +	ts_code_ave = DIV_ROUND_CLOSEST(MCELSIUS(ts_code_ave), TSU_READ_STEPS);
> +
> +	/*
> +	 * According to the HW manual (section 40.4.4 Procedure for Measuring the Temperature)
> +	 * the computation formula is as follows:
> +	 *
> +	 * Tj = (ts_code_ave - priv->calib1) * 165 / (priv->calib0 - priv->calib1) - 40
> +	 *
> +	 * Convert everything to mili Celsius before applying the formula to avoid
> +	 * losing precision.
> +	 */
> +
> +	*temp = DIV_ROUND_CLOSEST((s64)(ts_code_ave - MCELSIUS(priv->calib1)) * MCELSIUS(165),
> +				  MCELSIUS(priv->calib0 - priv->calib1)) - MCELSIUS(40);

The issue Geert points out, can that not be solved by holding off 
converting to MCELSIUS() to after you have done the calculation?

Other then this issue I think this change is very clear and ready.

> +
> +	/* Report it in mili degrees Celsius and round it up to 0.5 degrees Celsius. */
> +	*temp = roundup(*temp, 500);
> +
> +	return ret;
> +}
> +
> +static void rzg3s_thermal_set_mode(struct rzg3s_thermal_priv *priv,
> +				   enum thermal_device_mode mode)
> +{
> +	struct device *dev = priv->dev;
> +	int ret;
> +
> +	ret = pm_runtime_resume_and_get(dev);
> +	if (ret)
> +		return;
> +
> +	if (mode == THERMAL_DEVICE_DISABLED) {
> +		writel(0, priv->base + TSU_SM);
> +	} else {
> +		writel(TSU_SM_EN, priv->base + TSU_SM);
> +		/*
> +		 * According to the HW manual (section 40.4.1 Procedure for
> +		 * Starting the TSU) we need to wait here 30us or more.
> +		 */
> +		usleep_range(30, 40);
> +
> +		writel(TSU_SM_OE | TSU_SM_EN, priv->base + TSU_SM);
> +		/*
> +		 * According to the HW manual (section 40.4.1 Procedure for
> +		 * Starting the TSU) we need to wait here 50us or more.
> +		 */
> +		usleep_range(50, 60);
> +	}
> +
> +	pm_runtime_mark_last_busy(dev);
> +	pm_runtime_put_autosuspend(dev);
> +}
> +
> +static int rzg3s_thermal_change_mode(struct thermal_zone_device *tz,
> +				     enum thermal_device_mode mode)
> +{
> +	struct rzg3s_thermal_priv *priv = thermal_zone_device_priv(tz);
> +
> +	if (priv->mode == mode)
> +		return 0;
> +
> +	rzg3s_thermal_set_mode(priv, mode);
> +	priv->mode = mode;
> +
> +	return 0;
> +}
> +
> +static const struct thermal_zone_device_ops rzg3s_tz_of_ops = {
> +	.get_temp = rzg3s_thermal_get_temp,
> +	.change_mode = rzg3s_thermal_change_mode,
> +};
> +
> +static int rzg3s_thermal_read_calib(struct rzg3s_thermal_priv *priv)
> +{
> +	struct device *dev = priv->dev;
> +	u32 val;
> +	int ret;
> +
> +	ret = pm_runtime_resume_and_get(dev);
> +	if (ret)
> +		return ret;
> +
> +	val = readl(priv->base + OTPTSUTRIM_REG(0));
> +	if (val & OTPTSUTRIM_EN_MASK)
> +		priv->calib0 = FIELD_GET(OTPTSUTRIM_MASK, val);
> +	else
> +		priv->calib0 = SW_CALIB0_VAL;
> +
> +	val = readl(priv->base + OTPTSUTRIM_REG(1));
> +	if (val & OTPTSUTRIM_EN_MASK)
> +		priv->calib1 = FIELD_GET(OTPTSUTRIM_MASK, val);
> +	else
> +		priv->calib1 = SW_CALIB1_VAL;
> +
> +	pm_runtime_mark_last_busy(dev);
> +	pm_runtime_put_autosuspend(dev);
> +
> +	return 0;
> +}
> +
> +static int rzg3s_thermal_probe_helper(struct platform_device *pdev, void *devres_group_id)
> +{
> +	struct rzg3s_thermal_priv *priv;
> +	struct device *dev = &pdev->dev;
> +	int ret;
> +
> +	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> +	if (!priv)
> +		return -ENOMEM;
> +
> +	priv->devres_group_id = devres_group_id;
> +
> +	priv->base = devm_platform_ioremap_resource(pdev, 0);
> +	if (IS_ERR(priv->base))
> +		return PTR_ERR(priv->base);
> +
> +	priv->channel = devm_iio_channel_get(dev, "tsu");
> +	if (IS_ERR(priv->channel))
> +		return dev_err_probe(dev, PTR_ERR(priv->channel), "Failed to get IIO channel!\n");
> +
> +	priv->rstc = devm_reset_control_get_exclusive_deasserted(dev, NULL);
> +	if (IS_ERR(priv->rstc))
> +		return dev_err_probe(dev, PTR_ERR(priv->rstc), "Failed to get reset!\n");
> +
> +	priv->dev = dev;
> +	priv->mode = THERMAL_DEVICE_DISABLED;
> +	platform_set_drvdata(pdev, priv);
> +
> +	pm_runtime_set_autosuspend_delay(dev, 300);
> +	pm_runtime_use_autosuspend(dev);
> +	ret = devm_pm_runtime_enable(dev);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "Failed to enable runtime PM!\n");
> +
> +	ret = rzg3s_thermal_read_calib(priv);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "Failed to read calibration data!\n");
> +
> +	priv->tz = devm_thermal_of_zone_register(dev, 0, priv, &rzg3s_tz_of_ops);
> +	if (IS_ERR(priv->tz))
> +		return dev_err_probe(dev, PTR_ERR(priv->tz), "Failed to register thermal zone!\n");
> +
> +	ret = devm_thermal_add_hwmon_sysfs(dev, priv->tz);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "Failed to add hwmon sysfs!\n");
> +
> +	return 0;
> +}
> +
> +static int rzg3s_thermal_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	void *devres_group_id;
> +	int ret;
> +
> +	/*
> +	 * Open a devres group to allow using devm_pm_runtime_enable()
> +	 * w/o interfeering with dev_pm_genpd_detach() in the platform bus
> +	 * remove. Otherwise, durring repeated unbind/bind operations,
> +	 * the TSU may be runtime resumed when it is not part of its power
> +	 * domain, leading to accessing TSU registers (through
> +	 * rzg3s_thermal_change_mode()) without its clocks being enabled
> +	 * and its PM domain being turned on.
> +	 */
> +	devres_group_id = devres_open_group(dev, NULL, GFP_KERNEL);
> +	if (!devres_group_id)
> +		return -ENOMEM;
> +
> +	ret = rzg3s_thermal_probe_helper(pdev, devres_group_id);
> +	if (ret)
> +		devres_release_group(dev, devres_group_id);
> +
> +	return ret;
> +}
> +
> +static void rzg3s_thermal_remove(struct platform_device *pdev)
> +{
> +	struct rzg3s_thermal_priv *priv = dev_get_drvdata(&pdev->dev);
> +
> +	devres_release_group(priv->dev, priv->devres_group_id);
> +}
> +
> +static int rzg3s_thermal_suspend(struct device *dev)
> +{
> +	struct rzg3s_thermal_priv *priv = dev_get_drvdata(dev);
> +
> +	rzg3s_thermal_set_mode(priv, THERMAL_DEVICE_DISABLED);
> +
> +	return reset_control_assert(priv->rstc);
> +}
> +
> +static int rzg3s_thermal_resume(struct device *dev)
> +{
> +	struct rzg3s_thermal_priv *priv = dev_get_drvdata(dev);
> +	int ret;
> +
> +	ret = reset_control_deassert(priv->rstc);
> +	if (ret)
> +		return ret;
> +
> +	if (priv->mode != THERMAL_DEVICE_DISABLED)
> +		rzg3s_thermal_set_mode(priv, priv->mode);
> +
> +	return 0;
> +}
> +
> +static const struct dev_pm_ops rzg3s_thermal_pm_ops = {
> +	SYSTEM_SLEEP_PM_OPS(rzg3s_thermal_suspend, rzg3s_thermal_resume)
> +};
> +
> +static const struct of_device_id rzg3s_thermal_dt_ids[] = {
> +	{ .compatible = "renesas,r9a08g045-tsu" },
> +	{ /* sentinel */ }
> +};
> +MODULE_DEVICE_TABLE(of, rzg3s_thermal_dt_ids);
> +
> +static struct platform_driver rzg3s_thermal_driver = {
> +	.driver = {
> +		.name = "rzg3s_thermal",
> +		.of_match_table = rzg3s_thermal_dt_ids,
> +		.pm = pm_ptr(&rzg3s_thermal_pm_ops),
> +	},
> +	.probe = rzg3s_thermal_probe,
> +	.remove = rzg3s_thermal_remove,
> +};
> +module_platform_driver(rzg3s_thermal_driver);
> +
> +MODULE_DESCRIPTION("Renesas RZ/G3S Thermal Sensor Unit Driver");
> +MODULE_AUTHOR("Claudiu Beznea <claudiu.beznea.uj at bp.renesas.com>");
> +MODULE_LICENSE("GPL");
> -- 
> 2.43.0
> 
> 

-- 
Kind Regards,
Niklas Söderlund



More information about the linux-arm-kernel mailing list