第3章
CHAPTER 3


基本I/O口控制






3.1本章导读
从基本I/O操作开始学习,读者可以了解到以下重要信息: 
(1) 通过流水灯例子进一步强化通过MDK(Microcontroller Development Kit,微控制器开发工具)建立工程的方法,配置的详细步骤。
(2) 掌握直接寄存器控制I/O的步骤和方法,认识时钟树的概念,对STM32的时钟系统有初步的了解。
(3) 认识I/O口,掌握I/O口的8种工作模式和各种模式对应应用的场合,以及如何采用寄存器配置各种模式。
(4) 了解库函数,通过库函数操作流水灯,总结寄存器方法和库函数方法的区别和共同点。
(5) 通过两个典型案例——数码管和简单按键操作实例,进一步掌握库函数编程的方法和I/O口控制的步骤。
3.2新建工程进阶

单击桌面Keil μVision5图标,启动软件。如果是第一次使用,会打开一个自带的工程文件,可以通过菜单栏Project→Close Project命令把它关掉。
新建的工程文件保存在一个文件夹中。首先新建一个名为“流水灯”的文件夹,把第2章建立好的工程复制到该文件夹下,可以参考书中附带的例子,从例子中添加这些代码,后续会陆续解释其含义。“流水灯”工程的子文件夹如图31所示。


图31“流水灯”工程的子文件夹



Libraries文件夹用来存放ST库里面最核心的文件,其中包含两个子文件夹: 
STM32F10x_StdPeriph_Driver和CMSIS。
 STM32F10x_StdPeriph_Driver文件夹用来存放STM32库里面芯片上的所有驱动。
inc和src两个文件夹也是直接从ST的库里面复制过来的。
inc里面是ST片上资源的驱动的头文件,如要用到某个资源,则必须把相应的头文件包含进来。 
src里面是ST片上资源的驱动文件,这些驱动文件涉及了大量的C语言的知识,是学习库的重点。
 CMSIS文件夹用来存放库自带的启动文件和一些M3系列通用的文件。CMSIS里面存放的文件适用于任何M3内核的单片机。CMSIS的全称为Cortex Microcontroller Software Interface Standard,是ARM Cortex微控制器软件接口标准,是ARM公司为芯片厂商提供的一套通用的且独立于芯片厂商的处理器软件接口。
LIST文件夹用来保存编译后生成的链接文件。
OBJ文件夹用来存放软件编译后输出的文件。
USER文件夹用来存放用户写的驱动文件,里面的readme.txt文件可以作为说明文档。


用户开始使用该软件时,建立工程相对困难,可以直接将demo.uvproj改为“流水灯.uvproj”。打开后重新编译,可以正常使用,这样就避免了工程名称都是demo的情况,便于管理。




打开工程后,可将Target 1工程名改为“流水灯”,这样更方便管理和阅读,如图32所示,以后类似工程的建立都可采用此方法,不用反复建立工程。


图32修改工程




3.3MDK工程配置
MDK工程配置步骤如下。
(1)  单击工具栏中的魔术棒按钮,弹出配置菜单,可以看到配置菜单关于Device、Target、Output、Listing、User、C/C++、Asm、Linker、Debug和Utilities选项的设置。
(2)  Device在新建工程时已经选定了器件,单击Target选项卡,勾选微库,这样是为了后面的串口例程可以使用printf函数,如图33所示。


图33Target选项卡


(3)  单击Output选项卡,再单击Select Folder for Objects…按钮,设置编译后输出文件保存的位置。同时勾选Debug Information、Create HEX File和Browse Information复选框,如图34所示。


图34Output选项卡


(4)  在Listing选项卡中,单击Select Folder Listings…按钮,定位到模板中的Listing文件夹,如图35所示。


图35Listing选项卡


(5)  在C/C++选项卡上需要设置的比较多。
① 在Define里输入添加STM32F10X_HD,USE_STDPERIPH_DRIVER两个宏。添加USE_STDPERIPH_DRIVER是为了屏蔽编译器的默认搜索路径,转而使用添加到工程中的ST的库,添加STM32F10X_HD是因为用的芯片是大容量的,添加了STM32F10X_HD宏之后,库文件为大容量定义的寄存器就可以用了。芯片是小或中容量时,宏要换成STM32F10X_LD或者STM32F10X_MD。
② 在Include Paths栏添加库文件的搜索路径,就可以屏蔽掉默认的搜索路径。
③ 当编译器在指定的路径下搜索不到时,还是会回到标准目录去搜索,就像有些ANSIC C的库文件,例如stdin.h、stdio.h。
库文件路径修改成功之后,如图36所示。


图36C/C++选项卡


(6) 单击Debug选项卡,选择Use Simulator,软件仿真设置完成,如图37所示。


图37Debug选项卡


(7)  单击菜单栏中的编译按钮,对该工程进行编译,弹出编译信息,编译成功。

Build target '流水灯'

compiling main.c...

compiling stm32f10x_it.c...

compiling core_cm3.c...

compiling system_stm32f10x.c...

assembling startup_stm32f10x_hd.s...

compiling misc.c...

compiling stm32f10x_gpio.c...

compiling stm32f10x_rcc.c...

linking...

