3.1 C语言实现GPIO输出实验(LED)——让C跑起来(一)

  • Post author:
  • Post category:其他


基于


汇编实现GPIO输出实验(LED)


硬件基础和分析。这里通过汇编完成C语言的环境搭建,用C语实现LED的点灯。



一、汇编搭建C语言运行环境



1、运行环境初始化

Cortex-A有九个运行模型,这里我们设置处理器运行在SVC模式下。处理器模式的设置是通过修改CPSR(程序状态)寄存器来完成的。

/******************************
*file name: start.S
*******************************/

.global _start

_start:
    /* 设置处理器进入SVC模式 */
    mrs r0, cpsr        /* 读取cpsr到r0*/
    bic r0, r0, #0x1f   /* 清除cpsr的bit4-0*/
    orr r0, r0, #0x13   /* 使用SVC模式*/
    msr cpsr, r0        /* 将r0写入到cpsr*/

    /* 设置SP指针 */
    ldr sp, =0x80200000
    b main              /* 跳转到C语言main函数*/

通过 ldr 指令设置 SVC 模式下的 SP 指针=0X80200000,因为 I.MX6U-ALPHA 的 开发板DDR3 地 址 范 围是

0X80000000~0XA0000000(512MB)

或者

0X80000000~0X90000000(256MB)

,不管是512MB版本还是256MB 版本的,其 DDR3 起始地址都是 0X80000000。由于 Cortex-A7的堆栈是向下增长的,所以将 SP指针设置为 0X80200000,因此SVC模式的栈大小0X80200000-0X80000000=0X200000=2MB。

b main就是跳转到main 函数,main 函数就是 C 语言代码了。



2、I.MX6U为什么不用初始化DDR

汇编程序就几行代码,用来设置处理器运行到 SVC 模式下、然后初始化 SP 指针、最终跳转到 C 文件的 main 函数中。在使用三星的 S3C2440 或者 S5PV210 的话,我们在使用 SDRAM 或者 DDR 之前必须先初始化 SDRAM或者DDR。所以S3C2440或者S5PV210的汇编文件里面是一定会有SDRAM或者DDR初始化代码的。我们上面编写的start.s文件中却没有初始化DDR3的代码,但是却将SVC模式下的SP指针设置到了DDR3的地址范围中,这不会出问题吗?肯定不会的, DDR3 肯定是要初始化的,但是不需要在 start.s 文件中完成。 DCD 数据包含了 DDR 配置参数, I.MX6U 内部的 Boot ROM 会读取 DCD 数据中的 DDR 配置参数然后完成 DDR 初始化的。

那DCD又是从何而来?你的设备里面得有代码啊,在


汇编实现GPIO输出实验(LED)


我们使用 imxdownload 这个软件将 led.bin 烧写到了 SD 卡中。imxdownload 会在 led.bin

前面添加一些头信息,重新生成一个叫做 load.imx 的文件,最终实际烧写的是 laod.imx。那么 imxdownload 究竟做了什么? load.imx 和 led.bin 究竟是什么关系? imxdownload 是如何将 led.bin 打包成 load.imx 的。 I.MX6U 不能直接烧写编译生成的.bin 文件,我们需要在.bin 文件前面添加一些头信息构成满足 I.MX6U 需求的最终可烧写文件, 可烧写文件组成为 IVT+Boot data+DCD+.bin。



Image vector table


:简称 IVT, IVT 里面包含了一系列的地址信息,这些地址信息在 ROM中按照固定的地址存放着。



Boot data


:启动数据,包含了镜像要拷贝到哪个地址,拷贝的大小是多少等等。



Device configuration data


:简称 DCD,设备配置信息,重点是 DDR3 的初始化配置。



.bin


:用户代码可执行文件,比如 led.bin。

