【stm32】GPIO详解
这里写目录标题
- 1.GPIO简介
- 2.GPIO框架
- 3.库函数的实现过程
- 3.1 stm32f10x.h地址映射
- 3.2 gpio库函数详解
- ①void GPIO_DeInit(GPIO_TypeDef* GPIOx);
- ②void GPIO_AFIODeInit(void);
- ③void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
- ④void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);
- ⑤uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
- ⑥uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
- ⑦uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
- ⑧uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
- ⑨void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
- ⑩void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
- ⑪void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
- ⑫void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
- 4.工程案例
- 5.GPIO位带操作
1.GPIO简介
GPIO是通用输入输出端口的简称,简单来说就是STM32可控制的引脚,STM32芯片的GPIO引脚与外部设备连接起来,从而实现与外部通讯、 控制以及数据采集的功能。STM32芯片的GPIO被分成很多组,每组有16个引脚,如型号为STM32F103VET6型号的芯片有GPIOA、GPIOB、 GPIOC至GPIOE共5组GPIO,芯片一共100个引脚,其中GPIO就占了一大部分,所有的GPIO引脚都有基本的输入输出功能。
最基本的输出功能是由STM32控制引脚输出高、低电平,实现开关控制,如把GPIO引脚接入到LED灯,那就可以控制LED灯的亮灭, 引脚接入到继电器或三极管,那就可以通过继电器或三极管控制外部大功率电路的通断。
最基本的输入功能是检测外部输入电平,如把GPIO引脚连接到按键,通过电平高低区分按键是否被按下。
2.GPIO框架

2.1基本结构分析
①保护二极管及上、下拉电阻
引脚的两个保护二级管可以防止引脚外部过高或过低的电压输入,当引脚电压高于VDD时, 上方的二极管导通,当引脚电压低于VSS时,下方的二极管导通,防止不正常电压引入芯片导致芯片烧毁。 尽管有这样的保护,并不意味着STM32的引脚能直接外接大功率驱动器件,如直接驱动电机,强制驱动要么电机不转,要么导致芯片烧坏,必须要加大功率及隔离电路驱动。
②P-MOS管和N-MOS管
GPIO引脚线路经过两个保护二极管后,向上流向“输入模式”结构,向下流向“输出模式”结构。先看输出模式部分,线路经过一个由P-MOS和N-MOS管组成的单元电路。 这个结构使GPIO具有了“推挽输出”和“开漏输出”两种模式。
所谓的推挽输出模式,是根据这两个MOS管的工作方式来命名的。在该结构中输入高电平时,经过反向后,上方的P-MOS导通,下方的N-MOS关闭, 对外输出高电平;而在该结构中输入低电平时,经过反向后,N-MOS管导通,P-MOS关闭,对外输出低电平。当引脚高低电平切换时,两个管子轮流导通, P管负责灌电流,N管负责拉电流,使其负载能力和开关速度都比普通的方式有很大的提高。推挽输出的低电平为0伏,高电平为3.3伏, 具体参考图 推挽等效电路 ,它是推挽输出模式时的等效电路。
而在开漏输出模式时,上方的P-MOS管完全不工作。如果我们控制输出为0,低电平,则P-MOS管关闭,N-MOS管导通,使输出接地, 若控制输出为1 (它无法直接输出高电平)时,则P-MOS管和N-MOS管都关闭,所以引脚既不输出高电平,也不输出低电平,为高阻态。 为正常使用时必须外部接上拉电阻,参考图 开漏电路 中等效电路。它具有“线与”特性,也就是说,若有很多个开漏模式引脚连接到一起时, 只有当所有引脚都输出高阻态,才由上拉电阻提供高电平,此高电平的电压为外部上拉电阻所接的电源的电压。若其中一个引脚为低电平, 那线路就相当于短路接地,使得整条线路都为低电平,0伏。
开漏输出一般应用在I2C、SMBUS通讯等需要“线与”功能的总线电路中。除此之外,还用在电平不匹配的场合,如需要输出5伏的高电平, 就可以在外部接一个上拉电阻,上拉电源为5伏,并且把GPIO设置为开漏模式,当输出高阻态时,由上拉电阻和电源向外输出5伏的电平

