STM32-标准库

本文详细介绍了STM32 GPIO端口的配置流程,包括定义初始化结构体、使能GPIO时钟、设置GPIO模式、初始化GPIO及输出电平。通过实例代码解析了GPIO_Init函数,讲解了GPIO工作模式、速度和模式设置。同时,展示了如何通过GPIO控制LED灯的亮灭。
摘要由CSDN通过智能技术生成

目录

点亮LED灯

GPIO配置流程

1.定义GPIO的初始化类型结构体:

2.使能GPIO时钟

3.设置GPIO_InitTypeDef结构体三个成员的值

4.初始化GPIO

5.GPIO电平输出

深度解析

GPIO_Init函数解析

参数GPIO_InitStruct


这里直接解析代码

点亮LED灯

STM32引脚GPI0分为八种模式(输入4种+输出4种)。八种模式分别为:

输入浮空 GPIO_Mode_IN_FLOATING

输入上拉 GPIO_Mode_IPU

输入下拉 GPIO_Mode_IPD

模拟输入 GPIO_Mode_AIN

具有上拉或下拉功能的开漏输出 GPIO_Mode_Out_OD

具有上拉或下拉功能的推挽输出 GPIO_Mode_Out_PP

具有上拉或下拉功能的复用功能推挽 GPIO_Mode_AF_PP

具有上拉或下拉功能的复用功能开漏 GPIO_Mode_AF_OD
我的STM32开发板板载两个LED小灯,电路图如下:

这里直接贴完整的代码

#include "stm32f10x.h"

void LED_Init(void);

#define LED2_OFF GPIO_SetBits(GPIOE,GPIO_Pin_5)
#define LED2_ON GPIO_ResetBits(GPIOE,GPIO_Pin_5)
#define LED2_REV GPIO_WriteBit(GPIOE, GPIO_Pin_5,(BitAction)(1-(GPIO_ReadOutputDataBit(GPIOE, GPIO_Pin_5))))

#define LED3_OFF GPIO_SetBits(GPIOB,GPIO_Pin_5)
#define LED3_ON GPIO_ResetBits(GPIOB,GPIO_Pin_5)
#define LED3_REV GPIO_WriteBit(GPIOB, GPIO_Pin_5,(BitAction)(1-(GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_5))))

void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;                     // 定义结构体变量
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);    //打开PB口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);    //打开PE口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;                //PB5,PE5引脚设置
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;        //设置输出速率50MHz
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;         //推挽输出模式
GPIO_Init(GPIOB, &GPIO_InitStructure);                   //初始化外设GPIOx寄存器
GPIO_Init(GPIOE, &GPIO_InitStructure);

}

int main()
{
    uint32_t i; 
    LED_Init(); //初始化LED

    LED2_ON;
    LED3_OFF;
    for(i=0; i<0xffffff; i++); //for循环不精确延时
    while(1)
    {
        for(i=0; i<0xfffff; i++); //for循环不精确延时
        LED2_REV;//LED2取反
        LED3_REV;//LED3取反
    }
}


看完是不是一脸懵。。。。。。。别急下面将为你一一讲解。

GPIO配置流程

由于STM32的GPIO工作模式有8种,所以在GPIO输出之前要先对要操作的GPIO进行配置:

定义GPIO的初始化结构体类型

使能GPIO的时钟

配置GPIO的引脚

配置GPIO口的输出类型为推挽

配置GPIO口的输出速度

初始化GPIO(初始化相应的寄存器)(代码如下)

GPIO_InitTypeDef GPIO_InitStructure;                     // 定义结构体变量
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);    //打开PB口时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);    //打开PE口时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;                //PB5,PE5引脚设置
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;        //设置输出速率50MHz
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;         //推挽输出模式
GPIO_Init(GPIOB, &GPIO_InitStructure);                   //初始化外设GPIOx寄存器
GPIO_Init(GPIOE, &GPIO_InitStructure);

1.定义GPIO的初始化类型结构体:

GPIO_InitTypeDef GPIO_InitStructure;

此结构体的定义是在stm32f10x_gpio.h文件中,其中包括3个成员。

