Funkwecker mit ESP in MicroPython - Teil 2 - IR Sender - AZ-Delivery

This post is also available as PDF document.

After the ESP32 can now take over IR codes from an RC (Remote Control), I want to teach him today to send the trained RC codes. This means that the ESP32 works as a RC automatically, for example, caused by sensor values ​​or received radio messages, devices that react to IR signals. This can be a television, a stereo or a recorder and so on.

On the way to a good solution, the Logic Analyzer served me again, because it can be connected to us Free Logic 2 program of Saleae say what structure the bursets (short pulse consequences) sent by the ESP32 have. Of course, the Logic Analyzer also reveals the duration of the bursts and the individual pulse of the consequences. From this we can calculate the frequency and check whether the time limits of the protocol are observed.

How this works in detail and what possibilities the module RMT offers, I will reveal in today's second episode on the RC controls topic

Micropython on the ESP32 and ESP8266


The ESP32 sends IR sequences

Some new components are added to the previous circuit. The focus is on the IR-Sendediode Ky-005 together with a transistor and two resistors, which together reinforce the weak signal of the GPIO pins. I use a small OLED display to display control information and a button is added as a trigger for the first transfer orders. The period in which processes can be triggered with the button shows an LED of any color.

The circuit

The circuit from the First part of the project I took over directly and upgraded for transmission.

So everything is still set up on the long Breadboard with 62 contact series. It's a good thing that I took the long board, I didn't have to rebuild because the smaller board would now have brought me into trouble with regard to slots.

Figure 1: Growth on the Breadboard

Figure 1: Growth on the Breadboard

Figure 2 shows the transmission unit in detail. But the circuit diagram shows the wiring much better.

Figure 2: Send unit

Figure 2: Send unit

Figure 3: Extended structure - circuit

Figure 3: Extended structure - circuit

In order to increase the transmission performance, the IR broadcast LED is not operated on 3.3V but to 5V. Because this controller does not have a 5V connection on the pen strips, I had to use an external 5V source. The LI-ACKU used with a cell holder is easier to use than a plug-in power supply and offers the advantage of mobile use because the holder electronics also provide the 3.3V that the rest of the circuit requires.

With the resistance of 47Ω in the collector management, the current can be set during the pulse and thus determine the maximum range of the transmitter. Due to the resistance of 47 Ω, also through the diode, a current of 73mA flows at VCC = 5V if the transistor switches through. That would be suboptimal for the IR LED, which tolerates a permanent current of approx. 20 to 30mA.

Life insurance for the IR diode is therefore another circuit knick, the 10kΩ resistor from the base of the switching transistor on GND. It prevents the transistor in the switch-on phase, while the GPIO PIN still runs as an entrance, unlocks through uncontrollably. This is OK for short impulses of a few microsis customers, but not for longer permanent current. The 10kΩ resistor pulls the base on GND potential, so that the transistor blocks until it gets a clear 3.3V level from GPIO16 over the 330Ω resistance. GPIO16 works as an outcome after initialization and delivers controlled levels.

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 with program Logic 2 from Saleae


Li-Akku (18650) with holder Or power supply

The software

For flashes 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

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. I have the process for Thonny 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.

Lighting radio

Ok, let's think of that now first part back. In order for the television to work with the ESP32, we have to send it pulse and breaks of 889µs or 1778µs. But that's not all, the pulses still have to be overlaid with a frequency of 36kHz or 38kHz. You can also express it differently, we have to modulate the frequency of 38kHz with the pulse pause lengths. In the HF technology, this would be an amplitude modulation with a depth of 100% - the carrier of 38kHz is sent with a pulse, while breaks are not sent - radio silence.

There is only one problem. The ESP32 is not fast enough under Micropython to implement the demands via Micropython program. But there is a solution and it is called RMT. The class RMT lives in the module ESP32 And was implemented exactly for cases like ours.

To create complex pulse consequences, we first need an RMT object. There are three variants. We use mode 1. Here is one list or a Tupel Created that contains pulse pause pairs. After each value, the level changes at the starting spin. The finest resolution is 12.5ns (1ns = 1 billionth of seconds). We do not need to dose that exactly, 1µS is enough for us as a base length. So we share the RMT cycle of 80 MHz by 80. I connected the Logic Analyzer with channel 0 to GPIO27.

