6-2-GPIO点亮LED灯(寄存器)

1 新建工程

参考之前的文章:http://ckun.fun/index.php/2024/11/29/stm32-new/

2 硬件链接

本次测试采用的是野火 STM32F429IGTb V2 开发板,该板LED灯连接原理图如下:

在这个LED灯电路连接中,三个LED灯的阳极(正极)连接到3.3V电源,而阴极(负极)则通过各自的电阻连接到STM32的三个GPIO引脚(PH10、PH11、PH12)。通过控制这些GPIO引脚的高低电平,可以实现对LED灯的开关控制。

  • 当GPIO引脚输出高电平(3.3V)时,由于阳极也连接到3.3V,LED灯不会亮起,因为没有电压差;
  • 当GPIO引脚输出低电平(0V),LED的阴极接地,与阳极的电压形成电压差,LED灯亮起。
  • 将GPIO引脚设置为推挽输出模式,允许引脚输出高电平和低电平;
  • 设置为下拉模式,则GPIO 输出低电平,LED灯点亮。

3 startup_stm32f429_439xx.s

参考文章:

4 stm32f4xx.h

  • 连接LED灯的GPIO引脚需要通过读写寄存器来实现控制。这意味着我们需要知道每个寄存器的具体地址,以便在程序中进行操作;
  • 在 STM32 的存储器映射中,GPIO外设和其他外设的寄存器地址是预定义的。我们需要在 stm32f4xx.h 文件中统一定义这些寄存器的地址;
  • 为了方便使用,这些寄存器地址会被强制转换为指针类型。这样,我们可以通过指针直接访问和操作寄存器;
  • RCC用于设置微控制器的时钟。在使用GPIO外设之前,我们必须开启其时钟,这样才能正确地使用GPIO功能;
  • 每个外设都有其对应的时钟,必须开启时钟才能正确使用该外设,代码如下:
/* stm32f4xx.h */

/* 片上外设基地址 */
#define PERIPH_BASE           ((unsigned int)0x40000000)

/* 总线基地址 */
#define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)

/* GPIO外设基地址 */
#define GPIOH_BASE            (AHB1PERIPH_BASE + 0x1C00)

/* GPIOH寄存器地址, 强制转换成指针 */
#define GPIOH_MODER           *(unsigned int*)(GPIOH_BASE + 0x00)
#define GPIOH_OTYPER          *(unsigned int*)(GPIOH_BASE + 0x04)
#define GPIOH_OSPEEDR         *(unsigned int*)(GPIOH_BASE + 0x08)
#define GPIOH_PUPDR           *(unsigned int*)(GPIOH_BASE + 0x0C)
#define GPIOH_IDR             *(unsigned int*)(GPIOH_BASE + 0x10)
#define GPIOH_ODR             *(unsigned int*)(GPIOH_BASE + 0x14)
#define GPIOH_BSRR            *(unsigned int*)(GPIOH_BASE + 0x18)
#define GPIOH_LCKR            *(unsigned int*)(GPIOH_BASE + 0x1C)
#define GPIOH_AFRL            *(unsigned int*)(GPIOH_BASE + 0x20)
#define GPIOH_AFRH            *(unsigned int*)(GPIOH_BASE + 0x24)

/* RCC外设基地址 */
#define RCC_BASE              (AHB1PERIPH_BASE + 0x3800)

/* RCC的AHB1时钟使能寄存器地址, 强制转换成指针 */
#define RCC_AHB1ENR          *(unsigned int*)(RCC_BASE + 0x30)

关于片上外设基地址、总线基地址、GPIO外设基地址,可查看另一篇文章:
http://ckun.fun/index.php/2024/11/29/stm32-register/

5 main.c

  • 创建 main 函数:首先定义一个空的 main 函数
int main(void)
{
}
  • 如果直接编译,会出现如下错误提示:
// 这个错误是由于启动文件中的 Reset_Handler 调用 SystemInit 函数,而该函数尚未定义。
Error: L6218E: Undefined symbol SystemInit (referred from startup_stm32f429_439xx.o)
  • 为了消除编译错误,可以在 main 文件中定义一个空的 SystemInit 函数,内容如下:
// 函数为空,目的是为了骗过编译器不报错,添加了空的 SystemInit 函数后,再次编译时将不会报错.
// 这样可以“骗过”编译器,使其认为 SystemInit 函数已被定义。
void SystemInit(void)
{
}

