目录

linux-STM32F开发㈠-makefile 构建与使用

一、前言

在 Windows 下有完善的 IDE(集成开发环境)开发工具,开发单片机程序变得简单和傻瓜化,图形界面上所见所得操作,基本不需要我们了解一些专业知识。正是 IDE 太过完善了,导致我们忽略一些技术知识,限制我们技术的提升。如果想进一步提升专业知识,必须要学会在 linux 下开发,本篇文章主要介绍 STM32F 项目工程使用 Makefile 构建、配置、编译。
如果你不熟悉 linux 和 Makefile,建议认真学习下面文章
linux-系统-Ubuntu 系统与工具
linux-命令-linux 基本命令使用
linux-编译-linux 编译构建工具

二、构建

1、使用【STM32CubeMX】构建操作

● 使用【STM32CubeMX】构建一个 demo 工程,在《STM32CubeMX 基本使用》中的第三节【STM32CubeMX 使用】已非常详细介绍了,这里不再赘述!不同的是最后一步设置生成工程时选择【Makefile】选项,如图:

../img/20220803_01_01.jpg


2、使用自己的【Makefile】构建操作

● 使用自己的 Makefile 构建(本人有自己风格的 Makefile 模板,并且可以对特定模块定制编译参数)。首先参照《STM32CubeMX 基本使用》中的第三节【STM32CubeMX 使用】方法构建一个基本工程,然后使用本人风格 Makefile 模板嵌入工程。至于具体如何编写 Makefile 请参考《linux-编译-linux 编译构建工具》中的【三、make+Makefile 脚本】章节。本节 demo 工程请到仓库中的【4_stm32f1xx】直接下载。将 Makefile 模板几个文件嵌入到软件工程操作方法,如图:

../img/20220803_01_02.jpg

补充:嵌入式处理器需要专用的交叉编译器和编译参数,会多一个启动文件(汇编)和 一个链接器文件(脚本),生成依赖信息的方法也不同,这些主要涉及 [makeenvi.mk] 和 [makecore.mk] 两个文件,具体请查阅仓库里的【4_stm32f1xx】demo 工程。至于具体参数表示何意,以及如何加入源文件、如何修改编译参数,下面章节将会详细介绍。同时建议你使用代码比较工具比较【4_stm32f1xx】与【3_large】这两个工程中的makeenvi.mk makecore.mk及三个Makefile文件的差异,直观了解 PC 纯软件 Makefile 与 嵌入式 Makefile 的差异!


3、关于【STM32F】嵌入式构建特点

一、相比 PC 纯软件工程构建,嵌入式工程构建会多一个启动文件(汇编)和 一个链接器文件(脚本),并且不同处理器需要使用不同启动文件及链接脚本文件(一般都是厂家提供的)。

文件 说明
startup_stm32f103xb.s 处理器启动文件(汇编)
STM32F103XB_FLASH.ld 链接器链接文件(脚本)

备注:这两个文件来源: HAL库\CMSIS\Device\ST\STM32F1xx\Source\Templates\gcc\。关于【HAL 库】请到【STM32CubeMX】库文件管理目录里提取,一般路径为:C:\Users\Administrator\STM32Cube\Repository\

二、嵌入式编译器主要生成的文件:

文件 说明
.elf 可执行与可链接格式文件(★业界标准文件★),包含了全部的编译链接信息和程序执行数据
.lst 是使用 objdump 反汇编 elf 文件得到的输出文件,它拥有比 map 文件更详细的信息
.map 源代码被工具链构建之后的详细信息,包括固件大小、函数符号、内存映射等
.hex 基于文本描述的 Intel 标准的十六进制数据,用于烧录固件
.bin 纯二进制数据,用于烧录固件

备注:从存储数据的信息量上看:ELF>AXF>HEX>BIN,所以可以将大信息量的文件格式向小信息量的文件格式转换。如:ELF 可以转换为 AXF、HEX、BIN。其中 HEX 文件可转换为 BIN 文件;如果指定了数据起始地址,也可以将 BIN 转换为 HEX 文件。

