首先,作为刚入行不久的新人,我在单片机开发这块并没有太多的经验,所以可能在写一些相关的文档的时候存在一些错误,希望大家多多包含!也希望各位不吝赐教,指点迷津!
好记性不如烂笔头,之所以选择开通博客是因为我想把自己在工作和学习过程中碰到的一些问题以及疑惑记录下来,同时积极地定位问题的源头以及寻求解决方案,或许在碰到相同的问题时就能很快地解决。同时在博客上也可以学习到很多工程师长期积累的经验,分享自己的一些心得,这对个人的自我提升有很好的帮助!
最近的工作和学习中接触STM32CubeMX的次数很多,这是STM32现在主推的一个工具,它可以为用户生成初始化代码,大大方便了用户,提升了开发效率。生成的代码使用的是HAL库,实际上跟标准库很像,但是现在标准库已经不更新了,HAL库必成主流。
从简单的开始,今天就从点灯和按键开始(涉及到定时器、外部中断),下面是CubeMX 的配置:
1、RCC配置,这里使用了外部高速时钟(HSE):
2、定时器,这里随便选择了TIM3,也可以选择其他的定时器,比如TIM6和TIM7这两个基本定时器:
因为TIM3是挂载在APB1上的,我用的这款单片机APB1 Timer clock最大频率是84(MHz),而我要做的是定时1ms进入一次中断,所以其他配置如图所示:
中断记得配置为使能:
3、KEY的配置(KEY1:PA0, KEY2:PE2, KEY3:PE3, KEY4:PE4):全部配置为外部中断
因为KEY1硬件设计的是按下后输入高电平,KEY2~KEY4则是按下后输入低电平,所以PA0配置为上升沿触发,PE2~4//代码效果参考:http://www.jhylw.com.cn/454029671.html
配置为下降沿触发:中断记得使能:
4、LED灯的配置跟KEY配置也差不多,只是模式配置为输出,因为硬件设计上我的这几个灯都是低电平点亮,所以初始化的时候我都配置为高电平,当需要其点亮的时候再配置为低电平:
5、时钟树配置:
这样基本上就都配置完成了,点击生成代码,打开工程进行代码添加。
1、首先为了方便对LED灯的控制,我做了几个LED的宏定义:
#define led1 0x01
#define led2 0x02
#define led3 0x03
#define led4 0x04
#define LED_ON(LEDx) { \
if(LEDx == led1) HAL_GPIO_WritePin(GPIOF, LED1_Pin, GPIO_PIN_RESET); \
else if(LEDx == led2) HAL_GPIO_WritePin(GPIOF, LED2_Pin, GPIO_PIN_RESET); \
else if(LEDx == led3) HAL_GPIO_WritePin(GPIOF, LED3_Pin, GPIO_PIN_RESET); \
else if(LEDx == led4) HAL_GPIO_WritePin(LED4_GPIO_Port, LED4_Pin, GPIOPIN//代码效果参考:http://www.jhylw.com.cn/011825011.html
RESET); \}
#define LED_OFF(LEDx) { \
if(LEDx == led1) HAL_GPIO_WritePin(GPIOF, LED1_Pin, GPIO_PIN_SET); \
else if(LEDx == led2) HAL_GPIO_WritePin(GPIOF, LED2_Pin, GPIO_PIN_SET); \
else if(LEDx == led3) HAL_GPIO_WritePin(GPIOF, LED3_Pin, GPIO_PIN_SET); \
else if(LEDx == led4) HAL_GPIO_WritePin(LED4_GPIO_Port, LED4_Pin, GPIO_PIN_SET); \
}
#define LED_Toggle(LEDx) { \
if(LEDx == led1) HAL_GPIO_TogglePin(GPIOF, LED1_Pin); \
else if(LEDx == led2) HAL_GPIO_TogglePin(GPIOF, LED2_Pin); \
else if(LEDx == led3) HAL_GPIO_TogglePin(GPIOF, LED3_Pin); \
else if(LEDx == led4) HAL_GPIO_TogglePin(LED4_GPIO_Port,LED4_Pin); \
}
2、接下来是按键检测,关于按键KEY我也做了相关的宏定义和一个结构体:
typedef struct KEY
{
uint8_t KeyStatus; / 当按键按下时引发第一次外部中断,就会引起这个状态位置1 /
uint32_t HoldTime; / KeyStatus置1后开始计时,即按下按键到松开按键之间持续的时间 /
uint8_t IsPress; / 按键是否按下,1按下,0没按下 /
uint8_t PressMode; / 按下的模式,0没按下,1短按,2长按 /
uint32_t LoosenTime; / 松开按键后计时10ms才可以进行下一次判断,避免松开按键后抖动引起误判 /
uint8_t IsLoosen; / 按下按键后是否已经松开按键,0是没松开,1是松开 /
}Key;
extern Key key【5】;
#define key1 0x01
#define key2 0x02
#define key3 0x03
#define key4 0x04
另外还写了一个检测按键引脚电平的函数:
/*
函数功能:返回按键引脚电平的状态值
函数形参:uint8_t KEYx----按键
函数返回值:1----按下,0----松开
备注:无
**/
uint8_t Key_Status(uint8_t KEYx)
{
uint8_t ret;
if(KEYx == key1)
{
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_SET)
{
ret = 1;
}
else
{
ret = 0;
}
}
else if(KEYx == key2)
{
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
ret = 1;
}
else
{
ret = 0;
}
}
else if(KEYx == key3)
{
if(HAL_GPIO_ReadPin(KEY3_GPIO_Port,KEY3_Pin) == GPIO_PIN_RESET)
{
ret = 1;
}
else
{
ret = 0;
}
}
else if(KEYx == key4)
{
if(HAL_GPIO_ReadPin(KEY4_GPIO_Port,KEY4_Pin) == GPIO_PIN_RESET)
{
ret = 1;
}
else
{
ret = 0;
}
}
return ret;
}
使用外部中断判断是否有电平变化:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if((GPIO_Pin == KEY1_Pin) && (!key【1】.LoosenTime) && (!key【1】.HoldTime))
{
key【1】.KeyStatus = 1;
}
else if((GPIO_Pin == KEY2_Pin) && (!key【2】.LoosenTime) && (!key【2】.HoldTime))
{
key【2】.KeyStatus = 1;
}
else if((GPIO_Pin == KEY3_Pin) && (!key【3】.LoosenTime) && (!key【3】.HoldTime))
{
key【3】.KeyStatus = 1;
}
else if((GPIO_Pin == KEY4_Pin) && (!key【4】.LoosenTime) && (!key【4】.HoldTime))
{
key【4】.KeyStatus = 1;
}
}
这里还加了key【x】.LoosenTime和key【x】.HoldTime这两个变量作为判断的条件,只有当这两个变量都为0的时候才会对对应的key【x】.KeyStatus 做判断,因为按键按下时和松开时都会有大概10ms左右的抖动,如果没有这两个变量作为判断条件,可能会重复判断,但是经过测试,其实这两个变量加不加影响不大,这里只是为了使逻辑更加严谨一些。这个回调函数会在按键按下时触发,接下来就是定时器起作用了。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
uint8_t index;
for(index = 1; index < 5; index++) //1ms进入一次中断,对四个按键进行轮询
{
if(key【index】.KeyStatus == 1) //key【index】.KeyStatus等于1,说明有按键引脚电平发生了突变
{
key【index】.HoldTime++; //开始计时,key【index】.HoldTime每1ms增加1
if(key【index】.HoldTime >= 10) //如果key【index】.HoldTime大于或等于10,这段时间是按键抖动的时间
{
if(Key_Status(index) == 1) //判断按键引脚的电平是否处于按下状态,如果是,key【index】.IsPress 置1
{
key【index】.IsPress = 1;
}
}
}
if(key【index】.IsPress == 1) //key【index】.IsPress为1说明可以确定按键是按下了,接下来就是判断是长按还是短按了
{
if(Key_Status(index) == 0) //按键按下后又松开了,此时判断从按下到松开的过程持续的时间多长,从而判断是长按还是短按
{
if(key【index】.HoldTime < 1500) //持续时间小于1500ms,短按,key【index】.PressMode置为1,key【index】.IsLoosen置为1,其余参数清零
{
key【index】.PressMode = 1;
key【index】.HoldTime = 0;
key【index】.KeyStatus = 0;
key【index】.IsPress = 0;
key【index】.IsLoosen = 1;
key【index】.LoosenTime = 0;
}
else
{
key【index】.PressMode = 2; //持续时间大于或等于1500ms,长按,key【index】.PressMode置为2,key【index】.IsLoosen置为1,其余参数清零
key【index】.HoldTime = 0;
key【index】.KeyStatus = 0;
key【index】.IsPress = 0;
key【index】.IsLoosen = 1;
key【index】.LoosenTime = 0;
}
}
}
if(key【index】.IsLoosen == 1) //key【index】.IsLoosen为1时,说明按键经历了按下到松开的过程,松开后会有10ms的抖动,不检测按键
{
key【index】.LoosenTime++;
if(key【index】.LoosenTime >= 10)
{
key【index】.IsLoosen = 0;
key【index】.LoosenTime = 0;
}
}
}
}
对了,定时器虽然在CubeMX配置的时候已经配置为中断使能,但是还要再工程中加入一个语句才能开启中断:
HAL_TIM_Base_Start_IT(&htim3);
这也是我调试代码后发现的,原来我以为只要在CubeMX配置为中断使能就可以开启中断了。
然后,我们只要去判断key【x】.PressMode的值,就可以知道某一时刻按键的状态了,也就可以做出相应的LED控制(也可以是其他的操作,视需求而定),执行完操作之后记得把key【x】.PressMode 清零,不然的话会一直判断为上一次的按键状态。
其实除了长按短按两种模式之外,我还尝试过增加双击的逻辑,测试效果并不是特别好,容易判断错误,可能是一些逻辑没处理好,由于时间关系就没有继续研究下去。但是我认为单片机的按键实际上不需要做得很复杂,能实现长按短按一般都是能满足大部分的需求了,太复杂反而显得累赘。
可能这段代码写得也不好,但是我认为也有一些可取的地方,比如结构体数组的应用,按键实现过程中会有一些属性或者标志位,我们把它们写到同一个结构体里就变得很清晰了。多个按键实际上它们都有相同的属性,那么再把它们做成一个数组,实现过程通过轮询方式就可以大大的简化代码了