第11章.创建MDK工程-基于自建库函数

目录

0. 《STM32单片机自学教程》专栏

11.1 基于库函数的开发方式

11.2 构建自己的库函数

11.2.1 头文件的常见操作

11.2.2 外设寄存器结构体定义

11.2.4 外设声明 

11.2.5 GPIO的位操作函数

11.2.5.1 位设置函数

11.2.5.2 位清除函数

11.2.6 定义GPIO初始化函数  

11.2.6.1 GPIO初始化结构体

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

11.2.6.3 定义GPIO 初始化函数  

11.3 基于自己构建库函数的主程序

11.4 程序现象 


0. 《STM32单片机自学教程》专栏

        本文作为专栏《STM32单片机自学教程》专栏其中的一部分,返回专栏总纲,阅读所有文章,点击Link:  

STM32单片机自学教程-[目录总纲]_stm32 学习-CSDN博客       

        本章在上一节的基础上,介绍如何创建库函数,实现点亮LED灯的MDK工程。 我们上一章用寄存器点亮了LED,代码好像没有几行,看着也很简单,但是我们需要明白,我们点亮LED这个案例功能非常简单,只用了STM32功能的九牛一毛。在用寄存器点亮LED的时候,每次配置写代码的时候都要对照着《STM32F10X-中文参考手册》中寄存器的说明,然后根据说明对每个控制的寄存器位写入特定参数,因此在配置的时候非常容易出错,而且代码可读性不强不好理解,难于维护。所以学习STM32最好的方法是用固件库,然后在固件库的基础上了解底层,学习寄存器。懂得原理后,我们开发自然是用已有的固件库去开发效率最高,也便于维护。

11.1 基于库函数的开发方式

        这个问题我们前面第8章已经进行过了介绍,这里再简单提一下。固件库是指“STM32标准函数库”,它是由ST公司针对STM32提供的函数接口,即API(Application Program Interface),开发者可调用这些函数接口来配置STM32的寄存器,使开发人员得以脱离最底层的寄存器操作,有开发快速,易于阅读,维护成本低等优点。当我们调用库API的时候不需要挖空心思去了解库底层的寄存器操作,就像当年我们编程的时候调用某个函数,我们会用就行,并不需要去研究它的源码实现。

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

图11.1-1 固件库开发与寄存器开发对比

        相对于库开发的方式,直接配置寄存器方式生成的代码量的确会少一点,但因为STM32 有充足的资源,权衡库的优势与不足,绝大部分时候,我们愿意牺牲一点CPU 资源,选择库开发。一般只有在对代码运行时间要求极苛刻的地方,才用直接配置寄存器的方式代替,对于库开发与直接配置寄存器的方式,就好比编程是用汇编好还是用C 好一样。

        那么对于STM32的学习哪种方式好呢?有人认为用寄存器好。事实上,库函数的底层实现正是直接配置寄存器的最好例子,它代替我们完成了寄存器配置的工作,而想深入了解芯片是如何工作的,我们只要直接查看库函数的最底层实现就能理解。等我们读懂了库函数的实现方式,一定会为它的严谨和优美的实现方式而倾倒,也是我们学习C语言的极好教材,ST的库实现方式堪称教科书级别的上好资料。所以基于ST库的学习,我们既能学会用寄存器控制STM32,还能学到库函数的封装技巧。

11.2 构建自己的库函数

        构建自己的库函数,其实就是把我们上一节中,寄存器地址计算和一些位操作封装起来到一个.c文件或者头文件中。然后用的时候直接调用即可。

        如图11.2-1,我们和上节一样的方式创建一个MDK工程,命名为LED_LibVersionTest.在文件夹中新建一个stm32f10x.h的空文件,并添加到Startup组里(或者从startup里右键创建也可以),这个文件用于我们后面编写库函数。如图11.2-2.

​图11.2-1 新建库函数的MDK工程

 ​11.2-2 新建自建库函数版MDK工程文件夹

