STM32操作GPIO外设(点亮LED灯)的两种方式——使用官方库函数或直接操作寄存器

STM32操作外设(点亮LED灯)的两种方式

准备工作:

  • 硬件gec6818开发板、搭载stm32f407zet6芯片
  • keil项目模板,准备好官方库函数
  • 官方提供的《STM32f407数据手册》、《STM32F4xx中文参考手册》
  • 《gec6818开发板原理图》

一、使用ST公司官方提供的库函数

首先获取LED0所使用的芯片引脚,由原理图可以查得LED灯使用的芯片引脚为PF9

PF9意为GPIO外设下F端口第9个引脚(引脚序号为0~15,共16个),根据官方给出的示例代码可以很容易地写出:

c 复制代码
/********************************************************************************
* @file    GPIO/GPIO_IOToggle/main.c 
* @author  MCD Application Team
* @version V1.4.0
* @date    2025/4/27
* @brief   使开发板上的LED0灯亮
******************************************************************************/

// ST公司提供的库函数
#include "stm32f4xx.h"

// 定义初始化对象
GPIO_InitTypeDef  GPIO_InitStructure;

int main()
{
	/* 打开外设时钟 */
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);

	/* 配置初始化对象 */
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // 设置要操作的引脚编号
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;  // 设置引脚模式为输出模式
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;  // 设置引脚类型为推挽模式
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; // 设置速度为100MHz
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;  // 设置上拉还是下拉,即引脚不给输出信号时的默认电平,上拉为高电平,下拉为低电平,nopull为浮空
    
    /* 初始化*/
	GPIO_Init(GPIOF, &GPIO_InitStructure); 
	
	while(1)
	{ 
		// 设置PF8为高电平
        // 为什么直接取GPIOF下的BSRRL就能找到对应的比特位呢?(第二节)
        GPIOF->BSRRL = GPIO_Pin_9;  // GPIOx_BSRR 为置位/复位寄存器(32位),高16位用作复位(BSRRH),低16位用作置位(BSRRL)
        
        //置位的第二种写法
        GPIO_SetBits(GPIOF, GPIO_Pin_8);
	}
}

问:ST公司提供的库函数使用起来非常方便简洁,但是底层是靠什么逻辑呢?

即答:底层靠预先封装好的寄存器地址寻址

所以,如果操作上完全不使用库函数也是可以直接做到点亮LED0的,只要能找到正确的寄存器地址并赋值。

二、直接使用寄存器地址操作寄存器

首先,地址映射规则由芯片厂商固化,我们可以通过《STM32f407数据手册》先看看STM32F407ZET6整体的编址结构:

找到第4章------Memory mapping,我们可以发现整体编址范围从0x0000_00000xFFFF_FFFF(共4G),分配给外设的部分为0x4000_00000x5FFF_FFFF,即:

  • 外设基地址为0x4000_0000

放大这部分可以看到这其中包含了四条总线:AHB2、AHB1、APB2、APB1,查询图后方的表格可知GPIOF位于AHB1总线上,且所属地址为0x4002_1400~0x4002_17FF,:

所以:

  • AHB1下的GPIOF的基地址为0x4002_1400

再往下深入的话,数据手册就派不上用场了,接着使用《STM32F4xx中文参考手册》查看具体的寄存器地址,找到7.4章节,其中可以找到每个端口下的寄存器的偏移地址:

逐个查询可知,本次需要配置的几个寄存器的偏移地址如下:

  • 端口模式:0x00
  • 端口输出类型:0x04
  • 端口输出速度:0x08
  • 端口上拉/下拉:0x0C
  • 端口置位/复位:0x18

问:现在知道寄存器的基地址和偏移地址了,只要用指针取地址下的值就可以操作寄存器了,但是现在要写什么数据进寄存器才能得到我们想要的结果呢?

端口下的每一个寄存器有32位,每2位对应一个引脚配置,例如文档所述的端口模式寄存器:

MODERy 中的y 即每个端口下的引脚序号(0~15),2个bit可以设置4种状态,因此我们想要设置9号引脚的端口模式为通用输出模式的话,只需设置端口模式寄存器的MODER9 (18和19位)为01即可。然后总结一下我们需要设置的参数:

(等号左边为位号,右边为电平值)

  • 端口模式:2*9 : 2*9+1 = 0 : 1
  • 端口输出类型:9 = 0
  • 端口输出速度:2*9 : 2*9+1 = 1 : 1
  • 端口上拉/下拉:2*9 : 2*9+1 = 0 : 0
  • 端口置位/复位:9 : 25 = 0 : 1

除了以上GPIO寄存器,接下来还有最重要的一个RCC寄存器需要设置,用来打开端口的时钟,这样才能成功配置端口。同样的查询步骤可以得到RCC AHB1外设时钟使能寄存器的地址和要设置的电平:

  • 基地址0x4002_3800,偏移地址0x30,5 = 1

