stm32—GPIO

0. 引入

单片机的最小系统:

        芯片(CPU + 总线 + 外设控制器) + 晶振电路 + 复位电路 + 供电电路
一个完整的系统:

        最小系统 + 其它的外设

芯片:

        整个系统的核心,相当于人类的大脑,会提供引脚与外部电路相连 

        芯片四周那些银白色的引脚是从芯片内部引申出来的。它负责芯片内部的控制单元与外部硬件的连接

        那么一个引脚其实本质上就是一根"电线"

引脚拥有输入功能 / 输出功能,比如:

        1. 引脚可以输出 / 输入一个电平信号(1/0)

                那么电平信号是针对于CPU来说的,因为CPU只能识别二进制0/1(数字信号)

        2. 引脚可以输入/输出一个高低电压

                因为对于外部电路来说,外部电路的工作是采用模拟电压信号(即多少V电压)

为什么一个引脚对CPU和外部的电路的输入/输出是不一样的信号?它是怎么做到的?

        这是因为引脚在芯片内部还需要通过"控制单元"才能够进入CPU,CPU只能识别二进制0/1(数字信号)


控制单元:
        不同硬件对应不同的控制单元:
                GPIO --->GPIO控制器 
                USART --->USART控制器 
                ......

在单片机产品中,我们常常可以见到三种模块:LED灯、KEY按键、BEEP蜂鸣器


LED灯:

        一个比较常见的LED电路

LED1  ------ 通过控制 LED0 引脚(电线)

        给它一个低电平(低电压),LCD1 灯就会亮

        给它应该高电平(高电压),LED1 灯就会灭

        1 ——> 高电平

        0 ——> 低电平

        电流:从电势高的地方流向电势低的地方


        CPU ===> 往 LED0 引脚去 写1,写0

                "output" 输出功能

KEY按键:

        一个比较常见的KEY电路


KEY0  ------  通过读取 KEY0 引脚的电平状态来知晓用户是否按下按键

        高电平(1) ---> 弹起

        低电平(0) ---> 按下

        CPU  ===> 读取KEY0引脚的电平状态

                "input" 输入功能

BEEP蜂鸣器:

        一个比较常见的BEEP电路

BEEP ----------- 通过控制BEEP引脚(电线)

        给它一个高电平(高电压),BEEP就会响

        给它一个低电平(低电压),BEEP就不响

        

        CPU  ===> 往BEEP引脚去 写1,写0

                "output" 输出功能

        

        两种三极管:PNP   and   NPN   ===> P指向N

这些引脚最终是接入到MCU的某个引脚(GPIO)上去的

控制LED灯、KEY按键、BEEP蜂鸣器等,可以在MCU上面写程序去控制这些引脚

1. GPIO到底是什么?

GPIO:General Purpose Input Output  通用功能的输入输出 线
 

GPIO就是从芯片内部引出一根功能复用的口线("电线"),可以由CPU配置成不同的功能

       如:输入功能,输出功能,复用功能,模拟模式

根据数据手册中列出的每个 I/O 端口的特性,可通过软件将通用 I/O(GPIO)端口的各个端口位分别配置为多种模式

        如:输入浮空、输入上拉、输入下拉、模拟功能、具有上拉或下拉功能的开漏输出、具有上拉或下拉功能的推挽输出、具有上拉或下拉功能的复用功能推挽、具有上拉或下拉功能的复用功能开漏


芯片或CPU控制整个世界就是通过这样的引脚(口线,GPIO)

STM32F4xx共有144个GPIO口线(引脚,pin),分为9组,记为GPIOA,GPIOB,GPIOC,GPIOD,GPIOE,GPIOF,GPIOG,GPIOH,GPIOI. 每组管理16个GPIO引脚,编号从0~15

        如:GPIOA这一组有16个引脚,分别记为GPIOA0,GPIOA1,GPIOA2,... GPIOA15
                其他组类似

                GPIOA0  -----> PA0

                GPIOB3  ------> PB3

                ......

这些GPIO引脚都是功能复用的,并且由GPIO控制器来控制它们的

        如果我们要使用这个GPIO,那么就必须先去配置它的寄存器组

        所有的外设都是由"外设控制器"来控制,外设是相当于CPU而言的(不是芯片)

GPIO控制器由不同的寄存器来配置或控制它们(GPIOx) 

可以通过<STM32F4xx中文参考手册>第2章可以查看

         边界地址              外设        总线
0x4002 2000 - 0x4002 23FF     GPIOI	
0x4002 1C00 - 0x4002 1FFF     GPIOH
0x4002 1800 - 0x4002 1BFF     GPIOG
0x4002 1400 - 0x4002 17FF     GPIOF
0x4002 1000 - 0x4002 13FF     GPIOE        AHB1
0x4002 0C00 - 0x4002 0FFF     GPIOD
0x4002 0800 - 0x4002 0BFF     GPIOC
0x4002 0400 - 0x4002 07FF     GPIOB
0x4002 0000 - 0x4002 03FF     GPIOA


上述的表有几个名字需要解释: 
	边界地址:指寄存器组的起始地址(基址)和结束地址
	外设:该寄存器组对应的硬件控制器
	总线:该硬件控制器挂载的时钟线 

2. STM32F4xx GPIO内部结构原理

