今天我们来写第一个ARM裸板程序-点亮LED
我们怎样去点亮一个LED呢? 共分为三步 。
点亮LED需要通电源,同时为了保护LED,加个电阻减小电流。 控制LED灯的亮灭,可以手动开关LED,但在电子系统中,不可能让人来控制开关,而是通过编程,利用芯片的引脚去控制开关。
如图所示:
LED的驱动方式,常见的有四种:
方式1:使用引脚输出3.3V点亮LED,输出0V熄灭LED。
方式2:使用引脚拉低到0V点亮LED,输出3.3V熄灭LED。
有的芯片为了省电等原因,其引脚驱动能力不足,这时可以使用三极管驱动。
方式3:使用引脚输出1.2V点亮LED,输出0V熄灭LED。
方式4:使用引脚输出0V点亮LED,输出1.2V熄灭LED。
由此,主芯片引脚输出高电平/低电平,即可改变LED状态,而无需关注GPIO引脚输出的是3.3V还是1.2V。 所以简称输出1或0:
查看原理图:
由图明显可以看出当引脚nLED 1/2/4 输出为低电平时,二极管导通发光。(n表示低电平有效)
在原理图中,同名的Net表示是连在一起的。 因此我们要找到nLED 1同名net.
由图可知,三个LED灯nLED 1/2/4 分别连接GPF 4/5/6
查看S3C2440芯片手册:
由图可知,GPF输入输出端口共有8组端口,若以nLED_1为例,那我们要操作的就是第四组(即GPF4)。
怎么样令GPF4输出1或0?
1. 配置为输出引脚;
2. 设置状态;
由图可知,
设置GPFCON[9:8]=0b01,即将GPF4配置为输出;
设置GPFDAT[4]=1或者0,即输出高电平或低电平;
S3C2440启动流程:
- Nor启动:
Nor Flash的基地址为0,片内RAM地址为0x4000 0000;
CPU读出Nor上第1个指令(前4字节),然后执行;
CPU继续读出其它指令执行。
- Nand启动:
片内4k RAM基地址为0,Nor Flash不可访问;
2440硬件把Nand前4K内容复制到片内的RAM,然后CPU从0地址取出第1条指令执行。
在开始写第1个程序前,先了解一些概念。
2440是一个SOC,它里面的CPU有R1、R2、R3……等 寄存器(可以直接通过寄存器名字来访问);
它里面的GPIO控制器也有很多寄存器,如 GPFCON、GPFDAT。 (需要通过地址访问)
这两个寄存器是有差异的,在写代码的时候,CPU里面的寄存器可以直接访问,其它的寄存器要以地址进行访问。
然后我们根据芯片手册中的信息可知GPFCON寄存器的地址。
把GPF4配置为输出,需要把GPFCON的第9位和第8位为0b01,即将01 0000 0000=256=0x100写入GPFCON这个寄存器,而这个寄存器只能通过地址来访问,即写到0x5600 0050上;
把GPF4输出1,需要把GPFDAT第4位,即0x10写到GPFDAT寄存器中,GPFDAT寄存器也只能通过地址来访问,其地址为0x5600 0054上; 把GPF4输出0,需要把0x00写到地址0x5600 0054上; 这里的写法会破坏寄存器的其它位,其它位是控制其它引脚的,为了让第一个裸板程序尽可能的简单,才简单粗暴的这样处理。
写程序需要用到几条汇编代码:
①LDR (load):读寄存器
举例:LDR R0,[R1]
假设R1的值是x,读取以x为首地址的4个连续内存单元的数据(4字节),保存到R0中;
②STR (store):写寄存器
举例:STR R0,[R1]
假设R1的值是x,把R0的值写到以x为首地址的4个连续内存单元(4字节);
③B 跳转
④MOV (move)移动,赋值 举例1:MOV R0,R1 把R1的值赋值给R0;
举例2:MOV R0,#0x100 把0x100赋值给R0,即R0=0x100;
⑤LDR
举例:LDR R0,=0x12345678 这是一条伪指令,即实际中并不存在这个指令,他会被拆分成几个真正的ARM指令,实现一样的效果。 最后结果是R0=0x12345678。
为什么会引入伪指令?
在ARM的32位指令中,有些字节表示指令,有些字节表示数据,因此表示数据的没有32位,不能表示一个32位的任意值,只能表示一个较小的简单值,这个简单值称为立即数。引入伪指令后,利用LDR可以为R0赋任意大小值,编译器会自动拆分成真正的的指令,实现目的。
有了前面5个汇编指令的基础,我们就可以写代码了。
第一个程序只能是汇编,以前你们可能写过单片机程序,一上来就写main()函数,那是编译器帮你封装好了。
因此,我们写除了第一个控制led1亮的程序:
/*点亮led1*/
.text
.global _start
_start:
/*将GPF4设置为输出端口:将GPFCON配置寄存器中的第9、8位设为0、1即可,即把0x100送入
GPFCON寄存器的地址
0x56000050中*/
ldr r0,0x56000050
ldr r1,0x100
str r1,[r0]
/*将GPF4输出状态设置为输出低电平0:将GPFDAT寄存器第4位设置输出为低电平0,也就是将
0x00送入GPFDAT寄存器的地址0向6000054中去*/
ldr r0,0x56000054
ldr r1,0x00
str r1,[r0]
/*由于我们不知道在程序代码执行完毕之后,NOR FLASH或NAND FLASH的内容未知,
所以要执行死循环halt*/
halt:
b halt
将代码上传到服务器, 先编译:
arm-linux-gcc -c -o led_on.o led_on.s ;
再链接:
arm-linux-ld -Ttext 0 led_on.o -o led_on.elf ;
生成bin文件:
arm-linux-objcopy -O binary -S led_on.elf led_on.bin ;
以上的命令,要是我们每次都输入会容易输错,因此我们把他们写到一个文件里,这个文件就叫Makefile. 本次所需的Makefile如下:
all:
arm-linux-gcc -c -o led_on.o led_on.S
arm-linux-ld -Ttext 0 led_on.o -o led_on.elf
arm-linux-objcopy -O binary -S led_on.elf led_on.bin
clean:
rm *.bin *.o *.elf
以后只需要 使用 make 命令进行编译, make clean 命令进行清理。
最后烧写到开发板上,即可看到只有一个LED亮,符合我们预期。
前面介绍过伪指令,伪指令是实际不存在的ARM命令,编译器在编译时转换成存在的ARM指令。
我们可以通过反汇编来查看在实际执行过程中的汇编指令。
在前面的Makefile中加上:
arm-linux-objdump -D led_on.elf > led_on.dis
上传服务器,编译。
生成的led_on.dis就是反汇编文件。led_on.dis如下:
结合我们的程序代码来分析一下:图中第一列为地址,第二列为机器码、第三列为汇编代码。
ldr r0,0x56000050
ldr r1,0x100
str r1,[r0]
/***************/
ldr r0,0x56000054
ldr r1,0x00
str r1,[r0]
为了便于分析,我们先来认识一下CPU内部的寄存器:
cpu内部共有16个寄存器,其中我们经常用到的有三个:R13\R14\R15.
R13: 又名sp,即Stack Pointer,栈指针,保存的始终是栈顶的地址
R14: 又名lr, 即Link Register,保存的是返回地址,用于保护断点和现场。
R15:又名pc,即Program Pointer,保存的是cpu将要执行的下一条指令的地址。由于CPU执行的流水线工作方式,也就是说为了提高cpu执行指令的效率,在CPU执行‘存放在地址A处的指令’的同时,CPU还会对下一条指令(存放在地址为A+4的指令)进行译码,并将之后的第二条指令(存放在地址为A+8的指令)的地址(也就是A+8)送入PC中。
首先我们对反汇编后的第一条代码进行解析:将以pc+20为首地址的四个字节的数据送到R0寄存器中。对于第一条指令,它的地址为0,所以pc=0+8=8,那么pc+20=28=0x1c,反汇编程序的下半部分就是即为对应地址中的数据,所以显然0x1c处的数据为0x56000050,那么这条指令执行完,R0的值就变成了0x56000050.第一条指令的功能就是将GPFCON的地址送入寄存器R0中。
我们接着对第二条指令进行解析:第二条指令将伪指令ldr转换成了mov指令,其功能为将立即数256,也就是0x100送入了寄存器R1中.
第三条指令:由于R0=0x56000050,R1=0x100,第三条指令就是把0x100送入以0x56000050为首地址的四个字节中。就是将GPFCON寄存器的值设为0x100,这样GPF4就被设置成了输出端口。
第四条指令:由于其地址为c,所以,pc=c+8=0x14,则pc+12=0x20。因此此指令便将首地址为0x20的四个字节的数据放入寄存器R0中。所以指令执行后R0的值为0x56000054.
第五条指令:将立即数0x00放入寄存器R1中。
第六条指令:R0=0x00,R1=0x56000054,所以此指令便将0x00放入以0x56000054为首地址的四个字节中,也就是GPFDAT寄存器中。
分析完汇编指令,我们完成一个作业:通过修改汇编代码来点亮led2。
通过查看芯片手册,我们知道要想点亮led2,首先需要将GPFCON寄存器中的第11、10位设置为01,GPFCON寄存器设置为0x400;然后,经GPFDAT寄存器内容设置为0x00。代码如下:
/*点亮led1*/
.text
.global _start
_start
/*将GPF5设置为输出端口:将GPFCON配置寄存器中的第11、10位设为0、1即可,
即把0x400送入GPFCON寄存器的地址0x56000050中*/
ldr r0,0x56000050
ldr r1,0x140
str r1,[r0]
/*将GPF4输出状态设置为输出低电平0:将GPFDAT寄存器第5位设置输出为低电平0,
也就是将0x00送入GPFDAT寄存器的地址0向6000054中去*/
ldr r0,0x56000054
ldr r1,0x00
str r1,[r0]
/*由于我们不知道在程序代码执行完毕之后,NOR FLASH或NAND FLASH的内容
未知,所以要执行死循环halt*/
halt:
b halt
接下来解析一下机器码:
上图是四字节机器码的格式,我们先不管其他的只看低16位:
Rd表示的寄存器,4位可以表示16个寄存器;
Shift_operand表示操作数,其中12位中的高4位表示rotate_4位,低8位表示immed_8位。表示的立即数为immed_8右移2*rotate_4位后的数值。
如图:第一行机器代码为e5 9f 00 14,其中Rd=0,所以寄存器为R0;rotate_4为0,14表示源操作数中的立即数为0x14,所以立即数为0x14右移2*rotate_4位,结果还是0x14,十进制为20。
第二行中的机器代码为e3 a0 1c 01;其中Rd=1,所以寄存器为R1;rotate_4为c;immed_8为01;所以立即数为0x01右移2*rotate=24位,结果为0x100。
分析完机器代码,我们来修改一下机器代码来修改程序点亮led2:
将第二行机器代码中的0x100修改为0x400;也就是将rotate_4由c改为b即可。因为0x100要由0x01右移24位,而0x400由0x10右移22位。
字节序:
假设int a = 0x12345678;
前面说了16进制每位是4个字节,在内存中,是以8个字节作为1byte进行存储的,因此0x12345678中每两位作为1byte,其中0x78是低位,0x12是高位。
在内存中的存储方式有两种:
0x12345678的低位(0x78)存在低地址,即方式1,叫做小字节序(Little endian);
0x12345678的高位(0x12)存在低地址,即方式2,叫做大字节序(Big endian);
一般的arm芯片都是小字节序,对于2440可以设置某个寄存器,让整个系统使用大字节序或小字节序,它默认使用小字节序。
置位- 把a的bit7、8置位(变为1)
int a = 0x123; int b = a|(1<<7)|(1<<8);–> c=0x1a3
清位 -把a的bit7、8清位(变为0)
int a = 0x123; int b = (a& ~(1<<7))&(~(1<<8));–> c=0x23
置位和清位在后面寄存器的操作中,会经常使用。
接下来,我们来编写C程序来控制led。
我们写出了main函数,以下有几个问题需要我们仔细考虑一下:
a, 谁来调用它?
b. main函数中变量保存在内存中, 这个内存地址是多少?
答: 我们还需要写一个汇编代码, 给main函数设置内存, 调用main函数
led.c源码:
int main()
{
unsigned int *pGPFCON = (unsigned int *)0x56000050;
unsigned int *pGPFDAT = (unsigned int *)0x56000054;
/*配置GPF4为输出引脚*/
*pGPFCON = 0x100;
/*配置GPF4输出0*/
*pGPFDAT = 0;
return 0;
}
start.S源码:
.text
.global _start
_start:
/*设置内存:sp栈*/
ldr sp,=4096 /*nand启动*/
// ldr sp, =0x40000000 /*nor启动*/
/*调用main*/
bl main
halt:
b halt
Makefile源码:
all:
arm-linux-gcc -c -o led.o led.c
arm-linux-gcc -c -o start.o start.S
arm-linux-ld -Ttext 0 start.o led.o -o led.elf
arm-linux-objcopy -O binary -S led.elf led.bin
arm-linux-objdump -D led.elf > led.dis
clean:
rm *.bin *.o *.elf *.dis
最后将上面三个文件放入Ubuntu主机编译,然后烧写到开发板即可
1,add
add r0,r1,#4
功能:r0=r1+4
2,sub
sub r0,r1,#4
功能:r0=r1-4
3,bl (branch and link)
bl xxx
功能:第一步,将返回地址保存到 lr (r14)寄存器中;第二步,跳转到xxx
4,ldm 和 stm
ldmia sp, {fp,sp,pc}
功能:读内存,将值写入到多个寄存器中。
stmdb sp!, {fp,ip,lr,pc}
功能:把多个寄存器的内容写入到内存中。
我们把两条指令放到一起来讲:
指令有4种前缀,分别为
ia:Increment After过后增加
ib:Increment Before预先增加
da:Increment Before过后减少
db:Decrement Before预先减少
并且,对于两条指令而言都是高地址内存写入高编号寄存器、高编号寄存器写入高编号地址。
针对以上两条举例来讲解:
上面我们共写了三段代码,下面进行分析。
1,start.S
start.S中,实际上做了两件事情;
-设置栈
-调用了main函数,并把返回地址存入lr寄存器。
2,led.c
led.c中,实际上也做了一下三件事情:
-定义两个局部变量
-设置变量
-return 0
由此我们可能要问一下几个问题:
1)为何要设置栈?
因为C函数要用。
2)怎么使用栈?
一是存放局部变量,二是存放lr等寄存器的值。
3)调用者如何传参数给被调用者?
4)被调用者如何传返回值给调用者?
5)怎么从栈中恢复那些寄存器?
接下来,以上节编写的代码为例来分析整个过程:
Disassembly of section .text:
00000000 <_start>:
0: e3a0da01 mov sp, #4096 ; 0x1000`
4: eb000000 bl c
00000008 :
8: eafffffe b 8
0000000c :
c: e1a0c00d mov ip, sp
10: e92dd800 stmdb sp!, {fp, ip, lr, pc}
14: e24cb004 sub fp, ip, #4 ; 0x4
18: e24dd008 sub sp, sp, #8 ; 0x8
1c: e3a03456 mov r3, #1442840576 ; 0x56000000
20: e2833050 add r3, r3, #80 ; 0x50
24: e50b3010 str r3, [fp, #-16]
28: e3a03456 mov r3, #1442840576 ; 0x56000000
2c: e2833054 add r3, r3, #84 ; 0x54
30: e50b3014 str r3, [fp, #-20]
34: e51b2010 ldr r2, [fp, #-16]
38: e3a03c01 mov r3, #256 ; 0x100
3c: e5823000 str r3, [r2]
40: e51b2014 ldr r2, [fp, #-20]
44: e3a03000 mov r3, #0 ; 0x0
48: e5823000 str r3, [r2]
4c: e3a03000 mov r3, #0 ; 0x0
50: e1a00003 mov r0, r3
54: e24bd00c sub sp, fp, #12 ; 0xc
58: e89da800 ldmia sp, {fp, sp, pc}
Disassembly of section .comment:
00000000 <.comment>:
0: 43434700 cmpmi r3, #0 ; 0x0
4: 4728203a undefined
8: 2029554e eorcs r5, r9, lr, asr #10
c: 2e342e33 mrccs 14, 1, r2, cr4, cr3, {1}
10: Address 0x10 is out of bounds.
上面是我们将led.bin文件进行反汇编后的文件。结合图示来分析;
机器代码会被复制到cpu中的4K的SDRAM中,如下图所示:
片内4KRAM:
我们根据汇编指令行号来一行一行的分析:
00000000 <_start>:
0: e3a0da01 mov sp, #4096 ; 0x1000 //sp=4096
4: eb000000 bl c
//跳转到main函数,同时将返回地址,即下一条指令的地址存入lr寄存器中。pc=0x0c,lr=0x08
00000008 :
8: eafffffe b 8
0000000c :
c: e1a0c00d mov ip, sp //ip=sp=4096
10: e92dd800 stmdb sp!, {fp, ip, lr, pc}
//我们已经学习了stm指令;fp,ip,lr,pc分别为寄存器r11,r12,r14,r15,db后缀为先减后存;
则内存sp=4096-4=4092,4092-4095四个字节存放的即为pc的值,也就是0x10+8=0x18
同理,sp=sp-4=4088,lr应为0x08,所以4088-4091保存的是0x08;sp=sp-4=4084,ip=4096,所以,
4084-4087保存的值为4096(0x1000);sp=sp-4=4080,4080-4083保存的是fp的值,也就是一个
未知值。
14: e24cb004 sub fp, ip, #4 ; 0x4 //fp=ip-4=4092
18: e24dd008 sub sp, sp, #8 ; 0x8 //sp=sp-8=4080-4=4076
1c: e3a03456 mov r3, #1442840576 ; 0x56000000 //r3=0x56000000
20: e2833050 add r3, r3, #80 ; 0x50 //r3=r3+80=0x56000050
24: e50b3010 str r3, [fp, #-16]
//fp-16=4092-16=4076,r3=0x56000050;所以,此操作将0x56000050置入4076-4079四个字节中;
也就是将GPFCON寄存器的地址置入内存4076-4079;
28: e3a03456 mov r3, #1442840576 ; 0x56000000 //r3=0x56000000
2c: e2833054 add r3, r3, #84 ; 0x54 //r3=r3+84=0x56000054
30: e50b3014 str r3, [fp, #-20]
//fp-20=4072,r3=0x56000054;将0x56000054置入4072-4075内存中;
34: e51b2010 ldr r2, [fp, #-16]
//fp-16=4076,将4076-4079的值(0x56000050)放入r2寄存器中,r2=0x56000050
38: e3a03c01 mov r3, #256 ; 0x100 //r3=0x100
3c: e5823000 str r3, [r2]
//将0x100送入内存0x56000050中;将GPF4寄存器设置为输出端口;
40: e51b2014 ldr r2, [fp, #-20]
//fp-20=4072,将4072-4075的值送入寄存器r2寄存器,r2=0x56000054
44: e3a03000 mov r3, #0 ; 0x0 //r3=0x0
48: e5823000 str r3, [r2]
//将0置入内存0x56000054中,即将GPF4输出0
4c: e3a03000 mov r3, #0 ; 0x0 //r3=0
50: e1a00003 mov r0, r3 //r0=r3=0
54: e24bd00c sub sp, fp, #12 ; 0xc //sp=fp-12=4080
58: e89da800 ldmia sp, {fp, sp, pc}
//ldm,将内存中的值存入寄存器中,ia表示先存后减;4080-4083的值存入fp中,
也就等于之前fp的未知值,sp+4=4084;4084-4087的值存入sp中,sp=4096,sp+4=4088;
4088-4091的值存入pc中,pc=0x08;所以,最后一条指令执行完毕后,pc的值为返回地址,
系统会跳回返回地址处。
好了,暂且放过这个反汇编代码吧!下面我们修改一下代码,实现led1和led2的闪烁!
1,start_circle.S
.text
.global _start
_start:
ldr sp,=4096
loop: ldr r0,=4
bl led_on
ldr r0,=100000
bl delay
ldr r0,=5
bl led_on
ldr r0,=100000
bl delay
b loop
halt:
b halt
2,led_on_circle.c
void delay(volatile int n)
//volatile是为了避免编译器的优化
{
while(n--);
}
int led_on(int n)
{
unsigned int *pGPFCON = (unsigned int *)0x56000050;
unsigned int *pGPFDAT = (unsigned int *)0x56000054;
/*配置GPF4为输出引脚*/
if(n==4)
*pGPFCON = 0x100;
else
*pGPFCON = 0x400;
/*配置GPF4输出0*/
*pGPFDAT = 0;
return 0;
}
3,makefile
all:
arm-linux-gcc -c -o led.o led_on_circle.c
arm-linux-gcc -c -o start.o start_circle.S
arm-linux-ld -Ttext -0 start.o led.o -o led.elf
arm-linux-objcopy -O binary -S led.elf led.bin
arm-linux-objdump -D led.elf > led.dis
clean:
rm -rf *.bin *.o *.elf *.dis
要想实现led灯的闪烁,非常重要的一点就是要进行参数的传递。由于在cpu的16个寄存器中,r0-r3是用来保存参数的,所以,在调用函数前,我们要先把参数置入r0-r3寄存器中。
接下来呈上我们的反汇编代码:
led.dis:
led.elf: file format elf32-littlearm
Disassembly of section .text:
00000000 <_start>:
0: e3a0da01 mov sp, #4096 ; 0x1000
00000004 :
4: e3a00004 mov r0, #4 ; 0x4
8: eb000015 bl 64
c: e59f0018 ldr r0, [pc, #24] ; 2c <.text+0x2c>
10: eb000006 bl 30
14: e3a00005 mov r0, #5 ; 0x5
18: eb000011 bl 64
1c: e59f0008 ldr r0, [pc, #8] ; 2c <.text+0x2c>
20: eb000002 bl 30
24: eafffff6 b 4
00000028 :
28: eafffffe b 28
2c: 000186a0 andeq r8, r1, r0, lsr #13
00000030 :
30: e1a0c00d mov ip, sp
34: e92dd800 stmdb sp!, {fp, ip, lr, pc}
38: e24cb004 sub fp, ip, #4 ; 0x4
3c: e24dd004 sub sp, sp, #4 ; 0x4
40: e50b0010 str r0, [fp, #-16]
44: e51b3010 ldr r3, [fp, #-16]
48: e2433001 sub r3, r3, #1 ; 0x1
4c: e50b3010 str r3, [fp, #-16]
50: e51b3010 ldr r3, [fp, #-16]
54: e3730001 cmn r3, #1 ; 0x1
58: 0a000000 beq 60 0x30 >
5c: eafffff8 b 44 0x14>
60: e89da808 ldmia sp, {r3, fp, sp, pc}
00000064 :
64: e1a0c00d mov ip, sp
68: e92dd800 stmdb sp!, {fp, ip, lr, pc}
6c: e24cb004 sub fp, ip, #4 ; 0x4
70: e24dd00c sub sp, sp, #12 ; 0xc
74: e50b0010 str r0, [fp, #-16]
78: e3a03456 mov r3, #1442840576 ; 0x56000000
7c: e2833050 add r3, r3, #80 ; 0x50
80: e50b3014 str r3, [fp, #-20]
84: e3a03456 mov r3, #1442840576 ; 0x56000000
88: e2833054 add r3, r3, #84 ; 0x54
8c: e50b3018 str r3, [fp, #-24]
90: e51b3010 ldr r3, [fp, #-16]
94: e3530004 cmp r3, #4 ; 0x4
98: 1a000003 bne ac 0x48>
9c: e51b2014 ldr r2, [fp, #-20]
a0: e3a03c01 mov r3, #256 ; 0x100
a4: e5823000 str r3, [r2]
a8: ea000002 b b8 0x54>
ac: e51b2014 ldr r2, [fp, #-20]
b0: e3a03b01 mov r3, #1024 ; 0x400
b4: e5823000 str r3, [r2]
b8: e51b3018 ldr r3, [fp, #-24]
bc: e3a02000 mov r2, #0 ; 0x0
c0: e5832000 str r2, [r3]
c4: e3a03000 mov r3, #0 ; 0x0
c8: e1a00003 mov r0, r3
cc: e24bd00c sub sp, fp, #12 ; 0xc
d0: e89da800 ldmia sp, {fp, sp, pc}
Disassembly of section .comment:
00000000 <.comment>:
0: 43434700 cmpmi r3, #0 ; 0x0
4: 4728203a undefined
8: 2029554e eorcs r5, r9, lr, asr #10
c: 2e342e33 mrccs 14, 1, r2, cr4, cr3, {1}
10: Address 0x10 is out of bounds.
简单的分析一下吧!
00000000 <_start>:
0: e3a0da01 mov sp, #4096 ; 0x1000
00000004 :
4: e3a00004 mov r0, #4 ; 0x4
8: eb000015 bl 64
c: e59f0018 ldr r0, [pc, #24] ; 2c <.text+0x2c>
10: eb000006 bl 30
14: e3a00005 mov r0, #5 ; 0x5
18: eb000011 bl 64
1c: e59f0008 ldr r0, [pc, #8] ; 2c <.text+0x2c>
20: eb000002 bl 30
24: eafffff6 b 4
00000028 :
28: eafffffe b 28
2c: 000186a0 andeq r8, r1, r0, lsr #13
这节我们来写一个完整的led程序:
第一步:关闭看门狗;
看门狗(WATCHDOG)是一个定时器,就是当这个“看门狗”开启时,会倒计时,当倒计时到0时,就会重启整个系统。它有什么用呢?看门狗实际上是电子系统中保证系统正常运行的一种机制。当系统死机时,就无法“喂狗”,那么等到它倒计时0时,整个系统就会重启。
我们写一个完整的循环程序来控制led的闪烁,所以不需要看门狗,那就先把它关闭掉。
由芯片手册可知,看门狗,也就是WATCHDOG配置寄存器的地址为0x53000000,关闭只需将其置零即可。
第二步:将GPF4\5\6设置为输出端口;
查看原理图,led1\2\4其分别对应端口GPF4\5\6,所以要将其置为输出端口。根据芯片手册,要将GPFCON寄存器的13、12,11、10以及9、8都设置为01、01、01.
第三步:通过控制GPFDAT相应位的输出值来控制led的亮灭。
对应位置0,则led亮,反之则灭。
完整代码如下:
start.S
.text
.global _start
_start:
//关闭看门狗
ldr r0,=0x53000000
ldr r1,=0
str r1,[r0]
//设置内存sp栈;然后分辨是nor启动还是nand启动;
mov r1,#0
ldr r0,[r1] //备份0地址的值
str r1,[r1] //试图将0写入0地址,若能写入,则为nand启动,否则为nor启动;
ldr r2,[r1] //读出0地址的值
cmp r1,r2 //与0比较可知0是否被写入内存
ldr sp,=0x40000000+4096 //先假设是nor启动;
moveq sp,#4096 //若r1=r2,则代码指令复制到了片内RAM,所以可以进行写操作,因此是nand启动
streq r0,[r1] //将被破坏的0地址的内容还原
bl led_shine
halt:
b halt
led_shine.c:
void Delay(int n)
{
while(n--);
}
void led_shine()
{
volatile unsigned int *pGPFCON=(volatile int*)0x56000050;
volatile unsigned int *pGPFDAT=(volatile int*)0x56000054;
int val=0;
//设置GPF4\5\6为输出端口
*pGPFCON &= ~((3<<8)|(3<<10)|(3<<12));
*pGPFCON |= ((1<<8)|(1<<10)|(1<<12));
//循环点亮led1\2\4
while(1)
{
*pGPFDAT &= ~(7<<4);
*pGPFDAT |=(~val <<4);
Delay(100000);
++val;
if(val==8)
val=0;
}
}
makefile:
all:
arm-linux-gcc -c -o led.o led_shine.c
arm-linux-gcc -c -o start.o start.S
arm-linux-ld -Ttext -0 start.o led.o -o led.elf
arm-linux-objcopy -O binary -S led.elf led.bin
arm-linux-objdump -D led.elf > led.dis
clean:
rm -rf *.bin *.o *.elf *.dis
下面继续努力,写一下通过按键来控制led.
第一步:关闭看门狗(同上)
第二步:将GPF4\5\6设置为输出端口(同上)
第三步:将GPF0\2和GPG3设置为输入端口
通过查看原理图和芯片手册,我们知道三个按键ENIT0,ENIT2和ENIT11分别对应于GPF0,GPF2和GPG3,因此,要将这三个端口设置为输入端口。而且,当按键松开时,端口输入为1;当按键按下时,端口输入0。
第四步:根据按键输入来控制led
具体方法如下:分别从GPFDAT的第0、2位,GPGDAT的第3位获取ENIT0、ENIT2、ENIT11三个按键的输入状态,若为1,则说明对应按键松开,那么就将GPFDAT或GPGDAT中对应的位置1,则对应的led的输出端口就输出1,处于熄灭状态;若为0,则说明对应按键被按下,那么GPFDAT或GPGDAT中对应的位就应配置为0,使对应的led输出端口输出0,led被点亮。
完整代码如下:
start.S
.text
.global _start
_start:
//关掉看门狗
mov r0,#0x53000000
mov r1,#0
str r1,[r0]
//辨别启动方式
mov r1,#0
ldr r0,[r1]
str r1,[r1]
ldr r2,[r1]
cmp r1,r2
ldr sp,=0x40000000+4096
moveq sp,#4096
streq r0,[r1]
bl led_key
halt:
b halt
led_key_head.h
#define GPFCON (*((volatile unsigned int *)0x56000050))
#define GPFDAT (*((volatile unsigned int *)0x56000054))
#define GPGCON (*((volatile unsigned int *)0x56000060))
#define GPGDAT (*((volatile unsigned int *)0x56000064))
led_key.c
#include"led_key_head.h"
void Delay(volatile int n)
{
while(n--);
}
void led_key()
{
int val1,val2;
//将GPF4、5、6设置为输出引脚
GPFCON &= ~((3<<8)|(3<<10)|(3<<12));
GPFCON |= ((1<<8)|(1<<10)|(1<<12));
//将GPF0,GPF2,GPG3设置为输入引脚
GPFCON &= ~((3<<0)|(3<<4));
GPGCON &= ~(3<<6);
//不断循环获取按键输入信息,控制led的状态
while(1)
{
val1=GPFDAT;
val2=GPGDAT;
if(val1 & (1<<0))//若ENIT0没有按下,则输入为1,此时GPF6输出1,即不亮
GPFDAT |= (1<<6);
else //反之,则亮;
GPFDAT &= ~(1<<6);
if(val1 & (1<<2))//若ENIT2没有按下,则输入为1,此时GPF5输出1,即不亮;
GPFDAT |= (1<<5);
else
GPFDAT &= ~(1<<5);
if(val2 & (1<<3))//若ENIT11没有按下,则输入为1,此时GPF4输出1,即不亮;
GPFDAT |= (1<<4);
else
GPFDAT &= ~(1<<4);
}
}
makefile:
all:
arm-linux-gcc -c -o led.o led_key.c
arm-linux-gcc -c -o start.o start.S
arm-linux-ld -Ttext -0 start.o led.o -o led.elf
arm-linux-objcopy -O binary -S led.elf led.bin
arm-linux-objdump -D led.elf > led.dis
clean:
rm -rf *.bin *.o *.elf *.dis