/* BTHome v2 temperature / humidity beacon – ultra-low-power ------------------------------------------------------------ Board : Pro Micro nRF52840 (nice!nano v2 compatible), powered by CR2032 Sensor : Adafruit SHT41 (I2C addr 0x44), power-gated from a GPIO Core : Adafruit nRF52 Arduino core Tools > Board > "Adafruit Feather nRF52840 Express" (same UF2 bootloader + S140 SoftDevice as the nice!nano) Behaviour - Measures T/RH + battery voltage every 60 s - Sensor is powered ONLY during the measurement (~15 ms) - Non-connectable BTHome v2 advertisement every 10 s - DC/DC enabled on REG0 (VDDH->VDD) and REG1 (VDD->1.3 V core) - CPU sleeps (FreeRTOS tickless idle -> sd_app_evt_wait) the rest of the time All pins below are RAW nRF GPIO numbers (port * 32 + pin), driven directly through registers, so the Feather pin mapping does not matter. */ #include // ---------------- user settings ---------------- #define SENS_PWR_PIN 17 // P0.17 (Pro Micro "D2" / "017") -> SHT41 VIN #define SENS_SDA_PIN 20 // P0.20 (Pro Micro "D3" / "020") -> SHT41 SDA #define SENS_SCL_PIN 22 // P0.22 (Pro Micro "D4" / "022") -> SHT41 SCL #define MEAS_INTERVAL_MS 60000UL // measurement period #define ADV_INTERVAL 16000 // 10000 ms / 0.625 ms #define TX_POWER_DBM 0 // -40,-20,-16,-12,-8,-4,0,2..8 (lower = less power, less range) #define DEVICE_NAME "SHT41" #define SHT41_ADDR 0x44 #define SHT41_CMD_MEAS_HP 0xFD // high precision, no heater (max 8.3 ms) // ---------------- state ---------------- static int16_t g_temp10 = 0; // °C * 10 (BTHome 0x45) static uint8_t g_rh = 0; // %RH, 1 % (BTHome 0x2E) static uint16_t g_battMv = 0; // mV (BTHome 0x0C) static uint8_t g_i2cBuf[6]; // EasyDMA buffer (must be RAM): TX = cmd byte, RX = 6 data bytes // ===================================================================== // Helpers // ===================================================================== #define PIN_CNF(dir, in, pull, drive) \ ((GPIO_PIN_CNF_DIR_##dir << GPIO_PIN_CNF_DIR_Pos) | \ (GPIO_PIN_CNF_INPUT_##in << GPIO_PIN_CNF_INPUT_Pos) | \ (GPIO_PIN_CNF_PULL_##pull << GPIO_PIN_CNF_PULL_Pos) | \ (GPIO_PIN_CNF_DRIVE_##drive << GPIO_PIN_CNF_DRIVE_Pos)) #define CNF_HIGHZ PIN_CNF(Input, Disconnect, Disabled, S0S1) // no input buffer, no pull: ~0 leakage #define CNF_I2C PIN_CNF(Input, Connect, Pullup, S0D1) // open-drain + internal pull-up (backup) #define CNF_PWR PIN_CNF(Output, Disconnect, Disabled, H0H1) // high-drive push-pull static NRF_GPIO_Type* port(uint32_t pin) { return (pin & 32) ? NRF_P1 : NRF_P0; } static void pinCfg(uint32_t pin, uint32_t cnf) { port(pin)->PIN_CNF[pin & 31] = cnf; } static void busPins(uint32_t cnf) { pinCfg(SENS_SDA_PIN, cnf); pinCfg(SENS_SCL_PIN, cnf); } static void sensorPower(bool on) { // set level first, then make it an output (no glitch) NRF_GPIO_Type* p = port(SENS_PWR_PIN); (on ? p->OUTSET : p->OUTCLR) = 1UL << (SENS_PWR_PIN & 31); pinCfg(SENS_PWR_PIN, CNF_PWR); } static void sensorOff() { // bus high-Z BEFORE removing power (no back-feeding) busPins(CNF_HIGHZ); sensorPower(false); // held low = sensor fully off } // Clear event, fire task, wait (with timeout) for the event static void trigger(volatile uint32_t& task, volatile uint32_t& evt, uint32_t timeoutMs) { evt = 0; task = 1; for (uint32_t t0 = millis(); !evt && millis() - t0 < timeoutMs; ) {} } // ===================================================================== // Minimal TWIM (I2C master) – enabled only during a measurement // ===================================================================== static bool twimRun(volatile uint32_t& task, uint32_t shorts) { NRF_TWIM0->EVENTS_STOPPED = 0; NRF_TWIM0->EVENTS_ERROR = 0; NRF_TWIM0->SHORTS = shorts; task = 1; uint32_t t0 = millis(); while (!NRF_TWIM0->EVENTS_STOPPED && !NRF_TWIM0->EVENTS_ERROR && millis() - t0 <= 20) {} bool ok = NRF_TWIM0->EVENTS_STOPPED && !NRF_TWIM0->EVENTS_ERROR; if (!ok) { // NACK / bus error / timeout: force STOP trigger(NRF_TWIM0->TASKS_STOP, NRF_TWIM0->EVENTS_STOPPED, 5); NRF_TWIM0->ERRORSRC = NRF_TWIM0->ERRORSRC; // write-1-to-clear NRF_TWIM0->EVENTS_ERROR = 0; } NRF_TWIM0->EVENTS_STOPPED = 0; return ok; } static uint8_t shtCrc(const uint8_t* d) { // CRC-8, poly 0x31, init 0xFF uint8_t crc = 0xFF; for (int i = 0; i < 2; i++) { crc ^= d[i]; for (int b = 0; b < 8; b++) crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : (crc << 1); } return crc; } // ===================================================================== // SHT41 measurement with power gating (updates g_temp10 / g_rh on success) // ===================================================================== static bool readSHT41() { uint8_t* b = g_i2cBuf; sensorPower(true); busPins(CNF_I2C); delay(3); // SHT41 power-up <1 ms + breakout caps NRF_TWIM0->ENABLE = TWIM_ENABLE_ENABLE_Enabled; b[0] = SHT41_CMD_MEAS_HP; bool ok = twimRun(NRF_TWIM0->TASKS_STARTTX, TWIM_SHORTS_LASTTX_STOP_Msk); if (ok) { delay(10); // CPU sleeps during conversion ok = twimRun(NRF_TWIM0->TASKS_STARTRX, TWIM_SHORTS_LASTRX_STOP_Msk) && shtCrc(b) == b[2] && shtCrc(b + 3) == b[5]; } NRF_TWIM0->ENABLE = TWIM_ENABLE_ENABLE_Disabled; sensorOff(); // power down before doing the math if (!ok) return false; // T = -45 + 175*raw/65535 (0.1 °C units), RH = -6 + 125*raw/65535 (1 %), one rounding step uint32_t rawT = (b[0] << 8) | b[1]; uint32_t rawRH = (b[3] << 8) | b[4]; g_temp10 = -450 + (int32_t)((1750UL * rawT + 32767UL) / 65535UL); int32_t rh = -6 + (int32_t)((125UL * rawRH + 32767UL) / 65535UL); g_rh = constrain(rh, 0, 100); return true; } // ===================================================================== // Battery voltage: VDDH/5 via SAADC (no external divider needed) // ===================================================================== static uint16_t readBatteryMv() { static volatile int16_t result; static bool calibrated = false; NRF_SAADC->RESOLUTION = SAADC_RESOLUTION_VAL_12bit; NRF_SAADC->OVERSAMPLE = SAADC_OVERSAMPLE_OVERSAMPLE_Over8x; NRF_SAADC->CH[0].CONFIG = // + internal 0.6 V ref, single-ended, no R (all 0) (SAADC_CH_CONFIG_GAIN_Gain1_2 << SAADC_CH_CONFIG_GAIN_Pos) | // range 0..1.2 V (SAADC_CH_CONFIG_TACQ_10us << SAADC_CH_CONFIG_TACQ_Pos) | (SAADC_CH_CONFIG_BURST_Enabled << SAADC_CH_CONFIG_BURST_Pos); NRF_SAADC->CH[0].PSELP = SAADC_CH_PSELP_PSELP_VDDHDIV5; NRF_SAADC->CH[0].PSELN = SAADC_CH_PSELN_PSELN_NC; NRF_SAADC->RESULT.PTR = (uint32_t)&result; NRF_SAADC->RESULT.MAXCNT = 1; NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Enabled; if (!calibrated) { trigger(NRF_SAADC->TASKS_CALIBRATEOFFSET, NRF_SAADC->EVENTS_CALIBRATEDONE, 10); calibrated = true; } trigger(NRF_SAADC->TASKS_START, NRF_SAADC->EVENTS_STARTED, 5); trigger(NRF_SAADC->TASKS_SAMPLE, NRF_SAADC->EVENTS_END, 5); trigger(NRF_SAADC->TASKS_STOP, NRF_SAADC->EVENTS_STOPPED, 5); NRF_SAADC->ENABLE = SAADC_ENABLE_ENABLE_Disabled; // ADC off between readings int32_t raw = result; return raw < 0 ? 0 : raw * 6000L / 4096L; // raw/4096 * 1.2 V * 5 } // ===================================================================== // BTHome v2 advertisement (objects in ascending ID order: 0x0C, 0x2E, 0x45) // ===================================================================== static void updateAdvertising() { const uint8_t sd[] = { 0xD2, 0xFC, // UUID 0xFCD2 (little endian) 0x40, // BTHome v2, unencrypted, regular interval 0x0C, uint8_t(g_battMv), uint8_t(g_battMv >> 8), // voltage, 1 mV 0x2E, g_rh, // humidity, 1 %, uint8 0x45, uint8_t(g_temp10), uint8_t(uint16_t(g_temp10) >> 8) // temperature, 0.1 °C, sint16 }; Bluefruit.Advertising.stop(); Bluefruit.Advertising.clearData(); Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_SERVICE_DATA, sd, sizeof(sd)); Bluefruit.Advertising.addName(); Bluefruit.Advertising.start(0); // advertise forever } // force = publish even if the measurement failed (used once at boot) static void measureAndPublish(bool force = false) { if (readSHT41()) { g_battMv = readBatteryMv(); // read right after sensor load } else if (!force) { return; // keep advertising last good values } updateAdvertising(); } // ===================================================================== void setup() { // No Serial.begin(): USB stays unused (it is unpowered on battery anyway) // DC/DC on both regulators – write directly while SoftDevice is still off uint8_t sdOn = 0; sd_softdevice_is_enabled(&sdOn); if (!sdOn) { NRF_POWER->DCDCEN0 = 1; // REG0: VDDH -> VDD NRF_POWER->DCDCEN = 1; // REG1: VDD -> core } sensorOff(); // TWIM settings are retained while disabled; pins are only taken over when ENABLE is set NRF_TWIM0->PSEL.SCL = SENS_SCL_PIN; NRF_TWIM0->PSEL.SDA = SENS_SDA_PIN; NRF_TWIM0->FREQUENCY = TWIM_FREQUENCY_FREQUENCY_K400; NRF_TWIM0->ADDRESS = SHT41_ADDR; NRF_TWIM0->TXD.PTR = (uint32_t)g_i2cBuf; NRF_TWIM0->TXD.MAXCNT = 1; NRF_TWIM0->RXD.PTR = (uint32_t)g_i2cBuf; NRF_TWIM0->RXD.MAXCNT = 6; Bluefruit.autoConnLed(false); // no blinking LED while advertising Bluefruit.begin(); sd_power_dcdc_mode_set(NRF_POWER_DCDC_ENABLE); // keep REG1 DC/DC on under SoftDevice Bluefruit.setTxPower(TX_POWER_DBM); Bluefruit.setName(DEVICE_NAME); Bluefruit.Advertising.setType(BLE_GAP_ADV_TYPE_NONCONNECTABLE_NONSCANNABLE_UNDIRECTED); Bluefruit.Advertising.setInterval(ADV_INTERVAL, ADV_INTERVAL); Bluefruit.Advertising.restartOnDisconnect(false); measureAndPublish(true); // start advertising even if first read fails } void loop() { delay(MEAS_INTERVAL_MS); // tickless idle -> System ON sleep measureAndPublish(); }