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

3861

积分

0

好友

505

主题
发表于 前天 23:28 | 查看: 16| 回复: 0

简介

串口以其使用便捷、成本低廉的特点,配合 RS485 芯片能轻松搭建远距离、抗干扰的局域网络,应用极为广泛。随着产品功能日益复杂,MCU 需要处理的任务激增,系统实时响应能力成了关键。绝大多数现代单片机(如 ARM7、Cortex-M3 系列)的串口外设都自带了硬件 FIFO 缓冲区。如果能充分利用它,在减少中断频率的同时提升收发效率,不就能一举两得了吗?在深入探讨之前,不妨先瞧瞧传统串口数据交互的几个短板:

  • 每接收一字节便触发一次中断,完全无视硬件 FIFO 的存在,中断次数居高不下。
  • 应答时采用阻塞式等待发送。串行传输的耗时远慢于 CPU 的处理速度,等待单字节发完再送下一个,无疑是对 CPU 资源的巨大浪费,系统整体响应变得迟钝(以 1200bps 为例,发送一字节约耗时 10ms,若连发几十个字节,CPU 就长时间空等)。
  • 应答时采用中断发送。虽不浪费 CPU,却额外增加了中断源,整体中断事件变多,从系统可靠性角度看,中断源自然是越精简越好。

针对这些痛点,本文将结合一种常用的自定义通讯协议,给出一个从底层收发到上层帧打包的完整优化方案。

深入串口 FIFO

串口 FIFO 本质上是为串口定制的硬件缓存,遵循先进先出原则。接收与发送 FIFO 通常是两个独立的硬件单元。数据进入串口后,会先暂存于接收 FIFO;当 FIFO 内数据量达到设定阈值(常见为 1、2、4、8、14 字节)或虽未满但在约 3.5 个字符的传输时间内再无新数据,硬件便通知 CPU 产生一次接收中断。发送时,只需把数据写入发送 FIFO,只要 FIFO 非空,硬件就会自动将数据逐个移出并发送,CPU 无须干预。单次写入发送 FIFO 的字节数受其最大深度限制,通常不超过 16 字节。以上参数依芯片而异,开发前查阅对应数据手册是必不可少的步骤。

数据接收与高效打包

FIFO 能够缓存接收到的数据,这为我们降低中断频率提供了可能。以 NXP 的 lpc1778 芯片为例,其接收 FIFO 触发级别可配置为 1、2、4、8、14 字节,建议设为 8 或 14 字节,这也是 PC 串口接收 FIFO 的常见默认值。如此一来,连续接收大量数据时,每满 8 或 14 字节才产生一次中断(最后一包除外),中断次数相比单字节触发模式将大幅锐减。配置方式非常简单,在 lpc1778 上只需操作 UART FIFO 控制寄存器 UnFCR 即可。

接收数据必须遵循既定的通讯协议,二者相辅相成。通常我们需要根据协议将接收到的数据打包成完整的一帧,再递交给上层逻辑处理。下面展示一套自定义协议帧格式,并提供通用的成帧方法。

自定义协议格式如下图所示:

自定义协议帧格式表格,包含帧首、地址号、命令号、长度、数据、校验列

  • 帧首:一般为 3~5 个 0xFF0xEE
  • 地址号:待通讯设备的地址编号,占 1 字节
  • 命令号:用于区分不同功能,占 1 字节
  • 长度:数据区的字节数,占 1 字节
  • 数据:与具体命令号相关,数据区长度可为 0,整帧长度不应超过 256 字节
  • 校验:可采用异或和校验(1 字节)或 CRC16 校验(2 字节),本例使用 CRC16

下文详细介绍如何将接收到的数据按此格式打包。

1. 定义数据结构

typedef struct
{
    uint8_t * dst_buf;                  //指向接收缓存
    uint8_t sfd;                       //帧首标志,为0xFF或者0xEE
    uint8_t sfd_flag;                  //找到帧首,一般是3~5个FF或EE
    uint8_t sfd_count;                 //帧首的个数,一般3~5个
    uint8_t received_len;              //已经接收的字节数
    uint8_t find_fram_flag;            //找到完整帧后,置1
    uint8_t frame_len;                 //本帧数据总长度,这个区域是可选的
}find_frame_struct;

2. 初始化数据结构,一般放在串口初始化中

/**
 * @brief    初始化寻找帧的数据结构
 * @param    p_fine_frame:指向打包帧数据结构体变量
 * @param    dst_buf:指向帧缓冲区
 * @param    sfd:帧首标志,一般为0xFF或者0xEE
 */
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;
}

3. 数据打包程序

