目录
7.1 实验要求
使用STM32标准库实现点亮开发板集成的LED灯模块。
7.2 硬件设计
这些 LED 灯的阴极都是连接到 STM32 的 GPIO 引脚,只要我们控制 GPIO 引脚的电平输出状态,即可控制 LED 灯的亮灭。若您使用的实验板 LED 灯的连接方式或引脚不一样,只需根据我们的工程修改引脚即可,程序的控制原理相同。
7.3 软件设计
为了使工程更加有条理,我们把 LED 灯控制相关的代码独立分开存储,方便以后移植。在“工程模板”之上新建“bsp_led.c”及“bsp_led.h”文件,其中的“bsp”即 Board Support Packet 的缩写 (板级支持包),这些文件也可根据您的喜好命名,这些文件不属于 STM32 标准库的内容,是由我们自己根据应用需要编写的。
7.3.1 编程思路
1、分析实际硬件电路中LED灯点亮条件,三个LED灯的正极均接到3.3V的电压,此时只需让MCU对应端口输出低电平发光二极管即可导通发光。
2、使能对应端口的时钟。
3、配置MCU对应端口为推挽输出模式。
7.3.2 项目中用到的标准库函数和结构体分析
7.3.2.1 GPIO初始化结构体定义
typedef struct
{
uint16_t GPIO_Pin; //指定要配置的 GPIO 引脚
GPIOSpeed_TypeDef GPIO_Speed; //指定所选引脚的速度
GPIOMode_TypeDef GPIO_Mode; //指定所选引脚的工作模式
}GPIO_InitTypeDef;
7.3.2.2 库函数分析
(1)void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)函数
①功能:使能APB2总线对应GPIO口时钟;
②参数:uint32_t RCC_APB2Periph 要使能的 GPIO口名称;
FunctionalState NewState 决定端口是否使能(ENABLE:使能,DISABLE:不使能);
例子:
//开启GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
(2)void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)函数
①功能:配置GPIO口的输出输入功能,将GPIO_InitTypeDef结构体中的寄存器依次配置为相应的模式。
②参数:GPIO_TypeDef* GPIOx 对应得GPIO端口;
GPIO_InitTypeDef* GPIO_InitStruct 是一个结构体指针类型;
例子:
//初始化对应端口
//GPIO_InitStruct为自己定义得结构体变量
GPIO_Init(GPIOA,GPIO_InitStruct)
7.3.3 代码解析
7.3.3.1 LED灯引脚宏定义
在编写应用程序的过程中,要考虑更改硬件环境的情况,例如 LED 灯的控制引脚与当前的不一样,我们希望程序只需要做最小的修改即可在新的环境正常运行。这个时候一般把硬件相关的部分使用宏来封装,若更改了硬件环境,只修改这些硬件相关的宏即可,这些定义一般存储在头文件,即本例子中的“bsp_led.h”文件中。
#ifndef _BSP_LED_H
#define _BSP_LED_H
#include "stm32f10x.h"
//红灯
#define LED1_GPIO_PORT GPIOB
#define LED1_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED1_GPIO_PIN GPIO_Pin_5
//绿灯
#define LED2_GPIO_PORT GPIOB
#define LED2_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED2_GPIO_PIN GPIO_Pin_0
//蓝灯
#define LED3_GPIO_PORT GPIOB
#define LED3_GPIO_CLK RCC_APB2Periph_GPIOB
#define LED3_GPIO_PIN GPIO_Pin_1
以上代码分别把控制 LED 灯的 GPIO 端口、 GPIO 引脚号以及 GPIO 端口时钟封装起来了。在实际控制的时候我们就直接用这些宏,以达到应用代码硬件无关的效果。
7.3.3.2 控制LED灯两灭状态得宏定义
//直接操作寄存器的方法控制IO口
#define digitalHi(p,i) {p->BSRR = i;}
#define digitalLo(p,i) {p->BRR = i;}
#define digitalToggle(p,i) {p->ODR = i;}
//定义控制IO的宏
#define LED1_Toggle digitalToggle(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED1_OFF digitalHi(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED1_ON digitalLo(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED2_Toggle digitalToggle(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED2_OFF digitalHi(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED2_ON digitalLo(LED2_GPIO_PORT,LED2_GPIO_PIN)
#define LED3_Toggle digitalToggle(LED3_GPIO_PORT,LED3_GPIO_PIN)
#define LED3_OFF digitalHi(LED3_GPIO_PORT,LED3_GPIO_PIN)
#define LED3_ON digitalLo(LED3_GPIO_PORT,LED3_GPIO_PIN)
//各种颜色的显示
#define LED_RED \
LED1_ON; \
LED2_OFF; \
LED3_OFF
#define LED_GREEN \
LED1_OFF; \
LED2_ON; \
LED3_OFF
#define LED_BLUE \
LED1_OFF; \
LED2_OFF; \
LED3_ON
#define LED_YELLOW \
LED1_ON; \
LED2_ON; \
LED3_OFF
#define LED_PURPLE \
LED1_ON; \
LED2_OFF; \
LED3_ON
#define LED_WHITE \
LED1_ON; \
LED2_ON; \
LED3_ON
#define LED_RGBOFF \
LED1_OFF; \
LED2_OFF; \
LED3_OFF
这部分宏控制 LED 亮灭的操作是直接向 BSRR、 BRR 和 ODR 这三个寄存器写入控制指令来实现的,对 BSRR 写 1 输出高电平,对 BRR 写 1 输出低电平,对 ODR 寄存器某位进行异或操作可反转位的状态。RGB 彩灯可以实现混色,如最后一段代码我们控制红灯和绿灯亮而蓝灯灭,可混出黄色效果。代码中的“”是 C 语言中的续行符语法,表示续行符的下一行与续行符所在的代码是同一行。代码中因为宏定义关键字“#define”只是对当前行有效,所以我们使用续行符来连接起来,以下的代码是等效的:
#define LED_YELLOW LED1_ON; LED2_ON; LED3_OFF
7.3.3.3 LED灯端口初始化函数
利用上面的宏,编写 LED 灯的初始化函数。
#include "bsp_led.h"
void LED_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(LED1_GPIO_CLK|
LED2_GPIO_CLK|
LED3_GPIO_CLK,ENABLE);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_Pin = LED1_GPIO_PIN;
GPIO_Init(LED1_GPIO_PORT,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = LED2_GPIO_PIN;
GPIO_Init(LED2_GPIO_PORT,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = LED3_GPIO_PIN;
GPIO_Init(LED3_GPIO_PORT,&GPIO_InitStruct);
//将所有的LED端口拉高
GPIO_SetBits(LED1_GPIO_PORT,LED1_GPIO_PIN);
GPIO_SetBits(LED2_GPIO_PORT,LED2_GPIO_PIN);
GPIO_SetBits(LED3_GPIO_PORT,LED3_GPIO_PIN);
}
函数执行的带盖流程:
(1) 使用 GPIO_InitTypeDef 定义 GPIO 初始化结构体变量,以便下面用于存储 GPIO 配置。
(2) 调用库函数 RCC_APB2PeriphClockCmd 来使能 LED 灯的 GPIO 端口时钟,在前面的章节中我们是直接向 RCC 寄存器赋值来使能时钟的,不如这样直观。该函数有两个输入参数,第一个参数用于指示要配置的时钟,如本例中的“RCC_APB2Periph_GPIOB”,应用时我们使用“|”操作同时配置 3 个 LED 灯的时钟;函数的第二个参数用于设置状态,可输入“Disable”关闭或“Enable”使能时钟。
(3) 向 GPIO 初始化结构体赋值,把引脚初始化成推挽输出模式,其中的 GPIO_Pin 使用宏“LEDx_GPIO_PIN”来赋值,使函数的实现方便移植。
(4) 使用以上初始化结构体的配置,调用 GPIO_Init 函数向寄存器写入参数,完成 GPIO 的初始化,这里的 GPIO 端口使用“LEDx_GPIO_PORT”宏来赋值,也是为了程序移植方便。
(5) 使用同样的初始化结构体,只修改控制的引脚和端口,初始化其它 LED 灯使用的 GPIO 引脚。
(6) 使用宏控制 RGB 灯默认关闭。
7.3.3.4 主函数部分代码
编写完 LED 灯的控制函数后,就可以在 main 函数中测试了。
#include "stm32f10x.h"
#include "bsp_led.h"
void delay(uint32_t count); //延时函数的声明
int main(void)
{
LED_GPIO_Config(); //初始化LED的端口
while(1)
{
//关闭相应端口控制的灯
GPIO_SetBits(LED2_GPIO_PORT,LED2_GPIO_PIN);
//软件延时
delay(0xFFFFF);
//打开相应端口控制的灯
GPIO_ResetBits(LED2_GPIO_PORT,LED2_GPIO_PIN);
delay(0xFFFFF);
}
}
void delay(uint32_t count)
{
for(;count != 0;count--);
}
在 main 函数中,调用我们前面定义的 LED_GPIO_Config 初始化好 LED 的控制引脚,然后直接调用各种控制 LED 灯亮灭的宏来实现 LED 灯的控制。以上,就是一个使用 STM32 标准软件库开发应用的流程。
7.4 下载验证
观察到开发板上的LED灯亮起并有规律的闪烁,用同样的方法可以把其他颜色的灯点亮。
7.5 防止头文件被重复包含
在 STM32 标准库的所有头文件以及我们自己编写的“bsp_led.h”头文件中,可看到类似代码清单的宏定义。它的功能是防止头文件被重复包含,避免引起编译错误。
#ifndef _BSP_LED_H
#define _BSP_LED_H
#endif /_BSP_LED_H/
在头文件的开头,使用“#ifndef”关键字,判断标号“_BSP_LED_H”是否被定义,若没有被定义,则从“#ifndef”至“#endif”关键字之间的内容都有效,也就是说,这个头文件若被其它文件“#include”,它就会被包含到其该文件中了,且头文件中紧接着使用
“#define”关键字定义上面判断的标号“_BSP_LED_H”。当这个头文件被同一个文件第二次“#include”包含的时候,由于有了第一次包含中的“#define _BSP_LED_H”定义,这时再判断“#ifndef _BSP_LED_H”,判断的结果就是假了,从“#ifndef”至#endif”之间的内容都无效,从而防止了同一个头文件被包含多次,编译时就不会出现“redefine(重复定义)”的错误了。
一般来说,我们不会直接在 C 的源文件写两个“#include”来包含同一个头文件,但可能因为头文件内部的包含导致重复,这种代码主要是避免这样的问题。如“bsp_led.h”文件中使用了“#include“stm32f10x.h”语句,按习惯,可能我们写主程序的时候会在 main 文件写“#include “bsp_led.h”及 #include“stm32f10x.h””,这个时候“stm32f10x.h”文件就被包含两次了,如果没有这种机制,就会出错。
至于为什么要用下划线来定义“_BSP_LED_H”标号,其实这只是防止它与其它普通宏定义重复了,如我们用“GPIO_PIN_0”来代替这个判断标号,就会因为 stm32f10x.h 已经定义了 GPIO_PIN_0,结果导致“bsp_led.h”文件无效了,“bsp_led.h”文件一次都没被包含。
7.6 拓展实验(STM32入门之流水灯)
流水灯是一种电子电路,通常由一系列 LED 灯组成,可以依次点亮或熄灭,形成一个像流水一样的效果。其基本原理是利用时序控制电路控制多个 LED 灯按照特定的顺序点亮或熄灭。
具体地说,流水灯的电路中通常包括一个时钟信号源、一个计数器和多个输出驱动器。时钟信号源会产生一个固定的频率的脉冲信号,计数器会将这个脉冲信号进行计数,并将计数器的计数值输出到输出驱动器。输出驱动器接收计数器的输出信号,并根据这个信号来控制 LED 灯的点亮或熄灭。通常情况下,流水灯的输出驱动器会有多个输出端口,每个输出端口对应一个 LED 灯。每当计数器的计数值发生改变时,输出驱动器会将之前点亮的 LED 灯熄灭,并点亮下一个 LED 灯,从而实现流水灯的效果。
总的来说,流水灯的基本原理就是利用计数器和输出驱动器来控制多个 LED 灯按照特定的顺序点亮或熄灭。
7.6.1 程序设计思路
根据流水灯的原理我们只需要让不同颜色的灯按照一定的规律循环点亮即可,代码层面上我们只需要更改main函数里的逻辑即可实现流水灯。
#include "stm32f10x.h"
#include "bsp_led.h"
#define SOFT_DELAY delay(0xFFFFF)
void delay(uint32_t count);
int main(void)
{
LED_GPIO_Config();
while(1)
{
//红灯
LED_RED;
SOFT_DELAY;
//绿灯
LED_GREEN;
SOFT_DELAY;
//蓝灯
LED_BLUE;
SOFT_DELAY;
}
}
void delay(uint32_t count)
{
for(;count != 0;count--);
}
7.6.2 下载验证
开发板的LED灯模块循环点亮红、绿、蓝三种颜色的灯。
注:流水灯实验是基于点亮LED代码基础上实现的,所以在代码实现上需要包含之前分析的版级支持包。
谢谢阅读!