在Pico W 上使用蓝牙 - 图1

蓝牙是一个使用广泛的无线通信协议,大部分手机和物联网设备都内置了蓝牙模块,通过蓝牙可以便捷的在设备间收发数据。 在本文中,我们将介绍如何在 Pico W 上使用蓝牙,特别是 BLE(低功耗蓝牙),并使用 C/C++ SDK 进行开发。

关于树莓派 Pico W

Raspberry Pi Pico W 在 Pico 的基础上,增加了 Wi-Fi 和蓝牙模块。Pico W 配备英飞凌 CYW43439 无线电芯片组,该芯片组 支持 802.11n Wi-Fi 和蓝牙 5.2。在发布之初,树莓派 Pico W 虽然具备蓝牙功能,但官方 SDK 并未加入蓝牙和 Wi-Fi 功能的支持。从 C/C++ SDK 1.5.1 版本开始,可以很方便的使用 BTStack 库来开发蓝牙相关功能。

在Pico W 上使用蓝牙 - 图2

树莓派 Pico W 系列包含 Pico W 和 WH ,WH 是增加了引脚焊接的 Pico W,功能和 Pico W 一致。 树莓派 Pico 没有蓝牙和 Wi-Fi 模块,所以如果你需要使用蓝牙功能,必须再购买一个 HC-05 之类的外置蓝牙模块。

蓝牙协议栈

在开始之前,我们还需要了解一下蓝牙协议相关的一些基础知识。

蓝牙分为经典蓝牙和低功耗蓝牙(BLE),目前主流是更新的低功耗蓝牙。

在Pico W 上使用蓝牙 - 图3

BLE 蓝牙栈主要分为主机层和控制器层。 应用程序开发通常位于主机层之上的层中,即我们所说的应用程序层。主机层是软件层,通过 HCI(主机控制器接口)与蓝牙硬件和无线电通信。控制器(及其控制器层)是物理 BLE 芯片的一部分,是控制蓝牙无线电硬件的硬件逻辑和固件的组合。 在实际的开发中,主要关注主机层中的协议。

GAP 协议

GAP(Generic Access Profile)主要用于广播数据、设备发现以及设备间建立安全连接。

在发送接收数据之前,需要使用 GAP 协议扫描发现蓝牙设备,并通过 GAP 建立安全连接。

GAP 定义了 4 种角色:广播者(Broadcaster)、观察者(Observer)、中心设备(Central)、外围设备(peripheral),每一个设备都可以同时有一个或多个角色。

GAP层有4种不同类型的广播:通用的、定向的、不可连接的以及可发现的。设备每次广播时,会在3个广播信道上发送相同的报文。这些报文被称为一个广播事件。

例如 Beacon 设备只是向外广播,不支持连接,而小米手环等设备就可以与中心设备连接。

大部分情况下,外设通过广播自己来让中心设备发现自己,并建立 GATT 连接,从而进行更多的数据交换。

GATT 协议

GATT(Generic Attribute Profile)构建在 ATT(Attribute Protocol) 协议基础之上,为属性协议传输和存储数据建立了一些通用操作和框架。 GATT 使用属性协议作为自身传输协议来交换设备间的数据,并且定义了如何使用这些属性进行读、写、通知和指示操作。GATT 包含了许多服务和特征,每一个服务可以包含多个特征,而每一个特征也可以有多个描述符。 在 GATT 协议中,有两种设备角色:服务器(Server)和客户端(Client)。服务器保存数据,并响应来自客户端的请求,客户端则发起请求并接收响应。以小米手环为例,小米手环作为 GATT 服务器,保存着心率、步数等数据,而手机作为 GATT 客户端,读取这些数据并进行处理。 GATT 连接建立后,客户端可以通过发现服务和特征,来了解服务器上提供的所有服务。服务和特征都是通过唯一的 UUID(Universally Unique Identifier)来标识的。标准服务和特征由蓝牙 SIG(Special Interest Group)定义,如心率服务和电池电量服务;也可以定义自定义服务和特征,以满足特定应用的需求。 GATT的这两种角色,和GAP的central/peripheral角色是相互独立的,它们都能作为Client或者Server,甚至能同时作为Client和Server。