③输出数据寄存器
前面提到的双MOS管结构电路的输入信号,是由GPIO“输出数据寄存器GPIOx_ODR”提供的,因此我们通过修改输出数据寄存器的值就可以修改GPIO引脚的输出电平。 而“置位/复位寄存器GPIOx_BSRR”可以通过修改输出数据寄存器的值从而影响电路的输出。
④复用功能输出
“复用功能输出”中的“复用”是指STM32的其它片上外设对GPIO引脚进行控制,此时GPIO引脚用作该外设功能的一部分,算是第二用途。 从其它外设引出来的“复用功能输出信号”与GPIO本身的数据据寄存器都连接到双MOS管结构的输入中,通过图中的梯形结构作为开关切换选择。
例如我们使用USART串口通讯时,需要用到某个GPIO引脚作为通讯发送引脚,这个时候就可以把该GPIO引脚配置成USART串口复用功能,由串口外设控制该引脚,发送数据。
⑤输入数据寄存器
看GPIO结构框图的上半部分,GPIO引脚经过内部的上、下拉电阻,可以配置成上/下拉输入,然后再连接到施密特触发器,信号经过触发器后, 模拟信号转化为0、1的数字信号,然后存储在“输入数据寄存器GPIOx_IDR”中,通过读取该寄存器就可以了解GPIO引脚的电平状态。
⑥复用功能输入
与“复用功能输出”模式类似,在“复用功能输入模式”时,GPIO引脚的信号传输到STM32其它片上外设,由该外设读取引脚状态。
同样,如我们使用USART串口通讯时,需要用到某个GPIO引脚作为通讯接收引脚,这个时候就可以把该GPIO引脚配置成USART串口复用功能,使USART可以通过该通讯引脚的接收远端数据。
⑦模拟输入输出
当GPIO引脚用于ADC采集电压的输入通道时,用作“模拟输入”功能,此时信号是不经过施密特触发器的,因为经过施密特触发器后信号只有0、1两种状态, 所以ADC外设要采集到原始的模拟信号,信号源输入必须在施密特触发器之前。类似地,当GPIO引脚用于DAC作为模拟电压输出通道时,此时作为“模拟输出”功能, DAC的模拟信号输出就不经过双MOS管结构,模拟信号直接输出到引脚。
2.2 GPIO工作模式
由GPIO的结构决定了GPIO可以配置成以下模式:
typedef enum
{
GPIO_Mode_AIN = 0x0, // 模拟输入
GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入
GPIO_Mode_IPD = 0x28, // 下拉输入
GPIO_Mode_IPU = 0x48, // 上拉输入
GPIO_Mode_Out_OD = 0x14, // 开漏输出
GPIO_Mode_Out_PP = 0x10, // 推挽输出
GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出
GPIO_Mode_AF_PP = 0x18 // 复用推挽输出
} GPIOMode_TypeDef;
在固件库中,GPIO总共有8种细分的工作模式,稍加整理可以大致归类为以下三类:
输入模式(模拟/浮空/上拉/下拉)
在输入模式时,施密特触发器打开,输出被禁止,可通过输入数据寄存器GPIOx_IDR读取I/O状态。其中输入模式,可设置为上拉、 下拉、浮空和模拟输入四种。上拉和下拉输入很好理解,默认的电平由上拉或者下拉决定。浮空输入的电平是不确定的,完全由外部的输入决定, 一般接按键的时候用的是这个模式。模拟输入则用于ADC采集。
输出模式(推挽/开漏)
在输出模式中,推挽模式时双MOS管以轮流方式工作,输出数据寄存器GPIOx_ODR可控制I/O输出高低电平。开漏模式时,只有N-MOS管工作, 输出数据寄存器可控制I/O输出高阻态或低电平。输出速度可配置,有2MHz10MHz50MHz的选项。此处的输出速度即I/O支持的高低电平状态最高切换频率, 支持的频率越高,功耗越大,如果功耗要求不严格,把速度设置成最大即可。
在输出模式时施密特触发器是打开的,即输入可用,通过输入数据寄存器GPIOx_IDR可读取I/O的实际状态。
复用功能(推挽/开漏)
复用功能模式中,输出使能,输出速度可配置,可工作在开漏及推挽模式,但是输出信号源于其它外设,输出数据寄存器GPIOx_ODR无效; 输入可用,通过输入数据寄存器可获取I/O实际状态,但一般直接用外设的寄存器来获取该数据信号。

2.2.1输入模式
浮空输入

浮空输入模式下,I/O端口的电平信号直接进入输入数据寄存器。也就是说,I/O的电平状态是不确定的,完全由外部输入决定;如果在该引脚悬空(在无信号输入)的情况下,读取该端口的电平是不确定的。所以在要读取外部信号时通常配置IO口为浮空输入模式。
上拉输入

上拉输入模式下,I/O端口的电平信号直接进入输入数据寄存器。但是在I/O端口悬空(在无信号输入)的情况下,输入端的电平可以保持在高电平;并且在I/O端口输入为低电平的时候,输入端的电平为低电平。
下拉输入

下拉输入模式下,IO口工作方式刚好和上拉模式相反。I/O端口的电平信号直接进入输入数据寄存器。但是在I/O端口悬空(在无信号输入)的情况下,输入端的电平可以保持在低电平;并且在I/O端口输入为高电平的时候,输入端为高电平。
模拟输入

模拟输入模式下,I/O端口的模拟信号(电压信号,而非电平信号)直接模拟输入到片上外设模块,比如ADC模块等等。
2.2.2输出模式
开漏输出

开漏输出模式下,通过设置位设置/清除寄存器或者输出数据寄存器的值,控制MOS管的导通。这里要注意N-MOS管,当设置输出的值为高电平的时候,N-MOS管处于关闭状态,此时I/O端口的电平就不会由输出的高低电平决定,而是由I/O端口外部的上拉或者下拉决定;当设置输出的值为低电平的时候,N-MOS管处于开启状态,此时I/O端口的电平就是低电平。同时,I/O端口的电平也可以通过输入电路进行读取;注意,I/O端口的电平不一定是输出的电平。通常使用开漏输出时外部要加一个上拉电阻。
推挽输出

推挽输出模式下,通过设置位设置/清除寄存器或者输出数据寄存器的值,控制P-MOS管和N-MOS管的导通来控制IO口输出高电平还是低电平。这里要注意P-MOS管和N-MOS管,当设置输出的值为1的时候,P-MOS管处于开启状态,N-MOS管处于关闭状态,此时I/O端口的电平就由P-MOS管决定为高电平;当设置输出的值为0的时候,P-MOS管处于关闭状态,N-MOS管处于开启状态,此时I/O端口的电平就由N-MOS管决定为低电平。同时,I/O端口的电平也可以通过输入电路进行读取;注意,此时I/O端口的电平一定是输出的电平。
复用开漏输出

开漏复用输出模式,与开漏输出模式很是类似。只是输出的高低电平的来源,不是让CPU直接写输出数据寄存器,取而代之利用片上外设模块的复用功能输出来决定的。
复用推挽输出