typedef struct
{
  uint16_t GPIO_Pin;       
  GPIOSpeed_TypeDef GPIO_Speed;  
  GPIOMode_TypeDef GPIO_Mode;   
}GPIO_InitTypeDef;

2.使能GPIO时钟

ARM与C51单片机不同的是,不用外设的时候,如IO口、ADC、定时器等等,都是禁止时钟的,以达到节能的目的,只有要用到的外设,才开启它的时钟。

void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);

此函数是在stm32f10x_rcc.c文件中定义的。其中第一个参数指要打开哪一组GPIO的时钟,取值参见stm32f10x_rcc.h文件中的宏定义,第二个参数为打开或关闭使能,取值参见stm32f10x.h文件中的定义,其中ENABLE代表开启使能,DISABLE代表关闭使能。

3.设置GPIO_InitTypeDef结构体三个成员的值

这里包括引脚、速度和工作模式,取值可参考第一部分。

4.初始化GPIO

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef*  GPIO_InitStruct);

函数配置GPIO,此函数是在stm32f10x_gpio.c文件中定义的,其中第一个参数代表要配置哪组GPIO,取值参见stm32f10x.h文件中的定义,第二个参数是第1步定义的GPIO的初始化类型结构体。

5.GPIO电平输出

官方让GPIO输出高低电平的函数:

GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

函数就是置位GPIO,即让相应的GPIO输出高电平;

void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

函数是让GPIO复位的,即让相应的GPIO输出低电平。
你以为到这里就完了吗?骚年接着往下看。。。。。。。。。。

深度解析

GPIO_Init函数解析

首先来看一下GPIO_Init函数的原型

void GPIO_Init (GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)

这个函数的实现是在Stm32f10x_gpio.c文件中,若要使用该函数在相应的应用程序的前面包含Stm32f10x_gpio.h头文件。

参数GPIO_TypeDef

该函数的第一个参数为GPIO_TypeDef,它是一个结构体类型,该类型在Stm32f10x.h中被定义。定义的原型为:

typedef struct

{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;

在这个结构体类型当中有7个32(8字节)位的变量,这些变量在存储空间的地址是相邻的。打开STM32数据手册不难看出,每个端口对应有16的引脚,由7个寄存器控制GPIO行为,并且这7个寄存器的顺序也是连续的。各个端口都有相同的结构。STM32的固件库就将这种结构抽象出一个类型GPIO_TypeDef。在操作寄存器之前你一定要有一个寄存器映射的操作,否则无法访问指定的寄存器,在这里我们只需要映射一次而不需要映射7此。这样做是不是很方便,也提高了代码的可读性,使代码规范化。

既然GPIO_Init的第一个参数GPIO_TypeDef的指针变量,这个指针变量存放的就是某一个端口的首地址。某一个程序的调用语句是这样的

GPIO_Init(GPIOD,&GPIO_InitStructure); //初始化GPIOD

这个_IO 是指静态 volatile (直接go to definition可以看到 “#define   __IO    volatile ” 这行宏定义 ) uint32_t 是指32位的无符号整形变量uint32_t 是指32位的无符号整形变量;

volatile

volatile 类型是这样的,其数据确实可能在未知的情况下发生变化。比如,硬件设备的终端更改了它,现在硬件设备往往也有自己的私有内存地址,比如显存,他们一般是通过映象的方式,反映到一段特定的内存地址当中,这样,在某些条件下,程序就可以直接访问这些私有内存了。另外,比如共享的内存地址,多个程序都对它操作的时候。你的程序并不知道,这个内存何时被改变了。如果不加这个voliatile修饰,程序是利用catch当中的数据,那个可能是过时的了,加了 voliatile,就在需要用的时候,程序重新去那个地址去提取,保证是最新的。归纳起来如下:


1. volatile变量可变允许除了程序之外的比如硬件来修改他的内容 
2. 访问该数据任何时候都会直接访问该地址处内容,即通过cache提高访问速度的优化被取消 

对于((volatile unsigned long *) 0xE0028000)为随硬件需要定义的一种地址,前面加上“*”指针,为直接指向该地址,整个定义约定符号IOPIN代替,调用的时候直接对指向的地址寄存器写内容既可。这实际上就是内存映射机制的方便性了。其中volatile关键字是嵌入式系统开发的一个重要特点。上述表达式拆开来分析,首先(volatile unsigned long *) 0xE0028000的意思是把0xE0028000强制转换成volatile unsigned long类型的指针,暂记为p,那么就是#define A *p,即A为P指针指向位置的内容了。这里就是通过内存寻址访问到寄存器A,可以读/写操作。

对于(volatile  unsigned  char  *)0x20我们再分析一下,它是由两部分组成:
1)(unsigned  char  *)0x20,0x20只是个值,前面加(unsigned  char  *)表示0x20是个地址,而且这个地址类型是unsigned  char  ,意思是说读写这个地址时,要写进unsigned  char  的值,读出也是unsigned  char  。
2)volatile,关键字volatile  确保本条指令不会因C  编译器的优化而被省略,且要求每次直接读值。例如用while((unsigned  char  *)0x20)时,有时系统可能不真正去读0x20的值,而是用第一次读出的值,如果这样,那这个循环可能是个死循环。用了volatile  则要求每次都去读0x20的实际值。

