野火学习笔记(4) —— 固件库




个人感觉了解一下就可以了




- 自己写库—构建库函数雏形

1. 什么是 STM32 函数库

以上所说的固件库是指 “STM32 标准函数库” ,它是由 ST 公司针对 STM32 提供的函数接口,即 API (Application Program Interface),开发者可调用这些函数接口来配置 STM32 的寄存器,使开发人员得以脱离最底层的寄存器操作,有开发快速,易于阅读,维护成本低等优点。

当我们调用库 API 的时候不需要挖空心思去了解库底层的寄存器操作,就像当年我们刚开始学习 C 语言的时候,用 prinft() 函数时只是学习它的使用格式,并没有去研究它的源码实现,但需要深入研究的时候,经过千锤百炼的库源码就是最佳学习范例。

实际上, 库是架设在寄存器与用户驱动层之间的代码,向下处理与寄存器直接相关的配置,向上为用户提供配置寄存器的接口。 库开发方式与直接配置寄存器方式的区别见 图 9-1。


在这里插入图片描述图 9-1 固件库开发与寄存器开发对比图



2. 为什么采用库来开发及学习?

在以前 8 位机时代的程序开发中, 一般直接配置芯片的寄存器,控制芯片的工作方式,如中断,定时器等。配置的时候, 常常要查阅寄存器表,看用到哪些配置位,为了配置某功能,该置 1 还是置 0。这些都是很琐碎的、机械的工作,因为 8 位机的软件相对来说较简单,而且资源很有限,所以可以直接配置寄存器的方式来开发。

对于 STM32,因为外设资源丰富,带来的必然是寄存器的数量和复杂度的增加,这时直接配置寄存器方式的缺陷就突显出来了:

① 开发速度慢
② 程序可读性差
③ 维护复杂

这些缺陷直接影响了开发效率,程序维护成本,交流成本。库开发方式则正好弥补了这些缺陷。

而坚持采用直接配置寄存器的方式开发的程序员,会列举以下原因

① 具体参数更直观
② 程序运行占用资源少

相对于库开发的方式,直接配置寄存器方式生成的代码量的确会少一点,但因为 STM32 有充足的资源,权衡库的优势与不足,绝大部分时候,我们愿意牺牲一点 CPU 资源,选择库开发。一般只有在对代码运行时间要求极苛刻的地方,才用直接配置寄存器的方式代替,如频繁调用的中断服务函数。

对于库开发与直接配置寄存器的方式,就好比编程是用汇编好还是用 C 好一样。在 STM32F1 系列刚推出函数库时引起程序员的激烈争论,但是,随着 ST 库的完善与大家对库的了解,更多的程序员选择了库开发。 现在 STM32F1 系列和 STM32F4 系列各有一套自己的函数库,但是它们大部分是兼容的, F1 和 F4 之间的程序移植,只需要小修改即可。而如果要移植用寄存器写的程序, 那简直跟脱胎换骨差不多。



3. 实验:构建库函数雏形

3.1 外设寄存器结构体定义

上一章中我们在操作寄存器的时候,操作的是都寄存器的绝对地址,如果每个外设寄存器都这样操作,那将非常麻烦。我们考虑到外设寄存器的地址都是基于外设基地址的偏移地址,都是在外设基地址上逐个连续递增的,每个寄存器占 32 个字节,这种方式跟结构体里面的成员类似。所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一样。这样我们操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。

在工程中的 “stm32f10x.h” 文件中,我们使用结构体封装 GPIO 及 RCC 外设的的寄存器,见 代码清单 9-1。结构体成员的顺序按照寄存器的偏移地址从低到高排列,成员类型跟寄存器类型一样。


代码清单 9-1 封装寄存器列表

//寄存器的值常常是芯片外设自动更改的,即使 CPU 没有执行程序,也有可能发生变化
//编译器有可能会对没有执行程序的变量进行优化

//volatile 表示易变的变量,防止编译器优化,
#define __IO volatile
typedef unsigned int uint32_t;
typedef unsigned short uint16_t;

// GPIO 寄存器结构体定义
typedef struct
{
    __IO uint32_t CRL; // 端口配置低寄存器, 地址偏移 0X00
    __IO uint32_t CRH; // 端口配置高寄存器, 地址偏移 0X04
    __IO uint32_t IDR; // 端口数据输入寄存器, 地址偏移 0X08
    __IO uint32_t ODR; // 端口数据输出寄存器, 地址偏移 0X0C
    __IO uint32_t BSRR; // 端口位设置/清除寄存器,地址偏移 0X10
    __IO uint32_t BRR; // 端口位清除寄存器, 地址偏移 0X14
    __IO uint32_t LCKR; // 端口配置锁定寄存器, 地址偏移 0X18
}GPIO_TypeDef;

这段代码在每个结构体成员前增加了一个 “__IO” 前缀,它的原型在这段代码的第一行,代表了 C 语言中的关键字 “volatile” ,在 C 语言中该关键字用于表示变量是易变的,要求编译器不要优化。这些结构体内的成员,都代表着寄存器,而寄存器很多时候是由外设或 STM32 芯片状态修改的,也就是说即使 CPU 不执行代码修改这些变量,变量的值也有可能被外设修改、更新,所以每次使用这些变量的时候,我们都要求 CPU 去该变量的地址重新访问。若没有这个关键字修饰,在某些情况下,编译器认为没有代码修改该变量,就直接从 CPU 的某个缓存获取该变量值,这时可以加快执行速度,但该缓存中的是陈旧数据,与我们要求的寄存器最新状态可能会有出入。


3.2 外设存储器映射

外设寄存器结构体定义仅仅是一个定义,要想实现给这个结构体赋值就达到操作寄存器的效果,我们还需要找到该寄存器的地址,就把寄存器地址跟结构体的地址对应起来。所以我们要再找到外设的地址,根据我们前面的学习,我们可以把这些外设的地址定义成一个个宏,实现外设存储器的映射。


/*片上外设基地址 */
#define PERIPH_BASE ((unsigned int)0x40000000)

/*APB2 总线基地址 */
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
/* AHB 总线基地址 */
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)

/*GPIO 外设基地址*/
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)

/*RCC 外设基地址*/
#define RCC_BASE (AHBPERIPH_BASE + 0x1000)

3.3 外设声明