Program Size: Code=484 RO-data=320 RW-data=0 ZI-data=1024  

FromELF: creating hex file...

".\OBJ\demo.axf" - 0 Error(s), 0 Warning(s).

3.4寄存器操作
STM32编程主要有两种方法: 寄存器法和库函数法。寄存器法更接近单片机内部寄存器直接控制单片机,库函数法直接调用ST公司提供的标准库控制单片机,两种方法各有千秋。以流水灯设计开始,其中发光二极管接到STM32的GPIOB口的8~15号引脚,如图38所示。


图38流水灯电路



在main.c文件里输入以下代码,实现流水灯的闪烁,有单片机编程经验的人很容易看懂这段代码,主要涉及几个寄存器: RCC>APB2ENR,GPIOB>CRH和GPIOB>ODR。STM32的普通I/O端口的使用配置过程大致是: 
① 先开启对应I/O端口时钟(RCC>APB2ENR); 
② 配置I/O端口(GPIOB>CRH); 
③ 给I/O端口赋值(GPIOB>ODR)。
这3步完成一个I/O端口的最基本操作,代码如下(后面会详述)。

1.#include "stm32f10x.h"

2.void Delay(IO u32 nCount);

3.int main(void)

4.{

5.RCC->APB2ENR |= (1<<3);

6.GPIOB->CRH = 0X22222222;

7.while (1)

8.{

9.GPIOB->ODR=0X0000;

10.Delay(0x0FFFEF);

11.GPIOB->ODR=0XFFFF;

12.Delay(0x0FFFEF);

13.}

14.}

15.void Delay(IO u32 nCount)	 

16.{

17.for(; nCount != 0; nCount--);

18.}

3.5时钟配置
STM32的时钟系统功能完善,但是十分复杂,目的是降低功耗,普通的微处理器一般简单配置好时钟,其他的寄存器就可以使用,但是STM32针对不同的功能,要相应地设置其时钟。
3.5.1时钟树
在使用51单片机时,时钟速度取决于外部晶振或内部RC振荡电路的频率,是不可以改变的。而ARM的出现打破了这个传统的法则,可以通过软件随意改变时钟速度。这让设计更加灵活,但也给设计增加了复杂性。在使用某一功能前,要先对其时钟进行初始化。图39是它的时钟树,不同的外设对应不同的时钟,在STM32中有5个时钟源,分别为HSI、HSE、LSI、LSE、PLL。PLL是由锁相环电路倍频得到PLL时钟。


图39时钟树


(1)  HSI是高速内部时钟,RC振荡器,频率为8MHz。
(2)  HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4~16MHz。
(3)  LSI是低速内部时钟,RC振荡器,频率为40kHz。
(4)  LSE是低速外部时钟,接频率为32.768kHz的石英晶体。
(5)  PLL为锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为2~16倍,但是其输出频率最高不得超过72MHz。
其中,40kHz的LSI供独立看门狗IWDG使用,另外它还可以选择为实时时钟RTC的时钟源。另外,实时时钟RTC的时钟源还可以选择LSE,或者是HSE的128分频。RTC的时钟源通过RTCSEL[1:0]来选择。
STM32中有一个全速功能的USB模块,其串行接口引擎需要一个频率为48MHz的时钟源。该时钟源只能从PLL输出端获取,可以选择为1.5分频或者1分频。也就是,当需要使用USB模块时,PLL必须使能,并且时钟频率配置为48MHz或72MHz。
另外,STM32还可以选择一个时钟信号输出到MCO引脚(PA8)上,可以选择为PLL输出的2分频、HSI、HSE或者系统时钟。

3.5.2时钟源
系统时钟SYSCLK是供STM32中绝大部分部件工作的时钟源。系统时钟可选择为PLL输出、HSI或者HSE。系统时钟最大频率为72MHz,通过AHB分频器分频后送给各模块使用,AHB分频器可选择1、2、4、8、16、64、128、256、512分频。其中,AHB分频器输出的时钟送给5大模块使用。
(1)  送给AHB总线、内核、内存和DMA使用的HCLK时钟。
(2)  通过8分频后送给Cortex的系统定时器时钟。
(3)  直接送给Cortex的空闲运行时钟FCLK。
(4)  送给APB1分频器。APB1分频器可选择1、2、4、8、16分频,其输出一路供APB1外设使用(PCLK1,最大频率36MHz),另一路送给定时器2、3、4倍频器使用。该倍频器可选择1或者2倍频,时钟输出供定时器2、3、4使用。
(5)  送给APB2分频器。APB2分频器可选择1、2、4、8、16分频,其输出一路供APB2外设使用(PCLK2,最大频率72MHz),另一路送给定时器1倍频器使用。该倍频器可选择1或者2倍频,时钟输出供定时器1使用。另外,APB2分频器还有一路输出供ADC分频器使用,分频后送给ADC模块使用。ADC分频器可选择为2、4、6、8分频。
在以上的时钟输出中,有很多是带使能控制的,例如AHB总线时钟、内核时钟、各种APB1外设、APB2外设等。当需要使用某模块时,一定要先使能对应的时钟。
需要注意定时器的倍频器,当APB的分频为1时,它的倍频值为1; 否则它的倍频值就为2。
连接在APB1(低速外设)上的设备有电源接口、备份接口、CAN、USB、I2C1、I2C2、UART2、UART3、SPI2、窗口看门狗、Timer2、Timer3、Timer4。注意USB模块虽然需要一个单独的48MHz时钟信号,但它不是供USB模块工作的时钟,只是提供给串行接口引擎(SIE)使用的时钟。USB模块工作的时钟应该是由APB1提供的。
连接在APB2(高速外设)上的设备有UART1、SPI1、Timer1、ADC1、ADC2、所有普通I/O端口(PA~PE)、第二功能I/O端口。
通过对时钟树的简单了解,知道了普通I/O端口连接在APB2设备上,需要初始化APB2的时钟,即时钟控制(RCC)的APB2的对应使能寄存器。
3.5.3APB2外设时钟使能寄存器(RCC_APB2ENR)
外设通常无访问等待周期。但在APB2总线上的外设被访问时,将插入等待状态直到APB2的外设访问结束。它的寄存器格式如图310所示。各位对应含义如表31所示。


