├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── gpio_keyboard.c ├── gpio_keyboard.py ├── gpio_keyboard.service ├── img ├── cplus4.jpg ├── cplus4_keyboard_matrix.png └── cplus4_wiring.jpg └── rpi_gpio.h /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeFiles 2 | CMakeCache.txt 3 | Makefile 4 | build 5 | cmake_install.cmake 6 | install_manifest.txt 7 | .vscode 8 | gpio_keyboard 9 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.7) 2 | project(rpi-gpio-matrix-keyboard) 3 | 4 | add_executable(gpio_keyboard gpio_keyboard.c) 5 | set(CMAKE_C_FLAGS "-O3") 6 | 7 | install(FILES gpio_keyboard.service DESTINATION /etc/systemd/system/) 8 | install(TARGETS gpio_keyboard DESTINATION /usr/local/bin) 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michał Szafrański 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi GPIO Matrix Keyboard 2 | User space GPIO matrix keyboard driver for Raspbery Pi. Can be used to put Raspberry Pi into an old computer case with a functional keyboard 3 | without any extra hardware like Arduino or Keyrah. 4 | 5 | The code is designed for Commodore Plus/4 keyboard layout, but can be easily modifed to support other 80s and 90s computers' keyboards (Commodore 64, 8-bit Atari, Amiga, etc.) 6 | 7 | ![Commodore Plus/4](img/cplus4.jpg?raw=true "Commodore Plus/4") 8 | 9 | ## Compilation 10 | ``` 11 | cmake . 12 | make 13 | ``` 14 | ## Installation 15 | To install the keyboard driver as a systemd serice 16 | ``` 17 | sudo cmake install 18 | ``` 19 | ## Commodore Plus/4 Wiring 20 | This is the wiring used to define the `cols`, `rows`, and `keymap`. Additionally the LED pins can be connected to a free GPIO and GND for extra functionality (or simply +5v). 21 | ![Plus/4 Keyboard Matrix](img/cplus4_keyboard_matrix.png?raw=true "Plus/4 Keyboard Matrix") 22 | 23 | | Plus/4 Pin | Pi GPIO Pin | GPIO No. | Matrix map | 24 | |-----------:|------------:|---------:|-----------:| 25 | | 1| 3| 2| Row 6 | 26 | | 2| 7| 4| Col 4 | 27 | | 3| -| -| LED GND | 28 | | 4| -| -| LED | 29 | | 5| 8| 14| Row 1 | 30 | | 6| 10| 15| Col 5 | 31 | | 7| 11| 17| Row 2 | 32 | | 8| 12| 18| Col 6 | 33 | | 9| 13| 27| Col 7 | 34 | | 10| 15| 22| Row 4 | 35 | | 11| 16| 23| Row 3 | 36 | | 12| 18| 24| Row 5 | 37 | | 13| 19| 10| Col 3 | 38 | | 14| 21| 9| Col 2 | 39 | | 15| 22| 25| Row 7 | 40 | | 16| 23| 11| Col 8 | 41 | | 17| 24| 8| Col 1 | 42 | | 18| 26| 7| Row 8 | 43 | 44 | ![Wiring to Raspberry Pi](img/cplus4_wiring.jpg?raw=true "Wiring to Raspberry Pi") 45 | 46 | ## Python Prototype 47 | The Python script `gpio_keyboard.py` is my initial version of this driver. It has basically the same funcitonality except: 48 | * Python `evdev` does not seem to support `EV_REP` on the uinput objects, so the emulated keyboard will not generate auto-repeat 49 | * At 60Hz scanning frequency it takes about 6% CPU compared to <1% for the C version. 50 | -------------------------------------------------------------------------------- /gpio_keyboard.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "rpi_gpio.h" 7 | 8 | #define UINPUT_DEV_NAME "Commodore Pi/4 Keyboard" 9 | const int cols[] = { 8, 9, 10, 4, 15, 18, 27, 11 }; 10 | const int ncols = 8; 11 | const int rows[] = { 14, 17, 23, 22, 24, 2, 25, 7 }; 12 | const int nrows = 8; 13 | const int keymap[] = { 14 | KEY_1, KEY_DELETE, KEY_LEFTCTRL, KEY_LEFTALT, KEY_SPACE, KEY_LEFTMETA, KEY_Q, KEY_2, 15 | KEY_3, KEY_W, KEY_A, KEY_LEFTSHIFT, KEY_Z, KEY_S, KEY_E, KEY_4, 16 | KEY_5, KEY_R, KEY_D, KEY_X, KEY_C, KEY_F, KEY_T, KEY_6, 17 | KEY_7, KEY_Y, KEY_G, KEY_V, KEY_B, KEY_H, KEY_U, KEY_8, 18 | KEY_9, KEY_I, KEY_J, KEY_N, KEY_M, KEY_K, KEY_O, KEY_0, 19 | KEY_DOWN, KEY_P, KEY_L, KEY_COMMA, KEY_DOT, KEY_SEMICOLON, KEY_MINUS, KEY_UP, 20 | KEY_LEFT, KEY_BACKSLASH, KEY_APOSTROPHE, KEY_SLASH, KEY_ESC, KEY_EQUAL, KEY_GRAVE, KEY_RIGHT, 21 | KEY_BACKSPACE, KEY_ENTER, KEY_RIGHTBRACE, KEY_LEFTBRACE, KEY_F1, KEY_F2, KEY_F3, KEY_F4, 22 | }; 23 | 24 | int uinput_init() { 25 | struct uinput_setup usetup; 26 | int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); 27 | if (fd < 0) return -1; 28 | ioctl(fd, UI_SET_EVBIT, EV_KEY); 29 | ioctl(fd, UI_SET_EVBIT, EV_REP); 30 | for (int i = 0; i < ncols * nrows; i++) { 31 | ioctl(fd, UI_SET_KEYBIT, keymap[i]); 32 | } 33 | memset(&usetup, 0, sizeof(usetup)); 34 | usetup.id.bustype = BUS_USB; 35 | usetup.id.vendor = 0x1209; // Generic 36 | usetup.id.product = 0x7501; 37 | strcpy(usetup.name, UINPUT_DEV_NAME); 38 | ioctl(fd, UI_DEV_SETUP, &usetup); 39 | ioctl(fd, UI_DEV_CREATE); 40 | return fd; 41 | } 42 | void uinput_emit(int fd, int type, int code, int val) { 43 | struct input_event ie; 44 | ie.type = type; 45 | ie.code = code; 46 | ie.value = val; 47 | ie.time.tv_sec = 0; 48 | ie.time.tv_usec = 0; 49 | write(fd, &ie, sizeof(ie)); 50 | } 51 | 52 | int main() { 53 | int uinput_fd = uinput_init(); 54 | if (uinput_fd < 0) { 55 | perror("Failed to initialize UInput"); 56 | return -1; 57 | } 58 | if (rpi_gpio_init() < 0) { 59 | perror("Failed to initialize RPi GPIO"); 60 | return -1; 61 | } 62 | for (int i = 0; i < nrows; i++) { 63 | rpi_gpio_setup(rows[i], INPUT, PUD_OFF); 64 | } 65 | for (int i = 0; i < ncols; i++) { 66 | rpi_gpio_setup(cols[i], INPUT, PUD_UP); 67 | } 68 | int pressed[nrows * ncols]; 69 | memset(pressed, 0, sizeof(pressed)); 70 | for (;;) { 71 | usleep(1000000 / 60); 72 | int syn = 0; 73 | for (int i = 0; i < nrows; i++) { 74 | rpi_gpio_output(rows[i], 0); 75 | rpi_gpio_set_fn(rows[i], OUTPUT); 76 | usleep(3); 77 | uint32_t all = rpi_gpio_input_all(); 78 | rpi_gpio_set_fn(rows[i], INPUT); 79 | for (int j = 0; j < ncols; j++) { 80 | int keycode = i * ncols + j; 81 | int val = !(all & (1 << cols[j])); 82 | if (val != pressed[keycode]) { 83 | pressed[keycode] = val; 84 | uinput_emit(uinput_fd, EV_KEY, keymap[keycode], val); 85 | syn++; 86 | } 87 | } 88 | } 89 | if (syn) uinput_emit(uinput_fd, EV_SYN, SYN_REPORT, 0); 90 | } 91 | return 0; 92 | } 93 | -------------------------------------------------------------------------------- /gpio_keyboard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import RPi.GPIO as GPIO 3 | from time import sleep 4 | from evdev import UInput, ecodes as e 5 | 6 | ui = UInput(name = "Commodore Pi/4 Keyboard", vendor = 0x1209, product = 0x7501) 7 | cols = [8, 9, 10, 4, 15, 18, 27, 11] 8 | rows = [14, 17, 23, 22, 24, 2, 25, 7] 9 | keymap = [ 10 | e.KEY_1, e.KEY_DELETE, e.KEY_LEFTCTRL, e.KEY_LEFTALT, e.KEY_SPACE, e.KEY_LEFTMETA, e.KEY_Q, e.KEY_2, 11 | e.KEY_3, e.KEY_W, e.KEY_A, e.KEY_LEFTSHIFT, e.KEY_Z, e.KEY_S, e.KEY_E, e.KEY_4, 12 | e.KEY_5, e.KEY_R, e.KEY_D, e.KEY_X, e.KEY_C, e.KEY_F, e.KEY_T, e.KEY_6, 13 | e.KEY_7, e.KEY_Y, e.KEY_G, e.KEY_V, e.KEY_B, e.KEY_H, e.KEY_U, e.KEY_8, 14 | e.KEY_9, e.KEY_I, e.KEY_J, e.KEY_N, e.KEY_M, e.KEY_K, e.KEY_O, e.KEY_0, 15 | e.KEY_DOWN, e.KEY_P, e.KEY_L, e.KEY_COMMA, e.KEY_DOT, e.KEY_SEMICOLON, e.KEY_MINUS, e.KEY_UP, 16 | e.KEY_LEFT, e.KEY_BACKSLASH, e.KEY_APOSTROPHE, e.KEY_SLASH, e.KEY_ESC, e.KEY_EQUAL, e.KEY_GRAVE, e.KEY_RIGHT, 17 | e.KEY_BACKSPACE, e.KEY_ENTER, e.KEY_RIGHTBRACE, e.KEY_LEFTBRACE, e.KEY_F1, e.KEY_F2, e.KEY_F3, e.KEY_F4, 18 | ] 19 | 20 | GPIO.setmode(GPIO.BCM) 21 | GPIO.setup(rows, GPIO.IN) 22 | GPIO.setup(cols, GPIO.IN, pull_up_down = GPIO.PUD_UP) 23 | pressed = set() 24 | while True: 25 | sleep(1/60) 26 | syn = False 27 | for i in range(len(rows)): 28 | GPIO.setup(rows[i], GPIO.OUT, initial = GPIO.LOW) 29 | for j in range(len(cols)): 30 | keycode = i * len(cols) + j 31 | newval = GPIO.input(cols[j]) == GPIO.LOW 32 | if newval and not keycode in pressed: 33 | pressed.add(keycode) 34 | GPIO.output(3, GPIO.HIGH) 35 | ui.write(e.EV_KEY, keymap[keycode], 1) 36 | syn = True 37 | elif not newval and keycode in pressed: 38 | pressed.discard(keycode) 39 | GPIO.output(3, GPIO.LOW) 40 | ui.write(e.EV_KEY, keymap[keycode], 0) 41 | syn = True 42 | GPIO.setup(rows[i], GPIO.IN) 43 | if syn: 44 | ui.syn() 45 | -------------------------------------------------------------------------------- /gpio_keyboard.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Commodore Pi/4 Keyboard Service 3 | 4 | [Service] 5 | ExecStart=/usr/local/bin/gpio_keyboard 6 | WorkingDirectory=/ 7 | StandardOutput=inherit 8 | StandardError=inherit 9 | Restart=always 10 | User=root 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /img/cplus4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutki/rpi-gpio-matrix-keyboard/d29202d947600f3782dc7f97d1864cb43f948264/img/cplus4.jpg -------------------------------------------------------------------------------- /img/cplus4_keyboard_matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutki/rpi-gpio-matrix-keyboard/d29202d947600f3782dc7f97d1864cb43f948264/img/cplus4_keyboard_matrix.png -------------------------------------------------------------------------------- /img/cplus4_wiring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nutki/rpi-gpio-matrix-keyboard/d29202d947600f3782dc7f97d1864cb43f948264/img/cplus4_wiring.jpg -------------------------------------------------------------------------------- /rpi_gpio.h: -------------------------------------------------------------------------------- 1 | #ifndef _RPI_GPIO_H 2 | #define _RPI_GPIO_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define INPUT 0 11 | #define OUTPUT 1 12 | 13 | #define PUD_OFF 0 14 | #define PUD_DOWN 1 15 | #define PUD_UP 2 16 | 17 | #define FSEL_OFFSET 0 // 0x00 / 4 18 | #define SET_OFFSET 7 // 0x1c / 4 19 | #define CLR_OFFSET 10 // 0x28 / 4 20 | #define PINLEVEL_OFFSET 13 // 0x34 / 4 21 | #define EVENT_DETECT_OFFSET 16 // 0x40 / 4 22 | #define RISING_ED_OFFSET 19 // 0x4c / 4 23 | #define FALLING_ED_OFFSET 22 // 0x58 / 4 24 | #define HIGH_DETECT_OFFSET 25 // 0x64 / 4 25 | #define LOW_DETECT_OFFSET 28 // 0x70 / 4 26 | #define PULLUPDN_OFFSET 37 // 0x94 / 4 27 | #define PULLUPDNCLK_OFFSET 38 // 0x98 / 4 28 | #define PULLUPDN_OFFSET_2711 57 // 0xe4 / 4 29 | #define IS_BCM2711 (gpio_map[PULLUPDN_OFFSET_2711 + 3] != 0x6770696f) 30 | 31 | #define GPIO_PAGE_SIZE 4096 32 | 33 | static volatile uint32_t *gpio_map; 34 | 35 | void spin_delay(int n) { 36 | for (int i=0; i> gpio) & 1; 41 | } 42 | static inline int read2(int reg, int gpio) { 43 | return (gpio_map[reg + gpio/16] >> (gpio % 16 * 2)) & 3; 44 | } 45 | static inline int read3(int reg, int gpio) { 46 | return (gpio_map[reg + gpio/10] >> (gpio % 10 * 3)) & 7; 47 | } 48 | static inline void write1(int reg, int gpio, int val) { 49 | uint32_t bit = 1 << gpio; 50 | if (val & 1) gpio_map[reg] |= bit; else gpio_map[reg] &= ~bit; 51 | } 52 | static inline void write2(int reg, int gpio, int val) { 53 | uint32_t bit = gpio % 16 * 2; 54 | reg += gpio / 16; 55 | gpio_map[reg] = gpio_map[reg] & ~(3 << bit) | ((val & 3) << bit); 56 | } 57 | static inline void write3(int reg, int gpio, int val) { 58 | uint32_t bit = gpio % 10 * 3; 59 | reg += gpio / 10; 60 | gpio_map[reg] = gpio_map[reg] & ~(7 << bit) | ((val & 7) << bit); 61 | } 62 | 63 | static inline int rpi_gpio_init(void) { 64 | int mem_fd = open("/dev/gpiomem", O_RDWR | O_SYNC); 65 | if (mem_fd < 0) 66 | return -1; 67 | gpio_map = (uint32_t *)mmap(NULL, GPIO_PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, mem_fd, 0); 68 | close(mem_fd); 69 | if (gpio_map == MAP_FAILED) 70 | return -1; 71 | return 0; 72 | } 73 | 74 | static inline void rpi_gpio_set_pull(int gpio, int pud) { 75 | if (IS_BCM2711) { 76 | // BCM2711 of Pi 4 has direct pull registers 77 | write2(PULLUPDN_OFFSET_2711, gpio, pud == PUD_UP ? 1 : pud == PUD_DOWN ? 2 : 0); 78 | } else { 79 | // Older Pi models (BCM2835/6/7) 80 | write3(PULLUPDN_OFFSET, 0, pud); 81 | spin_delay(150); 82 | write1(PULLUPDNCLK_OFFSET, gpio, 1); 83 | spin_delay(150); 84 | write3(PULLUPDN_OFFSET, 0, 0); 85 | write1(PULLUPDNCLK_OFFSET, gpio, 0); 86 | } 87 | } 88 | 89 | static inline void rpi_gpio_set_fn(int gpio, int direction) { 90 | write3(FSEL_OFFSET, gpio, direction); 91 | } 92 | 93 | static inline void rpi_gpio_setup(int gpio, int direction, int pud) { 94 | rpi_gpio_set_pull(gpio, pud); 95 | rpi_gpio_set_fn(gpio, direction); 96 | } 97 | 98 | static inline int rpi_gpio_get_fn(int gpio) { 99 | return read3(FSEL_OFFSET, gpio); 100 | } 101 | 102 | static inline void rpi_gpio_output(int gpio, int value) { 103 | gpio_map[value ? SET_OFFSET : CLR_OFFSET] = 1 << gpio; 104 | } 105 | 106 | void rpi_gpio_output_all(uint32_t mask, int value) { 107 | gpio_map[value ? SET_OFFSET : CLR_OFFSET] = mask; 108 | } 109 | 110 | int rpi_gpio_input(int gpio) { 111 | return read1(PINLEVEL_OFFSET, gpio); 112 | } 113 | 114 | uint32_t rpi_gpio_input_all(void) { 115 | return gpio_map[PINLEVEL_OFFSET]; 116 | } 117 | 118 | void rpi_gpio_cleanup(void) { 119 | if (gpio_map) 120 | munmap((void *)gpio_map, GPIO_PAGE_SIZE); 121 | } 122 | 123 | #endif 124 | --------------------------------------------------------------------------------