1 // SPDX-License-Identifier: GPL-2.0-only 2 /* 3 * PWM-based multi-color LED control 4 * 5 * Copyright 2022 Sven Schwermer <sven.schwermer@disruptive-technologies.com> 6 */ 7 8 #include <linux/err.h> 9 #include <linux/kernel.h> 10 #include <linux/led-class-multicolor.h> 11 #include <linux/leds.h> 12 #include <linux/mod_devicetable.h> 13 #include <linux/module.h> 14 #include <linux/mutex.h> 15 #include <linux/platform_device.h> 16 #include <linux/property.h> 17 #include <linux/pwm.h> 18 19 struct pwm_led { 20 struct pwm_device *pwm; 21 struct pwm_state state; 22 bool active_low; 23 }; 24 25 struct pwm_mc_led { 26 struct led_classdev_mc mc_cdev; 27 struct mutex lock; 28 struct pwm_led leds[]; 29 }; 30 31 static int led_pwm_mc_set(struct led_classdev *cdev, 32 enum led_brightness brightness) 33 { 34 struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev); 35 struct pwm_mc_led *priv = container_of(mc_cdev, struct pwm_mc_led, mc_cdev); 36 unsigned long long duty; 37 int ret = 0; 38 int i; 39 40 led_mc_calc_color_components(mc_cdev, brightness); 41 42 mutex_lock(&priv->lock); 43 44 for (i = 0; i < mc_cdev->num_colors; i++) { 45 duty = priv->leds[i].state.period; 46 duty *= mc_cdev->subled_info[i].brightness; 47 do_div(duty, cdev->max_brightness); 48 49 if (priv->leds[i].active_low) 50 duty = priv->leds[i].state.period - duty; 51 52 priv->leds[i].state.duty_cycle = duty; 53 /* 54 * Disabling a PWM doesn't guarantee that it emits the inactive level. 55 * So keep it on. Only for suspending the PWM should be disabled because 56 * otherwise it refuses to suspend. The possible downside is that the 57 * LED might stay (or even go) on. 58 */ 59 priv->leds[i].state.enabled = !(cdev->flags & LED_SUSPENDED); 60 ret = pwm_apply_might_sleep(priv->leds[i].pwm, 61 &priv->leds[i].state); 62 if (ret) 63 break; 64 } 65 66 mutex_unlock(&priv->lock); 67 68 return ret; 69 } 70 71 static int iterate_subleds(struct device *dev, struct pwm_mc_led *priv, 72 struct fwnode_handle *mcnode) 73 { 74 struct mc_subled *subled = priv->mc_cdev.subled_info; 75 struct fwnode_handle *fwnode; 76 struct pwm_led *pwmled; 77 u32 color; 78 int ret; 79 80 /* iterate over the nodes inside the multi-led node */ 81 fwnode_for_each_child_node(mcnode, fwnode) { 82 pwmled = &priv->leds[priv->mc_cdev.num_colors]; 83 pwmled->pwm = devm_fwnode_pwm_get(dev, fwnode, NULL); 84 if (IS_ERR(pwmled->pwm)) { 85 ret = dev_err_probe(dev, PTR_ERR(pwmled->pwm), "unable to request PWM\n"); 86 goto release_fwnode; 87 } 88 pwm_init_state(pwmled->pwm, &pwmled->state); 89 pwmled->active_low = fwnode_property_read_bool(fwnode, "active-low"); 90 91 ret = fwnode_property_read_u32(fwnode, "color", &color); 92 if (ret) { 93 dev_err(dev, "cannot read color: %d\n", ret); 94 goto release_fwnode; 95 } 96 97 subled[priv->mc_cdev.num_colors].color_index = color; 98 priv->mc_cdev.num_colors++; 99 } 100 101 return 0; 102 103 release_fwnode: 104 fwnode_handle_put(fwnode); 105 return ret; 106 } 107 108 static int led_pwm_mc_probe(struct platform_device *pdev) 109 { 110 struct fwnode_handle *mcnode; 111 struct led_init_data init_data = {}; 112 struct led_classdev *cdev; 113 struct mc_subled *subled; 114 struct pwm_mc_led *priv; 115 unsigned int count; 116 int ret = 0; 117 118 mcnode = device_get_named_child_node(&pdev->dev, "multi-led"); 119 if (!mcnode) 120 return dev_err_probe(&pdev->dev, -ENODEV, 121 "expected multi-led node\n"); 122 123 /* count the nodes inside the multi-led node */ 124 count = fwnode_get_child_node_count(mcnode); 125 126 priv = devm_kzalloc(&pdev->dev, struct_size(priv, leds, count), 127 GFP_KERNEL); 128 if (!priv) { 129 ret = -ENOMEM; 130 goto release_mcnode; 131 } 132 mutex_init(&priv->lock); 133 134 subled = devm_kcalloc(&pdev->dev, count, sizeof(*subled), GFP_KERNEL); 135 if (!subled) { 136 ret = -ENOMEM; 137 goto release_mcnode; 138 } 139 priv->mc_cdev.subled_info = subled; 140 141 /* init the multicolor's LED class device */ 142 cdev = &priv->mc_cdev.led_cdev; 143 ret = fwnode_property_read_u32(mcnode, "max-brightness", 144 &cdev->max_brightness); 145 if (ret) 146 goto release_mcnode; 147 148 cdev->flags = LED_CORE_SUSPENDRESUME; 149 cdev->brightness_set_blocking = led_pwm_mc_set; 150 151 ret = iterate_subleds(&pdev->dev, priv, mcnode); 152 if (ret) 153 goto release_mcnode; 154 155 init_data.fwnode = mcnode; 156 ret = devm_led_classdev_multicolor_register_ext(&pdev->dev, 157 &priv->mc_cdev, 158 &init_data); 159 if (ret) { 160 dev_err(&pdev->dev, 161 "failed to register multicolor PWM led for %s: %d\n", 162 cdev->name, ret); 163 goto release_mcnode; 164 } 165 166 ret = led_pwm_mc_set(cdev, cdev->brightness); 167 if (ret) 168 return dev_err_probe(&pdev->dev, ret, 169 "failed to set led PWM value for %s\n", 170 cdev->name); 171 172 platform_set_drvdata(pdev, priv); 173 return 0; 174 175 release_mcnode: 176 fwnode_handle_put(mcnode); 177 return ret; 178 } 179 180 static const struct of_device_id of_pwm_leds_mc_match[] = { 181 { .compatible = "pwm-leds-multicolor", }, 182 {} 183 }; 184 MODULE_DEVICE_TABLE(of, of_pwm_leds_mc_match); 185 186 static struct platform_driver led_pwm_mc_driver = { 187 .probe = led_pwm_mc_probe, 188 .driver = { 189 .name = "leds_pwm_multicolor", 190 .of_match_table = of_pwm_leds_mc_match, 191 }, 192 }; 193 module_platform_driver(led_pwm_mc_driver); 194 195 MODULE_AUTHOR("Sven Schwermer <sven.schwermer@disruptive-technologies.com>"); 196 MODULE_DESCRIPTION("multi-color PWM LED driver"); 197 MODULE_LICENSE("GPL v2"); 198 MODULE_ALIAS("platform:leds-pwm-multicolor"); 199