图310APB2外设时钟使能寄存器格式




表31使能寄存器位置功能表



位描述

位31:15
保留,始终读为0
位14 

USART1EN
USART1时钟使能,由软件置“1”或清“0”

0: USART1时钟关闭

1: USART1时钟开启
位13
保留,始终读为0
位12

SPI1EN
SPI1时钟使能,由软件置“1”或清“0”

0: SPI1时钟关闭

1: SPI1时钟开启
位11

TIM1EN
TIM1定时器时钟使能,由软件置“1”或清“0”

0: TIM1定时器时钟关闭

1: TIM1定时器时钟开启
位10

ADC2EN
ADC2接口时钟使能,由软件置“1”或清“0”

0: ADC2接口时钟关闭

1: ADC2接口时钟开启
位9

ADC1EN
ADC1接口时钟使能,由软件置“1”或清“0”

0: ADC1接口时钟关闭

1: ADC1接口时钟开启
位8:7
保留,始终读为0
位6

IOPEEN
I/O端口E时钟使能,由软件置“1”或清“0”

0: I/O端口E时钟关闭

1: I/O端口E时钟开启
位5

IOPDEN
I/O端口D时钟使能,由软件置“1”或清“0”

0: I/O端口D时钟关闭

1: I/O端口D时钟开启


续表


位描述

位4

IOPCEN
I/O端口C时钟使能,由软件置1或清0

0: I/O端口C时钟关闭

1: I/O端口C时钟开启
位3 IOPBEN
I/O端口B时钟使能,由软件置1或清0

0: I/O端口B时钟关闭

1: I/O端口B时钟开启
位2 IOPAEN
I/O端口A时钟使能,由软件置1或清0

0: I/O端口A时钟关闭

1: I/O端口A时钟开启
位1
保留,始终读为0
位0

AFIOEN
辅助功能I/O时钟使能,由软件置1或清0

0: 辅助功能I/O时钟关闭

1: 辅助功能I/O时钟开启

例如,开启PB口时钟的寄存器操作为

RCC->APB2ENR |= (1<<3); //开启PB口的时钟

3.6I/O端口配置
I/O在使用之前需要进行配置,通过PB的使用,说明它的一般配置过程。
3.6.1I/O基本情况
每个GPIO端口有: 
(1) 两个32位配置寄存器(GPIOx_CRL,GPIOx_CRH); 
(2) 两个32位数据寄存器(GPIOx_IDR和GPIOx_ODR); 
(3) 一个32位置位/复位寄存器(GPIOx_BSRR); 
(4) 一个16位复位寄存器(GPIOx_BRR); 
(5) 一个32位锁定寄存器(GPIOx_LCKR)。
根据数据手册中列出的每个I/O端口的特定硬件特征,GPIO端口的每个位可以由软件分别配置成多种模式。
STM32的I/O端口可以由软件配置成如下8种模式: 
(1)  浮空输入; 
(2)  上拉输入; 
(3) 下拉输入; 
(4)  模拟输入; 
(5) 开漏输出; 
(6) 推挽输出; 
(7) 复用开漏输出; 
(8) 复用推挽输出。
每个I/O端口可以自由编程,但I/O端口寄存器必须要按32位访问。STM32的很多I/O端口都是兼容5V的,这些I/O端口在与5V电压的外设连接时很有优势,具体哪些I/O端口是兼容5V的,可以从该芯片的数据手册引脚描述章节查到。
STM32的每个I/O端口都有7个寄存器来控制。常用的I/O端口寄存器只有4个,分别为CRL、CRH、IDR、ODR。CRL和CRH控制着每个I/O端口的模式及输出速率。
STM32的I/O端口位配置如表32所示。


表32STM32的I/O端口位配置表



配 置 模 式
CNF1
CNF0
MODE1
MODE0
PxODR寄存器

通用

输出
复用功
能输出

推挽式输出
0
0
开漏输出
0
1
推挽式输出
1
0
开漏输出
1
1
01(最大输出速率10MHz)

10(最大输出速率2MHz)

11(最大输出速率50MHz)
0或1
0或1
不使用
不使用
输入
模拟输入
0
0
浮空输入
0
1
下拉输入
1
0
上拉输入
1
0
00(保留)
不使用
不使用
0
1

