Linux-ARM裸机(二)-LED驱动实验

原理分析

为什么要学习ARM Cortex-A汇编

  • 需要用汇编初始化一些SOC外设
  • 使用汇编初始化DDR(I.MX6ULL此款芯片不用使用汇编初始化DDR。因为NXP在I.MX6ULL 内部96KB的ROM中存放了自己编写的启动代码,这些启动代码可以读取DDR配置信息,并且完成DDR的初始化)
  • 设置sp指针,一般指向DDR,设置好C语言运行环境。(C语言运行环境就是指设置sp指针,因为C语言运行环境中需要保护现场—入栈和出栈,而入栈和出栈就要用到sp指针)

阿尔法开发板LED灯硬件原理分析

LED灯原理图(开发板右下角红色灯),LED0接在GPIO1_3的IO口。当 GPIO1_IO03输出低电平(0)的时候发光二极管 LED0 就会导通点亮,当 GPIO1_IO03 输出高电平(1)的时候发光二极管 LED0 不会导通,LED0 也就不会点亮。LED0 的亮灭取决于 GPIO1_IO03的输出电平,输出 0 就亮,输出 1 就灭。

点亮LED灯IO初始化流程

复习一下STM32 IO的初始化

  • 使能GPIO时钟
  • 设置PIN口复用功能(对于STM32来说,PIN口默认就是GPIO功能,如果要用到PIN其他功能就要先设置复用。比如:将PA9复用为USART1 TX这种操作)
  • 配置GPIO的电气属性
  • 使用GPIO输出高低电平

I.MX6ULL  IO的初始化流程

  • 使能时钟

CCGR0~CCGR6这7个寄存器控制着I.MX6ULL所有外设的时钟的使能,比如:下图CCGR0的30-31位控制着gpio2的时钟使能,28-29位控制着uart2的时钟使能。把这两个位置1,即可使能对应外设的时钟(这7个寄存器在I.MX6UL参考手册第18章中有详细介绍,这里就不全部介绍了)

设置CCGR0~CCGR6这7个寄存器全部为0xFFFFFFFF,相当于使能所有外设时钟。

  • IO复用,寄存器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03。

bit31~5位全部保留,只有bit4~0这五个位用到,其中bit3~0这四个位最重要:设置IO复用模式。比如:0000就复用为I²C1的SDA脚,0101=5就将GPIO1_IO03复用为也就是用作gpio功能,依次类推如下图:

  •  配置 IO电气属性:操作IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03寄存器。
常用到的配置电气属性

包括:压摆率,速度,驱动能力,开漏,上下拉等。

SRE(bit0): 对应图中的 SRE,设置压摆率,当此位为 0 的时候是低压摆率,当为 1的时候是高压摆率。这里压摆率就是 IO 电平跳变所需要的时间,比如从 0 到 1 需要多少时间,时间越小波形就越陡,说明压摆率越高;时间越长波形就越缓,压摆率就越低。如果你的产品要过 EMC 的话那就可以使用小的压摆率,因为波形缓和,如果使用 IO做高速通信的话就可以使用高压摆率。

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

SPEED(bit7:6): 对应图中的 SPEED,当 IO 用作输出的时候,此位用来设置 IO 速度,设置如下表:

ODE(bit11):对应图中的 ODE,当 IO 作为输出的时候,此位用来禁止或者使能开路输出,此位为 0 的时候禁止开路输出, 当此位为 1 的时候就使能开路输出功能。
PKE(bit12): 对应图中的 PKE,此位用来使能或者禁止上下拉/状态保持器功能,为0 时禁止上下拉/状态保持器,为 1 时使能上下拉和状态保持器。
PUE(bit13): 图中没有没有给出,当 IO 作为输入的时候,这个位用来设置 IO 使用上下拉还是状态保持器。当为 0 的时候使用状态保持器,当为 1 的时候使用上下拉。状态保持器在IO 作为输入的时候才有用,就是当外部电路断电以后此 IO 口可以保持住以前的状态。
PUS(bit15:14): 对应图中的 PUS,用来设置上下拉电阻的,一共有四种选项可以选择,如下表:

HYS(bit16):对应图中 HYS,用来使能迟滞比较器,当 IO 作为输入功能的时候有效,用于设置输入接收器的施密特触发器是否使能。如果需要对输入波形进行整形的话可以使能此位。此位为 0 的时候禁止迟滞比较器,为 1 的时候使能迟滞比较器。

注意一个容易混淆的部分:

