STM32基础--自己构建库函数
什么是 STM32 函数库
固件库是指“STM32 标准函数库”,它是由 ST 公司针对 STM32 提供的函数接口,即API (Application Program Interface),开发者可调用这些函数接口来配置 STM32 的寄存器,使开发人员得以脱离最底层的寄存器操作,有开发快速,易于阅读,维护成本低等优点。当我们调用库 API 的时候不需要挖空心思去了解库底层的寄存器操作,就像当年我们刚开始学习 C 语言的时候,用 prinft() 函数时只是学习它的使用格式,并没有去研究它的源码实现,但需要深入研究的时候,经过千锤百炼的库源码就是最佳学习范例。实际上,库是架设在寄存器与用户驱动层之间的代码,向下处理与寄存器直接相关的配置,向上为用户提供配置寄存器的接口。库开发方式与直接配置寄存器方式的区别见图固件库开发与寄存器开发对比图。
为什么采用库来开发及学习
在以前 8 位机时代的程序开发中,一般直接配置芯片的寄存器,控制芯片的工作方式,如中断,定时器等。配置的时候,常常要查阅寄存器表,看用到哪些配置位,为了配置某功能,该置 1 还是置 0。这些都是很琐碎的、机械的工作,因为 8 位机的软件相对来说较简单,而且资源很有限,所以可以直接配置寄存器的方式来开发。
对于 STM32,因为外设资源丰富,带来的必然是寄存器的数量和复杂度的增加,这时直接配置寄存器方式的缺陷就突显出来了:
(1) 开发速度慢
(2) 程序可读性差
(3) 维护复杂
这些缺陷直接影响了开发效率,程序维护成本,交流成本。库开发方式则正好弥补了这些缺陷。
而坚持采用直接配置寄存器的方式开发的程序员,会列举以下原因:
(1) 具体参数更直观
(2) 程序运行占用资源少
相对于库开发的方式,直接配置寄存器方式生成的代码量的确会少一点,但因为 STM32 有充足的资源,权衡库的优势与不足,绝大部分时候,我们愿意牺牲一点 CPU 资源,选择库开发。一般只有在对代码运行时间要求极苛刻的地方,才用直接配置寄存器的方式代替,如频繁调用的中断服务函数。对于库开发与直接配置寄存器的方式,就好比编程是用汇编好还是用 C 好一样。
在 STM32F1 系列刚推出函数库时引起程序员的激烈争论,但是,随着 ST 库的完善与大家对库的了解,更多的程序员选择了库开发。现在 STM32F1 系列和 STM32F4 系列各有一套自己的函数库,但是它们大部分是兼容的,F1 和 F4 之间的程序移植,只需要小修改即可。而如果要移植用寄存器写的程序,那简直跟脱胎换骨差不多。
构建库函数雏形
虽然库的优点多多,但很多人对库还是很忌惮,因为一开始用库的时候有很多代码,很多文件,不知道如何入手。不知道您是否认同这么一句话:一切的恐惧都来源于无知。我们对库忌惮那是因为我们不知道什么是库,不知道库是怎么实现的。接下来,我们在寄存器点亮 LED 的代码上继续完善,把代码一层层封装,实现库的最初的雏形。
外设寄存器结构体定义(写在stm32f10x.h中)
我们在操作寄存器的时候,操作的是都寄存器的绝对地址,如果每个外设寄存器都这样操作,那将非常麻烦。我们考虑到外设寄存器的地址都是基于外设基地址的偏移地址,都是在外设基地址上逐个连续递增的,每个寄存器占 32 个字节,这种方式跟结构体里面的成员类似。所以我们可以定义一种外设结构体,结构体的地址等于外设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一样。这样我们操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的全部寄存器,即操作结构体的成员即可。在工程中的“stm32f10x.h”文件中,我们使用结构体封装 GPIO 及 RCC 外设的的寄存器,如下。结构体成员的顺序按照寄存器的偏移地址从低到高排列,成员类型跟寄存器类
型一样。
//寄存器的值常常是芯片外设自动更改的,即使 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;// RCC 寄存器结构体定义
typedef struct
{uint32_t CR;uint32_t CFGR;uint32_t CIR;uint32_t APB2RSTR;uint32_t APB1RSTR;uint32_t AHBENR;uint32_t APB2ENR;uint32_t APB1ENR;uint32_t BDCR;uint32_t CSR;
}RCC_TypeDef;
这段代码在每个结构体成员前增加了一个“__IO”前缀,它的原型在这段代码的第一行,代表了C 语言中的关键字“volatile”,在 C 语言中该关键字用于表示变量是易变的,要求编译器不要优化。这些结构体内的成员,都代表着寄存器,而寄存器很多时候是由外设或 STM32 芯片状态修改的,也就是说即使 CPU 不执行代码修改这些变量,变量的值也有可能被外设修改、更新,所以每次使用这些变量的时候,我们都要求 CPU 去该变量的地址重新访问。若没有这个关键字修饰,在某些情况下,编译器认为没有代码修改该变量,就直接从 CPU 的某个缓存获取该变量值,这时可以加快执行速度,但该缓存中的是陈旧数据,与我们要求的寄存器最新状态可能会有出入。(将白了就是可能有脏数据,也就是我们有时候说的玄学问题)
外设存储器映射(写在stm32f10x.h中)
外设寄存器结构体定义仅仅是一个定义,要想实现给这个结构体赋值就达到操作寄存器的效果,我们还需要找到该寄存器的地址,就把寄存器地址跟结构体的地址对应起来。所以我们要再找到外设的地址,根据我们前面的学习,我们可以把这些外设的地址定义成一个个宏,实现外设存储器的映射。
/* 片上外设基地址*/
#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)
外设声明 (写在stm32f10x.h中)
定义好外设寄存器结构体,实现完外设存储器映射后,我们再把外设的基址强制类型转换成相应的外设寄存器结构体指针,然后再把该指针声明成外设名,这样一来,外设名就跟外设的地址对应起来了,而且该外设名还是一个该外设类型的寄存器结构体指针,通过该指针可以直接操作该外设的全部寄存器,如下。
// 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)
首先通过强制类型转换把外设的基地址转换成 GPIO_TypeDef 类型的结构体指针,然后通过宏定义把 GPIOA、GPIOB 等定义成外设的结构体指针,通过外设的结构体指针我们就可以达到访问外设的寄存器的目的。
C语言小知识,条件编译
/*
* C 语言知识,条件编译
* #if 为真
* 执行这里的程序
* #else
* 否则执行这里的程序
* #endif
*/
通过操作外设结构体指针的方式,我们把 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”文件中,这两个文件需要自己新建。
定义位操作函数(写在stm32f10x_gpio.c中)
在“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;
}
这两个函数体内都是只有一个语句,对 GPIOx 的 BSRR 或 BRR 寄存器赋值,从而设置引脚为高电平或低电平,操作 BSRR 或者 BRR 可以实现单独的操作某一位,有关这两个的寄存器说明见图 BSRR 寄存器说明 和图 BRR 寄存器说明。其中 GPIOx 是一个指针变量,通过函数的输入参数我们可以修改它的值,如给它赋予 GPIOA、GPIOB、GPIOH 等结构体指针值,这个函数就可以控制相应的 GPIOA、GPIOB、GPIOH 等端口的输出。
验证(写在main.c中)
利用这两个位操作函数,可以方便地操作各种 GPIO 的引脚电平,控制各种端口引脚的范例如下:
// 开启 GPIOB 端口时钟RCC->APB2ENR |= (1<<3);//清空控制 PB0,4,5 的端口位GPIOB->CRL &= ~( 0x0F<< (4*0));GPIOB->CRL &= ~( 0x0F<< (4*4));GPIOB->CRL &= ~( 0x0F<< (4*5));// 配置 PB0,4,5 为通用推挽输出,速度为 10MGPIOB->CRL |= (1<<4*0);GPIOB->CRL |= (1<<4*4);GPIOB->CRL |= (1<<4*5); /* 控制 GPIOB 的引脚 0 输出高电平 */GPIO_SetBits(GPIOB,(uint16_t)(1<<0));/* 控制 GPIOB 的引脚 0 输出低电平 */GPIO_ResetBits(GPIOB,(uint16_t)(1<<0)); /* 控制 GPIOB 的引脚 0、引脚 4 输出高电平,使用“|”同时控制多个引脚 */GPIO_SetBits(GPIOB,(uint16_t)(1<<0)|(uint16_t)(1<<4));/* 控制 GPIOB 的引脚 0、引脚 4 输出低电平 */GPIO_ResetBits(GPIOB,(uint16_t)(1<<0)|(uint16_t)(1<<4));
封装引脚(写在stm32f10x.h中)
使用以上函数输入参数,设置引脚号时,还是稍感不便,为此我们把表示 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)/*!< 选择全部引脚 */
验证(写在main.c中)
这些宏代表的参数是某位置“1”其它位置“0”的数值,其中最后一个“GPIO_Pin_ALL”是所有数据位都为“1”,所以用它可以一次控制设置整个端口的 0-15 所有引脚。利用这些宏,GPIO 的控制代码可改为:
// 开启 GPIOB 端口时钟RCC->APB2ENR |= (1<<3);//清空控制 PB0,4,5 的端口位GPIOB->CRL &= ~( 0x0F<< (4*0));GPIOB->CRL &= ~( 0x0F<< (4*4));GPIOB->CRL &= ~( 0x0F<< (4*5));// 配置 PB0,4,5 为通用推挽输出,速度为 10MGPIOB->CRL |= (1<<4*0);GPIOB->CRL |= (1<<4*4);GPIOB->CRL |= (1<<4*5); /* 控制 GPIOB 的引脚 0 输出高电平 */GPIO_SetBits(GPIOB,GPIO_Pin_0);/* 控制 GPIOB 的引脚 0 输出低电平 */GPIO_ResetBits(GPIOB,GPIO_Pin_0); /* 控制 GPIOB 的引脚 0、引脚 4 输出高电平,使用“|”同时控制多个引脚 */GPIO_SetBits(GPIOB,GPIO_Pin_0|GPIO_Pin_1);/* 控制 GPIOB 的引脚 0、引脚 4 输出低电平 */GPIO_ResetBits(GPIOB,GPIO_Pin_0|GPIO_Pin_1);
使用以上代码控制 GPIO,我们就不需要再看寄存器了,直接从函数名和输入参数就可以直观看出这个语句要实现什么操作。(英文中“Set”表示“置位”,即高电平,“Reset”表示“复位”,即低电平)
定义初始化结构体 GPIO_InitTypeDef(写在stm32f10x.h中)
定义位操作函数后,控制 GPIO 输出电平的代码得到了简化,但在控制 GPIO 输出电平前还需要初始化 GPIO 引脚的各种模式,这部分代码涉及的寄存器有很多,我们希望初始化 GPIO 也能以如此简单的方法去实现。为此,我们先根据 GPIO 初始化时涉及到的初始化参数以结构体的形式封装起来,声明一个名为 GPIO_InitTypeDef 的结构体类型,如下
typedef struct
{
uint16_t GPIO_Pin;/*!< 选择要配置的 GPIO 引脚 */uint16_t GPIO_Speed;/*!< 选择 GPIO 引脚的速率 */uint16_t GPIO_Mode;/*!< 选择 GPIO 引脚的工作模式 */
} GPIO_InitTypeDef;
这个结构体中包含了初始化 GPIO 所需要的信息,包括引脚号、工作模式、输出速率。设计这个结构体的思路是:初始化 GPIO 前,先定义一个这样的结构体变量,根据需要配置 GPIO 的模式,对这个结构体的各个成员进行赋值,然后把这个变量作为“GPIO 初始化函数”的输入参数,该函数能根据这个变量值中的内容去配置寄存器,从而实现 GPIO 的初始化。
定义引脚模式的枚举类型(写在stm32f10x.h中)
上面定义的结构体很直接,美中不足的是在对结构体中各个成员赋值实现某个功能时还需要查询手册的寄存器说明,我们不希望每次用到的时候都要去查询手册,我们可以使用 C 语言中的枚举定义功能,根据手册把每个成员的所有取值都定义好,如下。GPIO_Speed 和GPIO_Mode 这两个成员对应的寄存器是 CRL 和 CRH 这两个端口配置寄存器,具体见端口配置低寄存器和端口配置高寄存器。
/**
* GPIO 输出速率枚举定义
*/
typedef enum
{GPIO_Speed_10MHz = 1,// 10MHZ(01)bGPIO_Speed_2MHz,// 2MHZ(10)bGPIO_Speed_50MHz// 50MHZ(11)b
} GPIOSpeed_TypeDef;/**
* GPIO 工作模式枚举定义
*/
typedef enum
{GPIO_Mode_AIN = 0x0,// 模拟输入(0000 0000)bGPIO_Mode_IN_FLOATING = 0x04,// 浮空输入(0000 0100)bGPIO_Mode_IPD = 0x28,// 下拉输入(0010 1000)bGPIO_Mode_IPU = 0x48,// 上拉输入(0100 1000)bGPIO_Mode_Out_OD = 0x14,// 开漏输出(0001 0100)bGPIO_Mode_Out_PP = 0x10,// 推挽输出(0001 0000)bGPIO_Mode_AF_OD = 0x1C,// 复用开漏输出(0001 1100)bGPIO_Mode_AF_PP = 0x18// 复用推挽输出(0001 1000)b
} GPIOMode_TypeDef;
关于这两个枚举类型的值如何跟端口控制寄存器里面的说明对应起来,我们简单分析下。有关速度的枚举类型有 (01)b 10MHZ、(10)b 2MHZ 和 (11)b 50MHZ,这三个值跟寄存器说明对得上,很容易理解。至于模式的枚举类型的值理解起来就比较绕,这让很多人费了脑筋,下面我们通过一个表格来梳理下,好帮助我们理解,具体如下。
如果但从这些枚举值的十六进制来看,很难发现规律,转化成二进制之后,就比较容易发现规律。bit4 用来区分端口是输入还是输出,0 表示输入,1 表示输出,bit2 和 bit3 对应寄存器的 CNFY[1:0]位,是我们真正要写入到 CRL 和 CRH 这两个端口控制寄存器中的值。bit0 和 bit1 对应寄存器的MODEY[1:0] 位,这里我们暂不初始化,在 GPIO_Init() 初始化函数中用来跟 GPIOSpeed 的值相加即可实现速率的配置。有关具体的代码分析见 GPIO_Init() 库函数。其中在下拉输入和上拉输入中我们设置 bit5 和 bit6 的值为 01 和 10 来以示区别。
有了这些枚举定义,我们的 GPIO_InitTypeDef 结构体就可以使用枚举类型来限定输入参数,如下。
(修改stm32f10x.h中的注释)
/**
* 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;
验证(写在main.c中)(写最上面不然可能报错,因为是C89标准不是C99标准)
如果不使用枚举类型,仍使用“uint16_t”类型来定义结构体成员,那么成员值的范围就是 0-255,而实际上这些成员都只能输入几个数值。所以使用枚举类型可以对结构体成员起到限定输入的作用,只能输入相应已定义的枚举值。利用这些枚举定义,给 GPIO_InitTypeDef 结构体类型赋值配置就变得非常直观,范例如下。
GPIO_InitTypeDef GPIO_InitStructure;/* GPIO 端口初始化 */
/* 选择要控制的 GPIO 引脚 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
/* 设置引脚模式为输出模式 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/* 设置引脚的输出类型为推挽输出 */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
定义 GPIO 初始化函数(放在stm32f10x_gpio.c中)
接着前面的思路,对初始化结构体赋值后,把它输入到 GPIO 初始化函数,由它来实现寄存器配置 。我们的 GPIO 初始化函数实现如下:
/**
* 函数功能:初始化引脚模式
* 参数说明:GPIOx,该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址
*GPIO_InitTypeDef:GPIO_InitTypeDef 结构体指针,指向初始化变量
*/
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
uint32_t currentmode =0x00,currentpin = 0x00,pinpos = 0x00,pos = 0x00;
uint32_t tmpreg = 0x00, pinmask = 0x00;/*---------------- GPIO 模式配置 -------------------*/// 把输入参数 GPIO_Mode 的低四位暂存在 currentmodecurrentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);// bit4 是 1 表示输出,bit4 是 0 则是输入// 判断 bit4 是 1 还是 0,即首选判断是输入还是输出模式if((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00){// 输出模式则要设置输出速度currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;}/*-----GPIO CRL 寄存器配置 CRL 寄存器控制着低 8 位 IO- ----*/// 配置端口低 8 位,即 Pin0~Pin7if (((uint32_t)GPIO_InitStruct->GPIO_Pin &((uint32_t)0x00FF)) != 0x00){// 先备份 CRL 寄存器的值tmpreg = GPIOx->CRL;// 循环,从 Pin0 开始配对,找出具体的 Pinfor (pinpos = 0x00; pinpos < 0x08; pinpos++){// pos 的值为 1 左移 pinpos 位pos = ((uint32_t)0x01) << pinpos;// 令 pos 与输入参数 GPIO_PIN 作位与运算currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;//若 currentpin=pos, 则找到使用的引脚if (currentpin == pos){//pinpos 的值左移两位 (乘以 4), 因为寄存器中 4 个位配置一个引脚pos = pinpos << 2;//把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变pinmask = ((uint32_t)0x0F) << pos;tmpreg &= ~pinmask;// 向寄存器写入将要配置的引脚的模式tmpreg |= (currentmode << pos);// 判断是否为下拉输入模式if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD){// 下拉输入模式, 引脚默认置 0, 对 BRR 寄存器写 1 对引脚置 0GPIOx->BRR = (((uint32_t)0x01) << pinpos);}else{// 判断是否为上拉输入模式if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU) {// 上拉输入模式, 引脚默认值为 1, 对 BSRR 寄存器写 1 对引脚置 1GPIOx->BSRR = (((uint32_t)0x01) << pinpos);}}}}// 把前面处理后的暂存值写入到 CRL 寄存器之中GPIOx->CRL = tmpreg;}/*--------GPIO CRH 寄存器配置 CRH 寄存器控制着高 8 位 IO- -----*/// 配置端口高 8 位,即 Pin8~Pin15if (GPIO_InitStruct->GPIO_Pin > 0x00FF){// // 先备份 CRH 寄存器的值tmpreg = GPIOx->CRH;// 循环,从 Pin8 开始配对,找出具体的 Pinfor (pinpos = 0x00; pinpos < 0x08; pinpos++){pos = (((uint32_t)0x01) << (pinpos + 0x08));// pos 与输入参数 GPIO_PIN 作位与运算currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);//若 currentpin=pos, 则找到使用的引脚if (currentpin == pos){//pinpos 的值左移两位 (乘以 4), 因为寄存器中 4 个位配置一个引脚pos = pinpos << 2; //把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变pinmask = ((uint32_t)0x0F) << pos;tmpreg &= ~pinmask;// 向寄存器写入将要配置的引脚的模式tmpreg |= (currentmode << pos);// 判断是否为下拉输入模式if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD){// 下拉输入模式, 引脚默认置 0, 对 BRR 寄存器写 1 可对引脚置 0GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));}// 判断是否为上拉输入模式if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU){// 上拉输入模式, 引脚默认值为 1, 对 BSRR 寄存器写 1 可对引脚置GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));}}}// 把前面处理后的暂存值写入到 CRH 寄存器之中GPIOx->CRH = tmpreg;}
}
这个函数有 GPIOx 和 GPIO_InitStruct 两个输入参数,分别是 GPIO 外设指针和 GPIO 初始化结构体指针。分别用来指定要初始化的 GPIO 端口及引脚的工作模式。
要充分理解这个 GPIO 初始化函数,得配合我们刚刚分析的 GPIO 引脚工作模式真值表来看。
- 先取得 GPIO_Mode 的值,判断 bit4 是 1 还是 0 来判断是输出还是输入。如果是输出则设置输出速率,即加上 GPIO_Speed 的值,输入没有速率之说,不用设置。
- 配置 CRL 寄存器。通过 GPIO_Pin 的值计算出具体需要初始化哪个引脚,算出后,然后把需要配置的值写入到 CRL 寄存器中,具体分析见代码注释。这里有一个比较有趣的是上/下拉输入并不是直接通过配置某一个寄存器来实现的,而是通过写 BSRR 或者 BRR 寄存器来实现。这让很多只看手册没看固件库底层源码的人摸不着头脑,因为手册的寄存器说明中没有明确的指出如何配置上拉/下拉,具体见上拉/下拉寄存器说明。
- 配置 CRH 寄存器过程同 CRL。
全新面貌,使用函数点亮 LED 灯(main.c里面)
完成以上的准备后,我们就可以用自己定义的函数来点亮 LED 灯,如下
GPIO_InitTypeDef GPIO_InitStructure;/* GPIO 端口初始化 *//* 选择要控制的 GPIO 引脚 */GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;/* 设置引脚模式为输出模式 */GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;/* 设置引脚的输出类型为推挽输出 */GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;// 开启 GPIO 端口时钟RCC_APB2ENR |= (1<<3);// 调用库函数,初始化 GPIO 引脚0GPIO_Init(GPIOB, &GPIO_InitStructure);// 使引脚输出低电平, 点亮 LED1GPIO_ResetBits(GPIOB,GPIO_Pin_0);while (1){// 使引脚输出低电平, 点亮 LEDGPIO_ResetBits(GPIOB,GPIO_Pin_0);/* 延时一段时间 */Delay(1);/* 使引脚输出高电平,关闭 LED1*/GPIO_SetBits(GPIOB,GPIO_Pin_0);/* 延时一段时间 */Delay(1);}
总结
什么是 ST 标准固件库?不懂的时候总觉得莫测高深,懂了之后一切都是纸老虎。我们从寄存器映射开始,把内存跟寄存器建立起一一对应的关系,然后操作寄存器点亮 LED,再把寄存器操作封装成一个个函数。一步一步走来,我们实现了库最简单的雏形,如果我们不断地增加操作外设的函数,并且把所有的外设都写完,一个完整的库就实现了。
本章中的 GPIO 相关库函数及结构体定义,实际上都是从 ST 标准库搬过来的。这样分析它纯粹是为了满足自己的求知欲,学习其编程的方式、思想,这对提高我们的编程水平是很有好处的,顺便感受一下 ST 库设计的严谨性,我认为这样的代码不仅严谨且华丽优美,不知您是否也有这
样的感受。
与直接配置寄存器相比,从执行效率上看会有额外的消耗:初始化变量赋值的过程、库函数在被调用的时候要耗费调用时间;在函数内部,对输入参数转换所需要的额外运算也消耗一些时间(如 GPIO 中运算求出引脚号时)。而其它的宏、枚举等解释操作是作编译过程完成的,这部分并不消耗内核的时间。那么函数库的优点呢?是我们可以快速上手 STM32 控制器;配置外设状态时,不需要再纠结要向寄存器写入什么数值;交流方便,查错简单。这就是我们选择库的原因。现在的处理器的主频是越来越高,我们不需要担心 CPU 耗费那么多时间来干活会不会被累倒,库主要应用是在初始化过程,而初始化过程一般是芯片刚上电或在核心运算之前的执行的,这段时间的等待是 0.02us 还是 0.01us 在很多时候并没有什么区别。相对来说,我们还是担心一下如果都用寄存器操作,每行代码都要查数据手册的寄存器说明,自己会不会被累倒吧。在以后开发的工程中,一般不会去分析 ST 的库函数的实现。因为外设的库函数是很类似的,库外设都包含初始化结构体,以及特定的宏或枚举标识符,这些封装被库函数这些转化成相应的值,写入到寄存器之中,函数内部的具体实现是十分枯燥和机械的工作。如果您有兴趣,在您掌握了如何使用外设的库函数之后,可以查看一下它的源码实现。通常我们只需要通过了解每种外设的“初始化结构体”就能够通过它去了解 STM32 的外设功能及控制。
如何在Keil5里面新建文件
1、在相关文件夹下新建需要的.c和.h文件
2、双击想加入的文件夹
加入.c文件,.h文件在头文件编译后自己就出来了。
全部代码
main.c中的
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
/*
*因为没学32的库函数版本,配置72MHZ,因此这里不配置PLL采用8MHZ
*/
#if 0
/*** ****************************************************************************** @file main.c* @brief 选择RGB灯亮那个颜色,* @param x=0时为绿色* x=4时为蓝色* x=5时为红色* @retval None* @author (六千里)* @date 2024-03-06* @copyright 无* ******************************************************************************/
void RGB_Color(int x){switch(x){case 0: {GPIOB_CRL &= ~(0x0F<<(4*0));GPIOB_CRL |= (1 << (4*0) );GPIOB_ODR |=0XFFFF;GPIOB_ODR &= ~(1<<0);break;}case 4: {GPIOB_CRL &= ~(0x0F<<(4*1));GPIOB_CRL |= (1 << (4*1) );GPIOB_ODR |=0XFFFF;GPIOB_ODR &= ~(1<<1);break;} case 5: {GPIOB_CRL &= ~(0x0F<<(4*5));GPIOB_CRL |= (1 << (4*5) );GPIOB_ODR |=0XFFFF;GPIOB_ODR &= ~(1<<5);break;} default:;}
}
#endif
/*** ****************************************************************************** @file main.c* @brief 延时函数(不准)* @param time:延时多少秒* @retval None* @author (六千里)* @date 2024-03-06* @copyright 无* ******************************************************************************/void Delay(int time){int temp_time=0x1FFFF;while(time--){temp_time=0x1FFFFF;while(temp_time--);}
}int main (void)
{ #if 0RCC_APB2ENR |= (1<<3);while(1){RGB_Color(0);Delay(1);RGB_Color(5);Delay(1);RGB_Color(4);Delay(1);}#elif 0// 开启 GPIOB 端口时钟RCC->APB2ENR |= (1<<3);//清空控制 PB0 的端口位GPIOB->CRL &= ~( 0x0F<< (4*0));// 配置 PB0 为通用推挽输出,速度为 10MGPIOB->CRL |= (1<<4*0);// PB0 输出 低电平GPIOB->ODR |= (0<<0);while (1);#elif 0// 开启 GPIOB 端口时钟RCC->APB2ENR |= (1<<3);//清空控制 PB0,4,5 的端口位GPIOB->CRL &= ~( 0x0F<< (4*0));GPIOB->CRL &= ~( 0x0F<< (4*4));GPIOB->CRL &= ~( 0x0F<< (4*5));// 配置 PB0,4,5 为通用推挽输出,速度为 10MGPIOB->CRL |= (1<<4*0);GPIOB->CRL |= (1<<4*4);GPIOB->CRL |= (1<<4*5); /* 控制 GPIOB 的引脚 0 输出高电平 */GPIO_SetBits(GPIOB,(uint16_t)(1<<0));/* 控制 GPIOB 的引脚 0 输出低电平 */GPIO_ResetBits(GPIOB,(uint16_t)(1<<0)); /* 控制 GPIOB 的引脚 0、引脚 4 输出高电平,使用“|”同时控制多个引脚 */GPIO_SetBits(GPIOB,(uint16_t)(1<<0)|(uint16_t)(1<<4));/* 控制 GPIOB 的引脚 0、引脚 4 输出低电平 */GPIO_ResetBits(GPIOB,(uint16_t)(1<<0)|(uint16_t)(1<<4));#elif 0 // 开启 GPIOB 端口时钟RCC->APB2ENR |= (1<<3);//清空控制 PB0,4,5 的端口位GPIOB->CRL &= ~( 0x0F<< (4*0));GPIOB->CRL &= ~( 0x0F<< (4*4));GPIOB->CRL &= ~( 0x0F<< (4*5));// 配置 PB0,4,5 为通用推挽输出,速度为 10MGPIOB->CRL |= (1<<4*0);GPIOB->CRL |= (1<<4*4);GPIOB->CRL |= (1<<4*5); /* 控制 GPIOB 的引脚 0 输出高电平 */GPIO_SetBits(GPIOB,GPIO_Pin_0);/* 控制 GPIOB 的引脚 0 输出低电平 */GPIO_ResetBits(GPIOB,GPIO_Pin_0); /* 控制 GPIOB 的引脚 0、引脚 4 输出高电平,使用“|”同时控制多个引脚 */GPIO_SetBits(GPIOB,GPIO_Pin_0|GPIO_Pin_1);/* 控制 GPIOB 的引脚 0、引脚 4 输出低电平 */GPIO_ResetBits(GPIOB,GPIO_Pin_0|GPIO_Pin_1);#elseGPIO_InitTypeDef GPIO_InitStructure;/* GPIO 端口初始化 *//* 选择要控制的 GPIO 引脚 */GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;/* 设置引脚模式为输出模式 */GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;/* 设置引脚的输出类型为推挽输出 */GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;// 开启 GPIO 端口时钟RCC_APB2ENR |= (1<<3);// 调用库函数,初始化 GPIO 引脚0GPIO_Init(GPIOB, &GPIO_InitStructure);// 使引脚输出低电平, 点亮 LED1GPIO_ResetBits(GPIOB,GPIO_Pin_0);while (1){// 使引脚输出低电平, 点亮 LEDGPIO_ResetBits(GPIOB,GPIO_Pin_0);/* 延时一段时间 */Delay(1);/* 使引脚输出高电平,关闭 LED1*/GPIO_SetBits(GPIOB,GPIO_Pin_0);/* 延时一段时间 */Delay(1);}#endif
}void SystemInit(void)
{// 函数体为空,目的是为了骗过编译器不报错
}
stm32f10x.h中
#ifndef __STM32F10X_H
#define __STM32F10X_H#if 0
/* 片上外设基地址*/
#define PERIPH_BASE ((unsigned int)0x40000000)/* 总线基地址,GPIO 都挂载到 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)#else //寄存器的值常常是芯片外设自动更改的,即使 CPU 没有执行程序,也有可能发生变化
//编译器有可能会对没有执行程序的变量进行优化//volatile 表示易变的变量,防止编译器优化,
#define __IO volatile/* 片上外设基地址*/
#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)/* 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)/*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)/*!< 选择全部引脚 */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;
// RCC 寄存器结构体定义
typedef struct
{uint32_t CR;uint32_t CFGR;uint32_t CIR;uint32_t APB2RSTR;uint32_t APB1RSTR;uint32_t AHBENR;uint32_t APB2ENR;uint32_t APB1ENR;uint32_t BDCR;uint32_t CSR;
}RCC_TypeDef;/**
* GPIO 输出速率枚举定义
*/
typedef enum
{GPIO_Speed_10MHz = 1,// 10MHZ(01)bGPIO_Speed_2MHz,// 2MHZ(10)bGPIO_Speed_50MHz// 50MHZ(11)b
} GPIOSpeed_TypeDef;/**
* GPIO 工作模式枚举定义
*/
typedef enum
{GPIO_Mode_AIN = 0x0,// 模拟输入(0000 0000)bGPIO_Mode_IN_FLOATING = 0x04,// 浮空输入(0000 0100)bGPIO_Mode_IPD = 0x28,// 下拉输入(0010 1000)bGPIO_Mode_IPU = 0x48,// 上拉输入(0100 1000)bGPIO_Mode_Out_OD = 0x14,// 开漏输出(0001 0100)bGPIO_Mode_Out_PP = 0x10,// 推挽输出(0001 0000)bGPIO_Mode_AF_OD = 0x1C,// 复用开漏输出(0001 1100)bGPIO_Mode_AF_PP = 0x18// 复用推挽输出(0001 1000)b
} GPIOMode_TypeDef;/**
* 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;#endif #endif /*__STM32F10X_H*/
stm32f10x_gpio.c中
#include "stm32f10x_gpio.h"/**
* 函数功能:设置引脚为高电平
* 参数说明: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;
}/**
* 函数功能:初始化引脚模式
* 参数说明:GPIOx,该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址
*GPIO_InitTypeDef:GPIO_InitTypeDef 结构体指针,指向初始化变量
*/
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{
uint32_t currentmode =0x00,currentpin = 0x00,pinpos = 0x00,pos = 0x00;
uint32_t tmpreg = 0x00, pinmask = 0x00;/*---------------- GPIO 模式配置 -------------------*/// 把输入参数 GPIO_Mode 的低四位暂存在 currentmodecurrentmode = ((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x0F);// bit4 是 1 表示输出,bit4 是 0 则是输入// 判断 bit4 是 1 还是 0,即首选判断是输入还是输出模式if((((uint32_t)GPIO_InitStruct->GPIO_Mode) & ((uint32_t)0x10)) != 0x00){// 输出模式则要设置输出速度currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;}/*-----GPIO CRL 寄存器配置 CRL 寄存器控制着低 8 位 IO- ----*/// 配置端口低 8 位,即 Pin0~Pin7if (((uint32_t)GPIO_InitStruct->GPIO_Pin &((uint32_t)0x00FF)) != 0x00){// 先备份 CRL 寄存器的值tmpreg = GPIOx->CRL;// 循环,从 Pin0 开始配对,找出具体的 Pinfor (pinpos = 0x00; pinpos < 0x08; pinpos++){// pos 的值为 1 左移 pinpos 位pos = ((uint32_t)0x01) << pinpos;// 令 pos 与输入参数 GPIO_PIN 作位与运算currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;//若 currentpin=pos, 则找到使用的引脚if (currentpin == pos){//pinpos 的值左移两位 (乘以 4), 因为寄存器中 4 个位配置一个引脚pos = pinpos << 2;//把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变pinmask = ((uint32_t)0x0F) << pos;tmpreg &= ~pinmask;// 向寄存器写入将要配置的引脚的模式tmpreg |= (currentmode << pos);// 判断是否为下拉输入模式if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD){// 下拉输入模式, 引脚默认置 0, 对 BRR 寄存器写 1 对引脚置 0GPIOx->BRR = (((uint32_t)0x01) << pinpos);}else{// 判断是否为上拉输入模式if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU) {// 上拉输入模式, 引脚默认值为 1, 对 BSRR 寄存器写 1 对引脚置 1GPIOx->BSRR = (((uint32_t)0x01) << pinpos);}}}}// 把前面处理后的暂存值写入到 CRL 寄存器之中GPIOx->CRL = tmpreg;}/*--------GPIO CRH 寄存器配置 CRH 寄存器控制着高 8 位 IO- -----*/// 配置端口高 8 位,即 Pin8~Pin15if (GPIO_InitStruct->GPIO_Pin > 0x00FF){// // 先备份 CRH 寄存器的值tmpreg = GPIOx->CRH;// 循环,从 Pin8 开始配对,找出具体的 Pinfor (pinpos = 0x00; pinpos < 0x08; pinpos++){pos = (((uint32_t)0x01) << (pinpos + 0x08));// pos 与输入参数 GPIO_PIN 作位与运算currentpin = ((GPIO_InitStruct->GPIO_Pin) & pos);//若 currentpin=pos, 则找到使用的引脚if (currentpin == pos){//pinpos 的值左移两位 (乘以 4), 因为寄存器中 4 个位配置一个引脚pos = pinpos << 2; //把控制这个引脚的 4 个寄存器位清零,其它寄存器位不变pinmask = ((uint32_t)0x0F) << pos;tmpreg &= ~pinmask;// 向寄存器写入将要配置的引脚的模式tmpreg |= (currentmode << pos);// 判断是否为下拉输入模式if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD){// 下拉输入模式, 引脚默认置 0, 对 BRR 寄存器写 1 可对引脚置 0GPIOx->BRR = (((uint32_t)0x01) << (pinpos + 0x08));}// 判断是否为上拉输入模式if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU){// 上拉输入模式, 引脚默认值为 1, 对 BSRR 寄存器写 1 可对引脚置GPIOx->BSRR = (((uint32_t)0x01) << (pinpos + 0x08));}}}// 把前面处理后的暂存值写入到 CRH 寄存器之中GPIOx->CRH = tmpreg;}
}
stm32f10x_gpio.h中
#ifndef __STM32F10X_GPIO_H
#define __STM32F10X_GPIO_H#include "stm32f10x.h"void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);#endif /*__STM32F10X_GPIO_H*/
参考:https://doc.embedfire.com/products/link/zh/latest/index.html
相关文章:

STM32基础--自己构建库函数
什么是 STM32 函数库 固件库是指“STM32 标准函数库”,它是由 ST 公司针对 STM32 提供的函数接口,即API (Application Program Interface),开发者可调用这些函数接口来配置 STM32 的寄存器,使开发人员得以脱离最底层的寄存器操作…...

网站被插入虚假恶意链接怎么办?
在当前的电信和网络环境中,诈骗案件频发,许多受害者不幸上当,主要原因是他们点击了诈骗者发送的假链接。这些诈骗网站经常模仿真实网站的外观,使人难以分辨真伪。那么,我们应如何鉴别这些诈骗链接呢? 下面…...

ThreeJs限制模型拖动的范围
之前有讲过ThreeJs中对模型的拖动功能,使用DragControl组件,将模型放到组件的集合中,就可以拖动点击的模型了,这节细化下怎么控制拖动,比如之拖动z轴,或者限制拖动x轴的范围在某个区间: 首先还是…...

关于JVM的小总结(待补充)
JVM组成及他们之间的关系 装载类子系统字节码执行引擎运行时数据区 装载类子系统 类加载器字节码调节器类加载运行时数据区 字节码执行引擎 运行时数据区 线程私有 虚拟机栈本地方法栈程序计数器 线程共享 堆方法区(元空间)...

