记得很久之前在NCS初探那篇博客我就讲过,ZephyrOS是一个相对复杂的RTOS,看到网上这部分讲的人很少,刚好最近也在看这部分,所以抽空写一下浅薄的理解,代码剖析那部分可能会需要花费一些时间理解。
Zepher版本:
3.0.99(非正式版)
工具链:
zephyr-sdk-0.14.1
硬件:
nrf52dk_nrf52832(PCA10040)
https://docs.zephyrproject.org/latest/develop/getting_started/index.html
本文会使用peripheral_hr这个例子,去浅谈zephyr蓝牙相关开发。
编译使用指令
west build -b nrf52dk_nrf52832 -d 52832 -p auto .\samples\bluetooth\peripheral_hr\
west具体参数请使用下面指令查看
west --help
烧写使用指令
west flash -d 52832
关于编译与烧写,其实也可以使用cmake,nrfjprog等命令直接操作,west就是对一些常用工具的封装。具体可以参考:
Application Development — Zephyr Project Documentation
烧写成功后,开发板打印:
*** Booting Zephyr OS build zephyr-v3.0.0-3133-gc33ce95277cc ***
Bluetooth initialized
Advertising successfully started
[00:00:00.266,387] bt_hci_core: HW Platform: Nordic Semiconductor (0x0002)
[00:00:00.266,387] bt_hci_core: HW Variant: nRF52x (0x0002)
[00:00:00.266,418] bt_hci_core: Firmware: Standard Bluetooth controller (0x00) Version 3.0 Build 99
[00:00:00.267,150] bt_hci_core: Identity: CB:47:F2:F2:BE:A3 (random)
[00:00:00.267,150] bt_hci_core: HCI: version 5.3 (0x0c) revision 0x0000, manufacturer 0x05f1
[00:00:00.267,181] bt_hci_core: LMP: version 5.3 (0x0c) subver 0xffff
打开手机nRF Connect连接上可以看到:
以及里面的一些服务:
可以看到UUID为2A19的为电量,当启用通知后会一直收到变化的电量信息,以及启用 2A37的心率通知后,会一直收到心率变化:
其他服务类型可以去详细阅读心率计的Profile。而在连接和启用通知、断开连接,开发板打印如下:
Connected
[00:10:56.926,971] bas: BAS Notifications enabled
[00:11:08.676,849] hrs: HRS notifications enabled
[00:27:34.121,887] hrs: HRS notifications disabled
[00:27:36.471,862] bas: BAS Notifications disabled
Disconnected (reason 0x13)
可以看到整个例子就一个main.c,但其实像很多模块比如蓝牙,会在编译时加进去,怎么加,从哪加后续会讲解。目前先了解当前目录:
里面保存有一些额外的设备树或KConfig配置,也就是在每种开发板或者SOC会有一些基础配置,但是在某些demo中需要打开一些额外的硬件配置,或者软件配置,就需要在原来的基础上做修改,为了方便,在这个文件夹下的文件会像一个补丁一样,添加那些需要额外开启的选项。
里面包含应用代码。
纯CMake语法,具体可以查看相关书籍,文末有推荐。
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(peripheral_hr)
FILE(GLOB app_sources src/*.c)
target_sources(app PRIVATE
${app_sources}
)
zephyr_library_include_directories(${ZEPHYR_BASE}/samples/bluetooth)
其实就把src下的所有.c编译为名字为app的静态库。关键在于 find_package 引入其他模块。
CONFIG_BT=y
CONFIG_BT_DEBUG_LOG=y
CONFIG_BT_SMP=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DIS=y
CONFIG_BT_DIS_PNP=n
CONFIG_BT_BAS=y
CONFIG_BT_HRS=y
CONFIG_BT_DEVICE_NAME="Zephyr Heartrate Sensor"
CONFIG_BT_DEVICE_APPEARANCE=833
里面有很多配置选项,比如CONFIG_BT=y,即启用蓝牙相关服务,就是关联很多代码里面的宏定义及编译选项。而prj_minimal.conf则是为了nRF52810和nRF52811想要使用这个demo所需要的最小配置。
简介。
一些对于这个sample的描述。
因为在编译时指定了固定目录,所以编译好的所有文件存于目录52832里:
可以看到只有一个app的静态库,它是用例子中的main.c编译而成的。
对于Zephyr引用的其他第三方模块,比如我们的开发板是nrf52_nrf52832是Nordic的开发板,Nordic是提供自己的hal库的,这个玩过52832的应该都知道,对于这些库,放在在modules文件夹下,比如:
但是,对于Zephyr自己的模块,比如这里我们使用的是Zephyr自己的蓝牙协议栈,它被生成在zephyr文件夹下,蓝牙属于subsys模块,所以在:
此处目标文件指的是elf/bin/hex文件,它在生成目录下的zephyr里:
其中比较重要的有:
.config
最终配置选项
zephyr.map
最终镜像的内存映射
zephyr.lst
所有段反汇编
zephyr.stat
ELF头分析
zephyr.dts
设备树
还有一个比较重要的是:
它里面包含了CMake相关变量参数等。
由上一章我们知道了,整个工程中,我们所需要开发的部分全部在main.c中,而其他部分,包含kernel、蓝牙协议栈等模块,都是通过配置KConfig选项来实现是否把OS中已经包含的这些代码编译链接到你的项目中,所以,我们应该先看一下main.c中也就是我们所需要编写的那部分代码都包含了什么:
void main(void)
{
int err;
err = bt_enable(NULL);
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
return;
}
bt_ready();
bt_conn_auth_cb_register(&auth_cb_display);
/* Implement notification. At the moment there is no suitable way
* of starting delayed work so we do it here
*/
while (1) {
k_sleep(K_SECONDS(1));
/* Heartrate measurements simulation */
hrs_notify();
/* Battery level simulation */
bas_notify();
}
}
以上是main.c中main函数代码,可以看到非常简单,具体内容添加注释后如下:
这个是蓝牙功能的核心,代码路径在zephyr/subsys/bluetooth/host/hci_core.c中,具体代码如下:
int bt_enable(bt_ready_cb_t cb)
{
int err;
if (!bt_dev.drv) {
BT_ERR("No HCI driver registered");
return -ENODEV;
}
atomic_clear_bit(bt_dev.flags, BT_DEV_DISABLE);
if (atomic_test_and_set_bit(bt_dev.flags, BT_DEV_ENABLE)) {
return -EALREADY;
}
if (IS_ENABLED(CONFIG_BT_SETTINGS)) {
err = bt_settings_init();
if (err) {
return err;
}
} else if (IS_ENABLED(CONFIG_BT_DEVICE_NAME_DYNAMIC)) {
err = bt_set_name(CONFIG_BT_DEVICE_NAME);
if (err) {
BT_WARN("Failed to set device name (%d)", err);
}
}
ready_cb = cb;
/* TX thread */
k_thread_create(&tx_thread_data, tx_thread_stack,
K_KERNEL_STACK_SIZEOF(tx_thread_stack),
hci_tx_thread, NULL, NULL, NULL,
K_PRIO_COOP(CONFIG_BT_HCI_TX_PRIO),
0, K_NO_WAIT);
k_thread_name_set(&tx_thread_data, "BT TX");
#if defined(CONFIG_BT_RECV_WORKQ_BT)
/* RX thread */
k_work_queue_start(&bt_workq, rx_thread_stack,
CONFIG_BT_RX_STACK_SIZE,
K_PRIO_COOP(CONFIG_BT_RX_PRIO), NULL);
k_thread_name_set(&bt_workq.thread, "BT RX");
#endif
if (IS_ENABLED(CONFIG_BT_TINYCRYPT_ECC)) {
bt_hci_ecc_init();
}
err = bt_dev.drv->open();
if (err) {
BT_ERR("HCI driver open failed (%d)", err);
return err;
}
bt_monitor_send(BT_MONITOR_OPEN_INDEX, NULL, 0);
if (!cb) {
return bt_init();
}
k_work_submit(&bt_dev.init);
return 0;
}
具体分析:
bt_dev的创建就在当前.c文件中:
struct bt_dev bt_dev = {
.init = Z_WORK_INITIALIZER(init_work),
/* Give cmd_sem allowing to send first HCI_Reset cmd, the only
* exception is if the controller requests to wait for an
* initial Command Complete for NOP.
*/
#if !defined(CONFIG_BT_WAIT_NOP)
.ncmd_sem = Z_SEM_INITIALIZER(bt_dev.ncmd_sem, 1, 1),
#else
.ncmd_sem = Z_SEM_INITIALIZER(bt_dev.ncmd_sem, 0, 1),
#endif
.cmd_tx_queue = Z_FIFO_INITIALIZER(bt_dev.cmd_tx_queue),
#if defined(CONFIG_BT_DEVICE_APPEARANCE_DYNAMIC)
.appearance = CONFIG_BT_DEVICE_APPEARANCE,
#endif
};
可以看到,bt_dev的初始化就是使用一些 宏 去初始化它的元素,比如拿 .init 这个项来说,在这个结构体的定义是:
而使用了 Z_WORK_INITIALIZER 这个宏就是为了方便初始化 handler这个结构体成员。
所以最终结果就是把 init_work 这个函数交给内核运行。它的代码是:
它主要负责bt各个层之间初始化,可以看到主要是从HCI层往上:
static int bt_init(void)
{
int err;
err = hci_init();
if (err) {
return err;
}
if (IS_ENABLED(CONFIG_BT_CONN)) {
err = bt_conn_init();
if (err) {
return err;
}
}
if (IS_ENABLED(CONFIG_BT_ISO)) {
err = bt_conn_iso_init();
if (err) {
return err;
}
}
if (IS_ENABLED(CONFIG_BT_SETTINGS)) {
if (!bt_dev.id_count) {
BT_INFO("No ID address. App must call settings_load()");
return 0;
}
atomic_set_bit(bt_dev.flags, BT_DEV_PRESET_ID);
}
bt_finalize_init();
return 0;
}
比如拿 hci_init 来讲,它负责HCI层和底层Controller之间初始化,对于这个初始化步骤有下图:
对照这个图标,我们进入代码中可以看到:
关于这部分感兴趣的请参照:蓝牙核心协议V5.3的Vol6 PartD 部分,里面是有关事件的时序,可以对照着图片和代码慢慢理解。 这里提示一点在 hci_init 中关于事件掩码 set_event_mask:
具体掩码类型定义在hci.h中,此部分可以参照:
GAP
tips
这里提示一点,如果你对于一些KConfig选项不了解,可以使用指令:
west build -t guiconfig -d 52832
-d后跟你自己的build文件夹,此时会弹出一个图形化配置器,如下图:
可以选择左上角jump to,去查找你想要配置的选项:
注意把前面的 CONFIG_ 前缀去掉再搜索。比如这里的CONFIG_BT_SETTINGS下面的注释就表明了它的含义与依赖等。
略过这些与核心规范紧密相关的初始化,我们已经知道蓝牙初始化是由bt_dev这个结构体的init成员完成,而在 bt_enable 的一开始部分:
上图的drv,根据前面的分析,并没有在 bt_dev 的初始化中找到它,那它到底在哪?
答案在 zephyr/subsys/bluetooth/controller/hci/hci_driver.c 中,这个文件中完成了HCI层的驱动的各种操作实现,并通过放在固定段内实现自动加载:
详细的不再展开,就提醒一点,在 hci_driver_open 中是有创建接收线程的,如下:
那发送有没有相关线程?答案是有的,就在 bt_enable 中,它在接收线程前被创建:
最后可以看到bt_init在这个demo中是会运行的,它和之前那个工作队列项的内容一模一样:
所以整个 bt_enable 简化下来就是:
1.创建发送线程
2.创建接收线程
3.通过HCI层和Controller层之间发送、接收一些固定的指令和应答,完成初始化。
这个函数内容很简单:
就一个开始广播,它是用了一个宏去定义一个结构体,保存了一些基本信息:
可以看到注释已经解释的很清楚了。
这个函数就是注册了一个回调函数,具体不再解释。
最后while中,是每隔一秒把电量减1和心率加1:
看到这里可能会疑惑,好像没有看到心率和电量相关的服务在哪里被定义和初始化,其实和nrf之前的SDK一样,它们都是通过宏直接被定义和初始化的。查看zephyr/subsys/bluetooth/services/CMakeList.txt 可以看到:
当相关选项被打开,则会自动包含相关.c文件。就拿hrs.c来说,所有的服务和特征如下:
全部使用宏去定义,而且可以很明显看到服务和特征的包含关系。
前言 - 《CMake菜谱(CMake Cookbook中文版)》 - 书栈网 · BookStack
ZephyrOS-doc
蓝牙核心规范V5.3