第一个裸机程序

379 阅读8分钟

序言

本文是学习韦东山老师裸机实战的学习笔记。

从裸机开发学起

什么是裸机?参考百度百科上的介绍裸机,英文名是Bare machine, Bare metal,指没有配置操作系统和其他软件的电子计算机。
不同于我们在 PC 上写程序,裸机没有操作系统和任何软件,在裸机上写程序是真正的从0开始。

第一个裸机程序

还记得学习 C 语言时的第一个程序 “Hello, World”。延续这一惯例,我们也从在裸机上以这一例程开始。 嘿嘿,开玩笑^_^,要在裸机上运行 "Hello, World" 程序可不是这么轻松。第一个在裸机上的程序就以点亮 LED 开始吧。

认识 LED

先来认识一下 LED,LED 是 light-emitting diode 的简写,中文名是发光二极管。它在原理图用下面的符号表示。
image.png

点亮 LED

认识原理图

我们先打开 JZ2440 的原理图,找到 LED 部分。

image.png
可以看到原理图上有 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 输出低电平呢?

image.png

认识芯片手册

我们在一个芯片平台上开发,这个芯片平台信息都在芯片手册上面,芯片手册会描述如何来配置引脚。芯片手册内容比较多,我们只需要查看自己关注的部分即可,拿到芯片手册先看它的目录。第 9 章的 I/O Ports 描述了 I/O 口的配置。我们找到 GPF 口。

image.png GPFCON 是配置 F 口的配置寄存器。
GPFDAT 是 F 口的数据寄存器。
GPFUP 先忽略。
要配置 GPF4 输出低电平,那先设置 GPFCON 将 GPF4 配置成输出引脚。

image.png GPF4 的第 [9:8] 位配置为 01 时表示它是一个输出引脚。然后再配置 GPFDAT 寄存器,让 GPF4 输出低电平即可。

image.png 看 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 名为 haltb 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:

上面代码的第二列是汇编代码编译后生成的机器码,如 e59f1014ldr r1, [pc, #20] 编译后的机器码。
下面通过一个练习来加深对汇编和机器码的理解。

练习:修改 led_on.S 点亮 nLED_2

image.png 看原理图可知 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

image.png 这条机器码的哪些位代表了0x100呢?这就需要看 ARM 架构手册上 MOV 指令的描述

image.png 手册上的 shifter_operand 表示 0x100,它是用立即数来计算的,shifter_operand的高4位代表 rotate,低8位代表 immed_8,它的计算方法如下:

立即数 = immed_8 循环右移 (rotate * 2)位

举个例子:0x100 = 0000 0001 循环右移 24(1100 * 2) 位。