本章使用汇编语言来编写,通过本章了解如何使用汇编语言来初始化 I.MX6ULL 外设寄存器、了解 I.MX6ULL 最基本的 IO 输出功能。
8.1 I.MX6ULL GPIO 详解
8.1.1 STM32 GPIO 回顾
初始化代码中重点要做的事情有一下几个:
①、使能指定 GPIO 的时钟;
②、初始化 GPIO,比如输出功能、上拉、速度等等;
③、 STM32 有的 IO 可以作为其它外设引脚,也就是 IO 复用,如果要将 IO 作为其它外设引脚使用的话就需要设置 IO 的复用功能;
④、最后设置 GPIO 输出高电平或者低电平。
其中I.MX6ULL的设置需要看了数据手册和参考手册,IMX6ULL 参考手册.pdf和IMX6ULL 数据手册(商用级)pdf。
8.1.2 I.MX6U IO 命名
I.MX6ULL 参考手册的第 32 章“Chapter 32: IOMUX Controller(IOMUXC)”,第 32 章的书签如图 8.1.2.1 所示:

图 8.1.2.1 I.MX6U GPIO 命名
I.MX6ULL 的 IO 分为两类: SNVS 域的和通用的,这两类 IO 本质上都是一样的,我们就有下面的常用 IO 为例,讲学习下 I.MX6ULL 的 IO 命名方式。
图 8.1.2.1 中的形如“IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00”的就是 GPIO 命名,命名形式就是“IOMUXC_SW_MUC_CTL_PAD_XX_XX”,后面的“XX_XX”就是 GPIO 命名,
比如: GPIO1_IO01、 UART1_TX_DATA、 JTAG_MOD 等等
注意:I.MX6ULL 的 GPIO 并不像 STM32一样以 PA0~15 这样命名,他是根据某个 IO 所拥有的功能来命名的。
8.3.1 I.MX6U IO复用
以“IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00”这个 IO 为例,打开参考手册的 1568 页,如图 8.1.3.1 所示:


图 8.1.3.1 GPIO1_IO00 复用
从图 8.1.3.1 可以看到有个名为: IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00 的寄存器,寄存器地址为 0X020E005C,这个寄存器是 32 位的,但是只用到了最低 5 位,其中
bit0~bit3(MUX_MODE)就是设置 GPIO1_IO00 的复用功能的。 GPIO1_IO00 一共可以复用为 9种功能 IO, 分别对应 ALT0~ALT8,其中 ALT5 就是作为 GPIO1_IO00。 GPIO1_IO00 还可以作
为 I2C2_SCL、 GPT1_CAPTURE1、 ANATOP_OTG1_ID 等。这个就是 I.MX6U 的 IO 复用,我们学习 STM32 的时候 STM32 的 GPIO 也是可以复用的。
再来看一个“IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA”这个 IO,这个 IO 对应的复用如图 8.1.3.2 所示:


图 8.1.3.2 UART1_TX_DATA IO 复用
同样的,从图 8.1.3.2 可以看出, UART1_TX_DATA 可以复用为 8 种不同功能的 IO,分为ALT0~ALT5 和 ALT8、 ATL9,其中 ALT5 表示 UART1_TX_DATA 可以复用为 GPIO1_IO16。
I.MX6ULL 的 GPIO 一共有 5 组: GPIO1、 GPIO2、 GPIO3、 GPIO4 和 GPIO5,其中 GPIO1 有 32 个 IO, GPIO2 有 22 个 IO, GPIO3 有 29 个 IO、 GPIO4 有 29 个 IO, GPIO5
最少,只有 12 个 IO,这样一共有 124 个 GPIO。如果只想看每个 IO 能复用什么外设的话可以直接查阅《IMX6ULL 参考手册》的第 4 章“Chapter 4 External Signals and Pin Multiplexing”。如
果我们要编写代码,设置某个 IO 的复用功能的话就需要查阅第 32 章“Chapter 32: IOMUXController(IOMUXC)” ,第 32 章详细的列出了所有 IO 对应的复用配置寄存器。
注意:I.MX6U 的 IO 是有复用功能的,和 STM32一样,如果某个 IO 要作为某个外设引脚使用的话,是需要配置复用寄存器的。
8.1.4 I.MX6U IO 配置
《 I.MX6UL 参考手册》第 30 章“ Chapter 30: IOMUX Controller(IOMUXC)”的书签中,每一个 IO 会出现两次:
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO00
上面两个都是跟 GPIO_IO00 有关的寄存器,名字上的区别就是红色部分,一个是“MUX”,一个是“PAD”。
IOMUX_SW_MUX_CTL_PAD_GPIO1_IO00 --- 用来配置GPIO1_IO00 复用功能的,
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO00 --- 用来配置GPIO1_IO00 包括速度设置、驱动能力设置、压摆率设置等等





