在嵌入式系统开发中,串口(UART)是最基础也是最常用的通信方式之一。无论是用于调试信息的打印、与外设通信,还是与主控模块的数据交互,一个稳定可靠、结构清晰的串口通信模块都是不可或缺的。
介绍一个基于 STM32F4 系列微控制器实现的串口通信模块,该模块采用环形缓冲区结构,并结合中断机制,实现了非阻塞、缓存式的数据收发。整体设计思路清晰、逻辑模块化,适合在嵌入式项目中直接复用。
模块结构概览
本模块主要由两个部分组成:
- 串口驱动模块(tty.c)
负责 UART 的初始化、收发控制与中断服务处理。 - 环形缓冲区模块(ringbuffer.c)
提供通用的循环数据缓存接口,实现数据的无损、非阻塞读写。
这种设计将通信协议与缓存机制分离,提升了系统的可维护性与移植性。
基本数据结构设计
本模块的核心是一个 ring_buf_t 类型,其内部应定义如下字段(见 ringbuffer.h):
typedef struct {
unsigned char *buf; // 实际数据缓冲区
unsigned int size; // 缓冲区总大小(必须为2的幂)
unsigned int front; // 数据读取指针
unsigned int rear; // 数据写入指针
} ring_buf_t;
设计约束:缓冲区大小必须为 2 的整数次幂。
这是为了优化环形地址 wrap-around 操作,使用按位与(&)替代取模运算。
函数接口说明
初始化与清空
bool ring_buf_init(ring_buf_t *r, unsigned char *buf, unsigned int len);
初始化一个空的环形缓冲区,要求 len 是 2 的幂,返回值表示初始化成功与否。
void ring_buf_clr(ring_buf_t *r);
将读写指针归零,清空所有数据。
数据写入
unsigned int ring_buf_put(ring_buf_t *r, unsigned char *buf, unsigned int len);
将外部数据 buf 写入到环形缓冲区中。若缓冲区剩余空间不足,则只写入能容纳的部分。返回实际写入长度。
关键点:
- 支持跨缓冲区尾部写入(wrap-around);
- 写入操作不会覆盖未读数据;
- 使用 rear 指针更新写入位置。
数据读取
unsigned int ring_buf_get(ring_buf_t *r, unsigned char *buf, unsigned int len);
从环形缓冲区读取数据至 buf,若请求数据超出已有长度,仅读取实际可用部分。返回值为实际读取字节数。
关键点:
- 支持跨缓冲区尾部读取;
- 读取数据后 front 指针更新;
- 数据一旦读取即“消费”,不可重复读取。
获取当前数据长度
unsigned int ring_buf_len(ring_buf_t *r);
返回当前缓冲区中已存数据长度(rear - front)。注意该实现默认读写指针不断增加,不会回绕,即 unsigned int 类型下支持最大 4G 字节空间。
性能优化点
- 位操作替代模运算:
缓冲区大小为 2 的幂时,可用 & (size - 1) 快速计算 wrap-around 的实际索引位置,减少 CPU 开销。
r->rear & (r->size - 1) // 相当于 r->rear % r->size
- 双段 memcpy 提高吞吐:
为处理尾部 wrap 情况,写入和读取都拆分成两个 memcpy(),分别处理尾部和头部两段。
一、串口收发的关键设计思想
1. 接收与发送分离
通过 USART1_IRQHandler 中断服务函数分别处理 接收中断 和 发送中断,每次接收到数据就放入接收缓冲区(rxbuf),每次发送缓冲区中有数据就启动发送中断。这样设计的优点是:
- 接收及时不中断,防止数据丢失;
- 发送自动控制,避免频繁轮询;
- 系统主循环更加干净清晰。
2. 非阻塞缓冲机制
通过自定义结构 ring_buf_t,配合 ring_buf_put 与 ring_buf_get,实现了一个灵活的环形数据缓冲区。相比一次性收发固定数据,这种缓存机制更具鲁棒性,适合串口波动大、数据密集或通信速率不一致的场合。
二、环形缓冲区的应用价值
环形缓冲区(Ring Buffer)是一种“循环”的数据结构,空间开销小、速度快,非常适合嵌入式实时系统中对性能要求高的通信模块。
在串口收发中,它的典型作用包括:
- 解决串口收发异步性问题,接收与处理分离;
- 支持可变长度数据帧的缓冲处理;
- 与中断或DMA天然契合,避免主线程阻塞;
- 数据临时缓存,保障高并发场景的数据完整性。
三、统一串口接口设计
为了提高代码复用性,模块中使用了一个结构体 tty_t 对串口操作进行统一抽象,包括:
- 串口初始化函数;
- 发送数据接口;
- 接收数据接口;
- 缓冲状态判断函数(是否满、是否空);
通过将这些函数指针封装在结构体中,可以非常方便地实现“控制台接口”或多串口同时支持,只需更换硬件配置部分即可。
const tty_t tty = {
uart_init,
uart_write,
uart_read,
tx_isfull,
tx_isempty,
rx_isempty
};
这种设计方式值得推广到其他如 SPI、CAN、I2C 等通信模块上,实现统一接口调用,提升代码一致性。
四、典型应用场景
这个串口收发模块适合嵌入式项目中的以下典型场景:
- 设备调试打印:串口作为 printf 的输出设备,缓存打印内容,防止打印阻塞主循环。
- 与上位机通讯:通过串口接收指令、发送响应数据,配合协议帧解析模块构成完整通讯链路。
- 传感器数据采集:将高频率传感器的串口数据接收后缓存,主线程按需读取处理。
- 工业控制通信:对实时性要求高,使用环形缓冲区和中断机制可避免数据积压。
五、设计优点总结
- 模块化清晰:缓存模块与串口驱动分离,便于独立调试、复用。
- 性能稳定:中断驱动 + 缓冲机制,避免数据丢失。
- 扩展灵活:支持任意大小的缓存、多个串口实例。
- 移植方便:与具体芯片无强耦合,适合在不同 STM32 系列中复用。
六、推荐使用方式
建议将此模块封装为标准组件,并在上层封装为串口服务层,例如:
tty.uart_init(115200);
tty.uart_write("Hello World", 11);
上层应用只需调用接口函数,无需关注底层缓冲逻辑与中断机制,提高应用开发效率。
七、后续可拓展方向
- 支持 DMA 模式收发,进一步提升数据吞吐;
- 加入帧协议解析支持(如 Modbus、自定义帧);
- 增加线程/RTOS安全访问控制;
- 缓冲区动态分配与多通道管理。
结语
一个好的串口模块设计,往往是嵌入式系统稳定运行的基础。本文介绍的环形缓冲机制与中断控制结合的串口收发架构,具有良好的通用性、扩展性与实际工程适用性,值得在项目中加以实践与改进。
如你也在做基于 STM32 的嵌入式项目,这套结构可以帮助你快速搭建一个健壮、可扩展的串口通信模块。
开源代码:
#include "ringbuffer.h"
#include <string.h>
#include <stddef.h>
#define min(a,b) ( (a) < (b) )? (a):(b)
/*
*@brief 构造一个空环形缓冲区
*@param[in] r - 环形缓冲区管理器
*@param[in] buf - 数据缓冲区
*@param[in] len - buf长度(必须是2的N次幂)
*@retval bool
*/
bool ring_buf_init(ring_buf_t *r,unsigned char *buf, unsigned int len)
{
r->buf = buf;
r->size = len;
r->front = r->rear = 0;
return buf != NULL && (len & len -1) == 0;
}
/*
*@brief 清空环形缓冲区
*@param[in] r - 待清空的环形缓冲区
*@retval none
*/
void ring_buf_clr(ring_buf_t *r)
{
r->front = r->rear = 0;
}
/*
*@brief 获取环形缓冲区数据长度
*@retval 环形缓冲区中有效字节数
*/
unsigned int ring_buf_len(ring_buf_t *r)
{
return r->rear - r->front;
}
/*
*@brief 将指定长度的数据放到环形缓冲区中
*@param[in] buf - 数据缓冲区
* len - 缓冲区长度
*@retval 实际放到中的数据
*/
unsigned int ring_buf_put(ring_buf_t *r,unsigned char *buf,unsigned int len)
{
unsigned int i;
unsigned int left;
left = r->size + r->front - r->rear;
len = min(len , left);
i = min(len, r->size - (r->rear & r->size - 1));
memcpy(r->buf + (r->rear & r->size - 1), buf, i);
memcpy(r->buf, buf + i, len - i);
r->rear += len;
return len;
}
/*
*@brief 从环形缓冲区中读取指定长度的数据
*@param[in] len - 读取长度
*@param[out] buf - 输出数据缓冲区
*@retval 实际读取长度
*/
unsigned int ring_buf_get(ring_buf_t *r,unsigned char *buf,unsigned int len)
{
unsigned int i;
unsigned int left;
left = r->rear - r->front;
len = min(len , left);
i = min(len, r->size - (r->front & r->size - 1));
memcpy(buf, r->buf + (r->front & r->size - 1), i);
memcpy(buf + i, r->buf, len - i);
r->front += len;
return len;
}