1、硬件设计

STM32与LED的连接见图1-1所示,这是一个RGB灯,由红蓝绿3个小灯构成,使用PWM控制时可以混合成256种不同的颜色。

STM32CubeMX教程点亮LED灯_初始化

这些LED的阴极都连接到STM32的GPIO引脚,只要我们控制GPIO引脚的电平输出状态,即可控制LED的亮灭。

2、软件设计
为了使工程更加有条理,我们把LED控制相关的代码独立分开存储,方便以后移植。在“工程模板”之上新建bsp_led.c及bsp_led.h文件,其中的bsp即Board Support Packet的缩写(板级支持包),这些文件也可根据个人喜好命名,不属于STM32标准库的内容,是由我们自己根据应用需要编写的。

2.1 编程要点
1)使能GPIO端口时钟;
2)初始化GPIO目标引脚为推挽输出模式;
3)编写简单测试程序,控制GPIO引脚输出高、低电平。

2.2 代码分析
1.LED引脚宏定义
在编写应用程序的过程中,要考虑更改硬件环境的情况,例如LED的控制引脚与当前的不一样,我们希望程序只需要做最小的修改即可在新的环境正常运行。这个时候一般把硬件相关的部分使用宏来封装,若更改了硬件环境,只修改这些硬件相关的宏即可。这些定义一般存储在头文件,即本例子中的bsp_led.h文件中,见代码清单1-1。
代码清单1-1 LED控制引脚相关的宏

1 // R-红色
    2 #define LED1_GPIO_PORT       GPIOB
    3 #define LED1_GPIO_CLK        RCC_APB2Periph_GPIOB
    4 #define LED1_GPIO_PIN        GPIO_Pin_5
    5 // G-绿色
    6 #define LED2_GPIO_PORT       GPIOB
    7 #define LED2_GPIO_CLK        RCC_APB2Periph_GPIOB
    8 #define LED2_GPIO_PIN        GPIO_Pin_0
    9 // B-蓝色
   10 #define LED3_GPIO_PORT       GPIOB
   11 #define LED3_GPIO_CLK        RCC_APB2Periph_GPIOB
   12 #define LED3_GPIO_PIN        GPIO_Pin_1

以上代码分别把控制LED的GPIO端口、GPIO引脚号以及GPIO端口时钟封装起来了。在实际控制的时候我们就直接用这些宏,以达到应用代码与硬件无关的效果。

其中的GPIO时钟宏RCC_APB2Periph_GPIOB是STM32标准库定义的GPIO端口时钟相关的宏,它的作用与GPIO_Pin_x这类宏类似,用于指示寄存器位,方便库函数使用,下面初始化GPIO时钟的时候可以看到它的用法。

2.控制LED亮灭状态的宏定义
为了方便控制LED,我们把LED常用的亮、灭及状态反转的控制也直接定义成宏,见代码清单1-2。
代码清单1-2 控制LED亮灭的宏

1 /* 直接操作寄存器的方法控制IO */
 2 #define digital Hi(p,i)         {p->BSRR=i;}     //输出高电平
 3 #define digital Lo(p,i)         {p->BRR=i;}      //输出低电平
 4 #define digital Toggle(p,i)     {p->ODR ^=i;}    //输出反转状态
 5
 6
 7 /* 定义控制IO的宏 */
 8 #define LED1_TOGGLE    digital Tog
gle(LED1_GPIO_PORT,LED1_GPIO_PIN)
 9 #define LED1_OFF       digital Hi(LED1_GPIO_PORT,LED1_GPIO_PIN)