图 8.1.4.1 IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO00 寄存器
从图 8.1.4.1 中可以看出, IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO00 也是个寄存器,寄存器地址为 0X020E02E8。这也是个 32 位寄存器,但是只用到了其中的低 17 位,先来看一下图 8.1.4.2 所示的 GPIO 功能图:

图 8.1.4.2 GPIO 功能图
对照着图 8.1.4.2 来详细看一下寄存器 IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO00 的各个位的含义:
HYS(bit16):对应图 8.1.4.2 中 HYS,用来使能迟滞比较器,当 IO 作为输入功能的时候有效,用于设置输入接收器的施密特触发器是否使能。如果需要对输入波形进行整形的话可以使能此位。此位为 0 的时候禁止迟滞比较器,为 1 的时候使能迟滞比较器。
PUS(bit15:14): 对应图 8.1.4.2 中的 PUS,用来设置上下拉电阻的,一共有四种选项可以选择,如表 8.1.4.1 所示:

表 8.1.4.1 上下拉设置
PUE(bit13): 图 8.1.4.2 没有给出来,当 IO 作为输入的时候,这个位用来设置 IO 使用上下拉还是状态保持器。当为 0 的时候使用状态保持器,当为 1 的时候使用上下拉。状态保持器在IO 作为输入的时候才有用,顾名思义,就是当外部电路断电以后此 IO 口可以保持住以前的状态。
PKE(bit12): 对应图 8.1.4.2 中的 PKE,此位用来使能或者禁止上下拉/状态保持器功能,为0 时禁止上下拉/状态保持器,为 1 时使能上下拉和状态保持器。
ODE(bit11):对应图 8.1.4.2 中的 ODE,当 IO 作为输出的时候,此位用来禁止或者使能开路输出,此位为 0 的时候禁止开路输出, 当此位为 1 的时候就使能开路输出功能。
SPEED(bit7:6): 对应图 8.1.4.2 中的 SPEED,当 IO 用作输出的时候,此位用来设置 IO 速度,设置如表 8.1.4.2 所示:

表 8.1.4.2 速度配置
DSE(bit5:3):对应图 8.1.4.2 中的 DSE,当 IO 用作输出的时候用来设置 IO 的驱动能力,总共有 8 个可选选项,如表 8.1.4.3 所示:

表 8.1.4.3 驱动能力设置
SRE(bit0): 对应图 8.1.4.2 中的 SRE,设置压摆率,当此位为 0 的时候是低压摆率,当为 1的时候是高压摆率。这里的压摆率就是 IO 电平跳变所需要的时间,比如从 0 到 1 需要多少时间,时间越小波形就越陡,说明压摆率越高;反之,时间越多波形就越缓,压摆率就越低。如果你的产品要过 EMC 的话那就可以使用小的压摆率,因为波形缓和,如果你当前所使用的 IO做高速通信的话就可以使用高压摆率。
可以看出寄存器 IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO00 是用来配置 GPIO1_IO00 的,包括速度设置、驱动能力设置、压摆率设置等等。
8.1.5 I.MX6U GPIO 配置
IOMUXC_SW_MUX_CTL_PAD_XX_XX 和 IOMUXC_SW_PAD_CTL_PAD_XX_XX 这两种寄存器都是配置 IO 的,注意是 IO!不是 GPIO 是一个 IO 众多复用功能中的一种。
如果我们要用 GPIO1_IO00 来点个灯、作为按键输入啥的就是使用其 GPIO(通用输入输出)的功能。将其复用为 GPIO 以后还需要对其 GPIO 的功能进行配置,关于 I.MX6U 的 GPIO 请参考《IMX6UL 参考手册》的第 26
章“Chapter 26 General Purpose Input/Ouput(GPIO)”, GPIO 结构如图 8.1.5.1 所示:

图 8.1.5.1 GPIO 结构图
在 图 8.1.5.1 的 左 下 角 的 IOMUXC 框 图 里 面 就 有 SW_MUX_CTL_PAD_* 和SW_PAD_CTL_PAD_*两种寄存器。这两种寄存器前面说了用来设置 IO 的复用功能和 IO 属性配置。左上角部分的 GPIO 框图就是,当 IO 用作 GPIO 的时候需要设置的寄存器,一共有八个:DR、 GDIR、 PSR、 ICR1、 ICR2、 EDGE_SEL、 IMR 和 ISR。前面我们说了 I.MX6U 一共有GPIO1~GPIO5 共五组 GPIO,每组 GPIO 都有这 8 个寄存器。
1、DR 寄存器:此寄存器是数据寄存器,DR 寄存器,此寄存器是数据寄存器:

此寄存器是 32 位的,一个 GPIO 组最大只有 32 个 IO,因此 DR 寄存器中的每个位都对应一个 GPIO。
GPIO 被配置为输出功能:向指定的位写入数据那么相应的 IO 就会输出相应的高低电平,比如要设置 GPIO1_IO00 输出高电平,那么就应该设置 GPIO1.DR=1;
配置为输入模式:此寄存器就保存着对应 IO 的电平值,每个位对对应一个 GPIO,例如,当 GPIO1_IO00 这个引脚接地的话,那么 GPIO1.DR 的 bit0 就是 0。
2、GDIR 寄存器:这是方向寄存器,用来设置某个 GPIO 的工作方向的,即输入/输出, GDIR 寄存器结构如图 8.1.5.3 所示:

图 8.1.5.3 GDIR 寄存器
GDIR 寄存器也是 32 位的,每个 IO 对应一个位,如果要设置 GPIO 为输入设置相应的位为 0,如果要设置为输出就设置为 1。比如要设置 GPIO1_IO00 为输入,那么 GPIO1.GDIR=0;
3、PSR 寄存器:这是 GPIO 状态寄存器

图 8.1.5.4 PSR 状态寄存器
PSR 寄存器也是 32 位的,每个 IO 对应一个位,读取相应的位即可获取对应的 GPIO 的状态,也就是 GPIO 的高低电平值。功能和输入状态下的 DR 寄存器一样。
3、ICR1和ICR2这两个寄存器:都是中断控制寄存器
ICR1用于配置低16个GPIO,ICR2 用于配置高 16 个 GPIO, ICR1 寄存器如图 8.1.5.5 所示:

图 8.1.5.5 ICR1 寄存器
ICR1 用于 IO0~15 的配置, ICR2 用于 IO16~31 的配置。 ICR1 寄存器中一个 GPIO 用两个位,这两个位用来配置中断的触发方式,和 STM32 的中断很类似,可配置的选线如表 8.1.5.1所示:

表 8.1.5.1 中断触发配置
以GPIO1_IO15为例,如果要设置GPIO1_IO15为上升沿触发中断,那么GPIO1.ICR1=2<<30,如果要设置 GPIO1 的 IO16~31 的话就需要设置 ICR2 寄存器了。
4、IMR 寄存器:中断屏蔽寄存器

图 8.1.5.6 IMR 寄存器
IMR 寄存器也是一个 GPIO 对应一个位, IMR 寄存器用来控制 GPIO 的中断禁止和使能,使能某个 GPIO 的中断,那么设置相应的位为 1 ,如果要禁止中断,那么就设置相应的位为 0 即可。例如,要使能 GPIO1_IO00 的中断,那么就可以设置 GPIO1.MIR=1 即可
5、寄存器 ISR:中断状态寄存器

图 8.1.5.7 ISR 寄存器
ISR 寄存器也是 32 位寄存器,一个 GPIO 对应一个位,只要某个 GPIO 的中断发生,那么ISR 中相应的位就会被置 1。所以,我们可以通过读取 ISR 寄存器来判断 GPIO 中断是否发生,相当于 ISR 中的这些位就是中断标志位。当我们处理完中断以后,必须清除中断标志位,清除方法就是向 ISR 中相应的位写 1,也就是写 1 清零。
6、EDGE_SEL 寄存器:这是边沿选择寄存器

