Linux-ARM裸机(四)-开发前工程准备

一、模仿STM32驱动开发格式

一、STM32寄存器结构体

对于STM32,使用一个结构体将一个外设的所有寄存器都放到一起,相当于将这个结构体抽象为外设,当使用的时候,直接访问结构体的各个成员,就相当于访问外设的各个寄存器。

以STM32F103电灯程序为例:

GPIOE->CRL&=0XFF0FFFFF;
GPIOE->CRL|=0X00300000; //PE5 推挽输出
GPIOE->ODR|=1<<5; //PE5 输出高

初始化 STM32 的 PE5 这个 GPIO 为推挽输出,需配置的就是 GPIOE 的寄存器 CRL 和 ODR,“GPIOE”的定义:

#define GPIOE  ((GPIO_TypeDef *) GPIOE_BASE)

GPIO_TypeDef 和 GPIOE_BASE 定义:

typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t BRR;
__IO uint32_t LCKR;
} GPIO_TypeDef;

#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define PERIPH_BASE ((uint32_t)0x40000000)

各个寄存器地址都是在GPIOE_BASE这个基地址基础上进行偏移得到。比如:32位寄存器CRL的地址是0x40011800,则寄存器CRH的地址则是0x40011804。

二、修改驱动程序

1、修改start.s汇编文件:在准备环境的汇编文件中添加清除bss段代码。

清除bss段:链接脚本中,从__bss_start开始将bss段清0一直清到__bss_end。体现在代码中就是,向r2寄存器写入一个0,然后将r2寄存器值写入到r0寄存器,每写一位,r0地址加1,一直加到r1,这样就实现了bss段清除。

.global _start
.global _bss_start
.global _bss_end

_bss_start:
	.word __bss_start   //.word表示在这里放一个变量,_bss_start就是这个变量的标签,类似变量名
	                    //类似C语言中的变量定义
_bss_end:
	.word __bss_end

_start:    /* 设置处理器进入SVC模式 */
    mrs r0,cpsr     /* 读取CPSR到R0 */
    bic r0,r0,#0x1f /* 对CPSR低五位清零 */
    orr r0,r0,#0x13 /* 使用SVC模式 */
    msr cpsr,r0     /* 将R0写入到CPSR中 */
	
	/*	清除bss段*/
	ldr r0,_bss_start    //将_bss_start数据保存到r0寄存器
	ldr r1,_bss_end      //将_bss_end数据保存到r1寄存器
	mov r2,#0    
    //使用LDR伪指令将数据加载到寄存器的时候需要在数据前面添"="前缀
    //使用MOV指令将数据加载到寄存器的时候需要在数据前面添加“#"前缀。
bss_loop:    //循环
	stmia r0!,{r2} //stmia指令是往r0寄存器的保存的存储器地址写入后面{}里的值,写完之后r0地址自增
	cmp r0,r1		/*比较R0和R1里面的值*/
	ble bss_loop	/*若r0地址小于等于r1,说明清bss段还未完成,应该跳到bss_loop继续清bss段*/
    //b指令和le指令合起来,le指令是小于等于的意思,与上面的cmp组合使用实现了类似if的效果
    /* 设置SP指针 */
    ldr sp,=0x80200000
    b main          /* 跳转到main函数中 */

__bss_start等于_bss_start的值,也就是bss段的起始地址。但是不能直接使用__bss_start。

2、仿照STM32编写寄存器结构体
  • 首先,编写外设结构体

先将同属于一个外设的所有寄存器编写到一个结构体里面,如 IO 复用寄存器组的结构体如下:结构体 IOMUX_SW_MUX_Type 就是 IO 复用寄存器组,成员变量是每个 IO 对应的复用寄存器,每个寄存器的地址是 32 位,每个成员都使用“volatile”进行了修饰,目的是防止编译器优化。

