drivers/usbhost: report HID keyboard through the keyboard upper half

The HID keyboard driver registers /dev/kbd[n] as a byte stream and only
ever reports key presses:  there is no way for it to say that a key was
released.  That is enough to type with, but not for an application that
has to know how long a key is held down, such as a game, where a
movement key would never stop being pressed.

Add CONFIG_HIDKBD_KBDUPPER, which registers the device through the
keyboard upper-half driver instead, so that reads return
struct keyboard_event_s.  Presses and releases are derived by comparing
each HID report against the previous one, which is the only way to tell
them apart given that the specification gives no significance to the
order of the keycodes in a report.  Modifiers are reported as keys of
their own, so an application can see that Ctrl is down and which key it
is held with.

The two interfaces are mutually exclusive because they share the device
name, so the byte stream is not built in this mode.

Signed-off-by: Jorge Guzman <jorge.gzm@gmail.com>
This commit is contained in:
Jorge Guzman 2026-07-29 22:51:12 -03:00
parent 8e37adbfeb
commit 4bcd105610
2 changed files with 345 additions and 19 deletions

View file

@ -399,6 +399,35 @@ config HIDKBD_ENCODED
keys, etc. If this not defined, only 7-bit print-able and control
ASCII characters will be provided to the user.
config HIDKBD_KBDUPPER
bool "Register as a keyboard upper-half device"
default n
depends on !HIDKBD_RAWSCANCODES
select INPUT
select INPUT_KEYBOARD
---help---
Register /dev/kbd[n] through the keyboard upper-half driver rather
than as a raw character device. Applications then read
struct keyboard_event_s samples instead of a byte stream, which
reports key release events in addition to key press events, and
reports modifiers (Ctrl, Shift, Alt, GUI) as keys of their own.
This is what an application needs when it must know how long a key
is held down, such as a game. Note that the byte stream interface
is not available at the same time, so this breaks applications that
expect to read characters from /dev/kbd[n].
if HIDKBD_KBDUPPER
config HIDKBD_KBDUPPER_BUFNUMBER
int "Keyboard upper-half buffer size"
default 8
---help---
The number of keyboard events buffered by the upper-half driver
for each opened instance.
endif # HIDKBD_KBDUPPER
config HIDKBD_ALLSCANCODES
bool "Use All Scancodes"
default n

View file

