在上一篇文章中我们详细讲解了ARM开发环境的搭建,我们选择了X86-linux平台交叉编译ARM程序,交叉编译链选用arm-linux-系列。另外,我们还说明了一些开发需要的基础知识。关于以上这些内容,请参见ARM芯片学习内容规划及ARM开发环境的搭建。
我们学习高层应用程序开发的时候,一般第一个程序是经典的”hello world”程序。我们学习嵌入式的开发,主要是根据应用需求,选用合理的电子器件设计硬件电路,然后使用主控芯片控制外设。所以,我们GPIO操作篇的内容选为让电路板上的一个LED灯闪烁。
在讲解实验之前,我还是说明一下。这些基础实验,都是在特定硬件平台上运行的,且现象也是在特定电路板上才能产生的。所以实验中的程序并没有通用的移植性。拿到程序之间编译后下载到您的开发板上不一定能正常执行。需要简单修改。而且,我写这一系列的教程是让大家了解使用一款32bit处理器的基本方法和思路。并不是针对某一硬件平台。之所以所有程序都在一个固定的硬件平台上运行是因为要保证程序及想法的正确性。
相信,有些朋友以前就学习过单片机。学习单片机时有一些应该知道的基本问题。同样,学习ARM等其他芯片的使用方法时也一样。下面,我已疑问的形式写在下面:
1. 我通过交叉编译链编译、链接好的程序怎么放到芯片里去?放在什么位置?
2. 芯片加电后从哪里读取第一条指令运行?
3. 交叉编译链编译出的程序地址、下载到芯片里的地址、真正运行时的地址之间到底是什么关系?
4. ARM芯片有没有中断向量表?在什么位置?
上面这些问题在开发普通PC机程序的时候,你不用考虑,因为这些编译器、操作系统、函数库都帮你做好了,所以你只需要把精力主要放到应用需求上就行了。但现在只有一个ARM芯片,没有任何其他支持,所以,这些问题你就需要理解并掌握。
针对第一个问题:LPC2220这款芯片支持在线编程功能,也就是可以通过串口下载你编译好的程序。(其实现方式是芯片内部有固化的loader,这个loader给你提供一些通过串口交互的简单命令。在PC机上一般都有其下载程序的上位机软件)下载到什么位置,要看这款芯片加电后的启动方式。不论从哪个地读取指令并启动,这个地址上应该连接着一种非易失性存储芯片,并且芯片要支持随机读。一般ARM类芯片,支持很多种启动方式,像 norflash启动、nandflash 启动、SD卡启动等等。而且启动方式是可以配置的。我选择的启动方式是norflash启动,这款norflash连接在0X80000000开始的地方。
所以,我们应该把编译、链接好的程序下载到0x8000000开始的地址中去。
针对第二个和第四个问题,我们选择了norflash启动方式,那么这款芯片会从物理地址0x8000000地址处开始取出第一条指令并执行。同时,ARM的中断向量表也会指向从0x8000000开始的32字节。见下面的表格:
0x00000000 Reset
0x00000004 Undefined Instruction
0x00000008 Software Interrupt
0x0000000C Prefetch Abort (instruction fetch memory fault)
0x00000010 Data Abort (data access memory fault)
0x00000014 Reserved *
0x00000018 IRQ
0x0000001C FIQ
也就是说,当有中断产生时,cpu还是会从上面列出的地址去取指令,只不过这些地址被重新映射到了0x8000000。也即,中断向量表对cpu来说是不变的,但是根据不同的启动方式,将这些逻辑地址重新映射到不同的物理地址上。选择norflash启动模式,就是将中断向量表映射到了物理地址0x8000000。在这种模式下,cpu一上电,还是从0x00000000地址取第一条指令,但是这只是针对cpu来说是逻辑上的0x00000000,其实的真正物理地址是0x80000000。
针对第三个疑问,经过上面的分析我们知道了芯片的启动方式、从哪里取第一条指令。也知道了怎么下载到指定的地址。在说明,下载地址、链接地址、运行地址之间的关系之前,我们先要理解一个问题:Cpu的工作方式。 cpu的工作方式是从指定的地址里取出一条指令进行解析,然后根据解析结果做相应的操作。像前面分析的启动方式,就是从物理地址0x8000000地址开始取指令并执行。我们编译出、链接出来的映像是具有一定格式的,具体格式请参考gnu链接器的说明,这里不做详细说明。我们要知道这样一个事实,我们程序里的code段是顺序存放的,而且我们程序里的跳转指令都要依据我们把第一条指令放在什么地址处。这样说比较晦涩难懂,我们举例说明。如果,我们指定第一条指令的地址为0x8000000,那第二条指令就会接着往后存放。
假设我们有一小段汇编指令。(假设编译出来的是ARM指令,每条指令32bit)
Start:LDR R0, =0xE0028028 @IO2DIR
LDR R1,= 0x10000000
STR R1,[R0] @设置P2.28为输出
LDR PC, Start
若我们编译、连接好的程序如下表:
链接好的地址 | 指令内容(编译出的是二进制指令编码,我们使用汇编表示) |
0x8000000 | LDR R0, =0xE0028028 |
0x8000004 | LDR R1,= 0x10000000 |
0x8000008 | STR R1,[R0] |
0x800000c | LDR PC, Start |
连接好的映像地址其实并不存储在我们编译、链接出的映像里。这些地址在存储器的地址线上体现,这里这样做表格只是为了好理解。假设,我们把上面的映像文件下载到0x8000000开始的地址处。那么,当执行LDR PC, Start这条指令时,其实这条指令相当于LDR PC, 0x8000000,就是把0x8000000这个地址赋给PC,接着cpu就从PC值指向的地址处开始取指令并执行。这样程序又从头开始执行了。实现了我们的想法。但是,如果我们链接时指定的代码段开始地址是0x8000000,但是我们将这段映像转载到了0x8100000开始的地方,并设置cpu从0x8100000开始取指执行。那么,当我们同样执行到LDR PC, Start指令时,还是把0x8000000这个地址赋值给PC,cpu还会从0x8000000取指令执行,这个时候0x8000000地址处并不是我们想要的内容,导致程序不能正常运行。
通过,上面的例子我们知道了,实际上链接器链接时要求地址是这段映像代码段各个指令位置的一个说明。你要保证程序真正运行的地址和编译器链接要求的地址保持一致,要不然一些绝对跳转指令将不能正常执行。从而导致程序出错。
关于,下载地址,当然可以和真正运行时的地址不同,但你要保证当程序真正运行时所在的地址和链接器要求的地址相一致。实现方法是利用一些和地址无关的指令,将代码在真正运行之前复制到链接器所要求的地址。
好了,这些基本问题搞明白了,我们就可以通过向ARM芯片控制外设了。这里当然是要控制led灯了。
用主控芯片控制外设的一般方法:
1. 看电路原理图,弄明白主控芯片和外设是怎么连接的。
2. 根据电路连接和需求对主控芯片进行设置。
3. 书写相应代码,实现功能。
我的ARM芯片和led灯的连接方式,如下图:
我们只控制LED1,通过看电路原理图,我们发现LED1和ARM芯片的P2.28引脚相连。
那下面我们看看怎么设置ARM芯片的P2.28引脚就可以了。很显然,我们让P2.28管脚输出0,LED灯会亮,输出1,LED灯会灭。
使用一个ARM芯片的管脚一般分以下两步:
1. 设置这个管脚的功能。(这个管脚可能有好多功能,根据需要设置)
2. 操作这个管脚
关于配置和操作这个管脚的寄存器地址和方法请参见LPC2220的datasheet。
接着就是写程序、makefile文件,然后在linux主机上编译链接,最后下载到norflash运行。
你看到的现象就是LED1会闪烁。
程序内容如下:
@******************************************************************************
@ 文件名 :control_led.s
@ 功 能:利用P2.28控制led灯闪烁
@
@ 作者 :张连聘
@ 创建时间:2014-06-08
@******************************************************************************
.text
.global _start
@定义程序中使用到的常量
.equ IO2DIR ,0xE0028028 @控制IO0的输入、输出属性寄存器
.equ IO2SET ,0xE0028024 @IO2输出1控制寄存器
.equ IO2CLR ,0xE002802C @IO2输出0控制寄存器
.equ LEDCON ,0x10000000 @(1<<28)
_start:
LDR PC, ResetAddr
ResetAddr:
.word ResetInit
ResetInit:
LDR R0,=IO2DIR @IO2DIR
LDR R1,=LEDCON
STR R1,[R0] @设置P2.28为输出
MaiLoop:
LDR R0,=IO2CLR
LDR R1,=LEDCON
STR R1,[R0] @P2.28为输出0,熄灭led
BL DELAYS @调用延时程序
LDR R0,=IO2SET
LDR R1,=LEDCON
STR R1,[R0] @P2.28为输出1,点亮led
BL DELAYS @调用延时程序
B MaiLoop
@******************************************************************************
@ 名 称:DELAYS
@ 功 能:软件延时
@ 入口参数:无
@ 出口参数:无
@ 占用资源:R7
@******************************************************************************
DELAYS:
MOV R7,#0x00002000 @延时参数
DELAYS_L1: SUBS R7,R7,#1 @ R7 = R7-1
BNE DELAYS_L1 @判断R7-1结果是否为0,若不为0则跳转
MOV PC,LR @返回
程序里需要说明的:
关于gnu arm汇编请参考相关书籍。这里只说明一些和这里相关的内容。
.text 定义一个代码段的开始。.global _start定义一个全局的标号,一般供链接器链接多个.o文件时使用。
.equ定义一个常量。@后面的内容为注释内容。
LDR PC, ResetAddr
ResetAddr:
.word ResetInit
ResetInit:
这段代码需要说明一下,有些朋友可能有这样的疑问,为什么第一条指针需要跳转指令,还有为什么不能用LDR PC, ResetInit。
关于这个问题,我们上面分析了,从0x8000000开始的32byte都是中断(异常)向量表,每个中断向量只有四个字节的空间,肯定不能放下中断处理程序,所以放置一条跳转指令。上电后,默认执行Reset,这时候对CPU来说,它认为PC地址为0x00000000,只不过这个地址被其他硬件设备重新映射到了0X80000000地址上了,但cpu并不知道这个硬件设备的存在。而直接LDR PC, ResetInit只能实现前后32 M空间的跳转,显然不能满足我们的意愿。通过上面的方法定义的ResetAddr,然后再用LDR PC, ResetAddr可以实现4G范围内跳转。
其他内容都很简单了,那条指令不明白可以查阅ARM指令集。
下面看makefile文件:
control_led.bin:control_led.s
arm-linux-gcc -g -c -o control_led.o control_led.s
arm-linux-ld -Ttext 0x80000000 -g control_led.o -o control_led_elf
arm-linux-objcopy -O binary -S control_led_elf control_led.bin
clean:
rm -f control_led.bin control_led_elf *.o
makefile的基本语法和格式,我就不多说了。
嵌入式linux下的arm-linux系列工具,默认链接出来的是在linux内核支持下的映像文件,是ELF格式的映像文件。而我们没有操作系统的支持,所以,我们要利用二进制工具将ELF格式的映像文件转换成纯二进制指令格式映像文件。
我们上面说的链接器链接地址的概念,在arm-linux-ld 的体现就是 arm-linux-ld -Ttext 0x80000000 -g control_led.o -o control_led_elf,其中-Ttext选项就是指代码段的起始链接地址。这里将地址连接到0x80000000开始的地方。
好了,关于ARM芯片的启动,链接地址,下载地址,执行地址以及第一个点亮LED灯的程序,makefile讲解就到这里吧。
我把这个试验中所涉及到的源码和word文档都打包上传到csdn的下载频道:
下载地址:ARM芯片基础实验之GPIO操作
上一篇:嵌入式之IO口总结
下一篇:ARM GPIO接口置位方法
推荐阅读最新更新时间:2024-03-16 16:09