第六章 跑马灯实验
[mw_shl_code=c,true]
硬件平台:正点原子探索者STM32F407开发板
软件平台:MDK5.1
固件库版本:V1.4.0
[/mw_shl_code]
任何一个单片机,最简单的外设莫过于
IO口的高低电平控制了,本章将通过一个经典的跑马灯程序,带大家开启
STM32F4之旅,通过本章的学习,你将了解到
STM32F4的
IO口作为输出使用的方法。在本章中,我们将通过代码控制
ALIENTEK探索者
STM32F4开发板上的两个
LED:
DS0和
DS1交替闪烁,实现类似跑马灯的效果。
本章分为如下四个小节:
6.1,
STM32F4 IO口简介
6.2, 硬件设计
6.3, 软件设计
6.4, 下载验证
6.1 STM32F4 IO简介
本章将要实现的是控制
ALIENTEK探索者
STM32F4开发板上的两个
LED实现一个类似跑马灯的效果,该实验的关键在于如何控制
STM32F4的
IO口输出。了解了
STM32F4的
IO口如何输出的,就可以实现跑马灯了。通过这一章的学习,你将初步掌握
STM32F4基本
IO口的使用,而这是迈向
STM32F4的第一步。
这一章节因为是第一个实验章节,所以我们在这一章将讲解一些知识为后面的实验做铺垫。为了小节标号与后面实验章节一样,这里我们不另起一节来讲。
在讲解
STM32F4的
GPIO之前,首先打开我们光盘的第一个固件库版本实验工程跑马灯实验工程(
光盘目录为:“4,程序源码标准例程-库函数版本实验1跑马灯/USER/
LED.uvproj”)
,可以看到我们的实验工程目录:
图
6.1.1 跑马灯实验目录结构
接下来我们逐一讲解一下我们的工程目录下面的组以及重要文件。
① 组
FWLib下面存放的是
ST官方提供的固件库函数,每一个源文件
stm32f4xx_ppp.c都对应一个头文件
stm32f4xx_ppp.h。分组内的文件我们可以根据工程需要添加和删除,但是一定要注意如果你引入了某个源文件,一定要在头文件
stm32f4xx_conf.h文件中确保对应的头文件也已经添加。比如我们跑马灯实验,我们只添加了
5个源文件,那么对应的头文件我们必须确保在
stm32f4xx_conf.h内也包含进来,否则工程会报错。
② 组
CORE下面存放的是固件库必须的核心文件和启动文件。这里面的文件用户不需要修改。大家可以根据自己的芯片型号选择对应的启动文件。
③ 组
SYSTEM是
ALIENTEK提供的共用代码,这些代码的作用和讲解在第五章都有讲解,大家可以翻过去看下。
④ 组
HARDWARE下面存放的是每个实验的外设驱动代码,他的实现是通过调用
FWLib下面的固件库文件实现的,比如
led.c里面调用
stm32f4xx_gpio.c内定义的函数对
led进行初始化,这里面的函数是讲解的重点。后面的实验中可以看到会引入多个源文件。
⑤ 组
USER下面存放的主要是用户代码。但是
system_stm32f4xx.c文件用户不需要修改,同时
stm32f4xx_it.c里面存放的是中断服务函数,这两个文件的作用在
3.1节有讲解,大家可以翻过去看看。
Main.c函数主要存放的是主函数了,这个大家应该很清楚。
工程分组情况我们就讲解到这里,接下来我们就要进入我们跑马灯实验的讲解部分了。这里需要说明一下,我们在讲解固件库之前会首先对重要寄存器进行一个讲解,这样是为了大家对寄存器有个初步的了解。大家学习固件库,并不需要记住每个寄存器的作用,而只是通过了解寄存器来对外设一些功能有个大致的了解,这样对以后的学习也很有帮助。
首先要提一下,在固件库中,
GPIO端口操作对应的库函数函数以及相关定义在文件
stm32f4xx_gpio.h和
stm32f4xx_gpio.c中。
相对于
STM32F1来说,
STM32F4的
GPIO设置显得更为复杂,也更加灵活,尤其是复用功能部分,比
STM32F1改进了很多,使用起来更加方便。
STM32F4每组通用
I/O 端口包括
4 个
32 位配置寄存器(
MODER、
OTYPER、
OSPEEDR 和
PUPDR)、
2 个
32 位数据寄存器(
IDR 和
ODR)、
1 个
32 位置位
/复位寄存器
(BSRR)、
1 个
32 位锁定寄存器
(LCKR) 和
2 个
32 位复用功能选择寄存器(
AFRH 和
AFRL)等。
这样,
STM32F4每组
IO有
10个
32位寄存器控制,其中常用的有
4个配置寄存器
+2个数据寄存器
+2个复用功能选择寄存器,共
8个,如果在使用的时候,每次都直接操作寄存器配置
IO,代码会比较多,也不容易记住,所以我们在讲解寄存器的同时会讲解是用库函数配置
IO的方法。
同
STM32F1一样,
STM32F4的
IO可以由软件配置成如下
8种模式中的任何一种:
1、输入浮空
2、输入上拉
3、输入下拉
4、模拟输入
5、开漏输出
6、推挽输出
7、推挽式复用功能
8、开漏式复用功能
关于这些模式的介绍及应用场景,我们这里就不详细介绍了,感兴趣的朋友,可以看看这个帖子了解下:
http://www.openedv.com/posts/list/32730.htm 。接下来我们详细介绍
IO配置常用的
8个寄存器:
MODER、
OTYPER、
OSPEEDR、
PUPDR、
ODR、
IDR 、
AFRH和
AFRL。同时讲解对应的库函数配置方法。
首先看
MODER寄存器,该寄存器是
GPIO端口模式控制寄存器,用于控制
GPIOx(
STM32F4最多有
9组
IO,分别用大写字母表示,即
x=A/B/C/D/E/F/G/H/I,下同)的工作模式,该寄存器各位描述如表
5.2.5.1所示:
表
5.2.5.1 GPIOx MODER寄存器各位描述
该寄存器各位在复位后,一般都是
0(个别不是
0,比如
JTAG占用的几个
IO口),也就是默认条件下一般是输入状态的。每组
IO下有
16个
IO口,该寄存器共
32位,每
2个位控制
1个
IO,不同设置所对应的模式见表
5.2.5.1描述。
然后看
OTYPER寄存器,该寄存器用于控制
GPIOx的输出类型,该寄存器各位描述见表
5.2.5.2所示:
表
5.2.5.2 GPIOx OTYPER寄存器各位描述
该寄存器仅用于输出模式,在输入模式(
MODER[1:0]=00/11时)下不起作用。该寄存器低
16位有效,每一个位控制一个
IO口,复位后,该寄存器值均为
0。
然后看
OSPEEDR寄存器,该寄存器用于控制
GPIOx的输出速度,该寄存器各位描述见表
5.2.5.3所示:
表
5.2.5.3 GPIOx OSPEEDR寄存器各位描述
该寄存器也仅用于输出模式,在输入模式(
MODER[1:0]=00/11时)下不起作用。该寄存器每
2个位控制一个
IO口,复位后,该寄存器值一般为
0。
然后看
PUPDR寄存器,该寄存器用于控制
GPIOx的上拉
/下拉,该寄存器各位描述见表
5.2.5.4所示:
表
5.2.5.4 GPIOx PUPDR寄存器各位描述
该寄存器每
2个位控制一个
IO口,用于设置上下拉,这里提醒大家,
STM32F1是通过
ODR寄存器控制上下拉的,而
STM32F4则由单独的寄存器
PUPDR控制上下拉,使用起来更加灵活。复位后,该寄存器值一般为
0。
前面,我们讲解了
4个重要的配置寄存器。顾名思义,配置寄存器就是用来配置
GPIO的相关模式和状态,接下来我们讲解怎么在库函数初始化
GPIO的配置。
GPIO相关的函数和定义分布在固件库文件
stm32f4xx_gpio.c和头文件
stm32f4xx_gpio.h文件中。
在固件库开发中,操作四个配置寄存器初始化
GPIO是通过
GPIO初始化函数完成:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef*
GPIO_InitStruct)
这个函数有两个参数,第一个参数是用来指定需要初始化的
GPIO对应的
GPIO组,取值范围为
GPIOA~GPIOK。第二个参数为初始化参数结构体指针,结构体类型为
GPIO_InitTypeDef。下面我们看看这个结构体的定义。首先我们打开我们光盘的跑马灯实验,然后找到
FWLib组下面的
stm32f4xx_gpio.c文件,定位到
GPIO_Init函数体处,双击入口参数类型
GPIO_InitTypeDef后右键选择“
Go to definition of …”可以查看结构体的定义:
typedef struct
{
uint32_t
GPIO_Pin;
GPIOMode_TypeDef GPIO_Mode;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOOType_TypeDef GPIO_OType;
GPIOPuPd_TypeDef GPIO_PuPd;
}GPIO_InitTypeDef;
下面我们通过一个
GPIO初始化实例来讲解这个结构体的成员变量的含义。
通过初始化结构体初始化
GPIO的常用格式是:
GPIO_InitTypeDef
GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9//GPIOF9
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化
GPIO
上面代码的意思是设置
GPIOF的第
9个端口为推挽输出模式,同时速度为
100M,上拉。
从上面初始化代码可以看出,结构体
GPIO_InitStructure的第一个成员变量
GPIO_Pin用来设置是要初始化哪个或者哪些
IO口,这个很好理解;第二个成员变量
GPIO_Mode是用来设置对应
IO端口的输出输入端口模式,这个值实际就是配置我们前面讲解的
GPIOx的
MODER寄存器的值。在
MDK中是通过一个枚举类型定义的,我们只需要选择对应的值即可:
typedef enum
{
GPIO_Mode_IN = 0x00, /*!<
GPIO Input Mode */
GPIO_Mode_OUT = 0x01, /*!<
GPIO Output Mode */
GPIO_Mode_AF = 0x02, /*!<
GPIO Alternate function Mode */
GPIO_Mode_AN = 0x03 /*!< GPIO Analog Mode */
}GPIOMode_TypeDef;
GPIO_Mode_IN是用来设置为复位状态的输入,
GPIO_Mode_OUT是通用输出模式,
GPIO_Mode_AF是复用功能模式,
GPIO_Mode_AN是模拟输入模式。
第三个参数
GPIO_Speed是
IO口输出速度设置,有四个可选值。实际上这就是配置的
GPIO对应的
OSPEEDR寄存器的值。在
MDK中同样是通过枚举类型定义:
typedef enum
{
GPIO_Low_Speed = 0x00, /*!<
Low speed */
GPIO_Medium_Speed = 0x01, /*!<
Medium speed */
GPIO_Fast_Speed = 0x02, /*!<
Fast speed */
GPIO_High_Speed = 0x03 /*!< High speed */
}GPIOSpeed_TypeDef;
/* Add legacy definition */
#define
GPIO_Speed_2MHz
GPIO_Low_Speed
#define
GPIO_Speed_25MHz
GPIO_Medium_Speed
#define
GPIO_Speed_50MHz GPIO_Fast_Speed
#define
GPIO_Speed_100MHz
GPIO_High_Speed
这里需要说明一下,实际我们的输入可以是
GPIOSpeed_TypeDef枚举类型中
GPIO_High_Speed枚举类型值,也可以是
GPIO_Speed_100MHz这样的值,实际上
GPIO_Speed_100MHz就是通过
define宏定义标识符定义出来的,它跟
GPIO_High_Speed是等同的。
第四个参数
GPIO_OType是
GPIO的输出类型设置,实际上是配置的
GPIO的
OTYPER寄存器的值。在
MDK中同样是通过枚举类型定义:
typedef enum
{
GPIO_OType_PP = 0x00,
GPIO_OType_OD = 0x01
}GPIOOType_TypeDef;
如果需要设置为输出推挽模式,那么选择值
GPIO_OType_PP,如果需要设置为输出开漏模式,那么设置值为
GPIO_OType_OD。
第五个参数
GPIO_PuPd用来设置
IO口的上下拉,实际上就是设置
GPIO的
PUPDR寄存器的值。同样通过一个枚举类型列出:
typedef enum
{
GPIO_PuPd_NOPULL = 0x00,
GPIO_PuPd_UP = 0x01,
GPIO_PuPd_DOWN = 0x02
}GPIOPuPd_TypeDef;
这三个值的意思很好理解,
GPIO_PuPd_NOPULL为不使用上下拉,
GPIO_PuPd_UP为上拉,
GPIO_PuPd_DOWN为下拉。我们根据我们 需要设置相应的值即可。
这些入口参数的取值范围怎么定位,怎么快速定位到这些入口参数取值范围的枚举类型,在我们上面章节
4.7的“快速组织代码”章节有讲解,不明白的朋友可以翻回去看一下,这里我们就不重复讲解,在后面的实验中,我们也不再去重复讲解怎么定位每个参数的取值范围的方法。
看完了
GPIO的参数配置寄存器,接下来我们看看
GPIO输入输出电平控制相关的寄存器。
首先我们看
ODR寄存器,该寄存器用于控制
GPIOx的输出,该寄存器各位描述见表
5.2.5.5所示:
表
5.2.5.5 GPIOx ODR寄存器各位描述
该寄存器用于设置某个
IO输出低电平
(ODRy=0)还是高电平
(ODRy=1),该寄存器也仅在输出模式下有效,在输入模式(
MODER[1:0]=00/11时)下不起作用。
在固件库中设置
ODR寄存器的值来控制
IO口的输出状态是通过函数
GPIO_Write来实现的:
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t
PortVal);
该函数一般用来往一次性一个
GPIO的多个端口设值。
使用实例如下:
GPIO_Write(GPIOA,0x0000);
大部分情况下,设置
IO口我们都不用这个函数,后面我们会讲解我们常用的设置
IO口电平的函数。
同时读
ODR寄存器还可以读出
IO口的输出状态,库函数为:
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef*
GPIOx, uint16_t GPIO_Pin);
这两个函数功能类似,只不过前面是用来一次读取一组
IO口所有
IO口输出状态,后面的函数用来一次读取一组
IO口中一个或者几个
IO口的输出状态。
接下来我们看看
IDR寄存器,该寄存器用于读取
GPIOx的输入,该寄存器各位描述见表
5.2.5.6所示:
表
5.2.5.6 GPIOx IDR寄存器各位描述
该寄存器用于读取某个
IO的电平,如果对应的位为
0(IDRy=0),则说明该
IO输入的是低电平,如果是
1(IDRy=1),则表示输入的是高电平。库函数相关函数为:
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx,
uint16_t GPIO_Pin);
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
前面的函数是用来读取一组
IO口的一个或者几个
IO口输入电平,后面的函数用来一次读取一组
IO口所有
IO口的输入电平。比如我们要读取
GPIOF.5的输入电平,方法为:
GPIO_ReadInputDataBit(GPIOF, GPIO_Pin_5);
接下来我们看看
32 位置位
/复位寄存器
(BSRR),顾名思义,这个寄存器是用来置位或者复位
IO口,该寄存器和
ODR寄存器具有类似的作用,都可以用来设置
GPIO端口的输出位是
1还是
0。寄存器描述如下:
表
5.2.5.7 BSRR寄存器各位描述
对于低
16位(
0-15),我们往相应的位写
1,那么对应的
IO口会输出高电平,往相应的位写
0,对
IO口没有任何影响。高
16位(
16-31)作用刚好相反,对相应的位写
1会输出低电平,写
0没有任何影响。也就是说,对于
BSRR寄存器,你写
0的话,对
IO口电平是没有任何影响的。我们要设置某个
IO口电平,只需要为相关位设置为
1即可。而
ODR寄存器,我们要设置某个
IO口电平,我们首先需要读出来
ODR寄存器的值,然后对整个
ODR寄存器重新赋值来打到设置某个或者某些
IO口的目的,而
BSRR寄存器,我们就不需要先读,而是直接设置。
BSRR寄存器使用方法如下:
GPIOA->BSRR=1<<1; //设置
GPIOA.1为高电平
GPIOA->BSRR=1<<(
16+1)
//设置
GPIOA.1为低电平
;
库函数操作
BSRR寄存器来设置
IO电平的函数为:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t
GPIO_Pin);
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t
GPIO_Pin);
函数
GPIO_SetBits用来设置一组
IO口中的一个或者多个
IO口为高电平。
GPIO_ResetBits用来设置一组
IO口中一个或者多个
IO口为低电平。比如我们要设置
GPIOB.5输出高,方法为:
GPIO_SetBits(GPIOB,GPIO_Pin_5);//GPIOB.5输出高
设置
GPIOB.5输出低电平,方法为:
GPIO_ResetBits(GPIOB,GPIO_Pin_5);//GPIOB.5输出低
最后我们来看看
2 个
32 位复用功能选择寄存器(
AFRH 和
AFRL),这两个寄存器是用来设置
IO口的复用功能的。关于这两个寄存器的配置以及相关库函数的使用,在我们前面章节
4.4 IO引脚复用和映射有详细讲解,这里我们就不做过多的说明。
GPIO相关的函数我们先讲解到这里。虽然
IO操作步骤很简单,这里我们还是做个概括性的总结,操作步骤为:
1) 使能
IO口时钟。调用函数为
RCC_AHB1PeriphClockCmd ()。
2) 初始化
IO参数。调用函数
GPIO_Init();
3) 操作
IO。操作
IO的方法就是上面我们讲解的方法。
上面我们讲解了
STM32F4 IO口的基本知识以及固件库操作
GPIO的一些函数方法,下面我们来讲解我们的跑马灯实验的硬件和软件设计。
6.2 硬件设计
本章用到的硬件只有
LED(
DS0和
DS1)。其电路在
ALIENTEK探索者
STM32F4开发板上默认是已经连接好了的。
DS0接
PF9,
DS1接
PF10。所以在硬件上不需要动任何东西。其连接原理图如图
6.2.1下:
图
6.2.1 LED与
STM32F4连接原理图
6.3 软件设计
这是我们的第一个实验,所以我教大家怎么从我们前面讲解的
Template工程一步一步加入我们的固件库以及我们的
led相关的驱动函数到我们工程,使之跟我们光盘的跑马灯实验工程一模一样。首先大家打开我们
3.3.2小节新建的库函数版本工程模板。如果您还没有新建,也可以直接打开我们光盘已经新建好了的工程模板,路径为:“
4,程序源码标准例程-库函数版本实验0
Template工程模板”。注意,是直接点击工程下面的
USER目录下面的
Template.uvproj。
大家可以看到,我们模板里面的
FWLIB分组下面,我们引入了所有的固件库源文件和对应的头文件,如下图
6.3.1:
图
6.3.1 Template模板工程结构
实际上,这些大家可以根据工程需要添加,比如我们跑马灯实验,我们并没有用到
ADC,自然我们可以去掉
stm32f4xx_adc.c,这样可以减少工程编译时间。
跑马灯实验我们主要用到的固件库文件是:
stm32f4xx_gpio.c /stm32f4xx_gpio.h
stm32f4xx_rcc.c/stm32f4xx_rcc.h
misc.c/ misc.h
stm32f4xx_usart .c/stm32f4xx_usart.h
stm32f4xx_syscfg.c/stm32f4xx_syscfg.h
其中
stm32f4xx_rcc.h头文件在每个实验中都要引入,因为系统时钟配置函数以及相关的外设时钟使能函数都在这个其源文件
stm32f4xx_rcc.c中。
stm32f4xx_usart.h和
misc.h头文件和对应的源文件在我们
SYSTEM文件夹中都需要使用到,所以每个实验都会引用。
stm32f4xx_syscfg.h和对应的源文件虽然本实验也没有用到,但是后面很多实验都要使用到,所以我们不妨也添加进来。
在
stm32f4xx_conf.h文件里面,这些头文件默认都是打开的,实际我们可以不用理。当然我们也可以注释掉其他不用的头文件,但是如果你引入了某个源文件,一定不能不包含对应的头文件:
#include "stm32f4xx_gpio.h"
#include "stm32f4xx_rcc.h"
#include "stm32f4xx_usart.h"
#include “
stm32f4xx_syscfg.h”
#include "misc.h"
接下来,我们讲解怎样去掉多余的其他的源文件,方法如下图,
右击Template,选择“Manage
project Items”,进入这个选项卡:
图
6.3.2
我们选中“
FWLIB”分组,然后选中不需要的源文件点击删除按钮删掉,留下下图中我们使用到的五个源文件,然后点击
OK:
图
6.3.3
这样我们的工程
FWLIB下面只剩下五个源文件:
图
6.3.4
然后我们进入我们工程的目录,
在工程根目录文件夹下面新建一个HARDWARE的文件夹,用来存储以后与硬件相关的代码。然后在HARDWARE文件夹下新建一个LED文件夹,用来存放与LED相关的代码。如图
6.3.5所示:
图
6.3.5新建
HARDWARE文件夹
接下来,我们回到我们的工程
(如果是使用的上面新建的工程模板,那么就是
Template.uvproj,大家可以将其重命名为
LED.uvproj),按

