参考教程:[编程技巧] 第1期 定时器实现非阻塞式程序 按键控制LED闪烁模式_哔哩哔哩_bilibili
一、实验前信息储备
1、程序功能与要求
(1)程序功能:两个按键分别控制两个LED灯的闪烁模式,每按下1个按键,对应的LED灯切换点亮模式。
(2)程序要求:
①按键灵敏,每次按键按下都能准确切换模式,不可出现按一次按键没有反应或者一口气做了若干次模式切换的情况。
②模块要高度封装,主程序调用要简洁。
③在任何时候模块代码都不能阻塞主程序。
2、阻塞和非阻塞的概念
(1)阻塞:执行某段程序时,CPU因为需要等待延时或者等待某个信号而被迫处于暂停状态一段时间,程序执行时间较长或者时间不定。
(2)非阻塞:执行某段程序时,CPU不会等待,程序很快执行结束。
3、定时器扫描按键的方法
(1)定时器扫描按键-单按键:
①使用定时中断,每隔20ms读取一次本次引脚电平和上次引脚电平。
②判断,如果本次次引脚电平是1,上次次引脚电平是0,则表示按键先前被按下且当前处于刚松手的状态。
(2)定时器扫描按键-多按键:
①先写一个获取键码值的子函数(非阻塞式,即获取当前哪个接了按键的引脚为低电平0,返回其对应的键码值,如全部接按键的引脚均为高电平0,则返回键码值0)。
②使用定时中断,每隔20ms读取一次本次键码值和上次键码值。
③判断,如果本次是0,上次非0,则表示按键按下且当前处于刚松手的状态,记录最近一次捕获到的非0键码,并向主程序报告此事件。
4、定时器实现LED闪烁
(1)在定时中断函数中定义计次变量(静态),每隔1ms计次变量自增,计到周期值时归零。
(2)判断,如果计次变量小于一个比较值则开灯,否则关灯。
二、实验步骤
1、准备工作
(1)拷贝一份STM32教程中“使用OLED屏进行显示”的工程文件夹,并更名为“定时器实现非阻塞式程序”。
(2)在STM32教程中“定时器定时中断”的工程文件夹中找到Timer.c和Timer.h文件,将其添加进本工程中,并将TIM2的定时时间配置为1ms。
void Timer_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);TIM_InternalClockConfig(TIM2);TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;TIM_TimeBaseInitStructure.TIM_Period = 10 - 1; //重装载值TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; //预分频系数(TIM2的频率是72MHz)TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);TIM_ClearFlag(TIM2, TIM_FLAG_Update);TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;NVIC_Init(&NVIC_InitStructure);TIM_Cmd(TIM2, ENABLE);
}
2、按键模块编写
(1)在Key.c文件中编写获取键码值的子函数。
uint8_t Key_GetState(void)
{if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0){return 1; //按键1按下,返回键码1}if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0){return 2; //按键2按下,返回键码2}return 0; //无按键按下,返回键码0
}
(2)在Key.c文件中编写多按键扫描(需要记录键码,而不仅仅是读取)的函数,并在头文件中声明,供main.c文件中的TIM2定时中断函数调用。
uint8_t Key_Num; //记录最近一次捕获到的非0键码(也可认为是事件标志位)void Key_Tick(void) //供TIM2定时中断函数调用
{static uint8_t Count; //用于分频static uint8_t CurrState; //存储当前按键状态static uint8_t PrevState; //存储上次按键状态Count++; //TIM2定时中断函数每1ms执行一次if(Count >= 20) //Key_Tick函数每20ms执行一次{Count = 0;PrevState = CurrState; //获取上次按键状态CurrState = Key_GetState(); //获取当前按键状态if(CurrState == 0 && PrevState != 0) //检测到有按键按下且当前已松手{Key_Num = PrevState; //记录最近一次捕获到的非0键码}}
}
(3)在Key.c文件中编写向主函数报告按键事件的函数,并在头文件中声明,供main.c文件中的主函数调用。
uint8_t Key_GetNum(void)
{uint8_t Temp;if(Key_Num) //定时中断没有记录到非0键码时勿入{Temp = Key_Num;//防止按键松开时定时中断发生在此处,中断结束后Key_Num清零//而Temp也没记录到键码,这会导致按键“失灵”Key_Num = 0; //事件标志位清零return Temp; //将非0键码返回}return 0;
}
3、LED模块编写
(1)在LED.c文件中编写控制LED灯闪烁的函数,并在头文件中声明,供main.c文件中的TIM2定时中断函数调用。
uint16_t LED1_Count; //LED1的计次变量
uint16_t LED2_Count; //LED2的计次变量
uint8_t LED1_Mode; //维护LED1的模式
uint8_t LED2_Mode; //维护LED2的模式void LED_Tick(void)
{if(LED1_Mode == 0) //常暗LED1_OFF();else if(LED1_Mode == 1) //常亮LED1_ON();else if(LED1_Mode == 2) //慢闪{LED1_Count++;LED1_Count %= 1000; //周期为1000msif(LED1_Count < 500) LED1_ON(); //周期内500ms,LED为亮else LED1_OFF();}else if(LED1_Mode == 3) //快闪{LED1_Count++;LED1_Count %= 100; //周期为100msif(LED1_Count < 50) LED1_ON(); //周期内50ms,LED为亮else LED1_OFF();}else if(LED1_Mode == 4) //点闪{LED1_Count++;LED1_Count %= 1000;if(LED1_Count < 100) LED1_ON();else LED1_OFF();}if(LED2_Mode == 0) //常暗LED2_OFF();else if(LED2_Mode == 1) //常亮LED2_ON();else if(LED2_Mode == 2) //慢闪{LED2_Count++;LED2_Count %= 1000;if(LED2_Count < 500) LED2_ON();else LED2_OFF();}else if(LED2_Mode == 3) //快闪{LED2_Count++;LED2_Count %= 100;if(LED2_Count < 50) LED2_ON();else LED2_OFF();}else if(LED2_Mode == 4) //点闪{LED2_Count++;LED2_Count %= 1000;if(LED2_Count < 100) LED2_ON();else LED2_OFF();}
}
(2)在LED.c文件中编写更改LED闪烁模式的函数,并在头文件中声明,函数参数为期望闪烁模式,供main.c文件中的主函数调用。
void LED1_SetMode(uint8_t Mode)
{if(Mode != LED1_Mode) //如果期望模式和当前模式一致,则跳过{LED1_Mode = Mode; //模式更改LED1_Count = 0; //周期可能变更,计次变量要从零开始自增}
}void LED2_SetMode(uint8_t Mode)
{if(Mode != LED2_Mode) //如果期望模式和当前模式一致,则跳过{LED2_Mode = Mode; //模式更改LED2_Count = 0; //周期可能变更,计次变量要从零开始自增}
}
4、main.c文件编写与调试
(1)将按键模块和LED模块供定时中断函数调用的函数添加进TIM2中断函数中。
void TIM2_IRQHandler(void)
{if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){Key_Tick();LED_Tick();TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}
(2)更改主函数和新增4个全局变量,并添加相应的头文件。
uint16_t i;
uint8_t KeyNum;
uint8_t LED1Mode, LED2Mode;int main(void)
{OLED_Init();Key_Init();LED_Init();Timer_Init();OLED_ShowString(1, 1, "i:");OLED_ShowString(2, 1, "LED1Mode:");OLED_ShowString(3, 1, "LED2Mode:");while (1){KeyNum = Key_GetNum(); //获取按键事件if(KeyNum == 1){LED1Mode++;LED1Mode %= 5; //闪烁模式轮换LED1_SetMode(LED1Mode); //模式设置}if(KeyNum == 2){LED2Mode++;LED2Mode %= 5; //闪烁模式轮换LED2_SetMode(LED2Mode); //模式设置}OLED_ShowNum(1, 3, i++, 5); //观察主循环是否在一直运行,是否保持快速刷新OLED_ShowNum(2, 10, LED1Mode, 1);OLED_ShowNum(3, 10, LED2Mode, 1);}
}
(3)将程序编译、下载,按照程序功能与要求进行调试。