前言
在开发新产品时,想必大家都曾像我一样碰到过一时难以解决的技术难题,在苦恼和无助中,只得求助于互联网,如果在网上突然发现有此问题相关的解决办法,以此解决了困扰了一整天甚至好几天的问题,这时大家的心情想必也是和我一样庆幸并心怀感激的。感谢互联网让我们生活变得如此便利,把世界上的人都聚集到了一起,大家可以畅所欲言;更感激大佬们愿意付出自己宝贵的时间写下各种技术分享,以便其他同仁遇到问题时能愉快地解决问题。本着开源共享,共同进步的精神,我也开始写分享!如有错误或讲述不妥的地方,请务必留言指出,也是帮助其他人避坑,谢谢!
什么是固件库
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:其实利用库函数开发,将应用层和底层分离开,只是好处其一。另一好处在于大家的应用驱动,至少对于底层这一块,大体的风格和调用的函数都是一致的,从而起到标准化程序开发的作用。
以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 的某个缓存获取该变量值,这时可以加快执行速度,但该缓存中的是陈旧数据,与我们要求的寄存器最新状态可能会有出入。
都看到这了,点个赞再走呗~~