>>> From machine import Pin code
>>> From ESP32 import RMT
>>> RMTPin = Pin code(27, Pin code.OUT, value = 0)
>>> RMT = RMT(0, pin code=RMTPin,
>>> pulses=(500,1000,1500,2000)

I start sampling with the R button and then send the command in Thonny.

>>> RMT.write_pulses(pulses,0)

The first corner shape now looks like this. The value 500 is output as a resting level (idle level), i.e. as a break and therefore does not appear in the plot.

Figure 4: 500-1000-1500-2000 on GPIO27 with idle_level = 0+data = 0

Figure 4: 500-1000-1500-2000 on GPIO27 with idle_level = 0+data = 0

Figure 5: 500-1000-1500-2000 on GPIO27 with idle_level = 0+data = 1

Figure 5: 500-1000-1500-2000 on GPIO27 with idle_level = 0+data = 1

We start a sampling again idle level= 0, but now set data On 1. The first value now appears as a pulse of length 500µs.

Further, idle level is set to 1.

>>> RMT = RMT(0, pin code=RMTPin,
>>> RMT.write_pulses(pulses,0)
Figure 6: 500-1000-1500-2000 on GPIO27 with idle_level = 1+data = 0

Figure 6: 500-1000-1500-2000 on GPIO27 with idle_level = 1+data = 0

And finally both values ​​on 1, that gives the inverted first case.

>>> RMT.write_pulses(pulses,1)
Figure 7: 500-1000-1500-2000 on GPIO27 with idle_level = 1+data = 1

Figure 7: 500-1000-1500-2000 on GPIO27 with idle_level = 1+data = 1

In part 1, IR receiver, I wrote that every RC5 code has to start with a 1-bit. This means that a break has to take a break, because the level in the second half of the clock decides on a 0 or 1. We also have to consider that the transistor negates the logical level. And that the IR-LED fires when the transistor is controlled (base to 3.3V) and the cathode of the LED pulls to GND. The LED does not release a light if the transistor blocks (base on GND). Which is the right configuration?

Yes, that must be version 2, see Figure 5.

Well, but we don't need a permanent light, but we have to divide the pulse of 889µs into 32 individual pulses of approx. 27µs period period in order to get the required frequency of 38kHz. That too can. We only need to add another parameter to the instantiation of the RMT object. The tupel that we are on tx_carrier Handed over, consists of the carrier frequency in Hertz, the Duty Cycle in percent and in turn a level.

>>> Cfreq=38000
>>> duty=50
>>> RMT = RMT(0, pin code=RMTPin,
         tx_carrier = (Cfreq, duty, 1))

At the GPIO27 output, a 38kHz burst is now always released when the rectangle signal is on low-low. It will look like that. This is exactly what we want. Every second value as a result should reflect the bit state. To make it clear, I did not choose the RC5 sequence here, but time intervals that can easily be assigned to the plots.

Figure 8: RC5 code powdered

Figure 8: RC5 code powdered

After we have basically found our way, we turn to the module with the class Irish to. It includes manageable 62 code lines.

The class irsend

We get the class array For the structure of the tupel with the pulse pause times. The module time-out() the closure provides us Timeoutms() for a non -blocking software timer. Because there is a lot of data waste, we get the garbage collector collect From the module GC.

From array import array
From time-out import *
From GC import collect

The declaration of the class follows Irish With the class attribute PULSE, which means that we set half the length of the BIT to 889µs.

The constructor takes a whole range of parameters, three of which are optional keyword parameters. The position parameter RMT we have to hand over an RMT object that must be instantified in the calling program. pulse max Sets the maximum number of pulse flanks in an RC5 word, tpackage The maximum period of the package. With Rdelay we define the broadcast break between the packages to be sent. Over debug We control the issue of reports at runtime. If the values ​​do not differ from the default values ​​entered, they do not have to be specified when creating the instance.

    def __init__(self,RMT,
                pulse max=28,
       self.ARR = array('H', 0 for _ in range(pulse max))

These argument values ​​then end up in instance attributes. We create an array of the maximum size for the absorption of 16 -bit values. A pointer (index) in the array is needed, .aptr, and set to 0. .level controls the flank change and .toggle The Toggelbit.

The method append() I borrowed from Peter Hinch's package. It does the insertion of an interval or more intervals into the array ARR. Every time you call "() are the ones in the list times contained interval times entered the array. The index is increased each time and the level is switched.

    def append(self, *times):  
       for T in times:
           self.ARR[self.APTR] = T
           self.APTR += 1
           self.level = need self.level  

In certain cases, the duration of an interval must be extended. This is the case when the level of the user signal changes. In this case, it is necessary to extend the duration of the previous interval by 889µs. That makes the method add() that I also borrowed from Peter.

    def add(self, T):  
       assert T > 0
       self.ARR[self.APTR - 1] += T

I have the mode of action of the two methods in the table sheet Coding tabelle.ods shown. In addition we come right away.

The order of interval times is in the method codeit() defined. The address of the RC and the data word are handed over as position parameters. Then we put the array index APTR on 0 and the attribute .level on false. .level marks the logical state of the Manchester signal. We use the device address and command word to the bit positions assigned to you. A special position has the seventh bit of the data word, which is to be transported to position 12 inverted. The Toggle-Bit ends up in position 11. We can display the code to be sent and put the mask for the transfer to 0x2000.

    def codeit(self, AddDR, data):
       D = (data & 0x3f) | ((AddDR & 0x1f) << 6) | \
          (((data & 0x40) ^ 0x40) << 6) | \
          ((intimately(self.toggle) & 1) << 11 )
       mask = 0x2000

The masking bit is now moved by one position to the right with every run of the While loop. The procedure ends when the value 0 is reached, what as False is interpreted. This then breaks off the grinding run.

        while mask:
           IF mask == 0x2000:
               bit = Bool(D & mask)
               IF bit ^ self.level:
                   self.append(PULSE, PULSE)
           mask >>= 1

At the first run, a pulse must be added, the 1-bit. Then we pick through Fierce of the data word D With the mask mask One bit out and convert the result, 0 or 1, into a Boolian value, false or true. The result is then with the current target state in .level exoderated. The truth table of this link can be found in Figure 10.

Is the result True, then a change of water is brought about by extending the interval time of the previous level and adding a normal interval time of 889µs. Figure 11 shows the action in the individual rounds of the While loop. The the associated table leaf you can download.

Figure 9: Manchester code egg

Figure 9: Manchester code egg

Figure 10: Coding table

Figure 10: Coding table

After the times are set, the pushing of the mask bite follows at the end of each run.

The method transmit() summarizes the coding, sending and managing the Toggle bits. It takes the device address, the command code and the number of broadcast repetitions with 2 as a default value. .codeit() then provides the array with the interval times. The for loop ensures the repetitions between which a break of .Rdelay Milliseconds must be.

The timer over Let us set the time of a Manchester package tpackage Milliseconds. over is a non-blocking software timer, during which other things can be done. This is made possible by a so -called Closure. Follow the link to learn more about it.

    def transmit(self, AddDR,data,repeat=2):
       for _ in range(repeat):
           while need over():
           while need delayed():
       self.toggle= need self.toggle

With .rmt.write_pulses() Let us out the pulse sequence on PIN 27 and then wait for the timer. That occurs when the function over() True returns. Then we can be sure that the transfer of the block is over.

Another timer is set to the break between the packages. We wait until it has expired and repeat the transfer. The repeated packages contain the same value of the Toggle bits.

Once everything is done, we negate the Toggle-Bit for the next call and clear the data waste.

The program

The program send() combines the program from the first partthat we as a function learning() Install with the preparations required for sending IR codes. It is actually sent by hand for the time being. After send() was started once.

We have an entire latte of imports.

From ESP32 import RMT
From machine import Pin code,Softi2c
From Irish import Irish
From buttons import Waitfortouch, Waitforrelease, Buttons
From time import sleep
From OLED import OLED
From GC import collect
From sys import exit

Framed data for the IR transfer are declared and defined as in Part 1.


Then we instance a key to PIN 13. This is associated with an LED of GPIO19, which always shines when the button is maintained.


Now we define the RMT PIN and create that RMT-Object RMT. With this we instance the Irsend object ir.

RMTPin = Pin code(27, Pin code.OUT, value = 0)
RMT = RMT(0, pin code=RMTPin,
         tx_carrier = (Cfreq, duty, 1))

We need the I2C object for the OLED instance.

I2C=Softi2c(scl=Pin code(22), sda=Pin code(23), freq=100000)

At GPIO5 we feed the signal from the IR receiver module.

plearn = Pin code(5, Pin code.IN) # IR entrance spin

We know the variables for the learning process from the first part.


The function learning() was the main program in the first part. You can find an exact description there.

def learning(P):
   global keypinated,rccode
   def CB(data, AddDR, CTRL):
       global keypinated,rccode
       IF data < 0:  # NEC Protocol Sends Repeat Codes.
           print("Repeat code.")
           rccode.append((data, AddDR, CTRL))

   ir = Rc5_ir(P, CB)  # Instantiate receivers

   while True:
       T=input("Key name on the RC ->")
       IF T=="Q":
           print(T,"Press the button on the RC briefly")
           while 1:
               IF keypinated:
           line=T+","+Str(Data)+","+Str(AddDR)+","+Str(CTRL)+"\ n" # or
           # line = ",". Join ([T, Str (Data), Str (AdDR), STR (CTRL), "\ n"])

readata() Reads that of learning() created file commands.cfg With the couples from RC keyname and IR code. First we declare an empty Dictionary codes. The display informs us about what to do. With Try - Except Let's capture any file errors.

EF readata():
   D.writer("Try Reading",0,0,False)
       wither open("commands.cfg","R") AS F:
           for line in F:
       D.writer("Got key codes",0,1)
       return codes
   except Oserror AS E:
       D.writer("Not found",0,2)
       return None

With with open Let us create a file handle for reading from the file. Line by line is read and divided into the commas. We unzip the list that arises into the variables key, data, AddDR and CTRL. key is the key to the dictionary codes Under that that Tupel (Data, AdDR, CTRL).

The success report is displayed for three seconds, then we delete the display and give the dictionary back to the main program. When leaving the With block, the file is closed automatically.

Except captures any errors and reports them on the display.

In the main program we start with the offer to read an RC and create a code table. If the button is pressed within 3 seconds, the learning phase starts.

D.writer("Learn >>> Key",0,0)
IF Waitfortouch(button,3):
   From import Rc5_ir
   D.writer("Codes saved in",0,0,False)
   D.writer("Next >> Key",0,2)

After deleting the display, we first finish the program. Transfer commands can then be granted in the following While loop. This time we only do that manually from Repl.

The circuit stands? The external modules are all uploaded to the flash memory of the ESP32? Then it can start.

We start From the editor window with F5.

>>> data
>>> AddDR
>>> ir.transmit(AddDR,data,1)
>>> ir.transmit(AddDR,data,1)
>>> ir.transmit(AddDR,data,1)

Now we can connect the Logic Analyzer to the collector of the driver transistor, start with "R" button.

>>> ir.transmit(AddDR,data,1)

Depending on the status of the Toggle bits, we get one of the following plots.

Figure 11: Toggle-Bit 0

Figure 11: Toggle-Bit 0

Figure 12: Toggle-Bit 1

Figure 12: Toggle-Bit 1

And when you zoom in into one of the bursies, you can see the 38kHz fine structure.

Figure 13: Burst des Transfer from 0-4-1 in detail

Figure 13: Burst des Transfer from 0-4-1 in detail

Next we will connect a PS/2 keyboard to the ESP32 and make the controller convert key codes into ASCII codes. Then we can create the IR code tables on the ESP32 without a PC. A large keyboard can also open various other doors to new projects.

See you then!

DisplaysEsp-32Projekte für anfängerSensoren


Andreas Wolter

Andreas Wolter

@Norbert Schulz: wurde korrigiert.

Andreas Wolter
AZ-Delivery Blog

Norbert Schulz

Norbert Schulz

Bitte den LInk zum Tabellenblatt Codiertabelle.ods korrigieren.

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