​        后面我们在上节寄存器点亮LED 的代码上继续完善,把代码一层层封装,实现库的最初的雏形,经过这一步的学习后,我们对库的理解和运用会更加深入。本节主要是实现GPIO的函数库,其他外设大同小异,我们直接参考ST标准库即可,不必自己写,懂得原理就够了。

        下面的代码都是标准库里的,我们只是摘出来,了解库的建立过程。举一反三,道理都是一样的。

11.2.1 头文件的常见操作

        在开始后面内容之前我们先讲一个C编程的常见知识点。假如我们编写了一个.c文件,文件中的变量或者函数,是可能被其他文件调用的,我们一般会相应创建一个同名的.h文件,用以对这个.c文件的声明。例如我们创建了一个head.c文件,对应的我们要新建一个head.h文件。而在head.h文件里,开头的语句一般都是固定的防重复包含的预处理指令#ifndef,#define,#endif语句。如下代码所示: 

#ifndef __HEAD_H  
#define __HEAD_H  
  
// ... 这里是头文件的内容,比如函数声明、结构体定义等 ...  
  
#endif // __HEAD_H

        在C语言(以及C++)中,使用#ifndef、#define和#endif预处理指令来防止头文件被多次包含(也称为“包含守卫”或“头文件保护”)是一种常见的做法。这样做的目的是避免在编译时因多次包含同一个头文件而导致的重复定义错误。

        具体来说,#ifndef __HEAD_H检查是否已定义了名为__HEAD_H的宏。如果没有定义(即这是第一次包含该头文件),则编译器会执行#define __HEAD_H,定义这个宏,并继续处理头文件中的其余内容。如果__HEAD_H已经被定义(即这不是第一次包含该头文件),则编译器会跳过头文件中的其余内容,从而避免了重复定义。

        注意:宏名(如__HEAD_H)通常是大写的,并且包含双下划线前缀和后缀,以避免与程序中的其他标识符冲突。

11.2.2 外设寄存器结构体定义

​        上一章我们在操作寄存器的时候,是查到寄存器的绝对地址后,挨个进行配置,如果每个外设寄存器都这样操作,那就太麻烦了。从前面第5章,我们知道外设寄存器的地址都是基于外设基地址加偏移地址,都是在外设基地址上逐个连续递增的,每个寄存器占32 个字节,这种方式跟结构体里面的成员类似。因此我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一样。这样我们操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。

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

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

 图11.2-3 寄存器结构体定义

        代码中结构体成员前增加了前缀“__IO”,代码的第一行#define__IO volatile,指定了C语言中的关键字“volatile”,含义是要求编译器不要优化,这个在前面《第2章.STM32开发C语言常用知识点》已有介绍。    

11.2.3 外设存储器映射 

        外设寄存器结构体定义之后,下一步就是把寄存器地址跟结构体的地址对应起来。映射的方法在上一节以及《第5章.STM32F1x的寄存器和存储器》里已经有提及。这块代码如下:        

/*片上外设基地址  */ 
#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) 

11.2.4 外设声明 

        实现完外设存储器映射后,我们再把外设的基地址进行强制类型转换,转换为我们前面定义的外设寄存器结构体指针类型,然后再把该指针声明成外设名,外设名(即寄存器结构体指针)就跟外设的地址对应起来了,通过该外设名可以直接操作该外设的全部寄存器,代码如下:

/* 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) 

        下面开始,我们就对上节main.c函数中出现的操作函数,一一进行函数定义,再写main.c的时候,就可以直接调用。

11.2.5 GPIO的位操作函数

         现在我们在组“Startup”里再新建2个文件,分别是stm32f10x_gpio.c和stm32f10x_gpio.h。操作方法如下图11.2-4.

图 11.2-4 新建.c和.h文件  

        把上节Main函数中对GPIO外设操作的函数及其宏定义分别存放在stm32f10x_gpio.c和stm32f10x_gpio.h文件中。可以理解为.c文件是用来描述函数的具体的实现方式,.h文件是对这些.c里定义的函数或变量的全局声明。也就是这2个文件都是和GPIO相关的。

        在上一节我们把PB0设置为0的时候,是通过把GPIO的ODR寄存器对应端口直接写入值实现,我们也可以通过BSRR和BRR寄存器对相应位进行置位或清除操作。

11.2.5.1 位设置函数

图11.2-5   STM32F10X-中文参考手册中位设置/清除寄存器BSRR说明

        如上图是BSRR端口设置/清除寄存器的说明, 我们如果要设置PB0为1,只需要设置BSRR寄存器的0位为1即可,即:

GPIOB->BSRR |= 0x0001;

        如果是设置第二位为1就是, GPIOB->BSRR |= 0x0002;第三位就是GPIOB->BSRR |= 0x0004;我们这里会发现一个问题,就是0x0002等不够形象,我们如果用宏定义,用对应的Pin名称来代替就会好很多,于是我们可以这么操作,在stm32f10x_gpio.h对各pin做如下宏定义:

#define GPIO_Pin_0    ((uint16_t)0x0001)  //Pin0 即(00000000 00000001)b 
#define GPIO_Pin_1    ((uint16_t)0x0002)  //Pin1 即(00000000 00000010)b 
#define GPIO_Pin_2    ((uint16_t)0x0004)  //Pin2 即(00000000 00000100)b 
#define GPIO_Pin_3    ((uint16_t)0x0008)  //Pin3 即(00000000 00001000)b 
#define GPIO_Pin_4    ((uint16_t)0x0010)  //Pin4 即(00000000 00010000)b 
#define GPIO_Pin_5    ((uint16_t)0x0020)  //Pin5 即(00000000 00100000)b 
#define GPIO_Pin_6    ((uint16_t)0x0040)  //Pin6 即(00000000 01000000)b 
#define GPIO_Pin_7    ((uint16_t)0x0080)  //Pin7 即(00000000 10000000)b
#define GPIO_Pin_8    ((uint16_t)0x0100)  //Pin8 即(00000001 00000000)b 
#define GPIO_Pin_9    ((uint16_t)0x0200)  //Pin9 即(00000010 00000000)b 
#define GPIO_Pin_10   ((uint16_t)0x0400)  //Pin10 即(00000100 00000000)b 
#define GPIO_Pin_11   ((uint16_t)0x0800)  //Pin11 即(00001000 00000000)b 
#define GPIO_Pin_12   ((uint16_t)0x1000)  //Pin12 即(00010000 00000000)b 
#define GPIO_Pin_13   ((uint16_t)0x2000)  //Pin13 即 (00100000 00000000)b 
#define GPIO_Pin_14   ((uint16_t)0x4000)  //Pin14 即(01000000 00000000)b 
#define GPIO_Pin_15   ((uint16_t)0x8000)  //Pin15 即(10000000 00000000)b 

        在stm32f10x_gpio. c中定义位设置函数GPIO_SetBits如下:

void GPIO_SetBits(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin)
{
	/*
	$函数功能:设置GPIOx对应引脚为高电平
	$参数说明:
		@GPIOx: 该参数为 GPIO_TypeDef 类型的指针,指向GPIO 端口的地址
		@GPIO_Pin: 选择要设置的 GPIO 端口引脚,可输入GPIO_Pin_0-15,表示 GPIOx 端口 0-15 号引脚
	*/	
	GPIOx->BSRR |= GPIO_Pin;
}
11.2.5.2 位清除函数

        位清除函数和位设置函数的操作方式一样,只是需要操作BRR寄存器,如图11.2-6. 这里不再赘述。

图11.2-6   STM32F10X-中文参考手册中位清除寄存器BRR说明

        在stm32f10x_gpio. c中定义位设置函数GPIO_ResetBits如下: 

void GPIO_ResetBits( GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin )
{	
	/*
	$函数功能:设置GPIOx对应引脚为低电平
	$参数说明:
		@GPIOx: 该参数为 GPIO_TypeDef 类型的指针,指向GPIO 端口的地址
		@GPIO_Pin: 选择要设置的 GPIO 端口引脚,可输入GPIO_Pin_0-15,表示 GPIOx 端口 0-15 号引脚
	*/	
	
	GPIOx->BRR |= GPIO_Pin;
}