3.6.2GPIO配置寄存器描述
(1)  端口配置低寄存器(GPIOx_CRL) (x=A,B,…,E),如图311所示。各位对应关系如表33所示。



图311端口配置低寄存器格式




表33GPIO端口配置低寄存器配置方式



位描述

位

31:30

27:26
23:22
19:18
15:14
11:10
7:6

3:2
CNFy[1:0]: 端口x配置位(y = 0,1,…,7)

软件通过这些位配置相应的I/O端口

在输入模式(MODE[1∶0]=00): 在输出模式(MODE[1∶0]>00): 

00: 模拟输入模式00: 通用推挽输出模式

01: 浮空输入模式(复位后的状态)01: 通用开漏输出模式

10: 上拉/下拉输入模式10: 复用功能推挽输出模式

11: 保留11: 复用功能开漏输出模式


续表


位描述

位
29:28
25:24
21:20
17:16
13:12
9:8 

5:4

1:0
MODEy[1:0]: 端口x的模式位(y=0,1,…,7)

软件通过这些位配置相应的I/O端口

00: 输入模式(复位后的状态)

01: 输出模式,最大速度10MHz

10: 输出模式,最大速度2MHz

11: 输出模式,最大速度50MHz

(2)  端口配置高寄存器(GPIOx_CRH)(x=A,B,…,E),如图312所示。各位对应关系如表34所示。


图312端口配置高寄存器格式




表34GPIO端口配置高寄存器配置方式



位描述

位

31:30
27:26
23:22
19:18
15:14
11:10
7:6

3:2
CNFy[1∶0]: 端口x配置位(y=8,9,…,15)

软件通过这些位配置相应的I/O端口

在输入模式(MODE[1:0]=00): 在输出模式(MODE[1:0]>00): 

00: 模拟输入模式00: 通用推挽输出模式

01: 浮空输入模式(复位后的状态)01: 通用开漏输出模式

10: 上拉/下拉输入模式10: 复用功能推挽输出模式

11: 保留11: 复用功能开漏输出模式
位
29:28
25:24
21:20
17:16
13:12
9:8

5:4

1:0
MODEy[1:0]: 端口x的模式位(y=8,9,…,15)

软件通过这些位配置相应的I/O端口

00: 输入模式(复位后的状态)

01: 输出模式,最大速度10MHz

10: 输出模式,最大速度2MHz

11: 输出模式,最大速度50MHz

例如,控制的是LED小灯,可以选择通用推挽输出模式,设置速度为2MHz,实现代码为: 

GPIOB->CRH = 0X22222222;

3.6.3端口输出数据寄存器
端口输出数据寄存器(GPIOx_ODR)(x=A,B,…,E),如图313所示。各位对应关系如表35所示。


图313端口输出数据寄存器格式




表35端口输出数据寄存器各位含义



位描述

位31:16
保留,始终读为0
位15:0
ODRy[15:0]: 端口输出数据(y=0,1,…,15),这些位可读可写并只能以字节(16位)的形式操作

注: 对GPIOx_BSRR(x=A,B,…,E),可以分别对各个ODR位进行独立地设置/清除

例如: 

GPIOB->ODR=0X0000;灯灭

GPIOB->ODR=0XFFFF;灯亮

3.7库函数操作
采用库函数控制流水灯,程序的可读性增强,代码维护方便,且操作简便。在主程序中输入以下代码。

1.#include "stm32f10x.h"

2.void Delay(IO u32 nCount);

3.int main(void)

4.{

5.GPIO_InitTypeDef GPIO_InitStructure;

6.RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); 	

7.GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_All;

8.GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_Out_PP;   

9.GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 

10.GPIO_Init(GPIOB, &GPIO_InitStructure);

11.while (1)

12.{

13.GPIO_Write(GPIOA, 0xFFFF);

14.Delay(0x0FFFEF);

15.GPIO_Write(GPIOA, 0x0000);

16.Delay(0x0FFFEF);

17.}

18.}

19.void Delay(IO u32 nCount)	 

20.{

21.for(; nCount != 0; nCount--);

22.}


这段代码在完成了LED初始化后(LED_GPIO_Config())实现了小灯的闪烁。LED初始化实际是库函数操作的核心,采用库函数来配置时钟、工作模式等。
3.7.1GPIO_Init函数
在maint文件中,第10行代码调用了GPIO_Init函数。通过《STM32F101xx和STM32F103xx固件函数库》手册找到该库函数的原型,表36为函数GPIO_Init。


表36函数GPIO_Init



函数名
GPIO_Init
函数原型
Void GPIO_Init(GPIO_TypeDef*GPIOx,GPIO_InitTypeDef*GPIO_InitStruct)
功能描述
根据GPIO_InitStruct中指定的参数初始化外设GPIOx寄存器
输入参数1
GPIOx: x可以是A,B,C,D或者E,来选择GPIO外设
输入参数2
GPIO_InitStruct:指向结构GPIO_InitTypeDef的指针,包含了外设GPIO的配置信息
输出参数
无
返回值
无
先决条件
无
被调用函数
无

