GPIO, ADC and FlexPWM interrupts all reach the Cortex-M7 through the same NVIC, all get a priority from 0 to 15, and all look interchangeable in the config tool.
With FreeRTOS they are not. FreeRTOS draws a hard line through that priority range, and the side an interrupt lands on decides whether its handler may call a FreeRTOS API at all. Get it wrong and nothing fails at start-up. You get an assert under load, or silent scheduler corruption months later.
So the useful question is not “how do I set up a GPIO interrupt”. It is “what is in my NVIC table, and is every entry on the right side of the line”.
In this article I show the setup pattern the three sources share, the two places where they really differ, and how to audit an NVIC table against the FreeRTOS barrier.
The barrier
The mental model: a lower number means more urgent, and more urgent means less allowed.
The i.MX RT1062 uses 4 priority bits, so every interrupt has a priority from 0 (most urgent)
to 15 (least urgent). FreeRTOS splits that range at
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY in FreeRTOSConfig.h:
- Numerically below it (more urgent): the interrupt is never masked by the kernel, so its
handler must not call any FreeRTOS API, not even the
...FromISRones. - At or above it (less urgent): the handler may call
xTaskNotifyFromISR(),xQueueSendFromISR()and the other...FromISRfunctions.
The kernel’s own interrupts, SysTick and PendSV, run at the lowest priority.
Priority 0 is the default
NVIC_SetPriority() first is therefore not allowed to
call FreeRTOS, even if its handler does. With configASSERT() defined, FreeRTOS catches this
at run time; without it, it is silent.A handler above the barrier can still hand data to a task: write a volatile variable,
add a memory barrier, and let a task or a lower-priority interrupt pick it up.
The pattern all three share
Every peripheral pin on the RT1062 goes through two layers before an interrupt can fire:
- IOMUXC selects the pin function and its electrical pad settings (pull, drive strength, slew rate, hysteresis).
- The peripheral clock gate must be on before you touch any peripheral register.
The config tool generates both into pins.c and clock_config.c. By hand, for a GPIO input
with a pull-up and hysteresis:
/* GPIO1_IO04 as a GPIO input with interrupt */
IOMUXC_SetPinMux(IOMUXC_GPIO_AD_B0_04_GPIO1_IO04, 0U);
IOMUXC_SetPinConfig(IOMUXC_GPIO_AD_B0_04_GPIO1_IO04,
IOMUXC_SW_PAD_CTL_PAD_HYS(1) | /* hysteresis for a slow or noisy edge */
IOMUXC_SW_PAD_CTL_PAD_PUS(1) | /* 47k pull-up */
IOMUXC_SW_PAD_CTL_PAD_PUE(1) |
IOMUXC_SW_PAD_CTL_PAD_PKE(1));From there the sequence is the same for all three peripherals:
- Enable the peripheral clock.
- Configure the peripheral.
- Clear the pending flag, or the first interrupt you take is a stale one.
- Enable the interrupt inside the peripheral.
- Set the NVIC priority, before
EnableIRQ(). EnableIRQ().
GPIO
Edge and level detection live inside the GPIO block. Each pin can trigger on a rising edge, a falling edge, both edges, a high level or a low level.
CLOCK_EnableClock(kCLOCK_Gpio1);
gpio_pin_config_t cfg = { kGPIO_DigitalInput, 0, kGPIO_IntRisingEdge };
GPIO_PinInit(GPIO1, 4U, &cfg);
GPIO_ClearPinsInterruptFlags(GPIO1, 1U << 4U); /* before enabling */
GPIO_EnableInterrupts(GPIO1, 1U << 4U);
NVIC_SetPriority(GPIO1_Combined_0_15_IRQn, 6U); /* at or above the barrier */
EnableIRQ(GPIO1_Combined_0_15_IRQn);void GPIO1_Combined_0_15_IRQHandler(void)
{
uint32_t flags = GPIO_GetPinsInterruptFlags(GPIO1);
if (flags & (1U << 4U))
{
GPIO_ClearPinsInterruptFlags(GPIO1, 1U << 4U);
BaseType_t woken = pdFALSE;
xTaskNotifyFromISR(xTargetTask, 0, eNoAction, &woken);
portYIELD_FROM_ISR(woken);
}
}One vector, many pins
GPIO1_Combined_0_15_IRQn. The handler must read the flag register, work out which pin
fired, and clear only that pin’s flag. Clearing all of them loses another pin’s edge.ADC
The RT1062 has two 12-bit SAR ADCs. The conversion-complete interrupt fires when the result is ready, and the flag clears when you read the result. There is no separate clear.
CLOCK_EnableClock(kCLOCK_Adc1);
adc_config_t adcCfg;
ADC_GetDefaultConfig(&adcCfg);
adcCfg.resolution = kADC_Resolution12Bit;
ADC_Init(ADC1, &adcCfg);
ADC_DoAutoCalibration(ADC1); /* calibrate after reset */
adc_channel_config_t chCfg = {
.channelNumber = 0U,
.enableInterruptOnConversionCompleted = true,
};
ADC_SetChannelConfig(ADC1, 0U, &chCfg); /* also starts the first conversion */
NVIC_SetPriority(ADC1_IRQn, 8U);
EnableIRQ(ADC1_IRQn);void ADC1_IRQHandler(void)
{
/* Reading the result register clears the interrupt flag. */
uint32_t result = ADC_GetChannelConversionValue(ADC1, 0U);
BaseType_t woken = pdFALSE;
xQueueSendFromISR(xAdcQueue, &result, &woken);
portYIELD_FROM_ISR(woken);
}Two things to watch. Run the calibration after reset; without it the readings carry an offset. And in continuous-conversion mode the interrupt fires once per sample, so at high sample rates it takes over the CPU. Use DMA there instead.
FlexPWM
The RT1062 has four FlexPWM modules, each with four submodules. Each submodule has its own counter and compare registers, and can interrupt on a compare match or a reload. Fault inputs have a vector of their own.
CLOCK_EnableClock(kCLOCK_Pwm1);
pwm_config_t pwmCfg;
PWM_GetDefaultConfig(&pwmCfg);
pwmCfg.reloadLogic = kPWM_ReloadPwmFullCycle;
pwmCfg.prescale = kPWM_Prescale_Divide_4;
PWM_Init(PWM1, kPWM_Module_0, &pwmCfg);
pwm_signal_param_t signal = {
.pwmChannel = kPWM_PwmA,
.dutyCyclePercent = 50U,
.level = kPWM_HighTrue,
};
PWM_SetupPwm(PWM1, kPWM_Module_0, &signal, 1U, kPWM_EdgeAligned,
20000U, CLOCK_GetFreq(kCLOCK_IpgClk));
PWM_EnableInterrupts(PWM1, kPWM_Module_0, kPWM_CompareVal0InterruptEnable);
NVIC_SetPriority(PWM1_0_IRQn, 6U);
EnableIRQ(PWM1_0_IRQn);
PWM_StartTimer(PWM1, kPWM_Control_Module_0);void PWM1_0_IRQHandler(void)
{
uint32_t flags = PWM_GetStatusFlags(PWM1, kPWM_Module_0);
if (flags & kPWM_CompareVal0Flag)
{
PWM_ClearStatusFlags(PWM1, kPWM_Module_0, kPWM_CompareVal0Flag);
BaseType_t woken = pdFALSE;
xTaskNotifyFromISR(xPwmTask, PWM_COMPARE_EVENT, eSetBits, &woken);
portYIELD_FROM_ISR(woken);
}
}A fault that is still there fires again
One more FlexPWM trap: the channels of a submodule share one counter. Committing one channel to a fixed PWM frequency fixes it for the others too, including the submodule’s input-capture logic. Plan the channel allocation before the board layout.
Where they really differ
Anatomy of a handler that posts to a task
The mental model: four steps, in this order, and each one has a failure mode.
The rules that are not negotiable:
- End every posting handler with
portYIELD_FROM_ISR(woken). Without it, the task you just woke stays ready but does not run until the next tick or the next context switch: latency you did not plan for. - Never call a blocking API from an ISR, such as
vTaskDelay()or a queue send with a timeout. Only the...FromISRfunctions belong there. - Interrupts have their own stack. On a Cortex-M, exceptions run on the main stack (MSP), not on the task’s stack. Size it for the worst case of nested interrupts.
How to audit your NVIC table
Open the interrupt overview in your config tool, or list every NVIC_SetPriority() call,
and check each enabled interrupt:
- Its priority is set explicitly. None is left at the reset default of 0.
- If its handler calls any FreeRTOS API, its priority is numerically at or above
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY. - Every handler above the barrier has been read, line by line, for FreeRTOS calls, including calls hidden inside helper functions and drivers.
- The flag is cleared before the interrupt is enabled, and inside the handler.
- The handler name matches the vector table entry in the startup file. A misspelt handler silently falls back to the default handler.
The table alone cannot tell you whether an interrupt above the barrier is correct. That depends on what its handler calls, so only reading the handler can.
Summary
- The setup is the same for all three sources: clock, configure, clear the flag, enable,
set the priority, then
EnableIRQ(). - The differences are where the trigger lives and how the flag clears: GPIO and FlexPWM need an explicit clear, the ADC clears when you read the result.
- The priority is the field that matters: it is the only one where a wrong value gives no error at configuration time and an intermittent failure under load. Audit it.
Sources
- NXP, i.MX RT1060 Processor Reference Manual: GPIO, ADC, FlexPWM and IOMUXC chapters.
- Arm, Cortex-M7 Devices Generic User Guide: NVIC priorities and exception entry.
- FreeRTOS documentation: “Running the RTOS on an ARM Cortex-M core” (interrupt priorities
and
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY). - NXP MCUXpresso SDK driver documentation for
fsl_gpio,fsl_adcandfsl_pwm.
💬 Questions, corrections, or your own war story? Leave a comment below.
💬 Discussion
Questions, corrections, or war stories from your own board? Leave a comment below — I read and answer all of them. (Sign-in with GitHub.)