Skip to content

LV050-汇编点亮LED

一、硬件原理图

我们先看底板原理图("阿尔法Linux开发板(A盘)-基础资料/02、开发板原理图/IMX6ULL_ALPHA_V2.2(底板原理图).pdf"

image-20230716170503614

可以看到LED0 接到了 GPIO_3 上, 这个GPIO_3是哪个引脚?感觉这里标的有问题,我们搜索一下,会发现这里接在一个BTB连接器母座上,所以我们再去搜一些核心板原理图 "阿尔法Linux开发板(A盘)-基础资料/02、开发板原理图/IMX6ULL_CORE_V1.6(核心板原理图).pdf":

image-20241107075112786

就会发现GPIO_3 就是 GPIO1_IO03,根据底板原理图,当 GPIO1_IO03输出低电平(0)的时候发光二极管 LED0 就会导通点亮,GPIO1_IO03 输出高电平(1)的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。所以 LED0 的亮灭取决于 GPIO1_IO03的输出电平,输出 0 就亮,输出 1 就灭。

二、怎么操作GPIO?

主要是配置三大控制模块:

txt
CCM    : Clock Controller Module      时钟控制模块
IOMUXC : IOMUX Controller             IO 复用控制器
GPIO   : General-purpose input/output 通用的输入输出口

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

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

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

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

2. GPIO寄存器操作

GPIO 寄存器的 2 种操作方法,原则是不能影响到其他位 。

(1)读出、修改对应位、写入

assembly
/* 设置bit n */
val = data_reg;
val = val | (1 << n);
data_reg = val;

/* 清除 bit n */
val = data_reg;
val = val & ~(1 << n);
data_reg = val;

(2)set-and-clear protocol

set_reg, clr_reg, data_reg 三个寄存器对应的是同一个物理寄存器,

assembly
/* 要设置 bit n */
set_reg = (1<<n);
/* 要清除 bit n */
clr_reg = (1<<n);

这种方式会影响到其他的位,还是上面的方式更好一些,只针对某一位进行操作。

三、代码编写与分析

1. led.s完整内容

assembly
.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

2. 详细分析

2.1 全局标号

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

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

2.2 使能时钟

2.2.1 GPIO时钟用哪个寄存器?

我们翻一下《i.MX 6ULL Applications Processor Reference Manual》18.4 System Clocks 这一节,我们看一下Table 18-3. System Clocks, Gating, and Override (continued) 这个表格,具体GPIO应该是在 P635:

image-20260114150508109

可以看到这里每一组的GPIO时钟是由CCM_CCGRx寄存器来控制,我们使用的是GPIO1所以对应的应该是CCM_CCGR1[CG13],我们看一下这个是什么,我们可以全局搜索CCGR1,就可以找到这个寄存器,它其实位于参考手册18.6.24 CCM Clock Gating Register 1 (CCM_CCGR1)这一节,大概在P 700:

image-20260114150637567

可以看到,这个是个32位寄存器,每两位控制一个时钟,我们的GPIO1就位于27-26,就是CG13,可以看到这个前面的表格是对应的,那么这两位怎么配置表示开启时钟?我们可以再在18.6.23 CCM Clock Gating Register 0 (CCM_CCGR0) 这一节找到:

image-20260114150716028

CGR时钟状态描述
00时钟在所有模式下都是关闭的
01时钟在运行模式下为开,但在等待和停止模式下为关
10保留
11除停止模式外,时钟一直开启

目前我们可以全部设置为1,我们看上面的寄存器描述中,有一个reset,这个表示默认状态下的复位后的值,发现它其实默认就是11,也就是说时钟默认是打开的,但是为了规范,我们还是需要操作一遍。

2.2.2 寄存器地址?

我们知道要用到哪个寄存器,并且也知道了要配什么值,那么怎么操作?我们需要找到这个寄存器的地址,然后写值进去,地址怎么找?其实刚才找寄存器的时候有的:

image-20260114150819958

这里就写了,基地址是20C_4000h,偏移地址是6Ch,加到一起就是绝对地址 20C_406Ch。其实参考手册还有一个地方可以看到,就是这里:18.6 CCM Memory Map/Register Definition:

image-20260114151104238

从这里可以直接看到寄存器的绝对地址和默认值。

2.2.3 代码分析
assembly
	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 寄存器所控制的所有外设时钟。

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

2.3 设置IO复用功能

2.3.1 用哪个寄存器?

前面我们知道IO有多种复用功能,这里我们需要将此引脚作为输出功能,就是普通的GPIO,用哪个寄存器?我们前面有说过,设置复用模式我们要找包含MUX_CTL关键词的配置寄存器,我们直接搜GPIO1_IO03,可以找到参考手册中这个32.6.10 SW_MUX_CTL_PAD_GPIO1_IO03 SW MUX Control Register (IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03) 这一节的这个寄存器:

image-20260114151302724

可以看到我们需要配置这个寄存器的[3:0]为0101,这个时候这个引脚就是GPIO1_IO03功能。

2.3.2 寄存器地址?

(1)参考手册32.6.10 SW_MUX_CTL_PAD_GPIO1_IO03 SW MUX ControlRegister (IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03)

image-20260114151337972

(2)参考手册32.6 IOMUXC Memory Map/Register Definition

image-20260114151411133

2.3.3 代码分析
assembly
	ldr r0, =0X020E0068	/* 将寄存器SW_MUX_GPIO1_IO03_BASE加载到r0中 */
	ldr r1, =0X5		/* 设置寄存器SW_MUX_GPIO1_IO03_BASE的MUX_MODE为5 */
	str r1,[r0]

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

2.4 设置IO属性

2.4.1 用哪个寄存器?

设置IO属性的时候就是配置一下上下来、压摆率等相关参数,前面的复用功能配置集训期命名为SW_MUX_CTL_PAD_GPIO1_IO03,那么这里IO属性配置寄存器大概应该就是SW_PAD_CTL_PAD_GPIO1_IO03,我们直接全局搜索GPIO1_IO03,会在参考手册32.6.156 SW_PAD_CTL_PAD_GPIO1_IO03 SW PAD Control Register (IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03) 中搜到这个寄存器:

image-20260114151610875

这里面就是配置IO属性的地方,这里详细的可以直接去看手册。

2.4.2 寄存器地址?

还是那两个地方:

(1)32.6.156 SW_PAD_CTL_PAD_GPIO1_IO03 SW PAD Control Register (IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03)

image-20260114151543021

(2)32.6 IOMUXC Memory Map/Register Definition

image-20260114151513874

2.4.3 代码分析
assembly
	/**
	 * 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]

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

2.5 设置GPIO功能

接下来就是配置GPIO为输出功能

2.5.1 用哪个寄存器?

这一次我们要用到GPIO外设模块的方向寄存器,我们找到参考手册的28.5.2 GPIO direction register (GPIOx_GDIR) :

image-20260114152041922

可以看到每一位控制一个GPIO引脚的方向,配置1为输出,0为输入,我们这里是GPIO1_IO03,所以这里就要配置GPIO[3]这一位为1。

2.5.2 寄存器地址?

那这个寄存器地址是什么?我们先看这里28.5.2 GPIO direction register (GPIOx_GDIR) :

image-20260114152109674

发现是Base + 4h,那么这个Base是什么?这个应该是GPIO外设的基地址,我们可以直接看28.5 GPIO Memory Map/Register Definition :

image-20260114152143146

可以看到绝对地址是209_C004。那么基地址的话,应该是209_C000,我们来看一下存储器映像Chapter 2Memory Maps 这节的Table 2-2. AIPS-1 memory map (continued) :

image-20260114152320222

会发现GPIO1相关寄存器的基地址就是209_C000。

2.5.3 代码分析
assembly
	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。

2.6 设置GPIO输出电平

2.6.1 用哪个寄存器?

前面说过了,这里要用数据寄存器28.5.1 GPIO data register (GPIOx_DR) :

image-20260114152415634

这里还是每一位控制一个引脚。

2.6.2 寄存器地址?

和方向寄存器一样,我们直接看28.5 GPIO Memory Map/Register Definition :

image-20260114152143146

2.6.3 代码分析
assembly
	ldr r0, =0X0209C000	/*寄存器GPIO1_DR */
	ldr r1, =0		
	str r1,[r0]

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

2.7 死循环

assembly
loop:
	b loop

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

四、编译程序

1. arm-linux-gnueabihf-gcc 编译文件

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

shell
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 文件。

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

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

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

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

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

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

image-20241108000446239

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

3. arm-linux-gnueabihf-objcopy 格式转换

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

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

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

image-20241108000520398

这个.bin文件也不能直接放到开发板上运行, 这次是因为需要在.bin文件缺少启动相关信息。

4. arm-linux-gnueabihf-objdump 反汇编

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

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

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

image-20241108000549113

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

image-20241108000654980

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

5. 创建 Makefile 文件

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

makefile
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
	
.PHONY: clean
clean:
	rm -rf *.o led.bin led.elf led.dis

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

五、程序烧写

后边一小节再说。