ARM裸机篇(三)——i.MX6ULL第一个裸机程序

linux系列目录:
linux基础篇(一)——GCC和Makefile编译过程
linux基础篇(二)——静态和动态链接
ARM裸机篇(一)——i.MX6ULL介绍
ARM裸机篇(二)——i.MX6ULL启动过程
ARM裸机篇(三)——i.MX6ULL第一个裸机程序
ARM裸机篇(四)——重定位和地址无关码
ARM裸机篇(五)——异常和中断
linux系统移植篇(一)—— linux系统组成
linux系统移植篇(二)—— Uboot使用介绍
linux系统移植篇(三)—— Linux 内核使用介绍
linux系统移植篇(四)—— 根文件系统使用介绍
linux驱动开发篇(一)—— Linux 内核模块介绍
linux驱动开发篇(二)—— 字符设备驱动框架
linux驱动开发篇(三)—— 总线设备驱动模型
linux驱动开发篇(四)—— platform平台设备驱动


文章目录

  • 一、汇编基础
  • 二、点亮LED灯


一、汇编基础

  1. 处理器内部数据传输指令
    在这里插入图片描述
  2. 存储器访问指令
    在这里插入图片描述
  3. 压栈和出栈指令
    在这里插入图片描述
  4. 跳转指令
    ARM裸机篇(三)——i.MX6ULL第一个裸机程序_第1张图片
  5. 算术运算指令
    ARM裸机篇(三)——i.MX6ULL第一个裸机程序_第2张图片
  6. 逻辑运算指令
    ARM裸机篇(三)——i.MX6ULL第一个裸机程序_第3张图片

二、点亮LED灯

实际开发过程中汇编用的很少,只有在C语言环境没有准备好的情况下,才必须使用汇编,比如初始化 DDR、设置堆栈指针 SP 等。
I.MX6U 内部的 Boot ROM 会读取 DCD 数据中的 DDR 配置参数然后完成 DDR 初始化的,所以我们编写的汇编就可以省了这一部分,只需要设置好栈指针就可以使用C语言了。

编写代码之前,先搞清楚以下几个要点:

  1. C函数为何要用栈
    总的来说,栈的作用就是:保存现场/上下文,传递参数。
  • 保存现场/上下文
    现场,相当于案发现场,总有一些现场的情况,要记录下来的,否则被别人破坏掉之后,你就无法恢复现场了。而此处说的现场,就是指CPU运行的时候,用到的一些寄存器,比如R0~R3,LR等等,对于这些寄存器的值,如果你不保存而直接跳转到函数中去执行,那么很可能会被破坏了,因为函数执行需要用到这些寄存器。
    因此在函数调用之前,应该将这些寄存器等现场,暂时保持起来,等调用函数执行完毕返回后,再恢复现场,这样CPU就可以正确的继续执行了。

  • 传递参数
    当函数被调用并且参数大于4个时,(不包括第4个参数)第4个参数后面的参数就保存在栈中。

  1. 汇编如何调用C函数
  • 当参数小于等下4个时,使用寄存器R0~R3来进行参数传递
  • 当参数大于4个时,前四个参数按照上面方法传递,剩余参数传送到栈中,入栈的顺序与参数顺序相反,即最后一个参数先入栈
  1. C程序如何返回结果给汇编程序
  • 结果为一个32位的整数时,通过寄存器R0返回
  • 结果为一个64位整数时,通过R0和R1返回,依此类推.
  • 结果为一个浮点数时,通过浮点运算部件的寄存器f0,d0或s0返回
  • 结果为一个复合的浮点数时,通过寄存器f0-fN或者d0~dN返回
  • 对于位数更多的结果,通过调用内存来传递

编写汇编部分:

.text            //代码段
.align 2         //设置字节对齐
.global _start   //定义全局变量

_start:          //程序的开始
	b reset      //跳转到reset标号处

    reset:
    mrc     p15, 0, r0, c1, c0, 0     /*读取CP15系统控制寄存器   */
    bic     r0,  r0, #(0x1 << 12)     /*  清除第12位(I位)禁用 I Cache  */
    bic     r0,  r0, #(0x1 <<  2)     /*  清除第 2位(C位)禁用 D Cache  */
    bic     r0,  r0, #0x2             /*  清除第 1位(A位)禁止严格对齐   */
    bic     r0,  r0, #(0x1 << 11)     /*  清除第11位(Z位)分支预测   */
    bic     r0,  r0, #0x1             /*  清除第 0位(M位)禁用 MMU   */
    mcr     p15, 0, r0, c1, c0, 0     /*  将修改后的值写回CP15寄存器   */

    ldr sp, =0x80200000   //栈指针暂时设置到这个位置
    b main        //跳转到main函数执行
    /*进入死循环*/
loop:
    b loop


编写C语言部分:


#define CCM_CCGR1 (volatile unsigned long*)0x20C406C          //时钟控制寄存器
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04 (volatile unsigned long*)0x20E006C//GPIO1_04复用功能选择寄存器
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04 (volatile unsigned long*)0x20E02F8 //PAD属性设置寄存器
#define GPIO1_GDIR (volatile unsigned long*)0x0209C004 //GPIO方向设置寄存器(输入或输出)
#define GPIO1_DR (volatile unsigned long*)0x0209C000   //GPIO输出状态寄存器


#define uint32_t  unsigned int 

/*简单延时函数*/
void delay()
{
	static uint32_t delay_time = 0x1FFFF;
	do{
		delay_time --;
	}
    while(delay_time);
	delay_time = 0x1FFFF;
}

int main()
{
    *(CCM_CCGR1) = 0xFFFFFFFF;   //开启GPIO1的时钟
    *(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04) = 0x5;//设置PAD复用功能为GPIO
    *(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04) = 0x1F838;//设置PAD属性
    *(GPIO1_GDIR) = 0x10;//设置GPIO为输出模式
    *(GPIO1_DR) = 0x0;   //设置输出电平为低电平

    while(1)
    {
        *(GPIO1_DR) = 0x0;
        delay();
        *(GPIO1_DR) = 1<<4;
        delay();
    }
    return 0;    
}

编写链接脚本:
链接脚本主要目的是描述输入文件中的段如何被映射到输出文件中,并且控制输出文件中的内存排布。
gcc的链接脚本后缀一般为lds,编写led.lds文件:

ENTRY(_start)
SECTIONS {
    . = 0X87800000;

    . = ALIGN(4);
    .text      :
    {
	   start.o (.text)
	   *(.text)
    }

    . = ALIGN(4);
    .rodata : { *(.rodata) }

    . = ALIGN(4);
    .data : { *(.data) }

    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss) *(.COMMON) }
    __bss_end = .;
}



结合代码各部分讲解如下:

  • 第1行,ENTRY(_start) 用于指定程序的入口,ENTRY( )是设置入口地址的命令, “_start”是程序的入口,本章的led程序的入口地址位于start.S的“_start”标号处。

  • 第2行,定义SECTIONS。SECTIONS可以理解为是一块区域,我们在这块区域排布我们的代码, 链接时链接器就会按照这里的指示链接我们的代码。

  • 第3行,“.”运算符代表当前位置。 我们在SECTION的最开始使用“.= 0X87800000”就是将链接起始地址设置为0X87800000。

  • 第5行,设置字节对齐。这里同样用到了“.”运算符,它表示从当前位置开始执行四字节对齐。 假设当前位置为0x80000001,执行该命令后当前地址将会空出三个字节转到0x80000004地址处。

  • 第6行,定义代码段。“.text :”用于定义代码段,固定的语法要求,我们按照要求写即可。 在“{}”中指定那些内容放在代码段。将start.o中的代码放到代码段的最前面。start.S是启动代码应当首先被执行, 所以通常情况下要把它放到代码段的最前面,其他源文件的代码按照系统默认的排放顺序即可, 通配符“*”在这里表示其他剩余所有的.o文件。

  • 第7-10行,设置数据段。同设置代码段类似,首先设置字节对齐,然后定义代码段。在代码段里使用“*”通配符, 将所有源文件中的代码添加到这个代码段中。

  • 第11-13行,设置BSS段。.bss 段的起始地址和结束地址保存在了“__bss_start”和“__bss_end”中,我们就可以直接在汇编或者 C 文件里面使用这两个符号。

连接器脚本编写完成后,在链接指令中使用链接脚本替换-Ttext 0X87800000

arm-none-eabi-ld -Tled.lds  $^ -o led.elf

编写makefile文件:
我们程序编写完成后需要依次输入编译、链接、格式转换 命令才能最终生成二进制文件。这种编译方式效率低、容易出错。可以使用makefile文件来干这件事。
makefile文件:

all: start.o led.o 
	arm-none-eabi-ld -Tled.lds  $^ -o led.elf
	arm-none-eabi-objcopy -O binary -S -g led.elf led.bin
	arm-none-eabi-objdump -D -m arm  led.elf  > led.dis
	
%.o : %.S
	arm-none-eabi-gcc -g -c $^ -o start.o
%.o : %.c
	arm-none-eabi-gcc -g -c $^ -o led.o


.PHONY: clean
clean:
	rm *.o *.elf *.bin
	

链接脚本led.lds和makefile编写完成后,就可以直接使用make命令,来一键完成编译和链接了。
在这里插入图片描述

然后,只需要把led.bin通过烧录工具制作成镜像文件烧录到SD卡就可以启动了。

你可能感兴趣的:(linux,linux,linux裸机,linux汇编点亮LED)