第5章通用输入/输出接口GPIO
本章学习目标
 了解GPIO功能及寄存器操作; 
 了解S5PV210芯片的GPIO控制器及其应用。
5.1GPIO硬件介绍
5.1.1GPIO概述

GPIO(General Purpose Input/Output,通用输入/输出接口),通俗点讲就是一些引脚,可以通过它们向外输出高低电平,或者读入引脚的状态,这里的状态也是通过高电平或低电平来反应,所以GPIO接口技术可以说是众多接口技术中最为简单的一种。
GPIO接口具有更低的功率损耗、布线简单、封装尺寸小、控制简单等优点,故其使用非常广泛,在嵌入式系统中占有很大的比重。GPIO接口通常至少有两个寄存器,即“通用I/O控制寄存器”和“通用I/O数据寄存器”,数据寄存器的各位直接引到芯片外部供外部设备使用,各位上对应的信号是输入、输出还是其他特殊功能,可以通过设置控制寄存器中的对应位独立地控制。除这两种基本的寄存器外,有时还有上拉寄存器,通过它可以设置I/O输出模式是高阻态的,还是带上拉电平输出的,或不带上拉电平输出的。在S5PV210中,还引入了驱动强度控制寄存器来调节输出电流的强度,此外,还有功耗控制寄存器用来设置相应引脚的功耗。这些额外增加的寄存器可以使电路设计变得简单,在信号控制上也方便很多。
5.1.2S5PV210的GPIO寄存器
S5PV210的GPIO寄存器在数量和功能上比之前的S3C2440增加了许多,有些PIN脚不能作为通常的输入/输出引脚来用,比如不能直接用作OneNAND控制信号和数据信号、I2S接口等。另外,GPIO接口组寄存器由4位来控制,扩展了GPIO引脚的功能。所以S5PV210的GPIO已不仅仅只有GPIO的功能,同时向后也是兼容的。本章只讨论常用的GPIO相关的知识,对于其他的功能引脚不做特别介绍,在后续章节用到时再做分析。
1. S5PV210的GPIO寄存器总览
GPA0: 8 in/out port——带流控的2×UART; 
GPA1: 4 in/out port——带流控的2×UART或1×UART; 
GPB: 8 in/out port——2×SPI总线接口; 
GPC0: 5 in/out port——I2S总线接口、PCM接口、AC97接口; 
GPC1: 5 in/out port——I2S总线接口、SPDIF接口、LCD_FRM接口; 
GPD0: 4 in/out port——PWM接口; 
GPD1: 6 in/out port——3×I2C接口、PWM接口、IEM接口; 
GPE0、1: 13 in/out port——Camera接口; 
GPF0、1、2、3: 30 in/out port——LCD接口; 
GPG0、1、2、3: 28 in/out port——4×MMC通道(通道0和2支持4位、8位模式,通道1和3仅支持4位模式); 
GPI: 低功率I2S接口、PCM接口(不使用in/out port),通过AUDIO_SS PDN寄存器配置低功耗PDN; 
GPJ0、1、2、3、4: 35 in/out port——Modem接口、CAMIF、CFCON、KEYPAD、SROM ADDR[22:16]; 
MP0_1,2,3: 20 in/out port——外部总线接口(EBI)信号控制(SROM、NF、OneNAND); 
MP0_4_5_6_7: 32 in/out memory port——EBI; 
MP1_0~8: 71 DRAM1 port(不使用in/out port); 
MP2_0~8: 71 DRAM2 port(不使用in/out port); 
ETC0、ETC1、ETC2、ETC4: 28 in/out ETC port——JTAG、Operating Mode、RESET、CLOCK(ETC3保留)。
2. 特征介绍
S5PV210关键特征如下: 
 支持146个可控的GPIO中断; 
 支持32个可控外部中断; 
 支持237个多功能输入/输出接口; 
 支持在系统睡眠模式下引脚可控(除GPH0、GPH1、GPH2和GPH3)。
3. S5PV210的GPIO功能介绍
S5PV210的GPIO包含两部分,即带电部分(alivepart)和不带电部分(offpart),对于alivepart模式下的GPIO寄存器,在睡眠模式时提供电源,所以寄存器中的值不会丢失; 在offpart模式下则不同。S5PV210的功能如图51所示。


