[STM32学习笔记(三)]初识库函数(上)

本文介绍STM32标准函数库的概念与优势,并通过实例演示如何构建库函数雏形,包括外设寄存器结构体定义、存储器映射、外设声明以及位操作函数的定义。

3.1 什么是STM32库函数

3.1.1 定义

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

当我们调用库 API 的时候不需要挖空心思去了解库底层的寄存器操作,就像刚开始学习 C 语言的时候,用 prinft()函数时只是学习它的使用格式,不需要去研究它的源码实现。

实际上, 是架设在寄存器用户驱动层之间的代码,向下处理与寄存器直接相关的配置,向上为用户提供配置寄存器的接口。
图1

3.1.2 为什么采用库来开发和学习

直接配置寄存器方式的缺点:

  1. 开发速度慢
  2. 程序可读性差
  3. 维护复杂

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

1、STM32F1 系列和 STM32F4 系列各有一套自己的函数库,但是它们大部分是兼容的, F1 和 F4 之间的程序移植,只需要小修改即可。
2、而如果要移植用寄存器写的程序, 那简直跟脱胎换骨差不多。

3.2 实验:构建库函数雏形

3.2.1 外设寄存器结构体定义

[STM32学习笔记(二)] 使用寄存器点亮LED小灯中,直接操作寄存器,都是在操作寄存器的绝对地址非常麻烦!!
外设寄存器的地址都是基于外设基地址的偏移地址,都是在外设基地址上逐个连续递增的,每个寄存器占 32 个字节这种方式跟结构体里面的成员类似
在工程中的“stm32f10x.h”文件中,我们使用结构体封装 GPIO 及 RCC 外设的的寄存器。结构体成员的顺序按照寄存器的偏移地址从低到高排列,成员类型跟寄存器类型一样。
下面对比两种使用结构体和不使用结构体写的“stm32f10x.h”文件:

1、不使用结构体

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

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

/*GPIOB外设基地址*/
#define GPIOB_BASE            (APB2PERIPH_BASE + 0x0C00)

/* GPIOB寄存器地址,强制转换成指针 */
#define GPIOB_CRL			*(unsigned int*)(GPIOB_BASE+0x00)
#define GPIOB_CRH			*(unsigned int*)(GPIOB_BASE+0x04)
#define GPIOB_IDR			*(unsigned int*)(GPIOB_BASE+0x08)
#define GPIOB_ODR			*(unsigned int*)(GPIOB_BASE+0x0C)
#define GPIOB_BSRR	  *(unsigned int*)(GPIOB_BASE+0x10)
#define GPIOB_BRR			*(unsigned int*)(GPIOB_BASE+0x14)
#define GPIOB_LCKR		*(unsigned int*)(GPIOB_BASE+0x18)

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

2、使用结构体

//寄存器的值常常是芯片外设自动更改的,即使 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 语言中该关键字用于表示变量是易变的,要求编译器不要优化

3.2.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.2.3 外设声明

1、首先定义好外设寄存器结构体
2、然后实现外设存储器映射
3、再把外设的基址强制类型转换成相应的外设寄存器结构体指针
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)

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

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

// 使用寄存器结构体指针点亮 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.2.4 定义位操作函数

使用在“stm32f10x_gpio.c”文件定义两个位操作函数举例
这两个函数分别用于控制引脚输出高电平和低电平

两个函数的代码如下:

/*
*函数功能:设置引脚为高电平
*参数说明: 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;
}

去除了注释后

void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{//函数功能:设置引脚为高电平
	GPIOx->BSRR = GPIO_Pin;
}
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{//函数功能:设置引脚为低电平
	GPIOx->BRR = GPIO_Pin;
}

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

位操作函数案例

/*控制 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 个引脚的操作数都定义成宏,代码如下:

/*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控制代码:

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值