/*
* IOMUX 寄存器组
*/
typedef struct
{
    volatile unsigned int BOOT_MODE0;
    volatile unsigned int BOOT_MODE1;
    volatile unsigned int SNVS_TAMPER0;
    volatile unsigned int SNVS_TAMPER1;
    ………
    volatile unsigned int CSI_DATA00;
    volatile unsigned int CSI_DATA01;
    volatile unsigned int CSI_DATA02;
    volatile unsigned int CSI_DATA03;
    volatile unsigned int CSI_DATA04;
    volatile unsigned int CSI_DATA05;
    volatile unsigned int CSI_DATA06;
    volatile unsigned int CSI_DATA07;
    /*部分 IO 复用寄存器省略 */
}IOMUX_SW_MUX_Tpye;

根据结构体 IOMUX_SW_MUX_Type 的定义,其第一个成员变量为 BOOT_MODE0,也就是 BOOT_MODE0 这个 IO 的 IO 复用寄存器,查 I.MX6U 的参考手册可知其地址为0X020E0044,所以 IO 复用寄存器组的基地址就是 0X020E0044,定义如下:

#define     IOMUX_SW_MUX_BASE     (0X020E0044)
  • 定义访问指针

访问指针定义如下:

#define     IOMUX_SW_MUX     ((IOMUX_SW_MUX_Type *)IOMUX_SW_MUX_BASE)

通过上面三步我们可通过“IOMUX_SW_MUX->GPIO1_IO03”来访问 GPIO1_IO03 的IO 复用寄存器。同样,其他的外设寄存器都也可通过这三步来定义。之后在main函数里面可如下直接调用:

#include "imx6u.h"

