敞开成长之旅!这是我参加「日新方案 2 月更文挑战」的第 24 天,点击查看活动概况

前言

  回忆一下,前面点亮led灯咱们都进行了哪些操作。

  首要需求看电路图,然后找到led灯的操控引脚,然后了解了操控引脚的方法是经过操作相应的物理地址,接着知道了能够映射物理地址也便是寄存器,经过寄存器来去装备,最终咱们经过去查找芯片手册,了解各个寄存器的功用,对需求的寄存器进行装备,完结点亮led灯的功用。

  到这儿,咱们成功将一大串的地址转化成可读性更好的寄存器,可是寄存器的操作相关于大部分人来说仍然是太杂乱,大部分人只需求点亮灯,并不想知道它需求用到哪些寄存器,更不想去进行杂乱的位操作,咱们更希望能将寄存器的这些功用再一次进行封装打包,最好是进行一些简略的传参就能够将这个引脚装备好,所以库函数诞生了。库函数的作用,便是将寄存器依据其功用封装成一个个愈加易于调用的函数接口,然后使代码的开发效率更高,可读性更好,愈加易于维护。

1、什么是STM32规范函数库

1.1、界说

  他是ST公司针对stm32设计的一系列函数接口,即API(Application Program Interface)。

1.2、作用

  让开发者可调用这些函数接口来装备 STM32的寄存器,使开发人员得以脱离最底层的寄存器操作。

创建自己的函数库

1.3、比照

  直接代码比照,第一个main函数和第二个main函数所完结的功用是相同的,可是第一个无论是否是开发者自己,都能很清楚理解的看理解代码在干嘛。而第二个main函数,只怕是开发者自己,时间长了也要回头挨条去查一下自己装备这些是在干嘛,一比照,高低立现。

int main(void)
{	
	led_init();
	LED_RED=ON;
	while(1);
}
int main(void)
{
    RCC_AHB1ENR |= (1<<7);
    GPIOH_MODER  &= ~( 0x03<< (2*10));
    GPIOH_MODER |= (1<<2*10);
    GPIOH_OTYPER &= ~(1<<1*10);
    GPIOH_OTYPER |= (0<<1*10);
    GPIOH_OSPEEDR &= ~(0x03<<2*10);
    GPIOH_OSPEEDR |= (0<<2*10);
    GPIOH_PUPDR &= ~(0x03<<2*10);
    GPIOH_PUPDR |= (1<<2*10);
    GPIOH_BSRR |= (1<<16<<10);
    while(1);
}

  到这儿,想说的话现已根本说完,后边的构建自己函数库,是否能搞懂其实并不重要,你只需求会用官方固件库即可。关于新手来说,我觉得必定要注意的是:悉数的悉数是围绕着目标去展开。 无论是地址,仍是寄存器,亦或是库函数,都只是咱们操控单片机的手法,能把这些全搞懂,很好很牛;只懂库函数操作去完结目标,也很好很牛。

2、构建库函数

2.1、修正寄存器地址封装

  首要咱们要知道,寄存器地址是基于物理地址的偏移地址,他们是连续的,和结构体的成员变量联系相似,所以咱们能够经过结构体的形势来进行封装,将寄存器映射为结构体变量,再经过结构体变量,宏界说等方式来完结可读性的提高。

  代码如下(示例):