推挽复用输出模式,与推挽输出模式很是类似。只是输出的高低电平的来源,不是让CPU直接写输出数据寄存器,取而代之利用片上外设模块的复用功能输出来决定的。
3.库函数的实现过程
3.1 stm32f10x.h地址映射
stm32f10x.h 文件
//寄存器的值常常是芯片外设自动更改的,即使CPU没有执行程序,也有可能发生变化
//编译器有可能会对没有执行程序的变量进行优化
>//volatile表示易变的变量,防止编译器优化,
#define __IO volatile //这段代码在core_cm3.h中
typedef unsigned int uint32_t;
typedef unsigned short uint16_t;
>// GPIO 寄存器结构体定义
typedef struct
{
__IO uint32_t CRL; // 端口配置低寄存器, 地址偏移0X00
__IO uint32_t CRH; // 端口配置高寄存器, 地址偏移0X04
__IO uint32_t IDR; // 端口数据输入寄存器, 地址偏移0X08
__IO uint32_t ODR; // 端口数据输出寄存器, 地址偏移0X0C
__IO uint32_t BSRR; // 端口位设置/清除寄存器,地址偏移0X10
__IO uint32_t BRR; // 端口位清除寄存器, 地址偏移0X14
__IO uint32_t LCKR; // 端口配置锁定寄存器, 地址偏移0X18
} GPIO_TypeDef;
这段代码在每个结构体成员前增加了一个“__IO”前缀,它的原型在这段代码的第一行,代表了C 语言中的关键字“volatile”,在 C 语言中该关键字用于表示变量是易变的,要求编译器不要优化。这些结构体内的成员,都代表着寄存器,而寄存器很多时候是由外设或 STM32 芯片状态修改的,也就是说即使 CPU 不执行代码修改这些变量,变量的值也有可能被外设修改、更新,所以每次使用这些变量的时候,我们都要求 CPU 去该变量的地址重新访问。若没有这个关键字修饰,在某些情况下,编译器认为没有代码修改该变量,就直接从 CPU 的某个缓存获取该变量值,这时可以加快执行速度,但该缓存中的是陈旧数据,与我们要求的寄存器最新状态可能会有出入。
>/* 片上外设基地址 */
#define PERIPH_BASE ((unsigned int)0x40000000)
/*APB2 总线基地址 */
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
/* AHB 总线基地址 */
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
/*GPIO 外设基地址 */
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)
/*RCC 外设基地址 */
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
>GPIO 外设声明
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
// RCC 外设声明
#define RCC ((RCC_TypeDef *) RCC_BASE)
/*RCC 的 AHB1 时钟使能寄存器地址, 强制转换成指针 */
#define RCC_APB2ENR *(unsigned int*)(RCC_BASE+0x18)
3.2 gpio库函数详解
void GPIO_DeInit(GPIO_TypeDef* GPIOx);//复位GPIO外设为缺省值
void GPIO_AFIODeInit(void);//复位AFIO外设
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);//初始化GPIO口
void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);//给GPIO结构体变量赋一个默认值
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//读取输入数据寄存器某个端口的输入值,返回值是高低电平(读取GPIOx_IDR)
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);//读取GPIO的每一位的值,返回值是16位的数据,每一位代表一个端口值(读取GPIOx_IDR)
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//读取输出寄存器的某一个位(读取GPIOx_ODR)
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);//读取整个输出寄存器(读取GPIOx_ODR)
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//把指定的端口设置成高电平(操作GPIOx_BSRR)
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//把指定的端口设置成低电平(操作GPIOx_BRR)
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);//设置某个端口设置为高低电平(设置高电平操作GPIOx_BSRR,低电平操作GPIOx_BRR)
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);//对GPIOx 16个端口进行写入操作(操作GPIOx_ODR的值)
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
void GPIO_EventOutputCmd(FunctionalState NewState);
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface);
①void GPIO_DeInit(GPIO_TypeDef* GPIOx);
函数功能:将 GPIOx 外设寄存器解除初始化,恢复至硬件复位默认值。
void GPIO_DeInit(GPIO_TypeDef* GPIOx)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
if (GPIOx == GPIOA)
{
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA, DISABLE);
}
else if (GPIOx == GPIOB)
{
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOB, DISABLE);
}
else if (GPIOx == GPIOC)
{
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOC, ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOC, DISABLE);
}
else if (GPIOx == GPIOD)
{
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOD, ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOD, DISABLE);
}
else if (GPIOx == GPIOE)
{
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOE, ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOE, DISABLE);
}
else if (GPIOx == GPIOF)
{
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOF, ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOF, DISABLE);
}
else
{
if (GPIOx == GPIOG)
{
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOG, ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOG, DISABLE);
}
}
}
RCC_APB2PeriphResetCmd函数功能操作APB2 外设复位寄存器 (RCC_APB2RSTR)
void RCC_APB2PeriphResetCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
{
/* Check the parameters */
assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
assert_param(IS_FUNCTIONAL_STATE(NewState));
if (NewState != DISABLE)
{
RCC->APB2RSTR |= RCC_APB2Periph;
}
else
{
RCC->APB2RSTR &= ~RCC_APB2Periph;
}
}


②void GPIO_AFIODeInit(void);
函数功能:复位AFIO外设。
void GPIO_AFIODeInit(void)
{
RCC_APB2PeriphResetCmd(RCC_APB2Periph_AFIO, ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_AFIO, DISABLE);
}

③void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
函数功能:初始化GPIO口
用结构体的参数初始化GPIO口,先定义一个结构体变量,然后在把结构体赋值,最后调用此函数
/*GPIO 引脚号定义 */
2 #define GPIO_Pin_0 (uint16_t)0x0001) /*!< 选择 Pin0 (1<<0) */
3 #define GPIO_Pin_1 ((uint16_t)0x0002) /*!< 选择 Pin1 (1<<1)*/
4 #define GPIO_Pin_2 ((uint16_t)0x0004) /*!< 选择 Pin2 (1<<2)*/
5 #define GPIO_Pin_3 ((uint16_t)0x0008) /*!< 选择 Pin3 (1<<3)*/
6 #define GPIO_Pin_4 ((uint16_t)0x0010) /*!< 选择 Pin4 */
7 #define GPIO_Pin_5 ((uint16_t)0x0020) /*!< 选择 Pin5 */
8 #define GPIO_Pin_6 ((uint16_t)0x0040) /*!< 选择 Pin6 */
9 #define GPIO_Pin_7 ((uint16_t)0x0080) /*!< 选择 Pin7 */
10 #define GPIO_Pin_8 ((uint16_t)0x0100) /*!< 选择 Pin8 */
11 #define GPIO_Pin_9 ((uint16_t)0x0200) /*!< 选择 Pin9 */
12 #define GPIO_Pin_10 ((uint16_t)0x0400) /*!< 选择 Pin10 */
13 #define GPIO_Pin_11 ((uint16_t)0x0800) /*!< 选择 Pin11 */
14 #define GPIO_Pin_12 ((uint16_t)0x1000) /*!< 选择 Pin12 */
15 #define GPIO_Pin_13 ((uint16_t)0x2000) /*!< 选择 Pin13 */
16 #define GPIO_Pin_14 ((uint16_t)0x4000) /*!< 选择 Pin14 */
17 #define GPIO_Pin_15 ((uint16_t)0x8000) /*!< 选择 Pin15 */
18 #define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< 选择全部引脚 */
/**
2 * GPIO 输出速率枚举定义
3 */
4 typedef enum
5 {
6 GPIO_Speed_10MHz = 1, // 10MHZ (01)b
7 GPIO_Speed_2MHz, // 2MHZ (10)b
8 GPIO_Speed_50MHz // 50MHZ (11)b
9 } GPIOSpeed_TypeDef;
10
11 /**
12 * GPIO 工作模式枚举定义
13 */
14 typedef enum
15 {
16 GPIO_Mode_AIN = 0x0, // 模拟输入 (0000 0000)b
17 GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入 (0000 0100)b
18 GPIO_Mode_IPD = 0x28, // 下拉输入 (0010 1000)b
19 GPIO_Mode_IPU = 0x48, // 上拉输入 (0100 1000)b
20 GPIO_Mode_Out_OD = 0x14, // 开漏输出 (0001 0100)b
21 GPIO_Mode_Out_PP = 0x10, // 推挽输出 (0001 0000)b
23 GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出 (0001 1100)b
24 GPIO_Mode_AF_PP = 0x18 // 复用推挽输出 (0001 1000)b
25 } GPIOMode_TypeDef;

/* GPIO 初始化结构体类型定义*/
typedef struct
{
uint16_t GPIO_Pin; /*!< 选择要配置的 GPIO 引脚可输入 GPIO_Pin_ 定义的宏 */
GPIOSpeed_TypeDef GPIO_Speed; /*!< 选择 GPIO 引脚的速率可输入 GPIOSpeed_TypeDef 定义的枚举值 */
GPIOMode_TypeDef GPIO_Mode; /*!< 选择 GPIO 引脚的工作模式可输入 GPIOMode_TypeDef 定义的枚举值 */
} GPIO_InitTypeDef;
/**
2 * 函数功能:初始化引脚模式
3 * 参数说明:GPIOx,该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址
4 * GPIO_InitTypeDef:GPIO_InitTypeDef 结构体指针,指向初始化变量
5 */
6 void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
7 {
8 uint32_t currentmode =0x00,currentpin = 0x00,pinpos = 0x00,pos = 0x00;
9 uint32_t tmpreg = 0x00, pinmask = 0x00;
10
11 /*---------------- GPIO 模式配置 -------------------*/
12 // 把输入参数 GPIO_Mode 的低四位暂存在 currentmode
13 currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) &
14 ((uint32_t)0x0F);
15
16 // bit4 是 1 表示输出,bit4 是 0 则是输入
17 // 判断 bit4 是 1 还是 0,即首选判断是输入还是输出模式
18 if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) &
19 ((uint32_t)0x10)) != 0x00)
20 {
21 // 输出模式则要设置输出速度
22 currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
23 }
24 /*-----GPIO CRL 寄存器配置 CRL 寄存器控制着低 8 位 IO- ----*/
25 // 配置端口低 8 位,即 Pin0~Pin7
26 if (((uint32_t)GPIO_InitStruct->GPIO_Pin &
27 ((uint32_t)0x00FF)) != 0x00)
28 {
29 // 先备份 CRL 寄存器的值
30 tmpreg = GPIOx->CRL;
31
32 // 循环,从 Pin0 开始配对,找出具体的 Pin
33 for (pinpos = 0x00; pinpos < 0x08; pinpos++)
34 {
35 // pos 的值为 1 左移 pinpos 位
36 pos = ((uint32_t)0x01) << pinpos;
37
38 // 令 pos 与输入参数 GPIO_PIN 作位与运算
39 currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
40
41 //若 currentpin=pos, 则找到使用的引脚
42 if (currentpin == pos)
43 {
44 //pinpos 的值左移两位 (乘以 4), 因为寄存器中 4 个位配置一个引脚
45 pos = pinpos << 2;
46 //把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变
47 pinmask = ((uint32_t)0x0F) << pos;
48 tmpreg &= ~pinmask;
49
50 // 向寄存器写入将要配置的引脚的模式
51 tmpreg |= (currentmode << pos);
52
53 // 判断是否为下拉输入模式
54 if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
55 {
56 // 下拉输入模式, 引脚默认置 0, 对 BRR 寄存器写 1 对引脚置 0
57 GPIOx->BRR = (((uint32_t)0x01) << pinpos);
58 }
59 else
60 {
61 // 判断是否为上拉输入模式
62 if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
63 {
64 // 上拉输入模式, 引脚默认值为 1, 对 BSRR 寄存器写 1 对引脚
置 1
65 GPIOx->BSRR = (((uint32_t)0x01) << pinpos);
66 }
67 }
68 }
69 }
70 // 把前面处理后的暂存值写入到 CRL 寄存器之中
71 GPIOx->CRL = tmpreg;
72 }
73 /*--------GPIO CRH 寄存器配置 CRH 寄存器控制着高 8 位 IO- -----*/
74 // 配置端口高 8 位,即 Pin8~Pin15
75 if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
76 {
77 // // 先备份 CRH 寄存器的值
78 tmpreg = GPIOx->CRH;
79
80 // 循环,从 Pin8 开始配对,找出具体的 Pin
81 for (pinpos = 0x00; pinpos < 0x08; pinpos++)
82 {
83 pos = (((uint32_t)0x01) << (pinpos + 0x08));
84
85 // pos 与输入参数 GPIO_PIN 作位与运算
86 currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);
87
88 //若 currentpin=pos, 则找到使用的引脚
89 if (currentpin == pos)
90 {
91 //pinpos 的值左移两位 (乘以 4), 因为寄存器中 4 个位配置一个引脚
92 pos = pinpos << 2;
93
94 //把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变
95 pinmask = ((uint32_t)0x0F) << pos;
96 tmpreg &= ~pinmask;
97
98 // 向寄存器写入将要配置的引脚的模式
99 tmpreg |= (currentmode << pos);
100
101 // 判断是否为下拉输入模式
102 if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
103 {
104 // 下拉输入模式, 引脚默认置 0, 对 BRR 寄存器写 1 可对引脚置 0
105 GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
106 }
107 // 判断是否为上拉输入模式
108 if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
109 {
110 // 上拉输入模式, 引脚默认值为 1, 对 BSRR 寄存器写 1 可对引脚置
1
111 GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));
112 }
113 }
114 }
115 // 把前面处理后的暂存值写入到 CRH 寄存器之中
116 GPIOx->CRH = tmpreg;
117 }
118 }



先取得 GPIO_Mode 的值,判断 bit4 是 1 还是 0 来判断是输出还是输入。如果是输出则设置输出速率,即加上 GPIO_Speed 的值,输入没有速率之说,不用设置。
配置 CRL 寄存器。通过 GPIO_Pin 的值计算出具体需要初始化哪个引脚,算出后,然后把需要配置的值写入到 CRL 寄存器中,具体分析见代码注释。这里有一个比较有趣的是上/下拉输入并不是直接通过配置某一个寄存器来实现的,而是通过写 BSRR 或者 BRR 寄存器来实现。这让很多只看手册没看固件库底层源码的人摸不着头脑,因为手册的寄存器说明中没有明确的指出如何配置上拉/下拉。
/* GPIO 端口初始化范例 */
/* 选择要控制的 GPIO 引脚 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
/* 设置引脚模式为输出模式 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/* 设置引脚的输出类型为推挽输出 */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
④void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);
函数功能:给GPIO结构体变量赋一个默认值
void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct)
{
/* Reset GPIO init structure parameters values */
GPIO_InitStruct->GPIO_Pin = GPIO_Pin_All;//选择全部引脚
GPIO_InitStruct->GPIO_Speed = GPIO_Speed_2MHz;
GPIO_InitStruct->GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
}
⑤uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
函数功能:读取输入数据寄存器某个端口的输入值,返回值是高低电平(GPIOx_IDR)
typedef enum
{ Bit_RESET = 0,
Bit_SET
}BitAction;
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
uint8_t bitstatus = 0x00;
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GET_GPIO_PIN(GPIO_Pin));
if ((GPIOx->IDR & GPIO_Pin) != (uint32_t)Bit_RESET)
{
bitstatus = (uint8_t)Bit_SET;
}
else
{
bitstatus = (uint8_t)Bit_RESET;
}
return bitstatus;
}

⑥uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
函数功能:读取GPIO的每一位的值,返回值是16位的数据,每一位代表一个端口值(GPIOx_IDR)
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
return ((uint16_t)GPIOx->IDR);
}
⑦uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
函数功能:读取输出寄存器的某一个位(GPIOx_ODR)
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
uint8_t bitstatus = 0x00;
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GET_GPIO_PIN(GPIO_Pin));
if ((GPIOx->ODR & GPIO_Pin) != (uint32_t)Bit_RESET)
{
bitstatus = (uint8_t)Bit_SET;
}
else
{
bitstatus = (uint8_t)Bit_RESET;
}
return bitstatus;
}

⑧uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
函数功能:读取整个输出寄存器(GPIOx_ODR)
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
return ((uint16_t)GPIOx->ODR);
}
⑨void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
函数功能:把指定的端口设置成高电平(操作GPIOx_BSRR)
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Pin));
GPIOx->BSRR = GPIO_Pin;
}

⑩void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
函数功能:把指定的端口设置成低电平(操作GPIOx_BRR)
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GPIO_PIN(GPIO_Pin));
GPIOx->BRR = GPIO_Pin;
}


⑪void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
函数功能:设置某个端口设置为高低电平(设置高电平操作GPIOx_BSRR,低电平操作GPIOx_BRR)
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
assert_param(IS_GET_GPIO_PIN(GPIO_Pin));
assert_param(IS_GPIO_BIT_ACTION(BitVal));
if (BitVal != Bit_RESET)
{
GPIOx->BSRR = GPIO_Pin;
}
else
{
GPIOx->BRR = GPIO_Pin;
}
}
⑫void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
函数功能:对GPIOx 16个端口进行写入操作(操作GPIOx_ODR的值)
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal)
{
/* Check the parameters */
assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
GPIOx->ODR = PortVal;
}
4.工程案例
4.1 使用固件库点亮LED(GPIO输出)
硬件设计
STM32 芯片与 LED 灯的连接见图 LED 硬件原理图 ,这是一个 RGB 灯,里面由红蓝绿三个小灯构成,使用 PWM 控制时可以混合成 256 不同的颜色。

这些 LED 灯的阴极都是连接到 STM32 的 GPIO 引脚,只要我们控制 GPIO 引脚的电平输出状态,
即可控制 LED 灯的亮灭。
软件设计
编程要点
- 使能 GPIO 端口时钟;
- 初始化 GPIO 目标引脚为推挽输出模式;
- 编写简单测试程序,控制 GPIO 引脚输出高、低电平。
bsp_led.h
#ifndef __LED_H
#define __LED_H
#include "stm32f10x.h"
/* 定义LED连接的GPIO端口, 用户只需要修改下面的代码即可改变控制的LED引脚 */
// R-红色
#define LED1_GPIO_PORT GPIOB /* GPIO端口 */
#define LED1_GPIO_CLK RCC_APB2Periph_GPIOB /* GPIO端口时钟 */
#define LED1_GPIO_PIN GPIO_Pin_5 /* 连接到SCL时钟线的GPIO */
// G-绿色
#define LED2_GPIO_PORT GPIOB /* GPIO端口 */
#define LED2_GPIO_CLK RCC_APB2Periph_GPIOB /* GPIO端口时钟 */
#define LED2_GPIO_PIN GPIO_Pin_0 /* 连接到SCL时钟线的GPIO */
// B-蓝色
#define LED3_GPIO_PORT GPIOB /* GPIO端口 */
#define LED3_GPIO_CLK RCC_APB2Periph_GPIOB /* GPIO端口时钟 */
#define LED3_GPIO_PIN GPIO_Pin_1 /* 连接到SCL时钟线的GPIO */
#define ON 0
#define OFF 1
/* 使用标准的固件库控制IO*/
#define LED1(a) if (a) \
GPIO_SetBits(LED1_GPIO_PORT,LED1_GPIO_PIN);\
else \
GPIO_ResetBits(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED2(a) if (a) \
GPIO_SetBits(LED2_GPIO_PORT,LED2_GPIO_PIN);\
else \
GPIO_ResetBits(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED3(a) if (a) \
GPIO_SetBits(LED3_GPIO_PORT,LED3_GPIO_PIN);\
else \
GPIO_ResetBits(LED3_GPIO_PORT,LED3_GPIO_PIN)
/* 直接操作寄存器的方法控制IO */
#define digitalHi(p,i) {p->BSRR=i;} //输出为高电平
#define digitalLo(p,i) {p->BRR=i;} //输出低电平
#define digitalToggle(p,i) {p->ODR ^=i;} //输出反转状态
/* 定义控制IO的宏 */
#define LED1_TOGGLE digitalToggle(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED1_OFF digitalHi(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED1_ON digitalLo(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED2_TOGGLE digitalToggle(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED2_OFF digitalHi(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED2_ON digitalLo(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED3_TOGGLE digitalToggle(LED3_GPIO_PORT,LED3_GPIO_PIN)
#define LED3_OFF digitalHi(LED3_GPIO_PORT,LED3_GPIO_PIN)
#define LED3_ON digitalLo(LED3_GPIO_PORT,LED3_GPIO_PIN)
/* 基本混色,后面高级用法使用PWM可混出全彩颜色,且效果更好 */
//红
#define LED_RED \
LED1_ON;\
LED2_OFF\
LED3_OFF
//绿
#define LED_GREEN \
LED1_OFF;\
LED2_ON\
LED3_OFF
//蓝
#define LED_BLUE \
LED1_OFF;\
LED2_OFF\
LED3_ON
//黄(红+绿)
#define LED_YELLOW \
LED1_ON;\
LED2_ON\
LED3_OFF
//紫(红+蓝)
#define LED_PURPLE \
LED1_ON;\
LED2_OFF\
LED3_ON
//青(绿+蓝)
#define LED_CYAN \
LED1_OFF;\
LED2_ON\
LED3_ON
//白(红+绿+蓝)
#define LED_WHITE \
LED1_ON;\
LED2_ON\
LED3_ON
//黑(全部关闭)
#define LED_RGBOFF \
LED1_OFF;\
LED2_OFF\
LED3_OFF
void LED_GPIO_Config(void);
#endif /* __LED_H */
bsp_led.c文件
#include "bsp_led.h"
/**
* @brief 初始化控制LED的IO
* @param 无
* @retval 无
*/
void LED_GPIO_Config(void)
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启LED相关的GPIO外设时钟*/
RCC_APB2PeriphClockCmd( LED1_GPIO_CLK | LED2_GPIO_CLK | LED3_GPIO_CLK, ENABLE);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = LED1_GPIO_PIN;
/*设置引脚模式为通用推挽输出*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/*设置引脚速率为50MHz */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/*调用库函数,初始化GPIO*/
GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStructure);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = LED2_GPIO_PIN;
/*调用库函数,初始化GPIO*/
GPIO_Init(LED2_GPIO_PORT, &GPIO_InitStructure);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = LED3_GPIO_PIN;
/*调用库函数,初始化GPIOF*/
GPIO_Init(LED3_GPIO_PORT, &GPIO_InitStructure);
/* 关闭所有led灯 */
GPIO_SetBits(LED1_GPIO_PORT, LED1_GPIO_PIN);
/* 关闭所有led灯 */
GPIO_SetBits(LED2_GPIO_PORT, LED2_GPIO_PIN);
/* 关闭所有led灯 */
GPIO_SetBits(LED3_GPIO_PORT, LED3_GPIO_PIN);
}
main.c文件
#include "stm32f10x.h"
#include "bsp_led.h"
#define SOFT_DELAY Delay(0x0FFFFF);
void Delay(__IO u32 nCount);
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
/* LED 端口初始化 */
LED_GPIO_Config();
while (1)
{
LED1_ON; // 亮
SOFT_DELAY;
LED1_OFF; // 灭
LED2_ON; // 亮
SOFT_DELAY;
LED2_OFF; // 灭
LED3_ON; // 亮
SOFT_DELAY;
LED3_OFF; // 灭
/*轮流显示 红绿蓝黄紫青白 颜色*/
LED_RED;
SOFT_DELAY;
LED_GREEN;
SOFT_DELAY;
LED_BLUE;
SOFT_DELAY;
LED_YELLOW;
SOFT_DELAY;
LED_PURPLE;
SOFT_DELAY;
LED_CYAN;
SOFT_DELAY;
LED_WHITE;
SOFT_DELAY;
LED_RGBOFF;
SOFT_DELAY;
}
}
void Delay(__IO uint32_t nCount) //简单的延时函数
{
for(; nCount != 0; nCount--);
}
4.2 按键检测(GPIO输入)
硬件设计
按键机械触点断开、闭合时,由于触点的弹性作用,按键开关不会马上稳定接通或一下子断开,使用按键时会产生图按键抖动说明图 中的带波纹信号,需要用软件消抖处理滤波,不方便输入检测。本实验板连接的按键带硬件消抖功能,见图按键原理图 ,它利用电容充放电的延时,消除了波纹,从而简化软件的处理,软件只需要直接检测引脚的电平即可。