那么(volatile  unsigned  char  *)0x20是一个固定的指针,是不可变的,不是变量。而char  *u则是个指针变量。
再在前面加"*":*(volatile  unsigned  char  *)0x20则变成了变量(普通的unsigned  char变量,不是指针变量),如果#define  i  (*(volatile  unsigned  char  *)0x20),那么与unsigned  char  i是一样了,只不过前面的i的地址是固定的。 

 回到 GPIO_TypeDef 这段代码,这个代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内又定义了 7 个 __IO uint32_t 类型的变量.这些变量每个都为 32 位,也就是每个变量占内存空间 4 个字节.在 c 语言中,结构体内变量的存储空间是连续的,也就是说假如我们定义了一个 GPIO_TypeDef ,这个结构体的首地址(变量 CRL 的地址)若为 0x4001 1000,那么结构体中第二个变量(CRH)的地址即为 0x4001 1000 +0x04 ,加上的这个 0x04 ,正是代表 4 个字节地址的偏移量.
“加上的这个 0x04 ,正是代表 4 个字节地址的偏移量”

GPID是固件库中定义的一个宏,在编译的时候会宏展开,先列出与GPIOD端口地址映射有关的宏定义如下:

#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)

#define GPIOD_BASE            (APB2PERIPH_BASE + 0x1400)

#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)

#define PERIPH_BASE           ((uint32_t)0x40000000)

看到了0x4000 0000这个数字是不是非常熟悉,它是外设的首地址。在STM32芯片的内部STM32有两个,一个叫APB1,一个叫APB2。每一个APB桥都会管理很多外设。STM32F10x把这两个APB的外设寄存器访问地址放在了不同的存储空间。0x10000就是APB2外设的存储空间首地址相对于整个外设的偏移。而0x1400是GPIOD端口外设首地址相对于APB2外设的存储空间首地址的偏移。这样就找到了GPIOD外设的基地址了!而((GPIO_TypeDef *) GPIOD_BASE)可以同时实现所有控制GPIOD端口的7个寄存器的映射。若访问某一个寄存器只需要通过指向GPIO_TypeDef 变量的指针。

((GPIO_TypeDef *) GPIOD_BASE)很多同学都在问是什么意思

可以直接理解为把一个整形的数据强转为一个结构体的指针。()是指把括号后的东西强转为括号里的类型。(GPIO_TypeDef *)就是个结构体指针类型。

只表示对该结构体申明,而((GPIO_TypeDef *) GPIOD_BASE)表示将其强制转换为指针类型,
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)的意思是用GPIOC来替换((GPIO_TypeDef *) GPIOD_BASE),那么这个时候GPIOC就是指针了.所以程序里面你才敢用GPIOD->CRL.不要过多纠结 这是stm8、32里面的

参数GPIO_InitStruct

