置顶链接
- PB-03F-kit规格书
PB-03F-kit规格书
- PB-03F-kit相关使用工具
https://docs.ai-thinker.com/blue_tooth_pb
- PB-03F-kit环境搭建、烧录教程参考
【蓝牙5.2 PB-03F教程】二次开发环境搭建
https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=45385&extra=page%3D1&_dsign=7c8fe8cb
【蓝牙5.2 PB-03F教程】烧录流程
https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=45392&extra=page%3D1&_dsign=5f5f2ec8
【蓝牙5.2 PB-03F教程】蓝牙基础+主从机指令的使用
https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=45396&extra=page%3D1&_dsign=63f9575e
【蓝牙5.2 PB-03教程】SDK二次开发入门,认识架构,开始点亮一盏LED
https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=45412&extra=page%3D5&_dsign=7c8fe8cb
低功耗实战(电池供电必学)
https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=46152&extra=page%3D1
【蓝牙5.2 PB-03F教程】GPIO中断
https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=45397&extra=page%3D6
- 非官方教程
收发广播包
OTA代码解析
OSAL
OSAL2
蓝牙协议栈
SPI
PWM
WS2812
DMA
OTA过程
FLASH
PHY 蓝牙系列(PHY6202/6212/6222/6252)
封装 QFN32 :PHY6202/6212/6222
QFN48 :PHY6202/6212
SSOP24:PHY6252
外部电路
有 DCDC :PHY6202/6212/6222(功耗低)
无 DCDC : PHY6202/6212/6222 /PHY6252(功耗高)
无 DCDC 情况下发射功率可以达到最大,例:6222 带 DCDC 最大功率为 5dBm, 不带 DCDC 最大发射功率可达到 10dBm
芯片不支持 J-link 烧录,但是支持 J-link 仿真,仅支持串口烧录。
(6202/6212 支持 JTAG 接口,6222/6252 支持 SWD 接口)
PHY6252是奉加微的产品,ST17H66是伦茨科技的产品。
但是所有的东西都一样的,SDK、文档、下载工具。
还和CC254x差不多,培训视频、文档资料相当多,拿来做参考有助于理解。BLE-CC254x-1.5.2.0.EXE
参数
支持低功耗蓝牙:
Bluetooth5.2, Bluetoothmesh;
蓝牙速率支持:125Kbps, 500Kbps,1Mbps, 2Mbps;支持广播扩展,
多广播,信道选择。
64KB SRAM,256KB flash, 96KB ROM, 256bit efuse;
Flash 存储区域映射表
256KB Flash ota_single_bank存储区域映射表
分区名称 |
起始地址 |
结束地址 |
大小 |
说明 |
Reserved By PhyPlus |
0x0000 |
0x1FFF |
8KB |
不可修改 |
1st Boot Info |
0x2000 |
0x2FFF |
4KB |
BootloaderInfo |
2nd Boot Info |
0x3000 |
0x3FFF |
4KB |
App Info |
FCDS |
0x4000 |
0x4FFF |
4KB |
FCDS(工厂配置数据存储区) |
OTA Bootloader |
0x5000 |
0x10FFF |
48KB |
Bootloader |
App Bank |
0x11000 |
0x1FFFF |
60KB |
App Sram |
XIP |
0x20000 |
0x3BFFF |
112KB |
eXecute In Place(原地执行) |
FS (UCDS) |
0x3C000 |
0x3DFFF |
8KB |
(文件系统 / 用户配置数据区) |
Resource |
0x3E000 |
0x3FFFF |
8KB |
FlashNVM |
FW Storage |
0x40000 |
0x3FFFF |
0 |
FW Storage |
APP bank和XIP共用172KB空间.除app bank起始地址做限制外。app bank size及xip不做限制
256KB Flash ota存储区域映射表
分区名称 |
起始地址 |
结束地址 |
大小 |
说明 |
Reserved By PhyPlus |
0x0000 |
0x1FFF |
8KB |
不可修改 |
1st Boot Info |
0x2000 |
0x2FFF |
4KB |
BootloaderInfo |
2nd Boot Info |
0x3000 |
0x3FFF |
4KB |
App Info |
FCDS |
0x4000 |
0x4FFF |
4KB |
FCDS(工厂配置数据存储区) |
SLB Bootloader |
0x5000 |
0x8FFF |
16KB |
单 Bank Bootloader 代码 |
Resource |
0x9000 |
0x9FFF |
8KB |
资源区(如字体、图标等) |
App (Map to Sram) |
0x11000 |
0x1FFFF |
60KB |
App Sram |
XIP |
0x20000 |
0x2BFFF |
48KB |
eXecute In Place(原地执行) |
FS (UCDS) |
0x2C000 |
0x2DFFF |
8KB |
(文件系统 / 用户配置数据区) |
Resource |
0x2E000 |
0x2DFFF |
0KB |
FlashNVM |
FW Storage |
0x2E000 |
0x3FFFF |
72KB |
FW Storage |
二次开发
SDK网址

下载PHY6252芯片的SDK,解压后打开SDK下面的example\peripheral\gpio例程

关闭低功耗模式
因为芯片休眠了,LED的输出也会关闭。

CFG_SLEEP_MODE=PWR_MODE_SLEEP
中插入 _NO
即可:CFG_SLEEP_MODE=PWR_MODE_NO_SLEEP
Options for Target选项卡中的C/C++里面的Preprocessor Symbols中的Define处定义了宏定义会参与工程的编译。
定义DEBUG_INFO=1,会打开调试日记。
定义DEBUG_INFO=0,会关闭调试日记。
加载分散文件
在Linker选项卡内Scatter File处可设置工程的分散加载文件。

点亮蓝色LED(GPIO_18 输出高电平)
修改gpio_demo.c
注释 void Key_Demo_Init(uint8 task_id)
整个函数,并修改为
void Key_Demo_Init(uint8 task_id)
{
key_TaskID = task_id;// 任务id,先暂时不用管。
// 此写函数默认会调用hal_gpio_pin_init(pin,GPIO_OUTPUT);
hal_gpio_write(GPIO_P18,HAL_HIGH_IDLE); // GPIO18 输出高电平,点亮LED
}
编译
After Build - User command #2: fromelf.exe .\Objects\gpio_demo.axf --i32combined --output .\bin\gpio_demo.hex
注意这一行的 --output .\bin\gpio_demo.hex告诉了我们输出的gpio_demo.hex固件在bin目录下
烧录


输入自己想要使用的MAC地址。
出现 UART TX ASCII: UXTDWU
时说明开发板连接成功。
此时长按复位按键,大概2s左右后,松开复位键。再进行第9和第10步烧录成功后再按RST复位运行。
奉加微电子PhyPlusKit使用视频教程
PhyPlusKit软件支持6种烧录模式
仅有应用固件的APP烧录
附带OTA功能的烧录
附带OTA和CHIPID模式的烧录
附带OTA和ENCIV模式的烧录:
ENC_IV:加密初始向量(Initialization Vector ),是加密算法的 “随机数种子”,让每次加密结果不同,增强安全性。
含义:固件包含 OTA 功能 + 加密相关逻辑(依赖 ENC_IV ),用于:
OTA 升级时,加密传输固件包(防止被篡改、窃取 );
设备通信时,加密敏感数据(如配网密钥、用户数据 )。
Preserve模式:
Preserve:“保留、维持”→ 烧录时保留设备原有部分数据 / 配置(如已存储的用户参数、校准数据、密钥 )。
含义:更新固件时,不覆盖特定分区 / 数据,避免重置设备已有信息。
场景:
升级应用层逻辑,但想保留用户设置(如蓝牙配对信息 );
修复 Bug 时,维持设备已存储的敏感参数(如加密密钥 )。
FCT烧录模式:
FCT:Factory Config Table(工厂配置表),是一种用于存储设备出厂配置信息的表格。
含义:固件包含 FCT 表格,用于:
存储设备出厂配置信息(如 MAC 地址、产品型号、版本号等 );
提供设备配置接口(如通过 APP 配置设备参数 )。
Keil烧录

依次点击HexF,Erase,Write烧录固件
SWD调试
P2 GPIO2/SWD debug data inout
P3 GPIO3/SWD debug clock
6222/6252 支持 SWD 接口
未关闭SWD调试的情况下可以通过Jlink读出来固件的,可以把SWD的io口拉低屏蔽。
hal_gpio_pin2pin3_control(GPIO_P02,0);
hal gpio pin2pin3 control(GPIO_P03,0);
OSAL(Operating System Abstraction Layer)即操作系统抽象层
一个单片机通用的软件调度器框架 --- 基于任务和事件的OSAL调度器。OSAL最初的概念是由德州仪器TI在ZigBee的协议栈Z-Stack上引入的,严格意义上来说,它并不是一个传统意义的操作系统,但可以实现部分类似操作系统的功能。简单来说就是初始化任务池以及轮询任务池。
默认情况下,OSAL最大任务数为9,最大事件数为16

任务调度:采用任务数组tasks_events来管理各层对应的任务事件,tasks_arr是任务事件的处理数组,使用osal_set_event()函数,可以为对应的任务设置事件,使用osal_clear_event()函数,可以清除对应的任务事件。
时间管理:OSAL使用STM32的滴答时钟systick作为时间基准,可以虚拟出多个软件定时器,使用这些软件定时器,可以设置延时任务事件,当延时时间到达后,会为对应的任务事件设置触发标志位,然后执行延时事件。
OSAL调度器主要涉及以下源文件:

osal.c和osal.h:主要是任务的注册和调度实现,以及提供任务事件函数设置函数和任务事件清除函数。
osal_clock.c和osal_clock.h:主要是OSAL调度器使用STM32的滴答时钟systick作为时基,更新系统的软件定时器计数值,以及查找定时任务是否到达定时时间。
osal_timers.c和osal_timers.h:主要是通过链表的方式管理系统的软件定时器,对外提供软件定时任务的启动函数,软件定时任务的停止函数。

以上流程图,板级初始化和OSAL任务初始化,在main函数中完成,具体的调用方式可以查看GitHub里面的源代码文件。run_system()函数主要在while(1)大循环中调用,不断更新软件定时器并查找是否有定时任务,如果有立即执行的任务或定时任务,则执行任务事件函数,事件处理函数执行完成后,清除事件标志位。
int app_main(void)
{
/* 初始化OSAL,包括初始化任务池osalInitTasks() */
osal_init_system();
osal_pwrmgr_device( PWRMGR_BATTERY );
/* Start OSAL */
osal_start_system(); // 轮询任务池
return 0;
}
其中osal_init_system()会调用osalInitTasks(),做OSAL初始化。
osal_init_system没有公开源码。
osal_start_system()会调用osal_run_system(),进行osal轮询。
但是osal_start_system、osal_run_system都没有公开源码。
用户能看到的是:
jump_table_base和tasksArr
run_system()函数主要在osal.c文件中实现,函数的实现方式如下所示:

举个例子:例如我们需要添加一个看门狗的任务来监测系统是否正在运行,并定时进行喂狗操作。那么可以新建任务源文件iwdg_task.c和头文件iwdg_task.h,然后在osal.h文件中定义一个任务ID:IWDG_TASK_ID,这个ID就代表看门狗任务,看门狗任务的一系列事件,可以在头文件iwdg_task.h中进行定义。
源文件iwdg_task.c提供看门狗任务的初始化函数和任务执行函数,每个任务都需要提供任务的初始化函数和任务执行函数,格式如下图所示。

在函数iwdg_task_init()中,先通过register_task_array()函数,绑定IWDG_TASK_ID任务ID和iwdg_task()任务处理函数,然后再初始化STM32的看门狗外设,最后通过osal_start_timer()函数启动一个定时器,不断定时进行喂狗操作。
喂狗事件IWDG_FEED_EVENT和喂狗周期IWDG_FEED_PERIOD是在头文件iwdg_task.h中进行定义的,这里需要注意的是,每个任务事件都是按位操作的,目前每个任务下面最多定义16个事件如下图所示。

对于OSAL调度器,以下函数接口使用得比较多:

函数osal_set_event()和函数osal_clear_event()主要用来设置和清除立即执行的任务事件,当任务事件设置后,在run_system()函数中,该事件会立即执行。
函数osal_start_timer()和osal_stop_timer()主要用来设置和停止定时器任务事件,当定时时间到达后事件才会执行。osal_start_timer()的第三个参数timeout_value表示首次执行的超时时间,第四个参数reload_timeout_value表示下一次执行的重装载时间。如果要定时任务事件只触发一次,只需要把第四个参数reload_timeout_value置为0即可。
对于德州仪器TI提供的原生OSAL调度器,还有消息通信机制,不同的任务之间通过消息队列来进行数据传输,以实现任务间的数据访问。但这里为了使调度器简单易用,并没有把消息通信机制一并整合进来,使用这个调度器,多个任务间的数据仍然以共享内存的方式进行访问。
osal_msg_send()函数用来发送消息,osal_msg_recv()函数用来接收消息。

大致的流程如下:事件发生后-->被打包为消息-->存放到消息队列-->事件处理函数取出消息并进行相应操作
任务池初始化
每一个层次都有一个对应的任务来处理本层次的事务,例如MAC层对应一个MAC层的任务、网络层对应一个网络层的任务、HAL对应一个HAL的任务,以及应用层对应一个应用层的任务等,这些各个层次的任务构成一个任务池,这个任务池也就是上节课讲到的tasksEvents数组,如图所示。

这个过程跟《OSAL的任务调度原理》章节中创建和初始化任务池是类似的,它们的主要差别在于:
首先,任务池存储的数据结构不同,这里的tasksEvents是一个uint16类型的数组。
其次,tasksEvents支持存放多种类型的任务,例如MAC层的任务、网络层的任务和应用层的任务等。
最后,这里的每一个任务中都可能会包含多个待处理事件。
前面章节中曾经讲到,tasksEvents中的每个任务可以包含0个或者多个待处理的事件,而这个数组类型变量pTaskEventHandlerFn是一个函数指针类型变量,用于指向事件对应的处理函数,因此这段代码定义了一个事件处理函数数组,这个数组中的每一个元素均表示某一个层次任务的事件处理函数,例如:
- MAC层任务对应的事件处理函数是macEventLoop(),它专门处理MAC层任务中的事件。
- 网络层任务对应的事件处理函数是nwk_event_loop(),它专门处理网络层任务中的事件。
- 应用层任务对应的事件处理函数是zclSampleSw_event_loop(),它专门处理应用层任中的事件。
OTA

启用 OTA 功能:再次改造 simpleBlePeripheral 项目,在 C/C++ 选项卡下添加预处理器定义 PHY_OTA_ENABLE=1
,启用 OTA 功能,然后重新编译生成 hex 文件。
PHY_OTA_ENABLE=1
其实就是帮你把一些OTA相关代码启用了。
烧写 OTA 相关固件:打开烧写软件,app 选择新编译的 hex 文件,BOOT 选择 SDK 中 example\OTA\OTA_internal_flash\bin 下预先编译的 ota 的 boot hex。OTA 模式选择 Single No FCT
(意思是,升级的时候蓝牙app会暂停程序且覆盖写入),进入烧写模式,先 erase 擦除,再 write 烧写。烧写完成后按 reset,开发板即具备 OTA 功能。
首先用户程序要添加OTA服务,如果在运行过程中检测到OTA升级,程序就会跳转到指定地址,运行完全独立的OTA程序。OTA程序会将接收到的数据写入原来用户程序的位置,写入完成后OTA程序会跳转执行下载好的程序。在profile里面添加sbpProfile_ota.c,ota_app_service.c.直接编译下载就可以。

烧录要内存空间要选择SLB OTA模式
APP OTA
以看到下面的界面,app首先会进行连接,然后会显示 特性Enable成功SBH App已准备好,就说明我们的APP已经准备好要对Bumble进行OTA升级了,接下来我们准备升级所需要的hex16文件

我们准备一个新版本的simpleBlePeripheral,只需在代码里加一个log以区分新旧版本,我们修改 simpleBLEPeripheral.c
,在main函数里添加一个log
SimpleBLEPeripheral_Init
函数的最后加了一行
LOG("======================THIS IS NEW OTA VERSION====================\n");
重新编译,生成hex文件,然后我们在烧写工具当中使用HEX Merge选项卡,app选择新编译出来的hex文件,而BOOT选择SDK当中预先编译的ota的boot hex,然后在OTA模式选择 Single No FCT
点击右侧绿色的Hex16按钮,生成hex16文件

