3. 协议栈源码结构:SDK目录树解析、核心库与示例工程、链接脚本与内存布局、编译系统
好,咱们进入第三章。这一章我打算带你把协议栈的「骨架」摸清楚。说白了,就是拿到一个陌生的SDK,你该怎么下手?哪些文件夹是核心,哪些是凑数的?链接脚本那玩意儿到底在干嘛?还有那个让人头疼的Makefile,到底怎么改才能跑起来?
嗯,别急。我一个一个给你拆开讲。这些都是我当年踩坑踩出来的经验。
3.1 SDK目录树解析:别被文件夹吓到
我第一次拿到某大厂的蓝牙SDK时,打开压缩包,好家伙,七八层目录,上百个文件夹。当时我就懵了。后来我总结出一个规律:再复杂的SDK,核心也就那么几个目录。
一般来说,一个典型的低功耗蓝牙协议栈SDK,目录结构长这样:
ble_sdk/
├── components/ # 核心组件库
│ ├── ble/ # 蓝牙协议栈核心
│ │ ├── include/ # 头文件(API声明)
│ │ ├── src/ # 源码(大部分是库,不给你看)
│ │ └── lib/ # 预编译的 .a 或 .lib 文件
│ ├── drivers/ # 外设驱动(GPIO、UART、SPI等)
│ └── os/ # 操作系统抽象层(FreeRTOS或裸机)
├── examples/ # 示例工程
│ ├── ble_heart_rate/ # 心率手环示例
│ ├── ble_hid/ # 键盘鼠标示例
│ └── ble_beacon/ # 信标示例
├── tools/ # 工具链(烧录脚本、打包工具)
├── docs/ # 文档(别指望太多,通常很简略)
└── projects/ # 用户工程目录(你自己写的代码放这)
我个人习惯,拿到SDK第一件事,就是先看 examples/ 目录。为什么?因为示例工程是官方帮你搭好的「样板间」。你直接编译它,能跑通,说明你的工具链和环境没问题。然后你再照着它的结构,去改自己的工程。
3.2 核心库与示例工程:哪些是「真」代码?
你打开 components/ble/lib/ 目录,里面通常是一堆 .a 或 .lib 文件。这些就是协议栈的「核心库」。说白了,芯片原厂把最底层的蓝牙协议栈实现编译好了,只给你留了几个API接口。你调用就行,别想着去改它。
那什么是「示例工程」?就是官方给你写好的一个完整应用。比如一个心率手环的demo,它包含了:
- 初始化流程:芯片上电后,先初始化时钟、GPIO、然后初始化蓝牙协议栈。
- 广播与扫描:手环作为外设,广播自己的心率数据。
- 连接与配对:手机连上手环,进行配对绑定。
- 数据传输:通过GATT协议,把心率值发给手机。
你想想看,如果让你从零开始写这些,光蓝牙协议栈的初始化参数配置,就能让你看文档看到吐。但有了示例工程,你只需要改几个回调函数里的业务逻辑就行。
举个例子,这是我从一个心率示例里摘出来的关键代码:
// 初始化蓝牙协议栈
void ble_stack_init(void)
{
ble_cfg_t cfg;
memset(&cfg, 0, sizeof(cfg));
// 配置广播参数
cfg.adv_cfg.adv_type = BLE_ADV_TYPE_IND;
cfg.adv_cfg.interval = 100; // 100ms广播间隔
cfg.adv_cfg.channel_map = BLE_ADV_CH_ALL;
// 调用协议栈初始化API
ble_stack_init(&cfg);
// 注册回调函数
ble_gap_register_cb(gap_event_handler);
ble_gatt_register_cb(gatt_event_handler);
}
你看,核心就这几行。剩下的都是业务逻辑。所以我的建议是:先跑通示例,再动手改。别一上来就自己新建工程,那是给自己找麻烦。
3.3 链接脚本与内存布局:你的代码到底放哪了?
链接脚本,就是 .ld 文件。很多初学者觉得这东西高深莫测,其实说白了,它就是一张「地图」,告诉编译器:你的代码该放在Flash的哪个地址,变量该放在RAM的哪个地址。
一个典型的链接脚本,核心内容就这几块:
| 段(Section) | 存放内容 | 存放位置 | 说明 |
|---|---|---|---|
.text |
代码指令 | Flash | 你的函数、中断向量表都在这 |
.rodata |
只读数据 | Flash | 字符串常量、const变量 |
.data |
已初始化全局变量 | Flash(运行时拷贝到RAM) | 上电后从Flash拷贝到RAM |
.bss |
未初始化全局变量 | RAM | 上电后清零 |
.heap |
堆空间 | RAM | malloc/free用 |
.stack |
栈空间 | RAM | 函数调用、局部变量用 |
嗯,这里要注意一点:蓝牙协议栈的库,通常会被强制链接到特定的Flash地址。为什么?因为协议栈里有些代码是位置相关的,比如中断向量表、或者某些跳转指令。如果你改了链接脚本,把协议栈库放到了错误的地址,那系统一启动就死机。
我在项目中遇到过一个问题:手环跑着跑着就死机,复位后又能跑一会儿。排查了三天,最后发现是链接脚本里,协议栈库的 .text 段和用户代码的 .text 段重叠了。说白了,就是Flash空间不够,两个段挤到一起了。解决方案很简单:要么换大容量Flash的芯片,要么优化代码体积。
3.4 编译系统:Makefile 与 CMake 的抉择
编译系统,说白了就是告诉编译器「怎么编译、链接哪些文件、生成什么格式的输出」。目前主流的就两种:Makefile 和 CMake。
Makefile 是「老前辈」了。它的语法比较原始,说白了就是一堆变量定义和规则。比如:
# 定义编译器
CC = arm-none-eabi-gcc
# 定义编译选项
CFLAGS = -mcpu=cortex-m4 -mthumb -O2 -Wall
# 定义源文件
SRCS = main.c ble_stack.c app_heart_rate.c
# 定义目标文件
OBJS = $(SRCS:.c=.o)
# 链接规则
$(TARGET).elf: $(OBJS)
$(CC) $(CFLAGS) -T $(LINKER_SCRIPT) -o $@ $^
你看,Makefile 的优点是直接、透明。你写的每一行,编译器都会照做。但缺点也很明显:跨平台能力差。你换个编译器,或者换个操作系统,Makefile 可能就得重写。
CMake 是后来者。它不直接生成可执行文件,而是先生成 Makefile(或者 Ninja 构建文件),然后再调用编译器。它的语法更高级,支持跨平台。比如:
cmake_minimum_required(VERSION 3.10)
project(ble_handring C ASM)
# 设置芯片型号
set(MCU cortex-m4)
# 添加源文件
add_executable(ble_handring
main.c
ble_stack.c
app_heart_rate.c
)
# 设置链接脚本
target_link_options(ble_handring PRIVATE
-T ${CMAKE_SOURCE_DIR}/linker.ld
)
我个人更推荐用 CMake。为什么?因为现在很多芯片原厂都开始用 CMake 了。你想想看,如果你以后换芯片,CMake 的移植成本比 Makefile 低得多。你只需要改几行配置,就能适配新的工具链。
不过,如果你用的是比较老的芯片,或者原厂只提供了 Makefile 示例,那也别纠结。Makefile 也能用,只是维护起来稍微麻烦点。
好了,这一章的内容就这些。总结一下:
- SDK目录树:核心看
components/和examples/,其他都是辅助。 - 核心库与示例:先跑通示例,再改业务逻辑。别自己从头搭工程。
- 链接脚本:别乱改。改之前先备份,改之后要测试。
- 编译系统:推荐 CMake,但 Makefile 也能用。关键是理解它的逻辑。
下一章,咱们开始动手移植。我会带你一步步把协议栈跑起来。到时候,你就能感受到「从零到一」的成就感了。