文章目录
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) { }