/*使能外设时钟*/
void clk_enable(void)
{
    CCM->CCGR0 = 0xffffffff;			    /*将外设指针CCM指向结构体成员CCGR0进行赋值初始化*/
    CCM->CCGR1 = 0xffffffff;
    CCM->CCGR2 = 0xffffffff;
    CCM->CCGR3 = 0xffffffff;
    CCM->CCGR4 = 0xffffffff;
    CCM->CCGR5 = 0xffffffff;
    CCM->CCGR6 = 0xffffffff;
}
/*初始化led*/
void led_init(void)
{
    IOMUX_SW_MUX->GPIO1_IO03 = 0x5;        /*复用为GPIO1_IO03*/
    IOMUX_SW_PAD->GPIO1_IO03 = 0x10B0;     /*设置GPIO01_IO03电气属性*/

    GPIO1->GDIR = 0x0000008;               /*设置为输出*/
    GPIO1->DR = 0x0;                       /*打开led*/
}
/*短延时*/
void delay_short(volatile unsigned int n)
{
    while(n--){}
}
/*长延时(大概一毫秒)*/
/*n:延时毫秒数*/
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置一*/
}
/*主函数*/
int main(void)
{
    /*使能外设时钟*/
    clk_enable();
    /*初始化led*/
    led_init();
    /*设置led闪烁*/
    while(1)
    {
        led_on();
        delay(500);

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

在结构体中添加寄存器的时候一定要注意地址的连续性,如果不连续要添加占位,如下图,跳过了两个寄存器的地址,0x020c4040与0x020c4044。

Makefile编写

objs	:= start.o main.o						#定义几个变量,方便后续的编写
ld 	 	:= arm-linux-gnueabihf-ld
gcc  	:= arm-linux-gnueabihf-gcc
objcopy	:= arm-linux-gnueabihf-objcopy
objdump	:= arm-linux-gnueabihf-objdump

ledc.bin : $(objs)
	$(ld) -Timx6u.lds -o ledc.elf $^			#引用之前定义的变量进行编译链接
	$(objcopy) -O binary -S ledc.elf $@
	$(objdump) -D -m arm ledc.elf > ledc.dis

%.o : %.c
	$(gcc) -Wall -nostdlib -c -O2 -o $@ $<

%.o : %.s
	$(gcc) -Wall -nostdlib -c -O2 -o $@ $<

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

二、NXP官方SDK移植

        NXP 针对 I.MX6ULL 编写了一个 SDK 包,这个 SDK 包就类似于 STM32 的 STD 库或者HAL 库,这个 SDK 包提供了 Windows 和 Linux 两种版本,分别针对主机系统 Windows 和Linux。因为我们是在 Windows 下使用 Source Insight 来编写代码的,因此我们使用的是 Windows版本的。 Windows 版本 SDK 里面的例程提供了 IAR 版本。

        NXP 提供了 IAR版本的 SDK,那为什么不用 IAR 来完成裸机试验,而要用复杂的GCC?我们要从简单的裸机开始掌握 Linux 下的 GCC 开发方法,包括 Ubuntu 操作系统的使用、 Makefile 的编写、shell 等。若偷懒而使用 IAR 开发裸机的话,那么后续学习 Uboot 移植、 Linux 移植和 Linux 驱动开发就会很难上手,因为开发环境都不熟悉!再者,不是所有的半导体厂商都会为 Cortex-A 架构的芯片编写裸机 SDK 包,我使用过那么多的 Cotex-A 系列芯片,也就发现了 NXP 给 I.MX6ULL 编写了裸机 SDK 包。在NXP 官网发现只有 I.MX6ULL这一款 Cotex-A 内核的芯片有裸机 SDK 包, NXP 的其它 Cotex-A 芯片都没有。说明在 NXP 的定位里面, I.MX6ULL 就是一个 Cotex-A 内核的高端单片机,定位类似 ST 的 STM32H7。使用 Cortex-A 内核芯片的时候不要想着有类似 STM32 库一样的东西,I.MX6ULL只是一个特例,基本所有的 Cortex-A 内核的芯片都不会提供裸机 SDK 包。因此在使用 STM32 的时候那些用起来很顺手的库文件,在 Cotex-A 芯片下基本都需要自行编写,比如.s 启动文件、寄存器定义等。

一、新建cc.h文件

SDK包或者一些第三方库里面会用到很多数据类型,因此在cc.h里面定义一些常用的数据类型。

#ifndef __CC_H
#define __CC_H
/*
 * 自定义一些数据类型供库文件使用
 */
#define     __I     volatile 
#define     __O     volatile 
#define     __IO    volatile

typedef   signed          char int8_t;
typedef   signed short     int int16_t;
typedef   signed           int int32_t;
typedef unsigned          char uint8_t;
typedef unsigned short     int uint16_t;
typedef unsigned           int uint32_t;
typedef unsigned long     long uint64_t;
typedef	  signed char  	 	   s8;		
typedef	  signed short 	  int  s16;
typedef	  signed int 		   s32;
typedef	  signed long long int s64;
typedef	unsigned char 		   u8;
typedef	unsigned short int     u16;
typedef	unsigned int 		   u32;
typedef	unsigned long long int u64;

#endif

二、移植SDK文件

设备为MCIMX6Y2,需要移植MCIMX6Y2.h,fsl_common.h,fsl_iomuxc.h三个文件。

三、裁剪SDK文件

四、编译调试

main.c文件

#include "fsl_common.h"
#include "fsl_iomuxc.h"
#include "MCIMX6Y2.h"        //这三个文件即从NXP官方移植过来的SDK包 头文件
/*使能外设时钟*/
void clk_enable(void){
    CCM->CCGR0 =0xFFFFFFFF;
    CCM->CCGR1 =0xFFFFFFFF;
    CCM->CCGR2 =0xFFFFFFFF;
    CCM->CCGR3 =0xFFFFFFFF;
    CCM->CCGR4 =0xFFFFFFFF;
    CCM->CCGR5 =0xFFFFFFFF;
    CCM->CCGR6 =0xFFFFFFFF;
}
/*初始化LED灯*/
void led_init(void){
    IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03,0); /*复用为GPIO--IO03 */
    IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03,0x10B0);/*设置GPIO__IO03电器属性*/

    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() {
    clk_enable();  //使能外设时钟
    led_init(); //初始化LED
    while(1){
        led_off();  
        delay(1000);
        led_on();
        delay(1000);
    }
    return 0;
}

main.c 有 7 个函数,这 7 个函数的含义都一样,只是本例程使用的是移植好的 NXP 官方 SDK 里面的寄存器定义。重点看一下 led_init 函数中的下面两句代码:

IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03,0); /*复用为GPIO_IO03 */
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO03_GPIO1_IO03,0x10B0);/*设置GPIO__IO03电器属性*/