每个GPIO内部都可以配置成:

        1. 输入功能:input mode

                CPU可以获取该GPIO口的外部输入的一个电平状态

                输入功能有四种模式:

                        (1) 输入悬空(input floating):不接上拉和下拉电阻

                                输入引脚处于浮空状态,‌即没有特定电压状态,‌引脚悬浮在空中

                                IO引脚的电平状态完全是外部输入所决定的,这时CPU能够通过读取数据的操作知道状态

                        (2) 带上拉输入(input pull-up):内部接上拉电阻

                                该引脚被设置为上拉输入时,引脚悬空的状态下,CPU读取到的电平状态为高电平,因为内部有一个上拉电阻;唯有当被外部输入信号下拉时,CPU读取到的电平才为低电平

                        (3) 带下拉输入(input pull-down): 内部接下拉电阻

                               该引脚被设置为下拉输入时,引脚悬空的状态下,CPU读取到的电平状态为低电平。唯有当被外部输入信号上拉时,CPU读取到的电平状态才为高电平

                        (4) 模拟输入(Input Analog)

                                该引脚被设置为模拟输入时,能够获取外部的模拟信号,通过芯片内的ADC转换为数字量,如变化的电压值


        2. 输出功能:output mode

                CPU可以往该GPIO口输出一个电平状态

                输入功能有两种模式:

                        (1) 输出推挽(Push-Pull):可以输出高、低电平

                                可以往外部引脚输出一个高电平(1)或低电平(0)

                                1:MOS管上方导通,下方不导通,在此处数字量变成模拟量,输出高电平

                                0:MOS管下方导通,上方不导通,在此处数字量变成模拟量,输出低电平

                        (2) 输出开漏(Open-Drain):不输出电压

                                低电平接地,高电平不接地(悬空状态)

                                如果外部电路接上拉电阻,则在CPU输出1时会接到外部上拉电阻的电源电压上

                                0:ping  接地

                                1:ping  悬空    此时需要外部电路中设计上拉电阻

        3. 复用功能:Alternate Function 

                复用功能是指GPIO口用作其它的功能口线,如: I2C,UART,SPI 等

                每个GPIO口都可以配置成多达16种复用功能,记为: AF0,AF1,AF2 ...  AF15,具体哪个GPIO口可以配置成哪种复用功能,需要看原理图

                其实在芯片设计的,每个GPIO口能够复用的功能,就已经定了

                UART0 ---> TX0 是不是任意一个GPIO口都可以复用这个Tx0这个引脚呢?

                        肯定不是啦,这个在芯片设计的时候,就已经定了
                

STM32F4xx每个GPIO口内部都有一个上拉/下拉电阻,你可以enable/disable它,根据具体应用场景需要

每个GPIO口的操作都是通过GPIO的寄存器来控制的

3. STM32F4xx GPIO寄存器说明

CPU是通过地址总线根据相应地址访问设备的,我们将 GPIO 看做设备,GPIO 内部寄存器看做设备中的空间,也就是说,如果我们需要操作相应寄存器相当于对 GPIO 设备内部的空间进行操作


关于设备地址,参考 <STM32F4xx中文参考手册.pdf> 第二章第三节 存储器映像

每个通用 I/O 端口包括:

        4 个 32 位配置寄存器

            (GPIOx_MODER、GPIOx_OTYPER、GPIOx_OSPEEDR 和 GPIOx_PUPDR)
         2 个 32 位数据寄存器(GPIOx_IDR 和 GPIOx_ODR)

        1 个 32 位置位/复位寄存器(GPIOx_BSRR)
        1 个 32 位锁定寄存器(GPIOx_LCKR)
        2 个 32 位复用功能选择寄存器(GPIOx_AFRL 和 GPIOx_AFRH)

可通过
字节(8位)半字(16位) 字(32位) 对 GPIO 寄存器进行访问

(1) GPIOx_MODER
        模式选择寄存器,该寄存器的地址偏移为 0x00
                地址偏移:每一组GPIO寄存器都有一个地址范围

                        如:GPIOA :  0x4002 0000 ~ 0x4002 03FF

                        GPIOA这一组的寄存器的基地址是0x4002 0000

                因此, GPIOA_MODER 的地址就是:0x4002 0000 + 0x00

        模式寄存器用来配置GPIO的功能的(input / output / analog(模拟) / AF)

        该寄存器用来控制x(x=A,B,C...,I)组的16个GPIO引脚的模式,每个GPIO口占2bits(总共有四种模式,四种功能)

        编号为y(y=0,1,2...,15)的GPIO引脚在该寄存器的bit位为GPIOx_MODER[2y+1:2y]

        具体配置如下:

                GPIOx_MODER[2y+1:2y]                     模式

                                00                                         输入模式

                                01                                        通用输出模式

                                10                                    复用功能,Alternate Function

                                11                                          模拟输入

例子:用C代码把PF9配置为输出模式

分析:
    PF组的基址:0x4002 1400
    模式寄存器的偏移地址:0x00
        所以GPIOF_MODER地址:0x4002 1400 + 0x00

如果要把PF9配置为输出模式,就需要将GPIOF_MODER[19:18] --> 01 
	把地址为0x40021400的寄存器中bit19清0,bit18置1

在STM32中表示地址用 unsigned long 来表示地址(32位:long(4字节),64位:long(8字节))
    unsigned long *p = (unsigned long *)0x40021400;
但是一般情况下我们会在地址前加一个volatile,如下:
    volatile unsigned long *p = (volatile unsigned long *)0x40021400;

    volatile的作用是作为指令关键字,禁止编译器优化,访问的就是实际地址,不需要优化

    bit19清0,bit18置1:
	    *p = *p & ~(1<<19);
		*p = *p | (1<<18);

但是如上的操作实际上对寄存器进行了两次操作,效率比较低,能不能对寄存器一次性修改到位?
    可以

比如: 
	unsigned long r = *p;
	r &= (~(1 << 19));
	r |= (1 << 18);
	*p = r; // 通过中间变量,一步到位

(2) GPIOx_OTYPER

        输出类型寄存器,该寄存器的地址偏移为 0x04
        该寄存器用来控制x(x=A,B,C,...,I)分组的16个GPIO的输出类型,每个GPIO占1bit

        编号为y(y=0,1,2,...,15)的GPIO在该寄存器的bit位置为GPIOx_OTYPER[y]
        具体输出类型如下:

                GPIOx_OTYPER[y]                     输出类型

                              0                                输出推挽(Push-Pull)

                              1                                输出开漏(Open-Drain)

        注:

                输出推挽(Push-Pull),不带上下拉电阻    

                        cpu写1  ----> (外部引脚)高电平

                                  0  -----> (外部引脚)低电平
                输出开漏(Open-Drain),不带上下拉电阻

                        cpu写0  ----> (外部引脚)接地

                                  1  ----> (外部引脚)悬空



