LV04-02-点灯-02-汇编点亮LED

本文主要使用汇编点亮ALPHA上的LED的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows版本 windows11
Ubuntu版本 Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
Win32DiskImager Win32DiskImager v1.0
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
https://elixir.bootlin.com/linux/latest/source 在线阅读linux kernel源码
nxp-imx/linux-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga NXP linux内核仓库tags中的rel_imx_4.1.15_2.1.0_ga
nxp-imx/uboot-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga NXP u-boot仓库tags中的rel_imx_4.1.15_2.1.0_ga
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)

一、硬件原理图

image-20230716170503614

可以看到LED0 接到了 GPIO_3 上, GPIO_3 就是 GPIO1_IO03,当 GPIO1_IO03输出低电平(0)的时候发光二极管 LED0 就会导通点亮,GPIO1_IO03 输出高电平(1)的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。所以 LED0 的亮灭取决于 GPIO1_IO03的输出电平,输出 0 就亮,输出 1 就灭。

二、点灯步骤

  • (1)使能 GPIO1 时钟

GPIO1 的时钟由 CCM_CCGR1 的 bit27 和 bit26 这两个位控制,将这两个位都设置位 11 即可。

  • (2)设置 GPIO1_IO03 的复用功能

找到 GPIO1_IO03 的复用寄存器“IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03”的地址为0X020E0068,然后设置此寄存器,将 GPIO1_IO03 这个 IO 复用为 GPIO 功能,也就是 ALT5。

  • (3)配置 GPIO1_IO03功能和参数

找到 GPIO1_IO03 的配置寄存器“IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03”的地址为0X020E02F4,根据实际使用情况,配置此寄存器。

  • (4)设置GPIO

将 GPIO1_IO03 复用为了 GPIO 功能,我们还需要配置 GPIO。 找到 GPIO3 对应的 GPIO 组寄存器地址,在《i.MX 6ULL Applications Processor
Reference Manual》的 28.5 GPIO Memory Map/Register Definition :

image-20230716170859666

GPIO1_IO03 是作为输出功能的,因此 GPIO1_GDIR 的 bit3 要设置为 1,表示输出。

  • (5)控制 GPIO 的输出电平

向 GPIO1_DR 寄存器的 bit3 写入 0 即可控制 GPIO1_IO03 输出低电平,打开 LED,向 bit3 写入 1 可控制 GPIO1_IO03 输出高电平,关闭 LED。

三、程序编写与编译

1. led.s

1.1 完整汇编程序

点击查看 led.s 文件内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
.global _start  /* 全局标号 */

/*
* 描述: _start函数,程序从此函数开始执行此函数完成时钟使能、
* GPIO初始化、最终控制GPIO输出低电平来点亮LED灯。
*/
_start:
/* 例程代码 */
/* 1、使能所有时钟 */
ldr r0, =0X020C4068 /* CCGR0 */
ldr r1, =0XFFFFFFFF
str r1, [r0]

ldr r0, =0X020C406C /* CCGR1 */
str r1, [r0]

ldr r0, =0X020C4070 /* CCGR2 */
str r1, [r0]

ldr r0, =0X020C4074 /* CCGR3 */
str r1, [r0]

ldr r0, =0X020C4078 /* CCGR4 */
str r1, [r0]

ldr r0, =0X020C407C /* CCGR5 */
str r1, [r0]

ldr r0, =0X020C4080 /* CCGR6 */
str r1, [r0]


/* 2、设置GPIO1_IO03复用为GPIO1_IO03 */
ldr r0, =0X020E0068 /* 将寄存器SW_MUX_GPIO1_IO03_BASE加载到r0中 */
ldr r1, =0X5 /* 设置寄存器SW_MUX_GPIO1_IO03_BASE的MUX_MODE为5 */
str r1,[r0]

/* 3、配置GPIO1_IO03的IO属性
*bit 16:0 HYS关闭
*bit [15:14]: 00 默认下拉
*bit [13]: 0 kepper功能
*bit [12]: 1 pull/keeper使能
*bit [11]: 0 关闭开路输出
*bit [7:6]: 10 速度100Mhz
*bit [5:3]: 110 R0/6驱动能力
*bit [0]: 0 低转换率
*/
ldr r0, =0X020E02F4 /*寄存器SW_PAD_GPIO1_IO03_BASE */
ldr r1, =0X10B0
str r1,[r0]

/* 4、设置GPIO1_IO03为输出 */
ldr r0, =0X0209C004 /*寄存器GPIO1_GDIR */
ldr r1, =0X0000008
str r1,[r0]

/* 5、打开LED0
* 设置GPIO1_IO03输出低电平
*/
ldr r0, =0X0209C000 /*寄存器GPIO1_DR */
ldr r1, =0
str r1,[r0]

/*
* 描述: loop死循环
*/
loop:
b loop

1.2 详细分析

1.2.1 全局标号

1
.global _start  /* 全局标号 */

定义了一个全局标号_start,代码就是从_start 这个标号开始顺序往下执行的。

