This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Low power PWM vs. app_pwm vs. hardware pwm driver?

SDK 14.2, NRF52840, S140

Hi,

Thank you so much in advance for answering my very long question. I am a little confused by the low power pwm example and app_PWM documentation and have some questions:

1) The pwm handler is called very frequently  -- is it called on each app timer tick? Is that 32768 times per second? Why is the timer configured as a single_shot, if the timeout needs to be called so repetitively?

 err_code = app_timer_create(p_pwm_instance->p_timer_id, APP_TIMER_MODE_SINGLE_SHOT, pwm_timeout_handler); // from low_power_pwm.c line 177

2) The pwm handler, however, seems to only be responsible for changing the duty cycle of the pin (I am pwm-ing an LED). Does this mean that if I don't want to change the duty cycle of the LED (that is, if I want to keep the same duty cycle always), does this mean I don't need the pwm handler? Or I still need it, but it can be an empty function? 

3) If I understand properly, the bitmask is supposed to be (1 << PIN_NUMBER)? So if I want to PWM GPIO P0.15, then I have to set bitmask = (1 << 15)? 
What if I wanted to PWM a pin in P1? 

4) It seems like the higher the duty cycle, the dimmer the LED gets. Is that right? This seems backwards. What units are the period and duty cycle in, and how is duty cycle defined relative to period? Is {period = 100, duty cycle = 50} functionally the same as {period = 200, duty cycle = 100}? How do these numbers relate to the timer -- are they ticks? 

5) The LED doesn't seem to be at a very constant brightness level while it is being PWM'd; every so often, I will get a brighter flash, which is a problem because it makes the light distractingly uneven. This is especially evident when the LED is relatively dim. The bright pulses are kind of like this: ______-_______---______---______. Why does this happen?

6) If I am turning on and off different LEDs, and I don't want them to be on all at the same time, before, I would call nrf_gpio_pin_set(LED1); nrf_gpio_pin_clear(LED2); 
Now, do I have to change these to low_power_pwm_start(LED1); low_power_pwm_stop(LED2); ? Would that result in the same behavior -- e.g, LED1 turning on while LED2 turns off, and when each is on, it is PWM'd to the correct respective level (as set by low_power_pwm_init and duty_set)? 

----

Here are my questions about the app_pwm, so I can make a more informed decision about which to choose. 

7) I notice that I need TIMER0 and TIMER1 to both be enabled, even if I am PWM-ing only one LED. I copied the code from the SDK PWM example, which uses TIMER1 for both LEDs. What is TIMER0 for then? If I want to PWM 3 LEDs with 3 different duty cycles, and have them be on and off at different times, it seems like I need a separate timer for each one (3 timers). And if I wanted them to all have the same duty cycle always, the only way to group them up is to use a 2 channel PWM and a separate 1 channel PWM (2 timers). Is this right?

8)  Is the equivalent of nrf_gpio_pin_set(LED1); nrf_gpio_pin_clear(LED2); using the app_pwm to do this: 

while (app_pwm_channel_duty_set(&PWM1, 0, 100) == NRF_ERROR_BUSY);

while (app_pwm_channel_duty_set(&PWM2, 0, 0) == NRF_ERROR_BUSY);

// where both are set to APP_PWM_POLARITY_ACTIVE_HIGH

The behavior I am experiencing is: 

  • if I have only LED1 set to be PWM'd, calling  nrf_gpio_pin_set(LED2); nrf_gpio_pin_clear(LED1); and also nrf_gpio_pin_set(LED1); nrf_gpio_pin_clear(LED2); seems to work, in that LED2 is turned on to full brightness while LED1 is turned off, and then LED2 is turned off while LED1 is turned on to the PWM brightness. 
  • If I have both LED1 and LED2 set to be PWM'd, nrf_gpio_pin_set/clear() don't work anymore. Calls to _set() and _clear() don't do anything. For example, in the following code, where PWM1 is config'd for LED1, and PWM2 is config'd for LED2, both LED1 and LED2 stay on indefinitely:
    while (app_pwm_channel_duty_set(&PWM1, 0, 100) == NRF_ERROR_BUSY);	// start off at 100% duty cycle
    while (app_pwm_channel_duty_set(&PWM2, 0, 50) == NRF_ERROR_BUSY);
    nrf_gpio_pin_clear(LED2);

9) I am confused that there are 4 different compare channels specified here (PWM Module documentation, p. 263). But each PWM instance can only have 2 channels, based on the code. EDIT: It looks like the hardware PWM HAL module is something completely different. Is there an example for how to use it? What are the benefits/drawbacks for each of the 3 PWM options (lpp, app_pwm, hardware pwm)? 

