用STM32点亮跑马灯(库函数)啰嗦版
扫描二维码
随时随地手机看文章
用STM32的GPIO来点亮跑马灯
步骤:
1.新建工程:复制模板中的一切文件夹,注意删除USER中模板Template产生的三个文件以及OBJ中的文件(OBJ中产生的是编译过程中产生的文件)。
2.打开keil,对工程中的各县设置进行修改(包括:右键Manage Project Items里面的各个组及头文件;魔术棒中的Output选项卡中HEX文件和Folder的修改;C/C++选项卡中Define和头文件路径的修改)
3.build工程,查看设置是否正确,注意此时各个文件前面有可能没有小加号,解决办法是Translate一下,要是还不行就关了重开。
至此新建工程完成
新建工程需注意的事项:
(1)在不完全手册中有着详细的新建步骤,要仔细的按照步骤来,
(2)Define:STM32F10X_HD,USE_STDPERIPH_DRIVER,
(3)FIWLIB添加文件的时候添加的是src中的文件,但在添加路径的时候是inc文件夹,并且注意一定要相应的添加,
(4)Translate和build的区别:Translate的时候只是检查语法的错误,并不产生HEX文件,
(5)出现无法gotodefinition的情况的时候是没有build工程,build一下就可以了,
想要对外设进行驱动就要相应的编写函数,我们在这里只将具体要做什么事(如亮多久灭多久,一起亮还是轮流亮)放于main文件中,每个外设的初始化函数我们都放在HARDWARE文件夹下面相应的.c和.h文件中(初始化函数是要根据外设连接到了哪个端口来编写的,内容包括用了哪个端口,处于什么工作模式下等),在这里使用到了这两个文件,不要忘记添加头文件和相应的路径。
1.对.h文件的编写
(1).h文件一般用于存放函数的声明和宏定义等
(2)使用到的函数:ifndef,define,endif
#ifndef __XXXX_H
#define __XXXX_H
A
#else
B
#endif
这是一个宏定义,防止重复定义:如果没有定义(if not define,ifndef)过xxxx.h,那么定义(define)xxxx.h并执行A,如果定义过了就执行B,通常我们在用的时候不会用到else,那么也就直接跳过了这段代码(endif),其中的这些下划线和H是c语言的书写规范,并且都是用大写来写,如led.h就会写成_LED_H。
关于这个函数的用法和重复定义的问题可以参考:http://blog.sina.com.cn/s/blog_aef927a50102wg9u.html
(3)举例:
#ifndef __LED_H//if not define led.h
#define __LED_H//define led.h,并执行下面的程序
void LED_Init(void);
#endif
注意:LED_Init是空函数,在末尾要加上分号,没有分号会报错。
说明:这里看到一个解释,说.h头文件并不是直接对应执行的,而是给编译器看的,在.h文件中的代码在实际编译的时候会直接替换掉.c中的#include "xxx.h"。如果没有这段代码,那么遇到main.c中包含了许多头文件,而这些头文件中又不止一次包含了这个xxx.h,在编译的时候就会出现重复定义的报错,也会浪费时间,而有了这段代码就可以保证了它只被定义一遍,因为不符合not define,所以直接跳过。
2.对.c文件的编写
在本实验中,.c文件里面存放了led的初始化函数LED_Init,
LED_Init包括:
(1)include相应的.h文件
(2)对GPIO时钟的初始化:GPIO用的是时钟2,函数为:RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOx,ENABLE);
(3)对GPIO端口的初始化:
GPIO_InitTypeDefGPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=
GPIO_InitStructure.GPIO_Pin=
GPIO_InitStructure.GPIO_Speed=
GPIO_Init(GPIOx,&GPIO_InitStructure);
GPIO_SetBits(GPIOx,GPIO_Pin_x);
注意这里一定是要先设置初始值再进行函数的调用。
下面来解释一下这些语句:
1.GPIO_Init(GPIOx,&GPIO_InitStructure);
这条语句为初始化语句,输入GPIO_Init后,可以goto去查看它的definition,可以看到是这个样子的:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct),也就是说我们在.c文件中调用的是这个函数。通过查看这个函数我们就可以知道这个函数的格式和需要设置的参数都有哪些,可以看出,想要调用这个函数,需要传递过来两个参数,一个是具体哪个管脚,一个是要被初始化的变量们组成的结构体。具体是哪个管脚,取决于硬件电路的连接。
这两句话可以类比于
hanshu(a,&b);//调用函数hanshu
void hanshu(int *p,char *q);//定义一个名叫hanshu的函数,有两个指针型变量,分别指向int和char型变量
GPIO_TypeDef* GPIOx:这句话表示一个结构体变量的地址,GPIO_TypeDef是类型,相当于int,float之类的,*GPIOx的意思是定义一个指针名叫GPIOx,整体这句话的意思就是定义一个名叫GPIOx的指针,这个指针指向GPIO_TypeDef类型的东西
在这里,要讲一下指针的问题:
指针是能够存放一个地址的一组存储单元(通常为2个或4个字节),这就像机器的一个字节可以存放一个char类型的数据,四个字节就可以存放一个long型的数据,当然了,四个字节我们就可以叫它单元了,存放着地址的存储单元,我们叫它指针,它也是一个变量,只是其他变量表示的是一个数或者字母或者其他什么东西,而它表示的是地址,表示的是它所指向的东西的所在地址。这样就可以理解指针到底是个什么东西了。
如果在A处存放了一个char类型的数据c(当然也可以是别的,但char只是一个字节的,用来举例比较好理解),若p是指向c的指针,那么,p中存放的就是这个c所在的地址,也就是A,根据学过的微机接口课程的内容我们可以知道,这个A实际上应该是一个类似于0xFFH之类的十六进制的东西,我们在这里也没有必要知道具体是什么,因为这个地址A是CPU自己分配的,我们想要进行操作,那么就直接操作指针p就可以了,因为p中存放的就是A呀。现在这个A是我这里用来理解而加入的字母,在程序中是不存在这个A的,我们想要将一个东西指向另一个东西,要怎么写呢?需要啷个运算符,一个是地址运算符&,一个是间接寻址或间接引用运算符*。
p=&c,这样p就指向c了。&可以取出内存中的对象的地址,即变量或数组元素的地址(也就是说在这样用的时候这个c只能够代表变量或者数组元素,如果这个c是一个常量或者是一个表达式就不行了),&c就表示c所在的地址,也就是我之前假设的A,这个A也就相当于&c。
*运算符用于指针的定义和寻找指针指向的东西,&运算符用于对变量的取址。
举一个例子用来理解*和&
int x=1,y=2,z[10];
int *p;//定义一个指向int类型的东西的指针,它的名字叫p
p=&x;//现在指针p指向x,p指向x也就是p表示x所在的位置,现在p中应该是一个地址
y=*p;//现在,y=1。*p表示取p中的内容所代表的地址中的内容,也就是去找p指向的东西,也就是x。
*p=0;//现在x=0,可以看出,指针是能够改变它本来的东西的值的,与值的传递有差别,这一点稍后详细解释
p=&z[0];//现在指针p又转而指向z[0]
这里简要说一下为什么要用这个指针,在这个例子中当然看不出它的优势,但是一旦出现对一个数组的每一个元素依次进行计算或者是x中存放的内容将会进行改变的情况,指针的方便就显现出来了。还有需要依次存储的时候,可以用数组下标来完成,也可以每次给地址+1,也就是用指针(这两者的区别是用指针的时候程序执行的速度更快,具体原理就不深入探究了,应该就是微机接口那一套东西,指令时间指令长度之类的)。还有一种情况必须要用指针就是你想要对一个数组进行计算,但需要调用子函数计算,因此就需要将这个数组传给调用函数,在传递的时候,c语言语法规定的是数组的名字代表这个数组第一个元素的地址,因此在调用子函数的时候传递的是这个数组的第一个元素的地址(参见c语言程序设计84页),这个时候你在编写子函数的时候,局部变量的变量类型必须设置成指针,因为存放的是地址(参见85-86页的例子)。其实到底什么时候用指针这个问题本人体会并不是非常深刻,所以就先写到这里,如果以后遇到再添加进来。指针给我的感觉就像是打包装箱一样,这样可以不用考虑长度啊,类型啊之类的问题,用起来还是比较方便的,当然前提是能用明白。。。
看书的时候还有一种感觉,正常将一个变量的值赋给另一个变量,就像将本地磁盘E中的文件复制粘贴到桌面上,无论我怎么对桌面上这个新文件进行操作,存在E盘中的那个原文件都不会被改变,但指针不一样,指针就像是将E盘中的这个文件创建了桌面的快捷方式,你能够看到它的路径是E盘而不是c盘的桌面,在进行修改的时候也会直接影响到E盘中的那个原文件。忽然明白为什么说用指针会降低程序的可读性了,因为你要是想要用这个指针,你就要追根溯源的去找它到底指向的是个什么东西而不是像变量那样可以直接根据名字或者所赋的值来知道这个变量是个什么东西,而且也容易找着找着就蒙了。个人认为,当我们看到一个变量在使用的时候前面带*,那么它应该就是一个指针,我们就要去找这个指针指向的东西,找的时候如果看见这个指针=&一个什么东西,那它应该就是指向这个东西了。
GPIO_InitTypeDef* GPIO_InitStruct:定义一个名叫GPIO_InitStruc的指针,指向GPIO_InitTypeDef型的数据。
整体来看这句话,就是我要调用这个初始化函数GPIO_Init(GPIOx,&GPIO_InitStructure);它要求我传过去两个参数,一个是到底是哪个端口,是GPIOA还是B还是C还是什么,另一个是我要初始化的东西,由于初始化的参量比较多,于是就放在一个结构体里面了关于结构体,继续看下文吧。
2.GPIO_InitTypeDefGPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=
GPIO_InitStructure.GPIO_Pin=
GPIO_InitStructure.GPIO_Speed=
这几句话的意思:定义一个GPIO_InitTypeDef类型的结构,名字叫做GPIO_InitStructure(当然可以叫别的名字)这个结构的成员(结构中定义的变量叫做成员)包括三个:GPIO_Mode,GPIO_Pin和GPIO_Speed并进行了初始化。至于为什么是这三个,稍后会进行解释。
在这里我们只是定义了一个结构,并没有对里面的成员进行定义(里面的都是默认值,需要设置哪个再单独去改),下面三行语句是引用了成员并对其进行了修改。
这里对结构进行一下说明:
结构是一个或者多个变量的集合,之前我们在用MSP430编程的时候,就体会过传递参数的时候一下子传过去好多个,要加好多注释,在matlab编程的时候也出现过这种状况,在这里,采用了结构(当然其他语言也采用了,之前没用过结构体也是因为我不会用,哦呵呵)。对于结构成员的引用的格式有两个:
(1)结构名.成员,也就是后三行的引用方式。
(2)p->结构成员,这个要求左边的这个结构必须为指针。若p是一个指向结构的指针,那么可以用这种方式来引用相应的结构成员,在库函数头文件中采用的就是这种方式(当我们gotodefinition的时候就可以发现),但是在我们的.c文件中不可以这么写,因为这里我们在进行定义的时候定义的是一个结构,是一个实体。
详细内容可以参照:http://www.cnblogs.com/winifred-tang94/p/5843440.html
(3)关于.和->的用法,有这样一个规律:点.要求左边的这个结构必须为实体,箭头->要求左边的这个结构必须为指针。
如果想要对一个结构进行操作,有三种方法:一是分别传递各个成员,二是传递整个结构,三是传递指向结构的指针。在这里就要再次提起刚刚讨论过的那个初始化函数了,它的第二个变量传递的就是结构体,并且采用的是第三种传递指针的方式。在结构中使用指针的使用方法和普通变量的指针类似。
这里引用正点原子不完全手册中的一个例子来解释一下结构体的优势:
在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如串口,它的初始化状态是由几个属性来决定的,比如串口号,波特率,极性,以及模式。对于这种情况,在我们没有 学习结构体的时候,我们一般的方法是:
void USART_Init(u8 usartx,u32 u32 BaudRate,u8 parity,u8 mode);
这种方式是有效的,同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里 面再传入一个参数,那么势必我们需要修改这个函数的定义,重新加入字长这个入口参数。于是我们的定义被修改为:
void USART_Init (u8 usartx,u32 BaudRate, u8 parity,u8 mode,u8 wordlength );
但是如果我们这个函数的入口参数是随着开发不断的增多,那么是不是我们就要不断的修改函 数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢? 这样如果我们使用到结构体就能解决这个问题了。我们可以在不改变入口参数的情况下, 只需要改变结构体的成员变量,就可以达到上面改变入口参数的目的。
结构体就是将多个变量组合为一个有机的整体。上面的函数,BaudRate,wordlength, Parity,mode,wordlength 这些参数,他们对于串口而言,是一个有机整体,都是来设置串口参 数的,所以我们可以将他们通过定义一个结构体来组合在一个。MDK 中是这样定义的:
typedef struct
{
uint32_t USART_BaudRate;
uint16_t USART_WordLength;
uint16_t USART_StopBits;
uint16_t USART_Parity;
uint16_t USART_Mode;
uint16_t USART_HardwareFlowControl;
} USART_InitTypeDef;
于是,我们在初始化串口的时候入口参数就可以是 USART_InitTypeDef 类型的变量或者指针变 量了,MDK 中是这样做的:
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需 要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义 就可以达到增加变量的目的。
在这里要说一下typedef函数,这是一个类型定义函数,可以用来建立新的数据类型名。用法为:
typedef旧的类型名新的类型名;
实际上这个函数并没有新出现一个什么新的东西,只是在原来的基础上命了新名字,作用类似于#define,但比define的功能更加强大,可以定义更多的东西。
举个例子,我们看这段程序的第一句话,也就是定义了一个GPIO_InitTypeDef类型的结构,我们这里并不知道这个GPIO_InitTypeDef类型是个什么类型,那么我们去gotodefinition,这样就可以看到,在stm32f10x_gpio.h文件中存在着typedef定义的一个结构,里面包含了三个成员,GPIO_Mode,GPIO_Pin和GPIO_Speed,这也就解释了为什么我们要对这几个进行赋值,对于其中的一个成员GPIO_Pin,我们可以看到,是一个uint16_t类型的数据,到这里我们仍然不知道这到底是个什么类型的数据,因为c语言中就只有int,char之类的类型,那么我们就再继续gotodefinition,就可以在stdint.h文件中看到这样一行语句:typedef unsigned shortint uint16_t;这句话我们可以类比typedef unsigned char uchar来看,用uchar代替unsigned char,这种方式在我们编写msp430的时候经常会用,只不过那时候用的是define。这样我们就明白了,GPIO_Pin是一个无符号短整型的数据,这和我们对430编程的时候的int x=0x00;中的x是一样的。
到这里我们已经剖开这个函数的层层外壳看到了最原始的定义,这也就和我们当时编写c51,msp430的时候自己定义的那些东西差不多了,而对于stm32来说,它的复杂程度已经远远超过了那两个单片机,所以stm公司自己在出厂的时候就把许多函数打包好了,经过层层包装来到我们面前的这个函数库,忽然觉得好震撼,这是一个多么庞大的工程啊,我们简直是站在巨人的肩膀上。(咳咳,跑题了。。。)
就举这一个例子,我实际操作又去看了一些其他的函数,发现后面带着TypeDef的东西都可以在gotodefinition的时候发现下一层函数,刚才举例的那个GPIO_Pin是属于包装层数少的了。
现在我们再回头来看这四行代码,疑问就可以解答了,整理如下:
为什么要定义一个结构呢?
GPIO_Init函数要求我们传递过去一个结构的地址,因此我们要在外面定义一个结构。
为什么是GPIO_Pin,GPIO_Speed和GPIO_Mode这三个变量呢?
因为在我们去到GPIO_Init函数里面的时候,看到它的第二个参量GPIO_InitTypeDef*GPIO_StructInit,这里定义了传过来的这个结构的类型,就是GPIO_InitTypeDef,我们再goto去看GPIO_InitTypeDef是什么类型的时候会发现它被定义成一个包含这三个成员的结构体,因此,我们在赋值的时候就要赋给这三个变量。
如果不赋值会怎样?
如果不赋值,那么这个结构体就默认为stm公司设置的初值,这个初值也可以看到,在GPIO_Init函数里面,查看它的第二个参量GPIO_InitTypeDef*GPIO_StructInit的GPIO_StructInit,可以看到它的初始值,pin_all,2MHz和IN_FLOTING。
那么究竟要给这三个成员赋怎样的初值呢?
这个的确定,我们需要用到一个工具,那就是技术手册或是什么别的手册,上面在讲到GPIO都会仔细的说明每种工作方式,工作速度的区别我们要用哪个模式和速度,是由我们具体要做什么东西来确定的,而具体使用哪个引脚则需要通过查找硬件电路图来确定。
选好想要的模式之后,具体的程序要怎么写呢,有什么格式的要求吗?
这就是对结构体成员的赋值问题了。这个我们可以参照刚刚看到的stm给的初值的形式,但要注意使用点(.)而不是箭头(->)。举个例子,如Speed,在我们回答第二个问题的时候,我们可以看到stm定义的结构体中的GPIO_Speed的类型为GPIOSpeed_TypeDef,当然我们不知道这是一个什么类型,我们gotodefinition去看一下,可以看到stm用typedef定义了一个enum(枚举类型)的东西,这里面包括三个:GPIO_Speed_10MHz, GPIO_Speed_2MHz,GPIO_Speed_50MHz,因此我们可以知道,这个speed只有这三个值可以赋,并且不能够写成20,50之类的任意格式,而必须是GPIO_Speed_10MHz,GPIO_Speed_2MHz,GPIO_Speed_50MHz其中的一个,假设说我们选择的是50MHz的速度,那么就要写成‘结构体名.GPIO_Speed_50MHz’的形式。对于工作模式的设置也是一样,goto之后就可以看到它也是一个枚举类型,里面包含八个成员。
解释到这里,这四句的意思大概已经清楚了。
还有一点要说的,c语言中规定所有的变量、结构体的声明(定义)都必须在代码段之前,就像我们之前define都要写在最上面一样,因此第一句的结构定义放在了最前面。
3.GPIO_SetBits(GPIOx,GPIO_Pin_x);
这句话比较好理解,设置LED默认不点亮,置1是不点亮,至于为什么,看电路图就明白了,LED的正连的高电平,负连的管脚。
对于程序的解析到此为止。
附加说明一个问题,我们是如何知道要用这些函数呢,比如GPIO_SetBits这个函数。其实和GPIO有关的函数有一个汇总,就在stm32f10x_gpio.h文件的最下面,有许多的函数,我们大致看一眼就可以知道那个函数是做什么用的,因为已经经过层层包装,名字已经很好辨认了,就像SetBits函数,很明显是置位用的。(这里总觉得不太对劲,应该还有什么别的方式来查找函数的)在这里我们也可以基本看到这个函数的格式,需要哪些参数,例如void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);可以看出来我们需要给它两个参数,一个是GPIOx,一个是pinx,如PA8就是GPIOA,GPIO_Pin_8。这里要说明的是,我们在使用原子家的mini板的时候,往往不会自己亲自动手从头再去编写初始化什么的东西,而是可以参考他给的例程,进行加工和修改。
我们刚刚编写.c和.h文件,性质上和stm32的开发者一样,都是在给程序打包装箱,只不过stm32给出的函数库更加的通用,而我们写的这两个文件比较有针对性。
3.main.c的编写
(1)include各个需要的文件
#include "stm32f10x.h"
#include "led.h"
#include "delay.h"
(2)编写main主函数:
一般情况下,main中包含两部分:
用到的各个部分的初始化,在本实验中包括led的初始化(也就是刚刚在led.c里面写的那个初始化函数),delay(延时)函数的初始化
delay_init();
led_Init();
while函数,基本上我们需要的函数都会放在while(1){ }里面,因为我们要让它一直执行,而不是执行一次就完事了,但是对于比较复杂的函数,往往不是这个样子的,会有许多的判断条件。
while(1)
{
GPIO_SetBits(GPIOA,GPIO_Pin_8);//设置LED默认不点亮
delay_ms(500);
GPIO_ResetBits(GPIOA,GPIO_Pin_8);//设置LED默认不点亮
delay_ms(500);
}
说明:本实验中,main函数的返回值类型是int型的,而不是void型的,void是空,是不提供返回值的意思
这里要写成int main (void){}
这里需要说明一下:c语言规定不接收任何参数也不返回任何信息的函数原型为“void foo(void);”。可能正是因为这个,所以很多人都误认为如果不需要程序返回值时可以把main函数定义成void main(void) 。然而这是错误的!c语言规定main 函数的返回值应该定义为 int 类型。虽然在一些编译器中,void main 可以通过编译(如我们用IAR写msp430的时候),但并非所有编译器都支持 void main(如我们现在用的keil),因为标准中从来没有定义过 void main 。
main 函数的返回值类型必须是 int ,这样返回值才能传递给程序的激活者(如操作系统),如果 main 函数的最后没有写 return 语句的话,C99 规定编译器要自动在生成的目标文件中(如 exe 文件)加入return 0; 。main函数的返回值用于说明程序的退出状态。如果返回0,则代表程序正常退出。返回其它数字的含义则由系统决定。通常,返回非零代表程序异常退出。
更加详细的解释可以参考:http://blog.csdn.net/piaojun_pj/article/details/5986516
PS:
1.go to Definition 的时候会出现以下这种情况,解决办法:在name下面任意双击其中一个就可以跳过去,但具体我们要找的是哪一个,需要自己去判断。