本节我们用点灯来体验一下arm的裸机程序开发;
cortex-A系统虽然比M系统更复杂,但是对于裸机开发,也就是寄存器寄存器寄存器,还是很好上手的(指点灯),那就来体验一把
使用开发板:FS4412,Soc:Exynos4412,内核型号:cortex-A9,架构:armv7;
文档:开发板原理图,Soc数据手册;
环境:文件编写及编译:Linux;下载程序:超级终端hypertrm;串口下载
编译器:交叉编译器arm-linux-gcc。
这张图应该很好理解吧,(除了LED灯方向画反了),汇编里的寄存器是cpu中的寄存器,操作它们只能驱动cpu进行相应的操作,如果要操作cpu外面的外设,那就得去操作这些外设对应的寄存器,因为每个外设都有自己的寄存器,以实现不同的功能,所以这些寄存器又叫特殊功能寄存器,英文缩写SFR,sfr;
本次实验是要驱动一个LED,使其发光,那么首先要知道这个LED在电路中是怎么接的,知道了电路接法才能知道怎么能使这个灯亮灭,一般就是要看LED的控制引脚连接的是Soc的哪个引脚;
对这个引脚进行相应的配置,需要设置对应的寄存器,寄存器的用法在芯片手册对应的模块中,需要找到寄存器的地址和这个寄存器的用法(哪几位是干啥的)
用C语言,汇编也能用,但是用C更方便;
如图,LED3,4,5分别接在Soc的GPX1_0,GPF3_4,GPF3_5上;
注:GPX1_0:第X个大组的第1个小组的第0个引脚;
(1) 二极管的阳极接在高电平上,阴极通过电阻和三极管接地,只要三极管导通,LED就亮,三极管截止,LED就灭;
(2) NPN三极管发射极接地,基极给高导通,给低截止;
(3) 可知:Soc引脚输出高电平亮灯,低电平灭灯;
有两个寄存器要用到:引脚模式设置GPX1CON,引脚输出电平设置GPX1DAT;
(1) GPX1_0这个引脚对应的寄存器为GPX1CON;
(2) GPX1CON寄存器是32位的,一个引脚需要4位进行配置,32位对应8个引脚,所以GPX1CON可以管理GPX1_0 GPX1_1…GPX1_7;
(3) GPX1_0这个引脚对应的是GPX1CON(地址为0x11000c20)寄存器的0-3位,要引脚输出,设置为0x01,其他位保持不变;
(4)GPX1DAT这个寄存器管理的是GPX1_0-GPX1_7的输出值,不是0就是1,GPX1_0对应的就是GPX1DAT的第0位,要想GPX1_0输出1,就让GPX1DAT的第0位为1就可以;
先点亮LED3,思路就是配置GPX1_0为输出,且输出1;
手册中查到GPX1CON的基地址为0x11000000,偏移量为0x0c20,所以其地址为0x11000c20;因为是32位地址,可以转化为int型的指针然后来访问;即(int *)0x11000c20;访问时,再解引用*((int *)0x11000c20);同理,GPX1DAT的地址为:0x11000c24;
程序为:
//没有包含任何头文件,所以连寄存器都要自己去宏定义;
//用volatile不让编译器去优化,防止出现改了值,但是cache没更新cpu不更新导致没现象;
#define GPX1CON *((volatile unsigned int *)0x11000c20)
#define GPX1DAT *((volatile unsigned int *)0x11000c24)
int main()
{
//设置GPX1_0引脚为输出功能,将GPX1CON的0-3位设置为0x01
GPX1CON &= ~(0xf << 0); //将0-3位清0
GPX1CON |= (0x1 << 0); //将0-3设置为0x1
//设置GPX1_0引脚输出1
GPX1DAT |= (0x1 << 0); //将第0位设置为1
return 0;
}
.global _start @.global关键字用来让一个符号对链接器可见,可以供其他链接对象模块使用;
_start:
b main @从_start进入后执行跳转到main;
链接脚本是简单易用的,但包含了很多东西,本次实验用到的是很简单的一个;
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") /*elf32-littlearm指定输出可执行文件是elf格式,32位ARM指令,小端,写的三个分别表示为默认为小端;如果是小端,改为小端;如果是大端,改为小端*/
OUTPUT_ARCH(arm)/*指定输出可执行文件的平台为ARM*/
ENTRY(_start) /*指定输出可执行文件的起始代码段为_start*/ //即程序入口
SECTIONS //使用'SECTIONS'来描述输出文件的内存布局.
{
/*指定可执行image文件的全局入口点,通常这个地址都放在ROM(flash)0x0位置。必须使编译器知道这个地址,通常都是修改此处来完成*/
. = 0; /*从0x0位置开始,代码应当被载入到地址'0x10000'处*/
. = ALIGN(4); /*代码以4字节对齐*/
.text :
{
*(.text) /*其它代码部分*/
}
. = ALIGN(4);
.data :
{ *(.data) } /*指定读/写数据段*/
. = ALIGN(4);
.bss :
{ *(.bss) }
}
简单讲解:
(1) .=0x0;对一个特殊的符号’.‘赋值, 这是一个定位计数器. 如果你没有以其它的方式指定输出节的地址, 那地址值就会被设为定位计数器的现有值. 在’SECTIONS’命令的开始处, 定位计数器拥有值’0’;
每个输出节之前都可以加上对定位计数器进行赋值,来表示这个输出节在内存中的起始位置;
如果不写,那么默认输出节跟在上一个之后,.bss就紧跟在.data内存区后;
(2) . = ALIGN(4)表示以4字节对齐;
(3) .text :定义一个输出节,‘.text’. 冒号是语法需要,现在可以被忽略. 节名后面的花括号中,你列出所有应当被放入到这个输出节中的输入节的名字. '‘是一个通配符,匹配任何文件名. 表达式’(.text)‘意思是所有的输
入文件中的’.text’输入节.
(4) 运行一个程序时第一个被执行到的指令称为"入口点",使用’ENTRY’连接脚本命令来设置入口点;
下面的这个makefile文件将start.s和mian.c编译为目标文件,然后再加上链接脚本,链接成一个.elf系统文件,再用objcopy生成.bin可执行的裸机程序,最后用objdump生成反汇编文件,以便查看;
注意:start的所有操作应该在main之前;因为程序入口是start而不是main;
CROSS = arm-none-linux-gnueabi- #交叉编译器
CC=$(CROSS)gcc #交叉编译命令
LD=$(CROSS)ld #交叉编译链接命令,将所有的.o文件链接生成可执行文件(.elf)
OBJCOPY=$(CROSS)objcopy #被用来复制一个目标文件的内容到另一个文件中,可用于不同源文件的之间的格式转换,实际上elf文件中的调试信息被删掉了;
all:
$(CC) -g -c -o start.o start.s #-g支持gdb,-c生成目标文件,-o改目标文件名
$(CC) -g -c -o main.o main.c
$(LD) start.o main.o -Tmap.lds -o led.elf #-T指定链接脚本,
$(OBJCOPY) -O binary -S led.elf led.bin # 用.elf文件生成.bin文件
$(CROSS)objdump -D led.elf > led.dis #objdump 将.elf文件进行反汇编生成反汇编文件(.dis),-D是反汇编所有部分,“>”是把结果写入到.dis文件中,如果没有">",那么反汇编语句直接到命令行窗口;
clean:
rm -f *.o *.elf *.bin *.dis
使用make启动makefile生成bin文件,下载到开发板就可以看到现象了;不同开发板下载方式和软件都不一样,这里不做讨论;
在四中,我们已经点亮了一个灯,接下来点亮LED4和LED5,再让它们循环地亮和灭即可;
这个程序只是把前面的东西组合包装了以下,实现了流水灯,不再作解释;
#define GPX1CON *((volatile unsigned int *)0x11000c20)
#define GPX1DAT *((volatile unsigned int *)0x11000c24)
#define GPF3CON *((volatile unsigned int *)0x114001e0)
#define GPF3DAT *((volatile unsigned int *)0x114001e4)
#define led3on GPX1DAT |= (0x1 << 0) //将第0位设置为1
#define led3of GPX1DAT &=~(0x1 << 0) //将第0位清0
#define led4on GPF3DAT |= (0x1 << 4)
#define led4of GPF3DAT &=~(0x1 << 4)
#define led5on GPF3DAT |= (0x1 << 5)
#define led5of GPF3DAT &=~(0x1 << 5)
void delay()
{
int i=0xfffff;
while(i--);
}
int main()
{
//设置GPX1_0引脚为输出功能,将GPX1CON的0-3位设置为0x01
GPX1CON &= ~(0xf << 0); //将0-3位清0
GPX1CON |= (0x1 << 0); //将0-3设置为0x1
GPF3CON &= ~(0xff << 16); //将16-23位清0
GPF3CON |= (0x11 << 16); //将16-23设置为0x11
while(1)
{
led3on;delay();
led3of;delay();
led4on;delay();
led4of;delay();
led5on;delay();
led5of;delay();
}
return 0;
}