Funkwecker mit ESP in MicroPython - Teil 3 - PS/2-Tastatur - AZ-Delivery

This post is also available as PDF document.

After the ESP32 now IR codes of take over a RC (remote control) and myself Send such Can, I want to teach him today to read the key codes of a PC keyboard and convert it into ASCII signs. We are able to do the names of the RC buttons when recording without a PC. In addition, there are certainly various other possible uses for the keyboard for future projects.

When researching the way of working a PC keyboard, the Logic Analyzer was again a helpful tool. Without the small thing or a DSO (digital memory oscilloscope) you hardly get the peculiarities of the key codes.

I invite you, the steps to the finished module to go with me, in the third episode on the current topic of IR transfer.

Micropython on the ESP32 and ESP8266


The ESP32 and the PS/2 keyboard

Only one adapter and a PS/2 keyboard are added to the previous circuit.

Figure 1: Simple adapter

Figure 1: Simple adapter

The first attempt to make the adapter with such a part unfortunately failed. The cables red, black, white and green were connected to other sockets than normal. But even after measuring and in a new assignment, no signal arrived at the end. So I tried another converter with whom I was immediately successful.

Figure 2: USB adapter

Figure 2: USB adapter

I made the adapter from this converter from PS/2 to USB by simply cutting the cable for the keyboard connection and extending with four thin cables.

A four -pin pin strip was soldered at the other end so that the thing can be placed on a breakboard.

Figure 3: PS/2 keyboard connection for the Breadboard

Figure 3: PS/2 keyboard connection for the Breadboard

Of course, you could also cut off the plug of the keyboard and solder the plug strip directly to the cables if you need the keyboard to nothing else.

Incidentally, this also works with a USB keyboard, then you only have to stick an adapter from USB to PS/2.

Figure 4: USB PS/2 adapter

Figure 4: USB PS/2 adapter

The circuit

The circuit from the Second part of the project I took over directly. In the end, I only had to supply the keyboard with excitement and connect the data line to the ESP32. The keyboard is very frugal and is satisfied with 3.3V.

The first program -technical approach was more effort. The data is transmitted to a clock signal on the PC synchronously on a separate data line. I will come back to the protocol in detail later. So I intended to do the same on the ESP32. Because that still didn't want to work after a day, I took a different path. Now a data line is sufficient, the data is transmitted asynchronous and accepted to GPIO18 from the ESP32. This solution is compatible with the previous periphery. SCL stays on 22, SDA at 23, the IR receiver at 5 and the transmitter at 27.

Everything is still structured on the Breadboard with 62 contact series. That means that there is still an ESP32 Lolin Designs, which is more narrow -breasted than his larger relatives. Thus, a contact series for jumper cables remains free on the two sides. For an ESP32 Dev Kit C V3 or ESP32 Dev Kit C V4 you need two Breadboards in parallel.

Figure 5: Structure with PS/2-keyboard connection

Figure 5: Structure with PS/2-keyboard connection

The circuit diagram shows the wiring a little more precisely.

Figure 6: circuit with a keyboard connection

Figure 6: circuit with a keyboard connection

We have already arrived at the hardware list.



ESP32 Dev Kit C unpleasant

or ESP32 NODEMCU Module WiFi Development Board

or Nodemcu-ESP-32S kit


Ky-022 Set IR receiver


Ky-005 IR Infrarot transmitter transceiver module


0.91 inch OLED I2C display 128 x 32 pixels


Breadboard Kit - 3x Jumper Wire M2M/F2M/F2F + 3 Set MB102 Breadbord Compatible with Arduino and Raspberry Pi - 1x Set


Ky-004 button module


Jumper Wire cable 3 x 40 pcs


NPN transistor BC337 or similar


Resistance 1.0 kΩ


Resistance 10 kΩ


Resistance 330 Ω


Resistance 47Ω


Resistance 560Ω


LED (color at will)


Adapter PS/2 according to USB or PS/2 socket


Logic Analyzer


PS/2 keyboard

I cut off the PS/2 socket with approx. 5cm cable from the adapter and stripped 2cm to solve the extension lines. They were then insulated with shrink hose pieces.

The connection scheme shows Figure 7.

Figure 7: occupancy of the keyboard socket

Figure 7: occupancy of the keyboard socket

What does the keyboard send?

I had already mentioned that the data from the keyboard is transmitted synchronously via a clock and data line. To get behind which protocol the transfer works, I first looked at the signals with the DSO. This told me that the clock is running with 12kHz (12278Hz), which is not exactly a usual baud rate.

Next I had to find out when the data bits had to be sampled. For this purpose, I switched to the Logic Analyzer because I can use it to record longer sequences in good resolution.

