Hardware Interfaces
Introduction
Embedded systems interact with the physical world through various hardware interfaces. Understanding how to properly interface with different types of hardware components is essential for developing functional embedded applications. This chapter covers the most common hardware interfaces used in embedded systems, including GPIO, SPI, I2C, UART, and other specialized interfaces.
GPIO (General Purpose Input/Output)
GPIO pins are the most fundamental interface in embedded systems, providing direct digital input and output capabilities.
GPIO Configuration and Control
#include <stdint.h>
#include <stdbool.h>
// GPIO pin modes
typedef enum {
GPIO_MODE_INPUT = 0,
GPIO_MODE_OUTPUT,
GPIO_MODE_AF, // Alternate function
GPIO_MODE_ANALOG
} gpio_mode_t;
// GPIO output types
typedef enum {
GPIO_OTYPE_PP = 0, // Push-pull
GPIO_OTYPE_OD // Open-drain
} gpio_otype_t;
// GPIO speed settings
typedef enum {
GPIO_SPEED_LOW = 0,
GPIO_SPEED_MEDIUM,
GPIO_SPEED_HIGH,
GPIO_SPEED_VERY_HIGH
} gpio_speed_t;
// GPIO pull-up/pull-down settings
typedef enum {
GPIO_PUPD_NONE = 0,
GPIO_PUPD_UP,
GPIO_PUPD_DOWN
} gpio_pupd_t;
// GPIO port definitions
typedef enum {
GPIO_PORT_A = 0,
GPIO_PORT_B,
GPIO_PORT_C,
GPIO_PORT_D,
GPIO_PORT_E,
GPIO_PORT_F,
GPIO_PORT_G,
GPIO_PORT_H
} gpio_port_t;
// GPIO pin definitions
typedef enum {
GPIO_PIN_0 = 0,
GPIO_PIN_1,
GPIO_PIN_2,
GPIO_PIN_3,
GPIO_PIN_4,
GPIO_PIN_5,
GPIO_PIN_6,
GPIO_PIN_7,
GPIO_PIN_8,
GPIO_PIN_9,
GPIO_PIN_10,
GPIO_PIN_11,
GPIO_PIN_12,
GPIO_PIN_13,
GPIO_PIN_14,
GPIO_PIN_15
} gpio_pin_t;
// GPIO register structure
typedef struct {
volatile uint32_t MODER; // Mode register
volatile uint32_t OTYPER; // Output type register
volatile uint32_t OSPEEDR; // Output speed register
volatile uint32_t PUPDR; // Pull-up/pull-down register
volatile uint32_t IDR; // Input data register
volatile uint32_t ODR; // Output data register
volatile uint32_t BSRR; // Bit set/reset register
volatile uint32_t LCKR; // Configuration lock register
volatile uint32_t AFR[2]; // Alternate function registers
} gpio_reg_t;
// Base addresses for GPIO ports
#define GPIOA_BASE 0x40020000
#define GPIOB_BASE 0x40020400
#define GPIOC_BASE 0x40020800
#define GPIOD_BASE 0x40020C00
#define GPIOE_BASE 0x40021000
#define GPIOF_BASE 0x40021400
#define GPIOG_BASE 0x40021800
#define GPIOH_BASE 0x40021C00
// Get GPIO register pointer
static gpio_reg_t* get_gpio_reg(gpio_port_t port) {
switch (port) {
case GPIO_PORT_A: return (gpio_reg_t*)GPIOA_BASE;
case GPIO_PORT_B: return (gpio_reg_t*)GPIOB_BASE;
case GPIO_PORT_C: return (gpio_reg_t*)GPIOC_BASE;
case GPIO_PORT_D: return (gpio_reg_t*)GPIOD_BASE;
case GPIO_PORT_E: return (gpio_reg_t*)GPIOE_BASE;
case GPIO_PORT_F: return (gpio_reg_t*)GPIOF_BASE;
case GPIO_PORT_G: return (gpio_reg_t*)GPIOG_BASE;
case GPIO_PORT_H: return (gpio_reg_t*)GPIOH_BASE;
default: return NULL;
}
}
// Configure GPIO pin
int gpio_init(gpio_port_t port, gpio_pin_t pin, gpio_mode_t mode,
gpio_otype_t otype, gpio_speed_t speed, gpio_pupd_t pupd) {
gpio_reg_t *gpio = get_gpio_reg(port);
if (gpio == NULL) {
return -1;
}
uint32_t pin_pos = (uint32_t)pin * 2;
// Configure mode
gpio->MODER &= ~(3UL << pin_pos);
gpio->MODER |= ((uint32_t)mode << pin_pos);
// Configure output type (only for output modes)
if (mode == GPIO_MODE_OUTPUT || mode == GPIO_MODE_AF) {
gpio->OTYPER &= ~(1UL << pin);
gpio->OTYPER |= ((uint32_t)otype << pin);
}
// Configure speed
gpio->OSPEEDR &= ~(3UL << pin_pos);
gpio->OSPEEDR |= ((uint32_t)speed << pin_pos);
// Configure pull-up/pull-down
gpio->PUPDR &= ~(3UL << pin_pos);
gpio->PUPDR |= ((uint32_t)pupd << pin_pos);
return 0;
}
// Set GPIO pin high
void gpio_set(gpio_port_t port, gpio_pin_t pin) {
gpio_reg_t *gpio = get_gpio_reg(port);
if (gpio != NULL) {
gpio->BSRR = (1UL << pin);
}
}
// Clear GPIO pin (set low)
void gpio_clear(gpio_port_t port, gpio_pin_t pin) {
gpio_reg_t *gpio = get_gpio_reg(port);
if (gpio != NULL) {
gpio->BSRR = (1UL << (pin + 16));
}
}
// Toggle GPIO pin
void gpio_toggle(gpio_port_t port, gpio_pin_t pin) {
gpio_reg_t *gpio = get_gpio_reg(port);
if (gpio != NULL) {
gpio->ODR ^= (1UL << pin);
}
}
// Read GPIO pin state
int gpio_read(gpio_port_t port, gpio_pin_t pin) {
gpio_reg_t *gpio = get_gpio_reg(port);
if (gpio != NULL) {
return (gpio->IDR & (1UL << pin)) ? 1 : 0;
}
return -1;
}
// Configure GPIO pin as input with pull-up
int gpio_config_input_pullup(gpio_port_t port, gpio_pin_t pin) {
return gpio_init(port, pin, GPIO_MODE_INPUT, GPIO_OTYPE_PP,
GPIO_SPEED_LOW, GPIO_PUPD_UP);
}
// Configure GPIO pin as output push-pull
int gpio_config_output(gpio_port_t port, gpio_pin_t pin) {
return gpio_init(port, pin, GPIO_MODE_OUTPUT, GPIO_OTYPE_PP,
GPIO_SPEED_HIGH, GPIO_PUPD_NONE);
}SPI (Serial Peripheral Interface)
SPI is a synchronous serial communication interface used for short-distance communication between microcontrollers and peripheral devices.
SPI Driver Implementation
#include <stdint.h>
#include <stdbool.h>
// SPI register structure
typedef struct {
volatile uint32_t CR1; // Control register 1
volatile uint32_t CR2; // Control register 2
volatile uint32_t SR; // Status register
volatile uint32_t DR; // Data register
volatile uint32_t CRCPR; // CRC polynomial register
volatile uint32_t RXCRCR; // RX CRC register
volatile uint32_t TXCRCR; // TX CRC register
volatile uint32_t I2SCFGR; // I2S configuration register
volatile uint32_t I2SPR; // I2S prescaler register
} spi_reg_t;
// SPI base addresses
#define SPI1_BASE 0x40013000
#define SPI2_BASE 0x40003800
#define SPI3_BASE 0x40003C00
#define SPI1 ((spi_reg_t*)SPI1_BASE)
#define SPI2 ((spi_reg_t*)SPI2_BASE)
#define SPI3 ((spi_reg_t*)SPI3_BASE)
// SPI control register bits
#define SPI_CR1_CPHA (1UL << 0) // Clock phase
#define SPI_CR1_CPOL (1UL << 1) // Clock polarity
#define SPI_CR1_MSTR (1UL << 2) // Master selection
#define SPI_CR1_BR (7UL << 3) // Baud rate control
#define SPI_CR1_SPE (1UL << 6) // SPI enable
#define SPI_CR1_LSBFIRST (1UL << 7) // Frame format
#define SPI_CR1_SSI (1UL << 8) // Internal slave select
#define SPI_CR1_SSM (1UL << 9) // Software slave management
#define SPI_CR1_RXONLY (1UL << 10) // Receive only
#define SPI_CR1_DFF (1UL << 11) // Data frame format
#define SPI_CR1_CRCNEXT (1UL << 12) // CRC transfer next
#define SPI_CR1_CRCEN (1UL << 13) // Hardware CRC calculation
#define SPI_CR1_BIDIOE (1UL << 14) // Output enable in bidirectional mode
#define SPI_CR1_BIDIMODE (1UL << 15) // Bidirectional data mode enable
// SPI status register bits
#define SPI_SR_RXNE (1UL << 0) // Receive buffer not empty
#define SPI_SR_TXE (1UL << 1) // Transmit buffer empty
#define SPI_SR_CHSIDE (1UL << 2) // Channel side
#define SPI_SR_UDR (1UL << 3) // Underrun flag
#define SPI_SR_CRCERR (1UL << 4) // CRC error flag
#define SPI_SR_MODF (1UL << 5) // Mode fault
#define SPI_SR_OVR (1UL << 6) // Overrun flag
#define SPI_SR_BSY (1UL << 7) // Busy flag
// SPI configuration structure
typedef struct {
uint32_t baud_rate; // Baud rate prescaler
bool master; // Master mode
bool cpol; // Clock polarity
bool cpha; // Clock phase
bool lsb_first; // LSB first
bool bidirectional; // Bidirectional mode
uint8_t data_size; // Data size (8 or 16 bits)
} spi_config_t;
// Initialize SPI peripheral
int spi_init(spi_reg_t *spi, const spi_config_t *config) {
if (spi == NULL || config == NULL) {
return -1;
}
// Disable SPI
spi->CR1 &= ~SPI_CR1_SPE;
// Configure SPI
uint32_t cr1 = 0;
// Set baud rate
cr1 |= (config->baud_rate << 3) & SPI_CR1_BR;
// Set master/slave mode
if (config->master) {
cr1 |= SPI_CR1_MSTR;
cr1 |= SPI_CR1_SSI; // Set internal slave select
}
// Set clock polarity and phase
if (config->cpol) cr1 |= SPI_CR1_CPOL;
if (config->cpha) cr1 |= SPI_CR1_CPHA;
// Set data frame format
if (config->data_size == 16) {
cr1 |= SPI_CR1_DFF;
}
// Set frame format
if (config->lsb_first) {
cr1 |= SPI_CR1_LSBFIRST;
}
// Enable software slave management
cr1 |= SPI_CR1_SSM;
spi->CR1 = cr1;
// Configure bidirectional mode
if (config->bidirectional) {
spi->CR1 |= SPI_CR1_BIDIMODE;
spi->CR1 |= SPI_CR1_BIDIOE; // Enable output
}
// Enable SPI
spi->CR1 |= SPI_CR1_SPE;
return 0;
}
// SPI transmit function
int spi_transmit(spi_reg_t *spi, const uint8_t *data, uint32_t length) {
if (spi == NULL || data == NULL || length == 0) {
return -1;
}
for (uint32_t i = 0; i < length; i++) {
// Wait for transmit buffer to be empty
while (!(spi->SR & SPI_SR_TXE));
// Send data
spi->DR = data[i];
// Wait for transmission to complete
while (spi->SR & SPI_SR_BSY);
}
return 0;
}
// SPI receive function
int spi_receive(spi_reg_t *spi, uint8_t *data, uint32_t length) {
if (spi == NULL || data == NULL || length == 0) {
return -1;
}
for (uint32_t i = 0; i < length; i++) {
// Wait for receive buffer to be empty
while (!(spi->SR & SPI_SR_TXE));
// Send dummy data to initiate reception
spi->DR = 0xFF;
// Wait for data to be received
while (!(spi->SR & SPI_SR_RXNE));
// Read received data
data[i] = spi->DR;
}
return 0;
}
// SPI transmit and receive function (full duplex)
int spi_transceive(spi_reg_t *spi, const uint8_t *tx_data,
uint8_t *rx_data, uint32_t length) {
if (spi == NULL || tx_data == NULL || rx_data == NULL || length == 0) {
return -1;
}
for (uint32_t i = 0; i < length; i++) {
// Wait for transmit buffer to be empty
while (!(spi->SR & SPI_SR_TXE));
// Send data
spi->DR = tx_data[i];
// Wait for data to be received
while (!(spi->SR & SPI_SR_RXNE));
// Read received data
rx_data[i] = spi->DR;
}
return 0;
}
// SPI chip select control
void spi_select_device(gpio_port_t cs_port, gpio_pin_t cs_pin) {
gpio_clear(cs_port, cs_pin); // Active low CS
}
void spi_deselect_device(gpio_port_t cs_port, gpio_pin_t cs_pin) {
gpio_set(cs_port, cs_pin); // Inactive high CS
}I2C (Inter-Integrated Circuit)
I2C is a multi-master, multi-slave, packet-switched, single-ended, serial communication bus used for short-distance communication.
I2C Driver Implementation
#include <stdint.h>
#include <stdbool.h>
// I2C register structure
typedef struct {
volatile uint32_t CR1; // Control register 1
volatile uint32_t CR2; // Control register 2
volatile uint32_t OAR1; // Own address register 1
volatile uint32_t OAR2; // Own address register 2
volatile uint32_t DR; // Data register
volatile uint32_t SR1; // Status register 1
volatile uint32_t SR2; // Status register 2
volatile uint32_t CCR; // Clock control register
volatile uint32_t TRISE; // TRISE register
volatile uint32_t FLTR; // FLTR register
} i2c_reg_t;
// I2C base addresses
#define I2C1_BASE 0x40005400
#define I2C2_BASE 0x40005800
#define I2C3_BASE 0x40005C00
#define I2C1 ((i2c_reg_t*)I2C1_BASE)
#define I2C2 ((i2c_reg_t*)I2C2_BASE)
#define I2C3 ((i2c_reg_t*)I2C3_BASE)
// I2C control register 1 bits
#define I2C_CR1_PE (1UL << 0) // Peripheral enable
#define I2C_CR1_SMBUS (1UL << 1) // SMBus mode
#define I2C_CR1_SMBTYPE (1UL << 3) // SMBus type
#define I2C_CR1_ENARP (1UL << 4) // ARP enable
#define I2C_CR1_ENPEC (1UL << 5) // PEC enable
#define I2C_CR1_ENGC (1UL << 6) // General call enable
#define I2C_CR1_NOSTRETCH (1UL << 7) // Clock stretching disable
#define I2C_CR1_START (1UL << 8) // Start generation
#define I2C_CR1_STOP (1UL << 9) // Stop generation
#define I2C_CR1_ACK (1UL << 10) // Acknowledge enable
#define I2C_CR1_POS (1UL << 11) // Acknowledge/PEC position
#define I2C_CR1_PEC (1UL << 12) // Packet error checking
#define I2C_CR1_ALERT (1UL << 13) // SMBus alert
#define I2C_CR1_SWRST (1UL << 15) // Software reset
// I2C status register 1 bits
#define I2C_SR1_SB (1UL << 0) // Start bit
#define I2C_SR1_ADDR (1UL << 1) // Address sent/matched
#define I2C_SR1_BTF (1UL << 2) // Byte transfer finished
#define I2C_SR1_ADD10 (1UL << 3) // 10-bit header sent
#define I2C_SR1_STOPF (1UL << 4) // Stop detection
#define I2C_SR1_RXNE (1UL << 6) // Data register not empty
#define I2C_SR1_TXE (1UL << 7) // Data register empty
#define I2C_SR1_BERR (1UL << 8) // Bus error
#define I2C_SR1_ARLO (1UL << 9) // Arbitration lost
#define I2C_SR1_AF (1UL << 10) // Acknowledge failure
#define I2C_SR1_OVR (1UL << 11) // Overrun/underrun
#define I2C_SR1_PECERR (1UL << 12) // PEC error
#define I2C_SR1_TIMEOUT (1UL << 14) // Timeout or Tlow error
#define I2C_SR1_SMBALERT (1UL << 15) // SMBus alert
// I2C configuration structure
typedef struct {
uint32_t clock_speed; // I2C clock speed in Hz
bool master; // Master mode
uint16_t own_address; // Own address (slave mode)
bool ack_enable; // Acknowledge enable
} i2c_config_t;
// Initialize I2C peripheral
int i2c_init(i2c_reg_t *i2c, const i2c_config_t *config) {
if (i2c == NULL || config == NULL) {
return -1;
}
// Disable I2C
i2c->CR1 &= ~I2C_CR1_PE;
// Configure I2C clock
uint32_t freq_mhz = 0; // Peripheral clock frequency in MHz
// freq_mhz = get_pclk1_frequency() / 1000000;
// Configure CR2 register (frequency)
i2c->CR2 = freq_mhz & 0x3F;
// Configure CCR register (clock control)
uint32_t ccr_value = 0;
if (config->clock_speed <= 100000) {
// Standard mode (up to 100kHz)
ccr_value = (freq_mhz * 1000000) / (config->clock_speed * 2);
if (ccr_value < 4) ccr_value = 4;
} else {
// Fast mode (up to 400kHz)
ccr_value = (freq_mhz * 1000000) / (config->clock_speed * 3);
if (ccr_value < 1) ccr_value = 1;
ccr_value |= (1UL << 15); // Fast mode
}
i2c->CCR = ccr_value;
// Configure TRISE register (rise time)
if (config->clock_speed <= 100000) {
// Standard mode
i2c->TRISE = freq_mhz + 1;
} else {
// Fast mode
i2c->TRISE = (freq_mhz * 300) / 1000 + 1;
}
// Configure own address (slave mode)
if (!config->master) {
i2c->OAR1 = (config->own_address << 1) | (1UL << 14);
}
// Enable acknowledge
if (config->ack_enable) {
i2c->CR1 |= I2C_CR1_ACK;
}
// Enable I2C
i2c->CR1 |= I2C_CR1_PE;
return 0;
}
// Generate start condition
static int i2c_start(i2c_reg_t *i2c) {
// Generate start condition
i2c->CR1 |= I2C_CR1_START;
// Wait for start condition to be generated
uint32_t timeout = 100000;
while (!(i2c->SR1 & I2C_SR1_SB) && timeout--) {
if (i2c->SR1 & (I2C_SR1_ARLO | I2C_SR1_BERR | I2C_SR1_AF)) {
return -1; // Error occurred
}
}
if (timeout == 0) {
return -2; // Timeout
}
return 0;
}
// Generate stop condition
static void i2c_stop(i2c_reg_t *i2c) {
i2c->CR1 |= I2C_CR1_STOP;
}
// Send address
static int i2c_send_address(i2c_reg_t *i2c, uint8_t address, bool read) {
// Send address
i2c->DR = (address << 1) | (read ? 1 : 0);
// Wait for address to be sent
uint32_t timeout = 100000;
while (!(i2c->SR1 & I2C_SR1_ADDR) && timeout--) {
if (i2c->SR1 & (I2C_SR1_ARLO | I2C_SR1_BERR | I2C_SR1_AF)) {
return -1; // Error occurred
}
}
if (timeout == 0) {
return -2; // Timeout
}
// Clear ADDR flag by reading SR1 and SR2
volatile uint32_t temp = i2c->SR1;
temp = i2c->SR2;
(void)temp; // Prevent compiler warning
return 0;
}
// Send data
static int i2c_send_data(i2c_reg_t *i2c, const uint8_t *data, uint32_t length) {
for (uint32_t i = 0; i < length; i++) {
// Wait for transmit buffer to be empty
uint32_t timeout = 100000;
while (!(i2c->SR1 & I2C_SR1_TXE) && timeout--) {
if (i2c->SR1 & (I2C_SR1_ARLO | I2C_SR1_BERR | I2C_SR1_AF | I2C_SR1_OVR)) {
return -1; // Error occurred
}
}
if (timeout == 0) {
return -2; // Timeout
}
// Send data
i2c->DR = data[i];
}
// Wait for byte transfer to finish
uint32_t timeout = 100000;
while (!(i2c->SR1 & I2C_SR1_BTF) && timeout--) {
if (i2c->SR1 & (I2C_SR1_ARLO | I2C_SR1_BERR | I2C_SR1_AF | I2C_SR1_OVR)) {
return -1; // Error occurred
}
}
if (timeout == 0) {
return -2; // Timeout
}
return 0;
}
// Receive data
static int i2c_receive_data(i2c_reg_t *i2c, uint8_t *data, uint32_t length) {
// Enable acknowledge for all bytes except the last one
if (length > 1) {
i2c->CR1 |= I2C_CR1_ACK;
} else {
i2c->CR1 &= ~I2C_CR1_ACK;
}
for (uint32_t i = 0; i < length; i++) {
// For the last byte, generate stop condition before reading
if (i == length - 1) {
i2c->CR1 &= ~I2C_CR1_ACK; // Disable acknowledge
i2c_stop(i2c); // Generate stop condition
}
// Wait for data to be received
uint32_t timeout = 100000;
while (!(i2c->SR1 & I2C_SR1_RXNE) && timeout--) {
if (i2c->SR1 & (I2C_SR1_ARLO | I2C_SR1_BERR | I2C_SR1_AF | I2C_SR1_OVR)) {
return -1; // Error occurred
}
}
if (timeout == 0) {
return -2; // Timeout
}
// Read data
data[i] = i2c->DR;
}
return 0;
}
// I2C write function
int i2c_write(i2c_reg_t *i2c, uint8_t device_address,
const uint8_t *data, uint32_t length) {
if (i2c == NULL || data == NULL || length == 0) {
return -1;
}
// Generate start condition
if (i2c_start(i2c) != 0) {
return -2;
}
// Send device address with write bit
if (i2c_send_address(i2c, device_address, false) != 0) {
i2c_stop(i2c);
return -3;
}
// Send data
if (i2c_send_data(i2c, data, length) != 0) {
i2c_stop(i2c);
return -4;
}
// Generate stop condition
i2c_stop(i2c);
return 0;
}
// I2C read function
int i2c_read(i2c_reg_t *i2c, uint8_t device_address,
uint8_t *data, uint32_t length) {
if (i2c == NULL || data == NULL || length == 0) {
return -1;
}
// Generate start condition
if (i2c_start(i2c) != 0) {
return -2;
}
// Send device address with read bit
if (i2c_send_address(i2c, device_address, true) != 0) {
i2c_stop(i2c);
return -3;
}
// Receive data
if (i2c_receive_data(i2c, data, length) != 0) {
return -4;
}
return 0;
}
// I2C write-read function (repeated start)
int i2c_write_read(i2c_reg_t *i2c, uint8_t device_address,
const uint8_t *write_data, uint32_t write_length,
uint8_t *read_data, uint32_t read_length) {
if (i2c == NULL || write_data == NULL || read_data == NULL ||
write_length == 0 || read_length == 0) {
return -1;
}
// Generate start condition
if (i2c_start(i2c) != 0) {
return -2;
}
// Send device address with write bit
if (i2c_send_address(i2c, device_address, false) != 0) {
i2c_stop(i2c);
return -3;
}
// Send write data
if (i2c_send_data(i2c, write_data, write_length) != 0) {
i2c_stop(i2c);
return -4;
}
// Generate repeated start condition
if (i2c_start(i2c) != 0) {
i2c_stop(i2c);
return -5;
}
// Send device address with read bit
if (i2c_send_address(i2c, device_address, true) != 0) {
i2c_stop(i2c);
return -6;
}
// Receive read data
if (i2c_receive_data(i2c, read_data, read_length) != 0) {
return -7;
}
return 0;
}UART (Universal Asynchronous Receiver-Transmitter)
UART is used for asynchronous serial communication between devices.
UART Driver Implementation
#include <stdint.h>
#include <stdbool.h>
// UART register structure
typedef struct {
volatile uint32_t SR; // Status register
volatile uint32_t DR; // Data register
volatile uint32_t BRR; // Baud rate register
volatile uint32_t CR1; // Control register 1
volatile uint32_t CR2; // Control register 2
volatile uint32_t CR3; // Control register 3
volatile uint32_t GTPR; // Guard time and prescaler register
} uart_reg_t;
// UART base addresses
#define USART1_BASE 0x40011000
#define USART2_BASE 0x40004400
#define USART3_BASE 0x40004800
#define UART4_BASE 0x40004C00
#define UART5_BASE 0x40005000
#define USART1 ((uart_reg_t*)USART1_BASE)
#define USART2 ((uart_reg_t*)USART2_BASE)
#define USART3 ((uart_reg_t*)USART3_BASE)
#define UART4 ((uart_reg_t*)UART4_BASE)
#define UART5 ((uart_reg_t*)UART5_BASE)
// UART status register bits
#define UART_SR_PE (1UL << 0) // Parity error
#define UART_SR_FE (1UL << 1) // Framing error
#define UART_SR_NE (1UL << 2) // Noise error
#define UART_SR_ORE (1UL << 3) // Overrun error
#define UART_SR_IDLE (1UL << 4) // IDLE line detected
#define UART_SR_RXNE (1UL << 5) // Read data register not empty
#define UART_SR_TC (1UL << 6) // Transmission complete
#define UART_SR_TXE (1UL << 7) // Transmit data register empty
#define UART_SR_LBD (1UL << 8) // LIN break detection flag
#define UART_SR_CTS (1UL << 9) // CTS flag
// UART control register 1 bits
#define UART_CR1_SBK (1UL << 0) // Send break
#define UART_CR1_RWU (1UL << 1) // Receiver wakeup
#define UART_CR1_RE (1UL << 2) // Receiver enable
#define UART_CR1_TE (1UL << 3) // Transmitter enable
#define UART_CR1_IDLEIE (1UL << 4) // IDLE interrupt enable
#define UART_CR1_RXNEIE (1UL << 5) // RXNE interrupt enable
#define UART_CR1_TCIE (1UL << 6) // Transmission complete interrupt enable
#define UART_CR1_TXEIE (1UL << 7) // TXE interrupt enable
#define UART_CR1_PEIE (1UL << 8) // PE interrupt enable
#define UART_CR1_PS (1UL << 9) // Parity selection
#define UART_CR1_PCE (1UL << 10) // Parity control enable
#define UART_CR1_WAKE (1UL << 11) // Wakeup method
#define UART_CR1_M (1UL << 12) // Word length
#define UART_CR1_UE (1UL << 13) // USART enable
#define UART_CR1_OVER8 (1UL << 15) // Oversampling mode
// UART configuration structure
typedef struct {
uint32_t baud_rate; // Baud rate
uint8_t data_bits; // Data bits (8 or 9)
uint8_t stop_bits; // Stop bits (1 or 2)
bool parity_enable; // Parity enable
bool parity_even; // Even parity (true) or odd parity (false)
uint32_t peripheral_clock; // Peripheral clock frequency
} uart_config_t;
// Initialize UART peripheral
int uart_init(uart_reg_t *uart, const uart_config_t *config) {
if (uart == NULL || config == NULL) {
return -1;
}
// Disable UART
uart->CR1 &= ~UART_CR1_UE;
// Configure baud rate
uint32_t baud_div = 0;
if (config->baud_rate != 0) {
baud_div = (config->peripheral_clock + (config->baud_rate / 2)) / config->baud_rate;
}
uart->BRR = baud_div;
// Configure data bits
if (config->data_bits == 9) {
uart->CR1 |= UART_CR1_M;
} else {
uart->CR1 &= ~UART_CR1_M;
}
// Configure parity
if (config->parity_enable) {
uart->CR1 |= UART_CR1_PCE;
if (config->parity_even) {
uart->CR1 &= ~UART_CR1_PS; // Even parity
} else {
uart->CR1 |= UART_CR1_PS; // Odd parity
}
} else {
uart->CR1 &= ~UART_CR1_PCE;
}
// Configure stop bits
uart->CR2 &= ~(3UL << 12); // Clear stop bits
if (config->stop_bits == 2) {
uart->CR2 |= (2UL << 12); // 2 stop bits
} else {
uart->CR2 |= (0UL << 12); // 1 stop bit
}
// Enable transmitter and receiver
uart->CR1 |= UART_CR1_TE | UART_CR1_RE;
// Enable UART
uart->CR1 |= UART_CR1_UE;
return 0;
}
// Send character
int uart_putc(uart_reg_t *uart, char c) {
if (uart == NULL) {
return -1;
}
// Wait for transmit data register to be empty
uint32_t timeout = 100000;
while (!(uart->SR & UART_SR_TXE) && timeout--) {
// Wait
}
if (timeout == 0) {
return -2; // Timeout
}
// Send character
uart->DR = c;
return 0;
}
// Send string
int uart_puts(uart_reg_t *uart, const char *str) {
if (uart == NULL || str == NULL) {
return -1;
}
while (*str) {
if (uart_putc(uart, *str++) != 0) {
return -2;
}
}
return 0;
}
// Receive character
int uart_getc(uart_reg_t *uart, char *c) {
if (uart == NULL || c == NULL) {
return -1;
}
// Check if data is available
if (!(uart->SR & UART_SR_RXNE)) {
return -2; // No data available
}
// Read character
*c = (char)(uart->DR & 0xFF);
// Check for errors
if (uart->SR & (UART_SR_PE | UART_SR_FE | UART_SR_NE | UART_SR_ORE)) {
return -3; // Error occurred
}
return 0;
}
// Check if data is available
bool uart_available(uart_reg_t *uart) {
if (uart == NULL) {
return false;
}
return (uart->SR & UART_SR_RXNE) ? true : false;
}
// Flush UART receive buffer
void uart_flush(uart_reg_t *uart) {
if (uart == NULL) {
return;
}
// Read all available data
while (uart->SR & UART_SR_RXNE) {
volatile uint32_t temp = uart->DR;
(void)temp; // Prevent compiler warning
}
}Practical Examples
Sensor Interface Example
#include <stdint.h>
#include <stdbool.h>
// Temperature sensor (I2C) driver
#define TEMP_SENSOR_ADDR 0x48
typedef struct {
i2c_reg_t *i2c;
uint8_t address;
} temp_sensor_t;
// Initialize temperature sensor
int temp_sensor_init(temp_sensor_t *sensor, i2c_reg_t *i2c) {
if (sensor == NULL || i2c == NULL) {
return -1;
}
sensor->i2c = i2c;
sensor->address = TEMP_SENSOR_ADDR;
// Configure sensor (example configuration)
uint8_t config_data[] = {0x01, 0x60}; // Configuration register and value
return i2c_write(sensor->i2c, sensor->address, config_data, 2);
}
// Read temperature
float temp_sensor_read(temp_sensor_t *sensor) {
if (sensor == NULL) {
return -1.0;
}
// Read temperature register
uint8_t reg_addr = 0x00; // Temperature register
uint8_t data[2];
if (i2c_write_read(sensor->i2c, sensor->address,
®_addr, 1, data, 2) != 0) {
return -1.0;
}
// Convert to temperature value
int16_t raw_temp = (data[0] << 8) | data[1];
float temperature = raw_temp / 256.0;
return temperature;
}
// EEPROM (I2C) driver
#define EEPROM_ADDR 0x50
typedef struct {
i2c_reg_t *i2c;
uint8_t address;
} eeprom_t;
// Initialize EEPROM
int eeprom_init(eeprom_t *eeprom, i2c_reg_t *i2c) {
if (eeprom == NULL || i2c == NULL) {
return -1;
}
eeprom->i2c = i2c;
eeprom->address = EEPROM_ADDR;
return 0;
}
// Write data to EEPROM
int eeprom_write(eeprom_t *eeprom, uint16_t address,
const uint8_t *data, uint32_t length) {
if (eeprom == NULL || data == NULL || length == 0) {
return -1;
}
// Write data in chunks (EEPROM page size is typically 16-64 bytes)
uint32_t written = 0;
while (written < length) {
uint32_t chunk_size = (length - written > 16) ? 16 : (length - written);
// Prepare data with address
uint8_t write_data[18]; // 2 bytes address + 16 bytes data
write_data[0] = (address + written) >> 8; // High byte of address
write_data[1] = (address + written) & 0xFF; // Low byte of address
for (uint32_t i = 0; i < chunk_size; i++) {
write_data[2 + i] = data[written + i];
}
// Write chunk
if (i2c_write(eeprom->i2c, eeprom->address,
write_data, 2 + chunk_size) != 0) {
return -2;
}
written += chunk_size;
// Wait for write to complete (EEPROM typical write time is 5ms)
rt_delay_ms(5);
}
return 0;
}
// Read data from EEPROM
int eeprom_read(eeprom_t *eeprom, uint16_t address,
uint8_t *data, uint32_t length) {
if (eeprom == NULL || data == NULL || length == 0) {
return -1;
}
// Send address
uint8_t addr_data[2] = {(address >> 8) & 0xFF, address & 0xFF};
if (i2c_write(eeprom->i2c, eeprom->address, addr_data, 2) != 0) {
return -2;
}
// Read data
return i2c_read(eeprom->i2c, eeprom->address, data, length);
}
// SPI Flash memory driver
typedef struct {
spi_reg_t *spi;
gpio_port_t cs_port;
gpio_pin_t cs_pin;
} flash_t;
// Initialize flash memory
int flash_init(flash_t *flash, spi_reg_t *spi,
gpio_port_t cs_port, gpio_pin_t cs_pin) {
if (flash == NULL || spi == NULL) {
return -1;
}
flash->spi = spi;
flash->cs_port = cs_port;
flash->cs_pin = cs_pin;
// Configure CS pin as output
gpio_config_output(cs_port, cs_pin);
gpio_set(cs_port, cs_pin); // Deselect flash
return 0;
}
// Send command to flash
static int flash_send_command(flash_t *flash, const uint8_t *cmd, uint32_t cmd_len,
const uint8_t *data_out, uint8_t *data_in, uint32_t data_len) {
if (flash == NULL || cmd == NULL || cmd_len == 0) {
return -1;
}
// Select flash
spi_select_device(flash->cs_port, flash->cs_pin);
// Send command
if (spi_transmit(flash->spi, cmd, cmd_len) != 0) {
spi_deselect_device(flash->cs_port, flash->cs_pin);
return -2;
}
// Send/receive data
if (data_out != NULL && data_in != NULL) {
if (spi_transceive(flash->spi, data_out, data_in, data_len) != 0) {
spi_deselect_device(flash->cs_port, flash->cs_pin);
return -3;
}
} else if (data_out != NULL) {
if (spi_transmit(flash->spi, data_out, data_len) != 0) {
spi_deselect_device(flash->cs_port, flash->cs_pin);
return -4;
}
} else if (data_in != NULL) {
if (spi_receive(flash->spi, data_in, data_len) != 0) {
spi_deselect_device(flash->cs_port, flash->cs_pin);
return -5;
}
}
// Deselect flash
spi_deselect_device(flash->cs_port, flash->cs_pin);
return 0;
}
// Read flash ID
int flash_read_id(flash_t *flash, uint8_t *id, uint32_t id_len) {
if (flash == NULL || id == NULL || id_len == 0) {
return -1;
}
uint8_t cmd = 0x9F; // Read ID command
return flash_send_command(flash, &cmd, 1, NULL, id, id_len);
}
// Read data from flash
int flash_read(flash_t *flash, uint32_t address, uint8_t *data, uint32_t length) {
if (flash == NULL || data == NULL || length == 0) {
return -1;
}
// Prepare read command with address
uint8_t cmd[4] = {0x03, // Read command
(address >> 16) & 0xFF, // Address high byte
(address >> 8) & 0xFF, // Address middle byte
address & 0xFF}; // Address low byte
return flash_send_command(flash, cmd, 4, NULL, data, length);
}
// Write data to flash (page program)
int flash_write(flash_t *flash, uint32_t address, const uint8_t *data, uint32_t length) {
if (flash == NULL || data == NULL || length == 0) {
return -1;
}
// Prepare write command with address
uint8_t cmd[4] = {0x02, // Page program command
(address >> 16) & 0xFF, // Address high byte
(address >> 8) & 0xFF, // Address middle byte
address & 0xFF}; // Address low byte
return flash_send_command(flash, cmd, 4, data, NULL, length);
}Summary
Hardware interfaces are fundamental to embedded systems, enabling communication with various peripheral devices and sensors:
- GPIO - Basic digital input/output for controlling LEDs, reading switches, etc.
- SPI - High-speed synchronous communication for flash memory, displays, and sensors
- I2C - Multi-master communication for EEPROMs, temperature sensors, and other devices
- UART - Asynchronous serial communication for debug output and device communication
Key considerations for hardware interface implementation: - Proper initialization and configuration of peripheral registers - Error handling and timeout mechanisms - Efficient data transfer methods - Proper use of chip select signals for SPI devices - Addressing schemes for I2C devices - Baud rate configuration for UART communication - Interrupt-driven vs. polling-based approaches - Resource sharing and synchronization in multi-threaded environments
These interfaces form the foundation for connecting embedded systems to the physical world, enabling the development of sophisticated applications that interact with sensors, actuators, displays, storage devices, and communication modules.