定义好外设寄存器结构体,实现完外设存储器映射后,我们再把外设的基址强制类型转换成相应的外设寄存器结构体指针,然后再把该指针声明成外设名,这样一来,外设名就跟外设的地址对应起来了,而且该外设名还是一个该外设类型的寄存器结构体指针,通过该指针可以直接操作该外设的全部寄存器,见 代码清单 9-2。


代码清单 9-2 指向外设首地址的结构体指针

// GPIO 外设声明
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)

// RCC 外设声明
#define RCC ((RCC_TypeDef *) RCC_BASE)

/*RCC 的 AHB1 时钟使能寄存器地址,强制转换成指针*/
#define RCC_APB2ENR *(unsigned int*)(RCC_BASE+0x18)

首先通过强制类型转换把外设的基地址转换成 GPIO_TypeDef 类型的结构体指针, 然后通过宏定义把 GPIOA、 GPIOB 等定义成外设的结构体指针,通过外设的结构体指针我们就可以达到访问外设的寄存器的目的。

通过操作外设结构体指针的方式,我们把 main 文件里对应的代码修改掉, 见 代码 9-2


代码 9-1 C 语言条件编译

/*
     * C 语言知识,条件编译
     * #if 为真
     * 执行这里的程序
     * #else
     * 否则执行这里的程序
     * #endif
*/

代码 9-2 使用寄存器结构体指针操作寄存器

// 使用寄存器结构体指针点亮 LED
int main(void)
{
#if 0 // 直接通过操作内存来控制寄存器
    // 开启 GPIOB 端口时钟
    RCC_APB2ENR |= (1<<3);
    
    // 空控制 PB0 的端口位
    GPIOB_CRL &= ~( 0x0F<< (4*0));
    // 配置 PB0 为通用推挽输出,速度为 10M
    GPIOB_CRL |= (1<<4*0);
    
    // PB0 输出 低电平
    GPIOB_ODR |= (0<<0);
    
    while (1);
    
#else // 通过寄存器结构体指针来控制寄存器
    
    // 开启 GPIOB 端口时钟
    RCC->APB2ENR |= (1<<3);
    
    // 空控制 PB0 的端口位
    GPIOB->CRL &= ~( 0x0F<< (4*0));
    // 配置 PB0 为通用推挽输出,速度为 10M
    GPIOB->CRL |= (1<<4*0);
    
    // PB0 输出 低电平
    GPIOB->ODR |= (0<<0);
    
    while (1);

#endif    
}

乍一看, 除了把 “_” 换成了 “->” ,其他都跟使用寄存器点亮 LED 那部分代码一样。这是因为我们现在只是实现了库函数的基础,还没有定义库函数。

打好了地基,下面我们就来建高楼。接下来使用函数来封装 GPIO 的基本操作,方便以后应用的时候不需要再查询寄存器,而是直接通过调用这里定义的函数来实现。我们把针对 GPIO 外 设 操 作 的 函 数 及 其 宏 定 义 分 别 存 放 在 “ stm32f10x_gpio.c” 和 “stm32f10x_gpio.h” 文件中,这两个文件需要自己新建。


3.4 定义位操作函数

在 “stm32f10x_gpio.c” 文件定义两个位操作函数,分别用于控制引脚输出高电平和低电平,见 代码清单 9-3。


代码清单 9-3 GPIO 置位函数与复位函数的定义

/*
     * 函数功能:设置引脚为高电平
     * 参数说明:GPIOx:该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址
     * GPIO_Pin:选择要设置的 GPIO 端口引脚,可输入宏 GPIO_Pin_0-15,
     * 表示 GPIOx 端口的 0-15 号引脚。
*/
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
    /*设置 GPIOx 端口 BSRR 寄存器的第 GPIO_Pin 位,使其输出高电平*/
    /*因为 BSRR 寄存器写 0 不影响,宏 GPIO_Pin 只是对应位为 1,其它位均为 0,所以可以直接赋值*/
    
    GPIOx->BSRR = GPIO_Pin;
}

/*
	* 函数功能:设置引脚为低电平
	* 参数说明:GPIOx:该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址
	* GPIO_Pin:选择要设置的 GPIO 端口引脚,可输入宏 GPIO_Pin_0-15,
	* 表示 GPIOx 端口的 0-15 号引脚。
*/
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
    /*设置 GPIOx 端口 BRR 寄存器的第 GPIO_Pin 位,使其输出低电平*/
    /*因为 BRR 寄存器写 0 不影响,宏 GPIO_Pin 只是对应位为 1,其它位均为 0,所以可以直接赋值*/
    
    GPIOx->BRR = GPIO_Pin;
}

这两个函数体内都是只有一个语句,对 GPIOx 的 BSRR 或 BRR 寄存器赋值,从而设置引脚为高电平或低电平,操作 BSRR 或者 BRR 可以实现单独的操作某一位,有关这两个的寄存器说明见 图 9-2 和 图 9-3。其中 GPIOx 是一个指针变量,通过函数的输入参数我们可以修改它的值,如给它赋予 GPIOA、 GPIOB、 GPIOH 等结构体指针值,这个函数就可以控制相应的 GPIOA、 GPIOB、 GPIOH 等端口的输出。


在这里插入图片描述
图 9-2


在这里插入图片描述图 9-3


利用这两个位操作函数, 可以方便地操作各种 GPIO 的引脚电平,控制各种端口引脚的范例见 代码清单 9-4。


代码清单 9-4 位操作函数使用范例

/*控制 GPIOB 的引脚 10 输出高电平*/
GPIO_SetBits(GPIOB,(uint16_t)(1<<10));
/*控制 GPIOB 的引脚 10 输出低电平*/
GPIO_ResetBits(GPIOB,(uint16_t)(1<<10));

/*控制 GPIOB 的引脚 10、引脚 11 输出高电平,使用“|”同时控制多个引脚*/
GPIO_SetBits(GPIOB,(uint16_t)(1<<10)|(uint16_t)(1<<11));
/*控制 GPIOB 的引脚 10、引脚 11 输出低电平*/
GPIO_ResetBits(GPIOB,(uint16_t)(1<<10)|(uint16_t)(1<<10));

/*控制 GPIOA 的引脚 8 输出高电平*/
GPIO_SetBits(GPIOA,(uint16_t)(1<<8));
/*控制 GPIOB 的引脚 9 输出低电平*/
 GPIO_ResetBits(GPIOB,(uint16_t)(1<<9));

