diff options
Diffstat (limited to 'drivers/lcd/st7565.c')
-rw-r--r-- | drivers/lcd/st7565.c | 496 |
1 files changed, 496 insertions, 0 deletions
diff --git a/drivers/lcd/st7565.c b/drivers/lcd/st7565.c new file mode 100644 index 000000000..49b13c00f --- /dev/null +++ b/drivers/lcd/st7565.c | |||
@@ -0,0 +1,496 @@ | |||
1 | /* | ||
2 | Copyright 2021 | ||
3 | |||
4 | This program is free software: you can redistribute it and/or modify | ||
5 | it under the terms of the GNU General Public License as published by | ||
6 | the Free Software Foundation, either version 2 of the License, or | ||
7 | (at your option) any later version. | ||
8 | |||
9 | This program is distributed in the hope that it will be useful, | ||
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
12 | GNU General Public License for more details. | ||
13 | |||
14 | You should have received a copy of the GNU General Public License | ||
15 | along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
16 | */ | ||
17 | |||
18 | #include "st7565.h" | ||
19 | |||
20 | #include <string.h> | ||
21 | |||
22 | #include "keyboard.h" | ||
23 | #include "progmem.h" | ||
24 | #include "timer.h" | ||
25 | #include "wait.h" | ||
26 | |||
27 | #include ST7565_FONT_H | ||
28 | |||
29 | // Fundamental Commands | ||
30 | #define CONTRAST 0x81 | ||
31 | #define DISPLAY_ALL_ON 0xA5 | ||
32 | #define DISPLAY_ALL_ON_RESUME 0xA4 | ||
33 | #define NORMAL_DISPLAY 0xA6 | ||
34 | #define INVERT_DISPLAY 0xA7 | ||
35 | #define DISPLAY_ON 0xAF | ||
36 | #define DISPLAY_OFF 0xAE | ||
37 | #define NOP 0xE3 | ||
38 | |||
39 | // Addressing Setting Commands | ||
40 | #define PAM_SETCOLUMN_LSB 0x00 | ||
41 | #define PAM_SETCOLUMN_MSB 0x10 | ||
42 | #define PAM_PAGE_ADDR 0xB0 // 0xb0 -- 0xb7 | ||
43 | |||
44 | // Hardware Configuration Commands | ||
45 | #define DISPLAY_START_LINE 0x40 | ||
46 | #define SEGMENT_REMAP 0xA0 | ||
47 | #define SEGMENT_REMAP_INV 0xA1 | ||
48 | #define COM_SCAN_INC 0xC0 | ||
49 | #define COM_SCAN_DEC 0xC8 | ||
50 | #define LCD_BIAS_7 0xA3 | ||
51 | #define LCD_BIAS_9 0xA2 | ||
52 | #define RESISTOR_RATIO 0x20 | ||
53 | #define POWER_CONTROL 0x28 | ||
54 | |||
55 | // Misc defines | ||
56 | #ifndef ST7565_BLOCK_COUNT | ||
57 | # define ST7565_BLOCK_COUNT (sizeof(ST7565_BLOCK_TYPE) * 8) | ||
58 | #endif | ||
59 | #ifndef ST7565_BLOCK_SIZE | ||
60 | # define ST7565_BLOCK_SIZE (ST7565_MATRIX_SIZE / ST7565_BLOCK_COUNT) | ||
61 | #endif | ||
62 | |||
63 | #define ST7565_ALL_BLOCKS_MASK (((((ST7565_BLOCK_TYPE)1 << (ST7565_BLOCK_COUNT - 1)) - 1) << 1) | 1) | ||
64 | |||
65 | #define HAS_FLAGS(bits, flags) ((bits & flags) == flags) | ||
66 | |||
67 | // Display buffer's is the same as the display memory layout | ||
68 | // this is so we don't end up with rounding errors with | ||
69 | // parts of the display unusable or don't get cleared correctly | ||
70 | // and also allows for drawing & inverting | ||
71 | uint8_t st7565_buffer[ST7565_MATRIX_SIZE]; | ||
72 | uint8_t * st7565_cursor; | ||
73 | ST7565_BLOCK_TYPE st7565_dirty = 0; | ||
74 | bool st7565_initialized = false; | ||
75 | bool st7565_active = false; | ||
76 | bool st7565_inverted = false; | ||
77 | display_rotation_t st7565_rotation = DISPLAY_ROTATION_0; | ||
78 | #if ST7565_TIMEOUT > 0 | ||
79 | uint32_t st7565_timeout; | ||
80 | #endif | ||
81 | #if ST7565_UPDATE_INTERVAL > 0 | ||
82 | uint16_t st7565_update_timeout; | ||
83 | #endif | ||
84 | |||
85 | // Flips the rendering bits for a character at the current cursor position | ||
86 | static void InvertCharacter(uint8_t *cursor) { | ||
87 | const uint8_t *end = cursor + ST7565_FONT_WIDTH; | ||
88 | while (cursor < end) { | ||
89 | *cursor = ~(*cursor); | ||
90 | cursor++; | ||
91 | } | ||
92 | } | ||
93 | |||
94 | bool st7565_init(display_rotation_t rotation) { | ||
95 | setPinOutput(ST7565_A0_PIN); | ||
96 | writePinHigh(ST7565_A0_PIN); | ||
97 | setPinOutput(ST7565_RST_PIN); | ||
98 | writePinHigh(ST7565_RST_PIN); | ||
99 | |||
100 | st7565_rotation = st7565_init_user(rotation); | ||
101 | |||
102 | spi_init(); | ||
103 | spi_start(ST7565_SS_PIN, false, 0, ST7565_SPI_CLK_DIVISOR); | ||
104 | |||
105 | st7565_reset(); | ||
106 | |||
107 | st7565_send_cmd(LCD_BIAS_7); | ||
108 | if (!HAS_FLAGS(st7565_rotation, DISPLAY_ROTATION_180)) { | ||
109 | st7565_send_cmd(SEGMENT_REMAP); | ||
110 | st7565_send_cmd(COM_SCAN_DEC); | ||
111 | } else { | ||
112 | st7565_send_cmd(SEGMENT_REMAP_INV); | ||
113 | st7565_send_cmd(COM_SCAN_INC); | ||
114 | } | ||
115 | st7565_send_cmd(DISPLAY_START_LINE | 0x00); | ||
116 | st7565_send_cmd(CONTRAST); | ||
117 | st7565_send_cmd(ST7565_CONTRAST); | ||
118 | st7565_send_cmd(RESISTOR_RATIO | 0x01); | ||
119 | st7565_send_cmd(POWER_CONTROL | 0x04); | ||
120 | wait_ms(50); | ||
121 | st7565_send_cmd(POWER_CONTROL | 0x06); | ||
122 | wait_ms(50); | ||
123 | st7565_send_cmd(POWER_CONTROL | 0x07); | ||
124 | wait_ms(10); | ||
125 | st7565_send_cmd(DISPLAY_ON); | ||
126 | st7565_send_cmd(DISPLAY_ALL_ON_RESUME); | ||
127 | st7565_send_cmd(NORMAL_DISPLAY); | ||
128 | |||
129 | spi_stop(); | ||
130 | |||
131 | #if ST7565_TIMEOUT > 0 | ||
132 | st7565_timeout = timer_read32() + ST7565_TIMEOUT; | ||
133 | #endif | ||
134 | |||
135 | st7565_clear(); | ||
136 | st7565_initialized = true; | ||
137 | st7565_active = true; | ||
138 | return true; | ||
139 | } | ||
140 | |||
141 | __attribute__((weak)) display_rotation_t st7565_init_user(display_rotation_t rotation) { return rotation; } | ||
142 | |||
143 | void st7565_clear(void) { | ||
144 | memset(st7565_buffer, 0, sizeof(st7565_buffer)); | ||
145 | st7565_cursor = &st7565_buffer[0]; | ||
146 | st7565_dirty = ST7565_ALL_BLOCKS_MASK; | ||
147 | } | ||
148 | |||
149 | uint8_t crot(uint8_t a, int8_t n) { | ||
150 | const uint8_t mask = 0x7; | ||
151 | n &= mask; | ||
152 | return a << n | a >> (-n & mask); | ||
153 | } | ||
154 | |||
155 | void st7565_render(void) { | ||
156 | if (!st7565_initialized) { | ||
157 | return; | ||
158 | } | ||
159 | |||
160 | // Do we have work to do? | ||
161 | st7565_dirty &= ST7565_ALL_BLOCKS_MASK; | ||
162 | if (!st7565_dirty) { | ||
163 | return; | ||
164 | } | ||
165 | |||
166 | // Find first dirty block | ||
167 | uint8_t update_start = 0; | ||
168 | while (!(st7565_dirty & ((ST7565_BLOCK_TYPE)1 << update_start))) { | ||
169 | ++update_start; | ||
170 | } | ||
171 | |||
172 | // Calculate commands to set memory addressing bounds. | ||
173 | uint8_t start_page = ST7565_BLOCK_SIZE * update_start / ST7565_DISPLAY_WIDTH; | ||
174 | uint8_t start_column = ST7565_BLOCK_SIZE * update_start % ST7565_DISPLAY_WIDTH; | ||
175 | // IC has 132 segment drivers, for panels with less width we need to offset the starting column | ||
176 | if (HAS_FLAGS(st7565_rotation, DISPLAY_ROTATION_180)) { | ||
177 | start_column += (132 - ST7565_DISPLAY_WIDTH); | ||
178 | } | ||
179 | |||
180 | spi_start(ST7565_SS_PIN, false, 0, ST7565_SPI_CLK_DIVISOR); | ||
181 | |||
182 | st7565_send_cmd(PAM_PAGE_ADDR | start_page); | ||
183 | st7565_send_cmd(PAM_SETCOLUMN_LSB | ((ST7565_COLUMN_OFFSET + start_column) & 0x0f)); | ||
184 | st7565_send_cmd(PAM_SETCOLUMN_MSB | ((ST7565_COLUMN_OFFSET + start_column) >> 4 & 0x0f)); | ||
185 | |||
186 | st7565_send_data(&st7565_buffer[ST7565_BLOCK_SIZE * update_start], ST7565_BLOCK_SIZE); | ||
187 | |||
188 | // Turn on display if it is off | ||
189 | st7565_on(); | ||
190 | |||
191 | // Clear dirty flag | ||
192 | st7565_dirty &= ~((ST7565_BLOCK_TYPE)1 << update_start); | ||
193 | } | ||
194 | |||
195 | void st7565_set_cursor(uint8_t col, uint8_t line) { | ||
196 | uint16_t index = line * ST7565_DISPLAY_WIDTH + col * ST7565_FONT_WIDTH; | ||
197 | |||
198 | // Out of bounds? | ||
199 | if (index >= ST7565_MATRIX_SIZE) { | ||
200 | index = 0; | ||
201 | } | ||
202 | |||
203 | st7565_cursor = &st7565_buffer[index]; | ||
204 | } | ||
205 | |||
206 | void st7565_advance_page(bool clearPageRemainder) { | ||
207 | uint16_t index = st7565_cursor - &st7565_buffer[0]; | ||
208 | uint8_t remaining = ST7565_DISPLAY_WIDTH - (index % ST7565_DISPLAY_WIDTH); | ||
209 | |||
210 | if (clearPageRemainder) { | ||
211 | // Remaining Char count | ||
212 | remaining = remaining / ST7565_FONT_WIDTH; | ||
213 | |||
214 | // Write empty character until next line | ||
215 | while (remaining--) st7565_write_char(' ', false); | ||
216 | } else { | ||
217 | // Next page index out of bounds? | ||
218 | if (index + remaining >= ST7565_MATRIX_SIZE) { | ||
219 | index = 0; | ||
220 | remaining = 0; | ||
221 | } | ||
222 | |||
223 | st7565_cursor = &st7565_buffer[index + remaining]; | ||
224 | } | ||
225 | } | ||
226 | |||
227 | void st7565_advance_char(void) { | ||
228 | uint16_t nextIndex = st7565_cursor - &st7565_buffer[0] + ST7565_FONT_WIDTH; | ||
229 | uint8_t remainingSpace = ST7565_DISPLAY_WIDTH - (nextIndex % ST7565_DISPLAY_WIDTH); | ||
230 | |||
231 | // Do we have enough space on the current line for the next character | ||
232 | if (remainingSpace < ST7565_FONT_WIDTH) { | ||
233 | nextIndex += remainingSpace; | ||
234 | } | ||
235 | |||
236 | // Did we go out of bounds | ||
237 | if (nextIndex >= ST7565_MATRIX_SIZE) { | ||
238 | nextIndex = 0; | ||
239 | } | ||
240 | |||
241 | // Update cursor position | ||
242 | st7565_cursor = &st7565_buffer[nextIndex]; | ||
243 | } | ||
244 | |||
245 | // Main handler that writes character data to the display buffer | ||
246 | void st7565_write_char(const char data, bool invert) { | ||
247 | // Advance to the next line if newline | ||
248 | if (data == '\n') { | ||
249 | // Old source wrote ' ' until end of line... | ||
250 | st7565_advance_page(true); | ||
251 | return; | ||
252 | } | ||
253 | |||
254 | if (data == '\r') { | ||
255 | st7565_advance_page(false); | ||
256 | return; | ||
257 | } | ||
258 | |||
259 | // copy the current render buffer to check for dirty after | ||
260 | static uint8_t st7565_temp_buffer[ST7565_FONT_WIDTH]; | ||
261 | memcpy(&st7565_temp_buffer, st7565_cursor, ST7565_FONT_WIDTH); | ||
262 | |||
263 | _Static_assert(sizeof(font) >= ((ST7565_FONT_END + 1 - ST7565_FONT_START) * ST7565_FONT_WIDTH), "ST7565_FONT_END references outside array"); | ||
264 | |||
265 | // set the reder buffer data | ||
266 | uint8_t cast_data = (uint8_t)data; // font based on unsigned type for index | ||
267 | if (cast_data < ST7565_FONT_START || cast_data > ST7565_FONT_END) { | ||
268 | memset(st7565_cursor, 0x00, ST7565_FONT_WIDTH); | ||
269 | } else { | ||
270 | const uint8_t *glyph = &font[(cast_data - ST7565_FONT_START) * ST7565_FONT_WIDTH]; | ||
271 | memcpy_P(st7565_cursor, glyph, ST7565_FONT_WIDTH); | ||
272 | } | ||
273 | |||
274 | // Invert if needed | ||
275 | if (invert) { | ||
276 | InvertCharacter(st7565_cursor); | ||
277 | } | ||
278 | |||
279 | // Dirty check | ||
280 | if (memcmp(&st7565_temp_buffer, st7565_cursor, ST7565_FONT_WIDTH)) { | ||
281 | uint16_t index = st7565_cursor - &st7565_buffer[0]; | ||
282 | st7565_dirty |= ((ST7565_BLOCK_TYPE)1 << (index / ST7565_BLOCK_SIZE)); | ||
283 | // Edgecase check if the written data spans the 2 chunks | ||
284 | st7565_dirty |= ((ST7565_BLOCK_TYPE)1 << ((index + ST7565_FONT_WIDTH - 1) / ST7565_BLOCK_SIZE)); | ||
285 | } | ||
286 | |||
287 | // Finally move to the next char | ||
288 | st7565_advance_char(); | ||
289 | } | ||
290 | |||
291 | void st7565_write(const char *data, bool invert) { | ||
292 | const char *end = data + strlen(data); | ||
293 | while (data < end) { | ||
294 | st7565_write_char(*data, invert); | ||
295 | data++; | ||
296 | } | ||
297 | } | ||
298 | |||
299 | void st7565_write_ln(const char *data, bool invert) { | ||
300 | st7565_write(data, invert); | ||
301 | st7565_advance_page(true); | ||
302 | } | ||
303 | |||
304 | void st7565_pan(bool left) { | ||
305 | uint16_t i = 0; | ||
306 | for (uint16_t y = 0; y < ST7565_DISPLAY_HEIGHT / 8; y++) { | ||
307 | if (left) { | ||
308 | for (uint16_t x = 0; x < ST7565_DISPLAY_WIDTH - 1; x++) { | ||
309 | i = y * ST7565_DISPLAY_WIDTH + x; | ||
310 | st7565_buffer[i] = st7565_buffer[i + 1]; | ||
311 | } | ||
312 | } else { | ||
313 | for (uint16_t x = ST7565_DISPLAY_WIDTH - 1; x > 0; x--) { | ||
314 | i = y * ST7565_DISPLAY_WIDTH + x; | ||
315 | st7565_buffer[i] = st7565_buffer[i - 1]; | ||
316 | } | ||
317 | } | ||
318 | } | ||
319 | st7565_dirty = ST7565_ALL_BLOCKS_MASK; | ||
320 | } | ||
321 | |||
322 | display_buffer_reader_t st7565_read_raw(uint16_t start_index) { | ||
323 | if (start_index > ST7565_MATRIX_SIZE) start_index = ST7565_MATRIX_SIZE; | ||
324 | display_buffer_reader_t ret_reader; | ||
325 | ret_reader.current_element = &st7565_buffer[start_index]; | ||
326 | ret_reader.remaining_element_count = ST7565_MATRIX_SIZE - start_index; | ||
327 | return ret_reader; | ||
328 | } | ||
329 | |||
330 | void st7565_write_raw_byte(const char data, uint16_t index) { | ||
331 | if (index > ST7565_MATRIX_SIZE) index = ST7565_MATRIX_SIZE; | ||
332 | if (st7565_buffer[index] == data) return; | ||
333 | st7565_buffer[index] = data; | ||
334 | st7565_dirty |= ((ST7565_BLOCK_TYPE)1 << (index / ST7565_BLOCK_SIZE)); | ||
335 | } | ||
336 | |||
337 | void st7565_write_raw(const char *data, uint16_t size) { | ||
338 | uint16_t cursor_start_index = st7565_cursor - &st7565_buffer[0]; | ||
339 | if ((size + cursor_start_index) > ST7565_MATRIX_SIZE) size = ST7565_MATRIX_SIZE - cursor_start_index; | ||
340 | for (uint16_t i = cursor_start_index; i < cursor_start_index + size; i++) { | ||
341 | uint8_t c = *data++; | ||
342 | if (st7565_buffer[i] == c) continue; | ||
343 | st7565_buffer[i] = c; | ||
344 | st7565_dirty |= ((ST7565_BLOCK_TYPE)1 << (i / ST7565_BLOCK_SIZE)); | ||
345 | } | ||
346 | } | ||
347 | |||
348 | void st7565_write_pixel(uint8_t x, uint8_t y, bool on) { | ||
349 | if (x >= ST7565_DISPLAY_WIDTH) { | ||
350 | return; | ||
351 | } | ||
352 | uint16_t index = x + (y / 8) * ST7565_DISPLAY_WIDTH; | ||
353 | if (index >= ST7565_MATRIX_SIZE) { | ||
354 | return; | ||
355 | } | ||
356 | uint8_t data = st7565_buffer[index]; | ||
357 | if (on) { | ||
358 | data |= (1 << (y % 8)); | ||
359 | } else { | ||
360 | data &= ~(1 << (y % 8)); | ||
361 | } | ||
362 | if (st7565_buffer[index] != data) { | ||
363 | st7565_buffer[index] = data; | ||
364 | st7565_dirty |= ((ST7565_BLOCK_TYPE)1 << (index / ST7565_BLOCK_SIZE)); | ||
365 | } | ||
366 | } | ||
367 | |||
368 | #if defined(__AVR__) | ||
369 | void st7565_write_P(const char *data, bool invert) { | ||
370 | uint8_t c = pgm_read_byte(data); | ||
371 | while (c != 0) { | ||
372 | st7565_write_char(c, invert); | ||
373 | c = pgm_read_byte(++data); | ||
374 | } | ||
375 | } | ||
376 | |||
377 | void st7565_write_ln_P(const char *data, bool invert) { | ||
378 | st7565_write_P(data, invert); | ||
379 | st7565_advance_page(true); | ||
380 | } | ||
381 | |||
382 | void st7565_write_raw_P(const char *data, uint16_t size) { | ||
383 | uint16_t cursor_start_index = st7565_cursor - &st7565_buffer[0]; | ||
384 | if ((size + cursor_start_index) > ST7565_MATRIX_SIZE) size = ST7565_MATRIX_SIZE - cursor_start_index; | ||
385 | for (uint16_t i = cursor_start_index; i < cursor_start_index + size; i++) { | ||
386 | uint8_t c = pgm_read_byte(data++); | ||
387 | if (st7565_buffer[i] == c) continue; | ||
388 | st7565_buffer[i] = c; | ||
389 | st7565_dirty |= ((ST7565_BLOCK_TYPE)1 << (i / ST7565_BLOCK_SIZE)); | ||
390 | } | ||
391 | } | ||
392 | #endif // defined(__AVR__) | ||
393 | |||
394 | bool st7565_on(void) { | ||
395 | if (!st7565_initialized) { | ||
396 | return st7565_active; | ||
397 | } | ||
398 | |||
399 | #if ST7565_TIMEOUT > 0 | ||
400 | st7565_timeout = timer_read32() + ST7565_TIMEOUT; | ||
401 | #endif | ||
402 | |||
403 | if (!st7565_active) { | ||
404 | spi_start(ST7565_SS_PIN, false, 0, ST7565_SPI_CLK_DIVISOR); | ||
405 | st7565_send_cmd(DISPLAY_ON); | ||
406 | spi_stop(); | ||
407 | st7565_active = true; | ||
408 | st7565_on_user(); | ||
409 | } | ||
410 | return st7565_active; | ||
411 | } | ||
412 | |||
413 | __attribute__((weak)) void st7565_on_user(void) {} | ||
414 | |||
415 | bool st7565_off(void) { | ||
416 | if (!st7565_initialized) { | ||
417 | return !st7565_active; | ||
418 | } | ||
419 | |||
420 | if (st7565_active) { | ||
421 | spi_start(ST7565_SS_PIN, false, 0, ST7565_SPI_CLK_DIVISOR); | ||
422 | st7565_send_cmd(DISPLAY_OFF); | ||
423 | spi_stop(); | ||
424 | st7565_active = false; | ||
425 | st7565_off_user(); | ||
426 | } | ||
427 | return !st7565_active; | ||
428 | } | ||
429 | |||
430 | __attribute__((weak)) void st7565_off_user(void) {} | ||
431 | |||
432 | bool st7565_is_on(void) { return st7565_active; } | ||
433 | |||
434 | bool st7565_invert(bool invert) { | ||
435 | if (!st7565_initialized) { | ||
436 | return st7565_inverted; | ||
437 | } | ||
438 | |||
439 | if (invert != st7565_inverted) { | ||
440 | spi_start(ST7565_SS_PIN, false, 0, ST7565_SPI_CLK_DIVISOR); | ||
441 | st7565_send_cmd(invert ? INVERT_DISPLAY : NORMAL_DISPLAY); | ||
442 | spi_stop(); | ||
443 | st7565_inverted = invert; | ||
444 | } | ||
445 | return st7565_inverted; | ||
446 | } | ||
447 | |||
448 | uint8_t st7565_max_chars(void) { return ST7565_DISPLAY_WIDTH / ST7565_FONT_WIDTH; } | ||
449 | |||
450 | uint8_t st7565_max_lines(void) { return ST7565_DISPLAY_HEIGHT / ST7565_FONT_HEIGHT; } | ||
451 | |||
452 | void st7565_task(void) { | ||
453 | if (!st7565_initialized) { | ||
454 | return; | ||
455 | } | ||
456 | |||
457 | #if ST7565_UPDATE_INTERVAL > 0 | ||
458 | if (timer_elapsed(st7565_update_timeout) >= ST7565_UPDATE_INTERVAL) { | ||
459 | st7565_update_timeout = timer_read(); | ||
460 | st7565_set_cursor(0, 0); | ||
461 | st7565_task_user(); | ||
462 | } | ||
463 | #else | ||
464 | st7565_set_cursor(0, 0); | ||
465 | st7565_task_user(); | ||
466 | #endif | ||
467 | |||
468 | // Smart render system, no need to check for dirty | ||
469 | st7565_render(); | ||
470 | |||
471 | // Display timeout check | ||
472 | #if ST7565_TIMEOUT > 0 | ||
473 | if (st7565_active && timer_expired32(timer_read32(), st7565_timeout)) { | ||
474 | st7565_off(); | ||
475 | } | ||
476 | #endif | ||
477 | } | ||
478 | |||
479 | __attribute__((weak)) void st7565_task_user(void) {} | ||
480 | |||
481 | void st7565_reset(void) { | ||
482 | writePinLow(ST7565_RST_PIN); | ||
483 | wait_ms(20); | ||
484 | writePinHigh(ST7565_RST_PIN); | ||
485 | wait_ms(20); | ||
486 | } | ||
487 | |||
488 | spi_status_t st7565_send_cmd(uint8_t cmd) { | ||
489 | writePinLow(ST7565_A0_PIN); | ||
490 | return spi_write(cmd); | ||
491 | } | ||
492 | |||
493 | spi_status_t st7565_send_data(uint8_t *data, uint16_t length) { | ||
494 | writePinHigh(ST7565_A0_PIN); | ||
495 | return spi_transmit(data, length); | ||
496 | } | ||