开发板学习Day7-第一个ARM裸板程序及引申

今天我们来写第一个ARM裸板程序-点亮LED
我们怎样去点亮一个LED呢? 共分为三步 。

  • 看原理图,确定控制LED的引脚;
  • 看主芯片的芯片手册,确定如何设置控制这个引脚;
  • 写具体的程序来实现;

第001节:硬件知识-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:

  • 逻辑1–>高电平
  • 逻辑0–>低电平

第002节:硬件知识-S3C2440启动流程与GPIO操作

查看原理图:
这里写图片描述
由图明显可以看出当引脚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. 设置状态;

首先查看芯片手册:找到GPF的配置寄存器GPFCON。
这里写图片描述

这里写图片描述

由图可知,
设置GPFCON[9:8]=0b01,即将GPF4配置为输出;
设置GPFDAT[4]=1或者0,即输出高电平或低电平;

S3C2440框架:
这里写图片描述

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条指令执行。

第003节:编写第1个程序点亮LED

在开始写第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亮,符合我们预期。
这里写图片描述

第004节_汇编与机器码

前面介绍过伪指令,伪指令是实际不存在的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位。
这里写图片描述

第005节编程知识进制(略)

第006节编程知识字节序_位操作

字节序:
假设int a = 0x12345678;
前面说了16进制每位是4个字节,在内存中,是以8个字节作为1byte进行存储的,因此0x12345678中每两位作为1byte,其中0x78是低位,0x12是高位。
在内存中的存储方式有两种:
这里写图片描述
0x12345678的低位(0x78)存在低地址,即方式1,叫做小字节序(Little endian);
0x12345678的高位(0x12)存在低地址,即方式2,叫做大字节序(Big endian);
一般的arm芯片都是小字节序,对于2440可以设置某个寄存器,让整个系统使用大字节序或小字节序,它默认使用小字节序。

  1. 置位- 把a的bit7、8置位(变为1)
    int a = 0x123; int b = a|(1<<7)|(1<<8);–> c=0x1a3

  2. 清位 -把a的bit7、8清位(变为0)
    int a = 0x123; int b = (a& ~(1<<7))&(~(1<<8));–> c=0x23
    置位和清位在后面寄存器的操作中,会经常使用。

第007节_编写C程序控制LED

接下来,我们来编写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主机编译,然后烧写到开发板即可

第008节_几条汇编指令_bl_add_sub_ldm_stm

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预先减少

并且,对于两条指令而言都是高地址内存写入高编号寄存器、高编号寄存器写入高编号地址。

针对以上两条举例来讲解:

这里写图片描述
其中,sp!表示sp的最终值为修改后的值。

这里写图片描述
其中,sp表示sp的值保持不变。

009节_解析C程序的内部机制

上面我们共写了三段代码,下面进行分析。

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 <main>

00000008 <halt>:
   8:   eafffffe    b   8 <halt>

0000000c <main>:
   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>                
   //跳转到main函数,同时将返回地址,即下一条指令的地址存入lr寄存器中。pc=0x0c,lr=0x08

00000008 <halt>:
   8:   eafffffe    b   8 <halt>

0000000c <main>:
   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=40924092-4095四个字节存放的即为pc的值,也就是0x10+8=0x18
  同理,sp=sp-4=4088,lr应为0x08,所以4088-4091保存的是0x08;sp=sp-4=4084,ip=4096,所以,
  4084-4087保存的值为40960x1000;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-407928:   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 <loop>:
   4:   e3a00004    mov r0, #4  ; 0x4
   8:   eb000015    bl  64 <led_on>
   c:   e59f0018    ldr r0, [pc, #24]   ; 2c <.text+0x2c>
  10:   eb000006    bl  30 <delay>
  14:   e3a00005    mov r0, #5  ; 0x5
  18:   eb000011    bl  64 <led_on>
  1c:   e59f0008    ldr r0, [pc, #8]    ; 2c <.text+0x2c>
  20:   eb000002    bl  30 <delay>
  24:   eafffff6    b   4 <loop>

00000028 <halt>:
  28:   eafffffe    b   28 <halt>
  2c:   000186a0    andeq   r8, r1, r0, lsr #13

00000030 <delay>:
  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 <delay+0x30>
  5c:   eafffff8    b   44 <delay+0x14>
  60:   e89da808    ldmia   sp, {r3, fp, sp, pc}

00000064 <led_on>:
  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 <led_on+0x48>
  9c:   e51b2014    ldr r2, [fp, #-20]
  a0:   e3a03c01    mov r3, #256    ; 0x100
  a4:   e5823000    str r3, [r2]
  a8:   ea000002    b   b8 <led_on+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 <loop>:
   4:   e3a00004    mov r0, #4  ; 0x4
   8:   eb000015    bl  64 <led_on>
   c:   e59f0018    ldr r0, [pc, #24]   ; 2c <.text+0x2c>
  10:   eb000006    bl  30 <delay>
  14:   e3a00005    mov r0, #5  ; 0x5
  18:   eb000011    bl  64 <led_on>
  1c:   e59f0008    ldr r0, [pc, #8]    ; 2c <.text+0x2c>
  20:   eb000002    bl  30 <delay>
  24:   eafffff6    b   4 <loop>

00000028 <halt>:
  28:   eafffffe    b   28 <halt>
  2c:   000186a0    andeq   r8, r1, r0, lsr #13

010节完善LED程序编写按键程序

这节我们来写一个完整的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

猜你喜欢

转载自blog.csdn.net/rdgfdd/article/details/80458073