The basic settings of the free program Logic 2 from Saleae you can take Figure 8.

Figure 8: Basic settings of the analyzer

Figure 8: Basic settings of the analyzer

I decided to choose a serial protocol. The selection of the analyzer appears when you click on the "+" sign.

Figure 9: Selection of the analyzer

Figure 9: Selection of the analyzer

The serial protocol offers various setting options such as the number of bits, parity bit as well as the number of stop bits and so on. With the first trial scan, I found that it should be eight bits per byte and that the ninth bit should be a parity bit followed by a stop bit. So access settings to enter this information.

Figure 10: Open Settings

Figure 10: Open Settings

Figure 11: Settings for the analyzer

Figure 11: Settings for the analyzer

With Save Save the whole thing. The keyboard is connected to the ESP32 and supplied with tension. I start the Logic Analyzer with the button R (PC) and tap on the PS/2 keyboard A. After three seconds measurement time, the analyzer automatically snaps into the first sampled byte. The brown points mark the bit positions and the start-bit can be seen that the database is sampled when the clock flank is falling. The frame begins with the LSB (Least Significant Bit = the low -quality bit).

Figure 12: Samping when the flank falls

Figure 12: Samping when the flank falls

If we zoom out with the mouse wheel, we realize that the first burst (pulse sequence) follows two more after about 70ms. Let's take a closer look.

Figure 13: A package consists of three bursts

Figure 13: A package consists of three bursts

Figure 14: The second and third byte

Figure 14: The second and third byte

The second byte has the value 0xF0 and after about 1ms the first byte is repeated. This is how it is for all letters, digits and function keys. If a button stays pressed permanently, the keyboard fires its code again after a break of 530ms at a distance of approx. 100ms. The end form again 0xF0 and the key code.

Figure 15: Code Reduction When holding the button

Figure 15: Code Reduction When holding the button

For capital letters, the sequence starts with the code for the Shift key, 0x12. After a break of approx. 270ms, the normal key code of the sign follows. Then come 0xF0 and the drawing code. The end of 0xF0 and 0x12. For a "A" we find 0x1c, 0xf0, 0x1c, for "A" 0x12, 0x1c, 0xf0, 0x1c, 0xf0, 0x12. It looks like 0xF0 for letting go of the button.

Some buttons in the main keyboard and the tens and control block fire a 5-series package that is for Cursor left looks like this.

0xe0, 0x6b, 0xe0, 0xf0, 0x6b

Very crazy sequences of 10 bytes and more have exotic Pressure, Break and roll. I did not take these keys into account in the following program.

The software

For flashing and the programming of the ESP32:

Thonny or


SaleaeLogic analyzer software (64 bit) For Windows 8, 10, 11

Used firmware for an ESP32:

Micropython firmware

V1.19.1 (2022-06-18).

Used firmware for an ESP8266:

V1.19.1 (2022-06-18).

The micropython programs for the project: Driver module Non -blocking software timer Keyboard driver

Micropython - Language - Modules and Programs

To install Thonny you will find one here Detailed instructions (English version). There is also a description of how that Micropython firmware (As of 05.02.2022) on the ESP chip burned becomes.

Micropython is an interpreter language. The main difference to the Arduino IDE, where you always flash entire programs, is that you only have to flash the Micropython firmware once on the ESP32 so that the controller understands micropython instructions. You can use Thonny, µpycraft or ESPTOOL.PY. For Thonny I have the process here described.

As soon as the firmware has flashed, you can easily talk to your controller in a dialogue, test individual commands and see the answer immediately without having to compile and transmit an entire program beforehand. That is exactly what bothers me on the Arduino IDE. You simply save an enormous time if you can check simple tests of the syntax and hardware to trying out and refining functions and entire program parts via the command line before knitting a program from it. For this purpose, I always like to create small test programs. As a kind of macro, they summarize recurring commands. Whole applications then develop from such program fragments.


If the program is to start autonomously by switching on the controller, copy the program text into a newly created blank tile. Save this file under in WorkSpace and upload it to the ESP chip. The program starts automatically the next time the reset or switching on.

Test programs

Programs from the current editor window in the Thonny-IDE are started manually via the F5 button. This can be done faster than the mouse click on the start button, or via the menu run. Only the modules used in the program must be in the flash of the ESP32.

In between, Arduino id again?

Should you later use the controller together with the Arduino IDE, just flash the program in the usual way. However, the ESP32/ESP8266 then forgot that it has ever spoken Micropython. Conversely, any espressif chip that contains a compiled program from the Arduino IDE or AT-Firmware or Lua or ... can be easily provided with the micropython firmware. The process is always like here described.

The program