第二个参数的为GPIO_InitTypeDef* GPIO_InitStruct。就是一个指向GPIO _InitTypeDef的地址。第一个参数只找到配置的目标寄存器,第二个参数就是对相应端口如何配置的数据参数。这些参数存储在指向GPIO_InitTypeDef变量的首地址处。先列处该参数由来的一断代码

GPIO_InitTypeDef  GPIO_InitStructure;

GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;

GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;

GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;

GPIO_InitTypeDef 是一个结构体的变量,该变量在Stm32f10x_gpio.h头文件中被定义,定义的原型如下:

typedef struct

{
uint16_t GPIO_Pin;

GPIOSpeed_TypeDef  GPIO_Speed;

GPIOMode_TypeDef  GPIO_Mode;

}GPIO_InitTypeDef;

GPIO_InitTypeDef的第一个变量为GPIO_Pin是一个16为的无符号数,该数只有16位,每一位代表一个引脚,若要配置某一个端口的某一个引脚只需要把相应的位设置为1就可以了。在STM32的固件库中有如下引脚号定义:

#define GPIO_Pin_0                 ((uint16_t)0x0001)  /*!< Pin 0 selected */

#define GPIO_Pin_1                 ((uint16_t)0x0002)  /*!< Pin 1 selected */

#define GPIO_Pin_2                 ((uint16_t)0x0004)  /*!< Pin 2 selected */

#define GPIO_Pin_3                 ((uint16_t)0x0008)  /*!< Pin 3 selected */

#define GPIO_Pin_4                 ((uint16_t)0x0010)  /*!< Pin 4 selected */

#define GPIO_Pin_5                 ((uint16_t)0x0020)  /*!< Pin 5 selected */

#define GPIO_Pin_6                 ((uint16_t)0x0040)  /*!< Pin 6 selected */

#define GPIO_Pin_7                 ((uint16_t)0x0080)  /*!< Pin 7 selected */

#define GPIO_Pin_8                 ((uint16_t)0x0100)  /*!< Pin 8 selected */

#define GPIO_Pin_9                 ((uint16_t)0x0200)  /*!< Pin 9 selected */

#define GPIO_Pin_10                ((uint16_t)0x0400)  /*!< Pin 10 selected */

#define GPIO_Pin_11                ((uint16_t)0x0800)  /*!< Pin 11 selected */

#define GPIO_Pin_12                ((uint16_t)0x1000)  /*!< Pin 12 selected */

#define GPIO_Pin_13                ((uint16_t)0x2000)  /*!< Pin 13 selected */

#define GPIO_Pin_14                ((uint16_t)0x4000)  /*!< Pin 14 selected */

#define GPIO_Pin_15                ((uint16_t)0x8000)  /*!< Pin 15 selected */

#define GPIO_Pin_All               ((uint16_t)0xFFFF)  /*!< All pins selected */

使用这些定义好的宏就方便多了,要配置某几个引脚只需要把相应的引脚相或就可以了。若你要多某一个端口的所有为进行配置,那么只需要使用一个宏GPIO_Pin_All 。简单吧!

GPIOSpeed_TypeDef是一个枚举变量,它用于存储GPIO速度的参数,它的定义如下:

typedef enum

{

GPIO_Speed_10MHz = 1,

GPIO_Speed_2MHz,

GPIO_Speed_50MHz

}GPIOSpeed_TypeDef;

通过定义可以知道,GPIOSpeed_TypeDef的变量有三种取值,那么GPIO的速度有三种,

枚举变量的值

对应的速度

1

10MHZ

2

2MHZ

3

50MHZ

GPIOMode_TypeDef也是一个枚举变量,它用于存储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;

设计这个枚举变量的可取值有一定的意义。在第四位当中只用到了其中的高两位,这两位数据用来存储到某一个引脚的模式控制位MODEx[1:0] ,而高四位用来标志某一些标志。

高四位的取值

意义

0

输入模式

1

输出模式

2

下拉输入

4

上拉输入

3、函数代码详解

上面是GPIO_Init函数参数的解释。我在我们就可以进入GPIO_Init函数的内部看看了。

