2018-08-20 21:42:35 +03:00
// SPDX-License-Identifier: GPL-2.0
2016-12-22 13:38:21 +03:00
/*
* R - Car Gen3 THS thermal sensor driver
* Based on rcar_thermal . c and work from Hien Dang and Khiem Nguyen .
*
* Copyright ( C ) 2016 Renesas Electronics Corporation .
* Copyright ( C ) 2016 Sang Engineering
*/
# include <linux/delay.h>
# include <linux/err.h>
# include <linux/interrupt.h>
# include <linux/io.h>
# include <linux/module.h>
# include <linux/of_device.h>
# include <linux/platform_device.h>
# include <linux/pm_runtime.h>
2017-10-17 14:36:13 +03:00
# include <linux/sys_soc.h>
2016-12-22 13:38:21 +03:00
# include <linux/thermal.h>
2017-03-29 21:43:54 +03:00
# include "thermal_core.h"
2019-02-11 22:56:20 +03:00
# include "thermal_hwmon.h"
2017-03-29 21:43:54 +03:00
2016-12-22 13:38:21 +03:00
/* Register offsets */
# define REG_GEN3_IRQSTR 0x04
# define REG_GEN3_IRQMSK 0x08
# define REG_GEN3_IRQCTL 0x0C
# define REG_GEN3_IRQEN 0x10
# define REG_GEN3_IRQTEMP1 0x14
# define REG_GEN3_IRQTEMP2 0x18
# define REG_GEN3_IRQTEMP3 0x1C
# define REG_GEN3_CTSR 0x20
# define REG_GEN3_THCTR 0x20
# define REG_GEN3_TEMP 0x28
# define REG_GEN3_THCODE1 0x50
# define REG_GEN3_THCODE2 0x54
# define REG_GEN3_THCODE3 0x58
2017-03-29 21:43:54 +03:00
/* IRQ{STR,MSK,EN} bits */
# define IRQ_TEMP1 BIT(0)
# define IRQ_TEMP2 BIT(1)
# define IRQ_TEMP3 BIT(2)
# define IRQ_TEMPD1 BIT(3)
# define IRQ_TEMPD2 BIT(4)
# define IRQ_TEMPD3 BIT(5)
2016-12-22 13:38:21 +03:00
/* CTSR bits */
# define CTSR_PONM BIT(8)
# define CTSR_AOUT BIT(7)
# define CTSR_THBGR BIT(5)
# define CTSR_VMEN BIT(4)
# define CTSR_VMST BIT(1)
# define CTSR_THSST BIT(0)
/* THCTR bits */
# define THCTR_PONM BIT(6)
# define THCTR_THSST BIT(0)
# define CTEMP_MASK 0xFFF
# define MCELSIUS(temp) ((temp) * 1000)
# define GEN3_FUSE_MASK 0xFFF
# define TSC_MAX_NUM 3
2019-05-13 23:03:55 +03:00
/* default THCODE values if FUSEs are missing */
static const int thcode [ TSC_MAX_NUM ] [ 3 ] = {
{ 3397 , 2800 , 2221 } ,
{ 3393 , 2795 , 2216 } ,
{ 3389 , 2805 , 2237 } ,
} ;
2016-12-22 13:38:21 +03:00
/* Structure for thermal temperature calculation */
struct equation_coefs {
int a1 ;
int b1 ;
int a2 ;
int b2 ;
} ;
struct rcar_gen3_thermal_tsc {
void __iomem * base ;
struct thermal_zone_device * zone ;
struct equation_coefs coef ;
2017-03-29 21:43:56 +03:00
int low ;
int high ;
2019-05-13 23:03:54 +03:00
int tj_t ;
2019-05-13 23:03:55 +03:00
int id ; /* thermal channel id */
2016-12-22 13:38:21 +03:00
} ;
struct rcar_gen3_thermal_priv {
struct rcar_gen3_thermal_tsc * tscs [ TSC_MAX_NUM ] ;
2017-03-29 21:43:53 +03:00
unsigned int num_tscs ;
2016-12-22 13:38:21 +03:00
void ( * thermal_init ) ( struct rcar_gen3_thermal_tsc * tsc ) ;
} ;
static inline u32 rcar_gen3_thermal_read ( struct rcar_gen3_thermal_tsc * tsc ,
u32 reg )
{
return ioread32 ( tsc - > base + reg ) ;
}
static inline void rcar_gen3_thermal_write ( struct rcar_gen3_thermal_tsc * tsc ,
u32 reg , u32 data )
{
iowrite32 ( data , tsc - > base + reg ) ;
}
/*
* Linear approximation for temperature
*
* [ reg ] = [ temp ] * a + b = > [ temp ] = ( [ reg ] - b ) / a
*
* The constants a and b are calculated using two triplets of int values PTAT
* and THCODE . PTAT and THCODE can either be read from hardware or use hard
* coded values from driver . The formula to calculate a and b are taken from
* BSP and sparsely documented and understood .
*
* Examining the linear formula and the formula used to calculate constants a
* and b while knowing that the span for PTAT and THCODE values are between
* 0x000 and 0xfff the largest integer possible is 0xfff * 0xfff = = 0xffe001 .
* Integer also needs to be signed so that leaves 7 bits for binary
* fixed point scaling .
*/
# define FIXPT_SHIFT 7
# define FIXPT_INT(_x) ((_x) << FIXPT_SHIFT)
2017-03-29 21:43:54 +03:00
# define INT_FIXPT(_x) ((_x) >> FIXPT_SHIFT)
2016-12-22 13:38:21 +03:00
# define FIXPT_DIV(_a, _b) DIV_ROUND_CLOSEST(((_a) << FIXPT_SHIFT), (_b))
# define FIXPT_TO_MCELSIUS(_x) ((_x) * 1000 >> FIXPT_SHIFT)
# define RCAR3_THERMAL_GRAN 500 /* mili Celsius */
/* no idea where these constants come from */
# define TJ_3 -41
2019-05-13 23:03:54 +03:00
static void rcar_gen3_thermal_calc_coefs ( struct rcar_gen3_thermal_tsc * tsc ,
2019-05-13 23:03:55 +03:00
int * ptat , const int * thcode ,
2019-05-13 23:03:53 +03:00
int ths_tj_1 )
2016-12-22 13:38:21 +03:00
{
/* TODO: Find documentation and document constant calculation formula */
/*
* Division is not scaled in BSP and if scaled it might overflow
* the dividend ( 4095 * 4095 < < 14 > INT_MAX ) so keep it unscaled
*/
2019-05-13 23:03:54 +03:00
tsc - > tj_t = ( FIXPT_INT ( ( ptat [ 1 ] - ptat [ 2 ] ) * 157 )
/ ( ptat [ 0 ] - ptat [ 2 ] ) ) + FIXPT_INT ( TJ_3 ) ;
2016-12-22 13:38:21 +03:00
2019-05-13 23:03:54 +03:00
tsc - > coef . a1 = FIXPT_DIV ( FIXPT_INT ( thcode [ 1 ] - thcode [ 2 ] ) ,
tsc - > tj_t - FIXPT_INT ( TJ_3 ) ) ;
tsc - > coef . b1 = FIXPT_INT ( thcode [ 2 ] ) - tsc - > coef . a1 * TJ_3 ;
2016-12-22 13:38:21 +03:00
2019-05-13 23:03:54 +03:00
tsc - > coef . a2 = FIXPT_DIV ( FIXPT_INT ( thcode [ 1 ] - thcode [ 0 ] ) ,
tsc - > tj_t - FIXPT_INT ( ths_tj_1 ) ) ;
tsc - > coef . b2 = FIXPT_INT ( thcode [ 0 ] ) - tsc - > coef . a2 * ths_tj_1 ;
2016-12-22 13:38:21 +03:00
}
static int rcar_gen3_thermal_round ( int temp )
{
int result , round_offs ;
round_offs = temp > = 0 ? RCAR3_THERMAL_GRAN / 2 :
- RCAR3_THERMAL_GRAN / 2 ;
result = ( temp + round_offs ) / RCAR3_THERMAL_GRAN ;
return result * RCAR3_THERMAL_GRAN ;
}
static int rcar_gen3_thermal_get_temp ( void * devdata , int * temp )
{
struct rcar_gen3_thermal_tsc * tsc = devdata ;
2019-05-13 23:03:55 +03:00
int mcelsius , val ;
2016-12-22 13:38:21 +03:00
u32 reg ;
/* Read register and convert to mili Celsius */
reg = rcar_gen3_thermal_read ( tsc , REG_GEN3_TEMP ) & CTEMP_MASK ;
2019-05-13 23:03:55 +03:00
if ( reg < = thcode [ tsc - > id ] [ 1 ] )
val = FIXPT_DIV ( FIXPT_INT ( reg ) - tsc - > coef . b1 ,
tsc - > coef . a1 ) ;
else
val = FIXPT_DIV ( FIXPT_INT ( reg ) - tsc - > coef . b2 ,
tsc - > coef . a2 ) ;
mcelsius = FIXPT_TO_MCELSIUS ( val ) ;
2016-12-22 13:38:21 +03:00
/* Make sure we are inside specifications */
if ( ( mcelsius < MCELSIUS ( - 40 ) ) | | ( mcelsius > MCELSIUS ( 125 ) ) )
return - EIO ;
/* Round value to device granularity setting */
* temp = rcar_gen3_thermal_round ( mcelsius ) ;
return 0 ;
}
2017-03-29 21:43:54 +03:00
static int rcar_gen3_thermal_mcelsius_to_temp ( struct rcar_gen3_thermal_tsc * tsc ,
int mcelsius )
{
2019-05-13 23:03:54 +03:00
int celsius , val ;
2017-03-29 21:43:54 +03:00
celsius = DIV_ROUND_CLOSEST ( mcelsius , 1000 ) ;
2019-05-13 23:03:54 +03:00
if ( celsius < = INT_FIXPT ( tsc - > tj_t ) )
val = celsius * tsc - > coef . a1 + tsc - > coef . b1 ;
else
val = celsius * tsc - > coef . a2 + tsc - > coef . b2 ;
2017-03-29 21:43:54 +03:00
2019-05-13 23:03:54 +03:00
return INT_FIXPT ( val ) ;
2017-03-29 21:43:54 +03:00
}
static int rcar_gen3_thermal_set_trips ( void * devdata , int low , int high )
{
struct rcar_gen3_thermal_tsc * tsc = devdata ;
2018-04-17 23:57:47 +03:00
low = clamp_val ( low , - 40000 , 120000 ) ;
high = clamp_val ( high , - 40000 , 120000 ) ;
2017-03-29 21:43:54 +03:00
rcar_gen3_thermal_write ( tsc , REG_GEN3_IRQTEMP1 ,
rcar_gen3_thermal_mcelsius_to_temp ( tsc , low ) ) ;
rcar_gen3_thermal_write ( tsc , REG_GEN3_IRQTEMP2 ,
rcar_gen3_thermal_mcelsius_to_temp ( tsc , high ) ) ;
2017-03-29 21:43:56 +03:00
tsc - > low = low ;
tsc - > high = high ;
2017-03-29 21:43:54 +03:00
return 0 ;
}
2017-08-08 18:08:57 +03:00
static const struct thermal_zone_of_device_ops rcar_gen3_tz_of_ops = {
2016-12-22 13:38:21 +03:00
. get_temp = rcar_gen3_thermal_get_temp ,
2017-03-29 21:43:54 +03:00
. set_trips = rcar_gen3_thermal_set_trips ,
2016-12-22 13:38:21 +03:00
} ;
2017-03-29 21:43:54 +03:00
static void rcar_thermal_irq_set ( struct rcar_gen3_thermal_priv * priv , bool on )
{
unsigned int i ;
u32 val = on ? IRQ_TEMPD1 | IRQ_TEMP2 : 0 ;
for ( i = 0 ; i < priv - > num_tscs ; i + + )
rcar_gen3_thermal_write ( priv - > tscs [ i ] , REG_GEN3_IRQMSK , val ) ;
}
static irqreturn_t rcar_gen3_thermal_irq ( int irq , void * data )
{
struct rcar_gen3_thermal_priv * priv = data ;
u32 status ;
2019-04-24 08:11:44 +03:00
int i ;
2017-03-29 21:43:54 +03:00
for ( i = 0 ; i < priv - > num_tscs ; i + + ) {
status = rcar_gen3_thermal_read ( priv - > tscs [ i ] , REG_GEN3_IRQSTR ) ;
rcar_gen3_thermal_write ( priv - > tscs [ i ] , REG_GEN3_IRQSTR , 0 ) ;
if ( status )
2019-04-24 08:11:44 +03:00
thermal_zone_device_update ( priv - > tscs [ i ] - > zone ,
THERMAL_EVENT_UNSPECIFIED ) ;
2017-03-29 21:43:54 +03:00
}
return IRQ_HANDLED ;
}
2017-10-17 14:36:13 +03:00
static const struct soc_device_attribute r8a7795es1 [ ] = {
{ . soc_id = " r8a7795 " , . revision = " ES1.* " } ,
{ /* sentinel */ }
} ;
static void rcar_gen3_thermal_init_r8a7795es1 ( struct rcar_gen3_thermal_tsc * tsc )
2016-12-22 13:38:21 +03:00
{
rcar_gen3_thermal_write ( tsc , REG_GEN3_CTSR , CTSR_THBGR ) ;
rcar_gen3_thermal_write ( tsc , REG_GEN3_CTSR , 0x0 ) ;
usleep_range ( 1000 , 2000 ) ;
rcar_gen3_thermal_write ( tsc , REG_GEN3_CTSR , CTSR_PONM ) ;
2017-03-29 21:43:54 +03:00
2016-12-22 13:38:21 +03:00
rcar_gen3_thermal_write ( tsc , REG_GEN3_IRQCTL , 0x3F ) ;
2017-03-29 21:43:54 +03:00
rcar_gen3_thermal_write ( tsc , REG_GEN3_IRQMSK , 0 ) ;
rcar_gen3_thermal_write ( tsc , REG_GEN3_IRQEN , IRQ_TEMPD1 | IRQ_TEMP2 ) ;
2016-12-22 13:38:21 +03:00
rcar_gen3_thermal_write ( tsc , REG_GEN3_CTSR ,
CTSR_PONM | CTSR_AOUT | CTSR_THBGR | CTSR_VMEN ) ;
usleep_range ( 100 , 200 ) ;
rcar_gen3_thermal_write ( tsc , REG_GEN3_CTSR ,
CTSR_PONM | CTSR_AOUT | CTSR_THBGR | CTSR_VMEN |
CTSR_VMST | CTSR_THSST ) ;
usleep_range ( 1000 , 2000 ) ;
}
2017-10-17 14:36:13 +03:00
static void rcar_gen3_thermal_init ( struct rcar_gen3_thermal_tsc * tsc )
2016-12-22 13:38:21 +03:00
{
u32 reg_val ;
reg_val = rcar_gen3_thermal_read ( tsc , REG_GEN3_THCTR ) ;
reg_val & = ~ THCTR_PONM ;
rcar_gen3_thermal_write ( tsc , REG_GEN3_THCTR , reg_val ) ;
usleep_range ( 1000 , 2000 ) ;
2019-03-27 12:03:18 +03:00
rcar_gen3_thermal_write ( tsc , REG_GEN3_IRQCTL , 0 ) ;
2017-03-29 21:43:54 +03:00
rcar_gen3_thermal_write ( tsc , REG_GEN3_IRQMSK , 0 ) ;
rcar_gen3_thermal_write ( tsc , REG_GEN3_IRQEN , IRQ_TEMPD1 | IRQ_TEMP2 ) ;
2016-12-22 13:38:21 +03:00
reg_val = rcar_gen3_thermal_read ( tsc , REG_GEN3_THCTR ) ;
reg_val | = THCTR_THSST ;
rcar_gen3_thermal_write ( tsc , REG_GEN3_THCTR , reg_val ) ;
2017-03-29 21:43:50 +03:00
usleep_range ( 1000 , 2000 ) ;
2016-12-22 13:38:21 +03:00
}
2019-05-13 23:03:53 +03:00
static const int rcar_gen3_ths_tj_1 = 126 ;
static const int rcar_gen3_ths_tj_1_m3_w = 116 ;
2016-12-22 13:38:21 +03:00
static const struct of_device_id rcar_gen3_thermal_dt_ids [ ] = {
2019-05-13 23:03:53 +03:00
{
. compatible = " renesas,r8a774a1-thermal " ,
. data = & rcar_gen3_ths_tj_1_m3_w ,
} ,
{
. compatible = " renesas,r8a7795-thermal " ,
. data = & rcar_gen3_ths_tj_1 ,
} ,
{
. compatible = " renesas,r8a7796-thermal " ,
. data = & rcar_gen3_ths_tj_1_m3_w ,
} ,
{
. compatible = " renesas,r8a77965-thermal " ,
. data = & rcar_gen3_ths_tj_1 ,
} ,
{
. compatible = " renesas,r8a77980-thermal " ,
. data = & rcar_gen3_ths_tj_1 ,
} ,
2016-12-22 13:38:21 +03:00
{ } ,
} ;
MODULE_DEVICE_TABLE ( of , rcar_gen3_thermal_dt_ids ) ;
static int rcar_gen3_thermal_remove ( struct platform_device * pdev )
{
struct device * dev = & pdev - > dev ;
2019-04-24 08:11:45 +03:00
struct rcar_gen3_thermal_priv * priv = dev_get_drvdata ( dev ) ;
rcar_thermal_irq_set ( priv , false ) ;
2016-12-22 13:38:21 +03:00
pm_runtime_put ( dev ) ;
pm_runtime_disable ( dev ) ;
return 0 ;
}
2019-02-11 22:56:20 +03:00
static void rcar_gen3_hwmon_action ( void * data )
{
struct thermal_zone_device * zone = data ;
thermal_remove_hwmon_sysfs ( zone ) ;
}
2016-12-22 13:38:21 +03:00
static int rcar_gen3_thermal_probe ( struct platform_device * pdev )
{
struct rcar_gen3_thermal_priv * priv ;
struct device * dev = & pdev - > dev ;
2019-05-13 23:03:53 +03:00
const int * rcar_gen3_ths_tj_1 = of_device_get_match_data ( dev ) ;
2016-12-22 13:38:21 +03:00
struct resource * res ;
struct thermal_zone_device * zone ;
2017-03-29 21:43:54 +03:00
int ret , irq , i ;
char * irqname ;
2016-12-22 13:38:21 +03:00
/* default values if FUSEs are missing */
/* TODO: Read values from hardware on supported platforms */
2018-04-17 23:57:46 +03:00
int ptat [ 3 ] = { 2631 , 1509 , 435 } ;
2016-12-22 13:38:21 +03:00
priv = devm_kzalloc ( dev , sizeof ( * priv ) , GFP_KERNEL ) ;
if ( ! priv )
return - ENOMEM ;
2017-10-17 14:36:13 +03:00
priv - > thermal_init = rcar_gen3_thermal_init ;
if ( soc_device_match ( r8a7795es1 ) )
priv - > thermal_init = rcar_gen3_thermal_init_r8a7795es1 ;
2017-03-29 21:43:55 +03:00
2016-12-22 13:38:21 +03:00
platform_set_drvdata ( pdev , priv ) ;
2017-03-29 21:43:54 +03:00
/*
* Request 2 ( of the 3 possible ) IRQs , the driver only needs to
* to trigger on the low and high trip points of the current
* temp window at this point .
*/
for ( i = 0 ; i < 2 ; i + + ) {
irq = platform_get_irq ( pdev , i ) ;
if ( irq < 0 )
return irq ;
irqname = devm_kasprintf ( dev , GFP_KERNEL , " %s:ch%d " ,
dev_name ( dev ) , i ) ;
if ( ! irqname )
return - ENOMEM ;
2019-04-24 08:11:44 +03:00
ret = devm_request_threaded_irq ( dev , irq , NULL ,
rcar_gen3_thermal_irq ,
IRQF_ONESHOT , irqname , priv ) ;
2017-03-29 21:43:54 +03:00
if ( ret )
return ret ;
}
2016-12-22 13:38:21 +03:00
pm_runtime_enable ( dev ) ;
pm_runtime_get_sync ( dev ) ;
for ( i = 0 ; i < TSC_MAX_NUM ; i + + ) {
struct rcar_gen3_thermal_tsc * tsc ;
2017-03-29 21:43:52 +03:00
res = platform_get_resource ( pdev , IORESOURCE_MEM , i ) ;
if ( ! res )
break ;
2016-12-22 13:38:21 +03:00
tsc = devm_kzalloc ( dev , sizeof ( * tsc ) , GFP_KERNEL ) ;
if ( ! tsc ) {
ret = - ENOMEM ;
goto error_unregister ;
}
tsc - > base = devm_ioremap_resource ( dev , res ) ;
if ( IS_ERR ( tsc - > base ) ) {
ret = PTR_ERR ( tsc - > base ) ;
goto error_unregister ;
}
2019-05-13 23:03:55 +03:00
tsc - > id = i ;
2016-12-22 13:38:21 +03:00
priv - > tscs [ i ] = tsc ;
2017-10-17 14:36:13 +03:00
priv - > thermal_init ( tsc ) ;
2019-05-13 23:03:54 +03:00
rcar_gen3_thermal_calc_coefs ( tsc , ptat , thcode [ i ] ,
2019-05-13 23:03:53 +03:00
* rcar_gen3_ths_tj_1 ) ;
2016-12-22 13:38:21 +03:00
zone = devm_thermal_zone_of_sensor_register ( dev , i , tsc ,
& rcar_gen3_tz_of_ops ) ;
if ( IS_ERR ( zone ) ) {
dev_err ( dev , " Can't register thermal zone \n " ) ;
ret = PTR_ERR ( zone ) ;
goto error_unregister ;
}
tsc - > zone = zone ;
2017-03-29 21:43:54 +03:00
2019-02-11 22:56:20 +03:00
tsc - > zone - > tzp - > no_hwmon = false ;
ret = thermal_add_hwmon_sysfs ( tsc - > zone ) ;
if ( ret )
goto error_unregister ;
2019-07-08 15:34:09 +03:00
ret = devm_add_action_or_reset ( dev , rcar_gen3_hwmon_action , zone ) ;
2019-02-11 22:56:20 +03:00
if ( ret ) {
goto error_unregister ;
}
2019-05-09 12:09:17 +03:00
ret = of_thermal_get_ntrips ( tsc - > zone ) ;
if ( ret < 0 )
goto error_unregister ;
2017-03-29 21:43:54 +03:00
dev_info ( dev , " TSC%d: Loaded %d trip points \n " , i , ret ) ;
2016-12-22 13:38:21 +03:00
}
2017-03-29 21:43:53 +03:00
priv - > num_tscs = i ;
if ( ! priv - > num_tscs ) {
ret = - ENODEV ;
goto error_unregister ;
}
2017-03-29 21:43:54 +03:00
rcar_thermal_irq_set ( priv , true ) ;
2016-12-22 13:38:21 +03:00
return 0 ;
error_unregister :
rcar_gen3_thermal_remove ( pdev ) ;
return ret ;
}
2017-03-29 21:43:56 +03:00
static int __maybe_unused rcar_gen3_thermal_suspend ( struct device * dev )
{
struct rcar_gen3_thermal_priv * priv = dev_get_drvdata ( dev ) ;
rcar_thermal_irq_set ( priv , false ) ;
return 0 ;
}
static int __maybe_unused rcar_gen3_thermal_resume ( struct device * dev )
{
struct rcar_gen3_thermal_priv * priv = dev_get_drvdata ( dev ) ;
unsigned int i ;
for ( i = 0 ; i < priv - > num_tscs ; i + + ) {
struct rcar_gen3_thermal_tsc * tsc = priv - > tscs [ i ] ;
2017-10-17 14:36:13 +03:00
priv - > thermal_init ( tsc ) ;
2017-03-29 21:43:56 +03:00
rcar_gen3_thermal_set_trips ( tsc , tsc - > low , tsc - > high ) ;
}
rcar_thermal_irq_set ( priv , true ) ;
return 0 ;
}
static SIMPLE_DEV_PM_OPS ( rcar_gen3_thermal_pm_ops , rcar_gen3_thermal_suspend ,
rcar_gen3_thermal_resume ) ;
2016-12-22 13:38:21 +03:00
static struct platform_driver rcar_gen3_thermal_driver = {
. driver = {
. name = " rcar_gen3_thermal " ,
2017-03-29 21:43:56 +03:00
. pm = & rcar_gen3_thermal_pm_ops ,
2016-12-22 13:38:21 +03:00
. of_match_table = rcar_gen3_thermal_dt_ids ,
} ,
. probe = rcar_gen3_thermal_probe ,
. remove = rcar_gen3_thermal_remove ,
} ;
module_platform_driver ( rcar_gen3_thermal_driver ) ;
MODULE_LICENSE ( " GPL v2 " ) ;
MODULE_DESCRIPTION ( " R-Car Gen3 THS thermal sensor driver " ) ;
MODULE_AUTHOR ( " Wolfram Sang <wsa+renesas@sang-engineering.com> " ) ;