第5行代码利用库定义了一个GPIO_InitStructure的结构体,结构体的类型为GPIO_InitTypeDef;它是利用typedef定义的新类型。追踪其定义原型,知道它位于stm32f10x_gpio.h文件中,代码为

typedef struct

{

uint16_t GPIO_Pin; 

GPIOSpeed_TypeDef GPIO_Speed;  

GPIOMode_TypeDef GPIO_Mode;    

}GPIO_InitTypeDef;

通过这段代码可知,GPIO_InitTypeDef类型的结构体有3个成员,分别为uint16_t类型的GPIO_Pin,GPIOSpeed_TypeDef类型的GPIO_Speed及GPIOMode_TypeDef类型的GPIO_Mode。
1. GPIO pin
该参数选择待设置的GPIO引脚,使用操作符“|”可以一次选中多个引脚。可以使用表37中的任意组合。


表37GPIO_Pin值



GPIO_Pin
描述
GPIO_Pin_None
无引脚被选中
GPIO_Pin_0
选中引脚0
GPIO_Pin_1
选中引脚1


续表


GPIO_Pin
描述
GPIO_Pin_2
选中引脚2
GPIO_Pin_3
选中引脚3
GPIO_Pin_4
选中引脚4
GPIO_Pin_5
选中引脚5
GPIO_Pin_6
选中引脚6
GPIO_Pin_7
选中引脚7
GPIO_Pin_8
选中引脚8
GPIO_Pin_9
选中引脚9
GPIO_Pin_10
选中引脚10
GPIO_Pin_11
选中引脚11
GPIO_Pin_12
选中引脚12
GPIO_Pin_13
选中引脚13
GPIO_Pin_14
选中引脚14
GPIO_Pin_15
选中引脚15
GPIO_Pin_All
选中全部引脚

这些宏的值,就是允许给结构体成员GPIO_Pin赋的值,例如给GPIO_Pin赋值为宏GPIO_Pin_0,表示选择了GPIO端口的第0个引脚,在后面会通过一个函数把这些宏的值进行处理,设置相应的寄存器,实现对GPIO端口的配置。
2. GPIOSpeed
GPIOSpeed_TypeDef库定义的新类型,GPIOSpeed_TypeDef原型如下: 

typedef enum

{

GPIO_Speed_10MHz = 1,

GPIO_Speed_2MHz,

GPIO_Speed_50MHz

}GPIOSpeed_TypeDef;

这是一个枚举类型,定义了3个枚举常量,GPIO_Speed值如表38所示。


表38GPIO_Speed值



GPIO_Speed
描述
GPIO_Speed_10MHz
最高输出速率10MHz
GPIO_Speed_2MHz
最高输出速率2MHz
GPIO_Speed_50MHz
最高输出速率50MHz

这些常量可用于标识GPIO引脚可以配置成的各自最高速度。所以在为结构体中的GPIO_Speed赋值的时候,就可以直接用这些含义清晰地枚举标识符。

3. GPIOMode
GPIOMode_TypeDef也是一个枚举类型定义符,分量值如表39所示,其原型如下: 

typedef enum

{ 

GPIO_Mode_AIN = 0x0,

GPIO_Mode_IN_FLOATING = 0x04,

GPIO_Mode_IPD = 0x28,

GPIO_Mode_IPU = 0x48,

GPIO_Mode_Out_OD = 0x14,

GPIO_Mode_Out_PP = 0x10,

GPIO_Mode_AF_OD = 0x1C,

GPIO_Mode_AF_PP = 0x18

}GPIOMode_TypeDef;


表39GPIO_Mode值



GPIO_Mode
描述
GPIO_Mode_AIN
模拟输入
GPIO_Mode_IN_FLOATING
浮空输入
GPIO_Mode_IPD
下拉输入
GPIO_Mode_IPU
上拉输入
GPIO_Mode_Out_OD
开漏输出
GPIO_Mode_Out_PP
推挽输出
GPIO_Mode_AF_OD
复用开漏输出
GPIO_Mode_AF_PP
复用推挽输出

这个枚举类型也定义了很多含义清晰的枚举常量,用来帮助配置GPIO引脚的模式,例如GPIO_Mode_AIN为模拟输入,GPIO_Mode_IN_FLOATING为浮空输入模式。可以明白GPIO_InitTypeDef类型结构体的作用,整个结构体包含GPIO_Pin、GPIO_Speed、GPIO_Mode 3个成员,这3个成员赋予不同的数值可以对GPIO端口进行不同的配置,这些可配置的数值已经由ST的库文件封装成见名知义的枚举常量,这使编写代码变得非常简便。
3.7.2RCC_APB2PeriphClockCmd
GPIO所用的时钟PCLK2采用默认值,为72MHz。采用默认值可以不修改分频器,但外设时钟默认处在关闭状态,所以外设时钟一般会在初始化外设时设置为开启,开启和关闭外设时钟也有封装好的库函数RCC_APB2PeriphClockCmd(),该函数如表310所示。


表310RCC_APB2PeriphClockCmd()库函数



函数名
RCC_APB2PeriphClockCmd()
函数原型
void RCC_APB2PeriphClockCmd(u32 RCC_APB2Periph,
FunctionalState NewState)
功能描述
使能或者失能APB2外设时钟
输入参数1
RCC_APB2Periph:门控APB2外设时钟
输入参数2
NewState: 指定外设时钟的新状态