IOMUXC_SW_MUX_CTL_PAD_XX_XX 和 IOMUXC_SW_PAD_CTL_PAD_XX_XX 这两种寄存器都是配置 IO 的,注意是 IO!不是 GPIO, GPIO 只是一个 IO 众多复用功能中的一种。比如 GPIO1_IO00 这个 IO 可以复用为: I2C2_SCL、 GPT1_CAPTURE1、ANATOP_OTG1_ID、ENET1_REF_CLK 、 MQS_RIGHT 、 GPIO1_IO00 、 ENET1_1588_EVENT0_IN 、SRC_SYSTEM_RESET 和 WDOG3_WDOG_B 这 9 个功能, GPIO1_IO00 也只是其中的一种。我们想要把 GPIO1_IO00 用作哪个外设就复用为哪个外设功能即可。我们用 GPIO1_IO00 来点个灯、作为按键输入啥的就是使用其 GPIO(通用输入输出)的功能。

  • I.MX6U GPIO 功能配置,输入/输出。

设置GPIO1_GDIR寄存器。设置GPIO1_IO03为输出模式(控制LED,因此要设置为输出模式):GPIO1_GDUR = 1<<3。设置GPIO1_DR为1输出高电平或为0输出低电平:GPIO1_DR = 1<<3。

GPIO的 8 个寄存器

当 IO 用作 GPIO 的时候需要配置的寄存器一共有八个:DR、 GDIR、 PSR、 ICR1、 ICR2、 EDGE_SEL、 IMR 和 ISR。前面我们说了 I.MX6U 一共有GPIO1~GPIO5 共五组 GPIO,每组 GPIO 都有这 8 个寄存器。

DR 寄存器:数据寄存器

此寄存器是 32 位的,一个 GPIO 组最大只有 32 个 IO,因此 DR 寄存器中的每个位都对应一个 GPIO。读每一个bit就代表读每个IO的数据,是高电平1或低电平0。也可以向DR寄存器的指定bit写入1/0来实现GPIO输出高/低电平。

GDIR 寄存器:方向寄存器

GDIR 寄存器也是 32 位的,此寄存器用来设置某个 IO 的工作方向,是输入还是输出。同样,每个 IO 对应一个位,如果要设置 GPIO 为输入的话就设置相应的位为 0,如果要设置为输出的话就设置为 1。比如要设置 GPIO1_IO00 为输入:GPIO1_GDIR=0。

PSR寄存器:GPIO 状态寄存器

PSR 寄存器也是一个 GPIO 对应一个位,读取相应的位即可获取对应的 GPIO 的状态,也就是 GPIO 的高低电平值。功能和输入状态下的 DR 寄存器一样。

ICR1和ICR2两个寄存器:都是中断控制寄存器。

ICR1用于配置低16个GPIO也就是 IO0~15 ,ICR2 用于配置高 16 个GPIO也就是IO16~31。举个例子:若要设置GPIO1_IO15为上升沿触发中断,那么GPIO1_ICR1=2<<30,如果要设置GPIO1 的 IO16~31 的话就需要设置 ICR2 寄存器了。ICR1 寄存器中一个 GPIO 用两个位来配置中断的触发方式如下表:

IMR 寄存器:中断屏蔽寄存器

IMR 寄存器也是一个 GPIO 对应一个位,本质上设置中断掩码,IMR 寄存器用来控制 GPIO 的中断禁止和使能,如果使能某个 GPIO 的中断,那么设置相应的位为 1 即可,反之,如果要禁止中断,那么就设置相应的位为 0 即可。例如,要使能 GPIO1_IO00 的中断,那么就可以设置 GPIO1_IMR=1 即可。

ISR 寄存器:中断状态寄存器

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

EDGE_SEL 寄存器:边沿选择寄存器

EDGE_SEL 也是一个32位寄存器,用来设置边沿中断,这个寄存器会覆盖 ICR1 和 ICR2 的设置,同样是一个 GPIO 对应一个位。如果相应的位被置 1,那么就相当于设置了对应的 GPIO 是上升沿和下降沿(双边沿)触发。例如,我们设置 GPIO1_EDGE_SEL=1,那么就表示 GPIO1_IO01 是双边沿触发中断,无论 GFPIO1_CR1 的设置为多少,都是双边沿触发。

汇编LED驱动编写

汇编编写LED驱动代码

第一步

首先使能I.MX6ULL所有外设的时钟,通过CCGR0~CCGR6这7个寄存器控制。

第二步

IO复用。配置IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03寄存器,将PIN口复用为gpio

第三步

配置电气属性。设置IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03寄存器。

第四步

配置gpio功能(输入/输出)

编译程序

如果在 Windows 下使用 Source Insight 编写的代码,就需要通过 FileZilla 将编写好的代码发送的 Ubuntu 中去编译。直接在 Ubuntu 下使用 VSCode 编译的代码,不需要使用 FileZilla 发送代码,可以直接进行编译。

第一步

首先将.c和.s文件变为.o文件

