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

Control 6 channel PWM at the same time which can modify duty-cycle by app_timer

Hi,

There have 4 PWM sample code (low_power_pwm, pwm_driver, pwm_library and pwr_mgmt) in SDK 15.2.

I my use case is "Control 6 PWM output with different duty-cycle and duty-cycle can modify by app_timer".

Which sample is suitable for this situation?

Thank you,

Chianglin

Parents
  • Hello,

    So you want to have 6 PWM signals with different duty cycles, but same PWM period, is that correct?

    I suggest you check out the low_power_pwm example from the SDK\examples\peripheral\low_power_pwm folder.

    An alternative is to use the PPI and a timer + GPIOTE to generate PWM signals.

    Attached is a main.c file that you can replace any of the peripheral examples main files with, and it uses the PPI to generate PWM signals.

    /* Copyright (c) 2009 Nordic Semiconductor. All Rights Reserved.
     *
     * The information contained herein is property of Nordic Semiconductor ASA.
     * Terms and conditions of usage are described in detail in NORDIC
     * SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT.
     *
     * Licensees are granted free, non-transferable use of the information. NO
     * WARRANTY of ANY KIND is provided. This heading must NOT be removed from
     * the file.
     *
     */
    
    #include "nrf.h"
    #include <stdbool.h>
    #include <stdint.h>
    #include "bsp.h"
    #include "nrf_gpio.h"
    #include "nrf_delay.h"
    #include "math.h"
    
    // Peripheral channel assignments
    #define PWM0_GPIOTE_CH      0
    #define PWM0_PPI_CH_A       0
    #define PWM0_PPI_CH_B       1
    #define PWM0_TIMER_CC_NUM   0
    
    #define PWM1_GPIOTE_CH      1
    #define PWM1_PPI_CH_A       2
    #define PWM1_TIMER_CC_NUM   1
    
    #define PWMN_GPIOTE_CH      {PWM0_GPIOTE_CH, PWM1_GPIOTE_CH, 2, 3, 4}
    #define PWMN_PPI_CH_A       {PWM0_PPI_CH_A, PWM1_PPI_CH_A, 3, 5, 6}
    #define PWMN_PPI_CH_B       {PWM0_PPI_CH_B, PWM0_PPI_CH_B, 4, 4, 7}
    #define PWMN_TIMER_CC_NUM   {PWM0_TIMER_CC_NUM, PWM1_TIMER_CC_NUM, 2, 3, 4}
    
    static uint32_t pwmN_gpiote_ch[]    = PWMN_GPIOTE_CH;
    static uint32_t pwmN_ppi_ch_a[]     = PWMN_PPI_CH_A;      
    static uint32_t pwmN_ppi_ch_b[]     = PWMN_PPI_CH_B;   
    static uint32_t pwmN_timer_cc_num[] = PWMN_TIMER_CC_NUM;
    
    // TIMER3 reload value. The PWM frequency equals '16000000 / TIMER_RELOAD'
    #define TIMER_RELOAD        1024
    // The timer CC register used to reset the timer. Be aware that not all timers in the nRF52 have 6 CC registers.
    #define TIMER_RELOAD_CC_NUM 5
    
    
    // This function initializes timer 3 with the following configuration:
    // 24-bit, base frequency 16 MHz, auto clear on COMPARE5 match (CC5 = TIMER_RELOAD)
    void timer_init()
    {
        NRF_TIMER3->BITMODE                 = TIMER_BITMODE_BITMODE_24Bit << TIMER_BITMODE_BITMODE_Pos;
        NRF_TIMER3->PRESCALER               = 0;
        NRF_TIMER3->SHORTS                  = TIMER_SHORTS_COMPARE0_CLEAR_Msk << TIMER_RELOAD_CC_NUM;
        NRF_TIMER3->MODE                    = TIMER_MODE_MODE_Timer << TIMER_MODE_MODE_Pos;
        NRF_TIMER3->CC[TIMER_RELOAD_CC_NUM] = TIMER_RELOAD;    
    }
    
    
    // Starts TIMER3
    void timer_start()
    {
        NRF_TIMER3->TASKS_START = 1;
    }
    
    
    // This function sets up TIMER3, the PPI and the GPIOTE modules to configure a single PWM channel
    // Timer CC num, PPI channel nums and GPIOTE channel num is defined at the top of this file
    void pwm0_init(uint32_t pinselect)
    {  
        NRF_GPIOTE->CONFIG[PWM0_GPIOTE_CH] = GPIOTE_CONFIG_MODE_Task << GPIOTE_CONFIG_MODE_Pos | 
                                             GPIOTE_CONFIG_POLARITY_Toggle << GPIOTE_CONFIG_POLARITY_Pos | 
                                             pinselect << GPIOTE_CONFIG_PSEL_Pos | 
                                             GPIOTE_CONFIG_OUTINIT_High << GPIOTE_CONFIG_OUTINIT_Pos;
    
        NRF_PPI->CH[PWM0_PPI_CH_A].EEP = (uint32_t)&NRF_TIMER3->EVENTS_COMPARE[PWM0_TIMER_CC_NUM];
        NRF_PPI->CH[PWM0_PPI_CH_A].TEP = (uint32_t)&NRF_GPIOTE->TASKS_CLR[PWM0_GPIOTE_CH];
        NRF_PPI->CH[PWM0_PPI_CH_B].EEP = (uint32_t)&NRF_TIMER3->EVENTS_COMPARE[TIMER_RELOAD_CC_NUM];
        NRF_PPI->CH[PWM0_PPI_CH_B].TEP = (uint32_t)&NRF_GPIOTE->TASKS_SET[PWM0_GPIOTE_CH];
        
        NRF_PPI->CHENSET               = (1 << PWM0_PPI_CH_A) | (1 << PWM0_PPI_CH_B);
    }
    
    
    // ### ------------------ TASK 1: STEP 1 ------------------
    // ### Implement a method for initializing PWM channel 1, for a total of 2 individual PWM channels, and call it 'pwm1_init(uint32_t pinselect)'
    // ### Hint: You can copy 'pwm0_init(..)' and use it as a starting point
    void pwm1_init(uint32_t pinselect)
    {
        NRF_GPIOTE->CONFIG[PWM1_GPIOTE_CH] = GPIOTE_CONFIG_MODE_Task << GPIOTE_CONFIG_MODE_Pos | 
                                             GPIOTE_CONFIG_POLARITY_Toggle << GPIOTE_CONFIG_POLARITY_Pos | 
                                             pinselect << GPIOTE_CONFIG_PSEL_Pos | 
                                             GPIOTE_CONFIG_OUTINIT_High << GPIOTE_CONFIG_OUTINIT_Pos;
    
        NRF_PPI->CH[PWM1_PPI_CH_A].EEP   = (uint32_t)&NRF_TIMER3->EVENTS_COMPARE[PWM1_TIMER_CC_NUM];
        NRF_PPI->CH[PWM1_PPI_CH_A].TEP   = (uint32_t)&NRF_GPIOTE->TASKS_CLR[PWM1_GPIOTE_CH];
        NRF_PPI->FORK[PWM0_PPI_CH_B].TEP = (uint32_t)&NRF_GPIOTE->TASKS_SET[PWM1_GPIOTE_CH];
        NRF_PPI->CHENSET                 = (1 << PWM1_PPI_CH_A) | (1 << PWM0_PPI_CH_B);    
    }
    // ### ----------------------------------------------------
    
    
    // ### ------------------ TASK 1: STEP 2 ------------------
    // ### Using the FORK feature of the PPI try to modify 'pwm1_init(..)' to reduce the number of PPI channels used
    // ### (in total it should be sufficient to use 3 PPI channels for 2 PWM outputs).
    // ### Hint: The FORK feature is useful when the same event needs to trigger several tasks.
    // ### ----------------------------------------------------
    
    
    // ### ------------------ TASK 1: STEP 6 (optional)--------
    // ### Make a generic init function that takes both the PWM channel number and the pinselect as arguments,
    // ### to avoid having to implement one function for each channel. The function should support up to 4 PWM channels total.
    // ### Hint: Don't start on the optional steps until all the required steps are complete. 
    void pwmN_init(uint32_t N, uint32_t pinselect)
    {    
        if(N <= 5)
        {
            NRF_GPIO->DIRSET = 1 << pinselect;
            
            NRF_GPIOTE->CONFIG[pwmN_gpiote_ch[N]] = GPIOTE_CONFIG_MODE_Task << GPIOTE_CONFIG_MODE_Pos | 
                                                    GPIOTE_CONFIG_POLARITY_Toggle << GPIOTE_CONFIG_POLARITY_Pos | 
                                                    pinselect << GPIOTE_CONFIG_PSEL_Pos | 
                                                    GPIOTE_CONFIG_OUTINIT_High << GPIOTE_CONFIG_OUTINIT_Pos;
    
            NRF_PPI->CH[pwmN_ppi_ch_a[N]].EEP     = (uint32_t)&NRF_TIMER3->EVENTS_COMPARE[pwmN_timer_cc_num[N]];
            NRF_PPI->CH[pwmN_ppi_ch_a[N]].TEP     = (uint32_t)&NRF_GPIOTE->TASKS_CLR[pwmN_gpiote_ch[N]];
            if((N % 2) == 0)
            {
                NRF_PPI->CH[pwmN_ppi_ch_b[N]].EEP = (uint32_t)&NRF_TIMER3->EVENTS_COMPARE[TIMER_RELOAD_CC_NUM];
                NRF_PPI->CH[pwmN_ppi_ch_b[N]].TEP = (uint32_t)&NRF_GPIOTE->TASKS_SET[pwmN_gpiote_ch[N]];
            }
            else
            {
                NRF_PPI->FORK[pwmN_ppi_ch_b[N-1]].TEP = (uint32_t)&NRF_GPIOTE->TASKS_SET[pwmN_gpiote_ch[N]];
            }
            NRF_PPI->CHENSET                      = (1 << pwmN_ppi_ch_a[N]) | (1 << pwmN_ppi_ch_b[N]); 
        }        
    }
    // ### ----------------------------------------------------
    
    
    // Function for changing the duty cycle on PWM channel 0
    void pwm0_set_duty_cycle(uint32_t value)
    {
        if(value == 0)
        {
            value = 1;
        }
        else if(value >= TIMER_RELOAD)
        {
            value = TIMER_RELOAD - 1;
        }
        NRF_TIMER3->CC[PWM0_TIMER_CC_NUM] = value;
    }
    
    
    // ### ------------------ TASK 1: STEP 3 ------------------
    // ### Implement a method for setting the duty cycle on PWM channel 1, and call it 'pwm1_set_duty_cycle(uint32_t value)'
    // ### Hint: You can copy 'pwm0_set_duty_cycle(..)' and use it as a starting point
    void pwm1_set_duty_cycle(uint32_t value)
    {
        if(value == 0)
        {
            value = 1;
        }
        else if(value >= TIMER_RELOAD)
        {
            value = TIMER_RELOAD - 1;
        }
        NRF_TIMER3->CC[PWM1_TIMER_CC_NUM] = value;
    }
    // ### ----------------------------------------------------
    
    
    // ### ------------------ TASK 1: STEP 7 (optional) ----------
    // ### Make a generic set duty cycle function to support a total of 4 PWM channels.
    void pwmN_set_duty_cycle(uint32_t N, uint32_t value)
    {
        if(N <= 5)
        {
            uint32_t pwmN_pin_assignment = (NRF_GPIOTE->CONFIG[pwmN_gpiote_ch[N]] & GPIOTE_CONFIG_PSEL_Msk) >> GPIOTE_CONFIG_PSEL_Pos;
            if(value == 0)
            {
                NRF_GPIOTE->CONFIG[pwmN_gpiote_ch[N]] &= ~GPIOTE_CONFIG_MODE_Msk;
                NRF_GPIOTE->CONFIG[pwmN_gpiote_ch[N]] |= GPIOTE_CONFIG_MODE_Disabled << GPIOTE_CONFIG_MODE_Pos;
                NRF_GPIO->OUTCLR = (1 << pwmN_pin_assignment);
            }
            else if(value >= TIMER_RELOAD)
            {
                NRF_GPIOTE->CONFIG[pwmN_gpiote_ch[N]] &= ~GPIOTE_CONFIG_MODE_Msk;
                NRF_GPIOTE->CONFIG[pwmN_gpiote_ch[N]] |= GPIOTE_CONFIG_MODE_Disabled << GPIOTE_CONFIG_MODE_Pos;
                NRF_GPIO->OUTSET = (1 << pwmN_pin_assignment);
            }
            else
            {
                NRF_GPIOTE->CONFIG[pwmN_gpiote_ch[N]] &= ~GPIOTE_CONFIG_MODE_Msk;
                NRF_GPIOTE->CONFIG[pwmN_gpiote_ch[N]] |= GPIOTE_CONFIG_MODE_Task << GPIOTE_CONFIG_MODE_Pos;
                NRF_TIMER3->CC[pwmN_timer_cc_num[N]] = value;    
            }            
        }
    }
    // ### ----------------------------------------------------
    
    
    // ### ------------------ TASK 1: STEP 8 (optional) ----------
    // ### Find a better workaround for a duty cycle of 0% or 100%, so that it is possible to set the PWM output either constantly high or low.
    // ### Implement the workaround in 'pwmN_set_duty_cycle(uint32_t N, uint32_t value)'
    // ### -------------------------------------------------------
    
    
    // Utility function for providing sin values, and converting them to integers.
    // input values in the range [0 - input_max] will be converted to 0-360 degrees (0-2*PI).
    // output values will be scaled to the range [output_min - output_max].
    uint32_t sin_scaled(uint32_t input, uint32_t input_max, uint32_t output_min, uint32_t output_max)
    {
        float sin_val = sinf((float)input * 2.0f * 3.141592f / (float)input_max);
        return (uint32_t)(((sin_val + 1.0f) / 2.0f) * (float)(output_max - output_min)) + output_min; 
    }
    
    
    int main(void)
    {
        uint32_t counter = 0;
        
        // Initialize the timer
        timer_init();
        
        // Initialize PWM channel 0
        pwm0_init(LED_1);
        
        // ### ------------------ TASK 1: STEP 4 ------------------
        // ### Call the init function implemented in STEP 1, and configure the additional PWM channel on LED_2.
        pwm1_init(LED_2);
        // ### ----------------------------------------------------
        
        // ### ------------------ TASK 1: STEP 9 (optional) -------
        // ### Call the generic init function implemented in STEP 6, and configure 2 more PWM channels on LED_3 and LED_4.
        pwmN_init(2, LED_3);
        pwmN_init(3, LED_4);
        // ### ----------------------------------------------------
        
        // Start the timer
        timer_start();
    
        while (true)
        {
            nrf_delay_us(4000);
            
            // Update the duty cycle of PWM channel 0 and increment the counter.
            pwm0_set_duty_cycle(sin_scaled(counter++, 200, 0, TIMER_RELOAD));
            
            // ### ------------------ TASK 1: STEP 5 ------------------
            // ### Update the duty cycle of PWM channel 1, and add an offset to the counter to make the LED's blink out of phase.
            pwm1_set_duty_cycle(sin_scaled(counter + 50, 200, 0, TIMER_RELOAD));
            // ### ----------------------------------------------------
            
            // ### ------------------ TASK 1: STEP 10 (optional) ------
            // ### Update the duty cycle of PWM channel 2 and 3, using the generic functions implemented earlier.
            pwmN_set_duty_cycle(2, sin_scaled(counter + 150, 200, 0, TIMER_RELOAD));
            pwmN_set_duty_cycle(3, sin_scaled(counter + 100, 200, 0, TIMER_RELOAD)); 
            // ### ----------------------------------------------------        
        }
    }
    

    Best regards,

    Edvin

  • Hi Edvin,

    Thank you for your source code.

    Yes, these 6 PWM signal need use different duty cycle but use the same PWM period.

    In your source code, the Max PWM channel is 4 channel, does it right?

    Would you please tell me how can I extend it to 6 channel?

    Thank you

  • Hello,

    This example may be a bit confusing since it is an exercise to generalize these setup functions. The limitation here is the number of CC (capture compare) registers in TIMER3. Note that the different timers have different number of CC channels.  

    TIMER3 has 6 CC registers (some other timers have 4), which is used to reset the PWM pulse (high to low), and reload the timer. This means that this is common for all PWM signals/pins, and you can have 5 PWM channels on this timer, unfortunately. One register is used for each signal, to decide when to go from low to high. You can of course decide whether you want active high or active low PWM signals. 

    If you need one more, you need to use two timers. 

    I see that the PWM peripheral (physical PWM peripheral) on the nRF52832 only has 4 PSEL.OUT registers, so this peripheral is limited to 4. 

    So if you need to use 6 PWM pins at once, you would either have to use the PWM peripheral for 4 of them, and e.g. PPI (the attached project) for the remaining 2, or use PPI for all 6, but you would have to use two timers to get enough CC channels. This will not guarantee that the PWM pins that are using different timers will reset at the same time, but that may be fine.

    I don't know on what speed your PWM signals need to run on. If there is a chance that it is running fairly slow, you may also look into the pwm_library example. This one could have up to 6 channels, but there is a drawback on this. This library needs to interrupt the CPU every time it flips the signal of a PWM pin. If you intend to use BLE in addition to PWM, then you may also expect that this PWM is not running as smooth as a PPI pwm implementation or the PWM HW peripheral. This is because if you get a SoftDevice (BLE) interrupt the CPU interrupt will be delayed.

    Best regards,

    Edvin

