STM32F103学习记录-----构建库函数雏形

来源:互联网 发布:华为云数据 编辑:程序博客网 时间:2024/05/14 16:24

什么是固件库

固件库是指“STM32标准函数库”,即API(Application Program Interface)。
库是架设在寄存器与用户驱动层之间的代码,向下处理与寄存器直接相关的配置,向上为用户提供配置寄存器的接口。
这里写图片描述

步骤

封装寄存器

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

外设存储器映射

外设寄存器结构体已经定义了,但是要想将这个结构体赋值就达到操作寄存器的效果,所以我们还要找到这个寄存器的地址,就把寄存器地址跟结构体的地址对应起来。所以我们要再找到外设的地址。
这里写图片描述

外设声明

Created with Raphaël 2.1.0外设寄存器结构体外设寄存器结构体寄存器地址寄存器地址我该怎么直接访问你?你需要写一个外部声明。

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

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

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

仔细观察就能发现其中的差别,只是把“_”换成了“->”。


理解这些之后,接下来开始封装GPIO 的基本操作,为了以后直接调用定义的函数而不是直接用寄存器。针 对 GPIO 外 设 操 作 的 函 数 及 其 宏 定 义 分 别 存 放 在 “ stm32f10x_gpio.c”和”stm32f10x_gpio.h”文件中,这两个文件需要自己新建。
在.c文件中定义两个位操作函数,分别控制引脚输出高电平和低电平。

void GPIO_SetBits(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin){    GPIOx->BSRR = GPIO_Pin;}void GPIO_ResettBits(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin){    GPIOx->BRR = GPIO_Pin;}

GPIOx是一个指针变量,赋予GPIOA、GPIOB、GPIOC等结构体指针值,函数可以控制相应的GPIOA、GPIOB、GPIOC等端口的输出。具体寄存器的操作可以参考stm32参考手册。
这里写图片描述
这里写图片描述

位操作的函数就可以这样使用了

/*控制 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));

这样使用引脚还是有些麻烦,如果我们直接把引脚也用宏定义的方式包装一下是不是就会好看许多。

#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所有引脚。

/*控制 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”表示“复位”,即低电平)。

定义初始化结构体 GPIO_InitTypeDef

位操作虽然能使得GPIO输出电平的代码得到简化,但是控制GPIO前还需要初始化GPIO引脚的各种模式,且涉及的寄存器有很多,所有固件库中采用声明一个名为GPIO_InitTypeDef 的结构体类型,将GPIO初始化涉及的初始化参数以结构体的形式封装起来。

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

该结构体中包含引脚号、工作模式及输出速率。

定义引脚模式的枚举类型

这里写图片描述
这里写图片描述
GPIO_Speed 和 GPIO_Mode 这两个成员对应的寄存器是 CRL 和 CRH 这两个端口配置寄存器。根据寄存器配置出如下的枚举类型。

 /** * 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 = 0x00, // 模拟输入 (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;

速率根据寄存器能直接理解。至于GPIO的模式如何理解,通过一张图也能很好地理解。
这里写图片描述
bit4 用来区分端口是输入还是输出,0 表示输入,1 表示输出,bit2 和 bit3 对应寄存器的CNF Y [1:0]位,是我们真正要写入到CRL和CRH这两个端口控制寄存器中的值。
使用枚举定义的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初始化函数

void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct){    .........;}

具体看函数还是在例程的固件库中看。

通过总结这章的知识点,拎清了固件库和寄存器地址是怎么连接的,明白了固件库大体的编写思路,看懂了GPIO的寄存器移位操作,同时也了解了结构体和枚举。如上资料取自野火零死角。

学路漫漫。

0 0