使用arm-linux-gnueabihf-gcc工具来编译,led.s 源文件,所以编译比较简单。先将 led.s 编译为对应的.o 文件,在终端中输入如下命令:

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 文件链接起来组合成可执行文件。

第二步

将所有的.o文件链接为elf格式的可执行文件(相比bin文件多了一些其它格式方面信息)

首先回忆一下STM32,一个 STM32 的工程,编译过后,在工程目录下的OBJ文件夹里面能找到很多.o文件,这些.o 文件会被 MDK 链接到某个地址去。0X08000000 就是 STM32 内部 ROM 的起始地址,编译出来的指令要从 0X08000000 这个地址开始存放。对于STM32 来说 0X08000000 就是它的链接地址,.o 文件就是这个链接地址开始依次存放,最终生成一个可以下载的 hex 或者 bin 文件,我们可以打开.map 文件查看一下这些文件的链接地址。在MDK下双击工程名称会打开对应工程的.map文件。从中可看出 STM32 的各个.o 文件所处的位置,起始位置是0X08000000。下图是STM32工程的.map文件。由此可知,用 MDK 开发 STM32 的时候也是有链接的,只是这些工作 MDK 都帮我们全部做好了,我们不用关心。但在 Linux 下用交叉编译器开发 ARM 的是时候就需要自己处理这些。

使用 arm-linux-gnueabihf-ld 链接文件,arm-linux-gnueabihf-ld 用来将众多的.o 文件链接到一起,并且链接一个指定的链接位置。我们这一步需要做的就是确定一下本试验最终的可执行文件其运行起始地址,也就是链接地址。

区分“存储地址”和“运行地址”两个概念。

“存储地址”就是可执行文件存储在哪里,可执行文件的存储地址可以随意选择。

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

正点原子裸机例程都是烧写到 SD 卡,上电以后 I.MX6U 的内部 boot rom 程序会将可执行文件拷贝到链接地址处。对于6ULL来说,链接地址应指向RAM地址。RAM分为内部RAM和外部RAM(外部RAM就是DDR)。链接地址可在 I.MX6U 的内部 128KB RAM 中(0X900000~0X91FFFF),也可在外部的 DDR 中。正点原子的所有裸机例程的链接地址都在DDR中,链接起始地址为 0X87800000。

而要使用DDR,必须要初始化DDR,对于IMX来说 bin 文件不能直接烧写到SD卡、EMMC、NAND等外置存储中然后从这些外置存储中启动运行。需要添加一个头部,这个头部信息包含了 DDR的初始化参数,IMX系列SOC内部 boot rom 会从SD卡,EMMC等外置存储中读取头部信息,然后初始化 DDR,并且将 bin 文件拷贝到指定的地方。Bin的运行地址一定要和链接起始地址一致。位置无关代码除外。虽然bin 文件不能直接烧写到SD卡、EMMC、NAND等外置存储中然后从这些外置存储中启动运行,但是使用JLINK将bin文件直接下载到内部RAM中还是可以运行的。

I.MX6U-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

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

第三步

将elf文件转为bin文件

第二步生成的led.elf 文件也不是我们最终烧写到 SD 卡中的可执行文件,我们要烧写的是.bin 文件,因此还需将 led.elf 文件转换为.bin 文件,这里就要用到 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 文件。

第四步

编译C语言的时候将elf文件转为汇编——反汇编操作

大多数情况下我们都是用 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

创建 Makefile 文件

如果我们修改了 led.s 文件,那么就需要在重复一次上面的这些命令,比较麻烦,此时我们就可以使用 Makefile 文件。

使用“touch”命令在工程根目录下创建一个名为“Makefile”的文件,创建好 Makefile 文件以后就需要根据 Makefile 语法编写 Makefile 文件了, Makefile 基本语法在本人之前Linux入门(三)-Linux_C编程这一篇里面有介绍,在 Makefile 中输入如下内容:

注意:每条命令前用Tab而不能用空格,空格会造成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”即可。

烧写bin文件

我们学习 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 卡以外,其他的一般都是焊接到了板子上的,我们没法直接烧写。但是 SD卡可以从板子上插拔。我们可以将 SD 卡插到电脑上,在电脑上使用软件将.bin文件烧写到 SD 卡中,然后再插到板子上即可,然后板子选择SD卡启动。

因此,我们在调试裸机和 Uboot 的时候是将代码下载到 SD 中,当调试完成以后将裸机或者 Uboot 烧写到 SPI NOR Flash、 EMMC、 NAND 等这些存储介质中。

如何将我们前面编译出来的 led.bin 烧写到 SD 卡中呢?编译出来的可执行文件是怎么存放到 SD 中的,存放的位置是什么?NXP对这些是有详细规定的!我们必须按照 NXP 的规定来将代码烧写到 SD 卡中,否则代码运行不起来。