图51GPIO功能模块图



5.1.3实验用到的寄存器详解
S5PV210的GPIO寄存器非常多,每个接口组有两种类型的控制寄存器,一种工作在正常模式,另一种工作在掉电模式(STOP、DEEPSTOP、SLEEP mode),下面只针对本章实验用到的GPIO接口GPC0进行介绍,其他的GPIO接口用法可以依葫芦画瓢。GPC0的控制寄存器有GPC0CON、GPC0DAT、GPC0PUD、GPC0DRV、GPC0CONPDN和GPC0PUDPDN,前面4类工作在正常模式,后面2类工作在掉电模式。
1) GPC0CON寄存器
此寄存器为GPC0引脚的控制寄存器,主要用途是配置各引脚的功能,此引脚对应的内存地址是0xE020_0060。表51是S5PV210手册里关于GPC0_3、GPC0_4引脚的配置信息。

表51GPC0控制寄存器



GPC0CON位描述初 始 状 态

GPC0CON[4][19:16]0000 =输入

0001 =输出

0010 = I2S_1_SDO

0011 = PCM_1_SOUT

0100 = AC97SDO

0101~1110 =保留

1111 = GPC0_INIT[4]中断0000
GPC0CON[3][15:12]0000 =输入

0001 =输出

0010 = I2S_1_SDI

0011 = PCM_1_SIN

0100 = AC97SDI

0101~1110 =保留

1111 = GPC0_INIT[3]中断0000

从表中可以看出,每4位控制一个引脚(GPC0_3或GPC0_4),当值为0b0000时引脚被设为输入功能,当值为0b0001时设为输出功能,当值为0b0010~0b0100时引脚设为特殊功能引脚,当值为0b1111时设为中断引脚,0b0101~0b1110保留未使用。
2) GPC0DAT寄存器
此引脚对应的内存地址是0xE020_0064,该寄存器决定了引脚的输入或输出电平的状态,当引脚设为输入时,通过读寄存器可知对应引脚电平状态是高还是低。当引脚设为输出时,写寄存器对应位可使引脚输出高电平或低电平; 当引脚被设为功能引脚时,如果读寄存器对应引脚的值是不确定的。实验使用的引脚是GPC0DAT[4:3]。
3) GPC0PUD寄存器
此引脚对应的内存地址是0xE020_0068,使用两位来控制1个引脚。当值为0b00时,对应引脚无上拉/下拉电阻; 当值为0b01时,有内部下拉电阻; 当值为0b10,有内部上拉电阻; 当值为0b11时为保留。
上拉/下拉电阻的作用是当GPIO引脚处于高阻态(既不是输出高电平,也不是输出低电平,相当于没有接入)时,它的电平状态由上拉电阻或下拉电阻决定。
4) GPC0DRV寄存器
此引脚对应的内存地址是0xE020_006C,该寄存器为接口组驱动能力控制寄存器,主要用于调节引脚的电流强度,S5PV210给出了4种强度,由2位控制,数值越大强度越大。通常对于高速信号或较弱的周边装置,可调大对应引脚的强度值,在实际使用中需要合理使用该寄存器,强度越大电流消耗也越大。
5) GPC0CONPDN寄存器
此引脚对应的内存地址是0xE020_0070,用2位来控制引脚的功能。当值为0b00时,引脚输出低电平; 当值为0b01时,输出高电平; 当值为0b10时,对应引脚被设为输入; 当值为0b11时,引脚保持原来的状态。
6) GPC0PUDPDN寄存器
此引脚对应的内存地址是0xE020_0074,用2位来控制引脚。当值设为0b00时,无上拉/下拉电阻; 当值为0b01时,有下拉电阻; 当值为0b10时,有上拉电阻; 当值为0b11时保留。GPCOPUDPDN寄存器的功能与GPCOPUD类似。


视频讲解


5.2S5PV210的GPIO应用实例
5.2.1GPIO实验
1. 实验目的