day37 贪心算法part6
738. 单调递增的数字 中等 提示 当且仅当每个相邻位数上的数字 x 和 y 满足 x < y 时,我们称这个整数是单调递增的。 给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。 不知道怎么讲思路……以9287举例,…...

38女神节:剧情热梗小游戏新品!预售1折秒杀,手慢无
抖音热剧情热梗小游戏《逆袭大冒险》登录 Cocos Store 预售开启!游戏包含 20剧情 40 关卡,先来看下视频吧! 游戏内嵌多种小游戏玩法,是不是很有亲切感呢?抽针、流体、重力 3.8女神节特价预售 欢迎加入迷萌游戏《逆袭大…...

岩土工程监测仪器振弦采集仪的发展历程与国内外研究现状
岩土工程监测仪器振弦采集仪的发展历程与国内外研究现状 岩土工程监测仪器河北稳控科技振弦采集仪是用于测量土体或岩石地层的力学性质、地层结构、地下水位等参数的一种仪器设备。它通过振动在地下传播的声波信号的传播速度和特性,来推断地层的物理性质。以下是对…...

Git 掌握
目录 一、前言 二、centos安装Git 三、Git基本操作 (1) 创建Git本地仓库 (2) 配置Git (3) 认识工作区,暂存区,版本库 四、添加文件 五、查看.git文件 六、修改文件 七、版本回退 八、撤销修改 (1) 场景一 对于还没有add的代码 (2) 场景二 已…...

面试题之——事务失效的八大情况
事务失效的八大情况 一、非public修饰的方法 Transactional注解只能在在public修饰的方法下使用。 /*** 私有方法上的注解,不生效(因私有方法Spring扫描不到该方法,所以无法生成代理)*/ Transactional private boolean test() …...

一些硬件知识(六)
防反接设计: 同步电路和异步电路的区别: 同步电路:存储电路中所有触发器的时钟输入端都接同一个时钟脉冲源,因而所有触发器的状态的变化都与所加的时钟脉冲信号同步。 异步电路:电路没有统一的时钟,有些触发器的时钟输入端与时钟脉冲源相连…...

前端React篇之哪些方法会触发 React 重新渲染?重新渲染 render 会做些什么?
目录 哪些方法会触发 React 重新渲染?重新渲染 render 会做些什么?setState()案例需求总结 forceUpdate()案例需求总结 props改变案例需求总结 context改变案例需求总结 哪些方法会触发 React 重新渲染?重新渲染 render 会做些什么࿱…...

PHP伪协议是什么?
PHP伪协议是一种特殊的URL协议,它允许PHP直接从PHP内部生成数据或者访问PHP自身处理的数据流,而不需要外部资源。这些协议是由PHP解释器内部定义和处理的,不同于HTTP、FTP、HTTPS等标准网络协议。下面是PHP伪协议的说明: 1. file…...

