韦东山嵌入式Linux学习笔记5-点亮LED灯例程

韦东山嵌入式Linux学习笔记-1 概述

第一个例程-点灯

  1. PC VS 嵌入式Linux
    PC: BIOS->引导操作系统->识别分区->启动应用程序
    嵌入式Linux:BootLoader(包括裸板程序)->引导Linux操作内核系统->挂接根文件系统->启动应用程序
  2. 教程第一部分:裸板程序
    • Windows系统上:ARM Developer Suite,IAR,Keil是用来写裸板程序的几种常见的集成开发环境(IDE);
    • Linux:使用GCC,GNU工具链,没有IDE,在Windows上使用文本编辑器后,移动到Linux服务器(虚拟机)上,编译链接,然后把生成的二进制文件拷贝回来,在windows上进行烧写;
    • 编辑程序代码->编译(一系列.o文件),链接(一个.exe或.bin,.hex文件等)->烧写测试。
  3. 什么是裸板程序
    • 裸板程序=启动代码+C程序;
    • 启动代码功能:进行硬件初始化设置,并给出C程序的函数指针以进行调用。

    大学里学习的C程序,其启动代码本质上式由操作系统提供的,它要求你的APP必须从main函数进入;而这里启动代码是要我们自己写的,可以不从main进入程序.
    启动代码:STM32中的.s文件;PC中的BIOS;嵌入式Linux中的BootLoader。

  4. 启动代码的内容
    • 设置CPU(把外设的地址告诉CPU,6410板子需要,2440板子则不需要此步骤);
    • 关闭开门狗:在板子上电之内3秒钟内,关闭看门狗,否则3秒后将重启。
    • 设置栈空间大小,调用C函数。
  5. 板子启动时都做了哪些活动?
    本板子的程序启动方式均为NAND Flash启动。上电时, NAND Flash的前8K的字节数据会被硬件原原本本复制到6410 CPU的0地址。把SP(栈顶)指向8K空间的尾地址。
  6. 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	
  1. 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: //标号(跳转点)
  1. 代码解读
  • 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 这种是伪指令,其中=表示该立即数应被视为一个地址,它后面必须与[]配合使用;

  1. 在linux系统上编译此汇编代码
  • 将上述汇编.s代码的整个工程拷贝到Linux服务器上,并在服务器上的Terminal打开存放此.s程序的目录,并执行以下命令;
  • arm-linux-gcc -c -o start.o start.S手动执行编译命令: -c表示编译但不链接,源文件为start.S,输出为start.o
  • arm-linux-ld -Ttext 0 -o led.elf start.o链接命令: ld表示链接, -Ttext 0表示代码段从文本0处开始,输出为led.elf,输入为start.o
  • arm-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          //恢复运行
  1. 点灯例程进阶-流水灯

.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	

  1. 使用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的使用

  1. make clean:清除编译结果;
  2. make [目标]:生成[目标]文件,如果只写了make,则生成Makefile中的第一个<目标文件>。
  3. 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
posted @ 2019-12-03 09:03  云远·笨小孩  阅读(758)  评论(0编辑  收藏  举报