diff options
Diffstat (limited to '')
-rw-r--r-- | drivers/auxdisplay/Kconfig | 519 | ||||
-rw-r--r-- | drivers/auxdisplay/Makefile | 16 | ||||
-rw-r--r-- | drivers/auxdisplay/arm-charlcd.c | 368 | ||||
-rw-r--r-- | drivers/auxdisplay/cfag12864b.c | 380 | ||||
-rw-r--r-- | drivers/auxdisplay/cfag12864bfb.c | 172 | ||||
-rw-r--r-- | drivers/auxdisplay/charlcd.c | 681 | ||||
-rw-r--r-- | drivers/auxdisplay/charlcd.h | 104 | ||||
-rw-r--r-- | drivers/auxdisplay/hd44780.c | 353 | ||||
-rw-r--r-- | drivers/auxdisplay/hd44780_common.c | 369 | ||||
-rw-r--r-- | drivers/auxdisplay/hd44780_common.h | 33 | ||||
-rw-r--r-- | drivers/auxdisplay/ht16k33.c | 835 | ||||
-rw-r--r-- | drivers/auxdisplay/img-ascii-lcd.c | 306 | ||||
-rw-r--r-- | drivers/auxdisplay/ks0108.c | 172 | ||||
-rw-r--r-- | drivers/auxdisplay/lcd2s.c | 376 | ||||
-rw-r--r-- | drivers/auxdisplay/line-display.c | 261 | ||||
-rw-r--r-- | drivers/auxdisplay/line-display.h | 43 | ||||
-rw-r--r-- | drivers/auxdisplay/panel.c | 1739 |
17 files changed, 6727 insertions, 0 deletions
diff --git a/drivers/auxdisplay/Kconfig b/drivers/auxdisplay/Kconfig new file mode 100644 index 000000000..64012cda4 --- /dev/null +++ b/drivers/auxdisplay/Kconfig @@ -0,0 +1,519 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# For a description of the syntax of this configuration file, +# see Documentation/kbuild/kconfig-language.rst. +# +# Auxiliary display drivers configuration. +# + +menuconfig AUXDISPLAY + bool "Auxiliary Display support" + help + Say Y here to get to see options for auxiliary display drivers. + This option alone does not add any kernel code. + + If you say N, all options in this submenu will be skipped and disabled. + +if AUXDISPLAY + +config CHARLCD + tristate "Character LCD core support" if COMPILE_TEST + help + This is the base system for character-based LCD displays. + It makes no sense to have this alone, you select your display driver + and if it needs the charlcd core, it will select it automatically. + This is some character LCD core interface that multiple drivers can + use. + +config LINEDISP + tristate "Character line display core support" if COMPILE_TEST + help + This is the core support for single-line character displays, to be + selected by drivers that use it. + +config HD44780_COMMON + tristate "Common functions for HD44780 (and compatibles) LCD displays" if COMPILE_TEST + select CHARLCD + help + This is a module with the common symbols for HD44780 (and compatibles) + displays. This is the code that multiple other modules use. It is not + useful alone. If you have some sort of HD44780 compatible display, + you very likely use this. It is selected automatically by selecting + your concrete display. + +config HD44780 + tristate "HD44780 Character LCD support" + depends on GPIOLIB || COMPILE_TEST + select HD44780_COMMON + help + Enable support for Character LCDs using a HD44780 controller. + The LCD is accessible through the /dev/lcd char device (10, 156). + This code can either be compiled as a module, or linked into the + kernel and started at boot. + If you don't understand what all this is about, say N. + +config KS0108 + tristate "KS0108 LCD Controller" + depends on PARPORT_PC + default n + help + If you have a LCD controlled by one or more KS0108 + controllers, say Y. You will need also another more specific + driver for your LCD. + + Depends on Parallel Port support. If you say Y at + parport, you will be able to compile this as a module (M) + and built-in as well (Y). + + To compile this as a module, choose M here: + the module will be called ks0108. + + If unsure, say N. + +config KS0108_PORT + hex "Parallel port where the LCD is connected" + depends on KS0108 + default 0x378 + help + The address of the parallel port where the LCD is connected. + + The first standard parallel port address is 0x378. + The second standard parallel port address is 0x278. + The third standard parallel port address is 0x3BC. + + You can specify a different address if you need. + + If you don't know what I'm talking about, load the parport module, + and execute "dmesg" or "cat /proc/ioports". You can see there how + many parallel ports are present and which address each one has. + + Usually you only need to use 0x378. + + If you compile this as a module, you can still override this + using the module parameters. + +config KS0108_DELAY + int "Delay between each control writing (microseconds)" + depends on KS0108 + default "2" + help + Amount of time the ks0108 should wait between each control write + to the parallel port. + + If your LCD seems to miss random writings, increment this. + + If you don't know what I'm talking about, ignore it. + + If you compile this as a module, you can still override this + value using the module parameters. + +config CFAG12864B + tristate "CFAG12864B LCD" + depends on X86 + depends on FB + depends on KS0108 + select FB_SYS_FILLRECT + select FB_SYS_COPYAREA + select FB_SYS_IMAGEBLIT + select FB_SYS_FOPS + default n + help + If you have a Crystalfontz 128x64 2-color LCD, cfag12864b Series, + say Y. You also need the ks0108 LCD Controller driver. + + For help about how to wire your LCD to the parallel port, + check Documentation/admin-guide/auxdisplay/cfag12864b.rst + + Depends on the x86 arch and the framebuffer support. + + The LCD framebuffer driver can be attached to a console. + It will work fine. However, you can't attach it to the fbdev driver + of the xorg server. + + To compile this as a module, choose M here: + the modules will be called cfag12864b and cfag12864bfb. + + If unsure, say N. + +config CFAG12864B_RATE + int "Refresh rate (hertz)" + depends on CFAG12864B + default "20" + help + Refresh rate of the LCD. + + As the LCD is not memory mapped, the driver has to make the work by + software. This means you should be careful setting this value higher. + If your CPUs are really slow or you feel the system is slowed down, + decrease the value. + + Be careful modifying this value to a very high value: + You can freeze the computer, or the LCD maybe can't draw as fast as you + are requesting. + + If you don't know what I'm talking about, ignore it. + + If you compile this as a module, you can still override this + value using the module parameters. + +config IMG_ASCII_LCD + tristate "Imagination Technologies ASCII LCD Display" + depends on HAS_IOMEM + default y if MIPS_MALTA + select MFD_SYSCON + select LINEDISP + help + Enable this to support the simple ASCII LCD displays found on + development boards such as the MIPS Boston, MIPS Malta & MIPS SEAD3 + from Imagination Technologies. + +config HT16K33 + tristate "Holtek Ht16K33 LED controller with keyscan" + depends on FB && I2C && INPUT + select FB_SYS_FOPS + select FB_SYS_FILLRECT + select FB_SYS_COPYAREA + select FB_SYS_IMAGEBLIT + select INPUT_MATRIXKMAP + select FB_BACKLIGHT + select NEW_LEDS + select LEDS_CLASS + select LINEDISP + help + Say yes here to add support for Holtek HT16K33, RAM mapping 16*8 + LED controller driver with keyscan. + +config LCD2S + tristate "lcd2s 20x4 character display over I2C console" + depends on I2C + select CHARLCD + help + This is a driver that lets you use the lcd2s 20x4 character display + from Modtronix engineering as a console output device. The display + is a simple single color character display. You have to connect it + to an I2C bus. + +config ARM_CHARLCD + bool "ARM Ltd. Character LCD Driver" + depends on PLAT_VERSATILE + help + This is a driver for the character LCD found on the ARM Ltd. + Versatile and RealView Platform Baseboards. It doesn't do + very much more than display the text "ARM Linux" on the first + line and the Linux version on the second line, but that's + still useful. + +menuconfig PARPORT_PANEL + tristate "Parallel port LCD/Keypad Panel support" + depends on PARPORT + select HD44780_COMMON + help + Say Y here if you have an HD44780 or KS-0074 LCD connected to your + parallel port. This driver also features 4 and 6-key keypads. The LCD + is accessible through the /dev/lcd char device (10, 156), and the + keypad through /dev/keypad (10, 185). This code can either be + compiled as a module, or linked into the kernel and started at boot. + If you don't understand what all this is about, say N. + +if PARPORT_PANEL + +config PANEL_PARPORT + int "Default parallel port number (0=LPT1)" + range 0 255 + default "0" + help + This is the index of the parallel port the panel is connected to. One + driver instance only supports one parallel port, so if your keypad + and LCD are connected to two separate ports, you have to start two + modules with different arguments. Numbering starts with '0' for LPT1, + and so on. + +config PANEL_PROFILE + int "Default panel profile (0-5, 0=custom)" + range 0 5 + default "5" + help + To ease configuration, the driver supports different configuration + profiles for past and recent wirings. These profiles can also be + used to define an approximative configuration, completed by a few + other options. Here are the profiles : + + 0 = custom (see further) + 1 = 2x16 parallel LCD, old keypad + 2 = 2x16 serial LCD (KS-0074), new keypad + 3 = 2x16 parallel LCD (Hantronix), no keypad + 4 = 2x16 parallel LCD (Nexcom NSA1045) with Nexcom's keypad + 5 = 2x40 parallel LCD (old one), with old keypad + + Custom configurations allow you to define how your display is + wired to the parallel port, and how it works. This is only intended + for experts. + +config PANEL_KEYPAD + depends on PANEL_PROFILE="0" + int "Keypad type (0=none, 1=old 6 keys, 2=new 6 keys, 3=Nexcom 4 keys)" + range 0 3 + default 0 + help + This enables and configures a keypad connected to the parallel port. + The keys will be read from character device 10,185. Valid values are : + + 0 : do not enable this driver + 1 : old 6 keys keypad + 2 : new 6 keys keypad, as used on the server at www.ant-computing.com + 3 : Nexcom NSA1045's 4 keys keypad + + New profiles can be described in the driver source. The driver also + supports simultaneous keys pressed when the keypad supports them. + +config PANEL_LCD + depends on PANEL_PROFILE="0" + int "LCD type (0=none, 1=custom, 2=old //, 3=ks0074, 4=hantronix, 5=Nexcom)" + range 0 5 + default 0 + help + This enables and configures an LCD connected to the parallel port. + The driver includes an interpreter for escape codes starting with + '\e[L' which are specific to the LCD, and a few ANSI codes. The + driver will be registered as character device 10,156, usually + under the name '/dev/lcd'. There are a total of 6 supported types : + + 0 : do not enable the driver + 1 : custom configuration and wiring (see further) + 2 : 2x16 & 2x40 parallel LCD (old wiring) + 3 : 2x16 serial LCD (KS-0074 based) + 4 : 2x16 parallel LCD (Hantronix wiring) + 5 : 2x16 parallel LCD (Nexcom wiring) + + When type '1' is specified, other options will appear to configure + more precise aspects (wiring, dimensions, protocol, ...). Please note + that those values changed from the 2.4 driver for better consistency. + +config PANEL_LCD_HEIGHT + depends on PANEL_PROFILE="0" && PANEL_LCD="1" + int "Number of lines on the LCD (1-2)" + range 1 2 + default 2 + help + This is the number of visible character lines on the LCD in custom profile. + It can either be 1 or 2. + +config PANEL_LCD_WIDTH + depends on PANEL_PROFILE="0" && PANEL_LCD="1" + int "Number of characters per line on the LCD (1-40)" + range 1 40 + default 40 + help + This is the number of characters per line on the LCD in custom profile. + Common values are 16,20,24,40. + +config PANEL_LCD_BWIDTH + depends on PANEL_PROFILE="0" && PANEL_LCD="1" + int "Internal LCD line width (1-40, 40 by default)" + range 1 40 + default 40 + help + Most LCDs use a standard controller which supports hardware lines of 40 + characters, although sometimes only 16, 20 or 24 of them are really wired + to the terminal. This results in some non-visible but addressable characters, + and is the case for most parallel LCDs. Other LCDs, and some serial ones, + however, use the same line width internally as what is visible. The KS0074 + for example, uses 16 characters per line for 16 visible characters per line. + + This option lets you configure the value used by your LCD in 'custom' profile. + If you don't know, put '40' here. + +config PANEL_LCD_HWIDTH + depends on PANEL_PROFILE="0" && PANEL_LCD="1" + int "Hardware LCD line width (1-64, 64 by default)" + range 1 64 + default 64 + help + Most LCDs use a single address bit to differentiate line 0 and line 1. Since + some of them need to be able to address 40 chars with the lower bits, they + often use the immediately superior power of 2, which is 64, to address the + next line. + + If you don't know what your LCD uses, in doubt let 16 here for a 2x16, and + 64 here for a 2x40. + +config PANEL_LCD_CHARSET + depends on PANEL_PROFILE="0" && PANEL_LCD="1" + int "LCD character set (0=normal, 1=KS0074)" + range 0 1 + default 0 + help + Some controllers such as the KS0074 use a somewhat strange character set + where many symbols are at unusual places. The driver knows how to map + 'standard' ASCII characters to the character sets used by these controllers. + Valid values are : + + 0 : normal (untranslated) character set + 1 : KS0074 character set + + If you don't know, use the normal one (0). + +config PANEL_LCD_PROTO + depends on PANEL_PROFILE="0" && PANEL_LCD="1" + int "LCD communication mode (0=parallel 8 bits, 1=serial)" + range 0 1 + default 0 + help + This driver now supports any serial or parallel LCD wired to a parallel + port. But before assigning signals, the driver needs to know if it will + be driving a serial LCD or a parallel one. Serial LCDs only use 2 wires + (SDA/SCL), while parallel ones use 2 or 3 wires for the control signals + (E, RS, sometimes RW), and 4 or 8 for the data. Use 0 here for a 8 bits + parallel LCD, and 1 for a serial LCD. + +config PANEL_LCD_PIN_E + depends on PANEL_PROFILE="0" && PANEL_LCD="1" && PANEL_LCD_PROTO="0" + int "Parallel port pin number & polarity connected to the LCD E signal (-17...17) " + range -17 17 + default 14 + help + This describes the number of the parallel port pin to which the LCD 'E' + signal has been connected. It can be : + + 0 : no connection (eg: connected to ground) + 1..17 : directly connected to any of these pins on the DB25 plug + -1..-17 : connected to the same pin through an inverter (eg: transistor). + + Default for the 'E' pin in custom profile is '14' (AUTOFEED). + +config PANEL_LCD_PIN_RS + depends on PANEL_PROFILE="0" && PANEL_LCD="1" && PANEL_LCD_PROTO="0" + int "Parallel port pin number & polarity connected to the LCD RS signal (-17...17) " + range -17 17 + default 17 + help + This describes the number of the parallel port pin to which the LCD 'RS' + signal has been connected. It can be : + + 0 : no connection (eg: connected to ground) + 1..17 : directly connected to any of these pins on the DB25 plug + -1..-17 : connected to the same pin through an inverter (eg: transistor). + + Default for the 'RS' pin in custom profile is '17' (SELECT IN). + +config PANEL_LCD_PIN_RW + depends on PANEL_PROFILE="0" && PANEL_LCD="1" && PANEL_LCD_PROTO="0" + int "Parallel port pin number & polarity connected to the LCD RW signal (-17...17) " + range -17 17 + default 16 + help + This describes the number of the parallel port pin to which the LCD 'RW' + signal has been connected. It can be : + + 0 : no connection (eg: connected to ground) + 1..17 : directly connected to any of these pins on the DB25 plug + -1..-17 : connected to the same pin through an inverter (eg: transistor). + + Default for the 'RW' pin in custom profile is '16' (INIT). + +config PANEL_LCD_PIN_SCL + depends on PANEL_PROFILE="0" && PANEL_LCD="1" && PANEL_LCD_PROTO!="0" + int "Parallel port pin number & polarity connected to the LCD SCL signal (-17...17) " + range -17 17 + default 1 + help + This describes the number of the parallel port pin to which the serial + LCD 'SCL' signal has been connected. It can be : + + 0 : no connection (eg: connected to ground) + 1..17 : directly connected to any of these pins on the DB25 plug + -1..-17 : connected to the same pin through an inverter (eg: transistor). + + Default for the 'SCL' pin in custom profile is '1' (STROBE). + +config PANEL_LCD_PIN_SDA + depends on PANEL_PROFILE="0" && PANEL_LCD="1" && PANEL_LCD_PROTO!="0" + int "Parallel port pin number & polarity connected to the LCD SDA signal (-17...17) " + range -17 17 + default 2 + help + This describes the number of the parallel port pin to which the serial + LCD 'SDA' signal has been connected. It can be : + + 0 : no connection (eg: connected to ground) + 1..17 : directly connected to any of these pins on the DB25 plug + -1..-17 : connected to the same pin through an inverter (eg: transistor). + + Default for the 'SDA' pin in custom profile is '2' (D0). + +config PANEL_LCD_PIN_BL + depends on PANEL_PROFILE="0" && PANEL_LCD="1" + int "Parallel port pin number & polarity connected to the LCD backlight signal (-17...17) " + range -17 17 + default 0 + help + This describes the number of the parallel port pin to which the LCD 'BL' signal + has been connected. It can be : + + 0 : no connection (eg: connected to ground) + 1..17 : directly connected to any of these pins on the DB25 plug + -1..-17 : connected to the same pin through an inverter (eg: transistor). + + Default for the 'BL' pin in custom profile is '0' (uncontrolled). + +endif # PARPORT_PANEL + +config PANEL_CHANGE_MESSAGE + bool "Change LCD initialization message ?" + depends on CHARLCD + default "n" + help + This allows you to replace the boot message indicating the kernel version + and the driver version with a custom message. This is useful on appliances + where a simple 'Starting system' message can be enough to stop a customer + from worrying. + + If you say 'Y' here, you'll be able to choose a message yourself. Otherwise, + say 'N' and keep the default message with the version. + +config PANEL_BOOT_MESSAGE + depends on PANEL_CHANGE_MESSAGE="y" + string "New initialization message" + default "" + help + This allows you to replace the boot message indicating the kernel version + and the driver version with a custom message. This is useful on appliances + where a simple 'Starting system' message can be enough to stop a customer + from worrying. + + An empty message will only clear the display at driver init time. Any other + printf()-formatted message is valid with newline and escape codes. + +choice + prompt "Backlight initial state" + default CHARLCD_BL_FLASH + help + Select the initial backlight state on boot or module load. + + Previously, there was no option for this: the backlight flashed + briefly on init. Now you can also turn it off/on. + + config CHARLCD_BL_OFF + bool "Off" + help + Backlight is initially turned off + + config CHARLCD_BL_ON + bool "On" + help + Backlight is initially turned on + + config CHARLCD_BL_FLASH + bool "Flash" + help + Backlight is flashed briefly on init + +endchoice + +endif # AUXDISPLAY + +config PANEL + tristate "Parallel port LCD/Keypad Panel support (OLD OPTION)" + depends on PARPORT + select AUXDISPLAY + select PARPORT_PANEL diff --git a/drivers/auxdisplay/Makefile b/drivers/auxdisplay/Makefile new file mode 100644 index 000000000..6968ed4d3 --- /dev/null +++ b/drivers/auxdisplay/Makefile @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for the kernel auxiliary displays device drivers. +# + +obj-$(CONFIG_CHARLCD) += charlcd.o +obj-$(CONFIG_HD44780_COMMON) += hd44780_common.o +obj-$(CONFIG_ARM_CHARLCD) += arm-charlcd.o +obj-$(CONFIG_KS0108) += ks0108.o +obj-$(CONFIG_CFAG12864B) += cfag12864b.o cfag12864bfb.o +obj-$(CONFIG_IMG_ASCII_LCD) += img-ascii-lcd.o +obj-$(CONFIG_HD44780) += hd44780.o +obj-$(CONFIG_HT16K33) += ht16k33.o +obj-$(CONFIG_PARPORT_PANEL) += panel.o +obj-$(CONFIG_LCD2S) += lcd2s.o +obj-$(CONFIG_LINEDISP) += line-display.o diff --git a/drivers/auxdisplay/arm-charlcd.c b/drivers/auxdisplay/arm-charlcd.c new file mode 100644 index 000000000..0b1c99cca --- /dev/null +++ b/drivers/auxdisplay/arm-charlcd.c @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Driver for the on-board character LCD found on some ARM reference boards + * This is basically an Hitachi HD44780 LCD with a custom IP block to drive it + * https://en.wikipedia.org/wiki/HD44780_Character_LCD + * Currently it will just display the text "ARM Linux" and the linux version + * + * Author: Linus Walleij <triad@df.lth.se> + */ +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/of.h> +#include <linux/completion.h> +#include <linux/delay.h> +#include <linux/io.h> +#include <linux/slab.h> +#include <linux/workqueue.h> +#include <generated/utsrelease.h> + +#define DRIVERNAME "arm-charlcd" +#define CHARLCD_TIMEOUT (msecs_to_jiffies(1000)) + +/* Offsets to registers */ +#define CHAR_COM 0x00U +#define CHAR_DAT 0x04U +#define CHAR_RD 0x08U +#define CHAR_RAW 0x0CU +#define CHAR_MASK 0x10U +#define CHAR_STAT 0x14U + +#define CHAR_RAW_CLEAR 0x00000000U +#define CHAR_RAW_VALID 0x00000100U + +/* Hitachi HD44780 display commands */ +#define HD_CLEAR 0x01U +#define HD_HOME 0x02U +#define HD_ENTRYMODE 0x04U +#define HD_ENTRYMODE_INCREMENT 0x02U +#define HD_ENTRYMODE_SHIFT 0x01U +#define HD_DISPCTRL 0x08U +#define HD_DISPCTRL_ON 0x04U +#define HD_DISPCTRL_CURSOR_ON 0x02U +#define HD_DISPCTRL_CURSOR_BLINK 0x01U +#define HD_CRSR_SHIFT 0x10U +#define HD_CRSR_SHIFT_DISPLAY 0x08U +#define HD_CRSR_SHIFT_DISPLAY_RIGHT 0x04U +#define HD_FUNCSET 0x20U +#define HD_FUNCSET_8BIT 0x10U +#define HD_FUNCSET_2_LINES 0x08U +#define HD_FUNCSET_FONT_5X10 0x04U +#define HD_SET_CGRAM 0x40U +#define HD_SET_DDRAM 0x80U +#define HD_BUSY_FLAG 0x80U + +/** + * struct charlcd - Private data structure + * @dev: a pointer back to containing device + * @phybase: the offset to the controller in physical memory + * @physize: the size of the physical page + * @virtbase: the offset to the controller in virtual memory + * @irq: reserved interrupt number + * @complete: completion structure for the last LCD command + * @init_work: delayed work structure to initialize the display on boot + */ +struct charlcd { + struct device *dev; + u32 phybase; + u32 physize; + void __iomem *virtbase; + int irq; + struct completion complete; + struct delayed_work init_work; +}; + +static irqreturn_t charlcd_interrupt(int irq, void *data) +{ + struct charlcd *lcd = data; + u8 status; + + status = readl(lcd->virtbase + CHAR_STAT) & 0x01; + /* Clear IRQ */ + writel(CHAR_RAW_CLEAR, lcd->virtbase + CHAR_RAW); + if (status) + complete(&lcd->complete); + else + dev_info(lcd->dev, "Spurious IRQ (%02x)\n", status); + return IRQ_HANDLED; +} + + +static void charlcd_wait_complete_irq(struct charlcd *lcd) +{ + int ret; + + ret = wait_for_completion_interruptible_timeout(&lcd->complete, + CHARLCD_TIMEOUT); + /* Disable IRQ after completion */ + writel(0x00, lcd->virtbase + CHAR_MASK); + + if (ret < 0) { + dev_err(lcd->dev, + "wait_for_completion_interruptible_timeout() " + "returned %d waiting for ready\n", ret); + return; + } + + if (ret == 0) { + dev_err(lcd->dev, "charlcd controller timed out " + "waiting for ready\n"); + return; + } +} + +static u8 charlcd_4bit_read_char(struct charlcd *lcd) +{ + u8 data; + u32 val; + int i; + + /* If we can, use an IRQ to wait for the data, else poll */ + if (lcd->irq >= 0) + charlcd_wait_complete_irq(lcd); + else { + i = 0; + val = 0; + while (!(val & CHAR_RAW_VALID) && i < 10) { + udelay(100); + val = readl(lcd->virtbase + CHAR_RAW); + i++; + } + + writel(CHAR_RAW_CLEAR, lcd->virtbase + CHAR_RAW); + } + msleep(1); + + /* Read the 4 high bits of the data */ + data = readl(lcd->virtbase + CHAR_RD) & 0xf0; + + /* + * The second read for the low bits does not trigger an IRQ + * so in this case we have to poll for the 4 lower bits + */ + i = 0; + val = 0; + while (!(val & CHAR_RAW_VALID) && i < 10) { + udelay(100); + val = readl(lcd->virtbase + CHAR_RAW); + i++; + } + writel(CHAR_RAW_CLEAR, lcd->virtbase + CHAR_RAW); + msleep(1); + + /* Read the 4 low bits of the data */ + data |= (readl(lcd->virtbase + CHAR_RD) >> 4) & 0x0f; + + return data; +} + +static bool charlcd_4bit_read_bf(struct charlcd *lcd) +{ + if (lcd->irq >= 0) { + /* + * If we'll use IRQs to wait for the busyflag, clear any + * pending flag and enable IRQ + */ + writel(CHAR_RAW_CLEAR, lcd->virtbase + CHAR_RAW); + init_completion(&lcd->complete); + writel(0x01, lcd->virtbase + CHAR_MASK); + } + readl(lcd->virtbase + CHAR_COM); + return charlcd_4bit_read_char(lcd) & HD_BUSY_FLAG ? true : false; +} + +static void charlcd_4bit_wait_busy(struct charlcd *lcd) +{ + int retries = 50; + + udelay(100); + while (charlcd_4bit_read_bf(lcd) && retries) + retries--; + if (!retries) + dev_err(lcd->dev, "timeout waiting for busyflag\n"); +} + +static void charlcd_4bit_command(struct charlcd *lcd, u8 cmd) +{ + u32 cmdlo = (cmd << 4) & 0xf0; + u32 cmdhi = (cmd & 0xf0); + + writel(cmdhi, lcd->virtbase + CHAR_COM); + udelay(10); + writel(cmdlo, lcd->virtbase + CHAR_COM); + charlcd_4bit_wait_busy(lcd); +} + +static void charlcd_4bit_char(struct charlcd *lcd, u8 ch) +{ + u32 chlo = (ch << 4) & 0xf0; + u32 chhi = (ch & 0xf0); + + writel(chhi, lcd->virtbase + CHAR_DAT); + udelay(10); + writel(chlo, lcd->virtbase + CHAR_DAT); + charlcd_4bit_wait_busy(lcd); +} + +static void charlcd_4bit_print(struct charlcd *lcd, int line, const char *str) +{ + u8 offset; + int i; + + /* + * We support line 0, 1 + * Line 1 runs from 0x00..0x27 + * Line 2 runs from 0x28..0x4f + */ + if (line == 0) + offset = 0; + else if (line == 1) + offset = 0x28; + else + return; + + /* Set offset */ + charlcd_4bit_command(lcd, HD_SET_DDRAM | offset); + + /* Send string */ + for (i = 0; i < strlen(str) && i < 0x28; i++) + charlcd_4bit_char(lcd, str[i]); +} + +static void charlcd_4bit_init(struct charlcd *lcd) +{ + /* These commands cannot be checked with the busy flag */ + writel(HD_FUNCSET | HD_FUNCSET_8BIT, lcd->virtbase + CHAR_COM); + msleep(5); + writel(HD_FUNCSET | HD_FUNCSET_8BIT, lcd->virtbase + CHAR_COM); + udelay(100); + writel(HD_FUNCSET | HD_FUNCSET_8BIT, lcd->virtbase + CHAR_COM); + udelay(100); + /* Go to 4bit mode */ + writel(HD_FUNCSET, lcd->virtbase + CHAR_COM); + udelay(100); + /* + * 4bit mode, 2 lines, 5x8 font, after this the number of lines + * and the font cannot be changed until the next initialization sequence + */ + charlcd_4bit_command(lcd, HD_FUNCSET | HD_FUNCSET_2_LINES); + charlcd_4bit_command(lcd, HD_DISPCTRL | HD_DISPCTRL_ON); + charlcd_4bit_command(lcd, HD_ENTRYMODE | HD_ENTRYMODE_INCREMENT); + charlcd_4bit_command(lcd, HD_CLEAR); + charlcd_4bit_command(lcd, HD_HOME); + /* Put something useful in the display */ + charlcd_4bit_print(lcd, 0, "ARM Linux"); + charlcd_4bit_print(lcd, 1, UTS_RELEASE); +} + +static void charlcd_init_work(struct work_struct *work) +{ + struct charlcd *lcd = + container_of(work, struct charlcd, init_work.work); + + charlcd_4bit_init(lcd); +} + +static int __init charlcd_probe(struct platform_device *pdev) +{ + int ret; + struct charlcd *lcd; + struct resource *res; + + lcd = kzalloc(sizeof(struct charlcd), GFP_KERNEL); + if (!lcd) + return -ENOMEM; + + lcd->dev = &pdev->dev; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) { + ret = -ENOENT; + goto out_no_resource; + } + lcd->phybase = res->start; + lcd->physize = resource_size(res); + + if (request_mem_region(lcd->phybase, lcd->physize, + DRIVERNAME) == NULL) { + ret = -EBUSY; + goto out_no_memregion; + } + + lcd->virtbase = ioremap(lcd->phybase, lcd->physize); + if (!lcd->virtbase) { + ret = -ENOMEM; + goto out_no_memregion; + } + + lcd->irq = platform_get_irq(pdev, 0); + /* If no IRQ is supplied, we'll survive without it */ + if (lcd->irq >= 0) { + if (request_irq(lcd->irq, charlcd_interrupt, 0, + DRIVERNAME, lcd)) { + ret = -EIO; + goto out_no_irq; + } + } + + platform_set_drvdata(pdev, lcd); + + /* + * Initialize the display in a delayed work, because + * it is VERY slow and would slow down the boot of the system. + */ + INIT_DELAYED_WORK(&lcd->init_work, charlcd_init_work); + schedule_delayed_work(&lcd->init_work, 0); + + dev_info(&pdev->dev, "initialized ARM character LCD at %08x\n", + lcd->phybase); + + return 0; + +out_no_irq: + iounmap(lcd->virtbase); +out_no_memregion: + release_mem_region(lcd->phybase, SZ_4K); +out_no_resource: + kfree(lcd); + return ret; +} + +static int charlcd_suspend(struct device *dev) +{ + struct charlcd *lcd = dev_get_drvdata(dev); + + /* Power the display off */ + charlcd_4bit_command(lcd, HD_DISPCTRL); + return 0; +} + +static int charlcd_resume(struct device *dev) +{ + struct charlcd *lcd = dev_get_drvdata(dev); + + /* Turn the display back on */ + charlcd_4bit_command(lcd, HD_DISPCTRL | HD_DISPCTRL_ON); + return 0; +} + +static const struct dev_pm_ops charlcd_pm_ops = { + .suspend = charlcd_suspend, + .resume = charlcd_resume, +}; + +static const struct of_device_id charlcd_match[] = { + { .compatible = "arm,versatile-lcd", }, + {} +}; + +static struct platform_driver charlcd_driver = { + .driver = { + .name = DRIVERNAME, + .pm = &charlcd_pm_ops, + .suppress_bind_attrs = true, + .of_match_table = of_match_ptr(charlcd_match), + }, +}; +builtin_platform_driver_probe(charlcd_driver, charlcd_probe); diff --git a/drivers/auxdisplay/cfag12864b.c b/drivers/auxdisplay/cfag12864b.c new file mode 100644 index 000000000..6526aa51f --- /dev/null +++ b/drivers/auxdisplay/cfag12864b.c @@ -0,0 +1,380 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Filename: cfag12864b.c + * Version: 0.1.0 + * Description: cfag12864b LCD driver + * Depends: ks0108 + * + * Author: Copyright (C) Miguel Ojeda <ojeda@kernel.org> + * Date: 2006-10-31 + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/fs.h> +#include <linux/slab.h> +#include <linux/cdev.h> +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/jiffies.h> +#include <linux/mutex.h> +#include <linux/uaccess.h> +#include <linux/vmalloc.h> +#include <linux/workqueue.h> +#include <linux/ks0108.h> +#include <linux/cfag12864b.h> + + +#define CFAG12864B_NAME "cfag12864b" + +/* + * Module Parameters + */ + +static unsigned int cfag12864b_rate = CONFIG_CFAG12864B_RATE; +module_param(cfag12864b_rate, uint, 0444); +MODULE_PARM_DESC(cfag12864b_rate, + "Refresh rate (hertz)"); + +unsigned int cfag12864b_getrate(void) +{ + return cfag12864b_rate; +} + +/* + * cfag12864b Commands + * + * E = Enable signal + * Every time E switch from low to high, + * cfag12864b/ks0108 reads the command/data. + * + * CS1 = First ks0108controller. + * If high, the first ks0108 controller receives commands/data. + * + * CS2 = Second ks0108 controller + * If high, the second ks0108 controller receives commands/data. + * + * DI = Data/Instruction + * If low, cfag12864b will expect commands. + * If high, cfag12864b will expect data. + * + */ + +#define bit(n) (((unsigned char)1)<<(n)) + +#define CFAG12864B_BIT_E (0) +#define CFAG12864B_BIT_CS1 (2) +#define CFAG12864B_BIT_CS2 (1) +#define CFAG12864B_BIT_DI (3) + +static unsigned char cfag12864b_state; + +static void cfag12864b_set(void) +{ + ks0108_writecontrol(cfag12864b_state); +} + +static void cfag12864b_setbit(unsigned char state, unsigned char n) +{ + if (state) + cfag12864b_state |= bit(n); + else + cfag12864b_state &= ~bit(n); +} + +static void cfag12864b_e(unsigned char state) +{ + cfag12864b_setbit(state, CFAG12864B_BIT_E); + cfag12864b_set(); +} + +static void cfag12864b_cs1(unsigned char state) +{ + cfag12864b_setbit(state, CFAG12864B_BIT_CS1); +} + +static void cfag12864b_cs2(unsigned char state) +{ + cfag12864b_setbit(state, CFAG12864B_BIT_CS2); +} + +static void cfag12864b_di(unsigned char state) +{ + cfag12864b_setbit(state, CFAG12864B_BIT_DI); +} + +static void cfag12864b_setcontrollers(unsigned char first, + unsigned char second) +{ + if (first) + cfag12864b_cs1(0); + else + cfag12864b_cs1(1); + + if (second) + cfag12864b_cs2(0); + else + cfag12864b_cs2(1); +} + +static void cfag12864b_controller(unsigned char which) +{ + if (which == 0) + cfag12864b_setcontrollers(1, 0); + else if (which == 1) + cfag12864b_setcontrollers(0, 1); +} + +static void cfag12864b_displaystate(unsigned char state) +{ + cfag12864b_di(0); + cfag12864b_e(1); + ks0108_displaystate(state); + cfag12864b_e(0); +} + +static void cfag12864b_address(unsigned char address) +{ + cfag12864b_di(0); + cfag12864b_e(1); + ks0108_address(address); + cfag12864b_e(0); +} + +static void cfag12864b_page(unsigned char page) +{ + cfag12864b_di(0); + cfag12864b_e(1); + ks0108_page(page); + cfag12864b_e(0); +} + +static void cfag12864b_startline(unsigned char startline) +{ + cfag12864b_di(0); + cfag12864b_e(1); + ks0108_startline(startline); + cfag12864b_e(0); +} + +static void cfag12864b_writebyte(unsigned char byte) +{ + cfag12864b_di(1); + cfag12864b_e(1); + ks0108_writedata(byte); + cfag12864b_e(0); +} + +static void cfag12864b_nop(void) +{ + cfag12864b_startline(0); +} + +/* + * cfag12864b Internal Commands + */ + +static void cfag12864b_on(void) +{ + cfag12864b_setcontrollers(1, 1); + cfag12864b_displaystate(1); +} + +static void cfag12864b_off(void) +{ + cfag12864b_setcontrollers(1, 1); + cfag12864b_displaystate(0); +} + +static void cfag12864b_clear(void) +{ + unsigned char i, j; + + cfag12864b_setcontrollers(1, 1); + for (i = 0; i < CFAG12864B_PAGES; i++) { + cfag12864b_page(i); + cfag12864b_address(0); + for (j = 0; j < CFAG12864B_ADDRESSES; j++) + cfag12864b_writebyte(0); + } +} + +/* + * Update work + */ + +unsigned char *cfag12864b_buffer; +static unsigned char *cfag12864b_cache; +static DEFINE_MUTEX(cfag12864b_mutex); +static unsigned char cfag12864b_updating; +static void cfag12864b_update(struct work_struct *delayed_work); +static struct workqueue_struct *cfag12864b_workqueue; +static DECLARE_DELAYED_WORK(cfag12864b_work, cfag12864b_update); + +static void cfag12864b_queue(void) +{ + queue_delayed_work(cfag12864b_workqueue, &cfag12864b_work, + HZ / cfag12864b_rate); +} + +unsigned char cfag12864b_enable(void) +{ + unsigned char ret; + + mutex_lock(&cfag12864b_mutex); + + if (!cfag12864b_updating) { + cfag12864b_updating = 1; + cfag12864b_queue(); + ret = 0; + } else + ret = 1; + + mutex_unlock(&cfag12864b_mutex); + + return ret; +} + +void cfag12864b_disable(void) +{ + mutex_lock(&cfag12864b_mutex); + + if (cfag12864b_updating) { + cfag12864b_updating = 0; + cancel_delayed_work(&cfag12864b_work); + flush_workqueue(cfag12864b_workqueue); + } + + mutex_unlock(&cfag12864b_mutex); +} + +unsigned char cfag12864b_isenabled(void) +{ + return cfag12864b_updating; +} + +static void cfag12864b_update(struct work_struct *work) +{ + unsigned char c; + unsigned short i, j, k, b; + + if (memcmp(cfag12864b_cache, cfag12864b_buffer, CFAG12864B_SIZE)) { + for (i = 0; i < CFAG12864B_CONTROLLERS; i++) { + cfag12864b_controller(i); + cfag12864b_nop(); + for (j = 0; j < CFAG12864B_PAGES; j++) { + cfag12864b_page(j); + cfag12864b_nop(); + cfag12864b_address(0); + cfag12864b_nop(); + for (k = 0; k < CFAG12864B_ADDRESSES; k++) { + for (c = 0, b = 0; b < 8; b++) + if (cfag12864b_buffer + [i * CFAG12864B_ADDRESSES / 8 + + k / 8 + (j * 8 + b) * + CFAG12864B_WIDTH / 8] + & bit(k % 8)) + c |= bit(b); + cfag12864b_writebyte(c); + } + } + } + + memcpy(cfag12864b_cache, cfag12864b_buffer, CFAG12864B_SIZE); + } + + if (cfag12864b_updating) + cfag12864b_queue(); +} + +/* + * cfag12864b Exported Symbols + */ + +EXPORT_SYMBOL_GPL(cfag12864b_buffer); +EXPORT_SYMBOL_GPL(cfag12864b_getrate); +EXPORT_SYMBOL_GPL(cfag12864b_enable); +EXPORT_SYMBOL_GPL(cfag12864b_disable); +EXPORT_SYMBOL_GPL(cfag12864b_isenabled); + +/* + * Is the module inited? + */ + +static unsigned char cfag12864b_inited; +unsigned char cfag12864b_isinited(void) +{ + return cfag12864b_inited; +} +EXPORT_SYMBOL_GPL(cfag12864b_isinited); + +/* + * Module Init & Exit + */ + +static int __init cfag12864b_init(void) +{ + int ret = -EINVAL; + + /* ks0108_init() must be called first */ + if (!ks0108_isinited()) { + printk(KERN_ERR CFAG12864B_NAME ": ERROR: " + "ks0108 is not initialized\n"); + goto none; + } + BUILD_BUG_ON(PAGE_SIZE < CFAG12864B_SIZE); + + cfag12864b_buffer = (unsigned char *) get_zeroed_page(GFP_KERNEL); + if (cfag12864b_buffer == NULL) { + printk(KERN_ERR CFAG12864B_NAME ": ERROR: " + "can't get a free page\n"); + ret = -ENOMEM; + goto none; + } + + cfag12864b_cache = kmalloc(CFAG12864B_SIZE, + GFP_KERNEL); + if (cfag12864b_cache == NULL) { + printk(KERN_ERR CFAG12864B_NAME ": ERROR: " + "can't alloc cache buffer (%i bytes)\n", + CFAG12864B_SIZE); + ret = -ENOMEM; + goto bufferalloced; + } + + cfag12864b_workqueue = create_singlethread_workqueue(CFAG12864B_NAME); + if (cfag12864b_workqueue == NULL) + goto cachealloced; + + cfag12864b_clear(); + cfag12864b_on(); + + cfag12864b_inited = 1; + return 0; + +cachealloced: + kfree(cfag12864b_cache); + +bufferalloced: + free_page((unsigned long) cfag12864b_buffer); + +none: + return ret; +} + +static void __exit cfag12864b_exit(void) +{ + cfag12864b_disable(); + cfag12864b_off(); + destroy_workqueue(cfag12864b_workqueue); + kfree(cfag12864b_cache); + free_page((unsigned long) cfag12864b_buffer); +} + +module_init(cfag12864b_init); +module_exit(cfag12864b_exit); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Miguel Ojeda <ojeda@kernel.org>"); +MODULE_DESCRIPTION("cfag12864b LCD driver"); diff --git a/drivers/auxdisplay/cfag12864bfb.c b/drivers/auxdisplay/cfag12864bfb.c new file mode 100644 index 000000000..0df474506 --- /dev/null +++ b/drivers/auxdisplay/cfag12864bfb.c @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Filename: cfag12864bfb.c + * Version: 0.1.0 + * Description: cfag12864b LCD framebuffer driver + * Depends: cfag12864b + * + * Author: Copyright (C) Miguel Ojeda <ojeda@kernel.org> + * Date: 2006-10-31 + */ + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/errno.h> +#include <linux/fb.h> +#include <linux/mm.h> +#include <linux/platform_device.h> +#include <linux/cfag12864b.h> + +#define CFAG12864BFB_NAME "cfag12864bfb" + +static const struct fb_fix_screeninfo cfag12864bfb_fix = { + .id = "cfag12864b", + .type = FB_TYPE_PACKED_PIXELS, + .visual = FB_VISUAL_MONO10, + .xpanstep = 0, + .ypanstep = 0, + .ywrapstep = 0, + .line_length = CFAG12864B_WIDTH / 8, + .accel = FB_ACCEL_NONE, +}; + +static const struct fb_var_screeninfo cfag12864bfb_var = { + .xres = CFAG12864B_WIDTH, + .yres = CFAG12864B_HEIGHT, + .xres_virtual = CFAG12864B_WIDTH, + .yres_virtual = CFAG12864B_HEIGHT, + .bits_per_pixel = 1, + .red = { 0, 1, 0 }, + .green = { 0, 1, 0 }, + .blue = { 0, 1, 0 }, + .left_margin = 0, + .right_margin = 0, + .upper_margin = 0, + .lower_margin = 0, + .vmode = FB_VMODE_NONINTERLACED, +}; + +static int cfag12864bfb_mmap(struct fb_info *info, struct vm_area_struct *vma) +{ + struct page *pages = virt_to_page(cfag12864b_buffer); + + return vm_map_pages_zero(vma, &pages, 1); +} + +static const struct fb_ops cfag12864bfb_ops = { + .owner = THIS_MODULE, + .fb_read = fb_sys_read, + .fb_write = fb_sys_write, + .fb_fillrect = sys_fillrect, + .fb_copyarea = sys_copyarea, + .fb_imageblit = sys_imageblit, + .fb_mmap = cfag12864bfb_mmap, +}; + +static int cfag12864bfb_probe(struct platform_device *device) +{ + int ret = -EINVAL; + struct fb_info *info = framebuffer_alloc(0, &device->dev); + + if (!info) + goto none; + + info->screen_base = (char __iomem *) cfag12864b_buffer; + info->screen_size = CFAG12864B_SIZE; + info->fbops = &cfag12864bfb_ops; + info->fix = cfag12864bfb_fix; + info->var = cfag12864bfb_var; + info->pseudo_palette = NULL; + info->par = NULL; + info->flags = FBINFO_FLAG_DEFAULT; + + if (register_framebuffer(info) < 0) + goto fballoced; + + platform_set_drvdata(device, info); + + fb_info(info, "%s frame buffer device\n", info->fix.id); + + return 0; + +fballoced: + framebuffer_release(info); + +none: + return ret; +} + +static int cfag12864bfb_remove(struct platform_device *device) +{ + struct fb_info *info = platform_get_drvdata(device); + + if (info) { + unregister_framebuffer(info); + framebuffer_release(info); + } + + return 0; +} + +static struct platform_driver cfag12864bfb_driver = { + .probe = cfag12864bfb_probe, + .remove = cfag12864bfb_remove, + .driver = { + .name = CFAG12864BFB_NAME, + }, +}; + +static struct platform_device *cfag12864bfb_device; + +static int __init cfag12864bfb_init(void) +{ + int ret = -EINVAL; + + /* cfag12864b_init() must be called first */ + if (!cfag12864b_isinited()) { + printk(KERN_ERR CFAG12864BFB_NAME ": ERROR: " + "cfag12864b is not initialized\n"); + goto none; + } + + if (cfag12864b_enable()) { + printk(KERN_ERR CFAG12864BFB_NAME ": ERROR: " + "can't enable cfag12864b refreshing (being used)\n"); + return -ENODEV; + } + + ret = platform_driver_register(&cfag12864bfb_driver); + + if (!ret) { + cfag12864bfb_device = + platform_device_alloc(CFAG12864BFB_NAME, 0); + + if (cfag12864bfb_device) + ret = platform_device_add(cfag12864bfb_device); + else + ret = -ENOMEM; + + if (ret) { + platform_device_put(cfag12864bfb_device); + platform_driver_unregister(&cfag12864bfb_driver); + } + } + +none: + return ret; +} + +static void __exit cfag12864bfb_exit(void) +{ + platform_device_unregister(cfag12864bfb_device); + platform_driver_unregister(&cfag12864bfb_driver); + cfag12864b_disable(); +} + +module_init(cfag12864bfb_init); +module_exit(cfag12864bfb_exit); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Miguel Ojeda <ojeda@kernel.org>"); +MODULE_DESCRIPTION("cfag12864b LCD framebuffer driver"); diff --git a/drivers/auxdisplay/charlcd.c b/drivers/auxdisplay/charlcd.c new file mode 100644 index 000000000..6d309e497 --- /dev/null +++ b/drivers/auxdisplay/charlcd.c @@ -0,0 +1,681 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Character LCD driver for Linux + * + * Copyright (C) 2000-2008, Willy Tarreau <w@1wt.eu> + * Copyright (C) 2016-2017 Glider bvba + */ + +#include <linux/atomic.h> +#include <linux/ctype.h> +#include <linux/fs.h> +#include <linux/miscdevice.h> +#include <linux/module.h> +#include <linux/notifier.h> +#include <linux/reboot.h> +#include <linux/slab.h> +#include <linux/uaccess.h> +#include <linux/workqueue.h> + +#include <generated/utsrelease.h> + +#include "charlcd.h" + +/* Keep the backlight on this many seconds for each flash */ +#define LCD_BL_TEMPO_PERIOD 4 + +#define LCD_ESCAPE_LEN 24 /* Max chars for LCD escape command */ +#define LCD_ESCAPE_CHAR 27 /* Use char 27 for escape command */ + +struct charlcd_priv { + struct charlcd lcd; + + struct delayed_work bl_work; + struct mutex bl_tempo_lock; /* Protects access to bl_tempo */ + bool bl_tempo; + + bool must_clear; + + /* contains the LCD config state */ + unsigned long flags; + + /* Current escape sequence and it's length or -1 if outside */ + struct { + char buf[LCD_ESCAPE_LEN + 1]; + int len; + } esc_seq; + + unsigned long long drvdata[]; +}; + +#define charlcd_to_priv(p) container_of(p, struct charlcd_priv, lcd) + +/* Device single-open policy control */ +static atomic_t charlcd_available = ATOMIC_INIT(1); + +/* turn the backlight on or off */ +void charlcd_backlight(struct charlcd *lcd, enum charlcd_onoff on) +{ + struct charlcd_priv *priv = charlcd_to_priv(lcd); + + if (!lcd->ops->backlight) + return; + + mutex_lock(&priv->bl_tempo_lock); + if (!priv->bl_tempo) + lcd->ops->backlight(lcd, on); + mutex_unlock(&priv->bl_tempo_lock); +} +EXPORT_SYMBOL_GPL(charlcd_backlight); + +static void charlcd_bl_off(struct work_struct *work) +{ + struct delayed_work *dwork = to_delayed_work(work); + struct charlcd_priv *priv = + container_of(dwork, struct charlcd_priv, bl_work); + + mutex_lock(&priv->bl_tempo_lock); + if (priv->bl_tempo) { + priv->bl_tempo = false; + if (!(priv->flags & LCD_FLAG_L)) + priv->lcd.ops->backlight(&priv->lcd, CHARLCD_OFF); + } + mutex_unlock(&priv->bl_tempo_lock); +} + +/* turn the backlight on for a little while */ +void charlcd_poke(struct charlcd *lcd) +{ + struct charlcd_priv *priv = charlcd_to_priv(lcd); + + if (!lcd->ops->backlight) + return; + + cancel_delayed_work_sync(&priv->bl_work); + + mutex_lock(&priv->bl_tempo_lock); + if (!priv->bl_tempo && !(priv->flags & LCD_FLAG_L)) + lcd->ops->backlight(lcd, CHARLCD_ON); + priv->bl_tempo = true; + schedule_delayed_work(&priv->bl_work, LCD_BL_TEMPO_PERIOD * HZ); + mutex_unlock(&priv->bl_tempo_lock); +} +EXPORT_SYMBOL_GPL(charlcd_poke); + +static void charlcd_home(struct charlcd *lcd) +{ + lcd->addr.x = 0; + lcd->addr.y = 0; + lcd->ops->home(lcd); +} + +static void charlcd_print(struct charlcd *lcd, char c) +{ + if (lcd->addr.x >= lcd->width) + return; + + if (lcd->char_conv) + c = lcd->char_conv[(unsigned char)c]; + + if (!lcd->ops->print(lcd, c)) + lcd->addr.x++; + + /* prevents the cursor from wrapping onto the next line */ + if (lcd->addr.x == lcd->width) + lcd->ops->gotoxy(lcd, lcd->addr.x - 1, lcd->addr.y); +} + +static void charlcd_clear_display(struct charlcd *lcd) +{ + lcd->ops->clear_display(lcd); + lcd->addr.x = 0; + lcd->addr.y = 0; +} + +/* + * Parses a movement command of the form "(.*);", where the group can be + * any number of subcommands of the form "(x|y)[0-9]+". + * + * Returns whether the command is valid. The position arguments are + * only written if the parsing was successful. + * + * For instance: + * - ";" returns (<original x>, <original y>). + * - "x1;" returns (1, <original y>). + * - "y2x1;" returns (1, 2). + * - "x12y34x56;" returns (56, 34). + * - "" fails. + * - "x" fails. + * - "x;" fails. + * - "x1" fails. + * - "xy12;" fails. + * - "x12yy12;" fails. + * - "xx" fails. + */ +static bool parse_xy(const char *s, unsigned long *x, unsigned long *y) +{ + unsigned long new_x = *x; + unsigned long new_y = *y; + char *p; + + for (;;) { + if (!*s) + return false; + + if (*s == ';') + break; + + if (*s == 'x') { + new_x = simple_strtoul(s + 1, &p, 10); + if (p == s + 1) + return false; + s = p; + } else if (*s == 'y') { + new_y = simple_strtoul(s + 1, &p, 10); + if (p == s + 1) + return false; + s = p; + } else { + return false; + } + } + + *x = new_x; + *y = new_y; + return true; +} + +/* + * These are the file operation function for user access to /dev/lcd + * This function can also be called from inside the kernel, by + * setting file and ppos to NULL. + * + */ + +static inline int handle_lcd_special_code(struct charlcd *lcd) +{ + struct charlcd_priv *priv = charlcd_to_priv(lcd); + + /* LCD special codes */ + + int processed = 0; + + char *esc = priv->esc_seq.buf + 2; + int oldflags = priv->flags; + + /* check for display mode flags */ + switch (*esc) { + case 'D': /* Display ON */ + priv->flags |= LCD_FLAG_D; + if (priv->flags != oldflags) + lcd->ops->display(lcd, CHARLCD_ON); + + processed = 1; + break; + case 'd': /* Display OFF */ + priv->flags &= ~LCD_FLAG_D; + if (priv->flags != oldflags) + lcd->ops->display(lcd, CHARLCD_OFF); + + processed = 1; + break; + case 'C': /* Cursor ON */ + priv->flags |= LCD_FLAG_C; + if (priv->flags != oldflags) + lcd->ops->cursor(lcd, CHARLCD_ON); + + processed = 1; + break; + case 'c': /* Cursor OFF */ + priv->flags &= ~LCD_FLAG_C; + if (priv->flags != oldflags) + lcd->ops->cursor(lcd, CHARLCD_OFF); + + processed = 1; + break; + case 'B': /* Blink ON */ + priv->flags |= LCD_FLAG_B; + if (priv->flags != oldflags) + lcd->ops->blink(lcd, CHARLCD_ON); + + processed = 1; + break; + case 'b': /* Blink OFF */ + priv->flags &= ~LCD_FLAG_B; + if (priv->flags != oldflags) + lcd->ops->blink(lcd, CHARLCD_OFF); + + processed = 1; + break; + case '+': /* Back light ON */ + priv->flags |= LCD_FLAG_L; + if (priv->flags != oldflags) + charlcd_backlight(lcd, CHARLCD_ON); + + processed = 1; + break; + case '-': /* Back light OFF */ + priv->flags &= ~LCD_FLAG_L; + if (priv->flags != oldflags) + charlcd_backlight(lcd, CHARLCD_OFF); + + processed = 1; + break; + case '*': /* Flash back light */ + charlcd_poke(lcd); + processed = 1; + break; + case 'f': /* Small Font */ + priv->flags &= ~LCD_FLAG_F; + if (priv->flags != oldflags) + lcd->ops->fontsize(lcd, CHARLCD_FONTSIZE_SMALL); + + processed = 1; + break; + case 'F': /* Large Font */ + priv->flags |= LCD_FLAG_F; + if (priv->flags != oldflags) + lcd->ops->fontsize(lcd, CHARLCD_FONTSIZE_LARGE); + + processed = 1; + break; + case 'n': /* One Line */ + priv->flags &= ~LCD_FLAG_N; + if (priv->flags != oldflags) + lcd->ops->lines(lcd, CHARLCD_LINES_1); + + processed = 1; + break; + case 'N': /* Two Lines */ + priv->flags |= LCD_FLAG_N; + if (priv->flags != oldflags) + lcd->ops->lines(lcd, CHARLCD_LINES_2); + + processed = 1; + break; + case 'l': /* Shift Cursor Left */ + if (lcd->addr.x > 0) { + if (!lcd->ops->shift_cursor(lcd, CHARLCD_SHIFT_LEFT)) + lcd->addr.x--; + } + + processed = 1; + break; + case 'r': /* shift cursor right */ + if (lcd->addr.x < lcd->width) { + if (!lcd->ops->shift_cursor(lcd, CHARLCD_SHIFT_RIGHT)) + lcd->addr.x++; + } + + processed = 1; + break; + case 'L': /* shift display left */ + lcd->ops->shift_display(lcd, CHARLCD_SHIFT_LEFT); + processed = 1; + break; + case 'R': /* shift display right */ + lcd->ops->shift_display(lcd, CHARLCD_SHIFT_RIGHT); + processed = 1; + break; + case 'k': { /* kill end of line */ + int x, xs, ys; + + xs = lcd->addr.x; + ys = lcd->addr.y; + for (x = lcd->addr.x; x < lcd->width; x++) + lcd->ops->print(lcd, ' '); + + /* restore cursor position */ + lcd->addr.x = xs; + lcd->addr.y = ys; + lcd->ops->gotoxy(lcd, lcd->addr.x, lcd->addr.y); + processed = 1; + break; + } + case 'I': /* reinitialize display */ + lcd->ops->init_display(lcd); + priv->flags = ((lcd->height > 1) ? LCD_FLAG_N : 0) | LCD_FLAG_D | + LCD_FLAG_C | LCD_FLAG_B; + processed = 1; + break; + case 'G': + if (lcd->ops->redefine_char) + processed = lcd->ops->redefine_char(lcd, esc); + else + processed = 1; + break; + + case 'x': /* gotoxy : LxXXX[yYYY]; */ + case 'y': /* gotoxy : LyYYY[xXXX]; */ + if (priv->esc_seq.buf[priv->esc_seq.len - 1] != ';') + break; + + /* If the command is valid, move to the new address */ + if (parse_xy(esc, &lcd->addr.x, &lcd->addr.y)) + lcd->ops->gotoxy(lcd, lcd->addr.x, lcd->addr.y); + + /* Regardless of its validity, mark as processed */ + processed = 1; + break; + } + + return processed; +} + +static void charlcd_write_char(struct charlcd *lcd, char c) +{ + struct charlcd_priv *priv = charlcd_to_priv(lcd); + + /* first, we'll test if we're in escape mode */ + if ((c != '\n') && priv->esc_seq.len >= 0) { + /* yes, let's add this char to the buffer */ + priv->esc_seq.buf[priv->esc_seq.len++] = c; + priv->esc_seq.buf[priv->esc_seq.len] = '\0'; + } else { + /* aborts any previous escape sequence */ + priv->esc_seq.len = -1; + + switch (c) { + case LCD_ESCAPE_CHAR: + /* start of an escape sequence */ + priv->esc_seq.len = 0; + priv->esc_seq.buf[priv->esc_seq.len] = '\0'; + break; + case '\b': + /* go back one char and clear it */ + if (lcd->addr.x > 0) { + /* back one char */ + if (!lcd->ops->shift_cursor(lcd, + CHARLCD_SHIFT_LEFT)) + lcd->addr.x--; + } + /* replace with a space */ + charlcd_print(lcd, ' '); + /* back one char again */ + if (!lcd->ops->shift_cursor(lcd, CHARLCD_SHIFT_LEFT)) + lcd->addr.x--; + + break; + case '\f': + /* quickly clear the display */ + charlcd_clear_display(lcd); + break; + case '\n': + /* + * flush the remainder of the current line and + * go to the beginning of the next line + */ + for (; lcd->addr.x < lcd->width; lcd->addr.x++) + lcd->ops->print(lcd, ' '); + + lcd->addr.x = 0; + lcd->addr.y = (lcd->addr.y + 1) % lcd->height; + lcd->ops->gotoxy(lcd, lcd->addr.x, lcd->addr.y); + break; + case '\r': + /* go to the beginning of the same line */ + lcd->addr.x = 0; + lcd->ops->gotoxy(lcd, lcd->addr.x, lcd->addr.y); + break; + case '\t': + /* print a space instead of the tab */ + charlcd_print(lcd, ' '); + break; + default: + /* simply print this char */ + charlcd_print(lcd, c); + break; + } + } + + /* + * now we'll see if we're in an escape mode and if the current + * escape sequence can be understood. + */ + if (priv->esc_seq.len >= 2) { + int processed = 0; + + if (!strcmp(priv->esc_seq.buf, "[2J")) { + /* clear the display */ + charlcd_clear_display(lcd); + processed = 1; + } else if (!strcmp(priv->esc_seq.buf, "[H")) { + /* cursor to home */ + charlcd_home(lcd); + processed = 1; + } + /* codes starting with ^[[L */ + else if ((priv->esc_seq.len >= 3) && + (priv->esc_seq.buf[0] == '[') && + (priv->esc_seq.buf[1] == 'L')) { + processed = handle_lcd_special_code(lcd); + } + + /* LCD special escape codes */ + /* + * flush the escape sequence if it's been processed + * or if it is getting too long. + */ + if (processed || (priv->esc_seq.len >= LCD_ESCAPE_LEN)) + priv->esc_seq.len = -1; + } /* escape codes */ +} + +static struct charlcd *the_charlcd; + +static ssize_t charlcd_write(struct file *file, const char __user *buf, + size_t count, loff_t *ppos) +{ + const char __user *tmp = buf; + char c; + + for (; count-- > 0; (*ppos)++, tmp++) { + if (((count + 1) & 0x1f) == 0) { + /* + * charlcd_write() is invoked as a VFS->write() callback + * and as such it is always invoked from preemptible + * context and may sleep. + */ + cond_resched(); + } + + if (get_user(c, tmp)) + return -EFAULT; + + charlcd_write_char(the_charlcd, c); + } + + return tmp - buf; +} + +static int charlcd_open(struct inode *inode, struct file *file) +{ + struct charlcd_priv *priv = charlcd_to_priv(the_charlcd); + int ret; + + ret = -EBUSY; + if (!atomic_dec_and_test(&charlcd_available)) + goto fail; /* open only once at a time */ + + ret = -EPERM; + if (file->f_mode & FMODE_READ) /* device is write-only */ + goto fail; + + if (priv->must_clear) { + priv->lcd.ops->clear_display(&priv->lcd); + priv->must_clear = false; + priv->lcd.addr.x = 0; + priv->lcd.addr.y = 0; + } + return nonseekable_open(inode, file); + + fail: + atomic_inc(&charlcd_available); + return ret; +} + +static int charlcd_release(struct inode *inode, struct file *file) +{ + atomic_inc(&charlcd_available); + return 0; +} + +static const struct file_operations charlcd_fops = { + .write = charlcd_write, + .open = charlcd_open, + .release = charlcd_release, + .llseek = no_llseek, +}; + +static struct miscdevice charlcd_dev = { + .minor = LCD_MINOR, + .name = "lcd", + .fops = &charlcd_fops, +}; + +static void charlcd_puts(struct charlcd *lcd, const char *s) +{ + const char *tmp = s; + int count = strlen(s); + + for (; count-- > 0; tmp++) { + if (((count + 1) & 0x1f) == 0) + cond_resched(); + + charlcd_write_char(lcd, *tmp); + } +} + +#ifdef CONFIG_PANEL_BOOT_MESSAGE +#define LCD_INIT_TEXT CONFIG_PANEL_BOOT_MESSAGE +#else +#define LCD_INIT_TEXT "Linux-" UTS_RELEASE "\n" +#endif + +#ifdef CONFIG_CHARLCD_BL_ON +#define LCD_INIT_BL "\x1b[L+" +#elif defined(CONFIG_CHARLCD_BL_FLASH) +#define LCD_INIT_BL "\x1b[L*" +#else +#define LCD_INIT_BL "\x1b[L-" +#endif + +/* initialize the LCD driver */ +static int charlcd_init(struct charlcd *lcd) +{ + struct charlcd_priv *priv = charlcd_to_priv(lcd); + int ret; + + priv->flags = ((lcd->height > 1) ? LCD_FLAG_N : 0) | LCD_FLAG_D | + LCD_FLAG_C | LCD_FLAG_B; + if (lcd->ops->backlight) { + mutex_init(&priv->bl_tempo_lock); + INIT_DELAYED_WORK(&priv->bl_work, charlcd_bl_off); + } + + /* + * before this line, we must NOT send anything to the display. + * Since charlcd_init_display() needs to write data, we have to + * enable mark the LCD initialized just before. + */ + if (WARN_ON(!lcd->ops->init_display)) + return -EINVAL; + + ret = lcd->ops->init_display(lcd); + if (ret) + return ret; + + /* display a short message */ + charlcd_puts(lcd, "\x1b[Lc\x1b[Lb" LCD_INIT_BL LCD_INIT_TEXT); + + /* clear the display on the next device opening */ + priv->must_clear = true; + charlcd_home(lcd); + return 0; +} + +struct charlcd *charlcd_alloc(void) +{ + struct charlcd_priv *priv; + struct charlcd *lcd; + + priv = kzalloc(sizeof(*priv), GFP_KERNEL); + if (!priv) + return NULL; + + priv->esc_seq.len = -1; + + lcd = &priv->lcd; + + return lcd; +} +EXPORT_SYMBOL_GPL(charlcd_alloc); + +void charlcd_free(struct charlcd *lcd) +{ + kfree(charlcd_to_priv(lcd)); +} +EXPORT_SYMBOL_GPL(charlcd_free); + +static int panel_notify_sys(struct notifier_block *this, unsigned long code, + void *unused) +{ + struct charlcd *lcd = the_charlcd; + + switch (code) { + case SYS_DOWN: + charlcd_puts(lcd, + "\x0cReloading\nSystem...\x1b[Lc\x1b[Lb\x1b[L+"); + break; + case SYS_HALT: + charlcd_puts(lcd, "\x0cSystem Halted.\x1b[Lc\x1b[Lb\x1b[L+"); + break; + case SYS_POWER_OFF: + charlcd_puts(lcd, "\x0cPower off.\x1b[Lc\x1b[Lb\x1b[L+"); + break; + default: + break; + } + return NOTIFY_DONE; +} + +static struct notifier_block panel_notifier = { + .notifier_call = panel_notify_sys, +}; + +int charlcd_register(struct charlcd *lcd) +{ + int ret; + + ret = charlcd_init(lcd); + if (ret) + return ret; + + ret = misc_register(&charlcd_dev); + if (ret) + return ret; + + the_charlcd = lcd; + register_reboot_notifier(&panel_notifier); + return 0; +} +EXPORT_SYMBOL_GPL(charlcd_register); + +int charlcd_unregister(struct charlcd *lcd) +{ + struct charlcd_priv *priv = charlcd_to_priv(lcd); + + unregister_reboot_notifier(&panel_notifier); + charlcd_puts(lcd, "\x0cLCD driver unloaded.\x1b[Lc\x1b[Lb\x1b[L-"); + misc_deregister(&charlcd_dev); + the_charlcd = NULL; + if (lcd->ops->backlight) { + cancel_delayed_work_sync(&priv->bl_work); + priv->lcd.ops->backlight(&priv->lcd, CHARLCD_OFF); + } + + return 0; +} +EXPORT_SYMBOL_GPL(charlcd_unregister); + +MODULE_LICENSE("GPL"); diff --git a/drivers/auxdisplay/charlcd.h b/drivers/auxdisplay/charlcd.h new file mode 100644 index 000000000..eed80063a --- /dev/null +++ b/drivers/auxdisplay/charlcd.h @@ -0,0 +1,104 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Character LCD driver for Linux + * + * Copyright (C) 2000-2008, Willy Tarreau <w@1wt.eu> + * Copyright (C) 2016-2017 Glider bvba + */ + +#ifndef _CHARLCD_H +#define _CHARLCD_H + +#define LCD_FLAG_B 0x0004 /* Blink on */ +#define LCD_FLAG_C 0x0008 /* Cursor on */ +#define LCD_FLAG_D 0x0010 /* Display on */ +#define LCD_FLAG_F 0x0020 /* Large font mode */ +#define LCD_FLAG_N 0x0040 /* 2-rows mode */ +#define LCD_FLAG_L 0x0080 /* Backlight enabled */ + +enum charlcd_onoff { + CHARLCD_OFF = 0, + CHARLCD_ON, +}; + +enum charlcd_shift_dir { + CHARLCD_SHIFT_LEFT, + CHARLCD_SHIFT_RIGHT, +}; + +enum charlcd_fontsize { + CHARLCD_FONTSIZE_SMALL, + CHARLCD_FONTSIZE_LARGE, +}; + +enum charlcd_lines { + CHARLCD_LINES_1, + CHARLCD_LINES_2, +}; + +struct charlcd { + const struct charlcd_ops *ops; + const unsigned char *char_conv; /* Optional */ + + int height; + int width; + + /* Contains the LCD X and Y offset */ + struct { + unsigned long x; + unsigned long y; + } addr; + + void *drvdata; +}; + +/** + * struct charlcd_ops - Functions used by charlcd. Drivers have to implement + * these. + * @backlight: Turn backlight on or off. Optional. + * @print: Print one character to the display at current cursor position. + * The buffered cursor position is advanced by charlcd. The cursor should not + * wrap to the next line at the end of a line. + * @gotoxy: Set cursor to x, y. The x and y values to set the cursor to are + * previously set in addr.x and addr.y by charlcd. + * @home: Set cursor to 0, 0. The values in addr.x and addr.y are set to 0, 0 by + * charlcd prior to calling this function. + * @clear_display: Clear the whole display and set the cursor to 0, 0. The + * values in addr.x and addr.y are set to 0, 0 by charlcd after to calling this + * function. + * @init_display: Initialize the display. + * @shift_cursor: Shift cursor left or right one position. + * @shift_display: Shift whole display content left or right. + * @display: Turn display on or off. + * @cursor: Turn cursor on or off. + * @blink: Turn cursor blink on or off. + * @lines: One or two lines. + * @redefine_char: Redefine the actual pixel matrix of character. + */ +struct charlcd_ops { + void (*backlight)(struct charlcd *lcd, enum charlcd_onoff on); + int (*print)(struct charlcd *lcd, int c); + int (*gotoxy)(struct charlcd *lcd, unsigned int x, unsigned int y); + int (*home)(struct charlcd *lcd); + int (*clear_display)(struct charlcd *lcd); + int (*init_display)(struct charlcd *lcd); + int (*shift_cursor)(struct charlcd *lcd, enum charlcd_shift_dir dir); + int (*shift_display)(struct charlcd *lcd, enum charlcd_shift_dir dir); + int (*display)(struct charlcd *lcd, enum charlcd_onoff on); + int (*cursor)(struct charlcd *lcd, enum charlcd_onoff on); + int (*blink)(struct charlcd *lcd, enum charlcd_onoff on); + int (*fontsize)(struct charlcd *lcd, enum charlcd_fontsize size); + int (*lines)(struct charlcd *lcd, enum charlcd_lines lines); + int (*redefine_char)(struct charlcd *lcd, char *esc); +}; + +void charlcd_backlight(struct charlcd *lcd, enum charlcd_onoff on); +struct charlcd *charlcd_alloc(void); +void charlcd_free(struct charlcd *lcd); + +int charlcd_register(struct charlcd *lcd); +int charlcd_unregister(struct charlcd *lcd); + +void charlcd_poke(struct charlcd *lcd); + +#endif /* CHARLCD_H */ diff --git a/drivers/auxdisplay/hd44780.c b/drivers/auxdisplay/hd44780.c new file mode 100644 index 000000000..d56a5d508 --- /dev/null +++ b/drivers/auxdisplay/hd44780.c @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * HD44780 Character LCD driver for Linux + * + * Copyright (C) 2000-2008, Willy Tarreau <w@1wt.eu> + * Copyright (C) 2016-2017 Glider bvba + */ + +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/module.h> +#include <linux/mod_devicetable.h> +#include <linux/platform_device.h> +#include <linux/property.h> +#include <linux/slab.h> + +#include "charlcd.h" +#include "hd44780_common.h" + +enum hd44780_pin { + /* Order does matter due to writing to GPIO array subsets! */ + PIN_DATA0, /* Optional */ + PIN_DATA1, /* Optional */ + PIN_DATA2, /* Optional */ + PIN_DATA3, /* Optional */ + PIN_DATA4, + PIN_DATA5, + PIN_DATA6, + PIN_DATA7, + PIN_CTRL_RS, + PIN_CTRL_RW, /* Optional */ + PIN_CTRL_E, + PIN_CTRL_BL, /* Optional */ + PIN_NUM +}; + +struct hd44780 { + struct gpio_desc *pins[PIN_NUM]; +}; + +static void hd44780_backlight(struct charlcd *lcd, enum charlcd_onoff on) +{ + struct hd44780_common *hdc = lcd->drvdata; + struct hd44780 *hd = hdc->hd44780; + + if (hd->pins[PIN_CTRL_BL]) + gpiod_set_value_cansleep(hd->pins[PIN_CTRL_BL], on); +} + +static void hd44780_strobe_gpio(struct hd44780 *hd) +{ + /* Maintain the data during 20 us before the strobe */ + udelay(20); + + gpiod_set_value_cansleep(hd->pins[PIN_CTRL_E], 1); + + /* Maintain the strobe during 40 us */ + udelay(40); + + gpiod_set_value_cansleep(hd->pins[PIN_CTRL_E], 0); +} + +/* write to an LCD panel register in 8 bit GPIO mode */ +static void hd44780_write_gpio8(struct hd44780 *hd, u8 val, unsigned int rs) +{ + DECLARE_BITMAP(values, 10); /* for DATA[0-7], RS, RW */ + unsigned int n; + + values[0] = val; + __assign_bit(8, values, rs); + n = hd->pins[PIN_CTRL_RW] ? 10 : 9; + + /* Present the data to the port */ + gpiod_set_array_value_cansleep(n, &hd->pins[PIN_DATA0], NULL, values); + + hd44780_strobe_gpio(hd); +} + +/* write to an LCD panel register in 4 bit GPIO mode */ +static void hd44780_write_gpio4(struct hd44780 *hd, u8 val, unsigned int rs) +{ + DECLARE_BITMAP(values, 6); /* for DATA[4-7], RS, RW */ + unsigned int n; + + /* High nibble + RS, RW */ + values[0] = val >> 4; + __assign_bit(4, values, rs); + n = hd->pins[PIN_CTRL_RW] ? 6 : 5; + + /* Present the data to the port */ + gpiod_set_array_value_cansleep(n, &hd->pins[PIN_DATA4], NULL, values); + + hd44780_strobe_gpio(hd); + + /* Low nibble */ + values[0] &= ~0x0fUL; + values[0] |= val & 0x0f; + + /* Present the data to the port */ + gpiod_set_array_value_cansleep(n, &hd->pins[PIN_DATA4], NULL, values); + + hd44780_strobe_gpio(hd); +} + +/* Send a command to the LCD panel in 8 bit GPIO mode */ +static void hd44780_write_cmd_gpio8(struct hd44780_common *hdc, int cmd) +{ + struct hd44780 *hd = hdc->hd44780; + + hd44780_write_gpio8(hd, cmd, 0); + + /* The shortest command takes at least 120 us */ + udelay(120); +} + +/* Send data to the LCD panel in 8 bit GPIO mode */ +static void hd44780_write_data_gpio8(struct hd44780_common *hdc, int data) +{ + struct hd44780 *hd = hdc->hd44780; + + hd44780_write_gpio8(hd, data, 1); + + /* The shortest data takes at least 45 us */ + udelay(45); +} + +static const struct charlcd_ops hd44780_ops_gpio8 = { + .backlight = hd44780_backlight, + .print = hd44780_common_print, + .gotoxy = hd44780_common_gotoxy, + .home = hd44780_common_home, + .clear_display = hd44780_common_clear_display, + .init_display = hd44780_common_init_display, + .shift_cursor = hd44780_common_shift_cursor, + .shift_display = hd44780_common_shift_display, + .display = hd44780_common_display, + .cursor = hd44780_common_cursor, + .blink = hd44780_common_blink, + .fontsize = hd44780_common_fontsize, + .lines = hd44780_common_lines, + .redefine_char = hd44780_common_redefine_char, +}; + +/* Send a command to the LCD panel in 4 bit GPIO mode */ +static void hd44780_write_cmd_gpio4(struct hd44780_common *hdc, int cmd) +{ + struct hd44780 *hd = hdc->hd44780; + + hd44780_write_gpio4(hd, cmd, 0); + + /* The shortest command takes at least 120 us */ + udelay(120); +} + +/* Send 4-bits of a command to the LCD panel in raw 4 bit GPIO mode */ +static void hd44780_write_cmd_raw_gpio4(struct hd44780_common *hdc, int cmd) +{ + DECLARE_BITMAP(values, 6); /* for DATA[4-7], RS, RW */ + struct hd44780 *hd = hdc->hd44780; + unsigned int n; + + /* Command nibble + RS, RW */ + values[0] = cmd & 0x0f; + n = hd->pins[PIN_CTRL_RW] ? 6 : 5; + + /* Present the data to the port */ + gpiod_set_array_value_cansleep(n, &hd->pins[PIN_DATA4], NULL, values); + + hd44780_strobe_gpio(hd); +} + +/* Send data to the LCD panel in 4 bit GPIO mode */ +static void hd44780_write_data_gpio4(struct hd44780_common *hdc, int data) +{ + struct hd44780 *hd = hdc->hd44780; + + hd44780_write_gpio4(hd, data, 1); + + /* The shortest data takes at least 45 us */ + udelay(45); +} + +static const struct charlcd_ops hd44780_ops_gpio4 = { + .backlight = hd44780_backlight, + .print = hd44780_common_print, + .gotoxy = hd44780_common_gotoxy, + .home = hd44780_common_home, + .clear_display = hd44780_common_clear_display, + .init_display = hd44780_common_init_display, + .shift_cursor = hd44780_common_shift_cursor, + .shift_display = hd44780_common_shift_display, + .display = hd44780_common_display, + .cursor = hd44780_common_cursor, + .blink = hd44780_common_blink, + .fontsize = hd44780_common_fontsize, + .lines = hd44780_common_lines, + .redefine_char = hd44780_common_redefine_char, +}; + +static int hd44780_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + unsigned int i, base; + struct charlcd *lcd; + struct hd44780_common *hdc; + struct hd44780 *hd; + int ifwidth, ret = -ENOMEM; + + /* Required pins */ + ifwidth = gpiod_count(dev, "data"); + if (ifwidth < 0) + return ifwidth; + + switch (ifwidth) { + case 4: + base = PIN_DATA4; + break; + case 8: + base = PIN_DATA0; + break; + default: + return -EINVAL; + } + + hdc = hd44780_common_alloc(); + if (!hdc) + return -ENOMEM; + + lcd = charlcd_alloc(); + if (!lcd) + goto fail1; + + hd = kzalloc(sizeof(struct hd44780), GFP_KERNEL); + if (!hd) + goto fail2; + + hdc->hd44780 = hd; + lcd->drvdata = hdc; + for (i = 0; i < ifwidth; i++) { + hd->pins[base + i] = devm_gpiod_get_index(dev, "data", i, + GPIOD_OUT_LOW); + if (IS_ERR(hd->pins[base + i])) { + ret = PTR_ERR(hd->pins[base + i]); + goto fail3; + } + } + + hd->pins[PIN_CTRL_E] = devm_gpiod_get(dev, "enable", GPIOD_OUT_LOW); + if (IS_ERR(hd->pins[PIN_CTRL_E])) { + ret = PTR_ERR(hd->pins[PIN_CTRL_E]); + goto fail3; + } + + hd->pins[PIN_CTRL_RS] = devm_gpiod_get(dev, "rs", GPIOD_OUT_HIGH); + if (IS_ERR(hd->pins[PIN_CTRL_RS])) { + ret = PTR_ERR(hd->pins[PIN_CTRL_RS]); + goto fail3; + } + + /* Optional pins */ + hd->pins[PIN_CTRL_RW] = devm_gpiod_get_optional(dev, "rw", + GPIOD_OUT_LOW); + if (IS_ERR(hd->pins[PIN_CTRL_RW])) { + ret = PTR_ERR(hd->pins[PIN_CTRL_RW]); + goto fail3; + } + + hd->pins[PIN_CTRL_BL] = devm_gpiod_get_optional(dev, "backlight", + GPIOD_OUT_LOW); + if (IS_ERR(hd->pins[PIN_CTRL_BL])) { + ret = PTR_ERR(hd->pins[PIN_CTRL_BL]); + goto fail3; + } + + /* Required properties */ + ret = device_property_read_u32(dev, "display-height-chars", + &lcd->height); + if (ret) + goto fail3; + ret = device_property_read_u32(dev, "display-width-chars", &lcd->width); + if (ret) + goto fail3; + + /* + * On displays with more than two rows, the internal buffer width is + * usually equal to the display width + */ + if (lcd->height > 2) + hdc->bwidth = lcd->width; + + /* Optional properties */ + device_property_read_u32(dev, "internal-buffer-width", &hdc->bwidth); + + hdc->ifwidth = ifwidth; + if (ifwidth == 8) { + lcd->ops = &hd44780_ops_gpio8; + hdc->write_data = hd44780_write_data_gpio8; + hdc->write_cmd = hd44780_write_cmd_gpio8; + } else { + lcd->ops = &hd44780_ops_gpio4; + hdc->write_data = hd44780_write_data_gpio4; + hdc->write_cmd = hd44780_write_cmd_gpio4; + hdc->write_cmd_raw4 = hd44780_write_cmd_raw_gpio4; + } + + ret = charlcd_register(lcd); + if (ret) + goto fail3; + + platform_set_drvdata(pdev, lcd); + return 0; + +fail3: + kfree(hd); +fail2: + kfree(lcd); +fail1: + kfree(hdc); + return ret; +} + +static int hd44780_remove(struct platform_device *pdev) +{ + struct charlcd *lcd = platform_get_drvdata(pdev); + struct hd44780_common *hdc = lcd->drvdata; + + charlcd_unregister(lcd); + kfree(hdc->hd44780); + kfree(lcd->drvdata); + + kfree(lcd); + return 0; +} + +static const struct of_device_id hd44780_of_match[] = { + { .compatible = "hit,hd44780" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, hd44780_of_match); + +static struct platform_driver hd44780_driver = { + .probe = hd44780_probe, + .remove = hd44780_remove, + .driver = { + .name = "hd44780", + .of_match_table = hd44780_of_match, + }, +}; + +module_platform_driver(hd44780_driver); +MODULE_DESCRIPTION("HD44780 Character LCD driver"); +MODULE_AUTHOR("Geert Uytterhoeven <geert@linux-m68k.org>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/auxdisplay/hd44780_common.c b/drivers/auxdisplay/hd44780_common.c new file mode 100644 index 000000000..7cbf375b0 --- /dev/null +++ b/drivers/auxdisplay/hd44780_common.c @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <linux/module.h> +#include <linux/sched.h> +#include <linux/slab.h> + +#include "charlcd.h" +#include "hd44780_common.h" + +/* LCD commands */ +#define LCD_CMD_DISPLAY_CLEAR 0x01 /* Clear entire display */ + +#define LCD_CMD_ENTRY_MODE 0x04 /* Set entry mode */ +#define LCD_CMD_CURSOR_INC 0x02 /* Increment cursor */ + +#define LCD_CMD_DISPLAY_CTRL 0x08 /* Display control */ +#define LCD_CMD_DISPLAY_ON 0x04 /* Set display on */ +#define LCD_CMD_CURSOR_ON 0x02 /* Set cursor on */ +#define LCD_CMD_BLINK_ON 0x01 /* Set blink on */ + +#define LCD_CMD_SHIFT 0x10 /* Shift cursor/display */ +#define LCD_CMD_DISPLAY_SHIFT 0x08 /* Shift display instead of cursor */ +#define LCD_CMD_SHIFT_RIGHT 0x04 /* Shift display/cursor to the right */ + +#define LCD_CMD_FUNCTION_SET 0x20 /* Set function */ +#define LCD_CMD_DATA_LEN_8BITS 0x10 /* Set data length to 8 bits */ +#define LCD_CMD_TWO_LINES 0x08 /* Set to two display lines */ +#define LCD_CMD_FONT_5X10_DOTS 0x04 /* Set char font to 5x10 dots */ + +#define LCD_CMD_SET_CGRAM_ADDR 0x40 /* Set char generator RAM address */ + +#define LCD_CMD_SET_DDRAM_ADDR 0x80 /* Set display data RAM address */ + +/* sleeps that many milliseconds with a reschedule */ +static void long_sleep(int ms) +{ + schedule_timeout_interruptible(msecs_to_jiffies(ms)); +} + +int hd44780_common_print(struct charlcd *lcd, int c) +{ + struct hd44780_common *hdc = lcd->drvdata; + + if (lcd->addr.x < hdc->bwidth) { + hdc->write_data(hdc, c); + return 0; + } + + return 1; +} +EXPORT_SYMBOL_GPL(hd44780_common_print); + +int hd44780_common_gotoxy(struct charlcd *lcd, unsigned int x, unsigned int y) +{ + struct hd44780_common *hdc = lcd->drvdata; + unsigned int addr; + + /* + * we force the cursor to stay at the end of the + * line if it wants to go farther + */ + addr = x < hdc->bwidth ? x & (hdc->hwidth - 1) : hdc->bwidth - 1; + if (y & 1) + addr += hdc->hwidth; + if (y & 2) + addr += hdc->bwidth; + hdc->write_cmd(hdc, LCD_CMD_SET_DDRAM_ADDR | addr); + return 0; +} +EXPORT_SYMBOL_GPL(hd44780_common_gotoxy); + +int hd44780_common_home(struct charlcd *lcd) +{ + return hd44780_common_gotoxy(lcd, 0, 0); +} +EXPORT_SYMBOL_GPL(hd44780_common_home); + +/* clears the display and resets X/Y */ +int hd44780_common_clear_display(struct charlcd *lcd) +{ + struct hd44780_common *hdc = lcd->drvdata; + + hdc->write_cmd(hdc, LCD_CMD_DISPLAY_CLEAR); + /* datasheet says to wait 1,64 milliseconds */ + long_sleep(2); + + /* + * The Hitachi HD44780 controller (and compatible ones) reset the DDRAM + * address when executing the DISPLAY_CLEAR command, thus the + * following call is not required. However, other controllers do not + * (e.g. NewHaven NHD-0220DZW-AG5), thus move the cursor to home + * unconditionally to support both. + */ + return hd44780_common_home(lcd); +} +EXPORT_SYMBOL_GPL(hd44780_common_clear_display); + +int hd44780_common_init_display(struct charlcd *lcd) +{ + struct hd44780_common *hdc = lcd->drvdata; + + void (*write_cmd_raw)(struct hd44780_common *hdc, int cmd); + u8 init; + + if (hdc->ifwidth != 4 && hdc->ifwidth != 8) + return -EINVAL; + + hdc->hd44780_common_flags = ((lcd->height > 1) ? LCD_FLAG_N : 0) | + LCD_FLAG_D | LCD_FLAG_C | LCD_FLAG_B; + + long_sleep(20); /* wait 20 ms after power-up for the paranoid */ + + /* + * 8-bit mode, 1 line, small fonts; let's do it 3 times, to make sure + * the LCD is in 8-bit mode afterwards + */ + init = LCD_CMD_FUNCTION_SET | LCD_CMD_DATA_LEN_8BITS; + if (hdc->ifwidth == 4) { + init >>= 4; + write_cmd_raw = hdc->write_cmd_raw4; + } else { + write_cmd_raw = hdc->write_cmd; + } + write_cmd_raw(hdc, init); + long_sleep(10); + write_cmd_raw(hdc, init); + long_sleep(10); + write_cmd_raw(hdc, init); + long_sleep(10); + + if (hdc->ifwidth == 4) { + /* Switch to 4-bit mode, 1 line, small fonts */ + hdc->write_cmd_raw4(hdc, LCD_CMD_FUNCTION_SET >> 4); + long_sleep(10); + } + + /* set font height and lines number */ + hdc->write_cmd(hdc, + LCD_CMD_FUNCTION_SET | + ((hdc->ifwidth == 8) ? LCD_CMD_DATA_LEN_8BITS : 0) | + ((hdc->hd44780_common_flags & LCD_FLAG_F) ? + LCD_CMD_FONT_5X10_DOTS : 0) | + ((hdc->hd44780_common_flags & LCD_FLAG_N) ? + LCD_CMD_TWO_LINES : 0)); + long_sleep(10); + + /* display off, cursor off, blink off */ + hdc->write_cmd(hdc, LCD_CMD_DISPLAY_CTRL); + long_sleep(10); + + hdc->write_cmd(hdc, + LCD_CMD_DISPLAY_CTRL | /* set display mode */ + ((hdc->hd44780_common_flags & LCD_FLAG_D) ? + LCD_CMD_DISPLAY_ON : 0) | + ((hdc->hd44780_common_flags & LCD_FLAG_C) ? + LCD_CMD_CURSOR_ON : 0) | + ((hdc->hd44780_common_flags & LCD_FLAG_B) ? + LCD_CMD_BLINK_ON : 0)); + + charlcd_backlight(lcd, + (hdc->hd44780_common_flags & LCD_FLAG_L) ? 1 : 0); + + long_sleep(10); + + /* entry mode set : increment, cursor shifting */ + hdc->write_cmd(hdc, LCD_CMD_ENTRY_MODE | LCD_CMD_CURSOR_INC); + + hd44780_common_clear_display(lcd); + return 0; +} +EXPORT_SYMBOL_GPL(hd44780_common_init_display); + +int hd44780_common_shift_cursor(struct charlcd *lcd, enum charlcd_shift_dir dir) +{ + struct hd44780_common *hdc = lcd->drvdata; + + if (dir == CHARLCD_SHIFT_LEFT) { + /* back one char if not at end of line */ + if (lcd->addr.x < hdc->bwidth) + hdc->write_cmd(hdc, LCD_CMD_SHIFT); + } else if (dir == CHARLCD_SHIFT_RIGHT) { + /* allow the cursor to pass the end of the line */ + if (lcd->addr.x < (hdc->bwidth - 1)) + hdc->write_cmd(hdc, + LCD_CMD_SHIFT | LCD_CMD_SHIFT_RIGHT); + } + + return 0; +} +EXPORT_SYMBOL_GPL(hd44780_common_shift_cursor); + +int hd44780_common_shift_display(struct charlcd *lcd, + enum charlcd_shift_dir dir) +{ + struct hd44780_common *hdc = lcd->drvdata; + + if (dir == CHARLCD_SHIFT_LEFT) + hdc->write_cmd(hdc, LCD_CMD_SHIFT | LCD_CMD_DISPLAY_SHIFT); + else if (dir == CHARLCD_SHIFT_RIGHT) + hdc->write_cmd(hdc, LCD_CMD_SHIFT | LCD_CMD_DISPLAY_SHIFT | + LCD_CMD_SHIFT_RIGHT); + + return 0; +} +EXPORT_SYMBOL_GPL(hd44780_common_shift_display); + +static void hd44780_common_set_mode(struct hd44780_common *hdc) +{ + hdc->write_cmd(hdc, + LCD_CMD_DISPLAY_CTRL | + ((hdc->hd44780_common_flags & LCD_FLAG_D) ? + LCD_CMD_DISPLAY_ON : 0) | + ((hdc->hd44780_common_flags & LCD_FLAG_C) ? + LCD_CMD_CURSOR_ON : 0) | + ((hdc->hd44780_common_flags & LCD_FLAG_B) ? + LCD_CMD_BLINK_ON : 0)); +} + +int hd44780_common_display(struct charlcd *lcd, enum charlcd_onoff on) +{ + struct hd44780_common *hdc = lcd->drvdata; + + if (on == CHARLCD_ON) + hdc->hd44780_common_flags |= LCD_FLAG_D; + else + hdc->hd44780_common_flags &= ~LCD_FLAG_D; + + hd44780_common_set_mode(hdc); + return 0; +} +EXPORT_SYMBOL_GPL(hd44780_common_display); + +int hd44780_common_cursor(struct charlcd *lcd, enum charlcd_onoff on) +{ + struct hd44780_common *hdc = lcd->drvdata; + + if (on == CHARLCD_ON) + hdc->hd44780_common_flags |= LCD_FLAG_C; + else + hdc->hd44780_common_flags &= ~LCD_FLAG_C; + + hd44780_common_set_mode(hdc); + return 0; +} +EXPORT_SYMBOL_GPL(hd44780_common_cursor); + +int hd44780_common_blink(struct charlcd *lcd, enum charlcd_onoff on) +{ + struct hd44780_common *hdc = lcd->drvdata; + + if (on == CHARLCD_ON) + hdc->hd44780_common_flags |= LCD_FLAG_B; + else + hdc->hd44780_common_flags &= ~LCD_FLAG_B; + + hd44780_common_set_mode(hdc); + return 0; +} +EXPORT_SYMBOL_GPL(hd44780_common_blink); + +static void hd44780_common_set_function(struct hd44780_common *hdc) +{ + hdc->write_cmd(hdc, + LCD_CMD_FUNCTION_SET | + ((hdc->ifwidth == 8) ? LCD_CMD_DATA_LEN_8BITS : 0) | + ((hdc->hd44780_common_flags & LCD_FLAG_F) ? + LCD_CMD_FONT_5X10_DOTS : 0) | + ((hdc->hd44780_common_flags & LCD_FLAG_N) ? + LCD_CMD_TWO_LINES : 0)); +} + +int hd44780_common_fontsize(struct charlcd *lcd, enum charlcd_fontsize size) +{ + struct hd44780_common *hdc = lcd->drvdata; + + if (size == CHARLCD_FONTSIZE_LARGE) + hdc->hd44780_common_flags |= LCD_FLAG_F; + else + hdc->hd44780_common_flags &= ~LCD_FLAG_F; + + hd44780_common_set_function(hdc); + return 0; +} +EXPORT_SYMBOL_GPL(hd44780_common_fontsize); + +int hd44780_common_lines(struct charlcd *lcd, enum charlcd_lines lines) +{ + struct hd44780_common *hdc = lcd->drvdata; + + if (lines == CHARLCD_LINES_2) + hdc->hd44780_common_flags |= LCD_FLAG_N; + else + hdc->hd44780_common_flags &= ~LCD_FLAG_N; + + hd44780_common_set_function(hdc); + return 0; +} +EXPORT_SYMBOL_GPL(hd44780_common_lines); + +int hd44780_common_redefine_char(struct charlcd *lcd, char *esc) +{ + /* Generator : LGcxxxxx...xx; must have <c> between '0' + * and '7', representing the numerical ASCII code of the + * redefined character, and <xx...xx> a sequence of 16 + * hex digits representing 8 bytes for each character. + * Most LCDs will only use 5 lower bits of the 7 first + * bytes. + */ + + struct hd44780_common *hdc = lcd->drvdata; + unsigned char cgbytes[8]; + unsigned char cgaddr; + int cgoffset; + int shift; + char value; + int addr; + + if (!strchr(esc, ';')) + return 0; + + esc++; + + cgaddr = *(esc++) - '0'; + if (cgaddr > 7) + return 1; + + cgoffset = 0; + shift = 0; + value = 0; + while (*esc && cgoffset < 8) { + int half; + + shift ^= 4; + half = hex_to_bin(*esc++); + if (half < 0) + continue; + + value |= half << shift; + if (shift == 0) { + cgbytes[cgoffset++] = value; + value = 0; + } + } + + hdc->write_cmd(hdc, LCD_CMD_SET_CGRAM_ADDR | (cgaddr * 8)); + for (addr = 0; addr < cgoffset; addr++) + hdc->write_data(hdc, cgbytes[addr]); + + /* ensures that we stop writing to CGRAM */ + lcd->ops->gotoxy(lcd, lcd->addr.x, lcd->addr.y); + return 1; +} +EXPORT_SYMBOL_GPL(hd44780_common_redefine_char); + +struct hd44780_common *hd44780_common_alloc(void) +{ + struct hd44780_common *hd; + + hd = kzalloc(sizeof(*hd), GFP_KERNEL); + if (!hd) + return NULL; + + hd->ifwidth = 8; + hd->bwidth = DEFAULT_LCD_BWIDTH; + hd->hwidth = DEFAULT_LCD_HWIDTH; + return hd; +} +EXPORT_SYMBOL_GPL(hd44780_common_alloc); + +MODULE_LICENSE("GPL"); diff --git a/drivers/auxdisplay/hd44780_common.h b/drivers/auxdisplay/hd44780_common.h new file mode 100644 index 000000000..a16aa8c29 --- /dev/null +++ b/drivers/auxdisplay/hd44780_common.h @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#define DEFAULT_LCD_BWIDTH 40 +#define DEFAULT_LCD_HWIDTH 64 + +struct hd44780_common { + int ifwidth; /* 4-bit or 8-bit (default) */ + int bwidth; /* Default set by hd44780_alloc() */ + int hwidth; /* Default set by hd44780_alloc() */ + unsigned long hd44780_common_flags; + void (*write_data)(struct hd44780_common *hdc, int data); + void (*write_cmd)(struct hd44780_common *hdc, int cmd); + /* write_cmd_raw4 is for 4-bit connected displays only */ + void (*write_cmd_raw4)(struct hd44780_common *hdc, int cmd); + void *hd44780; +}; + +int hd44780_common_print(struct charlcd *lcd, int c); +int hd44780_common_gotoxy(struct charlcd *lcd, unsigned int x, unsigned int y); +int hd44780_common_home(struct charlcd *lcd); +int hd44780_common_clear_display(struct charlcd *lcd); +int hd44780_common_init_display(struct charlcd *lcd); +int hd44780_common_shift_cursor(struct charlcd *lcd, + enum charlcd_shift_dir dir); +int hd44780_common_shift_display(struct charlcd *lcd, + enum charlcd_shift_dir dir); +int hd44780_common_display(struct charlcd *lcd, enum charlcd_onoff on); +int hd44780_common_cursor(struct charlcd *lcd, enum charlcd_onoff on); +int hd44780_common_blink(struct charlcd *lcd, enum charlcd_onoff on); +int hd44780_common_fontsize(struct charlcd *lcd, enum charlcd_fontsize size); +int hd44780_common_lines(struct charlcd *lcd, enum charlcd_lines lines); +int hd44780_common_redefine_char(struct charlcd *lcd, char *esc); +struct hd44780_common *hd44780_common_alloc(void); diff --git a/drivers/auxdisplay/ht16k33.c b/drivers/auxdisplay/ht16k33.c new file mode 100644 index 000000000..02425991c --- /dev/null +++ b/drivers/auxdisplay/ht16k33.c @@ -0,0 +1,835 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * HT16K33 driver + * + * Author: Robin van der Gracht <robin@protonic.nl> + * + * Copyright: (C) 2016 Protonic Holland. + * Copyright (C) 2021 Glider bv + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/interrupt.h> +#include <linux/i2c.h> +#include <linux/property.h> +#include <linux/fb.h> +#include <linux/backlight.h> +#include <linux/input.h> +#include <linux/input/matrix_keypad.h> +#include <linux/leds.h> +#include <linux/workqueue.h> +#include <linux/mm.h> + +#include <linux/map_to_7segment.h> +#include <linux/map_to_14segment.h> + +#include <asm/unaligned.h> + +#include "line-display.h" + +/* Registers */ +#define REG_SYSTEM_SETUP 0x20 +#define REG_SYSTEM_SETUP_OSC_ON BIT(0) + +#define REG_DISPLAY_SETUP 0x80 +#define REG_DISPLAY_SETUP_ON BIT(0) +#define REG_DISPLAY_SETUP_BLINK_OFF (0 << 1) +#define REG_DISPLAY_SETUP_BLINK_2HZ (1 << 1) +#define REG_DISPLAY_SETUP_BLINK_1HZ (2 << 1) +#define REG_DISPLAY_SETUP_BLINK_0HZ5 (3 << 1) + +#define REG_ROWINT_SET 0xA0 +#define REG_ROWINT_SET_INT_EN BIT(0) +#define REG_ROWINT_SET_INT_ACT_HIGH BIT(1) + +#define REG_BRIGHTNESS 0xE0 + +/* Defines */ +#define DRIVER_NAME "ht16k33" + +#define MIN_BRIGHTNESS 0x1 +#define MAX_BRIGHTNESS 0x10 + +#define HT16K33_MATRIX_LED_MAX_COLS 8 +#define HT16K33_MATRIX_LED_MAX_ROWS 16 +#define HT16K33_MATRIX_KEYPAD_MAX_COLS 3 +#define HT16K33_MATRIX_KEYPAD_MAX_ROWS 12 + +#define BYTES_PER_ROW (HT16K33_MATRIX_LED_MAX_ROWS / 8) +#define HT16K33_FB_SIZE (HT16K33_MATRIX_LED_MAX_COLS * BYTES_PER_ROW) + +enum display_type { + DISP_MATRIX = 0, + DISP_QUAD_7SEG, + DISP_QUAD_14SEG, +}; + +struct ht16k33_keypad { + struct i2c_client *client; + struct input_dev *dev; + uint32_t cols; + uint32_t rows; + uint32_t row_shift; + uint32_t debounce_ms; + uint16_t last_key_state[HT16K33_MATRIX_KEYPAD_MAX_COLS]; + + wait_queue_head_t wait; + bool stopped; +}; + +struct ht16k33_fbdev { + struct fb_info *info; + uint32_t refresh_rate; + uint8_t *buffer; + uint8_t *cache; +}; + +struct ht16k33_seg { + struct linedisp linedisp; + union { + struct seg7_conversion_map seg7; + struct seg14_conversion_map seg14; + } map; + unsigned int map_size; + char curr[4]; +}; + +struct ht16k33_priv { + struct i2c_client *client; + struct delayed_work work; + struct led_classdev led; + struct ht16k33_keypad keypad; + union { + struct ht16k33_fbdev fbdev; + struct ht16k33_seg seg; + }; + enum display_type type; + uint8_t blink; +}; + +static const struct fb_fix_screeninfo ht16k33_fb_fix = { + .id = DRIVER_NAME, + .type = FB_TYPE_PACKED_PIXELS, + .visual = FB_VISUAL_MONO10, + .xpanstep = 0, + .ypanstep = 0, + .ywrapstep = 0, + .line_length = HT16K33_MATRIX_LED_MAX_ROWS, + .accel = FB_ACCEL_NONE, +}; + +static const struct fb_var_screeninfo ht16k33_fb_var = { + .xres = HT16K33_MATRIX_LED_MAX_ROWS, + .yres = HT16K33_MATRIX_LED_MAX_COLS, + .xres_virtual = HT16K33_MATRIX_LED_MAX_ROWS, + .yres_virtual = HT16K33_MATRIX_LED_MAX_COLS, + .bits_per_pixel = 1, + .red = { 0, 1, 0 }, + .green = { 0, 1, 0 }, + .blue = { 0, 1, 0 }, + .left_margin = 0, + .right_margin = 0, + .upper_margin = 0, + .lower_margin = 0, + .vmode = FB_VMODE_NONINTERLACED, +}; + +static const SEG7_DEFAULT_MAP(initial_map_seg7); +static const SEG14_DEFAULT_MAP(initial_map_seg14); + +static ssize_t map_seg_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct ht16k33_priv *priv = dev_get_drvdata(dev); + + memcpy(buf, &priv->seg.map, priv->seg.map_size); + return priv->seg.map_size; +} + +static ssize_t map_seg_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t cnt) +{ + struct ht16k33_priv *priv = dev_get_drvdata(dev); + + if (cnt != priv->seg.map_size) + return -EINVAL; + + memcpy(&priv->seg.map, buf, cnt); + return cnt; +} + +static DEVICE_ATTR(map_seg7, 0644, map_seg_show, map_seg_store); +static DEVICE_ATTR(map_seg14, 0644, map_seg_show, map_seg_store); + +static int ht16k33_display_on(struct ht16k33_priv *priv) +{ + uint8_t data = REG_DISPLAY_SETUP | REG_DISPLAY_SETUP_ON | priv->blink; + + return i2c_smbus_write_byte(priv->client, data); +} + +static int ht16k33_display_off(struct ht16k33_priv *priv) +{ + return i2c_smbus_write_byte(priv->client, REG_DISPLAY_SETUP); +} + +static int ht16k33_brightness_set(struct ht16k33_priv *priv, + unsigned int brightness) +{ + int err; + + if (brightness == 0) { + priv->blink = REG_DISPLAY_SETUP_BLINK_OFF; + return ht16k33_display_off(priv); + } + + err = ht16k33_display_on(priv); + if (err) + return err; + + return i2c_smbus_write_byte(priv->client, + REG_BRIGHTNESS | (brightness - 1)); +} + +static int ht16k33_brightness_set_blocking(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct ht16k33_priv *priv = container_of(led_cdev, struct ht16k33_priv, + led); + + return ht16k33_brightness_set(priv, brightness); +} + +static int ht16k33_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, unsigned long *delay_off) +{ + struct ht16k33_priv *priv = container_of(led_cdev, struct ht16k33_priv, + led); + unsigned int delay; + uint8_t blink; + int err; + + if (!*delay_on && !*delay_off) { + blink = REG_DISPLAY_SETUP_BLINK_1HZ; + delay = 1000; + } else if (*delay_on <= 750) { + blink = REG_DISPLAY_SETUP_BLINK_2HZ; + delay = 500; + } else if (*delay_on <= 1500) { + blink = REG_DISPLAY_SETUP_BLINK_1HZ; + delay = 1000; + } else { + blink = REG_DISPLAY_SETUP_BLINK_0HZ5; + delay = 2000; + } + + err = i2c_smbus_write_byte(priv->client, + REG_DISPLAY_SETUP | REG_DISPLAY_SETUP_ON | + blink); + if (err) + return err; + + priv->blink = blink; + *delay_on = *delay_off = delay; + return 0; +} + +static void ht16k33_fb_queue(struct ht16k33_priv *priv) +{ + struct ht16k33_fbdev *fbdev = &priv->fbdev; + + schedule_delayed_work(&priv->work, HZ / fbdev->refresh_rate); +} + +/* + * This gets the fb data from cache and copies it to ht16k33 display RAM + */ +static void ht16k33_fb_update(struct work_struct *work) +{ + struct ht16k33_priv *priv = container_of(work, struct ht16k33_priv, + work.work); + struct ht16k33_fbdev *fbdev = &priv->fbdev; + + uint8_t *p1, *p2; + int len, pos = 0, first = -1; + + p1 = fbdev->cache; + p2 = fbdev->buffer; + + /* Search for the first byte with changes */ + while (pos < HT16K33_FB_SIZE && first < 0) { + if (*(p1++) - *(p2++)) + first = pos; + pos++; + } + + /* No changes found */ + if (first < 0) + goto requeue; + + len = HT16K33_FB_SIZE - first; + p1 = fbdev->cache + HT16K33_FB_SIZE - 1; + p2 = fbdev->buffer + HT16K33_FB_SIZE - 1; + + /* Determine i2c transfer length */ + while (len > 1) { + if (*(p1--) - *(p2--)) + break; + len--; + } + + p1 = fbdev->cache + first; + p2 = fbdev->buffer + first; + if (!i2c_smbus_write_i2c_block_data(priv->client, first, len, p2)) + memcpy(p1, p2, len); +requeue: + ht16k33_fb_queue(priv); +} + +static int ht16k33_initialize(struct ht16k33_priv *priv) +{ + uint8_t data[HT16K33_FB_SIZE]; + uint8_t byte; + int err; + + /* Clear RAM (8 * 16 bits) */ + memset(data, 0, sizeof(data)); + err = i2c_smbus_write_block_data(priv->client, 0, sizeof(data), data); + if (err) + return err; + + /* Turn on internal oscillator */ + byte = REG_SYSTEM_SETUP_OSC_ON | REG_SYSTEM_SETUP; + err = i2c_smbus_write_byte(priv->client, byte); + if (err) + return err; + + /* Configure INT pin */ + byte = REG_ROWINT_SET | REG_ROWINT_SET_INT_ACT_HIGH; + if (priv->client->irq > 0) + byte |= REG_ROWINT_SET_INT_EN; + return i2c_smbus_write_byte(priv->client, byte); +} + +static int ht16k33_bl_update_status(struct backlight_device *bl) +{ + int brightness = bl->props.brightness; + struct ht16k33_priv *priv = bl_get_data(bl); + + if (bl->props.power != FB_BLANK_UNBLANK || + bl->props.fb_blank != FB_BLANK_UNBLANK || + bl->props.state & BL_CORE_FBBLANK) + brightness = 0; + + return ht16k33_brightness_set(priv, brightness); +} + +static int ht16k33_bl_check_fb(struct backlight_device *bl, struct fb_info *fi) +{ + struct ht16k33_priv *priv = bl_get_data(bl); + + return (fi == NULL) || (fi->par == priv); +} + +static const struct backlight_ops ht16k33_bl_ops = { + .update_status = ht16k33_bl_update_status, + .check_fb = ht16k33_bl_check_fb, +}; + +/* + * Blank events will be passed to the actual device handling the backlight when + * we return zero here. + */ +static int ht16k33_blank(int blank, struct fb_info *info) +{ + return 0; +} + +static int ht16k33_mmap(struct fb_info *info, struct vm_area_struct *vma) +{ + struct ht16k33_priv *priv = info->par; + struct page *pages = virt_to_page(priv->fbdev.buffer); + + return vm_map_pages_zero(vma, &pages, 1); +} + +static const struct fb_ops ht16k33_fb_ops = { + .owner = THIS_MODULE, + .fb_read = fb_sys_read, + .fb_write = fb_sys_write, + .fb_blank = ht16k33_blank, + .fb_fillrect = sys_fillrect, + .fb_copyarea = sys_copyarea, + .fb_imageblit = sys_imageblit, + .fb_mmap = ht16k33_mmap, +}; + +/* + * This gets the keys from keypad and reports it to input subsystem. + * Returns true if a key is pressed. + */ +static bool ht16k33_keypad_scan(struct ht16k33_keypad *keypad) +{ + const unsigned short *keycodes = keypad->dev->keycode; + u16 new_state[HT16K33_MATRIX_KEYPAD_MAX_COLS]; + __le16 data[HT16K33_MATRIX_KEYPAD_MAX_COLS]; + unsigned long bits_changed; + int row, col, code; + int rc; + bool pressed = false; + + rc = i2c_smbus_read_i2c_block_data(keypad->client, 0x40, + sizeof(data), (u8 *)data); + if (rc != sizeof(data)) { + dev_err(&keypad->client->dev, + "Failed to read key data, rc=%d\n", rc); + return false; + } + + for (col = 0; col < keypad->cols; col++) { + new_state[col] = le16_to_cpu(data[col]); + if (new_state[col]) + pressed = true; + bits_changed = keypad->last_key_state[col] ^ new_state[col]; + + for_each_set_bit(row, &bits_changed, BITS_PER_LONG) { + code = MATRIX_SCAN_CODE(row, col, keypad->row_shift); + input_event(keypad->dev, EV_MSC, MSC_SCAN, code); + input_report_key(keypad->dev, keycodes[code], + new_state[col] & BIT(row)); + } + } + input_sync(keypad->dev); + memcpy(keypad->last_key_state, new_state, sizeof(u16) * keypad->cols); + + return pressed; +} + +static irqreturn_t ht16k33_keypad_irq_thread(int irq, void *dev) +{ + struct ht16k33_keypad *keypad = dev; + + do { + wait_event_timeout(keypad->wait, keypad->stopped, + msecs_to_jiffies(keypad->debounce_ms)); + if (keypad->stopped) + break; + } while (ht16k33_keypad_scan(keypad)); + + return IRQ_HANDLED; +} + +static int ht16k33_keypad_start(struct input_dev *dev) +{ + struct ht16k33_keypad *keypad = input_get_drvdata(dev); + + keypad->stopped = false; + mb(); + enable_irq(keypad->client->irq); + + return 0; +} + +static void ht16k33_keypad_stop(struct input_dev *dev) +{ + struct ht16k33_keypad *keypad = input_get_drvdata(dev); + + keypad->stopped = true; + mb(); + wake_up(&keypad->wait); + disable_irq(keypad->client->irq); +} + +static void ht16k33_linedisp_update(struct linedisp *linedisp) +{ + struct ht16k33_priv *priv = container_of(linedisp, struct ht16k33_priv, + seg.linedisp); + + schedule_delayed_work(&priv->work, 0); +} + +static void ht16k33_seg7_update(struct work_struct *work) +{ + struct ht16k33_priv *priv = container_of(work, struct ht16k33_priv, + work.work); + struct ht16k33_seg *seg = &priv->seg; + char *s = seg->curr; + uint8_t buf[9]; + + buf[0] = map_to_seg7(&seg->map.seg7, *s++); + buf[1] = 0; + buf[2] = map_to_seg7(&seg->map.seg7, *s++); + buf[3] = 0; + buf[4] = 0; + buf[5] = 0; + buf[6] = map_to_seg7(&seg->map.seg7, *s++); + buf[7] = 0; + buf[8] = map_to_seg7(&seg->map.seg7, *s++); + + i2c_smbus_write_i2c_block_data(priv->client, 0, ARRAY_SIZE(buf), buf); +} + +static void ht16k33_seg14_update(struct work_struct *work) +{ + struct ht16k33_priv *priv = container_of(work, struct ht16k33_priv, + work.work); + struct ht16k33_seg *seg = &priv->seg; + char *s = seg->curr; + uint8_t buf[8]; + + put_unaligned_le16(map_to_seg14(&seg->map.seg14, *s++), buf); + put_unaligned_le16(map_to_seg14(&seg->map.seg14, *s++), buf + 2); + put_unaligned_le16(map_to_seg14(&seg->map.seg14, *s++), buf + 4); + put_unaligned_le16(map_to_seg14(&seg->map.seg14, *s++), buf + 6); + + i2c_smbus_write_i2c_block_data(priv->client, 0, ARRAY_SIZE(buf), buf); +} + +static int ht16k33_led_probe(struct device *dev, struct led_classdev *led, + unsigned int brightness) +{ + struct led_init_data init_data = {}; + int err; + + /* The LED is optional */ + init_data.fwnode = device_get_named_child_node(dev, "led"); + if (!init_data.fwnode) + return 0; + + init_data.devicename = "auxdisplay"; + init_data.devname_mandatory = true; + + led->brightness_set_blocking = ht16k33_brightness_set_blocking; + led->blink_set = ht16k33_blink_set; + led->flags = LED_CORE_SUSPENDRESUME; + led->brightness = brightness; + led->max_brightness = MAX_BRIGHTNESS; + + err = devm_led_classdev_register_ext(dev, led, &init_data); + if (err) + dev_err(dev, "Failed to register LED\n"); + + return err; +} + +static int ht16k33_keypad_probe(struct i2c_client *client, + struct ht16k33_keypad *keypad) +{ + struct device *dev = &client->dev; + u32 rows = HT16K33_MATRIX_KEYPAD_MAX_ROWS; + u32 cols = HT16K33_MATRIX_KEYPAD_MAX_COLS; + int err; + + keypad->client = client; + init_waitqueue_head(&keypad->wait); + + keypad->dev = devm_input_allocate_device(dev); + if (!keypad->dev) + return -ENOMEM; + + input_set_drvdata(keypad->dev, keypad); + + keypad->dev->name = DRIVER_NAME"-keypad"; + keypad->dev->id.bustype = BUS_I2C; + keypad->dev->open = ht16k33_keypad_start; + keypad->dev->close = ht16k33_keypad_stop; + + if (!device_property_read_bool(dev, "linux,no-autorepeat")) + __set_bit(EV_REP, keypad->dev->evbit); + + err = device_property_read_u32(dev, "debounce-delay-ms", + &keypad->debounce_ms); + if (err) { + dev_err(dev, "key debounce delay not specified\n"); + return err; + } + + err = matrix_keypad_parse_properties(dev, &rows, &cols); + if (err) + return err; + if (rows > HT16K33_MATRIX_KEYPAD_MAX_ROWS || + cols > HT16K33_MATRIX_KEYPAD_MAX_COLS) { + dev_err(dev, "%u rows or %u cols out of range in DT\n", rows, + cols); + return -ERANGE; + } + + keypad->rows = rows; + keypad->cols = cols; + keypad->row_shift = get_count_order(cols); + + err = matrix_keypad_build_keymap(NULL, NULL, rows, cols, NULL, + keypad->dev); + if (err) { + dev_err(dev, "failed to build keymap\n"); + return err; + } + + err = devm_request_threaded_irq(dev, client->irq, NULL, + ht16k33_keypad_irq_thread, + IRQF_TRIGGER_HIGH | IRQF_ONESHOT, + DRIVER_NAME, keypad); + if (err) { + dev_err(dev, "irq request failed %d, error %d\n", client->irq, + err); + return err; + } + + ht16k33_keypad_stop(keypad->dev); + + return input_register_device(keypad->dev); +} + +static int ht16k33_fbdev_probe(struct device *dev, struct ht16k33_priv *priv, + uint32_t brightness) +{ + struct ht16k33_fbdev *fbdev = &priv->fbdev; + struct backlight_device *bl = NULL; + int err; + + if (priv->led.dev) { + err = ht16k33_brightness_set(priv, brightness); + if (err) + return err; + } else { + /* backwards compatibility with DT lacking an led subnode */ + struct backlight_properties bl_props; + + memset(&bl_props, 0, sizeof(struct backlight_properties)); + bl_props.type = BACKLIGHT_RAW; + bl_props.max_brightness = MAX_BRIGHTNESS; + + bl = devm_backlight_device_register(dev, DRIVER_NAME"-bl", dev, + priv, &ht16k33_bl_ops, + &bl_props); + if (IS_ERR(bl)) { + dev_err(dev, "failed to register backlight\n"); + return PTR_ERR(bl); + } + + bl->props.brightness = brightness; + ht16k33_bl_update_status(bl); + } + + /* Framebuffer (2 bytes per column) */ + BUILD_BUG_ON(PAGE_SIZE < HT16K33_FB_SIZE); + fbdev->buffer = (unsigned char *) get_zeroed_page(GFP_KERNEL); + if (!fbdev->buffer) + return -ENOMEM; + + fbdev->cache = devm_kmalloc(dev, HT16K33_FB_SIZE, GFP_KERNEL); + if (!fbdev->cache) { + err = -ENOMEM; + goto err_fbdev_buffer; + } + + fbdev->info = framebuffer_alloc(0, dev); + if (!fbdev->info) { + err = -ENOMEM; + goto err_fbdev_buffer; + } + + err = device_property_read_u32(dev, "refresh-rate-hz", + &fbdev->refresh_rate); + if (err) { + dev_err(dev, "refresh rate not specified\n"); + goto err_fbdev_info; + } + fb_bl_default_curve(fbdev->info, 0, MIN_BRIGHTNESS, MAX_BRIGHTNESS); + + INIT_DELAYED_WORK(&priv->work, ht16k33_fb_update); + fbdev->info->fbops = &ht16k33_fb_ops; + fbdev->info->screen_base = (char __iomem *) fbdev->buffer; + fbdev->info->screen_size = HT16K33_FB_SIZE; + fbdev->info->fix = ht16k33_fb_fix; + fbdev->info->var = ht16k33_fb_var; + fbdev->info->bl_dev = bl; + fbdev->info->pseudo_palette = NULL; + fbdev->info->flags = FBINFO_FLAG_DEFAULT; + fbdev->info->par = priv; + + err = register_framebuffer(fbdev->info); + if (err) + goto err_fbdev_info; + + ht16k33_fb_queue(priv); + return 0; + +err_fbdev_info: + framebuffer_release(fbdev->info); +err_fbdev_buffer: + free_page((unsigned long) fbdev->buffer); + + return err; +} + +static int ht16k33_seg_probe(struct device *dev, struct ht16k33_priv *priv, + uint32_t brightness) +{ + struct ht16k33_seg *seg = &priv->seg; + int err; + + err = ht16k33_brightness_set(priv, brightness); + if (err) + return err; + + switch (priv->type) { + case DISP_MATRIX: + /* not handled here */ + err = -EINVAL; + break; + + case DISP_QUAD_7SEG: + INIT_DELAYED_WORK(&priv->work, ht16k33_seg7_update); + seg->map.seg7 = initial_map_seg7; + seg->map_size = sizeof(seg->map.seg7); + err = device_create_file(dev, &dev_attr_map_seg7); + break; + + case DISP_QUAD_14SEG: + INIT_DELAYED_WORK(&priv->work, ht16k33_seg14_update); + seg->map.seg14 = initial_map_seg14; + seg->map_size = sizeof(seg->map.seg14); + err = device_create_file(dev, &dev_attr_map_seg14); + break; + } + if (err) + return err; + + err = linedisp_register(&seg->linedisp, dev, 4, seg->curr, + ht16k33_linedisp_update); + if (err) + goto err_remove_map_file; + + return 0; + +err_remove_map_file: + device_remove_file(dev, &dev_attr_map_seg7); + device_remove_file(dev, &dev_attr_map_seg14); + return err; +} + +static int ht16k33_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + const struct of_device_id *id; + struct ht16k33_priv *priv; + uint32_t dft_brightness; + int err; + + if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) { + dev_err(dev, "i2c_check_functionality error\n"); + return -EIO; + } + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->client = client; + id = i2c_of_match_device(dev->driver->of_match_table, client); + if (id) + priv->type = (uintptr_t)id->data; + i2c_set_clientdata(client, priv); + + err = ht16k33_initialize(priv); + if (err) + return err; + + err = device_property_read_u32(dev, "default-brightness-level", + &dft_brightness); + if (err) { + dft_brightness = MAX_BRIGHTNESS; + } else if (dft_brightness > MAX_BRIGHTNESS) { + dev_warn(dev, + "invalid default brightness level: %u, using %u\n", + dft_brightness, MAX_BRIGHTNESS); + dft_brightness = MAX_BRIGHTNESS; + } + + /* LED */ + err = ht16k33_led_probe(dev, &priv->led, dft_brightness); + if (err) + return err; + + /* Keypad */ + if (client->irq > 0) { + err = ht16k33_keypad_probe(client, &priv->keypad); + if (err) + return err; + } + + switch (priv->type) { + case DISP_MATRIX: + /* Frame Buffer Display */ + err = ht16k33_fbdev_probe(dev, priv, dft_brightness); + break; + + case DISP_QUAD_7SEG: + case DISP_QUAD_14SEG: + /* Segment Display */ + err = ht16k33_seg_probe(dev, priv, dft_brightness); + break; + } + return err; +} + +static void ht16k33_remove(struct i2c_client *client) +{ + struct ht16k33_priv *priv = i2c_get_clientdata(client); + struct ht16k33_fbdev *fbdev = &priv->fbdev; + + cancel_delayed_work_sync(&priv->work); + + switch (priv->type) { + case DISP_MATRIX: + unregister_framebuffer(fbdev->info); + framebuffer_release(fbdev->info); + free_page((unsigned long)fbdev->buffer); + break; + + case DISP_QUAD_7SEG: + case DISP_QUAD_14SEG: + linedisp_unregister(&priv->seg.linedisp); + device_remove_file(&client->dev, &dev_attr_map_seg7); + device_remove_file(&client->dev, &dev_attr_map_seg14); + break; + } +} + +static const struct i2c_device_id ht16k33_i2c_match[] = { + { "ht16k33", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ht16k33_i2c_match); + +static const struct of_device_id ht16k33_of_match[] = { + { + /* 0.56" 4-Digit 7-Segment FeatherWing Display (Red) */ + .compatible = "adafruit,3108", .data = (void *)DISP_QUAD_7SEG, + }, { + /* 0.54" Quad Alphanumeric FeatherWing Display (Red) */ + .compatible = "adafruit,3130", .data = (void *)DISP_QUAD_14SEG, + }, { + /* Generic, assumed Dot-Matrix Display */ + .compatible = "holtek,ht16k33", .data = (void *)DISP_MATRIX, + }, + { } +}; +MODULE_DEVICE_TABLE(of, ht16k33_of_match); + +static struct i2c_driver ht16k33_driver = { + .probe_new = ht16k33_probe, + .remove = ht16k33_remove, + .driver = { + .name = DRIVER_NAME, + .of_match_table = ht16k33_of_match, + }, + .id_table = ht16k33_i2c_match, +}; +module_i2c_driver(ht16k33_driver); + +MODULE_DESCRIPTION("Holtek HT16K33 driver"); +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Robin van der Gracht <robin@protonic.nl>"); diff --git a/drivers/auxdisplay/img-ascii-lcd.c b/drivers/auxdisplay/img-ascii-lcd.c new file mode 100644 index 000000000..fa23e415f --- /dev/null +++ b/drivers/auxdisplay/img-ascii-lcd.c @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2016 Imagination Technologies + * Author: Paul Burton <paul.burton@mips.com> + */ + +#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 "line-display.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 linedisp *linedisp); +}; + +/** + * struct img_ascii_lcd_ctx - Private data structure + * @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 + * @linedisp: line display structure + * @curr: the string currently displayed on the LCD + */ +struct img_ascii_lcd_ctx { + union { + void __iomem *base; + struct regmap *regmap; + }; + u32 offset; + const struct img_ascii_lcd_config *cfg; + struct linedisp linedisp; + char curr[] __aligned(8); +}; + +/* + * MIPS Boston development board + */ + +static void boston_update(struct linedisp *linedisp) +{ + struct img_ascii_lcd_ctx *ctx = + container_of(linedisp, struct img_ascii_lcd_ctx, linedisp); + 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 linedisp *linedisp) +{ + struct img_ascii_lcd_ctx *ctx = + container_of(linedisp, struct img_ascii_lcd_ctx, linedisp); + unsigned int i; + int err = 0; + + for (i = 0; i < linedisp->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 linedisp *linedisp) +{ + struct img_ascii_lcd_ctx *ctx = + container_of(linedisp, struct img_ascii_lcd_ctx, linedisp); + unsigned int i; + int err = 0; + + for (i = 0; i < linedisp->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_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 device *dev = &pdev->dev; + struct img_ascii_lcd_ctx *ctx; + int err; + + match = of_match_device(img_ascii_lcd_matches, dev); + if (!match) + return -ENODEV; + + cfg = match->data; + ctx = devm_kzalloc(dev, sizeof(*ctx) + cfg->num_chars, GFP_KERNEL); + if (!ctx) + return -ENOMEM; + + if (cfg->external_regmap) { + ctx->regmap = syscon_node_to_regmap(dev->parent->of_node); + if (IS_ERR(ctx->regmap)) + return PTR_ERR(ctx->regmap); + + if (of_property_read_u32(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); + } + + err = linedisp_register(&ctx->linedisp, dev, cfg->num_chars, ctx->curr, + cfg->update); + if (err) + return err; + + /* for backwards compatibility */ + err = compat_only_sysfs_link_entry_to_kobj(&dev->kobj, + &ctx->linedisp.dev.kobj, + "message", NULL); + if (err) + goto err_unregister; + + platform_set_drvdata(pdev, ctx); + return 0; + +err_unregister: + linedisp_unregister(&ctx->linedisp); + 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); + + sysfs_remove_link(&pdev->dev.kobj, "message"); + linedisp_unregister(&ctx->linedisp); + 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"); diff --git a/drivers/auxdisplay/ks0108.c b/drivers/auxdisplay/ks0108.c new file mode 100644 index 000000000..234f9dbe6 --- /dev/null +++ b/drivers/auxdisplay/ks0108.c @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Filename: ks0108.c + * Version: 0.1.0 + * Description: ks0108 LCD Controller driver + * Depends: parport + * + * Author: Copyright (C) Miguel Ojeda <ojeda@kernel.org> + * Date: 2006-10-31 + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/init.h> +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/delay.h> +#include <linux/parport.h> +#include <linux/ks0108.h> + +#define KS0108_NAME "ks0108" + +/* + * Module Parameters + */ + +static unsigned int ks0108_port = CONFIG_KS0108_PORT; +module_param(ks0108_port, uint, 0444); +MODULE_PARM_DESC(ks0108_port, "Parallel port where the LCD is connected"); + +static unsigned int ks0108_delay = CONFIG_KS0108_DELAY; +module_param(ks0108_delay, uint, 0444); +MODULE_PARM_DESC(ks0108_delay, "Delay between each control writing (microseconds)"); + +/* + * Device + */ + +static struct parport *ks0108_parport; +static struct pardevice *ks0108_pardevice; + +/* + * ks0108 Exported Commands (don't lock) + * + * You _should_ lock in the top driver: This functions _should not_ + * get race conditions in any way. Locking for each byte here would be + * so slow and useless. + * + * There are not bit definitions because they are not flags, + * just arbitrary combinations defined by the documentation for each + * function in the ks0108 LCD controller. If you want to know what means + * a specific combination, look at the function's name. + * + * The ks0108_writecontrol bits need to be reverted ^(0,1,3) because + * the parallel port also revert them using a "not" logic gate. + */ + +#define bit(n) (((unsigned char)1)<<(n)) + +void ks0108_writedata(unsigned char byte) +{ + parport_write_data(ks0108_parport, byte); +} + +void ks0108_writecontrol(unsigned char byte) +{ + udelay(ks0108_delay); + parport_write_control(ks0108_parport, byte ^ (bit(0) | bit(1) | bit(3))); +} + +void ks0108_displaystate(unsigned char state) +{ + ks0108_writedata((state ? bit(0) : 0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5)); +} + +void ks0108_startline(unsigned char startline) +{ + ks0108_writedata(min_t(unsigned char, startline, 63) | bit(6) | + bit(7)); +} + +void ks0108_address(unsigned char address) +{ + ks0108_writedata(min_t(unsigned char, address, 63) | bit(6)); +} + +void ks0108_page(unsigned char page) +{ + ks0108_writedata(min_t(unsigned char, page, 7) | bit(3) | bit(4) | + bit(5) | bit(7)); +} + +EXPORT_SYMBOL_GPL(ks0108_writedata); +EXPORT_SYMBOL_GPL(ks0108_writecontrol); +EXPORT_SYMBOL_GPL(ks0108_displaystate); +EXPORT_SYMBOL_GPL(ks0108_startline); +EXPORT_SYMBOL_GPL(ks0108_address); +EXPORT_SYMBOL_GPL(ks0108_page); + +/* + * Is the module inited? + */ + +static unsigned char ks0108_inited; +unsigned char ks0108_isinited(void) +{ + return ks0108_inited; +} +EXPORT_SYMBOL_GPL(ks0108_isinited); + +static void ks0108_parport_attach(struct parport *port) +{ + struct pardev_cb ks0108_cb; + + if (port->base != ks0108_port) + return; + + memset(&ks0108_cb, 0, sizeof(ks0108_cb)); + ks0108_cb.flags = PARPORT_DEV_EXCL; + ks0108_pardevice = parport_register_dev_model(port, KS0108_NAME, + &ks0108_cb, 0); + if (!ks0108_pardevice) { + pr_err("ERROR: parport didn't register new device\n"); + return; + } + if (parport_claim(ks0108_pardevice)) { + pr_err("could not claim access to parport %i. Aborting.\n", + ks0108_port); + goto err_unreg_device; + } + + ks0108_parport = port; + ks0108_inited = 1; + return; + +err_unreg_device: + parport_unregister_device(ks0108_pardevice); + ks0108_pardevice = NULL; +} + +static void ks0108_parport_detach(struct parport *port) +{ + if (port->base != ks0108_port) + return; + + if (!ks0108_pardevice) { + pr_err("%s: already unregistered.\n", KS0108_NAME); + return; + } + + parport_release(ks0108_pardevice); + parport_unregister_device(ks0108_pardevice); + ks0108_pardevice = NULL; + ks0108_parport = NULL; +} + +/* + * Module Init & Exit + */ + +static struct parport_driver ks0108_parport_driver = { + .name = "ks0108", + .match_port = ks0108_parport_attach, + .detach = ks0108_parport_detach, + .devmodel = true, +}; +module_parport_driver(ks0108_parport_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Miguel Ojeda <ojeda@kernel.org>"); +MODULE_DESCRIPTION("ks0108 LCD Controller driver"); + diff --git a/drivers/auxdisplay/lcd2s.c b/drivers/auxdisplay/lcd2s.c new file mode 100644 index 000000000..135831a16 --- /dev/null +++ b/drivers/auxdisplay/lcd2s.c @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Console driver for LCD2S 4x20 character displays connected through i2c. + * The display also has a SPI interface, but the driver does not support + * this yet. + * + * This is a driver allowing you to use a LCD2S 4x20 from Modtronix + * engineering as auxdisplay character device. + * + * (C) 2019 by Lemonage Software GmbH + * Author: Lars Pöschel <poeschel@lemonage.de> + * All rights reserved. + */ +#include <linux/kernel.h> +#include <linux/mod_devicetable.h> +#include <linux/module.h> +#include <linux/property.h> +#include <linux/slab.h> +#include <linux/i2c.h> +#include <linux/delay.h> + +#include "charlcd.h" + +#define LCD2S_CMD_CUR_MOVES_FWD 0x09 +#define LCD2S_CMD_CUR_BLINK_OFF 0x10 +#define LCD2S_CMD_CUR_UL_OFF 0x11 +#define LCD2S_CMD_DISPLAY_OFF 0x12 +#define LCD2S_CMD_CUR_BLINK_ON 0x18 +#define LCD2S_CMD_CUR_UL_ON 0x19 +#define LCD2S_CMD_DISPLAY_ON 0x1a +#define LCD2S_CMD_BACKLIGHT_OFF 0x20 +#define LCD2S_CMD_BACKLIGHT_ON 0x28 +#define LCD2S_CMD_WRITE 0x80 +#define LCD2S_CMD_MOV_CUR_RIGHT 0x83 +#define LCD2S_CMD_MOV_CUR_LEFT 0x84 +#define LCD2S_CMD_SHIFT_RIGHT 0x85 +#define LCD2S_CMD_SHIFT_LEFT 0x86 +#define LCD2S_CMD_SHIFT_UP 0x87 +#define LCD2S_CMD_SHIFT_DOWN 0x88 +#define LCD2S_CMD_CUR_ADDR 0x89 +#define LCD2S_CMD_CUR_POS 0x8a +#define LCD2S_CMD_CUR_RESET 0x8b +#define LCD2S_CMD_CLEAR 0x8c +#define LCD2S_CMD_DEF_CUSTOM_CHAR 0x92 +#define LCD2S_CMD_READ_STATUS 0xd0 + +#define LCD2S_CHARACTER_SIZE 8 + +#define LCD2S_STATUS_BUF_MASK 0x7f + +struct lcd2s_data { + struct i2c_client *i2c; + struct charlcd *charlcd; +}; + +static s32 lcd2s_wait_buf_free(const struct i2c_client *client, int count) +{ + s32 status; + + status = i2c_smbus_read_byte_data(client, LCD2S_CMD_READ_STATUS); + if (status < 0) + return status; + + while ((status & LCD2S_STATUS_BUF_MASK) < count) { + mdelay(1); + status = i2c_smbus_read_byte_data(client, + LCD2S_CMD_READ_STATUS); + if (status < 0) + return status; + } + return 0; +} + +static int lcd2s_i2c_master_send(const struct i2c_client *client, + const char *buf, int count) +{ + s32 status; + + status = lcd2s_wait_buf_free(client, count); + if (status < 0) + return status; + + return i2c_master_send(client, buf, count); +} + +static int lcd2s_i2c_smbus_write_byte(const struct i2c_client *client, u8 value) +{ + s32 status; + + status = lcd2s_wait_buf_free(client, 1); + if (status < 0) + return status; + + return i2c_smbus_write_byte(client, value); +} + +static int lcd2s_print(struct charlcd *lcd, int c) +{ + struct lcd2s_data *lcd2s = lcd->drvdata; + u8 buf[2] = { LCD2S_CMD_WRITE, c }; + + lcd2s_i2c_master_send(lcd2s->i2c, buf, sizeof(buf)); + return 0; +} + +static int lcd2s_gotoxy(struct charlcd *lcd, unsigned int x, unsigned int y) +{ + struct lcd2s_data *lcd2s = lcd->drvdata; + u8 buf[3] = { LCD2S_CMD_CUR_POS, y + 1, x + 1 }; + + lcd2s_i2c_master_send(lcd2s->i2c, buf, sizeof(buf)); + + return 0; +} + +static int lcd2s_home(struct charlcd *lcd) +{ + struct lcd2s_data *lcd2s = lcd->drvdata; + + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_CUR_RESET); + return 0; +} + +static int lcd2s_init_display(struct charlcd *lcd) +{ + struct lcd2s_data *lcd2s = lcd->drvdata; + + /* turn everything off, but display on */ + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_DISPLAY_ON); + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_BACKLIGHT_OFF); + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_CUR_MOVES_FWD); + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_CUR_BLINK_OFF); + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_CUR_UL_OFF); + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_CLEAR); + + return 0; +} + +static int lcd2s_shift_cursor(struct charlcd *lcd, enum charlcd_shift_dir dir) +{ + struct lcd2s_data *lcd2s = lcd->drvdata; + + if (dir == CHARLCD_SHIFT_LEFT) + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_MOV_CUR_LEFT); + else + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_MOV_CUR_RIGHT); + + return 0; +} + +static int lcd2s_shift_display(struct charlcd *lcd, enum charlcd_shift_dir dir) +{ + struct lcd2s_data *lcd2s = lcd->drvdata; + + if (dir == CHARLCD_SHIFT_LEFT) + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_SHIFT_LEFT); + else + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_SHIFT_RIGHT); + + return 0; +} + +static void lcd2s_backlight(struct charlcd *lcd, enum charlcd_onoff on) +{ + struct lcd2s_data *lcd2s = lcd->drvdata; + + if (on) + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_BACKLIGHT_ON); + else + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_BACKLIGHT_OFF); +} + +static int lcd2s_display(struct charlcd *lcd, enum charlcd_onoff on) +{ + struct lcd2s_data *lcd2s = lcd->drvdata; + + if (on) + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_DISPLAY_ON); + else + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_DISPLAY_OFF); + + return 0; +} + +static int lcd2s_cursor(struct charlcd *lcd, enum charlcd_onoff on) +{ + struct lcd2s_data *lcd2s = lcd->drvdata; + + if (on) + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_CUR_UL_ON); + else + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_CUR_UL_OFF); + + return 0; +} + +static int lcd2s_blink(struct charlcd *lcd, enum charlcd_onoff on) +{ + struct lcd2s_data *lcd2s = lcd->drvdata; + + if (on) + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_CUR_BLINK_ON); + else + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_CUR_BLINK_OFF); + + return 0; +} + +static int lcd2s_fontsize(struct charlcd *lcd, enum charlcd_fontsize size) +{ + return 0; +} + +static int lcd2s_lines(struct charlcd *lcd, enum charlcd_lines lines) +{ + return 0; +} + +/* + * Generator: LGcxxxxx...xx; must have <c> between '0' and '7', + * representing the numerical ASCII code of the redefined character, + * and <xx...xx> a sequence of 16 hex digits representing 8 bytes + * for each character. Most LCDs will only use 5 lower bits of + * the 7 first bytes. + */ +static int lcd2s_redefine_char(struct charlcd *lcd, char *esc) +{ + struct lcd2s_data *lcd2s = lcd->drvdata; + u8 buf[LCD2S_CHARACTER_SIZE + 2] = { LCD2S_CMD_DEF_CUSTOM_CHAR }; + u8 value; + int shift, i; + + if (!strchr(esc, ';')) + return 0; + + esc++; + + buf[1] = *(esc++) - '0'; + if (buf[1] > 7) + return 1; + + i = 2; + shift = 0; + value = 0; + while (*esc && i < LCD2S_CHARACTER_SIZE + 2) { + int half; + + shift ^= 4; + half = hex_to_bin(*esc++); + if (half < 0) + continue; + + value |= half << shift; + if (shift == 0) { + buf[i++] = value; + value = 0; + } + } + + lcd2s_i2c_master_send(lcd2s->i2c, buf, sizeof(buf)); + return 1; +} + +static int lcd2s_clear_display(struct charlcd *lcd) +{ + struct lcd2s_data *lcd2s = lcd->drvdata; + + /* This implicitly sets cursor to first row and column */ + lcd2s_i2c_smbus_write_byte(lcd2s->i2c, LCD2S_CMD_CLEAR); + return 0; +} + +static const struct charlcd_ops lcd2s_ops = { + .print = lcd2s_print, + .backlight = lcd2s_backlight, + .gotoxy = lcd2s_gotoxy, + .home = lcd2s_home, + .clear_display = lcd2s_clear_display, + .init_display = lcd2s_init_display, + .shift_cursor = lcd2s_shift_cursor, + .shift_display = lcd2s_shift_display, + .display = lcd2s_display, + .cursor = lcd2s_cursor, + .blink = lcd2s_blink, + .fontsize = lcd2s_fontsize, + .lines = lcd2s_lines, + .redefine_char = lcd2s_redefine_char, +}; + +static int lcd2s_i2c_probe(struct i2c_client *i2c) +{ + struct charlcd *lcd; + struct lcd2s_data *lcd2s; + int err; + + if (!i2c_check_functionality(i2c->adapter, + I2C_FUNC_SMBUS_WRITE_BYTE_DATA | + I2C_FUNC_SMBUS_WRITE_BLOCK_DATA)) + return -EIO; + + lcd2s = devm_kzalloc(&i2c->dev, sizeof(*lcd2s), GFP_KERNEL); + if (!lcd2s) + return -ENOMEM; + + /* Test, if the display is responding */ + err = lcd2s_i2c_smbus_write_byte(i2c, LCD2S_CMD_DISPLAY_OFF); + if (err < 0) + return err; + + lcd = charlcd_alloc(); + if (!lcd) + return -ENOMEM; + + lcd->drvdata = lcd2s; + lcd2s->i2c = i2c; + lcd2s->charlcd = lcd; + + /* Required properties */ + err = device_property_read_u32(&i2c->dev, "display-height-chars", + &lcd->height); + if (err) + goto fail1; + + err = device_property_read_u32(&i2c->dev, "display-width-chars", + &lcd->width); + if (err) + goto fail1; + + lcd->ops = &lcd2s_ops; + + err = charlcd_register(lcd2s->charlcd); + if (err) + goto fail1; + + i2c_set_clientdata(i2c, lcd2s); + return 0; + +fail1: + charlcd_free(lcd2s->charlcd); + return err; +} + +static void lcd2s_i2c_remove(struct i2c_client *i2c) +{ + struct lcd2s_data *lcd2s = i2c_get_clientdata(i2c); + + charlcd_unregister(lcd2s->charlcd); + charlcd_free(lcd2s->charlcd); +} + +static const struct i2c_device_id lcd2s_i2c_id[] = { + { "lcd2s", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, lcd2s_i2c_id); + +static const struct of_device_id lcd2s_of_table[] = { + { .compatible = "modtronix,lcd2s" }, + { } +}; +MODULE_DEVICE_TABLE(of, lcd2s_of_table); + +static struct i2c_driver lcd2s_i2c_driver = { + .driver = { + .name = "lcd2s", + .of_match_table = lcd2s_of_table, + }, + .probe_new = lcd2s_i2c_probe, + .remove = lcd2s_i2c_remove, + .id_table = lcd2s_i2c_id, +}; +module_i2c_driver(lcd2s_i2c_driver); + +MODULE_DESCRIPTION("LCD2S character display driver"); +MODULE_AUTHOR("Lars Poeschel"); +MODULE_LICENSE("GPL"); diff --git a/drivers/auxdisplay/line-display.c b/drivers/auxdisplay/line-display.c new file mode 100644 index 000000000..03e7f104a --- /dev/null +++ b/drivers/auxdisplay/line-display.c @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Character line display core support + * + * Copyright (C) 2016 Imagination Technologies + * Author: Paul Burton <paul.burton@mips.com> + * + * Copyright (C) 2021 Glider bv + */ + +#include <generated/utsrelease.h> + +#include <linux/device.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/string.h> +#include <linux/sysfs.h> +#include <linux/timer.h> + +#include "line-display.h" + +#define DEFAULT_SCROLL_RATE (HZ / 2) + +/** + * linedisp_scroll() - scroll the display by a character + * @t: really a pointer to the private data structure + * + * Scroll the current message along the display by one character, rearming the + * timer if required. + */ +static void linedisp_scroll(struct timer_list *t) +{ + struct linedisp *linedisp = from_timer(linedisp, t, timer); + unsigned int i, ch = linedisp->scroll_pos; + unsigned int num_chars = linedisp->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 < linedisp->message_len; i++, ch++) + linedisp->buf[i] = linedisp->message[ch]; + + /* wrap around to the start of the string */ + ch = 0; + } + + /* update the display */ + linedisp->update(linedisp); + + /* move on to the next character */ + linedisp->scroll_pos++; + linedisp->scroll_pos %= linedisp->message_len; + + /* rearm the timer */ + if (linedisp->message_len > num_chars && linedisp->scroll_rate) + mod_timer(&linedisp->timer, jiffies + linedisp->scroll_rate); +} + +/** + * linedisp_display() - set the message to be displayed + * @linedisp: pointer to the private data structure + * @msg: the message to display + * @count: length of msg, or -1 + * + * Display a new message @msg on the display. @msg can be longer than the + * number of characters the display can display, in which case it will begin + * scrolling across the display. + * + * Return: 0 on success, -ENOMEM on memory allocation failure + */ +static int linedisp_display(struct linedisp *linedisp, const char *msg, + ssize_t count) +{ + char *new_msg; + + /* stop the scroll timer */ + del_timer_sync(&linedisp->timer); + + if (count == -1) + count = strlen(msg); + + /* if the string ends with a newline, trim it */ + if (msg[count - 1] == '\n') + count--; + + if (!count) { + /* Clear the display */ + kfree(linedisp->message); + linedisp->message = NULL; + linedisp->message_len = 0; + memset(linedisp->buf, ' ', linedisp->num_chars); + linedisp->update(linedisp); + return 0; + } + + new_msg = kmemdup_nul(msg, count, GFP_KERNEL); + if (!new_msg) + return -ENOMEM; + + kfree(linedisp->message); + + linedisp->message = new_msg; + linedisp->message_len = count; + linedisp->scroll_pos = 0; + + /* update the display */ + linedisp_scroll(&linedisp->timer); + + return 0; +} + +/** + * message_show() - read message via sysfs + * @dev: the display device + * @attr: the display message attribute + * @buf: the buffer to read the message into + * + * Read the current message being displayed or scrolled across the 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 linedisp *linedisp = container_of(dev, struct linedisp, dev); + + return sysfs_emit(buf, "%s\n", linedisp->message); +} + +/** + * message_store() - write a new message via sysfs + * @dev: the display device + * @attr: the display 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 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 linedisp *linedisp = container_of(dev, struct linedisp, dev); + int err; + + err = linedisp_display(linedisp, buf, count); + return err ?: count; +} + +static DEVICE_ATTR_RW(message); + +static ssize_t scroll_step_ms_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct linedisp *linedisp = container_of(dev, struct linedisp, dev); + + return sysfs_emit(buf, "%u\n", jiffies_to_msecs(linedisp->scroll_rate)); +} + +static ssize_t scroll_step_ms_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct linedisp *linedisp = container_of(dev, struct linedisp, dev); + unsigned int ms; + + if (kstrtouint(buf, 10, &ms) != 0) + return -EINVAL; + + linedisp->scroll_rate = msecs_to_jiffies(ms); + if (linedisp->message && linedisp->message_len > linedisp->num_chars) { + del_timer_sync(&linedisp->timer); + if (linedisp->scroll_rate) + linedisp_scroll(&linedisp->timer); + } + + return count; +} + +static DEVICE_ATTR_RW(scroll_step_ms); + +static struct attribute *linedisp_attrs[] = { + &dev_attr_message.attr, + &dev_attr_scroll_step_ms.attr, + NULL, +}; +ATTRIBUTE_GROUPS(linedisp); + +static const struct device_type linedisp_type = { + .groups = linedisp_groups, +}; + +/** + * linedisp_register - register a character line display + * @linedisp: pointer to character line display structure + * @parent: parent device + * @num_chars: the number of characters that can be displayed + * @buf: pointer to a buffer that can hold @num_chars characters + * @update: Function called to update the display. This must not sleep! + * + * Return: zero on success, else a negative error code. + */ +int linedisp_register(struct linedisp *linedisp, struct device *parent, + unsigned int num_chars, char *buf, + void (*update)(struct linedisp *linedisp)) +{ + static atomic_t linedisp_id = ATOMIC_INIT(-1); + int err; + + memset(linedisp, 0, sizeof(*linedisp)); + linedisp->dev.parent = parent; + linedisp->dev.type = &linedisp_type; + linedisp->update = update; + linedisp->buf = buf; + linedisp->num_chars = num_chars; + linedisp->scroll_rate = DEFAULT_SCROLL_RATE; + + device_initialize(&linedisp->dev); + dev_set_name(&linedisp->dev, "linedisp.%lu", + (unsigned long)atomic_inc_return(&linedisp_id)); + + /* initialise a timer for scrolling the message */ + timer_setup(&linedisp->timer, linedisp_scroll, 0); + + err = device_add(&linedisp->dev); + if (err) + goto out_del_timer; + + /* display a default message */ + err = linedisp_display(linedisp, "Linux " UTS_RELEASE " ", -1); + if (err) + goto out_del_dev; + + return 0; + +out_del_dev: + device_del(&linedisp->dev); +out_del_timer: + del_timer_sync(&linedisp->timer); + put_device(&linedisp->dev); + return err; +} +EXPORT_SYMBOL_GPL(linedisp_register); + +/** + * linedisp_unregister - unregister a character line display + * @linedisp: pointer to character line display structure registered previously + * with linedisp_register() + */ +void linedisp_unregister(struct linedisp *linedisp) +{ + device_del(&linedisp->dev); + del_timer_sync(&linedisp->timer); + kfree(linedisp->message); + put_device(&linedisp->dev); +} +EXPORT_SYMBOL_GPL(linedisp_unregister); + +MODULE_LICENSE("GPL"); diff --git a/drivers/auxdisplay/line-display.h b/drivers/auxdisplay/line-display.h new file mode 100644 index 000000000..0f5891d34 --- /dev/null +++ b/drivers/auxdisplay/line-display.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Character line display core support + * + * Copyright (C) 2016 Imagination Technologies + * Author: Paul Burton <paul.burton@mips.com> + * + * Copyright (C) 2021 Glider bv + */ + +#ifndef _LINEDISP_H +#define _LINEDISP_H + +/** + * struct linedisp - character line display private data structure + * @dev: the line display device + * @timer: timer used to implement scrolling + * @update: function called to update the display + * @buf: pointer to the buffer for the string currently displayed + * @message: the full message to display or scroll on the display + * @num_chars: the number of characters that can be displayed + * @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 + */ +struct linedisp { + struct device dev; + struct timer_list timer; + void (*update)(struct linedisp *linedisp); + char *buf; + char *message; + unsigned int num_chars; + unsigned int message_len; + unsigned int scroll_pos; + unsigned int scroll_rate; +}; + +int linedisp_register(struct linedisp *linedisp, struct device *parent, + unsigned int num_chars, char *buf, + void (*update)(struct linedisp *linedisp)); +void linedisp_unregister(struct linedisp *linedisp); + +#endif /* LINEDISP_H */ diff --git a/drivers/auxdisplay/panel.c b/drivers/auxdisplay/panel.c new file mode 100644 index 000000000..eba04c0de --- /dev/null +++ b/drivers/auxdisplay/panel.c @@ -0,0 +1,1739 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Front panel driver for Linux + * Copyright (C) 2000-2008, Willy Tarreau <w@1wt.eu> + * Copyright (C) 2016-2017 Glider bvba + * + * This code drives an LCD module (/dev/lcd), and a keypad (/dev/keypad) + * connected to a parallel printer port. + * + * The LCD module may either be an HD44780-like 8-bit parallel LCD, or a 1-bit + * serial module compatible with Samsung's KS0074. The pins may be connected in + * any combination, everything is programmable. + * + * The keypad consists in a matrix of push buttons connecting input pins to + * data output pins or to the ground. The combinations have to be hard-coded + * in the driver, though several profiles exist and adding new ones is easy. + * + * Several profiles are provided for commonly found LCD+keypad modules on the + * market, such as those found in Nexcom's appliances. + * + * FIXME: + * - the initialization/deinitialization process is very dirty and should + * be rewritten. It may even be buggy. + * + * TODO: + * - document 24 keys keyboard (3 rows of 8 cols, 32 diodes + 2 inputs) + * - make the LCD a part of a virtual screen of Vx*Vy + * - make the inputs list smp-safe + * - change the keyboard to a double mapping : signals -> key_id -> values + * so that applications can change values without knowing signals + * + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/module.h> + +#include <linux/types.h> +#include <linux/errno.h> +#include <linux/signal.h> +#include <linux/sched.h> +#include <linux/spinlock.h> +#include <linux/interrupt.h> +#include <linux/miscdevice.h> +#include <linux/slab.h> +#include <linux/ioport.h> +#include <linux/fcntl.h> +#include <linux/init.h> +#include <linux/delay.h> +#include <linux/kernel.h> +#include <linux/ctype.h> +#include <linux/parport.h> +#include <linux/list.h> + +#include <linux/io.h> +#include <linux/uaccess.h> + +#include "charlcd.h" +#include "hd44780_common.h" + +#define LCD_MAXBYTES 256 /* max burst write */ + +#define KEYPAD_BUFFER 64 + +/* poll the keyboard this every second */ +#define INPUT_POLL_TIME (HZ / 50) +/* a key starts to repeat after this times INPUT_POLL_TIME */ +#define KEYPAD_REP_START (10) +/* a key repeats this times INPUT_POLL_TIME */ +#define KEYPAD_REP_DELAY (2) + +/* converts an r_str() input to an active high, bits string : 000BAOSE */ +#define PNL_PINPUT(a) ((((unsigned char)(a)) ^ 0x7F) >> 3) + +#define PNL_PBUSY 0x80 /* inverted input, active low */ +#define PNL_PACK 0x40 /* direct input, active low */ +#define PNL_POUTPA 0x20 /* direct input, active high */ +#define PNL_PSELECD 0x10 /* direct input, active high */ +#define PNL_PERRORP 0x08 /* direct input, active low */ + +#define PNL_PBIDIR 0x20 /* bi-directional ports */ +/* high to read data in or-ed with data out */ +#define PNL_PINTEN 0x10 +#define PNL_PSELECP 0x08 /* inverted output, active low */ +#define PNL_PINITP 0x04 /* direct output, active low */ +#define PNL_PAUTOLF 0x02 /* inverted output, active low */ +#define PNL_PSTROBE 0x01 /* inverted output */ + +#define PNL_PD0 0x01 +#define PNL_PD1 0x02 +#define PNL_PD2 0x04 +#define PNL_PD3 0x08 +#define PNL_PD4 0x10 +#define PNL_PD5 0x20 +#define PNL_PD6 0x40 +#define PNL_PD7 0x80 + +#define PIN_NONE 0 +#define PIN_STROBE 1 +#define PIN_D0 2 +#define PIN_D1 3 +#define PIN_D2 4 +#define PIN_D3 5 +#define PIN_D4 6 +#define PIN_D5 7 +#define PIN_D6 8 +#define PIN_D7 9 +#define PIN_AUTOLF 14 +#define PIN_INITP 16 +#define PIN_SELECP 17 +#define PIN_NOT_SET 127 + +#define NOT_SET -1 + +/* macros to simplify use of the parallel port */ +#define r_ctr(x) (parport_read_control((x)->port)) +#define r_dtr(x) (parport_read_data((x)->port)) +#define r_str(x) (parport_read_status((x)->port)) +#define w_ctr(x, y) (parport_write_control((x)->port, (y))) +#define w_dtr(x, y) (parport_write_data((x)->port, (y))) + +/* this defines which bits are to be used and which ones to be ignored */ +/* logical or of the output bits involved in the scan matrix */ +static __u8 scan_mask_o; +/* logical or of the input bits involved in the scan matrix */ +static __u8 scan_mask_i; + +enum input_type { + INPUT_TYPE_STD, + INPUT_TYPE_KBD, +}; + +enum input_state { + INPUT_ST_LOW, + INPUT_ST_RISING, + INPUT_ST_HIGH, + INPUT_ST_FALLING, +}; + +struct logical_input { + struct list_head list; + __u64 mask; + __u64 value; + enum input_type type; + enum input_state state; + __u8 rise_time, fall_time; + __u8 rise_timer, fall_timer, high_timer; + + union { + struct { /* valid when type == INPUT_TYPE_STD */ + void (*press_fct)(int); + void (*release_fct)(int); + int press_data; + int release_data; + } std; + struct { /* valid when type == INPUT_TYPE_KBD */ + char press_str[sizeof(void *) + sizeof(int)] __nonstring; + char repeat_str[sizeof(void *) + sizeof(int)] __nonstring; + char release_str[sizeof(void *) + sizeof(int)] __nonstring; + } kbd; + } u; +}; + +static LIST_HEAD(logical_inputs); /* list of all defined logical inputs */ + +/* physical contacts history + * Physical contacts are a 45 bits string of 9 groups of 5 bits each. + * The 8 lower groups correspond to output bits 0 to 7, and the 9th group + * corresponds to the ground. + * Within each group, bits are stored in the same order as read on the port : + * BAPSE (busy=4, ack=3, paper empty=2, select=1, error=0). + * So, each __u64 is represented like this : + * 0000000000000000000BAPSEBAPSEBAPSEBAPSEBAPSEBAPSEBAPSEBAPSEBAPSE + * <-----unused------><gnd><d07><d06><d05><d04><d03><d02><d01><d00> + */ + +/* what has just been read from the I/O ports */ +static __u64 phys_read; +/* previous phys_read */ +static __u64 phys_read_prev; +/* stabilized phys_read (phys_read|phys_read_prev) */ +static __u64 phys_curr; +/* previous phys_curr */ +static __u64 phys_prev; +/* 0 means that at least one logical signal needs be computed */ +static char inputs_stable; + +/* these variables are specific to the keypad */ +static struct { + bool enabled; +} keypad; + +static char keypad_buffer[KEYPAD_BUFFER]; +static int keypad_buflen; +static int keypad_start; +static char keypressed; +static wait_queue_head_t keypad_read_wait; + +/* lcd-specific variables */ +static struct { + bool enabled; + bool initialized; + + int charset; + int proto; + + /* TODO: use union here? */ + struct { + int e; + int rs; + int rw; + int cl; + int da; + int bl; + } pins; + + struct charlcd *charlcd; +} lcd; + +/* Needed only for init */ +static int selected_lcd_type = NOT_SET; + +/* + * Bit masks to convert LCD signals to parallel port outputs. + * _d_ are values for data port, _c_ are for control port. + * [0] = signal OFF, [1] = signal ON, [2] = mask + */ +#define BIT_CLR 0 +#define BIT_SET 1 +#define BIT_MSK 2 +#define BIT_STATES 3 +/* + * one entry for each bit on the LCD + */ +#define LCD_BIT_E 0 +#define LCD_BIT_RS 1 +#define LCD_BIT_RW 2 +#define LCD_BIT_BL 3 +#define LCD_BIT_CL 4 +#define LCD_BIT_DA 5 +#define LCD_BITS 6 + +/* + * each bit can be either connected to a DATA or CTRL port + */ +#define LCD_PORT_C 0 +#define LCD_PORT_D 1 +#define LCD_PORTS 2 + +static unsigned char lcd_bits[LCD_PORTS][LCD_BITS][BIT_STATES]; + +/* + * LCD protocols + */ +#define LCD_PROTO_PARALLEL 0 +#define LCD_PROTO_SERIAL 1 +#define LCD_PROTO_TI_DA8XX_LCD 2 + +/* + * LCD character sets + */ +#define LCD_CHARSET_NORMAL 0 +#define LCD_CHARSET_KS0074 1 + +/* + * LCD types + */ +#define LCD_TYPE_NONE 0 +#define LCD_TYPE_CUSTOM 1 +#define LCD_TYPE_OLD 2 +#define LCD_TYPE_KS0074 3 +#define LCD_TYPE_HANTRONIX 4 +#define LCD_TYPE_NEXCOM 5 + +/* + * keypad types + */ +#define KEYPAD_TYPE_NONE 0 +#define KEYPAD_TYPE_OLD 1 +#define KEYPAD_TYPE_NEW 2 +#define KEYPAD_TYPE_NEXCOM 3 + +/* + * panel profiles + */ +#define PANEL_PROFILE_CUSTOM 0 +#define PANEL_PROFILE_OLD 1 +#define PANEL_PROFILE_NEW 2 +#define PANEL_PROFILE_HANTRONIX 3 +#define PANEL_PROFILE_NEXCOM 4 +#define PANEL_PROFILE_LARGE 5 + +/* + * Construct custom config from the kernel's configuration + */ +#define DEFAULT_PARPORT 0 +#define DEFAULT_PROFILE PANEL_PROFILE_LARGE +#define DEFAULT_KEYPAD_TYPE KEYPAD_TYPE_OLD +#define DEFAULT_LCD_TYPE LCD_TYPE_OLD +#define DEFAULT_LCD_HEIGHT 2 +#define DEFAULT_LCD_WIDTH 40 +#define DEFAULT_LCD_CHARSET LCD_CHARSET_NORMAL +#define DEFAULT_LCD_PROTO LCD_PROTO_PARALLEL + +#define DEFAULT_LCD_PIN_E PIN_AUTOLF +#define DEFAULT_LCD_PIN_RS PIN_SELECP +#define DEFAULT_LCD_PIN_RW PIN_INITP +#define DEFAULT_LCD_PIN_SCL PIN_STROBE +#define DEFAULT_LCD_PIN_SDA PIN_D0 +#define DEFAULT_LCD_PIN_BL PIN_NOT_SET + +#ifdef CONFIG_PANEL_PARPORT +#undef DEFAULT_PARPORT +#define DEFAULT_PARPORT CONFIG_PANEL_PARPORT +#endif + +#ifdef CONFIG_PANEL_PROFILE +#undef DEFAULT_PROFILE +#define DEFAULT_PROFILE CONFIG_PANEL_PROFILE +#endif + +#if DEFAULT_PROFILE == 0 /* custom */ +#ifdef CONFIG_PANEL_KEYPAD +#undef DEFAULT_KEYPAD_TYPE +#define DEFAULT_KEYPAD_TYPE CONFIG_PANEL_KEYPAD +#endif + +#ifdef CONFIG_PANEL_LCD +#undef DEFAULT_LCD_TYPE +#define DEFAULT_LCD_TYPE CONFIG_PANEL_LCD +#endif + +#ifdef CONFIG_PANEL_LCD_HEIGHT +#undef DEFAULT_LCD_HEIGHT +#define DEFAULT_LCD_HEIGHT CONFIG_PANEL_LCD_HEIGHT +#endif + +#ifdef CONFIG_PANEL_LCD_WIDTH +#undef DEFAULT_LCD_WIDTH +#define DEFAULT_LCD_WIDTH CONFIG_PANEL_LCD_WIDTH +#endif + +#ifdef CONFIG_PANEL_LCD_BWIDTH +#undef DEFAULT_LCD_BWIDTH +#define DEFAULT_LCD_BWIDTH CONFIG_PANEL_LCD_BWIDTH +#endif + +#ifdef CONFIG_PANEL_LCD_HWIDTH +#undef DEFAULT_LCD_HWIDTH +#define DEFAULT_LCD_HWIDTH CONFIG_PANEL_LCD_HWIDTH +#endif + +#ifdef CONFIG_PANEL_LCD_CHARSET +#undef DEFAULT_LCD_CHARSET +#define DEFAULT_LCD_CHARSET CONFIG_PANEL_LCD_CHARSET +#endif + +#ifdef CONFIG_PANEL_LCD_PROTO +#undef DEFAULT_LCD_PROTO +#define DEFAULT_LCD_PROTO CONFIG_PANEL_LCD_PROTO +#endif + +#ifdef CONFIG_PANEL_LCD_PIN_E +#undef DEFAULT_LCD_PIN_E +#define DEFAULT_LCD_PIN_E CONFIG_PANEL_LCD_PIN_E +#endif + +#ifdef CONFIG_PANEL_LCD_PIN_RS +#undef DEFAULT_LCD_PIN_RS +#define DEFAULT_LCD_PIN_RS CONFIG_PANEL_LCD_PIN_RS +#endif + +#ifdef CONFIG_PANEL_LCD_PIN_RW +#undef DEFAULT_LCD_PIN_RW +#define DEFAULT_LCD_PIN_RW CONFIG_PANEL_LCD_PIN_RW +#endif + +#ifdef CONFIG_PANEL_LCD_PIN_SCL +#undef DEFAULT_LCD_PIN_SCL +#define DEFAULT_LCD_PIN_SCL CONFIG_PANEL_LCD_PIN_SCL +#endif + +#ifdef CONFIG_PANEL_LCD_PIN_SDA +#undef DEFAULT_LCD_PIN_SDA +#define DEFAULT_LCD_PIN_SDA CONFIG_PANEL_LCD_PIN_SDA +#endif + +#ifdef CONFIG_PANEL_LCD_PIN_BL +#undef DEFAULT_LCD_PIN_BL +#define DEFAULT_LCD_PIN_BL CONFIG_PANEL_LCD_PIN_BL +#endif + +#endif /* DEFAULT_PROFILE == 0 */ + +/* global variables */ + +/* Device single-open policy control */ +static atomic_t keypad_available = ATOMIC_INIT(1); + +static struct pardevice *pprt; + +static int keypad_initialized; + +static DEFINE_SPINLOCK(pprt_lock); +static struct timer_list scan_timer; + +MODULE_DESCRIPTION("Generic parallel port LCD/Keypad driver"); + +static int parport = DEFAULT_PARPORT; +module_param(parport, int, 0000); +MODULE_PARM_DESC(parport, "Parallel port index (0=lpt1, 1=lpt2, ...)"); + +static int profile = DEFAULT_PROFILE; +module_param(profile, int, 0000); +MODULE_PARM_DESC(profile, + "1=16x2 old kp; 2=serial 16x2, new kp; 3=16x2 hantronix; " + "4=16x2 nexcom; default=40x2, old kp"); + +static int keypad_type = NOT_SET; +module_param(keypad_type, int, 0000); +MODULE_PARM_DESC(keypad_type, + "Keypad type: 0=none, 1=old 6 keys, 2=new 6+1 keys, 3=nexcom 4 keys"); + +static int lcd_type = NOT_SET; +module_param(lcd_type, int, 0000); +MODULE_PARM_DESC(lcd_type, + "LCD type: 0=none, 1=compiled-in, 2=old, 3=serial ks0074, 4=hantronix, 5=nexcom"); + +static int lcd_height = NOT_SET; +module_param(lcd_height, int, 0000); +MODULE_PARM_DESC(lcd_height, "Number of lines on the LCD"); + +static int lcd_width = NOT_SET; +module_param(lcd_width, int, 0000); +MODULE_PARM_DESC(lcd_width, "Number of columns on the LCD"); + +static int lcd_bwidth = NOT_SET; /* internal buffer width (usually 40) */ +module_param(lcd_bwidth, int, 0000); +MODULE_PARM_DESC(lcd_bwidth, "Internal LCD line width (40)"); + +static int lcd_hwidth = NOT_SET; /* hardware buffer width (usually 64) */ +module_param(lcd_hwidth, int, 0000); +MODULE_PARM_DESC(lcd_hwidth, "LCD line hardware address (64)"); + +static int lcd_charset = NOT_SET; +module_param(lcd_charset, int, 0000); +MODULE_PARM_DESC(lcd_charset, "LCD character set: 0=standard, 1=KS0074"); + +static int lcd_proto = NOT_SET; +module_param(lcd_proto, int, 0000); +MODULE_PARM_DESC(lcd_proto, + "LCD communication: 0=parallel (//), 1=serial, 2=TI LCD Interface"); + +/* + * These are the parallel port pins the LCD control signals are connected to. + * Set this to 0 if the signal is not used. Set it to its opposite value + * (negative) if the signal is negated. -MAXINT is used to indicate that the + * pin has not been explicitly specified. + * + * WARNING! no check will be performed about collisions with keypad ! + */ + +static int lcd_e_pin = PIN_NOT_SET; +module_param(lcd_e_pin, int, 0000); +MODULE_PARM_DESC(lcd_e_pin, + "# of the // port pin connected to LCD 'E' signal, with polarity (-17..17)"); + +static int lcd_rs_pin = PIN_NOT_SET; +module_param(lcd_rs_pin, int, 0000); +MODULE_PARM_DESC(lcd_rs_pin, + "# of the // port pin connected to LCD 'RS' signal, with polarity (-17..17)"); + +static int lcd_rw_pin = PIN_NOT_SET; +module_param(lcd_rw_pin, int, 0000); +MODULE_PARM_DESC(lcd_rw_pin, + "# of the // port pin connected to LCD 'RW' signal, with polarity (-17..17)"); + +static int lcd_cl_pin = PIN_NOT_SET; +module_param(lcd_cl_pin, int, 0000); +MODULE_PARM_DESC(lcd_cl_pin, + "# of the // port pin connected to serial LCD 'SCL' signal, with polarity (-17..17)"); + +static int lcd_da_pin = PIN_NOT_SET; +module_param(lcd_da_pin, int, 0000); +MODULE_PARM_DESC(lcd_da_pin, + "# of the // port pin connected to serial LCD 'SDA' signal, with polarity (-17..17)"); + +static int lcd_bl_pin = PIN_NOT_SET; +module_param(lcd_bl_pin, int, 0000); +MODULE_PARM_DESC(lcd_bl_pin, + "# of the // port pin connected to LCD backlight, with polarity (-17..17)"); + +/* Deprecated module parameters - consider not using them anymore */ + +static int lcd_enabled = NOT_SET; +module_param(lcd_enabled, int, 0000); +MODULE_PARM_DESC(lcd_enabled, "Deprecated option, use lcd_type instead"); + +static int keypad_enabled = NOT_SET; +module_param(keypad_enabled, int, 0000); +MODULE_PARM_DESC(keypad_enabled, "Deprecated option, use keypad_type instead"); + +/* for some LCD drivers (ks0074) we need a charset conversion table. */ +static const unsigned char lcd_char_conv_ks0074[256] = { + /* 0|8 1|9 2|A 3|B 4|C 5|D 6|E 7|F */ + /* 0x00 */ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + /* 0x08 */ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + /* 0x10 */ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + /* 0x18 */ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + /* 0x20 */ 0x20, 0x21, 0x22, 0x23, 0xa2, 0x25, 0x26, 0x27, + /* 0x28 */ 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + /* 0x30 */ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, + /* 0x38 */ 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, + /* 0x40 */ 0xa0, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + /* 0x48 */ 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + /* 0x50 */ 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, + /* 0x58 */ 0x58, 0x59, 0x5a, 0xfa, 0xfb, 0xfc, 0x1d, 0xc4, + /* 0x60 */ 0x96, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + /* 0x68 */ 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, + /* 0x70 */ 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + /* 0x78 */ 0x78, 0x79, 0x7a, 0xfd, 0xfe, 0xff, 0xce, 0x20, + /* 0x80 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + /* 0x88 */ 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, + /* 0x90 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + /* 0x98 */ 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, + /* 0xA0 */ 0x20, 0x40, 0xb1, 0xa1, 0x24, 0xa3, 0xfe, 0x5f, + /* 0xA8 */ 0x22, 0xc8, 0x61, 0x14, 0x97, 0x2d, 0xad, 0x96, + /* 0xB0 */ 0x80, 0x8c, 0x82, 0x83, 0x27, 0x8f, 0x86, 0xdd, + /* 0xB8 */ 0x2c, 0x81, 0x6f, 0x15, 0x8b, 0x8a, 0x84, 0x60, + /* 0xC0 */ 0xe2, 0xe2, 0xe2, 0x5b, 0x5b, 0xae, 0xbc, 0xa9, + /* 0xC8 */ 0xc5, 0xbf, 0xc6, 0xf1, 0xe3, 0xe3, 0xe3, 0xe3, + /* 0xD0 */ 0x44, 0x5d, 0xa8, 0xe4, 0xec, 0xec, 0x5c, 0x78, + /* 0xD8 */ 0xab, 0xa6, 0xe5, 0x5e, 0x5e, 0xe6, 0xaa, 0xbe, + /* 0xE0 */ 0x7f, 0xe7, 0xaf, 0x7b, 0x7b, 0xaf, 0xbd, 0xc8, + /* 0xE8 */ 0xa4, 0xa5, 0xc7, 0xf6, 0xa7, 0xe8, 0x69, 0x69, + /* 0xF0 */ 0xed, 0x7d, 0xa8, 0xe4, 0xec, 0x5c, 0x5c, 0x25, + /* 0xF8 */ 0xac, 0xa6, 0xea, 0xef, 0x7e, 0xeb, 0xb2, 0x79, +}; + +static const char old_keypad_profile[][4][9] = { + {"S0", "Left\n", "Left\n", ""}, + {"S1", "Down\n", "Down\n", ""}, + {"S2", "Up\n", "Up\n", ""}, + {"S3", "Right\n", "Right\n", ""}, + {"S4", "Esc\n", "Esc\n", ""}, + {"S5", "Ret\n", "Ret\n", ""}, + {"", "", "", ""} +}; + +/* signals, press, repeat, release */ +static const char new_keypad_profile[][4][9] = { + {"S0", "Left\n", "Left\n", ""}, + {"S1", "Down\n", "Down\n", ""}, + {"S2", "Up\n", "Up\n", ""}, + {"S3", "Right\n", "Right\n", ""}, + {"S4s5", "", "Esc\n", "Esc\n"}, + {"s4S5", "", "Ret\n", "Ret\n"}, + {"S4S5", "Help\n", "", ""}, + /* add new signals above this line */ + {"", "", "", ""} +}; + +/* signals, press, repeat, release */ +static const char nexcom_keypad_profile[][4][9] = { + {"a-p-e-", "Down\n", "Down\n", ""}, + {"a-p-E-", "Ret\n", "Ret\n", ""}, + {"a-P-E-", "Esc\n", "Esc\n", ""}, + {"a-P-e-", "Up\n", "Up\n", ""}, + /* add new signals above this line */ + {"", "", "", ""} +}; + +static const char (*keypad_profile)[4][9] = old_keypad_profile; + +static DECLARE_BITMAP(bits, LCD_BITS); + +static void lcd_get_bits(unsigned int port, int *val) +{ + unsigned int bit, state; + + for (bit = 0; bit < LCD_BITS; bit++) { + state = test_bit(bit, bits) ? BIT_SET : BIT_CLR; + *val &= lcd_bits[port][bit][BIT_MSK]; + *val |= lcd_bits[port][bit][state]; + } +} + +/* sets data port bits according to current signals values */ +static int set_data_bits(void) +{ + int val; + + val = r_dtr(pprt); + lcd_get_bits(LCD_PORT_D, &val); + w_dtr(pprt, val); + return val; +} + +/* sets ctrl port bits according to current signals values */ +static int set_ctrl_bits(void) +{ + int val; + + val = r_ctr(pprt); + lcd_get_bits(LCD_PORT_C, &val); + w_ctr(pprt, val); + return val; +} + +/* sets ctrl & data port bits according to current signals values */ +static void panel_set_bits(void) +{ + set_data_bits(); + set_ctrl_bits(); +} + +/* + * Converts a parallel port pin (from -25 to 25) to data and control ports + * masks, and data and control port bits. The signal will be considered + * unconnected if it's on pin 0 or an invalid pin (<-25 or >25). + * + * Result will be used this way : + * out(dport, in(dport) & d_val[2] | d_val[signal_state]) + * out(cport, in(cport) & c_val[2] | c_val[signal_state]) + */ +static void pin_to_bits(int pin, unsigned char *d_val, unsigned char *c_val) +{ + int d_bit, c_bit, inv; + + d_val[0] = 0; + c_val[0] = 0; + d_val[1] = 0; + c_val[1] = 0; + d_val[2] = 0xFF; + c_val[2] = 0xFF; + + if (pin == 0) + return; + + inv = (pin < 0); + if (inv) + pin = -pin; + + d_bit = 0; + c_bit = 0; + + switch (pin) { + case PIN_STROBE: /* strobe, inverted */ + c_bit = PNL_PSTROBE; + inv = !inv; + break; + case PIN_D0...PIN_D7: /* D0 - D7 = 2 - 9 */ + d_bit = 1 << (pin - 2); + break; + case PIN_AUTOLF: /* autofeed, inverted */ + c_bit = PNL_PAUTOLF; + inv = !inv; + break; + case PIN_INITP: /* init, direct */ + c_bit = PNL_PINITP; + break; + case PIN_SELECP: /* select_in, inverted */ + c_bit = PNL_PSELECP; + inv = !inv; + break; + default: /* unknown pin, ignore */ + break; + } + + if (c_bit) { + c_val[2] &= ~c_bit; + c_val[!inv] = c_bit; + } else if (d_bit) { + d_val[2] &= ~d_bit; + d_val[!inv] = d_bit; + } +} + +/* + * send a serial byte to the LCD panel. The caller is responsible for locking + * if needed. + */ +static void lcd_send_serial(int byte) +{ + int bit; + + /* + * the data bit is set on D0, and the clock on STROBE. + * LCD reads D0 on STROBE's rising edge. + */ + for (bit = 0; bit < 8; bit++) { + clear_bit(LCD_BIT_CL, bits); /* CLK low */ + panel_set_bits(); + if (byte & 1) { + set_bit(LCD_BIT_DA, bits); + } else { + clear_bit(LCD_BIT_DA, bits); + } + + panel_set_bits(); + udelay(2); /* maintain the data during 2 us before CLK up */ + set_bit(LCD_BIT_CL, bits); /* CLK high */ + panel_set_bits(); + udelay(1); /* maintain the strobe during 1 us */ + byte >>= 1; + } +} + +/* turn the backlight on or off */ +static void lcd_backlight(struct charlcd *charlcd, enum charlcd_onoff on) +{ + if (lcd.pins.bl == PIN_NONE) + return; + + /* The backlight is activated by setting the AUTOFEED line to +5V */ + spin_lock_irq(&pprt_lock); + if (on) + set_bit(LCD_BIT_BL, bits); + else + clear_bit(LCD_BIT_BL, bits); + panel_set_bits(); + spin_unlock_irq(&pprt_lock); +} + +/* send a command to the LCD panel in serial mode */ +static void lcd_write_cmd_s(struct hd44780_common *hdc, int cmd) +{ + spin_lock_irq(&pprt_lock); + lcd_send_serial(0x1F); /* R/W=W, RS=0 */ + lcd_send_serial(cmd & 0x0F); + lcd_send_serial((cmd >> 4) & 0x0F); + udelay(40); /* the shortest command takes at least 40 us */ + spin_unlock_irq(&pprt_lock); +} + +/* send data to the LCD panel in serial mode */ +static void lcd_write_data_s(struct hd44780_common *hdc, int data) +{ + spin_lock_irq(&pprt_lock); + lcd_send_serial(0x5F); /* R/W=W, RS=1 */ + lcd_send_serial(data & 0x0F); + lcd_send_serial((data >> 4) & 0x0F); + udelay(40); /* the shortest data takes at least 40 us */ + spin_unlock_irq(&pprt_lock); +} + +/* send a command to the LCD panel in 8 bits parallel mode */ +static void lcd_write_cmd_p8(struct hd44780_common *hdc, int cmd) +{ + spin_lock_irq(&pprt_lock); + /* present the data to the data port */ + w_dtr(pprt, cmd); + udelay(20); /* maintain the data during 20 us before the strobe */ + + set_bit(LCD_BIT_E, bits); + clear_bit(LCD_BIT_RS, bits); + clear_bit(LCD_BIT_RW, bits); + set_ctrl_bits(); + + udelay(40); /* maintain the strobe during 40 us */ + + clear_bit(LCD_BIT_E, bits); + set_ctrl_bits(); + + udelay(120); /* the shortest command takes at least 120 us */ + spin_unlock_irq(&pprt_lock); +} + +/* send data to the LCD panel in 8 bits parallel mode */ +static void lcd_write_data_p8(struct hd44780_common *hdc, int data) +{ + spin_lock_irq(&pprt_lock); + /* present the data to the data port */ + w_dtr(pprt, data); + udelay(20); /* maintain the data during 20 us before the strobe */ + + set_bit(LCD_BIT_E, bits); + set_bit(LCD_BIT_RS, bits); + clear_bit(LCD_BIT_RW, bits); + set_ctrl_bits(); + + udelay(40); /* maintain the strobe during 40 us */ + + clear_bit(LCD_BIT_E, bits); + set_ctrl_bits(); + + udelay(45); /* the shortest data takes at least 45 us */ + spin_unlock_irq(&pprt_lock); +} + +/* send a command to the TI LCD panel */ +static void lcd_write_cmd_tilcd(struct hd44780_common *hdc, int cmd) +{ + spin_lock_irq(&pprt_lock); + /* present the data to the control port */ + w_ctr(pprt, cmd); + udelay(60); + spin_unlock_irq(&pprt_lock); +} + +/* send data to the TI LCD panel */ +static void lcd_write_data_tilcd(struct hd44780_common *hdc, int data) +{ + spin_lock_irq(&pprt_lock); + /* present the data to the data port */ + w_dtr(pprt, data); + udelay(60); + spin_unlock_irq(&pprt_lock); +} + +static const struct charlcd_ops charlcd_ops = { + .backlight = lcd_backlight, + .print = hd44780_common_print, + .gotoxy = hd44780_common_gotoxy, + .home = hd44780_common_home, + .clear_display = hd44780_common_clear_display, + .init_display = hd44780_common_init_display, + .shift_cursor = hd44780_common_shift_cursor, + .shift_display = hd44780_common_shift_display, + .display = hd44780_common_display, + .cursor = hd44780_common_cursor, + .blink = hd44780_common_blink, + .fontsize = hd44780_common_fontsize, + .lines = hd44780_common_lines, + .redefine_char = hd44780_common_redefine_char, +}; + +/* initialize the LCD driver */ +static void lcd_init(void) +{ + struct charlcd *charlcd; + struct hd44780_common *hdc; + + hdc = hd44780_common_alloc(); + if (!hdc) + return; + + charlcd = charlcd_alloc(); + if (!charlcd) { + kfree(hdc); + return; + } + + hdc->hd44780 = &lcd; + charlcd->drvdata = hdc; + + /* + * Init lcd struct with load-time values to preserve exact + * current functionality (at least for now). + */ + charlcd->height = lcd_height; + charlcd->width = lcd_width; + hdc->bwidth = lcd_bwidth; + hdc->hwidth = lcd_hwidth; + + switch (selected_lcd_type) { + case LCD_TYPE_OLD: + /* parallel mode, 8 bits */ + lcd.proto = LCD_PROTO_PARALLEL; + lcd.charset = LCD_CHARSET_NORMAL; + lcd.pins.e = PIN_STROBE; + lcd.pins.rs = PIN_AUTOLF; + + charlcd->width = 40; + hdc->bwidth = 40; + hdc->hwidth = 64; + charlcd->height = 2; + break; + case LCD_TYPE_KS0074: + /* serial mode, ks0074 */ + lcd.proto = LCD_PROTO_SERIAL; + lcd.charset = LCD_CHARSET_KS0074; + lcd.pins.bl = PIN_AUTOLF; + lcd.pins.cl = PIN_STROBE; + lcd.pins.da = PIN_D0; + + charlcd->width = 16; + hdc->bwidth = 40; + hdc->hwidth = 16; + charlcd->height = 2; + break; + case LCD_TYPE_NEXCOM: + /* parallel mode, 8 bits, generic */ + lcd.proto = LCD_PROTO_PARALLEL; + lcd.charset = LCD_CHARSET_NORMAL; + lcd.pins.e = PIN_AUTOLF; + lcd.pins.rs = PIN_SELECP; + lcd.pins.rw = PIN_INITP; + + charlcd->width = 16; + hdc->bwidth = 40; + hdc->hwidth = 64; + charlcd->height = 2; + break; + case LCD_TYPE_CUSTOM: + /* customer-defined */ + lcd.proto = DEFAULT_LCD_PROTO; + lcd.charset = DEFAULT_LCD_CHARSET; + /* default geometry will be set later */ + break; + case LCD_TYPE_HANTRONIX: + /* parallel mode, 8 bits, hantronix-like */ + default: + lcd.proto = LCD_PROTO_PARALLEL; + lcd.charset = LCD_CHARSET_NORMAL; + lcd.pins.e = PIN_STROBE; + lcd.pins.rs = PIN_SELECP; + + charlcd->width = 16; + hdc->bwidth = 40; + hdc->hwidth = 64; + charlcd->height = 2; + break; + } + + /* Overwrite with module params set on loading */ + if (lcd_height != NOT_SET) + charlcd->height = lcd_height; + if (lcd_width != NOT_SET) + charlcd->width = lcd_width; + if (lcd_bwidth != NOT_SET) + hdc->bwidth = lcd_bwidth; + if (lcd_hwidth != NOT_SET) + hdc->hwidth = lcd_hwidth; + if (lcd_charset != NOT_SET) + lcd.charset = lcd_charset; + if (lcd_proto != NOT_SET) + lcd.proto = lcd_proto; + if (lcd_e_pin != PIN_NOT_SET) + lcd.pins.e = lcd_e_pin; + if (lcd_rs_pin != PIN_NOT_SET) + lcd.pins.rs = lcd_rs_pin; + if (lcd_rw_pin != PIN_NOT_SET) + lcd.pins.rw = lcd_rw_pin; + if (lcd_cl_pin != PIN_NOT_SET) + lcd.pins.cl = lcd_cl_pin; + if (lcd_da_pin != PIN_NOT_SET) + lcd.pins.da = lcd_da_pin; + if (lcd_bl_pin != PIN_NOT_SET) + lcd.pins.bl = lcd_bl_pin; + + /* this is used to catch wrong and default values */ + if (charlcd->width <= 0) + charlcd->width = DEFAULT_LCD_WIDTH; + if (hdc->bwidth <= 0) + hdc->bwidth = DEFAULT_LCD_BWIDTH; + if (hdc->hwidth <= 0) + hdc->hwidth = DEFAULT_LCD_HWIDTH; + if (charlcd->height <= 0) + charlcd->height = DEFAULT_LCD_HEIGHT; + + if (lcd.proto == LCD_PROTO_SERIAL) { /* SERIAL */ + charlcd->ops = &charlcd_ops; + hdc->write_data = lcd_write_data_s; + hdc->write_cmd = lcd_write_cmd_s; + + if (lcd.pins.cl == PIN_NOT_SET) + lcd.pins.cl = DEFAULT_LCD_PIN_SCL; + if (lcd.pins.da == PIN_NOT_SET) + lcd.pins.da = DEFAULT_LCD_PIN_SDA; + + } else if (lcd.proto == LCD_PROTO_PARALLEL) { /* PARALLEL */ + charlcd->ops = &charlcd_ops; + hdc->write_data = lcd_write_data_p8; + hdc->write_cmd = lcd_write_cmd_p8; + + if (lcd.pins.e == PIN_NOT_SET) + lcd.pins.e = DEFAULT_LCD_PIN_E; + if (lcd.pins.rs == PIN_NOT_SET) + lcd.pins.rs = DEFAULT_LCD_PIN_RS; + if (lcd.pins.rw == PIN_NOT_SET) + lcd.pins.rw = DEFAULT_LCD_PIN_RW; + } else { + charlcd->ops = &charlcd_ops; + hdc->write_data = lcd_write_data_tilcd; + hdc->write_cmd = lcd_write_cmd_tilcd; + } + + if (lcd.pins.bl == PIN_NOT_SET) + lcd.pins.bl = DEFAULT_LCD_PIN_BL; + + if (lcd.pins.e == PIN_NOT_SET) + lcd.pins.e = PIN_NONE; + if (lcd.pins.rs == PIN_NOT_SET) + lcd.pins.rs = PIN_NONE; + if (lcd.pins.rw == PIN_NOT_SET) + lcd.pins.rw = PIN_NONE; + if (lcd.pins.bl == PIN_NOT_SET) + lcd.pins.bl = PIN_NONE; + if (lcd.pins.cl == PIN_NOT_SET) + lcd.pins.cl = PIN_NONE; + if (lcd.pins.da == PIN_NOT_SET) + lcd.pins.da = PIN_NONE; + + if (lcd.charset == NOT_SET) + lcd.charset = DEFAULT_LCD_CHARSET; + + if (lcd.charset == LCD_CHARSET_KS0074) + charlcd->char_conv = lcd_char_conv_ks0074; + else + charlcd->char_conv = NULL; + + pin_to_bits(lcd.pins.e, lcd_bits[LCD_PORT_D][LCD_BIT_E], + lcd_bits[LCD_PORT_C][LCD_BIT_E]); + pin_to_bits(lcd.pins.rs, lcd_bits[LCD_PORT_D][LCD_BIT_RS], + lcd_bits[LCD_PORT_C][LCD_BIT_RS]); + pin_to_bits(lcd.pins.rw, lcd_bits[LCD_PORT_D][LCD_BIT_RW], + lcd_bits[LCD_PORT_C][LCD_BIT_RW]); + pin_to_bits(lcd.pins.bl, lcd_bits[LCD_PORT_D][LCD_BIT_BL], + lcd_bits[LCD_PORT_C][LCD_BIT_BL]); + pin_to_bits(lcd.pins.cl, lcd_bits[LCD_PORT_D][LCD_BIT_CL], + lcd_bits[LCD_PORT_C][LCD_BIT_CL]); + pin_to_bits(lcd.pins.da, lcd_bits[LCD_PORT_D][LCD_BIT_DA], + lcd_bits[LCD_PORT_C][LCD_BIT_DA]); + + lcd.charlcd = charlcd; + lcd.initialized = true; +} + +/* + * These are the file operation function for user access to /dev/keypad + */ + +static ssize_t keypad_read(struct file *file, + char __user *buf, size_t count, loff_t *ppos) +{ + unsigned i = *ppos; + char __user *tmp = buf; + + if (keypad_buflen == 0) { + if (file->f_flags & O_NONBLOCK) + return -EAGAIN; + + if (wait_event_interruptible(keypad_read_wait, + keypad_buflen != 0)) + return -EINTR; + } + + for (; count-- > 0 && (keypad_buflen > 0); + ++i, ++tmp, --keypad_buflen) { + put_user(keypad_buffer[keypad_start], tmp); + keypad_start = (keypad_start + 1) % KEYPAD_BUFFER; + } + *ppos = i; + + return tmp - buf; +} + +static int keypad_open(struct inode *inode, struct file *file) +{ + int ret; + + ret = -EBUSY; + if (!atomic_dec_and_test(&keypad_available)) + goto fail; /* open only once at a time */ + + ret = -EPERM; + if (file->f_mode & FMODE_WRITE) /* device is read-only */ + goto fail; + + keypad_buflen = 0; /* flush the buffer on opening */ + return 0; + fail: + atomic_inc(&keypad_available); + return ret; +} + +static int keypad_release(struct inode *inode, struct file *file) +{ + atomic_inc(&keypad_available); + return 0; +} + +static const struct file_operations keypad_fops = { + .read = keypad_read, /* read */ + .open = keypad_open, /* open */ + .release = keypad_release, /* close */ + .llseek = default_llseek, +}; + +static struct miscdevice keypad_dev = { + .minor = KEYPAD_MINOR, + .name = "keypad", + .fops = &keypad_fops, +}; + +static void keypad_send_key(const char *string, int max_len) +{ + /* send the key to the device only if a process is attached to it. */ + if (!atomic_read(&keypad_available)) { + while (max_len-- && keypad_buflen < KEYPAD_BUFFER && *string) { + keypad_buffer[(keypad_start + keypad_buflen++) % + KEYPAD_BUFFER] = *string++; + } + wake_up_interruptible(&keypad_read_wait); + } +} + +/* this function scans all the bits involving at least one logical signal, + * and puts the results in the bitfield "phys_read" (one bit per established + * contact), and sets "phys_read_prev" to "phys_read". + * + * Note: to debounce input signals, we will only consider as switched a signal + * which is stable across 2 measures. Signals which are different between two + * reads will be kept as they previously were in their logical form (phys_prev). + * A signal which has just switched will have a 1 in + * (phys_read ^ phys_read_prev). + */ +static void phys_scan_contacts(void) +{ + int bit, bitval; + char oldval; + char bitmask; + char gndmask; + + phys_prev = phys_curr; + phys_read_prev = phys_read; + phys_read = 0; /* flush all signals */ + + /* keep track of old value, with all outputs disabled */ + oldval = r_dtr(pprt) | scan_mask_o; + /* activate all keyboard outputs (active low) */ + w_dtr(pprt, oldval & ~scan_mask_o); + + /* will have a 1 for each bit set to gnd */ + bitmask = PNL_PINPUT(r_str(pprt)) & scan_mask_i; + /* disable all matrix signals */ + w_dtr(pprt, oldval); + + /* now that all outputs are cleared, the only active input bits are + * directly connected to the ground + */ + + /* 1 for each grounded input */ + gndmask = PNL_PINPUT(r_str(pprt)) & scan_mask_i; + + /* grounded inputs are signals 40-44 */ + phys_read |= (__u64)gndmask << 40; + + if (bitmask != gndmask) { + /* + * since clearing the outputs changed some inputs, we know + * that some input signals are currently tied to some outputs. + * So we'll scan them. + */ + for (bit = 0; bit < 8; bit++) { + bitval = BIT(bit); + + if (!(scan_mask_o & bitval)) /* skip unused bits */ + continue; + + w_dtr(pprt, oldval & ~bitval); /* enable this output */ + bitmask = PNL_PINPUT(r_str(pprt)) & ~gndmask; + phys_read |= (__u64)bitmask << (5 * bit); + } + w_dtr(pprt, oldval); /* disable all outputs */ + } + /* + * this is easy: use old bits when they are flapping, + * use new ones when stable + */ + phys_curr = (phys_prev & (phys_read ^ phys_read_prev)) | + (phys_read & ~(phys_read ^ phys_read_prev)); +} + +static inline int input_state_high(struct logical_input *input) +{ +#if 0 + /* FIXME: + * this is an invalid test. It tries to catch + * transitions from single-key to multiple-key, but + * doesn't take into account the contacts polarity. + * The only solution to the problem is to parse keys + * from the most complex to the simplest combinations, + * and mark them as 'caught' once a combination + * matches, then unmatch it for all other ones. + */ + + /* try to catch dangerous transitions cases : + * someone adds a bit, so this signal was a false + * positive resulting from a transition. We should + * invalidate the signal immediately and not call the + * release function. + * eg: 0 -(press A)-> A -(press B)-> AB : don't match A's release. + */ + if (((phys_prev & input->mask) == input->value) && + ((phys_curr & input->mask) > input->value)) { + input->state = INPUT_ST_LOW; /* invalidate */ + return 1; + } +#endif + + if ((phys_curr & input->mask) == input->value) { + if ((input->type == INPUT_TYPE_STD) && + (input->high_timer == 0)) { + input->high_timer++; + if (input->u.std.press_fct) + input->u.std.press_fct(input->u.std.press_data); + } else if (input->type == INPUT_TYPE_KBD) { + /* will turn on the light */ + keypressed = 1; + + if (input->high_timer == 0) { + char *press_str = input->u.kbd.press_str; + + if (press_str[0]) { + int s = sizeof(input->u.kbd.press_str); + + keypad_send_key(press_str, s); + } + } + + if (input->u.kbd.repeat_str[0]) { + char *repeat_str = input->u.kbd.repeat_str; + + if (input->high_timer >= KEYPAD_REP_START) { + int s = sizeof(input->u.kbd.repeat_str); + + input->high_timer -= KEYPAD_REP_DELAY; + keypad_send_key(repeat_str, s); + } + /* we will need to come back here soon */ + inputs_stable = 0; + } + + if (input->high_timer < 255) + input->high_timer++; + } + return 1; + } + + /* else signal falling down. Let's fall through. */ + input->state = INPUT_ST_FALLING; + input->fall_timer = 0; + + return 0; +} + +static inline void input_state_falling(struct logical_input *input) +{ +#if 0 + /* FIXME !!! same comment as in input_state_high */ + if (((phys_prev & input->mask) == input->value) && + ((phys_curr & input->mask) > input->value)) { + input->state = INPUT_ST_LOW; /* invalidate */ + return; + } +#endif + + if ((phys_curr & input->mask) == input->value) { + if (input->type == INPUT_TYPE_KBD) { + /* will turn on the light */ + keypressed = 1; + + if (input->u.kbd.repeat_str[0]) { + char *repeat_str = input->u.kbd.repeat_str; + + if (input->high_timer >= KEYPAD_REP_START) { + int s = sizeof(input->u.kbd.repeat_str); + + input->high_timer -= KEYPAD_REP_DELAY; + keypad_send_key(repeat_str, s); + } + /* we will need to come back here soon */ + inputs_stable = 0; + } + + if (input->high_timer < 255) + input->high_timer++; + } + input->state = INPUT_ST_HIGH; + } else if (input->fall_timer >= input->fall_time) { + /* call release event */ + if (input->type == INPUT_TYPE_STD) { + void (*release_fct)(int) = input->u.std.release_fct; + + if (release_fct) + release_fct(input->u.std.release_data); + } else if (input->type == INPUT_TYPE_KBD) { + char *release_str = input->u.kbd.release_str; + + if (release_str[0]) { + int s = sizeof(input->u.kbd.release_str); + + keypad_send_key(release_str, s); + } + } + + input->state = INPUT_ST_LOW; + } else { + input->fall_timer++; + inputs_stable = 0; + } +} + +static void panel_process_inputs(void) +{ + struct logical_input *input; + + keypressed = 0; + inputs_stable = 1; + list_for_each_entry(input, &logical_inputs, list) { + switch (input->state) { + case INPUT_ST_LOW: + if ((phys_curr & input->mask) != input->value) + break; + /* if all needed ones were already set previously, + * this means that this logical signal has been + * activated by the releasing of another combined + * signal, so we don't want to match. + * eg: AB -(release B)-> A -(release A)-> 0 : + * don't match A. + */ + if ((phys_prev & input->mask) == input->value) + break; + input->rise_timer = 0; + input->state = INPUT_ST_RISING; + fallthrough; + case INPUT_ST_RISING: + if ((phys_curr & input->mask) != input->value) { + input->state = INPUT_ST_LOW; + break; + } + if (input->rise_timer < input->rise_time) { + inputs_stable = 0; + input->rise_timer++; + break; + } + input->high_timer = 0; + input->state = INPUT_ST_HIGH; + fallthrough; + case INPUT_ST_HIGH: + if (input_state_high(input)) + break; + fallthrough; + case INPUT_ST_FALLING: + input_state_falling(input); + } + } +} + +static void panel_scan_timer(struct timer_list *unused) +{ + if (keypad.enabled && keypad_initialized) { + if (spin_trylock_irq(&pprt_lock)) { + phys_scan_contacts(); + + /* no need for the parport anymore */ + spin_unlock_irq(&pprt_lock); + } + + if (!inputs_stable || phys_curr != phys_prev) + panel_process_inputs(); + } + + if (keypressed && lcd.enabled && lcd.initialized) + charlcd_poke(lcd.charlcd); + + mod_timer(&scan_timer, jiffies + INPUT_POLL_TIME); +} + +static void init_scan_timer(void) +{ + if (scan_timer.function) + return; /* already started */ + + timer_setup(&scan_timer, panel_scan_timer, 0); + scan_timer.expires = jiffies + INPUT_POLL_TIME; + add_timer(&scan_timer); +} + +/* converts a name of the form "({BbAaPpSsEe}{01234567-})*" to a series of bits. + * if <omask> or <imask> are non-null, they will be or'ed with the bits + * corresponding to out and in bits respectively. + * returns 1 if ok, 0 if error (in which case, nothing is written). + */ +static u8 input_name2mask(const char *name, __u64 *mask, __u64 *value, + u8 *imask, u8 *omask) +{ + const char sigtab[] = "EeSsPpAaBb"; + u8 im, om; + __u64 m, v; + + om = 0; + im = 0; + m = 0ULL; + v = 0ULL; + while (*name) { + int in, out, bit, neg; + const char *idx; + + idx = strchr(sigtab, *name); + if (!idx) + return 0; /* input name not found */ + + in = idx - sigtab; + neg = (in & 1); /* odd (lower) names are negated */ + in >>= 1; + im |= BIT(in); + + name++; + if (*name >= '0' && *name <= '7') { + out = *name - '0'; + om |= BIT(out); + } else if (*name == '-') { + out = 8; + } else { + return 0; /* unknown bit name */ + } + + bit = (out * 5) + in; + + m |= 1ULL << bit; + if (!neg) + v |= 1ULL << bit; + name++; + } + *mask = m; + *value = v; + if (imask) + *imask |= im; + if (omask) + *omask |= om; + return 1; +} + +/* tries to bind a key to the signal name <name>. The key will send the + * strings <press>, <repeat>, <release> for these respective events. + * Returns the pointer to the new key if ok, NULL if the key could not be bound. + */ +static struct logical_input *panel_bind_key(const char *name, const char *press, + const char *repeat, + const char *release) +{ + struct logical_input *key; + + key = kzalloc(sizeof(*key), GFP_KERNEL); + if (!key) + return NULL; + + if (!input_name2mask(name, &key->mask, &key->value, &scan_mask_i, + &scan_mask_o)) { + kfree(key); + return NULL; + } + + key->type = INPUT_TYPE_KBD; + key->state = INPUT_ST_LOW; + key->rise_time = 1; + key->fall_time = 1; + + strncpy(key->u.kbd.press_str, press, sizeof(key->u.kbd.press_str)); + strncpy(key->u.kbd.repeat_str, repeat, sizeof(key->u.kbd.repeat_str)); + strncpy(key->u.kbd.release_str, release, + sizeof(key->u.kbd.release_str)); + list_add(&key->list, &logical_inputs); + return key; +} + +#if 0 +/* tries to bind a callback function to the signal name <name>. The function + * <press_fct> will be called with the <press_data> arg when the signal is + * activated, and so on for <release_fct>/<release_data> + * Returns the pointer to the new signal if ok, NULL if the signal could not + * be bound. + */ +static struct logical_input *panel_bind_callback(char *name, + void (*press_fct)(int), + int press_data, + void (*release_fct)(int), + int release_data) +{ + struct logical_input *callback; + + callback = kmalloc(sizeof(*callback), GFP_KERNEL); + if (!callback) + return NULL; + + memset(callback, 0, sizeof(struct logical_input)); + if (!input_name2mask(name, &callback->mask, &callback->value, + &scan_mask_i, &scan_mask_o)) + return NULL; + + callback->type = INPUT_TYPE_STD; + callback->state = INPUT_ST_LOW; + callback->rise_time = 1; + callback->fall_time = 1; + callback->u.std.press_fct = press_fct; + callback->u.std.press_data = press_data; + callback->u.std.release_fct = release_fct; + callback->u.std.release_data = release_data; + list_add(&callback->list, &logical_inputs); + return callback; +} +#endif + +static void keypad_init(void) +{ + int keynum; + + init_waitqueue_head(&keypad_read_wait); + keypad_buflen = 0; /* flushes any eventual noisy keystroke */ + + /* Let's create all known keys */ + + for (keynum = 0; keypad_profile[keynum][0][0]; keynum++) { + panel_bind_key(keypad_profile[keynum][0], + keypad_profile[keynum][1], + keypad_profile[keynum][2], + keypad_profile[keynum][3]); + } + + init_scan_timer(); + keypad_initialized = 1; +} + +/**************************************************/ +/* device initialization */ +/**************************************************/ + +static void panel_attach(struct parport *port) +{ + struct pardev_cb panel_cb; + + if (port->number != parport) + return; + + if (pprt) { + pr_err("%s: port->number=%d parport=%d, already registered!\n", + __func__, port->number, parport); + return; + } + + memset(&panel_cb, 0, sizeof(panel_cb)); + panel_cb.private = &pprt; + /* panel_cb.flags = 0 should be PARPORT_DEV_EXCL? */ + + pprt = parport_register_dev_model(port, "panel", &panel_cb, 0); + if (!pprt) { + pr_err("%s: port->number=%d parport=%d, parport_register_device() failed\n", + __func__, port->number, parport); + return; + } + + if (parport_claim(pprt)) { + pr_err("could not claim access to parport%d. Aborting.\n", + parport); + goto err_unreg_device; + } + + /* must init LCD first, just in case an IRQ from the keypad is + * generated at keypad init + */ + if (lcd.enabled) { + lcd_init(); + if (!lcd.charlcd || charlcd_register(lcd.charlcd)) + goto err_unreg_device; + } + + if (keypad.enabled) { + keypad_init(); + if (misc_register(&keypad_dev)) + goto err_lcd_unreg; + } + return; + +err_lcd_unreg: + if (scan_timer.function) + del_timer_sync(&scan_timer); + if (lcd.enabled) + charlcd_unregister(lcd.charlcd); +err_unreg_device: + kfree(lcd.charlcd); + lcd.charlcd = NULL; + parport_unregister_device(pprt); + pprt = NULL; +} + +static void panel_detach(struct parport *port) +{ + if (port->number != parport) + return; + + if (!pprt) { + pr_err("%s: port->number=%d parport=%d, nothing to unregister.\n", + __func__, port->number, parport); + return; + } + if (scan_timer.function) + del_timer_sync(&scan_timer); + + if (keypad.enabled) { + misc_deregister(&keypad_dev); + keypad_initialized = 0; + } + + if (lcd.enabled) { + charlcd_unregister(lcd.charlcd); + lcd.initialized = false; + kfree(lcd.charlcd->drvdata); + kfree(lcd.charlcd); + lcd.charlcd = NULL; + } + + /* TODO: free all input signals */ + parport_release(pprt); + parport_unregister_device(pprt); + pprt = NULL; +} + +static struct parport_driver panel_driver = { + .name = "panel", + .match_port = panel_attach, + .detach = panel_detach, + .devmodel = true, +}; + +/* init function */ +static int __init panel_init_module(void) +{ + int selected_keypad_type = NOT_SET, err; + + /* take care of an eventual profile */ + switch (profile) { + case PANEL_PROFILE_CUSTOM: + /* custom profile */ + selected_keypad_type = DEFAULT_KEYPAD_TYPE; + selected_lcd_type = DEFAULT_LCD_TYPE; + break; + case PANEL_PROFILE_OLD: + /* 8 bits, 2*16, old keypad */ + selected_keypad_type = KEYPAD_TYPE_OLD; + selected_lcd_type = LCD_TYPE_OLD; + + /* TODO: This two are a little hacky, sort it out later */ + if (lcd_width == NOT_SET) + lcd_width = 16; + if (lcd_hwidth == NOT_SET) + lcd_hwidth = 16; + break; + case PANEL_PROFILE_NEW: + /* serial, 2*16, new keypad */ + selected_keypad_type = KEYPAD_TYPE_NEW; + selected_lcd_type = LCD_TYPE_KS0074; + break; + case PANEL_PROFILE_HANTRONIX: + /* 8 bits, 2*16 hantronix-like, no keypad */ + selected_keypad_type = KEYPAD_TYPE_NONE; + selected_lcd_type = LCD_TYPE_HANTRONIX; + break; + case PANEL_PROFILE_NEXCOM: + /* generic 8 bits, 2*16, nexcom keypad, eg. Nexcom. */ + selected_keypad_type = KEYPAD_TYPE_NEXCOM; + selected_lcd_type = LCD_TYPE_NEXCOM; + break; + case PANEL_PROFILE_LARGE: + /* 8 bits, 2*40, old keypad */ + selected_keypad_type = KEYPAD_TYPE_OLD; + selected_lcd_type = LCD_TYPE_OLD; + break; + } + + /* + * Overwrite selection with module param values (both keypad and lcd), + * where the deprecated params have lower prio. + */ + if (keypad_enabled != NOT_SET) + selected_keypad_type = keypad_enabled; + if (keypad_type != NOT_SET) + selected_keypad_type = keypad_type; + + keypad.enabled = (selected_keypad_type > 0); + + if (lcd_enabled != NOT_SET) + selected_lcd_type = lcd_enabled; + if (lcd_type != NOT_SET) + selected_lcd_type = lcd_type; + + lcd.enabled = (selected_lcd_type > 0); + + if (lcd.enabled) { + /* + * Init lcd struct with load-time values to preserve exact + * current functionality (at least for now). + */ + lcd.charset = lcd_charset; + lcd.proto = lcd_proto; + lcd.pins.e = lcd_e_pin; + lcd.pins.rs = lcd_rs_pin; + lcd.pins.rw = lcd_rw_pin; + lcd.pins.cl = lcd_cl_pin; + lcd.pins.da = lcd_da_pin; + lcd.pins.bl = lcd_bl_pin; + } + + switch (selected_keypad_type) { + case KEYPAD_TYPE_OLD: + keypad_profile = old_keypad_profile; + break; + case KEYPAD_TYPE_NEW: + keypad_profile = new_keypad_profile; + break; + case KEYPAD_TYPE_NEXCOM: + keypad_profile = nexcom_keypad_profile; + break; + default: + keypad_profile = NULL; + break; + } + + if (!lcd.enabled && !keypad.enabled) { + /* no device enabled, let's exit */ + pr_err("panel driver disabled.\n"); + return -ENODEV; + } + + err = parport_register_driver(&panel_driver); + if (err) { + pr_err("could not register with parport. Aborting.\n"); + return err; + } + + if (pprt) + pr_info("panel driver registered on parport%d (io=0x%lx).\n", + parport, pprt->port->base); + else + pr_info("panel driver not yet registered\n"); + return 0; +} + +static void __exit panel_cleanup_module(void) +{ + parport_unregister_driver(&panel_driver); +} + +module_init(panel_init_module); +module_exit(panel_cleanup_module); +MODULE_AUTHOR("Willy Tarreau"); +MODULE_LICENSE("GPL"); |