亿加合和智能车制作

标题: 【跟我学OSKinetis】第3课-开始干正事吧!GPIO! [打印本页]

作者: 洋葱圈    时间: 2013-10-15 16:30
标题: 【跟我学OSKinetis】第3课-开始干正事吧!GPIO!
本帖最后由 洋葱圈 于 2013-10-19 19:20 编辑

似乎学习一款单片机如何使用,首先学习它的IO引脚使用已经成了大家的共识。因为使用IO引脚来控制外围器件比如LED等亮灭是检测单片机是否正常运行的最简单标准。那么作为OSKinetis的编程实战,我们也首先从K60的GPIO模块开刀吧。

接下来的课程我们都将针对OSKinetis编程使用方法来讲,换个方式说就是教大家如何通过固件库来使用K60的各个模块,重点并非Kinetis单片机模块的讲解。当然为了使大家快速掌握模块的编程方法,我们会简单介绍该模块的原理等信息。如果读者想要深入学习Kinetis K60单片机的各个模块,可以阅读我们出的书籍《Cortex-M4自学笔记-基于Kinetis K60》


GPIO模块讲解

说起GPIO,其实就是通用输入输出的英文缩写(General purpose input/output),模块本身没有什么复杂的功能,就是字面意思——输入输出功能。只要在程序中设定某个引脚为逻辑1,那么相应的引脚就会变成高电平,反之为低电平。但是针对于Kinetis单片机来说,他的GPIO要比51单片机来说发杂了一点,这一点体现在寄存器上。


相信大部分朋友都接触过51单片机,这个单片机你在程序中置某一引脚为1,那么他就直接输出高电平,要读取某一引脚的逻辑电平状态,只要先置1再读取就可以了,简单方便。但是对于Kinetis来说,他的GPIO包含了比较多的寄存器,含有:端口数据寄存器端口置逻辑1寄存器端口清逻辑1寄存器端口状态翻转寄存器端口数据输入寄存器以及最重要的端口数据方向寄存器。使用Kinetis的GPIO引脚读取或输出数据之前,你要先配置他是输入还是输出功能,这就涉及到端口数据方向寄存器,接下来的输出还是输入操作,只要使用剩下的寄存器就可以了。


读者可以详细参考GPIO模块的技术文档,例如RUSH K60开发板采用的是K60DN系列的100MHz 144引脚的单片机,你需要参考文档K60P144M100SF2RM.pdf的“Chapter 54 General purpose input/output (GPIO)”一节,你会发现这节是所有章节中最简单的章节之一。


但实际上,Kinetis在物理上的每个引脚,大多是复用的,也就是说这个引脚既有GPIO,模块的输入输出功能也可能是ADC模块的模拟电压输入引脚、也可能是FTM模块的输入输出引脚。那么K60是如何区分不同引脚的复用功能是什么呢,下面一节我们会讲解。


Kinetis的引脚复用

还是要扯到51单片机,为什么51单片机(基础型号)没有引脚复用这个概念呢,说白了就是它的功能太简单了,只有GPIO功能……。但是Kinetis系列单片机的引脚大部分都是功能复用的,那么我们怎么控制他选择什么功能,答案就是PORT模块。


PORT字面上市端口的意思,然而在参考手册中全程是端口控制和中断模块(Port control and interrupts),还是字面上的意思很容易理解,PORT模块负责端口的复用和其他控制、以及端口的外部中断功能。PORT模块主要用到的寄存器是它的引脚控制寄存器以及中断状态标志寄存器引脚控制寄存器主要负责引脚复用功能的选择、中断\DMA触发模式的配置、内部上下拉、是否为开漏等功能。中断状态标志寄存器主要用来判断到底是哪个引脚产生了外部中断。


说了半天,不知道大家是不是已经把GPIO和PORT这两个概念给混淆了呢?因为他们都含有端口、引脚、输入输出等含义。但是区别在于,GPIO仅代表普通IO口模块,他负责输出或输入逻辑电平;而PORT是掌管单片机所有外部引脚功能和配置的模块。举个简单的例子,单片机的所有功能引脚相当于一个公司中的所有职工GPIO相当于某一个部门比如采购部吧,PORT相当于公司的人事部,那么人事部(PORT)就要运行自己的权利(功能配置),把不同的职工(功能引脚)划分到不同的部门(比如GPIO、ADC等)。


GPIO固件库编程思路

前面提及的所有概念都是单片机相关的,但也仅仅是概念,我们并没有说的特别细,因为那些在参考手册里已经写的很详细了。下面我们将讲讲如何通过OSKinetis V3固件库来使用GPIO模块了。