使用以上函数输入参数,设置引脚号时,还是稍感不便,为此我们把表示 16 个引脚的操作数都定义成宏,见 代码清单 9-5。


代码清单 9-5 选择引脚参数的宏

/*GPIO 引脚号定义*/
#define GPIO_Pin_0 ((uint16_t)0x0001) /*!< 选择 Pin0 (1<<0) */
#define GPIO_Pin_1 ((uint16_t)0x0002) /*!< 选择 Pin1 (1<<1)*/
#define GPIO_Pin_2 ((uint16_t)0x0004) /*!< 选择 Pin2 (1<<2)*/
#define GPIO_Pin_3 ((uint16_t)0x0008) /*!< 选择 Pin3 (1<<3)*/
#define GPIO_Pin_4 ((uint16_t)0x0010) /*!< 选择 Pin4 */
#define GPIO_Pin_5 ((uint16_t)0x0020) /*!< 选择 Pin5 */
#define GPIO_Pin_6 ((uint16_t)0x0040) /*!< 选择 Pin6 */
#define GPIO_Pin_7 ((uint16_t)0x0080) /*!< 选择 Pin7 */
#define GPIO_Pin_8 ((uint16_t)0x0100) /*!< 选择 Pin8 */
#define GPIO_Pin_9 ((uint16_t)0x0200) /*!< 选择 Pin9 */
#define GPIO_Pin_10 ((uint16_t)0x0400) /*!< 选择 Pin10 */
#define GPIO_Pin_11 ((uint16_t)0x0800) /*!< 选择 Pin11 */
#define GPIO_Pin_12 ((uint16_t)0x1000) /*!< 选择 Pin12 */
#define GPIO_Pin_13 ((uint16_t)0x2000) /*!< 选择 Pin13 */
#define GPIO_Pin_14 ((uint16_t)0x4000) /*!< 选择 Pin14 */
#define GPIO_Pin_15 ((uint16_t)0x8000) /*!< 选择 Pin15 */
#define GPIO_Pin_All ((uint16_t)0xFFFF) /*!< 选择全部引脚 */

这些宏代表的参数是某位置 “ 1 ” 其它位置 “ 0 ” 的数值,其中最后一个 “GPIO_Pin_ALL” 是所有数据位都为 “1” ,所以用它可以一次控制设置整个端口的 0-15 所有引脚。利用这些宏, GPIO 的控制代码可改为 代码清单 9-6。


代码清单 9-6 使用位操作函数及宏控制 GPIO

/*控制 GPIOB 的引脚 10 输出高电平*/
GPIO_SetBits(GPIOB,GPIO_Pin_10);
/*控制 GPIOB 的引脚 10 输出低电平*/
GPIO_ResetBits(GPIOB,GPIO_Pin_10);

/*控制 GPIOB 的引脚 10、引脚 11 输出高电平,使用“|”,同时控制多个引脚*/
GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
/*控制 GPIOB 的引脚 10、引脚 11 输出低电平*/
GPIO_ResetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
/*控制 GPIOB 的所有输出低电平*/
GPIO_ResetBits(GPIOB,GPIO_Pin_ALL);

/*控制 GPIOA 的引脚 8 输出高电平*/
GPIO_SetBits(GPIOA,GPIO_Pin_8);
/*控制 GPIOB 的引脚 9 输出低电平*/
 GPIO_ResetBits(GPIOB,GPIO_Pin_9);

使用以上代码控制 GPIO,我们就不需要再看寄存器了,直接从函数名和输入参数就可以直观看出这个语句要实现什么操作。 (英文中―Set‖表示“置位”,即高电平,“ Reset” 表示“复位”,即低电平)


3.5 定义初始化结构体 GPIO_InitTypeDef

定义位操作函数后,控制 GPIO 输出电平的代码得到了简化,但在控制 GPIO 输出电平前还需要初始化 GPIO 引脚的各种模式,这部分代码涉及的寄存器有很多,我们希望初始化 GPIO 也能以如此简单的方法去实现。为此,我们先根据 GPIO 初始化时涉及到的初始化参数以结构体的形式封装起来,声明一个名为 GPIO_InitTypeDef 的结构体类型,见 代码 9-3


typedef struct
{
    uint16_t GPIO_Pin; /*!< 选择要配置的 GPIO 引脚 */
    
    uint16_t GPIO_Speed; /*!< 选择 GPIO 引脚的速率 */
    
    uint16_t GPIO_Mode; /*!< 选择 GPIO 引脚的工作模式 */
}GPIO_InitTypeDef;

这个结构体中包含了初始化 GPIO 所需要的信息,包括引脚号、工作模式、输出速率。设计这个结构体的思路是:初始化 GPIO 前,先定义一个这样的结构体变量,根据需要配置 GPIO 的模式,对这个结构体的各个成员进行赋值,然后把这个变量作为“GPIO 初始化函数”的输入参数,该函数能根据这个变量值中的内容去配置寄存器,从而实现 GPIO 的初始化。


3.6 定义引脚模式的枚举类型

上面定义的结构体很直接,美中不足的是在对结构体中各个成员赋值实现某个功能时还需要查询手册的寄存器说明,我们不希望每次用到的时候都要去查询手册, 我们可以使用 C 语言中的枚举定义功能,根据手册把每个成员的所有取值都定义好, 具体见 代码 9-4 。GPIO_Speed 和 GPIO_Mode 这两个成员对应的寄存器是 CRL 和 CRH 这两个端口配置寄存器,具体见 图 9-4 和 图 9-5 。


在这里插入图片描述
图 9-4


在这里插入图片描述
图 9-5


代码 9-4 GPIO 枚举类型定义

/*
	* GPIO 输出速率枚举定义
*/
typedef enum
{
    GPIO_Speed_10MHz = 1, 	// 10MHZ (01)b
    GPIO_Speed_2MHz, 		// 2MHZ (10)b
    GPIO_Speed_50MHz 		// 50MHZ (11)b
}GPIOSpeed_TypeDef;

/*
	* GPIO 工作模式枚举定义
*/
typedef enum
{
    GPIO_Mode_AIN = 0x0, 			// 模拟输入 (0000 0000)b
    GPIO_Mode_IN_FLOATING = 0x04, 	// 浮空输入 (0000 0100)b
    GPIO_Mode_IPD = 0x28, 			// 下拉输入 (0010 1000)b
    GPIO_Mode_IPU = 0x48, 			// 上拉输入 (0100 1000)b
    
    GPIO_Mode_Out_OD = 0x14, 		// 开漏输出 (0001 0100)b
    GPIO_Mode_Out_PP = 0x10, 		// 推挽输出 (0001 0000)b
    GPIO_Mode_AF_OD = 0x1C, 		// 复用开漏输出 (0001 1100)b
    GPIO_Mode_AF_PP = 0x18 			// 复用推挽输出 (0001 1000)b
}GPIOMode_TypeDef;

关于这两个枚举类型的值如何跟端口控制寄存器里面的说明对应起来,我们简单分析下。有关速度的枚举类型有 (01)b 10MHZ、 (10)b 2MHZ 和 (11)b 50MHZ ,这三个值跟寄存器说明对得上,很容易理解。至于模式的枚举类型的值理解起来就比较绕,这让很多人费了脑筋,下面我们通过一个表格来梳理下,好帮助我们理解,具体见 图 9-6 。

在这里插入图片描述图 9-6


如果但从这些枚举值的十六进制来看,很难发现规律,转化成二进制之后,就比较容易发现规律。 bit4 用来区分端口是输入还是输出, 0 表示输入, 1 表示输出, bit2 和 bit3 对应寄存器的 CNFY[1:0] 位,是我们真正要写入到 CRL 和 CRH 这两个端口控制寄存器中的值。 bit0 和 bit1 对应寄存器的 MODEY[1:0] 位,这里我们暂不初始化,在 GPIO_Init() 初始化函数中用来跟 GPIOSpeed 的值相加即可实现速率的配置。有关具体的代码分析见 GPIO_Init() 库函数。 其中在下拉输入和上拉输入中我们设置 bit5 和 bit6 的值为 01 和 10 来以示区别。

PS:个人观点,这个 图 9-6 GPIO 引脚工作模式真值表分析 存在上下拉十六进制对应不上二进制,参考《STM32F1xx中文参考手册.pdf》可以看下图 个人观点 9-1,应该以十六进制为准,上下拉二进制的 bit3 实际是要置为1


在这里插入图片描述
个人观点 9-1


有了这些枚举定义,我们的 GPIO_InitTypeDef 结构体就可以使用枚举类型来限定输入参数, 见 代码 9-5 使用枚举定义的 GPIO 初始化结构体。


/*
	* GPIO 初始化结构体类型定义
*/
typedef struct
{
    uint16_t GPIO_Pin; /*!< 选择要配置的 GPIO 引脚 可输入 GPIO_Pin 定义的宏 */
    
    GPIOSpeed_TypeDef GPIO_Speed; /*!< 选择 GPIO 引脚的速率 可输入 GPIOSpeed_TypeDef 定义的枚举值 */
    
    GPIOMode_TypeDef GPIO_Mode; /*!< 选择 GPIO 引脚的工作模式 可输入 GPIOMode_TypeDef 定义的枚举值 */
}GPIO_InitTypeDef;

如果不使用枚举类型,仍使用 “uint16_t” 类型来定义结构体成员,那么成员值的范围就是 0-255 ,而实际上这些成员都只能输入几个数值。所以使用枚举类型可以对结构体成员起到限定输入的作用,只能输入相应已定义的枚举值。

利用这些枚举定义,给 GPIO_InitTypeDef 结构体类型赋值配置就变得非常直观,范例见 代码清单 9-7。


代码清单 9-7 给 GPIO_InitTypeDef 初始化结构体赋值范例

GPIO_InitTypeDef GPIO_InitStructure;

/* GPIO 端口初始化 */
/*选择要控制的 GPIO 引脚*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
/*设置引脚模式为输出模式*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/*设置引脚的输出类型为推挽输出*/
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

3.7 定义 GPIO 初始化函数

接着前面的思路,对初始化结构体赋值后,把它输入到 GPIO 初始化函数,由它来实现寄存器配置。我们的 GPIO 初始化函数实现见 代码 9-6 。


代码 9-6 GPIO 初始化函数

/*
	* 函数功能:初始化引脚模式
	* 参数说明: GPIOx,该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址
	* GPIO_InitTypeDef:GPIO_InitTypeDef 结构体指针,指向初始化变量
*/
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;
	
    /*---------------- GPIO 模式配置 -------------------*/
    // 把输入参数 GPIO_Mode 的低四位暂存在 currentmode
    currentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) &
        		  ((uint32_t)0x0F);
    
    // bit4 是 1 表示输出, bit4 是 0 则是输入
    // 判断 bit4 是 1 还是 0,即首选判断是输入还是输出模式
    if ((((uint32_t)GPIO_InitStruct->GPIO_Mode) &
         ((uint32_t)0x10)) != 0x00)
    {
        // 输出模式则要设置输出速度
        currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
    }
    /*-----GPIO CRL 寄存器配置 CRL 寄存器控制着低 8 位 IO- ----*/
    // 配置端口低 8 位,即 Pin0~Pin7
    if (((uint32_t)GPIO_InitStruct->GPIO_Pin &
        ((uint32_t)0x00FF)) != 0x00)
    {
        // 先备份 CRL 寄存器的值
        tmpreg = GPIOx->CRL;
        
        // 循环,从 Pin0 开始配对,找出具体的 Pin
        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,则找到使用的引脚
            if (currentpin == pos)
            {
                // pinpos 的值左移两位(乘以 4),因为寄存器中 4 个位配置一个引脚
                pos = pinpos << 2;
                // 把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变
                pinmask = ((uint32_t)0x0F) << pos;
                tmpreg &= ~pinmask;
                
                // 向寄存器写入将要配置的引脚的模式
                tmpreg |= (currentmode << pos);
                
                // 判断是否为下拉输入模式
                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;
    }
    /*--------GPIO CRH 寄存器配置 CRH 寄存器控制着高 8 位 IO- -----*/
    // 配置端口高 8 位,即 Pin8~Pin15
    if (GPIO_InitStruct->GPIO_Pin > 0x00FF)
    {
        // 先备份 CRH 寄存器的值
        tmpreg = GPIOx->CRH;
        
        // 循环,从 Pin8 开始配对,找出具体的 Pin
        for (pinpos = 0x00; pinpos < 0x08; pinpos++)
        {
            pos = (((uint32_t)0x01) << (pinpos + 0x08));
            
            // pos 与输入参数 GPIO_PIN 作位与运算
            currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);
            
            //若 currentpin=pos,则找到使用的引脚
            if (currentpin == pos)
            {
             	// pinpos 的值左移两位(乘以 4),因为寄存器中 4 个位配置一个引脚
                pos = pinpos << 2;
                
                // 把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变
                pinmask = ((uint32_t)0x0F) << pos;
                tmpreg &= ~pinmask;
                
                // 向寄存器写入将要配置的引脚的模式
                tmpreg |= (currentmode << pos);
                
                // 判断是否为下拉输入模式
                if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
                {
                    // 下拉输入模式,引脚默认置 0,对 BRR 寄存器写 1 可对引脚置 0
                    GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));
                }
                // 判断是否为上拉输入模式
                if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
                {
                    // 上拉输入模式,引脚默认值为 1,对 BSRR 寄存器写 1 可对引脚置 1
                    GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));
                }
            }
        }
        // 把前面处理后的暂存值写入到 CRH 寄存器之中
        GPIOx->CRH = tmpreg;
    }
}

这个函数有 GPIOx 和 GPIO_InitStruct 两个输入参数,分别是 GPIO 外设指针和 GPIO 初始化结构体指针。分别用来指定要初始化的 GPIO 端口及引脚的工作模式。

要充分理解这个 GPIO 初始化函数,得配合我们刚刚分析的 GPIO 引脚工作模式真值表来看。


在这里插入图片描述图 9-7 GPIO 引脚工作模式真值表


① 先取得 GPIO_Mode 的值,判断 bit4 是 1 还是 0 来判断是输出还是输入。如果是输出则设置输出速率,即加上 GPIO_Speed 的值,输入没有速率之说,不用设置。

② 配置 CRL 寄存器。通过 GPIO_Pin 的值计算出具体需要初始化哪个引脚,算出后,然后把需要配置的值写入到 CRL 寄存器中,具体分析见代码注释。这里有一个比较有趣的是上/下拉输入并不是直接通过配置某一个寄存器来实现的,而是通过写 BSRR 或者 BRR 寄存器来实现。这让很多只看手册没看固件库底层源码的人摸不着头脑,因为手册的寄存器说明中没有明确的指出如何配置上拉/下拉,具体见 图 9-8 。

③ 配置 CRH 寄存器过程同 CRL。


在这里插入图片描述
图 9-8 上拉/下拉寄存器说明


3.8 全新面貌,使用函数点亮 LED 灯

完成以上的准备后,我们就可以用自己定义的函数来点亮 LED 灯,见 代码 9-7 。


// 使用固件库点亮 LED
int main(void)
{
    // 定义一个 GPIO_InitTypeDef 类型的结构体
    GPIO_InitTypeDef GPIO_InitStructure;
    
    // 开启 GPIO 端口时钟
    RCC_APB2ENR |= (1<<3);
    
    // 选择要控制的 GPIO 引脚
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    
    // 设置引脚模式为通用推挽输出
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    
    // 设置引脚速率为 50MHz
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
    // 调用库函数,初始化 GPIO 引脚
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    
    // 使引脚输出低电平,点亮 LED1
    GPIO_ResetBits(GPIOB,GPIO_Pin_0);
    
    while (1)
    {
        // 使引脚输出低电平,点亮 LED
        GPIO_ResetBits(GPIOB,GPIO_Pin_0);
        
        /*延时一段时间*/
        Delay(0xFFFF);
        
        /*使引脚输出高电平,关闭 LED1*/
        GPIO_SetBits(GPIOB,GPIO_Pin_0);
        
        /*延时一段时间*/
        Delay(0xFFFF);
    }
}

现在看起来,使用函数来控制 LED 灯与之前直接控制寄存器已经有了很大的区别: main 函数中先定义了一个 GPIO 初始化结构体变量 GPIO_InitStructure,然后对该变量的各个成员按点亮 LED 灯所需要的 GPIO 配置模式进行赋值,赋值后,调用 GPIO_Init 函数,让它根据结构体成员值对 GPIO 寄存器写入控制参数,完成 GPIO 引脚初始化。控制电平时,直接使用 GPIO_SetBits 和 GPIO_Resetbits 函数控制输出。如若对其它引脚进行不同模式的初始化,只要修改 GPIO 初始化结构体 GPIO_InitStructure 的成员值,把新的参数值输入到 GPIO_Init 函数再调用即可。

代码中新增的 Delay 函数,主要功能是延时,让我们可以看清楚实验现象(不延时的话指令执行太快,肉眼看不出来),它的实现原理是让 CPU 执行无意义的指令,消耗时间,在此不要纠结它的延时时间,写一个大概输入参数值,下载到实验板实测,觉得太久了就把参数值改小,短了就改大即可。需要精确延时的时候我们会用 STM32 的定时器外设进行精确延时的。

3.9 总结

什么是 ST 标准固件库?不懂的时候总觉得莫测高深,懂了之后一切都是纸老虎。

我们从寄存器映射开始,把内存跟寄存器建立起一一对应的关系,然后操作寄存器点亮 LED,再把寄存器操作封装成一个个函数。一步一步走来,我们实现了库最简单的雏形,如果我们不断地增加操作外设的函数,并且把所有的外设都写完,一个完整的库就实现了。

本章中的 GPIO 相关库函数及结构体定义,实际上都是从 ST 标准库搬过来的。这样分析它纯粹是为了满足自己的求知欲,学习其编程的方式、思想,这对提高我们的编程水平是很有好处的,顺便感受一下 ST 库设计的严谨性,我认为这样的代码不仅严谨且华丽优美,不知您是否也有这样的感受。

与直接配置寄存器相比,从执行效率上看会有额外的消耗:初始化变量赋值的过程、库函数在被调用的时候要耗费调用时间;在函数内部,对输入参数转换所需要的额外运算也消耗一些时间(如 GPIO 中运算求出引脚号时)。而其它的宏、枚举等解释操作是作编译过程完成的,这部分并不消耗内核的时间。那么函数库的优点呢?是我们可以快速上手 STM32 控制器;配置外设状态时,不需要再纠结要向寄存器写入什么数值;交流方便,查错简单。这就是我们选择库的原因。