11.2.6 定义GPIO初始化函数  

        上一节我们知道,除了位操作,还有GPIO工作模式以及速度等的设置。下面我们开始这一部分的功能设计。设计的核心思想其实就是用“名称”去替代那些难以记忆的数字,把一切操作尽量都做到“名称化”,只要看到名称就知道是什么意思,提高代码的可读性和可操作性,不用再每写一个功能就去不停地翻看参考手册。

        那么根据前面一节main.c中这部分的代码,需要“名称化”的内容主要有:GPIO引脚,GPIO速度,GPIO工作模式,以及GPIO的初始化函数。GPIO引脚前面已经实现了,这里就不讲了。

11.2.6.1 GPIO初始化结构体

        为方便后续的GPIO初始化,我们有必要声明一个名为GPIO_InitTypeDef的结构体类型。 我们在头文件stm32f10x_gpio.h中进行如下定义:

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

        定义这个结构体之后,我们以后在初始化某个GPIO前,就可以先定义一个这样的结构体变量,根据需要配置的GPIO模式,对这个结构体的成员进行赋值,最后再把这个变量作为“GPIO初始化函数”的输入参数,该函数能根据这个输入参数值中的内容去配置相应寄存器,从而实现了GPIO的初始化操作。 

        但是我们上述定义的结构体类型,速率和模式仍使用“uint16_t”类型,那么成员值还得输入数字,赋值时还需要查询参考手册的寄存器说明。而实际上像速度和模式只能输入几个固定的数值。我们如何解决这个问题呢,让代码看上去既形象又不易出错?答案就是使用枚举类型。枚举类型可以对结构体成员起到限定输入的作用,只能输入相应已定义的枚举值,而且比较形象,见名知意。

        下面我们就对GPIO的速率和工作模式进行枚举类型定义。     

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

        在上一节中,我们知道GPIO的PB0的工作模式和速率是在CRL寄存器配置的,CRL控制GPIOB的低8位,CRH控制高8位,因为我们用的是0位,这里方便起见,我们就只配置CRL。        

图.11.2-7 CRL寄存器配置图 

GPIO_Speed枚举类型:

        由上图11.2-7可见,GPIO_Speed主要有10MHZ,2MHZ,50MHZ三个值,分别对应二进制数0b01,0b10,0b11,对应十进制的1,2,3.那么定义枚举类型就非常简单了,我们在 stm32f10x_gpio.h中做如下定义:

typedef enum
{ 
  GPIO_Speed_10MHz = 1,  // 10MHZ:(01)b
  GPIO_Speed_2MHz, 		// 2MHZ :(10)b
  GPIO_Speed_50MHz 	   // 50MHZ : (11)b
}GPIOSpeed_TypeDef;

        如上代码中,枚举类型的定义中,第一个给出数字后,后面的如果是比前面得都大1,那么后面的枚举定义可以不用再写“=多少” ,当然写上也是无所谓的。如果不是这种后面比前面大1的关系,就必须每个都进行赋值。

GPIO_Mode枚举类型:

        工作模式的枚举类型定义就比较难理解一些,我们先看代码,代码我们也是直接参考标准库。

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;

        单纯从定义的这些数值,我们很难发现什么规律,可以说之所以这么定义,完全是人为的,在便于理解的前提下通过后续我们编写的函数实现引脚的初始化配置。也就是根据我们人为指定的这个枚举类型,进行工作模式的配置。在引脚的初始化中引脚工作模式和速率是都要指定和配置的,这2个要结合起来看。为了便于理解,整理如下图11.2-8,转化成二进制之后,就比较容易发现规律。

