published on in Electronics
tags: psoc usb

Communicating using the keyboard status LEDs

While learning how USB works I tried using the keyboard status LED to transmit information from the host to the device. This isn’t an original idea, I believe someone presented this in the past as a possible attack vector since USB HID devices require no special drivers and any application can toggle the status LEDs without special permissions. This post is, however, much more innocent and serves as a simple proof of concept.

Theory of Operation

Each keyboard gets updated by the host device on the state of the three status LEDs, num lock, caps lock and scroll lock. By implementing a USB HID ‘keyboard’ using a microcontroller and a small app on the USB host we can use this 3-bit wide interface to transmit data in both directions.

I decided to use num lock and caps lock as data and scroll lock as clock, creating a synchronous interface. Since a keyboard can respond back in a couple of ways (keypresses, toggling the LEDs), it’s possible to come up with other transmission schemes.

Timing diagram for communicating with the LEDs.

To keep things simple, I decided not to have any acknowledgment from the device. With this configuration we can transmit one byte in 4 clock cycles.


For my device I used a Cypress PSoC5 Prototyping kit. These are pretty cheap ($10/piece without shipping) and the PSoC platform is extremely capable in my opinion (with one downside being windows-only proprietary toolchain).

On the board you can find the CY8C5888LTI-LP097 which has support for full-speed USB for implementing our faux USB keyboard.

PSoC Configuration & Circuit

The circuit is pretty simple, I am using a standard 2x16 character LCD, the on-board USB port and 3 LEDs for the keyboard status LEDs.

Internal and external circuit.

To set the contrast for the LCD I used one of the internal voltage DACs buffered by an internal OpAmp. The DAC has almost no driving capability so it doesn’t work at all without the buffer.

For the LEDs I used some random N-Channel MOSFETs I had lying around (2N7000) since the pins can’t source/sink too much current.

USB Configuration

Firstly, we should define the USB HID Descriptor. Luckily Cypress includes a complete keyboard descriptor with their IDE so we just have to import it. At the HID Descriptor tab of the USBFS Configuration window you can import descriptor with the green arrow button.

Under the Device Descriptor tab we can set some general parameters for our device.

Device Attributes:
- Vendor ID: Make something up!
- Product ID: Make something up again!
- Device Class: 0x00 (We will define it below)
- Manufacturing String: Your name
- Product String: PSoC Legit Keyboard

Configuration Descriptor:
- Max power: 100mA
- Device power: Bus Powered
- Remote wakeup: Disabled

Interface Descriptor -> Alternate Setting 0:
- Class: 0x03 (HID)
- Subclass: 0x00 (No subclass)
- Protocol: 0x00

Alternate Setting 0 -> HID class Descriptor
- Descriptor Type: Report
- Country Code: Not supported
- HID Report: Keyboard with LEDs (the descriptor we created above)

Alternate Setting 0 -> Endpoint Attributes:
- Endpoint Number: EP1
- Direction: IN
- Transfer Type: INT
- Interval: 10ms
- Max Packet Size: 8

To keep this post short, I won’t go into how HID descriptors work. The included one will work just fine for this project.


I tried to keep the code as simple as possible for this, our main.c looks like this:

#include <project.h>
#include "usb.h" 