10 #define LED1_ON         digital Lo(LED1_GPIO_PORT,LED1_GPIO_PIN)
11
12 #define LED2_TOGGLE        digital Toggle(LED2_GPIO_PORT,LED2_GPIO_PIN)
13 #define LED2_OFF           digital Hi(LED2_GPIO_PORT,LED2_GPIO_PIN)
14 #define LED2_ON            digital Lo(LED2_GPIO_PORT,LED2_GPIO_PIN)
15
16 #define LED3_TOGGLE        digital Toggle(LED2_GPIO_PORT,LED3_GPIO_PIN)
17 #define LED3_OFF           digital Hi(LED2_GPIO_PORT,LED3_GPIO_PIN)
18 #define LED3_ON            digital Lo(LED2_GPIO_PORT,LED3_GPIO_PIN)
19
20 /* 基本混色,后面高级用法使用PWM可混出全彩颜色,且效果更好 */
21
22 //红
23 #define LED_RED       \
24                       LED1_ON;\
25                       LED2_OFF;\
26                       LED3_OFF
27
28 //绿
29 #define LED_GREEN    \
30                       LED1_OFF;\
31                       LED2_ON;\
32                       LED3_OFF
33
34 //蓝
35 #define LED_BLUE      \
36                       LED1_OFF;\
37                       LED2_OFF;\
38                       LED3_ON
39
40
41 //黄(红+绿)
42 #define LED_YELLOW   \
43                       LED1_ON;\
44                       LED2_ON;\
45                       LED3_OFF
46 //紫(红+蓝)
47 #define LED_PURPLE   \
48                       LED1_ON;\
49                       LED2_OFF;\
50                       LED3_ON
51
52 //青(绿+蓝)
53 #define LED_CYAN      \
54                       LED1_OFF;\
55                       LED2_ON;\
56                       LED3_ON
57
58 //白(红+绿+蓝)
59 #define LED_WHITE    \
60                       LED1_ON;\
61                       LED2_ON;\
62                       LED3_ON
63
64 //黑(全部关闭)
65 #define LED_RGBOFF   \
66                       LED1_OFF;\
67                       LED2_OFF;\
68                       LED3_OFF

这部分宏控制LED亮灭的操作是直接向BSRR、BRR和ODR这3个寄存器写入控制指令来实现的,对BSRR写1输出高电平,对BRR写1输出低电平,对ODR寄存器某位进行异或操作可反转位的状态。
RGB彩灯可以实现混色,如第41行代码控制红灯和绿灯亮而蓝灯灭,可混出黄色效果。

代码中的“\”是C语言中的续行符语法,表示续行符的下一行与续行符所在的代码是同一行。因为代码中宏定义关键字“#define”只对当前行有效,所以我们使用续行符来连接起来。以下的代码是等效的:
#define LED_YELLOW LED1_ON; LED2_ON; LED3_OFF
应用续行符的时候要注意,在“\”后面不能有任何字符(包括注释、空格),只能直接换行。

3.LED GPIO初始化函数
利用上面的宏即可编写LED的初始化函数,见代码清单11-3。
代码清单11-3 LED GPIO初始化函数

1 void LED_GPIO_Config(void)
   2 {
   3     /*定义一个GPIO_Init Type Def类型的结构体*/
   4     GPIO_Init Type Def GPIO_Init Structure;
   5
   6     /*开启LED相关的GPIO外设时钟*/
   7     RCC_APB2Periph Clock Cmd(  LED1_GPIO_CLK|
   8                                LED2_GPIO_CLK|
   9                                LED3_GPIO_CLK, ENABLE);
  10     /*选择要控制的GPIO引脚*/
  11     GPIO_Init Structure.GPIO_Pin = LED1_GPIO_PIN;
  12
  13     /*设置引脚模式为通用推挽输出*/
  14     GPIO_Init Structure.GPIO_Mode = GPIO_Mode_Out_PP;
  15
  16     /*设置引脚速率为50MHz */
  17     GPIO_Init Structure.GPIO_Speed = GPIO_Speed_50MHz;
  18
  19     /*调用库函数,初始化GPIO*/
  20     GPIO_Init(LED1_GPIO_PORT, &GPIO_Init Structure);
  21
  22     /*选择要控制的GPIO引脚*/
  23     GPIO_Init Structure.GPIO_Pin = LED2_GPIO_PIN;
  24
  25     /*调用库函数,初始化GPIO */
  26     GPIO_Init(LED2_GPIO_PORT, &GPIO_Init Structure);
  27
  28     /*选择要控制的GPIO引脚*/
  29     GPIO_Init Structure.GPIO_Pin = LED3_GPIO_PIN;
  30
  31     /*调用库函数,初始化GPIO */
  32     GPIO_Init(LED3_GPIO_PORT, &GPIO_Init Structure);
  33
  34     /* 关闭LED灯1 */
  35     GPIO_Set Bits(LED1_GPIO_PORT, LED1_GPIO_PIN);
  36
  37     /* 关闭LED灯2 */
  38     GPIO_Set Bits(LED2_GPIO_PORT, LED2_GPIO_PIN);
  39
  40     /* 关闭LED灯3 */
  41     GPIO_Set Bits(LED3_GPIO_PORT, LED3_GPIO_PIN);
  42 }