烧写具体步骤

正点原子专门编写了一个软件“imxdownload”来将编译出来的.bin 文件烧写到 SD 卡中。imxdownlaod 只能在 Ubuntu 下使用,使用步骤如下:

1、将 imxdownload 拷贝到工程根目录下

将 imxdownload 拷贝到工程根目录下,也就是和 led.bin 处于同一个文件夹下。

2、给予 imxdownload 可执行权限

直接将软件 imxdownload 从 Windows 下复制到 Ubuntu 中以后, imxdownload 默认是没有可执行权限的。我们需要给予 imxdownload 可执行权限,使用命令“chmod”。如下图:

当给予 imxdownload 可执行权限以后其文件名字变成了绿色的,对比第一步的图片中的文件名,如果没有可执行权限的话其名字颜色是白色的。我们可以从文件名字的颜色初步判断其是否具有可执行权限。

3、确定要烧写的 SD 卡。

准备一张新的 SD(TF)卡,确保 SD 卡里面没有数据,因为在烧写代码的时候可能会格式化 SD 卡!
Ubuntu 下所有的设备文件都在目录“/dev”里面,所以插上 SD 卡以后也会出现在“/dev”里,其中存储设备都是以“/dev/sd”开头的。在不插 SD 卡的时候先看看电脑都有哪些存储设备,然后插入 SD 卡以后看新增的那个设备就是SD卡设备。(使用读卡器将 SD 卡插到电脑,一定要确保 SD 卡是挂载到了 Ubuntu 系统中,而不是 Windows下)。输入如下所示命令查看 /dev挂载的设备:

ls /dev/sd*

4、向 SD 卡烧写 bin 文件

使用 imxdownload 向 SD 卡烧写 led.bin 文件,命令格式如下:

./imxdownload <.bin file> <SD Card>

其中.bin 就是要烧写的.bin 文件, SD Card 就是你要烧写的 SD 卡,比如我的电脑使用如下命令烧写 led.bin 到/dev/sdb 中:

./imxdownload led.bin /dev/sdb      //不能烧写到/dev/sda 或 sda1 设备里面!那是系统磁盘
我在这遇到两个小问题
第一个问题:我SD卡分了两个区,我执行 ./imxdownload led.bin /dev/ sdb1 命令的,这样I.MX6U貌似读取不到程序。必须执行./imxdownload led.bin /dev/ sdb 命令,将程序烧到sdb里面,这样子才能实现实验效果。(感觉类似于只能读取第一层目录,深度层次的目录读取不到这个意思。具体的我也没有深究,有知道具体是什么原因的友友欢迎评论一下呀!)

第二个问题:快速插拔SD卡或者在没有插入SD卡时执行了烧录命令(./imxdownload)导致所烧录至的目标设备 /dev/sdb变为了假的设备节点,颜色变为灰色的,可使用下面命令删除此节点。

sudo rm -rf /dev/sdb

烧写的最后一行会显示烧写大小、用时和速度,比如下图中: led.bin 烧写到 SD 卡中的大小是 3.3KB,用时 0.122869s,烧写速度是 26.5KB/s。注意这个烧写速度,如果这个烧写速度在几百 KB/s 以下那么就是正常烧写。如果这个烧写速度大于几十 MB/s、甚至几百 MB/s 那么肯定是烧写失败了!解决方法只能重启Ubuntu。

烧写完成以后会在当前工程目录下生成一个 load.imx 的文件,load.imx 这个文件就是软件imxdownload 根据 NXP 官方启动方式的规定,在 led.bin 文件前面添加了一些数据头以后生成的。最终烧写进到 SD 卡里面的是这个 load.imx 文件,而非led.bin。

代码验证

代码已经烧写到了 SD 卡中了,接下来就是将 SD 卡插到开发板的 SD 卡槽中,然后设置拨码开关为 SD 卡启动,设置好以后按一下开发板的复位键,如果代码运行正常的话 LED0 就会被点亮。

会有很短一段时间的延迟才会点亮LED灯的原因:首先上电之后,主芯片它的内部Boot ROM运行,然后检测用户设置的启动方式,检测到是SD卡启动后,再去读SD卡里面内容。然后读取一些配置信息,主芯片再根据你设置的配置信息配置自己的片上外设,比如:DDR,因为我们配置把bin文件拷贝到DDR的0x87800000这个起始地址去。主芯片它的内部Boot ROM就会把bin文件拷贝到DDR的0x87800000,主芯片怎么知道要把bin文件拷贝到DDR的0x87800000地址,因为用户配置,在编译程序第二步设置的链接地址。

C语言LED驱动程序编写

一、构建C语言运行环境

1、设置处理器模式

