Arduino (AVR) PWM with duty cycle for IR

This post will give you some code to generate high frequency square waves for IR sensors as well as explain how the Fast PWM mode works for Arduino.

block-diagram3I needed to interface with some IR sensors when designing the high tech mini golf hole and had to generate a 38 kHz square wave to drive the infrared LED so that it could be seen by the TSOP4838 IR receivers.

The basic Arduino analogWrite function is limited to less than 1 kHz (depending on your board), so you need to directly access one of the other on board Timer/PWM modules. The Arduino Uno (ATmega328) has 3 timer/counters:

  • Timer/Counter0: 8 bit, already used by Arduino for timing functions like millis() and delay().
  • Timer/Counter1: 16 bit, which is more than I need for this, so I'd rather save it for something else.
  • Timer/Counter2: 8 bit, fits my needs perfectly.

Once you choose which timer you want to use, you would head off to the datasheet of the ATmega328 (which is the chip that the Arduino Uno actually uses) and figure out how to use the timer/counter/PWM module. I'm going to explain that for you now, but you should still make a point of at least skimming the datasheet at some point. Arduino is a great platform for getting you up and running quickly, but sooner or later, you will have to be able to understand the datasheet to access all of the really good stuff.


    // Setup Timer/Counter2 for fast PWM mode with duty cycle control
    // For Arduino Uno (ATmega328)
    // Outputs wave on OC2B pin (Arduino Uno pin #3)
    // OCR2A controls output wave period (and thus frequency)
    //   frequency = 16e6/prescaler/(1+OCR2A)  //prescaler = 8 in example below
    // OCR2B controls output wave duty cycle = (1+OCR2B)/(1+OCR2A), if OCR2B <= OCR2A
    //                                       = 100%,                if OCR2B >= OCR2A
    //   setting OCR2B to 0 does not give 0% duty cycle. See above formula.

    #define IR_LED_PIN 3
    pinMode(IR_LED_PIN, OUTPUT); //set OC2B pin as an output

    //configure registers for desired settings (fast PWM mode & duty cycle control)
    //operation: set output high.
    //           once count matches OCR2B register, set output low.
    //           once count matches OCR2A register, reset the counter and start over.
    TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
    TCCR2B = _BV(WGM22);

    //set frequency
    OCR2A = 52; //with prescaler of 8, should give 37.74 kHz. Measured 37.68 kHz with EX330

    //set duty cycle.
    OCR2B = 26; //50.94% duty cycle. calculation: (1+26)/(1+52)

    //start timer with prescaler of 8
    TCCR2B |= _BV(CS21);

The above snippet should be all you need to get up and running. Read on if you want to understand the guts of how it works in the actual microcontroller.


What is going on in the hardware?

The following lines of code select how the Timer/Counter 2 module will run by specifying the values for TCCR2A and TCCR2B.

TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
TCCR2B = _BV(WGM22);

The "_BV(bit)" macro simply allows you to set bits high by their position. I could have written TCCR2A = 0b00100011 or 0x23, but this loses the context of what I'm trying to do: set the bits high that correspond to COM2B1, WGM21, and WGM20.

//_BV(bit number) written out
_BV(0) = 0b00000001 //set the 0th bit to 1
_BV(1) = 0b00000010 //set the 1st bit to 1
_BV(2) = 0b00000100 //set the 2nd bit to 1
_BV(3) = 0b00001000 //set the 3rd bit to 1
_BV(4) = 0b00010000 //set the 4th bit to 1
_BV(7) = 0b10000000 //set the 7th bit to 1


Table 17-8 in the datasheet explains that bits WGM2:0 control the mode of Timer 2. I've selected "Fast PWM" mode to get max speed and simplicity. Note that the three WGM2:0 bits are actually split between registers TCCR2A and TCCR2B.wgm-settings2

Table 17-6 of the datasheet shows that our PWM output pin will be OC2B and that it will be set high and cleared low (more on this below).


Figure 17-1 of the datasheet (see below) is where things start to get interesting. The Timer/Counter 2 module mainly consist of a timer register TCNT2, and two output compare registers OCR2A and OCR2B.

