pico-sdk(三)-程序架构之构建系统
RP系列微控制器在同类产品中算是功能强大的芯片,尤其在系统RAM方面,拥有大容量内存空间。但它毕竟是一个嵌入式环境,所以在 RAM、CPU 周期和程序空间上仍然很宝贵。因此,性能和其他因素(例如,边界情况错误处理、运行时与编译时配置)之间的权衡对于开发者来说比在其他更高层级的平台上更加明显。
SDK 的设计意图是让开发接口能够开箱即用,使用默认配置而不需要额外设置,但同时也给予开发者尽可能多的控制权(如果他们需要的话),以便他们对正在构建的应用程序和所使用的库进行微调。
pico-sdk
这一系列文章除了介绍 SDK 的使用方法,更多的是想展示 pico-sdk
背后的一些设计决策,不光介绍它设计了什么,也对其背后的设计原因作了一定的解释。
构建系统
SDK 使用 CMake
构建系统,绝大多数 IDE 都支持使用 CMake ,IDE 通过 CMakeLists.txt
文件来解析项目包含的头文件并生成代码自动补全建议。CMakeLists.txt
文件中包含应用程序的构建方式配置,CMake 解析 CMakeLists.txt 文件,生成 make、ninja 等构建工具支持的构建系统。开发人员通过设置 CMake 变量来生成特定平台(例如,Windows 或 Linux 发行版)的构建系统。
以下展示了 blink_simple
的主程序 CMake 构建源码
add_executable(blink_simple
blink_simple.c
)
# pull in common dependencies
target_link_libraries(blink_simple pico_stdlib)
# create map/bin/hex/uf2 file etc.
pico_add_extra_outputs(blink_simple)
# call pico_set_program_url to set path to example on github, so users can find the source for an example via picotool
example_auto_set_url(blink_simple)
在 blink_simple
示例中,定义了一个名为 blink_simple
的可执行文件目标,这个可执行程序仅依赖 pico_stdlib
这个 SDK 库。示例中还使用了 SDK 提供的 pico_add_extra_outputs
命令来生成其他格式的二进制文件(.uf2、.hex、.bin、.map、.dis 等)。
SDK 构建的是一个 bare metal
1 可执行文件,即它包含了在设备上运行所需的全部代码(除了 RP 系列微控制器 bootrom
2 中包含的启动引导代码)。
pico_stdlib
是一个接口库,它提供了编译和链接 blink_simple
应用程序所需的所有其余代码和配置。编译器在编译构建 blink_simple
应用程序的过程中,除了会编译单个 blink_simple.c
文件外,还会编译包含在 pico_stdlib
库中的数十个其他源文件,以使 blink_simple
应用程序能够在 RP 系列微控制器上运行。
每个库都是一个 CMake INTERFACE
库
SDK 中的所有库都是 CMake INTERFACE
库。(注意,不包括操作系统提供的 C/C++ 标准库)。从概念上讲,CMake 接口库是以下内容的集合:
- 源文件
- 包含路径
- 编译器定义(在代码中以
#define
形式出现) - 编译和链接选项
- 依赖关系(对其他库的依赖)
CMake 根据在 CMakeLists.txt
文件中列出的库,并递归收集这些库所依赖的库,形成了一个接口库依赖关系树,每个库都为构建系统提供源文件、包含路径、编译器定义和编译 / 链接选项。为了构建应用程序,编译器使用解析出来的包含路径、编译器定义和选项进行编译,并根据提供的链接选项将编译输出文件链接成一个可执行文件。
当使用 SDK 构建可执行文件时,编译器会把该可执行程序的源代码及依赖的SDK库的源代码重新编译,输出二进制文件。这种构建方式,能够在每个应用程序中都单独指定 SDK 的编译配置(例如,启用 / 禁用断言,设置静态缓冲区的大小)。不仅能使生成的二进制文件更小、运行速度更快,还能完全移除可执行文件中不需要的功能。
在示例程序的 CMakeLists.txt
文件中,声明了可执行程序对(INTERFACE
)库 pico_stdlib
的依赖。这个库同时还依赖于其他库(pico_runtime
、hardware_gpio
、hardware_uart
等)。pico_stdlib
库提供了简单应用程序运行所需的所有基本功能,及 GPIO 端口切换功能(切换到向 UART 输出数据),而且链接器会对未调用的函数进行垃圾回收,所以不会使你的二进制文件膨胀。可以快速查看一下 hardware_gpio
库的目录结构,我们的 blink_simple
示例就是用它来控制发光二极管(LED)的亮灭。
hardware_gpio
├── CMakeLists.txt
├── gpio.c
└── include
└── hardware
└── gpio.h
该应用程序依赖 hardware_gpio INTERFACE
接口库,以至于 gpio.c
源文件会被编译并链接到可执行文件中,同时会将上面显示的目录路径添加到编译器的依赖文件搜索路径中,这样一来,代码中使用 #include "hardware/gpio.h"
就能引入正确的头文件。
接口库还便于将相关的功能模块聚合一个库(例如 pico_stdlib),这些模块本身不直接包含代码,但依赖一些较低层的库来实现功能。就像 metapackage
3 一样,这样能够很方便的引入一组与特定目标相关的库,而无需逐个列出它们的名称。
SDK 的功能被分组到不同的接口(INTERFACE)库中,每个接口库都提供该库所需的代码和包含路径。使用时需直接(或通过另一个接口库间接)声明对所需接口库的依赖关系,这样在编译源文件时(或在集成开发环境(IDE)中进行代码补全时)才能找到头文件。