GPIO接口

《嵌入式Linux应用完全开发手册》第2篇第5章总结归纳

本章目标

  1. 掌握嵌入式开发的步骤:编程、编译、烧写程序、运行
  2. 通过GPIO口的操作了解软件如何控制硬件

GPIO硬件介绍

GPIO(Genaral Purpose I/O Ports)意思是通过通用输入/输出端口,通俗的说,就是一些引脚,可以通过它们输出高低电平或者通过它们读入引脚的状态—是高电平还是低电平。
S3C2410有117个I/O端口,共分为A-H 8组,GPA、GPB、……、GPH。S3C2440有130个I/O端口,共分为A-H 9组,GPA、GPB、……、GPJ。可以通过设置寄存器来确定某个引脚用于输入、输出还是其他特殊功能。比如可以设置GPH6作为一般的输入、输出引脚,或者用于串口。
GPIO的操作是所有硬件操作的基础,由此扩展开来可以了解所有硬件的操作,这是底层开发人员必须掌握的。

通过寄存器来操作GPIO引脚

既然一个引脚可以用于输入、输出或其他功能,那么一定有寄存器用来选择这些功能;对于输入,一定可以通过读取某个寄存器来确定引脚的电平是高还是低;对于输出,一定可以通过写入某个寄存器来让这个引脚输出高电平还是低电平;对于其他特殊功能,则有另外的寄存器来控制它。
对于这几组GPIO引脚,它们的寄存器是相似的;GPxCON用于选择引脚功能。GPxDAT用于读写引脚数据。GPxUP用于确定是否使用内部上拉电阻。

GPxCON寄存器

从寄存器的名字可以看出,它用于配置(Configure)—选择引脚的功能。
PORT A与PORT B-PORT H/J在功能选择方面有所不同,GPACON中每一位对应一根引脚(共23根引脚)。当某位被设置为0时,相应引脚为输出引脚,此时我们可以在GPADAT中的相应位写入0或1让此引脚输出高电平或者低电平。当某位被设置为1时,相应的引脚为地址线,或用于地址控制。此时的GPADAT无用。通常而言GPACON全被设置为1,以便访问外部存储器件。
PORT B-PORT H/J 在寄存器操作方面完全相同。GPxCON中每两位控制一根引脚,00表示输入、01表示输出、10表示特殊功能、11保留。

GPxDAT寄存器

GPxDAT用于读/写引脚;当引脚被设置为输入时,读此寄存器可知相应引脚的电平状态是高还是低;当引脚被设置为输出时,写此寄存器相应位可令此引脚输出高电平或者低电平。

GPxUP

某位为1时,相应引脚无内部上拉电阻;为0时,相应引脚使用内部上拉电阻。
img not found
上拉、下拉电阻的作用在于,当GPIO引脚处于第三态(既不是输出高电平,也不是输出低电平,而是呈高阻态)时,它的电平状态由上拉、下拉电阻决定。

怎样使用软件来访问硬件

访问单个引脚

单个引脚的操作无外乎3种:输出高低电平、检测引脚状态、中断。对某个引脚的操作一般通过读写寄存器来完成。
比如对于图5.2所示的电路,可以设置GPBCON寄存器将GPB5、GPB6、GPB7和GPB8设为输出功能,然会写GPBDAT的寄存器使得这四个引脚输出高电平或低电平。输出高电平时,相应的LED灯熄灭,输出低电平时,相应的LED灯点亮。
还可以设置GPFCON寄存器将GPF0、GPF2、GPF3和GPF11设为输入功能,然后通过读出GPFDAT/GPGDAT寄存器并判断相应位是0还是1来确定各个按键是否被按下。某个按键按下时,相应引脚电平为低,GPFDAT/GPGDAT寄存器相应位为0,否则为1。
那么怎么访问这些寄存器呢,通过软件,读写它们的地址。比如,GPBCON和GPBDAT寄存器的地址都是0x56000010、0x56000014,可以通过如下的指令让GPB5输出低电平,点亮LED1。
img not found

1
2
3
4
5
#define GPBCON (*(volatile unsigned long *)0x56000010)
#define GPBDAT (*(volatile unsigned long *)0x56000014)
#define GPB5_out (1<<(5*2))
GPBCON = GPB5_out;//GPB5引脚设置为输出
GPBDAT &= ~(1<<5);//GPB5输出低电平

以总线方式访问硬件

并非只能通过寄存器才能发出硬件信号,实际上,通过访问总线的方式控制硬件更常见。以NOR Flash的访问为例:
img not found
图中,缓冲器的作用是为了提高驱动力、隔离前后级信号。NOR Flash AM29LV800BB的片选信号使用S3C2410/S3C2440的nGCS0信号,当CPU发出的地址信号处于0x00000000-0x07FFFFFF之间时,nGCS0信号有效,于是NOR Flash被选中。这时,CPU发出的地址信号传到NOR Flash;进行写操作时,nWE信号为低,数据信号从CPU发出给NOR Flash;进行读操作时,nWE信号为高,数据信号从NOR Flash发给CPU。上图所示的硬件连线决定了读写操作都是以16位为单位的。

