diff --git a/src/config/config_usb.c b/src/config/config_usb.c index 9c9305d7..dc0e6e6a 100644 --- a/src/config/config_usb.c +++ b/src/config/config_usb.c @@ -43,3 +43,10 @@ REQUIRE_OBJECT ( ehci ); #ifdef USB_HCD_UHCI REQUIRE_OBJECT ( uhci ); #endif + +/* + * Drag in USB peripherals + */ +#ifdef USB_KEYBOARD +REQUIRE_OBJECT ( usbkbd ); +#endif diff --git a/src/config/defaults/pcbios.h b/src/config/defaults/pcbios.h index dfd3e2f8..4ad81210 100644 --- a/src/config/defaults/pcbios.h +++ b/src/config/defaults/pcbios.h @@ -39,6 +39,7 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); #define USB_HCD_XHCI /* xHCI USB host controller */ #define USB_HCD_EHCI /* EHCI USB host controller */ #define USB_HCD_UHCI /* UHCI USB host controller */ +#define USB_KEYBOARD /* USB keyboards */ #define REBOOT_CMD /* Reboot command */ #define CPUID_CMD /* x86 CPU feature detection command */ diff --git a/src/config/usb.h b/src/config/usb.h index 0181c50e..52e82eaa 100644 --- a/src/config/usb.h +++ b/src/config/usb.h @@ -19,6 +19,12 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); //#undef USB_HCD_EHCI /* EHCI USB host controller */ //#undef USB_HCD_UHCI /* UHCI USB host controller */ +/* + * USB peripherals + * + */ +//#undef USB_KEYBOARD /* USB keyboards */ + #include #include NAMED_CONFIG(usb.h) #include 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 . + * + * 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 +#include +#include +#include +#include +#include +#include +#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, +}; diff --git a/src/drivers/usb/usbkbd.h b/src/drivers/usb/usbkbd.h new file mode 100644 index 00000000..7eab24e4 --- /dev/null +++ b/src/drivers/usb/usbkbd.h @@ -0,0 +1,154 @@ +#ifndef _USBKBD_H +#define _USBKBD_H + +/** @file + * + * USB keyboard driver + * + */ + +FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); + +#include +#include +#include + +/** Keyboard protocol */ +#define USBKBD_PROTOCOL 1 + +/** A USB keyboard report */ +struct usb_keyboard_report { + /** Modifier keys */ + uint8_t modifiers; + /** Reserved */ + uint8_t reserved; + /** Keycodes */ + uint8_t keycode[6]; +} __attribute__ (( packed )); + +/** USB modifier keys */ +enum usb_keyboard_modifier { + /** Left Ctrl key */ + USBKBD_CTRL_LEFT = 0x01, + /** Left Shift key */ + USBKBD_SHIFT_LEFT = 0x02, + /** Left Alt key */ + USBKBD_ALT_LEFT = 0x04, + /** Left GUI key */ + USBKBD_GUI_LEFT = 0x08, + /** Right Ctrl key */ + USBKBD_CTRL_RIGHT = 0x10, + /** Right Shift key */ + USBKBD_SHIFT_RIGHT = 0x20, + /** Right Alt key */ + USBKBD_ALT_RIGHT = 0x40, + /** Right GUI key */ + USBKBD_GUI_RIGHT = 0x80, +}; + +/** Either Ctrl key */ +#define USBKBD_CTRL ( USBKBD_CTRL_LEFT | USBKBD_CTRL_RIGHT ) + +/** Either Shift key */ +#define USBKBD_SHIFT ( USBKBD_SHIFT_LEFT | USBKBD_SHIFT_RIGHT ) + +/** Either Alt key */ +#define USBKBD_ALT ( USBKBD_ALT_LEFT | USBKBD_ALT_RIGHT ) + +/** Either GUI key */ +#define USBKBD_GUI ( USBKBD_GUI_LEFT | USBKBD_GUI_RIGHT ) + +/** USB keycodes */ +enum usb_keycode { + USBKBD_KEY_A = 0x04, + USBKBD_KEY_Z = 0x1d, + USBKBD_KEY_1 = 0x1e, + USBKBD_KEY_0 = 0x27, + USBKBD_KEY_ENTER = 0x28, + USBKBD_KEY_SPACE = 0x2c, + USBKBD_KEY_MINUS = 0x2d, + USBKBD_KEY_SLASH = 0x38, + USBKBD_KEY_CAPSLOCK = 0x39, + USBKBD_KEY_UP = 0x52, +}; + +/** Keyboard idle duration (in 4ms units) + * + * This is a policy decision. We choose to use an autorepeat rate of + * approximately 40ms. + */ +#define USBKBD_IDLE_DURATION 10 /* 10 x 4ms = 40ms */ + +/** Keyboard auto-repeat hold-off (in units of USBKBD_IDLE_DURATION) + * + * This is a policy decision. We choose to use an autorepeat delay of + * approximately 500ms. + */ +#define USBKBD_HOLDOFF 12 /* 12 x 40ms = 480ms */ + +/** Interrupt endpoint maximum fill level + * + * When idling, we are likely to poll the USB endpoint at only the + * 18.2Hz system timer tick rate. With a typical observed bInterval + * of 10ms (which will be rounded down to 8ms by the HCI drivers), + * this gives approximately 7 completions per poll. + */ +#define USBKBD_INTR_MAX_FILL 8 + +/** Keyboard buffer size + * + * Must be a power of two. + */ +#define USBKBD_BUFSIZE 8 + +/** A USB keyboard device */ +struct usb_keyboard { + /** Name */ + const char *name; + /** List of all USB keyboards */ + struct list_head list; + + /** USB bus */ + struct usb_bus *bus; + /** USB human interface device */ + struct usb_hid hid; + + /** Most recent keyboard report */ + struct usb_keyboard_report report; + /** Most recently pressed non-modifier key (if any) */ + unsigned int keycode; + /** Autorepeat hold-off time (in number of completions reported) */ + unsigned int holdoff; + + /** Keyboard buffer + * + * This stores iPXE key values. + */ + unsigned int key[USBKBD_BUFSIZE]; + /** Keyboard buffer producer counter */ + unsigned int prod; + /** Keyboard buffer consumer counter */ + unsigned int cons; + /** Keyboard buffer sub-consumer counter + * + * This represents the index within the ANSI escape sequence + * corresponding to an iPXE key value. + */ + unsigned int subcons; +}; + +/** + * Calculate keyboard buffer fill level + * + * @v kbd USB keyboard + * @ret fill Keyboard buffer fill level + */ +static inline __attribute__ (( always_inline )) unsigned int +usbkbd_fill ( struct usb_keyboard *kbd ) { + unsigned int fill = ( kbd->prod - kbd->cons ); + + assert ( fill <= USBKBD_BUFSIZE ); + return fill; +} + +#endif /* _USBKBD_H */ diff --git a/src/include/ipxe/errfile.h b/src/include/ipxe/errfile.h index c736c624..eca9bbd4 100644 --- a/src/include/ipxe/errfile.h +++ b/src/include/ipxe/errfile.h @@ -82,6 +82,7 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); #define ERRFILE_ehci ( ERRFILE_DRIVER | 0x000a0000 ) #define ERRFILE_uhci ( ERRFILE_DRIVER | 0x000b0000 ) #define ERRFILE_usbhid ( ERRFILE_DRIVER | 0x000c0000 ) +#define ERRFILE_usbkbd ( ERRFILE_DRIVER | 0x000d0000 ) #define ERRFILE_nvs ( ERRFILE_DRIVER | 0x00100000 ) #define ERRFILE_spi ( ERRFILE_DRIVER | 0x00110000 ) diff --git a/src/include/ipxe/keys.h b/src/include/ipxe/keys.h index 783de6cb..d15267a1 100644 --- a/src/include/ipxe/keys.h +++ b/src/include/ipxe/keys.h @@ -58,6 +58,8 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL ); */ #define KEY_ANSI( n, terminator ) ( 0x100 * ( (n) + 1 ) + (terminator) ) +#define KEY_ANSI_N( key ) ( ( (key) / 0x100 ) - 1 ) +#define KEY_ANSI_TERMINATOR( key ) ( (key) & 0xff ) #define KEY_MIN 0x101 #define KEY_UP KEY_ANSI ( 0, 'A' ) /**< Up arrow */