在 Pico W 上使用蓝牙

下面我们通过一个简单蓝牙收发数据示例项目来学习 Pico W 的蓝牙功能。

创建工程

首先,我们创建工程文件,并且拷贝cmake 文件。

  1. mdkir pico_ble
  2. cd pico_ble
  3. touch pico_ble.c CMakeLists.txt
  4. cp $PICO_SDK_PATH/external/pico_sdk_import.cmake .

配置 BTStack

树莓派 Pico C/C++ SDK 基于 BTStack 封装了蓝牙操作库 pico_btstack,相关接口可查看官方文档

使用 BTstack 需要在源码根目录中提供一个 btstack_config.h 文件,并配置 CMakeLists.txt 将其位置添加到你的包含路径中。

btstack_config.h 如下:

  1. #ifndef _PICO_BTSTACK_BTSTACK_CONFIG_H
  2. #define _PICO_BTSTACK_BTSTACK_CONFIG_H
  3. #ifndef ENABLE_BLE
  4. #error Please link to pico_btstack_ble
  5. #endif
  6. // BTstack features that can be enabled
  7. #define ENABLE_LE_PERIPHERAL
  8. #define ENABLE_LOG_INFO
  9. #define ENABLE_LOG_ERROR
  10. #define ENABLE_PRINTF_HEXDUMP
  11. // for the client
  12. #if RUNNING_AS_CLIENT
  13. #define ENABLE_LE_CENTRAL
  14. #define MAX_NR_GATT_CLIENTS 1
  15. #else
  16. #define MAX_NR_GATT_CLIENTS 0
  17. #endif
  18. // BTstack configuration. buffers, sizes, ...
  19. #define HCI_OUTGOING_PRE_BUFFER_SIZE 4
  20. #define HCI_ACL_PAYLOAD_SIZE (255 + 4)
  21. #define HCI_ACL_CHUNK_SIZE_ALIGNMENT 4
  22. #define MAX_NR_HCI_CONNECTIONS 1
  23. #define MAX_NR_SM_LOOKUP_ENTRIES 3
  24. #define MAX_NR_WHITELIST_ENTRIES 16
  25. #define MAX_NR_LE_DEVICE_DB_ENTRIES 16
  26. // Limit number of ACL/SCO Buffer to use by stack to avoid cyw43 shared bus overrun
  27. #define MAX_NR_CONTROLLER_ACL_BUFFERS 3
  28. #define MAX_NR_CONTROLLER_SCO_PACKETS 3
  29. // Enable and configure HCI Controller to Host Flow Control to avoid cyw43 shared bus overrun
  30. #define ENABLE_HCI_CONTROLLER_TO_HOST_FLOW_CONTROL
  31. #define HCI_HOST_ACL_PACKET_LEN (255+4)
  32. #define HCI_HOST_ACL_PACKET_NUM 3
  33. #define HCI_HOST_SCO_PACKET_LEN 120
  34. #define HCI_HOST_SCO_PACKET_NUM 3
  35. // Link Key DB and LE Device DB using TLV on top of Flash Sector interface
  36. #define NVM_NUM_DEVICE_DB_ENTRIES 16
  37. #define NVM_NUM_LINK_KEYS 16
  38. // We don't give btstack a malloc, so use a fixed-size ATT DB.
  39. #define MAX_ATT_DB_SIZE 512
  40. // BTstack HAL configuration
  41. #define HAVE_EMBEDDED_TIME_MS
  42. // map btstack_assert onto Pico SDK assert()
  43. #define HAVE_ASSERT
  44. // Some USB dongles take longer to respond to HCI reset (e.g. BCM20702A).
  45. #define HCI_RESET_RESEND_TIMEOUT_MS 1000
  46. #define ENABLE_SOFTWARE_AES128
  47. #define ENABLE_MICRO_ECC_FOR_LE_SECURE_CONNECTIONS
  48. #endif // MICROPY_INCLUDED_EXTMOD_BTSTACK_BTSTACK_CONFIG_H