这个参数可以取ENABLE或者DISABLE
输出参数
无
返回值
无
先决条件
无
被调用函数
无

该参数被门控的APB2外设时钟,可以取表311中的一个或者多个值的组合作为该参数的值。


表311APB2外设时钟的取值参数



RCC_AHB2Periph
描述
RCC_APB2Periph_AFIO
功能复用
RCC_APB2Periph_GPIOA
GPIOA
RCC_APB2Periph_GPIOB
GPIOB
RCC_APB2Periph_GPIOC
GPIOC
RCC_APB2Periph_GPIOD
GPIOD
RCC_APB2Periph_GPIOE
GPIOE
RCC_APB2Periph_ADC1
ADC1
RCC_APB2Periph_ADC2
ADC2
RCC_APB2Periph_TIM1
TIM1
RCC_APB2Periph_SPI1
SPI1
RCC_APB2Periph_USART1
USART1
RCC_APB2Periph_ALL
全部

例如,使能GPIOA, GPIOB和SPI1时钟,代码为

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB |RCC_APB2Periph_SPI1, ENABLE);

3.7.3控制I/O输出电平
前面选择好了引脚,配置了其功能及开启了相应的时钟,终于可以正式控制I/O端口的电平高低,从而实现控制LED灯的亮与灭。
前面提到过,要控制GPIO引脚的电平高低,只要在GPIOx_BSRR寄存器相应的位写入控制参数即可。ST库也提供了具有这样功能的函数,可以分别用GPIO_SetBits()(见表312)控制输出高电平和GPIO_ResetBits()(见表313)控制输出低电平。GPIO_Write()可以向指定GPIO数据端口写入数据,如表314所示。


表312函数GPIO_SetBits



函数名
GPIO_SetBits
函数原型
void GPIO_SetBits(GPIO_TypeDef* GPIOx, u16 GPIO_Pin)
功能描述
设置指定的数据端口位
输入参数1
GPIOx: x可以是A,B,C,D或者E,来选择GPIO外设
输入参数2
GPIO_Pin: 待设置的端口位

该参数可以取GPIO_Pin_x(x可以是0~15)的任意组合
输出参数
无
返回值
无
先决条件
无
被调用函数
无





表313函数GPIO_ResetBits



函数名
GPIO_ResetBits
函数原型
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, u16 GPIO_Pin)
功能描述
清除指定的数据端口位
输入参数1
GPIOx: x可以是A,B,C,D或者E,来选择GPIO外设
输入参数2
GPIO_Pin: 待清除的端口位

该参数可以取GPIO_Pin_x(x可以是0~15)的任意组合
输出参数
无
返回值
无
先决条件
无
被调用函数
无




表314函数GPIO_Write



函数名GPIO_Write
函数原形void GPIO_Write(GPIO_TypeDef* GPIOx, u16 PortVal)
功能描述向指定 GPIO 数据端口写入数据
输入参数 1GPIOx:x 可以是 A,B,C,D 或者 E,来选择 GPIO 外设
输入参数 2PortVal: 待写入端口数据寄存器的值
输出参数无
返回值无
先决条件无
被调用函数无
例如,设置GPIOA端口pin10和pin15为高电平,代码为

GPIO_SetBits(GPIOA, GPIO_Pin_10 | GPIO_Pin_15);

例如,设置GPIOA端口pin10和pin15为低电平,代码为

GPIO_ResetBits(GPIOA,GPIO_Pin_10 | GPIO_Pin_15);

例如,设置GPIOA口,代码为

GPIO_Write(GPIOA, 0x1101);




3.8数码管操作实例
通过数码管的例子,进一步说明I/O的使用方法。常用GPIO库函数如表315所示,这些函数在以后的I/O控制中会陆续使用到。


表315GPIO库函数



函数名
描述
GPIO_DeInit
将外设GPIOx寄存器重设为默认值
GPIO_AFIODeInit
将复用功能(重映射事件控制和EXTI设置)重设为默认值
GPIO_Init
根据GPIO_InitStruct中指定的参数初始化外设GPIOx寄存器


续表


函数名
描述
GPIO_StructInit
把GPIO_InitStruct中的每一个参数按默认值填入
GPIO_ReadInputDataBit
读取指定端口引脚的输入
GPIO_ReadInputData
读取指定的GPIO端口输入
GPIO_ReadOutputDataBit
读取指定端口引脚的输出
GPIO_ReadOutputData
读取指定的GPIO端口输出
GPIO_SetBits
设置指定的数据端口位
GPIO_ResetBits
清除指定的数据端口位
GPIO_WriteBit
设置或者清除指定的数据端口位
GPIO_Write
向指定GPIO数据端口写入数据
GPIO_PinLockConfig
锁定GPIO引脚设置寄存器
GPIO_EventOutputConfig
选择GPIO引脚用作事件输出
GPIO_EventOutputCmd
使能或者失能事件输出
GPIO_PinRemapConfig
改变指定引脚的映射
GPIO_EXTILineConfig
选择GPIO引脚用作外部中断线路

