【stm32_STD_lib学习】学习自己写GPIO标准库(的一部分)

一.先尝试直接用寄存器操作GPIO

1.首先定义要用到的外设的基地址(RCC和GPIO),以及两个数据类型

//两个常用数据类型
#define uint32_t	unsigned int
#define uint16_t 	unsigned short
//RCC基地址
#define RCC_BASE	0x40021000
//GPIO相关总线的基地址
#define AHB_BASE	0x40018000
#define APB2_BASE	0x40010000
//GPIO的基地址
#define GPIOA_BASE	( APB2_BASE + 0x800)
#define GPIOB_BASE	( APB2_BASE + 0xC00)
#define GPIOC_BASE	( APB2_BASE + 0x1000)
#define GPIOD_BASE	( APB2_BASE + 0x1400)
#define GPIOE_BASE	( APB2_BASE + 0x1800)
#define GPIOF_BASE	( APB2_BASE + 0x2000)
#define GPIOG_BASE	( APB2_BASE + 0x2000)

2.把外设中用到的寄存器宏定义

//宏定义后,用的时候就不用关注地址是多少了
//注意这里的(uint32_t *),形似(XXXX *)的意思就是把它后面的立即数看成地址
//对地址不能直接写,要用*地址,才能对地址指向的内容读写,这些细节写在宏里,调用时就接触不到
#define RCC_APB2ENR     *(uint32_t *)( RCC_BASE + 0x18)
#define GPIOB_CRL	*(uint32_t *)(GPIOB_BASE )
#define GPIOB_ODR	*(uint32_t *)(GPIOB_BASE + 0x0C)

3.操作寄存器ver0.1

//首先要打开GPIO所在总线时钟
RCC_APB2ENR |= (uint32_t)(1<<3);
//然后设置GPIOB的工作模式
GPIOB_CRL |= (uint32_t)(0xA<<4);
//然后设置Pin_1的IO值
GPIOB_ODR |= (uint32_t)(1<<1);

4.考虑到GPIO的寄存器不只有CRL和ODR,得把剩下的寄存器也定义出来,要看STM32F10x-中文参考手册

//宏定义GPIOB的所有寄存器
#define GPIOB_CRL	*(uint32_t *)(GPIOB_BASE )
#define GPIOB_CRH	*(uint32_t *)(GPIOB_BASE + 0x04)
#define GPIOB_IDR	*(uint32_t *)(GPIOB_BASE + 0x08)
#define GPIOB_ODR	*(uint32_t *)(GPIOB_BASE + 0x0C)
#define GPIOB_BSRR	*(uint32_t *)(GPIOB_BASE + 0x10)
#define GPIOB_BRR	*(uint32_t *)(GPIOB_BASE + 0x14)
#define GPIOB_LCKR	*(uint32_t *)(GPIOB_BASE + 0x18)

5.以上是宏定义写法,也能采用结构体方法定义GPIOB

//声明易变量
#define __IO	volatile

//声明GPIO结构体
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;

//也声明RCC_TypeDef
typedef struct{
	
	__IO	uint32_t	CR;
	__IO	uint32_t	CFGR;
	__IO	uint32_t	CIR;
	__IO	uint32_t	APB2RSTR;
	__IO	uint32_t	APB1RSTR;
	__IO	uint32_t	AHBENR;
	__IO	uint32_t	APB2ENR;
	__IO	uint32_t	APB1ENR;
	__IO	uint32_t	BDCR;
	__IO	uint32_t	CSR;

} RCC_TypeDef;

6.通过GPIO_TypeDef结构体,我们可以用较少的代码声明全部的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)

7.用结构体控制GPIO

GPIOB ->CRL |= (uint32_t)(0xA<<4);
GPIOB ->ODR |= (uint32_t)(1<<1);

二.自己封装GPIO的STD库

把GPIO的配置从寄存器中抽象出来

1.分析前文:

GPIO的每个引脚需要4个位来设置输入输出模式和速率,所以工作模式寄存器有两个32位的CRL和CRH,对应低8位引脚和高8位引脚;
引脚工作模式的地址,实际上在相邻引脚之间会有四位的偏移;