定义 GATT 服务和特征

使用 BTStack 库时,我们需要定义一个 .gatt 文件来描述 GATT 服务和特征。以下是一个读写数据服务的 gatt 文件,定义了一个三个服务,分别为设备名称读取服务、GATT 数据库哈希、读写数据服务。

有关 gatt 描述文件的详细配置请查阅 BTStack 文档

  1. PRIMARY_SERVICE, GAP_SERVICE
  2. CHARACTERISTIC, GAP_DEVICE_NAME, READ, "picow_ble"
  3. PRIMARY_SERVICE, GATT_SERVICE
  4. CHARACTERISTIC, GATT_DATABASE_HASH, READ,
  5. // ReadWrite Service
  6. PRIMARY_SERVICE, AABB
  7. // ReadWrite Characteristic, with read, write, write_without_response and notify
  8. CHARACTERISTIC, 0000FF11-0000-1000-8000-00805F9B34FB, READ | NOTIFY | WRITE | WRITE_WITHOUT_RESPONSE | DYNAMIC,

配置 CMakeLists.txt

CMakeLists.txt 文件是项目的构建脚本,用于定义如何编译和链接项目。在使用 BTStack 库时,需要在 CMakeLists.txt 文件中添加相关配置。

  1. # 设置Cmake 最小依赖版本
  2. cmake_minimum_required(VERSION 3.17)
  3. # 设置c/c++ 编译版本
  4. set(CMAKE_C_STANDARD 11)
  5. set(CMAKE_CXX_STANDARD 17)
  6. set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
  7. # 导入 pico sdk cmake
  8. include(pico_sdk_import.cmake)
  9. # ====================================================================================
  10. # 设置开发板为 pico w (如果你的开发板是 pico 可以注释这行)
  11. set(PICO_BOARD pico_w CACHE STRING "Board type")
  12. # 设置项目名
  13. project(pico_ble C CXX ASM)
  14. # 初始化sdk
  15. pico_sdk_init()
  16. # 添加执行文件
  17. add_executable(pico_ble
  18. pico_ble.c
  19. )
  20. # 设置项目名称字符串
  21. pico_set_program_name(pico_ble "pico_ble")
  22. # 设置项目版本号
  23. pico_set_program_version(pico_ble "0.1")
  24. # Modify the below lines to enable/disable output over UART/USB
  25. # 是否打开 UART 串口
  26. pico_enable_stdio_uart(pico_ble 0)
  27. # 是否打开USB 串口
  28. pico_enable_stdio_usb(pico_ble 1)
  29. # 添加依赖库 (pico w 必须加入 pico_cyw43_arch_none )
  30. target_link_libraries(pico_ble
  31. pico_stdlib # for core functionality
  32. pico_btstack_ble # 蓝牙ble 支持库
  33. pico_btstack_cyw43 # cyw43 芯片蓝牙支持库
  34. pico_cyw43_arch_none
  35. )
  36. # Add the standard include files to the build
  37. target_include_directories(pico_ble PRIVATE
  38. ${CMAKE_CURRENT_LIST_DIR} # 编译包含 btstack_config.h 文件
  39. ${CMAKE_CURRENT_LIST_DIR}/.. # for our common lwipopts or any other standard includes, if required
  40. )
  41. pico_btstack_make_gatt_header(pico_ble PRIVATE "${CMAKE_CURRENT_LIST_DIR}/picow_ble.gatt")
  42. # 设置输出文件
  43. pico_add_extra_outputs(pico_ble)

我们在 CMakeLists.txt 文件中,添加了 pico_btstack_ble 和 pico_btstack_cyw43 链接库。如果你要使用经典蓝牙则需要将 pico_btstack_ble 替换为 pico_btstack_classic。