前面我们知道了GPIO具有输入、输出逻辑电平的功能,配合PORT模块的使用,还可以作为外部中断使用。所以根据功能来推理编程思路,就十分简单了:

输入功能:初始化GPIO相关引脚为输入—>读取引脚电平状态

输出功能:初始化GPIO相关引脚为输出—>输出电平

外部中断功能:初始化GPIO相关引脚为输入并配置中断—>中断函数


具体到固件库的编程上,顺序是一样的,首先我们要调用GPIO模块的初始化库函数,对GPIO进行初始化,然后就可以根据不同功能进行使用了。


GPIO例程讲解

下面我们通过两个例程来理解GPIO固件库的编程方法,分别是例程包中的“02-(GPIO)LPLD_LedLight”和“03-(GPIOint)LPLD_ButtonPress”工程。


LED流水灯-LPLD_LedLight

该例程中通过CARD宏定义区分了K60 Card和K60 Nano两款K60核心板,这里我们用默认的K60 Card核心板做讲解。

程序首先调用函数init_gpio()对GPIO模块进行初始化,其实现代码如下:


01
GPIO_InitTypeDef gpio_init_struct;
02
void init_gpio()
03
{
04
  gpio_init_struct.GPIO_PTx = PTA;
05
  gpio_init_struct.GPIO_Pins = GPIO_Pin4|GPIO_Pin6|GPIO_Pin8|GPIO_Pin10;
06
  gpio_init_struct.GPIO_Dir = DIR_OUTPUT;
07
  gpio_init_struct.GPIO_Output = OUTPUT_H;
08
  gpio_init_struct.GPIO_PinControl = IRQC_DIS;
09
  LPLD_GPIO_Init(gpio_init_struct);
10
}

Line 1:声明一个GPIO初始化结构体变量,用于配置GPIO的各项参数。该结构体内部含有多个成员变量,你完全可以通过浏览这些成员变量的注释来理解GPIO都有哪些功能。
Line 4~8:行开始配置这个结构体变量的各个成员变量。我们之所以推荐各位使用IAR 6.4或以上的版本,就是因为新版的IAR加入了对成员变量自动补全的功能,非常适合快速编写代码,例如你先打了“gpio_init_struct”这个结构体,再打一个点“.”后,开发环境会列出所有该结构体的成员变量,你只要选择其一即可。又由于我们对成员变量书写的格式非常规范,你只要光看名字就基本知道这个变量是干什么用的了,在下面的开发中你会充分体现到这一点。
Line 4:配置结构体的GPIO_PTx变量,选择使用PTD组的GPIO引脚。
Line 5:配置结构体的GPIO_Pins变量,选择使用编号为D8~D15的引脚。
Line 6:配置结构体的GPIO_Dir变量,设置PTD的相关引脚方向为输出。
Line 7:配置结构体的GPIO_Output变量,设置PTD的相关引脚初始化输出为高电平。
Line 8:配置结构体的GPIO_PinControl变量,配置端口的控制模式为禁止中断。
Line 9:调用GPIO初始化的库函数,并将初始化结构体变量传入其中,完成初始化。


注意,这里我们定义的GPIO初始化结构体为全局变量,全局结构体的成员变量在你不初始化它的时候,它默认是0的,因此有些成员变量我们不用初始化,使用默认值就行。但是如果你的结构体变量为局部变量,即定义在函数内部的变量,那么它的所有成员变量的默认值是随机的,因此需要对所有成员变量进行赋值,如果是可以不必须初始化的变量,那么赋NULL就可以了。如果不对局部结构体变量的全部成员变量赋值,那么会导致意想不到的结果。

对GPIO初始化完毕后,PTD的第8~15引脚就已经输出高电平,此时K60 Card的8个LED灯的负极连接的是这8个引脚,因此所有LED均为熄灭状态。


接下来请看main函数中while循环内的程序代码:


01
i=8;
02
while(1)
03
{
04
  //D1至D8依次触发点亮、熄灭
05
  LPLD_GPIO_Toggle_b(PTD, i);
06
  i+=1;
07
  if(i==16)
08
    i=8;
09
  delay();
10
}

Line 1:在进入while之前,先对变量i赋初值8,变量i负责记录8个PTD的引脚哪个需要进行电平翻转。
Line 5:调用LPLD_GPIO_Toggle_b()单个引脚翻转库函数对标号为i的PTD引脚进行电平翻转操作。如果是第1次运行到这里,那么i=8,即对PTD8引脚的电平做逻辑翻转操作。之前对所有已经的初始化逻辑电平为高,那么运行之后PTD8的引脚就变为了低电平,此时链接PTD8的LED灯就被点亮了。
Line 6~8:对i进行自加操作,目的是依次翻转8~15号的引脚,当i加到16时调回到8。
Line 9:延时一小段时间。