利用S5PV210的GPC0_3、GPC0_4这两个GPIO引脚控制2个LED发光二极管,分别用汇编语言和C语言实现。
2. 实验原理
如图52所示,LED1、LED2分别与GPC0_3、GPC0_4相连,中间接两个NPN型三极管。当GPIO引脚输出高电平时三极管与地(GND)导通,LED灯亮; 反之,输出低电平,LED灯就灭。这里的三极管有点像“开关”,控制着LED的亮灭。


图52GPIO功能模块图


注: 从本章开始所有的实验都基于TQ210开发板设计与测试,但是原理适用于所有ARM架构学习板,读者有兴趣可以将本书所有实验移植到其他学习板上,做到学以致用。


视频讲解


5.2.2程序设计与代码详解
1. 用汇编语言实现点亮LED1

在上一章我们对S5PV210的启动过程以及SD启动卡的制作都进行了详细的介绍,下面主要分析程序设计部分,源代码存放在/opt/hardware/ch6/led_on,汇编语言操控GPIO的代码如下所示,相关代码在led_on.S文件中。

01 .text

02 .global _start			/* 声明一个全局的标号 */

03 _start:

04ldrr0,=0xE0200060/* GPC0CON寄存器的地址为0xE0200060 */

05ldr r1, [r0] /* 读出GPC0CON寄存器原来的值 */

06bic r1, r1, #0xf000/* bit[15:12]清零 */

07orrr1,r1,#0x1000/* 设置GPC0_3[15:12]=0b0001 */

08strr1,[r0]/* 写入GPC0CON,配置GPC0_3为输出引脚 */

09ldrr0,=0xE0200064/* GPC0DAT寄存器的地址为0xE0200064 */

10ldrr1,[r0]/* 读出GPC0DAT寄存器原来的值 */

11bicr1,r1,#0x8/* bit[3]清零 */

12orrr1,r1,#0x8/* bit[3]=1 */

13strr1,[r0]/* 写入GPC0DAT,GPC0_3输出高电平 */

14 halt_loop:

15 bhalt_loop/* 死循环,不让程序跑飞 */

整个代码量很少也很简单,这里有两个地方需要注意: 
(1) 在S3C2440等平台上,我们都要先关看门狗,如果是用C语言实现,还要设置栈。而在S5PV210中,这些动作都已在厂家固化的代码BL0里做好了,如对此不清楚,可以再回顾第4章的内容。
(2) 对寄存器位的修改,通常都是先读出寄存器,然后修改对应位,再写回寄存器,其他位保持不变,这样做的目的是不改变其他引脚状态。
下面是编译用到的Makefile文件: 

01 led_on.bin:led_on.S

02arm-linux-gcc -c -o led_on.o led_on.S

03arm-linux-ld -Ttext 0xD0020010 led_on.o -o led_on_elf

04arm-linux-objcopy -O binary -S led_on_elf led_on.bin

05arm-linux-objdump -D led_on_elf > led_on.dis

06 clean:

07rm -f led_on.bin led_on_elf *.o

这个Makefile文件可以说是一个最基本的Makefile语法格式(目标依赖命令),各编译工具及其参数在配套资源补充资料第1章中有介绍。0xD002_0010为程序的链接地址(即运行地址,也是BL1的有效地址)。这里要再次提醒的是,命令前面一定要有一个Tab制表符,这是Makefile语法上的规定,否则make工具就不认识了!
接下来就可以用FTP将所有代码上传到宿主机(Ubuntu系统)编译,编译非常简单,只要在代码所在的目录下执行make命令即可,最终生成二进制格式的bin文件。

$ cd /opt/examples/ch6/led_on

$ make

最后将生成好的bin文件按第4章介绍的步骤烧写到SD卡中,将目标板拨到SD卡启动,插入SD卡上电即可看到LED1灯被点亮。
关于本书所使用的TQ210开发板的SD卡启动方式和NAND启动方式,对应拨码开关的设置如图53所示。


图53TQ210开发板启动方式拨码开关设置


2. 用C语言实现循环点亮两个LED
用C语言实现,其汇编代码就变得更加简单了,只需一个跳转语句。下面分三步来实现C语言。
循环点亮两个LED灯,代码在/opt/hardware/ch6/led_on_c。
1) 启动代码start.S

01 .text

02 .global _start  /* 声明一个全局的标号 */

