PWM Interrupts / Interrupt Priority Levels

Zephyr 3.2.99 - nRF Connect 2.2.0

We have a 4 digit 7 segment display that we use on one of our products. Instead of using something like timers to keep the display updated we designed it to use PWM interrupts instead. So every time an interrupt happens we increment the current digit and set the PWM pulse cycles / brightness. The PWM is inverted and it's set to use a 4ms period. So, each segment can have a min brightness of 4ms - 100us (3,999,900ns) and a max brightness of 160us (160,000ns). We never use full brightness (0ns) or no brightness (4,000,000ns) because the interrupts would get disabled and cause the display to no longer work. This has been working great, but we noticed there was a "bleed" on the display (some of the segments are partially lit when they shouldn't be). After looking into it we found that the interrupt callback timing was not consistent. The callback gets fired usually around 130us to a little over 200us (normally around 150us). This is causing the digit switch to be delayed, which partially lights a segment up. This all goes back to the max brightness; the lower the value the more it bleeds and the higher the value the more stable it is (but at the sacrifice of a dimmer display). Here is a visual example of what we are seeing:

"Set Digit" (the top reading) happens as soon as the interrupt callback is fired. The "Display Seg A" is a wire hooked to the capacitor that the segment A LED uses.

Here is what it should look like and has no bleeding (you can see it fires before the period starts giving it enough time to switch digits properly):

I guess I have three questions.

1) Is there any way to make the callback a little more consistent, or can the variable 130us - 200us be expected? I know there's always going to be variable timings, but it would be nice to minimize the range if possible. My goal is to find the optimal max brightness value that causes the least bleeding.

2) Is there any way to shorten the time it takes for the callback to be fired? The quicker it gets called the brighter we can make the display. I don't think it will make a drastic difference, but every little bit helps.

3) One thing we tried was to increase the priority of PWM1 and PWM2. By default everything has a priority of 1 (NRF_DEFAULT_IRQ_PRIORITY), so we change this to 3 and made the PWM1 and PWM2 have a priority of 2. This didn't help. However, it does bring up a question about the interrupt priorities. According to the documentation (https://infocenter.nordicsemi.com/index.jsp?topic=%2Fsds_s140%2FSDS%2Fs1xx%2Fprocessor_avail_interrupt_latency%2Fexception_mgmt_sd.html) it states that priority level 1 is reserved by the SoftDevice. So, is it OK to use priority level 1 (like NRF_DEFAULT_IRQ_PRIORITY) or should we stick to using 2 & 3? We want the display to have the highest priority over the other devices.

Here is a quick mock-up of something similar to what we're doing (see configure_pwm and pwm_callback):

struct pwm_nrfx_config {
    nrfx_pwm_t pwm;
    nrfx_pwm_config_t initial_config;
    nrf_pwm_sequence_t seq;
    #ifdef CONFIG_PINCTRL
        const struct pinctrl_dev_config *pcfg;
    #endif
};

static bool configure_digits(void);
static bool configure_pwm(void);
static void pwm_callback(nrfx_pwm_evt_type_t event_type, void *context);

static uint32_t pwm_period;
static uint32_t min_brightness;
static uint32_t max_brightness;
static struct k_work update_display_work;

static const struct gpio_dt_spec display_d1 = GPIO_DT_SPEC_GET(DT_ALIAS(display_d1), gpios); // Digit 1
static const struct gpio_dt_spec display_d2 = GPIO_DT_SPEC_GET(DT_ALIAS(display_d2), gpios); // Digit 2
static const struct gpio_dt_spec display_d3 = GPIO_DT_SPEC_GET(DT_ALIAS(display_d3), gpios); // Digit 3
static const struct gpio_dt_spec display_d4 = GPIO_DT_SPEC_GET(DT_ALIAS(display_d4), gpios); // Digit 4

