0、前期准备
1、参考首篇文章搭建好esp32环境
2、准备好一块esp32开发开发板(本作者使用了esp32c3作为开发平台)
1、知识储备
1.1 概述
一般在编写单片机程序的时候,需要手动在main函数初始化调用初始化函数进行设备初始化,为了解决这个问题,实现优雅编程,我们可以借鉴 linux 内核 module_init的原理:利用gcc编译器的特性,将某个函数的入口地址放在某个数据段中,所以我们在可以写一个初始化函数,去访问该数据段(函数地址)即可,实现函数调用。
1.2 基础知识
__attribute__ : 用设置函数、变量和类型属性
# 使用例子
__attribute__((constructor)) : 使用该属性修饰的函数,表示在调用main前调用该函数
__attribute__((destructor)) : 使用该属性修饰的函数,表示在调用main后调用该函数
const init_func_t _init_driver_##func __attribute__((section(".init_driver"),used)) = func: 使用该属性修饰的变量,表示将_init_driver_##fun函数变量名放到.init_driver数据段
1.3 esp32的编译链接脚本语法
[type:name]
key: value
key:
value
type : 片段类型(值:段(section)、协议(scheme)和映射(mapping)) name : 片段名字(唯一)
key :键值关键字(entries 和 archive)
注意:
在段和协议中,仅支持entries键
在映射中,支持archive和entries键
多个片段的类型和名称相同时会引发异常
片段名称和键值只能使用字母、数字和下划线
段:
定义了 GCC 编译器输出的一系列目标文件段,可以是默认段(如 .text、.data),也可以是用户通过 __attribute__ 关键字定义的段
'+' 表示段列表开始,且当前段为列表中的第一个段
例子:
[sections:name]
entries:
.section+ # 第一个段
.section
...
协议:
协议定义了每个段对应的目标
例子:
[scheme:name]
entries:
sections -> target
sections -> target
...
映射:
映射定义了可映射实体(即目标文件、函数名、变量名和库对应的协议
例子:
[mapping]
archive: archive # 构建后输出的库文件名称(即 libxxx.a)
entries:
object:symbol (scheme) # 符号
object (scheme) # 目标
* (scheme) # 库
有三种存放粒度:
符号:指定了目标文件名称和符号名称。符号名称可以是函数名或变量名。
目标:只指定目标文件名称。
库:指定 *,即某个库下面所有目标文件的简化表达法。
除了实体和协议,条目中也支持指定如下标志:(注:<> = 参数名称,[] = 可选参数)
ALIGN(<alignment>[, pre, post])
根据 alignment 中指定的数字对齐存放区域,根据是否指定 pre 和 post,或两者都指定,在输入段描述(生成于映射条目)的前面和/或后面生成:
SORT([<sort_by_first>, <sort_by_second>])
在输入段描述中输出 SORT_BY_NAME, SORT_BY_ALIGNMENT, SORT_BY_INIT_PRIORITY 或 SORT。
sort_by_first 和 sort_by_second 的值可以是:name、alignment、init_priority
如果既没指定 sort_by_first 也没指定 sort_by_second,则输入段会按照名称排序,如果两者都指定了,那么嵌套排序会遵循 https://sourceware.org/binutils/docs/ld/Input-Section-Wildcards.html 中的规则。
KEEP()
用KEEP命令包围输入段描述,从而防止链接器丢弃存放区域。更多细节请参考 https://sourceware.org/binutils/docs/ld/Input-Section-Keep.html
SURROUND(<name>)
在存放区域的前面和后面生成符号,生成的符号遵循 _<name>_start 和 _<name>_end 的命名方式,例如,如果 name == sym1
在添加标志时,协议中需要指定具体的 section -> target。对于多个 section -> target,使用逗号作为分隔符,
例如:
# 注意
# A. entity-scheme 后使用分号
# B. section2 -> target2 前使用逗号
# C. 在 scheme1 条目中定义 section1 -> target1 和 section2 -> target2
entity1 (scheme1);
section1 -> target1 KEEP() ALIGN(4, pre, post),
section2 -> target2 SURROUND(sym) ALIGN(4, post) SORT()
合并后,如下的映射:
[mapping:name]
archive: lib1.a
entries:
obj1 (noflash);
rodata -> dram0_data KEEP() SORT() ALIGN(8) SURROUND(my_sym)
会在链接器脚本上生成如下输出:
. = ALIGN(8)
_my_sym_start = ABSOLUTE(.)
KEEP(lib1.a:obj1.*( SORT(.rodata) SORT(.rodata.*) ))
_my_sym_end = ABSOLUTE(.)
2、链接脚本格式
1、在组件文件夹内新建xxx.lf文件,写入如下内容
# 段列表
[sections:init_driver]
entries:
.init_driver
# 指定段位置
[scheme:init_dirver_desc]
entries:
init_driver -> flash_rodata
# 段入口
[mapping:init_driver]
archive: *
entries:
* (init_dirver_desc);
init_driver -> flash_rodata KEEP() SORT(name) SURROUND(driver_init_func_array)
2、打开CMakeLists.txt文件中,在idf_component_register 函数中引入该脚本,例子如下:
idf_component_register(
...
LDFRAGMENTS "driver_linker.lf"
...
WHOLE_ARCHIVE)
3、使用流程
# 添加组件
idf.py -C components create-component test #test为组件名
# 进入到组件文件夹并且新建链接脚本(内容见附件1)
cd components/test
vim test.lf
# 修改CMakeFile.txt文件,内容如下:
...
idf_component_register(...
LDFRAGMENTS "test.lf"
...)
# 创建初始化函数(见附件1)
vim test.c test.h
# 在main.c中注册一个测试函数,内容如下:
void test(void) {
printf("test\n");
}
INIT_DRIVER(test);
# 编译,烧录之后,在monitor中就可以观察到test函数被调用
附件1
test.lf
[sections:init_test]
entries:
.init_test
[scheme:init_test_desc]
entries:
init_test -> flash_rodata
[mapping:init_test]
archive: *
entries:
* (init_test_desc);
init_test -> flash_rodata KEEP() SORT(name) SURROUND(test_init_func_array)
test.h
#ifndef TEST_H
#define TEST_H
typedef void (*init_func_t)(void);
#define INIT_DRIVER(func) \
const init_func_t _init_test_##func __attribute__((section(".init_test"),used)) = func
#endif
test.c
static void __attribute__((constructor)) init_test_register(void) {
extern const init_func_t _init_func_array_start;
extern const init_func_t _init_func_array_end;
for (const init_func_t* it = &_init_func_array_start; it != &_init_func_array_end; ++it) {
(*it)();
}
}