通过这个例程,相信大家都对GPIO的库函数使用有了初步的认识,至此我们没有涉及任何寄存器的编写。今后也希望大家形成一个良好的学习模式,就是通过学习理不同解模块的功能和原理,来使用固件库进行编程。这就相当于只要学会怎样开车,而无需学会怎样造车一样。怎么样,是不是相当easy呢!当然没有一个库或者函数是万能,但是希望大家能够以学会使用现在的库为基础,将来自己写出更好的库函数为目标!


(帖子太长,一个楼发布下,下面还有2层)



作者: 洋葱圈    时间: 2013-10-15 16:31


轮训和中断方式判断按键-LPLD_ButtonPress

该例程同样是GPIO的基础例程之一,这里我们实现了通过GPIO的引脚读取或者引脚外部中断功能判断按键的按下操作。

本例程通过一个宏定义来设置是使用轮训方式还是中断方式,如下代码所示:


1
//若使用中断方式则为1,若使查询方式则为0
2
#define INT 0


轮询方式

首先我们定义INT为0,即采用轮询方式读取IO口的电平状态来判断按键是否按下。接下来请看GPIO初始化函数内部的编写:


1
// 配置 PTB7、PTB6 为GPIO功能,输入,内部上拉,不产生中断
2
gpio_init_struct.GPIO_PTx = PTB; //PORTB
3
gpio_init_struct.GPIO_Pins = GPIO_Pin6|GPIO_Pin7; //引脚6、7
4
gpio_init_struct.GPIO_Dir = DIR_INPUT; //输入
5
gpio_init_struct.GPIO_PinControl = INPUT_PULL_UP|IRQC_DIS; //内部上拉|不产生中断
6
LPLD_GPIO_Init(gpio_init_struct);

上述例程讲过的部分我们就不再赘述了,我们来说说这次的初始化中和上次不同的配置和写法。
Line 3:初始化PTB组端口的端口号,这里初始化PTB6和PTB7引脚,写法为GPIO_Pin6与GPIO_Pin7做“或”操作,效果是同时初始化两个引脚为一样的功能。不管是GPIO_Pin6还是GPIO_Pin8_15,都代表了对应端口号的掩码,因此是可以通过“或”操作来组合不同的端口号的。
Line 4:初始化传输方向为输入。
Line 5:配置结构体的GPIO_PinControl变量,这次和上次又有点不一样,多“或”了一个INPUT_PULL_UP宏定义,还是字面意思,配置这两个引脚为输入上拉,且不使能中断。虽然在配置参数时使用了“或”操作符,但是实际的结果是输入上拉“”禁用中断。更多的配置定义可以直接参见该结构体变量的注释。

初始化完毕后看while循环内的代码:


01
if(PTB6_I == 0)
02
{
03
  //去抖
04
  delay();
05
  if(PTB6_I==0)
06
  {
07
    printf("Button1-PTB6 Pressed!\r\n");
08
  }
09
  //直到按键松开再运行
10
  while(PTB6_I==0);
11
}

Line 1:读取PTB6的逻辑电平并判断是否等于低电平,这里使用了位带定义操作,使之看起来就像使用51单片机的IO引脚一样简单。由于Kinetis是32位单片机,因此的GPIO数据寄存器一般是32位的,每一位对应一个GPIO引脚,如果我们直接读这个32位的数据寄存器,还需要进行移位或者相与操作才能判断某一位的电平。但是位带操作可以使我们直接读取某一位的数值,它的原理是在单片机的寻址空间内有一段地址,每个字都映射到了GPIO的32位数据寄存器的某一位上,因此我们只需要读取该字节的地址,就可以获取某一位的电平状态了,如此简单的解释可能你会理解不了,我们会单独拿出时间来给大家讲这一块。输出操作的话亦是如此。
Line 4:因为初始化时PTB6为内部上拉,因此当没有按键按下时,PTB6是高电平,如果为低电平则先延时一小会,以达到去除抖动的效果,这里的去抖操作原理比较简单,目的是以演示GPIO操作为主。
Line 5:延时去抖后,再次判断PTB6的电平状态,如果还是低电平,则判断为有效按键操作。
Line 7:打印相关信息,告诉我按键已经按下了。
Line 10:这句比较关键,判断按键是否已经松开,如果没有松开,那么PTB6的电平状态依然是低的,因此就一直在此处循环。如果已经松开,那么PTB6为高电平,while判断为假,则继续运行下面的程序。