static const struct pwm_dt_spec display_a = PWM_DT_SPEC_GET(DT_ALIAS(display_a));   // Segment A - PWM1
static const struct pwm_dt_spec display_b = PWM_DT_SPEC_GET(DT_ALIAS(display_b));   // Segment B - PWM1
static const struct pwm_dt_spec display_c = PWM_DT_SPEC_GET(DT_ALIAS(display_c));   // Segment C - PWM1
static const struct pwm_dt_spec display_d = PWM_DT_SPEC_GET(DT_ALIAS(display_d));   // Segment D - PWM1
static const struct pwm_dt_spec display_e = PWM_DT_SPEC_GET(DT_ALIAS(display_e));   // Segment E - PWM2
static const struct pwm_dt_spec display_f = PWM_DT_SPEC_GET(DT_ALIAS(display_f));   // Segment F - PWM2
static const struct pwm_dt_spec display_g = PWM_DT_SPEC_GET(DT_ALIAS(display_g));   // Segment G - PWM2
static const struct pwm_dt_spec display_dp = PWM_DT_SPEC_GET(DT_ALIAS(display_dp)); // Segment DP - PWM2

void main(void)
{
    __ASSERT_NO_MSG(configure_pwm());

    pwm_period = display_a.period;      // 4ms
    min_brightness = pwm_period - 100U; // 3.9ms
    max_brightness = 160000U;           // 160us
    // ...

    // Initialize and kick off an update to get the interrupts going
    k_work_init(&update_display_work, update_display);
    k_work_submit(&update_display_work);
}

static bool configure_pwm(void)
{
    const struct device *pwm1_device = DEVICE_DT_GET(DT_NODELABEL(pwm1));
    const struct device *pwm2_device = DEVICE_DT_GET(DT_NODELABEL(pwm2));
    const struct pwm_nrfx_config *config = !pwm2_device ? NULL : pwm2_device->config;

    if (!device_is_ready(pwm1_device) || !device_is_ready(pwm2_device) || config == NULL)
    {
        LOG_DISPLAY_E("[Critical] The PWM device(s) are not ready.");
        return false;
    }

    nrfx_pwm_uninit(&config->pwm);
    IRQ_DIRECT_CONNECT(PWM2_IRQn, 0, nrfx_pwm_2_irq_handler, 0);
    return nrfx_pwm_init(&config->pwm, &config->initial_config, &pwm_callback, NULL) == NRFX_SUCCESS;
}

static void pwm_callback(nrfx_pwm_evt_type_t event_type, void *context)
{
    // [Enable "Set Digit" Trace Pin]
    // gpio_pin_set(current digit)
    // [Disable "Set Digit" Trace Pin]

    // Setting the PWM cycles is slow (>200us), offload it so it's doesn't hold up the interrupt
    k_work_submit(&update_display_work);
}

static void update_display(struct k_work *item)
{
    // Compute segment brightness, set PWM pulse cycles, etc.
}

Appreciate any help.