使用了两个函数IOMUXC_SetPinMuxIOMUXC_SetPinConfig,函数IOMUXC_SetPinMux 用来设置IO复用功能,根本上设置的是寄存器“IOMUXC_SW_MUX_CTL_PAD_XX”。函数 IOMUXC_SetPinConfig 设置的是 IO 的上下拉、速度等的,也就是寄存器“IOMUXC_SW_PAD_CTL_PAD_XX”,上面两个函数其实就是上一章中的:
 

IOMUX_SW_MUX->GPIO1_IO03 = 0X5;
IOMUX_SW_PAD->GPIO1_IO03 = 0X10B0;

函数 IOMUXC_SetPinMux 在文件 fsl_iomuxc.h 中定义,其源码如下:

static inline void IOMUXC_SetPinMux(uint32_t muxRegister,
                                    uint32_t muxMode,
                                    uint32_t inputRegister,
                                    uint32_t inputDaisy,
                                    uint32_t configRegister,
                                    uint32_t inputOnfield)
{
    *((volatile uint32_t *)muxRegister) =
        IOMUXC_SW_MUX_CTL_PAD_MUX_MODE(muxMode) |
            IOMUXC_SW_MUX_CTL_PAD_SION(inputOnfield);
    if (inputRegister)  
    {  
        *((volatile uint32_t *)inputRegister) =
                IOMUXC_SELECT_INPUT_DAISY(inputDaisy);
    }
}

函数 IOMUXC_SetPinMux 有 6 个参数,这 6 个参数的函数如下:
muxRegister : IO 的 复 用 寄 存 器 地 址 , 比 如 GPIO1_IO03 的 IO 复 用 寄 存 器SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068。
muxMode: IO 复用值,也就是 ALT0~ALT8,对应数字 0~8,比如要将 GPIO1_IO03 设置为 GPIO 功能的话此参数就要设置为 5。
inputRegister: 外设输入 IO 选择寄存器地址,有些 IO 在设置为其他的复用功能以后还需要设置 IO 输入寄存器,比如 GPIO1_IO03 要复用为 UART1_RX 的话还需要设置寄存器
UART1_RX_DATA_SELECT_INPUT,此寄存器地址为 0X020E0624。
inputDaisy: 寄存器 inputRegister 的值,比如 GPIO1_IO03 要作为 UART1_RX 引脚的话此参数就是 1。
configRegister:未使用,函数 IOMUXC_SetPinConfig 会使用这个寄存器。
inputOnfield : IO 软 件 输 入 使 能 , 以 GPIO1_IO03 为 例 就 是 寄 存 器
SW_MUX_CTL_PAD_GPIO1_IO03 的 SION 位(bit4)。如果需要使能 GPIO1_IO03 的软件输入功能的话此参数应该为 1,否则的话就为 0。
IOMUXC_SetPinMux 的函数体很简单,根据参数对寄存器 muxRegister 和inputRegister进行赋值。使用此函数将 GPIO1_IO03 的复用功能设置为GPIO,如下示例:
 

IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_GPIO1_IO03, 0);

IOMUXC_GPIO1_IO03_GPIO1_IO03这是个宏,在文件fsl_iomuxc.h 中有定义, NXP 的 SDK 库将一个 IO 的所有复用功能都定义了一个宏,比如GPIO1_IO03 有如下 9 个宏定义,9 个宏定义分别对应着 GPIO1_IO03 的九种复用功能。

IOMUXC_GPIO1_IO03_I2C1_SDA
IOMUXC_GPIO1_IO03_GPT1_COMPARE3
IOMUXC_GPIO1_IO03_USB_OTG2_OC
IOMUXC_GPIO1_IO03_USDHC1_CD_B
IOMUXC_GPIO1_IO03_GPIO1_IO03           
IOMUXC_GPIO1_IO03_CCM_DI0_EXT_CLK
IOMUXC_GPIO1_IO03_SRC_TESTER_ACK
IOMUXC_GPIO1_IO03_UART1_RX
IOMUXC_GPIO1_IO03_UART1_TX

如下为复用为 GPIO 的宏定义(直接定义好五个参数):

#define IOMUXC_GPIO1_IO03_GPIO1_IO03     0x020E0068U, 0x5U, 0x00000000U,
                                         0x0U, 0x020E02F4U

