历史上的今天
今天是:2025年03月11日(星期二)
2020年03月11日 | 开发板学习Day7-第一个ARM裸板程序及引申
2020-03-11 来源:eefocus
今天我们来写第一个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?
1. 配置为输出引脚;
2. 设置状态;
首先查看芯片手册:找到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 -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个寄存器,其中我们经常用到的有三个:R13R14R15.
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节编程知识字节序_位操作
字节序:
假设int a = 0x12345678;
前面说了16进制每位是4个字节,在内存中,是以8个字节作为1byte进行存储的,因此0x12345678中每两位作为1byte,其中0x78是低位,0x12是高位。
在内存中的存储方式有两种:

0x12345678的低位(0x78)存在低地址,即方式1,叫做小字节序(Little endian);
0x12345678的高位(0x12)存在低地址,即方式2,叫做大字节序(Big endian);
一般的arm芯片都是小字节序,对于2440可以设置某个寄存器,让整个系统使用大字节序或小字节序,它默认使用小字节序。
置位- 把a的bit7、8置位(变为1)
int a = 0x123; int b = a|(1<<7)|(1<
清位 -把a的bit7、8清位(变为0)
int a = 0x123; int b = (a& ~(1<<7))&(~(1<
置位和清位在后面寄存器的操作中,会经常使用。
第007节_编写C程序控制LED
接下来,我们来编写C程序来控制led。
我们写出了main函数,以下有几个问题需要我们仔细考虑一下:
a, 谁来调用它?
b. main函数中变量保存在内存中, 这个内存地址是多少?
答: 我们还需要写一个汇编代码, 给main函数设置内存, 调用main函数
led.c源码:
int main()
{
unsigned int *pGPFCON = (unsigned int *)0x56000050;
unsigned int *pGPFDAT = (unsigned int *)0x56000054;
/*配置GPF4为输出引脚*/
*pGPFCON = 0x100;
/*配置GPF4输出0*/
*pGPFDAT = 0;
return 0;
}
start.S源码:
.text
.global _start
_start:
/*设置内存:sp栈*/
ldr sp,=4096 /*nand启动*/
// ldr sp, =0x40000000 /*nor启动*/
/*调用main*/
bl main
halt:
b halt
Makefile源码:
all:
arm-linux-gcc -c -o led.o led.c
arm-linux-gcc -c -o start.o start.S
arm-linux-ld -Ttext 0 start.o led.o -o led.elf
arm-linux-objcopy -O binary -S led.elf led.bin
arm-linux-objdump -D led.elf > led.dis
clean:
rm *.bin *.o *.elf *.dis
最后将上面三个文件放入Ubuntu主机编译,然后烧写到开发板即可
第008节_几条汇编指令_bl_add_sub_ldm_stm
1,add
add r0,r1,#4
1
功能:r0=r1+4
2,sub
sub r0,r1,#4
1
功能:r0=r1-4
3,bl (branch and link)
bl xxx
1
功能:第一步,将返回地址保存到 lr (r14)寄存器中;第二步,跳转到xxx
4,ldm 和 stm
ldmia sp, {fp,sp,pc}
1
功能:读内存,将值写入到多个寄存器中。
stmdb sp!, {fp,ip,lr,pc}
1
功能:把多个寄存器的内容写入到内存中。
我们把两条指令放到一起来讲:
指令有4种前缀,分别为
ia:Increment After过后增加
ib:Increment Before预先增加
da:Increment Before过后减少
db:Decrement Before预先减少
并且,对于两条指令而言都是高地址内存写入高编号寄存器、高编号寄存器写入高编号地址。
针对以上两条举例来讲解:
其中,sp!表示sp的最终值为修改后的值。
其中,sp表示sp的值保持不变。
009节_解析C程序的内部机制
上面我们共写了三段代码,下面进行分析。
1,start.S
start.S中,实际上做了两件事情;
-设置栈
-调用了main函数,并把返回地址存入lr寄存器。
2,led.c
led.c中,实际上也做了一下三件事情:
-定义两个局部变量
-设置变量
-return 0
由此我们可能要问一下几个问题:
1)为何要设置栈?
因为C函数要用。
2)怎么使用栈?
一是存放局部变量,二是存放lr等寄存器的值。
3)调用者如何传参数给被调用者?
4)被调用者如何传返回值给调用者?
5)怎么从栈中恢复那些寄存器?
接下来,以上节编写的代码为例来分析整个过程:
Disassembly of section .text:
00000000
0: e3a0da01 mov sp, #4096 ; 0x1000`
4: eb000000 bl c
00000008
8: eafffffe b 8
0000000c
c: e1a0c00d mov ip, sp
10: e92dd800 stmdb sp!, {fp, ip, lr, pc}
14: e24cb004 sub fp, ip, #4 ; 0x4
18: e24dd008 sub sp, sp, #8 ; 0x8
1c: e3a03456 mov r3, #1442840576 ; 0x56000000
20: e2833050 add r3, r3, #80 ; 0x50
24: e50b3010 str r3, [fp, #-16]
28: e3a03456 mov r3, #1442840576 ; 0x56000000
2c: e2833054 add r3, r3, #84 ; 0x54
30: e50b3014 str r3, [fp, #-20]
34: e51b2010 ldr r2, [fp, #-16]
38: e3a03c01 mov r3, #256 ; 0x100
3c: e5823000 str r3, [r2]
40: e51b2014 ldr r2, [fp, #-20]
44: e3a03000 mov r3, #0 ; 0x0
48: e5823000 str r3, [r2]
4c: e3a03000 mov r3, #0 ; 0x0
50: e1a00003 mov r0, r3
54: e24bd00c sub sp, fp, #12 ; 0xc
58: e89da800 ldmia sp, {fp, sp, pc}
Disassembly of section .comment:
00000000
0: 43434700 cmpmi r3, #0 ; 0x0
4: 4728203a undefined
8: 2029554e eorcs r5, r9, lr, asr #10
c: 2e342e33 mrccs 14, 1, r2, cr4, cr3, {1}
10: Address 0x10 is out of bounds.
上面是我们将led.bin文件进行反汇编后的文件。结合图示来分析;
机器代码会被复制到cpu中的4K的SDRAM中,如下图所示:
片内4KRAM:

我们根据汇编指令行号来一行一行的分析:
00000000
0: e3a0da01 mov sp, #4096 ; 0x1000 //sp=4096
史海拾趣
|
2007年秋桂林市核子软件有限公司招聘信息 2007年桂林市核子软件开发有限公司招聘信息公司简介: 桂林市核子软件开发有限公司主要从事于嵌入式软件及网络新技术的研究和开发,主要由深圳市双赢伟业科技有限公司投资兴办。公司主要客户是双赢伟 ...… 查看全部问答> |
|
红外距离指的是红外光所达到的距离,从几米到几百米,甚至是数公里。而夜间可视距离是通过监视器所能够看到的清晰有效最大距离,是由红外灯的发光距离、摄像机的感红外程度、现场反射红外情况、供电电源的质量及镜头的匹配情况来决定的。它们之间相 ...… 查看全部问答> |
|
向你请教 WINCE + 2410 + I2C + AT24C02 的问题! 我想用 2410 开发板,WinCE 4.2 操作系统,I2C 总线读写 AT24C02 EEPROM。 我已经看懂了 2410 的 I2C 寄存器和 AT24C02 的读写方法;也看懂了 ADS 下的纯 C 语言程序。 因为我以前一直做桌面的程序,底子比较差;就是对 WINCE 下的 I2C 流驱动没 ...… 查看全部问答> |
|
如题,我用cypress的芯片控制nand flash,型号是k9f1g08u0a,gpif波形图如下,现在我换了一个flash,时序上不一样,没弄过这些,不知道这些图形都代表什么,盼高手指点。 d:\\flash\\gpif\\1.jpg d:\\flash\\gpif\\2.jpg d:\\flash\\gpif\\3.jpg ...… 查看全部问答> |
|
本帖最后由 paulhyde 于 2014-9-15 03:51 编辑 做一个低频信号分析仪要求 基本要求 在输入交流信号电压峰峰值Vpp为500m V~5V,频率范围为50Hz~50kHz条件下,完成以下参数测试; 正弦波,三角波和方波频率测试,测量误差小于1%; 正弦波 ...… 查看全部问答> |




