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
cgbc_bl_read_brightness(struct cgbc_bl_data * bl_data)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
cgbc_bl_update_status(struct backlight_device * bl)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
cgbc_bl_get_brightness(struct backlight_device * bl)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
cgbc_bl_probe(struct platform_device * pdev)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