../img/20220803_01_03.jpg
../img/20220803_01_04.jpg

三、嵌入式编译器有自己独特的选项参数:

选项
说明
-mcpu=cortex-m3 编译/链接:处理器内核架构
-mthumb 编译/链接:指令集架构
-mfpu=fpv4-sp-d16 编译/链接:浮点运算单元(F4系列才有)
-Wa,-a,-ad,-alms=xxx.lst 编译:生成 lst 文件,它拥有比 map 文件更详细的信息(-Wa,表示将编译器参数传给汇编器)
-MMD -MP -MF"xxx.d" 编译:生成 d 文件,它是源文件包含文件的依赖信息
-Dxxx 编译:加入工程全局宏定义,可多个 -Dxxx 全局宏定义
-g -gdwarf-2 编译:生成 gdb 调试信息 格式为[dward-2]
-fdata-sections 编译:对每个数据创建一个 section(section 是 GCC 的最小链接单元)
-ffunction-sections 编译:对每个函数创建一个 section(section 是 GCC 的最小链接单元)
-Wl,--gc-sections 链接:特别不链接未使用的 section(函数/数据),从而减小执行文件大小(-Wl,表示将编译器参数传给链接器)
-Wl,-print-gc-sections 链接:打印链接器优化掉的 section(函数/数据),方便程序员查优化问题(-Wl,表示将编译器参数传给链接器)
-Wl,-Map=xxx.map,--cref 链接:打印链接表信息到[xxx.map]文件,--cref 表示输出一个交叉引用表(-Wl,表示将编译器参数传给链接器)
-Txxx.ld 链接:使用[xxx.ld]脚本文件作为链接器脚本
-specs=nano.specs 链接:替换精简 C 库以缩小代码大小
-lc -lm -lnosys 链接:标准C库(C lib)、数学库(math)、nosys 库

参数示范如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 链接
build/zzz.elf: build/xxx.o build/yyy.o build/bbb.a
	arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb \
                      -T./STM32F103CBTx_FLASH.ld \
                      -specs=nano.specs \
                      -Wl,--gc-sections \
                      -Wl,-Map=build/zzz.map,--cref \
                      -o build/zzz.elf \
                      build/xxx.o build/yyy.o build/bbb.a -lc -lm -lnosys
# 编译-源文件(多了 lst 生成)
%.o: %.c
	arm-none-eabi-gcc -c -mcpu=cortex-m3 -mthumb \
                      -g -gdwarf-2 -Wall -Og -fdata-sections -ffunction-sections \
                      -DUSE_HAL_DRIVER -DSTM32F103xB -I./inc/ \
                      -MMD -MP -MF"build/xxx.d" \
                      -Wa,-a,-ad,-alms=build/xxx.lst \
                      xxxx/xxx.c -o build/xxx.o
# 编译-汇编文件(少了 lst 生成)
%.o: %.s
	arm-none-eabi-gcc -x assembler-with-cpp \
                      -c -mcpu=cortex-m3 -mthumb \
                      -g -gdwarf-2 -Wall -Og -fdata-sections -ffunction-sections \
                      -DUSE_HAL_DRIVER -DSTM32F103xB -I./inc/ \
                      -MMD -MP -MF"build/yyy.d" \
                      ./yyy.s -o build/yyy.o

备注:这些参数主要涉及 [makeenvi.mk] 和 [makecore.mk] 两个文件,具体请查看【4_stm32f1xx】demo 工程。

三、使用

1、基于【STM32CubeMX】构建的使用

一、在实际的工程应用中,必定要加入自己的源代码文件和配置相关参数。下面为通过与 Keil MDK 的比较,让你快速了解 Makefile 的相关配置,如图:

../img/20220803_02_11.jpg
../img/20220803_02_12.jpg
../img/20220803_02_13.jpg

二、使用 Makefile 编译工程:

1
2
3
4
5
6
####################################################
# 在[shell]中进入[Makefile]所在的目录,输入命令执行:
####################################################
cd x_stm32f1xx # 进入操作目录
make all       # 执行编译操作(如果想重新全编译,先执行下面命令)
make clean     # 执行清除操作(清除所有编译出的文件,包括 hex 等)

2、基于自己的【Makefile】构建的使用

一、在实际的工程应用中,必定要加入自己的源代码文件和配置相关参数。下面为通过与 Keil MDK 的比较,让你快速了解 Makefile 的相关配置,如图:

../img/20220803_02_21.jpg
../img/20220803_02_22.jpg
../img/20220803_02_23.jpg
../img/20220803_02_24.jpg

备注:以上主要涉及一个父 Makefile(配置工程及子 Makefile 文件所在目录),一个子 Makefile(统一设置编译文件、包含路径等),n 个子 Makefile(定制模块编译文件、编译参数等)。

二、使用 Makefile 编译工程:

1
2
3
4
5
6
####################################################
# 在[shell]中进入[makecore.mk]所在的目录,输入命令执行:
####################################################
cd 4_stm32f1xx    # 进入操作目录
make clean        # 执行清除操作(同时创建所需文件夹)
make all          # 执行编译操作(清除操作之后会重新全编译)

四、提升

1、各种编译器的识别宏

1.1、用于在程序里识别不同编译器,通过这些宏可以使用编译器各自特性来编译相关代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 1、MDK-ARM 使用编译器的宏名称(ARM RealView)
#if   defined(__CC_ARM) || defined(__CLANG_ARM)

// 2、IAR-ARM 使用编译器的宏名称(IAR EWARM)
#elif defined(__ICCARM__)

// 3、GNU-gcc 使用编译器的宏名称(GNU Compiler Collection)
#elif defined(__GNUC__)

#endif

1.2、ARM 主流编译器:armcc、clang、iccarm、gcc 等,具体请参考下面网文:

ARM 主流编译器介绍


2、gcc 链接脚本基本知识

2.1、默认链接脚本的导出:
gcc 编译器都会有默认的链接脚本,当你需要定制链接脚本时(编译时指定链接脚本的选项参数为-T,用法如:-Txxx.ld。在嵌入式应用领域,厂家一般会提供针对其处理器定制的链接脚本,无需我们自己重新编写!),通过命令可以直接导出默认链接脚本:

1
2
3
4
5
6
7
8
9
# 1、PC 的 gcc 默认链接脚本导出方法:
ld --verbose > my_pc.ld

# 2、ARM 的 gcc 默认链接脚本导出方法:
arm-none-eabi-ld --verbose > my_arm.ld

# 特别备注:
# 1)导出的默认链接脚本中,“=====”及前面的文字只是说明信息,首先要把它们删除!
# 2)如果你想保留这些说明信息,可以使用 /**/ 注释符把它们注释掉。

2.2、链接脚本常见关键字:
gcc 编译器的链接脚本常用关键字及语法需要我们有所了解,特别是在嵌入式应用领域,需要配置 ROM(FLASH)、RAM 的大小,上电运行第一段代码(函数)等,这些都是通过链接脚本来实现的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
############################################
# 1、`ENTRY`定义上电运行的第一段代码(函数)
############################################
ENTRY(Reset_Handler)   /* 表示上电运行的第一段代码(函数): Reset_Handler */

############################################
# 2、`MEMORY`定义储存空间(起始地址及大小)
############################################
MEMORY
{
  RAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 20K   /* x:执行,r:可读,w:可写 */
  FLASH (rx) : ORIGIN = 0x8000000,  LENGTH = 128K  /* ORIGIN:起始地址,LENGTH:大小 */
}

############################################
# 3、`SECTIONS`定义一些段的链接分布,例如:
# text、data、bss 等段。
############################################
SECTIONS
{
  .text :
  {
    . = ALIGN(4);
    *(.text)
    *(.text*)
  } >FLASH

  .data : 
  {
    ……
  } >RAM AT> FLASH

  .bss :
  {
    _sbss = .;         /* bss 段开始地址 */
    ……
    _ebss = .;         /* bss 段结束地址 */
  } >RAM

  ……
}