设置6ULL处于SVC模式下,将CPSR寄存器的bit4~0,也就是M[4:0],设置为10011=0x13。读写状态寄存器需要MRS和MSR指令。

这里回忆一下MRS和MSR指令:

MRS 指令,用于将特殊寄存器(如 CPSR 和 SPSR)中的数据传递给通用寄存器,要读取特殊寄存器的数据只能使用 MRS 指令!

MRS  R0, CPSR       @将特殊寄存器 CPSR 里面的数据传递给 R0

MSR 指令,用来将通用寄存器的数据传递给特殊寄存器,也就是写特殊寄存器,写特殊寄存器只能使用 MSR!

MSR  CPSR, R0       @将 R0 中的数据复制到特殊寄存器CPSR 中

2、设置sp指针

sp可以指向内部RAM,也可以指向DDR,我们将其指向DDR。

sp指针具体设置到哪里

首先看DDR的存储范围,512MB的范围:0x80000000~0x9FFFFFFF,其次,确定处理器栈增长方式,对于Cortex-A7而言是向下增长,这样子的话sp指针就不能指向0x80000000,否则指针一移动就不知道指向到内存哪个位置了,就会低于0x80000000了。设置栈大小0x200000=2MB,sp指针则指向0x80200000=0x80000000+0x20000的位置。当出栈入栈的时候sp就是从0x80200000位置向下移动,2MB的空间用来写裸机程序完全够用不用担心溢出。

为啥一般的芯片的开始地址是0x80000000,那是因为芯片制造商的想法,一个颗32位架构的芯片它的寻址空间为:0 ~ 0xFFFFFFFF。0xFFFFFFFF是一个32位16进制地址。这个地址代表4GB大小。在整个4GB的地址空间内,前2GB的地址空间有其他用处,后2GB的地址空间才分配给了内存控制器,所以在操作内存地址的时候都是从0x80000000地址开始的,这其实就限制了该类型(32位架构)的芯片最大支持内存为4GB,而i.MX6ULL芯片所支持的最大的内存为2GB。【16进制地址最高位(第8位)1个单位代表了 4GB/16=256M 如地址:0x10000000 为256M的地址。16进制地址最高位(第7位)1个单位代表了 256M/16=16M 如地址:  0x1000000 为16M的地址。依次类推。】

3、跳转到C语言

使用汇编的 b指令跳转到C语言的函数,比如:main函数。裸机程序一般main函数作为入口函数,跳到main函数里面去即可开始执行C语言的main函数。

设置sp指针指向的位置之前一定要初始化DDR,有的处理器并没有在bin文件之前初始化DDR,需要自行先配置DDR初始化,再设置sp指针,比如三星的处理器。NXP的处理器已经在bin文件之前初始化好了,不需要再自行配置。

I.MX6U 的汇编部分代码和 STM32 的启动文件 startup_stm32f10x_hd.s 很类似,本实验初始化 C 环境的汇编部分代码如下:

.global _start     /* 定义一个全局标号_start */
    /* 描述:_start 函数,程序从此函数开始执行,此函数主要功能是设置 C 运行环境。*/
_start:        /* 全局标号_start开始的地方 */
    /* 设置处理器进入 SVC 模式 */
    mrs r0, cpsr
    bic r0, r0, #0x1f /* 将 r0 的低 5 位清零,也就是 cpsr 的 M0~M4 */
    orr r0, r0, #0x13 /* r0 或上 0x13,表示使用 SVC 模式 */
    msr cpsr, r0 /* 将 r0 的数据写入到 cpsr_c 中 */
    ldr sp, =0X80200000 /* 设置栈指针 */
    b main /* 跳转到 main 函数 */

汇编部分程序就几行代码,用来设置处理器运行到 SVC 模式下、然后初始化 SP 指针、最终跳转到 C 文件的 main 函数中。

注意:如果使用的并非NXP处理器,比如:三星的 S3C2440 或 S5PV210 的在使用 SDRAM 或者 DDR 之前必须先初始化 SDRAM 或 DDR。所以使用S3C2440或 S5PV210处理器软件部分的汇编文件里面一定有 SDRAM 或 DDR 初始化代码,DDR3 肯定是要初始化的。

若使用NXP处理器,但是不需要在 start.s 文件中完成。比如:上面编写的start.s 文件中就没有初始化 DDR3 的代码,但是将 SVC 模式下的 SP 指针设置到了 DDR3 的地址范围中。之前分析 DCD 数据的时已知,DCD 数据包含了 DDR 配置参数,I.MX6U 内部的 Boot ROM 会读取 DCD 数据中的 DDR 配置参数然后完成 DDR 初始化。

二、C 语言部分实验程序编写

代码部分(.h和.c文件)

