GPIO(General Purpose I/O Ports)意思为通用输入/输出端口,通俗地说,就是一些引脚,可以通过它们输出高低电平、或者通过它们读入引脚的状态——是高电平还是低电平。
三星Exynos4412,它有304个 GPIO,分为GPA0、GPA1、GPB、GPC0、GPC1等共37组。可以通过设置寄存器来确定某个引脚用于输入、输出还是其它特殊功能。比如可以设置GPC0、GPC1作为一般的输入引脚、输出引脚,或者用于AC97、SPDIF、I2C、SPI口。
GPIO的操作是所有硬件操作的基础,由此扩展开来可以了解所有硬件的操作,这是底层开发人员必须掌握的。
Exynos4412芯片的GPIO寄存器:
既然一个引脚可以用于输入、输出或其它特殊功能,那么一定有寄存器用来选择这些功能;对于输入,一定可以通过读取某个寄存器来确定引脚的电平是高还是低;对于输出,一定可以通过写入某个寄存器来让这个引脚输出高电平或低电平;对于其它特殊功能,则有另外的寄存器来控制它——这些特殊功能现在先不关注。
如上推测,对于这几组GPIO引脚,它们的寄存器是相似的:GPXXCON用于选择引脚功能,GPXXDAT用于读/写引脚数据;另外,GPXXPUD用于确定是否使用内部上拉/下拉电阻。
GPXXCON寄存器:
从寄存器的名字即可看出,它用于"配置"(Configure)——选择引脚的功能。该寄存器中,使用4位来配置1个引脚。比如下图1:
图1. GPIO配置寄存器
GPXXDAT寄存器:
用于读/写引脚:当引脚被设为输入时,读此寄存器可知相应引脚的电平状态是高还是低;当引脚被设为输出时,写此寄存器相应位可令此引脚输出高电平或低电平。
GPXXPUD寄存器:
使用2位来控制1个引脚:值为0b00时,相应引脚无内部上拉/下拉电阻;值为0b01时,使用内部下拉电阻;值为0b11时,使用内部上拉电阻;0b10为保留值。
所谓上拉电阻、下拉电阻,如图2所示:
图2. 上拉电阻和下拉电阻
上拉电阻、下拉电阻的作用在于,当GPIO引脚处于第三态(既不是输出高电平,也不是输出低电平,而是呈高阻态,即相当于没接芯片)时,它的电平状态由上拉电阻、下阻电阻确定。
怎样使用软件来访问硬件:
单引脚的操作无外乎三种:输出高低电平、检测引脚状态、中断。对某个引脚的操作一般通过读写寄存器来完成。
比如对于图3的电路,可以设置GPM4CON寄存器将GPM4_0、GPM4_1、GPM4_2和GPM4_3设为输出功能,然后写GPM4DAT寄存器的相应位使得这4个引脚输出高电平或低电平:输出低电平时,相应的LED点亮;输出高电平时,相应的LED熄灭。
还可以设置GPX3CON寄存器将GPX3_2、GPX3_3、GPX3_4、GPX3_5设为输入功能,然后通过读出GPX3DAT寄存器并判断bit2、bit3、bit4、bit5是0还是1来确定按键是否被按下:按键被按下时,相应引脚电平为低,相应位为0;否则为1。
那么,怎么访问这些寄存器呢?通过软件,读写它们的地址。比如,Exynos4412的GPM4CON、GPM4DAT寄存器地址分别是0x110002E0、0x110002E4,可以通过如下的指令让GPM4_0输出低电平,点亮LED1:
#define GPM4CON (*(volatile unsigned long *)0x110002E0)
#define GPM4DAT (*(volatile unsigned long *)0x110002E4)
#define GPM40_OUT (1<<0)
#define GPM40_MSK (0xf<<0)
GPM4CON &= ~GPM40_MSK; //GPM4_0引脚对应的4位清零(因为我们不知道它原来的值是多少,清零最可靠)
GPM4CON |= GPM40_OUT; //GPM4_0引脚设为输出
GPM4DAT &= ~(1<<0); //GPM4_0输出低电平
图3. LED与按键连线图
以某种协议的接口访问硬件:
比如UART、I2C、SPI等接口,我们只需要根据协议要求做好相关设置,然后把数据填入某个寄存器,这类接口部件就会把数据按某种格式发送出去。
如图4所示,以UART为例:设置好UART的波特率等格式后,把要发送的数据写入UART的寄存器,UART控制器就会把数据逐位发送出去。
图4. 协议类接口示例UART
GPIO操作实例:LED和按键:
从这节开始,将涉及在单板上运行程序了,下面用几个例子由简到繁地介绍。LED和按键与处理器的电路连接如图3所示。
本小节有3个实例,通过读写GPIO寄存器来驱动LED、获得按键状态。先使用汇编程序编写一个简单的点亮LED的程序,然后使用C语言实现了更复杂的功能。
示例1:使用汇编代码点亮一个LED
只是简单地点亮发光二极管LED1。本实例的目的是让读者对开发流程有个基本概念。
主要有三个文件,首先,是led.S文件,里面全是汇编语言来完成的。已经做了详细的注释,不再讲解。
.text //表示下面的代码是text段
.global _start //定义个全局标号
_start:
//设置GPM4_0为输出引脚
ldr r0, =0x110002E0 //将0x110002E0数字保存的R0寄存器中
ldr r1, [r0] //将R0中保存的0x110002E0地址中的数据取出来,保存到R1寄存器
bic r1, r1, #0xF //将R1寄存器中的数据低四位清零
orr r1, r1, #0x01 //将R1寄存器中的数据bit0置1
str r1, [r0] //将R1寄存器中的数据保存到R0寄存器中的数据表示的地址中
//设置GPM4_0输出低电平
ldr r0, =0x110002E4 //将0x110002E4数字保存的R0寄存器中
ldr r1, [r0] //将R0中保存的0x110002E4地址中的数据取出来,保存到R1寄存器
bic r1, r1, #0x1 //将R1寄存器中的数据bit0清零
str r1, [r0] //将R1寄存器中的数据保存到R0寄存器中的数据表示的地址中
halt: //死循环
b halt
值得注意的是:要修改寄存器的某些位时,最好先把原值读出并修改这些位,再把值写回去。寄存器里每位都有它的作用,这样做可以避免影响到其他位。
下面的是链接脚本文件leds.lds中的内容;很简单,如果有不明白的可以网上查询。
SECTIONS {
. = 0x02023400; //链接地址,也就是说,希望程序运行在此地址
.text : { *(.text) } //代码段
.rodata ALIGN(4) : {*(.rodata)} //只读数据段
.data ALIGN(4) : { *(.data) } //数据段
.bss ALIGN(4) : { *(.bss) *(COMMON) } //bss段
}
最后,介绍Makefile文件的内容:
led.bin : led.S
arm-linux-gcc -c -o led.o led.S #预处理、编译、汇编,不链接
arm-linux-ld -Tleds.lds -g led.o -o led.elf #链接
arm-linux-objcopy -O binary -S led.elf led.bin #将ELF格式文件转换为BIN文件
arm-linux-objdump -D led.elf > led.dis #反汇编
clean:
rm *.o *.elf *.dis *.bin
make指令比较第1行中文件led.bin和文件led.S的时间,如果led.S的时间比led.bin的时间新(led_on.bin未生成时,此条件默认成立),则执行第2~5行的命令重新生成led.bin及其他文件。也可以不用指令make,而直接一条一条地执行2~5行的指令——但是这样效率比较低。
第2行的指令是编译,第3行是连接,第4行是把ELF格式的可执行文件led.elf转换成二进制格式文件led.bin,第5行是得到它的反汇编文件(目前没用到该文件)。
执行"make clean"时强制执行第7行的删除命令。
注意:Makefile文件中相应的命令行前一定要有一个制表符(TAB)。
如果对Makefile中每句是详细意思,请参考交叉编译工具链的使用。
现在,来介绍将led.bin文件烧写到SD卡中,然后插入到开发板的SD卡槽中,设置启动模式,然后开启电源运行。
把SD卡接入读卡器,让VMware软件位于窗口最前面,然后把读卡器接入电脑。
点击VMware菜单"VM"->"Removable Devices",找到新出现的USB存储设备,点击"Connect"。
注意:如果菜单里没有新的设备出现,原因是VMware的USB服务未启动。可用以下方式启动:打开"控制面板"->"系统和安全"->"管理工具"->"服务",找到"VMware USB Arbitration Service",双击启动它。然后重启VMware软件。
然后执行以下命令,一般最后一个"不带数字的"设备节点就是SD卡的设备名:
$ ls /dev/sd*
输出示例:
brw-rw---- 1 root disk 8, 0 Nov 18 09:22 /dev/sda
brw-rw---- 1 root disk 8, 1 Nov 18 09:22 /dev/sda1
brw-rw---- 1 root disk 8, 2 Nov 18 09:22 /dev/sda2
brw-rw---- 1 root disk 8, 5 Nov 18 09:22 /dev/sda5
brw-rw---- 1 root disk 8, 16 Nov 18 17:42 /dev/sdb
brw-rw---- 1 root disk 8, 17 Nov 18 17:42 /dev/sdb1
在上面的输出示例中,/dev/sdb就是该卡的设备名。
首先,将led.bin文件拷贝到sd_fusing.sh文件的目录下,也就是同一个目录。执行如下命令将led.bin写入SD卡:
$ sudo sd_fusing.sh /dev/sdb led.bin
注意看该命令的输出信息,若有"source file image is fused successfully."即表示烧写成功。否则请根据错误信息解决。
注意:sd_fusing.sh的作用后面会详细讲解。
把SD卡取下接到开发板上,开发重新上电即可看到LED1灯被点亮了。
汇编语言可读性太差,这次用C语言来实现了同样的功能,而以后的实验也尽量用C语言实现。
示例2:使用C语言代码点亮一个LED
C语言程序执行的第一条指令,并不在main函数中。生成一个C程序的可执行文件时,编译器通常会在我们的代码中加上几个被称为启动文件的代码——crt1.o、crti.o、crtend.o、crtn.o等,它们是标准库文件。这些代码设置C程序的堆栈等,然后调用main函数。它们依赖于操作系统,在裸板上这些代码无法执行,所以需要自己写一个。
led.S中的代码很简单,关键指令只有2条。文件内容如下:
.text
.global _start
_start:
ldr sp, =0x02027400 //调用C函数之前必须设置栈,栈用于保存运行环境,给局部变量分配空间;
//参考ROM手册P14,我们把栈指向BL2的最上方;
//即:0x02020000(iROM基地址)+5K(iROM代码用)+8K(BL1用)+16K(BL2用)
bl main //跳转到C函数中执行
halt: //死循环
b halt
它在第4行设置好栈指针后,就可以通过第8行调用C函数main了──C函数执行前,必须设置栈。
现在,可以很容易写出控制LED的程序了。main函数在main.c文件中,代码如下:
//定义两个宏,方便操作使用到的寄存器
#define GPM4CON (*(volatile int *)0x110002E0)
#define GPM4DAT (*(volatile int *)0x110002E4)
int main()
{
//设置GPM4_0引脚为输出
GPM4CON &= ~0xF; //GPM4CON寄存器的低4位清零
GPM4CON |= 0x1; //GPM4CON寄存器的bit0置1,设置为输出引脚
//设置GPM4_0引脚为低电平
GPM4DAT &= ~0x1; //GPM4DAT寄存器bit0清零,输出低电平
return 0;
}
程序很简单,值得一提的是寄存器的操作方法。以GPM4CON为例,它被定义为:
#define GPM4CON (*(volatile unsigned int *)0x110002E0)
要理解上述宏定义,先看看以下代码:
int a;
int *p;
p = &a;
*p = 123;
指针p指向变量a,即p等于变量a的地址,假设a的地址为0xAABB,那么相当于:
int *p = (int *)0xAABB
要修改地址为0xAABB的内存值为0x123,只需要执行:
*p = 0x123;
要偷懒,连"*"号都不想写,怎么做呢:
#define PP (*(int *)0xAABB)
PP = 0x123;
所以,读写GPM4CON的实质,就是读写地址为0x110002E0的空间。
最后来看看Makefile:
led.bin : led.S main.c
arm-linux-gcc -c -o led.o led.S #预处理、编译、汇编、不链接
arm-linux-gcc -c -o main.o main.c #预处理、编译、汇编、不链接
arm-linux-ld -Tleds.lds -g led.o main.o -o led.elf
arm-linux-objcopy -O binary -S led.elf led.bin
arm-linux-objdump -D led.elf > led.dis
clean:
rm *.o *.elf *.dis *.bin
和上面那个Makefile文件几乎没有差别,这里不做详细介绍。
最后,执行make命令生成可执行文件 led.bin。然后,使用上面介绍的烧写SD卡的方法,将其烧写到SD中,插入SD卡到开发板,上电运行。查看LED1被点亮。
示例3:使用按键来控制LED
程序功能为:按下K1/K2/K3/K4则点亮LED1/LED2/LED3/LED4,松开按键则熄灭相应的LED——即用按键来控制灯。
key.c文件的代码如下:
#define GPM4CON (*(volatile int *)0x110002E0)
#define GPM4DAT (*(volatile int *)0x110002E4)
#define GPX3CON (*(volatile int *)0x11000C60)
#define GPX3DAT (*(volatile int *)0x11000C64)
int main()
{
unsigned int val = 0;
int i;
//配置GPM4CON的0~3引脚为输出
GPM4CON &= ~(0xFFFF);
GPM4CON |= 0x1111;
//配置GPX3CON的2~5引脚为输入
GPX3CON &= ~(0xFFFF00);
while(1)
{
val = GPX3DAT;
if(val & (1<<2))
{
GPM4DAT |= (0x1 << 0);
}else{
GPM4DAT &= ~(0x1 << 0);
}
if(val & (1<<3))
{
GPM4DAT |= (0x1 << 1);
}else{
GPM4DAT &= ~(0x1 << 1);
}
if(val & (1<<4))
{
GPM4DAT |= (0x1 << 2);
}else{
GPM4DAT &= ~(0x1 << 2);
}
if(val & (1<<5))
{
GPM4DAT |= (0x1 << 3);
}else{
GPM4DAT &= ~(0x1 << 3);
}
}
return 0;
}
上面的程序很简单,首先,设置GPM4的0/1/2/3引脚为输出,配置GPX3CON的2~5引脚为输入;然后再while循环里面判断是否有KEY按下,如果有则点亮相应的LED。不做详细解读。
好了,本文基本介绍完了。但是,应该有人会有疑问,为什么把链接地址设置为0x02023400,从SD卡启动时的CPU启动流程是什么? 以及烧写脚本是干什么的等等疑问?那我们就在下篇文章中做详细的解读。