另一个按键是由PTB7判断的,方法一样,不再赘述。


中断方式

在例程中定义INT为1,该工程就变为了中断方式操作按键了。还是看init_gpio()函数内部的初始化代码:


1
// 配置 PTB7、PTB6 为GPIO功能,输入,内部上拉,上升沿产生中断
2
gpio_init_struct.GPIO_PTx = PTB; //PORTB
3
gpio_init_struct.GPIO_Pins = GPIO_Pin6|GPIO_Pin7; //引脚6、7
4
gpio_init_struct.GPIO_Dir = DIR_INPUT; //输入
5
gpio_init_struct.GPIO_PinControl = INPUT_PULL_UP|IRQC_FA; //内部上拉|上升沿中断
6
gpio_init_struct.GPIO_Isr = portb_isr; //中断函数
7
LPLD_GPIO_Init(gpio_init_struct);
8
//使能中断
9
LPLD_GPIO_EnableIrq(gpio_init_struct);


Line 5:与轮询方式不同,这里的引脚控制参数由引用中断IRQC_DIS变为了IRQC_FA——上升沿触发外部中断。此时PTB6和PTB7引脚就被配置为了可触发外部中断的输入引脚了。
Line 6:配置成员变量GPIO_Isr,该变量用于存储中断函数的地址指针,简单理解就是将中断的函数名赋值给该变量。这个中断函数的编写方法和普通函数一样,你只需要定义一个无返回值、无输入参数的函数即可作为中断函数。
Line 7:依旧是调用初始化函数,传入初始化结构体变量。
Line 8:由于我们使用了外部中断,因此要想中断被触发,就必须调用GPIO的使能中断函数。有同学会问,我们不是已经在GPIO_PinControl变量中配置中断了吗,为什么还要使能它呢。其实GPIO初始化函数只会只能引脚的中断功能,但是Kinetis单片机内部还有一个叫NVIC的东东,它是可嵌套中断向量控制器的英文缩写。它相当于所有中断的总闸,不仅每个模块内部要使能中断,我们还要在NVIC中使能这个中断,中断才能在有请求的时候被触发。

接下来看看portb_isr()中断函数内部是如何写的:


01
//如果PTB6产生中断
02
if(LPLD_GPIO_IsPinxExt(PORTB, GPIO_Pin6))
03
{
04
  //去抖
05
  delay();
06
  if(PTB6_I==0)
07
  {
08
    printf("Button1-PTB6 Interrupt!\r\n");
09
  }
10
}

首先大家要知道,每组GPIO端口的外部中断向量号只有1个,因此同组端口不同号的引脚会进入同一个中断。但是大家都挤进一个中断,我们如何来区分是哪个引脚触发的中断呢?
Line 2:首先调用宏定义的函数LPLD_GPIO_IsPinxExt()来区分到底是哪个引脚触发的中断,该宏定义通过对比PORTB的中断状态标志寄存器的相关掩码位来判断该位是否置1,从而判断出是否对应的引脚产生了中断。这里的参数为什么不是PTB而是PORTB呢,前面我们说过了,PORT模块是控制引脚的中断的,因此这里的中断状态标志寄存器也是PORT的寄存器,因此要传入的是PORTx这个参数,而PTB则代表的是GPIOB模块。

接下来的代码和轮询方法差不多了,但是没有了等待按键松开的while循环体。因为只有当按键按下的时候,PTB6产生下降沿时才会进入这个中断函数,如果按键一直处于按下状态,那么PTB6引脚并不会一直进入这个中断函数。




作者: 洋葱圈    时间: 2013-10-15 16:31
本帖最后由 洋葱圈 于 2013-10-17 20:22 编辑


GPIO拾遗补缺关于代码阅读

通过上述的两个简单例程,我们已经基本搞定了GPIO的常用操作。但是要想深入的去玩儿,还需要大家去看每个函数、变量的注释。拿初始化结构体GPIO_InitTypeDef来说,你可以在固件库的代码中仔细阅读该结构体的每个成员变量的注释,我们在编写固件库的同时会把注释详细的写下来,怎么看这个注释呢,请看具体代码:


01
/*
02
    描述:
03
      选择GPIO的输入输出方向
04
    取值:
05
      输入-DIR_INPUT
06
      输出-DIR_OUTPUT
07
    初始化:
08
      必须初始化
09
  */
10
  uint8 GPIO_Dir;

