一:stm32 GPIO介绍
1. GPIO概念
GPIO(general purpose intput output) 是通用输入输出端口的简称, 可以通过软件来控制其输入和输出。 STM32 芯片的 GPIO 引脚与外部设备连接起来,从而实现与外部通讯、 控制以及数据采集的功能。 不过 GPIO 最简单的应用还属点亮 LED 灯了, 只需通过软件控制 GPIO 输出高低电平即可。 当然 GPIO 还可以作为输入控制, 比如在引脚上接入一个按键, 通过电平的高低判断按键是否按下。
那么是不是所有引脚都是 GPIO 呢? 当然不是, STM32 引脚可以分为这么几大类:
(1) 电源引脚: 引脚图中的 VDD、 VSS、 VREF+、 VREF-、 VSSA、 VDDA 等都属于电源引脚。
(2) 晶振引脚: 引脚图中的 PC14、 PC15 和 OSC_IN、 OSC_OUT 都属于晶振引脚, 不过它们还可以作为普通引脚使用。
(3) 复位引脚: 引脚图中的 NRST 属于复位引脚, 不做其他功能使用。
(4) 下载引脚: 引脚图中的 PA13、 PA14、 PA15、 PB3 和 PB4 属于 JTAG 或SW 下载引脚。 不过它们还可以作为普通引脚或者特殊功能使用, 具体的功能可以查看芯片数据手册, 里面都会有附加功能说明。 当然, STM32 的串口功能引脚也是可以作为下载引脚使用。
(5) BOOT 引脚: 引脚图中的 BOOT0 和 PB2(BOOT1)属于 BOOT 引脚, PB2 还可以作为普通管脚使用。 在 STM32 启动中会有模式选择, 其中就是依靠着 BOOT0和 BOOT1 的电平来决定。
(6) GPIO 引脚: 引脚图中的 PA、 PB、 PC、 PD 等均属于 GPIO 引脚。 从引脚图可以看出, GPIO 占用了 STM32 芯片大部分的引脚。 并且每一个端口都有 16 个引脚, 比如 PA 端口, 它有 PA0-PA15。 其他的 PB、 PC 等端口是一样的。
2. GPIO结构框图
前面我们讲解了 STM32 GPIO 的基本概念及引脚分类。 现在我们看下 STM32GPIO 内部的结构是怎样的。
可以看出 GPIO 内部结构还是比较复杂的, 只要将这张 GPIO结构图理解好, 那么关于 GPIO 的各种应用模式将非常清楚。 图中最右端 I/O 端口就是 STM32 芯片的引脚, 其它部分都在 STM32 芯片内部。 上图中我们将每部分都用红线圈起来标号了, 按照顺序我们逐一讲解。
(1) 保护二极管
引脚内部加上这两个保护二级管可以防止引脚外部过高或过低的电压输入,当引脚电压高于 VDD_FT 或 VDD 时, 上方的二极管导通吸收这个高电压, 当引脚电压低于 VSS 时, 下方的二极管导通, 防止不正常电压引入芯片导致芯片烧毁。尽管 STM32 芯片内部有这样的保护, 但并不意味着 STM32 的引脚就无所不能, 如果直接将引脚连接大功率器件, 比如电机, 那么要么电机不转, 要么烧坏芯片。如果要驱动一些大功率器件, 必须要加大功率及隔离电路驱动。 也可以说 STM32引脚是用来做控制, 而不是做驱动使用的。
(2) 上下拉电阻
从图中可以看到, 上拉和下拉电阻上都有一个开关, 通过配置上下拉电阻开关, 可以控制引脚的默认状态电平。 当开启上拉时引脚默认电压为高电平, 开启下拉时, 引脚默认电压为低电平, 这样就可以消除引脚不定状态的影响。 当然也可以将上拉和下拉的开关都关断, 这种状态我们称为浮空模式, 一旦配置成这个模式, 引脚的电压是不确定的, 如果用万用表测量此模式下管脚电压时会发现只有 1 点几伏, 而且还不时改变, 所以一般情况下我们都会给引脚设置成上拉或者下拉模式, 使它有一个默认状态。 STM32 上下拉及浮空模式的配置是通过GPIOx_CRL 和 GPIOx_CRH 寄存器控制的。STM32 内部的上拉其实是一个弱上拉, 也就是说通过此上拉电阻输出的电流很小, 如果想要输出一个大电流, 那么就需要外接上拉电阻了。
(3) P-MOS 和 N-MOS 管
GPIO 引脚经过两个保护二极管后就分成两路, 上面一路是“输入模式” ,下面一路是“输出模式” 。 我们先讲输出模式, 线路经过一个由 P-MOS 和 N-MOS管组成的单元电路, 这让 GPIO 引脚具有了推挽和开漏两种输出模式。所谓推挽输出模式, 是根据 P-MOS 和 N-MOS 管的工作方式命名的。 在该结构单元输入一个高电平时, P-MOS 管导通, N-MOS 管截止(可以将 P-MOS 当作 NPN三极管, N-MOS 当作 PNP 三极管来看就非常清楚) , 对外输出高电平(3.3V) 。在该单元输入一个低电平时, P-MOS 管截止, N-MOS 管导通, 对外输出低电平(0V)。如果当切换输入高低电平时, 两个 MOS 管将轮流导通, 一个负责灌电流(电流输出到负载) , 一个负责拉电流(负载电流流向芯片) , 使其负载能力和开关速度都比普通的方式有很大的提高。推挽输出模式的等效电路。:
在开漏输出模式时, 不论输入是高电平还是低电平, P-MOS 管总处于关闭状态。 当给这个单元电路输入低电平时, N-MOS 管导通, 输出即为低电平。 当输入高电平时, N-MOS 管截止, 这个时候引脚状态既不是高电平, 又不是低电平, 我们称之为高阻态。 如果想让引脚输出高电平, 那么引脚必须外接一个上拉电阻,由上拉电阻提供高电平。 开漏输出模式等效电路图:
在开漏输出模式中还有一个特点, 引脚具有“线与” 关系。 就是说如果有很多个开漏输出模式的引脚接在一起, 只要有一个引脚为低电平, 其他所有管脚都为低, 即把所有引脚连接在一起的这条总线拉低了。 只有当所有引脚输出高阻态时这条总线的电平才由上拉电阻的 VDD 决定。 如果 VDD 连接的是 3.3V, 那么引脚输出的就是 3.3V, 如果 VDD 连接的是 5V, 那么引脚输出的就是 5V。 因此如果想要让 STM32 管脚输出 5V, 可以选择开漏输出模式, 然后在外接上拉电阻的电源 VDD 选择 5V 即可, 前提是这个 STM32 引脚是容忍 5V 的。 开漏输出模式一般应用在 I2C、 SMBUS 通讯等需要“线与” 功能的总线电路中。 还可以用在电平不匹配的场合中, 就如上面说的输出 5V 一样。
推挽输出模式一般应用在输出电平为 0-3.3V 而且需要高速切换开关状态的场合。 除了必须要用开漏输出模式的场合, 我们一般选择推挽输出模式。 要配置引脚是开漏输出还是推挽输出模式可以使用 GPIOx_CRL 和 GPIOx_CRH 寄存器。
(4) 输出数据寄存器
前面提到的双 MOS 管结构电路的输入信号, 是由 GPIO“输出数据寄存器GPIOx_ODR”提供的, 因此我们通过修改输出数据寄存器的值就可以修改 GPIO 引脚的输出电平。 而“置位/复位寄存器 GPIOx_BSRR” 可以通过修改输出数据寄存器的值从而影响电路的输出。
(5) 复用功能输出
由于 STM32 的 GPIO 引脚具有第二功能, 因此当使用复用功能的时候, 也就是通过其他外设复用功能输出信号与 GPIO 数据寄存器一起连接到双 MOS 管电路的输入, 其中梯形结构是用来选择使用复用功能还是普通 IO 口功能。 例如我们使用 USART 串口通讯时, 需要用到某个 GPIO 引脚作为通讯发送引脚, 这个时候就可以把该 GPIO 引脚配置成 USART 串口复用功能, 由串口外设控制该引脚,发送数据。
(6) 输入数据寄存器
输入数据寄存器是由 IO 口经过上下拉电阻、 施密特触发器引入。 当信号经过触发器, 模拟信号将变为数字信号 0 或 1, 然后存储在输入数据寄存器中, 通过读取输入数据寄存器 GPIOx_IDR 就可以知道 IO 口的电平状态。
(7) 复用功能输入
此模式与前面讲解的复用功能输出类似。 在复用功能输入模式时, GPIO 引脚的信号传输到 STM32 其他片上外设, 由该外设读取引脚的状态。 同样, 如我们使用 USART 串口通讯时, 需要用到某个 GPIO 引脚作为通讯接收引脚, 这个时候就可以把该 GPIO 引脚配置成 USART 串口复用功能, 使 USART 可以通过该通讯引脚的接收远端数据。
(8) 模拟输入输出
当 GPIO 引脚用于 ADC 采集电压的输入通道时, 用作“模拟输入” 功能,此时信号是不经过施密特触发器的, 因为经过施密特触发器后信号只有 0、 1 两种状态, ADC 外设要采集到原始的模拟信号, 信号源输入必须在施密特触发器之前。 类似地, 当 GPIO 引脚用于 DAC 作为模拟电压输出通道时, 此时作为“模拟输出” 功能, DAC 的模拟信号输出就不经过双 MOS 管结构了, 模拟信号直接通过管脚输出。
3. GPIO工作模式
通过 GPIO 内部的结构关系, 决定了 GPIO 可以配置成以下几种模式。
(1) 输入模式(模拟、 上拉、 下拉、 浮空)
在输入模式时, 施密特触发器打开, 输出被禁止。 可通过输入数据寄存器GPIOx_IDR 读取 I/O 状态。 输入模式可以配置为模拟、 上拉、 下拉以及浮空模式。 上拉和下拉输入很好理解, 默认的电平由上拉或者下拉决定。 浮空输入的电平是不确定的, 完全由外部的输入决定, 一般接按键的时候可以使用这个模式。模拟输入则用于 ADC 采集。
(2) 输出模式(推挽/开漏)
在输出模式中, 推挽模式时双 MOS 管以推挽方式工作, 输出数据寄存器GPIOx_ODR 可控制 I/O 输出高低电平。 开漏模式时, 只有 N-MOS 管工作, 输出数 据 寄 存 器 可 控 制 I/O 输 出 高 阻 态 或 低 电 平 。 输 出 速 度 可 配 置 , 有2MHz\25MHz\50MHz 的选项。 此处的输出速度即 I/O 支持的高低电平状态最高切换频率, 支持的频率越高, 功耗越大, 如果功耗要求不严格, 把速度设置成最大即可。 在输出模式时, 施密特触发器是打开的, 即输入可用, 通过输入数据寄存器 GPIOx_IDR 可读取 I/O 的实际状态。
(3) 复用功能(推挽/开漏)
复用功能模式中, 输出使能, 输出速度可配置, 可工作在开漏及推挽模式,但是输出信号源于其它外设, 输出数据寄存器 GPIOx_ODR 无效; 输入可用, 通过输入数据寄存器可获取 I/O 实际状态, 但一般直接用外设的寄存器来获取该数据信号。
(4) 模拟输入输出(上下拉无影响)
模拟输入输出模式中, 双 MOS 管结构被关闭, 施密特触发器停用, 上/下拉也被禁止。 其它外设通 过模拟通道进行输入输出。 通过对 GPIO 寄存器写入不同的参数, 就可以改变 GPIO 的应用模式。 在 GPIO 外设中, 通过设置“端口配置寄存器 GPIOx_CRL 和 GPIOx_CRH” 可配置 GPIO 的工作模式和输出速度。 CRH 控制端口的高八位, CRL 控制端口的低八位。
二 :硬件设计
开发板硬件电路:
相同网络标号表示它们是连接在一起的, 因此 D1-D8 发光二极管阴极是连接在 STM32 的 PC0-PC7 管脚上。 如果要使 D1 指示灯亮, 只需要控制 PC0 管脚输出低电平, 如果要使 D1 指示灯灭, 只需控制 PC0 输出高电平。 对于其他的 LED 控制方法一样。 如果你们使用的是其他板子, 连接 LED 的管脚和极性不一样, 那么只需要在程序中修改对应的 GPIO 管脚和输出电平状态即可, 原理是一样的。
三:软件设计
在“寄存器模板创建” 章节中已经创建了一个寄存器工程模板, 这里面我们直接复制这个模板到本章的实验中, 在此模板基础上进行程序编写。 前面寄存器模板创建的时候我们使用到了 3 个文件, 一个是 startup_stm32f10x_hd.s 启动文件, 一个是 main.c 文件, 还有一个是 stm32f10x.h 文件。main.c 和 stm32f10x.h文件内没有内容, 只有 startup_stm32f10x_hd.s 文件有, 我们就来了解下这个启动文件内部的一些东西。
启动文件里边是使用汇编语言写好了基本程序, 当 STM32 芯片上电启动的时候, 首先会执行这里的汇编程序, 从而建立起 C 语言的运行环境, 所以我们把这个文件称为启动文件。 该文件使用的汇编指令是 Cortex-M3 内核支持的指令, 可参考《 Cortex-M3 权威指南中文》 内指令集章节。
startup_stm32f10x_hd.s 文件是由 ST 官方提供的, 一般有需要也是在官方的基础上修改, 不会自己完全重写。 该文件可以从 KEIL5 安装目录找到, 也可以从 ST 库里面找到, 找到该文件后把启动文件添加到工程里面即可。 不同型号的芯片以及不同编译环境下使用的汇编文件是不一样的, 但功能相同。对于启动文件这部分我们主要总结它的功能, 不详细讲解里面的代码, 其功能如下:
初始化堆栈指针 SP;
初始化程序计数器指针 PC;
设置堆、 栈的大小;
设置中断向量表的入口地址;
配置外部 SRAM 作为数据存储器(这个由用户配置, 一般的开发板可没有外部 SRAM) ;
调用 SystemInit() 函数配置 STM32 的系统时钟。
设置 C 库的分支入口“ __main” (最终用来调用 main 函数) ;
先去除繁枝细节, 挑重点的讲, 主要理解最后两点, 在启动文件中有一段复位后立即执行的程序, 代码如图 所示。 在实际工程中阅读时, 可使用编辑器的搜索(Ctrl+F)功能查找这段代码在文件中的位置。
开头 148 行的是程序注释, 在汇编里面注释用的是“ ;” , 相当于 C 语言的“ //” 注释符
第 149 行是定义了一个子程序: Reset_Handler。 PROC 是子程序定义伪指令。 这里就相当于 C 语言里定义了一个函数, 函数名为 Reset_Handler。
第 150 行 EXPORT 表示 Reset_Handler 这个子程序可供其他模块调用。 相当于 C 语言的函数声明。 关键字[WEAK] 表示弱定义, 如果编译器发现在别处定义了同名的函数, 则在链接时用别处的地址进行链接, 如果其它地方没有定义,编译器也不报错, 以此处地址进行链接, 如果不理解 WEAK, 那就忽略它好了。第 151 行和第 152 行 IMPORT 说明 __main 和 SystemInit 这两个标号在其他文件, 在链接的时候需要到其他文件去寻找。 相当于 C 语言中, 从其它文件引入函数声明。 以便下面对外部函数进行调用。
SystemInit 需要由我们自己实现, 即我们要编写一个具有该名称的函数,用来初始化 STM32 芯片的时钟, 一般包括初始化 AHB、 APB 等各总线的时钟,需要经过一系列的配置 STM32 才能达到稳定运行的状态。 __main 其实不是我们定义的(不要与 C 语言中的 main 函数混淆), 当编译器编译时, 只要遇到这个标号就会定义这个函数, 该函数的主要功能是: 负责初始化栈、 堆, 配置系统环境, 准备好 C 语言并在最后跳转到用户自定义的 main 函数, 从此来到 C 的世界。
第 153 行把 SystemInit 的地址加载到寄存器 R0。
第 154 行程序跳转到 R0 中的地址执行程序, 即执行 SystemInit 函数的内容。
第 155 行把__main 的地址加载到寄存器 R0。
第 156 行程序跳转到 R0 中的地址执行程序, 即执行__main 函数, 执行完毕之后即可进入 main 函数。
第 157 行表示子程序的结束。
总之, 看完这段代码后, 了解到如下内容即可: 我们需要在外部定义一个SystemInit 函数设置 STM32 的时钟; STM32 上电后, 会执行 SystemInit 函数, 最后执行我们 C 语言中的 main 函数。
下面就开始使用寄存器来操作 STM32 使 PC0 输出一个低电平。 要操作 STM32寄存器, 我们就需要使用 C 语言对其封装, 这部分程序我们都放在 stm32f10x.h中。 具体代码如下:
#define PERIPH_BASE ((unsigned int)0x40000000)
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOC_CRL *(unsigned int*)(GPIOC_BASE+0x00)
#define GPIOC_CRH *(unsigned int*)(GPIOC_BASE+0x04)
#define GPIOC_IDR *(unsigned int*)(GPIOC_BASE+0x08)
#define GPIOC_ODR *(unsigned int*)(GPIOC_BASE+0x0C)
#define GPIOC_BSRR *(unsigned int*)(GPIOC_BASE+0x10)
#define GPIOC_BRR *(unsigned int*)(GPIOC_BASE+0x14)
#define GPIOC_LCKR *(unsigned int*)(GPIOC_BASE+0x18)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)
#define RCC_APB2ENR *(unsigned int*)(RCC_BASE+0x18)
要控制 PC0 输出低电平, 需知道 GPIO 这个外设它是挂接在哪个总线上的,通过 Block2 外设基地址及 APB2 总线的偏移地址就可以得到 APB2 外设的基地址。GPIO 就是挂接在 APB2 总线上的, 根据 GPIOC 的偏移地址就可以得到 GPIOC 外设的基地址, GPIOC 外设内部含有很多个寄存器, 比如 GPIOC_CRL、 GPIOC_CRH 端口配置寄存器、 GPIOC_BSRR 置位复位寄存器等, 通过他们各自的偏移地址就可以获取对应的寄存器地址, 然后要操作地址里面的内容就需要使用到指针, 将其强制转换为 unsigned int*指针类型, 然后在通过一个*指针来操作该地址里面的内容。 在 STM32 中凡是使用到外设功能, 都要使能对应的外设时钟, 否则即使配置好端口初始化也无法正常使用。 因此还需要知道时钟 RCC 外设的基地址, 通过《STM32F103ZET6 数据手册》 “4 Memory mapping” 的“存储器映射” 章节可以知道 RCC 时钟外设是挂接在 AHB 总线上, 根据其偏移值可以得到 RCC 时钟外设的基地址, 然后可通过《STM32F1xx 中文参考手册》 的“6 小容量、 中容量和大容量产品的复位和时钟控制(RCC)” 的“ 6.3.7 APB2 外设时钟使能寄存器(RCC_APB2ENR)” 可找到对应的端口 RCC 使能寄存器, 只要将 GPIOC 端口时钟使能即可。
#include "stm32f10x.h" //1
void SystemInit() //2
{
}
int main()
{
RCC_APB2ENR |= 1<<4; //3
GPIOC_CRL &= ~( 0x0F<< (4*0));
GPIOC_CRL |= (3<<4*0); //4
GPIOC_BSRR=(1<<(16+0)); //5
while(1)
{
}
在上述代码后面我们标注了序号, 下面按照序号顺序介绍:
(1) 包含 stm32f10x.h 头文件, 在这个头文件中我们定义的都是寄存器,因此如果要在其他文件中使用这些寄存器就需要把这个头文件包含进来, 否则编译就会报错。
(2) SystemInit 函数, 在前面讲解启动文件时已经说明, 程序运行的时候先进入这个函数进行 STM32 的初始化, 如果不写这个函数编译器就会报错。 这里我们编写这个函数, 里面并不对其操作。
(3) 开启 GPIOC 时钟。 要使 PC0 正常工作输出一个低电平, 必须要打开它的时钟。 RCC_APB2ENR 寄存器是在 stm32f10x.h 头文件中定义好的, 只要查下《STM32F1xx 中文参考手册》 RCC 时钟使能寄存器内容就可以知道此寄存器的第4 位是控制 GPIOC 外设的时钟使能位, 只有该位为 1 时才使能, 如果为 0 即关闭GPIOC 时钟。 所以要让 1 左移 4 位。
(4) 配置 GPIOC 为通用推完输出模式。 STM32 的 GPIO 模式有很多, 可根据CRx 寄存器设置, CRL 对应 GPIO 的低 8 位, CRH 对应 GPIO 的高 8 位。 如果不是特殊需求, 一般输出采用推完输出模式。 我们要让 PC0 管脚输出一个低电平, 故使用推完输出模式。 只要查下《STM32F1xx 中文参考手册》 GPIO 配置寄存器内容就可以知道此寄存器内每 4 位控制一个管脚。
(5) 使 PC0 输出低电平。 GPIOC_BSRR 为置位复位寄存器, 只要查下《STM32F1xx 中文参考手册》 GPIO 置位复位寄存器内容就可以知道, 其高 16 位用于复位, 如果当高 16 位某位为 1, 表示那一位管脚输出低电平, 为 0 不影响其输出电平。 如果当低 16 位的某位为 1, 表示那一位管脚输出高电平, 为 0 不影响其输出电平。 所以要让 1 左移 16+0 位。
程序到这里就算完成, 如果我们要让 LED 闪烁的话, 其实也很简单, 只需要让 PC0 管脚循环输出一个高低电平, 为了能使我们肉眼看得清楚, 在输出高或低电平后让它延时一会。 因此还需要编写一个延时函数, 在这里我们先简单写一个延时函数.
typedef unsigned int u32; //类型重定义 unsigned int -- u32
void SystemInit()
{
void delay(u32 i)
{
while(i--);
}
{
RCC_APB2ENR |= 1<<4;
GPIOC_CRL &= ~( 0x0F<< (4*0));
GPIOC_CRL |= (3<<4*0);
GPIOC_BSRR=(1<<(16+0));
while(1)
{
GPIOC_BSRR=(1<<(16+0));
delay(0xFFFFF);
GPIOC_BSRR=(1<<(0));
delay(0xFFFFF);
}
}
这个程序就比前面点亮 LED 稍复杂一些, 其实也是一样的, 只不过这里加了一个延时函数和输出高电平部分。 我们会输出低电平, 那么输出高电平就是一样的。 GPIOC_BSRR 寄存器的低 16 位即控制输出高电平的。 延时函数 delay 内部通过一个 while 循环占用 CPU 起到一个延时功能, 但是这个延时并不准确, 在后面的学习中我们会给大家介绍怎么来精确延时。 这里就不计较到底延时多长, 大家只要能够看到 LED 闪烁效果即可。 在 delay 函数中有一个形参, 其类型是 u32,这个类型我们需要通过 typedef 进行类型声明定义, 将 unsigned int 定义为 u32类型, 这里的 32 表示是 4 字节。 所以形参 i 值最大是 0XFFFFFFFF。 如果大家觉得 LED 闪烁快了或者慢了, 可以通过修改这个形参值调节时间。
到这里整个程序就编写完成, 我们编译一下 :
可以看到没有错误, 也没有警告。 从编译信息可以看出, 我们的代码占用FLASH 大小为: 540 字节(220+320), 所用的 SRAM 大小为: 1024 个字节(1024+0)。这里我们解释一下, 编译结果里面的几个数据的意义:Code: 表示程序所占用 FLASH 的大小。RO-data: 即 Read Only-data, 表示程序定义的常量, 存储在 FLASH 内。RW-data: 即 Read Write-data, 表示已被初始化的变量, 存储在 SRAM 内。ZI-data: 即 Zero Init-data, 表示未被初始化的变量, 存储在 SRAM 内。有了这个就可以知道你当前使用的 flash 和 sram 大小了, 所以, 一定要注意的是程序的大小不是.hex 文件的大小, 而是编译后的 Code 和 RO-data 之和。