npm使用
要查看当前 npm 使用的镜像源地址,你可以使用以下命令: npm get registry这个命令会输出当前 npm 配置的镜像源地址。如果你想查看所有可用的镜像源列表,可以使用 nrm 这个工具,它是一个 npm 源管理器,可以帮助你查看…...

美国国家安全局(NSA)和美国政府将Delphi/Object Pascal列为推荐政府机构和企业使用的内存安全编程语言
上周,美国政府发布了《回到构建块:通往安全和可衡量软件的道路》的报告。本报告是美国网络安全战略的一部分,重点关注多个领域,包括内存安全漏洞和质量指标。 许多在线杂志都对这份报告发表了评论,这些杂志强调了对 C…...

C++中的内部类
一、内部类的概念 如果一个类定义在另一个类的内部,那么这个类就叫做内部类。(内部类其实和一个独立的类没有区别,只是它会受到外部类访问限定符以及类域的限制,且是外部类的友元) 如果B类是A类的内部类,…...

华为“仓颉”不是中文编程:中文编程早有所属,势如破竹
“何时能见证中国自主研发的编程语言崛起?”这是我们这些对IT生态心怀关切的人常常深思的问题。 语言,作为文化的灵魂,总是与特定的环境和人群紧密相连。无论是中文还是英语,它们都不仅仅是交流的工具,更是各自文化背…...