5.1 开启外设时钟

通过官方文档可得知,GPIOH 对应的总线是AHB1,在AHB1 下有时钟RCC,因此需要使能RCC 时钟,才能是GPIO外设生效:

由下图可知,GPIOH端口的时钟由RCC_AHB1ENR寄存器的第7位控制,设置该位为1即可启用GPIOH的时钟。

  • STM32的每个外设都对应一个时钟,为了降低功耗,芯片上电时这些时钟默认是关闭的,要使外设正常工作,必须先打开相应的时钟;
  • RCC负责管理所有外设的时钟;
  • 所有GPIO引脚都挂载在AHB1总线上,因此它们的时钟由AHB1外设时钟使能寄存器(RCC_AHB1ENR)控制;
  • GPIOH端口的时钟由RCC_AHB1ENR寄存器的第7位控制,设置该位为1即可启用GPIOH的时钟。
  • 在访问GPIO寄存器之前,确保使能GPIOH的时钟,代码如下:
/* 开启 GPIOH 时钟,使用外设时都要先开启它的时钟 */
RCC_AHB1ENR |= (1<<7);

5.2 GPIO 模式

  • GPIO引脚配置为输出模式;
  • MODER寄存器用于配置GPIO引脚的模式(输入、输出、复用、模拟)。
  • 每个GPIO引脚占用MODER寄存器的2个位;
  • 例如,PH10引脚对应的MODER位是MODER10,占用MODER寄存器的第20和21位。
  • 将MODER10的2个位设置为“01”,表示输出模式,代码如下:
/* GPIOH MODER10 清空 */
GPIOH_MODER &= ~(0x03 << (2 * 10));

/* PH10 MODER10 = 01b 输出模式 */
GPIOH_MODER |= (1 << (2 * 10));

// 解释一下这段代码的原理
/* GPIOH_MODER &= ~(0x03 << (2 * 10));
1、GPIOH_MODER:这是GPIOH端口的模式寄存器(MODER),用于配置引脚的工作模式(输入、输出、复用、模拟)。
2、0x03:这是一个十六进制数,表示二进制的 11,即2个位。
3、(2 * 10):计算出MODER10位的起始位置。每个引脚占用2个位,所以PH10对应的位是第20和21位。
4、<<:左移操作符,将 0x03 左移 (2 * 10) 位,即左移20位,结果为:0000 0000 0011 0000 0000 0000 0000 0000。
5、~(0x03 << (2 * 10)):对每个位取反,将第20和21位清零,其他位保持不变,结果为:1111 1111 11001111 1111 1111 1111 1111。
6、&=:按位与操作,由于第20位和21位为0,则GPIOH_MODER 寄存器的第20和21位一定会清零,其他位保持不变。
*/

/* GPIOH_MODER |= (1 << (2 * 10));
1、1:表示二进制的 01,即输出模式。
2、(2 * 10):计算出MODER10位的起始位置,即第20位。
3、<<:左移操作符,将 1 左移 (2 * 10) 位,即左移20位。
4、(1 << (2 * 10)):生成一个值,将第20位置1,第21位置0,表示输出模式,结果为:0000 0000 0001 0000 0000 0000 0000 0000。
5、|=:按位或操作,将 GPIOH_MODER 寄存器的第20位置1,其他位保持不变。
*/

// 注意:在这里没有必要纠结于如何写出如何复杂的公式,因为在以后的学习中基本不会这样写

5.3 输出类型

  • GPIO 输出有推挽和开漏两种类型;
  • 我们了解到开漏类型不能直接输出高电平,要输出高电平还要在芯片外部接上拉电阻,不符合我们的硬件设计,所以我们直接使用推挽模式;
  • 配置 OTYPER寄存中的 OTYPER10 寄存器位,该位设置为 0 时 PH10 引脚即为推挽模式,代码如下:
/*GPIOH OTYPER10 清空 */
GPIOH_OTYPER &= ~(1<<1*10);
/*PH10 OTYPER10 = 0b 推挽模式 */
GPIOH_OTYPER |= (0<<1*10);