Reply
  • Hello,

    This example may be a bit confusing since it is an exercise to generalize these setup functions. The limitation here is the number of CC (capture compare) registers in TIMER3. Note that the different timers have different number of CC channels.  

    TIMER3 has 6 CC registers (some other timers have 4), which is used to reset the PWM pulse (high to low), and reload the timer. This means that this is common for all PWM signals/pins, and you can have 5 PWM channels on this timer, unfortunately. One register is used for each signal, to decide when to go from low to high. You can of course decide whether you want active high or active low PWM signals. 

    If you need one more, you need to use two timers. 

    I see that the PWM peripheral (physical PWM peripheral) on the nRF52832 only has 4 PSEL.OUT registers, so this peripheral is limited to 4. 

    So if you need to use 6 PWM pins at once, you would either have to use the PWM peripheral for 4 of them, and e.g. PPI (the attached project) for the remaining 2, or use PPI for all 6, but you would have to use two timers to get enough CC channels. This will not guarantee that the PWM pins that are using different timers will reset at the same time, but that may be fine.

    I don't know on what speed your PWM signals need to run on. If there is a chance that it is running fairly slow, you may also look into the pwm_library example. This one could have up to 6 channels, but there is a drawback on this. This library needs to interrupt the CPU every time it flips the signal of a PWM pin. If you intend to use BLE in addition to PWM, then you may also expect that this PWM is not running as smooth as a PPI pwm implementation or the PWM HW peripheral. This is because if you get a SoftDevice (BLE) interrupt the CPU interrupt will be delayed.

    Best regards,

    Edvin

