diff options
author | Michael Brown <mcb30@ipxe.org> | 2015-05-11 15:42:54 +0100 |
---|---|---|
committer | Michael Brown <mcb30@ipxe.org> | 2015-05-12 15:53:22 +0100 |
commit | 372672275e48a7f0fcd058476bd742bf950e4074 (patch) | |
tree | e4d781c7be57583d93aaac215c0fa5e01bd89d34 /src/drivers/usb/usbkbd.c | |
parent | a8e4187c455aa89f2de7376f959a24f6c7327e11 (diff) | |
download | ipxe-372672275e48a7f0fcd058476bd742bf950e4074.tar.gz |
[usb] Add basic support for USB keyboards
When USB network card drivers are used, the BIOS' legacy USB
capability is necessarily disabled since there is no way to share the
host controller between the BIOS and iPXE. This currently results in
USB keyboards becoming non-functional in USB-enabled builds of iPXE.
Fix by adding basic support for USB keyboards, enabled by default in
iPXE builds which include USB support.
Signed-off-by: Michael Brown <mcb30@ipxe.org>
Diffstat (limited to 'src/drivers/usb/usbkbd.c')
-rw-r--r-- | src/drivers/usb/usbkbd.c | 509 |
1 files changed, 509 insertions, 0 deletions
diff --git a/src/drivers/usb/usbkbd.c b/src/drivers/usb/usbkbd.c new file mode 100644 index 00000000..ea94f2e6 --- /dev/null +++ b/src/drivers/usb/usbkbd.c @@ -0,0 +1,509 @@ +/* + * Copyright (C) 2015 Michael Brown <mbrown@fensystems.co.uk>. + * + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + * + * You can also choose to distribute this program under the terms of + * the Unmodified Binary Distribution Licence (as given in the file + * COPYING.UBDL), provided that you have satisfied its requirements. + */ + +FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); + +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <assert.h> +#include <ipxe/console.h> +#include <ipxe/keys.h> +#include <ipxe/usb.h> +#include "usbkbd.h" + +/** @file + * + * USB keyboard driver + * + */ + +/** List of USB keyboards */ +static LIST_HEAD ( usb_keyboards ); + +/****************************************************************************** + * + * Keyboard map + * + ****************************************************************************** + */ + +/** + * Map USB keycode to iPXE key + * + * @v keycode Keycode + * @v modifiers Modifiers + * @ret key iPXE key + * + * Key codes are defined in the USB HID Usage Tables Keyboard/Keypad + * page. + */ +static unsigned int usbkbd_map ( unsigned int keycode, + unsigned int modifiers ) { + unsigned int key; + + if ( keycode < USBKBD_KEY_A ) { + /* Not keys */ + key = 0; + } else if ( keycode <= USBKBD_KEY_Z ) { + /* Alphabetic keys */ + key = ( keycode - USBKBD_KEY_A + 'a' ); + if ( modifiers & USBKBD_CTRL ) { + key -= ( 'a' - CTRL_A ); + } else if ( modifiers & USBKBD_SHIFT ) { + key -= ( 'a' - 'A' ); + } + } else if ( keycode <= USBKBD_KEY_0 ) { + /* Numeric key row */ + if ( modifiers & USBKBD_SHIFT ) { + key = "!@#$%^&*()" [ keycode - USBKBD_KEY_1 ]; + } else { + key = ( ( ( keycode - USBKBD_KEY_1 + 1 ) % 10 ) + '0' ); + } + } else if ( keycode <= USBKBD_KEY_SPACE ) { + /* Unmodifiable keys */ + static const uint8_t unmodifable[] = + { LF, ESC, BACKSPACE, TAB, ' ' }; + key = unmodifable[ keycode - USBKBD_KEY_ENTER ]; + } else if ( keycode <= USBKBD_KEY_SLASH ) { + /* Punctuation keys */ + if ( modifiers & USBKBD_SHIFT ) { + key = "_+{}|~:\"~<>?" [ keycode - USBKBD_KEY_MINUS ]; + } else { + key = "-=[]\\#;'`,./" [ keycode - USBKBD_KEY_MINUS ]; + } + } else if ( keycode <= USBKBD_KEY_UP ) { + /* Special keys */ + static const uint16_t special[] = { + 0, 0, 0, 0, 0, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, + KEY_F10, KEY_F11, KEY_F12, 0, 0, 0, KEY_IC, KEY_HOME, + KEY_PPAGE, KEY_DC, KEY_END, KEY_NPAGE, KEY_RIGHT, + KEY_LEFT, KEY_DOWN, KEY_UP + }; + key = special[ keycode - USBKBD_KEY_CAPSLOCK ]; + } else { + key = 0; + } + + return key; +} + +/****************************************************************************** + * + * Keyboard buffer + * + ****************************************************************************** + */ + +/** + * Insert keypress into keyboard buffer + * + * @v kbd USB keyboard + * @v keycode Keycode + * @v modifiers Modifiers + */ +static void usbkbd_produce ( struct usb_keyboard *kbd, unsigned int keycode, + unsigned int modifiers ) { + unsigned int key; + + /* Map to iPXE key */ + key = usbkbd_map ( keycode, modifiers ); + + /* Do nothing if this keycode has no corresponding iPXE key */ + if ( ! key ) { + DBGC ( kbd, "KBD %s has no key for keycode %#02x:%#02x\n", + kbd->name, modifiers, keycode ); + return; + } + + /* Check for buffer overrun */ + if ( usbkbd_fill ( kbd ) >= USBKBD_BUFSIZE ) { + DBGC ( kbd, "KBD %s buffer overrun (key %#02x)\n", + kbd->name, key ); + return; + } + + /* Insert into buffer */ + kbd->key[ ( kbd->prod++ ) % USBKBD_BUFSIZE ] = key; + DBGC2 ( kbd, "KBD %s key %#02x produced\n", kbd->name, key ); +} + +/** + * Consume character from keyboard buffer + * + * @v kbd USB keyboard + * @ret character Character + */ +static unsigned int usbkbd_consume ( struct usb_keyboard *kbd ) { + static char buf[] = "\x1b[xx~"; + char *tmp = &buf[2]; + unsigned int key; + unsigned int character; + unsigned int ansi_n; + unsigned int len; + + /* Sanity check */ + assert ( usbkbd_fill ( kbd ) > 0 ); + + /* Get current keypress */ + key = kbd->key[ kbd->cons % USBKBD_BUFSIZE ]; + + /* If this is a straightforward key, just consume and return it */ + if ( key < KEY_MIN ) { + kbd->cons++; + DBGC2 ( kbd, "KBD %s key %#02x consumed\n", kbd->name, key ); + return key; + } + + /* Construct ANSI sequence */ + ansi_n = KEY_ANSI_N ( key ); + if ( ansi_n ) + tmp += sprintf ( tmp, "%d", ansi_n ); + *(tmp++) = KEY_ANSI_TERMINATOR ( key ); + *tmp = '\0'; + len = ( tmp - buf ); + assert ( len < sizeof ( buf ) ); + if ( kbd->subcons == 0 ) { + DBGC2 ( kbd, "KBD %s key %#02x consumed as ^[%s\n", + kbd->name, key, &buf[1] ); + } + + /* Extract character from ANSI sequence */ + assert ( kbd->subcons < len ); + character = buf[ kbd->subcons++ ]; + + /* Consume key if applicable */ + if ( kbd->subcons == len ) { + kbd->cons++; + kbd->subcons = 0; + } + + return character; +} + +/****************************************************************************** + * + * Keyboard report + * + ****************************************************************************** + */ + +/** + * Check for presence of keycode in report + * + * @v report Keyboard report + * @v keycode Keycode (must be non-zero) + * @ret has_keycode Keycode is present in report + */ +static int usbkbd_has_keycode ( struct usb_keyboard_report *report, + unsigned int keycode ) { + unsigned int i; + + /* Check for keycode */ + for ( i = 0 ; i < ( sizeof ( report->keycode ) / + sizeof ( report->keycode[0] ) ) ; i++ ) { + if ( report->keycode[i] == keycode ) + return keycode; + } + + return 0; +} + +/** + * Handle keyboard report + * + * @v kbd USB keyboard + * @v new New keyboard report + */ +static void usbkbd_report ( struct usb_keyboard *kbd, + struct usb_keyboard_report *new ) { + struct usb_keyboard_report *old = &kbd->report; + unsigned int keycode; + unsigned int i; + + /* Check if current key has been released */ + if ( kbd->keycode && ! usbkbd_has_keycode ( new, kbd->keycode ) ) { + DBGC2 ( kbd, "KBD %s keycode %#02x released\n", + kbd->name, kbd->keycode ); + kbd->keycode = 0; + } + + /* Decrement auto-repeat hold-off timer, if applicable */ + if ( kbd->holdoff ) + kbd->holdoff--; + + /* Check if a new key has been pressed */ + for ( i = 0 ; i < ( sizeof ( new->keycode ) / + sizeof ( new->keycode[0] ) ) ; i++ ) { + + /* Ignore keys present in the previous report */ + keycode = new->keycode[i]; + if ( ( keycode == 0 ) || usbkbd_has_keycode ( old, keycode ) ) + continue; + DBGC2 ( kbd, "KBD %s keycode %#02x pressed\n", + kbd->name, keycode ); + + /* Insert keypress into keyboard buffer */ + usbkbd_produce ( kbd, keycode, new->modifiers ); + + /* Record as most recent keycode */ + kbd->keycode = keycode; + + /* Start auto-repeat hold-off timer */ + kbd->holdoff = USBKBD_HOLDOFF; + } + + /* Insert auto-repeated keypress into keyboard buffer, if applicable */ + if ( kbd->keycode && ! kbd->holdoff ) + usbkbd_produce ( kbd, kbd->keycode, new->modifiers ); + + /* Record report */ + memcpy ( old, new, sizeof ( *old ) ); +} + +/****************************************************************************** + * + * Interrupt endpoint + * + ****************************************************************************** + */ + +/** + * Complete interrupt transfer + * + * @v ep USB endpoint + * @v iobuf I/O buffer + * @v rc Completion status code + */ +static void usbkbd_complete ( struct usb_endpoint *ep, + struct io_buffer *iobuf, int rc ) { + struct usb_keyboard *kbd = container_of ( ep, struct usb_keyboard, + hid.in ); + struct usb_keyboard_report *report; + + /* Ignore packets cancelled when the endpoint closes */ + if ( ! ep->open ) + goto drop; + + /* Ignore packets with errors */ + if ( rc != 0 ) { + DBGC ( kbd, "KBD %s interrupt IN failed: %s\n", + kbd->name, strerror ( rc ) ); + goto drop; + } + + /* Ignore underlength packets */ + if ( iob_len ( iobuf ) < sizeof ( *report ) ) { + DBGC ( kbd, "KBD %s underlength report:\n", kbd->name ); + DBGC_HDA ( kbd, 0, iobuf->data, iob_len ( iobuf ) ); + goto drop; + } + report = iobuf->data; + + /* Handle keyboard report */ + usbkbd_report ( kbd, report ); + + drop: + /* Recycle I/O buffer */ + usb_recycle ( &kbd->hid.in, iobuf ); +} + +/** Interrupt endpoint operations */ +static struct usb_endpoint_driver_operations usbkbd_operations = { + .complete = usbkbd_complete, +}; + +/****************************************************************************** + * + * USB interface + * + ****************************************************************************** + */ + +/** + * Probe device + * + * @v func USB function + * @v config Configuration descriptor + * @ret rc Return status code + */ +static int usbkbd_probe ( struct usb_function *func, + struct usb_configuration_descriptor *config ) { + struct usb_device *usb = func->usb; + struct usb_keyboard *kbd; + int rc; + + /* Allocate and initialise structure */ + kbd = zalloc ( sizeof ( *kbd ) ); + if ( ! kbd ) { + rc = -ENOMEM; + goto err_alloc; + } + kbd->name = func->name; + kbd->bus = usb->port->hub->bus; + usbhid_init ( &kbd->hid, func, &usbkbd_operations, NULL ); + usb_refill_init ( &kbd->hid.in, sizeof ( kbd->report ), + USBKBD_INTR_MAX_FILL ); + + /* Describe USB human interface device */ + if ( ( rc = usbhid_describe ( &kbd->hid, config ) ) != 0 ) { + DBGC ( kbd, "KBD %s could not describe: %s\n", + kbd->name, strerror ( rc ) ); + goto err_describe; + } + DBGC ( kbd, "KBD %s using %s (len %zd)\n", + kbd->name, usb_endpoint_name ( &kbd->hid.in ), kbd->hid.in.mtu ); + + /* Set boot protocol */ + if ( ( rc = usbhid_set_protocol ( usb, func->interface[0], + USBHID_PROTOCOL_BOOT ) ) != 0 ) { + DBGC ( kbd, "KBD %s could not set boot protocol: %s\n", + kbd->name, strerror ( rc ) ); + goto err_set_protocol; + } + + /* Set idle time */ + if ( ( rc = usbhid_set_idle ( usb, func->interface[0], 0, + USBKBD_IDLE_DURATION ) ) != 0 ) { + DBGC ( kbd, "KBD %s could not set idle time: %s\n", + kbd->name, strerror ( rc ) ); + goto err_set_idle; + } + + /* Open USB human interface device */ + if ( ( rc = usbhid_open ( &kbd->hid ) ) != 0 ) { + DBGC ( kbd, "KBD %s could not open: %s\n", + kbd->name, strerror ( rc ) ); + goto err_open; + } + + /* Add to list of USB keyboards */ + list_add_tail ( &kbd->list, &usb_keyboards ); + + usb_func_set_drvdata ( func, kbd ); + return 0; + + usbhid_close ( &kbd->hid ); + err_open: + err_set_idle: + err_set_protocol: + err_describe: + free ( kbd ); + err_alloc: + return rc; +} + +/** + * Remove device + * + * @v func USB function + */ +static void usbkbd_remove ( struct usb_function *func ) { + struct usb_keyboard *kbd = usb_func_get_drvdata ( func ); + + /* Remove from list of USB keyboards */ + list_del ( &kbd->list ); + + /* Close USB human interface device */ + usbhid_close ( &kbd->hid ); + + /* Free device */ + free ( kbd ); +} + +/** USB keyboard device IDs */ +static struct usb_device_id usbkbd_ids[] = { + { + .name = "kbd", + .vendor = USB_ANY_ID, + .product = USB_ANY_ID, + .class = { + .class = USB_CLASS_HID, + .subclass = USB_SUBCLASS_HID_BOOT, + .protocol = USBKBD_PROTOCOL, + }, + }, +}; + +/** USB keyboard driver */ +struct usb_driver usbkbd_driver __usb_driver = { + .ids = usbkbd_ids, + .id_count = ( sizeof ( usbkbd_ids ) / sizeof ( usbkbd_ids[0] ) ), + .probe = usbkbd_probe, + .remove = usbkbd_remove, +}; + +/****************************************************************************** + * + * Console interface + * + ****************************************************************************** + */ + +/** + * Read a character from the console + * + * @ret character Character read + */ +static int usbkbd_getchar ( void ) { + struct usb_keyboard *kbd; + + /* Consume first available key */ + list_for_each_entry ( kbd, &usb_keyboards, list ) { + if ( usbkbd_fill ( kbd ) ) + return usbkbd_consume ( kbd ); + } + + return 0; +} + +/** + * Check for available input + * + * @ret is_available Input is available + */ +static int usbkbd_iskey ( void ) { + struct usb_keyboard *kbd; + unsigned int fill; + + /* Poll all USB keyboards and refill endpoints */ + list_for_each_entry ( kbd, &usb_keyboards, list ) { + usb_poll ( kbd->bus ); + usb_refill ( &kbd->hid.in ); + } + + /* Check for a non-empty keyboard buffer */ + list_for_each_entry ( kbd, &usb_keyboards, list ) { + fill = usbkbd_fill ( kbd ); + if ( fill ) + return fill; + } + + return 0; +} + +/** USB keyboard console */ +struct console_driver usbkbd_console __console_driver = { + .getchar = usbkbd_getchar, + .iskey = usbkbd_iskey, +}; |