解读STM32标准库的程序架构 - 以GPIO操作为例

前言

在开发新产品时,想必大家都曾像我一样碰到过一时难以解决的技术难题,在苦恼和无助中,只得求助于互联网,如果在网上突然发现有此问题相关的解决办法,以此解决了困扰了一整天甚至好几天的问题,这时大家的心情想必也是和我一样庆幸并心怀感激的。感谢互联网让我们生活变得如此便利,把世界上的人都聚集到了一起,大家可以畅所欲言;更感激大佬们愿意付出自己宝贵的时间写下各种技术分享,以便其他同仁遇到问题时能愉快地解决问题。本着开源共享,共同进步的精神,我也开始写分享!如有错误或讲述不妥的地方,请务必留言指出,也是帮助其他人避坑,谢谢!

 
 

什么是固件库

STM32 标准函数库,它是由 ST 公司针对 STM32 提供的函数接口,即API(Application Program Interface),开发者可调用这些函数接口来配置 STM32的寄存器,使开发人员得以脱离最底层的寄存器操作,有开发快速,易于阅读,维护成本低等优点。
如果学过STC89C51单片机肯定知道有个头文件是reg52.h,其中的内容就是将所有的寄存器取一个别名,之后你想对这个寄存器进行操作只需要直接赋值即可
举例:P0 = 0x01;//就是将P0寄存器的bit0置1,其余位清0
STM32当然也可以直接通过指针访问寄存器,举例:

*(unsigned int*)(0xFFFFFFFF) = 0x00000001;//将0xFFFFFFFF地址的寄存器设置为0x00000001

STM32 标准函数库不仅对芯片内所有寄存器取了别名,还将底层一些必要的配置操作全部封装成函数,供用户直接调用来配置底层,让用户即使对相关外设寄存器不熟悉,也能轻易将需要用的片上外设配置完成,从而有更多时间专注于应用层的开发。
下图中,驱动层就是用户针对自己的设备调用库函数提供的接口开发的驱动,比如说最简单的驱动LED灯闪烁,就直接调用库函数提供的GPIO函数控制相应的IO口输出高低电平即可实现。
PS:其实利用库函数开发,将应用层和底层分离开,只是好处其一。另一好处在于大家的应用驱动,至少对于底层这一块,大体的风格和调用的函数都是一致的,从而起到标准化程序开发的作用。
asdad

 
 

以GPIO操作为例详解标准库底层代码

【首先看到stm32f10x.h中相关代码段】

#define     __IO    volatile     //防止编译器优化,在core_cm3.h中
typedef unsigned           int uint32_t;  //stdint.h文件  C99新定义的标准,独立于库函数外

typedef struct
{
    
    
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;
//注意以上结构体成员的排列顺序恰好跟GPIO外设相关寄存器的排列顺序相同

#define PERIPH_BASE           ((uint32_t)0x40000000) //片上外设基地址

#define APB1PERIPH_BASE       PERIPH_BASE  //APB1总线的基地址,与外设基地址相同,表明它是片上外设的第一个外设
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)  //APB2总线基地址
#define AHBPERIPH_BASE        (PERIPH_BASE + 0x20000)  //AHB总线基地址

#define GPIOA_BASE            (APB2PERIPH_BASE + 0x0800)  //GPIOA的基地址
//以上代码通过 (基地址+偏移地址) 的形式 用宏定义封装了所有的寄存器,以上只是抽取其中一部分

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)  

大家注意最后的这句#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)

将GPIOA_BASE强制转换成(GPIO_TypeDef *) 类型的指针,即 ((GPIO_TypeDef *) GPIOA_BASE) 指向的是一个GPIO_TypeDef 类型的结构体,该结构体的定义在前面已经给出了,可以直接通过 ((GPIO_TypeDef *) GPIOA_BASE) 访问到该结构体的成员,比如((GPIO_TypeDef *) GPIOA_BASE) ->BSRR ;由于宏定义,可以写成GPIOA->BSRR;其实这就完成了将寄存器的地址与结构体成员的地址一一对应起来,访问结构体成员就是访问寄存器;

 
 

下面再看看其他GPIO操作相关的代码:
【stm32f10x.h】

