1 // SPDX-License-Identifier: GPL-2.0-only 2 /* 3 * Congatec Board Controller (CGBC) Backlight Driver 4 * 5 * This driver provides backlight control for LCD displays connected to 6 * Congatec boards via the CGBC (Congatec Board Controller). It integrates 7 * with the Linux backlight subsystem and communicates with hardware through 8 * the cgbc-core module. 9 * 10 * Copyright (C) 2025 Novatron Oy 11 * 12 * Author: Petri Karhula <petri.karhula@novatron.fi> 13 */ 14 15 #include <linux/backlight.h> 16 #include <linux/bitfield.h> 17 #include <linux/bits.h> 18 #include <linux/mfd/cgbc.h> 19 #include <linux/module.h> 20 #include <linux/platform_device.h> 21 22 #define BLT_PWM_DUTY_MASK GENMASK(6, 0) 23 24 /* CGBC command for PWM brightness control*/ 25 #define CGBC_CMD_BLT0_PWM 0x75 26 27 #define CGBC_BL_MAX_BRIGHTNESS 100 28 29 /** 30 * CGBC backlight driver data 31 * @dev: Pointer to the platform device 32 * @cgbc: Pointer to the parent CGBC device data 33 * @current_brightness: Current brightness level (0-100) 34 */ 35 struct cgbc_bl_data { 36 struct device *dev; 37 struct cgbc_device_data *cgbc; 38 unsigned int current_brightness; 39 }; 40 41 static int cgbc_bl_read_brightness(struct cgbc_bl_data *bl_data) 42 { 43 u8 cmd_buf[4] = { CGBC_CMD_BLT0_PWM }; 44 u8 reply_buf[3]; 45 int ret; 46 47 ret = cgbc_command(bl_data->cgbc, cmd_buf, sizeof(cmd_buf), 48 reply_buf, sizeof(reply_buf), NULL); 49 if (ret < 0) 50 return ret; 51 52 /* 53 * Get only PWM duty factor percentage, 54 * ignore polarity inversion bit (bit 7) 55 */ 56 bl_data->current_brightness = FIELD_GET(BLT_PWM_DUTY_MASK, reply_buf[0]); 57 58 return 0; 59 } 60 61 static int cgbc_bl_update_status(struct backlight_device *bl) 62 { 63 struct cgbc_bl_data *bl_data = bl_get_data(bl); 64 u8 cmd_buf[4] = { CGBC_CMD_BLT0_PWM }; 65 u8 reply_buf[3]; 66 u8 brightness; 67 int ret; 68 69 brightness = backlight_get_brightness(bl); 70 71 if (brightness != bl_data->current_brightness) { 72 /* Read the current values */ 73 ret = cgbc_command(bl_data->cgbc, cmd_buf, sizeof(cmd_buf), reply_buf, 74 sizeof(reply_buf), NULL); 75 if (ret < 0) { 76 dev_err(bl_data->dev, "Failed to read PWM settings: %d\n", ret); 77 return ret; 78 } 79 80 /* 81 * Prepare command buffer for writing new settings. Only 2nd byte is changed 82 * to set new brightness (PWM duty cycle %). Other values (polarity, frequency) 83 * are preserved from the read values. 84 */ 85 cmd_buf[1] = (reply_buf[0] & ~BLT_PWM_DUTY_MASK) | 86 FIELD_PREP(BLT_PWM_DUTY_MASK, brightness); 87 cmd_buf[2] = reply_buf[1]; 88 cmd_buf[3] = reply_buf[2]; 89 90 ret = cgbc_command(bl_data->cgbc, cmd_buf, sizeof(cmd_buf), reply_buf, 91 sizeof(reply_buf), NULL); 92 if (ret < 0) { 93 dev_err(bl_data->dev, "Failed to set brightness: %d\n", ret); 94 return ret; 95 } 96 97 bl_data->current_brightness = reply_buf[0] & BLT_PWM_DUTY_MASK; 98 99 /* Verify the setting was applied correctly */ 100 if (bl_data->current_brightness != brightness) { 101 dev_err(bl_data->dev, 102 "Brightness setting verification failed (got %u, expected %u)\n", 103 bl_data->current_brightness, (unsigned int)brightness); 104 return -EIO; 105 } 106 } 107 108 return 0; 109 } 110 111 static int cgbc_bl_get_brightness(struct backlight_device *bl) 112 { 113 struct cgbc_bl_data *bl_data = bl_get_data(bl); 114 int ret; 115 116 ret = cgbc_bl_read_brightness(bl_data); 117 if (ret < 0) { 118 dev_err(bl_data->dev, "Failed to read brightness: %d\n", ret); 119 return ret; 120 } 121 122 return bl_data->current_brightness; 123 } 124 125 static const struct backlight_ops cgbc_bl_ops = { 126 .options = BL_CORE_SUSPENDRESUME, 127 .update_status = cgbc_bl_update_status, 128 .get_brightness = cgbc_bl_get_brightness, 129 }; 130 131 static int cgbc_bl_probe(struct platform_device *pdev) 132 { 133 struct cgbc_device_data *cgbc = dev_get_drvdata(pdev->dev.parent); 134 struct backlight_properties props = { }; 135 struct backlight_device *bl_dev; 136 struct cgbc_bl_data *bl_data; 137 int ret; 138 139 bl_data = devm_kzalloc(&pdev->dev, sizeof(*bl_data), GFP_KERNEL); 140 if (!bl_data) 141 return -ENOMEM; 142 143 bl_data->dev = &pdev->dev; 144 bl_data->cgbc = cgbc; 145 146 ret = cgbc_bl_read_brightness(bl_data); 147 if (ret < 0) 148 return dev_err_probe(&pdev->dev, ret, 149 "Failed to read initial brightness\n"); 150 151 props.type = BACKLIGHT_PLATFORM; 152 props.max_brightness = CGBC_BL_MAX_BRIGHTNESS; 153 props.brightness = bl_data->current_brightness; 154 props.scale = BACKLIGHT_SCALE_LINEAR; 155 156 bl_dev = devm_backlight_device_register(&pdev->dev, "cgbc-backlight", 157 &pdev->dev, bl_data, 158 &cgbc_bl_ops, &props); 159 if (IS_ERR(bl_dev)) 160 return dev_err_probe(&pdev->dev, PTR_ERR(bl_dev), 161 "Failed to register backlight device\n"); 162 163 platform_set_drvdata(pdev, bl_data); 164 165 return 0; 166 } 167 168 static struct platform_driver cgbc_bl_driver = { 169 .driver = { 170 .name = "cgbc-backlight", 171 }, 172 .probe = cgbc_bl_probe, 173 }; 174 175 module_platform_driver(cgbc_bl_driver); 176 177 MODULE_AUTHOR("Petri Karhula <petri.karhula@novatron.fi>"); 178 MODULE_DESCRIPTION("Congatec Board Controller (CGBC) Backlight Driver"); 179 MODULE_LICENSE("GPL"); 180 MODULE_ALIAS("platform:cgbc-backlight"); 181