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

nRF52 PWM / Stepper Motor

I am using the software PWM library (SDK12.0) with the DRV8834 to drive a bipolar stepper motor. Although the motor is working in both directions with the code below, the input for the STEP pin on the driver board wants the following:

#define STEP_PULSE(steps, microsteps, rpm) (60*1000000L/steps/microsteps/rpm) - detail in link below

Consequently, the PWM_library is not a good structure as the above values are in the opposite direction of duty cycle which is the main input for the PWM project. 

#include <stdbool.h>
#include <stdint.h>
#include "nrf.h"
#include "app_error.h"
#include "bsp.h"
#include "nrf_delay.h"
#include "app_pwm.h"
#include "nrf_drv_gpiote.h"


///* Using PWM
APP_PWM_INSTANCE(PWM1,1);             // Create the instance "PWM1" using TIMER1.

int main(void)
{	
	ret_code_t err_code;
    uint16_t MTR_DIR = 26;
	uint16_t MTR1_STEP = 27;
	
   nrf_gpio_cfg_output(MTR_DIR);
	  
   app_pwm_config_t pwm1_cfg = APP_PWM_DEFAULT_CONFIG_1CH(5000L, MTR1_STEP);
	  
	
 // Switch the polarity of the first channel. 
    pwm1_cfg.pin_polarity[0] = APP_PWM_POLARITY_ACTIVE_HIGH;
	 	 	  
    // Initialize and enable PWM. 
    err_code = app_pwm_init(&PWM1,&pwm1_cfg,NULL);
	 
	APP_ERROR_CHECK(err_code);
    app_pwm_enable(&PWM1);
			
	uint8_t rpm_slow = 20;
    uint8_t rpm_fast = 80; 
						
    while (true)
    {
								
	nrf_gpio_pin_write(MTR_DIR, 1); 
		
	while (app_pwm_channel_duty_set(&PWM1, 0, rpm_fast) == NRF_ERROR_BUSY);
	nrf_delay_ms(1000);
		
	//Reverse Motor
	nrf_gpio_pin_write(MTR_DIR, 0); 
		
	while (app_pwm_channel_duty_set(&PWM1, 0, rpm_slow) == NRF_ERROR_BUSY);
	nrf_delay_ms(1000);
	
	}
			
}

My question is as follows:

Is there another example project that is more suitable for these popular drivers (they are all the same - DRV8825, A4988, DRV8880 etc) where the values are not the same as the PWM signal? Or would it be better to just explicitly control the timing, pin values, and pin states? I believe that is what the C++ driver for the boards is doing (link here). 

Thanks in Advance

Parents
  • Hello,

    It may be easier to use the PPI and a timer directly for such applications. We don't have any examples showing how to use this directly, but attached you will find a main.c file that should work in most SDK versions. It uses timer3 (you can change this) to generate 2 PWM signals. If you run it on a DK you can see that it will control two of the LEDs, but it is easy to change the pin numbers.

    /* 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
    
    
    // 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);
    }
    
    // ### Implement a method for initializing PWM channel 1, for a total of 2 individual PWM channels, and call it 'pwm1_init(uint32_t pinselect)'
    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);    
    }
    
    
    
    // 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;
    }
    
    
    // Function for changing the duty cycle on PWM channel 1
    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;
    }
    
    
    
    
    // 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 and 1
        pwm0_init(LED_1);
        pwm1_init(LED_2);
            
        // Start the timer
        timer_start();
    
        while (true)
        {
            nrf_delay_us(4000);
            
            // Update the duty cycle of PWM channel 0 and 1 and increment the counter.
            pwm0_set_duty_cycle(sin_scaled(counter++, 200, 0, TIMER_RELOAD));
            pwm1_set_duty_cycle(sin_scaled(counter + 100, 200, 0, TIMER_RELOAD));
        }
    }
    

    As you can see, there is not a lot of fancy  function wrappers, but it uses the registers for the timers, PPI and GPIOTE peripherals directly.

    See if you can understand how it works. I suggest you keep the nrf_gpio_pin_write() function to control the direction of the motor.

    Best regards,
    Edvin

Reply
  • Hello,

    It may be easier to use the PPI and a timer directly for such applications. We don't have any examples showing how to use this directly, but attached you will find a main.c file that should work in most SDK versions. It uses timer3 (you can change this) to generate 2 PWM signals. If you run it on a DK you can see that it will control two of the LEDs, but it is easy to change the pin numbers.

    /* 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
    
    
    // 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);
    }
    
    // ### Implement a method for initializing PWM channel 1, for a total of 2 individual PWM channels, and call it 'pwm1_init(uint32_t pinselect)'
    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);    
    }
    
    
    
    // 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;
    }
    
    
    // Function for changing the duty cycle on PWM channel 1
    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;
    }
    
    
    
    
    // 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 and 1
        pwm0_init(LED_1);
        pwm1_init(LED_2);
            
        // Start the timer
        timer_start();
    
        while (true)
        {
            nrf_delay_us(4000);
            
            // Update the duty cycle of PWM channel 0 and 1 and increment the counter.
            pwm0_set_duty_cycle(sin_scaled(counter++, 200, 0, TIMER_RELOAD));
            pwm1_set_duty_cycle(sin_scaled(counter + 100, 200, 0, TIMER_RELOAD));
        }
    }
    

    As you can see, there is not a lot of fancy  function wrappers, but it uses the registers for the timers, PPI and GPIOTE peripherals directly.

    See if you can understand how it works. I suggest you keep the nrf_gpio_pin_write() function to control the direction of the motor.

    Best regards,
    Edvin

Children
  • Edvin - Thank you for this! With minimal changes, the code works and is a much better method for communicating with the common stepper motor drivers.

    However, my project has BLE, TWI, and softdevice and there appears to be some issues when I combine with my full project (everything combines fine w/ the PWM Library method above). Granted, I haven't spent a lot of time trying to figure out what is wrong. So just a quick follow up question - Is there a reason why the combined project would work with PWM and not PPI that I need to be aware of?  

  • Hello,

    What you typically have to look out for when you use the softdevice is that you don't use TIMER0, because this is used by the SoftDevice.. The file that I sent uses TIMER3, so that shouldn't be the problem.

    However, if you copied the main() function as well, you will probably run in to some issues. The softdevice is not a huge fan of nrf_delay_ms() or nrf_delay_us(), as it just keeps the CPU busy for that long (4ms in this case). If this causes the softdevice to miss some important events, things will start to behave strange.

    The advantage of using this function is only the fact that it is super easy to set up.

    So when you use the softdevice, avoid using this function (nrf_delay_us()), and rather use a proper timer, such as the app_timer if you need interrupts to trigger the changes of the duty cycles.

    If that doesn't solve it, please let me know.

    Best regards,

    Edvin

Related