实验板使用的是 Cortex-A7 架构,完成LED闪烁功能,初步体验Linux开发。
单片机开发时,每个工程会有一个.s启动文件,Linux裸机开发同样需要,.s文件使用汇报语言,一般用于设置堆栈、工作模式、程序跳转等工作。
Cortex-A7 处理器有 9 种处理模式
程序状态寄存器(CPSR):
sp指针可以指向内部RAM,也可以指向DDR。这里我们指向512MB的DDR存储器,地址范围是0x80000000~0x9FFFFFFF。
A7处理器栈的增长方式是向下增长,若设置栈大小为0x200000=2MB,其实地址为0x80000000,那么sp指针应设置为0x80200000。
启动文件负责跳转到C语言的main函数。
.global _start /* 全局标号 */
/* * 描述: _start函数,程序从此函数开始执行,此函数主要功能是设置C运行环境。 */
_start:
/* 进入SVC模式 */
mrs r0, cpsr /* 将CPSR的值读到R0寄存器中 */
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函数 */
创建C文件
#include "main.h"
/* * @description : 使能I.MX6U所有外设时钟
* @param : 无
* @return : 无 */
void clk_enable(void)
{
CCM_CCGR0 = 0xffffffff;
CCM_CCGR1 = 0xffffffff;
CCM_CCGR2 = 0xffffffff;
CCM_CCGR3 = 0xffffffff;
CCM_CCGR4 = 0xffffffff;
CCM_CCGR5 = 0xffffffff;
CCM_CCGR6 = 0xffffffff;
}
/* * @description : 初始化LED对应的GPIO
* @param : 无
* @return : 无 */
void led_init(void)
{
/* 1、初始化IO复用 */
SW_MUX_GPIO1_IO03 = 0x5; /* 复用为GPIO1_IO03 */
/* 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_GDIR = 0X0000008; /* GPIO1_IO03设置为输出 */
/* 4、设置GPIO1_IO03输出低电平,打开LED0 */
GPIO1_DR = 0X0;
}
/* * @description : 打开LED灯
* @param : 无
* @return : 无 */
void led_on(void)
{
/*
* 将GPIO1_DR的bit3清零
*/
GPIO1_DR &= ~(1<<3);
}
/* * @description : 关闭LED灯
* @param : 无
* @return : 无 */
void led_off(void)
{
/*
* 将GPIO1_DR的bit3置1
*/
GPIO1_DR |= (1<<3);
}
/* * @description : 短时间延时函数
* @param - n : 要延时循环次数(空操作循环次数,模式延时)
* @return : 无 */
void delay_short(volatile unsigned int n)
{
while(n--){}
}
/* * @description : 延时函数,在396Mhz的主频下
* 延时时间大约为1ms
* @param - n : 要延时的ms数
* @return : 无 */
void delay(volatile unsigned int n)
{
while(n--)
{
delay_short(0x7ff);
}
}
/* * @description : mian函数
* @param : 无
* @return : 无 */
int main(void)
{
clk_enable(); /* 使能所有的时钟 */
led_init(); /* 初始化led */
while(1) /* 死循环 */
{
led_off(); /* 关闭LED */
delay(500); /* 延时大约500ms */
led_on(); /* 打开LED */
delay(500); /* 延时大约500ms */
}
return 0;
}
创建头文件:
#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
Make工具:
利用make可以自动完成编译工作
利用这种自动编译可以简化开发工作,避免不必要的重新编译。Make工具通过一个makefile文件来完成并自动维护编译工作,makefile文件描述了整个工程的编译、连接规则。
目标…... : 依赖文件集合……
命令 1
命令 2
…
第1条规则:
1 main: main.o input.o calcu.o
2 gcc -o main main.o input.o calcu.o
第2条规则:
3 main.o: main.c
4 gcc -c main.c
第3条规则:
5 input.o: input.c
6 gcc -c input.c
第4条规则:
7 calcu.o: calcu.c
8 gcc -c calcu.c
第5条规则:
10 clean:
11 rm *.o
12 rm main
跟 C 语言一样 Makefile 也支持变量的,先看一下前面的例子
1 #Makefile 变量的使用
2 objects = main.o input.o calcu.o
3 main: $(objects)
4 gcc -o main $(objects)
使用“=”在给变量的赋值的时候,不一定要用已经定义好的值,也可以使用后面定义的值。
name = zxl
curname = $(name)
name = zhongnanyin
print:
@echo curname: $(curname)
则输出结果是:zhongnanyin
借助另外一个变量,可以将变量的真实,值推到后面去定义。也就是变量的真实值取决于它所引用的变量的最后一次有效值。
name = zxl
curname := $(name)
name = zhongnanyin
print:
@echo curname: $(curname)
输出结果是:zxl
赋值符“:=”不会使用后面定义的变量,只能使用前面已经定义好的,这就是“=”和“:=”两个的区别。
curname ?= zhongnanyin。
上述代码的意思就是,如果变量 curname 前面没有被赋值,那么此变量就是“zhongnanyin”,
如果前面已经赋过值了,那么就使用前面赋的值。
Makefile 中的变量是字符串,有时候我们需要给前面已经定义好的变量添加一些字符串进
去,此时就要使用到符号“+=”。
objects = main.o inpiut.o
objects += calcu.o
一开始变量 objects 的值为“main.o input.o”,后面我们给他追加了一个“calcu.o”,因此变
量 objects 变成了“main.o input.o calcu.o”,这个就是变量的追加。
4.1写的代码中第2、3、4条规则是将对应的.c 源文件编译为.o 文件,如果工程中 C 文件很多的话显然不能这么做。
因此就要使用Makefile 中的模式规则,通过模式规则我们就可以使用一条规则来将所有的.c 文件编译为对应的.o 文件。
“%”表示长度任意的非空字符串,比如“%.c”就是所有的以.c 结尾的文件,类似与通配符,a.%.c 就表示以 a.开头,以.c 结束的所有文件。
objects = main.o input.o calcu.o
main: $(objects)
gcc -o main $(objects)
%.o : %.c
#命令
clean:
rm *.o
rm main
命令怎么写呢?
所谓自动化变量就是这种变量会把模式中所定义的一系列的文件自动的挨个取出,直至所有的符合模式的文件都取完,自动化变量只应该出现在规则的命令中。
自动化变量 | 描述 |
$@ | 规则中的目标集合,在模式规则中,如果有多个目标的话,“$@”表示匹配模式中定义的目标集合 |
$< | 依赖文件集合中的第一个文件,如果依赖文件是以模式(即“%”)定义的,那么“$<”就是符合模式的一系列的文件集合 |
$^ | 所有依赖文件的集合,使用空格分开,如果在依赖文件中有多个重复的文件,“$^”会去除重复的依赖文件,值保留一份。 |
$+ | 和“$^”类似,但是当依赖文件存在重复的话不会去除重复的依赖文件。 |
$* | 这个变量表示目标模式中"%"及其之前的部分,如果目标是 test/a.test.c,目标模 式为 a.%.c,那么“$*”就是 test/a.test。 表 3.4.4.1 自动化变量 |
$% | 当目标是函数库的时候表示规则中的目标成员名,如果目标不是函数库文件,那么其值为空 |
$< | 依赖文件集合中的第一个文件,如果依赖文件是以模式(即“%”)定义的,那么“$<”就是符合模式的一系列的文件集合。 |
$? | 所有比目标新的依赖目标集合,以空格分开。 |
使用变量、模式规则、自动化变量优化4.1小结中的代码:
objects = main.o input.o calcu.o
main: $(objects)
gcc -o main $(objects)
%.o : %.c
gcc -c $<
clean:
rm *.o
rm main
伪目标不代表真正的目标名,在执行 make 命令的时候通过指定这个伪目标来执行其所在规则的定义的命令。
使用上面介绍的makefile知识,创建本次工程的makefile文件:
objs := start.o main.o
ledc.bin:$(objs) /*最终目标编译出ledc.bin文件,需要依赖与start.o mian.o两个文件**/
arm-linux-gnueabihf-ld -Timx6ul.lds -o ledc.elf $^
/*连接.O文件到指定地址,生成elf文件*/
arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
/*将elf文件转换成bin文件*/
arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis
/*将elf文件反汇编成dis文件*/
%.o:%.s /*.o文件 依赖于.c文件*/
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $< /*将.s文件编译成.o文件*/
/*-O2 是优化等级*/
%.o:%.S
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $< /*将.S文件编译成.o文件*/
%.o:%.c
arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o $@ $< /*将.c文件编译成.o文件*/
clean:
rm -rf *.o ledc.bin ledc.elf ledc.dis
上面makefile文件中的Timx6ul.lds是一个链接脚本文件,他描述了要链接的文件、链接顺序、链接首地址。
链接脚本文件每个命令是一个带有参数的关键字或者一个对符号的赋值,可以使用分号分隔命令。如下:
SECTIONS{ /*关键词SECTIONS和一对大括号 表示连接脚本文件 */
. = 0X10000000;
/* “.”在链接脚本里面叫做定位计数器,默认值为0,对其赋值就是修改起始地址*/
.text : {*(.text)}
/* “.text”是段名,后面的冒号是语法要求,冒号后面的大括号里面可以填上要链接到“.text”这个段里面的所有文件, “*(.text)”中的“*”是通配符,表示所有输入文件的.text段都放到“.text”中。*/
. = 0X10000000; /*定义数据段 起始地址*/
.data ALIGN(4) : { *(.data) }
.bss ALIGN(4) : { *(.bss) } /** ALIGN(4)表示 4 字节对齐*/
}
下面创建本次工程使用的链接脚本文件“imx6ul.lds”
SECTIONS{
. = 0X87800000; /**设置定位计数器为0X87800000*/
.text :
{
start.o /*开始位置的文件为start.o,一定要链接到最开始的地方*/
main.o
*(.text) /*剩余的所有text段,程序编译出来的都属于text段*/
}
.rodata ALIGN(4) : {*(.rodata*)}
.data ALIGN(4) : { *(.data) }
__bss_start = .; /*设置起始地址*/
.bss ALIGN(4) : { *(.bss) *(COMMON) } /*.bss 表示定义 但没有被初始化的变量*/
__bss_end = .; /*编译后“.” 的值会变,这里获取结束地址*/
}
插入SD卡到ubuntu虚拟机: