summaryrefslogtreecommitdiffstats
path: root/src/drivers/usb
diff options
context:
space:
mode:
authorMichael Brown2015-05-11 16:42:54 +0200
committerMichael Brown2015-05-12 16:53:22 +0200
commit372672275e48a7f0fcd058476bd742bf950e4074 (patch)
treee4d781c7be57583d93aaac215c0fa5e01bd89d34 /src/drivers/usb
parent[usb] Add generic USB human interface device (HID) framework (diff)
downloadipxe-372672275e48a7f0fcd058476bd742bf950e4074.tar.gz
ipxe-372672275e48a7f0fcd058476bd742bf950e4074.tar.xz
ipxe-372672275e48a7f0fcd058476bd742bf950e4074.zip
[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')
-rw-r--r--src/drivers/usb/usbkbd.c509
-rw-r--r--src/drivers/usb/usbkbd.h154
2 files changed, 663 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,
+};
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 <assert.h>
+#include <ipxe/usb.h>
+#include <ipxe/usbhid.h>
+
+/** 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 */