现在的处理器的主频是越来越高,我们不需要担心 CPU 耗费那么多时间来干活会不会被累倒,库主要应用是在初始化过程,而初始化过程一般是芯片刚上电或在核心运算之前的执行的,这段时间的等待是 0.02us 还是 0.01us 在很多时候并没有什么区别。相对来说,我们还是担心一下如果都用寄存器操作,每行代码都要查数据手册的寄存器说明,自己会不会被累倒吧。

在以后开发的工程中,一般不会去分析 ST 的库函数的实现。因为外设的库函数是很类似的,库外设都包含初始化结构体,以及特定的宏或枚举标识符,这些封装被库函数这些转化成相应的值,写入到寄存器之中,函数内部的具体实现是十分枯燥和机械的工作。如果您有兴趣,在您掌握了如何使用外设的库函数之后,可以查看一下它的源码实现。

通常我们只需要通过了解每种外设的“初始化结构体”就能够通过它去了解 STM32 的外设功能及控制。







- 初识 STM32 标准库

1. CMSIS 标准及库层次关系

因为基于 Cortex 系列芯片采用的内核都是相同的,区别主要为核外的片上外设的差异,这些差异却导致软件在同内核,不同外设的芯片上移植困难。为了解决不同的芯片厂商生产的 Cortex 微控制器软件的兼容性问题, ARM 与芯片厂商建立了 CMSIS 标准 (Cortex MicroController Software Interface Standard) 。

所谓 CMSIS 标准,实际是新建了一个软件抽象层。见 图 10-1 。


在这里插入图片描述
图 10-1 CMSIS 架构


CMSIS 标准中最主要的为 CMSIS 核心层,它包括了:

  • 内核函数层:其中包含用于访问内核寄存器的名称、地址定义,主要由 ARM 公司提供。

  • 设备外设访问层:提供了片上的核外外设的地址和中断定义,主要由芯片生产商提供。

可见 CMSIS 层位于硬件层与操作系统或用户层之间,提供了与芯片生产商无关的硬件抽象层,可以为接口外设、实时操作系统提供简单的处理器软件接口,屏蔽了硬件差异,这对软件的移植是有极大的好处的。 STM32 的库,就是按照 CMSIS 标准建立的。


1.1 库目录、文件简介

进入其目录:“STM32F10x_StdPeriph_Lib_V3.5.0\”

  • Libraries: 文件夹下是驱动库的源代码及启动文件,这个非常重要,我们要使用的固件库就在这个文件夹里面。

  • Project : 文件夹下是用驱动库写的例子和工程模板,其中那些为每个外设写好的例程对我们非常有用,我们在学习的时候就可以参考这里面的例程,非常全面,简直就是穷尽了外设的所有功能。

  • Utilities:包含了基于 ST 官方实验板的例程,不需要用到,略过即可。

  • stm32f10x_stdperiph_lib_um.chm: 库帮助文档,这个很有用,不喜欢直接看源码的可以在合理查询每个外设的函数说明,非常详细。这是一个已经编译好的 HTML 文件,主要讲述如何使用驱动库来编写自己的应用程序。说得形象一点,这个 HTML 就是告诉我们: ST 公司已经为你写好了每个外设的驱动了,想知道如何运用这些例子就来向我求救吧。不幸的是,这个帮助文档是英文的,这对很多英文不好的朋友来说是一个很大的障碍。但这里要告诉大家,英文仅仅是一种工具,绝对不能让它成为我们学习的障碍。其实这些英文还是很简单的,我们需要的是拿下它的勇气。

在使用库开发时,我们需要把 libraries 目录下的库函数文件添加到工程中,并查阅库帮助文档来了解 ST 提供的库函数,这个文档说明了每一个库函数的使用方法。

进入 Libraries 文件夹看到,关于内核与外设的库文件分别存放在 CMSIS 和 STM32F10x_StdPeriph_Driver 文件夹中。


1.1.1 CMSIS 文件夹

STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\文件夹展开内容见 图 10-3。


在这里插入图片描述图 10-3 CMSIS 文件夹内容 目录: Libraries\CMSIS\


其中黄色框框住的是我们需要用到的内容,下面我们一一讲解下这几个文件的作用。

内核相关文件

在 CoreSupport 文件夹中有 core_cm3.c 和 core_cm3.h 两个文件。 Core_cm3.h 头文件里面实现了内核的寄存器映射,对应外设头文件 stm32f10x.h,区别就是一个针对内核的外设,一个针对片上(内核之外)的外设。 core_cm3.c 文件实现了一下操作内核外设寄存器的函数,用的比较少。

我们还需要了解的是 core_cm3.h 头文件中包含了 “stdint.h” 这个头文件,这是一个 ANSI C 文件,是独立于处理器之外的,就像我们熟知的 C 语言头文件 “stdio.h” 文件一样。位于 RVMDK 这个软件的安装目录下,主要作用是提供一些类型定义。见 代码清单 10-1。


代码清单 10-1: stdint.h 文件中的类型定义

/* exact-width signed integer types */
typedef signed char int8_t;
typedef signed short int int16_t;
typedef signed int int32_t;
typedef signed __int64 int64_t;

/* exact-width unsigned integer types */
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned __int64 uint64_t;

这些新类型定义屏蔽了在不同芯片平台时,出现的诸如 int 的大小是 16 位,还是 32 位的差异。所以在我们以后的程序中,都将使用新类型如 uint8_t 、 uint16_t 等。

在稍旧版的程序中还经常会出现如 u8、 u16、 u32 这样的类型,分别表示的无符号的 8 位、 16 位、 32 位整型。初学者碰到这样的旧类型感觉一头雾水,它们定义的位置在 STM32f10x.h 文件中。建议在以后的新程序中尽量使用 uint8_t 、 uint16_t 类型的定义。

启动文件
启动文件放在 startup/arm 这个文件夹下面,这里面启动文件有很多个,不同型号的单片机用的启动文件不一样,有关每个启动文件的详细说明见表


