mirror of
				https://github.com/mfulz/qmk_firmware.git
				synced 2025-10-31 13:22:31 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			459 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			459 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /* Copyright 2020 Sergey Vlasov <sigprof@gmail.com>
 | ||
|  *
 | ||
|  * This program is free software: you can redistribute it and/or modify
 | ||
|  * it under the terms of the GNU General Public License as published by
 | ||
|  * the Free Software Foundation, either version 2 of the License, or
 | ||
|  * (at your option) any later version.
 | ||
|  *
 | ||
|  * This program is distributed in the hope that it will be useful,
 | ||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||
|  * GNU General Public License for more details.
 | ||
|  *
 | ||
|  * You should have received a copy of the GNU General Public License
 | ||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||
|  */
 | ||
| 
 | ||
| #include QMK_KEYBOARD_H
 | ||
| 
 | ||
| enum tap_dances {
 | ||
|     TD_OLED,
 | ||
| };
 | ||
| 
 | ||
| enum oled_test_modes {
 | ||
|     // Modes between TEST_FIRST and TEST_LAST (inclusive) can be switched with a keypress.
 | ||
|     TEST_FIRST,
 | ||
|     TEST_LOGO = TEST_FIRST,
 | ||
|     TEST_CHARACTERS,
 | ||
|     TEST_SLOW_UPDATE,
 | ||
|     TEST_ALL_ON,
 | ||
|     TEST_FRAME,
 | ||
|     TEST_ALL_OFF,
 | ||
|     TEST_FILL_HORZ_0,
 | ||
|     TEST_FILL_HORZ_1,
 | ||
|     TEST_FILL_VERT_0,
 | ||
|     TEST_FILL_VERT_1,
 | ||
|     TEST_FILL_CHECKERBOARD_1,
 | ||
|     TEST_FILL_CHECKERBOARD_2,
 | ||
|     TEST_FILL_CHECKERBOARD_4,
 | ||
|     TEST_LAST = TEST_FILL_CHECKERBOARD_4,
 | ||
| 
 | ||
|     // Special modes which are not reachable normally.
 | ||
|     TEST_DRAW_ALWAYS_ON,
 | ||
|     TEST_DRAW_ALWAYS_OFF,
 | ||
| };
 | ||
| 
 | ||
| static enum oled_test_modes test_mode = TEST_FIRST;
 | ||
| 
 | ||
| static oled_rotation_t rotation = OLED_ROTATION_0;
 | ||
| 
 | ||
| static bool     scrolling;
 | ||
| static uint8_t  scrolling_speed;
 | ||
| static bool     need_update = true;
 | ||
| static bool     draw_always;
 | ||
| static bool     update_speed_test;
 | ||
| static uint32_t update_speed_start_timer;
 | ||
| static uint16_t update_speed_count;
 | ||
| static bool     restart_test;
 | ||
| 
 | ||