#define GPIO_BRR_BR0                         ((uint16_t)0x0001)            /*!< Port x Reset bit 0 */
#define GPIO_BRR_BR1                         ((uint16_t)0x0002)            /*!< Port x Reset bit 1 */
#define GPIO_BRR_BR2                         ((uint16_t)0x0004)            /*!< Port x Reset bit 2 */
#define GPIO_BRR_BR3                         ((uint16_t)0x0008)            /*!< Port x Reset bit 3 */
#define GPIO_BRR_BR4                         ((uint16_t)0x0010)            /*!< Port x Reset bit 4 */
#define GPIO_BRR_BR5                         ((uint16_t)0x0020)            /*!< Port x Reset bit 5 */
#define GPIO_BRR_BR6                         ((uint16_t)0x0040)            /*!< Port x Reset bit 6 */
#define GPIO_BRR_BR7                         ((uint16_t)0x0080)            /*!< Port x Reset bit 7 */
#define GPIO_BRR_BR8                         ((uint16_t)0x0100)            /*!< Port x Reset bit 8 */
#define GPIO_BRR_BR9                         ((uint16_t)0x0200)            /*!< Port x Reset bit 9 */
#define GPIO_BRR_BR10                        ((uint16_t)0x0400)            /*!< Port x Reset bit 10 */
#define GPIO_BRR_BR11                        ((uint16_t)0x0800)            /*!< Port x Reset bit 11 */
#define GPIO_BRR_BR12                        ((uint16_t)0x1000)            /*!< Port x Reset bit 12 */
#define GPIO_BRR_BR13                        ((uint16_t)0x2000)            /*!< Port x Reset bit 13 */
#define GPIO_BRR_BR14                        ((uint16_t)0x4000)            /*!< Port x Reset bit 14 */
#define GPIO_BRR_BR15                        ((uint16_t)0x8000)            /*!< Port x Reset bit 15 */

#define GPIO_LCKR_LCK0                       ((uint32_t)0x00000001)        /*!< Port x Lock bit 0 */
#define GPIO_LCKR_LCK1                       ((uint32_t)0x00000002)        /*!< Port x Lock bit 1 */
#define GPIO_LCKR_LCK2                       ((uint32_t)0x00000004)        /*!< Port x Lock bit 2 */
#define GPIO_LCKR_LCK3                       ((uint32_t)0x00000008)        /*!< Port x Lock bit 3 */
#define GPIO_LCKR_LCK4                       ((uint32_t)0x00000010)        /*!< Port x Lock bit 4 */
#define GPIO_LCKR_LCK5                       ((uint32_t)0x00000020)        /*!< Port x Lock bit 5 */
#define GPIO_LCKR_LCK6                       ((uint32_t)0x00000040)        /*!< Port x Lock bit 6 */
#define GPIO_LCKR_LCK7                       ((uint32_t)0x00000080)        /*!< Port x Lock bit 7 */
#define GPIO_LCKR_LCK8                       ((uint32_t)0x00000100)        /*!< Port x Lock bit 8 */
#define GPIO_LCKR_LCK9                       ((uint32_t)0x00000200)        /*!< Port x Lock bit 9 */
#define GPIO_LCKR_LCK10                      ((uint32_t)0x00000400)        /*!< Port x Lock bit 10 */
#define GPIO_LCKR_LCK11                      ((uint32_t)0x00000800)        /*!< Port x Lock bit 11 */
#define GPIO_LCKR_LCK12                      ((uint32_t)0x00001000)        /*!< Port x Lock bit 12 */
#define GPIO_LCKR_LCK13                      ((uint32_t)0x00002000)        /*!< Port x Lock bit 13 */
#define GPIO_LCKR_LCK14                      ((uint32_t)0x00004000)        /*!< Port x Lock bit 14 */
#define GPIO_LCKR_LCK15                      ((uint32_t)0x00008000)        /*!< Port x Lock bit 15 */
#define GPIO_LCKR_LCKK                       ((uint32_t)0x00010000)        /*!< Lock key */
......//省略

这些都是对相关寄存器的每一个位设立开关宏,以供库函数调用,最主要是为了便于程序阅读。

 
 

【stm32f10x_gpio.h】

typedef enum
{
    
     
  GPIO_Speed_10MHz = 1,
  GPIO_Speed_2MHz, 
  GPIO_Speed_50MHz
}GPIOSpeed_TypeDef;

typedef enum
{
    
     GPIO_Mode_AIN = 0x0,           
  GPIO_Mode_IN_FLOATING = 0x04,  
  GPIO_Mode_IPD = 0x28,          
  GPIO_Mode_IPU = 0x48,          
  GPIO_Mode_Out_OD = 0x14,       
  GPIO_Mode_Out_PP = 0x10,       
  GPIO_Mode_AF_OD = 0x1C,       
  GPIO_Mode_AF_PP = 0x18         
}GPIOMode_TypeDef;