Python的基本数据类型
上一篇博客,我们介绍了Python的基础语法(Python基础语法:从入门到精通的必备指南),相信大家看过后,对python的整个语法逻辑有了一些了解,不了解也没有关系。接下来,我们将正式开始&a…...

24考研有感
我考11408,总分339,408考了112分 408考的不甚满意,但是客观来说也没有低多少,毕竟我的学习时间太极限了,平均5天一本书,题只做了数据结构和计组的一部分选择,最后草草研究了几年的大题就上阵了…...

k8s中的PV和PVC存储介绍
目录 一.PV介绍 1.含义 2.关键配置参数 二.PVC介绍 1.含义 2.关键参数配置 三.PV和PVC的生命周期问题 1.PV的生命周期会有4个阶段 2.用户申请空间PV的周期流程 3.PV和PVC的使用/释放/回收 四.案例演示 1.NFS配置 2.新建PV 3.新建PVC 4.新建Pod测试 5.模拟删除P…...

SpringMVC--03--前端传数组给后台
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 案例1乘客个人信息方法1:表单提交,以字段数组接收方法2:表单提交,以BeanListModel接收方法3:将Json对象序…...

【C++干货基地】六大默认成员函数: This指针 | 构造函数 | 析构函数
🎬 鸽芷咕:个人主页 🔥 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想,就是为了理想的生活! 引入 哈喽各位铁汁们好啊,我是博主鸽芷咕《C干货基地》是由我的襄阳家乡零食基地有感而发,不知道各位的…...