1.2.2 使能时钟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ldr r0, =0X020C4068 	/* CCGR0 */
ldr r1, =0XFFFFFFFF
str r1, [r0]

ldr r0, =0X020C406C /* CCGR1 */
str r1, [r0]

ldr r0, =0X020C4070 /* CCGR2 */
str r1, [r0]

ldr r0, =0X020C4074 /* CCGR3 */
str r1, [r0]

ldr r0, =0X020C4078 /* CCGR4 */
str r1, [r0]

ldr r0, =0X020C407C /* CCGR5 */
str r1, [r0]

ldr r0, =0X020C4080 /* CCGR6 */
str r1, [r0]

第 1 行:使用 ldr 指令向寄存器 r0 写入 0X020C4068,也就是 r0=0X020C4068,这个是CCM_CCGR0 寄存器的地址。

第 2 行:使用 ldr 指令向寄存器 r1 写入 0XFFFFFFFF,也就是 r1=0XFFFFFFFF。因为我们:要开启所有的外设时钟,因此 CCM_CCGR0~CCM_CCGR6 所有寄存器的 32 位都要置 1,也就是写入 0XFFFFFFFF。

第 3 行:使用 str 将 r1 中的值写入到 r0 所保存的地址中去,也就是给 0X020C4068 这个地址写入 0XFFFFFFFF,相当于CCM_CCGR0 = 0XFFFFFFFF,就是打开 CCM_CCGR0 寄存器所控制的所有外设时钟。

第 521 行都是向 CCM_CCGRX(X=16)寄存器写入 0XFFFFFFFF。这样我就通过汇编代码使能了 I.MX6U 的所有外设时钟。

1.2.3 设置GPIO复用功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ldr r0, =0X020E0068	/* 将寄存器SW_MUX_GPIO1_IO03_BASE加载到r0中 */
ldr r1, =0X5 /* 设置寄存器SW_MUX_GPIO1_IO03_BASE的MUX_MODE为5 */
str r1,[r0]

/**
* bit 16:0 HYS关闭
* bit [15:14]: 00 默认下拉
* bit [13]: 0 kepper功能
* bit [12]: 1 pull/keeper使能
* bit [11]: 0 关闭开路输出
* bit [7:6]: 10 速度100Mhz
* bit [5:3]: 110 R0/6驱动能力
* bit [0]: 0 低转换率
*/
ldr r0, =0X020E02F4 /*寄存器SW_PAD_GPIO1_IO03_BASE */
ldr r1, =0X10B0
str r1,[r0]

第 1~3 行:是设置GPIO1_IO03的复用功能, GPIO1_IO03的复用寄存器地址为0X020E0068,寄 存 器 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的 MUX_MODE 设 置 为 5 就 是 将GPIO1_IO03 设置为 GPIO。

第 15~17 行:是设置 GPIO1_IO03 的配置寄存器,也就是寄存器 IOMUX_SW_PAD_CTL_PAD_GPIO1_IO03 的值,此寄存器地址为 0X020E02F4,代码里面已经给出了这个寄存器详细的位设置。

1.2.4 设置GPIO功能

1
2
3
ldr r0, =0X0209C004	/*寄存器GPIO1_GDIR */
ldr r1, =0X0000008
str r1,[r0]

第 1~3 行:是设置 GPIO 功能,经过上面几步操作, GPIO1_IO03 这个 IO 已经被配置为了:GPIO 功能,所以还需要设置跟 GPIO 有关的寄存器。这几行是设置 GPIO1→GDIR 寄存器,将 GPIO1_IO03 设置为输出模式,也就是寄存器的 GPIO1_GDIR 的 bit3 置 1。

1.2.5 设置GPIO输出

1
2
3
ldr r0, =0X0209C000	/*寄存器GPIO1_DR */
ldr r1, =0
str r1,[r0]

第 1~3 行设置 GPIO1→DR 寄存器,也就是设置 GPIO1_IO03 的输出,我们要点亮开发板上的 LED0,那么 GPIO1_IO03 就必须输出低电平,所以这里设置 GPIO1_DR 寄存器为 0。

1.2.6 死循环

1
2
loop:
b loop

通过 b 指令, CPU 重复不断的跳到 loop 函数执行,进入一个死循环。 防止程序不受控制,跑到位置的地方。

2. 编译程序

2.1 arm-linux-gnueabihf-gcc 编译文件

我们是要编译出在 ARM 开发板上运行的可执行文件,所以要使用我们安装的交叉编译器 arm-linux-gnueabihf-gcc 来编译。先将 led.s 编译为对应的.o 文件,在终端中输入如下命令:

1
arm-linux-gnueabihf-gcc -g -c led.s -o led.o

上述命令就是将 led.s 编译为 led.o,其中“-g”选项是产生调试信息, GDB 能够使用这些调试信息进行代码调试。“-c”选项是编译源文件,但是不链接。“-o”选项是指定编译产生的文件名字,这里我们指定 led.s 编译完成以后的文件名字为 led.o。执行上述命令以后就会编译生成一个 led.o 文件,如下图所示:

image-20230716175902724

