【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);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)