Introduction: Use a PS/2 Keyboard in Microcontroller-projects

About: An electrical engineers homemade tinkering.

Sometimes there are microcontroller-projects, that require buttons. A lot of buttons. It is easy to connect one, two or three buttons to some GPIO-pins (general purpose input/output) of an uC. But what if the project requires more buttons than there are pins? I planned a project with one button for each letter, one button for all of the numbers from 1 to 26 and 4 buttons to toggle some LEDs summing up to 56 buttons. Yes, there are uCs with 64 or even 100 pins. But would that be the smartest way to do things? Frankly speaking even the 48 pin uC I used is quite overkill for my cute little project. I just used it, because I got the uC-board cheap and it features the uC-family I know best. But isn't there some very common hardware, that scans a lot of buttons and writes the result to just a few wires? Sure there is: The keyboard.

Supplies

  • a uC board (e.g. STM32F103 bluepill)
  • a PS/2 keyboad
  • a Toolchain (the SW you use for programming your uC | e.g. STM32CubeIDE)
  • perhaps some HW to program the uC (if it isn't on board | e.g. ST-LINK V2 debugger)
  • USB to TTL HW and a terminal emulator or some other way to read out data from your uC (e.g. FTDI232 breakout and HTerm)

Step 1: Check the Alternatives

I planned this project for quite some time and had many ideas. One of those might be a better fit for your project. So it won't hurt to mention them:

  • a rotary switch instead of the 4 buttons to select the "encryption"
    • no potential to safe pins as I can already select 16 "codes with 4 buttons, but an interesting input device for uC projects
    • there a switches, which connect one common contact to a different contact per position (e.g. 12 positions -> 12 GPIOs needed)
    • there are binary coded switches, which "compress" the information to fewer lines (e.g. 16 positions -> 4 GPIOs needed)
  • a incremental encoder
    • it can look like a rotary switch and outputs two shifted square waves when turned
    • this allows to determine the direction of rotation and either increase or decrease the count
    • the current value could be displayed via a seven-segment-display or the like
  • port expansion using shift registers
    • there are shift registers to convert data output from parallel to serial and vice versa (get the right one)
    • by writing serial to the register and giving the content out in parallel you can increase the number of outputs
    • by reading in information in parallel on a signal and reading it to the uC in serial afterwards, you can increase the number of inputs
    • it takes some time and you have to regulary check the register inputs our give out the information to all register outputs every time the state of one output pin changes
  • using a scan matrix to read x*y keys state with x+y pins
    • you have to regularly scan the matrix for changes
    • that's what a keyboard can do for you
    • you could use the inlay of a keyboard without the logic, a keypad or wired switches
  • make a small computer program with GUI (graphical user interface)
    • it would have been the fastest, easiest and cheapest way to get my project done, but it was just not what I wanted
  • built it all without a uC, using discrete logic
    • after I added up the required ICs to a few hundred I dropped the idea fast
    • maybe there would have been some smart ways to safe some ICs but yeah, still not worth it

Step 2: Learn More About Keyboards

To learn more about about keyboard means to learn from Adam Chapweske. His original publication seems to not be online anymore but there are copies. Trying to find a good reference I found many dead links and disliked it. So I won't reference to a site, that might not be online any longer when you need it. Use a search engine and type in "Adam Chapweske PS/2" or something. And maybe read about keyboards on Wikipedia. I will tell you the bits of information, that I used for my project and maybe a tad more. I hope you learn enough to fill the gaps.

The main question is: How easy is it to read out a keyboard?

Quite easy. If it is a PS/2 keyboard. Or more precisely a keyboard more or less compatible to the IBM PS/2 keyboard from 1987. Or the IBM AT keyboard from 1984. Or even the PC/XT keyboard from 1981. It is unlikley you get a keyboard, that supports all of the features of one of those keyboards, as with computers it's always more about compatibility than standards, but if you don't get a USB only keyboard or some Apple keyboard, it should be close enough to work with it. Don't rely to much on one of the more exotic features.

What is the difference between the mentioned keyboards? The most obvious things are the connectors. Then there are the scan codes and the oldest one could only send data to the host, while the newer two types can also receive some commands for configuration ans so on. The AT keyboard and the PC/2 keyboard are widely compatible and there might be keyboards with AT connector closer to the PC/2 features and vice versa.

  • PC/XT: 5-pin DIN connector, unidirectional (no commands from computer to keyboard)
  • AT: 5-pin DIN connector, bidirectional, not backward compatible with PC/XT keyboard
  • PC/2: 6-pin DIN mini connector, bidirectional, compatible with AT only

What to do, if you only have a USB keyboard? Try it out. If your lucky it works. How can it work? Only 4 oft the 5 or 6 wires of the old keyboard cables are in use. So a USB Type A plug with it's for pins can be used as well. Two wires are used for the power supply. GND and 5V - USB happens to work at 5V as well. So some keyboards support both protocols. They use the voltage level of the data lines respectively the data and the clock line to detect, if they are connected to a USB or via a passive adapter to a PS/2 port. I don't know how standardised the implementation of PS/2 interfaces with USB plugs is. Maybe you need to swap the inner two pins. GND and Vcc are fixed by the USB standard

Now we know about the connectors and the pins in use. But what do they do? GND and Vcc are for the power supply. Data and Clock are the lines we need to communicate with the keyboard. So it is a synchronous serial interface. The Clock signal is always generated by the keyboard. Data are sent one byte at a time. 11 bit from the keyboard to the host and 12 bit from the host to the keyboard (which I didn't implement, because I just need many buttons).

  • 1 start bit (always 0)
  • 8 data bit (least significant first)
  • 1 parity bit (odd)
  • 1 stop bit (alwas 1)
    • (1 acknowledge bit | host to keyboard only)

    In idle state the data and clock line are high. If you want to implement communication from your uC to the keyboard to change some settings, it must be able to pull the data and clock line to low. Before you send data to the keyboard pull Clock to low to inhibit communication for at least 100 µs. Pull Data to low and release Clock to request to send. The keyboard checks for this state at least every 10 ms. After detecting it, it will generate a Clock signal. The host changes the data bits, when Clock line is low.

    For now I am only interested in keyboard to host communication. There it is the opposite and the Data changes, when the clock is high. So the data are valid, when the clock is low. The clock frequency is supposed to 10 to 16.7 kHz (so the cycle time is between 66 and 100 µs, half cycle time 33 to 50 µs). The change of the Data has to occur at least 5 µs and maximal 25 µs after the edge. I don't really check the timings since I assume that the keyboards I work with behave like keyboards. I looked at the clock signal with a logic analyser through and the frequency was at 13.2 kHz well in the specified range.

    The keyboard will send scancodes. There are make codes when keys are pressed and break-codes when keys are released. Make codes might be repeated while keys stay pressed. The original XT scancode (scancode set 1) increases make codes by 0x80 to generate break codes. Some make and break codes are extended by a leading byte 0xE0. The Pause key sends 6 bytes for some reason. Scancode set 2 also uses 0xe0 as extension for some codes and sends a leading byte 0xF0 to indicate a break code. Break codes of extended scancodes start with 0xF0 followed by 0xE0 and Pause sends 8 bytes. Scancode set 3 has only single byte make and two byte break codes leaded by 0xF0.

    Set 2 seems to be the most common and is the only one I needed by now. I also only needed "normal" keys so I ignored the characteristics of Pause. The rules I implemented are:

    • 1 or 2 bytes for make codes
    • 1 byte more for break codes
    • break codes for keys with 1 byte make code are 0xF0, make code
    • "extended" 2 bytes make codes start with the byte 0xE0
    • their break codes are 0xE0, 0xF0, second make code byte

    It is good to know which scancode set is used by your keyboard and which rules is follows. It makes it easier to write some software to receive and interpret the data sent by the keyboard. Knowing about the PC/2 protocol can also help to read out a mouse and other hardware. But I didn't try it out by now.

    Note that not every keyboard might be fit for you project. If you need more than two pressed keys at the same time, not every keyboard can process this. The reason is the intern scan matrix. How does it work? There are lines an columns of wires and at each crossing point those can be connected by a button. The voltage level at the columns is usually high. One column at a time is pulled to low. The voltage level at the lines is read out at the same time. If it changes the line and the active column are connected by the button at the crosspoint. But if more than two keys are pressed and three of them are at the corners of a rectangle, two of them can be determined but it is unclear which of the other two corner points is (or if both are) pressed.

    So the simplest keyboards possible just consider the two keys pressed first (two-key rollover). That might be your worst case. Some keyboards consider all keys until a uncertainty occurs (multiple-key rollover). Uncertainty are avoided for keys that are likely pressed at the same time (Shift + some Letter or Ctrl + Alt + Delete). A diode for each key, allowing the current only to flow in one direction only, makes it possible to build keyboards that can theoretically even detect if all keys are pressed at the same time (n-key rollover). As unlikely it might be, that this is a meaningful input. This increases the production cost and is not to be expected for standard keyboards.

    Step 3: Set Up the UC

    The first keyboards I got to tinker around were two cheap foldable silicone ones. At this point I thought the computer is the part, that is able to support the PC/2 protocol via the USB Interface. And my idea was, that some cheap keyboards had a high chance to use some cheap old-fashioned PS/2 keyboard-controllers. This reasoning was faulty as the keyboard is the part with both protocols implemented. But by now nobody seems to bother to still implement PS/2 compatible interfaces. So contollers, that support both are old-fashioned and maybe cheap. I was lucky enough to get a keyboard that supported PS/2 and when I connected it to a uC without any pull-ups or pull downs it was interpreted as a PS/2 interface so I didn't notice my error in reasoning for quite some time.

    One of the keyboards didn't work, so I cut it open to look inside. It was a nice excuse to do so. Under the big elevation on the left is the circuit board. For the keys there is the typical plastic sheet with the printed conductor paths. Each key has somthing conductive at the underside to short-cut between those paths. In principle it is a rubber dome keyboard without the hard plastic keys on top.

    The problem was the connection to the plug itself (kinda unexpected) so I cut the cable to connect directly. If you don't like to cut cables as much as I do or don't want to use the keyboard exclusively to tinker around, you can prepeare a breakout-board. Today I tried out my good mechanical keyboard, that I got a few years later, without cutting the cable (obviously). It seems to be a USB-keyboard only. Or maybe I would need some pull-ups, to emulate the PS/2-interface better and/or swap the data and clock lines.

    Since I don't send data to the keyboard I just connected the clock and data line directly to two uC input pins without any further elements. It works with the keyboards I want to use, so I see no need to change it. In my case it is okay to connect the keyboard directly, because my 3.3 volt powered uC features 5V resilient inputs. I made sure to use pins marked as such in the manual and to use the input without internal pull-up or pull-down resistors, because that's the only configuration in which the input can handle 5V.

    If your uC is powered with less than 5V, make sure if the inputs can handle the voltage or use level shifters. To send data to the keyboard you need to pull the lines to low level. If you use a 5 Volt powered uC and the outputs seem robust enough, you might be able to do so directly. Even switching the same two pins between input and output to read and right might work. If your uC runs at lower voltage you should in any case spend the two additional pins and use transistors as shown in the picture to send data to the keyboard.

    I used STMCubeMX to configure the peripheries - this safes quite some time - and Keil uVision to do the coding. If you also use a STM32 uC the STM32CubeIDE can do both. I tried it out by now and it worked well. If you use another uC there might be similar tools. First I always configure the periphery I really need and that doesn't work at just any pin. In my case this was the single wire debug interface (SWD). It really suckes when you forget it and can't program your uC anymore. Yes, it happened and I feared the board was a goner but programming under reset saved me. So now I always configure the SWD first.

    Then I needed the USART to enable communication with my PC. If you already have a display running in your uC project, you can just use this to show the values you get from the keyboard. There are no fancy requirements for the USART. I set it to asynchronous mode and left the configuration just as suggested. Maybe I even lowered the baud rate. The communication doesn't need to be fast to send some key presses to the PC. And I enabled the interrupt to run the UART in a non blocking mode. To use the DMA to relieve the processor even more was just not necessary.

    The rest could be handled by some of the many free GPIOs. I used two of them as inputs for the PS/2 Clock and Data. No pull_ups and downs, interrupts enabled and set to falling edge. And I used 7 pins as Output to light some LEDs. That's it.

    Step 4: Listen to the Keyboard

    Now it was time to use the things I had learned about keyboards and write some code. Using a switch statement I implemented a state machine right in the GPIO interrupt service routine. Since the communication is synchronous it is okay to look there only when there is a signal edge occurs. I don't even look at all of the edges but only on the falling ones (since the Data are valid when the clock is low and the sequence starts with data pulled low) and don't process most of the falling edges on the data line. By watching more closely at the signal and also checking it between the edges might lead to a less interference-prone data input, but since it works well enough I didn't bother to do so. The whole point of synchronous communication is to know when I can trust my signal anyway. Blocking the processor in interrupt service routines for a long time should be avoided. But to run through one of those small states goes really fast so everything is fine.

    The states are:

    • wait - could have called it idle: Clock and Data are high, no communication, ends when Data is pulled low (falling edge on data - the only edge on data I process) and sets the state flag to start
    • start: ends with the falling edge on Clock, when Data is low the state flag is set to receive, the bit mask is set to 0x01 (least significant bit), the parity counter to 0, if it is a first byte of the scan code (new data flag unequal to data partly received) all bytes are cleared and the size is set to one byte
    • receive: when a falling edge on Clock occurs and data is high the bit mask is added to the input and the parity count is increased by one; the bit mask is shifted left independently of the Data level and masks now the next higher bit; if the 1-bit is shifted out of the 8-bit variable the state flag is set to parity (so receive occures on 8 falling clock edges in a row)
    • parity: when a falling flag on Clock occurs the Data level (1 for high, 0 for low) is added to parity count, if the parity count is odd (least significant bit is 1) the state flag is set to stop
    • stop: when a falling edge on Clock occurs and Data level is high; if the just received byte is 0xF0 (indicating a break code) or 0xE0 (indicating a extended scan code) the size is increased by one, the new input flag is set to data partly received, the state flag is set to wait, for any other contents of the just received byte the new input flag is set to data received, the state flag is also set to wait

    The state machine assumes, that the data are always sent in the right way and doesn't handle errors well. It would be stuck somewhere in the middle. So I check in the main loop if the wait state is reached. If I don't detect a wait state for 250 ms I reset all PS/2 variables forcing the state machine to wait/idle mode.

    Received bytes are immediately forwarded by the UART interface. This way I could learn more about my keyboard. As you can see, some knowledge about scancodes flew already in the state machine so it is not the first draft. It can already handle a series of bytes leaded by 0xE0 (extended scancode set 1&2) and/or 0xF0 (break code set 2&3) as unity. It can not handle Pause codes for set 1&2 as unity.

    Now I can listen to what my keyboard has to say and learn more about its behaviour, that can be differ between different keyboards. As I told you I used the UART and a terminal emulator.

    Teminals are as old as computers. In the beginning, when you could count all computers at the fingers of one hand and every singe one was a masterpiece of cutting edge technology, obviously not everyone had one under his desk or on his desktop. It wouldn't have fitted under there or would have flattened your writing table. It had it's own building or at least a room. It generated a lot of heat and most likely also a lot of noise. You didn't sit right next to it. But if you didn't only want to run programs on punched paper tape without a chance to variate some inputs, you had to access it.

    Compared to your PC these computers might have been very slow and computing power might not impress you now, but there was no software as complex as nowadays and the Computer didn't bother to calculate pixels and 3D graphics. By hand you couldn't feed it as fast with input, as it could process it. And since it was a hellish expensive piece of hardware it made perfect sense to utilise it as much as possible. So usually more than one person wanted to access it at a time. So there were terminals. A terminal features a way to input data and a way to output data. A keyboard and a monitor are very convenient to emulate a terminal. But the first terminals didn't feature monitors, that was still a long way off.

    The people had to be content with some printers, that could hammer down some signs on a piece of paper. A terminal maybe locked like some hacked typewriter. So it makes quite some sense, that the standard output function in Cs stdio-lib is called printf(). Yes, a server and your coputer, that is connected to it, are somehow similar. You can run a web-application on a server and your computer only serves as an interface. It displays what the server wants it to display and forwards your inputs to the server. But your computer is much smarter than a terminal (even than a "smart" terminal, that is somehow programmable). It is a computer in itself and not some interface. And a terminal connected you to one computer that was like the center of the universe and not to some huge worldwide network.

    Your computer is perfectly capable to emulate some stupid interface. And the very first big computers, the modern day servers and the tiny microcontrollers have one thing in common. In many cases they don't feature such nice input and output devices as your computer. So it's attractive to connect them to your computer.

    The times when it was usual to find a RS-232 port for terminal applications at the back of your PC are long over. The modern serial interface is USB. It comes with the advantage, that it doesn't work with +/-15V, burning your uC to cinders, an with the disadvantage that it is quite a bit more complex. Maybe you uC supports USB. But if you don't plan to build some USB applications it might be unnecessary to get it running. The magical word is USB to serial adapter. I got a little FTDI-232 board. It works at TTL level (5V) or even 3.3 volt, when I place the jumper right. Nice and even nicer. With 3.3 volt I don't need to think about input modes, 5V resilience and level shifters. When I connect it to a USB-port my device manager sees a new serial port. What I need is the port number. COM4 it is.

    So now I start my terminal emulator. I like to use HTerm since I can easily switch between ASCII, Hex, Dec and Bin - it can even display all of those the same time. And it can send series of signs at fixed intervals, this can be very convenient. It is not good to show strings in a formatted way with nice rows and columns, in that respect it is not quite a terminal emulator but in my case it is just what I need. My uC won't react to anything I send this time, because I didn't implement it, so I'm just listening. Contrary to other projects I have quite the powerful input device directly connected to my uC: A keyboard.

    After selecting COM4 I choose the same configurations I selected for the uC and hit the connect button. What will happen, when I press a key on my cheap tinker keyboard? Nothing when I don't supply my uC with power. That one is easy to fix. After hitting the connect button once again and the A-key right after I get 0x1C (scancode set 2 make code A) followed by 0xF0, 0x1C (break code A). What a surprise!

    Just for you I recorded the signals for the letter A using a logic analyser. The real signal wouldn't be so perfectly angular but like the uC the analyser only knows 1 and 0. And I didn't have a oscilloscope at hand. The clock frequency is as specified and the changes on Data occur while Clock is high. So everything is, as it is supposed to be. Pressing the key for maybe 3 seconds I get circa 30 make codes followed by the break code. So make codes are repeated. Later on I must ignore make codes of key that are already knowingly pressed or my buffer would overflow quite soon.

    Pressing down A followed by S (make code 0x1B) the make code of A is repeated until S is pressed. From then on the make code S is repeated.

    By pressing more than two keys I see immediately that the keyboard has no 2-key rollover. I pressed 8 keys, one with each finger except the thumbs. I don't remember which ones so let's have a look at the data:

    • 0x1C -> press A
    • 0x1D -> press W
    • 0x2A -> press V
    • 0x3B -> press J
    • 0x44 -> press O
    • 0x41 -> press ,
    • 0xF0, 0x3B -> release J
    • 0x00 -> keyboard error
    • 0xF0, 0x1D -> release W
    • 0xF0, 0x44 -> release O
    • 0xF0, 0x41 -> release ,
    • 0xF0, 0x2D -> release R
    • 0xF0, 0x2A -> release V
    • 0xD1 -> press W
    • 0xF0, 0xD1 -> release W
    • 0xF0, 0x1C -> release A

    So what did I learn? The keyboard detected up to 6 keys at a time. A keyboard error occurred - that might explain why there is a break code for R but no make code. Overall 7 out of the 8 keys are listed. I got no n-key rollover (all keys can be scanned independently) but that was expected. The keyboard was cheap. But I got multi-key rollover. The keyboard should be able to read more than 2 keys in common combinations. That's a lot better than 2 key rollover. For this reason I made a 8 byte buffer to store the keys currently pressed. If there are constellations where the keyboard can recognize more keys, than 8 I can live with my uC being more forgetful than the keyboard. I can't think of a meaningful input with more than 8 keys in a uC-application. Even more so, if you have first to try out, if the keyboard in use can even recognize those 8 specific keys at the same time. And when ever needed it is no trouble to increase the buffer.

    And there was an error code so the keyboard is not faultless. But it was cheap and I pressed quite a few key at the same time...

    Step 5: Implement the Keyboard in a Project

    I hope I can show more about my project later and I didn't use this keyboard in the end. I wanted to relabel my keys and it is damn difficult to write on silicone or stick anything to silicone. I wrapped cling film around the keyboard to mark it for my first tests. That alone should tell everything. What I need are 26 keys with letters, 26 keys with numbers and 4 keys for some settings.

    I had already a layout - more of one even but this one was marked with a green check mark so it seems I hat already decided to go with it. In the end I found a nicer keyboard (in many ways but with a 2-key rollover - haha - but that's okay for my project). I will show it later.

    But here in short how I process the output of my scan code reading state machine further:

    • I check if the first byte is 0xE0, if yes I set a extended flag (ef) and check the next byte, else I check this byte again
    • I check if the byte is 0xF0, if yes I set a break flag (bf) and check the next byte next, else I feed this byte in my switch statement
    • I check for all scan codes, that can happen in my application
      • if it can only happen with ef I check only with ef
      • if it can only happen without ef I check it only without
      • if both can happen I check both (as in V = 0x7A, Y = 0xE0, 0x7A | not according to scancode but to my keyboard layout)
      • I notice if it is a number, a letter, a code and which number, letter, code
    • if the bf is set
      • and it is a number, the number is deleted from the 8 byte number buffer
      • and it is a letter, the letter is deleted from the 8 byte letter buffer
      • and it is a code, the code is no longer locked (each code key toggels a bit when pressed, if the keyboard repeats the make code it doesn't make sense to toggle the bit each time, so I lock the codes of pressed keys)
    • if the bf is not set
      • and it is a number, the 8 byte number buffer is searched for the number, if it isn't there it will be added
      • and it is a letter, the 8 byte letter buffer is searched for the letter, if it isn't there it will be added
      • and it is a code and the code-key is not logged, the code bit is toggled the signal LED changes its state

    That's all there is to implementing a keyboard to my uC-project.