nRF52 PWM / Stepper Motor

The article  RE: nRF52 PWM / Stepper Motor has most of what I need.  The unfortunate thing is that the solution is written for a version of the SDK that I do not know.  I am new to the nRF52* platform, and have come up to speed with using nRFConnect V2.1.0 with VSCode.  The solution does not compile under this newer version of the SDK.  Would someone be able to translate it?  

Specifically, I am trying to drive 2 stepper motors without the use of a secondary MCU to control the steps/acceleration/position/etc that comes with steppers.  I was able to use PWM to drive one motor, but I need 2 motors running at an independent frequency for differing speeds. According to the nRF52840 spec, the same PWM frequency is shared with all the PWM outputs. I would love to try the PPI/counter example to see if it would work for me, but as far as demo code for PPI and Counters with nRFConnect2.1.0, examples are scarce. 

So bottom line is, with nRF.Connect v2.1.0, is there a low-overhead method that would allow the chip to handle 2 stepper motors with minimal overhead?  

  • According to the nRF52840 spec, the same PWM frequency is shared with all the PWM outputs.

    All the outputs of a single PWM peripheral share a frequency. But there are several PWM peripherals in the nRF52840. You should be able to use two PWM peripherals independently.

  • Hello,

    I have a few repositories that may be interesting for you. 

    The bare metal PPI implementation was ported for a side project, and you can find it here:

    https://github.com/edvinand/ppi_pwm_hands_on

    Please see the file: suggested_solution_main.c

    If you want to go with this approach, and you need different PWM frequencies, I believe you need to use two separate timer instances for this, since this PPI implementation resets the timer counter register when the PWM period is reached. 

    If you want to use the PWM peripheral, and you want the two PWM signals to have different periods, you would need to use two instances of the PWM. As you can see here, the nRF52840 has 4 PWM instances, which can be set with their own countertops (which will give different frequencies). 

    I have not played too much around with the PWM peripheral in NCS, but I suggest that you try to use the nrfx pwm drivers directly in case you want to test this. That gives you better control of the peripherals than if you use the generic Zephyr drivers.

    Best regards,

    Edvin

  • I misread the spec. I see now that the nRF52840 chip contains Four PWM hardware modules that have 4 PWM outputs with a common frequency per module. I was able to get two motors running, each at a different frequency.   It took me a bit of time to get the overlay file setup correctly, as this is still a bit foreign to me. 

    I modeled the PWM1 and PWM2 after the PWM0/LED setup as described for the nRF52840DK board.  So in the end, I now have 3 PWM modules: PWM0 for LEDs, PWM1 for Motor0, PWM2 for Motor1.   

    The source of what I've created is below.  It only contains the PWM step setup and drive.  The question I have now, is....

    should I be modeling the pwmmotors node as a pwm-leds compatible entity?  or should it be something else?

    //nrf52840_nrf52840.overlay
    
    &pwm1 {
        status = "okay";
        pinctrl-0 = <&pwm1_default>;
        pinctrl-1 = <&pwm1_sleep>;
        pinctrl-names = "default", "sleep";
    };
    
    &pwm2 {
        status = "okay";
        pinctrl-0 = <&pwm2_default>;
        pinctrl-1 = <&pwm2_sleep>;
        pinctrl-names = "default", "sleep";
    };
    
    
    &pinctrl {
        pwm1_sleep: pwm1_sleep {
    		group1 {
    			psels = <NRF_PSEL(PWM_OUT0, 1, 3)>;
    			low-power-enable;
    		};
    	};
        pwm1_default: pwm1_default {
            group1 {
                psels = <NRF_PSEL(PWM_OUT0, 1, 3)>;
                nordic,invert;
            };
        };
        pwm2_sleep: pwm2_sleep {
    		group1 {
    			psels = <NRF_PSEL(PWM_OUT0, 1, 4)>;
    			low-power-enable;
    		};
    	};
        pwm2_default: pwm2_default {
            group1 {
                psels = <NRF_PSEL(PWM_OUT0, 1, 4)>;
                nordic,invert;
            };
        };
    };
    
    / {
        pwmmotors {
            compatible = "pwm-leds";
            pwm_motor0: pwm_motor_0 {
                pwms = <&pwm1 0 PWM_MSEC(20) PWM_POLARITY_INVERTED>;
            };
            pwm_motor1: pwm_motor_1 {
                pwms = <&pwm2 0 PWM_MSEC(20) PWM_POLARITY_INVERTED>;
            };
        };
        aliases {
            pwm-motor0 = &pwm_motor0;
            pwm-motor1 = &pwm_motor1;
        };
    };
    //main.c
    
    #include <zephyr/zephyr.h>
    #include <zephyr/sys/printk.h>
    #include <zephyr/device.h>
    #include <zephyr/drivers/pwm.h>
    
    static const struct pwm_dt_spec pwm_led0 = PWM_DT_SPEC_GET(DT_ALIAS(pwm_led0));
    static const struct pwm_dt_spec pwm_motor0 = PWM_DT_SPEC_GET(DT_ALIAS(pwm_motor0));
    static const struct pwm_dt_spec pwm_motor1 = PWM_DT_SPEC_GET(DT_ALIAS(pwm_motor1));
    
    #define FREQUENCY_MIN (512U)
    #define FREQUENCY_MAX (16384U)
    #define FREQUENCY_DELTA (512U)
    
    void main(void)
    {
    	int ret;
    
    	printk("PWM1-based motor control\n");
    
    	if (!device_is_ready(pwm_led0.dev)) {
    		printk("Error: PWM0 device %s is not ready\n",
    		       pwm_led0.dev->name);
    		return;
    	}
    	if (!device_is_ready(pwm_motor0.dev)) {
    		printk("Error: PWM1 device %s is not ready\n",
    		       pwm_motor0.dev->name);
    		return;
    	}
    	if (!device_is_ready(pwm_motor1.dev)) {
    		printk("Error: PWM2 device %s is not ready\n",
    		       pwm_motor1.dev->name);
    		return;
    	}
    	while(1) {
    		for(int i = FREQUENCY_MIN; i <= FREQUENCY_MAX; i+=FREQUENCY_DELTA) {
    			printk("%i:",i);
    			if (pwm_set_dt(&pwm_motor0, PWM_HZ(i), PWM_HZ(i) / 2U))
    				printk("Error %d: failed to set pulse width\n", ret);
    			
    			printk("%i:", FREQUENCY_MAX-i+FREQUENCY_MIN);
    			if (pwm_set_dt(&pwm_motor1, PWM_HZ(FREQUENCY_MAX-i+FREQUENCY_MIN),
    				PWM_HZ(FREQUENCY_MAX-i+FREQUENCY_MIN)/2U))
    					printk("Error %d: failed to set motor0 pulse width\n", ret);
    		}
    
    		for(int i = FREQUENCY_MAX; i >= FREQUENCY_MIN; i-=FREQUENCY_DELTA) {
    			printk("%i:",i);
    			if (pwm_set_dt(&pwm_motor0, PWM_HZ(i), PWM_HZ(i) / 2U))
    				printk("Error %d: failed to set pulse width\n", ret);
    
    			printk("%i:", FREQUENCY_MAX-i+FREQUENCY_MIN);
    			if (pwm_set_dt(&pwm_motor1, PWM_HZ(FREQUENCY_MAX-i+FREQUENCY_MIN),
    				PWM_HZ(FREQUENCY_MAX-i+FREQUENCY_MIN)/2U))
    					printk("Error %d: failed to set motor0 pulse width\n", ret);
    		}
    		
    	}
    }
    
  • Thank you for these;  in going through them, and the documentation, I gained a better understanding of the hardware and how it is setup. It also helped setup the overlay needed for the PWM example in this thread. 

  • I edited the above to use pins P1.03 and P1.04.... these are unused on the nRF52840DK board.  Also, my prj.conf file is here:

    CONFIG_STDOUT_CONSOLE=y
    CONFIG_PRINTK=y
    CONFIG_PWM=y
    CONFIG_LOG=y
    CONFIG_LOG_PRINTK=y
    CONFIG_LOG_MODE_IMMEDIATE=y
    CONFIG_PWM_LOG_LEVEL_DBG=y

    The key thing being the CONFIG_LOG_MODE_IMMEDIATE=y

    This slows down the processing enough for you to experience the physical device changing speeds with the different PWM.

Related