【STM32炒冷饭】GPIO和外部中断(标准外设库)
用STM32系列单片机做项目已经一年了,突然想开个新坑写点东西,相当于自己的理解吧,有空就更,搭建开发环境和配置工程就不多说了,网上大把教程,介于本人实在是看不惯MDK的UI风格,所以一直使用VSCode+Keil Assist+MDK-ARM的开发方式
直接进入主题,本系列会写标准外设库和HAL库两版,使用的单片机为STM32F407ZGT6,使用了正点原子提供的SYSTEM文件夹,因为库函数本没有提供延时函数,为了方便直接使用正点原子提供的延时函数,后面也会讲如何使用系统滴答时钟构建延时函数。
关于F4系列的系统时钟
在我们使用STM32CubeMX构建工程的时候,在时钟树内可以直观的看到系统的锁相环时钟、分频、倍频,更为简单的可以直接设置你所想要的频率,STM32CubeMX会帮你自动配置好,那么在使用标准外设库时,我们没有类似的工具,而且F4系列与F1不同的是,F1已经默认为外部高速晶振为8MHz,并且将系统时钟设置为最高的72MHz,在F4系列中,我的板子HSE晶振为8MHz,那么我们想使用F407能跑到的最高168MHz,在system_stm32f4xx.c这个文件中,将宏定义改为#define PLL_M 8即可
一、什么是GPIO
GPIO的全程是(General-purpose input/output)通用输入输出口,简单的理解就是可以输出1或者0;我们来看STM32的GPIO的基本结构: 可以看到通过下方的P-MOS和N-MOS可以设置引脚为推挽或者开漏,通过内部的上拉或者下拉电阻可以直接把引脚输出强高电平。这里的5V容忍是指引脚可以承受5V的电压,但并不是所有的引脚都可以承受5V的电压,在STMF4xx系列的数据手册中有明确标明能够容忍5V电压的管脚,在这里并不推荐直接给管脚接入5V电压,该芯片的VDD电压标称3.3V,不建议给GPIO直接输入5V电压。 下面我们分别通过发光二极管、蜂鸣器、按键来验证引脚的输入输出功能:
二、代码编写
1.输出部分
首先我们配置LED和蜂鸣器的相关管脚: LED的阴极与STM32的GPIO相连,阳极通过一个分压电阻接到3.3V,那么当我们的GPIO输出低电平时,LED就能正常点亮; 通过宏定义先定义相关的管脚,使用宏定义的目的是为了提高代码的可移植性,也为了美观和便于理解阅读。这里也使用到了正点原子sys.c文件中提供的直接操作GPIO寄存器的接口函数,便于我们用51的思想去理解GPIO,51的寄存器数量在STM32面前九牛一毛,背寄存器显然是不太现实的,但是并不能完全不懂寄存器的操作。
#define LEDx_GPIO_PROT GPIOF
#define LEDx_RCC_CLOCK RCC_AHB1Periph_GPIOF
#define LED0_PIN_NUM GPIO_Pin_9
#define LED1_PIN_NUM GPIO_Pin_10
#define LED_NUM_0 PFout(9)
#define LED_NUM_1 PFout(10)
typedef enum
{
LED_ON = 0,
LED_OFF
} LedStatus_TypeDef_t;
上面我们枚举了LED的两种状态,便于理解;
在GPIO的初始化中,根据实际硬件,将GPIO设置为推挽输出模式;这里设置上拉或者下拉都无所谓,推挽输出本就可以直接输出强力高电平和低电平,开漏输出只能输出低电平,需要外部上拉电阻才能输出高电平。 下面提供了两个函数,只是对ST提供的GPIO相关函数进行了封装方便使用;
void Led_Init(LedStatus_TypeDef_t InitState)
{
GPIO_InitTypeDef GPIO_InitSturcture;
RCC_AHB1PeriphClockCmd(LEDx_RCC_CLOCK, ENABLE);
GPIO_InitSturcture.GPIO_Pin = LED0_PIN_NUM | LED1_PIN_NUM;
GPIO_InitSturcture.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitSturcture.GPIO_OType = GPIO_OType_PP;
GPIO_InitSturcture.GPIO_PuPd = GPIO_PuPd_DOWN;
GPIO_InitSturcture.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(LEDx_GPIO_PROT, &GPIO_InitSturcture);
LED_NUM_0 = InitState;
LED_NUM_1 = InitState;
}
void Led_SetState(uint16_t LedPinx, LedStatus_TypeDef_t LedState)
{
GPIO_WriteBit(LEDx_GPIO_PROT, LedPinx, (BitAction)LedState);
}
void Led_ToggleState(uint16_t LedPinx)
{
GPIO_ToggleBits(LEDx_GPIO_PROT, LedPinx);
}
蜂鸣器同理: 硬件电路上使用一个三极管驱动蜂鸣器,个人认为应该在蜂鸣器两端并联一个0.1uF的陶瓷电容,感性元件接通关断的瞬间会产生一个尖刺,可使用0.1uF的电容消除尖刺。S8050是一个NPN型的三极管,基极输入高电平时导通。
#define BUZZER_GPIO_PORT GPIOF
#define BUZZER_RCC_CLOCK RCC_AHB1Periph_GPIOF
#define BUZZER_PIN_NUM GPIO_Pin_8
typedef enum
{
BUZZER_OFF = 0,
BUZZER_ON
} BuzzerStatus_TypeDef_t;
void Buzzer_Init(void)
{
GPIO_InitTypeDef GPIO_InitSturcture;
RCC_AHB1PeriphClockCmd(BUZZER_RCC_CLOCK, ENABLE);
GPIO_InitSturcture.GPIO_Pin = BUZZER_PIN_NUM;
GPIO_InitSturcture.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitSturcture.GPIO_OType = GPIO_OType_PP;
GPIO_InitSturcture.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_InitSturcture.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(BUZZER_GPIO_PORT, &GPIO_InitSturcture);
}
void Buzzer_SetState(BuzzerStatus_TypeDef_t BuzzerState)
{
GPIO_WriteBit(BUZZER_GPIO_PORT, BUZZER_PIN_NUM, (BitAction)BuzzerState);
}
void Buzzer_ToggleState(void)
{
GPIO_ToggleBits(BUZZER_GPIO_PORT, BUZZER_PIN_NUM);
}
void Buzzer_Beep(void)
{
Buzzer_ToggleState();
delay_ms(10);
Buzzer_ToggleState();
}
2.输入部分
通过原理图可以看到,当按键按下时将GPIO直接与GND相接,我们可以通过判断引脚的输入电平是高电平还是低电平进行判断按键按下与否:
#define KEYx_GPIO_PORT GPIOE
#define WAKE_UP_GPIO_PORT GPIOA
#define KEYx_RCC_CLOCK RCC_AHB1Periph_GPIOE
#define WAKE_UP_RCC_CLOCK RCC_AHB1Periph_GPIOA
#define KEY0_PIN_NUM GPIO_Pin_4
#define KEY1_PIN_NUM GPIO_Pin_3
#define KEY2_PIN_NUM GPIO_Pin_2
#define WAKE_UP_PIN_NUM GPIO_Pin_0
#define KEY0 PEin(4)
#define KEY1 PEin(3)
#define KEY2 PEin(2)
#define WAKE_UP PAin(0)
typedef enum
{
KEY_DOWN = 0,
KEY_UP
} KeyStatus_TypeDef_t;
#define KEY_MODE_NORMAL 0x00
#define KEY_MODE_INTERRUPT 0x01
#define KEY_TRIGGER_MODE KEY_MODE_NORMAL
void Key_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(KEYx_RCC_CLOCK, ENABLE);
RCC_AHB1PeriphClockCmd(WAKE_UP_RCC_CLOCK, ENABLE);
GPIO_InitStructure.GPIO_Pin = KEY0_PIN_NUM | KEY1_PIN_NUM | KEY2_PIN_NUM;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_Init(KEYx_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = WAKE_UP_PIN_NUM;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN;
GPIO_Init(WAKE_UP_GPIO_PORT, &GPIO_InitStructure);
#if KEY_TRIGGER_MODE
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource2);
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource3);
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource4);
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
EXTI_InitStructure.EXTI_Line = EXTI_Line2 | EXTI_Line3 | EXTI_Line4;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x03;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x01;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
#endif
}
uint8_t Key_Scan(void)
{
static uint8_t KeyState = KEY_UP;
if (KeyState && (KEY0 == KEY_DOWN || KEY1 == KEY_DOWN || KEY2 == KEY_DOWN || WAKE_UP == KEY_UP))
{
delay_ms(10);
KeyState = KEY_DOWN;
if (KEY0 == KEY_DOWN)
{
Buzzer_Beep();
return 1;
}
else if (KEY1 == KEY_DOWN)
{
Buzzer_Beep();
return 2;
}
else if (KEY2 == KEY_DOWN)
{
Buzzer_Beep();
return 3;
}
else if (WAKE_UP == KEY_UP)
{
Buzzer_Beep();
return 4;
}
}
else if (KEY0 == KEY_UP && KEY1 == KEY_UP && KEY2 == KEY_UP && WAKE_UP == KEY_DOWN)
KeyState = KEY_UP;
return 0;
}
细心地小伙伴已经发现,在按键的初始化代码中,加入了外部中断的初始化,在这里我通过预编译指令进行对按键模式的初始化,通过改变对按键模式的宏定义可以选择是否使能按键触发外部中断的功能
验证功能
在主函数中加入这样一段代码,使用按键来改变LED的状态;
switch (Key_Scan())
{
case 1:
Led_ToggleState(LED0_PIN_NUM);
break;
case 2:
Led_ToggleState(LED1_PIN_NUM);
break;
case 3:
Led_ToggleState(LED0_PIN_NUM);
Led_ToggleState(LED1_PIN_NUM);
break;
}
烧录验证: GIF中无法听到按下按键时蜂鸣器的bibi提升音;
|