1 // SPDX-License-Identifier: GPL-2.0
2 /*
3 * R-Car Gen3 THS thermal sensor driver
4 * Based on rcar_thermal.c and work from Hien Dang and Khiem Nguyen.
5 *
6 * Copyright (C) 2016 Renesas Electronics Corporation.
7 * Copyright (C) 2016 Sang Engineering
8 */
9 #include <linux/delay.h>
10 #include <linux/err.h>
11 #include <linux/interrupt.h>
12 #include <linux/io.h>
13 #include <linux/module.h>
14 #include <linux/of.h>
15 #include <linux/platform_device.h>
16 #include <linux/pm_runtime.h>
17 #include <linux/thermal.h>
18
19 #include "../thermal_hwmon.h"
20
21 /* Register offsets */
22 #define REG_GEN3_IRQSTR 0x04
23 #define REG_GEN3_IRQMSK 0x08
24 #define REG_GEN3_IRQCTL 0x0c
25 #define REG_GEN3_IRQEN 0x10
26 #define REG_GEN3_IRQTEMP1 0x14
27 #define REG_GEN3_IRQTEMP2 0x18
28 #define REG_GEN3_IRQTEMP3 0x1c
29 #define REG_GEN3_THCTR 0x20
30 #define REG_GEN3_TEMP 0x28
31 #define REG_GEN3_THCODE1 0x50
32 #define REG_GEN3_THCODE2 0x54
33 #define REG_GEN3_THCODE3 0x58
34 #define REG_GEN3_PTAT1 0x5c
35 #define REG_GEN3_PTAT2 0x60
36 #define REG_GEN3_PTAT3 0x64
37 #define REG_GEN3_THSCP 0x68
38 #define REG_GEN4_THSFMON00 0x180
39 #define REG_GEN4_THSFMON01 0x184
40 #define REG_GEN4_THSFMON02 0x188
41 #define REG_GEN4_THSFMON15 0x1bc
42 #define REG_GEN4_THSFMON16 0x1c0
43 #define REG_GEN4_THSFMON17 0x1c4
44
45 /* IRQ{STR,MSK,EN} bits */
46 #define IRQ_TEMP1 BIT(0)
47 #define IRQ_TEMP2 BIT(1)
48 #define IRQ_TEMP3 BIT(2)
49 #define IRQ_TEMPD1 BIT(3)
50 #define IRQ_TEMPD2 BIT(4)
51 #define IRQ_TEMPD3 BIT(5)
52
53 /* THCTR bits */
54 #define THCTR_PONM BIT(6)
55 #define THCTR_THSST BIT(0)
56
57 /* THSCP bits */
58 #define THSCP_COR_PARA_VLD (BIT(15) | BIT(14))
59
60 #define CTEMP_MASK 0xfff
61
62 #define MCELSIUS(temp) ((temp) * 1000)
63 #define GEN3_FUSE_MASK 0xfff
64 #define GEN4_FUSE_MASK 0xfff
65
66 #define TSC_MAX_NUM 5
67
68 struct rcar_gen3_thermal_priv;
69
70 struct rcar_gen3_thermal_fuse_info {
71 u32 ptat[3];
72 u32 thcode[3];
73 u32 mask;
74 };
75
76 struct rcar_thermal_info {
77 int scale;
78 int adj_below;
79 int adj_above;
80 const struct rcar_gen3_thermal_fuse_info *fuses;
81 };
82
83 struct equation_set_coef {
84 int a;
85 int b;
86 };
87
88 struct rcar_gen3_thermal_tsc {
89 struct rcar_gen3_thermal_priv *priv;
90 void __iomem *base;
91 struct thermal_zone_device *zone;
92 /* Different coefficients are used depending on a threshold. */
93 struct {
94 struct equation_set_coef below;
95 struct equation_set_coef above;
96 } coef;
97 int thcode[3];
98 };
99
100 struct rcar_gen3_thermal_priv {
101 struct rcar_gen3_thermal_tsc *tscs[TSC_MAX_NUM];
102 struct thermal_zone_device_ops ops;
103 unsigned int num_tscs;
104 int ptat[3];
105 int tj_t;
106 const struct rcar_thermal_info *info;
107 };
108
rcar_gen3_thermal_read(struct rcar_gen3_thermal_tsc * tsc,u32 reg)109 static inline u32 rcar_gen3_thermal_read(struct rcar_gen3_thermal_tsc *tsc,
110 u32 reg)
111 {
112 return ioread32(tsc->base + reg);
113 }
114
rcar_gen3_thermal_write(struct rcar_gen3_thermal_tsc * tsc,u32 reg,u32 data)115 static inline void rcar_gen3_thermal_write(struct rcar_gen3_thermal_tsc *tsc,
116 u32 reg, u32 data)
117 {
118 iowrite32(data, tsc->base + reg);
119 }
120
121 /*
122 * Linear approximation for temperature
123 *
124 * [temp] = ((thadj - [reg]) * a) / b + adj
125 * [reg] = thadj - ([temp] - adj) * b / a
126 *
127 * The constants a and b are calculated using two triplets of int values PTAT
128 * and THCODE. PTAT and THCODE can either be read from hardware or use hard
129 * coded values from the driver. The formula to calculate a and b are taken from
130 * the datasheet. Different calculations are needed for a and b depending on
131 * if the input variables ([temp] or [reg]) are above or below a threshold. The
132 * threshold is also calculated from PTAT and THCODE using formulas from the
133 * datasheet.
134 *
135 * The constant thadj is one of the THCODE values, which one to use depends on
136 * the threshold and input value.
137 *
138 * The constants adj is taken verbatim from the datasheet. Two values exists,
139 * which one to use depends on the input value and the calculated threshold.
140 * Furthermore different SoC models supported by the driver have different sets
141 * of values. The values for each model are stored in the device match data.
142 */
143
rcar_gen3_thermal_shared_coefs(struct rcar_gen3_thermal_priv * priv)144 static void rcar_gen3_thermal_shared_coefs(struct rcar_gen3_thermal_priv *priv)
145 {
146 priv->tj_t =
147 DIV_ROUND_CLOSEST((priv->ptat[1] - priv->ptat[2]) * priv->info->scale,
148 priv->ptat[0] - priv->ptat[2])
149 + priv->info->adj_below;
150 }
rcar_gen3_thermal_tsc_coefs(struct rcar_gen3_thermal_priv * priv,struct rcar_gen3_thermal_tsc * tsc)151 static void rcar_gen3_thermal_tsc_coefs(struct rcar_gen3_thermal_priv *priv,
152 struct rcar_gen3_thermal_tsc *tsc)
153 {
154 tsc->coef.below.a = priv->info->scale * (priv->ptat[2] - priv->ptat[1]);
155 tsc->coef.above.a = priv->info->scale * (priv->ptat[0] - priv->ptat[1]);
156
157 tsc->coef.below.b = (priv->ptat[2] - priv->ptat[0]) * (tsc->thcode[2] - tsc->thcode[1]);
158 tsc->coef.above.b = (priv->ptat[0] - priv->ptat[2]) * (tsc->thcode[1] - tsc->thcode[0]);
159 }
160
rcar_gen3_thermal_get_temp(struct thermal_zone_device * tz,int * temp)161 static int rcar_gen3_thermal_get_temp(struct thermal_zone_device *tz, int *temp)
162 {
163 struct rcar_gen3_thermal_tsc *tsc = thermal_zone_device_priv(tz);
164 struct rcar_gen3_thermal_priv *priv = tsc->priv;
165 const struct equation_set_coef *coef;
166 int adj, decicelsius, reg, thcode;
167
168 /* Read register and convert to mili Celsius */
169 reg = rcar_gen3_thermal_read(tsc, REG_GEN3_TEMP) & CTEMP_MASK;
170
171 if (reg < tsc->thcode[1]) {
172 adj = priv->info->adj_below;
173 coef = &tsc->coef.below;
174 thcode = tsc->thcode[2];
175 } else {
176 adj = priv->info->adj_above;
177 coef = &tsc->coef.above;
178 thcode = tsc->thcode[0];
179 }
180
181 /*
182 * The dividend can't be grown as it might overflow, instead shorten the
183 * divisor to convert to decidegree Celsius. If we convert after the
184 * division precision is lost as we will scale up from whole degrees
185 * Celsius.
186 */
187 decicelsius = DIV_ROUND_CLOSEST(coef->a * (thcode - reg), coef->b / 10);
188
189 /* Guaranteed operating range is -40C to 125C. */
190
191 /* Reporting is done in millidegree Celsius */
192 *temp = decicelsius * 100 + adj * 1000;
193
194 return 0;
195 }
196
rcar_gen3_thermal_mcelsius_to_temp(struct rcar_gen3_thermal_tsc * tsc,int mcelsius)197 static int rcar_gen3_thermal_mcelsius_to_temp(struct rcar_gen3_thermal_tsc *tsc,
198 int mcelsius)
199 {
200 struct rcar_gen3_thermal_priv *priv = tsc->priv;
201 const struct equation_set_coef *coef;
202 int adj, celsius, thcode;
203
204 celsius = DIV_ROUND_CLOSEST(mcelsius, 1000);
205 if (celsius < priv->tj_t) {
206 coef = &tsc->coef.below;
207 adj = priv->info->adj_below;
208 thcode = tsc->thcode[2];
209 } else {
210 coef = &tsc->coef.above;
211 adj = priv->info->adj_above;
212 thcode = tsc->thcode[0];
213 }
214
215 return thcode - DIV_ROUND_CLOSEST((celsius - adj) * coef->b, coef->a);
216 }
217
rcar_gen3_thermal_set_trips(struct thermal_zone_device * tz,int low,int high)218 static int rcar_gen3_thermal_set_trips(struct thermal_zone_device *tz, int low, int high)
219 {
220 struct rcar_gen3_thermal_tsc *tsc = thermal_zone_device_priv(tz);
221 u32 irqmsk = 0;
222
223 if (low != -INT_MAX) {
224 irqmsk |= IRQ_TEMPD1;
225 rcar_gen3_thermal_write(tsc, REG_GEN3_IRQTEMP1,
226 rcar_gen3_thermal_mcelsius_to_temp(tsc, low));
227 }
228
229 if (high != INT_MAX) {
230 irqmsk |= IRQ_TEMP2;
231 rcar_gen3_thermal_write(tsc, REG_GEN3_IRQTEMP2,
232 rcar_gen3_thermal_mcelsius_to_temp(tsc, high));
233 }
234
235 rcar_gen3_thermal_write(tsc, REG_GEN3_IRQMSK, irqmsk);
236
237 return 0;
238 }
239
240 static const struct thermal_zone_device_ops rcar_gen3_tz_of_ops = {
241 .get_temp = rcar_gen3_thermal_get_temp,
242 .set_trips = rcar_gen3_thermal_set_trips,
243 };
244
rcar_gen3_thermal_irq(int irq,void * data)245 static irqreturn_t rcar_gen3_thermal_irq(int irq, void *data)
246 {
247 struct rcar_gen3_thermal_priv *priv = data;
248 unsigned int i;
249 u32 status;
250
251 for (i = 0; i < priv->num_tscs; i++) {
252 status = rcar_gen3_thermal_read(priv->tscs[i], REG_GEN3_IRQSTR);
253 rcar_gen3_thermal_write(priv->tscs[i], REG_GEN3_IRQSTR, 0);
254 if (status && priv->tscs[i]->zone)
255 thermal_zone_device_update(priv->tscs[i]->zone,
256 THERMAL_EVENT_UNSPECIFIED);
257 }
258
259 return IRQ_HANDLED;
260 }
261
rcar_gen3_thermal_fetch_fuses(struct rcar_gen3_thermal_priv * priv)262 static void rcar_gen3_thermal_fetch_fuses(struct rcar_gen3_thermal_priv *priv)
263 {
264 const struct rcar_gen3_thermal_fuse_info *fuses = priv->info->fuses;
265
266 /*
267 * Set the pseudo calibration points with fused values.
268 * PTAT is shared between all TSCs but only fused for the first
269 * TSC while THCODEs are fused for each TSC.
270 */
271 priv->ptat[0] = rcar_gen3_thermal_read(priv->tscs[0], fuses->ptat[0])
272 & fuses->mask;
273 priv->ptat[1] = rcar_gen3_thermal_read(priv->tscs[0], fuses->ptat[1])
274 & fuses->mask;
275 priv->ptat[2] = rcar_gen3_thermal_read(priv->tscs[0], fuses->ptat[2])
276 & fuses->mask;
277
278 for (unsigned int i = 0; i < priv->num_tscs; i++) {
279 struct rcar_gen3_thermal_tsc *tsc = priv->tscs[i];
280
281 tsc->thcode[0] = rcar_gen3_thermal_read(tsc, fuses->thcode[0])
282 & fuses->mask;
283 tsc->thcode[1] = rcar_gen3_thermal_read(tsc, fuses->thcode[1])
284 & fuses->mask;
285 tsc->thcode[2] = rcar_gen3_thermal_read(tsc, fuses->thcode[2])
286 & fuses->mask;
287 }
288 }
289
rcar_gen3_thermal_read_fuses(struct rcar_gen3_thermal_priv * priv)290 static bool rcar_gen3_thermal_read_fuses(struct rcar_gen3_thermal_priv *priv)
291 {
292 unsigned int i;
293 u32 thscp;
294
295 /* If fuses are not set, fallback to pseudo values. */
296 thscp = rcar_gen3_thermal_read(priv->tscs[0], REG_GEN3_THSCP);
297 if (!priv->info->fuses ||
298 (thscp & THSCP_COR_PARA_VLD) != THSCP_COR_PARA_VLD) {
299 /* Default THCODE values in case FUSEs are not set. */
300 static const int thcodes[TSC_MAX_NUM][3] = {
301 { 3397, 2800, 2221 },
302 { 3393, 2795, 2216 },
303 { 3389, 2805, 2237 },
304 { 3415, 2694, 2195 },
305 { 3356, 2724, 2244 },
306 };
307
308 priv->ptat[0] = 2631;
309 priv->ptat[1] = 1509;
310 priv->ptat[2] = 435;
311
312 for (i = 0; i < priv->num_tscs; i++) {
313 struct rcar_gen3_thermal_tsc *tsc = priv->tscs[i];
314
315 tsc->thcode[0] = thcodes[i][0];
316 tsc->thcode[1] = thcodes[i][1];
317 tsc->thcode[2] = thcodes[i][2];
318 }
319
320 return false;
321 }
322
323 rcar_gen3_thermal_fetch_fuses(priv);
324
325 return true;
326 }
327
rcar_gen3_thermal_init(struct rcar_gen3_thermal_priv * priv,struct rcar_gen3_thermal_tsc * tsc)328 static void rcar_gen3_thermal_init(struct rcar_gen3_thermal_priv *priv,
329 struct rcar_gen3_thermal_tsc *tsc)
330 {
331 u32 reg_val;
332
333 reg_val = rcar_gen3_thermal_read(tsc, REG_GEN3_THCTR);
334 reg_val &= ~THCTR_PONM;
335 rcar_gen3_thermal_write(tsc, REG_GEN3_THCTR, reg_val);
336
337 usleep_range(1000, 2000);
338
339 rcar_gen3_thermal_write(tsc, REG_GEN3_IRQCTL, 0);
340 rcar_gen3_thermal_write(tsc, REG_GEN3_IRQMSK, 0);
341 if (priv->ops.set_trips)
342 rcar_gen3_thermal_write(tsc, REG_GEN3_IRQEN,
343 IRQ_TEMPD1 | IRQ_TEMP2);
344
345 reg_val = rcar_gen3_thermal_read(tsc, REG_GEN3_THCTR);
346 reg_val |= THCTR_THSST;
347 rcar_gen3_thermal_write(tsc, REG_GEN3_THCTR, reg_val);
348
349 usleep_range(1000, 2000);
350 }
351
352 static const struct rcar_gen3_thermal_fuse_info rcar_gen3_thermal_fuse_info_gen3 = {
353 .ptat = { REG_GEN3_PTAT1, REG_GEN3_PTAT2, REG_GEN3_PTAT3 },
354 .thcode = { REG_GEN3_THCODE1, REG_GEN3_THCODE2, REG_GEN3_THCODE3 },
355 .mask = GEN3_FUSE_MASK,
356 };
357
358 static const struct rcar_gen3_thermal_fuse_info rcar_gen3_thermal_fuse_info_gen4 = {
359 .ptat = { REG_GEN4_THSFMON16, REG_GEN4_THSFMON17, REG_GEN4_THSFMON15 },
360 .thcode = { REG_GEN4_THSFMON01, REG_GEN4_THSFMON02, REG_GEN4_THSFMON00 },
361 .mask = GEN4_FUSE_MASK,
362 };
363
364 static const struct rcar_thermal_info rcar_m3w_thermal_info = {
365 .scale = 157,
366 .adj_below = -41,
367 .adj_above = 116,
368 .fuses = &rcar_gen3_thermal_fuse_info_gen3,
369 };
370
371 static const struct rcar_thermal_info rcar_gen3_thermal_info = {
372 .scale = 167,
373 .adj_below = -41,
374 .adj_above = 126,
375 .fuses = &rcar_gen3_thermal_fuse_info_gen3,
376 };
377
378 static const struct rcar_thermal_info rcar_gen4_thermal_info = {
379 .scale = 167,
380 .adj_below = -41,
381 .adj_above = 126,
382 .fuses = &rcar_gen3_thermal_fuse_info_gen4,
383 };
384
385 static const struct of_device_id rcar_gen3_thermal_dt_ids[] = {
386 {
387 .compatible = "renesas,r8a774a1-thermal",
388 .data = &rcar_m3w_thermal_info,
389 },
390 {
391 .compatible = "renesas,r8a774b1-thermal",
392 .data = &rcar_gen3_thermal_info,
393 },
394 {
395 .compatible = "renesas,r8a774e1-thermal",
396 .data = &rcar_gen3_thermal_info,
397 },
398 {
399 .compatible = "renesas,r8a7795-thermal",
400 .data = &rcar_gen3_thermal_info,
401 },
402 {
403 .compatible = "renesas,r8a7796-thermal",
404 .data = &rcar_m3w_thermal_info,
405 },
406 {
407 .compatible = "renesas,r8a77961-thermal",
408 .data = &rcar_m3w_thermal_info,
409 },
410 {
411 .compatible = "renesas,r8a77965-thermal",
412 .data = &rcar_gen3_thermal_info,
413 },
414 {
415 .compatible = "renesas,r8a77980-thermal",
416 .data = &rcar_gen3_thermal_info,
417 },
418 {
419 .compatible = "renesas,r8a779a0-thermal",
420 .data = &rcar_gen3_thermal_info,
421 },
422 {
423 .compatible = "renesas,r8a779f0-thermal",
424 .data = &rcar_gen4_thermal_info,
425 },
426 {
427 .compatible = "renesas,r8a779g0-thermal",
428 .data = &rcar_gen4_thermal_info,
429 },
430 {
431 .compatible = "renesas,r8a779h0-thermal",
432 .data = &rcar_gen4_thermal_info,
433 },
434 {},
435 };
436 MODULE_DEVICE_TABLE(of, rcar_gen3_thermal_dt_ids);
437
rcar_gen3_thermal_remove(struct platform_device * pdev)438 static void rcar_gen3_thermal_remove(struct platform_device *pdev)
439 {
440 struct device *dev = &pdev->dev;
441
442 pm_runtime_put(dev);
443 pm_runtime_disable(dev);
444 }
445
rcar_gen3_hwmon_action(void * data)446 static void rcar_gen3_hwmon_action(void *data)
447 {
448 struct thermal_zone_device *zone = data;
449
450 thermal_remove_hwmon_sysfs(zone);
451 }
452
rcar_gen3_thermal_request_irqs(struct rcar_gen3_thermal_priv * priv,struct platform_device * pdev)453 static int rcar_gen3_thermal_request_irqs(struct rcar_gen3_thermal_priv *priv,
454 struct platform_device *pdev)
455 {
456 struct device *dev = &pdev->dev;
457 unsigned int i;
458 char *irqname;
459 int ret, irq;
460
461 for (i = 0; i < 2; i++) {
462 irq = platform_get_irq_optional(pdev, i);
463 if (irq < 0)
464 return irq;
465
466 irqname = devm_kasprintf(dev, GFP_KERNEL, "%s:ch%d",
467 dev_name(dev), i);
468 if (!irqname)
469 return -ENOMEM;
470
471 ret = devm_request_threaded_irq(dev, irq, NULL,
472 rcar_gen3_thermal_irq,
473 IRQF_ONESHOT, irqname, priv);
474 if (ret)
475 return ret;
476 }
477
478 return 0;
479 }
480
rcar_gen3_thermal_probe(struct platform_device * pdev)481 static int rcar_gen3_thermal_probe(struct platform_device *pdev)
482 {
483 struct rcar_gen3_thermal_priv *priv;
484 struct device *dev = &pdev->dev;
485 struct resource *res;
486 struct thermal_zone_device *zone;
487 unsigned int i;
488 int ret;
489
490 priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
491 if (!priv)
492 return -ENOMEM;
493
494 priv->ops = rcar_gen3_tz_of_ops;
495
496 priv->info = of_device_get_match_data(dev);
497 platform_set_drvdata(pdev, priv);
498
499 if (rcar_gen3_thermal_request_irqs(priv, pdev))
500 priv->ops.set_trips = NULL;
501
502 pm_runtime_enable(dev);
503 pm_runtime_get_sync(dev);
504
505 for (i = 0; i < TSC_MAX_NUM; i++) {
506 struct rcar_gen3_thermal_tsc *tsc;
507
508 res = platform_get_resource(pdev, IORESOURCE_MEM, i);
509 if (!res)
510 break;
511
512 tsc = devm_kzalloc(dev, sizeof(*tsc), GFP_KERNEL);
513 if (!tsc) {
514 ret = -ENOMEM;
515 goto error_unregister;
516 }
517
518 tsc->priv = priv;
519 tsc->base = devm_ioremap_resource(dev, res);
520 if (IS_ERR(tsc->base)) {
521 ret = PTR_ERR(tsc->base);
522 goto error_unregister;
523 }
524
525 priv->tscs[i] = tsc;
526 }
527
528 priv->num_tscs = i;
529
530 if (!rcar_gen3_thermal_read_fuses(priv))
531 dev_info(dev, "No calibration values fused, fallback to driver values\n");
532
533 rcar_gen3_thermal_shared_coefs(priv);
534
535 for (i = 0; i < priv->num_tscs; i++) {
536 struct rcar_gen3_thermal_tsc *tsc = priv->tscs[i];
537
538 rcar_gen3_thermal_init(priv, tsc);
539 rcar_gen3_thermal_tsc_coefs(priv, tsc);
540
541 zone = devm_thermal_of_zone_register(dev, i, tsc, &priv->ops);
542 if (IS_ERR(zone)) {
543 dev_err(dev, "Sensor %u: Can't register thermal zone\n", i);
544 ret = PTR_ERR(zone);
545 goto error_unregister;
546 }
547 tsc->zone = zone;
548
549 ret = thermal_add_hwmon_sysfs(tsc->zone);
550 if (ret)
551 goto error_unregister;
552
553 ret = devm_add_action_or_reset(dev, rcar_gen3_hwmon_action, zone);
554 if (ret)
555 goto error_unregister;
556
557 dev_info(dev, "Sensor %u: Loaded\n", i);
558 }
559
560 if (!priv->num_tscs) {
561 ret = -ENODEV;
562 goto error_unregister;
563 }
564
565 return 0;
566
567 error_unregister:
568 rcar_gen3_thermal_remove(pdev);
569
570 return ret;
571 }
572
rcar_gen3_thermal_resume(struct device * dev)573 static int __maybe_unused rcar_gen3_thermal_resume(struct device *dev)
574 {
575 struct rcar_gen3_thermal_priv *priv = dev_get_drvdata(dev);
576 unsigned int i;
577
578 for (i = 0; i < priv->num_tscs; i++) {
579 struct rcar_gen3_thermal_tsc *tsc = priv->tscs[i];
580
581 rcar_gen3_thermal_init(priv, tsc);
582 }
583
584 return 0;
585 }
586
587 static SIMPLE_DEV_PM_OPS(rcar_gen3_thermal_pm_ops, NULL,
588 rcar_gen3_thermal_resume);
589
590 static struct platform_driver rcar_gen3_thermal_driver = {
591 .driver = {
592 .name = "rcar_gen3_thermal",
593 .pm = &rcar_gen3_thermal_pm_ops,
594 .of_match_table = rcar_gen3_thermal_dt_ids,
595 },
596 .probe = rcar_gen3_thermal_probe,
597 .remove = rcar_gen3_thermal_remove,
598 };
599 module_platform_driver(rcar_gen3_thermal_driver);
600
601 MODULE_LICENSE("GPL v2");
602 MODULE_DESCRIPTION("R-Car Gen3 THS thermal sensor driver");
603 MODULE_AUTHOR("Wolfram Sang <wsa+renesas@sang-engineering.com>");
604