然后确定
配置项目信息,这里我们直接完成。
开启透视图 -> 是
配置GPIO之前,我们先要知道我们的目的是干什么,哦对,点灯,那怎么才能点亮一个LED呢?
当LED处于正向工作状态时,电流从LED阳极流向阴极时,半导体晶体就发出从紫外到红外不同颜色的光线,光的强弱与电流有关。
那我们目的明确了,就是要让LED导通嘛,看一眼商家发的原理图
当我们向对应GPIO写入低电平的时候,(VCC作为高电平,GPIO作为低电平),则可以导通LED,即可以点亮LED。
有同学可能就要问了啥是GPIO啊?
GPIO就是通用输入输出。在最基本的层面上,GPIO 是指计算机主板或附加卡上的一组引脚。这些引脚可以发送或接收电信号,但它们不是为任何特定目的而设计的。这就是为什么它们被称为“通用”IO。
那我们现在就要让STM32运行时让对应的GPIO输出为低电平,则可以点亮LED了。
现在让笔者来告诉你配置GPIO的基本流程吧
为什么要配置 GPIO 的时钟?
人有心脏,MCU 也有,时钟就是 MCU 的心脏。心脏的周期性收缩将血液泵向身体各处,MCU 也一样。心脏对于人体好比时钟对于 MCU,微控制器(MCU)的运行要靠周期性的时钟脉冲来驱动,而这个脉冲的始源往往是由外部晶体振荡器提供时钟输入,最终转换为多个外部设备的周期性运作。这种时钟“能量”的传递路径犹如大树的养分由主干流向个分支,因此称为时钟树。
在 STM32 中每个外设都有其单独的时钟,在使用某个外设之前必须打开该外设的时钟 。而咱们的 STM32 自身的时钟频率非常的高的(相比而言),对于下面控制的外设,咱们就要“迁就一下”(由该外设的时钟频率决定),所以需要进行一层又一层的分频。
STM32CubeIDE 有一个非常友好的图形化界面的配置工具
1. 选择时钟源(配置 RCC)
HSI是高速内部时钟,RC振荡器,频率为8MHz
HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz
里面的选项:
BYPASS Clock Source(旁路时钟源)
Crystal/Ceramic Resonator(晶体/陶瓷晶振)
下面来解释一下:
所谓HSE旁路时钟源,是指无需使用外部晶体时所需的芯片内部时钟驱动组件,直接从外界导入时钟信号。犹如芯片内部的驱动组件被旁路了。
外部晶体/陶瓷谐振器(HSE晶体)模式该时钟源是由外部无源晶体与MCU内部时钟驱动电路共同配合形成,有一定的启动时间,精度较高。
笔者使用的开发板都为Crystal/Ceramic Resonator,故都选择这个选项。
2. 选择Clock Configuration,进入时钟树的图形化配置
因为是初学者,我们就不追求节能,我们就安装开发板的最高性能去配置分频器。
当我们发现出现了一些红色的框时,这就是告诉我们,我们的配置超出了当前外设的最大承载能力,需要我们重新设置分频器,来降低频率适应外设。
直到全员蓝色。
至此,我们便配置完了时钟树。Yeah!
1. 查看原理图
配置之前,你得先知道你的LED所对应的GPIO是哪个
笔者的LED对应的是PA8,上面的图随便找的,主要是告诉大家可以通过原理图来得知自己的GPIO对应外设。
2. 配置复用
配置复用为GPIO,并同时选择GPIO方向为输出
前面分析过,点亮LED应该让PA8输出低电平,所以这个配置输出
(这里解释复用的含义:一个引脚可以干很多事情,配置复用为GPIO就是设置该引脚现在的功能为GPIO)
3. 配置GPIO的电气属性
System Core->GPIO中点击PA8
笔者现在来解释一下这些选项的含义:
GPIO output level | Low / High |
通常有两种选项:高电平/ 低电平,分别代表将该Output口设为默认输出高电平/ 默认输出低电平。
GPIO mode | Output Push Pull / Output Open Drain |
推挽模式(Output Push Pull):在该结构中输入高电平时,经过反向后,上方的P-MOS导通,下方的N-MOS关闭,对外输出高电平;而在该结构中输入低电平时,经过反向后,N-MOS管导通,P-MOS关闭,对外输出低电平。当引脚高低电平切换时,两个管子轮流导通,P管负责灌电流,N管负责拉电流,使其负载能力和开关速度都比普通的方式有很大的提高。推挽输出的低电平为0伏,高电平为3.3伏。
开漏模式(Output Open Drain):在开漏输出模式时,上方的P-MOS管完全不工作。如果我们控制GPIO输出为0,低电平,则P-MOS管关闭,N-MOS管导通,使输出接地,若控制GPIO输出为1 时,则P-MOS管和N-MOS管都关闭,所以引脚既不输出高电平,也不输出低电平,为高阻态。
GPIO Pull-up/Pull-down | No Pull-up and Pull-down / Pull-up / Pull-down |
这三个选项分别对应的是 不拉(让它悬空)或者 上拉 或者 下拉
上拉电阻的目的是为了保证在无信号输入时输入端的电平为高电平。而在信号输入为低电平是输入端的电平应该也为低电平。如果没有上拉电阻,在没有外界输入的情况下输入端是悬空的,它的电平是未知的无法保证的,上拉电阻就是为了保证无信号输入时输入端的电平为高电平,同样还有下拉电阻它是为了保证无信号输入时输入端的电平为低电平。
Maximum output speed | High / Medium / Low |
引脚速度,又称输出驱动电路的响应速度。
GPIO的引脚速度跟应用相匹配,速度配置越高,噪声越大,功耗越大。可以理解为输出驱动电路的带宽:即一个驱动电路可以不失真地通过信号的最大频率,就好比是公路的设计时速,汽车速度低于设计时速时,可以平稳地运行,如果超过设计时速就会颠簸,甚至翻车。
User Label | 自己定义 |
用户定义的该GPIO的别名,STM32CubeIDE里面会以宏的形式命名在main.h
4. 配置Debug 环境
笔者将代码下载到STM32的方式选择使用STLink等Debug工具进行。
若使用ISP烧录:
即STM32复位之后,如果检测到Boot1引脚为低电平,boot0引脚为高电平,芯片就执行内部固话的ISP引导程序。
这样比较麻烦,也相对比较缓慢。
这里笔者使用的STLink调试工具,故选择Serial Wire。
(若未选择,将默认使用烧录模式,这时候Debug的方式就下载不了程序了,
解决方法:
BOOT0设置为1
BOOT1设置为0
重新使用Debug方式下载程序代码
最后再将BOOT0改为0即可)
时钟源在没有使用OS操作系统时,我们默认用滴答定时器作为我们的时钟源(主要用于实现 HAL_Delay () 以及作为各种 timeout 的时钟基准)
5. 生成工程文件
当然点锤子(编译构建)或者保存都可以开始构建工程代码。
1. 必须知道概念:沙盒
2. 因为HAL的封装,我们已经不用直接操作控制寄存器了,所以我们可以直接使用HAL的函数来操作PA8。
讲个STM32CubeIDE必学快捷键:Alt + / 代码补全
输入个开头就能帮你补全了,非常地实用
HAL_GPIO_WritePin(GPIOx, GPIO_Pin, PinState);
GPIOx: GPIOA
GPIO_Pin: GPIO_PIN_8
PinState: GPIO_PIN_RESET/ GPIO_PIN_SET
GPIO_PIN_RESET:低电平
GPIO_PIN_SET:高电平
因为笔者要控制的GPIO为PA8,即GPIOA的IO8号,配置为低电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
不过,还记得我们设置了User Label了吗,所以我们的代码也可以写成这样,
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
至此,我们的Led驱动就完成了!
可以去测试一下了。
接线
按照笔者买的型号的话:
STLINK | STM32 |
---|---|
SWCLK | CLK |
SWDIO | DIO |
GND | GND |
3V3 | 3V3 |
如果是正点原子的开发板,使用的12V电源供电,要避免重供电,则不接3V3线,仅接入另外三根即可。
这里笔者已经接好了
然后插上电脑
电源灯亮起 电脑也响了一声,就说明OK了
可能遇见的情况:
一般第一次使用STLink需要STLinkUpgrade,在上方菜单栏help(帮助)栏中找到STLinkUpgrade(STLink更新)
若一切正常:
烧录成功!
LED成功点亮
恭喜你,你已经迈入了点灯工程师的大门了。
那我们再学一个API,延时函数
HAL_Delay(Delay);
//Delay:延时的时间,单位为毫秒
于是,我们有了下面这个闪光灯,1秒亮灭
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
HAL_Delay(1000);
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);
HAL_Delay(1000);
/* USER CODE END WHILE */
不过,这代码还不够简洁,让我们再学习一个API,GPIO输出反转函数
HAL_GPIO_TogglePin(GPIOx, GPIO_Pin);
这两个参数和之前的一样,选择你需要控制的GPIO,往上面一填写就可以了
这个API的功能就是实现电平反转,之前是低电平现在就是高电平,之前是高电平现在就是低电平。
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
HAL_Delay(1000);
/* USER CODE END WHILE */
于是我们就用了两句话来实现这个反转灯了,是不是非常的优雅。
作为好奇宝宝,笔者非常好奇刚刚的API以及图形化界面为什么能实现配置我们的底层寄存器。
于是我们先右键点开了我们刚刚使用的API的内部实现。
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
/* Check the parameters */
assert_param(IS_GPIO_PIN(GPIO_Pin));
assert_param(IS_GPIO_PIN_ACTION(PinState));
if (PinState != GPIO_PIN_RESET)
{
GPIOx->BSRR = GPIO_Pin;
}
else
{
GPIOx->BSRR = (uint32_t)GPIO_Pin << 16u;
}
}
我们可以看见头两句都是什么assert_param()
的东东
我们又点开它的实现
#ifdef USE_FULL_ASSERT
#define assert_param(expr) ((expr) ? (void)0U : assert_failed((uint8_t *)__FILE__, __LINE__))
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0U)
#endif /* USE_FULL_ASSERT */
笔者来解读一下,就是
如果USE_FULL_ASSERT
没有宏定义,则执行((void)0)
,即什么都不做。
如果USE_FULL_ASSERT
宏定义了,则执行下面的代码:
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
参数expr
为false
,则执行后面的assert_failed()函数。__FILE__
, __LINE__
是标准库函数中的宏定义,表示文件名和行号。
而这个assert_failed
我们在main.c
中对此进行了原形定义:
不过呢,现在里面默认什么都没有。
等以后我们学到了串口打印,我们便可以加入
#ifdef USE_FULL_ASSERT
void assert_failed(uint8_t* file, uint32_t line)
{
printf("Wrong parameters value: file %s on line %d\r\n", file, line);
while(1);
}
#end
在进入断言后,则函数停止运行,同时输出错误信息,HAL库中的大部分函数都有断言机制!
知道了这些我们也可以在自己的函数中加入断言了(记得定义USE_FULL_ASSERT)
void odd(uint8_t k)
{
assert_param((k%2)?1:0);
//函数实现
return;
}
上面的函数是对k进行运算,断言机制则判断是k是否是奇数,如果为偶数,则会进入assert,输出报错信息,中止信息,这种机制在调试过程中应该是很有用的!
结果串口输出如下信息:(串口信息是通过printf函数重定义进行输出的,至于printf的具体实现,后面笔者也将会更新)
Wrong parameters value: file ..\User\main.c on line 211
报错信息清晰的输出了错误所在文件和行号,是不是很方便呢
你也许会好奇串口竟然输出了文件和行号,简直太神奇了(我承认刚接触时我自己确实认为太神奇了,哈哈),不要着急咱们继续往下看
我们首先来看几个编译器内置宏
ANSI C标准中有几个标准预定义宏(也是常用的):
__LINE__:在源代码中插入当前源代码行号;
__FILE__:在源文件中插入当前源文件名;
__DATE__:在源文件中插入当前的编译日期
__TIME__:在源文件中插入当前编译时间;
__STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
__cplusplus:当编写C++程序时该标识符被定义。
想了解更多关于断言机制的,可以参考:
stm32之断言详细讲解
那我们现在看看HAL用了这个断言机制判断了什么呢
IS_GPIO_PIN(GPIO_Pin)
这个从字面上理解其实很简单,意思就是:
这个()里面的东西它是一个GPIO的Pin吗?
从它的实现也能看出
#define IS_GPIO_PIN(PIN) (((((uint32_t)PIN) & GPIO_PIN_MASK ) != 0x00u) && ((((uint32_t)PIN) & ~GPIO_PIN_MASK) == 0x00u))
这是GPIO_Pin的定义
#define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */
#define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */
#define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected */
#define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */
#define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */
#define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */
#define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */
#define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */
#define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */
#define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */
#define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */
#define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */
#define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */
#define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */
#define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */
#define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */
#define GPIO_PIN_All ((uint16_t)0xFFFF) /* All pins selected */
#define GPIO_PIN_MASK 0x0000FFFFu /* PIN mask for assert test */
可以看出(((uint32_t)PIN) & GPIO_PIN_MASK ) != 0x00u
,即上面的PIN
的值,和0xffff相与,肯定不是0x00。
(((uint32_t)PIN) & ~GPIO_PIN_MASK) == 0x00u
也一样,~ GPIO_PIN_MASK
就等于0xffff0000,和PIN
相与,若是上面的PIN
的值,那也是必定成立。
于是,这个IS_GPIO_PIN(PIN)
就是判断PIN
是否为GPIO_PIN_x
其中一个。
同理,
#define IS_GPIO_PIN_ACTION(ACTION) (((ACTION) == GPIO_PIN_RESET) || ((ACTION) == GPIO_PIN_SET))
这个宏则是判断PinState
是否为GPIO_PIN_RESET
或者GPIO_PIN_SET
相信大家也看见了,有些不认识的数据类型:
uint8_t / uint16_t / uint32_t /uint64_t
这些数据类型是 C99 中定义的,具体定义在:/usr/include/stdint.h ISO C99: 7.18 Integer types
#ifndef __int8_t_defined
# define __int8_t_defined
typedef signed char int8_t;
typedef short int int16_t;
typedef int int32_t;
# if __WORDSIZE == 64
typedef long int int64_t;
# else
__extension__
typedef long long int int64_t;
# endif
#endif
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
#ifndef __uint32_t_defined
typedef unsigned int uint32_t;
# define __uint32_t_defined
#endif
#if __WORDSIZE == 64
typedef unsigned long int uint64_t;
#else
__extension__
typedef unsigned long long int uint64_t;
#endif
其实都是些老朋友了,这是换了名字罢了,
这个名字的含义其实就是他们的数据大小的位数,例如:
uint8_t
就是unsigned char
的大小:
2个字节 = 8位
还有一个常用的就是volatile
了。
这是HAL库里volatile
的一些别名
#define __I volatile const /*!< defines 'read only' permissions */
#define __O volatile /*!< defines 'write only' permissions */
#define __IO volatile /*!< defines 'read / write' permissions */
volatile
影响编译器编译的结果,volatile
指出 变量是随时可能发生变化的,与volatile
变量有关的运算,不要进行编译优化,以免出错。
例如:
volatile int i=10;
int j = i;
...
int k = i;
volatile
告诉编译器i
是随时可能发生变化的,每次使用它的时候必须从i
的地址中读取,因而编译器生成的可执行码会重新从i
的地址读取数据放在k
中。
而优化做法是,由于编译器 发现两次从i
读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在k
中。而不是重新从i
里面读。
这样以来,如果i
是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile
可以保证对特殊地址的稳定访问,不会出错。
详情可以参考:
C语言中volatile的用法及意义
volatile 关键字,你真的理解吗?
GPIOx->BSRR = GPIO_Pin;
我们看见了这个句子
仔细观察,不难发现,GPIOx是一个结构体指针,它的类型是GPIO_TypeDef
,
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;
那么为什么要定义这么一个结构体呢,我们不能直接像51那样定义一个宏等于一个寄存器的地址吗?
可以是可以,但那样就需要定义的太多了。这样的方式可以只做一个GPIO寄存器的模板,后面相似的GPIO的寄存器分布是一样,只需改变指针就行了。
结构体分配的内存空间的连续的,也就是说CRL到LCKR的这些__IO uint32_t
的结构体成员将会每个人分到32位的空间,而且还是连续的。
就比如
struct N
{
int a;
int b;
};
假设若能指定在地址为0x8080000这个地址开始声明这个结构体,那么就会有
而指定结构体一开始在哪里声明的方法就是结构体指针,让我们来看一下HAL库是怎么实现的吧
OMG,原来是一个又一次的地址偏移。
让我们算一下GPIOA
的地址为0x40010800
就是当GPIOx
被传入GPIOA
的时候,也就是一个((GPIO_TypeDef *)0x40010800)
让我们查看一下参考手册,验证一下我们的计算是否正确
和我们的计算结果一致,非常好。
看来是算对了的!!可以好好休息一下,奖励一下自己了。
有了首地址(这个结构体指针),我们在这个首地址建立结构体,映射到实际的物理地址上的每个寄存器,再使用结构体指针去访问。
这样就是实现了结构体的每个成员按照寄存器地址偏移的多少来“各找各妈,各回各家”。就比如我们只要GPIOA->BSRR = 8
,就能向这个BSRR寄存器里写入8这个值了,非常的方便!
那么BSRR寄存器是干嘛的呢
这个寄存器就是我们前面说的控制寄存器,这里的用法是BRy(高16位) 为清除端口x的位y,(Port x的pin_y,y = 0…15),清除嘛,就是写0了,就是低电平。而BSy(低16位) 为设置端口x的位y,(Port x的pin_y,y = 0…15),就是配置高电平咯。
至此,我们揭秘了一个HAL库封装起来的HAL_GPIO_WritePin
函数。
和BSRR类似的还有ODR寄存器,也能控制GPIO输出的高低电平。
可以参考:
ODR, BSRR, BRR的差别
那我们再用这些知识看看HAL库为我们生成的图像化配置的代码。
Core/Src/gpio.c
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);
/*Configure GPIO pin : PtPin */
GPIO_InitStruct.Pin = LED1_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(LED1_GPIO_Port, &GPIO_InitStruct);
}
这里仔细讲解一下
GPIO_InitTypeDef GPIO_InitStruct = {0};
声明结构体来存放配置所需要的信息,就像填表一样,最后方便统一赋值给初始化函数。
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
使能时钟。我们之前只是配置了时钟树,但具体到一些外设头上来,这个时钟是默认关闭的,我们需要为这个时钟使能。
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);
默认高电平
GPIO_InitStruct.Pin = LED1_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(LED1_GPIO_Port, &GPIO_InitStruct);
配置这个存放配置信息的结构体,并把这个结构体交给HAL_GPIO_Init()
,就像交表一样。按照我们刚刚的知识,我们可以知道这个初始化函数肯定也是拿着我们配置信息,配置了很多很多配置寄存器。这里笔者不再多言,朋友们感兴趣可以查看函数内部实现和参考手册对应部分。
为了让我们的学习更加有成就感,也是为了方便我们的代码,我们会对代码进行模块化管理。
我们在主目录创建HardWare/led目录
创建led.c
相同方法创建led.h文件
编写他们
led.h
//
// Created by Whisky on 2023/1/8.
//
#ifndef HELLOWORLD_LED_H
#define HELLOWORLD_LED_H
#include "main.h"
void led_init(void);
#define LED1_Pin GPIO_PIN_5
#define LED1_GPIO_Port GPIOE
#define __LED1_CLK_ON() do{__HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)
#define LED0_Pin GPIO_PIN_5
#define LED0_GPIO_Port GPIOB
#define __LED0_CLK_ON() do{__HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)
#define LED1(x) LED1_GPIO_Port->BSRR |= (LED1_Pin << (16 * (!x)))
#define LED0(x) LED0_GPIO_Port->BSRR |= (LED0_Pin << (16 * (!x)))
#define LED0_Tog() HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin)
#define LED1_Tog() HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin)
#endif //HELLOWORLD_LED_H
led.c
//
// Created by Whisky on 2023/1/8.
//
#include "led.h"
void led_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__LED0_CLK_ON();
__LED1_CLK_ON();
/*Configure GPIO pin Output Level */
LED0(1);
LED1(1);
/*Configure GPIO pin : PtPin */
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Pin = LED0_Pin;
HAL_GPIO_Init(LED0_GPIO_Port, &GPIO_InitStruct);
GPIO_InitStruct.Pin = LED1_Pin;
HAL_GPIO_Init(LED1_GPIO_Port, &GPIO_InitStruct);
}
不过,我们也要让这两个文件也被编译,就得添加头文件和源文件。
分别为Debug和release添加源文件
为main.c添加:
#include "led.h"
头文件引用
led_init();
初始化函数
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2023 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "led.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
led_init();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
LED0(0);
HAL_Delay(500);
LED0(1);
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
当然,这个时候我们可以关闭STM32CubeIDE自动生成GPIO的代码了,因为都被我们抄到我们的led_init
里面去了。
于是,我们有了我们的第一块积木——LED模块
这只是我们STM32之旅的一个小小的开始。( ̄_, ̄ )