图11.2-8  GPIO 引脚工作模式真值表 

        这个表里的高4位是人为定义的,可以根据个人习惯随意配置,仅仅是为了后续的GPIO初始化函数方便区分,真正要写进寄存器的是bit2和bit3,对应寄存器的CNFY[1:0]位,是我们真正要写入到CRL这个端口控制寄存器中的值。而bit1和bit0之所以都配置为0,主要是后续GPIO的初始化函数里,这2位是由前面的GPIO_Speed定义的。 bit4用来区分端口是输入还是输出,0表示输入,1表示输出。其中在下拉输入和上拉输入中我们设置 bit5 和 bit6 的值为 01 和 10 来以示区别。

        至此,我们就可以对上节的GPIO初始化结构体,再进行改进。 我们的 GPIO_InitTypeDef 结构体就可以使用枚举类型来限定输入参数,也更形象。代码修改如下,unit16_t就可以替换为枚举类型了:

typedef struct
{
	uint16_t GPIO_Pin;
	GPIOSpeed_TypeDef GPIO_Speed;
	GPIOMode_TypeDef GPIO_Mode;
}GPIO_InitTypeDef;
11.2.6.3 定义GPIO 初始化函数  

        在开始写函数之前,需要首先讲一个知识点,否则代码就会看的云里雾里。如前面“图11.2-7 CRL寄存器配置图”,这里面上拉和下拉输入对应的CNF位都是10,并没有说明是怎么配置实现区分的。实际上是而是通过写BSRR 或者 BRR寄存器来实现的。        

        *下拉输入模式,引脚默认置0,对BRR寄存器写1对引脚置0;

        *上拉输入模式,引脚默认值为1,对BSRR寄存器写1对引脚置1;

代码如下:

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
/*
* 函数功能:初始化引脚模式
* 参数说明:GPIOx,该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址
* GPIO_InitTypeDef:GPIO_InitTypeDef 结构体指针,指向初始化变量
*/
  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还是0,即首选判断是输入还是输出模式,bit4是1表示输出,bit4是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与输入参数GPIO_PIN作位与运算,为下面的判断作准备
      currentpin = (GPIO_InitStruct->GPIO_Pin) & ( ((uint32_t)0x01) << pinpos);
			
	  //找到使用的引脚
      if (currentpin !=0)
      {
		pos = pinpos << 2;// pinpos的值左移两位,相等于乘以4,因为寄存器中4个寄存器位配置一个引脚
       //把控制这个引脚的4个寄存器位清零,其它寄存器位不变
        pinmask = ((uint32_t)0x0F) << pos;
        tmpreg &= ~pinmask;
				
        // 向寄存器写入将要配置的引脚的模式
        tmpreg |= (currentmode << pos);  
				
		// 判断是否为下拉输入模式
        if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
        {
		  // 下拉输入模式,引脚默认置0,对BRR寄存器写1可对引脚置0,因为配置为0无影响,可以直接用=覆盖其他位,也可以用|=
          GPIOx->BRR = (((uint32_t)0x01) << pinpos);
        }				
        else
        {
          // 判断是否为上拉输入模式
          if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
          {
		    // 上拉输入模式,引脚默认值为1,对BSRR寄存器写1可对引脚置1,因为配置为0无影响,可以直接用=覆盖其他位,也可以用|=
            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与输入参数GPIO_PIN作位与运算
      currentpin = (GPIO_InitStruct->GPIO_Pin) & ((((uint32_t)0x01) << (pinpos + 0x08)));
			
	 //找到使用的引脚
      if (currentpin !=0)
      {
		//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;
  }
}

下图是对程序中循环体的解释说明:

 图11.2-9 程序循环体说明

11.3 基于自己构建库函数的主程序

        完成以上工作后,我们就可以基于自己写的库函数,点亮LED。为和上次寄存器版本的做区分,这次我们点亮PB1端口。

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

        因为只是为了讲解原理,为使篇幅不至太长,上述代码中,RCC部分我们还没有构建函数,但道理是一样的,有兴趣的朋友可以自己尝试一下。

11.4 程序现象 

        编译下载后,LED成功点亮,如图11.4-1.

图11.4-1 程序现象 

 相关程序文件已上传资源:

https://download.csdn.net/download/weixin_42109443/89410983

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值