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平台设备驱动
实际开发过程中汇编用的很少,只有在C语言环境没有准备好的情况下,才必须使用汇编,比如初始化 DDR、设置堆栈指针 SP 等。
I.MX6U 内部的 Boot ROM 会读取 DCD 数据中的 DDR 配置参数然后完成 DDR 初始化的,所以我们编写的汇编就可以省了这一部分,只需要设置好栈指针就可以使用C语言了。
编写代码之前,先搞清楚以下几个要点:
保存现场/上下文
现场,相当于案发现场,总有一些现场的情况,要记录下来的,否则被别人破坏掉之后,你就无法恢复现场了。而此处说的现场,就是指CPU运行的时候,用到的一些寄存器,比如R0~R3,LR等等,对于这些寄存器的值,如果你不保存而直接跳转到函数中去执行,那么很可能会被破坏了,因为函数执行需要用到这些寄存器。
因此在函数调用之前,应该将这些寄存器等现场,暂时保持起来,等调用函数执行完毕返回后,再恢复现场,这样CPU就可以正确的继续执行了。
传递参数
当函数被调用并且参数大于4个时,(不包括第4个参数)第4个参数后面的参数就保存在栈中。
编写汇编部分:
.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卡就可以启动了。