韦东山嵌入式Linux学习笔记5-点亮LED灯例程
韦东山嵌入式Linux学习笔记-1 概述
第一个例程-点灯
- PC VS 嵌入式Linux
PC:BIOS
->引导操作系统
->识别分区
->启动应用程序
嵌入式Linux:BootLoader(包括裸板程序)
->引导Linux操作内核系统
->挂接根文件系统
->启动应用程序
- 教程第一部分:裸板程序
- Windows系统上:ARM Developer Suite,IAR,Keil是用来写裸板程序的几种常见的集成开发环境(IDE);
- Linux:使用GCC,GNU工具链,没有IDE,在Windows上使用文本编辑器后,移动到Linux服务器(虚拟机)上,编译链接,然后把生成的二进制文件拷贝回来,在windows上进行烧写;
- 编辑程序代码->编译(一系列.o文件),链接(一个.exe或.bin,.hex文件等)->烧写测试。
- 什么是裸板程序
- 裸板程序=启动代码+C程序;
- 启动代码功能:进行硬件初始化设置,并给出C程序的函数指针以进行调用。
大学里学习的C程序,其启动代码本质上式由操作系统提供的,它要求你的APP必须从main函数进入;而这里启动代码是要我们自己写的,可以不从main进入程序.
启动代码:STM32中的.s
文件;PC中的BIOS;嵌入式Linux中的BootLoader。 - 启动代码的内容
- 设置CPU(把外设的地址告诉CPU,6410板子需要,2440板子则不需要此步骤);
- 关闭开门狗:在板子上电之内3秒钟内,关闭看门狗,否则3秒后将重启。
- 设置栈空间大小,调用C函数。
- 板子启动时都做了哪些活动?
本板子的程序启动方式均为NAND Flash启动。上电时, NAND Flash的前8K的字节数据会被硬件原原本本复制到6410 CPU的0地址。把SP(栈顶)指向8K空间的尾地址。 START.s
裸板程序
.globl _start
_start:
/* 硬件相关的设置:把外设的基地址告诉CPU */
/* 因为CPU访问内存地址(0x0000_0000-0x6FFF_FFFF)和外设地址(0x7000_0000-0x7FFF_FFFF)的方法不一样: */
/* Peri port setup */
ldr r0, =0x70000000
orr r0, r0, #0x13
mcr p15,0,r0,c15,c2,4 @ 256M(0x70000000-0x7fffffff)-ARM11手册中规定了外设的寻址空间
/* 关看门狗 */
/* 往WTCON(0x7E004000)写0 */
ldr r0, =0x7E004000
mov r1, #0
str r1, [r0]
/* 配置寄存器GPMCON(0x7F008820地址)为0x1000, 让GPM3作为输出引脚 */
ldr r1, =0x7F008820
mov r0, #0x1000
str r0, [r1]
/* 配置寄存器GPMDAT(0x7F008824地址)为0x0, 让GPM3输出为低电平*/
ldr r1, =0x7F008824
mov r0, #0
str r0, [r1]
halt:
b halt
- ARM汇编约定
在ARM 汇编指令中,{条件}是指令执行的条件代码。当{条件}忽略时(没有指定时)指令为无条件执行。ARM 微处理器可支持多达 16 个协处理器,用于各种协处理操作,在程序执行的过程中,每个协处理器只执行针对自身的协处理指令,忽略 ARM 处理器和其他协处理器的指令。
- 1 Byte <=> 8-bits, 1 Word <=> 32 bits.
- =0x7000_0000: 伪汇编指令。
-
0: 立即数表示的数据。
- ARM核共有37个寄存器,任何工作模式下都只能访存18个寄存器。37个寄存器中,30个为"通用"型,1个固定用作"PC",一个固定用作"CPSR",5个固定用作5中异常模式下的"SPSR"。
以下寄存器中如无特别指明,均为ARM处理器寄存器。八种寻址方式:
- 寄存器寻址 mov r1, r2
- 立即寻址 mov r0, #0xFF00
- 寄存器移位寻址 mov r0, r1, lsl #3 //r1 左移 3 位给 r0 --> r0=r1*8
- 寄存器间接寻址 ldr r1, [r2] //r2 存放的内容指向的地址的值给 r1
- 基址变址寻址 ldr r1, [r2, #4] //r2 存放的内容加 4 指向的地址的值给 r1
- 多寄存器寻址 ldmia r1!, {r2-r7, r12} //将 r1 内容指向的地址(相当于数组的首地址)依次加载到 r2 到 r7 和 r12 寄存器
- 堆栈寻址 stmfd sp!, {r2-r7, lr} //从栈里面依次连续访问多个字节放的寄存器了(即用于压栈操作)
- 相对寻址 beq flag //跳转到标号处
- flag: //标号(跳转点)
- 代码解读
- LDR指令(LoadRegister):加载指令,格式为
LDR{条件} <目标寄存器>,<存储器地址>
,这里R0寄存器赋值为0x7000_0000; - ORR指令(OrRegister): 位或指令,格式为
ORR{条件} <目标寄存器>,<操作数1>,<操作数2>
,这里将R0的bit0,bit1,bit4置为1,R0更新为0x7000_0013;* MCR指令(MoveCRegister):ARM处理器寄存器
到协处理器寄存器
的数据传送指令,格式为MCR{条件} <协处理器编码>, <协处理器操作码1>, <源寄存器,不能为PC寄存器>, <目标寄存器1>, <目标寄存器2>{,<协处理器操作码2>}
;这里将ARM处理器寄存器R0的数值移动到了协处理器P15的寄存器C15和C2处。则C15寄存器的值更新为0x7000_0013;表示外设地址空间的基地址为0x7000_0000,Size为256MByte. - STR指令(StoreRegister):这里把0存储到[0x7E004000]处,实现关看门狗(往
WTCON(0x7E00_4000)
写0);
注意:
ldr r0, =0x7E004000
这种是伪指令,其中=
表示该立即数应被视为一个地址,它后面必须与[]
配合使用;
- 在linux系统上编译此汇编代码
- 将上述汇编.s代码的整个工程拷贝到Linux服务器上,并在服务器上的Terminal打开存放此
.s
程序的目录,并执行以下命令; arm-linux-gcc -c -o start.o start.S
手动执行编译命令: -c表示编译但不链接,源文件为start.S,输出为start.oarm-linux-ld -Ttext 0 -o led.elf start.o
链接命令: ld表示链接, -Ttext 0表示代码段从文本0处开始,输出为led.elf,输入为start.oarm-linux-objcopy -O binary -S led.elf led.bin
文件转换命令: 将.elf文件转换为二进制.bin文件。arm-linux-objdump -D led.elf > led.dis
反汇编命令: 将得到的.elf反汇编为汇编命令。- 将服务器上 编译/链接的结果
.bin
拷贝回开发机器上。 - 连接开发板,打开OpenJtag GUI软件,选择要烧写的Target板,设置工程目录,点击Connect连接开发板,点击Telnet,输入以下对开发板进行烧写。
halt //停止当前开发板的运行
nand probe 0 //查找器件 `NAND 0`
nand erase 0 0 0x20000 //擦除`NAND 0`的0地址处,擦除大小为0x20000,即1个block
nand write 0 led.bin 0 //将led.bin烧写到`NAND 0`的0地址处
reset //复位当前开发板
halt //停止运行
resume //恢复运行
- 点灯例程进阶-流水灯
.globl _start
_start:
/* 硬件相关的设置 */
/* Peri port setup */
ldr r0, =0x70000000
orr r0, r0, #0x13
mcr p15,0,r0,c15,c2,4 @ 256M(0x70000000-0x7fffffff)
/* 关看门狗 */
/* 往WTCON(0x7E004000)写0 */
ldr r0, =0x7E004000
mov r1, #0
str r1, [r0]
/* 设置GPMCON让GPM0/1/2/3作为输出引脚 */
ldr r1, =0x7F008820
ldr r0, =0x1111
str r0, [r1]
/* 循环设置GPMDAT让GPM0/1/2/3闪烁 */
ldr r1, =0x7F008824
mov r0, #0
loop:
str r0, [r1] /*刚开始时灯全亮,后面每delay一次,r0从0加到16,依次对灯进行点亮*/
add r0, r0, #1 /*计数器加1*/
cmp r0, #16 /*当计数器达到16时*/
moveq r0, #0 /*令r0为0*/
bl delay /* bl 跳转指令: 跳转到delay块(计数从0x10000到0),并把下一条指令的地址存储到LR寄存器*/
b loop /* b 跳转指令: 跳转到loop语句块*/
delay: /* 实现了从0x10000到0的倒计数*/
mov r2, #0x10000
delay_loop:
sub r2, r2, #1
cmp r2, #0
bne delay_loop /* 当r2 NotEqual 0, 跳转到delay_loop语句*/
mov pc, lr /* 对应于bl 跳转指令: LR寄存器的地址放回PC,返回原调用块,继续执行后续指令*/
halt:
b halt
- 使用C语言的流水灯程序
首先在start.S中调用C程序的地址
.globl _start
_start:
/* 硬件相关的设置 */
/* Peri port setup */
ldr r0, =0x70000000
orr r0, r0, #0x13
mcr p15,0,r0,c15,c2,4 @ 256M(0x70000000-0x7fffffff)
/* 关看门狗 */
/* 往WTCON(0x7E004000)写0 */
ldr r0, =0x7E004000
mov r1, #0
str r1, [r0]
/* 设置栈空间大小,并调用C程序*/
ldr sp,=8*1024 /*栈的地址指向8K空间的尾地址,栈空间用于存储C程序的变量和函数指针等,SP指针是向下生长的*/
bl xxxxx /*不一定非要用main函数*/
halt:
b halt
对应的C语言:
void delay()
{
volatile int i = 0x10000; //volatile防止编译器把该变量优化掉
while (i--);
}
int xxxxx()
{
int i = 0;
volatile unsigned long *gpmcon = (volatile unsigned long *)0x7F008820;
volatile unsigned long *gpmdat = (volatile unsigned long *)0x7F008824;
/* gpm0,1,2,3设为输出引脚 */
*gpmcon = 0x1111;
while (1)
{
*gpmdat = i;
i++;
if (i == 16)
i = 0;
delay();
}
return 0;
}
在启动文件.S中,调用XXXXX函数时,都做了什么(可以通过反汇编查看)? 1. 保护现场:
stmdb sp!, {fp,ip,lr,pc}
, store multi-reg decrease before,SP指针进行修改(减去4),然后把PC(r15),LR(r14),IP(r12),FP(r11)转存到刚刚空出的4个内存空间. 2 执行调用程序 3. 恢复现场:ldmia sp,{r3,fp,sp,pc}
,load multi-reg increase after,把寄存器PC(r15),SP(r14),FP(r12),r3的值恢复到寄存器,
《嵌入式Linux应用开发完全手册》中讲述了ATCPS规则,汇编程序和C程序之间的调用如果要传参数,则第一个参数用r0传输,第二个参数用r1传输,...依次类推,这样最多传输4个参数.超过4个参数时,多出的参数需要存储到栈空间的6K-8地址处更详细的规则请看《嵌入式Linux应用开发完全手册》。
提醒:C程序操作寄存器应全部使用位操作来保证不影响其他寄存器。如
*gpmcon = (*gpmcon & ~0xffff) | 0x1111;
就仅仅把低16位改为0x1111;
C程序调用汇编程序(程序7)
汇编.S文件中:
/*声明delay函数global,C程序才能调用*/
.globl delay
/*...*/
/*实现的delay函数*/
delay:
delay_loop:
cmp r0,#0
sub r0,r0,#1
bne delay_loop
mov pc,lr
C程序中,
void delay(int count);
void xxxxx(int start)
{
//...
//...
while (1)
{
*gpmdat = (*gpmdat & ~0xf) | i;
i++;
if(i == 16)
i=0;
delay(0x10000);
}
}
ARM汇编指令/伪指令学习
每条ARM指令的长度为4字节,所以MOV R0,#0x12345678
这种包含32位立即数的指令并不能真实存在,因此,为了让指令能正常运行, 需要使用LDR R0,=0x12345678
.
ldr r1, [r2, #4] /*将地址为r2+4的内存单元数据读取到r1中*/
ldr r1, [r2], #4 /*将地址为r2的内存单元数据读取到r1中,然后r2 = r2 + 4*/
/*将数值0x00004000存储到以0x56000010为地址的存储单元中*/
/* 汇编代码 //反汇编的代码 */
LDR R0,=0x56000010 //ldr r0, [pc, #68] ; 0x4c, 可见该语句LDR R0,=0x56000010 被转换成ldr指令来执行
; PC = 当前指令的地址+0x08 = 0x08; PC+68=76 = 0x4c; LDR将4C地址处的值(0x56000010)存储到r0;
MOV R1,#0x00004000 //mov r1, #16384 ; 0x4000
STR R1,[R0] //str r1, [r0]
/*将数值0x00004000存储到以0x56000010为地址的存储单元中*/
/* 汇编代码 //反汇编的代码 */
LDR R0,=0x56000000 //mov r0, #1442840576 ; 0x56000000, 可见该语句LDR R0,=0x56000010 被转换成mov指令来执行
MOV R1,#0x00004000 //mov r1, #16384 ; 0x4000
STR R1,[R0] //str r1, [r0]
/*通过以上两个例子:LDR伪指令是根据地址值是否**太过复杂**,来决定转换为ldr指令或MOV指令执行。*/
Makefile的使用
make clean
:清除编译结果;make [目标]
:生成[目标]文件,如果只写了make
,则生成Makefile中的第一个<目标文件>。- Makefile的语法规则如下,
<目标文件>:[space]<依赖文件1>[space]<依赖文件2>
[Tab]命令
当依赖文件已经更新,或者目标文件尚未生成时,命令才能被执行。但这时如果依赖文件也找不到,Makefile就会自动查找可以生成该依赖文件的规则。
以下是Makefile的简单示例,它包含了3条规则:
led.bin: start.o
arm-linux-ld -Ttext 0 -o led.elf start.o
arm-linux-objcopy -O binary -S led.elf led.bin
arm-linux-objdump -D led.elf > led.dis
start.o : start.S
arm-linux-gcc -o start.o start.S -c
clean:
rm *.o led.elf led.bin led.dis