I notice the app PWM library lets you specify period in us, and also duty cycle out of 100%. Does this mean there are possibly rounding errors (even if they are small) when specifying a period that doesn't correspond with a frequency that is an even divisor of the clock frequency? I also assume that the duty cycle gets translated to a compare value, and that this may result in some rounding as well -- is this the case? I am wondering if it is possible to utilize the full dynamic range of the PWM hardware feature, which seems like it has 15-bit resolution. is it possible to specify COUNTERTOP and the COMPARE register values directly? 

I searched but didn't see that COUNTERTOP was even referenced in the app_pwm library, so maybe I am misunderstanding how this works:

10) Related to #9, I thought duty cycle is a number out of 100, but I see that app_pwm_duty_t is a uint16_t. But if I set duty cycle = 5000, somehow app_pwm_channel_duty_get(...) shows that the 5000 got turned into 84. how did this happen, and why is app_pwm_duty_t a uint16? 

Thanks!

  • Hi,

    Most of these questions would have been answered if you tested the examples and used a bit more time with the available documentation on infocenter. Either way, I've tried answering them all... 

    1) The pwm handler is called very frequently  -- is it called on each app timer tick? Is that 32768 times per second? Why is the timer configured as a single_shot, if the timeout needs to be called so repetitively?

    No, it's called per period ticks as specified during initialization. For example, LED 0 has a period of 220, the timeout handler in main.c will therefore be called per 220 ticks. Note that the LED 0 will only change duty cycle after the timeout handler has been called 500 times. It's a bit more complex then just calling the timeout handler repetitively, the driver has to control the states. See the driver pwm_timeout_handler().

     

    2) The pwm handler, however, seems to only be responsible for changing the duty cycle of the pin (I am pwm-ing an LED). Does this mean that if I don't want to change the duty cycle of the LED (that is, if I want to keep the same duty cycle always), does this mean I don't need the pwm handler? Or I still need it, but it can be an empty function? 

     Yes.

     

    3) If I understand properly, the bitmask is supposed to be (1 << PIN_NUMBER)? So if I want to PWM GPIO P0.15, then I have to set bitmask = (1 << 15)? 
    What if I wanted to PWM a pin in P1? 

     Just set it to 1. Note that PIN P0.01 is connected to the lowfreq crystal on the devkit.

    4) It seems like the higher the duty cycle, the dimmer the LED gets. Is that right? This seems backwards. What units are the period and duty cycle in, and how is duty cycle defined relative to period? Is {period = 100, duty cycle = 50} functionally the same as {period = 200, duty cycle = 100}? How do these numbers relate to the timer -- are they ticks? 

    It depends on the polarity that is used. In the example case, the higher the duty cycle the brighter the LED, as the LED will be more ON then OFF during one period.  Both period and ticks are stored as uint8_t, period is given in number of ticks, while duty cycle is given in percentage where 100% is 255. 

     

    5) The LED doesn't seem to be at a very constant brightness level while it is being PWM'd; every so often, I will get a brighter flash, which is a problem because it makes the light distractingly uneven. This is especially evident when the LED is relatively dim. The bright pulses are kind of like this: ______-_______---______---______. Why does this happen?

     Because the duty cycle is changed continuously in the example.

     

    6) If I am turning on and off different LEDs, and I don't want them to be on all at the same time, before, I would call nrf_gpio_pin_set(LED1); nrf_gpio_pin_clear(LED2); 
    Now, do I have to change these to low_power_pwm_start(LED1); low_power_pwm_stop(LED2); ? Would that result in the same behavior -- e.g, LED1 turning on while LED2 turns off, and when each is on, it is PWM'd to the correct respective level (as set by low_power_pwm_init and duty_set)? 

     A PWM signal and a set signal from a GPIO is not the same. A PWM signal is a square wave, where the latter is a constant signal. How much a PWM signal is ON over a period depends on the duty cycle. If a PWM signal is running at 100% duty cycle then it's effectively ON during the whole period, which means that's effectively the same as a set GPIO signal. A quick google search returned this article that nicely explains PWM. 

     

    7) I notice that I need TIMER0 and TIMER1 to both be enabled, even if I am PWM-ing only one LED. I copied the code from the SDK PWM example, which uses TIMER1 for both LEDs. What is TIMER0 for then? If I want to PWM 3 LEDs with 3 different duty cycles, and have them be on and off at different times, it seems like I need a separate timer for each one (3 timers). And if I wanted them to all have the same duty cycle always, the only way to group them up is to use a 2 channel PWM and a separate 1 channel PWM (2 timers). Is this right?

     Only one Timer is needed, and TIMER0 is not really in use in the example. You need one timer per period/frequency that you want the PWM signal to run at.

     

    8)  Is the equivalent of nrf_gpio_pin_set(LED1); nrf_gpio_pin_clear(LED2); using the app_pwm to do this: 

    Setting the duty cycle to 100 is not the same as setting the duty cycle to 100%. The duty cycle in the app_pwm library is stored as a uint16_t. A 100% would therefore be equal to all the bits set to 1 in binary. But yes, a PWM signal at 100% duty cycle is effectively ON the whole time and a signal at 0% is effectively OFF.

     

    9) I am confused that there are 4 different compare channels specified here (PWM Module documentation, p. 263). But each PWM instance can only have 2 channels, based on the code. EDIT: It looks like the hardware PWM HAL module is something completely different. Is there an example for how to use it? What are the benefits/drawbacks for each of the 3 PWM options (lpp, app_pwm, hardware pwm)? 

     app_pwm and low_power_pwm produces PWM by using bit-banging, which means that it's purely SW. The PWM peripheral however is a HW peripheral on the IC and is much more accurate than the SW implementations. It can also be a bit more complex to start with, so what you want to use depends on your experience and use case.

     

    10) Related to #9, I thought duty cycle is a number out of 100, but I see that app_pwm_duty_t is a uint16_t. But if I set duty cycle = 5000, somehow app_pwm_channel_duty_get(...) shows that the 5000 got turned into 84. how did this happen, and why is app_pwm_duty_t a uint16? 

     They duty cycle is converted to ticks in app_pwm_channel_duty_set(). 

    regards

    Jared

  • Hi Jared, thank you very much for answering my questions. I did test the examples, including reading the code from the libraries & changed values in the examples to see how it would behave, and I did read the documentation. I just had some outstanding questions whose answers were not obvious from my experimentation. I appreciate you taking the time to answer them. 

    1, 2, 4, 7, 9) very helpful, thank you!

    3) perhaps my question wasn't clear enough. I was saying that everything in P0 (as in port 0, pins 0-31) make sense as far as how you set the bit mask, because there are 32 bits in the bitmask. But everything above, in P1 (port 1) would require more than 32 bits to distinguish from P0. Does this mean pins in P1 are not accessible for lpp? 

    5) perhaps my question here was unclear too - I disabled the duty cycle change by commenting out the code in the handler, so it should have been one brightness the whole time. doing the same thing with app_pwm worked just fine, but with lpp, it resulted in a random brighter flash every once in a while. 

    6) i think you misunderstand my question. my question here is the same as #8, except for the lpp instead. Of course I know what PWM is, or i wouldn't be trying to use it. I appreciate you taking the time to explain though. I am asking: using the lpp, if I want to turn on and off different LEDs, such that when the are on, they are being PWM'd with the right duty cycle, do I have to use the lpp start and stop functions instead of the GPIO set and clear functions? 

    8)  So there is no way to PWM the LED, turn it off, and then PWM it again to the same duty cycle, except by keeping the original duty cycle as a global variable? Since turning it off requires it to be set to 0 duty cycle. Or is there some other way of turning it off and on, where the duty cycle info is preserved inside the PWM instance? 

    10) 

    This looks like the duty cycle is being divided by 100, which implies that duty cycle is a percentage out of 100%, contrary to what you suggested here:

    Setting the duty cycle to 100 is not the same as setting the duty cycle to 100%

    But when I run this code, 

    void pwm_ready_callback(uint32_t pwm_id)    // PWM callback function
    {
    	NRF_LOG_INFO("LED on? - green: %d, blue: %d, red: %d", nrf_gpio_pin_read(LED_GREEN), nrf_gpio_pin_read(LED_BLUE), nrf_gpio_pin_read(LED_RED));
    	NRF_LOG_INFO("Duty cycle - green: %d, blue: %d, red: %d", app_pwm_channel_duty_get(&PWM_G, 0), app_pwm_channel_duty_get(&PWM_B, 0), app_pwm_channel_duty_get(&PWM_R, 0));
    }
    what I see is that when I do 

    while (app_pwm_channel_duty_set(&PWM_G, 0, X) == NRF_ERROR_BUSY);

    if X is between 0-100, it prints out duty cycle = that same number between 0-100. 

    If I set something over 100, like 102, it still prints out duty cycle = 100. 

    Based on what you were saying in #8, I would expect a number between 0-100 to all be interpreted as almost 0% duty cycle, since 100/65535 is a fraction of a percent, so already this is deviating from expectation. But it gets weirder:

    If I set X = 3000, I get duty cycle = 50. 

    X = 5000 --> duty cycle = 84. 

    X = 5500 --> duty cycle = 93. 

    X = 5600 --> duty cycle = 29.

    X = 5800 --> duty cycle = 65

    X = 5880 --> duty cycle = 100. 

    X = 6000 --> duty cycle = 100.

    X = 7000 --> duty cycle = 100. 

    X = 14000 --> duty cycle = 73

    X = 14300 --> duty cycle = 45

    Basically, none of these make sense, especially not the way you explained in #8. So what is happening here?

    again, I am using SDK 14.2, NRF52840.

    Thank you!

  • Hi,

    It's clear that I misunderstood several of your questions in my previous reply Slight smile

    nordev said:

    3) perhaps my question wasn't clear enough. I was saying that everything in P0 (as in port 0, pins 0-31) make sense as far as how you set the bit mask, because there are 32 bits in the bitmask. But everything above, in P1 (port 1) would require more than 32 bits to distinguish from P0. Does this mean pins in P1 are not accessible for lpp? 

     
    Yes, if you're using an IC that has Port 1 pins such as the nRF52840 you have to modify the example a bit. You need to redefine NRF_GPIO to NRF_P1 instead of NRF_P0.
    nordev said:

    5) perhaps my question here was unclear too - I disabled the duty cycle change by commenting out the code in the handler, so it should have been one brightness the whole time. doing the same thing with app_pwm worked just fine, but with lpp, it resulted in a random brighter flash every once in a while. 

     Strange, I tried commenting out line 92 and did not see any change in LED1. The LED was steady at the set duty cycle. I was not able to reproduce this. 
    nordev said:

    6) i think you misunderstand my question. my question here is the same as #8, except for the lpp instead. Of course I know what PWM is, or i wouldn't be trying to use it. I appreciate you taking the time to explain though. I am asking: using the lpp, if I want to turn on and off different LEDs, such that when the are on, they are being PWM'd with the right duty cycle, do I have to use the lpp start and stop functions instead of the GPIO set and clear functions? 

     

    Yes, you need to have low_power_pwm_config_t struct as a global variable as it's passed to low_power_pwm_start(). If you do that then it should be possible to call low_power_pwm_stop and low_power_pwm_start() to restart the PWM without having to call low_power_pwm_duty_set() in between.
    nordev said:

    8)  So there is no way to PWM the LED, turn it off, and then PWM it again to the same duty cycle, except by keeping the original duty cycle as a global variable? Since turning it off requires it to be set to 0 duty cycle. Or is there some other way of turning it off and on, where the duty cycle info is preserved inside the PWM instance? 

    No not really, the library doesn't have a function for simply turning the PWM off, you would have to un-initialize it. The duty cycle has to be set after each time the module is un-initialized. 

    nordev said:

    10) 

    This looks like the duty cycle is being divided by 100, which implies that duty cycle is a percentage out of 100%, contrary to what you suggested here:

    After taking a second look at this question again, I totally agree that the duty cycle is a percentage out of 100%. Can't quite remember my reasoning behind my previous reply, but I guessed I confused it with the low_power_pwm example. 

    regards

    Jared

  • Thank you so much for your help, Jared. 

    re: #10, the duty cycle in the PWM HAL is set with counter compare values, so maybe that is where you had gotten it from? I started experimenting with it, and I have a question: 

    By default, when I embed it in my own code, it looks like 0 = brightest & COUNTERTOP = off. 

    In the HAL example, it was the opposite (0 = off, 0x8000 = brightest). But I cannot tell how this was set. I thought it was via the below code: 

    but adding NRF_DRV_PWM_PIN_INVERTED to the pins in my code seem to have no effect at all. For example, setting channel 0 of .output pin to 17 or 17 | NRF_DRV_PWM_PIN_INVERTED both result in 0 = brightest and 32767 = off. I am using a custom board with a separate RGB LED & nrf52840, sdk 14.2. Do you know how I can change it so that 0 is off? 

    Also, I saw the example code use 0x8000 (demo 2, seq 1), but I thought the highest that should be possible is 0x7FFF (32767) because only 15 bits are available to set the brightness. How come 0x8000 is valid / possible?

     Strange, I tried commenting out line 92 and did not see any change in LED1. The LED was steady at the set duty cycle. I was not able to reproduce this. 

    Maybe this is because I was using a separate through-hole LED. Is it possibly because of a power requirement difference? 

  • Hello Jared, just wanted to follow up on the following 2 questions:

    Do you know how I can change it so that 0 is off? 

    Also, I saw the example code use 0x8000 (demo 2, seq 1), but I thought the highest that should be possible is 0x7FFF (32767) because only 15 bits are available to set the brightness. How come 0x8000 is valid / possible?

    Thanks a lot!

Related