点灯——跟我一起写STM32(第二期)

文章目录

  • 3. 成为点灯工程师
    • 3.1 建立工程
    • 3.2 配置GPIO
      • 3.2.1 明确目标和流程
      • 3.2.2 配置GPIO时钟
      • 3.2.3 配置复用和操作相应的配置寄存器
      • 3.2.4 操作相应的控制寄存器
    • 3.3 LED驱动测试
    • 3.4 花式点灯
  • 4.揭秘点灯背后的故事
    • 4.1 断言机制
    • 4.2 新的数据类型?
    • 4.3 结构体指针的妙用
    • 4.4 看不见的寄存器
    • 4.5 模块化编程

3. 成为点灯工程师

3.1 建立工程

点灯——跟我一起写STM32(第二期)_第1张图片
点灯——跟我一起写STM32(第二期)_第2张图片

然后确定

点灯——跟我一起写STM32(第二期)_第3张图片

配置项目信息,这里我们直接完成。

点灯——跟我一起写STM32(第二期)_第4张图片

开启透视图 -> 是

3.2 配置GPIO

3.2.1 明确目标和流程

配置GPIO之前,我们先要知道我们的目的是干什么,哦对,点灯,那怎么才能点亮一个LED呢?
当LED处于正向工作状态时,电流从LED阳极流向阴极时,半导体晶体就发出从紫外到红外不同颜色的光线,光的强弱与电流有关。
那我们目的明确了,就是要让LED导通嘛,看一眼商家发的原理图

点灯——跟我一起写STM32(第二期)_第5张图片
当我们向对应GPIO写入低电平的时候,(VCC作为高电平,GPIO作为低电平),则可以导通LED,即可以点亮LED。
有同学可能就要问了啥是GPIO啊?
GPIO就是通用输入输出。在最基本的层面上,GPIO 是指计算机主板或附加卡上的一组引脚。这些引脚可以发送或接收电信号,但它们不是为任何特定目的而设计的。这就是为什么它们被称为“通用”IO。
那我们现在就要让STM32运行时让对应的GPIO输出为低电平,则可以点亮LED了。
现在让笔者来告诉你配置GPIO的基本流程
way

3.2.2 配置GPIO时钟

为什么要配置 GPIO 的时钟?
人有心脏,MCU 也有,时钟就是 MCU 的心脏。心脏的周期性收缩将血液泵向身体各处,MCU 也一样。心脏对于人体好比时钟对于 MCU,微控制器(MCU)的运行要靠周期性的时钟脉冲来驱动,而这个脉冲的始源往往是由外部晶体振荡器提供时钟输入,最终转换为多个外部设备的周期性运作。这种时钟“能量”的传递路径犹如大树的养分由主干流向个分支,因此称为时钟树。
在 STM32 中每个外设都有其单独的时钟,在使用某个外设之前必须打开该外设的时钟 。而咱们的 STM32 自身的时钟频率非常的高的(相比而言),对于下面控制的外设,咱们就要“迁就一下”(由该外设的时钟频率决定),所以需要进行一层又一层的分频。
STM32CubeIDE 有一个非常友好的图形化界面的配置工具
1. 选择时钟源(配置 RCC)
点灯——跟我一起写STM32(第二期)_第6张图片
HSI是高速内部时钟,RC振荡器,频率为8MHz
HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz
里面的选项:
BYPASS Clock Source(旁路时钟源)
Crystal/Ceramic Resonator(晶体/陶瓷晶振)
下面来解释一下:
所谓HSE旁路时钟源,是指无需使用外部晶体时所需的芯片内部时钟驱动组件,直接从外界导入时钟信号。犹如芯片内部的驱动组件被旁路了。
外部晶体/陶瓷谐振器(HSE晶体)模式该时钟源是由外部无源晶体与MCU内部时钟驱动电路共同配合形成,有一定的启动时间,精度较高。
笔者使用的开发板都为Crystal/Ceramic Resonator,故都选择这个选项。