These findings should meet for creating a program. I thought and got to work with enthusiasm. But the high mood gradually crawled into the mausel hole. The ESP32 did not come up with decrypting the key codes with interrupts or with polling methods. Micropython itself is just too slow.

But wait - the signal from the keyboard puts on a serial protocol, comparable to that of the RS232, albeit with an exotic baud rate. And the ESP32 has a free serial interface with UART2. So quickly create a uart object and drive a test. The data line comes to GPIO16, which is RDX2, as the following plot shows.

>>> From machine import Uart
>>> uart = Uart(2, baud rate=12000,
>>> print(uart)
Uart(2, baud rate=12000, bits=8, parity=1, Stop=1, tx=17,
RX=16, RTS=-1, CTS=-1, txbuf=256, rxbuf=256, time-out=0, timeout_char=0)

Press the "A" button on the PS/2 keyboard and read in the code.

B '\ X1C \ XF0 \ X1C'

Puhuuu! - Functions!

I can now do without querying the clock line.

The first small test program then quickly became a full-grown module with the PS2 class, which we are now taking a closer look at.

The PS2 class

We need some ingredients. The class comes as the centerpiece Uart With the import from the module machine. There is a lot of data waste that we with the function Collect () collect. For short breaks we get sleep. The module delivers a software timer time-out. All imported identifiers are with the spelling From inserted into the global name room (scope).

From machine import Uart
From GC import collect
From time import sleep
From time-out import *

This is followed by the class declaration with the constructor, the method __init__().

class Ps2():
   def __init__(self,debug=False):
       self.uart = Uart(2, baud rate=12000,
       self.codable = {}
       self.flush buffer()

The only parameter debug is optional, it regulates the issue of debug reports during the term when he True is set. If no parameter is handed over when the constructor is called up, then receives debug The default value False.

Uart0 is through Replica UART1 has its lines in the GPIO area, which is used for the SPI transfer to the Flash-Eeprom. So since only Uart2 can be considered anyway, we instantiate the uart object in the class itself. This runs transparently for the user, which means that he doesn't notice anything. Parity in the transmission is with Parity = 1 on odd set. This means that the number of 1 bits together with parity bit must be odd. The parity bit is therefore set when a number 1 bits occurs in the data byte. Compare Figure 11. In 0x1c there are three 1 bits, so the parity bit is not set.

We find out all the details about the interface Print (self.uart). For the entrance RX GPIO16 is confirmed.

We get the translation of key code into ASCII code later through that Dictionary .codtable. To do this, the table still has to be filled. At the end we do that.

The content of the parameter debug we take over to the instance variable ., so that it is available in all methods. For a clean start we delete the reception buffer of the interface.

The method key() reports the number of characters in the receiving buffer. That whispers the method for us .uart.any().

    def key(self):
       return self.uart.Any()

The raw values ​​from the keyboard fetches radraw(). The list keys Will record the bytes. We look at how many bytes are in the buffer. The next line outputs this number if debug on True stands. I have the trick behind this line in the first episode this series explained exactly. Of course there is only something to pick up if n is larger than zero.

    def radraw(self):
       self.debug and print(n)
       IF n > 0:
           data = bytearar(n)
           self.uart.reap(data, n)
           self.debug and print(data)
           for I in range(n):
               self.debug and print(I,hex(code))
           self.debug and print(keys)
           return keys

The routine .reap() requires the specification of an object that supports the buffer protocol, which is why we have a bytearar n Create elements. The for-Schuffel shovels the bytes code and hangs the Hex-String on the list keys your turn. We collect the data waste and give back the list.

The method just declared is from showcodes() used to continuously display the key codes. We sit n to 0 and enter the While loop.

    def showcodes(self):
       while 1:
           while n == 0:

The second While loop will only be left when at least one byte has arrived. In Figure 11 we have already seen that the second byte is waiting for something, at least around 70 to 80 ms, together with the Shift key, even 170ms. Therefore, we have to take a short break of 200ms until all bytes are incurred in the receiving buffer. Then we pick up the codes, give out and set n Again on 0 for the next round.

readkey() treats the three cases described above and returns the key code. We prepare for the reception of at least three bytes. If there is at least one byte in the memory, then there will certainly be more and we can go to decoding. But we are still waiting 100ms until we really start. Then we pick up 3 bytes. If the first byte 0xe0, the message consists of a total of five bytes. But the key code is already in the second array element. Nevertheless, we still have to pick up two bytes to empty the reception buffer and check the integrity of the key code. It is ok if the second of the last bytes picked up with the value in code matches.

    def readkey(self):
       data = bytearar(3)
       self.debug and print(n)
       IF n>0:
           self.uart.reap(data, 3)
           self.debug and print(data)
           IF data[0] == 0xe0: #follow code 0xe0 0xf0 code
               self.uart.reap(data, 2)
               IF data[1] == code:
                   return code

If the first received byte is 0x12, then five bytes follow, of which we already have three. The second byte is again the key code that we remember. Of the three other bytes, the first with the value in code to match. For the code table, we characterize capital letters and special characters such as "!", "§" etc. by placing 12, which can be achieved by Oder with 0x1200.

            elif data[0] == 0x12: # Code 0xF0 Code 0xF0 0x12
               self.uart.reap(data, 3)
               IF data[0] == code:
                   return 0x1200 | code

Three bytes have arrived when the first and third byte are equal to value and the second byte 0xF0 is. 0x00 is returned for other bytendes.

            elif data[0] == data[2] and data[1] == 0xf0:
               self.debug and print(hex(data[0]))
               return data[0]
               return 0x00

The method awaitkey() Use the method just defined. The parameter delay determined how long to maintain the button. With delay = 0 is waited until the Saint Nimmerlein Day. With Timeoutms() we create a non -blocking software timer. There is a so -called closure behind it. You can find out more about these objects in Closures and decorators.

    def awaitkey(self,delay=200):
       while n==0 and need timedout():
       IF need timedout():
           self.debug and print(code)
       return code

We sit n to 0 and also prove code before with 0. The While loop runs until at least one byte arrived and the timer has not yet expired. With sleep() Such a construction would not be possible. If the loop was left before the timer has expired, we wait a little again, then get the key code and give it back. In the case of a timeout, the 0x00 is.

The method toascii() takes the key code and tries to convert it into ASCII code by searching for the key of the key code in the cod table. An exception is thrown if the code does not exist. So we have to work with Try and Except so that the program does not crash. In the event of a fault, the empty string is returned, otherwise the ASCII sign.

    def toascii(self, SC):
       return C

With scan() we build the code table .codtable on. On the PC keyboard we are waiting to enter the key designation. The process is canceled when "Quit" is entered. The code table is output. We can copy the plot into the clipboard and the content into the constructor self.codtable = {} insert instead of {}.

    def scan(self):
       button=input("Key name:")
       IF button == "Quit":
           self.debug and print(button)
           return button
           IF self.key():
               print(button, hex(SC))

When bytes are there, we get the code, give out the key name and hex string of the key code and carry the key value pair in codable a. The Hex-String is the new key and the tactile name of the ASCII code. At the end we use this routine to build the cod table in one round.

As a rule, you will not need the key code, but the ASCII code, fetchar() fulfills the wish. When there are bytes, the routine gets a key code and returns the ASCII code.

    def fetchar(self):
       IF self.key():
           return self.toascii(SC)

Input of longer texts should also be possible. godword() does that. With the empty string we declare Word and C. In the endless loop, we are waiting for a button infinitely. The key code of the Enter key is 0x5a. We end the input line and leave the loop. All other buttons are converted into ASCII code, output in the input line and to Word attached.

    def godword(self):
       C = ""
       while 1:
           IF C == intimately("0x5a"):
               print("\ n")
               print(C, end="")
               Word += C
       return Word

When deleting the receiving buffer with flush buffer() Let's look whether there are bytes, then the mandatory 200ms wait and read everything the buffer has to offer to send it to nirvana.

A short program is followed by the class declaration, which is (only) executed when the module is started as the main program.

IF __Surname__ == "__Main__":
   # Lead key codes
   # Scan buttons for the code table
   while 1:
       IF key=="Quit":

A PS2 object is instantiated and key deleted. In the main loop we call the method scan() to add an entry to the code table. Delivers scan() "Quit" back, we leave the loop.

A button is pressed on the PS/2 keyboard and then the corresponding button name is entered on the PC keyboard. This can also be done in the block, which makes things very easier. For example, we can type in an entire series of keys on the PS/2 keyboard and then send it into the table on the same train from the PC keyboard. The order must of course be the same. After the content of a dictionary is outstanding in a random sequence, an alphabetical order does not matter when recording. Individual buttons can also be subsequently scan() capture and insert it into the table by copy and paste.

Figure 16: ESP32 with keyboard

Figure 16: ESP32 with keyboard

What's next?

Now the goal is an alarm clock with special functions. This requires the most precise clock with a wake -up function. This is exactly the next episode in which we will put a DS3231 into service.

See you then!

DisplaysEsp-32Projekte für anfänger

Leave a comment

All comments are moderated before being published

Recommended blog posts

  1. ESP32 jetzt über den Boardverwalter installieren - AZ-Delivery
  2. Internet-Radio mit dem ESP32 - UPDATE - AZ-Delivery
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1 - AZ-Delivery
  4. ESP32 - das Multitalent - AZ-Delivery