tiny4412开发板GPIO试验

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配置寄存器

  • 当GPA0CON的bit[31:28]值为0b0000时,引脚GPA0被设置为输入引脚;
  • 当GPA0CON的bit[31:28]值为0b0001时,引脚GPA0被设置为输出引脚;
  • 当GPA0CON的bit[31:28]值为0b0010时,引脚GPA0被设置为特殊功能UART引脚;
  • 当GPA0CON的bit[31:28]值为0b0011时,引脚GPA0被设置为特殊功能I2C引脚;
  • 当GPA0CON的bit[31:28]值为0b1111时,引脚GPA0被设置为中断引脚。

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。不做详细解读。

  1. 按前述方法把led.bin烧入SD卡,并用它来启动开发板;
  2. 按下/松开KEY1/2/3/4,观察LED1/2/3/4的状态。

好了,本文基本介绍完了。但是,应该有人会有疑问,为什么把链接地址设置为0x02023400,从SD卡启动时的CPU启动流程是什么? 以及烧写脚本是干什么的等等疑问?那我们就在下篇文章中做详细的解读。

猜你喜欢

转载自blog.csdn.net/caihaitao2000/article/details/84201820