零死角玩转stm32

    5、流水灯的前后今生

   通过前面的内容,读者对库仅仅是建立了一个非常模糊的印象。

作为大家的第一个STM32例程,野火认为很有必要进行足够深入的分析,才能从根本上扫清读者对使用库函数的困惑。而且,只要读者利用这个LED例程,真正领会了库开发的流程以及原理,再进行其它外设的开发就变得相当简单了。

所以本章的任务是:

从STM32库的实现原理上解答 库到底是什么、为什么要用库、用库与直接配置寄存器的区别等问题。

让读者了解具体利用库的开发流程,熟悉库函数的结构,达到举一反三的效果,这次可就不是喝稀粥了,保证有吃干饭,所学就是所用的效果。

5.1 STM32的GPIO

想要控制LED灯,当然是通过控制STM32芯片的I/O引脚电平的高低来实现。在STM32芯片上,I/O引脚可以被软件设置成各种不同的功能,如输入或输出,所以被称为GPIO (General-purpose I/O)。而GPIO引脚又被分为GPIOA、GPIOB……GPIOG不同的组,每组端口分为0~15,共16个不同的引脚,对于不同型号的芯片,端口的组和引脚的数量不同,具体请参考相应芯片型号的datasheet。

于是,控制LED的步骤就自然整理出来了:

GPIO端口引脚多 --> 就要选定需要控制的特定引脚

GPIO功能如此丰富 --> 配置需要的特定功能

控制LED的亮和灭 --> 设置GPIO输出电压的高低

继续思考,要控制GPIO端口,就要涉及到控制相关的寄存器。这时我们就要查一查与GPIO相关的寄存器了,可以通过《STM32参考手册》来查看,见图 51


STM32-1

图 51

图中的7个寄存器,相应的功能在文档上有详细的说明。可以分为以下4类,其功能简要概括如下:

配置寄存器:选定GPIO的特定功能,最基本的如:选择作为输入还是输出端口。

数据寄存器:保存了GPIO的输入电平 或 将要输出的电平

位控制寄存器:设置某引脚的数据 为1或0,控制输出的电平。

锁定寄存器:设置某锁定引脚后,就不能修改其配置。

注:要想知道其功能严谨、详细的描述,请读者养成习惯在正式使用时,要以官方的datasheet为准,在这里只是简单地概括其功能进行说明。

关于寄存器名称上标号x 的意义,如:GPIOx_CRL、GPIOx_CRH ,这个x的取值可以为图中括号内的值(A……E),表示这些寄存器也跟GPIO一样,也是分组的。也就是说,对于端口GPIOA和GPIOB,它们都有互不相干的一组寄存器,如控制GPIOA的寄存器名为GPIOA_CRL、GPIOA_CRH等,而控制GPIOB的则是不同的、被命名为GPIOB_CRL、GPIOB_CRH等寄存器。

我们的程序代码以野火STM32第二代开发板为例,根据其硬件连接图来分析,见图 52及图 53错误!未找到引用源

STM32-2

图 52

STM32-3

图 53

从这个图我们可以知道STM32的功能,实际上也是通过配置寄存器来实现的。配置寄存器的具体参数,需要参考《STM32参考手册》的寄存器说明。见图 54。

STM32-4

图 54

如图,对于GPIO端口,每个端口有16个引脚,每个引脚 的模式由寄存器的4个位控制,每四位又分为两位控制引脚配置(CNFy[1:0]),两位控制引脚的 模式及最高速度(MODEy[1:0]),其中y表示第y个引脚。这个图是GPIOx_CRH寄存器的说明,配置GPIO引脚模式的一共有两个寄存器,CRH是高寄存器,用来配置高8位引脚:pin8~pin15。还有一个称为CRL寄存器,如果我们要配置pin0~pin7引脚,则要在寄存器CRL中进行配置。

举例说明对CRH的寄存器的配置:当给GPIOx_CRH寄存器的第28至29位设置为参数“11”,并在第30至31位 设置为参数“00”,则把x端口第15个引脚 的模式配置成了“输出的最大速度为50MHz的 通用推挽输出模式、”,其它引脚可通过其GPIOx_CRH或GPIOx_CRL的其它寄存器位来配置。至于x端口的x是指端口GPIOA还是GPIOB还要具体到不同的寄存器基址,这将在后面分析。

接下来分析要控制引脚电平高低,需要对寄存器进行什么具体的操作。见图 55。

STM32-5

图 55

由寄存器说明图可知,一个引脚y的输出数据由GPIOx_BSRR寄存器位的2个位来控制分别为BRy (Bit Reset y)BSy (Bit Set y),BRy位用于写1清零,使引脚输出低电平,BSy位用来写1置1,使引脚输出高 电平。而对这两个位进行写零都是无效的。(还可以通过设置寄存器ODR来控制引脚的输出。)

例如:对x端口的寄存器GPIOx_BSRR的第0位(BS0) 进行写1,则x端口的第0引脚被设置为1,输出高电平,若要令第0引脚再输出低电平,则需要向GPIOx_BSRR的第16位(BR0) 写1。

5.2 STM32的地址映射

温故而知新——stm32f10x.h文件

首先请大家回顾一下在51单片机上点亮LED是怎样实现的。这太简单了,几行代码就搞定。

#include<reg52.h>

int main (void)

{

P0=0;

while(1);

}

以上代码就可以点亮P0端口与LED阴极相连的LED灯了,当然,这里省略了启动代码。为什么这个P0 =0; 句子就能控制P0端口为低电平?很多刚入门51单片机的同学还真解释不来,关键之处在于这个代码所包含的头文件<reg52.h>。

在这个文件下有以下的定义:

/*  BYTE Registers  */

sfr P0    = 0x80;

sfr P1    = 0x90;

sfr P2    = 0xA0;

sfr P3    = 0xB0;

sfr PSW   = 0xD0;

sfr ACC   = 0xE0;

sfr B     = 0xF0;

sfr SP    = 0x81;

sfr DPL   = 0x82;

sfr DPH   = 0x83;

sfr PCON  = 0x87;

sfr TCON  = 0x88;

sfr TMOD  = 0x89;

sfr TL0   = 0x8A;

sfr TL1   = 0x8B;

sfr TH0   = 0x8C;

sfr TH1   = 0x8D;

sfr IE    = 0xA8;

sfr IP    = 0xB8;

sfr SCON  = 0x98;

sfr SBUF  = 0x99;

STM32-6

这些定义被称为地址映射

所谓地址映射,就是将芯片上的存储器 甚至I/O等资源与地址建立一一对应的关系。如果某地址对应着某寄存器,我们就可以运用c语言的指针来寻址并修改这个地址上的内容,从而实现修改该寄存器的内容。

正是因为<reg52.h>头文件中有了对于各种寄存器I/O端口的地址映射,我们才可以在51单片机程序中方便地使用P0 =0xFF; TMOD =0xFF等赋值句子对寄存器进行配置,从而控制单片机。