03 _start:

04bl main  /* 跳转到C函数中执行 */

05

06 halt_loop:

07 bhalt_loop  /*死循环,不让程序跑飞*/

2) 循环点亮LED灯

01 #define GPC0CON*((volatile unsigned int *)0xE0200060)

02 #define GPC0DAT*((volatile unsigned int *)0xE0200064)

03

04 #defineGPC0_3_out(1<<(3*4))

05 #defineGPC0_4_out(1<<(4*4))

06

07 #defineGPC0_3_MASK(0xF<<(3*4))

08 #defineGPC0_4_MASK(0xF<<(4*4))

09

10 void delay(volatile unsigned long dly)

11 {

12volatile unsigned int t = 0xFFFF;

13while (dly--)//CortexA8默认时钟频率都达到了667MHz

13 for(; t > 0; t--);//循环次数必须设大一点,否则看不出闪烁效果

14 }

15

16 int main(void)

17 {

18unsigned long i = (1 << 3);

19GPC0CON &= ~(GPC0_3_MASK | GPC0_4_MASK);//清bit[15:12]和bit[19:16]

20GPC0CON |= (GPC0_3_out | GPC0_4_out); //配置GPC0_3和GPC0_4为输出引脚

21

22while (1)

23{

24delay(0x50000);//延时

25GPC0DAT &= ~(0x3 << 3);//LED1和LED2熄灭

26if (i == 0x08)

27i = (1 << 4);

28else

29i = (1 << 3);

30GPC0DAT |= i;//循环点亮LED灯

31}

32return 0;

33 }

上述代码的功能实现比汇编稍复杂,这里是循环点亮了两个LED,不过基本原理是一样的,都是配置寄存器和往寄存器写入数值。在操作方法上,C语言对寄存器的操作是通过宏函数实现的,这个宏就代表了寄存器的地址,往这个地址中写入数据就可以配置GPIO引脚的功能。下面通过一个C语言指针的例子来理解宏定义的功能。

int a;   //定义一个整型变量a

int *p;   //定义一个整型指针变量p

p=&a;  //指针指向变量a

*p = 6  //变量a的值等于6

上面几行代码是C语言指针最基本的用法,通常可以把指针看作一个地址,比如这里指针p就等于变量a的地址,假设变量a的地址为0x12345678,对指针p操作就相当于操作地址: 

int *p;

p = 0x12345678;

为方便理解,实际p = (int *)0x12345678,这里的(int *)是将地址转换为int类型。
*p表示地址中的内容,比如上面的6,换句话说,变量a的值与*p是同一个值,现在修改地址0x12345678的内容为8,只需要如下操作即可: 

*p = 8;    //与a = 8是等价的

也就相当于: 

*(0x12345678) = 8;
或者严格写成*((int *)0x12345678) = 8;

上面这样写看上去有点别扭,可以定义一个宏来表示: 

#define A *((int *)0x12345678)

A = 8;

分析到这里,再看看程序中定义的宏与这里的宏是不是很相似了。这里还有一个关键字volatile需要说明下,它的本意是“易变的”,由于访问寄存器要比访问内存单元快得多,所以编译器一般都会进行减少存取内存的优化,直接从缓存cache中取数据这样会有一个问题,即可能会读取到“脏”数据(数据被其他程序代码修改)。当用volatile声明的时候,其实就是告诉编译器对访问该地址的代码不要优化了,使用的时候系统总是从它的内存读取数据,以保证不“脏”。
3) 编写Makefile

01 objs := start.o leds.o

02

03 leds.bin: $(objs)

04arm-linux-ld -Ttext 0xD0020010 -o leds.elf $^

05arm-linux-objcopy -O binary -S leds.elf $@

06arm-linux-objdump -D leds.elf > leds.dis

07

08 %.o : %.c

09arm-linux-gcc -c -O2 $< -o $@

10 %.o : %.S

11arm-linux-gcc -c -O2 $< -o $@

12 clean:

13rm -f *.o *.elf *.bin *.dis

这里的Makefile文件看似比上一个Makefile要复杂,主要是引入了一些变量的用法,目的是加深对Makefile的了解,更多Makefile知识见配套资源补充资料第1章中的内容。