今天我们来写第一个ARM裸板程序-点亮LED
我们怎样去点亮一个LED呢? 共分为三步 。
- 看原理图,确定控制LED的引脚;
- 看主芯片的芯片手册,确定如何设置控制这个引脚;
- 写具体的程序来实现;
第001节:硬件知识-LED原理图
点亮LED需要通电源,同时为了保护LED,加个电阻减小电流。 控制LED灯的亮灭,可以手动开关LED,但在电子系统中,不可能让人来控制开关,而是通过编程,利用芯片的引脚去控制开关。
如图所示:
LED的驱动方式,常见的有四种:
方式1:使用引脚输出3.3V点亮LED,输出0V熄灭LED。
方式2:使用引脚拉低到0V点亮LED,输出3.3V熄灭LED。
有的芯片为了省电等原因,其引脚驱动能力不足,这时可以使用三极管驱动。
方式3:使用引脚输出1.2V点亮LED,输出0V熄灭LED。
方式4:使用引脚输出0V点亮LED,输出1.2V熄灭LED。
由此,主芯片引脚输出高电平/低电平,即可改变LED状态,而无需关注GPIO引脚输出的是3.3V还是1.2V。 所以简称输出1或0:
- 逻辑1–>高电平
- 逻辑0–>低电平
第002节:硬件知识-S3C2440启动流程与GPIO操作
查看原理图:
由图明显可以看出当引脚nLED 1/2/4 输出为低电平时,二极管导通发光。(n表示低电平有效)
在原理图中,同名的Net表示是连在一起的。 因此我们要找到nLED 1同名net.
由图可知,三个LED灯nLED 1/2/4 分别连接GPF 4/5/6
查看S3C2440芯片手册:
由图可知,GPF输入输出端口共有8组端口,若以nLED_1为例,那我们要操作的就是第四组(即GPF4)。
怎么样令GPF4输出1或0?
- 配置为输出引脚;
- 设置状态;
首先查看芯片手册:找到GPF的配置寄存器GPFCON。
由图可知,
设置GPFCON[9:8]=0b01,即将GPF4配置为输出;
设置GPFDAT[4]=1或者0,即输出高电平或低电平;
S3C2440框架:
S3C2440启动流程:
- Nor启动:
Nor Flash的基地址为0,片内RAM地址为0x4000 0000;
CPU读出Nor上第1个指令(前4字节),然后执行;
CPU继续读出其它指令执行。
- Nand启动:
片内4k RAM基地址为0,Nor Flash不可访问;
2440硬件把Nand前4K内容复制到片内的RAM,然后CPU从0地址取出第1条指令执行。
第003节:编写第1个程序点亮LED
在开始写第1个程序前,先了解一些概念。
2440是一个SOC,它里面的CPU有R1、R2、R3……等 寄存器(可以直接通过寄存器名字来访问);
它里面的GPIO控制器也有很多寄存器,如 GPFCON、GPFDAT。 (需要通过地址访问)
这两个寄存器是有差异的,在写代码的时候,CPU里面的寄存器可以直接访问,其它的寄存器要以地址进行访问。
然后我们根据芯片手册中的信息可知GPFCON寄存器的地址。
把GPF4配置为输出,需要把GPFCON的第9位和第8位为0b01,即将01 0000 0000=256=0x100写入GPFCON这个寄存器,而这个寄存器只能通过地址来访问,即写到0x5600 0050上;
把GPF4输出1,需要把GPFDAT第4位,即0x10写到GPFDAT寄存器中,GPFDAT寄存器也只能通过地址来访问,其地址为0x5600 0054上; 把GPF4输出0,需要把0x00写到地址0x5600 0054上; 这里的写法会破坏寄存器的其它位,其它位是控制其它引脚的,为了让第一个裸板程序尽可能的简单,才简单粗暴的这样处理。
写程序需要用到几条汇编代码:
①LDR (load):读寄存器
举例:LDR R0,[R1]
假设R1的值是x,读取以x为首地址的4个连续内存单元的数据(4字节),保存到R0中;
②STR (store):写寄存器
举例:STR R0,[R1]
假设R1的值是x,把R0的值写到以x为首地址的4个连续内存单元(4字节);
③B 跳转
④MOV (move)移动,赋值 举例1:MOV R0,R1 把R1的值赋值给R0;
举例2:MOV R0,#0x100 把0x100赋值给R0,即R0=0x100;
⑤LDR
举例:LDR R0,=0x12345678 这是一条伪指令,即实际中并不存在这个指令,他会被拆分成几个真正的ARM指令,实现一样的效果。 最后结果是R0=0x12345678。
为什么会引入伪指令?
在ARM的32位指令中,有些字节表示指令,有些字节表示数据,因此表示数据的没有32位,不能表示一个32位的任意值,只能表示一个较小的简单值,这个简单值称为立即数。引入伪指令后,利用LDR可以为R0赋任意大小值,编译器会自动拆分成真正的的指令,实现目的。
有了前面5个汇编指令的基础,我们就可以写代码了。
第一个程序只能是汇编,以前你们可能写过单片机程序,一上来就写main()函数,那是编译器帮你封装好了。
因此,我们写除了第一个控制led1亮的程序:
/*点亮led1*/
.text
.global _start
_start:
/*将GPF4设置为输出端口:将GPFCON配置寄存器中的第9、8位设为0、1即可,即把0x100送入
GPFCON寄存器的地址
0x56000050中*/
ldr r0,0x56000050
ldr r1,0x100
str r1,[r0]
/*将GPF4输出状态设置为输出低电平0:将GPFDAT寄存器第4位设置输出为低电平0,也就是将
0x00送入GPFDAT寄存器的地址0向6000054中去*/
ldr r0,0x56000054
ldr r1,0x00
str r1,[r0]
/*由于我们不知道在程序代码执行完毕之后,NOR FLASH或NAND FLASH的内容未知,
所以要执行死循环halt*/
halt:
b halt
接下来的操作需要用到交叉编译工具(前面已经介绍过)arm-linux-gcc。现在我们就先把工具安装好:
下载并解压:
打开文件/etc/environment添加环境变量,地址就是我们前面解压缩的选项 -C指定的地址/usr/local/arm
里面的arm-linux-gcc目录下的bin:
添加好了以后,使用source命令使我们的设置生效:
最后,确认arm-linux-gcc安装成功!
接下来,就可以进行下一步的工作了!
将代码上传到服务器, 先把汇编语言源文件led_on.s使用-c选项进行汇编:
arm-linux-gcc -c -o led_on.o led_on.s ;
再链接:
arm-linux-ld -Ttext 0 led_on.o -o led_on.elf ;
生成bin文件:
arm-linux-objcopy -O binary -S led_on.elf led_on.bin ;
以上的命令,要是我们每次都输入会容易输错,因此我们把他们写到一个文件里,这个文件就叫Makefile. 本次所需的Makefile如下:
all:
arm-linux-gcc -c -o led_on.o led_on.S
arm-linux-ld -Ttext 0 led_on.o -o led_on.elf
arm-linux-objcopy -O binary -S led_on.elf led_on.bin
clean:
rm *.bin *.o *.elf
以后只需要 使用 make 命令进行编译, make clean 命令进行清理。
最后烧写到开发板上,即可看到只有一个LED亮,符合我们预期。
第004节_汇编与机器码
前面介绍过伪指令,伪指令是实际不存在的ARM命令,编译器在编译时转换成存在的ARM指令。
我们可以通过反汇编来查看在实际执行过程中的汇编指令。
在前面的Makefile中加上:
arm-linux-objdump -D led_on.elf > led_on.dis
上传服务器,编译。
生成的led_on.dis就是反汇编文件。led_on.dis如下:
结合我们的程序代码来分析一下:图中第一列为地址,第二列为机器码、第三列为汇编代码。
ldr r0,0x56000050
ldr r1,0x100
str r1,[r0]
/***************/
ldr r0,0x56000054
ldr r1,0x00
str r1,[r0]
为了便于分析,我们先来认识一下CPU内部的寄存器:
cpu内部共有16个寄存器,其中我们经常用到的有三个:R13\R14\R15.
R13: 又名sp,即Stack Pointer,栈指针,保存的始终是栈顶的地址
R14: 又名lr, 即Link Register,保存的是返回地址,用于保护断点和现场。
R15:又名pc,即Program Pointer,保存的是cpu将要执行的下一条指令的地址。由于CPU执行的流水线工作方式,也就是说为了提高cpu执行指令的效率,在CPU执行‘存放在地址A处的指令’的同时,CPU还会对下一条指令(存放在地址为A+4的指令)进行译码,并将之后的第二条指令(存放在地址为A+8的指令)的地址(也就是A+8)送入PC中。
首先我们对反汇编后的第一条代码进行解析:将以pc+20为首地址的四个字节的数据送到R0寄存器中。对于第一条指令,它的地址为0,所以pc=0+8=8,那么pc+20=28=0x1c,反汇编程序的下半部分就是即为对应地址中的数据,所以显然0x1c处的数据为0x56000050,那么这条指令执行完,R0的值就变成了0x56000050.第一条指令的功能就是将GPFCON的地址送入寄存器R0中。
我们接着对第二条指令进行解析:第二条指令将伪指令ldr转换成了mov指令,其功能为将立即数256,也就是0x100送入了寄存器R1中.
第三条指令:由于R0=0x56000050,R1=0x100,第三条指令就是把0x100送入以0x56000050为首地址的四个字节中。就是将GPFCON寄存器的值设为0x100,这样GPF4就被设置成了输出端口。
第四条指令:由于其地址为c,所以,pc=c+8=0x14,则pc+12=0x20。因此此指令便将首地址为0x20的四个字节的数据放入寄存器R0中。所以指令执行后R0的值为0x56000054.
第五条指令:将立即数0x00放入寄存器R1中。
第六条指令:R0=0x00,R1=0x56000054,所以此指令便将0x00放入以0x56000054为首地址的四个字节中,也就是GPFDAT寄存器中。
分析完汇编指令,我们完成一个作业:通过修改汇编代码来点亮led2。
通过查看芯片手册,我们知道要想点亮led2,首先需要将GPFCON寄存器中的第11、10位设置为01,GPFCON寄存器设置为0x400;然后,经GPFDAT寄存器内容设置为0x00。代码如下:
/*点亮led1*/
.text
.global _start
_start
/*将GPF5设置为输出端口:将GPFCON配置寄存器中的第11、10位设为0、1即可,
即把0x400送入GPFCON寄存器的地址0x56000050中*/
ldr r0,0x56000050
ldr r1,0x140
str r1,[r0]
/*将GPF4输出状态设置为输出低电平0:将GPFDAT寄存器第5位设置输出为低电平0,
也就是将0x00送入GPFDAT寄存器的地址0向6000054中去*/
ldr r0,0x56000054
ldr r1,0x00
str r1,[r0]
/*由于我们不知道在程序代码执行完毕之后,NOR FLASH或NAND FLASH的内容
未知,所以要执行死循环halt*/
halt:
b halt
接下来解析一下机器码:
上图是四字节机器码的格式,我们先不管其他的只看低16位:
Rd表示的寄存器,4位可以表示16个寄存器;
Shift_operand表示操作数,其中12位中的高4位表示rotate_4位,低8位表示immed_8位。表示的立即数为immed_8右移2*rotate_4位后的数值。
如图:第一行机器代码为e5 9f 00 14,其中Rd=0,所以寄存器为R0;rotate_4为0,14表示源操作数中的立即数为0x14,所以立即数为0x14右移2*rotate_4位,结果还是0x14,十进制为20。
第二行中的机器代码为e3 a0 1c 01;其中Rd=1,所以寄存器为R1;rotate_4为c;immed_8为01;所以立即数为0x01右移2*rotate=24位,结果为0x100。
分析完机器代码,我们来修改一下机器代码来修改程序点亮led2:
将第二行机器代码中的0x100修改为0x400;也就是将rotate_4由c改为b即可。因为0x100要由0x01右移24位,而0x400由0x10右移22位。
第005节_编程知识_进制(略)
第006节_编程知识_字节序_位操作
1. 字节序:
假设int a = 0x12345678;
前面说了16进制每位是4个字节,在内存中,是以8个字节作为1byte进行存储的,因此0x12345678中每两位作为1byte,其中0x78是低位,0x12是高位。
在内存中的存储方式有两种:
0x12345678的低位(0x78)存在低地址,即方式1,叫做小字节序(Little endian);
0x12345678的高位(0x12)存在低地址,即方式2,叫做大字节序(Big endian);
一般的arm芯片都是小字节序,对于2440可以设置某个寄存器,让整个系统使用大字节序或小字节序,它默认使用小字节序。
下面我们写个程序来在ubuntu上运行判断其字节序:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int a=0x12345678;
int *p=&a;
char s[4];
for(int i=0;i<4;i++)
{
s[i]=*((char*)p+i);
printf("s[%d]=%x\n",i,s[i]);
}
return 0;
}
输出结果如下:
s[0]=78
s[1]=56
s[2]=34
s[3]=12
根据代码可知,s[0]-s[4]分别存放的是整型变量a存放数据0x12345678的四个字节,并且s[0]-s[4]依次存放的字节的地址是从小到大的,也就是说s[0]是低地址,存放的数据时低位的0x78,s[4]是高地址,存放的是高位的0x12。因此,我的ubuntu上采用的是little endian。
2. 位操作
1)左移 << ,右移 >>
int a=0x8;
int b=a<<3;//64
int c=a>>3;//1
int d=a>>4;//0
2)取反 ~
int a=0x123;
int b=~a;//0xfffffedc
int c=0x8;
int d=~c;//-9或0xfffffff7
3)位与 &
int a=123;
int b=456;
int c=a&b;//0x2
4)位或
int a=123;
int b=456;
int c