@ -56,11 +56,16 @@
#include <nuttx/usb/hid.h>
#include <nuttx/usb/usbhost_devaddr.h>
#ifdef CONFIG_HIDKBD_ENCODED
#if defined(CONFIG_HIDKBD_ENCODED) || defined(CONFIG_HIDKBD_KBDUPPER)
# include <nuttx/streams.h>
# include <nuttx/input/kbd_codec.h>
#endif
#ifdef CONFIG_HIDKBD_KBDUPPER
# include <nuttx/nuttx.h>
# include <nuttx/input/keyboard.h>
#endif
/* Don't compile if prerequisites are not met */
#if defined(CONFIG_USBHOST) && !defined(CONFIG_USBHOST_INT_DISABLE)
@ -133,6 +138,19 @@
#ifdef CONFIG_HIDKBD_RAWSCANCODES
# undef CONFIG_HIDKBD_ENCODED
# undef CONFIG_HIDKBD_KBDUPPER
#endif
/* The table that maps scancodes to special key encodings is shared by the
* encoded byte stream and by the keyboard upper-half interface.
*/
#if defined(CONFIG_HIDKBD_ENCODED) || defined(CONFIG_HIDKBD_KBDUPPER)
# define HAVE_KBD_ENCODING 1
#endif
#ifndef CONFIG_HIDKBD_KBDUPPER_BUFNUMBER
# define CONFIG_HIDKBD_KBDUPPER_BUFNUMBER 8
#endif
/* Driver support ***********************************************************/
@ -238,8 +256,12 @@ struct usbhost_state_s
struct work_s rwork; /* For interrupt transfer work */
int16_t nbytes; /* # of bytes actually transferred */
#endif
#ifndef CONFIG_HIDKBD_NODEBOUNCE
uint8_t lastkey[6]; /* For debouncing */
#if !defined(CONFIG_HIDKBD_NODEBOUNCE) || defined(CONFIG_HIDKBD_KBDUPPER)
uint8_t lastkey[6]; /* Keys down in the previous report */
#endif
#ifdef CONFIG_HIDKBD_KBDUPPER
struct keyboard_lowerhalf_s lower; /* Keyboard upper-half interface */
uint8_t lastmod; /* Modifiers held in previous report */
#endif
};
@ -272,8 +294,10 @@ static inline void usbhost_mkdevname(FAR struct usbhost_state_s *priv,
/* Keyboard polling thread */
static void usbhost_destroy(FAR void *arg);
#ifndef CONFIG_HIDKBD_KBDUPPER
static void usbhost_putbuffer(FAR struct usbhost_state_s *priv,
uint8_t keycode);
#endif
#ifdef CONFIG_HIDKBD_ENCODED
static void usbhost_putstream(FAR struct lib_outstream_s *self, int ch);
#endif
@ -335,8 +359,17 @@ static int usbhost_connect(FAR struct usbhost_class_s *usbclass,
FAR const uint8_t *configdesc, int desclen);
static int usbhost_disconnected(FAR struct usbhost_class_s *usbclass);
/* Driver methods. We export the keyboard as a standard character driver */
/* Driver methods. We export the keyboard either as a standard character
* driver or through the keyboard upper-half driver.
*/
static int usbhost_open_priv(FAR struct usbhost_state_s *priv);
static int usbhost_close_priv(FAR struct usbhost_state_s *priv);
#ifdef CONFIG_HIDKBD_KBDUPPER
static int usbhost_kbdupper_open(FAR struct keyboard_lowerhalf_s *lower);
static int usbhost_kbdupper_close(FAR struct keyboard_lowerhalf_s *lower);
#else
static int usbhost_open(FAR struct file *filep);
static int usbhost_close(FAR struct file *filep);
static ssize_t usbhost_read(FAR struct file *filep,
@ -345,6 +378,7 @@ static ssize_t usbhost_write(FAR struct file *filep,
FAR const char *buffer, size_t len);
static int usbhost_poll(FAR struct file *filep, FAR struct pollfd *fds,
bool setup);
#endif
/****************************************************************************
* Private Data
@ -374,6 +408,7 @@ static struct usbhost_registry_s g_hidkbd =
&g_hidkbd_id /* id[] */
};
#ifndef CONFIG_HIDKBD_KBDUPPER
static const struct file_operations g_hidkbd_fops =
{
usbhost_open, /* open */
@ -386,6 +421,24 @@ static const struct file_operations g_hidkbd_fops =
NULL, /* truncate */
usbhost_poll /* poll */
};
#endif
#ifdef CONFIG_HIDKBD_KBDUPPER
/* Maps the bits of the HID report modifier byte onto keycodes, so that a
* modifier can be reported as a key in its own right. Indexed by bit
* number: the modifier byte is defined by the HID specification to hold
* LCtrl, LShift, LAlt, LGUI, RCtrl, RShift, RAlt and RGUI, in that order.
*/
#define USBHID_NMODIFIERS 8
static const uint8_t g_modkeycode[USBHID_NMODIFIERS] =
{
KEYCODE_LCTRL, KEYCODE_LSHIFT, KEYCODE_LALT, KEYCODE_LGUI,
KEYCODE_RCTRL, KEYCODE_RSHIFT, KEYCODE_RALT, KEYCODE_RGUI
};
#endif
/* This is a bitmap that is used to allocate device names /dev/kbda-z. */
@ -403,7 +456,7 @@ static bool g_caps_lock = false;
*/
#ifndef CONFIG_HIDKBD_RAWSCANCODES
#ifdef CONFIG_HIDKBD_ENCODED
#ifdef HAVE_KBD_ENCODING
/* The first and last scancode values with encode-able values */
@ -800,7 +853,11 @@ static void usbhost_destroy(FAR void *arg)
uinfo("Unregister driver\n");
usbhost_mkdevname(priv, devname);
#ifdef CONFIG_HIDKBD_KBDUPPER
keyboard_unregister(&priv->lower, devname);
#else
unregister_driver(devname);
#endif
/* Release the device name used by this connection */
@ -860,6 +917,7 @@ static void usbhost_destroy(FAR void *arg)
*
****************************************************************************/
#ifndef CONFIG_HIDKBD_KBDUPPER
static void usbhost_putbuffer(FAR struct usbhost_state_s *priv,
uint8_t keycode)
{
@ -899,6 +957,7 @@ static void usbhost_putbuffer(FAR struct usbhost_state_s *priv,
priv->headndx = head;
}
#endif /* CONFIG_HIDKBD_KBDUPPER */
/****************************************************************************
* Name: usbhost_putstream
@ -1076,6 +1135,161 @@ static inline void usbhost_toggle_capslock(void)
spin_unlock_irqrestore(&g_lock, flags);
}
/****************************************************************************
* Name: usbhost_kbdupper_keycode
*
* Description:
* Map a HID scancode onto the keycode space used by the keyboard
* upper-half driver: 7-bit ASCII for printable keys, and the values of
* enum kbd_keycode_e for everything else.
*
* Input Parameters:
* scancode - Scan code to be mapped.
* modifier - Ctrl, Alt, Shift, GUI modifier bits
*
* Returned Value:
* The keycode, or zero if the scancode has no representation.
*
****************************************************************************/
#ifdef CONFIG_HIDKBD_KBDUPPER
static uint32_t usbhost_kbdupper_keycode(uint8_t scancode, uint8_t modifier)
{
/* Check for a special key first. Otherwise a key such as ENTER would be
* reported as the control character that it also maps to.
*/
if (scancode >= FIRST_ENCODING && scancode <= LAST_ENCODING)
{
uint8_t encoded = encoding[scancode - FIRST_ENCODING];
if (encoded != 0)
{
return encoded;
}
}
/* Otherwise fall back to the printable ASCII mapping. Note that Ctrl is
* deliberately not folded into a control character here: modifiers are
* reported as keys of their own, so the application still knows that Ctrl
* is down and can see which key it is being held with.
*/
return usbhost_mapscancode(scancode, modifier);
}
/****************************************************************************
* Name: usbhost_kbdupper_report
*
* Description:
* Turn a HID keyboard report into key press and key release events and
* hand them to the keyboard upper-half driver.
*
* The HID specification gives no significance to the order of the
* keycodes in the report array, so the only way to know what changed is
* to compare the report against the previous one.
*
* Input Parameters:
* priv - Driver internal state
*
* Returned Value:
* None
*
****************************************************************************/
static void usbhost_kbdupper_report(FAR struct usbhost_state_s *priv)
{
FAR struct usbhid_kbdreport_s *rpt =
(FAR struct usbhid_kbdreport_s *)priv->tbuffer;
uint32_t keycode;
uint8_t changed;
int i;
int j;
/* Report the modifiers whose state changed since the last report */
changed = rpt->modifier ^ priv->lastmod;
for (i = 0; changed != 0 && i < USBHID_NMODIFIERS; i++)
{
if ((changed & (1 << i)) != 0)
{
keyboard_event(&priv->lower, g_modkeycode[i],
(rpt->modifier & (1 << i)) != 0 ?
KEYBOARD_PRESS : KEYBOARD_RELEASE);
}
}
/* Report the keys that were released: down in the last report, but not
* in this one.
*/
for (i = 0; i < 6; i++)
{
if (priv->lastkey[i] == USBHID_KBDUSE_NONE)
{
continue;
}
for (j = 0; j < 6; j++)
{
if (rpt->key[j] == priv->lastkey[i])
{
break;
}
}
if (j >= 6)
{
keycode = usbhost_kbdupper_keycode(priv->lastkey[i],
rpt->modifier);
if (keycode != 0)
{
keyboard_event(&priv->lower, keycode, KEYBOARD_RELEASE);
}
}
}
/* Report the keys that were pressed: down in this report, but not in the
* last one.
*/
for (i = 0; i < 6; i++)
{
if (rpt->key[i] == USBHID_KBDUSE_NONE)
{
continue;
}
for (j = 0; j < 6; j++)
{
if (priv->lastkey[j] == rpt->key[i])
{
break;
}
}
if (j >= 6)
{
if (rpt->key[i] == USBHID_KBDUSE_CAPSLOCK)
{
usbhost_toggle_capslock();
}
keycode = usbhost_kbdupper_keycode(rpt->key[i], rpt->modifier);
if (keycode != 0)
{
keyboard_event(&priv->lower, keycode, KEYBOARD_PRESS);
}
}
}
/* Remember this report so that the next one can be compared against it */
memcpy(priv->lastkey, rpt->key, sizeof(priv->lastkey));
priv->lastmod = rpt->modifier;
}
#endif /* CONFIG_HIDKBD_KBDUPPER */
/****************************************************************************
* Name: usbhost_extract_keys
*
@ -1095,6 +1309,14 @@ static inline void usbhost_toggle_capslock(void)
static int usbhost_extract_keys(FAR struct usbhost_state_s *priv)
{
#ifdef CONFIG_HIDKBD_KBDUPPER
/* The upper-half driver does its own buffering and poll notification, so
* there is nothing else to do here.
*/
usbhost_kbdupper_report(priv);
return OK;
#else
struct usbhid_kbdreport_s *rpt =
(struct usbhid_kbdreport_s *)priv->tbuffer;
uint8_t keycode;
@ -1234,6 +1456,7 @@ static int usbhost_extract_keys(FAR struct usbhost_state_s *priv)
nxmutex_unlock(&priv->lock);
return 0;
#endif /* CONFIG_HIDKBD_KBDUPPER */
}
/****************************************************************************
@ -1991,7 +2214,20 @@ static inline int usbhost_devinit(FAR struct usbhost_state_s *priv)
uinfo("Register driver\n");
usbhost_mkdevname(priv, devname);
#ifdef CONFIG_HIDKBD_KBDUPPER
/* Do not set lower.priv here: keyboard_register() claims that field for
* the upper half. We recover our own state with container_of().
*/
priv->lower.open = usbhost_kbdupper_open;
priv->lower.close = usbhost_kbdupper_close;
priv->lower.write = NULL;
ret = keyboard_register(&priv->lower, devname,
CONFIG_HIDKBD_KBDUPPER_BUFNUMBER);
#else
ret = register_driver(devname, &g_hidkbd_fops, 0600, priv);
#endif
/* We now have to be concerned about asynchronous modification of crefs
* because the driver has been registered.
@ -2468,23 +2704,21 @@ static int usbhost_disconnected(FAR struct usbhost_class_s *usbclass)
}
/****************************************************************************
* Name: usbhost_open
* Name: usbhost_open_priv
*
* Description:
* Standard character driver open method.
* Take a reference on the driver instance. This is the common part of
* opening the device, shared by the character driver and the keyboard
* upper-half interfaces.
*
****************************************************************************/
static int usbhost_open(FAR struct file *filep)
static int usbhost_open_priv(FAR struct usbhost_state_s *priv)
{
FAR struct inode *inode;
FAR struct usbhost_state_s *priv;
irqstate_t flags;
int ret;
uinfo("Entry\n");
inode = filep->f_inode;
priv = inode->i_private;
/* Make sure that we have exclusive access to the private data structure */
@ -2526,23 +2760,22 @@ static int usbhost_open(FAR struct file *filep)
}
/****************************************************************************
* Name: usbhost_close
* Name: usbhost_close_priv
*
* Description:
* Standard character driver close method.
* Drop a reference on the driver instance, destroying it if this was the
* last reference to a disconnected device. This is the common part of
* closing the device, shared by the character driver and the keyboard
* upper-half interfaces.
*
****************************************************************************/
static int usbhost_close(FAR struct file *filep)
static int usbhost_close_priv(FAR struct usbhost_state_s *priv)
{
FAR struct inode *inode;
FAR struct usbhost_state_s *priv;
irqstate_t flags;
int ret;
uinfo("Entry\n");
inode = filep->f_inode;
priv = inode->i_private;
/* Decrement the reference count on the driver */
@ -2621,6 +2854,68 @@ static int usbhost_close(FAR struct file *filep)
return OK;
}
#ifdef CONFIG_HIDKBD_KBDUPPER
/****************************************************************************
* Name: usbhost_kbdupper_open
*
* Description:
* Keyboard upper-half open method.
*
****************************************************************************/
static int usbhost_kbdupper_open(FAR struct keyboard_lowerhalf_s *lower)
{
/* Note that lower->priv cannot be used to find our state: it belongs to
* the upper half, which overwrites it in keyboard_register().
*/
return usbhost_open_priv(container_of(lower, struct usbhost_state_s,
lower));
}
/****************************************************************************
* Name: usbhost_kbdupper_close
*
* Description:
* Keyboard upper-half close method.
*
****************************************************************************/
static int usbhost_kbdupper_close(FAR struct keyboard_lowerhalf_s *lower)
{
return usbhost_close_priv(container_of(lower, struct usbhost_state_s,
lower));
}
#else /* CONFIG_HIDKBD_KBDUPPER */
/****************************************************************************
* Name: usbhost_open
*
* Description:
* Standard character driver open method.
*
****************************************************************************/
static int usbhost_open(FAR struct file *filep)
{
return usbhost_open_priv(filep->f_inode->i_private);
}
/****************************************************************************
* Name: usbhost_close
*
* Description:
* Standard character driver close method.
*
****************************************************************************/
static int usbhost_close(FAR struct file *filep)
{
return usbhost_close_priv(filep->f_inode->i_private);
}
/****************************************************************************
* Name: usbhost_read
*
@ -2851,6 +3146,8 @@ errout:
return ret;
}
#endif /* CONFIG_HIDKBD_KBDUPPER */
/****************************************************************************
* Public Functions
****************************************************************************/