(3) GPIOx_OSPEEDR

        端口输出速度寄存器,偏移地址:0x08
        该寄存器用来控制x(x=A,B,C,...,I)分组的16个GPIO的输出速度,每个GPIO口2bits

        编号为y(y=0,1,2,...,15)的GPIO在该寄存器中的bit位置为GPIOx_OSPEEDRGPIO

[2y+1:2y]

        具体输出速度如下:

                                GPIOx_OSPEEDRGPIO[2y+1:2y]                          输出速度

                                                        00                                                     2Mhz (低速)

                                                        01                                                     25Mhz (中速)

                                                        10                                                     50Mhz (快速)

                                                        11                                              30pF 时为 100Mhz(高速)

                                                                                             15pF 时为 80Mhz输出(最大速度)

 

(4) GPIOx_PUPDR

        pu: pull up  上拉

        pd: pull down  下拉

        端口上拉/下拉寄存器,地址偏移为:0x0C

        该寄存器用来控制x(x=A,B,C,..,I)分组的16个GPIO内部上下拉电阻的选择,每个GPIO口2bits

        编号为y(y=0,1,2..., 15)的GPIO口在该寄存器中的bit 位置为 GPIOx_PUPDR[2y+1:2y]

        具体选择情况如下:

              GPIOx_PUPDR[2y+1:2y]                         上下拉情况

                       00                                  无上拉也无下拉 disable pull-up  disable pull_down

                       01                                               上拉   enable pull-up

                       10                                               下拉   enable pull-down

                       11                                                    保留,没用到



(5) GPIOx_IDR

        Input Data Register  输入数据寄存器

        端口输入数据寄存器,偏移地址为: 0x10,复位值: 0x0000 XXXX (开机后高16位值为0,低16位的值不确定)

        该寄存器用来表示x(x=A,B,C,...,I)分组的16GPIO引脚输入的值,其中31:16保留
        高16bits,必须保持复位值。bit 15:0 表示相应的GPIO引脚的输入电平的状态。这些bit,只能为只读,只能在字模式下访问

比如:CPU想要知道GPIOA7是高电平还是低电平

if (GPIOA_IDR & (1 << 7)) {
    PA7是高电平
} else {
    PA7是低电平
}

(6) GPIOx_ODR
        Output Data Register 输出数据寄存器

        端口输出数据寄存器,偏移地址为:0x14,复位值为: 0x0000 0000
        该寄存器用来表示x(x=A,B,C,...,I)分组的16个GPIO的输出值,其中31:16保留。 低位15:0 表示相应的GPIO口线的输出状态

        可以读取也可以写入


(7) GPIOx_BSRR

        BSR: Bit Set Reset  位置位复位操作,该寄存器允许对GPIO寄存器进行原子读 / 修改操作
                Set ----> 把相应的bit位置1
                Reset -----> 把相应的bit位置0
       
 端口置位 / 复位寄存器,偏移地址:0x18,复位值为:0x0000 0000
                位 31:16  BRx: Bit Reset 端口的复位bit位,这些位为只写形式,只能在字、半字、或字节模式下访问

                        往相应的bit位写:

                                1: 对相应的ODR(x-16)位进行复位(0)

                                0: 不会对相应的ODR(x-16)执行任何操作

                位 15:0  BSx: Bit Set  端口x置位,这些位为只写形式,只能在字、半字、或字节模式下访问
                        往相应的bit位写:

                                1: 对相应的ODRx位进行置位(1)
                                0: 不会对相应的ODRx位进行操作


注意:如果同时对BSx和BRx置位,则BSx的优先级更高


(8) GPIOx_LCKR
        端口配置锁定寄存器,偏移地址为:0x1C,复位值为:0x0000 0000

        

        LOCK:

                当一个GPIO引脚的各配置寄存器设定好后,为了防止程序对这些已经配置好的寄存器的一些误操作,可以 LOCK一下

                如果是一个LOCK的状态,该GPIO引脚的配置就不能再修改了

                如果要修改就必须 UNLOCK

        

        位 31:17保留,必须保持复位值
        位 16 LCKK[16]:锁定键,可随时读取此位

    
 

(9) GPIOx_AFRL 

         AFR:Alternate Function Register  复用功能寄存器

        GPIO 复用功能低位寄存器,偏移量  0x20

(10) GPIOx_AFRL   

        GPIO 复用功能低位寄存器,偏移量  0x24     

        每一个GPIO口可以复用多达16种复用功能,每一个GPIO口就需要4bits,一组GPIO口

(16个GPIO口),就需要 16x4= 64bits

        所以复用功能寄存器就需要两个32bits的寄存器 GPIOx_AFRL,GPIOx_AFRH

        编号 0...7   -----> GPIOx_AFRL(x = A...I)
        编号 8...15 -----> GPIOx_AFRH(x = A...I)
        0000:AF0

        0001:AF1

        0010:AF2

        ......

        1111:AF15

                        AFx到底是复用何种功能,得看芯片手册

volatile:

        在C语言中,volatile是一个关键字,告诉编译器该变量值容易发生改变,在编译、读取、存储该变量的时候都不要做任何优化,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取存储数据,不做优化,因为它可能会被程序之外的因素改变

我们首先需要知道,什么是编译器优化

int a;
void main()
{
    a = 1;
}
/*
    在之前的内容中我们已经了解到,对于a=1的底层操作是这样的:
        1. CPU 将 1 存入到 CPU内部寄存器中,比如说R0
        2. CPU 将寄存器内容存入到 &a 对应的空间中
*/

int a,b;
void main()
{
    a = 1;
    b = a;
}
/*
    程序在执行的时候,
    对于a=1这条语句:
        3. CPU将 1 存入到 CPU内部寄存器中,比如说R0
        4. CPU将寄存器内容存入到 &a 对应的空间中
    对于b=a这条语句:
        5. 会先把a这个内存地址的值(也就是1)取出来先存到寄存器里
        6. 然后再把寄存器里的值存储到变量b的内存地址里
*/