使用 CMake 函数 pico_btstack_make_gatt_header 以运行 BTstack 的 compile_gatt 工具,从 BTstack GATT 文件生成 GATT 头文件。运行 cmake .. 命令时,会自动在 build/generated 目录下生成 .gatt 文件同名的 .h 文件。

蓝牙连接和读写数据操作

以下是一个示例程序,演示如何在 Pico W 上使用 BTStack 库进行蓝牙连接和数据读写操作。

  1. #include <stdio.h>
  2. #include "btstack.h"
  3. #include "pico/cyw43_arch.h"
  4. #include "pico/btstack_cyw43.h"
  5. #include "pico/stdlib.h"
  6. #include "picow_ble.h"
  7. /*
  8. * @section Advertisements
  9. *
  10. * @text The Flags attribute in the Advertisement Data indicates if a device is dual-mode or le-only.
  11. */
  12. /* LISTING_START(advertisements): Advertisement data: Flag 0x02 indicates dual-mode device */
  13. // 广播数据格式: length , type, data
  14. const uint8_t adv_data[] = {
  15. // Flags general discoverable
  16. 0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x02,
  17. // Name
  18. 0x0a, BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME, 'P', 'i', 'c', 'o', 'W', '-', 'B', 'L', 'E',
  19. // Incomplete List of 16-bit Service Class UUIDs
  20. 0x03, BLUETOOTH_DATA_TYPE_INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, 0xbb, 0xaa,
  21. };
  22. /* LISTING_END */
  23. uint8_t adv_data_len = sizeof(adv_data);
  24. static btstack_timer_source_t heartbeat;
  25. static btstack_packet_callback_registration_t hci_event_callback_registration;
  26. // 蓝牙堆栈事件回调
  27. static void packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size) {
  28. UNUSED(size);
  29. UNUSED(channel);
  30. bd_addr_t local_addr;
  31. if (packet_type != HCI_EVENT_PACKET)
  32. return;
  33. uint8_t event_type = hci_event_packet_get_type(packet);
  34. printf("event_type: %d\n", event_type);
  35. switch (event_type)
  36. {
  37. case BTSTACK_EVENT_STATE:
  38. if (btstack_event_state_get_state(packet) != HCI_STATE_WORKING)
  39. return;
  40. gap_local_bd_addr(local_addr);
  41. printf("BTstack up and running on %s.\n", bd_addr_to_str(local_addr));
  42. // 设置广播数据
  43. uint16_t adv_int_min = 800;
  44. uint16_t adv_int_max = 800;
  45. uint8_t adv_type = 0;
  46. bd_addr_t null_addr;
  47. memset(null_addr, 0, 6);
  48. gap_advertisements_set_params(adv_int_min, adv_int_max, adv_type, 0, null_addr, 0x07, 0x00);
  49. assert(adv_data_len <= 31); // ble limitation
  50. gap_advertisements_set_data(adv_data_len, (uint8_t *)adv_data);
  51. // 开始广播
  52. gap_advertisements_enable(1);
  53. break;
  54. case HCI_EVENT_DISCONNECTION_COMPLETE:
  55. break;
  56. case ATT_EVENT_CAN_SEND_NOW:
  57. break;
  58. case ATT_WRITE_REQUEST:
  59. break;
  60. case ATT_WRITE_COMMAND:
  61. printf("ATT_WRITE_COMMAND\n");
  62. break;
  63. case GATT_WRITE_VALUE_OF_CHARACTERISTIC_WITHOUT_RESPONSE:
  64. printf("GATT_WRITE_VALUE_OF_CHARACTERISTIC_WITHOUT_RESPONSE\n");
  65. break;
  66. case HCI_EVENT_TRANSPORT_PACKET_SENT:
  67. printf("HCI_EVENT_TRANSPORT_PACKET_SENT\n");
  68. break;
  69. default:
  70. break;
  71. }
  72. }
  73. uint16_t att_read_callback(hci_con_handle_t connection_handle, uint16_t att_handle, uint16_t offset, uint8_t * buffer, uint16_t buffer_size) {
  74. UNUSED(connection_handle);
  75. printf("Read callback for handle 0x%04x\n", att_handle);
  76. return 0;
  77. }
  78. int att_write_callback(hci_con_handle_t connection_handle, uint16_t att_handle, uint16_t transaction_mode, uint16_t offset, uint8_t *buffer, uint16_t buffer_size) {
  79. UNUSED(transaction_mode);
  80. UNUSED(offset);
  81. UNUSED(buffer_size);
  82. printf("att_write_callback\n");
  83. // 读取客户端写入的数据
  84. printf("Handle: 0x%04X\n", att_handle);
  85. printf("Data: ");
  86. for (int i = 0; i < buffer_size; i++)
  87. {
  88. printf("%02X ", buffer[i]);
  89. }
  90. printf("\n");
  91. return 0;
  92. }
  93. int main() {
  94. // 初始化标准输入输出
  95. stdio_init_all();
  96. // initialize CYW43 driver architecture (will enable BT if/because CYW43_ENABLE_BLUETOOTH == 1)
  97. if (cyw43_arch_init()) {
  98. printf("failed to initialise cyw43_arch\n");
  99. return -1;
  100. }
  101. // 初始化 BTStack
  102. l2cap_init();
  103. sm_init();
  104. att_server_init(profile_data, att_read_callback, att_write_callback);
  105. // inform about BTstack state
  106. hci_event_callback_registration.callback = &packet_handler;
  107. hci_add_event_handler(&hci_event_callback_registration);
  108. // register for ATT event
  109. att_server_register_packet_handler(packet_handler);
  110. // 打开蓝牙
  111. hci_power_control(HCI_POWER_ON);
  112. // 进入主循环
  113. while (true)
  114. {
  115. // sleep_ms(1000);
  116. }
  117. return 0;
  118. }

