嵌入式ARM裸板程序——GPIO

本博文是根据百问网韦东山老师的嵌入式视频教程,基于jz2440进行嵌入式开发学习所记录的学习体会,如有雷同纯属巧合。

GPIO

首先是第一个裸板程序GPIO,由于视频教程实在足够详细,因此我们直接先看程序源码理解好一些重要的过程原理:

@******************************************************************************
@ File:crt0.S
@ 功能:通过它转入C程序
@******************************************************************************       

.text
.global _start
_start:
            ldr     r0, =0x53000000     @ WATCHDOG寄存器地址
            mov     r1, #0x0                     
            str   r1, [r0]              @ 写入0,禁止WATCHDOG,否则CPU会不断重启

            ldr     sp, =1024*4         @ 设置堆栈,注意:不能大于4k, 因为现在可用的内存只有4K
                                        @ nand flash中的代码在复位后会移到内部ram中,此ram只有4K
            bl      main                @ 调用C程序中的main函数
halt_loop:
            b       halt_loop

首先是第一个crt0.S的程序源文件,这是一个汇编文件,里面的代码是ARM架构下的汇编源码,这个源代码已经给出一部分注释了,接下来我们结合自身的学习情况对其进行一个系统性的自我分析与学习:

首先这个文件编译后的作用是为了可以转入到后面的主要功能部分c程序中执行对GPIO操作从而点亮开发板led灯的作用,开头的.text代表了代码段的起始位置,所谓代码段在程序编译——链接的概念中是属于一个重要的概念部分,代码段text我们可以理解为就是程序执行指令所在的内存区域部分,这部分里面的代码在编译链接后会负责存放一些直接可以被CPU一条一条依序执行的机器指令,而且这部分区域通常是在程序运行前就已经指定好了,且该段一般来说是只读属性,这是OS保护机制的一个特性,为了防止其他恶意或者无意间访问这个代码段从而破坏程序的行为。这里我们只做简单的理解并不继续深究。

而.global _start _start:代表了符号链接,可以这么直白理解为这是告诉编译器,可以根据这个符号标志进行程序入口地址的定位,这个_start:就是一个这样的入口标志。
接着,汇编代码

ldr     r0, =0x53000000

ldr是汇编的一条传输指令,主要的作用是将0x53000000这个内存地址处的4字节数据传输至r0寄存器中。在注释中简单的理解就是定位watchdog看门狗的地址。而后mov指令将r1寄存器赋值0,之后的str指令便将r1中保存的0直接设置给r0中看门狗的地址。这里的看门狗其实是一个定时器监控芯片,在微机中通常主要是用作定期的查看芯片内部的情况,一旦发生错误就向芯片发出重启信号的电路。看门狗命令在程序的中断中拥有最高的优先级。因此为了让程序可以正常的运行,就必须要先关闭设置看门狗,否则开发板会一直不断复位重启。

随后的ldr sp, =1024*4指令会完成初始化堆栈的任务。这里要介绍到一个程序栈的概念。
我们通常所说的堆栈,除了数据结构中的基本算法结构之外,更多的是代表着程序的函数调用堆栈,通常来说,对于c程序尤其是UNIX/Linux的可执行文件ELF类型的程序来说,程序除了上述所提及的代码段text之外,相对来说还有ELF文件头结构(主要有文件属性、段表等)、数据段data(一般存放已经初始化的全局变量和局部静态变量)、只读数据段rodata(一般是只读const变量和字符串常量)、bss段(一般存放未初始化的全局变量和静态局部变量)、comment注释信息段(主要存放注释相关的数据)等.当然除此之外还有其他段信息,但是常用的最重要的也就是上述这几个分段。
之所以要将可执行文件(其实更多的应该是目标文件.o,因为目标文件.o的文件布局就是上述这种分段格式,而目标文件链接后就成为可执行文件)按照这类分段思维进行处理,是因为OS的段式内存管理和保护模式的要求,具体的我们往后会结合相关博文再进一步详细解析。

回到汇编程序中,了解了程序分段布局结构后,我们就可以了解到,当程序被加载运行后,肯定是在内存的某一些区域(当然有可能程序占用的内存区域不连续,被分成几个区域),而C程序占用的内存分为以下几个部分:
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数名,局部变量的名等。
2、堆区(heap)— 由程序动态分配释放, 若程序在执行过程中没有释放动态分配的内存空间,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、静态区(static)—全局变量和局部静态变量的存储是放在一块的。程序结束后由系统释放。——>相当于data段
4、文字常量区—常量字符串就是放在这里的,程序结束后由系统释放 。——>相当于rodata段
5、程序代码区— 存放程序执行代码的区域,可以直接被CPU获取执行指令。——>相当于上述的text段

至此,我们可以简单了解到一个c程序在内存中的基本布局结构。因此,汇编程序中的初始化设置堆栈的作用就是为了可以在内存中开辟一段区域标记为栈区存放位置,而且栈的增长方向是从上往下,堆则是从下往上。那么最为重要的程序代码段也就是text的设置应该放在何处呢?

韦老师视频中有介绍到,如果是NANDFLASH的启动方式,那么2440开发板的一个特性就是在单板上电后,nandflash控制器会自动的从flash存储介质上拷贝4K的数据到片内内存SRAM中,这里片内内存SRAM的容量恰好就是4K,而拷贝时开发板是不管有效的数据到底有没有4K的大小,它都会照搬不误。因此,当我们写好程序烧录进flash中时,程序会自动将我们烧进去的bin文件拷贝到4K内存中去。而这个点灯例子的程序远远不到4K的大小,这些编译完后的程序代码会拷贝到内存0地址起始的位置。此时从0地址开始的代码就是相当于text段。所以,我们可以清楚的了解到这部分汇编源码的主要目的。

