From cf7bc5ccea1249d62bcff4e91dde45ab51472fff Mon Sep 17 00:00:00 2001 From: Frank Adams Date: Thu, 20 Aug 2020 19:47:36 -0700 Subject: [PATCH] Add files via upload --- .../Lenovo_Y480_LC/Lenovo_Y480_LC.ino | 433 ++++++++++++++++++ .../Lenovo_Y480_LC/y480_matrix.xlsx | Bin 0 -> 13074 bytes 2 files changed, 433 insertions(+) create mode 100644 Example_Keyboards/Lenovo_Y480_LC/Lenovo_Y480_LC.ino create mode 100644 Example_Keyboards/Lenovo_Y480_LC/y480_matrix.xlsx diff --git a/Example_Keyboards/Lenovo_Y480_LC/Lenovo_Y480_LC.ino b/Example_Keyboards/Lenovo_Y480_LC/Lenovo_Y480_LC.ino new file mode 100644 index 0000000..f4efb47 --- /dev/null +++ b/Example_Keyboards/Lenovo_Y480_LC/Lenovo_Y480_LC.ino @@ -0,0 +1,433 @@ +/* Copyright 2020 Frank Adams + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +// This software implements a Lenovo Y480 Laptop Keyboard Controller using a Teensy LC on +// a daughterboard with a 24 pin FPC connector. The keyboard part number is V-133020AK1-IT. +// This routine uses the Teensyduino "Micro-Manager Method" to send Normal and Modifier +// keys over USB. Multi-media keys are sent with keyboard press and release functions. +// Description of Teensyduino keyboard functions is at www.pjrc.com/teensy/td_keyboard.html +// +// Revision History +// Luigi Caradonna modified the code for the Y480 with Italian key layout - Aug 18th 2020 +// Compile with tools --> keyboared layout: Italian +#define MODIFIERKEY_FN 0x8f // give Fn key a HID code Lock +// +const byte rows_max = 16; // sets the number of rows in the matrix +const byte cols_max = 8; // sets the number of columns in the matrix + +// Data to manage the backlight +const byte blpin = 23; // PWM pin to use +byte blight = 0; // initial state + +// +// Load the normal key matrix with the Teensyduino key names described at www.pjrc.com/teensy/td_keyboard.html +// A zero indicates no normal key at that location. +// The Italian vowel grave keys are listed in the LAYOUT_ITALIAN section of the keylayouts.h file located at: +// C:\Program Files (x86)\Arduino\hardware\teensy\avr\cores\teensy3 +// +/* Notes from Luigi Caradonna +For a non-US keyboard, look at the file keylayouts.h under the desired language and find the desired key, then use the corresponding US key name. +Example for the quote ' character on the Italian keyboard, you will find the quote in the comment +#define ASCII_27 KEY_MINUS // 39 ' +thus, into the normal keys array, where the quote would go, you insert KEY_MINUS +Example for the + key on the Italian keyboard, you will find the + in the comment +#define ASCII_2B KEY_RIGHT_BRACE // 43 + +thus you must use KEY_RIGHT_BRACE where + would go. Do this for all other special keys. +The backslash is not listed because it seems it is a unique character but once you know the ASCII code is 92, +you can use the US key with the same ASCII code. The tilde key has ASCII code 92. +#define ASCII_5C KEY_TILDE // 92. +The "less than key" < also known as "angular braces key" doesn't work like the other keys. Using KEY_NON_US_100 did not have any effect on the output. +To make it work, this key is intercepted in the code and the correct literal is sent using Keyboard.press(). +Look at lines 375 to 387 for < and > key coding. +*/ +// +int normal[rows_max][cols_max] = { +// 0 1 2 3 4 5 6 7 + {0, 0, 0, 0, 0, 0, 0, 0}, // 0 + {KEY_TAB, 0, KEY_Z, KEY_A, KEY_1, KEY_Q, KEY_TILDE, KEY_ESC}, // 1 + {KEY_Y, KEY_N, KEY_M, KEY_J, KEY_7, KEY_U, KEY_6, KEY_H}, // 2 + {KEY_F3, 0, KEY_C, KEY_D, KEY_3, KEY_E, KEY_F2, KEY_F4}, // 3 + {KEY_CAPS_LOCK, 0, KEY_X, KEY_S, KEY_2, KEY_W, KEY_F1, KEY_NON_US_100},// 4 + {KEY_T, KEY_B, KEY_V, KEY_F, KEY_4, KEY_R, KEY_5, KEY_G}, // 5 + {KEY_F7, 0, KEY_PERIOD, KEY_L, KEY_9, KEY_O, KEY_F8, 0}, // 6 + {ISO_8859_1_E8, KEY_SLASH, ISO_8859_1_F9, ISO_8859_1_F2, KEY_0, KEY_P, KEY_MINUS, ISO_8859_1_E0}, // 7 + {KEY_RIGHT_BRACE, 0, KEY_COMMA, KEY_K, KEY_8, KEY_I, ISO_8859_1_EC, KEY_F6}, // 8 + {0, 0, 0, 0, 0, 0, 0, 0}, // 9 + {0, 0, 0, 0, KEY_PRINTSCREEN, 0, 0, 0}, // 10 + {0, KEY_RIGHT, 0, 0, KEY_F12, 0, 0, 0}, // 11 + {0, KEY_LEFT, KEY_DELETE, 0, KEY_PAGE_DOWN, 0, KEY_HOME, KEY_UP}, // 12 + {0, KEY_DOWN, 0, 0, KEY_F11, 0, 0, 0}, // 13 + {KEY_BACKSPACE, KEY_SPACE, KEY_ENTER, 0, KEY_F10, 0, KEY_F9, KEY_F5}, // 14 + {0, 0, 0, KEY_MENU, KEY_PAGE_UP, 0, KEY_END, 0} // 15 +}; +// Load the modifier key matrix with key names at the correct row-column location. +// A zero indicates no modifier key at that location. +int modifier[rows_max][cols_max] = { + {MODIFIERKEY_LEFT_SHIFT,0,MODIFIERKEY_RIGHT_SHIFT,0,0,0,0,0}, + {0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0}, + {0,0,MODIFIERKEY_RIGHT_CTRL,0,0,0,MODIFIERKEY_LEFT_CTRL,0}, + {0,MODIFIERKEY_RIGHT_ALT,0,0,0,0,0,MODIFIERKEY_LEFT_ALT}, + {0,0,0,0,0,0,0,0}, + {0,0,0,MODIFIERKEY_FN,0,0,0,0}, + {0,0,0,0,0,0,0,0}, + {0,0,0,0,0,0,0,0}, + {0,0,0,0,0,MODIFIERKEY_GUI,0,0} +}; +// Load the media key matrix with Fn key names at the correct row-column location. +// A zero indicates no media key at that location. +int media[rows_max][cols_max] = { + {0,0,0,0,0,0,0,0}, // 0 + {0,0,0,0,0,0,0,0}, // 1 + {0,0,KEYPAD_0,KEYPAD_1,KEYPAD_7,KEYPAD_4,0,0}, // 2 + {0,0,0,0,0,0,0,0}, // 3 + {0,0,0,0,0,0,0,0}, // 4 + {0,0,0,0,0,0,0,0}, // 5 + {0,0,KEYPAD_PERIOD,KEYPAD_3,KEYPAD_9,KEYPAD_6,0,0}, // 6 + {0,KEYPAD_PLUS,0,KEYPAD_MINUS,KEYPAD_SLASH,KEYPAD_ASTERIX,0,0}, // 7 + {0,0,0,KEYPAD_2,KEYPAD_8,KEYPAD_5,0,0}, // 8 + {0,0,0,0,0,0,0,0}, // 9 + {0,0,0,0,0,0,0,0}, // 10 + {0,KEY_MEDIA_VOLUME_INC,0,0,KEY_MEDIA_NEXT_TRACK,0,0,0}, // 11 + {0,0,0,0,0,0,0,0}, // 12 + {0,KEY_MEDIA_VOLUME_DEC,0,0,KEY_MEDIA_PREV_TRACK,0,0,0}, // 13 + {0,KEY_SPACE,0,0,KEY_MEDIA_STOP,0,KEY_MEDIA_PLAY_PAUSE,0}, // 14 + {0,0,0,0,0,0,0,0} // 15 +}; +// Initialize the old_key matrix with one's. +// 1 = key not pressed, 0 = key is pressed +boolean old_key[rows_max][cols_max] = { + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1}, + {1,1,1,1,1,1,1,1} +}; +// +// Define the Teensy LC I/O numbers (translated from the FPC pin #) +// Row FPC pin # 01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16,17 + +int Row_IO[rows_max] = {2,25,5,19,18,7,17,8,16,9,15,10,14,11,26,12}; // Teensy LC I/O numbers for rows +// +// Column FPC pin # 18,19,20,21,22,23,24,25 + +int Col_IO[cols_max] = {22,1,24,21,3,4,20,6}; // Teensy LC I/O numbers for columns + +// Declare variables that will be used by functions +boolean slots_full = LOW; // Goes high when slots 1 thru 6 contain normal keys +// slot 1 thru slot 6 hold the normal key values to be sent over USB. +int slot1 = 0; //value of 0 means the slot is empty and can be used. +int slot2 = 0; +int slot3 = 0; +int slot4 = 0; +int slot5 = 0; +int slot6 = 0; +// +int mod_shift_l = 0; // These variables are sent over USB as modifier keys. +int mod_shift_r = 0; // Each is either set to 0 or MODIFIER_ ... +int mod_ctrl_l = 0; +int mod_ctrl_r = 0; +int mod_alt_l = 0; +int mod_alt_r = 0; +int mod_gui = 0; +// +// Function to load the key name into the first available slot +void load_slot(int key) { + if (!slot1) { + slot1 = key; + } + else if (!slot2) { + slot2 = key; + } + else if (!slot3) { + slot3 = key; + } + else if (!slot4) { + slot4 = key; + } + else if (!slot5) { + slot5 = key; + } + else if (!slot6) { + slot6 = key; + } + if (!slot1 || !slot2 || !slot3 || !slot4 || !slot5 || !slot6) { + slots_full = LOW; // slots are not full + } + else { + slots_full = HIGH; // slots are full + } +} +// +// Function to clear the slot that contains the key name +void clear_slot(int key) { + if (slot1 == key) { + slot1 = 0; + } + else if (slot2 == key) { + slot2 = 0; + } + else if (slot3 == key) { + slot3 = 0; + } + else if (slot4 == key) { + slot4 = 0; + } + else if (slot5 == key) { + slot5 = 0; + } + else if (slot6 == key) { + slot6 = 0; + } + if (!slot1 || !slot2 || !slot3 || !slot4 || !slot5 || !slot6) { + slots_full = LOW; // slots are not full + } + else { + slots_full = HIGH; // slots are full + } +} +// +// Function to load the modifier key name into the appropriate mod variable +void load_mod(int m_key) { + if (m_key == MODIFIERKEY_LEFT_SHIFT) { + mod_shift_l = m_key; + } + else if (m_key == MODIFIERKEY_RIGHT_SHIFT) { + mod_shift_r = m_key; + } + else if (m_key == MODIFIERKEY_LEFT_CTRL) { + mod_ctrl_l = m_key; + } + else if (m_key == MODIFIERKEY_RIGHT_CTRL) { + mod_ctrl_r = m_key; + } + else if (m_key == MODIFIERKEY_LEFT_ALT) { + mod_alt_l = m_key; + } + else if (m_key == MODIFIERKEY_RIGHT_ALT) { + mod_alt_r = m_key; + } + else if (m_key == MODIFIERKEY_GUI) { + mod_gui = m_key; + } +} +// +// Function to load 0 into the appropriate mod variable +void clear_mod(int m_key) { + if (m_key == MODIFIERKEY_LEFT_SHIFT) { + mod_shift_l = 0; + } + else if (m_key == MODIFIERKEY_RIGHT_SHIFT) { + mod_shift_r = 0; + } + else if (m_key == MODIFIERKEY_LEFT_CTRL) { + mod_ctrl_l = 0; + } + else if (m_key == MODIFIERKEY_RIGHT_CTRL) { + mod_ctrl_r = 0; + } + else if (m_key == MODIFIERKEY_LEFT_ALT) { + mod_alt_l = 0; + } + else if (m_key == MODIFIERKEY_RIGHT_ALT) { + mod_alt_r = 0; + } + else if (m_key == MODIFIERKEY_GUI) { + mod_gui = 0; + } +} +// +// Function to send the modifier keys over usb +void send_mod() { + Keyboard.set_modifier(mod_shift_l | mod_shift_r | mod_ctrl_l | mod_ctrl_r | mod_alt_l | mod_alt_r | mod_gui); + Keyboard.send_now(); +} +// +// Function to send the normal keys in the 6 slots over usb +void send_normals() { + Keyboard.set_key1(slot1); + Keyboard.set_key2(slot2); + Keyboard.set_key3(slot3); + Keyboard.set_key4(slot4); + Keyboard.set_key5(slot5); + Keyboard.set_key6(slot6); + Keyboard.send_now(); +} +// +// Function to set a pin to high impedance (acts like open drain output) +void go_z(int pin) +{ + pinMode(pin, INPUT); + digitalWrite(pin, HIGH); +} +// +// Function to set a pin as an input with a pullup +void go_pu(int pin) +{ + pinMode(pin, INPUT_PULLUP); + digitalWrite(pin, HIGH); +} +// +// Function to send a pin to a logic low +void go_0(int pin) +{ + pinMode(pin, OUTPUT); + digitalWrite(pin, LOW); +} +// +// Function to send a pin to a logic high +void go_1(int pin) +{ + pinMode(pin, OUTPUT); + digitalWrite(pin, HIGH); +} +// +//----------------------------------Setup------------------------------------------- +void setup() { + // Backlight initial state is off + pinMode(blpin, OUTPUT); + analogWriteFrequency(blpin, 400); // sets blpin to 400Hz PWM frequency + analogWrite(blpin, 0); // off + + for (int a = 0; a < cols_max; a++) { // loop thru all column pins + go_pu(Col_IO[a]); // set each column pin as an input with a pullup + } +// + for (int b = 0; b < rows_max; b++) { // loop thru all row pins + go_z(Row_IO[b]); // set each row pin as a floating output + } +} +// +boolean Fn_pressed = HIGH; // Initialize Fn key to HIGH = "not pressed" +// extern volatile uint8_t keyboard_leds; // 8 bits sent from Pi to Teensy that give keyboard LED status. Caps lock is bit D1. +// +//---------------------------------Main Loop--------------------------------------------- +// +void loop() { + // Scan keyboard matrix with an outer loop that drives each row low and an inner loop that reads every column (with pull ups). + // The routine looks at each key's present state (by reading the column input pin) and also the previous state from the last scan + // that was 30msec ago. The status of a key that was just pressed or just released is sent over USB and the state is saved in the old_key matrix. + // The keyboard keys will read as logic low if they are pressed (negative logic). + // The old_key matrix also uses negative logic (low=pressed). + // + for (int x = 0; x < rows_max; x++) { // loop thru the rows + go_0(Row_IO[x]); // Activate Row (send it low) + delayMicroseconds(10); // give the row time to go low and settle out + for (int y = 0; y < cols_max; y++) { // loop thru the columns + // **********Modifier keys including the Fn special case + if (modifier[x][y] != 0) { // check if modifier key exists at this location in the array (a non-zero value) + if (!digitalRead(Col_IO[y]) && (old_key[x][y])) { // Read column to see if key is low (pressed) and was previously not pressed + if (modifier[x][y] != MODIFIERKEY_FN) { // Exclude Fn modifier key + load_mod(modifier[x][y]); // function reads which modifier key is pressed and loads it into the appropriate mod_... variable + send_mod(); // function sends the state of all modifier keys over usb including the one that just got pressed + old_key[x][y] = LOW; // Save state of key as "pressed" + } + else { + Fn_pressed = LOW; // Fn status variable is active low + old_key[x][y] = LOW; // old_key state is "pressed" (active low) + } + } + else if (digitalRead(Col_IO[y]) && (!old_key[x][y])) { //check if key is not pressed and was previously pressed + if (modifier[x][y] != MODIFIERKEY_FN) { // Exclude Fn modifier key + clear_mod(modifier[x][y]); // function reads which modifier key was released and loads 0 into the appropriate mod_... variable + send_mod(); // function sends all mod's over usb including the one that just released + old_key[x][y] = HIGH; // Save state of key as "not pressed" + } + else { + Fn_pressed = HIGH; // Fn is no longer active + old_key[x][y] = HIGH; // old_key state is "not pressed" + } + } + } + // ***********end of modifier section + // + // ***********Normal keys and media keys in this section + else if ((normal[x][y] != 0) || (media[x][y] != 0)) { // check if normal or media key exists at this location in the array + if (!digitalRead(Col_IO[y]) && (old_key[x][y]) && (!slots_full)) { // check if key is pressed and was not previously pressed and slots not full + old_key[x][y] = LOW; // Save state of key as "pressed" + if (Fn_pressed) { // Fn_pressed is active low so it is not pressed and normal key needs to be sent + // **** SPECIAL for < and > which are not supported **** + // If the < key is pressed and also one between left or right shift is pressed + if(normal[x][y] == KEY_NON_US_100 && (mod_shift_l == MODIFIERKEY_LEFT_SHIFT || mod_shift_r == MODIFIERKEY_RIGHT_SHIFT)) { + Keyboard.press('>'); // > literal is sent using keyboard press function + delay(5); // delay 5 milliseconds before releasing to make sure it gets sent over USB + Keyboard.release('>'); // send key release + } + // If only < is pressed + else if (normal[x][y] == KEY_NON_US_100) { + Keyboard.press('<'); // < literal is sent using keyboard press function + delay(5); // delay 5 milliseconds before releasing to make sure it gets sent over USB + Keyboard.release('<'); // send key release + } + // All the other normal keys + else { + load_slot(normal[x][y]); //update first available slot with normal key name + send_normals(); // send all slots over USB including the key that just got pressed + } + } + else if (media[x][y] != 0) { // Fn is pressed so send media if a key exists in the matrix + if(media[x][y] != KEY_SPACE) { + Keyboard.press(media[x][y]); // media key is sent using keyboard press function per PJRC + delay(5); // delay 5 milliseconds before releasing to make sure it gets sent over USB + Keyboard.release(media[x][y]); // send media key release + } + else { // Fn+Space are pressed, set the backlight + switch(blight) { + case 0: // backlight off, turn on + analogWrite(blpin, 250); // sets blpin to a PWM duty cycle of 250/256=97% + blight = 1; // update the blight state + break; + case 1: // backlight on, turn off + analogWrite(blpin, 0); // off + blight = 0; // update the blight state + break; + default: + break; + } + // Keyboard.press(media[x][y]); // media key is sent using keyboard press function per PJRC + delay(5); // delay 5 milliseconds before releasing to make sure it gets sent over USB + Keyboard.release(media[x][y]); // send media key release + } + } + } + else if (digitalRead(Col_IO[y]) && (!old_key[x][y])) { //check if key is not pressed, but was previously pressed + old_key[x][y] = HIGH; // Save state of key as "not pressed" + if (Fn_pressed) { // Fn is not pressed + clear_slot(normal[x][y]); //clear the slot that contains the normal key name + send_normals(); // send all slots over USB including the key that was just released + } + } + } + // **************end of normal and media key section + } + go_z(Row_IO[x]); // De-activate Row (send it to hi-z) + } + // + // **********keyboard scan complete + delay(25); // The overall keyboard scanning rate is about 30ms +} diff --git a/Example_Keyboards/Lenovo_Y480_LC/y480_matrix.xlsx b/Example_Keyboards/Lenovo_Y480_LC/y480_matrix.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..533320c825521b0c11dda19c8c1aa54a65b77953 GIT binary patch literal 13074 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