在对芯片进行开发时,我们对芯片的操作本质上就是对芯片底层寄存器进行操作,在C语言中对寄存器进行操作则是通过寄存器的地址进行数据的赋值。

#ifndef __MAIN_H
#define __MAIN_H

//CCM 相关寄存器地址
#define  CCM_CCGR0 		*((volatile unsigned int *)0X020C4068)
#define  CCM_CCGR1 		*((volatile unsigned int *)0X020C406C)
#define  CCM_CCGR2 		*((volatile unsigned int *)0X020C4070)
#define  CCM_CCGR3 		*((volatile unsigned int *)0X020C4074)
#define  CCM_CCGR4 		*((volatile unsigned int *)0X020C4078)
#define  CCM_CCGR5 		*((volatile unsigned int *)0X020C407C)
#define  CCM_CCGR6 		*((volatile unsigned int *)0X020C4080)

//IOMUX 相关寄存器地址
#define SW_MUX_GPIO1_IO03 *((volatile unsigned int *)0X020E0068)
#define SW_PAD_GPIO1_IO03 *((volatile unsigned int *)0X020E02F4)

//GPIO1 相关寄存器地址
#define GPIO1_DR *((volatile unsigned int *)0X0209C000)
#define GPIO1_GDIR *((volatile unsigned int *)0X0209C004)
#define GPIO1_PSR *((volatile unsigned int *)0X0209C008)
#define GPIO1_ICR1 *((volatile unsigned int *)0X0209C00C)
#define GPIO1_ICR2 *((volatile unsigned int *)0X0209C010)
#define GPIO1_IMR *((volatile unsigned int *)0X0209C014)
#define GPIO1_ISR *((volatile unsigned int *)0X0209C018)
#define GPIO1_EDGE_SEL *((volatile unsigned int *)0X0209C01C)

#endif

这段代码用宏定义来替换掉寄存器的地址,方便用户直接用宏定义名称来操作寄存器。这部分代码用来映射寄存器,使用 volatile unsigned int * 来将一个寄存器地址强制转为 unsigned int 的指针,同时使用 volatile 关键字对寄存器地址进行修饰,告诉编译器,该地址运行过程中随时可能变化,编译时不要过优化该地址。在将寄存器地址强制转化为指针地址后,在前面在加上 * 指向地址存放的值,之后我们直接调用宏定义就可以操作寄存器了,如下(使能 I.MX6U 外设时钟,初始化GPIO等,都是调用宏定义操作寄存器直接赋值)

#include "main.h"

void clk_enable(void)  //使能 I.MX6U 所有外设时钟
{
    CCM_CCGR0 = 0xffffffff;
    CCM_CCGR1 = 0xffffffff;
    CCM_CCGR2 = 0xffffffff;
    CCM_CCGR3 = 0xffffffff;
    CCM_CCGR4 = 0xffffffff;
    CCM_CCGR5 = 0xffffffff;
    CCM_CCGR6 = 0xffffffff;
}
void led_init(void)  //初始化 LED 对应的 GPIO
{
    // 1、初始化 IO 复用, 复用为 GPIO1_IO03
    SW_MUX_GPIO1_IO03 = 0x5;
    /* 2、配置 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 低转换率
     */
     SW_PAD_GPIO1_IO03 = 0X10B0;
    //3、初始化 GPIO, GPIO1_IO03 设置为输出
     GPIO1_GDIR = 0X0000008;
    // 4、设置 GPIO1_IO03 输出低电平,打开 LED0
     GPIO1_DR = 0X0;
}
void led_on(void)  //打开LED灯
{
    //将 GPIO1_DR 的 bit3 清零
    GPIO1_DR &= ~(1<<3);
}
void led_off(void)  //关闭LED灯
{
    //将 GPIO1_DR 的 bit3 置 1
    GPIO1_DR |= (1<<3);
}
//短时间延时函数
void delay_short(volatile unsigned int n)  //参数:要延时循环次数(空操作循环次数,模式延时)
{
    while(n--){}
}
//延时函数,在 396Mhz 的主频下延时时间大约为 1ms
void delay(volatile unsigned int n)  //参数:要延时的 ms 数
{
    while(n--)
    {
        delay_short(0x7ff);
    }
}
int main(void)  //main函数
{
    clk_enable(); /* 使能所有的时钟 */
    led_init(); /* 初始化 led */

    while(1) /* 死循环 */
    {
        led_off(); /* 关闭 LED */
        delay(500); /* 延时大约 500ms */
        led_on(); /* 打开 LED */
        delay(500); /* 延时大约 500ms */
    }
    return 0;
}