将这个宏拆开带入到IOMUXC_SetPinMux函数的参数中就是:

IOMUXC_SetPinMux (0x020E0068U, 0x5U, 0x00000000U, 0x0U, 0x020E02F4U, 0);

同理,当我们要将 GPIO1_IO03 复用为 I2C1_SDA 的话可以使用如下代码:

IOMUXC_SetPinMux(IOMUXC_GPIO1_IO03_I2C1_SDA, 0);

同理,函数 IOMUXC_SetPinConfig,同样在文件 fsl_iomuxc.h 中有定义,函数源码如下:

static inline void IOMUXC_SetPinConfig(uint32_t muxRegister,
                                       uint32_t muxMode,
                                       uint32_t inputRegister,
                                       uint32_t inputDaisy,
                                       uint32_t configRegister,
                                       uint32_t configValue)
{
    if (configRegister)
    {
        *((volatile uint32_t *)configRegister) = configValue;
    }    //将configValue值写入到configRegister寄存器
}

函数 IOMUXC_SetPinConfig 有 6 个参数,其中前五个参数和函数 IOMUXC_SetPinMux 一样,但此函数只使用了参数 configRegister 和 configValue,cofigRegister是 IO 配置寄存器地址,比如GPIO1_IO03的 IO 配置寄存器为IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03,
其地址为 0X020E02F4,参数 configValue 就是要写入到寄存器 configRegister 的值。同理,将这个宏展开带入到IOMUXC_SetPinConfig 函数的参数中就是如下代码所示(将寄存器0x020E02F4 的值设置为 0X10B0):

IOMUXC_SetPinConfig(0x020E0068U, 0x5U, 0x00000000U, 0x0U, 0x020E02F4U, 0X10B0);

以后就可以使用这两个函数来方便的配置 IO 的复用功能和 IO 配置。

Makefie文件(内核版本,使用到了变量)
CROSS_COMPILE  ?= arm-linux-gnueabihf-    //“?=”表示如果该变量没有被赋值,则赋予等号后的值。
NAME 		   ?= ledc

CC 			   := $(CROSS_COMPILE)gcc        //”:=”就表示直接赋值,赋予当前位置的值
LD			   := $(CROSS_COMPILE)ld
OBJCOPY		   := $(CROSS_COMPILE)objcopy
OBJDUMP		   := $(CROSS_COMPILE)objdump

OBJS           := start.o  main.o

$(NAME).bin : $(OBJS)
	$(LD) -Timx6ul.lds -o $(NAME).elf $^
	$(OBJCOPY) -O binary -S $(NAME).elf $@
	$(OBJDUMP) -D -m arm $(NAME).elf > $(NAME).dis

%.o : %.c 
	$(CC) -Wall -nostdlib -c -O2 -o $@  $<

%.o : %.s
	$(CC) -Wall -nostdlib -c -O2 -o $@ $<

clean:
	rm -rf *.o $(NAME).elf $(NAME).bin $(NAME).dis
链接脚本
SECTIONS
{
    . = 0x87800000;
    .text :
    {
        start.o
        *(.text)
    }
    .rodata ALIGN(4) : {*(.rodata*)}
    .data ALIGN(4) : {*(.data)}
    __bss_start = .;
    .bss ALIGN(4) : { *(.bss) *(COMMON)}
    __bss_end = .;
}

三、BSP工程管理

工程目录结构

        在之前,我们都是将所有的源码文件放到工程的根目录下,但在工程文件多了之后这样会使工程看起来较为混乱,我们需要进行工程管理:根据各文件的属性,将它们放在各自的目录里,这种方法即可称为BSP的工程管理。