1
2
3
4
/*地址对齐的16位读操作*/
unsigned short *pwAddr = (unsigned short *)0x2;
unsigned short wVal;
wVal = *pwAddr;

上述代码就会向NOR Flash发起读操作:CPU发出的读地址为0x2,则地址总线ADDR1-ADDR20、A0-A19的信号都是1、0、…、0(CPU的ADDR0为0,不过ADDR0没有接到NOR Flash上)。NOR Flash接收到的地址就是0x1,NOR Flash在稍后的时间里将此址上的16位数据取出,并通过数据总线D0-D15发给CPU。

1
2
3
4
/*地址不对齐的16位读操作*/
unsigned short *pwAddr = (unsigned short *)0x1;
unsigned short wVal;
wVal = *pwAddr;

由于地址位0x1;不是2对齐的,但是BANK0的位宽被设为16,这将导致异常。我们可以设置异常处理函数来处理这种情况。在异常处理函数中,使用0x0、0x2发起两次读操作,然后将两个结果组合起来。使用地址0x0读到两字节数据D0、D1,再使用地址0x2读到D2、D3。最后D1、D2组合成一个16位的数返回给wVal。如果没有设置地址不对齐异常处理函数,那么上述代码将出错。如果某个BANK位宽被设置为n,访问此BANK时,在总线上永远只会看到地址对齐的n位操作。

1
2
3
4
/*8位读操作*/
unsigned char *pucAddr = (unsigned char *)0x6;
unsigned char ucVal;
ucVal = *pucAddr;

CPU首先使用地址0x6对NOR Flash发起16位的读操作,得到两字节的数据,假设位D1、D0,然后将D0取出赋值给uvVal。在读操作期间,地址总线ADDR1-ADDR20、A0-A19的信号都是1、1、1、…、0。CPU会自动丢弃D1。

1
2
3
4
/*32位读操作*/
unsigned int *pdwAddr = (unsigned int *)0x6;
unsigned int dwVal;
dwVal = *pdwAddr;

CPU首先使用地址0x6对NOR Flash发起16位的读操作,得到两字节的数据,假设为D0,D1;再使用地址0x8发起读操作,得到两字节的数据D2,D3,最后将这4个字节的数据组合后再赋值给变量dwVal。
由于NOR Flash的特性,使得对NOR Flash的写操作比较复杂—比如要先发出特定的地址信号通知NOR Flash准备接受数据,然后才发出数据。

1
2
3
/*16位写操作*/
unsigned short *pwAddr = (unsigned short *)0x6;
*pwAddr = 0x1234;

CPU发起一次对NOR Flash的写操作,地址总线ADDR1-ADDR20、A0-A19的信号都时1、1、…、0;数据线DATA0-DATA15、D0-D15的信号为0、0、1、0、1、1、0、0、0、1、0、0、1、0、0、0。
由此可见,CPU使用某个地址进行访问时,这个32位的地址值和ADDR0-ADDR31一一对应,外接的设备可以以8位、16位、32位进行操作—取决于硬件设计。如果以8位进行操作,那么数据出现在数据信号DATA0-DATA7上,如果以16位进行操作,则数值出现在数据线DATA0-DATA15上;如果以32位进行操作,则数值出现在DATA0-DATA31上。

GPIO操作实例

LED和按键与处理器的电路连接基于图5.2。程序基于裸机开发,不带操作系统。

纯汇编实现点亮一个LED

汇编程序实现。

1
2
3
4
5
6
7
8
9
10
11
.text
.global _start
_start:
ldr r0,=0x56000010 @r0设置为GPBCON寄存器,用于配置GPIOB系列引脚。
mov r1,#0x00000400 @r1赋值立即数0x00000400
str r1,[r0] @GPBCON写入0x00000400,设置GPB5为输出口,位[10:9]=0b01
ldr r0,=0x56000014 @r0设置为GPBDAT寄存器,用于读写GPIOB系列引脚的数据。
mov r1,#0x00000020 @r1赋值立即数0x00000020
str r1,[r0] @点亮LED1
main_loop:
b main_lopp @死循环

编译如下:

1
2
3
arm-linux-gcc -g -c -o led_on.o led_on.S                        @编译
arm-linux-ld -Ttext 0x00000000 -g led_on.o -o led_on_elf @链接
arm-linux-objcopy -O binary -S led_on_elf led_on.bin @将elf格式的可执行文件转换位二进制格式

C代码实现点亮一个LED

