第12章 GPIO输入-按键检测—零死角玩转STM32-F429系列

本文详细介绍STM32微控制器中按键检测的硬件设计与软件编程方法,包括GPIO输入配置、按键状态检测及LED控制等内容。
第12章     GPIO输入—按键检测

全套200集视频教程和1000PDF教程请到秉火论坛下载:www.firebbs.cn

野火视频教程优酷观看网址:http://i.youku.com/firege

 

本章参考资料:《STM32F4xx参考手册》、库帮助文档《stm32f4xx_dsp_stdperiph_lib_um.chm》。

按键检测使用到GPIO外设的基本输入功能,本章中不再赘述GPIO外设的概念,如您忘记了,可重读前面"GPIO框图剖析"小节,STM32标准库中GPIO初始化结构体GPIO_TypeDef的定义与"定义引脚模式的枚举类型"小节中讲解的相同。

12.1 硬件设计

按键机械触点断开、闭合时,由于触点的弹性作用,按键开关不会马上稳定接通或一下子断开,使用按键时会产生图 121中的带波纹信号,需要用软件消抖处理滤波,不方便输入检测。本实验板连接的按键带硬件消抖功能,见图 122,它利用电容充放电的延时,消除了波纹,从而简化软件的处理,软件只需要直接检测引脚的电平即可。

121 按键抖动说明图

122 按键原理图

从按键的原理图可知,这些按键在没有被按下的时候,GPIO引脚的输入状态为低电平(按键所在的电路不通,引脚接地),当按键按下时,GPIO引脚的输入状态为高电平(按键所在的电路导通,引脚接到电源)。只要我们检测引脚的输入电平,即可判断按键是否被按下。

若您使用的实验板按键的连接方式或引脚不一样,只需根据我们的工程修改引脚即可,程序的控制原理相同。

12.2 软件设计

LED的工程,为了使工程更加有条理,我们把按键相关的代码独立分开存储,方便以后移植。在"工程模板"之上新建"bsp_key.c"及"bsp_key.h"文件,这些文件也可根据您的喜好命名,这些文件不属于STM32标准库的内容,是由我们自己根据应用需要编写的。

12.2.1 编程要点

1.    使能GPIO端口时钟;

2.    初始化GPIO目标引脚为输入模式(引脚默认电平受按键电路影响,浮空/上拉/下拉均没有区别);

3.    编写简单测试程序,检测按键的状态,实现按键控制LED灯。

12.2.2 代码分析
1.    按键引脚宏定义

同样,在编写按键驱动时,也要考虑更改硬件环境的情况。我们把按键检测引脚相关的宏定义到"bsp_key.h"文件中,见代码清单 111

代码清单 121 按键检测引脚相关的宏

1 //引脚定义

2 /*******************************************************/

3 #define KEY1_PIN GPIO_Pin_0

4 #define KEY1_GPIO_PORT GPIOA

5 #define KEY1_GPIO_CLK RCC_AHB1Periph_GPIOA

6

7 #define KEY2_PIN GPIO_Pin_13

8 #define KEY2_GPIO_PORT GPIOC

9 #define KEY2_GPIO_CLK RCC_AHB1Periph_GPIOC

10 /*******************************************************/

以上代码根据按键的硬件连接,把检测按键输入的GPIO端口、GPIO引脚号以及GPIO端口时钟封装起来了。

2.    按键 GPIO初始化函数

利用上面的宏,编写按键的初始化函数,见代码清单 122

代码清单 122 按键GPIO初始化函数

1 /**

2 * @brief 配置按键用到的I/O

3 * @param

4 * @retval

5 */

6 void Key_GPIO_Config(void)

7 {

8 GPIO_InitTypeDef GPIO_InitStructure;

9

10 /*开启按键GPIO口的时钟*/

11 RCC_AHB1PeriphClockCmd(KEY1_GPIO_CLK|KEY2_GPIO_CLK,ENABLE);

12

13 /*选择按键的引脚*/

14 GPIO_InitStructure.GPIO_Pin = KEY1_PIN;

15

16 /*设置引脚为输入模式*/

17 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;

18

19 /*设置引脚不上拉也不下拉*/

20 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;

21

22 /*使用上面的结构体初始化按键*/

23 GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStructure);