整个函数与第8章中的类似,主要区别是硬件相关的部分使用宏来代替,初始化GPIO端口时钟时也采用了STM32库函数。函数执行流程如下:

1)使用GPIO_Init Type Def定义GPIO初始化结构体变量,以便后面用于存储GPIO配置。

2)调用库函数RCC_APB2Periph Clock Cmd来使能LED的GPIO端口时钟。在前面的章节中我们是直接向RCC寄存器赋值来使能时钟的,不如这样直观。该函数有两个输入参数,第1个参数用于指示要配置的时钟,如本例中的RCC_ APB2Periph_GPIOB,应用时我们使用“|”操作同时配置3个LED的时钟;函数的第2个参数用于设置状态,可输入Disable关闭或Enable使能时钟。

3)向GPIO初始化结构体赋值,把引脚初始化成推挽输出模式,其中的GPIO_Pin使用宏LEDx_GPIO_PIN来赋值,使函数的实现便于移植。

4)使用以上初始化结构体的配置,调用GPIO_Init函数向寄存器写入参数,完成GPIO的初始化。这里的GPIO端口使LEDx_GPIO_PORT宏来赋值,也是为了程序移植方便。

5)使用同样的初始化结构体,只修改控制的引脚和端口,初始化其他LED使用的GPIO引脚。

6)使用宏控制RGB灯默认关闭。

4.main函数
编写完LED的控制函数后,就可以在main函数中测试了,见代码清单1-4。
代码清单1-4 控制LED,main文件

1 #include“stm32f10x.h”
    2 #include“./led/bsp_led.h”
    3
    4 #define SOFT_DELAY Delay(0x0FFFFF);
    5
    6 void Delay(__IO u32 n Count);
    7
    8 /**
    9   * @brief  main函数
    10   * @param  无
    11   * @retval无
    12   */
    13 int main(void)
    14 {
    15     /* LED端口初始化 */
    16     LED_GPIO_Config();
    17
    18     while (1)
    19     {
    20          LED1_ON;            //亮
    21          SOFT_DELAY;
    22          LED1_OFF;           //灭
    23
    24          LED2_ON;            //亮
    25          SOFT_DELAY;
    26          LED2_OFF;           //灭
    27
    28          LED3_ON;            //亮
    29          SOFT_DELAY;
    30          LED3_OFF;           //灭
    31
    32          /*轮流显示红、绿、蓝、黄、紫、青、白颜色*/
    33          LED_RED;
    34          SOFT_DELAY;
    35
    36          LED_GREEN;
    37          SOFT_DELAY;
    38
    39          LED_BLUE;
    40          SOFT_DELAY;
    41
    42          LED_YELLOW;
    43          SOFT_DELAY;
    44
    45          LED_PURPLE;
    46          SOFT_DELAY;
    47
    48          LED_CYAN;
    49          SOFT_DELAY;
    50
    51          LED_WHITE;
    52          SOFT_DELAY;
    53
    54          LED_RGBOFF;
    55          SOFT_DELAY;
    56     }
    57 }
    58
    59 void Delay(__IO uint32_t n Count)     //简单的延时函数
    60 {
    61     for (; n Count != 0; n Count--);
    62 }

