1. 环形队列
队列就是一种先入先出的数据结构。实际应用中,譬如按键检测,把按键信息存入环形队列,然后,在循环中再读出进行处理。环形队列的元素,不一定是简单的字节数据,也可以是抽象的结构体数据,譬如按键的事件信息结构体,里面包含按键的ID,键值等等。
结合到动态分配,可以将队列的长度进行灵活的设置。环形队列的结构体如下:
typedef struct {
uint8_t * fifo; // 环形fifot头地址
uint16_t size; // 环形fifo大小
uint16_t pWrite; // 写位置
uint16_t pRead; // 读位置
}ringBuffer,* pRingBuffer;
抽象出这个结构体目的为了指定操作对象,本质是对里面的元素fifo指向的数组进行操作,pWrite指向待写入的数据位置,pRead指向待读出的数据位置。这里需要注意,pWrite指向的地方,还没有数据写入,实际操作过程中,如果队列有空间,则先写入数据,再把pWrite位置后移。pRead指向的位置,已经写入了数据,读出该有效数据之后,再把pRead位置后移。故读和写,都是先操作数据,然后再操作位置。
队列为空的条件为 读和写的位置相同。队列为满的条件,是写的位置处于读的位置后面,及(pWrite+1)%队列长度 == pRead , 这里需要说明,为了避免读写位置相等时候,是队列满还是空的歧义,才指定上述的队列满的条件。以此指导,队列的大小如果为N,则队列里面最大只能够储存N-1个数据。相当于牺牲了一个存储空间。环形队列的初始化函数如下:
int driver_ringFIFO_Init(pRingBuffer pBuffer, uint16_t size)
{
if((pBuffer==NULL)||(size==0)) return -1;
pBuffer->size = size;
pBuffer->fifo = (uint8_t *)malloc(pBuffer->size); // 包含于stdlib.h
pBuffer->pWrite = 0;
pBuffer->pRead = 0;
return 0;
}
写入1个字节的函数:
int driver_ringFIFO_writeByte(pRingBuffer pBuffer, uint8_t dat)
{
if((pBuffer==NULL)||(pBuffer->fifo==NULL)) return -1;
if((pBuffer->pWrite+1)%pBuffer->size == pBuffer->pRead)
{
return -2; // fifo满了,无法写入
}
else
{
pBuffer->fifo[pBuffer->pWrite] = dat;
pBuffer->pWrite = (pBuffer->pWrite + 1)%pBuffer->size; // 移动到写一个待写入位置
}
return 0;
}
读出1个字节的函数:
int driver_ringFIFO_readByte(pRingBuffer pBuffer, uint8_t * dat)
{
if((pBuffer==NULL)||(pBuffer->fifo==NULL)||(dat==NULL)) return -1;
if(pBuffer->pWrite == pBuffer->pRead)
{
return -2; // fifo空了,无法读出数据
}
else
{
*dat = (pBuffer->fifo[pBuffer->pRead]);
pBuffer->pRead = (pBuffer->pRead + 1)%pBuffer->size; // 移动到写一个待读出位置
}
return 0;
}
读或者写多个字节的数据,就是调用上述读写1个字节的函数,不再赘述。
2. 按键检测
不同于以前的软件延时,这次视频中的方法配合了 外部中断和定时器中断的方法。当按键按下或者弹起的时候,会有多次的整栋,如果此时按键对应的IO开启了上升沿和下降沿触发的中断,会触发多次中断。开一个变量记录触发时间,每次中断的时候,就更新触发时间,触发时间往后加50ms(也可以是其它值)相当于按键检测判断的时刻,在定时器中断中,不断的检测时间,如果时间等于按键检测判断时间,则读按键的电平状态,如果是0,则是按下,记录按下时刻。如果是1,则是弹起,记录弹起时刻。这两个时间都不为0,则发生了一次按键按下弹起的事件。故可以抽象一个按键事件的结构体:
typedef struct _keyEvent
{
uint16_t ID; // 按键ID
uint16_t time; // 按键持续时间
}KeyEvent,*pKeyEvent;
其实按键结构体可以根据实际,有更加复杂的元素。譬如可以加入按键处理的回调函数,如加入函数指针。当然也可以定义一个虚函数,作为有按键事件的处理函数。
结合上面的环形队列,可以定义一个按键事件的对象:
static ringBuffer keyEventFIFO={0}; // KeyEventFIFO对象
大小定义可以保存16个按键事件的结构体
driver_ringFIFO_Init(&keyEventFIFO, sizeof(KeyEvent)<<4); // 16个 KeyEvent结构体的大小
这里需要说明,实际中无法正好同时保存16个按键事件,原因是虽然开辟了16*4 = 64个字节的空间,但是只能够保存63个数据,队列就满了。当有按键事件时候,写入环形队列:
void driver_key_tickScan(void)
{
KeyEvent keyPressEvent = {0};
static uint32_t keyDownTick = 0; // 按键按下的tick
static uint32_t keyUpTick = 0; // 按键松开的tick
uint32_t temp = 0;
temp = HAL_GetTick();
if(keyTriggerTick==temp)
{
if(0==HAL_GPIO_ReadPin(KEY_GPIO_PORT,KEY_GPIO_PIN)) // 按键按下
{
keyDownTick = temp;
}
else // 按键松开
{
keyUpTick = temp;
}
if((keyUpTick!=0)&&(keyDownTick!=0))
{
keyPressEvent.ID = 'A'; // 按键ID
keyPressEvent.time = keyUpTick - keyDownTick;
keyUpTick = keyDownTick = 0; // 清零,方便下次使用
driver_ringFIFO_writeNBytes(&keyEventFIFO, (uint8_t *)&keyPressEvent.ID, sizeof(keyPressEvent));
}
}
}
该按键扫描函数,可以放入1ms的定时器中断中,如systick中断中。上面写入环形队列的函数中,传入的是结构体第一个元素的地址,也可以是结构体的地址,每次大小就是结构体的大小。
大循环中执行的函数如下:
/* 放到大循转之中,按键处理函数 */
void driver_key_exe(void)
{
KeyEvent keyPressEvent = {0};
// 1. 按键队列中取出按键事件
driver_ringFIFO_readNBytes(&keyEventFIFO, (uint8_t *)&keyPressEvent, sizeof(keyPressEvent));
// 2. 执行按键处理函数
driver_key_Callback(&keyPressEvent);
}
这里看到读出函数是将环形队列中的数据,整体读出到结构体中,这样非常方便。最后调用按键处理函数,并将刚才读出的按键信息传入。该回调函数定义为虚函数,方便用户自己实现具体内容:
/* 按键处理函数 */
__weak void driver_key_Callback(KeyEvent * pKeyEvent)
{
UNUSED(pKeyEvent);
}
这样子,调用该按键的方式就很简单了,首先定义一个 按键事件环形队列,然后将driver_key_exe()放入大循环中,最后自己重定义按键处理的回调函数,如本例
/* 按键回调函数 */
void driver_key_Callback(KeyEvent * pKeyEvent)
{
uint16_t i = 0;
if(pKeyEvent->ID=='A')
{
driver_led_writeStatus(LED_TOGGLE);
printf("ID = %c , time = 0x%x\r\n",pKeyEvent->ID,pKeyEvent->time);
}
}
如果是按键‘A’,则小灯电平反转,并输出按键的ID和time信息。