嵌入式开发之旅:初探STM32世界—跑马灯实验
任何一个单片机,最简单的外设莫过于 IO 口的高低电平控制了,本章将通过一个经典的跑马灯程序,带大家开启 STM32F4 之旅,通过本次的学习,你将了解到 STM32F4 的 IO 口作为输出使用的方法。我们将通过代码控制正点原子探索者 STM32F4 开发板上的两个 LED:DS0 和 DS1 交替闪烁,实现类似跑马灯的效果。
目录
一、实现的功能
二、硬件设计
三、程序设计
3.1 创建工程模板
3.2 工程中创建对应的文件
3.3 添加文件路径
3.4 添加相应的外设固件库和所需文件
3.5 程序流程图
3.5.1 编写LED灯初始化函数/驱动代码
3.5.2 编写延时函数
3.5.3 程序主函数/代码主要逻辑
四、下载验证
一、实现的功能
实现 STM32F407开发板上LED 灯:LED0 和 LED1 每过 500ms 一次交替闪烁,实现类似跑马灯的效果。
二、硬件设计
本次用到的硬件只有 LED(DS0 和 DS1)。其电路在探索者 STM32F4 开发板上默认是已经连接好了的。DS0 接 PF9,DS1 接 PF10。所以在硬件上不需要动任何东西。其连接原理图如图所示。
相同网络标号表示它们是连接在一起的,因此 DS0 发光二极管阴极是连接在 STM32 的 PF9 管脚上,DS1 指示灯阴极连接在 PF10 管脚上。如果要使 DS0 指示灯亮,只需要控制 PF9 管脚输出低电平,如果要使 DS0 指示灯灭,只需控制 PF9 输出高电平。同理,DS1也是如此。
三、程序设计
关于库函数版本的工程模板如何创建,我在之前的博客已经详细记录,本次工程直接使用,并同时讲解实际开发的多文件编程的整个过程。
3.1 创建工程模板
因为我们采用的是库函数开发,所以直接复制创建好的库函数模板, 在此模板上进行程序开发。将复制过来的模板文件夹重新命名为“实验一:跑马灯实验”。打开此文件夹,在Project目录下新建一个文件夹,命名为:MyLed,用于存放LED的驱动程序,然后再新建一个文件夹,命名为:MyDelay,用来存放延时模块,这里我们使用单片机内部的系统节拍定时器实现延时,比起软件延时,更加精准,后面介绍系统节拍定时器,后期详细介绍,如下图所示。
3.2 工程中创建对应的文件
打开工程,在项目下创建与文件夹中同名的文件夹:MyLed和MyDelay文件夹,保存在对应的文件夹路径中,同时创建对应的源文件.c文件和头文件.h文件,这两个文件内容是我们自己需要编写的,不是库文件。 通常 xxx.c 文件用于存放编写的驱动程序,xxx.h 文件用于存放 xxx.c 内的 stm32 头文件、全局变量声明、函数声明等内容,这样做的好处是方便我们能够快速移植代码,并且工程目录也非常清晰,对后续维护带来方便。 MyLed文件夹用于存放我们编写的 led 驱动程序,假如后面要操作开发板上的蜂鸣器,同样新建一个 MyBeep 文件夹用于存放蜂鸣器的驱动程序。创建好后如下图所示:
3.3 添加文件路径
要想工程在编译阶段能够找到创建的文件,必须要加入文件路径,按照下面的步骤操作即可,如下图所示:
3.4 添加相应的外设固件库和所需文件
至此,工程所需文件已经全部创建完毕,接下来就是梳理程序逻辑,编写代码。
3.5 程序流程图
程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很好的主导作用。本实验的程序流程图如下:
3.5.1 编写LED灯初始化函数/驱动代码
GPIO 输出配置步骤:
1)使能对应 GPIO 时钟
STM32 在使用任何外设之前,我们都要先使能其时钟。本实验用到 PF9 和 PF10 两 个 IO 口,因此需要先使能 GPIOF 的时钟。
2)设置对应 GPIO 工作模式
本实验 GPIO 使用推挽输出模式,控制 LED 亮灭,通过定义GPIO结构体,设置好其中的成员变量参数,再调用库函数实现即可。
3)控制 GPIO 引脚输出高低电平
在配置好 GPIO 工作模式后,我们就可以通过相应的库函数控制 GPIO 引脚输出高低电平,从而控制 LED 的亮灭了。
注意:在配置 STM32 外设的时候,任何时候都要先使能该外设的时钟!通过查看该外设挂载在哪个总线下,从而决定开启那个哪个时钟。所有的GPIO 是挂载在 AHB1 总线上的外设,在固件库中对挂载在 AHB1 总线上的外设时钟使能是通过函数 RCC_AHB1PeriphClockCmd ()来实现的。
在 myled.h文件夹下编写如下代码:
#ifndef __MYLED_H
#define __MYLED_H
void LED_Init(void);
#endif
在头文件的开头,使用“#ifndef”关键字,判断标号“__MYLED_H”是否被定义,若没有被定义,则从“#ifndef”至“#endif”关键字之间的内容都有效,也就是说,这个头文件若被其它文件“#include”,它就会被包含到其该文件中了,且头文件中紧接着使用 “#define”关键字定义上面判断的标号“__MYLED_H”。当这个头文件被同一个文件第二次 “#include”包含的时候,由于有了第一次包含中的“#define __MYLED_H”定义,这时再判断“#ifndef __LED_H”,判断的结果就是假了,从“#ifndef”至“#endif”之间的内容都无效,从而防止了同一个头文件被包含多次,编译时就不会出现“redefine(重复定义)” 的错误了。 一般来说,我们不会直接在 C 的源文件写两个“#include”来包含同一个头文件,但可能因为头文件内部的包含导致重复,这种代码主要是避免这样的问题。如“bsp_led.h” 文件中使用了“#include ―stm32f4xx.h ”语句,按习惯,可能我们写主程序的时候会在 main 文件写“#include ―bsp_led.h‖ 及#include ―stm32f4xx.h”,这个时候“stm32f4xx.h”文件就被包含两次了,如果没有这种机制,就会出错。 至于为什么要用两个下划线来定义“__LED_H”标号,其实这只是防止它与其它普通宏定义重复了。
在 myled.c文件夹下编写如下代码:
#include "stm32f4xx.h" // Device header
#include "myled.h"
void LED_Init(void)
{
//第一步:使能GPIOF两个口的时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);//使能 GPIOF 时钟
//第二步:GPIOF9,F10 初始化设置
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;//LED0 和 LED1 对应 IO 口
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化 GPIO
//第三步:设置灯的初始状态
GPIO_SetBits(GPIOF,GPIO_Pin_9 | GPIO_Pin_10);//GPIOF9,F10 设置高电平,灯灭
}
因为我们使用固件库中的一系列函数和寄存器结构体的封装,所以,我们必须要引入固件库的头文件,其次,多文件编程,要引入相应的的头文件。这段代码的作用是使能 AHB1 总线上的 GPIOF 时钟。 在设置完时钟之后,LED_Init 调用 GPIO_Init 函数完成对 PF9 和 PF10 的初始化配置,然 后调用函数 GPIO_SetBits 控制 LED0 和 LED1 输出 1(LED 灭)。至此,两个 LED 的初始化完毕。这样就完成了对这两个 IO 口的初始化。
3.5.2 编写延时函数
在 mydelay.h文件夹下编写如下代码:
利用系统节拍定时器可以实现精准延时(后期博客详细总结),因此我们实现了三个延时任意时间的函数,后续项目使用直接引入头文件,然后直接调用相应的函数即可。
#ifndef __MYDELAY_H__
#define __MYDELAY_H__
void My_Delay_us(uint32_t num); //延时任意微秒
void My_Delay_ms(uint32_t num); //延时任意毫秒
void My_Delay_s(uint32_t num); //延时任意秒
#endif
在 mydelay.c文件夹下编写如下代码:
#include "stm32f4xx.h" // Device header
#include "mydelay.h"
//延时num微秒
void My_Delay_us(uint32_t num)
{
while(num--)
{
SysTick ->CTRL = (1 << 0);
SysTick ->CTRL &= ~(1<<2);
SysTick ->CTRL &= ~(1<<1);
SysTick ->VAL = 0x0;
SysTick ->LOAD = 21; //1秒 21000000HZ,1毫秒 21000HZ 1微秒 21 HZ
while(!(SysTick ->CTRL & (1<<16)));
SysTick ->CTRL = ~(1<<0);
}
}
//延时num毫秒,1毫秒等于1000微秒
void My_Delay_ms(uint32_t num)
{
while(num--)
{
My_Delay_us(1000);
}
}
//延时num秒,1秒等于1000毫秒
void My_Delay_s(uint32_t num)
{
while(num--)
{
My_Delay_ms(1000);
}
}
3.5.3 程序主函数/代码主要逻辑
main()函数非常简单,先调用 LED_Init()来初始化 GPIOF.9 和 GPIOF.10 为输出。最后在死循环里面实现 LED0 和 LED1 交替闪烁,间隔为 500ms。编写代码如下:
#include "stm32f4xx.h" // Device header
#include "myled.h"
#include "mydelay.h"
int main(void)
{
//1.初始化 LED 端口
LED_Init();
//2.通过直接操作库函数的方式实现 IO 控制
while(1)
{
//LED0 对应引脚 GPIOF.9 拉低,点亮 ;
GPIO_ResetBits(GPIOF,GPIO_Pin_9);
//LED1 对应引脚 GPIOF.10 拉高,熄灭 ;
GPIO_SetBits(GPIOF,GPIO_Pin_10);
My_Delay_ms(500); //延时 500ms
//LED0对应引脚GPIOF.0拉高,熄灭;
GPIO_SetBits(GPIOF,GPIO_Pin_9);
//LED1 对应引脚 GPIOF.10 拉低,点亮
GPIO_ResetBits(GPIOF,GPIO_Pin_10);
My_Delay_ms(500); //延时 500ms
}
}
四、下载验证
我们先来看看编译结果,如图所示:
可以看到 0 错误,0 警告,编译通过接下来,大家就可以下载验证了。这里我们使用 DAP 仿真器下载。下载完之后,运行结果如图所示,LED0 和 LED1 循环闪烁:
至此,我们的跑马灯实验的学习就结束了,本章介绍了 STM32F407 的 IO 口的使用及注意事项,是后面学习的基础,希望大家好好理解。
作者:未来可期,静待花开~