//volatile 表明易变的变量,防止编译器优化
#define __IO volatile
typedef unsigned int uint32_t;
typedef unsigned short uint16_t;
/* GPIO 寄存器列表 */
typedef struct {
    __IO uint32_t MODER; /*GPIO 形式寄存器 地址偏移: 0x00 */
    __IO uint32_t OTYPER; /*GPIO 输出类型寄存器 地址偏移: 0x04 */
    __IO uint32_t OSPEEDR; /*GPIO 输出速度寄存器 地址偏移: 0x08 */
    __IO uint32_t PUPDR; /*GPIO 上拉/下拉寄存器 地址偏移: 0x0C */
    __IO uint32_t IDR; /*GPIO 输入数据寄存器 地址偏移: 0x10 */
    __IO uint32_t ODR; /*GPIO 输出数据寄存器 地址偏移: 0x14 */
    __IO uint16_t BSRRL; /*GPIO 置位/复位寄存器低 16 位部分 地址偏移: 0x18 */
    __IO uint16_t BSRRH; /*GPIO 置位/复位寄存器 高 16 位部分地址偏移: 0x1A */
    __IO uint32_t LCKR; /*GPIO 装备锁定寄存器 地址偏移: 0x1C */
    __IO uint32_t AFR[2]; /*GPIO 复用功用装备寄存器 地址偏移: 0x20-0x24 */
} GPIO_TypeDef;
/*RCC 寄存器列表*/
typedef struct {
    __IO uint32_t CR; /*!< RCC 时钟操控寄存器,地址偏移: 0x00 */
    __IO uint32_t PLLCFGR; /*!< RCC PLL 装备寄存器,地址偏移: 0x04 */
    __IO uint32_t CFGR; /*!< RCC 时钟装备寄存器,地址偏移: 0x08 */
    __IO uint32_t CIR; /*!< RCC 时钟中断寄存器,地址偏移: 0x0C */
    __IO uint32_t AHB1RSTR; /*!< RCC AHB1 外设复位寄存器,地址偏移: 0x10 */
    __IO uint32_t AHB2RSTR; /*!< RCC AHB2 外设复位寄存器,地址偏移: 0x14 */
    __IO uint32_t AHB3RSTR; /*!< RCC AHB3 外设复位寄存器,地址偏移: 0x18 */
    __IO uint32_t RESERVED0; /*!< 保存, 地址偏移:0x1C */
    __IO uint32_t APB1RSTR; /*!< RCC APB1 外设复位寄存器,地址偏移: 0x20 */
    __IO uint32_t APB2RSTR; /*!< RCC APB2 外设复位寄存器,地址偏移: 0x24*/
    __IO uint32_t RESERVED1[2]; /*!< 保存,地址偏移:0x28-0x2C*/
    __IO uint32_t AHB1ENR; /*!< RCC AHB1 外设时钟寄存器,地址偏移: 0x30 */
    __IO uint32_t AHB2ENR; /*!< RCC AHB2 外设时钟寄存器,地址偏移: 0x34 */
    __IO uint32_t AHB3ENR; /*!< RCC AHB3 外设时钟寄存器,地址偏移: 0x38 */
    /*RCC 后边还有许多寄存器,此处省略*/
} RCC_TypeDef;

  简略剖析一下代码,前面几行将volatile,unsigned int,unsigned short这几种关键字进行了宏界说,接着用这些宏界说后的关键字创立了一个姓名为GPIO_TypeDef结构体和一个姓名为RCC_TypeDef的结构体。

  看到这,先提出几个问题,为什么不直接用C语言所支撑的关键字而将其进行宏界说后,再用宏界说装备?后边创立的结构体所依据的是什么,成员变量界说的依据是什么?

  先说第一个,我认为是便利移植更新,c语言中的关键字在其它语言中可能并不能生效,这样做的好处是,假如volatile,int这些类型在其他渠道是叫其他姓名,那么只需求将这个当地一替换那么整个代码都将会替换掉,这样能够很好地移植或者更新。相反假如你直接用的是关键字,那么则要将悉数用到这个关键字的当地悉数替换掉。从这儿咱们应该学到一个很重要的经验,关于一些高频用到的又可能有改动的变量,关键字等等,用宏界说去界说一下再去运用,能够在你修正代码时十分便利。

  第二个结构体的创立所依据的是芯片手册,而变量的姓名,排序及其巨细都是按照芯片手册中寄存器的姓名,排序,及其巨细去设计的。

创建自己的函数库

2.2、界说拜访的结构体指针和引脚

  代码如下(示例):

/*界说 GPIOA-H 寄存器结构体指针*/
#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)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
/*界说 RCC 外设 寄存器结构体指针*/
#define RCC ((RCC_TypeDef *) RCC_BASE)
/*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) /*!< 挑选悉数引脚 */

  有了这两组界说,接下来就能够写封装函数了。

3、创立封装函数

3.1、创立拉低引脚函数

  代码如下(示例):

void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
    GPIOx->BSRRH = GPIO_Pin;
}

  创立的函数有两个传参,一个是GPIO类型,一个是引脚号。也便是说咱们想将某一个引脚拉低,只需求调用这个函数,将对应引脚的类型和引脚号填写上即可。比方咱们之前操控PH10引脚。

  代码如下(示例):

GPIO_ResetBits(GPIOH,GPIO_Pin_10);

3.2、创立引脚初始化函数

  接下来创立一个杂乱一点的端口初始化函数,首要依据上一篇咱们知道了要装备一个端口,需求对引脚号、工作形式、输出速率、输出类型以及上/下拉形式这些进行装备。那么咱们就以此创立结构体。   代码如下:

typedef uint8_t unsigned char;
/**
* GPIO 初始化结构体类型界说
*/
typedef struct {
    /*!< 挑选要装备的 GPIO 引脚可输入 GPIO_Pin_ 界说的宏 */
    uint32_t GPIO_Pin;
    /*!< 挑选 GPIO 引脚的工作形式可输入二进制值: 00 、01、 10、 11表明输入/输出/复用/模拟 */
    uint8_t GPIO_Mode;
    /*!< 挑选 GPIO 引脚的速率可输入二进制值: 00 、01、 10、 11表明 2/25/50/100MHz */
    uint8_t GPIO_Speed;
    /*!< 挑选GPIO引脚输出类型可输入二进制值: 0 、1表明推挽/开漏 */
    uint8_t GPIO_OType;
    /*!<挑选GPIO引脚的上/下拉形式可输入二进制值: 00 、01、 10表明浮空/上拉/下拉*/
    uint8_t GPIO_PuPd;
} GPIO_InitTypeDef;

  假如这样装备的话,那么每个变量赋值仍然是要进行位操作赋值,依旧很欠好识别,所以咱们能够经过创立枚举来处理这个问题。

  代码如下:

typedef enum {
    GPIO_Mode_IN = 0x00,    /*!< 输入形式 */
    GPIO_Mode_OUT = 0x01,   /*!< 输出形式 */
    GPIO_Mode_AF = 0x02,    /*!< 复用形式 */
    GPIO_Mode_AN = 0x03     /*!< 模拟形式 */
} GPIOMode_TypeDef;
/**
* GPIO 输出类型枚举界说
*/
typedef enum {
    GPIO_OType_PP = 0x00,   /*!< 推挽形式 */
    GPIO_OType_OD = 0x01    /*!< 开漏形式 */
} GPIOOType_TypeDef;
/**
* GPIO 输出速率枚举界说
*/
typedef enum {
    GPIO_Speed_2MHz = 0x00,     /*!< 2MHz */
    GPIO_Speed_25MHz = 0x01,    /*!< 25MHz */
    GPIO_Speed_50MHz = 0x02,    /*!< 50MHz */
    GPIO_Speed_100MHz = 0x03    /*!<100MHz */
} GPIOSpeed_TypeDef;
/**
*GPIO 上/下拉装备枚举界说
*/
typedef enum {
    GPIO_PuPd_NOPULL = 0x00,    /*浮空*/
    GPIO_PuPd_UP = 0x01,        /*上拉*/
    GPIO_PuPd_DOWN = 0x02       /*下拉*/
} GPIOPuPd_TypeDef;

  然后经过这些枚举去界说开始的结构体成员。

  代码如下:

typedef struct {
    /*!< 挑选要装备的 GPIO 引脚可输入 GPIO_Pin_ 界说的宏 */
    uint32_t GPIO_Pin; 
    /*!< 挑选 GPIO 引脚的工作形式可输入 GPIOMode_TypeDef 界说的枚举值*/
    GPIOMode_TypeDef GPIO_Mode; 
    /*!< 挑选 GPIO 引脚的速率可输入 GPIOSpeed_TypeDef 界说的枚举值 */
    GPIOSpeed_TypeDef GPIO_Speed; 
    /*!< 挑选 GPIO 引脚输出类型可输入 GPIOOType_TypeDef 界说的枚举值*/
    GPIOOType_TypeDef GPIO_OType; 
    /*!<挑选 GPIO 引脚的上/下拉形式可输入 GPIOPuPd_TypeDef 界说的枚举值*/
    GPIOPuPd_TypeDef GPIO_PuPd; 
} GPIO_InitTypeDef;

  这样,在咱们装备时,只需求给变量附上对应的枚举值就好了。

  代码如下:

GPIO_InitTypeDef InitStruct;
/* LED 端口初始化 */
/*挑选要操控的 GPIO 引脚*/
InitStruct.GPIO_Pin = GPIO_Pin_10;
/*设置引脚形式为输出形式*/
InitStruct.GPIO_Mode = GPIO_Mode_OUT;
/*设置引脚的输出类型为推挽输出*/
InitStruct.GPIO_OType = GPIO_OType_PP;
/*设置引脚为上拉形式*/
InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
/*设置引脚速率为 2MHz */
InitStruct.GPIO_Speed = GPIO_Speed_2MHz;

  这样,咱们将InitStruct这个结构体的各个成员都赋上值了,接着便是创立一个函数,来处理这个结构体的值。

  代码如下:

/**
*函数功用:初始化引脚形式
*参数阐明:GPIOx,该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址
* GPIO_InitTypeDef:GPIO_InitTypeDef 结构体指针,指向初始化变量
*/
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
    uint32_t pinpos = 0x00, pos = 0x00 , currentpin = 0x00;
    /*-- GPIO Mode Configuration --*/
    for (pinpos = 0x00; pinpos < 16; pinpos++) {
        /*以下运算是为了经过 GPIO_InitStruct->GPIO_Pin 算出引脚号 0-15*/
        /*经过运算后 pos 的 pinpos 位为 1,其余为 0,与 GPIO_Pin_x 宏对应。pinpos 变量每次循环加 1,*/
        pos = ((uint32_t)0x01) << pinpos;
        /* pos 与 GPIO_InitStruct->GPIO_Pin 做 & 运算,若运算结果 currentpin == pos,
        则表明 GPIO_InitStruct->GPIO_Pin 的 pinpos 位也为 1,
        然后可知 pinpos 便是 GPIO_InitStruct->GPIO_Pin 对应的引脚号:0-15*/
        currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
        /*currentpin == pos 时执行初始化*/
        if (currentpin == pos) {
            /*GPIOx 端口,MODER 寄存器的 GPIO_InitStruct->GPIO_Pin 对应的引脚,MODER 位清空*/
            GPIOx->MODER &= ~(3 << (2 *pinpos));
            /*GPIOx 端口,MODER 寄存器的 GPIO_Pin 引脚, MODER 位设置"输入/输出/复用输出/模拟"形式*/
            GPIOx->MODER |= (((uint32_t)GPIO_InitStruct->GPIO_Mode) << (2 *pinpos));
            /*GPIOx 端口,PUPDR 寄存器的 GPIO_Pin 引脚,PUPDR 位清空*/
            GPIOx->PUPDR &= ~(3 << ((2 *pinpos)));
            /*GPIOx 端口,PUPDR 寄存器的 GPIO_Pin 引脚,PUPDR 位设置"上/下拉"形式*/
            GPIOx->PUPDR |= (((uint32_t)GPIO_InitStruct->GPIO_PuPd) << (2 *pinpos));
            /*若形式为"输出/复用输出"形式,则设置速度与输出类型*/
            if ((GPIO_InitStruct->GPIO_Mode == GPIO_Mode_OUT) ||
                (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_AF)) {
                /*GPIOx 端口,OSPEEDR 寄存器的 GPIO_Pin 引脚,OSPEEDR 位清空*/
                GPIOx->OSPEEDR &= ~(3 << (2 *pinpos));
                /*GPIOx 端口,OSPEEDR 寄存器的 GPIO_Pin 引脚,OSPEEDR 位设置输出速度*/
                GPIOx->OSPEEDR |= ((uint32_t)(GPIO_InitStruct->GPIO_Speed)<<(2 *pinpos));
                /*GPIOx 端口,OTYPER 寄存器的 GPIO_Pin 引脚,OTYPER 位清空*/
                GPIOx->OTYPER &= ~(1 << (pinpos)) ;
                /*GPIOx 端口,OTYPER 位寄存器的 GPIO_Pin 引脚,OTYPER 位设置"推挽/开漏"输出类型*/
                GPIOx->OTYPER |= (uint16_t)(( GPIO_InitStruct->GPIO_OType)<< (pinpos));
            }
        }
    }
}

  读一下这个函数,有两个传参,第一个是端口类型,也便是之前咱们创立的那些GPIOx指针(x=A…H),第二个便是咱们刚刚赋值的结构体,然后函数内部将结构体变量的值传给对应寄存器,最终操控电路板完结端口初始化。

  咱们要想完结拉低PH10引脚,只需求调用这两个函数便能完结。

  代码如下:

int main(void)
{
    GPIO_InitTypeDef InitStruct;
    /*敞开 GPIOH 时钟,运用外设时都要先敞开它的时钟*/
    RCC->AHB1ENR |= (1<<7);
    /* LED 端口初始化 */
    /*初始化 PH10 引脚*/
    /*挑选要操控的 GPIO 引脚*/
    InitStruct.GPIO_Pin = GPIO_Pin_10;
    /*设置引脚形式为输出形式*/
    InitStruct.GPIO_Mode = GPIO_Mode_OUT;
    /*设置引脚的输出类型为推挽输出*/
    InitStruct.GPIO_OType = GPIO_OType_PP;
    /*设置引脚为上拉形式*/
    InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    /*设置引脚速率为 2MHz */
    InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    /*调用库函数,运用上面装备的 GPIO_InitStructure 初始化 GPIO*/
    GPIO_Init(GPIOH, &InitStruct);
    /*使引脚输出低电平,点亮 LED1*/
    GPIO_ResetBits(GPIOH,GPIO_Pin_10);
    while (1);
}

总结

  这一篇主要篇幅比较长,主要想分享为什么要有库函数,以及库函数为什么要这么去写,这么写的好处是什么,在今后的应用中,咱们很少需求自己去写库函数,规范库函数现已满意咱们绝大部分的需求了,咱们只需求去调用。不过相关于学习自身,我更希望分享如何去学习,这样才干触类旁通,在这个科技与狠活快速更新的年代,始终跟的上脚步。