main.c文件里面有7个函数。

  • clk_enable函数是使能CCGR0~CCGR6所控制的所有外设时钟。
  • led_init函数是初始化 LED 灯所使用的 IO,包括设置IO的复用功能、 IO的属性配置和GPIO功能,最终控制GPIO输出低电平来打开 LED 灯。
  • led_on 和 led_off 用来控制 LED 灯亮灭。
  • delay_short()和 delay()是延时函数,delay_short()函数靠空循环来实现延时,delay()是对 delay_short()的简单封装,在 I.MX6U 工作在396MHz(Boot ROM 设置的396MHz) 的主频时delay_short(0x7ff)基本能够实现大约 1ms 的延时,所以 delay()函数我们可以用来完成 ms 延时。
  • main函数就是主函数了,main 函数中先调用函数 clk_enable()和 led_init()来完成时钟使能和 LED 初始化,最终在while(1)循环中实现 LED 循环亮灭,亮灭时间大约是 500ms。

编写Makefile

如下Makefile,用到了 Makefile 变量和自动变量。

objects = start.o main.o
ledc.bin:$(objects)
	arm-linux-gnueabihf-ld -Ttext 0x87800000 $^ -o ledc.elf 
	arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
	arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis

%.o:%.s
	arm-linux-gneuabihf-gcc -Wall -nostdlib -c -o $@ $<

%.o:%.c
	arm-linux-gneuabihf-gcc -Wall -nostdlib -c -o $@ $<

clean:
	rm -rf *.o ledc.elf ledc.dis ledc.bin

-Ttext 就是指定链接地址

“-o”选项指定链接生成的 elf 文件名,这里我们命名为 led.elf

-Wall表示显示编译的时候所有警告

-nostdlib表示不链接系统标准启动文件和库文件,否则编译可能会出错

-O2表示优化等级,和MDK上的设置含义一样。

第1行定义了一个变量objects,objects包含要生成 ledc.bin 所需的依赖文件: start.o 和 main.o,也就是当前工程下 start.s 和 main.c 这两个文件编译后的.o 文件。注意 start.o 一定要放到最前面!因为在后面链接的时候 start.o 在最前面,start.o是最先要执行的文件!
第2行就是默认目标,目的是生成最终的可执行文件 ledc.bin,ledc.bin 依赖start.o和main.o,如果当前工程没有 start.o 和 main.o 的时候就会找到相应的规则去生成 start.o 和 main.o。比如start.o 是 start.s 文件编译生成的,因此会执行第7行的规则。
第3行是使用 arm-linux-gnueabihf-ld 进行链接,链接起始地址是 0X87800000,这一行用到了自动变量“$^”,“$^”意思是所有依赖文件的集合,在这里就是 objects这个变量的值:start.o 和main.o。链接的时候 start.o 要链接到最前面,因为第一行代码就是 start.o 里面的,因此这一行就相当于:

arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf start.o main.o

第4行使用 arm-linux-gnueabihf-objcopy将 ledc.elf 文件转为 ledc.bin,也用到了自动变量“$@”,“$@”的意思是目标集合,在这里就是“ledc.bin”,那么本行就相当于:

arm-linux-gnueabihf-objcopy -O binary -S ledc.elf ledc.bin

第5行使用 arm-linux-gnueabihf-objdump 来反汇编,生成 ledc.dis 文件。

第7~11行就是针对不同的文件类型将其编译成对应的.o 文件,其实就是汇编.s和.c 文件,比如 start.s 就会使用第7行的规则来生成对应的 start.o 文件。

第8行就是具体的命令,也用到了自动变量“$@”和“$<”,其中“$<”是依赖目标集合的第一个文件。比如start.s 要编译成 start.o 的话第7行和第8行就相当于:

start.o:start.s
    arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o start.o start.s

第13行就是工程清理规则,通过命令“make clean”就可以清理工程。

最后,在执行make命令编译完成以后可以使用软件 imxdownload 将其下载到 SD 卡中

chmod 777 imxdownload //给予 imxdownoad 可执行权限,一次即可
./imxdownload ledc.bin /dev/sdb //下载到 SD 卡中, 不能烧写到/dev/sda 或 sda1 设备里面!

三、链接脚本

在上面的 Makefile 中我们链接代码的时候使用如下语句:

arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^

        上面语句中我们通过“-Ttext”来指定链接地址是 0X87800000,这样的话所有的文件都会链接到以 0X87800000 为起始地址的区域。但有时我们很多文件需链接到指定区域,或者叫做段里面,比如Linux里面初始化函数就会放到 init 段里面。因此我们需要能够自定义一些段,这些段的起始地址我们可以自由指定,同样的我们也可以指定一个文件或者函数应该存放到哪个段里面去。要完成这个功能我们就需要使用到链接脚本,链接脚本主要用于描述文件应该如何被链接在一起形成最终的可执行文件。其主要目的是描述输入文件中的段如何被映射到输出文件中,并且控制输出文件中的内存排布。比如我们编译生成的文件一般都包含 text 段、data 段等等。

        链接脚本的语法很简单,就是编写一系列的命令组成了链接脚本,每个命令是一个带有参数的关键字或一个对符号的赋值,可用分号分隔命令。像文件名之类的字符串可以直接键入,也可以使用通配符“*”。最简单的链接脚本可以只包含一个命令“SECTIONS”,我们可在这一个“SECTIONS”里面来描述输出文件的内存布局。我们一般编译出来的代码都包含在 text、 data、 bss 和 rodata 这四个段内。

        假设现在的代码要被链接到 0X10000000 这个地址,数据要被链接到 0X30000000 这个地方,下面就是完成此功能的最简单的链接脚本:

SECTIONS{
    . = 0X10000000;
    .text : {*(.text)}
    . = 0X30000000;
    .data ALIGN(4) : { *(.data) }
    .bss ALIGN(4) : { *(.bss) }
}

第 1 行先写了一个关键字“SECTIONS”,看起来就跟 C 语言里面的函数一样。

第 2 行对一个特殊符号“ . ”进行赋值,“  .  ”在链接脚本里面叫做定位计数器,默认的定位计数器为 0。要求代码链接到以 0X10000000 为起始地址的地方,因此这一行给“ . ”赋值0X10000000,表示以 0X10000000 开始,后面的文件或者段都会以 0X10000000 为起始地址开始链接。

第 3 行“.text”是段名,后面的冒号是语法要求,冒号后面的大括号里面可以填上要链接到“.text”这个段里面的所有文件,“ *(.text) ”中的“ * ”是通配符,表示所有输入文件的.text段都放到“.text”中。

第 4 行,要求是数据放到 0X30000000 开始的地方,所以我们需重新设置定位计数器“ . ”,将其改为 0X30000000。

如果不重新设置的话会怎么样?

假设“.text”段大小为 0X10000,那么接下来的.data 段开始地址就是0X10000000+0X10000=0X10010000,这明显不符合我们的要求。所以我们必须调整定位计数器为 0X30000000。

第 5 行跟第 3 行一样,定义了一个名为“.data”的段,然后所有文件的“.data”段都放到这里面。但是这一行多了一个“ALIGN(4)”。这是用来对“.data”这个段的起始地址做字节对齐的, ALIGN(4)表示 4 字节对齐。也就是说段“.data”的起始地址要能被 4 整除,一般常见都是 ALIGN(4)或ALIGN(8),也就是 4 字节或者 8 字节对齐。

第 6 行定义了一个“.bss”段,所有文件中的“.bss”数据都会被放到这个里面,“.bss”数据就是那些定义了但是没有被初始化的变量。

上面是链接脚本最基本的语法格式,接下来按照基本的语法格式编写我们本试验的链接脚本,我们本试验的链接脚本要求如下:

  • 链接起始地址为 0X87800000。
  • start.o 要被链接到最开始的地方,因为 start.o 里面包含这第一个要执行的命令。
SECTIONS{
    . = 0X87800000;  //定义链接起始地址,然后后面定义不同的段
    .text:       //创建一个.text段,.text是段名,后面{}里面表示所有输入文件的.text
    {
        start.o    //start.o 里包含着第一个要执行的指令,一定要链接到最开始的地方
        *(.text)    //后面“ * ”通配符表示所有其它输入文件的.text,这里就表示main.o
    }               //程序编译出来都属于text
    .rodata ALIGN(4): {*(.rodata*)}    //创建一个只读数据段.rodata,保存.rodata*数据
    .data ALIGN(4) : {*(.data)}     //再创建一个数据段.data,其它所有的数据放到.data里面
    __bss_start=.;
    .bss ALIGN(4) : {*(.bss)*(COMMON)}
    __bss_end=.;
}

“__bss_start”和“__bss_end”是符号,第10、12这两行是对这两个符号进行赋值,其值为定位符“.”,这两个符号用来保存.bss 段的起始地址和结束地址,.bss 段是定义了但是没有被初始化的变量,我们需要手动对.bss 段的变量清零的,因此我们需要知道.bss 段的起始和结束地址,这样我们直接对这段内存赋 0 即可完成清零。通过第10、12行代码,.bss 段的起始地址和结束地址就保存在了“__bss_start”和“__bss_end”中,我们就可以直接在汇编或者 C 文件里面使用这两个符号。

写在最后:本篇以点亮LED灯试验为示例,记录了如何配置寄存器,编写驱动,编译代码,并将代码烧写进 SD 卡中进行测试。代码有汇编LED驱动代码和C语言LED驱动代码。如果文章帮到你了,麻烦帮忙点个赞哦!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值