从按键的原理图可知,这些按键在没有被按下的时候,GPIO 引脚的输入状态为低电平 (按键所在的电路不通,引脚接地),当按键按下时,GPIO 引脚的输入状态为高电平 (按键所在的电路导通,引脚接到电源)。只要我们检测引脚的输入电平,即可判断按键是否被按下。
软件设计
编程要点
- 使能 GPIO 端口时钟;
- 初始化 GPIO 目标引脚为输入模式 (浮空输入);
- 编写简单测试程序,检测按键的状态,实现按键控制 LED 灯。
bsp_key.h文件
#ifndef __KEY_H
#define __KEY_H
#include "stm32f10x.h"
// 引脚定义
#define KEY1_GPIO_CLK RCC_APB2Periph_GPIOA
#define KEY1_GPIO_PORT GPIOA
#define KEY1_GPIO_PIN GPIO_Pin_0
#define KEY2_GPIO_CLK RCC_APB2Periph_GPIOC
#define KEY2_GPIO_PORT GPIOC
#define KEY2_GPIO_PIN GPIO_Pin_13
/** 按键按下标置宏
* 按键按下为高电平,设置 KEY_ON=1, KEY_OFF=0
* 若按键按下为低电平,把宏设置成KEY_ON=0 ,KEY_OFF=1 即可
*/
#define KEY_ON 1
#define KEY_OFF 0
void Key_GPIO_Config(void);
uint8_t Key_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin);
#endif /* __KEY_H */
bsp_key.c文件
#include "./led/bsp_led.h"
/**
* @brief 初始化控制LED的IO
* @param 无
* @retval 无
*/
void LED_GPIO_Config(void)
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启LED相关的GPIO外设时钟*/
RCC_APB2PeriphClockCmd( LED1_GPIO_CLK | LED2_GPIO_CLK | LED3_GPIO_CLK, ENABLE);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = LED1_GPIO_PIN;
/*设置引脚模式为通用推挽输出*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/*设置引脚速率为50MHz */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/*调用库函数,初始化GPIO*/
GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStructure);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = LED2_GPIO_PIN;
/*调用库函数,初始化GPIO*/
GPIO_Init(LED2_GPIO_PORT, &GPIO_InitStructure);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = LED3_GPIO_PIN;
/*调用库函数,初始化GPIOF*/
GPIO_Init(LED3_GPIO_PORT, &GPIO_InitStructure);
/* 关闭所有led灯 */
GPIO_SetBits(LED1_GPIO_PORT, LED1_GPIO_PIN);
/* 关闭所有led灯 */
GPIO_SetBits(LED2_GPIO_PORT, LED2_GPIO_PIN);
/* 关闭所有led灯 */
GPIO_SetBits(LED3_GPIO_PORT, LED3_GPIO_PIN);
}
void assert_failed(uint8_t* file, uint32_t line)
{
// 断言错误时执行的代码
LED1_ON;
}
main.c文件
#include "stm32f10x.h"
#include "bsp_led.h"
#include "bsp_key.h"
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
/* LED端口初始化 */
LED_GPIO_Config();
LED1_ON;
/* 按键端口初始化 */
Key_GPIO_Config();
/* 轮询按键状态,若按键按下则反转LED */
while(1)
{
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON )
{
/*LED1反转*/
LED1_TOGGLE;
}
if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) == KEY_ON )
{
/*LED2反转*/
LED2_TOGGLE;
}
}
}
5.GPIO位带操作
5.1 位带简介
位操作就是可以单独的对一个比特位读和写,这个在 51 单片机中非常常见。51 单片机中通过关键字 sbit 来实现位定义,STM32 没有这样的关键字,而是通过访问位带别名区来实现。
在 STM32 中,有两个地方实现了位带,一个是 SRAM 区的最低 1MB 空间,另一个是外设区最低 1MB 空间。这两个 1MB 的空间除了可以像正常的 RAM 一样操作外,他们还有自己的位带别名区,位带别名区把这 1MB 的空间的每一个位膨胀成一个 32 位的字,当访问位带别名区的这些字时,就可以达到访问位带区某个比特位的目的。