Cortex-M3的地址映射也是类似的。Cortex-M3有32根地址线,所以它的寻址空间大小为2^32 bit=4GB。ARM公司设计时,预先把这4GB的寻址空间大致地分配好了。它把地址从0x4000 0000至0x5FFF FFFF( 512MB )的地址分配给片上外设。通过把片上外设的寄存器映射到这个地址区,就可以简单地以访问内存的方式,访问这些外设的寄存器,从而控制外设的工作。结果,片上外设可以使用 C 语言来操作。M3存储器映射见图 57

STM32-7

图 57

stm32f10x.h这个文件中重要的内容就是把STM32的所有寄存器进行地址映射。如同51单片机的<reg52.h>头文件一样,stm32f10x.h像一个大表格,我们在使用的时候就是通过宏定义进行类似查表的操作,大家想像一下没有这个文件的话,我们要怎样访问STM32的寄存器?有什么缺点?

不进行这些宏定义的缺点有:

1、地址容易写错

2、我们需要查大量的手册来确定哪个地址对应哪个寄存器

3、看起来还不好看,且容易造成编程的错误,效率低,影响开发进度。

当然,这些工作都是由ST的固件工程师来完成的,只有设计M3的人才是最了解M3的,才能写出完美的库。

在这里我们以外接了LED灯的外设GPIOC为例,在这个文件中有这样的一系列宏定义:

#define GPIOC_BASE            (APB2PERIPH_BASE + 0x1000)

#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)

#define PERIPH_BASE           ((uint32_t)0x40000000)

这几个宏定义是从文件中的几个部分抽离出来的,具体的读者可参考stm32f10x.h源码。

外设基地址

首先看到PERIPH_BASE这个宏,宏展开为0x4000 0000,并把它强制转换为uint32_t的32位类型数据,这是因为地STM32的地址是32位的,是不是觉得0x4000 0000这个地址很熟?是的,这个是Cortex-M3核分配给片上外设的从0x4000 0000至0x5FFF FFFF的512MB寻址空间中 的第一个地址,我们把0x4000 0000称为外设基地址。

总线基地址

接下来是宏APB2PERIPH_BASE,宏展开为PERIPH_BASE(外设基地址)加上偏移地址0x1 0000,即指向的地址为0x4001 0000。这个APB2PERIPH_BASE宏是什么地址呢?STM32不同的外设是挂载在不同的总线上的,见图 58。有AHB总线、APB2总线、APB1总线,挂载在这些总线上的外设有特定的地址范围。

STM32-8

图 58

其中像GPIO、串口1、ADC及部分定时器是挂载这个被称为APB2的总线上,挂载到APB2总线上的外设地址空间是从0x4001 0000至地址0x4001 3FFF。这里的第一个地址,也就是0x4001 0000,被称为APB2PERIPH_BASE (APB2总线外设的基地址)。

而APB2总线基地址相对于外设基地址的偏移量为0x1 0000个地址,即为APB2相对外设基地址的偏移地址。

见表:

b1

由这个表我们可以知道,stm32f10x.h这个文件中必然还有以下的宏:

#define APB1PERIPH_BASE       PERIPH_BASE

因为偏移量为零,所以APB1的地址直接就等于外设基地址

寄存器组基地址

最后到了宏GPIOC_BASE,宏展开为APB2PERIPH_BASE (APB2总线外设的基地址)加上相对APB2总线基地址的偏移量0x1000得到了GPIOC端口的寄存器组的基地址。这个所谓的寄存器组又是什么呢?它包括什么寄存器?

细看stm32f10x.h文件,我们还可以发现以下类似的宏:

#define GPIOA_BASE            (APB2PERIPH_BASE + 0x0800)

#define GPIOB_BASE            (APB2PERIPH_BASE + 0x0C00)

#define GPIOC_BASE            (APB2PERIPH_BASE + 0x1000)

#define GPIOD_BASE            (APB2PERIPH_BASE + 0x1400)

除了GPIOC寄存器组的地址,还有GPIOA、GPIOB、GPIOD的地址,并且这些地址是不一样的。

前面提到,每组GPIO都对应着独立的一组寄存器,查看stm32的datasheet,看到寄存器说明如下图:

STM32-9

图 59

注意到这个说明中有一个偏移地址:0x04,这里的偏移地址的是相对哪个地址的偏移呢?下面进行举例说明。

对于GPIOC组的寄存器,GPIOC含有的 端口配置高寄存器(GPIOC_CRH) 寄存器地址为:GPIOC_BASE +0x04

假如是GPIOA组的寄存器,则GPIOA含有的 端口配置高寄存器(GPIOA_CRH)寄存器地址为:GPIOA_BASE+0x04

也就是说,这个偏移地址,就是该寄存器 相对所在寄存器组基地址的偏移量。

于是,读者可能会想,大概这个文件含有一个类似如下的宏( 当初野火也是这么想的 ):

#define  GPIOC_CRH   (GPIOC_BASE + 0x04)

这个宏,定义了GPIOC_CRH寄存器的具体地址,然而,在stm32f10x.h文件中并没有这样的宏。ST公司的工程师采用了更巧妙的方式来确定这些地址,请看下一小节——STM32库对寄存器的封装。

5.3 STM32库对寄存器的封装

ST的工程师用结构体的形式,封装了寄存器组,c语言结构体学的不好的同学,可以在这里补补课了。在stm32f10x.h文件中,有以下代码:

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)

#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)

#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)

有了这些宏,我们就可以定位到具体的寄存器地址,在这里发现了一个陌生的类型GPIO_TypeDef ,追踪它的定义,可以在stm32f10x.h 文件中找到如下代码:

typedef struct

{

__IO uint32_t CRL;

__IO uint32_t CRH;

__IO uint32_t IDR;

__IO uint32_t ODR;

__IO uint32_t BSRR;

__IO uint32_t BRR;

__IO uint32_t LCKR;

} GPIO_TypeDef;

其中 __IO 也是一个ST库定义的宏,宏定义如下:

#define   __O  volatile /*!< defines 'write only' permissions  */

#define   __IO  volatile /*!< defines 'read / write' permissions */

volatitle 是c语言的一个关键字,有关volatitle的用法可查阅相关的C语言书籍。

回到GPIO_TypeDef 这段代码,这个代码用typedef 关键字声明了名为GPIO_TypeDef的结构体类型,结构体内又定义了7个 __IO  uint32_t 类型的变量。这些变量每个都为32位,也就是每个变量占内存空间4个字节。在c语言中,结构体内变量的存储空间是连续的,也就是说假如我们定义了一个GPIO_TypeDef ,这个结构体的首地址(变量CRL的地址)若为0x4001 1000, 那么结构体中第二个变量(CRH)的地址即为0x4001 1000 +0x04 ,加上的这个0x04,正是代表4个字节地址的偏移量。

细心的读者会发现,这个0x04偏移量,正是GPIOx_CRH寄存器相对于所在寄存器组的偏移地址,见图 59。同理,GPIO_TypeDef 结构体内其它变量的偏移量,也和相应的寄存器偏移地址相符。于是,只要我们匹配了结构体的首地址,就可以确定各寄存器的具体地址了。