/**
 * @brief    寻找一帧数据  返回处理的数据个数
 * @param    p_find_frame:指向打包帧数据结构体变量
 * @param    src_buf:指向串口接收的原始数据
 * @param    data_len:src_buf本次串口接收到的原始数据个数
 * @param    sum_len:帧缓存的最大长度
 * @return   本次处理的数据个数
 */
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)
                {
                    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
        {   //是否是"长度"字节? Y->获取这帧的数据长度
            if(7==p_find_frame ->received_len)
            {
                p_find_frame->frame_len=src_buf[src_len]+5+1+1+1+2; //帧首+地址号+命令号+数据长度+校验      
                if(p_find_frame->frame_len>=sum_len)
                {   //这里处理方法根据具体应用不一定相同
                    MY_DEBUGF(SLAVE_DEBUG,("数据长度超出缓存!\n"));
                    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;
}

使用例子:

定义数据结构体变量:

find_frame_struct slave_find_frame_srt;

定义接收数据缓冲区:

#define SLAVE_REC_DATA_LEN  128
uint8_t slave_rec_buf[SLAVE_REC_DATA_LEN];

在串口初始化中调用结构体变量初始化函数:

init_find_frame_struct(&slave_find_frame_srt,slave_rec_buf,0xEE);

在串口接收中断中调用数据打包函数:

find_one_frame(&slave_find_frame_srt,tmp_rec_buf,data_len,SLAVE_REC_DATA_LEN);

其中,tmp_rec_buf 是串口接收临时缓冲区,data_len 是本次接收的数据长度。

数据发送优化

如前文所述,传统等待发送浪费 CPU 资源,中断发送则增加中断源。在实际开发中,定时器中断几乎是每套系统都会用到的,我们完全可以借助定时器中断和硬件 FIFO 来驱动数据发送。经过合理设计,这套方案既能避免 CPU 空转,又不必新增中断源。不过,这种方法并非万能。如果你的应用根本没有开启任何定时中断,那自然无法使用;若定时中断的间隔较长而通讯波特率又极高,同样不太适用。这类场景下,还是需要回归 后端 & 架构 设计原则,根据吞吐需求在系统层面做好整体规划。我们目前项目常用的波特率较低(1200bps、2400bps),在此条件下,定时器间隔保持在 10ms 以下即可满足要求;若间隔缩短至 1ms 以下,理论上 115200bps 也能胜任。

本方案的核心思想是:每次定时器中断触发时,检查是否有待发数据。若有且满足发送条件,就将数据填入发送 FIFO(以 lpc1778 为例,一次最多 16 字节)。随后硬件会自动启动发送流程,CPU 无需再操心。下面以 RS485 总线的 lpc1778 为例介绍,代码与硬件操作强相关,但设计思路可以通用。

1. 定义数据结构

/*串口帧发送结构体*/
typedef struct
{
    uint16_t send_sum_len;          //要发送的帧数据长度
    uint8_t  send_cur_len;          //当前已经发送的数据长度
    uint8_t  send_flag;             //是否发送标志
    uint8_t * send_data;            //指向要发送的数据缓冲区
}uart_send_struct;

2. 定时处理函数

/**
 * @brief    定时发送函数,在定时器中断中调用,不使用发送中断的情况下减少发送等待
 * @param    UARTx:指向硬件串口寄存器基地址
 * @param    p:指向串口帧发送结构体变量
 */
#define FARME_SEND_FALG 0x5A          
#define SEND_DATA_NUM   12
static void uart_send_com(LPC_UART_TypeDef *UARTx,uart_send_struct *p)
{
    uint32_t i;
    uint32_t tmp32;

    if(UARTx->LSR &(0x01<<6))                      //发送为空
    {        
        if(p->send_flag==FARME_SEND_FALG)
        {                          
            RS485ClrDE;                             // 置485为发送状态

            tmp32=p->send_sum_len-p->send_cur_len;
            if(tmp32>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;                     
            }
        }
        else
        {
            RS485SetDE;
        }
    }
}

其中,RS485ClrDE 为宏定义,将 RS485 设置为发送模式;RS485SetDE 同为宏定义,设置为接收模式。

使用例子:

定义数据结构体变量:

uart_send_struct uart0_send_str;

定义发送缓冲区:

uint8_t uart0_send_buf[UART0_SEND_LEN];

根据具体硬件串口,对定时处理函数进行二次封装:

void uart0_send_data(void)
{
    uart_send_com(LPC_UART0,&uart0_send_str);
}

将封装函数 uart0_send_data() 放入定时器中断服务函数中。在需要发送数据的位置,设置串口帧发送结构体变量:

uart0_send_str.send_sum_len=data_len;      //data_len为要发送的数据长度
uart0_send_str.send_cur_len=0;             //固定为0
uart0_send_str.send_data=uart0_send_buf;   //绑定发送缓冲区
uart0_send_str.send_flag=FARME_SEND_FALG;  //设置发送标志

总结

本文完整地探讨了一套高效的串口数据收发策略,并附上了具体 C 代码实现。在 MCU 需并行处理繁杂任务的当下,这套方法占用资源极少,不失为一种提升系统整体响应性能的新思路。如果你也在嵌入式开发中追求更优雅的中断管理与数据吞吐,欢迎来 云栈社区 和众多开发者一起挖掘更多底层优化技巧。




上一篇:IC内部结构全解析:从I/O端口到ESD防护的80页深度笔记
下一篇:Claude Code Artifacts 发布:终端生成实时交互网页,一键分享协作
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-6-21 10:43 , Processed in 0.592630 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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