代码解析
添加ota_app_service.c文件。将其ota_app_AddService();在app应用程序的初始化函数bleuart_Init如下代码段添加,即完成了对该工程demo的OTA支持。
void bleuart_Init( uint8 task_id )
{
...
// Initialize GATT attributes
GGS_AddService( GATT_ALL_SERVICES ); // GAP 0xFFFFFFFF
GATTServApp_AddService( GATT_ALL_SERVICES ); // GATT attributes
DevInfo_AddService(); // Device Information Service
bleuart_AddService(on_bleuartServiceEvt);
ota_app_AddService(); //添加ota服务
at_Init(); // initial uart for AT cmd first.
...
osal_set_event( bleuart_TaskID, BUP_OSAL_EVT_START_DEVICE );
}
当 BLE从机设备和支持BLE OTA的手机APP建立连接之后,就是可以实现BLE设备的OTA升级。
其过程分为三个阶段:
1、启动OTA升级 命令OTA_CMD_START_OTA,可以启动OTA过程。
2、应用参数传递(此步骤为可选步骤) OTA_CMD_START_OTA命令的参数如果param_size字段不为0,那么自动进入参数传递状态,进行参数的传递。
3、应用固件传输以及烧写 如果之前的OTA_CMD_START_OTA命令param_size字段为0或者参数传递已经完成,就可以通过OTA_CMD_PARTITION_INFO命令开始块数据的传输。
通常一个应用固件由2~3个partition组成。目前OTA最多支持16个partition。
实现原理可以参考ota_app_service.c里的代码。
第一次点击OTA后,手机APP会跟BLE设备断开,BLE设备会从运行应用程序跳转运行OTA bootloder程序,所以其广播的蓝牙名称为PPlusOTA。如下图,我们再次使用手机APP对其连接:
NVM的使用
NVM就是用来存储设备参数的一个功能,把程序运行过程中的一些需要断电保存的数据,存储到flash里面,例如存储音量、亮度、设备号等等。
本SDK里面用到的一个NVM组件,是由系统组件osal_snv.c 调用 flash文件系统fs.c构建的,存储位置为FS区 #define NVM_BASE_ADDR 0x1103C000
- 添加源文件:


- 指定代码链接空间(不指定会出现空间不足的故障) :
在“Options for Target 'Target 1' -> Linker -> Edit”里面的添加“fs.o(+RO)”和“osal_snv.o(+RO)”

- 例子:
初始化nvm,并读取参数
osal_snv_init();
osal_snv_read(123, 2, &derver_num);//参数1:位置,参数2:长度,参数3:源
参数被修改后,进行写入保存
osal_snv_write(123, 2, &derver_num);
PHY6252 BLE
学习奉加的蓝牙工程建议先看最简单的simplebleperipheral.uvprojx工程。看这个工程可以结合PHY蓝牙连接流程.docx文档一起看。其他如bleuart_at,multi工程也有对应的文档,可以先结合文档一起看。

蓝牙名称默认是BUMBLE-FFFFFFFF
主机连接流程
- 在SimpleBLECentral_Init配置各种参数,启动START_DEVICE_EVT 事件osal_set_event(simpleBLETaskId, START_DEVICE_EVT);
- 收到START_DEVICE_EVT事件:a:GAPCentralRole_StartDevice(初始化central)b: GAPBondMgr_Register(注册配对绑定回调函数)。
- 收到GAP_DEVICE_INIT_DONE_EVENT:central初始化完成后gap层会发送此事件到应用层回调函数simpleBLECentralEventCB,然后发送CENTRAL_INIT_DONE_EVT事件 osal_set_event(simpleBLETaskId,CENTRAL_INIT_DONE_EVT);
- 收到CENTRAL_INIT_DONE_EVT:调用simpleBLECentral_DiscoverDevice函数,开始扫描广播。
- gap层每次扫描到一个广播就发送GAP_DEVICE_INFO_EVENT到应用层回调函数 simpleBLECentralEventCB,并调用simpleBLEAddDeviceInfo函数将扫描到的设备加到设备列表。
- 收到GAP_DEVICE_DISCOVERY_EVENT:central扫描结束收到此事件,可以在此事件做相应处理,比如打印扫描到设备地址,地址类型等。然后调用osal_set_event(simpleBLETaskId,CENTRAL_DISCOVER_DEVDONE_EVT);,发送CENTRAL_DISCOVER_DEVDONE_EVT事件。
- 收到CENTRAL_DISCOVER_DEVDONE_EVT:收到此事件后调用simpleBLECentral_LinkDevice GAPCentralRole_EstablishLink连接设备。
- 收到GAP_LINK_ESTABLISHED_EVENT:收到此事件时代表连接成功。可以做后续的处理,比如更新MTU,设置PHY mode等,这些根据具体需求来做调用。然后调用osal_start_timerEx( simpleBLETaskId, START_DISCOVERY_SERVICE_EVT,DEFAULT_SVC_DISCOVERY_DELAY );
- 收到DEFAULT_SVC_DISCOVERY_DELAY:收到此事件后调用simpleBLECentralStartDiscoveryService GATT_DiscAllPrimaryServices函数来发现peripheral设备的服务和特征。
- 收到gatt层发送的SYS_EVENT_MSG事件 simpleBLECentral_ProcessOSALMsg GATT_MSG_EVENT simpleBLECentralProcessGATTMsg simpleBLEGATTDiscoveryEvent,最后到simpleBLEGATTDiscoveryEvent函数,此函数会将发现到服务和特征保存到SimpleClientInfo变量中。
到此为一个基本的连接过程,后面的配对绑定在连接后面处理,原理也是一样的:
首先在SimpleBLECentral_Init函数配置初始化函数时设置pairMode 为GAPBOND_PAIRING_MODE_INITIATE;
然后在START_DEVICE_EVT事件处向gapbondmgr层注册配对绑定回调函数GAPBondMgr_Register((gapBondCBs_t*)&simpleBLEBondCB);
在连接成功后central会自动发起配对。配对过程大部分流程都由gapbondmgr完成,如果在初始化时设置为不需要密钥输入,则不需要应用层参与,如果设置为需要密钥输入,则gapbondmgr会回调simpleBLECentralPasscodeCB函数让用户输入密钥和显示密钥。在配对过程中会在发送事件到simpleBLECentralPairStateCB,如GAPBOND_PAIRING_STATE_STARTED,GAPBOND_PAIRING_STATE_COMPLETE,GAPBOND_PAIRING_STATE_BONDED,让用户做相应处理。
从机连接流程
- 在SimpleBLEPeripheral_Init配置各种参数,启动START_DEVICE_EVT 事件osal_set_event(simpleBLETaskId, START_DEVICE_EVT);
- 收到START_DEVICE_EVT事件:a: GAPRole_StartDevice(初始化peripheral)b: GAPBondMgr_Register(注册配对绑定回调函数)。
- 在gapRole_ProcessGAPMsg收到GAP_DEVICE_INIT_DONE_EVENT:收到此事件说明初始化成功,然后调用GAP_UpdateAdvertisingData更新广播数据。
- 收到GAP_ADV_DATA_UPDATE_DONE_EVENT事件:继续调用GAP_UpdateAdvertisingData更新扫描回复数据。
- 收到GAP_ADV_DATA_UPDATE_DONE_EVENT事件:调用osal_set_event( gapRole_TaskID, START_ADVERTISING_EVT );
- 在GAPRole_ProcessEvent收到START_ADVERTISING_EVT事件:然后调用GAP_MakeDiscoverable函数开始广播。
- 在gapRole_ProcessGAPMsg收到GAP_MAKE_DISCOVERABLE_DONE_EVENT事件:在此事件下更新相关状态。并等待连接请求,连接过程会由gap层完成,连接成功后会发送相应事件。
- 收到GAP_LINK_ESTABLISHED_EVENT事件:说明连接成功。连接成功后可以做后续处理,如连接参数更新,读rssi,开始配对绑定流程等,具体根据需求来调用相关函数。
关于配对绑定流程涉及到的事件参照上文主机配对绑定流程。

SimpleBlePeripheral工程解析
【PHY6222】simpleBLEPeripheral剖析-CSDN
st17h66低功耗蓝牙soc开发(5)——simplebleperipheral私有服务修改-爱代码爱编程
这个工程是一个基于Phyplus PHY6222(52)芯片的蓝牙低功耗(BLE)外设示例项目。
蓝牙广播数据的基本格式是 LTV,由 Length(1 字节 )、Type(1 字节 )、Value(值 )三部分构成 ,用于规范蓝牙广播数据包里有效数据的组织形式 。
和我们应用最贴近的设置是添加服务的函数:

