PWM testing, learning

Eventually I would like to drive the typical IR_LED at 38Khz (26 microseconds per cycle). I cloned Binky_PWM to start. 

I thought it prudent to flash a visible LED (led0) at 1hz to confirm I understand... It does flash, but I doubt the rate is 1hz and I don't understand at least three things:

  1. I can't set PWM_HZ(4) to anything lower than 4 or I get no flashing. I expected to enter a 1 here.
  2. I don't fully understand: ret = pwm_set_dt(&pwm_led0, period, period / 2U);  especially "period / 2U". I understand this to be the pulse width, but I expected a value similar to "50" (50%) or "13" (13µs, 50% of the duty cycle at 38Khz)
  3. I don't understand how k_msleep(xxx); works here. Any value for k_msleep doesn't seem to affect the flashing rate, however I am used to bare metal programming and only beginning to use threads, so it's unsurprising that k_msleep behavior is unclear to me.

Ultimately I wish to flash:  38Khz, 50% duty cycle

Potentially, I will also need to modulate the carrier on/off every 600us (I'm using 1ms, 10ms, etc just for my current testing). However unlike TSOP modules, the TSSP module (TSSP58038) I am looking at doesn't appear to require modulation. I thought k_msleep is how I would modulate the 38Khz every 10ms, but maybe that's not how it works.

/**
 * @file Sample app to demonstrate PWM.
 */

#include <zephyr/kernel.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));

#define ir_rate PWM_HZ(4) //value must be >=4 ???

int main(void)
{
	uint32_t period;
	int ret;

	printk("PWM-based LED Control\n");

	if (!pwm_is_ready_dt(&pwm_led0)) {
		printk("Error: PWM device %s is not ready\n",
		       pwm_led0.dev->name);
		return 0;
	}

	period = ir_rate;
	while (1) {
		ret = pwm_set_dt(&pwm_led0, period, period / 2U); //don't understand duty cycle setting???

		if (ret) {
			printk("Error %d: failed to set pulse width\n", ret);
			return 0;
		}

		k_msleep(10000); //don't understand how k_msleep works here. Doesn't seem to affect anything???
	}
	return 0;
}

Parents
  • Hello,

    I suggest to try and fail a bit, and observe what you measure on the output to understand how this works. In essence the pulse period is the total duration of an ON and OFF state for the PWM signal. The pulse width is the duration of the ON state for the PWM signal. So that way you can control both the frequency and the duty cycle. The k_msleep() just put the CPU in sleep until the CPU will continue to run the code, e.g. if you want to periodically update the PWM signal you can set the sleep to 600us if you want.

    pwm_set_dt(const struct pwm_dt_spec *spec, uint32_t period, uint32_t pulse)

    Kenneth

Reply
  • Hello,

    I suggest to try and fail a bit, and observe what you measure on the output to understand how this works. In essence the pulse period is the total duration of an ON and OFF state for the PWM signal. The pulse width is the duration of the ON state for the PWM signal. So that way you can control both the frequency and the duty cycle. The k_msleep() just put the CPU in sleep until the CPU will continue to run the code, e.g. if you want to periodically update the PWM signal you can set the sleep to 600us if you want.

    pwm_set_dt(const struct pwm_dt_spec *spec, uint32_t period, uint32_t pulse)

    Kenneth

Children
  • Hi Kenneth, Thank you for your reply. After waiting for 3 days I was hoping for more than "try and fail a bit". This isn't a hobby for me. I am trying to standardize all our engineering solutions on Nordic processors, but the ramp up time has prevented making any meaningful progress for the last couple of years so we keep reverting to what we know. I am trying to change that in 2024. I count on DevZone Support to help get us where we want to be in a reasonable amount of time. I understand the theory of PWM. I've been implementing it effortlessly on PIC, Arduino, ESP32 for years... My problem is with implementing it with Nordic/Zephyr.

    Here are my specific questions:

    #define ir_rate PWM_HZ(1) why does the value have to be greater than 4 in the example?

    #define ir_rate PWM_KHZ(38); please confirm this is the correct setting for 38KHz?

    pwm_set_dt(&pwm_led0, period, period / 2U); what value would I use here for 50% duty cycle? Does "period/2U" set a 50% duty cycle? I'm guessing that it does...?

    I probably need to do more research into exactly what k_msleep does, but what I have done, even a value as long as 10000ms, doesn't pause the LED flashing at all. I expected it to interrupt the flashing for some period of time, but it doesn't seem to.

  • The period and pulse parameters are in nanoseconds, so the PWM_HZ() macro bascially convert Hz to nanoseconds. Since the register is 32bit, the max value is 4294967296ns, which is 4.3seconds. So that may explain the limit of 4. Also see the documentation on the api:

    /**
     * @name PWM period set helpers
     * The period cell in the PWM specifier needs to be provided in nanoseconds.
     * However, in some applications it is more convenient to use another scale.
     * @{
     */
    
    /** Specify PWM period in nanoseconds */
    #define PWM_NSEC(x)	(x)
    /** Specify PWM period in microseconds */
    #define PWM_USEC(x)	(PWM_NSEC(x) * 1000UL)
    /** Specify PWM period in milliseconds */
    #define PWM_MSEC(x)	(PWM_USEC(x) * 1000UL)
    /** Specify PWM period in seconds */
    #define PWM_SEC(x)	(PWM_MSEC(x) * 1000UL)
    /** Specify PWM frequency in hertz */
    #define PWM_HZ(x)	(PWM_SEC(1UL) / (x))
    /** Specify PWM frequency in kilohertz */
    #define PWM_KHZ(x)	(PWM_HZ((x) * 1000UL))

    /**
     * @brief Set the period and pulse width in nanoseconds from a struct
     *        pwm_dt_spec (with custom period).
     *
     * This is equivalent to:
     *
     *     pwm_set(spec->dev, spec->channel, period, pulse, spec->flags)
     *
     * The period specified in @p spec is ignored. This API call can be used when
     * the period specified in Devicetree needs to be changed at runtime.
     *
     * @param[in] spec PWM specification from devicetree.
     * @param period Period (in nanoseconds) set to the PWM.
     * @param pulse Pulse width (in nanoseconds) set to the PWM.
     *
     * @return A value from pwm_set().
     *
     * @see pwm_set_pulse_dt()
     */
    static inline int pwm_set_dt(const struct pwm_dt_spec *spec, uint32_t period,
    			     uint32_t pulse)
    {
    	return pwm_set(spec->dev, spec->channel, period, pulse, spec->flags);
    }

    Setting pulse half of period should give 50% duty cycle yes.

    The PWM peripheral will run independent of the CPU, so whether the CPU run some arbitrary code or go to sleep, this will not affect the PWM in any way. It's really only pwm_set_dt() that will affect the PWM output.

    Kenneth

  • This might not be relevant, but 4 is a suspicious number when used in the nRF52 PWM context. Some parameters are used as (n-1) and so 4 may be used as 3; I'd check the library calls for you but I don't have nRFConnect loaded. Here is the relevant register for COUNTERTOP:

    Field       Value     Description
    COUNTERTOP [3..32767] Value up to which the pulse generator counter counts.

    Note that it cannot take a value lower than 3 (Hmm 4-1 ..). The reason - we think - is that COUNTERTOP is loaded via DMA every complete PWM cycle, even when not changing, and there is a bandwidth limitation on the AHB bus (speculation).

    Anyway, worth testing just in case; using a value of 4 set a breakpoint just after the initialisation of the PWM and examine the PWM COUNTERTOP register to see what value it is; if less than 3 then the PWM will not work. This exact same issue cost us some time a while back.

    (The 32-bit explanation doesn't hold water as of course 3 seconds in that nanosecond context is less than 32-bit.)

  • Kenneth Thanks! I hadn't considered that the "PWM peripheral will run independent of the CPU". That's going to be very useful. Sounds like I chose an unfortunate value to test with... more realistic values should be fine:

    1. #define ir_rate PWM_KHZ(38)
    2. ret = pwm_set_dt(&pwm_led0, period, period / 2U); //50% duty cycle
    3. k_yield(); //PWM peripheral will run independent of the CPU
  •    Thanks for your reply! I tried some additional variations of PWM_SEC() to get a 1s flash rate, but still couldn't get it to work correctly. And, "4" keep popping up as a limit...? It's beyond me... My real goal was to PWM at 38KHz. That seems like it should work without issue. I don't have a scope here, but I trust that it's working.

Related