]>
Commit | Line | Data |
---|---|---|
2874c5fd | 1 | // SPDX-License-Identifier: GPL-2.0-or-later |
0cad855f PB |
2 | /* |
3 | * Copyright (C) 2016 Imagination Technologies | |
fb615d61 | 4 | * Author: Paul Burton <paul.burton@mips.com> |
0cad855f PB |
5 | */ |
6 | ||
7 | #include <generated/utsrelease.h> | |
8 | #include <linux/kernel.h> | |
9 | #include <linux/io.h> | |
10 | #include <linux/mfd/syscon.h> | |
11 | #include <linux/module.h> | |
12 | #include <linux/of_address.h> | |
13 | #include <linux/of_platform.h> | |
14 | #include <linux/platform_device.h> | |
15 | #include <linux/regmap.h> | |
16 | #include <linux/slab.h> | |
17 | #include <linux/sysfs.h> | |
18 | ||
19 | struct img_ascii_lcd_ctx; | |
20 | ||
21 | /** | |
22 | * struct img_ascii_lcd_config - Configuration information about an LCD model | |
23 | * @num_chars: the number of characters the LCD can display | |
24 | * @external_regmap: true if registers are in a system controller, else false | |
25 | * @update: function called to update the LCD | |
26 | */ | |
27 | struct img_ascii_lcd_config { | |
28 | unsigned int num_chars; | |
29 | bool external_regmap; | |
30 | void (*update)(struct img_ascii_lcd_ctx *ctx); | |
31 | }; | |
32 | ||
33 | /** | |
34 | * struct img_ascii_lcd_ctx - Private data structure | |
35 | * @pdev: the ASCII LCD platform device | |
36 | * @base: the base address of the LCD registers | |
37 | * @regmap: the regmap through which LCD registers are accessed | |
38 | * @offset: the offset within regmap to the start of the LCD registers | |
39 | * @cfg: pointer to the LCD model configuration | |
40 | * @message: the full message to display or scroll on the LCD | |
41 | * @message_len: the length of the @message string | |
42 | * @scroll_pos: index of the first character of @message currently displayed | |
43 | * @scroll_rate: scroll interval in jiffies | |
44 | * @timer: timer used to implement scrolling | |
45 | * @curr: the string currently displayed on the LCD | |
46 | */ | |
47 | struct img_ascii_lcd_ctx { | |
48 | struct platform_device *pdev; | |
49 | union { | |
50 | void __iomem *base; | |
51 | struct regmap *regmap; | |
52 | }; | |
53 | u32 offset; | |
54 | const struct img_ascii_lcd_config *cfg; | |
55 | char *message; | |
56 | unsigned int message_len; | |
57 | unsigned int scroll_pos; | |
58 | unsigned int scroll_rate; | |
59 | struct timer_list timer; | |
60 | char curr[] __aligned(8); | |
61 | }; | |
62 | ||
63 | /* | |
64 | * MIPS Boston development board | |
65 | */ | |
66 | ||
67 | static void boston_update(struct img_ascii_lcd_ctx *ctx) | |
68 | { | |
69 | ulong val; | |
70 | ||
71 | #if BITS_PER_LONG == 64 | |
72 | val = *((u64 *)&ctx->curr[0]); | |
73 | __raw_writeq(val, ctx->base); | |
74 | #elif BITS_PER_LONG == 32 | |
75 | val = *((u32 *)&ctx->curr[0]); | |
76 | __raw_writel(val, ctx->base); | |
77 | val = *((u32 *)&ctx->curr[4]); | |
78 | __raw_writel(val, ctx->base + 4); | |
79 | #else | |
80 | # error Not 32 or 64 bit | |
81 | #endif | |
82 | } | |
83 | ||
84 | static struct img_ascii_lcd_config boston_config = { | |
85 | .num_chars = 8, | |
86 | .update = boston_update, | |
87 | }; | |
88 | ||
89 | /* | |
90 | * MIPS Malta development board | |
91 | */ | |
92 | ||
93 | static void malta_update(struct img_ascii_lcd_ctx *ctx) | |
94 | { | |
95 | unsigned int i; | |
26a2c54d | 96 | int err = 0; |
0cad855f PB |
97 | |
98 | for (i = 0; i < ctx->cfg->num_chars; i++) { | |
99 | err = regmap_write(ctx->regmap, | |
100 | ctx->offset + (i * 8), ctx->curr[i]); | |
101 | if (err) | |
102 | break; | |
103 | } | |
104 | ||
105 | if (unlikely(err)) | |
106 | pr_err_ratelimited("Failed to update LCD display: %d\n", err); | |
107 | } | |
108 | ||
109 | static struct img_ascii_lcd_config malta_config = { | |
110 | .num_chars = 8, | |
111 | .external_regmap = true, | |
112 | .update = malta_update, | |
113 | }; | |
114 | ||
115 | /* | |
116 | * MIPS SEAD3 development board | |
117 | */ | |
118 | ||
119 | enum { | |
120 | SEAD3_REG_LCD_CTRL = 0x00, | |
121 | #define SEAD3_REG_LCD_CTRL_SETDRAM BIT(7) | |
122 | SEAD3_REG_LCD_DATA = 0x08, | |
123 | SEAD3_REG_CPLD_STATUS = 0x10, | |
124 | #define SEAD3_REG_CPLD_STATUS_BUSY BIT(0) | |
125 | SEAD3_REG_CPLD_DATA = 0x18, | |
126 | #define SEAD3_REG_CPLD_DATA_BUSY BIT(7) | |
127 | }; | |
128 | ||
129 | static int sead3_wait_sm_idle(struct img_ascii_lcd_ctx *ctx) | |
130 | { | |
131 | unsigned int status; | |
132 | int err; | |
133 | ||
134 | do { | |
135 | err = regmap_read(ctx->regmap, | |
136 | ctx->offset + SEAD3_REG_CPLD_STATUS, | |
137 | &status); | |
138 | if (err) | |
139 | return err; | |
140 | } while (status & SEAD3_REG_CPLD_STATUS_BUSY); | |
141 | ||
142 | return 0; | |
143 | ||
144 | } | |
145 | ||
146 | static int sead3_wait_lcd_idle(struct img_ascii_lcd_ctx *ctx) | |
147 | { | |
148 | unsigned int cpld_data; | |
149 | int err; | |
150 | ||
151 | err = sead3_wait_sm_idle(ctx); | |
152 | if (err) | |
153 | return err; | |
154 | ||
155 | do { | |
156 | err = regmap_read(ctx->regmap, | |
157 | ctx->offset + SEAD3_REG_LCD_CTRL, | |
158 | &cpld_data); | |
159 | if (err) | |
160 | return err; | |
161 | ||
162 | err = sead3_wait_sm_idle(ctx); | |
163 | if (err) | |
164 | return err; | |
165 | ||
166 | err = regmap_read(ctx->regmap, | |
167 | ctx->offset + SEAD3_REG_CPLD_DATA, | |
168 | &cpld_data); | |
169 | if (err) | |
170 | return err; | |
171 | } while (cpld_data & SEAD3_REG_CPLD_DATA_BUSY); | |
172 | ||
173 | return 0; | |
174 | } | |
175 | ||
176 | static void sead3_update(struct img_ascii_lcd_ctx *ctx) | |
177 | { | |
178 | unsigned int i; | |
26a2c54d | 179 | int err = 0; |
0cad855f PB |
180 | |
181 | for (i = 0; i < ctx->cfg->num_chars; i++) { | |
182 | err = sead3_wait_lcd_idle(ctx); | |
183 | if (err) | |
184 | break; | |
185 | ||
186 | err = regmap_write(ctx->regmap, | |
187 | ctx->offset + SEAD3_REG_LCD_CTRL, | |
188 | SEAD3_REG_LCD_CTRL_SETDRAM | i); | |
189 | if (err) | |
190 | break; | |
191 | ||
192 | err = sead3_wait_lcd_idle(ctx); | |
193 | if (err) | |
194 | break; | |
195 | ||
196 | err = regmap_write(ctx->regmap, | |
197 | ctx->offset + SEAD3_REG_LCD_DATA, | |
198 | ctx->curr[i]); | |
199 | if (err) | |
200 | break; | |
201 | } | |
202 | ||
203 | if (unlikely(err)) | |
204 | pr_err_ratelimited("Failed to update LCD display: %d\n", err); | |
205 | } | |
206 | ||
207 | static struct img_ascii_lcd_config sead3_config = { | |
208 | .num_chars = 16, | |
209 | .external_regmap = true, | |
210 | .update = sead3_update, | |
211 | }; | |
212 | ||
213 | static const struct of_device_id img_ascii_lcd_matches[] = { | |
214 | { .compatible = "img,boston-lcd", .data = &boston_config }, | |
215 | { .compatible = "mti,malta-lcd", .data = &malta_config }, | |
216 | { .compatible = "mti,sead3-lcd", .data = &sead3_config }, | |
abda288b | 217 | { /* sentinel */ } |
0cad855f | 218 | }; |
750100a4 | 219 | MODULE_DEVICE_TABLE(of, img_ascii_lcd_matches); |
0cad855f PB |
220 | |
221 | /** | |
222 | * img_ascii_lcd_scroll() - scroll the display by a character | |
6a78b4dd | 223 | * @t: really a pointer to the private data structure |
0cad855f PB |
224 | * |
225 | * Scroll the current message along the LCD by one character, rearming the | |
226 | * timer if required. | |
227 | */ | |
607a6301 | 228 | static void img_ascii_lcd_scroll(struct timer_list *t) |
0cad855f | 229 | { |
607a6301 | 230 | struct img_ascii_lcd_ctx *ctx = from_timer(ctx, t, timer); |
0cad855f PB |
231 | unsigned int i, ch = ctx->scroll_pos; |
232 | unsigned int num_chars = ctx->cfg->num_chars; | |
233 | ||
234 | /* update the current message string */ | |
235 | for (i = 0; i < num_chars;) { | |
236 | /* copy as many characters from the string as possible */ | |
237 | for (; i < num_chars && ch < ctx->message_len; i++, ch++) | |
238 | ctx->curr[i] = ctx->message[ch]; | |
239 | ||
240 | /* wrap around to the start of the string */ | |
241 | ch = 0; | |
242 | } | |
243 | ||
244 | /* update the LCD */ | |
245 | ctx->cfg->update(ctx); | |
246 | ||
247 | /* move on to the next character */ | |
248 | ctx->scroll_pos++; | |
249 | ctx->scroll_pos %= ctx->message_len; | |
250 | ||
251 | /* rearm the timer */ | |
252 | if (ctx->message_len > ctx->cfg->num_chars) | |
253 | mod_timer(&ctx->timer, jiffies + ctx->scroll_rate); | |
254 | } | |
255 | ||
256 | /** | |
257 | * img_ascii_lcd_display() - set the message to be displayed | |
258 | * @ctx: pointer to the private data structure | |
259 | * @msg: the message to display | |
260 | * @count: length of msg, or -1 | |
261 | * | |
262 | * Display a new message @msg on the LCD. @msg can be longer than the number of | |
263 | * characters the LCD can display, in which case it will begin scrolling across | |
264 | * the LCD display. | |
265 | * | |
266 | * Return: 0 on success, -ENOMEM on memory allocation failure | |
267 | */ | |
268 | static int img_ascii_lcd_display(struct img_ascii_lcd_ctx *ctx, | |
269 | const char *msg, ssize_t count) | |
270 | { | |
271 | char *new_msg; | |
272 | ||
273 | /* stop the scroll timer */ | |
274 | del_timer_sync(&ctx->timer); | |
275 | ||
276 | if (count == -1) | |
277 | count = strlen(msg); | |
278 | ||
279 | /* if the string ends with a newline, trim it */ | |
280 | if (msg[count - 1] == '\n') | |
281 | count--; | |
282 | ||
283 | new_msg = devm_kmalloc(&ctx->pdev->dev, count + 1, GFP_KERNEL); | |
284 | if (!new_msg) | |
285 | return -ENOMEM; | |
286 | ||
287 | memcpy(new_msg, msg, count); | |
288 | new_msg[count] = 0; | |
289 | ||
290 | if (ctx->message) | |
291 | devm_kfree(&ctx->pdev->dev, ctx->message); | |
292 | ||
293 | ctx->message = new_msg; | |
294 | ctx->message_len = count; | |
295 | ctx->scroll_pos = 0; | |
296 | ||
297 | /* update the LCD */ | |
607a6301 | 298 | img_ascii_lcd_scroll(&ctx->timer); |
0cad855f PB |
299 | |
300 | return 0; | |
301 | } | |
302 | ||
303 | /** | |
304 | * message_show() - read message via sysfs | |
305 | * @dev: the LCD device | |
306 | * @attr: the LCD message attribute | |
307 | * @buf: the buffer to read the message into | |
308 | * | |
309 | * Read the current message being displayed or scrolled across the LCD display | |
310 | * into @buf, for reads from sysfs. | |
311 | * | |
312 | * Return: the number of characters written to @buf | |
313 | */ | |
314 | static ssize_t message_show(struct device *dev, struct device_attribute *attr, | |
315 | char *buf) | |
316 | { | |
317 | struct img_ascii_lcd_ctx *ctx = dev_get_drvdata(dev); | |
318 | ||
319 | return sprintf(buf, "%s\n", ctx->message); | |
320 | } | |
321 | ||
322 | /** | |
323 | * message_store() - write a new message via sysfs | |
324 | * @dev: the LCD device | |
325 | * @attr: the LCD message attribute | |
326 | * @buf: the buffer containing the new message | |
327 | * @count: the size of the message in @buf | |
328 | * | |
329 | * Write a new message to display or scroll across the LCD display from sysfs. | |
330 | * | |
331 | * Return: the size of the message on success, else -ERRNO | |
332 | */ | |
333 | static ssize_t message_store(struct device *dev, struct device_attribute *attr, | |
334 | const char *buf, size_t count) | |
335 | { | |
336 | struct img_ascii_lcd_ctx *ctx = dev_get_drvdata(dev); | |
337 | int err; | |
338 | ||
339 | err = img_ascii_lcd_display(ctx, buf, count); | |
340 | return err ?: count; | |
341 | } | |
342 | ||
343 | static DEVICE_ATTR_RW(message); | |
344 | ||
345 | /** | |
346 | * img_ascii_lcd_probe() - probe an LCD display device | |
347 | * @pdev: the LCD platform device | |
348 | * | |
349 | * Probe an LCD display device, ensuring that we have the required resources in | |
350 | * order to access the LCD & setting up private data as well as sysfs files. | |
351 | * | |
352 | * Return: 0 on success, else -ERRNO | |
353 | */ | |
354 | static int img_ascii_lcd_probe(struct platform_device *pdev) | |
355 | { | |
356 | const struct of_device_id *match; | |
357 | const struct img_ascii_lcd_config *cfg; | |
358 | struct img_ascii_lcd_ctx *ctx; | |
359 | struct resource *res; | |
360 | int err; | |
361 | ||
362 | match = of_match_device(img_ascii_lcd_matches, &pdev->dev); | |
363 | if (!match) | |
364 | return -ENODEV; | |
365 | ||
366 | cfg = match->data; | |
367 | ctx = devm_kzalloc(&pdev->dev, sizeof(*ctx) + cfg->num_chars, | |
368 | GFP_KERNEL); | |
369 | if (!ctx) | |
370 | return -ENOMEM; | |
371 | ||
372 | if (cfg->external_regmap) { | |
373 | ctx->regmap = syscon_node_to_regmap(pdev->dev.parent->of_node); | |
374 | if (IS_ERR(ctx->regmap)) | |
375 | return PTR_ERR(ctx->regmap); | |
376 | ||
377 | if (of_property_read_u32(pdev->dev.of_node, "offset", | |
378 | &ctx->offset)) | |
379 | return -EINVAL; | |
380 | } else { | |
381 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
382 | ctx->base = devm_ioremap_resource(&pdev->dev, res); | |
383 | if (IS_ERR(ctx->base)) | |
384 | return PTR_ERR(ctx->base); | |
385 | } | |
386 | ||
387 | ctx->pdev = pdev; | |
388 | ctx->cfg = cfg; | |
389 | ctx->message = NULL; | |
390 | ctx->scroll_pos = 0; | |
391 | ctx->scroll_rate = HZ / 2; | |
392 | ||
393 | /* initialise a timer for scrolling the message */ | |
607a6301 | 394 | timer_setup(&ctx->timer, img_ascii_lcd_scroll, 0); |
0cad855f PB |
395 | |
396 | platform_set_drvdata(pdev, ctx); | |
397 | ||
398 | /* display a default message */ | |
399 | err = img_ascii_lcd_display(ctx, "Linux " UTS_RELEASE " ", -1); | |
400 | if (err) | |
401 | goto out_del_timer; | |
402 | ||
403 | err = device_create_file(&pdev->dev, &dev_attr_message); | |
404 | if (err) | |
405 | goto out_del_timer; | |
406 | ||
407 | return 0; | |
408 | out_del_timer: | |
409 | del_timer_sync(&ctx->timer); | |
410 | return err; | |
411 | } | |
412 | ||
413 | /** | |
414 | * img_ascii_lcd_remove() - remove an LCD display device | |
415 | * @pdev: the LCD platform device | |
416 | * | |
417 | * Remove an LCD display device, freeing private resources & ensuring that the | |
418 | * driver stops using the LCD display registers. | |
419 | * | |
420 | * Return: 0 | |
421 | */ | |
422 | static int img_ascii_lcd_remove(struct platform_device *pdev) | |
423 | { | |
424 | struct img_ascii_lcd_ctx *ctx = platform_get_drvdata(pdev); | |
425 | ||
426 | device_remove_file(&pdev->dev, &dev_attr_message); | |
427 | del_timer_sync(&ctx->timer); | |
428 | return 0; | |
429 | } | |
430 | ||
431 | static struct platform_driver img_ascii_lcd_driver = { | |
432 | .driver = { | |
433 | .name = "img-ascii-lcd", | |
434 | .of_match_table = img_ascii_lcd_matches, | |
435 | }, | |
436 | .probe = img_ascii_lcd_probe, | |
437 | .remove = img_ascii_lcd_remove, | |
438 | }; | |
439 | module_platform_driver(img_ascii_lcd_driver); | |
09c479f7 JC |
440 | |
441 | MODULE_DESCRIPTION("Imagination Technologies ASCII LCD Display"); | |
442 | MODULE_AUTHOR("Paul Burton <paul.burton@mips.com>"); | |
443 | MODULE_LICENSE("GPL"); |