typedef struct
{
    
    
  uint16_t GPIO_Pin; 
  GPIOSpeed_TypeDef GPIO_Speed; 
  GPIOMode_TypeDef GPIO_Mode;    
}GPIO_InitTypeDef;

GPIO_InitTypeDef是GPIO初始化结构体类型,主要用于对相关的GPIO外设作初始化操作。GPIOSpeed_TypeDef和GPIOMode_TypeDef 是两个枚举类型。

下面看相关的库函数代码举例:

uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx)
{
    
    
  /* Check the parameters */
  assert_param(IS_GPIO_ALL_PERIPH(GPIOx)); //断言检查参数
  
  return ((uint16_t)GPIOx->IDR);
}

应用举例:GPIO_read_data = GPIO_ReadInputData(GPIOA);
读整个GPIOA端口的数据,由函数中可以看出事实上就是返回了GPIOA->IDR的值。IDR是端口输入数据寄存器。

void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
    
    
  /* Check the parameters */
  assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
  assert_param(IS_GPIO_PIN(GPIO_Pin));
  
  GPIOx->BSRR = GPIO_Pin;
}

应用举例:GPIO_SetBits(GPIOA, GPIO_Pin_0);
将GPIOA端口的Pin0拉高,由函数中可以看出事实上就是将GPIOA->BSRR赋值为GPIO_Pin_0的值。BSRR是端口位设置/清除寄存器。
在stm32f10x_gpio.h中给出了GPIO_Pin_0的定义如下
#define GPIO_Pin_0 ((uint16_t)0x0001)

stm32f10x_gpio.c文件中最重要的是GPIO_Init函数,函数原型如下:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
该函数主要用于根据传进来的GPIO_TypeDef类型的结构体确定要配置的GPIO端口号,GPIO_InitTypeDef类型的结构体是确定配置的内容,F1的3.5.0标准库主要配置对象是引脚号,引脚模式,引脚翻转速度,由于GPIO_Init函数过长,程序就不在这里贴出来了。

通过库函数,GPIO点灯过程举例:

int main(void)
{
    
    
	GPIO_InitTypeDef GPIO_InitStructure;//定义GPIO_InitTypeDef类型的结构体GPIO_InitStructure用于初始化配置GPIO
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);//打开GPIOB的时钟,因为默认是关闭的
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//选定引脚
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //通用推挽输出模式
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//引脚速率50Hz
	GPIO_Init(GPIOB, &GPIO_InitStructure);//调用GPIO_Init按照上述的参数进行配置

	GPIO_ResetBits(GPIOB,GPIO_Pin_0);//假设LED是低电平点亮,输出低电平即可点亮连接在GPIOB0的LED灯
}

结束语

希望可以帮到大家,有问题可以提出来大家一起探讨,谢谢!不喜勿喷,感谢
 
 

题外话

#ifndef __STM32F10x_H
#define __STM32F10x_H

#endif
经常在每个头文件之前都会看到类似这样的一段代码,这是为了防止头文件重复包含

 
 

#ifdef __cplusplus
extern “C” {
#endif

//代码…

#ifdef __cplusplus
}
#endif
这是为了兼容C++编译器,在C++编译器中,通常会添加宏定义__cplusplus,这样上下两个条件编译就会成立,为了在C++代码中调用用C写成的库文件,就需要用extern"C"来告诉编译器:这是一个用C写成的库文件,请用C的方式来链接它们。这样在C++的工程中也能顺利编译这个C文件

 
 

标准库函数中用于映射寄存器的结构体比如GPIO_TypeDef,其中的结构体成员为什么要用volatile修饰呢?
C 语言中的关键字“volatile”,在 C 语言中该关键字用于表示变量是易变的,要求编译器不要优化。这些结构体内的成员,都代表着寄存器,而寄存器很多时候是由外设或 STM32 芯片状态修改的,也就是说即使 CPU 不执行代码修改这些变量,变量的值也有可能被外设修改、更新,所以每次使用这些变量的时候,我们都要求 CPU 去该变量的地址重新访问。若没有这个关键字修饰,在某些情况下,编译器认为没有代码修改该变量,就直接从 CPU 的某个缓存获取该变量值,这时可以加快执行速度,但该缓存中的是陈旧数据,与我们要求的寄存器最新状态可能会有出入。

都看到这了,点个赞再走呗~~

猜你喜欢

转载自blog.csdn.net/weixin_44788542/article/details/95329243