STM32初学之闪烁LED——以Cortex-M0核心的STM32F030F4P6为例
刚刚接触STM32,就被它超高的性价比所吸引了,真的如宣传的那样“花8位MCU的钱,买32位MCU的性能”。
看看这个是我花10元买的STM32F030F4P6的板子:
1.jpg
比同价格的Arduino性能不知高多少倍,外设也好丰富!
也正是因为STM32相对51单片机而言复杂的多,所以开发过程也复杂得多,包括开发环境搭建、建立工程、代码编写与下载调试,都变得复杂了。虽然现在事后想想其实也还好,但是一开始什么都不懂的时候,真的是不知所措,所以有必要记录一下。
=====================阶段一:开发环境搭建=================
还记得51单片机用的是什么开发环境吗?嗯,一般都是用的Keil uVision2,图标长这个样子:
2.jpg
界面长这个样子:
3.jpg
很亲切吧~好消息是,STM32的开发环境非常类似,用的是Keil uVision4或者Keil uVision5。我用的是Keil uVision5,图标长这样:
4.jpg
界面长这样:
5.jpg
菜单也基本一样。所以不会再感到陌生了吧~
好了,那么就开始安装吧:
(1)下载#HREF"http://pan.baidu.com/s/1mgZwFNU"#-HREF1MDK511.exe#-HREF2(MCU Development Kit 5.11,也叫做Keil MDK,MDK ARM等等),双击运行,一路按照提示安装:
6.jpg
(2)安装完MDK之后,会出现是否安装“KEIL – Tools By ARM”的对话框,如图:
7.jpg
果断选择安装;
(3)安装完成~
安装完成之后,会弹出”Pack Installer“窗口:
8.jpg
这个是用来配置各种器件库的。可以暂时不用理他,直接关闭,我们待会儿需要时会找出他来。
=====================阶段二:配置器件库======================
现在来说说刚刚弹出的器件库。在Keil uVision4及之前的版本中,软件自带了各种器件的资料、手册等。然而,随着五花八门的器件越来越多,MDK变得越来越大。另一方面,对于某个特定的开发者而言,大多数器件都是不会用到的。所以,自从Keil uVision5开始,器件改用在线下载的方式了。而管理器件的软件就是这个Pack Installer。
打开Keil uVision5,点击下图中按钮来运行Pack Installer:
9.jpg
可以在右边的”Devices“标签页中找自己的型号。
比如接下来我们会用到STM32F030F4P6,那么就在Devices标签中,找到STMicroelectronics => STM32F0 Series => STM32F030 => STM32F030F4,点击它:
10.jpg
选中之后,左边的Packs标签页下就出现了相关的资源,可以按需要点击”Install“:
11.jpg
由于Keil::STM32F0xx_DFP是必需的,所以我们安装之。IwIP可选可不选,无所谓,需要时再来装也不迟。
至此,器件库也已经配置好了。
========================阶段三:制作代码模板====================
由于STM32比较复杂,有上百个寄存器,所以继续使用51单片机那样直接操作寄存器的编程方法显然力不从心了。于是,STM32官方就编写了一个庞大的函数库,这个函数库中的函数把对寄存器的操作封装了起来,让程序员可以从繁杂的寄存器操作中解放出来,提高了开发效率,也减少了出错的可能性。当然,这是以性能作为代价的。所以,初学者还是使用这个函数库来编程比较合适,而水平高了以后,可以逐渐接触直接操作寄存器的方式。
这个函数库就叫做固件库。
而代码模板呢,说白了就是一个最简工程,它由固件库、启动代码、中断函数组成。以后每当要新建一个工程,直接把代码模板复制一份,然后开始写用户自己的代码即可。
代码模板将在下文以压缩包的方式提供。
(1)在任意位置新建一个文件夹STM32F0_template,比如我的是D:STM32F0_template;
(2)在STM32F0_template中新建如下文件夹:boot、interrupt、library、CMSIS和src;
(3)将startup_stm32f0xx.s复制到boot目录下,这个是STM32F0系列的启动代码和中断向量表;
(4)将stm32f0xx_it.h和stm32f0xx_it.c复制到interrupt目录,这个是中断函数相关代码;
(5)把STM32F0xx_StdPeriph_Driver中的inc与src两个文件夹复制到library目录,这个是官方固件库;
(6)把core_cm0.h、core_cm0plus.h、stm32f0xx.h、system_stm32f0xx.h和system_stm32f0xx.h复制到CMSIS目录;
(7)把stm32f0xx_conf.h复制到src目录。
至此,STM32F0xx系列的代码模板完成了。可以直接下载#HREF"http://pan.baidu.com/s/1skeCnd7"#-HREF1STM32F0_template.zip#-HREF2。
=====================阶段四:创建工程=======================
有了代码模板之后,就可以在此基础上创建工程的基本环境了。
(1)新建一个文件夹D:STM32F030F4P6_blink,把代码模板中boot、interrupt、library、CMSIS和src五个文件夹复制进去;
(2)打开Keil uVision5,菜单Project => New uVision Project,双击STM32F030F4P6_blink文件夹,工程名为blink,点击保存:
12.jpg
(3)之后会弹出窗口”Select Device for Target ‘Target 1’“,让你选择目标设备,我的板子是STM32F030F4P6,所以选择STMicroelectronice => STM32F0 Series => STM32F030 => STM32F030F4:
13.jpg
(4)接下来会弹出”Manage Run-Time Environment“窗口,不用管它,直接关闭:
14.jpg
(5)接下来生成了工程。在Target1上右击 => Manager Project Items,弹出窗口如下:
15.jpg
在Project Targets中重命名Target1为blink,在Groups中删除Source Group 1,并且新建boot、interrupt、library、CMSIS和src五个组,如图:
16.jpg
选中boot,点击Add Files,把D:STM32F030F4P6_blinkbootstartup_stm32f0xx.s添加入boot组中,同理,把interrupt目录下的stm32f0xx_it.c添加入interrupt组,把CMSIS目录下的system_stm32f0xx.c添加入CMSIS组,把library/src目录下的C文件添加入library组。最后结果如图:
17.jpg
注意,通常添加入组的文件都是C文件或者汇编文件,告诉工程管理器这些文件是需要编译的。头文件只是辅助编译的功能而已,所以通常不加入组(加入组也没有关系),但是必须要在项目中指定include path,否则编译可能找不到头文件。
(6)指定include path。菜单Project => Options for Target ‘blink’:
18.jpg
在弹出的窗口中选中C/C++,并且在Include Paths中填写”.boot;.interrupt;.libraryinc;.CMSIS;.src“,也就是在那5个文件夹中搜索头文件:
19.jpg
至此,一个工程的基本框架就算搭建完了。
======================阶段五:编写代码========================
在src组新建main.c,然后贴入如下代码:
+++code
#include "stm32f0xx_conf.h"