不同服务有各自的添加函数。而不同服务实质上是一个个属性数组。
1. 工程概述
这是一个典型的BLE外设应用程序,实现了基本的BLE广播、连接和数据交换功能。工程基于OSAL(操作系统抽象层)构建,采用事件驱动的编程模型。
2. 文件结构
主要源代码文件位于source目录下:
- main.c: 系统入口点,初始化硬件和OSAL系统
- SimpleBLEPeripheral_Main.c: 应用程序主入口,包含app_main函数
- OSAL_SimpleBLEPeripheral.c: OSAL任务初始化,定义了任务优先级和事件处理函数
- simpleBLEPeripheral.c: 主应用程序逻辑,包含初始化和事件处理函数
- simpleBLEPeripheral.h: 应用程序相关的宏定义和函数声明
- sbpProfile_ota.c/h: 自定义GATT服务和特性的实现
- halPeripheral.c/h: 硬件抽象层,处理GPIO、按键和LED等外设
3. 系统架构
OSAL任务结构
系统基于OSAL任务调度,在OSAL_SimpleBLEPeripheral.c中定义了以下任务(按优先级排序):
- LL_ProcessEvent - 链路层事件处理
- HCI_ProcessEvent - 主机控制器接口事件处理
- L2CAP_ProcessEvent - 逻辑链路控制与适配协议事件处理
- SM_ProcessEvent - 安全管理器事件处理
- GAP_ProcessEvent - 通用访问配置文件事件处理
- GATT_ProcessEvent - 通用属性配置文件事件处理
- GAPRole_ProcessEvent - GAP角色事件处理
- GAPBondMgr_ProcessEvent - 绑定管理器事件处理(可选)
- GATTServApp_ProcessEvent - GATT服务应用事件处理
- SimpleBLEPeripheral_ProcessEvent - 应用程序事件处理
应用程序初始化流程
- 系统启动时,main.c中的app_main()函数被调用
- 初始化操作系统(osal_init_system)
- 设置电源管理模式(osal_pwrmgr_device)
- 启动OSAL系统(osal_start_system)
- OSAL初始化各个任务(osalInitTasks)
- 应用程序初始化(SimpleBLEPeripheral_Init)
4. BLE功能实现
广播实现
工程实现了标准的BLE广播功能:
- 在simpleBLEPeripheral.c中定义了广播数据(advertData)和扫描响应数据(scanRspData)
- 支持配置广播间隔、广播类型和广播通道
- 支持iBeacon格式的广播数据
GATT服务
工程实现了一个自定义的GATT服务(sbpProfile_ota.c),包含以下特性:
- 特性4(SIMPLEPROFILE_CHAR4): 用于电源控制,支持读写
- 特性5(SIMPLEPROFILE_CHAR5): 用于复位,支持读写和无响应写
- 特性6(SIMPLEPROFILE_CHAR6): 用于通知,支持读和通知
- 特性7(SIMPLEPROFILE_CHAR7): 支持读和无响应写
连接参数更新
工程支持BLE连接参数更新:
- 最小连接间隔: 30ms (DEFAULT_DESIRED_MIN_CONN_INTERVAL)
- 最大连接间隔: 1000ms (DEFAULT_DESIRED_MAX_CONN_INTERVAL)
- 从设备延迟: 0 (DEFAULT_DESIRED_SLAVE_LATENCY)
- 监督超时: 5000ms (DEFAULT_DESIRED_CONN_TIMEOUT)
5. 硬件抽象层
halPeripheral.c/h文件实现了硬件抽象层,包括:
- GPIO控制
- 按键处理
- LED控制
- UART通信
- 命令行接口(CLI)
6. 高级功能
工程还包含一些高级功能和测试选项:
- OTA固件升级支持(PHY_OTA_ENABLE)
- 可解析私有地址测试(APP_CFG_RPA_TEST)
- 延迟测试(LATENCY_TEST)
- 动态时钟变更测试(DYNAMIC_CLK_CHG_TEST)
- Flash测试(DBG_SPIF_TEST)
- RTC测试(DBG_RTC_TEST)
7. 事件处理
应用程序通过SimpleBLEPeripheral_ProcessEvent函数处理各种事件:
- SBP_START_DEVICE_EVT: 启动设备
- SBP_PERIODIC_EVT: 周期性事件,用于发送通知
- SBP_RESET_ADV_EVT: 重置广播
- 其他测试和调试事件
总结
这个SimpleBlePeripheral工程是一个完整的BLE外设示例,展示了PHY6222(52)芯片的BLE功能,包括广播、连接、GATT服务和各种高级功能。它采用OSAL事件驱动架构,提供了良好的代码结构和功能模块化,可以作为开发自定义BLE应用的良好起点。
PHY6252 BLE MESH
PhyMesh 使用文档
PHY6252-SDK-blemesh
PHY6222 BLE Mesh Light 工程功能详细介绍
这是一个基于PHY6222芯片的BLE Mesh智能灯光控制系统工程,实现了完整的Mesh网络节点功能。
🏗️ 工程架构
核心组件:
- 主控芯片:PHY6222 (ARM Cortex-M0内核)
- 协议栈:基于Ethermind的BLE Mesh协议栈
- 开发环境:支持Keil MDK和GCC编译
- 内存配置:7KB大堆内存,2KB Mesh专用堆内存
🌐 BLE Mesh网络功能
1. Mesh协议栈支持
- 完整的BLE Mesh 1.0协议栈实现
- 支持7层Mesh协议架构(承载层→网络层→传输层→访问层→模型层)
- 自动网络配置和设备入网(Provisioning)
- 支持Relay、Proxy、Friend、Low Power Node功能
2. 网络管理
- 自动设备发现和配网
- 网络密钥和应用密钥管理
- 心跳机制和网络健康监控
- 支持多子网和密钥更新
💡 智能灯光控制功能
1. 多种灯光模型支持
- Generic OnOff Model:基础开关控制
- Light HSL Model:色相、饱和度、亮度控制
- Light CTL Model:色温和亮度控制
- Scene Model:场景模式存储和切换
- Vendor Model:自定义厂商功能
2. 硬件控制
- GPIO引脚定义:
- QFN32封装:红(P31)、绿(P32)、蓝(P33)
- 其他封装:红(P2)、绿(P3)、蓝(P7)
- PWM控制:支持精确的亮度和颜色调节
- LED状态指示:支持多种颜色和闪烁模式
3. 灯光效果
- 8种预定义颜色:关闭、红、绿、蓝、青、黄、品红、白
- 多种闪烁模式:极快、快速、慢速、极慢
- 平滑过渡和渐变效果
- 场景记忆和恢复功能
🔧 设备管理功能
1. 配网和绑定
- 支持PB-ADV和PB-GATT配网方式
- 自动应用密钥绑定
- 设备信息服务(Device Information Service)
- OTA固件升级支持
2. 状态管理
- 设备状态持久化存储
- 网络配置信息保存
- 断电恢复功能
- 工厂重置支持
sdk代码参考
应用流程
● 定义功能+注册模块(客户端注册客户端的,服务端注册服务端的)
○ 注册回调(用来处理接收响应)
● 填目标地址,密码appkey
● 调用api发送
API位置
● MS_config_api.h
● MS_generic_onoff_api.h
● vendormodel_client.h
● vendormodel_server.h
客户端(元素和模型的注册):
retval = MS_access_create_node(&node_id);
/* Register Element */
/**
TBD: Define GATT Namespace Descriptions from
https://www.bluetooth.com/specifications/assigned-numbers/gatt-namespace-descriptors
Using 'main' (0x0106) as Location temporarily.
*/
element.loc = 0x0106;
retval = MS_access_register_element
(
node_id,
&element,
&element_handle
);
if (API_SUCCESS == retval)
{
/* Register Configuration model client */
retval = UI_register_config_model_client(element_handle);
}
if (API_SUCCESS == retval)
{
/* Register Generic OnOff model client */
retval = UI_register_generic_onoff_model_client(element_handle);
}
#ifdef USE_VENDORMODEL
if (API_SUCCESS == retval)
{
/* Register Vendor Defined model server */
retval = UI_register_vendor_defined_model_client(element_handle);
}
服务端(元素和模型的注册):
etval = MS_access_create_node(&node_id);
/* Register Element */
/**
TBD: Define GATT Namespace Descriptions from
https://www.bluetooth.com/specifications/assigned-numbers/gatt-namespace-descriptors
Using 'main' (0x0106) as Location temporarily.
*/
element.loc = 0x0106;
retval = MS_access_register_element
(
node_id,
&element,
&element_handle
);
if (API_SUCCESS == retval)
{
/* Register foundation model servers */
retval = UI_register_foundation_model_servers(element_handle);
}
if (API_SUCCESS == retval)
{
/* Register Generic OnOff model server */
retval = UI_register_generic_onoff_model_server(element_handle);
}
#ifdef USE_HSL
if (API_SUCCESS == retval)
{
/* Register Light Lightness model server */
retval = UI_register_light_hsl_model_server(element_handle);
}
#endif
#ifdef USE_CTL
if (API_SUCCESS == retval)
{
/* Register Light Lightness model server */
retval = UI_register_light_ctl_model_server(element_handle);
}
#endif
#ifdef USE_SCENE
if (API_SUCCESS == retval)
{
/* Register Light Scene model server */
retval = UI_register_scene_model_server(element_handle);
}
#endif
#ifdef USE_VENDORMODEL
if (API_SUCCESS == retval)
{
/* Register Vendor Defined model server */
retval = UI_register_vendor_defined_model_server(element_handle);
}
地址的分配(gateway节点):
获取地址列表,增加地址后再发送到配网中的设备
case PROV_EVT_PROVDATA_INFO_REQ:
CONSOLE_OUT ("Recvd PROV_EVT_PROVDATA_INFO_REQ\n");
CONSOLE_OUT ("Status - 0x%04X\n", event_result);
/* Send Provisioning Data */
CONSOLE_OUT ("Send Provisioning Data...\n");
/* Update the next device address if provisioned devices are present in database */
retval = MS_access_cm_get_prov_devices_list
(
&prov_dev_list[0],
&num_entries,
&pointer_entries
);
if ((API_SUCCESS == retval) /*&&
(0 != num_entries)*/)
{
// UI_prov_data.uaddr = prov_dev_list[num_entries - 1].uaddr +
// prov_dev_list[num_entries - 1].num_elements;
UI_prov_data.uaddr = pointer_entries;//地址分配
}
printf("Updating Provisioning Start Addr to 0x%04X\n", UI_prov_data.uaddr);
//Get network key
MS_access_cm_get_netkey_at_offset(0,0,UI_prov_data.netkey);
//Get key refresh state
MS_access_cm_get_key_refresh_phase(0,&key_refresh_state);
key_refresh_state = (MS_ACCESS_KEY_REFRESH_PHASE_2 == key_refresh_state) ? 0x01 : 0x00;
UI_prov_data.ivindex = ms_iv_index.iv_index;
UI_prov_data.flags = ((ms_iv_index.iv_update_state & 0x01) << 1) | key_refresh_state;
blebrr_scan_pl(FALSE); //by hq
retval = MS_prov_data (phandle, &UI_prov_data);//发送给配网的设备
CONSOLE_OUT ("Retval - 0x%04X\n", retval);
break;
Publish和Subscribe
Publish(gateway节点):
UI_prov_data.uaddr = 0xCFFF;
UI_set_publish_address(UI_prov_data.uaddr, UI_generic_onoff_client_model_handle,MS_FALSE);
UI_set_publish_address(UI_prov_data.uaddr, UI_vendor_defined_client_model_handle,MS_FALSE);
Subscribe(light节点):
void vm_subscriptiong_delete (MS_NET_ADDR addr)
{
MS_ACCESS_ADDRESS sub_addr;
sub_addr.use_label=0;
sub_addr.addr=addr;
MS_access_ps_store_disable(MS_TRUE);
MS_access_cm_delete_model_subscription(UI_generic_onoff_server_model_handle,&sub_addr);
#ifdef USE_LIGHTNESS
MS_access_cm_delete_model_subscription(UI_light_lightness_server_model_handle,&sub_addr);
#endif
#ifdef USE_CTL
MS_access_cm_delete_model_subscription(UI_light_ctl_server_model_handle,&sub_addr);
MS_access_cm_delete_model_subscription(UI_light_ctl_setup_server_model_handle,&sub_addr);
#endif
#ifdef USE_HSL
MS_access_cm_delete_model_subscription(UI_light_hsl_server_model_handle,&sub_addr);
MS_access_cm_delete_model_subscription(UI_light_hsl_setup_server_model_handle,&sub_addr);
#endif
#ifdef USE_SCENE
MS_access_cm_delete_model_subscription(UI_scene_server_model_handle,&sub_addr);
MS_access_cm_delete_model_subscription(UI_scene_setup_server_model_handle,&sub_addr);
#endif
MS_access_ps_store_disable(MS_FALSE);
MS_access_ps_store_all_record();
}
设备密钥devicekey
是在配网的过程中,由配网器和入网设备共同生成的,每个节点只有1个,专门给config模型使用的(配置用的)!服务端和客户端一致,才能入网。
流程:
- 客户端(gateway节点)
a. 生成devicekey
b. 获取devicekey(根据需要配置的节点“地址”来获取)
c. 在config模型的publish绑定中使用
d. 发送config数据
- 服务端(light节点)
a. 生成devicekey
b. 默认绑定config模型,可以查看
c. 根据客户端发来的命令,回调处理
以下代码是一些代码片段,需要了解完整内容的话,可以在工程里面进行全局搜索!
客户端(gateway节点):
1.定义生成
#define UI_DEVICE_UUID {0x25, 0x22, 0x42, 0x54, 0x50, 0x02, 0x10, 0x03, 0x22, 0x14, 0x52, 0x81, 0x11, 0x14, 0x90, 0x22}
2.获取devicekey
retval = MS_access_cm_get_device_key_handle
(
publish_info.addr.addr,
&dev_key_handle
);
3.在设置publish时,用devicekey绑定定config模型
retval = MS_access_cm_set_model_publication
(
model_handle,
&publish_info
);
整体参考:
//void UI_set_publish_address(UINT16 addr, MS_ACCESS_MODEL_HANDLE model_handle,UINT8 config_mode)里面的片段
if(config_mode)
{
publish_info.remote = MS_FALSE;
retval = MS_access_cm_get_device_key_handle
(
publish_info.addr.addr,
&dev_key_handle//重点
);
if (API_SUCCESS == retval)
{
publish_info.appkey_index = MS_CONFIG_LIMITS(MS_MAX_APPS) + dev_key_handle;//重点
CONSOLE_OUT("DevKey -> AppKey Index: 0x%04X\n", publish_info.appkey_index);
}
}
retval = MS_access_cm_set_model_publication
(
model_handle,
&publish_info
);
4.发送config数据(在publish中配置好了目标地址和devicekey,调用MS_config_api.h里面的API直接发送命令即可)
/* ----------------------------------------- Functions */
/* Model Client - Configuration Models */
/* Send Config Composition Data Get */
void UI_config_client_get_composition_data(UCHAR page)
{
API_RESULT retval;
ACCESS_CONFIG_COMPDATA_GET_PARAM param;
CONSOLE_OUT
("Send Config Composition Data Get\n");
param.page = page;
retval = MS_config_client_composition_data_get(¶m);
CONSOLE_OUT
("Retval - 0x%04X\n", retval);
}
服务端(light节点):
1.定义生成
/** Unprovisioned device identifier */
//DECL_STATIC PROV_DEVICE_S UI_lprov_device =
PROV_DEVICE_S UI_lprov_device =
{
/** UUID */
{0x25, 0x22, 0x42, 0x54, 0x50, 0x02, 0x10, 0x03, 0x22, 0x14, 0x52, 0x81, 0x11, 0x14, 0x90, 0x22},
/** OOB Flag */
0x00,
/**
Encoded URI Information
For example, to give a web address, "https://www.abc.com"
the URI encoded data would be -
0x17 0x2F 0x2F 0x77 0x77 0x77 0x2E 0x61 0x62 0x63 0x2E 0x63 0x6F 0x6D
where 0x17 is the URI encoding for https:
*/
NULL
};
2.绑定(默认绑定config模型)
3.回调处理
API_RESULT UI_app_config_server_callback
应用密钥appkey
流程
- 客户端(gateway节点)
a. 存储appkey
b. 获取appkey
c. 自身模型绑定appkey,用于认证“对端设备”的appkey(这里的绑定类似于给功能模型加上密码)
d. 在publish绑定里面使用appkey,用于给对端设备作认证(相当于填密码)
e. 发送publish。
- 服务端(light节点)
a. 获取appkey
b. 绑定到模型上
c. 根据客户端发来的命令,回调处理
客户端(gateway节点):
1.添加appkey到本地数据库
MS_access_cm_add_appkey
(
0, /* subnet_handle */
param.appkey_index, /* appkey_index */
¶m.appkey[0] /* app_key */
);
2.通过句柄handle获取appkey数据(这里只是用来打印显示)
retval = MS_access_cm_get_app_key
(
handle,
&key,
&aid
);
CONSOLE_OUT("App Key[0x%02X]: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X\r\n",
handle, key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7],
key[8], key[9], key[10], key[11], key[12], key[13], key[14], key[15]);
3.通过句柄handle来绑定第1步存储本地数据库里面的那个appkey(服务端的模型也要绑定)
retval=MS_access_bind_model_app(UI_generic_onoff_client_model_handle, handle);
#ifdef USE_VENDORMODEL
retval=MS_access_bind_model_app(UI_vendor_defined_client_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_vendor_defined_client_model_handle,handle);
#endif
4.在注册publish时,用appkey绑定应用模型(只需调用索引即可)
/* Set the Publish address for onoff and vendor model Client */
UI_prov_data.uaddr = 0xCFFF;
UI_set_publish_address(UI_prov_data.uaddr, UI_generic_onoff_client_model_handle,MS_FALSE);
UI_set_publish_address(UI_prov_data.uaddr, UI_vendor_defined_client_model_handle,MS_FALSE);
if(config_mode)
{
publish_info.remote = MS_FALSE;
retval = MS_access_cm_get_device_key_handle
(
publish_info.addr.addr,
&dev_key_handle
);
if (API_SUCCESS == retval)
{
publish_info.appkey_index = MS_CONFIG_LIMITS(MS_MAX_APPS) + dev_key_handle;
CONSOLE_OUT("DevKey -> AppKey Index: 0x%04X\n", publish_info.appkey_index);
}
}
else
{
publish_info.remote = MS_TRUE;
publish_info.appkey_index = 0;//重点
CONSOLE_OUT("AppKey Index: 0x%04X\n", publish_info.appkey_index);
}
//重点
retval = MS_access_cm_set_model_publication
(
model_handle,
&publish_info
);
5.发送publish
void UI_generic_onoff_set(UCHAR state)
{
API_RESULT retval;
MS_GENERIC_ONOFF_SET_STRUCT param;
CONSOLE_OUT
("Send Generic Onoff Set\n");
param.onoff = state;
param.tid = 0;
param.optional_fields_present = 0x00;
retval = MS_generic_onoff_set(¶m);//重点
CONSOLE_OUT
("Retval - 0x%04X\n", retval);
}
#define MS_generic_onoff_set(param) \
MS_generic_onoff_client_send_reliable_pdu \
(\
MS_ACCESS_GENERIC_ONOFF_SET_OPCODE,\
param,\
MS_ACCESS_GENERIC_ONOFF_STATUS_OPCODE\
)
服务端(light节点):
1.获取appkey
retval = MS_access_cm_get_app_key
(
handle,
&key,
&aid
);
2.绑定appkey
retval=MS_access_bind_model_app(UI_generic_onoff_server_model_handle, handle);
#ifdef USE_LIGHTNESS
retval=MS_access_bind_model_app(UI_light_lightness_server_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_light_lightness_server_model_handle,handle);
#endif
#ifdef USE_CTL
retval=MS_access_bind_model_app(UI_light_ctl_server_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_light_ctl_server_model_handle,handle);
#endif
#ifdef USE_HSL
retval=MS_access_bind_model_app(UI_light_hsl_server_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_light_hsl_server_model_handle,handle);
#endif
#ifdef USE_SCENE
retval=MS_access_bind_model_app(UI_scene_server_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_scene_server_model_handle,handle);
#endif
#ifdef USE_VENDORMODEL
retval=MS_access_bind_model_app(UI_vendor_defined_server_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_vendor_defined_server_model_handle,handle);
#endif
3.接收回调处理
static API_RESULT UI_generic_onoff_server_cb
(
/* IN */ MS_ACCESS_MODEL_REQ_MSG_CONTEXT* ctx,
/* IN */ MS_ACCESS_MODEL_REQ_MSG_RAW* msg_raw,
/* IN */ MS_ACCESS_MODEL_REQ_MSG_T* req_type,
/* IN */ MS_ACCESS_MODEL_STATE_PARAMS* state_params,
/* IN */ MS_ACCESS_MODEL_EXT_PARAMS* ext_params
)
{
MS_STATE_GENERIC_ONOFF_STRUCT param;
MS_ACCESS_MODEL_STATE_PARAMS current_state_params;
API_RESULT retval;
retval = API_SUCCESS;
/* Check message type */
if (MS_ACCESS_MODEL_REQ_MSG_T_GET == req_type->type)
{
CONSOLE_OUT("[GENERIC_ONOFF] GET Request.\n");
UI_generic_onoff_model_state_get(state_params->state_type, 0, ¶m, 0);
current_state_params.state_type = state_params->state_type;
current_state_params.state = ¶m;
/* Using same as target state and remaining time as 0 */
}
else if (MS_ACCESS_MODEL_REQ_MSG_T_SET == req_type->type)
{
CONSOLE_OUT("[GENERIC_ONOFF] SET Request.\n");
retval = UI_generic_onoff_model_state_set(state_params->state_type, 0, (MS_STATE_GENERIC_ONOFF_STRUCT*)state_params->state, 0);
current_state_params.state_type = state_params->state_type;
current_state_params.state = (MS_STATE_GENERIC_ONOFF_STRUCT*)state_params->state;
}
/* See if to be acknowledged */
if (0x01 == req_type->to_be_acked)
{
CONSOLE_OUT("[GENERIC_ONOFF] Sending Response.\n");
/* Parameters: Request Context, Current State, Target State (NULL: to be ignored), Remaining Time (0: to be ignored), Additional Parameters (NULL: to be ignored) */
retval = MS_generic_onoff_server_state_update(ctx, ¤t_state_params, NULL, 0, NULL);
}
return retval;
}
config模型的应用(配置应用)
config模型,专门用来给节点做“配置”用的,例如,订阅、发布、绑定appkey等配置。