/*
    上面的逻辑中有没有问题呢?
        注意对比 4 5 步骤,CPU明明已经存储了 a 的内容,为什么还要加载一遍,
直接存储到变量b的空间中不就行了吗?
        所以,你都能想到的问题,设计编译器的技术大佬会没有想过吗。所以编译
器编译时会基于一些规律/规则对代码进行优化。比如说:一般访问CPU内部寄存器要
比访问内存(RAM)的效率高
*/

也就是说,在上述的例子经过编译器代码优化以后,为了执行效率更高,执行这段程序的流程就会被优化

最终程序执行可能就直接把寄存器的值赋值给变量b这个内存地址了,而不是重新从变量a的内存地址里读取到寄存器,这样效率就提高了
 
编译器优化原则之一:减少对内存访问的次数,因为从内存里读写数据效率比较低
也就是说,遇到这个关键字 volatile 声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化

4. STM32F4xx GPIO时钟使能

在STM32中,外围设备的寄存器在上电的情况下默认是没有时钟的,不给时钟的情况下操作外设是无效的,外设也不会工作,这样的目的是降低功耗。 所以在操作外设之前必须要先使能它的时钟,这就需要我们用RCC来完成时钟的使能


参考<STM32F4xx中文参考手册.pdf> 2.3 存储器映射可知:


GPIO所有的分组全部属于AHB1时钟线

时钟的相关配置  ===> RCC (Reset Clock Control 复位时钟控制)
参考<STM32F4xx中文参考手册.pdf> 2.3 存储器映射 可知:

那么我们现在的目的就是找到有关AHB1外设使能的寄存器,参考<STM32F4xx中文参考手册.pdf> 6.3.12 RCC AHB1 外设时钟使能寄存器(RCC_AHB1RSTR)可知:

此寄存器[8:0]分别控制了GPIOx的时钟使能 
        1     使能时钟 
        0     禁止时钟 

 

比如:

        把RCC_AHB1ENR[5] ---> 1
        使能了GPIOF组的时钟 


总结:

利用寄存器实现GPIO功能的配置的步骤

1) 从原理图找出对应的引脚 比如:点灯
       LED1 ----- LED0 ----- PF9
           经过分析:CPU对PF9输出一个低电平,D1就会亮 
2) 配置 GPIO 分组时钟
3) 配置 GPIO 模式寄存器
4) 配置 GPIO 输出类型
5) 配置 GPIO 输出速率寄存器
6) 配置 GPIO 上下拉寄存器
7) 输出模式,对输出数据寄存器进行操作,输入模式,读取输入数据寄存器

5. 寄存器点灯

led_reg.h

#ifndef __LED_REG_H__
#define __LED_REG_H__

/*
	LED1  PF9
	LED2  PF10
	LED3  PE13
	LED4  PE14
*/

// AHB1 时钟总线寄存器
#define rRCC_AHB1ENR *((volatile unsigned long *)0x40023830)

// GPIO 每组的基址
#define GPIOA_BASE 0x40020000
#define GPIOB_BASE 0x40020400
#define GPIOC_BASE 0x40020800
#define GPIOD_BASE 0x40020C00
#define GPIOE_BASE 0x40021000
#define GPIOF_BASE 0x40021400
#define GPIOG_BASE 0x40021800
#define GPIOH_BASE 0x40021C00
#define GPIOI_BASE 0x40022000

// GPIOF相关的寄存器的地址 
// 模式选择寄存器,偏移地址:0x00
#define rGPIOF_MODER   *((volatile unsigned long *)(GPIOF_BASE + 0x00))
// 输出类型寄存器,偏移地址:0x04
#define rGPIOF_OTYPER  *((volatile unsigned long *)(GPIOF_BASE + 0x04))
// 端口输出速度寄存器,偏移地址:0x08
#define rGPIOF_OSPEEDR *((volatile unsigned long *)(GPIOF_BASE + 0x08))
// 端口上拉/下拉寄存器,地址偏移为:0x0C
#define rGPIOF_PUPDR   *((volatile unsigned long *)(GPIOF_BASE + 0x0C))
// 输入数据寄存器,地址偏移为:0x10
#define rGPIOF_IDR     *((volatile unsigned long *)(GPIOF_BASE + 0x10))
// 输出数据寄存器,地址偏移为:0x14
#define rGPIOF_ODR     *((volatile unsigned long *)(GPIOF_BASE + 0x14))
// 端口置位/复位寄存器,偏移地址:0x18
#define rGPIOF_BSRR    *((volatile unsigned long *)(GPIOF_BASE + 0x18))

// LED1灯 亮
void LED1_ON(void);

// LED1灯 灭
void LED1_OFF(void);

void LED1_Init(void);

#endif

led_reg.c

#include "led_reg.h"

// LED1灯 亮
void LED1_ON(void) {

	rGPIOF_BSRR |= 1 << 15;
}

// LED1灯 灭
void LED1_OFF(void) {
	
	unsigned long r = rGPIOF_BSRR;
	
	r &= ~(1 << 15);

	r |= 1 << 9;
	
	rGPIOF_BSRR = r;
}

void LED1_Init(void) {
	
	unsigned long r = 0;

	// 使能时钟
	rRCC_AHB1ENR |= (1 << 5);
	
	// 模式选择寄存器 ---> 输出模式[19:18] 01
	r = rGPIOF_MODER;
	r &= ~(1 << 19);
	r |= (1 << 18);
	rGPIOF_MODER = r;
	
	// 输出类型寄存器 ---> 推挽输出 0 
	rGPIOF_OTYPER &= ~(1 << 9);
	
	// 端口输出速度寄存器 ---> [19:18] 11
	r = rGPIOF_OSPEEDR;
	r |= (1 << 19);
	r |= (1 << 18);
	rGPIOF_OSPEEDR = r;

	// 端口上拉/下拉寄存器 --->无上下拉 [19:18] 00
	r = rGPIOF_PUPDR;
	r &= ~(1 << 19);
	r &= ~(1 << 18);
	rGPIOF_PUPDR = r;
	
	LED1_ON();
}