图 8.1.5.8 EDGE_SEL 寄存器
EDGE_SEL 寄存器用来设置边沿中断,这个寄存器会覆盖 ICR1 和 ICR2 的设置,同样是一个 GPIO 对应一个位。如果相应的位被置 1,那么就相当与设置了对应的 GPIO 是上升沿和下降沿(双边沿)触发。例如,我们设置 GPIO1.EDGE_SEL=1,那么就表示 GPIO1_IO01 是双边沿触发中断,无论 GFPIO1_CR1 的设置为多少,都是双边沿触发。
8.1.6 I.MX6U GPIO 时钟使能
I.MX6U 的系统时钟参考《I.MX6UL 参考手册》的第 18 章“Chapter 18: Clock Controller Module(CCM)”,I.MX6U 的时钟系统很复杂先不研究 , 只 看 一 下 CCM 里 面 的 外 设 时 钟 使 能 寄 存 器 。 CMM 有CCM_CCGR0~CCM_CCGR6 这 7 个寄存器,这 7 个寄存器控制着 I.MX6U 的所有外设时钟开关,我们以 CCM_CCGR0 为例来看一下如何禁止或使能一个外设的时钟, CCM_CCGR0 结构体如图 8.1.6.1 所示


图 8.1.6.1 CCM_CCGR0 寄存器
CCM_CCGR0 是个 32 位寄存器,其中每 2 位控制一个外设的时钟,比如 bit31:30 控制着GPIO2 的外设时钟,两个位就有 4 种操作方式,如表 8.1.6.1 所示:

表 8.1.6.1 外设时钟控制
根据表 8.1.6.1 中的位设置,如果我们要打开 GPIO2 的外设时钟,那么只需要设置CCM_CCGR0 的 bit31 和 bit30 都为 1 即可,也就是 CCM_CCGR0=3 << 30。反之,如果要关闭GPIO2 的 外 设 时 钟 , 那 就 设 置 CCM_CCGR0 的 bit31 和 bit30 都 为 0 即 可 。
CCM_CCGR0~CCM_CCGR6 这 7 个寄存器操作都是类似的,只是不同的寄存器对应不同的外设时钟,I.MX6U 的每个外设的时钟都可以独立的禁止和使能。后面所有的例程将 I.MX6U 的所有外设时钟都打开。
总结:要将 I.MX6U 的 IO 作为 GPIO 使用,我们需要一下几步
①、使能 GPIO 对应的时钟。
②、设置寄存器 IOMUXC_SW_MUX_CTL_PAD_XX_XX,设置 IO 的复用功能,使其复用为 GPIO 功能。
③、设置寄存器 IOMUXC_SW_PAD_CTL_PAD_XX_XX,设置 IO 的上下拉、速度等等。
④、第②步已经将 IO 复用为了 GPIO 功能,所以需要配置 GPIO,设置输入/输出、是否使用中断、默认输出电平等
8.2 硬件原理分析
I.MX6ULL-ALPHA 开发板底板原理图,LED0 接到了 GPIO_3 上, GPIO_3 就是 GPIO1_IO03,当 GPIO1_IO03输出低电平(0)的时候发光二极管 LED0 就会导通点亮,当 GPIO1_IO03 输出高电平(1)的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。

图 8.2.1 LED 原理图
8.3 实验程序编写
GPIO1_IO03 做如下设置:
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 组寄存器地址,在《IMX6ULL 参考手册》的 1357 页,如图 8.3.1 所示:

图 8.3.1 GPIO1 对应的 GPIO 寄存器地址
GPIO1_IO03 是作为输出功能的,因此 GPIO1_GDIR 的 bit3 要设置为 1,表示输出。
5、控制 GPIO 的输出电平
GPIO1_IO03 已经配置好了,只需要向 GPIO1_DR 寄存器的 bit3 写入 0 即可控制 GPIO1_IO03 输出低电平,打开 LED,向 bit3 写入 1 可控制 GPIO1_IO03 输出高电平,关闭 LED。
代码实现:
/*************************************************************************************
*实验说明:汇编语言点亮LED灯
*参考手册:IMX6ULL参考手册.pdf
*具体步骤:
*Step1:使能 GPIO1 时钟
*我们将所有GPIO的时钟都使能 (手册699行)
*CMM有CCM_CCGR0~CCM_CCGR6 这 7 个寄存器,寄存器地址分别如下
*Address: 20C_4000h base + 68h offset = 20C_4068h
*Address: 20C_4000h base + 6Ch offset = 20C_406Ch
*Address: 20C_4000h base + 70h offset = 20C_4070h
*Address: 20C_4000h base + 74h offset = 20C_4074h
*Address: 20C_4000h base + 78h offset = 20C_4078h
*Address: 20C_4000h base + 7Ch offset = 20C_407Ch
*Address: 20C_4000h base + 80h offset = 20C_4080h
*
*Step2:设置 GPIO1_IO03 的复用功能 (手册1571行)
*寄存器:GPIO1_IO03 的复用寄存器 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03
*寄存器地址:Address: 20E_0000h base + 68h offset = 20E_0068h
*取值:该寄存器只用了低五位,其中第四位(从0开始)设置为0,低四位设置如下:
*0101 ALT5 — Select mux mode: ALT5 mux port: GPIO1_IO03 of instance: gpio1
*
*Step3:配置 GPIO1_IO03为输出 (手册1357行)
*寄存器:GPIO1_IO03 的配置寄存器 IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03
*寄存器地址:Address: 20E_0000h base + 2F4h offset = 20E_02F4h
*取值:根据实际使用情况,配置此寄存器
*
*Step4:设置 GPIO1_IO03 复用为了 GPIO 功能 (手册1357行)
*寄存器及地址:
*Absolute address (hex)|Register name|Width(in bits)|Access|Reset value|Section/ pag
*209_C000 GPIO data register (GPIO1_DR) 32 R/W 0000_0000h 28.5.1/1358
*209_C004 GPIO direction register (GPIO1_GDIR) 32 R/W 0000_0000h 28.5.2/1359
*209_C008 GPIO pad status register (GPIO1_PSR) 32 R 0000_0000h 28.5.3/1359
*209_C00C GPIO interrupt configuration register1 (GPIO1_ICR1) 32 R/W 0000_0000h 28.5.4/1360
*209_C010 GPIO interrupt configuration register2 (GPIO1_ICR2) 32 R/W 0000_0000h 28.5.5/1364
*209_C014 GPIO interrupt mask register (GPIO1_IMR) 32 R/W 0000_0000h 28.5.6/1367
*209_C018 GPIO interrupt status register (GPIO1_ISR) 32 w1c 0000_0000h 28.5.7/1368
*209_C01C GPIO edge select register (GPIO1_EDGE_SEL) 32 R/W 0000_0000h 28.5.8/1369
*取值:GPIO1_IO03 是作为输出功能的,因此 GPIO1_GDIR 的 bit3 要设置为 1,表示输出
*
*Step5:控制 GPIO 的输出电平
*取值:GPIO1_DR 寄存器的 bit3 写入 0 即可控制 GPIO1_IO03 输出低电平,打开 LED,向 bit3 写入 1
*可控制 GPIO1_IO03 输出高电平,关闭 LED
************************************************************************************/
.global _start /* 全局标号 */
/*
* 描述:_start函数,程序从此函数开始执行此函数完成时钟使能、
* GPIO初始化、最终控制GPIO输出低电平来点亮LED灯。
*/
_start:
/* 1、使能所有GPIO时钟 */
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 /* 寄存器 CCGR6 */
ldr r1, = 0X5
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 /*寄存器 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 */
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
8.4 编译下载验证
8.4.1 编译代码
1、 arm-linux-gnueabihf-gcc 编译文件
要编译出在 ARM 开发板上运行的可执行文件,使用交叉编译器 arm-linux-gnueabihf-gcc 来编译。 先将 led.s 编译为对应的.o 文件,在终端中输入如下命令:
arm-linux-gnueabihf-gcc -g -c led.s -o 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 文件链接到一个指定的链接位置。在学习STM32在MDK做了如下界面设置