启动文件区别
startup_stm32f10x_ld.sld: low-density 小容量,FLASH 容量在 16-32K 之间
startup_stm32f10x_md.smd: medium-density 中容量,FLASH 容量在 64-128K 之间
startup_stm32f10x_hd.shd: high-density 中容量,FLASH 容量在 256-512K 之间
startup_stm32f10x_xl.sxl: 超大容量,FLASH 容量在 512-1024K 之间

以上四种都属于基本型,包括 STM32F101xx、 STM32F102xx、 STM32F103xx 系列


startup_stm32f10x_cl.scl:connectivity line devices 互联型,特指 STM32F105xx 和 STM32F107xx 系列
startup_stm32f10x_ld_vl.svl:value line devices 超值型系列,特指 STM32F100xx 系列
startup_stm32f10x_md_vl.svl:value line devices 超值型系列,特指 STM32F100xx 系列
startup_stm32f10x_hd_vl.svl:value line devices 超值型系列,特指 STM32F100xx 系列

我们开发板中用的 STM32F103VET6 或者 STM32F103ZET6 的 FLASH 都是 512K ,属于基本型的大容量产品,启动文件统一选择 startup_stm32f10x_hd.s。


Stm32f10x.h
这个头文件实现了片上外设的所以寄存器的映射,是一个非常重要的头文件,在内核中与之想对应的头文件是 core_cm3.h。

system_stm32f10x.c
system_stm32f10x.c 文件实现了 STM32 的时钟配置, 操作的是片上的 RCC 这个外设。系统在上电之后,首选会执行由汇编编写的启动文件, 启动文件中的复位函数中调用的 SystemInit 函数就在这个文件里面定义。调用完之后,系统的时钟就被初始化成 72M 。 如果后面我们需要重新配置系统时钟,我们就可以参考这个函数重写。为了维持库的完整性,我们不会直接在这个文件里面修改时钟配置函数。


1.1.2 STM32F10x_StdPeriph_Driver 文件夹

文件目录: Libraries\STM32F10x_StdPeriph_Driver

进入 libraries 目录下的 STM32F10x_StdPeriph_Driver 文件夹,见 图 10-4 。


在这里插入图片描述
图 10-4 外设驱动


STM32F10x_StdPeriph_Driver 文件夹下有 inc(include 的缩写)跟 src(source 的简写)这两个文件夹,这里的文件属于 CMSIS 之外的的、芯片片上外设部分。 src 里面是每个设备外设的驱动源程序, inc 则是相对应的外设头文件。 src 及 inc 文件夹是 ST 标准库的主要内容,甚至不少人直接认为 ST 标准库就是指这些文件,可见其重要性。

在 src 和 inc 文件夹里的就是 ST 公司针对每个 STM32 外设而编写的库函数文件,每个外设对应一个 .c 和 .h 后缀的文件。我们把这类外设文件统称为: stm32f10x_ppp.c 或 stm32f10x_ppp.h 文件, PPP 表示外设名称。 如在上一章中我们自建的 stm32f10x_gpio.c 及 stm32f10x_gpio.h 文件,就属于这一类。

如针对模数转换(ADC)外设,在 src 文件夹下有一个 stm32f10x_adc.c 源文件,在 inc 文件夹下有一个 stm32f10x_adc.h 头文件,若我们开发的工程中用到了 STM32 内部的 ADC,则至少要把这两个文件包含到工程里。见 图 10-5。


在这里插入图片描述
图 10-5 驱动的源文件及头文件

这两个文件夹中,还有一个很特别的 misc.c 文件,这个文件提供了外设对内核中的 NVIC (中断向量控制器)的访问函数,在配置中断时,我们必须把这个文件添加到工程中。


1.1.3 stm32f10x_it.c、 stm32f10x_conf.h 和 system_stm32f10x.c 文件

文件目录: STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template

在这个文件目录下, 存放了官方的一个库工程模板,我们在用库建立一个完整的工程时 , 还 需 要 添 加 这 个 目 录 下 的 stm32f10x_it.c 、 stm32f10x_it.h 、 stm32f10x_conf.h 和 system_stm32f10x.c 这四个文件。

stm32f10x_it.c:这个文件是专门用来编写中断服务函数的,在我们修改前,这个文件已经定义了一些系统异常(特殊中断)的接口,其它普通中断服务函数由我们自己添加。但是我们怎么知道这些中断服务函数的接口如何写?是不是可以自定义呢?答案当然不是,这些都可以在汇编启动文件中找到, 在学习中断和启动文件的时候我们会详细介绍。

system_stm32f10x.c:这个文件包含了 STM32 芯片上电后初始化系统时钟、扩展外部存储器用的函数,例如我们前两章提到供启动文件调用的 “SystemInit” 函数,用于上电后初始化时钟,该函数的定义就存储在 system_stm32f10x.c 文件。 STM32F103 系列的芯片,调用库的这个 SystemInit 函数后,系统时钟被初始化为 72MHz,如有需要可以修改这个文件的内容,设置成自己所需的时钟频率,但鉴于保持库的完整性,我们在做系统时钟配置的时候会另外重写时钟配置函数。

stm32f10x_conf.h: 这个文件被包含进 stm32f10x.h 文件。 当我们使用固件库编程的时候,如果需要某个外设的驱动库,就需要包含该外设的头文件: stm32f10x_ppp.h,包含一个还好,如果是用了多外设,就需要包含多个头文件,这不仅影响代码美观也不好管理,现我们用一个头文件 stm32f10x_conf.h 把这些外设的头文件都包含在里面,让这个配置头文件统一管理这些外设的头文件,我们在应用程序中只需要包含这个配置头文件即可,我们又知道这个头文件在 stm32f10x.h 的最后被包含,所以最终我们只需要包含 stm32f10x.h 这个头文件即可,非常方便。 Stm32f10x_conf.h 见代码清单 10-2。 默认情况下是所以头文件都被包含,没有被注释掉。我们也可以把不要的都注释掉,只留下需要使用的即可。


代码清单 10-2 stm32f10x_conf.h 文件配置软件库

#include "stm32f10x_adc.h"
#include "stm32f10x_bkp.h"
#include "stm32f10x_can.h"
#include "stm32f10x_cec.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"

stm32f10x_conf.h 这个文件还可配置是否使用“断言” 编译选项, 见 代码清单 10-3。


代码清单 10-3 断言配置

#ifdef USE_FULL_ASSERT