6. 使用stm32官方固件库来操作GPIO口

stm32固件库移植 -----> 具体移植操作参考文档
 

(1) 配置AHB1总线上的外设时钟   RCC_AHB1PeriphClockCmd    ----> rcc.h
// 等同于RCC_AHB1ENR寄存器

void RCC_AHB1PeriphClockCmd(uint32_t RCC_AHB1Periph, 
                                FunctionalState NewState);

@RCC_AHB1Periph:指定AHB1总线外设,也就是要配置时钟的外设
    可以是以下任意一个宏:
        RCC_AHB1Periph_GPIOA
        RCC_AHB1Periph_GPIOB
        ...
        RCC_AHB1Periph_GPIOI
 
@NewState:指定该外设的时钟状态
    ENBALE 使能,为该外设提供时钟信号
     DISBALE 禁止,不提供
------------------------------------------------------------------
比如:使能GPIOF时钟 
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);
(2) 初始化配置GPIO(完成GPIO的4个配置寄存器的赋值)     GPIO_init   ----> gpio.h
void GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_InitStruct);

@GPIOx:指定要初始化的GPIO分组
            GPIOA
            GPIOB
            ...
            GPIOI
 
@GPIO_InitStruct:指向GPIO初始化信息结构体
    该结构体原型如下所示:(已经定义在头文件中,直接使用就好)
        typedef struct {
            uint32_t GPIO_Pin;         
            GPIOMode_TypeDef GPIO_Mode;     
            GPIOSpeed_TypeDef GPIO_Speed;  
            GPIOOType_TypeDef GPIO_OType;   
            GPIOPuPd_TypeDef GPIO_PuPd;   
       } GPIO_InitTypeDef;

        @GPIO_Pin:指定要配置的GPIO引脚(可以位或多个,表示配置同一组的多个引脚为相
同模式,如:GPIO_Pin_0 | GPIO_Pin_1) 
                GPIO_Pin_0
                GPIO_Pin_1
                ...
                GPIO_Pin_15

         @GPIO_Mode:指定要配置的GPIO引脚的功能模式
                GPIO_Mode_IN   输入模式
                GPIO_Mode_OUT  输出模式
                GPIO_Mode_AF   复用功能模式
                GPIO_Mode_AN   模拟模式
 
        @GPIO_Speed:指定引脚速率 
                GPIO_Speed_2MHz    2M低速
                GPIO_Speed_25MHz   25M中速
                GPIO_Speed_50MHz   50M快速
                GPIO_Speed_100MHz  100M高速
 
        @GPIO_OType:指定输出类型 
                GPIO_OType_PP   输出推挽
                GPIO_OType_OD   输出开漏
        
        @GPIO_PuPd:指定上下拉选择
                GPIO_PuPd_NOPULL   无上拉,也无下拉
                GPIO_PuPd_UP       上拉
                GPIO_PuPd_DOWN     下拉
---------------------------------------------------------------
比如:配置PF9为带下拉的推挽输出模式

// 定义GPIO初始化信息结构体
GPIO_InitTypeDef g;

// 根据配置需要,对结构体成员赋值
g.GPIO_Pin = GPIO_Pin_9; // 9号引脚
g.GPIO_Mode = GPIO_Mode_OUT; // 输出模式
g.GPIO_Speed = GPIO_Speed_50MHz; // 50MHz
g.GPIO_OType = GPIO_OType_PP; // 输出推挽
g.GPIO_PuPd = GPIO_PuPd_DOWN; // 下拉

// 根据结构体信息,完成GPIO配置
GPIO_Init(GPIOF, &g);                
(3) 输入:从配置好的GPIO引脚,获取外部电平
a. 获取输入寄存器的指定的GPIO引脚的值
    
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

    @GPIOx:指定GPIO分组
        GPIOA
        GPIOB
        GPIOC
        ...
        GPIOI
    
    @GPIO_Pin:指定GPIO引脚编号
        GPIO_Pin_0
        GPIO_Pin_1
        ...
        GPIO_Pin_15

    返回值:
        返回指定GPIO引脚的电平状态(1个引脚的状态)
            Bit_SET     表示该GPIO引脚输入为高电平  ===> 1
            Bit_RESET   表示该GPIO引脚输入为低电平  ===> 0
--------------------------------------------------------------------------------
b. 获取指定分组的输入寄存器整组的值(16位)

uint16_t GPIO_ReadInputData(GPIO_TypeDef *GPIOx);

    @GPIOx:指定GPIO分组
        GPIOA
        GPIOB
        ...
        GPIOI
        
    返回值:
        返回获取到的GPIO分组的输入数据寄存器中数据
    
要注意的是该返回值是uint16_t类型,具有16bits,分别对应该GPIO分组的16个GPIO引脚的输入
电平状态
    bit0  ---> 0号引脚
    ...
    bit15 ---> 15号引脚
--------------------------------------------------------------------------------
c. 获取输出数据寄存器的指定的GPIO引脚的值

uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
--------------------------------------------------------------------------------
d. 获取指定分组的输出寄存器整组的值(16位)

uint16_t GPIO_ReadOutputData(GPIO_TypeDef *GPIOx);
(4) 输出:CPU通过引脚向外部电路输出一个电平值
a. 向指定的引脚输出指定的电平值

void GPIO_WriteBit(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, BitAction BitVal);

    @GPIOx:指定GPIO分组
        GPIOA
        GPIOB
        GPIOC
        ...
        GPIOI
    
    @GPIO_Pin:指定GPIO引脚编号
        GPIO_Pin_0
        GPIO_Pin_1
        ...
        GPIO_Pin_15

    @BitVal:想输出的电平 
		Bit_RESET  低电平  ===> 0
		Bit_SET    高电平  ===> 1
------------------------------------------------------------------------------
b. 往指定的GPIO分组整组输出(16个引脚一块输出)