如上图工程目录结构:

        其中 bsp 用来存放驱动文件,我们前面的实验中所有的驱动相关的函数都写到了 main.c 文件中,比如函数 clk_enable、 led_init 和 delay,这三个函数为:时钟驱动、 LED 驱动和延时驱动。我们可在 bsp 文件夹下创建三个子文件夹: clk、delay 和 led,分别用来存放时钟驱动文件、 延时驱动文件和 LED 驱动文件;

        imx6ul 用来存放跟芯片有关的文件,比如 NXP 官方的 SDK库文件,将之前移植的 cc.h、 fsl_common.h、 fsl_iomuxc.h 和 MCIMX6Y2.h 这四个文件拷贝到文件夹 imx6ul 中;

        obj 用来存放编译生成的.o 文件。

        project 存放 start.S 和 main.c 文件,也就是应用文件。

        这样程序功能模块会比较清晰。工程文件夹创建好后就要编写代码,其实就是将时钟、 LED和延时驱动相关函数从 main.c 中提取出来做成一个独立的驱动文件 。

设置Vscode头文件路径。

先创建.vscode目录,然后打开C/C++配置器(Ctrl+shift+P,然后搜索:C/C++Edit),会在.vscode目录下生成一个叫做c_cpp_properties.json的文件,如下:

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",        
                "${workspaceFolder}/bsp/clk",          //添加保存有.h头文件的文件夹路径
                "${workspaceFolder}/bsp/led",
                "${workspaceFolder}/bsp/delay",
                "${workspaceFolder}/imx6ul",
                "${workspaceFolder}/project"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c11",
            "cppStandard": "c++98",
            "intelliSenseMode": "linux-gcc-x64",
            "configurationProvider": "ms-vscode.makefile-tools"
        }
    ],
    "version": 4
}
编写通用Makefile

由于进行工程分类管理,之前较为简单的Makefile不能继续用,需要编写一个新的通用Makefile。

Makefile指定头文件路径,需要-I。我们编译源码的时候需要指定头文件路径。比如:bsp/clk/bsp_clk.h变为 -I bsp/clk/bsp_clk.h。

首先通过一堆变量将要编译的原材料准备好。

Makefile中的模式字符串替换函数——patsubst

格式:$(patsubst  <pattern>,<replacement>,<text> ) 

功能:查找<text>中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。这里,<pattern>可以包括通配符“%”,表示任意长度的字串。如果<replacement>中也包含“%”,那么,<replacement>中的这个“%”将是<pattern>中的那个“%”所代表的字串。

返回:函数返回被替换过后的字符串。

示例:$(patsubst  %.c,%.o, a.c b.c)

把字串“a.c b.c”符合模式[%.c]的单词替换成[%.o],返回结果是“a.o b.o”

Makefile中的循环函数——foreach。

功能:用来循环处理文件列表,列出符合条件的文件目录名。Makefile 中的 foreach 函数几乎是仿照于 Unix 标准 Shell(/bin /sh)中的 for 语句,或是 C-Shell(/bin/csh)中的 foreach 语句而构建的。

格式:$(foreach   <var>, <list>, <text>)

(1) var:临时变量;

(2) list:以空格隔开的文件列表,每一次取一个文件名单词赋值为变量 var;

(3) text:对 var 变量进行操作(一般使用 var 变量,不然没意义),每次操作结果都会以空格隔开,最后返回空格隔开的列表。

函数解释:把参数 <list> 中的单词逐一取出放到参数 <var> 所指定的变量中,然后再执行 <text> 所包含的表达式。每一次 <text> 会返回一个字符串,循环过程中,<text> 表达式所返回的每个字符串会以空格分隔。

返回值:最后当整个循环结束时,<text> 所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函数的返回值。

<var> 最好是一个变量名,<list> 可以是一个表达式,而 <text> 中一般会使用 <var> 这个参数来依次枚举 <list> 中的单词。

Makefile中的wildcard函数

原型(格式):$(wildcard <PATTERN...>)
wildcard函数是针对通配符在函数或变量定义中展开无效情况下使用的,用于获取匹配该模式下的所有文件列表让通配符在变量中展开。<PATTERN...>参数若有多个则用空格分隔。若没有找到指定的匹配模式则返回为空。

通用Makefile,后面例程也会一直用此Makefile。

