自从部署 HomeAssistant 后就忍不住试图测量和智能化一切东西,所以急迫的想做一款蓝牙传感器,同时这也是第一次尝试使用 nRF 和 Zephyr 并进行低功耗优化。
原理图非常简单,仅有一颗 nRF52832 和 SHT4x 传感器,使用一颗CR2032供电——因此需要堆叠电容避免在低温环境下内阻过高瞬时压降导致重启。
由于 Zephyr 已经包含了 SHT40 的驱动和全部所必须的低功耗处理部分,只需要简单编写一些胶水即可使其顺利工作。值得注意的是,任何情况下激活调试端口会大幅提高本底电流值,因此在最终构建中需要完全移除 CONSOLE 配置。同样,saadc 因为调用的是 nrfx 库而不能正确在空闲线程中休眠,所以在每次读取结束后都需要 deinit 下电。对于 Zephyr,ktimer 中不能执行过长时间的任务(甚至可能不足以执行任何库函数!),否则可能导致一些竞态。对于生产应用来说,在 UICR_CUSTOM[15:0] 为非全 0xFF 时将选取 UICR 而非预定义密钥。完整工程可以在 https://github.com/AlanCui4080/nRF52832SHT40Sensor 找到。
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/crypto.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/pm/device.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
#include "main.h"
#if !DT_HAS_COMPAT_STATUS_OKAY(sensirion_sht4x)
#error "No sensirion,sht4x compatible node found in the device tree"
#endif
static const struct device* sht40_device;
#if CONFIG_BT_ID_MAX > 1
#error "This application supports only one Bluetooth identity"
#endif
static const uint8_t* const uicr_predefined_key = (const uint8_t*)(0x10001000 + 0x080); // UICR COUSTOM[15:0]
static uint8_t encryption_nonce[16] = { 0 }; // actually 13 bytes
static const uint8_t predefined_key[16] = { 0x8b, 0xa5, 0x91, 0xa5, 0xef, 0x8f, 0xd5, 0x99,
0x90, 0x31, 0x6d, 0x38, 0xe0, 0x4a, 0xe9, 0xed };
#define ADV_PARAM BT_LE_ADV_PARAM(BT_LE_ADV_OPT_USE_IDENTITY | BT_LE_ADV_OPT_CODED | BT_LE_ADV_OPT_REQUIRE_S8_CODING, BT_ADV_MIN_INTERVAL, BT_ADV_MAX_INTERVAL, NULL)
static struct bthome_raw_data raw_data = { .battery_object_id = BTHOMEV2_OBJID_BATTERY_U8_1,
.battery_level = 42,
.temperature_object_id = BTHOMEV2_OBJID_TEMPERATURE_S16_0P01,
.temperature = 0000,
.humidity_object_id = BTHOMEV2_OBJID_HUMIDITY_U16_0P01,
.humidity = 0000 };
static struct bthome_payload advertising_payload = { .service_uuid = { BT_UUID_16_ENCODE(BTHOMEV2_SERVICE_UUID) },
.device_info_byte = BTHOMEV2_DIB_ENCRYPTED | BTHOMEV2_DIB_VER,
.encrypted_data = { 0 },
.counter = 0x00000000,
.mic = { 0 } };
static struct bt_data advertising_data[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR),
BT_DATA(BT_DATA_SVC_DATA16, &advertising_payload, sizeof(advertising_payload))
};
static struct bt_data scan_response_data[] = {
BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};
#include <nrfx_saadc.h>
#define SAADC_INPUT_PIN NRFX_ANALOG_INTERNAL_VDD
static int16_t battery_sample_voltage;
static nrfx_saadc_channel_t battery_sample_channel = NRFX_SAADC_DEFAULT_CHANNEL_SE(SAADC_INPUT_PIN, 0);
static int battery_sample_time_due = 0;
void battery_sample()
{
LOG_INF("fetching saadc");
nrfx_err_t err = nrfx_saadc_init(DT_IRQ(DT_NODELABEL(adc), priority));
if (err != 0)
{
LOG_ERR("nrfx_saadc_mode_trigger error: %d", (int)err);
return;
}
battery_sample_channel.channel_config.gain = NRF_SAADC_GAIN1_6;
err = nrfx_saadc_channels_config(&battery_sample_channel, 1);
if (err != 0)
{
LOG_ERR("nrfx_saadc_channels_config error: %d", (int)err);
return;
}
err = nrfx_saadc_simple_mode_set(BIT(0), NRF_SAADC_RESOLUTION_12BIT, NRF_SAADC_OVERSAMPLE_128X, NULL);
if (err != 0)
{
LOG_ERR("nrfx_saadc_simple_mode_set error: %d", (int)err);
return;
}
err = nrfx_saadc_buffer_set(&battery_sample_voltage, 1);
if (err != 0)
{
LOG_ERR("nrfx_saadc_buffer_set error: %d", (int)err);
return;
}
err = nrfx_saadc_offset_calibrate(NULL);
if (err != 0)
{
LOG_ERR("nrfx_saadc_offset_calibrate error: %d", (int)err);
return;
}
err = nrfx_saadc_mode_trigger();
if (err != 0)
{
LOG_ERR("nrfx_saadc_mode_trigger error: %d", (int)err);
return;
}
int batt_volt = ((600 * 6) * battery_sample_voltage) / ((1 << 12));
raw_data.battery_level = (batt_volt > 3200) ? 100 : (batt_volt < 2500) ? 0 : (batt_volt - 2500) / 7;
nrfx_saadc_uninit(); // save power
LOG_INF(
"sampled battery voltage: %d mV, orignal data: %hd, level: %hhu%%",
batt_volt,
battery_sample_voltage,
raw_data.battery_level);
}
void battery_sample_timer_handler(struct k_timer* timer)
{
battery_sample_time_due = 1;
}
K_TIMER_DEFINE(battery_sample_timer, battery_sample_timer_handler, NULL);
static int sensor_sample_time_due = 0;
void sensor_sample()
{
if (sht40_device == NULL)
{
LOG_ERR("SHT4X device pointer is NULL");
return;
}
if (!device_is_ready(sht40_device))
{
LOG_ERR("sht40 %s is not ready", sht40_device->name);
return;
}
else
{
LOG_INF("sht40 %s device ready", sht40_device->name);
}
if (sensor_sample_fetch(sht40_device))
{
LOG_ERR("fetch sample from SHT4X device failed ");
return;
}
struct sensor_value sht40_t = { 0 };
struct sensor_value sht40_rh = { 0 };
sensor_channel_get(sht40_device, SENSOR_CHAN_AMBIENT_TEMP, &sht40_t); // data zero initialized so failure is
sensor_channel_get(sht40_device, SENSOR_CHAN_HUMIDITY, &sht40_rh);
raw_data.temperature = (int16_t)(sht40_t.val1 * 100 + sht40_t.val2 / 10000);
raw_data.humidity = (uint16_t)(sht40_rh.val1 * 100 + sht40_rh.val2 / 10000);
LOG_INF("sample fetched from SHT4X device: temp=%hd, humidity=%hu", raw_data.temperature, raw_data.humidity);
}
void sensor_sample_timer_handler(struct k_timer* timer)
{
sensor_sample_time_due = 1;
}
K_TIMER_DEFINE(sensor_sample_timer, sensor_sample_timer_handler, NULL);
static int encrypt_init()
{
int result = bt_rand(&(advertising_payload.counter), sizeof(advertising_payload.counter));
if (result)
{
LOG_INF("counter initlization by bt_rand() filling failed: %d", result);
return -1;
}
bt_addr_le_t mac_addr[CONFIG_BT_ID_MAX];
size_t mac_count = CONFIG_BT_ID_MAX;
bt_id_get(mac_addr, &mac_count);
if (mac_count == 0)
{
LOG_INF("nonce initlization by bt_id_get() failed: no MAC addresses found");
return -1;
}
for (size_t i = 0; i < mac_count; i++)
{
char addr_str[BT_ADDR_LE_STR_LEN] = { 0 };
bt_addr_le_to_str(&mac_addr[i], addr_str, sizeof(addr_str));
LOG_INF("device MAC address %u: %s", (unsigned int)i, addr_str);
}
encryption_nonce[5] = mac_addr[0].a.val[0];
encryption_nonce[4] = mac_addr[0].a.val[1];
encryption_nonce[3] = mac_addr[0].a.val[2];
encryption_nonce[2] = mac_addr[0].a.val[3];
encryption_nonce[1] = mac_addr[0].a.val[4];
encryption_nonce[0] = mac_addr[0].a.val[5];
memcpy(encryption_nonce + 6, advertising_payload.service_uuid, 2);
memcpy(encryption_nonce + 8, &(advertising_payload.device_info_byte), 1);
memcpy(encryption_nonce + 9, &(advertising_payload.counter), 4);
LOG_INF("encryption initialization completed");
return 0;
}
static int encrypt_payload(
struct bthome_payload* payload,
const struct bthome_raw_data* raw_data,
uint8_t* nonce,
const uint8_t* key)
{
if (payload == NULL || raw_data == NULL || nonce == NULL || key == NULL)
{
LOG_INF("encrypt_payload() invalid parameter");
return -1;
}
memcpy(nonce + 9, &(payload->counter), 4);
uint8_t enc_data_buffer[sizeof(struct bthome_raw_data) + 4] = { 0 };
int result =
bt_ccm_encrypt(key, nonce, (uint8_t*)raw_data, sizeof(struct bthome_raw_data), NULL, 0, enc_data_buffer, 4);
if (result)
{
LOG_INF("bt_ccm_encrypt() failed: %d", result);
return result;
}
memcpy(&(payload->encrypted_data), enc_data_buffer, sizeof(struct bthome_raw_data));
memcpy(&(payload->mic), enc_data_buffer + sizeof(struct bthome_raw_data), 4);
return result;
}
static void bt_ready(int result)
{
if (result)
{
LOG_ERR("bluetooth init failed: %d", result);
return;
}
result = bt_le_adv_start(
ADV_PARAM, advertising_data, ARRAY_SIZE(advertising_data), scan_response_data, ARRAY_SIZE(scan_response_data));
if (result)
{
LOG_ERR(
"bluetooth adsr failed to set data: %d, sizeof(advertising_payload):%d",
result,
sizeof(advertising_payload));
return;
}
LOG_INF("bluetooth advertising started");
}
int main(void)
{
LOG_INF("Alan nRF52832SHT40 BTHomev2 Sensor");
LOG_INF("fetching sht40");
sht40_device = DEVICE_DT_GET_ANY(sensirion_sht4x);
if (sht40_device == NULL)
{
LOG_ERR("sht40 device not found");
return -1;
}
else
{
LOG_INF("sht40 device found: %s", sht40_device->name);
}
LOG_INF("starting battery monitoring timer");
k_timer_start(&battery_sample_timer, K_NO_WAIT, K_MSEC(BATTERY_SAMPLE_INTERVAL_MS));
LOG_INF("starting sensor timer");
k_timer_start(&sensor_sample_timer, K_NO_WAIT, K_MSEC(SENSOR_SAMPLE_INTERVAL_MS));
LOG_INF("enabling bluetooth");
int result = bt_enable(bt_ready);
if (result)
{
LOG_ERR("bluetooth init failed: %d", result);
return -1;
}
else
{
LOG_INF("bluetooth ready");
}
result = encrypt_init();
if (result)
{
LOG_ERR("encryption initialization failed: %d", result);
return -1;
}
if (memcmp(uicr_predefined_key, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", 16) != 0)
{
LOG_INF("using UICR predefined key for encryption");
}
else
{
LOG_INF("using default predefined key for encryption");
}
while (1)
{
if (battery_sample_time_due)
{
battery_sample_time_due = 0;
battery_sample();
}
if (sensor_sample_time_due)
{
sensor_sample_time_due = 0;
sensor_sample();
}
if (memcmp(uicr_predefined_key, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", 16) != 0)
{
result = encrypt_payload(&advertising_payload, &raw_data, encryption_nonce, uicr_predefined_key);
}
else
{
result = encrypt_payload(&advertising_payload, &raw_data, encryption_nonce, predefined_key);
}
if (result)
{
LOG_ERR("payload encryption failed: %d", result);
continue;
}
result = bt_le_adv_update_data(
advertising_data, ARRAY_SIZE(advertising_data), scan_response_data, ARRAY_SIZE(scan_response_data));
if (result)
{
LOG_ERR("bluetooth adsr update failed to set data: %d", result);
continue;
}
advertising_payload.counter++;
k_sleep(K_MSEC(BT_TICK_TO_MSEC(BT_ADV_MIN_INTERVAL)));
}
return 0;
}在测试中,三颗成品保持了相当的一致性,平均电流5.47uA,预计使用寿命可达三年十个月。在启用 BT_LE_ADV_OPT_REQUIRE_S8_CODING 后,信噪比提升了约 7dB,使得全屋传感器仅需使用一个配有 8dB 的胶棒天线蓝牙终端收集。

