
CPU Core → [Timer ISR] → GPIO Pin↓ ↑ ↓[Bit Counter] ← [Shift Register] → [Serial Out]↓ ↑ ↓[State Machine] ← [Bit Timer] → [Start/Stop Bits]↓ ↑ ↓[Receive Buffer] ← [Sample Points] → [Transmit Buffer]Timer Interval = 1/(Baud Rate × Oversampling Factor)Each ISR: Sample input, shift output, update counters
When your embedded application needs more serial ports than the available hardware UARTs, or you’re working with a microcontroller that lacks sufficient UART peripherals, software UART (also known as bit-banging UART) provides a flexible solution. By using GPIO pins and timer interrupts, you can implement serial communication entirely in software.
This approach is particularly useful in scenarios like:
Software UART relies on precise timing to serialize and deserialize data bits. The fundamental operations include:
The key insight is that UART communication is synchronous at the bit level but asynchronous between devices - each device uses its own clock to sample/transmit bits at the agreed-upon baud rate.
The foundation of reliable software UART is accurate timing. Critical parameters include:
Baud Rate: Bits per second (e.g., 9600, 115200) Bit Time: 1 / Baud Rate (e.g., 104.167µs at 9600 baud) Oversampling Factor: Typically 8x, 16x, or 32x for noise immunity Timer Period: Bit Time / Oversampling Factor
For example, at 115200 baud with 16x oversampling:
The transmit process follows this sequence in each timer ISR:
1. If in start bit state: drive GPIO low, decrement bit counter2. If in data bit state: output LSB of shift register, shift right, decrement counter3. If in parity state: output calculated parity bit, decrement counter4. If in stop bit state: drive GPIO high, decrement counter5. If all bits transmitted: return to idle state
Key optimization techniques:
Receiving requires sampling the GPIO pin at the optimal point within each bit period:
1. Detect falling edge (start bit)2. Wait half a bit time to reach bit center3. Sample GPIO pin every N timer ticks (where N = oversampling factor)4. Use majority voting over multiple samples for noise immunity5. Assemble received bits into bytes6. Validate stop bit(s) and transfer data to receive buffer
The receive state machine needs to handle:
Software UART consumes CPU resources proportional to the baud rate and oversampling factor. The interrupt rate is:
Interrupts per second = Baud Rate × Oversampling Factor × 2 (TX + RX)
For example, full-duplex 115200 baud with 16x oversampling requires:
This represents significant CPU load, especially on lower-frequency MCUs. Mitigation strategies include:
Here’s a simplified structure for STM32F4:
// Timer configuration (TIM2 for 1.844 MHz at 115200 baud, 16x oversampling)void UART_Init(void) {// Configure GPIO pins: PA2 (TX), PA3 (RX) as alternate function// Configure TIM2: ARR = 0, PSC = (SystemCoreClock/2/1843200)-1// Enable TIM2 interrupt}// Timer ISR - called every 0.5425µsvoid TIM2_IRQHandler(void) {if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {TIM_ClearITPendingBit(TIM2, TIM_IT_Update);// Handle TX state machineUartTxIsr();// Handle RX state machineUartRxIsr();}}// Transmit ISR (called every timer tick)void UartTxIsr(void) {static uint8_t tx_bit_counter = 0;static uint16_t tx_shift_reg = 0;switch (tx_state) {case UART_TX_IDLE:if (tx_buffer_has_data()) {tx_shift_reg = tx_buffer_pop() | 0xFF00; // Add stop bittx_state = UART_TX_START;tx_bit_counter = 0;GPIO_WriteBit(UART_TX_PORT, UART_TX_PIN, Bit_RESET); // Start bit}break;case UART_TX_START:if (++tx_bit_counter >= OVERSAMPLING) {tx_bit_counter = 0;tx_state = UART_TX_DATA;}break;case UART_TX_DATA:if (tx_bit_counter >= OVERSAMPLING) {tx_bit_counter = 0;if (tx_shift_reg & 0x01) {GPIO_WriteBit(UART_TX_PORT, UART_TX_PIN, Bit_SET);} else {GPIO_WriteBit(UART_TX_PORT, UART_TX_PIN, Bit_RESET);}tx_shift_reg >>= 1;if (++tx_data_bit_count >= 8) {tx_state = UART_TX_STOP;}}break;// ... handle stop bit state similarly}}
Several techniques can improve software UART performance and reliability:
1. Interrupt Prioritization
2. Efficient ISR Design
3. Double Buffering
4. GPIO Optimization
5. Adaptive Oversampling
Pitfall 1: Timer Jitter Problem: Variable ISR latency causes bit timing errors Solution: Use highest interrupt priority, minimize ISR work, consider using DMA trigger
Pitfall 2: GPIO Delay Variations Problem: Different GPIO pins have different output delays Solution: Calibrate per-pin delays, use same port for TX/RX when possible
Pitfall 3: Noise on RX Line Problem: False start bit detection or bit errors Solution: Implement debouncing, use majority voting, add hardware RC filter
Pitfall 4: Buffer Overrun Problem: RX data overwritten before application reads it Solution: Implement buffer depth checking, use flow control (RTS/CTS), increase buffer size
Pitfall 5: Priority Inversion Problem: Low-priority task holds resource needed by UART ISR Solution: Use priority inheritance protocols, keep ISR non-blocking
Choose software UART when:
Stick with hardware UART when:
Software UART using GPIO and timer interrupts provides a versatile solution for adding serial communication capabilities to embedded systems. While it consumes more CPU resources than hardware UART, proper implementation can achieve reliable communication at practical baud rates.
Key takeaways for successful implementation:
With careful design and testing, software UART can be a dependable tool in your embedded development toolkit, especially when hardware resources are constrained.
portable/GCC/ARM_CM3/port.c — vPortSetupTimerInterrupt() configurationQuick Links
Legal Stuff