所以imxdownload 所生成的 load.imx 就是在 led.bin 前面加上IVT+Bootdata+DCD。内部 Boot ROM 会将 load.imx 拷贝到 DDR 中,用户代码是从0X87800000 这个地方开始的,因为链接地址为 0X87800000, load.imx 在用户代码前面又有 3KByte 的 IVT+Boot Data+DCD 数据,因此led.bin 在 DDR 中的起始地址就是 0X87800000-3072=0X877FF400。



二、C语言实现

1、在头文件使用宏定义将寄存器重命名,使其可以像变量一样直接操作寄存器。

/*************************
*file name : main.h
**************************/

#ifndef __MAIN_H
#define __MAIN_H

/* 定义要使用的寄存器 */

/*时钟相关寄存器地址*/
#define CCM_CCGR0    *((volatile unsigned int *)0X020C4068)
#define CCM_CCGR1    *((volatile unsigned int *)0X020C406C)
#define CCM_CCGR2    *((volatile unsigned int *)0X020C4070)
#define CCM_CCGR3    *((volatile unsigned int *)0X020C4074)
#define CCM_CCGR4    *((volatile unsigned int *)0X020C4078)
#define CCM_CCGR5    *((volatile unsigned int *)0X020C407C)
#define CCM_CCGR6    *((volatile unsigned int *)0X020C4080)

/* 
 * IOMUX相关寄存器地址 
 */
#define SW_MUX_GPIO1_IO03  *((volatile unsigned int *)0X020E0068)
#define SW_PAD_GPIO1_IO03  *((volatile unsigned int *)0X020E02F4)

/* 
 * GPIO1相关寄存器地址 
 */
#define GPIO1_DR     *((volatile unsigned int *)0X0209C000)
#define GPIO1_GDIR   *((volatile unsigned int *)0X0209C004)
#define GPIO1_PSR    *((volatile unsigned int *)0X0209C008)
#define GPIO1_ICR1   *((volatile unsigned int *)0X0209C00C)
#define GPIO1_ICR2   *((volatile unsigned int *)0X0209C010)
#define GPIO1_IMR    *((volatile unsigned int *)0X0209C014)
#define GPIO1_ISR    *((volatile unsigned int *)0X0209C018)
#define GPIO1_EDGE_SEL   *((volatile unsigned int *)0X0209C01C)

#endif

2、在C源文件中对不同功能操作使用函数进行封闭。

/***************************
*file name : main.c
***************************/

#include "main.h"

/* 使能外设时钟 */
void clk_enable(void)
{
    CCM_CCGR1 = 0xFFFFFFFF;
    CCM_CCGR2 = 0xFFFFFFFF;
    CCM_CCGR3 = 0xFFFFFFFF;
    CCM_CCGR4 = 0xFFFFFFFF;
    CCM_CCGR5 = 0xFFFFFFFF;
    CCM_CCGR6 = 0xFFFFFFFF;
}

/* 初始化LED */
void led_init(void)
{
    SW_MUX_GPIO1_IO03 = 0x5;    /* 复用为GPIO1——IO03*/
    SW_PAD_GPIO1_IO03 = 0X10B0; /*设置GPIO1_IO03电气属性 */

    /* GPIO初始化 */
    GPIO1_GDIR = 0x8;           /* 设置为输出 */
    GPIO1_DR = 0X0;             /* 打开LED灯 */
}

/* 短延时 */
void delay_short(volatile unsigned int n)
{
    while(n--){}
}

/* 延时,一次循环大概是1ms,在主频396MHz
* n :延时ms数
*/
void delay(volatile unsigned int n)
{
    while(n--) {
        delay_short(0x7ff);
    }
}

/* 打开LED灯 */
void led_on(void)
{
    GPIO1_DR &= ~(1<<3); /* bit3清零 */
}

/* 关闭LED灯 */
void led_off(void)
{
    GPIO1_DR |= (1<<3); /* bit3置1 */
}

