STM32F10X-中文参考手册2
9. 自己写库—构建库函数雏形
本章参考资料:《STM32F10X-中文参考手册》GPIO章节和RCC章节。
虽然我们上面用寄存器点亮了 LED,乍看一下好像代码也很简单,但是我们别侥幸以后就可以一直用寄存器开发。 在用寄存器点亮 LED 的时候,我们会发现 STM32 的寄存器都是 32位的,每次配置的时候都要对照着《STM32F10X-中文参考手册》中寄存器的说明, 然后根据说明对每个控制的寄存器位写入特定参数,因此在配置的时候非常容易出错,而且代码还很不好理解, 不便于维护。所以学习 STM32 最好的方法是用固件库,然后在固件库的基础上了解底层,学习寄存器。
9.1. 什么是STM32函数库
以上所说的固件库是指“STM32标准函数库”,它是由ST公司针对STM32提供的函数接口, 即API (Application Program Interface), 开发者可调用这些函数接口来配置STM32的寄存器,使开发人员得以脱离最底层的寄存器操作,有开发快速,易于阅读,维护成本低等优点。
当我们调用库API的时候不需要挖空心思去了解库底层的寄存器操作,就像当年我们刚开始学习C语言的时候,用prinft()函数时只是学习它的使用格式, 并没有去研究它的源码实现,但需要深入研究的时候,经过千锤百炼的库源码就是最佳学习范例。
实际上,库是架设在寄存器与用户驱动层之间的代码,向下处理与寄存器直接相关的配置,向上为用户提供配置寄存器的接口。 库开发方式与直接配置寄存器方式的区别见图 固件库开发与寄存器开发对比图。
9.2. 为什么采用库来开发及学习?
在以前8位机时代的程序开发中,一般直接配置芯片的寄存器,控制芯片的工作方式,如中断,定时器等。配置的时候, 常常要查阅寄存器表,看用到哪些配置位,为了配置某功能,该置1还是置0。这些都是很琐碎的、机械的工作,因为8位机的软件相对来说较简单, 而且资源很有限,所以可以直接配置寄存器的方式来开发。
对于STM32,因为外设资源丰富,带来的必然是寄存器的数量和复杂度的增加,这时直接配置寄存器方式的缺陷就突显出来了:
-
开发速度慢
-
程序可读性差
-
维护复杂
这些缺陷直接影响了开发效率,程序维护成本,交流成本。库开发方式则正好弥补了这些缺陷。
而坚持采用直接配置寄存器的方式开发的程序员,会列举以下原因:
-
具体参数更直观
-
程序运行占用资源少
相对于库开发的方式,直接配置寄存器方式生成的代码量的确会少一点,但因为STM32有充足的资源,权衡库的优势与不足, 绝大部分时候,我们愿意牺牲一点CPU资源,选择库开发。一般只有在对代码运行时间要求极苛刻的地方,才用直接配置寄存器的方式代替,如频繁调用的中断服务函数。
对于库开发与直接配置寄存器的方式,就好比编程是用汇编好还是用 C 好一样。在STM32F1系列刚推出函数库时引起程序员的激烈争论, 但是,随着ST库的完善与大家对库的了解,更多的程序员选择了库开发。现在STM32F1系列和STM32F4系列各有一套自己的函数库, 但是它们大部分是兼容的,F1和F4之间的程序移植,只需要小修改即可。而如果要移植用寄存器写的程序,那简直跟脱胎换骨差不多。
用库来进行开发,市场已有定论,用户群说明了一切,但对于STM32的学习仍然有人认为用寄存器好,而且汇编不是还没退出大学教材么? 认为这种方法直观,能够了解到是配置了哪些寄存器,怎样配置寄存器。事实上,库函数的底层实现恰恰是直接配置寄存器方式的最佳例子, 它代替我们完成了寄存器配置的工作,而想深入了解芯片是如何工作的话,只要直接查看库函数的最底层实现就能理解,相信你会为它严谨、 优美的实现方式而陶醉,要想修炼C语言,就从ST的库开始吧。所以在以后的章节中,使用软件库是我们的重点, 而且我们通过讲解库API去高效地学习STM32的寄存器,并不至于因为用库学习,就不会用寄存器控制STM32芯片。
9.3. 实验:构建库函数雏形
虽然库的优点多多,但很多人对库还是很忌惮,因为一开始用库的时候有很多代码,很多文件,不知道如何入手。 不知道您是否认同这么一句话:一切的恐惧都来源于无知。我们对库忌惮那是因为我们不知道什么是库,不知道库是怎么实现的。
接下来,我们在寄存器点亮 LED 的代码上继续完善,把代码一层层封装,实现库的最初的雏形,相信经过这一步的学习后, 您对库的运用会游刃有余。这里我们只讲如何实现GPIO函数库,其他外设的我们直接参考ST标准库学习即可,不必自己写。
下面请打开本章配套例程“自己写库—构建库函数雏形”来阅读理解,该例程是在上一章的基础上修改得来的。
9.3.1. 外设寄存器结构体定义
上一章中我们在操作寄存器的时候,操作的是都寄存器的绝对地址,如果每个外设寄存器都这样操作,那将非常麻烦。 我们考虑到外设寄存器的地址都是基于外设基地址的偏移地址,都是在外设基地址上逐个连续递增的,每个寄存器占 32 个字节, 这种方式跟结构体里面的成员类似。所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器, 成员的排列顺序跟寄存器的顺序一样。这样我们操作寄存器的时候就不用每次都找到绝对地址, 只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。
在工程中的“stm32f10x.h”文件中,我们使用结构体封装GPIO及RCC外设的的寄存器,见 代码清单:构建库-1。 结构体成员的顺序按照寄存器的偏移地址从低到高排列,成员类型跟寄存器类型一样。
代码清单:构建库-1 封装寄存器列表
123456789 10 11 12 13 14 15 16 17 18 19 | //寄存器的值常常是芯片外设自动更改的,即使CPU没有执行程序,也有可能发生变化 //编译器有可能会对没有执行程序的变量进行优化,所以用volatile来修饰寄存器变量//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的某个缓存获取该变量值,这时可以加快执行速度,但该缓存中的是陈旧数据,与我们要求的寄存器最新状态可能会 有出入。
9.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)
9.3.3. 外设声明
定义好外设寄存器结构体,实现完外设存储器映射后,我们再把外设的基址强制类型转换成相应的外设寄存器结构体指针, 然后再把该指针声明成外设名,这样一来,外设名就跟外设的地址对应起来了,而且该外设名还是一个该外设类型的寄存器结构体指针, 通过该指针可以直接操作该外设的全部寄存器,见 代码清单:构建库-2。
代码清单:构建库-2 指向外设首地址的结构体指针
123456789 10 11 12 13 14 | // 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文件里对应的代码修改掉,见 代码清单:构建库-3 else部分。
代码清单:构建库-3 C语言条件编译
1 2 3 4 5 6 7 8 | /* * C语言知识,条件编译 * #if 为真 * 执行这里的程序 * #else * 否则执行这里的程序 * #endif */ |
代码清单:构建库-4 使用寄存器结构体指针操作寄存器
123456789 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // 使用寄存器结构体指针点亮LED int main(void) { #if 0 // 直接通过操作内存来控制寄存器// 开启GPIOB 端口时钟RCC_APB2ENR |= (1<<3);//清空控制PB0的端口位GPIOB_CRL &= ~( 0x0F<< (4*0));// 配置PB0为通用推挽输出,速度为10MGPIOB_CRL |= (1<<4*0);// PB0 输出 低电平GPIOB_ODR |= (0<<0);while (1);#else // 通过寄存器结构体指针来控制寄存器// 开启GPIOB 端口时钟RCC->APB2ENR |= (1<<3);//清空控制PB0的端口位GPIOB->CRL &= ~( 0x0F<< (4*0));// 配置PB0为通用推挽输出,速度为10MGPIOB->CRL |= (1<<4*0);// PB0 输出 低电平GPIOB->ODR |= (0<<0);while (1);#endif } |
乍一看,除了把“_”换成了“->”,其他都跟使用寄存器点亮LED那部分代码一样。这是因为我们现在只是实现了库函数的基础,还没有定义库函数。
打好了地基,下面我们就来建高楼。接下来使用函数来封装GPIO的基本操作,方便以后应用的时候不需要再查询寄存器, 而是直接通过调用这里定义的函数来实现。我们把针对GPIO外设操作的函数及其宏定义分别存放在“stm32f10x_gpio.c”和“stm32f10x_gpio.h”文件中, 这两个文件需要自己新建。
9.3.4. 定义位操作函数
在“stm32f10x_gpio.c”文件定义两个位操作函数,分别用于控制引脚输出高电平和低电平,见 代码清单:构建库-5。
代码清单:构建库-5 GPIO置位函数与复位函数的定义
123456789 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /** *函数功能:设置引脚为高电平 *参数说明: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可以实现单独的操作某一位,有关这两个的寄存器说明见图 BSRR寄存器说明 和图 BRR寄存器说明。 其中GPIOx是一个指针变量,通过函数的输入参数我们可以修改它的值,如给它赋予GPIOA、GPIOB、GPIOH等结构体指针值, 这个函数就可以控制相应的GPIOA、GPIOB、GPIOH等端口的输出。
利用这两个位操作函数,可以方便地操作各种GPIO的引脚电平, 控制各种端口引脚的范例见 代码清单:构建库-6。
代码清单:构建库-6 位操作函数使用范例
123456789 10 11 12 13 14 | /*控制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个引脚的操作数都定义成宏,见 代码清单:构建库-7。
代码清单:构建库-7 选择引脚参数的宏
123456789 10 11 12 13 14 15 16 17 18 | /*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的控制代码可改为 代码清单:构建库-8。
代码清单:构建库-8 使用位操作函数及宏控制GPIO
123456789 10 11 12 13 14 15 16 | /*控制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”表示“复位”,即低电平)
9.3.5. 定义初始化结构体GPIO_InitTypeDef
定义位操作函数后,控制GPIO输出电平的代码得到了简化,但在控制GPIO输出电平前还需要初始化GPIO引脚的各种模式, 这部分代码涉及的寄存器有很多,我们希望初始化GPIO也能以如此简单的方法去实现。为此,我们先根据GPIO初始化时涉及到的初始化参数以结构体的形式封装起来, 声明一个名为GPIO_InitTypeDef的结构体类型,见 代码清单:构建库-9
代码清单:构建库-9 定义GPIO初始化结构体
1 2 3 4 5 6 7 8 | typedef struct {uint16_t GPIO_Pin; /*!< 选择要配置的GPIO引脚 */uint16_t GPIO_Speed; /*!< 选择GPIO引脚的速率 */uint16_t GPIO_Mode; /*!< 选择GPIO引脚的工作模式 */ } GPIO_InitTypeDef; |
这个结构体中包含了初始化GPIO所需要的信息,包括引脚号、工作模式、输出速率。设计这个结构体的思路是:初始化GPIO前, 先定义一个这样的结构体变量,根据需要配置GPIO的模式,对这个结构体的各个成员进行赋值,然后把这个变量作为“GPIO初始化函数”的输入参数, 该函数能根据这个变量值中的内容去配置寄存器,从而实现GPIO的初始化。
9.3.6. 定义引脚模式的枚举类型
上面定义的结构体很直接,美中不足的是在对结构体中各个成员赋值实现某个功能时还需要查询手册的寄存器说明, 我们不希望每次用到的时候都要去查询手册,我们可以使用C语言中的枚举定义功能,根据手册把每个成员的所有取值都定义好, 具体见代码9‑4。GPIO_Speed和GPIO_Mode这两个成员对应的寄存器是CRL和CRH这两个端口配置寄存器, 具体见图 端口配置低寄存器 和图 端口配置高寄存器。
代码清单:构建库-4 GPIO枚举类型定义
123456789 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * GPIO输出速率枚举定义 */ typedef enum {GPIO_Speed_10MHz = 1, // 10MHZ (01)bGPIO_Speed_2MHz, // 2MHZ (10)bGPIO_Speed_50MHz // 50MHZ (11)b } GPIOSpeed_TypeDef;/** * GPIO工作模式枚举定义 */ typedef enum {GPIO_Mode_AIN = 0x0, // 模拟输入 (0000 0000)bGPIO_Mode_IN_FLOATING = 0x04, // 浮空输入 (0000 0100)bGPIO_Mode_IPD = 0x28, // 下拉输入 (0010 1000)bGPIO_Mode_IPU = 0x48, // 上拉输入 (0100 1000)bGPIO_Mode_Out_OD = 0x14, // 开漏输出 (0001 0100)bGPIO_Mode_Out_PP = 0x10, // 推挽输出 (0001 0000)bGPIO_Mode_AF_OD = 0x1C, // 复用开漏输出 (0001 1100)bGPIO_Mode_AF_PP = 0x18 // 复用推挽输出 (0001 1000)b } GPIOMode_TypeDef; |
关于这两个枚举类型的值如何跟端口控制寄存器里面的说明对应起来,我们简单分析下。 有关速度的枚举类型有(01)b 10MHZ、(10)b 2MHZ和(11)b 50MHZ,这三个值跟寄存器说明对得上,很容易理解。 至于模式的枚举类型的值理解起来就比较绕,这让很多人费了脑筋,下面我们通过一个表格来梳理下,好帮助我们理解, 具体见图 GPIO引脚工作模式真值表分析。
如果但从这些枚举值的十六进制来看,很难发现规律,转化成二进制之后,就比较容易发现规律。bit4用来区分端口是输入还是输出, 0表示输入,1表示输出,bit2和bit3对应寄存器的CNFY[1:0]位,是我们真正要写入到CRL和CRH这两个端口控制寄存器中的值。 bit0和bit1对应寄存器的MODEY[1:0]位,这里我们暂不初始化, 在GPIO_Init()初始化函数中用来跟GPIOSpeed的值相加即可实现速率的配置。有关具体的代码分析见GPIO_Init()库函数。 其中在下拉输入和上拉输入中我们设置bit5和bit6的值为01和10来以示区别。
有了这些枚举定义,我们的GPIO_InitTypeDef结构体就可以使用枚举类型来限定输入参数, 见 代码清单:构建库-11。
代码清单:构建库-11 使用枚举定义的GPIO初始化结构体
123456789 10 11 12 13 14 | /** * 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结构体类型赋值配置就变得非常直观,范例见 代码清单:构建库-12。
代码清单:构建库-12给GPIO_InitTypeDef初始化结构体赋值范例
1 2 3 4 5 6 7 8 9 | 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; |
9.3.7. 定义GPIO初始化函数
接着前面的思路,对初始化结构体赋值后,把它输入到GPIO初始化函数,由它来实现寄存器配置。 我们的GPIO初始化函数实现见 代码清单:构建库-13。
代码清单:构建库-13 GPIO初始化函数
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | /** *函数功能:初始化引脚模式 *参数说明: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的低四位暂存在currentmodecurrentmode = ((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~Pin7if (((uint32_t)GPIO_InitStruct->GPIO_Pin &((uint32_t)0x00FF)) != 0x00){// 先备份CRL寄存器的值tmpreg = GPIOx->CRL;// 循环,从Pin0开始配对,找出具体的Pinfor (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对引脚置0GPIOx->BRR = (((uint32_t)0x01) << pinpos);}else{// 判断是否为上拉输入模式if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU){// 上拉输入模式,引脚默认值为1,对BSRR寄存器写1对引脚置1GPIOx->BSRR = (((uint32_t)0x01) << pinpos);}}}}// 把前面处理后的暂存值写入到CRL寄存器之中GPIOx->CRL = tmpreg;}/*--------GPIO CRH 寄存器配置 CRH寄存器控制着高8位IO- -----*/// 配置端口高8位,即Pin8~Pin15if (GPIO_InitStruct->GPIO_Pin > 0x00FF){// // 先备份CRH寄存器的值tmpreg = GPIOx->CRH;// 循环,从Pin8开始配对,找出具体的Pinfor (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可对引脚置0GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));}// 判断是否为上拉输入模式if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU){// 上拉输入模式,引脚默认值为1,对BSRR寄存器写1可对引脚置1GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));}}}// 把前面处理后的暂存值写入到CRH寄存器之中GPIOx->CRH = tmpreg;} } |
这个函数有GPIOx和GPIO_InitStruct两个输入参数,分别是GPIO外设指针和GPIO初始化结构体指针。 分别用来指定要初始化的GPIO端口及引脚的工作模式。
要充分理解这个GPIO初始化函数,得配合我们刚刚分析的GPIO 引脚工作模式真值表来看。
-
先取得GPIO_Mode的值,判断bit4是1还是0来判断是输出还是输入。 如果是输出则设置输出速率,即加上GPIO_Speed的值,输入没有速率之说,不用设置。
-
配置CRL寄存器。通过GPIO_Pin的值计算出具体需要初始化哪个引脚,算出后,然后把需要配置的值写入到CRL寄存器中,具体分析见代码注释。 这里有一个比较有趣的是上/下拉输入并不是直接通过配置某一个寄存器来实现的,而是通过写BSRR或者BRR寄存器来实现。 这让很多只看手册没看固件库底层源码的人摸不着头脑,因为手册的寄存器说明中没有明确的指出如何配置上拉/下拉, 具体见图 上拉_下拉寄存器说明。
-
配置CRH寄存器过程同CRL。
9.3.8. 全新面貌,使用函数点亮LED灯
完成以上的准备后,我们就可以用自己定义的函数来点亮LED灯,见
代码清单:构建库-14 使用函数点亮LED
123456789 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | // 使用固件库点亮LED int main(void) {// 定义一个GPIO_InitTypeDef类型的结构体GPIO_InitTypeDef GPIO_InitStructure;// 开启GPIO端口时钟RCC_APB2ENR |= (1<<4);// 选择要控制的GPIO引脚GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;// 设置引脚模式为通用推挽输出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;// 设置引脚速率为50MHzGPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;// 调用库函数,初始化GPIO引脚GPIO_Init(GPIOC, &GPIO_InitStructure);// 使引脚输出低电平,点亮LED1GPIO_ResetBits(GPIOC,GPIO_Pin_2);while (1){// 使引脚输出低电平,点亮LEDGPIO_ResetBits(GPIOC,GPIO_Pin_2);/*延时一段时间*/Delay(0xFFFF);/*使引脚输出高电平,关闭LED1*/GPIO_SetBits(GPIOC,GPIO_Pin_2);/*延时一段时间*/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的定时器外设进行精确延时的。
9.3.9. 下载验证
把编译好的程序下载到开发板并复位,可看到板子上的灯已经被点亮。
10.1. CMSIS标准及库层次关系
因为基于Cortex系列芯片采用的内核都是相同的,区别主要为核外的片上外设的差异,这些差异却导致软件在同内核,不同外设的芯片上移植困难。 为了解决不同的芯片厂商生产的Cortex微控制器软件 的兼容性问题, ARM与芯片厂商建立了CMSIS标准(Cortex MicroController Software Interface Standard)。
所谓CMSIS标准,实际是新建了一个软件抽象层。见图 CMSIS架构。
CMSIS标准中最主要的为CMSIS核心层,它包括了:
内核函数层:其中包含用于访问内核寄存器的名称、地址定义,主要由ARM公司提供。
设备外设访问层:提供了片上的核外外设的地址和中断定义,主要由芯片生产商提供。
可见CMSIS层位于硬件层与操作系统或用户层之间,提供了与芯片生产商无关的硬件抽象层,可以为接口外设、实时操作系统提供简单的处理器软件接口, 屏蔽了硬件差异,这对软件的移植是有极大的好处的。STM32的库,就是按照CMSIS标准建立的。
10.1.1. 库目录、文件简介
STM32标准库可以从官网获得,也可以直接从本书的配套资料得到。本书讲解的例程全部采用3.5.0库文件。以下内容请大家打开STM32标准库文件配合阅读。
解压库文件后进入其目录:
STM32F10x_StdPeriph_Lib_V3.5.0\
软件库各文件夹的内容说明见图 ST标准库。目录: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文件夹中。
10.1.1.1. CMSIS文件夹。
STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\文件夹展开内容见图 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这个软件的安装目录下,主要作用是提供一些类型定义。 见 代码清单:标准库-1。
代码清单:标准库-1:stdint.h文件中的类型定义
123456789 10 11 | /* 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这个文件夹下面,这里面启动文件有很多个,不同型号的单片机用的启动文件不一样,有关每个启动文件的 详细说明见表
我们开发板中用的STM32F103VET6或者STM32F103ZET6的FLASH都是512K,属于基本型的大容量产品,启动文件统一选择startup_stm32f10x_hd.s。
Stm32f10x.h
这个头文件实现了片上外设的所有寄存器的映射,是一个非常重要的头文件,在内核中与之想对应的头文件是core_cm3.h。
system_stm32f10x.c
system_stm32f10x.c文件实现了STM32的时钟配置,操作的是片上的RCC这个外设。系统在上电之后,首选会执行由汇编编写的启动文件, 启动文件中的复位函数中调用的SystemInit函数就在这个文件里面定义。调用完之后,系统的时钟就被初始化成72M。如果后面我们需要重新配置系统时钟, 我们就可以参考这个函数重写。为了维持库的完整性,我们不会直接在这个文件里面修改时钟配置函数。
10.1.1.2. STM32F10x_StdPeriph_Driver文件夹
文件目录:Libraries\STM32F10x_StdPeriph_Driver
进入libraries目录下的STM32F10x_StdPeriph_Driver文件夹,见图 外设驱动。
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,则至少要把这两个文件包含到工程里。见图 驱动的源文件及头文件。
这两个文件夹中,还有一个很特别的misc.c文件,这个文件提供了外设对内核中的NVIC(中断向量控制器)的访问函数, 在配置中断时,我们必须把这个文件添加到工程中。
10.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见 代码清单:标准库-2。 默认情况下是所以头文件都被包含,没有被注释掉。我们也可以把不要的都注释掉,只留下需要使用的即可。
代码清单:标准库-2 stm32f10x_conf.h文件配置软件库
123456789 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #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这个文件还可配置是否使用“断言”编译选项,见 代码清单:标准库-3。
代码清单:标准库-3 断言配置
123456789 10 11 12 13 14 15 16 | #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宏来去掉断言功能,使程序全速运行。
10.1.2. 库各文件间的关系
前面向大家简单介绍了各个库文件的作用,库文件是直接包含进工程即可,丝毫不用修改,而有的文件就要我们在使用的时候根据具体的需要进行配置。 接下来从整体上把握一下各个文件在库工程中的层次或关系,这些文件对应到CMSIS标准架构上。见图 库各文件关系。
图 库各文件关系 描述了STM32库各文件之间的调用关系,在实际的使用库开发工程的过程中,我们把位于CMSIS层的文件包含进工程, 除了特殊系统时钟需要修改system_stm32f10x.c,其它文件丝毫不用修改,也不建议修改。
对于位于用户层的几个文件,就是我们在使用库的时候,针对不同的应用对库文件进行增删(用条件编译的方法增删)和改动的文件。
10.2. 使帮助文档
我们知道,授之以鱼不如授之以渔。官方资料是所有关于STM32知识的源头,所以在本小节介绍如何使用官方资料。官方的帮助手册, 是最好的教程,几乎包含了所有在开发过程中遇到的问题。这些资料已整理到了本书附录资料中。
10.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》
这个就是本章提到的库的帮助文档,在使用库函数时,我们最好通过查阅此文件来了解标准库提供了哪些外设、 函数原型或库函数的调用的方法。也可以直接阅读源码里面的函数的函数说明。
10.2.2. 初识库函数
所谓库函数,就是STM32的库文件中为我们编写好驱动外设的函数接口,我们只要调用这些库函数,就可以对STM32进行配置, 达到控制目的。我们可以不知道库函数是如何实现的,但我们调用函数必须要知道函数的功能、可传入的参数及其意义、和函数的返回值。
于是,有读者就问那么多函数我怎么记呀?我的回答是:会查就行了,哪个人记得了那么多。所以我们学会查阅库帮助文档 是很有必要的。
打开库帮助文档《stm32f10x_stdperiph_lib_um.chm》见图 库帮助文档
层层打开文档的目录标签:
标签目录:Modules\STM32F10x_StdPeriph_Driver\
可看到STM32F10x _StdPeriph_Driver标签下有很多外设驱动文件的名字MISC、ADC、BKP、CAN等标签。
我们试着查看GPIO的“位设置函数GPIO_SetBits”看看,打开标签:
标签目录:Modules\STM32F10x_StdPeriph_Driver\GPIO\Functions\GPIO_SetBits 见图 库帮助文档的函数说明。
利用这个文档,我们即使没有去看它的具体源代码,也知道要怎么利用它了。
如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 就可以查看这个类型的声明了。