目录
这里直接解析代码
点亮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引脚配置进行说明。