什么是GPIO?(详细介绍)

以STM32F4系列的单片机做例子

一.引入

单片机最小系统的组成:

芯片 + 供电电路 + 复位电路 + 时钟(晶振)电路

一个完成的系统的组成

最小系统 + 项目所需要的其他硬件(外设)

芯片:

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

引脚(俗称 官方称呼“GPIO”)

二. GPIO

GPIO是什么?

General Purpose Input Output 通用功能输出输出

GPIO就是从芯片内部引出来一根功能复用的口线(电线)

功能复用是指:GPIO的引脚可以由CPU配置成不同的功能

比如:输入功能 输出功能 模拟功能 复用功能等等

分析GPIO内部结构图如picture/STM32F4XX_GPIO内部结构.PNG

通过图我们可以得知 每个GPIO可以独立地被配置成不同的功能。

GPIO配置功能如下:

(1)输入功能

CPU可以通过该GPIO的来获取外部电路输入的一个电平状态

输入功能又可以分为几种模式:

a.带上拉的输入(input pull-up)

默认接一个上拉电阻

此时就算IO引脚没有外部输入信号时 CPU也能读到一个高电平

只有在外部电路输入低电平的时候 CPU读取到的才是低电平

b.带下拉的输入

默认接一个下拉电阻

此时就算IO引脚没有外部输入信号时 CPU也能读到一个低电平

只有在外部电路输入高电平的时候 CPU读取到的才是高电平

c.输入悬空

既不接上拉电阻 也不接下拉电阻

这种情况下 IO引脚的电平状态完全由外部输入所绝对 此时CPU可以通过读取数据的

操作来获取外部电路的工作状态

d.模拟输入

该引脚被设置为模拟输入的时候 能够获取到模拟信号

通过ADC转换为数字量

(2)输出功能

CPU可以通过该GPIO口往外部输出一个电平状态(相当于可以控制外部电路工作)

输出功能也可以分为以下两种模式

a.输出推挽 (PP: push-pull)

CPU往外写高电平(1)时,此时引脚输出一个高电平

CPU往外写低电平(0)时,此时引脚输出一个低电平

b.输出开漏 (OD: open drain)

不输出电压

CPU往外写低电平(0)时 此时引脚接VSS(GND)相当于接地

CPU外外写高电平(1)时 此时引脚的电平状态由上下拉电阻决定

(3)复用功能

复用功能是指GPIO口用作其他的外设的功能口线

比如:

I2C USART SPI等等

每个GPIO口都可以被配置成多达16中复用功能

具体哪个引脚可以被复用成哪种功能 需要看原理图

STM32F4xx共有144个GPIO引脚

分为九组 记为GPIOA , GPIOB ... GPIOI

简写PA PB ... PI

每组有16根引脚 编号从0~15

也就是说:

比如GPIOA这一组就有

GPIOA0 PA0

GPIOA1 PA1

GPIOA2 PA2

...

GPIOA15 PA15

而这些GPIO的功能 都有独立的寄存器组(不同的GPIO硬件控制器)来配置他们

也就是说我们如果要使用比如GPIO口的输入功能的话 我们首先需要把对应寄存器组配置好。

那么如果我们要去配置(访问)寄存器的话 就必须知道寄存器的地址

每组GPIO的地址分布如下: 参考:第 192 页的第 7.4.11 节:GPIO 寄存器映射

边界地址 外设 总线:

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

边界地址:指对应的寄存器组的起始地址(基址)和结束地址

外设: 该寄存器组对应的硬件控制器

总线: 该硬件控制器所处的系统时钟总线

请注意:任何一个硬件控制器想要去正常工作 都必须开启(使能)时钟

而总线 就是给硬件控制器提供时钟的

那么有哪些寄存器呢?分别有什么用呢?

三.STM32F4XX GPIO寄存器

每个通用 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_AFRH 和 GPIOx_AFRL)。

(1)GPIOx_MODER :功能模式选择寄存器

偏移地址:0x00 (寄存器地址 = 基址 + 偏移地址)

比如 :GPIOA_MODER的地址 0x40020000 + 0x00 = 0x40020000

该寄存器用来控制GPIOx(x=A,B,C...I)组的16个引脚的模式(4种:输入、输出,模拟,复用)

一个寄存器是32bits 一组GPIOx共有16个引脚

每个GPIO引脚占2bits

2bits正好可以表示4种状态

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

具体配置如下:

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

00 输入模式

01 输出模式

10 复用模式

