How does a keyboard send a key over?

Fang Jin
5 min readAug 27, 2023

When you hit a key on your keyboard, there will be a key code sent to the computer instantly. You figured that once you see the keystroke was printed on your computer screen, as I am typing this sentence. But how exactly is this process executed? If you wonder why, please follow me, I’ll try to explain it from the beginning.

Hardware

The first step is a physical key, it can be as fancy as a mechanical key, or a switch button. As long as this physical switch generates a short circuit, it allows the hardware detect a press action, which can be then captured via the following firmware:

void loop()
{
keySwitch.read();
if (keySwitch.wasPressed())
{
sendKey(keycode);
}
}

The code can’t get any simpler, we are lucky these day, since the Arduino community has pretty much written most of the low level code, all we need to do is to program like BASIC, sorry that is C code.

USB

Most of our gadgets (refers as Human Interface Devic UID), are connected with the computer via USB interface. With a wire (or without a wire, such as Bluetooth), the hardware talks to the computer through a protocol. What’s interesting is that when you plug the keyboard into the USB port, the computer will start to ask couple of questions to the device which is supposed to answer them one by one. Make a guess, what are the questions?

Basically it will ask what kind of device are you? Ex. a keyboard. This info is referred as device descriptor. These info are quite concise and made to allow the computer quickly setup so that it can later communicate with the keyboard correctly. One of the vital info is called report descriptor:

static const uint8_t _hidReportDescriptor[] PROGMEM = {
// Keyboard
0x05, 0x01, // USAGE_PAGE (Generic Desktop) // 47
0x09, 0x06, // USAGE (Keyboard)
0xa1, 0x01, // COLLECTION (Application)
0x85, 0x02, // REPORT_ID (2)
0x05, 0x07, // USAGE_PAGE (Keyboard)
...
};

The report descriptor is the specification between the keyboard and the computer. This is the interface. I won’t go through all the fields, but just want to point out two numbers. Each time when computer talks from or to the keyboard, it uses report formatted with the above specification. Since there’ll be lots of different report descriptor, each has a unique id per device, in our example, 2 or 0x02 . Since there’ll be lots of reports with the same format, there’s a dedicated usage category id associated with this type of report, in our case, 0x07 . Keep in mind each usage page id has been standardized for the purpose of your device. Use a plain language, this report 0x02 means a key report, when you press or release a key:

void Keyboard_::sendReport(KeyReport* keys)
{
HID().SendReport(2, keys, sizeof(KeyReport));
}

typedef struct
{
uint8_t modifiers;
uint8_t reserved;
uint8_t keys[6];
} KeyReport;

For your curiosity, I also listed the content of each report, which is not exactly a key, instead it’s a six keys array with a modifier bits. Modifier bits will tell you whether shift or ctrl has been pressed at the same time. The keys array is just to make sure you might happen to press multiple keys at the same time. But most of time, there’ll be only one key in the array.

If I install some software to listen to the report coming from the hardware I can see something like the following:

The text up and down indicates whether you are pressing or releasing the key. Notice there are two numbers to the right: the usage page 0x0007 which is the same number specified by our report descriptor. There’s another number which is referred as a usage id . What is that?

It turns out for each key within a usage page that represents a particular type of input or output event; for instance, if you type A on the keyboard, the usage id will be 0x04 . Notice we don’t use ascii here. So how does the A translates into 0x04 then?

This depends on your keyboard layout. Each key event has its dedicated usage id, for instance a is assigned 0x04 . Ok, so the firmware will have to do this translation for us before sending the report, you guessed it right. Let’s take a look at how this is done in the firmware:

size_t Keyboard_::press(uint8_t k)
{
uint8_t i;
if (k >= 136) { // it's a non-printing key (not a modifier)
k = k - 136;
} else if (k >= 128) { // it's a modifier key
_keyReport.modifiers |= (1<<(k-128));
k = 0;
} else { // it's a printing key
k = pgm_read_byte(_asciimap + k);
}

_keyReport.keys[0] = k;
sendReport(&_keyReport);
}

The last two lines set the key and send the report, before that happens it does some translation by using a keyboard layout map via _asciimap, different keyboard layout carries with a different variable array to map the key over. Please ignore pgm_read_byte , it tries to read from a memory space.

This is almost the whole story of hitting a key on your keyboard and receiving it via your OS. Believe it or not, if you follow this process, every device can act like a keyboard, no matter how weird the keyboard you build.

Ascii, keycode or whatever

I do want to comment on the k variable in the above code, because I find it strange. Before sending the report, this k refers to as an ascii code, which ranges from 0 to 255 . But apparently the UID usage id does not support that many, for instance, any strange character can not have a physical representation on the keyboard. So for the firmware I’m using, it’s Arduino core, it decides to stop at 136 usages. So what happens if I give it a larger number, for instance if I send a code 140 , it will become 4 by subtracting 136 from it. And 4 stands for the same usage as hitting the letter A . Isn’t it? Let’s give it a short, shall we?

Bingo, well, welcome to the keyboard world!

--

--

Fang Jin
Fang Jin

Written by Fang Jin

Front-end Engineer, book author of “Designing React Hooks the Right Way” and "Think in Recursion"

No responses yet