99.qt qml-单例程序实现
在之前讲过: 58.qt quick-qml系统托盘实现https://nuoqian.blog.csdn.net/article/details/121855993 由于,该示例只是简单讲解了系统托盘实现,并没有实现单例程序,所以多次打开后就会出现多个exe出现的可能,本章出一章QML单例程序实现, 多次打开始终只显示出第一个打开…...

【软件工程】可用性测试:提升软件、网站与产品用户体验的关键环节
🍎个人博客:个人主页 🏆个人专栏:Linux ⛳️ 功不唐捐,玉汝于成 目录 正文 关注点 界面设计: 导航测试: 交互测试: 易用性测试: 多平台兼容性: 我…...

EPLAN的国产平替软件?SuperWORKS自动化版尝鲜
在电气设计领域,EPLAN作为德国老牌软件,知名度较高,使用体验也非常好!在中国市场,是否有一款国产软件与之媲美?答案当然是有的! 接下来为大家分享一款宝藏级别的国产电气设计软件——SuperWORK…...

【MySQL 系列】MySQL 架构篇
在我们开始了解 MySQL 核心功能之前,首先我们需要站在一个全局的视角,来看 SQL 是如何运作执行的。通过这种方式,我们可以在头脑中构建出一幅 MySQL 各组件之间的协同工作方式,有助于我们加深对 MySQL 服务器的理解。 文章目录 1、…...