11 模拟模式

例子: 用c代码将PF9配置称为输出模式

分析:

GPIOF组寄存器的起始地址(基址):0x4002 1400

GPIOx_MODER的偏移地址是0x00

so:

GPIOF_MODER的寄存器地址:0x4002 1400 + 0x00 = 0x4002 1400

如果要将PF9设置为输出模式 就需要将

GPIOF_MODER[2*9+1:2*9]

GPIOF_MODER[19:18] ==> 01

把地址为0x40021400的寄存器中的bit19置为0 bit18置为1 怎么做到这两点呢?

通过地址我们就可以将寄存器中的bit置位

在STM32中 用unsigned long来表示地址的值

unsigned long * p = (unsigned long *)0x4002 1400

但是一般情况下我们会在地址的前面加上volatile 变成如下:

volatile unsigned long * p = (volatile unsigned long *)0x4002 1400

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

而不会被编译器优化成别的地址 一般用于多线程的全局变量 中断处理函数访问

的全局变量 状态寄存器。

那么我们就可以通过指针p将地址0x40021400的寄存器中的bit19置为0 bit18置为1

操作如下:

xxxxxxxxxxxx yy xxxxxxxxxxxxxxxxxx

<----12----> <------ 18 ------>

& 111111111111 01 111111111111111111 <=先把bit19置为0

===> xxxxxxxxxxxx 0y xxxxxxxxxxxxxxxxxx

1<<19 000000000000 10 000000000000000000

~(1<<19) 111111111111 01 111111111111111111

*p = *p & ~(1<<19)

再把bit18置为1

类似与上面操作 为:

*p = *p | (1<< 18)

所以我们分两步完成这个操作:

*p = *p & ~(1<<19)

*p = *p | (1<< 18)

但是实际上面的操作对寄存器进行了两次操作 效率太低

有点耗费硬件资源 我们对寄存器的修改必须一步到位

所以我们会先定义一个中间变量 用来记录寄存器的值

然后再通过中间变量 一步到位去修改寄存器的值 如下操作

unsigned long r = 0 ;

r = *p ; //先用r保存寄存器中的值 并按照需求修改r值

r &= ~(1<<19);

r |= (1<<18);

*p = r; //通过中间变量 一步到位修改寄存器的值

例子: 将PA0配置为输入模式

用C语言配置一次 用汇编配置一次

C语言:

volatile unsigned long * p = (volatile unsigned long * )0x40020000

unsigned long r = 0 ;

r = *p;

r &= ~3 ; //把bit1和bit0都设置为0 00表示输入模式

*p = r;

汇编:

LDR R0,=0X40020000

LDR ,R1,[R0] ;R1就相当于r 此指令相当于r = *p

BIC R1,R1,#0X03 ; r &= ~3

STR R1,[R0]

(2)GPIOx_OTYPER :Output Type Register 输出类型选择寄存器

偏移地址:0x04

该寄存器用来选择GPIOx(x=A,B...I)这组的16个GPIO引脚的输出类型

寄存器有32bits

低16个bit用于保存对于编号引脚的输出类型 高16bit保留

一个bit保存一个引脚

一个bit有两种状态 分别对应开漏输出和推挽输出

每个GPIO引脚占1bit 编号为y(y=0,1,2...15)的引脚在该寄存器中对于的bit为GPIOx_OTYPER[y]

具体配置如下:

GPIOx_OTYPER[y] 输出类型

1 输出开漏(OD)

0 输出推挽(PP)

(3)GPIOx_OSPEEDR:Output Speed Register 输出速率寄存器

偏移地址:0x08

用于控制GPIOx组的16个GPIO引脚的输出速率

每个引脚占2bit

编号为y的引脚在该寄存器中的bit位是GPIOx_OSPEEDR[2y+1:2y]

具体配置如下:

GPIOx_OSPEEDR 速率

00 2MHZ

01 25MHZ

10 50MHZ

11 30pf则为100MHZ

15pf则为80MHZ

(4)GPIOx_PUPDR:Pull Up Pull Down Register 端口上拉/下拉寄存器

偏移地址:0x0c

该寄存器用来控制GPIOx组的16个引脚的上拉/下拉选择

每个GPIO引脚占2bits

编号为y的GPIO引脚在该寄存器中所在的bit为GPIOx_PUPDR[2y+1:2y]

具体配置如下:

GPIOx_PUPDR 上下拉选择

00 无上拉、无下拉

01 上拉

10 下拉

11 保留

