一、(8-2)
一个芯片上面有片内SRAM内存(4K),NOR Flash(2M) , Nand控制器(256M),GPIO控制器 启动过程:(大多数ARM芯片从0地址启动) 1、NOR 启动, NOR Flash基址为0 CPU读取NOR上第一个指令(前4字节)执行,CPU继续读取其他指令执行。
2、NAND 启动, 片内4K RAM基地址为0,它会把NandFlash前面4K的内容拷贝到RAM中来,然后CPU从0地址取出第一条指令执行。
总结:也就是我设为Nor启动的时候,我的NOR FLASH上面0地址才是基地址,而片内RAM的地址为0x4000 0000 = 1GB;而用到NAND FLASH的时候是用的片内RAM,4K的片内RAM的基地址0开始运行,NOR FLASH不可访问。
1KB = 1024B = 2^10B = 0x400 1MB = 1024 * 1024 = 2^20B = 0X10000 = 1 0000 0000 0000 0000 0000(二进制) 1GB = 2^30B
二、仅通过汇编指令实现点灯(8-3)
led_on.s
.text
.global _start
_start:
ldr r1 ,=0x56000050
mov r0, #0x100
str r0, [r1]
ldr r1 ,=0x56000054
ldr r0, =0
str r0, [r1]
halt:
b halt
.text 部分是处理器开始执行代码的地方,指定了后续编译出来的内容放在代码段【可执行】,是arm-gcc编译器的关键词。 .global关键字用来让一个符号对链接器可见,可以供其他链接对象模块使用;告诉编译器后续跟的是一个全局可见的名字【可能是变量,也可以是函数名】 global _start 让 _start 符号成为可见的标识符,这样链接器就知道跳转到程序中的什么地方并开始执行。 _start是默认起始地址,也是编译、链接后程序的起始地址。由于程序是通过加载器来加载的,必须要找到 _start名字的函数,因此_start必须定义成全局的,以便存在于编译后的全局符号表中,供其它程序【如加载器】寻找到
arm-linux-gcc -c - o led_on.o led_on.S //编译 arm-linux-ld -Ttest 0 led_on.o -o led_on.elf //链接 arm-linux-objcopy - o binaru -S led_on.elf led_on.bin //生成bin文件 可以写在一个makefile中
all:
arm-linux-gcc -c -o led_on.o led_on.S
arm-linux-ld -Ttest 0 led_on.o -o led_on.elf
arm-linux-objcopy -o binaru -S led_on.elf led_on.bin
clean:
rm *.bin *.o *.elf
生成 elf 文件并不是能直接用在嵌入式平台上面裸跑的,因为我们并没有操作系统,我们不需要elf文件头的那些指示信息提供给操作系统(在linux上头部会有魔数,告诉操作系统我是JAVA还是elf文件还是#!/bin/bash类型的文件),指示系统怎么去加载文件,在嵌入式上面的完全没有那个必要,只需要将实际的代码提取出来,直接运行就OK,也就是 objcopy的操作。
三、汇编与机器码(8-4)
前面说到ldr是一个伪指令,我们将elf文件反汇编一下查看一下真正汇编指令
all:
arm-linux-gcc -c -o led_on.o led_on.S
arm-linux-ld -Ttest0 led_on.o -o led_on.elf
arm-linux-objcopy -o binaru -S led_on.elf led_on.bin
arm-linux-objcopy -D led_on.elf > led_on.dis
clean:
rm *.bin *.o *.elf
查看反汇编dis文件的内容 第一列是地址,第二列是机器码,第三列是汇编码
pc = 当前指令地址+8 因为ARM是以流水线的方式运行的 当前执行地址A的指令,已经在对地址A+4 的指令进行译码,已经在读取A+8(PC的当前值)的指令。
0地址的指令可以变成 r1 = [ pc + 20 ] = [ 8 + 20 ] = [0x1c] 去0x1c地址去读取它的内存的值 = 0x5600 0050 8地址就是把0x100写到0x56000050地址中去 c地址上 r1 = [0xc + 8 + 12 ] = [ 32 ] = [ 0x20 ] 去0x20地址去读取它的内存的值 = 0x5600 0054
我们看到旁边的机器码,在bin文件通过16进制可以看到与反汇编中机器码是一致的,编译器把这些伪指令转换成真正的汇编码 如果想要电量GPF5的话需要写0x400到0x5600 0050 查看MOV的机器码。发现影响立即数的就是最后的0-11位 这12位如何表示:分为高4位(rotate移位数),低8位(immed_8) 立即数= immed_8 循环右移(2 * rotate)位 = 1 << (1100 = 12) = 1右移24位 = 31个0 1 右移24位后就是 0x100 = (23个0) 1 (8个0) 因此我们少移动2位的话就是0x400 = 1 循环右移22位
可以总结 C/汇编语言是给人类看的 —> bin文件是给机器看的,CPU只管机器码
四、用c语言实现点灯程序(8-7)
int main()
{
unsigned int *pGPFCON = (unsigned int *)0x56000050;
unsigned int *pGPFDAT = (unsigned int *)0x56000054;
*pGPFCON = 0x100;
*pGPFDAT = 0;
return 0;
}
a. 我们写出了main函数, 谁来调用它? b. main函数中变量保存在内存中, 这个内存地址是多少? 答: 我们还需要写一个汇编代码, 给main函数设置内存, 调用main函数
还需要写一个汇编代码,给main函数设置内存,调用main函数
.text
.global _start
_start:
ldr sp, =4096
bl main
halt:
b halt
局部变量都应该存放在栈中
设置为NAND启动的时候使用片内的4K内存,也就是4096,我们把栈 设置在最顶部 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
五、解析C程序的内部机制,汇编和C如何调用起来的
首先我们知道
start.s作用: (1) 设置栈 (2)调用main并把返回地址保存在lr中
led.c : main : (1)定义两个局部变量 (2)设置变量 (3)return 0
那就有几个疑问了: (1)为何要设置栈
因此C函数要用
(2)怎么使用栈
a.保存局部变量 b.保存lr等寄存器
(3)a.被调用如何把返回值返回给调用者?b.调用者如何把参数传给被调用者?
ATPCS:ARM-THUMB procedure call standard(ARM-Thumb过程调用标准) 调用者 通过r0-r3传给或传回 被调用者,r4-r11可能被使用,所以在函数的入口保存他们,在出口恢复他们。 (下面解析代码的时候main函数中的return 0就是保存在)
bl main 返回地址保存在lr寄存器里面,假设main函数也调用其他函数,调用完其他函数后,他也应该返回地址到lr上,main函数的返回地址就被子函数的返回地址覆盖了
dis反汇编文件(我们来开始一条一条的分析执行过程):
led.elf: file format elf32-littlearm
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.
我们可以看到栈中前面几位是用来保存寄存器的值的,函数返回之前会从这里恢复寄存器,下面的内容就是局部变量,sp进去之前是4096,出来后还是4096
bin文件中最后一个确实是e89da800,elf中的comment段并没有放进去,comment的中文名字叫做注释,bin文件肯定不需要注释
现在的代码只涉及被调用者给调用者返回值,那调用者如何传参给被调用者呢?
直接通过r0传入就可以了,见下面的代码,都是通过r0传入来实现不同的形参传入效果
C代码:
void delay(volatile int d)
{
while (d--);
}
int led_on(int which)
{
unsigned int *pGPFCON = (unsigned int *)0x56000050;
unsigned int *pGPFDAT = (unsigned int *)0x56000054;
if (which == 4)
{
*pGPFCON = 0x100;
}
else if (which == 5)
{
*pGPFCON = 0x400;
}
*pGPFDAT = 0;
return 0;
}
汇编代码:
.text
.global _start
_start:
ldr sp, =4096
mov r0, #4
bl led_on
ldr r0, =100000
bl delay
mov r0, #5
bl led_on
halt:
b halt
因为我们的程序特别小,设置栈是向下生长的,只要跟我们的程序代码部分不冲突就可以了。
六、实现按键程序(看门口)
看门口:通过定时器去保持系统稳定,他在倒数,当倒数到0之前你要去设置它,如果到0的话他就会复位我们的系统,避免系统卡死。
norflash可以理解成硬盘一样的东西,可以像内存一样读,但不能想内存那样去写,如果一个硬盘很容易去写的话,那岂不是很容易被破坏,所以要写的时候需要发送一定的格式才可以。所以根据这个特性,nor不可以去写,那我们去写一个值到0地址,如果读取出来不是0那就是nand如果是0那就是nor
.text
.global _start
_start:
ldr r0, =0x53000000
ldr 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 main
halt:
b halt
附:汇编指令
LDR 读内存
LDR R0,[R1] //假设R1的值是x,读取地址x上的数据(4字节),保存到R0
STR 写内存命令
STR R0, [R1] 假设R1的值是x,把R0的值写到地址x(4字节)
B 跳转
MOV
MOV R0 ,R1 // 把R1的值赋值给R0 MOV R0, #100 // R0 = 0x100
MOV和LDR的区别
R0, = 0x12345678 // R0 = 0x12345678
伪指令,它会被拆分为几条真正的ARM指令,如果用MOV对于32位的指令,他有位表示MOV有几位表示R0,那剩下的不足32位村放不下0x12345678,不可以表示任意值,只能表示简单值比如0x100就是简单值(这个简单值叫做立即数),所以如果用MOV R0,0x12345678这是错误的。
add
add r0, r1 ,#4 // r0 = r1 + 4 add r0, r1, r2 // r0 = r1 + r2
sub
sub r0, r1 ,#4 // r0 = r1 - 4 sub r0, r1, r2 // r0 = r1 - r2
BL: brarch and link
bl xxx -> (1 )跳转到xxx (2) 把 返回地址(下一条指令的地址)保存到 lr 寄存器中
ldm、stm (8-8)
ldmia stmdb m表示many,ldr的时候一次只能操作一个寄存器
ldm 读内存,写入多个寄存器
stm 把多个寄存器的值写入内存
ia 、db
stmdb sp!,{fp, ip, lr, pc}
对上面一行汇编代码进行解析: 假设sp = 4096,db表示预先减少
因为内存空间是0-4095才对,4096已经超出了,所以我们使用db预先减少 因此是先减后存,我们已经减完了到sp’ = sp - 4 = 4092,后面需要存储寄存器的值 规则:高编号的寄存器存在高寄存器,因此{}里面顺序随便 因此:4092-4095 存放PC = R15 在执行先减后存的操作 sp; = sp - 4 = 4092 - 4 = 4088 4088-4091 存放lr= R14 后面一次执行先减后存的操作得到: 4084-4087 存放ip= R12 4080-4083 存放fp = R11
ldmia sp!,{fp, sp, pc}
ia表示后增,那就是先读后增 ,高编号寄存器存放高地址内存值 。
(1) 因此刚开始是fp = 4080- 4083的值 = 原来保存的fp (低编号寄存器存放低地址内容的值) (2) 后增sp’ = sp + 4 = 4084 (3) 先读: sp = 4084 - 4087的值= 原来保存的ip (4) 后增sp’ = sp + 4 = 4088 (5) 先读: sp = 4088 - 4091的值= 原来保存的p’c (6) 后增sp’ = sp + 4 = 4092 这里的sp后面没有!:表示sp修改后的地址值不存入sp中,因此在第(3)步中已经把原来传入的ip赋值给sp了,而这个ip在程序的最开始的时候就是传入的最原始的sp = 4096,见下图。
|