左侧的 IROM1 是设置 STM32 芯片的 ROM 起始地址和大小的,右边的 IRAM1 是设置 STM32 芯片的 RAM 起始地址和大小的。其中 0X08000000 就是 STM32 内部 ROM 的起始地址,编译出来的指令肯定是要从 0X08000000 这个地址开始存放的。对于STM32 来说 0X08000000 就是它的链接地址。在 Linux 下用交叉编译器开发 ARM 的是时候就需要自己处理这些了。
下来区分“存储地址”和“运行地址”这两个概念:
“存储地址”:就是可执行文件存储在哪里,可执行文件的存储地址可以随意选择。比如 I.MX6ULL 支持 SD 卡、 EMMC、 NAND 启动,因此代码可以存储到 SD 卡、 EMMC 或者 NAND 中,但是要运行的话就必须将代码从 SD 卡、 EMMC 或者NAND 中拷贝到其运行地址(链接地址)处
“运行地址”:就是代码运行的时候所处的地址,这个我们在链接的时候就已经确定好了,代码要运行,那就必须处于运行地址处,否则代码肯定运行出错。
注意:“存储地址”和“运行地址”可以一样,比如STM32 的存储起始地址和运行起始地址都是 0X08000000。
本教程所有的裸机例程都是烧写到 SD 卡中,上电以后 I.MX6ULL 的内部 boot rom 程序会将可执行文件拷贝到链接地址处,这个链接地址可以在 I.MX6ULL 的内部 128KB RAM 中(0X900000~0X91FFFF),也可以在外部的 DDR 中。本教程所有裸机例程的链接地址都在 DDR中,链接起始地址为 0X87800000。 I.MX6ULL-ALPHA 开发板的 DDR 容量有两种: 512MB 和 256MB,起始地址都为 0X80000000,只不过 512MB 的终止地址为 0X9FFFFFFF,而 256MB 容量的终止地址为 0X8FFFFFFF。之所以选择 0X87800000 这个地址是因为后面要讲的 Uboot 其链接地址就是 0X87800000,这样我们统一使用 0X87800000 这个链接地址,不容易记混。确定了链接地址以后就可以使用 arm-linux-gnueabihf-ld 来将前面编译出来的 led.o 文件链接到 0X87800000 这个地址,使用如下命令:
arm-linux-gnueabihf-ld -Ttext 0X87800000 led.o -o led.elf
参数说明:
-Ttex:就是指定链接地址
-o:指定链接生成的 elf 文件名
led.elf 文件也不是我们最终烧写到 SD 卡中的可执行文件,我们要烧写的.bin 文件,因此还需要将 led.elf 文件转换为.bin 文件,这里我们就需要用到 arm-linux-gnueabihf-objcopy 这个工具。
3、 arm-linux-gnueabihf-objcopy 格式转换
arm-linux-gnueabihf-objcopy 更像一个格式转换工具,我们需要用它将 led.elf 文件转换为led.bin 文件,命令如下:
arm-linux-gnueabihf-objcopy -O binary -S -g led.elf led.bin
参数说明:
-O:指定以什么格式输出,后面的“ binary”表示以二进制格式输出
-S:表示不要复制源文件中的重定位信息和符号信息
-g:表示不复制源文件中的调试信息
上述命令执行完成以后,工程目录,至此led.bin 文件生成了。

4、 arm-linux-gnueabihf-objdump 反汇编
对于C 语言写试的验例程,有时候需要查看其汇编代码来调试代码,因此就需要进行反汇编,一般可以将 elf 文件反汇编,比如如下命令:
arm-linux-gnueabihf-objdump -D led.elf > led.dis
参数说明:
-D:表示反汇编所有的段
反汇编完成以后就会在当前目录下出现一个名为 led.dis 文件

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

可以看出 led.dis 里面是汇编代码,还可以看到内存分配情况,在0X87800000 处就是全局标号_start,也就是程序开始的地方。通过 led.dis 这个反汇编文件可以明显的看出我们的代码已经链接到了以 0X87800000 为起始地址的区域。
上述步骤总结:
为了编译 ARM 开发板上运行的 led.o 这个文件使用了如下命令:
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
如果修改了 led.s 文件,那么就需要在重复一次上面的这些命令,太麻烦了,这个时候我们就可以使用第三章讲解的 Makefile 文件了。
8.4.2 创建 Makefile 文件
创建一个名为“Makefile”的文件,并根据 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
clean:
rm -rf *.o led.bin led.elf led.dis
创建好 Makefile 以后我们就只需要执行一次“make”命令即可完成编译,如下图所示:

要清理工程的话执行“make clean”即可,如下图所示:

8.4.3 代码烧写
STM32 等其他的单片机的时候,编译完代码以后可以直接通过 MDK 或者 IAR下载到内部的 flash 中。但是 I.MX6U 虽然内部有 96K 的 ROM,但是这 96K 的 ROM 是 NXP自己用的,不向用户开放。所以相当于说 I.MX6U 是没有内部 flash ,I.MX6U 支持从外置的 NOR Flash、 NAND Flash、 SD/EMMC、 SPI NOR Flash和 QSPI Flash 这些存储介质中启动,所以我们可以将代码烧写到这些存储介质中。编译出来的可执行文件是怎么存放到 SD 中的,存放的位置是什么?这个 NXP 是有详细规定的!必须按照 NXP 的规定来将代码烧写到 SD 卡中,否则代码是绝对运行不起来的。《IMX6UL 参考手册》的第 8 章“Chapter 8 System Boot”就是专门讲解 I.MX6U 启动。正点原子专门编写了一个软件来将编译出来的.bin 文件烧写到 SD 卡中,这个软件叫做imxdownload, imxdownlaod 只能在 Ubuntu 下使用,使用步骤如下:
1、将 imxdownload 拷贝到工程根目录下
与 led.bin 处于同一个文件夹下,要不然烧写会失败:

2、给予 imxdownload 可执行权限、
imxdownload 从 Windows 下复制到 Ubuntu 中以后, imxdownload 默认是没有可执行权限的。我们需要给予 imxdownload 可执行权限,执行
chmod +x imxdownload
3、确定要烧写的 SD 卡
准备一张新的 SD(TF)卡,确保 SD 卡里面没有数据,因为我们在烧写代码的时候可能会格式化 SD 卡。
Ubuntu 下所有的设备文件都在目录“/dev”里面,所以插上 SD 卡以后也会出现在“/dev”里面,其中存储设备都是以“/dev/sd”开头。当把SD卡插到电脑后会弹出如下界面,选择如下,将SD卡挂载到了 Ubuntu 系统中,而不是 Windows下(如果默认挂载到Windos下,可以在Vmware的左上角 虚拟机 -> 可移动设备(D)-> 相应设备名 -> 连接)

输入如下命令看可以看到SD卡已挂载到了Ubuntu下:
ls /dev/sd*

其中插上SD卡后多出来了/dev/sda /dev/sda1(名字可能不太一样),其中/dev/sda是SD卡,/dev/sda1是它的一个分区,确定好SD 卡以后我们就可以使用软件 imxdownload 向 SD 卡烧写 led.bin 文件了。
4、向 SD 卡烧写 bin 文件
使用 imxdownload 向 SD 卡烧写 led.bin 文件,命令格式如下:
./imxdownload <.bin file> <SD Card>
其中.bin 就是要烧写的.bin 文件, SD Card 就是你要烧写的 SD 卡,比如我的电脑使用如下命令烧写 led.bin 到/dev/sdd 中:
./imxdownload led.bin /dev/sdb //不能烧写到/dev/sda 或 sda1 设备里面!那是系统磁盘
烧写的过程中可能会让你输入密码,输入你的 Ubuntu 密码即可完成烧写,烧写过程如下图:

如果这个烧写速度大于几十 MB/s、甚至几百 MB/s 那么肯定是烧写失败了!解决方法就是重新插拔 SD 卡,一般出现这种情况,重新插拔 SD 卡基本没啥用,只有重启Ubuntu。
烧写完成以后会在当前工程目录下生成一个 load.imx 的文件,如下图所示:

load.imx 这个文件就是软件 imxdownload 根据 NXP 官方启动方式介绍的内容,在 led.bin 文件前面添加了一些数据头以后生成的。最终烧写到 SD 卡里面的就是这个 load.imx 文件,而非led.bin。
8.4.4 代码验证
代码已经烧写到了 SD 卡中了,接下来就是将 SD 卡插到开发板的 SD 卡槽中,然后设置拨码开关为 SD 卡启动,拨码开关设置如下图所示:

设置好以后按一下开发板的复位键,如果代码运行正常的话 LED0 就会被点亮。