序言
本文是学习韦东山老师裸机实战的学习笔记。
从裸机开发学起
什么是裸机?参考百度百科上的介绍裸机,英文名是Bare machine, Bare metal,指没有配置操作系统和其他软件的电子计算机。
不同于我们在 PC 上写程序,裸机没有操作系统和任何软件,在裸机上写程序是真正的从0开始。
第一个裸机程序
还记得学习 C 语言时的第一个程序 “Hello, World”。延续这一惯例,我们也从在裸机上以这一例程开始。 嘿嘿,开玩笑^_^,要在裸机上运行 "Hello, World" 程序可不是这么轻松。第一个在裸机上的程序就以点亮 LED 开始吧。
认识 LED
先来认识一下 LED,LED 是 light-emitting diode 的简写,中文名是发光二极管。它在原理图用下面的符号表示。
点亮 LED
认识原理图
我们先打开 JZ2440 的原理图,找到 LED 部分。
可以看到原理图上有 3 个 LED 灯,图中的 VDD3.3V,nLED_1 表示一个 net,同名的 net是相连的,nLED 的 n 表示低电平有效。以 LED D10 为例,当 nLED_1 输出低电平时 LED D10 点亮。与 nLED_1 连接在一起的是 S3C2440 的 GPF4 引脚。GPF4 是一个 GPIO 引脚。让 GPF4 输出低电平就可以将 LED D10 点亮啦。那么问题来了?如何让 GPF4 输出低电平呢?
认识芯片手册
我们在一个芯片平台上开发,这个芯片平台信息都在芯片手册上面,芯片手册会描述如何来配置引脚。芯片手册内容比较多,我们只需要查看自己关注的部分即可,拿到芯片手册先看它的目录。第 9 章的 I/O Ports 描述了 I/O 口的配置。我们找到 GPF 口。
GPFCON 是配置 F 口的配置寄存器。
GPFDAT 是 F 口的数据寄存器。
GPFUP 先忽略。
要配置 GPF4 输出低电平,那先设置 GPFCON 将 GPF4 配置成输出引脚。
GPF4 的第 [9:8] 位配置为 01 时表示它是一个输出引脚。然后再配置 GPFDAT 寄存器,让 GPF4 输出低电平即可。
看 GPFDAT 的描述,就是让对应的 bit 设置为 0,就是低电平了。
开始写程序
了解完上面点亮 LED 的步骤就可以开始程序了,不过因为我们是在裸机上开发,还没有 C 代码运行的环境,所以现在我们写的是汇编程序。 先来认识要用的一些 ARM 的汇编指令。
- LDR
语法:
LDR Rn, label
LDR 指令负责将 label 所代表的存储器中的数据搬移到内部寄存器 Rn 中,如:
# 下面的语句读取寄存器R1上的数据(4byte),保存到R0中。
LDR R0, [R1]
- STR 语法:
STR Rn, label
STR 指令将 Rn 寄存器上的数据(4byte)搬移到 label 所代表的存储器中,如:
# 下面的语句将寄存器R0上的数据,保存到R1
STR R0, [R1]
- B 语法:
B label
B 指令会跳转到 label 处的代码执行,可以用它来实现一个死循环,如:
# 下面的语句定义了一个 label 名为 halt,b halt跳转到 halt 处执行,是一个死循环
halt:
b halt
- MOV 语法:
MOV Rn, label
有了上面的几条汇编指令就可以开始写汇编程序了:
/* 这是ARM汇编程序的开头,先照着写 */
.global _start
_start:
/* 配置GPF4为输出引脚,将0x56000050的[9:8]位配置为01,即0x100
* 把0x100写到地址0x56000050
*/
ldr r1, = 0x56000050
ldr r0, = 0x100
str r0, [r1]
/* 设置GPF4输出高电平,将0x56000054设置为0
* 把0写到地址0x56000054
*/
ldr r1, = 0x56000054
ldr r0, = 0x0
str r0, [r1]
/* 死循环,防止代码跑飞 */
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
最后将bin文件烧写到开发板。
认识汇编与机器码
前面我们写了一个汇编程序 led_on.S,并将它编译成了机器码。现在我们将编译生成的 elf 文件用下面的命令来反汇编:
arm-linux-objdump -D led_on.elf > led_on.dis
生成的 led_on.dis 内容如下:
led_on.elf: file format elf32-littlearm
Disassembly of section .text:
00000000 <_start>:
0: e59f1014 ldr r1, [pc, #20] ; 1c <halt+0x4>
4: e3a00c01 mov r0, #256 ; 0x100
8: e5810000 str r0, [r1]
c: e59f100c ldr r1, [pc, #12] ; 20 <halt+0x8>
10: e3a00000 mov r0, #0 ; 0x0
14: e5810000 str r0, [r1]
00000018 <halt>:
18: eafffffe b 18 <halt>
1c: 56000050 .word 0x56000050
20: 56000054 .word 0x56000054
Disassembly of section .ARM.attributes:
00000000 <.ARM.attributes>:
0: 00001741 andeq r1, r0, r1, asr #14
4: 61656100 cmnvs r5, r0, lsl #2
8: 01006962 tsteq r0, r2, ror #18
c: 0000000d andeq r0, r0, sp
10: 00543405 subseq r3, r4, r5, lsl #8
14: 01080206 tsteq r8, r6, lsl #4
_start 段的第一行:
0: e59f1014 ldr r1, [pc, #20] ; 1c <halt+0x4>
从内存 [pc, #20] 中读取值存储到 r1 处。[pc, #20] 在 halt 段的 1c 处。也就是读取 halt 段地址 1c 上的值 0x56000050 存储到 r1上。与前面我们写的汇编代码 ldr r1, = 0x56000050 是匹配的。
第二行:
4: e3a00c01 mov r0, #256 ; 0x100
它使用了一个 MOV 指令,将 0x100 存储到了 r0,这是编译器做了优化,将前面我们写的 ldr r0, = 0x100 优化了。
第三行:
8: e5810000 str r0, [r1]
与前面写的汇编一致。
第四行与第一行的分析一致,就不多赘述啦。
ARM的寄存器
ARM 上有 R0 到 R15 16个寄存器,其中 R15 寄存器的别名是 pc(Program Counter)程序计数器,R14 的别名是 lr(Link Register)返回地址,R13 的别名是 sp(Stack Pointer)栈指针。
由于 ARM 上 CPU 是以流水线的方式执行指令的,例如,当前执行地址 A 的指令,已经在对地址 A+4 的指令译码,并读取地址 A+8 的指令,这个 A+8 的值就是 pc 的值。所以 _start 段第一行处的 1c 其实就是 [pc + 20] = [当前地址0 + 偏移8 + 20] = 0x1c
led_on.elf: file format elf32-littlearm
Disassembly of section .text:
00000000 <_start>:
/* r1 = [pc + 20] = [0 + 8 + 20] = [0x1c] = 0x56000050 */
0: e59f1014 ldr r1, [pc, #20] ; 1c <halt+0x4>
4: e3a00c01 mov r0, #256 ; 0x100
8: e5810000 str r0, [r1]
c: e59f100c ldr r1, [pc, #12] ; 20 <halt+0x8>
10: e3a00000 mov r0, #0 ; 0x0
14: e5810000 str r0, [r1]
00000018 <halt>:
18: eafffffe b 18 <halt>
1c: 56000050 .word 0x56000050
20: 56000054 .word 0x56000054
Disassembly of section .ARM.attributes:
上面代码的第二列是汇编代码编译后生成的机器码,如 e59f1014 是 ldr r1, [pc, #20] 编译后的机器码。
下面通过一个练习来加深对汇编和机器码的理解。
练习:修改 led_on.S 点亮 nLED_2
看原理图可知 nLED_2 由 GPF5 引脚控制,将 GPF5 配置成输出引脚,并让它输出低电平。GPF5 是第
[11:10] 位控制。
/* 这是ARM汇编程序的开头,先照着写 */
.global _start
_start:
/* 配置GPF5为输出引脚,将0x56000050的[11:10]位配置为01,即0x400
* 把0x100写到地址0x56000050
*/
ldr r1, = 0x56000050
ldr r0, = 0x400 /* 修改为0x400,将第[11:10]位配置为01 */
str r0, [r1]
/* 设置GPF5输出高电平,将0x56000054设置为0
* 把0写到地址0x56000054
*/
ldr r1, = 0x56000054
ldr r0, = 0x0
str r0, [r1]
halt:
b halt
对比生成的 bin 文件,只有 E3A00C01 改变成了 E3A00B01
这条机器码的哪些位代表了0x100呢?这就需要看 ARM 架构手册上 MOV 指令的描述
手册上的
shifter_operand 表示 0x100,它是用立即数来计算的,shifter_operand的高4位代表 rotate,低8位代表 immed_8,它的计算方法如下:
立即数 = immed_8 循环右移 (rotate * 2)位
举个例子:0x100 = 0000 0001 循环右移 24(1100 * 2) 位。