基于CH573的BLE温湿度传感器
在使用ESP32制作一些项目时,经常会想要加入温湿度检测的功能。但在实践中存在一个很严重的问题,即放在机壳内的温湿度传感器会被ESP32散发出的热量影响,导致度数出现温升。本文介绍基于CH573制作的低功耗的温湿度传感器,并使用蓝牙将数据传输到ESP32上。
为了解决将温湿度传感器放到机壳内的温升问题,最直接同时也是效果最好的方案就是将传感器放在机身外,同时传感器使用电池供电。但是这就会引入低功耗与通信的问题,即传感器如何在保证低功耗的前提下,将数据通过无线传输到ESP32上。
ESP32支持三种无线通信协议,它们分别是WiFi
、ESP-NOW
与BLE
。为了能够直接使用ESP32接受数据而不需要添加额外的无线收发芯片,传感器与ESP32通信的协议应该也从这三种协议里面选择。
其中,ESP-NOW
协议只有乐鑫的芯片支持,但是乐鑫并没有低功耗芯片,因此排除。Wi-Fi
连接、传输等的功耗也都非常大,因此BLE
便成了最好的选择。
目前,市面上已经有基于BLE的温湿度传感器了,例如米家温湿度计2
等。我也亲自购入了一个,研究了它的实现原理。
对于使用BLE发送传感器读数的场景,目前主要有两种做法:
- 设备开启蓝牙广播,将自身标识为可连接,当有手机或网关连接时,建立
GATT
连接并传输数据。这种方式比较适合米家温湿度计2这种本身带有屏幕显示读数,蓝牙主要用于传输保存的历史数据的场景。 - 设备将读数放入蓝牙广播中,进行周期性广播,并将自身标识为不可连接。网关通过不断进行BLE扫描来获取广播包内的传感器数据。这种方式比较适合本身不带屏幕,需要使用其他设备显示读数的场景。
对于一个没有屏幕显示的Beacon来说,第二种做法显然是更合适的。同时,由于不需要进行蓝牙连接,相比第一种做法也能节约下更多的电量。确定了方案后,接下来就是芯片选型了。
CH573是沁恒推出的一款BLE MCU,价格非常便宜,外围电路也非常简单,官方甚至贴心的提供了PCB天线的参考设计,配合内置的匹配电路,只需要复制粘贴天线即可完成简单的射频设计,对没有射频匹配条件的业余玩家来说非常友好。
对于一个BLE Beacon来说,除了传感器和MCU之外,剩下的只需要一个LED灯用于指示状态,一个按键用于切换状态,以及一个开关用于彻底断电。
下面是我制作的PCB实物与渲染图。板子采用2层板设计,板载SHT31温湿度传感器,使用CR2032电池供电。
PCB天线与匹配电路,我直接参考的沁恒官方提供的双层板天线。经测试信号尚可,0dbm
的发射功率下,信号穿过一堵墙仍然可以被ESP32接收到。
完成了硬件的开发,接下来就是软件了。软件使用沁恒官方提供的开发包,在MounRiver
下开发完成,可以到GitHub仓库ch573-sht3x-beacon中查看。
程序中将任务分为了3个任务,分别是task_broadcaster
task_sensor
与task_bsp
。
task_broadcaster
任务处理与蓝牙相关的操作,主要包括初始化蓝牙广播者模式、发送广播数据并处理传感器更新事件。task_sensor
任务负责初始化传感器,并定期触发传感器转换,在传感器转换完成时响应转换完成事件,并向task_broadcaster
通知转换已完成。task_bsp
主要负责执行定期射频校准工作。
值得一提的时,蓝牙广播数据在蓝牙启动的时候是无法更新的。因此,在task_broadcaster
接收到传感器更新事件时,需要先关闭广播,待广播数据更新完毕后产生更新完成事件后,在事件处理函数内重启开启广播。
BLE的广播数据是最长可以有31字节,在这里我使用了30字节。
static uint8_t advertising_data[30] = {
2, //字段长度
GAP_ADTYPE_FLAGS, //蓝牙属性标志位
GAP_ADTYPE_FLAGS_BREDR_NOT_SUPPORTED | GAP_ADTYPE_FLAGS_GENERAL, //不支持BR/EDR,General discoverable
13,
GAP_ADTYPE_LOCAL_NAME_COMPLETE, //设备名称
'C', 'H', '5', '7', '3', '-', 'S', 'e', 'n', 's', 'o', 'r',
12,
GAP_ADTYPE_MANUFACTURER_SPECIFIC, //厂商自定义数据
'Y', 'Z', //厂商ID
0x00, 0x02, 0x00, 0x00, 0x12, 0x00, 0x00, 0x21, 0x00
};
广播包主要分为3部分:
- 第一部分是蓝牙属性标志位,总共置位了
GAP_ADTYPE_FLAGS_BREDR_NOT_SUPPORTED
与GAP_ADTYPE_FLAGS_GENERAL
两个标志位,分别代表该设备不支持经典蓝牙与处于普通可发现模式。 - 第二部分是设备名称,也就是手机App显示的设备名称,这里设置为
CH573-Sensor
。 - 第三部分是厂商自定义数据,其中包含了温湿度与电池电压信息,具体如下:
字节 | 1-2 | 3 | 4 | 5-6 | 7 | 8-9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|
数据 | 0x59, 0x5a | 0x00 | 0x02 | 100倍温度数值 | 0x12 | 100倍湿度数值 | 0x21 | 100倍(电池电压-1V) |
说明 | 厂商ID,‘Y’ ‘Z’ | 高4位为协议类型,低4位为协议版本 | 高4位为数据类型,低4位为数据长度 | 小端int16_t,Temp*100 | 高4位为数据类型,低4位为数据长度 | 小端uint16_t | 高4位为数据类型,低4位为数据长度 | uint8_t |
对于ESP32,只需要不断进行BLE扫描,并对扫描得到的每个广播数据通过设备名与厂商ID进行过滤,即可得到广播包内的传感器数据。下面是蓝牙的初始化部分:
nvs_flash_init();
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
esp_bt_controller_init(&bt_cfg);
esp_bt_controller_enable(ESP_BT_MODE_BLE); //配置为BLE模式
esp_bluedroid_init();
esp_bluedroid_enable(); //开启蓝牙
esp_ble_gap_register_callback(esp_gap_cb); //注册回调函数
由于Beacon的广播频率为2s
,因此将扫描频率设置为3s
,来保证能够搜到蓝牙广播包。初始化完毕后,只需要在主循环中不断循环扫描,并在回调函数中处理搜索结果即可:
while(1) {
esp_ble_gap_start_scanning(3) //扫描2秒
vTaskDelay(pdMS_TO_TICKS(5000));
}
const char *device_name_filter = "CH573-Sensor";
const char vendor_id[2] = {'Y', 'Z'};
/**
* @brief 在蓝牙广播包中寻找特定字段
*
* @param adv_data 广播数据
* @param adv_data_len 广播数据长度
* @param type 要查找的字段标识
* @return uint8_t* 字段起始位置,包括长度与类型
*/
uint8_t *ble_adv_get_data(uint8_t *adv_data, uint8_t adv_data_len, uint8_t type)
{
uint8_t *ptr = adv_data;
uint8_t len = adv_data_len;
while(len) {
if(ptr[1] != type) { //第2个字节是字段类型
len -= (ptr[0]+1); //第1个字节是字段长度
ptr += ptr[0]+1; //不是要寻找的字段类型,移动到下一个字段
} else { //是要寻找的字段
return ptr;
}
}
return NULL;
}
void ble_adv_scan_handler(struct ble_scan_result_evt_param *scan_rst)
{
if(scan_rst->adv_data_len == 0) { //忽略没有广播内容的设备
return;
}
uint8_t *adv_manufacturer_defined_data =
ble_adv_get_data(scan_rst->ble_adv, scan_rst->adv_data_len, 0xFF); //查找厂家自定义信息
if(adv_manufacturer_defined_data == NULL) {
return;
}
/* 检查厂商ID */
if(adv_manufacturer_defined_data[2] == vendor_id[0] && adv_manufacturer_defined_data[3] == vendor_id[1]) {
/* 是传感器设备 */
int16_t temp = adv_manufacturer_defined_data[6] | adv_manufacturer_defined_data[7]<<8;
int16_t humid = adv_manufacturer_defined_data[9] | adv_manufacturer_defined_data[10]<<8;
uint8_t voltage = adv_manufacturer_defined_data[12];
printf(MACSTR": rssi:%d, temp:%.1fC, humid:%.1f%%, batt:%.2fV\n", MAC2STR(scan_rst->bda),
scan_rst->rssi, (float)temp/100, (float)humid/100, (float)voltage/100+2.0f); //打印设备信息与温湿度信息
}
}
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
esp_err_t err;
switch (event) {
case ESP_GAP_BLE_SCAN_RESULT_EVT: //扫描结果事件,每一个扫描到的设备均会触发一次该函数
if ((err = param->scan_start_cmpl.status) == ESP_BT_STATUS_SUCCESS) {
// printf(MACSTR": rssi:%d, \n", MAC2STR(param->scan_rst.bda), param->scan_rst.rssi); //打印设备信息
ble_adv_scan_handler(¶m->scan_rst);
}
break;
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: //开始扫描完成事件
if ((err = param->scan_start_cmpl.status) == ESP_BT_STATUS_SUCCESS) {
ESP_LOGI(TAG, "start scanning...");
} else {
ESP_LOGE(TAG, "error while starting scanning, error code %s", esp_err_to_name(err));
}
break;
default:
ESP_LOGW(TAG, "unhandled bt event #%d", event);
break;
}
}
使用上述代码,即可扫描并获取传感器数据了。
若有多个传感器,也可以根据MAC地址区分它们。从扫描结果中可以看出,SHT31
传感器的精度与一致性非常不错。