GPIO
学习使用STM32F407VET6GPIO外设
寄存器和标准外设库
1. 寄存器
- 存储器映射
存储器本身是不具有地址的,是一块具有特定功能的内存单元,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就叫做存储区映射。给内存单元分配地址之后,就可以通过指针去操作内存地址。 - 存储器映射表
STM32是一个32位的单片机,它的地址范围为2的32次方,也就是4GB的地址空间。为了降低不同客户在相同应用时的软件复杂度,存储映射是按Cortex-M4处理器提供的规则预先定义的。在存储器映射表中,一部分地址空间由Arm Cortex-M4的系统外设所占用,且不可更改,其余部分地址空间可由芯片供应商定义使用,如下图所示。
- 什么是寄存器
寄存器是读取速度最快的存储单元,具有特定功能的内存单元,通过操作这些内存单元可以驱动外设工作。寄存器按功能又可分为指令寄存器、地址寄存器和数据寄存器,处理器可以使用相互独立的总线来读取指令和加载/存储数据。 - 寄存器映射
程序存储器,数据存储器,寄存器和I / O端口都在同一个线性的4 GB的地址空间之内。每一个寄存器都对应不同的功能,操作相应的寄存器就可以配置不同的功能。如果我们要控制某个外设工作,那我们可以找到这个单元的起始地址,然后通过c语言指针的方式来访问这些内存单元。但通常我们会给这个特殊的内存单元取一个名字,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射,这个别名就是我们所说的寄存器。 - 寄存器重映射
有时在映射的结果中,地址会不够而造成重复,这里给寄存器再分配一个地址的过程叫做寄存器重映射。 - 总线基地址
片上外设区域分为四条总线,分别为AHB1总线、AHB2总线、APB1总线和APB2总线。AHB总线最高时钟可达168MHZ,APB1总线最高时钟可达42MHZ,APB2总线时钟最高可达84MHZ。根据外设速度的不同,不同的总线挂载着不同的外设。总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址,可以通过参考手册中存储器映射这一节查询,四条总线地址如下表所示。
总线名称 | 总线基地址 | 总线地址范围 |
---|---|---|
APB1 | 0x4000 0000 | 0x4000 0000-0x4000 FFFF |
APB2 | 0x4001 0000 | 0x4001 0000-0x4001 FFFF |
AHB1 | 0x4002 0000 | 0x4002 0000-0x4FFF FFFF |
AHB2 | 0x5000 0000 | 0x5000 0000-0x5FFF FFFF |
- 外设基地址
每个总线上都挂载着很多外设,这些外设也都有自己的地址范围。 - 外设寄存器地址
在外设的地址范围内,分布着该外设的寄存器。以GPIO外设为例,GPIO外设地址范围内有很多个寄存器,每一个都有特定的功能,通过操作对应的寄存器来配置GPIO的功能。每个寄存器都为32位,占4个字节,这里我们以GPIOA端口的寄存器进行介绍。 - 如何操作寄存器
比如我们想让GPIOA端口的16个引脚都置1。我们需要去配置端口输出寄存器GPIOx_ODR(输出数据寄存器),通过查找用户手册283页可以知道这个寄存器的地址偏移量为0x14,GPIOA端口的基地址为0x4002 0000,所以GPIOx_ODR寄存器的地址为0x4002 0000 + 0x14 = 0x4002 0014,那我们就是对这个地址进行操作。
该寄存器高16位保留,仅需配置低16位即可。每一位对应一个引脚输出,即向GPIOx_ODR写入0x0000FFFF即可。
- 通过绝对地址访问内存单元
代码如下
/*GPIOA端口16个引脚全部输出高电平*/
*(unsigned int*)(0x40020014) = 0x0000FFFF;//通过指针向0x40020014这个地址写入数值
- 通过别名访问内存单元
由于直接操作地址很麻烦,可通过预定义将所需用到的地址进行归纳,代码如下
/* GPIOA 端口的16个引脚全部输出高电平 */
#define GPIOA_ODR (unsigned int*)(0x40020014)
*GPIOA_ODR = 0xFFFF;
2. 标准库函数
- 为什么要用库函数
从上一节我们了解到如何去用寄存器驱动外设,但我们也同时了解到STM32的寄存器数量非常多,这么多的寄存器光是定义就需要花费很多的时间,更不用说还要去查找对应的功能,找到对应的地址,然后配置需要的值,这在难度和时间上都是不可取的。为此,库函数就在这种情况下应运而生,库函数能使我们的开发效率大大提高。关于STM32的标准外设库函数,STM32的官方已经给我们开发好了,我们只需要移植到我们的工程使用即可。库函数的使用不需要让我们去了解硬件的机制,只需要根据需要的功能去查找对应的函数,然后调用即可,大大降低了开发要求。 - 标准库函数介绍
库函数就是在寄存器的基础上又封装了一层,使操作起来更简单,最后还是通过寄存器来实现的。 - 寄存器和库函数区别
- 寄存器更能理解原理,更直观,库函数相对来说屏蔽底层,直接面向应用
- 使用库函数较寄存器代码量会增大,库函数会把所有情况都考虑到函数里,有时会造成代码的冗余
- 库函数使用起来相对简单,容易上手,可快速开发应用,大大提高效率
- 寄存器占用内存少,速度快,在资源有限或者要求执行速度的情况下寄存器是一个不错的选择
GPIO外设
每个GPIO具有 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)。
GPIO基本结构
GPIO外设位于AHB1总线。每个端口通过16个引脚引出,由配置寄存器进行配置。
I/O引脚多路复用和映射
微控制器 I/O 引脚通过多路复用器连接到板载外设/模块,该多路复用器一次只允许一个外设的备用功能(AF)连接到 I/O 引脚。这样,共享同一 I/O 引脚的外设之间就不会发生冲突。同时每个 I/O 引脚都有一个多路复用器,具有 16 个备用功能输入(AF0 至 AF15),可通过 GPIOx_AFRL(引脚 0 至 7)和 GPIOx_AFRH(引脚 8 至 15)寄存器进行配置。在更小的封装内实现更多的外设数量。
输入模式
GPIO输入基本结构如下
当I/O端口被编辑为输入模式时,输出驱动断开。
输出模式
GPIO输出基本结构如下
当I/O端口被编辑为输出模式时,输入驱动没有断开,具有采样功能
复用功能配置
GPIO复用基本结构如下
和输出模式类似,只不过增加了复用功能输入和复用功能输出。
LED灯
LED等基础知识
- LED灯结构组成
LED灯,也称发光二极管,是一种能够将电能转化为可见光的固态的半导体器件,它可以直接把电转化为光。LED的内部是一个半导体的晶片,晶片的一端附在一个支架上,一端是负极,另一端连接电源的正极,整个晶片被环氧树脂封装起来。 - LED灯发光原理
半导体晶片由两部分组成,一部分是P型半导体,另一端是N型半导体。这两种半导体连接起来的时候,它们之间就形成了一个P-N结。当电流通过导线作用于这个晶片的时候,电子就会被推向P区,在P区里电子跟空穴复合,然后就会以光子的形式发出能量,这就是LED灯发光的原理。 - LED灯驱动原理
LED灯的驱动比较简单,只需要给将对应的正负极接到单片机的正负极即可驱动。需要注意的是LED灯的颜色不同,对应的电压也不同,同时需要串联分压电阻。板载LED如下图所示
LED灯驱动流程
通过上面的原理图可知LED灯正极接到电源上,负极连接到单片机GPIO口上,这里给PA6和PA7引脚输出低电平即可点亮LED。
寄存器点亮LED
STM32所有的外设资源时钟默认都是关闭的,因此在配置外设之前需要先开启对应的时钟。点亮板载LED需要使用GPIO外设,在stm32f4xx_gpio.c
文件中知,使用GPIO端口,需要有一下几个步骤:
- 开启GPIO时钟
- 配置GPIO模式
- 配置GPIO输出
开启GPIO外设端口时钟
STM32F407的GPIO外设在AHB1总线,需要配置AHB1使能寄存器,AHB1外设时钟寄存器如下图
该寄存器地址偏移量为0x30
,查表得RCC外设基地址为0x4002 3800
,那么RCC_AHB1ENR寄存器地址为:0x4002 3800 + 0x30 = 0x4002 3830
。所使用得引脚是GPIOA端口,第零位Bit 0值为1,为保持其他位不变这里采用一个或运算。
代码为RCC->AHB1ENR |= 0x01;
。
配置GPIO模式
GPIO的模式配置可分为两步
- 通过控制寄存器(GPIOx_MODER)配置为输入功能,输出功能,复用功能还是模拟功能
- 通过 GPIO 上/下拉寄存器(GPIOx_PUPDR)配置GPIO的上下拉模式或者浮空
该寄存器地址偏移量为0x00
,GPIOA端口基地址为0x4002 0000
,则GPIOx_MODER寄存器地址为:0x4002 0000 + 0x00 = 0x4002 0000
,要使用的是第6号和第7号引脚,即选择该寄存器位12、13、14和15,为保持其他位数据不变,需先清空这两位后再写入数值。操作为:GPIOA_MODER &= 0xFFFF 0FFF;
和GPIOA_MODER |= 0x0000 5000;
。
输出模式一般配置为浮空模式,输入模式才需要考虑上拉还是下拉。通过寄存器GPIOx_PUPDR进行配置
该寄存器地址偏移量为0x0C
,GPIOA端口基地址为0x4002 0000
,则GPIOx_MODER寄存器地址为:0x4002 0000 + 0x0C = 0x4002 000C
,要使用的是第6号和第7号引脚,即选择该寄存器位12、13、14和15,为保持其他位数据不变,需先清空这两位后再写入数值。操作为:GPIOx_PUPDR &= 0xFFFF 0FFF;
和GPIOx_PUPDR |= 0x0000 0000;
。
转化为代码如下
GPIOA->MODER |= (0x05 << 2 * 6);//配置为输出模式
GPIOA->PUPDR &= ~(0x05 << 2 * 6);//配置为浮空模式
配置GPIO的输出
配置GPIO的输出同样分为两步,输出模式和端口速度。
- 通过端口输出模式寄存器(GPIOx_OTYPER)配置为推挽模式、开漏模式
- 通过端口输出速度寄存器(GPIOx_OSPEEDR)配置四种速度
寄存器写入数值过程同上。
转化为代码如下
GPIOA->OTYPER |= 0x0000;//配置为推挽输出
GPIOA->OSPEEDR |= (0x0F << 2 * 6);//配置为最高速
配置GPIO输出高低电平
配置GPIO引脚输出高低电平,即板载引脚PA6和PA7输出高低电平,可以通过端口输出数据寄存器GPIOx_ODR进行配置。
转化为代码:GPIOA->ODR |= 0x0C;//输出高电平
总体代码如下
#include "stm32f4xx.h" // Device header
int main()
{
RCC->AHB1ENR |= 0x01;//开启AHB1总线时钟
GPIOA->MODER |= (0x05 << 2 * 6);//配置为输出模式
GPIOA->PUPDR &= ~(0x05 << 2 * 6);//配置为浮空模式
GPIOA->OTYPER &= ~(0x03 << 2 * 3);//配置为推挽模式
GPIOA->OSPEEDR |= (0x0F << 2 * 6);//配置为最高速
// GPIOA->ODR |= (0x03 << 2 * 3);//输出高电平
GPIOA->ODR |= (0x00 << 2 * 3);//输出低电平
}
库函数点亮LED
同样按照上述步骤进行编写代码,这里新建一个LED.c文件便于对外设进行管理。
//LED.c文件
#include "LED.h"
void LED_Init()
{
//初始化PA6和PA7为输出口
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;//LED0和LED1对应IO口
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;//浮空
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIO
GPIO_SetBits(GPIOA,GPIO_Pin_6 | GPIO_Pin_7);//设置高,灯灭
}
void LED1_ON()
{
GPIO_ResetBits(GPIOA, GPIO_Pin_6);
}
void LED2_ON()
{
GPIO_ResetBits(GPIOA, GPIO_Pin_7);
}
void LED1_OFF()
{
GPIO_SetBits(GPIOA, GPIO_Pin_6);
}
void LED2_OFF()
{
GPIO_SetBits(GPIOA, GPIO_Pin_7);
}
所对应的头文件LED.h代码如下
//LED.c文件
#ifndef __LED_H
#define __LED_H
#include "stm32f4xx.h" // Device header
void LED_Init(void);//初始化
void LED1_ON();
void LED1_OFF();
void LED2_ON();
void LED2_OFF();
#endif
主函数main代码如下
#include "LED.h"
int main()
{
//库函数点亮LED
LED_Init();
LED1_OFF();
LED2_ON();
while(1)
{
}
}