STM32-10

有了这些准备,就可以分析本小节的第一段代码了:

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)

#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)

#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)

GPIOA_BASE 在上一小节已解析,是一个代表GPIOA组寄存器的基地址。(GPIO_TypeDef *)在这里的作用则是把GPIOA_BASE 地址转换为GPIO_TypeDef  结构体指针类型。

有了这样的宏,以后我们写代码的时候,如果要修改GPIO的寄存器,就可以用以下的方式来实现。代码分析见注释。

GPIO_TypeDef * GPIOx;      //定义一个GPIO_TypeDef型结构体指针GPIOx

GPIOx = GPIOA;             //把指针地址设置为宏GPIOA地址

GPIOx->CRL = 0xffffffff;   //通过指针访问并修改GPIOA_CRL寄存器

通过类似的方式,我们就可以给具体的寄存器写上适当的参数,控制STM32了。是不是觉得很巧妙?但这只是库开发的皮毛,而且实际上我们并不是这样使用库的,库为我们提供了更简单的开发方式。M3的库可谓尽情绽放了c的魅力,如果你是单片机初学者,c语言初学者,那么请你不要放弃与M3库邂逅的机会。是否选择库,就差你一个闪亮的回眸。

5.4 STM32的时钟系统

STM32芯片为了实现低功耗,设计了一个功能完善但却非常复杂的时钟系统。普通的MCU,一般只要配置好GPIO的寄存器,就可以使用了,但STM32还有一个步骤,就是开启外设时钟。

5.4.1时钟树&时钟源

首先,从整体上了解STM32的时钟系统。见图 011

STM32-11

图 011

这个图说明了STM32的时钟走向,从图的左边开始,从时钟源一步步分配到外设时钟。

从时钟频率来说,又分为高速时钟低速时钟,高速时钟是提供给芯片主体的主时钟,而低速时钟只是提供给芯片中的RTC(实时时钟)及独立看门狗使用。

从芯片角度来说,时钟源分为内部时钟外部时钟源 ,内部时钟是在芯片内部RC振荡器产生的,起振较快,所以时钟在芯片刚上电的时候,默认使用内部高速时钟。而外部时钟信号是由外部的晶振输入的,在精度和稳定性上都有很大优势,所以上电之后我们再通过软件配置,转而采用外部时钟信号。

所以,STM32有以下4个时钟源:

高速外部时钟(HSE):以外部晶振作时钟源,晶振频率可取范围为4~16MHz,我们一般采用8MHz的晶振。

高速内部时钟(HSI): 由内部RC振荡器产生,频率为8MHz,但不稳定。

低速外部时钟(LSE):以外部晶振作时钟源,主要提供给实时时钟模块,所以一般采用32.768KHz。野火M3实验板上用的是32.768KHz,6p负载规格的晶振。

低速内部时钟(LSI):由内部RC振荡器产生,也主要提供给实时时钟模块,频率大约为40KHz。

5.4.2高速外部时钟(HSE)

我们以最常用的高速外部时钟为例分析,首先假定我们在外部提供的晶振的频率为8MHz的。

1、从左端的OSC_OUT和OSC_IN开始,这两个引脚分别接到外部晶振的两端。

2、8MHz的时钟遇到了第一个分频器PLLXTPRE(HSE divider for PLL entry),在这个分频器中,可以通过寄存器配置,选择它的输出。它的输出时钟可以是对输入时钟的二分频或不分频。本例子中,我们选择不分频,所以经过PLLXTPRE后,还是8MHz的时钟。

3、8MHz的时钟遇到开关PLLSRC(PLL entry clock source),我们可以选择其输出,输出为外部高速时钟(HSE)或是内部高速时钟(HSI)。这里选择输出为HSE,接着遇到锁相环PLL,具有倍频作用,在这里我们可以输入倍频因子PLLMUL(PLL multiplication factor),哥们,你要是想超频,就得在这个寄存器上做手脚啦。经过PLL的时钟称为PLLCLK。倍频因子我们设定为9倍频,也就是说,经过PLL之后,我们的时钟从原来8MHz的 HSE变为72MHz的PLLCLK。

4、紧接着又遇到了一个开关SW,经过这个开关之后就是STM32的系统时钟(SYSCLK)了。通过这个开关,可以切换SYSCLK的时钟源,可以选择为HSI、PLLCLK、HSE。我们选择为PLLCLK时钟,所以SYSCLK就为72MHz了。

5、PLLCLK在输入到SW前,还流向了USB预分频器,这个分频器输出为USB外设的时钟(USBCLK)。

6、回到SYSCLK,SYSCLK经过AHB预分频器,分频后再输入到其它外设。如输出到称为HCLK、FCLK的时钟,还直接输出到SDIO外设的SDIOCLK时钟、存储器控制器FSMC的FSMCCLK时钟,和作为APB1、APB2的预分频器的输入端。本例子设置AHB预分频器不分频,即输出的频率为72MHz。

7、GPIO外设是挂载在APB2总线上的, APB2的时钟是APB2预分频器的输出,而APB2预分频器的时钟来源是AHB预分频器。因此,把APB2预分频器设置为不分频,那么我们就可以得到GPIO外设的时钟也等于HCLK,为72MHz了。

5.4.3 HCLK、FCLK、PCLK1、PCLK2

从时钟树的分析,看到经过一系列的倍频、分频后得到了几个与我们开发密切相关的时钟。

SYSCLK:系统时钟,STM32大部分器件的时钟来源。主要由AHB预分频器分配到各个部件。

HCLK:由AHB预分频器直接输出得到,它是高速总线AHB的时钟信号,提供给存储器,DMA及cortex内核,是cortex内核运行的时钟,cpu主频就是这个信号,它的大小与STM32运算速度,数据存取速度密切相关。

FCLK:同样由AHB预分频器输出得到,是内核的“自由运行时钟”。“自由”表现在它不来自时钟 HCLK,因此在HCLK时钟停止时 FCLK 也继续运行。它的存在,可以保证在处理器休眠时,也能够采样和到中断和跟踪休眠事件 ,它与HCLK互相同步。

PCLK1:外设时钟,由APB1预分频器输出得到,最大频率为36MHz,提供给挂载在APB1总线上的外设。

PCLK2:外设时钟,由APB2预分频器输出得到,最大频率可为72MHz,提供给挂载在APB2总线上的外设。

为什么STM32的时钟系统如此复杂,有倍频、分频及一系列的外设时钟的开关。需要倍频是考虑到电磁兼容性,如外部直接提供一个72MHz的晶振,太高的振荡频率可能会给制作电路板带来一定的难度。分频是因为STM32既有高速外设又有低速外设,各种外设的工作频率不尽相同,如同pc机上的南北桥,把高速的和低速的设备分开来管理。最后,每个外设都配备了外设时钟的开关,当我们不使用某个外设时,可以把这个外设时钟关闭,从而降低STM32的整体功耗。所以,当我们使用外设时,一定要记得开启外设的时钟啊,亲。

5.5 LED具体代码分析

有了以上对STM32存储器映像,时钟系统,以及基本的库函数知识,我们就可以分析LED例程的代码了,不知现在你有没饱饱的感觉了,如果还饿,那继续。

5.5.1实验描述及工程文件清单

b2

5.5.2配置工程环境

LED实验中用到了GPIO和RCC(用于设置外设时钟)这两个片上外设,所以在操作I/O之前我们需要把关于这两个外设的库文件添加到工程模板之中。它们分别为stm32f10x_gpio.c 和stm32f10x_rcc.c文件 。其中stm32f10x_gpio.c 用于操作I/O,而stm32f10x_rcc.c用于配置系统时钟和外设时钟,由于每个外设都要配置时钟,所以它是每个外设都需要用到的库文件。

在添加完这两个库文件之后立即编译的话会出错,因为每个外设库对应于一个stm32f10x_xxx.c文件的同时还对应着一个stm32f10x_xxx.h头文件,头文件包含了相应外设的c语言函数实现的声明,只有我们把相应的头文件也包含进工程才能够使用这些外设库。在库中有一个专门的文件stm32f10x_conf.h来管理所有库的头文件,stm32f10x_conf.h 源码如下:

* Includes ------------------------------------------------------------------*/

/* Uncomment the line below to enable peripheral header file inclusion */

/* #include "stm32f10x_adc.h" */

/* #include "stm32f10x_bkp.h" */

/* #include "stm32f10x_can.h" */

/* #include "stm32f10x_crc.h" */

/* #include "stm32f10x_dac.h" */

/* #include "stm32f10x_dbgmcu.h" */

/* #include "stm32f10x_dma.h" */

/* #include "stm32f10x_exti.h" */

/* #include "stm32f10x_flash.h"*/

/* #include "stm32f10x_fsmc.h" */

/* #include "stm32f10x_gpio.h" */

/* #include "stm32f10x_i2c.h" */

/* #include "stm32f10x_iwdg.h" */

/* #include "stm32f10x_pwr.h" */

/* #include "stm32f10x_rcc.h" */

/* #include "stm32f10x_rtc.h" */

/* #include "stm32f10x_sdio.h" */

/* #include "stm32f10x_spi.h" */

/* #include "stm32f10x_tim.h" */

/* #include "stm32f10x_usart.h" */

/* #include "stm32f10x_wwdg.h" */

/*#include "misc.h"*/  /* High level functions for NVIC and SysTick (add-on to CMSIS functions) */

这是没有修改过的代码,默认情况下所有外设的头文件包含都被注释 掉了。当我们需要用到某个外设驱动时直接把相应的注释去掉即可,非常方便。如本LED实验中我们用到了RCCGPIO这两个外设,所以我们应取消其注释,使第13、17行的代码#include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" 这两个语句生效,修改后如下所示:

/* Includes ------------------------------------------------------------------*/

/* Uncomment the line below to enable peripheral header file inclusion */

/* #include "stm32f10x_adc.h" */

/* #include "stm32f10x_bkp.h" */

/* #include "stm32f10x_can.h" */

/* #include "stm32f10x_crc.h" */

/* #include "stm32f10x_dac.h" */

/* #include "stm32f10x_dbgmcu.h" */

/* #include "stm32f10x_dma.h" */

/* #include "stm32f10x_exti.h" */

/* #include "stm32f10x_flash.h"*/

/* #include "stm32f10x_fsmc.h" */

#include "stm32f10x_gpio.h"

/* #include "stm32f10x_i2c.h" */

/* #include "stm32f10x_iwdg.h" */

/* #include "stm32f10x_pwr.h" */

#include "stm32f10x_rcc.h"

/* #include "stm32f10x_rtc.h" */

/* #include "stm32f10x_sdio.h" */

/* #include "stm32f10x_spi.h" */

/* #include "stm32f10x_tim.h" */

/* #include "stm32f10x_usart.h" */

/* #include "stm32f10x_wwdg.h" */

/*#include "misc.h"*/  /* High level functions for NVIC and SysTick (add-on to CMSIS functions) */

到这里,我们就可以用库自带的函数来操作I/O口了,这时我们可以编译一下,会发现既没有Warning也没有Error。

5.5.3编写用户文件

前期工程环境设置完毕,接下来我们就可以专心编写自己的应用程序了。我们把应用程序放在USER这个文件夹下,这个文件夹下至少包含了main.cstm32f10x_it.c、xxx.c这三个源文件。其中main函数就位于main.c这个c文件中,main函数只是用来测试我们的应用程序。stm32f10x_it.c为我们提供了M3所有中断函数的入口,默认情况下这些中断服务程序都为空,等到用到的时候需要用户自己编写。所以现在我们把stm32f10x_it.c包含到USER这个目录可以了。

xxx.c就是由用户编写的文件,xxx是应用程序的名字,用户可自由命名。我们把应用程序的具体实现放在了这个文件之中,程序的实现和应用分开在不同的文件中,这样就实现了很好的封装性。本书的例程都严格遵从这个规则,每个外设的用户文件都由独立的源文件与头文件构成,这样可以更方便地实现代码重用了。

于是,我们在工程中新建两个文件,分别为led.cled.h,保存在USER目录下,并把led.c添加到工程之中。led.c文件中输入代码如下:

/******************** (C) COPYRIGHT 2012 WildFire Team *********

* 文件名  :led.c

* 描述    :led 应用函数库

* 实验平台:野火STM32开发板

* 硬件连接:-----------------

*          |   PC3 - LED1     |

*          |   PC4 - LED2     |

*          |   PC5 - LED3     |

*           -----------------

* 库版本  :ST3.5.0

* 作者    :wildfire team

* 论坛    :www.ourdev.cn/bbs/bbs_list.jsp?bbs_id=1008

* 淘宝    :http://firestm32.taobao.com

***********************************************************/

#include "led.h"

/*

* 函数名:LED_GPIO_Config

* 描述  :配置LED用到的I/O口

* 输入  :无

* 输出  :无

*/

void LED_GPIO_Config(void)

{

/*定义一个GPIO_InitTypeDef类型的结构体*/

GPIO_InitTypeDef GPIO_InitStructure;

/*开启GPIOC的外设时钟*/

RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC, ENABLE);

/*选择要控制的GPIOC引脚*/

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5;

/*设置引脚模式为通用推挽输出*/

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

/*设置引脚速率为50MHz */

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

/*调用库函数,初始化GPIOC*/

GPIO_Init(GPIOC, &GPIO_InitStructure);

/* 关闭所有led灯 */

GPIO_SetBits(GPIOC, GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5);

}

/********* (C) COPYRIGHT 2012 WildFire Team *****END OF FILE********/

在这个文件中,我们定义了一个函数LED_GPIO_Config(),在这个函数里,实现了所有为点亮led的配置。

5.5.4初始化结构体——GPIO_InitTypeDef类型

LED_GPIO_Config()函数中,在文件的第26行的代码:GPIO_InitTypeDef  GPIO_InitStructure; 这是利用库,定义了一个名为GPIO_InitStructure的结构体,结构体类型为GPIO_InitTypeDef。GPIO_InitTypeDef类型与前面介绍的库对寄存器的封装类似,是库文件利用关键字typedef定义的新类型。追踪其定义原型如下,位于stm32f10x_gpio.h文件中:

typedef struct

{

uint16_t GPIO_Pin;             /*指定将要进行配置的GPIO引脚*/

GPIOSpeed_TypeDef GPIO_Speed;  /*指定GPIO引脚可输出的最高频率*/

GPIOMode_TypeDef GPIO_Mode;   /*指定GPIO引脚将要配置成的工作状态*/

}GPIO_InitTypeDef;

于是我们知道,GPIO_InitTypeDef类型的结构体有三个成员,分别为uint16_t类型的GPIO_Pin,GPIOSpeed_TypeDef 类型的GPIO_SpeedGPIOMode_TypeDef类型的GPIO_Mode

uint16_t类型的GPIO_Pin为我们将要选择配置的引脚,在stm32f10x_gpio.h文件中有如下宏定义:

#define GPIO_Pin_0          ((uint16_t)0x0001)  /*!< Pin 0 selected */

#define GPIO_Pin_1          ((uint16_t)0x0002)  /*!< Pin 1 selected */

#define GPIO_Pin_2          ((uint16_t)0x0004)  /*!< Pin 2 selected */

#define GPIO_Pin_3          ((uint16_t)0x0008)  /*!< Pin 3 selected */

这些宏的值,就是允许我们给结构体成员GPIO_Pin赋的值,如我们给GPIO_Pin赋值为宏GPIO_Pin_0,表示我们选择了GPIO端口的第0个引脚,在后面会通过一个函数把这些宏的值进行处理,设置相应的寄存器,实现我们对GPIO端口的配置。如led.c代码中的第32行,意义为我们将要选择GPIO的Pin3、Pin4、Pin5引脚进行配置。

GPIOSpeed_TypeDef 和GPIOMode_TypeDef又是两个库定义的新类型,GPIOSpeed_TypeDef原型如下:

typedef enum

{

GPIO_Speed_10MHz = 1, //枚举常量,值为1,代表输出速率最高为10MHz

GPIO_Speed_2MHz,      //对不赋值的枚举变量,自动加1,此常量值为2

GPIO_Speed_50MHz      //常量值为3

}GPIOSpeed_TypeDef;

这是一个枚举类型,定义了三个枚举常量,即GPIO_Speed_10MHz=1,GPIO_Speed_2MHz=2,GPIO_Speed_50MHz=3。这些常量可用于标识GPIO引脚可以配置成的各个最高速度。所以我们在为结构体中的GPIO_Speed 赋值的时候,就可以直接用这些含义清晰的枚举标识符了。如led.c代码中的第38行,给GPIO_Speed赋值为3,意义为使其最高频率可达到50MHz。

同样,GPIOMode_TypeDef也是一个枚举类型定义符,原型如下:

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;

这个枚举类型也定义了很多含义清晰的枚举常量,是用来帮助配置GPIO引脚的模式的,如GPIO_Mode_AIN意义为模拟输入、GPIO_Mode_IN_FLOATING为浮空输入模式。在led.c代码中的第35行意义为把引脚设置为通用推挽输出模式。

于是,我们可以总结GPIO_InitTypeDef类型结构体的作用,整个结构体包含GPIO_Pin 、GPIO_Speed、GPIO_Mode三个成员,我们对这三个成员赋予不同的数值可以对GPIO端口进行不同的配置,而这些可配置的数值,已经由ST的库文件封装成见名知义的枚举常量。这使我们编写代码变得非常简便。

5.5.5 初始化库函数——GPIO_Init()

在前面我们已经接触到ST的库文件,以及各种各样由ST库定义的新类型,但所有的这些,都只是为库函数服务的。在led.c文件的第41行,我们用到了第一个用于初始化的库函数GPIO_Init()。

在我们应用库函数的时候,只需要知道它的功能及输入什么类型的参数,允许的参数值就足够了,这些我们都可以能通过查找库帮助文档获得,详细方法见0使用库帮助文档小节。查询结果见图 012。

STM32-12

图 012 GPIO_Init函数

这个函数有两个输入参数,分别为GPIO_TypeDefGPIO_InitTypeDef型的指针。其允许值为GPIOA……GPIOG,和GPIO_InitTypeDef型指针变量。

在调用的时候,如led.c文件的第41行,GPIO_Init(GPIOC, &GPIO_InitStructure);第一个参数,说明它将要对GPIOC端口进行初始化。初始化的配置以第二个参数GPIO_InitStructure结构体的成员值为准。这个结构体的成员,我们在调用GPIO_Init()前,已对它们赋予了控制参数。

/*选择要控制的GPIOC引脚*/

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5;

/*设置引脚模式为通用推挽输出*/

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;

/*设置引脚速率为50MHz */

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

于是,在调用GPIO_Init()函数后,GPIOC的Pin3、Pin4、Pin5就被配置成了最高频率为50MHz的通用推挽输出模式了。

在这个函数的内部,实现了把输入的这些参数按照一定的规则转化,进而写入寄存器,实现了配置GPIO端口的功能。函数的实现将在0小节进行详细分析。

5.5.6开启外设时钟

调用了GPIO_Init()函数之后,对GPIO的初始化也就基本完成了,那还缺少什么呢?就是在前面强调过的必须要开启外设时钟,在开启外设时钟之前,我们首先要配置好系统时钟SYSCLK, 0小节提到,为配置SYSCLK,要设置一系列的时钟来源、倍频、分频等控制参数。这些工作由SystemInit()库函数完成。

5.5.6.1启动文件及SystemInit()函数分析

startup_stm32f10x_hd.s启动文件中,有如下一段启动代码:

;Reset_Handler子程序开始

Reset_Handler   PROC

;输出子程序Reset_Handler到外部文件

EXPORT  Reset_Handler            [WEAK]

;从外部文件中引入main函数

IMPORT  __main

;从外部文件引入SystemInit函数

IMPORT  SystemInit

;把SystemInit函数调用地址加载到通用寄存器r0

LDR     R0, =SystemInit

;跳转到r0中保存的地址执行程序(调用SystemInit函数)

BLX     R0

;把main函数调用地址加载到通用寄存器r0

LDR     R0, =__main

;跳转到r0中保存的地址执行程序(调用main函数)

BX      R0

;Reset_Handler子程序结束

ENDP

注:这是一段汇编代码,对汇编比较陌生的读者请配以 ” ; ” 后面的注释来阅读,” ; ”表示注释其后的单行代码,相当于c语言中的” // ” 和 ” /* */ ”。

当芯片被复位(包括上电复位)的时候,将开始运行这一段代码,运行过程为先调用了SystemInit()函数,再进入c语言中的main函数执行。读者是否曾思考过?为什么c语言程序都从main函数开始执行?就是因为我们的启动文件中有了这一段代码,可以尝试一下把第8行引入main函数,及第20行的加载main函数的标识符修改掉,看其效果。如改成:

IMPORT __wildfire

……

LDR R0 ,=__wildfire

这样修改以后,内核就会从wildfire()函数中开始执行第一个c语言的代码啦。有些比较狡猾的朋友就会这么干,让人家看他的代码时找不到main函数,何其险恶呀:)。

但是,前面强调了,进入main函数之前调用了一个名为SystemInit() 的函数。这个函数的定义在system_stm32f10x.c文件之中。它的作用是设置系统时钟SYSCLK。函数的执行流程是先将与配置时钟相关的寄存器都复位为默认值,复位寄存器后,调用了另外一个函数SetSysClock()SetSysClock()代码如下:

static void SetSysClock(void)

{

#ifdef SYSCLK_FREQ_HSE

SetSysClockToHSE();

#elif defined SYSCLK_FREQ_24MHz

SetSysClockTo24();

#elif defined SYSCLK_FREQ_36MHz

SetSysClockTo36();

#elif defined SYSCLK_FREQ_48MHz

SetSysClockTo48();

#elif defined SYSCLK_FREQ_56MHz

SetSysClockTo56();

#elif defined SYSCLK_FREQ_72MHz

SetSysClockTo72();

#endif

/* If none of the define above is enabled, the HSI is used as System clock

source (default after reset) */

}

SetSysClock()代码可以知道,它是根据我们设置的条件编译宏来进行不同的时钟配置的。

system_stm32f10x.c文件的开头,已经默认有了如下的条件编译定义:

#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)

/* #define SYSCLK_FREQ_HSE    HSE_VALUE */

#define SYSCLK_FREQ_24MHz  24000000

#else

/* #define SYSCLK_FREQ_HSE    HSE_VALUE */

/* #define SYSCLK_FREQ_24MHz  24000000 */

/* #define SYSCLK_FREQ_36MHz  36000000 */

/* #define SYSCLK_FREQ_48MHz  48000000 */

/* #define SYSCLK_FREQ_56MHz  56000000 */

#define SYSCLK_FREQ_72MHz  72000000

#endif

在第10行定义了SYSCLK_FREQ_72MHz条件编译的标识符,所以在SetSysClock()函数中将调用SetSysClockTo72()函数把芯片的系统时钟SYSCLK设置为72MHz当然,前提是输入的外部时钟源HSE的振荡频率要为8MHz

其中的SetSysClockTo72() 函数就是最底层的库函数了,那些跟寄存器打交道的活都是由它来完成的,如果大家想知道我们的系统时钟是如何配置成72M的话,可以研究这个函数的源码。但大可不必这样,我们应该抛开传统的直接跟寄存器打交道来学单片机的方法,而是直接用ST的库给我们提供的上层接口,这样会简化我们很多的工作,还能提高我们开发产品的效率,何乐而不为呢?对这一类直接跟寄存器打交道的函数分析在0小节以GPIO_Init()函数为例来分析。

注意:3.5版本的库在启动文件中调用了SystemInit(),所以不必在main()函数中再次调用。但如果使用的是3.0版本的库则必须在main函数中调用SystemInit(),以设置系统时钟,因为在3.0版本的启动代码中并没有调用SystemInit()函数。

5.5.6.2开启外设时钟

SYSCLK由SystemInit()配置好了,而GPIO所用的时钟PCLK2我们采用默认值,也为72MHz。我们采用默认值可以不修改分频器,但外设时钟默认是处在关闭状态的。所以外设时钟一般会在初始化外设的时候设置为开启(根据设计的产品功耗要求,也可以在使用的时候才打开) 。开启和关闭外设时钟也有封装好的库函数 RCC_APB2PeriphClockCmd()。在led.c文件中的第29行,我们调用了这个函数。

查看其使用手册见图 013

STM32-13

图 013 APB2时钟使能函数

调用的时候需要向它输入两个参数,一个参数为将要控制的,挂载在APB2总线上的外设时钟,第二个参数为选择要开启还是关闭该时钟。

led.c文件中对它的调用:RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC, ENABLE);

就表示将要ENABLE(使能)GPIOC外设时钟。

在这里强调一点,如果我们用到了I/O的引脚复用功能,还要开启其复用功能时钟

如GPIOC的Pin4还可以作为ADC1的输入引脚,现在我们把它作为ADC1来使用,除了开启GPIOC时钟外,还要开启ADC1的时钟:

RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC, ENABLE);

RCC_APB2PeriphClockCmd( RCC_APB2Periph_ADC1, ENABLE);

我们知道有的外设是挂载在高速外设总线APB2上使用PCLK2时钟,还有的是挂载在低速外设总线APB1上,使用PCLK1时钟。既然时钟源是不同的,当然也就有另一个函数来开启APB1总线外设的时钟:

RCC_APB1PeriphClockCmd()函数,这两个函数名,正是根据其挂载在的总线命名的。可输入的参数自然也就不一样,使用的时候要注意区分。其中所有的GPIO都是挂载在APB2上的。

5.5.7控制I/O输出高、低电平

前面我们选择好了引脚,配置了其功能及开启了相应的时钟,我们可以终于可以正式控制I/O口的电平高低了,从而实现控制LED灯的亮与灭。

前面提到过,要控制GPIO引脚的电平高低,只要在GPIOx_BSRR寄存器相应的位写入控制参数就可以了。ST库也为我们提供了具有这样功能的函数,可以分别是用GPIO_SetBits()控制输出高电平,和用GPIO_ResetBits()控制输出低电平。见图 014及图 015

STM32-14

图 014 GPIO引脚置1函数

STM32-15

图 015 GPIO引脚清零函数

输入参数有两个,第一个为将要控制的GPIO端口:GPIOA……GPIOG,第二个为要控制的引脚号:Pin0~Pin15。

在led.c文件的第44行,LED_GPIO_Config()函数中,我们在调用GPIO_Init()函数之后就调用了GPIO_SetBits()函数,从而让这几个引脚输出高电平,使三盏LED初始化后都处于灭状态。

5.5.8 led.h文件

接下来,分析led.h文件。其内容如下

#ifndef __LED_H

#define __LED_H

#include "stm32f10x.h"

/* the macro definition to trigger the led on or off

* 1 - off

- 0 - on

*/

#define ON  0

#define OFF 1

//带参宏,可以像内联函数一样使用

#define LED1(a) if (a)  \

GPIO_SetBits(GPIOC,GPIO_Pin_3);\

else        \

GPIO_ResetBits(GPIOC,GPIO_Pin_3)

#define LED2(a) if (a)  \

GPIO_SetBits(GPIOC,GPIO_Pin_4);\

else        \

GPIO_ResetBits(GPIOC,GPIO_Pin_4)

#define LED3(a) if (a)  \

GPIO_SetBits(GPIOC,GPIO_Pin_5);\

else        \

GPIO_ResetBits(GPIOC,GPIO_Pin_5)

void LED_GPIO_Config(void);

#endif /* __LED_H */