/**
     * @brief The assert_param macro is used for parameters check.
     * @param expr: If expr is false, it calls assert_failed function
     * which reports the name of the source file and the source
     * line number of the call that failed.
     * If expr is true, it returns no value.
     * @retval None
*/
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ---------------------------------- */
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0)
#endif /* USE_FULL_ASSERT */

在 ST 标准库的函数中,一般会包含输入参数检查,即上述代码中的 “assert_param” 宏,当参数不符合要求时,会调用 “assert_failed” 函数,这个函数默认是空的。

实际开发中使用断言时,先通过定义 USE_FULL_ASSERT 宏来使能断言,然后定义 “assert_failed” 函数,通常我们会让它调用 printf 函数输出错误说明。 使能断言后,程序运行时会检查函数的输入参数,当软件经过测试,可发布时, 会取消 USE_FULL_ASSERT 宏来去掉断言功能, 使程序全速运行。


1.2 库各文件间的关系

前面向大家简单介绍了各个库文件的作用,库文件是直接包含进工程即可,丝毫不用修改,而有的文件就要我们在使用的时候根据具体的需要进行配置。接下来从整体上把握一下各个文件在库工程中的层次或关系,这些文件对应到 CMSIS 标准架构上。见 图 10-6。


在这里插入图片描述
图 10-6 库各文件关系


图 10-6 描述了 STM32 库各文件之间的调用关系,在实际的使用库开发工程的过程中,我们把位于 CMSIS 层的文件包含进工程, 除了特殊系统时钟需要修改 system_stm32f10x.c,其它文件丝毫不用修改,也不建议修改。

对于位于用户层的几个文件,就是我们在使用库的时候,针对不同的应用对库文件进行增删(用条件编译的方法增删)和改动的文件。


2. 使帮助文档

2.1 常用官方资料

  • 《STM32F10X-中文参考手册》.
    这个文件全方位介绍了 STM32 芯片的各种片上外设,它把 STM32 的时钟、存储器架构、及各种外设、寄存器都描述得清清楚楚。当我们对 STM32 的外设感到困惑时,可查阅这个文档。 以直接配置寄存器方式开发的话,查阅这个文档寄存器部分的频率会相当高,但这样效率太低了。

  • 《STM32 规格书》
    本文档相当于 STM32 的 datasheet,包含了 STM32 芯片所有的引脚功能说明及存储器架构、芯片外设架构说明。后面我们使用 STM32 其它外设时,常常需要查找这个手册,了解外设对应到 STM32 的哪个 GPIO 引脚。

  • 《Cortex™-M3 内核编程手册》
    本文档由 ST 公司提供,主要讲解 STM32 内核寄存器相关的说明,例如系统定时器、NVIC 等核外设的寄存器。这部分的内容是《STM32F10X-中文参考手册》没涉及到的内核部分的补充。相对来说,本文档虽然介绍了内核寄存器,但不如以下两个文档详细,要了解内核时,可作为以下两个手册的配合资料使用。

  • 《Cortex-M3 权威指南》
    这个手册是由 ARM 公司提供的,它详细讲解了 Cortex 内核的架构和特性,要深入了解 Cortex-M 内核,这是首选,经典中的经典。这个手册也被翻译成中文,出版成书,我们配套的资料里面有提供中文版的电子版。

  • 《stm32f10x_stdperiph_lib_um.chm》
    这个就是本章提到的库的帮助文档,在使用库函数时,我们最好通过查阅此文件来了解标准库提供了哪些外设、函数原型或库函数的调用的方法。也可以直接阅读源码里面的函数的函数说明。


2.2 初识库函数

所谓库函数,就是 STM32 的库文件中为我们编写好驱动外设的函数接口,我们只要调用这些库函数,就可以对 STM32 进行配置,达到控制目的。我们可以不知道库函数是如何实现的,但我们调用函数必须要知道函数的功能、 可传入的参数及其意义、和函数的返回值。

于是,有读者就问那么多函数我怎么记呀? 我的回答是:会查就行了,哪个人记得了那么多。所以我们学会查阅库帮助文档 是很有必要的。

打开库帮助文档《stm32f10x_stdperiph_lib_um.chm》 见 图 10-7


在这里插入图片描述
图 10-7 库帮助文档


层层打开文档的目录标签:
标签目录: Modules\STM32F10x_StdPeriph_Driver\

可看到 STM32F10x _StdPeriph_Driver 标签下有很多外设驱动文件的名字 MISC、 ADC、BKP、 CAN 等标签。

我们试着查看 GPIO 的“位设置函数 GPIO_SetBits”看看, 打开标签:
标签目录: Modules\STM32F10x_StdPeriph_Driver\GPIO\Functions\GPIO_SetBits 见 图 10-8。


在这里插入图片描述
图 10-8 库帮助文档的函数说明

利用这个文档,我们即使没有去看它的具体源代码,也知道要怎么利用它了。

如 GPIO_SetBits ,函数的原型为 void GPIO_SetBits(GPIO_TypeDef * GPIOx , uint16_t GPIO_Pin) 。它的功能是:输入一个类型为 GPIO_TypeDef 的指针 GPIOx 参数,选定要控制的 GPIO 端口;输入 GPIO_Pin_x 宏,其中 x 指端口的引脚号,指定要控制的引脚。

其中输入的参数 GPIOx 为 ST 标准库中定义的自定义数据类型,这两个传入参数均为结构体指针。初学时,我们并不知道如 GPIO_TypeDef 这样的类型是什么意思,可以点击函数原型中带下划线的 GPIO_TypeDef 就可以查看这个类型的声明了。

就这样初步了解了一下库函数,读者就可以发现 STM32 的库是写得很优美的。每个函数和数据类型都符合见名知义的原则,当然,这样的名称写起来特别长,而且对于我们来说要输入这么长的英文,很容易出错,所以在开发软件的时候,在用到库函数的地方,直接把库帮助文档中的函数名称复制粘贴到工程文件就可以了。 而且,配合 MDK 软件的代码自动补全功能,可以减少输入量。

有的用户觉得使用库文档麻烦,也可以直接查阅 STM32 标准库的源码,库帮助文档的说明都是根据源码生成的,所以直接看源码也可以了解函数功能。


摘抄自:
[野火EmbedFire]《STM32库开发实战指南——基于野火霸道开发板》.pdf

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值