按钮新建一个文件,然后按

保存在
HARDWARE->LED文件夹下面,保存为
led.c,操作步骤如下图:
图
6.3.6 新建文件
图
6.3.7 保存
led.c
然后在
lcd.c文件中输入如下代码
(代码大家可以直接打开我们光盘的跑马灯实验,从相应的文件中间复制过来),输入后保存即可:
#include "led.h"
//初始化
PF9和
PF10为输出口
.并使能这两个口的时钟
//LED IO初始化
void LED_Init(void)
{
GPIO_InitTypeDef
GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);//使能
GPIOF时钟
//GPIOF9,F10初始化设置
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_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化
GPIO
GPIO_SetBits(GPIOF,GPIO_Pin_9 | GPIO_Pin_10);//GPIOF9,F10设置高,灯灭
}
该代码里面就包含了一个函数
void LED_Init(void),该函数的功能就是用来实现配置
PF9和
PF10为推挽输出。这里需要注意的是:在配置
STM32外设的时候,任何时候都要先使能该外设的时钟!
GPIO是挂载在
AHB1总线上的外设,在固件库中对挂载在
AHB1总线上的外设时钟使能是通过函数
RCC_AHB1PeriphClockCmd ()来实现的。对于这个入口参数设置,在我们前面的“
4.7快速组织代码”章节已经讲解很清楚了。看看我们的代码:
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);//使能
GPIOF时钟
这行代码的作用是使能
AHB1总线上的
GPIOF时钟。
在设置完时钟之后,
LED_Init调用
GPIO_Init函数完成对
PF9和
PF10的初始化配置,然后调用函数
GPIO_SetBits控制
LED0和
LED1输出
1(
LED灭)。至此,两个
LED的初始化完毕。这样就完成了对这两个
IO口的初始化。这段代码的具体含义,大家可以看前面一小节,我们有详细的讲解。初始化函数代码如下:
//GPIOF9,F10初始化设置
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_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉
GPIO_Init(GPIOF, &GPIO_InitStructure);//初始化
GPIO
GPIO_SetBits(GPIOF,GPIO_Pin_9 | GPIO_Pin_10);//GPIOF9,F10设置高,灯灭
保存
led.c代码,然后我们按同样的方法,新建一个
led.h文件,也保存在
LED文件夹下面。在
led.h中输入如下代码:
#ifndef __LED_H
#define __LED_H
#include "sys.h"
//LED端口定义
#define LED0 PFout(9) // DS0
#define LED1 PFout(10) // DS1
void LED_Init(void);//初始化
#endif
这段代码里面最关键就是
2个宏定义:
#define LED0 PFout(9) // DS0 PF9
#define LED1 PFout(10) // DS1 PF10
这里使用的是位带操作来实现操作某个
IO口的
1个位的,关于位带操作前面第五章
5.2.1已经有介绍,这里不再多说。需要说明的是,这里同样可以使用固件库操作来实现
IO口操作。如下:
GPIO_SetBits(GPIOF, GPIO_Pin_9); //设置
GPIOF.9输出
1,等同
LED0=1;
GPIO_ResetBits (GPIOF, GPIO_Pin_9); //设置
GPIOF.9输出
0,等同
LED0=0;
有兴趣的朋友不妨修改我们的位带操作为库函数直接操作,这样也有利于学习。
将
led.h也保存一下。接着,我们在
Manage Components管理里面新建一个
HARDWARE的组,并把
led.c加入到这个组里面,如图
6.3.8所示:
图
6.3.8 给工程新增
HARDWARE组
单击
OK,回到工程,然后你会发现在
Project Workspace里面多了一个
HARDWARE的组,在该组下面有一个
led.c的文件。如图
6.3.9所示:
图
6.3.9新增
HARDWARE组
然后用之前介绍的方法(在
3.3.2节介绍的)将
led.h头文件的路径加入到工程里面,然后点击
OK回到主界面。
图
6.3.10 添加
LED目录到
PATH
回到主界面后,在
main函数里面编写如下代码:
#include
"sys.h"
#include
"delay.h"
#include
"usart.h"
#include
"led.h"
int
main(void)
{
delay_init(168); //初始化延时函数
LED_Init(); //初始化
LED端口
/**下面是通过直接操作库函数的方式实现
IO控制
**/
while(1)
{
GPIO_ResetBits(GPIOF,GPIO_Pin_9); //LED0对应引脚
GPIOF.9拉低,亮
等同
LED0=0;
GPIO_SetBits(GPIOF,GPIO_Pin_10); //LED1对应引脚
GPIOF.10拉高,灭 等同
LED1=1;
delay_ms(500);
//延时
500ms
GPIO_SetBits(GPIOF,GPIO_Pin_9);
//LED0对应引脚
GPIOF.0拉高,灭
等同
LED0=1;
GPIO_ResetBits(GPIOF,GPIO_Pin_10); //LED1对应引脚
GPIOF.10拉低,亮 等同
LED1=0;
delay_ms(500); //延时
500ms
}
}
代码包含了
#include
"led.h"这句,使得
LED0、
LED1、
LED_Init等能在
main()函数里被调用。这里我们需要重申的是,在固件库中,系统在启动的时候会调用
system_stm32f4xx.c中的函数
SystemInit()对系统时钟进行初始化,在时钟初始化完毕之后会调用
main()函数。 所以我们不需要再在
main()函数中调用
SystemInit()函数。当然如果有需要重新设置时钟系统,可以写自己的时钟设置代码,
SystemInit()只是将时钟系统初始化为默认状态。
main()函数非常简单,先调用
delay_init()初始化延时,接着就是调用
LED_Init()来初始化
GPIOF.9和
GPIOF.10为输出。最后在死循环里面实现
LED0和
LED1交替闪烁,间隔为
500ms。
上面是通过库函数来实现的
IO操作,我们也可以修改
main()函数,直接通过位带操作达到同样的效果,大家不妨试试。位带操作的代码如下:
int main(void)
{
delay_init(168); //初始化延时函数
LED_Init(); //初始化
LED端口
while(1)
{
LED0=0; //LED0亮
LED1=1; //LED1灭
delay_ms(500);
LED0=1; //LED0灭
LED1=0; //LED1亮
delay_ms(500);
}
}
当然我们也可以通过直接操作相关寄存器的方法来设置
IO,我们只需要将主函数修改为如下内容:
int main(void)
{
delay_init(168);
//初始化延时函数
LED_Init(); //初始化
LED端口
while(1)
{
GPIOF->BSRRH=GPIO_Pin_9;//LED0亮
GPIOF->BSRRL=GPIO_Pin_10;//LED1灭
delay_ms(500);
GPIOF->BSRRL=GPIO_Pin_9;//LED0灭
GPIOF->BSRRH=GPIO_Pin_10;//LED1亮
delay_ms(500);
}
}
将主函数替换为上面代码,然后重新执行,可以看到,结果跟库函数操作和位带操作一样的效果。大家可以对比一下。这个代码在我们跑马灯实验的
main.c文件中有注释掉,大家可以替换试试。
然后按