客户端:
初始化
注册config client模型
/* Register Configuration model client */
retval = UI_register_config_model_client(element_handle);
设置config client发布地址和devicekey
/* Set the Publish address for Config Client */
UI_set_publish_address(UI_prov_data.uaddr, UI_config_client_model_handle,MS_TRUE);
发送获取Composition数据命令
/* Get the Composition data */
UI_config_client_get_composition_data(0x00);
服务端:
初始化
注册config server模型
/* Register foundation model servers */
retval = UI_register_foundation_model_servers(element_handle);
APP_config_server_CB_init(UI_app_config_server_callback);
获取devicekey?
UI_sample_get_device_key();
不处理Composition命令,返回Composition状态
null
客户端:
发送添加appkey命令
/* Add Appkey */
UI_config_client_appkey_add(0, 0, UI_appkey);
服务端:
绑定appkey到各个模型
case MS_ACCESS_CONFIG_APPKEY_ADD_OPCODE:
#ifdef EASY_BOUNDING
blebrr_scan_pl(FALSE);
ms_provisioner_addr = saddr;
vm_subscriptiong_binding_cb();//重要
// ms_provisioner_addr = saddr;
#if (CFG_HEARTBEAT_MODE)
UI_trn_set_heartbeat_subscription(saddr);
#endif
#else
// CONSOLE_OUT("Stop timer\n");
ms_provisioner_addr = saddr;
// EM_stop_timer(&procfg_timer_handle);
blebrr_prov_started = MS_FALSE;
#endif
break;
客户端:
处理appkey添加完成响应回调
case MS_ACCESS_CONFIG_APPKEY_STATUS_OPCODE:
{
CONSOLE_OUT(
"MS_ACCESS_CONFIG_APPKEY_STATUS_OPCODE\n");
#ifdef EASY_BOUNDING
UI_SET_RAW_DATA_DST_ADDR(UI_prov_data.uaddr);
MS_access_cm_set_transmit_state(MS_NETWORK_TX_STATE, (0<<3)|0);
/* Set provision started */
blebrr_prov_started = MS_FALSE;
EM_stop_timer(&procfg_timer_handle);
CONSOLE_OUT(
"PROVISION AND CONFIG DONE!!!\n");
// UI_config_proxy_set(0);
/* Send a Generic ON */
#else
/* Bind the model to Appkey */
UI_config_client_model_app_bind(UI_prov_data.uaddr, 0, MS_ACCESS_MODEL_TYPE_SIG, MS_MODEL_ID_GENERIC_ONOFF_SERVER);
#endif
}
generic模型的应用示例(订阅、发布)
generic模型,就是sig mesh协议里面规定的一套通用功能模型,各个厂家只要按照协议去设计设备功能,设备之间即可兼容使用!
下面就用元素0x0106里面的“通用开关模型(generic_onoff_model)”来举例,了解并记录SDK里面,具体是如何应用generic模型的!
订阅、发布模式:

单播模式:

客户端(gateway节点):
初始化
if (API_SUCCESS == retval)
{
/* Register Configuration model client */
retval = UI_register_config_model_client(element_handle);
}
if (API_SUCCESS == retval)
{
/* Register Generic OnOff model client */
retval = UI_register_generic_onoff_model_client(element_handle);
}
#ifdef USE_VENDORMODEL
if (API_SUCCESS == retval)
{
/* Register Vendor Defined model server */
retval = UI_register_vendor_defined_model_client(element_handle);
}
#endif
UI_config_client_appkey_binding(0,0,UI_appkey);//重点
printf("0x%08X 0x%08X 0x%02X\n",ms_iv_index.iv_index,ms_iv_index.iv_expire_time,ms_iv_index.iv_update_state);
UI_prov_data.uaddr = 0x0001;
//Get network key
retval = MS_access_cm_get_netkey_at_offset(0,0,UI_prov_data.netkey);
if(retval == ACCESS_NO_MATCH) //if can't find netkey,generate it
{
UI_netkey_generate(UI_prov_data.netkey);
}
//Get key refresh state
MS_access_cm_get_key_refresh_phase(0,&key_refresh_state);
key_refresh_state = (MS_ACCESS_KEY_REFRESH_PHASE_2 == key_refresh_state) ? 0x01 : 0x00;
UI_prov_data.ivindex = ms_iv_index.iv_index;
UI_prov_data.flags = ((ms_iv_index.iv_update_state & 0x01) << 1) | key_refresh_state;
retval = UI_set_provision_data(UI_prov_data.uaddr);
/* Set the Publish address for onoff and vendor model Client */
UI_prov_data.uaddr = 0xCFFF;//重点
UI_set_publish_address(UI_prov_data.uaddr, UI_generic_onoff_client_model_handle,MS_FALSE);//重点
UI_set_publish_address(UI_prov_data.uaddr, UI_vendor_defined_client_model_handle,MS_FALSE);
发送“generic_onoff_set”命令
void UI_generic_onoff_set(UCHAR state)
{
API_RESULT retval;
MS_GENERIC_ONOFF_SET_STRUCT param;
CONSOLE_OUT
("Send Generic Onoff Set\n");
param.onoff = state;
param.tid = 0;
param.optional_fields_present = 0x00;
retval = MS_generic_onoff_set(¶m);//重点
CONSOLE_OUT
("Retval - 0x%04X\n", retval);
}
#define MS_generic_onoff_set(param) \
MS_generic_onoff_client_send_reliable_pdu \
(\
MS_ACCESS_GENERIC_ONOFF_SET_OPCODE,\//重点
param,\
MS_ACCESS_GENERIC_ONOFF_STATUS_OPCODE\
)
处理generic_onoff回调
API_RESULT UI_generic_onoff_client_cb
(
/* IN */ MS_ACCESS_MODEL_HANDLE* handle,
/* IN */ UINT32 opcode,
/* IN */ UCHAR* data_param,
/* IN */ UINT16 data_len
)
{
API_RESULT retval;
retval = API_SUCCESS;
CONSOLE_OUT (
"[GENERIC_ONOFF_CLIENT] Callback. Opcode 0x%04X\n", opcode);
appl_dump_bytes(data_param, data_len);
switch(opcode)
{
case MS_ACCESS_GENERIC_ONOFF_STATUS_OPCODE://重点
{
CONSOLE_OUT(
"MS_ACCESS_GENERIC_ONOFF_STATUS_OPCODE\n");
}
break;
}
return retval;
}
服务端(light节点):
初始化
if (API_SUCCESS == retval)
{
/* Register Generic OnOff model server */
retval = UI_register_generic_onoff_model_server(element_handle);
}
绑定appkey
API_RESULT UI_sample_binding_app_key(void)
{
MS_APPKEY_HANDLE handle;
UINT8* key;
UINT8 aid;
DECL_CONST UINT8 t_key[16] = {0};
API_RESULT retval;
CONSOLE_OUT("Fetching App Key for Handle 0x0000\n");
handle = 0x0000;
retval = MS_access_cm_get_app_key
(
handle,
&key,
&aid
);
/* Check Retval. Print App Key */
if (API_SUCCESS == retval)
{
CONSOLE_OUT("App Key[0x%02X]: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X\r\n",
handle, key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7],
key[8], key[9], key[10], key[11], key[12], key[13], key[14], key[15]);
if (0 == EM_mem_cmp(key, t_key, 16))
{
/* NO AppKey Bound */
retval = API_FAILURE;
}
else
{
/* Found a Valid App Key */
/* Keeping the retval as API_SUCCESS */
retval=MS_access_bind_model_app(UI_generic_onoff_server_model_handle, handle);//重点
#ifdef USE_LIGHTNESS
retval=MS_access_bind_model_app(UI_light_lightness_server_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_light_lightness_server_model_handle,handle);
#endif
#ifdef USE_CTL
retval=MS_access_bind_model_app(UI_light_ctl_server_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_light_ctl_server_model_handle,handle);
#endif
#ifdef USE_HSL
retval=MS_access_bind_model_app(UI_light_hsl_server_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_light_hsl_server_model_handle,handle);
#endif
#ifdef USE_SCENE
retval=MS_access_bind_model_app(UI_scene_server_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_scene_server_model_handle,handle);
#endif
#ifdef USE_VENDORMODEL
retval=MS_access_bind_model_app(UI_vendor_defined_server_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_vendor_defined_server_model_handle,handle);
#endif
}
}
#ifdef EASY_BOUNDING
//Provision ok,stop provision/config timeout handler by hq
CONSOLE_OUT("Stop timer\n");
EM_stop_timer(&procfg_timer_handle);
blebrr_prov_started = MS_FALSE;
#endif
return retval;
}
订阅0xCFFF
vm_subscriptiong_add (0xCFFF);//重点
void vm_subscriptiong_add (MS_NET_ADDR addr)
{
CONSOLE_OUT("vm_subscriptiong_add:%x\n",addr);
MS_ACCESS_ADDRESS sub_addr;
sub_addr.use_label=0;
sub_addr.addr=addr;
MS_access_ps_store_disable(MS_TRUE);
MS_access_cm_add_model_subscription(UI_generic_onoff_server_model_handle,&sub_addr);
#ifdef USE_LIGHTNESS
MS_access_cm_add_model_subscription(UI_light_lightness_server_model_handle,&sub_addr);
#endif
#ifdef USE_CTL
MS_access_cm_add_model_subscription(UI_light_ctl_server_model_handle,&sub_addr);
MS_access_cm_add_model_subscription(UI_light_ctl_setup_server_model_handle,&sub_addr);
#endif
#ifdef USE_HSL
MS_access_cm_add_model_subscription(UI_light_hsl_server_model_handle,&sub_addr);
MS_access_cm_add_model_subscription(UI_light_hsl_setup_server_model_handle,&sub_addr);
#endif
#ifdef USE_SCENE
MS_access_cm_add_model_subscription(UI_scene_server_model_handle,&sub_addr);
MS_access_cm_add_model_subscription(UI_scene_setup_server_model_handle,&sub_addr);
#endif
MS_access_ps_store_disable(MS_FALSE);
MS_access_ps_store_all_record();
}
回调处理
static API_RESULT UI_generic_onoff_server_cb
(
/* IN */ MS_ACCESS_MODEL_REQ_MSG_CONTEXT* ctx,
/* IN */ MS_ACCESS_MODEL_REQ_MSG_RAW* msg_raw,
/* IN */ MS_ACCESS_MODEL_REQ_MSG_T* req_type,
/* IN */ MS_ACCESS_MODEL_STATE_PARAMS* state_params,
/* IN */ MS_ACCESS_MODEL_EXT_PARAMS* ext_params
)
{
MS_STATE_GENERIC_ONOFF_STRUCT param;
MS_ACCESS_MODEL_STATE_PARAMS current_state_params;
API_RESULT retval;
retval = API_SUCCESS;
/* Check message type */
if (MS_ACCESS_MODEL_REQ_MSG_T_GET == req_type->type)
{
CONSOLE_OUT("[GENERIC_ONOFF] GET Request.\n");
UI_generic_onoff_model_state_get(state_params->state_type, 0, ¶m, 0);
current_state_params.state_type = state_params->state_type;
current_state_params.state = ¶m;
/* Using same as target state and remaining time as 0 */
}
else if (MS_ACCESS_MODEL_REQ_MSG_T_SET == req_type->type)//重点
{
CONSOLE_OUT("[GENERIC_ONOFF] SET Request.\n");
retval = UI_generic_onoff_model_state_set(state_params->state_type, 0, (MS_STATE_GENERIC_ONOFF_STRUCT*)state_params->state, 0);//重点
current_state_params.state_type = state_params->state_type;
current_state_params.state = (MS_STATE_GENERIC_ONOFF_STRUCT*)state_params->state;
}
/* See if to be acknowledged */
if (0x01 == req_type->to_be_acked)
{
CONSOLE_OUT("[GENERIC_ONOFF] Sending Response.\n");
/* Parameters: Request Context, Current State, Target State (NULL: to be ignored), Remaining Time (0: to be ignored), Additional Parameters (NULL: to be ignored) */
retval = MS_generic_onoff_server_state_update(ctx, ¤t_state_params, NULL, 0, NULL);
}
return retval;
}
static API_RESULT UI_generic_onoff_model_state_set(UINT16 state_t, UINT16 state_inst, void* param, UINT8 direction)
{
API_RESULT retval;
UCHAR proxy_state;
retval = API_SUCCESS;
switch (state_t)
{
case MS_STATE_GENERIC_ONOFF_T:
{
MS_STATE_GENERIC_ONOFF_STRUCT* param_p;
param_p = (MS_STATE_GENERIC_ONOFF_STRUCT*)param;
/* Instantaneous Change */
UI_generic_onoff.onoff = param_p->onoff;
*param_p = UI_generic_onoff;
CONSOLE_OUT("[state] current: 0x%02X\n", UI_generic_onoff.onoff);
CONSOLE_OUT("[state] target: 0x%02X\n", UI_generic_onoff.target_onoff);
CONSOLE_OUT("[state] remaining_time: 0x%02X\n", UI_generic_onoff.transition_time);
proxy_state = UI_proxy_state_get();
if(proxy_state==MS_PROXY_CONNECTED)
{
EM_start_timer (&proxy_dly_thandle, (EM_TIMEOUT_MILLISEC | 100), proxy_dly_generic_onoff, NULL, 0);
}
else
{
generic_onoff_set_pl(param_p->onoff);//重点
}
/* Ignoring Instance and direction right now */
}
break;
default:
break;
}
return retval;
}
vendor模型的应用示例(透传+publish)
蓝牙mesh组网实践(厂商透传模型介绍)
vendor模型,即厂家自定义模型,在通用模型中没有的功能,可以在vendor模型中通过自定义的方式来添加厂家特有的功能!
vendor在SDK里面,有两种应用方法:
1.以raw的方式(透传),该方式需要填写目标地址和应用密钥
2.以publish的方式,该方式只需填写目标地址
应用层流程(raw透传的方式)