而接着的bl main这条跳转指令就是通知cpu,将程序计数器PC指针指向c程序中的main函数入口地址,之所以要用到这条指令,是在此指令之前,程序计数器PC都是从0地址开始逐一逐条进行取指执行。所以这条指令是这个汇编程序最重要的一步。从这条指令完成后,程序流便会转向c文件中的main函数开始执行,并且利用开始设置好的堆栈进行一个函数调用过程。

接下来让我们继续分析下c程序:


#define GPFCON      (*(volatile unsigned long *)0x56000050)
#define GPFDAT      (*(volatile unsigned long *)0x56000054)

#define GPF4_out    (1<<(4*2))
#define GPF5_out    (1<<(5*2))
#define GPF6_out    (1<<(6*2))

void  wait(volatile unsigned long dly)
{
    for(; dly > 0; dly--);
}

int main(void)
{
    unsigned long i = 0;

    GPFCON = GPF4_out|GPF5_out|GPF6_out;        // 将LED1,2,4对应的GPF4/5/6三个引脚设为输出

    while(1){
        wait(30000);
        GPFDAT = (~(i<<4));        // 根据i的值,点亮LED1,2,4
        if(++i == 8)
            i = 0;
    }

    return 0;
}

直接看源码进行解析,首先开头的两个宏定义我们可以根据视频教程了解到这是为了用GPFCON和GPFDAT两个符号标记代表了相应的寄存器位,以便可以直接对对应的寄存器进行操作设置。这是两个指针,这两个指针的地址分别就是0x56000050和0x56000054这两个地址,分别对应着GPFCON寄存器和GPFDAT寄存器的寄存器内存地址。以前一开始自己有一个模糊的概念,这两个所谓寄存器的内存地址到底位于SRAM还是SDRAM中的呢?答案是都不是!实际上,寄存器的地址大都是编入CPU的统一编址,和内存一样,因此这两部分的地址空间其实是统一可以提供给CPU访问但是同时又独立分开不在同一个区域内的。
之后的三个宏定义则是为了标识操作码,这三个操作码的作用就是为了执行按位或操作后将结果保存到GPFCON寄存器中,从而达到设置将GPFCON所控制的对应led引脚设置为高电平即输出状态,从而达到点灯效果。
而另一个寄存器GPFDAT寄存器则是实现对相应的位进行对应的设置,所以在while循环中,程序对对应的寄存器位进行赋值操作从而达到拉低引脚电平实现LED点灯效果。
这部分需要结合视频并且看芯片的数据手册,几乎单片机系列的开发都得观看数据手册,可以详细了解到芯片的物理特性。
而另外一个函数则是简单的延时程序函数,该函数简单的执行了一个循环空操作,通过让CPU将执行资源浪费在这个for循环的判断中从而达到简单延迟。

其他的程序包括:

SECTIONS {
    . = 0x00;
    .text          :   { *(.text) }
    .rodata ALIGN(4) : {*(.rodata)} 
    .data ALIGN(4) : { *(.data) }
    .bss ALIGN(4)  : { *(.bss)  *(COMMON) }
}

这是一个链接脚本,所谓链接脚本是GNU的连接器ld所提供的一种用来把一定量的目标文件跟档案文件链接在一起,并重新定位它们的数据,链接符号引用。一般编译一个程序时,最后一步就是运行ld进行链接。每一个链接都被一个链接脚本所控制,这个脚本是用链接命令语言书写的。
链接脚本的一个主要目的是描述输入文件中的各个段(数据段、代码段、堆、栈、bss)如何被映射到输出文件中,并控制输出文件的内存排布。链接器总是使用链接脚本的,如果你不提供,则链接器会使用一个缺省的脚本,这个脚本是被编译进链接器可执行文件的。
我们可以使用–verbose命令行显示缺省的链接器脚本的内容。也可以使用-T命令行来提供你自己的链接脚本来替换缺省的链接脚本。下面所讲述的makefile就是利用这种方式。
所以这个链接脚本就是通知GCC编译器,说明程序段的一些布局设置。具体的在视频教程中有介绍到,而且也可以结合前面的说明理解清晰,这里不再赘述。

CFLAGS  := -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -ffreestanding
leds.bin : crt0.S  leds.c
    arm-linux-gcc $(CFLAGS) -c -o crt0.o crt0.S
    arm-linux-gcc $(CFLAGS) -c -o leds.o leds.c
    arm-linux-ld -Ttext 0x0000000 crt0.o leds.o -o leds_elf
#   arm-linux-ld -Tleds.lds  crt0.o leds.o -o leds_elf
    arm-linux-objcopy -O binary -S leds_elf leds.bin
    arm-linux-objdump -D -m arm  leds_elf > leds.dis
clean:
    rm -f   leds.dis leds.bin leds_elf *.o

最后是一个makefile文件,这个makefile文件中我们主要看链接部分的代码:

arm-linux-ld -Ttext 0x0000000 crt0.o leds.o -o leds_elf

这是告诉编译器,程序链接时将crt0.o leds.o这两个目标文件中的可执行代码存放在代码段内存地址是0x0000000的位置。

arm-linux-ld -Tleds.lds crt0.o leds.o -o leds_elf

而这条指令则被注释掉,它的作用也是通知编译器,说明程序链接时应该要按照已规定好的链接脚本也就是上述的那个链接脚本进行一个程序的链接过程

至此,我们算是详细地分析了所有源码的基本原理和相关引申知识。blog是一个再学习的过程,第一次写关于ARM嵌入式开发的博客,难免杂乱无章并且有所不足。希望接下来可以继续坚持!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值