4. UART驱动封装实战(HAL层)
UART,说白了就是串口。做嵌入式开发的人,没人离得开它。打印日志、跟传感器通信、跟PC机交互,全得靠它。但很多工程师写UART驱动,就是直接操作寄存器,中断来了就往全局变量里塞数据。小项目还行,项目一复杂,问题就全冒出来了。
我个人习惯,在HAL层把UART封装成一套标准接口。不管底层是STM32、GD32还是NXP,上层调用的API长一个样。这样换芯片平台,驱动层一换,应用层代码几乎不用动。
4.1 UART标准接口定义
接口设计,我遵循一个原则:够用就好。别搞几十个API,没人记得住。核心就四个:初始化、发送、接收、中断/DMA控制。
| 接口函数 | 功能描述 | 参数说明 |
|---|---|---|
uart_init() |
初始化UART外设,配置波特率、数据位、停止位、校验位 | uart_id, baud, config结构体指针 |
uart_send() |
发送一帧数据,支持阻塞/非阻塞模式 | uart_id, data_buf, len, timeout |
uart_receive() |
接收一帧数据,从环形缓冲区读取 | uart_id, recv_buf, len, timeout |
uart_irq_handler() |
中断服务函数,由底层中断向量表调用 | uart_id |
你看,就这四个。初始化配置硬件,发送和接收处理数据流,中断函数负责把数据搬进搬出环形缓冲区。简洁、清晰、好维护。
4.2 环形缓冲区设计与实现
为什么要用环形缓冲区?你想想看,UART接收数据是异步的。中断随时可能来,而主循环可能在处理其他任务。如果没有缓冲区,数据就丢了。
环形缓冲区,本质上就是一个固定大小的数组,加上两个指针:读指针和写指针。写指针追着读指针跑,数据写满了就覆盖旧数据——当然,我们一般会做防覆盖保护。
/* 环形缓冲区结构体 */
typedef struct {
uint8_t *buffer; /* 数据存储区 */
uint32_t size; /* 缓冲区大小 */
volatile uint32_t head; /* 写指针 */
volatile uint32_t tail; /* 读指针 */
} ring_buffer_t;
这里有个关键点:head和tail必须加volatile。为什么?因为中断和主循环会同时访问它们。不加volatile,编译器优化后可能把变量值缓存在寄存器里,导致数据不同步。我见过有人因为这个bug调了三天...
环形缓冲区的核心操作就三个:
- 写数据:检查缓冲区是否已满,未满则写入并移动head指针
- 读数据:检查缓冲区是否为空,非空则读取并移动tail指针
- 查询状态:获取已用空间、剩余空间
/* 写入一个字节到环形缓冲区 */
uint32_t ring_buf_write(ring_buffer_t *rb, uint8_t data) {
uint32_t next_head = (rb->head + 1) % rb->size;
/* 缓冲区满,丢弃数据 */
if (next_head == rb->tail) {
return 0; /* 写入失败 */
}
rb->buffer[rb->head] = data;
rb->head = next_head;
return 1; /* 写入成功 */
}
/* 从环形缓冲区读取一个字节 */
uint32_t ring_buf_read(ring_buffer_t *rb, uint8_t *data) {
if (rb->head == rb->tail) {
return 0; /* 缓冲区空 */
}
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % rb->size;
return 1; /* 读取成功 */
}
(head + 1) & 0xFF就等价于(head + 1) % 256。
4.3 基于STM32的UART HAL实现
好了,理论说完了,咱们上真家伙。以STM32F4为例,把上面的接口和环形缓冲区落地。
先定义UART设备结构体,把硬件外设、环形缓冲区、配置参数都封装在一起:
/* UART设备结构体 */
typedef struct {
USART_TypeDef *instance; /* STM32 UART外设基地址 */
ring_buffer_t tx_rb; /* 发送环形缓冲区 */
ring_buffer_t rx_rb; /* 接收环形缓冲区 */
uint32_t baudrate; /* 波特率 */
uint8_t data_bits; /* 数据位 */
uint8_t stop_bits; /* 停止位 */
uint8_t parity; /* 校验位 */
void (*irq_callback)(uint8_t); /* 用户回调函数 */
} uart_device_t;
初始化函数,说白了就是配置GPIO、配置UART参数、使能中断:
int32_t uart_init(uint8_t uart_id, uart_config_t *config) {
uart_device_t *dev = &uart_devices[uart_id];
/* 1. 配置GPIO复用功能 */
gpio_init_for_uart(dev->instance);
/* 2. 配置UART参数 */
dev->instance->CR1 = 0;
dev->instance->CR1 |= (config->data_bits << 12); /* 数据位 */
dev->instance->CR2 |= (config->stop_bits << 12); /* 停止位 */
dev->instance->CR1 |= (config->parity << 10); /* 校验位 */
/* 3. 设置波特率 */
uint32_t usartdiv = SystemCoreClock / config->baudrate;
dev->instance->BRR = usartdiv;
/* 4. 使能接收中断 */
dev->instance->CR1 |= USART_CR1_RXNEIE;
/* 5. 使能UART */
dev->instance->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
/* 6. 初始化环形缓冲区 */
ring_buf_init(&dev->rx_rb, dev->rx_buffer, RX_BUF_SIZE);
ring_buf_init(&dev->tx_rb, dev->tx_buffer, TX_BUF_SIZE);
return 0;
}
中断处理函数,这是整个驱动的核心。数据来了,第一时间存进环形缓冲区:
void uart_irq_handler(uint8_t uart_id) {
uart_device_t *dev = &uart_devices[uart_id];
/* 接收中断 */
if (dev->instance->SR & USART_SR_RXNE) {
uint8_t data = (uint8_t)(dev->instance->DR & 0xFF);
ring_buf_write(&dev->rx_rb, data);
/* 调用用户回调 */
if (dev->irq_callback) {
dev->irq_callback(data);
}
}
/* 发送中断(发送缓冲区非空时触发) */
if (dev->instance->SR & USART_SR_TXE) {
uint8_t data;
if (ring_buf_read(&dev->tx_rb, &data)) {
dev->instance->DR = data;
} else {
/* 发送缓冲区空,关闭发送中断 */
dev->instance->CR1 &= ~USART_CR1_TXEIE;
}
}
}
发送函数,支持两种模式:
int32_t uart_send(uint8_t uart_id, uint8_t *data, uint32_t len, uint32_t timeout) {
uart_device_t *dev = &uart_devices[uart_id];
uint32_t tick_start = get_tick();
for (uint32_t i = 0; i < len; i++) {
/* 等待发送缓冲区有空位 */
while (ring_buf_is_full(&dev->tx_rb)) {
if (get_tick() - tick_start > timeout) {
return -1; /* 超时 */
}
}
ring_buf_write(&dev->tx_rb, data[i]);
}
/* 使能发送中断,开始发送 */
dev->instance->CR1 |= USART_CR1_TXEIE;
return 0;
}
接收函数,从环形缓冲区读取数据:
int32_t uart_receive(uint8_t uart_id, uint8_t *buf, uint32_t len, uint32_t timeout) {
uart_device_t *dev = &uart_devices[uart_id];
uint32_t tick_start = get_tick();
uint32_t received = 0;
while (received < len) {
if (ring_buf_read(&dev->rx_rb, &buf[received])) {
received++;
} else {
if (get_tick() - tick_start > timeout) {
return received; /* 返回已接收的字节数 */
}
}
}
return received;
}
4.4 整体架构图
下面这张图,把UART驱动的分层结构和数据流画清楚了。应用层调用标准API,HAL层管理环形缓冲区,底层驱动处理硬件寄存器:
4.5 使用示例
最后,看看实际怎么用。主循环里调用API,清爽得很:
int main(void) {
uart_config_t config = {
.baudrate = 115200,
.data_bits = UART_DATA_8BITS,
.stop_bits = UART_STOP_1BIT,
.parity = UART_PARITY_NONE
};
/* 初始化UART1 */
uart_init(1, &config);
uint8_t recv_buf[64];
while (1) {
/* 接收数据,超时100ms */
int32_t len = uart_receive(1, recv_buf, 64, 100);
if (len > 0) {
/* 收到数据,原样回显 */
uart_send(1, recv_buf, len, 1000);
}
/* 其他任务... */
}
}
你看,应用层完全不知道底层是STM32还是别的芯片。它只管调用uart_init、uart_send、uart_receive。这就是分层封装的价值。