找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1526

积分

0

好友

222

主题
发表于 6 天前 | 查看: 22| 回复: 0

串口通信因其简单可靠、成本低廉的特性,配合RS-485接口更可实现稳定长距离传输,在嵌入式领域应用广泛。随着设备功能日益复杂,系统对实时响应的要求也相应提高。大多数现代微控制器(如ARM7、Cortex-M3系列)的串口模块都集成了硬件FIFO缓冲区。本文将深入探讨如何利用硬件FIFO优化串口通信,在减少接收中断频率的同时,提出一种不依赖发送中断即可提升数据发送效率的实用方法。首先,我们分析传统串口数据收发方式的几点局限性:

  1. 中断频繁:每接收到一个字节便触发一次中断,未能有效利用硬件FIFO来合并中断,导致CPU频繁响应。
  2. 发送效率低:采用“查询-等待”方式发送数据时,CPU必须等待当前字节完全发送完毕才能处理下一个字节。在低波特率下(如1200bps),发送几十字节将导致CPU长时间空转,浪费宝贵的计算资源。
  3. 中断源增加:若采用发送中断方式,虽可解放CPU,但引入了额外的中断源,可能影响系统整体中断管理的稳定性与确定性
  4. 缺乏打包机制:接收到的原始字节流需要高效、可靠地还原为具有实际意义的应用层数据帧。

针对以上问题,本文将结合一个实际的自定义通信协议,提供一套完整的软硬件协同优化方案。

串口硬件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占用的数据发送。这套方法为资源受限的嵌入式系统提供了一种提升串口通信效率、增强系统实时响应能力的实用思路,尤其适用于对稳定性和效率有较高要求的工业控制、智能设备等领域。




上一篇:MSF框架利用永恒之蓝漏洞实战复现:Windows SMB漏洞扫描与利用教程
下一篇:Spring Boot实现MCP Server与工具调用:解决Flux流式编程中LLM递归调用难题
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-24 21:10 , Processed in 0.401474 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表