3.8.1数码管基础知识
(1) 一个数码管有8段,分别为A,B,C,D,E,F,G,DP,即由8个发光二极管组成,如图314所示。


图314数码管示意图


(2) 发光二极管导通的方向是一定的(导通电压一般取为1.7V),这8个发光二极管的公共端有两种,可以分别接+5V(即为共阳极数码管)或接地(即为共阴极数码管)。
(3) 可分共阳极(公共端接高电平或+5V电压)和共阴极(共低电平或接地)两种数码管。
① 共阳极数码管编码表。
位选为高电平(即1)选中数码管,各段选为低电平(即0接地时)选中各数码段亮,由0到f的编码为
uchar code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,

0xf8,0x80,0x90,0x88,0x83,0xc6,0xa1,

0x86,0x8e};

② 共阴极数码管编码表。
位选为低电平(即0)选中数码管,各段选为高电平(即1接+5V时)选中各数码段亮,由0到f的编码为
uchar code table[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,

0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,

0x79,0x71};

(4) 其中,每个段均有0(不导通)和1(导通发光)两种状态,但共阳极数码管和共阴极数码管显然是不同的。
(5) 它在程序中的应用是用一个8位二进制数表示,A为最低位,……,F为最高位(第8位)。

3.8.2硬件电路设计
如图315所示的电路使用共阳极数码管,段选端接到PB0~PB7,位选端接到PB8~PB15,通过PNP三极管实现电流的放大,增加驱动能力,提高显示的亮度,当位选端为低电平时,三极管导通,段选端输入正确的数据,数码管就能实现正确的显示。



图315数码管接口电路


通过这个例子进一步了解I/O端口的库函数操作方法,以及I/O复用端口的设置问题。
3.8.3软件说明
下面这段代码简单实现了数码管从1~F的亮灭,控制过程类似LED,这里使用RCC_APB2Periph_AFIO、GPIO_PinRemapConfig的配置。下面对程序进行解释和说明。
STM32F10x系列的MCU复位后,PA13/14/15&PB3/4默认配置为JTAG功能。有时为了充分利用STM32I/O端口的资源,会把这些端口设置为普通I/O端口。STM32的PB3、PB4,分别是JTAG的JTDO和NJTRST引脚,在没关闭JTAG功能之前,程序中配置不了这些引脚的功能。要配置这些引脚,首先要开启AFIO时钟,然后在AFIO中设置释放这些引脚。


1. #include "stm32f10x.h"

2. void Delay(__IO u32 nCount);

3. u8  table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,\

4. 0x80,0x90,0x88,0x83,0xc6,0xa1,0x86,0x8e};

5. u8 i;

6. int main(void)

7. {	

8. 

9. GPIO_InitTypeDef GPIO_InitStructure;

10. RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOB , ENABLE);

11. GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable,ENABLE);		  

12. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;	

13. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;   

14. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 

15. GPIO_Init(GPIOB, &GPIO_InitStructure);	

16.while (1)

17.{ 

18.for(i=0;i<16;i++)

19.{

20. GPIO_Write(GPIOB, table[i]);

21. Delay(0x0FFFEF5);

22.if(i==15) i=0;	

23.}

24.}

25.}

26. void Delay(__IO u32 nCount)	 

27.{

28.for(;nCount != 0; nCount--);

29.} 

30. void Delay(__IO u32 nCount)	 

31. {

32. for(; nCount != 0; nCount--);

33. }

第10行代码,开启AFIO时钟和GPIOB的时钟。
第11行代码,禁用JTAG功能,重新映射为普通的I/O端口。
为了优化64引脚或100引脚封装的外设数目,可以把一些复用功能重新映射到其他引脚上。设置复用重映射和调试I/O配置寄存器(AFIO_MAPR)实现引脚的重新映射。这时,复用功能不再映射到它们的原始分配上。GPIO_PinRemapConfig函数如表316所示。


表316函数GPIO_PinRemapConfig



函数名
GPIO_ PinRemapConfig
函数原型
void GPIO_PinRemapConfig(u32 GPIO_Remap, FunctionalState NewState)
功能描述
改变指定引脚的映射
输入参数1
GPIO_Remap:选择重映射的引脚
输入参数2
NewState:引脚重映射的新状态

这个参数可以取ENABLE或者DISABLE
输出参数
无


续表


函数名
GPIO_ PinRemapConfig
返回值
无
先决条件
无
被调用函数
无

GPIO_Remap用以选择用作事件输出的GPIO端口。表317给出了该参数可取的值。


表317GPIO_Remap



GPIO_Remap
描述
GPIO_Remap_SPI1
SPI1复用功能映射
GPIO_Remap_I2C1
I2C1复用功能映射
GPIO_Remap_USART1
USART1复用功能映射
GPIO_PartialRemap_USART3
USART2复用功能映射
GPIO_FullRemap_USART3
USART3复用功能完全映射
GPIO_PartialRemap_TIM1
USART3复用功能部分映射
GPIO_FullRemap_TIM1
TIM1复用功能完全映射
GPIO_PartialRemap1_TIM2
TIM2复用功能部分映射1
GPIO_PartialRemap2_TIM2
TIM2复用功能部分映射2
GPIO_FullRemap_TIM2
TIM2复用功能完全映射
GPIO_PartialRemap_TIM3
TIM3复用功能部分映射
GPIO_FullRemap_TIM3
TIM3复用功能完全映射
GPIO_Remap_TIM4
TIM4复用功能映射
GPIO_Remap1_CAN
CAN复用功能映射1
GPIO_Remap2_CAN
CAN复用功能映射2
GPIO_Remap_PD01
PD01复用功能映射
GPIO_Remap_SWJ_NoJTRST
除JTRST外SWJ完全使能(JTAG+SWDP)
GPIO_Remap_SWJ_JTAGDisable
JTAGDP失能+SWDP使能
GPIO_Remap_SWJ_Disable
SWJ完全失能(JTAG+SWDP)


