A PS Vita kernel plugin (skprx) that emulates a USB HID keyboard.
When activated, the Vita appears to a connected host as a standard
USB keyboard (PID 054c:1338) and can inject keystrokes into the host.
This is a fork of mswlandi/vitakeyboard that would absolutely not exist without mswlandi's work.
I added three architectural fixes to make it work better:
- Cache flush before every DMA send (
ksceKernelDcacheCleanRange) - Connection-state gating before sending any report
- Correct
_startsignature as a weak alias ofmodule_start
This fork has been built for the quark engine, for it to be able to send keyboard inputs.
You can see an example usage of it in keyboard_vita.c.
Here is my current usage of this fork for reference:
┌──────────────────────┐ JS bindings ┌──────────────────────┐
│ Quark JS │ ────────────► │ Userland wrapper │
│ (Duktape, in app) │ │ (vitakeyboard.c) │
└──────────────────────┘ └──────────┬───────────┘
│ syscalls
▼
┌──────────────────────────────────────────────────────────────┐
│ Kernel plugin (skprx) │
│ │
│ ┌──────────────┐ single-slot queue ┌──────────────┐ │
│ │ syscall │ ────────────────────► │ update_thread│ │
│ │ handlers │ (hasPendingKey/mod/ │ 10ms poll │ │
│ └──────────────┘ pendingKey + mutex) └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ send_inputs │ │
│ │ + cache flush│ │
│ └──────┬───────┘ │
└─────────────────────────────────────────────────┼────────────┘
▼
┌──────────────┐
│ UDCD │
│ interrupt-IN │
│ endpoint │
└──────┬───────┘
│ USB
▼
[host PC]
The kernel plugin registers itself as a UDCD driver named VITA_KEYBOARD.
When hidkeyboard_user_start is invoked it deactivates Sony's default USB
drivers (MTP, PSPCommunication, Serial), starts the keyboard driver, and
activates the device with USB PID 0x1338.
A background thread polls every 10ms for pending key presses. When a key is
queued, it sends a press report on the next tick and a release report on the
tick after. Critically, the report buffer is cache-flushed before each
ksceUdcdReqSend call to ensure the USB controller's DMA reads the current
buffer contents and not stale cached data.
| Field | Value |
|---|---|
| Vendor ID | 0x054c (Sony) |
| Product ID | 0x1338 |
| USB class | HID (0x03) |
| Subclass | 0x00 (not boot interface) |
| Protocol | 0x00 (not boot keyboard) |
| Endpoints | EP0 control, EP1 interrupt-IN |
| Polling interval | 1ms (bInterval=0x01) |
| HID version | 1.11 |
| Speed | High-Speed |
The interrupt-IN endpoint sends 8-byte HID reports:
| Offset | Name | Description |
|---|---|---|
| 0 | modifier | Bitmask: Ctrl/Shift/Alt/Meta L/R |
| 1 | reserved | Always 0 |
| 2 | key1 | Primary HID keycode |
| 3 | key2 | (unused, 0) |
| 4 | key3 | (unused, 0) |
| 5 | key4 | (unused, 0) |
| 6 | key5 | (unused, 0) |
| 7 | key6 | (unused, 0) |
The current implementation only uses key1; the other slots are reserved for
future N-key rollover support.
Modifier bitmask values (KEY_MOD_* in ascii_to_usb_hid.h):
| Constant | Value | Meaning |
|---|---|---|
KEY_MOD_LCTRL |
0x01 | Left Control |
KEY_MOD_LSHIFT |
0x02 | Left Shift |
KEY_MOD_LALT |
0x04 | Left Alt |
KEY_MOD_LMETA |
0x08 | Left Meta/Win/Cmd |
KEY_MOD_RCTRL |
0x10 | Right Control |
KEY_MOD_RSHIFT |
0x20 | Right Shift |
KEY_MOD_RALT |
0x40 | Right Alt / AltGr |
KEY_MOD_RMETA |
0x80 | Right Meta |
HID keycodes follow USB HID 1.11 Usage Page 0x07 (Keyboard/Keypad). See
ascii_to_usb_hid.h for the full table.
All kernel functions are exposed as syscalls and must be linked against
libhidkeyboard_stub_weak.a (or _stub.a) generated by vita_create_stubs.
Activates USB keyboard mode. Deactivates the current USB driver (typically
MTP), stops competing drivers, starts the keyboard driver, and activates it
with PID 0x1338. The host will enumerate the device as a USB HID keyboard
within ~1 second.
Returns:
0on successHIDKEYBOARD_ERROR_DRIVER_NOT_REGISTEREDifmodule_startdid not runHIDKEYBOARD_ERROR_DRIVER_ALREADY_ACTIVATEDif already started- Negative SCE error code on UDCD failure
Side effects: disconnects any existing USB session (e.g. content manager, MTP transfer in progress).
Deactivates keyboard mode and restores the default Vita USB behaviour
(MTP, PID 0x04e4).
Returns:
0on successHIDKEYBOARD_ERROR_DRIVER_NOT_ACTIVATEDif not currently active
Queues a single key press with an optional modifier mask. The press is emitted on the next 10ms tick after the connection is established, followed by a release on the tick after.
Parameters:
modifier— bitwise OR ofKEY_MOD_*constants (0 for no modifier)key— HID keycode (0x00–0xFF). 0 sends no key.
Returns: 0 always (currently). The actual transmission happens
asynchronously in the kernel thread.
Queue semantics: the kernel has a single-slot queue. If you call this faster than the kernel thread can drain it (~20ms per character), later calls overwrite earlier ones before they are transmitted. Userland code must space calls by at least 20–30ms.
Example:
// Send Ctrl+C
HidKeyBoardSendModifierAndKey(KEY_MOD_LCTRL, KEY_C); // 0x01, 0x06Queues a single character using a keyboard layout mapping (currently
hardcoded to pt-BR; see layouts/). For characters outside the layout map,
returns 0 without queuing anything.
Parameters:
c— UTF-16 code unit
Returns: 0 always. Silently drops unsupported characters.
This function exists to handle non-ASCII characters via dead keys / AltGr sequences. For ASCII-only use cases, prefer the lookup table in the userland wrapper which is faster.
The userland wrapper provides a higher-level API and handles inter-character timing.
Stops any existing keyboard session (ignoring errors) and starts a new one. Safe to call repeatedly.
Returns: result of hidkeyboard_user_start().
Stops the keyboard and restores default USB behaviour.
Returns: result of hidkeyboard_user_stop().
Sends a single character. Routing rules:
| Character range | Handling |
|---|---|
\b (0x08) |
Backspace (HID 0x2A) |
\n (0x0A), \r (0x0D) |
Enter (HID 0x28) |
\t (0x09) |
Tab (HID 0x2B) |
| ESC (0x1B) | Escape (HID 0x29) |
| DEL (0x7F) | Delete (HID 0x4C) |
| Printable ASCII (0x20–0x7E) | ascii_to_hid_key_map[c - 0x20] |
| Other | Falls through to HidKeyboardSendChar |
Returns: 0 on success, negative on failure.
Does not delay. This is fire-and-forget — the kernel will emit press and
release on its own timing. To send multiple characters, use
vita_keyboard_send_string or insert your own delays.
Sends a null-terminated string. Iterates over each byte and calls
vita_keyboard_send_char, with a delay between characters to let the kernel
drain its single-slot queue.
Parameters:
str— null-terminated C string (must not be NULL)
Returns: 0 on success, -1 if str is NULL, negative SCE error on
syscall failure.
Timing: INTER_CHAR_DELAY_US (default 50000μs = 50ms) between characters.
Increase if characters get dropped on slower thread scheduling; decrease for
faster typing.
Direct passthrough to HidKeyBoardSendModifierAndKey. Useful for sending
shortcuts.
The kernel plugin must be loaded by taiHEN at boot.
ur0:/tai/config.txt:
*KERNEL
ur0:tai/hidkeyboard.skprx
The entry must be under *KERNEL, not *main. The plugin conflicts with
any other module that controls UDCD; vitastick.skprx and udcd_uvc.skprx
must be disabled (commented out) for the keyboard to enumerate.
A reboot is required after changing the skprx or config.txt — taiHEN does not reload kernel modules at runtime.
The skprx is built with VitaSDK. From the skprx/ directory:
mkdir build && cd build
cmake ..
makeThis produces hidkeyboard.skprx and stubs :
libhidkeyboard_stub.alibhidkeyboard_stub_weak.a
Userland apps link against the weak stub:
target_link_libraries(myapp hidkeyboard_stub_weak)Make sure the userland app picks up the freshly generated stub, not a stale copy in the source tree. Whenever exports change, regenerate the stubs and recopy them to wherever your build expects them.
- Single-slot queue. Sending characters faster than the kernel can drain
them causes drops. Mitigated by
INTER_CHAR_DELAY_USin the userland wrapper. - One key at a time. No N-key rollover, no simultaneous modifier+key+key.
The HID report has 6 key slots but only
key1is used. - No LED feedback. Caps Lock / Num Lock state from the host is not read.
- Layout is hardcoded. Non-ASCII characters use the pt-BR layout table.
- No release notifications. Userland cannot know when a character has finished transmitting; the inter-character delay is a fixed guess.
- Polling architecture. The 10ms tick caps the maximum typing rate at
~50 characters/second. A future event-driven port (modelled on
vitastick) would lift this limit.
Host receives press but never release (auto-repeat fires forever)
The cache flush before ksceUdcdReqSend is missing or the wrong NID is being
linked. Verify ksceKernelDcacheCleanRange(g_inputs, sizeof(g_inputs)) is
called in send_inputs() and that <psp2kern/kernel/cpu.h> is included.
Module crashes at boot with PC: 0x0
The _start symbol is wrong. It must be a weak alias of module_start with
the exact signature int module_start(SceSize args, void *argp).
Module loads but device doesn't enumerate (still PID 0x04e4)
A competing USB driver is active. Check config.txt for vitastick.skprx,
udcd_uvc.skprx, or other UDCD-using modules and disable them.
Keys are dropped when sending a long string
INTER_CHAR_DELAY_US is too low. Increase from 50000 to 75000 or 100000.
App crashes on JS bindings call with PC: 0x0
Userland is linked against an outdated stub that doesn't include the export
being called. Rebuild the skprx, copy the new libhidkeyboard_stub_weak.a
to wherever your app's CMakeLists expects it, then rebuild the app.