OCR2A controls the PWM frequency.

In our "Fast PWM" mode, TCNT2 counts up. When its value matches OCR2A (yellow & purple lines), the control logic understands that it is at the TOP and prepares to reset the timer to zero on the next clock pulse. OCR2A allows us to control the frequency of our PWM signal by determining how many clock pulses before restarting the timerWe don't use the output pin OC2A, because it is tied to match events from OCR2A and would only allow us to output a frequency with a fixed 50% duty cycle. Using output OC2B allows us to have control over frequency and duty cycle.

OCR2B controls the PWM duty cycle.

The output pin (OC2B) is set high when the timer reaches OCR2A and is reset back to zero. The output continues to stay high until the timer count matches OCR2B (yellow & blue lines) and the output pin is cleared.


Special case: you cannot set duty cycle to zero!

From this design, you can see that setting OCR2B = 0 will actually not result in a duty cycle of zero. Let's see what happens when OCR2B = 0. You have to remember that everything happens with the input clock. When TCNT2TOP, the control logic sets up 2 things to happen on the next clock pulse: (1) TCNT2 will reset back to zero and (2) the output pin will be set high. When the next clock pulse arrives, the output is set high, TCNT2 is reset to zero and now matches OCR2B. Now that OCR2B matches TCNT2, the control logic prepares to set the output pin low at the next clock pulse. This delay prevents the duty cycle from ever being 0.

Using the inverted output option allows you to be able to set the duty cycle to zero, but then prevents you from having 100% duty cycle. A bit of a crummy design if you ask me. I think "Phase Correct PWM Mode" allows setting duty cycle between 0 and 100%, but has the limitation of operating at half the speed of "Fast PWM Mode".

// When running in inverted Fast PWM Mode
// OCR2B controls output wave duty cycle = (OCR2A-OCR2B)/(1+OCR2A), if OCR2B <= OCR2A
//                                       = 0%,                      if OCR2B >= OCR2A

Special case: what if OCR2A = OCR2B?

Figure 17-1 isn't clear in this case. Assume OCR2A = OCR2B = 50. When the timer counts up from 49 to 50 it matches OCR2A and the control logic gets ready to reset the timer, and set the output pin high. However, the timer also matches OCR2B and the control logic gets ready to set the output pin low. This contradiction of setting the output low and high at the same time is covered in a footnote for Table 17-6: "A special case occurs when OCR2B equals TOP and COM2B1 is set. In this case, the Compare Match is ignored, but the set or clear is done at BOTTOM." The result of this special case means that when OCR2B is greater than or equal to OCR2A, the duty cycle will be 100%.




  1. Pedro Ayala says:

    It would be nice to see an example of thow to send a simple bit or byte with a IR LED in an arduino to a TSOP4838 receiver in another arduino.

    1. Adam Fraser-Kruck says:

      Hi Pedro,

      For more robust IR communications, you'll want to use a protocol like RC5, NEC, or some other pulse position/width modulation scheme.

      I'd recommend you take a look at this sparkfun tutorial: https://learn.sparkfun.com/tutorials/ir-communication

  2. Arduino Playground - Timer1

  3. GURU says:

    I have given

    OCR2B = 0.4; FOR 70% DUTY CYCLE

    but i can get the frequency of PWM with 1 mhz but i am getting duty cycle 0f 50%...can you please say how to get 70 % duty cycle..thanks in advance

    1. mostafa abdelghani says:

      your clock frequency too low to generate 38KHz even if you use rescaler 1 need to increase your clock FPWM (38000) = 1M/1.256 = 0.1 which is not proper.

  4. Nikij21 says:

    Can you also please explain timer 1 phase correct mode briefly ?

    1. Adam Fraser-Kruck says:

      Hi Nikij21! Sorry, but I haven't touched an AVR for quite some time and would need to read up on it again. All the best!

  5. Barry Jobsis says:

    Thank you for this unique explenation of the relation between OCR2A and OCR2B. I could not find it in any documentation.

    1. Adam Fraser-Kruck says:

      you're welcome! Happy tinkering :)

Leave a Reply

Your email address will not be published. Required fields are marked *