串口通信因其简单可靠、成本低廉的特性,配合RS-485接口更可实现稳定长距离传输,在嵌入式领域应用广泛。随着设备功能日益复杂,系统对实时响应的要求也相应提高。大多数现代微控制器(如ARM7、Cortex-M3系列)的串口模块都集成了硬件FIFO缓冲区。本文将深入探讨如何利用硬件FIFO优化串口通信,在减少接收中断频率的同时,提出一种不依赖发送中断即可提升数据发送效率的实用方法。首先,我们分析传统串口数据收发方式的几点局限性:
- 中断频繁:每接收到一个字节便触发一次中断,未能有效利用硬件FIFO来合并中断,导致CPU频繁响应。
- 发送效率低:采用“查询-等待”方式发送数据时,CPU必须等待当前字节完全发送完毕才能处理下一个字节。在低波特率下(如1200bps),发送几十字节将导致CPU长时间空转,浪费宝贵的计算资源。
- 中断源增加:若采用发送中断方式,虽可解放CPU,但引入了额外的中断源,可能影响系统整体中断管理的稳定性与确定性。
- 缺乏打包机制:接收到的原始字节流需要高效、可靠地还原为具有实际意义的应用层数据帧。
针对以上问题,本文将结合一个实际的自定义通信协议,提供一套完整的软硬件协同优化方案。
串口硬件FIFO工作机制
串口硬件FIFO(先进先出队列)是专为串口数据流设计的缓存区,通常包含独立的接收FIFO和发送FIFO。
- 接收FIFO:串口接收到的数据首先存入此FIFO。当数据量达到预设的触发水平(如1、4、8、14字节),或虽未达到水平但在一段超时时间(如3.5个字符传输时间)内无新数据到达时,硬件才会产生接收中断通知CPU。
- 发送FIFO:CPU将待发送数据写入此FIFO后,只要FIFO非空,硬件便会自动从中取出数据发送,无需CPU持续干预。单次写入FIFO的深度通常有限制(例如16字节)。
合理配置并利用FIFO,可以大幅减少中断次数,提升通信效率。具体配置参数(如触发深度、超时时间)需参考对应芯片的数据手册。
数据接收与协议帧打包
利用接收FIFO的高触发阈值,可以批量处理数据。以NXP LPC1778为例,推荐将接收FIFO触发级别设置为8或14字节。这样,在连续接收数据时,每积累8或14个字节才产生一次中断,显著降低了中断频率。
接收到的原始字节需要根据既定协议打包成完整的应用数据帧。以下是一个典型的自定义串口通信帧格式示例:

