|  | // SPDX-License-Identifier: GPL-2.0-or-later | 
|  | /* | 
|  | * Copyright (C) 2016 Imagination Technologies | 
|  | * Author: Paul Burton <paul.burton@mips.com> | 
|  | */ | 
|  |  | 
|  | #include <generated/utsrelease.h> | 
|  | #include <linux/kernel.h> | 
|  | #include <linux/io.h> | 
|  | #include <linux/mfd/syscon.h> | 
|  | #include <linux/module.h> | 
|  | #include <linux/of_address.h> | 
|  | #include <linux/of_platform.h> | 
|  | #include <linux/platform_device.h> | 
|  | #include <linux/regmap.h> | 
|  | #include <linux/slab.h> | 
|  | #include <linux/sysfs.h> | 
|  |  | 
|  | struct img_ascii_lcd_ctx; | 
|  |  | 
|  | /** | 
|  | * struct img_ascii_lcd_config - Configuration information about an LCD model | 
|  | * @num_chars: the number of characters the LCD can display | 
|  | * @external_regmap: true if registers are in a system controller, else false | 
|  | * @update: function called to update the LCD | 
|  | */ | 
|  | struct img_ascii_lcd_config { | 
|  | unsigned int num_chars; | 
|  | bool external_regmap; | 
|  | void (*update)(struct img_ascii_lcd_ctx *ctx); | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * struct img_ascii_lcd_ctx - Private data structure | 
|  | * @pdev: the ASCII LCD platform device | 
|  | * @base: the base address of the LCD registers | 
|  | * @regmap: the regmap through which LCD registers are accessed | 
|  | * @offset: the offset within regmap to the start of the LCD registers | 
|  | * @cfg: pointer to the LCD model configuration | 
|  | * @message: the full message to display or scroll on the LCD | 
|  | * @message_len: the length of the @message string | 
|  | * @scroll_pos: index of the first character of @message currently displayed | 
|  | * @scroll_rate: scroll interval in jiffies | 
|  | * @timer: timer used to implement scrolling | 
|  | * @curr: the string currently displayed on the LCD | 
|  | */ | 
|  | struct img_ascii_lcd_ctx { | 
|  | struct platform_device *pdev; | 
|  | union { | 
|  | void __iomem *base; | 
|  | struct regmap *regmap; | 
|  | }; | 
|  | u32 offset; | 
|  | const struct img_ascii_lcd_config *cfg; | 
|  | char *message; | 
|  | unsigned int message_len; | 
|  | unsigned int scroll_pos; | 
|  | unsigned int scroll_rate; | 
|  | struct timer_list timer; | 
|  | char curr[] __aligned(8); | 
|  | }; | 
|  |  | 
|  | /* | 
|  | * MIPS Boston development board | 
|  | */ | 
|  |  | 
|  | static void boston_update(struct img_ascii_lcd_ctx *ctx) | 
|  | { | 
|  | ulong val; | 
|  |  | 
|  | #if BITS_PER_LONG == 64 | 
|  | val = *((u64 *)&ctx->curr[0]); | 
|  | __raw_writeq(val, ctx->base); | 
|  | #elif BITS_PER_LONG == 32 | 
|  | val = *((u32 *)&ctx->curr[0]); | 
|  | __raw_writel(val, ctx->base); | 
|  | val = *((u32 *)&ctx->curr[4]); | 
|  | __raw_writel(val, ctx->base + 4); | 
|  | #else | 
|  | # error Not 32 or 64 bit | 
|  | #endif | 
|  | } | 
|  |  | 
|  | static struct img_ascii_lcd_config boston_config = { | 
|  | .num_chars = 8, | 
|  | .update = boston_update, | 
|  | }; | 
|  |  | 
|  | /* | 
|  | * MIPS Malta development board | 
|  | */ | 
|  |  | 
|  | static void malta_update(struct img_ascii_lcd_ctx *ctx) | 
|  | { | 
|  | unsigned int i; | 
|  | int err = 0; | 
|  |  | 
|  | for (i = 0; i < ctx->cfg->num_chars; i++) { | 
|  | err = regmap_write(ctx->regmap, | 
|  | ctx->offset + (i * 8), ctx->curr[i]); | 
|  | if (err) | 
|  | break; | 
|  | } | 
|  |  | 
|  | if (unlikely(err)) | 
|  | pr_err_ratelimited("Failed to update LCD display: %d\n", err); | 
|  | } | 
|  |  | 
|  | static struct img_ascii_lcd_config malta_config = { | 
|  | .num_chars = 8, | 
|  | .external_regmap = true, | 
|  | .update = malta_update, | 
|  | }; | 
|  |  | 
|  | /* | 
|  | * MIPS SEAD3 development board | 
|  | */ | 
|  |  | 
|  | enum { | 
|  | SEAD3_REG_LCD_CTRL		= 0x00, | 
|  | #define SEAD3_REG_LCD_CTRL_SETDRAM	BIT(7) | 
|  | SEAD3_REG_LCD_DATA		= 0x08, | 
|  | SEAD3_REG_CPLD_STATUS		= 0x10, | 
|  | #define SEAD3_REG_CPLD_STATUS_BUSY	BIT(0) | 
|  | SEAD3_REG_CPLD_DATA		= 0x18, | 
|  | #define SEAD3_REG_CPLD_DATA_BUSY	BIT(7) | 
|  | }; | 
|  |  | 
|  | static int sead3_wait_sm_idle(struct img_ascii_lcd_ctx *ctx) | 
|  | { | 
|  | unsigned int status; | 
|  | int err; | 
|  |  | 
|  | do { | 
|  | err = regmap_read(ctx->regmap, | 
|  | ctx->offset + SEAD3_REG_CPLD_STATUS, | 
|  | &status); | 
|  | if (err) | 
|  | return err; | 
|  | } while (status & SEAD3_REG_CPLD_STATUS_BUSY); | 
|  |  | 
|  | return 0; | 
|  |  | 
|  | } | 
|  |  | 
|  | static int sead3_wait_lcd_idle(struct img_ascii_lcd_ctx *ctx) | 
|  | { | 
|  | unsigned int cpld_data; | 
|  | int err; | 
|  |  | 
|  | err = sead3_wait_sm_idle(ctx); | 
|  | if (err) | 
|  | return err; | 
|  |  | 
|  | do { | 
|  | err = regmap_read(ctx->regmap, | 
|  | ctx->offset + SEAD3_REG_LCD_CTRL, | 
|  | &cpld_data); | 
|  | if (err) | 
|  | return err; | 
|  |  | 
|  | err = sead3_wait_sm_idle(ctx); | 
|  | if (err) | 
|  | return err; | 
|  |  | 
|  | err = regmap_read(ctx->regmap, | 
|  | ctx->offset + SEAD3_REG_CPLD_DATA, | 
|  | &cpld_data); | 
|  | if (err) | 
|  | return err; | 
|  | } while (cpld_data & SEAD3_REG_CPLD_DATA_BUSY); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static void sead3_update(struct img_ascii_lcd_ctx *ctx) | 
|  | { | 
|  | unsigned int i; | 
|  | int err = 0; | 
|  |  | 
|  | for (i = 0; i < ctx->cfg->num_chars; i++) { | 
|  | err = sead3_wait_lcd_idle(ctx); | 
|  | if (err) | 
|  | break; | 
|  |  | 
|  | err = regmap_write(ctx->regmap, | 
|  | ctx->offset + SEAD3_REG_LCD_CTRL, | 
|  | SEAD3_REG_LCD_CTRL_SETDRAM | i); | 
|  | if (err) | 
|  | break; | 
|  |  | 
|  | err = sead3_wait_lcd_idle(ctx); | 
|  | if (err) | 
|  | break; | 
|  |  | 
|  | err = regmap_write(ctx->regmap, | 
|  | ctx->offset + SEAD3_REG_LCD_DATA, | 
|  | ctx->curr[i]); | 
|  | if (err) | 
|  | break; | 
|  | } | 
|  |  | 
|  | if (unlikely(err)) | 
|  | pr_err_ratelimited("Failed to update LCD display: %d\n", err); | 
|  | } | 
|  |  | 
|  | static struct img_ascii_lcd_config sead3_config = { | 
|  | .num_chars = 16, | 
|  | .external_regmap = true, | 
|  | .update = sead3_update, | 
|  | }; | 
|  |  | 
|  | static const struct of_device_id img_ascii_lcd_matches[] = { | 
|  | { .compatible = "img,boston-lcd", .data = &boston_config }, | 
|  | { .compatible = "mti,malta-lcd", .data = &malta_config }, | 
|  | { .compatible = "mti,sead3-lcd", .data = &sead3_config }, | 
|  | { /* sentinel */ } | 
|  | }; | 
|  | MODULE_DEVICE_TABLE(of, img_ascii_lcd_matches); | 
|  |  | 
|  | /** | 
|  | * img_ascii_lcd_scroll() - scroll the display by a character | 
|  | * @t: really a pointer to the private data structure | 
|  | * | 
|  | * Scroll the current message along the LCD by one character, rearming the | 
|  | * timer if required. | 
|  | */ | 
|  | static void img_ascii_lcd_scroll(struct timer_list *t) | 
|  | { | 
|  | struct img_ascii_lcd_ctx *ctx = from_timer(ctx, t, timer); | 
|  | unsigned int i, ch = ctx->scroll_pos; | 
|  | unsigned int num_chars = ctx->cfg->num_chars; | 
|  |  | 
|  | /* update the current message string */ | 
|  | for (i = 0; i < num_chars;) { | 
|  | /* copy as many characters from the string as possible */ | 
|  | for (; i < num_chars && ch < ctx->message_len; i++, ch++) | 
|  | ctx->curr[i] = ctx->message[ch]; | 
|  |  | 
|  | /* wrap around to the start of the string */ | 
|  | ch = 0; | 
|  | } | 
|  |  | 
|  | /* update the LCD */ | 
|  | ctx->cfg->update(ctx); | 
|  |  | 
|  | /* move on to the next character */ | 
|  | ctx->scroll_pos++; | 
|  | ctx->scroll_pos %= ctx->message_len; | 
|  |  | 
|  | /* rearm the timer */ | 
|  | if (ctx->message_len > ctx->cfg->num_chars) | 
|  | mod_timer(&ctx->timer, jiffies + ctx->scroll_rate); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * img_ascii_lcd_display() - set the message to be displayed | 
|  | * @ctx: pointer to the private data structure | 
|  | * @msg: the message to display | 
|  | * @count: length of msg, or -1 | 
|  | * | 
|  | * Display a new message @msg on the LCD. @msg can be longer than the number of | 
|  | * characters the LCD can display, in which case it will begin scrolling across | 
|  | * the LCD display. | 
|  | * | 
|  | * Return: 0 on success, -ENOMEM on memory allocation failure | 
|  | */ | 
|  | static int img_ascii_lcd_display(struct img_ascii_lcd_ctx *ctx, | 
|  | const char *msg, ssize_t count) | 
|  | { | 
|  | char *new_msg; | 
|  |  | 
|  | /* stop the scroll timer */ | 
|  | del_timer_sync(&ctx->timer); | 
|  |  | 
|  | if (count == -1) | 
|  | count = strlen(msg); | 
|  |  | 
|  | /* if the string ends with a newline, trim it */ | 
|  | if (msg[count - 1] == '\n') | 
|  | count--; | 
|  |  | 
|  | new_msg = devm_kmalloc(&ctx->pdev->dev, count + 1, GFP_KERNEL); | 
|  | if (!new_msg) | 
|  | return -ENOMEM; | 
|  |  | 
|  | memcpy(new_msg, msg, count); | 
|  | new_msg[count] = 0; | 
|  |  | 
|  | if (ctx->message) | 
|  | devm_kfree(&ctx->pdev->dev, ctx->message); | 
|  |  | 
|  | ctx->message = new_msg; | 
|  | ctx->message_len = count; | 
|  | ctx->scroll_pos = 0; | 
|  |  | 
|  | /* update the LCD */ | 
|  | img_ascii_lcd_scroll(&ctx->timer); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * message_show() - read message via sysfs | 
|  | * @dev: the LCD device | 
|  | * @attr: the LCD message attribute | 
|  | * @buf: the buffer to read the message into | 
|  | * | 
|  | * Read the current message being displayed or scrolled across the LCD display | 
|  | * into @buf, for reads from sysfs. | 
|  | * | 
|  | * Return: the number of characters written to @buf | 
|  | */ | 
|  | static ssize_t message_show(struct device *dev, struct device_attribute *attr, | 
|  | char *buf) | 
|  | { | 
|  | struct img_ascii_lcd_ctx *ctx = dev_get_drvdata(dev); | 
|  |  | 
|  | return sprintf(buf, "%s\n", ctx->message); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * message_store() - write a new message via sysfs | 
|  | * @dev: the LCD device | 
|  | * @attr: the LCD message attribute | 
|  | * @buf: the buffer containing the new message | 
|  | * @count: the size of the message in @buf | 
|  | * | 
|  | * Write a new message to display or scroll across the LCD display from sysfs. | 
|  | * | 
|  | * Return: the size of the message on success, else -ERRNO | 
|  | */ | 
|  | static ssize_t message_store(struct device *dev, struct device_attribute *attr, | 
|  | const char *buf, size_t count) | 
|  | { | 
|  | struct img_ascii_lcd_ctx *ctx = dev_get_drvdata(dev); | 
|  | int err; | 
|  |  | 
|  | err = img_ascii_lcd_display(ctx, buf, count); | 
|  | return err ?: count; | 
|  | } | 
|  |  | 
|  | static DEVICE_ATTR_RW(message); | 
|  |  | 
|  | /** | 
|  | * img_ascii_lcd_probe() - probe an LCD display device | 
|  | * @pdev: the LCD platform device | 
|  | * | 
|  | * Probe an LCD display device, ensuring that we have the required resources in | 
|  | * order to access the LCD & setting up private data as well as sysfs files. | 
|  | * | 
|  | * Return: 0 on success, else -ERRNO | 
|  | */ | 
|  | static int img_ascii_lcd_probe(struct platform_device *pdev) | 
|  | { | 
|  | const struct of_device_id *match; | 
|  | const struct img_ascii_lcd_config *cfg; | 
|  | struct img_ascii_lcd_ctx *ctx; | 
|  | int err; | 
|  |  | 
|  | match = of_match_device(img_ascii_lcd_matches, &pdev->dev); | 
|  | if (!match) | 
|  | return -ENODEV; | 
|  |  | 
|  | cfg = match->data; | 
|  | ctx = devm_kzalloc(&pdev->dev, sizeof(*ctx) + cfg->num_chars, | 
|  | GFP_KERNEL); | 
|  | if (!ctx) | 
|  | return -ENOMEM; | 
|  |  | 
|  | if (cfg->external_regmap) { | 
|  | ctx->regmap = syscon_node_to_regmap(pdev->dev.parent->of_node); | 
|  | if (IS_ERR(ctx->regmap)) | 
|  | return PTR_ERR(ctx->regmap); | 
|  |  | 
|  | if (of_property_read_u32(pdev->dev.of_node, "offset", | 
|  | &ctx->offset)) | 
|  | return -EINVAL; | 
|  | } else { | 
|  | ctx->base = devm_platform_ioremap_resource(pdev, 0); | 
|  | if (IS_ERR(ctx->base)) | 
|  | return PTR_ERR(ctx->base); | 
|  | } | 
|  |  | 
|  | ctx->pdev = pdev; | 
|  | ctx->cfg = cfg; | 
|  | ctx->message = NULL; | 
|  | ctx->scroll_pos = 0; | 
|  | ctx->scroll_rate = HZ / 2; | 
|  |  | 
|  | /* initialise a timer for scrolling the message */ | 
|  | timer_setup(&ctx->timer, img_ascii_lcd_scroll, 0); | 
|  |  | 
|  | platform_set_drvdata(pdev, ctx); | 
|  |  | 
|  | /* display a default message */ | 
|  | err = img_ascii_lcd_display(ctx, "Linux " UTS_RELEASE "       ", -1); | 
|  | if (err) | 
|  | goto out_del_timer; | 
|  |  | 
|  | err = device_create_file(&pdev->dev, &dev_attr_message); | 
|  | if (err) | 
|  | goto out_del_timer; | 
|  |  | 
|  | return 0; | 
|  | out_del_timer: | 
|  | del_timer_sync(&ctx->timer); | 
|  | return err; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * img_ascii_lcd_remove() - remove an LCD display device | 
|  | * @pdev: the LCD platform device | 
|  | * | 
|  | * Remove an LCD display device, freeing private resources & ensuring that the | 
|  | * driver stops using the LCD display registers. | 
|  | * | 
|  | * Return: 0 | 
|  | */ | 
|  | static int img_ascii_lcd_remove(struct platform_device *pdev) | 
|  | { | 
|  | struct img_ascii_lcd_ctx *ctx = platform_get_drvdata(pdev); | 
|  |  | 
|  | device_remove_file(&pdev->dev, &dev_attr_message); | 
|  | del_timer_sync(&ctx->timer); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static struct platform_driver img_ascii_lcd_driver = { | 
|  | .driver = { | 
|  | .name		= "img-ascii-lcd", | 
|  | .of_match_table	= img_ascii_lcd_matches, | 
|  | }, | 
|  | .probe	= img_ascii_lcd_probe, | 
|  | .remove	= img_ascii_lcd_remove, | 
|  | }; | 
|  | module_platform_driver(img_ascii_lcd_driver); | 
|  |  | 
|  | MODULE_DESCRIPTION("Imagination Technologies ASCII LCD Display"); | 
|  | MODULE_AUTHOR("Paul Burton <paul.burton@mips.com>"); | 
|  | MODULE_LICENSE("GPL"); |