#define LED_GPIO_CLK RCC_AHBPeriph_GPIOA
#define LED_PORT GPIOA
#define LED_PIN GPIO_Pin_4

void led_init()
{
    GPIO_InitTypeDef t_gpio;
    RCC_AHBPeriphClockCmd(LED_GPIO_CLK,ENABLE);
    t_gpio.GPIO_Pin=LED_PIN;
    t_gpio.GPIO_Speed=GPIO_Speed_2MHz;
    t_gpio.GPIO_Mode=GPIO_Mode_OUT;
    t_gpio.GPIO_OType=GPIO_OType_PP;
    GPIO_Init(LED_PORT,&t_gpio);
}

void led_on()
{
    GPIO_SetBits(LED_PORT,LED_PIN);
}

void led_off()
{
    GPIO_ResetBits(LED_PORT,LED_PIN);
}

void delay()
{
    unsigned int t_tick=0xFFFFF;
    while(t_tick--);
}

int main()
{
    led_init();
    while(1)
    {
        led_on();
        delay();
        led_off();
        delay();
    }
}
---code
代码的讲解放到最后的附注中,现在先让代码跑起来,以快速获得成就感~
菜单Project => Options for Target ‘blink’,
20.jpg
在弹出的对话框中选择”Output“标签页,勾选”Create HEX File“:
21.jpg
之后,点击Build按钮,编译项目:
22.jpg
成功结束后,会生成blink.hex文件:
23.jpg
这个hex文件就可以烧录到板子里去啦!
=======================阶段六:烧录========================
我喜欢用串口来下载程序,使用USB-TTL即可。连线如下:
24.jpg
注意是3.3V,不要接到5V上去了!
25.jpg
将USB-TTL插入电脑。
板子的烧录可以使用官方提供的Flash Loader Demonstrator,不过个人不推荐,因为软件有很多bug,很容易卡死。可以使用国人自己开发的FlyMcu来烧录。
双击运行#HREF"http://pan.baidu.com/s/1kUzhxA3"#-HREF1FlyMcu.exe#-HREF2,如果已经把USB-TTL插入了电脑的话,应该是能够直接搜索到串口的,比如我这里是COM3:
26.jpg
在“联机下载时的程序文件”中指定hex文件的路径,我的是“D:STM32F030F4P6_blinkblink.hex”,如图:
27.jpg
注意此时的STM32F030F4P6板子的BOOT0的跳线帽是拔掉的,也就是说BOOT0引脚悬空:
28.jpg
然后点击那个大大的按钮“开始编程”,眨眼间程序就烧录完了:
29.jpg
此时,断开STM32F030F4P6板子的电源,套上跳线帽,重新上电,会发现板子上的红色LED不停闪烁~~(好吧,不得不承认是由两张图合成的。。。):
30.gif
在烧录阶段的最后要补充解释一下那个BOOT0引脚是怎么回事。
STM32有三种启动方式:
#TABLE
#TBODY
#TR
#THBOOT0#-TH
#THBOOT1#-TH
#TH启动方式#-TH
#-TR
#TR
#TD0#-TD
#TDx#-TD
#TD从FLASH启动,运行用户代码#-TD
#TR
#TR
#TD1#-TD
#TD0#-TD
#TD从ROM启动,运行厂商的启动程序(通常支持串口下载)#-TD
#TR
#TR
#TD1#-TD
#TD1#-TD
#TD从内置SRAM启动,通常用于调试#-TD
#TR
#-TBODY
#-TABLE
由于SRAM中的数据或代码掉电丢失,而且现在电脑上有软件仿真,所以从内置SRAM启动的这种方式很少使用。于是,STM32F030F4P6这块板子设计的时候,就直接把CPU的BOOT1引脚接地(始终为0),板子上就没有BOOT1引脚了。而CPU的BOOT0引脚通过一个电阻上拉到电源。BOOT0和BOOT1的电路如图:
31.jpg
所以当把板子上的BOOT0悬空时,相当于BOOT0接高电平,于是可以串口下载程序,而当通过跳线帽接地时,就运行用户代码。
=======================附注:代码讲解===================
先从main函数讲起来:
+++code
int main()
{
    //初始化LED
    led_init();
    while(1)
    {
        //点亮LED
        led_on();
        //延时一段时间
        delay();
        //熄灭LED
        led_off();
        //延时一段时间
        delay();
    }
}
---code
这个很简单~
接下来是delay函数:
+++code
void delay()
{
    unsigned int i=0xFFFFF;
    while(i--);
}
---code
这个就更加简单了,那个0xFFFFF也是随便写的一个数字。。。;
然后是led_on和led_off两个函数:
+++code
void led_on()
{
    GPIO_SetBits(LED_PORT,LED_PIN);
}