所有数据查询完毕,接下来终于可以着手写代码了:

c 复制代码
/********************************************************************************
* @file    GPIO/GPIO_IOToggle/main.c 
* @author  MCD Application Team
* @version V1.4.0
* @date    2025/4/27
* @brief   用直接操作寄存器的方式使开发板上的LED0灯亮
******************************************************************************/

#define RCC_AHB1Periph_GPIOF 	(*(volatile unsigned int*)(0x40023800 + 0x30))			// RCC AHB1外设时钟使能寄存器
#define GPIOF_MODER 			(*(volatile unsigned int*)(0x40021400 + 0x00))			// 端口模式
#define GPIOF_OTYPER 			(*(volatile unsigned int*)(0x40021400 + 0x04))			// 端口输出类型
#define GPIOF_OSPEEDR 			(*(volatile unsigned int*)(0x40021400 + 0x08))			// 端口输出速度
#define GPIOF_PUPDR 			(*(volatile unsigned int*)(0x40021400 + 0x0C))			// 端口上拉/下拉
#define GPIOx_BSRR 				(*(volatile unsigned int*)(0x40021400 + 0x18))			// 端口置位/复位
	
int main()
{
	/* 打开外设时钟 */
	RCC_AHB1Periph_GPIOF |= 1 << 5;

	/* 设置引脚模式为输出模式 */
	GPIOF_MODER &= ~(1 << 9 );
	
	/* 设置引脚类型为推挽模式 */
	GPIOF_OTYPER |= 1 << 2*9 ;
	GPIOF_OTYPER |= 1 << (2*9 + 1);
	
	/* 设置输出速度为100MHz	*/
	GPIOF_OSPEEDR &= ~(1 << 2*9 );
	GPIOF_OSPEEDR |= 1 << (2*9 + 1);
	
	/* 设置上拉/下拉为浮空 */
	GPIOF_PUPDR &= ~(1 << 2*9 );
	GPIOF_PUPDR &= ~(1 << (2*9 + 1));

	while(1)
	{ 
		// 设置PF8为高电平
		GPIOx_BSRR &= ~(1 << 9 );
		GPIOx_BSRR |= 1 << 25;
	}
}

以上代码中没有包含任何库文件,即可完成对LED0灯的点亮!!

三、官方库函数底层代码

看过两种方式点亮LED0后,再回头看官方的库函数的底层代码,本质上也是通过封装寄存器的地址来点亮LED的:

c 复制代码
// stm32f4xx.h

/** 
  * @brief General Purpose I/O
  */
typedef struct
{
  __IO uint32_t MODER;    /*!< GPIO port mode register,               Address offset: 0x00      */
  __IO uint32_t OTYPER;   /*!< GPIO port output type register,        Address offset: 0x04      */
  __IO uint32_t OSPEEDR;  /*!< GPIO port output speed register,       Address offset: 0x08      */
  __IO uint32_t PUPDR;    /*!< GPIO port pull-up/pull-down register,  Address offset: 0x0C      */
  __IO uint32_t IDR;      /*!< GPIO port input data register,         Address offset: 0x10      */
  __IO uint32_t ODR;      /*!< GPIO port output data register,        Address offset: 0x14      */
  __IO uint16_t BSRRL;    /*!< GPIO port bit set/reset low register,  Address offset: 0x18      */
  __IO uint16_t BSRRH;    /*!< GPIO port bit set/reset high register, Address offset: 0x1A      */
  __IO uint32_t LCKR;     /*!< GPIO port configuration lock register, Address offset: 0x1C      */
  __IO uint32_t AFR[2];   /*!< GPIO alternate function registers,     Address offset: 0x20-0x24 */
} GPIO_TypeDef;
...
#define PERIPH_BASE           ((uint32_t)0x40000000) /* 外设基地址 */
...
#define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000) /* AHB1总线基地址 */
...
#define GPIOF_BASE            (AHB1PERIPH_BASE + 0x1400)  /* GPIOF端口基地址 */
...
#define GPIOF               ((GPIO_TypeDef *) GPIOF_BASE)  /* 端口接口体指针 */

看过以上库函数的封装以后,就可以解释一开头的那句 GPIOF->BSRRL = GPIO_Pin_9;为什么可以直接对引脚置位了。

四、总结

点亮LED灯,应该说是"麻雀虽小五脏俱全",整个过程也蕴含了完整的开发流程和知识体系。总的来说,使用官方提高的库函数确实极大地提高了开发效率,但是却非常的抽象;通过阅读手册,更加深入地理解硬件却能更好地掌控代码与硬件之间的直接联系,这个过程着实能让人学到不少东西。