Getting Started with STM32 Low Layer (LL): UART Transmit Using Interrupt

Eliminate the CPU bottlenecks caused by blocking transmission loops by shifting to an interrupt-driven UART architecture using Low Layer (LL) drivers. This guide demonstrates how to leverage the Transmit Data Register Empty (TXE) interrupt to background-stream character buffers, allowing your main application logic to run completely uninterrupted while the hardware handles the bit-clocking.

In this guide, we shall cover the following:

  • Why interrupt over polling.
  • STM32CubeMX setup.
  • Firmware development.
  • Results.

1. Why Interrupt Over Polling:

The choice between Interrupt-driven execution and Polling is one of the most fundamental architectural decisions in firmware engineering. It represents the trade-off between strict CPU dedication and asynchronous multitasking.

Here is a detailed breakdown of why interrupts are preferred over polling in professional embedded applications:

1. CPU Efficiency and Utilization
  • Polling (The Bottleneck): In a polling mode, the CPU is held hostage by a hardware flag. For example, when waiting for a UART byte to transmit at 9600 baud, a single character takes about $1\text{ ms}$. At a $168\text{ MHz}$ CPU clock speed, the processor executes roughly 168,000 completely useless instruction cycles just sitting in a while(!TXE) loop.
  • Interrupts (The Fix): With interrupts, the CPU continues executing your main application logic (calculating sensor data, updating user interfaces, running state machines). When the hardware buffer becomes empty, the hardware autonomously triggers an execution pause, hands the CPU a single byte to send, and immediately returns control to the main loop. The CPU only spends a fraction of a microsecond handling the hardware event.
2. Power Optimization
  • Polling: Because the processor must continuously check a register flag in a tight loop, it must remain running in full-power mode. This quickly drains batteries and generates unnecessary heat.
  • Interrupts: Interrupts are the gateway to low-power design. You can put the microcontroller into a deep sleep mode (SleepStop, or Standby), which completely shuts down the CPU core clock. The core consumes almost zero power until an external event or hardware peripheral trips an interrupt line, instantly waking the CPU to handle the task before it drops back to sleep.
3. Real-Time Responsiveness and Latency
  • Polling: If your main loop takes $20\text{ ms}$ to execute due to complex math operations, and a critical sensor changes state at millisecond 2, your application will not know about that change until the loop finishes its cycle $18\text{ ms}$ later. This jitter is unacceptable in safety-critical systems (like motor control or over-current protection).
  • Interrupts: The Nested Vectored Interrupt Controller (NVIC) inside the ARM Cortex core watches the hardware lines directly. The exact nanosecond a line switches state, the core halts the main program, preserves the current register stack, and handles the event within a few clock cycles, ensuring deterministic real-time response.
4. Architectural Scalability
  • Polling: Polling works well for a single, trivial task. However, as soon as you add a display, multiple communication lines (UART, SPI, $I^2C$), and a couple of user buttons, a polling architecture collapses. One slow peripheral will stall the entire system, causing dropped communication bytes and missed button presses.
  • Interrupts: By treating external inputs and peripheral buffers as asynchronous background tasks, your software architecture remains clean, scalable, and modular. It separates your time-critical hardware drivers from your high-level application logic.
Direct Comparison
FeaturePolling ModeInterrupt Mode
CPU OverheadHigh (Stuck in blocking loops)Low (Executes only on demand)
Power ConsumptionHigh (CPU must stay fully active)Low (Allows sleep/wake cycles)
Response TimeDependent on main loop execution timeInstantaneous (Hardware-triggered)
ComplexitySimple to write and debugRequires ISR management & race-condition care
Dropped Data RiskHigh if the CPU is busy elsewhereVery Low due to immediate hardware buffering

2. STM32CubeMX Setup:

We shall continue from the previous which can be found here.

From connectivity, USART2, From NVIC settings, enable USART2 global interrupt as follows:

Next, from System Core, NVIC, disable Generate IRQ handler and click Generate Code as follows:

Thats all for STM32CubeMX configuration.

3. Firmware Development:

In order to send data using interrupt, the code flow as follows:

In summery:

Phase 1: Transmit Data Register Empty (TXE) Handling

  • Step 1: Verify the Source The handler checks if the TXE hardware flag is active and if the TXE interrupt is actually enabled. This ensures the handler only processes data when the hardware buffer is truly ready to accept a new byte.
  • Step 2: Stream Data If the current buffer position (tx_index) is less than the total message length (buff_len), the next character from uart_buff is written directly to the data register using LL_USART_TransmitData8, and the index is post-incremented.
  • Step 3: Detect the Last Byte Immediately after loading the byte, the handler checks if tx_index has reached buff_len.
  • Step 4: Shift Gears to TC If it was the final byte of the buffer, the handler disables the TXE interrupt (since there is no more data to load) and enables the TC (Transmission Complete) interrupt.

Phase 2: Transmission Complete (TC) Handling

  • Step 5: Verify the Wire is Idle The handler checks if the TC flag is active and if the TC interrupt is enabled. This flag only trips after the very last byte has completely cleared the internal Shift Register and physical TX pin.
  • Step 6: Clear and Cleanup Once confirmed, the handler clears the hardware TC flag using LL_USART_ClearFlag_TC and disables the TC interrupt to prevent the handler from firing repeatedly.
  • Step 7: Flag Completion The application-level status flag Tx_Done is set to 1, signaling your main program loop that the entire string has been completely and successfully transmitted over the wire.

Next, the firmware.

In user code begin PV, declare the following variables:

uint16_t tx_index;

uint8_t Tx_Done;

Next, USART2 IRQ handler as follows:

void USART2_IRQHandler(void)
{
	if (LL_USART_IsActiveFlag_TXE(USART2) && LL_USART_IsEnabledIT_TXE(USART2))
	{
		if (tx_index < buff_len)
		{
			LL_USART_TransmitData8(USART2, uart_buff[tx_index++]);

			if (tx_index == buff_len)
			{
				LL_USART_DisableIT_TXE(USART2);
				LL_USART_EnableIT_TC(USART2);
			}
		}
	}

	if (LL_USART_IsActiveFlag_TC(USART2) && LL_USART_IsEnabledIT_TC(USART2))
	{
	        LL_USART_ClearFlag_TC(USART2);
	        LL_USART_DisableIT_TC(USART2);
	        Tx_Done = 1;
	}
}

Next, declare a function that will send buffer over UART using interrupt:

void UART_Send_IT(void)
{
	tx_index = 0;
	Tx_Done = 0;

	LL_USART_ClearFlag_TC(USART2);
	LL_USART_EnableIT_TXE(USART2);

}

The function resets the index and tx done flag then clears TC flag and enable TXE (Transfer Buffer Empty).

In user code begin 3 in while 1 loop:

buff_len=sprintf(uart_buff,"Counter Value =%d \r\n",counter++);

UART_Send_IT();
while(Tx_Done==0);
Tx_Done=0;

Create the required string.

Send the data using interrupt and wait for the transmission to end.

Thats all for the firmware.

Save, build and run the project as follows:

4. Results:

Open your favourite terminal application, set the baudrate to 115200 and you should get the following:

Congratulations, you send the string successfully using interrupt.

You may download the project from here.

Happy coding 😉

Add Comment

Your email address will not be published. Required fields are marked *