void GPIO_Write(GPIO_TypeDef *GPIOx, uint16_t PortVal);
				
    @GPIOx:指定GPIO分组
        GPIOA
        GPIOB
        GPIOC
        ...
        GPIOI

	@PortVal:uint16_t类型(16位的整数)
		bit0 ---> 引脚0
        bit1 ---> 引脚1
        bit2 ---> 引脚2
        ...
        bit15 ---> 引脚15

比如:
    GPIO_Write(GPIOF, 0xF0F0); // 0xF0F0:1111 0000 1111 0000
    将PF15~PF12和PF7~PF4输出高电平,其它的输出低电平
------------------------------------------------------------------------------
c. 用来将指定的GPIO引脚输出为高电平(GPIO_BSRR:位置位复位寄存器)
    
void GPIO_SetBits(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
------------------------------------------------------------------------------
d. 用来将指定的GPIO引脚输出低电平(GPIO_BSRR:位置位复位寄存器)

void GPIO_ResetBits(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
------------------------------------------------------------------------------
e. 将指定引脚的输出状态进行翻转(1--->0,0--->1)

void GPIO_ToggleBits(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
(5) 当GPIO引脚被配置为复用模式的时候才需要使用
void GPIO_PinAFConfig(GPIO_TypeDef *GPIOx, uint16_t GPIO_PinSource, 
                                              uint8_t GPIO_AF);
			
    @GPIOx:指定的GPIO分组 
		GPIOA 
		GPIOB 
		.....
        GPIOI

	@GPIO_PinSource:指定GPIO引脚(不可以位或)
		GPIO_PinSource0
		GPIO_PinSource1
		.....
        GPIO_PinSource15
			
    @GPIO_AF:指定复用成什么功能 		
        GPIO_AF_TIM1
		......

7. 固件库操作GPIO

利用按键去控制灯,蜂鸣器
        第一次按下 KYE1 按键的时候,LED1 和 LED2 亮,第二次按下 KEY1 按键的时候,LED1和LED2灭
                KEY2 ---> LED3 和 LED4
                KEY3 ---> BEEP     


消抖:
        抖动的原因:单片机上的按键大部分都是机械弹性按键,这种按键在按下或者弹起的时候发生抖动,对实验造成影响

       

        解决方法:

                1. 硬件消抖

                        在按键的电路上并联一个电容,利用电容的充放电特性对毛刺进行平滑出来,有效果,但是现在基本不会用这种方法

                        增加了成本

                2. 软件消抖

                        增加一个小延时跳过抖动的时间,大部分的按键延时 10ms 左右就可以跳过抖动了

led.h

#ifndef __LED_H__
#define __LED_H__

#include "stm32f4xx.h"

/*
	LED1:PF9
	LED2:PF10
	LED3:PE13
	LED4:PE14
*/

// 哪组寄存器(LED灯引脚所在的GPIO分组)
#define LED1_GPIO GPIOF
#define LED2_GPIO GPIOF
#define LED3_GPIO GPIOE
#define LED4_GPIO GPIOE

// 哪个引脚(LED灯引脚)
#define LED1_Pin  GPIO_Pin_9
#define LED2_Pin  GPIO_Pin_10
#define LED3_Pin  GPIO_Pin_13
#define LED4_Pin  GPIO_Pin_14

// LED灯需要配置的时钟外设
#define LED1_RCC_AHB1Periph   	RCC_AHB1Periph_GPIOF
#define LED2_RCC_AHB1Periph   	RCC_AHB1Periph_GPIOF
#define LED3_RCC_AHB1Periph   	RCC_AHB1Periph_GPIOE
#define LED4_RCC_AHB1Periph   	RCC_AHB1Periph_GPIOE

// 使能时钟
#define LED1_Clock()   	RCC_AHB1PeriphClockCmd(LED1_RCC_AHB1Periph, ENABLE)
#define LED2_Clock()   	RCC_AHB1PeriphClockCmd(LED2_RCC_AHB1Periph, ENABLE)
#define LED3_Clock()   	RCC_AHB1PeriphClockCmd(LED3_RCC_AHB1Periph, ENABLE)
#define LED4_Clock()   	RCC_AHB1PeriphClockCmd(LED4_RCC_AHB1Periph, ENABLE)

// 开
#define LED1_ON()       GPIO_ResetBits(LED1_GPIO, LED1_Pin)
// 关
#define LED1_OFF()      GPIO_SetBits(LED1_GPIO, LED1_Pin)

#define LED2_ON()       GPIO_ResetBits(LED2_GPIO, LED2_Pin)
#define LED2_OFF()      GPIO_SetBits(LED2_GPIO, LED2_Pin)

#define LED3_ON()       GPIO_ResetBits(LED3_GPIO, LED3_Pin)
#define LED3_OFF()      GPIO_SetBits(LED3_GPIO, LED3_Pin)

#define LED4_ON()       GPIO_ResetBits(LED4_GPIO, LED4_Pin)
#define LED4_OFF()      GPIO_SetBits(LED4_GPIO, LED4_Pin)

// 将LED灯的输出状态进行翻转(亮--->灭,灭--->亮)
#define LED1_reversal_status() 		GPIO_ToggleBits(LED1_GPIO, LED1_Pin)
#define LED2_reversal_status() 		GPIO_ToggleBits(LED2_GPIO, LED2_Pin)
#define LED3_reversal_status() 		GPIO_ToggleBits(LED3_GPIO, LED3_Pin)
#define LED4_reversal_status() 		GPIO_ToggleBits(LED4_GPIO, LED4_Pin)

/* 
	初始化配置GPIO(完成GPIO的4个配置寄存器的赋值)
		@LEDx_GPIO:指定要初始化的GPIO分组
			LED1_GPIO
			LED2_GPIO
			LED3_GPIO
			LED4_GPIO
		@LEDx_Pin:指定要配置的GPIO引脚
                   (可以位或多个,表示配置同一组的多个引脚为相同模式) 
			LED1_Pin
			LED2_Pin
			LED3_Pin
			LED4_Pin
*/
void LED_Init(GPIO_TypeDef *LEDx_GPIO, uint16_t LEDx_Pin);

#endif

led.c

#include "led.h"

/* 
	初始化配置GPIO(完成GPIO的4个配置寄存器的赋值)
		@LEDx_GPIO:指定要初始化的GPIO分组
			LED1_GPIO
			LED2_GPIO
			LED3_GPIO
			LED4_GPIO
		@LEDx_Pin:指定要配置的GPIO引脚
                   (可以位或多个,表示配置同一组的多个引脚为相同模式) 
			LED1_Pin
			LED2_Pin
			LED3_Pin
			LED4_Pin
*/
void LED_Init(GPIO_TypeDef *LEDx_GPIO, uint16_t LEDx_Pin) {

	// 1.定义GPIO初始化信息结构体
	GPIO_InitTypeDef g;

	// 2.根据配置需要,对结构体成员赋值
	g.GPIO_Pin = LEDx_Pin; // 几号引脚
	g.GPIO_Mode = GPIO_Mode_OUT; // 输出模式
	g.GPIO_Speed = GPIO_Speed_2MHz; // 2MHz
	g.GPIO_OType = GPIO_OType_PP; // 输出类型
	g.GPIO_PuPd = GPIO_PuPd_NOPULL; // 无上拉也无下拉

	// 3.根据结构体信息,完成GPIO配置
	GPIO_Init(LEDx_GPIO, &g);
}

key.h

#ifndef __KEY_H__
#define __KEY_H__

#include "stm32f4xx.h"

/*
	KEY1:PA0
	KEY2:PE2
	KEY3:PE3
	KEY3:PE4
*/

// 哪组寄存器(KEY按键引脚所在的GPIO分组)
#define KEY1_GPIO  GPIOA
#define KEY2_GPIO  GPIOE
#define KEY3_GPIO  GPIOE
#define KEY4_GPIO  GPIOE

// 哪个引脚(KEY按键引脚)
#define KEY1_Pin  GPIO_Pin_0
#define KEY2_Pin  GPIO_Pin_2
#define KEY3_Pin  GPIO_Pin_3
#define KEY4_Pin  GPIO_Pin_4

// KEY按键需要配置的时钟外设
#define KEY1_RCC_AHB1Periph   RCC_AHB1Periph_GPIOA
#define KEY2_RCC_AHB1Periph   RCC_AHB1Periph_GPIOE
#define KEY3_RCC_AHB1Periph   RCC_AHB1Periph_GPIOE
#define KEY4_RCC_AHB1Periph   RCC_AHB1Periph_GPIOE

// 使能时钟
#define KEY1_Clock()   RCC_AHB1PeriphClockCmd(KEY1_RCC_AHB1Periph, ENABLE)
#define KEY2_Clock()   RCC_AHB1PeriphClockCmd(KEY2_RCC_AHB1Periph, ENABLE)
#define KEY3_Clock()   RCC_AHB1PeriphClockCmd(KEY3_RCC_AHB1Periph, ENABLE)
#define KEY4_Clock()   RCC_AHB1PeriphClockCmd(KEY4_RCC_AHB1Periph, ENABLE)

/* 
	初始化配置GPIO(完成GPIO的4个配置寄存器的赋值)
		@KEYx_GPIO:指定要初始化的GPIO分组
			KEY1_GPIO
			KEY2_GPIO
			KEY3_GPIO
			KEY4_GPIO
		@KEYx_Pin:指定要配置的GPIO引脚
                   (可以位或多个,表示配置同一组的多个引脚为相同模式) 
			KEY1_Pin
			KEY2_Pin
			KEY3_Pin
			KEY4_Pin
*/
void KEY_Init(GPIO_TypeDef *KEYx_GPIO, uint16_t KEYx_Pin);

/* 
	获取输入寄存器的指定的GPIO引脚的值
		@KEYx_GPIO:指定要初始化的GPIO分组
			KEY1_GPIO
			KEY2_GPIO
			KEY3_GPIO
			KEY4_GPIO
		@KEYx_Pin:指定要配置的GPIO引脚
			KEY1_Pin
			KEY2_Pin
			KEY3_Pin
			KEY4_Pin
	返回值:
		高电平(1) ---> 弹起
		低电平(0) ---> 按下
*/
int KEY_ReadInputData(GPIO_TypeDef *KEYx_GPIO, uint16_t KEYx_Pin);

#endif

key.c

#include "key.h"

/* 
	初始化配置GPIO(完成GPIO的4个配置寄存器的赋值)
		@KEYx_GPIO:指定要初始化的GPIO分组
			KEY1_GPIO
			KEY2_GPIO
			KEY3_GPIO
			KEY4_GPIO
		@KEYx_Pin:指定要配置的GPIO引脚
                   (可以位或多个,表示配置同一组的多个引脚为相同模式) 
			KEY1_Pin
			KEY2_Pin
			KEY3_Pin
			KEY4_Pin
*/
void KEY_Init(GPIO_TypeDef *KEYx_GPIO, uint16_t KEYx_Pin) {

	// 1.定义GPIO初始化信息结构体
	GPIO_InitTypeDef g;
	
	// 2.根据配置需要,对结构体成员赋值
	g.GPIO_Pin = KEYx_Pin; // 几号引脚
	g.GPIO_Mode = GPIO_Mode_IN; // 输入模式
	g.GPIO_PuPd = GPIO_PuPd_NOPULL; // 无上拉也无下拉
	
	// 3.根据结构体信息,完成GPIO配置
	GPIO_Init(KEYx_GPIO, &g);
}

/* 
	获取输入寄存器的指定的GPIO引脚的值
		@KEYx_GPIO:指定要初始化的GPIO分组
			KEY1_GPIO
			KEY2_GPIO
			KEY3_GPIO
			KEY4_GPIO
		@KEYx_Pin:指定要配置的GPIO引脚
			KEY1_Pin
			KEY2_Pin
			KEY3_Pin
			KEY4_Pin
	返回值:
		高电平(1) ---> 弹起
		低电平(0) ---> 按下
*/
int KEY_ReadInputData(GPIO_TypeDef *KEYx_GPIO, uint16_t KEYx_Pin) {
	
	if (GPIO_ReadInputDataBit(KEYx_GPIO, KEYx_Pin) == Bit_SET) {
		return 1;
	}
	return 0;
}

beep.h

#ifndef __BEEP_H__
#define __BEEP_H__

#include "stm32f4xx.h"

/*
	BEEP:PF8
*/

// 哪组寄存器(BEEP蜂鸣器引脚所在的GPIO分组)
#define BEEP_GPIO GPIOF

// 哪个引脚(BEEP蜂鸣器引脚)
#define BEEP_Pin  GPIO_Pin_8

// BEEP需要配置的时钟外设
#define BEEP_RCC_AHB1Periph   RCC_AHB1Periph_GPIOF

// 使能时钟
#define BEEP_Clock()   RCC_AHB1PeriphClockCmd(BEEP_RCC_AHB1Periph, ENABLE)

// 响
#define BEEP_ON()       GPIO_SetBits(BEEP_GPIO, BEEP_Pin)
// 不响
#define BEEP_OFF()      GPIO_ResetBits(BEEP_GPIO, BEEP_Pin)

// 将BEEP蜂鸣器的输出状态进行翻转(响--->不响,不响--->响)
#define BEEP_reversal_status() 		GPIO_ToggleBits(BEEP_GPIO, BEEP_Pin)

/* 
	初始化配置GPIO(完成GPIO的4个配置寄存器的赋值)
		@BEEPx_GPIO:指定要初始化的GPIO分组
			BEEP_GPIO
		@BEEP_Pin:指定要配置的GPIO引脚
				   (可以位或多个,表示配置同一组的多个引脚为相同模式) 
			BEEP_Pin
*/
void BEEP_Init(GPIO_TypeDef *BEEPx_GPIO, uint16_t BEEPx_Pin);

#endif

beep.c

#include "beep.h"

/* 
	初始化配置GPIO(完成GPIO的4个配置寄存器的赋值)
		@BEEPx_GPIO:指定要初始化的GPIO分组
			BEEP_GPIO
		@BEEP_Pin:指定要配置的GPIO引脚
				   (可以位或多个,表示配置同一组的多个引脚为相同模式) 
			BEEP_Pin
*/
void BEEP_Init(GPIO_TypeDef *BEEPx_GPIO, uint16_t BEEPx_Pin) {

	// 1.定义GPIO初始化信息结构体
	GPIO_InitTypeDef g;
	
	// 2.根据配置需要,对结构体成员赋值
	g.GPIO_Pin = BEEPx_Pin; // 指定要配置的GPIO引脚
	g.GPIO_Mode = GPIO_Mode_OUT; // 指定要配置的GPIO引脚的功能模式
	g.GPIO_Speed = GPIO_Speed_2MHz; // 指定引脚速率
	g.GPIO_OType = GPIO_OType_PP; // 指定输出类型 
	g.GPIO_PuPd = GPIO_PuPd_NOPULL; // 指定上下拉选择 
	
	// 3. 根据结构体信息,完成GPIO配置
	GPIO_Init(BEEPx_GPIO, &g);
}

main.c

#include "stm32f4xx.h"
#include "systick.h"
#include "led.h"
#include "beep.h"
#include "key.h"

int main(void) {
	
	/* LED1 */
	LED1_Clock();
	LED_Init(LED1_GPIO, LED1_Pin);
	LED1_OFF();
	
	/* LED2 */
	LED2_Clock();
	LED_Init(LED2_GPIO, LED2_Pin);
	LED2_OFF();
	
	/* LED3 */
	LED3_Clock();
	LED_Init(LED3_GPIO, LED3_Pin);
	LED3_OFF();
	
	/* LED4 */
	LED4_Clock();
	LED_Init(LED4_GPIO, LED4_Pin);
	LED4_OFF();
	
	/* KEY1 */
	KEY1_Clock();
	KEY_Init(KEY1_GPIO, KEY1_Pin);
	
	/* KEY2 */
	KEY2_Clock();
	KEY_Init(KEY2_GPIO, KEY2_Pin);
	
	/* KEY3 */
	KEY3_Clock();
	KEY_Init(KEY3_GPIO, KEY3_Pin);
	
	/* KEY4 */
	KEY4_Clock();
	KEY_Init(KEY4_GPIO, KEY4_Pin);
	 
	/* BEEP */
	BEEP_Clock();
	BEEP_Init(BEEP_GPIO, BEEP_Pin);
	BEEP_OFF();
	
	while (1) {
		
		// 按键KEY1
		if (KEY_ReadInputData(KEY1_GPIO, KEY1_Pin) == 0) { // 按下
			// 消抖
			delay_ms(10);
			if (KEY_ReadInputData(KEY1_GPIO, KEY1_Pin) == 0) { // 确定是人为按下
				LED1_reversal_status();
				LED2_reversal_status();
			}
			while (KEY_ReadInputData(KEY1_GPIO, KEY1_Pin) == 0);				
		}
		
		// 按键KEY2
		if (KEY_ReadInputData(KEY2_GPIO, KEY2_Pin) == 0) { // 按下
			// 消抖
			delay_ms(10);
			if (KEY_ReadInputData(KEY2_GPIO, KEY2_Pin) == 0) { // 确定是人为按下
				LED3_reversal_status();
				LED4_reversal_status();
			}
			while (KEY_ReadInputData(KEY2_GPIO, KEY2_Pin) == 0);				
		}
		
		// 按键KEY3
		if (KEY_ReadInputData(KEY3_GPIO, KEY3_Pin) == 0) { // 按下
			// 消抖
			delay_ms(10);
			if (KEY_ReadInputData(KEY3_GPIO, KEY3_Pin) == 0) { // 确定是人为按下
				BEEP_reversal_status();
			}
			while (KEY_ReadInputData(KEY3_GPIO, KEY3_Pin) == 0);				
		}
	}
}
  • 14
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值