3.9简单按键操作实例
显示模块I/O端口都是作为显示模块控制,即输出使用,本节通过简单按键控制,配合LED小灯,了解I/O端的输入使用方法。按键被按下,LED小灯熄灭。图316为按键电路图。

主程序:
1.#include "stm32f10x.h"

2.#include "led.h"

3.#include "key.h" 

4.int main(void)

5.{	

6.LED_GPIO_Config();

7.LED5(ON);

8.Key_GPIO_Config();	

9.while(1)                            

10.{

11.if( Key_Scan(GPIOA,GPIO_Pin_0) == KEY_ON  )

12.{

13.LED5(OFF);

14.}   

15.}

16.}



图316按键电路图


第6行代码配置LED和之前使用方法一致。
第8行代码初始化按键操作。
第11行代码按键识别程序,检测PA0是否被按下,如果被按下,LED5小灯熄灭。
按键配置代码:
1.void Key_GPIO_Config(void)

2.{

3.GPIO_InitTypeDef GPIO_InitStructure;

4.RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

5.GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; 

6.GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz; 

7.GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; 

8.GPIO_Init(GPIOA, &GPIO_InitStructure);

9.}

按键初始化与LED初始化类似,其中第7行代码设置PA口为上拉输入模式。
按键识别代码:
1.uint8_t Key_Scan(GPIO_TypeDef* GPIOx,u16 GPIO_Pin)

2.{	

3.if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON ) 

4.{

5.Delay(10000);

6.if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON )  

7.{

8.while(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON);   

9.return  KEY_ON;	 

10.}

11.else

12.return KEY_OFF;

13.}

14.else

15.return KEY_OFF;

16.}

第3行代码利用GPIO_ReadInputDataBit()函数读取输入数据,若从相应引脚读取的数据等于(KEY_ON),低电平,表明可能有按键按下,调用延时函数; 否则返回KEY_OFF,表示按键没有被按下。
第6行代码,延时之后再次利用GPIO_ReadInputDataBit()函数读取输入数据,若依然为低电平,则表明确实有按键被按下; 否则返回KEY_OFF,表示没有按键被按下。
第8行代码,循环调用GPIO_ReadInputDataBit()函数(见表318),一直检测按键的电平,直至按键被释放。释放后,返回表示按键被按下的标志KEY_ON。


表318函数GPIO_ReadInputDataBit



函数名
GPIO_ReadInputDataBit
函数原型
u8 GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, u16 GPIO_Pin)
功能描述
读取指定端口引脚的输入
输入参数1
GPIOx: x可以是A,B,C,D或者E,来选择GPIO外设
输入参数2
GPIO_Pin: 待读取的端口位
输出参数
无
返回值
输入端口引脚值
先决条件
无
被调用函数
无

3.10本章小结
本章通过流水灯例子进一步强化通过MDK建立工程的方法,配置的详细步骤。采用两种方法编程: 寄存器法和库函数法。两种方法控制I/O的步骤和方法一致,寄存器法有助于帮助学习者更好地了解STM32的内部结构,而库函数法能让学习者更方便地编程。不管哪种方法,包括后面的应用,始终要遵循配置I/O的如下步骤: 
(1) 配置时钟。
(2) 配置I/O工作模式,常用的工作模式为推挽输出模式和上拉输入模式。
(3) 操作I/O口(赋值或者读入数据)。
后期的串口操作,定时器操作,DMA操作等部分内容,均是在这三步的基础上的扩展。这三步是配置的核心。
配置时钟,核心是了解时钟树,常用的功能主要用APB2和APB1时钟树相关联的库函数,常规操作I/O口工作模式均用推挽输出模式和上拉输入模式,涉及总线操作用浮空模式,涉及模拟输入采用模拟输入模式,对于I/O口的操作和一般微处理器操作没有不同,通过数码管操作实例和简单按键操作实例进一步强化I/O的使用步骤和方法。
3.11习题
(1) 简述通过MDK建立工程的方法。
(2) 直接库函数操作和寄存器操作有哪些区别?
(3) 分析STM32的时钟数结构。
(4) 通用GPIO的初始化过程是什么?
(5) 采用查询法编写按键识别代码。
(6) 编写复杂流水灯程序,灯光闪烁的频率为第一首歌曲的频率。
(7) 利用数码管编写动态显示12345678。
(8) 利用数码管和简单按键操作实例编写万年历代码,如171130,表示2017年11月30日,通过按键可以切换显示103045,表示10点30分45秒,可以通过按键修改时钟。