TA的每日心情 | 慵懒 2015-5-29 12:01 |
---|
签到天数: 11 天 连续签到: 1 天 [LV.3]偶尔看看II
|
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公司的工程师采用了更巧妙的方式来确定这些地址,请看下一小节&mdash;&mdash;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.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实验中我们用到了RCC跟GPIO这两个外设,所以我们应取消其注释,使第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.c、stm32f10x_it.c、xxx.c这三个源文件。其中main函数就位于main.c这个c文件中,main函数只是用来测试我们的应用程序。stm32f10x_it.c为我们提供了M3所有中断函数的入口,默认情况下这些中断服务程序都为空,等到用到的时候需要用户自己编写。所以现在我们把stm32f10x_it.c包含到USER这个目录可以了。
而xxx.c就是由用户编写的文件,xxx是应用程序的名字,用户可自由命名。我们把应用程序的具体实现放在了这个文件之中,程序的实现和应用分开在不同的文件中,这样就实现了很好的封装性。本书的例程都严格遵从这个规则,每个外设的用户文件都由独立的源文件与头文件构成,这样可以更方便地实现代码重用了。
于是,我们在工程中新建两个文件,分别为led.c和led.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
* 淘宝 :https://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初始化结构体&mdash;&mdash;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_Speed及GPIOMode_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 初始化库函数&mdash;&mdash;GPIO_Init()
在前面我们已经接触到ST的库文件,以及各种各样由ST库定义的新类型,但所有的这些,都只是为库函数服务的。在led.c文件的第41行,我们用到了第一个用于初始化的库函数GPIO_Init()。
在我们应用库函数的时候,只需要知道它的功能及输入什么类型的参数,允许的参数值就足够了,这些我们都可以能通过查找库帮助文档获得,详细方法见0使用库帮助文档小节。查询结果见图 012。
图 012 GPIO_Init函数
这个函数有两个输入参数,分别为GPIO_TypeDef和GPIO_InitTypeDef型的指针。其允许值为GPIOA&hellip;&hellip;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
&hellip;&hellip;
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&hellip;&hellip;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
&hellip;&hellip;
#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流水灯,频率可调&hellip;&hellip;
* 实验平台 :野火STM32开发板
* 库版本 :ST3.5.0
*
* 作者 :wildfire team
* 论坛 :www.ourdev.cn/bbs/bbs_list.jsp?bbs_id=1008
* 淘宝 :https://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.h和led.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一定的频率闪烁。
<span style="color: #3366ff;"><strong>5 |
|