Situation

In my current Bachelor Thesis project I come across some really high priority and timing relevant interrupt handlers. This lead me down the path of learning how interrupt actually work on the STM32H7 platform. As a simple low-risk excercise I therefore reimplemented my UART4 transmission complete handler without using the HAL provided utilities. In this case I have a ringbuffer for stdout filled by the _write syscall, that feeds UART4 via DMA. The exact architecture in this case is not really relevant and the basic concept should apply to most other handlers.

Goal

The short and simple goal of this exercise is to implement a tiny ISR for UART4, that handles the Transfer-Complete (TC) Interrupt. Any other interrupt is not-handled as they are expected to be disabled. If you come accross any other interrupts being thrown, but not handled you might get stuck in a busy loop and I haven't come accross that yet.

Steps

The whole project is generated using CubeMX and therefore most implementations are wrapped by ST's HAL to some extend. To utilize your own handler you could simply go ahead and disable "UART4 global interrupts" checkbox, but that would also require us to arm and disarm the interrupts for uart4. Instead I prefer to specifically disable the Generate IRQ Handler setting in "System Core -> NVIC -> Code Generation" in CubeMX. This still enables the IRQ and the linker has a weak symbol of UART4_IRQHandler, that we can now override with our custom code and not conflict with the HALs implementations. In this case we only need to check if the ISR of the UART4 (Interrupt Status Register) has the Transfer Complete bit set and if, clear it to indicate that this interrupt has been handled to then implement our custom function

void UART4_IRQHandler()
{
    // ISR = Interrupt Status register
    if (UART4->ISR & USART_ISR_TC)
    {
        // ICR = Interrupt Clear register
        UART4->ICR = USART_ICR_TCCF;
        // We disable the UART transaction
        UART4->CR1 &= ~USART_CR1_TCIE;

        // Call our custom code
        stdout_uart_txcmplt(&huart4, &stdout_buf);
    }
}

Results

I measured the default HAL implementation and compared it to our custom implementation and got the following measurements. For the example instead of any actual handler I utilized the volatile int x = 1 instruction to prevent any nondeterministic behavior. I tested both in the default Debug and Release compile settings, which in my case were STs autogenerated settings without any tweaking. The cycle count was obtained by fetching the content of DWT->CYCNT at the entrance and exit of the UART4_IRQHandler method regardless if its content is by the HAL or custom.

Implementation Cycles (Debug Build) Cycles (Release Build)
HAL 580 389
Custom 116 65
Difference(cycles) 464 324
Difference(time@480MHz) 0.97us 0.675us

In Debug build this results in about a factor x5 improvement, which would equate at the maximum clockrate of 480MHz to almost a whole us. For a Tx complete interrupt this would be less significant, but other ISRs might be more relevant. Even the release build is still significantly faster. Especically accounting for the fact, that on average our ISR will have about 24 cycles overhead (12 on entry and 12 on exit) our handler is about as small as it can get (besides asm optimization now). The 24 cycles are pulled from the ST programming manual PM0253 (section 2.4.7), which is basically just a rehash of the ARM Cortex M7 user guide (section 2.3.7).

Final words

Overall this could dramatically cut down on the interrupt handler time. This custom handler obviously has no error handling or any other possible interrupts implemented. Incase I actually want other callbacks this would obviously grow, but once the code runs and you want to optimize, this seems like an easy way to drastically improve your ISRs.

Other peripherals (like DMA) might require more or different handling obviously, but in that case the PM is your friend.