Design a simple UART driver API — what functions would you expose?
A production UART driver API should provide five core functions that separate configuration from data flow and support both blocking and non-blocking operation:
typedef struct {uint32_t baudrate;uint8_t word_length; // 8 or 9uint8_t parity; // NONE, EVEN, ODDuint8_t stop_bits; // 1 or 2} uart_config_t;int uart_init(uint8_t port, const uart_config_t *cfg);int uart_send(uint8_t port, const uint8_t *data, uint16_t len);void uart_register_rx_callback(uint8_t port,void (*cb)(const uint8_t *data, uint16_t len));int uart_get_error(uint8_t port);void uart_deinit(uint8_t port);
uart_init() configures the peripheral, sets up DMA channels and interrupts, and allocates internal ring buffers. It returns an error code if the port is invalid or already initialized. uart_send() is non-blocking — it copies data into a transmit ring buffer and starts the DMA or interrupt-driven transfer. It returns immediately, allowing the caller to continue processing. If the TX buffer is full, it returns an error rather than blocking. uart_register_rx_callback() lets the application provide a function pointer that is called from the RX half-transfer or idle-line ISR with a pointer to the received data and its length.
uart_get_error() returns a bitmask of error flags (framing error, overrun, parity error, buffer overflow) accumulated since the last call, and clears them. This is better than returning errors from individual send/receive calls because errors often occur asynchronously. uart_deinit() disables the peripheral, releases DMA channels, and frees resources — essential for low-power modes where you need to shut down unused peripherals. The key design principle is that the API header contains no hardware-specific types — no UART_HandleTypeDef, no register addresses, no STM32-specific enums. This makes the API portable: swapping MCU families requires only a new implementation file, not changes to every file that calls the driver.
Source: Driver Design Q&A
