本文讲解如何在该SDK中添加用户自居定义的BLE服务。该服务的源码可以存放在自己希望的位置,但为符合工程目录的合理性,建议放在工程所在的目录下面,假设我们的工程名称为: headset_ghp
那么,笔者的BLE自定义服务源码存放的路径是:
bta_sdk\mcu\project\ab1565_ab1568_evk\apps\headset_ghp\ble_ghp\
笔者自定义的服务目录如下图所示,创建时参考了:bta_sdk\mcu\middleware\MTK\bt_air\src\ble_air目录下的服务:

bta_sdk\mcu\project\ab1565_ab1568_evk\apps\headset_ghp\GCC\Makefile);
- #######################################################
-
- # Main APP files
-
- APP_PATH := $(patsubst $(SDK_PATH)/%,%,$(abspath $(dir $(PWD))))
-
- APP_PATH_SRC := $(APP_PATH)/src
-
-
- include $(SOURCE_DIR)/$(APP_PATH_SRC)/apps/module.mk
-
- include $(SOURCE_DIR)/$(APP_PATH)/ble_ghp/module.mk
注意:添加的行需要在APP_PATH 定义的后面,否则会module.mk中所使用的变量APP_PATH会为空值导至找不到源码文件;
- BT_GHP_PATH = $(SOURCE_DIR)/$(APP_PATH)/ble_ghp/src
-
- C_FILES += $(BT_GHP_PATH)/ble_ghp_service.c
- #################################################################################
- #include path
- #################################################################################
- CFLAGS += -I$(SOURCE_DIR)/$(APP_PATH)/ble_ghp/inc
- CFLAGS += -I$(SOURCE_DIR)/kernel/rtos/FreeRTOS/Source/include
- CFLAGS += -I$(SOURCE_DIR)/kernel/rtos/FreeRTOS/Source/portable/GCC/ARM_CM4F
- CFLAGS += -I$(SOURCE_DIR)/driver/chip/inc
- CFLAGS += -I$(SOURCE_DIR)/kernel/service/inc
- CFLAGS += -I$(SOURCE_DIR)/middleware/MTK/bt_callback_manager/inc
- CFLAGS += -I$(SOURCE_DIR)/middleware/MTK/nvdm/inc
-
- ifeq ($(MTK_AWS_MCE_ENABLE),y)
- ifeq ($(SUPPORT_ROLE_HANDOVER_SERVICE),y)
- CFLAGS += -I$(SOURCE_DIR)/middleware/MTK/bluetooth_service/bt_role_handover_service/inc
- endif
- endif
-
- ifeq ($(MTK_CUS_FEATURE_BLE_GHP_ENABLE),y)
- CFLAGS += -DMTK_CUS_FEATURE_BLE_GHP_ENABLE
- endif
从上面可以看出,笔者使用了一个宏定义来开或关该服务:MTK_CUS_FEATURE_BLE_GHP_ENABLE
自定义的BLE服务需要使用128位的uuid,UUID的值用户可以自行定义;同时,服务中的属性可以支持不同的操作权限,如可读/可写/可订阅等,或者是需要鉴权后才可以操作属性,这些权限的定义在头文件:bt_gatt.h中均有定义,如
BT_GATT_CHARC_PROP_READ/BT_GATT_CHARC_PROP_NOTIFY/BT_GATT_CHARC_PROP_WRITE
最常使用的属性的操作有三个:读/写/订阅;这三个权限再实例化属性描述符时就需要进行声明,并且对读和写的操作,还需要有相应的回调函数或者是对值的描述符的定义。此处笔者采用了通过回调函数来支持读和写及订阅的操作。
笔者的UUID的及各描述符的定义如下所示:
//此处定义一个日志类型,这样在调试看日志的时候,就可以以此为过滤条件来仅显示该类型的日志;
log_create_module(GHP, PRINT_LEVEL_INFO);
#define ble_ghp_hex_dump(_message,...) LOG_HEXDUMP_I(GHP, (_message), ##__VA_ARGS__)
//下面是主服务的UUID及该服务下两个属性的UUID的定义,注意观察这些值,其中大部分的值都是一样的,仅有2个字节用来区分不同的属性,如粗体数值所示。
- #define GHP_SERVICE_UUID \
- {{0x45, 0x4C, 0x42, 0x61, 0x68, 0x6F, 0x72, 0x69, \
- 0x41, 0x03, 0xAB, 0x2D, 0x4D, 0x49, 0x19, 0x73}}
-
- #define GHP_RX_CHAR_UUID \
- {{0x45, 0x4C, 0x42, 0x61, 0x68, 0x6F, 0x72, 0x69, \
- 0x41, 0x32, 0xAB, 0x2D, 0x52, 0x41, 0x19, 0x73}}
-
- #define GHP_TX_CHAR_UUID \
- {{0x45, 0x4C, 0x42, 0x61, 0x68, 0x6F, 0x72, 0x69, \
- 0x41, 0x31, 0xAB, 0x2D, 0x52, 0x41, 0x19, 0x73}}
-
-
- 如下代码,还需要定义两个属性的句柄,在描述符实例化的时候需要使用。
-
- const bt_uuid_t GHP_RX_CHAR_UUID128 = GHP_RX_CHAR_UUID;
- const bt_uuid_t GHP_TX_CHAR_UUID128 = GHP_TX_CHAR_UUID;
- //主服务对象的定义
- BT_GATTS_NEW_PRIMARY_SERVICE_128(ble_ghp_primary_service, GHP_SERVICE_UUID);
-
- //第一个属性的定义,可以发现,该属性支持开放式的读+写+订阅,
- BT_GATTS_NEW_CHARC_128(ble_ghp_rx_char, BT_GATT_CHARC_PROP_WRITE_WITHOUT_RSP |
- BT_GATT_CHARC_PROP_WRITE |
- BT_GATT_CHARC_PROP_READ |
- BT_GATT_CHARC_PROP_NOTIFY, GHP_RX_CHAR_VALUE_HANDLE, GHP_RX_CHAR_UUID);
-
- //下面这个是对该属性的值对象的定义,定义了该值是可读和可写,这个需要和属性对象的定义对应起来,因为属性定义中支持读和写。并且还需需要添加一个回调函数,当客户端对该值进行读或写时,就会触发该回调函数,在该函数中来实现对应值的读和写操作。
- BT_GATTS_NEW_CHARC_VALUE_CALLBACK(ble_ghp_rx_char_value, GHP_RX_CHAR_UUID128,
- BT_GATTS_REC_PERM_WRITABLE |
- BT_GATTS_REC_PERM_READABLE, ble_ghp_rx_char_callback);
- //下面这行是对该属性起了一个易读的名字,通常是个字符串,该实例中采用了定长字符串来显示,当然,也可以使用动态的方式,这个就需要使用宏:BT_GATTS_NEW_CHARC_USER_DESCRIPTION来实现,因为它支持回调函数,可以在回调函数中来动态更改该易读的名字。
- BT_GATTS_NEW_CHARC_USER_DESCRIPTION_STR16(ble_ghp_rx_desc, BT_GATTS_REC_PERM_READABLE, 3,"Rx");
//因为该属性同时还支持订阅,因此还需要添加一个cccd描述符对象,用来给客户端提供订阅的接口。客户端点击订阅或取消都会触发该宏定义中的回调函数,其值决定了是否打开订阅功能(1=打开,0=取消订阅)。所以,该描述符需要支持读和写的权限。
BT_GATTS_NEW_CLIENT_CHARC_CONFIG(ble_ghp_rx_client_config, BT_GATTS_REC_PERM_READABLE|BT_GATTS_REC_PERM_WRITABLE, ble_ghp_rx_char_cccd_callback);
//下面是定义了第二个属性,其原更同第一个类似,这里不再重复解释。其中一个区别是该属性的值不支持写操作,且默认值为123;
- BT_GATTS_NEW_CHARC_128(ble_ghp_tx_char, BT_GATT_CHARC_PROP_NOTIFY, GHP_TX_CHAR_VALUE_HANDLE, GHP_TX_CHAR_UUID);
- BT_GATTS_NEW_CHARC_VALUE_UINT8(ble_ghp_tx_char_value, GHP_TX_CHAR_UUID128, BT_GATTS_REC_PERM_READABLE, 123);
- BT_GATTS_NEW_CHARC_USER_DESCRIPTION_STR16(ble_ghp_tx_desc, BT_GATTS_REC_PERM_READABLE, 3,"Tx");
- BT_GATTS_NEW_CLIENT_CHARC_CONFIG(ble_ghp_tx_client_config, BT_GATTS_REC_PERM_READABLE|BT_GATTS_REC_PERM_WRITABLE, ble_ghp_tx_char_cccd_callback);
上面对各服务的属性的对象定义之后,还需要把这些对象有序的组织起来,如下代码所示,注意,这里顺序是有要求的:
- static const bt_gatts_service_rec_t *ble_ghp_service_rec[] = {
- (const bt_gatts_service_rec_t*) &ble_ghp_primary_service, //starting_handle=0xA401
-
- (const bt_gatts_service_rec_t*) &ble_ghp_rx_char, //0xA402
- (const bt_gatts_service_rec_t*) &ble_ghp_rx_char_value, //0xA403
- (const bt_gatts_service_rec_t*) &ble_ghp_rx_desc, //0xA404
- (const bt_gatts_service_rec_t*) &ble_ghp_rx_client_config,//0xA405
-
- (const bt_gatts_service_rec_t*) &ble_ghp_tx_char, //0xA406
- (const bt_gatts_service_rec_t*) &ble_ghp_tx_char_value, //0xA407
- (const bt_gatts_service_rec_t*) &ble_ghp_tx_desc, //0xA408
- (const bt_gatts_service_rec_t*) &ble_ghp_tx_client_config,//ending_handle=0xA409
- };
下面这个定义,用来声明该服务下面所有描述符对象的句柄的范围,值为uint16_t类型,如下代码中的值0xA401和0xA409;
那么,这两个值的定义有什么要求呢?我们看上面的代码ble_ghp_service_rec[]的定义中的注释,可以发现其数据的长度刚好是starting_handle和ending_handle的差值的长度,且句柄的起始值从starting_handle开始。
- const bt_gatts_service_t ble_ghp_service = {
- .starting_handle = 0xA401,
- .ending_handle = 0xA409,
- .required_encryption_key_size = 0,
- .records = ble_ghp_service_rec
- };
那么另一个问题,为什么是0xA401开始?这个就和SDK中其它服务句柄的定义有关系了。我们可以查看如下路径中的文件:
bta_sdk\mcu\project\ab1565_ab1568_evk\apps\headset_ref_design\src\bt_app_utility\gatt_service.c
找到如下代码:
- /**< gatt server collects all service. */
- const bt_gatts_service_t *bt_if_clm_gatt_server[] = {
- &bt_if_gap_service, /**< handle range: 0x0001 to 0x0009. */
- &bt_if_gatt_service,/**< handle range: 0x0011 to 0x0015. */
- #ifdef MTK_BLE_SMTCN_ENABLE
- &bt_if_dtp_service, //0x0014-0x0017
- #endif
- /*&bt_if_dogp_service,*/ /**< handle range: 0x0020 to 0x0025. */
- #ifdef __BLE_BAS__
- &ble_bas_service,/**< handle range: 0x0031 to 0x0034. */
- #endif
- #ifdef MTK_BLE_IAS
- &ble_ias_service,/**< handle range: 0x0040 to 0x0042. */
- #endif
- #ifdef MTK_PORT_SERVICE_BT_ENABLE
- &ble_air_service,/**< handle range: 0x0051 to 0x0056. */
- #endif
- &ble_dis_service,/**< handle range: 0x0060 to 0x0072. */
- #ifdef MTK_AMA_ENABLE
- &ble_ama_service,/**handle range: 0x00D0 to 0x00D5. */
- #endif
- #ifdef MTK_VA_XIAOWEI_ENABLE
- &xiaowei_ble_service,
- #endif
- #ifdef MTK_VA_XIAOAI_ENABLE
- &xiaoai_ble_service, /**< handle range: 0x00F0 to 0x00F5. */
- #endif
- #ifdef __BT_FAST_PAIR_ENABLE__
- &bt_fast_pair_service,/**< handle range: 0x0100 to 0x013F. */
- #endif
- #ifdef AIR_LE_AUDIO_ENABLE
- &ble_ascs_service, /**< handle range: 0x1103 to 0x110F. */
- &ble_pacs_service, /**< handle range: 0x1200 to 0x1212. */
- &ble_vcs_service, /**< handle range: 0x1301 to 0x1309. */
- &ble_vocs_service_channel_1, /**< handle range: 0x2001 to 0x200C. */
- #ifdef AIR_LE_AUDIO_HEADSET_ENABLE
- &ble_vocs_service_channel_2, /**< handle range: 0x3001 to 0x300C. */
- #endif
- &ble_aics_service, /**< handle range: 0x4001 to 0x4010. */
- &ble_mics_service, /**< handle range: 0x5001 to 0x5004. */
- &ble_csis_service, /**< handle range: 0x6001 to 0x600A. */
- &ble_cas_service, /**< handle range: 0x7001 to 0x7002. */
- &ble_bass_service, /**< handle range: 0xA201 to 0xA209. */
- &ble_tmas_service, /**< handle range: 0xA301 to 0xA303. */
- #endif
- #ifdef MTK_CUS_FEATURE_BLE_GHP_ENABLE
- &ble_ghp_service, /**< handle range: 0xA401 to 0xA409. */
- #endif
- NULL
- };
从这段代码的注释中我们可以发现,每个Primary services都会占用若干个句柄的值,且大至是按顺序去使用0x0000~0xFFFF这段的值,因为SDK中已经使用到了0xA303,因此笔者继续从0xA401值开始定义。当然,你也可以从0xA304开始,只要不和系统中已经被占用的句柄的值有冲突就基本是没问题的,但还是建议按照顺序的规则来使用,这样不容易乱。
再看属性值的读和写操作,回调函数如下所示:
- static uint32_t ble_ghp_rx_char_cccd_callback(const uint8_t rw, uint16_t handle, void *data, uint16_t size, uint16_t offset)
- {
- uint16_t val=255;
-
- if(size == 2) val = *(uint16_t*)data;
- LOG_MSGID_I(GHP, "[BLE_GHP]ble_ghp_rx_char_cccd_callback,hd:%d, rw%d,size%d, offset%d,val%d\r\n", 5, handle, rw, size, offset, val);
- if ((handle != BT_HANDLE_INVALID) && (handle == g_ghp_cntx.conn_handle)) {
- /** record for each connection. */
- if (rw == BT_GATTS_CALLBACK_WRITE) {
- if (size != sizeof(uint16_t)) { //Size check
- return 0;
- }
- g_ghp_cntx.notify_enabled = (val&1)<<1;
- LOG_MSGID_I(GHP, "[BLE_GHP]rx_data:%d \r\n", 1, g_ghp_cntx.notify_enabled);
- } else if (rw == BT_GATTS_CALLBACK_READ) {
- if (size != 0) {
- uint16_t *buf = (uint16_t*) data;
- *buf = (uint16_t) g_ghp_cntx.notify_enabled;
- LOG_MSGID_I(GHP, "[BLE_GHP]read rx cccd value = %d\r\n", 1, *buf);
- return size;
- }
- }
- return sizeof(uint16_t);
- }
- return 0;
- }
其中参数rw即是用来区分对属性是读(BT_GATTS_CALLBACK_READ)还是写(BT_GATTS_CALLBACK_WRITE)。注意这里的size的值,在读的时候,若size的值为0,表示系统需要通过该函数的返回值来确认待读数据的长度,因此这时不能往data参数中写数据,仅当size值不为0时才可以向data中写数据。在对属性值读的时候,系统都会首先通过该回调函数来获取待读数据的长度,然后再来读有效的数据。
其中参数offset用来表示向目标数据中写或读的偏移量,因为目标数据可以是一个很大的数据,无法采用普通的如byte,uint16,uint32这样的数据来表示,所以这时候就需要使用offset这个参数来实现对复合数据的读写。
再来看CCCD的回调函数:
- static uint32_t ble_ghp_tx_char_cccd_callback(const uint8_t rw, uint16_t handle, void *data, uint16_t size, uint16_t offset)
- {
- uint16_t val=255;
-
- if(size == 2) val = *(uint16_t*)data;
- LOG_MSGID_I(GHP, "[BLE_GHP]ble_ghp_tx_char_cccd_callback, hd:%d, rw%d,size%d,offset%d,val%d\r\n", 5, handle, rw, size,offset, val);
- if ((handle != BT_HANDLE_INVALID) && (handle == g_ghp_cntx.conn_handle)) {
- /** record for each connection. */
- if (rw == BT_GATTS_CALLBACK_WRITE) {
- if (size != sizeof(uint16_t)) { //Size check
- return 0;
- }
- g_ghp_cntx.notify_enabled = (val&1)<<0;
- LOG_MSGID_I(GHP, "[BLE_GHP]rx_data:%d \r\n", 1, g_ghp_cntx.notify_enabled);
- } else if (rw == BT_GATTS_CALLBACK_READ) {
- if (size != 0) {
- uint16_t *buf = (uint16_t*) data;
- *buf = (uint16_t) g_ghp_cntx.notify_enabled;
- LOG_MSGID_I(GHP, "[BLE_GHP]read tx cccd value = %d\r\n", 1, *buf);
- return size;
- }
- }
- return sizeof(uint16_t);
- }
- return 0;
- }
同属性值的读写的回调函数类似,这里操作的不是属性值,而是一个用户自己用来记录订阅是否开启的变量:g_ghp_cntx.notify_enabled;
那么,对于订阅的功能,笔者采用了一个Timer来周期的通知客户端,Timer的回调函数如下所示,当然,也可以在工程中别的地方通过调用函数:
ble_ghp_write_data来实现对客户端的数据通知。
- uint16_t lgTxVal=0;
- static void notify_timer_callback(TimerHandle_t expiredTimer)
- {
- lgTxVal++;
- LOG_MSGID_I(GHP,"[BLE_GHP]in timer callback function, tx:%d\r\n", 1, lgTxVal);
-
- ble_ghp_write_data(g_ghp_cntx.conn_handle, &lgTxVal, 2);
- }
-
-
- /**
- * @brief Function for sending the Ghp service tx characteristic value.
- *
- * @param[in] conn_handle connection handle.
- *
- * @return ble_status_t 0 means success.
- */
- static bt_status_t ble_ghp_service_tx_send(bt_handle_t conn_handle, uint8_t *data, uint32_t length)
- {
- bt_status_t status = BT_STATUS_FAIL;
-
- LOG_MSGID_I(GHP, "[BLE_GHP]ble_ghp_service_tx_send! hd:%d,len:%d \r\n", 2, conn_handle, length);
- uint8_t *buf = NULL;
- bt_gattc_charc_value_notification_indication_t *ghp_noti_rsp;
- ble_ghp_cntx_t *buffer_t = ble_ghp_get_cntx_by_handle(conn_handle);
-
- buf = (uint8_t *)pvPortMalloc(5 + length);
- if (buf == NULL) {
- LOG_MSGID_I(GHP, "[BLE_GHP]ble_ghp_service_tx_send fail, OOM!\r\n", 0);
- return status;
- }
- ghp_noti_rsp = (bt_gattc_charc_value_notification_indication_t*) buf;
- LOG_MSGID_I(GHP, "[BLE_GHP]ble_ghp_service_tx_send, new RX Char Value is %d\r\n", 1, data[0]);
-
- if ((conn_handle != BT_HANDLE_INVALID) && (buffer_t) &&
- (BLE_GHP_CCCD_NOTIFICATION == buffer_t->notify_enabled)) {
- ghp_noti_rsp->att_req.opcode = BT_ATT_OPCODE_HANDLE_VALUE_NOTIFICATION;
- ghp_noti_rsp->att_req.handle = GHP_TX_CHAR_VALUE_HANDLE;
- memcpy((void*)(ghp_noti_rsp->att_req.attribute_value), data, length);
- ghp_noti_rsp->attribute_value_length = 3 + length;
-
- LOG_MSGID_I(GHP, "[BLE_GHP]ble_ghp_service_notify conn_handle is %x, send data is %d\r\n", 2, conn_handle, data[0]);
-
- status = bt_gatts_send_charc_value_notification_indication(conn_handle, ghp_noti_rsp);
- }
- if (buf != NULL) {
- vPortFree(buf);
- }
- return status;
- }
-
-
-
- /**
- * @brief Function for application to write data to the send buffer.
- */
- uint32_t ble_ghp_write_data(uint16_t conn_handle, uint8_t *buffer, uint32_t size)
- {
- BLEGHP_MUTEX_LOCK();
- LOG_MSGID_I(GHP, "[BLE_GHP]ble_ghp_write_data incoming, hd:%d,size:%d\r\n", 2, conn_handle, size);
-
- uint32_t send_size = 0;
- uint32_t mtu_size;
- ble_ghp_cntx_t *buffer_t = ble_ghp_get_cntx_by_handle(conn_handle);
-
- if ((conn_handle != BT_HANDLE_INVALID) && (buffer_t) )
- {
- mtu_size = bt_gattc_get_mtu((bt_handle_t)conn_handle);
-
- LOG_MSGID_I(GHP, "[BLE_GHP]mtu = %d\r\n", 1, mtu_size);
- if ((mtu_size - 3) < size) {
- send_size = mtu_size - 3;
- } else {
- send_size = size;
- }
- if (0 == send_size) {
- LOG_MSGID_I(GHP, "[BLE_GHP] ble_ghp_send_data send_size is 0!\r\n", 0);
- BLEGHP_MUTEX_UNLOCK();
- return 0;
- }
-
- #ifdef BLE_GHP_LOW_POWER_CONTROL
- ble_ghp_set_remote_device_type(conn_handle, BLE_GHP_REMOTE_DEVICE_IOS);
- ble_ghp_update_connection_interval(conn_handle);
- #endif
-
- if (BT_STATUS_SUCCESS == ble_ghp_service_tx_send(conn_handle, buffer, send_size)) {
- LOG_MSGID_I(GHP, "[BLE_GHP] ble_ghp_send_data: send_size[%d]\r\n", 1, send_size);
- BLEGHP_MUTEX_UNLOCK();
- return send_size;
- }
- }
- BLEGHP_MUTEX_UNLOCK();
- return 0;
- }
其订阅测试如下图所示:

