零死角玩转stm32-初级篇之流水灯的前后今生

作者:
上传时间为: 2013-07-08 04:56 PM

    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

图 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错误!未找到引用源

图 52

图 53

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

图 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。

图 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;

这些定义被称为地址映射

所谓地址映射,就是将芯片上的存储器 甚至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

图 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总线,挂载在这些总线上的外设有特定的地址范围。

图 58

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

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

见表:

由这个表我们可以知道,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,看到寄存器说明如下图:

图 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 结构体内其它变量的偏移量,也和相应的寄存器偏移地址相符。于是,只要我们匹配了结构体的首地址,就可以确定各寄存器的具体地址了。

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

#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

图 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实验描述及工程文件清单

5.5.2配置工程环境

LED实验中用到了GPIO和RCC(用于设置外设时钟)这两个片上外设,所以在操作I/O之前我们需要把关于这两个外设的库文件添加到工程模板之中。它们分别为stm32f10x_gpio.cstm32f10x_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。

图 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

图 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

图 014 GPIO引脚置1函数

图 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

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

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

#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()函数的实现

全部评论 ()

创建讨论帖子

登录 后参与评论
系统提示