1 /*
2 * Copyright (c) 2023 Jan Henke <Jan.Henke@taujhe.de>
3 *
4 * SPDX-License-Identifier: Apache-2.0
5 */
6
7 #define DT_DRV_COMPAT sparkfun_serlcd
8
9 #include <stdlib.h>
10 #include <string.h>
11 #include <zephyr/device.h>
12 #include <zephyr/devicetree.h>
13 #include <zephyr/drivers/i2c.h>
14 #include <zephyr/drivers/auxdisplay.h>
15 #include <zephyr/kernel.h>
16 #include <zephyr/sys/util.h>
17 #include <zephyr/logging/log.h>
18
19 LOG_MODULE_REGISTER(auxdisplay_serlcd, CONFIG_AUXDISPLAY_LOG_LEVEL);
20
21 /*
22 * | in ASCII, used to begin a display command
23 */
24 #define SERLCD_BEGIN_COMMAND 0x7C
25
26 /*
27 * special command for the underlying display controller
28 */
29 #define SERLCD_BEGIN_SPECIAL_COMMAND 0xFE
30
31 /*
32 * maximum amount of custom chars the display supports
33 */
34 #define SERLCD_CUSTOM_CHAR_MAX_COUNT 8
35
36 /*
37 * height of a custom char in bits
38 */
39 #define SERLCD_CUSTOM_CHAR_HEIGHT 8
40
41 /*
42 * width of a custom char in bits
43 */
44 #define SERLCD_CUSTOM_CHAR_WIDTH 5
45
46 /*
47 * char code for the first custom char
48 */
49 #define SERLCD_CUSTOM_CHAR_INDEX_BASE 0x08
50
51 /*
52 * bitmask for custom character detection
53 */
54 #define SERLCD_CUSTOM_CHAR_BITMASK 0xf8
55
56 /*
57 * bit to set in the display control special command to indicate the display should be powered on
58 */
59 #define SERLCD_DISPLAY_CONTROL_POWER_BIT BIT(2)
60
61 /*
62 * bit to set in the display control special command to indicate the cursor should be displayed
63 */
64 #define SERLCD_DISPLAY_CONTROL_CURSOR_BIT BIT(1)
65
66 /*
67 * bit to set in the display control special command to indicate the cursor should be blinking
68 */
69 #define SERLCD_DISPLAY_CONTROL_BLINKING_BIT BIT(0)
70
71 struct auxdisplay_serlcd_data {
72 bool power;
73 bool cursor;
74 bool blinking;
75 uint16_t cursor_x;
76 uint16_t cursor_y;
77 };
78
79 struct auxdisplay_serlcd_config {
80 struct auxdisplay_capabilities capabilities;
81 struct i2c_dt_spec bus;
82 uint16_t command_delay_ms;
83 uint16_t special_command_delay_ms;
84 };
85
86 enum auxdisplay_serlcd_command {
87 SERLCD_COMMAND_SET_CUSTOM_CHAR = 0x1B,
88 SERLCD_COMMAND_WRITE_CUSTOM_CHAR = 0x23,
89 SERLCD_COMMAND_CLEAR = 0x2D,
90 };
91
92 enum auxdisplay_serlcd_special_command {
93 SERLCD_SPECIAL_RETURN_HOME = 0x02,
94 SERLCD_SPECIAL_DISPLAY_CONTROL = 0x08,
95 SERLCD_SPECIAL_SET_DD_RAM_ADDRESS = 0x80,
96 };
97
auxdisplay_serlcd_send_command(const struct device * dev,const enum auxdisplay_serlcd_command command)98 static int auxdisplay_serlcd_send_command(const struct device *dev,
99 const enum auxdisplay_serlcd_command command)
100 {
101 const struct auxdisplay_serlcd_config *config = dev->config;
102 const uint8_t buffer[2] = {SERLCD_BEGIN_COMMAND, command};
103
104 int rc = i2c_write_dt(&config->bus, buffer, sizeof(buffer));
105
106 k_sleep(K_MSEC(config->command_delay_ms));
107 return rc;
108 }
109
110 static int
auxdisplay_serlcd_send_special_command(const struct device * dev,const enum auxdisplay_serlcd_special_command command)111 auxdisplay_serlcd_send_special_command(const struct device *dev,
112 const enum auxdisplay_serlcd_special_command command)
113 {
114 const struct auxdisplay_serlcd_config *config = dev->config;
115 const uint8_t buffer[2] = {SERLCD_BEGIN_SPECIAL_COMMAND, command};
116
117 int rc = i2c_write_dt(&config->bus, buffer, sizeof(buffer));
118
119 k_sleep(K_MSEC(config->special_command_delay_ms));
120 return rc;
121 }
122
auxdisplay_serlcd_send_display_state(const struct device * dev,const struct auxdisplay_serlcd_data * data)123 static int auxdisplay_serlcd_send_display_state(const struct device *dev,
124 const struct auxdisplay_serlcd_data *data)
125 {
126 uint8_t command = SERLCD_SPECIAL_DISPLAY_CONTROL;
127
128 if (data->power) {
129 command |= SERLCD_DISPLAY_CONTROL_POWER_BIT;
130 }
131 if (data->cursor) {
132 command |= SERLCD_DISPLAY_CONTROL_CURSOR_BIT;
133 }
134 if (data->blinking) {
135 command |= SERLCD_DISPLAY_CONTROL_BLINKING_BIT;
136 }
137
138 return auxdisplay_serlcd_send_special_command(dev, command);
139 }
140
auxdisplay_serlcd_display_on(const struct device * dev)141 static int auxdisplay_serlcd_display_on(const struct device *dev)
142 {
143 struct auxdisplay_serlcd_data *data = dev->data;
144
145 data->power = true;
146
147 return auxdisplay_serlcd_send_display_state(dev, data);
148 }
149
auxdisplay_serlcd_display_off(const struct device * dev)150 static int auxdisplay_serlcd_display_off(const struct device *dev)
151 {
152 struct auxdisplay_serlcd_data *data = dev->data;
153
154 data->power = false;
155
156 return auxdisplay_serlcd_send_display_state(dev, data);
157 }
158
auxdisplay_serlcd_cursor_set_enabled(const struct device * dev,bool enable)159 static int auxdisplay_serlcd_cursor_set_enabled(const struct device *dev, bool enable)
160 {
161 struct auxdisplay_serlcd_data *data = dev->data;
162
163 data->cursor = enable;
164
165 return auxdisplay_serlcd_send_display_state(dev, data);
166 }
167
auxdisplay_serlcd_position_blinking_set_enabled(const struct device * dev,bool enable)168 static int auxdisplay_serlcd_position_blinking_set_enabled(const struct device *dev, bool enable)
169 {
170 struct auxdisplay_serlcd_data *data = dev->data;
171
172 data->blinking = enable;
173
174 return auxdisplay_serlcd_send_display_state(dev, data);
175 }
176
auxdisplay_serlcd_cursor_position_set(const struct device * dev,enum auxdisplay_position type,int16_t x,int16_t y)177 static int auxdisplay_serlcd_cursor_position_set(const struct device *dev,
178 enum auxdisplay_position type, int16_t x,
179 int16_t y)
180 {
181 static const uint8_t row_offsets[] = {0x00, 0x40, 0x14, 0x54};
182
183 const struct auxdisplay_serlcd_config *config = dev->config;
184 const struct auxdisplay_capabilities capabilities = config->capabilities;
185 const uint16_t columns = capabilities.columns;
186 const uint16_t rows = capabilities.rows;
187 struct auxdisplay_serlcd_data *data = dev->data;
188
189 if (type == AUXDISPLAY_POSITION_ABSOLUTE) {
190 /*
191 * shortcut for (0,0) position
192 */
193 if (x == 0 && y == 0) {
194 data->cursor_x = x;
195 data->cursor_y = y;
196 return auxdisplay_serlcd_send_special_command(dev,
197 SERLCD_SPECIAL_RETURN_HOME);
198 }
199
200 /*
201 * bounds checking
202 */
203 if (x < 0 || x >= columns) {
204 return -EINVAL;
205 }
206 if (y < 0 || y >= rows) {
207 return -EINVAL;
208 }
209
210 data->cursor_x = x;
211 data->cursor_y = y;
212
213 const uint8_t cursor_address = x + row_offsets[y];
214
215 return auxdisplay_serlcd_send_special_command(
216 dev, SERLCD_SPECIAL_SET_DD_RAM_ADDRESS | cursor_address);
217
218 } else if (type == AUXDISPLAY_POSITION_RELATIVE) {
219 /*
220 * clip relative move to display dimensions
221 */
222 const int new_x = (data->cursor_x + x) % columns;
223 const int new_y = (data->cursor_y + y + x / columns) % rows;
224 const uint16_t column = new_x < 0 ? new_x + columns : new_x;
225 const uint16_t row = new_y < 0 ? new_y + rows : new_y;
226
227 data->cursor_x = column;
228 data->cursor_y = row;
229
230 const uint8_t cursor_address = column + row_offsets[row];
231
232 return auxdisplay_serlcd_send_special_command(
233 dev, SERLCD_SPECIAL_SET_DD_RAM_ADDRESS | cursor_address);
234 }
235
236 /*
237 * other types of movement are not implemented/supported
238 */
239 return -ENOSYS;
240 }
241
auxdisplay_serlcd_cursor_position_get(const struct device * dev,int16_t * x,int16_t * y)242 static int auxdisplay_serlcd_cursor_position_get(const struct device *dev, int16_t *x, int16_t *y)
243 {
244 const struct auxdisplay_serlcd_data *data = dev->data;
245
246 *x = (int16_t)data->cursor_x;
247 *y = (int16_t)data->cursor_y;
248
249 return 0;
250 }
251
auxdisplay_serlcd_capabilities_get(const struct device * dev,struct auxdisplay_capabilities * capabilities)252 static int auxdisplay_serlcd_capabilities_get(const struct device *dev,
253 struct auxdisplay_capabilities *capabilities)
254 {
255 const struct auxdisplay_serlcd_config *config = dev->config;
256
257 memcpy(capabilities, &config->capabilities, sizeof(struct auxdisplay_capabilities));
258
259 return 0;
260 }
261
auxdisplay_serlcd_clear(const struct device * dev)262 static int auxdisplay_serlcd_clear(const struct device *dev)
263 {
264 const struct auxdisplay_serlcd_config *config = dev->config;
265
266 int rc = auxdisplay_serlcd_send_command(dev, SERLCD_COMMAND_CLEAR);
267
268 k_sleep(K_MSEC(config->command_delay_ms));
269 return rc;
270 }
271
auxdisplay_serlcd_custom_character_set(const struct device * dev,struct auxdisplay_character * character)272 static int auxdisplay_serlcd_custom_character_set(const struct device *dev,
273 struct auxdisplay_character *character)
274 {
275 const struct auxdisplay_serlcd_config *config = dev->config;
276 int rc;
277
278 /*
279 * only indexes 0..7 are supported
280 */
281 const uint8_t char_index = character->index;
282
283 if (char_index > (SERLCD_CUSTOM_CHAR_MAX_COUNT - 1)) {
284 return -EINVAL;
285 }
286
287 /*
288 * custom characters are accessible via char codes 0x08..0x0f
289 */
290 character->character_code = SERLCD_CUSTOM_CHAR_INDEX_BASE | char_index;
291
292 rc = auxdisplay_serlcd_send_command(dev, SERLCD_COMMAND_SET_CUSTOM_CHAR + char_index);
293 if (!rc) {
294 return rc;
295 }
296
297 /*
298 * the display expects the custom character as 8 lines of 5 bit each, shades are not
299 * supported
300 */
301 for (int l = 0; l < SERLCD_CUSTOM_CHAR_HEIGHT; ++l) {
302 uint8_t buffer = 0;
303
304 for (int i = 0; i < SERLCD_CUSTOM_CHAR_WIDTH; ++i) {
305 if (character->data[(l * 5) + i]) {
306 buffer |= BIT(4 - i);
307 }
308 }
309 rc = i2c_write_dt(&config->bus, &buffer, sizeof(buffer));
310 if (!rc) {
311 return rc;
312 }
313 }
314
315 return rc;
316 }
317
auxdisplay_serlcd_advance_current_position(const struct device * dev)318 static void auxdisplay_serlcd_advance_current_position(const struct device *dev)
319 {
320 const struct auxdisplay_serlcd_config *config = dev->config;
321 struct auxdisplay_serlcd_data *data = dev->data;
322
323 ++(data->cursor_x);
324 if (data->cursor_x >= config->capabilities.columns) {
325 data->cursor_x = 0;
326 ++(data->cursor_y);
327 }
328 if (data->cursor_y >= config->capabilities.rows) {
329 data->cursor_y = 0;
330 }
331 }
332
auxdisplay_serlcd_write(const struct device * dev,const uint8_t * text,uint16_t len)333 static int auxdisplay_serlcd_write(const struct device *dev, const uint8_t *text, uint16_t len)
334 {
335 const struct auxdisplay_serlcd_config *config = dev->config;
336
337 int rc = 0;
338
339 /*
340 * the display wraps around by itself, just write the text and update the position data
341 */
342 for (int i = 0; i < len; ++i) {
343 uint8_t character = text[i];
344
345 /*
346 * customer characters require a special command, so check for custom char
347 */
348 if ((character & SERLCD_CUSTOM_CHAR_BITMASK) == SERLCD_CUSTOM_CHAR_INDEX_BASE) {
349 const uint8_t command = SERLCD_COMMAND_WRITE_CUSTOM_CHAR +
350 (character & ~SERLCD_CUSTOM_CHAR_BITMASK);
351
352 rc = auxdisplay_serlcd_send_command(dev, command);
353 if (!rc) {
354 return rc;
355 }
356 auxdisplay_serlcd_advance_current_position(dev);
357 } else if (character == SERLCD_BEGIN_COMMAND ||
358 character == SERLCD_BEGIN_SPECIAL_COMMAND) {
359 /*
360 * skip these characters in text, as they have a special meaning, if
361 * required a custom character can be used as replacement
362 */
363 continue;
364 } else {
365 rc = i2c_write_dt(&config->bus, text, len);
366 if (!rc) {
367 return rc;
368 }
369 auxdisplay_serlcd_advance_current_position(dev);
370 }
371 }
372
373 return rc;
374 }
375
auxdisplay_serlcd_init(const struct device * dev)376 static int auxdisplay_serlcd_init(const struct device *dev)
377 {
378 const struct auxdisplay_serlcd_config *config = dev->config;
379 struct auxdisplay_serlcd_data *data = dev->data;
380
381 /*
382 * Initialize our data structure
383 */
384 data->power = true;
385
386 if (!device_is_ready(config->bus.bus)) {
387 return -ENODEV;
388 }
389
390 auxdisplay_serlcd_clear(dev);
391
392 return 0;
393 }
394
395 static DEVICE_API(auxdisplay, auxdisplay_serlcd_auxdisplay_api) = {
396 .display_on = auxdisplay_serlcd_display_on,
397 .display_off = auxdisplay_serlcd_display_off,
398 .cursor_set_enabled = auxdisplay_serlcd_cursor_set_enabled,
399 .position_blinking_set_enabled = auxdisplay_serlcd_position_blinking_set_enabled,
400 .cursor_position_set = auxdisplay_serlcd_cursor_position_set,
401 .cursor_position_get = auxdisplay_serlcd_cursor_position_get,
402 .capabilities_get = auxdisplay_serlcd_capabilities_get,
403 .clear = auxdisplay_serlcd_clear,
404 .custom_character_set = auxdisplay_serlcd_custom_character_set,
405 .write = auxdisplay_serlcd_write,
406 };
407
408 #define AUXDISPLAY_SERLCD_INST(inst) \
409 static const struct auxdisplay_serlcd_config auxdisplay_serlcd_config_##inst = { \
410 .capabilities = { \
411 .columns = DT_INST_PROP(inst, columns), \
412 .rows = DT_INST_PROP(inst, rows), \
413 .mode = 0, \
414 .brightness.minimum = AUXDISPLAY_LIGHT_NOT_SUPPORTED, \
415 .brightness.maximum = AUXDISPLAY_LIGHT_NOT_SUPPORTED, \
416 .backlight.minimum = AUXDISPLAY_LIGHT_NOT_SUPPORTED, \
417 .backlight.maximum = AUXDISPLAY_LIGHT_NOT_SUPPORTED, \
418 .custom_characters = SERLCD_CUSTOM_CHAR_MAX_COUNT, \
419 .custom_character_width = SERLCD_CUSTOM_CHAR_WIDTH, \
420 .custom_character_height = SERLCD_CUSTOM_CHAR_HEIGHT, \
421 }, \
422 .bus = I2C_DT_SPEC_INST_GET(inst), \
423 .command_delay_ms = DT_INST_PROP(inst, command_delay_ms), \
424 .special_command_delay_ms = DT_INST_PROP(inst, special_command_delay_ms), \
425 }; \
426 \
427 static struct auxdisplay_serlcd_data auxdisplay_serlcd_data_##inst; \
428 \
429 DEVICE_DT_INST_DEFINE(inst, &auxdisplay_serlcd_init, NULL, &auxdisplay_serlcd_data_##inst, \
430 &auxdisplay_serlcd_config_##inst, POST_KERNEL, \
431 CONFIG_AUXDISPLAY_INIT_PRIORITY, &auxdisplay_serlcd_auxdisplay_api);
432
433 DT_INST_FOREACH_STATUS_OKAY(AUXDISPLAY_SERLCD_INST)
434