24

25 /*选择按键的引脚*/

26 GPIO_InitStructure.GPIO_Pin = KEY2_PIN;

27

28 /*使用上面的结构体初始化按键*/

29 GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStructure);

30 }

 

同为GPIO的初始化函数,初始化的流程与"LED GPIO初始化函数"章节中的类似,主要区别是引脚的模式。函数执行流程如下:

(1)    使用GPIO_InitTypeDef定义GPIO初始化结构体变量,以便下面用于存储GPIO配置。

(2)    调用库函数RCC_AHB1PeriphClockCmd来使能按键的GPIO端口时钟,调用时我们使用"|"操作同时配置两个按键的时钟。

(3)    GPIO初始化结构体赋值,把引脚初始化成浮空输入模式,其中的GPIO_Pin使用宏"KEYx_PIN"来赋值,使函数的实现方便移植。由于引脚的默认电平受按键电路影响,所以设置成"浮空/上拉/下拉"模式均没有区别。

(4)    使用以上初始化结构体的配置,调用GPIO_Init函数向寄存器写入参数,完成GPIO的初始化,这里的GPIO端口使用"KEYx_GPIO_PORT"宏来赋值,也是为了程序移植方便。

(5)    使用同样的初始化结构体,只修改控制的引脚和端口,初始化其它按键检测时使用的GPIO引脚。

3.    检测按键的状态

初始化按键后,就可以通过检测对应引脚的电平来判断按键状态了,见代码清单 123

代码清单 123 检测按键的状态

1 /** 按键按下标置宏

2 * 按键按下为高电平,设置 KEY_ON=1 KEY_OFF=0

3 * 若按键按下为低电平,把宏设置成KEY_ON=0 KEY_OFF=1 即可

4 */

5 #define KEY_ON 1

6 #define KEY_OFF 0

7

8 /**

9 * @brief 检测是否有按键按下

10 * @param GPIOx:具体的端口, x可以是(A...K

11 * @param GPIO_PIN:具体的端口位,可以是GPIO_PIN_xx可以是0...15

12 * @retval 按键的状态

13 * @arg KEY_ON:按键按下

14 * @arg KEY_OFF:按键没按下

15 */

16 uint8_t Key_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin)

17 {

18 /*检测是否有按键按下 */

19 if (GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON ) {

20 /*等待按键释放 */

21 while (GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON);

22 return KEY_ON;

23 } else

24 return KEY_OFF;

25 }

在这里我们定义了一个Key_Scan函数用于扫描按键状态。GPIO引脚的输入电平可通过读取IDR寄存器对应的数据位来感知,而STM32标准库提供了库函数GPIO_ReadInputDataBit来获取位状态,该函数输入GPIO端口及引脚号,函数返回该引脚的电平状态,高电平返回1,低电平返回0Key_Scan函数中以GPIO_ReadInputDataBit的返回值与自定义的宏"KEY_ON"对比,若检测到按键按下,则使用while循环持续检测按键状态,直到按键释放,按键释放后Key_Scan函数返回一个"KEY_ON"值;若没有检测到按键按下,则函数直接返回"KEY_OFF"。若按键的硬件没有做消抖处理,需要在这个Key_Scan函数中做软件滤波,防止波纹抖动引起误触发。

4.    主函数

接下来我们使用主函数编写按键检测流程,见代码清单 124

代码清单 124 按键检测主函数

 

1 /**

2 * @brief 主函数

3 * @param

4 * @retval

5 */

6 int main(void)

7 {

8 /* LED 端口初始化 */

9 LED_GPIO_Config();

10

11 /*初始化按键*/

12 Key_GPIO_Config();

13

14 /* 轮询按键状态,若按键按下则反转LED */

15 while (1) {

16 if ( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON ) {

17 /*LED1反转*/

18 LED1_TOGGLE;

19 }

20

21 if ( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON ) {

22 /*LED2反转*/

23 LED2_TOGGLE;

24 }

25 }

26 }

代码中初始化LED灯及按键后,在while函数里不断调用Key_Scan函数,并判断其返回值,若返回值表示按键按下,则反转LED灯的状态。

12.2.3 下载验证

把编译好的程序下载到开发板并复位,按下按键可以控制LED灯亮、灭状态。