C语言执行的第一条指令并不在main函数中。生成一个C程序的可执行文件时,编译器通常会在我们的代码中加上几个被称为启动文件的代码—crtl.o、crti.o、crtend.o、crtn.o等,它们是标准库文件。这些代码设置C程序的堆栈等,然后调用main函数。它们依赖于操作系统,在裸板上这些代码无法执行,所以需要自己写一个。
这段代码很简单,只有6条指令,自己编写的crt0.S文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
File:crt0.S
通过它转入C程序
*/
.text
.global _start
_start:
ldr r0,=0x56000010 @watchdog地址寄存器
mov r1,#0x0 @
str r1,[r0] @写入0,禁止watchdog,否则CPU会不断重启
ldr sp,=1024*4 @设置堆栈,注意不能大于4KB,因为当前可以内存只有4KB
@NAND Flash中的代码会在复位后移到内部RAM(只有4KB)
bl main @调用C程序中的main函数

halt_loop:
b halt_loop

设置好堆栈指针后,就可以调用C函数main了。C函数执行前,必须设置栈。
所以现在可以写出控制LED的C程序了。main函数在led_on_c.c中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
#define GPBCON (*(volatile unsigned long *)0x56000010)
#define GPBDAT (*(volatile unsigned long *)0x56000014)


int main()
{
GPBCON = 0x00000040; //设置GPB5为输出端口,位[10:9]=0b01
GPBDAT = 0x00000000; //GPB5输出0,LED1点亮

return 0;
}

编译如下:

1
2
3
4
5
arm-linux-gcc -g -c -o crt0.o crt0.S
arm-linux-gcc -g -c -o led_on_c.0 led_on_c.c
arm-linux-ld -Ttext 0x00000000 -g crt0.o led_on_c.o -o led_on_c_elf
arm-linux-objcopy -O binary -S led_on_c_elf led_on_c.bin
arm-linux-objdump -D -m arm led_on_c_elf > led_on_c.dis

先分别编译crt0.S和led_on_c.c(不连接)。然后将编译的结果连接起来。然后把得到的ELF格式的文件led_on_c_elf转换成二进制的bin文件。最后将结果转换为汇编代码以供查看。

按键来控制LED

当K1-K4某个按键按下时,点亮LED1-LED4中相应的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/*
File:key_led.c
*/
#define GPBCON (* (unsigned long *) 0x56000010)
#define GPBDAT (* (unsigned long *) 0x56000014)
#define GPFCON (* (unsigned long *) 0x56000050)
#define GPFDAT (* (unsigned long *) 0x56000054)
#define GPGCON (* (unsigned long *) 0x56000060)
#define GPGDAT (* (unsigned long *) 0x56000064)

/*
LED1-LED4对应GPB5、GPB6、GPB7、GPB8
*/
#define GPB5_out (1<<(5*2))
#define GPB6_out (1<<(6*2))
#define GPB7_out (1<<(7*2))
#define GPB8_out (1<<(8*2))

/*
K1-K4对应GPG11、GPG3、GPF2、GPF0
*/
#define GPG11_in ~(3<<(11*2))
#define GPG3_in ~(3<<(3*2))
#define GPF2_in ~(3<<(2*2))
#define GPF0_in ~(3<<(0*2))

int main()
{
unsigned long dwDat;

//LED1-LED4对应的4根引脚设置为输出
GPBCON = GPB5_out | GPB6_out | GPB7_out | GPB8_out ;

//K1-K2对应的两根引脚设为输入
GPGCON = GPG11_in & GPG3_in;

while(1)
{
dwDat = GPGDAT; //读取GPG管脚电平状态

if(dwDat & (1 << 11)) //K1没有按下
GPBDAT |= (1 << 5); //LED1熄灭
else
GPBDAT &= ~(1 << 5); //LED1点亮

if(dwDat & (1 << 3)) //K2没有按下
GPBDAT |= (1 << 6); //LED2熄灭
else
GPBDAT &= ~(1 << 6); //LED2点亮

dwDat = GPFDAT; //读取GPF管脚电平状态

if(dwDat & (1 << 2)) //K3没有按下
GPBDAT |= (1 << 7); //LED3熄灭
else
GPBDAT &= ~(1 << 7); //LED3点亮

if(dwDat & (1 << 0)) //K4没有按下
GPBDAT |= (1 << 8); //LED4熄灭
else
GPBDAT &= ~(1 << 8); //LED4点亮
}

return 0;
}

代码先将LED1-LED4对应的引脚GPB5-GPB8设为输出引脚。
然后将K1、K2对应的引脚GPG11、GPG3设为输入引脚,K3、K4对应的引脚GPF2、GPF0设为输入引脚。
然后就是一个无穷循环,读取GPGDAT、GPFDAT寄存器,从中判断K1、K2、K3、K4是否按下。若按下则点亮相应的LED,否则熄灭相应的LED。