2.提出问题:

如图CRL寄存器说明,有一个问题,当我使用不同的引脚时,我需要配置不同的寄存器,可能是CRL或CRH;
以及需要在赋值模式的时候需要做一个偏移;如设置Pin_0时,CRL = 0xN ;
若Pin_1也设置为这个模式,我还得做偏移再赋值,CRL = ( 0xN<<2 )。

3.期望达到的效果:

这样有些不方便,我想让使用者在使用不同引脚时,不需要关注它是高8位还是低8位,不需要关注相邻引脚之间的模式值会有四位的偏移;
我想让让使用者只传入Pin、Mode、Speed就能配置好GPIO(即使在实际的寄存器中,Mode和Speed的关系有点扯不清);

4.代码思路:

a.寄存器抽象为模式和速度结构体

要实现期望效果的话,首先得把引脚的工作模式从寄存器中抽象出来,全拆分成见名知意的Mode和Speed;
还得定义一个方便实现算法的Pin宏,后面会提到;
最后定义一个GPIO_InitTypeDef,包含这前面的Pin、Speed、Mode。

//Pin宏定义,这里为什么把Pin定义成这样,仅仅在初始化函数的角度来说,能更快的区分出传入参数包含哪些Pin;
//没有深究,以后有关于抽象寄存器的问题可以再来参考GPIO;
//+ 发现这样的Pin定义不仅适用初始化的实现算法,对配置BSRR这样的寄存器也很方便;
#define GPIO_Pin_0		((uint16_t)0x0001)
#define GPIO_Pin_1		((uint16_t)0x0002)
#define GPIO_Pin_2		((uint16_t)0x0004)
#define GPIO_Pin_3		((uint16_t)0x0008)
#define GPIO_Pin_4		((uint16_t)0x0010)
#define GPIO_Pin_5		((uint16_t)0x0020)
#define GPIO_Pin_6		((uint16_t)0x0040)
#define GPIO_Pin_7		((uint16_t)0x0080)
#define GPIO_Pin_8		((uint16_t)0x0100)
#define GPIO_Pin_9		((uint16_t)0x0200)
#define GPIO_Pin_10		((uint16_t)0x0400)
#define GPIO_Pin_11		((uint16_t)0x0800)
#define GPIO_Pin_12		((uint16_t)0x1000)
#define GPIO_Pin_13		((uint16_t)0x2000)
#define GPIO_Pin_14		((uint16_t)0x4000)
#define GPIO_Pin_15		((uint16_t)0x8000)
#define GPIO_Pin_ALL	        ((uint16_t)0xFFFF)

//拆分Mode
typedef enum
{
	GPIO_Mode_AIN = 0x0,
	GPIO_Mode_IN_FLOATING = 0x4,
	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;

//拆分Speed
typedef enum
{
	GPIO_Speed_10MHz = 1,
	GPIO_Speed_2MHz,
	GPIO_Speed_50MHz
} GPIOSpeed_TypeDef;
//Speed和Mode定义要放在调用它们的GPIO_InitTypeDef定义前面

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

b.封装配置引脚工作模式、速度的函数

定义好抽象的结构体只是第一步,还要把实际配置工作模式、速度的过程都封装起来,只把形似GPIO_Init (GPIOx, GPIO_InitStruct) 这样的接口展现给使用者
这里只讲细节,代码整体参考标准库函数就行

//定义配置引脚工作模式的函数GPIO_Init(),和前面提到的一样,函数只接受代表GPIO选择的GPIOx,和Pin、Mode、Speed
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)

//判断Bit4,表示是输入还是输出,输出则要设置输出速度
//currentmode变量实际上是作为存储Mode来使用的,在这个程序中它的变化思路是:
//要把传入参数Mode和Speed结合为实际对寄存器操作的0xXX值,然后要把Mode进行偏移,最后赋值给CRL或CRH
if ( (((uint32_t)GPIO_InitStruct->GPIO_Mode) & (uint32_t)(0x10)) != 0x00 )
{
	currentmode |= (uint32_t)GPIO_InitStruct->GPIO_Speed;
}