Parents
  • Are you sure you cannot do this in hardware instead of software?

    The PWM peripheral is pretty advanced (https://infocenter.nordicsemi.com/topic/ps_nrf52840/pwm.html?cp=5_0_0_5_16).

    You can set up sequences in RAM that it follows. To synchronise timings, you can use PPI for starting all PWMs at the exact correct time, maybe in a combination with a TIMER peripheral.

  • Thank you for the response, Emil.

    I looked into this and the NRFX PWM driver, in Zephyr, is already doing this behind the scenes. When you call pwm_set_pulse_dt, pwm_set_dt, pwm_set, etc. it will setup the period and pulse cycles and then call pwm_set_cycles, which in turn calls Nordics NRFX driver's pwm_nrfx_set_cycles function. In there it updates the sequence value stored in RAM for that channel and then calls nrfx_pwm_simple_playback to play the sequence back once.

    I also looked into the GPIOTE in order to do the digit switching. One thing I found out is that it would be required to disable the Zephyr GPIO implementation since you can't have Zephyr's GPIO and NRFX's GPIOTE enabled at the same time. That would mean I would have to change all my code that uses the Zephyr GPIO implementation over to use NRF GPIO implementation. This would also mean that I would have to rewrite any Zephyr drivers that use the Zephyr GPIO, like the EEPROM driver as an example. I don't feel like rewriting those, nor do I think the company wants me to rewrite a lot of things I have already written.

    Going back to the NRFX PWM, I noticed that when pwm_set_cycles gets called it's updating the sequence value and calling nrfx_pwm_simple_playback for every channel... In our case, for the 4-digit 7-segment display, that's 8 times per update. That's highly inefficient and was causing the slowdown we were seeing. I turned off Zephyr's implementation of PWM and rewrote the driver to update all 8 values in ram at once and then only call the nrfx_pwm_simple_playback once. So, to answer your question, I'm sure it can be done in hardware if we disable a lot of Zephyr's functionality. 

    Do you see any problems with this or know a better way that will work with Zephyr?

     

       

  • I have not used Zephyr myself, so cannot really comment on this. It seems strange if you want to use the GPIOTE/PWM manually, you will be locked out to use Zephyr for the rest of the peripherals though...

  • Zephyr registers it's own IRQ handler for the GPIO and GPIOTE also tries to register it's own IRQ handler for the GPIO. If you leave the Zephyr GPIO enabled and then try to enable NRFX GPIO you get the following:

    FAILED: zephyr/isr_tables.c zephyr/isrList.bin
    multiple registrations at table_index 6 for irq 6 (0x6)
    Existing handler 0x6d09, new handler 0x6d09
    Has IRQ_CONNECT or IRQ_DIRECT_CONNECT accidentally been invoked on the same irq multiple times?

    I tried this using the sample from /boards/nrf/nrfx

    The readme even states: "Zephyr GPIO driver is disabled to prevent it from registering its own handler for the GPIOTE interrupt"

  • Hi James,

    My apologies for late reply. I am not trying to ghost you, but this ticket went into idle due my mistake of seeing that there is ongoing discussion which is similar to my thoughts and the status of ticket wrongly showing the tag that made me assume that the last response was not from the author of the ticket (you).

    In Zephyr we have an vendor specific interface layer which connects the Zephyr API to the vendor HAL. In this case of GPIO that interface is zephyr\drivers\gpio\gpio_nrfx.c. As you can see When enabling GPIO config in Zephyr, this file is pulled in and the IRQ is already connected here.

    JamesDougherty said:
    I also looked into the GPIOTE in order to do the digit switching. One thing I found out is that it would be required to disable the Zephyr GPIO implementation since you can't have Zephyr's GPIO and NRFX's GPIOTE enabled at the same time.

    This seems like an architectural limitation and very specific to GPIO and GPIOTE since the interrupt and the interrupt handler is the same for them.

    I do not think that any other peripheral would have such limitation which does not share the same interrupt line. Since GPIO and GPIOTE have same interrupt, it is understandable that nrf_gpiote.x->ISR is plugged in either by NRF_GPIO through zephyr or using nrfx_gpiote directly.

    If you want to use GPIOTE/PWM,  would it not be possible to use Zephyr GPIO API instead of using directly nrfx_gpiote?

  • Hey Susheel,

    Thank you for getting back with me. No worries, you're one of the few that have always been helpful and knowledgeable. Thank you for that.

    A few months ago I took a deep look into overall timings and everything is much better, so I have not seen this display issue in a while. However, I still know it's not 100% where it needs to be either. If I can get everything working on the hardware level, then that would be ideal. This afternoon, or early Monday, I'll look into this again.

    I'm thinking if I could setup a PWM sequence in RAM that just continuously plays non-stop and then use PPI to switch the digits really fast, then it should work and be all hardware level. Of course I would have to update the sequence on the fly to account for blinking, fading, etc. as well. Do you have an example that would achieve something similar like this using Zephyr? Maybe using PWM to power LEDs and using PPI to switch between the LEDs?

    Appreciate it Susheel.

  • Hey Susheel,

    I put some thought into this and I do not think it will work with Zephyr's GPIO API. Let me explain in a high-level overview of what I would like to ideally achieve.

    I currently have 4 GPIO pins configured, one for each digit on the display. When I want a certain digit to display something, I enable that GPIO pin and disable the other 3 pins. So, I’ll enable digit 1 and disable 2, 3, and 4. Then, I’ll enable 2 and disable 1, 3, and 4. I do this to all 4 digits and then repeat back at 1. I also have all 4 channels of PWM1 and all 4 channels of PWM2 dedicated for the LED segments on the display. I play the same sequence for both PWM1 and PWM2 so the LEDs have the same brightness (the brightness is user defined, but we also blink and fade the display at certain times as well).

    For switching the digits, I call nrfx_pwm_init and configure a callback for PWM2. When I play the sequence for both PWM1 and PWM2 I get notified when PWM2 has finished playing the sequence (NRFX_PWM_EVT_FINISHED). From there, I increment the display digit from 1 to 2 and replay the sequence. Then when that sequence finishes I increment the display digit again and replay the sequence over. I keep doing this and wrapping the display digit (1, 2, 3, 4, 1, 2, 3, 4, 1, 2, …) forever. So, I’m relying on the event finished to switch display digits.

    With all of that being said, is it possible to wire up the GPIO pins that are used for the display digits to automatically increment when then the NRFX_PWM_EVT_FINISHED event fires on PWM2? If so, do you happen to know of any example or suggestions on how to achieve this?

    Appreciate it.

Reply
  • Hey Susheel,

    I put some thought into this and I do not think it will work with Zephyr's GPIO API. Let me explain in a high-level overview of what I would like to ideally achieve.

    I currently have 4 GPIO pins configured, one for each digit on the display. When I want a certain digit to display something, I enable that GPIO pin and disable the other 3 pins. So, I’ll enable digit 1 and disable 2, 3, and 4. Then, I’ll enable 2 and disable 1, 3, and 4. I do this to all 4 digits and then repeat back at 1. I also have all 4 channels of PWM1 and all 4 channels of PWM2 dedicated for the LED segments on the display. I play the same sequence for both PWM1 and PWM2 so the LEDs have the same brightness (the brightness is user defined, but we also blink and fade the display at certain times as well).

    For switching the digits, I call nrfx_pwm_init and configure a callback for PWM2. When I play the sequence for both PWM1 and PWM2 I get notified when PWM2 has finished playing the sequence (NRFX_PWM_EVT_FINISHED). From there, I increment the display digit from 1 to 2 and replay the sequence. Then when that sequence finishes I increment the display digit again and replay the sequence over. I keep doing this and wrapping the display digit (1, 2, 3, 4, 1, 2, 3, 4, 1, 2, …) forever. So, I’m relying on the event finished to switch display digits.

    With all of that being said, is it possible to wire up the GPIO pins that are used for the display digits to automatically increment when then the NRFX_PWM_EVT_FINISHED event fires on PWM2? If so, do you happen to know of any example or suggestions on how to achieve this?

    Appreciate it.

Children
  • How about using PWM3 to drive the 4 digit select pins? Hardware solution, no delays

    Edit:

    Second tip, when an interrupt is not available, such as when a PWM interrupt is hogged by the irritating OS, try using PPI to trigger a spare EGU interrupt from the PWM hardware event.

    Moving a slow piece of code out of an interrupt which has to be kept very fast can also be done by not setting a flag or starting a task but by simply triggering a software interrupt using a spare EGU channel at a lower priority, which also generates a wakeup to handle the software interrupt.

  • I think it is possible to use PWM3 to drive the GPIO pins to drive the 4 digits. Ill ask the PWM expert on how to synchronize them. That is PWM3's next sequence should start after we have NRFX_PWM_EVT_FINISHED from the PWM2. 

    I am guessing the second tip from Hugh would behave the same as in the callback from the event NRFX_PWM_EVT_FINISHED (just less interrupt post processing overhead when using EGU). Will make it little faster but I am not sure if it makes huge difference in terms of power consumption.

  • Hi 

    Susheel is currently unavailable, and I will help out in the mean time. 

    I agree that using PWM3 to drive the digit select pins make a lot of sense. 

    As mentioned earlier it is possible to synchronize all the PWM modules by using the EGU and PPI controller, allowing you to start them all in parallel. Doing this you can prepare separate buffers for each PWM controller and have them played back in sync. 

    Then you should be able to define at minimum a four element buffer for each PWM, setting one digit select pin high for each value, and ensuring that PWM1 and PWM2 get set to the right values for each element depending on the output of PWM3. 

    Then all you have to do is to update the buffers assigned to PWM1 and PWM2 when you want to change the display state. 

    I am pretty sure you will need to bypass the Zephyr driver and use the nrfx_pwm driver directly to do this though, as the Zephyr driver has no concept of Nordic specific concepts such as tasks and events. 

    Best regards
    Torbjørn

  • Thank you all, that's a great idea! I will look into it more this week and get back to you.

  • Good luck! If you have more questions just let us know Slight smile

Related