int main(void)
{
    clk_enable();   /* 使能外设时钟 */
    led_init();     /* 初始化LED */
    
    /* 设置LED闪烁 */
    while(1) 
    {
        led_on();
        delay(500);

        led_off();
        delay(500);
    }
    return 0;
}



三、编译文件和链接文件



1、Makefile

objs = start.o main.o 

ledc.bin : $(objs)
 arm-linux-gnueabihf-ld -Timx6u.lds $^ -o ledc.elf 
 arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
 arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis

%.o : %.c
 arm-linux-gnueabihf-gcc -Wall -nostdlib -c  -O2 -o $@ $<

%.o : %.S
 arm-linux-gnueabihf-gcc -Wall -nostdlib -c  -O2 -o $@ $<

clean:
 rm -rf *.o ledc.bin ledc.elf ledc.dis

定义了一个变量 objs, objs 包含着要生成 ledc.bin 所需的材料: start.o 和 main.o,也就是当前工程下的 start.s 和 main.c 这两个文件编译后的.o 文件。这里要注意 start.o 一定要放到最前面!因为在后面链接的时候 start.o 要在最前面,因为 start.o 是最先要执行的文件;

ledc.bin为默认目标,目的是生成最终的可执行文件 ledc.bin, ledc.bin 依赖 start.o 和 main.o如果当前工程没有 start.o 和 main.o 的时候就会找到相应的规则去生成 start.o 和 main.o;

arm-linux-gnueabihf-ld 进行链接,链接起始地址是 0X87800000,但是这一行用到了自动变量“



^”,“




































^”的意思是所有依赖文件的集合,在这里就是 objs 这个变量的值:start.o 和 main.o。链接的时候 start.o 要链接到最前面,因为第一行代码就是 start.o 里面的,因此这一行就相当于:arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf start.o main.o;

arm-linux-gnueabihf-objcopy 来将 ledc.elf 文件转为 ledc.bin,本行也用到了自动变量“



@

@”,“






@














@”的意思是目标集合,在这里就“ledc.bin”,那么本行就相当于:arm-linux-gnueabihf-objcopy -O binary -S ledc.elf ledc.bin;

arm-linux-gnueabihf-objdump 来反汇编,生成 ledc.dis 文件;

针对不同的文件类型将其编译成对应的.o 文件,其实就是汇编.s(.S)和.c 文件,用到了自动变量“



@

@”和“






@














<”,其中“$<”的意思是依赖目标集合的第一个文件。比如start.s 要编译成start.o 的话以下两个种写法等效:

%.o : %.S
	arm-linux-gnueabihf-gcc -Wall -nostdlib -c  -O2 -o $@ $<

start.o:start.s
	arm-linux-gnueabihf-gcc -Wall -nostdlib -c -O2 -o start.o start.s



2、imx6u.lds链接脚本

前面的 Makefile 中我们链接代码的时候使用如下语句:arm-linux-gnueabihf-ld -Ttext 0X87800000 -o ledc.elf $^上面语句中我们是通过“-Ttext”来指定链接地址是 0X87800000 的,这样的话所有的文件都会链接到以 0X87800000 为起始地址的区域。但是有时候我们很多文件需要链接到指定的区域,或者叫做段里面,比如在 Linux 里面初始化函数就会放到 init 段里面。因此我们需要能够自定义一些段,这些段的起始地址我们可以自由指定,同样的我们也可以指定一个文件或者函数应该存放到哪个段里面去。要完成这个功能我们就需要使用到链接脚本,看名字就知道链接脚本主要用于链接的,用于描述文件应该如何被链接在一起形成最终的可执行文件。其主要目的是描述输入文件中的段如何被映射到输出文件中,并且控制输出文件中的内存排布。比如我们编译生成的文件一般都包含 text 段、 data 段等等。

SECTIONS{
    . = 0x87800000;
    .text :
    {
        start.o
        *(.text)
    }
    .rodata ALIGN(4) : {*(.rodata*)}
    .data ALIGN(4) : {*(.data)}
    __bss_start=.;
    .bss ALIGN(4) : {*(.bss) *(COMMON)}
    __bss_end=.;
}