在main函数中,调用我们前面定义的LED_GPIO_Config初始化好LED的控制引脚,然后直接调用各种控制LED亮灭的宏来实现LED的控制。
以上就是一个使用STM32标准软件库开发应用的流程。

3 下载验证
把编译好的程序下载到开发板并复位,可看到RGB彩灯轮流显示不同的颜色。

4 STM32标准库补充知识
1.System Init函数在哪里
在前面章节中我们自己新建工程的时候需要定义一个System Init空函数,但是在这个用STM32标准库开发的工程却没有这样做,System Init函数在哪里呢?

这个函数在STM32标准库的system_stm32f10x.c文件中定义了,而我们的工程已经包含该文件。
标准库中的System Init函数把STM32芯片的系统时钟设置成了72MHz,即此时AHB时钟频率为72MHz,APB2为72MHz,APB1为36MHz。当STM32芯片上电后,执行启动文件中的指令时,会调用该函数,将系统时钟设置为以上状态。

2.断言
细心对比前几章我们自己定义的GPIO_Init函数与STM32标准库中同名函数,会发现标准库中的函数内容多了一些乱七八糟的东西,那是断言.

3.Doxygen注释规范
在STM32标准库以及我们自己编写的bsp_led.c文件中,可以看到一些比较特别的注释,类似代码清单1-8。
代码清单1-8 Doxygen注释规范

1 /**
      2     * @brief  初始化控制LED的IO
      3     * @param  无
      4     * @retval无
      5     */

这是一种名为Doxygen的注释规范,如果在工程文件中按照这种规范去注释,可以使用Doxygen软件自动根据注释生成帮助文档。我们所说的非常重要的库帮助文档《stm32f10x_stdperiph_lib_um.chm》,就是由该软件根据库文件的注释生成的。关于Doxygen注释规范本教程不作讲解,感兴趣的读者可自行搜索网络上的资料学习。

4.防止头文件重复包含
在STM32标准库的所有头文件以及我们自己编写的bsp_led.h头文件中,可看到类似代码清单11-9的宏定义。它的功能是防止头文件被重复包含,避免引起编译错误。
代码清单1-9 防止头文件重复包含的宏

1 #ifndef __LED_H
    2 #define __LED_H
    3
    4 /*此处省略头文件的具体内容*/
    5
    6 #endif /* end of __LED_H */

在头文件的开头,使用“#ifndef”关键字,判断标号“__LED_H”是否被定义,若没有被定义,则从“#ifndef”至“#endif”关键字之间的内容都有效。
也就是说,这个头文件若被其他文件“#include”,它就会被包含到该文件中了,且头文件中紧接着使用“#define”关键字定义上面判断的标号“__LED_H”。
当这个头文件被同一个文件第2次“#include”包含的时候,由于有了第1次包含中的“#define __LED_H”定义,这时再判断“#ifndef __LED_H”,判断的结果就是假了,
从“#ifndef”至“#endif”之间的内容都无效,从而防止了同一个头文件被包含多次,编译时就不会出现“redefine(重复定义)”的错误了。

一般来说,我们不会直接在C的源文件写两个“#include”来包含同一个头文件,但可能因为头文件内部的包含导致重复,这种代码主要是避免这样的问题。
如bsp_led.h文件中使用了“#include “stm32f10x.h””语句,按习惯,可能我们写主程序的时候会在main文件写“#include "bsp_led.h"及#include “stm32f10x.h””,这个时候“stm32f10x.h”文件就被包含两次了,如果没有这种机制,就会出错。

至于为什么要用两个下划线来定义“__LED_H”标号,其实这只是防止它与其他普通宏定义重复了,如我们用“GPIO_PIN_0”来代替这个判断标号,就会因为stm32f10x.h已经定义了GPIO_PIN_0,结果导致bsp_led.h文件无效了,bsp_led.h文件一次都没被包含。