| static void stop_scrolling(void) {
 | ||
|     if (scrolling) {
 | ||
|         oled_scroll_off();
 | ||
|         scrolling = false;
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| static void dance_oled_finished(qk_tap_dance_state_t *state, void *user_data) {
 | ||
|     switch (state->count) {
 | ||
|         case 1:
 | ||
|             if (state->pressed) {
 | ||
|                 // single hold - step through rotations
 | ||
|                 switch (rotation) {
 | ||
|                     case OLED_ROTATION_0:
 | ||
|                         rotation = OLED_ROTATION_90;
 | ||
|                         break;
 | ||
|                     case OLED_ROTATION_90:
 | ||
|                         rotation = OLED_ROTATION_180;
 | ||
|                         break;
 | ||
|                     case OLED_ROTATION_180:
 | ||
|                         rotation = OLED_ROTATION_270;
 | ||
|                         break;
 | ||
|                     default:
 | ||
|                         rotation = OLED_ROTATION_0;
 | ||
|                         break;
 | ||
|                 }
 | ||
|                 stop_scrolling();
 | ||
|                 oled_init(rotation);
 | ||
|             } else {
 | ||
|                 // single tap - step through test modes
 | ||
|                 if (test_mode < TEST_LAST) {
 | ||
|                     ++test_mode;
 | ||
|                 } else {
 | ||
|                     test_mode = TEST_FIRST;
 | ||
|                 }
 | ||
|                 stop_scrolling();
 | ||
|                 oled_clear();
 | ||
|             }
 | ||
|             restart_test = true;
 | ||
|             need_update  = true;
 | ||
|             break;
 | ||
| 
 | ||
|         case 2:
 | ||
|             if (state->pressed) {
 | ||
|                 // tap + hold - change scrolling speed
 | ||
|                 scrolling_speed = (scrolling_speed + 1) % 8;
 | ||
|                 stop_scrolling();
 | ||
|                 oled_scroll_set_speed(scrolling_speed);
 | ||
|                 // Cannot reactivate scrolling here, because oled_scroll_off()
 | ||
|                 // marks the whole display as dirty, and oled_scroll_left()
 | ||
|                 // silently does nothing if either the display is dirty or
 | ||
|                 // scrolling is already active.
 | ||
|             } else {
 | ||
|                 // double tap - toggle scrolling
 | ||
|                 if (!scrolling) {
 | ||
|                     scrolling = true;
 | ||
|                     oled_scroll_left();
 | ||
|                 } else {
 | ||
|                     scrolling = false;
 | ||
|                     oled_scroll_off();
 | ||
|                 }
 | ||
|             }
 | ||
|             need_update = true;
 | ||
|             break;
 | ||
| 
 | ||
|         case 3:
 | ||
|             if (state->pressed) {
 | ||
|                 // double tap + hold - toggle `draw_always`
 | ||
|                 draw_always = !draw_always;
 | ||
|                 if (draw_always) {
 | ||
|                     test_mode = TEST_DRAW_ALWAYS_ON;
 | ||
|                 } else {
 | ||
|                     test_mode = TEST_DRAW_ALWAYS_OFF;
 | ||
|                 }
 | ||
|                 stop_scrolling();
 | ||
|                 oled_clear();
 | ||
|                 restart_test = true;
 | ||
|                 need_update  = true;
 | ||
|             } else {
 | ||
|                 // triple tap - toggle update speed test
 | ||
|                 update_speed_test = !update_speed_test;
 | ||
|                 if (update_speed_test) {
 | ||
|                     stop_scrolling();
 | ||
|                     update_speed_start_timer = timer_read32();
 | ||
|                     update_speed_count       = 0;
 | ||
|                 }
 | ||
|             }
 | ||
|             break;
 | ||
|         case 4:
 | ||
|             if (!state->pressed) {
 | ||
|                 // quadruple tap - step through brightness levels
 | ||
|                 oled_set_brightness(oled_get_brightness() + 0x10);
 | ||
|             }
 | ||
|             break;
 | ||
|         default:
 | ||
|             break;
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| qk_tap_dance_action_t tap_dance_actions[] = {[TD_OLED] = ACTION_TAP_DANCE_FN(dance_oled_finished)};
 | ||
| 
 | ||
| const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {LAYOUT_ortho_1x1(TD(TD_OLED))};
 | ||
| 
 | ||
| // `bool oled_is_dirty(void)` does not exist at the moment
 | ||
| extern OLED_BLOCK_TYPE oled_dirty;
 | ||
| 
 | ||
| static inline uint8_t pixel_width(void) {
 | ||
|     if (!(rotation & OLED_ROTATION_90)) {
 | ||
|         return OLED_DISPLAY_WIDTH;
 | ||
|     }
 | ||
|     return OLED_DISPLAY_HEIGHT;
 | ||
| }
 | ||
| 
 | ||
| static inline uint8_t pixel_height(void) {
 | ||
|     if (!(rotation & OLED_ROTATION_90)) {
 | ||
|         return OLED_DISPLAY_HEIGHT;
 | ||
|     }
 | ||
|     return OLED_DISPLAY_WIDTH;
 | ||
| }
 | ||
| 
 | ||
| // Draw the QMK logo at the top left corner, clipping if it does not fit.
 | ||
| static void test_logo(void) {
 | ||
|     uint8_t lines = oled_max_lines();
 | ||
|     if (lines > 3) {
 | ||
|         lines = 3;
 | ||
|     }
 | ||
|     uint8_t chars = oled_max_chars();
 | ||
|     if (chars > 21) {
 | ||
|         chars = 21;
 | ||
|     }
 | ||
|     for (uint8_t row = 0; row < lines; ++row) {
 | ||
|         oled_set_cursor(0, row);
 | ||
|         for (uint8_t col = 0; col < chars; ++col) {
 | ||
|             oled_write_char(0x80 + 0x20 * row + col, false);
 | ||
|         }
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| static const PROGMEM char fill_ff[OLED_MATRIX_SIZE] = {[0 ... OLED_MATRIX_SIZE - 1] = 0xff};
 | ||
| 
 | ||
| // Fill the whole screen with a pattern made from two bytes alternating after the specified number of repeats.
 | ||
| static void test_fill(uint8_t byte0, uint8_t byte1, uint8_t repeats) {
 | ||
|     uint8_t  width = pixel_width();
 | ||
|     uint8_t  lines = oled_max_lines();
 | ||
|     uint16_t index = 0;
 | ||
|     for (uint8_t row = 0; row < lines; ++row) {
 | ||
|         for (uint8_t col = 0; col < width; ++col) {
 | ||
|             uint8_t byte = ((col / repeats) % 2) ? byte1 : byte0;
 | ||
|             oled_write_raw_byte(byte, index++);
 | ||
|         }
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| // Draw a frame at the edges of the OLED screen.
 | ||
| static void test_frame(void) {
 | ||
|     uint8_t width  = pixel_width();
 | ||
|     uint8_t height = pixel_height();
 | ||
|     for (uint8_t x = 0; x < width; ++x) {
 | ||
|         oled_write_pixel(x, 0, true);
 | ||
|         oled_write_pixel(x, height - 1, true);
 | ||
|     }
 | ||
|     for (uint8_t y = 1; y < height - 1; ++y) {
 | ||
|         oled_write_pixel(0, y, true);
 | ||
|         oled_write_pixel(width - 1, y, true);
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| // Use all 94 visible ASCII characters for testing.
 | ||
| #define TEST_CHAR_COUNT ('~' - '!' + 1)
 | ||
| 
 | ||
| static char get_test_char(uint8_t char_index) { return char_index + '!'; }
 | ||
| 
 | ||
| // Fill the whole screen with distinct characters (if the display is large enough to show more than 94 characters
 | ||
| // at once, the sequence is repeated the second time with inverted characters).
 | ||
| static void test_characters(void) {
 | ||
|     uint8_t cols       = oled_max_chars();
 | ||
|     uint8_t rows       = oled_max_lines();
 | ||
|     bool    invert     = false;
 | ||
|     uint8_t char_index = 0;
 | ||
|     for (uint8_t row = 0; row < rows; ++row) {
 | ||
|         for (uint8_t col = 0; col < cols; ++col) {
 | ||
|             oled_write_char(get_test_char(char_index), invert);
 | ||
|             if (++char_index >= TEST_CHAR_COUNT) {
 | ||
|                 char_index = 0;
 | ||
|                 invert     = !invert;
 | ||
|             }
 | ||
|         }
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| // Test screen updating after drawing a single character or pixel.
 | ||
| void test_slow_update(void) {
 | ||
|     static uint8_t  phase, x, y, char_index, first_char;
 | ||
|     static uint16_t timer;
 | ||
|     static uint16_t delay = 500;
 | ||
| 
 | ||
|     if (restart_test) {
 | ||
|         // Initialize all state variables before starting the test.
 | ||
|         restart_test = false;
 | ||
|         phase        = 0;
 | ||
|         x            = 0;
 | ||
|         y            = 0;
 | ||
|         char_index   = 0;
 | ||
|         first_char   = 0;
 | ||
|         delay        = 500;
 | ||
|     } else {
 | ||
|         // Wait for the specified time between steps.
 | ||
|         if (timer_elapsed(timer) < delay) {
 | ||
|             return;
 | ||
|         }
 | ||
|     }
 | ||
| 
 | ||
|     timer = timer_read();
 | ||
|     switch (phase) {
 | ||
|         case 0:
 | ||
|             // Phase 0: fill the whole screen with mostly distinct characters, one character at a time.  Here the
 | ||
|             // inversion trick is not used, so that the frame which is drawn in subsequent phases would not be
 | ||
|             // overlapped by the inverted character background.
 | ||
|             oled_set_cursor(x, y);
 | ||
|             oled_write_char(get_test_char(char_index), false);
 | ||
|             if (++char_index >= TEST_CHAR_COUNT) {
 | ||
|                 char_index = 0;
 | ||
|             }
 | ||
|             if (++x >= oled_max_chars()) {
 | ||
|                 x = 0;
 | ||
|                 if (++y >= oled_max_lines()) {
 | ||
|                     // The whole screen was filled - start the next phase.
 | ||
|                     ++phase;
 | ||
|                     x = y = 0;
 | ||
|                 }
 | ||
|             }
 | ||
|             delay = 250;
 | ||
|             break;
 | ||
| 
 | ||
|         case 1:
 | ||
|             // Phase 1: draw a line along the left edge of the screen, one pixel at a time.
 | ||
|             oled_write_pixel(x, y, true);
 | ||
|             if (y < pixel_height() - 1) {
 | ||
|                 ++y;
 | ||
|             } else {
 | ||
|                 // The bottom left corner is reached - start the next phase.
 | ||
|                 ++phase;
 | ||
|                 ++x;
 | ||
|             }
 | ||
|             delay = 50;
 | ||
|             break;
 | ||
| 
 | ||
|         case 2:
 | ||
|             // Phase 2: draw a line along the bottom edge of the screen, one pixel at a time.
 | ||
|             oled_write_pixel(x, y, true);
 | ||
|             if (x < pixel_width() - 1) {
 | ||
|                 ++x;
 | ||
|             } else {
 | ||
|                 // The bottom right corner was reached - start the next phase.
 | ||
|                 ++phase;
 | ||
|                 --y;
 | ||
|             }
 | ||
|             delay = 50;
 | ||
|             break;
 | ||
| 
 | ||
|         case 3:
 | ||
|             // Phase 3: draw a line along the right edge of the screen, one pixel at a time.
 | ||
|             oled_write_pixel(x, y, true);
 | ||
|             if (y > 0) {
 | ||
|                 --y;
 | ||
|             } else {
 | ||
|                 // The top right corner was reached - start the next phase.
 | ||
|                 ++phase;
 | ||
|                 --x;
 | ||
|             }
 | ||
|             delay = 50;
 | ||
|             break;
 | ||
| 
 | ||
|         case 4:
 | ||
|             // Phase 4: draw a line along the top edge of the screen, one pixel at a time.
 | ||
|             oled_write_pixel(x, y, true);
 | ||
|             if (x > 0) {
 | ||
|                 --x;
 | ||
|             } else {
 | ||
|                 // The top left corner was reached - start the next phase.
 | ||
|                 ++phase;
 | ||
|             }
 | ||
|             delay = 50;
 | ||
|             break;
 | ||
| 
 | ||
|         default:
 | ||
|             // Restart from phase 0, but change the first character of the sequence to make screen updates visible.
 | ||
|             if (++first_char >= TEST_CHAR_COUNT) {
 | ||
|                 first_char = 0;
 | ||
|             }
 | ||
|             phase      = 0;
 | ||
|             x          = 0;
 | ||
|             y          = 0;
 | ||
|             char_index = first_char;
 | ||
|             delay      = 500;
 | ||
|             break;
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| oled_rotation_t oled_init_user(oled_rotation_t rotation) {
 | ||
|     oled_scroll_set_area(0, 0);
 | ||
|     oled_scroll_set_speed(scrolling_speed);
 | ||
|     return rotation;
 | ||
| }
 | ||
| 
 | ||
| void oled_task_user(void) {
 | ||
|     if (update_speed_test) {
 | ||
|         // Speed test mode - wait for screen update completion.
 | ||
|         if (!oled_dirty) {
 | ||
|             // Update statistics and send the measurement result to the console.
 | ||
|             update_speed_count++;
 | ||
|             if (update_speed_count % 256 == 0) {
 | ||
|                 uprintf("OLED: %u updates, %lu ms\n", update_speed_count, timer_elapsed32(update_speed_start_timer));
 | ||
|             }
 | ||
| 
 | ||
|             // Toggle between the "all on" and "all off" states and trigger the screen update again.
 | ||
|             if (test_mode == TEST_ALL_ON) {
 | ||
|                 test_mode = TEST_ALL_OFF;
 | ||
|             } else {
 | ||
|                 test_mode = TEST_ALL_ON;
 | ||
|             }
 | ||
|             need_update = true;
 | ||
|         }
 | ||
|     }
 | ||
| 
 | ||
|     // The sample implementation of oled_task_user() in the documentation redraws the image after every call, relying on
 | ||
|     // the fact that drawing functions check whether the output actually changes anything in the image, and set dirty
 | ||
|     // bits only when something has actually changed.  However, redrawing the image only when some of the underlying
 | ||
|     // data has changed is more efficient.  Make it possible to test both modes here.
 | ||
|     if (!draw_always || update_speed_test) {
 | ||
|         // Draw the image only when the `need_update` flag is set, except for the "slow update" test.
 | ||
|         // This mode is also forced when the screen update speed test is performed.
 | ||
|         if (!need_update) {
 | ||
|             if (test_mode != TEST_SLOW_UPDATE) {
 | ||
|                 return;
 | ||
|             }
 | ||
|         }
 | ||
|         need_update = false;
 | ||
|     }
 | ||
| 
 | ||
|     switch (test_mode) {
 | ||
|         case TEST_LOGO:
 | ||
|             test_logo();
 | ||
|             break;
 | ||
|         case TEST_CHARACTERS:
 | ||
|             test_characters();
 | ||
|             break;
 | ||
|         case TEST_SLOW_UPDATE:
 | ||
|             test_slow_update();
 | ||
|             break;
 | ||
|         case TEST_ALL_ON:
 | ||
|             oled_write_raw_P(fill_ff, sizeof(fill_ff));
 | ||
|             break;
 | ||
|         case TEST_FRAME:
 | ||
|             test_frame();
 | ||
|             break;
 | ||
|         case TEST_ALL_OFF:
 | ||
|             // `oled_clear()` is faster, but cannot be used with `draw_always`, because it does not check the previous
 | ||
|             // content of the buffer and always marks the whole buffer as dirty.
 | ||
|             if (update_speed_test) {
 | ||
|                 oled_clear();
 | ||
|             } else {
 | ||
|                 test_fill(0x00, 0x00, 1);
 | ||
|             }
 | ||
|             break;
 | ||
|         case TEST_FILL_HORZ_0:
 | ||
|             test_fill(0x55, 0x55, 1);
 | ||
|             break;
 | ||
|         case TEST_FILL_HORZ_1:
 | ||
|             test_fill(0xaa, 0xaa, 1);
 | ||
|             break;
 | ||
|         case TEST_FILL_VERT_0:
 | ||
|             test_fill(0xff, 0x00, 1);
 | ||
|             break;
 | ||
|         case TEST_FILL_VERT_1:
 | ||
|             test_fill(0x00, 0xff, 1);
 | ||
|             break;
 | ||
|         case TEST_FILL_CHECKERBOARD_1:
 | ||
|             test_fill(0x55, 0xaa, 1);
 | ||
|             break;
 | ||
|         case TEST_FILL_CHECKERBOARD_2:
 | ||
|             test_fill(0x33, 0xcc, 2);
 | ||
|             break;
 | ||
|         case TEST_FILL_CHECKERBOARD_4:
 | ||
|             test_fill(0x0f, 0xf0, 4);
 | ||
|             break;
 | ||
| 
 | ||
|         case TEST_DRAW_ALWAYS_ON:
 | ||
|             oled_write_P(PSTR("Draw Always"), false);
 | ||
|             break;
 | ||
|         case TEST_DRAW_ALWAYS_OFF:
 | ||
|             oled_write_P(PSTR("Draw Once"), false);
 | ||
|             break;
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| void keyboard_post_init_user(void) {
 | ||
|     // Console messages are used for update speed test results
 | ||
|     debug_enable = true;
 | ||
| }
 | 