//判断传入的Pin有低8位吗?有的话把传入Pin里面属于低8位引脚的都配置好模式
if ( ((GPIO_InitStruct->GPIO_Pin) & ((uint32_t)(0x00FF)))  != 0x00)
{
  //....
}

//设置属于低8位的模式
for (pinpos = 0x00; pinpos < 0x08; pinpos++)             //1
{
	pos = ((uint32_t)(0x01) << pinpos);              //2
        
	currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;  //3

	if (currentpin == pos){                          //4
                //3、4步操作的 (a & b == b),判断的意思是,b是a的子集吗?
                //注意为了函数最后能传入多引脚,如(Pin_0 | Pin_1 | Pin_x | ...),函数内就得从Pin0遍历到Pin1,
                //通过(a & b == b)的操作判断需要设置这个Pin吗?需要的话就设置这个Pin的Mode,不需要的话就跳过

		//1、2、3步操作,实现用按位与&来判断子集的功能,和前面的Pin宏定义紧密联系,那时的宏定义的形式估计就是为了这里能实现这种,用按位与&来判断子集的功能

                //程序中,0x03、0x04这样变化的数,乘4用来计算设置Mode的偏移;0x01、0x02、0x04、0x08这样变化的数,可以通过&运算符判断是否是子集
                //先清零该Pin的Mode,然后保存偏移后的Mode
		pos = pinpos << 2;                        
		pinmask = ((uint32_t)0x0F) << pos;
		tmpreg &= ~pinmask;
		//保存引脚模式
		tmpreg |= (currentmode << pos);
	}
//...
}

//还需判断是否上/下拉,程序对引脚进行上拉或下拉
if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
{
//下拉输出,对寄存器清0
        GPIOx->BRR = (((uint32_t)0x01) << pinpos ); 
}else{
	if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
        {
	//上拉输出,对寄存器置1
        GPIOx->BSRR = ((uint32_t)0x01 << pinpos);
	}
}

//最后把Mode暂存值写到寄存器中
GPIOx->CRL = tmpreg;

//到这里就完成了低8位引脚的Mode配置,高8位思路是一样的,只是判断条件有点不同不再赘述

c.封装操作引脚输出的函数

GPIO_Init()函数只封装了对CRL和CRH寄存器的操作(引脚工作模式的配置),所以我们还得封装引脚实际输出0还是1;
在3.操作寄存器ver0.1中,我们用到了ODR寄存器来操作引脚输出,GPIO还有2个32位的寄存器BRR和BSRR,BSRR 端口设置清除寄存器;
低 16 位用于设置 GPIO 口对应位输出高电平,高 16 位用于设置 GPIO 口对应位输出低电平,与ODR相区别的是,写0不对原来的结果产生影响
也就是说,写就完事了,ODR |= 这样的操作包括了读和写操作,实际效率更慢。

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 ->BSRR = GPIO_Pin << 16;
}

d.使用实例(可以和寄存器版本对比)

注:我这里的GPIOB_Pin_1是接了个LED

//随便写的延时函数
void delay(uint32_t number){
	for( uint32_t i = 0; i < number; i++){
	}
}

int main(void)
{
	//使能时钟这部分还没封装
	RCC->APB2ENR |= (uint32_t)(1<<3);
	
        //初始化Pin、Mode、Speed
	GPIO_InitTypeDef GPIO_Init_LED;
	GPIO_Init_LED.GPIO_Pin = (GPIO_Pin_1);
	GPIO_Init_LED.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_Init_LED.GPIO_Speed = GPIO_Speed_2MHz;
	
        //调用初始化函数
	GPIO_Init(GPIOB, &GPIO_Init_LED);
	
        //高电平切换
	for(;;)
        {
		GPIO_ResetBits(GPIOB, GPIO_Pin_1);
		delay(1000000);
		GPIO_SetBits(GPIOB, GPIO_Pin_1);
		delay(1000000);
	}
}
posted @   咸鱼菜菜籽  阅读(220)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
点击右上角即可分享
微信分享提示