先把函数的代码列出,对代码的解释都放在了注释当中 ,如下:

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)

{

uint32_t currentmode = 0x00;
uint32_t currentpin = 0x00,;
uint32_t pinpos = 0x00;
uint32_t pos = 0x00;

uint32_t tmpreg = 0x00, pinmask = 0x00;

/* Check the parameters 检查参数*/

assert_param(IS_GPIO_ALL_PERIPH(GPIOx));

assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));

assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));

/*---------------------------- GPIO Mode Configuration -----------------------*/

currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);

if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00)
//若为输出上拉就会配置GPIO的速度

{

/* Check the parameters */

assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));

/* Output mode */

currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;

}

/*---------------------------- GPIO CRL Configuration ------------------------*/

/* Configure the eight low port pins */

if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)//若对第八个引脚进行配置,GPIO_Pin的值某一位为1就会对该引脚配置

{

tmpreg = GPIOx->CRL;//暂存GPIO控制寄存器原来的值

for (pinpos = 0x00; pinpos < 0x08; pinpos++)//扫描8次决定,查看哪一引脚需要配置,若 //需要配置则进行配置

{

pos = ((uint32_t)0x01) << pinpos;//获得要查看的某一个引脚所对应的位为1的值

/* Get the port pins position */

currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;//currentpin 的值为0或者为pos

if (currentpin == pos)//若为pos说明该位需要配置

{

pos = pinpos << 2;//pinpos 的值乘以4得到某一引脚配置位的最低位号:0,4,8......28

/* Clear the corresponding low control register bits *///用于屏蔽某一个引脚的配置位, 使这4位为0

pinmask = ((uint32_t)0x0F) << pos;

tmpreg &= ~pinmask;

/* Write the mode configuration in the corresponding bits */

tmpreg |= (currentmode << pos);//因为模式所对应的数都存放在第四位,
所以需要向左移位到某一个引脚对应的配置位的最低位出,然后对存储到tmpreg 中

/* Reset the corresponding ODR bit */

if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)//若为输入下拉,需要打开相 应的开关

{

GPIOx->BRR = (((uint32_t)0x01) << pinpos);

}

else

{

/* Set the corresponding ODR bit */

if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)//若为输入下拉,需要打开 相应的开关

{

GPIOx->BSRR = (((uint32_t)0x01) << pinpos);

}

}

}

}

GPIOx->CRL = tmpreg;//对低8个引脚配置寄存器赋值

}

/*---------------------------- GPIO CRH Configuration ------------------------*/

/* Configure the eight high port pins */

if (GPIO_InitStruct->GPIO_Pin > 0x00FF)

{

tmpreg = GPIOx->CRH;

for (pinpos = 0x00; pinpos < 0x08; pinpos++)

{

pos = (((uint32_t)0x01) << (pinpos + 0x08));

/* Get the port pins position */

currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);

if (currentpin == pos)

{

pos = pinpos << 2;

/* Clear the corresponding high control register bits */

pinmask = ((uint32_t)0x0F) << pos;

tmpreg &= ~pinmask;

/* Write the mode configuration in the corresponding bits */

tmpreg |= (currentmode << pos);

/* Reset the corresponding ODR bit */

if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)

{

GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));

}

/* Set the corresponding ODR bit */

if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)

{

GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));

}

}

}

GPIOx->CRH = tmpreg;

}

}

4、备注

assert_param函数是对参数的检测。参数要么是逻辑0或者1。IS_GPIO_ALL_PERIPH也是一个宏,宏定义为:

#define IS_GPIO_ALL_PERIPH(PERIPH) (((PERIPH) == GPIOA) || \

((PERIPH) == GPIOB) || \

((PERIPH) == GPIOC) || \

((PERIPH) == GPIOD) || \

((PERIPH) == GPIOE) || \

((PERIPH) == GPIOF) || \

((PERIPH) == GPIOG))

其他的参数检测函数当中使用的宏都是相似的,具体可以查看相应的宏定义,在此不一一列出。

对低8位的配置和对高8位的配置原理是一样的。所以在此只对低8引脚配置进行说明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值