第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章建立好的工程复制到该文件夹下,可以参考书中附带的例子,从例子中添加这些代码,后续会陆续解释其含义。“流水灯”工程的子文件夹如图31所示。 图31“流水灯”工程的子文件夹 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工程名改为“流水灯”,这样更方便管理和阅读,如图32所示,以后类似工程的建立都可采用此方法,不用反复建立工程。 图32修改工程 3.3MDK工程配置 MDK工程配置步骤如下。 (1) 单击工具栏中的魔术棒按钮,弹出配置菜单,可以看到配置菜单关于Device、Target、Output、Listing、User、C/C++、Asm、Linker、Debug和Utilities选项的设置。 (2) Device在新建工程时已经选定了器件,单击Target选项卡,勾选微库,这样是为了后面的串口例程可以使用printf函数,如图33所示。 图33Target选项卡 (3) 单击Output选项卡,再单击Select Folder for Objects…按钮,设置编译后输出文件保存的位置。同时勾选Debug Information、Create HEX File和Browse Information复选框,如图34所示。 图34Output选项卡 (4) 在Listing选项卡中,单击Select Folder Listings…按钮,定位到模板中的Listing文件夹,如图35所示。 图35Listing选项卡 (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。 库文件路径修改成功之后,如图36所示。 图36C/C++选项卡 (6) 单击Debug选项卡,选择Use Simulator,软件仿真设置完成,如图37所示。 图37Debug选项卡 (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号引脚,如图38所示。 图38流水灯电路 在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的出现打破了这个传统的法则,可以通过软件随意改变时钟速度。这让设计更加灵活,但也给设计增加了复杂性。在使用某一功能前,要先对其时钟进行初始化。图39是它的时钟树,不同的外设对应不同的时钟,在STM32中有5个时钟源,分别为HSI、HSE、LSI、LSE、PLL。PLL是由锁相环电路倍频得到PLL时钟。 图39时钟树 (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的外设访问结束。它的寄存器格式如图310所示。各位对应含义如表31所示。 图310APB2外设时钟使能寄存器格式 表31使能寄存器位置功能表 位描述 位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端口位配置如表32所示。 表32STM32的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),如图311所示。各位对应关系如表33所示。 图311端口配置低寄存器格式 表33GPIO端口配置低寄存器配置方式 位描述 位 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),如图312所示。各位对应关系如表34所示。 图312端口配置高寄存器格式 表34GPIO端口配置高寄存器配置方式 位描述 位 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),如图313所示。各位对应关系如表35所示。 图313端口输出数据寄存器格式 表35端口输出数据寄存器各位含义 位描述 位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固件函数库》手册找到该库函数的原型,表36为函数GPIO_Init。 表36函数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引脚,使用操作符“|”可以一次选中多个引脚。可以使用表37中的任意组合。 表37GPIO_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值如表38所示。 表38GPIO_Speed值 GPIO_Speed 描述 GPIO_Speed_10MHz 最高输出速率10MHz GPIO_Speed_2MHz 最高输出速率2MHz GPIO_Speed_50MHz 最高输出速率50MHz 这些常量可用于标识GPIO引脚可以配置成的各自最高速度。所以在为结构体中的GPIO_Speed赋值的时候,就可以直接用这些含义清晰地枚举标识符。 3. GPIOMode GPIOMode_TypeDef也是一个枚举类型定义符,分量值如表39所示,其原型如下: 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; 表39GPIO_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(),该函数如表310所示。 表310RCC_APB2PeriphClockCmd()库函数 函数名 RCC_APB2PeriphClockCmd() 函数原型 void RCC_APB2PeriphClockCmd(u32 RCC_APB2Periph, FunctionalState NewState) 功能描述 使能或者失能APB2外设时钟 输入参数1 RCC_APB2Periph:门控APB2外设时钟 输入参数2 NewState: 指定外设时钟的新状态 这个参数可以取ENABLE或者DISABLE 输出参数 无 返回值 无 先决条件 无 被调用函数 无 该参数被门控的APB2外设时钟,可以取表311中的一个或者多个值的组合作为该参数的值。 表311APB2外设时钟的取值参数 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()(见表312)控制输出高电平和GPIO_ResetBits()(见表313)控制输出低电平。GPIO_Write()可以向指定GPIO数据端口写入数据,如表314所示。 表312函数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)的任意组合 输出参数 无 返回值 无 先决条件 无 被调用函数 无 表313函数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)的任意组合 输出参数 无 返回值 无 先决条件 无 被调用函数 无 表314函数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库函数如表315所示,这些函数在以后的I/O控制中会陆续使用到。 表315GPIO库函数 函数名 描述 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个发光二极管组成,如图314所示。 图314数码管示意图 (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硬件电路设计 如图315所示的电路使用共阳极数码管,段选端接到PB0~PB7,位选端接到PB8~PB15,通过PNP三极管实现电流的放大,增加驱动能力,提高显示的亮度,当位选端为低电平时,三极管导通,段选端输入正确的数据,数码管就能实现正确的显示。 图315数码管接口电路 通过这个例子进一步了解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函数如表316所示。 表316函数GPIO_PinRemapConfig 函数名 GPIO_ PinRemapConfig 函数原型 void GPIO_PinRemapConfig(u32 GPIO_Remap, FunctionalState NewState) 功能描述 改变指定引脚的映射 输入参数1 GPIO_Remap:选择重映射的引脚 输入参数2 NewState:引脚重映射的新状态 这个参数可以取ENABLE或者DISABLE 输出参数 无 续表 函数名 GPIO_ PinRemapConfig 返回值 无 先决条件 无 被调用函数 无 GPIO_Remap用以选择用作事件输出的GPIO端口。表317给出了该参数可取的值。 表317GPIO_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+SWDP) GPIO_Remap_SWJ_JTAGDisable JTAGDP失能+SWDP使能 GPIO_Remap_SWJ_Disable SWJ完全失能(JTAG+SWDP) 3.9简单按键操作实例 显示模块I/O端口都是作为显示模块控制,即输出使用,本节通过简单按键控制,配合LED小灯,了解I/O端的输入使用方法。按键被按下,LED小灯熄灭。图316为按键电路图。 主程序: 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.} 图316按键电路图 第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()函数(见表318),一直检测按键的电平,直至按键被释放。释放后,返回表示按键被按下的标志KEY_ON。 表318函数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) 利用数码管和简单按键操作实例编写万年历代码,如171130,表示2017年11月30日,通过按键可以切换显示103045,表示10点30分45秒,可以通过按键修改时钟。