设置定位计数器为0X87800000,因为我们的链接地址就是0X87800000。设置链接到开始位置的文件为start.o,因为 start.o 里面包含着第一个要执行的指令,所以一定要链接到最开始的地方。 main.o这个文件,其实可以不用写出来,因为 main.o 的位置就无所谓了,可以由编译器自行决定链接位置。“__bss_start”和“__bss_end”这两个是什么呢?“__bss_start”和“__bss_end”是符号号,”__bss_start=.;“和”__bss_end=.;”这两行其实就是对这两个符号进行赋值,其值为定位符“.”,这两个符号用来保存.bss 段的起始地址和结束地址。前面说了.bss 段是定义了但是没有被初始 化的变量,我们需要手动对.bss 段的变量清零的,因此我们需要知道.bss 段的起始和结束地址,这样我们直接对这段内存赋 0 即可完成清零。通过这两行代码 将.bss 段的起始地址和结束地址就保存在了“__bss_start”和“__bss_end”中,我们就可以直接在汇编或者 C 文件里面使用这两个符号。

链接脚本的语法很简单,就是编写一系列的命令,这些命令组成了链接脚本,每个命令是一个带有参数的关键字或者一个对符号的赋值,可以使用分号分隔命令。像文件名之类的字符串可以直接键入,也可以使用通配符“

”。最简单的链接脚本可以只包含一个命令“SECTIONS”,我们可以在这一个“SECTIONS”里面来描述输出文件的内存布局。我们一般编译出来的代码都包含在text、 data、 bss 和 rodata 这四个段内,假设现在的代码要被链接到 0X10000000 这个地址,数据要被链接到0X30000000 这个地方,下面就是完成此功能的最简单的链接脚本:

SECTIONS{


. = 0X10000000;

.text : {


(.text)}

. = 0X30000000;

.data ALIGN(4) : {

(.data) }

.bss ALIGN(4) : {

(.bss) }

}

a.先写了一个关键字“SECTIONS”,后面跟了一个大括号,这个大括号和最后一行大括号是一对,这是必须的;

b.一个特殊符号“.”进行赋值,“.”在链接脚本里面叫做定位计数器,默认的定位计数器为 0。我们要求代码链接到以 0X10000000 为起始地址的地方,因此这一行给“.”赋值 0X10000000,表示以 0X10000000 开始,后面的文件或者段都会以 0X10000000 为起始地址开始链接;

c. “.text”是段名,后面的冒号是语法要求,冒号后面的大括号里面可以填上要链接到“.text”这个段里面的所有文件,“

(.text)”中的“

”是通配符,表示所有输入文件的.text段都放到“.text”中;

d. 我们的要求是数据放到 0X30000000 开始的地方,所以我们需要重新设置定位计数器“.”,将其改为 0X30000000。如果不重新设置的话会怎么样?假设“.text”段大小为 0X10000,那么接下来的.data 段开始地址就是 0X10000000+0X10000=0X10010000,这明显不符合我们的

要求。

e. 定义了一个名为“.data”的段,然后所有文件的“.data”段都放到这里面。但是这一行多了一个“ALIGN(4)”,是用来对“.data”这个段的起始地址做字节对齐的, ALIGN(4)表示 4 字节对齐。也就是说段“.data”的起始地址要能被 4 整除,一般常见的都是 ALIGN(4)或者 ALIGN(8),也就是 4 字节或者 8 字节对齐。

f. 定义了一个“.bss”段,所有文件中的“.bss”数据都会被放到这个里面,“.bss”数据就是那些定义了但是没有被初始化的变量。



四、编译和烧写

参考


汇编实现GPIO输出实验(LED)


中的第三小节(代码编译及烧写)



版权声明:本文为weixin_40391982原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。