2. 选择Clock Configuration,进入时钟树的图形化配置
点灯——跟我一起写STM32(第二期)_第7张图片
点灯——跟我一起写STM32(第二期)_第8张图片

因为是初学者,我们就不追求节能,我们就安装开发板的最高性能去配置分频器。

2

当我们发现出现了一些红色的框时,这就是告诉我们,我们的配置超出了当前外设的最大承载能力,需要我们重新设置分频器,来降低频率适应外设。

点灯——跟我一起写STM32(第二期)_第9张图片

直到全员蓝色。
至此,我们便配置完了时钟树。Yeah!

3.2.3 配置复用和操作相应的配置寄存器

1. 查看原理图

配置之前,你得先知道你的LED所对应的GPIO是哪个
点灯——跟我一起写STM32(第二期)_第10张图片
笔者的LED对应的是PA8,上面的图随便找的,主要是告诉大家可以通过原理图来得知自己的GPIO对应外设。

2. 配置复用

配置复用为GPIO,并同时选择GPIO方向为输出
前面分析过,点亮LED应该让PA8输出低电平,所以这个配置输出
(这里解释复用的含义:一个引脚可以干很多事情,配置复用为GPIO就是设置该引脚现在的功能为GPIO)
点灯——跟我一起写STM32(第二期)_第11张图片
3. 配置GPIO的电气属性

System Core->GPIO中点击PA8
点灯——跟我一起写STM32(第二期)_第12张图片
笔者现在来解释一下这些选项的含义:

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即可)
点灯——跟我一起写STM32(第二期)_第13张图片时钟源在没有使用OS操作系统时,我们默认用滴答定时器作为我们的时钟源(主要用于实现 HAL_Delay () 以及作为各种 timeout 的时钟基准)

5. 生成工程文件

点灯——跟我一起写STM32(第二期)_第14张图片

3

当然点锤子(编译构建)或者保存都可以开始构建工程代码。

点灯——跟我一起写STM32(第二期)_第15张图片
OHHHHHHHH!

3.2.4 操作相应的控制寄存器

1. 必须知道概念:沙盒

点灯——跟我一起写STM32(第二期)_第16张图片
代码只有写在这里面才有效,不然下一次生成就给你洗掉了。

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驱动就完成了!
可以去测试一下了。

3.3 LED驱动测试

准备好的东西有:
STM32板子 x 1:
点灯——跟我一起写STM32(第二期)_第17张图片

STLink x 1:
点灯——跟我一起写STM32(第二期)_第18张图片

接线
按照笔者买的型号的话:

STLINK STM32
SWCLK CLK
SWDIO DIO
GND GND
3V3 3V3

如果是正点原子的开发板,使用的12V电源供电,要避免重供电,则不接3V3线,仅接入另外三根即可。
点灯——跟我一起写STM32(第二期)_第19张图片
点灯——跟我一起写STM32(第二期)_第20张图片

这里笔者已经接好了

点灯——跟我一起写STM32(第二期)_第21张图片

然后插上电脑

点灯——跟我一起写STM32(第二期)_第22张图片

电源灯亮起 电脑也响了一声,就说明OK了

在这里插入图片描述

点灯——跟我一起写STM32(第二期)_第23张图片

可能遇见的情况:
一般第一次使用STLink需要STLinkUpgrade,在上方菜单栏help(帮助)栏中找到STLinkUpgrade(STLink更新)

  1. STLinkUpgrade
    点灯——跟我一起写STM32(第二期)_第24张图片
    点击Refresh device list和Open in updata mode,等待Upgrade亮起并点击(若无效,可尝试插拔STLink,点击Open in updata mode,当Upgrade亮起后并点击Upgrade)

若一切正常:

点灯——跟我一起写STM32(第二期)_第25张图片

烧录成功!

点灯——跟我一起写STM32(第二期)_第26张图片

LED成功点亮
恭喜你,你已经迈入了点灯工程师的大门了。

3.4 花式点灯

那我们再学一个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 */

于是我们就用了两句话来实现这个反转灯了,是不是非常的优雅。