int main() {
    // Enable global interrupts

    // Initialize the LCD
    VDAC8_Contrast_Start(); // Enable contrast pin output
    // Start USB
    LCD_PrintString("Waiting for host");
    // Wait for enumeration
    while (!USBFS_bGetConfiguration());
    // Begin USB traffic
    USBFS_LoadInEP(1, Keyboard_Data, 8);

    // Main loop
    for (;;) {
        // Wait for ACK from host
        if (USBFS_bGetEPAckState(1)) {
            // Send data to host

            // Receive data from host	

Inside the usb.h file we define the USB_Send() and USB_Receive() functions together with some helpers:

#define NumLock_On (Status_LED_Data & 0x01) != 0
#define CapsLock_On (Status_LED_Data & 0x02) != 0
#define ScrollLock_On (Status_LED_Data & 0x04) != 0

/* Array of Keycode information to send to PC */
static unsigned char Keyboard_Data[8] =
    {0, 0, 0, 0, 0, 0, 0, 0}; 
/* Status LEDs */
static unsigned char Status_LED_Data = 0;

/* Incoming characters to display on the LCD */
static unsigned char Input_Text[16] = {
    ' ', ' ', ' ', ' ', ' ', ' ',
    ' ', ' ', ' ', ' ', ' ', ' ',
    ' ', ' ', ' ', ' '
}; // These are spaces

/* Which byte are we receiving right now */
unsigned char byteIndex = 0;

/* Which bit of the byte above are we receiving right now */
unsigned char bitIndex = 0;

/* Was the last cycle a clock? */
unsigned char lastClock = 0;

 * Send data to the host.
void USB_Send(void);

 * Receive data from the host.
void USB_Receive(void);

Since our keyboard won’t actually have to transmit any keypresses, USB_Send() will be empty. All the magic happens inside USB_Receive():

void USB_Receive(void) {
    // Read the incoming report data 
    Status_LED_Data[0] =
    // If numlock is enabled, turn on the LED
    if (NumLock_On) {
        Keyboard_Data[2] = 0x00;
        USBFS_LoadInEP(1, Keyboard_Data, 8);
    } else {
    // If capslock is enabled, turn on the LED
    if (CapsLock_On) {
        Keyboard_Data[2] = 0x00;
        USBFS_LoadInEP(1, Keyboard_Data, 8);
    } else {
    // If scroll lock is enabled, turn on the LED
    if (ScrollLock_On) {
        Keyboard_Data[2] = 0x00;
        USBFS_LoadInEP(1, Keyboard_Data, 8);
        // Scroll lock is also our clock so if it's on, we should
        // store the state of numlock and capslock as it's our
        // data.        
        if (lastClock == 0) {
            lastClock = 1;
            // Store numlock
            if (NumLock_On) {
                Input_Text[byteIndex] |= 1 << (bitIndex);
            // Advance the bit index since we stored one bit.
            // Store caps lock
            if (CapsLock_On) { 
                Input_Text[byteIndex] |= 1 << (bitIndex);
            bitIndex++; // Another bit
            // If this byte is received, move to the next one
            if (bitIndex == 8) {
                bitIndex = 0;
    } else {
        // Clock cycle finished...
        if (lastClock == 1) {
            lastClock = 0;
            // .. check if we have received 16 bytes ...
            if (byteIndex == 16) {
                byteIndex = 0;
                // and print them on the screen
                LCD_Position(1, 0); // row 1, column 0
                unsigned char i;
                for (i = 0; i < 16; i++) {
                    // Clear for the next batch
                    Input_Text[i] = ' ';

Our PSoC keyboard should now enumerate correctly and the LEDs should match the state of num-lock, caps-lock and scroll-lock. To test transferring information I wrote a small C# app to send over user input. The .NET framework doesn’t include any APIs for controlling the keyboard LEDs but it can be done by using some native calls. You can find the code I am using below over here.

const int ClockSpeed = 10; // In Hz
const int Delay = 1000 / ClockSpeed;

static void Main(string[] args) {
    Console.WriteLine("Clock (Hz): " +  ClockSpeed);

    while (true) {
        // Read one line
        var text = Console.ReadLine();
        // Trim down to 16 characters
        if (text.Length > 16) { text = text.Substring(0, 16); }
        // Explode text to a bit array
        var bits = new BitArray(

        // Turn everything off
        StatusLED.NumLock = false;
        StatusLED.CapsLock = false;
        StatusLED.ScrollLock = false;

        // Transmit data
        for (var i = 0; i < bits.Length; i += 2) {
            // Set data lines
            StatusLED.NumLock = bits[i];
            StatusLED.CapsLock = bits[i + 1];
            // Cycle clock
            Thread.Sleep(Delay / 3);
            StatusLED.ScrollLock = true;
            Thread.Sleep(Delay / 3);
            StatusLED.ScrollLock = false;
            Thread.Sleep(Delay / 3);

        // Return the LEDs in a common configuration.
        StatusLED.NumLock = true;
        StatusLED.CapsLock = false;

In my testing I have found that you can send data reliably at around 4 bytes/second. Above that things start to get sketchy. Maybe adding an ACK message (in the form of a keypress) or using some basic form of error correction could improve transfer rates.

Transferring 1KB at 4 bytes per second would take around 4 minutes! It seems impractical to try and steal data this way, if you can spend 4 minutes at a computer might as well take a photo of the screen. This also assumes you can use something like Windows Script Host to deploy some code on the host machine to actually transmit the data back to the device.


You can find all the source code and the PSoC schematics for this project on my Github repository.