一、前言
在 Windows 下有完善的 IDE(集成开发环境)开发工具,开发单片机程序变得简单和傻瓜化,图形界面上所见所得操作,基本不需要我们了解一些专业知识。正是 IDE 太过完善了,导致我们忽略一些技术知识,限制我们技术的提升。如果想进一步提升专业知识,必须要学会在 linux 下开发,本篇文章主要介绍 STM32F 项目工程使用 Makefile 构建、配置、编译。
如果你不熟悉 linux 和 Makefile,建议认真学习下面文章:
《linux-系统-Ubuntu 系统与工具》
《linux-命令-linux 基本命令使用》
《linux-编译-linux 编译构建工具》
二、构建
1、使用【STM32CubeMX】构建操作
● 使用【STM32CubeMX】构建一个 demo 工程,在《STM32CubeMX 基本使用》中的第三节【STM32CubeMX 使用】已非常详细介绍了,这里不再赘述!不同的是最后一步设置生成工程时选择【Makefile】选项,如图:
2、使用自己的【Makefile】构建操作
● 使用自己的 Makefile 构建(本人有自己风格的 Makefile 模板,并且可以对特定模块定制编译参数)。首先参照《STM32CubeMX 基本使用》中的第三节【STM32CubeMX 使用】方法构建一个基本工程,然后使用本人风格 Makefile 模板嵌入工程。至于具体如何编写 Makefile 请参考《linux-编译-linux 编译构建工具》中的【三、make+Makefile 脚本】章节。本节 demo 工程请到仓库中的【4_stm32f1xx】直接下载。将 Makefile 模板几个文件嵌入到软件工程操作方法,如图:
补充:嵌入式处理器需要专用的交叉编译器和编译参数,会多一个启动文件(汇编)和 一个链接器文件(脚本),生成依赖信息的方法也不同,这些主要涉及 [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 文件。
三、嵌入式编译器有自己独特的选项参数:
选项 |
说明 |
-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 的相关配置,如图:
二、使用 Makefile 编译工程:
1
2
3
4
5
6
|
####################################################
# 在[shell]中进入[Makefile]所在的目录,输入命令执行:
####################################################
cd x_stm32f1xx # 进入操作目录
make all # 执行编译操作(如果想重新全编译,先执行下面命令)
make clean # 执行清除操作(清除所有编译出的文件,包括 hex 等)
|
2、基于自己的【Makefile】构建的使用
一、在实际的工程应用中,必定要加入自己的源代码文件和配置相关参数。下面为通过与 Keil MDK 的比较,让你快速了解 Makefile 的相关配置,如图:
备注:以上主要涉及一个父 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、各段数据段分布:
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 编译出错! |
● 出现问题: |
■ 问题一:
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 |