Children
  • Hi Edvin,

    Thank you for your detail explain.

    Actually, I want use nRF52832 to control a "Six-axis robot", so I need 6 PWM single to control servo-motor.  The PWM frequency is fix at 50Hz, but I need change it's duty-cycle to modify the "servo-motor rotate angle".  Maybe pwm_library example is a better choice, because it looks easy to use.  It is more complex for me to use PPI and Timer low-level function to create PWM.

    Does APP_PWM_xxxx can support up to 6 PWM with 6 difference duty-cycle and the same frequency?  How to do this job?

    Would you please give me an simple?

    Thank you,

    Chianglin

  • I see. If I am not mistaken those kind of motors have a PWM period of 20ms (50Hz) and typically a PWM Duty Cycle of 1-2 ms depending on the desired angle, right?

    Do you plan to use Bluetooth Low Energy as well?

    The reason I am asking is that the app_pwm uses the application timer to toggle the pulses. This means that it uses a physical timer for the app timer, but the CPU to handle the timer interrupts, and these toggles the pins. The difference using PPI or the PWM peripheral is that you don't need to interrupt the CPU to toggle the pins. It is done "automatically" in HW. 

    The issue by toggling these pins using CPU interrupts is that if the CPU is busy doing something else, e.g. handling a SoftDevice/BLE event, then the toggling of the pin will be delayed, which may cause twitches on the motors. If you test the app_pwm using LEDs, or you motors together with some BLE activity, you will be able to see this with your eyes. If this is tolerable, then that is fine. But if these twitches are too much, you must use the TIMER+PPI. 

    Best regards,

    Edvin

  • Hi Edvin,,

    I am try to add a nrf timer into my original ble_app_uart project, but the system will crash when execute "ble_stack_init()" function.

    Please find following source code

    #include "nrf.h"
    #include "nrf_log.h"
    #include "nrf_log_ctrl.h"
    #include "nrf_drv_timer.h"
    #include "bsp.h"
    #include "nrf_gpio.h"
    #include "app_error.h"
    #include "TimerPtocess.h"
    
    const nrf_drv_timer_t TIMER_US_ID = NRF_DRV_TIMER_INSTANCE(0);
    void timer_us_event_handler(nrf_timer_event_t event_type, void* p_context)
    {
        switch (event_type)
        {
            case NRF_TIMER_EVENT_COMPARE0:
            	nrf_gpio_pin_toggle(25);
        		break;
    
            default:
                //Do nothing.
                break;
        }
    }
    
    void Ctimers_init(void)
    {
    	uint32_t				time_us = 52; //Time(in miliseconds) between consecutive compare events.
    	uint32_t				time_ticks;
    	uint32_t				err_code = NRF_SUCCESS;
    	nrf_drv_timer_config_t	timer_cfg = NRF_DRV_TIMER_DEFAULT_CONFIG;
    
    	err_code = nrf_drv_timer_init(&TIMER_US_ID, &timer_cfg, timer_us_event_handler);
    	APP_ERROR_CHECK(err_code);
    	NRF_LOG_INFO("B0001");
    	time_ticks = nrf_drv_timer_us_to_ticks(&TIMER_US_ID, time_us);
    	NRF_LOG_INFO("B0002");
    	nrf_drv_timer_extended_compare(&TIMER_US_ID, NRF_TIMER_CC_CHANNEL0, time_ticks, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, true);
    	NRF_LOG_INFO("B0003");
    	nrf_drv_timer_enable(&TIMER_US_ID);
    	NRF_LOG_INFO("B0004");
    }
    

    And I insert Ctimers_init() in original main.c (like following code)

    	gpio_init();
    	uart_init();
    	log_init();
    	timers_init();
    	NRF_LOG_INFO("--------------------- Start ---------------");
    	Ctimers_init();
    
    	twi_init();
    	NRF_LOG_INFO("B0006");
    	power_management_init();
    	NRF_LOG_INFO("B0010");
    	ble_stack_init();
    	NRF_LOG_INFO("B0011");

    The debug message is

    <info> app: --------------------- Start ---------------
    00> <info> app: B0006
    00> <info> app: B0010
    00> <error> app: Fatal error

    Does nrf timer can not use in ble_app_uart project?

    Because I need a quick GPIO on/off output (100KHz),, but APP timer can not create so-much quick timer.

    Would you please tell me how can I do?

    Best Regards,

    Chianglin

Related