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.0cmake_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");
#endifif (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
ZephyrOS--浅谈Bluetooth LE相关推荐
- ant man 什么意思_浅谈为什么很多蓝牙模块厂家选择nRF52832?
浅谈为什么很多蓝牙模块厂家选择nRF52832? 现在蓝牙低功耗(BLE)SOC作为新一代蓝牙,以其低功耗的优势,正凸显出强大的市场竞争力,而其中Nordic公司的nRF52832这一款低功耗蓝牙芯片 ...
- 浅谈Google认证失败项分析
https://www.cnblogs.com/houser0323/p/14002924.html 标签:Google认证.GMS认证.XTS失败项分析.Android.cts 作者:秋城 http ...
- 浅谈OCR之Onenote 2010
原文:浅谈OCR之Onenote 2010 上一次我们讨论了Tesseract OCR引擎的用法,作为一款老牌的OCR引擎,目前已经开源,最新版本3.0中更是加入了中文OCR功能,再加上Google的 ...
- matlab函数 bsxfun浅谈
matlab函数 bsxfun浅谈 网上关于bsxfun的东西不多,今天需要看到一个,由于原博文插入的图片显示不出来,于是笔者大发善心进行了contrl+V 以及alt+ctrl+A的操作,供大家交流 ...
- 洛谷P1156 垃圾陷阱 题解浅谈刷表法与填表法
洛谷P1156 垃圾陷阱 题解&浅谈刷表法与填表法 填表法 :就是一般的动态规划,当前点的状态,可以直接用状态方程,根据之前点的状态推导出来. 刷表法:由当前点的状态,更新其他点的状态.需要注 ...
- 浅谈MySQL存储引擎-InnoDBMyISAM
浅谈MySQL存储引擎-InnoDB&MyISAM 存储引擎在MySQL的逻辑架构中位于第三层,负责MySQL中的数据的存储和提取.MySQL存储引擎有很多,不同的存储引擎保存数据和索引的方式 ...
- 【大话设计模式】——浅谈设计模式基础
初学设计模式给我最大的感受是:人类真是伟大啊!单单是设计模式的基础课程就让我感受到了强烈的生活气息. 个人感觉<大话设计模式>这本书写的真好.让貌似非常晦涩难懂的设计模式变的生活化.趣味化 ...
- 学校计算机机房好处,浅谈学校计算机机房维护
浅谈学校计算机机房维护 现在的学校机房都配置了数量较多的计算机,而且机房的使用非常频繁.对于怎样维护好计算机,特别是计算机软件系统,对广大计算机教师来说是一个很重要且非常现实的问题.下面就本人在 ...
- java 中的单元测试_浅谈Java 中的单元测试
单元测试编写 Junit 单元测试框架 对于Java语言而言,其单元测试框架,有Junit和TestNG这两种, 下面是一个典型的JUnit测试类的结构 package com.example.dem ...
最新文章
- 有些原理,讲着讲着自己也就相信了
- 零基础Java学习之初始化块
- /etc/profile /etc/profile .bash_profile .bashrc解释
- android安全问题(二) 程序锁
- 灵魂拷问:机器学习、深度学习专业已经沦为调包专业了吗?
- 【转】阿里技术专家详解DDD系列 第二讲 - 应用架构
- xshess 要继续使用此程序,您必须应用最新的更新
- 布客·ApacheCN 编程/后端/大数据/人工智能学习资源 2021.7
- 为什么不断做迁移,那是在还技术债
- (三十九)数据的持久化存储-plist实现(XML属性表)
- awk的基本使用方法
- matlab课后答案杨德平,MATLAB基础教程习题答案作者杨德平例题源程序课件.pdf
- 微星主板 新版bios 开启虚拟化
- 爬取超星考试题目_2020超星测试题库导入网课答案
- 加ing形式的单词有哪些_动词ing形式变化规则有哪些
- allegro异形孔导出gerber
- vue-router同一路由地址同页面切换无效解决
- jsonp跨域原理及使用
- vue动画transition
- 虚拟的超级计算机和云计算,概念PK:云计算与高性能计算(HPC)
热门文章
- 超简洁的十进制转十六进制
- 中国电子学会2022年06月份青少年软件编程Scratch图形化等级考试试卷三级真题(含答案)
- win10下用nginx搭建一个点播服务器支持各类视频格式
- Unity 图片渐变色的实现
- Python中的Assert语句简明教程
- Python编写程序,利用“凯撒密码”方案,实现对用户输入文字的加密操作
- 视频号做哪些内容比较受欢迎?
- 人生就是一个不断妥协的过程
- fileupload控件的属性_ASP:FileUpload控件(文件上传控件)
- fun在matlab里面啥意思_matlab 中.*和*有什么区别 matlab 中| || ~ 都是什么含义呢。谢谢~~...