这是GPIO_InitTypeDef结构体的成员变量GPIO_Dir的注释及声明,注释首先写了该变量的描述,你可以基本了解这个变量是用作定义GPIO数据方向的;接下来写了该变量的取值范围,变量的取值和描述会通过“-”一一对应,有时候描述在前,有时候描述在后,很好辨别;最后写的是初始化的值,这里会提示你这个变量是否为必须初始化,如果非必须初始化,我们会告诉你它的默认值是多少。不知道仔细阅读的各位还是否记得前面说的,如果你将结构体变量定义为全局变量,那么非必须初始化的变量体就不必赋值,如果你将结构体变量定义为局部变量,那么非必须初始化的变量如果想用默认值,就赋值为NULL即可。

在成百上千行的代码中,我们应该如何快速找到某个变量或函数的定义呢,IAR环境有个很好用的索引功能,你只需要在代码中的变量名或者函数名上右键,点击“Go to Definition of” 变量/函数”即可快速跳转到该变量/函数的定义处。如果该方法不行,你还可以用IAR的全局搜索功能,点击Edit->Find and Replace->Find in Files…即可在整个工程中搜索变量/函数的名字。


关于引脚功能的配置

我们在上述例程中仅仅将引脚配置过输入上拉、上升沿中断、禁用中断等模式,其实引脚可以配置的功能有很多,有:上拉下拉、压摆率、是否滤波、是否为输出开漏、是否增强驱动力、中断/DMA类型等。这些功能的参数之间都可以用“或”操作符“|”来组合赋值给变量GPIO_PinControl,而这些参数的宏定义你完全可以通过程序注释或者在线库函数手册来获得。

例如我们需要用一个引脚输出5V电平来控制舵机,但是Kinetis芯片只能输出3.3V电平,那么我们就可以用引脚的开漏输出模式,在引脚外部上拉一个电阻到5V即可,因为开漏模式表示当引脚输出低电平时,它内部直接接地,而要输出高电平时,它内部放开与引脚的连接,外部输出就直接被拉高到5V了。代码如下:


1
gpio_init_struct.GPIO_PinControl = OUTPUT_OD_EN; //使能开漏模式


关于单个引脚的输入输出操作

前面我们提到了位带操作可以像操作51的IO口一样操作Kinetis的引脚,在例程中我们只用到了输入操作,还有方向设置、输出操作。请见下面例程:


1
DDRA0 = 1;   //设置PortA0口为输出
2
DDRC19 = 0;  //设置PortC19口为输入
3

4
PTA0_O = 1;   //设置PortA0输出高电平
5
PTA1_O = 0;   //设置PortA1输出低电平
6

7
uint8 data;
8
data = PTA0_I;   //读取PortA0口的电平

关于其他GPIO库函数

在固件库中还有许多其他函数,可以让你设置、读取、翻转某1位、8位、32位的引脚电平,具体请参考在线版本的函数手册。

GPIO的在线函数手册:http://wiki.lpld.cn/index.php?title=HW_GPIO


小节

这节课是我们头一次“正经”地玩儿固件库,虽然GPIO模块很简单,但是要说的还真不少,其实讲得再多,也不如大家去仔细读读代码、看看注释来的快。一旦你熟悉了如何阅读固件库的代码,会比看这个教程学得更多更快。当然,如果你觉得固件库这点儿东西你已经玩烂了,就去直接看K60的参考文档(K60P144M100SF2RM.pdf)吧,那里的东西更多,能学到的也更多。

不知道大家还有什么疑问,我们的教程文字较多,很少用颜色和图片,因为我们相信能仔细读到这里的同学一定学到了东西,而过多的彩色渲染+图片更像是幼儿读物。文字中已经将重要的信息做了简单标注。



作者: chaijb2008    时间: 2013-10-15 17:12
支持!!!
作者: 邵志伟    时间: 2013-10-15 17:26
支持支持!!!
作者: 925901184    时间: 2013-10-15 20:10
学习学习
作者: loveme06    时间: 2013-10-15 20:15
受教,,
作者: kanwoe    时间: 2013-10-15 21:54
占个座!!
作者: 假精哟    时间: 2013-10-29 18:43
挺好的,支持
作者: liumingyang    时间: 2013-11-23 20:18
写得相当好
作者: 蒙着面会很强    时间: 2015-1-24 09:26
俺是做硬件的...看了这个对引脚很有感觉了
作者: 小泉子    时间: 2015-12-19 15:51
很受用 谢楼主

作者: 王锋MX    时间: 2016-3-19 00:42
:):):):):)
作者: 王锋MX    时间: 2016-3-19 10:50
真心感谢
作者: zmhzc111234    时间: 2017-1-13 20:22
收获了好多
作者: 学习学习1    时间: 2018-5-23 14:38
谢谢楼主!




欢迎光临 亿加合和智能车制作 (http://111.231.132.190/) Powered by Discuz! X3.2