,编译工程,得到结果如图
6.3.11所示:
图
6.3.11编译结果
可以看到没有错误,也没有警告。从编译信息可以看出,我们的代码占用
FLASH大小为:
5678字节(
5256+424),所用的
SRAM大小为:
1880个字节(
1832+48)。
这里我们解释一下,编译结果里面的几个数据的意义:
Code:表示程序所占用
FLASH的大小(
FLASH)。
RO-data:即
Read Only-data,表示程序定义的常量(
FLASH)。
RW-data:即
Read Write-data,表示已被初始化的变量(
SRAM)
ZI-data:即
Zero Init-data,表示未被初始化的变量
(SRAM)
有了这个就可以知道你当前使用的
flash和
sram大小了,所以,一定要注意的是程序的大小不是
.hex文件的大小,而是编译后的
Code和
RO-data之和。
接下来,大家就可以下载验证了。如果有
JTAG,则可以用
jtag进行在线调试(需要先下载代码),单步查看代码的运行,
STM32F4的在线调试方法介绍,参见:
3.4.2节。
6.4 下载验证
这里我们使用
flymcu下载(也可以通过
JTAG等仿真器下载,具体方法请参考
3.4.2小节),如图
6.4.1所示:
6.4.1 利用
flymcu下载代码
下载完之后,运行结果如图
6.4.2所示,
LED0和
LED1循环闪烁:
图6.4.2 程序运行结果
至此,我们的第一章的学习就结束了,本章作为
STM32F4的入门第一个例子,介绍了
STM32F4的
IO口的使用及注意事项,同时巩固了前面的学习,希望大家好好理解一下。
实验详细手册和源码下载地址:http://www.openedv.com/posts/list/41586.htm
正点原子探索者STM32F407开发板购买地址:http://item.taobao.com/item.htm?id=41855882779
这两个函数功能类似,只不过前面是用来一次读取一组IO口所有IO口输出状态,后面的函数用来一次读取一组IO口中一个或者几个IO口的输出状态。
这个函数不能用来一次读取几个IO口输出状态吧。
库函数定义:
if (((GPIOx->ODR) & GPIO_Pin) != (uint32_t)Bit_RESET)
{
bitstatus = (uint8_t)Bit_SET;
}
else
{
bitstatus = (uint8_t)Bit_RESET;
}
return bitstatus;
如果几个PIN传进去,只要有一个PIN是高电平,返回就是1
一周热门 更多>