这个头文件的内容不多,但也把它独立成一个头文件,方便以后扩展或移植使用。希望读者养成良好的工程习惯,在写头文件的时候,加上类似以下这样的条件编译。

#ifndef __LED_H

#define __LED_H

……

#endif

这样可以防止头文件重复包含,使得工程的兼容性更好。读者问为什么要加两个下划线”__” ?在这里加两个下划线可以避免这个宏标识符与其它定义重名,因为在其它部分代码定义的宏或变量,一般都不会出现这样有下划线的名字。

在led.h头文件的部分,首先包含了前面提到的最重要的ST库必备头文件stm32f10x.h。有了它我们才可以使用各种库定义、库函数。

在led.h文件的第14~27行,是我们利用GPIO_SetBits()、GPIO_ResetBits() 库函数编写的带参宏定义,带参宏与C++中的内联函数作用很类似。在编译过程,编译器会把带参宏展开,在相应的位置替换为宏展开代码。其中的反斜杠符号“ \”叫做续行符,用来连接上下行代码,表示下面一行代码属于“\”所在的代码行,这在ST库经常出现。“\”的语法要求极其严格,在它的后面不能有空格、注释等一切“杂物”,在论坛上经常有读者反映遇到编译错误,却不知道正是错在这里。群里很多朋友都问到“ \”是个什么东西,那野火可要打你pp了,你这是c语言不及格呀,亲。

最后,在led.h文件中的第29行代码,声明 了我们在led.c源文件定义的LED_GPIO_Config()用户函数。因此,我们要使用led.c文件定义的函数时,只要把led.h包含到调用到函数的文件中就可以了。

5.5.9 main文件

写好了led.c、led.h两个文件,我们控制LED灯的驱动程序就全部完成了。接下来,就可以利用写好的驱动文件,在main文件中编写应用程序代码了。本LED例程的main文件内容如下:

/******* (C) COPYRIGHT 2012 WildFire Team **************************

* 文件名  :main.c

* 描述    :LED流水灯,频率可调……

* 实验平台 :野火STM32开发板

* 库版本   :ST3.5.0

*

* 作者    :wildfire team

* 论坛    :www.ourdev.cn/bbs/bbs_list.jsp?bbs_id=1008

************************************************************/

#include "stm32f10x.h"

#include "led.h"

void Delay(__IO u32 nCount);

/*

* 函数名:main

* 描述  :主函数

* 输入  :无

* 输出  :无

*/

int main(void)

{

/* LED 端口初始化 */

LED_GPIO_Config();

while (1)

{

LED1( ON );           // 亮

Delay(0x0FFFEF);

LED1( OFF );          // 灭

LED2( ON );

Delay(0x0FFFEF);

LED2( OFF );

LED3( ON );

Delay(0x0FFFEF);

LED3( OFF );

}

}

void Delay(__IO u32 nCount)  //简单的延时函数

{

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

}

/******* (C) COPYRIGHT 2012 WildFire Team *****END OF FILE********/

main文件的开头部分首先包含所需的头文件,stm32f10x.hled.h

在第14行还声明了一个简单的延时函数,其定义在main文件的末尾。它是利用for循环实现的,用作短暂的,对精度要求不高的延时,延时的时间与输入的参数并无准确的计算公式,请不要深究。需要精准的延时的时候,我们会采用定时器来精确控制。

在芯片上电(复位)后,经过启动文件中SystemInit()函数配置好了时钟,就进入main函数了。接下来,从main函数开始分析代码的执行。

首先,调用了在led.c文件编写好的LED_GPIO_Config()函数,完成了对GPIOC的Pin3、Pin4、Pin5的初始化。紧接着就在while死循环里不断执行在led.h文件中编写的带参宏代码,并加上延时函数,使各盏LED轮流亮灭。当然,在LED控制的部分,如果不习惯带参宏的方式,读者也可以直接使用GPIO_SetBits()GPIO_ResetBits()函数实现对LED的控制。

如果使用的是3.0版本 的库,由于启动文件中没有调用SystemInit() 函数,所以要在初始化GPIO等外设之前,也就是在main函数的第1行代码,就调用SystemInit()函数,以完成对系统时钟的配置。

到此,我们整个控制LED灯的工程的讲解就完成了。

5.5.10 实验现象

将程序烧写到野火STM32开发板中,即可看到3个LED一定的频率闪烁。

5.6 GPIO_Init()函数的实现

在我们控制LED灯的工程中,调用了很多库函数,有SystemInit()、GPIO_Init()、GPIO_SetBits()、GPIO_ResetBits()等等。虽说为了开发速度,我们只管函数的功能和如何调用就行了,但免不了有种不踏实的感觉。

所以在本小节以GPIO_Init()函数实现的分析为例,可以帮助读者理解ST库的本质,让读者在使用库开发的时候心里更有底。

5.6.1规范的位操作方法

由于库函数的实现涉及到不少位操作,首先为读者介绍一下几个常用的位操作方法,排除阅读代码的障碍。

1、将char型变量a的第七位(bit6)清0,其它位不变。

a &= ~(1<<6);   //括号内1左移6位,得二进制数:0100 0000

//按位取反,得1011 1111 ,所得的数与a作”位与&”运算,

// a的第7位(bit6)被置零,而其它位不变。

2、同理,将变量a的第七位(bit6)置1,其它位不变的方法如下。

a |= (1<<6);        //把第七位(bit6)置1,其它为不变

3、将变量a的第七位(bit6)取反,其它位不变。

a ^=(1<<6);   //把第七位(bit6)取反,其它位不变

5.6.2 GPIO_Init()实现代码分析

有了上面的位操作知识准备后,就可以分析GPIO_Init()函数的定义代码了。

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)

{

uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00;

uint32_t tmpreg = 0x00, pinmask = 0x00;

/* 断言,用于检查输入的参数是否正确 */

assert_param(IS_GPIO_ALL_PERIPH(GPIOx));

assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode));

assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin));

/*---------------------------- GPIO的模式配置 -----------------------*/

/*把输入参数GPIO_Mode的低四位暂存在currentmode*/

currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);

/*判断是否为输出模式,输出模式,可输入参数中输出模式的bit4位都是1*/

if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00)

{

/* 检查输入参数 */

assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed));

/* 输出模式,所以要配置GPIO的速率:00(输入模式) 01(10MHz) 10(2MHz) 11 */

currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;

}

/*----------------------------配置GPIO的CRL寄存器 ------------------------*/

/* 判断要配置的是否为pin0 ~~ pin7 */

if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)