4.揭秘点灯背后的故事

作为好奇宝宝,笔者非常好奇刚刚的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;
  }
}

4.1 断言机制

我们可以看见头两句都是什么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__))  

参数exprfalse,则执行后面的assert_failed()函数。__FILE__, __LINE__是标准库函数中的宏定义,表示文件名和行号。

而这个assert_failed我们在main.c中对此进行了原形定义:
点灯——跟我一起写STM32(第二期)_第27张图片
不过呢,现在里面默认什么都没有。
等以后我们学到了串口打印,我们便可以加入

#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

4.2 新的数据类型?

相信大家也看见了,有些不认识的数据类型:

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 关键字,你真的理解吗?

4.3 结构体指针的妙用

 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这个地址开始声明这个结构体,那么就会有

struct

而指定结构体一开始在哪里声明的方法就是结构体指针,让我们来看一下HAL库是怎么实现的吧

点灯——跟我一起写STM32(第二期)_第28张图片

OMG,原来是一个又一次的地址偏移。
让我们算一下GPIOA的地址为0x40010800

点灯——跟我一起写STM32(第二期)_第29张图片
就是当GPIOx被传入GPIOA的时候,也就是一个((GPIO_TypeDef *)0x40010800)
让我们查看一下参考手册,验证一下我们的计算是否正确


和我们的计算结果一致,非常好。
看来是算对了的!!可以好好休息一下,奖励一下自己了。

有了首地址(这个结构体指针),我们在这个首地址建立结构体,映射到实际的物理地址上的每个寄存器,再使用结构体指针去访问。
这样就是实现了结构体的每个成员按照寄存器地址偏移的多少来“各找各妈,各回各家”。就比如我们只要GPIOA->BSRR = 8,就能向这个BSRR寄存器里写入8这个值了,非常的方便!
点灯——跟我一起写STM32(第二期)_第30张图片

4.4 看不见的寄存器

那么BSRR寄存器是干嘛的呢
这个寄存器就是我们前面说的控制寄存器,这里的用法是BRy(高16位) 为清除端口x的位y,(Port x的pin_y,y = 0…15),清除嘛,就是写0了,就是低电平。而BSy(低16位) 为设置端口x的位y,(Port x的pin_y,y = 0…15),就是配置高电平咯。
点灯——跟我一起写STM32(第二期)_第31张图片
至此,我们揭秘了一个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(),就像交表一样。按照我们刚刚的知识,我们可以知道这个初始化函数肯定也是拿着我们配置信息,配置了很多很多配置寄存器。这里笔者不再多言,朋友们感兴趣可以查看函数内部实现和参考手册对应部分。

4.5 模块化编程

为了让我们的学习更加有成就感,也是为了方便我们的代码,我们会对代码进行模块化管理。
点灯——跟我一起写STM32(第二期)_第32张图片

我们在主目录创建HardWare/led目录

点灯——跟我一起写STM32(第二期)_第33张图片

点灯——跟我一起写STM32(第二期)_第34张图片

创建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);

}

不过,我们也要让这两个文件也被编译,就得添加头文件和源文件。

点灯——跟我一起写STM32(第二期)_第35张图片

点灯——跟我一起写STM32(第二期)_第36张图片

点灯——跟我一起写STM32(第二期)_第37张图片

点灯——跟我一起写STM32(第二期)_第38张图片

点灯——跟我一起写STM32(第二期)_第39张图片

分别为Debug和release添加源文件

点灯——跟我一起写STM32(第二期)_第40张图片

点灯——跟我一起写STM32(第二期)_第41张图片

为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 */

点灯——跟我一起写STM32(第二期)_第42张图片
当然,这个时候我们可以关闭STM32CubeIDE自动生成GPIO的代码了,因为都被我们抄到我们的led_init里面去了。

于是,我们有了我们的第一块积木——LED模块

这只是我们STM32之旅的一个小小的开始。( ̄_, ̄ )

你可能感兴趣的:(跟我一起写STM32,stm32,单片机,arm,c语言,嵌入式硬件)