本期带来的是基于stm32的按键驱动框架的编写,本框架适用于多种按键状态的判断 在这我只写了三种状态 分别是单击 双击和长按,学会了此框架也可以把此驱动移植到其它外设上 ,本驱动主要涉及 游标链表的编写 环形队列的编写 ,游标链表主要是用来放入按键的属性 环形队列则是用来处理按键的事件 ,好接下来我们一起来完成这个按键驱动框架的编写。
游标链表
在我们普通的数据结构中链表是一种地址不相同但可以通过指针把地址关联起来 ,而游标链表则是通过 链表与数组下标新结合 它与普通链表的区别在内存方面普通链表需要malloc和free来动态的分配内存 而 游标链表的内存则是由自己管理 普通链表容易产生内存的碎片化 游标链表则不会 ,游标链表具有确定性,因为内存在程序开的时候就已经分配完成了所以不需要考虑内存方面的问题,接下来就是游标链表的具体构成
.h
typedef void* sLinkType;
extern uint32_t sys_tick_ms; // 毫秒计数器
typedef struct
{
sLinkType data;
uint16_t cur;
}sLink_t;
typedef struct
{
sLink_t *skil;
uint16_t max_size;
}staticll_t;
typedef enum
{
LINK_LIST_false = 0,
LINK_LIST_true = 1
}Link_list_status;
.c
Link_list_status Link_list_Init(staticll_t *slink,sLink_t *skill,uint16_t max_size)
{
slink->max_size = max_size;
slink->skil = skill;
for(int i = 0; i < max_size-1; i++)
{
slink->skil[i].cur = i+1;
}
slink->skil[max_size-1].cur = 0;
return LINK_LIST_true;
}
//结点内存的分配
uint16_t Link_list_malloc(staticll_t *slink)
{
uint16_t i = slink->skil[0].cur;
if(slink->skil[0].cur)
{
slink->skil[0].cur = slink->skil[i].cur;
}
return i;
}
//结点内存的释放
void Link_list_free(staticll_t *slink,uint16_t index)
{
slink->skil[index].cur = slink->skil[0].cur;
slink->skil[0].cur = index;
}
//结点个数的统计
uint16_t Link_list_length(staticll_t *slink)
{
int count = 0;
for(int i = slink->skil[0].cur;i != 0;i = slink->skil[i].cur)
{
count++;
}
return count;
}
//结点的插入
Link_list_status Link_list_insert(staticll_t *slink, uint16_t i, sLinkType e)
{
uint16_t j,l;
int k = slink->max_size-1;
if(i<1|| i>Link_list_length(slink)+1)
{
return LINK_LIST_false;
}
j = Link_list_malloc(slink);
if (j)
{
slink->skil[j].data = e;
for (l = 1; l<i-1; l++)
{
k = slink->skil[k].cur;
}
slink->skil[j].cur = slink->skil[k].cur;
slink->skil[k].cur = j;
return LINK_LIST_true;
}
return LINK_LIST_false;
}
//删除结点
Link_list_status Link_list_delete(staticll_t *slink, uint16_t i)
{
int k = slink->skil[0].cur;
int l,j;
if(i<1|| i>Link_list_length(slink)+1)
{
return LINK_LIST_false;
}
for(j=1;j<i-1;j++)
{
k = slink->skil[k].cur;
}
j = slink->skil[k].cur;
slink->skil[k].cur = slink->skil[j].cur;
Link_list_free(slink, j);
return LINK_LIST_true;
}
//查找到结点的值
sLinkType Link_list_get(staticll_t *slink, sLinkType data)
{
int j = slink->skil[0].cur;
while (slink->skil[j].data != data && j!=0)
{
j = slink->skil[j].cur;
}
if (j != 0)
{
return slink->skil[j].data;
}
else
return NULL;
}
在这里我们为什么需要用到游标链表而不使用普通链表呢 主要是因为在单片机中它的内存是十分宝贵的资源如果我们使用普通的链表会产生许多的内存碎片,而游标链表的内存是确定了的因此游标链表更适合在嵌入式的场景下。
环形队列
在本次程序中环形队列的用法主要是用来存储按键的事件,通过把事件发送到环形队列中然后分别读取出来相应事件来做相应的处理。
.h
#define KEY_QUEUE_SIZE 16
//环形队列的主要的结构体
typedef struct
{
int writer;//写指针
int read;//读指针
key_event_t buf[KEY_QUEUE_SIZE];//主要的存放载体
int count;//队列中存放的元素个数
}KEY_QUEN_st;
//环形队列的状态
typedef enum
{
KEY_QUEUE_OK,
KEY_QUEUE_FULL,
KEY_QUEUE_EMPTY,
KEY_QUEUE_ERROR
}KEY_QUEN_STATUS_em;
KEY_QUEN_STATUS_em KEY_QUEN_Init(KEY_QUEN_st *quen);
KEY_QUEN_STATUS_em KEY_QUEN_ADD(KEY_QUEN_st *quen,key_event_t *event);
KEY_QUEN_STATUS_em KEY_QUEN_GET(KEY_QUEN_st *quen,key_event_t *event);
KEY_QUEN_STATUS_em KEY_QUEN_IS_EMPTY(KEY_QUEN_st *quen);
KEY_QUEN_STATUS_em KEY_QUEN_IS_FULL(KEY_QUEN_st *quen);
.c
KEY_QUEN_STATUS_em KEY_QUEN_Init(KEY_QUEN_st *quen)
{
quen->count = 0;
quen->read = 0;
quen->writer = 0;
return KEY_QUEUE_OK;
}
KEY_QUEN_STATUS_em KEY_QUEN_ADD(KEY_QUEN_st *quen,key_event_t *event)
{
if(KEY_QUEN_IS_FULL(quen) == KEY_QUEUE_FULL || event == NULL)
{
return KEY_QUEUE_ERROR;
}
memcpy(&quen->buf[quen->writer], event, sizeof(key_event_t));
quen->writer = (quen->writer + 1) % KEY_QUEUE_SIZE;
quen->count++;
return KEY_QUEUE_OK;
}
KEY_QUEN_STATUS_em KEY_QUEN_GET(KEY_QUEN_st *quen,key_event_t *event)
{
if(KEY_QUEN_IS_EMPTY(quen) == KEY_QUEUE_EMPTY || event == NULL)
{
return KEY_QUEUE_ERROR;
}
memcpy(event, &quen->buf[quen->read], sizeof(key_event_t));
quen->read = (quen->read + 1) % KEY_QUEUE_SIZE;
quen->count--;
return KEY_QUEUE_OK;
}
KEY_QUEN_STATUS_em KEY_QUEN_IS_EMPTY(KEY_QUEN_st *quen)
{
return (quen->count == 0) ? KEY_QUEUE_EMPTY : KEY_QUEUE_OK;
}
KEY_QUEN_STATUS_em KEY_QUEN_IS_FULL(KEY_QUEN_st *quen)
{
return (quen->count == KEY_QUEUE_SIZE) ? KEY_QUEUE_FULL : KEY_QUEUE_OK;
}
通过这两个数据结构的构建 我们按键驱动框架也差不多完成大部分,接下来就是具体功能程序的实现
.h
//按键的状态
typedef enum
{
KEY_UP,
KEY_DOWN,
KEY_PRESS,
KEY_RELEASE,
KEY_ERROR,
KEY_OK
}KEY_STATUS_em;
/* ─── 按键事件结构体 ─── */
typedef struct {
uint8_t key_id; // 哪个按键
KEY_STATUS_em event; // 什么事件
uint32_t timestamp; // 时间戳
} key_event_t;
#define KEY_MAX_NUM 8
/* 按键ID */
typedef enum {
KEY_ID_1 = 1,
KEY_ID_2 = 2,
KEY_ID_3 = 3,
KEY_ID_4 = 4,
} key_id_em;
/* 按键内部状态 */
typedef enum {
KEY_STATE_IDLE = 0,
KEY_STATE_PRESS = 1,
KEY_STATE_WAIT = 2,
} key_state_em;
/* 按键配置结构体(链表节点存的数据) */
typedef struct {
uint8_t key_id;
GPIO_TypeDef *gpio_port; // 端口:GPIOA / GPIOB...
uint16_t gpio_pin; // 引脚:GPIO_Pin_0 / GPIO_Pin_1...
uint8_t active_level; // 有效电平:0=低电平有效,1=高电平有效
void (*short_cb)(void);
void (*long_cb)(void);
void (*double_cb)(void);
/*长按短按双击标志位*/
uint8_t key_long_f;
uint8_t key_short_f;
uint8_t key_double_f;
/* 内部使用 */
key_state_em state;
uint8_t press_cnt;
uint32_t press_tick;
uint32_t release_tick;
} key_info_st;
/* 函数声明 */
void KEY_Init(void);
void key_register(key_info_st *key);
void key_scan(void);
void key_event_process(void);
.c
/* 链表:管理所有按键 */
static sLink_t key_sll[KEY_MAX_NUM + 1];
static staticll_t key_link;
static uint16_t key_data_head = 0;
/* 事件队列 */
static KEY_QUEN_st key_event_quen;
/* ─── 按键初始化 ─── */
void KEY_Init(void)
{
Link_list_Init(&key_link, key_sll, KEY_MAX_NUM + 1);
KEY_QUEN_Init(&key_event_quen);
key_data_head = 0;
}
/* ─── 注册按键(把按键加入链表) ─── */
void key_register(key_info_st *key)
{
uint16_t node;
/* ─── 开启对应的 GPIO 时钟 ─── */
if (key->gpio_port == GPIOA)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
else if (key->gpio_port == GPIOB)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
else if (key->gpio_port == GPIOC)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
else if (key->gpio_port == GPIOD)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
else if (key->gpio_port == GPIOE)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);
/* ─── 配置引脚为输入模式 ─── */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = key->gpio_pin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(key->gpio_port, &GPIO_InitStructure);
/* ─── 从空闲链申请一个节点 ─── */
node = Link_list_malloc(&key_link);
if (node == 0) return;
/* ─── 把按键信息存入节点的 data ─── */
key_sll[node].data = key;
/* ─── 初始化按键内部状态 ─── */
key->state = KEY_STATE_IDLE;
key->press_cnt = 0;
/* ─── 挂到数据链尾部 ─── */
if (key_data_head == 0)
{
key_data_head = node;
key_sll[node].cur = 0;
}
else
{
uint16_t tail = key_data_head;
while (key_sll[tail].cur != 0)
{
tail = key_sll[tail].cur;
}
key_sll[tail].cur = node;
key_sll[node].cur = 0;
}
}
/* ─── 按键扫描(每10ms调用一次) ─── */
void key_scan(void)
{
uint32_t now = Get_Tick();
uint16_t node = key_data_head;
while (node != 0)
{
key_info_st *key = (key_info_st *)key_sll[node].data;
/* 读 GPIO */
uint8_t pin_level = GPIO_ReadInputDataBit(key->gpio_port, key->gpio_pin);
uint8_t is_pressed;
if (key->active_level == 0)
is_pressed = (pin_level == Bit_RESET); // 低电平有效
else
is_pressed = (pin_level == Bit_SET); // 高电平有效
/* 状态机 */
switch (key->state)
{
case KEY_STATE_IDLE:
if (is_pressed)
{
key->state = KEY_STATE_PRESS;
key->press_tick = now;
key->key_double_f = 0;
}
break;
case KEY_STATE_PRESS:
if (!is_pressed)
{
uint32_t duration = now - key->press_tick;
if (duration > 2000) // 长按 > 2秒
{
key_event_t e;
e.key_id = key->key_id;
e.event = KEY_PRESS;
e.timestamp = now;
KEY_QUEN_ADD(&key_event_quen, &e);
key->state = KEY_STATE_IDLE;
}
else
{
key->release_tick = now;
key->state = KEY_STATE_WAIT;
}
}
break;
case KEY_STATE_WAIT:
if (is_pressed) // 又按了 → 双击
{
key_event_t e;
e.key_id = key->key_id;
e.event = KEY_RELEASE;
e.timestamp = now;
KEY_QUEN_ADD(&key_event_quen, &e);
key->key_double_f = 1;
key->state = KEY_STATE_PRESS;
key->press_tick = now;
}
else if (now - key->release_tick > 300) // 超时 → 短按
{
if (!key->key_double_f)
{
key_event_t e;
e.key_id = key->key_id;
e.event = KEY_DOWN;
e.timestamp = now;
KEY_QUEN_ADD(&key_event_quen, &e);
}
key->state = KEY_STATE_IDLE;
}
break;
}
node = key_sll[node].cur; // 下一个按键
}
}
/* ─── 事件处理(主循环调用) ─── */
void key_event_process(void)
{
key_event_t e;
/* 取出所有待处理事件 */
while (KEY_QUEN_GET(&key_event_quen, &e) == KEY_QUEUE_OK)
{
/* 在链表中找到对应按键 */
uint16_t node = key_data_head;
key_info_st *key = NULL;
while (node != 0)
{
key = (key_info_st *)key_sll[node].data;
if (key->key_id == e.key_id)
{
break;
}
node = key_sll[node].cur;
key = NULL;
}
if (key == NULL) continue;
/* 根据事件类型执行回调 */
switch (e.event)
{
case KEY_DOWN: // 短按
if (key->short_cb) key->short_cb();
break;
case KEY_PRESS: // 长按
if (key->long_cb) key->long_cb();
break;
case KEY_RELEASE: // 双击
if (key->double_cb) key->double_cb();
break;
default:
break;
}
}
}
本次就是按键驱动框架的全部内容如果有不足的地方还请广大网友积极的纠正,谢谢