• 韦东山嵌入式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
    
  • 相关阅读:
    2021/1/10周总结一
    java基础复习五
    java基础复习四
    构建之法读书笔记三
    javaweb学生教务系统
    java基础复习三
    关于eclipse项目错误的一些小经验总结
    Java基础复习二
    JavaDoc文档
    2017-2018-2 20179216 《网络攻防与实践》 第四周总结
  • 原文地址:https://www.cnblogs.com/charleechan/p/11974658.html
Copyright © 2020-2023  润新知