Commit | Line | Data |
---|---|---|
9e03cf1b BN |
1 | /* |
2 | * Broadcom STB AVS TMON thermal sensor driver | |
3 | * | |
4 | * Copyright (c) 2015-2017 Broadcom | |
5 | * | |
6 | * This software is licensed under the terms of the GNU General Public | |
7 | * License version 2, as published by the Free Software Foundation, and | |
8 | * may be copied, distributed, and modified under those terms. | |
9 | * | |
10 | * This program is distributed in the hope that it will be useful, | |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
13 | * GNU General Public License for more details. | |
14 | * | |
15 | */ | |
16 | ||
17 | #define DRV_NAME "brcmstb_thermal" | |
18 | ||
19 | #define pr_fmt(fmt) DRV_NAME ": " fmt | |
20 | ||
21 | #include <linux/bitops.h> | |
22 | #include <linux/device.h> | |
23 | #include <linux/err.h> | |
24 | #include <linux/io.h> | |
25 | #include <linux/irqreturn.h> | |
26 | #include <linux/interrupt.h> | |
27 | #include <linux/kernel.h> | |
28 | #include <linux/module.h> | |
29 | #include <linux/platform_device.h> | |
30 | #include <linux/of_device.h> | |
31 | #include <linux/thermal.h> | |
32 | ||
33 | #define AVS_TMON_STATUS 0x00 | |
34 | #define AVS_TMON_STATUS_valid_msk BIT(11) | |
35 | #define AVS_TMON_STATUS_data_msk GENMASK(10, 1) | |
36 | #define AVS_TMON_STATUS_data_shift 1 | |
37 | ||
38 | #define AVS_TMON_EN_OVERTEMP_RESET 0x04 | |
39 | #define AVS_TMON_EN_OVERTEMP_RESET_msk BIT(0) | |
40 | ||
41 | #define AVS_TMON_RESET_THRESH 0x08 | |
42 | #define AVS_TMON_RESET_THRESH_msk GENMASK(10, 1) | |
43 | #define AVS_TMON_RESET_THRESH_shift 1 | |
44 | ||
45 | #define AVS_TMON_INT_IDLE_TIME 0x10 | |
46 | ||
47 | #define AVS_TMON_EN_TEMP_INT_SRCS 0x14 | |
48 | #define AVS_TMON_EN_TEMP_INT_SRCS_high BIT(1) | |
49 | #define AVS_TMON_EN_TEMP_INT_SRCS_low BIT(0) | |
50 | ||
51 | #define AVS_TMON_INT_THRESH 0x18 | |
52 | #define AVS_TMON_INT_THRESH_high_msk GENMASK(26, 17) | |
53 | #define AVS_TMON_INT_THRESH_high_shift 17 | |
54 | #define AVS_TMON_INT_THRESH_low_msk GENMASK(10, 1) | |
55 | #define AVS_TMON_INT_THRESH_low_shift 1 | |
56 | ||
57 | #define AVS_TMON_TEMP_INT_CODE 0x1c | |
58 | #define AVS_TMON_TP_TEST_ENABLE 0x20 | |
59 | ||
60 | /* Default coefficients */ | |
61 | #define AVS_TMON_TEMP_SLOPE -487 | |
62 | #define AVS_TMON_TEMP_OFFSET 410040 | |
63 | ||
64 | /* HW related temperature constants */ | |
65 | #define AVS_TMON_TEMP_MAX 0x3ff | |
66 | #define AVS_TMON_TEMP_MIN -88161 | |
67 | #define AVS_TMON_TEMP_MASK AVS_TMON_TEMP_MAX | |
68 | ||
69 | enum avs_tmon_trip_type { | |
70 | TMON_TRIP_TYPE_LOW = 0, | |
71 | TMON_TRIP_TYPE_HIGH, | |
72 | TMON_TRIP_TYPE_RESET, | |
73 | TMON_TRIP_TYPE_MAX, | |
74 | }; | |
75 | ||
76 | struct avs_tmon_trip { | |
77 | /* HW bit to enable the trip */ | |
78 | u32 enable_offs; | |
79 | u32 enable_mask; | |
80 | ||
81 | /* HW field to read the trip temperature */ | |
82 | u32 reg_offs; | |
83 | u32 reg_msk; | |
84 | int reg_shift; | |
85 | }; | |
86 | ||
87 | static struct avs_tmon_trip avs_tmon_trips[] = { | |
88 | /* Trips when temperature is below threshold */ | |
89 | [TMON_TRIP_TYPE_LOW] = { | |
90 | .enable_offs = AVS_TMON_EN_TEMP_INT_SRCS, | |
91 | .enable_mask = AVS_TMON_EN_TEMP_INT_SRCS_low, | |
92 | .reg_offs = AVS_TMON_INT_THRESH, | |
93 | .reg_msk = AVS_TMON_INT_THRESH_low_msk, | |
94 | .reg_shift = AVS_TMON_INT_THRESH_low_shift, | |
95 | }, | |
96 | /* Trips when temperature is above threshold */ | |
97 | [TMON_TRIP_TYPE_HIGH] = { | |
98 | .enable_offs = AVS_TMON_EN_TEMP_INT_SRCS, | |
99 | .enable_mask = AVS_TMON_EN_TEMP_INT_SRCS_high, | |
100 | .reg_offs = AVS_TMON_INT_THRESH, | |
101 | .reg_msk = AVS_TMON_INT_THRESH_high_msk, | |
102 | .reg_shift = AVS_TMON_INT_THRESH_high_shift, | |
103 | }, | |
104 | /* Automatically resets chip when above threshold */ | |
105 | [TMON_TRIP_TYPE_RESET] = { | |
106 | .enable_offs = AVS_TMON_EN_OVERTEMP_RESET, | |
107 | .enable_mask = AVS_TMON_EN_OVERTEMP_RESET_msk, | |
108 | .reg_offs = AVS_TMON_RESET_THRESH, | |
109 | .reg_msk = AVS_TMON_RESET_THRESH_msk, | |
110 | .reg_shift = AVS_TMON_RESET_THRESH_shift, | |
111 | }, | |
112 | }; | |
113 | ||
114 | struct brcmstb_thermal_priv { | |
115 | void __iomem *tmon_base; | |
116 | struct device *dev; | |
117 | struct thermal_zone_device *thermal; | |
118 | }; | |
119 | ||
120 | static void avs_tmon_get_coeffs(struct thermal_zone_device *tz, int *slope, | |
121 | int *offset) | |
122 | { | |
123 | *slope = thermal_zone_get_slope(tz); | |
124 | *offset = thermal_zone_get_offset(tz); | |
125 | } | |
126 | ||
127 | /* Convert a HW code to a temperature reading (millidegree celsius) */ | |
128 | static inline int avs_tmon_code_to_temp(struct thermal_zone_device *tz, | |
129 | u32 code) | |
130 | { | |
131 | const int val = code & AVS_TMON_TEMP_MASK; | |
132 | int slope, offset; | |
133 | ||
134 | avs_tmon_get_coeffs(tz, &slope, &offset); | |
135 | ||
136 | return slope * val + offset; | |
137 | } | |
138 | ||
139 | /* | |
140 | * Convert a temperature value (millidegree celsius) to a HW code | |
141 | * | |
142 | * @temp: temperature to convert | |
143 | * @low: if true, round toward the low side | |
144 | */ | |
145 | static inline u32 avs_tmon_temp_to_code(struct thermal_zone_device *tz, | |
146 | int temp, bool low) | |
147 | { | |
148 | int slope, offset; | |
149 | ||
150 | if (temp < AVS_TMON_TEMP_MIN) | |
151 | return AVS_TMON_TEMP_MAX; /* Maximum code value */ | |
152 | ||
153 | avs_tmon_get_coeffs(tz, &slope, &offset); | |
154 | ||
155 | if (temp >= offset) | |
156 | return 0; /* Minimum code value */ | |
157 | ||
158 | if (low) | |
159 | return (u32)(DIV_ROUND_UP(offset - temp, abs(slope))); | |
160 | else | |
161 | return (u32)((offset - temp) / abs(slope)); | |
162 | } | |
163 | ||
164 | static int brcmstb_get_temp(void *data, int *temp) | |
165 | { | |
166 | struct brcmstb_thermal_priv *priv = data; | |
167 | u32 val; | |
168 | long t; | |
169 | ||
170 | val = __raw_readl(priv->tmon_base + AVS_TMON_STATUS); | |
171 | ||
172 | if (!(val & AVS_TMON_STATUS_valid_msk)) { | |
173 | dev_err(priv->dev, "reading not valid\n"); | |
174 | return -EIO; | |
175 | } | |
176 | ||
177 | val = (val & AVS_TMON_STATUS_data_msk) >> AVS_TMON_STATUS_data_shift; | |
178 | ||
179 | t = avs_tmon_code_to_temp(priv->thermal, val); | |
180 | if (t < 0) | |
181 | *temp = 0; | |
182 | else | |
183 | *temp = t; | |
184 | ||
185 | return 0; | |
186 | } | |
187 | ||
188 | static void avs_tmon_trip_enable(struct brcmstb_thermal_priv *priv, | |
189 | enum avs_tmon_trip_type type, int en) | |
190 | { | |
191 | struct avs_tmon_trip *trip = &avs_tmon_trips[type]; | |
192 | u32 val = __raw_readl(priv->tmon_base + trip->enable_offs); | |
193 | ||
194 | dev_dbg(priv->dev, "%sable trip, type %d\n", en ? "en" : "dis", type); | |
195 | ||
196 | if (en) | |
197 | val |= trip->enable_mask; | |
198 | else | |
199 | val &= ~trip->enable_mask; | |
200 | ||
201 | __raw_writel(val, priv->tmon_base + trip->enable_offs); | |
202 | } | |
203 | ||
204 | static int avs_tmon_get_trip_temp(struct brcmstb_thermal_priv *priv, | |
205 | enum avs_tmon_trip_type type) | |
206 | { | |
207 | struct avs_tmon_trip *trip = &avs_tmon_trips[type]; | |
208 | u32 val = __raw_readl(priv->tmon_base + trip->reg_offs); | |
209 | ||
210 | val &= trip->reg_msk; | |
211 | val >>= trip->reg_shift; | |
212 | ||
213 | return avs_tmon_code_to_temp(priv->thermal, val); | |
214 | } | |
215 | ||
216 | static void avs_tmon_set_trip_temp(struct brcmstb_thermal_priv *priv, | |
217 | enum avs_tmon_trip_type type, | |
218 | int temp) | |
219 | { | |
220 | struct avs_tmon_trip *trip = &avs_tmon_trips[type]; | |
221 | u32 val, orig; | |
222 | ||
223 | dev_dbg(priv->dev, "set temp %d to %d\n", type, temp); | |
224 | ||
225 | /* round toward low temp for the low interrupt */ | |
226 | val = avs_tmon_temp_to_code(priv->thermal, temp, | |
227 | type == TMON_TRIP_TYPE_LOW); | |
228 | ||
229 | val <<= trip->reg_shift; | |
230 | val &= trip->reg_msk; | |
231 | ||
232 | orig = __raw_readl(priv->tmon_base + trip->reg_offs); | |
233 | orig &= ~trip->reg_msk; | |
234 | orig |= val; | |
235 | __raw_writel(orig, priv->tmon_base + trip->reg_offs); | |
236 | } | |
237 | ||
238 | static int avs_tmon_get_intr_temp(struct brcmstb_thermal_priv *priv) | |
239 | { | |
240 | u32 val; | |
241 | ||
242 | val = __raw_readl(priv->tmon_base + AVS_TMON_TEMP_INT_CODE); | |
243 | return avs_tmon_code_to_temp(priv->thermal, val); | |
244 | } | |
245 | ||
246 | static irqreturn_t brcmstb_tmon_irq_thread(int irq, void *data) | |
247 | { | |
248 | struct brcmstb_thermal_priv *priv = data; | |
249 | int low, high, intr; | |
250 | ||
251 | low = avs_tmon_get_trip_temp(priv, TMON_TRIP_TYPE_LOW); | |
252 | high = avs_tmon_get_trip_temp(priv, TMON_TRIP_TYPE_HIGH); | |
253 | intr = avs_tmon_get_intr_temp(priv); | |
254 | ||
255 | dev_dbg(priv->dev, "low/intr/high: %d/%d/%d\n", | |
256 | low, intr, high); | |
257 | ||
258 | /* Disable high-temp until next threshold shift */ | |
259 | if (intr >= high) | |
260 | avs_tmon_trip_enable(priv, TMON_TRIP_TYPE_HIGH, 0); | |
261 | /* Disable low-temp until next threshold shift */ | |
262 | if (intr <= low) | |
263 | avs_tmon_trip_enable(priv, TMON_TRIP_TYPE_LOW, 0); | |
264 | ||
265 | /* | |
266 | * Notify using the interrupt temperature, in case the temperature | |
267 | * changes before it can next be read out | |
268 | */ | |
269 | thermal_zone_device_update(priv->thermal, intr); | |
270 | ||
271 | return IRQ_HANDLED; | |
272 | } | |
273 | ||
274 | static int brcmstb_set_trips(void *data, int low, int high) | |
275 | { | |
276 | struct brcmstb_thermal_priv *priv = data; | |
277 | ||
278 | dev_dbg(priv->dev, "set trips %d <--> %d\n", low, high); | |
279 | ||
280 | /* | |
281 | * Disable low-temp if "low" is too small. As per thermal framework | |
282 | * API, we use -INT_MAX rather than INT_MIN. | |
283 | */ | |
284 | if (low <= -INT_MAX) { | |
285 | avs_tmon_trip_enable(priv, TMON_TRIP_TYPE_LOW, 0); | |
286 | } else { | |
287 | avs_tmon_set_trip_temp(priv, TMON_TRIP_TYPE_LOW, low); | |
288 | avs_tmon_trip_enable(priv, TMON_TRIP_TYPE_LOW, 1); | |
289 | } | |
290 | ||
291 | /* Disable high-temp if "high" is too big. */ | |
292 | if (high == INT_MAX) { | |
293 | avs_tmon_trip_enable(priv, TMON_TRIP_TYPE_HIGH, 0); | |
294 | } else { | |
295 | avs_tmon_set_trip_temp(priv, TMON_TRIP_TYPE_HIGH, high); | |
296 | avs_tmon_trip_enable(priv, TMON_TRIP_TYPE_HIGH, 1); | |
297 | } | |
298 | ||
299 | return 0; | |
300 | } | |
301 | ||
302 | static struct thermal_zone_of_device_ops of_ops = { | |
303 | .get_temp = brcmstb_get_temp, | |
304 | .set_trips = brcmstb_set_trips, | |
305 | }; | |
306 | ||
307 | static const struct of_device_id brcmstb_thermal_id_table[] = { | |
308 | { .compatible = "brcm,avs-tmon" }, | |
309 | {}, | |
310 | }; | |
311 | MODULE_DEVICE_TABLE(of, brcmstb_thermal_id_table); | |
312 | ||
313 | static int brcmstb_thermal_probe(struct platform_device *pdev) | |
314 | { | |
315 | struct thermal_zone_device *thermal; | |
316 | struct brcmstb_thermal_priv *priv; | |
317 | struct resource *res; | |
318 | int irq, ret; | |
319 | ||
320 | priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); | |
321 | if (!priv) | |
322 | return -ENOMEM; | |
323 | ||
324 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
325 | priv->tmon_base = devm_ioremap_resource(&pdev->dev, res); | |
326 | if (IS_ERR(priv->tmon_base)) | |
327 | return PTR_ERR(priv->tmon_base); | |
328 | ||
329 | priv->dev = &pdev->dev; | |
330 | platform_set_drvdata(pdev, priv); | |
331 | ||
adad7c7d JL |
332 | thermal = devm_thermal_zone_of_sensor_register(&pdev->dev, 0, priv, |
333 | &of_ops); | |
9e03cf1b BN |
334 | if (IS_ERR(thermal)) { |
335 | ret = PTR_ERR(thermal); | |
336 | dev_err(&pdev->dev, "could not register sensor: %d\n", ret); | |
337 | return ret; | |
338 | } | |
339 | ||
340 | priv->thermal = thermal; | |
341 | ||
342 | irq = platform_get_irq(pdev, 0); | |
343 | if (irq < 0) { | |
344 | dev_err(&pdev->dev, "could not get IRQ\n"); | |
adad7c7d | 345 | return irq; |
9e03cf1b BN |
346 | } |
347 | ret = devm_request_threaded_irq(&pdev->dev, irq, NULL, | |
348 | brcmstb_tmon_irq_thread, IRQF_ONESHOT, | |
349 | DRV_NAME, priv); | |
350 | if (ret < 0) { | |
351 | dev_err(&pdev->dev, "could not request IRQ: %d\n", ret); | |
adad7c7d | 352 | return ret; |
9e03cf1b BN |
353 | } |
354 | ||
355 | dev_info(&pdev->dev, "registered AVS TMON of-sensor driver\n"); | |
356 | ||
357 | return 0; | |
9e03cf1b BN |
358 | } |
359 | ||
360 | static struct platform_driver brcmstb_thermal_driver = { | |
361 | .probe = brcmstb_thermal_probe, | |
9e03cf1b BN |
362 | .driver = { |
363 | .name = DRV_NAME, | |
364 | .of_match_table = brcmstb_thermal_id_table, | |
365 | }, | |
366 | }; | |
367 | module_platform_driver(brcmstb_thermal_driver); | |
368 | ||
369 | MODULE_LICENSE("GPL v2"); | |
370 | MODULE_AUTHOR("Brian Norris"); | |
371 | MODULE_DESCRIPTION("Broadcom STB AVS TMON thermal driver"); |