这个程序初始化 BLE 并使用 .gatt 生成的 profile_data 初始化 att 服务 和设置 att 读写回调,然后使用 hci_add_event_handler 添加 BTstack state 事件回调监听,当收到 HCI_STATE_WORKING 蓝牙设备打开事件时,打开设备 GAP 可连接广播。

在 att_write_callback 回调中,会将对端发送的数据打印出来。

调试验证

编译项目,并且将 uf2 文件拖拽烧录到 树莓派 Pico W 中。

使用 Android 手机 打开 BLE调试助手搜索蓝牙设备,可以看到名称为 “PicoW-BLE” 的设备,点击”Connect”连接。选择最后一个服务并发送数据 “123”。

在Pico W 上使用蓝牙 - 图4 在Pico W 上使用蓝牙 - 图5

在 CoolTerm 上可以看到调试打印信息 Data: 31 32 33 (对应 123 Unicode 码)

在Pico W 上使用蓝牙 - 图6

如果你看到上面的打印信息,恭喜🎉,你成功使用 Pico W 完成了蓝牙读写功能的开发。

总结

通过本文,我们学习了 使用 C/C++ SDK 在 Pico W 上开发蓝牙功能。首先,我们需要导入 btstack_config.h 和编写 gatt 配置文件 ,并且在 CMakeLists.txt 中做相应的配置。接下来,我们在 pico_ble.c 文件中开发蓝牙核心功能。最后,通过调试,我们验证了蓝牙收发数据功能。

源码在 GitHub开源: https://github.com/xtcel/Raspberry_Pi_Pico_C_Tutorial/tree/master/pico_ble, 有需要的请自行取用。

参考

蓝牙协议: https://github.com/Eronwu/Getting-Started-with-Bluetooth-Low-Energy-in-Chinese

低功耗蓝牙开发手册: https://ingchips.github.io/application-notes/pg_ble_stack_cn/