led.o 文件并不是我们可以下载到开发板中运行的文件,一个工程中所有的 C 文件和汇编文件都会编译生成一个对应的.o 文件,我们需要将这.o 文件链接起来组合成可执行文件。

2.2 arm-linux-gnueabihf-ld 链接文件

arm-linux-gnueabihf-ld 用来将众多的.o 文件链接到一个指定的链接位置。我们在学习SMT32 的时候基本就没有听过“链接”这个词,我们一般用 MDK 编写好代码,然后点击“编译”, MDK 或者 IAR 就会自动帮我们编译好整个工程,最后再点击“下载”就可以将代码下载到开发板中。 详细的内容这里就不再说了,之前学习STM32的时候详细的分析过。

我们要区分“存储地址”和“运行地址”这两个概念,“存储地址”就是可执行文件存储在哪里,可执行文件的存储地址可以随意选择。“运行地址”就是代码运行的时候所处的地址,这个我们在链接的时候就已经确定好了,代码要运行,那就必须处于运行地址处,否则代码肯定运行出错。比如 I.MX6U 支持 SD 卡、 EMMC、 NAND 启动,因此代码可以存储到 SD 卡、 EMMC 或者 NAND 中,但是要运行的话就必须将代码从 SD 卡、 EMMC 或者NAND 中拷贝到其运行地址(链接地址)处,“存储地址”和“运行地址”可以一样,比如STM32 的存储起始地址和运行起始地址都是 0X08000000。

因此我们现在需要做的就是确定一下点灯的汇编程序最终的可执行文件其运行起始地址,也就是链接地址。上电以后 I.MX6U 的内部 boot rom 程序会将可执行文件拷贝到链接地址处,这个链接地址可以在 I.MX6U 的内部 128KB RAM 中(0X900000~0X91FFFF),也可以在外部的 DDR 中,DDR中链接起始地址为 0X87800000。我们可以看一下I.MX6ULL的存储器映像

image-20230716190816114

I.MX6U-ALPHA 开发板的 DDR 容量有两种: 512MB 和256MB,起始地址都为 0X80000000,只不过 512MB 的终止地址为 0X9FFFFFFF,而256MB 容量的终止地址为 0X8FFFFFFF。之所以选择 0X87800000 这个地址是因为后面要学习的 u-boot 其链接地址就是 0X87800000,这样我们统一使用 0X87800000 这个链接地址,不容易记混。

确定了链接地址以后就可以使用 arm-linux-gnueabihf-ld 来将前面编译出来的 led.o 文件链接到 0X87800000 这个地址,使用如下命令:

1
arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf

“ -Ttext ”就是指定链接地址,“-o”选项指定链接生成的 elf 文件名,这里我们命名为 led.elf。上述命令执行完以后就会在工程目录下多一个 led.elf 文件 :

image-20230716190948033

led.elf 文件也不是我们最终烧写到 SD 卡中的可执行文件,我们要烧写镜像是使用的.bin 文件,因此还需要将 led.elf 文件转换为.bin 文件,这里我们就需要用到 arm-linux-gnueabihf-objcopy 这个工具了。

2.3 arm-linux-gnueabihf-objcopy 格式转换

arm-linux-gnueabihf-objcopy 更像一个格式转换工具,我们需要用它将 led.elf 文件转换为led.bin 文件,命令如下:

1
arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin

“-O”选项指定以什么格式输出,后面的“binary”表示以二进制格式输出,选项“-S”表示不要复制源文件中的重定位信息和符号信息,“-g”表示不复制源文件中的调试信息。上述命令执行完成以后,就会生成一个bin文件:

image-20230716191216340

2.4 arm-linux-gnueabihf-objdump 反汇编

大多数情况下我们都是用 C 语言写程序的,有时候需要查看其汇编代码来调试代码,因此就需要进行反汇编,一般可以将 elf 文件反汇编,比如如下命令:

1
arm-linux-gnueabihf-objdump -D led.elf > led.dis

“-D”选项表示反汇编所有的段,反汇编完成以后就会在当前目录下出现一个名为 led.dis 文件:

image-20230716191344195

可以打开 led.dis 文件看一下,看看是不是汇编代码 :

image-20230716191433154

可以看出 led.dis 里面是汇编代码,而且还可以看到内存分配情况。在0X87800000 处就是全局标号_start,也就是程序开始的地方。通过 led.dis 这个反汇编文件可以明显的看出我们的代码已经链接到了以 0X87800000 为起始地址的区域。

3. 创建 Makefile 文件

前边那样一条一条太麻烦了,之前都学过makefile了,那刚好可以用啦。用“touch”命令在工程根目录下创建一个名为“Makefile”的文件,并添加以下内容:

1
2
3
4
5
6
7
led.bin:led.s
arm-linux-gnueabihf-gcc -g -c led.s -o led.o
arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf
arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
arm-linux-gnueabihf-objdump -D led.elf > led.dis
clean:
rm -rf *.o led.bin led.elf led.dis

之后我们直接一个make就可以完成编译啦。

四、程序烧写

后边一小节再说。