- 帧头:连续3-5个固定同步字节(如0xFF或0xEE)。
- 设备地址:1字节,标识目标设备。
- 命令字:1字节,指示帧的功能。
- 数据长度:1字节,指示后续“数据域”的字节数。
- 数据域:长度可变(0~N字节),承载具体信息。
- 校验和:1字节异或校验或2字节CRC16校验,用于确保数据完整性。本例采用CRC16校验。
下面介绍实现该格式帧打包的通用数据结构与算法。
定义帧打包状态结构体
typedef struct {
uint8_t *dst_buf; // 指向最终帧的存储缓冲区
uint8_t sfd; // 帧头标志字节(如0xEE)
uint8_t sfd_flag; // 帧头匹配状态标志
uint8_t sfd_count; // 已连续匹配到的帧头字节数
uint8_t received_len; // 当前已接收到dst_buf中的字节数
uint8_t find_fram_flag; // 完整帧接收成功标志
uint8_t frame_len; // 当前正在处理帧的总长度(通过协议计算得出)
} find_frame_struct;
初始化结构体
此函数应在串口初始化阶段调用。
void init_find_frame_struct(find_frame_struct *p_find_frame, uint8_t *dst_buf, uint8_t sfd) {
p_find_frame->dst_buf = dst_buf;
p_find_frame->sfd = sfd;
p_find_frame->find_fram_flag = 0;
p_find_frame->frame_len = 10; // 默认长度,后续会被实际值覆盖
p_find_frame->received_len = 0;
p_find_frame->sfd_count = 0;
p_find_frame->sfd_flag = 0;
}
核心帧打包函数
该函数在每次串口接收中断中调用,处理新到达的原始数据。
uint32_t find_one_frame(find_frame_struct *p_find_frame,
const uint8_t *src_buf,
uint32_t data_len,
uint32_t sum_len) {
uint32_t src_len = 0;
while (data_len--) {
if (p_find_frame->sfd_flag == 0) {
// 阶段一:寻找帧头
if (src_buf[src_len++] == p_find_frame->sfd) {
p_find_frame->dst_buf[p_find_frame->received_len++] = p_find_frame->sfd;
if (++p_find_frame->sfd_count == 5) { // 假设帧头为5个连续sfd
p_find_frame->sfd_flag = 1; // 进入帧体接收阶段
p_find_frame->sfd_count = 0;
p_find_frame->frame_len = 10; // 重置为默认长度
}
} else {
// 匹配中断,状态清零
p_find_frame->sfd_count = 0;
p_find_frame->received_len = 0;
}
} else {
// 阶段二:接收帧体
// 当接收到“数据长度”字节时,计算整帧长度
if (7 == p_find_frame->received_len) { // 假设长度字节在帧中的位置是第7字节
p_find_frame->frame_len = src_buf[src_len] + 5 + 1 + 1 + 1 + 2; // 帧头+地址+命令+长度+数据+校验
if (p_find_frame->frame_len >= sum_len) {
// 错误处理:帧长超过缓冲区容量
p_find_frame->frame_len = sum_len;
}
}
p_find_frame->dst_buf[p_find_frame->received_len++] = src_buf[src_len++];
// 检查是否已接收完一帧
if (p_find_frame->received_len == p_find_frame->frame_len) {
p_find_frame->received_len = 0;
p_find_frame->sfd_flag = 0;
p_find_frame->find_fram_flag = 1; // 设置帧就绪标志
return src_len; // 返回本次已处理的数据长度
}
}
}
p_find_frame->find_fram_flag = 0;
return src_len;
}
使用示例
// 1. 定义状态结构体和帧缓冲区
find_frame_struct slave_find_frame_srt;
#define SLAVE_REC_DATA_LEN 128
uint8_t slave_rec_buf[SLAVE_REC_DATA_LEN];
// 2. 在串口初始化中初始化
init_find_frame_struct(&slave_find_frame_srt, slave_rec_buf, 0xEE);
// 3. 在串口接收中断服务程序中调用(假设tmp_rec_buf存放新数据,rec_len为新数据长度)
find_one_frame(&slave_find_frame_srt, tmp_rec_buf, rec_len, SLAVE_REC_DATA_LEN);
// 4. 在主循环中检查 find_fram_flag,处理完整的帧
基于定时器与FIFO的高效数据发送方案
传统等待发送法效率低下,而发送中断法会增加系统复杂度。一个折中的高效方案是利用系统中已有的定时器中断,配合发送FIFO进行数据搬运。此方法既不独占CPU,也不新增专用中断源。
工作原理:在定时器中断服务函数中,检查是否有数据待发送。若有,则根据发送FIFO的剩余空间,一次性写入多个字节(例如最多16字节)。写入后,硬件自动发送,CPU可立即退出中断处理其他任务。此方案特别适合中等及以下波特率的通信场景(如1200bps ~ 115200bps),定时器周期建议在1ms~10ms之间。
以下以LPC1778和RS-485接口为例给出实现代码(思想通用)。
定义发送管理结构体
typedef struct {
uint16_t send_sum_len; // 待发送帧的总长度
uint8_t send_cur_len; // 当前已发送的字节数
uint8_t send_flag; // 发送任务激活标志
uint8_t *send_data; // 指向发送数据缓冲区的指针
} uart_send_struct;
定时器中断中的发送处理函数
#define FRAME_SEND_FLAG 0x5A // 自定义的发送任务标志
#define SEND_DATA_NUM 12 // 每次中断尝试填充FIFO的字节数(小于等于FIFO深度)
static void uart_send_com(LPC_UART_TypeDef *UARTx, uart_send_struct *p) {
uint32_t i;
uint32_t tmp32;
// 检查UART发送保持寄存器或FIFO是否为空(可接收新数据)
if (UARTx->LSR & (0x01 << 6)) {
if (p->send_flag == FRAME_SEND_FLAG) {
RS485_SET_TX_MODE(); // 宏:设置RS485为发送模式(如控制DE引脚为高)
tmp32 = p->send_sum_len - p->send_cur_len; // 计算剩余未发送字节数
if (tmp32 > SEND_DATA_NUM) {
// 剩余数据较多,填充SEND_DATA_NUM个字节到发送FIFO
for (i = 0; i < SEND_DATA_NUM; i++) {
UARTx->THR = p->send_data[p->send_cur_len++];
}
} else {
// 发送最后一部分数据
for (i = 0; i < tmp32; i++) {
UARTx->THR = p->send_data[p->send_cur_len++];
}
p->send_flag = 0; // 清除发送标志,本次发送任务完成
// 注意:此时不立刻切换回接收模式,等待FIFO发送完毕
}
} else {
// 无发送任务时,确保RS485处于接收模式
RS485_SET_RX_MODE(); // 宏:设置RS485为接收模式(如控制DE引脚为低)
}
}
}
使用示例
// 1. 定义发送结构体和缓冲区
uart_send_struct uart0_send_str;
#define UART0_SEND_LEN 64
uint8_t uart0_send_buf[UART0_SEND_LEN];
// 2. 对发送函数进行硬件层封装
void uart0_timer_send_handler(void) {
uart_send_com(LPC_UART0, &uart0_send_str);
}
// 将 uart0_timer_send_handler() 函数放入系统已有的定时器中断服务程序中。
// 3. 在应用层需要发送数据时,组织并启动发送任务
void prepare_and_start_tx(uint8_t *data, uint16_t len) {
// 将数据拷贝到发送缓冲区 (uart0_send_buf)
memcpy(uart0_send_buf, data, len);
// 设置发送管理结构体
uart0_send_str.send_sum_len = len;
uart0_send_str.send_cur_len = 0;
uart0_send_str.send_data = uart0_send_buf;
uart0_send_str.send_flag = FRAME_SEND_FLAG; // 激活发送任务
}
总结
本文详细阐述了在嵌入式开发中利用串口硬件FIFO优化数据收发的综合方案。通过提高接收FIFO触发阈值结合自定义协议帧打包,有效降低了接收中断频率;通过利用现有定时器中断驱动发送FIFO,实现了高效、低CPU占用的数据发送。这套方法为资源受限的嵌入式系统提供了一种提升串口通信效率、增强系统实时响应能力的实用思路,尤其适用于对稳定性和效率有较高要求的工业控制、智能设备等领域。