详解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来向外部输出一个电平信息
如果是复用模式 则后面再讲