5.1 外设位带区
外 设 外 带 区 的 地 址 为:0X40000000~0X40100000,大小为1MB,这1MB 的 大 小 在103 系 列 大/中/小 容 量 型号的单片机中包 含 了 片 上外设 的全部寄存器,这些寄存器的地址为:0X40000000~0X40029FFF。
外设位带区经过膨胀后的位带别名区地址为:0X42000000~0X43FFFFFF,这个地址仍然在 CM3 片上外设的地址空间中。在 103 系列大/中小容量型号的单片机里面,0X40030000~0X4FFFFFFF 属于保留地址,膨胀后的32MB 位带别名区刚好就落到这个地址范围内,不会跟片上外设的其他寄存器地址重合。
STM32 的全部寄存器都可以通过访问位带别名区的方式来达到访问原始寄存器比特位的效果,这比 51 单片机强大很多。因为 51 单片机里面并不是所有的寄存器都是可以比特位操作,有些寄存器还是得字节操作,比如SBUF。虽然说全部寄存器都可以实现比特操作,但我们在实际项目中并不会这么做,甚至不会这么做。有时候为了特定的项目需要,比如需要频繁的操作很多 IO 口,这个时候我们可以考虑把 IO 相关的寄存器实现比特操作。
对于片上外设位带区的某个比特,记它所在字节的地址为 A, 位序号为 n(0<=n<=31)(n 的范围根据具体寄存器能控制的位决定),则该比特在别名区的地址为:
AliasAddr= =0x42000000+ (A-0x40000000)*8*4 +n*4
0X42000000 是外设位带别名区的起始地址,0x40000000 是外设位带区的起始地址, (A-0x40000000)表示该比特前面有多少个字节,一个字节有 8 位,所以 *8,一个位膨胀后是 4 个字节,所以 *4,n 表示该比特在 A 地址的序号,因为一个位经过膨胀后是四个字节,所以也 *4。
5.2 SRAM 位带区
SRAM 的位带区的地址为:0X2000 0000~X2010 0000,大小为 1MB,经过膨胀后的位带别名区地址为:0X2200 0000~0X23FF FFFF,大小为32MB。操作 SRAM 的比特位这个用得很少。
对于 SRAM 位带区的某个比特,记它所在字节的地址为 A, 位序号为n(0<=n<=31)(n 的范围根据具体寄存器能控制的位决定),则该比特在别名区的地址为:
AliasAddr= =0x22000000+ (A-0x20000000)*8*4 +n*4
统一公式
为了方便操作,我们可以把这两个公式合并成一个公式,把“位带地址 + 位序号”转换成别名区地址统一成一个宏。
// 把“位带地址 + 位序号”转换成别名地址的宏
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x00FFFFFF)<<5)+(bitnum<<2))
addr & 0xF0000000 是为了区别 SRAM 还是外设,实际效果就是取出 4 或者 2,如果是外设,则取出的是 4,+0X02000000 之后就等于0X42000000,0X42000000 是外设别名区的起始地址。如果是 SRAM,则取出的是 2,+0X02000000 之后就等于 0X22000000,0X22000000 是 SRAM 别名区的起始地址。
addr & 0x00FFFFFF 屏蔽了高三位,相当于减去 0X20000000 或者 0X40000000,但是为什么是屏蔽高三位?因为外设的最高地址是:0X20100000,跟起始地址 0X20000000 相减的时候,总是低5 位才有效,所以干脆就把高三位屏蔽掉来达到减去起始地址的效果,具体屏蔽掉多少位跟最高地址有关。SRAM 同理分析即可。«5 相当于 84,«2 相当于 *4。
5.3 代码实现
#include "stm32f10x.h"
// 这里只定义了 GPIO ODR和IDR这两个寄存器的位带别名区地址,其他寄存器的没有定义
//SRAM 位带区: 0X2000 0000~0X2010 0000
//SRAM 位带别名区:0X2200 0000~0X23FF FFFF
//外设 位带区: 0X4000 0000~0X4010 0000
//外设 位带别名区:0X4200 0000~0X43FF FFFF
// 把“位带地址+位序号”转换成别名地址的宏
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x00FFFFFF)<<5)+(bitnum<<2))
/*
*addr & 0xF0000000,取地址的高4位,看看是2还是4,用于区分SRAM和外设地址,
*如果是2,+0x02000000则=0X2200 0000,即是SRAM,如果是4,+0x02000000则=0X4200 0000,即是外设
*
*addr & 0x000FFFFFF,屏蔽掉高两位,相当于-0X2000 0000或者-0X4000 0000,结果表示偏移位带区多少个字节
*<<5 等于*8*4,因为位带区一个地址表示一个字节,一个字节有8个bit,一个bit可以膨胀成一个字,即4个字节
*<<2 等于*4,因为一个位可以膨胀成一个字,即4个字节
*
*分解成两条公式应该就是这样:
*SRAM位带别名地址
*AliasAddr= 0x22000000+((A-0x20000000)*8+n)*4 =0x22000000+ (A-0x20000000)*8*4 +n*4
*外设位带别名地址
*AliasAddr= 0x22000000+((A-0x20000000)*8+n)*4 =0x22000000+ (A-0x20000000)*8*4 +n*4
*/
// 把一个地址转换成一个指针
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
// 把位带别名区地址转换成指针
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
// GPIO ODR 和 IDR 寄存器地址映射
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C
#define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C
#define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C
#define GPIOD_ODR_Addr (GPIOD_BASE+12) //0x4001140C
#define GPIOE_ODR_Addr (GPIOE_BASE+12) //0x4001180C
#define GPIOF_ODR_Addr (GPIOF_BASE+12) //0x40011A0C
#define GPIOG_ODR_Addr (GPIOG_BASE+12) //0x40011E0C
#define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808
#define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08
#define GPIOC_IDR_Addr (GPIOC_BASE+8) //0x40011008
#define GPIOD_IDR_Addr (GPIOD_BASE+8) //0x40011408
#define GPIOE_IDR_Addr (GPIOE_BASE+8) //0x40011808
#define GPIOF_IDR_Addr (GPIOF_BASE+8) //0x40011A08
#define GPIOG_IDR_Addr (GPIOG_BASE+8) //0x40011E08
// 单独操作 GPIO的某一个IO口,n(0,1,2...15),n表示具体是哪一个IO口
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入
#define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出
#define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //输入
#define PDout(n) BIT_ADDR(GPIOD_ODR_Addr,n) //输出
#define PDin(n) BIT_ADDR(GPIOD_IDR_Addr,n) //输入
#define PEout(n) BIT_ADDR(GPIOE_ODR_Addr,n) //输出
#define PEin(n) BIT_ADDR(GPIOE_IDR_Addr,n) //输入
#define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //输出
#define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入
#define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //输出
#define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //输入
void SOFT_Delay(__IO uint32_t nCount);
void LED_GPIO_Config(void);
int main(void)
{
// 程序来到main函数之前,启动文件:statup_stm32f10x_hd.s已经调用
// SystemInit()函数把系统时钟初始化成72MHZ
// SystemInit()在system_stm32f10x.c中定义
// 如果用户想修改系统时钟,可自行编写程序修改
LED_GPIO_Config();
while( 1 )
{
// PB0 = 0,点亮LED
PBout(0)= 0;
SOFT_Delay(0x0FFFFF);
// PB0 = 1,熄灭LED
PBout(0)= 1;
SOFT_Delay(0x0FFFFF);
}
}
void LED_GPIO_Config(void)
{
// 定义一个GPIO_InitTypeDef类型的结构体
GPIO_InitTypeDef GPIO_InitStructure;
// 开启GPIOB的时钟
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);
// 选择要控制的IO口
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
// 设置引脚为推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
// 设置引脚速率为50MHz
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
/*调用库函数,初始化GPIOB0*/
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 关闭LED
GPIO_SetBits(GPIOB, GPIO_Pin_0);
}
// 简陋的软件延时函数
void SOFT_Delay(__IO uint32_t nCount)
{
for(; nCount != 0; nCount--);
}
更多推荐
所有评论(0)