// 解释一下这段代码的原理
/* GPIOH_OTYPER &= ~(1<<1*10);
1、GPIOH_OTYPER:这是GPIOH的输出类型寄存器,用于配置每个引脚的输出类型(推挽或开漏)。
2、1 << (1 * 10):这部分计算出需要清零的目标位。1 * 10 计算出PH10引脚对应的OTYPER10位,左移1位表示该引脚的位置。
3、~(1 << (1 * 10)):对每个位取反,用于清除OTYPER10位,意味着将该位设置为0。
4、&=:按位与操作,清空OTYPER10位而不影响其他位。这样,PH10引脚的输出类型被重置。
*/

/* GPIOH_OTYPER |= (0 << (1 * 10));
1、0 << (1 * 10):将0左移到OTYPER10位,表示希望将该位设置为0(推挽模式)。
2、|=:按位或操作,将OTYPER10位设置为0,确保PH10引脚处于推挽模式。
*/

5.4 输出速度

  • GPIO 引脚的输出速度是引脚支持高低电平切换的最高频率,本实验可以随便设置;
  • 此处我们配置 OSPEEDR 寄存器中的寄存器位 OSPEEDR6 即可控制 PF6 的输出速度,代码如下:
/*GPIOH OSPEEDR10 清空 */
GPIOH_OSPEEDR &= ~(0x03<<2*10);
/*PH10 OSPEEDR10 = 0b 速率 2MHz*/
GPIOH_OSPEEDR |= (0<<2*10);

5.5 上/下拉模式

  • 通过硬件链接可知,当设置为下拉模式时,LED灯点亮,这里设置为下拉模式,代码如下:
/* GPIOH PUPDR10清空 */
GPIOH_PUPDR &= ~(0x03 << (2 * 10));
/* PH10 PUPDR10 = 01b 下拉模式 */
GPIOH_PUPDR |= (1 << (2 * 10));

5.6 控制引脚输出电平

  • 在输出模式时,对 BSRR 寄存器和 ODR 寄存器写入参数即可控制引脚的电平状态;
  • 我们使用 BSRR 寄存器控制,对相应的 BR10 位设置为 1 时 PH10 即为低电平,点亮 LED 灯;
  • 对它的 BS10 位设置为 1 时 PH10 即为高电平,关闭 LED 灯,代码如下:
/* PH10 BSRR寄存器的 BR10置1,使引脚输出低电平,LED灯亮 */
GPIOH_BSRR |= (1 << (16 + 10));

/* PH10 BSRR寄存器的 BS10置1,使引脚输出高电平,LED灯灭 */
// GPIOH_BSRR |= (1 << 10);

// 注意:当同时置1 时,BS优先级高于BR,引脚输出高电平。

5.7 完整代码

#include "stm32f4xx.h"

/**
  *   主函数
  */
int main(void)
{
  /* 开启 GPIOH 时钟,使用外设时都要先开启它的时钟 */
  RCC_AHB1ENR |= (1<<7);

  /* LED 端口初始化 */

  /* GPIOH MODER10清空 */
  GPIOH_MODER  &= ~(0x03 << (2 * 10));
  /* PH10 MODER10 = 01b 输出模式 */
  GPIOH_MODER |= (1 << (2 * 10));

  /* GPIOH OTYPER10清空 */
  GPIOH_OTYPER &= ~(1 << 10);
  /* PH10 OTYPER10 = 0b 推挽模式 */
  GPIOH_OTYPER |= (0 << 10);

  /* GPIOH OSPEEDR10清空 */
  GPIOH_OSPEEDR &= ~(0x03 << (2 * 10));
  /* PH10 OSPEEDR10 = 0b 速率2MHz */
  GPIOH_OSPEEDR |= (0 << (2 * 10));

  /* GPIOH PUPDR10清空 */
  GPIOH_PUPDR &= ~(0x03 << (2 * 10));
  /* PH10 PUPDR10 = 01b 下拉模式 */
  GPIOH_PUPDR |= (1 << (2 * 10));

  /* PH10 BSRR寄存器的 BR10置1,使引脚输出低电平 */
  GPIOH_BSRR |= (1 << (16 + 10));

  /* PH10 BSRR寄存器的 BS10置1,使引脚输出高电平 */
  // GPIOH_BSRR |= (1 << 10);

  while(1);
}

// 函数为空,目的是为了骗过编译器不报错
void SystemInit(void)
{
}

发表评论