############################################
# 4、`.`表示当前地址值
############################################
. = ALIGN(4);                   /* 表示将当前开始地址强制 4 字节对齐 */
_sdata = .;                     /* 表示将当前地址值传给[_sdata]变量 */

############################################
# 5、`KEEP`防止段(sections)的内容不被优化掉,
# 因为 -Wl,--gc-sections 链接参数可能会强制
# 优化掉__attribute__((section("xxx")) 定义
# 的 xxx 数据段!
############################################
__fsymtab_start = .;
KEEP(*(FSymTab))                /* 国产 rt-thread 实时操作系统中为其 shell 定义一段专用只读数据段 */
__fsymtab_end = .;
__hard_init_fn_start = .;
KEEP(*(SORT(.hard_init_fn.*)))  /* 本人做的专用硬件初始化的分段列表,SORT()表示对小分段进行递增排序 */
__hard_init_fn_end = .;

##############################################################################
# 6、LMA (load memory address):   加载地址,也就是所有程序和数据储存空间位置。
#   VMA (vortual memory address):执行地址,如将 Flash 数据加载至 RAM 上运行。
#   对于单片机应用,一般不用设置 VMA,也就是说“载入地址”与“运行地址”是同一地址上!
#    也就是说单片机程序是在 flash 里运行,则运行地址和加载地址是相同的。
##############################################################################

##############################################################################
# 7、输入段、输出段
# 输出段:是指生成的文件,例如 elf 中的每个段。
# 输入段:是指提供链接的所有目标文件(OBJ)中的段。
##############################################################################

更多知识请参考网文:《gcc ld 链接脚语法简明讲解》和《Linker Script 链接脚本说明


3、关于 text、data、bss、heap、stack 的分布

3.1、数据段大小含义:

类型
说明
text 代码(Code)和常量(RO-Data)的大小(ROM)
data 已初始化的全局变量(global)和静态变量(static)的大小(RAM/ROM)
bss 未初始化的全局变量(global)和静态变量(static)的大小(RAM)。 其初始值一般默认默认为零!从 STM32F103 官方的 .ld 链接文件生成的 .map 文件查到,其包括:heap(堆)和 stack (栈)的大小!
dec text + data + bss 的总和值(十进制表示)
hex text + data + bss 的总和值(十六进制表示)
补充 1、程序固件大小(ROM):text + data
2、程序已用内存(RAM):data + bss(包括:heap 和 stack 的大小)
RAM 堆栈
地址分布
说明
(1)堆区(heap) 在中地址 一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。分配方式类似于数据结构中的链表。通过malloc函数申请,通过free函数释放!堆:向高地址扩展!
(2)栈区(stack) 在高地址 由编译器自动分配和释放,存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈。函数调用及函数退出时自动处理!栈:向低地址扩展!

3.2、各段数据段分布:

../img/20220804_4_33.jpg

3.3、数据段大小获取:

1
2
3
4
5
6
7
8
// 在 C 语言中,通过如下方式获取某个分段
// 的起始与结束地址,再由计算可得出大小。
// 具体变量名称在链接脚本中找出!!!!!
extern int _sbss;
extern int _ebss;
#define LINKER_VAR_ZI_START ((void *)&_sbss)
#define LINKER_VAR_ZI_LIMIT ((void *)&_ebss)
#define LINKER_VAR_ZI_SIZE  (((void *)&_ebss) - ((void *)&_sbss))

3.4、数据段空间不足:

1
2
3
# 当数据段空间不足时,编译时一般有如下错误信息:
xxxx.elf section '.xxx' will not fit in region 'FLASH'
# 表示'FLASH'空间不足,装不下'.xxx'分段数据!

4、关于 Keil 获取 ROM、RAM 编译大小的方法

获取 ARM 编译后的 ROM 及 RAM 大小方法及原理


5、使用自己的【Makefile】构建 rt-thread 工程

5.1、基本构建与使用:

从【4_stm32f1xx】提取 makeenvi.mk、makecore.mk、Makefile、startup_stm32f103xe.s、STM32F103XE_FLASH.ld 文件,按照上面两章节教程,完成构建、加入编译文件等操作(备注:最终搭建并整理的完整工程请到仓库中【5_rt-thread】直接下载)。同时在父 Makefile 文件根据【STM32F103 正点原子战舰V3开发板】keil MDK 工程配置参数加入 rt-thread 定义的全局宏__RTTHREAD__等:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
################################
# 因 rt-thread 使用了几个全局宏,
# 需要将它加入变量中:
################################
# 添加全局宏定义,作用于整个项目工程【多个用空格分隔】
# STM32F103芯片选择: STM32F103x6,STM32F103xB,STM32F103xE,STM32F103xG
export G_DEF  = STM32F103xE                        \
                USE_HAL_DRIVER                     \
                RT_USING_LIBC                      \
                RT_USING_ARM_LIBC                  \
                __STDC_LIMIT_MACROS                \
                __CLK_TCK=RT_TICK_PER_SECOND       \
                __RTTHREAD__

5.2、修改启动文件:

《startup_stm32f103xe.s》

1
2
3
4
5
6
7
8
9
################################
# 因 rt-thread 在 gcc 编译环境下,
# 运行的第一个应用函数为 entry(),
# 所以需要修改启动文件:
################################
#将
bl main
#改为
bl entry

5.3、修改链接脚本:

《STM32F103XE_FLASH.ld》

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
################################
# 1、根据实际修改 FLASH、RAM、堆、
# 栈的大小,例如:
################################
_estack = 0x2000FFFF;    /* end of RAM */               /* RAM   的结束地址  */
_Min_Heap_Size = 0x8;    /* required amount of heap  */ /* heap (堆)的大小。★特别说明:被 rt-thread 动态内存模块取代,所以这里设置为 8 字节 */
_Min_Stack_Size = 0x400; /* required amount of stack */ /* stack(栈)的大小 */
FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 512K    /* FLASH 的起始地址及大小 */
RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 64K     /* RAM   的起始地址及大小 */

################################
# 2、为 rt-thread 加入其定义的段:
################################
  .text :
  {
    . = ALIGN(4);
    *(.text)           /* .text sections (code) */
    *(.text*)          /* .text* sections (code) */
    *(.glue_7)         /* glue arm to thumb code */
    *(.glue_7t)        /* glue thumb to arm code */
    *(.eh_frame)

    KEEP (*(.init))
    KEEP (*(.fini))

    /* rt-thread 定义的 section(段)>>>>> */
    /* section information for finsh shell */
    . = ALIGN(4);
    __fsymtab_start = .;
    KEEP(*(FSymTab))
    __fsymtab_end = .;

    . = ALIGN(4);
    __vsymtab_start = .;
    KEEP(*(VSymTab))
    __vsymtab_end = .;

    /* section information for utest */
    . = ALIGN(4);
    __rt_utest_tc_tab_start = .;
    KEEP(*(UtestTcTab))
    __rt_utest_tc_tab_end = .;

    /* section information for at server */
    . = ALIGN(4);
    __rtatcmdtab_start = .;
    KEEP(*(RtAtCmdTab))
    __rtatcmdtab_end = .;

    /* section information for initial. */
    . = ALIGN(4);
    __rt_init_start = .;
    KEEP(*(SORT(.rti_fn*)))
    __rt_init_end = .;
    /* rt-thread 定义的 section(段)<<<<< */

    . = ALIGN(4);
    _etext = .;        /* define a global symbols at end of code */
  } >FLASH

################################
# 3、为 stack 加入结束地址变量:
################################
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
    _estack = .;         /* 加入 stack 结束地址变量 */
    __bss_end = _estack; /* 加入 bss   结束地址变量 */
  } >RAM

5.4、修改动态内存:

《board.h》

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
################################
# 1、根据实际修改 RAM 的大小:
################################
#define STM32_SRAM_SIZE      64

################################
# 2、修改动态内存的起始地址:
################################
extern int __bss_end;
#define HEAP_BEGIN      ((void *)&__bss_end)

5.5、编译问题说明:

使用 arm-none-eabi-gcc 10.3 编译时出现编译器与 rt-thread V4.0.3 源码有几个宏名称相同的错误(目前解决方法:使用 arm-none-eabi-gcc 5.4 旧版本编译!):

1
2
3
error: redefinition of 'union sigval'
error: redefinition of 'struct sigevent'
error: conflicting types for 'siginfo_t'

6、使用 rt-thread 的【EVN】工具构建 Makefile

1、在【STM32F103 正点原子战舰V3开发板】工程根目录 stm32f103-atk-warshipv3 下右键点击弹出 ConEmu Here 菜单进入【EVN】控制台;
2、输入命令scons --target=makefile生成 Makefile 即可完成构建;
3、rt-thread 编写的 Makefile 主要涉及下面几个文件:

文件
说明
tools\rtthread.mk Makefile 核心代码:目标、依赖、编译命令
bsp\stm32\stm32f103-atk-warshipv3\Makefile Makefile 入口文件,主要编译入口
bsp\stm32\stm32f103-atk-warshipv3\config.mk Makefile 工程参数:主路径、编译参数、头文件包含路径、全局宏定义
bsp\stm32\stm32f103-atk-warshipv3\src.mk Makefile 编译文件。
bsp\stm32\stm32f103-atk-warshipv3\board\linker_scripts\link.lds 链接脚本
bsp\stm32\libraries\STM32F1xx_HAL\CMSIS\Device\ST\STM32F1xx\Source\Templates\gcc\startup_stm32f103xe.s 启动文件

补充说明:【EVN】工具支持多种工程构建:mdk, ses, cb, vs2012, cdk, vs, makefile, vsc, mdk4, mdk5, eclipse, ua, iar。关于查看【EVN】工具支持哪些工程构建的技巧,命令输入一个非法的构建目标,例如:scons --target=aaa,【EVN】就会提示错误并指出支持哪些工程构建。关于【rtthread.mk】脚本核心代码有个缺陷,就是没有生成源码文件的包含文件的依赖关系信息,当修改其某个头文件时,不会重新编译这个源码文件!

五、插曲

1、Windows 系统下编译出错
● 差异表现:
在 linux 编译成功,但在 Windows 编译出错!
● 运行环境:
在 Windows 系统 +【TDM-GCC】的 make 工具(注:make 命令名为mingw32-make)+【arm-none-eabi-gcc】交叉编译器 +【git】命令窗口(其集成 linux 基本命令工具)运行 Makefile。
● 出现问题:
问题一
opening dependency file D:D:/Program Files (x86)/Git/Downloads/tmp/4_stm32f1xx/build/delay.d: Invalid argument错误,一个很奇怪的问题,从提示信息中可以看到 git 的路径硬生生插入到依赖文件的路径中!经过查找,发现是由-MMD -MP -MF"$(@:%.o=%.d)"参数引起。这组参数是我参考【STM32CubeMX】生成的 Makefile 加入的,它是指在编译过程中同时生成依赖信息文件。我写的 Makefile 操作路径都使用了【绝对路径】,当我把这个参数强行改为【相对路径】-MMD -MP -MF"../../build/$(notdir $(@:%.o=%.d))"进行测试,竟然编译成功了。在 Windows 下-MF为什么不支持【绝对路径】,本人还没找出原因!
◆ 解决方法一:
按老方法使用-MM参数生成依赖信息文件,避开使用-MF引发问题!但因每编译一个文件会多运行一次 gcc 和调用一次 sed 处理文本,效率会低很多!
◆ 解决方法二:
调整 Makefile 机制,全部改为使用【相对路径】,于 2022-08-10 已更新全部 demo 工程!
问题二
../user/key/key.c:12:99: fatal error: app_cfg.h: No such file or directory错误。其实在此子 Makefile 的编译指令已加-I ./指出包含头文件所在目录,但gcc编译还是找不到头文件,不明白原因!
◆ 解决方法:
在此子 Makefile 所在目录-I ./基础上再增加指出上级目录名称,例如上级目录名为《applications》,则增加-I ../applications