实验任务
本实验为RT-Thread Nano 入门线上培训作业题目3:利用按键实现某个动态线程创建和删除。
实验目的
熟悉RT-Thread按键的检测,熟悉动态线程的创建和删除。
实验环境
1、硬件环境:野火STM32霸道开发板
2、软件环境:RT-Thread Nano 3.1.3,MDK 5.25
实验思路
1、编写按键驱动程序。按键驱动程序要完成的任务就是初始化GPIO,然后实现检测按键状态的检测函数,这个函数一般会被定时器周期调用,当检测到按键有动作时,通过消息队列的形式,向外发送按键消息。
2、创建按键的处理线程,当按键1按下的时候,创建一个新的线程A,当按键2按下的时候,删除线程A。
3、为了方便观察,我这里动态创建和删除的线程A的功能是实现1s间隔的蜂鸣器鸣响。
废话不多说,直接上代码。
4、首先是按键对应的GPIO初始化。
static void ButtonGpioInit(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
//-------------------------------------------------------------------------------------------
// 对应外设时钟与GPIO配置
RCC_APB2PeriphClockCmd(GPIO_CLK_Key1 , ENABLE ); // 使能通道时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_Key1; // IO口选择
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Key1; // 配置模式
GPIO_Init(GPIO_Key1, &GPIO_InitStructure); // 使能
//-------------------------------------------------------------------------------------------
// 对应外设时钟与GPIO配置
RCC_APB2PeriphClockCmd(GPIO_CLK_Key2 , ENABLE ); // 使能通道时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_Key2; // IO口选择
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Key2; // 配置模式
GPIO_Init(GPIO_Key2, &GPIO_InitStructure); // 使能
/****************************************************************************************/
//用户添加自定义按键配置
/****************************************************************************************/
}
5、然后需要实现按键的检测函数,这边使用的是本次线上培训何老师做的按键检测软件包,本质上是一个状态机。我们先来看看软件包所抽象出来的关键数据结构———按键结构体。这个结构体将检测按键需要用到的一些参数进行抽象。这样在检测按键状态的的时候就会比较清晰。
typedef struct
{
unsigned char (*IsKeyDownFunc)(void); // 函数指针,指向判断按键是否按下的函数
unsigned char wFilterCount; // 滤波器计数器
unsigned char wFilterTime; // 滤波时间(最大255,表示255ms)
unsigned short wLongCount; // 长按计数器
unsigned short wLongTime; // 长按键时间, 0表示不检测长按
unsigned char byState; // 按键当前状态(按下还是弹起)
unsigned char byKeyCodeUp; // 按键弹起的键值代码, 0表示不检测按键弹起
unsigned char byKeyCodeDown; // 按键按下的键值代码, 0表示不检测按键按下
unsigned char byKeyCodeLong; // 按键长按的键值代码, 0表示不检测长按
}Button_t;
6、还有几个比较关键的宏定义
#define BUTTON_FILTER_TIME 10 // 滤波时间,单位为ms
#define BUTTON_LONG_TIME 1000 // 持续1秒,认为长按事件
//-----------------------------------------------------------------------------------------------
// Key按键按下时的电平,=0,按下时为低电平;=1,按下时为高电平
#define KeyPressedLevel 1
//-----------------------------------------------------------------------------------------------
// 获取按键按下函数
static unsigned char IsKey1Down(void) {if (Key1In != KeyPressedLevel) return 0; return 1;}
static unsigned char IsKey2Down(void) {if (Key2In != KeyPressedLevel) return 0; return 1;}
7、接下来就是对按键进行初始化,我这边使用了2个按键,就初始化2个按键,其实就是设置他们的回调函数,滤波时间,是否检测长按,等等,也就是对Button结构体赋值。
static void ButtonVarInit(void)
{
s_tBtnKey1.IsKeyDownFunc=IsKey1Down; // 检测按键按下函数
s_tBtnKey1.wFilterCount=0; // 滤波器计数器
s_tBtnKey1.wFilterTime =BUTTON_FILTER_TIME; // 滤波时间
s_tBtnKey1.wLongCount =0; // 长按计数器
s_tBtnKey1.wLongTime=BUTTON_LONG_TIME; // 长按时间
s_tBtnKey1.byState=0; // 按键当前状态(按下还是弹起)
s_tBtnKey1.byKeyCodeUp=KEY1_UP; // 按键弹起的键值代码, 0表示不检测按键弹起
s_tBtnKey1.byKeyCodeDown=KEY1_DOWN; // 按键按下的键值代码, 0表示不检测按键按下
s_tBtnKey1.byKeyCodeLong=KEY1_LONG; // 按键长按的键值代码, 0表示不检测长按
s_tBtnKey2.IsKeyDownFunc=IsKey2Down; // 检测按键按下函数
s_tBtnKey2.wFilterCount=0; // 滤波器计数器
s_tBtnKey2.wFilterTime =BUTTON_FILTER_TIME; // 滤波时间
s_tBtnKey2.wLongCount =0; // 长按计数器
s_tBtnKey2.wLongTime=BUTTON_LONG_TIME; // 长按时间
s_tBtnKey2.byState=0; // 按键当前状态(按下还是弹起)
s_tBtnKey2.byKeyCodeUp=KEY2_UP; // 按键弹起的键值代码, 0表示不检测按键弹起
s_tBtnKey2.byKeyCodeDown=KEY2_DOWN; // 按键按下的键值代码, 0表示不检测按键按下
s_tBtnKey2.byKeyCodeLong=KEY2_LONG; // 按键长按的键值代码, 0表示不检测长按
/****************************************************************************************/
//用户添加自定义按键变量初始化
/****************************************************************************************/
}
8、然后就是实现按键的检测了。这本质是是一个状态机,并且还做了按键的滤波。这个函数传入的是一个按键结构体,因为之前按键的初始化的时候对两个按键的结构体赋值了,这里就是直接使用他们的值来进行按键的检测。
首先判断传入的按键是否按下,如果按下了,就进行滤波(消抖)操作,因为物理按键按下和抬起的时候,往往会带有几十毫秒的抖动时间,这段时间的电平是忽高忽低的,这里就通过一个滤波计数器进行消抖操作。当检测到按下的时候,滤波计数器就++,检测到抬起的时候,滤波计数器就–,当滤波计数器加到了滤波时间的时候,才认为按下事件发生了。
当按下事件发生了,先判断下原来按键的状态。
如果原来的按键是没有按下的,那现在是按下了,就修改按键的状态,然后通过消息队列发送按下消息。
而如果原来的按键是按下的呢?那就进行长按检测,当长按计数到达长按的时间时,就通过消息队列发送长按消息。
当然按键抬起的检测也是类似的,当滤波计数器减小到0的时候,就说明按键被抬起了。这时候如果原来的按键是按下的,就修改按键状态为抬起,然后通过消息队列发送按键抬起消息。并且将长按计数器清零。
static void ButtonDetect(Button_t *ptBtn)
{
if(ptBtn->IsKeyDownFunc && ptBtn->IsKeyDownFunc()) // 检测按键函数是否存在,按键是否按下
{
if(ptBtn->wFilterCount < ptBtn->wFilterTime) // 滤波检测,滤波计数器到达滤波时间
{
ptBtn->wFilterCount++; // 计数器加一
return; // 退出检测函数
}
if(ptBtn->byState == 0) // 检测是否是按键按下
{
ptBtn->byState = 1;
rt_mq_send(button_mq, // 写入(发送)队列的ID(句柄)
&(ptBtn->byKeyCodeDown), // 写入(发送)的数据所对应地址
sizeof(ptBtn->byKeyCodeDown) // 数据的长度
);
return;
}
if( ptBtn->wLongCount++ == ptBtn->wLongTime) // 检测是否是按键长按,长按计数器是否到达长按时间
{
rt_mq_send(button_mq, // 写入(发送)队列的ID(句柄)
&(ptBtn->byKeyCodeLong), // 写入(发送)的数据所对应地址
sizeof(ptBtn->byKeyCodeLong) // 数据的长度
);
return;
}
}
else
{
if(ptBtn->wFilterCount) // 滤波检测,滤波计数器是否为0
{
ptBtn->wFilterCount--; // 计数器减一
return; // 退出检测函数
}
if(ptBtn->byState ==1) // 检测是否是按键弹起
{
ptBtn->byState =0;
rt_mq_send(button_mq, // 写入(发送)队列的ID(句柄)
&(ptBtn->byKeyCodeUp), // 写入(发送)的数据所对应地址
sizeof(ptBtn->byKeyCodeUp) // 数据的长度
);
}
ptBtn->wLongCount = 0; // 按键长按计数器清零
}
}
9、然后就实现这两个按键的周期回调函数即可,也就是把两个按键实例传进去。
void ButtonProj(void)
{
//该函数在定时器中断回调函数中调用,定时中断周期为1ms
ButtonDetect(&s_tBtnKey1); // 检测 K1 键
ButtonDetect(&s_tBtnKey2); // 检测 K2 键
/****************************************************************************************/
//用户添加自定义按键变量初始化
//例如:ButtonDetect(&s_tBtnKeyN);
/****************************************************************************************/
}
10、最后就是创建一个按键处理线程,这个线程创建一个软件定时器,1ms周期调用ButtonProj函数检测按键的状态,线程的主要任务就是等待消息队列的数据,当有消息到达的时候,就执行想做的操作。PS:线程A的代码我就不贴了,不是啥关键的东西。
void button_thread_entry(void *parameter)//用户消息处理入口函数
{
rt_err_t uwRet = RT_EOK;
uint8_t r_queue; //用于接收msg_mq消息队列信息
rt_timer_t button_timer; //用于定时调用按键检测函数
rt_thread_t tid; //用于记录动态线程创建的对象
button_mq = rt_mq_create("button_mq", //消息队列名字
1, //消息的最大长度, bytes
256, //消息队列的最大容量(个数)
RT_IPC_FLAG_FIFO //如果有多个线程等待此消息,则使用先进先出的方式进行线程调用
);
if(button_mq != RT_NULL)
rt_kprintf("button_mq create success\n\n");
ButtonInit(); //按键硬件接口初始化
button_timer = rt_timer_create("button_timer", //定时器名字
button_timer_timeout, //定时器回调函数
RT_NULL, //参数
1, //1ms检测一次按键
RT_TIMER_FLAG_PERIODIC | //周期调用
RT_TIMER_FLAG_SOFT_TIMER ); //软件定时
if (button_timer != RT_NULL)
rt_timer_start(button_timer);
while(1)
{
uwRet = rt_mq_recv(button_mq, //获取队列信息
&r_queue,
sizeof(r_queue),
RT_WAITING_FOREVER
);
if(RT_EOK == uwRet )
{
switch(r_queue) //根据接收到的消息内容分别进行处理
{
case KEY1_DOWN:rt_kprintf("Receive message:KEY1(PA.0) Down\n\n");
if( tid != RT_NULL )
{
tid = rt_thread_create("button_process",
A_thread_entry, RT_NULL,
BUTTON_THREAD_STACK_SIZE, BUTTON_THREAD_PRIORITY, 10);
if (tid != NULL)
{
rt_thread_startup(tid);
rt_kprintf("线程A创建成功!\n");
}
}
else
{
rt_kprintf("线程A已存在,不能重复创建!\n");
}
break;
case KEY1_UP:rt_kprintf("Receive message:KEY1(PA.0) Up\n\n");break;
case KEY1_LONG:rt_kprintf("Receive message:KEY1(PA.0) LongPressed Down\n\n");break;
case KEY2_DOWN:rt_kprintf("Receive message:KEY2(PC.13) Down\n\n");
if( tid != RT_NULL )
{
rt_thread_delete(tid);
rt_kprintf("线程A删除成功!\n");
tid = RT_NULL;
}
else
{
rt_kprintf("线程A不存在,无法删除!\n");
}
break;
case KEY2_UP:rt_kprintf("Receive message:KEY2(PC.13) Up\n\n");break;
case KEY2_LONG:rt_kprintf("Receive message:KEY2(PC.13) LongPressed Down\n\n");break;
default: rt_kprintf("No button Message!\n\n");break;
}
}
else
{
rt_kprintf("数据接收错误,错误代码:0x%lx\n\n",uwRet);
}
}
}
int button_process_init(void)
{
rt_thread_t tid;
tid = rt_thread_create("button_process",
button_thread_entry, RT_NULL,
BUTTON_THREAD_STACK_SIZE, BUTTON_THREAD_PRIORITY, 10);
if (tid != NULL)
rt_thread_startup(tid);
return 0;
}
INIT_APP_EXPORT(button_process_init);