12.3 每课一问

1.    工程中的Key_Scan函数使用while循环来阻塞检测,等待按键释放,若按键一直被按下,会导致CPU无法进行其它操作,降低效率。尝试修改按键检测的方式,避免阻塞等待。

 

<think>我们正在使用野火指南者STM32F103R6开发板实现万年历功能,需要结合RTC(实时时钟)和LCD显示。根据用户要求,重点参考RCC(复位和时钟控制)配置以及LCD驱动部分。以下是实现步骤:###一、硬件配置1.**开发板**:野火指南者STM32F103R6(主芯片为STM32F103RBT6,与R6引脚兼容)2.**RTC时钟源**:使用外部低速晶振LSE(32.768kHz),由备份电池(VBAT)供电3.**LCD显示**:可使用板载TFT-LCD或外接LCD1602(根据引用[1]提供的方法)###二、RCC配置(关键步骤)引用[3]中提到,STM32F103的时钟树配置中,RTC时钟源可以选择LSE(外部低速晶振)或LSI(内部低速RC振荡器)。为了获得精确的RTC计时,必须使用LSE。配置步骤如下:####1.启用LSE并配置RCC```c#include"stm32f10x.h"voidRCC_Configuration(void){//1.开启PWR和BKP时钟(RTC所需)RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR|RCC_APB1Periph_BKP,ENABLE);//2.允许访问备份寄存器PWR_BackupAccessCmd(ENABLE);//3.复位备份寄存器(可选)BKP_DeInit();//4.开启LSE外部低速晶振RCC_LSEConfig(RCC_LSE_ON);//5.等待LSE就绪while(RCC_GetFlagStatus(RCC_FLAG_LSERDY)==RESET);//6.设置RTC时钟源为LSERCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);//7.使能RTC时钟RCC_RTCCLKCmd(ENABLE);//8.等待RTC寄存器同步RTC_WaitForSynchro();}```>注意:如果LSE无法起振,可尝试使用LSI(但精度较差)或检查硬件连接[^3]。###三、RTC初始化配置RTC的分频器以获得1秒的计数:```cvoidRTC_Configuration(void){//等待RTC寄存器操作完成RTC_WaitForLastTask();//设置预分频器:32768/(32767+1)=1HzRTC_SetPrescaler(32767);RTC_WaitForLastTask();}```###四、LCD显示配置(以LCD1602为例)参考引用[1]中STM32F103C8T6驱动LCD1602的代码,适配到野火指南者开发板:####1.硬件连接假设使用GPIOA控制LCD1602:-PA0:RS-PA1:RW-PA2:E-PA3~PA6:DB4~DB7(4位模式)####2.LCD初始化代码(4位模式)```c//引脚定义#defineLCD_RSGPIO_Pin_0#defineLCD_RWGPIO_Pin_1#defineLCD_EGPIO_Pin_2#defineLCD_D4GPIO_Pin_3#defineLCD_D5GPIO_Pin_4#defineLCD_D6GPIO_Pin_5#defineLCD_D7GPIO_Pin_6#defineLCD_PORTGPIOAvoidLCD_Init(void){//初始化GPIO(已在RCC配置中开启GPIOA时钟)GPIO_InitTypeDefGPIO_InitStruct;GPIO_InitStruct.GPIO_Pin=LCD_RS|LCD_RW|LCD_E|LCD_D4|LCD_D5|LCD_D6|LCD_D7;GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(LCD_PORT,&GPIO_InitStruct);//初始化序列(4位模式)LCD_WriteCmd(0x33);//初始化1LCD_WriteCmd(0x32);//初始化2LCD_WriteCmd(0x28);//4位总线,2行显示LCD_WriteCmd(0x0C);//显示开,无光标LCD_WriteCmd(0x06);//光标右移LCD_WriteCmd(0x01);//清屏DelayMs(2);//清屏需要延时}```####3.LCD写命令函数```cvoidLCD_WriteCmd(uint8_tcmd){//高4位GPIO_WriteBit(LCD_PORT,LCD_RS,Bit_RESET);//命令模式GPIO_WriteBit(LCD_PORT,LCD_RW,Bit_RESET);//写操作GPIO_Write(LCD_PORT,(GPIO_ReadOutputData(LCD_PORT)&0x87)|((cmd&0xF0)>>1));LCD_EnablePulse();//低4位GPIO_Write(LCD_PORT,(GPIO_ReadOutputData(LCD_PORT)&0x87)|((cmd&0x0F)<<3));LCD_EnablePulse();}voidLCD_EnablePulse(void){GPIO_WriteBit(LCD_PORT,LCD_E,Bit_SET);DelayUs(10);GPIO_WriteBit(LCD_PORT,LCD_E,Bit_RESET);DelayUs(100);}```###五、万年历功能整合####1.读取RTC时间并显示```cvoidDisplay_Time(void){uint32_tcounter=RTC_GetCounter();//获取RTC计数器值(秒数)uint8_thours=counter/3600%24;uint8_tminutes=(counter%3600)/60;uint8_tseconds=counter%60;chartime_str[16];sprintf(time_str,"Time:%02d:%02d:%02d",hours,minutes,seconds);LCD_SetCursor(0,0);//第一行LCD_WriteString(time_str);}//日期计算(需自行实现,RTC只有秒计数器)//建议使用结构体存储日期,并通过秒数换算```>注意:STM32F103的RTC只有一个32位计数器(秒数),日期需要软件计算。建议使用标准时间戳转换库。####2.主函数流程```cintmain(void){//初始化系统RCC_Configuration();//配置时钟RTC_Configuration();//配置RTCLCD_Init();//初始化LCD//首次运行设置初始时间(如2023-01-0100:00:00)if(BKP_ReadBackupRegister(BKP_DR1)!=0xA5A5){RTC_SetCounter(0);//设置初始时间戳(需根据实际日期计算)BKP_WriteBackupRegister(BKP_DR1,0xA5A5);//标记已初始化}while(1){Display_Time();//显示时间DelayMs(1000);//每秒更新}}```###六、关键问题解决1.**RTC不运行**:-检查LSE晶振是否起振(可用示波器观察)-确保备份电池连接正常-在`RCC_Configuration()`中增加LSE故障切换代码[^3]2.**LCD1602无显示**:-调整对比度电位器-检查4位/8位模式设置-确保使能信号(E)脉冲宽度足够3.**日期计算**:```c//简易日期计算(不考虑闰秒)structDateTime{uint16_tyear;uint8_tmonth;uint8_tday;uint8_thour;uint8_tminute;uint8_tsecond;};voidSecondsToDateTime(uint32_tseconds,structDateTime*dt){//实现从1970年1月1日开始的秒数转换//可使用标准库gmtime()(需添加time.h)}```>完整工程可参考野火官方例程《死角玩转STM32》的RTC节[^1]。---###相关问题1.如何为STM32F103R6的RTC添加闹钟功能?2.如何使用STM32F103的内部温度传感器校准RTC精度?3.如何通过串口调整野火指南者开发板的RTC时间?4.如何实现STM32F103的RTC自动闰年计算?5.野火指南者开发板如何驱动OLED显示RTC时间?[^1]:STM32F103C8T6点亮LCD1602[^2]:stm32f103r6最小系统原理图[^3]:STM32f103系列单片机RCC</think>### 野火指南者STM32F103R6万年历实现(RCC + LCD) 以下是基于野火指南者开发板的万年历实现方案,整合RTC时钟、RCC配置和LCD显示功能: --- ### 一、硬件配置 1. **核心芯片**:STM32F103R6 2. **时钟源**: - HSE:8MHz外部高速晶振(主时钟) - LSE:32.768kHz低速晶振(RTC时钟源) 3. **显示设备**:LCD1602(兼容引用[1]的驱动方式) 4. **备份电源**:CR1220电池(连接VBAT引脚) --- ### 二、RCC时钟树配置 根据引用[3]配置系统时钟和RTC时钟源: ```c void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; RCC_PeriphCLKInitTypeDef PeriphClkInit = {0}; // 1. 配置LSE作为RTC时钟源 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE | RCC_OSCILLATORTYPE_LSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 启用8MHz HSE RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 启用32.768kHz LSE HAL_RCC_OscConfig(&RCC_OscInitStruct); // 2. 配置PLL(72MHz系统时钟) RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2); // 3. 将RTC时钟源设置为LSE PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC; PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit); } ``` --- ### 三、RTC初始化 ```c RTC_HandleTypeDef hrtc; void MX_RTC_Init(void) { hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 127; // LSE分频:32768/(127+1)=256Hz hrtc.Init.SynchPrediv = 255; // 同步分频:256/(255+1)=1Hz hrtc.Init.OutPut = RTC_OUTPUT_DISABLE; if (HAL_RTC_Init(&hrtc) != HAL_OK) { Error_Handler(); } // 启用备份寄存器访问 HAL_PWR_EnableBkUpAccess(); __HAL_RCC_BKP_CLK_ENABLE(); } ``` --- ### 四、LCD1602驱动(适配野火指南者) 根据引用[1]修改引脚配置: #### 1. GPIO初始化 ```c void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; // 启用GPIOB时钟 __HAL_RCC_GPIOB_CLK_ENABLE(); // 配置PB0-PB7为输出 GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 控制引脚定义 #define LCD_RS GPIO_PIN_8 // PB8 #define LCD_RW GPIO_PIN_9 // PB9 #define LCD_EN GPIO_PIN_10 // PB10 } ``` #### 2. LCD初始化 ```c void LCD_Init(void) { HAL_Delay(50); LCD_Cmd(0x38); // 8位模式,2行显示 LCD_Cmd(0x0C); // 开启显示,关闭光标 LCD_Cmd(0x06); // 地址自动增加 LCD_Cmd(0x01); // 清屏 HAL_Delay(2); } ``` --- ### 五、万年历功能整合 #### 1. 主函数实现 ```c int main(void) { HAL_Init(); SystemClock_Config(); LCD_GPIO_Init(); LCD_Init(); MX_RTC_Init(); // 首次上电初始化时间 RTC_DateTypeDef date = {.Year=23, .Month=10, .Date=1, .WeekDay=RTC_WEEKDAY_MONDAY}; RTC_TimeTypeDef time = {.Hours=12, .Minutes=0, .Seconds=0}; HAL_RTC_SetTime(&hrtc, &time, RTC_FORMAT_BIN); HAL_RTC_SetDate(&hrtc, &date, RTC_FORMAT_BIN); while (1) { // 获取并显示时间 RTC_TimeTypeDef currentTime; RTC_DateTypeDef currentDate; HAL_RTC_GetTime(&hrtc, &currentTime, RTC_FORMAT_BIN); HAL_RTC_GetDate(&hrtc, &currentDate, RTC_FORMAT_BIN); // 显示到LCD1602 char buf[17]; sprintf(buf, "Date:%02d/%02d/%02d", currentDate.Date, currentDate.Month, currentDate.Year+2000); LCD_SetCursor(0, 0); LCD_String(buf); sprintf(buf, "Time:%02d:%02d:%02d", currentTime.Hours, currentTime.Minutes, currentTime.Seconds); LCD_SetCursor(0, 1); LCD_String(buf); HAL_Delay(1000); } } ``` #### 2. 闰年自动处理 ```c uint8_t Is_Leap_Year(uint16_t year) { return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0); } ``` --- ### 六、关键调试要点 1. **LSE不起振**: ```c // 在main()中添加检测 if(__HAL_RCC_GET_FLAG(RCC_FLAG_LSERDY) == RESET) { // 处理晶振故障 } ``` 2. **RTC备份域复位**: ```c // 在RTC初始化前添加 __HAL_RCC_BACKUPRESET_FORCE(); __HAL_RCC_BACKUPRESET_RELEASE(); ``` 3. **LCD显示优化**: ```c void LCD_String(char *str) { while (*str) LCD_Data(*str++); } ``` > 完整工程可参考野火官方例程《死角玩转STM32》的RTC和LCD节[^1][^3] --- ### 相关问题 1. 如何为STM32F103R6的RTC添加温度补偿功能? 2. 野火指南者开发板如何驱动I2C接口的OLED显示日历? 3. 如何通过按键调整STM32的RTC日期和时间? 4. STM32F103的备份寄存器(BKP)在RTC应用中的使用方法? 5. 如何计算和显示STM32万年历的农历日期? [^1]: STM32F103C8T6点亮LCD1602 [^2]: stm32f103r6最小系统原理图 [^3]: STM32f103系列单片机 RCC
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值