STM32F1单片机项目工程管理指南

概述

        一个keil项目中,有很多的文件.c和.h文件,这些文件有我们自己创建的,也有我们经常所说的官网的库。然后在工程里面实现各种逻辑运算,算法等等。但这些最终都是为了控制单片机的输入输出。

        对单片机的操作,其实就是对单片机内存地址,寄存器地址进行读写功能的操作。那么我们在写程序的时候,很少直接去对寄存器相关的地址进行操作,而是直接使用官方库,进行调用相关的函数即可。并且是使用到那个外设,就在工程里面添加对于的源文件.c和.h文件就可以。那么这些官网提供方库的底层逻辑是什么呢?各种文件之间的关系又是怎么样的呢?

一、.c与.h文件

        .h文件是用来声明的,声明变量、声明函数、声明结构体、声明枚举等。声明,是给外部调用的接口,声明仅仅是声明,不做任何实例化功能的过程。但是.h文件不局限于声明,还可以定义宏这些。

        .c文件是用来声明和定义的,可以有和.h文件一样的声明功能,还可以定义在.h文件里面的声明的东西和.c文件自己声明的东西。可以定义变量、定义函数、定义结构体、定义枚举等。定义是指具体的实例化,比如在函数里面实现的功能逻辑等。

        我们都知道,在.h文件里面声明,然后在.c文件里面进行定义和调用。接着在其他.c里面就可以调用。我们在编写的时候也是这样的,写一个具体的功能模块时,有一个.h和一个.c文件。吧需要声明的声明放在.h文件里面进行声明,然后在.c文件里面包含.h文件,在.c文件里对已经声明的一些东西进行定义或者调用。

        如LED.h和LED.c,主要实现LED相关功能的模块。在LED.h里面声明一个函数LED_INIT();然后在LED.c文件里进行定义,具体写这个函数的功能。

        那么.h和.c之间是什么关系呢?其实.h和.c之间没有什么必然的联系,并不是说有一个.h文件就需要有一个对应的.c文件。.c文件是通过包含.h文件后(#include ****.h),.c文件才有权限去访问.h文件里面声明的接口的(访问可以是定义与调用)。换句话说,不论什么名字的.c文件,只要包含了.h文件,那么这个.c文件都可以去定义或调用.h文件里面声明的东西。只是通常我们会统一在一个.c文件里面进行定义(就是我们理解的.h对应的.c文件),然后在其它.c文件中调用。

        如创建了一个LED.h文件,在文件里面进行声明了extern int LED_FLAG_U8;这个变量和void LED_INIT()函数。

        然后还有一个LED.c和一个main.c两个文件,并且在这两个.c文件中都包含了LED.h文件。那这时候我们就可以在LED.c文件中定义.h文件里面声明的东西。也可以在main.c文件中定义.h文件里面声明的东西(只能二选一,一个声明,只能对应一次定义,多次调用。)或者分开定义也行,如在LED.c文件中定义LED_FLAG_U8这个变量,然后在main.c文件中定义LED_INIT()这个函数也可以的。

        但是为了程序的规范性与可读性以及移植性。一般都是在LED.c文件中全部定义了。然后在main.c文件以及其它包含了LED.h文件的.c文件中去调用。这样似乎就看成了LED.h和LED.c这两个是一对的了。其实他们的实际关系并不是的。因为有没有LED.c文件都行,或者是其它.c文件都行,只要包含了LED.h文件,就可以去访问(定义或者访问)LED.h里面声明的东西。

        一个.c文件只要包含了.h文件,那这个.c文件就可以去定义或者调用.h文件里面声明的东西了。所以一个.c文件不单只包含一个.h文件,可以包含多个.h文件,都可以去定义或者调用各.h文件里面声明的东西,不受限制。

        由前面的介绍,我们知道在一个.h文件里面声明的东西。在其它.c文件里只要包含了.h文件,那这个.c文件就可以去定义或者调用.h文件里面声明的东西。

        那么我们有见过在一个.c文件里面包含另外一个.c文件的吗?那肯定没有的,也不支持。

        我们知道,在.c文件里也是可以声明东西,如函数这些等。所以在.c文件里面声明的东西只能在.c文件内进行定义与调用。在其它.c文件中无法对其进行定义与调用。

        需要值得注意的是:在.h文件里面声明的东西,需要先进行定义,再能调用,只需要定义一次就可以在不论那个.c文件都可以调用了。并且是不论在那个.c文件中定义都行。只要定义与调用的.c文件包含了.h文件即可。

        .c文件不支持被其它.c文件包含,而.h不仅支持在.c文件中被包含,也支持在其它.h文件中被包含。如LED.h文件中包含了KEY.h和Time.h两个文件。如果我在LED.c文件中包含了LED.h文件,那此时在LED.c文件中不仅可以去定义LED.h里面声明的东西,也可以直接去定义或调用KEY.h和Time.h里面声明的东西的。而不需要在LED.c文件中去包含KEY.h与Time.h了。因为在LED.h中包含了其它.h,那LED.h这个文件就有权限去定义或者调用被包含的.h里面声明的东西了。所以在.c文件中去包含了LED.h的时候,也有了这个权限了。

        那.h文件能否包含自己呢?答案是可以的,起码编译的时候不会报错。这样就演变成了两个.h文件直接是可以相互包含。比如在LED.h里面包含了KEY.h。然后同时在KEY.h中包含LED.h。在一些库中就是这样的。

二、STM32F1标准库(外设库)

2.1、概述

        我们知道,单片机中有很多的外设,如ADC、GPIO、串口、SPI等等。这些外设都有对应的寄存器,然后寄存器在内存中有对应的地址,一般把这些地址叫做寄存器地址。我们需要操作相关外设是就是去对相关的寄存器地址进行一个读写。如果我们直接去对这些寄存器地址进行读写时,在程序的执行效率来说是最快的。但是对于开发人员来说是最麻烦的。因为需要清楚的知道每个外设都有什么寄存器,然后这些寄存器都有什么作用,以及这些寄存器地址在内存中的地址是什么这些。一系列下来对开发人员的要求很高,难度大。

        所以芯片的官方就给出了一套库,这套库就是对外设寄存器地址的封装,我们在开发时,不在去需要关注一个外设都包含什么寄存器,寄存器的作用是什么,它们的地址是多少等这些信息。我们只需要了解库里面封装了什么函数,函数的形参需要填写什么信息就可以了。通过调用库的函数,就能帮我们吧底层的相关寄存器进行读写了。

        官网提供的库有很多种,比如针对软件上实现系统的逻辑库(RTOS库)、一些显示屏的驱动库、以及对外设寄存器进行操作的外设库。我们这里讲解的是外设库。不包含单片机的启动文件这些的讲解。

        所谓官方提供的外设标准库,只不过是芯片厂家为了开发人员的开发更加方便、快捷难度不能那么大。对外设寄存器的操作进行一个封装而已。只不过这些封装都比较完善,可靠性强,移植性与阅读性高等。这些库也只是普通的.c与.h文件,只要大家了解了上面介绍的.c与.h文件的原理,那么接下来介绍的官方库的框架才会有更好的了解。

2.2、标准外设库的文件框架

        以下是一个工程文件中,标准库有点几个文件。

        说明:一个单片机的外设标准库就放在CMSIS和STM32F10x_StdPeriph_Driver两个文件夹中。两个文件夹各自的有点文件如下。

        CMSIS问价如下。

     说明:这个文件夹下面的文件有好几个,其中属于外设库的只有框中的两个,stm32f10x.h与stm32f10x_conf.h这两个头文件。其它的暂时不管。

STM32F10x_StdPeriph_Driver文件夹如下。包含两个子文件夹。

说明:其中inc文件夹是头文件夹,它下面包含了单片机所有外设的对应的库函数的.h文件。如下

src文件夹是.c文件的文件夹,它包含了单片机所有外设的对应的库函数的.c文件。如下。

        

从上面的文件夹介绍得出,一个标准的外设库,由很多的.c与.h文件组成。主要分成三部分。

(1):stm32f10x.h文件。

(2):stm32f10x_conf.h文件。

(3):以及每种外设对应的一个.h和一个.c文件。

2.2.1、stm32f10x.h文件文件内容

        我们知道,单片机中有很多种的外设,每一种外设又有很多个,具体多少,得看单片机的资源。同一种外设,虽然有多个,但是每一个的资源基本都是相同的(这里说的资源是指寄存器以及寄存器的功能)。只是地址不同而已。比如GPIO外设,这种外设又有GPIOA、GPIOB等多个外设。

        而在stm32f10x.h文件中,就主要是对所有外设种类都自定义成了一个结构体类型。一种外设定义成一个结构体。结构体名就是外设名,结构体成员变量就是外设寄存器。包括成员变量名都是根据外设寄存器名来定义的,并且成员顺序也是根据外设的寄存器在内存中排布的顺序。如下

        根据外设名与外设寄存器定义好结构体后,就根据每一种外设的n个外设的外设名来定义结构体类型的地址。每一个结构体类型地址都代表一个外设。然后把对应外设的基地址强转成对应类型的结构体类型。如下。吧各组GPIO外设的基地址强转成对应外设结构体类型的指针。然后赋值给宏定义中。

        

        当然,stm32f10x.h文件除了把各种类型外设都定义成了结构体与宏定义好各个外设的基地址外。还有其它很多内容。比如吧所有外设的寄存器以及对应的地址的位带操作地址都一一的重映射了出来。如下

最后,stm32f10x.h文件还包含了stm32f10x_conf.h文件,如下。这个不用多说了吧,包含了stm32f10x_conf.h文件,那么在其它文件中包含stm32f10x.h文件时,也是可以直接访问stm32f10x_conf.h里的东西的。

        

2.2.2、stm32f10x_conf.h文件内容

        这是一个所有外设对应的.h文件的集合文件,如下,

说明:这个.h文件仅仅是包含了所有的外设对应的.h文件。而这个stm32f10x_conf.h文件又包含在了stm32f10x.h这个文件中。这样的好处是方便管理,开发人员只需要调用stm32f10x.h一个文件,就可以去访问(定义(这里使用的更多的是调用))所有外设的东西了。就不需要一个个的外设.h文件去包含添加进来。

2.2.3、每种外设对应的一个.h和一个.c文件内容

        在stm32f10x.h文件中,我们已经知道这个文件是对各种外设进行封装成了一个个的结构体,以及对每一个外设的基地址都重映射了,这是全部外设的,是属于公共的,所以每一个种外设的.h文件都必须去包含stm32f10x.h文件了。那么每种外设对应的一个.h和一个.c文件所做的事情就是针对各自外设的寄存器对应定义的结构体变量如何进行读写的操作了。

        我们拿GPIO来说即可。如下

        在stm32f10x_gpio.h文件中就包含了一个stm32f10x.h文件。其它的就没有任何包含了。这里包含stm32f10x.h文件的目的,主要是能够去调用stm32f10x.h文件里面定义的GPIO外设对应的一个结构体和结构体变量这些东西。

        然后在这个.h文件中,还声明了很多的东西,如枚举、结构体、函数等等,如下。

        

        然后对应的stm32f10x_gpio.c文件,这个.c文件主要包含了下面两个.h文件。

        说明:其实stm32f10x_rcc.h文件不用包含也是可以的,因为在stm32f10x_conf.h文件中已经包含了这个头文件,并且这个头文件包含在了stm32f10x.h文件中,而且在stm32f10x_gpio.h文件中已经stm32f10x.h文件,所以在stm32f10x_gpio.c文件保护了stm32f10x_gpio.h文件,也是可以直接去访问stm32f10x_rcc.h文件的东西的。

        stm32f10x_gpio.c这个文件主要是对stm32f10x_gpio.h文件中声明的东西进行实例化。至于stm32f10x_gpio.h文件里面有什么声明,就主要是根据GPIO这个外设需要到对什么寄存器地址进行读写来声明的。比如让GPIO的某个引脚输出一个高电平,就主要去是去操作对应寄存器的写了。如下定义的一个函数。

这个函数就是对BSRR寄存器对应的位写1。我们通过看手册可以知道,BSRR寄存器是属于GPIO外设里面的一个寄存器。然后对应的程序结构体里面也定义了一个相同名字的结构体成员变量。

        我们通过函数的形参可以看出,第一个形参是GPIO_TypeDef这个结构体类型的指针,所以我们只需要传入在stm32f10x.h文件中使用宏定义定义重映射出对应GPIO的基地址就可以了,如GPIOA。然后第二个形参是一个u16行的值,是一个普通的数据类型。这个形参是往BSRR这个变量里面写的值,范围是0到15,对应的是BSRR这个寄存器地址的0到15位。

        如我要GPIOA的第0脚输出高电平,只需要调用这个函数,并传入GPIOA,0即可。如下。

        

        

而不需要我们去直接操作这个GPIOA外设的对应的BSRR这个寄存器的地址去往第0位写1。如下

这两种方式对比,是不是直接使用库函数更方便呢。只要根据函数名的大致意思以及传入的形参就能大概知道这个函数是对这个外设做什么了。而不需要去看一堆地址。

总结:关于官网提供的标准外设库,外设库的文件只要包括三部分,(1)stm32f10x.h公共的头文件;(2)stm32f10x_conf.h各外设头文件的集合。(3)每种外设对应的一个.h和一个.c文件。

其中每种外设对应的一个.h文件都需要依赖于stm32f10x.h这个公共的文件。

三、自定义的外设模块

        根据上面介绍的,已经给我把外设的寄存器都封装好了成了相应的结构体,结构体变量和函数了。但是封装好后,当我们要去操作相关的外设时,是需要我们自己去调用相关函数的。比如我们要操作GPIO这个外设,那么我们需要对这个GPIO对应的引脚进行初始化,和往这个引脚上面输出高低电平这些功能。

        此时按照我们的习惯,就会定义两个文件,包含了一个.h文件和一个.c文件。如下。

        说明:创建了led.h和led.c两个文件,其中,led.h文件仅仅包含了stm32f10x.h这个文件。不需要我们去包含stm32f10x_gpio.h这个文件,也可以直接调用stm32f10x_gpio.h里面的东西。因为在stm32f10x.h函数中已经包含了stm32f10x_conf.h文件了。并且在stm32f10x_conf.h里面包含了各种外设的.h文件了。所以只需要包含stm32f10x.h就可以访问所有外设的.h文件了。

        接着在led.c文件里面包含了led.h文件,对led.h文件里面声明的函数进行了实例化。并且在led.c文件里面可以直接调用外设库对应的.h文件的东西了。

然后在其他.c文件中包含led.h文件,就可以调用led.h里面声明的函数了。如下

        

上述就是官方标准外设库的封装框架,文件管理和我们自己定义的模块的一个项目中的文件管理过程。当然,在一些比较规范的项目上面,不完全会是想我们一样一个模块对应一个.h和一个.c文件的。更多的会是想标准外设库一样的管理文件模块的模式,一个.h文件或者多个.h文件都包含在一个集合里面。然后一个.c文件,包含多个.h文件,对多个.h文件里面声明的东西进行定义和调用的。

四、标准库里面的命名规则

4.1、stm32f10x.h文件

        我们知道,stm32f10x.h文件主要是对各种外设定义的对应的各种结构体,以及对各个外设的基地址重映射的关系。

        在定义结构体时,结构体名称都是根据外设类型来命名的,比如GPIO类型外设,定义的结构如下。使用GPIO_TypeDef来命名。而且结构体成员变量的名称也是直接和外设中包含的寄存器名称是一样的。这样看起来就非常的直观了。

        然后在对各个外设的基地址进行重映射时,也是直接使用各个外设的名字来的,如下。

        这样映射外设GPIOA的基地址时,也是直接使用GPIOA这个名字来。因为结构成员的排序是和外设各寄存器地址在内存中的顺序来的。所以根据重映射的基地址,然后去访问成员的偏移量就是直接去访问外设的相关寄存器了。

地址重映射格式如下:

重映射符号  = 外设基地址(所有外设的基地址)+总线基地址+具体外设店基地址

4.2、每种外设对应的一个.h和.c文件

        在.h里面声明的各种结构体或者函数,都是根据要实现一个什么功能来命名的。如对一个IO进初始化时,需要对IO设置成什么状态。需要对什么寄存器里面写入什么值。就有如下定义。

        说明:根据结构体名GPIO_InitTypeDef,可以大概得出,这是一个GPIO的初始化的结构体,里面成员反应了初始化一个GPIO需要有设置三个值,分别是引脚、速度、还有模式。这三个值的类型基本都是枚举类型的。就是往相关寄存器写入的目标值。

        然后声明了一个初始化函数,就是吧上面结构体的值写入到对应寄存器里面的。如下

上面的命名,就让人很容易的大致知道这个结构体是干嘛的,这个函数是干嘛的。而不是随便乱启的名字。

        在如我们需要某IO输出高电平,那么这个在GPIO外设中是有相关一个寄存器的。然后官方对这个寄存器的封装如下。

我们要操作一个IO输出高电平,就需要把寄存器BSRR的相关位置1。然后官方对这个寄存器的封装函数名字是没有直接与BSRR有明确关系的。直接声明成GPIO_SetBits(),理解成GPIO_设置位。把GPIO的相关位置1的理解。以及对IO进行初始化时,也没有在命名上体现出是对什么寄存器进行操作。而是根据功能来命名。

这样就是程序的命名规范原则了。简洁明了通熟易懂。

更多精彩内容,请持续关注下一篇!

        

作者:小沈vlog

物联沃分享整理
物联沃-IOTWORD物联网 » STM32F1单片机项目工程管理指南

发表评论