diff options
Diffstat (limited to 'drivers/leds')
118 files changed, 37479 insertions, 0 deletions
diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig new file mode 100644 index 000000000..03c4c5e4d --- /dev/null +++ b/drivers/leds/Kconfig @@ -0,0 +1,936 @@ +# SPDX-License-Identifier: GPL-2.0-only +config LEDS_GPIO_REGISTER + bool + help + This option provides the function gpio_led_register_device. + As this function is used by arch code it must not be compiled as a + module. + +menuconfig NEW_LEDS + bool "LED Support" + help + Say Y to enable Linux LED support. This allows control of supported + LEDs from both userspace and optionally, by kernel events (triggers). + +if NEW_LEDS + +config LEDS_CLASS + tristate "LED Class Support" + help + This option enables the LED sysfs class in /sys/class/leds. You'll + need this to do anything useful with LEDs. If unsure, say N. + +config LEDS_CLASS_FLASH + tristate "LED Flash Class Support" + depends on LEDS_CLASS + help + This option enables the flash LED sysfs class in /sys/class/leds. + It wraps LED Class and adds flash LEDs specific sysfs attributes + and kernel internal API to it. You'll need this to provide support + for the flash related features of a LED device. It can be built + as a module. + +config LEDS_CLASS_MULTICOLOR + tristate "LED Multicolor Class Support" + depends on LEDS_CLASS + help + This option enables the multicolor LED sysfs class in /sys/class/leds. + It wraps LED class and adds multicolor LED specific sysfs attributes + and kernel internal API to it. You'll need this to provide support + for multicolor LEDs that are grouped together. This class is not + intended for single color LEDs. It can be built as a module. + +config LEDS_BRIGHTNESS_HW_CHANGED + bool "LED Class brightness_hw_changed attribute support" + depends on LEDS_CLASS + help + This option enables support for the brightness_hw_changed attribute + for LED sysfs class devices under /sys/class/leds. + + See Documentation/ABI/testing/sysfs-class-led for details. + +comment "LED drivers" + +config LEDS_88PM860X + tristate "LED Support for Marvell 88PM860x PMIC" + depends on LEDS_CLASS + depends on MFD_88PM860X + help + This option enables support for on-chip LED drivers found on Marvell + Semiconductor 88PM8606 PMIC. + +config LEDS_AAT1290 + tristate "LED support for the AAT1290" + depends on LEDS_CLASS_FLASH + depends on V4L2_FLASH_LED_CLASS || !V4L2_FLASH_LED_CLASS + depends on GPIOLIB || COMPILE_TEST + depends on OF + depends on PINCTRL + help + This option enables support for the LEDs on the AAT1290. + +config LEDS_AN30259A + tristate "LED support for Panasonic AN30259A" + depends on LEDS_CLASS && I2C && OF + help + This option enables support for the AN30259A 3-channel + LED driver. + + To compile this driver as a module, choose M here: the module + will be called leds-an30259a. + +config LEDS_APU + tristate "Front panel LED support for PC Engines APU/APU2/APU3 boards" + depends on LEDS_CLASS + depends on X86 && DMI + help + This driver makes the PC Engines APU1 front panel LEDs + accessible from userspace programs through the LED subsystem. + + If you're looking for APU2/3, use the pcengines-apu2 driver. + (symbol CONFIG_PCENGINES_APU2) + + To compile this driver as a module, choose M here: the + module will be called leds-apu. + +config LEDS_ARIEL + tristate "Dell Wyse 3020 status LED support" + depends on LEDS_CLASS + depends on (MACH_MMP3_DT && MFD_ENE_KB3930) || COMPILE_TEST + help + This driver adds support for controlling the front panel status + LEDs on Dell Wyse 3020 (Ariel) board via the KB3930 Embedded + Controller. + + Say Y to if your machine is a Dell Wyse 3020 thin client. + +config LEDS_AS3645A + tristate "AS3645A and LM3555 LED flash controllers support" + depends on I2C && LEDS_CLASS_FLASH + depends on V4L2_FLASH_LED_CLASS || !V4L2_FLASH_LED_CLASS + help + Enable LED flash class support for AS3645A LED flash + controller. V4L2 flash API is provided as well if + CONFIG_V4L2_FLASH_API is enabled. + +config LEDS_AW2013 + tristate "LED support for Awinic AW2013" + depends on LEDS_CLASS && I2C && OF + select REGMAP_I2C + help + This option enables support for the AW2013 3-channel + LED driver. + + To compile this driver as a module, choose M here: the module + will be called leds-aw2013. + +config LEDS_BCM6328 + tristate "LED Support for Broadcom BCM6328" + depends on LEDS_CLASS + depends on HAS_IOMEM + depends on OF + help + This option enables support for LEDs connected to the BCM6328 + LED HW controller accessed via MMIO registers. + +config LEDS_BCM6358 + tristate "LED Support for Broadcom BCM6358" + depends on LEDS_CLASS + depends on HAS_IOMEM + depends on OF + help + This option enables support for LEDs connected to the BCM6358 + LED HW controller accessed via MMIO registers. + +config LEDS_CPCAP + tristate "LED Support for Motorola CPCAP" + depends on LEDS_CLASS + depends on MFD_CPCAP + depends on OF + help + This option enables support for LEDs offered by Motorola's + CPCAP PMIC. + +config LEDS_CR0014114 + tristate "LED Support for Crane CR0014114" + depends on LEDS_CLASS + depends on SPI + depends on OF + help + This option enables support for CR0014114 LED Board which + is widely used in vending machines produced by + Crane Merchandising Systems. + + To compile this driver as a module, choose M here: the module + will be called leds-cr0014114. + +config LEDS_EL15203000 + tristate "LED Support for Crane EL15203000" + depends on LEDS_CLASS + depends on SPI + depends on OF + help + This option enables support for EL15203000 LED Board + (aka RED LED board) which is widely used in coffee vending + machines produced by Crane Merchandising Systems. + + To compile this driver as a module, choose M here: the module + will be called leds-el15203000. + +config LEDS_TURRIS_OMNIA + tristate "LED support for CZ.NIC's Turris Omnia" + depends on LEDS_CLASS_MULTICOLOR + depends on I2C + depends on MACH_ARMADA_38X || COMPILE_TEST + depends on OF + help + This option enables basic support for the LEDs found on the front + side of CZ.NIC's Turris Omnia router. There are 12 RGB LEDs on the + front panel. + +config LEDS_LM3530 + tristate "LCD Backlight driver for LM3530" + depends on LEDS_CLASS + depends on I2C + help + This option enables support for the LCD backlight using + LM3530 ambient light sensor chip. This ALS chip can be + controlled manually or using PWM input or using ambient + light automatically. + +config LEDS_LM3532 + tristate "LCD Backlight driver for LM3532" + select REGMAP_I2C + depends on LEDS_CLASS + depends on I2C + help + This option enables support for the LCD backlight using + LM3532 ambient light sensor chip. This ALS chip can be + controlled manually or using PWM input or using ambient + light automatically. + +config LEDS_LM3533 + tristate "LED support for LM3533" + depends on LEDS_CLASS + depends on MFD_LM3533 + help + This option enables support for the LEDs on National Semiconductor / + TI LM3533 Lighting Power chips. + + The LEDs can be controlled directly, through PWM input, or by the + ambient-light-sensor interface. The chip supports + hardware-accelerated blinking with maximum on and off periods of 9.8 + and 77 seconds respectively. + +config LEDS_LM3642 + tristate "LED support for LM3642 Chip" + depends on LEDS_CLASS && I2C + select REGMAP_I2C + help + This option enables support for LEDs connected to LM3642. + The LM3642 is a 4MHz fixed-frequency synchronous boost + converter plus 1.5A constant current driver for a high-current + white LED. + +config LEDS_LM3692X + tristate "LED support for LM3692x Chips" + depends on LEDS_CLASS && I2C && OF + select REGMAP_I2C + help + This option enables support for the TI LM3692x family + of white LED string drivers used for backlighting. + +config LEDS_LM3601X + tristate "LED support for LM3601x Chips" + depends on LEDS_CLASS && I2C + depends on LEDS_CLASS_FLASH + select REGMAP_I2C + help + This option enables support for the TI LM3601x family + of flash, torch and indicator classes. + +config LEDS_LOCOMO + tristate "LED Support for Locomo device" + depends on LEDS_CLASS + depends on SHARP_LOCOMO + help + This option enables support for the LEDs on Sharp Locomo. + Zaurus models SL-5500 and SL-5600. + +config LEDS_MIKROTIK_RB532 + tristate "LED Support for Mikrotik Routerboard 532" + depends on LEDS_CLASS + depends on MIKROTIK_RB532 + help + This option enables support for the so called "User LED" of + Mikrotik's Routerboard 532. + +config LEDS_MT6323 + tristate "LED Support for Mediatek MT6323 PMIC" + depends on LEDS_CLASS + depends on MFD_MT6397 + help + This option enables support for on-chip LED drivers found on + Mediatek MT6323 PMIC. + +config LEDS_S3C24XX + tristate "LED Support for Samsung S3C24XX GPIO LEDs" + depends on LEDS_CLASS + depends on ARCH_S3C24XX || COMPILE_TEST + help + This option enables support for LEDs connected to GPIO lines + on Samsung S3C24XX series CPUs, such as the S3C2410 and S3C2440. + +config LEDS_NET48XX + tristate "LED Support for Soekris net48xx series Error LED" + depends on LEDS_CLASS + depends on SCx200_GPIO + help + This option enables support for the Soekris net4801 and net4826 error + LED. + +config LEDS_FSG + tristate "LED Support for the Freecom FSG-3" + depends on LEDS_CLASS + depends on MACH_FSG + help + This option enables support for the LEDs on the Freecom FSG-3. + +config LEDS_WRAP + tristate "LED Support for the WRAP series LEDs" + depends on LEDS_CLASS + depends on SCx200_GPIO + help + This option enables support for the PCEngines WRAP programmable LEDs. + +config LEDS_COBALT_QUBE + tristate "LED Support for the Cobalt Qube series front LED" + depends on LEDS_CLASS + depends on MIPS_COBALT || COMPILE_TEST + help + This option enables support for the front LED on Cobalt Qube series + +config LEDS_COBALT_RAQ + bool "LED Support for the Cobalt Raq series" + depends on LEDS_CLASS=y && (MIPS_COBALT || COMPILE_TEST) + select LEDS_TRIGGERS + help + This option enables support for the Cobalt Raq series LEDs. + +config LEDS_SUNFIRE + tristate "LED support for SunFire servers." + depends on LEDS_CLASS + depends on SPARC64 + select LEDS_TRIGGERS + help + This option enables support for the Left, Middle, and Right + LEDs on the I/O and CPU boards of SunFire UltraSPARC servers. + +config LEDS_IPAQ_MICRO + tristate "LED Support for the Compaq iPAQ h3xxx" + depends on LEDS_CLASS + depends on MFD_IPAQ_MICRO + help + Choose this option if you want to use the notification LED on + Compaq/HP iPAQ h3100 and h3600. + +config LEDS_HP6XX + tristate "LED Support for the HP Jornada 6xx" + depends on LEDS_CLASS + depends on SH_HP6XX + help + This option enables LED support for the handheld + HP Jornada 620/660/680/690. + +config LEDS_PCA9532 + tristate "LED driver for PCA9532 dimmer" + depends on LEDS_CLASS + depends on I2C && INPUT + help + This option enables support for NXP pca9532 + LED controller. It is generally only useful + as a platform driver + +config LEDS_PCA9532_GPIO + bool "Enable GPIO support for PCA9532" + depends on LEDS_PCA9532 + depends on GPIOLIB + help + Allow unused pins on PCA9532 to be used as gpio. + + To use a pin as gpio pca9532_type in pca9532_platform data needs to + set to PCA9532_TYPE_GPIO. + +config LEDS_GPIO + tristate "LED Support for GPIO connected LEDs" + depends on LEDS_CLASS + depends on GPIOLIB || COMPILE_TEST + help + This option enables support for the LEDs connected to GPIO + outputs. To be useful the particular board must have LEDs + and they must be connected to the GPIO lines. The LEDs must be + defined as platform devices and/or OpenFirmware platform devices. + The code to use these bindings can be selected below. + +config LEDS_LP3944 + tristate "LED Support for N.S. LP3944 (Fun Light) I2C chip" + depends on LEDS_CLASS + depends on I2C + help + This option enables support for LEDs connected to the National + Semiconductor LP3944 Lighting Management Unit (LMU) also known as + Fun Light Chip. + + To compile this driver as a module, choose M here: the + module will be called leds-lp3944. + +config LEDS_LP3952 + tristate "LED Support for TI LP3952 2 channel LED driver" + depends on LEDS_CLASS + depends on I2C + depends on GPIOLIB + select REGMAP_I2C + help + This option enables support for LEDs connected to the Texas + Instruments LP3952 LED driver. + + To compile this driver as a module, choose M here: the + module will be called leds-lp3952. + +config LEDS_LP50XX + tristate "LED Support for TI LP5036/30/24/18/12/9 LED driver chip" + depends on LEDS_CLASS && REGMAP_I2C + depends on LEDS_CLASS_MULTICOLOR || !LEDS_CLASS_MULTICOLOR + help + If you say yes here you get support for the Texas Instruments + LP5036, LP5030, LP5024, LP5018, LP5012 and LP5009 LED driver. + + To compile this driver as a module, choose M here: the + module will be called leds-lp50xx. + +config LEDS_LP55XX_COMMON + tristate "Common Driver for TI/National LP5521/5523/55231/5562/8501" + depends on LEDS_CLASS + depends on LEDS_CLASS_MULTICOLOR || !LEDS_CLASS_MULTICOLOR + depends on OF + depends on I2C + select FW_LOADER + select FW_LOADER_USER_HELPER + help + This option supports common operations for LP5521/5523/55231/5562/8501 + devices. + +config LEDS_LP5521 + tristate "LED Support for N.S. LP5521 LED driver chip" + depends on LEDS_CLASS && I2C + depends on LEDS_LP55XX_COMMON + help + If you say yes here you get support for the National Semiconductor + LP5521 LED driver. It is 3 channel chip with programmable engines. + Driver provides direct control via LED class and interface for + programming the engines. + +config LEDS_LP5523 + tristate "LED Support for TI/National LP5523/55231 LED driver chip" + depends on LEDS_CLASS && I2C + depends on LEDS_LP55XX_COMMON + help + If you say yes here you get support for TI/National Semiconductor + LP5523/55231 LED driver. + It is 9 channel chip with programmable engines. + Driver provides direct control via LED class and interface for + programming the engines. + +config LEDS_LP5562 + tristate "LED Support for TI LP5562 LED driver chip" + depends on LEDS_CLASS && I2C + depends on LEDS_LP55XX_COMMON + help + If you say yes here you get support for TI LP5562 LED driver. + It is 4 channels chip with programmable engines. + Driver provides direct control via LED class and interface for + programming the engines. + +config LEDS_LP8501 + tristate "LED Support for TI LP8501 LED driver chip" + depends on LEDS_CLASS && I2C + depends on LEDS_LP55XX_COMMON + help + If you say yes here you get support for TI LP8501 LED driver. + It is 9 channel chip with programmable engines. + Driver provides direct control via LED class and interface for + programming the engines. + It is similar as LP5523, but output power selection is available. + And register layout and engine program schemes are different. + +config LEDS_LP8788 + tristate "LED support for the TI LP8788 PMIC" + depends on LEDS_CLASS + depends on MFD_LP8788 + help + This option enables support for the Keyboard LEDs on the LP8788 PMIC. + +config LEDS_LP8860 + tristate "LED support for the TI LP8860 4 channel LED driver" + depends on LEDS_CLASS && I2C && OF + select REGMAP_I2C + help + If you say yes here you get support for the TI LP8860 4 channel + LED driver. + This option enables support for the display cluster LEDs + on the LP8860 4 channel LED driver using the I2C communication + bus. + +config LEDS_CLEVO_MAIL + tristate "Mail LED on Clevo notebook" + depends on LEDS_CLASS + depends on X86 && SERIO_I8042 && DMI + help + This driver makes the mail LED accessible from userspace + programs through the leds subsystem. This LED have three + known mode: off, blink at 0.5Hz and blink at 1Hz. + + The driver supports two kinds of interface: using ledtrig-timer + or through /sys/class/leds/clevo::mail/brightness. As this LED + cannot change it's brightness it blinks instead. The brightness + value 0 means off, 1..127 means blink at 0.5Hz and 128..255 means + blink at 1Hz. + + This module can drive the mail LED for the following notebooks: + + Clevo D400P + Clevo D410J + Clevo D410V + Clevo D400V/D470V (not tested, but might work) + Clevo M540N + Clevo M5x0N (not tested, but might work) + Positivo Mobile (Clevo M5x0V) + + If your model is not listed here you can try the "nodetect" + module parameter. + + To compile this driver as a module, choose M here: the + module will be called leds-clevo-mail. + +config LEDS_PCA955X + tristate "LED Support for PCA955x I2C chips" + depends on LEDS_CLASS + depends on I2C + help + This option enables support for LEDs connected to PCA955x + LED driver chips accessed via the I2C bus. Supported + devices include PCA9550, PCA9551, PCA9552, and PCA9553. + +config LEDS_PCA955X_GPIO + bool "Enable GPIO support for PCA955X" + depends on LEDS_PCA955X + depends on GPIOLIB + help + Allow unused pins on PCA955X to be used as gpio. + + To use a pin as gpio the pin type should be set to + PCA955X_TYPE_GPIO in the device tree. + + +config LEDS_PCA963X + tristate "LED support for PCA963x I2C chip" + depends on LEDS_CLASS + depends on I2C + help + This option enables support for LEDs connected to the PCA963x + LED driver chip accessed via the I2C bus. Supported + devices include PCA9633 and PCA9634 + +config LEDS_WM831X_STATUS + tristate "LED support for status LEDs on WM831x PMICs" + depends on LEDS_CLASS + depends on MFD_WM831X + help + This option enables support for the status LEDs of the WM831x + series of PMICs. + +config LEDS_WM8350 + tristate "LED Support for WM8350 AudioPlus PMIC" + depends on LEDS_CLASS + depends on MFD_WM8350 + help + This option enables support for LEDs driven by the Wolfson + Microelectronics WM8350 AudioPlus PMIC. + +config LEDS_DA903X + tristate "LED Support for DA9030/DA9034 PMIC" + depends on LEDS_CLASS + depends on PMIC_DA903X + help + This option enables support for on-chip LED drivers found + on Dialog Semiconductor DA9030/DA9034 PMICs. + +config LEDS_DA9052 + tristate "Dialog DA9052/DA9053 LEDS" + depends on LEDS_CLASS + depends on PMIC_DA9052 + help + This option enables support for on-chip LED drivers found + on Dialog Semiconductor DA9052-BC and DA9053-AA/Bx PMICs. + +config LEDS_DAC124S085 + tristate "LED Support for DAC124S085 SPI DAC" + depends on LEDS_CLASS + depends on SPI + help + This option enables support for DAC124S085 SPI DAC from NatSemi, + which can be used to control up to four LEDs. + +config LEDS_PWM + tristate "PWM driven LED Support" + depends on LEDS_CLASS + depends on PWM + help + This option enables support for pwm driven LEDs + +config LEDS_REGULATOR + tristate "REGULATOR driven LED support" + depends on LEDS_CLASS + depends on REGULATOR + help + This option enables support for regulator driven LEDs. + +config LEDS_BD2802 + tristate "LED driver for BD2802 RGB LED" + depends on LEDS_CLASS + depends on I2C + help + This option enables support for BD2802GU RGB LED driver chips + accessed via the I2C bus. + +config LEDS_INTEL_SS4200 + tristate "LED driver for Intel NAS SS4200 series" + depends on LEDS_CLASS + depends on PCI && DMI + depends on X86 + help + This option enables support for the Intel SS4200 series of + Network Attached Storage servers. You may control the hard + drive or power LEDs on the front panel. Using this driver + can stop the front LED from blinking after startup. + +config LEDS_LT3593 + tristate "LED driver for LT3593 controllers" + depends on LEDS_CLASS + depends on GPIOLIB || COMPILE_TEST + depends on OF + help + This option enables support for LEDs driven by a Linear Technology + LT3593 controller. This controller uses a special one-wire pulse + coding protocol to set the brightness. + +config LEDS_ADP5520 + tristate "LED Support for ADP5520/ADP5501 PMIC" + depends on LEDS_CLASS + depends on PMIC_ADP5520 + help + This option enables support for on-chip LED drivers found + on Analog Devices ADP5520/ADP5501 PMICs. + + To compile this driver as a module, choose M here: the module will + be called leds-adp5520. + +config LEDS_MC13783 + tristate "LED Support for MC13XXX PMIC" + depends on LEDS_CLASS + depends on MFD_MC13XXX + help + This option enables support for on-chip LED drivers found + on Freescale Semiconductor MC13783/MC13892/MC34708 PMIC. + +config LEDS_NS2 + tristate "LED support for Network Space v2 GPIO LEDs" + depends on LEDS_CLASS + depends on MACH_KIRKWOOD || MACH_ARMADA_370 || COMPILE_TEST + default y + help + This option enables support for the dual-GPIO LEDs found on the + following LaCie/Seagate boards: + + Network Space v2 (and parents: Max, Mini) + Internet Space v2 + d2 Network v2 + n090401 (Seagate NAS 4-Bay) + +config LEDS_NETXBIG + tristate "LED support for Big Network series LEDs" + depends on LEDS_CLASS + depends on MACH_KIRKWOOD || COMPILE_TEST + depends on OF_GPIO + default y + help + This option enables support for LEDs found on the LaCie 2Big + and 5Big Network v2 boards. The LEDs are wired to a CPLD and are + controlled through a GPIO extension bus. + +config LEDS_ASIC3 + bool "LED support for the HTC ASIC3" + depends on LEDS_CLASS=y + depends on MFD_ASIC3 + default y + help + This option enables support for the LEDs on the HTC ASIC3. The HTC + ASIC3 LED GPIOs are inputs, not outputs, thus the leds-gpio driver + cannot be used. This driver supports hardware blinking with an on+off + period from 62ms to 125s. Say Y to enable LEDs on the HP iPAQ hx4700. + +config LEDS_TCA6507 + tristate "LED Support for TCA6507 I2C chip" + depends on LEDS_CLASS && I2C + help + This option enables support for LEDs connected to TC6507 + LED driver chips accessed via the I2C bus. + Driver support brightness control and hardware-assisted blinking. + +config LEDS_TLC591XX + tristate "LED driver for TLC59108 and TLC59116 controllers" + depends on LEDS_CLASS && I2C + select REGMAP_I2C + help + This option enables support for Texas Instruments TLC59108 + and TLC59116 LED controllers. + +config LEDS_MAX77650 + tristate "LED support for Maxim MAX77650 PMIC" + depends on LEDS_CLASS && MFD_MAX77650 + help + LEDs driver for MAX77650 family of PMICs from Maxim Integrated. + +config LEDS_MAX77693 + tristate "LED support for MAX77693 Flash" + depends on LEDS_CLASS_FLASH + depends on V4L2_FLASH_LED_CLASS || !V4L2_FLASH_LED_CLASS + depends on MFD_MAX77693 + depends on OF + help + This option enables support for the flash part of the MAX77693 + multifunction device. It has build in control for two leds in flash + and torch mode. + +config LEDS_MAX8997 + tristate "LED support for MAX8997 PMIC" + depends on LEDS_CLASS && MFD_MAX8997 + help + This option enables support for on-chip LED drivers on + MAXIM MAX8997 PMIC. + +config LEDS_LM355x + tristate "LED support for LM3554 and LM3556 chips" + depends on LEDS_CLASS && I2C + select REGMAP_I2C + help + This option enables support for LEDs connected to LM3554 + and LM3556. It includes Torch, Flash and Indicator functions. + +config LEDS_OT200 + tristate "LED support for the Bachmann OT200" + depends on LEDS_CLASS && HAS_IOMEM && (X86_32 || COMPILE_TEST) + help + This option enables support for the LEDs on the Bachmann OT200. + Say Y to enable LEDs on the Bachmann OT200. + +config LEDS_MENF21BMC + tristate "LED support for the MEN 14F021P00 BMC" + depends on LEDS_CLASS && MFD_MENF21BMC + help + Say Y here to include support for the MEN 14F021P00 BMC LEDs. + + This driver can also be built as a module. If so the module + will be called leds-menf21bmc. + +config LEDS_KTD2692 + tristate "LED support for KTD2692 flash LED controller" + depends on LEDS_CLASS_FLASH && OF + depends on GPIOLIB || COMPILE_TEST + help + This option enables support for KTD2692 LED flash connected + through ExpressWire interface. + + Say Y to enable this driver. + +config LEDS_IS31FL319X + tristate "LED Support for ISSI IS31FL319x I2C LED controller family" + depends on LEDS_CLASS && I2C && OF + select REGMAP_I2C + help + This option enables support for LEDs connected to ISSI IS31FL319x + fancy LED driver chips accessed via the I2C bus. + Driver supports individual PWM brightness control for each channel. + + This driver can also be built as a module. If so the module will be + called leds-is31fl319x. + +config LEDS_IS31FL32XX + tristate "LED support for ISSI IS31FL32XX I2C LED controller family" + depends on LEDS_CLASS && I2C && OF + help + Say Y here to include support for ISSI IS31FL32XX and Si-En SN32xx + LED controllers. They are I2C devices with multiple constant-current + channels, each with independent 256-level PWM control. + +config LEDS_SC27XX_BLTC + tristate "LED support for the SC27xx breathing light controller" + depends on LEDS_CLASS && MFD_SC27XX_PMIC + depends on OF + help + Say Y here to include support for the SC27xx breathing light controller + LEDs. + + This driver can also be built as a module. If so the module will be + called leds-sc27xx-bltc. + +comment "LED driver for blink(1) USB RGB LED is under Special HID drivers (HID_THINGM)" + +config LEDS_BLINKM + tristate "LED support for the BlinkM I2C RGB LED" + depends on LEDS_CLASS + depends on I2C + help + This option enables support for the BlinkM RGB LED connected + through I2C. Say Y to enable support for the BlinkM LED. + +config LEDS_POWERNV + tristate "LED support for PowerNV Platform" + depends on LEDS_CLASS + depends on PPC_POWERNV + depends on OF + help + This option enables support for the system LEDs present on + PowerNV platforms. Say 'y' to enable this support in kernel. + To compile this driver as a module, choose 'm' here: the module + will be called leds-powernv. + +config LEDS_SYSCON + bool "LED support for LEDs on system controllers" + depends on LEDS_CLASS=y + depends on MFD_SYSCON + depends on OF + help + This option enables support for the LEDs on syscon type + devices. This will only work with device tree enabled + devices. + +config LEDS_PM8058 + tristate "LED Support for the Qualcomm PM8058 PMIC" + depends on MFD_PM8XXX + depends on LEDS_CLASS + help + Choose this option if you want to use the LED drivers in + the Qualcomm PM8058 PMIC. + +config LEDS_MLXCPLD + tristate "LED support for the Mellanox boards" + depends on X86 && DMI + depends on LEDS_CLASS + help + This option enables support for the LEDs on the Mellanox + boards. Say Y to enable these. + +config LEDS_MLXREG + tristate "LED support for the Mellanox switches management control" + depends on LEDS_CLASS + help + This option enables support for the LEDs on the Mellanox Ethernet and + InfiniBand switches. The driver can be activated by the platform device + device add call. Say Y to enable these. To compile this driver as a + module, choose 'M' here: the module will be called leds-mlxreg. + +config LEDS_USER + tristate "Userspace LED support" + depends on LEDS_CLASS + help + This option enables support for userspace LEDs. Say 'y' to enable this + support in kernel. To compile this driver as a module, choose 'm' here: + the module will be called uleds. + +config LEDS_NIC78BX + tristate "LED support for NI PXI NIC78bx devices" + depends on LEDS_CLASS + depends on X86 && ACPI + help + This option enables support for the User1 and User2 LEDs on NI + PXI NIC78bx devices. + + To compile this driver as a module, choose M here: the module + will be called leds-nic78bx. + +config LEDS_SPI_BYTE + tristate "LED support for SPI LED controller with a single byte" + depends on LEDS_CLASS + depends on SPI + depends on OF + help + This option enables support for LED controller which use a single byte + for controlling the brightness. Currently the following controller is + supported: Ubiquiti airCube ISP microcontroller based LED controller. + +config LEDS_TI_LMU_COMMON + tristate "LED driver for TI LMU" + depends on LEDS_CLASS + select REGMAP + help + Say Y to enable the LED driver for TI LMU devices. + This supports common features between the TI LM3532, LM3631, LM3632, + LM3633, LM3695 and LM3697. + +config LEDS_LM3697 + tristate "LED driver for LM3697" + depends on LEDS_TI_LMU_COMMON + depends on I2C && OF + help + Say Y to enable the LM3697 LED driver for TI LMU devices. + This supports the LED device LM3697. + +config LEDS_LM36274 + tristate "LED driver for LM36274" + depends on LEDS_TI_LMU_COMMON + depends on MFD_TI_LMU + help + Say Y to enable the LM36274 LED driver for TI LMU devices. + This supports the LED device LM36274. + +config LEDS_TPS6105X + tristate "LED support for TI TPS6105X" + depends on LEDS_CLASS + depends on TPS6105X + default y if TPS6105X + help + This driver supports TPS61050/TPS61052 LED chips. + It is a single boost converter primarily for white LEDs and + audio amplifiers. + +config LEDS_IP30 + tristate "LED support for SGI Octane machines" + depends on LEDS_CLASS + depends on SGI_MFD_IOC3 || COMPILE_TEST + help + This option enables support for the Red and White LEDs of + SGI Octane machines. + + To compile this driver as a module, choose M here: the module + will be called leds-ip30. + +config LEDS_SGM3140 + tristate "LED support for the SGM3140" + depends on LEDS_CLASS_FLASH + depends on V4L2_FLASH_LED_CLASS || !V4L2_FLASH_LED_CLASS + help + This option enables support for the SGM3140 500mA Buck/Boost Charge + Pump LED Driver. + +config LEDS_ACER_A500 + tristate "Power button LED support for Acer Iconia Tab A500" + depends on LEDS_CLASS && MFD_ACER_A500_EC + help + This option enables support for the Power Button LED of + Acer Iconia Tab A500. + +comment "LED Triggers" +source "drivers/leds/trigger/Kconfig" + +endif # NEW_LEDS diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile new file mode 100644 index 000000000..73e603e17 --- /dev/null +++ b/drivers/leds/Makefile @@ -0,0 +1,107 @@ +# SPDX-License-Identifier: GPL-2.0 + +# LED Core +obj-$(CONFIG_NEW_LEDS) += led-core.o +obj-$(CONFIG_LEDS_CLASS) += led-class.o +obj-$(CONFIG_LEDS_CLASS_FLASH) += led-class-flash.o +obj-$(CONFIG_LEDS_CLASS_MULTICOLOR) += led-class-multicolor.o +obj-$(CONFIG_LEDS_TRIGGERS) += led-triggers.o + +# LED Platform Drivers (keep this sorted, M-| sort) +obj-$(CONFIG_LEDS_88PM860X) += leds-88pm860x.o +obj-$(CONFIG_LEDS_AAT1290) += leds-aat1290.o +obj-$(CONFIG_LEDS_ACER_A500) += leds-acer-a500.o +obj-$(CONFIG_LEDS_ADP5520) += leds-adp5520.o +obj-$(CONFIG_LEDS_AN30259A) += leds-an30259a.o +obj-$(CONFIG_LEDS_APU) += leds-apu.o +obj-$(CONFIG_LEDS_ARIEL) += leds-ariel.o +obj-$(CONFIG_LEDS_AS3645A) += leds-as3645a.o +obj-$(CONFIG_LEDS_ASIC3) += leds-asic3.o +obj-$(CONFIG_LEDS_AW2013) += leds-aw2013.o +obj-$(CONFIG_LEDS_BCM6328) += leds-bcm6328.o +obj-$(CONFIG_LEDS_BCM6358) += leds-bcm6358.o +obj-$(CONFIG_LEDS_BD2802) += leds-bd2802.o +obj-$(CONFIG_LEDS_BLINKM) += leds-blinkm.o +obj-$(CONFIG_LEDS_CLEVO_MAIL) += leds-clevo-mail.o +obj-$(CONFIG_LEDS_COBALT_QUBE) += leds-cobalt-qube.o +obj-$(CONFIG_LEDS_COBALT_RAQ) += leds-cobalt-raq.o +obj-$(CONFIG_LEDS_CPCAP) += leds-cpcap.o +obj-$(CONFIG_LEDS_DA903X) += leds-da903x.o +obj-$(CONFIG_LEDS_DA9052) += leds-da9052.o +obj-$(CONFIG_LEDS_FSG) += leds-fsg.o +obj-$(CONFIG_LEDS_GPIO) += leds-gpio.o +obj-$(CONFIG_LEDS_GPIO_REGISTER) += leds-gpio-register.o +obj-$(CONFIG_LEDS_HP6XX) += leds-hp6xx.o +obj-$(CONFIG_LEDS_INTEL_SS4200) += leds-ss4200.o +obj-$(CONFIG_LEDS_IP30) += leds-ip30.o +obj-$(CONFIG_LEDS_IPAQ_MICRO) += leds-ipaq-micro.o +obj-$(CONFIG_LEDS_IS31FL319X) += leds-is31fl319x.o +obj-$(CONFIG_LEDS_IS31FL32XX) += leds-is31fl32xx.o +obj-$(CONFIG_LEDS_KTD2692) += leds-ktd2692.o +obj-$(CONFIG_LEDS_LM3530) += leds-lm3530.o +obj-$(CONFIG_LEDS_LM3532) += leds-lm3532.o +obj-$(CONFIG_LEDS_LM3533) += leds-lm3533.o +obj-$(CONFIG_LEDS_LM355x) += leds-lm355x.o +obj-$(CONFIG_LEDS_LM3601X) += leds-lm3601x.o +obj-$(CONFIG_LEDS_LM36274) += leds-lm36274.o +obj-$(CONFIG_LEDS_LM3642) += leds-lm3642.o +obj-$(CONFIG_LEDS_LM3692X) += leds-lm3692x.o +obj-$(CONFIG_LEDS_LM3697) += leds-lm3697.o +obj-$(CONFIG_LEDS_LOCOMO) += leds-locomo.o +obj-$(CONFIG_LEDS_LP3944) += leds-lp3944.o +obj-$(CONFIG_LEDS_LP3952) += leds-lp3952.o +obj-$(CONFIG_LEDS_LP50XX) += leds-lp50xx.o +obj-$(CONFIG_LEDS_LP5521) += leds-lp5521.o +obj-$(CONFIG_LEDS_LP5523) += leds-lp5523.o +obj-$(CONFIG_LEDS_LP5562) += leds-lp5562.o +obj-$(CONFIG_LEDS_LP55XX_COMMON) += leds-lp55xx-common.o +obj-$(CONFIG_LEDS_LP8501) += leds-lp8501.o +obj-$(CONFIG_LEDS_LP8788) += leds-lp8788.o +obj-$(CONFIG_LEDS_LP8860) += leds-lp8860.o +obj-$(CONFIG_LEDS_LT3593) += leds-lt3593.o +obj-$(CONFIG_LEDS_MAX77650) += leds-max77650.o +obj-$(CONFIG_LEDS_MAX77693) += leds-max77693.o +obj-$(CONFIG_LEDS_MAX8997) += leds-max8997.o +obj-$(CONFIG_LEDS_MC13783) += leds-mc13783.o +obj-$(CONFIG_LEDS_MENF21BMC) += leds-menf21bmc.o +obj-$(CONFIG_LEDS_MIKROTIK_RB532) += leds-rb532.o +obj-$(CONFIG_LEDS_MLXCPLD) += leds-mlxcpld.o +obj-$(CONFIG_LEDS_MLXREG) += leds-mlxreg.o +obj-$(CONFIG_LEDS_MT6323) += leds-mt6323.o +obj-$(CONFIG_LEDS_NET48XX) += leds-net48xx.o +obj-$(CONFIG_LEDS_NETXBIG) += leds-netxbig.o +obj-$(CONFIG_LEDS_NIC78BX) += leds-nic78bx.o +obj-$(CONFIG_LEDS_NS2) += leds-ns2.o +obj-$(CONFIG_LEDS_OT200) += leds-ot200.o +obj-$(CONFIG_LEDS_PCA9532) += leds-pca9532.o +obj-$(CONFIG_LEDS_PCA955X) += leds-pca955x.o +obj-$(CONFIG_LEDS_PCA963X) += leds-pca963x.o +obj-$(CONFIG_LEDS_PM8058) += leds-pm8058.o +obj-$(CONFIG_LEDS_POWERNV) += leds-powernv.o +obj-$(CONFIG_LEDS_PWM) += leds-pwm.o +obj-$(CONFIG_LEDS_REGULATOR) += leds-regulator.o +obj-$(CONFIG_LEDS_S3C24XX) += leds-s3c24xx.o +obj-$(CONFIG_LEDS_SC27XX_BLTC) += leds-sc27xx-bltc.o +obj-$(CONFIG_LEDS_SGM3140) += leds-sgm3140.o +obj-$(CONFIG_LEDS_SUNFIRE) += leds-sunfire.o +obj-$(CONFIG_LEDS_SYSCON) += leds-syscon.o +obj-$(CONFIG_LEDS_TCA6507) += leds-tca6507.o +obj-$(CONFIG_LEDS_TI_LMU_COMMON) += leds-ti-lmu-common.o +obj-$(CONFIG_LEDS_TLC591XX) += leds-tlc591xx.o +obj-$(CONFIG_LEDS_TPS6105X) += leds-tps6105x.o +obj-$(CONFIG_LEDS_TURRIS_OMNIA) += leds-turris-omnia.o +obj-$(CONFIG_LEDS_WM831X_STATUS) += leds-wm831x-status.o +obj-$(CONFIG_LEDS_WM8350) += leds-wm8350.o +obj-$(CONFIG_LEDS_WRAP) += leds-wrap.o + +# LED SPI Drivers +obj-$(CONFIG_LEDS_CR0014114) += leds-cr0014114.o +obj-$(CONFIG_LEDS_DAC124S085) += leds-dac124s085.o +obj-$(CONFIG_LEDS_EL15203000) += leds-el15203000.o +obj-$(CONFIG_LEDS_SPI_BYTE) += leds-spi-byte.o + +# LED Userspace Drivers +obj-$(CONFIG_LEDS_USER) += uleds.o + +# LED Triggers +obj-$(CONFIG_LEDS_TRIGGERS) += trigger/ diff --git a/drivers/leds/TODO b/drivers/leds/TODO new file mode 100644 index 000000000..bfa60fa1d --- /dev/null +++ b/drivers/leds/TODO @@ -0,0 +1,75 @@ +-*- org -*- + +* On/off LEDs should have max_brightness of 1 +* Get rid of enum led_brightness + +It is really an integer, as maximum is configurable. Get rid of it, or +make it into typedef or something. + +* Review atomicity requirements in LED subsystem + +Calls that may and that may not block are mixed in same structure, and +semantics is sometimes non-intuitive. (For example blink callback may +not sleep.) Review the requirements for any bugs and document them +clearly. + +* LED names are still a mess + +No two LEDs have same name, so the names are probably unusable for the +userland. Nudge authors into creating common LED names for common +functionality. + +? Perhaps check for known LED names during boot, and warn if there are +LEDs not on the list? + +* Split drivers into subdirectories + +The number of drivers is getting big, and driver for on/off LED on a +i/o port is really quite different from camera flash LED, which is +really different from driver for RGB color LED that can run its own +microcode. Split the drivers somehow. + +* Figure out what to do with RGB leds + +Multicolor is a bit too abstract. Yes, we can have +Green-Magenta-Ultraviolet LED, but so far all the LEDs we support are +RGB, and not even RGB-White or RGB-Yellow variants emerged. + +Multicolor is not a good fit for RGB LED. It does not really know +about LED color. In particular, there's no way to make LED "white". + +Userspace is interested in knowing "this LED can produce arbitrary +color", which not all multicolor LEDs can. + + Proposal: let's add "rgb" to led_colors in drivers/leds/led-core.c, + add corresponding device tree defines, and use that, instead of + multicolor for RGB LEDs. + + We really need to do that now; "white" stuff can wait. + +RGB LEDs are quite common, and it would be good to be able to turn LED +white and to turn it into any arbitrary color. It is essential that +userspace is able to set arbitrary colors, and it might be good to +have that ability from kernel, too... to allow full-color triggers. + +* Command line utility to manipulate the LEDs? + +/sys interface is not really suitable to use by hand, should we have +an utility to perform LED control? + +In particular, LED names are still a mess (see above) and utility +could help there by presenting both old and new names while we clean +them up. + +In future, I'd like utility to accept both old and new names while we +clean them up. + +It would be also nice to have useful listing mode -- name, type, +current brightness/trigger... + +In future, it would be good to be able to set rgb led to particular +color. + +And probably user-friendly interface to access LEDs for particular +ethernet interface would be nice. + diff --git a/drivers/leds/led-class-flash.c b/drivers/leds/led-class-flash.c new file mode 100644 index 000000000..6eeb9effc --- /dev/null +++ b/drivers/leds/led-class-flash.c @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED Flash class interface + * + * Copyright (C) 2015 Samsung Electronics Co., Ltd. + * Author: Jacek Anaszewski <j.anaszewski@samsung.com> + */ + +#include <linux/device.h> +#include <linux/init.h> +#include <linux/led-class-flash.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/slab.h> +#include "leds.h" + +#define has_flash_op(fled_cdev, op) \ + (fled_cdev && fled_cdev->ops->op) + +#define call_flash_op(fled_cdev, op, args...) \ + ((has_flash_op(fled_cdev, op)) ? \ + (fled_cdev->ops->op(fled_cdev, args)) : \ + -EINVAL) + +static const char * const led_flash_fault_names[] = { + "led-over-voltage", + "flash-timeout-exceeded", + "controller-over-temperature", + "controller-short-circuit", + "led-power-supply-over-current", + "indicator-led-fault", + "led-under-voltage", + "controller-under-voltage", + "led-over-temperature", +}; + +static ssize_t flash_brightness_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + unsigned long state; + ssize_t ret; + + mutex_lock(&led_cdev->led_access); + + if (led_sysfs_is_disabled(led_cdev)) { + ret = -EBUSY; + goto unlock; + } + + ret = kstrtoul(buf, 10, &state); + if (ret) + goto unlock; + + ret = led_set_flash_brightness(fled_cdev, state); + if (ret < 0) + goto unlock; + + ret = size; +unlock: + mutex_unlock(&led_cdev->led_access); + return ret; +} + +static ssize_t flash_brightness_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + + /* no lock needed for this */ + led_update_flash_brightness(fled_cdev); + + return sprintf(buf, "%u\n", fled_cdev->brightness.val); +} +static DEVICE_ATTR_RW(flash_brightness); + +static ssize_t max_flash_brightness_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + + return sprintf(buf, "%u\n", fled_cdev->brightness.max); +} +static DEVICE_ATTR_RO(max_flash_brightness); + +static ssize_t flash_strobe_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + unsigned long state; + ssize_t ret = -EINVAL; + + mutex_lock(&led_cdev->led_access); + + if (led_sysfs_is_disabled(led_cdev)) { + ret = -EBUSY; + goto unlock; + } + + ret = kstrtoul(buf, 10, &state); + if (ret) + goto unlock; + + if (state > 1) { + ret = -EINVAL; + goto unlock; + } + + ret = led_set_flash_strobe(fled_cdev, state); + if (ret < 0) + goto unlock; + ret = size; +unlock: + mutex_unlock(&led_cdev->led_access); + return ret; +} + +static ssize_t flash_strobe_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + bool state; + int ret; + + /* no lock needed for this */ + ret = led_get_flash_strobe(fled_cdev, &state); + if (ret < 0) + return ret; + + return sprintf(buf, "%u\n", state); +} +static DEVICE_ATTR_RW(flash_strobe); + +static ssize_t flash_timeout_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + unsigned long flash_timeout; + ssize_t ret; + + mutex_lock(&led_cdev->led_access); + + if (led_sysfs_is_disabled(led_cdev)) { + ret = -EBUSY; + goto unlock; + } + + ret = kstrtoul(buf, 10, &flash_timeout); + if (ret) + goto unlock; + + ret = led_set_flash_timeout(fled_cdev, flash_timeout); + if (ret < 0) + goto unlock; + + ret = size; +unlock: + mutex_unlock(&led_cdev->led_access); + return ret; +} + +static ssize_t flash_timeout_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + + return sprintf(buf, "%u\n", fled_cdev->timeout.val); +} +static DEVICE_ATTR_RW(flash_timeout); + +static ssize_t max_flash_timeout_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + + return sprintf(buf, "%u\n", fled_cdev->timeout.max); +} +static DEVICE_ATTR_RO(max_flash_timeout); + +static ssize_t flash_fault_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + u32 fault, mask = 0x1; + char *pbuf = buf; + int i, ret, buf_len; + + ret = led_get_flash_fault(fled_cdev, &fault); + if (ret < 0) + return -EINVAL; + + *buf = '\0'; + + for (i = 0; i < LED_NUM_FLASH_FAULTS; ++i) { + if (fault & mask) { + buf_len = sprintf(pbuf, "%s ", + led_flash_fault_names[i]); + pbuf += buf_len; + } + mask <<= 1; + } + + return sprintf(buf, "%s\n", buf); +} +static DEVICE_ATTR_RO(flash_fault); + +static struct attribute *led_flash_strobe_attrs[] = { + &dev_attr_flash_strobe.attr, + NULL, +}; + +static struct attribute *led_flash_timeout_attrs[] = { + &dev_attr_flash_timeout.attr, + &dev_attr_max_flash_timeout.attr, + NULL, +}; + +static struct attribute *led_flash_brightness_attrs[] = { + &dev_attr_flash_brightness.attr, + &dev_attr_max_flash_brightness.attr, + NULL, +}; + +static struct attribute *led_flash_fault_attrs[] = { + &dev_attr_flash_fault.attr, + NULL, +}; + +static const struct attribute_group led_flash_strobe_group = { + .attrs = led_flash_strobe_attrs, +}; + +static const struct attribute_group led_flash_timeout_group = { + .attrs = led_flash_timeout_attrs, +}; + +static const struct attribute_group led_flash_brightness_group = { + .attrs = led_flash_brightness_attrs, +}; + +static const struct attribute_group led_flash_fault_group = { + .attrs = led_flash_fault_attrs, +}; + +static void led_flash_resume(struct led_classdev *led_cdev) +{ + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + + call_flash_op(fled_cdev, flash_brightness_set, + fled_cdev->brightness.val); + call_flash_op(fled_cdev, timeout_set, fled_cdev->timeout.val); +} + +static void led_flash_init_sysfs_groups(struct led_classdev_flash *fled_cdev) +{ + struct led_classdev *led_cdev = &fled_cdev->led_cdev; + const struct led_flash_ops *ops = fled_cdev->ops; + const struct attribute_group **flash_groups = fled_cdev->sysfs_groups; + + int num_sysfs_groups = 0; + + flash_groups[num_sysfs_groups++] = &led_flash_strobe_group; + + if (ops->flash_brightness_set) + flash_groups[num_sysfs_groups++] = &led_flash_brightness_group; + + if (ops->timeout_set) + flash_groups[num_sysfs_groups++] = &led_flash_timeout_group; + + if (ops->fault_get) + flash_groups[num_sysfs_groups++] = &led_flash_fault_group; + + led_cdev->groups = flash_groups; +} + +int led_classdev_flash_register_ext(struct device *parent, + struct led_classdev_flash *fled_cdev, + struct led_init_data *init_data) +{ + struct led_classdev *led_cdev; + const struct led_flash_ops *ops; + int ret; + + if (!fled_cdev) + return -EINVAL; + + led_cdev = &fled_cdev->led_cdev; + + if (led_cdev->flags & LED_DEV_CAP_FLASH) { + if (!led_cdev->brightness_set_blocking) + return -EINVAL; + + ops = fled_cdev->ops; + if (!ops || !ops->strobe_set) + return -EINVAL; + + led_cdev->flash_resume = led_flash_resume; + + /* Select the sysfs attributes to be created for the device */ + led_flash_init_sysfs_groups(fled_cdev); + } + + /* Register led class device */ + ret = led_classdev_register_ext(parent, led_cdev, init_data); + if (ret < 0) + return ret; + + return 0; +} +EXPORT_SYMBOL_GPL(led_classdev_flash_register_ext); + +void led_classdev_flash_unregister(struct led_classdev_flash *fled_cdev) +{ + if (!fled_cdev) + return; + + led_classdev_unregister(&fled_cdev->led_cdev); +} +EXPORT_SYMBOL_GPL(led_classdev_flash_unregister); + +static void devm_led_classdev_flash_release(struct device *dev, void *res) +{ + led_classdev_flash_unregister(*(struct led_classdev_flash **)res); +} + +int devm_led_classdev_flash_register_ext(struct device *parent, + struct led_classdev_flash *fled_cdev, + struct led_init_data *init_data) +{ + struct led_classdev_flash **dr; + int ret; + + dr = devres_alloc(devm_led_classdev_flash_release, sizeof(*dr), + GFP_KERNEL); + if (!dr) + return -ENOMEM; + + ret = led_classdev_flash_register_ext(parent, fled_cdev, init_data); + if (ret) { + devres_free(dr); + return ret; + } + + *dr = fled_cdev; + devres_add(parent, dr); + + return 0; +} +EXPORT_SYMBOL_GPL(devm_led_classdev_flash_register_ext); + +static int devm_led_classdev_flash_match(struct device *dev, + void *res, void *data) +{ + struct led_classdev_flash **p = res; + + if (WARN_ON(!p || !*p)) + return 0; + + return *p == data; +} + +void devm_led_classdev_flash_unregister(struct device *dev, + struct led_classdev_flash *fled_cdev) +{ + WARN_ON(devres_release(dev, + devm_led_classdev_flash_release, + devm_led_classdev_flash_match, fled_cdev)); +} +EXPORT_SYMBOL_GPL(devm_led_classdev_flash_unregister); + +static void led_clamp_align(struct led_flash_setting *s) +{ + u32 v, offset; + + v = s->val + s->step / 2; + v = clamp(v, s->min, s->max); + offset = v - s->min; + offset = s->step * (offset / s->step); + s->val = s->min + offset; +} + +int led_set_flash_timeout(struct led_classdev_flash *fled_cdev, u32 timeout) +{ + struct led_classdev *led_cdev = &fled_cdev->led_cdev; + struct led_flash_setting *s = &fled_cdev->timeout; + + s->val = timeout; + led_clamp_align(s); + + if (!(led_cdev->flags & LED_SUSPENDED)) + return call_flash_op(fled_cdev, timeout_set, s->val); + + return 0; +} +EXPORT_SYMBOL_GPL(led_set_flash_timeout); + +int led_get_flash_fault(struct led_classdev_flash *fled_cdev, u32 *fault) +{ + return call_flash_op(fled_cdev, fault_get, fault); +} +EXPORT_SYMBOL_GPL(led_get_flash_fault); + +int led_set_flash_brightness(struct led_classdev_flash *fled_cdev, + u32 brightness) +{ + struct led_classdev *led_cdev = &fled_cdev->led_cdev; + struct led_flash_setting *s = &fled_cdev->brightness; + + s->val = brightness; + led_clamp_align(s); + + if (!(led_cdev->flags & LED_SUSPENDED)) + return call_flash_op(fled_cdev, flash_brightness_set, s->val); + + return 0; +} +EXPORT_SYMBOL_GPL(led_set_flash_brightness); + +int led_update_flash_brightness(struct led_classdev_flash *fled_cdev) +{ + struct led_flash_setting *s = &fled_cdev->brightness; + u32 brightness; + + if (has_flash_op(fled_cdev, flash_brightness_get)) { + int ret = call_flash_op(fled_cdev, flash_brightness_get, + &brightness); + if (ret < 0) + return ret; + + s->val = brightness; + } + + return 0; +} +EXPORT_SYMBOL_GPL(led_update_flash_brightness); + +MODULE_AUTHOR("Jacek Anaszewski <j.anaszewski@samsung.com>"); +MODULE_DESCRIPTION("LED Flash class interface"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/led-class-multicolor.c b/drivers/leds/led-class-multicolor.c new file mode 100644 index 000000000..e31740858 --- /dev/null +++ b/drivers/leds/led-class-multicolor.c @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-2.0 +// LED Multicolor class interface +// Copyright (C) 2019-20 Texas Instruments Incorporated - http://www.ti.com/ +// Author: Dan Murphy <dmurphy@ti.com> + +#include <linux/device.h> +#include <linux/init.h> +#include <linux/led-class-multicolor.h> +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/uaccess.h> + +#include "leds.h" + +int led_mc_calc_color_components(struct led_classdev_mc *mcled_cdev, + enum led_brightness brightness) +{ + struct led_classdev *led_cdev = &mcled_cdev->led_cdev; + int i; + + for (i = 0; i < mcled_cdev->num_colors; i++) + mcled_cdev->subled_info[i].brightness = brightness * + mcled_cdev->subled_info[i].intensity / + led_cdev->max_brightness; + + return 0; +} +EXPORT_SYMBOL_GPL(led_mc_calc_color_components); + +static ssize_t multi_intensity_store(struct device *dev, + struct device_attribute *intensity_attr, + const char *buf, size_t size) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *mcled_cdev = lcdev_to_mccdev(led_cdev); + int nrchars, offset = 0; + int intensity_value[LED_COLOR_ID_MAX]; + int i; + ssize_t ret; + + mutex_lock(&led_cdev->led_access); + + for (i = 0; i < mcled_cdev->num_colors; i++) { + ret = sscanf(buf + offset, "%i%n", + &intensity_value[i], &nrchars); + if (ret != 1) { + ret = -EINVAL; + goto err_out; + } + offset += nrchars; + } + + offset++; + if (offset < size) { + ret = -EINVAL; + goto err_out; + } + + for (i = 0; i < mcled_cdev->num_colors; i++) + mcled_cdev->subled_info[i].intensity = intensity_value[i]; + + led_set_brightness(led_cdev, led_cdev->brightness); + ret = size; +err_out: + mutex_unlock(&led_cdev->led_access); + return ret; +} + +static ssize_t multi_intensity_show(struct device *dev, + struct device_attribute *intensity_attr, + char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *mcled_cdev = lcdev_to_mccdev(led_cdev); + int len = 0; + int i; + + for (i = 0; i < mcled_cdev->num_colors; i++) { + len += sprintf(buf + len, "%d", + mcled_cdev->subled_info[i].intensity); + if (i < mcled_cdev->num_colors - 1) + len += sprintf(buf + len, " "); + } + + buf[len++] = '\n'; + return len; +} +static DEVICE_ATTR_RW(multi_intensity); + +static ssize_t multi_index_show(struct device *dev, + struct device_attribute *multi_index_attr, + char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_classdev_mc *mcled_cdev = lcdev_to_mccdev(led_cdev); + int len = 0; + int index; + int i; + + for (i = 0; i < mcled_cdev->num_colors; i++) { + index = mcled_cdev->subled_info[i].color_index; + len += sprintf(buf + len, "%s", led_colors[index]); + if (i < mcled_cdev->num_colors - 1) + len += sprintf(buf + len, " "); + } + + buf[len++] = '\n'; + return len; +} +static DEVICE_ATTR_RO(multi_index); + +static struct attribute *led_multicolor_attrs[] = { + &dev_attr_multi_intensity.attr, + &dev_attr_multi_index.attr, + NULL, +}; +ATTRIBUTE_GROUPS(led_multicolor); + +int led_classdev_multicolor_register_ext(struct device *parent, + struct led_classdev_mc *mcled_cdev, + struct led_init_data *init_data) +{ + struct led_classdev *led_cdev; + + if (!mcled_cdev) + return -EINVAL; + + if (mcled_cdev->num_colors <= 0) + return -EINVAL; + + if (mcled_cdev->num_colors > LED_COLOR_ID_MAX) + return -EINVAL; + + led_cdev = &mcled_cdev->led_cdev; + mcled_cdev->led_cdev.groups = led_multicolor_groups; + + return led_classdev_register_ext(parent, led_cdev, init_data); +} +EXPORT_SYMBOL_GPL(led_classdev_multicolor_register_ext); + +void led_classdev_multicolor_unregister(struct led_classdev_mc *mcled_cdev) +{ + if (!mcled_cdev) + return; + + led_classdev_unregister(&mcled_cdev->led_cdev); +} +EXPORT_SYMBOL_GPL(led_classdev_multicolor_unregister); + +static void devm_led_classdev_multicolor_release(struct device *dev, void *res) +{ + led_classdev_multicolor_unregister(*(struct led_classdev_mc **)res); +} + +int devm_led_classdev_multicolor_register_ext(struct device *parent, + struct led_classdev_mc *mcled_cdev, + struct led_init_data *init_data) +{ + struct led_classdev_mc **dr; + int ret; + + dr = devres_alloc(devm_led_classdev_multicolor_release, + sizeof(*dr), GFP_KERNEL); + if (!dr) + return -ENOMEM; + + ret = led_classdev_multicolor_register_ext(parent, mcled_cdev, + init_data); + if (ret) { + devres_free(dr); + return ret; + } + + *dr = mcled_cdev; + devres_add(parent, dr); + + return 0; +} +EXPORT_SYMBOL_GPL(devm_led_classdev_multicolor_register_ext); + +static int devm_led_classdev_multicolor_match(struct device *dev, + void *res, void *data) +{ + struct led_classdev_mc **p = res; + + if (WARN_ON(!p || !*p)) + return 0; + + return *p == data; +} + +void devm_led_classdev_multicolor_unregister(struct device *dev, + struct led_classdev_mc *mcled_cdev) +{ + WARN_ON(devres_release(dev, + devm_led_classdev_multicolor_release, + devm_led_classdev_multicolor_match, mcled_cdev)); +} +EXPORT_SYMBOL_GPL(devm_led_classdev_multicolor_unregister); + +MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>"); +MODULE_DESCRIPTION("Multicolor LED class interface"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/led-class.c b/drivers/leds/led-class.c new file mode 100644 index 000000000..fcb9eee3b --- /dev/null +++ b/drivers/leds/led-class.c @@ -0,0 +1,549 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED Class Core + * + * Copyright (C) 2005 John Lenz <lenz@cs.wisc.edu> + * Copyright (C) 2005-2007 Richard Purdie <rpurdie@openedhand.com> + */ + +#include <linux/ctype.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/leds.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/property.h> +#include <linux/slab.h> +#include <linux/spinlock.h> +#include <linux/timer.h> +#include <uapi/linux/uleds.h> +#include <linux/of.h> +#include "leds.h" + +static struct class *leds_class; + +static ssize_t brightness_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + + /* no lock needed for this */ + led_update_brightness(led_cdev); + + return sprintf(buf, "%u\n", led_cdev->brightness); +} + +static ssize_t brightness_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + unsigned long state; + ssize_t ret; + + mutex_lock(&led_cdev->led_access); + + if (led_sysfs_is_disabled(led_cdev)) { + ret = -EBUSY; + goto unlock; + } + + ret = kstrtoul(buf, 10, &state); + if (ret) + goto unlock; + + if (state == LED_OFF) + led_trigger_remove(led_cdev); + led_set_brightness(led_cdev, state); + flush_work(&led_cdev->set_brightness_work); + + ret = size; +unlock: + mutex_unlock(&led_cdev->led_access); + return ret; +} +static DEVICE_ATTR_RW(brightness); + +static ssize_t max_brightness_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + + return sprintf(buf, "%u\n", led_cdev->max_brightness); +} +static DEVICE_ATTR_RO(max_brightness); + +#ifdef CONFIG_LEDS_TRIGGERS +static BIN_ATTR(trigger, 0644, led_trigger_read, led_trigger_write, 0); +static struct bin_attribute *led_trigger_bin_attrs[] = { + &bin_attr_trigger, + NULL, +}; +static const struct attribute_group led_trigger_group = { + .bin_attrs = led_trigger_bin_attrs, +}; +#endif + +static struct attribute *led_class_attrs[] = { + &dev_attr_brightness.attr, + &dev_attr_max_brightness.attr, + NULL, +}; + +static const struct attribute_group led_group = { + .attrs = led_class_attrs, +}; + +static const struct attribute_group *led_groups[] = { + &led_group, +#ifdef CONFIG_LEDS_TRIGGERS + &led_trigger_group, +#endif + NULL, +}; + +#ifdef CONFIG_LEDS_BRIGHTNESS_HW_CHANGED +static ssize_t brightness_hw_changed_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + + if (led_cdev->brightness_hw_changed == -1) + return -ENODATA; + + return sprintf(buf, "%u\n", led_cdev->brightness_hw_changed); +} + +static DEVICE_ATTR_RO(brightness_hw_changed); + +static int led_add_brightness_hw_changed(struct led_classdev *led_cdev) +{ + struct device *dev = led_cdev->dev; + int ret; + + ret = device_create_file(dev, &dev_attr_brightness_hw_changed); + if (ret) { + dev_err(dev, "Error creating brightness_hw_changed\n"); + return ret; + } + + led_cdev->brightness_hw_changed_kn = + sysfs_get_dirent(dev->kobj.sd, "brightness_hw_changed"); + if (!led_cdev->brightness_hw_changed_kn) { + dev_err(dev, "Error getting brightness_hw_changed kn\n"); + device_remove_file(dev, &dev_attr_brightness_hw_changed); + return -ENXIO; + } + + return 0; +} + +static void led_remove_brightness_hw_changed(struct led_classdev *led_cdev) +{ + sysfs_put(led_cdev->brightness_hw_changed_kn); + device_remove_file(led_cdev->dev, &dev_attr_brightness_hw_changed); +} + +void led_classdev_notify_brightness_hw_changed(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + if (WARN_ON(!led_cdev->brightness_hw_changed_kn)) + return; + + led_cdev->brightness_hw_changed = brightness; + sysfs_notify_dirent(led_cdev->brightness_hw_changed_kn); +} +EXPORT_SYMBOL_GPL(led_classdev_notify_brightness_hw_changed); +#else +static int led_add_brightness_hw_changed(struct led_classdev *led_cdev) +{ + return 0; +} +static void led_remove_brightness_hw_changed(struct led_classdev *led_cdev) +{ +} +#endif + +/** + * led_classdev_suspend - suspend an led_classdev. + * @led_cdev: the led_classdev to suspend. + */ +void led_classdev_suspend(struct led_classdev *led_cdev) +{ + led_cdev->flags |= LED_SUSPENDED; + led_set_brightness_nopm(led_cdev, 0); + flush_work(&led_cdev->set_brightness_work); +} +EXPORT_SYMBOL_GPL(led_classdev_suspend); + +/** + * led_classdev_resume - resume an led_classdev. + * @led_cdev: the led_classdev to resume. + */ +void led_classdev_resume(struct led_classdev *led_cdev) +{ + led_set_brightness_nopm(led_cdev, led_cdev->brightness); + + if (led_cdev->flash_resume) + led_cdev->flash_resume(led_cdev); + + led_cdev->flags &= ~LED_SUSPENDED; +} +EXPORT_SYMBOL_GPL(led_classdev_resume); + +#ifdef CONFIG_PM_SLEEP +static int led_suspend(struct device *dev) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + + if (led_cdev->flags & LED_CORE_SUSPENDRESUME) + led_classdev_suspend(led_cdev); + + return 0; +} + +static int led_resume(struct device *dev) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + + if (led_cdev->flags & LED_CORE_SUSPENDRESUME) + led_classdev_resume(led_cdev); + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(leds_class_dev_pm_ops, led_suspend, led_resume); + +/** + * of_led_get() - request a LED device via the LED framework + * @np: device node to get the LED device from + * @index: the index of the LED + * + * Returns the LED device parsed from the phandle specified in the "leds" + * property of a device tree node or a negative error-code on failure. + */ +struct led_classdev *of_led_get(struct device_node *np, int index) +{ + struct device *led_dev; + struct led_classdev *led_cdev; + struct device_node *led_node; + + led_node = of_parse_phandle(np, "leds", index); + if (!led_node) + return ERR_PTR(-ENOENT); + + led_dev = class_find_device_by_of_node(leds_class, led_node); + of_node_put(led_node); + put_device(led_dev); + + if (!led_dev) + return ERR_PTR(-EPROBE_DEFER); + + led_cdev = dev_get_drvdata(led_dev); + + if (!try_module_get(led_cdev->dev->parent->driver->owner)) { + put_device(led_cdev->dev); + return ERR_PTR(-ENODEV); + } + + return led_cdev; +} +EXPORT_SYMBOL_GPL(of_led_get); + +/** + * led_put() - release a LED device + * @led_cdev: LED device + */ +void led_put(struct led_classdev *led_cdev) +{ + module_put(led_cdev->dev->parent->driver->owner); + put_device(led_cdev->dev); +} +EXPORT_SYMBOL_GPL(led_put); + +static void devm_led_release(struct device *dev, void *res) +{ + struct led_classdev **p = res; + + led_put(*p); +} + +/** + * devm_of_led_get - Resource-managed request of a LED device + * @dev: LED consumer + * @index: index of the LED to obtain in the consumer + * + * The device node of the device is parse to find the request LED device. + * The LED device returned from this function is automatically released + * on driver detach. + * + * @return a pointer to a LED device or ERR_PTR(errno) on failure. + */ +struct led_classdev *__must_check devm_of_led_get(struct device *dev, + int index) +{ + struct led_classdev *led; + struct led_classdev **dr; + + if (!dev) + return ERR_PTR(-EINVAL); + + led = of_led_get(dev->of_node, index); + if (IS_ERR(led)) + return led; + + dr = devres_alloc(devm_led_release, sizeof(struct led_classdev *), + GFP_KERNEL); + if (!dr) { + led_put(led); + return ERR_PTR(-ENOMEM); + } + + *dr = led; + devres_add(dev, dr); + + return led; +} +EXPORT_SYMBOL_GPL(devm_of_led_get); + +static int led_classdev_next_name(const char *init_name, char *name, + size_t len) +{ + unsigned int i = 0; + int ret = 0; + struct device *dev; + + strlcpy(name, init_name, len); + + while ((ret < len) && + (dev = class_find_device_by_name(leds_class, name))) { + put_device(dev); + ret = snprintf(name, len, "%s_%u", init_name, ++i); + } + + if (ret >= len) + return -ENOMEM; + + return i; +} + +/** + * led_classdev_register_ext - register a new object of led_classdev class + * with init data. + * + * @parent: parent of LED device + * @led_cdev: the led_classdev structure for this device. + * @init_data: LED class device initialization data + */ +int led_classdev_register_ext(struct device *parent, + struct led_classdev *led_cdev, + struct led_init_data *init_data) +{ + char composed_name[LED_MAX_NAME_SIZE]; + char final_name[LED_MAX_NAME_SIZE]; + const char *proposed_name = composed_name; + int ret; + + if (init_data) { + if (init_data->devname_mandatory && !init_data->devicename) { + dev_err(parent, "Mandatory device name is missing"); + return -EINVAL; + } + ret = led_compose_name(parent, init_data, composed_name); + if (ret < 0) + return ret; + + if (init_data->fwnode) + fwnode_property_read_string(init_data->fwnode, + "linux,default-trigger", + &led_cdev->default_trigger); + } else { + proposed_name = led_cdev->name; + } + + ret = led_classdev_next_name(proposed_name, final_name, sizeof(final_name)); + if (ret < 0) + return ret; + + mutex_init(&led_cdev->led_access); + mutex_lock(&led_cdev->led_access); + led_cdev->dev = device_create_with_groups(leds_class, parent, 0, + led_cdev, led_cdev->groups, "%s", final_name); + if (IS_ERR(led_cdev->dev)) { + mutex_unlock(&led_cdev->led_access); + return PTR_ERR(led_cdev->dev); + } + if (init_data && init_data->fwnode) { + led_cdev->dev->fwnode = init_data->fwnode; + led_cdev->dev->of_node = to_of_node(init_data->fwnode); + } + + if (ret) + dev_warn(parent, "Led %s renamed to %s due to name collision", + proposed_name, dev_name(led_cdev->dev)); + + if (led_cdev->flags & LED_BRIGHT_HW_CHANGED) { + ret = led_add_brightness_hw_changed(led_cdev); + if (ret) { + device_unregister(led_cdev->dev); + led_cdev->dev = NULL; + mutex_unlock(&led_cdev->led_access); + return ret; + } + } + + led_cdev->work_flags = 0; +#ifdef CONFIG_LEDS_TRIGGERS + init_rwsem(&led_cdev->trigger_lock); +#endif +#ifdef CONFIG_LEDS_BRIGHTNESS_HW_CHANGED + led_cdev->brightness_hw_changed = -1; +#endif + /* add to the list of leds */ + down_write(&leds_list_lock); + list_add_tail(&led_cdev->node, &leds_list); + up_write(&leds_list_lock); + + if (!led_cdev->max_brightness) + led_cdev->max_brightness = LED_FULL; + + led_update_brightness(led_cdev); + + led_init_core(led_cdev); + +#ifdef CONFIG_LEDS_TRIGGERS + led_trigger_set_default(led_cdev); +#endif + + mutex_unlock(&led_cdev->led_access); + + dev_dbg(parent, "Registered led device: %s\n", + led_cdev->name); + + return 0; +} +EXPORT_SYMBOL_GPL(led_classdev_register_ext); + +/** + * led_classdev_unregister - unregisters a object of led_properties class. + * @led_cdev: the led device to unregister + * + * Unregisters a previously registered via led_classdev_register object. + */ +void led_classdev_unregister(struct led_classdev *led_cdev) +{ + if (IS_ERR_OR_NULL(led_cdev->dev)) + return; + +#ifdef CONFIG_LEDS_TRIGGERS + down_write(&led_cdev->trigger_lock); + if (led_cdev->trigger) + led_trigger_set(led_cdev, NULL); + up_write(&led_cdev->trigger_lock); +#endif + + led_cdev->flags |= LED_UNREGISTERING; + + /* Stop blinking */ + led_stop_software_blink(led_cdev); + + led_set_brightness(led_cdev, LED_OFF); + + flush_work(&led_cdev->set_brightness_work); + + if (led_cdev->flags & LED_BRIGHT_HW_CHANGED) + led_remove_brightness_hw_changed(led_cdev); + + device_unregister(led_cdev->dev); + + down_write(&leds_list_lock); + list_del(&led_cdev->node); + up_write(&leds_list_lock); + + mutex_destroy(&led_cdev->led_access); +} +EXPORT_SYMBOL_GPL(led_classdev_unregister); + +static void devm_led_classdev_release(struct device *dev, void *res) +{ + led_classdev_unregister(*(struct led_classdev **)res); +} + +/** + * devm_led_classdev_register_ext - resource managed led_classdev_register_ext() + * + * @parent: parent of LED device + * @led_cdev: the led_classdev structure for this device. + * @init_data: LED class device initialization data + */ +int devm_led_classdev_register_ext(struct device *parent, + struct led_classdev *led_cdev, + struct led_init_data *init_data) +{ + struct led_classdev **dr; + int rc; + + dr = devres_alloc(devm_led_classdev_release, sizeof(*dr), GFP_KERNEL); + if (!dr) + return -ENOMEM; + + rc = led_classdev_register_ext(parent, led_cdev, init_data); + if (rc) { + devres_free(dr); + return rc; + } + + *dr = led_cdev; + devres_add(parent, dr); + + return 0; +} +EXPORT_SYMBOL_GPL(devm_led_classdev_register_ext); + +static int devm_led_classdev_match(struct device *dev, void *res, void *data) +{ + struct led_classdev **p = res; + + if (WARN_ON(!p || !*p)) + return 0; + + return *p == data; +} + +/** + * devm_led_classdev_unregister() - resource managed led_classdev_unregister() + * @parent: The device to unregister. + * @led_cdev: the led_classdev structure for this device. + */ +void devm_led_classdev_unregister(struct device *dev, + struct led_classdev *led_cdev) +{ + WARN_ON(devres_release(dev, + devm_led_classdev_release, + devm_led_classdev_match, led_cdev)); +} +EXPORT_SYMBOL_GPL(devm_led_classdev_unregister); + +static int __init leds_init(void) +{ + leds_class = class_create(THIS_MODULE, "leds"); + if (IS_ERR(leds_class)) + return PTR_ERR(leds_class); + leds_class->pm = &leds_class_dev_pm_ops; + leds_class->dev_groups = led_groups; + return 0; +} + +static void __exit leds_exit(void) +{ + class_destroy(leds_class); +} + +subsys_initcall(leds_init); +module_exit(leds_exit); + +MODULE_AUTHOR("John Lenz, Richard Purdie"); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("LED Class Interface"); diff --git a/drivers/leds/led-core.c b/drivers/leds/led-core.c new file mode 100644 index 000000000..c4e780bdb --- /dev/null +++ b/drivers/leds/led-core.c @@ -0,0 +1,485 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED Class Core + * + * Copyright 2005-2006 Openedhand Ltd. + * + * Author: Richard Purdie <rpurdie@openedhand.com> + */ + +#include <linux/kernel.h> +#include <linux/leds.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/property.h> +#include <linux/rwsem.h> +#include <linux/slab.h> +#include <uapi/linux/uleds.h> +#include "leds.h" + +DECLARE_RWSEM(leds_list_lock); +EXPORT_SYMBOL_GPL(leds_list_lock); + +LIST_HEAD(leds_list); +EXPORT_SYMBOL_GPL(leds_list); + +const char * const led_colors[LED_COLOR_ID_MAX] = { + [LED_COLOR_ID_WHITE] = "white", + [LED_COLOR_ID_RED] = "red", + [LED_COLOR_ID_GREEN] = "green", + [LED_COLOR_ID_BLUE] = "blue", + [LED_COLOR_ID_AMBER] = "amber", + [LED_COLOR_ID_VIOLET] = "violet", + [LED_COLOR_ID_YELLOW] = "yellow", + [LED_COLOR_ID_IR] = "ir", + [LED_COLOR_ID_MULTI] = "multicolor", + [LED_COLOR_ID_RGB] = "rgb", +}; +EXPORT_SYMBOL_GPL(led_colors); + +static int __led_set_brightness(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (!led_cdev->brightness_set) + return -ENOTSUPP; + + led_cdev->brightness_set(led_cdev, value); + + return 0; +} + +static int __led_set_brightness_blocking(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (!led_cdev->brightness_set_blocking) + return -ENOTSUPP; + + return led_cdev->brightness_set_blocking(led_cdev, value); +} + +static void led_timer_function(struct timer_list *t) +{ + struct led_classdev *led_cdev = from_timer(led_cdev, t, blink_timer); + unsigned long brightness; + unsigned long delay; + + if (!led_cdev->blink_delay_on || !led_cdev->blink_delay_off) { + led_set_brightness_nosleep(led_cdev, LED_OFF); + clear_bit(LED_BLINK_SW, &led_cdev->work_flags); + return; + } + + if (test_and_clear_bit(LED_BLINK_ONESHOT_STOP, + &led_cdev->work_flags)) { + clear_bit(LED_BLINK_SW, &led_cdev->work_flags); + return; + } + + brightness = led_get_brightness(led_cdev); + if (!brightness) { + /* Time to switch the LED on. */ + if (test_and_clear_bit(LED_BLINK_BRIGHTNESS_CHANGE, + &led_cdev->work_flags)) + brightness = led_cdev->new_blink_brightness; + else + brightness = led_cdev->blink_brightness; + delay = led_cdev->blink_delay_on; + } else { + /* Store the current brightness value to be able + * to restore it when the delay_off period is over. + */ + led_cdev->blink_brightness = brightness; + brightness = LED_OFF; + delay = led_cdev->blink_delay_off; + } + + led_set_brightness_nosleep(led_cdev, brightness); + + /* Return in next iteration if led is in one-shot mode and we are in + * the final blink state so that the led is toggled each delay_on + + * delay_off milliseconds in worst case. + */ + if (test_bit(LED_BLINK_ONESHOT, &led_cdev->work_flags)) { + if (test_bit(LED_BLINK_INVERT, &led_cdev->work_flags)) { + if (brightness) + set_bit(LED_BLINK_ONESHOT_STOP, + &led_cdev->work_flags); + } else { + if (!brightness) + set_bit(LED_BLINK_ONESHOT_STOP, + &led_cdev->work_flags); + } + } + + mod_timer(&led_cdev->blink_timer, jiffies + msecs_to_jiffies(delay)); +} + +static void set_brightness_delayed(struct work_struct *ws) +{ + struct led_classdev *led_cdev = + container_of(ws, struct led_classdev, set_brightness_work); + int ret = 0; + + if (test_and_clear_bit(LED_BLINK_DISABLE, &led_cdev->work_flags)) { + led_cdev->delayed_set_value = LED_OFF; + led_stop_software_blink(led_cdev); + } + + ret = __led_set_brightness(led_cdev, led_cdev->delayed_set_value); + if (ret == -ENOTSUPP) + ret = __led_set_brightness_blocking(led_cdev, + led_cdev->delayed_set_value); + if (ret < 0 && + /* LED HW might have been unplugged, therefore don't warn */ + !(ret == -ENODEV && (led_cdev->flags & LED_UNREGISTERING) && + (led_cdev->flags & LED_HW_PLUGGABLE))) + dev_err(led_cdev->dev, + "Setting an LED's brightness failed (%d)\n", ret); +} + +static void led_set_software_blink(struct led_classdev *led_cdev, + unsigned long delay_on, + unsigned long delay_off) +{ + int current_brightness; + + current_brightness = led_get_brightness(led_cdev); + if (current_brightness) + led_cdev->blink_brightness = current_brightness; + if (!led_cdev->blink_brightness) + led_cdev->blink_brightness = led_cdev->max_brightness; + + led_cdev->blink_delay_on = delay_on; + led_cdev->blink_delay_off = delay_off; + + /* never on - just set to off */ + if (!delay_on) { + led_set_brightness_nosleep(led_cdev, LED_OFF); + return; + } + + /* never off - just set to brightness */ + if (!delay_off) { + led_set_brightness_nosleep(led_cdev, + led_cdev->blink_brightness); + return; + } + + set_bit(LED_BLINK_SW, &led_cdev->work_flags); + mod_timer(&led_cdev->blink_timer, jiffies + 1); +} + + +static void led_blink_setup(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + if (!test_bit(LED_BLINK_ONESHOT, &led_cdev->work_flags) && + led_cdev->blink_set && + !led_cdev->blink_set(led_cdev, delay_on, delay_off)) + return; + + /* blink with 1 Hz as default if nothing specified */ + if (!*delay_on && !*delay_off) + *delay_on = *delay_off = 500; + + led_set_software_blink(led_cdev, *delay_on, *delay_off); +} + +void led_init_core(struct led_classdev *led_cdev) +{ + INIT_WORK(&led_cdev->set_brightness_work, set_brightness_delayed); + + timer_setup(&led_cdev->blink_timer, led_timer_function, 0); +} +EXPORT_SYMBOL_GPL(led_init_core); + +void led_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + del_timer_sync(&led_cdev->blink_timer); + + clear_bit(LED_BLINK_SW, &led_cdev->work_flags); + clear_bit(LED_BLINK_ONESHOT, &led_cdev->work_flags); + clear_bit(LED_BLINK_ONESHOT_STOP, &led_cdev->work_flags); + + led_blink_setup(led_cdev, delay_on, delay_off); +} +EXPORT_SYMBOL_GPL(led_blink_set); + +void led_blink_set_oneshot(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off, + int invert) +{ + if (test_bit(LED_BLINK_ONESHOT, &led_cdev->work_flags) && + timer_pending(&led_cdev->blink_timer)) + return; + + set_bit(LED_BLINK_ONESHOT, &led_cdev->work_flags); + clear_bit(LED_BLINK_ONESHOT_STOP, &led_cdev->work_flags); + + if (invert) + set_bit(LED_BLINK_INVERT, &led_cdev->work_flags); + else + clear_bit(LED_BLINK_INVERT, &led_cdev->work_flags); + + led_blink_setup(led_cdev, delay_on, delay_off); +} +EXPORT_SYMBOL_GPL(led_blink_set_oneshot); + +void led_stop_software_blink(struct led_classdev *led_cdev) +{ + del_timer_sync(&led_cdev->blink_timer); + led_cdev->blink_delay_on = 0; + led_cdev->blink_delay_off = 0; + clear_bit(LED_BLINK_SW, &led_cdev->work_flags); +} +EXPORT_SYMBOL_GPL(led_stop_software_blink); + +void led_set_brightness(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + /* + * If software blink is active, delay brightness setting + * until the next timer tick. + */ + if (test_bit(LED_BLINK_SW, &led_cdev->work_flags)) { + /* + * If we need to disable soft blinking delegate this to the + * work queue task to avoid problems in case we are called + * from hard irq context. + */ + if (brightness == LED_OFF) { + set_bit(LED_BLINK_DISABLE, &led_cdev->work_flags); + schedule_work(&led_cdev->set_brightness_work); + } else { + set_bit(LED_BLINK_BRIGHTNESS_CHANGE, + &led_cdev->work_flags); + led_cdev->new_blink_brightness = brightness; + } + return; + } + + led_set_brightness_nosleep(led_cdev, brightness); +} +EXPORT_SYMBOL_GPL(led_set_brightness); + +void led_set_brightness_nopm(struct led_classdev *led_cdev, + enum led_brightness value) +{ + /* Use brightness_set op if available, it is guaranteed not to sleep */ + if (!__led_set_brightness(led_cdev, value)) + return; + + /* If brightness setting can sleep, delegate it to a work queue task */ + led_cdev->delayed_set_value = value; + schedule_work(&led_cdev->set_brightness_work); +} +EXPORT_SYMBOL_GPL(led_set_brightness_nopm); + +void led_set_brightness_nosleep(struct led_classdev *led_cdev, + enum led_brightness value) +{ + led_cdev->brightness = min(value, led_cdev->max_brightness); + + if (led_cdev->flags & LED_SUSPENDED) + return; + + led_set_brightness_nopm(led_cdev, led_cdev->brightness); +} +EXPORT_SYMBOL_GPL(led_set_brightness_nosleep); + +int led_set_brightness_sync(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (led_cdev->blink_delay_on || led_cdev->blink_delay_off) + return -EBUSY; + + led_cdev->brightness = min(value, led_cdev->max_brightness); + + if (led_cdev->flags & LED_SUSPENDED) + return 0; + + return __led_set_brightness_blocking(led_cdev, led_cdev->brightness); +} +EXPORT_SYMBOL_GPL(led_set_brightness_sync); + +int led_update_brightness(struct led_classdev *led_cdev) +{ + int ret = 0; + + if (led_cdev->brightness_get) { + ret = led_cdev->brightness_get(led_cdev); + if (ret >= 0) { + led_cdev->brightness = ret; + return 0; + } + } + + return ret; +} +EXPORT_SYMBOL_GPL(led_update_brightness); + +u32 *led_get_default_pattern(struct led_classdev *led_cdev, unsigned int *size) +{ + struct fwnode_handle *fwnode = led_cdev->dev->fwnode; + u32 *pattern; + int count; + + count = fwnode_property_count_u32(fwnode, "led-pattern"); + if (count < 0) + return NULL; + + pattern = kcalloc(count, sizeof(*pattern), GFP_KERNEL); + if (!pattern) + return NULL; + + if (fwnode_property_read_u32_array(fwnode, "led-pattern", pattern, count)) { + kfree(pattern); + return NULL; + } + + *size = count; + + return pattern; +} +EXPORT_SYMBOL_GPL(led_get_default_pattern); + +/* Caller must ensure led_cdev->led_access held */ +void led_sysfs_disable(struct led_classdev *led_cdev) +{ + lockdep_assert_held(&led_cdev->led_access); + + led_cdev->flags |= LED_SYSFS_DISABLE; +} +EXPORT_SYMBOL_GPL(led_sysfs_disable); + +/* Caller must ensure led_cdev->led_access held */ +void led_sysfs_enable(struct led_classdev *led_cdev) +{ + lockdep_assert_held(&led_cdev->led_access); + + led_cdev->flags &= ~LED_SYSFS_DISABLE; +} +EXPORT_SYMBOL_GPL(led_sysfs_enable); + +static void led_parse_fwnode_props(struct device *dev, + struct fwnode_handle *fwnode, + struct led_properties *props) +{ + int ret; + + if (!fwnode) + return; + + if (fwnode_property_present(fwnode, "label")) { + ret = fwnode_property_read_string(fwnode, "label", &props->label); + if (ret) + dev_err(dev, "Error parsing 'label' property (%d)\n", ret); + return; + } + + if (fwnode_property_present(fwnode, "color")) { + ret = fwnode_property_read_u32(fwnode, "color", &props->color); + if (ret) + dev_err(dev, "Error parsing 'color' property (%d)\n", ret); + else if (props->color >= LED_COLOR_ID_MAX) + dev_err(dev, "LED color identifier out of range\n"); + else + props->color_present = true; + } + + + if (!fwnode_property_present(fwnode, "function")) + return; + + ret = fwnode_property_read_string(fwnode, "function", &props->function); + if (ret) { + dev_err(dev, + "Error parsing 'function' property (%d)\n", + ret); + } + + if (!fwnode_property_present(fwnode, "function-enumerator")) + return; + + ret = fwnode_property_read_u32(fwnode, "function-enumerator", + &props->func_enum); + if (ret) { + dev_err(dev, + "Error parsing 'function-enumerator' property (%d)\n", + ret); + } else { + props->func_enum_present = true; + } +} + +int led_compose_name(struct device *dev, struct led_init_data *init_data, + char *led_classdev_name) +{ + struct led_properties props = {}; + struct fwnode_handle *fwnode = init_data->fwnode; + const char *devicename = init_data->devicename; + + /* We want to label LEDs that can produce full range of colors + * as RGB, not multicolor */ + BUG_ON(props.color == LED_COLOR_ID_MULTI); + + if (!led_classdev_name) + return -EINVAL; + + led_parse_fwnode_props(dev, fwnode, &props); + + if (props.label) { + /* + * If init_data.devicename is NULL, then it indicates that + * DT label should be used as-is for LED class device name. + * Otherwise the label is prepended with devicename to compose + * the final LED class device name. + */ + if (!devicename) { + strscpy(led_classdev_name, props.label, + LED_MAX_NAME_SIZE); + } else { + snprintf(led_classdev_name, LED_MAX_NAME_SIZE, "%s:%s", + devicename, props.label); + } + } else if (props.function || props.color_present) { + char tmp_buf[LED_MAX_NAME_SIZE]; + + if (props.func_enum_present) { + snprintf(tmp_buf, LED_MAX_NAME_SIZE, "%s:%s-%d", + props.color_present ? led_colors[props.color] : "", + props.function ?: "", props.func_enum); + } else { + snprintf(tmp_buf, LED_MAX_NAME_SIZE, "%s:%s", + props.color_present ? led_colors[props.color] : "", + props.function ?: ""); + } + if (init_data->devname_mandatory) { + snprintf(led_classdev_name, LED_MAX_NAME_SIZE, "%s:%s", + devicename, tmp_buf); + } else { + strscpy(led_classdev_name, tmp_buf, LED_MAX_NAME_SIZE); + + } + } else if (init_data->default_label) { + if (!devicename) { + dev_err(dev, "Legacy LED naming requires devicename segment"); + return -EINVAL; + } + snprintf(led_classdev_name, LED_MAX_NAME_SIZE, "%s:%s", + devicename, init_data->default_label); + } else if (is_of_node(fwnode)) { + strscpy(led_classdev_name, to_of_node(fwnode)->name, + LED_MAX_NAME_SIZE); + } else + return -EINVAL; + + return 0; +} +EXPORT_SYMBOL_GPL(led_compose_name); diff --git a/drivers/leds/led-triggers.c b/drivers/leds/led-triggers.c new file mode 100644 index 000000000..4e7b78a84 --- /dev/null +++ b/drivers/leds/led-triggers.c @@ -0,0 +1,463 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED Triggers Core + * + * Copyright 2005-2007 Openedhand Ltd. + * + * Author: Richard Purdie <rpurdie@openedhand.com> + */ + +#include <linux/export.h> +#include <linux/kernel.h> +#include <linux/list.h> +#include <linux/spinlock.h> +#include <linux/device.h> +#include <linux/timer.h> +#include <linux/rwsem.h> +#include <linux/leds.h> +#include <linux/slab.h> +#include <linux/mm.h> +#include "leds.h" + +/* + * Nests outside led_cdev->trigger_lock + */ +static DECLARE_RWSEM(triggers_list_lock); +LIST_HEAD(trigger_list); + + /* Used by LED Class */ + +static inline bool +trigger_relevant(struct led_classdev *led_cdev, struct led_trigger *trig) +{ + return !trig->trigger_type || trig->trigger_type == led_cdev->trigger_type; +} + +ssize_t led_trigger_write(struct file *filp, struct kobject *kobj, + struct bin_attribute *bin_attr, char *buf, + loff_t pos, size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct led_trigger *trig; + int ret = count; + + mutex_lock(&led_cdev->led_access); + + if (led_sysfs_is_disabled(led_cdev)) { + ret = -EBUSY; + goto unlock; + } + + if (sysfs_streq(buf, "none")) { + led_trigger_remove(led_cdev); + goto unlock; + } + + down_read(&triggers_list_lock); + list_for_each_entry(trig, &trigger_list, next_trig) { + if (sysfs_streq(buf, trig->name) && trigger_relevant(led_cdev, trig)) { + down_write(&led_cdev->trigger_lock); + led_trigger_set(led_cdev, trig); + up_write(&led_cdev->trigger_lock); + + up_read(&triggers_list_lock); + goto unlock; + } + } + /* we come here only if buf matches no trigger */ + ret = -EINVAL; + up_read(&triggers_list_lock); + +unlock: + mutex_unlock(&led_cdev->led_access); + return ret; +} +EXPORT_SYMBOL_GPL(led_trigger_write); + +__printf(3, 4) +static int led_trigger_snprintf(char *buf, ssize_t size, const char *fmt, ...) +{ + va_list args; + int i; + + va_start(args, fmt); + if (size <= 0) + i = vsnprintf(NULL, 0, fmt, args); + else + i = vscnprintf(buf, size, fmt, args); + va_end(args); + + return i; +} + +static int led_trigger_format(char *buf, size_t size, + struct led_classdev *led_cdev) +{ + struct led_trigger *trig; + int len = led_trigger_snprintf(buf, size, "%s", + led_cdev->trigger ? "none" : "[none]"); + + list_for_each_entry(trig, &trigger_list, next_trig) { + bool hit; + + if (!trigger_relevant(led_cdev, trig)) + continue; + + hit = led_cdev->trigger && !strcmp(led_cdev->trigger->name, trig->name); + + len += led_trigger_snprintf(buf + len, size - len, + " %s%s%s", hit ? "[" : "", + trig->name, hit ? "]" : ""); + } + + len += led_trigger_snprintf(buf + len, size - len, "\n"); + + return len; +} + +/* + * It was stupid to create 10000 cpu triggers, but we are stuck with it now. + * Don't make that mistake again. We work around it here by creating binary + * attribute, which is not limited by length. This is _not_ good design, do not + * copy it. + */ +ssize_t led_trigger_read(struct file *filp, struct kobject *kobj, + struct bin_attribute *attr, char *buf, + loff_t pos, size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct led_classdev *led_cdev = dev_get_drvdata(dev); + void *data; + int len; + + down_read(&triggers_list_lock); + down_read(&led_cdev->trigger_lock); + + len = led_trigger_format(NULL, 0, led_cdev); + data = kvmalloc(len + 1, GFP_KERNEL); + if (!data) { + up_read(&led_cdev->trigger_lock); + up_read(&triggers_list_lock); + return -ENOMEM; + } + len = led_trigger_format(data, len + 1, led_cdev); + + up_read(&led_cdev->trigger_lock); + up_read(&triggers_list_lock); + + len = memory_read_from_buffer(buf, count, &pos, data, len); + + kvfree(data); + + return len; +} +EXPORT_SYMBOL_GPL(led_trigger_read); + +/* Caller must ensure led_cdev->trigger_lock held */ +int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig) +{ + unsigned long flags; + char *event = NULL; + char *envp[2]; + const char *name; + int ret; + + if (!led_cdev->trigger && !trig) + return 0; + + name = trig ? trig->name : "none"; + event = kasprintf(GFP_KERNEL, "TRIGGER=%s", name); + + /* Remove any existing trigger */ + if (led_cdev->trigger) { + write_lock_irqsave(&led_cdev->trigger->leddev_list_lock, flags); + list_del(&led_cdev->trig_list); + write_unlock_irqrestore(&led_cdev->trigger->leddev_list_lock, + flags); + cancel_work_sync(&led_cdev->set_brightness_work); + led_stop_software_blink(led_cdev); + if (led_cdev->trigger->deactivate) + led_cdev->trigger->deactivate(led_cdev); + device_remove_groups(led_cdev->dev, led_cdev->trigger->groups); + led_cdev->trigger = NULL; + led_cdev->trigger_data = NULL; + led_cdev->activated = false; + led_set_brightness(led_cdev, LED_OFF); + } + if (trig) { + write_lock_irqsave(&trig->leddev_list_lock, flags); + list_add_tail(&led_cdev->trig_list, &trig->led_cdevs); + write_unlock_irqrestore(&trig->leddev_list_lock, flags); + led_cdev->trigger = trig; + + if (trig->activate) + ret = trig->activate(led_cdev); + else + ret = 0; + + if (ret) + goto err_activate; + + ret = device_add_groups(led_cdev->dev, trig->groups); + if (ret) { + dev_err(led_cdev->dev, "Failed to add trigger attributes\n"); + goto err_add_groups; + } + } + + if (event) { + envp[0] = event; + envp[1] = NULL; + if (kobject_uevent_env(&led_cdev->dev->kobj, KOBJ_CHANGE, envp)) + dev_err(led_cdev->dev, + "%s: Error sending uevent\n", __func__); + kfree(event); + } + + return 0; + +err_add_groups: + + if (trig->deactivate) + trig->deactivate(led_cdev); +err_activate: + + write_lock_irqsave(&led_cdev->trigger->leddev_list_lock, flags); + list_del(&led_cdev->trig_list); + write_unlock_irqrestore(&led_cdev->trigger->leddev_list_lock, flags); + led_cdev->trigger = NULL; + led_cdev->trigger_data = NULL; + led_set_brightness(led_cdev, LED_OFF); + kfree(event); + + return ret; +} +EXPORT_SYMBOL_GPL(led_trigger_set); + +void led_trigger_remove(struct led_classdev *led_cdev) +{ + down_write(&led_cdev->trigger_lock); + led_trigger_set(led_cdev, NULL); + up_write(&led_cdev->trigger_lock); +} +EXPORT_SYMBOL_GPL(led_trigger_remove); + +void led_trigger_set_default(struct led_classdev *led_cdev) +{ + struct led_trigger *trig; + + if (!led_cdev->default_trigger) + return; + + down_read(&triggers_list_lock); + down_write(&led_cdev->trigger_lock); + list_for_each_entry(trig, &trigger_list, next_trig) { + if (!strcmp(led_cdev->default_trigger, trig->name) && + trigger_relevant(led_cdev, trig)) { + led_cdev->flags |= LED_INIT_DEFAULT_TRIGGER; + led_trigger_set(led_cdev, trig); + break; + } + } + up_write(&led_cdev->trigger_lock); + up_read(&triggers_list_lock); +} +EXPORT_SYMBOL_GPL(led_trigger_set_default); + +void led_trigger_rename_static(const char *name, struct led_trigger *trig) +{ + /* new name must be on a temporary string to prevent races */ + BUG_ON(name == trig->name); + + down_write(&triggers_list_lock); + /* this assumes that trig->name was originaly allocated to + * non constant storage */ + strcpy((char *)trig->name, name); + up_write(&triggers_list_lock); +} +EXPORT_SYMBOL_GPL(led_trigger_rename_static); + +/* LED Trigger Interface */ + +int led_trigger_register(struct led_trigger *trig) +{ + struct led_classdev *led_cdev; + struct led_trigger *_trig; + + rwlock_init(&trig->leddev_list_lock); + INIT_LIST_HEAD(&trig->led_cdevs); + + down_write(&triggers_list_lock); + /* Make sure the trigger's name isn't already in use */ + list_for_each_entry(_trig, &trigger_list, next_trig) { + if (!strcmp(_trig->name, trig->name) && + (trig->trigger_type == _trig->trigger_type || + !trig->trigger_type || !_trig->trigger_type)) { + up_write(&triggers_list_lock); + return -EEXIST; + } + } + /* Add to the list of led triggers */ + list_add_tail(&trig->next_trig, &trigger_list); + up_write(&triggers_list_lock); + + /* Register with any LEDs that have this as a default trigger */ + down_read(&leds_list_lock); + list_for_each_entry(led_cdev, &leds_list, node) { + down_write(&led_cdev->trigger_lock); + if (!led_cdev->trigger && led_cdev->default_trigger && + !strcmp(led_cdev->default_trigger, trig->name) && + trigger_relevant(led_cdev, trig)) { + led_cdev->flags |= LED_INIT_DEFAULT_TRIGGER; + led_trigger_set(led_cdev, trig); + } + up_write(&led_cdev->trigger_lock); + } + up_read(&leds_list_lock); + + return 0; +} +EXPORT_SYMBOL_GPL(led_trigger_register); + +void led_trigger_unregister(struct led_trigger *trig) +{ + struct led_classdev *led_cdev; + + if (list_empty_careful(&trig->next_trig)) + return; + + /* Remove from the list of led triggers */ + down_write(&triggers_list_lock); + list_del_init(&trig->next_trig); + up_write(&triggers_list_lock); + + /* Remove anyone actively using this trigger */ + down_read(&leds_list_lock); + list_for_each_entry(led_cdev, &leds_list, node) { + down_write(&led_cdev->trigger_lock); + if (led_cdev->trigger == trig) + led_trigger_set(led_cdev, NULL); + up_write(&led_cdev->trigger_lock); + } + up_read(&leds_list_lock); +} +EXPORT_SYMBOL_GPL(led_trigger_unregister); + +static void devm_led_trigger_release(struct device *dev, void *res) +{ + led_trigger_unregister(*(struct led_trigger **)res); +} + +int devm_led_trigger_register(struct device *dev, + struct led_trigger *trig) +{ + struct led_trigger **dr; + int rc; + + dr = devres_alloc(devm_led_trigger_release, sizeof(*dr), + GFP_KERNEL); + if (!dr) + return -ENOMEM; + + *dr = trig; + + rc = led_trigger_register(trig); + if (rc) + devres_free(dr); + else + devres_add(dev, dr); + + return rc; +} +EXPORT_SYMBOL_GPL(devm_led_trigger_register); + +/* Simple LED Trigger Interface */ + +void led_trigger_event(struct led_trigger *trig, + enum led_brightness brightness) +{ + struct led_classdev *led_cdev; + unsigned long flags; + + if (!trig) + return; + + read_lock_irqsave(&trig->leddev_list_lock, flags); + list_for_each_entry(led_cdev, &trig->led_cdevs, trig_list) + led_set_brightness(led_cdev, brightness); + read_unlock_irqrestore(&trig->leddev_list_lock, flags); +} +EXPORT_SYMBOL_GPL(led_trigger_event); + +static void led_trigger_blink_setup(struct led_trigger *trig, + unsigned long *delay_on, + unsigned long *delay_off, + int oneshot, + int invert) +{ + struct led_classdev *led_cdev; + unsigned long flags; + + if (!trig) + return; + + read_lock_irqsave(&trig->leddev_list_lock, flags); + list_for_each_entry(led_cdev, &trig->led_cdevs, trig_list) { + if (oneshot) + led_blink_set_oneshot(led_cdev, delay_on, delay_off, + invert); + else + led_blink_set(led_cdev, delay_on, delay_off); + } + read_unlock_irqrestore(&trig->leddev_list_lock, flags); +} + +void led_trigger_blink(struct led_trigger *trig, + unsigned long *delay_on, + unsigned long *delay_off) +{ + led_trigger_blink_setup(trig, delay_on, delay_off, 0, 0); +} +EXPORT_SYMBOL_GPL(led_trigger_blink); + +void led_trigger_blink_oneshot(struct led_trigger *trig, + unsigned long *delay_on, + unsigned long *delay_off, + int invert) +{ + led_trigger_blink_setup(trig, delay_on, delay_off, 1, invert); +} +EXPORT_SYMBOL_GPL(led_trigger_blink_oneshot); + +void led_trigger_register_simple(const char *name, struct led_trigger **tp) +{ + struct led_trigger *trig; + int err; + + trig = kzalloc(sizeof(struct led_trigger), GFP_KERNEL); + + if (trig) { + trig->name = name; + err = led_trigger_register(trig); + if (err < 0) { + kfree(trig); + trig = NULL; + pr_warn("LED trigger %s failed to register (%d)\n", + name, err); + } + } else { + pr_warn("LED trigger %s failed to register (no memory)\n", + name); + } + *tp = trig; +} +EXPORT_SYMBOL_GPL(led_trigger_register_simple); + +void led_trigger_unregister_simple(struct led_trigger *trig) +{ + if (trig) + led_trigger_unregister(trig); + kfree(trig); +} +EXPORT_SYMBOL_GPL(led_trigger_unregister_simple); diff --git a/drivers/leds/leds-88pm860x.c b/drivers/leds/leds-88pm860x.c new file mode 100644 index 000000000..508d0d859 --- /dev/null +++ b/drivers/leds/leds-88pm860x.c @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED driver for Marvell 88PM860x + * + * Copyright (C) 2009 Marvell International Ltd. + * Haojian Zhuang <haojian.zhuang@marvell.com> + */ + +#include <linux/kernel.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/slab.h> +#include <linux/mfd/88pm860x.h> +#include <linux/module.h> + +#define LED_PWM_MASK (0x1F) +#define LED_CURRENT_MASK (0x07 << 5) + +#define LED_BLINK_MASK (0x7F) + +#define LED_ON_CONTINUOUS (0x0F << 3) + +#define LED1_BLINK_EN (1 << 1) +#define LED2_BLINK_EN (1 << 2) + +struct pm860x_led { + struct led_classdev cdev; + struct i2c_client *i2c; + struct pm860x_chip *chip; + struct mutex lock; + char name[MFD_NAME_SIZE]; + + int port; + int iset; + unsigned char brightness; + unsigned char current_brightness; + + int reg_control; + int reg_blink; + int blink_mask; +}; + +static int led_power_set(struct pm860x_chip *chip, int port, int on) +{ + int ret = -EINVAL; + + switch (port) { + case 0: + case 1: + case 2: + ret = on ? pm8606_osc_enable(chip, RGB1_ENABLE) : + pm8606_osc_disable(chip, RGB1_ENABLE); + break; + case 3: + case 4: + case 5: + ret = on ? pm8606_osc_enable(chip, RGB2_ENABLE) : + pm8606_osc_disable(chip, RGB2_ENABLE); + break; + } + return ret; +} + +static int pm860x_led_set(struct led_classdev *cdev, + enum led_brightness value) +{ + struct pm860x_led *led = container_of(cdev, struct pm860x_led, cdev); + struct pm860x_chip *chip; + unsigned char buf[3]; + int ret; + + chip = led->chip; + mutex_lock(&led->lock); + led->brightness = value >> 3; + + if ((led->current_brightness == 0) && led->brightness) { + led_power_set(chip, led->port, 1); + if (led->iset) { + pm860x_set_bits(led->i2c, led->reg_control, + LED_CURRENT_MASK, led->iset); + } + pm860x_set_bits(led->i2c, led->reg_blink, + LED_BLINK_MASK, LED_ON_CONTINUOUS); + pm860x_set_bits(led->i2c, PM8606_WLED3B, led->blink_mask, + led->blink_mask); + } + pm860x_set_bits(led->i2c, led->reg_control, LED_PWM_MASK, + led->brightness); + + if (led->brightness == 0) { + pm860x_bulk_read(led->i2c, led->reg_control, 3, buf); + ret = buf[0] & LED_PWM_MASK; + ret |= buf[1] & LED_PWM_MASK; + ret |= buf[2] & LED_PWM_MASK; + if (ret == 0) { + /* unset current since no led is lighting */ + pm860x_set_bits(led->i2c, led->reg_control, + LED_CURRENT_MASK, 0); + pm860x_set_bits(led->i2c, PM8606_WLED3B, + led->blink_mask, 0); + led_power_set(chip, led->port, 0); + } + } + led->current_brightness = led->brightness; + dev_dbg(chip->dev, "Update LED. (reg:%d, brightness:%d)\n", + led->reg_control, led->brightness); + mutex_unlock(&led->lock); + + return 0; +} + +#ifdef CONFIG_OF +static int pm860x_led_dt_init(struct platform_device *pdev, + struct pm860x_led *data) +{ + struct device_node *nproot, *np; + int iset = 0; + + if (!dev_of_node(pdev->dev.parent)) + return -ENODEV; + nproot = of_get_child_by_name(dev_of_node(pdev->dev.parent), "leds"); + if (!nproot) { + dev_err(&pdev->dev, "failed to find leds node\n"); + return -ENODEV; + } + for_each_available_child_of_node(nproot, np) { + if (of_node_name_eq(np, data->name)) { + of_property_read_u32(np, "marvell,88pm860x-iset", + &iset); + data->iset = PM8606_LED_CURRENT(iset); + of_node_put(np); + break; + } + } + of_node_put(nproot); + return 0; +} +#else +#define pm860x_led_dt_init(x, y) (-1) +#endif + +static int pm860x_led_probe(struct platform_device *pdev) +{ + struct pm860x_chip *chip = dev_get_drvdata(pdev->dev.parent); + struct pm860x_led_pdata *pdata = dev_get_platdata(&pdev->dev); + struct pm860x_led *data; + struct resource *res; + int ret = 0; + + data = devm_kzalloc(&pdev->dev, sizeof(struct pm860x_led), GFP_KERNEL); + if (data == NULL) + return -ENOMEM; + res = platform_get_resource_byname(pdev, IORESOURCE_REG, "control"); + if (!res) { + dev_err(&pdev->dev, "No REG resource for control\n"); + return -ENXIO; + } + data->reg_control = res->start; + res = platform_get_resource_byname(pdev, IORESOURCE_REG, "blink"); + if (!res) { + dev_err(&pdev->dev, "No REG resource for blink\n"); + return -ENXIO; + } + data->reg_blink = res->start; + memset(data->name, 0, MFD_NAME_SIZE); + switch (pdev->id) { + case 0: + data->blink_mask = LED1_BLINK_EN; + sprintf(data->name, "led0-red"); + break; + case 1: + data->blink_mask = LED1_BLINK_EN; + sprintf(data->name, "led0-green"); + break; + case 2: + data->blink_mask = LED1_BLINK_EN; + sprintf(data->name, "led0-blue"); + break; + case 3: + data->blink_mask = LED2_BLINK_EN; + sprintf(data->name, "led1-red"); + break; + case 4: + data->blink_mask = LED2_BLINK_EN; + sprintf(data->name, "led1-green"); + break; + case 5: + data->blink_mask = LED2_BLINK_EN; + sprintf(data->name, "led1-blue"); + break; + } + data->chip = chip; + data->i2c = (chip->id == CHIP_PM8606) ? chip->client : chip->companion; + data->port = pdev->id; + if (pm860x_led_dt_init(pdev, data)) + if (pdata) + data->iset = pdata->iset; + + data->current_brightness = 0; + data->cdev.name = data->name; + data->cdev.brightness_set_blocking = pm860x_led_set; + mutex_init(&data->lock); + + ret = led_classdev_register(chip->dev, &data->cdev); + if (ret < 0) { + dev_err(&pdev->dev, "Failed to register LED: %d\n", ret); + return ret; + } + pm860x_led_set(&data->cdev, 0); + + platform_set_drvdata(pdev, data); + + return 0; +} + +static int pm860x_led_remove(struct platform_device *pdev) +{ + struct pm860x_led *data = platform_get_drvdata(pdev); + + led_classdev_unregister(&data->cdev); + + return 0; +} + +static struct platform_driver pm860x_led_driver = { + .driver = { + .name = "88pm860x-led", + }, + .probe = pm860x_led_probe, + .remove = pm860x_led_remove, +}; + +module_platform_driver(pm860x_led_driver); + +MODULE_DESCRIPTION("LED driver for Marvell PM860x"); +MODULE_AUTHOR("Haojian Zhuang <haojian.zhuang@marvell.com>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:88pm860x-led"); diff --git a/drivers/leds/leds-aat1290.c b/drivers/leds/leds-aat1290.c new file mode 100644 index 000000000..589484b22 --- /dev/null +++ b/drivers/leds/leds-aat1290.c @@ -0,0 +1,556 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED Flash class driver for the AAT1290 + * 1.5A Step-Up Current Regulator for Flash LEDs + * + * Copyright (C) 2015, Samsung Electronics Co., Ltd. + * Author: Jacek Anaszewski <j.anaszewski@samsung.com> + */ + +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/led-class-flash.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/pinctrl/consumer.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <media/v4l2-flash-led-class.h> + +#define AAT1290_MOVIE_MODE_CURRENT_ADDR 17 +#define AAT1290_MAX_MM_CURR_PERCENT_0 16 +#define AAT1290_MAX_MM_CURR_PERCENT_100 1 + +#define AAT1290_FLASH_SAFETY_TIMER_ADDR 18 + +#define AAT1290_MOVIE_MODE_CONFIG_ADDR 19 +#define AAT1290_MOVIE_MODE_OFF 1 +#define AAT1290_MOVIE_MODE_ON 3 + +#define AAT1290_MM_CURRENT_RATIO_ADDR 20 +#define AAT1290_MM_TO_FL_1_92 1 + +#define AAT1290_MM_TO_FL_RATIO 1000 / 1920 +#define AAT1290_MAX_MM_CURRENT(fl_max) (fl_max * AAT1290_MM_TO_FL_RATIO) + +#define AAT1290_LATCH_TIME_MIN_US 500 +#define AAT1290_LATCH_TIME_MAX_US 1000 +#define AAT1290_EN_SET_TICK_TIME_US 1 +#define AAT1290_FLEN_OFF_DELAY_TIME_US 10 +#define AAT1290_FLASH_TM_NUM_LEVELS 16 +#define AAT1290_MM_CURRENT_SCALE_SIZE 15 + +#define AAT1290_NAME "aat1290" + + +struct aat1290_led_config_data { + /* maximum LED current in movie mode */ + u32 max_mm_current; + /* maximum LED current in flash mode */ + u32 max_flash_current; + /* maximum flash timeout */ + u32 max_flash_tm; + /* external strobe capability */ + bool has_external_strobe; + /* max LED brightness level */ + enum led_brightness max_brightness; +}; + +struct aat1290_led { + /* platform device data */ + struct platform_device *pdev; + /* secures access to the device */ + struct mutex lock; + + /* corresponding LED Flash class device */ + struct led_classdev_flash fled_cdev; + /* V4L2 Flash device */ + struct v4l2_flash *v4l2_flash; + + /* FLEN pin */ + struct gpio_desc *gpio_fl_en; + /* EN|SET pin */ + struct gpio_desc *gpio_en_set; + /* movie mode current scale */ + int *mm_current_scale; + /* device mode */ + bool movie_mode; + /* brightness cache */ + unsigned int torch_brightness; +}; + +static struct aat1290_led *fled_cdev_to_led( + struct led_classdev_flash *fled_cdev) +{ + return container_of(fled_cdev, struct aat1290_led, fled_cdev); +} + +static struct led_classdev_flash *led_cdev_to_fled_cdev( + struct led_classdev *led_cdev) +{ + return container_of(led_cdev, struct led_classdev_flash, led_cdev); +} + +static void aat1290_as2cwire_write(struct aat1290_led *led, int addr, int value) +{ + int i; + + gpiod_direction_output(led->gpio_fl_en, 0); + gpiod_direction_output(led->gpio_en_set, 0); + + udelay(AAT1290_FLEN_OFF_DELAY_TIME_US); + + /* write address */ + for (i = 0; i < addr; ++i) { + udelay(AAT1290_EN_SET_TICK_TIME_US); + gpiod_direction_output(led->gpio_en_set, 0); + udelay(AAT1290_EN_SET_TICK_TIME_US); + gpiod_direction_output(led->gpio_en_set, 1); + } + + usleep_range(AAT1290_LATCH_TIME_MIN_US, AAT1290_LATCH_TIME_MAX_US); + + /* write data */ + for (i = 0; i < value; ++i) { + udelay(AAT1290_EN_SET_TICK_TIME_US); + gpiod_direction_output(led->gpio_en_set, 0); + udelay(AAT1290_EN_SET_TICK_TIME_US); + gpiod_direction_output(led->gpio_en_set, 1); + } + + usleep_range(AAT1290_LATCH_TIME_MIN_US, AAT1290_LATCH_TIME_MAX_US); +} + +static void aat1290_set_flash_safety_timer(struct aat1290_led *led, + unsigned int micro_sec) +{ + struct led_classdev_flash *fled_cdev = &led->fled_cdev; + struct led_flash_setting *flash_tm = &fled_cdev->timeout; + int flash_tm_reg = AAT1290_FLASH_TM_NUM_LEVELS - + (micro_sec / flash_tm->step) + 1; + + aat1290_as2cwire_write(led, AAT1290_FLASH_SAFETY_TIMER_ADDR, + flash_tm_reg); +} + +/* LED subsystem callbacks */ + +static int aat1290_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct led_classdev_flash *fled_cdev = led_cdev_to_fled_cdev(led_cdev); + struct aat1290_led *led = fled_cdev_to_led(fled_cdev); + + mutex_lock(&led->lock); + + if (brightness == 0) { + gpiod_direction_output(led->gpio_fl_en, 0); + gpiod_direction_output(led->gpio_en_set, 0); + led->movie_mode = false; + } else { + if (!led->movie_mode) { + aat1290_as2cwire_write(led, + AAT1290_MM_CURRENT_RATIO_ADDR, + AAT1290_MM_TO_FL_1_92); + led->movie_mode = true; + } + + aat1290_as2cwire_write(led, AAT1290_MOVIE_MODE_CURRENT_ADDR, + AAT1290_MAX_MM_CURR_PERCENT_0 - brightness); + aat1290_as2cwire_write(led, AAT1290_MOVIE_MODE_CONFIG_ADDR, + AAT1290_MOVIE_MODE_ON); + } + + mutex_unlock(&led->lock); + + return 0; +} + +static int aat1290_led_flash_strobe_set(struct led_classdev_flash *fled_cdev, + bool state) + +{ + struct aat1290_led *led = fled_cdev_to_led(fled_cdev); + struct led_classdev *led_cdev = &fled_cdev->led_cdev; + struct led_flash_setting *timeout = &fled_cdev->timeout; + + mutex_lock(&led->lock); + + if (state) { + aat1290_set_flash_safety_timer(led, timeout->val); + gpiod_direction_output(led->gpio_fl_en, 1); + } else { + gpiod_direction_output(led->gpio_fl_en, 0); + gpiod_direction_output(led->gpio_en_set, 0); + } + + /* + * To reenter movie mode after a flash event the part must be cycled + * off and back on to reset the movie mode and reprogrammed via the + * AS2Cwire. Therefore the brightness and movie_mode properties needs + * to be updated here to reflect the actual state. + */ + led_cdev->brightness = 0; + led->movie_mode = false; + + mutex_unlock(&led->lock); + + return 0; +} + +static int aat1290_led_flash_timeout_set(struct led_classdev_flash *fled_cdev, + u32 timeout) +{ + /* + * Don't do anything - flash timeout is cached in the led-class-flash + * core and will be applied in the strobe_set op, as writing the + * safety timer register spuriously turns the torch mode on. + */ + + return 0; +} + +static int aat1290_led_parse_dt(struct aat1290_led *led, + struct aat1290_led_config_data *cfg, + struct device_node **sub_node) +{ + struct device *dev = &led->pdev->dev; + struct device_node *child_node; +#if IS_ENABLED(CONFIG_V4L2_FLASH_LED_CLASS) + struct pinctrl *pinctrl; +#endif + int ret = 0; + + led->gpio_fl_en = devm_gpiod_get(dev, "flen", GPIOD_ASIS); + if (IS_ERR(led->gpio_fl_en)) { + ret = PTR_ERR(led->gpio_fl_en); + dev_err(dev, "Unable to claim gpio \"flen\".\n"); + return ret; + } + + led->gpio_en_set = devm_gpiod_get(dev, "enset", GPIOD_ASIS); + if (IS_ERR(led->gpio_en_set)) { + ret = PTR_ERR(led->gpio_en_set); + dev_err(dev, "Unable to claim gpio \"enset\".\n"); + return ret; + } + +#if IS_ENABLED(CONFIG_V4L2_FLASH_LED_CLASS) + pinctrl = devm_pinctrl_get_select_default(&led->pdev->dev); + if (IS_ERR(pinctrl)) { + cfg->has_external_strobe = false; + dev_info(dev, + "No support for external strobe detected.\n"); + } else { + cfg->has_external_strobe = true; + } +#endif + + child_node = of_get_next_available_child(dev_of_node(dev), NULL); + if (!child_node) { + dev_err(dev, "No DT child node found for connected LED.\n"); + return -EINVAL; + } + + ret = of_property_read_u32(child_node, "led-max-microamp", + &cfg->max_mm_current); + /* + * led-max-microamp will default to 1/20 of flash-max-microamp + * in case it is missing. + */ + if (ret < 0) + dev_warn(dev, + "led-max-microamp DT property missing\n"); + + ret = of_property_read_u32(child_node, "flash-max-microamp", + &cfg->max_flash_current); + if (ret < 0) { + dev_err(dev, + "flash-max-microamp DT property missing\n"); + goto err_parse_dt; + } + + ret = of_property_read_u32(child_node, "flash-max-timeout-us", + &cfg->max_flash_tm); + if (ret < 0) { + dev_err(dev, + "flash-max-timeout-us DT property missing\n"); + goto err_parse_dt; + } + + *sub_node = child_node; + +err_parse_dt: + of_node_put(child_node); + + return ret; +} + +static void aat1290_led_validate_mm_current(struct aat1290_led *led, + struct aat1290_led_config_data *cfg) +{ + int i, b = 0, e = AAT1290_MM_CURRENT_SCALE_SIZE; + + while (e - b > 1) { + i = b + (e - b) / 2; + if (cfg->max_mm_current < led->mm_current_scale[i]) + e = i; + else + b = i; + } + + cfg->max_mm_current = led->mm_current_scale[b]; + cfg->max_brightness = b + 1; +} + +static int init_mm_current_scale(struct aat1290_led *led, + struct aat1290_led_config_data *cfg) +{ + static const int max_mm_current_percent[] = { + 20, 22, 25, 28, 32, 36, 40, 45, 50, 56, + 63, 71, 79, 89, 100 + }; + int i, max_mm_current = + AAT1290_MAX_MM_CURRENT(cfg->max_flash_current); + + led->mm_current_scale = devm_kzalloc(&led->pdev->dev, + sizeof(max_mm_current_percent), + GFP_KERNEL); + if (!led->mm_current_scale) + return -ENOMEM; + + for (i = 0; i < AAT1290_MM_CURRENT_SCALE_SIZE; ++i) + led->mm_current_scale[i] = max_mm_current * + max_mm_current_percent[i] / 100; + + return 0; +} + +static int aat1290_led_get_configuration(struct aat1290_led *led, + struct aat1290_led_config_data *cfg, + struct device_node **sub_node) +{ + int ret; + + ret = aat1290_led_parse_dt(led, cfg, sub_node); + if (ret < 0) + return ret; + /* + * Init non-linear movie mode current scale basing + * on the max flash current from led configuration. + */ + ret = init_mm_current_scale(led, cfg); + if (ret < 0) + return ret; + + aat1290_led_validate_mm_current(led, cfg); + +#if IS_ENABLED(CONFIG_V4L2_FLASH_LED_CLASS) +#else + devm_kfree(&led->pdev->dev, led->mm_current_scale); +#endif + + return 0; +} + +static void aat1290_init_flash_timeout(struct aat1290_led *led, + struct aat1290_led_config_data *cfg) +{ + struct led_classdev_flash *fled_cdev = &led->fled_cdev; + struct led_flash_setting *setting; + + /* Init flash timeout setting */ + setting = &fled_cdev->timeout; + setting->min = cfg->max_flash_tm / AAT1290_FLASH_TM_NUM_LEVELS; + setting->max = cfg->max_flash_tm; + setting->step = setting->min; + setting->val = setting->max; +} + +#if IS_ENABLED(CONFIG_V4L2_FLASH_LED_CLASS) +static enum led_brightness aat1290_intensity_to_brightness( + struct v4l2_flash *v4l2_flash, + s32 intensity) +{ + struct led_classdev_flash *fled_cdev = v4l2_flash->fled_cdev; + struct aat1290_led *led = fled_cdev_to_led(fled_cdev); + int i; + + for (i = AAT1290_MM_CURRENT_SCALE_SIZE - 1; i >= 0; --i) + if (intensity >= led->mm_current_scale[i]) + return i + 1; + + return 1; +} + +static s32 aat1290_brightness_to_intensity(struct v4l2_flash *v4l2_flash, + enum led_brightness brightness) +{ + struct led_classdev_flash *fled_cdev = v4l2_flash->fled_cdev; + struct aat1290_led *led = fled_cdev_to_led(fled_cdev); + + return led->mm_current_scale[brightness - 1]; +} + +static int aat1290_led_external_strobe_set(struct v4l2_flash *v4l2_flash, + bool enable) +{ + struct aat1290_led *led = fled_cdev_to_led(v4l2_flash->fled_cdev); + struct led_classdev_flash *fled_cdev = v4l2_flash->fled_cdev; + struct led_classdev *led_cdev = &fled_cdev->led_cdev; + struct pinctrl *pinctrl; + + gpiod_direction_output(led->gpio_fl_en, 0); + gpiod_direction_output(led->gpio_en_set, 0); + + led->movie_mode = false; + led_cdev->brightness = 0; + + pinctrl = devm_pinctrl_get_select(&led->pdev->dev, + enable ? "isp" : "host"); + if (IS_ERR(pinctrl)) { + dev_warn(&led->pdev->dev, "Unable to switch strobe source.\n"); + return PTR_ERR(pinctrl); + } + + return 0; +} + +static void aat1290_init_v4l2_flash_config(struct aat1290_led *led, + struct aat1290_led_config_data *led_cfg, + struct v4l2_flash_config *v4l2_sd_cfg) +{ + struct led_classdev *led_cdev = &led->fled_cdev.led_cdev; + struct led_flash_setting *s; + + strlcpy(v4l2_sd_cfg->dev_name, led_cdev->dev->kobj.name, + sizeof(v4l2_sd_cfg->dev_name)); + + s = &v4l2_sd_cfg->intensity; + s->min = led->mm_current_scale[0]; + s->max = led_cfg->max_mm_current; + s->step = 1; + s->val = s->max; + + v4l2_sd_cfg->has_external_strobe = led_cfg->has_external_strobe; +} + +static const struct v4l2_flash_ops v4l2_flash_ops = { + .external_strobe_set = aat1290_led_external_strobe_set, + .intensity_to_led_brightness = aat1290_intensity_to_brightness, + .led_brightness_to_intensity = aat1290_brightness_to_intensity, +}; +#else +static inline void aat1290_init_v4l2_flash_config(struct aat1290_led *led, + struct aat1290_led_config_data *led_cfg, + struct v4l2_flash_config *v4l2_sd_cfg) +{ +} +static const struct v4l2_flash_ops v4l2_flash_ops; +#endif + +static const struct led_flash_ops flash_ops = { + .strobe_set = aat1290_led_flash_strobe_set, + .timeout_set = aat1290_led_flash_timeout_set, +}; + +static int aat1290_led_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *sub_node = NULL; + struct aat1290_led *led; + struct led_classdev *led_cdev; + struct led_classdev_flash *fled_cdev; + struct led_init_data init_data = {}; + struct aat1290_led_config_data led_cfg = {}; + struct v4l2_flash_config v4l2_sd_cfg = {}; + int ret; + + led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->pdev = pdev; + platform_set_drvdata(pdev, led); + + fled_cdev = &led->fled_cdev; + fled_cdev->ops = &flash_ops; + led_cdev = &fled_cdev->led_cdev; + + ret = aat1290_led_get_configuration(led, &led_cfg, &sub_node); + if (ret < 0) + return ret; + + mutex_init(&led->lock); + + /* Initialize LED Flash class device */ + led_cdev->brightness_set_blocking = aat1290_led_brightness_set; + led_cdev->max_brightness = led_cfg.max_brightness; + led_cdev->flags |= LED_DEV_CAP_FLASH; + + aat1290_init_flash_timeout(led, &led_cfg); + + init_data.fwnode = of_fwnode_handle(sub_node); + init_data.devicename = AAT1290_NAME; + + /* Register LED Flash class device */ + ret = led_classdev_flash_register_ext(&pdev->dev, fled_cdev, + &init_data); + if (ret < 0) + goto err_flash_register; + + aat1290_init_v4l2_flash_config(led, &led_cfg, &v4l2_sd_cfg); + + /* Create V4L2 Flash subdev. */ + led->v4l2_flash = v4l2_flash_init(dev, of_fwnode_handle(sub_node), + fled_cdev, &v4l2_flash_ops, + &v4l2_sd_cfg); + if (IS_ERR(led->v4l2_flash)) { + ret = PTR_ERR(led->v4l2_flash); + goto error_v4l2_flash_init; + } + + return 0; + +error_v4l2_flash_init: + led_classdev_flash_unregister(fled_cdev); +err_flash_register: + mutex_destroy(&led->lock); + + return ret; +} + +static int aat1290_led_remove(struct platform_device *pdev) +{ + struct aat1290_led *led = platform_get_drvdata(pdev); + + v4l2_flash_release(led->v4l2_flash); + led_classdev_flash_unregister(&led->fled_cdev); + + mutex_destroy(&led->lock); + + return 0; +} + +static const struct of_device_id aat1290_led_dt_match[] = { + { .compatible = "skyworks,aat1290" }, + {}, +}; +MODULE_DEVICE_TABLE(of, aat1290_led_dt_match); + +static struct platform_driver aat1290_led_driver = { + .probe = aat1290_led_probe, + .remove = aat1290_led_remove, + .driver = { + .name = "aat1290", + .of_match_table = aat1290_led_dt_match, + }, +}; + +module_platform_driver(aat1290_led_driver); + +MODULE_AUTHOR("Jacek Anaszewski <j.anaszewski@samsung.com>"); +MODULE_DESCRIPTION("Skyworks Current Regulator for Flash LEDs"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-acer-a500.c b/drivers/leds/leds-acer-a500.c new file mode 100644 index 000000000..8cf0b11f4 --- /dev/null +++ b/drivers/leds/leds-acer-a500.c @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-2.0+ + +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> + +#define A500_EC_LED_DELAY_USEC (100 * 1000) + +enum { + REG_RESET_LEDS = 0x40, + REG_POWER_LED_ON = 0x42, + REG_CHARGE_LED_ON = 0x43, + REG_ANDROID_LEDS_OFF = 0x5a, +}; + +struct a500_led { + struct led_classdev cdev; + const struct reg_sequence *enable_seq; + struct a500_led *other; + struct regmap *rmap; +}; + +static const struct reg_sequence a500_ec_leds_reset_seq[] = { + REG_SEQ(REG_RESET_LEDS, 0x0, A500_EC_LED_DELAY_USEC), + REG_SEQ(REG_ANDROID_LEDS_OFF, 0x0, A500_EC_LED_DELAY_USEC), +}; + +static const struct reg_sequence a500_ec_white_led_enable_seq[] = { + REG_SEQ(REG_POWER_LED_ON, 0x0, A500_EC_LED_DELAY_USEC), +}; + +static const struct reg_sequence a500_ec_orange_led_enable_seq[] = { + REG_SEQ(REG_CHARGE_LED_ON, 0x0, A500_EC_LED_DELAY_USEC), +}; + +static int a500_ec_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct a500_led *led = container_of(led_cdev, struct a500_led, cdev); + struct reg_sequence control_seq[2]; + unsigned int num_regs = 1; + + if (value) { + control_seq[0] = led->enable_seq[0]; + } else { + /* + * There is no separate controls which can disable LEDs + * individually, there is only RESET_LEDS command that turns + * off both LEDs. + * + * RESET_LEDS turns off both LEDs, thus restore other LED if + * it's turned ON. + */ + if (led->other->cdev.brightness) + num_regs = 2; + + control_seq[0] = a500_ec_leds_reset_seq[0]; + control_seq[1] = led->other->enable_seq[0]; + } + + return regmap_multi_reg_write(led->rmap, control_seq, num_regs); +} + +static int a500_ec_leds_probe(struct platform_device *pdev) +{ + struct a500_led *white_led, *orange_led; + struct regmap *rmap; + int err; + + rmap = dev_get_regmap(pdev->dev.parent, "KB930"); + if (!rmap) + return -EINVAL; + + /* reset and turn off LEDs */ + regmap_multi_reg_write(rmap, a500_ec_leds_reset_seq, 2); + + white_led = devm_kzalloc(&pdev->dev, sizeof(*white_led), GFP_KERNEL); + if (!white_led) + return -ENOMEM; + + white_led->cdev.name = "power:white"; + white_led->cdev.brightness_set_blocking = a500_ec_led_brightness_set; + white_led->cdev.flags = LED_CORE_SUSPENDRESUME; + white_led->cdev.max_brightness = 1; + white_led->enable_seq = a500_ec_white_led_enable_seq; + white_led->rmap = rmap; + + orange_led = devm_kzalloc(&pdev->dev, sizeof(*orange_led), GFP_KERNEL); + if (!orange_led) + return -ENOMEM; + + orange_led->cdev.name = "power:orange"; + orange_led->cdev.brightness_set_blocking = a500_ec_led_brightness_set; + orange_led->cdev.flags = LED_CORE_SUSPENDRESUME; + orange_led->cdev.max_brightness = 1; + orange_led->enable_seq = a500_ec_orange_led_enable_seq; + orange_led->rmap = rmap; + + white_led->other = orange_led; + orange_led->other = white_led; + + err = devm_led_classdev_register(&pdev->dev, &white_led->cdev); + if (err) { + dev_err(&pdev->dev, "failed to register white LED\n"); + return err; + } + + err = devm_led_classdev_register(&pdev->dev, &orange_led->cdev); + if (err) { + dev_err(&pdev->dev, "failed to register orange LED\n"); + return err; + } + + return 0; +} + +static struct platform_driver a500_ec_leds_driver = { + .driver = { + .name = "acer-a500-iconia-leds", + }, + .probe = a500_ec_leds_probe, +}; +module_platform_driver(a500_ec_leds_driver); + +MODULE_DESCRIPTION("LED driver for Acer Iconia Tab A500 Power Button"); +MODULE_AUTHOR("Dmitry Osipenko <digetx@gmail.com>"); +MODULE_ALIAS("platform:acer-a500-iconia-leds"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-adp5520.c b/drivers/leds/leds-adp5520.c new file mode 100644 index 000000000..5a0cc7af2 --- /dev/null +++ b/drivers/leds/leds-adp5520.c @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * LEDs driver for Analog Devices ADP5520/ADP5501 MFD PMICs + * + * Copyright 2009 Analog Devices Inc. + * + * Loosely derived from leds-da903x: + * Copyright (C) 2008 Compulab, Ltd. + * Mike Rapoport <mike@compulab.co.il> + * + * Copyright (C) 2006-2008 Marvell International Ltd. + * Eric Miao <eric.miao@marvell.com> + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/mfd/adp5520.h> +#include <linux/slab.h> + +struct adp5520_led { + struct led_classdev cdev; + struct device *master; + int id; + int flags; +}; + +static int adp5520_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct adp5520_led *led; + + led = container_of(led_cdev, struct adp5520_led, cdev); + return adp5520_write(led->master, ADP5520_LED1_CURRENT + led->id - 1, + value >> 2); +} + +static int adp5520_led_setup(struct adp5520_led *led) +{ + struct device *dev = led->master; + int flags = led->flags; + int ret = 0; + + switch (led->id) { + case FLAG_ID_ADP5520_LED1_ADP5501_LED0: + ret |= adp5520_set_bits(dev, ADP5520_LED_TIME, + (flags >> ADP5520_FLAG_OFFT_SHIFT) & + ADP5520_FLAG_OFFT_MASK); + ret |= adp5520_set_bits(dev, ADP5520_LED_CONTROL, + ADP5520_LED1_EN); + break; + case FLAG_ID_ADP5520_LED2_ADP5501_LED1: + ret |= adp5520_set_bits(dev, ADP5520_LED_TIME, + ((flags >> ADP5520_FLAG_OFFT_SHIFT) & + ADP5520_FLAG_OFFT_MASK) << 2); + ret |= adp5520_clr_bits(dev, ADP5520_LED_CONTROL, + ADP5520_R3_MODE); + ret |= adp5520_set_bits(dev, ADP5520_LED_CONTROL, + ADP5520_LED2_EN); + break; + case FLAG_ID_ADP5520_LED3_ADP5501_LED2: + ret |= adp5520_set_bits(dev, ADP5520_LED_TIME, + ((flags >> ADP5520_FLAG_OFFT_SHIFT) & + ADP5520_FLAG_OFFT_MASK) << 4); + ret |= adp5520_clr_bits(dev, ADP5520_LED_CONTROL, + ADP5520_C3_MODE); + ret |= adp5520_set_bits(dev, ADP5520_LED_CONTROL, + ADP5520_LED3_EN); + break; + } + + return ret; +} + +static int adp5520_led_prepare(struct platform_device *pdev) +{ + struct adp5520_leds_platform_data *pdata = dev_get_platdata(&pdev->dev); + struct device *dev = pdev->dev.parent; + int ret = 0; + + ret |= adp5520_write(dev, ADP5520_LED1_CURRENT, 0); + ret |= adp5520_write(dev, ADP5520_LED2_CURRENT, 0); + ret |= adp5520_write(dev, ADP5520_LED3_CURRENT, 0); + ret |= adp5520_write(dev, ADP5520_LED_TIME, pdata->led_on_time << 6); + ret |= adp5520_write(dev, ADP5520_LED_FADE, FADE_VAL(pdata->fade_in, + pdata->fade_out)); + + return ret; +} + +static int adp5520_led_probe(struct platform_device *pdev) +{ + struct adp5520_leds_platform_data *pdata = dev_get_platdata(&pdev->dev); + struct adp5520_led *led, *led_dat; + struct led_info *cur_led; + int ret, i; + + if (pdata == NULL) { + dev_err(&pdev->dev, "missing platform data\n"); + return -ENODEV; + } + + if (pdata->num_leds > ADP5520_01_MAXLEDS) { + dev_err(&pdev->dev, "can't handle more than %d LEDS\n", + ADP5520_01_MAXLEDS); + return -EFAULT; + } + + led = devm_kcalloc(&pdev->dev, pdata->num_leds, sizeof(*led), + GFP_KERNEL); + if (!led) + return -ENOMEM; + + ret = adp5520_led_prepare(pdev); + if (ret) { + dev_err(&pdev->dev, "failed to write\n"); + return ret; + } + + for (i = 0; i < pdata->num_leds; ++i) { + cur_led = &pdata->leds[i]; + led_dat = &led[i]; + + led_dat->cdev.name = cur_led->name; + led_dat->cdev.default_trigger = cur_led->default_trigger; + led_dat->cdev.brightness_set_blocking = adp5520_led_set; + led_dat->cdev.brightness = LED_OFF; + + if (cur_led->flags & ADP5520_FLAG_LED_MASK) + led_dat->flags = cur_led->flags; + else + led_dat->flags = i + 1; + + led_dat->id = led_dat->flags & ADP5520_FLAG_LED_MASK; + + led_dat->master = pdev->dev.parent; + + ret = led_classdev_register(led_dat->master, &led_dat->cdev); + if (ret) { + dev_err(&pdev->dev, "failed to register LED %d\n", + led_dat->id); + goto err; + } + + ret = adp5520_led_setup(led_dat); + if (ret) { + dev_err(&pdev->dev, "failed to write\n"); + i++; + goto err; + } + } + + platform_set_drvdata(pdev, led); + return 0; + +err: + if (i > 0) { + for (i = i - 1; i >= 0; i--) + led_classdev_unregister(&led[i].cdev); + } + + return ret; +} + +static int adp5520_led_remove(struct platform_device *pdev) +{ + struct adp5520_leds_platform_data *pdata = dev_get_platdata(&pdev->dev); + struct adp5520_led *led; + int i; + + led = platform_get_drvdata(pdev); + + adp5520_clr_bits(led->master, ADP5520_LED_CONTROL, + ADP5520_LED1_EN | ADP5520_LED2_EN | ADP5520_LED3_EN); + + for (i = 0; i < pdata->num_leds; i++) { + led_classdev_unregister(&led[i].cdev); + } + + return 0; +} + +static struct platform_driver adp5520_led_driver = { + .driver = { + .name = "adp5520-led", + }, + .probe = adp5520_led_probe, + .remove = adp5520_led_remove, +}; + +module_platform_driver(adp5520_led_driver); + +MODULE_AUTHOR("Michael Hennerich <hennerich@blackfin.uclinux.org>"); +MODULE_DESCRIPTION("LEDS ADP5520(01) Driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:adp5520-led"); diff --git a/drivers/leds/leds-an30259a.c b/drivers/leds/leds-an30259a.c new file mode 100644 index 000000000..a0df1fb28 --- /dev/null +++ b/drivers/leds/leds-an30259a.c @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: GPL-2.0+ +// +// Driver for Panasonic AN30259A 3-channel LED driver +// +// Copyright (c) 2018 Simon Shields <simon@lineageos.org> +// +// Datasheet: +// https://www.alliedelec.com/m/d/a9d2b3ee87c2d1a535a41dd747b1c247.pdf + +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/regmap.h> + +#define AN30259A_MAX_LEDS 3 + +#define AN30259A_REG_SRESET 0x00 +#define AN30259A_LED_SRESET BIT(0) + +/* LED power registers */ +#define AN30259A_REG_LED_ON 0x01 +#define AN30259A_LED_EN(x) BIT((x) - 1) +#define AN30259A_LED_SLOPE(x) BIT(((x) - 1) + 4) + +#define AN30259A_REG_LEDCC(x) (0x03 + ((x) - 1)) + +/* slope control registers */ +#define AN30259A_REG_SLOPE(x) (0x06 + ((x) - 1)) +#define AN30259A_LED_SLOPETIME1(x) (x) +#define AN30259A_LED_SLOPETIME2(x) ((x) << 4) + +#define AN30259A_REG_LEDCNT1(x) (0x09 + (4 * ((x) - 1))) +#define AN30259A_LED_DUTYMAX(x) ((x) << 4) +#define AN30259A_LED_DUTYMID(x) (x) + +#define AN30259A_REG_LEDCNT2(x) (0x0A + (4 * ((x) - 1))) +#define AN30259A_LED_DELAY(x) ((x) << 4) +#define AN30259A_LED_DUTYMIN(x) (x) + +/* detention time control (length of each slope step) */ +#define AN30259A_REG_LEDCNT3(x) (0x0B + (4 * ((x) - 1))) +#define AN30259A_LED_DT1(x) (x) +#define AN30259A_LED_DT2(x) ((x) << 4) + +#define AN30259A_REG_LEDCNT4(x) (0x0C + (4 * ((x) - 1))) +#define AN30259A_LED_DT3(x) (x) +#define AN30259A_LED_DT4(x) ((x) << 4) + +#define AN30259A_REG_MAX 0x14 + +#define AN30259A_BLINK_MAX_TIME 7500 /* ms */ +#define AN30259A_SLOPE_RESOLUTION 500 /* ms */ + +#define AN30259A_NAME "an30259a" + +#define STATE_OFF 0 +#define STATE_KEEP 1 +#define STATE_ON 2 + +struct an30259a; + +struct an30259a_led { + struct an30259a *chip; + struct fwnode_handle *fwnode; + struct led_classdev cdev; + u32 num; + u32 default_state; + bool sloping; +}; + +struct an30259a { + struct mutex mutex; /* held when writing to registers */ + struct i2c_client *client; + struct an30259a_led leds[AN30259A_MAX_LEDS]; + struct regmap *regmap; + int num_leds; +}; + +static int an30259a_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct an30259a_led *led; + int ret; + unsigned int led_on; + + led = container_of(cdev, struct an30259a_led, cdev); + mutex_lock(&led->chip->mutex); + + ret = regmap_read(led->chip->regmap, AN30259A_REG_LED_ON, &led_on); + if (ret) + goto error; + + switch (brightness) { + case LED_OFF: + led_on &= ~AN30259A_LED_EN(led->num); + led_on &= ~AN30259A_LED_SLOPE(led->num); + led->sloping = false; + break; + default: + led_on |= AN30259A_LED_EN(led->num); + if (led->sloping) + led_on |= AN30259A_LED_SLOPE(led->num); + ret = regmap_write(led->chip->regmap, + AN30259A_REG_LEDCNT1(led->num), + AN30259A_LED_DUTYMAX(0xf) | + AN30259A_LED_DUTYMID(0xf)); + if (ret) + goto error; + break; + } + + ret = regmap_write(led->chip->regmap, AN30259A_REG_LED_ON, led_on); + if (ret) + goto error; + + ret = regmap_write(led->chip->regmap, AN30259A_REG_LEDCC(led->num), + brightness); + +error: + mutex_unlock(&led->chip->mutex); + + return ret; +} + +static int an30259a_blink_set(struct led_classdev *cdev, + unsigned long *delay_off, unsigned long *delay_on) +{ + struct an30259a_led *led; + int ret, num; + unsigned int led_on; + unsigned long off = *delay_off, on = *delay_on; + + led = container_of(cdev, struct an30259a_led, cdev); + + mutex_lock(&led->chip->mutex); + num = led->num; + + /* slope time can only be a multiple of 500ms. */ + if (off % AN30259A_SLOPE_RESOLUTION || on % AN30259A_SLOPE_RESOLUTION) { + ret = -EINVAL; + goto error; + } + + /* up to a maximum of 7500ms. */ + if (off > AN30259A_BLINK_MAX_TIME || on > AN30259A_BLINK_MAX_TIME) { + ret = -EINVAL; + goto error; + } + + /* if no blink specified, default to 1 Hz. */ + if (!off && !on) { + *delay_off = off = 500; + *delay_on = on = 500; + } + + /* convert into values the HW will understand. */ + off /= AN30259A_SLOPE_RESOLUTION; + on /= AN30259A_SLOPE_RESOLUTION; + + /* duty min should be zero (=off), delay should be zero. */ + ret = regmap_write(led->chip->regmap, AN30259A_REG_LEDCNT2(num), + AN30259A_LED_DELAY(0) | AN30259A_LED_DUTYMIN(0)); + if (ret) + goto error; + + /* reset detention time (no "breathing" effect). */ + ret = regmap_write(led->chip->regmap, AN30259A_REG_LEDCNT3(num), + AN30259A_LED_DT1(0) | AN30259A_LED_DT2(0)); + if (ret) + goto error; + ret = regmap_write(led->chip->regmap, AN30259A_REG_LEDCNT4(num), + AN30259A_LED_DT3(0) | AN30259A_LED_DT4(0)); + if (ret) + goto error; + + /* slope time controls on/off cycle length. */ + ret = regmap_write(led->chip->regmap, AN30259A_REG_SLOPE(num), + AN30259A_LED_SLOPETIME1(off) | + AN30259A_LED_SLOPETIME2(on)); + if (ret) + goto error; + + /* Finally, enable slope mode. */ + ret = regmap_read(led->chip->regmap, AN30259A_REG_LED_ON, &led_on); + if (ret) + goto error; + + led_on |= AN30259A_LED_SLOPE(num) | AN30259A_LED_EN(led->num); + + ret = regmap_write(led->chip->regmap, AN30259A_REG_LED_ON, led_on); + + if (!ret) + led->sloping = true; +error: + mutex_unlock(&led->chip->mutex); + + return ret; +} + +static int an30259a_dt_init(struct i2c_client *client, + struct an30259a *chip) +{ + struct device_node *np = dev_of_node(&client->dev), *child; + int count, ret; + int i = 0; + const char *str; + struct an30259a_led *led; + + count = of_get_available_child_count(np); + if (!count || count > AN30259A_MAX_LEDS) + return -EINVAL; + + for_each_available_child_of_node(np, child) { + u32 source; + + ret = of_property_read_u32(child, "reg", &source); + if (ret != 0 || !source || source > AN30259A_MAX_LEDS) { + dev_err(&client->dev, "Couldn't read LED address: %d\n", + ret); + count--; + continue; + } + + led = &chip->leds[i]; + + led->num = source; + led->chip = chip; + led->fwnode = of_fwnode_handle(child); + + if (!of_property_read_string(child, "default-state", &str)) { + if (!strcmp(str, "on")) + led->default_state = STATE_ON; + else if (!strcmp(str, "keep")) + led->default_state = STATE_KEEP; + else + led->default_state = STATE_OFF; + } + + i++; + } + + if (!count) + return -EINVAL; + + chip->num_leds = i; + + return 0; +} + +static const struct regmap_config an30259a_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = AN30259A_REG_MAX, +}; + +static void an30259a_init_default_state(struct an30259a_led *led) +{ + struct an30259a *chip = led->chip; + int led_on, err; + + switch (led->default_state) { + case STATE_ON: + led->cdev.brightness = LED_FULL; + break; + case STATE_KEEP: + err = regmap_read(chip->regmap, AN30259A_REG_LED_ON, &led_on); + if (err) + break; + + if (!(led_on & AN30259A_LED_EN(led->num))) { + led->cdev.brightness = LED_OFF; + break; + } + regmap_read(chip->regmap, AN30259A_REG_LEDCC(led->num), + &led->cdev.brightness); + break; + default: + led->cdev.brightness = LED_OFF; + } + + an30259a_brightness_set(&led->cdev, led->cdev.brightness); +} + +static int an30259a_probe(struct i2c_client *client) +{ + struct an30259a *chip; + int i, err; + + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + err = an30259a_dt_init(client, chip); + if (err < 0) + return err; + + mutex_init(&chip->mutex); + chip->client = client; + i2c_set_clientdata(client, chip); + + chip->regmap = devm_regmap_init_i2c(client, &an30259a_regmap_config); + + if (IS_ERR(chip->regmap)) { + err = PTR_ERR(chip->regmap); + dev_err(&client->dev, "Failed to allocate register map: %d\n", + err); + goto exit; + } + + for (i = 0; i < chip->num_leds; i++) { + struct led_init_data init_data = {}; + + an30259a_init_default_state(&chip->leds[i]); + chip->leds[i].cdev.brightness_set_blocking = + an30259a_brightness_set; + chip->leds[i].cdev.blink_set = an30259a_blink_set; + + init_data.fwnode = chip->leds[i].fwnode; + init_data.devicename = AN30259A_NAME; + init_data.default_label = ":"; + + err = devm_led_classdev_register_ext(&client->dev, + &chip->leds[i].cdev, + &init_data); + if (err < 0) + goto exit; + } + return 0; + +exit: + mutex_destroy(&chip->mutex); + return err; +} + +static int an30259a_remove(struct i2c_client *client) +{ + struct an30259a *chip = i2c_get_clientdata(client); + + mutex_destroy(&chip->mutex); + + return 0; +} + +static const struct of_device_id an30259a_match_table[] = { + { .compatible = "panasonic,an30259a", }, + { /* sentinel */ }, +}; + +MODULE_DEVICE_TABLE(of, an30259a_match_table); + +static const struct i2c_device_id an30259a_id[] = { + { "an30259a", 0 }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(i2c, an30259a_id); + +static struct i2c_driver an30259a_driver = { + .driver = { + .name = "leds-an30259a", + .of_match_table = of_match_ptr(an30259a_match_table), + }, + .probe_new = an30259a_probe, + .remove = an30259a_remove, + .id_table = an30259a_id, +}; + +module_i2c_driver(an30259a_driver); + +MODULE_AUTHOR("Simon Shields <simon@lineageos.org>"); +MODULE_DESCRIPTION("AN30259A LED driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-apu.c b/drivers/leds/leds-apu.c new file mode 100644 index 000000000..7fd557ace --- /dev/null +++ b/drivers/leds/leds-apu.c @@ -0,0 +1,213 @@ +/* + * drivers/leds/leds-apu.c + * Copyright (C) 2017 Alan Mizrahi, alan at mizrahi dot com dot ve + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the names of the copyright holders nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * Alternatively, this software may be distributed under the terms of the + * GNU General Public License ("GPL") version 2 as published by the Free + * Software Foundation. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/dmi.h> +#include <linux/err.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/kernel.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/platform_device.h> + +#define APU1_FCH_ACPI_MMIO_BASE 0xFED80000 +#define APU1_FCH_GPIO_BASE (APU1_FCH_ACPI_MMIO_BASE + 0x01BD) +#define APU1_LEDON 0x08 +#define APU1_LEDOFF 0xC8 +#define APU1_NUM_GPIO 3 +#define APU1_IOSIZE sizeof(u8) + +/* LED access parameters */ +struct apu_param { + void __iomem *addr; /* for ioread/iowrite */ +}; + +/* LED private data */ +struct apu_led_priv { + struct led_classdev cdev; + struct apu_param param; +}; +#define cdev_to_priv(c) container_of(c, struct apu_led_priv, cdev) + +/* LED profile */ +struct apu_led_profile { + const char *name; + enum led_brightness brightness; + unsigned long offset; /* for devm_ioremap */ +}; + +struct apu_led_pdata { + struct platform_device *pdev; + struct apu_led_priv *pled; + spinlock_t lock; +}; + +static struct apu_led_pdata *apu_led; + +static const struct apu_led_profile apu1_led_profile[] = { + { "apu:green:1", LED_ON, APU1_FCH_GPIO_BASE + 0 * APU1_IOSIZE }, + { "apu:green:2", LED_OFF, APU1_FCH_GPIO_BASE + 1 * APU1_IOSIZE }, + { "apu:green:3", LED_OFF, APU1_FCH_GPIO_BASE + 2 * APU1_IOSIZE }, +}; + +static const struct dmi_system_id apu_led_dmi_table[] __initconst = { + { + .ident = "apu", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "PC Engines"), + DMI_MATCH(DMI_PRODUCT_NAME, "APU") + } + }, + {} +}; +MODULE_DEVICE_TABLE(dmi, apu_led_dmi_table); + +static void apu1_led_brightness_set(struct led_classdev *led, enum led_brightness value) +{ + struct apu_led_priv *pled = cdev_to_priv(led); + + spin_lock(&apu_led->lock); + iowrite8(value ? APU1_LEDON : APU1_LEDOFF, pled->param.addr); + spin_unlock(&apu_led->lock); +} + +static int apu_led_config(struct device *dev, struct apu_led_pdata *apuld) +{ + int i; + int err; + + apu_led->pled = devm_kcalloc(dev, + ARRAY_SIZE(apu1_led_profile), sizeof(struct apu_led_priv), + GFP_KERNEL); + + if (!apu_led->pled) + return -ENOMEM; + + for (i = 0; i < ARRAY_SIZE(apu1_led_profile); i++) { + struct apu_led_priv *pled = &apu_led->pled[i]; + struct led_classdev *led_cdev = &pled->cdev; + + led_cdev->name = apu1_led_profile[i].name; + led_cdev->brightness = apu1_led_profile[i].brightness; + led_cdev->max_brightness = 1; + led_cdev->flags = LED_CORE_SUSPENDRESUME; + led_cdev->brightness_set = apu1_led_brightness_set; + + pled->param.addr = devm_ioremap(dev, + apu1_led_profile[i].offset, APU1_IOSIZE); + if (!pled->param.addr) { + err = -ENOMEM; + goto error; + } + + err = led_classdev_register(dev, led_cdev); + if (err) + goto error; + + apu1_led_brightness_set(led_cdev, apu1_led_profile[i].brightness); + } + + return 0; + +error: + while (i-- > 0) + led_classdev_unregister(&apu_led->pled[i].cdev); + + return err; +} + +static int __init apu_led_probe(struct platform_device *pdev) +{ + apu_led = devm_kzalloc(&pdev->dev, sizeof(*apu_led), GFP_KERNEL); + + if (!apu_led) + return -ENOMEM; + + apu_led->pdev = pdev; + + spin_lock_init(&apu_led->lock); + return apu_led_config(&pdev->dev, apu_led); +} + +static struct platform_driver apu_led_driver = { + .driver = { + .name = KBUILD_MODNAME, + }, +}; + +static int __init apu_led_init(void) +{ + struct platform_device *pdev; + int err; + + if (!(dmi_match(DMI_SYS_VENDOR, "PC Engines") && + dmi_match(DMI_PRODUCT_NAME, "APU"))) { + pr_err("No PC Engines APUv1 board detected. For APUv2,3 support, enable CONFIG_PCENGINES_APU2\n"); + return -ENODEV; + } + + pdev = platform_device_register_simple(KBUILD_MODNAME, -1, NULL, 0); + if (IS_ERR(pdev)) { + pr_err("Device allocation failed\n"); + return PTR_ERR(pdev); + } + + err = platform_driver_probe(&apu_led_driver, apu_led_probe); + if (err) { + pr_err("Probe platform driver failed\n"); + platform_device_unregister(pdev); + } + + return err; +} + +static void __exit apu_led_exit(void) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(apu1_led_profile); i++) + led_classdev_unregister(&apu_led->pled[i].cdev); + + platform_device_unregister(apu_led->pdev); + platform_driver_unregister(&apu_led_driver); +} + +module_init(apu_led_init); +module_exit(apu_led_exit); + +MODULE_AUTHOR("Alan Mizrahi"); +MODULE_DESCRIPTION("PC Engines APU1 front LED driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:leds_apu"); diff --git a/drivers/leds/leds-ariel.c b/drivers/leds/leds-ariel.c new file mode 100644 index 000000000..bb68ba23a --- /dev/null +++ b/drivers/leds/leds-ariel.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: BSD-2-Clause OR GPL-2.0-or-later +/* + * Dell Wyse 3020 a.k.a. "Ariel" Embedded Controller LED Driver + * + * Copyright (C) 2020 Lubomir Rintel + */ + +#include <linux/module.h> +#include <linux/leds.h> +#include <linux/regmap.h> +#include <linux/of_platform.h> + +enum ec_index { + EC_BLUE_LED = 0x01, + EC_AMBER_LED = 0x02, + EC_GREEN_LED = 0x03, +}; + +enum { + EC_LED_OFF = 0x00, + EC_LED_STILL = 0x01, + EC_LED_FADE = 0x02, + EC_LED_BLINK = 0x03, +}; + +struct ariel_led { + struct regmap *ec_ram; + enum ec_index ec_index; + struct led_classdev led_cdev; +}; + +#define led_cdev_to_ariel_led(c) container_of(c, struct ariel_led, led_cdev) + +static enum led_brightness ariel_led_get(struct led_classdev *led_cdev) +{ + struct ariel_led *led = led_cdev_to_ariel_led(led_cdev); + unsigned int led_status = 0; + + if (regmap_read(led->ec_ram, led->ec_index, &led_status)) + return LED_OFF; + + if (led_status == EC_LED_STILL) + return LED_FULL; + else + return LED_OFF; +} + +static void ariel_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct ariel_led *led = led_cdev_to_ariel_led(led_cdev); + + if (brightness == LED_OFF) + regmap_write(led->ec_ram, led->ec_index, EC_LED_OFF); + else + regmap_write(led->ec_ram, led->ec_index, EC_LED_STILL); +} + +static int ariel_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, unsigned long *delay_off) +{ + struct ariel_led *led = led_cdev_to_ariel_led(led_cdev); + + if (*delay_on == 0 && *delay_off == 0) + return -EINVAL; + + if (*delay_on == 0) { + regmap_write(led->ec_ram, led->ec_index, EC_LED_OFF); + } else if (*delay_off == 0) { + regmap_write(led->ec_ram, led->ec_index, EC_LED_STILL); + } else { + *delay_on = 500; + *delay_off = 500; + regmap_write(led->ec_ram, led->ec_index, EC_LED_BLINK); + } + + return 0; +} + +#define NLEDS 3 + +static int ariel_led_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct ariel_led *leds; + struct regmap *ec_ram; + int ret; + int i; + + ec_ram = dev_get_regmap(dev->parent, "ec_ram"); + if (!ec_ram) + return -ENODEV; + + leds = devm_kcalloc(dev, NLEDS, sizeof(*leds), GFP_KERNEL); + if (!leds) + return -ENOMEM; + + leds[0].ec_index = EC_BLUE_LED; + leds[0].led_cdev.name = "blue:power", + leds[0].led_cdev.default_trigger = "default-on"; + + leds[1].ec_index = EC_AMBER_LED; + leds[1].led_cdev.name = "amber:status", + + leds[2].ec_index = EC_GREEN_LED; + leds[2].led_cdev.name = "green:status", + leds[2].led_cdev.default_trigger = "default-on"; + + for (i = 0; i < NLEDS; i++) { + leds[i].ec_ram = ec_ram; + leds[i].led_cdev.brightness_get = ariel_led_get; + leds[i].led_cdev.brightness_set = ariel_led_set; + leds[i].led_cdev.blink_set = ariel_blink_set; + + ret = devm_led_classdev_register(dev, &leds[i].led_cdev); + if (ret) + return ret; + } + + return 0; +} + +static struct platform_driver ariel_led_driver = { + .probe = ariel_led_probe, + .driver = { + .name = "dell-wyse-ariel-led", + }, +}; +module_platform_driver(ariel_led_driver); + +MODULE_AUTHOR("Lubomir Rintel <lkundrak@v3.sk>"); +MODULE_DESCRIPTION("Dell Wyse 3020 Status LEDs Driver"); +MODULE_LICENSE("Dual BSD/GPL"); diff --git a/drivers/leds/leds-as3645a.c b/drivers/leds/leds-as3645a.c new file mode 100644 index 000000000..80411d41e --- /dev/null +++ b/drivers/leds/leds-as3645a.c @@ -0,0 +1,774 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * drivers/leds/leds-as3645a.c - AS3645A and LM3555 flash controllers driver + * + * Copyright (C) 2008-2011 Nokia Corporation + * Copyright (c) 2011, 2017 Intel Corporation. + * + * Based on drivers/media/i2c/as3645a.c. + * + * Contact: Sakari Ailus <sakari.ailus@iki.fi> + */ + +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/i2c.h> +#include <linux/led-class-flash.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/property.h> +#include <linux/slab.h> + +#include <media/v4l2-flash-led-class.h> + +#define AS_TIMER_US_TO_CODE(t) (((t) / 1000 - 100) / 50) +#define AS_TIMER_CODE_TO_US(c) ((50 * (c) + 100) * 1000) + +/* Register definitions */ + +/* Read-only Design info register: Reset state: xxxx 0001 */ +#define AS_DESIGN_INFO_REG 0x00 +#define AS_DESIGN_INFO_FACTORY(x) (((x) >> 4)) +#define AS_DESIGN_INFO_MODEL(x) ((x) & 0x0f) + +/* Read-only Version control register: Reset state: 0000 0000 + * for first engineering samples + */ +#define AS_VERSION_CONTROL_REG 0x01 +#define AS_VERSION_CONTROL_RFU(x) (((x) >> 4)) +#define AS_VERSION_CONTROL_VERSION(x) ((x) & 0x0f) + +/* Read / Write (Indicator and timer register): Reset state: 0000 1111 */ +#define AS_INDICATOR_AND_TIMER_REG 0x02 +#define AS_INDICATOR_AND_TIMER_TIMEOUT_SHIFT 0 +#define AS_INDICATOR_AND_TIMER_VREF_SHIFT 4 +#define AS_INDICATOR_AND_TIMER_INDICATOR_SHIFT 6 + +/* Read / Write (Current set register): Reset state: 0110 1001 */ +#define AS_CURRENT_SET_REG 0x03 +#define AS_CURRENT_ASSIST_LIGHT_SHIFT 0 +#define AS_CURRENT_LED_DET_ON (1 << 3) +#define AS_CURRENT_FLASH_CURRENT_SHIFT 4 + +/* Read / Write (Control register): Reset state: 1011 0100 */ +#define AS_CONTROL_REG 0x04 +#define AS_CONTROL_MODE_SETTING_SHIFT 0 +#define AS_CONTROL_STROBE_ON (1 << 2) +#define AS_CONTROL_OUT_ON (1 << 3) +#define AS_CONTROL_EXT_TORCH_ON (1 << 4) +#define AS_CONTROL_STROBE_TYPE_EDGE (0 << 5) +#define AS_CONTROL_STROBE_TYPE_LEVEL (1 << 5) +#define AS_CONTROL_COIL_PEAK_SHIFT 6 + +/* Read only (D3 is read / write) (Fault and info): Reset state: 0000 x000 */ +#define AS_FAULT_INFO_REG 0x05 +#define AS_FAULT_INFO_INDUCTOR_PEAK_LIMIT (1 << 1) +#define AS_FAULT_INFO_INDICATOR_LED (1 << 2) +#define AS_FAULT_INFO_LED_AMOUNT (1 << 3) +#define AS_FAULT_INFO_TIMEOUT (1 << 4) +#define AS_FAULT_INFO_OVER_TEMPERATURE (1 << 5) +#define AS_FAULT_INFO_SHORT_CIRCUIT (1 << 6) +#define AS_FAULT_INFO_OVER_VOLTAGE (1 << 7) + +/* Boost register */ +#define AS_BOOST_REG 0x0d +#define AS_BOOST_CURRENT_DISABLE (0 << 0) +#define AS_BOOST_CURRENT_ENABLE (1 << 0) + +/* Password register is used to unlock boost register writing */ +#define AS_PASSWORD_REG 0x0f +#define AS_PASSWORD_UNLOCK_VALUE 0x55 + +#define AS_NAME "as3645a" +#define AS_I2C_ADDR (0x60 >> 1) /* W:0x60, R:0x61 */ + +#define AS_FLASH_TIMEOUT_MIN 100000 /* us */ +#define AS_FLASH_TIMEOUT_MAX 850000 +#define AS_FLASH_TIMEOUT_STEP 50000 + +#define AS_FLASH_INTENSITY_MIN 200000 /* uA */ +#define AS_FLASH_INTENSITY_MAX_1LED 500000 +#define AS_FLASH_INTENSITY_MAX_2LEDS 400000 +#define AS_FLASH_INTENSITY_STEP 20000 + +#define AS_TORCH_INTENSITY_MIN 20000 /* uA */ +#define AS_TORCH_INTENSITY_MAX 160000 +#define AS_TORCH_INTENSITY_STEP 20000 + +#define AS_INDICATOR_INTENSITY_MIN 0 /* uA */ +#define AS_INDICATOR_INTENSITY_MAX 10000 +#define AS_INDICATOR_INTENSITY_STEP 2500 + +#define AS_PEAK_mA_MAX 2000 +#define AS_PEAK_mA_TO_REG(a) \ + ((min_t(u32, AS_PEAK_mA_MAX, a) - 1250) / 250) + +/* LED numbers for Devicetree */ +#define AS_LED_FLASH 0 +#define AS_LED_INDICATOR 1 + +enum as_mode { + AS_MODE_EXT_TORCH = 0 << AS_CONTROL_MODE_SETTING_SHIFT, + AS_MODE_INDICATOR = 1 << AS_CONTROL_MODE_SETTING_SHIFT, + AS_MODE_ASSIST = 2 << AS_CONTROL_MODE_SETTING_SHIFT, + AS_MODE_FLASH = 3 << AS_CONTROL_MODE_SETTING_SHIFT, +}; + +struct as3645a_config { + u32 flash_timeout_us; + u32 flash_max_ua; + u32 assist_max_ua; + u32 indicator_max_ua; + u32 voltage_reference; + u32 peak; +}; + +struct as3645a { + struct i2c_client *client; + + struct mutex mutex; + + struct led_classdev_flash fled; + struct led_classdev iled_cdev; + + struct v4l2_flash *vf; + struct v4l2_flash *vfind; + + struct fwnode_handle *flash_node; + struct fwnode_handle *indicator_node; + + struct as3645a_config cfg; + + enum as_mode mode; + unsigned int timeout; + unsigned int flash_current; + unsigned int assist_current; + unsigned int indicator_current; + enum v4l2_flash_strobe_source strobe_source; +}; + +#define fled_to_as3645a(__fled) container_of(__fled, struct as3645a, fled) +#define iled_cdev_to_as3645a(__iled_cdev) \ + container_of(__iled_cdev, struct as3645a, iled_cdev) + +/* Return negative errno else zero on success */ +static int as3645a_write(struct as3645a *flash, u8 addr, u8 val) +{ + struct i2c_client *client = flash->client; + int rval; + + rval = i2c_smbus_write_byte_data(client, addr, val); + + dev_dbg(&client->dev, "Write Addr:%02X Val:%02X %s\n", addr, val, + rval < 0 ? "fail" : "ok"); + + return rval; +} + +/* Return negative errno else a data byte received from the device. */ +static int as3645a_read(struct as3645a *flash, u8 addr) +{ + struct i2c_client *client = flash->client; + int rval; + + rval = i2c_smbus_read_byte_data(client, addr); + + dev_dbg(&client->dev, "Read Addr:%02X Val:%02X %s\n", addr, rval, + rval < 0 ? "fail" : "ok"); + + return rval; +} + +/* ----------------------------------------------------------------------------- + * Hardware configuration and trigger + */ + +/** + * as3645a_set_config - Set flash configuration registers + * @flash: The flash + * + * Configure the hardware with flash, assist and indicator currents, as well as + * flash timeout. + * + * Return 0 on success, or a negative error code if an I2C communication error + * occurred. + */ +static int as3645a_set_current(struct as3645a *flash) +{ + u8 val; + + val = (flash->flash_current << AS_CURRENT_FLASH_CURRENT_SHIFT) + | (flash->assist_current << AS_CURRENT_ASSIST_LIGHT_SHIFT) + | AS_CURRENT_LED_DET_ON; + + return as3645a_write(flash, AS_CURRENT_SET_REG, val); +} + +static int as3645a_set_timeout(struct as3645a *flash) +{ + u8 val; + + val = flash->timeout << AS_INDICATOR_AND_TIMER_TIMEOUT_SHIFT; + + val |= (flash->cfg.voltage_reference + << AS_INDICATOR_AND_TIMER_VREF_SHIFT) + | ((flash->indicator_current ? flash->indicator_current - 1 : 0) + << AS_INDICATOR_AND_TIMER_INDICATOR_SHIFT); + + return as3645a_write(flash, AS_INDICATOR_AND_TIMER_REG, val); +} + +/** + * as3645a_set_control - Set flash control register + * @flash: The flash + * @mode: Desired output mode + * @on: Desired output state + * + * Configure the hardware with output mode and state. + * + * Return 0 on success, or a negative error code if an I2C communication error + * occurred. + */ +static int +as3645a_set_control(struct as3645a *flash, enum as_mode mode, bool on) +{ + u8 reg; + + /* Configure output parameters and operation mode. */ + reg = (flash->cfg.peak << AS_CONTROL_COIL_PEAK_SHIFT) + | (on ? AS_CONTROL_OUT_ON : 0) + | mode; + + if (mode == AS_MODE_FLASH && + flash->strobe_source == V4L2_FLASH_STROBE_SOURCE_EXTERNAL) + reg |= AS_CONTROL_STROBE_TYPE_LEVEL + | AS_CONTROL_STROBE_ON; + + return as3645a_write(flash, AS_CONTROL_REG, reg); +} + +static int as3645a_get_fault(struct led_classdev_flash *fled, u32 *fault) +{ + struct as3645a *flash = fled_to_as3645a(fled); + int rval; + + /* NOTE: reading register clears fault status */ + rval = as3645a_read(flash, AS_FAULT_INFO_REG); + if (rval < 0) + return rval; + + if (rval & AS_FAULT_INFO_INDUCTOR_PEAK_LIMIT) + *fault |= LED_FAULT_OVER_CURRENT; + + if (rval & AS_FAULT_INFO_INDICATOR_LED) + *fault |= LED_FAULT_INDICATOR; + + dev_dbg(&flash->client->dev, "%u connected LEDs\n", + rval & AS_FAULT_INFO_LED_AMOUNT ? 2 : 1); + + if (rval & AS_FAULT_INFO_TIMEOUT) + *fault |= LED_FAULT_TIMEOUT; + + if (rval & AS_FAULT_INFO_OVER_TEMPERATURE) + *fault |= LED_FAULT_OVER_TEMPERATURE; + + if (rval & AS_FAULT_INFO_SHORT_CIRCUIT) + *fault |= LED_FAULT_OVER_CURRENT; + + if (rval & AS_FAULT_INFO_OVER_VOLTAGE) + *fault |= LED_FAULT_INPUT_VOLTAGE; + + return rval; +} + +static unsigned int __as3645a_current_to_reg(unsigned int min, unsigned int max, + unsigned int step, + unsigned int val) +{ + if (val < min) + val = min; + + if (val > max) + val = max; + + return (val - min) / step; +} + +static unsigned int as3645a_current_to_reg(struct as3645a *flash, bool is_flash, + unsigned int ua) +{ + if (is_flash) + return __as3645a_current_to_reg(AS_TORCH_INTENSITY_MIN, + flash->cfg.assist_max_ua, + AS_TORCH_INTENSITY_STEP, ua); + else + return __as3645a_current_to_reg(AS_FLASH_INTENSITY_MIN, + flash->cfg.flash_max_ua, + AS_FLASH_INTENSITY_STEP, ua); +} + +static int as3645a_set_indicator_brightness(struct led_classdev *iled_cdev, + enum led_brightness brightness) +{ + struct as3645a *flash = iled_cdev_to_as3645a(iled_cdev); + int rval; + + flash->indicator_current = brightness; + + rval = as3645a_set_timeout(flash); + if (rval) + return rval; + + return as3645a_set_control(flash, AS_MODE_INDICATOR, brightness); +} + +static int as3645a_set_assist_brightness(struct led_classdev *fled_cdev, + enum led_brightness brightness) +{ + struct led_classdev_flash *fled = lcdev_to_flcdev(fled_cdev); + struct as3645a *flash = fled_to_as3645a(fled); + int rval; + + if (brightness) { + /* Register value 0 is 20 mA. */ + flash->assist_current = brightness - 1; + + rval = as3645a_set_current(flash); + if (rval) + return rval; + } + + return as3645a_set_control(flash, AS_MODE_ASSIST, brightness); +} + +static int as3645a_set_flash_brightness(struct led_classdev_flash *fled, + u32 brightness_ua) +{ + struct as3645a *flash = fled_to_as3645a(fled); + + flash->flash_current = as3645a_current_to_reg(flash, true, + brightness_ua); + + return as3645a_set_current(flash); +} + +static int as3645a_set_flash_timeout(struct led_classdev_flash *fled, + u32 timeout_us) +{ + struct as3645a *flash = fled_to_as3645a(fled); + + flash->timeout = AS_TIMER_US_TO_CODE(timeout_us); + + return as3645a_set_timeout(flash); +} + +static int as3645a_set_strobe(struct led_classdev_flash *fled, bool state) +{ + struct as3645a *flash = fled_to_as3645a(fled); + + return as3645a_set_control(flash, AS_MODE_FLASH, state); +} + +static const struct led_flash_ops as3645a_led_flash_ops = { + .flash_brightness_set = as3645a_set_flash_brightness, + .timeout_set = as3645a_set_flash_timeout, + .strobe_set = as3645a_set_strobe, + .fault_get = as3645a_get_fault, +}; + +static int as3645a_setup(struct as3645a *flash) +{ + struct device *dev = &flash->client->dev; + u32 fault = 0; + int rval; + + /* clear errors */ + rval = as3645a_read(flash, AS_FAULT_INFO_REG); + if (rval < 0) + return rval; + + dev_dbg(dev, "Fault info: %02x\n", rval); + + rval = as3645a_set_current(flash); + if (rval < 0) + return rval; + + rval = as3645a_set_timeout(flash); + if (rval < 0) + return rval; + + rval = as3645a_set_control(flash, AS_MODE_INDICATOR, false); + if (rval < 0) + return rval; + + /* read status */ + rval = as3645a_get_fault(&flash->fled, &fault); + if (rval < 0) + return rval; + + dev_dbg(dev, "AS_INDICATOR_AND_TIMER_REG: %02x\n", + as3645a_read(flash, AS_INDICATOR_AND_TIMER_REG)); + dev_dbg(dev, "AS_CURRENT_SET_REG: %02x\n", + as3645a_read(flash, AS_CURRENT_SET_REG)); + dev_dbg(dev, "AS_CONTROL_REG: %02x\n", + as3645a_read(flash, AS_CONTROL_REG)); + + return rval & ~AS_FAULT_INFO_LED_AMOUNT ? -EIO : 0; +} + +static int as3645a_detect(struct as3645a *flash) +{ + struct device *dev = &flash->client->dev; + int rval, man, model, rfu, version; + const char *vendor; + + rval = as3645a_read(flash, AS_DESIGN_INFO_REG); + if (rval < 0) { + dev_err(dev, "can't read design info reg\n"); + return rval; + } + + man = AS_DESIGN_INFO_FACTORY(rval); + model = AS_DESIGN_INFO_MODEL(rval); + + rval = as3645a_read(flash, AS_VERSION_CONTROL_REG); + if (rval < 0) { + dev_err(dev, "can't read version control reg\n"); + return rval; + } + + rfu = AS_VERSION_CONTROL_RFU(rval); + version = AS_VERSION_CONTROL_VERSION(rval); + + /* Verify the chip model and version. */ + if (model != 0x01 || rfu != 0x00) { + dev_err(dev, "AS3645A not detected (model %d rfu %d)\n", + model, rfu); + return -ENODEV; + } + + switch (man) { + case 1: + vendor = "AMS, Austria Micro Systems"; + break; + case 2: + vendor = "ADI, Analog Devices Inc."; + break; + case 3: + vendor = "NSC, National Semiconductor"; + break; + case 4: + vendor = "NXP"; + break; + case 5: + vendor = "TI, Texas Instrument"; + break; + default: + vendor = "Unknown"; + } + + dev_info(dev, "Chip vendor: %s (%d) Version: %d\n", vendor, + man, version); + + rval = as3645a_write(flash, AS_PASSWORD_REG, AS_PASSWORD_UNLOCK_VALUE); + if (rval < 0) + return rval; + + return as3645a_write(flash, AS_BOOST_REG, AS_BOOST_CURRENT_DISABLE); +} + +static int as3645a_parse_node(struct as3645a *flash, + struct fwnode_handle *fwnode) +{ + struct as3645a_config *cfg = &flash->cfg; + struct fwnode_handle *child; + int rval; + + fwnode_for_each_child_node(fwnode, child) { + u32 id = 0; + + fwnode_property_read_u32(child, "reg", &id); + + switch (id) { + case AS_LED_FLASH: + flash->flash_node = child; + fwnode_handle_get(child); + break; + case AS_LED_INDICATOR: + flash->indicator_node = child; + fwnode_handle_get(child); + break; + default: + dev_warn(&flash->client->dev, + "unknown LED %u encountered, ignoring\n", id); + break; + } + } + + if (!flash->flash_node) { + dev_err(&flash->client->dev, "can't find flash node\n"); + return -ENODEV; + } + + rval = fwnode_property_read_u32(flash->flash_node, "flash-timeout-us", + &cfg->flash_timeout_us); + if (rval < 0) { + dev_err(&flash->client->dev, + "can't read flash-timeout-us property for flash\n"); + goto out_err; + } + + rval = fwnode_property_read_u32(flash->flash_node, "flash-max-microamp", + &cfg->flash_max_ua); + if (rval < 0) { + dev_err(&flash->client->dev, + "can't read flash-max-microamp property for flash\n"); + goto out_err; + } + + rval = fwnode_property_read_u32(flash->flash_node, "led-max-microamp", + &cfg->assist_max_ua); + if (rval < 0) { + dev_err(&flash->client->dev, + "can't read led-max-microamp property for flash\n"); + goto out_err; + } + + fwnode_property_read_u32(flash->flash_node, "voltage-reference", + &cfg->voltage_reference); + + fwnode_property_read_u32(flash->flash_node, "ams,input-max-microamp", + &cfg->peak); + cfg->peak = AS_PEAK_mA_TO_REG(cfg->peak); + + if (!flash->indicator_node) { + dev_warn(&flash->client->dev, + "can't find indicator node\n"); + rval = -ENODEV; + goto out_err; + } + + + rval = fwnode_property_read_u32(flash->indicator_node, + "led-max-microamp", + &cfg->indicator_max_ua); + if (rval < 0) { + dev_err(&flash->client->dev, + "can't read led-max-microamp property for indicator\n"); + goto out_err; + } + + return 0; + +out_err: + fwnode_handle_put(flash->flash_node); + fwnode_handle_put(flash->indicator_node); + + return rval; +} + +static int as3645a_led_class_setup(struct as3645a *flash) +{ + struct led_classdev *fled_cdev = &flash->fled.led_cdev; + struct led_classdev *iled_cdev = &flash->iled_cdev; + struct led_init_data init_data = {}; + struct led_flash_setting *cfg; + int rval; + + iled_cdev->brightness_set_blocking = as3645a_set_indicator_brightness; + iled_cdev->max_brightness = + flash->cfg.indicator_max_ua / AS_INDICATOR_INTENSITY_STEP; + iled_cdev->flags = LED_CORE_SUSPENDRESUME; + + init_data.fwnode = flash->indicator_node; + init_data.devicename = AS_NAME; + init_data.default_label = "indicator"; + + rval = led_classdev_register_ext(&flash->client->dev, iled_cdev, + &init_data); + if (rval < 0) + return rval; + + cfg = &flash->fled.brightness; + cfg->min = AS_FLASH_INTENSITY_MIN; + cfg->max = flash->cfg.flash_max_ua; + cfg->step = AS_FLASH_INTENSITY_STEP; + cfg->val = flash->cfg.flash_max_ua; + + cfg = &flash->fled.timeout; + cfg->min = AS_FLASH_TIMEOUT_MIN; + cfg->max = flash->cfg.flash_timeout_us; + cfg->step = AS_FLASH_TIMEOUT_STEP; + cfg->val = flash->cfg.flash_timeout_us; + + flash->fled.ops = &as3645a_led_flash_ops; + + fled_cdev->brightness_set_blocking = as3645a_set_assist_brightness; + /* Value 0 is off in LED class. */ + fled_cdev->max_brightness = + as3645a_current_to_reg(flash, false, + flash->cfg.assist_max_ua) + 1; + fled_cdev->flags = LED_DEV_CAP_FLASH | LED_CORE_SUSPENDRESUME; + + init_data.fwnode = flash->flash_node; + init_data.devicename = AS_NAME; + init_data.default_label = "flash"; + + rval = led_classdev_flash_register_ext(&flash->client->dev, + &flash->fled, &init_data); + if (rval) + goto out_err; + + return rval; + +out_err: + led_classdev_unregister(iled_cdev); + dev_err(&flash->client->dev, + "led_classdev_flash_register() failed, error %d\n", + rval); + return rval; +} + +static int as3645a_v4l2_setup(struct as3645a *flash) +{ + struct led_classdev_flash *fled = &flash->fled; + struct led_classdev *led = &fled->led_cdev; + struct v4l2_flash_config cfg = { + .intensity = { + .min = AS_TORCH_INTENSITY_MIN, + .max = flash->cfg.assist_max_ua, + .step = AS_TORCH_INTENSITY_STEP, + .val = flash->cfg.assist_max_ua, + }, + }; + struct v4l2_flash_config cfgind = { + .intensity = { + .min = AS_INDICATOR_INTENSITY_MIN, + .max = flash->cfg.indicator_max_ua, + .step = AS_INDICATOR_INTENSITY_STEP, + .val = flash->cfg.indicator_max_ua, + }, + }; + + strlcpy(cfg.dev_name, led->dev->kobj.name, sizeof(cfg.dev_name)); + strlcpy(cfgind.dev_name, flash->iled_cdev.dev->kobj.name, + sizeof(cfgind.dev_name)); + + flash->vf = v4l2_flash_init( + &flash->client->dev, flash->flash_node, &flash->fled, NULL, + &cfg); + if (IS_ERR(flash->vf)) + return PTR_ERR(flash->vf); + + flash->vfind = v4l2_flash_indicator_init( + &flash->client->dev, flash->indicator_node, &flash->iled_cdev, + &cfgind); + if (IS_ERR(flash->vfind)) { + v4l2_flash_release(flash->vf); + return PTR_ERR(flash->vfind); + } + + return 0; +} + +static int as3645a_probe(struct i2c_client *client) +{ + struct as3645a *flash; + int rval; + + if (!dev_fwnode(&client->dev)) + return -ENODEV; + + flash = devm_kzalloc(&client->dev, sizeof(*flash), GFP_KERNEL); + if (flash == NULL) + return -ENOMEM; + + flash->client = client; + + rval = as3645a_parse_node(flash, dev_fwnode(&client->dev)); + if (rval < 0) + return rval; + + rval = as3645a_detect(flash); + if (rval < 0) + goto out_put_nodes; + + mutex_init(&flash->mutex); + i2c_set_clientdata(client, flash); + + rval = as3645a_setup(flash); + if (rval) + goto out_mutex_destroy; + + rval = as3645a_led_class_setup(flash); + if (rval) + goto out_mutex_destroy; + + rval = as3645a_v4l2_setup(flash); + if (rval) + goto out_led_classdev_flash_unregister; + + return 0; + +out_led_classdev_flash_unregister: + led_classdev_flash_unregister(&flash->fled); + +out_mutex_destroy: + mutex_destroy(&flash->mutex); + +out_put_nodes: + fwnode_handle_put(flash->flash_node); + fwnode_handle_put(flash->indicator_node); + + return rval; +} + +static int as3645a_remove(struct i2c_client *client) +{ + struct as3645a *flash = i2c_get_clientdata(client); + + as3645a_set_control(flash, AS_MODE_EXT_TORCH, false); + + v4l2_flash_release(flash->vf); + v4l2_flash_release(flash->vfind); + + led_classdev_flash_unregister(&flash->fled); + led_classdev_unregister(&flash->iled_cdev); + + mutex_destroy(&flash->mutex); + + fwnode_handle_put(flash->flash_node); + fwnode_handle_put(flash->indicator_node); + + return 0; +} + +static const struct i2c_device_id as3645a_id_table[] = { + { AS_NAME, 0 }, + { }, +}; +MODULE_DEVICE_TABLE(i2c, as3645a_id_table); + +static const struct of_device_id as3645a_of_table[] = { + { .compatible = "ams,as3645a" }, + { }, +}; +MODULE_DEVICE_TABLE(of, as3645a_of_table); + +static struct i2c_driver as3645a_i2c_driver = { + .driver = { + .of_match_table = as3645a_of_table, + .name = AS_NAME, + }, + .probe_new = as3645a_probe, + .remove = as3645a_remove, + .id_table = as3645a_id_table, +}; + +module_i2c_driver(as3645a_i2c_driver); + +MODULE_AUTHOR("Laurent Pinchart <laurent.pinchart@ideasonboard.com>"); +MODULE_AUTHOR("Sakari Ailus <sakari.ailus@iki.fi>"); +MODULE_DESCRIPTION("LED flash driver for AS3645A, LM3555 and their clones"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-asic3.c b/drivers/leds/leds-asic3.c new file mode 100644 index 000000000..8cbc1b8ba --- /dev/null +++ b/drivers/leds/leds-asic3.c @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2011 Paul Parsons <lost.distance@yahoo.com> + */ + +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/slab.h> + +#include <linux/mfd/asic3.h> +#include <linux/mfd/core.h> +#include <linux/module.h> + +/* + * The HTC ASIC3 LED GPIOs are inputs, not outputs. + * Hence we turn the LEDs on/off via the TimeBase register. + */ + +/* + * When TimeBase is 4 the clock resolution is about 32Hz. + * This driver supports hardware blinking with an on+off + * period from 62ms (2 clocks) to 125s (4000 clocks). + */ +#define MS_TO_CLK(ms) DIV_ROUND_CLOSEST(((ms)*1024), 32000) +#define CLK_TO_MS(clk) (((clk)*32000)/1024) +#define MAX_CLK 4000 /* Fits into 12-bit Time registers */ +#define MAX_MS CLK_TO_MS(MAX_CLK) + +static const unsigned int led_n_base[ASIC3_NUM_LEDS] = { + [0] = ASIC3_LED_0_Base, + [1] = ASIC3_LED_1_Base, + [2] = ASIC3_LED_2_Base, +}; + +static void brightness_set(struct led_classdev *cdev, + enum led_brightness value) +{ + struct platform_device *pdev = to_platform_device(cdev->dev->parent); + const struct mfd_cell *cell = mfd_get_cell(pdev); + struct asic3 *asic = dev_get_drvdata(pdev->dev.parent); + u32 timebase; + unsigned int base; + + timebase = (value == LED_OFF) ? 0 : (LED_EN|0x4); + + base = led_n_base[cell->id]; + asic3_write_register(asic, (base + ASIC3_LED_PeriodTime), 32); + asic3_write_register(asic, (base + ASIC3_LED_DutyTime), 32); + asic3_write_register(asic, (base + ASIC3_LED_AutoStopCount), 0); + asic3_write_register(asic, (base + ASIC3_LED_TimeBase), timebase); +} + +static int blink_set(struct led_classdev *cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct platform_device *pdev = to_platform_device(cdev->dev->parent); + const struct mfd_cell *cell = mfd_get_cell(pdev); + struct asic3 *asic = dev_get_drvdata(pdev->dev.parent); + u32 on; + u32 off; + unsigned int base; + + if (*delay_on > MAX_MS || *delay_off > MAX_MS) + return -EINVAL; + + if (*delay_on == 0 && *delay_off == 0) { + /* If both are zero then a sensible default should be chosen */ + on = MS_TO_CLK(500); + off = MS_TO_CLK(500); + } else { + on = MS_TO_CLK(*delay_on); + off = MS_TO_CLK(*delay_off); + if ((on + off) > MAX_CLK) + return -EINVAL; + } + + base = led_n_base[cell->id]; + asic3_write_register(asic, (base + ASIC3_LED_PeriodTime), (on + off)); + asic3_write_register(asic, (base + ASIC3_LED_DutyTime), on); + asic3_write_register(asic, (base + ASIC3_LED_AutoStopCount), 0); + asic3_write_register(asic, (base + ASIC3_LED_TimeBase), (LED_EN|0x4)); + + *delay_on = CLK_TO_MS(on); + *delay_off = CLK_TO_MS(off); + + return 0; +} + +static int asic3_led_probe(struct platform_device *pdev) +{ + struct asic3_led *led = dev_get_platdata(&pdev->dev); + int ret; + + ret = mfd_cell_enable(pdev); + if (ret < 0) + return ret; + + led->cdev = devm_kzalloc(&pdev->dev, sizeof(struct led_classdev), + GFP_KERNEL); + if (!led->cdev) { + ret = -ENOMEM; + goto out; + } + + led->cdev->name = led->name; + led->cdev->flags = LED_CORE_SUSPENDRESUME; + led->cdev->brightness_set = brightness_set; + led->cdev->blink_set = blink_set; + led->cdev->default_trigger = led->default_trigger; + + ret = led_classdev_register(&pdev->dev, led->cdev); + if (ret < 0) + goto out; + + return 0; + +out: + (void) mfd_cell_disable(pdev); + return ret; +} + +static int asic3_led_remove(struct platform_device *pdev) +{ + struct asic3_led *led = dev_get_platdata(&pdev->dev); + + led_classdev_unregister(led->cdev); + + return mfd_cell_disable(pdev); +} + +#ifdef CONFIG_PM_SLEEP +static int asic3_led_suspend(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + const struct mfd_cell *cell = mfd_get_cell(pdev); + int ret; + + ret = 0; + if (cell->suspend) + ret = (*cell->suspend)(pdev); + + return ret; +} + +static int asic3_led_resume(struct device *dev) +{ + struct platform_device *pdev = to_platform_device(dev); + const struct mfd_cell *cell = mfd_get_cell(pdev); + int ret; + + ret = 0; + if (cell->resume) + ret = (*cell->resume)(pdev); + + return ret; +} +#endif + +static SIMPLE_DEV_PM_OPS(asic3_led_pm_ops, asic3_led_suspend, asic3_led_resume); + +static struct platform_driver asic3_led_driver = { + .probe = asic3_led_probe, + .remove = asic3_led_remove, + .driver = { + .name = "leds-asic3", + .pm = &asic3_led_pm_ops, + }, +}; + +module_platform_driver(asic3_led_driver); + +MODULE_AUTHOR("Paul Parsons <lost.distance@yahoo.com>"); +MODULE_DESCRIPTION("HTC ASIC3 LED driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:leds-asic3"); diff --git a/drivers/leds/leds-aw2013.c b/drivers/leds/leds-aw2013.c new file mode 100644 index 000000000..80d937454 --- /dev/null +++ b/drivers/leds/leds-aw2013.c @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: GPL-2.0+ +// Driver for Awinic AW2013 3-channel LED driver + +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/regulator/consumer.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/regmap.h> + +#define AW2013_MAX_LEDS 3 + +/* Reset and ID register */ +#define AW2013_RSTR 0x00 +#define AW2013_RSTR_RESET 0x55 +#define AW2013_RSTR_CHIP_ID 0x33 + +/* Global control register */ +#define AW2013_GCR 0x01 +#define AW2013_GCR_ENABLE BIT(0) + +/* LED channel enable register */ +#define AW2013_LCTR 0x30 +#define AW2013_LCTR_LE(x) BIT((x)) + +/* LED channel control registers */ +#define AW2013_LCFG(x) (0x31 + (x)) +#define AW2013_LCFG_IMAX_MASK (BIT(0) | BIT(1)) // Should be 0-3 +#define AW2013_LCFG_MD BIT(4) +#define AW2013_LCFG_FI BIT(5) +#define AW2013_LCFG_FO BIT(6) + +/* LED channel PWM registers */ +#define AW2013_REG_PWM(x) (0x34 + (x)) + +/* LED channel timing registers */ +#define AW2013_LEDT0(x) (0x37 + (x) * 3) +#define AW2013_LEDT0_T1(x) ((x) << 4) // Should be 0-7 +#define AW2013_LEDT0_T2(x) (x) // Should be 0-5 + +#define AW2013_LEDT1(x) (0x38 + (x) * 3) +#define AW2013_LEDT1_T3(x) ((x) << 4) // Should be 0-7 +#define AW2013_LEDT1_T4(x) (x) // Should be 0-7 + +#define AW2013_LEDT2(x) (0x39 + (x) * 3) +#define AW2013_LEDT2_T0(x) ((x) << 4) // Should be 0-8 +#define AW2013_LEDT2_REPEAT(x) (x) // Should be 0-15 + +#define AW2013_REG_MAX 0x77 + +#define AW2013_TIME_STEP 130 /* ms */ + +struct aw2013; + +struct aw2013_led { + struct aw2013 *chip; + struct led_classdev cdev; + u32 num; + unsigned int imax; +}; + +struct aw2013 { + struct mutex mutex; /* held when writing to registers */ + struct regulator *vcc_regulator; + struct i2c_client *client; + struct aw2013_led leds[AW2013_MAX_LEDS]; + struct regmap *regmap; + int num_leds; + bool enabled; +}; + +static int aw2013_chip_init(struct aw2013 *chip) +{ + int i, ret; + + ret = regmap_write(chip->regmap, AW2013_GCR, AW2013_GCR_ENABLE); + if (ret) { + dev_err(&chip->client->dev, "Failed to enable the chip: %d\n", + ret); + return ret; + } + + for (i = 0; i < chip->num_leds; i++) { + ret = regmap_update_bits(chip->regmap, + AW2013_LCFG(chip->leds[i].num), + AW2013_LCFG_IMAX_MASK, + chip->leds[i].imax); + if (ret) { + dev_err(&chip->client->dev, + "Failed to set maximum current for led %d: %d\n", + chip->leds[i].num, ret); + return ret; + } + } + + return ret; +} + +static void aw2013_chip_disable(struct aw2013 *chip) +{ + int ret; + + if (!chip->enabled) + return; + + regmap_write(chip->regmap, AW2013_GCR, 0); + + ret = regulator_disable(chip->vcc_regulator); + if (ret) { + dev_err(&chip->client->dev, + "Failed to disable regulator: %d\n", ret); + return; + } + + chip->enabled = false; +} + +static int aw2013_chip_enable(struct aw2013 *chip) +{ + int ret; + + if (chip->enabled) + return 0; + + ret = regulator_enable(chip->vcc_regulator); + if (ret) { + dev_err(&chip->client->dev, + "Failed to enable regulator: %d\n", ret); + return ret; + } + chip->enabled = true; + + ret = aw2013_chip_init(chip); + if (ret) + aw2013_chip_disable(chip); + + return ret; +} + +static bool aw2013_chip_in_use(struct aw2013 *chip) +{ + int i; + + for (i = 0; i < chip->num_leds; i++) + if (chip->leds[i].cdev.brightness) + return true; + + return false; +} + +static int aw2013_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct aw2013_led *led = container_of(cdev, struct aw2013_led, cdev); + int ret, num; + + mutex_lock(&led->chip->mutex); + + if (aw2013_chip_in_use(led->chip)) { + ret = aw2013_chip_enable(led->chip); + if (ret) + goto error; + } + + num = led->num; + + ret = regmap_write(led->chip->regmap, AW2013_REG_PWM(num), brightness); + if (ret) + goto error; + + if (brightness) { + ret = regmap_update_bits(led->chip->regmap, AW2013_LCTR, + AW2013_LCTR_LE(num), 0xFF); + } else { + ret = regmap_update_bits(led->chip->regmap, AW2013_LCTR, + AW2013_LCTR_LE(num), 0); + if (ret) + goto error; + ret = regmap_update_bits(led->chip->regmap, AW2013_LCFG(num), + AW2013_LCFG_MD, 0); + } + if (ret) + goto error; + + if (!aw2013_chip_in_use(led->chip)) + aw2013_chip_disable(led->chip); + +error: + mutex_unlock(&led->chip->mutex); + + return ret; +} + +static int aw2013_blink_set(struct led_classdev *cdev, + unsigned long *delay_on, unsigned long *delay_off) +{ + struct aw2013_led *led = container_of(cdev, struct aw2013_led, cdev); + int ret, num = led->num; + unsigned long off = 0, on = 0; + + /* If no blink specified, default to 1 Hz. */ + if (!*delay_off && !*delay_on) { + *delay_off = 500; + *delay_on = 500; + } + + if (!led->cdev.brightness) { + led->cdev.brightness = LED_FULL; + ret = aw2013_brightness_set(&led->cdev, led->cdev.brightness); + if (ret) + return ret; + } + + /* Never on - just set to off */ + if (!*delay_on) { + led->cdev.brightness = LED_OFF; + return aw2013_brightness_set(&led->cdev, LED_OFF); + } + + mutex_lock(&led->chip->mutex); + + /* Never off - brightness is already set, disable blinking */ + if (!*delay_off) { + ret = regmap_update_bits(led->chip->regmap, AW2013_LCFG(num), + AW2013_LCFG_MD, 0); + goto out; + } + + /* Convert into values the HW will understand. */ + off = min(5, ilog2((*delay_off - 1) / AW2013_TIME_STEP) + 1); + on = min(7, ilog2((*delay_on - 1) / AW2013_TIME_STEP) + 1); + + *delay_off = BIT(off) * AW2013_TIME_STEP; + *delay_on = BIT(on) * AW2013_TIME_STEP; + + /* Set timings */ + ret = regmap_write(led->chip->regmap, + AW2013_LEDT0(num), AW2013_LEDT0_T2(on)); + if (ret) + goto out; + ret = regmap_write(led->chip->regmap, + AW2013_LEDT1(num), AW2013_LEDT1_T4(off)); + if (ret) + goto out; + + /* Finally, enable the LED */ + ret = regmap_update_bits(led->chip->regmap, AW2013_LCFG(num), + AW2013_LCFG_MD, 0xFF); + if (ret) + goto out; + + ret = regmap_update_bits(led->chip->regmap, AW2013_LCTR, + AW2013_LCTR_LE(num), 0xFF); + +out: + mutex_unlock(&led->chip->mutex); + + return ret; +} + +static int aw2013_probe_dt(struct aw2013 *chip) +{ + struct device_node *np = dev_of_node(&chip->client->dev), *child; + int count, ret = 0, i = 0; + struct aw2013_led *led; + + count = of_get_available_child_count(np); + if (!count || count > AW2013_MAX_LEDS) + return -EINVAL; + + regmap_write(chip->regmap, AW2013_RSTR, AW2013_RSTR_RESET); + + for_each_available_child_of_node(np, child) { + struct led_init_data init_data = {}; + u32 source; + u32 imax; + + ret = of_property_read_u32(child, "reg", &source); + if (ret != 0 || source >= AW2013_MAX_LEDS) { + dev_err(&chip->client->dev, + "Couldn't read LED address: %d\n", ret); + count--; + continue; + } + + led = &chip->leds[i]; + led->num = source; + led->chip = chip; + init_data.fwnode = of_fwnode_handle(child); + + if (!of_property_read_u32(child, "led-max-microamp", &imax)) { + led->imax = min_t(u32, imax / 5000, 3); + } else { + led->imax = 1; // 5mA + dev_info(&chip->client->dev, + "DT property led-max-microamp is missing\n"); + } + + led->cdev.brightness_set_blocking = aw2013_brightness_set; + led->cdev.blink_set = aw2013_blink_set; + + ret = devm_led_classdev_register_ext(&chip->client->dev, + &led->cdev, &init_data); + if (ret < 0) { + of_node_put(child); + return ret; + } + + i++; + } + + if (!count) + return -EINVAL; + + chip->num_leds = i; + + return 0; +} + +static const struct regmap_config aw2013_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = AW2013_REG_MAX, +}; + +static int aw2013_probe(struct i2c_client *client) +{ + struct aw2013 *chip; + int ret; + unsigned int chipid; + + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + mutex_init(&chip->mutex); + mutex_lock(&chip->mutex); + + chip->client = client; + i2c_set_clientdata(client, chip); + + chip->regmap = devm_regmap_init_i2c(client, &aw2013_regmap_config); + if (IS_ERR(chip->regmap)) { + ret = PTR_ERR(chip->regmap); + dev_err(&client->dev, "Failed to allocate register map: %d\n", + ret); + goto error; + } + + chip->vcc_regulator = devm_regulator_get(&client->dev, "vcc"); + ret = PTR_ERR_OR_ZERO(chip->vcc_regulator); + if (ret) { + if (ret != -EPROBE_DEFER) + dev_err(&client->dev, + "Failed to request regulator: %d\n", ret); + goto error; + } + + ret = regulator_enable(chip->vcc_regulator); + if (ret) { + dev_err(&client->dev, + "Failed to enable regulator: %d\n", ret); + goto error; + } + + ret = regmap_read(chip->regmap, AW2013_RSTR, &chipid); + if (ret) { + dev_err(&client->dev, "Failed to read chip ID: %d\n", + ret); + goto error_reg; + } + + if (chipid != AW2013_RSTR_CHIP_ID) { + dev_err(&client->dev, "Chip reported wrong ID: %x\n", + chipid); + ret = -ENODEV; + goto error_reg; + } + + ret = aw2013_probe_dt(chip); + if (ret < 0) + goto error_reg; + + ret = regulator_disable(chip->vcc_regulator); + if (ret) { + dev_err(&client->dev, + "Failed to disable regulator: %d\n", ret); + goto error; + } + + mutex_unlock(&chip->mutex); + + return 0; + +error_reg: + regulator_disable(chip->vcc_regulator); + +error: + mutex_destroy(&chip->mutex); + return ret; +} + +static int aw2013_remove(struct i2c_client *client) +{ + struct aw2013 *chip = i2c_get_clientdata(client); + + aw2013_chip_disable(chip); + + mutex_destroy(&chip->mutex); + + return 0; +} + +static const struct of_device_id aw2013_match_table[] = { + { .compatible = "awinic,aw2013", }, + { /* sentinel */ }, +}; + +MODULE_DEVICE_TABLE(of, aw2013_match_table); + +static struct i2c_driver aw2013_driver = { + .driver = { + .name = "leds-aw2013", + .of_match_table = of_match_ptr(aw2013_match_table), + }, + .probe_new = aw2013_probe, + .remove = aw2013_remove, +}; + +module_i2c_driver(aw2013_driver); + +MODULE_AUTHOR("Nikita Travkin <nikitos.tr@gmail.com>"); +MODULE_DESCRIPTION("AW2013 LED driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-bcm6328.c b/drivers/leds/leds-bcm6328.c new file mode 100644 index 000000000..226d17d25 --- /dev/null +++ b/drivers/leds/leds-bcm6328.c @@ -0,0 +1,487 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Driver for BCM6328 memory-mapped LEDs, based on leds-syscon.c + * + * Copyright 2015 Álvaro Fernández Rojas <noltari@gmail.com> + * Copyright 2015 Jonas Gorski <jogo@openwrt.org> + */ +#include <linux/io.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/spinlock.h> + +#define BCM6328_REG_INIT 0x00 +#define BCM6328_REG_MODE_HI 0x04 +#define BCM6328_REG_MODE_LO 0x08 +#define BCM6328_REG_HWDIS 0x0c +#define BCM6328_REG_STROBE 0x10 +#define BCM6328_REG_LNKACTSEL_HI 0x14 +#define BCM6328_REG_LNKACTSEL_LO 0x18 +#define BCM6328_REG_RBACK 0x1c +#define BCM6328_REG_SERMUX 0x20 + +#define BCM6328_LED_MAX_COUNT 24 +#define BCM6328_LED_DEF_DELAY 500 + +#define BCM6328_LED_BLINK_DELAYS 2 +#define BCM6328_LED_BLINK_MS 20 + +#define BCM6328_LED_BLINK_MASK 0x3f +#define BCM6328_LED_BLINK1_SHIFT 0 +#define BCM6328_LED_BLINK1_MASK (BCM6328_LED_BLINK_MASK << \ + BCM6328_LED_BLINK1_SHIFT) +#define BCM6328_LED_BLINK2_SHIFT 6 +#define BCM6328_LED_BLINK2_MASK (BCM6328_LED_BLINK_MASK << \ + BCM6328_LED_BLINK2_SHIFT) +#define BCM6328_SERIAL_LED_EN BIT(12) +#define BCM6328_SERIAL_LED_MUX BIT(13) +#define BCM6328_SERIAL_LED_CLK_NPOL BIT(14) +#define BCM6328_SERIAL_LED_DATA_PPOL BIT(15) +#define BCM6328_SERIAL_LED_SHIFT_DIR BIT(16) +#define BCM6328_LED_SHIFT_TEST BIT(30) +#define BCM6328_LED_TEST BIT(31) +#define BCM6328_INIT_MASK (BCM6328_SERIAL_LED_EN | \ + BCM6328_SERIAL_LED_MUX | \ + BCM6328_SERIAL_LED_CLK_NPOL | \ + BCM6328_SERIAL_LED_DATA_PPOL | \ + BCM6328_SERIAL_LED_SHIFT_DIR) + +#define BCM6328_LED_MODE_MASK 3 +#define BCM6328_LED_MODE_ON 0 +#define BCM6328_LED_MODE_BLINK1 1 +#define BCM6328_LED_MODE_BLINK2 2 +#define BCM6328_LED_MODE_OFF 3 +#define BCM6328_LED_SHIFT(X) ((X) << 1) + +/** + * struct bcm6328_led - state container for bcm6328 based LEDs + * @cdev: LED class device for this LED + * @mem: memory resource + * @lock: memory lock + * @pin: LED pin number + * @blink_leds: blinking LEDs + * @blink_delay: blinking delay + * @active_low: LED is active low + */ +struct bcm6328_led { + struct led_classdev cdev; + void __iomem *mem; + spinlock_t *lock; + unsigned long pin; + unsigned long *blink_leds; + unsigned long *blink_delay; + bool active_low; +}; + +static void bcm6328_led_write(void __iomem *reg, unsigned long data) +{ +#ifdef CONFIG_CPU_BIG_ENDIAN + iowrite32be(data, reg); +#else + writel(data, reg); +#endif +} + +static unsigned long bcm6328_led_read(void __iomem *reg) +{ +#ifdef CONFIG_CPU_BIG_ENDIAN + return ioread32be(reg); +#else + return readl(reg); +#endif +} + +/** + * LEDMode 64 bits / 24 LEDs + * bits [31:0] -> LEDs 8-23 + * bits [47:32] -> LEDs 0-7 + * bits [63:48] -> unused + */ +static unsigned long bcm6328_pin2shift(unsigned long pin) +{ + if (pin < 8) + return pin + 16; /* LEDs 0-7 (bits 47:32) */ + else + return pin - 8; /* LEDs 8-23 (bits 31:0) */ +} + +static void bcm6328_led_mode(struct bcm6328_led *led, unsigned long value) +{ + void __iomem *mode; + unsigned long val, shift; + + shift = bcm6328_pin2shift(led->pin); + if (shift / 16) + mode = led->mem + BCM6328_REG_MODE_HI; + else + mode = led->mem + BCM6328_REG_MODE_LO; + + val = bcm6328_led_read(mode); + val &= ~(BCM6328_LED_MODE_MASK << BCM6328_LED_SHIFT(shift % 16)); + val |= (value << BCM6328_LED_SHIFT(shift % 16)); + bcm6328_led_write(mode, val); +} + +static void bcm6328_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct bcm6328_led *led = + container_of(led_cdev, struct bcm6328_led, cdev); + unsigned long flags; + + spin_lock_irqsave(led->lock, flags); + + /* Remove LED from cached HW blinking intervals */ + led->blink_leds[0] &= ~BIT(led->pin); + led->blink_leds[1] &= ~BIT(led->pin); + + /* Set LED on/off */ + if ((led->active_low && value == LED_OFF) || + (!led->active_low && value != LED_OFF)) + bcm6328_led_mode(led, BCM6328_LED_MODE_ON); + else + bcm6328_led_mode(led, BCM6328_LED_MODE_OFF); + + spin_unlock_irqrestore(led->lock, flags); +} + +static unsigned long bcm6328_blink_delay(unsigned long delay) +{ + unsigned long bcm6328_delay; + + bcm6328_delay = delay + BCM6328_LED_BLINK_MS / 2; + bcm6328_delay = bcm6328_delay / BCM6328_LED_BLINK_MS; + if (bcm6328_delay == 0) + bcm6328_delay = 1; + + return bcm6328_delay; +} + +static int bcm6328_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, unsigned long *delay_off) +{ + struct bcm6328_led *led = + container_of(led_cdev, struct bcm6328_led, cdev); + unsigned long delay, flags; + int rc; + + if (!*delay_on) + *delay_on = BCM6328_LED_DEF_DELAY; + if (!*delay_off) + *delay_off = BCM6328_LED_DEF_DELAY; + + delay = bcm6328_blink_delay(*delay_on); + if (delay != bcm6328_blink_delay(*delay_off)) { + dev_dbg(led_cdev->dev, + "fallback to soft blinking (delay_on != delay_off)\n"); + return -EINVAL; + } + + if (delay > BCM6328_LED_BLINK_MASK) { + dev_dbg(led_cdev->dev, + "fallback to soft blinking (delay > %ums)\n", + BCM6328_LED_BLINK_MASK * BCM6328_LED_BLINK_MS); + return -EINVAL; + } + + spin_lock_irqsave(led->lock, flags); + /* + * Check if any of the two configurable HW blinking intervals is + * available: + * 1. No LEDs assigned to the HW blinking interval. + * 2. Only this LED is assigned to the HW blinking interval. + * 3. LEDs with the same delay assigned. + */ + if (led->blink_leds[0] == 0 || + led->blink_leds[0] == BIT(led->pin) || + led->blink_delay[0] == delay) { + unsigned long val; + + /* Add LED to the first HW blinking interval cache */ + led->blink_leds[0] |= BIT(led->pin); + + /* Remove LED from the second HW blinking interval cache */ + led->blink_leds[1] &= ~BIT(led->pin); + + /* Cache first HW blinking interval delay */ + led->blink_delay[0] = delay; + + /* Update the delay for the first HW blinking interval */ + val = bcm6328_led_read(led->mem + BCM6328_REG_INIT); + val &= ~BCM6328_LED_BLINK1_MASK; + val |= (delay << BCM6328_LED_BLINK1_SHIFT); + bcm6328_led_write(led->mem + BCM6328_REG_INIT, val); + + /* Set the LED to first HW blinking interval */ + bcm6328_led_mode(led, BCM6328_LED_MODE_BLINK1); + + rc = 0; + } else if (led->blink_leds[1] == 0 || + led->blink_leds[1] == BIT(led->pin) || + led->blink_delay[1] == delay) { + unsigned long val; + + /* Remove LED from the first HW blinking interval */ + led->blink_leds[0] &= ~BIT(led->pin); + + /* Add LED to the second HW blinking interval */ + led->blink_leds[1] |= BIT(led->pin); + + /* Cache second HW blinking interval delay */ + led->blink_delay[1] = delay; + + /* Update the delay for the second HW blinking interval */ + val = bcm6328_led_read(led->mem + BCM6328_REG_INIT); + val &= ~BCM6328_LED_BLINK2_MASK; + val |= (delay << BCM6328_LED_BLINK2_SHIFT); + bcm6328_led_write(led->mem + BCM6328_REG_INIT, val); + + /* Set the LED to second HW blinking interval */ + bcm6328_led_mode(led, BCM6328_LED_MODE_BLINK2); + + rc = 0; + } else { + dev_dbg(led_cdev->dev, + "fallback to soft blinking (delay already set)\n"); + rc = -EINVAL; + } + spin_unlock_irqrestore(led->lock, flags); + + return rc; +} + +static int bcm6328_hwled(struct device *dev, struct device_node *nc, u32 reg, + void __iomem *mem, spinlock_t *lock) +{ + int i, cnt; + unsigned long flags, val; + + spin_lock_irqsave(lock, flags); + val = bcm6328_led_read(mem + BCM6328_REG_HWDIS); + val &= ~BIT(reg); + bcm6328_led_write(mem + BCM6328_REG_HWDIS, val); + spin_unlock_irqrestore(lock, flags); + + /* Only LEDs 0-7 can be activity/link controlled */ + if (reg >= 8) + return 0; + + cnt = of_property_count_elems_of_size(nc, "brcm,link-signal-sources", + sizeof(u32)); + for (i = 0; i < cnt; i++) { + u32 sel; + void __iomem *addr; + + if (reg < 4) + addr = mem + BCM6328_REG_LNKACTSEL_LO; + else + addr = mem + BCM6328_REG_LNKACTSEL_HI; + + of_property_read_u32_index(nc, "brcm,link-signal-sources", i, + &sel); + + if (reg / 4 != sel / 4) { + dev_warn(dev, "invalid link signal source\n"); + continue; + } + + spin_lock_irqsave(lock, flags); + val = bcm6328_led_read(addr); + val |= (BIT(reg % 4) << (((sel % 4) * 4) + 16)); + bcm6328_led_write(addr, val); + spin_unlock_irqrestore(lock, flags); + } + + cnt = of_property_count_elems_of_size(nc, + "brcm,activity-signal-sources", + sizeof(u32)); + for (i = 0; i < cnt; i++) { + u32 sel; + void __iomem *addr; + + if (reg < 4) + addr = mem + BCM6328_REG_LNKACTSEL_LO; + else + addr = mem + BCM6328_REG_LNKACTSEL_HI; + + of_property_read_u32_index(nc, "brcm,activity-signal-sources", + i, &sel); + + if (reg / 4 != sel / 4) { + dev_warn(dev, "invalid activity signal source\n"); + continue; + } + + spin_lock_irqsave(lock, flags); + val = bcm6328_led_read(addr); + val |= (BIT(reg % 4) << ((sel % 4) * 4)); + bcm6328_led_write(addr, val); + spin_unlock_irqrestore(lock, flags); + } + + return 0; +} + +static int bcm6328_led(struct device *dev, struct device_node *nc, u32 reg, + void __iomem *mem, spinlock_t *lock, + unsigned long *blink_leds, unsigned long *blink_delay) +{ + struct led_init_data init_data = {}; + struct bcm6328_led *led; + const char *state; + int rc; + + led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->pin = reg; + led->mem = mem; + led->lock = lock; + led->blink_leds = blink_leds; + led->blink_delay = blink_delay; + + if (of_property_read_bool(nc, "active-low")) + led->active_low = true; + + if (!of_property_read_string(nc, "default-state", &state)) { + if (!strcmp(state, "on")) { + led->cdev.brightness = LED_FULL; + } else if (!strcmp(state, "keep")) { + void __iomem *mode; + unsigned long val, shift; + + shift = bcm6328_pin2shift(led->pin); + if (shift / 16) + mode = mem + BCM6328_REG_MODE_HI; + else + mode = mem + BCM6328_REG_MODE_LO; + + val = bcm6328_led_read(mode) >> + BCM6328_LED_SHIFT(shift % 16); + val &= BCM6328_LED_MODE_MASK; + if ((led->active_low && val == BCM6328_LED_MODE_OFF) || + (!led->active_low && val == BCM6328_LED_MODE_ON)) + led->cdev.brightness = LED_FULL; + else + led->cdev.brightness = LED_OFF; + } else { + led->cdev.brightness = LED_OFF; + } + } else { + led->cdev.brightness = LED_OFF; + } + + bcm6328_led_set(&led->cdev, led->cdev.brightness); + + led->cdev.brightness_set = bcm6328_led_set; + led->cdev.blink_set = bcm6328_blink_set; + init_data.fwnode = of_fwnode_handle(nc); + + rc = devm_led_classdev_register_ext(dev, &led->cdev, &init_data); + if (rc < 0) + return rc; + + dev_dbg(dev, "registered LED %s\n", led->cdev.name); + + return 0; +} + +static int bcm6328_leds_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *np = dev_of_node(&pdev->dev); + struct device_node *child; + void __iomem *mem; + spinlock_t *lock; /* memory lock */ + unsigned long val, *blink_leds, *blink_delay; + + mem = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(mem)) + return PTR_ERR(mem); + + lock = devm_kzalloc(dev, sizeof(*lock), GFP_KERNEL); + if (!lock) + return -ENOMEM; + + blink_leds = devm_kcalloc(dev, BCM6328_LED_BLINK_DELAYS, + sizeof(*blink_leds), GFP_KERNEL); + if (!blink_leds) + return -ENOMEM; + + blink_delay = devm_kcalloc(dev, BCM6328_LED_BLINK_DELAYS, + sizeof(*blink_delay), GFP_KERNEL); + if (!blink_delay) + return -ENOMEM; + + spin_lock_init(lock); + + bcm6328_led_write(mem + BCM6328_REG_HWDIS, ~0); + bcm6328_led_write(mem + BCM6328_REG_LNKACTSEL_HI, 0); + bcm6328_led_write(mem + BCM6328_REG_LNKACTSEL_LO, 0); + + val = bcm6328_led_read(mem + BCM6328_REG_INIT); + val &= ~(BCM6328_INIT_MASK); + if (of_property_read_bool(np, "brcm,serial-leds")) + val |= BCM6328_SERIAL_LED_EN; + if (of_property_read_bool(np, "brcm,serial-mux")) + val |= BCM6328_SERIAL_LED_MUX; + if (of_property_read_bool(np, "brcm,serial-clk-low")) + val |= BCM6328_SERIAL_LED_CLK_NPOL; + if (!of_property_read_bool(np, "brcm,serial-dat-low")) + val |= BCM6328_SERIAL_LED_DATA_PPOL; + if (!of_property_read_bool(np, "brcm,serial-shift-inv")) + val |= BCM6328_SERIAL_LED_SHIFT_DIR; + bcm6328_led_write(mem + BCM6328_REG_INIT, val); + + for_each_available_child_of_node(np, child) { + int rc; + u32 reg; + + if (of_property_read_u32(child, "reg", ®)) + continue; + + if (reg >= BCM6328_LED_MAX_COUNT) { + dev_err(dev, "invalid LED (%u >= %d)\n", reg, + BCM6328_LED_MAX_COUNT); + continue; + } + + if (of_property_read_bool(child, "brcm,hardware-controlled")) + rc = bcm6328_hwled(dev, child, reg, mem, lock); + else + rc = bcm6328_led(dev, child, reg, mem, lock, + blink_leds, blink_delay); + + if (rc < 0) { + of_node_put(child); + return rc; + } + } + + return 0; +} + +static const struct of_device_id bcm6328_leds_of_match[] = { + { .compatible = "brcm,bcm6328-leds", }, + { }, +}; +MODULE_DEVICE_TABLE(of, bcm6328_leds_of_match); + +static struct platform_driver bcm6328_leds_driver = { + .probe = bcm6328_leds_probe, + .driver = { + .name = "leds-bcm6328", + .of_match_table = bcm6328_leds_of_match, + }, +}; + +module_platform_driver(bcm6328_leds_driver); + +MODULE_AUTHOR("Álvaro Fernández Rojas <noltari@gmail.com>"); +MODULE_AUTHOR("Jonas Gorski <jogo@openwrt.org>"); +MODULE_DESCRIPTION("LED driver for BCM6328 controllers"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:leds-bcm6328"); diff --git a/drivers/leds/leds-bcm6358.c b/drivers/leds/leds-bcm6358.c new file mode 100644 index 000000000..9d2e487fa --- /dev/null +++ b/drivers/leds/leds-bcm6358.c @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Driver for BCM6358 memory-mapped LEDs, based on leds-syscon.c + * + * Copyright 2015 Álvaro Fernández Rojas <noltari@gmail.com> + */ +#include <linux/delay.h> +#include <linux/io.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/spinlock.h> + +#define BCM6358_REG_MODE 0x0 +#define BCM6358_REG_CTRL 0x4 + +#define BCM6358_SLED_CLKDIV_MASK 3 +#define BCM6358_SLED_CLKDIV_1 0 +#define BCM6358_SLED_CLKDIV_2 1 +#define BCM6358_SLED_CLKDIV_4 2 +#define BCM6358_SLED_CLKDIV_8 3 + +#define BCM6358_SLED_POLARITY BIT(2) +#define BCM6358_SLED_BUSY BIT(3) + +#define BCM6358_SLED_MAX_COUNT 32 +#define BCM6358_SLED_WAIT 100 + +/** + * struct bcm6358_led - state container for bcm6358 based LEDs + * @cdev: LED class device for this LED + * @mem: memory resource + * @lock: memory lock + * @pin: LED pin number + * @active_low: LED is active low + */ +struct bcm6358_led { + struct led_classdev cdev; + void __iomem *mem; + spinlock_t *lock; + unsigned long pin; + bool active_low; +}; + +static void bcm6358_led_write(void __iomem *reg, unsigned long data) +{ +#ifdef CONFIG_CPU_BIG_ENDIAN + iowrite32be(data, reg); +#else + writel(data, reg); +#endif +} + +static unsigned long bcm6358_led_read(void __iomem *reg) +{ +#ifdef CONFIG_CPU_BIG_ENDIAN + return ioread32be(reg); +#else + return readl(reg); +#endif +} + +static unsigned long bcm6358_led_busy(void __iomem *mem) +{ + unsigned long val; + + while ((val = bcm6358_led_read(mem + BCM6358_REG_CTRL)) & + BCM6358_SLED_BUSY) + udelay(BCM6358_SLED_WAIT); + + return val; +} + +static void bcm6358_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct bcm6358_led *led = + container_of(led_cdev, struct bcm6358_led, cdev); + unsigned long flags, val; + + spin_lock_irqsave(led->lock, flags); + bcm6358_led_busy(led->mem); + val = bcm6358_led_read(led->mem + BCM6358_REG_MODE); + if ((led->active_low && value == LED_OFF) || + (!led->active_low && value != LED_OFF)) + val |= BIT(led->pin); + else + val &= ~(BIT(led->pin)); + bcm6358_led_write(led->mem + BCM6358_REG_MODE, val); + spin_unlock_irqrestore(led->lock, flags); +} + +static int bcm6358_led(struct device *dev, struct device_node *nc, u32 reg, + void __iomem *mem, spinlock_t *lock) +{ + struct led_init_data init_data = {}; + struct bcm6358_led *led; + const char *state; + int rc; + + led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->pin = reg; + led->mem = mem; + led->lock = lock; + + if (of_property_read_bool(nc, "active-low")) + led->active_low = true; + + if (!of_property_read_string(nc, "default-state", &state)) { + if (!strcmp(state, "on")) { + led->cdev.brightness = LED_FULL; + } else if (!strcmp(state, "keep")) { + unsigned long val; + val = bcm6358_led_read(led->mem + BCM6358_REG_MODE); + val &= BIT(led->pin); + if ((led->active_low && !val) || + (!led->active_low && val)) + led->cdev.brightness = LED_FULL; + else + led->cdev.brightness = LED_OFF; + } else { + led->cdev.brightness = LED_OFF; + } + } else { + led->cdev.brightness = LED_OFF; + } + + bcm6358_led_set(&led->cdev, led->cdev.brightness); + + led->cdev.brightness_set = bcm6358_led_set; + init_data.fwnode = of_fwnode_handle(nc); + + rc = devm_led_classdev_register_ext(dev, &led->cdev, &init_data); + if (rc < 0) + return rc; + + dev_dbg(dev, "registered LED %s\n", led->cdev.name); + + return 0; +} + +static int bcm6358_leds_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *np = dev_of_node(&pdev->dev); + struct device_node *child; + void __iomem *mem; + spinlock_t *lock; /* memory lock */ + unsigned long val; + u32 clk_div; + + mem = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(mem)) + return PTR_ERR(mem); + + lock = devm_kzalloc(dev, sizeof(*lock), GFP_KERNEL); + if (!lock) + return -ENOMEM; + + spin_lock_init(lock); + + val = bcm6358_led_busy(mem); + val &= ~(BCM6358_SLED_POLARITY | BCM6358_SLED_CLKDIV_MASK); + if (of_property_read_bool(np, "brcm,clk-dat-low")) + val |= BCM6358_SLED_POLARITY; + of_property_read_u32(np, "brcm,clk-div", &clk_div); + switch (clk_div) { + case 8: + val |= BCM6358_SLED_CLKDIV_8; + break; + case 4: + val |= BCM6358_SLED_CLKDIV_4; + break; + case 2: + val |= BCM6358_SLED_CLKDIV_2; + break; + default: + val |= BCM6358_SLED_CLKDIV_1; + break; + } + bcm6358_led_write(mem + BCM6358_REG_CTRL, val); + + for_each_available_child_of_node(np, child) { + int rc; + u32 reg; + + if (of_property_read_u32(child, "reg", ®)) + continue; + + if (reg >= BCM6358_SLED_MAX_COUNT) { + dev_err(dev, "invalid LED (%u >= %d)\n", reg, + BCM6358_SLED_MAX_COUNT); + continue; + } + + rc = bcm6358_led(dev, child, reg, mem, lock); + if (rc < 0) { + of_node_put(child); + return rc; + } + } + + return 0; +} + +static const struct of_device_id bcm6358_leds_of_match[] = { + { .compatible = "brcm,bcm6358-leds", }, + { }, +}; +MODULE_DEVICE_TABLE(of, bcm6358_leds_of_match); + +static struct platform_driver bcm6358_leds_driver = { + .probe = bcm6358_leds_probe, + .driver = { + .name = "leds-bcm6358", + .of_match_table = bcm6358_leds_of_match, + }, +}; + +module_platform_driver(bcm6358_leds_driver); + +MODULE_AUTHOR("Álvaro Fernández Rojas <noltari@gmail.com>"); +MODULE_DESCRIPTION("LED driver for BCM6358 controllers"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:leds-bcm6358"); diff --git a/drivers/leds/leds-bd2802.c b/drivers/leds/leds-bd2802.c new file mode 100644 index 000000000..8bbaef5a2 --- /dev/null +++ b/drivers/leds/leds-bd2802.c @@ -0,0 +1,801 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * leds-bd2802.c - RGB LED Driver + * + * Copyright (C) 2009 Samsung Electronics + * Kim Kyuwon <q1.kim@samsung.com> + * + * Datasheet: http://www.rohm.com/products/databook/driver/pdf/bd2802gu-e.pdf + */ + +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/gpio/consumer.h> +#include <linux/delay.h> +#include <linux/leds.h> +#include <linux/leds-bd2802.h> +#include <linux/slab.h> +#include <linux/pm.h> + +#define LED_CTL(rgb2en, rgb1en) ((rgb2en) << 4 | ((rgb1en) << 0)) + +#define BD2802_LED_OFFSET 0xa +#define BD2802_COLOR_OFFSET 0x3 + +#define BD2802_REG_CLKSETUP 0x00 +#define BD2802_REG_CONTROL 0x01 +#define BD2802_REG_HOURSETUP 0x02 +#define BD2802_REG_CURRENT1SETUP 0x03 +#define BD2802_REG_CURRENT2SETUP 0x04 +#define BD2802_REG_WAVEPATTERN 0x05 + +#define BD2802_CURRENT_032 0x10 /* 3.2mA */ +#define BD2802_CURRENT_000 0x00 /* 0.0mA */ + +#define BD2802_PATTERN_FULL 0x07 +#define BD2802_PATTERN_HALF 0x03 + +enum led_ids { + LED1, + LED2, + LED_NUM, +}; + +enum led_colors { + RED, + GREEN, + BLUE, +}; + +enum led_bits { + BD2802_OFF, + BD2802_BLINK, + BD2802_ON, +}; + +/* + * State '0' : 'off' + * State '1' : 'blink' + * State '2' : 'on'. + */ +struct led_state { + unsigned r:2; + unsigned g:2; + unsigned b:2; +}; + +struct bd2802_led { + struct bd2802_led_platform_data *pdata; + struct i2c_client *client; + struct gpio_desc *reset; + struct rw_semaphore rwsem; + + struct led_state led[2]; + + /* + * Making led_classdev as array is not recommended, because array + * members prevent using 'container_of' macro. So repetitive works + * are needed. + */ + struct led_classdev cdev_led1r; + struct led_classdev cdev_led1g; + struct led_classdev cdev_led1b; + struct led_classdev cdev_led2r; + struct led_classdev cdev_led2g; + struct led_classdev cdev_led2b; + + /* + * Advanced Configuration Function(ADF) mode: + * In ADF mode, user can set registers of BD2802GU directly, + * therefore BD2802GU doesn't enter reset state. + */ + int adf_on; + + enum led_ids led_id; + enum led_colors color; + enum led_bits state; + + /* General attributes of RGB LEDs */ + int wave_pattern; + int rgb_current; +}; + + +/*--------------------------------------------------------------*/ +/* BD2802GU helper functions */ +/*--------------------------------------------------------------*/ + +static inline int bd2802_is_rgb_off(struct bd2802_led *led, enum led_ids id, + enum led_colors color) +{ + switch (color) { + case RED: + return !led->led[id].r; + case GREEN: + return !led->led[id].g; + case BLUE: + return !led->led[id].b; + default: + dev_err(&led->client->dev, "%s: Invalid color\n", __func__); + return -EINVAL; + } +} + +static inline int bd2802_is_led_off(struct bd2802_led *led, enum led_ids id) +{ + if (led->led[id].r || led->led[id].g || led->led[id].b) + return 0; + + return 1; +} + +static inline int bd2802_is_all_off(struct bd2802_led *led) +{ + int i; + + for (i = 0; i < LED_NUM; i++) + if (!bd2802_is_led_off(led, i)) + return 0; + + return 1; +} + +static inline u8 bd2802_get_base_offset(enum led_ids id, enum led_colors color) +{ + return id * BD2802_LED_OFFSET + color * BD2802_COLOR_OFFSET; +} + +static inline u8 bd2802_get_reg_addr(enum led_ids id, enum led_colors color, + u8 reg_offset) +{ + return reg_offset + bd2802_get_base_offset(id, color); +} + + +/*--------------------------------------------------------------*/ +/* BD2802GU core functions */ +/*--------------------------------------------------------------*/ + +static int bd2802_write_byte(struct i2c_client *client, u8 reg, u8 val) +{ + int ret = i2c_smbus_write_byte_data(client, reg, val); + if (ret >= 0) + return 0; + + dev_err(&client->dev, "%s: reg 0x%x, val 0x%x, err %d\n", + __func__, reg, val, ret); + + return ret; +} + +static void bd2802_update_state(struct bd2802_led *led, enum led_ids id, + enum led_colors color, enum led_bits led_bit) +{ + int i; + u8 value; + + for (i = 0; i < LED_NUM; i++) { + if (i == id) { + switch (color) { + case RED: + led->led[i].r = led_bit; + break; + case GREEN: + led->led[i].g = led_bit; + break; + case BLUE: + led->led[i].b = led_bit; + break; + default: + dev_err(&led->client->dev, + "%s: Invalid color\n", __func__); + return; + } + } + } + + if (led_bit == BD2802_BLINK || led_bit == BD2802_ON) + return; + + if (!bd2802_is_led_off(led, id)) + return; + + if (bd2802_is_all_off(led) && !led->adf_on) { + gpiod_set_value(led->reset, 1); + return; + } + + /* + * In this case, other led is turned on, and current led is turned + * off. So set RGB LED Control register to stop the current RGB LED + */ + value = (id == LED1) ? LED_CTL(1, 0) : LED_CTL(0, 1); + bd2802_write_byte(led->client, BD2802_REG_CONTROL, value); +} + +static void bd2802_configure(struct bd2802_led *led) +{ + struct bd2802_led_platform_data *pdata = led->pdata; + u8 reg; + + reg = bd2802_get_reg_addr(LED1, RED, BD2802_REG_HOURSETUP); + bd2802_write_byte(led->client, reg, pdata->rgb_time); + + reg = bd2802_get_reg_addr(LED2, RED, BD2802_REG_HOURSETUP); + bd2802_write_byte(led->client, reg, pdata->rgb_time); +} + +static void bd2802_reset_cancel(struct bd2802_led *led) +{ + gpiod_set_value(led->reset, 0); + udelay(100); + bd2802_configure(led); +} + +static void bd2802_enable(struct bd2802_led *led, enum led_ids id) +{ + enum led_ids other_led = (id == LED1) ? LED2 : LED1; + u8 value, other_led_on; + + other_led_on = !bd2802_is_led_off(led, other_led); + if (id == LED1) + value = LED_CTL(other_led_on, 1); + else + value = LED_CTL(1 , other_led_on); + + bd2802_write_byte(led->client, BD2802_REG_CONTROL, value); +} + +static void bd2802_set_on(struct bd2802_led *led, enum led_ids id, + enum led_colors color) +{ + u8 reg; + + if (bd2802_is_all_off(led) && !led->adf_on) + bd2802_reset_cancel(led); + + reg = bd2802_get_reg_addr(id, color, BD2802_REG_CURRENT1SETUP); + bd2802_write_byte(led->client, reg, led->rgb_current); + reg = bd2802_get_reg_addr(id, color, BD2802_REG_CURRENT2SETUP); + bd2802_write_byte(led->client, reg, BD2802_CURRENT_000); + reg = bd2802_get_reg_addr(id, color, BD2802_REG_WAVEPATTERN); + bd2802_write_byte(led->client, reg, BD2802_PATTERN_FULL); + + bd2802_enable(led, id); + bd2802_update_state(led, id, color, BD2802_ON); +} + +static void bd2802_set_blink(struct bd2802_led *led, enum led_ids id, + enum led_colors color) +{ + u8 reg; + + if (bd2802_is_all_off(led) && !led->adf_on) + bd2802_reset_cancel(led); + + reg = bd2802_get_reg_addr(id, color, BD2802_REG_CURRENT1SETUP); + bd2802_write_byte(led->client, reg, BD2802_CURRENT_000); + reg = bd2802_get_reg_addr(id, color, BD2802_REG_CURRENT2SETUP); + bd2802_write_byte(led->client, reg, led->rgb_current); + reg = bd2802_get_reg_addr(id, color, BD2802_REG_WAVEPATTERN); + bd2802_write_byte(led->client, reg, led->wave_pattern); + + bd2802_enable(led, id); + bd2802_update_state(led, id, color, BD2802_BLINK); +} + +static void bd2802_turn_on(struct bd2802_led *led, enum led_ids id, + enum led_colors color, enum led_bits led_bit) +{ + if (led_bit == BD2802_OFF) { + dev_err(&led->client->dev, + "Only 'blink' and 'on' are allowed\n"); + return; + } + + if (led_bit == BD2802_BLINK) + bd2802_set_blink(led, id, color); + else + bd2802_set_on(led, id, color); +} + +static void bd2802_turn_off(struct bd2802_led *led, enum led_ids id, + enum led_colors color) +{ + u8 reg; + + if (bd2802_is_rgb_off(led, id, color)) + return; + + reg = bd2802_get_reg_addr(id, color, BD2802_REG_CURRENT1SETUP); + bd2802_write_byte(led->client, reg, BD2802_CURRENT_000); + reg = bd2802_get_reg_addr(id, color, BD2802_REG_CURRENT2SETUP); + bd2802_write_byte(led->client, reg, BD2802_CURRENT_000); + + bd2802_update_state(led, id, color, BD2802_OFF); +} + +#define BD2802_SET_REGISTER(reg_addr, reg_name) \ +static ssize_t bd2802_store_reg##reg_addr(struct device *dev, \ + struct device_attribute *attr, const char *buf, size_t count) \ +{ \ + struct bd2802_led *led = i2c_get_clientdata(to_i2c_client(dev));\ + unsigned long val; \ + int ret; \ + if (!count) \ + return -EINVAL; \ + ret = kstrtoul(buf, 16, &val); \ + if (ret) \ + return ret; \ + down_write(&led->rwsem); \ + bd2802_write_byte(led->client, reg_addr, (u8) val); \ + up_write(&led->rwsem); \ + return count; \ +} \ +static struct device_attribute bd2802_reg##reg_addr##_attr = { \ + .attr = {.name = reg_name, .mode = 0644}, \ + .store = bd2802_store_reg##reg_addr, \ +}; + +BD2802_SET_REGISTER(0x00, "0x00"); +BD2802_SET_REGISTER(0x01, "0x01"); +BD2802_SET_REGISTER(0x02, "0x02"); +BD2802_SET_REGISTER(0x03, "0x03"); +BD2802_SET_REGISTER(0x04, "0x04"); +BD2802_SET_REGISTER(0x05, "0x05"); +BD2802_SET_REGISTER(0x06, "0x06"); +BD2802_SET_REGISTER(0x07, "0x07"); +BD2802_SET_REGISTER(0x08, "0x08"); +BD2802_SET_REGISTER(0x09, "0x09"); +BD2802_SET_REGISTER(0x0a, "0x0a"); +BD2802_SET_REGISTER(0x0b, "0x0b"); +BD2802_SET_REGISTER(0x0c, "0x0c"); +BD2802_SET_REGISTER(0x0d, "0x0d"); +BD2802_SET_REGISTER(0x0e, "0x0e"); +BD2802_SET_REGISTER(0x0f, "0x0f"); +BD2802_SET_REGISTER(0x10, "0x10"); +BD2802_SET_REGISTER(0x11, "0x11"); +BD2802_SET_REGISTER(0x12, "0x12"); +BD2802_SET_REGISTER(0x13, "0x13"); +BD2802_SET_REGISTER(0x14, "0x14"); +BD2802_SET_REGISTER(0x15, "0x15"); + +static struct device_attribute *bd2802_addr_attributes[] = { + &bd2802_reg0x00_attr, + &bd2802_reg0x01_attr, + &bd2802_reg0x02_attr, + &bd2802_reg0x03_attr, + &bd2802_reg0x04_attr, + &bd2802_reg0x05_attr, + &bd2802_reg0x06_attr, + &bd2802_reg0x07_attr, + &bd2802_reg0x08_attr, + &bd2802_reg0x09_attr, + &bd2802_reg0x0a_attr, + &bd2802_reg0x0b_attr, + &bd2802_reg0x0c_attr, + &bd2802_reg0x0d_attr, + &bd2802_reg0x0e_attr, + &bd2802_reg0x0f_attr, + &bd2802_reg0x10_attr, + &bd2802_reg0x11_attr, + &bd2802_reg0x12_attr, + &bd2802_reg0x13_attr, + &bd2802_reg0x14_attr, + &bd2802_reg0x15_attr, +}; + +static void bd2802_enable_adv_conf(struct bd2802_led *led) +{ + int i, ret; + + for (i = 0; i < ARRAY_SIZE(bd2802_addr_attributes); i++) { + ret = device_create_file(&led->client->dev, + bd2802_addr_attributes[i]); + if (ret) { + dev_err(&led->client->dev, "failed: sysfs file %s\n", + bd2802_addr_attributes[i]->attr.name); + goto failed_remove_files; + } + } + + if (bd2802_is_all_off(led)) + bd2802_reset_cancel(led); + + led->adf_on = 1; + + return; + +failed_remove_files: + for (i--; i >= 0; i--) + device_remove_file(&led->client->dev, + bd2802_addr_attributes[i]); +} + +static void bd2802_disable_adv_conf(struct bd2802_led *led) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(bd2802_addr_attributes); i++) + device_remove_file(&led->client->dev, + bd2802_addr_attributes[i]); + + if (bd2802_is_all_off(led)) + gpiod_set_value(led->reset, 1); + + led->adf_on = 0; +} + +static ssize_t bd2802_show_adv_conf(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct bd2802_led *led = i2c_get_clientdata(to_i2c_client(dev)); + ssize_t ret; + + down_read(&led->rwsem); + if (led->adf_on) + ret = sprintf(buf, "on\n"); + else + ret = sprintf(buf, "off\n"); + up_read(&led->rwsem); + + return ret; +} + +static ssize_t bd2802_store_adv_conf(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + struct bd2802_led *led = i2c_get_clientdata(to_i2c_client(dev)); + + if (!count) + return -EINVAL; + + down_write(&led->rwsem); + if (!led->adf_on && !strncmp(buf, "on", 2)) + bd2802_enable_adv_conf(led); + else if (led->adf_on && !strncmp(buf, "off", 3)) + bd2802_disable_adv_conf(led); + up_write(&led->rwsem); + + return count; +} + +static struct device_attribute bd2802_adv_conf_attr = { + .attr = { + .name = "advanced_configuration", + .mode = 0644, + }, + .show = bd2802_show_adv_conf, + .store = bd2802_store_adv_conf, +}; + +#define BD2802_CONTROL_ATTR(attr_name, name_str) \ +static ssize_t bd2802_show_##attr_name(struct device *dev, \ + struct device_attribute *attr, char *buf) \ +{ \ + struct bd2802_led *led = i2c_get_clientdata(to_i2c_client(dev));\ + ssize_t ret; \ + down_read(&led->rwsem); \ + ret = sprintf(buf, "0x%02x\n", led->attr_name); \ + up_read(&led->rwsem); \ + return ret; \ +} \ +static ssize_t bd2802_store_##attr_name(struct device *dev, \ + struct device_attribute *attr, const char *buf, size_t count) \ +{ \ + struct bd2802_led *led = i2c_get_clientdata(to_i2c_client(dev));\ + unsigned long val; \ + int ret; \ + if (!count) \ + return -EINVAL; \ + ret = kstrtoul(buf, 16, &val); \ + if (ret) \ + return ret; \ + down_write(&led->rwsem); \ + led->attr_name = val; \ + up_write(&led->rwsem); \ + return count; \ +} \ +static struct device_attribute bd2802_##attr_name##_attr = { \ + .attr = { \ + .name = name_str, \ + .mode = 0644, \ + }, \ + .show = bd2802_show_##attr_name, \ + .store = bd2802_store_##attr_name, \ +}; + +BD2802_CONTROL_ATTR(wave_pattern, "wave_pattern"); +BD2802_CONTROL_ATTR(rgb_current, "rgb_current"); + +static struct device_attribute *bd2802_attributes[] = { + &bd2802_adv_conf_attr, + &bd2802_wave_pattern_attr, + &bd2802_rgb_current_attr, +}; + +#define BD2802_CONTROL_RGBS(name, id, clr) \ +static int bd2802_set_##name##_brightness(struct led_classdev *led_cdev,\ + enum led_brightness value) \ +{ \ + struct bd2802_led *led = \ + container_of(led_cdev, struct bd2802_led, cdev_##name); \ + led->led_id = id; \ + led->color = clr; \ + if (value == LED_OFF) { \ + led->state = BD2802_OFF; \ + bd2802_turn_off(led, led->led_id, led->color); \ + } else { \ + led->state = BD2802_ON; \ + bd2802_turn_on(led, led->led_id, led->color, BD2802_ON);\ + } \ + return 0; \ +} \ +static int bd2802_set_##name##_blink(struct led_classdev *led_cdev, \ + unsigned long *delay_on, unsigned long *delay_off) \ +{ \ + struct bd2802_led *led = \ + container_of(led_cdev, struct bd2802_led, cdev_##name); \ + if (*delay_on == 0 || *delay_off == 0) \ + return -EINVAL; \ + led->led_id = id; \ + led->color = clr; \ + led->state = BD2802_BLINK; \ + bd2802_turn_on(led, led->led_id, led->color, BD2802_BLINK); \ + return 0; \ +} + +BD2802_CONTROL_RGBS(led1r, LED1, RED); +BD2802_CONTROL_RGBS(led1g, LED1, GREEN); +BD2802_CONTROL_RGBS(led1b, LED1, BLUE); +BD2802_CONTROL_RGBS(led2r, LED2, RED); +BD2802_CONTROL_RGBS(led2g, LED2, GREEN); +BD2802_CONTROL_RGBS(led2b, LED2, BLUE); + +static int bd2802_register_led_classdev(struct bd2802_led *led) +{ + int ret; + + led->cdev_led1r.name = "led1_R"; + led->cdev_led1r.brightness = LED_OFF; + led->cdev_led1r.brightness_set_blocking = bd2802_set_led1r_brightness; + led->cdev_led1r.blink_set = bd2802_set_led1r_blink; + + ret = led_classdev_register(&led->client->dev, &led->cdev_led1r); + if (ret < 0) { + dev_err(&led->client->dev, "couldn't register LED %s\n", + led->cdev_led1r.name); + goto failed_unregister_led1_R; + } + + led->cdev_led1g.name = "led1_G"; + led->cdev_led1g.brightness = LED_OFF; + led->cdev_led1g.brightness_set_blocking = bd2802_set_led1g_brightness; + led->cdev_led1g.blink_set = bd2802_set_led1g_blink; + + ret = led_classdev_register(&led->client->dev, &led->cdev_led1g); + if (ret < 0) { + dev_err(&led->client->dev, "couldn't register LED %s\n", + led->cdev_led1g.name); + goto failed_unregister_led1_G; + } + + led->cdev_led1b.name = "led1_B"; + led->cdev_led1b.brightness = LED_OFF; + led->cdev_led1b.brightness_set_blocking = bd2802_set_led1b_brightness; + led->cdev_led1b.blink_set = bd2802_set_led1b_blink; + + ret = led_classdev_register(&led->client->dev, &led->cdev_led1b); + if (ret < 0) { + dev_err(&led->client->dev, "couldn't register LED %s\n", + led->cdev_led1b.name); + goto failed_unregister_led1_B; + } + + led->cdev_led2r.name = "led2_R"; + led->cdev_led2r.brightness = LED_OFF; + led->cdev_led2r.brightness_set_blocking = bd2802_set_led2r_brightness; + led->cdev_led2r.blink_set = bd2802_set_led2r_blink; + + ret = led_classdev_register(&led->client->dev, &led->cdev_led2r); + if (ret < 0) { + dev_err(&led->client->dev, "couldn't register LED %s\n", + led->cdev_led2r.name); + goto failed_unregister_led2_R; + } + + led->cdev_led2g.name = "led2_G"; + led->cdev_led2g.brightness = LED_OFF; + led->cdev_led2g.brightness_set_blocking = bd2802_set_led2g_brightness; + led->cdev_led2g.blink_set = bd2802_set_led2g_blink; + + ret = led_classdev_register(&led->client->dev, &led->cdev_led2g); + if (ret < 0) { + dev_err(&led->client->dev, "couldn't register LED %s\n", + led->cdev_led2g.name); + goto failed_unregister_led2_G; + } + + led->cdev_led2b.name = "led2_B"; + led->cdev_led2b.brightness = LED_OFF; + led->cdev_led2b.brightness_set_blocking = bd2802_set_led2b_brightness; + led->cdev_led2b.blink_set = bd2802_set_led2b_blink; + led->cdev_led2b.flags |= LED_CORE_SUSPENDRESUME; + + ret = led_classdev_register(&led->client->dev, &led->cdev_led2b); + if (ret < 0) { + dev_err(&led->client->dev, "couldn't register LED %s\n", + led->cdev_led2b.name); + goto failed_unregister_led2_B; + } + + return 0; + +failed_unregister_led2_B: + led_classdev_unregister(&led->cdev_led2g); +failed_unregister_led2_G: + led_classdev_unregister(&led->cdev_led2r); +failed_unregister_led2_R: + led_classdev_unregister(&led->cdev_led1b); +failed_unregister_led1_B: + led_classdev_unregister(&led->cdev_led1g); +failed_unregister_led1_G: + led_classdev_unregister(&led->cdev_led1r); +failed_unregister_led1_R: + + return ret; +} + +static void bd2802_unregister_led_classdev(struct bd2802_led *led) +{ + led_classdev_unregister(&led->cdev_led2b); + led_classdev_unregister(&led->cdev_led2g); + led_classdev_unregister(&led->cdev_led2r); + led_classdev_unregister(&led->cdev_led1b); + led_classdev_unregister(&led->cdev_led1g); + led_classdev_unregister(&led->cdev_led1r); +} + +static int bd2802_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct bd2802_led *led; + int ret, i; + + led = devm_kzalloc(&client->dev, sizeof(struct bd2802_led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->client = client; + i2c_set_clientdata(client, led); + + /* + * Configure RESET GPIO (L: RESET, H: RESET cancel) + * + * We request the reset GPIO as OUT_LOW which means de-asserted, + * board files specifying this GPIO line in a machine descriptor + * table should take care to specify GPIO_ACTIVE_LOW for this line. + */ + led->reset = devm_gpiod_get(&client->dev, "reset", GPIOD_OUT_LOW); + if (IS_ERR(led->reset)) + return PTR_ERR(led->reset); + + /* Tacss = min 0.1ms */ + udelay(100); + + /* Detect BD2802GU */ + ret = bd2802_write_byte(client, BD2802_REG_CLKSETUP, 0x00); + if (ret < 0) { + dev_err(&client->dev, "failed to detect device\n"); + return ret; + } else + dev_info(&client->dev, "return 0x%02x\n", ret); + + /* To save the power, reset BD2802 after detecting */ + gpiod_set_value(led->reset, 1); + + /* Default attributes */ + led->wave_pattern = BD2802_PATTERN_HALF; + led->rgb_current = BD2802_CURRENT_032; + + init_rwsem(&led->rwsem); + + for (i = 0; i < ARRAY_SIZE(bd2802_attributes); i++) { + ret = device_create_file(&led->client->dev, + bd2802_attributes[i]); + if (ret) { + dev_err(&led->client->dev, "failed: sysfs file %s\n", + bd2802_attributes[i]->attr.name); + goto failed_unregister_dev_file; + } + } + + ret = bd2802_register_led_classdev(led); + if (ret < 0) + goto failed_unregister_dev_file; + + return 0; + +failed_unregister_dev_file: + for (i--; i >= 0; i--) + device_remove_file(&led->client->dev, bd2802_attributes[i]); + return ret; +} + +static int bd2802_remove(struct i2c_client *client) +{ + struct bd2802_led *led = i2c_get_clientdata(client); + int i; + + gpiod_set_value(led->reset, 1); + bd2802_unregister_led_classdev(led); + if (led->adf_on) + bd2802_disable_adv_conf(led); + for (i = 0; i < ARRAY_SIZE(bd2802_attributes); i++) + device_remove_file(&led->client->dev, bd2802_attributes[i]); + + return 0; +} + +#ifdef CONFIG_PM_SLEEP +static void bd2802_restore_state(struct bd2802_led *led) +{ + int i; + + for (i = 0; i < LED_NUM; i++) { + if (led->led[i].r) + bd2802_turn_on(led, i, RED, led->led[i].r); + if (led->led[i].g) + bd2802_turn_on(led, i, GREEN, led->led[i].g); + if (led->led[i].b) + bd2802_turn_on(led, i, BLUE, led->led[i].b); + } +} + +static int bd2802_suspend(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct bd2802_led *led = i2c_get_clientdata(client); + + gpiod_set_value(led->reset, 1); + + return 0; +} + +static int bd2802_resume(struct device *dev) +{ + struct i2c_client *client = to_i2c_client(dev); + struct bd2802_led *led = i2c_get_clientdata(client); + + if (!bd2802_is_all_off(led) || led->adf_on) { + bd2802_reset_cancel(led); + bd2802_restore_state(led); + } + + return 0; +} +#endif + +static SIMPLE_DEV_PM_OPS(bd2802_pm, bd2802_suspend, bd2802_resume); + +static const struct i2c_device_id bd2802_id[] = { + { "BD2802", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, bd2802_id); + +static struct i2c_driver bd2802_i2c_driver = { + .driver = { + .name = "BD2802", + .pm = &bd2802_pm, + }, + .probe = bd2802_probe, + .remove = bd2802_remove, + .id_table = bd2802_id, +}; + +module_i2c_driver(bd2802_i2c_driver); + +MODULE_AUTHOR("Kim Kyuwon <q1.kim@samsung.com>"); +MODULE_DESCRIPTION("BD2802 LED driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-blinkm.c b/drivers/leds/leds-blinkm.c new file mode 100644 index 000000000..e11fe1788 --- /dev/null +++ b/drivers/leds/leds-blinkm.c @@ -0,0 +1,748 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * leds-blinkm.c + * (c) Jan-Simon Möller (dl9pf@gmx.de) + */ + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/jiffies.h> +#include <linux/i2c.h> +#include <linux/err.h> +#include <linux/mutex.h> +#include <linux/sysfs.h> +#include <linux/printk.h> +#include <linux/pm_runtime.h> +#include <linux/leds.h> +#include <linux/delay.h> + +/* Addresses to scan - BlinkM is on 0x09 by default*/ +static const unsigned short normal_i2c[] = { 0x09, I2C_CLIENT_END }; + +static int blinkm_transfer_hw(struct i2c_client *client, int cmd); +static int blinkm_test_run(struct i2c_client *client); + +struct blinkm_led { + struct i2c_client *i2c_client; + struct led_classdev led_cdev; + int id; +}; + +#define cdev_to_blmled(c) container_of(c, struct blinkm_led, led_cdev) + +struct blinkm_data { + struct i2c_client *i2c_client; + struct mutex update_lock; + /* used for led class interface */ + struct blinkm_led blinkm_leds[3]; + /* used for "blinkm" sysfs interface */ + u8 red; /* color red */ + u8 green; /* color green */ + u8 blue; /* color blue */ + /* next values to use for transfer */ + u8 next_red; /* color red */ + u8 next_green; /* color green */ + u8 next_blue; /* color blue */ + /* internal use */ + u8 args[7]; /* set of args for transmission */ + u8 i2c_addr; /* i2c addr */ + u8 fw_ver; /* firmware version */ + /* used, but not from userspace */ + u8 hue; /* HSB hue */ + u8 saturation; /* HSB saturation */ + u8 brightness; /* HSB brightness */ + u8 next_hue; /* HSB hue */ + u8 next_saturation; /* HSB saturation */ + u8 next_brightness; /* HSB brightness */ + /* currently unused / todo */ + u8 fade_speed; /* fade speed 1 - 255 */ + s8 time_adjust; /* time adjust -128 - 127 */ + u8 fade:1; /* fade on = 1, off = 0 */ + u8 rand:1; /* rand fade mode on = 1 */ + u8 script_id; /* script ID */ + u8 script_repeats; /* repeats of script */ + u8 script_startline; /* line to start */ +}; + +/* Colors */ +#define RED 0 +#define GREEN 1 +#define BLUE 2 + +/* mapping command names to cmd chars - see datasheet */ +#define BLM_GO_RGB 0 +#define BLM_FADE_RGB 1 +#define BLM_FADE_HSB 2 +#define BLM_FADE_RAND_RGB 3 +#define BLM_FADE_RAND_HSB 4 +#define BLM_PLAY_SCRIPT 5 +#define BLM_STOP_SCRIPT 6 +#define BLM_SET_FADE_SPEED 7 +#define BLM_SET_TIME_ADJ 8 +#define BLM_GET_CUR_RGB 9 +#define BLM_WRITE_SCRIPT_LINE 10 +#define BLM_READ_SCRIPT_LINE 11 +#define BLM_SET_SCRIPT_LR 12 /* Length & Repeats */ +#define BLM_SET_ADDR 13 +#define BLM_GET_ADDR 14 +#define BLM_GET_FW_VER 15 +#define BLM_SET_STARTUP_PARAM 16 + +/* BlinkM Commands + * as extracted out of the datasheet: + * + * cmdchar = command (ascii) + * cmdbyte = command in hex + * nr_args = number of arguments (to send) + * nr_ret = number of return values (to read) + * dir = direction (0 = read, 1 = write, 2 = both) + * + */ +static const struct { + char cmdchar; + u8 cmdbyte; + u8 nr_args; + u8 nr_ret; + u8 dir:2; +} blinkm_cmds[17] = { + /* cmdchar, cmdbyte, nr_args, nr_ret, dir */ + { 'n', 0x6e, 3, 0, 1}, + { 'c', 0x63, 3, 0, 1}, + { 'h', 0x68, 3, 0, 1}, + { 'C', 0x43, 3, 0, 1}, + { 'H', 0x48, 3, 0, 1}, + { 'p', 0x70, 3, 0, 1}, + { 'o', 0x6f, 0, 0, 1}, + { 'f', 0x66, 1, 0, 1}, + { 't', 0x74, 1, 0, 1}, + { 'g', 0x67, 0, 3, 0}, + { 'W', 0x57, 7, 0, 1}, + { 'R', 0x52, 2, 5, 2}, + { 'L', 0x4c, 3, 0, 1}, + { 'A', 0x41, 4, 0, 1}, + { 'a', 0x61, 0, 1, 0}, + { 'Z', 0x5a, 0, 1, 0}, + { 'B', 0x42, 5, 0, 1}, +}; + +static ssize_t show_color_common(struct device *dev, char *buf, int color) +{ + struct i2c_client *client; + struct blinkm_data *data; + int ret; + + client = to_i2c_client(dev); + data = i2c_get_clientdata(client); + + ret = blinkm_transfer_hw(client, BLM_GET_CUR_RGB); + if (ret < 0) + return ret; + switch (color) { + case RED: + return scnprintf(buf, PAGE_SIZE, "%02X\n", data->red); + case GREEN: + return scnprintf(buf, PAGE_SIZE, "%02X\n", data->green); + case BLUE: + return scnprintf(buf, PAGE_SIZE, "%02X\n", data->blue); + default: + return -EINVAL; + } + return -EINVAL; +} + +static int store_color_common(struct device *dev, const char *buf, int color) +{ + struct i2c_client *client; + struct blinkm_data *data; + int ret; + u8 value; + + client = to_i2c_client(dev); + data = i2c_get_clientdata(client); + + ret = kstrtou8(buf, 10, &value); + if (ret < 0) { + dev_err(dev, "BlinkM: value too large!\n"); + return ret; + } + + switch (color) { + case RED: + data->next_red = value; + break; + case GREEN: + data->next_green = value; + break; + case BLUE: + data->next_blue = value; + break; + default: + return -EINVAL; + } + + dev_dbg(dev, "next_red = %d, next_green = %d, next_blue = %d\n", + data->next_red, data->next_green, data->next_blue); + + /* if mode ... */ + ret = blinkm_transfer_hw(client, BLM_GO_RGB); + if (ret < 0) { + dev_err(dev, "BlinkM: can't set RGB\n"); + return ret; + } + return 0; +} + +static ssize_t show_red(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return show_color_common(dev, buf, RED); +} + +static ssize_t store_red(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + + ret = store_color_common(dev, buf, RED); + if (ret < 0) + return ret; + return count; +} + +static DEVICE_ATTR(red, S_IRUGO | S_IWUSR, show_red, store_red); + +static ssize_t show_green(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return show_color_common(dev, buf, GREEN); +} + +static ssize_t store_green(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + + int ret; + + ret = store_color_common(dev, buf, GREEN); + if (ret < 0) + return ret; + return count; +} + +static DEVICE_ATTR(green, S_IRUGO | S_IWUSR, show_green, store_green); + +static ssize_t show_blue(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return show_color_common(dev, buf, BLUE); +} + +static ssize_t store_blue(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + + ret = store_color_common(dev, buf, BLUE); + if (ret < 0) + return ret; + return count; +} + +static DEVICE_ATTR(blue, S_IRUGO | S_IWUSR, show_blue, store_blue); + +static ssize_t show_test(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return scnprintf(buf, PAGE_SIZE, + "#Write into test to start test sequence!#\n"); +} + +static ssize_t store_test(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + + struct i2c_client *client; + int ret; + client = to_i2c_client(dev); + + /*test */ + ret = blinkm_test_run(client); + if (ret < 0) + return ret; + + return count; +} + +static DEVICE_ATTR(test, S_IRUGO | S_IWUSR, show_test, store_test); + +/* TODO: HSB, fade, timeadj, script ... */ + +static struct attribute *blinkm_attrs[] = { + &dev_attr_red.attr, + &dev_attr_green.attr, + &dev_attr_blue.attr, + &dev_attr_test.attr, + NULL, +}; + +static const struct attribute_group blinkm_group = { + .name = "blinkm", + .attrs = blinkm_attrs, +}; + +static int blinkm_write(struct i2c_client *client, int cmd, u8 *arg) +{ + int result; + int i; + int arglen = blinkm_cmds[cmd].nr_args; + /* write out cmd to blinkm - always / default step */ + result = i2c_smbus_write_byte(client, blinkm_cmds[cmd].cmdbyte); + if (result < 0) + return result; + /* no args to write out */ + if (arglen == 0) + return 0; + + for (i = 0; i < arglen; i++) { + /* repeat for arglen */ + result = i2c_smbus_write_byte(client, arg[i]); + if (result < 0) + return result; + } + return 0; +} + +static int blinkm_read(struct i2c_client *client, int cmd, u8 *arg) +{ + int result; + int i; + int retlen = blinkm_cmds[cmd].nr_ret; + for (i = 0; i < retlen; i++) { + /* repeat for retlen */ + result = i2c_smbus_read_byte(client); + if (result < 0) + return result; + arg[i] = result; + } + + return 0; +} + +static int blinkm_transfer_hw(struct i2c_client *client, int cmd) +{ + /* the protocol is simple but non-standard: + * e.g. cmd 'g' (= 0x67) for "get device address" + * - which defaults to 0x09 - would be the sequence: + * a) write 0x67 to the device (byte write) + * b) read the value (0x09) back right after (byte read) + * + * Watch out for "unfinished" sequences (i.e. not enough reads + * or writes after a command. It will make the blinkM misbehave. + * Sequence is key here. + */ + + /* args / return are in private data struct */ + struct blinkm_data *data = i2c_get_clientdata(client); + + /* We start hardware transfers which are not to be + * mixed with other commands. Aquire a lock now. */ + if (mutex_lock_interruptible(&data->update_lock) < 0) + return -EAGAIN; + + /* switch cmd - usually write before reads */ + switch (cmd) { + case BLM_FADE_RAND_RGB: + case BLM_GO_RGB: + case BLM_FADE_RGB: + data->args[0] = data->next_red; + data->args[1] = data->next_green; + data->args[2] = data->next_blue; + blinkm_write(client, cmd, data->args); + data->red = data->args[0]; + data->green = data->args[1]; + data->blue = data->args[2]; + break; + case BLM_FADE_HSB: + case BLM_FADE_RAND_HSB: + data->args[0] = data->next_hue; + data->args[1] = data->next_saturation; + data->args[2] = data->next_brightness; + blinkm_write(client, cmd, data->args); + data->hue = data->next_hue; + data->saturation = data->next_saturation; + data->brightness = data->next_brightness; + break; + case BLM_PLAY_SCRIPT: + data->args[0] = data->script_id; + data->args[1] = data->script_repeats; + data->args[2] = data->script_startline; + blinkm_write(client, cmd, data->args); + break; + case BLM_STOP_SCRIPT: + blinkm_write(client, cmd, NULL); + break; + case BLM_GET_CUR_RGB: + data->args[0] = data->red; + data->args[1] = data->green; + data->args[2] = data->blue; + blinkm_write(client, cmd, NULL); + blinkm_read(client, cmd, data->args); + data->red = data->args[0]; + data->green = data->args[1]; + data->blue = data->args[2]; + break; + case BLM_GET_ADDR: + data->args[0] = data->i2c_addr; + blinkm_write(client, cmd, NULL); + blinkm_read(client, cmd, data->args); + data->i2c_addr = data->args[0]; + break; + case BLM_SET_TIME_ADJ: + case BLM_SET_FADE_SPEED: + case BLM_READ_SCRIPT_LINE: + case BLM_WRITE_SCRIPT_LINE: + case BLM_SET_SCRIPT_LR: + case BLM_SET_ADDR: + case BLM_GET_FW_VER: + case BLM_SET_STARTUP_PARAM: + dev_err(&client->dev, + "BlinkM: cmd %d not implemented yet.\n", cmd); + break; + default: + dev_err(&client->dev, "BlinkM: unknown command %d\n", cmd); + mutex_unlock(&data->update_lock); + return -EINVAL; + } /* end switch(cmd) */ + + /* transfers done, unlock */ + mutex_unlock(&data->update_lock); + return 0; +} + +static int blinkm_led_common_set(struct led_classdev *led_cdev, + enum led_brightness value, int color) +{ + /* led_brightness is 0, 127 or 255 - we just use it here as-is */ + struct blinkm_led *led = cdev_to_blmled(led_cdev); + struct blinkm_data *data = i2c_get_clientdata(led->i2c_client); + + switch (color) { + case RED: + /* bail out if there's no change */ + if (data->next_red == (u8) value) + return 0; + data->next_red = (u8) value; + break; + case GREEN: + /* bail out if there's no change */ + if (data->next_green == (u8) value) + return 0; + data->next_green = (u8) value; + break; + case BLUE: + /* bail out if there's no change */ + if (data->next_blue == (u8) value) + return 0; + data->next_blue = (u8) value; + break; + + default: + dev_err(&led->i2c_client->dev, "BlinkM: unknown color.\n"); + return -EINVAL; + } + + blinkm_transfer_hw(led->i2c_client, BLM_GO_RGB); + dev_dbg(&led->i2c_client->dev, + "# DONE # next_red = %d, next_green = %d," + " next_blue = %d\n", + data->next_red, data->next_green, + data->next_blue); + return 0; +} + +static int blinkm_led_red_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + return blinkm_led_common_set(led_cdev, value, RED); +} + +static int blinkm_led_green_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + return blinkm_led_common_set(led_cdev, value, GREEN); +} + +static int blinkm_led_blue_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + return blinkm_led_common_set(led_cdev, value, BLUE); +} + +static void blinkm_init_hw(struct i2c_client *client) +{ + int ret; + ret = blinkm_transfer_hw(client, BLM_STOP_SCRIPT); + ret = blinkm_transfer_hw(client, BLM_GO_RGB); +} + +static int blinkm_test_run(struct i2c_client *client) +{ + int ret; + struct blinkm_data *data = i2c_get_clientdata(client); + + data->next_red = 0x01; + data->next_green = 0x05; + data->next_blue = 0x10; + ret = blinkm_transfer_hw(client, BLM_GO_RGB); + if (ret < 0) + return ret; + msleep(2000); + + data->next_red = 0x25; + data->next_green = 0x10; + data->next_blue = 0x31; + ret = blinkm_transfer_hw(client, BLM_FADE_RGB); + if (ret < 0) + return ret; + msleep(2000); + + data->next_hue = 0x50; + data->next_saturation = 0x10; + data->next_brightness = 0x20; + ret = blinkm_transfer_hw(client, BLM_FADE_HSB); + if (ret < 0) + return ret; + msleep(2000); + + return 0; +} + +/* Return 0 if detection is successful, -ENODEV otherwise */ +static int blinkm_detect(struct i2c_client *client, struct i2c_board_info *info) +{ + struct i2c_adapter *adapter = client->adapter; + int ret; + int count = 99; + u8 tmpargs[7]; + + if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA + | I2C_FUNC_SMBUS_WORD_DATA + | I2C_FUNC_SMBUS_WRITE_BYTE)) + return -ENODEV; + + /* Now, we do the remaining detection. Simple for now. */ + /* We might need more guards to protect other i2c slaves */ + + /* make sure the blinkM is balanced (read/writes) */ + while (count > 0) { + ret = blinkm_write(client, BLM_GET_ADDR, NULL); + if (ret) + return ret; + usleep_range(5000, 10000); + ret = blinkm_read(client, BLM_GET_ADDR, tmpargs); + if (ret) + return ret; + usleep_range(5000, 10000); + if (tmpargs[0] == 0x09) + count = 0; + count--; + } + + /* Step 1: Read BlinkM address back - cmd_char 'a' */ + ret = blinkm_write(client, BLM_GET_ADDR, NULL); + if (ret < 0) + return ret; + usleep_range(20000, 30000); /* allow a small delay */ + ret = blinkm_read(client, BLM_GET_ADDR, tmpargs); + if (ret < 0) + return ret; + + if (tmpargs[0] != 0x09) { + dev_err(&client->dev, "enodev DEV ADDR = 0x%02X\n", tmpargs[0]); + return -ENODEV; + } + + strlcpy(info->type, "blinkm", I2C_NAME_SIZE); + return 0; +} + +static int blinkm_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct blinkm_data *data; + struct blinkm_led *led[3]; + int err, i; + char blinkm_led_name[28]; + + data = devm_kzalloc(&client->dev, + sizeof(struct blinkm_data), GFP_KERNEL); + if (!data) { + err = -ENOMEM; + goto exit; + } + + data->i2c_addr = 0x08; + /* i2c addr - use fake addr of 0x08 initially (real is 0x09) */ + data->fw_ver = 0xfe; + /* firmware version - use fake until we read real value + * (currently broken - BlinkM confused!) */ + data->script_id = 0x01; + data->i2c_client = client; + + i2c_set_clientdata(client, data); + mutex_init(&data->update_lock); + + /* Register sysfs hooks */ + err = sysfs_create_group(&client->dev.kobj, &blinkm_group); + if (err < 0) { + dev_err(&client->dev, "couldn't register sysfs group\n"); + goto exit; + } + + for (i = 0; i < 3; i++) { + /* RED = 0, GREEN = 1, BLUE = 2 */ + led[i] = &data->blinkm_leds[i]; + led[i]->i2c_client = client; + led[i]->id = i; + led[i]->led_cdev.max_brightness = 255; + led[i]->led_cdev.flags = LED_CORE_SUSPENDRESUME; + switch (i) { + case RED: + snprintf(blinkm_led_name, sizeof(blinkm_led_name), + "blinkm-%d-%d-red", + client->adapter->nr, + client->addr); + led[i]->led_cdev.name = blinkm_led_name; + led[i]->led_cdev.brightness_set_blocking = + blinkm_led_red_set; + err = led_classdev_register(&client->dev, + &led[i]->led_cdev); + if (err < 0) { + dev_err(&client->dev, + "couldn't register LED %s\n", + led[i]->led_cdev.name); + goto failred; + } + break; + case GREEN: + snprintf(blinkm_led_name, sizeof(blinkm_led_name), + "blinkm-%d-%d-green", + client->adapter->nr, + client->addr); + led[i]->led_cdev.name = blinkm_led_name; + led[i]->led_cdev.brightness_set_blocking = + blinkm_led_green_set; + err = led_classdev_register(&client->dev, + &led[i]->led_cdev); + if (err < 0) { + dev_err(&client->dev, + "couldn't register LED %s\n", + led[i]->led_cdev.name); + goto failgreen; + } + break; + case BLUE: + snprintf(blinkm_led_name, sizeof(blinkm_led_name), + "blinkm-%d-%d-blue", + client->adapter->nr, + client->addr); + led[i]->led_cdev.name = blinkm_led_name; + led[i]->led_cdev.brightness_set_blocking = + blinkm_led_blue_set; + err = led_classdev_register(&client->dev, + &led[i]->led_cdev); + if (err < 0) { + dev_err(&client->dev, + "couldn't register LED %s\n", + led[i]->led_cdev.name); + goto failblue; + } + break; + } /* end switch */ + } /* end for */ + + /* Initialize the blinkm */ + blinkm_init_hw(client); + + return 0; + +failblue: + led_classdev_unregister(&led[GREEN]->led_cdev); + +failgreen: + led_classdev_unregister(&led[RED]->led_cdev); + +failred: + sysfs_remove_group(&client->dev.kobj, &blinkm_group); +exit: + return err; +} + +static int blinkm_remove(struct i2c_client *client) +{ + struct blinkm_data *data = i2c_get_clientdata(client); + int ret = 0; + int i; + + /* make sure no workqueue entries are pending */ + for (i = 0; i < 3; i++) + led_classdev_unregister(&data->blinkm_leds[i].led_cdev); + + /* reset rgb */ + data->next_red = 0x00; + data->next_green = 0x00; + data->next_blue = 0x00; + ret = blinkm_transfer_hw(client, BLM_FADE_RGB); + if (ret < 0) + dev_err(&client->dev, "Failure in blinkm_remove ignored. Continuing.\n"); + + /* reset hsb */ + data->next_hue = 0x00; + data->next_saturation = 0x00; + data->next_brightness = 0x00; + ret = blinkm_transfer_hw(client, BLM_FADE_HSB); + if (ret < 0) + dev_err(&client->dev, "Failure in blinkm_remove ignored. Continuing.\n"); + + /* red fade to off */ + data->next_red = 0xff; + ret = blinkm_transfer_hw(client, BLM_GO_RGB); + if (ret < 0) + dev_err(&client->dev, "Failure in blinkm_remove ignored. Continuing.\n"); + + /* off */ + data->next_red = 0x00; + ret = blinkm_transfer_hw(client, BLM_FADE_RGB); + if (ret < 0) + dev_err(&client->dev, "Failure in blinkm_remove ignored. Continuing.\n"); + + sysfs_remove_group(&client->dev.kobj, &blinkm_group); + return 0; +} + +static const struct i2c_device_id blinkm_id[] = { + {"blinkm", 0}, + {} +}; + +MODULE_DEVICE_TABLE(i2c, blinkm_id); + + /* This is the driver that will be inserted */ +static struct i2c_driver blinkm_driver = { + .class = I2C_CLASS_HWMON, + .driver = { + .name = "blinkm", + }, + .probe = blinkm_probe, + .remove = blinkm_remove, + .id_table = blinkm_id, + .detect = blinkm_detect, + .address_list = normal_i2c, +}; + +module_i2c_driver(blinkm_driver); + +MODULE_AUTHOR("Jan-Simon Moeller <dl9pf@gmx.de>"); +MODULE_DESCRIPTION("BlinkM RGB LED driver"); +MODULE_LICENSE("GPL"); + diff --git a/drivers/leds/leds-clevo-mail.c b/drivers/leds/leds-clevo-mail.c new file mode 100644 index 000000000..f512e99b9 --- /dev/null +++ b/drivers/leds/leds-clevo-mail.c @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-2.0-only +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/module.h> + +#include <linux/platform_device.h> +#include <linux/err.h> +#include <linux/leds.h> + +#include <linux/io.h> +#include <linux/dmi.h> + +#include <linux/i8042.h> + +#define CLEVO_MAIL_LED_OFF 0x0084 +#define CLEVO_MAIL_LED_BLINK_1HZ 0x008A +#define CLEVO_MAIL_LED_BLINK_0_5HZ 0x0083 + +MODULE_AUTHOR("Márton Németh <nm127@freemail.hu>"); +MODULE_DESCRIPTION("Clevo mail LED driver"); +MODULE_LICENSE("GPL"); + +static bool nodetect; +module_param_named(nodetect, nodetect, bool, 0); +MODULE_PARM_DESC(nodetect, "Skip DMI hardware detection"); + +static struct platform_device *pdev; + +static int __init clevo_mail_led_dmi_callback(const struct dmi_system_id *id) +{ + pr_info("'%s' found\n", id->ident); + return 1; +} + +/* + * struct clevo_mail_led_dmi_table - List of known good models + * + * Contains the known good models this driver is compatible with. + * When adding a new model try to be as strict as possible. This + * makes it possible to keep the false positives (the model is + * detected as working, but in reality it is not) as low as + * possible. + */ +static const struct dmi_system_id clevo_mail_led_dmi_table[] __initconst = { + { + .callback = clevo_mail_led_dmi_callback, + .ident = "Clevo D410J", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "VIA"), + DMI_MATCH(DMI_PRODUCT_NAME, "K8N800"), + DMI_MATCH(DMI_PRODUCT_VERSION, "VT8204B") + } + }, + { + .callback = clevo_mail_led_dmi_callback, + .ident = "Clevo M5x0N", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "CLEVO Co."), + DMI_MATCH(DMI_PRODUCT_NAME, "M5x0N") + } + }, + { + .callback = clevo_mail_led_dmi_callback, + .ident = "Clevo M5x0V", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "CLEVO Co. "), + DMI_MATCH(DMI_BOARD_NAME, "M5X0V "), + DMI_MATCH(DMI_PRODUCT_VERSION, "VT6198") + } + }, + { + .callback = clevo_mail_led_dmi_callback, + .ident = "Clevo D400P", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Clevo"), + DMI_MATCH(DMI_BOARD_NAME, "D400P"), + DMI_MATCH(DMI_BOARD_VERSION, "Rev.A"), + DMI_MATCH(DMI_PRODUCT_VERSION, "0106") + } + }, + { + .callback = clevo_mail_led_dmi_callback, + .ident = "Clevo D410V", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "Clevo, Co."), + DMI_MATCH(DMI_BOARD_NAME, "D400V/D470V"), + DMI_MATCH(DMI_BOARD_VERSION, "SS78B"), + DMI_MATCH(DMI_PRODUCT_VERSION, "Rev. A1") + } + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, clevo_mail_led_dmi_table); + +static void clevo_mail_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + i8042_lock_chip(); + + if (value == LED_OFF) + i8042_command(NULL, CLEVO_MAIL_LED_OFF); + else if (value <= LED_HALF) + i8042_command(NULL, CLEVO_MAIL_LED_BLINK_0_5HZ); + else + i8042_command(NULL, CLEVO_MAIL_LED_BLINK_1HZ); + + i8042_unlock_chip(); + +} + +static int clevo_mail_led_blink(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + int status = -EINVAL; + + i8042_lock_chip(); + + if (*delay_on == 0 /* ms */ && *delay_off == 0 /* ms */) { + /* Special case: the leds subsystem requested us to + * chose one user friendly blinking of the LED, and + * start it. Let's blink the led slowly (0.5Hz). + */ + *delay_on = 1000; /* ms */ + *delay_off = 1000; /* ms */ + i8042_command(NULL, CLEVO_MAIL_LED_BLINK_0_5HZ); + status = 0; + + } else if (*delay_on == 500 /* ms */ && *delay_off == 500 /* ms */) { + /* blink the led with 1Hz */ + i8042_command(NULL, CLEVO_MAIL_LED_BLINK_1HZ); + status = 0; + + } else if (*delay_on == 1000 /* ms */ && *delay_off == 1000 /* ms */) { + /* blink the led with 0.5Hz */ + i8042_command(NULL, CLEVO_MAIL_LED_BLINK_0_5HZ); + status = 0; + + } else { + pr_debug("clevo_mail_led_blink(..., %lu, %lu)," + " returning -EINVAL (unsupported)\n", + *delay_on, *delay_off); + } + + i8042_unlock_chip(); + + return status; +} + +static struct led_classdev clevo_mail_led = { + .name = "clevo::mail", + .brightness_set = clevo_mail_led_set, + .blink_set = clevo_mail_led_blink, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static int __init clevo_mail_led_probe(struct platform_device *pdev) +{ + return led_classdev_register(&pdev->dev, &clevo_mail_led); +} + +static int clevo_mail_led_remove(struct platform_device *pdev) +{ + led_classdev_unregister(&clevo_mail_led); + return 0; +} + +static struct platform_driver clevo_mail_led_driver = { + .remove = clevo_mail_led_remove, + .driver = { + .name = KBUILD_MODNAME, + }, +}; + +static int __init clevo_mail_led_init(void) +{ + int error = 0; + int count = 0; + + /* Check with the help of DMI if we are running on supported hardware */ + if (!nodetect) { + count = dmi_check_system(clevo_mail_led_dmi_table); + } else { + count = 1; + pr_err("Skipping DMI detection. " + "If the driver works on your hardware please " + "report model and the output of dmidecode in tracker " + "at http://sourceforge.net/projects/clevo-mailled/\n"); + } + + if (!count) + return -ENODEV; + + pdev = platform_device_register_simple(KBUILD_MODNAME, -1, NULL, 0); + if (!IS_ERR(pdev)) { + error = platform_driver_probe(&clevo_mail_led_driver, + clevo_mail_led_probe); + if (error) { + pr_err("Can't probe platform driver\n"); + platform_device_unregister(pdev); + } + } else + error = PTR_ERR(pdev); + + return error; +} + +static void __exit clevo_mail_led_exit(void) +{ + platform_device_unregister(pdev); + platform_driver_unregister(&clevo_mail_led_driver); + + clevo_mail_led_set(NULL, LED_OFF); +} + +module_init(clevo_mail_led_init); +module_exit(clevo_mail_led_exit); diff --git a/drivers/leds/leds-cobalt-qube.c b/drivers/leds/leds-cobalt-qube.c new file mode 100644 index 000000000..ef22e1e94 --- /dev/null +++ b/drivers/leds/leds-cobalt-qube.c @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2006 - Florian Fainelli <florian@openwrt.org> + * + * Control the Cobalt Qube/RaQ front LED + */ +#include <linux/io.h> +#include <linux/ioport.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/types.h> + +#define LED_FRONT_LEFT 0x01 +#define LED_FRONT_RIGHT 0x02 + +static void __iomem *led_port; +static u8 led_value; + +static void qube_front_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + if (brightness) + led_value = LED_FRONT_LEFT | LED_FRONT_RIGHT; + else + led_value = ~(LED_FRONT_LEFT | LED_FRONT_RIGHT); + writeb(led_value, led_port); +} + +static struct led_classdev qube_front_led = { + .name = "qube::front", + .brightness = LED_FULL, + .brightness_set = qube_front_led_set, + .default_trigger = "default-on", +}; + +static int cobalt_qube_led_probe(struct platform_device *pdev) +{ + struct resource *res; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -EBUSY; + + led_port = devm_ioremap(&pdev->dev, res->start, resource_size(res)); + if (!led_port) + return -ENOMEM; + + led_value = LED_FRONT_LEFT | LED_FRONT_RIGHT; + writeb(led_value, led_port); + + return devm_led_classdev_register(&pdev->dev, &qube_front_led); +} + +static struct platform_driver cobalt_qube_led_driver = { + .probe = cobalt_qube_led_probe, + .driver = { + .name = "cobalt-qube-leds", + }, +}; + +module_platform_driver(cobalt_qube_led_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Front LED support for Cobalt Server"); +MODULE_AUTHOR("Florian Fainelli <florian@openwrt.org>"); +MODULE_ALIAS("platform:cobalt-qube-leds"); diff --git a/drivers/leds/leds-cobalt-raq.c b/drivers/leds/leds-cobalt-raq.c new file mode 100644 index 000000000..045c239c7 --- /dev/null +++ b/drivers/leds/leds-cobalt-raq.c @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * LEDs driver for the Cobalt Raq series. + * + * Copyright (C) 2007 Yoichi Yuasa <yuasa@linux-mips.org> + */ +#include <linux/init.h> +#include <linux/io.h> +#include <linux/ioport.h> +#include <linux/leds.h> +#include <linux/platform_device.h> +#include <linux/spinlock.h> +#include <linux/types.h> +#include <linux/export.h> + +#define LED_WEB 0x04 +#define LED_POWER_OFF 0x08 + +static void __iomem *led_port; +static u8 led_value; +static DEFINE_SPINLOCK(led_value_lock); + +static void raq_web_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + unsigned long flags; + + spin_lock_irqsave(&led_value_lock, flags); + + if (brightness) + led_value |= LED_WEB; + else + led_value &= ~LED_WEB; + writeb(led_value, led_port); + + spin_unlock_irqrestore(&led_value_lock, flags); +} + +static struct led_classdev raq_web_led = { + .name = "raq::web", + .brightness_set = raq_web_led_set, +}; + +static void raq_power_off_led_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + unsigned long flags; + + spin_lock_irqsave(&led_value_lock, flags); + + if (brightness) + led_value |= LED_POWER_OFF; + else + led_value &= ~LED_POWER_OFF; + writeb(led_value, led_port); + + spin_unlock_irqrestore(&led_value_lock, flags); +} + +static struct led_classdev raq_power_off_led = { + .name = "raq::power-off", + .brightness_set = raq_power_off_led_set, + .default_trigger = "power-off", +}; + +static int cobalt_raq_led_probe(struct platform_device *pdev) +{ + struct resource *res; + int retval; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -EBUSY; + + led_port = devm_ioremap(&pdev->dev, res->start, resource_size(res)); + if (!led_port) + return -ENOMEM; + + retval = led_classdev_register(&pdev->dev, &raq_power_off_led); + if (retval) + goto err_null; + + retval = led_classdev_register(&pdev->dev, &raq_web_led); + if (retval) + goto err_unregister; + + return 0; + +err_unregister: + led_classdev_unregister(&raq_power_off_led); + +err_null: + led_port = NULL; + + return retval; +} + +static struct platform_driver cobalt_raq_led_driver = { + .probe = cobalt_raq_led_probe, + .driver = { + .name = "cobalt-raq-leds", + }, +}; + +builtin_platform_driver(cobalt_raq_led_driver); diff --git a/drivers/leds/leds-cpcap.c b/drivers/leds/leds-cpcap.c new file mode 100644 index 000000000..7d41ce8c9 --- /dev/null +++ b/drivers/leds/leds-cpcap.c @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (c) 2017 Sebastian Reichel <sre@kernel.org> + */ + +#include <linux/leds.h> +#include <linux/mfd/motorola-cpcap.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/regulator/consumer.h> + +#define CPCAP_LED_NO_CURRENT 0x0001 + +struct cpcap_led_info { + u16 reg; + u16 mask; + u16 limit; + u16 init_mask; + u16 init_val; +}; + +static const struct cpcap_led_info cpcap_led_red = { + .reg = CPCAP_REG_REDC, + .mask = 0x03FF, + .limit = 31, +}; + +static const struct cpcap_led_info cpcap_led_green = { + .reg = CPCAP_REG_GREENC, + .mask = 0x03FF, + .limit = 31, +}; + +static const struct cpcap_led_info cpcap_led_blue = { + .reg = CPCAP_REG_BLUEC, + .mask = 0x03FF, + .limit = 31, +}; + +/* aux display light */ +static const struct cpcap_led_info cpcap_led_adl = { + .reg = CPCAP_REG_ADLC, + .mask = 0x000F, + .limit = 1, + .init_mask = 0x7FFF, + .init_val = 0x5FF0, +}; + +/* camera privacy led */ +static const struct cpcap_led_info cpcap_led_cp = { + .reg = CPCAP_REG_CLEDC, + .mask = 0x0007, + .limit = 1, + .init_mask = 0x03FF, + .init_val = 0x0008, +}; + +struct cpcap_led { + struct led_classdev led; + const struct cpcap_led_info *info; + struct device *dev; + struct regmap *regmap; + struct mutex update_lock; + struct regulator *vdd; + bool powered; + + u32 current_limit; +}; + +static u16 cpcap_led_val(u8 current_limit, u8 duty_cycle) +{ + current_limit &= 0x1f; /* 5 bit */ + duty_cycle &= 0x0f; /* 4 bit */ + + return current_limit << 4 | duty_cycle; +} + +static int cpcap_led_set_power(struct cpcap_led *led, bool status) +{ + int err; + + if (status == led->powered) + return 0; + + if (status) + err = regulator_enable(led->vdd); + else + err = regulator_disable(led->vdd); + + if (err) { + dev_err(led->dev, "regulator failure: %d", err); + return err; + } + + led->powered = status; + + return 0; +} + +static int cpcap_led_set(struct led_classdev *ledc, enum led_brightness value) +{ + struct cpcap_led *led = container_of(ledc, struct cpcap_led, led); + int brightness; + int err; + + mutex_lock(&led->update_lock); + + if (value > LED_OFF) { + err = cpcap_led_set_power(led, true); + if (err) + goto exit; + } + + if (value == LED_OFF) { + /* Avoid HW issue by turning off current before duty cycle */ + err = regmap_update_bits(led->regmap, + led->info->reg, led->info->mask, CPCAP_LED_NO_CURRENT); + if (err) { + dev_err(led->dev, "regmap failed: %d", err); + goto exit; + } + + brightness = cpcap_led_val(value, LED_OFF); + } else { + brightness = cpcap_led_val(value, LED_ON); + } + + err = regmap_update_bits(led->regmap, led->info->reg, led->info->mask, + brightness); + if (err) { + dev_err(led->dev, "regmap failed: %d", err); + goto exit; + } + + if (value == LED_OFF) { + err = cpcap_led_set_power(led, false); + if (err) + goto exit; + } + +exit: + mutex_unlock(&led->update_lock); + return err; +} + +static const struct of_device_id cpcap_led_of_match[] = { + { .compatible = "motorola,cpcap-led-red", .data = &cpcap_led_red }, + { .compatible = "motorola,cpcap-led-green", .data = &cpcap_led_green }, + { .compatible = "motorola,cpcap-led-blue", .data = &cpcap_led_blue }, + { .compatible = "motorola,cpcap-led-adl", .data = &cpcap_led_adl }, + { .compatible = "motorola,cpcap-led-cp", .data = &cpcap_led_cp }, + {}, +}; +MODULE_DEVICE_TABLE(of, cpcap_led_of_match); + +static int cpcap_led_probe(struct platform_device *pdev) +{ + struct cpcap_led *led; + int err; + + led = devm_kzalloc(&pdev->dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + platform_set_drvdata(pdev, led); + led->info = device_get_match_data(&pdev->dev); + led->dev = &pdev->dev; + + if (led->info->reg == 0x0000) { + dev_err(led->dev, "Unsupported LED"); + return -ENODEV; + } + + led->regmap = dev_get_regmap(pdev->dev.parent, NULL); + if (!led->regmap) + return -ENODEV; + + led->vdd = devm_regulator_get(&pdev->dev, "vdd"); + if (IS_ERR(led->vdd)) { + err = PTR_ERR(led->vdd); + dev_err(led->dev, "Couldn't get regulator: %d", err); + return err; + } + + err = device_property_read_string(&pdev->dev, "label", &led->led.name); + if (err) { + dev_err(led->dev, "Couldn't read LED label: %d", err); + return err; + } + + if (led->info->init_mask) { + err = regmap_update_bits(led->regmap, led->info->reg, + led->info->init_mask, led->info->init_val); + if (err) { + dev_err(led->dev, "regmap failed: %d", err); + return err; + } + } + + mutex_init(&led->update_lock); + + led->led.max_brightness = led->info->limit; + led->led.brightness_set_blocking = cpcap_led_set; + err = devm_led_classdev_register(&pdev->dev, &led->led); + if (err) { + dev_err(led->dev, "Couldn't register LED: %d", err); + return err; + } + + return 0; +} + +static struct platform_driver cpcap_led_driver = { + .probe = cpcap_led_probe, + .driver = { + .name = "cpcap-led", + .of_match_table = cpcap_led_of_match, + }, +}; +module_platform_driver(cpcap_led_driver); + +MODULE_DESCRIPTION("CPCAP LED driver"); +MODULE_AUTHOR("Sebastian Reichel <sre@kernel.org>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-cr0014114.c b/drivers/leds/leds-cr0014114.c new file mode 100644 index 000000000..d03cfd3c0 --- /dev/null +++ b/drivers/leds/leds-cr0014114.c @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2018 Crane Merchandising Systems. All rights reserved. +// Copyright (C) 2018 Oleh Kravchenko <oleg@kaa.org.ua> + +#include <linux/delay.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/spi/spi.h> +#include <linux/workqueue.h> + +/* + * CR0014114 SPI protocol descrtiption: + * +----+-----------------------------------+----+ + * | CMD| BRIGHTNESS |CRC | + * +----+-----------------------------------+----+ + * | | LED0| LED1| LED2| LED3| LED4| LED5| | + * | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | + * | |R|G|B|R|G|B|R|G|B|R|G|B|R|G|B|R|G|B| | + * | 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 1 | + * | |1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1|1| | + * | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | + * | | 18 | | + * +----+-----------------------------------+----+ + * | 20 | + * +---------------------------------------------+ + * + * PS: Boards can be connected to the chain: + * SPI -> board0 -> board1 -> board2 .. + */ + +/* CR0014114 SPI commands */ +#define CR_SET_BRIGHTNESS 0x80 +#define CR_INIT_REENUMERATE 0x81 +#define CR_NEXT_REENUMERATE 0x82 + +/* CR0014114 default settings */ +#define CR_MAX_BRIGHTNESS GENMASK(6, 0) +#define CR_FW_DELAY_MSEC 10 +#define CR_RECOUNT_DELAY (HZ * 3600) + +#define CR_DEV_NAME "cr0014114" + +struct cr0014114_led { + struct cr0014114 *priv; + struct led_classdev ldev; + u8 brightness; +}; + +struct cr0014114 { + bool do_recount; + size_t count; + struct delayed_work work; + struct device *dev; + struct mutex lock; + struct spi_device *spi; + u8 *buf; + unsigned long delay; + struct cr0014114_led leds[]; +}; + +static void cr0014114_calc_crc(u8 *buf, const size_t len) +{ + size_t i; + u8 crc; + + for (i = 1, crc = 1; i < len - 1; i++) + crc += buf[i]; + crc |= BIT(7); + + /* special case when CRC matches the SPI commands */ + if (crc == CR_SET_BRIGHTNESS || + crc == CR_INIT_REENUMERATE || + crc == CR_NEXT_REENUMERATE) + crc = 0xfe; + + buf[len - 1] = crc; +} + +static int cr0014114_recount(struct cr0014114 *priv) +{ + int ret; + size_t i; + u8 cmd; + + dev_dbg(priv->dev, "LEDs recount is started\n"); + + cmd = CR_INIT_REENUMERATE; + ret = spi_write(priv->spi, &cmd, sizeof(cmd)); + if (ret) + goto err; + + cmd = CR_NEXT_REENUMERATE; + for (i = 0; i < priv->count; i++) { + msleep(CR_FW_DELAY_MSEC); + + ret = spi_write(priv->spi, &cmd, sizeof(cmd)); + if (ret) + goto err; + } + +err: + dev_dbg(priv->dev, "LEDs recount is finished\n"); + + if (ret) + dev_err(priv->dev, "with error %d", ret); + + return ret; +} + +static int cr0014114_sync(struct cr0014114 *priv) +{ + int ret; + size_t i; + unsigned long udelay, now = jiffies; + + /* to avoid SPI mistiming with firmware we should wait some time */ + if (time_after(priv->delay, now)) { + udelay = jiffies_to_usecs(priv->delay - now); + usleep_range(udelay, udelay + 1); + } + + if (unlikely(priv->do_recount)) { + ret = cr0014114_recount(priv); + if (ret) + goto err; + + priv->do_recount = false; + msleep(CR_FW_DELAY_MSEC); + } + + priv->buf[0] = CR_SET_BRIGHTNESS; + for (i = 0; i < priv->count; i++) + priv->buf[i + 1] = priv->leds[i].brightness; + cr0014114_calc_crc(priv->buf, priv->count + 2); + ret = spi_write(priv->spi, priv->buf, priv->count + 2); + +err: + priv->delay = jiffies + msecs_to_jiffies(CR_FW_DELAY_MSEC); + + return ret; +} + +static void cr0014114_recount_work(struct work_struct *work) +{ + int ret; + struct cr0014114 *priv = container_of(work, + struct cr0014114, + work.work); + + mutex_lock(&priv->lock); + priv->do_recount = true; + ret = cr0014114_sync(priv); + mutex_unlock(&priv->lock); + + if (ret) + dev_warn(priv->dev, "sync of LEDs failed %d\n", ret); + + schedule_delayed_work(&priv->work, CR_RECOUNT_DELAY); +} + +static int cr0014114_set_sync(struct led_classdev *ldev, + enum led_brightness brightness) +{ + int ret; + struct cr0014114_led *led = container_of(ldev, + struct cr0014114_led, + ldev); + + dev_dbg(led->priv->dev, "Set brightness to %d\n", brightness); + + mutex_lock(&led->priv->lock); + led->brightness = (u8)brightness; + ret = cr0014114_sync(led->priv); + mutex_unlock(&led->priv->lock); + + return ret; +} + +static int cr0014114_probe_dt(struct cr0014114 *priv) +{ + size_t i = 0; + struct cr0014114_led *led; + struct fwnode_handle *child; + struct led_init_data init_data = {}; + int ret; + + device_for_each_child_node(priv->dev, child) { + led = &priv->leds[i]; + + led->priv = priv; + led->ldev.max_brightness = CR_MAX_BRIGHTNESS; + led->ldev.brightness_set_blocking = cr0014114_set_sync; + + init_data.fwnode = child; + init_data.devicename = CR_DEV_NAME; + init_data.default_label = ":"; + + ret = devm_led_classdev_register_ext(priv->dev, &led->ldev, + &init_data); + if (ret) { + dev_err(priv->dev, + "failed to register LED device, err %d", ret); + fwnode_handle_put(child); + return ret; + } + + i++; + } + + return 0; +} + +static int cr0014114_probe(struct spi_device *spi) +{ + struct cr0014114 *priv; + size_t count; + int ret; + + count = device_get_child_node_count(&spi->dev); + if (!count) { + dev_err(&spi->dev, "LEDs are not defined in device tree!"); + return -ENODEV; + } + + priv = devm_kzalloc(&spi->dev, struct_size(priv, leds, count), + GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->buf = devm_kzalloc(&spi->dev, count + 2, GFP_KERNEL); + if (!priv->buf) + return -ENOMEM; + + mutex_init(&priv->lock); + INIT_DELAYED_WORK(&priv->work, cr0014114_recount_work); + priv->count = count; + priv->dev = &spi->dev; + priv->spi = spi; + priv->delay = jiffies - + msecs_to_jiffies(CR_FW_DELAY_MSEC); + + priv->do_recount = true; + ret = cr0014114_sync(priv); + if (ret) { + dev_err(priv->dev, "first recount failed %d\n", ret); + return ret; + } + + priv->do_recount = true; + ret = cr0014114_sync(priv); + if (ret) { + dev_err(priv->dev, "second recount failed %d\n", ret); + return ret; + } + + ret = cr0014114_probe_dt(priv); + if (ret) + return ret; + + /* setup recount work to workaround buggy firmware */ + schedule_delayed_work(&priv->work, CR_RECOUNT_DELAY); + + spi_set_drvdata(spi, priv); + + return 0; +} + +static int cr0014114_remove(struct spi_device *spi) +{ + struct cr0014114 *priv = spi_get_drvdata(spi); + + cancel_delayed_work_sync(&priv->work); + mutex_destroy(&priv->lock); + + return 0; +} + +static const struct of_device_id cr0014114_dt_ids[] = { + { .compatible = "crane,cr0014114", }, + {}, +}; + +MODULE_DEVICE_TABLE(of, cr0014114_dt_ids); + +static struct spi_driver cr0014114_driver = { + .probe = cr0014114_probe, + .remove = cr0014114_remove, + .driver = { + .name = KBUILD_MODNAME, + .of_match_table = cr0014114_dt_ids, + }, +}; + +module_spi_driver(cr0014114_driver); + +MODULE_AUTHOR("Oleh Kravchenko <oleg@kaa.org.ua>"); +MODULE_DESCRIPTION("cr0014114 LED driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("spi:cr0014114"); diff --git a/drivers/leds/leds-da903x.c b/drivers/leds/leds-da903x.c new file mode 100644 index 000000000..2b5fb0043 --- /dev/null +++ b/drivers/leds/leds-da903x.c @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LEDs driver for Dialog Semiconductor DA9030/DA9034 + * + * Copyright (C) 2008 Compulab, Ltd. + * Mike Rapoport <mike@compulab.co.il> + * + * Copyright (C) 2006-2008 Marvell International Ltd. + * Eric Miao <eric.miao@marvell.com> + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/mfd/da903x.h> +#include <linux/slab.h> + +#define DA9030_LED1_CONTROL 0x20 +#define DA9030_LED2_CONTROL 0x21 +#define DA9030_LED3_CONTROL 0x22 +#define DA9030_LED4_CONTROL 0x23 +#define DA9030_LEDPC_CONTROL 0x24 +#define DA9030_MISC_CONTROL_A 0x26 /* Vibrator Control */ + +#define DA9034_LED1_CONTROL 0x35 +#define DA9034_LED2_CONTROL 0x36 +#define DA9034_VIBRA 0x40 + +struct da903x_led { + struct led_classdev cdev; + struct device *master; + int id; + int flags; +}; + +#define DA9030_LED_OFFSET(id) ((id) - DA9030_ID_LED_1) +#define DA9034_LED_OFFSET(id) ((id) - DA9034_ID_LED_1) + +static int da903x_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct da903x_led *led = + container_of(led_cdev, struct da903x_led, cdev); + uint8_t val; + int offset, ret = -EINVAL; + + switch (led->id) { + case DA9030_ID_LED_1: + case DA9030_ID_LED_2: + case DA9030_ID_LED_3: + case DA9030_ID_LED_4: + case DA9030_ID_LED_PC: + offset = DA9030_LED_OFFSET(led->id); + val = led->flags & ~0x87; + val |= value ? 0x80 : 0; /* EN bit */ + val |= (0x7 - (value >> 5)) & 0x7; /* PWM<2:0> */ + ret = da903x_write(led->master, DA9030_LED1_CONTROL + offset, + val); + break; + case DA9030_ID_VIBRA: + val = led->flags & ~0x80; + val |= value ? 0x80 : 0; /* EN bit */ + ret = da903x_write(led->master, DA9030_MISC_CONTROL_A, val); + break; + case DA9034_ID_LED_1: + case DA9034_ID_LED_2: + offset = DA9034_LED_OFFSET(led->id); + val = (value * 0x5f / LED_FULL) & 0x7f; + val |= (led->flags & DA9034_LED_RAMP) ? 0x80 : 0; + ret = da903x_write(led->master, DA9034_LED1_CONTROL + offset, + val); + break; + case DA9034_ID_VIBRA: + val = value & 0xfe; + ret = da903x_write(led->master, DA9034_VIBRA, val); + break; + } + + return ret; +} + +static int da903x_led_probe(struct platform_device *pdev) +{ + struct led_info *pdata = dev_get_platdata(&pdev->dev); + struct da903x_led *led; + int id, ret; + + if (pdata == NULL) + return 0; + + id = pdev->id; + + if (!((id >= DA9030_ID_LED_1 && id <= DA9030_ID_VIBRA) || + (id >= DA9034_ID_LED_1 && id <= DA9034_ID_VIBRA))) { + dev_err(&pdev->dev, "invalid LED ID (%d) specified\n", id); + return -EINVAL; + } + + led = devm_kzalloc(&pdev->dev, sizeof(struct da903x_led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->cdev.name = pdata->name; + led->cdev.default_trigger = pdata->default_trigger; + led->cdev.brightness_set_blocking = da903x_led_set; + led->cdev.brightness = LED_OFF; + + led->id = id; + led->flags = pdata->flags; + led->master = pdev->dev.parent; + + ret = led_classdev_register(led->master, &led->cdev); + if (ret) { + dev_err(&pdev->dev, "failed to register LED %d\n", id); + return ret; + } + + platform_set_drvdata(pdev, led); + + return 0; +} + +static int da903x_led_remove(struct platform_device *pdev) +{ + struct da903x_led *led = platform_get_drvdata(pdev); + + led_classdev_unregister(&led->cdev); + + return 0; +} + +static struct platform_driver da903x_led_driver = { + .driver = { + .name = "da903x-led", + }, + .probe = da903x_led_probe, + .remove = da903x_led_remove, +}; + +module_platform_driver(da903x_led_driver); + +MODULE_DESCRIPTION("LEDs driver for Dialog Semiconductor DA9030/DA9034"); +MODULE_AUTHOR("Eric Miao <eric.miao@marvell.com>"); +MODULE_AUTHOR("Mike Rapoport <mike@compulab.co.il>"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:da903x-led"); diff --git a/drivers/leds/leds-da9052.c b/drivers/leds/leds-da9052.c new file mode 100644 index 000000000..04060c862 --- /dev/null +++ b/drivers/leds/leds-da9052.c @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * LED Driver for Dialog DA9052 PMICs. + * + * Copyright(c) 2012 Dialog Semiconductor Ltd. + * + * Author: David Dajun Chen <dchen@diasemi.com> + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/slab.h> + +#include <linux/mfd/da9052/reg.h> +#include <linux/mfd/da9052/da9052.h> +#include <linux/mfd/da9052/pdata.h> + +#define DA9052_OPENDRAIN_OUTPUT 2 +#define DA9052_SET_HIGH_LVL_OUTPUT (1 << 3) +#define DA9052_MASK_UPPER_NIBBLE 0xF0 +#define DA9052_MASK_LOWER_NIBBLE 0x0F +#define DA9052_NIBBLE_SHIFT 4 +#define DA9052_MAX_BRIGHTNESS 0x5f + +struct da9052_led { + struct led_classdev cdev; + struct da9052 *da9052; + unsigned char led_index; + unsigned char id; +}; + +static unsigned char led_reg[] = { + DA9052_LED_CONT_4_REG, + DA9052_LED_CONT_5_REG, +}; + +static int da9052_set_led_brightness(struct da9052_led *led, + enum led_brightness brightness) +{ + u8 val; + int error; + + val = (brightness & 0x7f) | DA9052_LED_CONT_DIM; + + error = da9052_reg_write(led->da9052, led_reg[led->led_index], val); + if (error < 0) + dev_err(led->da9052->dev, "Failed to set led brightness, %d\n", + error); + return error; +} + +static int da9052_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct da9052_led *led = + container_of(led_cdev, struct da9052_led, cdev); + + return da9052_set_led_brightness(led, value); +} + +static int da9052_configure_leds(struct da9052 *da9052) +{ + int error; + unsigned char register_value = DA9052_OPENDRAIN_OUTPUT + | DA9052_SET_HIGH_LVL_OUTPUT; + + error = da9052_reg_update(da9052, DA9052_GPIO_14_15_REG, + DA9052_MASK_LOWER_NIBBLE, + register_value); + + if (error < 0) { + dev_err(da9052->dev, "Failed to write GPIO 14-15 reg, %d\n", + error); + return error; + } + + error = da9052_reg_update(da9052, DA9052_GPIO_14_15_REG, + DA9052_MASK_UPPER_NIBBLE, + register_value << DA9052_NIBBLE_SHIFT); + if (error < 0) + dev_err(da9052->dev, "Failed to write GPIO 14-15 reg, %d\n", + error); + + return error; +} + +static int da9052_led_probe(struct platform_device *pdev) +{ + struct da9052_pdata *pdata; + struct da9052 *da9052; + struct led_platform_data *pled; + struct da9052_led *led = NULL; + int error = -ENODEV; + int i; + + da9052 = dev_get_drvdata(pdev->dev.parent); + pdata = dev_get_platdata(da9052->dev); + if (pdata == NULL) { + dev_err(&pdev->dev, "No platform data\n"); + goto err; + } + + pled = pdata->pled; + if (pled == NULL) { + dev_err(&pdev->dev, "No platform data for LED\n"); + goto err; + } + + led = devm_kcalloc(&pdev->dev, + pled->num_leds, sizeof(struct da9052_led), + GFP_KERNEL); + if (!led) { + error = -ENOMEM; + goto err; + } + + for (i = 0; i < pled->num_leds; i++) { + led[i].cdev.name = pled->leds[i].name; + led[i].cdev.brightness_set_blocking = da9052_led_set; + led[i].cdev.brightness = LED_OFF; + led[i].cdev.max_brightness = DA9052_MAX_BRIGHTNESS; + led[i].led_index = pled->leds[i].flags; + led[i].da9052 = dev_get_drvdata(pdev->dev.parent); + + error = led_classdev_register(pdev->dev.parent, &led[i].cdev); + if (error) { + dev_err(&pdev->dev, "Failed to register led %d\n", + led[i].led_index); + goto err_register; + } + + error = da9052_set_led_brightness(&led[i], + led[i].cdev.brightness); + if (error) { + dev_err(&pdev->dev, "Unable to init led %d\n", + led[i].led_index); + continue; + } + } + error = da9052_configure_leds(led->da9052); + if (error) { + dev_err(&pdev->dev, "Failed to configure GPIO LED%d\n", error); + goto err_register; + } + + platform_set_drvdata(pdev, led); + + return 0; + +err_register: + for (i = i - 1; i >= 0; i--) + led_classdev_unregister(&led[i].cdev); +err: + return error; +} + +static int da9052_led_remove(struct platform_device *pdev) +{ + struct da9052_led *led = platform_get_drvdata(pdev); + struct da9052_pdata *pdata; + struct da9052 *da9052; + struct led_platform_data *pled; + int i; + + da9052 = dev_get_drvdata(pdev->dev.parent); + pdata = dev_get_platdata(da9052->dev); + pled = pdata->pled; + + for (i = 0; i < pled->num_leds; i++) { + da9052_set_led_brightness(&led[i], LED_OFF); + led_classdev_unregister(&led[i].cdev); + } + + return 0; +} + +static struct platform_driver da9052_led_driver = { + .driver = { + .name = "da9052-leds", + }, + .probe = da9052_led_probe, + .remove = da9052_led_remove, +}; + +module_platform_driver(da9052_led_driver); + +MODULE_AUTHOR("Dialog Semiconductor Ltd <dchen@diasemi.com>"); +MODULE_DESCRIPTION("LED driver for Dialog DA9052 PMIC"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-dac124s085.c b/drivers/leds/leds-dac124s085.c new file mode 100644 index 000000000..20dc9b9d7 --- /dev/null +++ b/drivers/leds/leds-dac124s085.c @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2008 + * Guennadi Liakhovetski, DENX Software Engineering, <lg@denx.de> + * + * LED driver for the DAC124S085 SPI DAC + */ + +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/slab.h> +#include <linux/spi/spi.h> + +struct dac124s085_led { + struct led_classdev ldev; + struct spi_device *spi; + int id; + char name[sizeof("dac124s085-3")]; + + struct mutex mutex; +}; + +struct dac124s085 { + struct dac124s085_led leds[4]; +}; + +#define REG_WRITE (0 << 12) +#define REG_WRITE_UPDATE (1 << 12) +#define ALL_WRITE_UPDATE (2 << 12) +#define POWER_DOWN_OUTPUT (3 << 12) + +static int dac124s085_set_brightness(struct led_classdev *ldev, + enum led_brightness brightness) +{ + struct dac124s085_led *led = container_of(ldev, struct dac124s085_led, + ldev); + u16 word; + int ret; + + mutex_lock(&led->mutex); + word = cpu_to_le16(((led->id) << 14) | REG_WRITE_UPDATE | + (brightness & 0xfff)); + ret = spi_write(led->spi, (const u8 *)&word, sizeof(word)); + mutex_unlock(&led->mutex); + + return ret; +} + +static int dac124s085_probe(struct spi_device *spi) +{ + struct dac124s085 *dac; + struct dac124s085_led *led; + int i, ret; + + dac = devm_kzalloc(&spi->dev, sizeof(*dac), GFP_KERNEL); + if (!dac) + return -ENOMEM; + + spi->bits_per_word = 16; + + for (i = 0; i < ARRAY_SIZE(dac->leds); i++) { + led = dac->leds + i; + led->id = i; + led->spi = spi; + snprintf(led->name, sizeof(led->name), "dac124s085-%d", i); + mutex_init(&led->mutex); + led->ldev.name = led->name; + led->ldev.brightness = LED_OFF; + led->ldev.max_brightness = 0xfff; + led->ldev.brightness_set_blocking = dac124s085_set_brightness; + ret = led_classdev_register(&spi->dev, &led->ldev); + if (ret < 0) + goto eledcr; + } + + spi_set_drvdata(spi, dac); + + return 0; + +eledcr: + while (i--) + led_classdev_unregister(&dac->leds[i].ldev); + + return ret; +} + +static int dac124s085_remove(struct spi_device *spi) +{ + struct dac124s085 *dac = spi_get_drvdata(spi); + int i; + + for (i = 0; i < ARRAY_SIZE(dac->leds); i++) + led_classdev_unregister(&dac->leds[i].ldev); + + return 0; +} + +static struct spi_driver dac124s085_driver = { + .probe = dac124s085_probe, + .remove = dac124s085_remove, + .driver = { + .name = "dac124s085", + }, +}; + +module_spi_driver(dac124s085_driver); + +MODULE_AUTHOR("Guennadi Liakhovetski <lg@denx.de>"); +MODULE_DESCRIPTION("DAC124S085 LED driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("spi:dac124s085"); diff --git a/drivers/leds/leds-el15203000.c b/drivers/leds/leds-el15203000.c new file mode 100644 index 000000000..6ca47f2a2 --- /dev/null +++ b/drivers/leds/leds-el15203000.c @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2019 Crane Merchandising Systems. All rights reserved. +// Copyright (C) 2019 Oleh Kravchenko <oleg@kaa.org.ua> + +#include <linux/delay.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/spi/spi.h> + +/* + * EL15203000 SPI protocol description: + * +-----+---------+ + * | LED | COMMAND | + * +-----+---------+ + * | 1 | 1 | + * +-----+---------+ + * (*) LEDs MCU board expects 20 msec delay per byte. + * + * LEDs: + * +----------+--------------+-------------------------------------------+ + * | ID | NAME | DESCRIPTION | + * +----------+--------------+-------------------------------------------+ + * | 'P' 0x50 | Pipe | Consists from 5 LEDs, controlled by board | + * +----------+--------------+-------------------------------------------+ + * | 'S' 0x53 | Screen frame | Light tube around the screen | + * +----------+--------------+-------------------------------------------+ + * | 'V' 0x56 | Vending area | Highlights a cup of coffee | + * +----------+--------------+-------------------------------------------+ + * + * COMMAND: + * +----------+-----------------+--------------+--------------+ + * | VALUES | PIPE | SCREEN FRAME | VENDING AREA | + * +----------+-----------------+--------------+--------------+ + * | '0' 0x30 | Off | + * +----------+-----------------------------------------------+ + * | '1' 0x31 | On | + * +----------+-----------------+--------------+--------------+ + * | '2' 0x32 | Cascade | Breathing | + * +----------+-----------------+--------------+ + * | '3' 0x33 | Inverse cascade | + * +----------+-----------------+ + * | '4' 0x34 | Bounce | + * +----------+-----------------+ + * | '5' 0x35 | Inverse bounce | + * +----------+-----------------+ + */ + +/* EL15203000 default settings */ +#define EL_FW_DELAY_USEC 20000ul +#define EL_PATTERN_DELAY_MSEC 800u +#define EL_PATTERN_LEN 10u +#define EL_PATTERN_HALF_LEN (EL_PATTERN_LEN / 2) + +enum el15203000_command { + /* for all LEDs */ + EL_OFF = '0', + EL_ON = '1', + + /* for Screen LED */ + EL_SCREEN_BREATHING = '2', + + /* for Pipe LED */ + EL_PIPE_CASCADE = '2', + EL_PIPE_INV_CASCADE = '3', + EL_PIPE_BOUNCE = '4', + EL_PIPE_INV_BOUNCE = '5', +}; + +struct el15203000_led { + struct el15203000 *priv; + struct led_classdev ldev; + u32 reg; +}; + +struct el15203000 { + struct device *dev; + struct mutex lock; + struct spi_device *spi; + unsigned long delay; + size_t count; + struct el15203000_led leds[]; +}; + +static int el15203000_cmd(struct el15203000_led *led, u8 brightness) +{ + int ret; + u8 cmd[2]; + size_t i; + + mutex_lock(&led->priv->lock); + + dev_dbg(led->priv->dev, "Set brightness of 0x%02x(%c) to 0x%02x(%c)", + led->reg, led->reg, brightness, brightness); + + /* to avoid SPI mistiming with firmware we should wait some time */ + if (time_after(led->priv->delay, jiffies)) { + dev_dbg(led->priv->dev, "Wait %luus to sync", + EL_FW_DELAY_USEC); + + usleep_range(EL_FW_DELAY_USEC, + EL_FW_DELAY_USEC + 1); + } + + cmd[0] = led->reg; + cmd[1] = brightness; + + for (i = 0; i < ARRAY_SIZE(cmd); i++) { + if (i) + usleep_range(EL_FW_DELAY_USEC, + EL_FW_DELAY_USEC + 1); + + ret = spi_write(led->priv->spi, &cmd[i], sizeof(cmd[i])); + if (ret) { + dev_err(led->priv->dev, + "spi_write() error %d", ret); + break; + } + } + + led->priv->delay = jiffies + usecs_to_jiffies(EL_FW_DELAY_USEC); + + mutex_unlock(&led->priv->lock); + + return ret; +} + +static int el15203000_set_blocking(struct led_classdev *ldev, + enum led_brightness brightness) +{ + struct el15203000_led *led = container_of(ldev, + struct el15203000_led, + ldev); + + return el15203000_cmd(led, brightness == LED_OFF ? EL_OFF : EL_ON); +} + +static int el15203000_pattern_set_S(struct led_classdev *ldev, + struct led_pattern *pattern, + u32 len, int repeat) +{ + struct el15203000_led *led = container_of(ldev, + struct el15203000_led, + ldev); + + if (repeat > 0 || len != 2 || + pattern[0].delta_t != 4000 || pattern[0].brightness != 0 || + pattern[1].delta_t != 4000 || pattern[1].brightness != 1) + return -EINVAL; + + dev_dbg(led->priv->dev, "Breathing mode for 0x%02x(%c)", + led->reg, led->reg); + + return el15203000_cmd(led, EL_SCREEN_BREATHING); +} + +static bool is_cascade(const struct led_pattern *pattern, u32 len, + bool inv, bool right) +{ + int val, t; + u32 i; + + if (len != EL_PATTERN_HALF_LEN) + return false; + + val = right ? BIT(4) : BIT(0); + + for (i = 0; i < len; i++) { + t = inv ? ~val & GENMASK(4, 0) : val; + + if (pattern[i].delta_t != EL_PATTERN_DELAY_MSEC || + pattern[i].brightness != t) + return false; + + val = right ? val >> 1 : val << 1; + } + + return true; +} + +static bool is_bounce(const struct led_pattern *pattern, u32 len, bool inv) +{ + if (len != EL_PATTERN_LEN) + return false; + + return is_cascade(pattern, EL_PATTERN_HALF_LEN, inv, false) && + is_cascade(pattern + EL_PATTERN_HALF_LEN, + EL_PATTERN_HALF_LEN, inv, true); +} + +static int el15203000_pattern_set_P(struct led_classdev *ldev, + struct led_pattern *pattern, + u32 len, int repeat) +{ + u8 cmd; + struct el15203000_led *led = container_of(ldev, + struct el15203000_led, + ldev); + + if (repeat > 0) + return -EINVAL; + + if (is_cascade(pattern, len, false, false)) { + dev_dbg(led->priv->dev, "Cascade mode for 0x%02x(%c)", + led->reg, led->reg); + + cmd = EL_PIPE_CASCADE; + } else if (is_cascade(pattern, len, true, false)) { + dev_dbg(led->priv->dev, "Inverse cascade mode for 0x%02x(%c)", + led->reg, led->reg); + + cmd = EL_PIPE_INV_CASCADE; + } else if (is_bounce(pattern, len, false)) { + dev_dbg(led->priv->dev, "Bounce mode for 0x%02x(%c)", + led->reg, led->reg); + + cmd = EL_PIPE_BOUNCE; + } else if (is_bounce(pattern, len, true)) { + dev_dbg(led->priv->dev, "Inverse bounce mode for 0x%02x(%c)", + led->reg, led->reg); + + cmd = EL_PIPE_INV_BOUNCE; + } else { + dev_err(led->priv->dev, "Invalid hw_pattern for 0x%02x(%c)!", + led->reg, led->reg); + + return -EINVAL; + } + + return el15203000_cmd(led, cmd); +} + +static int el15203000_pattern_clear(struct led_classdev *ldev) +{ + struct el15203000_led *led = container_of(ldev, + struct el15203000_led, + ldev); + + return el15203000_cmd(led, EL_OFF); +} + +static int el15203000_probe_dt(struct el15203000 *priv) +{ + struct el15203000_led *led = priv->leds; + struct fwnode_handle *child; + int ret; + + device_for_each_child_node(priv->dev, child) { + struct led_init_data init_data = {}; + + ret = fwnode_property_read_u32(child, "reg", &led->reg); + if (ret) { + dev_err(priv->dev, "LED without ID number"); + fwnode_handle_put(child); + + break; + } + + if (led->reg > U8_MAX) { + dev_err(priv->dev, "LED value %d is invalid", led->reg); + fwnode_handle_put(child); + + return -EINVAL; + } + + led->priv = priv; + led->ldev.max_brightness = LED_ON; + led->ldev.brightness_set_blocking = el15203000_set_blocking; + + if (led->reg == 'S') { + led->ldev.pattern_set = el15203000_pattern_set_S; + led->ldev.pattern_clear = el15203000_pattern_clear; + } else if (led->reg == 'P') { + led->ldev.pattern_set = el15203000_pattern_set_P; + led->ldev.pattern_clear = el15203000_pattern_clear; + } + + init_data.fwnode = child; + ret = devm_led_classdev_register_ext(priv->dev, &led->ldev, + &init_data); + if (ret) { + dev_err(priv->dev, + "failed to register LED device %s, err %d", + led->ldev.name, ret); + fwnode_handle_put(child); + + break; + } + + led++; + } + + return ret; +} + +static int el15203000_probe(struct spi_device *spi) +{ + struct el15203000 *priv; + size_t count; + + count = device_get_child_node_count(&spi->dev); + if (!count) { + dev_err(&spi->dev, "LEDs are not defined in device tree!"); + return -ENODEV; + } + + priv = devm_kzalloc(&spi->dev, struct_size(priv, leds, count), + GFP_KERNEL); + if (!priv) + return -ENOMEM; + + mutex_init(&priv->lock); + priv->count = count; + priv->dev = &spi->dev; + priv->spi = spi; + priv->delay = jiffies - + usecs_to_jiffies(EL_FW_DELAY_USEC); + + spi_set_drvdata(spi, priv); + + return el15203000_probe_dt(priv); +} + +static int el15203000_remove(struct spi_device *spi) +{ + struct el15203000 *priv = spi_get_drvdata(spi); + + mutex_destroy(&priv->lock); + + return 0; +} + +static const struct of_device_id el15203000_dt_ids[] = { + { .compatible = "crane,el15203000", }, + {}, +}; + +MODULE_DEVICE_TABLE(of, el15203000_dt_ids); + +static struct spi_driver el15203000_driver = { + .probe = el15203000_probe, + .remove = el15203000_remove, + .driver = { + .name = KBUILD_MODNAME, + .of_match_table = el15203000_dt_ids, + }, +}; + +module_spi_driver(el15203000_driver); + +MODULE_AUTHOR("Oleh Kravchenko <oleg@kaa.org.ua>"); +MODULE_DESCRIPTION("el15203000 LED driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("spi:el15203000"); diff --git a/drivers/leds/leds-fsg.c b/drivers/leds/leds-fsg.c new file mode 100644 index 000000000..bc6b42063 --- /dev/null +++ b/drivers/leds/leds-fsg.c @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED Driver for the Freecom FSG-3 + * + * Copyright (c) 2008 Rod Whitby <rod@whitby.id.au> + * + * Author: Rod Whitby <rod@whitby.id.au> + * + * Based on leds-spitz.c + * Copyright 2005-2006 Openedhand Ltd. + * Author: Richard Purdie <rpurdie@openedhand.com> + */ + +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/io.h> +#include <mach/hardware.h> + +#define FSG_LED_WLAN_BIT 0 +#define FSG_LED_WAN_BIT 1 +#define FSG_LED_SATA_BIT 2 +#define FSG_LED_USB_BIT 4 +#define FSG_LED_RING_BIT 5 +#define FSG_LED_SYNC_BIT 7 + +static short __iomem *latch_address; +static unsigned short latch_value; + + +static void fsg_led_wlan_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (value) { + latch_value &= ~(1 << FSG_LED_WLAN_BIT); + *latch_address = latch_value; + } else { + latch_value |= (1 << FSG_LED_WLAN_BIT); + *latch_address = latch_value; + } +} + +static void fsg_led_wan_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (value) { + latch_value &= ~(1 << FSG_LED_WAN_BIT); + *latch_address = latch_value; + } else { + latch_value |= (1 << FSG_LED_WAN_BIT); + *latch_address = latch_value; + } +} + +static void fsg_led_sata_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (value) { + latch_value &= ~(1 << FSG_LED_SATA_BIT); + *latch_address = latch_value; + } else { + latch_value |= (1 << FSG_LED_SATA_BIT); + *latch_address = latch_value; + } +} + +static void fsg_led_usb_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (value) { + latch_value &= ~(1 << FSG_LED_USB_BIT); + *latch_address = latch_value; + } else { + latch_value |= (1 << FSG_LED_USB_BIT); + *latch_address = latch_value; + } +} + +static void fsg_led_sync_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (value) { + latch_value &= ~(1 << FSG_LED_SYNC_BIT); + *latch_address = latch_value; + } else { + latch_value |= (1 << FSG_LED_SYNC_BIT); + *latch_address = latch_value; + } +} + +static void fsg_led_ring_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (value) { + latch_value &= ~(1 << FSG_LED_RING_BIT); + *latch_address = latch_value; + } else { + latch_value |= (1 << FSG_LED_RING_BIT); + *latch_address = latch_value; + } +} + + +static struct led_classdev fsg_wlan_led = { + .name = "fsg:blue:wlan", + .brightness_set = fsg_led_wlan_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static struct led_classdev fsg_wan_led = { + .name = "fsg:blue:wan", + .brightness_set = fsg_led_wan_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static struct led_classdev fsg_sata_led = { + .name = "fsg:blue:sata", + .brightness_set = fsg_led_sata_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static struct led_classdev fsg_usb_led = { + .name = "fsg:blue:usb", + .brightness_set = fsg_led_usb_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static struct led_classdev fsg_sync_led = { + .name = "fsg:blue:sync", + .brightness_set = fsg_led_sync_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static struct led_classdev fsg_ring_led = { + .name = "fsg:blue:ring", + .brightness_set = fsg_led_ring_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + + +static int fsg_led_probe(struct platform_device *pdev) +{ + int ret; + + /* Map the LED chip select address space */ + latch_address = (unsigned short *) devm_ioremap(&pdev->dev, + IXP4XX_EXP_BUS_BASE(2), 512); + if (!latch_address) + return -ENOMEM; + + latch_value = 0xffff; + *latch_address = latch_value; + + ret = devm_led_classdev_register(&pdev->dev, &fsg_wlan_led); + if (ret < 0) + return ret; + + ret = devm_led_classdev_register(&pdev->dev, &fsg_wan_led); + if (ret < 0) + return ret; + + ret = devm_led_classdev_register(&pdev->dev, &fsg_sata_led); + if (ret < 0) + return ret; + + ret = devm_led_classdev_register(&pdev->dev, &fsg_usb_led); + if (ret < 0) + return ret; + + ret = devm_led_classdev_register(&pdev->dev, &fsg_sync_led); + if (ret < 0) + return ret; + + ret = devm_led_classdev_register(&pdev->dev, &fsg_ring_led); + if (ret < 0) + return ret; + + return ret; +} + +static struct platform_driver fsg_led_driver = { + .probe = fsg_led_probe, + .driver = { + .name = "fsg-led", + }, +}; + +module_platform_driver(fsg_led_driver); + +MODULE_AUTHOR("Rod Whitby <rod@whitby.id.au>"); +MODULE_DESCRIPTION("Freecom FSG-3 LED driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-gpio-register.c b/drivers/leds/leds-gpio-register.c new file mode 100644 index 000000000..b9187e71e --- /dev/null +++ b/drivers/leds/leds-gpio-register.c @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2011 Pengutronix + * Uwe Kleine-Koenig <u.kleine-koenig@pengutronix.de> + */ +#include <linux/err.h> +#include <linux/leds.h> +#include <linux/platform_device.h> +#include <linux/slab.h> + +/** + * gpio_led_register_device - register a gpio-led device + * @pdata: the platform data used for the new device + * + * Makes a copy of pdata and pdata->leds and registers a new leds-gpio device + * with the result. This allows to have pdata and pdata-leds in .init.rodata + * and so saves some bytes compared to a static struct platform_device with + * static platform data. + * + * Returns the registered device or an error pointer. + */ +struct platform_device *__init gpio_led_register_device( + int id, const struct gpio_led_platform_data *pdata) +{ + struct platform_device *ret; + struct gpio_led_platform_data _pdata = *pdata; + + if (!pdata->num_leds) + return ERR_PTR(-EINVAL); + + _pdata.leds = kmemdup(pdata->leds, + pdata->num_leds * sizeof(*pdata->leds), GFP_KERNEL); + if (!_pdata.leds) + return ERR_PTR(-ENOMEM); + + ret = platform_device_register_resndata(NULL, "leds-gpio", id, + NULL, 0, &_pdata, sizeof(_pdata)); + if (IS_ERR(ret)) + kfree(_pdata.leds); + + return ret; +} diff --git a/drivers/leds/leds-gpio.c b/drivers/leds/leds-gpio.c new file mode 100644 index 000000000..93f5b1b60 --- /dev/null +++ b/drivers/leds/leds-gpio.c @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LEDs driver for GPIOs + * + * Copyright (C) 2007 8D Technologies inc. + * Raphael Assenat <raph@8d.com> + * Copyright (C) 2008 Freescale Semiconductor, Inc. + */ +#include <linux/err.h> +#include <linux/gpio.h> +#include <linux/gpio/consumer.h> +#include <linux/kernel.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/property.h> +#include <linux/slab.h> + +struct gpio_led_data { + struct led_classdev cdev; + struct gpio_desc *gpiod; + u8 can_sleep; + u8 blinking; + gpio_blink_set_t platform_gpio_blink_set; +}; + +static inline struct gpio_led_data * + cdev_to_gpio_led_data(struct led_classdev *led_cdev) +{ + return container_of(led_cdev, struct gpio_led_data, cdev); +} + +static void gpio_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct gpio_led_data *led_dat = cdev_to_gpio_led_data(led_cdev); + int level; + + if (value == LED_OFF) + level = 0; + else + level = 1; + + if (led_dat->blinking) { + led_dat->platform_gpio_blink_set(led_dat->gpiod, level, + NULL, NULL); + led_dat->blinking = 0; + } else { + if (led_dat->can_sleep) + gpiod_set_value_cansleep(led_dat->gpiod, level); + else + gpiod_set_value(led_dat->gpiod, level); + } +} + +static int gpio_led_set_blocking(struct led_classdev *led_cdev, + enum led_brightness value) +{ + gpio_led_set(led_cdev, value); + return 0; +} + +static int gpio_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, unsigned long *delay_off) +{ + struct gpio_led_data *led_dat = cdev_to_gpio_led_data(led_cdev); + + led_dat->blinking = 1; + return led_dat->platform_gpio_blink_set(led_dat->gpiod, GPIO_LED_BLINK, + delay_on, delay_off); +} + +static int create_gpio_led(const struct gpio_led *template, + struct gpio_led_data *led_dat, struct device *parent, + struct fwnode_handle *fwnode, gpio_blink_set_t blink_set) +{ + struct led_init_data init_data = {}; + int ret, state; + + led_dat->cdev.default_trigger = template->default_trigger; + led_dat->can_sleep = gpiod_cansleep(led_dat->gpiod); + if (!led_dat->can_sleep) + led_dat->cdev.brightness_set = gpio_led_set; + else + led_dat->cdev.brightness_set_blocking = gpio_led_set_blocking; + led_dat->blinking = 0; + if (blink_set) { + led_dat->platform_gpio_blink_set = blink_set; + led_dat->cdev.blink_set = gpio_blink_set; + } + if (template->default_state == LEDS_GPIO_DEFSTATE_KEEP) { + state = gpiod_get_value_cansleep(led_dat->gpiod); + if (state < 0) + return state; + } else { + state = (template->default_state == LEDS_GPIO_DEFSTATE_ON); + } + led_dat->cdev.brightness = state ? LED_FULL : LED_OFF; + if (!template->retain_state_suspended) + led_dat->cdev.flags |= LED_CORE_SUSPENDRESUME; + if (template->panic_indicator) + led_dat->cdev.flags |= LED_PANIC_INDICATOR; + if (template->retain_state_shutdown) + led_dat->cdev.flags |= LED_RETAIN_AT_SHUTDOWN; + + ret = gpiod_direction_output(led_dat->gpiod, state); + if (ret < 0) + return ret; + + if (template->name) { + led_dat->cdev.name = template->name; + ret = devm_led_classdev_register(parent, &led_dat->cdev); + } else { + init_data.fwnode = fwnode; + ret = devm_led_classdev_register_ext(parent, &led_dat->cdev, + &init_data); + } + + return ret; +} + +struct gpio_leds_priv { + int num_leds; + struct gpio_led_data leds[]; +}; + +static struct gpio_leds_priv *gpio_leds_create(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct fwnode_handle *child; + struct gpio_leds_priv *priv; + int count, ret; + + count = device_get_child_node_count(dev); + if (!count) + return ERR_PTR(-ENODEV); + + priv = devm_kzalloc(dev, struct_size(priv, leds, count), GFP_KERNEL); + if (!priv) + return ERR_PTR(-ENOMEM); + + device_for_each_child_node(dev, child) { + struct gpio_led_data *led_dat = &priv->leds[priv->num_leds]; + struct gpio_led led = {}; + const char *state = NULL; + + /* + * Acquire gpiod from DT with uninitialized label, which + * will be updated after LED class device is registered, + * Only then the final LED name is known. + */ + led.gpiod = devm_fwnode_get_gpiod_from_child(dev, NULL, child, + GPIOD_ASIS, + NULL); + if (IS_ERR(led.gpiod)) { + fwnode_handle_put(child); + return ERR_CAST(led.gpiod); + } + + led_dat->gpiod = led.gpiod; + + if (!fwnode_property_read_string(child, "default-state", + &state)) { + if (!strcmp(state, "keep")) + led.default_state = LEDS_GPIO_DEFSTATE_KEEP; + else if (!strcmp(state, "on")) + led.default_state = LEDS_GPIO_DEFSTATE_ON; + else + led.default_state = LEDS_GPIO_DEFSTATE_OFF; + } + + if (fwnode_property_present(child, "retain-state-suspended")) + led.retain_state_suspended = 1; + if (fwnode_property_present(child, "retain-state-shutdown")) + led.retain_state_shutdown = 1; + if (fwnode_property_present(child, "panic-indicator")) + led.panic_indicator = 1; + + ret = create_gpio_led(&led, led_dat, dev, child, NULL); + if (ret < 0) { + fwnode_handle_put(child); + return ERR_PTR(ret); + } + /* Set gpiod label to match the corresponding LED name. */ + gpiod_set_consumer_name(led_dat->gpiod, + led_dat->cdev.dev->kobj.name); + priv->num_leds++; + } + + return priv; +} + +static const struct of_device_id of_gpio_leds_match[] = { + { .compatible = "gpio-leds", }, + {}, +}; + +MODULE_DEVICE_TABLE(of, of_gpio_leds_match); + +static struct gpio_desc *gpio_led_get_gpiod(struct device *dev, int idx, + const struct gpio_led *template) +{ + struct gpio_desc *gpiod; + unsigned long flags = GPIOF_OUT_INIT_LOW; + int ret; + + /* + * This means the LED does not come from the device tree + * or ACPI, so let's try just getting it by index from the + * device, this will hit the board file, if any and get + * the GPIO from there. + */ + gpiod = devm_gpiod_get_index(dev, NULL, idx, GPIOD_OUT_LOW); + if (!IS_ERR(gpiod)) { + gpiod_set_consumer_name(gpiod, template->name); + return gpiod; + } + if (PTR_ERR(gpiod) != -ENOENT) + return gpiod; + + /* + * This is the legacy code path for platform code that + * still uses GPIO numbers. Ultimately we would like to get + * rid of this block completely. + */ + + /* skip leds that aren't available */ + if (!gpio_is_valid(template->gpio)) + return ERR_PTR(-ENOENT); + + if (template->active_low) + flags |= GPIOF_ACTIVE_LOW; + + ret = devm_gpio_request_one(dev, template->gpio, flags, + template->name); + if (ret < 0) + return ERR_PTR(ret); + + gpiod = gpio_to_desc(template->gpio); + if (!gpiod) + return ERR_PTR(-EINVAL); + + return gpiod; +} + +static int gpio_led_probe(struct platform_device *pdev) +{ + struct gpio_led_platform_data *pdata = dev_get_platdata(&pdev->dev); + struct gpio_leds_priv *priv; + int i, ret = 0; + + if (pdata && pdata->num_leds) { + priv = devm_kzalloc(&pdev->dev, struct_size(priv, leds, pdata->num_leds), + GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->num_leds = pdata->num_leds; + for (i = 0; i < priv->num_leds; i++) { + const struct gpio_led *template = &pdata->leds[i]; + struct gpio_led_data *led_dat = &priv->leds[i]; + + if (template->gpiod) + led_dat->gpiod = template->gpiod; + else + led_dat->gpiod = + gpio_led_get_gpiod(&pdev->dev, + i, template); + if (IS_ERR(led_dat->gpiod)) { + dev_info(&pdev->dev, "Skipping unavailable LED gpio %d (%s)\n", + template->gpio, template->name); + continue; + } + + ret = create_gpio_led(template, led_dat, + &pdev->dev, NULL, + pdata->gpio_blink_set); + if (ret < 0) + return ret; + } + } else { + priv = gpio_leds_create(pdev); + if (IS_ERR(priv)) + return PTR_ERR(priv); + } + + platform_set_drvdata(pdev, priv); + + return 0; +} + +static void gpio_led_shutdown(struct platform_device *pdev) +{ + struct gpio_leds_priv *priv = platform_get_drvdata(pdev); + int i; + + for (i = 0; i < priv->num_leds; i++) { + struct gpio_led_data *led = &priv->leds[i]; + + if (!(led->cdev.flags & LED_RETAIN_AT_SHUTDOWN)) + gpio_led_set(&led->cdev, LED_OFF); + } +} + +static struct platform_driver gpio_led_driver = { + .probe = gpio_led_probe, + .shutdown = gpio_led_shutdown, + .driver = { + .name = "leds-gpio", + .of_match_table = of_gpio_leds_match, + }, +}; + +module_platform_driver(gpio_led_driver); + +MODULE_AUTHOR("Raphael Assenat <raph@8d.com>, Trent Piepho <tpiepho@freescale.com>"); +MODULE_DESCRIPTION("GPIO LED driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:leds-gpio"); diff --git a/drivers/leds/leds-hp6xx.c b/drivers/leds/leds-hp6xx.c new file mode 100644 index 000000000..54af9e63c --- /dev/null +++ b/drivers/leds/leds-hp6xx.c @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED Triggers Core + * For the HP Jornada 620/660/680/690 handhelds + * + * Copyright 2008 Kristoffer Ericson <kristoffer.ericson@gmail.com> + * this driver is based on leds-spitz.c by Richard Purdie. + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <asm/hd64461.h> +#include <mach/hp6xx.h> + +static void hp6xxled_green_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + u8 v8; + + v8 = inb(PKDR); + if (value) + outb(v8 & (~PKDR_LED_GREEN), PKDR); + else + outb(v8 | PKDR_LED_GREEN, PKDR); +} + +static void hp6xxled_red_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + u16 v16; + + v16 = inw(HD64461_GPBDR); + if (value) + outw(v16 & (~HD64461_GPBDR_LED_RED), HD64461_GPBDR); + else + outw(v16 | HD64461_GPBDR_LED_RED, HD64461_GPBDR); +} + +static struct led_classdev hp6xx_red_led = { + .name = "hp6xx:red", + .default_trigger = "hp6xx-charge", + .brightness_set = hp6xxled_red_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static struct led_classdev hp6xx_green_led = { + .name = "hp6xx:green", + .default_trigger = "disk-activity", + .brightness_set = hp6xxled_green_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static int hp6xxled_probe(struct platform_device *pdev) +{ + int ret; + + ret = devm_led_classdev_register(&pdev->dev, &hp6xx_red_led); + if (ret < 0) + return ret; + + return devm_led_classdev_register(&pdev->dev, &hp6xx_green_led); +} + +static struct platform_driver hp6xxled_driver = { + .probe = hp6xxled_probe, + .driver = { + .name = "hp6xx-led", + }, +}; + +module_platform_driver(hp6xxled_driver); + +MODULE_AUTHOR("Kristoffer Ericson <kristoffer.ericson@gmail.com>"); +MODULE_DESCRIPTION("HP Jornada 6xx LED driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:hp6xx-led"); diff --git a/drivers/leds/leds-ip30.c b/drivers/leds/leds-ip30.c new file mode 100644 index 000000000..1f952bad0 --- /dev/null +++ b/drivers/leds/leds-ip30.c @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * LED Driver for SGI Octane machines + */ + +#include <asm/io.h> +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/leds.h> + +#define IP30_LED_SYSTEM 0 +#define IP30_LED_FAULT 1 + +struct ip30_led { + struct led_classdev cdev; + u32 __iomem *reg; +}; + +static void ip30led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct ip30_led *led = container_of(led_cdev, struct ip30_led, cdev); + + writel(value, led->reg); +} + +static int ip30led_create(struct platform_device *pdev, int num) +{ + struct resource *res; + struct ip30_led *data; + + res = platform_get_resource(pdev, IORESOURCE_MEM, num); + if (!res) + return -EBUSY; + + data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->reg = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(data->reg)) + return PTR_ERR(data->reg); + + + switch (num) { + case IP30_LED_SYSTEM: + data->cdev.name = "white:power"; + break; + case IP30_LED_FAULT: + data->cdev.name = "red:fault"; + break; + default: + return -EINVAL; + } + + data->cdev.brightness = readl(data->reg); + data->cdev.max_brightness = 1; + data->cdev.brightness_set = ip30led_set; + + return devm_led_classdev_register(&pdev->dev, &data->cdev); +} + +static int ip30led_probe(struct platform_device *pdev) +{ + int ret; + + ret = ip30led_create(pdev, IP30_LED_SYSTEM); + if (ret < 0) + return ret; + + return ip30led_create(pdev, IP30_LED_FAULT); +} + +static struct platform_driver ip30led_driver = { + .probe = ip30led_probe, + .driver = { + .name = "ip30-leds", + }, +}; + +module_platform_driver(ip30led_driver); + +MODULE_AUTHOR("Thomas Bogendoerfer <tbogendoerfer@suse.de>"); +MODULE_DESCRIPTION("SGI Octane LED driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:ip30-leds"); diff --git a/drivers/leds/leds-ipaq-micro.c b/drivers/leds/leds-ipaq-micro.c new file mode 100644 index 000000000..504a95b6e --- /dev/null +++ b/drivers/leds/leds-ipaq-micro.c @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * + * h3xxx atmel micro companion support, notification LED subdevice + * + * Author : Linus Walleij <linus.walleij@linaro.org> + */ + +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/mfd/ipaq-micro.h> +#include <linux/leds.h> + +#define LED_YELLOW 0x00 +#define LED_GREEN 0x01 + +#define LED_EN (1 << 4) /* LED ON/OFF 0:off, 1:on */ +#define LED_AUTOSTOP (1 << 5) /* LED ON/OFF auto stop set 0:disable, 1:enable */ +#define LED_ALWAYS (1 << 6) /* LED Interrupt Mask 0:No mask, 1:mask */ + +static int micro_leds_brightness_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct ipaq_micro *micro = dev_get_drvdata(led_cdev->dev->parent->parent); + /* + * In this message: + * Byte 0 = LED color: 0 = yellow, 1 = green + * yellow LED is always ~30 blinks per minute + * Byte 1 = duration (flags?) appears to be ignored + * Byte 2 = green ontime in 1/10 sec (deciseconds) + * 1 = 1/10 second + * 0 = 256/10 second + * Byte 3 = green offtime in 1/10 sec (deciseconds) + * 1 = 1/10 second + * 0 = 256/10 seconds + */ + struct ipaq_micro_msg msg = { + .id = MSG_NOTIFY_LED, + .tx_len = 4, + }; + + msg.tx_data[0] = LED_GREEN; + msg.tx_data[1] = 0; + if (value) { + msg.tx_data[2] = 0; /* Duty cycle 256 */ + msg.tx_data[3] = 1; + } else { + msg.tx_data[2] = 1; + msg.tx_data[3] = 0; /* Duty cycle 256 */ + } + return ipaq_micro_tx_msg_sync(micro, &msg); +} + +/* Maximum duty cycle in ms 256/10 sec = 25600 ms */ +#define IPAQ_LED_MAX_DUTY 25600 + +static int micro_leds_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct ipaq_micro *micro = dev_get_drvdata(led_cdev->dev->parent->parent); + /* + * In this message: + * Byte 0 = LED color: 0 = yellow, 1 = green + * yellow LED is always ~30 blinks per minute + * Byte 1 = duration (flags?) appears to be ignored + * Byte 2 = green ontime in 1/10 sec (deciseconds) + * 1 = 1/10 second + * 0 = 256/10 second + * Byte 3 = green offtime in 1/10 sec (deciseconds) + * 1 = 1/10 second + * 0 = 256/10 seconds + */ + struct ipaq_micro_msg msg = { + .id = MSG_NOTIFY_LED, + .tx_len = 4, + }; + + msg.tx_data[0] = LED_GREEN; + if (*delay_on > IPAQ_LED_MAX_DUTY || + *delay_off > IPAQ_LED_MAX_DUTY) + return -EINVAL; + + if (*delay_on == 0 && *delay_off == 0) { + *delay_on = 100; + *delay_off = 100; + } + + msg.tx_data[1] = 0; + if (*delay_on >= IPAQ_LED_MAX_DUTY) + msg.tx_data[2] = 0; + else + msg.tx_data[2] = (u8) DIV_ROUND_CLOSEST(*delay_on, 100); + if (*delay_off >= IPAQ_LED_MAX_DUTY) + msg.tx_data[3] = 0; + else + msg.tx_data[3] = (u8) DIV_ROUND_CLOSEST(*delay_off, 100); + return ipaq_micro_tx_msg_sync(micro, &msg); +} + +static struct led_classdev micro_led = { + .name = "led-ipaq-micro", + .brightness_set_blocking = micro_leds_brightness_set, + .blink_set = micro_leds_blink_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static int micro_leds_probe(struct platform_device *pdev) +{ + int ret; + + ret = devm_led_classdev_register(&pdev->dev, µ_led); + if (ret) { + dev_err(&pdev->dev, "registering led failed: %d\n", ret); + return ret; + } + dev_info(&pdev->dev, "iPAQ micro notification LED driver\n"); + + return 0; +} + +static struct platform_driver micro_leds_device_driver = { + .driver = { + .name = "ipaq-micro-leds", + }, + .probe = micro_leds_probe, +}; +module_platform_driver(micro_leds_device_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("driver for iPAQ Atmel micro leds"); +MODULE_ALIAS("platform:ipaq-micro-leds"); diff --git a/drivers/leds/leds-is31fl319x.c b/drivers/leds/leds-is31fl319x.c new file mode 100644 index 000000000..4161b9dd7 --- /dev/null +++ b/drivers/leds/leds-is31fl319x.c @@ -0,0 +1,455 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2015-16 Golden Delicious Computers + * + * Author: Nikolaus Schaller <hns@goldelico.com> + * + * LED driver for the IS31FL319{0,1,3,6,9} to drive 1, 3, 6 or 9 light + * effect LEDs. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <linux/delay.h> +#include <linux/gpio/consumer.h> + +/* register numbers */ +#define IS31FL319X_SHUTDOWN 0x00 +#define IS31FL319X_CTRL1 0x01 +#define IS31FL319X_CTRL2 0x02 +#define IS31FL319X_CONFIG1 0x03 +#define IS31FL319X_CONFIG2 0x04 +#define IS31FL319X_RAMP_MODE 0x05 +#define IS31FL319X_BREATH_MASK 0x06 +#define IS31FL319X_PWM(channel) (0x07 + channel) +#define IS31FL319X_DATA_UPDATE 0x10 +#define IS31FL319X_T0(channel) (0x11 + channel) +#define IS31FL319X_T123_1 0x1a +#define IS31FL319X_T123_2 0x1b +#define IS31FL319X_T123_3 0x1c +#define IS31FL319X_T4(channel) (0x1d + channel) +#define IS31FL319X_TIME_UPDATE 0x26 +#define IS31FL319X_RESET 0xff + +#define IS31FL319X_REG_CNT (IS31FL319X_RESET + 1) + +#define IS31FL319X_MAX_LEDS 9 + +/* CS (Current Setting) in CONFIG2 register */ +#define IS31FL319X_CONFIG2_CS_SHIFT 4 +#define IS31FL319X_CONFIG2_CS_MASK 0x7 +#define IS31FL319X_CONFIG2_CS_STEP_REF 12 + +#define IS31FL319X_CURRENT_MIN ((u32)5000) +#define IS31FL319X_CURRENT_MAX ((u32)40000) +#define IS31FL319X_CURRENT_STEP ((u32)5000) +#define IS31FL319X_CURRENT_DEFAULT ((u32)20000) + +/* Audio gain in CONFIG2 register */ +#define IS31FL319X_AUDIO_GAIN_DB_MAX ((u32)21) +#define IS31FL319X_AUDIO_GAIN_DB_STEP ((u32)3) + +/* + * regmap is used as a cache of chip's register space, + * to avoid reading back brightness values from chip, + * which is known to hang. + */ +struct is31fl319x_chip { + const struct is31fl319x_chipdef *cdef; + struct i2c_client *client; + struct gpio_desc *shutdown_gpio; + struct regmap *regmap; + struct mutex lock; + u32 audio_gain_db; + + struct is31fl319x_led { + struct is31fl319x_chip *chip; + struct led_classdev cdev; + u32 max_microamp; + bool configured; + } leds[IS31FL319X_MAX_LEDS]; +}; + +struct is31fl319x_chipdef { + int num_leds; +}; + +static const struct is31fl319x_chipdef is31fl3190_cdef = { + .num_leds = 1, +}; + +static const struct is31fl319x_chipdef is31fl3193_cdef = { + .num_leds = 3, +}; + +static const struct is31fl319x_chipdef is31fl3196_cdef = { + .num_leds = 6, +}; + +static const struct is31fl319x_chipdef is31fl3199_cdef = { + .num_leds = 9, +}; + +static const struct of_device_id of_is31fl319x_match[] = { + { .compatible = "issi,is31fl3190", .data = &is31fl3190_cdef, }, + { .compatible = "issi,is31fl3191", .data = &is31fl3190_cdef, }, + { .compatible = "issi,is31fl3193", .data = &is31fl3193_cdef, }, + { .compatible = "issi,is31fl3196", .data = &is31fl3196_cdef, }, + { .compatible = "issi,is31fl3199", .data = &is31fl3199_cdef, }, + { .compatible = "si-en,sn3199", .data = &is31fl3199_cdef, }, + { } +}; +MODULE_DEVICE_TABLE(of, of_is31fl319x_match); + +static int is31fl319x_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct is31fl319x_led *led = container_of(cdev, struct is31fl319x_led, + cdev); + struct is31fl319x_chip *is31 = led->chip; + int chan = led - is31->leds; + int ret; + int i; + u8 ctrl1 = 0, ctrl2 = 0; + + dev_dbg(&is31->client->dev, "%s %d: %d\n", __func__, chan, brightness); + + mutex_lock(&is31->lock); + + /* update PWM register */ + ret = regmap_write(is31->regmap, IS31FL319X_PWM(chan), brightness); + if (ret < 0) + goto out; + + /* read current brightness of all PWM channels */ + for (i = 0; i < is31->cdef->num_leds; i++) { + unsigned int pwm_value; + bool on; + + /* + * since neither cdev nor the chip can provide + * the current setting, we read from the regmap cache + */ + + ret = regmap_read(is31->regmap, IS31FL319X_PWM(i), &pwm_value); + dev_dbg(&is31->client->dev, "%s read %d: ret=%d: %d\n", + __func__, i, ret, pwm_value); + on = ret >= 0 && pwm_value > LED_OFF; + + if (i < 3) + ctrl1 |= on << i; /* 0..2 => bit 0..2 */ + else if (i < 6) + ctrl1 |= on << (i + 1); /* 3..5 => bit 4..6 */ + else + ctrl2 |= on << (i - 6); /* 6..8 => bit 0..2 */ + } + + if (ctrl1 > 0 || ctrl2 > 0) { + dev_dbg(&is31->client->dev, "power up %02x %02x\n", + ctrl1, ctrl2); + regmap_write(is31->regmap, IS31FL319X_CTRL1, ctrl1); + regmap_write(is31->regmap, IS31FL319X_CTRL2, ctrl2); + /* update PWMs */ + regmap_write(is31->regmap, IS31FL319X_DATA_UPDATE, 0x00); + /* enable chip from shut down */ + ret = regmap_write(is31->regmap, IS31FL319X_SHUTDOWN, 0x01); + } else { + dev_dbg(&is31->client->dev, "power down\n"); + /* shut down (no need to clear CTRL1/2) */ + ret = regmap_write(is31->regmap, IS31FL319X_SHUTDOWN, 0x00); + } + +out: + mutex_unlock(&is31->lock); + + return ret; +} + +static int is31fl319x_parse_child_dt(const struct device *dev, + const struct device_node *child, + struct is31fl319x_led *led) +{ + struct led_classdev *cdev = &led->cdev; + int ret; + + if (of_property_read_string(child, "label", &cdev->name)) + cdev->name = child->name; + + ret = of_property_read_string(child, "linux,default-trigger", + &cdev->default_trigger); + if (ret < 0 && ret != -EINVAL) /* is optional */ + return ret; + + led->max_microamp = IS31FL319X_CURRENT_DEFAULT; + ret = of_property_read_u32(child, "led-max-microamp", + &led->max_microamp); + if (!ret) { + if (led->max_microamp < IS31FL319X_CURRENT_MIN) + return -EINVAL; /* not supported */ + led->max_microamp = min(led->max_microamp, + IS31FL319X_CURRENT_MAX); + } + + return 0; +} + +static int is31fl319x_parse_dt(struct device *dev, + struct is31fl319x_chip *is31) +{ + struct device_node *np = dev_of_node(dev), *child; + int count; + int ret; + + if (!np) + return -ENODEV; + + is31->shutdown_gpio = devm_gpiod_get_optional(dev, + "shutdown", + GPIOD_OUT_HIGH); + if (IS_ERR(is31->shutdown_gpio)) { + ret = PTR_ERR(is31->shutdown_gpio); + dev_err(dev, "Failed to get shutdown gpio: %d\n", ret); + return ret; + } + + is31->cdef = device_get_match_data(dev); + + count = of_get_available_child_count(np); + + dev_dbg(dev, "probing with %d leds defined in DT\n", count); + + if (!count || count > is31->cdef->num_leds) { + dev_err(dev, "Number of leds defined must be between 1 and %u\n", + is31->cdef->num_leds); + return -ENODEV; + } + + for_each_available_child_of_node(np, child) { + struct is31fl319x_led *led; + u32 reg; + + ret = of_property_read_u32(child, "reg", ®); + if (ret) { + dev_err(dev, "Failed to read led 'reg' property\n"); + goto put_child_node; + } + + if (reg < 1 || reg > is31->cdef->num_leds) { + dev_err(dev, "invalid led reg %u\n", reg); + ret = -EINVAL; + goto put_child_node; + } + + led = &is31->leds[reg - 1]; + + if (led->configured) { + dev_err(dev, "led %u is already configured\n", reg); + ret = -EINVAL; + goto put_child_node; + } + + ret = is31fl319x_parse_child_dt(dev, child, led); + if (ret) { + dev_err(dev, "led %u DT parsing failed\n", reg); + goto put_child_node; + } + + led->configured = true; + } + + is31->audio_gain_db = 0; + ret = of_property_read_u32(np, "audio-gain-db", &is31->audio_gain_db); + if (!ret) + is31->audio_gain_db = min(is31->audio_gain_db, + IS31FL319X_AUDIO_GAIN_DB_MAX); + + return 0; + +put_child_node: + of_node_put(child); + return ret; +} + +static bool is31fl319x_readable_reg(struct device *dev, unsigned int reg) +{ /* we have no readable registers */ + return false; +} + +static bool is31fl319x_volatile_reg(struct device *dev, unsigned int reg) +{ /* volatile registers are not cached */ + switch (reg) { + case IS31FL319X_DATA_UPDATE: + case IS31FL319X_TIME_UPDATE: + case IS31FL319X_RESET: + return true; /* always write-through */ + default: + return false; + } +} + +static const struct reg_default is31fl319x_reg_defaults[] = { + { IS31FL319X_CONFIG1, 0x00}, + { IS31FL319X_CONFIG2, 0x00}, + { IS31FL319X_PWM(0), 0x00}, + { IS31FL319X_PWM(1), 0x00}, + { IS31FL319X_PWM(2), 0x00}, + { IS31FL319X_PWM(3), 0x00}, + { IS31FL319X_PWM(4), 0x00}, + { IS31FL319X_PWM(5), 0x00}, + { IS31FL319X_PWM(6), 0x00}, + { IS31FL319X_PWM(7), 0x00}, + { IS31FL319X_PWM(8), 0x00}, +}; + +static struct regmap_config regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = IS31FL319X_REG_CNT, + .cache_type = REGCACHE_FLAT, + .readable_reg = is31fl319x_readable_reg, + .volatile_reg = is31fl319x_volatile_reg, + .reg_defaults = is31fl319x_reg_defaults, + .num_reg_defaults = ARRAY_SIZE(is31fl319x_reg_defaults), +}; + +static inline int is31fl319x_microamp_to_cs(struct device *dev, u32 microamp) +{ /* round down to nearest supported value (range check done by caller) */ + u32 step = microamp / IS31FL319X_CURRENT_STEP; + + return ((IS31FL319X_CONFIG2_CS_STEP_REF - step) & + IS31FL319X_CONFIG2_CS_MASK) << + IS31FL319X_CONFIG2_CS_SHIFT; /* CS encoding */ +} + +static inline int is31fl319x_db_to_gain(u32 dezibel) +{ /* round down to nearest supported value (range check done by caller) */ + return dezibel / IS31FL319X_AUDIO_GAIN_DB_STEP; +} + +static int is31fl319x_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct is31fl319x_chip *is31; + struct device *dev = &client->dev; + int err; + int i = 0; + u32 aggregated_led_microamp = IS31FL319X_CURRENT_MAX; + + if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) + return -EIO; + + is31 = devm_kzalloc(&client->dev, sizeof(*is31), GFP_KERNEL); + if (!is31) + return -ENOMEM; + + mutex_init(&is31->lock); + + err = is31fl319x_parse_dt(&client->dev, is31); + if (err) + goto free_mutex; + + if (is31->shutdown_gpio) { + gpiod_direction_output(is31->shutdown_gpio, 0); + mdelay(5); + gpiod_direction_output(is31->shutdown_gpio, 1); + } + + is31->client = client; + is31->regmap = devm_regmap_init_i2c(client, ®map_config); + if (IS_ERR(is31->regmap)) { + dev_err(&client->dev, "failed to allocate register map\n"); + err = PTR_ERR(is31->regmap); + goto free_mutex; + } + + i2c_set_clientdata(client, is31); + + /* check for write-reply from chip (we can't read any registers) */ + err = regmap_write(is31->regmap, IS31FL319X_RESET, 0x00); + if (err < 0) { + dev_err(&client->dev, "no response from chip write: err = %d\n", + err); + err = -EIO; /* does not answer */ + goto free_mutex; + } + + /* + * Kernel conventions require per-LED led-max-microamp property. + * But the chip does not allow to limit individual LEDs. + * So we take minimum from all subnodes for safety of hardware. + */ + for (i = 0; i < is31->cdef->num_leds; i++) + if (is31->leds[i].configured && + is31->leds[i].max_microamp < aggregated_led_microamp) + aggregated_led_microamp = is31->leds[i].max_microamp; + + regmap_write(is31->regmap, IS31FL319X_CONFIG2, + is31fl319x_microamp_to_cs(dev, aggregated_led_microamp) | + is31fl319x_db_to_gain(is31->audio_gain_db)); + + for (i = 0; i < is31->cdef->num_leds; i++) { + struct is31fl319x_led *led = &is31->leds[i]; + + if (!led->configured) + continue; + + led->chip = is31; + led->cdev.brightness_set_blocking = is31fl319x_brightness_set; + + err = devm_led_classdev_register(&client->dev, &led->cdev); + if (err < 0) + goto free_mutex; + } + + return 0; + +free_mutex: + mutex_destroy(&is31->lock); + return err; +} + +static int is31fl319x_remove(struct i2c_client *client) +{ + struct is31fl319x_chip *is31 = i2c_get_clientdata(client); + + mutex_destroy(&is31->lock); + return 0; +} + +/* + * i2c-core (and modalias) requires that id_table be properly filled, + * even though it is not used for DeviceTree based instantiation. + */ +static const struct i2c_device_id is31fl319x_id[] = { + { "is31fl3190" }, + { "is31fl3191" }, + { "is31fl3193" }, + { "is31fl3196" }, + { "is31fl3199" }, + { "sn3199" }, + {}, +}; +MODULE_DEVICE_TABLE(i2c, is31fl319x_id); + +static struct i2c_driver is31fl319x_driver = { + .driver = { + .name = "leds-is31fl319x", + .of_match_table = of_match_ptr(of_is31fl319x_match), + }, + .probe = is31fl319x_probe, + .remove = is31fl319x_remove, + .id_table = is31fl319x_id, +}; + +module_i2c_driver(is31fl319x_driver); + +MODULE_AUTHOR("H. Nikolaus Schaller <hns@goldelico.com>"); +MODULE_AUTHOR("Andrey Utkin <andrey_utkin@fastmail.com>"); +MODULE_DESCRIPTION("IS31FL319X LED driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-is31fl32xx.c b/drivers/leds/leds-is31fl32xx.c new file mode 100644 index 000000000..899ed94b6 --- /dev/null +++ b/drivers/leds/leds-is31fl32xx.c @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Driver for ISSI IS31FL32xx family of I2C LED controllers + * + * Copyright 2015 Allworx Corp. + * + * Datasheets: + * http://www.issi.com/US/product-analog-fxled-driver.shtml + * http://www.si-en.com/product.asp?parentid=890 + */ + +#include <linux/device.h> +#include <linux/i2c.h> +#include <linux/kernel.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> + +/* Used to indicate a device has no such register */ +#define IS31FL32XX_REG_NONE 0xFF + +/* Software Shutdown bit in Shutdown Register */ +#define IS31FL32XX_SHUTDOWN_SSD_ENABLE 0 +#define IS31FL32XX_SHUTDOWN_SSD_DISABLE BIT(0) + +/* IS31FL3216 has a number of unique registers */ +#define IS31FL3216_CONFIG_REG 0x00 +#define IS31FL3216_LIGHTING_EFFECT_REG 0x03 +#define IS31FL3216_CHANNEL_CONFIG_REG 0x04 + +/* Software Shutdown bit in 3216 Config Register */ +#define IS31FL3216_CONFIG_SSD_ENABLE BIT(7) +#define IS31FL3216_CONFIG_SSD_DISABLE 0 + +struct is31fl32xx_priv; +struct is31fl32xx_led_data { + struct led_classdev cdev; + u8 channel; /* 1-based, max priv->cdef->channels */ + struct is31fl32xx_priv *priv; +}; + +struct is31fl32xx_priv { + const struct is31fl32xx_chipdef *cdef; + struct i2c_client *client; + unsigned int num_leds; + struct is31fl32xx_led_data leds[]; +}; + +/** + * struct is31fl32xx_chipdef - chip-specific attributes + * @channels : Number of LED channels + * @shutdown_reg : address of Shutdown register (optional) + * @pwm_update_reg : address of PWM Update register + * @global_control_reg : address of Global Control register (optional) + * @reset_reg : address of Reset register (optional) + * @pwm_register_base : address of first PWM register + * @pwm_registers_reversed: : true if PWM registers count down instead of up + * @led_control_register_base : address of first LED control register (optional) + * @enable_bits_per_led_control_register: number of LEDs enable bits in each + * @reset_func: : pointer to reset function + * + * For all optional register addresses, the sentinel value %IS31FL32XX_REG_NONE + * indicates that this chip has no such register. + * + * If non-NULL, @reset_func will be called during probing to set all + * necessary registers to a known initialization state. This is needed + * for chips that do not have a @reset_reg. + * + * @enable_bits_per_led_control_register must be >=1 if + * @led_control_register_base != %IS31FL32XX_REG_NONE. + */ +struct is31fl32xx_chipdef { + u8 channels; + u8 shutdown_reg; + u8 pwm_update_reg; + u8 global_control_reg; + u8 reset_reg; + u8 pwm_register_base; + bool pwm_registers_reversed; + u8 led_control_register_base; + u8 enable_bits_per_led_control_register; + int (*reset_func)(struct is31fl32xx_priv *priv); + int (*sw_shutdown_func)(struct is31fl32xx_priv *priv, bool enable); +}; + +static const struct is31fl32xx_chipdef is31fl3236_cdef = { + .channels = 36, + .shutdown_reg = 0x00, + .pwm_update_reg = 0x25, + .global_control_reg = 0x4a, + .reset_reg = 0x4f, + .pwm_register_base = 0x01, + .led_control_register_base = 0x26, + .enable_bits_per_led_control_register = 1, +}; + +static const struct is31fl32xx_chipdef is31fl3235_cdef = { + .channels = 28, + .shutdown_reg = 0x00, + .pwm_update_reg = 0x25, + .global_control_reg = 0x4a, + .reset_reg = 0x4f, + .pwm_register_base = 0x05, + .led_control_register_base = 0x2a, + .enable_bits_per_led_control_register = 1, +}; + +static const struct is31fl32xx_chipdef is31fl3218_cdef = { + .channels = 18, + .shutdown_reg = 0x00, + .pwm_update_reg = 0x16, + .global_control_reg = IS31FL32XX_REG_NONE, + .reset_reg = 0x17, + .pwm_register_base = 0x01, + .led_control_register_base = 0x13, + .enable_bits_per_led_control_register = 6, +}; + +static int is31fl3216_reset(struct is31fl32xx_priv *priv); +static int is31fl3216_software_shutdown(struct is31fl32xx_priv *priv, + bool enable); +static const struct is31fl32xx_chipdef is31fl3216_cdef = { + .channels = 16, + .shutdown_reg = IS31FL32XX_REG_NONE, + .pwm_update_reg = 0xB0, + .global_control_reg = IS31FL32XX_REG_NONE, + .reset_reg = IS31FL32XX_REG_NONE, + .pwm_register_base = 0x10, + .pwm_registers_reversed = true, + .led_control_register_base = 0x01, + .enable_bits_per_led_control_register = 8, + .reset_func = is31fl3216_reset, + .sw_shutdown_func = is31fl3216_software_shutdown, +}; + +static int is31fl32xx_write(struct is31fl32xx_priv *priv, u8 reg, u8 val) +{ + int ret; + + dev_dbg(&priv->client->dev, "writing register 0x%02X=0x%02X", reg, val); + + ret = i2c_smbus_write_byte_data(priv->client, reg, val); + if (ret) { + dev_err(&priv->client->dev, + "register write to 0x%02X failed (error %d)", + reg, ret); + } + return ret; +} + +/* + * Custom reset function for IS31FL3216 because it does not have a RESET + * register the way that the other IS31FL32xx chips do. We don't bother + * writing the GPIO and animation registers, because the registers we + * do write ensure those will have no effect. + */ +static int is31fl3216_reset(struct is31fl32xx_priv *priv) +{ + unsigned int i; + int ret; + + ret = is31fl32xx_write(priv, IS31FL3216_CONFIG_REG, + IS31FL3216_CONFIG_SSD_ENABLE); + if (ret) + return ret; + for (i = 0; i < priv->cdef->channels; i++) { + ret = is31fl32xx_write(priv, priv->cdef->pwm_register_base+i, + 0x00); + if (ret) + return ret; + } + ret = is31fl32xx_write(priv, priv->cdef->pwm_update_reg, 0); + if (ret) + return ret; + ret = is31fl32xx_write(priv, IS31FL3216_LIGHTING_EFFECT_REG, 0x00); + if (ret) + return ret; + ret = is31fl32xx_write(priv, IS31FL3216_CHANNEL_CONFIG_REG, 0x00); + if (ret) + return ret; + + return 0; +} + +/* + * Custom Software-Shutdown function for IS31FL3216 because it does not have + * a SHUTDOWN register the way that the other IS31FL32xx chips do. + * We don't bother doing a read/modify/write on the CONFIG register because + * we only ever use a value of '0' for the other fields in that register. + */ +static int is31fl3216_software_shutdown(struct is31fl32xx_priv *priv, + bool enable) +{ + u8 value = enable ? IS31FL3216_CONFIG_SSD_ENABLE : + IS31FL3216_CONFIG_SSD_DISABLE; + + return is31fl32xx_write(priv, IS31FL3216_CONFIG_REG, value); +} + +/* + * NOTE: A mutex is not needed in this function because: + * - All referenced data is read-only after probe() + * - The I2C core has a mutex on to protect the bus + * - There are no read/modify/write operations + * - Intervening operations between the write of the PWM register + * and the Update register are harmless. + * + * Example: + * PWM_REG_1 write 16 + * UPDATE_REG write 0 + * PWM_REG_2 write 128 + * UPDATE_REG write 0 + * vs: + * PWM_REG_1 write 16 + * PWM_REG_2 write 128 + * UPDATE_REG write 0 + * UPDATE_REG write 0 + * are equivalent. Poking the Update register merely applies all PWM + * register writes up to that point. + */ +static int is31fl32xx_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + const struct is31fl32xx_led_data *led_data = + container_of(led_cdev, struct is31fl32xx_led_data, cdev); + const struct is31fl32xx_chipdef *cdef = led_data->priv->cdef; + u8 pwm_register_offset; + int ret; + + dev_dbg(led_cdev->dev, "%s: %d\n", __func__, brightness); + + /* NOTE: led_data->channel is 1-based */ + if (cdef->pwm_registers_reversed) + pwm_register_offset = cdef->channels - led_data->channel; + else + pwm_register_offset = led_data->channel - 1; + + ret = is31fl32xx_write(led_data->priv, + cdef->pwm_register_base + pwm_register_offset, + brightness); + if (ret) + return ret; + + return is31fl32xx_write(led_data->priv, cdef->pwm_update_reg, 0); +} + +static int is31fl32xx_reset_regs(struct is31fl32xx_priv *priv) +{ + const struct is31fl32xx_chipdef *cdef = priv->cdef; + int ret; + + if (cdef->reset_reg != IS31FL32XX_REG_NONE) { + ret = is31fl32xx_write(priv, cdef->reset_reg, 0); + if (ret) + return ret; + } + + if (cdef->reset_func) + return cdef->reset_func(priv); + + return 0; +} + +static int is31fl32xx_software_shutdown(struct is31fl32xx_priv *priv, + bool enable) +{ + const struct is31fl32xx_chipdef *cdef = priv->cdef; + int ret; + + if (cdef->shutdown_reg != IS31FL32XX_REG_NONE) { + u8 value = enable ? IS31FL32XX_SHUTDOWN_SSD_ENABLE : + IS31FL32XX_SHUTDOWN_SSD_DISABLE; + ret = is31fl32xx_write(priv, cdef->shutdown_reg, value); + if (ret) + return ret; + } + + if (cdef->sw_shutdown_func) + return cdef->sw_shutdown_func(priv, enable); + + return 0; +} + +static int is31fl32xx_init_regs(struct is31fl32xx_priv *priv) +{ + const struct is31fl32xx_chipdef *cdef = priv->cdef; + int ret; + + ret = is31fl32xx_reset_regs(priv); + if (ret) + return ret; + + /* + * Set enable bit for all channels. + * We will control state with PWM registers alone. + */ + if (cdef->led_control_register_base != IS31FL32XX_REG_NONE) { + u8 value = + GENMASK(cdef->enable_bits_per_led_control_register-1, 0); + u8 num_regs = cdef->channels / + cdef->enable_bits_per_led_control_register; + int i; + + for (i = 0; i < num_regs; i++) { + ret = is31fl32xx_write(priv, + cdef->led_control_register_base+i, + value); + if (ret) + return ret; + } + } + + ret = is31fl32xx_software_shutdown(priv, false); + if (ret) + return ret; + + if (cdef->global_control_reg != IS31FL32XX_REG_NONE) { + ret = is31fl32xx_write(priv, cdef->global_control_reg, 0x00); + if (ret) + return ret; + } + + return 0; +} + +static int is31fl32xx_parse_child_dt(const struct device *dev, + const struct device_node *child, + struct is31fl32xx_led_data *led_data) +{ + struct led_classdev *cdev = &led_data->cdev; + int ret = 0; + u32 reg; + + ret = of_property_read_u32(child, "reg", ®); + if (ret || reg < 1 || reg > led_data->priv->cdef->channels) { + dev_err(dev, + "Child node %pOF does not have a valid reg property\n", + child); + return -EINVAL; + } + led_data->channel = reg; + + cdev->brightness_set_blocking = is31fl32xx_brightness_set; + + return 0; +} + +static struct is31fl32xx_led_data *is31fl32xx_find_led_data( + struct is31fl32xx_priv *priv, + u8 channel) +{ + size_t i; + + for (i = 0; i < priv->num_leds; i++) { + if (priv->leds[i].channel == channel) + return &priv->leds[i]; + } + + return NULL; +} + +static int is31fl32xx_parse_dt(struct device *dev, + struct is31fl32xx_priv *priv) +{ + struct device_node *child; + int ret = 0; + + for_each_available_child_of_node(dev_of_node(dev), child) { + struct led_init_data init_data = {}; + struct is31fl32xx_led_data *led_data = + &priv->leds[priv->num_leds]; + const struct is31fl32xx_led_data *other_led_data; + + led_data->priv = priv; + + ret = is31fl32xx_parse_child_dt(dev, child, led_data); + if (ret) + goto err; + + /* Detect if channel is already in use by another child */ + other_led_data = is31fl32xx_find_led_data(priv, + led_data->channel); + if (other_led_data) { + dev_err(dev, + "Node %pOF 'reg' conflicts with another LED\n", + child); + ret = -EINVAL; + goto err; + } + + init_data.fwnode = of_fwnode_handle(child); + + ret = devm_led_classdev_register_ext(dev, &led_data->cdev, + &init_data); + if (ret) { + dev_err(dev, "Failed to register LED for %pOF: %d\n", + child, ret); + goto err; + } + + priv->num_leds++; + } + + return 0; + +err: + of_node_put(child); + return ret; +} + +static const struct of_device_id of_is31fl32xx_match[] = { + { .compatible = "issi,is31fl3236", .data = &is31fl3236_cdef, }, + { .compatible = "issi,is31fl3235", .data = &is31fl3235_cdef, }, + { .compatible = "issi,is31fl3218", .data = &is31fl3218_cdef, }, + { .compatible = "si-en,sn3218", .data = &is31fl3218_cdef, }, + { .compatible = "issi,is31fl3216", .data = &is31fl3216_cdef, }, + { .compatible = "si-en,sn3216", .data = &is31fl3216_cdef, }, + {}, +}; + +MODULE_DEVICE_TABLE(of, of_is31fl32xx_match); + +static int is31fl32xx_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + const struct is31fl32xx_chipdef *cdef; + struct device *dev = &client->dev; + struct is31fl32xx_priv *priv; + int count; + int ret = 0; + + cdef = device_get_match_data(dev); + + count = of_get_available_child_count(dev_of_node(dev)); + if (!count) + return -EINVAL; + + priv = devm_kzalloc(dev, struct_size(priv, leds, count), + GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->client = client; + priv->cdef = cdef; + i2c_set_clientdata(client, priv); + + ret = is31fl32xx_init_regs(priv); + if (ret) + return ret; + + ret = is31fl32xx_parse_dt(dev, priv); + if (ret) + return ret; + + return 0; +} + +static int is31fl32xx_remove(struct i2c_client *client) +{ + struct is31fl32xx_priv *priv = i2c_get_clientdata(client); + + return is31fl32xx_reset_regs(priv); +} + +/* + * i2c-core (and modalias) requires that id_table be properly filled, + * even though it is not used for DeviceTree based instantiation. + */ +static const struct i2c_device_id is31fl32xx_id[] = { + { "is31fl3236" }, + { "is31fl3235" }, + { "is31fl3218" }, + { "sn3218" }, + { "is31fl3216" }, + { "sn3216" }, + {}, +}; + +MODULE_DEVICE_TABLE(i2c, is31fl32xx_id); + +static struct i2c_driver is31fl32xx_driver = { + .driver = { + .name = "is31fl32xx", + .of_match_table = of_is31fl32xx_match, + }, + .probe = is31fl32xx_probe, + .remove = is31fl32xx_remove, + .id_table = is31fl32xx_id, +}; + +module_i2c_driver(is31fl32xx_driver); + +MODULE_AUTHOR("David Rivshin <drivshin@allworx.com>"); +MODULE_DESCRIPTION("ISSI IS31FL32xx LED driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-ktd2692.c b/drivers/leds/leds-ktd2692.c new file mode 100644 index 000000000..f341da150 --- /dev/null +++ b/drivers/leds/leds-ktd2692.c @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED driver : leds-ktd2692.c + * + * Copyright (C) 2015 Samsung Electronics + * Ingi Kim <ingi2.kim@samsung.com> + */ + +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/gpio/consumer.h> +#include <linux/led-class-flash.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/regulator/consumer.h> + +/* Value related the movie mode */ +#define KTD2692_MOVIE_MODE_CURRENT_LEVELS 16 +#define KTD2692_MM_TO_FL_RATIO(x) ((x) / 3) +#define KTD2692_MM_MIN_CURR_THRESHOLD_SCALE 8 + +/* Value related the flash mode */ +#define KTD2692_FLASH_MODE_TIMEOUT_LEVELS 8 +#define KTD2692_FLASH_MODE_TIMEOUT_DISABLE 0 +#define KTD2692_FLASH_MODE_CURR_PERCENT(x) (((x) * 16) / 100) + +/* Macro for getting offset of flash timeout */ +#define GET_TIMEOUT_OFFSET(timeout, step) ((timeout) / (step)) + +/* Base register address */ +#define KTD2692_REG_LVP_BASE 0x00 +#define KTD2692_REG_FLASH_TIMEOUT_BASE 0x20 +#define KTD2692_REG_MM_MIN_CURR_THRESHOLD_BASE 0x40 +#define KTD2692_REG_MOVIE_CURRENT_BASE 0x60 +#define KTD2692_REG_FLASH_CURRENT_BASE 0x80 +#define KTD2692_REG_MODE_BASE 0xA0 + +/* Set bit coding time for expresswire interface */ +#define KTD2692_TIME_RESET_US 700 +#define KTD2692_TIME_DATA_START_TIME_US 10 +#define KTD2692_TIME_HIGH_END_OF_DATA_US 350 +#define KTD2692_TIME_LOW_END_OF_DATA_US 10 +#define KTD2692_TIME_SHORT_BITSET_US 4 +#define KTD2692_TIME_LONG_BITSET_US 12 + +/* KTD2692 default length of name */ +#define KTD2692_NAME_LENGTH 20 + +enum ktd2692_bitset { + KTD2692_LOW = 0, + KTD2692_HIGH, +}; + +/* Movie / Flash Mode Control */ +enum ktd2692_led_mode { + KTD2692_MODE_DISABLE = 0, /* default */ + KTD2692_MODE_MOVIE, + KTD2692_MODE_FLASH, +}; + +struct ktd2692_led_config_data { + /* maximum LED current in movie mode */ + u32 movie_max_microamp; + /* maximum LED current in flash mode */ + u32 flash_max_microamp; + /* maximum flash timeout */ + u32 flash_max_timeout; + /* max LED brightness level */ + enum led_brightness max_brightness; +}; + +struct ktd2692_context { + /* Related LED Flash class device */ + struct led_classdev_flash fled_cdev; + + /* secures access to the device */ + struct mutex lock; + struct regulator *regulator; + + struct gpio_desc *aux_gpio; + struct gpio_desc *ctrl_gpio; + + enum ktd2692_led_mode mode; + enum led_brightness torch_brightness; +}; + +static struct ktd2692_context *fled_cdev_to_led( + struct led_classdev_flash *fled_cdev) +{ + return container_of(fled_cdev, struct ktd2692_context, fled_cdev); +} + +static void ktd2692_expresswire_start(struct ktd2692_context *led) +{ + gpiod_direction_output(led->ctrl_gpio, KTD2692_HIGH); + udelay(KTD2692_TIME_DATA_START_TIME_US); +} + +static void ktd2692_expresswire_reset(struct ktd2692_context *led) +{ + gpiod_direction_output(led->ctrl_gpio, KTD2692_LOW); + udelay(KTD2692_TIME_RESET_US); +} + +static void ktd2692_expresswire_end(struct ktd2692_context *led) +{ + gpiod_direction_output(led->ctrl_gpio, KTD2692_LOW); + udelay(KTD2692_TIME_LOW_END_OF_DATA_US); + gpiod_direction_output(led->ctrl_gpio, KTD2692_HIGH); + udelay(KTD2692_TIME_HIGH_END_OF_DATA_US); +} + +static void ktd2692_expresswire_set_bit(struct ktd2692_context *led, bool bit) +{ + /* + * The Low Bit(0) and High Bit(1) is based on a time detection + * algorithm between time low and time high + * Time_(L_LB) : Low time of the Low Bit(0) + * Time_(H_LB) : High time of the LOW Bit(0) + * Time_(L_HB) : Low time of the High Bit(1) + * Time_(H_HB) : High time of the High Bit(1) + * + * It can be simplified to: + * Low Bit(0) : 2 * Time_(H_LB) < Time_(L_LB) + * High Bit(1) : 2 * Time_(L_HB) < Time_(H_HB) + * HIGH ___ ____ _.. _________ ___ + * |_________| |_.. |____| |__| + * LOW <L_LB> <H_LB> <L_HB> <H_HB> + * [ Low Bit (0) ] [ High Bit(1) ] + */ + if (bit) { + gpiod_direction_output(led->ctrl_gpio, KTD2692_LOW); + udelay(KTD2692_TIME_SHORT_BITSET_US); + gpiod_direction_output(led->ctrl_gpio, KTD2692_HIGH); + udelay(KTD2692_TIME_LONG_BITSET_US); + } else { + gpiod_direction_output(led->ctrl_gpio, KTD2692_LOW); + udelay(KTD2692_TIME_LONG_BITSET_US); + gpiod_direction_output(led->ctrl_gpio, KTD2692_HIGH); + udelay(KTD2692_TIME_SHORT_BITSET_US); + } +} + +static void ktd2692_expresswire_write(struct ktd2692_context *led, u8 value) +{ + int i; + + ktd2692_expresswire_start(led); + for (i = 7; i >= 0; i--) + ktd2692_expresswire_set_bit(led, value & BIT(i)); + ktd2692_expresswire_end(led); +} + +static int ktd2692_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + struct ktd2692_context *led = fled_cdev_to_led(fled_cdev); + + mutex_lock(&led->lock); + + if (brightness == LED_OFF) { + led->mode = KTD2692_MODE_DISABLE; + gpiod_direction_output(led->aux_gpio, KTD2692_LOW); + } else { + ktd2692_expresswire_write(led, brightness | + KTD2692_REG_MOVIE_CURRENT_BASE); + led->mode = KTD2692_MODE_MOVIE; + } + + ktd2692_expresswire_write(led, led->mode | KTD2692_REG_MODE_BASE); + mutex_unlock(&led->lock); + + return 0; +} + +static int ktd2692_led_flash_strobe_set(struct led_classdev_flash *fled_cdev, + bool state) +{ + struct ktd2692_context *led = fled_cdev_to_led(fled_cdev); + struct led_flash_setting *timeout = &fled_cdev->timeout; + u32 flash_tm_reg; + + mutex_lock(&led->lock); + + if (state) { + flash_tm_reg = GET_TIMEOUT_OFFSET(timeout->val, timeout->step); + ktd2692_expresswire_write(led, flash_tm_reg + | KTD2692_REG_FLASH_TIMEOUT_BASE); + + led->mode = KTD2692_MODE_FLASH; + gpiod_direction_output(led->aux_gpio, KTD2692_HIGH); + } else { + led->mode = KTD2692_MODE_DISABLE; + gpiod_direction_output(led->aux_gpio, KTD2692_LOW); + } + + ktd2692_expresswire_write(led, led->mode | KTD2692_REG_MODE_BASE); + + fled_cdev->led_cdev.brightness = LED_OFF; + led->mode = KTD2692_MODE_DISABLE; + + mutex_unlock(&led->lock); + + return 0; +} + +static int ktd2692_led_flash_timeout_set(struct led_classdev_flash *fled_cdev, + u32 timeout) +{ + return 0; +} + +static void ktd2692_init_movie_current_max(struct ktd2692_led_config_data *cfg) +{ + u32 offset, step; + u32 movie_current_microamp; + + offset = KTD2692_MOVIE_MODE_CURRENT_LEVELS; + step = KTD2692_MM_TO_FL_RATIO(cfg->flash_max_microamp) + / KTD2692_MOVIE_MODE_CURRENT_LEVELS; + + do { + movie_current_microamp = step * offset; + offset--; + } while ((movie_current_microamp > cfg->movie_max_microamp) && + (offset > 0)); + + cfg->max_brightness = offset; +} + +static void ktd2692_init_flash_timeout(struct led_classdev_flash *fled_cdev, + struct ktd2692_led_config_data *cfg) +{ + struct led_flash_setting *setting; + + setting = &fled_cdev->timeout; + setting->min = KTD2692_FLASH_MODE_TIMEOUT_DISABLE; + setting->max = cfg->flash_max_timeout; + setting->step = cfg->flash_max_timeout + / (KTD2692_FLASH_MODE_TIMEOUT_LEVELS - 1); + setting->val = cfg->flash_max_timeout; +} + +static void ktd2692_setup(struct ktd2692_context *led) +{ + led->mode = KTD2692_MODE_DISABLE; + ktd2692_expresswire_reset(led); + gpiod_direction_output(led->aux_gpio, KTD2692_LOW); + + ktd2692_expresswire_write(led, (KTD2692_MM_MIN_CURR_THRESHOLD_SCALE - 1) + | KTD2692_REG_MM_MIN_CURR_THRESHOLD_BASE); + ktd2692_expresswire_write(led, KTD2692_FLASH_MODE_CURR_PERCENT(45) + | KTD2692_REG_FLASH_CURRENT_BASE); +} + +static void regulator_disable_action(void *_data) +{ + struct device *dev = _data; + struct ktd2692_context *led = dev_get_drvdata(dev); + int ret; + + ret = regulator_disable(led->regulator); + if (ret) + dev_err(dev, "Failed to disable supply: %d\n", ret); +} + +static int ktd2692_parse_dt(struct ktd2692_context *led, struct device *dev, + struct ktd2692_led_config_data *cfg) +{ + struct device_node *np = dev_of_node(dev); + struct device_node *child_node; + int ret; + + if (!dev_of_node(dev)) + return -ENXIO; + + led->ctrl_gpio = devm_gpiod_get(dev, "ctrl", GPIOD_ASIS); + ret = PTR_ERR_OR_ZERO(led->ctrl_gpio); + if (ret) { + dev_err(dev, "cannot get ctrl-gpios %d\n", ret); + return ret; + } + + led->aux_gpio = devm_gpiod_get(dev, "aux", GPIOD_ASIS); + ret = PTR_ERR_OR_ZERO(led->aux_gpio); + if (ret) { + dev_err(dev, "cannot get aux-gpios %d\n", ret); + return ret; + } + + led->regulator = devm_regulator_get(dev, "vin"); + if (IS_ERR(led->regulator)) + led->regulator = NULL; + + if (led->regulator) { + ret = regulator_enable(led->regulator); + if (ret) { + dev_err(dev, "Failed to enable supply: %d\n", ret); + } else { + ret = devm_add_action_or_reset(dev, + regulator_disable_action, dev); + if (ret) + return ret; + } + } + + child_node = of_get_next_available_child(np, NULL); + if (!child_node) { + dev_err(dev, "No DT child node found for connected LED.\n"); + return -EINVAL; + } + + led->fled_cdev.led_cdev.name = + of_get_property(child_node, "label", NULL) ? : child_node->name; + + ret = of_property_read_u32(child_node, "led-max-microamp", + &cfg->movie_max_microamp); + if (ret) { + dev_err(dev, "failed to parse led-max-microamp\n"); + goto err_parse_dt; + } + + ret = of_property_read_u32(child_node, "flash-max-microamp", + &cfg->flash_max_microamp); + if (ret) { + dev_err(dev, "failed to parse flash-max-microamp\n"); + goto err_parse_dt; + } + + ret = of_property_read_u32(child_node, "flash-max-timeout-us", + &cfg->flash_max_timeout); + if (ret) { + dev_err(dev, "failed to parse flash-max-timeout-us\n"); + goto err_parse_dt; + } + +err_parse_dt: + of_node_put(child_node); + return ret; +} + +static const struct led_flash_ops flash_ops = { + .strobe_set = ktd2692_led_flash_strobe_set, + .timeout_set = ktd2692_led_flash_timeout_set, +}; + +static int ktd2692_probe(struct platform_device *pdev) +{ + struct ktd2692_context *led; + struct led_classdev *led_cdev; + struct led_classdev_flash *fled_cdev; + struct ktd2692_led_config_data led_cfg; + int ret; + + led = devm_kzalloc(&pdev->dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + fled_cdev = &led->fled_cdev; + led_cdev = &fled_cdev->led_cdev; + + ret = ktd2692_parse_dt(led, &pdev->dev, &led_cfg); + if (ret) + return ret; + + ktd2692_init_flash_timeout(fled_cdev, &led_cfg); + ktd2692_init_movie_current_max(&led_cfg); + + fled_cdev->ops = &flash_ops; + + led_cdev->max_brightness = led_cfg.max_brightness; + led_cdev->brightness_set_blocking = ktd2692_led_brightness_set; + led_cdev->flags |= LED_CORE_SUSPENDRESUME | LED_DEV_CAP_FLASH; + + mutex_init(&led->lock); + + platform_set_drvdata(pdev, led); + + ret = led_classdev_flash_register(&pdev->dev, fled_cdev); + if (ret) { + dev_err(&pdev->dev, "can't register LED %s\n", led_cdev->name); + mutex_destroy(&led->lock); + return ret; + } + + ktd2692_setup(led); + + return 0; +} + +static int ktd2692_remove(struct platform_device *pdev) +{ + struct ktd2692_context *led = platform_get_drvdata(pdev); + + led_classdev_flash_unregister(&led->fled_cdev); + + mutex_destroy(&led->lock); + + return 0; +} + +static const struct of_device_id ktd2692_match[] = { + { .compatible = "kinetic,ktd2692", }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, ktd2692_match); + +static struct platform_driver ktd2692_driver = { + .driver = { + .name = "ktd2692", + .of_match_table = ktd2692_match, + }, + .probe = ktd2692_probe, + .remove = ktd2692_remove, +}; + +module_platform_driver(ktd2692_driver); + +MODULE_AUTHOR("Ingi Kim <ingi2.kim@samsung.com>"); +MODULE_DESCRIPTION("Kinetic KTD2692 LED driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-lm3530.c b/drivers/leds/leds-lm3530.c new file mode 100644 index 000000000..2f8362f6b --- /dev/null +++ b/drivers/leds/leds-lm3530.c @@ -0,0 +1,501 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2011 ST-Ericsson SA. + * Copyright (C) 2009 Motorola, Inc. + * + * Simple driver for National Semiconductor LM3530 Backlight driver chip + * + * Author: Shreshtha Kumar SAHU <shreshthakumar.sahu@stericsson.com> + * based on leds-lm3530.c by Dan Murphy <D.Murphy@motorola.com> + */ + +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/input.h> +#include <linux/led-lm3530.h> +#include <linux/types.h> +#include <linux/regulator/consumer.h> +#include <linux/module.h> + +#define LM3530_LED_DEV "lcd-backlight" +#define LM3530_NAME "lm3530-led" + +#define LM3530_GEN_CONFIG 0x10 +#define LM3530_ALS_CONFIG 0x20 +#define LM3530_BRT_RAMP_RATE 0x30 +#define LM3530_ALS_IMP_SELECT 0x41 +#define LM3530_BRT_CTRL_REG 0xA0 +#define LM3530_ALS_ZB0_REG 0x60 +#define LM3530_ALS_ZB1_REG 0x61 +#define LM3530_ALS_ZB2_REG 0x62 +#define LM3530_ALS_ZB3_REG 0x63 +#define LM3530_ALS_Z0T_REG 0x70 +#define LM3530_ALS_Z1T_REG 0x71 +#define LM3530_ALS_Z2T_REG 0x72 +#define LM3530_ALS_Z3T_REG 0x73 +#define LM3530_ALS_Z4T_REG 0x74 +#define LM3530_REG_MAX 14 + +/* General Control Register */ +#define LM3530_EN_I2C_SHIFT (0) +#define LM3530_RAMP_LAW_SHIFT (1) +#define LM3530_MAX_CURR_SHIFT (2) +#define LM3530_EN_PWM_SHIFT (5) +#define LM3530_PWM_POL_SHIFT (6) +#define LM3530_EN_PWM_SIMPLE_SHIFT (7) + +#define LM3530_ENABLE_I2C (1 << LM3530_EN_I2C_SHIFT) +#define LM3530_ENABLE_PWM (1 << LM3530_EN_PWM_SHIFT) +#define LM3530_POL_LOW (1 << LM3530_PWM_POL_SHIFT) +#define LM3530_ENABLE_PWM_SIMPLE (1 << LM3530_EN_PWM_SIMPLE_SHIFT) + +/* ALS Config Register Options */ +#define LM3530_ALS_AVG_TIME_SHIFT (0) +#define LM3530_EN_ALS_SHIFT (3) +#define LM3530_ALS_SEL_SHIFT (5) + +#define LM3530_ENABLE_ALS (3 << LM3530_EN_ALS_SHIFT) + +/* Brightness Ramp Rate Register */ +#define LM3530_BRT_RAMP_FALL_SHIFT (0) +#define LM3530_BRT_RAMP_RISE_SHIFT (3) + +/* ALS Resistor Select */ +#define LM3530_ALS1_IMP_SHIFT (0) +#define LM3530_ALS2_IMP_SHIFT (4) + +/* Zone Boundary Register defaults */ +#define LM3530_ALS_ZB_MAX (4) +#define LM3530_ALS_WINDOW_mV (1000) +#define LM3530_ALS_OFFSET_mV (4) + +/* Zone Target Register defaults */ +#define LM3530_DEF_ZT_0 (0x7F) +#define LM3530_DEF_ZT_1 (0x66) +#define LM3530_DEF_ZT_2 (0x4C) +#define LM3530_DEF_ZT_3 (0x33) +#define LM3530_DEF_ZT_4 (0x19) + +/* 7 bits are used for the brightness : LM3530_BRT_CTRL_REG */ +#define MAX_BRIGHTNESS (127) + +struct lm3530_mode_map { + const char *mode; + enum lm3530_mode mode_val; +}; + +static struct lm3530_mode_map mode_map[] = { + { "man", LM3530_BL_MODE_MANUAL }, + { "als", LM3530_BL_MODE_ALS }, + { "pwm", LM3530_BL_MODE_PWM }, +}; + +/** + * struct lm3530_data + * @led_dev: led class device + * @client: i2c client + * @pdata: LM3530 platform data + * @mode: mode of operation - manual, ALS, PWM + * @regulator: regulator + * @brighness: previous brightness value + * @enable: regulator is enabled + */ +struct lm3530_data { + struct led_classdev led_dev; + struct i2c_client *client; + struct lm3530_platform_data *pdata; + enum lm3530_mode mode; + struct regulator *regulator; + enum led_brightness brightness; + bool enable; +}; + +/* + * struct lm3530_als_data + * @config : value of ALS configuration register + * @imp_sel : value of ALS resistor select register + * @zone : values of ALS ZB(Zone Boundary) registers + */ +struct lm3530_als_data { + u8 config; + u8 imp_sel; + u8 zones[LM3530_ALS_ZB_MAX]; +}; + +static const u8 lm3530_reg[LM3530_REG_MAX] = { + LM3530_GEN_CONFIG, + LM3530_ALS_CONFIG, + LM3530_BRT_RAMP_RATE, + LM3530_ALS_IMP_SELECT, + LM3530_BRT_CTRL_REG, + LM3530_ALS_ZB0_REG, + LM3530_ALS_ZB1_REG, + LM3530_ALS_ZB2_REG, + LM3530_ALS_ZB3_REG, + LM3530_ALS_Z0T_REG, + LM3530_ALS_Z1T_REG, + LM3530_ALS_Z2T_REG, + LM3530_ALS_Z3T_REG, + LM3530_ALS_Z4T_REG, +}; + +static int lm3530_get_mode_from_str(const char *str) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(mode_map); i++) + if (sysfs_streq(str, mode_map[i].mode)) + return mode_map[i].mode_val; + + return -EINVAL; +} + +static void lm3530_als_configure(struct lm3530_platform_data *pdata, + struct lm3530_als_data *als) +{ + int i; + u32 als_vmin, als_vmax, als_vstep; + + if (pdata->als_vmax == 0) { + pdata->als_vmin = 0; + pdata->als_vmax = LM3530_ALS_WINDOW_mV; + } + + als_vmin = pdata->als_vmin; + als_vmax = pdata->als_vmax; + + if ((als_vmax - als_vmin) > LM3530_ALS_WINDOW_mV) + pdata->als_vmax = als_vmax = als_vmin + LM3530_ALS_WINDOW_mV; + + /* n zone boundary makes n+1 zones */ + als_vstep = (als_vmax - als_vmin) / (LM3530_ALS_ZB_MAX + 1); + + for (i = 0; i < LM3530_ALS_ZB_MAX; i++) + als->zones[i] = (((als_vmin + LM3530_ALS_OFFSET_mV) + + als_vstep + (i * als_vstep)) * LED_FULL) / 1000; + + als->config = + (pdata->als_avrg_time << LM3530_ALS_AVG_TIME_SHIFT) | + (LM3530_ENABLE_ALS) | + (pdata->als_input_mode << LM3530_ALS_SEL_SHIFT); + + als->imp_sel = + (pdata->als1_resistor_sel << LM3530_ALS1_IMP_SHIFT) | + (pdata->als2_resistor_sel << LM3530_ALS2_IMP_SHIFT); +} + +static int lm3530_led_enable(struct lm3530_data *drvdata) +{ + int ret; + + if (drvdata->enable) + return 0; + + ret = regulator_enable(drvdata->regulator); + if (ret) { + dev_err(drvdata->led_dev.dev, "Failed to enable vin:%d\n", ret); + return ret; + } + + drvdata->enable = true; + return 0; +} + +static void lm3530_led_disable(struct lm3530_data *drvdata) +{ + int ret; + + if (!drvdata->enable) + return; + + ret = regulator_disable(drvdata->regulator); + if (ret) { + dev_err(drvdata->led_dev.dev, "Failed to disable vin:%d\n", + ret); + return; + } + + drvdata->enable = false; +} + +static int lm3530_init_registers(struct lm3530_data *drvdata) +{ + int ret = 0; + int i; + u8 gen_config; + u8 brt_ramp; + u8 brightness; + u8 reg_val[LM3530_REG_MAX]; + struct lm3530_platform_data *pdata = drvdata->pdata; + struct i2c_client *client = drvdata->client; + struct lm3530_pwm_data *pwm = &pdata->pwm_data; + struct lm3530_als_data als; + + memset(&als, 0, sizeof(struct lm3530_als_data)); + + gen_config = (pdata->brt_ramp_law << LM3530_RAMP_LAW_SHIFT) | + ((pdata->max_current & 7) << LM3530_MAX_CURR_SHIFT); + + switch (drvdata->mode) { + case LM3530_BL_MODE_MANUAL: + gen_config |= LM3530_ENABLE_I2C; + break; + case LM3530_BL_MODE_ALS: + gen_config |= LM3530_ENABLE_I2C; + lm3530_als_configure(pdata, &als); + break; + case LM3530_BL_MODE_PWM: + gen_config |= LM3530_ENABLE_PWM | LM3530_ENABLE_PWM_SIMPLE | + (pdata->pwm_pol_hi << LM3530_PWM_POL_SHIFT); + break; + } + + brt_ramp = (pdata->brt_ramp_fall << LM3530_BRT_RAMP_FALL_SHIFT) | + (pdata->brt_ramp_rise << LM3530_BRT_RAMP_RISE_SHIFT); + + if (drvdata->brightness) + brightness = drvdata->brightness; + else + brightness = drvdata->brightness = pdata->brt_val; + + if (brightness > drvdata->led_dev.max_brightness) + brightness = drvdata->led_dev.max_brightness; + + reg_val[0] = gen_config; /* LM3530_GEN_CONFIG */ + reg_val[1] = als.config; /* LM3530_ALS_CONFIG */ + reg_val[2] = brt_ramp; /* LM3530_BRT_RAMP_RATE */ + reg_val[3] = als.imp_sel; /* LM3530_ALS_IMP_SELECT */ + reg_val[4] = brightness; /* LM3530_BRT_CTRL_REG */ + reg_val[5] = als.zones[0]; /* LM3530_ALS_ZB0_REG */ + reg_val[6] = als.zones[1]; /* LM3530_ALS_ZB1_REG */ + reg_val[7] = als.zones[2]; /* LM3530_ALS_ZB2_REG */ + reg_val[8] = als.zones[3]; /* LM3530_ALS_ZB3_REG */ + reg_val[9] = LM3530_DEF_ZT_0; /* LM3530_ALS_Z0T_REG */ + reg_val[10] = LM3530_DEF_ZT_1; /* LM3530_ALS_Z1T_REG */ + reg_val[11] = LM3530_DEF_ZT_2; /* LM3530_ALS_Z2T_REG */ + reg_val[12] = LM3530_DEF_ZT_3; /* LM3530_ALS_Z3T_REG */ + reg_val[13] = LM3530_DEF_ZT_4; /* LM3530_ALS_Z4T_REG */ + + ret = lm3530_led_enable(drvdata); + if (ret) + return ret; + + for (i = 0; i < LM3530_REG_MAX; i++) { + /* do not update brightness register when pwm mode */ + if (lm3530_reg[i] == LM3530_BRT_CTRL_REG && + drvdata->mode == LM3530_BL_MODE_PWM) { + if (pwm->pwm_set_intensity) + pwm->pwm_set_intensity(reg_val[i], + drvdata->led_dev.max_brightness); + continue; + } + + ret = i2c_smbus_write_byte_data(client, + lm3530_reg[i], reg_val[i]); + if (ret) + break; + } + + return ret; +} + +static void lm3530_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brt_val) +{ + int err; + struct lm3530_data *drvdata = + container_of(led_cdev, struct lm3530_data, led_dev); + struct lm3530_platform_data *pdata = drvdata->pdata; + struct lm3530_pwm_data *pwm = &pdata->pwm_data; + u8 max_brightness = led_cdev->max_brightness; + + switch (drvdata->mode) { + case LM3530_BL_MODE_MANUAL: + + if (!drvdata->enable) { + err = lm3530_init_registers(drvdata); + if (err) { + dev_err(&drvdata->client->dev, + "Register Init failed: %d\n", err); + break; + } + } + + /* set the brightness in brightness control register*/ + err = i2c_smbus_write_byte_data(drvdata->client, + LM3530_BRT_CTRL_REG, brt_val); + if (err) + dev_err(&drvdata->client->dev, + "Unable to set brightness: %d\n", err); + else + drvdata->brightness = brt_val; + + if (brt_val == 0) + lm3530_led_disable(drvdata); + break; + case LM3530_BL_MODE_ALS: + break; + case LM3530_BL_MODE_PWM: + if (pwm->pwm_set_intensity) + pwm->pwm_set_intensity(brt_val, max_brightness); + break; + default: + break; + } +} + +static ssize_t lm3530_mode_get(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3530_data *drvdata; + int i, len = 0; + + drvdata = container_of(led_cdev, struct lm3530_data, led_dev); + for (i = 0; i < ARRAY_SIZE(mode_map); i++) + if (drvdata->mode == mode_map[i].mode_val) + len += sprintf(buf + len, "[%s] ", mode_map[i].mode); + else + len += sprintf(buf + len, "%s ", mode_map[i].mode); + + len += sprintf(buf + len, "\n"); + + return len; +} + +static ssize_t lm3530_mode_set(struct device *dev, struct device_attribute + *attr, const char *buf, size_t size) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3530_data *drvdata; + struct lm3530_pwm_data *pwm; + u8 max_brightness; + int mode, err; + + drvdata = container_of(led_cdev, struct lm3530_data, led_dev); + pwm = &drvdata->pdata->pwm_data; + max_brightness = led_cdev->max_brightness; + mode = lm3530_get_mode_from_str(buf); + if (mode < 0) { + dev_err(dev, "Invalid mode\n"); + return mode; + } + + drvdata->mode = mode; + + /* set pwm to low if unnecessary */ + if (mode != LM3530_BL_MODE_PWM && pwm->pwm_set_intensity) + pwm->pwm_set_intensity(0, max_brightness); + + err = lm3530_init_registers(drvdata); + if (err) { + dev_err(dev, "Setting %s Mode failed :%d\n", buf, err); + return err; + } + + return sizeof(drvdata->mode); +} +static DEVICE_ATTR(mode, 0644, lm3530_mode_get, lm3530_mode_set); + +static struct attribute *lm3530_attrs[] = { + &dev_attr_mode.attr, + NULL +}; +ATTRIBUTE_GROUPS(lm3530); + +static int lm3530_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct lm3530_platform_data *pdata = dev_get_platdata(&client->dev); + struct lm3530_data *drvdata; + int err = 0; + + if (pdata == NULL) { + dev_err(&client->dev, "platform data required\n"); + return -ENODEV; + } + + /* BL mode */ + if (pdata->mode > LM3530_BL_MODE_PWM) { + dev_err(&client->dev, "Illegal Mode request\n"); + return -EINVAL; + } + + if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) { + dev_err(&client->dev, "I2C_FUNC_I2C not supported\n"); + return -EIO; + } + + drvdata = devm_kzalloc(&client->dev, sizeof(struct lm3530_data), + GFP_KERNEL); + if (drvdata == NULL) + return -ENOMEM; + + drvdata->mode = pdata->mode; + drvdata->client = client; + drvdata->pdata = pdata; + drvdata->brightness = LED_OFF; + drvdata->enable = false; + drvdata->led_dev.name = LM3530_LED_DEV; + drvdata->led_dev.brightness_set = lm3530_brightness_set; + drvdata->led_dev.max_brightness = MAX_BRIGHTNESS; + drvdata->led_dev.groups = lm3530_groups; + + i2c_set_clientdata(client, drvdata); + + drvdata->regulator = devm_regulator_get(&client->dev, "vin"); + if (IS_ERR(drvdata->regulator)) { + dev_err(&client->dev, "regulator get failed\n"); + err = PTR_ERR(drvdata->regulator); + drvdata->regulator = NULL; + return err; + } + + if (drvdata->pdata->brt_val) { + err = lm3530_init_registers(drvdata); + if (err < 0) { + dev_err(&client->dev, + "Register Init failed: %d\n", err); + return err; + } + } + err = led_classdev_register(&client->dev, &drvdata->led_dev); + if (err < 0) { + dev_err(&client->dev, "Register led class failed: %d\n", err); + return err; + } + + return 0; +} + +static int lm3530_remove(struct i2c_client *client) +{ + struct lm3530_data *drvdata = i2c_get_clientdata(client); + + lm3530_led_disable(drvdata); + led_classdev_unregister(&drvdata->led_dev); + return 0; +} + +static const struct i2c_device_id lm3530_id[] = { + {LM3530_NAME, 0}, + {} +}; +MODULE_DEVICE_TABLE(i2c, lm3530_id); + +static struct i2c_driver lm3530_i2c_driver = { + .probe = lm3530_probe, + .remove = lm3530_remove, + .id_table = lm3530_id, + .driver = { + .name = LM3530_NAME, + }, +}; + +module_i2c_driver(lm3530_i2c_driver); + +MODULE_DESCRIPTION("Back Light driver for LM3530"); +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Shreshtha Kumar SAHU <shreshthakumar.sahu@stericsson.com>"); diff --git a/drivers/leds/leds-lm3532.c b/drivers/leds/leds-lm3532.c new file mode 100644 index 000000000..0bf25bdde --- /dev/null +++ b/drivers/leds/leds-lm3532.c @@ -0,0 +1,747 @@ +// SPDX-License-Identifier: GPL-2.0 +// TI LM3532 LED driver +// Copyright (C) 2019 Texas Instruments Incorporated - https://www.ti.com/ +// https://www.ti.com/lit/ds/symlink/lm3532.pdf + +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/slab.h> +#include <linux/regmap.h> +#include <linux/types.h> +#include <linux/regulator/consumer.h> +#include <linux/module.h> +#include <uapi/linux/uleds.h> +#include <linux/gpio/consumer.h> + +#define LM3532_NAME "lm3532-led" +#define LM3532_BL_MODE_MANUAL 0x00 +#define LM3532_BL_MODE_ALS 0x01 + +#define LM3532_REG_OUTPUT_CFG 0x10 +#define LM3532_REG_STARTSHUT_RAMP 0x11 +#define LM3532_REG_RT_RAMP 0x12 +#define LM3532_REG_PWM_A_CFG 0x13 +#define LM3532_REG_PWM_B_CFG 0x14 +#define LM3532_REG_PWM_C_CFG 0x15 +#define LM3532_REG_ZONE_CFG_A 0x16 +#define LM3532_REG_CTRL_A_FS_CURR 0x17 +#define LM3532_REG_ZONE_CFG_B 0x18 +#define LM3532_REG_CTRL_B_FS_CURR 0x19 +#define LM3532_REG_ZONE_CFG_C 0x1a +#define LM3532_REG_CTRL_C_FS_CURR 0x1b +#define LM3532_REG_ENABLE 0x1d +#define LM3532_ALS_CONFIG 0x23 +#define LM3532_REG_ZN_0_HI 0x60 +#define LM3532_REG_ZN_0_LO 0x61 +#define LM3532_REG_ZN_1_HI 0x62 +#define LM3532_REG_ZN_1_LO 0x63 +#define LM3532_REG_ZN_2_HI 0x64 +#define LM3532_REG_ZN_2_LO 0x65 +#define LM3532_REG_ZN_3_HI 0x66 +#define LM3532_REG_ZN_3_LO 0x67 +#define LM3532_REG_ZONE_TRGT_A 0x70 +#define LM3532_REG_ZONE_TRGT_B 0x75 +#define LM3532_REG_ZONE_TRGT_C 0x7a +#define LM3532_REG_MAX 0x7e + +/* Control Enable */ +#define LM3532_CTRL_A_ENABLE BIT(0) +#define LM3532_CTRL_B_ENABLE BIT(1) +#define LM3532_CTRL_C_ENABLE BIT(2) + +/* PWM Zone Control */ +#define LM3532_PWM_ZONE_MASK 0x7c +#define LM3532_PWM_ZONE_0_EN BIT(2) +#define LM3532_PWM_ZONE_1_EN BIT(3) +#define LM3532_PWM_ZONE_2_EN BIT(4) +#define LM3532_PWM_ZONE_3_EN BIT(5) +#define LM3532_PWM_ZONE_4_EN BIT(6) + +/* Brightness Configuration */ +#define LM3532_I2C_CTRL BIT(0) +#define LM3532_ALS_CTRL 0 +#define LM3532_LINEAR_MAP BIT(1) +#define LM3532_ZONE_MASK (BIT(2) | BIT(3) | BIT(4)) +#define LM3532_ZONE_0 0 +#define LM3532_ZONE_1 BIT(2) +#define LM3532_ZONE_2 BIT(3) +#define LM3532_ZONE_3 (BIT(2) | BIT(3)) +#define LM3532_ZONE_4 BIT(4) + +#define LM3532_ENABLE_ALS BIT(3) +#define LM3532_ALS_SEL_SHIFT 6 + +/* Zone Boundary Register */ +#define LM3532_ALS_WINDOW_mV 2000 +#define LM3532_ALS_ZB_MAX 4 +#define LM3532_ALS_OFFSET_mV 2 + +#define LM3532_CONTROL_A 0 +#define LM3532_CONTROL_B 1 +#define LM3532_CONTROL_C 2 +#define LM3532_MAX_CONTROL_BANKS 3 +#define LM3532_MAX_LED_STRINGS 3 + +#define LM3532_OUTPUT_CFG_MASK 0x3 +#define LM3532_BRT_VAL_ADJUST 8 +#define LM3532_RAMP_DOWN_SHIFT 3 + +#define LM3532_NUM_RAMP_VALS 8 +#define LM3532_NUM_AVG_VALS 8 +#define LM3532_NUM_IMP_VALS 32 + +#define LM3532_FS_CURR_MIN 5000 +#define LM3532_FS_CURR_MAX 29800 +#define LM3532_FS_CURR_STEP 800 + +/* + * struct lm3532_als_data + * @config: value of ALS configuration register + * @als1_imp_sel: value of ALS1 resistor select register + * @als2_imp_sel: value of ALS2 resistor select register + * @als_avrg_time: ALS averaging time + * @als_input_mode: ALS input mode for brightness control + * @als_vmin: Minimum ALS voltage + * @als_vmax: Maximum ALS voltage + * @zone_lo: values of ALS lo ZB(Zone Boundary) registers + * @zone_hi: values of ALS hi ZB(Zone Boundary) registers + */ +struct lm3532_als_data { + u8 config; + u8 als1_imp_sel; + u8 als2_imp_sel; + u8 als_avrg_time; + u8 als_input_mode; + u32 als_vmin; + u32 als_vmax; + u8 zones_lo[LM3532_ALS_ZB_MAX]; + u8 zones_hi[LM3532_ALS_ZB_MAX]; +}; + +/** + * struct lm3532_led + * @led_dev: led class device + * @priv: Pointer the device data structure + * @control_bank: Control bank the LED is associated to + * @mode: Mode of the LED string + * @ctrl_brt_pointer: Zone target register that controls the sink + * @num_leds: Number of LED strings are supported in this array + * @full_scale_current: The full-scale current setting for the current sink. + * @led_strings: The LED strings supported in this array + * @enabled: Enabled status + */ +struct lm3532_led { + struct led_classdev led_dev; + struct lm3532_data *priv; + + int control_bank; + int mode; + int ctrl_brt_pointer; + int num_leds; + int full_scale_current; + unsigned int enabled:1; + u32 led_strings[LM3532_MAX_CONTROL_BANKS]; +}; + +/** + * struct lm3532_data + * @enable_gpio: Hardware enable gpio + * @regulator: regulator + * @client: i2c client + * @regmap: Devices register map + * @dev: Pointer to the devices device struct + * @lock: Lock for reading/writing the device + * @als_data: Pointer to the als data struct + * @runtime_ramp_up: Runtime ramp up setting + * @runtime_ramp_down: Runtime ramp down setting + * @leds: Array of LED strings + */ +struct lm3532_data { + struct gpio_desc *enable_gpio; + struct regulator *regulator; + struct i2c_client *client; + struct regmap *regmap; + struct device *dev; + struct mutex lock; + + struct lm3532_als_data *als_data; + + u32 runtime_ramp_up; + u32 runtime_ramp_down; + + struct lm3532_led leds[]; +}; + +static const struct reg_default lm3532_reg_defs[] = { + {LM3532_REG_OUTPUT_CFG, 0xe4}, + {LM3532_REG_STARTSHUT_RAMP, 0xc0}, + {LM3532_REG_RT_RAMP, 0xc0}, + {LM3532_REG_PWM_A_CFG, 0x82}, + {LM3532_REG_PWM_B_CFG, 0x82}, + {LM3532_REG_PWM_C_CFG, 0x82}, + {LM3532_REG_ZONE_CFG_A, 0xf1}, + {LM3532_REG_CTRL_A_FS_CURR, 0xf3}, + {LM3532_REG_ZONE_CFG_B, 0xf1}, + {LM3532_REG_CTRL_B_FS_CURR, 0xf3}, + {LM3532_REG_ZONE_CFG_C, 0xf1}, + {LM3532_REG_CTRL_C_FS_CURR, 0xf3}, + {LM3532_REG_ENABLE, 0xf8}, + {LM3532_ALS_CONFIG, 0x44}, + {LM3532_REG_ZN_0_HI, 0x35}, + {LM3532_REG_ZN_0_LO, 0x33}, + {LM3532_REG_ZN_1_HI, 0x6a}, + {LM3532_REG_ZN_1_LO, 0x66}, + {LM3532_REG_ZN_2_HI, 0xa1}, + {LM3532_REG_ZN_2_LO, 0x99}, + {LM3532_REG_ZN_3_HI, 0xdc}, + {LM3532_REG_ZN_3_LO, 0xcc}, +}; + +static const struct regmap_config lm3532_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = LM3532_REG_MAX, + .reg_defaults = lm3532_reg_defs, + .num_reg_defaults = ARRAY_SIZE(lm3532_reg_defs), + .cache_type = REGCACHE_FLAT, +}; + +static const int als_imp_table[LM3532_NUM_IMP_VALS] = {37000, 18500, 12330, + 92500, 7400, 6170, 5290, + 4630, 4110, 3700, 3360, + 3080, 2850, 2640, 2440, + 2310, 2180, 2060, 1950, + 1850, 1760, 1680, 1610, + 1540, 1480, 1420, 1370, + 1320, 1280, 1230, 1190}; +static int lm3532_get_als_imp_index(int als_imped) +{ + int i; + + if (als_imped > als_imp_table[1]) + return 0; + + if (als_imped < als_imp_table[LM3532_NUM_IMP_VALS - 1]) + return LM3532_NUM_IMP_VALS - 1; + + for (i = 1; i < LM3532_NUM_IMP_VALS; i++) { + if (als_imped == als_imp_table[i]) + return i; + + /* Find an approximate index by looking up the table */ + if (als_imped < als_imp_table[i - 1] && + als_imped > als_imp_table[i]) { + if (als_imped - als_imp_table[i - 1] < + als_imp_table[i] - als_imped) + return i + 1; + else + return i; + } + } + + return -EINVAL; +} + +static int lm3532_get_index(const int table[], int size, int value) +{ + int i; + + for (i = 1; i < size; i++) { + if (value == table[i]) + return i; + + /* Find an approximate index by looking up the table */ + if (value > table[i - 1] && + value < table[i]) { + if (value - table[i - 1] < table[i] - value) + return i - 1; + else + return i; + } + } + + return -EINVAL; +} + +static const int als_avrg_table[LM3532_NUM_AVG_VALS] = {17920, 35840, 71680, + 1433360, 286720, 573440, + 1146880, 2293760}; +static int lm3532_get_als_avg_index(int avg_time) +{ + if (avg_time <= als_avrg_table[0]) + return 0; + + if (avg_time > als_avrg_table[LM3532_NUM_AVG_VALS - 1]) + return LM3532_NUM_AVG_VALS - 1; + + return lm3532_get_index(&als_avrg_table[0], LM3532_NUM_AVG_VALS, + avg_time); +} + +static const int ramp_table[LM3532_NUM_RAMP_VALS] = { 8, 1024, 2048, 4096, 8192, + 16384, 32768, 65536}; +static int lm3532_get_ramp_index(int ramp_time) +{ + if (ramp_time <= ramp_table[0]) + return 0; + + if (ramp_time > ramp_table[LM3532_NUM_RAMP_VALS - 1]) + return LM3532_NUM_RAMP_VALS - 1; + + return lm3532_get_index(&ramp_table[0], LM3532_NUM_RAMP_VALS, + ramp_time); +} + +/* Caller must take care of locking */ +static int lm3532_led_enable(struct lm3532_led *led_data) +{ + int ctrl_en_val = BIT(led_data->control_bank); + int ret; + + if (led_data->enabled) + return 0; + + ret = regmap_update_bits(led_data->priv->regmap, LM3532_REG_ENABLE, + ctrl_en_val, ctrl_en_val); + if (ret) { + dev_err(led_data->priv->dev, "Failed to set ctrl:%d\n", ret); + return ret; + } + + ret = regulator_enable(led_data->priv->regulator); + if (ret < 0) + return ret; + + led_data->enabled = 1; + + return 0; +} + +/* Caller must take care of locking */ +static int lm3532_led_disable(struct lm3532_led *led_data) +{ + int ctrl_en_val = BIT(led_data->control_bank); + int ret; + + if (!led_data->enabled) + return 0; + + ret = regmap_update_bits(led_data->priv->regmap, LM3532_REG_ENABLE, + ctrl_en_val, 0); + if (ret) { + dev_err(led_data->priv->dev, "Failed to set ctrl:%d\n", ret); + return ret; + } + + ret = regulator_disable(led_data->priv->regulator); + if (ret < 0) + return ret; + + led_data->enabled = 0; + + return 0; +} + +static int lm3532_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brt_val) +{ + struct lm3532_led *led = + container_of(led_cdev, struct lm3532_led, led_dev); + u8 brightness_reg; + int ret; + + mutex_lock(&led->priv->lock); + + if (led->mode == LM3532_ALS_CTRL) { + if (brt_val > LED_OFF) + ret = lm3532_led_enable(led); + else + ret = lm3532_led_disable(led); + + goto unlock; + } + + if (brt_val == LED_OFF) { + ret = lm3532_led_disable(led); + goto unlock; + } + + ret = lm3532_led_enable(led); + if (ret) + goto unlock; + + brightness_reg = LM3532_REG_ZONE_TRGT_A + led->control_bank * 5 + + (led->ctrl_brt_pointer >> 2); + + ret = regmap_write(led->priv->regmap, brightness_reg, brt_val); + +unlock: + mutex_unlock(&led->priv->lock); + return ret; +} + +static int lm3532_init_registers(struct lm3532_led *led) +{ + struct lm3532_data *drvdata = led->priv; + unsigned int runtime_ramp_val; + unsigned int output_cfg_val = 0; + unsigned int output_cfg_shift = 0; + unsigned int output_cfg_mask = 0; + unsigned int brightness_config_reg; + unsigned int brightness_config_val; + int fs_current_reg; + int fs_current_val; + int ret, i; + + if (drvdata->enable_gpio) + gpiod_direction_output(drvdata->enable_gpio, 1); + + brightness_config_reg = LM3532_REG_ZONE_CFG_A + led->control_bank * 2; + /* + * This could be hard coded to the default value but the control + * brightness register may have changed during boot. + */ + ret = regmap_read(drvdata->regmap, brightness_config_reg, + &led->ctrl_brt_pointer); + if (ret) + return ret; + + led->ctrl_brt_pointer &= LM3532_ZONE_MASK; + brightness_config_val = led->ctrl_brt_pointer | led->mode; + ret = regmap_write(drvdata->regmap, brightness_config_reg, + brightness_config_val); + if (ret) + return ret; + + if (led->full_scale_current) { + fs_current_reg = LM3532_REG_CTRL_A_FS_CURR + led->control_bank * 2; + fs_current_val = (led->full_scale_current - LM3532_FS_CURR_MIN) / + LM3532_FS_CURR_STEP; + + ret = regmap_write(drvdata->regmap, fs_current_reg, + fs_current_val); + if (ret) + return ret; + } + + for (i = 0; i < led->num_leds; i++) { + output_cfg_shift = led->led_strings[i] * 2; + output_cfg_val |= (led->control_bank << output_cfg_shift); + output_cfg_mask |= LM3532_OUTPUT_CFG_MASK << output_cfg_shift; + } + + ret = regmap_update_bits(drvdata->regmap, LM3532_REG_OUTPUT_CFG, + output_cfg_mask, output_cfg_val); + if (ret) + return ret; + + runtime_ramp_val = drvdata->runtime_ramp_up | + (drvdata->runtime_ramp_down << LM3532_RAMP_DOWN_SHIFT); + + return regmap_write(drvdata->regmap, LM3532_REG_RT_RAMP, + runtime_ramp_val); +} + +static int lm3532_als_configure(struct lm3532_data *priv, + struct lm3532_led *led) +{ + struct lm3532_als_data *als = priv->als_data; + u32 als_vmin, als_vmax, als_vstep; + int zone_reg = LM3532_REG_ZN_0_HI; + int ret; + int i; + + als_vmin = als->als_vmin; + als_vmax = als->als_vmax; + + als_vstep = (als_vmax - als_vmin) / ((LM3532_ALS_ZB_MAX + 1) * 2); + + for (i = 0; i < LM3532_ALS_ZB_MAX; i++) { + als->zones_lo[i] = ((als_vmin + als_vstep + (i * als_vstep)) * + LED_FULL) / 1000; + als->zones_hi[i] = ((als_vmin + LM3532_ALS_OFFSET_mV + + als_vstep + (i * als_vstep)) * LED_FULL) / 1000; + + zone_reg = LM3532_REG_ZN_0_HI + i * 2; + ret = regmap_write(priv->regmap, zone_reg, als->zones_lo[i]); + if (ret) + return ret; + + zone_reg += 1; + ret = regmap_write(priv->regmap, zone_reg, als->zones_hi[i]); + if (ret) + return ret; + } + + als->config = (als->als_avrg_time | (LM3532_ENABLE_ALS) | + (als->als_input_mode << LM3532_ALS_SEL_SHIFT)); + + return regmap_write(priv->regmap, LM3532_ALS_CONFIG, als->config); +} + +static int lm3532_parse_als(struct lm3532_data *priv) +{ + struct lm3532_als_data *als; + int als_avg_time; + int als_impedance; + int ret; + + als = devm_kzalloc(priv->dev, sizeof(*als), GFP_KERNEL); + if (als == NULL) + return -ENOMEM; + + ret = device_property_read_u32(&priv->client->dev, "ti,als-vmin", + &als->als_vmin); + if (ret) + als->als_vmin = 0; + + ret = device_property_read_u32(&priv->client->dev, "ti,als-vmax", + &als->als_vmax); + if (ret) + als->als_vmax = LM3532_ALS_WINDOW_mV; + + if (als->als_vmax > LM3532_ALS_WINDOW_mV) { + ret = -EINVAL; + return ret; + } + + ret = device_property_read_u32(&priv->client->dev, "ti,als1-imp-sel", + &als_impedance); + if (ret) + als->als1_imp_sel = 0; + else + als->als1_imp_sel = lm3532_get_als_imp_index(als_impedance); + + ret = device_property_read_u32(&priv->client->dev, "ti,als2-imp-sel", + &als_impedance); + if (ret) + als->als2_imp_sel = 0; + else + als->als2_imp_sel = lm3532_get_als_imp_index(als_impedance); + + ret = device_property_read_u32(&priv->client->dev, "ti,als-avrg-time-us", + &als_avg_time); + if (ret) + als->als_avrg_time = 0; + else + als->als_avrg_time = lm3532_get_als_avg_index(als_avg_time); + + ret = device_property_read_u8(&priv->client->dev, "ti,als-input-mode", + &als->als_input_mode); + if (ret) + als->als_input_mode = 0; + + if (als->als_input_mode > LM3532_BL_MODE_ALS) { + ret = -EINVAL; + return ret; + } + + priv->als_data = als; + + return ret; +} + +static int lm3532_parse_node(struct lm3532_data *priv) +{ + struct fwnode_handle *child = NULL; + struct lm3532_led *led; + int control_bank; + u32 ramp_time; + size_t i = 0; + int ret; + + priv->enable_gpio = devm_gpiod_get_optional(&priv->client->dev, + "enable", GPIOD_OUT_LOW); + if (IS_ERR(priv->enable_gpio)) + priv->enable_gpio = NULL; + + priv->regulator = devm_regulator_get(&priv->client->dev, "vin"); + if (IS_ERR(priv->regulator)) + priv->regulator = NULL; + + ret = device_property_read_u32(&priv->client->dev, "ramp-up-us", + &ramp_time); + if (ret) + dev_info(&priv->client->dev, "ramp-up-ms property missing\n"); + else + priv->runtime_ramp_up = lm3532_get_ramp_index(ramp_time); + + ret = device_property_read_u32(&priv->client->dev, "ramp-down-us", + &ramp_time); + if (ret) + dev_info(&priv->client->dev, "ramp-down-ms property missing\n"); + else + priv->runtime_ramp_down = lm3532_get_ramp_index(ramp_time); + + device_for_each_child_node(priv->dev, child) { + struct led_init_data idata = { + .fwnode = child, + .default_label = ":", + .devicename = priv->client->name, + }; + + led = &priv->leds[i]; + + ret = fwnode_property_read_u32(child, "reg", &control_bank); + if (ret) { + dev_err(&priv->client->dev, "reg property missing\n"); + fwnode_handle_put(child); + goto child_out; + } + + if (control_bank > LM3532_CONTROL_C) { + dev_err(&priv->client->dev, "Control bank invalid\n"); + continue; + } + + led->control_bank = control_bank; + + ret = fwnode_property_read_u32(child, "ti,led-mode", + &led->mode); + if (ret) { + dev_err(&priv->client->dev, "ti,led-mode property missing\n"); + fwnode_handle_put(child); + goto child_out; + } + + if (fwnode_property_present(child, "led-max-microamp") && + fwnode_property_read_u32(child, "led-max-microamp", + &led->full_scale_current)) + dev_err(&priv->client->dev, + "Failed getting led-max-microamp\n"); + else + led->full_scale_current = min(led->full_scale_current, + LM3532_FS_CURR_MAX); + + if (led->mode == LM3532_BL_MODE_ALS) { + led->mode = LM3532_ALS_CTRL; + ret = lm3532_parse_als(priv); + if (ret) + dev_err(&priv->client->dev, "Failed to parse als\n"); + else + lm3532_als_configure(priv, led); + } else { + led->mode = LM3532_I2C_CTRL; + } + + led->num_leds = fwnode_property_count_u32(child, "led-sources"); + if (led->num_leds > LM3532_MAX_LED_STRINGS) { + dev_err(&priv->client->dev, "Too many LED string defined\n"); + continue; + } + + ret = fwnode_property_read_u32_array(child, "led-sources", + led->led_strings, + led->num_leds); + if (ret) { + dev_err(&priv->client->dev, "led-sources property missing\n"); + fwnode_handle_put(child); + goto child_out; + } + + led->priv = priv; + led->led_dev.brightness_set_blocking = lm3532_brightness_set; + + ret = devm_led_classdev_register_ext(priv->dev, &led->led_dev, &idata); + if (ret) { + dev_err(&priv->client->dev, "led register err: %d\n", + ret); + fwnode_handle_put(child); + goto child_out; + } + + ret = lm3532_init_registers(led); + if (ret) { + dev_err(&priv->client->dev, "register init err: %d\n", + ret); + fwnode_handle_put(child); + goto child_out; + } + + i++; + } + +child_out: + return ret; +} + +static int lm3532_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct lm3532_data *drvdata; + int ret = 0; + int count; + + count = device_get_child_node_count(&client->dev); + if (!count) { + dev_err(&client->dev, "LEDs are not defined in device tree!"); + return -ENODEV; + } + + drvdata = devm_kzalloc(&client->dev, struct_size(drvdata, leds, count), + GFP_KERNEL); + if (drvdata == NULL) + return -ENOMEM; + + drvdata->client = client; + drvdata->dev = &client->dev; + + drvdata->regmap = devm_regmap_init_i2c(client, &lm3532_regmap_config); + if (IS_ERR(drvdata->regmap)) { + ret = PTR_ERR(drvdata->regmap); + dev_err(&client->dev, "Failed to allocate register map: %d\n", + ret); + return ret; + } + + mutex_init(&drvdata->lock); + i2c_set_clientdata(client, drvdata); + + ret = lm3532_parse_node(drvdata); + if (ret) { + dev_err(&client->dev, "Failed to parse node\n"); + return ret; + } + + return ret; +} + +static int lm3532_remove(struct i2c_client *client) +{ + struct lm3532_data *drvdata = i2c_get_clientdata(client); + + mutex_destroy(&drvdata->lock); + + if (drvdata->enable_gpio) + gpiod_direction_output(drvdata->enable_gpio, 0); + + return 0; +} + +static const struct of_device_id of_lm3532_leds_match[] = { + { .compatible = "ti,lm3532", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_lm3532_leds_match); + +static const struct i2c_device_id lm3532_id[] = { + {LM3532_NAME, 0}, + {} +}; +MODULE_DEVICE_TABLE(i2c, lm3532_id); + +static struct i2c_driver lm3532_i2c_driver = { + .probe = lm3532_probe, + .remove = lm3532_remove, + .id_table = lm3532_id, + .driver = { + .name = LM3532_NAME, + .of_match_table = of_lm3532_leds_match, + }, +}; +module_i2c_driver(lm3532_i2c_driver); + +MODULE_DESCRIPTION("Back Light driver for LM3532"); +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>"); diff --git a/drivers/leds/leds-lm3533.c b/drivers/leds/leds-lm3533.c new file mode 100644 index 000000000..b3edee703 --- /dev/null +++ b/drivers/leds/leds-lm3533.c @@ -0,0 +1,757 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * leds-lm3533.c -- LM3533 LED driver + * + * Copyright (C) 2011-2012 Texas Instruments + * + * Author: Johan Hovold <jhovold@gmail.com> + */ + +#include <linux/module.h> +#include <linux/leds.h> +#include <linux/mfd/core.h> +#include <linux/mutex.h> +#include <linux/platform_device.h> +#include <linux/slab.h> + +#include <linux/mfd/lm3533.h> + + +#define LM3533_LVCTRLBANK_MIN 2 +#define LM3533_LVCTRLBANK_MAX 5 +#define LM3533_LVCTRLBANK_COUNT 4 +#define LM3533_RISEFALLTIME_MAX 7 +#define LM3533_ALS_CHANNEL_LV_MIN 1 +#define LM3533_ALS_CHANNEL_LV_MAX 2 + +#define LM3533_REG_CTRLBANK_BCONF_BASE 0x1b +#define LM3533_REG_PATTERN_ENABLE 0x28 +#define LM3533_REG_PATTERN_LOW_TIME_BASE 0x71 +#define LM3533_REG_PATTERN_HIGH_TIME_BASE 0x72 +#define LM3533_REG_PATTERN_RISETIME_BASE 0x74 +#define LM3533_REG_PATTERN_FALLTIME_BASE 0x75 + +#define LM3533_REG_PATTERN_STEP 0x10 + +#define LM3533_REG_CTRLBANK_BCONF_MAPPING_MASK 0x04 +#define LM3533_REG_CTRLBANK_BCONF_ALS_EN_MASK 0x02 +#define LM3533_REG_CTRLBANK_BCONF_ALS_CHANNEL_MASK 0x01 + +#define LM3533_LED_FLAG_PATTERN_ENABLE 1 + + +struct lm3533_led { + struct lm3533 *lm3533; + struct lm3533_ctrlbank cb; + struct led_classdev cdev; + int id; + + struct mutex mutex; + unsigned long flags; +}; + + +static inline struct lm3533_led *to_lm3533_led(struct led_classdev *cdev) +{ + return container_of(cdev, struct lm3533_led, cdev); +} + +static inline int lm3533_led_get_ctrlbank_id(struct lm3533_led *led) +{ + return led->id + 2; +} + +static inline u8 lm3533_led_get_lv_reg(struct lm3533_led *led, u8 base) +{ + return base + led->id; +} + +static inline u8 lm3533_led_get_pattern(struct lm3533_led *led) +{ + return led->id; +} + +static inline u8 lm3533_led_get_pattern_reg(struct lm3533_led *led, + u8 base) +{ + return base + lm3533_led_get_pattern(led) * LM3533_REG_PATTERN_STEP; +} + +static int lm3533_led_pattern_enable(struct lm3533_led *led, int enable) +{ + u8 mask; + u8 val; + int pattern; + int state; + int ret = 0; + + dev_dbg(led->cdev.dev, "%s - %d\n", __func__, enable); + + mutex_lock(&led->mutex); + + state = test_bit(LM3533_LED_FLAG_PATTERN_ENABLE, &led->flags); + if ((enable && state) || (!enable && !state)) + goto out; + + pattern = lm3533_led_get_pattern(led); + mask = 1 << (2 * pattern); + + if (enable) + val = mask; + else + val = 0; + + ret = lm3533_update(led->lm3533, LM3533_REG_PATTERN_ENABLE, val, mask); + if (ret) { + dev_err(led->cdev.dev, "failed to enable pattern %d (%d)\n", + pattern, enable); + goto out; + } + + __change_bit(LM3533_LED_FLAG_PATTERN_ENABLE, &led->flags); +out: + mutex_unlock(&led->mutex); + + return ret; +} + +static int lm3533_led_set(struct led_classdev *cdev, + enum led_brightness value) +{ + struct lm3533_led *led = to_lm3533_led(cdev); + + dev_dbg(led->cdev.dev, "%s - %d\n", __func__, value); + + if (value == 0) + lm3533_led_pattern_enable(led, 0); /* disable blink */ + + return lm3533_ctrlbank_set_brightness(&led->cb, value); +} + +static enum led_brightness lm3533_led_get(struct led_classdev *cdev) +{ + struct lm3533_led *led = to_lm3533_led(cdev); + u8 val; + int ret; + + ret = lm3533_ctrlbank_get_brightness(&led->cb, &val); + if (ret) + return ret; + + dev_dbg(led->cdev.dev, "%s - %u\n", __func__, val); + + return val; +} + +/* Pattern generator defines (delays in us). */ +#define LM3533_LED_DELAY1_VMIN 0x00 +#define LM3533_LED_DELAY2_VMIN 0x3d +#define LM3533_LED_DELAY3_VMIN 0x80 + +#define LM3533_LED_DELAY1_VMAX (LM3533_LED_DELAY2_VMIN - 1) +#define LM3533_LED_DELAY2_VMAX (LM3533_LED_DELAY3_VMIN - 1) +#define LM3533_LED_DELAY3_VMAX 0xff + +#define LM3533_LED_DELAY1_TMIN 16384U +#define LM3533_LED_DELAY2_TMIN 1130496U +#define LM3533_LED_DELAY3_TMIN 10305536U + +#define LM3533_LED_DELAY1_TMAX 999424U +#define LM3533_LED_DELAY2_TMAX 9781248U +#define LM3533_LED_DELAY3_TMAX 76890112U + +/* t_step = (t_max - t_min) / (v_max - v_min) */ +#define LM3533_LED_DELAY1_TSTEP 16384 +#define LM3533_LED_DELAY2_TSTEP 131072 +#define LM3533_LED_DELAY3_TSTEP 524288 + +/* Delay limits for hardware accelerated blinking (in ms). */ +#define LM3533_LED_DELAY_ON_MAX \ + ((LM3533_LED_DELAY2_TMAX + LM3533_LED_DELAY2_TSTEP / 2) / 1000) +#define LM3533_LED_DELAY_OFF_MAX \ + ((LM3533_LED_DELAY3_TMAX + LM3533_LED_DELAY3_TSTEP / 2) / 1000) + +/* + * Returns linear map of *t from [t_min,t_max] to [v_min,v_max] with a step + * size of t_step, where + * + * t_step = (t_max - t_min) / (v_max - v_min) + * + * and updates *t to reflect the mapped value. + */ +static u8 time_to_val(unsigned *t, unsigned t_min, unsigned t_step, + u8 v_min, u8 v_max) +{ + unsigned val; + + val = (*t + t_step / 2 - t_min) / t_step + v_min; + + *t = t_step * (val - v_min) + t_min; + + return (u8)val; +} + +/* + * Returns time code corresponding to *delay (in ms) and updates *delay to + * reflect actual hardware delay. + * + * Hardware supports 256 discrete delay times, divided into three groups with + * the following ranges and step-sizes: + * + * [ 16, 999] [0x00, 0x3e] step 16 ms + * [ 1130, 9781] [0x3d, 0x7f] step 131 ms + * [10306, 76890] [0x80, 0xff] step 524 ms + * + * Note that delay group 3 is only available for delay_off. + */ +static u8 lm3533_led_get_hw_delay(unsigned *delay) +{ + unsigned t; + u8 val; + + t = *delay * 1000; + + if (t >= (LM3533_LED_DELAY2_TMAX + LM3533_LED_DELAY3_TMIN) / 2) { + t = clamp(t, LM3533_LED_DELAY3_TMIN, LM3533_LED_DELAY3_TMAX); + val = time_to_val(&t, LM3533_LED_DELAY3_TMIN, + LM3533_LED_DELAY3_TSTEP, + LM3533_LED_DELAY3_VMIN, + LM3533_LED_DELAY3_VMAX); + } else if (t >= (LM3533_LED_DELAY1_TMAX + LM3533_LED_DELAY2_TMIN) / 2) { + t = clamp(t, LM3533_LED_DELAY2_TMIN, LM3533_LED_DELAY2_TMAX); + val = time_to_val(&t, LM3533_LED_DELAY2_TMIN, + LM3533_LED_DELAY2_TSTEP, + LM3533_LED_DELAY2_VMIN, + LM3533_LED_DELAY2_VMAX); + } else { + t = clamp(t, LM3533_LED_DELAY1_TMIN, LM3533_LED_DELAY1_TMAX); + val = time_to_val(&t, LM3533_LED_DELAY1_TMIN, + LM3533_LED_DELAY1_TSTEP, + LM3533_LED_DELAY1_VMIN, + LM3533_LED_DELAY1_VMAX); + } + + *delay = (t + 500) / 1000; + + return val; +} + +/* + * Set delay register base to *delay (in ms) and update *delay to reflect + * actual hardware delay used. + */ +static u8 lm3533_led_delay_set(struct lm3533_led *led, u8 base, + unsigned long *delay) +{ + unsigned t; + u8 val; + u8 reg; + int ret; + + t = (unsigned)*delay; + + /* Delay group 3 is only available for low time (delay off). */ + if (base != LM3533_REG_PATTERN_LOW_TIME_BASE) + t = min(t, LM3533_LED_DELAY2_TMAX / 1000); + + val = lm3533_led_get_hw_delay(&t); + + dev_dbg(led->cdev.dev, "%s - %lu: %u (0x%02x)\n", __func__, + *delay, t, val); + reg = lm3533_led_get_pattern_reg(led, base); + ret = lm3533_write(led->lm3533, reg, val); + if (ret) + dev_err(led->cdev.dev, "failed to set delay (%02x)\n", reg); + + *delay = t; + + return ret; +} + +static int lm3533_led_delay_on_set(struct lm3533_led *led, unsigned long *t) +{ + return lm3533_led_delay_set(led, LM3533_REG_PATTERN_HIGH_TIME_BASE, t); +} + +static int lm3533_led_delay_off_set(struct lm3533_led *led, unsigned long *t) +{ + return lm3533_led_delay_set(led, LM3533_REG_PATTERN_LOW_TIME_BASE, t); +} + +static int lm3533_led_blink_set(struct led_classdev *cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct lm3533_led *led = to_lm3533_led(cdev); + int ret; + + dev_dbg(led->cdev.dev, "%s - on = %lu, off = %lu\n", __func__, + *delay_on, *delay_off); + + if (*delay_on > LM3533_LED_DELAY_ON_MAX || + *delay_off > LM3533_LED_DELAY_OFF_MAX) + return -EINVAL; + + if (*delay_on == 0 && *delay_off == 0) { + *delay_on = 500; + *delay_off = 500; + } + + ret = lm3533_led_delay_on_set(led, delay_on); + if (ret) + return ret; + + ret = lm3533_led_delay_off_set(led, delay_off); + if (ret) + return ret; + + return lm3533_led_pattern_enable(led, 1); +} + +static ssize_t show_id(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3533_led *led = to_lm3533_led(led_cdev); + + return scnprintf(buf, PAGE_SIZE, "%d\n", led->id); +} + +/* + * Pattern generator rise/fall times: + * + * 0 - 2048 us (default) + * 1 - 262 ms + * 2 - 524 ms + * 3 - 1.049 s + * 4 - 2.097 s + * 5 - 4.194 s + * 6 - 8.389 s + * 7 - 16.78 s + */ +static ssize_t show_risefalltime(struct device *dev, + struct device_attribute *attr, + char *buf, u8 base) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3533_led *led = to_lm3533_led(led_cdev); + ssize_t ret; + u8 reg; + u8 val; + + reg = lm3533_led_get_pattern_reg(led, base); + ret = lm3533_read(led->lm3533, reg, &val); + if (ret) + return ret; + + return scnprintf(buf, PAGE_SIZE, "%x\n", val); +} + +static ssize_t show_risetime(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return show_risefalltime(dev, attr, buf, + LM3533_REG_PATTERN_RISETIME_BASE); +} + +static ssize_t show_falltime(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return show_risefalltime(dev, attr, buf, + LM3533_REG_PATTERN_FALLTIME_BASE); +} + +static ssize_t store_risefalltime(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len, u8 base) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3533_led *led = to_lm3533_led(led_cdev); + u8 val; + u8 reg; + int ret; + + if (kstrtou8(buf, 0, &val) || val > LM3533_RISEFALLTIME_MAX) + return -EINVAL; + + reg = lm3533_led_get_pattern_reg(led, base); + ret = lm3533_write(led->lm3533, reg, val); + if (ret) + return ret; + + return len; +} + +static ssize_t store_risetime(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + return store_risefalltime(dev, attr, buf, len, + LM3533_REG_PATTERN_RISETIME_BASE); +} + +static ssize_t store_falltime(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + return store_risefalltime(dev, attr, buf, len, + LM3533_REG_PATTERN_FALLTIME_BASE); +} + +static ssize_t show_als_channel(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3533_led *led = to_lm3533_led(led_cdev); + unsigned channel; + u8 reg; + u8 val; + int ret; + + reg = lm3533_led_get_lv_reg(led, LM3533_REG_CTRLBANK_BCONF_BASE); + ret = lm3533_read(led->lm3533, reg, &val); + if (ret) + return ret; + + channel = (val & LM3533_REG_CTRLBANK_BCONF_ALS_CHANNEL_MASK) + 1; + + return scnprintf(buf, PAGE_SIZE, "%u\n", channel); +} + +static ssize_t store_als_channel(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3533_led *led = to_lm3533_led(led_cdev); + unsigned channel; + u8 reg; + u8 val; + u8 mask; + int ret; + + if (kstrtouint(buf, 0, &channel)) + return -EINVAL; + + if (channel < LM3533_ALS_CHANNEL_LV_MIN || + channel > LM3533_ALS_CHANNEL_LV_MAX) + return -EINVAL; + + reg = lm3533_led_get_lv_reg(led, LM3533_REG_CTRLBANK_BCONF_BASE); + mask = LM3533_REG_CTRLBANK_BCONF_ALS_CHANNEL_MASK; + val = channel - 1; + + ret = lm3533_update(led->lm3533, reg, val, mask); + if (ret) + return ret; + + return len; +} + +static ssize_t show_als_en(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3533_led *led = to_lm3533_led(led_cdev); + bool enable; + u8 reg; + u8 val; + int ret; + + reg = lm3533_led_get_lv_reg(led, LM3533_REG_CTRLBANK_BCONF_BASE); + ret = lm3533_read(led->lm3533, reg, &val); + if (ret) + return ret; + + enable = val & LM3533_REG_CTRLBANK_BCONF_ALS_EN_MASK; + + return scnprintf(buf, PAGE_SIZE, "%d\n", enable); +} + +static ssize_t store_als_en(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3533_led *led = to_lm3533_led(led_cdev); + unsigned enable; + u8 reg; + u8 mask; + u8 val; + int ret; + + if (kstrtouint(buf, 0, &enable)) + return -EINVAL; + + reg = lm3533_led_get_lv_reg(led, LM3533_REG_CTRLBANK_BCONF_BASE); + mask = LM3533_REG_CTRLBANK_BCONF_ALS_EN_MASK; + + if (enable) + val = mask; + else + val = 0; + + ret = lm3533_update(led->lm3533, reg, val, mask); + if (ret) + return ret; + + return len; +} + +static ssize_t show_linear(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3533_led *led = to_lm3533_led(led_cdev); + u8 reg; + u8 val; + int linear; + int ret; + + reg = lm3533_led_get_lv_reg(led, LM3533_REG_CTRLBANK_BCONF_BASE); + ret = lm3533_read(led->lm3533, reg, &val); + if (ret) + return ret; + + if (val & LM3533_REG_CTRLBANK_BCONF_MAPPING_MASK) + linear = 1; + else + linear = 0; + + return scnprintf(buf, PAGE_SIZE, "%x\n", linear); +} + +static ssize_t store_linear(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3533_led *led = to_lm3533_led(led_cdev); + unsigned long linear; + u8 reg; + u8 mask; + u8 val; + int ret; + + if (kstrtoul(buf, 0, &linear)) + return -EINVAL; + + reg = lm3533_led_get_lv_reg(led, LM3533_REG_CTRLBANK_BCONF_BASE); + mask = LM3533_REG_CTRLBANK_BCONF_MAPPING_MASK; + + if (linear) + val = mask; + else + val = 0; + + ret = lm3533_update(led->lm3533, reg, val, mask); + if (ret) + return ret; + + return len; +} + +static ssize_t show_pwm(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3533_led *led = to_lm3533_led(led_cdev); + u8 val; + int ret; + + ret = lm3533_ctrlbank_get_pwm(&led->cb, &val); + if (ret) + return ret; + + return scnprintf(buf, PAGE_SIZE, "%u\n", val); +} + +static ssize_t store_pwm(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3533_led *led = to_lm3533_led(led_cdev); + u8 val; + int ret; + + if (kstrtou8(buf, 0, &val)) + return -EINVAL; + + ret = lm3533_ctrlbank_set_pwm(&led->cb, val); + if (ret) + return ret; + + return len; +} + +static LM3533_ATTR_RW(als_channel); +static LM3533_ATTR_RW(als_en); +static LM3533_ATTR_RW(falltime); +static LM3533_ATTR_RO(id); +static LM3533_ATTR_RW(linear); +static LM3533_ATTR_RW(pwm); +static LM3533_ATTR_RW(risetime); + +static struct attribute *lm3533_led_attributes[] = { + &dev_attr_als_channel.attr, + &dev_attr_als_en.attr, + &dev_attr_falltime.attr, + &dev_attr_id.attr, + &dev_attr_linear.attr, + &dev_attr_pwm.attr, + &dev_attr_risetime.attr, + NULL, +}; + +static umode_t lm3533_led_attr_is_visible(struct kobject *kobj, + struct attribute *attr, int n) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3533_led *led = to_lm3533_led(led_cdev); + umode_t mode = attr->mode; + + if (attr == &dev_attr_als_channel.attr || + attr == &dev_attr_als_en.attr) { + if (!led->lm3533->have_als) + mode = 0; + } + + return mode; +}; + +static const struct attribute_group lm3533_led_attribute_group = { + .is_visible = lm3533_led_attr_is_visible, + .attrs = lm3533_led_attributes +}; + +static const struct attribute_group *lm3533_led_attribute_groups[] = { + &lm3533_led_attribute_group, + NULL +}; + +static int lm3533_led_setup(struct lm3533_led *led, + struct lm3533_led_platform_data *pdata) +{ + int ret; + + ret = lm3533_ctrlbank_set_max_current(&led->cb, pdata->max_current); + if (ret) + return ret; + + return lm3533_ctrlbank_set_pwm(&led->cb, pdata->pwm); +} + +static int lm3533_led_probe(struct platform_device *pdev) +{ + struct lm3533 *lm3533; + struct lm3533_led_platform_data *pdata; + struct lm3533_led *led; + int ret; + + dev_dbg(&pdev->dev, "%s\n", __func__); + + lm3533 = dev_get_drvdata(pdev->dev.parent); + if (!lm3533) + return -EINVAL; + + pdata = dev_get_platdata(&pdev->dev); + if (!pdata) { + dev_err(&pdev->dev, "no platform data\n"); + return -EINVAL; + } + + if (pdev->id < 0 || pdev->id >= LM3533_LVCTRLBANK_COUNT) { + dev_err(&pdev->dev, "illegal LED id %d\n", pdev->id); + return -EINVAL; + } + + led = devm_kzalloc(&pdev->dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->lm3533 = lm3533; + led->cdev.name = pdata->name; + led->cdev.default_trigger = pdata->default_trigger; + led->cdev.brightness_set_blocking = lm3533_led_set; + led->cdev.brightness_get = lm3533_led_get; + led->cdev.blink_set = lm3533_led_blink_set; + led->cdev.brightness = LED_OFF; + led->cdev.groups = lm3533_led_attribute_groups, + led->id = pdev->id; + + mutex_init(&led->mutex); + + /* The class framework makes a callback to get brightness during + * registration so use parent device (for error reporting) until + * registered. + */ + led->cb.lm3533 = lm3533; + led->cb.id = lm3533_led_get_ctrlbank_id(led); + led->cb.dev = lm3533->dev; + + platform_set_drvdata(pdev, led); + + ret = led_classdev_register(pdev->dev.parent, &led->cdev); + if (ret) { + dev_err(&pdev->dev, "failed to register LED %d\n", pdev->id); + return ret; + } + + led->cb.dev = led->cdev.dev; + + ret = lm3533_led_setup(led, pdata); + if (ret) + goto err_deregister; + + ret = lm3533_ctrlbank_enable(&led->cb); + if (ret) + goto err_deregister; + + return 0; + +err_deregister: + led_classdev_unregister(&led->cdev); + + return ret; +} + +static int lm3533_led_remove(struct platform_device *pdev) +{ + struct lm3533_led *led = platform_get_drvdata(pdev); + + dev_dbg(&pdev->dev, "%s\n", __func__); + + lm3533_ctrlbank_disable(&led->cb); + led_classdev_unregister(&led->cdev); + + return 0; +} + +static void lm3533_led_shutdown(struct platform_device *pdev) +{ + + struct lm3533_led *led = platform_get_drvdata(pdev); + + dev_dbg(&pdev->dev, "%s\n", __func__); + + lm3533_ctrlbank_disable(&led->cb); + lm3533_led_set(&led->cdev, LED_OFF); /* disable blink */ +} + +static struct platform_driver lm3533_led_driver = { + .driver = { + .name = "lm3533-leds", + }, + .probe = lm3533_led_probe, + .remove = lm3533_led_remove, + .shutdown = lm3533_led_shutdown, +}; +module_platform_driver(lm3533_led_driver); + +MODULE_AUTHOR("Johan Hovold <jhovold@gmail.com>"); +MODULE_DESCRIPTION("LM3533 LED driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:lm3533-leds"); diff --git a/drivers/leds/leds-lm355x.c b/drivers/leds/leds-lm355x.c new file mode 100644 index 000000000..150552124 --- /dev/null +++ b/drivers/leds/leds-lm355x.c @@ -0,0 +1,531 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* +* Simple driver for Texas Instruments LM355x LED Flash driver chip +* Copyright (C) 2012 Texas Instruments +*/ + +#include <linux/module.h> +#include <linux/delay.h> +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/fs.h> +#include <linux/regmap.h> +#include <linux/platform_data/leds-lm355x.h> + +enum lm355x_type { + CHIP_LM3554 = 0, + CHIP_LM3556, +}; + +enum lm355x_regs { + REG_FLAG = 0, + REG_TORCH_CFG, + REG_TORCH_CTRL, + REG_STROBE_CFG, + REG_FLASH_CTRL, + REG_INDI_CFG, + REG_INDI_CTRL, + REG_OPMODE, + REG_MAX, +}; + +/* operation mode */ +enum lm355x_mode { + MODE_SHDN = 0, + MODE_INDIC, + MODE_TORCH, + MODE_FLASH +}; + +/* register map info. */ +struct lm355x_reg_data { + u8 regno; + u8 mask; + u8 shift; +}; + +struct lm355x_chip_data { + struct device *dev; + enum lm355x_type type; + + struct led_classdev cdev_flash; + struct led_classdev cdev_torch; + struct led_classdev cdev_indicator; + + struct lm355x_platform_data *pdata; + struct regmap *regmap; + struct mutex lock; + + unsigned int last_flag; + struct lm355x_reg_data *regs; +}; + +/* specific indicator function for lm3556 */ +enum lm3556_indic_pulse_time { + PULSE_TIME_0_MS = 0, + PULSE_TIME_32_MS, + PULSE_TIME_64_MS, + PULSE_TIME_92_MS, + PULSE_TIME_128_MS, + PULSE_TIME_160_MS, + PULSE_TIME_196_MS, + PULSE_TIME_224_MS, + PULSE_TIME_256_MS, + PULSE_TIME_288_MS, + PULSE_TIME_320_MS, + PULSE_TIME_352_MS, + PULSE_TIME_384_MS, + PULSE_TIME_416_MS, + PULSE_TIME_448_MS, + PULSE_TIME_480_MS, +}; + +enum lm3556_indic_n_blank { + INDIC_N_BLANK_0 = 0, + INDIC_N_BLANK_1, + INDIC_N_BLANK_2, + INDIC_N_BLANK_3, + INDIC_N_BLANK_4, + INDIC_N_BLANK_5, + INDIC_N_BLANK_6, + INDIC_N_BLANK_7, + INDIC_N_BLANK_8, + INDIC_N_BLANK_9, + INDIC_N_BLANK_10, + INDIC_N_BLANK_11, + INDIC_N_BLANK_12, + INDIC_N_BLANK_13, + INDIC_N_BLANK_14, + INDIC_N_BLANK_15, +}; + +enum lm3556_indic_period { + INDIC_PERIOD_0 = 0, + INDIC_PERIOD_1, + INDIC_PERIOD_2, + INDIC_PERIOD_3, + INDIC_PERIOD_4, + INDIC_PERIOD_5, + INDIC_PERIOD_6, + INDIC_PERIOD_7, +}; + +#define INDIC_PATTERN_SIZE 4 + +struct indicator { + u8 blinking; + u8 period_cnt; +}; + +/* indicator pattern data only for lm3556 */ +static struct indicator indicator_pattern[INDIC_PATTERN_SIZE] = { + [0] = {(INDIC_N_BLANK_1 << 4) | PULSE_TIME_32_MS, INDIC_PERIOD_1}, + [1] = {(INDIC_N_BLANK_15 << 4) | PULSE_TIME_32_MS, INDIC_PERIOD_2}, + [2] = {(INDIC_N_BLANK_10 << 4) | PULSE_TIME_32_MS, INDIC_PERIOD_4}, + [3] = {(INDIC_N_BLANK_5 << 4) | PULSE_TIME_32_MS, INDIC_PERIOD_7}, +}; + +static struct lm355x_reg_data lm3554_regs[REG_MAX] = { + [REG_FLAG] = {0xD0, 0xBF, 0}, + [REG_TORCH_CFG] = {0xE0, 0x80, 7}, + [REG_TORCH_CTRL] = {0xA0, 0x38, 3}, + [REG_STROBE_CFG] = {0xE0, 0x04, 2}, + [REG_FLASH_CTRL] = {0xB0, 0x78, 3}, + [REG_INDI_CFG] = {0xE0, 0x08, 3}, + [REG_INDI_CTRL] = {0xA0, 0xC0, 6}, + [REG_OPMODE] = {0xA0, 0x03, 0}, +}; + +static struct lm355x_reg_data lm3556_regs[REG_MAX] = { + [REG_FLAG] = {0x0B, 0xFF, 0}, + [REG_TORCH_CFG] = {0x0A, 0x10, 4}, + [REG_TORCH_CTRL] = {0x09, 0x70, 4}, + [REG_STROBE_CFG] = {0x0A, 0x20, 5}, + [REG_FLASH_CTRL] = {0x09, 0x0F, 0}, + [REG_INDI_CFG] = {0xFF, 0xFF, 0}, + [REG_INDI_CTRL] = {0x09, 0x70, 4}, + [REG_OPMODE] = {0x0A, 0x03, 0}, +}; + +static char lm355x_name[][I2C_NAME_SIZE] = { + [CHIP_LM3554] = LM3554_NAME, + [CHIP_LM3556] = LM3556_NAME, +}; + +/* chip initialize */ +static int lm355x_chip_init(struct lm355x_chip_data *chip) +{ + int ret; + unsigned int reg_val; + struct lm355x_platform_data *pdata = chip->pdata; + + /* input and output pins configuration */ + switch (chip->type) { + case CHIP_LM3554: + reg_val = (u32)pdata->pin_tx2 | (u32)pdata->ntc_pin; + ret = regmap_update_bits(chip->regmap, 0xE0, 0x28, reg_val); + if (ret < 0) + goto out; + reg_val = (u32)pdata->pass_mode; + ret = regmap_update_bits(chip->regmap, 0xA0, 0x04, reg_val); + if (ret < 0) + goto out; + break; + + case CHIP_LM3556: + reg_val = (u32)pdata->pin_tx2 | (u32)pdata->ntc_pin | + (u32)pdata->pass_mode; + ret = regmap_update_bits(chip->regmap, 0x0A, 0xC4, reg_val); + if (ret < 0) + goto out; + break; + default: + return -ENODATA; + } + + return ret; +out: + dev_err(chip->dev, "%s:i2c access fail to register\n", __func__); + return ret; +} + +/* chip control */ +static int lm355x_control(struct lm355x_chip_data *chip, + u8 brightness, enum lm355x_mode opmode) +{ + int ret; + unsigned int reg_val; + struct lm355x_platform_data *pdata = chip->pdata; + struct lm355x_reg_data *preg = chip->regs; + + ret = regmap_read(chip->regmap, preg[REG_FLAG].regno, &chip->last_flag); + if (ret < 0) + goto out; + if (chip->last_flag & preg[REG_FLAG].mask) + dev_info(chip->dev, "%s Last FLAG is 0x%x\n", + lm355x_name[chip->type], + chip->last_flag & preg[REG_FLAG].mask); + /* brightness 0 means shutdown */ + if (!brightness) + opmode = MODE_SHDN; + + switch (opmode) { + case MODE_TORCH: + ret = + regmap_update_bits(chip->regmap, preg[REG_TORCH_CTRL].regno, + preg[REG_TORCH_CTRL].mask, + (brightness - 1) + << preg[REG_TORCH_CTRL].shift); + if (ret < 0) + goto out; + + if (pdata->pin_tx1 != LM355x_PIN_TORCH_DISABLE) { + ret = + regmap_update_bits(chip->regmap, + preg[REG_TORCH_CFG].regno, + preg[REG_TORCH_CFG].mask, + 0x01 << + preg[REG_TORCH_CFG].shift); + if (ret < 0) + goto out; + opmode = MODE_SHDN; + dev_info(chip->dev, + "torch brt is set - ext. torch pin mode\n"); + } + break; + + case MODE_FLASH: + + ret = + regmap_update_bits(chip->regmap, preg[REG_FLASH_CTRL].regno, + preg[REG_FLASH_CTRL].mask, + (brightness - 1) + << preg[REG_FLASH_CTRL].shift); + if (ret < 0) + goto out; + + if (pdata->pin_strobe != LM355x_PIN_STROBE_DISABLE) { + if (chip->type == CHIP_LM3554) + reg_val = 0x00; + else + reg_val = 0x01; + ret = + regmap_update_bits(chip->regmap, + preg[REG_STROBE_CFG].regno, + preg[REG_STROBE_CFG].mask, + reg_val << + preg[REG_STROBE_CFG].shift); + if (ret < 0) + goto out; + opmode = MODE_SHDN; + dev_info(chip->dev, + "flash brt is set - ext. strobe pin mode\n"); + } + break; + + case MODE_INDIC: + ret = + regmap_update_bits(chip->regmap, preg[REG_INDI_CTRL].regno, + preg[REG_INDI_CTRL].mask, + (brightness - 1) + << preg[REG_INDI_CTRL].shift); + if (ret < 0) + goto out; + + if (pdata->pin_tx2 != LM355x_PIN_TX_DISABLE) { + ret = + regmap_update_bits(chip->regmap, + preg[REG_INDI_CFG].regno, + preg[REG_INDI_CFG].mask, + 0x01 << + preg[REG_INDI_CFG].shift); + if (ret < 0) + goto out; + opmode = MODE_SHDN; + } + break; + case MODE_SHDN: + break; + default: + return -EINVAL; + } + /* operation mode control */ + ret = regmap_update_bits(chip->regmap, preg[REG_OPMODE].regno, + preg[REG_OPMODE].mask, + opmode << preg[REG_OPMODE].shift); + if (ret < 0) + goto out; + return ret; +out: + dev_err(chip->dev, "%s:i2c access fail to register\n", __func__); + return ret; +} + +/* torch */ + +static int lm355x_torch_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct lm355x_chip_data *chip = + container_of(cdev, struct lm355x_chip_data, cdev_torch); + int ret; + + mutex_lock(&chip->lock); + ret = lm355x_control(chip, brightness, MODE_TORCH); + mutex_unlock(&chip->lock); + return ret; +} + +/* flash */ + +static int lm355x_strobe_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct lm355x_chip_data *chip = + container_of(cdev, struct lm355x_chip_data, cdev_flash); + int ret; + + mutex_lock(&chip->lock); + ret = lm355x_control(chip, brightness, MODE_FLASH); + mutex_unlock(&chip->lock); + return ret; +} + +/* indicator */ + +static int lm355x_indicator_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct lm355x_chip_data *chip = + container_of(cdev, struct lm355x_chip_data, cdev_indicator); + int ret; + + mutex_lock(&chip->lock); + ret = lm355x_control(chip, brightness, MODE_INDIC); + mutex_unlock(&chip->lock); + return ret; +} + +/* indicator pattern only for lm3556*/ +static ssize_t lm3556_indicator_pattern_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + ssize_t ret; + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm355x_chip_data *chip = + container_of(led_cdev, struct lm355x_chip_data, cdev_indicator); + unsigned int state; + + ret = kstrtouint(buf, 10, &state); + if (ret) + goto out; + if (state > INDIC_PATTERN_SIZE - 1) + state = INDIC_PATTERN_SIZE - 1; + + ret = regmap_write(chip->regmap, 0x04, + indicator_pattern[state].blinking); + if (ret < 0) + goto out; + + ret = regmap_write(chip->regmap, 0x05, + indicator_pattern[state].period_cnt); + if (ret < 0) + goto out; + + return size; +out: + dev_err(chip->dev, "%s:i2c access fail to register\n", __func__); + return ret; +} + +static DEVICE_ATTR(pattern, S_IWUSR, NULL, lm3556_indicator_pattern_store); + +static struct attribute *lm355x_indicator_attrs[] = { + &dev_attr_pattern.attr, + NULL +}; +ATTRIBUTE_GROUPS(lm355x_indicator); + +static const struct regmap_config lm355x_regmap = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 0xFF, +}; + +/* module initialize */ +static int lm355x_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct lm355x_platform_data *pdata = dev_get_platdata(&client->dev); + struct lm355x_chip_data *chip; + + int err; + + if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) { + dev_err(&client->dev, "i2c functionality check fail.\n"); + return -EOPNOTSUPP; + } + + if (pdata == NULL) { + dev_err(&client->dev, "needs Platform Data.\n"); + return -ENODATA; + } + + chip = devm_kzalloc(&client->dev, + sizeof(struct lm355x_chip_data), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->dev = &client->dev; + chip->type = id->driver_data; + switch (id->driver_data) { + case CHIP_LM3554: + chip->regs = lm3554_regs; + break; + case CHIP_LM3556: + chip->regs = lm3556_regs; + break; + default: + return -ENOSYS; + } + chip->pdata = pdata; + + chip->regmap = devm_regmap_init_i2c(client, &lm355x_regmap); + if (IS_ERR(chip->regmap)) { + err = PTR_ERR(chip->regmap); + dev_err(&client->dev, + "Failed to allocate register map: %d\n", err); + return err; + } + + mutex_init(&chip->lock); + i2c_set_clientdata(client, chip); + + err = lm355x_chip_init(chip); + if (err < 0) + goto err_out; + + /* flash */ + chip->cdev_flash.name = "flash"; + chip->cdev_flash.max_brightness = 16; + chip->cdev_flash.brightness_set_blocking = lm355x_strobe_brightness_set; + chip->cdev_flash.default_trigger = "flash"; + err = led_classdev_register(&client->dev, &chip->cdev_flash); + if (err < 0) + goto err_out; + /* torch */ + chip->cdev_torch.name = "torch"; + chip->cdev_torch.max_brightness = 8; + chip->cdev_torch.brightness_set_blocking = lm355x_torch_brightness_set; + chip->cdev_torch.default_trigger = "torch"; + err = led_classdev_register(&client->dev, &chip->cdev_torch); + if (err < 0) + goto err_create_torch_file; + /* indicator */ + chip->cdev_indicator.name = "indicator"; + if (id->driver_data == CHIP_LM3554) + chip->cdev_indicator.max_brightness = 4; + else + chip->cdev_indicator.max_brightness = 8; + chip->cdev_indicator.brightness_set_blocking = + lm355x_indicator_brightness_set; + /* indicator pattern control only for LM3556 */ + if (id->driver_data == CHIP_LM3556) + chip->cdev_indicator.groups = lm355x_indicator_groups; + err = led_classdev_register(&client->dev, &chip->cdev_indicator); + if (err < 0) + goto err_create_indicator_file; + + dev_info(&client->dev, "%s is initialized\n", + lm355x_name[id->driver_data]); + return 0; + +err_create_indicator_file: + led_classdev_unregister(&chip->cdev_torch); +err_create_torch_file: + led_classdev_unregister(&chip->cdev_flash); +err_out: + return err; +} + +static int lm355x_remove(struct i2c_client *client) +{ + struct lm355x_chip_data *chip = i2c_get_clientdata(client); + struct lm355x_reg_data *preg = chip->regs; + + regmap_write(chip->regmap, preg[REG_OPMODE].regno, 0); + led_classdev_unregister(&chip->cdev_indicator); + led_classdev_unregister(&chip->cdev_torch); + led_classdev_unregister(&chip->cdev_flash); + dev_info(&client->dev, "%s is removed\n", lm355x_name[chip->type]); + + return 0; +} + +static const struct i2c_device_id lm355x_id[] = { + {LM3554_NAME, CHIP_LM3554}, + {LM3556_NAME, CHIP_LM3556}, + {} +}; + +MODULE_DEVICE_TABLE(i2c, lm355x_id); + +static struct i2c_driver lm355x_i2c_driver = { + .driver = { + .name = LM355x_NAME, + .pm = NULL, + }, + .probe = lm355x_probe, + .remove = lm355x_remove, + .id_table = lm355x_id, +}; + +module_i2c_driver(lm355x_i2c_driver); + +MODULE_DESCRIPTION("Texas Instruments Flash Lighting driver for LM355x"); +MODULE_AUTHOR("Daniel Jeong <daniel.jeong@ti.com>"); +MODULE_AUTHOR("G.Shark Jeong <gshark.jeong@gmail.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-lm3601x.c b/drivers/leds/leds-lm3601x.c new file mode 100644 index 000000000..3d1272748 --- /dev/null +++ b/drivers/leds/leds-lm3601x.c @@ -0,0 +1,479 @@ +// SPDX-License-Identifier: GPL-2.0 +// Flash and torch driver for Texas Instruments LM3601X LED +// Flash driver chip family +// Copyright (C) 2018 Texas Instruments Incorporated - https://www.ti.com/ + +#include <linux/delay.h> +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/led-class-flash.h> +#include <linux/module.h> +#include <linux/regmap.h> +#include <linux/slab.h> + +#define LM3601X_LED_IR 0x0 +#define LM3601X_LED_TORCH 0x1 + +/* Registers */ +#define LM3601X_ENABLE_REG 0x01 +#define LM3601X_CFG_REG 0x02 +#define LM3601X_LED_FLASH_REG 0x03 +#define LM3601X_LED_TORCH_REG 0x04 +#define LM3601X_FLAGS_REG 0x05 +#define LM3601X_DEV_ID_REG 0x06 + +#define LM3601X_SW_RESET BIT(7) + +/* Enable Mode bits */ +#define LM3601X_MODE_STANDBY 0x00 +#define LM3601X_MODE_IR_DRV BIT(0) +#define LM3601X_MODE_TORCH BIT(1) +#define LM3601X_MODE_STROBE (BIT(0) | BIT(1)) +#define LM3601X_STRB_EN BIT(2) +#define LM3601X_STRB_EDGE_TRIG BIT(3) +#define LM3601X_IVFM_EN BIT(4) + +#define LM36010_BOOST_LIMIT_28 BIT(5) +#define LM36010_BOOST_FREQ_4MHZ BIT(6) +#define LM36010_BOOST_MODE_PASS BIT(7) + +/* Flag Mask */ +#define LM3601X_FLASH_TIME_OUT BIT(0) +#define LM3601X_UVLO_FAULT BIT(1) +#define LM3601X_THERM_SHUTDOWN BIT(2) +#define LM3601X_THERM_CURR BIT(3) +#define LM36010_CURR_LIMIT BIT(4) +#define LM3601X_SHORT_FAULT BIT(5) +#define LM3601X_IVFM_TRIP BIT(6) +#define LM36010_OVP_FAULT BIT(7) + +#define LM3601X_MAX_TORCH_I_UA 376000 +#define LM3601X_MIN_TORCH_I_UA 2400 +#define LM3601X_TORCH_REG_DIV 2965 + +#define LM3601X_MAX_STROBE_I_UA 1500000 +#define LM3601X_MIN_STROBE_I_UA 11000 +#define LM3601X_STROBE_REG_DIV 11800 + +#define LM3601X_TIMEOUT_MASK 0x1e +#define LM3601X_ENABLE_MASK (LM3601X_MODE_IR_DRV | LM3601X_MODE_TORCH) + +#define LM3601X_LOWER_STEP_US 40000 +#define LM3601X_UPPER_STEP_US 200000 +#define LM3601X_MIN_TIMEOUT_US 40000 +#define LM3601X_MAX_TIMEOUT_US 1600000 +#define LM3601X_TIMEOUT_XOVER_US 400000 + +enum lm3601x_type { + CHIP_LM36010 = 0, + CHIP_LM36011, +}; + +/** + * struct lm3601x_led - + * @fled_cdev: flash LED class device pointer + * @client: Pointer to the I2C client + * @regmap: Devices register map + * @lock: Lock for reading/writing the device + * @led_name: LED label for the Torch or IR LED + * @flash_timeout: the timeout for the flash + * @last_flag: last known flags register value + * @torch_current_max: maximum current for the torch + * @flash_current_max: maximum current for the flash + * @max_flash_timeout: maximum timeout for the flash + * @led_mode: The mode to enable either IR or Torch + */ +struct lm3601x_led { + struct led_classdev_flash fled_cdev; + struct i2c_client *client; + struct regmap *regmap; + struct mutex lock; + + unsigned int flash_timeout; + unsigned int last_flag; + + u32 torch_current_max; + u32 flash_current_max; + u32 max_flash_timeout; + + u32 led_mode; +}; + +static const struct reg_default lm3601x_regmap_defs[] = { + { LM3601X_ENABLE_REG, 0x20 }, + { LM3601X_CFG_REG, 0x15 }, + { LM3601X_LED_FLASH_REG, 0x00 }, + { LM3601X_LED_TORCH_REG, 0x00 }, +}; + +static bool lm3601x_volatile_reg(struct device *dev, unsigned int reg) +{ + switch (reg) { + case LM3601X_FLAGS_REG: + return true; + default: + return false; + } +} + +static const struct regmap_config lm3601x_regmap = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = LM3601X_DEV_ID_REG, + .reg_defaults = lm3601x_regmap_defs, + .num_reg_defaults = ARRAY_SIZE(lm3601x_regmap_defs), + .cache_type = REGCACHE_RBTREE, + .volatile_reg = lm3601x_volatile_reg, +}; + +static struct lm3601x_led *fled_cdev_to_led(struct led_classdev_flash *fled_cdev) +{ + return container_of(fled_cdev, struct lm3601x_led, fled_cdev); +} + +static int lm3601x_read_faults(struct lm3601x_led *led) +{ + int flags_val; + int ret; + + ret = regmap_read(led->regmap, LM3601X_FLAGS_REG, &flags_val); + if (ret < 0) + return -EIO; + + led->last_flag = 0; + + if (flags_val & LM36010_OVP_FAULT) + led->last_flag |= LED_FAULT_OVER_VOLTAGE; + + if (flags_val & (LM3601X_THERM_SHUTDOWN | LM3601X_THERM_CURR)) + led->last_flag |= LED_FAULT_OVER_TEMPERATURE; + + if (flags_val & LM3601X_SHORT_FAULT) + led->last_flag |= LED_FAULT_SHORT_CIRCUIT; + + if (flags_val & LM36010_CURR_LIMIT) + led->last_flag |= LED_FAULT_OVER_CURRENT; + + if (flags_val & LM3601X_UVLO_FAULT) + led->last_flag |= LED_FAULT_UNDER_VOLTAGE; + + if (flags_val & LM3601X_IVFM_TRIP) + led->last_flag |= LED_FAULT_INPUT_VOLTAGE; + + if (flags_val & LM3601X_THERM_SHUTDOWN) + led->last_flag |= LED_FAULT_LED_OVER_TEMPERATURE; + + return led->last_flag; +} + +static int lm3601x_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(cdev); + struct lm3601x_led *led = fled_cdev_to_led(fled_cdev); + int ret, led_mode_val; + + mutex_lock(&led->lock); + + ret = lm3601x_read_faults(led); + if (ret < 0) + goto out; + + if (led->led_mode == LM3601X_LED_TORCH) + led_mode_val = LM3601X_MODE_TORCH; + else + led_mode_val = LM3601X_MODE_IR_DRV; + + if (brightness == LED_OFF) { + ret = regmap_update_bits(led->regmap, LM3601X_ENABLE_REG, + led_mode_val, LED_OFF); + goto out; + } + + ret = regmap_write(led->regmap, LM3601X_LED_TORCH_REG, brightness); + if (ret < 0) + goto out; + + ret = regmap_update_bits(led->regmap, LM3601X_ENABLE_REG, + LM3601X_MODE_TORCH | LM3601X_MODE_IR_DRV, + led_mode_val); +out: + mutex_unlock(&led->lock); + return ret; +} + +static int lm3601x_strobe_set(struct led_classdev_flash *fled_cdev, + bool state) +{ + struct lm3601x_led *led = fled_cdev_to_led(fled_cdev); + int timeout_reg_val; + int current_timeout; + int ret; + + mutex_lock(&led->lock); + + ret = regmap_read(led->regmap, LM3601X_CFG_REG, ¤t_timeout); + if (ret < 0) + goto out; + + if (led->flash_timeout >= LM3601X_TIMEOUT_XOVER_US) + timeout_reg_val = led->flash_timeout / LM3601X_UPPER_STEP_US + 0x07; + else + timeout_reg_val = led->flash_timeout / LM3601X_LOWER_STEP_US - 0x01; + + if (led->flash_timeout != current_timeout) + ret = regmap_update_bits(led->regmap, LM3601X_CFG_REG, + LM3601X_TIMEOUT_MASK, timeout_reg_val); + + if (state) + ret = regmap_update_bits(led->regmap, LM3601X_ENABLE_REG, + LM3601X_MODE_TORCH | LM3601X_MODE_IR_DRV, + LM3601X_MODE_STROBE); + else + ret = regmap_update_bits(led->regmap, LM3601X_ENABLE_REG, + LM3601X_MODE_STROBE, LED_OFF); + + ret = lm3601x_read_faults(led); +out: + mutex_unlock(&led->lock); + return ret; +} + +static int lm3601x_flash_brightness_set(struct led_classdev_flash *fled_cdev, + u32 brightness) +{ + struct lm3601x_led *led = fled_cdev_to_led(fled_cdev); + u8 brightness_val; + int ret; + + mutex_lock(&led->lock); + ret = lm3601x_read_faults(led); + if (ret < 0) + goto out; + + if (brightness == LED_OFF) { + ret = regmap_update_bits(led->regmap, LM3601X_ENABLE_REG, + LM3601X_MODE_STROBE, LED_OFF); + goto out; + } + + brightness_val = brightness / LM3601X_STROBE_REG_DIV; + + ret = regmap_write(led->regmap, LM3601X_LED_FLASH_REG, brightness_val); +out: + mutex_unlock(&led->lock); + return ret; +} + +static int lm3601x_flash_timeout_set(struct led_classdev_flash *fled_cdev, + u32 timeout) +{ + struct lm3601x_led *led = fled_cdev_to_led(fled_cdev); + + mutex_lock(&led->lock); + + led->flash_timeout = timeout; + + mutex_unlock(&led->lock); + + return 0; +} + +static int lm3601x_strobe_get(struct led_classdev_flash *fled_cdev, bool *state) +{ + struct lm3601x_led *led = fled_cdev_to_led(fled_cdev); + int strobe_state; + int ret; + + mutex_lock(&led->lock); + + ret = regmap_read(led->regmap, LM3601X_ENABLE_REG, &strobe_state); + if (ret < 0) + goto out; + + *state = strobe_state & LM3601X_MODE_STROBE; + +out: + mutex_unlock(&led->lock); + return ret; +} + +static int lm3601x_flash_fault_get(struct led_classdev_flash *fled_cdev, + u32 *fault) +{ + struct lm3601x_led *led = fled_cdev_to_led(fled_cdev); + + lm3601x_read_faults(led); + + *fault = led->last_flag; + + return 0; +} + +static const struct led_flash_ops flash_ops = { + .flash_brightness_set = lm3601x_flash_brightness_set, + .strobe_set = lm3601x_strobe_set, + .strobe_get = lm3601x_strobe_get, + .timeout_set = lm3601x_flash_timeout_set, + .fault_get = lm3601x_flash_fault_get, +}; + +static int lm3601x_register_leds(struct lm3601x_led *led, + struct fwnode_handle *fwnode) +{ + struct led_classdev *led_cdev; + struct led_flash_setting *setting; + struct led_init_data init_data = {}; + + led->fled_cdev.ops = &flash_ops; + + setting = &led->fled_cdev.timeout; + setting->min = LM3601X_MIN_TIMEOUT_US; + setting->max = led->max_flash_timeout; + setting->step = LM3601X_LOWER_STEP_US; + setting->val = led->max_flash_timeout; + + setting = &led->fled_cdev.brightness; + setting->min = LM3601X_MIN_STROBE_I_UA; + setting->max = led->flash_current_max; + setting->step = LM3601X_TORCH_REG_DIV; + setting->val = led->flash_current_max; + + led_cdev = &led->fled_cdev.led_cdev; + led_cdev->brightness_set_blocking = lm3601x_brightness_set; + led_cdev->max_brightness = DIV_ROUND_UP(led->torch_current_max, + LM3601X_TORCH_REG_DIV); + led_cdev->flags |= LED_DEV_CAP_FLASH; + + init_data.fwnode = fwnode; + init_data.devicename = led->client->name; + init_data.default_label = (led->led_mode == LM3601X_LED_TORCH) ? + "torch" : "infrared"; + return devm_led_classdev_flash_register_ext(&led->client->dev, + &led->fled_cdev, &init_data); +} + +static int lm3601x_parse_node(struct lm3601x_led *led, + struct fwnode_handle **fwnode) +{ + struct fwnode_handle *child = NULL; + int ret = -ENODEV; + + child = device_get_next_child_node(&led->client->dev, child); + if (!child) { + dev_err(&led->client->dev, "No LED Child node\n"); + return ret; + } + + ret = fwnode_property_read_u32(child, "reg", &led->led_mode); + if (ret) { + dev_err(&led->client->dev, "reg DT property missing\n"); + goto out_err; + } + + if (led->led_mode > LM3601X_LED_TORCH || + led->led_mode < LM3601X_LED_IR) { + dev_warn(&led->client->dev, "Invalid led mode requested\n"); + ret = -EINVAL; + goto out_err; + } + + ret = fwnode_property_read_u32(child, "led-max-microamp", + &led->torch_current_max); + if (ret) { + dev_warn(&led->client->dev, + "led-max-microamp DT property missing\n"); + goto out_err; + } + + ret = fwnode_property_read_u32(child, "flash-max-microamp", + &led->flash_current_max); + if (ret) { + dev_warn(&led->client->dev, + "flash-max-microamp DT property missing\n"); + goto out_err; + } + + ret = fwnode_property_read_u32(child, "flash-max-timeout-us", + &led->max_flash_timeout); + if (ret) { + dev_warn(&led->client->dev, + "flash-max-timeout-us DT property missing\n"); + goto out_err; + } + + *fwnode = child; + +out_err: + fwnode_handle_put(child); + return ret; +} + +static int lm3601x_probe(struct i2c_client *client) +{ + struct lm3601x_led *led; + struct fwnode_handle *fwnode; + int ret; + + led = devm_kzalloc(&client->dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->client = client; + i2c_set_clientdata(client, led); + + ret = lm3601x_parse_node(led, &fwnode); + if (ret) + return -ENODEV; + + led->regmap = devm_regmap_init_i2c(client, &lm3601x_regmap); + if (IS_ERR(led->regmap)) { + ret = PTR_ERR(led->regmap); + dev_err(&client->dev, + "Failed to allocate register map: %d\n", ret); + return ret; + } + + mutex_init(&led->lock); + + return lm3601x_register_leds(led, fwnode); +} + +static int lm3601x_remove(struct i2c_client *client) +{ + struct lm3601x_led *led = i2c_get_clientdata(client); + + return regmap_update_bits(led->regmap, LM3601X_ENABLE_REG, + LM3601X_ENABLE_MASK, + LM3601X_MODE_STANDBY); +} + +static const struct i2c_device_id lm3601x_id[] = { + { "LM36010", CHIP_LM36010 }, + { "LM36011", CHIP_LM36011 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, lm3601x_id); + +static const struct of_device_id of_lm3601x_leds_match[] = { + { .compatible = "ti,lm36010", }, + { .compatible = "ti,lm36011", }, + { } +}; +MODULE_DEVICE_TABLE(of, of_lm3601x_leds_match); + +static struct i2c_driver lm3601x_i2c_driver = { + .driver = { + .name = "lm3601x", + .of_match_table = of_lm3601x_leds_match, + }, + .probe_new = lm3601x_probe, + .remove = lm3601x_remove, + .id_table = lm3601x_id, +}; +module_i2c_driver(lm3601x_i2c_driver); + +MODULE_DESCRIPTION("Texas Instruments Flash Lighting driver for LM3601X"); +MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-lm36274.c b/drivers/leds/leds-lm36274.c new file mode 100644 index 000000000..a23a9424c --- /dev/null +++ b/drivers/leds/leds-lm36274.c @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-2.0 +// TI LM36274 LED chip family driver +// Copyright (C) 2019 Texas Instruments Incorporated - https://www.ti.com/ + +#include <linux/bitops.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/leds.h> +#include <linux/leds-ti-lmu-common.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> + +#include <linux/mfd/ti-lmu.h> +#include <linux/mfd/ti-lmu-register.h> + +#include <uapi/linux/uleds.h> + +#define LM36274_MAX_STRINGS 4 +#define LM36274_BL_EN BIT(4) + +/** + * struct lm36274 + * @pdev: platform device + * @led_dev: led class device + * @lmu_data: Register and setting values for common code + * @regmap: Devices register map + * @dev: Pointer to the devices device struct + * @led_sources: The LED strings supported in this array + * @num_leds: Number of LED strings are supported in this array + */ +struct lm36274 { + struct platform_device *pdev; + struct led_classdev led_dev; + struct ti_lmu_bank lmu_data; + struct regmap *regmap; + struct device *dev; + + u32 led_sources[LM36274_MAX_STRINGS]; + int num_leds; +}; + +static int lm36274_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brt_val) +{ + struct lm36274 *chip = container_of(led_cdev, struct lm36274, led_dev); + + return ti_lmu_common_set_brightness(&chip->lmu_data, brt_val); +} + +static int lm36274_init(struct lm36274 *chip) +{ + int enable_val = 0; + int i; + + for (i = 0; i < chip->num_leds; i++) + enable_val |= (1 << chip->led_sources[i]); + + if (!enable_val) { + dev_err(chip->dev, "No LEDs were enabled\n"); + return -EINVAL; + } + + enable_val |= LM36274_BL_EN; + + return regmap_write(chip->regmap, LM36274_REG_BL_EN, enable_val); +} + +static int lm36274_parse_dt(struct lm36274 *chip, + struct led_init_data *init_data) +{ + struct device *dev = chip->dev; + struct fwnode_handle *child; + int ret; + + /* There should only be 1 node */ + if (device_get_child_node_count(dev) != 1) + return -EINVAL; + + child = device_get_next_child_node(dev, NULL); + + init_data->fwnode = child; + init_data->devicename = chip->pdev->name; + /* for backwards compatibility when `label` property is not present */ + init_data->default_label = ":"; + + chip->num_leds = fwnode_property_count_u32(child, "led-sources"); + if (chip->num_leds <= 0) { + ret = -ENODEV; + goto err; + } + + ret = fwnode_property_read_u32_array(child, "led-sources", + chip->led_sources, chip->num_leds); + if (ret) { + dev_err(dev, "led-sources property missing\n"); + goto err; + } + + return 0; +err: + fwnode_handle_put(child); + return ret; +} + +static int lm36274_probe(struct platform_device *pdev) +{ + struct ti_lmu *lmu = dev_get_drvdata(pdev->dev.parent); + struct led_init_data init_data = {}; + struct lm36274 *chip; + int ret; + + chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->pdev = pdev; + chip->dev = &pdev->dev; + chip->regmap = lmu->regmap; + platform_set_drvdata(pdev, chip); + + ret = lm36274_parse_dt(chip, &init_data); + if (ret) { + dev_err(chip->dev, "Failed to parse DT node\n"); + return ret; + } + + ret = lm36274_init(chip); + if (ret) { + fwnode_handle_put(init_data.fwnode); + dev_err(chip->dev, "Failed to init the device\n"); + return ret; + } + + chip->lmu_data.regmap = chip->regmap; + chip->lmu_data.max_brightness = MAX_BRIGHTNESS_11BIT; + chip->lmu_data.msb_brightness_reg = LM36274_REG_BRT_MSB; + chip->lmu_data.lsb_brightness_reg = LM36274_REG_BRT_LSB; + + chip->led_dev.max_brightness = MAX_BRIGHTNESS_11BIT; + chip->led_dev.brightness_set_blocking = lm36274_brightness_set; + + ret = devm_led_classdev_register_ext(chip->dev, &chip->led_dev, + &init_data); + if (ret) + dev_err(chip->dev, "Failed to register LED for node %pfw\n", + init_data.fwnode); + + fwnode_handle_put(init_data.fwnode); + + return ret; +} + +static const struct of_device_id of_lm36274_leds_match[] = { + { .compatible = "ti,lm36274-backlight", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_lm36274_leds_match); + +static struct platform_driver lm36274_driver = { + .probe = lm36274_probe, + .driver = { + .name = "lm36274-leds", + .of_match_table = of_lm36274_leds_match, + }, +}; +module_platform_driver(lm36274_driver) + +MODULE_DESCRIPTION("Texas Instruments LM36274 LED driver"); +MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-lm3642.c b/drivers/leds/leds-lm3642.c new file mode 100644 index 000000000..62c14872c --- /dev/null +++ b/drivers/leds/leds-lm3642.c @@ -0,0 +1,416 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* +* Simple driver for Texas Instruments LM3642 LED Flash driver chip +* Copyright (C) 2012 Texas Instruments +*/ +#include <linux/module.h> +#include <linux/delay.h> +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/fs.h> +#include <linux/regmap.h> +#include <linux/platform_data/leds-lm3642.h> + +#define REG_FILT_TIME (0x0) +#define REG_IVFM_MODE (0x1) +#define REG_TORCH_TIME (0x6) +#define REG_FLASH (0x8) +#define REG_I_CTRL (0x9) +#define REG_ENABLE (0xA) +#define REG_FLAG (0xB) +#define REG_MAX (0xB) + +#define UVLO_EN_SHIFT (7) +#define IVM_D_TH_SHIFT (2) +#define TORCH_RAMP_UP_TIME_SHIFT (3) +#define TORCH_RAMP_DN_TIME_SHIFT (0) +#define INDUCTOR_I_LIMIT_SHIFT (6) +#define FLASH_RAMP_TIME_SHIFT (3) +#define FLASH_TOUT_TIME_SHIFT (0) +#define TORCH_I_SHIFT (4) +#define FLASH_I_SHIFT (0) +#define IVFM_SHIFT (7) +#define TX_PIN_EN_SHIFT (6) +#define STROBE_PIN_EN_SHIFT (5) +#define TORCH_PIN_EN_SHIFT (4) +#define MODE_BITS_SHIFT (0) + +#define UVLO_EN_MASK (0x1) +#define IVM_D_TH_MASK (0x7) +#define TORCH_RAMP_UP_TIME_MASK (0x7) +#define TORCH_RAMP_DN_TIME_MASK (0x7) +#define INDUCTOR_I_LIMIT_MASK (0x1) +#define FLASH_RAMP_TIME_MASK (0x7) +#define FLASH_TOUT_TIME_MASK (0x7) +#define TORCH_I_MASK (0x7) +#define FLASH_I_MASK (0xF) +#define IVFM_MASK (0x1) +#define TX_PIN_EN_MASK (0x1) +#define STROBE_PIN_EN_MASK (0x1) +#define TORCH_PIN_EN_MASK (0x1) +#define MODE_BITS_MASK (0x73) +#define EX_PIN_CONTROL_MASK (0x71) +#define EX_PIN_ENABLE_MASK (0x70) + +enum lm3642_mode { + MODES_STASNDBY = 0, + MODES_INDIC, + MODES_TORCH, + MODES_FLASH +}; + +struct lm3642_chip_data { + struct device *dev; + + struct led_classdev cdev_flash; + struct led_classdev cdev_torch; + struct led_classdev cdev_indicator; + + u8 br_flash; + u8 br_torch; + u8 br_indicator; + + enum lm3642_torch_pin_enable torch_pin; + enum lm3642_strobe_pin_enable strobe_pin; + enum lm3642_tx_pin_enable tx_pin; + + struct lm3642_platform_data *pdata; + struct regmap *regmap; + struct mutex lock; + + unsigned int last_flag; +}; + +/* chip initialize */ +static int lm3642_chip_init(struct lm3642_chip_data *chip) +{ + int ret; + struct lm3642_platform_data *pdata = chip->pdata; + + /* set enable register */ + ret = regmap_update_bits(chip->regmap, REG_ENABLE, EX_PIN_ENABLE_MASK, + pdata->tx_pin); + if (ret < 0) + dev_err(chip->dev, "Failed to update REG_ENABLE Register\n"); + return ret; +} + +/* chip control */ +static int lm3642_control(struct lm3642_chip_data *chip, + u8 brightness, enum lm3642_mode opmode) +{ + int ret; + + ret = regmap_read(chip->regmap, REG_FLAG, &chip->last_flag); + if (ret < 0) { + dev_err(chip->dev, "Failed to read REG_FLAG Register\n"); + return ret; + } + + if (chip->last_flag) + dev_info(chip->dev, "Last FLAG is 0x%x\n", chip->last_flag); + + /* brightness 0 means off-state */ + if (!brightness) + opmode = MODES_STASNDBY; + + switch (opmode) { + case MODES_TORCH: + ret = regmap_update_bits(chip->regmap, REG_I_CTRL, + TORCH_I_MASK << TORCH_I_SHIFT, + (brightness - 1) << TORCH_I_SHIFT); + + if (chip->torch_pin) + opmode |= (TORCH_PIN_EN_MASK << TORCH_PIN_EN_SHIFT); + break; + + case MODES_FLASH: + ret = regmap_update_bits(chip->regmap, REG_I_CTRL, + FLASH_I_MASK << FLASH_I_SHIFT, + (brightness - 1) << FLASH_I_SHIFT); + + if (chip->strobe_pin) + opmode |= (STROBE_PIN_EN_MASK << STROBE_PIN_EN_SHIFT); + break; + + case MODES_INDIC: + ret = regmap_update_bits(chip->regmap, REG_I_CTRL, + TORCH_I_MASK << TORCH_I_SHIFT, + (brightness - 1) << TORCH_I_SHIFT); + break; + + case MODES_STASNDBY: + + break; + + default: + return -EINVAL; + } + if (ret < 0) { + dev_err(chip->dev, "Failed to write REG_I_CTRL Register\n"); + return ret; + } + + if (chip->tx_pin) + opmode |= (TX_PIN_EN_MASK << TX_PIN_EN_SHIFT); + + ret = regmap_update_bits(chip->regmap, REG_ENABLE, + MODE_BITS_MASK << MODE_BITS_SHIFT, + opmode << MODE_BITS_SHIFT); + return ret; +} + +/* torch */ + +/* torch pin config for lm3642 */ +static ssize_t lm3642_torch_pin_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + ssize_t ret; + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3642_chip_data *chip = + container_of(led_cdev, struct lm3642_chip_data, cdev_indicator); + unsigned int state; + + ret = kstrtouint(buf, 10, &state); + if (ret) + return ret; + if (state != 0) + state = 0x01 << TORCH_PIN_EN_SHIFT; + + chip->torch_pin = state; + ret = regmap_update_bits(chip->regmap, REG_ENABLE, + TORCH_PIN_EN_MASK << TORCH_PIN_EN_SHIFT, + state); + if (ret < 0) { + dev_err(chip->dev, "%s:i2c access fail to register\n", __func__); + return ret; + } + + return size; +} + +static DEVICE_ATTR(torch_pin, S_IWUSR, NULL, lm3642_torch_pin_store); + +static int lm3642_torch_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct lm3642_chip_data *chip = + container_of(cdev, struct lm3642_chip_data, cdev_torch); + int ret; + + mutex_lock(&chip->lock); + chip->br_torch = brightness; + ret = lm3642_control(chip, chip->br_torch, MODES_TORCH); + mutex_unlock(&chip->lock); + return ret; +} + +/* flash */ + +/* strobe pin config for lm3642*/ +static ssize_t lm3642_strobe_pin_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + ssize_t ret; + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct lm3642_chip_data *chip = + container_of(led_cdev, struct lm3642_chip_data, cdev_indicator); + unsigned int state; + + ret = kstrtouint(buf, 10, &state); + if (ret) + return ret; + if (state != 0) + state = 0x01 << STROBE_PIN_EN_SHIFT; + + chip->strobe_pin = state; + ret = regmap_update_bits(chip->regmap, REG_ENABLE, + STROBE_PIN_EN_MASK << STROBE_PIN_EN_SHIFT, + state); + if (ret < 0) { + dev_err(chip->dev, "%s:i2c access fail to register\n", __func__); + return ret; + } + + return size; +} + +static DEVICE_ATTR(strobe_pin, S_IWUSR, NULL, lm3642_strobe_pin_store); + +static int lm3642_strobe_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct lm3642_chip_data *chip = + container_of(cdev, struct lm3642_chip_data, cdev_flash); + int ret; + + mutex_lock(&chip->lock); + chip->br_flash = brightness; + ret = lm3642_control(chip, chip->br_flash, MODES_FLASH); + mutex_unlock(&chip->lock); + return ret; +} + +/* indicator */ +static int lm3642_indicator_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct lm3642_chip_data *chip = + container_of(cdev, struct lm3642_chip_data, cdev_indicator); + int ret; + + mutex_lock(&chip->lock); + chip->br_indicator = brightness; + ret = lm3642_control(chip, chip->br_indicator, MODES_INDIC); + mutex_unlock(&chip->lock); + return ret; +} + +static const struct regmap_config lm3642_regmap = { + .reg_bits = 8, + .val_bits = 8, + .max_register = REG_MAX, +}; + +static struct attribute *lm3642_flash_attrs[] = { + &dev_attr_strobe_pin.attr, + NULL +}; +ATTRIBUTE_GROUPS(lm3642_flash); + +static struct attribute *lm3642_torch_attrs[] = { + &dev_attr_torch_pin.attr, + NULL +}; +ATTRIBUTE_GROUPS(lm3642_torch); + +static int lm3642_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct lm3642_platform_data *pdata = dev_get_platdata(&client->dev); + struct lm3642_chip_data *chip; + + int err; + + if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) { + dev_err(&client->dev, "i2c functionality check fail.\n"); + return -EOPNOTSUPP; + } + + if (pdata == NULL) { + dev_err(&client->dev, "needs Platform Data.\n"); + return -ENODATA; + } + + chip = devm_kzalloc(&client->dev, + sizeof(struct lm3642_chip_data), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->dev = &client->dev; + chip->pdata = pdata; + + chip->tx_pin = pdata->tx_pin; + chip->torch_pin = pdata->torch_pin; + chip->strobe_pin = pdata->strobe_pin; + + chip->regmap = devm_regmap_init_i2c(client, &lm3642_regmap); + if (IS_ERR(chip->regmap)) { + err = PTR_ERR(chip->regmap); + dev_err(&client->dev, "Failed to allocate register map: %d\n", + err); + return err; + } + + mutex_init(&chip->lock); + i2c_set_clientdata(client, chip); + + err = lm3642_chip_init(chip); + if (err < 0) + goto err_out; + + /* flash */ + chip->cdev_flash.name = "flash"; + chip->cdev_flash.max_brightness = 16; + chip->cdev_flash.brightness_set_blocking = lm3642_strobe_brightness_set; + chip->cdev_flash.default_trigger = "flash"; + chip->cdev_flash.groups = lm3642_flash_groups, + err = led_classdev_register(&client->dev, &chip->cdev_flash); + if (err < 0) { + dev_err(chip->dev, "failed to register flash\n"); + goto err_out; + } + + /* torch */ + chip->cdev_torch.name = "torch"; + chip->cdev_torch.max_brightness = 8; + chip->cdev_torch.brightness_set_blocking = lm3642_torch_brightness_set; + chip->cdev_torch.default_trigger = "torch"; + chip->cdev_torch.groups = lm3642_torch_groups, + err = led_classdev_register(&client->dev, &chip->cdev_torch); + if (err < 0) { + dev_err(chip->dev, "failed to register torch\n"); + goto err_create_torch_file; + } + + /* indicator */ + chip->cdev_indicator.name = "indicator"; + chip->cdev_indicator.max_brightness = 8; + chip->cdev_indicator.brightness_set_blocking = + lm3642_indicator_brightness_set; + err = led_classdev_register(&client->dev, &chip->cdev_indicator); + if (err < 0) { + dev_err(chip->dev, "failed to register indicator\n"); + goto err_create_indicator_file; + } + + dev_info(&client->dev, "LM3642 is initialized\n"); + return 0; + +err_create_indicator_file: + led_classdev_unregister(&chip->cdev_torch); +err_create_torch_file: + led_classdev_unregister(&chip->cdev_flash); +err_out: + return err; +} + +static int lm3642_remove(struct i2c_client *client) +{ + struct lm3642_chip_data *chip = i2c_get_clientdata(client); + + led_classdev_unregister(&chip->cdev_indicator); + led_classdev_unregister(&chip->cdev_torch); + led_classdev_unregister(&chip->cdev_flash); + regmap_write(chip->regmap, REG_ENABLE, 0); + return 0; +} + +static const struct i2c_device_id lm3642_id[] = { + {LM3642_NAME, 0}, + {} +}; + +MODULE_DEVICE_TABLE(i2c, lm3642_id); + +static struct i2c_driver lm3642_i2c_driver = { + .driver = { + .name = LM3642_NAME, + .pm = NULL, + }, + .probe = lm3642_probe, + .remove = lm3642_remove, + .id_table = lm3642_id, +}; + +module_i2c_driver(lm3642_i2c_driver); + +MODULE_DESCRIPTION("Texas Instruments Flash Lighting driver for LM3642"); +MODULE_AUTHOR("Daniel Jeong <daniel.jeong@ti.com>"); +MODULE_AUTHOR("G.Shark Jeong <gshark.jeong@gmail.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-lm3692x.c b/drivers/leds/leds-lm3692x.c new file mode 100644 index 000000000..55e644399 --- /dev/null +++ b/drivers/leds/leds-lm3692x.c @@ -0,0 +1,535 @@ +// SPDX-License-Identifier: GPL-2.0 +// TI LM3692x LED chip family driver +// Copyright (C) 2017-18 Texas Instruments Incorporated - https://www.ti.com/ + +#include <linux/gpio/consumer.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/leds.h> +#include <linux/log2.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/of_gpio.h> +#include <linux/regmap.h> +#include <linux/regulator/consumer.h> +#include <linux/slab.h> + +#define LM36922_MODEL 0 +#define LM36923_MODEL 1 + +#define LM3692X_REV 0x0 +#define LM3692X_RESET 0x1 +#define LM3692X_EN 0x10 +#define LM3692X_BRT_CTRL 0x11 +#define LM3692X_PWM_CTRL 0x12 +#define LM3692X_BOOST_CTRL 0x13 +#define LM3692X_AUTO_FREQ_HI 0x15 +#define LM3692X_AUTO_FREQ_LO 0x16 +#define LM3692X_BL_ADJ_THRESH 0x17 +#define LM3692X_BRT_LSB 0x18 +#define LM3692X_BRT_MSB 0x19 +#define LM3692X_FAULT_CTRL 0x1e +#define LM3692X_FAULT_FLAGS 0x1f + +#define LM3692X_SW_RESET BIT(0) +#define LM3692X_DEVICE_EN BIT(0) +#define LM3692X_LED1_EN BIT(1) +#define LM3692X_LED2_EN BIT(2) +#define LM36923_LED3_EN BIT(3) +#define LM3692X_ENABLE_MASK (LM3692X_DEVICE_EN | LM3692X_LED1_EN | \ + LM3692X_LED2_EN | LM36923_LED3_EN) + +/* Brightness Control Bits */ +#define LM3692X_BL_ADJ_POL BIT(0) +#define LM3692X_RAMP_RATE_125us 0x00 +#define LM3692X_RAMP_RATE_250us BIT(1) +#define LM3692X_RAMP_RATE_500us BIT(2) +#define LM3692X_RAMP_RATE_1ms (BIT(1) | BIT(2)) +#define LM3692X_RAMP_RATE_2ms BIT(3) +#define LM3692X_RAMP_RATE_4ms (BIT(3) | BIT(1)) +#define LM3692X_RAMP_RATE_8ms (BIT(2) | BIT(3)) +#define LM3692X_RAMP_RATE_16ms (BIT(1) | BIT(2) | BIT(3)) +#define LM3692X_RAMP_EN BIT(4) +#define LM3692X_BRHT_MODE_REG 0x00 +#define LM3692X_BRHT_MODE_PWM BIT(5) +#define LM3692X_BRHT_MODE_MULTI_RAMP BIT(6) +#define LM3692X_BRHT_MODE_RAMP_MULTI (BIT(5) | BIT(6)) +#define LM3692X_MAP_MODE_EXP BIT(7) + +/* PWM Register Bits */ +#define LM3692X_PWM_FILTER_100 BIT(0) +#define LM3692X_PWM_FILTER_150 BIT(1) +#define LM3692X_PWM_FILTER_200 (BIT(0) | BIT(1)) +#define LM3692X_PWM_HYSTER_1LSB BIT(2) +#define LM3692X_PWM_HYSTER_2LSB BIT(3) +#define LM3692X_PWM_HYSTER_3LSB (BIT(3) | BIT(2)) +#define LM3692X_PWM_HYSTER_4LSB BIT(4) +#define LM3692X_PWM_HYSTER_5LSB (BIT(4) | BIT(2)) +#define LM3692X_PWM_HYSTER_6LSB (BIT(4) | BIT(3)) +#define LM3692X_PWM_POLARITY BIT(5) +#define LM3692X_PWM_SAMP_4MHZ BIT(6) +#define LM3692X_PWM_SAMP_24MHZ BIT(7) + +/* Boost Control Bits */ +#define LM3692X_OCP_PROT_1A BIT(0) +#define LM3692X_OCP_PROT_1_25A BIT(1) +#define LM3692X_OCP_PROT_1_5A (BIT(0) | BIT(1)) +#define LM3692X_OVP_21V BIT(2) +#define LM3692X_OVP_25V BIT(3) +#define LM3692X_OVP_29V (BIT(2) | BIT(3)) +#define LM3692X_MIN_IND_22UH BIT(4) +#define LM3692X_BOOST_SW_1MHZ BIT(5) +#define LM3692X_BOOST_SW_NO_SHIFT BIT(6) + +/* Fault Control Bits */ +#define LM3692X_FAULT_CTRL_OVP BIT(0) +#define LM3692X_FAULT_CTRL_OCP BIT(1) +#define LM3692X_FAULT_CTRL_TSD BIT(2) +#define LM3692X_FAULT_CTRL_OPEN BIT(3) + +/* Fault Flag Bits */ +#define LM3692X_FAULT_FLAG_OVP BIT(0) +#define LM3692X_FAULT_FLAG_OCP BIT(1) +#define LM3692X_FAULT_FLAG_TSD BIT(2) +#define LM3692X_FAULT_FLAG_SHRT BIT(3) +#define LM3692X_FAULT_FLAG_OPEN BIT(4) + +/** + * struct lm3692x_led - + * @lock - Lock for reading/writing the device + * @client - Pointer to the I2C client + * @led_dev - LED class device pointer + * @regmap - Devices register map + * @enable_gpio - VDDIO/EN gpio to enable communication interface + * @regulator - LED supply regulator pointer + * @led_enable - LED sync to be enabled + * @model_id - Current device model ID enumerated + */ +struct lm3692x_led { + struct mutex lock; + struct i2c_client *client; + struct led_classdev led_dev; + struct regmap *regmap; + struct gpio_desc *enable_gpio; + struct regulator *regulator; + int led_enable; + int model_id; + + u8 boost_ctrl, brightness_ctrl; + bool enabled; +}; + +static const struct reg_default lm3692x_reg_defs[] = { + {LM3692X_EN, 0xf}, + {LM3692X_BRT_CTRL, 0x61}, + {LM3692X_PWM_CTRL, 0x73}, + {LM3692X_BOOST_CTRL, 0x6f}, + {LM3692X_AUTO_FREQ_HI, 0x0}, + {LM3692X_AUTO_FREQ_LO, 0x0}, + {LM3692X_BL_ADJ_THRESH, 0x0}, + {LM3692X_BRT_LSB, 0x7}, + {LM3692X_BRT_MSB, 0xff}, + {LM3692X_FAULT_CTRL, 0x7}, +}; + +static const struct regmap_config lm3692x_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = LM3692X_FAULT_FLAGS, + .reg_defaults = lm3692x_reg_defs, + .num_reg_defaults = ARRAY_SIZE(lm3692x_reg_defs), + .cache_type = REGCACHE_RBTREE, +}; + +static int lm3692x_fault_check(struct lm3692x_led *led) +{ + int ret; + unsigned int read_buf; + + ret = regmap_read(led->regmap, LM3692X_FAULT_FLAGS, &read_buf); + if (ret) + return ret; + + if (read_buf) + dev_err(&led->client->dev, "Detected a fault 0x%X\n", read_buf); + + /* The first read may clear the fault. Check again to see if the fault + * still exits and return that value. + */ + regmap_read(led->regmap, LM3692X_FAULT_FLAGS, &read_buf); + if (read_buf) + dev_err(&led->client->dev, "Second read of fault flags 0x%X\n", + read_buf); + + return read_buf; +} + +static int lm3692x_leds_enable(struct lm3692x_led *led) +{ + int enable_state; + int ret, reg_ret; + + if (led->enabled) + return 0; + + if (led->regulator) { + ret = regulator_enable(led->regulator); + if (ret) { + dev_err(&led->client->dev, + "Failed to enable regulator: %d\n", ret); + return ret; + } + } + + if (led->enable_gpio) + gpiod_direction_output(led->enable_gpio, 1); + + ret = lm3692x_fault_check(led); + if (ret) { + dev_err(&led->client->dev, "Cannot read/clear faults: %d\n", + ret); + goto out; + } + + ret = regmap_write(led->regmap, LM3692X_BRT_CTRL, 0x00); + if (ret) + goto out; + + /* + * For glitch free operation, the following data should + * only be written while LEDx enable bits are 0 and the device enable + * bit is set to 1. + * per Section 7.5.14 of the data sheet + */ + ret = regmap_write(led->regmap, LM3692X_EN, LM3692X_DEVICE_EN); + if (ret) + goto out; + + /* Set the brightness to 0 so when enabled the LEDs do not come + * on with full brightness. + */ + ret = regmap_write(led->regmap, LM3692X_BRT_MSB, 0); + if (ret) + goto out; + + ret = regmap_write(led->regmap, LM3692X_BRT_LSB, 0); + if (ret) + goto out; + + ret = regmap_write(led->regmap, LM3692X_PWM_CTRL, + LM3692X_PWM_FILTER_100 | LM3692X_PWM_SAMP_24MHZ); + if (ret) + goto out; + + ret = regmap_write(led->regmap, LM3692X_BOOST_CTRL, led->boost_ctrl); + if (ret) + goto out; + + ret = regmap_write(led->regmap, LM3692X_AUTO_FREQ_HI, 0x00); + if (ret) + goto out; + + ret = regmap_write(led->regmap, LM3692X_AUTO_FREQ_LO, 0x00); + if (ret) + goto out; + + ret = regmap_write(led->regmap, LM3692X_BL_ADJ_THRESH, 0x00); + if (ret) + goto out; + + ret = regmap_write(led->regmap, LM3692X_BRT_CTRL, + LM3692X_BL_ADJ_POL | LM3692X_RAMP_EN); + if (ret) + goto out; + + switch (led->led_enable) { + case 0: + default: + if (led->model_id == LM36923_MODEL) + enable_state = LM3692X_LED1_EN | LM3692X_LED2_EN | + LM36923_LED3_EN; + else + enable_state = LM3692X_LED1_EN | LM3692X_LED2_EN; + + break; + case 1: + enable_state = LM3692X_LED1_EN; + break; + case 2: + enable_state = LM3692X_LED2_EN; + break; + + case 3: + if (led->model_id == LM36923_MODEL) { + enable_state = LM36923_LED3_EN; + break; + } + + ret = -EINVAL; + dev_err(&led->client->dev, + "LED3 sync not available on this device\n"); + goto out; + } + + ret = regmap_update_bits(led->regmap, LM3692X_EN, LM3692X_ENABLE_MASK, + enable_state | LM3692X_DEVICE_EN); + + led->enabled = true; + return ret; +out: + dev_err(&led->client->dev, "Fail writing initialization values\n"); + + if (led->enable_gpio) + gpiod_direction_output(led->enable_gpio, 0); + + if (led->regulator) { + reg_ret = regulator_disable(led->regulator); + if (reg_ret) + dev_err(&led->client->dev, + "Failed to disable regulator: %d\n", reg_ret); + } + + return ret; +} + +static int lm3692x_leds_disable(struct lm3692x_led *led) +{ + int ret; + + if (!led->enabled) + return 0; + + ret = regmap_update_bits(led->regmap, LM3692X_EN, LM3692X_DEVICE_EN, 0); + if (ret) { + dev_err(&led->client->dev, "Failed to disable regulator: %d\n", + ret); + return ret; + } + + if (led->enable_gpio) + gpiod_direction_output(led->enable_gpio, 0); + + if (led->regulator) { + ret = regulator_disable(led->regulator); + if (ret) + dev_err(&led->client->dev, + "Failed to disable regulator: %d\n", ret); + } + + led->enabled = false; + return ret; +} + +static int lm3692x_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brt_val) +{ + struct lm3692x_led *led = + container_of(led_cdev, struct lm3692x_led, led_dev); + int ret; + int led_brightness_lsb = (brt_val >> 5); + + mutex_lock(&led->lock); + + if (brt_val == 0) { + ret = lm3692x_leds_disable(led); + goto out; + } else { + lm3692x_leds_enable(led); + } + + ret = lm3692x_fault_check(led); + if (ret) { + dev_err(&led->client->dev, "Cannot read/clear faults: %d\n", + ret); + goto out; + } + + ret = regmap_write(led->regmap, LM3692X_BRT_MSB, brt_val); + if (ret) { + dev_err(&led->client->dev, "Cannot write MSB: %d\n", ret); + goto out; + } + + ret = regmap_write(led->regmap, LM3692X_BRT_LSB, led_brightness_lsb); + if (ret) { + dev_err(&led->client->dev, "Cannot write LSB: %d\n", ret); + goto out; + } +out: + mutex_unlock(&led->lock); + return ret; +} + +static enum led_brightness lm3692x_max_brightness(struct lm3692x_led *led, + u32 max_cur) +{ + u32 max_code; + + /* see p.12 of LM36922 data sheet for brightness formula */ + max_code = ((max_cur * 1000) - 37806) / 12195; + if (max_code > 0x7FF) + max_code = 0x7FF; + + return max_code >> 3; +} + +static int lm3692x_probe_dt(struct lm3692x_led *led) +{ + struct fwnode_handle *child = NULL; + struct led_init_data init_data = {}; + u32 ovp, max_cur; + int ret; + + led->enable_gpio = devm_gpiod_get_optional(&led->client->dev, + "enable", GPIOD_OUT_LOW); + if (IS_ERR(led->enable_gpio)) { + ret = PTR_ERR(led->enable_gpio); + dev_err(&led->client->dev, "Failed to get enable gpio: %d\n", + ret); + return ret; + } + + led->regulator = devm_regulator_get_optional(&led->client->dev, "vled"); + if (IS_ERR(led->regulator)) { + ret = PTR_ERR(led->regulator); + if (ret != -ENODEV) + return dev_err_probe(&led->client->dev, ret, + "Failed to get vled regulator\n"); + + led->regulator = NULL; + } + + led->boost_ctrl = LM3692X_BOOST_SW_1MHZ | + LM3692X_BOOST_SW_NO_SHIFT | + LM3692X_OCP_PROT_1_5A; + ret = device_property_read_u32(&led->client->dev, + "ti,ovp-microvolt", &ovp); + if (ret) { + led->boost_ctrl |= LM3692X_OVP_29V; + } else { + switch (ovp) { + case 17000000: + break; + case 21000000: + led->boost_ctrl |= LM3692X_OVP_21V; + break; + case 25000000: + led->boost_ctrl |= LM3692X_OVP_25V; + break; + case 29000000: + led->boost_ctrl |= LM3692X_OVP_29V; + break; + default: + dev_err(&led->client->dev, "Invalid OVP %d\n", ovp); + return -EINVAL; + } + } + + child = device_get_next_child_node(&led->client->dev, child); + if (!child) { + dev_err(&led->client->dev, "No LED Child node\n"); + return -ENODEV; + } + + ret = fwnode_property_read_u32(child, "reg", &led->led_enable); + if (ret) { + fwnode_handle_put(child); + dev_err(&led->client->dev, "reg DT property missing\n"); + return ret; + } + + ret = fwnode_property_read_u32(child, "led-max-microamp", &max_cur); + led->led_dev.max_brightness = ret ? LED_FULL : + lm3692x_max_brightness(led, max_cur); + + init_data.fwnode = child; + init_data.devicename = led->client->name; + init_data.default_label = ":"; + + ret = devm_led_classdev_register_ext(&led->client->dev, &led->led_dev, + &init_data); + if (ret) + dev_err(&led->client->dev, "led register err: %d\n", ret); + + fwnode_handle_put(init_data.fwnode); + return ret; +} + +static int lm3692x_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct lm3692x_led *led; + int ret; + + led = devm_kzalloc(&client->dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + mutex_init(&led->lock); + led->client = client; + led->led_dev.brightness_set_blocking = lm3692x_brightness_set; + led->model_id = id->driver_data; + i2c_set_clientdata(client, led); + + led->regmap = devm_regmap_init_i2c(client, &lm3692x_regmap_config); + if (IS_ERR(led->regmap)) { + ret = PTR_ERR(led->regmap); + dev_err(&client->dev, "Failed to allocate register map: %d\n", + ret); + return ret; + } + + ret = lm3692x_probe_dt(led); + if (ret) + return ret; + + ret = lm3692x_leds_enable(led); + if (ret) + return ret; + + return 0; +} + +static int lm3692x_remove(struct i2c_client *client) +{ + struct lm3692x_led *led = i2c_get_clientdata(client); + int ret; + + ret = lm3692x_leds_disable(led); + if (ret) + return ret; + mutex_destroy(&led->lock); + + return 0; +} + +static const struct i2c_device_id lm3692x_id[] = { + { "lm36922", LM36922_MODEL }, + { "lm36923", LM36923_MODEL }, + { } +}; +MODULE_DEVICE_TABLE(i2c, lm3692x_id); + +static const struct of_device_id of_lm3692x_leds_match[] = { + { .compatible = "ti,lm36922", }, + { .compatible = "ti,lm36923", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_lm3692x_leds_match); + +static struct i2c_driver lm3692x_driver = { + .driver = { + .name = "lm3692x", + .of_match_table = of_lm3692x_leds_match, + }, + .probe = lm3692x_probe, + .remove = lm3692x_remove, + .id_table = lm3692x_id, +}; +module_i2c_driver(lm3692x_driver); + +MODULE_DESCRIPTION("Texas Instruments LM3692X LED driver"); +MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-lm3697.c b/drivers/leds/leds-lm3697.c new file mode 100644 index 000000000..912e8bb22 --- /dev/null +++ b/drivers/leds/leds-lm3697.c @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: GPL-2.0 +// TI LM3697 LED chip family driver +// Copyright (C) 2018 Texas Instruments Incorporated - https://www.ti.com/ + +#include <linux/gpio/consumer.h> +#include <linux/i2c.h> +#include <linux/of.h> +#include <linux/of_gpio.h> +#include <linux/regulator/consumer.h> +#include <linux/leds-ti-lmu-common.h> + +#define LM3697_REV 0x0 +#define LM3697_RESET 0x1 +#define LM3697_OUTPUT_CONFIG 0x10 +#define LM3697_CTRL_A_RAMP 0x11 +#define LM3697_CTRL_B_RAMP 0x12 +#define LM3697_CTRL_A_B_RT_RAMP 0x13 +#define LM3697_CTRL_A_B_RAMP_CFG 0x14 +#define LM3697_CTRL_A_B_BRT_CFG 0x16 +#define LM3697_CTRL_A_FS_CURR_CFG 0x17 +#define LM3697_CTRL_B_FS_CURR_CFG 0x18 +#define LM3697_PWM_CFG 0x1c +#define LM3697_CTRL_A_BRT_LSB 0x20 +#define LM3697_CTRL_A_BRT_MSB 0x21 +#define LM3697_CTRL_B_BRT_LSB 0x22 +#define LM3697_CTRL_B_BRT_MSB 0x23 +#define LM3697_CTRL_ENABLE 0x24 + +#define LM3697_SW_RESET BIT(0) + +#define LM3697_CTRL_A_EN BIT(0) +#define LM3697_CTRL_B_EN BIT(1) +#define LM3697_CTRL_A_B_EN (LM3697_CTRL_A_EN | LM3697_CTRL_B_EN) + +#define LM3697_MAX_LED_STRINGS 3 + +#define LM3697_CONTROL_A 0 +#define LM3697_CONTROL_B 1 +#define LM3697_MAX_CONTROL_BANKS 2 + +/** + * struct lm3697_led - + * @hvled_strings: Array of LED strings associated with a control bank + * @label: LED label + * @led_dev: LED class device + * @priv: Pointer to the device struct + * @lmu_data: Register and setting values for common code + * @control_bank: Control bank the LED is associated to. 0 is control bank A + * 1 is control bank B + */ +struct lm3697_led { + u32 hvled_strings[LM3697_MAX_LED_STRINGS]; + char label[LED_MAX_NAME_SIZE]; + struct led_classdev led_dev; + struct lm3697 *priv; + struct ti_lmu_bank lmu_data; + int control_bank; + int enabled; + int num_leds; +}; + +/** + * struct lm3697 - + * @enable_gpio: Hardware enable gpio + * @regulator: LED supply regulator pointer + * @client: Pointer to the I2C client + * @regmap: Devices register map + * @dev: Pointer to the devices device struct + * @lock: Lock for reading/writing the device + * @leds: Array of LED strings + */ +struct lm3697 { + struct gpio_desc *enable_gpio; + struct regulator *regulator; + struct i2c_client *client; + struct regmap *regmap; + struct device *dev; + struct mutex lock; + + int bank_cfg; + int num_banks; + + struct lm3697_led leds[]; +}; + +static const struct reg_default lm3697_reg_defs[] = { + {LM3697_OUTPUT_CONFIG, 0x6}, + {LM3697_CTRL_A_RAMP, 0x0}, + {LM3697_CTRL_B_RAMP, 0x0}, + {LM3697_CTRL_A_B_RT_RAMP, 0x0}, + {LM3697_CTRL_A_B_RAMP_CFG, 0x0}, + {LM3697_CTRL_A_B_BRT_CFG, 0x0}, + {LM3697_CTRL_A_FS_CURR_CFG, 0x13}, + {LM3697_CTRL_B_FS_CURR_CFG, 0x13}, + {LM3697_PWM_CFG, 0xc}, + {LM3697_CTRL_A_BRT_LSB, 0x0}, + {LM3697_CTRL_A_BRT_MSB, 0x0}, + {LM3697_CTRL_B_BRT_LSB, 0x0}, + {LM3697_CTRL_B_BRT_MSB, 0x0}, + {LM3697_CTRL_ENABLE, 0x0}, +}; + +static const struct regmap_config lm3697_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = LM3697_CTRL_ENABLE, + .reg_defaults = lm3697_reg_defs, + .num_reg_defaults = ARRAY_SIZE(lm3697_reg_defs), + .cache_type = REGCACHE_FLAT, +}; + +static int lm3697_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brt_val) +{ + struct lm3697_led *led = container_of(led_cdev, struct lm3697_led, + led_dev); + int ctrl_en_val = (1 << led->control_bank); + struct device *dev = led->priv->dev; + int ret; + + mutex_lock(&led->priv->lock); + + if (brt_val == LED_OFF) { + ret = regmap_update_bits(led->priv->regmap, LM3697_CTRL_ENABLE, + ctrl_en_val, ~ctrl_en_val); + if (ret) { + dev_err(dev, "Cannot write ctrl register\n"); + goto brightness_out; + } + + led->enabled = LED_OFF; + } else { + ret = ti_lmu_common_set_brightness(&led->lmu_data, brt_val); + if (ret) { + dev_err(dev, "Cannot write brightness\n"); + goto brightness_out; + } + + if (!led->enabled) { + ret = regmap_update_bits(led->priv->regmap, + LM3697_CTRL_ENABLE, + ctrl_en_val, ctrl_en_val); + if (ret) { + dev_err(dev, "Cannot enable the device\n"); + goto brightness_out; + } + + led->enabled = brt_val; + } + } + +brightness_out: + mutex_unlock(&led->priv->lock); + return ret; +} + +static int lm3697_init(struct lm3697 *priv) +{ + struct device *dev = priv->dev; + struct lm3697_led *led; + int i, ret; + + if (priv->enable_gpio) { + gpiod_direction_output(priv->enable_gpio, 1); + } else { + ret = regmap_write(priv->regmap, LM3697_RESET, LM3697_SW_RESET); + if (ret) { + dev_err(dev, "Cannot reset the device\n"); + goto out; + } + } + + ret = regmap_write(priv->regmap, LM3697_CTRL_ENABLE, 0x0); + if (ret) { + dev_err(dev, "Cannot write ctrl enable\n"); + goto out; + } + + ret = regmap_write(priv->regmap, LM3697_OUTPUT_CONFIG, priv->bank_cfg); + if (ret) + dev_err(dev, "Cannot write OUTPUT config\n"); + + for (i = 0; i < priv->num_banks; i++) { + led = &priv->leds[i]; + ret = ti_lmu_common_set_ramp(&led->lmu_data); + if (ret) + dev_err(dev, "Setting the ramp rate failed\n"); + } +out: + return ret; +} + +static int lm3697_probe_dt(struct lm3697 *priv) +{ + struct fwnode_handle *child = NULL; + struct device *dev = priv->dev; + struct lm3697_led *led; + int ret = -EINVAL; + int control_bank; + size_t i = 0; + int j; + + priv->enable_gpio = devm_gpiod_get_optional(dev, "enable", + GPIOD_OUT_LOW); + if (IS_ERR(priv->enable_gpio)) + return dev_err_probe(dev, PTR_ERR(priv->enable_gpio), + "Failed to get enable GPIO\n"); + + priv->regulator = devm_regulator_get(dev, "vled"); + if (IS_ERR(priv->regulator)) + priv->regulator = NULL; + + device_for_each_child_node(dev, child) { + struct led_init_data init_data = {}; + + ret = fwnode_property_read_u32(child, "reg", &control_bank); + if (ret) { + dev_err(dev, "reg property missing\n"); + fwnode_handle_put(child); + goto child_out; + } + + if (control_bank > LM3697_CONTROL_B) { + dev_err(dev, "reg property is invalid\n"); + ret = -EINVAL; + fwnode_handle_put(child); + goto child_out; + } + + led = &priv->leds[i]; + + ret = ti_lmu_common_get_brt_res(dev, child, &led->lmu_data); + if (ret) + dev_warn(dev, + "brightness resolution property missing\n"); + + led->control_bank = control_bank; + led->lmu_data.regmap = priv->regmap; + led->lmu_data.runtime_ramp_reg = LM3697_CTRL_A_RAMP + + control_bank; + led->lmu_data.msb_brightness_reg = LM3697_CTRL_A_BRT_MSB + + led->control_bank * 2; + led->lmu_data.lsb_brightness_reg = LM3697_CTRL_A_BRT_LSB + + led->control_bank * 2; + + led->num_leds = fwnode_property_count_u32(child, "led-sources"); + if (led->num_leds > LM3697_MAX_LED_STRINGS) { + dev_err(dev, "Too many LED strings defined\n"); + continue; + } + + ret = fwnode_property_read_u32_array(child, "led-sources", + led->hvled_strings, + led->num_leds); + if (ret) { + dev_err(dev, "led-sources property missing\n"); + fwnode_handle_put(child); + goto child_out; + } + + for (j = 0; j < led->num_leds; j++) + priv->bank_cfg |= + (led->control_bank << led->hvled_strings[j]); + + ret = ti_lmu_common_get_ramp_params(dev, child, &led->lmu_data); + if (ret) + dev_warn(dev, "runtime-ramp properties missing\n"); + + init_data.fwnode = child; + init_data.devicename = priv->client->name; + /* for backwards compatibility if `label` is not present */ + init_data.default_label = ":"; + + led->priv = priv; + led->led_dev.max_brightness = led->lmu_data.max_brightness; + led->led_dev.brightness_set_blocking = lm3697_brightness_set; + + ret = devm_led_classdev_register_ext(dev, &led->led_dev, + &init_data); + if (ret) { + dev_err(dev, "led register err: %d\n", ret); + fwnode_handle_put(child); + goto child_out; + } + + i++; + } + +child_out: + return ret; +} + +static int lm3697_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct device *dev = &client->dev; + struct lm3697 *led; + int count; + int ret; + + count = device_get_child_node_count(dev); + if (!count || count > LM3697_MAX_CONTROL_BANKS) { + dev_err(dev, "Strange device tree!"); + return -ENODEV; + } + + led = devm_kzalloc(dev, struct_size(led, leds, count), GFP_KERNEL); + if (!led) + return -ENOMEM; + + mutex_init(&led->lock); + i2c_set_clientdata(client, led); + + led->client = client; + led->dev = dev; + led->num_banks = count; + led->regmap = devm_regmap_init_i2c(client, &lm3697_regmap_config); + if (IS_ERR(led->regmap)) { + ret = PTR_ERR(led->regmap); + dev_err(dev, "Failed to allocate register map: %d\n", ret); + return ret; + } + + ret = lm3697_probe_dt(led); + if (ret) + return ret; + + return lm3697_init(led); +} + +static int lm3697_remove(struct i2c_client *client) +{ + struct lm3697 *led = i2c_get_clientdata(client); + struct device *dev = &led->client->dev; + int ret; + + ret = regmap_update_bits(led->regmap, LM3697_CTRL_ENABLE, + LM3697_CTRL_A_B_EN, 0); + if (ret) { + dev_err(dev, "Failed to disable the device\n"); + return ret; + } + + if (led->enable_gpio) + gpiod_direction_output(led->enable_gpio, 0); + + if (led->regulator) { + ret = regulator_disable(led->regulator); + if (ret) + dev_err(dev, "Failed to disable regulator\n"); + } + + mutex_destroy(&led->lock); + + return 0; +} + +static const struct i2c_device_id lm3697_id[] = { + { "lm3697", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, lm3697_id); + +static const struct of_device_id of_lm3697_leds_match[] = { + { .compatible = "ti,lm3697", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_lm3697_leds_match); + +static struct i2c_driver lm3697_driver = { + .driver = { + .name = "lm3697", + .of_match_table = of_lm3697_leds_match, + }, + .probe = lm3697_probe, + .remove = lm3697_remove, + .id_table = lm3697_id, +}; +module_i2c_driver(lm3697_driver); + +MODULE_DESCRIPTION("Texas Instruments LM3697 LED driver"); +MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-locomo.c b/drivers/leds/leds-locomo.c new file mode 100644 index 000000000..42dc46e3f --- /dev/null +++ b/drivers/leds/leds-locomo.c @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * linux/drivers/leds/leds-locomo.c + * + * Copyright (C) 2005 John Lenz <lenz@cs.wisc.edu> + */ + +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/module.h> +#include <linux/device.h> +#include <linux/leds.h> + +#include <mach/hardware.h> +#include <asm/hardware/locomo.h> + +static void locomoled_brightness_set(struct led_classdev *led_cdev, + enum led_brightness value, int offset) +{ + struct locomo_dev *locomo_dev = LOCOMO_DEV(led_cdev->dev->parent); + unsigned long flags; + + local_irq_save(flags); + if (value) + locomo_writel(LOCOMO_LPT_TOFH, locomo_dev->mapbase + offset); + else + locomo_writel(LOCOMO_LPT_TOFL, locomo_dev->mapbase + offset); + local_irq_restore(flags); +} + +static void locomoled_brightness_set0(struct led_classdev *led_cdev, + enum led_brightness value) +{ + locomoled_brightness_set(led_cdev, value, LOCOMO_LPT0); +} + +static void locomoled_brightness_set1(struct led_classdev *led_cdev, + enum led_brightness value) +{ + locomoled_brightness_set(led_cdev, value, LOCOMO_LPT1); +} + +static struct led_classdev locomo_led0 = { + .name = "locomo:amber:charge", + .default_trigger = "main-battery-charging", + .brightness_set = locomoled_brightness_set0, +}; + +static struct led_classdev locomo_led1 = { + .name = "locomo:green:mail", + .default_trigger = "nand-disk", + .brightness_set = locomoled_brightness_set1, +}; + +static int locomoled_probe(struct locomo_dev *ldev) +{ + int ret; + + ret = devm_led_classdev_register(&ldev->dev, &locomo_led0); + if (ret < 0) + return ret; + + return devm_led_classdev_register(&ldev->dev, &locomo_led1); +} + + +static struct locomo_driver locomoled_driver = { + .drv = { + .name = "locomoled" + }, + .devid = LOCOMO_DEVID_LED, + .probe = locomoled_probe, +}; + +static int __init locomoled_init(void) +{ + return locomo_driver_register(&locomoled_driver); +} +module_init(locomoled_init); + +MODULE_AUTHOR("John Lenz <lenz@cs.wisc.edu>"); +MODULE_DESCRIPTION("Locomo LED driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-lp3944.c b/drivers/leds/leds-lp3944.c new file mode 100644 index 000000000..838e6f19d --- /dev/null +++ b/drivers/leds/leds-lp3944.c @@ -0,0 +1,442 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * leds-lp3944.c - driver for National Semiconductor LP3944 Funlight Chip + * + * Copyright (C) 2009 Antonio Ospite <ospite@studenti.unina.it> + */ + +/* + * I2C driver for National Semiconductor LP3944 Funlight Chip + * http://www.national.com/pf/LP/LP3944.html + * + * This helper chip can drive up to 8 leds, with two programmable DIM modes; + * it could even be used as a gpio expander but this driver assumes it is used + * as a led controller. + * + * The DIM modes are used to set _blink_ patterns for leds, the pattern is + * specified supplying two parameters: + * - period: from 0s to 1.6s + * - duty cycle: percentage of the period the led is on, from 0 to 100 + * + * LP3944 can be found on Motorola A910 smartphone, where it drives the rgb + * leds, the camera flash light and the displays backlights. + */ + +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/slab.h> +#include <linux/leds.h> +#include <linux/mutex.h> +#include <linux/leds-lp3944.h> + +/* Read Only Registers */ +#define LP3944_REG_INPUT1 0x00 /* LEDs 0-7 InputRegister (Read Only) */ +#define LP3944_REG_REGISTER1 0x01 /* None (Read Only) */ + +#define LP3944_REG_PSC0 0x02 /* Frequency Prescaler 0 (R/W) */ +#define LP3944_REG_PWM0 0x03 /* PWM Register 0 (R/W) */ +#define LP3944_REG_PSC1 0x04 /* Frequency Prescaler 1 (R/W) */ +#define LP3944_REG_PWM1 0x05 /* PWM Register 1 (R/W) */ +#define LP3944_REG_LS0 0x06 /* LEDs 0-3 Selector (R/W) */ +#define LP3944_REG_LS1 0x07 /* LEDs 4-7 Selector (R/W) */ + +/* These registers are not used to control leds in LP3944, they can store + * arbitrary values which the chip will ignore. + */ +#define LP3944_REG_REGISTER8 0x08 +#define LP3944_REG_REGISTER9 0x09 + +#define LP3944_DIM0 0 +#define LP3944_DIM1 1 + +/* period in ms */ +#define LP3944_PERIOD_MIN 0 +#define LP3944_PERIOD_MAX 1600 + +/* duty cycle is a percentage */ +#define LP3944_DUTY_CYCLE_MIN 0 +#define LP3944_DUTY_CYCLE_MAX 100 + +#define ldev_to_led(c) container_of(c, struct lp3944_led_data, ldev) + +/* Saved data */ +struct lp3944_led_data { + u8 id; + enum lp3944_type type; + struct led_classdev ldev; + struct i2c_client *client; +}; + +struct lp3944_data { + struct mutex lock; + struct i2c_client *client; + struct lp3944_led_data leds[LP3944_LEDS_MAX]; +}; + +static int lp3944_reg_read(struct i2c_client *client, u8 reg, u8 *value) +{ + int tmp; + + tmp = i2c_smbus_read_byte_data(client, reg); + if (tmp < 0) + return tmp; + + *value = tmp; + + return 0; +} + +static int lp3944_reg_write(struct i2c_client *client, u8 reg, u8 value) +{ + return i2c_smbus_write_byte_data(client, reg, value); +} + +/** + * Set the period for DIM status + * + * @client: the i2c client + * @dim: either LP3944_DIM0 or LP3944_DIM1 + * @period: period of a blink, that is a on/off cycle, expressed in ms. + */ +static int lp3944_dim_set_period(struct i2c_client *client, u8 dim, u16 period) +{ + u8 psc_reg; + u8 psc_value; + int err; + + if (dim == LP3944_DIM0) + psc_reg = LP3944_REG_PSC0; + else if (dim == LP3944_DIM1) + psc_reg = LP3944_REG_PSC1; + else + return -EINVAL; + + /* Convert period to Prescaler value */ + if (period > LP3944_PERIOD_MAX) + return -EINVAL; + + psc_value = (period * 255) / LP3944_PERIOD_MAX; + + err = lp3944_reg_write(client, psc_reg, psc_value); + + return err; +} + +/** + * Set the duty cycle for DIM status + * + * @client: the i2c client + * @dim: either LP3944_DIM0 or LP3944_DIM1 + * @duty_cycle: percentage of a period during which a led is ON + */ +static int lp3944_dim_set_dutycycle(struct i2c_client *client, u8 dim, + u8 duty_cycle) +{ + u8 pwm_reg; + u8 pwm_value; + int err; + + if (dim == LP3944_DIM0) + pwm_reg = LP3944_REG_PWM0; + else if (dim == LP3944_DIM1) + pwm_reg = LP3944_REG_PWM1; + else + return -EINVAL; + + /* Convert duty cycle to PWM value */ + if (duty_cycle > LP3944_DUTY_CYCLE_MAX) + return -EINVAL; + + pwm_value = (duty_cycle * 255) / LP3944_DUTY_CYCLE_MAX; + + err = lp3944_reg_write(client, pwm_reg, pwm_value); + + return err; +} + +/** + * Set the led status + * + * @led: a lp3944_led_data structure + * @status: one of LP3944_LED_STATUS_OFF + * LP3944_LED_STATUS_ON + * LP3944_LED_STATUS_DIM0 + * LP3944_LED_STATUS_DIM1 + */ +static int lp3944_led_set(struct lp3944_led_data *led, u8 status) +{ + struct lp3944_data *data = i2c_get_clientdata(led->client); + u8 id = led->id; + u8 reg; + u8 val = 0; + int err; + + dev_dbg(&led->client->dev, "%s: %s, status before normalization:%d\n", + __func__, led->ldev.name, status); + + switch (id) { + case LP3944_LED0: + case LP3944_LED1: + case LP3944_LED2: + case LP3944_LED3: + reg = LP3944_REG_LS0; + break; + case LP3944_LED4: + case LP3944_LED5: + case LP3944_LED6: + case LP3944_LED7: + id -= LP3944_LED4; + reg = LP3944_REG_LS1; + break; + default: + return -EINVAL; + } + + if (status > LP3944_LED_STATUS_DIM1) + return -EINVAL; + + /* + * Invert status only when it's < 2 (i.e. 0 or 1) which means it's + * controlling the on/off state directly. + * When, instead, status is >= 2 don't invert it because it would mean + * to mess with the hardware blinking mode. + */ + if (led->type == LP3944_LED_TYPE_LED_INVERTED && status < 2) + status = 1 - status; + + mutex_lock(&data->lock); + lp3944_reg_read(led->client, reg, &val); + + val &= ~(LP3944_LED_STATUS_MASK << (id << 1)); + val |= (status << (id << 1)); + + dev_dbg(&led->client->dev, "%s: %s, reg:%d id:%d status:%d val:%#x\n", + __func__, led->ldev.name, reg, id, status, val); + + /* set led status */ + err = lp3944_reg_write(led->client, reg, val); + mutex_unlock(&data->lock); + + return err; +} + +static int lp3944_led_set_blink(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct lp3944_led_data *led = ldev_to_led(led_cdev); + u16 period; + u8 duty_cycle; + int err; + + /* units are in ms */ + if (*delay_on + *delay_off > LP3944_PERIOD_MAX) + return -EINVAL; + + if (*delay_on == 0 && *delay_off == 0) { + /* Special case: the leds subsystem requires a default user + * friendly blink pattern for the LED. Let's blink the led + * slowly (1Hz). + */ + *delay_on = 500; + *delay_off = 500; + } + + period = (*delay_on) + (*delay_off); + + /* duty_cycle is the percentage of period during which the led is ON */ + duty_cycle = 100 * (*delay_on) / period; + + /* invert duty cycle for inverted leds, this has the same effect of + * swapping delay_on and delay_off + */ + if (led->type == LP3944_LED_TYPE_LED_INVERTED) + duty_cycle = 100 - duty_cycle; + + /* NOTE: using always the first DIM mode, this means that all leds + * will have the same blinking pattern. + * + * We could find a way later to have two leds blinking in hardware + * with different patterns at the same time, falling back to software + * control for the other ones. + */ + err = lp3944_dim_set_period(led->client, LP3944_DIM0, period); + if (err) + return err; + + err = lp3944_dim_set_dutycycle(led->client, LP3944_DIM0, duty_cycle); + if (err) + return err; + + dev_dbg(&led->client->dev, "%s: OK hardware accelerated blink!\n", + __func__); + + lp3944_led_set(led, LP3944_LED_STATUS_DIM0); + + return 0; +} + +static int lp3944_led_set_brightness(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct lp3944_led_data *led = ldev_to_led(led_cdev); + + dev_dbg(&led->client->dev, "%s: %s, %d\n", + __func__, led_cdev->name, brightness); + + return lp3944_led_set(led, !!brightness); +} + +static int lp3944_configure(struct i2c_client *client, + struct lp3944_data *data, + struct lp3944_platform_data *pdata) +{ + int i, err = 0; + + for (i = 0; i < pdata->leds_size; i++) { + struct lp3944_led *pled = &pdata->leds[i]; + struct lp3944_led_data *led = &data->leds[i]; + led->client = client; + led->id = i; + + switch (pled->type) { + + case LP3944_LED_TYPE_LED: + case LP3944_LED_TYPE_LED_INVERTED: + led->type = pled->type; + led->ldev.name = pled->name; + led->ldev.max_brightness = 1; + led->ldev.brightness_set_blocking = + lp3944_led_set_brightness; + led->ldev.blink_set = lp3944_led_set_blink; + led->ldev.flags = LED_CORE_SUSPENDRESUME; + + err = led_classdev_register(&client->dev, &led->ldev); + if (err < 0) { + dev_err(&client->dev, + "couldn't register LED %s\n", + led->ldev.name); + goto exit; + } + + /* to expose the default value to userspace */ + led->ldev.brightness = + (enum led_brightness) pled->status; + + /* Set the default led status */ + err = lp3944_led_set(led, pled->status); + if (err < 0) { + dev_err(&client->dev, + "%s couldn't set STATUS %d\n", + led->ldev.name, pled->status); + goto exit; + } + break; + + case LP3944_LED_TYPE_NONE: + default: + break; + + } + } + return 0; + +exit: + if (i > 0) + for (i = i - 1; i >= 0; i--) + switch (pdata->leds[i].type) { + + case LP3944_LED_TYPE_LED: + case LP3944_LED_TYPE_LED_INVERTED: + led_classdev_unregister(&data->leds[i].ldev); + break; + + case LP3944_LED_TYPE_NONE: + default: + break; + } + + return err; +} + +static int lp3944_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct lp3944_platform_data *lp3944_pdata = + dev_get_platdata(&client->dev); + struct lp3944_data *data; + int err; + + if (lp3944_pdata == NULL) { + dev_err(&client->dev, "no platform data\n"); + return -EINVAL; + } + + /* Let's see whether this adapter can support what we need. */ + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_BYTE_DATA)) { + dev_err(&client->dev, "insufficient functionality!\n"); + return -ENODEV; + } + + data = devm_kzalloc(&client->dev, sizeof(struct lp3944_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->client = client; + i2c_set_clientdata(client, data); + + mutex_init(&data->lock); + + err = lp3944_configure(client, data, lp3944_pdata); + if (err < 0) + return err; + + dev_info(&client->dev, "lp3944 enabled\n"); + return 0; +} + +static int lp3944_remove(struct i2c_client *client) +{ + struct lp3944_platform_data *pdata = dev_get_platdata(&client->dev); + struct lp3944_data *data = i2c_get_clientdata(client); + int i; + + for (i = 0; i < pdata->leds_size; i++) + switch (data->leds[i].type) { + case LP3944_LED_TYPE_LED: + case LP3944_LED_TYPE_LED_INVERTED: + led_classdev_unregister(&data->leds[i].ldev); + break; + + case LP3944_LED_TYPE_NONE: + default: + break; + } + + return 0; +} + +/* lp3944 i2c driver struct */ +static const struct i2c_device_id lp3944_id[] = { + {"lp3944", 0}, + {} +}; + +MODULE_DEVICE_TABLE(i2c, lp3944_id); + +static struct i2c_driver lp3944_driver = { + .driver = { + .name = "lp3944", + }, + .probe = lp3944_probe, + .remove = lp3944_remove, + .id_table = lp3944_id, +}; + +module_i2c_driver(lp3944_driver); + +MODULE_AUTHOR("Antonio Ospite <ospite@studenti.unina.it>"); +MODULE_DESCRIPTION("LP3944 Fun Light Chip"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-lp3952.c b/drivers/leds/leds-lp3952.c new file mode 100644 index 000000000..6ee9131fb --- /dev/null +++ b/drivers/leds/leds-lp3952.c @@ -0,0 +1,288 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED driver for TI lp3952 controller + * + * Copyright (C) 2016, DAQRI, LLC. + * Author: Tony Makkiel <tony.makkiel@daqri.com> + */ + +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/i2c.h> +#include <linux/io.h> +#include <linux/kernel.h> +#include <linux/leds.h> +#include <linux/leds-lp3952.h> +#include <linux/module.h> +#include <linux/notifier.h> +#include <linux/platform_device.h> +#include <linux/pm.h> +#include <linux/reboot.h> +#include <linux/regmap.h> + +static int lp3952_register_write(struct i2c_client *client, u8 reg, u8 val) +{ + int ret; + struct lp3952_led_array *priv = i2c_get_clientdata(client); + + ret = regmap_write(priv->regmap, reg, val); + + if (ret) + dev_err(&client->dev, "%s: reg 0x%x, val 0x%x, err %d\n", + __func__, reg, val, ret); + return ret; +} + +static void lp3952_on_off(struct lp3952_led_array *priv, + enum lp3952_leds led_id, bool on) +{ + int ret, val; + + dev_dbg(&priv->client->dev, "%s LED %d to %d\n", __func__, led_id, on); + + val = 1 << led_id; + if (led_id == LP3952_LED_ALL) + val = LP3952_LED_MASK_ALL; + + ret = regmap_update_bits(priv->regmap, LP3952_REG_LED_CTRL, val, + on ? val : 0); + if (ret) + dev_err(&priv->client->dev, "%s, Error %d\n", __func__, ret); +} + +/* + * Using Imax to control brightness. There are 4 possible + * setting 25, 50, 75 and 100 % of Imax. Possible values are + * values 0-4. 0 meaning turn off. + */ +static int lp3952_set_brightness(struct led_classdev *cdev, + enum led_brightness value) +{ + unsigned int reg, shift_val; + struct lp3952_ctrl_hdl *led = container_of(cdev, + struct lp3952_ctrl_hdl, + cdev); + struct lp3952_led_array *priv = (struct lp3952_led_array *)led->priv; + + dev_dbg(cdev->dev, "Brightness request: %d on %d\n", value, + led->channel); + + if (value == LED_OFF) { + lp3952_on_off(priv, led->channel, false); + return 0; + } + + if (led->channel > LP3952_RED_1) { + dev_err(cdev->dev, " %s Invalid LED requested", __func__); + return -EINVAL; + } + + if (led->channel >= LP3952_BLUE_1) { + reg = LP3952_REG_RGB1_MAX_I_CTRL; + shift_val = (led->channel - LP3952_BLUE_1) * 2; + } else { + reg = LP3952_REG_RGB2_MAX_I_CTRL; + shift_val = led->channel * 2; + } + + /* Enable the LED in case it is not enabled already */ + lp3952_on_off(priv, led->channel, true); + + return regmap_update_bits(priv->regmap, reg, 3 << shift_val, + --value << shift_val); +} + +static int lp3952_get_label(struct device *dev, const char *label, char *dest) +{ + int ret; + const char *str; + + ret = device_property_read_string(dev, label, &str); + if (ret) + return ret; + + strncpy(dest, str, LP3952_LABEL_MAX_LEN); + return 0; +} + +static int lp3952_register_led_classdev(struct lp3952_led_array *priv) +{ + int i, acpi_ret, ret = -ENODEV; + static const char *led_name_hdl[LP3952_LED_ALL] = { + "blue2", + "green2", + "red2", + "blue1", + "green1", + "red1" + }; + + for (i = 0; i < LP3952_LED_ALL; i++) { + acpi_ret = lp3952_get_label(&priv->client->dev, led_name_hdl[i], + priv->leds[i].name); + if (acpi_ret) + continue; + + priv->leds[i].cdev.name = priv->leds[i].name; + priv->leds[i].cdev.brightness = LED_OFF; + priv->leds[i].cdev.max_brightness = LP3952_BRIGHT_MAX; + priv->leds[i].cdev.brightness_set_blocking = + lp3952_set_brightness; + priv->leds[i].channel = i; + priv->leds[i].priv = priv; + + ret = devm_led_classdev_register(&priv->client->dev, + &priv->leds[i].cdev); + if (ret < 0) { + dev_err(&priv->client->dev, + "couldn't register LED %s\n", + priv->leds[i].cdev.name); + break; + } + } + return ret; +} + +static int lp3952_set_pattern_gen_cmd(struct lp3952_led_array *priv, + u8 cmd_index, u8 r, u8 g, u8 b, + enum lp3952_tt tt, enum lp3952_cet cet) +{ + int ret; + struct ptrn_gen_cmd line = { + { + { + .r = r, + .g = g, + .b = b, + .cet = cet, + .tt = tt + } + } + }; + + if (cmd_index >= LP3952_CMD_REG_COUNT) + return -EINVAL; + + ret = lp3952_register_write(priv->client, + LP3952_REG_CMD_0 + cmd_index * 2, + line.bytes.msb); + if (ret) + return ret; + + return lp3952_register_write(priv->client, + LP3952_REG_CMD_0 + cmd_index * 2 + 1, + line.bytes.lsb); +} + +static int lp3952_configure(struct lp3952_led_array *priv) +{ + int ret; + + /* Disable any LEDs on from any previous conf. */ + ret = lp3952_register_write(priv->client, LP3952_REG_LED_CTRL, 0); + if (ret) + return ret; + + /* enable rgb patter, loop */ + ret = lp3952_register_write(priv->client, LP3952_REG_PAT_GEN_CTRL, + LP3952_PATRN_LOOP | LP3952_PATRN_GEN_EN); + if (ret) + return ret; + + /* Update Bit 6 (Active mode), Select both Led sets, Bit [1:0] */ + ret = lp3952_register_write(priv->client, LP3952_REG_ENABLES, + LP3952_ACTIVE_MODE | LP3952_INT_B00ST_LDR); + if (ret) + return ret; + + /* Set Cmd1 for RGB intensity,cmd and transition time */ + return lp3952_set_pattern_gen_cmd(priv, 0, I46, I71, I100, TT0, + CET197); +} + +static const struct regmap_config lp3952_regmap = { + .reg_bits = 8, + .val_bits = 8, + .max_register = REG_MAX, + .cache_type = REGCACHE_RBTREE, +}; + +static int lp3952_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + int status; + struct lp3952_led_array *priv; + + priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->client = client; + + priv->enable_gpio = devm_gpiod_get(&client->dev, "nrst", + GPIOD_OUT_HIGH); + if (IS_ERR(priv->enable_gpio)) { + status = PTR_ERR(priv->enable_gpio); + dev_err(&client->dev, "Failed to enable gpio: %d\n", status); + return status; + } + + priv->regmap = devm_regmap_init_i2c(client, &lp3952_regmap); + if (IS_ERR(priv->regmap)) { + int err = PTR_ERR(priv->regmap); + + dev_err(&client->dev, "Failed to allocate register map: %d\n", + err); + return err; + } + + i2c_set_clientdata(client, priv); + + status = lp3952_configure(priv); + if (status) { + dev_err(&client->dev, "Probe failed. Device not found (%d)\n", + status); + return status; + } + + status = lp3952_register_led_classdev(priv); + if (status) { + dev_err(&client->dev, "Unable to register led_classdev: %d\n", + status); + return status; + } + + return 0; +} + +static int lp3952_remove(struct i2c_client *client) +{ + struct lp3952_led_array *priv; + + priv = i2c_get_clientdata(client); + lp3952_on_off(priv, LP3952_LED_ALL, false); + gpiod_set_value(priv->enable_gpio, 0); + + return 0; +} + +static const struct i2c_device_id lp3952_id[] = { + {LP3952_NAME, 0}, + {} +}; +MODULE_DEVICE_TABLE(i2c, lp3952_id); + +static struct i2c_driver lp3952_i2c_driver = { + .driver = { + .name = LP3952_NAME, + }, + .probe = lp3952_probe, + .remove = lp3952_remove, + .id_table = lp3952_id, +}; + +module_i2c_driver(lp3952_i2c_driver); + +MODULE_AUTHOR("Tony Makkiel <tony.makkiel@daqri.com>"); +MODULE_DESCRIPTION("lp3952 I2C LED controller driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-lp50xx.c b/drivers/leds/leds-lp50xx.c new file mode 100644 index 000000000..d45290829 --- /dev/null +++ b/drivers/leds/leds-lp50xx.c @@ -0,0 +1,633 @@ +// SPDX-License-Identifier: GPL-2.0 +// TI LP50XX LED chip family driver +// Copyright (C) 2018-20 Texas Instruments Incorporated - https://www.ti.com/ + +#include <linux/gpio/consumer.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/of_gpio.h> +#include <linux/regmap.h> +#include <linux/regulator/consumer.h> +#include <linux/slab.h> +#include <uapi/linux/uleds.h> + +#include <linux/led-class-multicolor.h> + +#include "leds.h" + +#define LP50XX_DEV_CFG0 0x00 +#define LP50XX_DEV_CFG1 0x01 +#define LP50XX_LED_CFG0 0x02 + +/* LP5009 and LP5012 registers */ +#define LP5012_BNK_BRT 0x03 +#define LP5012_BNKA_CLR 0x04 +#define LP5012_BNKB_CLR 0x05 +#define LP5012_BNKC_CLR 0x06 +#define LP5012_LED0_BRT 0x07 +#define LP5012_OUT0_CLR 0x0b +#define LP5012_RESET 0x17 + +/* LP5018 and LP5024 registers */ +#define LP5024_BNK_BRT 0x03 +#define LP5024_BNKA_CLR 0x04 +#define LP5024_BNKB_CLR 0x05 +#define LP5024_BNKC_CLR 0x06 +#define LP5024_LED0_BRT 0x07 +#define LP5024_OUT0_CLR 0x0f +#define LP5024_RESET 0x27 + +/* LP5030 and LP5036 registers */ +#define LP5036_LED_CFG1 0x03 +#define LP5036_BNK_BRT 0x04 +#define LP5036_BNKA_CLR 0x05 +#define LP5036_BNKB_CLR 0x06 +#define LP5036_BNKC_CLR 0x07 +#define LP5036_LED0_BRT 0x08 +#define LP5036_OUT0_CLR 0x14 +#define LP5036_RESET 0x38 + +#define LP50XX_SW_RESET 0xff +#define LP50XX_CHIP_EN BIT(6) + +/* There are 3 LED outputs per bank */ +#define LP50XX_LEDS_PER_MODULE 3 + +#define LP5009_MAX_LED_MODULES 2 +#define LP5012_MAX_LED_MODULES 4 +#define LP5018_MAX_LED_MODULES 6 +#define LP5024_MAX_LED_MODULES 8 +#define LP5030_MAX_LED_MODULES 10 +#define LP5036_MAX_LED_MODULES 12 + +static const struct reg_default lp5012_reg_defs[] = { + {LP50XX_DEV_CFG0, 0x0}, + {LP50XX_DEV_CFG1, 0x3c}, + {LP50XX_LED_CFG0, 0x0}, + {LP5012_BNK_BRT, 0xff}, + {LP5012_BNKA_CLR, 0x0f}, + {LP5012_BNKB_CLR, 0x0f}, + {LP5012_BNKC_CLR, 0x0f}, + {LP5012_LED0_BRT, 0x0f}, + /* LEDX_BRT registers are all 0xff for defaults */ + {0x08, 0xff}, {0x09, 0xff}, {0x0a, 0xff}, + {LP5012_OUT0_CLR, 0x0f}, + /* OUTX_CLR registers are all 0x0 for defaults */ + {0x0c, 0x00}, {0x0d, 0x00}, {0x0e, 0x00}, {0x0f, 0x00}, {0x10, 0x00}, + {0x11, 0x00}, {0x12, 0x00}, {0x13, 0x00}, {0x14, 0x00}, {0x15, 0x00}, + {0x16, 0x00}, + {LP5012_RESET, 0x00} +}; + +static const struct reg_default lp5024_reg_defs[] = { + {LP50XX_DEV_CFG0, 0x0}, + {LP50XX_DEV_CFG1, 0x3c}, + {LP50XX_LED_CFG0, 0x0}, + {LP5024_BNK_BRT, 0xff}, + {LP5024_BNKA_CLR, 0x0f}, + {LP5024_BNKB_CLR, 0x0f}, + {LP5024_BNKC_CLR, 0x0f}, + {LP5024_LED0_BRT, 0x0f}, + /* LEDX_BRT registers are all 0xff for defaults */ + {0x08, 0xff}, {0x09, 0xff}, {0x0a, 0xff}, {0x0b, 0xff}, {0x0c, 0xff}, + {0x0d, 0xff}, {0x0e, 0xff}, + {LP5024_OUT0_CLR, 0x0f}, + /* OUTX_CLR registers are all 0x0 for defaults */ + {0x10, 0x00}, {0x11, 0x00}, {0x12, 0x00}, {0x13, 0x00}, {0x14, 0x00}, + {0x15, 0x00}, {0x16, 0x00}, {0x17, 0x00}, {0x18, 0x00}, {0x19, 0x00}, + {0x1a, 0x00}, {0x1b, 0x00}, {0x1c, 0x00}, {0x1d, 0x00}, {0x1e, 0x00}, + {0x1f, 0x00}, {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00}, {0x23, 0x00}, + {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, + {LP5024_RESET, 0x00} +}; + +static const struct reg_default lp5036_reg_defs[] = { + {LP50XX_DEV_CFG0, 0x0}, + {LP50XX_DEV_CFG1, 0x3c}, + {LP50XX_LED_CFG0, 0x0}, + {LP5036_LED_CFG1, 0x0}, + {LP5036_BNK_BRT, 0xff}, + {LP5036_BNKA_CLR, 0x0f}, + {LP5036_BNKB_CLR, 0x0f}, + {LP5036_BNKC_CLR, 0x0f}, + {LP5036_LED0_BRT, 0x0f}, + /* LEDX_BRT registers are all 0xff for defaults */ + {0x08, 0xff}, {0x09, 0xff}, {0x0a, 0xff}, {0x0b, 0xff}, {0x0c, 0xff}, + {0x0d, 0xff}, {0x0e, 0xff}, {0x0f, 0xff}, {0x10, 0xff}, {0x11, 0xff}, + {0x12, 0xff}, {0x13, 0xff}, + {LP5036_OUT0_CLR, 0x0f}, + /* OUTX_CLR registers are all 0x0 for defaults */ + {0x15, 0x00}, {0x16, 0x00}, {0x17, 0x00}, {0x18, 0x00}, {0x19, 0x00}, + {0x1a, 0x00}, {0x1b, 0x00}, {0x1c, 0x00}, {0x1d, 0x00}, {0x1e, 0x00}, + {0x1f, 0x00}, {0x20, 0x00}, {0x21, 0x00}, {0x22, 0x00}, {0x23, 0x00}, + {0x24, 0x00}, {0x25, 0x00}, {0x26, 0x00}, {0x27, 0x00}, {0x28, 0x00}, + {0x29, 0x00}, {0x2a, 0x00}, {0x2b, 0x00}, {0x2c, 0x00}, {0x2d, 0x00}, + {0x2e, 0x00}, {0x2f, 0x00}, {0x30, 0x00}, {0x31, 0x00}, {0x32, 0x00}, + {0x33, 0x00}, {0x34, 0x00}, {0x35, 0x00}, {0x36, 0x00}, {0x37, 0x00}, + {LP5036_RESET, 0x00} +}; + +static const struct regmap_config lp5012_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = LP5012_RESET, + .reg_defaults = lp5012_reg_defs, + .num_reg_defaults = ARRAY_SIZE(lp5012_reg_defs), + .cache_type = REGCACHE_FLAT, +}; + +static const struct regmap_config lp5024_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = LP5024_RESET, + .reg_defaults = lp5024_reg_defs, + .num_reg_defaults = ARRAY_SIZE(lp5024_reg_defs), + .cache_type = REGCACHE_FLAT, +}; + +static const struct regmap_config lp5036_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = LP5036_RESET, + .reg_defaults = lp5036_reg_defs, + .num_reg_defaults = ARRAY_SIZE(lp5036_reg_defs), + .cache_type = REGCACHE_FLAT, +}; + +enum lp50xx_model { + LP5009, + LP5012, + LP5018, + LP5024, + LP5030, + LP5036, +}; + +/** + * struct lp50xx_chip_info - + * @lp50xx_regmap_config: regmap register configuration + * @model_id: LED device model + * @max_modules: total number of supported LED modules + * @num_leds: number of LED outputs available on the device + * @led_brightness0_reg: first brightness register of the device + * @mix_out0_reg: first color mix register of the device + * @bank_brt_reg: bank brightness register + * @bank_mix_reg: color mix register + * @reset_reg: device reset register + */ +struct lp50xx_chip_info { + const struct regmap_config *lp50xx_regmap_config; + int model_id; + u8 max_modules; + u8 num_leds; + u8 led_brightness0_reg; + u8 mix_out0_reg; + u8 bank_brt_reg; + u8 bank_mix_reg; + u8 reset_reg; +}; + +static const struct lp50xx_chip_info lp50xx_chip_info_tbl[] = { + [LP5009] = { + .model_id = LP5009, + .max_modules = LP5009_MAX_LED_MODULES, + .num_leds = LP5009_MAX_LED_MODULES * LP50XX_LEDS_PER_MODULE, + .led_brightness0_reg = LP5012_LED0_BRT, + .mix_out0_reg = LP5012_OUT0_CLR, + .bank_brt_reg = LP5012_BNK_BRT, + .bank_mix_reg = LP5012_BNKA_CLR, + .reset_reg = LP5012_RESET, + .lp50xx_regmap_config = &lp5012_regmap_config, + }, + [LP5012] = { + .model_id = LP5012, + .max_modules = LP5012_MAX_LED_MODULES, + .num_leds = LP5012_MAX_LED_MODULES * LP50XX_LEDS_PER_MODULE, + .led_brightness0_reg = LP5012_LED0_BRT, + .mix_out0_reg = LP5012_OUT0_CLR, + .bank_brt_reg = LP5012_BNK_BRT, + .bank_mix_reg = LP5012_BNKA_CLR, + .reset_reg = LP5012_RESET, + .lp50xx_regmap_config = &lp5012_regmap_config, + }, + [LP5018] = { + .model_id = LP5018, + .max_modules = LP5018_MAX_LED_MODULES, + .num_leds = LP5018_MAX_LED_MODULES * LP50XX_LEDS_PER_MODULE, + .led_brightness0_reg = LP5024_LED0_BRT, + .mix_out0_reg = LP5024_OUT0_CLR, + .bank_brt_reg = LP5024_BNK_BRT, + .bank_mix_reg = LP5024_BNKA_CLR, + .reset_reg = LP5024_RESET, + .lp50xx_regmap_config = &lp5024_regmap_config, + }, + [LP5024] = { + .model_id = LP5024, + .max_modules = LP5024_MAX_LED_MODULES, + .num_leds = LP5024_MAX_LED_MODULES * LP50XX_LEDS_PER_MODULE, + .led_brightness0_reg = LP5024_LED0_BRT, + .mix_out0_reg = LP5024_OUT0_CLR, + .bank_brt_reg = LP5024_BNK_BRT, + .bank_mix_reg = LP5024_BNKA_CLR, + .reset_reg = LP5024_RESET, + .lp50xx_regmap_config = &lp5024_regmap_config, + }, + [LP5030] = { + .model_id = LP5030, + .max_modules = LP5030_MAX_LED_MODULES, + .num_leds = LP5030_MAX_LED_MODULES * LP50XX_LEDS_PER_MODULE, + .led_brightness0_reg = LP5036_LED0_BRT, + .mix_out0_reg = LP5036_OUT0_CLR, + .bank_brt_reg = LP5036_BNK_BRT, + .bank_mix_reg = LP5036_BNKA_CLR, + .reset_reg = LP5036_RESET, + .lp50xx_regmap_config = &lp5036_regmap_config, + }, + [LP5036] = { + .model_id = LP5036, + .max_modules = LP5036_MAX_LED_MODULES, + .num_leds = LP5036_MAX_LED_MODULES * LP50XX_LEDS_PER_MODULE, + .led_brightness0_reg = LP5036_LED0_BRT, + .mix_out0_reg = LP5036_OUT0_CLR, + .bank_brt_reg = LP5036_BNK_BRT, + .bank_mix_reg = LP5036_BNKA_CLR, + .reset_reg = LP5036_RESET, + .lp50xx_regmap_config = &lp5036_regmap_config, + }, +}; + +struct lp50xx_led { + struct led_classdev_mc mc_cdev; + struct lp50xx *priv; + unsigned long bank_modules; + int led_intensity[LP50XX_LEDS_PER_MODULE]; + u8 ctrl_bank_enabled; + int led_number; +}; + +/** + * struct lp50xx - + * @enable_gpio: hardware enable gpio + * @regulator: LED supply regulator pointer + * @client: pointer to the I2C client + * @regmap: device register map + * @dev: pointer to the devices device struct + * @lock: lock for reading/writing the device + * @chip_info: chip specific information (ie num_leds) + * @num_of_banked_leds: holds the number of banked LEDs + * @leds: array of LED strings + */ +struct lp50xx { + struct gpio_desc *enable_gpio; + struct regulator *regulator; + struct i2c_client *client; + struct regmap *regmap; + struct device *dev; + struct mutex lock; + const struct lp50xx_chip_info *chip_info; + int num_of_banked_leds; + + /* This needs to be at the end of the struct */ + struct lp50xx_led leds[]; +}; + +static struct lp50xx_led *mcled_cdev_to_led(struct led_classdev_mc *mc_cdev) +{ + return container_of(mc_cdev, struct lp50xx_led, mc_cdev); +} + +static int lp50xx_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct led_classdev_mc *mc_dev = lcdev_to_mccdev(cdev); + struct lp50xx_led *led = mcled_cdev_to_led(mc_dev); + const struct lp50xx_chip_info *led_chip = led->priv->chip_info; + u8 led_offset, reg_val; + int ret = 0; + int i; + + mutex_lock(&led->priv->lock); + if (led->ctrl_bank_enabled) + reg_val = led_chip->bank_brt_reg; + else + reg_val = led_chip->led_brightness0_reg + + led->led_number; + + ret = regmap_write(led->priv->regmap, reg_val, brightness); + if (ret) { + dev_err(&led->priv->client->dev, + "Cannot write brightness value %d\n", ret); + goto out; + } + + for (i = 0; i < led->mc_cdev.num_colors; i++) { + if (led->ctrl_bank_enabled) { + reg_val = led_chip->bank_mix_reg + i; + } else { + led_offset = (led->led_number * 3) + i; + reg_val = led_chip->mix_out0_reg + led_offset; + } + + ret = regmap_write(led->priv->regmap, reg_val, + mc_dev->subled_info[i].intensity); + if (ret) { + dev_err(&led->priv->client->dev, + "Cannot write intensity value %d\n", ret); + goto out; + } + } +out: + mutex_unlock(&led->priv->lock); + return ret; +} + +static int lp50xx_set_banks(struct lp50xx *priv, u32 led_banks[]) +{ + u8 led_config_lo, led_config_hi; + u32 bank_enable_mask = 0; + int ret; + int i; + + for (i = 0; i < priv->chip_info->max_modules; i++) { + if (led_banks[i]) + bank_enable_mask |= (1 << led_banks[i]); + } + + led_config_lo = (u8)(bank_enable_mask & 0xff); + led_config_hi = (u8)(bank_enable_mask >> 8) & 0xff; + + ret = regmap_write(priv->regmap, LP50XX_LED_CFG0, led_config_lo); + if (ret) + return ret; + + if (priv->chip_info->model_id >= LP5030) + ret = regmap_write(priv->regmap, LP5036_LED_CFG1, led_config_hi); + + return ret; +} + +static int lp50xx_reset(struct lp50xx *priv) +{ + return regmap_write(priv->regmap, priv->chip_info->reset_reg, LP50XX_SW_RESET); +} + +static int lp50xx_enable_disable(struct lp50xx *priv, int enable_disable) +{ + int ret; + + if (priv->enable_gpio) { + ret = gpiod_direction_output(priv->enable_gpio, enable_disable); + if (ret) + return ret; + } + + if (enable_disable) + return regmap_write(priv->regmap, LP50XX_DEV_CFG0, LP50XX_CHIP_EN); + else + return regmap_write(priv->regmap, LP50XX_DEV_CFG0, 0); + +} + +static int lp50xx_probe_leds(struct fwnode_handle *child, struct lp50xx *priv, + struct lp50xx_led *led, int num_leds) +{ + u32 led_banks[LP5036_MAX_LED_MODULES] = {0}; + int led_number; + int ret; + + if (num_leds > 1) { + if (num_leds > priv->chip_info->max_modules) { + dev_err(&priv->client->dev, "reg property is invalid\n"); + return -EINVAL; + } + + priv->num_of_banked_leds = num_leds; + + ret = fwnode_property_read_u32_array(child, "reg", led_banks, num_leds); + if (ret) { + dev_err(&priv->client->dev, "reg property is missing\n"); + return ret; + } + + ret = lp50xx_set_banks(priv, led_banks); + if (ret) { + dev_err(&priv->client->dev, "Cannot setup banked LEDs\n"); + return ret; + } + + led->ctrl_bank_enabled = 1; + } else { + ret = fwnode_property_read_u32(child, "reg", &led_number); + if (ret) { + dev_err(&priv->client->dev, "led reg property missing\n"); + return ret; + } + + if (led_number > priv->chip_info->num_leds) { + dev_err(&priv->client->dev, "led-sources property is invalid\n"); + return -EINVAL; + } + + led->led_number = led_number; + } + + return 0; +} + +static int lp50xx_probe_dt(struct lp50xx *priv) +{ + struct fwnode_handle *child = NULL; + struct fwnode_handle *led_node = NULL; + struct led_init_data init_data = {}; + struct led_classdev *led_cdev; + struct mc_subled *mc_led_info; + struct lp50xx_led *led; + int ret = -EINVAL; + int num_colors; + u32 color_id; + int i = 0; + + priv->enable_gpio = devm_gpiod_get_optional(priv->dev, "enable", GPIOD_OUT_LOW); + if (IS_ERR(priv->enable_gpio)) { + ret = PTR_ERR(priv->enable_gpio); + dev_err(&priv->client->dev, "Failed to get enable gpio: %d\n", + ret); + return ret; + } + + priv->regulator = devm_regulator_get(priv->dev, "vled"); + if (IS_ERR(priv->regulator)) + priv->regulator = NULL; + + device_for_each_child_node(priv->dev, child) { + led = &priv->leds[i]; + ret = fwnode_property_count_u32(child, "reg"); + if (ret < 0) { + dev_err(&priv->client->dev, "reg property is invalid\n"); + goto child_out; + } + + ret = lp50xx_probe_leds(child, priv, led, ret); + if (ret) + goto child_out; + + init_data.fwnode = child; + num_colors = 0; + + /* + * There are only 3 LEDs per module otherwise they should be + * banked which also is presented as 3 LEDs. + */ + mc_led_info = devm_kcalloc(priv->dev, LP50XX_LEDS_PER_MODULE, + sizeof(*mc_led_info), GFP_KERNEL); + if (!mc_led_info) { + ret = -ENOMEM; + goto child_out; + } + + fwnode_for_each_child_node(child, led_node) { + ret = fwnode_property_read_u32(led_node, "color", + &color_id); + if (ret) { + fwnode_handle_put(led_node); + dev_err(priv->dev, "Cannot read color\n"); + goto child_out; + } + + mc_led_info[num_colors].color_index = color_id; + num_colors++; + } + + led->priv = priv; + led->mc_cdev.num_colors = num_colors; + led->mc_cdev.subled_info = mc_led_info; + led_cdev = &led->mc_cdev.led_cdev; + led_cdev->brightness_set_blocking = lp50xx_brightness_set; + + ret = devm_led_classdev_multicolor_register_ext(&priv->client->dev, + &led->mc_cdev, + &init_data); + if (ret) { + dev_err(&priv->client->dev, "led register err: %d\n", + ret); + goto child_out; + } + i++; + } + + return 0; + +child_out: + fwnode_handle_put(child); + return ret; +} + +static int lp50xx_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct lp50xx *led; + int count; + int ret; + + count = device_get_child_node_count(&client->dev); + if (!count) { + dev_err(&client->dev, "LEDs are not defined in device tree!"); + return -ENODEV; + } + + led = devm_kzalloc(&client->dev, struct_size(led, leds, count), + GFP_KERNEL); + if (!led) + return -ENOMEM; + + mutex_init(&led->lock); + led->client = client; + led->dev = &client->dev; + led->chip_info = &lp50xx_chip_info_tbl[id->driver_data]; + i2c_set_clientdata(client, led); + led->regmap = devm_regmap_init_i2c(client, + led->chip_info->lp50xx_regmap_config); + if (IS_ERR(led->regmap)) { + ret = PTR_ERR(led->regmap); + dev_err(&client->dev, "Failed to allocate register map: %d\n", + ret); + return ret; + } + + ret = lp50xx_reset(led); + if (ret) + return ret; + + ret = lp50xx_enable_disable(led, 1); + if (ret) + return ret; + + return lp50xx_probe_dt(led); +} + +static int lp50xx_remove(struct i2c_client *client) +{ + struct lp50xx *led = i2c_get_clientdata(client); + int ret; + + ret = lp50xx_enable_disable(led, 0); + if (ret) { + dev_err(&led->client->dev, "Failed to disable chip\n"); + return ret; + } + + if (led->regulator) { + ret = regulator_disable(led->regulator); + if (ret) + dev_err(&led->client->dev, + "Failed to disable regulator\n"); + } + + mutex_destroy(&led->lock); + + return 0; +} + +static const struct i2c_device_id lp50xx_id[] = { + { "lp5009", LP5009 }, + { "lp5012", LP5012 }, + { "lp5018", LP5018 }, + { "lp5024", LP5024 }, + { "lp5030", LP5030 }, + { "lp5036", LP5036 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, lp50xx_id); + +static const struct of_device_id of_lp50xx_leds_match[] = { + { .compatible = "ti,lp5009", .data = (void *)LP5009 }, + { .compatible = "ti,lp5012", .data = (void *)LP5012 }, + { .compatible = "ti,lp5018", .data = (void *)LP5018 }, + { .compatible = "ti,lp5024", .data = (void *)LP5024 }, + { .compatible = "ti,lp5030", .data = (void *)LP5030 }, + { .compatible = "ti,lp5036", .data = (void *)LP5036 }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_lp50xx_leds_match); + +static struct i2c_driver lp50xx_driver = { + .driver = { + .name = "lp50xx", + .of_match_table = of_lp50xx_leds_match, + }, + .probe = lp50xx_probe, + .remove = lp50xx_remove, + .id_table = lp50xx_id, +}; +module_i2c_driver(lp50xx_driver); + +MODULE_DESCRIPTION("Texas Instruments LP50XX LED driver"); +MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-lp5521.c b/drivers/leds/leds-lp5521.c new file mode 100644 index 000000000..a9e7507c9 --- /dev/null +++ b/drivers/leds/leds-lp5521.c @@ -0,0 +1,623 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LP5521 LED chip driver. + * + * Copyright (C) 2010 Nokia Corporation + * Copyright (C) 2012 Texas Instruments + * + * Contact: Samu Onkalo <samu.p.onkalo@nokia.com> + * Milo(Woogyom) Kim <milo.kim@ti.com> + */ + +#include <linux/delay.h> +#include <linux/firmware.h> +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/platform_data/leds-lp55xx.h> +#include <linux/slab.h> +#include <linux/of.h> + +#include "leds-lp55xx-common.h" + +#define LP5521_PROGRAM_LENGTH 32 +#define LP5521_MAX_LEDS 3 +#define LP5521_CMD_DIRECT 0x3F + +/* Registers */ +#define LP5521_REG_ENABLE 0x00 +#define LP5521_REG_OP_MODE 0x01 +#define LP5521_REG_R_PWM 0x02 +#define LP5521_REG_G_PWM 0x03 +#define LP5521_REG_B_PWM 0x04 +#define LP5521_REG_R_CURRENT 0x05 +#define LP5521_REG_G_CURRENT 0x06 +#define LP5521_REG_B_CURRENT 0x07 +#define LP5521_REG_CONFIG 0x08 +#define LP5521_REG_STATUS 0x0C +#define LP5521_REG_RESET 0x0D +#define LP5521_REG_R_PROG_MEM 0x10 +#define LP5521_REG_G_PROG_MEM 0x30 +#define LP5521_REG_B_PROG_MEM 0x50 + +/* Base register to set LED current */ +#define LP5521_REG_LED_CURRENT_BASE LP5521_REG_R_CURRENT +/* Base register to set the brightness */ +#define LP5521_REG_LED_PWM_BASE LP5521_REG_R_PWM + +/* Bits in ENABLE register */ +#define LP5521_MASTER_ENABLE 0x40 /* Chip master enable */ +#define LP5521_LOGARITHMIC_PWM 0x80 /* Logarithmic PWM adjustment */ +#define LP5521_EXEC_RUN 0x2A +#define LP5521_ENABLE_DEFAULT \ + (LP5521_MASTER_ENABLE | LP5521_LOGARITHMIC_PWM) +#define LP5521_ENABLE_RUN_PROGRAM \ + (LP5521_ENABLE_DEFAULT | LP5521_EXEC_RUN) + +/* CONFIG register */ +#define LP5521_PWM_HF 0x40 /* PWM: 0 = 256Hz, 1 = 558Hz */ +#define LP5521_PWRSAVE_EN 0x20 /* 1 = Power save mode */ +#define LP5521_CP_MODE_OFF 0 /* Charge pump (CP) off */ +#define LP5521_CP_MODE_BYPASS 8 /* CP forced to bypass mode */ +#define LP5521_CP_MODE_1X5 0x10 /* CP forced to 1.5x mode */ +#define LP5521_CP_MODE_AUTO 0x18 /* Automatic mode selection */ +#define LP5521_R_TO_BATT 0x04 /* R out: 0 = CP, 1 = Vbat */ +#define LP5521_CLK_INT 0x01 /* Internal clock */ +#define LP5521_DEFAULT_CFG \ + (LP5521_PWM_HF | LP5521_PWRSAVE_EN | LP5521_CP_MODE_AUTO) + +/* Status */ +#define LP5521_EXT_CLK_USED 0x08 + +/* default R channel current register value */ +#define LP5521_REG_R_CURR_DEFAULT 0xAF + +/* Reset register value */ +#define LP5521_RESET 0xFF + +/* Program Memory Operations */ +#define LP5521_MODE_R_M 0x30 /* Operation Mode Register */ +#define LP5521_MODE_G_M 0x0C +#define LP5521_MODE_B_M 0x03 +#define LP5521_LOAD_R 0x10 +#define LP5521_LOAD_G 0x04 +#define LP5521_LOAD_B 0x01 + +#define LP5521_R_IS_LOADING(mode) \ + ((mode & LP5521_MODE_R_M) == LP5521_LOAD_R) +#define LP5521_G_IS_LOADING(mode) \ + ((mode & LP5521_MODE_G_M) == LP5521_LOAD_G) +#define LP5521_B_IS_LOADING(mode) \ + ((mode & LP5521_MODE_B_M) == LP5521_LOAD_B) + +#define LP5521_EXEC_R_M 0x30 /* Enable Register */ +#define LP5521_EXEC_G_M 0x0C +#define LP5521_EXEC_B_M 0x03 +#define LP5521_EXEC_M 0x3F +#define LP5521_RUN_R 0x20 +#define LP5521_RUN_G 0x08 +#define LP5521_RUN_B 0x02 + +static inline void lp5521_wait_opmode_done(void) +{ + /* operation mode change needs to be longer than 153 us */ + usleep_range(200, 300); +} + +static inline void lp5521_wait_enable_done(void) +{ + /* it takes more 488 us to update ENABLE register */ + usleep_range(500, 600); +} + +static void lp5521_set_led_current(struct lp55xx_led *led, u8 led_current) +{ + led->led_current = led_current; + lp55xx_write(led->chip, LP5521_REG_LED_CURRENT_BASE + led->chan_nr, + led_current); +} + +static void lp5521_load_engine(struct lp55xx_chip *chip) +{ + enum lp55xx_engine_index idx = chip->engine_idx; + static const u8 mask[] = { + [LP55XX_ENGINE_1] = LP5521_MODE_R_M, + [LP55XX_ENGINE_2] = LP5521_MODE_G_M, + [LP55XX_ENGINE_3] = LP5521_MODE_B_M, + }; + + static const u8 val[] = { + [LP55XX_ENGINE_1] = LP5521_LOAD_R, + [LP55XX_ENGINE_2] = LP5521_LOAD_G, + [LP55XX_ENGINE_3] = LP5521_LOAD_B, + }; + + lp55xx_update_bits(chip, LP5521_REG_OP_MODE, mask[idx], val[idx]); + + lp5521_wait_opmode_done(); +} + +static void lp5521_stop_all_engines(struct lp55xx_chip *chip) +{ + lp55xx_write(chip, LP5521_REG_OP_MODE, 0); + lp5521_wait_opmode_done(); +} + +static void lp5521_stop_engine(struct lp55xx_chip *chip) +{ + enum lp55xx_engine_index idx = chip->engine_idx; + static const u8 mask[] = { + [LP55XX_ENGINE_1] = LP5521_MODE_R_M, + [LP55XX_ENGINE_2] = LP5521_MODE_G_M, + [LP55XX_ENGINE_3] = LP5521_MODE_B_M, + }; + + lp55xx_update_bits(chip, LP5521_REG_OP_MODE, mask[idx], 0); + + lp5521_wait_opmode_done(); +} + +static void lp5521_run_engine(struct lp55xx_chip *chip, bool start) +{ + int ret; + u8 mode; + u8 exec; + + /* stop engine */ + if (!start) { + lp5521_stop_engine(chip); + lp55xx_write(chip, LP5521_REG_OP_MODE, LP5521_CMD_DIRECT); + lp5521_wait_opmode_done(); + return; + } + + /* + * To run the engine, + * operation mode and enable register should updated at the same time + */ + + ret = lp55xx_read(chip, LP5521_REG_OP_MODE, &mode); + if (ret) + return; + + ret = lp55xx_read(chip, LP5521_REG_ENABLE, &exec); + if (ret) + return; + + /* change operation mode to RUN only when each engine is loading */ + if (LP5521_R_IS_LOADING(mode)) { + mode = (mode & ~LP5521_MODE_R_M) | LP5521_RUN_R; + exec = (exec & ~LP5521_EXEC_R_M) | LP5521_RUN_R; + } + + if (LP5521_G_IS_LOADING(mode)) { + mode = (mode & ~LP5521_MODE_G_M) | LP5521_RUN_G; + exec = (exec & ~LP5521_EXEC_G_M) | LP5521_RUN_G; + } + + if (LP5521_B_IS_LOADING(mode)) { + mode = (mode & ~LP5521_MODE_B_M) | LP5521_RUN_B; + exec = (exec & ~LP5521_EXEC_B_M) | LP5521_RUN_B; + } + + lp55xx_write(chip, LP5521_REG_OP_MODE, mode); + lp5521_wait_opmode_done(); + + lp55xx_update_bits(chip, LP5521_REG_ENABLE, LP5521_EXEC_M, exec); + lp5521_wait_enable_done(); +} + +static int lp5521_update_program_memory(struct lp55xx_chip *chip, + const u8 *data, size_t size) +{ + enum lp55xx_engine_index idx = chip->engine_idx; + u8 pattern[LP5521_PROGRAM_LENGTH] = {0}; + static const u8 addr[] = { + [LP55XX_ENGINE_1] = LP5521_REG_R_PROG_MEM, + [LP55XX_ENGINE_2] = LP5521_REG_G_PROG_MEM, + [LP55XX_ENGINE_3] = LP5521_REG_B_PROG_MEM, + }; + unsigned cmd; + char c[3]; + int nrchars; + int ret; + int offset = 0; + int i = 0; + + while ((offset < size - 1) && (i < LP5521_PROGRAM_LENGTH)) { + /* separate sscanfs because length is working only for %s */ + ret = sscanf(data + offset, "%2s%n ", c, &nrchars); + if (ret != 1) + goto err; + + ret = sscanf(c, "%2x", &cmd); + if (ret != 1) + goto err; + + pattern[i] = (u8)cmd; + offset += nrchars; + i++; + } + + /* Each instruction is 16bit long. Check that length is even */ + if (i % 2) + goto err; + + for (i = 0; i < LP5521_PROGRAM_LENGTH; i++) { + ret = lp55xx_write(chip, addr[idx] + i, pattern[i]); + if (ret) + return -EINVAL; + } + + return size; + +err: + dev_err(&chip->cl->dev, "wrong pattern format\n"); + return -EINVAL; +} + +static void lp5521_firmware_loaded(struct lp55xx_chip *chip) +{ + const struct firmware *fw = chip->fw; + + if (fw->size > LP5521_PROGRAM_LENGTH) { + dev_err(&chip->cl->dev, "firmware data size overflow: %zu\n", + fw->size); + return; + } + + /* + * Program memory sequence + * 1) set engine mode to "LOAD" + * 2) write firmware data into program memory + */ + + lp5521_load_engine(chip); + lp5521_update_program_memory(chip, fw->data, fw->size); +} + +static int lp5521_post_init_device(struct lp55xx_chip *chip) +{ + int ret; + u8 val; + + /* + * Make sure that the chip is reset by reading back the r channel + * current reg. This is dummy read is required on some platforms - + * otherwise further access to the R G B channels in the + * LP5521_REG_ENABLE register will not have any effect - strange! + */ + ret = lp55xx_read(chip, LP5521_REG_R_CURRENT, &val); + if (ret) { + dev_err(&chip->cl->dev, "error in resetting chip\n"); + return ret; + } + if (val != LP5521_REG_R_CURR_DEFAULT) { + dev_err(&chip->cl->dev, + "unexpected data in register (expected 0x%x got 0x%x)\n", + LP5521_REG_R_CURR_DEFAULT, val); + ret = -EINVAL; + return ret; + } + usleep_range(10000, 20000); + + /* Set all PWMs to direct control mode */ + ret = lp55xx_write(chip, LP5521_REG_OP_MODE, LP5521_CMD_DIRECT); + + /* Update configuration for the clock setting */ + val = LP5521_DEFAULT_CFG; + if (!lp55xx_is_extclk_used(chip)) + val |= LP5521_CLK_INT; + + ret = lp55xx_write(chip, LP5521_REG_CONFIG, val); + if (ret) + return ret; + + /* Initialize all channels PWM to zero -> leds off */ + lp55xx_write(chip, LP5521_REG_R_PWM, 0); + lp55xx_write(chip, LP5521_REG_G_PWM, 0); + lp55xx_write(chip, LP5521_REG_B_PWM, 0); + + /* Set engines are set to run state when OP_MODE enables engines */ + ret = lp55xx_write(chip, LP5521_REG_ENABLE, LP5521_ENABLE_RUN_PROGRAM); + if (ret) + return ret; + + lp5521_wait_enable_done(); + + return 0; +} + +static int lp5521_run_selftest(struct lp55xx_chip *chip, char *buf) +{ + struct lp55xx_platform_data *pdata = chip->pdata; + int ret; + u8 status; + + ret = lp55xx_read(chip, LP5521_REG_STATUS, &status); + if (ret < 0) + return ret; + + if (pdata->clock_mode != LP55XX_CLOCK_EXT) + return 0; + + /* Check that ext clock is really in use if requested */ + if ((status & LP5521_EXT_CLK_USED) == 0) + return -EIO; + + return 0; +} + +static int lp5521_multicolor_brightness(struct lp55xx_led *led) +{ + struct lp55xx_chip *chip = led->chip; + int ret; + int i; + + mutex_lock(&chip->lock); + for (i = 0; i < led->mc_cdev.num_colors; i++) { + ret = lp55xx_write(chip, + LP5521_REG_LED_PWM_BASE + + led->mc_cdev.subled_info[i].channel, + led->mc_cdev.subled_info[i].brightness); + if (ret) + break; + } + mutex_unlock(&chip->lock); + return ret; +} + +static int lp5521_led_brightness(struct lp55xx_led *led) +{ + struct lp55xx_chip *chip = led->chip; + int ret; + + mutex_lock(&chip->lock); + ret = lp55xx_write(chip, LP5521_REG_LED_PWM_BASE + led->chan_nr, + led->brightness); + mutex_unlock(&chip->lock); + + return ret; +} + +static ssize_t show_engine_mode(struct device *dev, + struct device_attribute *attr, + char *buf, int nr) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + enum lp55xx_engine_mode mode = chip->engines[nr - 1].mode; + + switch (mode) { + case LP55XX_ENGINE_RUN: + return sprintf(buf, "run\n"); + case LP55XX_ENGINE_LOAD: + return sprintf(buf, "load\n"); + case LP55XX_ENGINE_DISABLED: + default: + return sprintf(buf, "disabled\n"); + } +} +show_mode(1) +show_mode(2) +show_mode(3) + +static ssize_t store_engine_mode(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len, int nr) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + struct lp55xx_engine *engine = &chip->engines[nr - 1]; + + mutex_lock(&chip->lock); + + chip->engine_idx = nr; + + if (!strncmp(buf, "run", 3)) { + lp5521_run_engine(chip, true); + engine->mode = LP55XX_ENGINE_RUN; + } else if (!strncmp(buf, "load", 4)) { + lp5521_stop_engine(chip); + lp5521_load_engine(chip); + engine->mode = LP55XX_ENGINE_LOAD; + } else if (!strncmp(buf, "disabled", 8)) { + lp5521_stop_engine(chip); + engine->mode = LP55XX_ENGINE_DISABLED; + } + + mutex_unlock(&chip->lock); + + return len; +} +store_mode(1) +store_mode(2) +store_mode(3) + +static ssize_t store_engine_load(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len, int nr) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + int ret; + + mutex_lock(&chip->lock); + + chip->engine_idx = nr; + lp5521_load_engine(chip); + ret = lp5521_update_program_memory(chip, buf, len); + + mutex_unlock(&chip->lock); + + return ret; +} +store_load(1) +store_load(2) +store_load(3) + +static ssize_t lp5521_selftest(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + int ret; + + mutex_lock(&chip->lock); + ret = lp5521_run_selftest(chip, buf); + mutex_unlock(&chip->lock); + + return scnprintf(buf, PAGE_SIZE, "%s\n", ret ? "FAIL" : "OK"); +} + +/* device attributes */ +static LP55XX_DEV_ATTR_RW(engine1_mode, show_engine1_mode, store_engine1_mode); +static LP55XX_DEV_ATTR_RW(engine2_mode, show_engine2_mode, store_engine2_mode); +static LP55XX_DEV_ATTR_RW(engine3_mode, show_engine3_mode, store_engine3_mode); +static LP55XX_DEV_ATTR_WO(engine1_load, store_engine1_load); +static LP55XX_DEV_ATTR_WO(engine2_load, store_engine2_load); +static LP55XX_DEV_ATTR_WO(engine3_load, store_engine3_load); +static LP55XX_DEV_ATTR_RO(selftest, lp5521_selftest); + +static struct attribute *lp5521_attributes[] = { + &dev_attr_engine1_mode.attr, + &dev_attr_engine2_mode.attr, + &dev_attr_engine3_mode.attr, + &dev_attr_engine1_load.attr, + &dev_attr_engine2_load.attr, + &dev_attr_engine3_load.attr, + &dev_attr_selftest.attr, + NULL +}; + +static const struct attribute_group lp5521_group = { + .attrs = lp5521_attributes, +}; + +/* Chip specific configurations */ +static struct lp55xx_device_config lp5521_cfg = { + .reset = { + .addr = LP5521_REG_RESET, + .val = LP5521_RESET, + }, + .enable = { + .addr = LP5521_REG_ENABLE, + .val = LP5521_ENABLE_DEFAULT, + }, + .max_channel = LP5521_MAX_LEDS, + .post_init_device = lp5521_post_init_device, + .brightness_fn = lp5521_led_brightness, + .multicolor_brightness_fn = lp5521_multicolor_brightness, + .set_led_current = lp5521_set_led_current, + .firmware_cb = lp5521_firmware_loaded, + .run_engine = lp5521_run_engine, + .dev_attr_group = &lp5521_group, +}; + +static int lp5521_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + int ret; + struct lp55xx_chip *chip; + struct lp55xx_led *led; + struct lp55xx_platform_data *pdata = dev_get_platdata(&client->dev); + struct device_node *np = dev_of_node(&client->dev); + + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->cfg = &lp5521_cfg; + + if (!pdata) { + if (np) { + pdata = lp55xx_of_populate_pdata(&client->dev, np, + chip); + if (IS_ERR(pdata)) + return PTR_ERR(pdata); + } else { + dev_err(&client->dev, "no platform data\n"); + return -EINVAL; + } + } + + led = devm_kcalloc(&client->dev, + pdata->num_channels, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + chip->cl = client; + chip->pdata = pdata; + + mutex_init(&chip->lock); + + i2c_set_clientdata(client, led); + + ret = lp55xx_init_device(chip); + if (ret) + goto err_init; + + dev_info(&client->dev, "%s programmable led chip found\n", id->name); + + ret = lp55xx_register_leds(led, chip); + if (ret) + goto err_out; + + ret = lp55xx_register_sysfs(chip); + if (ret) { + dev_err(&client->dev, "registering sysfs failed\n"); + goto err_out; + } + + return 0; + +err_out: + lp55xx_deinit_device(chip); +err_init: + return ret; +} + +static int lp5521_remove(struct i2c_client *client) +{ + struct lp55xx_led *led = i2c_get_clientdata(client); + struct lp55xx_chip *chip = led->chip; + + lp5521_stop_all_engines(chip); + lp55xx_unregister_sysfs(chip); + lp55xx_deinit_device(chip); + + return 0; +} + +static const struct i2c_device_id lp5521_id[] = { + { "lp5521", 0 }, /* Three channel chip */ + { } +}; +MODULE_DEVICE_TABLE(i2c, lp5521_id); + +#ifdef CONFIG_OF +static const struct of_device_id of_lp5521_leds_match[] = { + { .compatible = "national,lp5521", }, + {}, +}; + +MODULE_DEVICE_TABLE(of, of_lp5521_leds_match); +#endif +static struct i2c_driver lp5521_driver = { + .driver = { + .name = "lp5521", + .of_match_table = of_match_ptr(of_lp5521_leds_match), + }, + .probe = lp5521_probe, + .remove = lp5521_remove, + .id_table = lp5521_id, +}; + +module_i2c_driver(lp5521_driver); + +MODULE_AUTHOR("Mathias Nyman, Yuri Zaporozhets, Samu Onkalo"); +MODULE_AUTHOR("Milo Kim <milo.kim@ti.com>"); +MODULE_DESCRIPTION("LP5521 LED engine"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-lp5523.c b/drivers/leds/leds-lp5523.c new file mode 100644 index 000000000..b1590cb4a --- /dev/null +++ b/drivers/leds/leds-lp5523.c @@ -0,0 +1,995 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * lp5523.c - LP5523, LP55231 LED Driver + * + * Copyright (C) 2010 Nokia Corporation + * Copyright (C) 2012 Texas Instruments + * + * Contact: Samu Onkalo <samu.p.onkalo@nokia.com> + * Milo(Woogyom) Kim <milo.kim@ti.com> + */ + +#include <linux/delay.h> +#include <linux/firmware.h> +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/platform_data/leds-lp55xx.h> +#include <linux/slab.h> + +#include "leds-lp55xx-common.h" + +#define LP5523_PROGRAM_LENGTH 32 /* bytes */ +/* Memory is used like this: + * 0x00 engine 1 program + * 0x10 engine 2 program + * 0x20 engine 3 program + * 0x30 engine 1 muxing info + * 0x40 engine 2 muxing info + * 0x50 engine 3 muxing info + */ +#define LP5523_MAX_LEDS 9 + +/* Registers */ +#define LP5523_REG_ENABLE 0x00 +#define LP5523_REG_OP_MODE 0x01 +#define LP5523_REG_ENABLE_LEDS_MSB 0x04 +#define LP5523_REG_ENABLE_LEDS_LSB 0x05 +#define LP5523_REG_LED_CTRL_BASE 0x06 +#define LP5523_REG_LED_PWM_BASE 0x16 +#define LP5523_REG_LED_CURRENT_BASE 0x26 +#define LP5523_REG_CONFIG 0x36 +#define LP5523_REG_STATUS 0x3A +#define LP5523_REG_RESET 0x3D +#define LP5523_REG_LED_TEST_CTRL 0x41 +#define LP5523_REG_LED_TEST_ADC 0x42 +#define LP5523_REG_MASTER_FADER_BASE 0x48 +#define LP5523_REG_CH1_PROG_START 0x4C +#define LP5523_REG_CH2_PROG_START 0x4D +#define LP5523_REG_CH3_PROG_START 0x4E +#define LP5523_REG_PROG_PAGE_SEL 0x4F +#define LP5523_REG_PROG_MEM 0x50 + +/* Bit description in registers */ +#define LP5523_ENABLE 0x40 +#define LP5523_AUTO_INC 0x40 +#define LP5523_PWR_SAVE 0x20 +#define LP5523_PWM_PWR_SAVE 0x04 +#define LP5523_CP_AUTO 0x18 +#define LP5523_AUTO_CLK 0x02 + +#define LP5523_EN_LEDTEST 0x80 +#define LP5523_LEDTEST_DONE 0x80 +#define LP5523_RESET 0xFF +#define LP5523_ADC_SHORTCIRC_LIM 80 +#define LP5523_EXT_CLK_USED 0x08 +#define LP5523_ENG_STATUS_MASK 0x07 + +#define LP5523_FADER_MAPPING_MASK 0xC0 +#define LP5523_FADER_MAPPING_SHIFT 6 + +/* Memory Page Selection */ +#define LP5523_PAGE_ENG1 0 +#define LP5523_PAGE_ENG2 1 +#define LP5523_PAGE_ENG3 2 +#define LP5523_PAGE_MUX1 3 +#define LP5523_PAGE_MUX2 4 +#define LP5523_PAGE_MUX3 5 + +/* Program Memory Operations */ +#define LP5523_MODE_ENG1_M 0x30 /* Operation Mode Register */ +#define LP5523_MODE_ENG2_M 0x0C +#define LP5523_MODE_ENG3_M 0x03 +#define LP5523_LOAD_ENG1 0x10 +#define LP5523_LOAD_ENG2 0x04 +#define LP5523_LOAD_ENG3 0x01 + +#define LP5523_ENG1_IS_LOADING(mode) \ + ((mode & LP5523_MODE_ENG1_M) == LP5523_LOAD_ENG1) +#define LP5523_ENG2_IS_LOADING(mode) \ + ((mode & LP5523_MODE_ENG2_M) == LP5523_LOAD_ENG2) +#define LP5523_ENG3_IS_LOADING(mode) \ + ((mode & LP5523_MODE_ENG3_M) == LP5523_LOAD_ENG3) + +#define LP5523_EXEC_ENG1_M 0x30 /* Enable Register */ +#define LP5523_EXEC_ENG2_M 0x0C +#define LP5523_EXEC_ENG3_M 0x03 +#define LP5523_EXEC_M 0x3F +#define LP5523_RUN_ENG1 0x20 +#define LP5523_RUN_ENG2 0x08 +#define LP5523_RUN_ENG3 0x02 + +#define LED_ACTIVE(mux, led) (!!(mux & (0x0001 << led))) + +enum lp5523_chip_id { + LP5523, + LP55231, +}; + +static int lp5523_init_program_engine(struct lp55xx_chip *chip); + +static inline void lp5523_wait_opmode_done(void) +{ + usleep_range(1000, 2000); +} + +static void lp5523_set_led_current(struct lp55xx_led *led, u8 led_current) +{ + led->led_current = led_current; + lp55xx_write(led->chip, LP5523_REG_LED_CURRENT_BASE + led->chan_nr, + led_current); +} + +static int lp5523_post_init_device(struct lp55xx_chip *chip) +{ + int ret; + + ret = lp55xx_write(chip, LP5523_REG_ENABLE, LP5523_ENABLE); + if (ret) + return ret; + + /* Chip startup time is 500 us, 1 - 2 ms gives some margin */ + usleep_range(1000, 2000); + + ret = lp55xx_write(chip, LP5523_REG_CONFIG, + LP5523_AUTO_INC | LP5523_PWR_SAVE | + LP5523_CP_AUTO | LP5523_AUTO_CLK | + LP5523_PWM_PWR_SAVE); + if (ret) + return ret; + + /* turn on all leds */ + ret = lp55xx_write(chip, LP5523_REG_ENABLE_LEDS_MSB, 0x01); + if (ret) + return ret; + + ret = lp55xx_write(chip, LP5523_REG_ENABLE_LEDS_LSB, 0xff); + if (ret) + return ret; + + return lp5523_init_program_engine(chip); +} + +static void lp5523_load_engine(struct lp55xx_chip *chip) +{ + enum lp55xx_engine_index idx = chip->engine_idx; + static const u8 mask[] = { + [LP55XX_ENGINE_1] = LP5523_MODE_ENG1_M, + [LP55XX_ENGINE_2] = LP5523_MODE_ENG2_M, + [LP55XX_ENGINE_3] = LP5523_MODE_ENG3_M, + }; + + static const u8 val[] = { + [LP55XX_ENGINE_1] = LP5523_LOAD_ENG1, + [LP55XX_ENGINE_2] = LP5523_LOAD_ENG2, + [LP55XX_ENGINE_3] = LP5523_LOAD_ENG3, + }; + + lp55xx_update_bits(chip, LP5523_REG_OP_MODE, mask[idx], val[idx]); + + lp5523_wait_opmode_done(); +} + +static void lp5523_load_engine_and_select_page(struct lp55xx_chip *chip) +{ + enum lp55xx_engine_index idx = chip->engine_idx; + static const u8 page_sel[] = { + [LP55XX_ENGINE_1] = LP5523_PAGE_ENG1, + [LP55XX_ENGINE_2] = LP5523_PAGE_ENG2, + [LP55XX_ENGINE_3] = LP5523_PAGE_ENG3, + }; + + lp5523_load_engine(chip); + + lp55xx_write(chip, LP5523_REG_PROG_PAGE_SEL, page_sel[idx]); +} + +static void lp5523_stop_all_engines(struct lp55xx_chip *chip) +{ + lp55xx_write(chip, LP5523_REG_OP_MODE, 0); + lp5523_wait_opmode_done(); +} + +static void lp5523_stop_engine(struct lp55xx_chip *chip) +{ + enum lp55xx_engine_index idx = chip->engine_idx; + static const u8 mask[] = { + [LP55XX_ENGINE_1] = LP5523_MODE_ENG1_M, + [LP55XX_ENGINE_2] = LP5523_MODE_ENG2_M, + [LP55XX_ENGINE_3] = LP5523_MODE_ENG3_M, + }; + + lp55xx_update_bits(chip, LP5523_REG_OP_MODE, mask[idx], 0); + + lp5523_wait_opmode_done(); +} + +static void lp5523_turn_off_channels(struct lp55xx_chip *chip) +{ + int i; + + for (i = 0; i < LP5523_MAX_LEDS; i++) + lp55xx_write(chip, LP5523_REG_LED_PWM_BASE + i, 0); +} + +static void lp5523_run_engine(struct lp55xx_chip *chip, bool start) +{ + int ret; + u8 mode; + u8 exec; + + /* stop engine */ + if (!start) { + lp5523_stop_engine(chip); + lp5523_turn_off_channels(chip); + return; + } + + /* + * To run the engine, + * operation mode and enable register should updated at the same time + */ + + ret = lp55xx_read(chip, LP5523_REG_OP_MODE, &mode); + if (ret) + return; + + ret = lp55xx_read(chip, LP5523_REG_ENABLE, &exec); + if (ret) + return; + + /* change operation mode to RUN only when each engine is loading */ + if (LP5523_ENG1_IS_LOADING(mode)) { + mode = (mode & ~LP5523_MODE_ENG1_M) | LP5523_RUN_ENG1; + exec = (exec & ~LP5523_EXEC_ENG1_M) | LP5523_RUN_ENG1; + } + + if (LP5523_ENG2_IS_LOADING(mode)) { + mode = (mode & ~LP5523_MODE_ENG2_M) | LP5523_RUN_ENG2; + exec = (exec & ~LP5523_EXEC_ENG2_M) | LP5523_RUN_ENG2; + } + + if (LP5523_ENG3_IS_LOADING(mode)) { + mode = (mode & ~LP5523_MODE_ENG3_M) | LP5523_RUN_ENG3; + exec = (exec & ~LP5523_EXEC_ENG3_M) | LP5523_RUN_ENG3; + } + + lp55xx_write(chip, LP5523_REG_OP_MODE, mode); + lp5523_wait_opmode_done(); + + lp55xx_update_bits(chip, LP5523_REG_ENABLE, LP5523_EXEC_M, exec); +} + +static int lp5523_init_program_engine(struct lp55xx_chip *chip) +{ + int i; + int j; + int ret; + u8 status; + /* one pattern per engine setting LED MUX start and stop addresses */ + static const u8 pattern[][LP5523_PROGRAM_LENGTH] = { + { 0x9c, 0x30, 0x9c, 0xb0, 0x9d, 0x80, 0xd8, 0x00, 0}, + { 0x9c, 0x40, 0x9c, 0xc0, 0x9d, 0x80, 0xd8, 0x00, 0}, + { 0x9c, 0x50, 0x9c, 0xd0, 0x9d, 0x80, 0xd8, 0x00, 0}, + }; + + /* hardcode 32 bytes of memory for each engine from program memory */ + ret = lp55xx_write(chip, LP5523_REG_CH1_PROG_START, 0x00); + if (ret) + return ret; + + ret = lp55xx_write(chip, LP5523_REG_CH2_PROG_START, 0x10); + if (ret) + return ret; + + ret = lp55xx_write(chip, LP5523_REG_CH3_PROG_START, 0x20); + if (ret) + return ret; + + /* write LED MUX address space for each engine */ + for (i = LP55XX_ENGINE_1; i <= LP55XX_ENGINE_3; i++) { + chip->engine_idx = i; + lp5523_load_engine_and_select_page(chip); + + for (j = 0; j < LP5523_PROGRAM_LENGTH; j++) { + ret = lp55xx_write(chip, LP5523_REG_PROG_MEM + j, + pattern[i - 1][j]); + if (ret) + goto out; + } + } + + lp5523_run_engine(chip, true); + + /* Let the programs run for couple of ms and check the engine status */ + usleep_range(3000, 6000); + ret = lp55xx_read(chip, LP5523_REG_STATUS, &status); + if (ret) + goto out; + status &= LP5523_ENG_STATUS_MASK; + + if (status != LP5523_ENG_STATUS_MASK) { + dev_err(&chip->cl->dev, + "could not configure LED engine, status = 0x%.2x\n", + status); + ret = -1; + } + +out: + lp5523_stop_all_engines(chip); + return ret; +} + +static int lp5523_update_program_memory(struct lp55xx_chip *chip, + const u8 *data, size_t size) +{ + u8 pattern[LP5523_PROGRAM_LENGTH] = {0}; + unsigned int cmd; + char c[3]; + int nrchars; + int ret; + int offset = 0; + int i = 0; + + while ((offset < size - 1) && (i < LP5523_PROGRAM_LENGTH)) { + /* separate sscanfs because length is working only for %s */ + ret = sscanf(data + offset, "%2s%n ", c, &nrchars); + if (ret != 1) + goto err; + + ret = sscanf(c, "%2x", &cmd); + if (ret != 1) + goto err; + + pattern[i] = (u8)cmd; + offset += nrchars; + i++; + } + + /* Each instruction is 16bit long. Check that length is even */ + if (i % 2) + goto err; + + for (i = 0; i < LP5523_PROGRAM_LENGTH; i++) { + ret = lp55xx_write(chip, LP5523_REG_PROG_MEM + i, pattern[i]); + if (ret) + return -EINVAL; + } + + return size; + +err: + dev_err(&chip->cl->dev, "wrong pattern format\n"); + return -EINVAL; +} + +static void lp5523_firmware_loaded(struct lp55xx_chip *chip) +{ + const struct firmware *fw = chip->fw; + + if (fw->size > LP5523_PROGRAM_LENGTH) { + dev_err(&chip->cl->dev, "firmware data size overflow: %zu\n", + fw->size); + return; + } + + /* + * Program memory sequence + * 1) set engine mode to "LOAD" + * 2) write firmware data into program memory + */ + + lp5523_load_engine_and_select_page(chip); + lp5523_update_program_memory(chip, fw->data, fw->size); +} + +static ssize_t show_engine_mode(struct device *dev, + struct device_attribute *attr, + char *buf, int nr) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + enum lp55xx_engine_mode mode = chip->engines[nr - 1].mode; + + switch (mode) { + case LP55XX_ENGINE_RUN: + return sprintf(buf, "run\n"); + case LP55XX_ENGINE_LOAD: + return sprintf(buf, "load\n"); + case LP55XX_ENGINE_DISABLED: + default: + return sprintf(buf, "disabled\n"); + } +} +show_mode(1) +show_mode(2) +show_mode(3) + +static ssize_t store_engine_mode(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len, int nr) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + struct lp55xx_engine *engine = &chip->engines[nr - 1]; + + mutex_lock(&chip->lock); + + chip->engine_idx = nr; + + if (!strncmp(buf, "run", 3)) { + lp5523_run_engine(chip, true); + engine->mode = LP55XX_ENGINE_RUN; + } else if (!strncmp(buf, "load", 4)) { + lp5523_stop_engine(chip); + lp5523_load_engine(chip); + engine->mode = LP55XX_ENGINE_LOAD; + } else if (!strncmp(buf, "disabled", 8)) { + lp5523_stop_engine(chip); + engine->mode = LP55XX_ENGINE_DISABLED; + } + + mutex_unlock(&chip->lock); + + return len; +} +store_mode(1) +store_mode(2) +store_mode(3) + +static int lp5523_mux_parse(const char *buf, u16 *mux, size_t len) +{ + u16 tmp_mux = 0; + int i; + + len = min_t(int, len, LP5523_MAX_LEDS); + + for (i = 0; i < len; i++) { + switch (buf[i]) { + case '1': + tmp_mux |= (1 << i); + break; + case '0': + break; + case '\n': + i = len; + break; + default: + return -1; + } + } + *mux = tmp_mux; + + return 0; +} + +static void lp5523_mux_to_array(u16 led_mux, char *array) +{ + int i, pos = 0; + + for (i = 0; i < LP5523_MAX_LEDS; i++) + pos += sprintf(array + pos, "%x", LED_ACTIVE(led_mux, i)); + + array[pos] = '\0'; +} + +static ssize_t show_engine_leds(struct device *dev, + struct device_attribute *attr, + char *buf, int nr) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + char mux[LP5523_MAX_LEDS + 1]; + + lp5523_mux_to_array(chip->engines[nr - 1].led_mux, mux); + + return sprintf(buf, "%s\n", mux); +} +show_leds(1) +show_leds(2) +show_leds(3) + +static int lp5523_load_mux(struct lp55xx_chip *chip, u16 mux, int nr) +{ + struct lp55xx_engine *engine = &chip->engines[nr - 1]; + int ret; + static const u8 mux_page[] = { + [LP55XX_ENGINE_1] = LP5523_PAGE_MUX1, + [LP55XX_ENGINE_2] = LP5523_PAGE_MUX2, + [LP55XX_ENGINE_3] = LP5523_PAGE_MUX3, + }; + + lp5523_load_engine(chip); + + ret = lp55xx_write(chip, LP5523_REG_PROG_PAGE_SEL, mux_page[nr]); + if (ret) + return ret; + + ret = lp55xx_write(chip, LP5523_REG_PROG_MEM, (u8)(mux >> 8)); + if (ret) + return ret; + + ret = lp55xx_write(chip, LP5523_REG_PROG_MEM + 1, (u8)(mux)); + if (ret) + return ret; + + engine->led_mux = mux; + return 0; +} + +static ssize_t store_engine_leds(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len, int nr) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + struct lp55xx_engine *engine = &chip->engines[nr - 1]; + u16 mux = 0; + ssize_t ret; + + if (lp5523_mux_parse(buf, &mux, len)) + return -EINVAL; + + mutex_lock(&chip->lock); + + chip->engine_idx = nr; + ret = -EINVAL; + + if (engine->mode != LP55XX_ENGINE_LOAD) + goto leave; + + if (lp5523_load_mux(chip, mux, nr)) + goto leave; + + ret = len; +leave: + mutex_unlock(&chip->lock); + return ret; +} +store_leds(1) +store_leds(2) +store_leds(3) + +static ssize_t store_engine_load(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len, int nr) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + int ret; + + mutex_lock(&chip->lock); + + chip->engine_idx = nr; + lp5523_load_engine_and_select_page(chip); + ret = lp5523_update_program_memory(chip, buf, len); + + mutex_unlock(&chip->lock); + + return ret; +} +store_load(1) +store_load(2) +store_load(3) + +static ssize_t lp5523_selftest(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + struct lp55xx_platform_data *pdata = chip->pdata; + int i, ret, pos = 0; + u8 status, adc, vdd; + + mutex_lock(&chip->lock); + + ret = lp55xx_read(chip, LP5523_REG_STATUS, &status); + if (ret < 0) + goto fail; + + /* Check that ext clock is really in use if requested */ + if (pdata->clock_mode == LP55XX_CLOCK_EXT) { + if ((status & LP5523_EXT_CLK_USED) == 0) + goto fail; + } + + /* Measure VDD (i.e. VBAT) first (channel 16 corresponds to VDD) */ + lp55xx_write(chip, LP5523_REG_LED_TEST_CTRL, LP5523_EN_LEDTEST | 16); + usleep_range(3000, 6000); /* ADC conversion time is typically 2.7 ms */ + ret = lp55xx_read(chip, LP5523_REG_STATUS, &status); + if (ret < 0) + goto fail; + + if (!(status & LP5523_LEDTEST_DONE)) + usleep_range(3000, 6000); /* Was not ready. Wait little bit */ + + ret = lp55xx_read(chip, LP5523_REG_LED_TEST_ADC, &vdd); + if (ret < 0) + goto fail; + + vdd--; /* There may be some fluctuation in measurement */ + + for (i = 0; i < LP5523_MAX_LEDS; i++) { + /* Skip non-existing channels */ + if (pdata->led_config[i].led_current == 0) + continue; + + /* Set default current */ + lp55xx_write(chip, LP5523_REG_LED_CURRENT_BASE + i, + pdata->led_config[i].led_current); + + lp55xx_write(chip, LP5523_REG_LED_PWM_BASE + i, 0xff); + /* let current stabilize 2 - 4ms before measurements start */ + usleep_range(2000, 4000); + lp55xx_write(chip, LP5523_REG_LED_TEST_CTRL, + LP5523_EN_LEDTEST | i); + /* ADC conversion time is 2.7 ms typically */ + usleep_range(3000, 6000); + ret = lp55xx_read(chip, LP5523_REG_STATUS, &status); + if (ret < 0) + goto fail; + + if (!(status & LP5523_LEDTEST_DONE)) + usleep_range(3000, 6000);/* Was not ready. Wait. */ + + ret = lp55xx_read(chip, LP5523_REG_LED_TEST_ADC, &adc); + if (ret < 0) + goto fail; + + if (adc >= vdd || adc < LP5523_ADC_SHORTCIRC_LIM) + pos += sprintf(buf + pos, "LED %d FAIL\n", i); + + lp55xx_write(chip, LP5523_REG_LED_PWM_BASE + i, 0x00); + + /* Restore current */ + lp55xx_write(chip, LP5523_REG_LED_CURRENT_BASE + i, + led->led_current); + led++; + } + if (pos == 0) + pos = sprintf(buf, "OK\n"); + goto release_lock; +fail: + pos = sprintf(buf, "FAIL\n"); + +release_lock: + mutex_unlock(&chip->lock); + + return pos; +} + +#define show_fader(nr) \ +static ssize_t show_master_fader##nr(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ +{ \ + return show_master_fader(dev, attr, buf, nr); \ +} + +#define store_fader(nr) \ +static ssize_t store_master_fader##nr(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t len) \ +{ \ + return store_master_fader(dev, attr, buf, len, nr); \ +} + +static ssize_t show_master_fader(struct device *dev, + struct device_attribute *attr, + char *buf, int nr) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + int ret; + u8 val; + + mutex_lock(&chip->lock); + ret = lp55xx_read(chip, LP5523_REG_MASTER_FADER_BASE + nr - 1, &val); + mutex_unlock(&chip->lock); + + if (ret == 0) + ret = sprintf(buf, "%u\n", val); + + return ret; +} +show_fader(1) +show_fader(2) +show_fader(3) + +static ssize_t store_master_fader(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len, int nr) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + int ret; + unsigned long val; + + if (kstrtoul(buf, 0, &val)) + return -EINVAL; + + if (val > 0xff) + return -EINVAL; + + mutex_lock(&chip->lock); + ret = lp55xx_write(chip, LP5523_REG_MASTER_FADER_BASE + nr - 1, + (u8)val); + mutex_unlock(&chip->lock); + + if (ret == 0) + ret = len; + + return ret; +} +store_fader(1) +store_fader(2) +store_fader(3) + +static ssize_t show_master_fader_leds(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + int i, ret, pos = 0; + u8 val; + + mutex_lock(&chip->lock); + + for (i = 0; i < LP5523_MAX_LEDS; i++) { + ret = lp55xx_read(chip, LP5523_REG_LED_CTRL_BASE + i, &val); + if (ret) + goto leave; + + val = (val & LP5523_FADER_MAPPING_MASK) + >> LP5523_FADER_MAPPING_SHIFT; + if (val > 3) { + ret = -EINVAL; + goto leave; + } + buf[pos++] = val + '0'; + } + buf[pos++] = '\n'; + ret = pos; +leave: + mutex_unlock(&chip->lock); + return ret; +} + +static ssize_t store_master_fader_leds(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + int i, n, ret; + u8 val; + + n = min_t(int, len, LP5523_MAX_LEDS); + + mutex_lock(&chip->lock); + + for (i = 0; i < n; i++) { + if (buf[i] >= '0' && buf[i] <= '3') { + val = (buf[i] - '0') << LP5523_FADER_MAPPING_SHIFT; + ret = lp55xx_update_bits(chip, + LP5523_REG_LED_CTRL_BASE + i, + LP5523_FADER_MAPPING_MASK, + val); + if (ret) + goto leave; + } else { + ret = -EINVAL; + goto leave; + } + } + ret = len; +leave: + mutex_unlock(&chip->lock); + return ret; +} + +static int lp5523_multicolor_brightness(struct lp55xx_led *led) +{ + struct lp55xx_chip *chip = led->chip; + int ret; + int i; + + mutex_lock(&chip->lock); + for (i = 0; i < led->mc_cdev.num_colors; i++) { + ret = lp55xx_write(chip, + LP5523_REG_LED_PWM_BASE + + led->mc_cdev.subled_info[i].channel, + led->mc_cdev.subled_info[i].brightness); + if (ret) + break; + } + mutex_unlock(&chip->lock); + return ret; +} + +static int lp5523_led_brightness(struct lp55xx_led *led) +{ + struct lp55xx_chip *chip = led->chip; + int ret; + + mutex_lock(&chip->lock); + ret = lp55xx_write(chip, LP5523_REG_LED_PWM_BASE + led->chan_nr, + led->brightness); + mutex_unlock(&chip->lock); + return ret; +} + +static LP55XX_DEV_ATTR_RW(engine1_mode, show_engine1_mode, store_engine1_mode); +static LP55XX_DEV_ATTR_RW(engine2_mode, show_engine2_mode, store_engine2_mode); +static LP55XX_DEV_ATTR_RW(engine3_mode, show_engine3_mode, store_engine3_mode); +static LP55XX_DEV_ATTR_RW(engine1_leds, show_engine1_leds, store_engine1_leds); +static LP55XX_DEV_ATTR_RW(engine2_leds, show_engine2_leds, store_engine2_leds); +static LP55XX_DEV_ATTR_RW(engine3_leds, show_engine3_leds, store_engine3_leds); +static LP55XX_DEV_ATTR_WO(engine1_load, store_engine1_load); +static LP55XX_DEV_ATTR_WO(engine2_load, store_engine2_load); +static LP55XX_DEV_ATTR_WO(engine3_load, store_engine3_load); +static LP55XX_DEV_ATTR_RO(selftest, lp5523_selftest); +static LP55XX_DEV_ATTR_RW(master_fader1, show_master_fader1, + store_master_fader1); +static LP55XX_DEV_ATTR_RW(master_fader2, show_master_fader2, + store_master_fader2); +static LP55XX_DEV_ATTR_RW(master_fader3, show_master_fader3, + store_master_fader3); +static LP55XX_DEV_ATTR_RW(master_fader_leds, show_master_fader_leds, + store_master_fader_leds); + +static struct attribute *lp5523_attributes[] = { + &dev_attr_engine1_mode.attr, + &dev_attr_engine2_mode.attr, + &dev_attr_engine3_mode.attr, + &dev_attr_engine1_load.attr, + &dev_attr_engine2_load.attr, + &dev_attr_engine3_load.attr, + &dev_attr_engine1_leds.attr, + &dev_attr_engine2_leds.attr, + &dev_attr_engine3_leds.attr, + &dev_attr_selftest.attr, + &dev_attr_master_fader1.attr, + &dev_attr_master_fader2.attr, + &dev_attr_master_fader3.attr, + &dev_attr_master_fader_leds.attr, + NULL, +}; + +static const struct attribute_group lp5523_group = { + .attrs = lp5523_attributes, +}; + +/* Chip specific configurations */ +static struct lp55xx_device_config lp5523_cfg = { + .reset = { + .addr = LP5523_REG_RESET, + .val = LP5523_RESET, + }, + .enable = { + .addr = LP5523_REG_ENABLE, + .val = LP5523_ENABLE, + }, + .max_channel = LP5523_MAX_LEDS, + .post_init_device = lp5523_post_init_device, + .brightness_fn = lp5523_led_brightness, + .multicolor_brightness_fn = lp5523_multicolor_brightness, + .set_led_current = lp5523_set_led_current, + .firmware_cb = lp5523_firmware_loaded, + .run_engine = lp5523_run_engine, + .dev_attr_group = &lp5523_group, +}; + +static int lp5523_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + int ret; + struct lp55xx_chip *chip; + struct lp55xx_led *led; + struct lp55xx_platform_data *pdata = dev_get_platdata(&client->dev); + struct device_node *np = dev_of_node(&client->dev); + + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->cfg = &lp5523_cfg; + + if (!pdata) { + if (np) { + pdata = lp55xx_of_populate_pdata(&client->dev, np, + chip); + if (IS_ERR(pdata)) + return PTR_ERR(pdata); + } else { + dev_err(&client->dev, "no platform data\n"); + return -EINVAL; + } + } + + led = devm_kcalloc(&client->dev, + pdata->num_channels, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + chip->cl = client; + chip->pdata = pdata; + + mutex_init(&chip->lock); + + i2c_set_clientdata(client, led); + + ret = lp55xx_init_device(chip); + if (ret) + goto err_init; + + dev_info(&client->dev, "%s Programmable led chip found\n", id->name); + + ret = lp55xx_register_leds(led, chip); + if (ret) + goto err_out; + + ret = lp55xx_register_sysfs(chip); + if (ret) { + dev_err(&client->dev, "registering sysfs failed\n"); + goto err_out; + } + + return 0; + +err_out: + lp55xx_deinit_device(chip); +err_init: + return ret; +} + +static int lp5523_remove(struct i2c_client *client) +{ + struct lp55xx_led *led = i2c_get_clientdata(client); + struct lp55xx_chip *chip = led->chip; + + lp5523_stop_all_engines(chip); + lp55xx_unregister_sysfs(chip); + lp55xx_deinit_device(chip); + + return 0; +} + +static const struct i2c_device_id lp5523_id[] = { + { "lp5523", LP5523 }, + { "lp55231", LP55231 }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, lp5523_id); + +#ifdef CONFIG_OF +static const struct of_device_id of_lp5523_leds_match[] = { + { .compatible = "national,lp5523", }, + { .compatible = "ti,lp55231", }, + {}, +}; + +MODULE_DEVICE_TABLE(of, of_lp5523_leds_match); +#endif + +static struct i2c_driver lp5523_driver = { + .driver = { + .name = "lp5523x", + .of_match_table = of_match_ptr(of_lp5523_leds_match), + }, + .probe = lp5523_probe, + .remove = lp5523_remove, + .id_table = lp5523_id, +}; + +module_i2c_driver(lp5523_driver); + +MODULE_AUTHOR("Mathias Nyman <mathias.nyman@nokia.com>"); +MODULE_AUTHOR("Milo Kim <milo.kim@ti.com>"); +MODULE_DESCRIPTION("LP5523 LED engine"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-lp5562.c b/drivers/leds/leds-lp5562.c new file mode 100644 index 000000000..31c14016d --- /dev/null +++ b/drivers/leds/leds-lp5562.c @@ -0,0 +1,618 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LP5562 LED driver + * + * Copyright (C) 2013 Texas Instruments + * + * Author: Milo(Woogyom) Kim <milo.kim@ti.com> + */ + +#include <linux/delay.h> +#include <linux/firmware.h> +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/platform_data/leds-lp55xx.h> +#include <linux/slab.h> + +#include "leds-lp55xx-common.h" + +#define LP5562_PROGRAM_LENGTH 32 +#define LP5562_MAX_LEDS 4 + +/* ENABLE Register 00h */ +#define LP5562_REG_ENABLE 0x00 +#define LP5562_EXEC_ENG1_M 0x30 +#define LP5562_EXEC_ENG2_M 0x0C +#define LP5562_EXEC_ENG3_M 0x03 +#define LP5562_EXEC_M 0x3F +#define LP5562_MASTER_ENABLE 0x40 /* Chip master enable */ +#define LP5562_LOGARITHMIC_PWM 0x80 /* Logarithmic PWM adjustment */ +#define LP5562_EXEC_RUN 0x2A +#define LP5562_ENABLE_DEFAULT \ + (LP5562_MASTER_ENABLE | LP5562_LOGARITHMIC_PWM) +#define LP5562_ENABLE_RUN_PROGRAM \ + (LP5562_ENABLE_DEFAULT | LP5562_EXEC_RUN) + +/* OPMODE Register 01h */ +#define LP5562_REG_OP_MODE 0x01 +#define LP5562_MODE_ENG1_M 0x30 +#define LP5562_MODE_ENG2_M 0x0C +#define LP5562_MODE_ENG3_M 0x03 +#define LP5562_LOAD_ENG1 0x10 +#define LP5562_LOAD_ENG2 0x04 +#define LP5562_LOAD_ENG3 0x01 +#define LP5562_RUN_ENG1 0x20 +#define LP5562_RUN_ENG2 0x08 +#define LP5562_RUN_ENG3 0x02 +#define LP5562_ENG1_IS_LOADING(mode) \ + ((mode & LP5562_MODE_ENG1_M) == LP5562_LOAD_ENG1) +#define LP5562_ENG2_IS_LOADING(mode) \ + ((mode & LP5562_MODE_ENG2_M) == LP5562_LOAD_ENG2) +#define LP5562_ENG3_IS_LOADING(mode) \ + ((mode & LP5562_MODE_ENG3_M) == LP5562_LOAD_ENG3) + +/* BRIGHTNESS Registers */ +#define LP5562_REG_R_PWM 0x04 +#define LP5562_REG_G_PWM 0x03 +#define LP5562_REG_B_PWM 0x02 +#define LP5562_REG_W_PWM 0x0E + +/* CURRENT Registers */ +#define LP5562_REG_R_CURRENT 0x07 +#define LP5562_REG_G_CURRENT 0x06 +#define LP5562_REG_B_CURRENT 0x05 +#define LP5562_REG_W_CURRENT 0x0F + +/* CONFIG Register 08h */ +#define LP5562_REG_CONFIG 0x08 +#define LP5562_PWM_HF 0x40 +#define LP5562_PWRSAVE_EN 0x20 +#define LP5562_CLK_INT 0x01 /* Internal clock */ +#define LP5562_DEFAULT_CFG (LP5562_PWM_HF | LP5562_PWRSAVE_EN) + +/* RESET Register 0Dh */ +#define LP5562_REG_RESET 0x0D +#define LP5562_RESET 0xFF + +/* PROGRAM ENGINE Registers */ +#define LP5562_REG_PROG_MEM_ENG1 0x10 +#define LP5562_REG_PROG_MEM_ENG2 0x30 +#define LP5562_REG_PROG_MEM_ENG3 0x50 + +/* LEDMAP Register 70h */ +#define LP5562_REG_ENG_SEL 0x70 +#define LP5562_ENG_SEL_PWM 0 +#define LP5562_ENG_FOR_RGB_M 0x3F +#define LP5562_ENG_SEL_RGB 0x1B /* R:ENG1, G:ENG2, B:ENG3 */ +#define LP5562_ENG_FOR_W_M 0xC0 +#define LP5562_ENG1_FOR_W 0x40 /* W:ENG1 */ +#define LP5562_ENG2_FOR_W 0x80 /* W:ENG2 */ +#define LP5562_ENG3_FOR_W 0xC0 /* W:ENG3 */ + +/* Program Commands */ +#define LP5562_CMD_DISABLE 0x00 +#define LP5562_CMD_LOAD 0x15 +#define LP5562_CMD_RUN 0x2A +#define LP5562_CMD_DIRECT 0x3F +#define LP5562_PATTERN_OFF 0 + +static inline void lp5562_wait_opmode_done(void) +{ + /* operation mode change needs to be longer than 153 us */ + usleep_range(200, 300); +} + +static inline void lp5562_wait_enable_done(void) +{ + /* it takes more 488 us to update ENABLE register */ + usleep_range(500, 600); +} + +static void lp5562_set_led_current(struct lp55xx_led *led, u8 led_current) +{ + static const u8 addr[] = { + LP5562_REG_R_CURRENT, + LP5562_REG_G_CURRENT, + LP5562_REG_B_CURRENT, + LP5562_REG_W_CURRENT, + }; + + led->led_current = led_current; + lp55xx_write(led->chip, addr[led->chan_nr], led_current); +} + +static void lp5562_load_engine(struct lp55xx_chip *chip) +{ + enum lp55xx_engine_index idx = chip->engine_idx; + static const u8 mask[] = { + [LP55XX_ENGINE_1] = LP5562_MODE_ENG1_M, + [LP55XX_ENGINE_2] = LP5562_MODE_ENG2_M, + [LP55XX_ENGINE_3] = LP5562_MODE_ENG3_M, + }; + + static const u8 val[] = { + [LP55XX_ENGINE_1] = LP5562_LOAD_ENG1, + [LP55XX_ENGINE_2] = LP5562_LOAD_ENG2, + [LP55XX_ENGINE_3] = LP5562_LOAD_ENG3, + }; + + lp55xx_update_bits(chip, LP5562_REG_OP_MODE, mask[idx], val[idx]); + + lp5562_wait_opmode_done(); +} + +static void lp5562_stop_engine(struct lp55xx_chip *chip) +{ + lp55xx_write(chip, LP5562_REG_OP_MODE, LP5562_CMD_DISABLE); + lp5562_wait_opmode_done(); +} + +static void lp5562_run_engine(struct lp55xx_chip *chip, bool start) +{ + int ret; + u8 mode; + u8 exec; + + /* stop engine */ + if (!start) { + lp55xx_write(chip, LP5562_REG_ENABLE, LP5562_ENABLE_DEFAULT); + lp5562_wait_enable_done(); + lp5562_stop_engine(chip); + lp55xx_write(chip, LP5562_REG_ENG_SEL, LP5562_ENG_SEL_PWM); + lp55xx_write(chip, LP5562_REG_OP_MODE, LP5562_CMD_DIRECT); + lp5562_wait_opmode_done(); + return; + } + + /* + * To run the engine, + * operation mode and enable register should updated at the same time + */ + + ret = lp55xx_read(chip, LP5562_REG_OP_MODE, &mode); + if (ret) + return; + + ret = lp55xx_read(chip, LP5562_REG_ENABLE, &exec); + if (ret) + return; + + /* change operation mode to RUN only when each engine is loading */ + if (LP5562_ENG1_IS_LOADING(mode)) { + mode = (mode & ~LP5562_MODE_ENG1_M) | LP5562_RUN_ENG1; + exec = (exec & ~LP5562_EXEC_ENG1_M) | LP5562_RUN_ENG1; + } + + if (LP5562_ENG2_IS_LOADING(mode)) { + mode = (mode & ~LP5562_MODE_ENG2_M) | LP5562_RUN_ENG2; + exec = (exec & ~LP5562_EXEC_ENG2_M) | LP5562_RUN_ENG2; + } + + if (LP5562_ENG3_IS_LOADING(mode)) { + mode = (mode & ~LP5562_MODE_ENG3_M) | LP5562_RUN_ENG3; + exec = (exec & ~LP5562_EXEC_ENG3_M) | LP5562_RUN_ENG3; + } + + lp55xx_write(chip, LP5562_REG_OP_MODE, mode); + lp5562_wait_opmode_done(); + + lp55xx_update_bits(chip, LP5562_REG_ENABLE, LP5562_EXEC_M, exec); + lp5562_wait_enable_done(); +} + +static int lp5562_update_firmware(struct lp55xx_chip *chip, + const u8 *data, size_t size) +{ + enum lp55xx_engine_index idx = chip->engine_idx; + u8 pattern[LP5562_PROGRAM_LENGTH] = {0}; + static const u8 addr[] = { + [LP55XX_ENGINE_1] = LP5562_REG_PROG_MEM_ENG1, + [LP55XX_ENGINE_2] = LP5562_REG_PROG_MEM_ENG2, + [LP55XX_ENGINE_3] = LP5562_REG_PROG_MEM_ENG3, + }; + unsigned cmd; + char c[3]; + int program_size; + int nrchars; + int offset = 0; + int ret; + int i; + + /* clear program memory before updating */ + for (i = 0; i < LP5562_PROGRAM_LENGTH; i++) + lp55xx_write(chip, addr[idx] + i, 0); + + i = 0; + while ((offset < size - 1) && (i < LP5562_PROGRAM_LENGTH)) { + /* separate sscanfs because length is working only for %s */ + ret = sscanf(data + offset, "%2s%n ", c, &nrchars); + if (ret != 1) + goto err; + + ret = sscanf(c, "%2x", &cmd); + if (ret != 1) + goto err; + + pattern[i] = (u8)cmd; + offset += nrchars; + i++; + } + + /* Each instruction is 16bit long. Check that length is even */ + if (i % 2) + goto err; + + program_size = i; + for (i = 0; i < program_size; i++) + lp55xx_write(chip, addr[idx] + i, pattern[i]); + + return 0; + +err: + dev_err(&chip->cl->dev, "wrong pattern format\n"); + return -EINVAL; +} + +static void lp5562_firmware_loaded(struct lp55xx_chip *chip) +{ + const struct firmware *fw = chip->fw; + + /* + * the firmware is encoded in ascii hex character, with 2 chars + * per byte + */ + if (fw->size > (LP5562_PROGRAM_LENGTH * 2)) { + dev_err(&chip->cl->dev, "firmware data size overflow: %zu\n", + fw->size); + return; + } + + /* + * Program memory sequence + * 1) set engine mode to "LOAD" + * 2) write firmware data into program memory + */ + + lp5562_load_engine(chip); + lp5562_update_firmware(chip, fw->data, fw->size); +} + +static int lp5562_post_init_device(struct lp55xx_chip *chip) +{ + int ret; + u8 cfg = LP5562_DEFAULT_CFG; + + /* Set all PWMs to direct control mode */ + ret = lp55xx_write(chip, LP5562_REG_OP_MODE, LP5562_CMD_DIRECT); + if (ret) + return ret; + + lp5562_wait_opmode_done(); + + /* Update configuration for the clock setting */ + if (!lp55xx_is_extclk_used(chip)) + cfg |= LP5562_CLK_INT; + + ret = lp55xx_write(chip, LP5562_REG_CONFIG, cfg); + if (ret) + return ret; + + /* Initialize all channels PWM to zero -> leds off */ + lp55xx_write(chip, LP5562_REG_R_PWM, 0); + lp55xx_write(chip, LP5562_REG_G_PWM, 0); + lp55xx_write(chip, LP5562_REG_B_PWM, 0); + lp55xx_write(chip, LP5562_REG_W_PWM, 0); + + /* Set LED map as register PWM by default */ + lp55xx_write(chip, LP5562_REG_ENG_SEL, LP5562_ENG_SEL_PWM); + + return 0; +} + +static int lp5562_led_brightness(struct lp55xx_led *led) +{ + struct lp55xx_chip *chip = led->chip; + static const u8 addr[] = { + LP5562_REG_R_PWM, + LP5562_REG_G_PWM, + LP5562_REG_B_PWM, + LP5562_REG_W_PWM, + }; + int ret; + + mutex_lock(&chip->lock); + ret = lp55xx_write(chip, addr[led->chan_nr], led->brightness); + mutex_unlock(&chip->lock); + + return ret; +} + +static void lp5562_write_program_memory(struct lp55xx_chip *chip, + u8 base, const u8 *rgb, int size) +{ + int i; + + if (!rgb || size <= 0) + return; + + for (i = 0; i < size; i++) + lp55xx_write(chip, base + i, *(rgb + i)); + + lp55xx_write(chip, base + i, 0); + lp55xx_write(chip, base + i + 1, 0); +} + +/* check the size of program count */ +static inline bool _is_pc_overflow(struct lp55xx_predef_pattern *ptn) +{ + return ptn->size_r >= LP5562_PROGRAM_LENGTH || + ptn->size_g >= LP5562_PROGRAM_LENGTH || + ptn->size_b >= LP5562_PROGRAM_LENGTH; +} + +static int lp5562_run_predef_led_pattern(struct lp55xx_chip *chip, int mode) +{ + struct lp55xx_predef_pattern *ptn; + int i; + + if (mode == LP5562_PATTERN_OFF) { + lp5562_run_engine(chip, false); + return 0; + } + + ptn = chip->pdata->patterns + (mode - 1); + if (!ptn || _is_pc_overflow(ptn)) { + dev_err(&chip->cl->dev, "invalid pattern data\n"); + return -EINVAL; + } + + lp5562_stop_engine(chip); + + /* Set LED map as RGB */ + lp55xx_write(chip, LP5562_REG_ENG_SEL, LP5562_ENG_SEL_RGB); + + /* Load engines */ + for (i = LP55XX_ENGINE_1; i <= LP55XX_ENGINE_3; i++) { + chip->engine_idx = i; + lp5562_load_engine(chip); + } + + /* Clear program registers */ + lp55xx_write(chip, LP5562_REG_PROG_MEM_ENG1, 0); + lp55xx_write(chip, LP5562_REG_PROG_MEM_ENG1 + 1, 0); + lp55xx_write(chip, LP5562_REG_PROG_MEM_ENG2, 0); + lp55xx_write(chip, LP5562_REG_PROG_MEM_ENG2 + 1, 0); + lp55xx_write(chip, LP5562_REG_PROG_MEM_ENG3, 0); + lp55xx_write(chip, LP5562_REG_PROG_MEM_ENG3 + 1, 0); + + /* Program engines */ + lp5562_write_program_memory(chip, LP5562_REG_PROG_MEM_ENG1, + ptn->r, ptn->size_r); + lp5562_write_program_memory(chip, LP5562_REG_PROG_MEM_ENG2, + ptn->g, ptn->size_g); + lp5562_write_program_memory(chip, LP5562_REG_PROG_MEM_ENG3, + ptn->b, ptn->size_b); + + /* Run engines */ + lp5562_run_engine(chip, true); + + return 0; +} + +static ssize_t lp5562_store_pattern(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + struct lp55xx_predef_pattern *ptn = chip->pdata->patterns; + int num_patterns = chip->pdata->num_patterns; + unsigned long mode; + int ret; + + ret = kstrtoul(buf, 0, &mode); + if (ret) + return ret; + + if (mode > num_patterns || !ptn) + return -EINVAL; + + mutex_lock(&chip->lock); + ret = lp5562_run_predef_led_pattern(chip, mode); + mutex_unlock(&chip->lock); + + if (ret) + return ret; + + return len; +} + +static ssize_t lp5562_store_engine_mux(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + u8 mask; + u8 val; + + /* LED map + * R ... Engine 1 (fixed) + * G ... Engine 2 (fixed) + * B ... Engine 3 (fixed) + * W ... Engine 1 or 2 or 3 + */ + + if (sysfs_streq(buf, "RGB")) { + mask = LP5562_ENG_FOR_RGB_M; + val = LP5562_ENG_SEL_RGB; + } else if (sysfs_streq(buf, "W")) { + enum lp55xx_engine_index idx = chip->engine_idx; + + mask = LP5562_ENG_FOR_W_M; + switch (idx) { + case LP55XX_ENGINE_1: + val = LP5562_ENG1_FOR_W; + break; + case LP55XX_ENGINE_2: + val = LP5562_ENG2_FOR_W; + break; + case LP55XX_ENGINE_3: + val = LP5562_ENG3_FOR_W; + break; + default: + return -EINVAL; + } + + } else { + dev_err(dev, "choose RGB or W\n"); + return -EINVAL; + } + + mutex_lock(&chip->lock); + lp55xx_update_bits(chip, LP5562_REG_ENG_SEL, mask, val); + mutex_unlock(&chip->lock); + + return len; +} + +static LP55XX_DEV_ATTR_WO(led_pattern, lp5562_store_pattern); +static LP55XX_DEV_ATTR_WO(engine_mux, lp5562_store_engine_mux); + +static struct attribute *lp5562_attributes[] = { + &dev_attr_led_pattern.attr, + &dev_attr_engine_mux.attr, + NULL, +}; + +static const struct attribute_group lp5562_group = { + .attrs = lp5562_attributes, +}; + +/* Chip specific configurations */ +static struct lp55xx_device_config lp5562_cfg = { + .max_channel = LP5562_MAX_LEDS, + .reset = { + .addr = LP5562_REG_RESET, + .val = LP5562_RESET, + }, + .enable = { + .addr = LP5562_REG_ENABLE, + .val = LP5562_ENABLE_DEFAULT, + }, + .post_init_device = lp5562_post_init_device, + .set_led_current = lp5562_set_led_current, + .brightness_fn = lp5562_led_brightness, + .run_engine = lp5562_run_engine, + .firmware_cb = lp5562_firmware_loaded, + .dev_attr_group = &lp5562_group, +}; + +static int lp5562_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + int ret; + struct lp55xx_chip *chip; + struct lp55xx_led *led; + struct lp55xx_platform_data *pdata = dev_get_platdata(&client->dev); + struct device_node *np = dev_of_node(&client->dev); + + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->cfg = &lp5562_cfg; + + if (!pdata) { + if (np) { + pdata = lp55xx_of_populate_pdata(&client->dev, np, + chip); + if (IS_ERR(pdata)) + return PTR_ERR(pdata); + } else { + dev_err(&client->dev, "no platform data\n"); + return -EINVAL; + } + } + + + led = devm_kcalloc(&client->dev, + pdata->num_channels, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + chip->cl = client; + chip->pdata = pdata; + + mutex_init(&chip->lock); + + i2c_set_clientdata(client, led); + + ret = lp55xx_init_device(chip); + if (ret) + goto err_init; + + ret = lp55xx_register_leds(led, chip); + if (ret) + goto err_out; + + ret = lp55xx_register_sysfs(chip); + if (ret) { + dev_err(&client->dev, "registering sysfs failed\n"); + goto err_out; + } + + return 0; + +err_out: + lp55xx_deinit_device(chip); +err_init: + return ret; +} + +static int lp5562_remove(struct i2c_client *client) +{ + struct lp55xx_led *led = i2c_get_clientdata(client); + struct lp55xx_chip *chip = led->chip; + + lp5562_stop_engine(chip); + + lp55xx_unregister_sysfs(chip); + lp55xx_deinit_device(chip); + + return 0; +} + +static const struct i2c_device_id lp5562_id[] = { + { "lp5562", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, lp5562_id); + +#ifdef CONFIG_OF +static const struct of_device_id of_lp5562_leds_match[] = { + { .compatible = "ti,lp5562", }, + {}, +}; + +MODULE_DEVICE_TABLE(of, of_lp5562_leds_match); +#endif + +static struct i2c_driver lp5562_driver = { + .driver = { + .name = "lp5562", + .of_match_table = of_match_ptr(of_lp5562_leds_match), + }, + .probe = lp5562_probe, + .remove = lp5562_remove, + .id_table = lp5562_id, +}; + +module_i2c_driver(lp5562_driver); + +MODULE_DESCRIPTION("Texas Instruments LP5562 LED Driver"); +MODULE_AUTHOR("Milo Kim"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-lp55xx-common.c b/drivers/leds/leds-lp55xx-common.c new file mode 100644 index 000000000..81de1346b --- /dev/null +++ b/drivers/leds/leds-lp55xx-common.c @@ -0,0 +1,710 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LP5521/LP5523/LP55231/LP5562 Common Driver + * + * Copyright 2012 Texas Instruments + * + * Author: Milo(Woogyom) Kim <milo.kim@ti.com> + * + * Derived from leds-lp5521.c, leds-lp5523.c + */ + +#include <linux/clk.h> +#include <linux/delay.h> +#include <linux/firmware.h> +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/platform_data/leds-lp55xx.h> +#include <linux/slab.h> +#include <linux/gpio/consumer.h> + +#include "leds-lp55xx-common.h" + +/* External clock rate */ +#define LP55XX_CLK_32K 32768 + +static struct lp55xx_led *cdev_to_lp55xx_led(struct led_classdev *cdev) +{ + return container_of(cdev, struct lp55xx_led, cdev); +} + +static struct lp55xx_led *dev_to_lp55xx_led(struct device *dev) +{ + return cdev_to_lp55xx_led(dev_get_drvdata(dev)); +} + +static struct lp55xx_led *mcled_cdev_to_led(struct led_classdev_mc *mc_cdev) +{ + return container_of(mc_cdev, struct lp55xx_led, mc_cdev); +} + +static void lp55xx_reset_device(struct lp55xx_chip *chip) +{ + struct lp55xx_device_config *cfg = chip->cfg; + u8 addr = cfg->reset.addr; + u8 val = cfg->reset.val; + + /* no error checking here because no ACK from the device after reset */ + lp55xx_write(chip, addr, val); +} + +static int lp55xx_detect_device(struct lp55xx_chip *chip) +{ + struct lp55xx_device_config *cfg = chip->cfg; + u8 addr = cfg->enable.addr; + u8 val = cfg->enable.val; + int ret; + + ret = lp55xx_write(chip, addr, val); + if (ret) + return ret; + + usleep_range(1000, 2000); + + ret = lp55xx_read(chip, addr, &val); + if (ret) + return ret; + + if (val != cfg->enable.val) + return -ENODEV; + + return 0; +} + +static int lp55xx_post_init_device(struct lp55xx_chip *chip) +{ + struct lp55xx_device_config *cfg = chip->cfg; + + if (!cfg->post_init_device) + return 0; + + return cfg->post_init_device(chip); +} + +static ssize_t led_current_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct lp55xx_led *led = dev_to_lp55xx_led(dev); + + return scnprintf(buf, PAGE_SIZE, "%d\n", led->led_current); +} + +static ssize_t led_current_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct lp55xx_led *led = dev_to_lp55xx_led(dev); + struct lp55xx_chip *chip = led->chip; + unsigned long curr; + + if (kstrtoul(buf, 0, &curr)) + return -EINVAL; + + if (curr > led->max_current) + return -EINVAL; + + if (!chip->cfg->set_led_current) + return len; + + mutex_lock(&chip->lock); + chip->cfg->set_led_current(led, (u8)curr); + mutex_unlock(&chip->lock); + + return len; +} + +static ssize_t max_current_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct lp55xx_led *led = dev_to_lp55xx_led(dev); + + return scnprintf(buf, PAGE_SIZE, "%d\n", led->max_current); +} + +static DEVICE_ATTR_RW(led_current); +static DEVICE_ATTR_RO(max_current); + +static struct attribute *lp55xx_led_attrs[] = { + &dev_attr_led_current.attr, + &dev_attr_max_current.attr, + NULL, +}; +ATTRIBUTE_GROUPS(lp55xx_led); + +static int lp55xx_set_mc_brightness(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct led_classdev_mc *mc_dev = lcdev_to_mccdev(cdev); + struct lp55xx_led *led = mcled_cdev_to_led(mc_dev); + struct lp55xx_device_config *cfg = led->chip->cfg; + + led_mc_calc_color_components(&led->mc_cdev, brightness); + return cfg->multicolor_brightness_fn(led); + +} + +static int lp55xx_set_brightness(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct lp55xx_led *led = cdev_to_lp55xx_led(cdev); + struct lp55xx_device_config *cfg = led->chip->cfg; + + led->brightness = (u8)brightness; + return cfg->brightness_fn(led); +} + +static int lp55xx_init_led(struct lp55xx_led *led, + struct lp55xx_chip *chip, int chan) +{ + struct lp55xx_platform_data *pdata = chip->pdata; + struct lp55xx_device_config *cfg = chip->cfg; + struct device *dev = &chip->cl->dev; + int max_channel = cfg->max_channel; + struct mc_subled *mc_led_info; + struct led_classdev *led_cdev; + char name[32]; + int i, j = 0; + int ret; + + if (chan >= max_channel) { + dev_err(dev, "invalid channel: %d / %d\n", chan, max_channel); + return -EINVAL; + } + + if (pdata->led_config[chan].led_current == 0) + return 0; + + if (pdata->led_config[chan].name) { + led->cdev.name = pdata->led_config[chan].name; + } else { + snprintf(name, sizeof(name), "%s:channel%d", + pdata->label ? : chip->cl->name, chan); + led->cdev.name = name; + } + + if (pdata->led_config[chan].num_colors > 1) { + mc_led_info = devm_kcalloc(dev, + pdata->led_config[chan].num_colors, + sizeof(*mc_led_info), GFP_KERNEL); + if (!mc_led_info) + return -ENOMEM; + + led_cdev = &led->mc_cdev.led_cdev; + led_cdev->name = led->cdev.name; + led_cdev->brightness_set_blocking = lp55xx_set_mc_brightness; + led->mc_cdev.num_colors = pdata->led_config[chan].num_colors; + for (i = 0; i < led->mc_cdev.num_colors; i++) { + mc_led_info[i].color_index = + pdata->led_config[chan].color_id[i]; + mc_led_info[i].channel = + pdata->led_config[chan].output_num[i]; + j++; + } + + led->mc_cdev.subled_info = mc_led_info; + } else { + led->cdev.brightness_set_blocking = lp55xx_set_brightness; + } + + led->cdev.groups = lp55xx_led_groups; + led->cdev.default_trigger = pdata->led_config[chan].default_trigger; + led->led_current = pdata->led_config[chan].led_current; + led->max_current = pdata->led_config[chan].max_current; + led->chan_nr = pdata->led_config[chan].chan_nr; + + if (led->chan_nr >= max_channel) { + dev_err(dev, "Use channel numbers between 0 and %d\n", + max_channel - 1); + return -EINVAL; + } + + if (pdata->led_config[chan].num_colors > 1) + ret = devm_led_classdev_multicolor_register(dev, &led->mc_cdev); + else + ret = devm_led_classdev_register(dev, &led->cdev); + + if (ret) { + dev_err(dev, "led register err: %d\n", ret); + return ret; + } + + return 0; +} + +static void lp55xx_firmware_loaded(const struct firmware *fw, void *context) +{ + struct lp55xx_chip *chip = context; + struct device *dev = &chip->cl->dev; + enum lp55xx_engine_index idx = chip->engine_idx; + + if (!fw) { + dev_err(dev, "firmware request failed\n"); + return; + } + + /* handling firmware data is chip dependent */ + mutex_lock(&chip->lock); + + chip->engines[idx - 1].mode = LP55XX_ENGINE_LOAD; + chip->fw = fw; + if (chip->cfg->firmware_cb) + chip->cfg->firmware_cb(chip); + + mutex_unlock(&chip->lock); + + /* firmware should be released for other channel use */ + release_firmware(chip->fw); + chip->fw = NULL; +} + +static int lp55xx_request_firmware(struct lp55xx_chip *chip) +{ + const char *name = chip->cl->name; + struct device *dev = &chip->cl->dev; + + return request_firmware_nowait(THIS_MODULE, false, name, dev, + GFP_KERNEL, chip, lp55xx_firmware_loaded); +} + +static ssize_t select_engine_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + + return sprintf(buf, "%d\n", chip->engine_idx); +} + +static ssize_t select_engine_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + unsigned long val; + int ret; + + if (kstrtoul(buf, 0, &val)) + return -EINVAL; + + /* select the engine to be run */ + + switch (val) { + case LP55XX_ENGINE_1: + case LP55XX_ENGINE_2: + case LP55XX_ENGINE_3: + mutex_lock(&chip->lock); + chip->engine_idx = val; + ret = lp55xx_request_firmware(chip); + mutex_unlock(&chip->lock); + break; + default: + dev_err(dev, "%lu: invalid engine index. (1, 2, 3)\n", val); + return -EINVAL; + } + + if (ret) { + dev_err(dev, "request firmware err: %d\n", ret); + return ret; + } + + return len; +} + +static inline void lp55xx_run_engine(struct lp55xx_chip *chip, bool start) +{ + if (chip->cfg->run_engine) + chip->cfg->run_engine(chip, start); +} + +static ssize_t run_engine_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t len) +{ + struct lp55xx_led *led = i2c_get_clientdata(to_i2c_client(dev)); + struct lp55xx_chip *chip = led->chip; + unsigned long val; + + if (kstrtoul(buf, 0, &val)) + return -EINVAL; + + /* run or stop the selected engine */ + + if (val <= 0) { + lp55xx_run_engine(chip, false); + return len; + } + + mutex_lock(&chip->lock); + lp55xx_run_engine(chip, true); + mutex_unlock(&chip->lock); + + return len; +} + +static DEVICE_ATTR_RW(select_engine); +static DEVICE_ATTR_WO(run_engine); + +static struct attribute *lp55xx_engine_attributes[] = { + &dev_attr_select_engine.attr, + &dev_attr_run_engine.attr, + NULL, +}; + +static const struct attribute_group lp55xx_engine_attr_group = { + .attrs = lp55xx_engine_attributes, +}; + +int lp55xx_write(struct lp55xx_chip *chip, u8 reg, u8 val) +{ + return i2c_smbus_write_byte_data(chip->cl, reg, val); +} +EXPORT_SYMBOL_GPL(lp55xx_write); + +int lp55xx_read(struct lp55xx_chip *chip, u8 reg, u8 *val) +{ + s32 ret; + + ret = i2c_smbus_read_byte_data(chip->cl, reg); + if (ret < 0) + return ret; + + *val = ret; + return 0; +} +EXPORT_SYMBOL_GPL(lp55xx_read); + +int lp55xx_update_bits(struct lp55xx_chip *chip, u8 reg, u8 mask, u8 val) +{ + int ret; + u8 tmp; + + ret = lp55xx_read(chip, reg, &tmp); + if (ret) + return ret; + + tmp &= ~mask; + tmp |= val & mask; + + return lp55xx_write(chip, reg, tmp); +} +EXPORT_SYMBOL_GPL(lp55xx_update_bits); + +bool lp55xx_is_extclk_used(struct lp55xx_chip *chip) +{ + struct clk *clk; + int err; + + clk = devm_clk_get(&chip->cl->dev, "32k_clk"); + if (IS_ERR(clk)) + goto use_internal_clk; + + err = clk_prepare_enable(clk); + if (err) + goto use_internal_clk; + + if (clk_get_rate(clk) != LP55XX_CLK_32K) { + clk_disable_unprepare(clk); + goto use_internal_clk; + } + + dev_info(&chip->cl->dev, "%dHz external clock used\n", LP55XX_CLK_32K); + + chip->clk = clk; + return true; + +use_internal_clk: + dev_info(&chip->cl->dev, "internal clock used\n"); + return false; +} +EXPORT_SYMBOL_GPL(lp55xx_is_extclk_used); + +int lp55xx_init_device(struct lp55xx_chip *chip) +{ + struct lp55xx_platform_data *pdata; + struct lp55xx_device_config *cfg; + struct device *dev = &chip->cl->dev; + int ret = 0; + + WARN_ON(!chip); + + pdata = chip->pdata; + cfg = chip->cfg; + + if (!pdata || !cfg) + return -EINVAL; + + if (pdata->enable_gpiod) { + gpiod_set_consumer_name(pdata->enable_gpiod, "LP55xx enable"); + gpiod_set_value(pdata->enable_gpiod, 0); + usleep_range(1000, 2000); /* Keep enable down at least 1ms */ + gpiod_set_value(pdata->enable_gpiod, 1); + usleep_range(1000, 2000); /* 500us abs min. */ + } + + lp55xx_reset_device(chip); + + /* + * Exact value is not available. 10 - 20ms + * appears to be enough for reset. + */ + usleep_range(10000, 20000); + + ret = lp55xx_detect_device(chip); + if (ret) { + dev_err(dev, "device detection err: %d\n", ret); + goto err; + } + + /* chip specific initialization */ + ret = lp55xx_post_init_device(chip); + if (ret) { + dev_err(dev, "post init device err: %d\n", ret); + goto err_post_init; + } + + return 0; + +err_post_init: + lp55xx_deinit_device(chip); +err: + return ret; +} +EXPORT_SYMBOL_GPL(lp55xx_init_device); + +void lp55xx_deinit_device(struct lp55xx_chip *chip) +{ + struct lp55xx_platform_data *pdata = chip->pdata; + + if (chip->clk) + clk_disable_unprepare(chip->clk); + + if (pdata->enable_gpiod) + gpiod_set_value(pdata->enable_gpiod, 0); +} +EXPORT_SYMBOL_GPL(lp55xx_deinit_device); + +int lp55xx_register_leds(struct lp55xx_led *led, struct lp55xx_chip *chip) +{ + struct lp55xx_platform_data *pdata = chip->pdata; + struct lp55xx_device_config *cfg = chip->cfg; + int num_channels = pdata->num_channels; + struct lp55xx_led *each; + u8 led_current; + int ret; + int i; + + if (!cfg->brightness_fn) { + dev_err(&chip->cl->dev, "empty brightness configuration\n"); + return -EINVAL; + } + + for (i = 0; i < num_channels; i++) { + + /* do not initialize channels that are not connected */ + if (pdata->led_config[i].led_current == 0) + continue; + + led_current = pdata->led_config[i].led_current; + each = led + i; + ret = lp55xx_init_led(each, chip, i); + if (ret) + goto err_init_led; + + chip->num_leds++; + each->chip = chip; + + /* setting led current at each channel */ + if (cfg->set_led_current) + cfg->set_led_current(each, led_current); + } + + return 0; + +err_init_led: + return ret; +} +EXPORT_SYMBOL_GPL(lp55xx_register_leds); + +int lp55xx_register_sysfs(struct lp55xx_chip *chip) +{ + struct device *dev = &chip->cl->dev; + struct lp55xx_device_config *cfg = chip->cfg; + int ret; + + if (!cfg->run_engine || !cfg->firmware_cb) + goto dev_specific_attrs; + + ret = sysfs_create_group(&dev->kobj, &lp55xx_engine_attr_group); + if (ret) + return ret; + +dev_specific_attrs: + return cfg->dev_attr_group ? + sysfs_create_group(&dev->kobj, cfg->dev_attr_group) : 0; +} +EXPORT_SYMBOL_GPL(lp55xx_register_sysfs); + +void lp55xx_unregister_sysfs(struct lp55xx_chip *chip) +{ + struct device *dev = &chip->cl->dev; + struct lp55xx_device_config *cfg = chip->cfg; + + if (cfg->dev_attr_group) + sysfs_remove_group(&dev->kobj, cfg->dev_attr_group); + + sysfs_remove_group(&dev->kobj, &lp55xx_engine_attr_group); +} +EXPORT_SYMBOL_GPL(lp55xx_unregister_sysfs); + +static int lp55xx_parse_common_child(struct device_node *np, + struct lp55xx_led_config *cfg, + int led_number, int *chan_nr) +{ + int ret; + + of_property_read_string(np, "chan-name", + &cfg[led_number].name); + of_property_read_u8(np, "led-cur", + &cfg[led_number].led_current); + of_property_read_u8(np, "max-cur", + &cfg[led_number].max_current); + + ret = of_property_read_u32(np, "reg", chan_nr); + if (ret) + return ret; + + if (*chan_nr < 0 || *chan_nr > cfg->max_channel) + return -EINVAL; + + return 0; +} + +static int lp55xx_parse_multi_led_child(struct device_node *child, + struct lp55xx_led_config *cfg, + int child_number, int color_number) +{ + int chan_nr, color_id, ret; + + ret = lp55xx_parse_common_child(child, cfg, child_number, &chan_nr); + if (ret) + return ret; + + ret = of_property_read_u32(child, "color", &color_id); + if (ret) + return ret; + + cfg[child_number].color_id[color_number] = color_id; + cfg[child_number].output_num[color_number] = chan_nr; + + return 0; +} + +static int lp55xx_parse_multi_led(struct device_node *np, + struct lp55xx_led_config *cfg, + int child_number) +{ + struct device_node *child; + int num_colors = 0, ret; + + for_each_available_child_of_node(np, child) { + ret = lp55xx_parse_multi_led_child(child, cfg, child_number, + num_colors); + if (ret) { + of_node_put(child); + return ret; + } + num_colors++; + } + + cfg[child_number].num_colors = num_colors; + + return 0; +} + +static int lp55xx_parse_logical_led(struct device_node *np, + struct lp55xx_led_config *cfg, + int child_number) +{ + int led_color, ret; + int chan_nr = 0; + + cfg[child_number].default_trigger = + of_get_property(np, "linux,default-trigger", NULL); + + ret = of_property_read_u32(np, "color", &led_color); + if (ret) + return ret; + + if (led_color == LED_COLOR_ID_RGB) + return lp55xx_parse_multi_led(np, cfg, child_number); + + ret = lp55xx_parse_common_child(np, cfg, child_number, &chan_nr); + if (ret < 0) + return ret; + + cfg[child_number].chan_nr = chan_nr; + + return ret; +} + +struct lp55xx_platform_data *lp55xx_of_populate_pdata(struct device *dev, + struct device_node *np, + struct lp55xx_chip *chip) +{ + struct device_node *child; + struct lp55xx_platform_data *pdata; + struct lp55xx_led_config *cfg; + int num_channels; + int i = 0; + int ret; + + pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return ERR_PTR(-ENOMEM); + + num_channels = of_get_available_child_count(np); + if (num_channels == 0) { + dev_err(dev, "no LED channels\n"); + return ERR_PTR(-EINVAL); + } + + cfg = devm_kcalloc(dev, num_channels, sizeof(*cfg), GFP_KERNEL); + if (!cfg) + return ERR_PTR(-ENOMEM); + + pdata->led_config = &cfg[0]; + pdata->num_channels = num_channels; + cfg->max_channel = chip->cfg->max_channel; + + for_each_available_child_of_node(np, child) { + ret = lp55xx_parse_logical_led(child, cfg, i); + if (ret) { + of_node_put(child); + return ERR_PTR(-EINVAL); + } + i++; + } + + of_property_read_string(np, "label", &pdata->label); + of_property_read_u8(np, "clock-mode", &pdata->clock_mode); + + pdata->enable_gpiod = devm_gpiod_get_optional(dev, "enable", + GPIOD_ASIS); + if (IS_ERR(pdata->enable_gpiod)) + return ERR_CAST(pdata->enable_gpiod); + + /* LP8501 specific */ + of_property_read_u8(np, "pwr-sel", (u8 *)&pdata->pwr_sel); + + return pdata; +} +EXPORT_SYMBOL_GPL(lp55xx_of_populate_pdata); + +MODULE_AUTHOR("Milo Kim <milo.kim@ti.com>"); +MODULE_DESCRIPTION("LP55xx Common Driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-lp55xx-common.h b/drivers/leds/leds-lp55xx-common.h new file mode 100644 index 000000000..2f38c5b33 --- /dev/null +++ b/drivers/leds/leds-lp55xx-common.h @@ -0,0 +1,211 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * LP55XX Common Driver Header + * + * Copyright (C) 2012 Texas Instruments + * + * Author: Milo(Woogyom) Kim <milo.kim@ti.com> + * + * Derived from leds-lp5521.c, leds-lp5523.c + */ + +#ifndef _LEDS_LP55XX_COMMON_H +#define _LEDS_LP55XX_COMMON_H + +#include <linux/led-class-multicolor.h> + +enum lp55xx_engine_index { + LP55XX_ENGINE_INVALID, + LP55XX_ENGINE_1, + LP55XX_ENGINE_2, + LP55XX_ENGINE_3, + LP55XX_ENGINE_MAX = LP55XX_ENGINE_3, +}; + +enum lp55xx_engine_mode { + LP55XX_ENGINE_DISABLED, + LP55XX_ENGINE_LOAD, + LP55XX_ENGINE_RUN, +}; + +#define LP55XX_DEV_ATTR_RW(name, show, store) \ + DEVICE_ATTR(name, S_IRUGO | S_IWUSR, show, store) +#define LP55XX_DEV_ATTR_RO(name, show) \ + DEVICE_ATTR(name, S_IRUGO, show, NULL) +#define LP55XX_DEV_ATTR_WO(name, store) \ + DEVICE_ATTR(name, S_IWUSR, NULL, store) + +#define show_mode(nr) \ +static ssize_t show_engine##nr##_mode(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ +{ \ + return show_engine_mode(dev, attr, buf, nr); \ +} + +#define store_mode(nr) \ +static ssize_t store_engine##nr##_mode(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t len) \ +{ \ + return store_engine_mode(dev, attr, buf, len, nr); \ +} + +#define show_leds(nr) \ +static ssize_t show_engine##nr##_leds(struct device *dev, \ + struct device_attribute *attr, \ + char *buf) \ +{ \ + return show_engine_leds(dev, attr, buf, nr); \ +} + +#define store_leds(nr) \ +static ssize_t store_engine##nr##_leds(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t len) \ +{ \ + return store_engine_leds(dev, attr, buf, len, nr); \ +} + +#define store_load(nr) \ +static ssize_t store_engine##nr##_load(struct device *dev, \ + struct device_attribute *attr, \ + const char *buf, size_t len) \ +{ \ + return store_engine_load(dev, attr, buf, len, nr); \ +} + +struct lp55xx_led; +struct lp55xx_chip; + +/* + * struct lp55xx_reg + * @addr : Register address + * @val : Register value + */ +struct lp55xx_reg { + u8 addr; + u8 val; +}; + +/* + * struct lp55xx_device_config + * @reset : Chip specific reset command + * @enable : Chip specific enable command + * @max_channel : Maximum number of channels + * @post_init_device : Chip specific initialization code + * @brightness_fn : Brightness function + * @multicolor_brightness_fn : Multicolor brightness function + * @set_led_current : LED current set function + * @firmware_cb : Call function when the firmware is loaded + * @run_engine : Run internal engine for pattern + * @dev_attr_group : Device specific attributes + */ +struct lp55xx_device_config { + const struct lp55xx_reg reset; + const struct lp55xx_reg enable; + const int max_channel; + + /* define if the device has specific initialization process */ + int (*post_init_device) (struct lp55xx_chip *chip); + + /* set LED brightness */ + int (*brightness_fn)(struct lp55xx_led *led); + + /* set multicolor LED brightness */ + int (*multicolor_brightness_fn)(struct lp55xx_led *led); + + /* current setting function */ + void (*set_led_current) (struct lp55xx_led *led, u8 led_current); + + /* access program memory when the firmware is loaded */ + void (*firmware_cb)(struct lp55xx_chip *chip); + + /* used for running firmware LED patterns */ + void (*run_engine) (struct lp55xx_chip *chip, bool start); + + /* additional device specific attributes */ + const struct attribute_group *dev_attr_group; +}; + +/* + * struct lp55xx_engine + * @mode : Engine mode + * @led_mux : Mux bits for LED selection. Only used in LP5523 + */ +struct lp55xx_engine { + enum lp55xx_engine_mode mode; + u16 led_mux; +}; + +/* + * struct lp55xx_chip + * @cl : I2C communication for access registers + * @pdata : Platform specific data + * @lock : Lock for user-space interface + * @num_leds : Number of registered LEDs + * @cfg : Device specific configuration data + * @engine_idx : Selected engine number + * @engines : Engine structure for the device attribute R/W interface + * @fw : Firmware data for running a LED pattern + */ +struct lp55xx_chip { + struct i2c_client *cl; + struct clk *clk; + struct lp55xx_platform_data *pdata; + struct mutex lock; /* lock for user-space interface */ + int num_leds; + struct lp55xx_device_config *cfg; + enum lp55xx_engine_index engine_idx; + struct lp55xx_engine engines[LP55XX_ENGINE_MAX]; + const struct firmware *fw; +}; + +/* + * struct lp55xx_led + * @chan_nr : Channel number + * @cdev : LED class device + * @mc_cdev : Multi color class device + * @color_components: Multi color LED map information + * @led_current : Current setting at each led channel + * @max_current : Maximun current at each led channel + * @brightness : Brightness value + * @chip : The lp55xx chip data + */ +struct lp55xx_led { + int chan_nr; + struct led_classdev cdev; + struct led_classdev_mc mc_cdev; + u8 led_current; + u8 max_current; + u8 brightness; + struct lp55xx_chip *chip; +}; + +/* register access */ +extern int lp55xx_write(struct lp55xx_chip *chip, u8 reg, u8 val); +extern int lp55xx_read(struct lp55xx_chip *chip, u8 reg, u8 *val); +extern int lp55xx_update_bits(struct lp55xx_chip *chip, u8 reg, + u8 mask, u8 val); + +/* external clock detection */ +extern bool lp55xx_is_extclk_used(struct lp55xx_chip *chip); + +/* common device init/deinit functions */ +extern int lp55xx_init_device(struct lp55xx_chip *chip); +extern void lp55xx_deinit_device(struct lp55xx_chip *chip); + +/* common LED class device functions */ +extern int lp55xx_register_leds(struct lp55xx_led *led, + struct lp55xx_chip *chip); + +/* common device attributes functions */ +extern int lp55xx_register_sysfs(struct lp55xx_chip *chip); +extern void lp55xx_unregister_sysfs(struct lp55xx_chip *chip); + +/* common device tree population function */ +extern struct lp55xx_platform_data +*lp55xx_of_populate_pdata(struct device *dev, struct device_node *np, + struct lp55xx_chip *chip); + +#endif /* _LEDS_LP55XX_COMMON_H */ diff --git a/drivers/leds/leds-lp8501.c b/drivers/leds/leds-lp8501.c new file mode 100644 index 000000000..2d2fda2ab --- /dev/null +++ b/drivers/leds/leds-lp8501.c @@ -0,0 +1,406 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * TI LP8501 9 channel LED Driver + * + * Copyright (C) 2013 Texas Instruments + * + * Author: Milo(Woogyom) Kim <milo.kim@ti.com> + */ + +#include <linux/delay.h> +#include <linux/firmware.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/platform_data/leds-lp55xx.h> +#include <linux/slab.h> + +#include "leds-lp55xx-common.h" + +#define LP8501_PROGRAM_LENGTH 32 +#define LP8501_MAX_LEDS 9 + +/* Registers */ +#define LP8501_REG_ENABLE 0x00 +#define LP8501_ENABLE BIT(6) +#define LP8501_EXEC_M 0x3F +#define LP8501_EXEC_ENG1_M 0x30 +#define LP8501_EXEC_ENG2_M 0x0C +#define LP8501_EXEC_ENG3_M 0x03 +#define LP8501_RUN_ENG1 0x20 +#define LP8501_RUN_ENG2 0x08 +#define LP8501_RUN_ENG3 0x02 + +#define LP8501_REG_OP_MODE 0x01 +#define LP8501_MODE_ENG1_M 0x30 +#define LP8501_MODE_ENG2_M 0x0C +#define LP8501_MODE_ENG3_M 0x03 +#define LP8501_LOAD_ENG1 0x10 +#define LP8501_LOAD_ENG2 0x04 +#define LP8501_LOAD_ENG3 0x01 + +#define LP8501_REG_PWR_CONFIG 0x05 +#define LP8501_PWR_CONFIG_M 0x03 + +#define LP8501_REG_LED_PWM_BASE 0x16 + +#define LP8501_REG_LED_CURRENT_BASE 0x26 + +#define LP8501_REG_CONFIG 0x36 +#define LP8501_PWM_PSAVE BIT(7) +#define LP8501_AUTO_INC BIT(6) +#define LP8501_PWR_SAVE BIT(5) +#define LP8501_CP_AUTO 0x18 +#define LP8501_INT_CLK BIT(0) +#define LP8501_DEFAULT_CFG \ + (LP8501_PWM_PSAVE | LP8501_AUTO_INC | LP8501_PWR_SAVE | LP8501_CP_AUTO) + +#define LP8501_REG_RESET 0x3D +#define LP8501_RESET 0xFF + +#define LP8501_REG_PROG_PAGE_SEL 0x4F +#define LP8501_PAGE_ENG1 0 +#define LP8501_PAGE_ENG2 1 +#define LP8501_PAGE_ENG3 2 + +#define LP8501_REG_PROG_MEM 0x50 + +#define LP8501_ENG1_IS_LOADING(mode) \ + ((mode & LP8501_MODE_ENG1_M) == LP8501_LOAD_ENG1) +#define LP8501_ENG2_IS_LOADING(mode) \ + ((mode & LP8501_MODE_ENG2_M) == LP8501_LOAD_ENG2) +#define LP8501_ENG3_IS_LOADING(mode) \ + ((mode & LP8501_MODE_ENG3_M) == LP8501_LOAD_ENG3) + +static inline void lp8501_wait_opmode_done(void) +{ + usleep_range(1000, 2000); +} + +static void lp8501_set_led_current(struct lp55xx_led *led, u8 led_current) +{ + led->led_current = led_current; + lp55xx_write(led->chip, LP8501_REG_LED_CURRENT_BASE + led->chan_nr, + led_current); +} + +static int lp8501_post_init_device(struct lp55xx_chip *chip) +{ + int ret; + u8 val = LP8501_DEFAULT_CFG; + + ret = lp55xx_write(chip, LP8501_REG_ENABLE, LP8501_ENABLE); + if (ret) + return ret; + + /* Chip startup time is 500 us, 1 - 2 ms gives some margin */ + usleep_range(1000, 2000); + + if (chip->pdata->clock_mode != LP55XX_CLOCK_EXT) + val |= LP8501_INT_CLK; + + ret = lp55xx_write(chip, LP8501_REG_CONFIG, val); + if (ret) + return ret; + + /* Power selection for each output */ + return lp55xx_update_bits(chip, LP8501_REG_PWR_CONFIG, + LP8501_PWR_CONFIG_M, chip->pdata->pwr_sel); +} + +static void lp8501_load_engine(struct lp55xx_chip *chip) +{ + enum lp55xx_engine_index idx = chip->engine_idx; + static const u8 mask[] = { + [LP55XX_ENGINE_1] = LP8501_MODE_ENG1_M, + [LP55XX_ENGINE_2] = LP8501_MODE_ENG2_M, + [LP55XX_ENGINE_3] = LP8501_MODE_ENG3_M, + }; + + static const u8 val[] = { + [LP55XX_ENGINE_1] = LP8501_LOAD_ENG1, + [LP55XX_ENGINE_2] = LP8501_LOAD_ENG2, + [LP55XX_ENGINE_3] = LP8501_LOAD_ENG3, + }; + + static const u8 page_sel[] = { + [LP55XX_ENGINE_1] = LP8501_PAGE_ENG1, + [LP55XX_ENGINE_2] = LP8501_PAGE_ENG2, + [LP55XX_ENGINE_3] = LP8501_PAGE_ENG3, + }; + + lp55xx_update_bits(chip, LP8501_REG_OP_MODE, mask[idx], val[idx]); + + lp8501_wait_opmode_done(); + + lp55xx_write(chip, LP8501_REG_PROG_PAGE_SEL, page_sel[idx]); +} + +static void lp8501_stop_engine(struct lp55xx_chip *chip) +{ + lp55xx_write(chip, LP8501_REG_OP_MODE, 0); + lp8501_wait_opmode_done(); +} + +static void lp8501_turn_off_channels(struct lp55xx_chip *chip) +{ + int i; + + for (i = 0; i < LP8501_MAX_LEDS; i++) + lp55xx_write(chip, LP8501_REG_LED_PWM_BASE + i, 0); +} + +static void lp8501_run_engine(struct lp55xx_chip *chip, bool start) +{ + int ret; + u8 mode; + u8 exec; + + /* stop engine */ + if (!start) { + lp8501_stop_engine(chip); + lp8501_turn_off_channels(chip); + return; + } + + /* + * To run the engine, + * operation mode and enable register should updated at the same time + */ + + ret = lp55xx_read(chip, LP8501_REG_OP_MODE, &mode); + if (ret) + return; + + ret = lp55xx_read(chip, LP8501_REG_ENABLE, &exec); + if (ret) + return; + + /* change operation mode to RUN only when each engine is loading */ + if (LP8501_ENG1_IS_LOADING(mode)) { + mode = (mode & ~LP8501_MODE_ENG1_M) | LP8501_RUN_ENG1; + exec = (exec & ~LP8501_EXEC_ENG1_M) | LP8501_RUN_ENG1; + } + + if (LP8501_ENG2_IS_LOADING(mode)) { + mode = (mode & ~LP8501_MODE_ENG2_M) | LP8501_RUN_ENG2; + exec = (exec & ~LP8501_EXEC_ENG2_M) | LP8501_RUN_ENG2; + } + + if (LP8501_ENG3_IS_LOADING(mode)) { + mode = (mode & ~LP8501_MODE_ENG3_M) | LP8501_RUN_ENG3; + exec = (exec & ~LP8501_EXEC_ENG3_M) | LP8501_RUN_ENG3; + } + + lp55xx_write(chip, LP8501_REG_OP_MODE, mode); + lp8501_wait_opmode_done(); + + lp55xx_update_bits(chip, LP8501_REG_ENABLE, LP8501_EXEC_M, exec); +} + +static int lp8501_update_program_memory(struct lp55xx_chip *chip, + const u8 *data, size_t size) +{ + u8 pattern[LP8501_PROGRAM_LENGTH] = {0}; + unsigned cmd; + char c[3]; + int update_size; + int nrchars; + int offset = 0; + int ret; + int i; + + /* clear program memory before updating */ + for (i = 0; i < LP8501_PROGRAM_LENGTH; i++) + lp55xx_write(chip, LP8501_REG_PROG_MEM + i, 0); + + i = 0; + while ((offset < size - 1) && (i < LP8501_PROGRAM_LENGTH)) { + /* separate sscanfs because length is working only for %s */ + ret = sscanf(data + offset, "%2s%n ", c, &nrchars); + if (ret != 1) + goto err; + + ret = sscanf(c, "%2x", &cmd); + if (ret != 1) + goto err; + + pattern[i] = (u8)cmd; + offset += nrchars; + i++; + } + + /* Each instruction is 16bit long. Check that length is even */ + if (i % 2) + goto err; + + update_size = i; + for (i = 0; i < update_size; i++) + lp55xx_write(chip, LP8501_REG_PROG_MEM + i, pattern[i]); + + return 0; + +err: + dev_err(&chip->cl->dev, "wrong pattern format\n"); + return -EINVAL; +} + +static void lp8501_firmware_loaded(struct lp55xx_chip *chip) +{ + const struct firmware *fw = chip->fw; + + if (fw->size > LP8501_PROGRAM_LENGTH) { + dev_err(&chip->cl->dev, "firmware data size overflow: %zu\n", + fw->size); + return; + } + + /* + * Program memory sequence + * 1) set engine mode to "LOAD" + * 2) write firmware data into program memory + */ + + lp8501_load_engine(chip); + lp8501_update_program_memory(chip, fw->data, fw->size); +} + +static int lp8501_led_brightness(struct lp55xx_led *led) +{ + struct lp55xx_chip *chip = led->chip; + int ret; + + mutex_lock(&chip->lock); + ret = lp55xx_write(chip, LP8501_REG_LED_PWM_BASE + led->chan_nr, + led->brightness); + mutex_unlock(&chip->lock); + + return ret; +} + +/* Chip specific configurations */ +static struct lp55xx_device_config lp8501_cfg = { + .reset = { + .addr = LP8501_REG_RESET, + .val = LP8501_RESET, + }, + .enable = { + .addr = LP8501_REG_ENABLE, + .val = LP8501_ENABLE, + }, + .max_channel = LP8501_MAX_LEDS, + .post_init_device = lp8501_post_init_device, + .brightness_fn = lp8501_led_brightness, + .set_led_current = lp8501_set_led_current, + .firmware_cb = lp8501_firmware_loaded, + .run_engine = lp8501_run_engine, +}; + +static int lp8501_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + int ret; + struct lp55xx_chip *chip; + struct lp55xx_led *led; + struct lp55xx_platform_data *pdata = dev_get_platdata(&client->dev); + struct device_node *np = dev_of_node(&client->dev); + + chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + chip->cfg = &lp8501_cfg; + + if (!pdata) { + if (np) { + pdata = lp55xx_of_populate_pdata(&client->dev, np, + chip); + if (IS_ERR(pdata)) + return PTR_ERR(pdata); + } else { + dev_err(&client->dev, "no platform data\n"); + return -EINVAL; + } + } + + led = devm_kcalloc(&client->dev, + pdata->num_channels, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + chip->cl = client; + chip->pdata = pdata; + + mutex_init(&chip->lock); + + i2c_set_clientdata(client, led); + + ret = lp55xx_init_device(chip); + if (ret) + goto err_init; + + dev_info(&client->dev, "%s Programmable led chip found\n", id->name); + + ret = lp55xx_register_leds(led, chip); + if (ret) + goto err_out; + + ret = lp55xx_register_sysfs(chip); + if (ret) { + dev_err(&client->dev, "registering sysfs failed\n"); + goto err_out; + } + + return 0; + +err_out: + lp55xx_deinit_device(chip); +err_init: + return ret; +} + +static int lp8501_remove(struct i2c_client *client) +{ + struct lp55xx_led *led = i2c_get_clientdata(client); + struct lp55xx_chip *chip = led->chip; + + lp8501_stop_engine(chip); + lp55xx_unregister_sysfs(chip); + lp55xx_deinit_device(chip); + + return 0; +} + +static const struct i2c_device_id lp8501_id[] = { + { "lp8501", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, lp8501_id); + +#ifdef CONFIG_OF +static const struct of_device_id of_lp8501_leds_match[] = { + { .compatible = "ti,lp8501", }, + {}, +}; + +MODULE_DEVICE_TABLE(of, of_lp8501_leds_match); +#endif + +static struct i2c_driver lp8501_driver = { + .driver = { + .name = "lp8501", + .of_match_table = of_match_ptr(of_lp8501_leds_match), + }, + .probe = lp8501_probe, + .remove = lp8501_remove, + .id_table = lp8501_id, +}; + +module_i2c_driver(lp8501_driver); + +MODULE_DESCRIPTION("Texas Instruments LP8501 LED driver"); +MODULE_AUTHOR("Milo Kim"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-lp8788.c b/drivers/leds/leds-lp8788.c new file mode 100644 index 000000000..9b9525ccc --- /dev/null +++ b/drivers/leds/leds-lp8788.c @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * TI LP8788 MFD - keyled driver + * + * Copyright 2012 Texas Instruments + * + * Author: Milo(Woogyom) Kim <milo.kim@ti.com> + */ + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/err.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/mutex.h> +#include <linux/mfd/lp8788.h> +#include <linux/mfd/lp8788-isink.h> + +#define MAX_BRIGHTNESS LP8788_ISINK_MAX_PWM +#define DEFAULT_LED_NAME "keyboard-backlight" + +struct lp8788_led { + struct lp8788 *lp; + struct mutex lock; + struct led_classdev led_dev; + enum lp8788_isink_number isink_num; + int on; +}; + +struct lp8788_led_config { + enum lp8788_isink_scale scale; + enum lp8788_isink_number num; + int iout; +}; + +static struct lp8788_led_config default_led_config = { + .scale = LP8788_ISINK_SCALE_100mA, + .num = LP8788_ISINK_3, + .iout = 0, +}; + +static int lp8788_led_init_device(struct lp8788_led *led, + struct lp8788_led_platform_data *pdata) +{ + struct lp8788_led_config *cfg = &default_led_config; + u8 addr, mask, val; + int ret; + + if (pdata) { + cfg->scale = pdata->scale; + cfg->num = pdata->num; + cfg->iout = pdata->iout_code; + } + + led->isink_num = cfg->num; + + /* scale configuration */ + addr = LP8788_ISINK_CTRL; + mask = 1 << (cfg->num + LP8788_ISINK_SCALE_OFFSET); + val = cfg->scale << (cfg->num + LP8788_ISINK_SCALE_OFFSET); + ret = lp8788_update_bits(led->lp, addr, mask, val); + if (ret) + return ret; + + /* current configuration */ + addr = lp8788_iout_addr[cfg->num]; + mask = lp8788_iout_mask[cfg->num]; + val = cfg->iout; + + return lp8788_update_bits(led->lp, addr, mask, val); +} + +static int lp8788_led_enable(struct lp8788_led *led, + enum lp8788_isink_number num, int on) +{ + int ret; + + u8 mask = 1 << num; + u8 val = on << num; + + ret = lp8788_update_bits(led->lp, LP8788_ISINK_CTRL, mask, val); + if (ret == 0) + led->on = on; + + return ret; +} + +static int lp8788_brightness_set(struct led_classdev *led_cdev, + enum led_brightness val) +{ + struct lp8788_led *led = + container_of(led_cdev, struct lp8788_led, led_dev); + + enum lp8788_isink_number num = led->isink_num; + int enable, ret; + + mutex_lock(&led->lock); + + switch (num) { + case LP8788_ISINK_1: + case LP8788_ISINK_2: + case LP8788_ISINK_3: + ret = lp8788_write_byte(led->lp, lp8788_pwm_addr[num], val); + if (ret < 0) + goto unlock; + break; + default: + mutex_unlock(&led->lock); + return -EINVAL; + } + + enable = (val > 0) ? 1 : 0; + if (enable != led->on) + ret = lp8788_led_enable(led, num, enable); +unlock: + mutex_unlock(&led->lock); + return ret; +} + +static int lp8788_led_probe(struct platform_device *pdev) +{ + struct lp8788 *lp = dev_get_drvdata(pdev->dev.parent); + struct lp8788_led_platform_data *led_pdata; + struct lp8788_led *led; + struct device *dev = &pdev->dev; + int ret; + + led = devm_kzalloc(dev, sizeof(struct lp8788_led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->lp = lp; + led->led_dev.max_brightness = MAX_BRIGHTNESS; + led->led_dev.brightness_set_blocking = lp8788_brightness_set; + + led_pdata = lp->pdata ? lp->pdata->led_pdata : NULL; + + if (!led_pdata || !led_pdata->name) + led->led_dev.name = DEFAULT_LED_NAME; + else + led->led_dev.name = led_pdata->name; + + mutex_init(&led->lock); + + ret = lp8788_led_init_device(led, led_pdata); + if (ret) { + dev_err(dev, "led init device err: %d\n", ret); + return ret; + } + + ret = devm_led_classdev_register(dev, &led->led_dev); + if (ret) { + dev_err(dev, "led register err: %d\n", ret); + return ret; + } + + return 0; +} + +static struct platform_driver lp8788_led_driver = { + .probe = lp8788_led_probe, + .driver = { + .name = LP8788_DEV_KEYLED, + }, +}; +module_platform_driver(lp8788_led_driver); + +MODULE_DESCRIPTION("Texas Instruments LP8788 Keyboard LED Driver"); +MODULE_AUTHOR("Milo Kim"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:lp8788-keyled"); diff --git a/drivers/leds/leds-lp8860.c b/drivers/leds/leds-lp8860.c new file mode 100644 index 000000000..f0533a337 --- /dev/null +++ b/drivers/leds/leds-lp8860.c @@ -0,0 +1,493 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * TI LP8860 4-Channel LED Driver + * + * Copyright (C) 2014 Texas Instruments + * + * Author: Dan Murphy <dmurphy@ti.com> + */ + +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/leds.h> +#include <linux/regmap.h> +#include <linux/regulator/consumer.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/of_gpio.h> +#include <linux/gpio/consumer.h> +#include <linux/slab.h> + +#define LP8860_DISP_CL1_BRT_MSB 0x00 +#define LP8860_DISP_CL1_BRT_LSB 0x01 +#define LP8860_DISP_CL1_CURR_MSB 0x02 +#define LP8860_DISP_CL1_CURR_LSB 0x03 +#define LP8860_CL2_BRT_MSB 0x04 +#define LP8860_CL2_BRT_LSB 0x05 +#define LP8860_CL2_CURRENT 0x06 +#define LP8860_CL3_BRT_MSB 0x07 +#define LP8860_CL3_BRT_LSB 0x08 +#define LP8860_CL3_CURRENT 0x09 +#define LP8860_CL4_BRT_MSB 0x0a +#define LP8860_CL4_BRT_LSB 0x0b +#define LP8860_CL4_CURRENT 0x0c +#define LP8860_CONFIG 0x0d +#define LP8860_STATUS 0x0e +#define LP8860_FAULT 0x0f +#define LP8860_LED_FAULT 0x10 +#define LP8860_FAULT_CLEAR 0x11 +#define LP8860_ID 0x12 +#define LP8860_TEMP_MSB 0x13 +#define LP8860_TEMP_LSB 0x14 +#define LP8860_DISP_LED_CURR_MSB 0x15 +#define LP8860_DISP_LED_CURR_LSB 0x16 +#define LP8860_DISP_LED_PWM_MSB 0x17 +#define LP8860_DISP_LED_PWM_LSB 0x18 +#define LP8860_EEPROM_CNTRL 0x19 +#define LP8860_EEPROM_UNLOCK 0x1a + +#define LP8860_EEPROM_REG_0 0x60 +#define LP8860_EEPROM_REG_1 0x61 +#define LP8860_EEPROM_REG_2 0x62 +#define LP8860_EEPROM_REG_3 0x63 +#define LP8860_EEPROM_REG_4 0x64 +#define LP8860_EEPROM_REG_5 0x65 +#define LP8860_EEPROM_REG_6 0x66 +#define LP8860_EEPROM_REG_7 0x67 +#define LP8860_EEPROM_REG_8 0x68 +#define LP8860_EEPROM_REG_9 0x69 +#define LP8860_EEPROM_REG_10 0x6a +#define LP8860_EEPROM_REG_11 0x6b +#define LP8860_EEPROM_REG_12 0x6c +#define LP8860_EEPROM_REG_13 0x6d +#define LP8860_EEPROM_REG_14 0x6e +#define LP8860_EEPROM_REG_15 0x6f +#define LP8860_EEPROM_REG_16 0x70 +#define LP8860_EEPROM_REG_17 0x71 +#define LP8860_EEPROM_REG_18 0x72 +#define LP8860_EEPROM_REG_19 0x73 +#define LP8860_EEPROM_REG_20 0x74 +#define LP8860_EEPROM_REG_21 0x75 +#define LP8860_EEPROM_REG_22 0x76 +#define LP8860_EEPROM_REG_23 0x77 +#define LP8860_EEPROM_REG_24 0x78 + +#define LP8860_LOCK_EEPROM 0x00 +#define LP8860_UNLOCK_EEPROM 0x01 +#define LP8860_PROGRAM_EEPROM 0x02 +#define LP8860_EEPROM_CODE_1 0x08 +#define LP8860_EEPROM_CODE_2 0xba +#define LP8860_EEPROM_CODE_3 0xef + +#define LP8860_CLEAR_FAULTS 0x01 + +#define LP8860_NAME "lp8860" + +/** + * struct lp8860_led - + * @lock - Lock for reading/writing the device + * @client - Pointer to the I2C client + * @led_dev - led class device pointer + * @regmap - Devices register map + * @eeprom_regmap - EEPROM register map + * @enable_gpio - VDDIO/EN gpio to enable communication interface + * @regulator - LED supply regulator pointer + */ +struct lp8860_led { + struct mutex lock; + struct i2c_client *client; + struct led_classdev led_dev; + struct regmap *regmap; + struct regmap *eeprom_regmap; + struct gpio_desc *enable_gpio; + struct regulator *regulator; +}; + +struct lp8860_eeprom_reg { + uint8_t reg; + uint8_t value; +}; + +static struct lp8860_eeprom_reg lp8860_eeprom_disp_regs[] = { + { LP8860_EEPROM_REG_0, 0xed }, + { LP8860_EEPROM_REG_1, 0xdf }, + { LP8860_EEPROM_REG_2, 0xdc }, + { LP8860_EEPROM_REG_3, 0xf0 }, + { LP8860_EEPROM_REG_4, 0xdf }, + { LP8860_EEPROM_REG_5, 0xe5 }, + { LP8860_EEPROM_REG_6, 0xf2 }, + { LP8860_EEPROM_REG_7, 0x77 }, + { LP8860_EEPROM_REG_8, 0x77 }, + { LP8860_EEPROM_REG_9, 0x71 }, + { LP8860_EEPROM_REG_10, 0x3f }, + { LP8860_EEPROM_REG_11, 0xb7 }, + { LP8860_EEPROM_REG_12, 0x17 }, + { LP8860_EEPROM_REG_13, 0xef }, + { LP8860_EEPROM_REG_14, 0xb0 }, + { LP8860_EEPROM_REG_15, 0x87 }, + { LP8860_EEPROM_REG_16, 0xce }, + { LP8860_EEPROM_REG_17, 0x72 }, + { LP8860_EEPROM_REG_18, 0xe5 }, + { LP8860_EEPROM_REG_19, 0xdf }, + { LP8860_EEPROM_REG_20, 0x35 }, + { LP8860_EEPROM_REG_21, 0x06 }, + { LP8860_EEPROM_REG_22, 0xdc }, + { LP8860_EEPROM_REG_23, 0x88 }, + { LP8860_EEPROM_REG_24, 0x3E }, +}; + +static int lp8860_unlock_eeprom(struct lp8860_led *led, int lock) +{ + int ret; + + mutex_lock(&led->lock); + + if (lock == LP8860_UNLOCK_EEPROM) { + ret = regmap_write(led->regmap, + LP8860_EEPROM_UNLOCK, + LP8860_EEPROM_CODE_1); + if (ret) { + dev_err(&led->client->dev, "EEPROM Unlock failed\n"); + goto out; + } + + ret = regmap_write(led->regmap, + LP8860_EEPROM_UNLOCK, + LP8860_EEPROM_CODE_2); + if (ret) { + dev_err(&led->client->dev, "EEPROM Unlock failed\n"); + goto out; + } + ret = regmap_write(led->regmap, + LP8860_EEPROM_UNLOCK, + LP8860_EEPROM_CODE_3); + if (ret) { + dev_err(&led->client->dev, "EEPROM Unlock failed\n"); + goto out; + } + } else { + ret = regmap_write(led->regmap, + LP8860_EEPROM_UNLOCK, + LP8860_LOCK_EEPROM); + } + +out: + mutex_unlock(&led->lock); + return ret; +} + +static int lp8860_fault_check(struct lp8860_led *led) +{ + int ret, fault; + unsigned int read_buf; + + ret = regmap_read(led->regmap, LP8860_LED_FAULT, &read_buf); + if (ret) + goto out; + + fault = read_buf; + + ret = regmap_read(led->regmap, LP8860_FAULT, &read_buf); + if (ret) + goto out; + + fault |= read_buf; + + /* Attempt to clear any faults */ + if (fault) + ret = regmap_write(led->regmap, LP8860_FAULT_CLEAR, + LP8860_CLEAR_FAULTS); +out: + return ret; +} + +static int lp8860_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brt_val) +{ + struct lp8860_led *led = + container_of(led_cdev, struct lp8860_led, led_dev); + int disp_brightness = brt_val * 255; + int ret; + + mutex_lock(&led->lock); + + ret = lp8860_fault_check(led); + if (ret) { + dev_err(&led->client->dev, "Cannot read/clear faults\n"); + goto out; + } + + ret = regmap_write(led->regmap, LP8860_DISP_CL1_BRT_MSB, + (disp_brightness & 0xff00) >> 8); + if (ret) { + dev_err(&led->client->dev, "Cannot write CL1 MSB\n"); + goto out; + } + + ret = regmap_write(led->regmap, LP8860_DISP_CL1_BRT_LSB, + disp_brightness & 0xff); + if (ret) { + dev_err(&led->client->dev, "Cannot write CL1 LSB\n"); + goto out; + } +out: + mutex_unlock(&led->lock); + return ret; +} + +static int lp8860_init(struct lp8860_led *led) +{ + unsigned int read_buf; + int ret, i, reg_count; + + if (led->regulator) { + ret = regulator_enable(led->regulator); + if (ret) { + dev_err(&led->client->dev, + "Failed to enable regulator\n"); + return ret; + } + } + + if (led->enable_gpio) + gpiod_direction_output(led->enable_gpio, 1); + + ret = lp8860_fault_check(led); + if (ret) + goto out; + + ret = regmap_read(led->regmap, LP8860_STATUS, &read_buf); + if (ret) + goto out; + + ret = lp8860_unlock_eeprom(led, LP8860_UNLOCK_EEPROM); + if (ret) { + dev_err(&led->client->dev, "Failed unlocking EEPROM\n"); + goto out; + } + + reg_count = ARRAY_SIZE(lp8860_eeprom_disp_regs) / sizeof(lp8860_eeprom_disp_regs[0]); + for (i = 0; i < reg_count; i++) { + ret = regmap_write(led->eeprom_regmap, + lp8860_eeprom_disp_regs[i].reg, + lp8860_eeprom_disp_regs[i].value); + if (ret) { + dev_err(&led->client->dev, "Failed writing EEPROM\n"); + goto out; + } + } + + ret = lp8860_unlock_eeprom(led, LP8860_LOCK_EEPROM); + if (ret) + goto out; + + ret = regmap_write(led->regmap, + LP8860_EEPROM_CNTRL, + LP8860_PROGRAM_EEPROM); + if (ret) { + dev_err(&led->client->dev, "Failed programming EEPROM\n"); + goto out; + } + + return ret; + +out: + if (ret) + if (led->enable_gpio) + gpiod_direction_output(led->enable_gpio, 0); + + if (led->regulator) { + ret = regulator_disable(led->regulator); + if (ret) + dev_err(&led->client->dev, + "Failed to disable regulator\n"); + } + + return ret; +} + +static const struct reg_default lp8860_reg_defs[] = { + { LP8860_DISP_CL1_BRT_MSB, 0x00}, + { LP8860_DISP_CL1_BRT_LSB, 0x00}, + { LP8860_DISP_CL1_CURR_MSB, 0x00}, + { LP8860_DISP_CL1_CURR_LSB, 0x00}, + { LP8860_CL2_BRT_MSB, 0x00}, + { LP8860_CL2_BRT_LSB, 0x00}, + { LP8860_CL2_CURRENT, 0x00}, + { LP8860_CL3_BRT_MSB, 0x00}, + { LP8860_CL3_BRT_LSB, 0x00}, + { LP8860_CL3_CURRENT, 0x00}, + { LP8860_CL4_BRT_MSB, 0x00}, + { LP8860_CL4_BRT_LSB, 0x00}, + { LP8860_CL4_CURRENT, 0x00}, + { LP8860_CONFIG, 0x00}, + { LP8860_FAULT_CLEAR, 0x00}, + { LP8860_EEPROM_CNTRL, 0x80}, + { LP8860_EEPROM_UNLOCK, 0x00}, +}; + +static const struct regmap_config lp8860_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = LP8860_EEPROM_UNLOCK, + .reg_defaults = lp8860_reg_defs, + .num_reg_defaults = ARRAY_SIZE(lp8860_reg_defs), + .cache_type = REGCACHE_NONE, +}; + +static const struct reg_default lp8860_eeprom_defs[] = { + { LP8860_EEPROM_REG_0, 0x00 }, + { LP8860_EEPROM_REG_1, 0x00 }, + { LP8860_EEPROM_REG_2, 0x00 }, + { LP8860_EEPROM_REG_3, 0x00 }, + { LP8860_EEPROM_REG_4, 0x00 }, + { LP8860_EEPROM_REG_5, 0x00 }, + { LP8860_EEPROM_REG_6, 0x00 }, + { LP8860_EEPROM_REG_7, 0x00 }, + { LP8860_EEPROM_REG_8, 0x00 }, + { LP8860_EEPROM_REG_9, 0x00 }, + { LP8860_EEPROM_REG_10, 0x00 }, + { LP8860_EEPROM_REG_11, 0x00 }, + { LP8860_EEPROM_REG_12, 0x00 }, + { LP8860_EEPROM_REG_13, 0x00 }, + { LP8860_EEPROM_REG_14, 0x00 }, + { LP8860_EEPROM_REG_15, 0x00 }, + { LP8860_EEPROM_REG_16, 0x00 }, + { LP8860_EEPROM_REG_17, 0x00 }, + { LP8860_EEPROM_REG_18, 0x00 }, + { LP8860_EEPROM_REG_19, 0x00 }, + { LP8860_EEPROM_REG_20, 0x00 }, + { LP8860_EEPROM_REG_21, 0x00 }, + { LP8860_EEPROM_REG_22, 0x00 }, + { LP8860_EEPROM_REG_23, 0x00 }, + { LP8860_EEPROM_REG_24, 0x00 }, +}; + +static const struct regmap_config lp8860_eeprom_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + + .max_register = LP8860_EEPROM_REG_24, + .reg_defaults = lp8860_eeprom_defs, + .num_reg_defaults = ARRAY_SIZE(lp8860_eeprom_defs), + .cache_type = REGCACHE_NONE, +}; + +static int lp8860_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + int ret; + struct lp8860_led *led; + struct device_node *np = dev_of_node(&client->dev); + struct device_node *child_node; + struct led_init_data init_data = {}; + + led = devm_kzalloc(&client->dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + child_node = of_get_next_available_child(np, NULL); + if (!child_node) + return -EINVAL; + + led->enable_gpio = devm_gpiod_get_optional(&client->dev, + "enable", GPIOD_OUT_LOW); + if (IS_ERR(led->enable_gpio)) { + ret = PTR_ERR(led->enable_gpio); + dev_err(&client->dev, "Failed to get enable gpio: %d\n", ret); + return ret; + } + + led->regulator = devm_regulator_get(&client->dev, "vled"); + if (IS_ERR(led->regulator)) + led->regulator = NULL; + + led->client = client; + led->led_dev.brightness_set_blocking = lp8860_brightness_set; + + mutex_init(&led->lock); + + i2c_set_clientdata(client, led); + + led->regmap = devm_regmap_init_i2c(client, &lp8860_regmap_config); + if (IS_ERR(led->regmap)) { + ret = PTR_ERR(led->regmap); + dev_err(&client->dev, "Failed to allocate register map: %d\n", + ret); + return ret; + } + + led->eeprom_regmap = devm_regmap_init_i2c(client, &lp8860_eeprom_regmap_config); + if (IS_ERR(led->eeprom_regmap)) { + ret = PTR_ERR(led->eeprom_regmap); + dev_err(&client->dev, "Failed to allocate register map: %d\n", + ret); + return ret; + } + + ret = lp8860_init(led); + if (ret) + return ret; + + init_data.fwnode = of_fwnode_handle(child_node); + init_data.devicename = LP8860_NAME; + init_data.default_label = ":display_cluster"; + + ret = devm_led_classdev_register_ext(&client->dev, &led->led_dev, + &init_data); + if (ret) { + dev_err(&client->dev, "led register err: %d\n", ret); + return ret; + } + + return 0; +} + +static int lp8860_remove(struct i2c_client *client) +{ + struct lp8860_led *led = i2c_get_clientdata(client); + int ret; + + if (led->enable_gpio) + gpiod_direction_output(led->enable_gpio, 0); + + if (led->regulator) { + ret = regulator_disable(led->regulator); + if (ret) + dev_err(&led->client->dev, + "Failed to disable regulator\n"); + } + + mutex_destroy(&led->lock); + + return 0; +} + +static const struct i2c_device_id lp8860_id[] = { + { "lp8860", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, lp8860_id); + +static const struct of_device_id of_lp8860_leds_match[] = { + { .compatible = "ti,lp8860", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_lp8860_leds_match); + +static struct i2c_driver lp8860_driver = { + .driver = { + .name = "lp8860", + .of_match_table = of_lp8860_leds_match, + }, + .probe = lp8860_probe, + .remove = lp8860_remove, + .id_table = lp8860_id, +}; +module_i2c_driver(lp8860_driver); + +MODULE_DESCRIPTION("Texas Instruments LP8860 LED driver"); +MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-lt3593.c b/drivers/leds/leds-lt3593.c new file mode 100644 index 000000000..7dab08773 --- /dev/null +++ b/drivers/leds/leds-lt3593.c @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2009,2018 Daniel Mack <daniel@zonque.org> + +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/delay.h> +#include <linux/gpio/consumer.h> +#include <linux/slab.h> +#include <linux/module.h> +#include <linux/of.h> + +#define LED_LT3593_NAME "lt3593" + +struct lt3593_led_data { + struct led_classdev cdev; + struct gpio_desc *gpiod; +}; + +static int lt3593_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct lt3593_led_data *led_dat = + container_of(led_cdev, struct lt3593_led_data, cdev); + int pulses; + + /* + * The LT3593 resets its internal current level register to the maximum + * level on the first falling edge on the control pin. Each following + * falling edge decreases the current level by 625uA. Up to 32 pulses + * can be sent, so the maximum power reduction is 20mA. + * After a timeout of 128us, the value is taken from the register and + * applied is to the output driver. + */ + + if (value == 0) { + gpiod_set_value_cansleep(led_dat->gpiod, 0); + return 0; + } + + pulses = 32 - (value * 32) / 255; + + if (pulses == 0) { + gpiod_set_value_cansleep(led_dat->gpiod, 0); + mdelay(1); + gpiod_set_value_cansleep(led_dat->gpiod, 1); + return 0; + } + + gpiod_set_value_cansleep(led_dat->gpiod, 1); + + while (pulses--) { + gpiod_set_value_cansleep(led_dat->gpiod, 0); + udelay(1); + gpiod_set_value_cansleep(led_dat->gpiod, 1); + udelay(1); + } + + return 0; +} + +static int lt3593_led_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct lt3593_led_data *led_data; + struct fwnode_handle *child; + int ret, state = LEDS_GPIO_DEFSTATE_OFF; + struct led_init_data init_data = {}; + const char *tmp; + + if (!dev_of_node(dev)) + return -ENODEV; + + led_data = devm_kzalloc(dev, sizeof(*led_data), GFP_KERNEL); + if (!led_data) + return -ENOMEM; + + if (device_get_child_node_count(dev) != 1) { + dev_err(dev, "Device must have exactly one LED sub-node."); + return -EINVAL; + } + + led_data->gpiod = devm_gpiod_get(dev, "lltc,ctrl", 0); + if (IS_ERR(led_data->gpiod)) + return PTR_ERR(led_data->gpiod); + + child = device_get_next_child_node(dev, NULL); + + if (!fwnode_property_read_string(child, "default-state", &tmp)) { + if (!strcmp(tmp, "on")) + state = LEDS_GPIO_DEFSTATE_ON; + } + + led_data->cdev.brightness_set_blocking = lt3593_led_set; + led_data->cdev.brightness = state ? LED_FULL : LED_OFF; + + init_data.fwnode = child; + init_data.devicename = LED_LT3593_NAME; + init_data.default_label = ":"; + + ret = devm_led_classdev_register_ext(dev, &led_data->cdev, &init_data); + fwnode_handle_put(child); + if (ret < 0) + return ret; + + platform_set_drvdata(pdev, led_data); + + return 0; +} + +static const struct of_device_id of_lt3593_leds_match[] = { + { .compatible = "lltc,lt3593", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_lt3593_leds_match); + +static struct platform_driver lt3593_led_driver = { + .probe = lt3593_led_probe, + .driver = { + .name = "leds-lt3593", + .of_match_table = of_match_ptr(of_lt3593_leds_match), + }, +}; + +module_platform_driver(lt3593_led_driver); + +MODULE_AUTHOR("Daniel Mack <daniel@zonque.org>"); +MODULE_DESCRIPTION("LED driver for LT3593 controllers"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:leds-lt3593"); diff --git a/drivers/leds/leds-max77650.c b/drivers/leds/leds-max77650.c new file mode 100644 index 000000000..1eeac56b0 --- /dev/null +++ b/drivers/leds/leds-max77650.c @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Copyright (C) 2018 BayLibre SAS +// Author: Bartosz Golaszewski <bgolaszewski@baylibre.com> +// +// LED driver for MAXIM 77650/77651 charger/power-supply. + +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/mfd/max77650.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> + +#define MAX77650_LED_NUM_LEDS 3 + +#define MAX77650_LED_A_BASE 0x40 +#define MAX77650_LED_B_BASE 0x43 + +#define MAX77650_LED_BR_MASK GENMASK(4, 0) +#define MAX77650_LED_EN_MASK GENMASK(7, 6) + +#define MAX77650_LED_MAX_BRIGHTNESS MAX77650_LED_BR_MASK + +/* Enable EN_LED_MSTR. */ +#define MAX77650_LED_TOP_DEFAULT BIT(0) + +#define MAX77650_LED_ENABLE GENMASK(7, 6) +#define MAX77650_LED_DISABLE 0x00 + +#define MAX77650_LED_A_DEFAULT MAX77650_LED_DISABLE +/* 100% on duty */ +#define MAX77650_LED_B_DEFAULT GENMASK(3, 0) + +struct max77650_led { + struct led_classdev cdev; + struct regmap *map; + unsigned int regA; + unsigned int regB; +}; + +static struct max77650_led *max77650_to_led(struct led_classdev *cdev) +{ + return container_of(cdev, struct max77650_led, cdev); +} + +static int max77650_led_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct max77650_led *led = max77650_to_led(cdev); + int val, mask; + + mask = MAX77650_LED_BR_MASK | MAX77650_LED_EN_MASK; + + if (brightness == LED_OFF) + val = MAX77650_LED_DISABLE; + else + val = MAX77650_LED_ENABLE | brightness; + + return regmap_update_bits(led->map, led->regA, mask, val); +} + +static int max77650_led_probe(struct platform_device *pdev) +{ + struct fwnode_handle *child; + struct max77650_led *leds, *led; + struct device *dev; + struct regmap *map; + int rv, num_leds; + u32 reg; + + dev = &pdev->dev; + + leds = devm_kcalloc(dev, sizeof(*leds), + MAX77650_LED_NUM_LEDS, GFP_KERNEL); + if (!leds) + return -ENOMEM; + + map = dev_get_regmap(dev->parent, NULL); + if (!map) + return -ENODEV; + + num_leds = device_get_child_node_count(dev); + if (!num_leds || num_leds > MAX77650_LED_NUM_LEDS) + return -ENODEV; + + device_for_each_child_node(dev, child) { + struct led_init_data init_data = {}; + + rv = fwnode_property_read_u32(child, "reg", ®); + if (rv || reg >= MAX77650_LED_NUM_LEDS) { + rv = -EINVAL; + goto err_node_put; + } + + led = &leds[reg]; + led->map = map; + led->regA = MAX77650_LED_A_BASE + reg; + led->regB = MAX77650_LED_B_BASE + reg; + led->cdev.brightness_set_blocking = max77650_led_brightness_set; + led->cdev.max_brightness = MAX77650_LED_MAX_BRIGHTNESS; + + init_data.fwnode = child; + init_data.devicename = "max77650"; + /* for backwards compatibility if `label` is not present */ + init_data.default_label = ":"; + + rv = devm_led_classdev_register_ext(dev, &led->cdev, + &init_data); + if (rv) + goto err_node_put; + + rv = regmap_write(map, led->regA, MAX77650_LED_A_DEFAULT); + if (rv) + goto err_node_put; + + rv = regmap_write(map, led->regB, MAX77650_LED_B_DEFAULT); + if (rv) + goto err_node_put; + } + + return regmap_write(map, + MAX77650_REG_CNFG_LED_TOP, + MAX77650_LED_TOP_DEFAULT); +err_node_put: + fwnode_handle_put(child); + return rv; +} + +static const struct of_device_id max77650_led_of_match[] = { + { .compatible = "maxim,max77650-led" }, + { } +}; +MODULE_DEVICE_TABLE(of, max77650_led_of_match); + +static struct platform_driver max77650_led_driver = { + .driver = { + .name = "max77650-led", + .of_match_table = max77650_led_of_match, + }, + .probe = max77650_led_probe, +}; +module_platform_driver(max77650_led_driver); + +MODULE_DESCRIPTION("MAXIM 77650/77651 LED driver"); +MODULE_AUTHOR("Bartosz Golaszewski <bgolaszewski@baylibre.com>"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:max77650-led"); diff --git a/drivers/leds/leds-max77693.c b/drivers/leds/leds-max77693.c new file mode 100644 index 000000000..5c1faeb55 --- /dev/null +++ b/drivers/leds/leds-max77693.c @@ -0,0 +1,1059 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED Flash class driver for the flash cell of max77693 mfd. + * + * Copyright (C) 2015, Samsung Electronics Co., Ltd. + * + * Authors: Jacek Anaszewski <j.anaszewski@samsung.com> + * Andrzej Hajda <a.hajda@samsung.com> + */ + +#include <linux/led-class-flash.h> +#include <linux/mfd/max77693.h> +#include <linux/mfd/max77693-common.h> +#include <linux/mfd/max77693-private.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> +#include <linux/slab.h> +#include <media/v4l2-flash-led-class.h> + +#define MODE_OFF 0 +#define MODE_FLASH(a) (1 << (a)) +#define MODE_TORCH(a) (1 << (2 + (a))) +#define MODE_FLASH_EXTERNAL(a) (1 << (4 + (a))) + +#define MODE_FLASH_MASK (MODE_FLASH(FLED1) | MODE_FLASH(FLED2) | \ + MODE_FLASH_EXTERNAL(FLED1) | \ + MODE_FLASH_EXTERNAL(FLED2)) +#define MODE_TORCH_MASK (MODE_TORCH(FLED1) | MODE_TORCH(FLED2)) + +#define FLED1_IOUT (1 << 0) +#define FLED2_IOUT (1 << 1) + +enum max77693_fled { + FLED1, + FLED2, +}; + +enum max77693_led_mode { + FLASH, + TORCH, +}; + +struct max77693_led_config_data { + const char *label[2]; + u32 iout_torch_max[2]; + u32 iout_flash_max[2]; + u32 flash_timeout_max[2]; + u32 num_leds; + u32 boost_mode; + u32 boost_vout; + u32 low_vsys; +}; + +struct max77693_sub_led { + /* corresponding FLED output identifier */ + int fled_id; + /* corresponding LED Flash class device */ + struct led_classdev_flash fled_cdev; + /* V4L2 Flash device */ + struct v4l2_flash *v4l2_flash; + + /* brightness cache */ + unsigned int torch_brightness; + /* flash timeout cache */ + unsigned int flash_timeout; + /* flash faults that may have occurred */ + u32 flash_faults; +}; + +struct max77693_led_device { + /* parent mfd regmap */ + struct regmap *regmap; + /* platform device data */ + struct platform_device *pdev; + /* secures access to the device */ + struct mutex lock; + + /* sub led data */ + struct max77693_sub_led sub_leds[2]; + + /* maximum torch current values for FLED outputs */ + u32 iout_torch_max[2]; + /* maximum flash current values for FLED outputs */ + u32 iout_flash_max[2]; + + /* current flash timeout cache */ + unsigned int current_flash_timeout; + /* ITORCH register cache */ + u8 torch_iout_reg; + /* mode of fled outputs */ + unsigned int mode_flags; + /* recently strobed fled */ + int strobing_sub_led_id; + /* bitmask of FLED outputs use state (bit 0. - FLED1, bit 1. - FLED2) */ + u8 fled_mask; + /* FLED modes that can be set */ + u8 allowed_modes; + + /* arrangement of current outputs */ + bool iout_joint; +}; + +static u8 max77693_led_iout_to_reg(u32 ua) +{ + if (ua < FLASH_IOUT_MIN) + ua = FLASH_IOUT_MIN; + return (ua - FLASH_IOUT_MIN) / FLASH_IOUT_STEP; +} + +static u8 max77693_flash_timeout_to_reg(u32 us) +{ + return (us - FLASH_TIMEOUT_MIN) / FLASH_TIMEOUT_STEP; +} + +static inline struct max77693_sub_led *flcdev_to_sub_led( + struct led_classdev_flash *fled_cdev) +{ + return container_of(fled_cdev, struct max77693_sub_led, fled_cdev); +} + +static inline struct max77693_led_device *sub_led_to_led( + struct max77693_sub_led *sub_led) +{ + return container_of(sub_led, struct max77693_led_device, + sub_leds[sub_led->fled_id]); +} + +static inline u8 max77693_led_vsys_to_reg(u32 mv) +{ + return ((mv - MAX_FLASH1_VSYS_MIN) / MAX_FLASH1_VSYS_STEP) << 2; +} + +static inline u8 max77693_led_vout_to_reg(u32 mv) +{ + return (mv - FLASH_VOUT_MIN) / FLASH_VOUT_STEP + FLASH_VOUT_RMIN; +} + +static inline bool max77693_fled_used(struct max77693_led_device *led, + int fled_id) +{ + u8 fled_bit = (fled_id == FLED1) ? FLED1_IOUT : FLED2_IOUT; + + return led->fled_mask & fled_bit; +} + +static int max77693_set_mode_reg(struct max77693_led_device *led, u8 mode) +{ + struct regmap *rmap = led->regmap; + int ret, v = 0, i; + + for (i = FLED1; i <= FLED2; ++i) { + if (mode & MODE_TORCH(i)) + v |= FLASH_EN_ON << TORCH_EN_SHIFT(i); + + if (mode & MODE_FLASH(i)) { + v |= FLASH_EN_ON << FLASH_EN_SHIFT(i); + } else if (mode & MODE_FLASH_EXTERNAL(i)) { + v |= FLASH_EN_FLASH << FLASH_EN_SHIFT(i); + /* + * Enable hw triggering also for torch mode, as some + * camera sensors use torch led to fathom ambient light + * conditions before strobing the flash. + */ + v |= FLASH_EN_TORCH << TORCH_EN_SHIFT(i); + } + } + + /* Reset the register only prior setting flash modes */ + if (mode & ~(MODE_TORCH(FLED1) | MODE_TORCH(FLED2))) { + ret = regmap_write(rmap, MAX77693_LED_REG_FLASH_EN, 0); + if (ret < 0) + return ret; + } + + return regmap_write(rmap, MAX77693_LED_REG_FLASH_EN, v); +} + +static int max77693_add_mode(struct max77693_led_device *led, u8 mode) +{ + u8 new_mode_flags; + int i, ret; + + if (led->iout_joint) + /* Span the mode on FLED2 for joint iouts case */ + mode |= (mode << 1); + + /* + * FLASH_EXTERNAL mode activates FLASHEN and TORCHEN pins in the device. + * Corresponding register bit fields interfere with SW triggered modes, + * thus clear them to ensure proper device configuration. + */ + for (i = FLED1; i <= FLED2; ++i) + if (mode & MODE_FLASH_EXTERNAL(i)) + led->mode_flags &= (~MODE_TORCH(i) & ~MODE_FLASH(i)); + + new_mode_flags = mode | led->mode_flags; + new_mode_flags &= led->allowed_modes; + + if (new_mode_flags ^ led->mode_flags) + led->mode_flags = new_mode_flags; + else + return 0; + + ret = max77693_set_mode_reg(led, led->mode_flags); + if (ret < 0) + return ret; + + /* + * Clear flash mode flag after setting the mode to avoid spurious flash + * strobing on each subsequent torch mode setting. + */ + if (mode & MODE_FLASH_MASK) + led->mode_flags &= ~mode; + + return ret; +} + +static int max77693_clear_mode(struct max77693_led_device *led, + u8 mode) +{ + if (led->iout_joint) + /* Clear mode also on FLED2 for joint iouts case */ + mode |= (mode << 1); + + led->mode_flags &= ~mode; + + return max77693_set_mode_reg(led, led->mode_flags); +} + +static void max77693_add_allowed_modes(struct max77693_led_device *led, + int fled_id, enum max77693_led_mode mode) +{ + if (mode == FLASH) + led->allowed_modes |= (MODE_FLASH(fled_id) | + MODE_FLASH_EXTERNAL(fled_id)); + else + led->allowed_modes |= MODE_TORCH(fled_id); +} + +static void max77693_distribute_currents(struct max77693_led_device *led, + int fled_id, enum max77693_led_mode mode, + u32 micro_amp, u32 iout_max[2], u32 iout[2]) +{ + if (!led->iout_joint) { + iout[fled_id] = micro_amp; + max77693_add_allowed_modes(led, fled_id, mode); + return; + } + + iout[FLED1] = min(micro_amp, iout_max[FLED1]); + iout[FLED2] = micro_amp - iout[FLED1]; + + if (mode == FLASH) + led->allowed_modes &= ~MODE_FLASH_MASK; + else + led->allowed_modes &= ~MODE_TORCH_MASK; + + max77693_add_allowed_modes(led, FLED1, mode); + + if (iout[FLED2]) + max77693_add_allowed_modes(led, FLED2, mode); +} + +static int max77693_set_torch_current(struct max77693_led_device *led, + int fled_id, u32 micro_amp) +{ + struct regmap *rmap = led->regmap; + u8 iout1_reg = 0, iout2_reg = 0; + u32 iout[2]; + + max77693_distribute_currents(led, fled_id, TORCH, micro_amp, + led->iout_torch_max, iout); + + if (fled_id == FLED1 || led->iout_joint) { + iout1_reg = max77693_led_iout_to_reg(iout[FLED1]); + led->torch_iout_reg &= TORCH_IOUT_MASK(TORCH_IOUT2_SHIFT); + } + if (fled_id == FLED2 || led->iout_joint) { + iout2_reg = max77693_led_iout_to_reg(iout[FLED2]); + led->torch_iout_reg &= TORCH_IOUT_MASK(TORCH_IOUT1_SHIFT); + } + + led->torch_iout_reg |= ((iout1_reg << TORCH_IOUT1_SHIFT) | + (iout2_reg << TORCH_IOUT2_SHIFT)); + + return regmap_write(rmap, MAX77693_LED_REG_ITORCH, + led->torch_iout_reg); +} + +static int max77693_set_flash_current(struct max77693_led_device *led, + int fled_id, + u32 micro_amp) +{ + struct regmap *rmap = led->regmap; + u8 iout1_reg, iout2_reg; + u32 iout[2]; + int ret = -EINVAL; + + max77693_distribute_currents(led, fled_id, FLASH, micro_amp, + led->iout_flash_max, iout); + + if (fled_id == FLED1 || led->iout_joint) { + iout1_reg = max77693_led_iout_to_reg(iout[FLED1]); + ret = regmap_write(rmap, MAX77693_LED_REG_IFLASH1, + iout1_reg); + if (ret < 0) + return ret; + } + if (fled_id == FLED2 || led->iout_joint) { + iout2_reg = max77693_led_iout_to_reg(iout[FLED2]); + ret = regmap_write(rmap, MAX77693_LED_REG_IFLASH2, + iout2_reg); + } + + return ret; +} + +static int max77693_set_timeout(struct max77693_led_device *led, u32 microsec) +{ + struct regmap *rmap = led->regmap; + u8 v; + int ret; + + v = max77693_flash_timeout_to_reg(microsec) | FLASH_TMR_LEVEL; + + ret = regmap_write(rmap, MAX77693_LED_REG_FLASH_TIMER, v); + if (ret < 0) + return ret; + + led->current_flash_timeout = microsec; + + return 0; +} + +static int max77693_get_strobe_status(struct max77693_led_device *led, + bool *state) +{ + struct regmap *rmap = led->regmap; + unsigned int v; + int ret; + + ret = regmap_read(rmap, MAX77693_LED_REG_FLASH_STATUS, &v); + if (ret < 0) + return ret; + + *state = v & FLASH_STATUS_FLASH_ON; + + return ret; +} + +static int max77693_get_flash_faults(struct max77693_sub_led *sub_led) +{ + struct max77693_led_device *led = sub_led_to_led(sub_led); + struct regmap *rmap = led->regmap; + unsigned int v; + u8 fault_open_mask, fault_short_mask; + int ret; + + sub_led->flash_faults = 0; + + if (led->iout_joint) { + fault_open_mask = FLASH_INT_FLED1_OPEN | FLASH_INT_FLED2_OPEN; + fault_short_mask = FLASH_INT_FLED1_SHORT | + FLASH_INT_FLED2_SHORT; + } else { + fault_open_mask = (sub_led->fled_id == FLED1) ? + FLASH_INT_FLED1_OPEN : + FLASH_INT_FLED2_OPEN; + fault_short_mask = (sub_led->fled_id == FLED1) ? + FLASH_INT_FLED1_SHORT : + FLASH_INT_FLED2_SHORT; + } + + ret = regmap_read(rmap, MAX77693_LED_REG_FLASH_INT, &v); + if (ret < 0) + return ret; + + if (v & fault_open_mask) + sub_led->flash_faults |= LED_FAULT_OVER_VOLTAGE; + if (v & fault_short_mask) + sub_led->flash_faults |= LED_FAULT_SHORT_CIRCUIT; + if (v & FLASH_INT_OVER_CURRENT) + sub_led->flash_faults |= LED_FAULT_OVER_CURRENT; + + return 0; +} + +static int max77693_setup(struct max77693_led_device *led, + struct max77693_led_config_data *led_cfg) +{ + struct regmap *rmap = led->regmap; + int i, first_led, last_led, ret; + u32 max_flash_curr[2]; + u8 v; + + /* + * Initialize only flash current. Torch current doesn't + * require initialization as ITORCH register is written with + * new value each time brightness_set op is called. + */ + if (led->iout_joint) { + first_led = FLED1; + last_led = FLED1; + max_flash_curr[FLED1] = led_cfg->iout_flash_max[FLED1] + + led_cfg->iout_flash_max[FLED2]; + } else { + first_led = max77693_fled_used(led, FLED1) ? FLED1 : FLED2; + last_led = max77693_fled_used(led, FLED2) ? FLED2 : FLED1; + max_flash_curr[FLED1] = led_cfg->iout_flash_max[FLED1]; + max_flash_curr[FLED2] = led_cfg->iout_flash_max[FLED2]; + } + + for (i = first_led; i <= last_led; ++i) { + ret = max77693_set_flash_current(led, i, + max_flash_curr[i]); + if (ret < 0) + return ret; + } + + v = TORCH_TMR_NO_TIMER | MAX77693_LED_TRIG_TYPE_LEVEL; + ret = regmap_write(rmap, MAX77693_LED_REG_ITORCHTIMER, v); + if (ret < 0) + return ret; + + if (led_cfg->low_vsys > 0) + v = max77693_led_vsys_to_reg(led_cfg->low_vsys) | + MAX_FLASH1_MAX_FL_EN; + else + v = 0; + + ret = regmap_write(rmap, MAX77693_LED_REG_MAX_FLASH1, v); + if (ret < 0) + return ret; + ret = regmap_write(rmap, MAX77693_LED_REG_MAX_FLASH2, 0); + if (ret < 0) + return ret; + + if (led_cfg->boost_mode == MAX77693_LED_BOOST_FIXED) + v = FLASH_BOOST_FIXED; + else + v = led_cfg->boost_mode | led_cfg->boost_mode << 1; + + if (max77693_fled_used(led, FLED1) && max77693_fled_used(led, FLED2)) + v |= FLASH_BOOST_LEDNUM_2; + + ret = regmap_write(rmap, MAX77693_LED_REG_VOUT_CNTL, v); + if (ret < 0) + return ret; + + v = max77693_led_vout_to_reg(led_cfg->boost_vout); + ret = regmap_write(rmap, MAX77693_LED_REG_VOUT_FLASH1, v); + if (ret < 0) + return ret; + + return max77693_set_mode_reg(led, MODE_OFF); +} + +/* LED subsystem callbacks */ +static int max77693_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + struct max77693_sub_led *sub_led = flcdev_to_sub_led(fled_cdev); + struct max77693_led_device *led = sub_led_to_led(sub_led); + int fled_id = sub_led->fled_id, ret; + + mutex_lock(&led->lock); + + if (value == 0) { + ret = max77693_clear_mode(led, MODE_TORCH(fled_id)); + if (ret < 0) + dev_dbg(&led->pdev->dev, + "Failed to clear torch mode (%d)\n", + ret); + goto unlock; + } + + ret = max77693_set_torch_current(led, fled_id, value * TORCH_IOUT_STEP); + if (ret < 0) { + dev_dbg(&led->pdev->dev, + "Failed to set torch current (%d)\n", + ret); + goto unlock; + } + + ret = max77693_add_mode(led, MODE_TORCH(fled_id)); + if (ret < 0) + dev_dbg(&led->pdev->dev, + "Failed to set torch mode (%d)\n", + ret); +unlock: + mutex_unlock(&led->lock); + + return ret; +} + +static int max77693_led_flash_brightness_set( + struct led_classdev_flash *fled_cdev, + u32 brightness) +{ + struct max77693_sub_led *sub_led = flcdev_to_sub_led(fled_cdev); + struct max77693_led_device *led = sub_led_to_led(sub_led); + int ret; + + mutex_lock(&led->lock); + ret = max77693_set_flash_current(led, sub_led->fled_id, brightness); + mutex_unlock(&led->lock); + + return ret; +} + +static int max77693_led_flash_strobe_set( + struct led_classdev_flash *fled_cdev, + bool state) +{ + struct max77693_sub_led *sub_led = flcdev_to_sub_led(fled_cdev); + struct max77693_led_device *led = sub_led_to_led(sub_led); + int fled_id = sub_led->fled_id; + int ret; + + mutex_lock(&led->lock); + + if (!state) { + ret = max77693_clear_mode(led, MODE_FLASH(fled_id)); + goto unlock; + } + + if (sub_led->flash_timeout != led->current_flash_timeout) { + ret = max77693_set_timeout(led, sub_led->flash_timeout); + if (ret < 0) + goto unlock; + } + + led->strobing_sub_led_id = fled_id; + + ret = max77693_add_mode(led, MODE_FLASH(fled_id)); + if (ret < 0) + goto unlock; + + ret = max77693_get_flash_faults(sub_led); + +unlock: + mutex_unlock(&led->lock); + return ret; +} + +static int max77693_led_flash_fault_get( + struct led_classdev_flash *fled_cdev, + u32 *fault) +{ + struct max77693_sub_led *sub_led = flcdev_to_sub_led(fled_cdev); + + *fault = sub_led->flash_faults; + + return 0; +} + +static int max77693_led_flash_strobe_get( + struct led_classdev_flash *fled_cdev, + bool *state) +{ + struct max77693_sub_led *sub_led = flcdev_to_sub_led(fled_cdev); + struct max77693_led_device *led = sub_led_to_led(sub_led); + int ret; + + if (!state) + return -EINVAL; + + mutex_lock(&led->lock); + + ret = max77693_get_strobe_status(led, state); + + *state = !!(*state && (led->strobing_sub_led_id == sub_led->fled_id)); + + mutex_unlock(&led->lock); + + return ret; +} + +static int max77693_led_flash_timeout_set( + struct led_classdev_flash *fled_cdev, + u32 timeout) +{ + struct max77693_sub_led *sub_led = flcdev_to_sub_led(fled_cdev); + struct max77693_led_device *led = sub_led_to_led(sub_led); + + mutex_lock(&led->lock); + sub_led->flash_timeout = timeout; + mutex_unlock(&led->lock); + + return 0; +} + +static int max77693_led_parse_dt(struct max77693_led_device *led, + struct max77693_led_config_data *cfg, + struct device_node **sub_nodes) +{ + struct device *dev = &led->pdev->dev; + struct max77693_sub_led *sub_leds = led->sub_leds; + struct device_node *node = dev_of_node(dev), *child_node; + struct property *prop; + u32 led_sources[2]; + int i, ret, fled_id; + + of_property_read_u32(node, "maxim,boost-mode", &cfg->boost_mode); + of_property_read_u32(node, "maxim,boost-mvout", &cfg->boost_vout); + of_property_read_u32(node, "maxim,mvsys-min", &cfg->low_vsys); + + for_each_available_child_of_node(node, child_node) { + prop = of_find_property(child_node, "led-sources", NULL); + if (prop) { + const __be32 *srcs = NULL; + + for (i = 0; i < ARRAY_SIZE(led_sources); ++i) { + srcs = of_prop_next_u32(prop, srcs, + &led_sources[i]); + if (!srcs) + break; + } + } else { + dev_err(dev, + "led-sources DT property missing\n"); + of_node_put(child_node); + return -EINVAL; + } + + if (i == 2) { + fled_id = FLED1; + led->fled_mask = FLED1_IOUT | FLED2_IOUT; + } else if (led_sources[0] == FLED1) { + fled_id = FLED1; + led->fled_mask |= FLED1_IOUT; + } else if (led_sources[0] == FLED2) { + fled_id = FLED2; + led->fled_mask |= FLED2_IOUT; + } else { + dev_err(dev, + "Wrong led-sources DT property value.\n"); + of_node_put(child_node); + return -EINVAL; + } + + if (sub_nodes[fled_id]) { + dev_err(dev, + "Conflicting \"led-sources\" DT properties\n"); + of_node_put(child_node); + return -EINVAL; + } + + sub_nodes[fled_id] = child_node; + sub_leds[fled_id].fled_id = fled_id; + + cfg->label[fled_id] = + of_get_property(child_node, "label", NULL) ? : + child_node->name; + + ret = of_property_read_u32(child_node, "led-max-microamp", + &cfg->iout_torch_max[fled_id]); + if (ret < 0) { + cfg->iout_torch_max[fled_id] = TORCH_IOUT_MIN; + dev_warn(dev, "led-max-microamp DT property missing\n"); + } + + ret = of_property_read_u32(child_node, "flash-max-microamp", + &cfg->iout_flash_max[fled_id]); + if (ret < 0) { + cfg->iout_flash_max[fled_id] = FLASH_IOUT_MIN; + dev_warn(dev, + "flash-max-microamp DT property missing\n"); + } + + ret = of_property_read_u32(child_node, "flash-max-timeout-us", + &cfg->flash_timeout_max[fled_id]); + if (ret < 0) { + cfg->flash_timeout_max[fled_id] = FLASH_TIMEOUT_MIN; + dev_warn(dev, + "flash-max-timeout-us DT property missing\n"); + } + + if (++cfg->num_leds == 2 || + (max77693_fled_used(led, FLED1) && + max77693_fled_used(led, FLED2))) { + of_node_put(child_node); + break; + } + } + + if (cfg->num_leds == 0) { + dev_err(dev, "No DT child node found for connected LED(s).\n"); + return -EINVAL; + } + + return 0; +} + +static void clamp_align(u32 *v, u32 min, u32 max, u32 step) +{ + *v = clamp_val(*v, min, max); + if (step > 1) + *v = (*v - min) / step * step + min; +} + +static void max77693_align_iout_current(struct max77693_led_device *led, + u32 *iout, u32 min, u32 max, u32 step) +{ + int i; + + if (led->iout_joint) { + if (iout[FLED1] > min) { + iout[FLED1] /= 2; + iout[FLED2] = iout[FLED1]; + } else { + iout[FLED1] = min; + iout[FLED2] = 0; + return; + } + } + + for (i = FLED1; i <= FLED2; ++i) + if (max77693_fled_used(led, i)) + clamp_align(&iout[i], min, max, step); + else + iout[i] = 0; +} + +static void max77693_led_validate_configuration(struct max77693_led_device *led, + struct max77693_led_config_data *cfg) +{ + u32 flash_iout_max = cfg->boost_mode ? FLASH_IOUT_MAX_2LEDS : + FLASH_IOUT_MAX_1LED; + int i; + + if (cfg->num_leds == 1 && + max77693_fled_used(led, FLED1) && max77693_fled_used(led, FLED2)) + led->iout_joint = true; + + cfg->boost_mode = clamp_val(cfg->boost_mode, MAX77693_LED_BOOST_NONE, + MAX77693_LED_BOOST_FIXED); + + /* Boost must be enabled if both current outputs are used */ + if ((cfg->boost_mode == MAX77693_LED_BOOST_NONE) && led->iout_joint) + cfg->boost_mode = MAX77693_LED_BOOST_FIXED; + + max77693_align_iout_current(led, cfg->iout_torch_max, + TORCH_IOUT_MIN, TORCH_IOUT_MAX, TORCH_IOUT_STEP); + + max77693_align_iout_current(led, cfg->iout_flash_max, + FLASH_IOUT_MIN, flash_iout_max, FLASH_IOUT_STEP); + + for (i = 0; i < ARRAY_SIZE(cfg->flash_timeout_max); ++i) + clamp_align(&cfg->flash_timeout_max[i], FLASH_TIMEOUT_MIN, + FLASH_TIMEOUT_MAX, FLASH_TIMEOUT_STEP); + + clamp_align(&cfg->boost_vout, FLASH_VOUT_MIN, FLASH_VOUT_MAX, + FLASH_VOUT_STEP); + + if (cfg->low_vsys) + clamp_align(&cfg->low_vsys, MAX_FLASH1_VSYS_MIN, + MAX_FLASH1_VSYS_MAX, MAX_FLASH1_VSYS_STEP); +} + +static int max77693_led_get_configuration(struct max77693_led_device *led, + struct max77693_led_config_data *cfg, + struct device_node **sub_nodes) +{ + int ret; + + ret = max77693_led_parse_dt(led, cfg, sub_nodes); + if (ret < 0) + return ret; + + max77693_led_validate_configuration(led, cfg); + + memcpy(led->iout_torch_max, cfg->iout_torch_max, + sizeof(led->iout_torch_max)); + memcpy(led->iout_flash_max, cfg->iout_flash_max, + sizeof(led->iout_flash_max)); + + return 0; +} + +static const struct led_flash_ops flash_ops = { + .flash_brightness_set = max77693_led_flash_brightness_set, + .strobe_set = max77693_led_flash_strobe_set, + .strobe_get = max77693_led_flash_strobe_get, + .timeout_set = max77693_led_flash_timeout_set, + .fault_get = max77693_led_flash_fault_get, +}; + +static void max77693_init_flash_settings(struct max77693_sub_led *sub_led, + struct max77693_led_config_data *led_cfg) +{ + struct led_classdev_flash *fled_cdev = &sub_led->fled_cdev; + struct max77693_led_device *led = sub_led_to_led(sub_led); + int fled_id = sub_led->fled_id; + struct led_flash_setting *setting; + + /* Init flash intensity setting */ + setting = &fled_cdev->brightness; + setting->min = FLASH_IOUT_MIN; + setting->max = led->iout_joint ? + led_cfg->iout_flash_max[FLED1] + + led_cfg->iout_flash_max[FLED2] : + led_cfg->iout_flash_max[fled_id]; + setting->step = FLASH_IOUT_STEP; + setting->val = setting->max; + + /* Init flash timeout setting */ + setting = &fled_cdev->timeout; + setting->min = FLASH_TIMEOUT_MIN; + setting->max = led_cfg->flash_timeout_max[fled_id]; + setting->step = FLASH_TIMEOUT_STEP; + setting->val = setting->max; +} + +#if IS_ENABLED(CONFIG_V4L2_FLASH_LED_CLASS) + +static int max77693_led_external_strobe_set( + struct v4l2_flash *v4l2_flash, + bool enable) +{ + struct max77693_sub_led *sub_led = + flcdev_to_sub_led(v4l2_flash->fled_cdev); + struct max77693_led_device *led = sub_led_to_led(sub_led); + int fled_id = sub_led->fled_id; + int ret; + + mutex_lock(&led->lock); + + if (enable) + ret = max77693_add_mode(led, MODE_FLASH_EXTERNAL(fled_id)); + else + ret = max77693_clear_mode(led, MODE_FLASH_EXTERNAL(fled_id)); + + mutex_unlock(&led->lock); + + return ret; +} + +static void max77693_init_v4l2_flash_config(struct max77693_sub_led *sub_led, + struct max77693_led_config_data *led_cfg, + struct v4l2_flash_config *v4l2_sd_cfg) +{ + struct max77693_led_device *led = sub_led_to_led(sub_led); + struct device *dev = &led->pdev->dev; + struct max77693_dev *iodev = dev_get_drvdata(dev->parent); + struct i2c_client *i2c = iodev->i2c; + struct led_flash_setting *s; + + snprintf(v4l2_sd_cfg->dev_name, sizeof(v4l2_sd_cfg->dev_name), + "%s %d-%04x", sub_led->fled_cdev.led_cdev.name, + i2c_adapter_id(i2c->adapter), i2c->addr); + + s = &v4l2_sd_cfg->intensity; + s->min = TORCH_IOUT_MIN; + s->max = sub_led->fled_cdev.led_cdev.max_brightness * TORCH_IOUT_STEP; + s->step = TORCH_IOUT_STEP; + s->val = s->max; + + /* Init flash faults config */ + v4l2_sd_cfg->flash_faults = LED_FAULT_OVER_VOLTAGE | + LED_FAULT_SHORT_CIRCUIT | + LED_FAULT_OVER_CURRENT; + + v4l2_sd_cfg->has_external_strobe = true; +} + +static const struct v4l2_flash_ops v4l2_flash_ops = { + .external_strobe_set = max77693_led_external_strobe_set, +}; +#else +static inline void max77693_init_v4l2_flash_config( + struct max77693_sub_led *sub_led, + struct max77693_led_config_data *led_cfg, + struct v4l2_flash_config *v4l2_sd_cfg) +{ +} +static const struct v4l2_flash_ops v4l2_flash_ops; +#endif + +static void max77693_init_fled_cdev(struct max77693_sub_led *sub_led, + struct max77693_led_config_data *led_cfg) +{ + struct max77693_led_device *led = sub_led_to_led(sub_led); + int fled_id = sub_led->fled_id; + struct led_classdev_flash *fled_cdev; + struct led_classdev *led_cdev; + + /* Initialize LED Flash class device */ + fled_cdev = &sub_led->fled_cdev; + fled_cdev->ops = &flash_ops; + led_cdev = &fled_cdev->led_cdev; + + led_cdev->name = led_cfg->label[fled_id]; + + led_cdev->brightness_set_blocking = max77693_led_brightness_set; + led_cdev->max_brightness = (led->iout_joint ? + led_cfg->iout_torch_max[FLED1] + + led_cfg->iout_torch_max[FLED2] : + led_cfg->iout_torch_max[fled_id]) / + TORCH_IOUT_STEP; + led_cdev->flags |= LED_DEV_CAP_FLASH; + + max77693_init_flash_settings(sub_led, led_cfg); + + /* Init flash timeout cache */ + sub_led->flash_timeout = fled_cdev->timeout.val; +} + +static int max77693_register_led(struct max77693_sub_led *sub_led, + struct max77693_led_config_data *led_cfg, + struct device_node *sub_node) +{ + struct max77693_led_device *led = sub_led_to_led(sub_led); + struct led_classdev_flash *fled_cdev = &sub_led->fled_cdev; + struct device *dev = &led->pdev->dev; + struct v4l2_flash_config v4l2_sd_cfg = {}; + int ret; + + /* Register in the LED subsystem */ + ret = led_classdev_flash_register(dev, fled_cdev); + if (ret < 0) + return ret; + + max77693_init_v4l2_flash_config(sub_led, led_cfg, &v4l2_sd_cfg); + + /* Register in the V4L2 subsystem. */ + sub_led->v4l2_flash = v4l2_flash_init(dev, of_fwnode_handle(sub_node), + fled_cdev, &v4l2_flash_ops, + &v4l2_sd_cfg); + if (IS_ERR(sub_led->v4l2_flash)) { + ret = PTR_ERR(sub_led->v4l2_flash); + goto err_v4l2_flash_init; + } + + return 0; + +err_v4l2_flash_init: + led_classdev_flash_unregister(fled_cdev); + return ret; +} + +static int max77693_led_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct max77693_dev *iodev = dev_get_drvdata(dev->parent); + struct max77693_led_device *led; + struct max77693_sub_led *sub_leds; + struct device_node *sub_nodes[2] = {}; + struct max77693_led_config_data led_cfg = {}; + int init_fled_cdev[2], i, ret; + + led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->pdev = pdev; + led->regmap = iodev->regmap; + led->allowed_modes = MODE_FLASH_MASK; + sub_leds = led->sub_leds; + + platform_set_drvdata(pdev, led); + ret = max77693_led_get_configuration(led, &led_cfg, sub_nodes); + if (ret < 0) + return ret; + + ret = max77693_setup(led, &led_cfg); + if (ret < 0) + return ret; + + mutex_init(&led->lock); + + init_fled_cdev[FLED1] = + led->iout_joint || max77693_fled_used(led, FLED1); + init_fled_cdev[FLED2] = + !led->iout_joint && max77693_fled_used(led, FLED2); + + for (i = FLED1; i <= FLED2; ++i) { + if (!init_fled_cdev[i]) + continue; + + /* Initialize LED Flash class device */ + max77693_init_fled_cdev(&sub_leds[i], &led_cfg); + + /* + * Register LED Flash class device and corresponding + * V4L2 Flash device. + */ + ret = max77693_register_led(&sub_leds[i], &led_cfg, + sub_nodes[i]); + if (ret < 0) { + /* + * At this moment FLED1 might have been already + * registered and it needs to be released. + */ + if (i == FLED2) + goto err_register_led2; + else + goto err_register_led1; + } + } + + return 0; + +err_register_led2: + /* It is possible than only FLED2 was to be registered */ + if (!init_fled_cdev[FLED1]) + goto err_register_led1; + v4l2_flash_release(sub_leds[FLED1].v4l2_flash); + led_classdev_flash_unregister(&sub_leds[FLED1].fled_cdev); +err_register_led1: + mutex_destroy(&led->lock); + + return ret; +} + +static int max77693_led_remove(struct platform_device *pdev) +{ + struct max77693_led_device *led = platform_get_drvdata(pdev); + struct max77693_sub_led *sub_leds = led->sub_leds; + + if (led->iout_joint || max77693_fled_used(led, FLED1)) { + v4l2_flash_release(sub_leds[FLED1].v4l2_flash); + led_classdev_flash_unregister(&sub_leds[FLED1].fled_cdev); + } + + if (!led->iout_joint && max77693_fled_used(led, FLED2)) { + v4l2_flash_release(sub_leds[FLED2].v4l2_flash); + led_classdev_flash_unregister(&sub_leds[FLED2].fled_cdev); + } + + mutex_destroy(&led->lock); + + return 0; +} + +static const struct of_device_id max77693_led_dt_match[] = { + { .compatible = "maxim,max77693-led" }, + {}, +}; +MODULE_DEVICE_TABLE(of, max77693_led_dt_match); + +static struct platform_driver max77693_led_driver = { + .probe = max77693_led_probe, + .remove = max77693_led_remove, + .driver = { + .name = "max77693-led", + .of_match_table = max77693_led_dt_match, + }, +}; + +module_platform_driver(max77693_led_driver); + +MODULE_AUTHOR("Jacek Anaszewski <j.anaszewski@samsung.com>"); +MODULE_AUTHOR("Andrzej Hajda <a.hajda@samsung.com>"); +MODULE_DESCRIPTION("Maxim MAX77693 led flash driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-max8997.c b/drivers/leds/leds-max8997.c new file mode 100644 index 000000000..512a11d14 --- /dev/null +++ b/drivers/leds/leds-max8997.c @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * leds-max8997.c - LED class driver for MAX8997 LEDs. + * + * Copyright (C) 2011 Samsung Electronics + * Donggeun Kim <dg77.kim@samsung.com> + */ + +#include <linux/module.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/leds.h> +#include <linux/mfd/max8997.h> +#include <linux/mfd/max8997-private.h> +#include <linux/platform_device.h> + +#define MAX8997_LED_FLASH_SHIFT 3 +#define MAX8997_LED_FLASH_CUR_MASK 0xf8 +#define MAX8997_LED_MOVIE_SHIFT 4 +#define MAX8997_LED_MOVIE_CUR_MASK 0xf0 + +#define MAX8997_LED_FLASH_MAX_BRIGHTNESS 0x1f +#define MAX8997_LED_MOVIE_MAX_BRIGHTNESS 0xf +#define MAX8997_LED_NONE_MAX_BRIGHTNESS 0 + +#define MAX8997_LED0_FLASH_MASK 0x1 +#define MAX8997_LED0_FLASH_PIN_MASK 0x5 +#define MAX8997_LED0_MOVIE_MASK 0x8 +#define MAX8997_LED0_MOVIE_PIN_MASK 0x28 + +#define MAX8997_LED1_FLASH_MASK 0x2 +#define MAX8997_LED1_FLASH_PIN_MASK 0x6 +#define MAX8997_LED1_MOVIE_MASK 0x10 +#define MAX8997_LED1_MOVIE_PIN_MASK 0x30 + +#define MAX8997_LED_BOOST_ENABLE_MASK (1 << 6) + +struct max8997_led { + struct max8997_dev *iodev; + struct led_classdev cdev; + bool enabled; + int id; + enum max8997_led_mode led_mode; + struct mutex mutex; +}; + +static void max8997_led_set_mode(struct max8997_led *led, + enum max8997_led_mode mode) +{ + int ret; + struct i2c_client *client = led->iodev->i2c; + u8 mask = 0, val; + + switch (mode) { + case MAX8997_FLASH_MODE: + mask = MAX8997_LED1_FLASH_MASK | MAX8997_LED0_FLASH_MASK; + val = led->id ? + MAX8997_LED1_FLASH_MASK : MAX8997_LED0_FLASH_MASK; + led->cdev.max_brightness = MAX8997_LED_FLASH_MAX_BRIGHTNESS; + break; + case MAX8997_MOVIE_MODE: + mask = MAX8997_LED1_MOVIE_MASK | MAX8997_LED0_MOVIE_MASK; + val = led->id ? + MAX8997_LED1_MOVIE_MASK : MAX8997_LED0_MOVIE_MASK; + led->cdev.max_brightness = MAX8997_LED_MOVIE_MAX_BRIGHTNESS; + break; + case MAX8997_FLASH_PIN_CONTROL_MODE: + mask = MAX8997_LED1_FLASH_PIN_MASK | + MAX8997_LED0_FLASH_PIN_MASK; + val = led->id ? + MAX8997_LED1_FLASH_PIN_MASK : MAX8997_LED0_FLASH_PIN_MASK; + led->cdev.max_brightness = MAX8997_LED_FLASH_MAX_BRIGHTNESS; + break; + case MAX8997_MOVIE_PIN_CONTROL_MODE: + mask = MAX8997_LED1_MOVIE_PIN_MASK | + MAX8997_LED0_MOVIE_PIN_MASK; + val = led->id ? + MAX8997_LED1_MOVIE_PIN_MASK : MAX8997_LED0_MOVIE_PIN_MASK; + led->cdev.max_brightness = MAX8997_LED_MOVIE_MAX_BRIGHTNESS; + break; + default: + led->cdev.max_brightness = MAX8997_LED_NONE_MAX_BRIGHTNESS; + break; + } + + if (mask) { + ret = max8997_update_reg(client, MAX8997_REG_LEN_CNTL, val, + mask); + if (ret) + dev_err(led->iodev->dev, + "failed to update register(%d)\n", ret); + } + + led->led_mode = mode; +} + +static void max8997_led_enable(struct max8997_led *led, bool enable) +{ + int ret; + struct i2c_client *client = led->iodev->i2c; + u8 val = 0, mask = MAX8997_LED_BOOST_ENABLE_MASK; + + if (led->enabled == enable) + return; + + val = enable ? MAX8997_LED_BOOST_ENABLE_MASK : 0; + + ret = max8997_update_reg(client, MAX8997_REG_BOOST_CNTL, val, mask); + if (ret) + dev_err(led->iodev->dev, + "failed to update register(%d)\n", ret); + + led->enabled = enable; +} + +static void max8997_led_set_current(struct max8997_led *led, + enum led_brightness value) +{ + int ret; + struct i2c_client *client = led->iodev->i2c; + u8 val = 0, mask = 0, reg = 0; + + switch (led->led_mode) { + case MAX8997_FLASH_MODE: + case MAX8997_FLASH_PIN_CONTROL_MODE: + val = value << MAX8997_LED_FLASH_SHIFT; + mask = MAX8997_LED_FLASH_CUR_MASK; + reg = led->id ? MAX8997_REG_FLASH2_CUR : MAX8997_REG_FLASH1_CUR; + break; + case MAX8997_MOVIE_MODE: + case MAX8997_MOVIE_PIN_CONTROL_MODE: + val = value << MAX8997_LED_MOVIE_SHIFT; + mask = MAX8997_LED_MOVIE_CUR_MASK; + reg = MAX8997_REG_MOVIE_CUR; + break; + default: + break; + } + + if (mask) { + ret = max8997_update_reg(client, reg, val, mask); + if (ret) + dev_err(led->iodev->dev, + "failed to update register(%d)\n", ret); + } +} + +static void max8997_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct max8997_led *led = + container_of(led_cdev, struct max8997_led, cdev); + + if (value) { + max8997_led_set_current(led, value); + max8997_led_enable(led, true); + } else { + max8997_led_set_current(led, value); + max8997_led_enable(led, false); + } +} + +static ssize_t max8997_led_show_mode(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct max8997_led *led = + container_of(led_cdev, struct max8997_led, cdev); + ssize_t ret = 0; + + mutex_lock(&led->mutex); + + switch (led->led_mode) { + case MAX8997_FLASH_MODE: + ret += sprintf(buf, "FLASH\n"); + break; + case MAX8997_MOVIE_MODE: + ret += sprintf(buf, "MOVIE\n"); + break; + case MAX8997_FLASH_PIN_CONTROL_MODE: + ret += sprintf(buf, "FLASH_PIN_CONTROL\n"); + break; + case MAX8997_MOVIE_PIN_CONTROL_MODE: + ret += sprintf(buf, "MOVIE_PIN_CONTROL\n"); + break; + default: + ret += sprintf(buf, "NONE\n"); + break; + } + + mutex_unlock(&led->mutex); + + return ret; +} + +static ssize_t max8997_led_store_mode(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct max8997_led *led = + container_of(led_cdev, struct max8997_led, cdev); + enum max8997_led_mode mode; + + mutex_lock(&led->mutex); + + if (!strncmp(buf, "FLASH_PIN_CONTROL", 17)) + mode = MAX8997_FLASH_PIN_CONTROL_MODE; + else if (!strncmp(buf, "MOVIE_PIN_CONTROL", 17)) + mode = MAX8997_MOVIE_PIN_CONTROL_MODE; + else if (!strncmp(buf, "FLASH", 5)) + mode = MAX8997_FLASH_MODE; + else if (!strncmp(buf, "MOVIE", 5)) + mode = MAX8997_MOVIE_MODE; + else + mode = MAX8997_NONE; + + max8997_led_set_mode(led, mode); + + mutex_unlock(&led->mutex); + + return size; +} + +static DEVICE_ATTR(mode, 0644, max8997_led_show_mode, max8997_led_store_mode); + +static struct attribute *max8997_attrs[] = { + &dev_attr_mode.attr, + NULL +}; +ATTRIBUTE_GROUPS(max8997); + +static int max8997_led_probe(struct platform_device *pdev) +{ + struct max8997_dev *iodev = dev_get_drvdata(pdev->dev.parent); + struct max8997_platform_data *pdata = dev_get_platdata(iodev->dev); + struct max8997_led *led; + char name[20]; + int ret = 0; + + if (pdata == NULL) { + dev_err(&pdev->dev, "no platform data\n"); + return -ENODEV; + } + + led = devm_kzalloc(&pdev->dev, sizeof(*led), GFP_KERNEL); + if (led == NULL) + return -ENOMEM; + + led->id = pdev->id; + snprintf(name, sizeof(name), "max8997-led%d", pdev->id); + + led->cdev.name = name; + led->cdev.brightness_set = max8997_led_brightness_set; + led->cdev.flags |= LED_CORE_SUSPENDRESUME; + led->cdev.brightness = 0; + led->cdev.groups = max8997_groups; + led->iodev = iodev; + + /* initialize mode and brightness according to platform_data */ + if (pdata->led_pdata) { + u8 mode = 0, brightness = 0; + + mode = pdata->led_pdata->mode[led->id]; + brightness = pdata->led_pdata->brightness[led->id]; + + max8997_led_set_mode(led, mode); + + if (brightness > led->cdev.max_brightness) + brightness = led->cdev.max_brightness; + max8997_led_set_current(led, brightness); + led->cdev.brightness = brightness; + } else { + max8997_led_set_mode(led, MAX8997_NONE); + max8997_led_set_current(led, 0); + } + + mutex_init(&led->mutex); + + ret = devm_led_classdev_register(&pdev->dev, &led->cdev); + if (ret < 0) + return ret; + + return 0; +} + +static struct platform_driver max8997_led_driver = { + .driver = { + .name = "max8997-led", + }, + .probe = max8997_led_probe, +}; + +module_platform_driver(max8997_led_driver); + +MODULE_AUTHOR("Donggeun Kim <dg77.kim@samsung.com>"); +MODULE_DESCRIPTION("MAX8997 LED driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:max8997-led"); diff --git a/drivers/leds/leds-mc13783.c b/drivers/leds/leds-mc13783.c new file mode 100644 index 000000000..675502c15 --- /dev/null +++ b/drivers/leds/leds-mc13783.c @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LEDs driver for Freescale MC13783/MC13892/MC34708 + * + * Copyright (C) 2010 Philippe Rétornaz + * + * Based on leds-da903x: + * Copyright (C) 2008 Compulab, Ltd. + * Mike Rapoport <mike@compulab.co.il> + * + * Copyright (C) 2006-2008 Marvell International Ltd. + * Eric Miao <eric.miao@marvell.com> + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/of.h> +#include <linux/mfd/mc13xxx.h> + +struct mc13xxx_led_devtype { + int led_min; + int led_max; + int num_regs; + u32 ledctrl_base; +}; + +struct mc13xxx_led { + struct led_classdev cdev; + int id; + struct mc13xxx_leds *leds; +}; + +struct mc13xxx_leds { + struct mc13xxx *master; + struct mc13xxx_led_devtype *devtype; + int num_leds; + struct mc13xxx_led *led; +}; + +static unsigned int mc13xxx_max_brightness(int id) +{ + if (id >= MC13783_LED_MD && id <= MC13783_LED_KP) + return 0x0f; + else if (id >= MC13783_LED_R1 && id <= MC13783_LED_B3) + return 0x1f; + + return 0x3f; +} + +static int mc13xxx_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct mc13xxx_led *led = + container_of(led_cdev, struct mc13xxx_led, cdev); + struct mc13xxx_leds *leds = led->leds; + unsigned int reg, bank, off, shift; + + switch (led->id) { + case MC13783_LED_MD: + case MC13783_LED_AD: + case MC13783_LED_KP: + reg = 2; + shift = 9 + (led->id - MC13783_LED_MD) * 4; + break; + case MC13783_LED_R1: + case MC13783_LED_G1: + case MC13783_LED_B1: + case MC13783_LED_R2: + case MC13783_LED_G2: + case MC13783_LED_B2: + case MC13783_LED_R3: + case MC13783_LED_G3: + case MC13783_LED_B3: + off = led->id - MC13783_LED_R1; + bank = off / 3; + reg = 3 + bank; + shift = (off - bank * 3) * 5 + 6; + break; + case MC13892_LED_MD: + case MC13892_LED_AD: + case MC13892_LED_KP: + off = led->id - MC13892_LED_MD; + reg = off / 2; + shift = 3 + (off - reg * 2) * 12; + break; + case MC13892_LED_R: + case MC13892_LED_G: + case MC13892_LED_B: + off = led->id - MC13892_LED_R; + bank = off / 2; + reg = 2 + bank; + shift = (off - bank * 2) * 12 + 3; + break; + case MC34708_LED_R: + case MC34708_LED_G: + reg = 0; + shift = 3 + (led->id - MC34708_LED_R) * 12; + break; + default: + BUG(); + } + + return mc13xxx_reg_rmw(leds->master, leds->devtype->ledctrl_base + reg, + mc13xxx_max_brightness(led->id) << shift, + value << shift); +} + +#ifdef CONFIG_OF +static struct mc13xxx_leds_platform_data __init *mc13xxx_led_probe_dt( + struct platform_device *pdev) +{ + struct mc13xxx_leds *leds = platform_get_drvdata(pdev); + struct mc13xxx_leds_platform_data *pdata; + struct device_node *parent, *child; + struct device *dev = &pdev->dev; + int i = 0, ret = -ENODATA; + + pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return ERR_PTR(-ENOMEM); + + parent = of_get_child_by_name(dev_of_node(dev->parent), "leds"); + if (!parent) + goto out_node_put; + + ret = of_property_read_u32_array(parent, "led-control", + pdata->led_control, + leds->devtype->num_regs); + if (ret) + goto out_node_put; + + pdata->num_leds = of_get_available_child_count(parent); + + pdata->led = devm_kcalloc(dev, pdata->num_leds, sizeof(*pdata->led), + GFP_KERNEL); + if (!pdata->led) { + ret = -ENOMEM; + goto out_node_put; + } + + for_each_available_child_of_node(parent, child) { + const char *str; + u32 tmp; + + if (of_property_read_u32(child, "reg", &tmp)) + continue; + pdata->led[i].id = leds->devtype->led_min + tmp; + + if (!of_property_read_string(child, "label", &str)) + pdata->led[i].name = str; + if (!of_property_read_string(child, "linux,default-trigger", + &str)) + pdata->led[i].default_trigger = str; + + i++; + } + + pdata->num_leds = i; + ret = i > 0 ? 0 : -ENODATA; + +out_node_put: + of_node_put(parent); + + return ret ? ERR_PTR(ret) : pdata; +} +#else +static inline struct mc13xxx_leds_platform_data __init *mc13xxx_led_probe_dt( + struct platform_device *pdev) +{ + return ERR_PTR(-ENOSYS); +} +#endif + +static int __init mc13xxx_led_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct mc13xxx_leds_platform_data *pdata = dev_get_platdata(dev); + struct mc13xxx *mcdev = dev_get_drvdata(dev->parent); + struct mc13xxx_led_devtype *devtype = + (struct mc13xxx_led_devtype *)pdev->id_entry->driver_data; + struct mc13xxx_leds *leds; + int i, id, ret = -ENODATA; + u32 init_led = 0; + + leds = devm_kzalloc(dev, sizeof(*leds), GFP_KERNEL); + if (!leds) + return -ENOMEM; + + leds->devtype = devtype; + leds->master = mcdev; + platform_set_drvdata(pdev, leds); + + if (dev_of_node(dev->parent)) { + pdata = mc13xxx_led_probe_dt(pdev); + if (IS_ERR(pdata)) + return PTR_ERR(pdata); + } else if (!pdata) + return -ENODATA; + + leds->num_leds = pdata->num_leds; + + if ((leds->num_leds < 1) || + (leds->num_leds > (devtype->led_max - devtype->led_min + 1))) { + dev_err(dev, "Invalid LED count %d\n", leds->num_leds); + return -EINVAL; + } + + leds->led = devm_kcalloc(dev, leds->num_leds, sizeof(*leds->led), + GFP_KERNEL); + if (!leds->led) + return -ENOMEM; + + for (i = 0; i < devtype->num_regs; i++) { + ret = mc13xxx_reg_write(mcdev, leds->devtype->ledctrl_base + i, + pdata->led_control[i]); + if (ret) + return ret; + } + + for (i = 0; i < leds->num_leds; i++) { + const char *name, *trig; + + ret = -EINVAL; + + id = pdata->led[i].id; + name = pdata->led[i].name; + trig = pdata->led[i].default_trigger; + + if ((id > devtype->led_max) || (id < devtype->led_min)) { + dev_err(dev, "Invalid ID %i\n", id); + break; + } + + if (init_led & (1 << id)) { + dev_warn(dev, "LED %i already initialized\n", id); + break; + } + + init_led |= 1 << id; + leds->led[i].id = id; + leds->led[i].leds = leds; + leds->led[i].cdev.name = name; + leds->led[i].cdev.default_trigger = trig; + leds->led[i].cdev.flags = LED_CORE_SUSPENDRESUME; + leds->led[i].cdev.brightness_set_blocking = mc13xxx_led_set; + leds->led[i].cdev.max_brightness = mc13xxx_max_brightness(id); + + ret = led_classdev_register(dev->parent, &leds->led[i].cdev); + if (ret) { + dev_err(dev, "Failed to register LED %i\n", id); + break; + } + } + + if (ret) + while (--i >= 0) + led_classdev_unregister(&leds->led[i].cdev); + + return ret; +} + +static int mc13xxx_led_remove(struct platform_device *pdev) +{ + struct mc13xxx_leds *leds = platform_get_drvdata(pdev); + int i; + + for (i = 0; i < leds->num_leds; i++) + led_classdev_unregister(&leds->led[i].cdev); + + return 0; +} + +static const struct mc13xxx_led_devtype mc13783_led_devtype = { + .led_min = MC13783_LED_MD, + .led_max = MC13783_LED_B3, + .num_regs = 6, + .ledctrl_base = 51, +}; + +static const struct mc13xxx_led_devtype mc13892_led_devtype = { + .led_min = MC13892_LED_MD, + .led_max = MC13892_LED_B, + .num_regs = 4, + .ledctrl_base = 51, +}; + +static const struct mc13xxx_led_devtype mc34708_led_devtype = { + .led_min = MC34708_LED_R, + .led_max = MC34708_LED_G, + .num_regs = 1, + .ledctrl_base = 54, +}; + +static const struct platform_device_id mc13xxx_led_id_table[] = { + { "mc13783-led", (kernel_ulong_t)&mc13783_led_devtype, }, + { "mc13892-led", (kernel_ulong_t)&mc13892_led_devtype, }, + { "mc34708-led", (kernel_ulong_t)&mc34708_led_devtype, }, + { } +}; +MODULE_DEVICE_TABLE(platform, mc13xxx_led_id_table); + +static struct platform_driver mc13xxx_led_driver = { + .driver = { + .name = "mc13xxx-led", + }, + .remove = mc13xxx_led_remove, + .id_table = mc13xxx_led_id_table, +}; +module_platform_driver_probe(mc13xxx_led_driver, mc13xxx_led_probe); + +MODULE_DESCRIPTION("LEDs driver for Freescale MC13XXX PMIC"); +MODULE_AUTHOR("Philippe Retornaz <philippe.retornaz@epfl.ch>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-menf21bmc.c b/drivers/leds/leds-menf21bmc.c new file mode 100644 index 000000000..6b1b47160 --- /dev/null +++ b/drivers/leds/leds-menf21bmc.c @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * MEN 14F021P00 Board Management Controller (BMC) LEDs Driver. + * + * This is the core LED driver of the MEN 14F021P00 BMC. + * There are four LEDs available which can be switched on and off. + * STATUS LED, HOT SWAP LED, USER LED 1, USER LED 2 + * + * Copyright (C) 2014 MEN Mikro Elektronik Nuernberg GmbH + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/i2c.h> + +#define BMC_CMD_LED_GET_SET 0xA0 +#define BMC_BIT_LED_STATUS BIT(0) +#define BMC_BIT_LED_HOTSWAP BIT(1) +#define BMC_BIT_LED_USER1 BIT(2) +#define BMC_BIT_LED_USER2 BIT(3) + +struct menf21bmc_led { + struct led_classdev cdev; + u8 led_bit; + const char *name; + struct i2c_client *i2c_client; +}; + +static struct menf21bmc_led leds[] = { + { + .name = "menf21bmc:led_status", + .led_bit = BMC_BIT_LED_STATUS, + }, + { + .name = "menf21bmc:led_hotswap", + .led_bit = BMC_BIT_LED_HOTSWAP, + }, + { + .name = "menf21bmc:led_user1", + .led_bit = BMC_BIT_LED_USER1, + }, + { + .name = "menf21bmc:led_user2", + .led_bit = BMC_BIT_LED_USER2, + } +}; + +static DEFINE_MUTEX(led_lock); + +static void +menf21bmc_led_set(struct led_classdev *led_cdev, enum led_brightness value) +{ + int led_val; + struct menf21bmc_led *led = container_of(led_cdev, + struct menf21bmc_led, cdev); + + mutex_lock(&led_lock); + led_val = i2c_smbus_read_byte_data(led->i2c_client, + BMC_CMD_LED_GET_SET); + if (led_val < 0) + goto err_out; + + if (value == LED_OFF) + led_val &= ~led->led_bit; + else + led_val |= led->led_bit; + + i2c_smbus_write_byte_data(led->i2c_client, + BMC_CMD_LED_GET_SET, led_val); +err_out: + mutex_unlock(&led_lock); +} + +static int menf21bmc_led_probe(struct platform_device *pdev) +{ + int i; + int ret; + struct i2c_client *i2c_client = to_i2c_client(pdev->dev.parent); + + for (i = 0; i < ARRAY_SIZE(leds); i++) { + leds[i].cdev.name = leds[i].name; + leds[i].cdev.brightness_set = menf21bmc_led_set; + leds[i].i2c_client = i2c_client; + ret = devm_led_classdev_register(&pdev->dev, &leds[i].cdev); + if (ret < 0) { + dev_err(&pdev->dev, "failed to register LED device\n"); + return ret; + } + } + dev_info(&pdev->dev, "MEN 140F21P00 BMC LED device enabled\n"); + + return 0; + +} + +static struct platform_driver menf21bmc_led = { + .probe = menf21bmc_led_probe, + .driver = { + .name = "menf21bmc_led", + }, +}; + +module_platform_driver(menf21bmc_led); + +MODULE_AUTHOR("Andreas Werner <andreas.werner@men.de>"); +MODULE_DESCRIPTION("MEN 14F021P00 BMC led driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:menf21bmc_led"); diff --git a/drivers/leds/leds-mlxcpld.c b/drivers/leds/leds-mlxcpld.c new file mode 100644 index 000000000..f4721f806 --- /dev/null +++ b/drivers/leds/leds-mlxcpld.c @@ -0,0 +1,435 @@ +/* + * drivers/leds/leds-mlxcpld.c + * Copyright (c) 2016 Mellanox Technologies. All rights reserved. + * Copyright (c) 2016 Vadim Pasternak <vadimp@mellanox.com> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the names of the copyright holders nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * Alternatively, this software may be distributed under the terms of the + * GNU General Public License ("GPL") version 2 as published by the Free + * Software Foundation. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include <linux/acpi.h> +#include <linux/device.h> +#include <linux/dmi.h> +#include <linux/hwmon.h> +#include <linux/hwmon-sysfs.h> +#include <linux/io.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mod_devicetable.h> +#include <linux/platform_device.h> +#include <linux/slab.h> + +#define MLXPLAT_CPLD_LPC_REG_BASE_ADRR 0x2500 /* LPC bus access */ + +/* Color codes for LEDs */ +#define MLXCPLD_LED_OFFSET_HALF 0x01 /* Offset from solid: 3Hz blink */ +#define MLXCPLD_LED_OFFSET_FULL 0x02 /* Offset from solid: 6Hz blink */ +#define MLXCPLD_LED_IS_OFF 0x00 /* Off */ +#define MLXCPLD_LED_RED_STATIC_ON 0x05 /* Solid red */ +#define MLXCPLD_LED_RED_BLINK_HALF (MLXCPLD_LED_RED_STATIC_ON + \ + MLXCPLD_LED_OFFSET_HALF) +#define MLXCPLD_LED_RED_BLINK_FULL (MLXCPLD_LED_RED_STATIC_ON + \ + MLXCPLD_LED_OFFSET_FULL) +#define MLXCPLD_LED_GREEN_STATIC_ON 0x0D /* Solid green */ +#define MLXCPLD_LED_GREEN_BLINK_HALF (MLXCPLD_LED_GREEN_STATIC_ON + \ + MLXCPLD_LED_OFFSET_HALF) +#define MLXCPLD_LED_GREEN_BLINK_FULL (MLXCPLD_LED_GREEN_STATIC_ON + \ + MLXCPLD_LED_OFFSET_FULL) +#define MLXCPLD_LED_BLINK_3HZ 167 /* ~167 msec off/on */ +#define MLXCPLD_LED_BLINK_6HZ 83 /* ~83 msec off/on */ + +/** + * mlxcpld_param - LED access parameters: + * @offset - offset for LED access in CPLD device + * @mask - mask for LED access in CPLD device + * @base_color - base color code for LED +**/ +struct mlxcpld_param { + u8 offset; + u8 mask; + u8 base_color; +}; + +/** + * mlxcpld_led_priv - LED private data: + * @cled - LED class device instance + * @param - LED CPLD access parameters +**/ +struct mlxcpld_led_priv { + struct led_classdev cdev; + struct mlxcpld_param param; +}; + +#define cdev_to_priv(c) container_of(c, struct mlxcpld_led_priv, cdev) + +/** + * mlxcpld_led_profile - system LED profile (defined per system class): + * @offset - offset for LED access in CPLD device + * @mask - mask for LED access in CPLD device + * @base_color - base color code + * @brightness - default brightness setting (on/off) + * @name - LED name +**/ +struct mlxcpld_led_profile { + u8 offset; + u8 mask; + u8 base_color; + enum led_brightness brightness; + const char *name; +}; + +/** + * mlxcpld_led_pdata - system LED private data + * @pdev - platform device pointer + * @pled - LED class device instance + * @profile - system configuration profile + * @num_led_instances - number of LED instances + * @lock - device access lock +**/ +struct mlxcpld_led_pdata { + struct platform_device *pdev; + struct mlxcpld_led_priv *pled; + struct mlxcpld_led_profile *profile; + int num_led_instances; + spinlock_t lock; +}; + +static struct mlxcpld_led_pdata *mlxcpld_led; + +/* Default profile fit the next Mellanox systems: + * "msx6710", "msx6720", "msb7700", "msn2700", "msx1410", + * "msn2410", "msb7800", "msn2740" + */ +static struct mlxcpld_led_profile mlxcpld_led_default_profile[] = { + { + 0x21, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, 1, + "mlxcpld:fan1:green", + }, + { + 0x21, 0xf0, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, + "mlxcpld:fan1:red", + }, + { + 0x21, 0x0f, MLXCPLD_LED_GREEN_STATIC_ON, 1, + "mlxcpld:fan2:green", + }, + { + 0x21, 0x0f, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, + "mlxcpld:fan2:red", + }, + { + 0x22, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, 1, + "mlxcpld:fan3:green", + }, + { + 0x22, 0xf0, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, + "mlxcpld:fan3:red", + }, + { + 0x22, 0x0f, MLXCPLD_LED_GREEN_STATIC_ON, 1, + "mlxcpld:fan4:green", + }, + { + 0x22, 0x0f, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, + "mlxcpld:fan4:red", + }, + { + 0x20, 0x0f, MLXCPLD_LED_GREEN_STATIC_ON, 1, + "mlxcpld:psu:green", + }, + { + 0x20, 0x0f, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, + "mlxcpld:psu:red", + }, + { + 0x20, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, 1, + "mlxcpld:status:green", + }, + { + 0x20, 0xf0, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, + "mlxcpld:status:red", + }, +}; + +/* Profile fit the Mellanox systems based on "msn2100" */ +static struct mlxcpld_led_profile mlxcpld_led_msn2100_profile[] = { + { + 0x21, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, 1, + "mlxcpld:fan:green", + }, + { + 0x21, 0xf0, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, + "mlxcpld:fan:red", + }, + { + 0x23, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, 1, + "mlxcpld:psu1:green", + }, + { + 0x23, 0xf0, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, + "mlxcpld:psu1:red", + }, + { + 0x23, 0x0f, MLXCPLD_LED_GREEN_STATIC_ON, 1, + "mlxcpld:psu2:green", + }, + { + 0x23, 0x0f, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, + "mlxcpld:psu2:red", + }, + { + 0x20, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, 1, + "mlxcpld:status:green", + }, + { + 0x20, 0xf0, MLXCPLD_LED_RED_STATIC_ON, LED_OFF, + "mlxcpld:status:red", + }, + { + 0x24, 0xf0, MLXCPLD_LED_GREEN_STATIC_ON, LED_OFF, + "mlxcpld:uid:blue", + }, +}; + +enum mlxcpld_led_platform_types { + MLXCPLD_LED_PLATFORM_DEFAULT, + MLXCPLD_LED_PLATFORM_MSN2100, +}; + +static const char *mlx_product_names[] = { + "DEFAULT", + "MSN2100", +}; + +static enum +mlxcpld_led_platform_types mlxcpld_led_platform_check_sys_type(void) +{ + const char *mlx_product_name; + int i; + + mlx_product_name = dmi_get_system_info(DMI_PRODUCT_NAME); + if (!mlx_product_name) + return MLXCPLD_LED_PLATFORM_DEFAULT; + + for (i = 1; i < ARRAY_SIZE(mlx_product_names); i++) { + if (strstr(mlx_product_name, mlx_product_names[i])) + return i; + } + + return MLXCPLD_LED_PLATFORM_DEFAULT; +} + +static void mlxcpld_led_bus_access_func(u16 base, u8 offset, u8 rw_flag, + u8 *data) +{ + u32 addr = base + offset; + + if (rw_flag == 0) + outb(*data, addr); + else + *data = inb(addr); +} + +static void mlxcpld_led_store_hw(u8 mask, u8 off, u8 vset) +{ + u8 nib, val; + + /* + * Each LED is controlled through low or high nibble of the relevant + * CPLD register. Register offset is specified by off parameter. + * Parameter vset provides color code: 0x0 for off, 0x5 for solid red, + * 0x6 for 3Hz blink red, 0xd for solid green, 0xe for 3Hz blink + * green. + * Parameter mask specifies which nibble is used for specific LED: mask + * 0xf0 - lower nibble is to be used (bits from 0 to 3), mask 0x0f - + * higher nibble (bits from 4 to 7). + */ + spin_lock(&mlxcpld_led->lock); + mlxcpld_led_bus_access_func(MLXPLAT_CPLD_LPC_REG_BASE_ADRR, off, 1, + &val); + nib = (mask == 0xf0) ? vset : (vset << 4); + val = (val & mask) | nib; + mlxcpld_led_bus_access_func(MLXPLAT_CPLD_LPC_REG_BASE_ADRR, off, 0, + &val); + spin_unlock(&mlxcpld_led->lock); +} + +static void mlxcpld_led_brightness_set(struct led_classdev *led, + enum led_brightness value) +{ + struct mlxcpld_led_priv *pled = cdev_to_priv(led); + + if (value) { + mlxcpld_led_store_hw(pled->param.mask, pled->param.offset, + pled->param.base_color); + return; + } + + mlxcpld_led_store_hw(pled->param.mask, pled->param.offset, + MLXCPLD_LED_IS_OFF); +} + +static int mlxcpld_led_blink_set(struct led_classdev *led, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct mlxcpld_led_priv *pled = cdev_to_priv(led); + + /* + * HW supports two types of blinking: full (6Hz) and half (3Hz). + * For delay on/off zero default setting 3Hz is used. + */ + if (!(*delay_on == 0 && *delay_off == 0) && + !(*delay_on == MLXCPLD_LED_BLINK_3HZ && + *delay_off == MLXCPLD_LED_BLINK_3HZ) && + !(*delay_on == MLXCPLD_LED_BLINK_6HZ && + *delay_off == MLXCPLD_LED_BLINK_6HZ)) + return -EINVAL; + + if (*delay_on == MLXCPLD_LED_BLINK_6HZ) + mlxcpld_led_store_hw(pled->param.mask, pled->param.offset, + pled->param.base_color + + MLXCPLD_LED_OFFSET_FULL); + else + mlxcpld_led_store_hw(pled->param.mask, pled->param.offset, + pled->param.base_color + + MLXCPLD_LED_OFFSET_HALF); + + return 0; +} + +static int mlxcpld_led_config(struct device *dev, + struct mlxcpld_led_pdata *cpld) +{ + int i; + int err; + + cpld->pled = devm_kcalloc(dev, + cpld->num_led_instances, + sizeof(struct mlxcpld_led_priv), + GFP_KERNEL); + if (!cpld->pled) + return -ENOMEM; + + for (i = 0; i < cpld->num_led_instances; i++) { + cpld->pled[i].cdev.name = cpld->profile[i].name; + cpld->pled[i].cdev.brightness = cpld->profile[i].brightness; + cpld->pled[i].cdev.max_brightness = 1; + cpld->pled[i].cdev.brightness_set = mlxcpld_led_brightness_set; + cpld->pled[i].cdev.blink_set = mlxcpld_led_blink_set; + cpld->pled[i].cdev.flags = LED_CORE_SUSPENDRESUME; + err = devm_led_classdev_register(dev, &cpld->pled[i].cdev); + if (err) + return err; + + cpld->pled[i].param.offset = mlxcpld_led->profile[i].offset; + cpld->pled[i].param.mask = mlxcpld_led->profile[i].mask; + cpld->pled[i].param.base_color = + mlxcpld_led->profile[i].base_color; + + if (mlxcpld_led->profile[i].brightness) + mlxcpld_led_brightness_set(&cpld->pled[i].cdev, + mlxcpld_led->profile[i].brightness); + } + + return 0; +} + +static int __init mlxcpld_led_probe(struct platform_device *pdev) +{ + enum mlxcpld_led_platform_types mlxcpld_led_plat = + mlxcpld_led_platform_check_sys_type(); + + mlxcpld_led = devm_kzalloc(&pdev->dev, sizeof(*mlxcpld_led), + GFP_KERNEL); + if (!mlxcpld_led) + return -ENOMEM; + + mlxcpld_led->pdev = pdev; + + switch (mlxcpld_led_plat) { + case MLXCPLD_LED_PLATFORM_MSN2100: + mlxcpld_led->profile = mlxcpld_led_msn2100_profile; + mlxcpld_led->num_led_instances = + ARRAY_SIZE(mlxcpld_led_msn2100_profile); + break; + + default: + mlxcpld_led->profile = mlxcpld_led_default_profile; + mlxcpld_led->num_led_instances = + ARRAY_SIZE(mlxcpld_led_default_profile); + break; + } + + spin_lock_init(&mlxcpld_led->lock); + + return mlxcpld_led_config(&pdev->dev, mlxcpld_led); +} + +static struct platform_driver mlxcpld_led_driver = { + .driver = { + .name = KBUILD_MODNAME, + }, +}; + +static int __init mlxcpld_led_init(void) +{ + struct platform_device *pdev; + int err; + + if (!dmi_match(DMI_CHASSIS_VENDOR, "Mellanox Technologies Ltd.")) + return -ENODEV; + + pdev = platform_device_register_simple(KBUILD_MODNAME, -1, NULL, 0); + if (IS_ERR(pdev)) { + pr_err("Device allocation failed\n"); + return PTR_ERR(pdev); + } + + err = platform_driver_probe(&mlxcpld_led_driver, mlxcpld_led_probe); + if (err) { + pr_err("Probe platform driver failed\n"); + platform_device_unregister(pdev); + } + + return err; +} + +static void __exit mlxcpld_led_exit(void) +{ + platform_device_unregister(mlxcpld_led->pdev); + platform_driver_unregister(&mlxcpld_led_driver); +} + +module_init(mlxcpld_led_init); +module_exit(mlxcpld_led_exit); + +MODULE_AUTHOR("Vadim Pasternak <vadimp@mellanox.com>"); +MODULE_DESCRIPTION("Mellanox board LED driver"); +MODULE_LICENSE("Dual BSD/GPL"); +MODULE_ALIAS("platform:leds_mlxcpld"); diff --git a/drivers/leds/leds-mlxreg.c b/drivers/leds/leds-mlxreg.c new file mode 100644 index 000000000..82aea1cd0 --- /dev/null +++ b/drivers/leds/leds-mlxreg.c @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: (GPL-2.0 OR BSD-3-Clause) +// +// Copyright (c) 2018 Mellanox Technologies. All rights reserved. +// Copyright (c) 2018 Vadim Pasternak <vadimp@mellanox.com> + +#include <linux/bitops.h> +#include <linux/device.h> +#include <linux/io.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/platform_data/mlxreg.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> + +/* Codes for LEDs. */ +#define MLXREG_LED_OFFSET_BLINK_3HZ 0x01 /* Offset from solid: 3Hz blink */ +#define MLXREG_LED_OFFSET_BLINK_6HZ 0x02 /* Offset from solid: 6Hz blink */ +#define MLXREG_LED_IS_OFF 0x00 /* Off */ +#define MLXREG_LED_RED_SOLID 0x05 /* Solid red */ +#define MLXREG_LED_GREEN_SOLID 0x0D /* Solid green */ +#define MLXREG_LED_AMBER_SOLID 0x09 /* Solid amber */ +#define MLXREG_LED_BLINK_3HZ 167 /* ~167 msec off/on - HW support */ +#define MLXREG_LED_BLINK_6HZ 83 /* ~83 msec off/on - HW support */ +#define MLXREG_LED_CAPABILITY_CLEAR GENMASK(31, 8) /* Clear mask */ + +/** + * struct mlxreg_led_data - led control data: + * + * @data: led configuration data; + * @led_classdev: led class data; + * @base_color: base led color (other colors have constant offset from base); + * @led_data: led data; + * @data_parent: pointer to private device control data of parent; + */ +struct mlxreg_led_data { + struct mlxreg_core_data *data; + struct led_classdev led_cdev; + u8 base_color; + void *data_parent; + char led_cdev_name[MLXREG_CORE_LABEL_MAX_SIZE]; +}; + +#define cdev_to_priv(c) container_of(c, struct mlxreg_led_data, led_cdev) + +/** + * struct mlxreg_led_priv_data - platform private data: + * + * @pdev: platform device; + * @pdata: platform data; + * @access_lock: mutex for attribute IO access; + */ +struct mlxreg_led_priv_data { + struct platform_device *pdev; + struct mlxreg_core_platform_data *pdata; + struct mutex access_lock; /* protect IO operations */ +}; + +static int +mlxreg_led_store_hw(struct mlxreg_led_data *led_data, u8 vset) +{ + struct mlxreg_led_priv_data *priv = led_data->data_parent; + struct mlxreg_core_platform_data *led_pdata = priv->pdata; + struct mlxreg_core_data *data = led_data->data; + u32 regval; + u32 nib; + int ret; + + /* + * Each LED is controlled through low or high nibble of the relevant + * register byte. Register offset is specified by off parameter. + * Parameter vset provides color code: 0x0 for off, 0x5 for solid red, + * 0x6 for 3Hz blink red, 0xd for solid green, 0xe for 3Hz blink + * green. + * Parameter mask specifies which nibble is used for specific LED: mask + * 0xf0 - lower nibble is to be used (bits from 0 to 3), mask 0x0f - + * higher nibble (bits from 4 to 7). + */ + mutex_lock(&priv->access_lock); + + ret = regmap_read(led_pdata->regmap, data->reg, ®val); + if (ret) + goto access_error; + + nib = (ror32(data->mask, data->bit) == 0xf0) ? rol32(vset, data->bit) : + rol32(vset, data->bit + 4); + regval = (regval & data->mask) | nib; + + ret = regmap_write(led_pdata->regmap, data->reg, regval); + +access_error: + mutex_unlock(&priv->access_lock); + + return ret; +} + +static enum led_brightness +mlxreg_led_get_hw(struct mlxreg_led_data *led_data) +{ + struct mlxreg_led_priv_data *priv = led_data->data_parent; + struct mlxreg_core_platform_data *led_pdata = priv->pdata; + struct mlxreg_core_data *data = led_data->data; + u32 regval; + int err; + + /* + * Each LED is controlled through low or high nibble of the relevant + * register byte. Register offset is specified by off parameter. + * Parameter vset provides color code: 0x0 for off, 0x5 for solid red, + * 0x6 for 3Hz blink red, 0xd for solid green, 0xe for 3Hz blink + * green. + * Parameter mask specifies which nibble is used for specific LED: mask + * 0xf0 - lower nibble is to be used (bits from 0 to 3), mask 0x0f - + * higher nibble (bits from 4 to 7). + */ + err = regmap_read(led_pdata->regmap, data->reg, ®val); + if (err < 0) { + dev_warn(led_data->led_cdev.dev, "Failed to get current brightness, error: %d\n", + err); + /* Assume the LED is OFF */ + return LED_OFF; + } + + regval = regval & ~data->mask; + regval = (ror32(data->mask, data->bit) == 0xf0) ? ror32(regval, + data->bit) : ror32(regval, data->bit + 4); + if (regval >= led_data->base_color && + regval <= (led_data->base_color + MLXREG_LED_OFFSET_BLINK_6HZ)) + return LED_FULL; + + return LED_OFF; +} + +static int +mlxreg_led_brightness_set(struct led_classdev *cled, enum led_brightness value) +{ + struct mlxreg_led_data *led_data = cdev_to_priv(cled); + + if (value) + return mlxreg_led_store_hw(led_data, led_data->base_color); + else + return mlxreg_led_store_hw(led_data, MLXREG_LED_IS_OFF); +} + +static enum led_brightness +mlxreg_led_brightness_get(struct led_classdev *cled) +{ + struct mlxreg_led_data *led_data = cdev_to_priv(cled); + + return mlxreg_led_get_hw(led_data); +} + +static int +mlxreg_led_blink_set(struct led_classdev *cled, unsigned long *delay_on, + unsigned long *delay_off) +{ + struct mlxreg_led_data *led_data = cdev_to_priv(cled); + int err; + + /* + * HW supports two types of blinking: full (6Hz) and half (3Hz). + * For delay on/off zero LED is setting to solid color. For others + * combination blinking is to be controlled by the software timer. + */ + if (!(*delay_on == 0 && *delay_off == 0) && + !(*delay_on == MLXREG_LED_BLINK_3HZ && + *delay_off == MLXREG_LED_BLINK_3HZ) && + !(*delay_on == MLXREG_LED_BLINK_6HZ && + *delay_off == MLXREG_LED_BLINK_6HZ)) + return -EINVAL; + + if (*delay_on == MLXREG_LED_BLINK_6HZ) + err = mlxreg_led_store_hw(led_data, led_data->base_color + + MLXREG_LED_OFFSET_BLINK_6HZ); + else if (*delay_on == MLXREG_LED_BLINK_3HZ) + err = mlxreg_led_store_hw(led_data, led_data->base_color + + MLXREG_LED_OFFSET_BLINK_3HZ); + else + err = mlxreg_led_store_hw(led_data, led_data->base_color); + + return err; +} + +static int mlxreg_led_config(struct mlxreg_led_priv_data *priv) +{ + struct mlxreg_core_platform_data *led_pdata = priv->pdata; + struct mlxreg_core_data *data = led_pdata->data; + struct mlxreg_led_data *led_data; + struct led_classdev *led_cdev; + enum led_brightness brightness; + u32 regval; + int i; + int err; + + for (i = 0; i < led_pdata->counter; i++, data++) { + led_data = devm_kzalloc(&priv->pdev->dev, sizeof(*led_data), + GFP_KERNEL); + if (!led_data) + return -ENOMEM; + + if (data->capability) { + err = regmap_read(led_pdata->regmap, data->capability, + ®val); + if (err) { + dev_err(&priv->pdev->dev, "Failed to query capability register\n"); + return err; + } + if (!(regval & data->bit)) + continue; + /* + * Field "bit" can contain one capability bit in 0 byte + * and offset bit in 1-3 bytes. Clear capability bit and + * keep only offset bit. + */ + data->bit &= MLXREG_LED_CAPABILITY_CLEAR; + } + + led_cdev = &led_data->led_cdev; + led_data->data_parent = priv; + if (strstr(data->label, "red") || + strstr(data->label, "orange")) { + brightness = LED_OFF; + led_data->base_color = MLXREG_LED_RED_SOLID; + } else if (strstr(data->label, "amber")) { + brightness = LED_OFF; + led_data->base_color = MLXREG_LED_AMBER_SOLID; + } else { + brightness = LED_OFF; + led_data->base_color = MLXREG_LED_GREEN_SOLID; + } + snprintf(led_data->led_cdev_name, sizeof(led_data->led_cdev_name), + "mlxreg:%s", data->label); + led_cdev->name = led_data->led_cdev_name; + led_cdev->brightness = brightness; + led_cdev->max_brightness = LED_ON; + led_cdev->brightness_set_blocking = + mlxreg_led_brightness_set; + led_cdev->brightness_get = mlxreg_led_brightness_get; + led_cdev->blink_set = mlxreg_led_blink_set; + led_cdev->flags = LED_CORE_SUSPENDRESUME; + led_data->data = data; + err = devm_led_classdev_register(&priv->pdev->dev, led_cdev); + if (err) + return err; + + if (led_cdev->brightness) + mlxreg_led_brightness_set(led_cdev, + led_cdev->brightness); + dev_info(led_cdev->dev, "label: %s, mask: 0x%02x, offset:0x%02x\n", + data->label, data->mask, data->reg); + } + + return 0; +} + +static int mlxreg_led_probe(struct platform_device *pdev) +{ + struct mlxreg_core_platform_data *led_pdata; + struct mlxreg_led_priv_data *priv; + + led_pdata = dev_get_platdata(&pdev->dev); + if (!led_pdata) { + dev_err(&pdev->dev, "Failed to get platform data.\n"); + return -EINVAL; + } + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + mutex_init(&priv->access_lock); + priv->pdev = pdev; + priv->pdata = led_pdata; + + return mlxreg_led_config(priv); +} + +static int mlxreg_led_remove(struct platform_device *pdev) +{ + struct mlxreg_led_priv_data *priv = dev_get_drvdata(&pdev->dev); + + mutex_destroy(&priv->access_lock); + + return 0; +} + +static struct platform_driver mlxreg_led_driver = { + .driver = { + .name = "leds-mlxreg", + }, + .probe = mlxreg_led_probe, + .remove = mlxreg_led_remove, +}; + +module_platform_driver(mlxreg_led_driver); + +MODULE_AUTHOR("Vadim Pasternak <vadimp@mellanox.com>"); +MODULE_DESCRIPTION("Mellanox LED regmap driver"); +MODULE_LICENSE("Dual BSD/GPL"); +MODULE_ALIAS("platform:leds-mlxreg"); diff --git a/drivers/leds/leds-mt6323.c b/drivers/leds/leds-mt6323.c new file mode 100644 index 000000000..f59e0e8bd --- /dev/null +++ b/drivers/leds/leds-mt6323.c @@ -0,0 +1,491 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * LED driver for Mediatek MT6323 PMIC + * + * Copyright (C) 2017 Sean Wang <sean.wang@mediatek.com> + */ +#include <linux/kernel.h> +#include <linux/leds.h> +#include <linux/mfd/mt6323/registers.h> +#include <linux/mfd/mt6397/core.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> + +/* + * Register field for MT6323_TOP_CKPDN0 to enable + * 32K clock common for LED device. + */ +#define MT6323_RG_DRV_32K_CK_PDN BIT(11) +#define MT6323_RG_DRV_32K_CK_PDN_MASK BIT(11) + +/* + * Register field for MT6323_TOP_CKPDN2 to enable + * individual clock for LED device. + */ +#define MT6323_RG_ISINK_CK_PDN(i) BIT(i) +#define MT6323_RG_ISINK_CK_PDN_MASK(i) BIT(i) + +/* + * Register field for MT6323_TOP_CKCON1 to select + * clock source. + */ +#define MT6323_RG_ISINK_CK_SEL_MASK(i) (BIT(10) << (i)) + +/* + * Register for MT6323_ISINK_CON0 to setup the + * duty cycle of the blink. + */ +#define MT6323_ISINK_CON0(i) (MT6323_ISINK0_CON0 + 0x8 * (i)) +#define MT6323_ISINK_DIM_DUTY_MASK (0x1f << 8) +#define MT6323_ISINK_DIM_DUTY(i) (((i) << 8) & \ + MT6323_ISINK_DIM_DUTY_MASK) + +/* Register to setup the period of the blink. */ +#define MT6323_ISINK_CON1(i) (MT6323_ISINK0_CON1 + 0x8 * (i)) +#define MT6323_ISINK_DIM_FSEL_MASK (0xffff) +#define MT6323_ISINK_DIM_FSEL(i) ((i) & MT6323_ISINK_DIM_FSEL_MASK) + +/* Register to control the brightness. */ +#define MT6323_ISINK_CON2(i) (MT6323_ISINK0_CON2 + 0x8 * (i)) +#define MT6323_ISINK_CH_STEP_SHIFT 12 +#define MT6323_ISINK_CH_STEP_MASK (0x7 << 12) +#define MT6323_ISINK_CH_STEP(i) (((i) << 12) & \ + MT6323_ISINK_CH_STEP_MASK) +#define MT6323_ISINK_SFSTR0_TC_MASK (0x3 << 1) +#define MT6323_ISINK_SFSTR0_TC(i) (((i) << 1) & \ + MT6323_ISINK_SFSTR0_TC_MASK) +#define MT6323_ISINK_SFSTR0_EN_MASK BIT(0) +#define MT6323_ISINK_SFSTR0_EN BIT(0) + +/* Register to LED channel enablement. */ +#define MT6323_ISINK_CH_EN_MASK(i) BIT(i) +#define MT6323_ISINK_CH_EN(i) BIT(i) + +#define MT6323_MAX_PERIOD 10000 +#define MT6323_MAX_LEDS 4 +#define MT6323_MAX_BRIGHTNESS 6 +#define MT6323_UNIT_DUTY 3125 +#define MT6323_CAL_HW_DUTY(o, p) DIV_ROUND_CLOSEST((o) * 100000ul,\ + (p) * MT6323_UNIT_DUTY) + +struct mt6323_leds; + +/** + * struct mt6323_led - state container for the LED device + * @id: the identifier in MT6323 LED device + * @parent: the pointer to MT6323 LED controller + * @cdev: LED class device for this LED device + * @current_brightness: current state of the LED device + */ +struct mt6323_led { + int id; + struct mt6323_leds *parent; + struct led_classdev cdev; + enum led_brightness current_brightness; +}; + +/** + * struct mt6323_leds - state container for holding LED controller + * of the driver + * @dev: the device pointer + * @hw: the underlying hardware providing shared + * bus for the register operations + * @lock: the lock among process context + * @led: the array that contains the state of individual + * LED device + */ +struct mt6323_leds { + struct device *dev; + struct mt6397_chip *hw; + /* protect among process context */ + struct mutex lock; + struct mt6323_led *led[MT6323_MAX_LEDS]; +}; + +static int mt6323_led_hw_brightness(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct mt6323_led *led = container_of(cdev, struct mt6323_led, cdev); + struct mt6323_leds *leds = led->parent; + struct regmap *regmap = leds->hw->regmap; + u32 con2_mask = 0, con2_val = 0; + int ret; + + /* + * Setup current output for the corresponding + * brightness level. + */ + con2_mask |= MT6323_ISINK_CH_STEP_MASK | + MT6323_ISINK_SFSTR0_TC_MASK | + MT6323_ISINK_SFSTR0_EN_MASK; + con2_val |= MT6323_ISINK_CH_STEP(brightness - 1) | + MT6323_ISINK_SFSTR0_TC(2) | + MT6323_ISINK_SFSTR0_EN; + + ret = regmap_update_bits(regmap, MT6323_ISINK_CON2(led->id), + con2_mask, con2_val); + return ret; +} + +static int mt6323_led_hw_off(struct led_classdev *cdev) +{ + struct mt6323_led *led = container_of(cdev, struct mt6323_led, cdev); + struct mt6323_leds *leds = led->parent; + struct regmap *regmap = leds->hw->regmap; + unsigned int status; + int ret; + + status = MT6323_ISINK_CH_EN(led->id); + ret = regmap_update_bits(regmap, MT6323_ISINK_EN_CTRL, + MT6323_ISINK_CH_EN_MASK(led->id), ~status); + if (ret < 0) + return ret; + + usleep_range(100, 300); + ret = regmap_update_bits(regmap, MT6323_TOP_CKPDN2, + MT6323_RG_ISINK_CK_PDN_MASK(led->id), + MT6323_RG_ISINK_CK_PDN(led->id)); + if (ret < 0) + return ret; + + return 0; +} + +static enum led_brightness +mt6323_get_led_hw_brightness(struct led_classdev *cdev) +{ + struct mt6323_led *led = container_of(cdev, struct mt6323_led, cdev); + struct mt6323_leds *leds = led->parent; + struct regmap *regmap = leds->hw->regmap; + unsigned int status; + int ret; + + ret = regmap_read(regmap, MT6323_TOP_CKPDN2, &status); + if (ret < 0) + return ret; + + if (status & MT6323_RG_ISINK_CK_PDN_MASK(led->id)) + return 0; + + ret = regmap_read(regmap, MT6323_ISINK_EN_CTRL, &status); + if (ret < 0) + return ret; + + if (!(status & MT6323_ISINK_CH_EN(led->id))) + return 0; + + ret = regmap_read(regmap, MT6323_ISINK_CON2(led->id), &status); + if (ret < 0) + return ret; + + return ((status & MT6323_ISINK_CH_STEP_MASK) + >> MT6323_ISINK_CH_STEP_SHIFT) + 1; +} + +static int mt6323_led_hw_on(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct mt6323_led *led = container_of(cdev, struct mt6323_led, cdev); + struct mt6323_leds *leds = led->parent; + struct regmap *regmap = leds->hw->regmap; + unsigned int status; + int ret; + + /* + * Setup required clock source, enable the corresponding + * clock and channel and let work with continuous blink as + * the default. + */ + ret = regmap_update_bits(regmap, MT6323_TOP_CKCON1, + MT6323_RG_ISINK_CK_SEL_MASK(led->id), 0); + if (ret < 0) + return ret; + + status = MT6323_RG_ISINK_CK_PDN(led->id); + ret = regmap_update_bits(regmap, MT6323_TOP_CKPDN2, + MT6323_RG_ISINK_CK_PDN_MASK(led->id), + ~status); + if (ret < 0) + return ret; + + usleep_range(100, 300); + + ret = regmap_update_bits(regmap, MT6323_ISINK_EN_CTRL, + MT6323_ISINK_CH_EN_MASK(led->id), + MT6323_ISINK_CH_EN(led->id)); + if (ret < 0) + return ret; + + ret = mt6323_led_hw_brightness(cdev, brightness); + if (ret < 0) + return ret; + + ret = regmap_update_bits(regmap, MT6323_ISINK_CON0(led->id), + MT6323_ISINK_DIM_DUTY_MASK, + MT6323_ISINK_DIM_DUTY(31)); + if (ret < 0) + return ret; + + ret = regmap_update_bits(regmap, MT6323_ISINK_CON1(led->id), + MT6323_ISINK_DIM_FSEL_MASK, + MT6323_ISINK_DIM_FSEL(1000)); + if (ret < 0) + return ret; + + return 0; +} + +static int mt6323_led_set_blink(struct led_classdev *cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct mt6323_led *led = container_of(cdev, struct mt6323_led, cdev); + struct mt6323_leds *leds = led->parent; + struct regmap *regmap = leds->hw->regmap; + unsigned long period; + u8 duty_hw; + int ret; + + /* + * LED subsystem requires a default user + * friendly blink pattern for the LED so using + * 1Hz duty cycle 50% here if without specific + * value delay_on and delay off being assigned. + */ + if (!*delay_on && !*delay_off) { + *delay_on = 500; + *delay_off = 500; + } + + /* + * Units are in ms, if over the hardware able + * to support, fallback into software blink + */ + period = *delay_on + *delay_off; + + if (period > MT6323_MAX_PERIOD) + return -EINVAL; + + /* + * Calculate duty_hw based on the percentage of period during + * which the led is ON. + */ + duty_hw = MT6323_CAL_HW_DUTY(*delay_on, period); + + /* hardware doesn't support zero duty cycle. */ + if (!duty_hw) + return -EINVAL; + + mutex_lock(&leds->lock); + /* + * Set max_brightness as the software blink behavior + * when no blink brightness. + */ + if (!led->current_brightness) { + ret = mt6323_led_hw_on(cdev, cdev->max_brightness); + if (ret < 0) + goto out; + led->current_brightness = cdev->max_brightness; + } + + ret = regmap_update_bits(regmap, MT6323_ISINK_CON0(led->id), + MT6323_ISINK_DIM_DUTY_MASK, + MT6323_ISINK_DIM_DUTY(duty_hw - 1)); + if (ret < 0) + goto out; + + ret = regmap_update_bits(regmap, MT6323_ISINK_CON1(led->id), + MT6323_ISINK_DIM_FSEL_MASK, + MT6323_ISINK_DIM_FSEL(period - 1)); +out: + mutex_unlock(&leds->lock); + + return ret; +} + +static int mt6323_led_set_brightness(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct mt6323_led *led = container_of(cdev, struct mt6323_led, cdev); + struct mt6323_leds *leds = led->parent; + int ret; + + mutex_lock(&leds->lock); + + if (!led->current_brightness && brightness) { + ret = mt6323_led_hw_on(cdev, brightness); + if (ret < 0) + goto out; + } else if (brightness) { + ret = mt6323_led_hw_brightness(cdev, brightness); + if (ret < 0) + goto out; + } else { + ret = mt6323_led_hw_off(cdev); + if (ret < 0) + goto out; + } + + led->current_brightness = brightness; +out: + mutex_unlock(&leds->lock); + + return ret; +} + +static int mt6323_led_set_dt_default(struct led_classdev *cdev, + struct device_node *np) +{ + struct mt6323_led *led = container_of(cdev, struct mt6323_led, cdev); + const char *state; + int ret = 0; + + state = of_get_property(np, "default-state", NULL); + if (state) { + if (!strcmp(state, "keep")) { + ret = mt6323_get_led_hw_brightness(cdev); + if (ret < 0) + return ret; + led->current_brightness = ret; + ret = 0; + } else if (!strcmp(state, "on")) { + ret = + mt6323_led_set_brightness(cdev, cdev->max_brightness); + } else { + ret = mt6323_led_set_brightness(cdev, LED_OFF); + } + } + + return ret; +} + +static int mt6323_led_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *np = dev_of_node(dev); + struct device_node *child; + struct mt6397_chip *hw = dev_get_drvdata(dev->parent); + struct mt6323_leds *leds; + struct mt6323_led *led; + int ret; + unsigned int status; + u32 reg; + + leds = devm_kzalloc(dev, sizeof(*leds), GFP_KERNEL); + if (!leds) + return -ENOMEM; + + platform_set_drvdata(pdev, leds); + leds->dev = dev; + + /* + * leds->hw points to the underlying bus for the register + * controlled. + */ + leds->hw = hw; + mutex_init(&leds->lock); + + status = MT6323_RG_DRV_32K_CK_PDN; + ret = regmap_update_bits(leds->hw->regmap, MT6323_TOP_CKPDN0, + MT6323_RG_DRV_32K_CK_PDN_MASK, ~status); + if (ret < 0) { + dev_err(leds->dev, + "Failed to update MT6323_TOP_CKPDN0 Register\n"); + return ret; + } + + for_each_available_child_of_node(np, child) { + struct led_init_data init_data = {}; + + ret = of_property_read_u32(child, "reg", ®); + if (ret) { + dev_err(dev, "Failed to read led 'reg' property\n"); + goto put_child_node; + } + + if (reg >= MT6323_MAX_LEDS || leds->led[reg]) { + dev_err(dev, "Invalid led reg %u\n", reg); + ret = -EINVAL; + goto put_child_node; + } + + led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); + if (!led) { + ret = -ENOMEM; + goto put_child_node; + } + + leds->led[reg] = led; + leds->led[reg]->id = reg; + leds->led[reg]->cdev.max_brightness = MT6323_MAX_BRIGHTNESS; + leds->led[reg]->cdev.brightness_set_blocking = + mt6323_led_set_brightness; + leds->led[reg]->cdev.blink_set = mt6323_led_set_blink; + leds->led[reg]->cdev.brightness_get = + mt6323_get_led_hw_brightness; + leds->led[reg]->parent = leds; + + ret = mt6323_led_set_dt_default(&leds->led[reg]->cdev, child); + if (ret < 0) { + dev_err(leds->dev, + "Failed to LED set default from devicetree\n"); + goto put_child_node; + } + + init_data.fwnode = of_fwnode_handle(child); + + ret = devm_led_classdev_register_ext(dev, &leds->led[reg]->cdev, + &init_data); + if (ret) { + dev_err(dev, "Failed to register LED: %d\n", ret); + goto put_child_node; + } + } + + return 0; + +put_child_node: + of_node_put(child); + return ret; +} + +static int mt6323_led_remove(struct platform_device *pdev) +{ + struct mt6323_leds *leds = platform_get_drvdata(pdev); + int i; + + /* Turn the LEDs off on driver removal. */ + for (i = 0 ; leds->led[i] ; i++) + mt6323_led_hw_off(&leds->led[i]->cdev); + + regmap_update_bits(leds->hw->regmap, MT6323_TOP_CKPDN0, + MT6323_RG_DRV_32K_CK_PDN_MASK, + MT6323_RG_DRV_32K_CK_PDN); + + mutex_destroy(&leds->lock); + + return 0; +} + +static const struct of_device_id mt6323_led_dt_match[] = { + { .compatible = "mediatek,mt6323-led" }, + {}, +}; +MODULE_DEVICE_TABLE(of, mt6323_led_dt_match); + +static struct platform_driver mt6323_led_driver = { + .probe = mt6323_led_probe, + .remove = mt6323_led_remove, + .driver = { + .name = "mt6323-led", + .of_match_table = mt6323_led_dt_match, + }, +}; + +module_platform_driver(mt6323_led_driver); + +MODULE_DESCRIPTION("LED driver for Mediatek MT6323 PMIC"); +MODULE_AUTHOR("Sean Wang <sean.wang@mediatek.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-net48xx.c b/drivers/leds/leds-net48xx.c new file mode 100644 index 000000000..a93468c13 --- /dev/null +++ b/drivers/leds/leds-net48xx.c @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LEDs driver for Soekris net48xx + * + * Copyright (C) 2006 Chris Boot <bootc@bootc.net> + * + * Based on leds-ams-delta.c + */ + +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/nsc_gpio.h> +#include <linux/scx200_gpio.h> +#include <linux/module.h> + +#define DRVNAME "net48xx-led" +#define NET48XX_ERROR_LED_GPIO 20 + +static struct platform_device *pdev; + +static void net48xx_error_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + scx200_gpio_ops.gpio_set(NET48XX_ERROR_LED_GPIO, value ? 1 : 0); +} + +static struct led_classdev net48xx_error_led = { + .name = "net48xx::error", + .brightness_set = net48xx_error_led_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static int net48xx_led_probe(struct platform_device *pdev) +{ + return devm_led_classdev_register(&pdev->dev, &net48xx_error_led); +} + +static struct platform_driver net48xx_led_driver = { + .probe = net48xx_led_probe, + .driver = { + .name = DRVNAME, + }, +}; + +static int __init net48xx_led_init(void) +{ + int ret; + + /* small hack, but scx200_gpio doesn't set .dev if the probe fails */ + if (!scx200_gpio_ops.dev) { + ret = -ENODEV; + goto out; + } + + ret = platform_driver_register(&net48xx_led_driver); + if (ret < 0) + goto out; + + pdev = platform_device_register_simple(DRVNAME, -1, NULL, 0); + if (IS_ERR(pdev)) { + ret = PTR_ERR(pdev); + platform_driver_unregister(&net48xx_led_driver); + goto out; + } + +out: + return ret; +} + +static void __exit net48xx_led_exit(void) +{ + platform_device_unregister(pdev); + platform_driver_unregister(&net48xx_led_driver); +} + +module_init(net48xx_led_init); +module_exit(net48xx_led_exit); + +MODULE_AUTHOR("Chris Boot <bootc@bootc.net>"); +MODULE_DESCRIPTION("Soekris net48xx LED driver"); +MODULE_LICENSE("GPL"); + diff --git a/drivers/leds/leds-netxbig.c b/drivers/leds/leds-netxbig.c new file mode 100644 index 000000000..68fbf0b66 --- /dev/null +++ b/drivers/leds/leds-netxbig.c @@ -0,0 +1,642 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * leds-netxbig.c - Driver for the 2Big and 5Big Network series LEDs + * + * Copyright (C) 2010 LaCie + * + * Author: Simon Guinot <sguinot@lacie.com> + */ + +#include <linux/module.h> +#include <linux/irq.h> +#include <linux/slab.h> +#include <linux/spinlock.h> +#include <linux/platform_device.h> +#include <linux/gpio/consumer.h> +#include <linux/leds.h> +#include <linux/of.h> +#include <linux/of_platform.h> + +struct netxbig_gpio_ext { + struct gpio_desc **addr; + int num_addr; + struct gpio_desc **data; + int num_data; + struct gpio_desc *enable; +}; + +enum netxbig_led_mode { + NETXBIG_LED_OFF, + NETXBIG_LED_ON, + NETXBIG_LED_SATA, + NETXBIG_LED_TIMER1, + NETXBIG_LED_TIMER2, + NETXBIG_LED_MODE_NUM, +}; + +#define NETXBIG_LED_INVALID_MODE NETXBIG_LED_MODE_NUM + +struct netxbig_led_timer { + unsigned long delay_on; + unsigned long delay_off; + enum netxbig_led_mode mode; +}; + +struct netxbig_led { + const char *name; + const char *default_trigger; + int mode_addr; + int *mode_val; + int bright_addr; + int bright_max; +}; + +struct netxbig_led_platform_data { + struct netxbig_gpio_ext *gpio_ext; + struct netxbig_led_timer *timer; + int num_timer; + struct netxbig_led *leds; + int num_leds; +}; + +/* + * GPIO extension bus. + */ + +static DEFINE_SPINLOCK(gpio_ext_lock); + +static void gpio_ext_set_addr(struct netxbig_gpio_ext *gpio_ext, int addr) +{ + int pin; + + for (pin = 0; pin < gpio_ext->num_addr; pin++) + gpiod_set_value(gpio_ext->addr[pin], (addr >> pin) & 1); +} + +static void gpio_ext_set_data(struct netxbig_gpio_ext *gpio_ext, int data) +{ + int pin; + + for (pin = 0; pin < gpio_ext->num_data; pin++) + gpiod_set_value(gpio_ext->data[pin], (data >> pin) & 1); +} + +static void gpio_ext_enable_select(struct netxbig_gpio_ext *gpio_ext) +{ + /* Enable select is done on the raising edge. */ + gpiod_set_value(gpio_ext->enable, 0); + gpiod_set_value(gpio_ext->enable, 1); +} + +static void gpio_ext_set_value(struct netxbig_gpio_ext *gpio_ext, + int addr, int value) +{ + unsigned long flags; + + spin_lock_irqsave(&gpio_ext_lock, flags); + gpio_ext_set_addr(gpio_ext, addr); + gpio_ext_set_data(gpio_ext, value); + gpio_ext_enable_select(gpio_ext); + spin_unlock_irqrestore(&gpio_ext_lock, flags); +} + +/* + * Class LED driver. + */ + +struct netxbig_led_data { + struct netxbig_gpio_ext *gpio_ext; + struct led_classdev cdev; + int mode_addr; + int *mode_val; + int bright_addr; + struct netxbig_led_timer *timer; + int num_timer; + enum netxbig_led_mode mode; + int sata; + spinlock_t lock; +}; + +static int netxbig_led_get_timer_mode(enum netxbig_led_mode *mode, + unsigned long delay_on, + unsigned long delay_off, + struct netxbig_led_timer *timer, + int num_timer) +{ + int i; + + for (i = 0; i < num_timer; i++) { + if (timer[i].delay_on == delay_on && + timer[i].delay_off == delay_off) { + *mode = timer[i].mode; + return 0; + } + } + return -EINVAL; +} + +static int netxbig_led_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct netxbig_led_data *led_dat = + container_of(led_cdev, struct netxbig_led_data, cdev); + enum netxbig_led_mode mode; + int mode_val; + int ret; + + /* Look for a LED mode with the requested timer frequency. */ + ret = netxbig_led_get_timer_mode(&mode, *delay_on, *delay_off, + led_dat->timer, led_dat->num_timer); + if (ret < 0) + return ret; + + mode_val = led_dat->mode_val[mode]; + if (mode_val == NETXBIG_LED_INVALID_MODE) + return -EINVAL; + + spin_lock_irq(&led_dat->lock); + + gpio_ext_set_value(led_dat->gpio_ext, led_dat->mode_addr, mode_val); + led_dat->mode = mode; + + spin_unlock_irq(&led_dat->lock); + + return 0; +} + +static void netxbig_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct netxbig_led_data *led_dat = + container_of(led_cdev, struct netxbig_led_data, cdev); + enum netxbig_led_mode mode; + int mode_val; + int set_brightness = 1; + unsigned long flags; + + spin_lock_irqsave(&led_dat->lock, flags); + + if (value == LED_OFF) { + mode = NETXBIG_LED_OFF; + set_brightness = 0; + } else { + if (led_dat->sata) + mode = NETXBIG_LED_SATA; + else if (led_dat->mode == NETXBIG_LED_OFF) + mode = NETXBIG_LED_ON; + else /* Keep 'timer' mode. */ + mode = led_dat->mode; + } + mode_val = led_dat->mode_val[mode]; + + gpio_ext_set_value(led_dat->gpio_ext, led_dat->mode_addr, mode_val); + led_dat->mode = mode; + /* + * Note that the brightness register is shared between all the + * SATA LEDs. So, change the brightness setting for a single + * SATA LED will affect all the others. + */ + if (set_brightness) + gpio_ext_set_value(led_dat->gpio_ext, + led_dat->bright_addr, value); + + spin_unlock_irqrestore(&led_dat->lock, flags); +} + +static ssize_t netxbig_led_sata_store(struct device *dev, + struct device_attribute *attr, + const char *buff, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct netxbig_led_data *led_dat = + container_of(led_cdev, struct netxbig_led_data, cdev); + unsigned long enable; + enum netxbig_led_mode mode; + int mode_val; + int ret; + + ret = kstrtoul(buff, 10, &enable); + if (ret < 0) + return ret; + + enable = !!enable; + + spin_lock_irq(&led_dat->lock); + + if (led_dat->sata == enable) { + ret = count; + goto exit_unlock; + } + + if (led_dat->mode != NETXBIG_LED_ON && + led_dat->mode != NETXBIG_LED_SATA) + mode = led_dat->mode; /* Keep modes 'off' and 'timer'. */ + else if (enable) + mode = NETXBIG_LED_SATA; + else + mode = NETXBIG_LED_ON; + + mode_val = led_dat->mode_val[mode]; + if (mode_val == NETXBIG_LED_INVALID_MODE) { + ret = -EINVAL; + goto exit_unlock; + } + + gpio_ext_set_value(led_dat->gpio_ext, led_dat->mode_addr, mode_val); + led_dat->mode = mode; + led_dat->sata = enable; + + ret = count; + +exit_unlock: + spin_unlock_irq(&led_dat->lock); + + return ret; +} + +static ssize_t netxbig_led_sata_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct netxbig_led_data *led_dat = + container_of(led_cdev, struct netxbig_led_data, cdev); + + return sprintf(buf, "%d\n", led_dat->sata); +} + +static DEVICE_ATTR(sata, 0644, netxbig_led_sata_show, netxbig_led_sata_store); + +static struct attribute *netxbig_led_attrs[] = { + &dev_attr_sata.attr, + NULL +}; +ATTRIBUTE_GROUPS(netxbig_led); + +static int create_netxbig_led(struct platform_device *pdev, + struct netxbig_led_platform_data *pdata, + struct netxbig_led_data *led_dat, + const struct netxbig_led *template) +{ + spin_lock_init(&led_dat->lock); + led_dat->gpio_ext = pdata->gpio_ext; + led_dat->cdev.name = template->name; + led_dat->cdev.default_trigger = template->default_trigger; + led_dat->cdev.blink_set = netxbig_led_blink_set; + led_dat->cdev.brightness_set = netxbig_led_set; + /* + * Because the GPIO extension bus don't allow to read registers + * value, there is no way to probe the LED initial state. + * So, the initial sysfs LED value for the "brightness" and "sata" + * attributes are inconsistent. + * + * Note that the initial LED state can't be reconfigured. + * The reason is that the LED behaviour must stay uniform during + * the whole boot process (bootloader+linux). + */ + led_dat->sata = 0; + led_dat->cdev.brightness = LED_OFF; + led_dat->cdev.max_brightness = template->bright_max; + led_dat->cdev.flags |= LED_CORE_SUSPENDRESUME; + led_dat->mode_addr = template->mode_addr; + led_dat->mode_val = template->mode_val; + led_dat->bright_addr = template->bright_addr; + led_dat->timer = pdata->timer; + led_dat->num_timer = pdata->num_timer; + /* + * If available, expose the SATA activity blink capability through + * a "sata" sysfs attribute. + */ + if (led_dat->mode_val[NETXBIG_LED_SATA] != NETXBIG_LED_INVALID_MODE) + led_dat->cdev.groups = netxbig_led_groups; + + return devm_led_classdev_register(&pdev->dev, &led_dat->cdev); +} + +/** + * netxbig_gpio_ext_remove() - Clean up GPIO extension data + * @data: managed resource data to clean up + * + * Since we pick GPIO descriptors from another device than the device our + * driver is probing to, we need to register a specific callback to free + * these up using managed resources. + */ +static void netxbig_gpio_ext_remove(void *data) +{ + struct netxbig_gpio_ext *gpio_ext = data; + int i; + + for (i = 0; i < gpio_ext->num_addr; i++) + gpiod_put(gpio_ext->addr[i]); + for (i = 0; i < gpio_ext->num_data; i++) + gpiod_put(gpio_ext->data[i]); + gpiod_put(gpio_ext->enable); +} + +/** + * netxbig_gpio_ext_get() - Obtain GPIO extension device data + * @dev: main LED device + * @gpio_ext_dev: the GPIO extension device + * @gpio_ext: the data structure holding the GPIO extension data + * + * This function walks the subdevice that only contain GPIO line + * handles in the device tree and obtains the GPIO descriptors from that + * device. + */ +static int netxbig_gpio_ext_get(struct device *dev, + struct device *gpio_ext_dev, + struct netxbig_gpio_ext *gpio_ext) +{ + struct gpio_desc **addr, **data; + int num_addr, num_data; + struct gpio_desc *gpiod; + int ret; + int i; + + ret = gpiod_count(gpio_ext_dev, "addr"); + if (ret < 0) { + dev_err(dev, + "Failed to count GPIOs in DT property addr-gpios\n"); + return ret; + } + num_addr = ret; + addr = devm_kcalloc(dev, num_addr, sizeof(*addr), GFP_KERNEL); + if (!addr) + return -ENOMEM; + + /* + * We cannot use devm_ managed resources with these GPIO descriptors + * since they are associated with the "GPIO extension device" which + * does not probe any driver. The device tree parser will however + * populate a platform device for it so we can anyway obtain the + * GPIO descriptors from the device. + */ + for (i = 0; i < num_addr; i++) { + gpiod = gpiod_get_index(gpio_ext_dev, "addr", i, + GPIOD_OUT_LOW); + if (IS_ERR(gpiod)) + return PTR_ERR(gpiod); + gpiod_set_consumer_name(gpiod, "GPIO extension addr"); + addr[i] = gpiod; + } + gpio_ext->addr = addr; + gpio_ext->num_addr = num_addr; + + ret = gpiod_count(gpio_ext_dev, "data"); + if (ret < 0) { + dev_err(dev, + "Failed to count GPIOs in DT property data-gpios\n"); + return ret; + } + num_data = ret; + data = devm_kcalloc(dev, num_data, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + for (i = 0; i < num_data; i++) { + gpiod = gpiod_get_index(gpio_ext_dev, "data", i, + GPIOD_OUT_LOW); + if (IS_ERR(gpiod)) + return PTR_ERR(gpiod); + gpiod_set_consumer_name(gpiod, "GPIO extension data"); + data[i] = gpiod; + } + gpio_ext->data = data; + gpio_ext->num_data = num_data; + + gpiod = gpiod_get(gpio_ext_dev, "enable", GPIOD_OUT_LOW); + if (IS_ERR(gpiod)) { + dev_err(dev, + "Failed to get GPIO from DT property enable-gpio\n"); + return PTR_ERR(gpiod); + } + gpiod_set_consumer_name(gpiod, "GPIO extension enable"); + gpio_ext->enable = gpiod; + + return devm_add_action_or_reset(dev, netxbig_gpio_ext_remove, gpio_ext); +} + +static int netxbig_leds_get_of_pdata(struct device *dev, + struct netxbig_led_platform_data *pdata) +{ + struct device_node *np = dev_of_node(dev); + struct device_node *gpio_ext_np; + struct platform_device *gpio_ext_pdev; + struct device *gpio_ext_dev; + struct device_node *child; + struct netxbig_gpio_ext *gpio_ext; + struct netxbig_led_timer *timers; + struct netxbig_led *leds, *led; + int num_timers; + int num_leds = 0; + int ret; + int i; + + /* GPIO extension */ + gpio_ext_np = of_parse_phandle(np, "gpio-ext", 0); + if (!gpio_ext_np) { + dev_err(dev, "Failed to get DT handle gpio-ext\n"); + return -EINVAL; + } + gpio_ext_pdev = of_find_device_by_node(gpio_ext_np); + if (!gpio_ext_pdev) { + dev_err(dev, "Failed to find platform device for gpio-ext\n"); + return -ENODEV; + } + gpio_ext_dev = &gpio_ext_pdev->dev; + + gpio_ext = devm_kzalloc(dev, sizeof(*gpio_ext), GFP_KERNEL); + if (!gpio_ext) { + of_node_put(gpio_ext_np); + ret = -ENOMEM; + goto put_device; + } + ret = netxbig_gpio_ext_get(dev, gpio_ext_dev, gpio_ext); + of_node_put(gpio_ext_np); + if (ret) + goto put_device; + pdata->gpio_ext = gpio_ext; + + /* Timers (optional) */ + ret = of_property_count_u32_elems(np, "timers"); + if (ret > 0) { + if (ret % 3) { + ret = -EINVAL; + goto put_device; + } + + num_timers = ret / 3; + timers = devm_kcalloc(dev, num_timers, sizeof(*timers), + GFP_KERNEL); + if (!timers) { + ret = -ENOMEM; + goto put_device; + } + for (i = 0; i < num_timers; i++) { + u32 tmp; + + of_property_read_u32_index(np, "timers", 3 * i, + &timers[i].mode); + if (timers[i].mode >= NETXBIG_LED_MODE_NUM) { + ret = -EINVAL; + goto put_device; + } + of_property_read_u32_index(np, "timers", + 3 * i + 1, &tmp); + timers[i].delay_on = tmp; + of_property_read_u32_index(np, "timers", + 3 * i + 2, &tmp); + timers[i].delay_off = tmp; + } + pdata->timer = timers; + pdata->num_timer = num_timers; + } + + /* LEDs */ + num_leds = of_get_available_child_count(np); + if (!num_leds) { + dev_err(dev, "No LED subnodes found in DT\n"); + ret = -ENODEV; + goto put_device; + } + + leds = devm_kcalloc(dev, num_leds, sizeof(*leds), GFP_KERNEL); + if (!leds) { + ret = -ENOMEM; + goto put_device; + } + + led = leds; + for_each_available_child_of_node(np, child) { + const char *string; + int *mode_val; + int num_modes; + + ret = of_property_read_u32(child, "mode-addr", + &led->mode_addr); + if (ret) + goto err_node_put; + + ret = of_property_read_u32(child, "bright-addr", + &led->bright_addr); + if (ret) + goto err_node_put; + + ret = of_property_read_u32(child, "max-brightness", + &led->bright_max); + if (ret) + goto err_node_put; + + mode_val = + devm_kcalloc(dev, + NETXBIG_LED_MODE_NUM, sizeof(*mode_val), + GFP_KERNEL); + if (!mode_val) { + ret = -ENOMEM; + goto err_node_put; + } + + for (i = 0; i < NETXBIG_LED_MODE_NUM; i++) + mode_val[i] = NETXBIG_LED_INVALID_MODE; + + ret = of_property_count_u32_elems(child, "mode-val"); + if (ret < 0 || ret % 2) { + ret = -EINVAL; + goto err_node_put; + } + num_modes = ret / 2; + if (num_modes > NETXBIG_LED_MODE_NUM) { + ret = -EINVAL; + goto err_node_put; + } + + for (i = 0; i < num_modes; i++) { + int mode; + int val; + + of_property_read_u32_index(child, + "mode-val", 2 * i, &mode); + of_property_read_u32_index(child, + "mode-val", 2 * i + 1, &val); + if (mode >= NETXBIG_LED_MODE_NUM) { + ret = -EINVAL; + goto err_node_put; + } + mode_val[mode] = val; + } + led->mode_val = mode_val; + + if (!of_property_read_string(child, "label", &string)) + led->name = string; + else + led->name = child->name; + + if (!of_property_read_string(child, + "linux,default-trigger", &string)) + led->default_trigger = string; + + led++; + } + + pdata->leds = leds; + pdata->num_leds = num_leds; + + return 0; + +err_node_put: + of_node_put(child); +put_device: + put_device(gpio_ext_dev); + return ret; +} + +static const struct of_device_id of_netxbig_leds_match[] = { + { .compatible = "lacie,netxbig-leds", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_netxbig_leds_match); + +static int netxbig_led_probe(struct platform_device *pdev) +{ + struct netxbig_led_platform_data *pdata; + struct netxbig_led_data *leds_data; + int i; + int ret; + + pdata = devm_kzalloc(&pdev->dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return -ENOMEM; + ret = netxbig_leds_get_of_pdata(&pdev->dev, pdata); + if (ret) + return ret; + + leds_data = devm_kcalloc(&pdev->dev, + pdata->num_leds, sizeof(*leds_data), + GFP_KERNEL); + if (!leds_data) + return -ENOMEM; + + for (i = 0; i < pdata->num_leds; i++) { + ret = create_netxbig_led(pdev, pdata, + &leds_data[i], &pdata->leds[i]); + if (ret < 0) + return ret; + } + + return 0; +} + +static struct platform_driver netxbig_led_driver = { + .probe = netxbig_led_probe, + .driver = { + .name = "leds-netxbig", + .of_match_table = of_netxbig_leds_match, + }, +}; + +module_platform_driver(netxbig_led_driver); + +MODULE_AUTHOR("Simon Guinot <sguinot@lacie.com>"); +MODULE_DESCRIPTION("LED driver for LaCie xBig Network boards"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:leds-netxbig"); diff --git a/drivers/leds/leds-nic78bx.c b/drivers/leds/leds-nic78bx.c new file mode 100644 index 000000000..f196f52ee --- /dev/null +++ b/drivers/leds/leds-nic78bx.c @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2016 National Instruments Corp. + */ + +#include <linux/acpi.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/spinlock.h> + +#define NIC78BX_USER1_LED_MASK 0x3 +#define NIC78BX_USER1_GREEN_LED BIT(0) +#define NIC78BX_USER1_YELLOW_LED BIT(1) + +#define NIC78BX_USER2_LED_MASK 0xC +#define NIC78BX_USER2_GREEN_LED BIT(2) +#define NIC78BX_USER2_YELLOW_LED BIT(3) + +#define NIC78BX_LOCK_REG_OFFSET 1 +#define NIC78BX_LOCK_VALUE 0xA5 +#define NIC78BX_UNLOCK_VALUE 0x5A + +#define NIC78BX_USER_LED_IO_SIZE 2 + +struct nic78bx_led_data { + u16 io_base; + spinlock_t lock; + struct platform_device *pdev; +}; + +struct nic78bx_led { + u8 bit; + u8 mask; + struct nic78bx_led_data *data; + struct led_classdev cdev; +}; + +static inline struct nic78bx_led *to_nic78bx_led(struct led_classdev *cdev) +{ + return container_of(cdev, struct nic78bx_led, cdev); +} + +static void nic78bx_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct nic78bx_led *nled = to_nic78bx_led(cdev); + unsigned long flags; + u8 value; + + spin_lock_irqsave(&nled->data->lock, flags); + value = inb(nled->data->io_base); + + if (brightness) { + value &= ~nled->mask; + value |= nled->bit; + } else { + value &= ~nled->bit; + } + + outb(value, nled->data->io_base); + spin_unlock_irqrestore(&nled->data->lock, flags); +} + +static enum led_brightness nic78bx_brightness_get(struct led_classdev *cdev) +{ + struct nic78bx_led *nled = to_nic78bx_led(cdev); + unsigned long flags; + u8 value; + + spin_lock_irqsave(&nled->data->lock, flags); + value = inb(nled->data->io_base); + spin_unlock_irqrestore(&nled->data->lock, flags); + + return (value & nled->bit) ? 1 : LED_OFF; +} + +static struct nic78bx_led nic78bx_leds[] = { + { + .bit = NIC78BX_USER1_GREEN_LED, + .mask = NIC78BX_USER1_LED_MASK, + .cdev = { + .name = "nilrt:green:user1", + .max_brightness = 1, + .brightness_set = nic78bx_brightness_set, + .brightness_get = nic78bx_brightness_get, + } + }, + { + .bit = NIC78BX_USER1_YELLOW_LED, + .mask = NIC78BX_USER1_LED_MASK, + .cdev = { + .name = "nilrt:yellow:user1", + .max_brightness = 1, + .brightness_set = nic78bx_brightness_set, + .brightness_get = nic78bx_brightness_get, + } + }, + { + .bit = NIC78BX_USER2_GREEN_LED, + .mask = NIC78BX_USER2_LED_MASK, + .cdev = { + .name = "nilrt:green:user2", + .max_brightness = 1, + .brightness_set = nic78bx_brightness_set, + .brightness_get = nic78bx_brightness_get, + } + }, + { + .bit = NIC78BX_USER2_YELLOW_LED, + .mask = NIC78BX_USER2_LED_MASK, + .cdev = { + .name = "nilrt:yellow:user2", + .max_brightness = 1, + .brightness_set = nic78bx_brightness_set, + .brightness_get = nic78bx_brightness_get, + } + } +}; + +static int nic78bx_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct nic78bx_led_data *led_data; + struct resource *io_rc; + int ret, i; + + led_data = devm_kzalloc(dev, sizeof(*led_data), GFP_KERNEL); + if (!led_data) + return -ENOMEM; + + led_data->pdev = pdev; + platform_set_drvdata(pdev, led_data); + + io_rc = platform_get_resource(pdev, IORESOURCE_IO, 0); + if (!io_rc) { + dev_err(dev, "missing IO resources\n"); + return -EINVAL; + } + + if (resource_size(io_rc) < NIC78BX_USER_LED_IO_SIZE) { + dev_err(dev, "IO region too small\n"); + return -EINVAL; + } + + if (!devm_request_region(dev, io_rc->start, resource_size(io_rc), + KBUILD_MODNAME)) { + dev_err(dev, "failed to get IO region\n"); + return -EBUSY; + } + + led_data->io_base = io_rc->start; + spin_lock_init(&led_data->lock); + + for (i = 0; i < ARRAY_SIZE(nic78bx_leds); i++) { + nic78bx_leds[i].data = led_data; + + ret = devm_led_classdev_register(dev, &nic78bx_leds[i].cdev); + if (ret) + return ret; + } + + /* Unlock LED register */ + outb(NIC78BX_UNLOCK_VALUE, + led_data->io_base + NIC78BX_LOCK_REG_OFFSET); + + return ret; +} + +static int nic78bx_remove(struct platform_device *pdev) +{ + struct nic78bx_led_data *led_data = platform_get_drvdata(pdev); + + /* Lock LED register */ + outb(NIC78BX_LOCK_VALUE, + led_data->io_base + NIC78BX_LOCK_REG_OFFSET); + + return 0; +} + +static const struct acpi_device_id led_device_ids[] = { + {"NIC78B3", 0}, + {"", 0}, +}; +MODULE_DEVICE_TABLE(acpi, led_device_ids); + +static struct platform_driver led_driver = { + .probe = nic78bx_probe, + .remove = nic78bx_remove, + .driver = { + .name = KBUILD_MODNAME, + .acpi_match_table = ACPI_PTR(led_device_ids), + }, +}; + +module_platform_driver(led_driver); + +MODULE_DESCRIPTION("National Instruments PXI User LEDs driver"); +MODULE_AUTHOR("Hui Chun Ong <hui.chun.ong@ni.com>"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/leds-ns2.c b/drivers/leds/leds-ns2.c new file mode 100644 index 000000000..1677d66d8 --- /dev/null +++ b/drivers/leds/leds-ns2.c @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * leds-ns2.c - Driver for the Network Space v2 (and parents) dual-GPIO LED + * + * Copyright (C) 2010 LaCie + * + * Author: Simon Guinot <sguinot@lacie.com> + * + * Based on leds-gpio.c by Raphael Assenat <raph@8d.com> + */ + +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/gpio/consumer.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of.h> +#include "leds.h" + +enum ns2_led_modes { + NS_V2_LED_OFF, + NS_V2_LED_ON, + NS_V2_LED_SATA, +}; + +/* + * If the size of this structure or types of its members is changed, + * the filling of array modval in function ns2_led_register must be changed + * accordingly. + */ +struct ns2_led_modval { + u32 mode; + u32 cmd_level; + u32 slow_level; +} __packed; + +/* + * The Network Space v2 dual-GPIO LED is wired to a CPLD. Three different LED + * modes are available: off, on and SATA activity blinking. The LED modes are + * controlled through two GPIOs (command and slow): each combination of values + * for the command/slow GPIOs corresponds to a LED mode. + */ + +struct ns2_led { + struct led_classdev cdev; + struct gpio_desc *cmd; + struct gpio_desc *slow; + bool can_sleep; + unsigned char sata; /* True when SATA mode active. */ + rwlock_t rw_lock; /* Lock GPIOs. */ + int num_modes; + struct ns2_led_modval *modval; +}; + +static int ns2_led_get_mode(struct ns2_led *led, enum ns2_led_modes *mode) +{ + int i; + int cmd_level; + int slow_level; + + cmd_level = gpiod_get_value_cansleep(led->cmd); + slow_level = gpiod_get_value_cansleep(led->slow); + + for (i = 0; i < led->num_modes; i++) { + if (cmd_level == led->modval[i].cmd_level && + slow_level == led->modval[i].slow_level) { + *mode = led->modval[i].mode; + return 0; + } + } + + return -EINVAL; +} + +static void ns2_led_set_mode(struct ns2_led *led, enum ns2_led_modes mode) +{ + int i; + unsigned long flags; + + for (i = 0; i < led->num_modes; i++) + if (mode == led->modval[i].mode) + break; + + if (i == led->num_modes) + return; + + write_lock_irqsave(&led->rw_lock, flags); + + if (!led->can_sleep) { + gpiod_set_value(led->cmd, led->modval[i].cmd_level); + gpiod_set_value(led->slow, led->modval[i].slow_level); + goto exit_unlock; + } + + gpiod_set_value_cansleep(led->cmd, led->modval[i].cmd_level); + gpiod_set_value_cansleep(led->slow, led->modval[i].slow_level); + +exit_unlock: + write_unlock_irqrestore(&led->rw_lock, flags); +} + +static void ns2_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct ns2_led *led = container_of(led_cdev, struct ns2_led, cdev); + enum ns2_led_modes mode; + + if (value == LED_OFF) + mode = NS_V2_LED_OFF; + else if (led->sata) + mode = NS_V2_LED_SATA; + else + mode = NS_V2_LED_ON; + + ns2_led_set_mode(led, mode); +} + +static int ns2_led_set_blocking(struct led_classdev *led_cdev, + enum led_brightness value) +{ + ns2_led_set(led_cdev, value); + return 0; +} + +static ssize_t ns2_led_sata_store(struct device *dev, + struct device_attribute *attr, + const char *buff, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct ns2_led *led = container_of(led_cdev, struct ns2_led, cdev); + int ret; + unsigned long enable; + + ret = kstrtoul(buff, 10, &enable); + if (ret < 0) + return ret; + + enable = !!enable; + + if (led->sata == enable) + goto exit; + + led->sata = enable; + + if (!led_get_brightness(led_cdev)) + goto exit; + + if (enable) + ns2_led_set_mode(led, NS_V2_LED_SATA); + else + ns2_led_set_mode(led, NS_V2_LED_ON); + +exit: + return count; +} + +static ssize_t ns2_led_sata_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct ns2_led *led = container_of(led_cdev, struct ns2_led, cdev); + + return sprintf(buf, "%d\n", led->sata); +} + +static DEVICE_ATTR(sata, 0644, ns2_led_sata_show, ns2_led_sata_store); + +static struct attribute *ns2_led_attrs[] = { + &dev_attr_sata.attr, + NULL +}; +ATTRIBUTE_GROUPS(ns2_led); + +static int ns2_led_register(struct device *dev, struct fwnode_handle *node, + struct ns2_led *led) +{ + struct led_init_data init_data = {}; + struct ns2_led_modval *modval; + enum ns2_led_modes mode; + int nmodes, ret; + + led->cmd = devm_fwnode_gpiod_get_index(dev, node, "cmd", 0, GPIOD_ASIS, + fwnode_get_name(node)); + if (IS_ERR(led->cmd)) + return PTR_ERR(led->cmd); + + led->slow = devm_fwnode_gpiod_get_index(dev, node, "slow", 0, + GPIOD_ASIS, + fwnode_get_name(node)); + if (IS_ERR(led->slow)) + return PTR_ERR(led->slow); + + ret = fwnode_property_count_u32(node, "modes-map"); + if (ret < 0 || ret % 3) { + dev_err(dev, "Missing or malformed modes-map for %pfw\n", node); + return -EINVAL; + } + + nmodes = ret / 3; + modval = devm_kcalloc(dev, nmodes, sizeof(*modval), GFP_KERNEL); + if (!modval) + return -ENOMEM; + + fwnode_property_read_u32_array(node, "modes-map", (void *)modval, + nmodes * 3); + + rwlock_init(&led->rw_lock); + + led->cdev.blink_set = NULL; + led->cdev.flags |= LED_CORE_SUSPENDRESUME; + led->cdev.groups = ns2_led_groups; + led->can_sleep = gpiod_cansleep(led->cmd) || gpiod_cansleep(led->slow); + if (led->can_sleep) + led->cdev.brightness_set_blocking = ns2_led_set_blocking; + else + led->cdev.brightness_set = ns2_led_set; + led->num_modes = nmodes; + led->modval = modval; + + ret = ns2_led_get_mode(led, &mode); + if (ret < 0) + return ret; + + /* Set LED initial state. */ + led->sata = (mode == NS_V2_LED_SATA) ? 1 : 0; + led->cdev.brightness = (mode == NS_V2_LED_OFF) ? LED_OFF : LED_FULL; + + init_data.fwnode = node; + + ret = devm_led_classdev_register_ext(dev, &led->cdev, &init_data); + if (ret) + dev_err(dev, "Failed to register LED for node %pfw\n", node); + + return ret; +} + +static int ns2_led_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct fwnode_handle *child; + struct ns2_led *leds; + int count; + int ret; + + count = device_get_child_node_count(dev); + if (!count) + return -ENODEV; + + leds = devm_kzalloc(dev, array_size(sizeof(*leds), count), GFP_KERNEL); + if (!leds) + return -ENOMEM; + + device_for_each_child_node(dev, child) { + ret = ns2_led_register(dev, child, leds++); + if (ret) { + fwnode_handle_put(child); + return ret; + } + } + + return 0; +} + +static const struct of_device_id of_ns2_leds_match[] = { + { .compatible = "lacie,ns2-leds", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_ns2_leds_match); + +static struct platform_driver ns2_led_driver = { + .probe = ns2_led_probe, + .driver = { + .name = "leds-ns2", + .of_match_table = of_ns2_leds_match, + }, +}; + +module_platform_driver(ns2_led_driver); + +MODULE_AUTHOR("Simon Guinot <sguinot@lacie.com>"); +MODULE_DESCRIPTION("Network Space v2 LED driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:leds-ns2"); diff --git a/drivers/leds/leds-ot200.c b/drivers/leds/leds-ot200.c new file mode 100644 index 000000000..12af1127d --- /dev/null +++ b/drivers/leds/leds-ot200.c @@ -0,0 +1,152 @@ +/* + * Bachmann ot200 leds driver. + * + * Author: Sebastian Andrzej Siewior <bigeasy@linutronix.de> + * Christian Gmeiner <christian.gmeiner@gmail.com> + * + * License: GPL as published by the FSF. + */ + +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/leds.h> +#include <linux/io.h> +#include <linux/module.h> + + +struct ot200_led { + struct led_classdev cdev; + const char *name; + unsigned long port; + u8 mask; +}; + +/* + * The device has three leds on the back panel (led_err, led_init and led_run) + * and can handle up to seven leds on the front panel. + */ + +static struct ot200_led leds[] = { + { + .name = "led_run", + .port = 0x5a, + .mask = BIT(0), + }, + { + .name = "led_init", + .port = 0x5a, + .mask = BIT(1), + }, + { + .name = "led_err", + .port = 0x5a, + .mask = BIT(2), + }, + { + .name = "led_1", + .port = 0x49, + .mask = BIT(6), + }, + { + .name = "led_2", + .port = 0x49, + .mask = BIT(5), + }, + { + .name = "led_3", + .port = 0x49, + .mask = BIT(4), + }, + { + .name = "led_4", + .port = 0x49, + .mask = BIT(3), + }, + { + .name = "led_5", + .port = 0x49, + .mask = BIT(2), + }, + { + .name = "led_6", + .port = 0x49, + .mask = BIT(1), + }, + { + .name = "led_7", + .port = 0x49, + .mask = BIT(0), + } +}; + +static DEFINE_SPINLOCK(value_lock); + +/* + * we need to store the current led states, as it is not + * possible to read the current led state via inb(). + */ +static u8 leds_back; +static u8 leds_front; + +static void ot200_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct ot200_led *led = container_of(led_cdev, struct ot200_led, cdev); + u8 *val; + unsigned long flags; + + spin_lock_irqsave(&value_lock, flags); + + if (led->port == 0x49) + val = &leds_front; + else if (led->port == 0x5a) + val = &leds_back; + else + BUG(); + + if (value == LED_OFF) + *val &= ~led->mask; + else + *val |= led->mask; + + outb(*val, led->port); + spin_unlock_irqrestore(&value_lock, flags); +} + +static int ot200_led_probe(struct platform_device *pdev) +{ + int i; + int ret; + + for (i = 0; i < ARRAY_SIZE(leds); i++) { + + leds[i].cdev.name = leds[i].name; + leds[i].cdev.brightness_set = ot200_led_brightness_set; + + ret = devm_led_classdev_register(&pdev->dev, &leds[i].cdev); + if (ret < 0) + return ret; + } + + leds_front = 0; /* turn off all front leds */ + leds_back = BIT(1); /* turn on init led */ + outb(leds_front, 0x49); + outb(leds_back, 0x5a); + + return 0; +} + +static struct platform_driver ot200_led_driver = { + .probe = ot200_led_probe, + .driver = { + .name = "leds-ot200", + }, +}; + +module_platform_driver(ot200_led_driver); + +MODULE_AUTHOR("Sebastian A. Siewior <bigeasy@linutronix.de>"); +MODULE_DESCRIPTION("ot200 LED driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:leds-ot200"); diff --git a/drivers/leds/leds-pca9532.c b/drivers/leds/leds-pca9532.c new file mode 100644 index 000000000..27d027165 --- /dev/null +++ b/drivers/leds/leds-pca9532.c @@ -0,0 +1,563 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * pca9532.c - 16-bit Led dimmer + * + * Copyright (C) 2011 Jan Weitzel + * Copyright (C) 2008 Riku Voipio + * + * Datasheet: http://www.nxp.com/documents/data_sheet/PCA9532.pdf + */ + +#include <linux/module.h> +#include <linux/i2c.h> +#include <linux/slab.h> +#include <linux/leds.h> +#include <linux/input.h> +#include <linux/mutex.h> +#include <linux/workqueue.h> +#include <linux/leds-pca9532.h> +#include <linux/gpio/driver.h> +#include <linux/of.h> +#include <linux/of_device.h> + +/* m = num_leds*/ +#define PCA9532_REG_INPUT(i) ((i) >> 3) +#define PCA9532_REG_OFFSET(m) ((m) >> 4) +#define PCA9532_REG_PSC(m, i) (PCA9532_REG_OFFSET(m) + 0x1 + (i) * 2) +#define PCA9532_REG_PWM(m, i) (PCA9532_REG_OFFSET(m) + 0x2 + (i) * 2) +#define LED_REG(m, led) (PCA9532_REG_OFFSET(m) + 0x5 + (led >> 2)) +#define LED_NUM(led) (led & 0x3) +#define LED_SHIFT(led) (LED_NUM(led) * 2) +#define LED_MASK(led) (0x3 << LED_SHIFT(led)) + +#define ldev_to_led(c) container_of(c, struct pca9532_led, ldev) + +struct pca9532_chip_info { + u8 num_leds; +}; + +struct pca9532_data { + struct i2c_client *client; + struct pca9532_led leds[16]; + struct mutex update_lock; + struct input_dev *idev; + struct work_struct work; +#ifdef CONFIG_LEDS_PCA9532_GPIO + struct gpio_chip gpio; +#endif + const struct pca9532_chip_info *chip_info; + u8 pwm[2]; + u8 psc[2]; +}; + +static int pca9532_probe(struct i2c_client *client, + const struct i2c_device_id *id); +static int pca9532_remove(struct i2c_client *client); + +enum { + pca9530, + pca9531, + pca9532, + pca9533, +}; + +static const struct i2c_device_id pca9532_id[] = { + { "pca9530", pca9530 }, + { "pca9531", pca9531 }, + { "pca9532", pca9532 }, + { "pca9533", pca9533 }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, pca9532_id); + +static const struct pca9532_chip_info pca9532_chip_info_tbl[] = { + [pca9530] = { + .num_leds = 2, + }, + [pca9531] = { + .num_leds = 8, + }, + [pca9532] = { + .num_leds = 16, + }, + [pca9533] = { + .num_leds = 4, + }, +}; + +#ifdef CONFIG_OF +static const struct of_device_id of_pca9532_leds_match[] = { + { .compatible = "nxp,pca9530", .data = (void *)pca9530 }, + { .compatible = "nxp,pca9531", .data = (void *)pca9531 }, + { .compatible = "nxp,pca9532", .data = (void *)pca9532 }, + { .compatible = "nxp,pca9533", .data = (void *)pca9533 }, + {}, +}; + +MODULE_DEVICE_TABLE(of, of_pca9532_leds_match); +#endif + +static struct i2c_driver pca9532_driver = { + .driver = { + .name = "leds-pca953x", + .of_match_table = of_match_ptr(of_pca9532_leds_match), + }, + .probe = pca9532_probe, + .remove = pca9532_remove, + .id_table = pca9532_id, +}; + +/* We have two pwm/blinkers, but 16 possible leds to drive. Additionally, + * the clever Thecus people are using one pwm to drive the beeper. So, + * as a compromise we average one pwm to the values requested by all + * leds that are not ON/OFF. + * */ +static int pca9532_calcpwm(struct i2c_client *client, int pwm, int blink, + enum led_brightness value) +{ + int a = 0, b = 0, i = 0; + struct pca9532_data *data = i2c_get_clientdata(client); + for (i = 0; i < data->chip_info->num_leds; i++) { + if (data->leds[i].type == PCA9532_TYPE_LED && + data->leds[i].state == PCA9532_PWM0+pwm) { + a++; + b += data->leds[i].ldev.brightness; + } + } + if (a == 0) { + dev_err(&client->dev, + "fear of division by zero %d/%d, wanted %d\n", + b, a, value); + return -EINVAL; + } + b = b/a; + if (b > 0xFF) + return -EINVAL; + data->pwm[pwm] = b; + data->psc[pwm] = blink; + return 0; +} + +static int pca9532_setpwm(struct i2c_client *client, int pwm) +{ + struct pca9532_data *data = i2c_get_clientdata(client); + u8 maxleds = data->chip_info->num_leds; + + mutex_lock(&data->update_lock); + i2c_smbus_write_byte_data(client, PCA9532_REG_PWM(maxleds, pwm), + data->pwm[pwm]); + i2c_smbus_write_byte_data(client, PCA9532_REG_PSC(maxleds, pwm), + data->psc[pwm]); + mutex_unlock(&data->update_lock); + return 0; +} + +/* Set LED routing */ +static void pca9532_setled(struct pca9532_led *led) +{ + struct i2c_client *client = led->client; + struct pca9532_data *data = i2c_get_clientdata(client); + u8 maxleds = data->chip_info->num_leds; + char reg; + + mutex_lock(&data->update_lock); + reg = i2c_smbus_read_byte_data(client, LED_REG(maxleds, led->id)); + /* zero led bits */ + reg = reg & ~LED_MASK(led->id); + /* set the new value */ + reg = reg | (led->state << LED_SHIFT(led->id)); + i2c_smbus_write_byte_data(client, LED_REG(maxleds, led->id), reg); + mutex_unlock(&data->update_lock); +} + +static int pca9532_set_brightness(struct led_classdev *led_cdev, + enum led_brightness value) +{ + int err = 0; + struct pca9532_led *led = ldev_to_led(led_cdev); + + if (value == LED_OFF) + led->state = PCA9532_OFF; + else if (value == LED_FULL) + led->state = PCA9532_ON; + else { + led->state = PCA9532_PWM0; /* Thecus: hardcode one pwm */ + err = pca9532_calcpwm(led->client, 0, 0, value); + if (err) + return err; + } + if (led->state == PCA9532_PWM0) + pca9532_setpwm(led->client, 0); + pca9532_setled(led); + return err; +} + +static int pca9532_set_blink(struct led_classdev *led_cdev, + unsigned long *delay_on, unsigned long *delay_off) +{ + struct pca9532_led *led = ldev_to_led(led_cdev); + struct i2c_client *client = led->client; + int psc; + int err = 0; + + if (*delay_on == 0 && *delay_off == 0) { + /* led subsystem ask us for a blink rate */ + *delay_on = 1000; + *delay_off = 1000; + } + if (*delay_on != *delay_off || *delay_on > 1690 || *delay_on < 6) + return -EINVAL; + + /* Thecus specific: only use PSC/PWM 0 */ + psc = (*delay_on * 152-1)/1000; + err = pca9532_calcpwm(client, 0, psc, led_cdev->brightness); + if (err) + return err; + if (led->state == PCA9532_PWM0) + pca9532_setpwm(led->client, 0); + pca9532_setled(led); + + return 0; +} + +static int pca9532_event(struct input_dev *dev, unsigned int type, + unsigned int code, int value) +{ + struct pca9532_data *data = input_get_drvdata(dev); + + if (!(type == EV_SND && (code == SND_BELL || code == SND_TONE))) + return -1; + + /* XXX: allow different kind of beeps with psc/pwm modifications */ + if (value > 1 && value < 32767) + data->pwm[1] = 127; + else + data->pwm[1] = 0; + + schedule_work(&data->work); + + return 0; +} + +static void pca9532_input_work(struct work_struct *work) +{ + struct pca9532_data *data = + container_of(work, struct pca9532_data, work); + u8 maxleds = data->chip_info->num_leds; + + mutex_lock(&data->update_lock); + i2c_smbus_write_byte_data(data->client, PCA9532_REG_PWM(maxleds, 1), + data->pwm[1]); + mutex_unlock(&data->update_lock); +} + +static enum pca9532_state pca9532_getled(struct pca9532_led *led) +{ + struct i2c_client *client = led->client; + struct pca9532_data *data = i2c_get_clientdata(client); + u8 maxleds = data->chip_info->num_leds; + char reg; + enum pca9532_state ret; + + mutex_lock(&data->update_lock); + reg = i2c_smbus_read_byte_data(client, LED_REG(maxleds, led->id)); + ret = (reg & LED_MASK(led->id)) >> LED_SHIFT(led->id); + mutex_unlock(&data->update_lock); + return ret; +} + +#ifdef CONFIG_LEDS_PCA9532_GPIO +static int pca9532_gpio_request_pin(struct gpio_chip *gc, unsigned offset) +{ + struct pca9532_data *data = gpiochip_get_data(gc); + struct pca9532_led *led = &data->leds[offset]; + + if (led->type == PCA9532_TYPE_GPIO) + return 0; + + return -EBUSY; +} + +static void pca9532_gpio_set_value(struct gpio_chip *gc, unsigned offset, int val) +{ + struct pca9532_data *data = gpiochip_get_data(gc); + struct pca9532_led *led = &data->leds[offset]; + + if (val) + led->state = PCA9532_ON; + else + led->state = PCA9532_OFF; + + pca9532_setled(led); +} + +static int pca9532_gpio_get_value(struct gpio_chip *gc, unsigned offset) +{ + struct pca9532_data *data = gpiochip_get_data(gc); + unsigned char reg; + + reg = i2c_smbus_read_byte_data(data->client, PCA9532_REG_INPUT(offset)); + + return !!(reg & (1 << (offset % 8))); +} + +static int pca9532_gpio_direction_input(struct gpio_chip *gc, unsigned offset) +{ + /* To use as input ensure pin is not driven */ + pca9532_gpio_set_value(gc, offset, 0); + + return 0; +} + +static int pca9532_gpio_direction_output(struct gpio_chip *gc, unsigned offset, int val) +{ + pca9532_gpio_set_value(gc, offset, val); + + return 0; +} +#endif /* CONFIG_LEDS_PCA9532_GPIO */ + +static int pca9532_destroy_devices(struct pca9532_data *data, int n_devs) +{ + int i = n_devs; + + if (!data) + return -EINVAL; + + while (--i >= 0) { + switch (data->leds[i].type) { + case PCA9532_TYPE_NONE: + case PCA9532_TYPE_GPIO: + break; + case PCA9532_TYPE_LED: + led_classdev_unregister(&data->leds[i].ldev); + break; + case PCA9532_TYPE_N2100_BEEP: + if (data->idev != NULL) { + cancel_work_sync(&data->work); + data->idev = NULL; + } + break; + } + } + +#ifdef CONFIG_LEDS_PCA9532_GPIO + if (data->gpio.parent) + gpiochip_remove(&data->gpio); +#endif + + return 0; +} + +static int pca9532_configure(struct i2c_client *client, + struct pca9532_data *data, struct pca9532_platform_data *pdata) +{ + int i, err = 0; + int gpios = 0; + u8 maxleds = data->chip_info->num_leds; + + for (i = 0; i < 2; i++) { + data->pwm[i] = pdata->pwm[i]; + data->psc[i] = pdata->psc[i]; + i2c_smbus_write_byte_data(client, PCA9532_REG_PWM(maxleds, i), + data->pwm[i]); + i2c_smbus_write_byte_data(client, PCA9532_REG_PSC(maxleds, i), + data->psc[i]); + } + + for (i = 0; i < data->chip_info->num_leds; i++) { + struct pca9532_led *led = &data->leds[i]; + struct pca9532_led *pled = &pdata->leds[i]; + led->client = client; + led->id = i; + led->type = pled->type; + switch (led->type) { + case PCA9532_TYPE_NONE: + break; + case PCA9532_TYPE_GPIO: + gpios++; + break; + case PCA9532_TYPE_LED: + if (pled->state == PCA9532_KEEP) + led->state = pca9532_getled(led); + else + led->state = pled->state; + led->name = pled->name; + led->ldev.name = led->name; + led->ldev.default_trigger = pled->default_trigger; + led->ldev.brightness = LED_OFF; + led->ldev.brightness_set_blocking = + pca9532_set_brightness; + led->ldev.blink_set = pca9532_set_blink; + err = led_classdev_register(&client->dev, &led->ldev); + if (err < 0) { + dev_err(&client->dev, + "couldn't register LED %s\n", + led->name); + goto exit; + } + pca9532_setled(led); + break; + case PCA9532_TYPE_N2100_BEEP: + BUG_ON(data->idev); + led->state = PCA9532_PWM1; + pca9532_setled(led); + data->idev = devm_input_allocate_device(&client->dev); + if (data->idev == NULL) { + err = -ENOMEM; + goto exit; + } + data->idev->name = pled->name; + data->idev->phys = "i2c/pca9532"; + data->idev->id.bustype = BUS_HOST; + data->idev->id.vendor = 0x001f; + data->idev->id.product = 0x0001; + data->idev->id.version = 0x0100; + data->idev->evbit[0] = BIT_MASK(EV_SND); + data->idev->sndbit[0] = BIT_MASK(SND_BELL) | + BIT_MASK(SND_TONE); + data->idev->event = pca9532_event; + input_set_drvdata(data->idev, data); + INIT_WORK(&data->work, pca9532_input_work); + err = input_register_device(data->idev); + if (err) { + cancel_work_sync(&data->work); + data->idev = NULL; + goto exit; + } + break; + } + } + +#ifdef CONFIG_LEDS_PCA9532_GPIO + if (gpios) { + data->gpio.label = "gpio-pca9532"; + data->gpio.direction_input = pca9532_gpio_direction_input; + data->gpio.direction_output = pca9532_gpio_direction_output; + data->gpio.set = pca9532_gpio_set_value; + data->gpio.get = pca9532_gpio_get_value; + data->gpio.request = pca9532_gpio_request_pin; + data->gpio.can_sleep = 1; + data->gpio.base = pdata->gpio_base; + data->gpio.ngpio = data->chip_info->num_leds; + data->gpio.parent = &client->dev; + data->gpio.owner = THIS_MODULE; + + err = gpiochip_add_data(&data->gpio, data); + if (err) { + /* Use data->gpio.dev as a flag for freeing gpiochip */ + data->gpio.parent = NULL; + dev_warn(&client->dev, "could not add gpiochip\n"); + } else { + dev_info(&client->dev, "gpios %i...%i\n", + data->gpio.base, data->gpio.base + + data->gpio.ngpio - 1); + } + } +#endif + + return 0; + +exit: + pca9532_destroy_devices(data, i); + return err; +} + +static struct pca9532_platform_data * +pca9532_of_populate_pdata(struct device *dev, struct device_node *np) +{ + struct pca9532_platform_data *pdata; + struct device_node *child; + int devid, maxleds; + int i = 0; + const char *state; + + devid = (int)(uintptr_t)of_device_get_match_data(dev); + maxleds = pca9532_chip_info_tbl[devid].num_leds; + + pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return ERR_PTR(-ENOMEM); + + of_property_read_u8_array(np, "nxp,pwm", &pdata->pwm[0], + ARRAY_SIZE(pdata->pwm)); + of_property_read_u8_array(np, "nxp,psc", &pdata->psc[0], + ARRAY_SIZE(pdata->psc)); + + for_each_available_child_of_node(np, child) { + if (of_property_read_string(child, "label", + &pdata->leds[i].name)) + pdata->leds[i].name = child->name; + of_property_read_u32(child, "type", &pdata->leds[i].type); + of_property_read_string(child, "linux,default-trigger", + &pdata->leds[i].default_trigger); + if (!of_property_read_string(child, "default-state", &state)) { + if (!strcmp(state, "on")) + pdata->leds[i].state = PCA9532_ON; + else if (!strcmp(state, "keep")) + pdata->leds[i].state = PCA9532_KEEP; + } + if (++i >= maxleds) { + of_node_put(child); + break; + } + } + + return pdata; +} + +static int pca9532_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + int devid; + struct pca9532_data *data = i2c_get_clientdata(client); + struct pca9532_platform_data *pca9532_pdata = + dev_get_platdata(&client->dev); + struct device_node *np = dev_of_node(&client->dev); + + if (!pca9532_pdata) { + if (np) { + pca9532_pdata = + pca9532_of_populate_pdata(&client->dev, np); + if (IS_ERR(pca9532_pdata)) + return PTR_ERR(pca9532_pdata); + } else { + dev_err(&client->dev, "no platform data\n"); + return -EINVAL; + } + devid = (int)(uintptr_t)of_device_get_match_data(&client->dev); + } else { + devid = id->driver_data; + } + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_BYTE_DATA)) + return -EIO; + + data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->chip_info = &pca9532_chip_info_tbl[devid]; + + dev_info(&client->dev, "setting platform data\n"); + i2c_set_clientdata(client, data); + data->client = client; + mutex_init(&data->update_lock); + + return pca9532_configure(client, data, pca9532_pdata); +} + +static int pca9532_remove(struct i2c_client *client) +{ + struct pca9532_data *data = i2c_get_clientdata(client); + + return pca9532_destroy_devices(data, data->chip_info->num_leds); +} + +module_i2c_driver(pca9532_driver); + +MODULE_AUTHOR("Riku Voipio"); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("PCA 9532 LED dimmer"); diff --git a/drivers/leds/leds-pca955x.c b/drivers/leds/leds-pca955x.c new file mode 100644 index 000000000..7087ca459 --- /dev/null +++ b/drivers/leds/leds-pca955x.c @@ -0,0 +1,592 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2007-2008 Extreme Engineering Solutions, Inc. + * + * Author: Nate Case <ncase@xes-inc.com> + * + * LED driver for various PCA955x I2C LED drivers + * + * Supported devices: + * + * Device Description 7-bit slave address + * ------ ----------- ------------------- + * PCA9550 2-bit driver 0x60 .. 0x61 + * PCA9551 8-bit driver 0x60 .. 0x67 + * PCA9552 16-bit driver 0x60 .. 0x67 + * PCA9553/01 4-bit driver 0x62 + * PCA9553/02 4-bit driver 0x63 + * + * Philips PCA955x LED driver chips follow a register map as shown below: + * + * Control Register Description + * ---------------- ----------- + * 0x0 Input register 0 + * .. + * NUM_INPUT_REGS - 1 Last Input register X + * + * NUM_INPUT_REGS Frequency prescaler 0 + * NUM_INPUT_REGS + 1 PWM register 0 + * NUM_INPUT_REGS + 2 Frequency prescaler 1 + * NUM_INPUT_REGS + 3 PWM register 1 + * + * NUM_INPUT_REGS + 4 LED selector 0 + * NUM_INPUT_REGS + 4 + * + NUM_LED_REGS - 1 Last LED selector + * + * where NUM_INPUT_REGS and NUM_LED_REGS vary depending on how many + * bits the chip supports. + */ + +#include <linux/ctype.h> +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/gpio/driver.h> +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/property.h> +#include <linux/slab.h> +#include <linux/string.h> + +#include <dt-bindings/leds/leds-pca955x.h> + +/* LED select registers determine the source that drives LED outputs */ +#define PCA955X_LS_LED_ON 0x0 /* Output LOW */ +#define PCA955X_LS_LED_OFF 0x1 /* Output HI-Z */ +#define PCA955X_LS_BLINK0 0x2 /* Blink at PWM0 rate */ +#define PCA955X_LS_BLINK1 0x3 /* Blink at PWM1 rate */ + +#define PCA955X_GPIO_INPUT LED_OFF +#define PCA955X_GPIO_HIGH LED_OFF +#define PCA955X_GPIO_LOW LED_FULL + +enum pca955x_type { + pca9550, + pca9551, + pca9552, + ibm_pca9552, + pca9553, +}; + +struct pca955x_chipdef { + int bits; + u8 slv_addr; /* 7-bit slave address mask */ + int slv_addr_shift; /* Number of bits to ignore */ +}; + +static struct pca955x_chipdef pca955x_chipdefs[] = { + [pca9550] = { + .bits = 2, + .slv_addr = /* 110000x */ 0x60, + .slv_addr_shift = 1, + }, + [pca9551] = { + .bits = 8, + .slv_addr = /* 1100xxx */ 0x60, + .slv_addr_shift = 3, + }, + [pca9552] = { + .bits = 16, + .slv_addr = /* 1100xxx */ 0x60, + .slv_addr_shift = 3, + }, + [ibm_pca9552] = { + .bits = 16, + .slv_addr = /* 0110xxx */ 0x30, + .slv_addr_shift = 3, + }, + [pca9553] = { + .bits = 4, + .slv_addr = /* 110001x */ 0x62, + .slv_addr_shift = 1, + }, +}; + +static const struct i2c_device_id pca955x_id[] = { + { "pca9550", pca9550 }, + { "pca9551", pca9551 }, + { "pca9552", pca9552 }, + { "ibm-pca9552", ibm_pca9552 }, + { "pca9553", pca9553 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, pca955x_id); + +struct pca955x { + struct mutex lock; + struct pca955x_led *leds; + struct pca955x_chipdef *chipdef; + struct i2c_client *client; +#ifdef CONFIG_LEDS_PCA955X_GPIO + struct gpio_chip gpio; +#endif +}; + +struct pca955x_led { + struct pca955x *pca955x; + struct led_classdev led_cdev; + int led_num; /* 0 .. 15 potentially */ + char name[32]; + u32 type; + const char *default_trigger; +}; + +struct pca955x_platform_data { + struct pca955x_led *leds; + int num_leds; +}; + +/* 8 bits per input register */ +static inline int pca95xx_num_input_regs(int bits) +{ + return (bits + 7) / 8; +} + +/* 4 bits per LED selector register */ +static inline int pca95xx_num_led_regs(int bits) +{ + return (bits + 3) / 4; +} + +/* + * Return an LED selector register value based on an existing one, with + * the appropriate 2-bit state value set for the given LED number (0-3). + */ +static inline u8 pca955x_ledsel(u8 oldval, int led_num, int state) +{ + return (oldval & (~(0x3 << (led_num << 1)))) | + ((state & 0x3) << (led_num << 1)); +} + +/* + * Write to frequency prescaler register, used to program the + * period of the PWM output. period = (PSCx + 1) / 38 + */ +static int pca955x_write_psc(struct i2c_client *client, int n, u8 val) +{ + struct pca955x *pca955x = i2c_get_clientdata(client); + int ret; + + ret = i2c_smbus_write_byte_data(client, + pca95xx_num_input_regs(pca955x->chipdef->bits) + 2*n, + val); + if (ret < 0) + dev_err(&client->dev, "%s: reg 0x%x, val 0x%x, err %d\n", + __func__, n, val, ret); + return ret; +} + +/* + * Write to PWM register, which determines the duty cycle of the + * output. LED is OFF when the count is less than the value of this + * register, and ON when it is greater. If PWMx == 0, LED is always OFF. + * + * Duty cycle is (256 - PWMx) / 256 + */ +static int pca955x_write_pwm(struct i2c_client *client, int n, u8 val) +{ + struct pca955x *pca955x = i2c_get_clientdata(client); + int ret; + + ret = i2c_smbus_write_byte_data(client, + pca95xx_num_input_regs(pca955x->chipdef->bits) + 1 + 2*n, + val); + if (ret < 0) + dev_err(&client->dev, "%s: reg 0x%x, val 0x%x, err %d\n", + __func__, n, val, ret); + return ret; +} + +/* + * Write to LED selector register, which determines the source that + * drives the LED output. + */ +static int pca955x_write_ls(struct i2c_client *client, int n, u8 val) +{ + struct pca955x *pca955x = i2c_get_clientdata(client); + int ret; + + ret = i2c_smbus_write_byte_data(client, + pca95xx_num_input_regs(pca955x->chipdef->bits) + 4 + n, + val); + if (ret < 0) + dev_err(&client->dev, "%s: reg 0x%x, val 0x%x, err %d\n", + __func__, n, val, ret); + return ret; +} + +/* + * Read the LED selector register, which determines the source that + * drives the LED output. + */ +static int pca955x_read_ls(struct i2c_client *client, int n, u8 *val) +{ + struct pca955x *pca955x = i2c_get_clientdata(client); + int ret; + + ret = i2c_smbus_read_byte_data(client, + pca95xx_num_input_regs(pca955x->chipdef->bits) + 4 + n); + if (ret < 0) { + dev_err(&client->dev, "%s: reg 0x%x, err %d\n", + __func__, n, ret); + return ret; + } + *val = (u8)ret; + return 0; +} + +static int pca955x_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct pca955x_led *pca955x_led; + struct pca955x *pca955x; + u8 ls; + int chip_ls; /* which LSx to use (0-3 potentially) */ + int ls_led; /* which set of bits within LSx to use (0-3) */ + int ret; + + pca955x_led = container_of(led_cdev, struct pca955x_led, led_cdev); + pca955x = pca955x_led->pca955x; + + chip_ls = pca955x_led->led_num / 4; + ls_led = pca955x_led->led_num % 4; + + mutex_lock(&pca955x->lock); + + ret = pca955x_read_ls(pca955x->client, chip_ls, &ls); + if (ret) + goto out; + + switch (value) { + case LED_FULL: + ls = pca955x_ledsel(ls, ls_led, PCA955X_LS_LED_ON); + break; + case LED_OFF: + ls = pca955x_ledsel(ls, ls_led, PCA955X_LS_LED_OFF); + break; + case LED_HALF: + ls = pca955x_ledsel(ls, ls_led, PCA955X_LS_BLINK0); + break; + default: + /* + * Use PWM1 for all other values. This has the unwanted + * side effect of making all LEDs on the chip share the + * same brightness level if set to a value other than + * OFF, HALF, or FULL. But, this is probably better than + * just turning off for all other values. + */ + ret = pca955x_write_pwm(pca955x->client, 1, 255 - value); + if (ret) + goto out; + ls = pca955x_ledsel(ls, ls_led, PCA955X_LS_BLINK1); + break; + } + + ret = pca955x_write_ls(pca955x->client, chip_ls, ls); + +out: + mutex_unlock(&pca955x->lock); + + return ret; +} + +#ifdef CONFIG_LEDS_PCA955X_GPIO +/* + * Read the INPUT register, which contains the state of LEDs. + */ +static int pca955x_read_input(struct i2c_client *client, int n, u8 *val) +{ + int ret = i2c_smbus_read_byte_data(client, n); + + if (ret < 0) { + dev_err(&client->dev, "%s: reg 0x%x, err %d\n", + __func__, n, ret); + return ret; + } + *val = (u8)ret; + return 0; + +} + +static int pca955x_gpio_request_pin(struct gpio_chip *gc, unsigned int offset) +{ + struct pca955x *pca955x = gpiochip_get_data(gc); + struct pca955x_led *led = &pca955x->leds[offset]; + + if (led->type == PCA955X_TYPE_GPIO) + return 0; + + return -EBUSY; +} + +static int pca955x_set_value(struct gpio_chip *gc, unsigned int offset, + int val) +{ + struct pca955x *pca955x = gpiochip_get_data(gc); + struct pca955x_led *led = &pca955x->leds[offset]; + + if (val) + return pca955x_led_set(&led->led_cdev, PCA955X_GPIO_HIGH); + + return pca955x_led_set(&led->led_cdev, PCA955X_GPIO_LOW); +} + +static void pca955x_gpio_set_value(struct gpio_chip *gc, unsigned int offset, + int val) +{ + pca955x_set_value(gc, offset, val); +} + +static int pca955x_gpio_get_value(struct gpio_chip *gc, unsigned int offset) +{ + struct pca955x *pca955x = gpiochip_get_data(gc); + struct pca955x_led *led = &pca955x->leds[offset]; + u8 reg = 0; + + /* There is nothing we can do about errors */ + pca955x_read_input(pca955x->client, led->led_num / 8, ®); + + return !!(reg & (1 << (led->led_num % 8))); +} + +static int pca955x_gpio_direction_input(struct gpio_chip *gc, + unsigned int offset) +{ + struct pca955x *pca955x = gpiochip_get_data(gc); + struct pca955x_led *led = &pca955x->leds[offset]; + + /* To use as input ensure pin is not driven. */ + return pca955x_led_set(&led->led_cdev, PCA955X_GPIO_INPUT); +} + +static int pca955x_gpio_direction_output(struct gpio_chip *gc, + unsigned int offset, int val) +{ + return pca955x_set_value(gc, offset, val); +} +#endif /* CONFIG_LEDS_PCA955X_GPIO */ + +static struct pca955x_platform_data * +pca955x_get_pdata(struct i2c_client *client, struct pca955x_chipdef *chip) +{ + struct pca955x_platform_data *pdata; + struct fwnode_handle *child; + int count; + + count = device_get_child_node_count(&client->dev); + if (!count || count > chip->bits) + return ERR_PTR(-ENODEV); + + pdata = devm_kzalloc(&client->dev, sizeof(*pdata), GFP_KERNEL); + if (!pdata) + return ERR_PTR(-ENOMEM); + + pdata->leds = devm_kcalloc(&client->dev, + chip->bits, sizeof(struct pca955x_led), + GFP_KERNEL); + if (!pdata->leds) + return ERR_PTR(-ENOMEM); + + device_for_each_child_node(&client->dev, child) { + const char *name; + u32 reg; + int res; + + res = fwnode_property_read_u32(child, "reg", ®); + if ((res != 0) || (reg >= chip->bits)) + continue; + + res = fwnode_property_read_string(child, "label", &name); + if ((res != 0) && is_of_node(child)) + name = to_of_node(child)->name; + + snprintf(pdata->leds[reg].name, sizeof(pdata->leds[reg].name), + "%s", name); + + pdata->leds[reg].type = PCA955X_TYPE_LED; + fwnode_property_read_u32(child, "type", &pdata->leds[reg].type); + fwnode_property_read_string(child, "linux,default-trigger", + &pdata->leds[reg].default_trigger); + } + + pdata->num_leds = chip->bits; + + return pdata; +} + +static const struct of_device_id of_pca955x_match[] = { + { .compatible = "nxp,pca9550", .data = (void *)pca9550 }, + { .compatible = "nxp,pca9551", .data = (void *)pca9551 }, + { .compatible = "nxp,pca9552", .data = (void *)pca9552 }, + { .compatible = "ibm,pca9552", .data = (void *)ibm_pca9552 }, + { .compatible = "nxp,pca9553", .data = (void *)pca9553 }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_pca955x_match); + +static int pca955x_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct pca955x *pca955x; + struct pca955x_led *pca955x_led; + struct pca955x_chipdef *chip; + struct i2c_adapter *adapter; + int i, err; + struct pca955x_platform_data *pdata; + int ngpios = 0; + + chip = &pca955x_chipdefs[id->driver_data]; + adapter = client->adapter; + pdata = dev_get_platdata(&client->dev); + if (!pdata) { + pdata = pca955x_get_pdata(client, chip); + if (IS_ERR(pdata)) + return PTR_ERR(pdata); + } + + /* Make sure the slave address / chip type combo given is possible */ + if ((client->addr & ~((1 << chip->slv_addr_shift) - 1)) != + chip->slv_addr) { + dev_err(&client->dev, "invalid slave address %02x\n", + client->addr); + return -ENODEV; + } + + dev_info(&client->dev, "leds-pca955x: Using %s %d-bit LED driver at " + "slave address 0x%02x\n", + client->name, chip->bits, client->addr); + + if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA)) + return -EIO; + + if (pdata->num_leds != chip->bits) { + dev_err(&client->dev, + "board info claims %d LEDs on a %d-bit chip\n", + pdata->num_leds, chip->bits); + return -ENODEV; + } + + pca955x = devm_kzalloc(&client->dev, sizeof(*pca955x), GFP_KERNEL); + if (!pca955x) + return -ENOMEM; + + pca955x->leds = devm_kcalloc(&client->dev, + chip->bits, sizeof(*pca955x_led), GFP_KERNEL); + if (!pca955x->leds) + return -ENOMEM; + + i2c_set_clientdata(client, pca955x); + + mutex_init(&pca955x->lock); + pca955x->client = client; + pca955x->chipdef = chip; + + for (i = 0; i < chip->bits; i++) { + pca955x_led = &pca955x->leds[i]; + pca955x_led->led_num = i; + pca955x_led->pca955x = pca955x; + pca955x_led->type = pdata->leds[i].type; + + switch (pca955x_led->type) { + case PCA955X_TYPE_NONE: + break; + case PCA955X_TYPE_GPIO: + ngpios++; + break; + case PCA955X_TYPE_LED: + /* + * Platform data can specify LED names and + * default triggers + */ + if (pdata->leds[i].name[0] == '\0') + snprintf(pdata->leds[i].name, + sizeof(pdata->leds[i].name), "%d", i); + + snprintf(pca955x_led->name, + sizeof(pca955x_led->name), "pca955x:%s", + pdata->leds[i].name); + + if (pdata->leds[i].default_trigger) + pca955x_led->led_cdev.default_trigger = + pdata->leds[i].default_trigger; + + pca955x_led->led_cdev.name = pca955x_led->name; + pca955x_led->led_cdev.brightness_set_blocking = + pca955x_led_set; + + err = devm_led_classdev_register(&client->dev, + &pca955x_led->led_cdev); + if (err) + return err; + + /* Turn off LED */ + err = pca955x_led_set(&pca955x_led->led_cdev, LED_OFF); + if (err) + return err; + } + } + + /* PWM0 is used for half brightness or 50% duty cycle */ + err = pca955x_write_pwm(client, 0, 255 - LED_HALF); + if (err) + return err; + + /* PWM1 is used for variable brightness, default to OFF */ + err = pca955x_write_pwm(client, 1, 0); + if (err) + return err; + + /* Set to fast frequency so we do not see flashing */ + err = pca955x_write_psc(client, 0, 0); + if (err) + return err; + err = pca955x_write_psc(client, 1, 0); + if (err) + return err; + +#ifdef CONFIG_LEDS_PCA955X_GPIO + if (ngpios) { + pca955x->gpio.label = "gpio-pca955x"; + pca955x->gpio.direction_input = pca955x_gpio_direction_input; + pca955x->gpio.direction_output = pca955x_gpio_direction_output; + pca955x->gpio.set = pca955x_gpio_set_value; + pca955x->gpio.get = pca955x_gpio_get_value; + pca955x->gpio.request = pca955x_gpio_request_pin; + pca955x->gpio.can_sleep = 1; + pca955x->gpio.base = -1; + pca955x->gpio.ngpio = ngpios; + pca955x->gpio.parent = &client->dev; + pca955x->gpio.owner = THIS_MODULE; + + err = devm_gpiochip_add_data(&client->dev, &pca955x->gpio, + pca955x); + if (err) { + /* Use data->gpio.dev as a flag for freeing gpiochip */ + pca955x->gpio.parent = NULL; + dev_warn(&client->dev, "could not add gpiochip\n"); + return err; + } + dev_info(&client->dev, "gpios %i...%i\n", + pca955x->gpio.base, pca955x->gpio.base + + pca955x->gpio.ngpio - 1); + } +#endif + + return 0; +} + +static struct i2c_driver pca955x_driver = { + .driver = { + .name = "leds-pca955x", + .of_match_table = of_pca955x_match, + }, + .probe = pca955x_probe, + .id_table = pca955x_id, +}; + +module_i2c_driver(pca955x_driver); + +MODULE_AUTHOR("Nate Case <ncase@xes-inc.com>"); +MODULE_DESCRIPTION("PCA955x LED driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-pca963x.c b/drivers/leds/leds-pca963x.c new file mode 100644 index 000000000..00aecd67e --- /dev/null +++ b/drivers/leds/leds-pca963x.c @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2011 bct electronic GmbH + * Copyright 2013 Qtechnology/AS + * + * Author: Peter Meerwald <p.meerwald@bct-electronic.com> + * Author: Ricardo Ribalda <ribalda@kernel.org> + * + * Based on leds-pca955x.c + * + * LED driver for the PCA9633 I2C LED driver (7-bit slave address 0x62) + * LED driver for the PCA9634/5 I2C LED driver (7-bit slave address set by hw.) + * + * Note that hardware blinking violates the leds infrastructure driver + * interface since the hardware only supports blinking all LEDs with the + * same delay_on/delay_off rates. That is, only the LEDs that are set to + * blink will actually blink but all LEDs that are set to blink will blink + * in identical fashion. The delay_on/delay_off values of the last LED + * that is set to blink will be used for all of the blinking LEDs. + * Hardware blinking is disabled by default but can be enabled by setting + * the 'blink_type' member in the platform_data struct to 'PCA963X_HW_BLINK' + * or by adding the 'nxp,hw-blink' property to the DTS. + */ + +#include <linux/module.h> +#include <linux/delay.h> +#include <linux/string.h> +#include <linux/ctype.h> +#include <linux/leds.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/property.h> +#include <linux/slab.h> +#include <linux/of.h> + +/* LED select registers determine the source that drives LED outputs */ +#define PCA963X_LED_OFF 0x0 /* LED driver off */ +#define PCA963X_LED_ON 0x1 /* LED driver on */ +#define PCA963X_LED_PWM 0x2 /* Controlled through PWM */ +#define PCA963X_LED_GRP_PWM 0x3 /* Controlled through PWM/GRPPWM */ + +#define PCA963X_MODE2_OUTDRV 0x04 /* Open-drain or totem pole */ +#define PCA963X_MODE2_INVRT 0x10 /* Normal or inverted direction */ +#define PCA963X_MODE2_DMBLNK 0x20 /* Enable blinking */ + +#define PCA963X_MODE1 0x00 +#define PCA963X_MODE2 0x01 +#define PCA963X_PWM_BASE 0x02 + +enum pca963x_type { + pca9633, + pca9634, + pca9635, +}; + +struct pca963x_chipdef { + u8 grppwm; + u8 grpfreq; + u8 ledout_base; + int n_leds; + unsigned int scaling; +}; + +static struct pca963x_chipdef pca963x_chipdefs[] = { + [pca9633] = { + .grppwm = 0x6, + .grpfreq = 0x7, + .ledout_base = 0x8, + .n_leds = 4, + }, + [pca9634] = { + .grppwm = 0xa, + .grpfreq = 0xb, + .ledout_base = 0xc, + .n_leds = 8, + }, + [pca9635] = { + .grppwm = 0x12, + .grpfreq = 0x13, + .ledout_base = 0x14, + .n_leds = 16, + }, +}; + +/* Total blink period in milliseconds */ +#define PCA963X_BLINK_PERIOD_MIN 42 +#define PCA963X_BLINK_PERIOD_MAX 10667 + +static const struct i2c_device_id pca963x_id[] = { + { "pca9632", pca9633 }, + { "pca9633", pca9633 }, + { "pca9634", pca9634 }, + { "pca9635", pca9635 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, pca963x_id); + +struct pca963x; + +struct pca963x_led { + struct pca963x *chip; + struct led_classdev led_cdev; + int led_num; /* 0 .. 15 potentially */ + u8 gdc; + u8 gfrq; +}; + +struct pca963x { + struct pca963x_chipdef *chipdef; + struct mutex mutex; + struct i2c_client *client; + unsigned long leds_on; + struct pca963x_led leds[]; +}; + +static int pca963x_brightness(struct pca963x_led *led, + enum led_brightness brightness) +{ + struct i2c_client *client = led->chip->client; + struct pca963x_chipdef *chipdef = led->chip->chipdef; + u8 ledout_addr, ledout, mask, val; + int shift; + int ret; + + ledout_addr = chipdef->ledout_base + (led->led_num / 4); + shift = 2 * (led->led_num % 4); + mask = 0x3 << shift; + ledout = i2c_smbus_read_byte_data(client, ledout_addr); + + switch (brightness) { + case LED_FULL: + val = (ledout & ~mask) | (PCA963X_LED_ON << shift); + ret = i2c_smbus_write_byte_data(client, ledout_addr, val); + break; + case LED_OFF: + val = ledout & ~mask; + ret = i2c_smbus_write_byte_data(client, ledout_addr, val); + break; + default: + ret = i2c_smbus_write_byte_data(client, + PCA963X_PWM_BASE + + led->led_num, + brightness); + if (ret < 0) + return ret; + + val = (ledout & ~mask) | (PCA963X_LED_PWM << shift); + ret = i2c_smbus_write_byte_data(client, ledout_addr, val); + break; + } + + return ret; +} + +static void pca963x_blink(struct pca963x_led *led) +{ + struct i2c_client *client = led->chip->client; + struct pca963x_chipdef *chipdef = led->chip->chipdef; + u8 ledout_addr, ledout, mask, val, mode2; + int shift; + + ledout_addr = chipdef->ledout_base + (led->led_num / 4); + shift = 2 * (led->led_num % 4); + mask = 0x3 << shift; + mode2 = i2c_smbus_read_byte_data(client, PCA963X_MODE2); + + i2c_smbus_write_byte_data(client, chipdef->grppwm, led->gdc); + + i2c_smbus_write_byte_data(client, chipdef->grpfreq, led->gfrq); + + if (!(mode2 & PCA963X_MODE2_DMBLNK)) + i2c_smbus_write_byte_data(client, PCA963X_MODE2, + mode2 | PCA963X_MODE2_DMBLNK); + + mutex_lock(&led->chip->mutex); + + ledout = i2c_smbus_read_byte_data(client, ledout_addr); + if ((ledout & mask) != (PCA963X_LED_GRP_PWM << shift)) { + val = (ledout & ~mask) | (PCA963X_LED_GRP_PWM << shift); + i2c_smbus_write_byte_data(client, ledout_addr, val); + } + + mutex_unlock(&led->chip->mutex); +} + +static int pca963x_power_state(struct pca963x_led *led) +{ + struct i2c_client *client = led->chip->client; + unsigned long *leds_on = &led->chip->leds_on; + unsigned long cached_leds = *leds_on; + + if (led->led_cdev.brightness) + set_bit(led->led_num, leds_on); + else + clear_bit(led->led_num, leds_on); + + if (!(*leds_on) != !cached_leds) + return i2c_smbus_write_byte_data(client, PCA963X_MODE1, + *leds_on ? 0 : BIT(4)); + + return 0; +} + +static int pca963x_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct pca963x_led *led; + int ret; + + led = container_of(led_cdev, struct pca963x_led, led_cdev); + + mutex_lock(&led->chip->mutex); + + ret = pca963x_brightness(led, value); + if (ret < 0) + goto unlock; + ret = pca963x_power_state(led); + +unlock: + mutex_unlock(&led->chip->mutex); + return ret; +} + +static unsigned int pca963x_period_scale(struct pca963x_led *led, + unsigned int val) +{ + unsigned int scaling = led->chip->chipdef->scaling; + + return scaling ? DIV_ROUND_CLOSEST(val * scaling, 1000) : val; +} + +static int pca963x_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, unsigned long *delay_off) +{ + unsigned long time_on, time_off, period; + struct pca963x_led *led; + u8 gdc, gfrq; + + led = container_of(led_cdev, struct pca963x_led, led_cdev); + + time_on = *delay_on; + time_off = *delay_off; + + /* If both zero, pick reasonable defaults of 500ms each */ + if (!time_on && !time_off) { + time_on = 500; + time_off = 500; + } + + period = pca963x_period_scale(led, time_on + time_off); + + /* If period not supported by hardware, default to someting sane. */ + if ((period < PCA963X_BLINK_PERIOD_MIN) || + (period > PCA963X_BLINK_PERIOD_MAX)) { + time_on = 500; + time_off = 500; + period = pca963x_period_scale(led, 1000); + } + + /* + * From manual: duty cycle = (GDC / 256) -> + * (time_on / period) = (GDC / 256) -> + * GDC = ((time_on * 256) / period) + */ + gdc = (pca963x_period_scale(led, time_on) * 256) / period; + + /* + * From manual: period = ((GFRQ + 1) / 24) in seconds. + * So, period (in ms) = (((GFRQ + 1) / 24) * 1000) -> + * GFRQ = ((period * 24 / 1000) - 1) + */ + gfrq = (period * 24 / 1000) - 1; + + led->gdc = gdc; + led->gfrq = gfrq; + + pca963x_blink(led); + + *delay_on = time_on; + *delay_off = time_off; + + return 0; +} + +static int pca963x_register_leds(struct i2c_client *client, + struct pca963x *chip) +{ + struct pca963x_chipdef *chipdef = chip->chipdef; + struct pca963x_led *led = chip->leds; + struct device *dev = &client->dev; + struct fwnode_handle *child; + bool hw_blink; + s32 mode2; + u32 reg; + int ret; + + if (device_property_read_u32(dev, "nxp,period-scale", + &chipdef->scaling)) + chipdef->scaling = 1000; + + hw_blink = device_property_read_bool(dev, "nxp,hw-blink"); + + mode2 = i2c_smbus_read_byte_data(client, PCA963X_MODE2); + if (mode2 < 0) + return mode2; + + /* default to open-drain unless totem pole (push-pull) is specified */ + if (device_property_read_bool(dev, "nxp,totem-pole")) + mode2 |= PCA963X_MODE2_OUTDRV; + else + mode2 &= ~PCA963X_MODE2_OUTDRV; + + /* default to non-inverted output, unless inverted is specified */ + if (device_property_read_bool(dev, "nxp,inverted-out")) + mode2 |= PCA963X_MODE2_INVRT; + else + mode2 &= ~PCA963X_MODE2_INVRT; + + ret = i2c_smbus_write_byte_data(client, PCA963X_MODE2, mode2); + if (ret < 0) + return ret; + + device_for_each_child_node(dev, child) { + struct led_init_data init_data = {}; + char default_label[32]; + + ret = fwnode_property_read_u32(child, "reg", ®); + if (ret || reg >= chipdef->n_leds) { + dev_err(dev, "Invalid 'reg' property for node %pfw\n", + child); + ret = -EINVAL; + goto err; + } + + led->led_num = reg; + led->chip = chip; + led->led_cdev.brightness_set_blocking = pca963x_led_set; + if (hw_blink) + led->led_cdev.blink_set = pca963x_blink_set; + + init_data.fwnode = child; + /* for backwards compatibility */ + init_data.devicename = "pca963x"; + snprintf(default_label, sizeof(default_label), "%d:%.2x:%u", + client->adapter->nr, client->addr, reg); + init_data.default_label = default_label; + + ret = devm_led_classdev_register_ext(dev, &led->led_cdev, + &init_data); + if (ret) { + dev_err(dev, "Failed to register LED for node %pfw\n", + child); + goto err; + } + + ++led; + } + + return 0; +err: + fwnode_handle_put(child); + return ret; +} + +static const struct of_device_id of_pca963x_match[] = { + { .compatible = "nxp,pca9632", }, + { .compatible = "nxp,pca9633", }, + { .compatible = "nxp,pca9634", }, + { .compatible = "nxp,pca9635", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_pca963x_match); + +static int pca963x_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct device *dev = &client->dev; + struct pca963x_chipdef *chipdef; + struct pca963x *chip; + int i, count; + + chipdef = &pca963x_chipdefs[id->driver_data]; + + count = device_get_child_node_count(dev); + if (!count || count > chipdef->n_leds) { + dev_err(dev, "Node %pfw must define between 1 and %d LEDs\n", + dev_fwnode(dev), chipdef->n_leds); + return -EINVAL; + } + + chip = devm_kzalloc(dev, struct_size(chip, leds, count), GFP_KERNEL); + if (!chip) + return -ENOMEM; + + i2c_set_clientdata(client, chip); + + mutex_init(&chip->mutex); + chip->chipdef = chipdef; + chip->client = client; + + /* Turn off LEDs by default*/ + for (i = 0; i < chipdef->n_leds / 4; i++) + i2c_smbus_write_byte_data(client, chipdef->ledout_base + i, 0x00); + + /* Disable LED all-call address, and power down initially */ + i2c_smbus_write_byte_data(client, PCA963X_MODE1, BIT(4)); + + return pca963x_register_leds(client, chip); +} + +static struct i2c_driver pca963x_driver = { + .driver = { + .name = "leds-pca963x", + .of_match_table = of_pca963x_match, + }, + .probe = pca963x_probe, + .id_table = pca963x_id, +}; + +module_i2c_driver(pca963x_driver); + +MODULE_AUTHOR("Peter Meerwald <p.meerwald@bct-electronic.com>"); +MODULE_DESCRIPTION("PCA963X LED driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-pm8058.c b/drivers/leds/leds-pm8058.c new file mode 100644 index 000000000..fb2ab72c0 --- /dev/null +++ b/drivers/leds/leds-pm8058.c @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Copyright (c) 2010, 2011, 2016 The Linux Foundation. All rights reserved. + */ +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/pm.h> +#include <linux/regmap.h> + +#define PM8058_LED_TYPE_COMMON 0x00 +#define PM8058_LED_TYPE_KEYPAD 0x01 +#define PM8058_LED_TYPE_FLASH 0x02 + +#define PM8058_LED_TYPE_COMMON_MASK 0xf8 +#define PM8058_LED_TYPE_KEYPAD_MASK 0xf0 +#define PM8058_LED_TYPE_COMMON_SHIFT 3 +#define PM8058_LED_TYPE_KEYPAD_SHIFT 4 + +struct pm8058_led { + struct regmap *map; + u32 reg; + u32 ledtype; + struct led_classdev cdev; +}; + +static void pm8058_led_set(struct led_classdev *cled, + enum led_brightness value) +{ + struct pm8058_led *led; + int ret = 0; + unsigned int mask = 0; + unsigned int val = 0; + + led = container_of(cled, struct pm8058_led, cdev); + switch (led->ledtype) { + case PM8058_LED_TYPE_COMMON: + mask = PM8058_LED_TYPE_COMMON_MASK; + val = value << PM8058_LED_TYPE_COMMON_SHIFT; + break; + case PM8058_LED_TYPE_KEYPAD: + case PM8058_LED_TYPE_FLASH: + mask = PM8058_LED_TYPE_KEYPAD_MASK; + val = value << PM8058_LED_TYPE_KEYPAD_SHIFT; + break; + default: + break; + } + + ret = regmap_update_bits(led->map, led->reg, mask, val); + if (ret) + pr_err("Failed to set LED brightness\n"); +} + +static enum led_brightness pm8058_led_get(struct led_classdev *cled) +{ + struct pm8058_led *led; + int ret; + unsigned int val; + + led = container_of(cled, struct pm8058_led, cdev); + + ret = regmap_read(led->map, led->reg, &val); + if (ret) { + pr_err("Failed to get LED brightness\n"); + return LED_OFF; + } + + switch (led->ledtype) { + case PM8058_LED_TYPE_COMMON: + val &= PM8058_LED_TYPE_COMMON_MASK; + val >>= PM8058_LED_TYPE_COMMON_SHIFT; + break; + case PM8058_LED_TYPE_KEYPAD: + case PM8058_LED_TYPE_FLASH: + val &= PM8058_LED_TYPE_KEYPAD_MASK; + val >>= PM8058_LED_TYPE_KEYPAD_SHIFT; + break; + default: + val = LED_OFF; + break; + } + + return val; +} + +static int pm8058_led_probe(struct platform_device *pdev) +{ + struct led_init_data init_data = {}; + struct device *dev = &pdev->dev; + struct pm8058_led *led; + struct device_node *np; + int ret; + struct regmap *map; + const char *state; + enum led_brightness maxbright; + + led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->ledtype = (u32)(unsigned long)of_device_get_match_data(dev); + + map = dev_get_regmap(dev->parent, NULL); + if (!map) { + dev_err(dev, "Parent regmap unavailable.\n"); + return -ENXIO; + } + led->map = map; + + np = dev_of_node(dev); + + ret = of_property_read_u32(np, "reg", &led->reg); + if (ret) { + dev_err(dev, "no register offset specified\n"); + return -EINVAL; + } + + led->cdev.brightness_set = pm8058_led_set; + led->cdev.brightness_get = pm8058_led_get; + if (led->ledtype == PM8058_LED_TYPE_COMMON) + maxbright = 31; /* 5 bits */ + else + maxbright = 15; /* 4 bits */ + led->cdev.max_brightness = maxbright; + + state = of_get_property(np, "default-state", NULL); + if (state) { + if (!strcmp(state, "keep")) { + led->cdev.brightness = pm8058_led_get(&led->cdev); + } else if (!strcmp(state, "on")) { + led->cdev.brightness = maxbright; + pm8058_led_set(&led->cdev, maxbright); + } else { + led->cdev.brightness = LED_OFF; + pm8058_led_set(&led->cdev, LED_OFF); + } + } + + if (led->ledtype == PM8058_LED_TYPE_KEYPAD || + led->ledtype == PM8058_LED_TYPE_FLASH) + led->cdev.flags = LED_CORE_SUSPENDRESUME; + + init_data.fwnode = of_fwnode_handle(np); + + ret = devm_led_classdev_register_ext(dev, &led->cdev, &init_data); + if (ret) + dev_err(dev, "Failed to register LED for %pOF\n", np); + + return ret; +} + +static const struct of_device_id pm8058_leds_id_table[] = { + { + .compatible = "qcom,pm8058-led", + .data = (void *)PM8058_LED_TYPE_COMMON + }, + { + .compatible = "qcom,pm8058-keypad-led", + .data = (void *)PM8058_LED_TYPE_KEYPAD + }, + { + .compatible = "qcom,pm8058-flash-led", + .data = (void *)PM8058_LED_TYPE_FLASH + }, + { }, +}; +MODULE_DEVICE_TABLE(of, pm8058_leds_id_table); + +static struct platform_driver pm8058_led_driver = { + .probe = pm8058_led_probe, + .driver = { + .name = "pm8058-leds", + .of_match_table = pm8058_leds_id_table, + }, +}; +module_platform_driver(pm8058_led_driver); + +MODULE_DESCRIPTION("PM8058 LEDs driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:pm8058-leds"); diff --git a/drivers/leds/leds-powernv.c b/drivers/leds/leds-powernv.c new file mode 100644 index 000000000..743e2cdd0 --- /dev/null +++ b/drivers/leds/leds-powernv.c @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * PowerNV LED Driver + * + * Copyright IBM Corp. 2015 + * + * Author: Vasant Hegde <hegdevasant@linux.vnet.ibm.com> + * Author: Anshuman Khandual <khandual@linux.vnet.ibm.com> + */ + +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/types.h> + +#include <asm/opal.h> + +/* Map LED type to description. */ +struct led_type_map { + const int type; + const char *desc; +}; +static const struct led_type_map led_type_map[] = { + {OPAL_SLOT_LED_TYPE_ID, "identify"}, + {OPAL_SLOT_LED_TYPE_FAULT, "fault"}, + {OPAL_SLOT_LED_TYPE_ATTN, "attention"}, + {-1, NULL}, +}; + +struct powernv_led_common { + /* + * By default unload path resets all the LEDs. But on PowerNV + * platform we want to retain LED state across reboot as these + * are controlled by firmware. Also service processor can modify + * the LEDs independent of OS. Hence avoid resetting LEDs in + * unload path. + */ + bool led_disabled; + + /* Max supported LED type */ + __be64 max_led_type; + + /* glabal lock */ + struct mutex lock; +}; + +/* PowerNV LED data */ +struct powernv_led_data { + struct led_classdev cdev; + char *loc_code; /* LED location code */ + int led_type; /* OPAL_SLOT_LED_TYPE_* */ + + struct powernv_led_common *common; +}; + + +/* Returns OPAL_SLOT_LED_TYPE_* for given led type string */ +static int powernv_get_led_type(const char *led_type_desc) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(led_type_map); i++) + if (!strcmp(led_type_map[i].desc, led_type_desc)) + return led_type_map[i].type; + + return -1; +} + +/* + * This commits the state change of the requested LED through an OPAL call. + * This function is called from work queue task context when ever it gets + * scheduled. This function can sleep at opal_async_wait_response call. + */ +static int powernv_led_set(struct powernv_led_data *powernv_led, + enum led_brightness value) +{ + int rc, token; + u64 led_mask, led_value = 0; + __be64 max_type; + struct opal_msg msg; + struct device *dev = powernv_led->cdev.dev; + struct powernv_led_common *powernv_led_common = powernv_led->common; + + /* Prepare for the OPAL call */ + max_type = powernv_led_common->max_led_type; + led_mask = OPAL_SLOT_LED_STATE_ON << powernv_led->led_type; + if (value) + led_value = led_mask; + + /* OPAL async call */ + token = opal_async_get_token_interruptible(); + if (token < 0) { + if (token != -ERESTARTSYS) + dev_err(dev, "%s: Couldn't get OPAL async token\n", + __func__); + return token; + } + + rc = opal_leds_set_ind(token, powernv_led->loc_code, + led_mask, led_value, &max_type); + if (rc != OPAL_ASYNC_COMPLETION) { + dev_err(dev, "%s: OPAL set LED call failed for %s [rc=%d]\n", + __func__, powernv_led->loc_code, rc); + goto out_token; + } + + rc = opal_async_wait_response(token, &msg); + if (rc) { + dev_err(dev, + "%s: Failed to wait for the async response [rc=%d]\n", + __func__, rc); + goto out_token; + } + + rc = opal_get_async_rc(msg); + if (rc != OPAL_SUCCESS) + dev_err(dev, "%s : OAPL async call returned failed [rc=%d]\n", + __func__, rc); + +out_token: + opal_async_release_token(token); + return rc; +} + +/* + * This function fetches the LED state for a given LED type for + * mentioned LED classdev structure. + */ +static enum led_brightness powernv_led_get(struct powernv_led_data *powernv_led) +{ + int rc; + __be64 mask, value, max_type; + u64 led_mask, led_value; + struct device *dev = powernv_led->cdev.dev; + struct powernv_led_common *powernv_led_common = powernv_led->common; + + /* Fetch all LED status */ + mask = cpu_to_be64(0); + value = cpu_to_be64(0); + max_type = powernv_led_common->max_led_type; + + rc = opal_leds_get_ind(powernv_led->loc_code, + &mask, &value, &max_type); + if (rc != OPAL_SUCCESS && rc != OPAL_PARTIAL) { + dev_err(dev, "%s: OPAL get led call failed [rc=%d]\n", + __func__, rc); + return LED_OFF; + } + + led_mask = be64_to_cpu(mask); + led_value = be64_to_cpu(value); + + /* LED status available */ + if (!((led_mask >> powernv_led->led_type) & OPAL_SLOT_LED_STATE_ON)) { + dev_err(dev, "%s: LED status not available for %s\n", + __func__, powernv_led->cdev.name); + return LED_OFF; + } + + /* LED status value */ + if ((led_value >> powernv_led->led_type) & OPAL_SLOT_LED_STATE_ON) + return LED_FULL; + + return LED_OFF; +} + +/* + * LED classdev 'brightness_get' function. This schedules work + * to update LED state. + */ +static int powernv_brightness_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct powernv_led_data *powernv_led = + container_of(led_cdev, struct powernv_led_data, cdev); + struct powernv_led_common *powernv_led_common = powernv_led->common; + int rc; + + /* Do not modify LED in unload path */ + if (powernv_led_common->led_disabled) + return 0; + + mutex_lock(&powernv_led_common->lock); + rc = powernv_led_set(powernv_led, value); + mutex_unlock(&powernv_led_common->lock); + + return rc; +} + +/* LED classdev 'brightness_get' function */ +static enum led_brightness powernv_brightness_get(struct led_classdev *led_cdev) +{ + struct powernv_led_data *powernv_led = + container_of(led_cdev, struct powernv_led_data, cdev); + + return powernv_led_get(powernv_led); +} + +/* + * This function registers classdev structure for any given type of LED on + * a given child LED device node. + */ +static int powernv_led_create(struct device *dev, + struct powernv_led_data *powernv_led, + const char *led_type_desc) +{ + int rc; + + /* Make sure LED type is supported */ + powernv_led->led_type = powernv_get_led_type(led_type_desc); + if (powernv_led->led_type == -1) { + dev_warn(dev, "%s: No support for led type : %s\n", + __func__, led_type_desc); + return -EINVAL; + } + + /* Create the name for classdev */ + powernv_led->cdev.name = devm_kasprintf(dev, GFP_KERNEL, "%s:%s", + powernv_led->loc_code, + led_type_desc); + if (!powernv_led->cdev.name) + return -ENOMEM; + + powernv_led->cdev.brightness_set_blocking = powernv_brightness_set; + powernv_led->cdev.brightness_get = powernv_brightness_get; + powernv_led->cdev.brightness = LED_OFF; + powernv_led->cdev.max_brightness = LED_FULL; + + /* Register the classdev */ + rc = devm_led_classdev_register(dev, &powernv_led->cdev); + if (rc) { + dev_err(dev, "%s: Classdev registration failed for %s\n", + __func__, powernv_led->cdev.name); + } + + return rc; +} + +/* Go through LED device tree node and register LED classdev structure */ +static int powernv_led_classdev(struct platform_device *pdev, + struct device_node *led_node, + struct powernv_led_common *powernv_led_common) +{ + const char *cur = NULL; + int rc = -1; + struct property *p; + struct device_node *np; + struct powernv_led_data *powernv_led; + struct device *dev = &pdev->dev; + + for_each_available_child_of_node(led_node, np) { + p = of_find_property(np, "led-types", NULL); + + while ((cur = of_prop_next_string(p, cur)) != NULL) { + powernv_led = devm_kzalloc(dev, sizeof(*powernv_led), + GFP_KERNEL); + if (!powernv_led) { + of_node_put(np); + return -ENOMEM; + } + + powernv_led->common = powernv_led_common; + powernv_led->loc_code = (char *)np->name; + + rc = powernv_led_create(dev, powernv_led, cur); + if (rc) { + of_node_put(np); + return rc; + } + } /* while end */ + } + + return rc; +} + +/* Platform driver probe */ +static int powernv_led_probe(struct platform_device *pdev) +{ + struct device_node *led_node; + struct powernv_led_common *powernv_led_common; + struct device *dev = &pdev->dev; + int rc; + + led_node = of_find_node_by_path("/ibm,opal/leds"); + if (!led_node) { + dev_err(dev, "%s: LED parent device node not found\n", + __func__); + return -EINVAL; + } + + powernv_led_common = devm_kzalloc(dev, sizeof(*powernv_led_common), + GFP_KERNEL); + if (!powernv_led_common) { + rc = -ENOMEM; + goto out; + } + + mutex_init(&powernv_led_common->lock); + powernv_led_common->max_led_type = cpu_to_be64(OPAL_SLOT_LED_TYPE_MAX); + + platform_set_drvdata(pdev, powernv_led_common); + + rc = powernv_led_classdev(pdev, led_node, powernv_led_common); +out: + of_node_put(led_node); + return rc; +} + +/* Platform driver remove */ +static int powernv_led_remove(struct platform_device *pdev) +{ + struct powernv_led_common *powernv_led_common; + + /* Disable LED operation */ + powernv_led_common = platform_get_drvdata(pdev); + powernv_led_common->led_disabled = true; + + /* Destroy lock */ + mutex_destroy(&powernv_led_common->lock); + + dev_info(&pdev->dev, "PowerNV led module unregistered\n"); + return 0; +} + +/* Platform driver property match */ +static const struct of_device_id powernv_led_match[] = { + { + .compatible = "ibm,opal-v3-led", + }, + {}, +}; +MODULE_DEVICE_TABLE(of, powernv_led_match); + +static struct platform_driver powernv_led_driver = { + .probe = powernv_led_probe, + .remove = powernv_led_remove, + .driver = { + .name = "powernv-led-driver", + .of_match_table = powernv_led_match, + }, +}; + +module_platform_driver(powernv_led_driver); + +MODULE_LICENSE("GPL v2"); +MODULE_DESCRIPTION("PowerNV LED driver"); +MODULE_AUTHOR("Vasant Hegde <hegdevasant@linux.vnet.ibm.com>"); diff --git a/drivers/leds/leds-pwm.c b/drivers/leds/leds-pwm.c new file mode 100644 index 000000000..f4c0507be --- /dev/null +++ b/drivers/leds/leds-pwm.c @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * linux/drivers/leds-pwm.c + * + * simple PWM based LED control + * + * Copyright 2009 Luotao Fu @ Pengutronix (l.fu@pengutronix.de) + * + * based on leds-gpio.c by Raphael Assenat <raph@8d.com> + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/of_platform.h> +#include <linux/leds.h> +#include <linux/err.h> +#include <linux/pwm.h> +#include <linux/slab.h> + +struct led_pwm { + const char *name; + u8 active_low; + unsigned int max_brightness; +}; + +struct led_pwm_data { + struct led_classdev cdev; + struct pwm_device *pwm; + struct pwm_state pwmstate; + unsigned int active_low; +}; + +struct led_pwm_priv { + int num_leds; + struct led_pwm_data leds[]; +}; + +static int led_pwm_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct led_pwm_data *led_dat = + container_of(led_cdev, struct led_pwm_data, cdev); + unsigned int max = led_dat->cdev.max_brightness; + unsigned long long duty = led_dat->pwmstate.period; + + duty *= brightness; + do_div(duty, max); + + if (led_dat->active_low) + duty = led_dat->pwmstate.period - duty; + + led_dat->pwmstate.duty_cycle = duty; + led_dat->pwmstate.enabled = true; + return pwm_apply_state(led_dat->pwm, &led_dat->pwmstate); +} + +__attribute__((nonnull)) +static int led_pwm_add(struct device *dev, struct led_pwm_priv *priv, + struct led_pwm *led, struct fwnode_handle *fwnode) +{ + struct led_pwm_data *led_data = &priv->leds[priv->num_leds]; + struct led_init_data init_data = { .fwnode = fwnode }; + int ret; + + led_data->active_low = led->active_low; + led_data->cdev.name = led->name; + led_data->cdev.brightness = LED_OFF; + led_data->cdev.max_brightness = led->max_brightness; + led_data->cdev.flags = LED_CORE_SUSPENDRESUME; + + led_data->pwm = devm_fwnode_pwm_get(dev, fwnode, NULL); + if (IS_ERR(led_data->pwm)) + return dev_err_probe(dev, PTR_ERR(led_data->pwm), + "unable to request PWM for %s\n", + led->name); + + led_data->cdev.brightness_set_blocking = led_pwm_set; + + pwm_init_state(led_data->pwm, &led_data->pwmstate); + + ret = devm_led_classdev_register_ext(dev, &led_data->cdev, &init_data); + if (ret) { + dev_err(dev, "failed to register PWM led for %s: %d\n", + led->name, ret); + return ret; + } + + ret = led_pwm_set(&led_data->cdev, led_data->cdev.brightness); + if (ret) { + dev_err(dev, "failed to set led PWM value for %s: %d", + led->name, ret); + return ret; + } + + priv->num_leds++; + return 0; +} + +static int led_pwm_create_fwnode(struct device *dev, struct led_pwm_priv *priv) +{ + struct fwnode_handle *fwnode; + struct led_pwm led; + int ret = 0; + + memset(&led, 0, sizeof(led)); + + device_for_each_child_node(dev, fwnode) { + ret = fwnode_property_read_string(fwnode, "label", &led.name); + if (ret && is_of_node(fwnode)) + led.name = to_of_node(fwnode)->name; + + if (!led.name) { + fwnode_handle_put(fwnode); + return -EINVAL; + } + + led.active_low = fwnode_property_read_bool(fwnode, + "active-low"); + fwnode_property_read_u32(fwnode, "max-brightness", + &led.max_brightness); + + ret = led_pwm_add(dev, priv, &led, fwnode); + if (ret) { + fwnode_handle_put(fwnode); + break; + } + } + + return ret; +} + +static int led_pwm_probe(struct platform_device *pdev) +{ + struct led_pwm_priv *priv; + int ret = 0; + int count; + + count = device_get_child_node_count(&pdev->dev); + + if (!count) + return -EINVAL; + + priv = devm_kzalloc(&pdev->dev, struct_size(priv, leds, count), + GFP_KERNEL); + if (!priv) + return -ENOMEM; + + ret = led_pwm_create_fwnode(&pdev->dev, priv); + + if (ret) + return ret; + + platform_set_drvdata(pdev, priv); + + return 0; +} + +static const struct of_device_id of_pwm_leds_match[] = { + { .compatible = "pwm-leds", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_pwm_leds_match); + +static struct platform_driver led_pwm_driver = { + .probe = led_pwm_probe, + .driver = { + .name = "leds_pwm", + .of_match_table = of_pwm_leds_match, + }, +}; + +module_platform_driver(led_pwm_driver); + +MODULE_AUTHOR("Luotao Fu <l.fu@pengutronix.de>"); +MODULE_DESCRIPTION("generic PWM LED driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("platform:leds-pwm"); diff --git a/drivers/leds/leds-rb532.c b/drivers/leds/leds-rb532.c new file mode 100644 index 000000000..b6447c172 --- /dev/null +++ b/drivers/leds/leds-rb532.c @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LEDs driver for the "User LED" on Routerboard532 + * + * Copyright (C) 2009 Phil Sutter <n0-1@freewrt.org> + * + * Based on leds-cobalt-qube.c by Florian Fainelly and + * rb-diag.c (my own standalone driver for both LED and + * button of Routerboard532). + */ + +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/platform_device.h> + +#include <asm/mach-rc32434/gpio.h> +#include <asm/mach-rc32434/rb.h> + +static void rb532_led_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + if (brightness) + set_latch_u5(LO_ULED, 0); + else + set_latch_u5(0, LO_ULED); +} + +static enum led_brightness rb532_led_get(struct led_classdev *cdev) +{ + return (get_latch_u5() & LO_ULED) ? LED_FULL : LED_OFF; +} + +static struct led_classdev rb532_uled = { + .name = "uled", + .brightness_set = rb532_led_set, + .brightness_get = rb532_led_get, + .default_trigger = "nand-disk", +}; + +static int rb532_led_probe(struct platform_device *pdev) +{ + return led_classdev_register(&pdev->dev, &rb532_uled); +} + +static int rb532_led_remove(struct platform_device *pdev) +{ + led_classdev_unregister(&rb532_uled); + return 0; +} + +static struct platform_driver rb532_led_driver = { + .probe = rb532_led_probe, + .remove = rb532_led_remove, + .driver = { + .name = "rb532-led", + }, +}; + +module_platform_driver(rb532_led_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("User LED support for Routerboard532"); +MODULE_AUTHOR("Phil Sutter <n0-1@freewrt.org>"); +MODULE_ALIAS("platform:rb532-led"); diff --git a/drivers/leds/leds-regulator.c b/drivers/leds/leds-regulator.c new file mode 100644 index 000000000..208c98918 --- /dev/null +++ b/drivers/leds/leds-regulator.c @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * leds-regulator.c - LED class driver for regulator driven LEDs. + * + * Copyright (C) 2009 Antonio Ospite <ospite@studenti.unina.it> + * + * Inspired by leds-wm8350 driver. + */ + +#include <linux/module.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/leds.h> +#include <linux/leds-regulator.h> +#include <linux/platform_device.h> +#include <linux/regulator/consumer.h> + +#define to_regulator_led(led_cdev) \ + container_of(led_cdev, struct regulator_led, cdev) + +struct regulator_led { + struct led_classdev cdev; + int enabled; + struct mutex mutex; + + struct regulator *vcc; +}; + +static inline int led_regulator_get_max_brightness(struct regulator *supply) +{ + int ret; + int voltage = regulator_list_voltage(supply, 0); + + if (voltage <= 0) + return 1; + + /* even if regulator can't change voltages, + * we still assume it can change status + * and the LED can be turned on and off. + */ + ret = regulator_set_voltage(supply, voltage, voltage); + if (ret < 0) + return 1; + + return regulator_count_voltages(supply); +} + +static int led_regulator_get_voltage(struct regulator *supply, + enum led_brightness brightness) +{ + if (brightness == 0) + return -EINVAL; + + return regulator_list_voltage(supply, brightness - 1); +} + + +static void regulator_led_enable(struct regulator_led *led) +{ + int ret; + + if (led->enabled) + return; + + ret = regulator_enable(led->vcc); + if (ret != 0) { + dev_err(led->cdev.dev, "Failed to enable vcc: %d\n", ret); + return; + } + + led->enabled = 1; +} + +static void regulator_led_disable(struct regulator_led *led) +{ + int ret; + + if (!led->enabled) + return; + + ret = regulator_disable(led->vcc); + if (ret != 0) { + dev_err(led->cdev.dev, "Failed to disable vcc: %d\n", ret); + return; + } + + led->enabled = 0; +} + +static int regulator_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct regulator_led *led = to_regulator_led(led_cdev); + int voltage; + int ret = 0; + + mutex_lock(&led->mutex); + + if (value == LED_OFF) { + regulator_led_disable(led); + goto out; + } + + if (led->cdev.max_brightness > 1) { + voltage = led_regulator_get_voltage(led->vcc, value); + dev_dbg(led->cdev.dev, "brightness: %d voltage: %d\n", + value, voltage); + + ret = regulator_set_voltage(led->vcc, voltage, voltage); + if (ret != 0) + dev_err(led->cdev.dev, "Failed to set voltage %d: %d\n", + voltage, ret); + } + + regulator_led_enable(led); + +out: + mutex_unlock(&led->mutex); + return ret; +} + +static int regulator_led_probe(struct platform_device *pdev) +{ + struct led_regulator_platform_data *pdata = + dev_get_platdata(&pdev->dev); + struct regulator_led *led; + struct regulator *vcc; + int ret = 0; + + if (pdata == NULL) { + dev_err(&pdev->dev, "no platform data\n"); + return -ENODEV; + } + + vcc = devm_regulator_get_exclusive(&pdev->dev, "vled"); + if (IS_ERR(vcc)) { + dev_err(&pdev->dev, "Cannot get vcc for %s\n", pdata->name); + return PTR_ERR(vcc); + } + + led = devm_kzalloc(&pdev->dev, sizeof(*led), GFP_KERNEL); + if (led == NULL) + return -ENOMEM; + + led->cdev.max_brightness = led_regulator_get_max_brightness(vcc); + if (pdata->brightness > led->cdev.max_brightness) { + dev_err(&pdev->dev, "Invalid default brightness %d\n", + pdata->brightness); + return -EINVAL; + } + + led->cdev.brightness_set_blocking = regulator_led_brightness_set; + led->cdev.name = pdata->name; + led->cdev.flags |= LED_CORE_SUSPENDRESUME; + led->vcc = vcc; + + /* to handle correctly an already enabled regulator */ + if (regulator_is_enabled(led->vcc)) + led->enabled = 1; + + mutex_init(&led->mutex); + + platform_set_drvdata(pdev, led); + + ret = led_classdev_register(&pdev->dev, &led->cdev); + if (ret < 0) + return ret; + + /* to expose the default value to userspace */ + led->cdev.brightness = pdata->brightness; + + /* Set the default led status */ + regulator_led_brightness_set(&led->cdev, led->cdev.brightness); + + return 0; +} + +static int regulator_led_remove(struct platform_device *pdev) +{ + struct regulator_led *led = platform_get_drvdata(pdev); + + led_classdev_unregister(&led->cdev); + regulator_led_disable(led); + return 0; +} + +static struct platform_driver regulator_led_driver = { + .driver = { + .name = "leds-regulator", + }, + .probe = regulator_led_probe, + .remove = regulator_led_remove, +}; + +module_platform_driver(regulator_led_driver); + +MODULE_AUTHOR("Antonio Ospite <ospite@studenti.unina.it>"); +MODULE_DESCRIPTION("Regulator driven LED driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:leds-regulator"); diff --git a/drivers/leds/leds-s3c24xx.c b/drivers/leds/leds-s3c24xx.c new file mode 100644 index 000000000..3c0c7aa63 --- /dev/null +++ b/drivers/leds/leds-s3c24xx.c @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* drivers/leds/leds-s3c24xx.c + * + * (c) 2006 Simtec Electronics + * http://armlinux.simtec.co.uk/ + * Ben Dooks <ben@simtec.co.uk> + * + * S3C24XX - LEDs GPIO driver +*/ + +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/gpio/consumer.h> +#include <linux/slab.h> +#include <linux/module.h> +#include <linux/platform_data/leds-s3c24xx.h> + +/* our context */ + +struct s3c24xx_gpio_led { + struct led_classdev cdev; + struct s3c24xx_led_platdata *pdata; + struct gpio_desc *gpiod; +}; + +static inline struct s3c24xx_gpio_led *to_gpio(struct led_classdev *led_cdev) +{ + return container_of(led_cdev, struct s3c24xx_gpio_led, cdev); +} + +static void s3c24xx_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct s3c24xx_gpio_led *led = to_gpio(led_cdev); + + gpiod_set_value(led->gpiod, !!value); +} + +static int s3c24xx_led_probe(struct platform_device *dev) +{ + struct s3c24xx_led_platdata *pdata = dev_get_platdata(&dev->dev); + struct s3c24xx_gpio_led *led; + int ret; + + led = devm_kzalloc(&dev->dev, sizeof(struct s3c24xx_gpio_led), + GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->cdev.brightness_set = s3c24xx_led_set; + led->cdev.default_trigger = pdata->def_trigger; + led->cdev.name = pdata->name; + led->cdev.flags |= LED_CORE_SUSPENDRESUME; + + led->pdata = pdata; + + /* Default to off */ + led->gpiod = devm_gpiod_get(&dev->dev, NULL, GPIOD_OUT_LOW); + if (IS_ERR(led->gpiod)) + return PTR_ERR(led->gpiod); + + /* register our new led device */ + ret = devm_led_classdev_register(&dev->dev, &led->cdev); + if (ret < 0) + dev_err(&dev->dev, "led_classdev_register failed\n"); + + return ret; +} + +static struct platform_driver s3c24xx_led_driver = { + .probe = s3c24xx_led_probe, + .driver = { + .name = "s3c24xx_led", + }, +}; + +module_platform_driver(s3c24xx_led_driver); + +MODULE_AUTHOR("Ben Dooks <ben@simtec.co.uk>"); +MODULE_DESCRIPTION("S3C24XX LED driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:s3c24xx_led"); diff --git a/drivers/leds/leds-sc27xx-bltc.c b/drivers/leds/leds-sc27xx-bltc.c new file mode 100644 index 000000000..e199ea15e --- /dev/null +++ b/drivers/leds/leds-sc27xx-bltc.c @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2018 Spreadtrum Communications Inc. + +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/regmap.h> + +/* PMIC global control register definition */ +#define SC27XX_MODULE_EN0 0xc08 +#define SC27XX_CLK_EN0 0xc18 +#define SC27XX_RGB_CTRL 0xebc + +#define SC27XX_BLTC_EN BIT(9) +#define SC27XX_RTC_EN BIT(7) +#define SC27XX_RGB_PD BIT(0) + +/* Breathing light controller register definition */ +#define SC27XX_LEDS_CTRL 0x00 +#define SC27XX_LEDS_PRESCALE 0x04 +#define SC27XX_LEDS_DUTY 0x08 +#define SC27XX_LEDS_CURVE0 0x0c +#define SC27XX_LEDS_CURVE1 0x10 + +#define SC27XX_CTRL_SHIFT 4 +#define SC27XX_LED_RUN BIT(0) +#define SC27XX_LED_TYPE BIT(1) + +#define SC27XX_DUTY_SHIFT 8 +#define SC27XX_DUTY_MASK GENMASK(15, 0) +#define SC27XX_MOD_MASK GENMASK(7, 0) + +#define SC27XX_CURVE_SHIFT 8 +#define SC27XX_CURVE_L_MASK GENMASK(7, 0) +#define SC27XX_CURVE_H_MASK GENMASK(15, 8) + +#define SC27XX_LEDS_OFFSET 0x10 +#define SC27XX_LEDS_MAX 3 +#define SC27XX_LEDS_PATTERN_CNT 4 +/* Stage duration step, in milliseconds */ +#define SC27XX_LEDS_STEP 125 +/* Minimum and maximum duration, in milliseconds */ +#define SC27XX_DELTA_T_MIN SC27XX_LEDS_STEP +#define SC27XX_DELTA_T_MAX (SC27XX_LEDS_STEP * 255) + +struct sc27xx_led { + struct fwnode_handle *fwnode; + struct led_classdev ldev; + struct sc27xx_led_priv *priv; + u8 line; + bool active; +}; + +struct sc27xx_led_priv { + struct sc27xx_led leds[SC27XX_LEDS_MAX]; + struct regmap *regmap; + struct mutex lock; + u32 base; +}; + +#define to_sc27xx_led(ldev) \ + container_of(ldev, struct sc27xx_led, ldev) + +static int sc27xx_led_init(struct regmap *regmap) +{ + int err; + + err = regmap_update_bits(regmap, SC27XX_MODULE_EN0, SC27XX_BLTC_EN, + SC27XX_BLTC_EN); + if (err) + return err; + + err = regmap_update_bits(regmap, SC27XX_CLK_EN0, SC27XX_RTC_EN, + SC27XX_RTC_EN); + if (err) + return err; + + return regmap_update_bits(regmap, SC27XX_RGB_CTRL, SC27XX_RGB_PD, 0); +} + +static u32 sc27xx_led_get_offset(struct sc27xx_led *leds) +{ + return leds->priv->base + SC27XX_LEDS_OFFSET * leds->line; +} + +static int sc27xx_led_enable(struct sc27xx_led *leds, enum led_brightness value) +{ + u32 base = sc27xx_led_get_offset(leds); + u32 ctrl_base = leds->priv->base + SC27XX_LEDS_CTRL; + u8 ctrl_shift = SC27XX_CTRL_SHIFT * leds->line; + struct regmap *regmap = leds->priv->regmap; + int err; + + err = regmap_update_bits(regmap, base + SC27XX_LEDS_DUTY, + SC27XX_DUTY_MASK, + (value << SC27XX_DUTY_SHIFT) | + SC27XX_MOD_MASK); + if (err) + return err; + + return regmap_update_bits(regmap, ctrl_base, + (SC27XX_LED_RUN | SC27XX_LED_TYPE) << ctrl_shift, + (SC27XX_LED_RUN | SC27XX_LED_TYPE) << ctrl_shift); +} + +static int sc27xx_led_disable(struct sc27xx_led *leds) +{ + struct regmap *regmap = leds->priv->regmap; + u32 ctrl_base = leds->priv->base + SC27XX_LEDS_CTRL; + u8 ctrl_shift = SC27XX_CTRL_SHIFT * leds->line; + + return regmap_update_bits(regmap, ctrl_base, + (SC27XX_LED_RUN | SC27XX_LED_TYPE) << ctrl_shift, 0); +} + +static int sc27xx_led_set(struct led_classdev *ldev, enum led_brightness value) +{ + struct sc27xx_led *leds = to_sc27xx_led(ldev); + int err; + + mutex_lock(&leds->priv->lock); + + if (value == LED_OFF) + err = sc27xx_led_disable(leds); + else + err = sc27xx_led_enable(leds, value); + + mutex_unlock(&leds->priv->lock); + + return err; +} + +static void sc27xx_led_clamp_align_delta_t(u32 *delta_t) +{ + u32 v, offset, t = *delta_t; + + v = t + SC27XX_LEDS_STEP / 2; + v = clamp_t(u32, v, SC27XX_DELTA_T_MIN, SC27XX_DELTA_T_MAX); + offset = v - SC27XX_DELTA_T_MIN; + offset = SC27XX_LEDS_STEP * (offset / SC27XX_LEDS_STEP); + + *delta_t = SC27XX_DELTA_T_MIN + offset; +} + +static int sc27xx_led_pattern_clear(struct led_classdev *ldev) +{ + struct sc27xx_led *leds = to_sc27xx_led(ldev); + struct regmap *regmap = leds->priv->regmap; + u32 base = sc27xx_led_get_offset(leds); + u32 ctrl_base = leds->priv->base + SC27XX_LEDS_CTRL; + u8 ctrl_shift = SC27XX_CTRL_SHIFT * leds->line; + int err; + + mutex_lock(&leds->priv->lock); + + /* Reset the rise, high, fall and low time to zero. */ + regmap_write(regmap, base + SC27XX_LEDS_CURVE0, 0); + regmap_write(regmap, base + SC27XX_LEDS_CURVE1, 0); + + err = regmap_update_bits(regmap, ctrl_base, + (SC27XX_LED_RUN | SC27XX_LED_TYPE) << ctrl_shift, 0); + + ldev->brightness = LED_OFF; + + mutex_unlock(&leds->priv->lock); + + return err; +} + +static int sc27xx_led_pattern_set(struct led_classdev *ldev, + struct led_pattern *pattern, + u32 len, int repeat) +{ + struct sc27xx_led *leds = to_sc27xx_led(ldev); + u32 base = sc27xx_led_get_offset(leds); + u32 ctrl_base = leds->priv->base + SC27XX_LEDS_CTRL; + u8 ctrl_shift = SC27XX_CTRL_SHIFT * leds->line; + struct regmap *regmap = leds->priv->regmap; + int err; + + /* + * Must contain 4 tuples to configure the rise time, high time, fall + * time and low time to enable the breathing mode. + */ + if (len != SC27XX_LEDS_PATTERN_CNT) + return -EINVAL; + + mutex_lock(&leds->priv->lock); + + sc27xx_led_clamp_align_delta_t(&pattern[0].delta_t); + err = regmap_update_bits(regmap, base + SC27XX_LEDS_CURVE0, + SC27XX_CURVE_L_MASK, + pattern[0].delta_t / SC27XX_LEDS_STEP); + if (err) + goto out; + + sc27xx_led_clamp_align_delta_t(&pattern[1].delta_t); + err = regmap_update_bits(regmap, base + SC27XX_LEDS_CURVE1, + SC27XX_CURVE_L_MASK, + pattern[1].delta_t / SC27XX_LEDS_STEP); + if (err) + goto out; + + sc27xx_led_clamp_align_delta_t(&pattern[2].delta_t); + err = regmap_update_bits(regmap, base + SC27XX_LEDS_CURVE0, + SC27XX_CURVE_H_MASK, + (pattern[2].delta_t / SC27XX_LEDS_STEP) << + SC27XX_CURVE_SHIFT); + if (err) + goto out; + + sc27xx_led_clamp_align_delta_t(&pattern[3].delta_t); + err = regmap_update_bits(regmap, base + SC27XX_LEDS_CURVE1, + SC27XX_CURVE_H_MASK, + (pattern[3].delta_t / SC27XX_LEDS_STEP) << + SC27XX_CURVE_SHIFT); + if (err) + goto out; + + err = regmap_update_bits(regmap, base + SC27XX_LEDS_DUTY, + SC27XX_DUTY_MASK, + (pattern[1].brightness << SC27XX_DUTY_SHIFT) | + SC27XX_MOD_MASK); + if (err) + goto out; + + /* Enable the LED breathing mode */ + err = regmap_update_bits(regmap, ctrl_base, + SC27XX_LED_RUN << ctrl_shift, + SC27XX_LED_RUN << ctrl_shift); + if (!err) + ldev->brightness = pattern[1].brightness; + +out: + mutex_unlock(&leds->priv->lock); + + return err; +} + +static int sc27xx_led_register(struct device *dev, struct sc27xx_led_priv *priv) +{ + int i, err; + + err = sc27xx_led_init(priv->regmap); + if (err) + return err; + + for (i = 0; i < SC27XX_LEDS_MAX; i++) { + struct sc27xx_led *led = &priv->leds[i]; + struct led_init_data init_data = {}; + + if (!led->active) + continue; + + led->line = i; + led->priv = priv; + led->ldev.brightness_set_blocking = sc27xx_led_set; + led->ldev.pattern_set = sc27xx_led_pattern_set; + led->ldev.pattern_clear = sc27xx_led_pattern_clear; + led->ldev.default_trigger = "pattern"; + + init_data.fwnode = led->fwnode; + init_data.devicename = "sc27xx"; + init_data.default_label = ":"; + + err = devm_led_classdev_register_ext(dev, &led->ldev, + &init_data); + if (err) + return err; + } + + return 0; +} + +static int sc27xx_led_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *np = dev_of_node(dev), *child; + struct sc27xx_led_priv *priv; + u32 base, count, reg; + int err; + + count = of_get_available_child_count(np); + if (!count || count > SC27XX_LEDS_MAX) + return -EINVAL; + + err = of_property_read_u32(np, "reg", &base); + if (err) { + dev_err(dev, "fail to get reg of property\n"); + return err; + } + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + platform_set_drvdata(pdev, priv); + mutex_init(&priv->lock); + priv->base = base; + priv->regmap = dev_get_regmap(dev->parent, NULL); + if (!priv->regmap) { + err = -ENODEV; + dev_err(dev, "failed to get regmap: %d\n", err); + return err; + } + + for_each_available_child_of_node(np, child) { + err = of_property_read_u32(child, "reg", ®); + if (err) { + of_node_put(child); + mutex_destroy(&priv->lock); + return err; + } + + if (reg >= SC27XX_LEDS_MAX || priv->leds[reg].active) { + of_node_put(child); + mutex_destroy(&priv->lock); + return -EINVAL; + } + + priv->leds[reg].fwnode = of_fwnode_handle(child); + priv->leds[reg].active = true; + } + + err = sc27xx_led_register(dev, priv); + if (err) + mutex_destroy(&priv->lock); + + return err; +} + +static int sc27xx_led_remove(struct platform_device *pdev) +{ + struct sc27xx_led_priv *priv = platform_get_drvdata(pdev); + + mutex_destroy(&priv->lock); + return 0; +} + +static const struct of_device_id sc27xx_led_of_match[] = { + { .compatible = "sprd,sc2731-bltc", }, + { } +}; +MODULE_DEVICE_TABLE(of, sc27xx_led_of_match); + +static struct platform_driver sc27xx_led_driver = { + .driver = { + .name = "sprd-bltc", + .of_match_table = sc27xx_led_of_match, + }, + .probe = sc27xx_led_probe, + .remove = sc27xx_led_remove, +}; + +module_platform_driver(sc27xx_led_driver); + +MODULE_DESCRIPTION("Spreadtrum SC27xx breathing light controller driver"); +MODULE_AUTHOR("Xiaotong Lu <xiaotong.lu@spreadtrum.com>"); +MODULE_AUTHOR("Baolin Wang <baolin.wang@linaro.org>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-sgm3140.c b/drivers/leds/leds-sgm3140.c new file mode 100644 index 000000000..f4f831570 --- /dev/null +++ b/drivers/leds/leds-sgm3140.c @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2020 Luca Weiss <luca@z3ntu.xyz> + +#include <linux/gpio/consumer.h> +#include <linux/led-class-flash.h> +#include <linux/module.h> +#include <linux/regulator/consumer.h> +#include <linux/platform_device.h> + +#include <media/v4l2-flash-led-class.h> + +#define FLASH_TIMEOUT_DEFAULT 250000U /* 250ms */ +#define FLASH_MAX_TIMEOUT_DEFAULT 300000U /* 300ms */ + +struct sgm3140 { + struct led_classdev_flash fled_cdev; + struct v4l2_flash *v4l2_flash; + + struct timer_list powerdown_timer; + + struct gpio_desc *flash_gpio; + struct gpio_desc *enable_gpio; + struct regulator *vin_regulator; + + bool enabled; + + /* current timeout in us */ + u32 timeout; + /* maximum timeout in us */ + u32 max_timeout; +}; + +static struct sgm3140 *flcdev_to_sgm3140(struct led_classdev_flash *flcdev) +{ + return container_of(flcdev, struct sgm3140, fled_cdev); +} + +static int sgm3140_strobe_set(struct led_classdev_flash *fled_cdev, bool state) +{ + struct sgm3140 *priv = flcdev_to_sgm3140(fled_cdev); + int ret; + + if (priv->enabled == state) + return 0; + + if (state) { + ret = regulator_enable(priv->vin_regulator); + if (ret) { + dev_err(fled_cdev->led_cdev.dev, + "failed to enable regulator: %d\n", ret); + return ret; + } + gpiod_set_value_cansleep(priv->flash_gpio, 1); + gpiod_set_value_cansleep(priv->enable_gpio, 1); + mod_timer(&priv->powerdown_timer, + jiffies + usecs_to_jiffies(priv->timeout)); + } else { + del_timer_sync(&priv->powerdown_timer); + gpiod_set_value_cansleep(priv->enable_gpio, 0); + gpiod_set_value_cansleep(priv->flash_gpio, 0); + ret = regulator_disable(priv->vin_regulator); + if (ret) { + dev_err(fled_cdev->led_cdev.dev, + "failed to disable regulator: %d\n", ret); + return ret; + } + } + + priv->enabled = state; + + return 0; +} + +static int sgm3140_strobe_get(struct led_classdev_flash *fled_cdev, bool *state) +{ + struct sgm3140 *priv = flcdev_to_sgm3140(fled_cdev); + + *state = timer_pending(&priv->powerdown_timer); + + return 0; +} + +static int sgm3140_timeout_set(struct led_classdev_flash *fled_cdev, + u32 timeout) +{ + struct sgm3140 *priv = flcdev_to_sgm3140(fled_cdev); + + priv->timeout = timeout; + + return 0; +} + +static const struct led_flash_ops sgm3140_flash_ops = { + .strobe_set = sgm3140_strobe_set, + .strobe_get = sgm3140_strobe_get, + .timeout_set = sgm3140_timeout_set, +}; + +static int sgm3140_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); + struct sgm3140 *priv = flcdev_to_sgm3140(fled_cdev); + bool enable = brightness == LED_ON; + int ret; + + if (priv->enabled == enable) + return 0; + + if (enable) { + ret = regulator_enable(priv->vin_regulator); + if (ret) { + dev_err(led_cdev->dev, + "failed to enable regulator: %d\n", ret); + return ret; + } + gpiod_set_value_cansleep(priv->enable_gpio, 1); + } else { + gpiod_set_value_cansleep(priv->enable_gpio, 0); + ret = regulator_disable(priv->vin_regulator); + if (ret) { + dev_err(led_cdev->dev, + "failed to disable regulator: %d\n", ret); + return ret; + } + } + + priv->enabled = enable; + + return 0; +} + +static void sgm3140_powerdown_timer(struct timer_list *t) +{ + struct sgm3140 *priv = from_timer(priv, t, powerdown_timer); + + gpiod_set_value(priv->enable_gpio, 0); + gpiod_set_value(priv->flash_gpio, 0); + regulator_disable(priv->vin_regulator); + + priv->enabled = false; +} + +static void sgm3140_init_flash_timeout(struct sgm3140 *priv) +{ + struct led_classdev_flash *fled_cdev = &priv->fled_cdev; + struct led_flash_setting *s; + + /* Init flash timeout setting */ + s = &fled_cdev->timeout; + s->min = 1; + s->max = priv->max_timeout; + s->step = 1; + s->val = FLASH_TIMEOUT_DEFAULT; +} + +#if IS_ENABLED(CONFIG_V4L2_FLASH_LED_CLASS) +static void sgm3140_init_v4l2_flash_config(struct sgm3140 *priv, + struct v4l2_flash_config *v4l2_sd_cfg) +{ + struct led_classdev *led_cdev = &priv->fled_cdev.led_cdev; + struct led_flash_setting *s; + + strscpy(v4l2_sd_cfg->dev_name, led_cdev->dev->kobj.name, + sizeof(v4l2_sd_cfg->dev_name)); + + /* Init flash intensity setting */ + s = &v4l2_sd_cfg->intensity; + s->min = 0; + s->max = 1; + s->step = 1; + s->val = 1; +} + +#else +static void sgm3140_init_v4l2_flash_config(struct sgm3140 *priv, + struct v4l2_flash_config *v4l2_sd_cfg) +{ +} +#endif + +static int sgm3140_probe(struct platform_device *pdev) +{ + struct sgm3140 *priv; + struct led_classdev *led_cdev; + struct led_classdev_flash *fled_cdev; + struct led_init_data init_data = {}; + struct fwnode_handle *child_node; + struct v4l2_flash_config v4l2_sd_cfg = {}; + int ret; + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->flash_gpio = devm_gpiod_get(&pdev->dev, "flash", GPIOD_OUT_LOW); + ret = PTR_ERR_OR_ZERO(priv->flash_gpio); + if (ret) + return dev_err_probe(&pdev->dev, ret, + "Failed to request flash gpio\n"); + + priv->enable_gpio = devm_gpiod_get(&pdev->dev, "enable", GPIOD_OUT_LOW); + ret = PTR_ERR_OR_ZERO(priv->enable_gpio); + if (ret) + return dev_err_probe(&pdev->dev, ret, + "Failed to request enable gpio\n"); + + priv->vin_regulator = devm_regulator_get(&pdev->dev, "vin"); + ret = PTR_ERR_OR_ZERO(priv->vin_regulator); + if (ret) + return dev_err_probe(&pdev->dev, ret, + "Failed to request regulator\n"); + + child_node = fwnode_get_next_available_child_node(pdev->dev.fwnode, + NULL); + if (!child_node) { + dev_err(&pdev->dev, + "No fwnode child node found for connected LED.\n"); + return -EINVAL; + } + + ret = fwnode_property_read_u32(child_node, "flash-max-timeout-us", + &priv->max_timeout); + if (ret) { + priv->max_timeout = FLASH_MAX_TIMEOUT_DEFAULT; + dev_warn(&pdev->dev, + "flash-max-timeout-us property missing\n"); + } + + /* + * Set default timeout to FLASH_DEFAULT_TIMEOUT except if max_timeout + * from DT is lower. + */ + priv->timeout = min(priv->max_timeout, FLASH_TIMEOUT_DEFAULT); + + timer_setup(&priv->powerdown_timer, sgm3140_powerdown_timer, 0); + + fled_cdev = &priv->fled_cdev; + led_cdev = &fled_cdev->led_cdev; + + fled_cdev->ops = &sgm3140_flash_ops; + + led_cdev->brightness_set_blocking = sgm3140_brightness_set; + led_cdev->max_brightness = LED_ON; + led_cdev->flags |= LED_DEV_CAP_FLASH; + + sgm3140_init_flash_timeout(priv); + + init_data.fwnode = child_node; + + platform_set_drvdata(pdev, priv); + + /* Register in the LED subsystem */ + ret = devm_led_classdev_flash_register_ext(&pdev->dev, + fled_cdev, &init_data); + if (ret) { + dev_err(&pdev->dev, "Failed to register flash device: %d\n", + ret); + goto err; + } + + sgm3140_init_v4l2_flash_config(priv, &v4l2_sd_cfg); + + /* Create V4L2 Flash subdev */ + priv->v4l2_flash = v4l2_flash_init(&pdev->dev, + child_node, + fled_cdev, NULL, + &v4l2_sd_cfg); + if (IS_ERR(priv->v4l2_flash)) { + ret = PTR_ERR(priv->v4l2_flash); + goto err; + } + + return ret; + +err: + fwnode_handle_put(child_node); + return ret; +} + +static int sgm3140_remove(struct platform_device *pdev) +{ + struct sgm3140 *priv = platform_get_drvdata(pdev); + + del_timer_sync(&priv->powerdown_timer); + + v4l2_flash_release(priv->v4l2_flash); + + return 0; +} + +static const struct of_device_id sgm3140_dt_match[] = { + { .compatible = "sgmicro,sgm3140" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, sgm3140_dt_match); + +static struct platform_driver sgm3140_driver = { + .probe = sgm3140_probe, + .remove = sgm3140_remove, + .driver = { + .name = "sgm3140", + .of_match_table = sgm3140_dt_match, + }, +}; + +module_platform_driver(sgm3140_driver); + +MODULE_AUTHOR("Luca Weiss <luca@z3ntu.xyz>"); +MODULE_DESCRIPTION("SG Micro SGM3140 charge pump LED driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-spi-byte.c b/drivers/leds/leds-spi-byte.c new file mode 100644 index 000000000..f1964c96f --- /dev/null +++ b/drivers/leds/leds-spi-byte.c @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2019 Christian Mauderer <oss@c-mauderer.de> + +/* + * The driver supports controllers with a very simple SPI protocol: + * - one LED is controlled by a single byte on MOSI + * - the value of the byte gives the brightness between two values (lowest to + * highest) + * - no return value is necessary (no MISO signal) + * + * The value for minimum and maximum brightness depends on the device + * (compatible string). + * + * Supported devices: + * - "ubnt,acb-spi-led": Microcontroller (SONiX 8F26E611LA) based device used + * for example in Ubiquiti airCube ISP. Reverse engineered protocol for this + * controller: + * * Higher two bits set a mode. Lower six bits are a parameter. + * * Mode: 00 -> set brightness between 0x00 (min) and 0x3F (max) + * * Mode: 01 -> pulsing pattern (min -> max -> min) with an interval. From + * some tests, the period is about (50ms + 102ms * parameter). There is a + * slightly different pattern starting from 0x10 (longer gap between the + * pulses) but the time still follows that calculation. + * * Mode: 10 -> same as 01 but with only a ramp from min to max. Again a + * slight jump in the pattern at 0x10. + * * Mode: 11 -> blinking (off -> 25% -> off -> 25% -> ...) with a period of + * (105ms * parameter) + * NOTE: This driver currently only supports mode 00. + */ + +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/spi/spi.h> +#include <linux/mutex.h> +#include <uapi/linux/uleds.h> + +struct spi_byte_chipdef { + /* SPI byte that will be send to switch the LED off */ + u8 off_value; + /* SPI byte that will be send to switch the LED to maximum brightness */ + u8 max_value; +}; + +struct spi_byte_led { + struct led_classdev ldev; + struct spi_device *spi; + char name[LED_MAX_NAME_SIZE]; + struct mutex mutex; + const struct spi_byte_chipdef *cdef; +}; + +static const struct spi_byte_chipdef ubnt_acb_spi_led_cdef = { + .off_value = 0x0, + .max_value = 0x3F, +}; + +static const struct of_device_id spi_byte_dt_ids[] = { + { .compatible = "ubnt,acb-spi-led", .data = &ubnt_acb_spi_led_cdef }, + {}, +}; + +MODULE_DEVICE_TABLE(of, spi_byte_dt_ids); + +static int spi_byte_brightness_set_blocking(struct led_classdev *dev, + enum led_brightness brightness) +{ + struct spi_byte_led *led = container_of(dev, struct spi_byte_led, ldev); + u8 value; + int ret; + + value = (u8) brightness + led->cdef->off_value; + + mutex_lock(&led->mutex); + ret = spi_write(led->spi, &value, sizeof(value)); + mutex_unlock(&led->mutex); + + return ret; +} + +static int spi_byte_probe(struct spi_device *spi) +{ + struct device_node *child; + struct device *dev = &spi->dev; + struct spi_byte_led *led; + const char *name = "leds-spi-byte::"; + const char *state; + int ret; + + if (of_get_available_child_count(dev_of_node(dev)) != 1) { + dev_err(dev, "Device must have exactly one LED sub-node."); + return -EINVAL; + } + child = of_get_next_available_child(dev_of_node(dev), NULL); + + led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + of_property_read_string(child, "label", &name); + strlcpy(led->name, name, sizeof(led->name)); + led->spi = spi; + mutex_init(&led->mutex); + led->cdef = device_get_match_data(dev); + led->ldev.name = led->name; + led->ldev.brightness = LED_OFF; + led->ldev.max_brightness = led->cdef->max_value - led->cdef->off_value; + led->ldev.brightness_set_blocking = spi_byte_brightness_set_blocking; + + state = of_get_property(child, "default-state", NULL); + if (state) { + if (!strcmp(state, "on")) { + led->ldev.brightness = led->ldev.max_brightness; + } else if (strcmp(state, "off")) { + /* all other cases except "off" */ + dev_err(dev, "default-state can only be 'on' or 'off'"); + return -EINVAL; + } + } + spi_byte_brightness_set_blocking(&led->ldev, + led->ldev.brightness); + + ret = devm_led_classdev_register(&spi->dev, &led->ldev); + if (ret) { + mutex_destroy(&led->mutex); + return ret; + } + spi_set_drvdata(spi, led); + + return 0; +} + +static int spi_byte_remove(struct spi_device *spi) +{ + struct spi_byte_led *led = spi_get_drvdata(spi); + + mutex_destroy(&led->mutex); + + return 0; +} + +static struct spi_driver spi_byte_driver = { + .probe = spi_byte_probe, + .remove = spi_byte_remove, + .driver = { + .name = KBUILD_MODNAME, + .of_match_table = spi_byte_dt_ids, + }, +}; + +module_spi_driver(spi_byte_driver); + +MODULE_AUTHOR("Christian Mauderer <oss@c-mauderer.de>"); +MODULE_DESCRIPTION("single byte SPI LED driver"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("spi:leds-spi-byte"); diff --git a/drivers/leds/leds-ss4200.c b/drivers/leds/leds-ss4200.c new file mode 100644 index 000000000..245de443f --- /dev/null +++ b/drivers/leds/leds-ss4200.c @@ -0,0 +1,561 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * SS4200-E Hardware API + * Copyright (c) 2009, Intel Corporation. + * Copyright IBM Corporation, 2009 + * + * Author: Dave Hansen <dave@sr71.net> + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/dmi.h> +#include <linux/init.h> +#include <linux/ioport.h> +#include <linux/kernel.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/pci.h> +#include <linux/types.h> +#include <linux/uaccess.h> + +MODULE_AUTHOR("Rodney Girod <rgirod@confocus.com>, Dave Hansen <dave@sr71.net>"); +MODULE_DESCRIPTION("Intel NAS/Home Server ICH7 GPIO Driver"); +MODULE_LICENSE("GPL"); + +/* + * ICH7 LPC/GPIO PCI Config register offsets + */ +#define PMBASE 0x040 +#define GPIO_BASE 0x048 +#define GPIO_CTRL 0x04c +#define GPIO_EN 0x010 + +/* + * The ICH7 GPIO register block is 64 bytes in size. + */ +#define ICH7_GPIO_SIZE 64 + +/* + * Define register offsets within the ICH7 register block. + */ +#define GPIO_USE_SEL 0x000 +#define GP_IO_SEL 0x004 +#define GP_LVL 0x00c +#define GPO_BLINK 0x018 +#define GPI_INV 0x030 +#define GPIO_USE_SEL2 0x034 +#define GP_IO_SEL2 0x038 +#define GP_LVL2 0x03c + +/* + * PCI ID of the Intel ICH7 LPC Device within which the GPIO block lives. + */ +static const struct pci_device_id ich7_lpc_pci_id[] = { + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ICH7_0) }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ICH7_1) }, + { PCI_DEVICE(PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_ICH7_30) }, + { } /* NULL entry */ +}; + +MODULE_DEVICE_TABLE(pci, ich7_lpc_pci_id); + +static int __init ss4200_led_dmi_callback(const struct dmi_system_id *id) +{ + pr_info("detected '%s'\n", id->ident); + return 1; +} + +static bool nodetect; +module_param_named(nodetect, nodetect, bool, 0); +MODULE_PARM_DESC(nodetect, "Skip DMI-based hardware detection"); + +/* + * struct nas_led_whitelist - List of known good models + * + * Contains the known good models this driver is compatible with. + * When adding a new model try to be as strict as possible. This + * makes it possible to keep the false positives (the model is + * detected as working, but in reality it is not) as low as + * possible. + */ +static const struct dmi_system_id nas_led_whitelist[] __initconst = { + { + .callback = ss4200_led_dmi_callback, + .ident = "Intel SS4200-E", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Intel"), + DMI_MATCH(DMI_PRODUCT_NAME, "SS4200-E"), + DMI_MATCH(DMI_PRODUCT_VERSION, "1.00.00") + } + }, + { + /* + * FUJITSU SIEMENS SCALEO Home Server/SS4200-E + * BIOS V090L 12/19/2007 + */ + .callback = ss4200_led_dmi_callback, + .ident = "Fujitsu Siemens SCALEO Home Server", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"), + DMI_MATCH(DMI_PRODUCT_NAME, "SCALEO Home Server"), + DMI_MATCH(DMI_PRODUCT_VERSION, "1.00.00") + } + }, + {} +}; + +/* + * Base I/O address assigned to the Power Management register block + */ +static u32 g_pm_io_base; + +/* + * Base I/O address assigned to the ICH7 GPIO register block + */ +static u32 nas_gpio_io_base; + +/* + * When we successfully register a region, we are returned a resource. + * We use these to identify which regions we need to release on our way + * back out. + */ +static struct resource *gp_gpio_resource; + +struct nasgpio_led { + char *name; + u32 gpio_bit; + struct led_classdev led_cdev; +}; + +/* + * gpio_bit(s) are the ICH7 GPIO bit assignments + */ +static struct nasgpio_led nasgpio_leds[] = { + { .name = "hdd1:blue:sata", .gpio_bit = 0 }, + { .name = "hdd1:amber:sata", .gpio_bit = 1 }, + { .name = "hdd2:blue:sata", .gpio_bit = 2 }, + { .name = "hdd2:amber:sata", .gpio_bit = 3 }, + { .name = "hdd3:blue:sata", .gpio_bit = 4 }, + { .name = "hdd3:amber:sata", .gpio_bit = 5 }, + { .name = "hdd4:blue:sata", .gpio_bit = 6 }, + { .name = "hdd4:amber:sata", .gpio_bit = 7 }, + { .name = "power:blue:power", .gpio_bit = 27}, + { .name = "power:amber:power", .gpio_bit = 28}, +}; + +#define NAS_RECOVERY 0x00000400 /* GPIO10 */ + +static struct nasgpio_led * +led_classdev_to_nasgpio_led(struct led_classdev *led_cdev) +{ + return container_of(led_cdev, struct nasgpio_led, led_cdev); +} + +static struct nasgpio_led *get_led_named(char *name) +{ + int i; + for (i = 0; i < ARRAY_SIZE(nasgpio_leds); i++) { + if (strcmp(nasgpio_leds[i].name, name)) + continue; + return &nasgpio_leds[i]; + } + return NULL; +} + +/* + * This protects access to the gpio ports. + */ +static DEFINE_SPINLOCK(nasgpio_gpio_lock); + +/* + * There are two gpio ports, one for blinking and the other + * for power. @port tells us if we're doing blinking or + * power control. + * + * Caller must hold nasgpio_gpio_lock + */ +static void __nasgpio_led_set_attr(struct led_classdev *led_cdev, + u32 port, u32 value) +{ + struct nasgpio_led *led = led_classdev_to_nasgpio_led(led_cdev); + u32 gpio_out; + + gpio_out = inl(nas_gpio_io_base + port); + if (value) + gpio_out |= (1<<led->gpio_bit); + else + gpio_out &= ~(1<<led->gpio_bit); + + outl(gpio_out, nas_gpio_io_base + port); +} + +static void nasgpio_led_set_attr(struct led_classdev *led_cdev, + u32 port, u32 value) +{ + spin_lock(&nasgpio_gpio_lock); + __nasgpio_led_set_attr(led_cdev, port, value); + spin_unlock(&nasgpio_gpio_lock); +} + +static u32 nasgpio_led_get_attr(struct led_classdev *led_cdev, u32 port) +{ + struct nasgpio_led *led = led_classdev_to_nasgpio_led(led_cdev); + u32 gpio_in; + + spin_lock(&nasgpio_gpio_lock); + gpio_in = inl(nas_gpio_io_base + port); + spin_unlock(&nasgpio_gpio_lock); + if (gpio_in & (1<<led->gpio_bit)) + return 1; + return 0; +} + +/* + * There is actual brightness control in the hardware, + * but it is via smbus commands and not implemented + * in this driver. + */ +static void nasgpio_led_set_brightness(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + u32 setting = 0; + if (brightness >= LED_HALF) + setting = 1; + /* + * Hold the lock across both operations. This ensures + * consistency so that both the "turn off blinking" + * and "turn light off" operations complete as a set. + */ + spin_lock(&nasgpio_gpio_lock); + /* + * LED class documentation asks that past blink state + * be disabled when brightness is turned to zero. + */ + if (brightness == 0) + __nasgpio_led_set_attr(led_cdev, GPO_BLINK, 0); + __nasgpio_led_set_attr(led_cdev, GP_LVL, setting); + spin_unlock(&nasgpio_gpio_lock); +} + +static int nasgpio_led_set_blink(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + u32 setting = 1; + if (!(*delay_on == 0 && *delay_off == 0) && + !(*delay_on == 500 && *delay_off == 500)) + return -EINVAL; + /* + * These are very approximate. + */ + *delay_on = 500; + *delay_off = 500; + + nasgpio_led_set_attr(led_cdev, GPO_BLINK, setting); + + return 0; +} + + +/* + * Initialize the ICH7 GPIO registers for NAS usage. The BIOS should have + * already taken care of this, but we will do so in a non destructive manner + * so that we have what we need whether the BIOS did it or not. + */ +static int ich7_gpio_init(struct device *dev) +{ + int i; + u32 config_data = 0; + u32 all_nas_led = 0; + + for (i = 0; i < ARRAY_SIZE(nasgpio_leds); i++) + all_nas_led |= (1<<nasgpio_leds[i].gpio_bit); + + spin_lock(&nasgpio_gpio_lock); + /* + * We need to enable all of the GPIO lines used by the NAS box, + * so we will read the current Use Selection and add our usage + * to it. This should be benign with regard to the original + * BIOS configuration. + */ + config_data = inl(nas_gpio_io_base + GPIO_USE_SEL); + dev_dbg(dev, ": Data read from GPIO_USE_SEL = 0x%08x\n", config_data); + config_data |= all_nas_led + NAS_RECOVERY; + outl(config_data, nas_gpio_io_base + GPIO_USE_SEL); + config_data = inl(nas_gpio_io_base + GPIO_USE_SEL); + dev_dbg(dev, ": GPIO_USE_SEL = 0x%08x\n\n", config_data); + + /* + * The LED GPIO outputs need to be configured for output, so we + * will ensure that all LED lines are cleared for output and the + * RECOVERY line ready for input. This too should be benign with + * regard to BIOS configuration. + */ + config_data = inl(nas_gpio_io_base + GP_IO_SEL); + dev_dbg(dev, ": Data read from GP_IO_SEL = 0x%08x\n", + config_data); + config_data &= ~all_nas_led; + config_data |= NAS_RECOVERY; + outl(config_data, nas_gpio_io_base + GP_IO_SEL); + config_data = inl(nas_gpio_io_base + GP_IO_SEL); + dev_dbg(dev, ": GP_IO_SEL = 0x%08x\n", config_data); + + /* + * In our final system, the BIOS will initialize the state of all + * of the LEDs. For now, we turn them all off (or Low). + */ + config_data = inl(nas_gpio_io_base + GP_LVL); + dev_dbg(dev, ": Data read from GP_LVL = 0x%08x\n", config_data); + /* + * In our final system, the BIOS will initialize the blink state of all + * of the LEDs. For now, we turn blink off for all of them. + */ + config_data = inl(nas_gpio_io_base + GPO_BLINK); + dev_dbg(dev, ": Data read from GPO_BLINK = 0x%08x\n", config_data); + + /* + * At this moment, I am unsure if anything needs to happen with GPI_INV + */ + config_data = inl(nas_gpio_io_base + GPI_INV); + dev_dbg(dev, ": Data read from GPI_INV = 0x%08x\n", config_data); + + spin_unlock(&nasgpio_gpio_lock); + return 0; +} + +static void ich7_lpc_cleanup(struct device *dev) +{ + /* + * If we were given exclusive use of the GPIO + * I/O Address range, we must return it. + */ + if (gp_gpio_resource) { + dev_dbg(dev, ": Releasing GPIO I/O addresses\n"); + release_region(nas_gpio_io_base, ICH7_GPIO_SIZE); + gp_gpio_resource = NULL; + } +} + +/* + * The OS has determined that the LPC of the Intel ICH7 Southbridge is present + * so we can retrive the required operational information and prepare the GPIO. + */ +static struct pci_dev *nas_gpio_pci_dev; +static int ich7_lpc_probe(struct pci_dev *dev, + const struct pci_device_id *id) +{ + int status; + u32 gc = 0; + + status = pci_enable_device(dev); + if (status) { + dev_err(&dev->dev, "pci_enable_device failed\n"); + return -EIO; + } + + nas_gpio_pci_dev = dev; + status = pci_read_config_dword(dev, PMBASE, &g_pm_io_base); + if (status) + goto out; + g_pm_io_base &= 0x00000ff80; + + status = pci_read_config_dword(dev, GPIO_CTRL, &gc); + if (!(GPIO_EN & gc)) { + status = -EEXIST; + dev_info(&dev->dev, + "ERROR: The LPC GPIO Block has not been enabled.\n"); + goto out; + } + + status = pci_read_config_dword(dev, GPIO_BASE, &nas_gpio_io_base); + if (0 > status) { + dev_info(&dev->dev, "Unable to read GPIOBASE.\n"); + goto out; + } + dev_dbg(&dev->dev, ": GPIOBASE = 0x%08x\n", nas_gpio_io_base); + nas_gpio_io_base &= 0x00000ffc0; + + /* + * Insure that we have exclusive access to the GPIO I/O address range. + */ + gp_gpio_resource = request_region(nas_gpio_io_base, ICH7_GPIO_SIZE, + KBUILD_MODNAME); + if (NULL == gp_gpio_resource) { + dev_info(&dev->dev, + "ERROR Unable to register GPIO I/O addresses.\n"); + status = -1; + goto out; + } + + /* + * Initialize the GPIO for NAS/Home Server Use + */ + ich7_gpio_init(&dev->dev); + +out: + if (status) { + ich7_lpc_cleanup(&dev->dev); + pci_disable_device(dev); + } + return status; +} + +static void ich7_lpc_remove(struct pci_dev *dev) +{ + ich7_lpc_cleanup(&dev->dev); + pci_disable_device(dev); +} + +/* + * pci_driver structure passed to the PCI modules + */ +static struct pci_driver nas_gpio_pci_driver = { + .name = KBUILD_MODNAME, + .id_table = ich7_lpc_pci_id, + .probe = ich7_lpc_probe, + .remove = ich7_lpc_remove, +}; + +static struct led_classdev *get_classdev_for_led_nr(int nr) +{ + struct nasgpio_led *nas_led = &nasgpio_leds[nr]; + struct led_classdev *led = &nas_led->led_cdev; + return led; +} + + +static void set_power_light_amber_noblink(void) +{ + struct nasgpio_led *amber = get_led_named("power:amber:power"); + struct nasgpio_led *blue = get_led_named("power:blue:power"); + + if (!amber || !blue) + return; + /* + * LED_OFF implies disabling future blinking + */ + pr_debug("setting blue off and amber on\n"); + + nasgpio_led_set_brightness(&blue->led_cdev, LED_OFF); + nasgpio_led_set_brightness(&amber->led_cdev, LED_FULL); +} + +static ssize_t nas_led_blink_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led = dev_get_drvdata(dev); + int blinking = 0; + if (nasgpio_led_get_attr(led, GPO_BLINK)) + blinking = 1; + return sprintf(buf, "%u\n", blinking); +} + +static ssize_t nas_led_blink_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + int ret; + struct led_classdev *led = dev_get_drvdata(dev); + unsigned long blink_state; + + ret = kstrtoul(buf, 10, &blink_state); + if (ret) + return ret; + + nasgpio_led_set_attr(led, GPO_BLINK, blink_state); + + return size; +} + +static DEVICE_ATTR(blink, 0644, nas_led_blink_show, nas_led_blink_store); + +static struct attribute *nasgpio_led_attrs[] = { + &dev_attr_blink.attr, + NULL +}; +ATTRIBUTE_GROUPS(nasgpio_led); + +static int register_nasgpio_led(int led_nr) +{ + int ret; + struct nasgpio_led *nas_led = &nasgpio_leds[led_nr]; + struct led_classdev *led = get_classdev_for_led_nr(led_nr); + + led->name = nas_led->name; + led->brightness = LED_OFF; + if (nasgpio_led_get_attr(led, GP_LVL)) + led->brightness = LED_FULL; + led->brightness_set = nasgpio_led_set_brightness; + led->blink_set = nasgpio_led_set_blink; + led->groups = nasgpio_led_groups; + ret = led_classdev_register(&nas_gpio_pci_dev->dev, led); + if (ret) + return ret; + + return 0; +} + +static void unregister_nasgpio_led(int led_nr) +{ + struct led_classdev *led = get_classdev_for_led_nr(led_nr); + led_classdev_unregister(led); +} +/* + * module load/initialization + */ +static int __init nas_gpio_init(void) +{ + int i; + int ret = 0; + int nr_devices = 0; + + nr_devices = dmi_check_system(nas_led_whitelist); + if (nodetect) { + pr_info("skipping hardware autodetection\n"); + pr_info("Please send 'dmidecode' output to dave@sr71.net\n"); + nr_devices++; + } + + if (nr_devices <= 0) { + pr_info("no LED devices found\n"); + return -ENODEV; + } + + pr_info("registering PCI driver\n"); + ret = pci_register_driver(&nas_gpio_pci_driver); + if (ret) + return ret; + for (i = 0; i < ARRAY_SIZE(nasgpio_leds); i++) { + ret = register_nasgpio_led(i); + if (ret) + goto out_err; + } + /* + * When the system powers on, the BIOS leaves the power + * light blue and blinking. This will turn it solid + * amber once the driver is loaded. + */ + set_power_light_amber_noblink(); + return 0; +out_err: + for (i--; i >= 0; i--) + unregister_nasgpio_led(i); + pci_unregister_driver(&nas_gpio_pci_driver); + return ret; +} + +/* + * module unload + */ +static void __exit nas_gpio_exit(void) +{ + int i; + pr_info("Unregistering driver\n"); + for (i = 0; i < ARRAY_SIZE(nasgpio_leds); i++) + unregister_nasgpio_led(i); + pci_unregister_driver(&nas_gpio_pci_driver); +} + +module_init(nas_gpio_init); +module_exit(nas_gpio_exit); diff --git a/drivers/leds/leds-sunfire.c b/drivers/leds/leds-sunfire.c new file mode 100644 index 000000000..eba731371 --- /dev/null +++ b/drivers/leds/leds-sunfire.c @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* leds-sunfire.c: SUNW,Ultra-Enterprise LED driver. + * + * Copyright (C) 2008 David S. Miller <davem@davemloft.net> + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/leds.h> +#include <linux/io.h> +#include <linux/platform_device.h> +#include <linux/slab.h> + +#include <asm/fhc.h> +#include <asm/upa.h> + +MODULE_AUTHOR("David S. Miller (davem@davemloft.net)"); +MODULE_DESCRIPTION("Sun Fire LED driver"); +MODULE_LICENSE("GPL"); + +struct sunfire_led { + struct led_classdev led_cdev; + void __iomem *reg; +}; +#define to_sunfire_led(d) container_of(d, struct sunfire_led, led_cdev) + +static void __clockboard_set(struct led_classdev *led_cdev, + enum led_brightness led_val, u8 bit) +{ + struct sunfire_led *p = to_sunfire_led(led_cdev); + u8 reg = upa_readb(p->reg); + + switch (bit) { + case CLOCK_CTRL_LLED: + if (led_val) + reg &= ~bit; + else + reg |= bit; + break; + + default: + if (led_val) + reg |= bit; + else + reg &= ~bit; + break; + } + upa_writeb(reg, p->reg); +} + +static void clockboard_left_set(struct led_classdev *led_cdev, + enum led_brightness led_val) +{ + __clockboard_set(led_cdev, led_val, CLOCK_CTRL_LLED); +} + +static void clockboard_middle_set(struct led_classdev *led_cdev, + enum led_brightness led_val) +{ + __clockboard_set(led_cdev, led_val, CLOCK_CTRL_MLED); +} + +static void clockboard_right_set(struct led_classdev *led_cdev, + enum led_brightness led_val) +{ + __clockboard_set(led_cdev, led_val, CLOCK_CTRL_RLED); +} + +static void __fhc_set(struct led_classdev *led_cdev, + enum led_brightness led_val, u32 bit) +{ + struct sunfire_led *p = to_sunfire_led(led_cdev); + u32 reg = upa_readl(p->reg); + + switch (bit) { + case FHC_CONTROL_LLED: + if (led_val) + reg &= ~bit; + else + reg |= bit; + break; + + default: + if (led_val) + reg |= bit; + else + reg &= ~bit; + break; + } + upa_writel(reg, p->reg); +} + +static void fhc_left_set(struct led_classdev *led_cdev, + enum led_brightness led_val) +{ + __fhc_set(led_cdev, led_val, FHC_CONTROL_LLED); +} + +static void fhc_middle_set(struct led_classdev *led_cdev, + enum led_brightness led_val) +{ + __fhc_set(led_cdev, led_val, FHC_CONTROL_MLED); +} + +static void fhc_right_set(struct led_classdev *led_cdev, + enum led_brightness led_val) +{ + __fhc_set(led_cdev, led_val, FHC_CONTROL_RLED); +} + +typedef void (*set_handler)(struct led_classdev *, enum led_brightness); +struct led_type { + const char *name; + set_handler handler; + const char *default_trigger; +}; + +#define NUM_LEDS_PER_BOARD 3 +struct sunfire_drvdata { + struct sunfire_led leds[NUM_LEDS_PER_BOARD]; +}; + +static int sunfire_led_generic_probe(struct platform_device *pdev, + struct led_type *types) +{ + struct sunfire_drvdata *p; + int i, err; + + if (pdev->num_resources != 1) { + dev_err(&pdev->dev, "Wrong number of resources %d, should be 1\n", + pdev->num_resources); + return -EINVAL; + } + + p = devm_kzalloc(&pdev->dev, sizeof(*p), GFP_KERNEL); + if (!p) + return -ENOMEM; + + for (i = 0; i < NUM_LEDS_PER_BOARD; i++) { + struct led_classdev *lp = &p->leds[i].led_cdev; + + p->leds[i].reg = (void __iomem *) pdev->resource[0].start; + lp->name = types[i].name; + lp->brightness = LED_FULL; + lp->brightness_set = types[i].handler; + lp->default_trigger = types[i].default_trigger; + + err = led_classdev_register(&pdev->dev, lp); + if (err) { + dev_err(&pdev->dev, "Could not register %s LED\n", + lp->name); + for (i--; i >= 0; i--) + led_classdev_unregister(&p->leds[i].led_cdev); + return err; + } + } + + platform_set_drvdata(pdev, p); + + return 0; +} + +static int sunfire_led_generic_remove(struct platform_device *pdev) +{ + struct sunfire_drvdata *p = platform_get_drvdata(pdev); + int i; + + for (i = 0; i < NUM_LEDS_PER_BOARD; i++) + led_classdev_unregister(&p->leds[i].led_cdev); + + return 0; +} + +static struct led_type clockboard_led_types[NUM_LEDS_PER_BOARD] = { + { + .name = "clockboard-left", + .handler = clockboard_left_set, + }, + { + .name = "clockboard-middle", + .handler = clockboard_middle_set, + }, + { + .name = "clockboard-right", + .handler = clockboard_right_set, + .default_trigger = "heartbeat", + }, +}; + +static int sunfire_clockboard_led_probe(struct platform_device *pdev) +{ + return sunfire_led_generic_probe(pdev, clockboard_led_types); +} + +static struct led_type fhc_led_types[NUM_LEDS_PER_BOARD] = { + { + .name = "fhc-left", + .handler = fhc_left_set, + }, + { + .name = "fhc-middle", + .handler = fhc_middle_set, + }, + { + .name = "fhc-right", + .handler = fhc_right_set, + .default_trigger = "heartbeat", + }, +}; + +static int sunfire_fhc_led_probe(struct platform_device *pdev) +{ + return sunfire_led_generic_probe(pdev, fhc_led_types); +} + +MODULE_ALIAS("platform:sunfire-clockboard-leds"); +MODULE_ALIAS("platform:sunfire-fhc-leds"); + +static struct platform_driver sunfire_clockboard_led_driver = { + .probe = sunfire_clockboard_led_probe, + .remove = sunfire_led_generic_remove, + .driver = { + .name = "sunfire-clockboard-leds", + }, +}; + +static struct platform_driver sunfire_fhc_led_driver = { + .probe = sunfire_fhc_led_probe, + .remove = sunfire_led_generic_remove, + .driver = { + .name = "sunfire-fhc-leds", + }, +}; + +static struct platform_driver * const drivers[] = { + &sunfire_clockboard_led_driver, + &sunfire_fhc_led_driver, +}; + +static int __init sunfire_leds_init(void) +{ + return platform_register_drivers(drivers, ARRAY_SIZE(drivers)); +} + +static void __exit sunfire_leds_exit(void) +{ + platform_unregister_drivers(drivers, ARRAY_SIZE(drivers)); +} + +module_init(sunfire_leds_init); +module_exit(sunfire_leds_exit); diff --git a/drivers/leds/leds-syscon.c b/drivers/leds/leds-syscon.c new file mode 100644 index 000000000..7eddb8ecb --- /dev/null +++ b/drivers/leds/leds-syscon.c @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Generic Syscon LEDs Driver + * + * Copyright (c) 2014, Linaro Limited + * Author: Linus Walleij <linus.walleij@linaro.org> + */ +#include <linux/io.h> +#include <linux/init.h> +#include <linux/of_device.h> +#include <linux/of_address.h> +#include <linux/platform_device.h> +#include <linux/stat.h> +#include <linux/slab.h> +#include <linux/mfd/syscon.h> +#include <linux/regmap.h> +#include <linux/leds.h> + +/** + * struct syscon_led - state container for syscon based LEDs + * @cdev: LED class device for this LED + * @map: regmap to access the syscon device backing this LED + * @offset: the offset into the syscon regmap for the LED register + * @mask: the bit in the register corresponding to the LED + * @state: current state of the LED + */ +struct syscon_led { + struct led_classdev cdev; + struct regmap *map; + u32 offset; + u32 mask; + bool state; +}; + +static void syscon_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct syscon_led *sled = + container_of(led_cdev, struct syscon_led, cdev); + u32 val; + int ret; + + if (value == LED_OFF) { + val = 0; + sled->state = false; + } else { + val = sled->mask; + sled->state = true; + } + + ret = regmap_update_bits(sled->map, sled->offset, sled->mask, val); + if (ret < 0) + dev_err(sled->cdev.dev, "error updating LED status\n"); +} + +static int syscon_led_probe(struct platform_device *pdev) +{ + struct led_init_data init_data = {}; + struct device *dev = &pdev->dev; + struct device_node *np = dev_of_node(dev); + struct device *parent; + struct regmap *map; + struct syscon_led *sled; + const char *state; + int ret; + + parent = dev->parent; + if (!parent) { + dev_err(dev, "no parent for syscon LED\n"); + return -ENODEV; + } + map = syscon_node_to_regmap(dev_of_node(parent)); + if (IS_ERR(map)) { + dev_err(dev, "no regmap for syscon LED parent\n"); + return PTR_ERR(map); + } + + sled = devm_kzalloc(dev, sizeof(*sled), GFP_KERNEL); + if (!sled) + return -ENOMEM; + + sled->map = map; + + if (of_property_read_u32(np, "offset", &sled->offset)) + return -EINVAL; + if (of_property_read_u32(np, "mask", &sled->mask)) + return -EINVAL; + + state = of_get_property(np, "default-state", NULL); + if (state) { + if (!strcmp(state, "keep")) { + u32 val; + + ret = regmap_read(map, sled->offset, &val); + if (ret < 0) + return ret; + sled->state = !!(val & sled->mask); + } else if (!strcmp(state, "on")) { + sled->state = true; + ret = regmap_update_bits(map, sled->offset, + sled->mask, + sled->mask); + if (ret < 0) + return ret; + } else { + sled->state = false; + ret = regmap_update_bits(map, sled->offset, + sled->mask, 0); + if (ret < 0) + return ret; + } + } + sled->cdev.brightness_set = syscon_led_set; + + init_data.fwnode = of_fwnode_handle(np); + + ret = devm_led_classdev_register_ext(dev, &sled->cdev, &init_data); + if (ret < 0) + return ret; + + platform_set_drvdata(pdev, sled); + dev_info(dev, "registered LED %s\n", sled->cdev.name); + + return 0; +} + +static const struct of_device_id of_syscon_leds_match[] = { + { .compatible = "register-bit-led", }, + {}, +}; + +static struct platform_driver syscon_led_driver = { + .probe = syscon_led_probe, + .driver = { + .name = "leds-syscon", + .of_match_table = of_syscon_leds_match, + .suppress_bind_attrs = true, + }, +}; +builtin_platform_driver(syscon_led_driver); diff --git a/drivers/leds/leds-tca6507.c b/drivers/leds/leds-tca6507.c new file mode 100644 index 000000000..caad9d3e0 --- /dev/null +++ b/drivers/leds/leds-tca6507.c @@ -0,0 +1,829 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * leds-tca6507 + * + * The TCA6507 is a programmable LED controller that can drive 7 + * separate lines either by holding them low, or by pulsing them + * with modulated width. + * The modulation can be varied in a simple pattern to produce a + * blink or double-blink. + * + * This driver can configure each line either as a 'GPIO' which is + * out-only (pull-up resistor required) or as an LED with variable + * brightness and hardware-assisted blinking. + * + * Apart from OFF and ON there are three programmable brightness + * levels which can be programmed from 0 to 15 and indicate how many + * 500usec intervals in each 8msec that the led is 'on'. The levels + * are named MASTER, BANK0 and BANK1. + * + * There are two different blink rates that can be programmed, each + * with separate time for rise, on, fall, off and second-off. Thus if + * 3 or more different non-trivial rates are required, software must + * be used for the extra rates. The two different blink rates must + * align with the two levels BANK0 and BANK1. This driver does not + * support double-blink so 'second-off' always matches 'off'. + * + * Only 16 different times can be programmed in a roughly logarithmic + * scale from 64ms to 16320ms. To be precise the possible times are: + * 0, 64, 128, 192, 256, 384, 512, 768, + * 1024, 1536, 2048, 3072, 4096, 5760, 8128, 16320 + * + * Times that cannot be closely matched with these must be handled in + * software. This driver allows 12.5% error in matching. + * + * This driver does not allow rise/fall rates to be set explicitly. + * When trying to match a given 'on' or 'off' period, an appropriate + * pair of 'change' and 'hold' times are chosen to get a close match. + * If the target delay is even, the 'change' number will be the + * smaller; if odd, the 'hold' number will be the smaller. + + * Choosing pairs of delays with 12.5% errors allows us to match + * delays in the ranges: 56-72, 112-144, 168-216, 224-27504, + * 28560-36720. + * 26% of the achievable sums can be matched by multiple pairings. + * For example 1536 == 1536+0, 1024+512, or 768+768. + * This driver will always choose the pairing with the least + * maximum - 768+768 in this case. Other pairings are not available. + * + * Access to the 3 levels and 2 blinks are on a first-come, + * first-served basis. Access can be shared by multiple leds if they + * have the same level and either same blink rates, or some don't + * blink. When a led changes, it relinquishes access and tries again, + * so it might lose access to hardware blink. + * + * If a blink engine cannot be allocated, software blink is used. If + * the desired brightness cannot be allocated, the closest available + * non-zero brightness is used. As 'full' is always available, the + * worst case would be to have two different blink rates at '1', with + * Max at '2', then other leds will have to choose between '2' and + * '16'. Hopefully this is not likely. + * + * Each bank (BANK0 and BANK1) has two usage counts - LEDs using the + * brightness and LEDs using the blink. It can only be reprogrammed + * when the appropriate counter is zero. The MASTER level has a + * single usage count. + * + * Each LED has programmable 'on' and 'off' time as milliseconds. + * With each there is a flag saying if it was explicitly requested or + * defaulted. Similarly the banks know if each time was explicit or a + * default. Defaults are permitted to be changed freely - they are + * not recognised when matching. + */ + +#include <linux/module.h> +#include <linux/slab.h> +#include <linux/leds.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/gpio/driver.h> +#include <linux/property.h> +#include <linux/workqueue.h> + +/* LED select registers determine the source that drives LED outputs */ +#define TCA6507_LS_LED_OFF 0x0 /* Output HI-Z (off) */ +#define TCA6507_LS_LED_OFF1 0x1 /* Output HI-Z (off) - not used */ +#define TCA6507_LS_LED_PWM0 0x2 /* Output LOW with Bank0 rate */ +#define TCA6507_LS_LED_PWM1 0x3 /* Output LOW with Bank1 rate */ +#define TCA6507_LS_LED_ON 0x4 /* Output LOW (on) */ +#define TCA6507_LS_LED_MIR 0x5 /* Output LOW with Master Intensity */ +#define TCA6507_LS_BLINK0 0x6 /* Blink at Bank0 rate */ +#define TCA6507_LS_BLINK1 0x7 /* Blink at Bank1 rate */ + +struct tca6507_platform_data { + struct led_platform_data leds; +#ifdef CONFIG_GPIOLIB + int gpio_base; +#endif +}; + +#define TCA6507_MAKE_GPIO 1 + +enum { + BANK0, + BANK1, + MASTER, +}; +static int bank_source[3] = { + TCA6507_LS_LED_PWM0, + TCA6507_LS_LED_PWM1, + TCA6507_LS_LED_MIR, +}; +static int blink_source[2] = { + TCA6507_LS_BLINK0, + TCA6507_LS_BLINK1, +}; + +/* PWM registers */ +#define TCA6507_REG_CNT 11 + +/* + * 0x00, 0x01, 0x02 encode the TCA6507_LS_* values, each output + * owns one bit in each register + */ +#define TCA6507_FADE_ON 0x03 +#define TCA6507_FULL_ON 0x04 +#define TCA6507_FADE_OFF 0x05 +#define TCA6507_FIRST_OFF 0x06 +#define TCA6507_SECOND_OFF 0x07 +#define TCA6507_MAX_INTENSITY 0x08 +#define TCA6507_MASTER_INTENSITY 0x09 +#define TCA6507_INITIALIZE 0x0A + +#define INIT_CODE 0x8 + +#define TIMECODES 16 +static int time_codes[TIMECODES] = { + 0, 64, 128, 192, 256, 384, 512, 768, + 1024, 1536, 2048, 3072, 4096, 5760, 8128, 16320 +}; + +/* Convert an led.brightness level (0..255) to a TCA6507 level (0..15) */ +static inline int TO_LEVEL(int brightness) +{ + return brightness >> 4; +} + +/* ...and convert back */ +static inline int TO_BRIGHT(int level) +{ + if (level) + return (level << 4) | 0xf; + return 0; +} + +#define NUM_LEDS 7 +struct tca6507_chip { + int reg_set; /* One bit per register where + * a '1' means the register + * should be written */ + u8 reg_file[TCA6507_REG_CNT]; + /* Bank 2 is Master Intensity and doesn't use times */ + struct bank { + int level; + int ontime, offtime; + int on_dflt, off_dflt; + int time_use, level_use; + } bank[3]; + struct i2c_client *client; + struct work_struct work; + spinlock_t lock; + + struct tca6507_led { + struct tca6507_chip *chip; + struct led_classdev led_cdev; + int num; + int ontime, offtime; + int on_dflt, off_dflt; + int bank; /* Bank used, or -1 */ + int blink; /* Set if hardware-blinking */ + } leds[NUM_LEDS]; +#ifdef CONFIG_GPIOLIB + struct gpio_chip gpio; + int gpio_map[NUM_LEDS]; +#endif +}; + +static const struct i2c_device_id tca6507_id[] = { + { "tca6507" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, tca6507_id); + +static int choose_times(int msec, int *c1p, int *c2p) +{ + /* + * Choose two timecodes which add to 'msec' as near as + * possible. The first returned is the 'on' or 'off' time. + * The second is to be used as a 'fade-on' or 'fade-off' time. + * If 'msec' is even, the first will not be smaller than the + * second. If 'msec' is odd, the first will not be larger + * than the second. + * If we cannot get a sum within 1/8 of 'msec' fail with + * -EINVAL, otherwise return the sum that was achieved, plus 1 + * if the first is smaller. + * If two possibilities are equally good (e.g. 512+0, + * 256+256), choose the first pair so there is more + * change-time visible (i.e. it is softer). + */ + int c1, c2; + int tmax = msec * 9 / 8; + int tmin = msec * 7 / 8; + int diff = 65536; + + /* We start at '1' to ensure we never even think of choosing a + * total time of '0'. + */ + for (c1 = 1; c1 < TIMECODES; c1++) { + int t = time_codes[c1]; + if (t*2 < tmin) + continue; + if (t > tmax) + break; + for (c2 = 0; c2 <= c1; c2++) { + int tt = t + time_codes[c2]; + int d; + if (tt < tmin) + continue; + if (tt > tmax) + break; + /* This works! */ + d = abs(msec - tt); + if (d >= diff) + continue; + /* Best yet */ + *c1p = c1; + *c2p = c2; + diff = d; + if (d == 0) + return msec; + } + } + if (diff < 65536) { + int actual; + if (msec & 1) { + c1 = *c2p; + *c2p = *c1p; + *c1p = c1; + } + actual = time_codes[*c1p] + time_codes[*c2p]; + if (*c1p < *c2p) + return actual + 1; + else + return actual; + } + /* No close match */ + return -EINVAL; +} + +/* + * Update the register file with the appropriate 3-bit state for the + * given led. + */ +static void set_select(struct tca6507_chip *tca, int led, int val) +{ + int mask = (1 << led); + int bit; + + for (bit = 0; bit < 3; bit++) { + int n = tca->reg_file[bit] & ~mask; + if (val & (1 << bit)) + n |= mask; + if (tca->reg_file[bit] != n) { + tca->reg_file[bit] = n; + tca->reg_set |= (1 << bit); + } + } +} + +/* Update the register file with the appropriate 4-bit code for one + * bank or other. This can be used for timers, for levels, or for + * initialization. + */ +static void set_code(struct tca6507_chip *tca, int reg, int bank, int new) +{ + int mask = 0xF; + int n; + if (bank) { + mask <<= 4; + new <<= 4; + } + n = tca->reg_file[reg] & ~mask; + n |= new; + if (tca->reg_file[reg] != n) { + tca->reg_file[reg] = n; + tca->reg_set |= 1 << reg; + } +} + +/* Update brightness level. */ +static void set_level(struct tca6507_chip *tca, int bank, int level) +{ + switch (bank) { + case BANK0: + case BANK1: + set_code(tca, TCA6507_MAX_INTENSITY, bank, level); + break; + case MASTER: + set_code(tca, TCA6507_MASTER_INTENSITY, 0, level); + break; + } + tca->bank[bank].level = level; +} + +/* Record all relevant time codes for a given bank */ +static void set_times(struct tca6507_chip *tca, int bank) +{ + int c1, c2; + int result; + + result = choose_times(tca->bank[bank].ontime, &c1, &c2); + if (result < 0) + return; + dev_dbg(&tca->client->dev, + "Chose on times %d(%d) %d(%d) for %dms\n", + c1, time_codes[c1], + c2, time_codes[c2], tca->bank[bank].ontime); + set_code(tca, TCA6507_FADE_ON, bank, c2); + set_code(tca, TCA6507_FULL_ON, bank, c1); + tca->bank[bank].ontime = result; + + result = choose_times(tca->bank[bank].offtime, &c1, &c2); + dev_dbg(&tca->client->dev, + "Chose off times %d(%d) %d(%d) for %dms\n", + c1, time_codes[c1], + c2, time_codes[c2], tca->bank[bank].offtime); + set_code(tca, TCA6507_FADE_OFF, bank, c2); + set_code(tca, TCA6507_FIRST_OFF, bank, c1); + set_code(tca, TCA6507_SECOND_OFF, bank, c1); + tca->bank[bank].offtime = result; + + set_code(tca, TCA6507_INITIALIZE, bank, INIT_CODE); +} + +/* Write all needed register of tca6507 */ + +static void tca6507_work(struct work_struct *work) +{ + struct tca6507_chip *tca = container_of(work, struct tca6507_chip, + work); + struct i2c_client *cl = tca->client; + int set; + u8 file[TCA6507_REG_CNT]; + int r; + + spin_lock_irq(&tca->lock); + set = tca->reg_set; + memcpy(file, tca->reg_file, TCA6507_REG_CNT); + tca->reg_set = 0; + spin_unlock_irq(&tca->lock); + + for (r = 0; r < TCA6507_REG_CNT; r++) + if (set & (1<<r)) + i2c_smbus_write_byte_data(cl, r, file[r]); +} + +static void led_release(struct tca6507_led *led) +{ + /* If led owns any resource, release it. */ + struct tca6507_chip *tca = led->chip; + if (led->bank >= 0) { + struct bank *b = tca->bank + led->bank; + if (led->blink) + b->time_use--; + b->level_use--; + } + led->blink = 0; + led->bank = -1; +} + +static int led_prepare(struct tca6507_led *led) +{ + /* Assign this led to a bank, configuring that bank if + * necessary. */ + int level = TO_LEVEL(led->led_cdev.brightness); + struct tca6507_chip *tca = led->chip; + int c1, c2; + int i; + struct bank *b; + int need_init = 0; + + led->led_cdev.brightness = TO_BRIGHT(level); + if (level == 0) { + set_select(tca, led->num, TCA6507_LS_LED_OFF); + return 0; + } + + if (led->ontime == 0 || led->offtime == 0) { + /* + * Just set the brightness, choosing first usable + * bank. If none perfect, choose best. Count + * backwards so we check MASTER bank first to avoid + * wasting a timer. + */ + int best = -1;/* full-on */ + int diff = 15-level; + + if (level == 15) { + set_select(tca, led->num, TCA6507_LS_LED_ON); + return 0; + } + + for (i = MASTER; i >= BANK0; i--) { + int d; + if (tca->bank[i].level == level || + tca->bank[i].level_use == 0) { + best = i; + break; + } + d = abs(level - tca->bank[i].level); + if (d < diff) { + diff = d; + best = i; + } + } + if (best == -1) { + /* Best brightness is full-on */ + set_select(tca, led->num, TCA6507_LS_LED_ON); + led->led_cdev.brightness = LED_FULL; + return 0; + } + + if (!tca->bank[best].level_use) + set_level(tca, best, level); + + tca->bank[best].level_use++; + led->bank = best; + set_select(tca, led->num, bank_source[best]); + led->led_cdev.brightness = TO_BRIGHT(tca->bank[best].level); + return 0; + } + + /* + * We have on/off time so we need to try to allocate a timing + * bank. First check if times are compatible with hardware + * and give up if not. + */ + if (choose_times(led->ontime, &c1, &c2) < 0) + return -EINVAL; + if (choose_times(led->offtime, &c1, &c2) < 0) + return -EINVAL; + + for (i = BANK0; i <= BANK1; i++) { + if (tca->bank[i].level_use == 0) + /* not in use - it is ours! */ + break; + if (tca->bank[i].level != level) + /* Incompatible level - skip */ + /* FIX: if timer matches we maybe should consider + * this anyway... + */ + continue; + + if (tca->bank[i].time_use == 0) + /* Timer not in use, and level matches - use it */ + break; + + if (!(tca->bank[i].on_dflt || + led->on_dflt || + tca->bank[i].ontime == led->ontime)) + /* on time is incompatible */ + continue; + + if (!(tca->bank[i].off_dflt || + led->off_dflt || + tca->bank[i].offtime == led->offtime)) + /* off time is incompatible */ + continue; + + /* looks like a suitable match */ + break; + } + + if (i > BANK1) + /* Nothing matches - how sad */ + return -EINVAL; + + b = &tca->bank[i]; + if (b->level_use == 0) + set_level(tca, i, level); + b->level_use++; + led->bank = i; + + if (b->on_dflt || + !led->on_dflt || + b->time_use == 0) { + b->ontime = led->ontime; + b->on_dflt = led->on_dflt; + need_init = 1; + } + + if (b->off_dflt || + !led->off_dflt || + b->time_use == 0) { + b->offtime = led->offtime; + b->off_dflt = led->off_dflt; + need_init = 1; + } + + if (need_init) + set_times(tca, i); + + led->ontime = b->ontime; + led->offtime = b->offtime; + + b->time_use++; + led->blink = 1; + led->led_cdev.brightness = TO_BRIGHT(b->level); + set_select(tca, led->num, blink_source[i]); + return 0; +} + +static int led_assign(struct tca6507_led *led) +{ + struct tca6507_chip *tca = led->chip; + int err; + unsigned long flags; + + spin_lock_irqsave(&tca->lock, flags); + led_release(led); + err = led_prepare(led); + if (err) { + /* + * Can only fail on timer setup. In that case we need + * to re-establish as steady level. + */ + led->ontime = 0; + led->offtime = 0; + led_prepare(led); + } + spin_unlock_irqrestore(&tca->lock, flags); + + if (tca->reg_set) + schedule_work(&tca->work); + return err; +} + +static void tca6507_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct tca6507_led *led = container_of(led_cdev, struct tca6507_led, + led_cdev); + led->led_cdev.brightness = brightness; + led->ontime = 0; + led->offtime = 0; + led_assign(led); +} + +static int tca6507_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct tca6507_led *led = container_of(led_cdev, struct tca6507_led, + led_cdev); + + if (*delay_on == 0) + led->on_dflt = 1; + else if (delay_on != &led_cdev->blink_delay_on) + led->on_dflt = 0; + led->ontime = *delay_on; + + if (*delay_off == 0) + led->off_dflt = 1; + else if (delay_off != &led_cdev->blink_delay_off) + led->off_dflt = 0; + led->offtime = *delay_off; + + if (led->ontime == 0) + led->ontime = 512; + if (led->offtime == 0) + led->offtime = 512; + + if (led->led_cdev.brightness == LED_OFF) + led->led_cdev.brightness = LED_FULL; + if (led_assign(led) < 0) { + led->ontime = 0; + led->offtime = 0; + led->led_cdev.brightness = LED_OFF; + return -EINVAL; + } + *delay_on = led->ontime; + *delay_off = led->offtime; + return 0; +} + +#ifdef CONFIG_GPIOLIB +static void tca6507_gpio_set_value(struct gpio_chip *gc, + unsigned offset, int val) +{ + struct tca6507_chip *tca = gpiochip_get_data(gc); + unsigned long flags; + + spin_lock_irqsave(&tca->lock, flags); + /* + * 'OFF' is floating high, and 'ON' is pulled down, so it has + * the inverse sense of 'val'. + */ + set_select(tca, tca->gpio_map[offset], + val ? TCA6507_LS_LED_OFF : TCA6507_LS_LED_ON); + spin_unlock_irqrestore(&tca->lock, flags); + if (tca->reg_set) + schedule_work(&tca->work); +} + +static int tca6507_gpio_direction_output(struct gpio_chip *gc, + unsigned offset, int val) +{ + tca6507_gpio_set_value(gc, offset, val); + return 0; +} + +static int tca6507_probe_gpios(struct device *dev, + struct tca6507_chip *tca, + struct tca6507_platform_data *pdata) +{ + int err; + int i = 0; + int gpios = 0; + + for (i = 0; i < NUM_LEDS; i++) + if (pdata->leds.leds[i].name && pdata->leds.leds[i].flags) { + /* Configure as a gpio */ + tca->gpio_map[gpios] = i; + gpios++; + } + + if (!gpios) + return 0; + + tca->gpio.label = "gpio-tca6507"; + tca->gpio.ngpio = gpios; + tca->gpio.base = pdata->gpio_base; + tca->gpio.owner = THIS_MODULE; + tca->gpio.direction_output = tca6507_gpio_direction_output; + tca->gpio.set = tca6507_gpio_set_value; + tca->gpio.parent = dev; +#ifdef CONFIG_OF_GPIO + tca->gpio.of_node = of_node_get(dev_of_node(dev)); +#endif + err = gpiochip_add_data(&tca->gpio, tca); + if (err) { + tca->gpio.ngpio = 0; + return err; + } + return 0; +} + +static void tca6507_remove_gpio(struct tca6507_chip *tca) +{ + if (tca->gpio.ngpio) + gpiochip_remove(&tca->gpio); +} +#else /* CONFIG_GPIOLIB */ +static int tca6507_probe_gpios(struct device *dev, + struct tca6507_chip *tca, + struct tca6507_platform_data *pdata) +{ + return 0; +} +static void tca6507_remove_gpio(struct tca6507_chip *tca) +{ +} +#endif /* CONFIG_GPIOLIB */ + +static struct tca6507_platform_data * +tca6507_led_dt_init(struct device *dev) +{ + struct tca6507_platform_data *pdata; + struct fwnode_handle *child; + struct led_info *tca_leds; + int count; + + count = device_get_child_node_count(dev); + if (!count || count > NUM_LEDS) + return ERR_PTR(-ENODEV); + + tca_leds = devm_kcalloc(dev, NUM_LEDS, sizeof(struct led_info), + GFP_KERNEL); + if (!tca_leds) + return ERR_PTR(-ENOMEM); + + device_for_each_child_node(dev, child) { + struct led_info led; + u32 reg; + int ret; + + if (fwnode_property_read_string(child, "label", &led.name)) + led.name = fwnode_get_name(child); + + if (fwnode_property_read_string(child, "linux,default-trigger", + &led.default_trigger)) + led.default_trigger = NULL; + + led.flags = 0; + if (fwnode_property_match_string(child, "compatible", + "gpio") >= 0) + led.flags |= TCA6507_MAKE_GPIO; + + ret = fwnode_property_read_u32(child, "reg", ®); + if (ret || reg >= NUM_LEDS) { + fwnode_handle_put(child); + return ERR_PTR(ret ? : -EINVAL); + } + + tca_leds[reg] = led; + } + + pdata = devm_kzalloc(dev, sizeof(struct tca6507_platform_data), + GFP_KERNEL); + if (!pdata) + return ERR_PTR(-ENOMEM); + + pdata->leds.leds = tca_leds; + pdata->leds.num_leds = NUM_LEDS; +#ifdef CONFIG_GPIOLIB + pdata->gpio_base = -1; +#endif + + return pdata; +} + +static const struct of_device_id __maybe_unused of_tca6507_leds_match[] = { + { .compatible = "ti,tca6507", }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_tca6507_leds_match); + +static int tca6507_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct device *dev = &client->dev; + struct i2c_adapter *adapter; + struct tca6507_chip *tca; + struct tca6507_platform_data *pdata; + int err; + int i = 0; + + adapter = client->adapter; + + if (!i2c_check_functionality(adapter, I2C_FUNC_I2C)) + return -EIO; + + pdata = tca6507_led_dt_init(dev); + if (IS_ERR(pdata)) { + dev_err(dev, "Need %d entries in platform-data list\n", NUM_LEDS); + return PTR_ERR(pdata); + } + tca = devm_kzalloc(dev, sizeof(*tca), GFP_KERNEL); + if (!tca) + return -ENOMEM; + + tca->client = client; + INIT_WORK(&tca->work, tca6507_work); + spin_lock_init(&tca->lock); + i2c_set_clientdata(client, tca); + + for (i = 0; i < NUM_LEDS; i++) { + struct tca6507_led *l = tca->leds + i; + + l->chip = tca; + l->num = i; + if (pdata->leds.leds[i].name && !pdata->leds.leds[i].flags) { + l->led_cdev.name = pdata->leds.leds[i].name; + l->led_cdev.default_trigger + = pdata->leds.leds[i].default_trigger; + l->led_cdev.brightness_set = tca6507_brightness_set; + l->led_cdev.blink_set = tca6507_blink_set; + l->bank = -1; + err = led_classdev_register(dev, &l->led_cdev); + if (err < 0) + goto exit; + } + } + err = tca6507_probe_gpios(dev, tca, pdata); + if (err) + goto exit; + /* set all registers to known state - zero */ + tca->reg_set = 0x7f; + schedule_work(&tca->work); + + return 0; +exit: + while (i--) { + if (tca->leds[i].led_cdev.name) + led_classdev_unregister(&tca->leds[i].led_cdev); + } + return err; +} + +static int tca6507_remove(struct i2c_client *client) +{ + int i; + struct tca6507_chip *tca = i2c_get_clientdata(client); + struct tca6507_led *tca_leds = tca->leds; + + for (i = 0; i < NUM_LEDS; i++) { + if (tca_leds[i].led_cdev.name) + led_classdev_unregister(&tca_leds[i].led_cdev); + } + tca6507_remove_gpio(tca); + cancel_work_sync(&tca->work); + + return 0; +} + +static struct i2c_driver tca6507_driver = { + .driver = { + .name = "leds-tca6507", + .of_match_table = of_match_ptr(of_tca6507_leds_match), + }, + .probe = tca6507_probe, + .remove = tca6507_remove, + .id_table = tca6507_id, +}; + +module_i2c_driver(tca6507_driver); + +MODULE_AUTHOR("NeilBrown <neilb@suse.de>"); +MODULE_DESCRIPTION("TCA6507 LED/GPO driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-ti-lmu-common.c b/drivers/leds/leds-ti-lmu-common.c new file mode 100644 index 000000000..d7f10ad72 --- /dev/null +++ b/drivers/leds/leds-ti-lmu-common.c @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright 2015 Texas Instruments +// Copyright 2018 Sebastian Reichel +// Copyright 2018 Pavel Machek <pavel@ucw.cz> +// TI LMU LED common framework, based on previous work from +// Milo Kim <milo.kim@ti.com> + +#include <linux/bitops.h> +#include <linux/err.h> +#include <linux/of_device.h> + +#include <linux/leds-ti-lmu-common.h> + +static const unsigned int ramp_table[16] = {2048, 262000, 524000, 1049000, + 2090000, 4194000, 8389000, 16780000, 33550000, + 41940000, 50330000, 58720000, 67110000, + 83880000, 100660000, 117440000}; + +static int ti_lmu_common_update_brightness(struct ti_lmu_bank *lmu_bank, + int brightness) +{ + struct regmap *regmap = lmu_bank->regmap; + u8 reg, val; + int ret; + + /* + * Brightness register update + * + * 11 bit dimming: update LSB bits and write MSB byte. + * MSB brightness should be shifted. + * 8 bit dimming: write MSB byte. + */ + if (lmu_bank->max_brightness == MAX_BRIGHTNESS_11BIT) { + reg = lmu_bank->lsb_brightness_reg; + ret = regmap_update_bits(regmap, reg, + LMU_11BIT_LSB_MASK, + brightness); + if (ret) + return ret; + + val = brightness >> LMU_11BIT_MSB_SHIFT; + } else { + val = brightness; + } + + reg = lmu_bank->msb_brightness_reg; + + return regmap_write(regmap, reg, val); +} + +int ti_lmu_common_set_brightness(struct ti_lmu_bank *lmu_bank, int brightness) +{ + return ti_lmu_common_update_brightness(lmu_bank, brightness); +} +EXPORT_SYMBOL(ti_lmu_common_set_brightness); + +static unsigned int ti_lmu_common_convert_ramp_to_index(unsigned int usec) +{ + int size = ARRAY_SIZE(ramp_table); + int i; + + if (usec <= ramp_table[0]) + return 0; + + if (usec > ramp_table[size - 1]) + return size - 1; + + for (i = 1; i < size; i++) { + if (usec == ramp_table[i]) + return i; + + /* Find an approximate index by looking up the table */ + if (usec > ramp_table[i - 1] && usec < ramp_table[i]) { + if (usec - ramp_table[i - 1] < ramp_table[i] - usec) + return i - 1; + else + return i; + } + } + + return 0; +} + +int ti_lmu_common_set_ramp(struct ti_lmu_bank *lmu_bank) +{ + struct regmap *regmap = lmu_bank->regmap; + u8 ramp, ramp_up, ramp_down; + + if (lmu_bank->ramp_up_usec == 0 && lmu_bank->ramp_down_usec == 0) { + ramp_up = 0; + ramp_down = 0; + } else { + ramp_up = ti_lmu_common_convert_ramp_to_index(lmu_bank->ramp_up_usec); + ramp_down = ti_lmu_common_convert_ramp_to_index(lmu_bank->ramp_down_usec); + } + + ramp = (ramp_up << 4) | ramp_down; + + return regmap_write(regmap, lmu_bank->runtime_ramp_reg, ramp); + +} +EXPORT_SYMBOL(ti_lmu_common_set_ramp); + +int ti_lmu_common_get_ramp_params(struct device *dev, + struct fwnode_handle *child, + struct ti_lmu_bank *lmu_data) +{ + int ret; + + ret = fwnode_property_read_u32(child, "ramp-up-us", + &lmu_data->ramp_up_usec); + if (ret) + dev_warn(dev, "ramp-up-us property missing\n"); + + + ret = fwnode_property_read_u32(child, "ramp-down-us", + &lmu_data->ramp_down_usec); + if (ret) + dev_warn(dev, "ramp-down-us property missing\n"); + + return 0; +} +EXPORT_SYMBOL(ti_lmu_common_get_ramp_params); + +int ti_lmu_common_get_brt_res(struct device *dev, struct fwnode_handle *child, + struct ti_lmu_bank *lmu_data) +{ + int ret; + + ret = device_property_read_u32(dev, "ti,brightness-resolution", + &lmu_data->max_brightness); + if (ret) + ret = fwnode_property_read_u32(child, + "ti,brightness-resolution", + &lmu_data->max_brightness); + if (lmu_data->max_brightness <= 0) { + lmu_data->max_brightness = MAX_BRIGHTNESS_8BIT; + return ret; + } + + if (lmu_data->max_brightness > MAX_BRIGHTNESS_11BIT) + lmu_data->max_brightness = MAX_BRIGHTNESS_11BIT; + + + return 0; +} +EXPORT_SYMBOL(ti_lmu_common_get_brt_res); + +MODULE_DESCRIPTION("TI LMU common LED framework"); +MODULE_AUTHOR("Sebastian Reichel"); +MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>"); +MODULE_LICENSE("GPL v2"); +MODULE_ALIAS("ti-lmu-led-common"); diff --git a/drivers/leds/leds-tlc591xx.c b/drivers/leds/leds-tlc591xx.c new file mode 100644 index 000000000..cb7bd1353 --- /dev/null +++ b/drivers/leds/leds-tlc591xx.c @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2014 Belkin Inc. + * Copyright 2015 Andrew Lunn <andrew@lunn.ch> + */ + +#include <linux/i2c.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_device.h> +#include <linux/regmap.h> +#include <linux/slab.h> + +#define TLC591XX_MAX_LEDS 16 +#define TLC591XX_MAX_BRIGHTNESS 256 + +#define TLC591XX_REG_MODE1 0x00 +#define MODE1_RESPON_ADDR_MASK 0xF0 +#define MODE1_NORMAL_MODE (0 << 4) +#define MODE1_SPEED_MODE (1 << 4) + +#define TLC591XX_REG_MODE2 0x01 +#define MODE2_DIM (0 << 5) +#define MODE2_BLINK (1 << 5) +#define MODE2_OCH_STOP (0 << 3) +#define MODE2_OCH_ACK (1 << 3) + +#define TLC591XX_REG_PWM(x) (0x02 + (x)) + +#define TLC591XX_REG_GRPPWM 0x12 +#define TLC591XX_REG_GRPFREQ 0x13 + +/* LED Driver Output State, determine the source that drives LED outputs */ +#define LEDOUT_OFF 0x0 /* Output LOW */ +#define LEDOUT_ON 0x1 /* Output HI-Z */ +#define LEDOUT_DIM 0x2 /* Dimming */ +#define LEDOUT_BLINK 0x3 /* Blinking */ +#define LEDOUT_MASK 0x3 + +#define ldev_to_led(c) container_of(c, struct tlc591xx_led, ldev) + +struct tlc591xx_led { + bool active; + unsigned int led_no; + struct led_classdev ldev; + struct tlc591xx_priv *priv; +}; + +struct tlc591xx_priv { + struct tlc591xx_led leds[TLC591XX_MAX_LEDS]; + struct regmap *regmap; + unsigned int reg_ledout_offset; +}; + +struct tlc591xx { + unsigned int max_leds; + unsigned int reg_ledout_offset; +}; + +static const struct tlc591xx tlc59116 = { + .max_leds = 16, + .reg_ledout_offset = 0x14, +}; + +static const struct tlc591xx tlc59108 = { + .max_leds = 8, + .reg_ledout_offset = 0x0c, +}; + +static int +tlc591xx_set_mode(struct regmap *regmap, u8 mode) +{ + int err; + u8 val; + + err = regmap_write(regmap, TLC591XX_REG_MODE1, MODE1_NORMAL_MODE); + if (err) + return err; + + val = MODE2_OCH_STOP | mode; + + return regmap_write(regmap, TLC591XX_REG_MODE2, val); +} + +static int +tlc591xx_set_ledout(struct tlc591xx_priv *priv, struct tlc591xx_led *led, + u8 val) +{ + unsigned int i = (led->led_no % 4) * 2; + unsigned int mask = LEDOUT_MASK << i; + unsigned int addr = priv->reg_ledout_offset + (led->led_no >> 2); + + val = val << i; + + return regmap_update_bits(priv->regmap, addr, mask, val); +} + +static int +tlc591xx_set_pwm(struct tlc591xx_priv *priv, struct tlc591xx_led *led, + u8 brightness) +{ + u8 pwm = TLC591XX_REG_PWM(led->led_no); + + return regmap_write(priv->regmap, pwm, brightness); +} + +static int +tlc591xx_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct tlc591xx_led *led = ldev_to_led(led_cdev); + struct tlc591xx_priv *priv = led->priv; + int err; + + switch ((int)brightness) { + case 0: + err = tlc591xx_set_ledout(priv, led, LEDOUT_OFF); + break; + case TLC591XX_MAX_BRIGHTNESS: + err = tlc591xx_set_ledout(priv, led, LEDOUT_ON); + break; + default: + err = tlc591xx_set_ledout(priv, led, LEDOUT_DIM); + if (!err) + err = tlc591xx_set_pwm(priv, led, brightness); + } + + return err; +} + +static const struct regmap_config tlc591xx_regmap = { + .reg_bits = 8, + .val_bits = 8, + .max_register = 0x1e, +}; + +static const struct of_device_id of_tlc591xx_leds_match[] = { + { .compatible = "ti,tlc59116", + .data = &tlc59116 }, + { .compatible = "ti,tlc59108", + .data = &tlc59108 }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_tlc591xx_leds_match); + +static int +tlc591xx_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct device_node *np, *child; + struct device *dev = &client->dev; + const struct tlc591xx *tlc591xx; + struct tlc591xx_priv *priv; + int err, count, reg; + + np = dev_of_node(dev); + if (!np) + return -ENODEV; + + tlc591xx = device_get_match_data(dev); + if (!tlc591xx) + return -ENODEV; + + count = of_get_available_child_count(np); + if (!count || count > tlc591xx->max_leds) + return -EINVAL; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->regmap = devm_regmap_init_i2c(client, &tlc591xx_regmap); + if (IS_ERR(priv->regmap)) { + err = PTR_ERR(priv->regmap); + dev_err(dev, "Failed to allocate register map: %d\n", err); + return err; + } + priv->reg_ledout_offset = tlc591xx->reg_ledout_offset; + + i2c_set_clientdata(client, priv); + + err = tlc591xx_set_mode(priv->regmap, MODE2_DIM); + if (err < 0) + return err; + + for_each_available_child_of_node(np, child) { + struct tlc591xx_led *led; + struct led_init_data init_data = {}; + + init_data.fwnode = of_fwnode_handle(child); + + err = of_property_read_u32(child, "reg", ®); + if (err) { + of_node_put(child); + return err; + } + if (reg < 0 || reg >= tlc591xx->max_leds || + priv->leds[reg].active) { + of_node_put(child); + return -EINVAL; + } + led = &priv->leds[reg]; + + led->active = true; + led->priv = priv; + led->led_no = reg; + led->ldev.brightness_set_blocking = tlc591xx_brightness_set; + led->ldev.max_brightness = TLC591XX_MAX_BRIGHTNESS; + err = devm_led_classdev_register_ext(dev, &led->ldev, + &init_data); + if (err < 0) { + of_node_put(child); + return dev_err_probe(dev, err, + "couldn't register LED %s\n", + led->ldev.name); + } + } + return 0; +} + +static const struct i2c_device_id tlc591xx_id[] = { + { "tlc59116" }, + { "tlc59108" }, + {}, +}; +MODULE_DEVICE_TABLE(i2c, tlc591xx_id); + +static struct i2c_driver tlc591xx_driver = { + .driver = { + .name = "tlc591xx", + .of_match_table = of_match_ptr(of_tlc591xx_leds_match), + }, + .probe = tlc591xx_probe, + .id_table = tlc591xx_id, +}; + +module_i2c_driver(tlc591xx_driver); + +MODULE_AUTHOR("Andrew Lunn <andrew@lunn.ch>"); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("TLC591XX LED driver"); diff --git a/drivers/leds/leds-tps6105x.c b/drivers/leds/leds-tps6105x.c new file mode 100644 index 000000000..09fd88a6c --- /dev/null +++ b/drivers/leds/leds-tps6105x.c @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2019 Sven Van Asbroeck + */ + +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/platform_device.h> +#include <linux/mfd/tps6105x.h> +#include <linux/regmap.h> + +struct tps6105x_priv { + struct regmap *regmap; + struct led_classdev cdev; + struct fwnode_handle *fwnode; +}; + +static void tps6105x_handle_put(void *data) +{ + struct tps6105x_priv *priv = data; + + fwnode_handle_put(priv->fwnode); +} + +static int tps6105x_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct tps6105x_priv *priv = container_of(cdev, struct tps6105x_priv, + cdev); + + return regmap_update_bits(priv->regmap, TPS6105X_REG_0, + TPS6105X_REG0_TORCHC_MASK, + brightness << TPS6105X_REG0_TORCHC_SHIFT); +} + +static int tps6105x_led_probe(struct platform_device *pdev) +{ + struct tps6105x *tps6105x = dev_get_platdata(&pdev->dev); + struct tps6105x_platform_data *pdata = tps6105x->pdata; + struct led_init_data init_data = { }; + struct tps6105x_priv *priv; + int ret; + + /* This instance is not set for torch mode so bail out */ + if (pdata->mode != TPS6105X_MODE_TORCH) { + dev_info(&pdev->dev, + "chip not in torch mode, exit probe"); + return -EINVAL; + } + + priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + /* fwnode/devicetree is optional. NULL is allowed for priv->fwnode */ + priv->fwnode = device_get_next_child_node(pdev->dev.parent, NULL); + ret = devm_add_action_or_reset(&pdev->dev, tps6105x_handle_put, priv); + if (ret) + return ret; + priv->regmap = tps6105x->regmap; + priv->cdev.brightness_set_blocking = tps6105x_brightness_set; + priv->cdev.max_brightness = 7; + init_data.devicename = "tps6105x"; + init_data.default_label = ":torch"; + init_data.fwnode = priv->fwnode; + + ret = regmap_update_bits(tps6105x->regmap, TPS6105X_REG_0, + TPS6105X_REG0_MODE_MASK | + TPS6105X_REG0_TORCHC_MASK, + TPS6105X_REG0_MODE_TORCH << + TPS6105X_REG0_MODE_SHIFT); + if (ret) + return ret; + + return devm_led_classdev_register_ext(&pdev->dev, &priv->cdev, + &init_data); +} + +static struct platform_driver led_driver = { + .probe = tps6105x_led_probe, + .driver = { + .name = "tps6105x-leds", + }, +}; + +module_platform_driver(led_driver); + +MODULE_DESCRIPTION("TPS6105x LED driver"); +MODULE_AUTHOR("Sven Van Asbroeck <TheSven73@gmail.com>"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-turris-omnia.c b/drivers/leds/leds-turris-omnia.c new file mode 100644 index 000000000..ec87a958f --- /dev/null +++ b/drivers/leds/leds-turris-omnia.c @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * CZ.NIC's Turris Omnia LEDs driver + * + * 2020 by Marek Behun <marek.behun@nic.cz> + */ + +#include <linux/i2c.h> +#include <linux/led-class-multicolor.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include "leds.h" + +#define OMNIA_BOARD_LEDS 12 +#define OMNIA_LED_NUM_CHANNELS 3 + +#define CMD_LED_MODE 3 +#define CMD_LED_MODE_LED(l) ((l) & 0x0f) +#define CMD_LED_MODE_USER 0x10 + +#define CMD_LED_STATE 4 +#define CMD_LED_STATE_LED(l) ((l) & 0x0f) +#define CMD_LED_STATE_ON 0x10 + +#define CMD_LED_COLOR 5 +#define CMD_LED_SET_BRIGHTNESS 7 +#define CMD_LED_GET_BRIGHTNESS 8 + +#define OMNIA_CMD 0 + +#define OMNIA_CMD_LED_COLOR_LED 1 +#define OMNIA_CMD_LED_COLOR_R 2 +#define OMNIA_CMD_LED_COLOR_G 3 +#define OMNIA_CMD_LED_COLOR_B 4 +#define OMNIA_CMD_LED_COLOR_LEN 5 + +struct omnia_led { + struct led_classdev_mc mc_cdev; + struct mc_subled subled_info[OMNIA_LED_NUM_CHANNELS]; + int reg; +}; + +#define to_omnia_led(l) container_of(l, struct omnia_led, mc_cdev) + +struct omnia_leds { + struct i2c_client *client; + struct mutex lock; + struct omnia_led leds[]; +}; + +static int omnia_led_brightness_set_blocking(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev); + struct omnia_leds *leds = dev_get_drvdata(cdev->dev->parent); + struct omnia_led *led = to_omnia_led(mc_cdev); + u8 buf[OMNIA_CMD_LED_COLOR_LEN], state; + int ret; + + mutex_lock(&leds->lock); + + led_mc_calc_color_components(&led->mc_cdev, brightness); + + buf[OMNIA_CMD] = CMD_LED_COLOR; + buf[OMNIA_CMD_LED_COLOR_LED] = led->reg; + buf[OMNIA_CMD_LED_COLOR_R] = mc_cdev->subled_info[0].brightness; + buf[OMNIA_CMD_LED_COLOR_G] = mc_cdev->subled_info[1].brightness; + buf[OMNIA_CMD_LED_COLOR_B] = mc_cdev->subled_info[2].brightness; + + state = CMD_LED_STATE_LED(led->reg); + if (buf[OMNIA_CMD_LED_COLOR_R] || buf[OMNIA_CMD_LED_COLOR_G] || buf[OMNIA_CMD_LED_COLOR_B]) + state |= CMD_LED_STATE_ON; + + ret = i2c_smbus_write_byte_data(leds->client, CMD_LED_STATE, state); + if (ret >= 0 && (state & CMD_LED_STATE_ON)) + ret = i2c_master_send(leds->client, buf, 5); + + mutex_unlock(&leds->lock); + + return ret; +} + +static int omnia_led_register(struct i2c_client *client, struct omnia_led *led, + struct device_node *np) +{ + struct led_init_data init_data = {}; + struct device *dev = &client->dev; + struct led_classdev *cdev; + int ret, color; + + ret = of_property_read_u32(np, "reg", &led->reg); + if (ret || led->reg >= OMNIA_BOARD_LEDS) { + dev_warn(dev, + "Node %pOF: must contain 'reg' property with values between 0 and %i\n", + np, OMNIA_BOARD_LEDS - 1); + return 0; + } + + ret = of_property_read_u32(np, "color", &color); + if (ret || color != LED_COLOR_ID_RGB) { + dev_warn(dev, + "Node %pOF: must contain 'color' property with value LED_COLOR_ID_RGB\n", + np); + return 0; + } + + led->subled_info[0].color_index = LED_COLOR_ID_RED; + led->subled_info[0].channel = 0; + led->subled_info[1].color_index = LED_COLOR_ID_GREEN; + led->subled_info[1].channel = 1; + led->subled_info[2].color_index = LED_COLOR_ID_BLUE; + led->subled_info[2].channel = 2; + + led->mc_cdev.subled_info = led->subled_info; + led->mc_cdev.num_colors = OMNIA_LED_NUM_CHANNELS; + + init_data.fwnode = &np->fwnode; + + cdev = &led->mc_cdev.led_cdev; + cdev->max_brightness = 255; + cdev->brightness_set_blocking = omnia_led_brightness_set_blocking; + + /* put the LED into software mode */ + ret = i2c_smbus_write_byte_data(client, CMD_LED_MODE, + CMD_LED_MODE_LED(led->reg) | + CMD_LED_MODE_USER); + if (ret < 0) { + dev_err(dev, "Cannot set LED %pOF to software mode: %i\n", np, ret); + return ret; + } + + /* disable the LED */ + ret = i2c_smbus_write_byte_data(client, CMD_LED_STATE, CMD_LED_STATE_LED(led->reg)); + if (ret < 0) { + dev_err(dev, "Cannot set LED %pOF brightness: %i\n", np, ret); + return ret; + } + + ret = devm_led_classdev_multicolor_register_ext(dev, &led->mc_cdev, &init_data); + if (ret < 0) { + dev_err(dev, "Cannot register LED %pOF: %i\n", np, ret); + return ret; + } + + return 1; +} + +/* + * On the front panel of the Turris Omnia router there is also a button which + * can be used to control the intensity of all the LEDs at once, so that if they + * are too bright, user can dim them. + * The microcontroller cycles between 8 levels of this global brightness (from + * 100% to 0%), but this setting can have any integer value between 0 and 100. + * It is therefore convenient to be able to change this setting from software. + * We expose this setting via a sysfs attribute file called "brightness". This + * file lives in the device directory of the LED controller, not an individual + * LED, so it should not confuse users. + */ +static ssize_t brightness_show(struct device *dev, struct device_attribute *a, char *buf) +{ + struct i2c_client *client = to_i2c_client(dev); + struct omnia_leds *leds = i2c_get_clientdata(client); + int ret; + + mutex_lock(&leds->lock); + ret = i2c_smbus_read_byte_data(client, CMD_LED_GET_BRIGHTNESS); + mutex_unlock(&leds->lock); + + if (ret < 0) + return ret; + + return sprintf(buf, "%d\n", ret); +} + +static ssize_t brightness_store(struct device *dev, struct device_attribute *a, const char *buf, + size_t count) +{ + struct i2c_client *client = to_i2c_client(dev); + struct omnia_leds *leds = i2c_get_clientdata(client); + unsigned int brightness; + int ret; + + if (sscanf(buf, "%u", &brightness) != 1) + return -EINVAL; + + if (brightness > 100) + return -EINVAL; + + mutex_lock(&leds->lock); + ret = i2c_smbus_write_byte_data(client, CMD_LED_SET_BRIGHTNESS, (u8) brightness); + mutex_unlock(&leds->lock); + + if (ret < 0) + return ret; + + return count; +} +static DEVICE_ATTR_RW(brightness); + +static struct attribute *omnia_led_controller_attrs[] = { + &dev_attr_brightness.attr, + NULL, +}; +ATTRIBUTE_GROUPS(omnia_led_controller); + +static int omnia_leds_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + struct device *dev = &client->dev; + struct device_node *np = dev_of_node(dev), *child; + struct omnia_leds *leds; + struct omnia_led *led; + int ret, count; + + count = of_get_available_child_count(np); + if (!count) { + dev_err(dev, "LEDs are not defined in device tree!\n"); + return -ENODEV; + } else if (count > OMNIA_BOARD_LEDS) { + dev_err(dev, "Too many LEDs defined in device tree!\n"); + return -EINVAL; + } + + leds = devm_kzalloc(dev, struct_size(leds, leds, count), GFP_KERNEL); + if (!leds) + return -ENOMEM; + + leds->client = client; + i2c_set_clientdata(client, leds); + + mutex_init(&leds->lock); + + led = &leds->leds[0]; + for_each_available_child_of_node(np, child) { + ret = omnia_led_register(client, led, child); + if (ret < 0) { + of_node_put(child); + return ret; + } + + led += ret; + } + + if (devm_device_add_groups(dev, omnia_led_controller_groups)) + dev_warn(dev, "Could not add attribute group!\n"); + + return 0; +} + +static int omnia_leds_remove(struct i2c_client *client) +{ + u8 buf[OMNIA_CMD_LED_COLOR_LEN]; + + /* put all LEDs into default (HW triggered) mode */ + i2c_smbus_write_byte_data(client, CMD_LED_MODE, + CMD_LED_MODE_LED(OMNIA_BOARD_LEDS)); + + /* set all LEDs color to [255, 255, 255] */ + buf[OMNIA_CMD] = CMD_LED_COLOR; + buf[OMNIA_CMD_LED_COLOR_LED] = OMNIA_BOARD_LEDS; + buf[OMNIA_CMD_LED_COLOR_R] = 255; + buf[OMNIA_CMD_LED_COLOR_G] = 255; + buf[OMNIA_CMD_LED_COLOR_B] = 255; + + i2c_master_send(client, buf, 5); + + return 0; +} + +static const struct of_device_id of_omnia_leds_match[] = { + { .compatible = "cznic,turris-omnia-leds", }, + {}, +}; + +static const struct i2c_device_id omnia_id[] = { + { "omnia", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, omnia_id); + +static struct i2c_driver omnia_leds_driver = { + .probe = omnia_leds_probe, + .remove = omnia_leds_remove, + .id_table = omnia_id, + .driver = { + .name = "leds-turris-omnia", + .of_match_table = of_omnia_leds_match, + }, +}; + +module_i2c_driver(omnia_leds_driver); + +MODULE_AUTHOR("Marek Behun <marek.behun@nic.cz>"); +MODULE_DESCRIPTION("CZ.NIC's Turris Omnia LEDs"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/leds-wm831x-status.c b/drivers/leds/leds-wm831x-status.c new file mode 100644 index 000000000..67f4235cb --- /dev/null +++ b/drivers/leds/leds-wm831x-status.c @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED driver for WM831x status LEDs + * + * Copyright(C) 2009 Wolfson Microelectronics PLC. + */ + +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/slab.h> +#include <linux/leds.h> +#include <linux/err.h> +#include <linux/mfd/wm831x/core.h> +#include <linux/mfd/wm831x/pdata.h> +#include <linux/mfd/wm831x/status.h> +#include <linux/module.h> + + +struct wm831x_status { + struct led_classdev cdev; + struct wm831x *wm831x; + struct mutex mutex; + + spinlock_t value_lock; + int reg; /* Control register */ + int reg_val; /* Control register value */ + + int blink; + int blink_time; + int blink_cyc; + int src; + enum led_brightness brightness; +}; + +#define to_wm831x_status(led_cdev) \ + container_of(led_cdev, struct wm831x_status, cdev) + +static void wm831x_status_set(struct wm831x_status *led) +{ + unsigned long flags; + + mutex_lock(&led->mutex); + + led->reg_val &= ~(WM831X_LED_SRC_MASK | WM831X_LED_MODE_MASK | + WM831X_LED_DUTY_CYC_MASK | WM831X_LED_DUR_MASK); + + spin_lock_irqsave(&led->value_lock, flags); + + led->reg_val |= led->src << WM831X_LED_SRC_SHIFT; + if (led->blink) { + led->reg_val |= 2 << WM831X_LED_MODE_SHIFT; + led->reg_val |= led->blink_time << WM831X_LED_DUR_SHIFT; + led->reg_val |= led->blink_cyc; + } else { + if (led->brightness != LED_OFF) + led->reg_val |= 1 << WM831X_LED_MODE_SHIFT; + } + + spin_unlock_irqrestore(&led->value_lock, flags); + + wm831x_reg_write(led->wm831x, led->reg, led->reg_val); + + mutex_unlock(&led->mutex); +} + +static int wm831x_status_brightness_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct wm831x_status *led = to_wm831x_status(led_cdev); + unsigned long flags; + + spin_lock_irqsave(&led->value_lock, flags); + led->brightness = value; + if (value == LED_OFF) + led->blink = 0; + spin_unlock_irqrestore(&led->value_lock, flags); + wm831x_status_set(led); + + return 0; +} + +static int wm831x_status_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + struct wm831x_status *led = to_wm831x_status(led_cdev); + unsigned long flags; + int ret = 0; + + /* Pick some defaults if we've not been given times */ + if (*delay_on == 0 && *delay_off == 0) { + *delay_on = 250; + *delay_off = 250; + } + + spin_lock_irqsave(&led->value_lock, flags); + + /* We only have a limited selection of settings, see if we can + * support the configuration we're being given */ + switch (*delay_on) { + case 1000: + led->blink_time = 0; + break; + case 250: + led->blink_time = 1; + break; + case 125: + led->blink_time = 2; + break; + case 62: + case 63: + /* Actually 62.5ms */ + led->blink_time = 3; + break; + default: + ret = -EINVAL; + break; + } + + if (ret == 0) { + switch (*delay_off / *delay_on) { + case 1: + led->blink_cyc = 0; + break; + case 3: + led->blink_cyc = 1; + break; + case 4: + led->blink_cyc = 2; + break; + case 8: + led->blink_cyc = 3; + break; + default: + ret = -EINVAL; + break; + } + } + + if (ret == 0) + led->blink = 1; + else + led->blink = 0; + + spin_unlock_irqrestore(&led->value_lock, flags); + wm831x_status_set(led); + + return ret; +} + +static const char * const led_src_texts[] = { + "otp", + "power", + "charger", + "soft", +}; + +static ssize_t wm831x_status_src_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct wm831x_status *led = to_wm831x_status(led_cdev); + int i; + ssize_t ret = 0; + + mutex_lock(&led->mutex); + + for (i = 0; i < ARRAY_SIZE(led_src_texts); i++) + if (i == led->src) + ret += sprintf(&buf[ret], "[%s] ", led_src_texts[i]); + else + ret += sprintf(&buf[ret], "%s ", led_src_texts[i]); + + mutex_unlock(&led->mutex); + + ret += sprintf(&buf[ret], "\n"); + + return ret; +} + +static ssize_t wm831x_status_src_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct wm831x_status *led = to_wm831x_status(led_cdev); + int i; + + i = sysfs_match_string(led_src_texts, buf); + if (i >= 0) { + mutex_lock(&led->mutex); + led->src = i; + mutex_unlock(&led->mutex); + wm831x_status_set(led); + } + + return size; +} + +static DEVICE_ATTR(src, 0644, wm831x_status_src_show, wm831x_status_src_store); + +static struct attribute *wm831x_status_attrs[] = { + &dev_attr_src.attr, + NULL +}; +ATTRIBUTE_GROUPS(wm831x_status); + +static int wm831x_status_probe(struct platform_device *pdev) +{ + struct wm831x *wm831x = dev_get_drvdata(pdev->dev.parent); + struct wm831x_pdata *chip_pdata; + struct wm831x_status_pdata pdata; + struct wm831x_status *drvdata; + struct resource *res; + int id = pdev->id % ARRAY_SIZE(chip_pdata->status); + int ret; + + res = platform_get_resource(pdev, IORESOURCE_REG, 0); + if (res == NULL) { + dev_err(&pdev->dev, "No register resource\n"); + return -EINVAL; + } + + drvdata = devm_kzalloc(&pdev->dev, sizeof(struct wm831x_status), + GFP_KERNEL); + if (!drvdata) + return -ENOMEM; + + drvdata->wm831x = wm831x; + drvdata->reg = res->start; + + if (dev_get_platdata(wm831x->dev)) + chip_pdata = dev_get_platdata(wm831x->dev); + else + chip_pdata = NULL; + + memset(&pdata, 0, sizeof(pdata)); + if (chip_pdata && chip_pdata->status[id]) + memcpy(&pdata, chip_pdata->status[id], sizeof(pdata)); + else + pdata.name = dev_name(&pdev->dev); + + mutex_init(&drvdata->mutex); + spin_lock_init(&drvdata->value_lock); + + /* We cache the configuration register and read startup values + * from it. */ + drvdata->reg_val = wm831x_reg_read(wm831x, drvdata->reg); + + if (drvdata->reg_val & WM831X_LED_MODE_MASK) + drvdata->brightness = LED_FULL; + else + drvdata->brightness = LED_OFF; + + /* Set a default source if configured, otherwise leave the + * current hardware setting. + */ + if (pdata.default_src == WM831X_STATUS_PRESERVE) { + drvdata->src = drvdata->reg_val; + drvdata->src &= WM831X_LED_SRC_MASK; + drvdata->src >>= WM831X_LED_SRC_SHIFT; + } else { + drvdata->src = pdata.default_src - 1; + } + + drvdata->cdev.name = pdata.name; + drvdata->cdev.default_trigger = pdata.default_trigger; + drvdata->cdev.brightness_set_blocking = wm831x_status_brightness_set; + drvdata->cdev.blink_set = wm831x_status_blink_set; + drvdata->cdev.groups = wm831x_status_groups; + + ret = led_classdev_register(wm831x->dev, &drvdata->cdev); + if (ret < 0) { + dev_err(&pdev->dev, "Failed to register LED: %d\n", ret); + return ret; + } + + platform_set_drvdata(pdev, drvdata); + + return 0; +} + +static int wm831x_status_remove(struct platform_device *pdev) +{ + struct wm831x_status *drvdata = platform_get_drvdata(pdev); + + led_classdev_unregister(&drvdata->cdev); + + return 0; +} + +static struct platform_driver wm831x_status_driver = { + .driver = { + .name = "wm831x-status", + }, + .probe = wm831x_status_probe, + .remove = wm831x_status_remove, +}; + +module_platform_driver(wm831x_status_driver); + +MODULE_AUTHOR("Mark Brown <broonie@opensource.wolfsonmicro.com>"); +MODULE_DESCRIPTION("WM831x status LED driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:wm831x-status"); diff --git a/drivers/leds/leds-wm8350.c b/drivers/leds/leds-wm8350.c new file mode 100644 index 000000000..8f243c413 --- /dev/null +++ b/drivers/leds/leds-wm8350.c @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED driver for WM8350 driven LEDS. + * + * Copyright(C) 2007, 2008 Wolfson Microelectronics PLC. + */ + +#include <linux/kernel.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/err.h> +#include <linux/mfd/wm8350/pmic.h> +#include <linux/regulator/consumer.h> +#include <linux/slab.h> +#include <linux/module.h> + +/* Microamps */ +static const int isink_cur[] = { + 4, + 5, + 6, + 7, + 8, + 10, + 11, + 14, + 16, + 19, + 23, + 27, + 32, + 39, + 46, + 54, + 65, + 77, + 92, + 109, + 130, + 154, + 183, + 218, + 259, + 308, + 367, + 436, + 518, + 616, + 733, + 872, + 1037, + 1233, + 1466, + 1744, + 2073, + 2466, + 2933, + 3487, + 4147, + 4932, + 5865, + 6975, + 8294, + 9864, + 11730, + 13949, + 16589, + 19728, + 23460, + 27899, + 33178, + 39455, + 46920, + 55798, + 66355, + 78910, + 93840, + 111596, + 132710, + 157820, + 187681, + 223191 +}; + +#define to_wm8350_led(led_cdev) \ + container_of(led_cdev, struct wm8350_led, cdev) + +static int wm8350_led_enable(struct wm8350_led *led) +{ + int ret = 0; + + if (led->enabled) + return ret; + + ret = regulator_enable(led->isink); + if (ret != 0) { + dev_err(led->cdev.dev, "Failed to enable ISINK: %d\n", ret); + return ret; + } + + ret = regulator_enable(led->dcdc); + if (ret != 0) { + dev_err(led->cdev.dev, "Failed to enable DCDC: %d\n", ret); + regulator_disable(led->isink); + return ret; + } + + led->enabled = 1; + + return ret; +} + +static int wm8350_led_disable(struct wm8350_led *led) +{ + int ret = 0; + + if (!led->enabled) + return ret; + + ret = regulator_disable(led->dcdc); + if (ret != 0) { + dev_err(led->cdev.dev, "Failed to disable DCDC: %d\n", ret); + return ret; + } + + ret = regulator_disable(led->isink); + if (ret != 0) { + dev_err(led->cdev.dev, "Failed to disable ISINK: %d\n", ret); + ret = regulator_enable(led->dcdc); + if (ret != 0) + dev_err(led->cdev.dev, "Failed to reenable DCDC: %d\n", + ret); + return ret; + } + + led->enabled = 0; + + return ret; +} + +static int wm8350_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + struct wm8350_led *led = to_wm8350_led(led_cdev); + unsigned long flags; + int ret; + int uA; + + led->value = value; + + spin_lock_irqsave(&led->value_lock, flags); + + if (led->value == LED_OFF) { + spin_unlock_irqrestore(&led->value_lock, flags); + return wm8350_led_disable(led); + } + + /* This scales linearly into the index of valid current + * settings which results in a linear scaling of perceived + * brightness due to the non-linear current settings provided + * by the hardware. + */ + uA = (led->max_uA_index * led->value) / LED_FULL; + spin_unlock_irqrestore(&led->value_lock, flags); + BUG_ON(uA >= ARRAY_SIZE(isink_cur)); + + ret = regulator_set_current_limit(led->isink, isink_cur[uA], + isink_cur[uA]); + if (ret != 0) { + dev_err(led->cdev.dev, "Failed to set %duA: %d\n", + isink_cur[uA], ret); + return ret; + } + + return wm8350_led_enable(led); +} + +static void wm8350_led_shutdown(struct platform_device *pdev) +{ + struct wm8350_led *led = platform_get_drvdata(pdev); + + led->value = LED_OFF; + wm8350_led_disable(led); +} + +static int wm8350_led_probe(struct platform_device *pdev) +{ + struct regulator *isink, *dcdc; + struct wm8350_led *led; + struct wm8350_led_platform_data *pdata = dev_get_platdata(&pdev->dev); + int i; + + if (pdata == NULL) { + dev_err(&pdev->dev, "no platform data\n"); + return -ENODEV; + } + + if (pdata->max_uA < isink_cur[0]) { + dev_err(&pdev->dev, "Invalid maximum current %duA\n", + pdata->max_uA); + return -EINVAL; + } + + isink = devm_regulator_get(&pdev->dev, "led_isink"); + if (IS_ERR(isink)) { + dev_err(&pdev->dev, "%s: can't get ISINK\n", __func__); + return PTR_ERR(isink); + } + + dcdc = devm_regulator_get(&pdev->dev, "led_vcc"); + if (IS_ERR(dcdc)) { + dev_err(&pdev->dev, "%s: can't get DCDC\n", __func__); + return PTR_ERR(dcdc); + } + + led = devm_kzalloc(&pdev->dev, sizeof(*led), GFP_KERNEL); + if (led == NULL) + return -ENOMEM; + + led->cdev.brightness_set_blocking = wm8350_led_set; + led->cdev.default_trigger = pdata->default_trigger; + led->cdev.name = pdata->name; + led->cdev.flags |= LED_CORE_SUSPENDRESUME; + led->enabled = regulator_is_enabled(isink); + led->isink = isink; + led->dcdc = dcdc; + + for (i = 0; i < ARRAY_SIZE(isink_cur) - 1; i++) + if (isink_cur[i] >= pdata->max_uA) + break; + led->max_uA_index = i; + if (pdata->max_uA != isink_cur[i]) + dev_warn(&pdev->dev, + "Maximum current %duA is not directly supported," + " check platform data\n", + pdata->max_uA); + + spin_lock_init(&led->value_lock); + led->value = LED_OFF; + platform_set_drvdata(pdev, led); + + return led_classdev_register(&pdev->dev, &led->cdev); +} + +static int wm8350_led_remove(struct platform_device *pdev) +{ + struct wm8350_led *led = platform_get_drvdata(pdev); + + led_classdev_unregister(&led->cdev); + wm8350_led_disable(led); + return 0; +} + +static struct platform_driver wm8350_led_driver = { + .driver = { + .name = "wm8350-led", + }, + .probe = wm8350_led_probe, + .remove = wm8350_led_remove, + .shutdown = wm8350_led_shutdown, +}; + +module_platform_driver(wm8350_led_driver); + +MODULE_AUTHOR("Mark Brown"); +MODULE_DESCRIPTION("WM8350 LED driver"); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:wm8350-led"); diff --git a/drivers/leds/leds-wrap.c b/drivers/leds/leds-wrap.c new file mode 100644 index 000000000..794697e16 --- /dev/null +++ b/drivers/leds/leds-wrap.c @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LEDs driver for PCEngines WRAP + * + * Copyright (C) 2006 Kristian Kielhofner <kris@krisk.org> + * + * Based on leds-net48xx.c + */ + +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/platform_device.h> +#include <linux/leds.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/scx200_gpio.h> +#include <linux/module.h> + +#define DRVNAME "wrap-led" +#define WRAP_POWER_LED_GPIO 2 +#define WRAP_ERROR_LED_GPIO 3 +#define WRAP_EXTRA_LED_GPIO 18 + +static struct platform_device *pdev; + +static void wrap_power_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (value) + scx200_gpio_set_low(WRAP_POWER_LED_GPIO); + else + scx200_gpio_set_high(WRAP_POWER_LED_GPIO); +} + +static void wrap_error_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (value) + scx200_gpio_set_low(WRAP_ERROR_LED_GPIO); + else + scx200_gpio_set_high(WRAP_ERROR_LED_GPIO); +} + +static void wrap_extra_led_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + if (value) + scx200_gpio_set_low(WRAP_EXTRA_LED_GPIO); + else + scx200_gpio_set_high(WRAP_EXTRA_LED_GPIO); +} + +static struct led_classdev wrap_power_led = { + .name = "wrap::power", + .brightness_set = wrap_power_led_set, + .default_trigger = "default-on", + .flags = LED_CORE_SUSPENDRESUME, +}; + +static struct led_classdev wrap_error_led = { + .name = "wrap::error", + .brightness_set = wrap_error_led_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static struct led_classdev wrap_extra_led = { + .name = "wrap::extra", + .brightness_set = wrap_extra_led_set, + .flags = LED_CORE_SUSPENDRESUME, +}; + +static int wrap_led_probe(struct platform_device *pdev) +{ + int ret; + + ret = devm_led_classdev_register(&pdev->dev, &wrap_power_led); + if (ret < 0) + return ret; + + ret = devm_led_classdev_register(&pdev->dev, &wrap_error_led); + if (ret < 0) + return ret; + + return devm_led_classdev_register(&pdev->dev, &wrap_extra_led); +} + +static struct platform_driver wrap_led_driver = { + .probe = wrap_led_probe, + .driver = { + .name = DRVNAME, + }, +}; + +static int __init wrap_led_init(void) +{ + int ret; + + if (!scx200_gpio_present()) { + ret = -ENODEV; + goto out; + } + + ret = platform_driver_register(&wrap_led_driver); + if (ret < 0) + goto out; + + pdev = platform_device_register_simple(DRVNAME, -1, NULL, 0); + if (IS_ERR(pdev)) { + ret = PTR_ERR(pdev); + platform_driver_unregister(&wrap_led_driver); + goto out; + } + +out: + return ret; +} + +static void __exit wrap_led_exit(void) +{ + platform_device_unregister(pdev); + platform_driver_unregister(&wrap_led_driver); +} + +module_init(wrap_led_init); +module_exit(wrap_led_exit); + +MODULE_AUTHOR("Kristian Kielhofner <kris@krisk.org>"); +MODULE_DESCRIPTION("PCEngines WRAP LED driver"); +MODULE_LICENSE("GPL"); + diff --git a/drivers/leds/leds.h b/drivers/leds/leds.h new file mode 100644 index 000000000..2d9eb48bb --- /dev/null +++ b/drivers/leds/leds.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * LED Core + * + * Copyright 2005 Openedhand Ltd. + * + * Author: Richard Purdie <rpurdie@openedhand.com> + */ +#ifndef __LEDS_H_INCLUDED +#define __LEDS_H_INCLUDED + +#include <linux/rwsem.h> +#include <linux/leds.h> + +static inline int led_get_brightness(struct led_classdev *led_cdev) +{ + return led_cdev->brightness; +} + +void led_init_core(struct led_classdev *led_cdev); +void led_stop_software_blink(struct led_classdev *led_cdev); +void led_set_brightness_nopm(struct led_classdev *led_cdev, + enum led_brightness value); +void led_set_brightness_nosleep(struct led_classdev *led_cdev, + enum led_brightness value); +ssize_t led_trigger_read(struct file *filp, struct kobject *kobj, + struct bin_attribute *attr, char *buf, + loff_t pos, size_t count); +ssize_t led_trigger_write(struct file *filp, struct kobject *kobj, + struct bin_attribute *bin_attr, char *buf, + loff_t pos, size_t count); + +extern struct rw_semaphore leds_list_lock; +extern struct list_head leds_list; +extern struct list_head trigger_list; +extern const char * const led_colors[LED_COLOR_ID_MAX]; + +#endif /* __LEDS_H_INCLUDED */ diff --git a/drivers/leds/trigger/Kconfig b/drivers/leds/trigger/Kconfig new file mode 100644 index 000000000..ce9429ca6 --- /dev/null +++ b/drivers/leds/trigger/Kconfig @@ -0,0 +1,147 @@ +# SPDX-License-Identifier: GPL-2.0-only +menuconfig LEDS_TRIGGERS + bool "LED Trigger support" + depends on LEDS_CLASS + help + This option enables trigger support for the leds class. + These triggers allow kernel events to drive the LEDs and can + be configured via sysfs. If unsure, say Y. + +if LEDS_TRIGGERS + +config LEDS_TRIGGER_TIMER + tristate "LED Timer Trigger" + help + This allows LEDs to be controlled by a programmable timer + via sysfs. Some LED hardware can be programmed to start + blinking the LED without any further software interaction. + For more details read Documentation/leds/leds-class.rst. + + If unsure, say Y. + +config LEDS_TRIGGER_ONESHOT + tristate "LED One-shot Trigger" + help + This allows LEDs to blink in one-shot pulses with parameters + controlled via sysfs. It's useful to notify the user on + sporadic events, when there are no clear begin and end trap points, + or on dense events, where this blinks the LED at constant rate if + rearmed continuously. + + It also shows how to use the led_blink_set_oneshot() function. + + If unsure, say Y. + +config LEDS_TRIGGER_DISK + bool "LED Disk Trigger" + depends on IDE_GD_ATA || ATA + help + This allows LEDs to be controlled by disk activity. + If unsure, say Y. + +config LEDS_TRIGGER_MTD + bool "LED MTD (NAND/NOR) Trigger" + depends on MTD + help + This allows LEDs to be controlled by MTD activity. + If unsure, say N. + +config LEDS_TRIGGER_HEARTBEAT + tristate "LED Heartbeat Trigger" + help + This allows LEDs to be controlled by a CPU load average. + The flash frequency is a hyperbolic function of the 1-minute + load average. + If unsure, say Y. + +config LEDS_TRIGGER_BACKLIGHT + tristate "LED backlight Trigger" + help + This allows LEDs to be controlled as a backlight device: they + turn off and on when the display is blanked and unblanked. + + If unsure, say N. + +config LEDS_TRIGGER_CPU + bool "LED CPU Trigger" + help + This allows LEDs to be controlled by active CPUs. This shows + the active CPUs across an array of LEDs so you can see which + CPUs are active on the system at any given moment. + + If unsure, say N. + +config LEDS_TRIGGER_ACTIVITY + tristate "LED activity Trigger" + help + This allows LEDs to be controlled by an immediate CPU usage. + The flash frequency and duty cycle varies from faint flashes to + intense brightness depending on the instant CPU load. + If unsure, say N. + +config LEDS_TRIGGER_GPIO + tristate "LED GPIO Trigger" + depends on GPIOLIB || COMPILE_TEST + help + This allows LEDs to be controlled by gpio events. It's good + when using gpios as switches and triggering the needed LEDs + from there. One use case is n810's keypad LEDs that could + be triggered by this trigger when user slides up to show + keypad. + + If unsure, say N. + +config LEDS_TRIGGER_DEFAULT_ON + tristate "LED Default ON Trigger" + help + This allows LEDs to be initialised in the ON state. + If unsure, say Y. + +comment "iptables trigger is under Netfilter config (LED target)" + depends on LEDS_TRIGGERS + +config LEDS_TRIGGER_TRANSIENT + tristate "LED Transient Trigger" + help + This allows one time activation of a transient state on + GPIO/PWM based hardware. + If unsure, say Y. + +config LEDS_TRIGGER_CAMERA + tristate "LED Camera Flash/Torch Trigger" + help + This allows LEDs to be controlled as a camera flash/torch device. + This enables direct flash/torch on/off by the driver, kernel space. + If unsure, say Y. + +config LEDS_TRIGGER_PANIC + bool "LED Panic Trigger" + help + This allows LEDs to be configured to blink on a kernel panic. + Enabling this option will allow to mark certain LEDs as panic indicators, + allowing to blink them on a kernel panic, even if they are set to + a different trigger. + If unsure, say Y. + +config LEDS_TRIGGER_NETDEV + tristate "LED Netdev Trigger" + depends on NET + help + This allows LEDs to be controlled by network device activity. + If unsure, say Y. + +config LEDS_TRIGGER_PATTERN + tristate "LED Pattern Trigger" + help + This allows LEDs to be controlled by a software or hardware pattern + which is a series of tuples, of brightness and duration (ms). + If unsure, say N + +config LEDS_TRIGGER_AUDIO + tristate "Audio Mute LED Trigger" + help + This allows LEDs to be controlled by audio drivers for following + the audio mute and mic-mute changes. + If unsure, say N + +endif # LEDS_TRIGGERS diff --git a/drivers/leds/trigger/Makefile b/drivers/leds/trigger/Makefile new file mode 100644 index 000000000..733a83e2a --- /dev/null +++ b/drivers/leds/trigger/Makefile @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_LEDS_TRIGGER_TIMER) += ledtrig-timer.o +obj-$(CONFIG_LEDS_TRIGGER_ONESHOT) += ledtrig-oneshot.o +obj-$(CONFIG_LEDS_TRIGGER_DISK) += ledtrig-disk.o +obj-$(CONFIG_LEDS_TRIGGER_MTD) += ledtrig-mtd.o +obj-$(CONFIG_LEDS_TRIGGER_HEARTBEAT) += ledtrig-heartbeat.o +obj-$(CONFIG_LEDS_TRIGGER_BACKLIGHT) += ledtrig-backlight.o +obj-$(CONFIG_LEDS_TRIGGER_GPIO) += ledtrig-gpio.o +obj-$(CONFIG_LEDS_TRIGGER_CPU) += ledtrig-cpu.o +obj-$(CONFIG_LEDS_TRIGGER_ACTIVITY) += ledtrig-activity.o +obj-$(CONFIG_LEDS_TRIGGER_DEFAULT_ON) += ledtrig-default-on.o +obj-$(CONFIG_LEDS_TRIGGER_TRANSIENT) += ledtrig-transient.o +obj-$(CONFIG_LEDS_TRIGGER_CAMERA) += ledtrig-camera.o +obj-$(CONFIG_LEDS_TRIGGER_PANIC) += ledtrig-panic.o +obj-$(CONFIG_LEDS_TRIGGER_NETDEV) += ledtrig-netdev.o +obj-$(CONFIG_LEDS_TRIGGER_PATTERN) += ledtrig-pattern.o +obj-$(CONFIG_LEDS_TRIGGER_AUDIO) += ledtrig-audio.o diff --git a/drivers/leds/trigger/ledtrig-activity.c b/drivers/leds/trigger/ledtrig-activity.c new file mode 100644 index 000000000..14ba7faae --- /dev/null +++ b/drivers/leds/trigger/ledtrig-activity.c @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Activity LED trigger + * + * Copyright (C) 2017 Willy Tarreau <w@1wt.eu> + * Partially based on Atsushi Nemoto's ledtrig-heartbeat.c. + */ + +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/kernel_stat.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/reboot.h> +#include <linux/sched.h> +#include <linux/slab.h> +#include <linux/timer.h> +#include "../leds.h" + +static int panic_detected; + +struct activity_data { + struct timer_list timer; + struct led_classdev *led_cdev; + u64 last_used; + u64 last_boot; + int time_left; + int state; + int invert; +}; + +static void led_activity_function(struct timer_list *t) +{ + struct activity_data *activity_data = from_timer(activity_data, t, + timer); + struct led_classdev *led_cdev = activity_data->led_cdev; + unsigned int target; + unsigned int usage; + int delay; + u64 curr_used; + u64 curr_boot; + s32 diff_used; + s32 diff_boot; + int cpus; + int i; + + if (test_and_clear_bit(LED_BLINK_BRIGHTNESS_CHANGE, &led_cdev->work_flags)) + led_cdev->blink_brightness = led_cdev->new_blink_brightness; + + if (unlikely(panic_detected)) { + /* full brightness in case of panic */ + led_set_brightness_nosleep(led_cdev, led_cdev->blink_brightness); + return; + } + + cpus = 0; + curr_used = 0; + + for_each_possible_cpu(i) { + struct kernel_cpustat kcpustat; + + kcpustat_cpu_fetch(&kcpustat, i); + + curr_used += kcpustat.cpustat[CPUTIME_USER] + + kcpustat.cpustat[CPUTIME_NICE] + + kcpustat.cpustat[CPUTIME_SYSTEM] + + kcpustat.cpustat[CPUTIME_SOFTIRQ] + + kcpustat.cpustat[CPUTIME_IRQ]; + cpus++; + } + + /* We come here every 100ms in the worst case, so that's 100M ns of + * cumulated time. By dividing by 2^16, we get the time resolution + * down to 16us, ensuring we won't overflow 32-bit computations below + * even up to 3k CPUs, while keeping divides cheap on smaller systems. + */ + curr_boot = ktime_get_boottime_ns() * cpus; + diff_boot = (curr_boot - activity_data->last_boot) >> 16; + diff_used = (curr_used - activity_data->last_used) >> 16; + activity_data->last_boot = curr_boot; + activity_data->last_used = curr_used; + + if (diff_boot <= 0 || diff_used < 0) + usage = 0; + else if (diff_used >= diff_boot) + usage = 100; + else + usage = 100 * diff_used / diff_boot; + + /* + * Now we know the total boot_time multiplied by the number of CPUs, and + * the total idle+wait time for all CPUs. We'll compare how they evolved + * since last call. The % of overall CPU usage is : + * + * 1 - delta_idle / delta_boot + * + * What we want is that when the CPU usage is zero, the LED must blink + * slowly with very faint flashes that are detectable but not disturbing + * (typically 10ms every second, or 10ms ON, 990ms OFF). Then we want + * blinking frequency to increase up to the point where the load is + * enough to saturate one core in multi-core systems or 50% in single + * core systems. At this point it should reach 10 Hz with a 10/90 duty + * cycle (10ms ON, 90ms OFF). After this point, the blinking frequency + * remains stable (10 Hz) and only the duty cycle increases to report + * the activity, up to the point where we have 90ms ON, 10ms OFF when + * all cores are saturated. It's important that the LED never stays in + * a steady state so that it's easy to distinguish an idle or saturated + * machine from a hung one. + * + * This gives us : + * - a target CPU usage of min(50%, 100%/#CPU) for a 10% duty cycle + * (10ms ON, 90ms OFF) + * - below target : + * ON_ms = 10 + * OFF_ms = 90 + (1 - usage/target) * 900 + * - above target : + * ON_ms = 10 + (usage-target)/(100%-target) * 80 + * OFF_ms = 90 - (usage-target)/(100%-target) * 80 + * + * In order to keep a good responsiveness, we cap the sleep time to + * 100 ms and keep track of the sleep time left. This allows us to + * quickly change it if needed. + */ + + activity_data->time_left -= 100; + if (activity_data->time_left <= 0) { + activity_data->time_left = 0; + activity_data->state = !activity_data->state; + led_set_brightness_nosleep(led_cdev, + (activity_data->state ^ activity_data->invert) ? + led_cdev->blink_brightness : LED_OFF); + } + + target = (cpus > 1) ? (100 / cpus) : 50; + + if (usage < target) + delay = activity_data->state ? + 10 : /* ON */ + 990 - 900 * usage / target; /* OFF */ + else + delay = activity_data->state ? + 10 + 80 * (usage - target) / (100 - target) : /* ON */ + 90 - 80 * (usage - target) / (100 - target); /* OFF */ + + + if (!activity_data->time_left || delay <= activity_data->time_left) + activity_data->time_left = delay; + + delay = min_t(int, activity_data->time_left, 100); + mod_timer(&activity_data->timer, jiffies + msecs_to_jiffies(delay)); +} + +static ssize_t led_invert_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct activity_data *activity_data = led_trigger_get_drvdata(dev); + + return sprintf(buf, "%u\n", activity_data->invert); +} + +static ssize_t led_invert_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + struct activity_data *activity_data = led_trigger_get_drvdata(dev); + unsigned long state; + int ret; + + ret = kstrtoul(buf, 0, &state); + if (ret) + return ret; + + activity_data->invert = !!state; + + return size; +} + +static DEVICE_ATTR(invert, 0644, led_invert_show, led_invert_store); + +static struct attribute *activity_led_attrs[] = { + &dev_attr_invert.attr, + NULL +}; +ATTRIBUTE_GROUPS(activity_led); + +static int activity_activate(struct led_classdev *led_cdev) +{ + struct activity_data *activity_data; + + activity_data = kzalloc(sizeof(*activity_data), GFP_KERNEL); + if (!activity_data) + return -ENOMEM; + + led_set_trigger_data(led_cdev, activity_data); + + activity_data->led_cdev = led_cdev; + timer_setup(&activity_data->timer, led_activity_function, 0); + if (!led_cdev->blink_brightness) + led_cdev->blink_brightness = led_cdev->max_brightness; + led_activity_function(&activity_data->timer); + set_bit(LED_BLINK_SW, &led_cdev->work_flags); + + return 0; +} + +static void activity_deactivate(struct led_classdev *led_cdev) +{ + struct activity_data *activity_data = led_get_trigger_data(led_cdev); + + del_timer_sync(&activity_data->timer); + kfree(activity_data); + clear_bit(LED_BLINK_SW, &led_cdev->work_flags); +} + +static struct led_trigger activity_led_trigger = { + .name = "activity", + .activate = activity_activate, + .deactivate = activity_deactivate, + .groups = activity_led_groups, +}; + +static int activity_reboot_notifier(struct notifier_block *nb, + unsigned long code, void *unused) +{ + led_trigger_unregister(&activity_led_trigger); + return NOTIFY_DONE; +} + +static int activity_panic_notifier(struct notifier_block *nb, + unsigned long code, void *unused) +{ + panic_detected = 1; + return NOTIFY_DONE; +} + +static struct notifier_block activity_reboot_nb = { + .notifier_call = activity_reboot_notifier, +}; + +static struct notifier_block activity_panic_nb = { + .notifier_call = activity_panic_notifier, +}; + +static int __init activity_init(void) +{ + int rc = led_trigger_register(&activity_led_trigger); + + if (!rc) { + atomic_notifier_chain_register(&panic_notifier_list, + &activity_panic_nb); + register_reboot_notifier(&activity_reboot_nb); + } + return rc; +} + +static void __exit activity_exit(void) +{ + unregister_reboot_notifier(&activity_reboot_nb); + atomic_notifier_chain_unregister(&panic_notifier_list, + &activity_panic_nb); + led_trigger_unregister(&activity_led_trigger); +} + +module_init(activity_init); +module_exit(activity_exit); + +MODULE_AUTHOR("Willy Tarreau <w@1wt.eu>"); +MODULE_DESCRIPTION("Activity LED trigger"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/trigger/ledtrig-audio.c b/drivers/leds/trigger/ledtrig-audio.c new file mode 100644 index 000000000..c6b437e63 --- /dev/null +++ b/drivers/leds/trigger/ledtrig-audio.c @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Audio Mute LED trigger +// + +#include <linux/kernel.h> +#include <linux/leds.h> +#include <linux/module.h> +#include "../leds.h" + +static enum led_brightness audio_state[NUM_AUDIO_LEDS]; + +static int ledtrig_audio_mute_activate(struct led_classdev *led_cdev) +{ + led_set_brightness_nosleep(led_cdev, audio_state[LED_AUDIO_MUTE]); + return 0; +} + +static int ledtrig_audio_micmute_activate(struct led_classdev *led_cdev) +{ + led_set_brightness_nosleep(led_cdev, audio_state[LED_AUDIO_MICMUTE]); + return 0; +} + +static struct led_trigger ledtrig_audio[NUM_AUDIO_LEDS] = { + [LED_AUDIO_MUTE] = { + .name = "audio-mute", + .activate = ledtrig_audio_mute_activate, + }, + [LED_AUDIO_MICMUTE] = { + .name = "audio-micmute", + .activate = ledtrig_audio_micmute_activate, + }, +}; + +enum led_brightness ledtrig_audio_get(enum led_audio type) +{ + return audio_state[type]; +} +EXPORT_SYMBOL_GPL(ledtrig_audio_get); + +void ledtrig_audio_set(enum led_audio type, enum led_brightness state) +{ + audio_state[type] = state; + led_trigger_event(&ledtrig_audio[type], state); +} +EXPORT_SYMBOL_GPL(ledtrig_audio_set); + +static int __init ledtrig_audio_init(void) +{ + led_trigger_register(&ledtrig_audio[LED_AUDIO_MUTE]); + led_trigger_register(&ledtrig_audio[LED_AUDIO_MICMUTE]); + return 0; +} +module_init(ledtrig_audio_init); + +static void __exit ledtrig_audio_exit(void) +{ + led_trigger_unregister(&ledtrig_audio[LED_AUDIO_MUTE]); + led_trigger_unregister(&ledtrig_audio[LED_AUDIO_MICMUTE]); +} +module_exit(ledtrig_audio_exit); + +MODULE_DESCRIPTION("LED trigger for audio mute control"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/trigger/ledtrig-backlight.c b/drivers/leds/trigger/ledtrig-backlight.c new file mode 100644 index 000000000..487577d22 --- /dev/null +++ b/drivers/leds/trigger/ledtrig-backlight.c @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Backlight emulation LED trigger + * + * Copyright 2008 (C) Rodolfo Giometti <giometti@linux.it> + * Copyright 2008 (C) Eurotech S.p.A. <info@eurotech.it> + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/slab.h> +#include <linux/init.h> +#include <linux/fb.h> +#include <linux/leds.h> +#include "../leds.h" + +#define BLANK 1 +#define UNBLANK 0 + +struct bl_trig_notifier { + struct led_classdev *led; + int brightness; + int old_status; + struct notifier_block notifier; + unsigned invert; +}; + +static int fb_notifier_callback(struct notifier_block *p, + unsigned long event, void *data) +{ + struct bl_trig_notifier *n = container_of(p, + struct bl_trig_notifier, notifier); + struct led_classdev *led = n->led; + struct fb_event *fb_event = data; + int *blank; + int new_status; + + /* If we aren't interested in this event, skip it immediately ... */ + if (event != FB_EVENT_BLANK) + return 0; + + blank = fb_event->data; + new_status = *blank ? BLANK : UNBLANK; + + if (new_status == n->old_status) + return 0; + + if ((n->old_status == UNBLANK) ^ n->invert) { + n->brightness = led->brightness; + led_set_brightness_nosleep(led, LED_OFF); + } else { + led_set_brightness_nosleep(led, n->brightness); + } + + n->old_status = new_status; + + return 0; +} + +static ssize_t bl_trig_invert_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct bl_trig_notifier *n = led_trigger_get_drvdata(dev); + + return sprintf(buf, "%u\n", n->invert); +} + +static ssize_t bl_trig_invert_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t num) +{ + struct led_classdev *led = led_trigger_get_led(dev); + struct bl_trig_notifier *n = led_trigger_get_drvdata(dev); + unsigned long invert; + int ret; + + ret = kstrtoul(buf, 10, &invert); + if (ret < 0) + return ret; + + if (invert > 1) + return -EINVAL; + + n->invert = invert; + + /* After inverting, we need to update the LED. */ + if ((n->old_status == BLANK) ^ n->invert) + led_set_brightness_nosleep(led, LED_OFF); + else + led_set_brightness_nosleep(led, n->brightness); + + return num; +} +static DEVICE_ATTR(inverted, 0644, bl_trig_invert_show, bl_trig_invert_store); + +static struct attribute *bl_trig_attrs[] = { + &dev_attr_inverted.attr, + NULL, +}; +ATTRIBUTE_GROUPS(bl_trig); + +static int bl_trig_activate(struct led_classdev *led) +{ + int ret; + + struct bl_trig_notifier *n; + + n = kzalloc(sizeof(struct bl_trig_notifier), GFP_KERNEL); + if (!n) + return -ENOMEM; + led_set_trigger_data(led, n); + + n->led = led; + n->brightness = led->brightness; + n->old_status = UNBLANK; + n->notifier.notifier_call = fb_notifier_callback; + + ret = fb_register_client(&n->notifier); + if (ret) + dev_err(led->dev, "unable to register backlight trigger\n"); + + return 0; +} + +static void bl_trig_deactivate(struct led_classdev *led) +{ + struct bl_trig_notifier *n = led_get_trigger_data(led); + + fb_unregister_client(&n->notifier); + kfree(n); +} + +static struct led_trigger bl_led_trigger = { + .name = "backlight", + .activate = bl_trig_activate, + .deactivate = bl_trig_deactivate, + .groups = bl_trig_groups, +}; +module_led_trigger(bl_led_trigger); + +MODULE_AUTHOR("Rodolfo Giometti <giometti@linux.it>"); +MODULE_DESCRIPTION("Backlight emulation LED trigger"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/trigger/ledtrig-camera.c b/drivers/leds/trigger/ledtrig-camera.c new file mode 100644 index 000000000..ab1c41087 --- /dev/null +++ b/drivers/leds/trigger/ledtrig-camera.c @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Camera Flash and Torch On/Off Trigger + * + * based on ledtrig-ide-disk.c + * + * Copyright 2013 Texas Instruments + * + * Author: Milo(Woogyom) Kim <milo.kim@ti.com> + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/leds.h> + +DEFINE_LED_TRIGGER(ledtrig_flash); +DEFINE_LED_TRIGGER(ledtrig_torch); + +void ledtrig_flash_ctrl(bool on) +{ + enum led_brightness brt = on ? LED_FULL : LED_OFF; + + led_trigger_event(ledtrig_flash, brt); +} +EXPORT_SYMBOL_GPL(ledtrig_flash_ctrl); + +void ledtrig_torch_ctrl(bool on) +{ + enum led_brightness brt = on ? LED_FULL : LED_OFF; + + led_trigger_event(ledtrig_torch, brt); +} +EXPORT_SYMBOL_GPL(ledtrig_torch_ctrl); + +static int __init ledtrig_camera_init(void) +{ + led_trigger_register_simple("flash", &ledtrig_flash); + led_trigger_register_simple("torch", &ledtrig_torch); + return 0; +} +module_init(ledtrig_camera_init); + +static void __exit ledtrig_camera_exit(void) +{ + led_trigger_unregister_simple(ledtrig_torch); + led_trigger_unregister_simple(ledtrig_flash); +} +module_exit(ledtrig_camera_exit); + +MODULE_DESCRIPTION("LED Trigger for Camera Flash/Torch Control"); +MODULE_AUTHOR("Milo Kim"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/trigger/ledtrig-cpu.c b/drivers/leds/trigger/ledtrig-cpu.c new file mode 100644 index 000000000..f19baed61 --- /dev/null +++ b/drivers/leds/trigger/ledtrig-cpu.c @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ledtrig-cpu.c - LED trigger based on CPU activity + * + * This LED trigger will be registered for first 8 CPUs and named + * as cpu0..cpu7. There's additional trigger called cpu that + * is on when any CPU is active. + * + * If you want support for arbitrary number of CPUs, make it one trigger, + * with additional sysfs file selecting which CPU to watch. + * + * It can be bound to any LED just like other triggers using either a + * board file or via sysfs interface. + * + * An API named ledtrig_cpu is exported for any user, who want to add CPU + * activity indication in their code. + * + * Copyright 2011 Linus Walleij <linus.walleij@linaro.org> + * Copyright 2011 - 2012 Bryan Wu <bryan.wu@canonical.com> + */ + +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/slab.h> +#include <linux/percpu.h> +#include <linux/syscore_ops.h> +#include <linux/rwsem.h> +#include <linux/cpu.h> +#include "../leds.h" + +#define MAX_NAME_LEN 8 + +struct led_trigger_cpu { + bool is_active; + char name[MAX_NAME_LEN]; + struct led_trigger *_trig; +}; + +static DEFINE_PER_CPU(struct led_trigger_cpu, cpu_trig); + +static struct led_trigger *trig_cpu_all; +static atomic_t num_active_cpus = ATOMIC_INIT(0); + +/** + * ledtrig_cpu - emit a CPU event as a trigger + * @evt: CPU event to be emitted + * + * Emit a CPU event on a CPU core, which will trigger a + * bound LED to turn on or turn off. + */ +void ledtrig_cpu(enum cpu_led_event ledevt) +{ + struct led_trigger_cpu *trig = this_cpu_ptr(&cpu_trig); + bool is_active = trig->is_active; + + /* Locate the correct CPU LED */ + switch (ledevt) { + case CPU_LED_IDLE_END: + case CPU_LED_START: + /* Will turn the LED on, max brightness */ + is_active = true; + break; + + case CPU_LED_IDLE_START: + case CPU_LED_STOP: + case CPU_LED_HALTED: + /* Will turn the LED off */ + is_active = false; + break; + + default: + /* Will leave the LED as it is */ + break; + } + + if (is_active != trig->is_active) { + unsigned int active_cpus; + unsigned int total_cpus; + + /* Update trigger state */ + trig->is_active = is_active; + atomic_add(is_active ? 1 : -1, &num_active_cpus); + active_cpus = atomic_read(&num_active_cpus); + total_cpus = num_present_cpus(); + + led_trigger_event(trig->_trig, + is_active ? LED_FULL : LED_OFF); + + + led_trigger_event(trig_cpu_all, + DIV_ROUND_UP(LED_FULL * active_cpus, total_cpus)); + + } +} +EXPORT_SYMBOL(ledtrig_cpu); + +static int ledtrig_cpu_syscore_suspend(void) +{ + ledtrig_cpu(CPU_LED_STOP); + return 0; +} + +static void ledtrig_cpu_syscore_resume(void) +{ + ledtrig_cpu(CPU_LED_START); +} + +static void ledtrig_cpu_syscore_shutdown(void) +{ + ledtrig_cpu(CPU_LED_HALTED); +} + +static struct syscore_ops ledtrig_cpu_syscore_ops = { + .shutdown = ledtrig_cpu_syscore_shutdown, + .suspend = ledtrig_cpu_syscore_suspend, + .resume = ledtrig_cpu_syscore_resume, +}; + +static int ledtrig_online_cpu(unsigned int cpu) +{ + ledtrig_cpu(CPU_LED_START); + return 0; +} + +static int ledtrig_prepare_down_cpu(unsigned int cpu) +{ + ledtrig_cpu(CPU_LED_STOP); + return 0; +} + +static int __init ledtrig_cpu_init(void) +{ + unsigned int cpu; + int ret; + + /* Supports up to 9999 cpu cores */ + BUILD_BUG_ON(CONFIG_NR_CPUS > 9999); + + /* + * Registering a trigger for all CPUs. + */ + led_trigger_register_simple("cpu", &trig_cpu_all); + + /* + * Registering CPU led trigger for each CPU core here + * ignores CPU hotplug, but after this CPU hotplug works + * fine with this trigger. + */ + for_each_possible_cpu(cpu) { + struct led_trigger_cpu *trig = &per_cpu(cpu_trig, cpu); + + if (cpu >= 8) + continue; + + snprintf(trig->name, MAX_NAME_LEN, "cpu%u", cpu); + + led_trigger_register_simple(trig->name, &trig->_trig); + } + + register_syscore_ops(&ledtrig_cpu_syscore_ops); + + ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "leds/trigger:starting", + ledtrig_online_cpu, ledtrig_prepare_down_cpu); + if (ret < 0) + pr_err("CPU hotplug notifier for ledtrig-cpu could not be registered: %d\n", + ret); + + pr_info("ledtrig-cpu: registered to indicate activity on CPUs\n"); + + return 0; +} +device_initcall(ledtrig_cpu_init); diff --git a/drivers/leds/trigger/ledtrig-default-on.c b/drivers/leds/trigger/ledtrig-default-on.c new file mode 100644 index 000000000..8207f85ec --- /dev/null +++ b/drivers/leds/trigger/ledtrig-default-on.c @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED Kernel Default ON Trigger + * + * Copyright 2008 Nick Forbes <nick.forbes@incepta.com> + * + * Based on Richard Purdie's ledtrig-timer.c. + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/leds.h> +#include "../leds.h" + +static int defon_trig_activate(struct led_classdev *led_cdev) +{ + led_set_brightness_nosleep(led_cdev, led_cdev->max_brightness); + return 0; +} + +static struct led_trigger defon_led_trigger = { + .name = "default-on", + .activate = defon_trig_activate, +}; +module_led_trigger(defon_led_trigger); + +MODULE_AUTHOR("Nick Forbes <nick.forbes@incepta.com>"); +MODULE_DESCRIPTION("Default-ON LED trigger"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/trigger/ledtrig-disk.c b/drivers/leds/trigger/ledtrig-disk.c new file mode 100644 index 000000000..074191078 --- /dev/null +++ b/drivers/leds/trigger/ledtrig-disk.c @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED Disk Activity Trigger + * + * Copyright 2006 Openedhand Ltd. + * + * Author: Richard Purdie <rpurdie@openedhand.com> + */ + +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/leds.h> + +#define BLINK_DELAY 30 + +DEFINE_LED_TRIGGER(ledtrig_disk); +DEFINE_LED_TRIGGER(ledtrig_disk_read); +DEFINE_LED_TRIGGER(ledtrig_disk_write); +DEFINE_LED_TRIGGER(ledtrig_ide); + +void ledtrig_disk_activity(bool write) +{ + unsigned long blink_delay = BLINK_DELAY; + + led_trigger_blink_oneshot(ledtrig_disk, + &blink_delay, &blink_delay, 0); + led_trigger_blink_oneshot(ledtrig_ide, + &blink_delay, &blink_delay, 0); + if (write) + led_trigger_blink_oneshot(ledtrig_disk_write, + &blink_delay, &blink_delay, 0); + else + led_trigger_blink_oneshot(ledtrig_disk_read, + &blink_delay, &blink_delay, 0); +} +EXPORT_SYMBOL(ledtrig_disk_activity); + +static int __init ledtrig_disk_init(void) +{ + led_trigger_register_simple("disk-activity", &ledtrig_disk); + led_trigger_register_simple("disk-read", &ledtrig_disk_read); + led_trigger_register_simple("disk-write", &ledtrig_disk_write); + led_trigger_register_simple("ide-disk", &ledtrig_ide); + + return 0; +} +device_initcall(ledtrig_disk_init); diff --git a/drivers/leds/trigger/ledtrig-gpio.c b/drivers/leds/trigger/ledtrig-gpio.c new file mode 100644 index 000000000..0120faa3d --- /dev/null +++ b/drivers/leds/trigger/ledtrig-gpio.c @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ledtrig-gio.c - LED Trigger Based on GPIO events + * + * Copyright 2009 Felipe Balbi <me@felipebalbi.com> + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/gpio.h> +#include <linux/interrupt.h> +#include <linux/leds.h> +#include <linux/slab.h> +#include "../leds.h" + +struct gpio_trig_data { + struct led_classdev *led; + + unsigned desired_brightness; /* desired brightness when led is on */ + unsigned inverted; /* true when gpio is inverted */ + unsigned gpio; /* gpio that triggers the leds */ +}; + +static irqreturn_t gpio_trig_irq(int irq, void *_led) +{ + struct led_classdev *led = _led; + struct gpio_trig_data *gpio_data = led_get_trigger_data(led); + int tmp; + + tmp = gpio_get_value_cansleep(gpio_data->gpio); + if (gpio_data->inverted) + tmp = !tmp; + + if (tmp) { + if (gpio_data->desired_brightness) + led_set_brightness_nosleep(gpio_data->led, + gpio_data->desired_brightness); + else + led_set_brightness_nosleep(gpio_data->led, LED_FULL); + } else { + led_set_brightness_nosleep(gpio_data->led, LED_OFF); + } + + return IRQ_HANDLED; +} + +static ssize_t gpio_trig_brightness_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct gpio_trig_data *gpio_data = led_trigger_get_drvdata(dev); + + return sprintf(buf, "%u\n", gpio_data->desired_brightness); +} + +static ssize_t gpio_trig_brightness_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t n) +{ + struct gpio_trig_data *gpio_data = led_trigger_get_drvdata(dev); + unsigned desired_brightness; + int ret; + + ret = sscanf(buf, "%u", &desired_brightness); + if (ret < 1 || desired_brightness > 255) { + dev_err(dev, "invalid value\n"); + return -EINVAL; + } + + gpio_data->desired_brightness = desired_brightness; + + return n; +} +static DEVICE_ATTR(desired_brightness, 0644, gpio_trig_brightness_show, + gpio_trig_brightness_store); + +static ssize_t gpio_trig_inverted_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct gpio_trig_data *gpio_data = led_trigger_get_drvdata(dev); + + return sprintf(buf, "%u\n", gpio_data->inverted); +} + +static ssize_t gpio_trig_inverted_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t n) +{ + struct led_classdev *led = led_trigger_get_led(dev); + struct gpio_trig_data *gpio_data = led_trigger_get_drvdata(dev); + unsigned long inverted; + int ret; + + ret = kstrtoul(buf, 10, &inverted); + if (ret < 0) + return ret; + + if (inverted > 1) + return -EINVAL; + + gpio_data->inverted = inverted; + + /* After inverting, we need to update the LED. */ + if (gpio_is_valid(gpio_data->gpio)) + gpio_trig_irq(0, led); + + return n; +} +static DEVICE_ATTR(inverted, 0644, gpio_trig_inverted_show, + gpio_trig_inverted_store); + +static ssize_t gpio_trig_gpio_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct gpio_trig_data *gpio_data = led_trigger_get_drvdata(dev); + + return sprintf(buf, "%u\n", gpio_data->gpio); +} + +static ssize_t gpio_trig_gpio_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t n) +{ + struct led_classdev *led = led_trigger_get_led(dev); + struct gpio_trig_data *gpio_data = led_trigger_get_drvdata(dev); + unsigned gpio; + int ret; + + ret = sscanf(buf, "%u", &gpio); + if (ret < 1) { + dev_err(dev, "couldn't read gpio number\n"); + return -EINVAL; + } + + if (gpio_data->gpio == gpio) + return n; + + if (!gpio_is_valid(gpio)) { + if (gpio_is_valid(gpio_data->gpio)) + free_irq(gpio_to_irq(gpio_data->gpio), led); + gpio_data->gpio = gpio; + return n; + } + + ret = request_threaded_irq(gpio_to_irq(gpio), NULL, gpio_trig_irq, + IRQF_ONESHOT | IRQF_SHARED | IRQF_TRIGGER_RISING + | IRQF_TRIGGER_FALLING, "ledtrig-gpio", led); + if (ret) { + dev_err(dev, "request_irq failed with error %d\n", ret); + } else { + if (gpio_is_valid(gpio_data->gpio)) + free_irq(gpio_to_irq(gpio_data->gpio), led); + gpio_data->gpio = gpio; + /* After changing the GPIO, we need to update the LED. */ + gpio_trig_irq(0, led); + } + + return ret ? ret : n; +} +static DEVICE_ATTR(gpio, 0644, gpio_trig_gpio_show, gpio_trig_gpio_store); + +static struct attribute *gpio_trig_attrs[] = { + &dev_attr_desired_brightness.attr, + &dev_attr_inverted.attr, + &dev_attr_gpio.attr, + NULL +}; +ATTRIBUTE_GROUPS(gpio_trig); + +static int gpio_trig_activate(struct led_classdev *led) +{ + struct gpio_trig_data *gpio_data; + + gpio_data = kzalloc(sizeof(*gpio_data), GFP_KERNEL); + if (!gpio_data) + return -ENOMEM; + + gpio_data->led = led; + gpio_data->gpio = -ENOENT; + + led_set_trigger_data(led, gpio_data); + + return 0; +} + +static void gpio_trig_deactivate(struct led_classdev *led) +{ + struct gpio_trig_data *gpio_data = led_get_trigger_data(led); + + if (gpio_is_valid(gpio_data->gpio)) + free_irq(gpio_to_irq(gpio_data->gpio), led); + kfree(gpio_data); +} + +static struct led_trigger gpio_led_trigger = { + .name = "gpio", + .activate = gpio_trig_activate, + .deactivate = gpio_trig_deactivate, + .groups = gpio_trig_groups, +}; +module_led_trigger(gpio_led_trigger); + +MODULE_AUTHOR("Felipe Balbi <me@felipebalbi.com>"); +MODULE_DESCRIPTION("GPIO LED trigger"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/trigger/ledtrig-heartbeat.c b/drivers/leds/trigger/ledtrig-heartbeat.c new file mode 100644 index 000000000..36b6709af --- /dev/null +++ b/drivers/leds/trigger/ledtrig-heartbeat.c @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED Heartbeat Trigger + * + * Copyright (C) 2006 Atsushi Nemoto <anemo@mba.ocn.ne.jp> + * + * Based on Richard Purdie's ledtrig-timer.c and some arch's + * CONFIG_HEARTBEAT code. + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/slab.h> +#include <linux/timer.h> +#include <linux/sched.h> +#include <linux/sched/loadavg.h> +#include <linux/leds.h> +#include <linux/reboot.h> +#include "../leds.h" + +static int panic_heartbeats; + +struct heartbeat_trig_data { + struct led_classdev *led_cdev; + unsigned int phase; + unsigned int period; + struct timer_list timer; + unsigned int invert; +}; + +static void led_heartbeat_function(struct timer_list *t) +{ + struct heartbeat_trig_data *heartbeat_data = + from_timer(heartbeat_data, t, timer); + struct led_classdev *led_cdev; + unsigned long brightness = LED_OFF; + unsigned long delay = 0; + + led_cdev = heartbeat_data->led_cdev; + + if (unlikely(panic_heartbeats)) { + led_set_brightness_nosleep(led_cdev, LED_OFF); + return; + } + + if (test_and_clear_bit(LED_BLINK_BRIGHTNESS_CHANGE, &led_cdev->work_flags)) + led_cdev->blink_brightness = led_cdev->new_blink_brightness; + + /* acts like an actual heart beat -- ie thump-thump-pause... */ + switch (heartbeat_data->phase) { + case 0: + /* + * The hyperbolic function below modifies the + * heartbeat period length in dependency of the + * current (1min) load. It goes through the points + * f(0)=1260, f(1)=860, f(5)=510, f(inf)->300. + */ + heartbeat_data->period = 300 + + (6720 << FSHIFT) / (5 * avenrun[0] + (7 << FSHIFT)); + heartbeat_data->period = + msecs_to_jiffies(heartbeat_data->period); + delay = msecs_to_jiffies(70); + heartbeat_data->phase++; + if (!heartbeat_data->invert) + brightness = led_cdev->blink_brightness; + break; + case 1: + delay = heartbeat_data->period / 4 - msecs_to_jiffies(70); + heartbeat_data->phase++; + if (heartbeat_data->invert) + brightness = led_cdev->blink_brightness; + break; + case 2: + delay = msecs_to_jiffies(70); + heartbeat_data->phase++; + if (!heartbeat_data->invert) + brightness = led_cdev->blink_brightness; + break; + default: + delay = heartbeat_data->period - heartbeat_data->period / 4 - + msecs_to_jiffies(70); + heartbeat_data->phase = 0; + if (heartbeat_data->invert) + brightness = led_cdev->blink_brightness; + break; + } + + led_set_brightness_nosleep(led_cdev, brightness); + mod_timer(&heartbeat_data->timer, jiffies + delay); +} + +static ssize_t led_invert_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct heartbeat_trig_data *heartbeat_data = + led_trigger_get_drvdata(dev); + + return sprintf(buf, "%u\n", heartbeat_data->invert); +} + +static ssize_t led_invert_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct heartbeat_trig_data *heartbeat_data = + led_trigger_get_drvdata(dev); + unsigned long state; + int ret; + + ret = kstrtoul(buf, 0, &state); + if (ret) + return ret; + + heartbeat_data->invert = !!state; + + return size; +} + +static DEVICE_ATTR(invert, 0644, led_invert_show, led_invert_store); + +static struct attribute *heartbeat_trig_attrs[] = { + &dev_attr_invert.attr, + NULL +}; +ATTRIBUTE_GROUPS(heartbeat_trig); + +static int heartbeat_trig_activate(struct led_classdev *led_cdev) +{ + struct heartbeat_trig_data *heartbeat_data; + + heartbeat_data = kzalloc(sizeof(*heartbeat_data), GFP_KERNEL); + if (!heartbeat_data) + return -ENOMEM; + + led_set_trigger_data(led_cdev, heartbeat_data); + heartbeat_data->led_cdev = led_cdev; + + timer_setup(&heartbeat_data->timer, led_heartbeat_function, 0); + heartbeat_data->phase = 0; + if (!led_cdev->blink_brightness) + led_cdev->blink_brightness = led_cdev->max_brightness; + led_heartbeat_function(&heartbeat_data->timer); + set_bit(LED_BLINK_SW, &led_cdev->work_flags); + + return 0; +} + +static void heartbeat_trig_deactivate(struct led_classdev *led_cdev) +{ + struct heartbeat_trig_data *heartbeat_data = + led_get_trigger_data(led_cdev); + + del_timer_sync(&heartbeat_data->timer); + kfree(heartbeat_data); + clear_bit(LED_BLINK_SW, &led_cdev->work_flags); +} + +static struct led_trigger heartbeat_led_trigger = { + .name = "heartbeat", + .activate = heartbeat_trig_activate, + .deactivate = heartbeat_trig_deactivate, + .groups = heartbeat_trig_groups, +}; + +static int heartbeat_reboot_notifier(struct notifier_block *nb, + unsigned long code, void *unused) +{ + led_trigger_unregister(&heartbeat_led_trigger); + return NOTIFY_DONE; +} + +static int heartbeat_panic_notifier(struct notifier_block *nb, + unsigned long code, void *unused) +{ + panic_heartbeats = 1; + return NOTIFY_DONE; +} + +static struct notifier_block heartbeat_reboot_nb = { + .notifier_call = heartbeat_reboot_notifier, +}; + +static struct notifier_block heartbeat_panic_nb = { + .notifier_call = heartbeat_panic_notifier, +}; + +static int __init heartbeat_trig_init(void) +{ + int rc = led_trigger_register(&heartbeat_led_trigger); + + if (!rc) { + atomic_notifier_chain_register(&panic_notifier_list, + &heartbeat_panic_nb); + register_reboot_notifier(&heartbeat_reboot_nb); + } + return rc; +} + +static void __exit heartbeat_trig_exit(void) +{ + unregister_reboot_notifier(&heartbeat_reboot_nb); + atomic_notifier_chain_unregister(&panic_notifier_list, + &heartbeat_panic_nb); + led_trigger_unregister(&heartbeat_led_trigger); +} + +module_init(heartbeat_trig_init); +module_exit(heartbeat_trig_exit); + +MODULE_AUTHOR("Atsushi Nemoto <anemo@mba.ocn.ne.jp>"); +MODULE_DESCRIPTION("Heartbeat LED trigger"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/trigger/ledtrig-mtd.c b/drivers/leds/trigger/ledtrig-mtd.c new file mode 100644 index 000000000..8fa763c22 --- /dev/null +++ b/drivers/leds/trigger/ledtrig-mtd.c @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED MTD trigger + * + * Copyright 2016 Ezequiel Garcia <ezequiel@vanguardiasur.com.ar> + * + * Based on LED IDE-Disk Activity Trigger + * + * Copyright 2006 Openedhand Ltd. + * + * Author: Richard Purdie <rpurdie@openedhand.com> + */ + +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/leds.h> + +#define BLINK_DELAY 30 + +DEFINE_LED_TRIGGER(ledtrig_mtd); +DEFINE_LED_TRIGGER(ledtrig_nand); + +void ledtrig_mtd_activity(void) +{ + unsigned long blink_delay = BLINK_DELAY; + + led_trigger_blink_oneshot(ledtrig_mtd, + &blink_delay, &blink_delay, 0); + led_trigger_blink_oneshot(ledtrig_nand, + &blink_delay, &blink_delay, 0); +} +EXPORT_SYMBOL(ledtrig_mtd_activity); + +static int __init ledtrig_mtd_init(void) +{ + led_trigger_register_simple("mtd", &ledtrig_mtd); + led_trigger_register_simple("nand-disk", &ledtrig_nand); + + return 0; +} +device_initcall(ledtrig_mtd_init); diff --git a/drivers/leds/trigger/ledtrig-netdev.c b/drivers/leds/trigger/ledtrig-netdev.c new file mode 100644 index 000000000..f4d670ec3 --- /dev/null +++ b/drivers/leds/trigger/ledtrig-netdev.c @@ -0,0 +1,465 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright 2017 Ben Whitten <ben.whitten@gmail.com> +// Copyright 2007 Oliver Jowett <oliver@opencloud.com> +// +// LED Kernel Netdev Trigger +// +// Toggles the LED to reflect the link and traffic state of a named net device +// +// Derived from ledtrig-timer.c which is: +// Copyright 2005-2006 Openedhand Ltd. +// Author: Richard Purdie <rpurdie@openedhand.com> + +#include <linux/atomic.h> +#include <linux/ctype.h> +#include <linux/device.h> +#include <linux/init.h> +#include <linux/jiffies.h> +#include <linux/kernel.h> +#include <linux/leds.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/netdevice.h> +#include <linux/spinlock.h> +#include <linux/timer.h> +#include "../leds.h" + +/* + * Configurable sysfs attributes: + * + * device_name - network device name to monitor + * interval - duration of LED blink, in milliseconds + * link - LED's normal state reflects whether the link is up + * (has carrier) or not + * tx - LED blinks on transmitted data + * rx - LED blinks on receive data + * + */ + +struct led_netdev_data { + spinlock_t lock; + + struct delayed_work work; + struct notifier_block notifier; + + struct led_classdev *led_cdev; + struct net_device *net_dev; + + char device_name[IFNAMSIZ]; + atomic_t interval; + unsigned int last_activity; + + unsigned long mode; +#define NETDEV_LED_LINK 0 +#define NETDEV_LED_TX 1 +#define NETDEV_LED_RX 2 +#define NETDEV_LED_MODE_LINKUP 3 +}; + +enum netdev_led_attr { + NETDEV_ATTR_LINK, + NETDEV_ATTR_TX, + NETDEV_ATTR_RX +}; + +static void set_baseline_state(struct led_netdev_data *trigger_data) +{ + int current_brightness; + struct led_classdev *led_cdev = trigger_data->led_cdev; + + current_brightness = led_cdev->brightness; + if (current_brightness) + led_cdev->blink_brightness = current_brightness; + if (!led_cdev->blink_brightness) + led_cdev->blink_brightness = led_cdev->max_brightness; + + if (!test_bit(NETDEV_LED_MODE_LINKUP, &trigger_data->mode)) + led_set_brightness(led_cdev, LED_OFF); + else { + if (test_bit(NETDEV_LED_LINK, &trigger_data->mode)) + led_set_brightness(led_cdev, + led_cdev->blink_brightness); + else + led_set_brightness(led_cdev, LED_OFF); + + /* If we are looking for RX/TX start periodically + * checking stats + */ + if (test_bit(NETDEV_LED_TX, &trigger_data->mode) || + test_bit(NETDEV_LED_RX, &trigger_data->mode)) + schedule_delayed_work(&trigger_data->work, 0); + } +} + +static ssize_t device_name_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_netdev_data *trigger_data = led_trigger_get_drvdata(dev); + ssize_t len; + + spin_lock_bh(&trigger_data->lock); + len = sprintf(buf, "%s\n", trigger_data->device_name); + spin_unlock_bh(&trigger_data->lock); + + return len; +} + +static ssize_t device_name_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t size) +{ + struct led_netdev_data *trigger_data = led_trigger_get_drvdata(dev); + + if (size >= IFNAMSIZ) + return -EINVAL; + + cancel_delayed_work_sync(&trigger_data->work); + + spin_lock_bh(&trigger_data->lock); + + if (trigger_data->net_dev) { + dev_put(trigger_data->net_dev); + trigger_data->net_dev = NULL; + } + + memcpy(trigger_data->device_name, buf, size); + trigger_data->device_name[size] = 0; + if (size > 0 && trigger_data->device_name[size - 1] == '\n') + trigger_data->device_name[size - 1] = 0; + + if (trigger_data->device_name[0] != 0) + trigger_data->net_dev = + dev_get_by_name(&init_net, trigger_data->device_name); + + clear_bit(NETDEV_LED_MODE_LINKUP, &trigger_data->mode); + if (trigger_data->net_dev != NULL) + if (netif_carrier_ok(trigger_data->net_dev)) + set_bit(NETDEV_LED_MODE_LINKUP, &trigger_data->mode); + + trigger_data->last_activity = 0; + + set_baseline_state(trigger_data); + spin_unlock_bh(&trigger_data->lock); + + return size; +} + +static DEVICE_ATTR_RW(device_name); + +static ssize_t netdev_led_attr_show(struct device *dev, char *buf, + enum netdev_led_attr attr) +{ + struct led_netdev_data *trigger_data = led_trigger_get_drvdata(dev); + int bit; + + switch (attr) { + case NETDEV_ATTR_LINK: + bit = NETDEV_LED_LINK; + break; + case NETDEV_ATTR_TX: + bit = NETDEV_LED_TX; + break; + case NETDEV_ATTR_RX: + bit = NETDEV_LED_RX; + break; + default: + return -EINVAL; + } + + return sprintf(buf, "%u\n", test_bit(bit, &trigger_data->mode)); +} + +static ssize_t netdev_led_attr_store(struct device *dev, const char *buf, + size_t size, enum netdev_led_attr attr) +{ + struct led_netdev_data *trigger_data = led_trigger_get_drvdata(dev); + unsigned long state; + int ret; + int bit; + + ret = kstrtoul(buf, 0, &state); + if (ret) + return ret; + + switch (attr) { + case NETDEV_ATTR_LINK: + bit = NETDEV_LED_LINK; + break; + case NETDEV_ATTR_TX: + bit = NETDEV_LED_TX; + break; + case NETDEV_ATTR_RX: + bit = NETDEV_LED_RX; + break; + default: + return -EINVAL; + } + + cancel_delayed_work_sync(&trigger_data->work); + + if (state) + set_bit(bit, &trigger_data->mode); + else + clear_bit(bit, &trigger_data->mode); + + set_baseline_state(trigger_data); + + return size; +} + +static ssize_t link_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return netdev_led_attr_show(dev, buf, NETDEV_ATTR_LINK); +} + +static ssize_t link_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + return netdev_led_attr_store(dev, buf, size, NETDEV_ATTR_LINK); +} + +static DEVICE_ATTR_RW(link); + +static ssize_t tx_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return netdev_led_attr_show(dev, buf, NETDEV_ATTR_TX); +} + +static ssize_t tx_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + return netdev_led_attr_store(dev, buf, size, NETDEV_ATTR_TX); +} + +static DEVICE_ATTR_RW(tx); + +static ssize_t rx_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return netdev_led_attr_show(dev, buf, NETDEV_ATTR_RX); +} + +static ssize_t rx_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + return netdev_led_attr_store(dev, buf, size, NETDEV_ATTR_RX); +} + +static DEVICE_ATTR_RW(rx); + +static ssize_t interval_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_netdev_data *trigger_data = led_trigger_get_drvdata(dev); + + return sprintf(buf, "%u\n", + jiffies_to_msecs(atomic_read(&trigger_data->interval))); +} + +static ssize_t interval_store(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t size) +{ + struct led_netdev_data *trigger_data = led_trigger_get_drvdata(dev); + unsigned long value; + int ret; + + ret = kstrtoul(buf, 0, &value); + if (ret) + return ret; + + /* impose some basic bounds on the timer interval */ + if (value >= 5 && value <= 10000) { + cancel_delayed_work_sync(&trigger_data->work); + + atomic_set(&trigger_data->interval, msecs_to_jiffies(value)); + set_baseline_state(trigger_data); /* resets timer */ + } + + return size; +} + +static DEVICE_ATTR_RW(interval); + +static struct attribute *netdev_trig_attrs[] = { + &dev_attr_device_name.attr, + &dev_attr_link.attr, + &dev_attr_rx.attr, + &dev_attr_tx.attr, + &dev_attr_interval.attr, + NULL +}; +ATTRIBUTE_GROUPS(netdev_trig); + +static int netdev_trig_notify(struct notifier_block *nb, + unsigned long evt, void *dv) +{ + struct net_device *dev = + netdev_notifier_info_to_dev((struct netdev_notifier_info *)dv); + struct led_netdev_data *trigger_data = + container_of(nb, struct led_netdev_data, notifier); + + if (evt != NETDEV_UP && evt != NETDEV_DOWN && evt != NETDEV_CHANGE + && evt != NETDEV_REGISTER && evt != NETDEV_UNREGISTER + && evt != NETDEV_CHANGENAME) + return NOTIFY_DONE; + + if (!(dev == trigger_data->net_dev || + (evt == NETDEV_CHANGENAME && !strcmp(dev->name, trigger_data->device_name)) || + (evt == NETDEV_REGISTER && !strcmp(dev->name, trigger_data->device_name)))) + return NOTIFY_DONE; + + cancel_delayed_work_sync(&trigger_data->work); + + spin_lock_bh(&trigger_data->lock); + + clear_bit(NETDEV_LED_MODE_LINKUP, &trigger_data->mode); + switch (evt) { + case NETDEV_CHANGENAME: + if (netif_carrier_ok(dev)) + set_bit(NETDEV_LED_MODE_LINKUP, &trigger_data->mode); + fallthrough; + case NETDEV_REGISTER: + if (trigger_data->net_dev) + dev_put(trigger_data->net_dev); + dev_hold(dev); + trigger_data->net_dev = dev; + break; + case NETDEV_UNREGISTER: + dev_put(trigger_data->net_dev); + trigger_data->net_dev = NULL; + break; + case NETDEV_UP: + case NETDEV_CHANGE: + if (netif_carrier_ok(dev)) + set_bit(NETDEV_LED_MODE_LINKUP, &trigger_data->mode); + break; + } + + set_baseline_state(trigger_data); + + spin_unlock_bh(&trigger_data->lock); + + return NOTIFY_DONE; +} + +/* here's the real work! */ +static void netdev_trig_work(struct work_struct *work) +{ + struct led_netdev_data *trigger_data = + container_of(work, struct led_netdev_data, work.work); + struct rtnl_link_stats64 *dev_stats; + unsigned int new_activity; + struct rtnl_link_stats64 temp; + unsigned long interval; + int invert; + + /* If we dont have a device, insure we are off */ + if (!trigger_data->net_dev) { + led_set_brightness(trigger_data->led_cdev, LED_OFF); + return; + } + + /* If we are not looking for RX/TX then return */ + if (!test_bit(NETDEV_LED_TX, &trigger_data->mode) && + !test_bit(NETDEV_LED_RX, &trigger_data->mode)) + return; + + dev_stats = dev_get_stats(trigger_data->net_dev, &temp); + new_activity = + (test_bit(NETDEV_LED_TX, &trigger_data->mode) ? + dev_stats->tx_packets : 0) + + (test_bit(NETDEV_LED_RX, &trigger_data->mode) ? + dev_stats->rx_packets : 0); + + if (trigger_data->last_activity != new_activity) { + led_stop_software_blink(trigger_data->led_cdev); + + invert = test_bit(NETDEV_LED_LINK, &trigger_data->mode); + interval = jiffies_to_msecs( + atomic_read(&trigger_data->interval)); + /* base state is ON (link present) */ + led_blink_set_oneshot(trigger_data->led_cdev, + &interval, + &interval, + invert); + trigger_data->last_activity = new_activity; + } + + schedule_delayed_work(&trigger_data->work, + (atomic_read(&trigger_data->interval)*2)); +} + +static int netdev_trig_activate(struct led_classdev *led_cdev) +{ + struct led_netdev_data *trigger_data; + int rc; + + trigger_data = kzalloc(sizeof(struct led_netdev_data), GFP_KERNEL); + if (!trigger_data) + return -ENOMEM; + + spin_lock_init(&trigger_data->lock); + + trigger_data->notifier.notifier_call = netdev_trig_notify; + trigger_data->notifier.priority = 10; + + INIT_DELAYED_WORK(&trigger_data->work, netdev_trig_work); + + trigger_data->led_cdev = led_cdev; + trigger_data->net_dev = NULL; + trigger_data->device_name[0] = 0; + + trigger_data->mode = 0; + atomic_set(&trigger_data->interval, msecs_to_jiffies(50)); + trigger_data->last_activity = 0; + + led_set_trigger_data(led_cdev, trigger_data); + + rc = register_netdevice_notifier(&trigger_data->notifier); + if (rc) + kfree(trigger_data); + + return rc; +} + +static void netdev_trig_deactivate(struct led_classdev *led_cdev) +{ + struct led_netdev_data *trigger_data = led_get_trigger_data(led_cdev); + + unregister_netdevice_notifier(&trigger_data->notifier); + + cancel_delayed_work_sync(&trigger_data->work); + + if (trigger_data->net_dev) + dev_put(trigger_data->net_dev); + + kfree(trigger_data); +} + +static struct led_trigger netdev_led_trigger = { + .name = "netdev", + .activate = netdev_trig_activate, + .deactivate = netdev_trig_deactivate, + .groups = netdev_trig_groups, +}; + +static int __init netdev_trig_init(void) +{ + return led_trigger_register(&netdev_led_trigger); +} + +static void __exit netdev_trig_exit(void) +{ + led_trigger_unregister(&netdev_led_trigger); +} + +module_init(netdev_trig_init); +module_exit(netdev_trig_exit); + +MODULE_AUTHOR("Ben Whitten <ben.whitten@gmail.com>"); +MODULE_AUTHOR("Oliver Jowett <oliver@opencloud.com>"); +MODULE_DESCRIPTION("Netdev LED trigger"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/trigger/ledtrig-oneshot.c b/drivers/leds/trigger/ledtrig-oneshot.c new file mode 100644 index 000000000..bee3bd452 --- /dev/null +++ b/drivers/leds/trigger/ledtrig-oneshot.c @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * One-shot LED Trigger + * + * Copyright 2012, Fabio Baltieri <fabio.baltieri@gmail.com> + * + * Based on ledtrig-timer.c by Richard Purdie <rpurdie@openedhand.com> + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/device.h> +#include <linux/ctype.h> +#include <linux/slab.h> +#include <linux/leds.h> +#include "../leds.h" + +#define DEFAULT_DELAY 100 + +struct oneshot_trig_data { + unsigned int invert; +}; + +static ssize_t led_shot(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct led_classdev *led_cdev = led_trigger_get_led(dev); + struct oneshot_trig_data *oneshot_data = led_trigger_get_drvdata(dev); + + led_blink_set_oneshot(led_cdev, + &led_cdev->blink_delay_on, &led_cdev->blink_delay_off, + oneshot_data->invert); + + /* content is ignored */ + return size; +} +static ssize_t led_invert_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct oneshot_trig_data *oneshot_data = led_trigger_get_drvdata(dev); + + return sprintf(buf, "%u\n", oneshot_data->invert); +} + +static ssize_t led_invert_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct led_classdev *led_cdev = led_trigger_get_led(dev); + struct oneshot_trig_data *oneshot_data = led_trigger_get_drvdata(dev); + unsigned long state; + int ret; + + ret = kstrtoul(buf, 0, &state); + if (ret) + return ret; + + oneshot_data->invert = !!state; + + if (oneshot_data->invert) + led_set_brightness_nosleep(led_cdev, LED_FULL); + else + led_set_brightness_nosleep(led_cdev, LED_OFF); + + return size; +} + +static ssize_t led_delay_on_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = led_trigger_get_led(dev); + + return sprintf(buf, "%lu\n", led_cdev->blink_delay_on); +} + +static ssize_t led_delay_on_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct led_classdev *led_cdev = led_trigger_get_led(dev); + unsigned long state; + int ret; + + ret = kstrtoul(buf, 0, &state); + if (ret) + return ret; + + led_cdev->blink_delay_on = state; + + return size; +} + +static ssize_t led_delay_off_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = led_trigger_get_led(dev); + + return sprintf(buf, "%lu\n", led_cdev->blink_delay_off); +} + +static ssize_t led_delay_off_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct led_classdev *led_cdev = led_trigger_get_led(dev); + unsigned long state; + int ret; + + ret = kstrtoul(buf, 0, &state); + if (ret) + return ret; + + led_cdev->blink_delay_off = state; + + return size; +} + +static DEVICE_ATTR(delay_on, 0644, led_delay_on_show, led_delay_on_store); +static DEVICE_ATTR(delay_off, 0644, led_delay_off_show, led_delay_off_store); +static DEVICE_ATTR(invert, 0644, led_invert_show, led_invert_store); +static DEVICE_ATTR(shot, 0200, NULL, led_shot); + +static struct attribute *oneshot_trig_attrs[] = { + &dev_attr_delay_on.attr, + &dev_attr_delay_off.attr, + &dev_attr_invert.attr, + &dev_attr_shot.attr, + NULL +}; +ATTRIBUTE_GROUPS(oneshot_trig); + +static void pattern_init(struct led_classdev *led_cdev) +{ + u32 *pattern; + unsigned int size = 0; + + pattern = led_get_default_pattern(led_cdev, &size); + if (!pattern) + goto out_default; + + if (size != 2) { + dev_warn(led_cdev->dev, + "Expected 2 but got %u values for delays pattern\n", + size); + goto out_default; + } + + led_cdev->blink_delay_on = pattern[0]; + led_cdev->blink_delay_off = pattern[1]; + kfree(pattern); + + return; + +out_default: + kfree(pattern); + led_cdev->blink_delay_on = DEFAULT_DELAY; + led_cdev->blink_delay_off = DEFAULT_DELAY; +} + +static int oneshot_trig_activate(struct led_classdev *led_cdev) +{ + struct oneshot_trig_data *oneshot_data; + + oneshot_data = kzalloc(sizeof(*oneshot_data), GFP_KERNEL); + if (!oneshot_data) + return -ENOMEM; + + led_set_trigger_data(led_cdev, oneshot_data); + + if (led_cdev->flags & LED_INIT_DEFAULT_TRIGGER) { + pattern_init(led_cdev); + /* + * Mark as initialized even on pattern_init() error because + * any consecutive call to it would produce the same error. + */ + led_cdev->flags &= ~LED_INIT_DEFAULT_TRIGGER; + } + + return 0; +} + +static void oneshot_trig_deactivate(struct led_classdev *led_cdev) +{ + struct oneshot_trig_data *oneshot_data = led_get_trigger_data(led_cdev); + + kfree(oneshot_data); + + /* Stop blinking */ + led_set_brightness(led_cdev, LED_OFF); +} + +static struct led_trigger oneshot_led_trigger = { + .name = "oneshot", + .activate = oneshot_trig_activate, + .deactivate = oneshot_trig_deactivate, + .groups = oneshot_trig_groups, +}; +module_led_trigger(oneshot_led_trigger); + +MODULE_AUTHOR("Fabio Baltieri <fabio.baltieri@gmail.com>"); +MODULE_DESCRIPTION("One-shot LED trigger"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/trigger/ledtrig-panic.c b/drivers/leds/trigger/ledtrig-panic.c new file mode 100644 index 000000000..5751cd032 --- /dev/null +++ b/drivers/leds/trigger/ledtrig-panic.c @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Kernel Panic LED Trigger + * + * Copyright 2016 Ezequiel Garcia <ezequiel@vanguardiasur.com.ar> + */ + +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/notifier.h> +#include <linux/leds.h> +#include "../leds.h" + +static struct led_trigger *trigger; + +/* + * This is called in a special context by the atomic panic + * notifier. This means the trigger can be changed without + * worrying about locking. + */ +static void led_trigger_set_panic(struct led_classdev *led_cdev) +{ + struct led_trigger *trig; + + list_for_each_entry(trig, &trigger_list, next_trig) { + if (strcmp("panic", trig->name)) + continue; + if (led_cdev->trigger) + list_del(&led_cdev->trig_list); + list_add_tail(&led_cdev->trig_list, &trig->led_cdevs); + + /* Avoid the delayed blink path */ + led_cdev->blink_delay_on = 0; + led_cdev->blink_delay_off = 0; + + led_cdev->trigger = trig; + if (trig->activate) + trig->activate(led_cdev); + break; + } +} + +static int led_trigger_panic_notifier(struct notifier_block *nb, + unsigned long code, void *unused) +{ + struct led_classdev *led_cdev; + + list_for_each_entry(led_cdev, &leds_list, node) + if (led_cdev->flags & LED_PANIC_INDICATOR) + led_trigger_set_panic(led_cdev); + return NOTIFY_DONE; +} + +static struct notifier_block led_trigger_panic_nb = { + .notifier_call = led_trigger_panic_notifier, +}; + +static long led_panic_blink(int state) +{ + led_trigger_event(trigger, state ? LED_FULL : LED_OFF); + return 0; +} + +static int __init ledtrig_panic_init(void) +{ + atomic_notifier_chain_register(&panic_notifier_list, + &led_trigger_panic_nb); + + led_trigger_register_simple("panic", &trigger); + panic_blink = led_panic_blink; + return 0; +} +device_initcall(ledtrig_panic_init); diff --git a/drivers/leds/trigger/ledtrig-pattern.c b/drivers/leds/trigger/ledtrig-pattern.c new file mode 100644 index 000000000..4d138d531 --- /dev/null +++ b/drivers/leds/trigger/ledtrig-pattern.c @@ -0,0 +1,463 @@ +// SPDX-License-Identifier: GPL-2.0 + +/* + * LED pattern trigger + * + * Idea discussed with Pavel Machek. Raphael Teysseyre implemented + * the first version, Baolin Wang simplified and improved the approach. + */ + +#include <linux/kernel.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/slab.h> +#include <linux/timer.h> + +#define MAX_PATTERNS 1024 +/* + * When doing gradual dimming, the led brightness will be updated + * every 50 milliseconds. + */ +#define UPDATE_INTERVAL 50 + +struct pattern_trig_data { + struct led_classdev *led_cdev; + struct led_pattern patterns[MAX_PATTERNS]; + struct led_pattern *curr; + struct led_pattern *next; + struct mutex lock; + u32 npatterns; + int repeat; + int last_repeat; + int delta_t; + bool is_indefinite; + bool is_hw_pattern; + struct timer_list timer; +}; + +static void pattern_trig_update_patterns(struct pattern_trig_data *data) +{ + data->curr = data->next; + if (!data->is_indefinite && data->curr == data->patterns) + data->repeat--; + + if (data->next == data->patterns + data->npatterns - 1) + data->next = data->patterns; + else + data->next++; + + data->delta_t = 0; +} + +static int pattern_trig_compute_brightness(struct pattern_trig_data *data) +{ + int step_brightness; + + /* + * If current tuple's duration is less than the dimming interval, + * we should treat it as a step change of brightness instead of + * doing gradual dimming. + */ + if (data->delta_t == 0 || data->curr->delta_t < UPDATE_INTERVAL) + return data->curr->brightness; + + step_brightness = abs(data->next->brightness - data->curr->brightness); + step_brightness = data->delta_t * step_brightness / data->curr->delta_t; + + if (data->next->brightness > data->curr->brightness) + return data->curr->brightness + step_brightness; + else + return data->curr->brightness - step_brightness; +} + +static void pattern_trig_timer_function(struct timer_list *t) +{ + struct pattern_trig_data *data = from_timer(data, t, timer); + + for (;;) { + if (!data->is_indefinite && !data->repeat) + break; + + if (data->curr->brightness == data->next->brightness) { + /* Step change of brightness */ + led_set_brightness(data->led_cdev, + data->curr->brightness); + mod_timer(&data->timer, + jiffies + msecs_to_jiffies(data->curr->delta_t)); + if (!data->next->delta_t) { + /* Skip the tuple with zero duration */ + pattern_trig_update_patterns(data); + } + /* Select next tuple */ + pattern_trig_update_patterns(data); + } else { + /* Gradual dimming */ + + /* + * If the accumulation time is larger than current + * tuple's duration, we should go next one and re-check + * if we repeated done. + */ + if (data->delta_t > data->curr->delta_t) { + pattern_trig_update_patterns(data); + continue; + } + + led_set_brightness(data->led_cdev, + pattern_trig_compute_brightness(data)); + mod_timer(&data->timer, + jiffies + msecs_to_jiffies(UPDATE_INTERVAL)); + + /* Accumulate the gradual dimming time */ + data->delta_t += UPDATE_INTERVAL; + } + + break; + } +} + +static int pattern_trig_start_pattern(struct led_classdev *led_cdev) +{ + struct pattern_trig_data *data = led_cdev->trigger_data; + + if (!data->npatterns) + return 0; + + if (data->is_hw_pattern) { + return led_cdev->pattern_set(led_cdev, data->patterns, + data->npatterns, data->repeat); + } + + /* At least 2 tuples for software pattern. */ + if (data->npatterns < 2) + return -EINVAL; + + data->delta_t = 0; + data->curr = data->patterns; + data->next = data->patterns + 1; + data->timer.expires = jiffies; + add_timer(&data->timer); + + return 0; +} + +static ssize_t repeat_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct pattern_trig_data *data = led_cdev->trigger_data; + int repeat; + + mutex_lock(&data->lock); + + repeat = data->last_repeat; + + mutex_unlock(&data->lock); + + return scnprintf(buf, PAGE_SIZE, "%d\n", repeat); +} + +static ssize_t repeat_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct pattern_trig_data *data = led_cdev->trigger_data; + int err, res; + + err = kstrtos32(buf, 10, &res); + if (err) + return err; + + /* Number 0 and negative numbers except -1 are invalid. */ + if (res < -1 || res == 0) + return -EINVAL; + + mutex_lock(&data->lock); + + del_timer_sync(&data->timer); + + if (data->is_hw_pattern) + led_cdev->pattern_clear(led_cdev); + + data->last_repeat = data->repeat = res; + /* -1 means repeat indefinitely */ + if (data->repeat == -1) + data->is_indefinite = true; + else + data->is_indefinite = false; + + err = pattern_trig_start_pattern(led_cdev); + + mutex_unlock(&data->lock); + return err < 0 ? err : count; +} + +static DEVICE_ATTR_RW(repeat); + +static ssize_t pattern_trig_show_patterns(struct pattern_trig_data *data, + char *buf, bool hw_pattern) +{ + ssize_t count = 0; + int i; + + mutex_lock(&data->lock); + + if (!data->npatterns || (data->is_hw_pattern ^ hw_pattern)) + goto out; + + for (i = 0; i < data->npatterns; i++) { + count += scnprintf(buf + count, PAGE_SIZE - count, + "%d %u ", + data->patterns[i].brightness, + data->patterns[i].delta_t); + } + + buf[count - 1] = '\n'; + +out: + mutex_unlock(&data->lock); + return count; +} + +static int pattern_trig_store_patterns_string(struct pattern_trig_data *data, + const char *buf, size_t count) +{ + int ccount, cr, offset = 0; + + while (offset < count - 1 && data->npatterns < MAX_PATTERNS) { + cr = 0; + ccount = sscanf(buf + offset, "%u %u %n", + &data->patterns[data->npatterns].brightness, + &data->patterns[data->npatterns].delta_t, &cr); + + if (ccount != 2 || + data->patterns[data->npatterns].brightness > data->led_cdev->max_brightness) { + data->npatterns = 0; + return -EINVAL; + } + + offset += cr; + data->npatterns++; + } + + return 0; +} + +static int pattern_trig_store_patterns_int(struct pattern_trig_data *data, + const u32 *buf, size_t count) +{ + unsigned int i; + + for (i = 0; i < count; i += 2) { + data->patterns[data->npatterns].brightness = buf[i]; + data->patterns[data->npatterns].delta_t = buf[i + 1]; + data->npatterns++; + } + + return 0; +} + +static ssize_t pattern_trig_store_patterns(struct led_classdev *led_cdev, + const char *buf, const u32 *buf_int, + size_t count, bool hw_pattern) +{ + struct pattern_trig_data *data = led_cdev->trigger_data; + int err = 0; + + mutex_lock(&data->lock); + + del_timer_sync(&data->timer); + + if (data->is_hw_pattern) + led_cdev->pattern_clear(led_cdev); + + data->is_hw_pattern = hw_pattern; + data->npatterns = 0; + + if (buf) + err = pattern_trig_store_patterns_string(data, buf, count); + else + err = pattern_trig_store_patterns_int(data, buf_int, count); + if (err) + goto out; + + err = pattern_trig_start_pattern(led_cdev); + if (err) + data->npatterns = 0; + +out: + mutex_unlock(&data->lock); + return err < 0 ? err : count; +} + +static ssize_t pattern_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct pattern_trig_data *data = led_cdev->trigger_data; + + return pattern_trig_show_patterns(data, buf, false); +} + +static ssize_t pattern_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + + return pattern_trig_store_patterns(led_cdev, buf, NULL, count, false); +} + +static DEVICE_ATTR_RW(pattern); + +static ssize_t hw_pattern_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + struct pattern_trig_data *data = led_cdev->trigger_data; + + return pattern_trig_show_patterns(data, buf, true); +} + +static ssize_t hw_pattern_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + + return pattern_trig_store_patterns(led_cdev, buf, NULL, count, true); +} + +static DEVICE_ATTR_RW(hw_pattern); + +static umode_t pattern_trig_attrs_mode(struct kobject *kobj, + struct attribute *attr, int index) +{ + struct device *dev = container_of(kobj, struct device, kobj); + struct led_classdev *led_cdev = dev_get_drvdata(dev); + + if (attr == &dev_attr_repeat.attr || attr == &dev_attr_pattern.attr) + return attr->mode; + else if (attr == &dev_attr_hw_pattern.attr && led_cdev->pattern_set) + return attr->mode; + + return 0; +} + +static struct attribute *pattern_trig_attrs[] = { + &dev_attr_pattern.attr, + &dev_attr_hw_pattern.attr, + &dev_attr_repeat.attr, + NULL +}; + +static const struct attribute_group pattern_trig_group = { + .attrs = pattern_trig_attrs, + .is_visible = pattern_trig_attrs_mode, +}; + +static const struct attribute_group *pattern_trig_groups[] = { + &pattern_trig_group, + NULL, +}; + +static void pattern_init(struct led_classdev *led_cdev) +{ + unsigned int size = 0; + u32 *pattern; + int err; + + pattern = led_get_default_pattern(led_cdev, &size); + if (!pattern) + return; + + if (size % 2) { + dev_warn(led_cdev->dev, "Expected pattern of tuples\n"); + goto out; + } + + err = pattern_trig_store_patterns(led_cdev, NULL, pattern, size, false); + if (err < 0) + dev_warn(led_cdev->dev, + "Pattern initialization failed with error %d\n", err); + +out: + kfree(pattern); +} + +static int pattern_trig_activate(struct led_classdev *led_cdev) +{ + struct pattern_trig_data *data; + + data = kzalloc(sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + if (!!led_cdev->pattern_set ^ !!led_cdev->pattern_clear) { + dev_warn(led_cdev->dev, + "Hardware pattern ops validation failed\n"); + led_cdev->pattern_set = NULL; + led_cdev->pattern_clear = NULL; + } + + data->is_indefinite = true; + data->last_repeat = -1; + mutex_init(&data->lock); + data->led_cdev = led_cdev; + led_set_trigger_data(led_cdev, data); + timer_setup(&data->timer, pattern_trig_timer_function, 0); + led_cdev->activated = true; + + if (led_cdev->flags & LED_INIT_DEFAULT_TRIGGER) { + pattern_init(led_cdev); + /* + * Mark as initialized even on pattern_init() error because + * any consecutive call to it would produce the same error. + */ + led_cdev->flags &= ~LED_INIT_DEFAULT_TRIGGER; + } + + return 0; +} + +static void pattern_trig_deactivate(struct led_classdev *led_cdev) +{ + struct pattern_trig_data *data = led_cdev->trigger_data; + + if (!led_cdev->activated) + return; + + if (led_cdev->pattern_clear) + led_cdev->pattern_clear(led_cdev); + + del_timer_sync(&data->timer); + + led_set_brightness(led_cdev, LED_OFF); + kfree(data); + led_cdev->activated = false; +} + +static struct led_trigger pattern_led_trigger = { + .name = "pattern", + .activate = pattern_trig_activate, + .deactivate = pattern_trig_deactivate, + .groups = pattern_trig_groups, +}; + +static int __init pattern_trig_init(void) +{ + return led_trigger_register(&pattern_led_trigger); +} + +static void __exit pattern_trig_exit(void) +{ + led_trigger_unregister(&pattern_led_trigger); +} + +module_init(pattern_trig_init); +module_exit(pattern_trig_exit); + +MODULE_AUTHOR("Raphael Teysseyre <rteysseyre@gmail.com>"); +MODULE_AUTHOR("Baolin Wang <baolin.wang@linaro.org>"); +MODULE_DESCRIPTION("LED Pattern trigger"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/trigger/ledtrig-timer.c b/drivers/leds/trigger/ledtrig-timer.c new file mode 100644 index 000000000..b4688d1d9 --- /dev/null +++ b/drivers/leds/trigger/ledtrig-timer.c @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * LED Kernel Timer Trigger + * + * Copyright 2005-2006 Openedhand Ltd. + * + * Author: Richard Purdie <rpurdie@openedhand.com> + */ + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/device.h> +#include <linux/ctype.h> +#include <linux/slab.h> +#include <linux/leds.h> + +static ssize_t led_delay_on_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = led_trigger_get_led(dev); + + return sprintf(buf, "%lu\n", led_cdev->blink_delay_on); +} + +static ssize_t led_delay_on_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct led_classdev *led_cdev = led_trigger_get_led(dev); + unsigned long state; + ssize_t ret; + + ret = kstrtoul(buf, 10, &state); + if (ret) + return ret; + + led_blink_set(led_cdev, &state, &led_cdev->blink_delay_off); + led_cdev->blink_delay_on = state; + + return size; +} + +static ssize_t led_delay_off_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = led_trigger_get_led(dev); + + return sprintf(buf, "%lu\n", led_cdev->blink_delay_off); +} + +static ssize_t led_delay_off_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct led_classdev *led_cdev = led_trigger_get_led(dev); + unsigned long state; + ssize_t ret; + + ret = kstrtoul(buf, 10, &state); + if (ret) + return ret; + + led_blink_set(led_cdev, &led_cdev->blink_delay_on, &state); + led_cdev->blink_delay_off = state; + + return size; +} + +static DEVICE_ATTR(delay_on, 0644, led_delay_on_show, led_delay_on_store); +static DEVICE_ATTR(delay_off, 0644, led_delay_off_show, led_delay_off_store); + +static struct attribute *timer_trig_attrs[] = { + &dev_attr_delay_on.attr, + &dev_attr_delay_off.attr, + NULL +}; +ATTRIBUTE_GROUPS(timer_trig); + +static void pattern_init(struct led_classdev *led_cdev) +{ + u32 *pattern; + unsigned int size = 0; + + pattern = led_get_default_pattern(led_cdev, &size); + if (!pattern) + return; + + if (size != 2) { + dev_warn(led_cdev->dev, + "Expected 2 but got %u values for delays pattern\n", + size); + goto out; + } + + led_cdev->blink_delay_on = pattern[0]; + led_cdev->blink_delay_off = pattern[1]; + /* led_blink_set() called by caller */ + +out: + kfree(pattern); +} + +static int timer_trig_activate(struct led_classdev *led_cdev) +{ + if (led_cdev->flags & LED_INIT_DEFAULT_TRIGGER) { + pattern_init(led_cdev); + /* + * Mark as initialized even on pattern_init() error because + * any consecutive call to it would produce the same error. + */ + led_cdev->flags &= ~LED_INIT_DEFAULT_TRIGGER; + } + + /* + * If "set brightness to 0" is pending in workqueue, we don't + * want that to be reordered after blink_set() + */ + flush_work(&led_cdev->set_brightness_work); + led_blink_set(led_cdev, &led_cdev->blink_delay_on, + &led_cdev->blink_delay_off); + + return 0; +} + +static void timer_trig_deactivate(struct led_classdev *led_cdev) +{ + /* Stop blinking */ + led_set_brightness(led_cdev, LED_OFF); +} + +static struct led_trigger timer_led_trigger = { + .name = "timer", + .activate = timer_trig_activate, + .deactivate = timer_trig_deactivate, + .groups = timer_trig_groups, +}; +module_led_trigger(timer_led_trigger); + +MODULE_AUTHOR("Richard Purdie <rpurdie@openedhand.com>"); +MODULE_DESCRIPTION("Timer LED trigger"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/trigger/ledtrig-transient.c b/drivers/leds/trigger/ledtrig-transient.c new file mode 100644 index 000000000..80635183f --- /dev/null +++ b/drivers/leds/trigger/ledtrig-transient.c @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// LED Kernel Transient Trigger +// +// Transient trigger allows one shot timer activation. Please refer to +// Documentation/leds/ledtrig-transient.rst for details +// Copyright (C) 2012 Shuah Khan <shuahkhan@gmail.com> +// +// Based on Richard Purdie's ledtrig-timer.c and Atsushi Nemoto's +// ledtrig-heartbeat.c +// Design and use-case input from Jonas Bonn <jonas@southpole.se> and +// Neil Brown <neilb@suse.de> + +#include <linux/module.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/device.h> +#include <linux/slab.h> +#include <linux/timer.h> +#include <linux/leds.h> +#include "../leds.h" + +struct transient_trig_data { + int activate; + int state; + int restore_state; + unsigned long duration; + struct timer_list timer; + struct led_classdev *led_cdev; +}; + +static void transient_timer_function(struct timer_list *t) +{ + struct transient_trig_data *transient_data = + from_timer(transient_data, t, timer); + struct led_classdev *led_cdev = transient_data->led_cdev; + + transient_data->activate = 0; + led_set_brightness_nosleep(led_cdev, transient_data->restore_state); +} + +static ssize_t transient_activate_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct transient_trig_data *transient_data = + led_trigger_get_drvdata(dev); + + return sprintf(buf, "%d\n", transient_data->activate); +} + +static ssize_t transient_activate_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct led_classdev *led_cdev = led_trigger_get_led(dev); + struct transient_trig_data *transient_data = + led_trigger_get_drvdata(dev); + unsigned long state; + ssize_t ret; + + ret = kstrtoul(buf, 10, &state); + if (ret) + return ret; + + if (state != 1 && state != 0) + return -EINVAL; + + /* cancel the running timer */ + if (state == 0 && transient_data->activate == 1) { + del_timer(&transient_data->timer); + transient_data->activate = state; + led_set_brightness_nosleep(led_cdev, + transient_data->restore_state); + return size; + } + + /* start timer if there is no active timer */ + if (state == 1 && transient_data->activate == 0 && + transient_data->duration != 0) { + transient_data->activate = state; + led_set_brightness_nosleep(led_cdev, transient_data->state); + transient_data->restore_state = + (transient_data->state == LED_FULL) ? LED_OFF : LED_FULL; + mod_timer(&transient_data->timer, + jiffies + msecs_to_jiffies(transient_data->duration)); + } + + /* state == 0 && transient_data->activate == 0 + timer is not active - just return */ + /* state == 1 && transient_data->activate == 1 + timer is already active - just return */ + + return size; +} + +static ssize_t transient_duration_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct transient_trig_data *transient_data = led_trigger_get_drvdata(dev); + + return sprintf(buf, "%lu\n", transient_data->duration); +} + +static ssize_t transient_duration_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct transient_trig_data *transient_data = + led_trigger_get_drvdata(dev); + unsigned long state; + ssize_t ret; + + ret = kstrtoul(buf, 10, &state); + if (ret) + return ret; + + transient_data->duration = state; + return size; +} + +static ssize_t transient_state_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct transient_trig_data *transient_data = + led_trigger_get_drvdata(dev); + int state; + + state = (transient_data->state == LED_FULL) ? 1 : 0; + return sprintf(buf, "%d\n", state); +} + +static ssize_t transient_state_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t size) +{ + struct transient_trig_data *transient_data = + led_trigger_get_drvdata(dev); + unsigned long state; + ssize_t ret; + + ret = kstrtoul(buf, 10, &state); + if (ret) + return ret; + + if (state != 1 && state != 0) + return -EINVAL; + + transient_data->state = (state == 1) ? LED_FULL : LED_OFF; + return size; +} + +static DEVICE_ATTR(activate, 0644, transient_activate_show, + transient_activate_store); +static DEVICE_ATTR(duration, 0644, transient_duration_show, + transient_duration_store); +static DEVICE_ATTR(state, 0644, transient_state_show, transient_state_store); + +static struct attribute *transient_trig_attrs[] = { + &dev_attr_activate.attr, + &dev_attr_duration.attr, + &dev_attr_state.attr, + NULL +}; +ATTRIBUTE_GROUPS(transient_trig); + +static int transient_trig_activate(struct led_classdev *led_cdev) +{ + struct transient_trig_data *tdata; + + tdata = kzalloc(sizeof(struct transient_trig_data), GFP_KERNEL); + if (!tdata) + return -ENOMEM; + + led_set_trigger_data(led_cdev, tdata); + tdata->led_cdev = led_cdev; + + timer_setup(&tdata->timer, transient_timer_function, 0); + + return 0; +} + +static void transient_trig_deactivate(struct led_classdev *led_cdev) +{ + struct transient_trig_data *transient_data = led_get_trigger_data(led_cdev); + + del_timer_sync(&transient_data->timer); + led_set_brightness_nosleep(led_cdev, transient_data->restore_state); + kfree(transient_data); +} + +static struct led_trigger transient_trigger = { + .name = "transient", + .activate = transient_trig_activate, + .deactivate = transient_trig_deactivate, + .groups = transient_trig_groups, +}; +module_led_trigger(transient_trigger); + +MODULE_AUTHOR("Shuah Khan <shuahkhan@gmail.com>"); +MODULE_DESCRIPTION("Transient LED trigger"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/leds/uleds.c b/drivers/leds/uleds.c new file mode 100644 index 000000000..7320337b2 --- /dev/null +++ b/drivers/leds/uleds.c @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Userspace driver for the LED subsystem + * + * Copyright (C) 2016 David Lechner <david@lechnology.com> + * + * Based on uinput.c: Aristeu Sergio Rozanski Filho <aris@cathedrallabs.org> + */ +#include <linux/fs.h> +#include <linux/init.h> +#include <linux/leds.h> +#include <linux/miscdevice.h> +#include <linux/module.h> +#include <linux/poll.h> +#include <linux/sched.h> +#include <linux/slab.h> + +#include <uapi/linux/uleds.h> + +#define ULEDS_NAME "uleds" + +enum uleds_state { + ULEDS_STATE_UNKNOWN, + ULEDS_STATE_REGISTERED, +}; + +struct uleds_device { + struct uleds_user_dev user_dev; + struct led_classdev led_cdev; + struct mutex mutex; + enum uleds_state state; + wait_queue_head_t waitq; + int brightness; + bool new_data; +}; + +static struct miscdevice uleds_misc; + +static void uleds_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct uleds_device *udev = container_of(led_cdev, struct uleds_device, + led_cdev); + + if (udev->brightness != brightness) { + udev->brightness = brightness; + udev->new_data = true; + wake_up_interruptible(&udev->waitq); + } +} + +static int uleds_open(struct inode *inode, struct file *file) +{ + struct uleds_device *udev; + + udev = kzalloc(sizeof(*udev), GFP_KERNEL); + if (!udev) + return -ENOMEM; + + udev->led_cdev.name = udev->user_dev.name; + udev->led_cdev.brightness_set = uleds_brightness_set; + + mutex_init(&udev->mutex); + init_waitqueue_head(&udev->waitq); + udev->state = ULEDS_STATE_UNKNOWN; + + file->private_data = udev; + stream_open(inode, file); + + return 0; +} + +static ssize_t uleds_write(struct file *file, const char __user *buffer, + size_t count, loff_t *ppos) +{ + struct uleds_device *udev = file->private_data; + const char *name; + int ret; + + if (count == 0) + return 0; + + ret = mutex_lock_interruptible(&udev->mutex); + if (ret) + return ret; + + if (udev->state == ULEDS_STATE_REGISTERED) { + ret = -EBUSY; + goto out; + } + + if (count != sizeof(struct uleds_user_dev)) { + ret = -EINVAL; + goto out; + } + + if (copy_from_user(&udev->user_dev, buffer, + sizeof(struct uleds_user_dev))) { + ret = -EFAULT; + goto out; + } + + name = udev->user_dev.name; + if (!name[0] || !strcmp(name, ".") || !strcmp(name, "..") || + strchr(name, '/')) { + ret = -EINVAL; + goto out; + } + + if (udev->user_dev.max_brightness <= 0) { + ret = -EINVAL; + goto out; + } + udev->led_cdev.max_brightness = udev->user_dev.max_brightness; + + ret = devm_led_classdev_register(uleds_misc.this_device, + &udev->led_cdev); + if (ret < 0) + goto out; + + udev->new_data = true; + udev->state = ULEDS_STATE_REGISTERED; + ret = count; + +out: + mutex_unlock(&udev->mutex); + + return ret; +} + +static ssize_t uleds_read(struct file *file, char __user *buffer, size_t count, + loff_t *ppos) +{ + struct uleds_device *udev = file->private_data; + ssize_t retval; + + if (count < sizeof(udev->brightness)) + return 0; + + do { + retval = mutex_lock_interruptible(&udev->mutex); + if (retval) + return retval; + + if (udev->state != ULEDS_STATE_REGISTERED) { + retval = -ENODEV; + } else if (!udev->new_data && (file->f_flags & O_NONBLOCK)) { + retval = -EAGAIN; + } else if (udev->new_data) { + retval = copy_to_user(buffer, &udev->brightness, + sizeof(udev->brightness)); + udev->new_data = false; + retval = sizeof(udev->brightness); + } + + mutex_unlock(&udev->mutex); + + if (retval) + break; + + if (!(file->f_flags & O_NONBLOCK)) + retval = wait_event_interruptible(udev->waitq, + udev->new_data || + udev->state != ULEDS_STATE_REGISTERED); + } while (retval == 0); + + return retval; +} + +static __poll_t uleds_poll(struct file *file, poll_table *wait) +{ + struct uleds_device *udev = file->private_data; + + poll_wait(file, &udev->waitq, wait); + + if (udev->new_data) + return EPOLLIN | EPOLLRDNORM; + + return 0; +} + +static int uleds_release(struct inode *inode, struct file *file) +{ + struct uleds_device *udev = file->private_data; + + if (udev->state == ULEDS_STATE_REGISTERED) { + udev->state = ULEDS_STATE_UNKNOWN; + devm_led_classdev_unregister(uleds_misc.this_device, + &udev->led_cdev); + } + kfree(udev); + + return 0; +} + +static const struct file_operations uleds_fops = { + .owner = THIS_MODULE, + .open = uleds_open, + .release = uleds_release, + .read = uleds_read, + .write = uleds_write, + .poll = uleds_poll, + .llseek = no_llseek, +}; + +static struct miscdevice uleds_misc = { + .fops = &uleds_fops, + .minor = MISC_DYNAMIC_MINOR, + .name = ULEDS_NAME, +}; + +static int __init uleds_init(void) +{ + return misc_register(&uleds_misc); +} +module_init(uleds_init); + +static void __exit uleds_exit(void) +{ + misc_deregister(&uleds_misc); +} +module_exit(uleds_exit); + +MODULE_AUTHOR("David Lechner <david@lechnology.com>"); +MODULE_DESCRIPTION("Userspace driver for the LED subsystem"); +MODULE_LICENSE("GPL"); |