xref: /linux/drivers/video/backlight/cgbc_bl.c (revision ca220141fa8ebae09765a242076b2b77338106b0)
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