vendor模型底层传输处理
vendormodel_client.c
vendormodel_server.c
客户端接收处理:
初始化时,MS_vendormodel_client_init把应用层的UI_phy_model_client_cb注册到vendormodel_client_UI_cb上!
收到信息后,先进入vendormodel_client_cb处理,然后再通过vendormodel_client_UI_cb调用应用层的UI_phy_model_client_cb!
服务端接收处理:
初始化时,MS_vendormodel_server_init把应用层的UI_phy_model_server_cb注册到vendormodel_server_UI_cb上!
收到信息后,先进入vendormodel_server_cb处理,然后再通过vendormodel_server_UI_cb调用应用层的UI_phy_model_server_c
vendor模型应用层处理
客户端:
注册vendor client 模型
/* Register Vendor Defined model server */
retval = UI_register_vendor_defined_model_client(element_handle);
绑定appkey到vendor client模型
UI_config_client_appkey_binding(0,0,UI_appkey);
#ifdef USE_VENDORMODEL
retval=MS_access_bind_model_app(UI_vendor_defined_client_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_vendor_defined_client_model_handle,handle);
#endif
设置目标地址(与publish的区别就是需要在发送时,填写目标地址)
UI_vendor_model_set_raw_addr();//
填充发送数据
#if 1
UINT8 ttl;
MS_STATE_VENDOR_EXAMPLE_TEST_STRUCT param;
ACCESS_CM_GET_DEFAULT_TTL(ttl);
param.total_len = test_len+7;
MS_PACK_LE_2_BYTE_VAL(&buffer[marker], param.total_len);
marker += 2;
param.is_to_be_ack = ack_en;
MS_PACK_LE_1_BYTE_VAL(&buffer[marker], param.is_to_be_ack);
marker++;
param.message_index = test_index;
MS_PACK_LE_2_BYTE_VAL(&buffer[marker], param.message_index);
marker += 2;
param.osal_tick = osal_sys_tick&0xffff;
MS_PACK_LE_2_BYTE_VAL(&buffer[marker], param.osal_tick);
marker += 2;
param.ttl = ttl;
MS_PACK_LE_1_BYTE_VAL(&buffer[marker], param.ttl);
marker++;
param.data_len = test_len;
MS_PACK_LE_1_BYTE_VAL(&buffer[marker], param.data_len);
marker++;
if(param.data_len)
{
EM_mem_set(&buffer[marker], 0, param.data_len);
marker += param.data_len;
}
set_param.vendormodel_param = &buffer[0];
获取模型appkey
MS_access_get_appkey_handle
(
&vendormodel_client_model_handle,
&key_handle
);
MS_access_get_appkey_handle
retval = MS_access_raw_data
(
&vendormodel_client_model_handle,
req_opcode,
dst_addr,
key_handle,
pdu_ptr,
marker,
MS_FALSE
);
服务端:
注册vendor server 模型
/* Register Vendor Defined model server */
retval = UI_register_vendor_defined_model_server(element_handle);
绑定appkey
vm_subscriptiong_binding_cb();
#ifdef USE_VENDORMODEL
retval=MS_access_bind_model_app(UI_vendor_defined_server_model_handle, handle);
CONSOLE_OUT("BINDING App Key %04x (%04x %04x)\n",retval,UI_vendor_defined_server_model_handle,handle);
#endif
vendor回调里面的notify数据处理
case MS_ACCESS_VENDORMODEL_NOTIFY_OPCODE:
{
uint16 message_index;
uint32 osal_tick;
UINT8 ack;
UINT8 ttl;
UINT8 len;
ACCESS_CM_GET_RX_TTL(ttl);
data_param = state_params->vendormodel_param;
MS_UNPACK_LE_1_BYTE(&ack, data_param+marker);
marker += 1;
MS_UNPACK_LE_2_BYTE(&message_index, data_param+marker);
marker += 2;
MS_UNPACK_LE_2_BYTE(&osal_tick, data_param+marker);
marker += 3;
MS_UNPACK_LE_1_BYTE(&len, data_param+marker);
marker += 1;
marker = 0;
if(ack)
{
MS_PACK_LE_1_BYTE_VAL(data_param+marker,++vendor_tid);
marker++;
MS_PACK_LE_1_BYTE_VAL(data_param+marker,0);
marker++;
MS_PACK_LE_2_BYTE_VAL(data_param+marker,message_index);
marker += 2;
MS_PACK_LE_2_BYTE_VAL(data_param+marker,osal_tick);
marker += 2;
MS_PACK_LE_1_BYTE_VAL(data_param+marker,ttl&0xff);
marker++;
MS_PACK_LE_1_BYTE_VAL(data_param+marker,len&0xff);
marker++;
if(len)
{
EM_mem_set(data_param+marker, 0, len);
marker += len;
}
state_params->vendormodel_param = data_param;
req_type->to_be_acked = 0x01;
}
// printf("[PDU_Rx] Pkt.INDEX:0x%04X\n",message_index);
}
发送回复
/* See if to be acknowledged */
if (0x01 == req_type->to_be_acked)
{
// CONSOLE_OUT(
// "[VENDOR_EXAMPLE] Sending Response.\n");
/* Parameters: Request Context, Current State, Target State (NULL: to be ignored), Remaining Time (0: to be ignored), Additional Parameters (NULL: to be ignored) */
retval = MS_vendormodel_server_state_update(ctx, ¤t_state_params, NULL, 0, NULL,marker);
}
客户端:
收到回复,打印信息
case MS_ACCESS_VENDORMODEL_NOTIFY_OPCODE:
{
uint16 message_index;
UINT8 ttl;
marker = 2;
MS_UNPACK_LE_2_BYTE(&message_index, msg_raw->data_param+marker);
marker += 4;
MS_UNPACK_LE_1_BYTE(&ttl, msg_raw->data_param+marker);
marker ++;
CONSOLE_OUT("[PDU_Rx] Pkt.INDEX:0x%04X,SRC:0x%04X,DST:0x%04X,TTL:0x%02X,TICK:0x%08X\r\n",
message_index,ctx->daddr,saddr,ttl,osal_sys_tick);
}
break;
低功耗、朋友、中继、代理SDK代码参考
MS_DISABLE_RELAY_FEATURE();//关闭中继
MS_ENABLE_PROXY_FEATURE();//使能代理
MS_DISABLE_FRIEND_FEATURE();//关闭好友
感谢名单
以上均为个人收集和整理。对应原作者都提供原文链接。对你们说:感谢在互联网世界分享每一个知识的你!辛苦啦!可以联系我加署名,我懒没加,侵删。