目录
前言
说起队列相信大家或多或少都使用过,像常用的一些操作系统内部都是有集成的,用户可以直接使用,但是如果让我们自己去实现一个队列,应该怎么去做呢?毕竟并不是所有的场景都可以有操作系统。
首先,只是单纯的初始化一个队列是没有任何意义的,它必须搭配相应的使用场景,例如常见的串口收发、DMA数据搬运、消息邮箱等。
为什么要使用队列呢?直接从串口寄存器读取数据保存到全局缓冲区(已usart为例),然后再发送出去,效果不是一样的吗?关于这个问题,浅谈一下个人对队列的理解:
优点:
- 提高程序执行效率;
- 提高代码的可追溯性,便于排查问题;
- 提高系统稳定性(功能解耦、异步处理)
缺点:
- 系统复杂度提高;
- 系统可用性降低
循环队列原理
关于循环队列大家应该知道是怎么回事,用户需要初始化一个数据缓冲区和一个读写位置,每次把需要保存到缓冲的数据压栈(保存到队列),需要用到的时候再从队列里边取出来,如果写入的数据(压栈数据)超过了最大容量,则再从头开始写,读操作与写操作类似(偷了一个图,嘿嘿)。
功能实现
话不多说,直接上代码,这样写的好处是用户可以自定义队列数据类型(类似于邮箱)
// 环形缓冲函数版.c
/**
* @file CircleBuffer.c
* @brief 环形缓冲
* @author ChenXJ
* @version 1.1
* @date 2024-02-22
*/
#include "string.h"
#include "CircleBuffer.h"
/**
* @brief 初始化环形缓冲
*
* @param p 被初始化的环形队列的指针
* @param itemSize 队列中1个元素的大小,单位:字节
* @param base 队列基地址
* @param size 队列最大元素个数
* @return true - 创建成功, false - 创建失败
*/
bool BufferInit(tCircleBuffer *p, unsigned char itemSize, void *base, unsigned int size)
{
if(p == NULL || base == NULL || itemSize == 0 || size == 0)
return false;
p->size = size;
p->rd = 0;
p->wr = 0;
p->pbuf = base;
p->itemSize = itemSize;
return true;
}
/**
* @brief 清空缓冲
*
* @param pdata p 环形缓冲的指针
*
* @return 1为空,0为非空
*/
bool BufferClear(tCircleBuffer *p)
{
if (p == 0)
return 0;
p->rd = p->wr = 0;
return 1;
}
/**
* @brief 检查环形缓冲是否为空
*
* @param pdata p 环形缓冲的指针
*
* @return 1为空,0为非空
*/
bool IsBufferEmpty(tCircleBuffer *p)
{
return p->wr == p->rd;
}
/**
* @brief 检查环形缓冲是否已满
*
* @param pdata p 环形缓冲的指针
*
* @return 1为满,0为未满
*/
bool IsBufferFull(tCircleBuffer *p)
{
return (p->wr + 1) % p->size == p->rd;
}
/**
* @brief 获取环形缓冲容量
*
* @param pdata p 环形缓冲的指针
*
* @return 环形缓冲容量
*/
unsigned int BufferCapacity(tCircleBuffer *p)
{
return p->size;
}
/**
* @brief 获取环形缓冲读取数据个数
*
* @param pdata p 环形缓冲的指针
*
* @return 环形缓冲读取个数
*/
unsigned int BufferNumToRead(tCircleBuffer *p)
{
unsigned int num = 0;
if(p == 0)
return 0;
// if(p->wr >= p->rd)
// {
num = (p->wr + p->size - p->rd) % p->size;
// }
// else
// {
// num = (p->wr + p->size - p->rd) / p->itemSize;
// }
return num;
}
/**
* @brief 从环形缓冲中读取1个数据
*
* @param pdata p 环形缓冲的指针
* @param out 指向读取的数据
*
* @return 1表示读取成功 0表示缓冲为空,读取失败
*/
bool BufferReadOne(tCircleBuffer *p, void *out)
{
if ((p == 0) || (out == 0))
{
return 0;
}
if (IsBufferEmpty(p))
{
return 0;
}
memcpy(out, (unsigned char *)p->pbuf + p->rd * p->itemSize, p->itemSize);
p->rd = (p->rd + 1) % p->size;
return 1;
}
/**
* @brief 从环形缓冲中读取多个数据
*
* @param pdata p 环形缓冲的指针
* @param out 指向读取的数据
* @param len 读取个数
*
* @return 1表示读取成功 0表示缓冲为空,读取失败
*/
bool BufferRead(tCircleBuffer *p, void *out,unsigned short len)
{
unsigned int remain_len;
if ((p == 0) || (out == 0))
{
return 0;
}
if (IsBufferEmpty(p))
{
return 0;
}
if(len > p->size)
{
return 0;
}
remain_len = p->size - p->rd;
if(remain_len > len)
{
memcpy(out, (unsigned char *)p->pbuf + p->rd *p->itemSize, len * p->itemSize);
p->rd = (p->rd + len) % p->size;
}
else
{
memcpy(out,(unsigned char *)p->pbuf + p->rd * p->itemSize,remain_len * p->itemSize);
memcpy((unsigned char *)out+(remain_len * p->itemSize),p->pbuf,(len - remain_len) * p->itemSize);
p->rd = (len - remain_len) % p->size;
}
return 1;
}
/**
* @brief 向环形缓冲写入1个数据
*
* @param pdata p 环形缓冲的指针
* @param in 指向写入的数据
*
* @return 1表示写入成功 0表示缓冲已满,写入失败
*/
bool BufferWriteOne(tCircleBuffer *p, void *in)
{
if ((p == 0) || (in == 0))
{
return 0;
}
if (IsBufferFull(p))
{
return 0;
}
memcpy((unsigned char *)p->pbuf + p->wr * p->itemSize, in, p->itemSize);
p->wr = (p->wr + 1) % p->size;
return 1;
}
/**
* @brief 向环形缓冲写入多个数据
*
* @param pdata p 环形缓冲的指针
* @param in 指向写入的数据
* @param len 写入个数
*
* @return 1表示写入成功 0表示缓冲已满,写入失败
*/
bool BufferWrite(tCircleBuffer *p, void *in,unsigned short len)
{
unsigned int remain_len;
if ((p == 0) || (in == 0))
{
return 0;
}
if (IsBufferFull(p))
{
return 0;
}
if(len > p->size)
{
return 0;
}
remain_len = p->size - p->wr;
if(remain_len > len)
{
memcpy((unsigned char *)p->pbuf + p->wr * p->itemSize, in, (len * p->itemSize));
p->wr = (p->wr + len) % p->size;
}
else
{
memcpy((unsigned char *)p->pbuf + p->wr * p->itemSize,in,(remain_len * p->itemSize));
memcpy(p->pbuf,(unsigned char *)in + (remain_len * p->itemSize),(len - remain_len) * p->itemSize);
p->wr = (len - remain_len) % p->size;
}
return 1;
}
队列类型如下:
//环形缓冲.h
/**
* @file CircleBuffer.h
* @brief 环形缓冲
* @author ChenXJ
* @version 1.1
* @date 2024-02-22
*/
#ifndef _CIRCLE_BUFFER_H_
#define _CIRCLE_BUFFER_H_
#include "stdbool.h"
#include "stdint.h"
/// 环形缓冲结构体
typedef struct _cyclebuffer
{
unsigned int size; ///< 环形缓冲大小,为环形缓冲能存储数据的个数
unsigned int rd; ///< 从环形缓冲读取数据的位置
unsigned int wr; ///< 向环形缓冲写数据的位置
void *pbuf; ///< 指向环形缓冲区的指针
unsigned char itemSize; ///< 1个数据的大小,单位:字节
}tCircleBuffer;
/**
* @brief 初始化环形缓冲
*
* @param p 被初始化的环形缓冲的指针
* @param itemSize 缓冲中1个数据的大小,单位:字节
* @param buf 环形缓冲的缓冲区
* @param bufSize 缓冲区的大小,单位:字节
*/
bool BufferInit(tCircleBuffer *p, unsigned char itemSize, void *buf, unsigned int bufSize);
/**
* @brief 清空缓冲
*
* @param pdata p 环形缓冲的指针
*
* @return 1为空,0为非空
*/
bool BufferClear(tCircleBuffer *p);
/**
* @brief 检查环形缓冲是否为空,如果缓冲的大小为0,认为缓冲为空
*
* @param pdata p 环形缓冲的指针
*
* @return 1为空,0为非空
*/
bool IsBufferEmpty(tCircleBuffer *p);
/**
* @brief 检查环形缓冲是否已满,如果缓冲的大小为0,认为缓冲为满
*
* @param pdata p 环形缓冲的指针
*
* @return 1为满,0为未满
*/
bool IsBufferFull(tCircleBuffer *p);
/**
* @brief 获取环形缓冲容量
*
* @param pdata p 环形缓冲的指针
*
* @return 环形缓冲容量
*/
unsigned int BufferCapacity(tCircleBuffer *p);
/**
* @brief 获取环形缓冲读取数据个数
*
* @param pdata p 环形缓冲的指针
*
* @return 环形缓冲读取个数,单位:p->itemSize
*/
unsigned int BufferNumToRead(tCircleBuffer *p);
/**
* @brief 从环形缓冲中读取一个数据
*
* @param pdata p 环形缓冲的指针
* @param out 指向读取的数据
*
* @return 1表示读取成功 0表示缓冲为空,读取失败
*/
bool BufferReadOne(tCircleBuffer *p, void *out);
/**
* @brief 从环形缓冲中读取多个数据
*
* @param pdata p 环形缓冲的指针
* @param out 指向读取的数据
* @param len 读取数据个数
*
* @return 1表示读取成功 0表示缓冲为空,读取失败
*/
bool BufferRead(tCircleBuffer *p, void *out,unsigned short len);
/**
* @brief 向环形缓冲写入一个数据
*
* @param pdata p 环形缓冲的指针
* @param in 指向写入的数据
*
* @return 1表示写入成功 0表示缓冲已满,写入失败
*/
bool BufferWriteOne(tCircleBuffer *p, void *in);
/**
* @brief 向环形缓冲写入多个数据
*
* @param pdata p 环形缓冲的指针
* @param in 指向写入的数据
* @param len 写入数据个数
*
* @return 1表示写入成功 0表示缓冲已满,写入失败
*/
bool BufferWrite(tCircleBuffer *p, void *in,unsigned short len);
#endif
代码亲测可用,大家可自行参考,如有不足之处还请提出,不胜感激!
例程(串口收发)
关于程序例程在这里不再赘述,主要以伪代码的方式说明循环队列的使用。
先说串口接收,正常情况下,串口接收大都是通过接收完成中断取一个字节,然后把数据放到接收缓冲区里,等到空闲中断(如果有)到来时再做数据解析。这是理想情况下,这么做确实没问题。但作为软件工程师应该考虑各种极端情况,如果串口接收了一半数据掉电了怎么办?串口从机程序阻塞,一包数据不是连续发送且不定长的话怎么办?(当然,如果是这种情况,这里还需要进行“拼包”),每次从中断中读完数据,处理完之后再清缓冲,再去读,当数据吞吐量足够大的时候,这也是一笔不小的时间开销。如果使用队列进行处理,我们只需要在接收到数据的时候压栈,当空闲中断(如果有)到来的时候从队列里读取数据解析(因为是循环队列,并不需要清理缓冲),哪怕有一次或几次空闲中断没有进入也没有关系(有溢出风险!),只要接收中断可以正常接收数据,当我从队列读数据时,一样可以把之前的数据全部读出来,并且可以通过队列缓冲更加直观的看到数据接收情况,便于排查问题。(如果没有空闲中断,也可以通过定时查询的方式读取队列)
串口发送有查询发送和中断发送两种方式,一般情况下,当数据吞吐量较小的时候两种发送区别不大,但是当有大量数据处理或者系统实时性要求较为严格时,中断发送还是很有必要的,毕竟中断调度的优先级要高于代码的优先级。查询式发送需要等待发送完成标志(虽然这段时间很快,但却是绝对阻塞的),中断发送却可以避免这个问题,发送完成标志通过中断处理,不需要用户的代码中存在阻塞的情况(同样的功能实现,执行效率更高的代码就是更好的代码,在面对功能需求的时候,实现功能是基础,在此基础上写出更高质量的代码才应该是我们的追求),那么中断发送时如何和队列结合起来?stm32标准库里边中断发送每次都是一个字节,怎么发送多字节呢?这个在hal库里边其实是有实现方式的,如果你看过hal库的发送接口,就不难发现其实它就是通过队列实现的。大概思路就是:先初始化一个队列,当用户需要发送数据的时候先压栈,同时打开发送中断,在中断服务中从队列中读数据,直到写入的数据发送完成,再关闭中断。试想一下这样实现的好处是什么?这样做的好处就是做到了发送和接收异步处理!!!这个很重要,这两个人线程互不干扰,发送和接收都是通过队列实现,省去了清缓存等一系列操作。用户只管把要发送的数据丢给发送队列,剩下的发送任务交给中断(功能解耦是模块化的前提,在多线程任务中尤为重要)
使用例程r如下:
uint8_t queue_rcv_buf[100];
uint8_t queue_snd_buf[100];
buffer_t p1,p2;
typedef struct {
uint8_t byte;
uint8_t flg;
}rcv_msg_t;
rcv_msg_t rcv_msg;
void it_handle(void)
{
if(txe)
{
clear;
uint32_t len = BufferNumToRead(&p2);
if(len) //有数据要发送
{
uint8_t byte = 0;
BufferReadOne(&p2,&byte);
usart->DR = byte;
}
else
{
disable_it_txe; //没有数据要发送的时候关闭发送中断
}
}
else if(rxne) //接收中断
{
clear;
rcv_msg.byte = usart->DR;
BufferWriteOne(&p1,&rcv_msg.byte);
}
else if(idle) //空闲中断
{
clear;
rcv_msg.flg = 1;
}
}
void init(void)
{
usart_init();
BufferInit(&p1,1,queue_rcv_buf,100);
BufferInit(&p2,1,queue_snd_buf,100);
}
//数据处理任务
void loop(void)
{
uint8_t recvbuf[x] = {0}; //接收到的数据
uint8_t sendbuf[x] = {0}; //要发送的数据
uint16_t len1 = 0,len2 = 0;
#if 有空闲中断
if(rcv_msg.flg)
{
rcv_msg.flg = 0;
len1 = BufferNumToRead(&p1); //获取queue长度
BufferRead(&p1,recvbuf,len1); //读数据
handle(recvbuf,len1); //数据解析
}
#else
(每10ms执行一次) //时间自定义
{
len1 = BufferNumToRead(&p1); //获取queue长度
BufferRead(&p,recvbuf,len1); //读数据
handle(recvbuf,len1); //数据解析
}
#endif
//需要通过串口发送数据
BufferWrite(&p2,sendbuf,len2);
enable_it_txe; //开启发送中断
}
写在最后
文章皆自原创,如因此对各位有所启发,实属我幸;当然,如有不恰当的地方,也欢迎看官提出,不胜感激!