void led_off()
{
    GPIO_ResetBits(LED_PORT,LED_PIN);
}
---code
GPIO_SetBits(port,pin)是STM32固件库里面的函数,作用是把指定端口的指定引脚置为1。GPIO_ResetBits(port,pin)也是STM32固件库里面的函数,作用是把指定端口的指定引脚置为0。port的取值可以是GPIOA、GPIOB、GPIOC等等,取决于具体型号的MCU所拥有的端口。而pin的意思就是某个端口的第几个引脚,一个端口最多有16个引脚。pin的取值可以是GPIO_Pin_0、GPIO_Pin_1、GPIO_Pin_2….GPIO_Pin_15,取决于具体的端口。
代码中定义
+++code
#define LED_PORT GPIOA
#define LED_PIN GPIO_Pin_4
---code
也就是说,led_on就是把PA4这个引脚置为1,led_off就是把PA4这个引脚置为0。为什么是这样呢?看电路图:
32.jpg
明白了吧。
最后是最复杂的函数led_init:
+++code
#define LED_GPIO_CLK RCC_AHBPeriph_GPIOA
#define LED_PORT GPIOA
#define LED_PIN GPIO_Pin_4

void led_init()
{
    GPIO_InitTypeDef t_gpio;
    //开启GPIOA的驱动电路的时钟
    RCC_AHBPeriphClockCmd(LED_GPIO_CLK,ENABLE);
    //要设置的GPIO是GPIOA
    t_gpio.GPIO_Pin=LED_PIN;
    //GPIO的最大变换速度是2MHz
    t_gpio.GPIO_Speed=GPIO_Speed_2MHz;
    //GPIO的模式是输出
    t_gpio.GPIO_Mode=GPIO_Mode_OUT;
    //GPIO的输出方式是推挽输出
    t_gpio.GPIO_OType=GPIO_OType_PP;
    //以上述配置GPIO
    GPIO_Init(LED_PORT,&t_gpio);
}
---code
首先要明白一个道理就是,所有的寄存器在读写时都是需要时钟驱动的。GPIO的操作本质上就是读写GPIO对应的寄存器,所以也是需要有时钟驱动的。这就是为什么有
+++code
RCC_AHBPeriphClockCmd(LED_GPIO_CLK,ENABLE);
---code
这么一行代码的原因,它的作用就是开启GPIOA对应的时钟驱动电路。你可能会奇怪,为什么51单片机就不需要在使用GPIO之前开启时钟呢?这是因为51单片机默认所有寄存器都使用同一个时钟信号,而且一上电就全部开启了。这样做的后果就是51单片机的很多寄存器即使不使用,也会耗电,所以功耗降不下来。而STM32为了让用户更好地掌握功耗,对每个外设的时钟都设置了开关,让用户可以精确地控制,关闭不需要的设备,达到节省供电的目的。
为什么是RCC_AHBPeriphClockCmd这个函数,而不是RCC_APB1PeriphClockCmd或者RCC_APB2PeriphClockCmd呢,这个要看内部结构图(截取一部分):
33.jpg
GPIO是由AHB总线控制的。
开启时钟之后,就可以配置GPIO了。这里又有两个知识点:
(1)IO翻转速度。当STM32的GPIO端口设置为输出模式时,有三种速度可以选择:2MHz、10MHz和50MHz,这个速度是指I/O口驱动电路的响应速度,是用来选择不同的输出驱动模块,达到最佳的噪声控制和降低功耗的目的。高频的驱动电路,噪声也高,当你不需要高的输出频率时,请选用低频驱动电路,这样非常有利于提高系统的EMI性能。当然如果你要输出较高频率的信号,但却选用了较低频率的驱动模块,你很可能会得到失真的输出信号。
(2)输出方式。普通的输出方式有两种,即推挽输出(GPIO_Mode_Out_PP)和开漏输出(GPIO_Mode_Out_OD)。在推挽输出模式下,当把引脚置为1,那么引脚就如同直接与电源连接,输出高电平,当把引脚置为0,那么引脚就如同直接与地连接,输出低电平。在开漏输出模式下,当把引脚置为1,那么引脚就悬空,呈高阻态,当把引脚置为0,那么引脚就如同直接与地连接,输出低电平。
现在应该能理解led_init的作用了吧。