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
  • Hi

    Are you using the SoftDevice controller (radio/BLE) in your application? I'm not able to see any calls taking use of the SoftDevice controller in your application, and if you're not using the radio, you should be free to set the PWM to whatever priority you'd like to avoid this. If you are, let me know and I'll look into some countermeasures for you.

    Best regards,

    Simon

Reply
  • Hi

    Are you using the SoftDevice controller (radio/BLE) in your application? I'm not able to see any calls taking use of the SoftDevice controller in your application, and if you're not using the radio, you should be free to set the PWM to whatever priority you'd like to avoid this. If you are, let me know and I'll look into some countermeasures for you.

    Best regards,

    Simon

Children
  • Hello Simonr,

    Thank you for the quick response. Yes, this is for a Bluetooth product, so we will be relying on the Bluetooth controller. We are redesigning our current product that uses a Microchip CPU and a Fanstel BLE radio module. The new product will use the nRF52840 for both of those purposes.

    Thank you,

    James

  • There are limitation that the Softdevice Controller puts on application are inherited from the MPSL. The integration notes of the MSPL gives a very clear documentation of what peripherals and priorities that are allowed for the application to use. Please go through the integration notes I linked to see if it is clear about what priority you cannot use for your PWM interrupt when BLE is enabled (and hence MPSL is initialized).

  • Hello Susheel.

    Thank you for the information on the interrupts, I didn't see that document. I updated the board definition to make sure those peripherals have the correct priorities.

    I was researching this some more and found out that it was a timing issues caused by the ADC (it appears the ADC was throwing everything off). I had the ADC configured to sample using PPI / Timer sampling 3 channels at the same time every 500us and it's setup like:

    static bool adc_configure(void)
    {
        const nrfx_saadc_adv_config_t adv_config = {
            .oversampling = NRF_SAADC_OVERSAMPLE_4X,
            .burst = NRF_SAADC_BURST_ENABLED,
            .internal_timer_cc = 0,
            .start_on_end = false,
        };
    
        //... Setup channels, calibrate, etc.
    
        return true;
    }
    
    static void event_handler(nrfx_saadc_evt_t const *event)
    {
        switch (event->type)
        {
            case NRFX_SAADC_EVT_DONE:
                // ... Process Samples
                nrf_saadc_task_trigger(NRF_SAADC, NRF_SAADC_TASK_START);
                break;
    
            default:
                break;
        }
    }

    However, this was causing erratic behavior:

    If I change the code to the following:

    static const bool adc_configure(void)
    {
        const nrfx_saadc_adv_config_t adv_config = {
            .oversampling = NRF_SAADC_OVERSAMPLE_4X,
            .burst = NRF_SAADC_BURST_ENABLED,
            .internal_timer_cc = 0,
            .start_on_end = true, // <---------- Change to true
        };
    
        //... Setup channels, calibrate, etc.
    
        return true;
    }
    
    static void event_handler(nrfx_saadc_evt_t const *event)
    {
        switch (event->type)
        {
            case NRFX_SAADC_EVT_DONE:
                // ... Process Samples
                
                // Do not trigger start task
                // nrf_saadc_task_trigger(NRF_SAADC, NRF_SAADC_TASK_START);
                break;
    
            default:
                break;
        }
    }

    Then I get consistent / expected behavior:

    I still don't get the PWM callback for ~150us, however, it has not gone over the threshold and caused the other segments to bleed either. I'm going to cleanup my code and flesh out the callbacks and see how everything runs.

Related