C++初阶:类与对象(初篇)
目录 1. 类与对象1.1 引子:结构体与类1.2 什么是类(类的定义方式)1.3 类和结构体的区别1.4 类的访问限定符与封装1.4.1 访问限定符1.4.2 类的作用域与类的实例化 1.5 类对象的模型1.5.1 类内部资源的存储方式1.5.3 类大小的计算方式 1.6 this…...

Docker 创建容器并指定时区
目录 1. 通过环境变量设置时区(推荐)2. 挂载宿主机的时区文件到容器中3. 总结 要在 Docker 容器中指定时区,可以通过两种方式来实现: 1. 通过环境变量设置时区(推荐) 在 Docker 运行时,可以通…...

springboot文件上传修改临时文件路径
1、配置:spring.servlet.multipart.location/data/tmp 2、代码 Configuration public class MultipartConfig {Value("${spring.servlet.multipart.location}")private String tmpLocation;Beanpublic MultipartConfigElement multipartConfigElement()…...

testvue-新增图表功能(教师那边-后续放到管理员那边)-src/main.js ,router/index.js
1.安装--然后在src/main.js中 导入 和 使用2修改:common/sidebar.vue ,page/ echarts.vue , router/index.js , src/main.js 3sidebar.vue <template><div class"sidebar"><el-menuclass"sidebar-el-menu":default-active&quo…...

[HackMyVM]Quick 2
kali:192.168.56.104 主机发现 arp-scan -l # arp-scan -l Interface: eth0, type: EN10MB, MAC: 00:0c:29:d2:e0:49, IPv4: 192.168.56.104 Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan) 192.168.56.1 0a:00:27:00:00:05 (Un…...