1  CROSS_COMPILE     ?= arm-linux-gnueabihf-
2  TARGET            ?= bsp                    //不同例程改一下目标名称
3
4  CC                := $(CROSS_COMPILE)gcc
5  LD                := $(CROSS_COMPILE)ld
6  OBJCOPY           := $(CROSS_COMPILE)objcopy
7  OBJDUMP           := $(CROSS_COMPILE)objdump
8 
9  INCDIRS           := imx6ul \                        //添加头文件路径
10                      bsp/clk \
11                      bsp/led \
12                      bsp/delay
13
14 SRCDIRS           := project \                       //添加源文件路径
15                      bsp/clk \
16                      bsp/led \
17                      bsp/delay
18
19 INCLUDE           := $(patsubst  %, -I %, $(INCDIRS))
20                    //%表示变量INCDIRS里面的字符串全部替换为 -I+空格+字符串名
21 SFILES            := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.S))
22 CFILES            := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))
23    //SFILES保存工程下所有汇编.s文件,CFILES保存所有.c文件
24 SFILENDIR         := $(notdir $(SFILES))
25 CFILENDIR         := $(notdir $(CFILES))
26
27 SOBJS             := $(patsubst %, obj/%, $(SFILENDIR:.s=.o))
28 COBJS             := $(patsubst %, obj/%, $(CFILENDIR:.c=.o))
29 OBJS              := $(SOBJS) $(COBJS)
30
31 VPATH             := $(SRCDIRS)
32
33 .PHONY: clean
34
35 $(TARGET).bin : $(OBJS)
36     $(LD) -Timx6ul.lds -o $(TARGET).elf $^
37     $(OBJCOPY) -O binary -S $(TARGET).elf $@
38     $(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis
39
40 $(SOBJS) : obj/%.o : %.S
41     $(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
42
43 $(COBJS) : obj/%.o : %.c
44     $(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
45
46 clean:
47     rm -rf $(TARGET).elf $(TARGET).dis $(TARGET).bin $(COBJS) $(SOBJS)

代码分析:

第 1~7 行定义了一些变量,除第 2 行外其它的都是跟编译器有关的,如果使用其它编译器的话只需要修改第 1 行即可。第 2 行的变量 TARGET 是目标名字,不同例程肯定名字不同。

第 9 行的变量 INCDIRS 包含整个工程的.h 头文件目录,文件中的所有头文件目录都要添加到变量INCDIRS中。比如本例程中包含.h头文件的目录有imx6ul、bsp/clk、bsp/delay和bsp/led,所以需要在变量 INCDIRS 中添加这些目录,即:

INCDIRS         := imx6ul \
                   bsp/clk \
                   bsp/led \
                   bsp/delay 

在这里有一个疑问,main.h所在的projec目录为什么不用包含到INCDIRS里面呢?

9~11 行后面都会有一个符号“\”,这个相当于“换行符”,表示本行和下一行属于同一行,一般一行写不下的时候就用符号“\”来换行。在后面的其它裸机例程中我们会根据实际情况来在变量 INCDIRS 中添加头文件目录。

第 14 行是变量 SRCDIRS,和变量 INCDIRS 一样,只是 SRCDIRS 包含的是整个工程的所有.c 和.s 文件目录。比如本例程包含有.c 和.s 的目录有 bsp/clk、bsp/delay、bsp/led 和 project,即:

SRCDIRS := project \
           bsp/clk \
           bsp/led \
           bsp/delay

同样的,后面的裸机例程中我们也要根据实际情况在变量 SRCDIRS 中添加相应的文件目录。

第 19 行的变量 INCLUDE 使用到了函数 patsubst,通过函数 patsubst 给变量 INCDIRS 添加一个“-I”,即:

INCLUDE         := -I imx6ul -I bsp/clk -I bsp/led -I bsp/delay

加“-I”的目的是因为 Makefile 语法要求指明头文件目录的时候需要加上“-I”

第 21 行变量 SFILES 保存工程中所有的.s 汇编文件(包含绝对路径),变量 SRCDIRS 已经存放了工程中所有的.c 和.S 文件,所以我们只需要从里面挑出所有的.S 汇编文件即可,这里借助函数 foreach 和函数 wildcard,最终 SFILES 如下:

SFILES             := project/start.s

第 22 行变量 CFILES 和变量 SFILES 一样,只是 CFILES 保存工程中所有的.c 文件(包含绝对路径),最终 CFILES 如下:

CFILES = project/main.c bsp/clk/bsp_clk.c bsp/led/bsp_led.c bsp/delay/bsp_delay.c

$(wildcard $(dir)/*.s))的意思是:获取dir下保存的所有的.s文件

SFILES   := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.s))的意思就是,在SRCDIRS中的各个目录下寻找.s文件然后一个个放到dir里面,然后执行$(wildcard $(dir)/*.s))。放一个执行一次$(wildcard $(dir)/*.s)),直到取到所有的.s文件,全都保存到SFILES变量里面。同理后面的CFILES中就存放所有的.c文件。

第 24 和 25 行的变量 SFILENDIR 和 CFILENDIR 包含所有的.S 汇编文件和.c 文件,相比变量 SFILES 和 CFILES, SFILENDIR 和 CFILNDIR 只是文件名,不包含文件的绝对路径。使用函数 notdir 将 SFILES 和 CFILES 中的路径去掉即可, SFILENDIR 和 CFILENDIR 如下:

SFILENDIR = start.S
CFILENDIR = main.c bsp_clk.c bsp_led.c bsp_delay.c

SFILENDIR    := $(notdir $(SFILES))          CFILENDIR    := $(notdir $(CFILES))

上面两句代码使用notdir将SFILES与CFILES中的.s文件与.c文件前面的路径去掉,只留下文件名,然后保存到变量SFILENDIR和CFILENDIR中。

第 27 和 28 行的变量 SOBJS 和 COBJS 是.s 和.c 文件编译以后对应的.o 文件目录,默认所
有的文件编译出来的.o 文件和源文件在obj文件夹下,第 29 行变量 OBJS 是变量 SOBJS 和 COBJS 的集合。

SOBJS = obj/start.o
COBJS = obj/main.o obj/bsp_clk.o obj/bsp_led.o obj/bsp_delay.o
OBJS = obj/start.o obj/main.o obj/bsp_clk.o obj/bsp_led.o obj/bsp_delay.o

第27行中,SOBJS      := $(patsubst %, obj/%, $(SFILENDIR:.s=.o)) ,$(SFILENDIR:.s=.o)表示将SFILENDIR下面的.s变为.o。

也可以写成$(patsubst  %.s, obj/%.o, $(SFILENDIR))

第 31 行的 VPATH 是指定搜索目录的,这里指定的搜素目录就是变量 SRCDIRS 所保存的目录,这样当编译的时候所需的.s 和.c 文件就会在 SRCDIRS 中指定的目录中查找。

VPATH     := $(SRCDIRS)    //源码路径

第 33 行使用.PHONY指定了一个伪目标 clean,伪目标前面讲解 Makefile 的时候已经讲解过了。

第40行写法叫做静态模式,静态模式可以更加容易地定义多目标的规则。

$(SOBJS) : obj/%.o : %.s

依赖所有的.s文件生成的.o文件放到obj/目录下面去。

$(INCLUDE)是Makefile 语法要求,相比于之前的Makefile增加了$(INCLUDE)。

$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $<
通用链接脚本

后面例程也会一直用此链接脚本,链接脚本 imx6ul.lds 的内容基本和之前一样, 仅仅设置了一下start.o 文件路径,start.o放到了obj目录下,因此本章所使用的 imx6ul.lds 链接脚本如下:

1 SECTIONS{
2       . = 0X87800000;
3   .text :
4   {
5       obj/start.o
6       *(.text)
7   }
8   .rodata ALIGN(4) : {*(.rodata*)}
9   .data ALIGN(4) : { *(.data) }
10  __bss_start = .;
11  .bss ALIGN(4) : { *(.bss) *(COMMON) }
12  __bss_end = .;
13  }

本篇博客记录了实际项目,驱动开发前期的一些工作,为了便于理解,从模拟实现STM32驱动开发引入,之后进行了一个NXP官方SDK库的裁剪移植(没有深入讲裁剪原因和具体步骤,后面会讲。PS:听已经工作的师兄说公司里面都是用自己开发的SDK库)。再后面记录了BSP工程管理,相当于工程中的各类文件进行分类。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值