记得很久之前在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相关推荐

  1. ant man 什么意思_浅谈为什么很多蓝牙模块厂家选择nRF52832?

    浅谈为什么很多蓝牙模块厂家选择nRF52832? 现在蓝牙低功耗(BLE)SOC作为新一代蓝牙,以其低功耗的优势,正凸显出强大的市场竞争力,而其中Nordic公司的nRF52832这一款低功耗蓝牙芯片 ...

  2. 浅谈Google认证失败项分析

    https://www.cnblogs.com/houser0323/p/14002924.html 标签:Google认证.GMS认证.XTS失败项分析.Android.cts 作者:秋城 http ...

  3. 浅谈OCR之Onenote 2010

    原文:浅谈OCR之Onenote 2010 上一次我们讨论了Tesseract OCR引擎的用法,作为一款老牌的OCR引擎,目前已经开源,最新版本3.0中更是加入了中文OCR功能,再加上Google的 ...

  4. matlab函数 bsxfun浅谈

    matlab函数 bsxfun浅谈 网上关于bsxfun的东西不多,今天需要看到一个,由于原博文插入的图片显示不出来,于是笔者大发善心进行了contrl+V 以及alt+ctrl+A的操作,供大家交流 ...

  5. 洛谷P1156 垃圾陷阱 题解浅谈刷表法与填表法

    洛谷P1156 垃圾陷阱 题解&浅谈刷表法与填表法 填表法 :就是一般的动态规划,当前点的状态,可以直接用状态方程,根据之前点的状态推导出来. 刷表法:由当前点的状态,更新其他点的状态.需要注 ...

  6. 浅谈MySQL存储引擎-InnoDBMyISAM

    浅谈MySQL存储引擎-InnoDB&MyISAM 存储引擎在MySQL的逻辑架构中位于第三层,负责MySQL中的数据的存储和提取.MySQL存储引擎有很多,不同的存储引擎保存数据和索引的方式 ...

  7. 【大话设计模式】——浅谈设计模式基础

    初学设计模式给我最大的感受是:人类真是伟大啊!单单是设计模式的基础课程就让我感受到了强烈的生活气息. 个人感觉<大话设计模式>这本书写的真好.让貌似非常晦涩难懂的设计模式变的生活化.趣味化 ...

  8. 学校计算机机房好处,浅谈学校计算机机房维护

    浅谈学校计算机机房维护    现在的学校机房都配置了数量较多的计算机,而且机房的使用非常频繁.对于怎样维护好计算机,特别是计算机软件系统,对广大计算机教师来说是一个很重要且非常现实的问题.下面就本人在 ...

  9. java 中的单元测试_浅谈Java 中的单元测试

    单元测试编写 Junit 单元测试框架 对于Java语言而言,其单元测试框架,有Junit和TestNG这两种, 下面是一个典型的JUnit测试类的结构 package com.example.dem ...

最新文章

  1. 有些原理,讲着讲着自己也就相信了
  2. 零基础Java学习之初始化块
  3. /etc/profile /etc/profile .bash_profile .bashrc解释
  4. android安全问题(二) 程序锁
  5. 灵魂拷问:机器学习、深度学习专业已经沦为调包专业了吗?
  6. 【转】阿里技术专家详解DDD系列 第二讲 - 应用架构
  7. xshess 要继续使用此程序,您必须应用最新的更新
  8. 布客·ApacheCN 编程/后端/大数据/人工智能学习资源 2021.7
  9. 为什么不断做迁移,那是在还技术债
  10. (三十九)数据的持久化存储-plist实现(XML属性表)
  11. awk的基本使用方法
  12. matlab课后答案杨德平,MATLAB基础教程习题答案作者杨德平例题源程序课件.pdf
  13. 微星主板 新版bios 开启虚拟化
  14. 爬取超星考试题目_2020超星测试题库导入网课答案
  15. 加ing形式的单词有哪些_动词ing形式变化规则有哪些
  16. allegro异形孔导出gerber
  17. vue-router同一路由地址同页面切换无效解决
  18. jsonp跨域原理及使用
  19. vue动画transition
  20. 虚拟的超级计算机和云计算,概念PK:云计算与高性能计算(HPC)

热门文章

  1. 超简洁的十进制转十六进制
  2. 中国电子学会2022年06月份青少年软件编程Scratch图形化等级考试试卷三级真题(含答案)
  3. win10下用nginx搭建一个点播服务器支持各类视频格式
  4. Unity 图片渐变色的实现
  5. Python中的Assert语句简明教程
  6. Python编写程序,利用“凯撒密码”方案,实现对用户输入文字的加密操作
  7. 视频号做哪些内容比较受欢迎?
  8. 人生就是一个不断妥协的过程
  9. fileupload控件的属性_ASP:FileUpload控件(文件上传控件)
  10. fun在matlab里面啥意思_matlab 中.*和*有什么区别 matlab 中| || ~ 都是什么含义呢。谢谢~~...