(5)GPIOx_IDR: Input Data Register 输入数据寄存器

偏移地址:0x10

该寄存器用来表示GPIOx这组的16个GPIO引脚的输入的电平状态值

每个GPIO引脚占1bits 该寄存器中高16bit保留没有使用

低16bit表示x组的16个引脚的电平状态

比如:GPIOx_IDR[0] ==>表示的就是该组的第0个引脚GPIOx0的输入电平状态

具体配置如下:

GPIOx_IDR[y] 编号为y的引脚的输入电平状态

1 高电平

0 低电平

比如:

CPU想要知道GPIOA0这个引脚输入的是高电平还是低电平?

思路:

if(GPIOA_IDR & 0X01 == 0X01)

{

PA0为高电平

}

else

{

PA0为低电平

}

===>

volatile unsigned long * p =(volatile unsigned long *)(0x40020000+0x10)

if(*p & 0x01)

{

PA0为高电平

}

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

偏移地址: 0x14

该寄存器保存了该组16个GPIO引脚的输出电平状态

高16bit保留的 低16个bit就是对于编号的引脚的输出电平状态

具体配置如下:

GPIOx_ODR[y] 编号为y的引脚的输出电平状态

1 高电平

0 低电平

(7)GPIOx_BSRR:Bit Set Reset Register 端口置位/复位寄存器

偏移地址:0x18

置位:set 把bit位置为1

复位:reset 把bit位置为0

该寄存器用来表示GPIOx组的16个GPIO引脚的输出状态

其中:

高16bits为端口复位寄存器

低16bits为端口置位寄存器

这个寄存器有点特殊 写1有效 写0无效

将GPIOx_BSRR[31:16]置为1 表示将GPIOx15~GPIOx0设置为0

将GPIOx_BSRR[15:0]置为1 表示将GPIOx15~GPIOx0设置为1

实现效果跟GPIOx_ODR一样 用来设置GPIO引脚的输出状态

(8)GPIOx_LCKR :锁定寄存器

(9)GPIOx_AFRL:复用功能低位寄存器 偏移地址:0x20

(10)GPIOx_AFRH:复用功能高位寄存器 偏移地址:0x24

GPIOx_AFRL和GPIOx_AFRH这两个寄存器是放在一起使用的

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

因为一个GPIO引脚最多有16个复用功能 那么1个GPIO引脚需要4个bit

所以16个引脚就需要16*4 = 64bits 也就是2个寄存器的空间

GPIO引脚编号为0-7由GPIOx_AFRL进行配置

8-15由GPIOx_AFRH进行配置

具体的值表示哪个复用功能或者引脚有哪些复用功能

需要结合电路原理图和功能手册来看

四. STM32F4XX GPIO时钟使能

根据上述的寄存器 就可以去实现所有基于GPIO能够完成的功能配置了

比如:

点亮led灯

获取key按键的状态(按下/松开)

控制蜂鸣器等等

但是我们在之前就提到过 任何一个硬件控制器想要工作 都必须去实现时钟使能

(GPIO所有分组全部属于AHB1时钟总线)

那么时钟的相关配置 请参考RCC部分(数据手册的第六章)

RCC: Reset Clock Control 复位时钟控制 基址:0x40023800

那么现在我们的目的是使能GPIO分组时钟:

RCC AHB1外设时钟使能寄存器(RCC_AHB1ENR)

偏移地址:0x30

该寄存器的第0位到第8位分别控制GPIOA到GPIOI组时钟的使能:

1 使能对于GPIO分组的时钟

0 禁止对应GPIO分组的时钟

比如:使能GPIOF组时钟

==》RCC_AHB1ENR[5] -> 1

C语言实现:

volatile unsigned long * p = (volatile unsigned long *)(0x40023800 + 0x30);

unsigned long r = 0;

r = *p;

r |= 1<<5;

*p = r;

GPIOF组的时钟使能后 就可以去配置GPIOF组的GPIO引脚了

配置后这些GPIO引脚可以与连接硬件电路正常工作了

总结:

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

1)配置GPIO分组时钟(RCC_AHB1ENR)

2)配置GPIO功能模式(GPIOx_MODER)

3)配置输出类型(输出功能)

4)配置输出速率

5)配置上拉/下拉

6)如果是输入模式 则通过GPIOx_IDR可以获取外部电路工作状态

如果是输出模式 则通过GPIOx_ODR或这GPIOx_BSRR来向外部输出一个电平信息

如果是复用模式 则后面再讲