ZephyrOS--浅谈Bluetooth LE

记得很久之前在NCS初探那篇博客我就讲过,ZephyrOS是一个相对复杂的RTOS,看到网上这部分讲的人很少,刚好最近也在看这部分,所以抽空写一下浅薄的理解,代码剖析那部分可能会需要花费一些时间理解。

1.相关工具版本

Zepher版本:

3.0.99(非正式版)

工具链:

zephyr-sdk-0.14.1

硬件:

nrf52dk_nrf52832(PCA10040)

2.环境搭建

https://docs.zephyrproject.org/latest/develop/getting_started/index.html

3.编译与烧写

本文会使用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

4.实现现象

烧写成功后,开发板打印:

*** Booting Zephyr OS build zephyr-v3.0.0-3133-gc33ce95277cc  ***
Bluetooth initialized
Advertising successfully started
[00:00:00.266,387] <inf> bt_hci_core: HW Platform: Nordic Semiconductor (0x0002)
[00:00:00.266,387] <inf> bt_hci_core: HW Variant: nRF52x (0x0002)
[00:00:00.266,418] <inf> bt_hci_core: Firmware: Standard Bluetooth controller (0x00) Version 3.0 Build 99
[00:00:00.267,150] <inf> bt_hci_core: Identity: CB:47:F2:F2:BE:A3 (random)
[00:00:00.267,150] <inf> bt_hci_core: HCI: version 5.3 (0x0c) revision 0x0000, manufacturer 0x05f1
[00:00:00.267,181] <inf> bt_hci_core: LMP: version 5.3 (0x0c) subver 0xffff

 打开手机nRF Connect连接上可以看到:

 以及里面的一些服务:

可以看到UUID为2A19的为电量,当启用通知后会一直收到变化的电量信息,以及启用 2A37的心率通知后,会一直收到心率变化:

 其他服务类型可以去详细阅读心率计的Profile。而在连接和启用通知、断开连接,开发板打印如下:


Connected
[00:10:56.926,971] <inf> bas: BAS Notifications enabled
[00:11:08.676,849] <inf> hrs: HRS notifications enabled
[00:27:34.121,887] <inf> hrs: HRS notifications disabled
[00:27:36.471,862] <inf> bas: BAS Notifications disabled
Disconnected (reason 0x13)

5.工程分析

5.1 sample工程

可以看到整个例子就一个main.c,但其实像很多模块比如蓝牙,会在编译时加进去,怎么加,从哪加后续会讲解。目前先了解当前目录:

5.1.1 boards

里面保存有一些额外的设备树或KConfig配置,也就是在每种开发板或者SOC会有一些基础配置,但是在某些demo中需要打开一些额外的硬件配置,或者软件配置,就需要在原来的基础上做修改,为了方便,在这个文件夹下的文件会像一个补丁一样,添加那些需要额外开启的选项。

5.1.2 src

里面包含应用代码。

5.1.3 CMakeList.txt

纯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 引入其他模块。

5.1.4 prj.conf/prj_minimal.conf

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所需要的最小配置。

5.1.5 README.rst

简介。

5.1.6 sample.yaml

一些对于这个sample的描述。

5.2 build工程

因为在编译时指定了固定目录,所以编译好的所有文件存于目录52832里:

 5.2.1 app

可以看到只有一个app的静态库,它是用例子中的main.c编译而成的。

5.2.2 其他模块

对于Zephyr引用的其他第三方模块,比如我们的开发板是nrf52_nrf52832是Nordic的开发板,Nordic是提供自己的hal库的,这个玩过52832的应该都知道,对于这些库,放在在modules文件夹下,比如:

但是,对于Zephyr自己的模块,比如这里我们使用的是Zephyr自己的蓝牙协议栈,它被生成在zephyr文件夹下,蓝牙属于subsys模块,所以在:

5.2.3 目标文件

此处目标文件指的是elf/bin/hex文件,它在生成目录下的zephyr里:

其中比较重要的有:

.config

最终配置选项

zephyr.map

最终镜像的内存映射

zephyr.lst

所有段反汇编

zephyr.stat

ELF头分析

zephyr.dts

设备树

还有一个比较重要的是:

它里面包含了CMake相关变量参数等。 

6.代码剖析

由上一章我们知道了,整个工程中,我们所需要开发的部分全部在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函数代码,可以看到非常简单,具体内容添加注释后如下:

6.1 bt_enable

这个是蓝牙功能的核心,代码路径在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之间初始化,对于这个初始化步骤有下图:

 对照这个图标,我们进入代码中可以看到:

就拿第一个 common_init 来说:

关于这部分感兴趣的请参照:蓝牙核心协议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层之间发送、接收一些固定的指令和应答,完成初始化。

6.2 bt_ready 

这个函数内容很简单:

就一个开始广播,它是用了一个宏去定义一个结构体,保存了一些基本信息:

可以看到注释已经解释的很清楚了。 

6.3 bt_conn_auth_cb_register

这个函数就是注册了一个回调函数,具体不再解释。

6.4 while

最后while中,是每隔一秒把电量减1和心率加1:

6.5 hrs.c与bas.c

看到这里可能会疑惑,好像没有看到心率和电量相关的服务在哪里被定义和初始化,其实和nrf之前的SDK一样,它们都是通过宏直接被定义和初始化的。查看zephyr/subsys/bluetooth/services/CMakeList.txt 可以看到:

 当相关选项被打开,则会自动包含相关.c文件。就拿hrs.c来说,所有的服务和特征如下:

全部使用宏去定义,而且可以很明显看到服务和特征的包含关系。 

推荐与引用:

前言 - 《CMake菜谱(CMake Cookbook中文版)》 - 书栈网 · BookStack

ZephyrOS-doc

蓝牙核心规范V5.3

  • 5
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值