{

/*备份原CRL寄存器的值*/

tmpreg = GPIOx->CRL;

/*循环,一个循环设置一个寄存器位*/

for (pinpos = 0x00; pinpos < 0x08; pinpos++)

{

/*pos的值为1左移pinpos位*/

pos = ((uint32_t)0x01) << pinpos;

/* 令pos与输入参数GPIO_PIN作位与运算,为下面的判断作准备 */

currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;

/*判断,若currentpin=pos,说明GPIO_PIN参数中含的第pos个引脚需要配置*/

if (currentpin == pos)

{

/*pos的值左移两位(乘以4),因为寄存器中4个寄存器位配置一个引脚*/

pos = pinpos << 2;

/*以下两个句子,把控制这个引脚的4个寄存器位清零,其它寄存器位不变*/

pinmask = ((uint32_t)0x0F) << pos;

tmpreg &= ~pinmask;

/* 向寄存器写入将要配置的引脚的模式 */

tmpreg |= (currentmode << pos);

/* 复位GPIO引脚的输入输出默认值*/

/*判断是否为下拉输入模式*/

if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)

{

/*下拉输入模式,引脚默认置0,对BRR寄存器写1可对引脚置0*/

GPIOx->BRR = (((uint32_t)0x01) << pinpos);

}

else

{

/*判断是否为上拉输入模式*/

if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)

{

/*上拉输入模式,引脚默认值为1,对BSRR寄存器写1可对引脚置1*/

GPIOx->BSRR = (((uint32_t)0x01) << pinpos);

}

}

}

}

/*把前面处理后的暂存值写入到CRL寄存器之中*/

GPIOx->CRL = tmpreg;

}

/*---------------------------- 以下部分是对CRH寄存器配置的 -----------------

--------当要配置的引脚为pin8 ~~ pin15的时候,配置CRH寄存器,-----

-------------   -----这过程和配置CRL寄存器类似------------------------------------

-------读者可自行分析,看看自己是否了解了上述过程--^_^-----------*/

/* Configure the eight high port pins */

if (GPIO_InitStruct->GPIO_Pin > 0x00FF)

{

tmpreg = GPIOx->CRH;

for (pinpos = 0x00; pinpos < 0x08; pinpos++)

{

pos = (((uint32_t)0x01) << (pinpos + 0x08));

/* Get the port pins position */

currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);

if (currentpin == pos)

{

pos = pinpos << 2;

/* Clear the corresponding high control register bits */

pinmask = ((uint32_t)0x0F) << pos;

tmpreg &= ~pinmask;

/* Write the mode configuration in the corresponding bits */

tmpreg |= (currentmode << pos);

/* Reset the corresponding ODR bit */

if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)

{

GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));

}

/* Set the corresponding ODR bit */

if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)

{

GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));

}

}

}

GPIOx->CRH = tmpreg;

}

}

这部分代码比较长,请读者配合代码中的注释,《STM32中文参考手册》中的CRL寄存器的说明图 016,及错误!未找到引用源。来理解这个函数。

STM32-16

图 016 GPIOx_CRL寄存器

STM32-17

   图 017 GPIO_Init分析

以我们led.c文件中对GPIO_Init()函数的调用为例。在调用函数前:

1、led.c代码的32行,对 GPIO_InitStructure.GPIO_Pin 结构体成员赋值为GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5,宏展开为(0000 0000 0011 1000)B,表明我们将要对这三个引脚进行配置。

2、第35行,对.GPIO_Mode 赋值为GPIO_Mode_Out_PP,宏展开为(0001 0100)B,表明我们要把这三个引脚都设置为通用推挽模式。

3、第38行,对.GPIO_Speed赋值为GPIO_Speed_50MHz,宏展开为(0011)B,表明我们设置这三个引脚的输出最大速度都为50MHz。

led.c的第41行调用GPIO_Init()的时候,就把GPIOC和上面这三个参数输入到函数了,经过这个函数处理,最终它向GPIOC组的CRL配置寄存器写入了一个值:

GPIOC->CRL = 0x44333444;

//二进制表示为(0100 0100 0011 0011 0011 0100 0100 0100)

把这个值化为二进制为:(0100 0100 0011 0011 0011 0100 0100 0100)B;

这个值的每4个二进制位代表一组引脚的控制值。Pin3、Pin4、Pin5的控制值都是(0011) B,有心的读者可以对比一下CRL寄存器的说明,这些控制值正好可以把GPIO设置为符合我们输入参数要求的状态,为最大速率为50MHz的通用推挽输出模式。

5.6.3 再论开发方式

了解库函数的实现后,我们现在就可以用实例来分析使用库函数与直接配置寄存器的区别了。

用直接配置寄存器的方法,只需要一个语句:

GPIOC->CRL = 0x44333444;

这样直接向寄存器赋值就完成了,以这样的方式配置是内核执行效率最高的方式,内核的工作是简单了,但我们为实现所需的配置,确定这样的一个值,却是一件麻烦事,工程量大的时候,缺点就显而易见了。

配置寄存器还可以用一些相对缓和的方法,前面提到的三种位操作方式。如:

GPIOC->CRL &=~(uint32_t)(1111<<4*3); //清空Pin3的4个控制位

GPIOC->CRL |=(uint32_t)(0011<<4*3);  //配置Pin3的4个控制位

GPIOC->CRL &=~(uint32_t)(1111<<4*4); //清空Pin4的4个控制位

GPIOC->CRL |=(uint32_t)(0011<<4*4);  //配置Pin4的4个控制位

GPIOC->CRL &=~(uint32_t)(1111<<4*5); //清空Pin5的4个控制位

GPIOC->CRL |=(uint32_t)(0011<<4*5);  //配置Pin5的4个控制位

这个方法也可以实现我们所需的配置,而且修改起来比较容易,但执行的效率就比第一个方法要低了。

最后就是我们的调用库函数的方法,从内核的执行效率上看,首先库函数在被调用的时候要耗费调用时间;在函数内部,把输入参数转换为可以直接写入到寄存器的值也耗费了一些运算时间。而其它的宏、枚举等解释操作是作编译过程完成的,这部分并不消耗内核的时间。而优点呢?则是我们可以快速上手STM32控制器;配置外设状态时,不需要再纠结要向寄存器写入什么数值;交流方便,查错简单。这就是我们选择库的原因。

现在的处理器的主频是越来越高,我们需不需要担心cpu耗费那么多时间来干活会不会被累倒,野火要告诉你的是,不需要,还是担心下自己字字查询datasheet会不会被累倒吧。

至此,我们就把GPIO_Init()库函数的实现分析完毕了。分析它纯粹是为了满足自己的求知欲,学习其编程的方式、思想,这对提高我们的编程水平是很有好处的,顺便感受一下ST库设计的严谨性,野火认为这样的代码不仅严谨且华丽优美,不知读者你是否也有这样的感受。就像野火在论坛里面说过:要我操作寄存器,我宁愿回家种田。

我们在以后开发的工程中,一般不会去分析ST的库函数的实现了。因为这些库函数是很类似的,都是把原来封装好的宏或枚举标识符转化成相应的值,写入到寄存器之中。这些都是十分枯燥和机械的工作,既然我们已经知道它的原理,又有现成的函数可供调用,就没必要再去探究了。

到了这里流水灯这个例程就算讲完了,如果你搞明白了流水灯编程的来龙去脉,那么后面的M3的学习路程将会简单而有趣。后面的例程也不再会像这个例程那么详细,所以大家要重点把握《4、初始STM32库》和《5、流水灯的前后今生》,把库的编程思想了然于胸。


  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值