今天在看韦东山老师的视频教程,特将关于GPIO操作的详细步骤写下来,以做总结。
一、用汇编语言点亮一个led灯
1.led原理图
在本次实验中,需要点亮的led为nLED_1,由原理图可知,只需将nLED_1置为低电平即可点亮,此时,需GPF4配置为输出,且输出为低电平。
详细的汇编源码
1 @******************************************************************************
2 @ File:led_on.S
3 @ 功能:LED点灯程序,点亮LED1
4 @******************************************************************************
5
6 .text
7 .global _start
8 _start:
9 LDR R0,=0x56000050 @ R0设为GPFCON寄存器。此寄存器
10 @ 用于选择端口B各引脚的功能:
11 @ 是输出、是输入、还是其他
12 MOV R1,#0x00000100
13 STR R1,[R0] @ 设置GPF4为输出口, 位[8:7]=0b01
14
15 LDR R0,=0x56000054 @ R0设为GPBDAT寄存器。此寄存器
16 @ 用于读/写端口B各引脚的数据
17 MOV R1,#0x00000000 @ 此值改为0x00000010,
18 @ 可让LED1熄灭
19 STR R1,[R0] @ GPF4输出0,LED1点亮
20 MAIN_LOOP:
21 B MAIN_LOOP
代码详解:
.text 定义一个代码段
.global 定义一个全局入口
_start 全局入口处
汇编指令详解
LDR
LDR R1,[R2]将R2的值作为地址,取出此地址中的数据保存在R1中。寄存器为操作数的地址指针。
LDR R0,=0x56000050等价于R0=0X56000050
LDR R2,[R3,#0X0F]将R3的值加0x0f作为地址,然后取出该地址的数据保存在R2中
STR R1,[R0,#-2]将R0的值减2作为地址,把R1中的内容保存到此地址位置
mov r1,#0
STR r0,[r1, #0x10];r1+0x10这个是所用的实际地址值,但是不会写入r1,在此句之后,r1=0。
STR r0,[r1], #0x10;r1+0x10这个是所用的实际地址值,这个值会写入r1,此句之后,r1=0x10
B 相对跳转指令,跳转到制定的地址执行程序
B MAIN_LOOP跳转到MAIN_LOOP标号处
B 0X1234跳转到绝对地址0x1234处
常用的代码段:
.text 代码段
.const 只读数据段(有些编译器不使用此段,将只读数据并入.data段)
.data 读写数据段
.bss 堆
Makefile文件源码
led_on.bin : led_on.S
arm-linux-gcc -g -c -o led_on.o led_on.S
arm-linux-ld -Ttext 0x0000000 -g led_on.o -o led_on_elf
arm-linux-objcopy -O binary -S led_on_elf led_on.bin
clean:
rm -f led_on.bin led_on_elf *.o
源码详解:
-g 表示加入调试信息
-c 编译不链接
-o 表示输出文件
arm-linux-ld 用于将多个目标文件,库文件链接成可执行文件
-Ttext 0x0000000表示代码段的地址为0x0000000
-Ttext是连接时将初始地址重定向为其它地址(若不注明此,则程序的起始地址为0)。假设,在mbr_start.S文件中函数inb()的编译完成后在mbr_start.o中的偏移地址是0x006b,则在连接时指定-Ttext=0x7c00,连接之后其地址为0x7c6b, 其他函数调用此函数时,也就会调用地址0x7c6b,而不会是0x006b。总之,程序连接后的地址空间要与其运行环境的地址空间相匹配,而-Ttext的功能就是指定程序段的起始地址。
arm-linux-objcopy 被用来复制一个目标文件的内容到另外一个文件中,可以使用不同于源文件的格式来输出目的文件,即可以进行格式转换。在此是将led_on_elf格式的文件转换为二进制文件格式。
-0 表示以指定的格式输出文件
-S 表示不从源文件中复制重定位信息和符号信息到目标文件中。
什么是重定位?
在装入程序前,系统会计算未使用的内存,然后将程序装入,并记下开始地址。在执行有相对地址的指令时,会将所有的地址加个刚才记下的开始地址,就叫重定位。
重定位与EXE文件无关,它是程序装入时的一种方式。利用重定位可提高内存空间的使用率。
如果要把文件烧写到flash中,需要将文件转化为二进制文件
-0 binary表输出二进制文件
为什么把代码段的地址写为0呢?这涉及到2440的启动方式,主要有两种启动方式
1)Nand启动
硬件强制把Nand Flash中的前4k内容拷贝到2440中的SRAM中,然后CPU从SRAM中的0地址开始执行。
2)Nor启动
Nor Flash可以像内存一样读数据,但是不能像内存一样写数据,可以通过其它方式写数据。
从Nor启动时,CPU也是从0地址开始启动,但此时的0地址位于NorFlash中。
程序编译过程
1)预处理(语法分析等处理...)
2)编译(.c文件编译为.s文件)
3)汇编(.s文件汇编为.o文件,即二进制文件)
4)链接(把多个.o文件合并成可执行文件,如bin文件)
二、用C语言点亮一个led灯
原理图和上述一样。
启动文件代码:
@******************************************************************************
@ File:crt0.S
@ 功能:通过它转入C程序
@******************************************************************************
.text
.global _start
_start:
ldr r0, =0x53000000 @ WATCHDOG寄存器地址
mov r1, #0x0
str r1, [r0] @ 写入0,禁止WATCHDOG,否则CPU会不断重启
ldr sp, =1024*4 @ 设置堆栈,注意:不能大于4k, 因为现在可用的内存只有4K
@ nand flash中的代码在复位后会移到内部ram中,此ram只有4K
bl main @ 调用C程序中的main函数
halt_loop:
b halt_loop
在这里为什么要设置堆栈?
通过查看反汇编语言可看到
Disassembly of section .text:
00000000 <_start>:
0: e3a00453 mov r0, #1392508928 ; 0x53000000
4: e3a01000 mov r1, #0 ; 0x0
8: e5801000 str r1, [r0]
c: e3a0da01 mov sp, #4096 ; 0x1000
10: eb000000 bl 18 <main>
00000014 <halt_loop>:
14: eafffffe b 14 <halt_loop>
00000018 <main>:
18: e1a0c00d mov ip, sp
1c: e92dd800 stmdb sp!, {fp, ip, lr, pc}
20: e24cb004 sub fp, ip, #4 ; 0x4
24: e3a03456 mov r3, #1442840576 ; 0x56000000
28: e2833050 add r3, r3, #80 ; 0x50
2c: e3a02c01 mov r2, #256 ; 0x100
30: e5832000 str r2, [r3]
34: e3a03456 mov r3, #1442840576 ; 0x56000000
38: e2833054 add r3, r3, #84 ; 0x54
3c: e3a02000 mov r2, #0 ; 0x0
40: e5832000 str r2, [r3]
44: e3a03000 mov r3, #0 ; 0x0
48: e1a00003 mov r0, r3
4c: e89da800 ldmia sp, {fp, sp, pc}
Disassembly of section .comment:
在执行bl main指令后,跳转到main函数,就执行stmdbsp!,{fp,ip,lr,pc},把fp,ip,ir,pc寄存器放到堆栈中,若不设置会出错。另外,汇编程序可以直接访问寄存器,而C语言函数的调用则是通过入栈和出栈来完成的,所以在进入main函数之前需要设置堆栈。此处觉得理解的还不够深入,有待提高。(为什么sp=4096?)
BL 带连接的跳转指令,该指令将下一条指令的地址拷贝到R14(即LR)链接寄存器中,然后跳转到指定位置执行程序,程序执行完后根据之前拷贝的地址返回来执行下一条指令。B跳转指令限制在当前指令的上下32MB的范围内。BL指令用于子程序调用。
main函数源文件:
1 #define GPFCON (*(volatile unsigned long *)0x56000050)
2 #define GPFDAT (*(volatile unsigned long *)0x56000054)
3
4 int main()
5 {
6 GPFCON = 0x00000100; // 设置GPF4为输出口, 位[9:8]=0b01
7 GPFDAT = 0x00000000; // GPF4输出0,LED1点亮
8
9 return 0;
10 }
用C程序实现比较简单,为什么此处要使用volatile呢?
volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
使用该关键字的例子如下:
int volatile nVint;
当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。
例如:
volatile int i=10;
int a = i;
...
//其他代码,并未明确告诉编译器,对i进行过操作
int b = i;
volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样一来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。
三、C语言点亮三个led灯
启动文件源码:
@******************************************************************************
@ File:crt0.S
@ 功能:通过它转入C程序
@******************************************************************************
.text
.global _start
_start:
ldr r0, =0x53000000 @ WATCHDOG寄存器地址
mov r1, #0x0
str r1, [r0] @ 写入0,禁止WATCHDOG,否则CPU会不断重启
ldr sp, =1024*4 @ 设置堆栈,注意:不能大于4k, 因为现在可用的内存只有4K
@ nand flash中的代码在复位后会移到内部ram中,此ram只有4K
bl main @ 调用C程序中的main函数
halt_loop:
b halt_loop
C语言源码:
#define GPFCON (*(volatile unsigned long *)0x56000050)
#define GPFDAT (*(volatile unsigned long *)0x56000054)
#define GPF4_out (1<<(4*2))
#define GPF5_out (1<<(5*2))
#define GPF6_out (1<<(6*2))
void wait(volatile unsigned long dly)
{
for(; dly > 0; dly--);
}
int main(void)
{
unsigned long i = 0;
GPFCON = GPF4_out|GPF5_out|GPF6_out; // 将LED1,2,4对应的GPF4/5/6三个引脚设为输出
while(1){
wait(30000);
GPFDAT = (~(i<<4)); // 根据i的值,点亮LED1,2,4
if(++i == 8)
i = 0;
}
return 0;
}
Makefile文件源码:
1 CFLAGS := -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -ffreestanding
2 leds.bin : crt0.S leds.c
3 arm-linux-gcc $(CFLAGS) -c -o crt0.o crt0.S
4 arm-linux-gcc $(CFLAGS) -c -o leds.o leds.c
5 arm-linux-ld -Ttext 0x0000000 crt0.o leds.o -o leds_elf
6 # arm-linux-ld -Tleds.lds crt0.o leds.o -o leds_elf
7 arm-linux-objcopy -O binary -S leds_elf leds.bin
8 arm-linux-objdump -D -m arm leds_elf > leds.dis
9 clean:
10 rm -f leds.dis leds.bin leds_elf *.o
在arm-linux-gcc后面出现$(CFLAGS),CFLAGS 你可以看成是gcc(或gcc同类编译器)编译命令的一部分。通常情况下我们编译源代码的时候因为需求不同等原因会加上各种各样选项和参数,而通过修改CFLAGS这一变量来达到在它作用域范围内修改编译命令的目的。CFLAGS不是makefile的关键字,他是一个变量,这个名称可以随便起,你可以起CFLAGSABCDEFG,只要你在编译时,用$引用这个变量就可以了,CFLAGS
没有什么具体含义.
arm-linux-objdump用于显示二进制文件信息
-D 把所有的段都反汇编
-m machine 指定反汇编目标文件时所使用的架构。可以用“-i”列出支持的架构
在做具体的实验时,由于对CFLAGS := -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -ffreestanding的轻视导致我在本实验耗费了很长时间,查资料时觉得$(CFLAGS)不重要,所以就没有细细的分析。这也让我认识到了细节的重要性,存在就有道理...呵呵呵...在此另起章节对GCC编译器的选项做进一步的说明。
四、用按键控制LED灯
原理图
其中EINT11接GPG3
key_led.c文件
#define GPFCON (*(volatile unsigned long *)0x56000050)
#define GPFDAT (*(volatile unsigned long *)0x56000054)
#define GPGCON (*(volatile unsigned long *)0x56000060)
#define GPGDAT (*(volatile unsigned long *)0x56000064)
/*
* LED1,LED2,LED4对应GPF4、GPF5、GPF6
*/
#define GPF4_out (1<<(4*2))
#define GPF5_out (1<<(5*2))
#define GPF6_out (1<<(6*2))
#define GPF4_msk (3<<(4*2))
#define GPF5_msk (3<<(5*2))
#define GPF6_msk (3<<(6*2))
/*
* S2,S3,S4对应GPF0、GPF2、GPG3
*/
#define GPF0_in (0<<(0*2))
#define GPF2_in (0<<(2*2))
#define GPG3_in (0<<(3*2))
#define GPF0_msk (3<<(0*2))
#define GPF2_msk (3<<(2*2))
#define GPG3_msk (3<<(3*2))
int main()
{
unsigned long dwDat;
// LED1,LED2,LED4对应的3根引脚设为输出
GPFCON &= ~(GPF4_msk | GPF5_msk | GPF6_msk);
GPFCON |= GPF4_out | GPF5_out | GPF6_out;
// S2,S3对应的2根引脚设为输入
GPFCON &= ~(GPF0_msk | GPF2_msk);
GPFCON |= GPF0_in | GPF2_in;
// S4对应的引脚设为输入
GPGCON &= ~GPG3_msk;
GPGCON |= GPG3_in;
while(1){
//若Kn为0(表示按下),则令LEDn为0(表示点亮)
dwDat = GPFDAT; // 读取GPF管脚电平状态
if (dwDat & (1<<0)) // S2没有按下
GPFDAT |= (1<<4); // LED1熄灭
else
GPFDAT &= ~(1<<4); // LED1点亮
if (dwDat & (1<<2)) // S3没有按下
GPFDAT |= (1<<5); // LED2熄灭
else
GPFDAT &= ~(1<<5); // LED2点亮
dwDat = GPGDAT; // 读取GPG管脚电平状态
if (dwDat & (1<<3)) // S4没有按下
GPFDAT |= (1<<6); // LED3熄灭
else
GPFDAT &= ~(1<<6); // LED3点亮
}
return 0;
}
启动文件crt0.S
@******************************************************************************
@ File:crt0.S
@ 功能:通过它转入C程序
@******************************************************************************
.text
.global _start
_start:
ldr r0, =0x53000000 @ WATCHDOG寄存器地址
mov r1, #0x0
str r1, [r0] @ 写入0,禁止WATCHDOG,否则CPU会不断重启
ldr sp, =1024*4 @ 设置堆栈,注意:不能大于4k, 因为现在可用的内存只有4K
@ nand flash中的代码在复位后会移到内部ram中,此ram只有4K
bl main @ 调用C程序中的main函数
halt_loop:
b halt_loop