Just got my nRF52840 board—can’t believe how tiny it is! Excited to start experimenting with it. In the photo, it’s shown next to the ESP32-H2-DevKitM-1.
You will find the Nordic nRF52840 far less thirsty for power than the Espressif series.
Check out some of the Nordic application notes, which I vaguely recall included a sample water meter design.
I did some initial experimentation, starting with a sketch in the Arduino IDE. I used the Adafruit Feather nRF52840 Express profile, which is the closest match for my nRF52840-compatible Nice Nano v2 board (supermini nrf52840) and provides BLE support.
The setup was powered by a single 18650 battery connected to P0.04 (AIN2, B+) and GND (B−). A resistive voltage divider (100 kΩ) was used for battery measurement, connected between P0.04 (AIN2), P0.31 (AIN7), and GND.
With this wiring, the battery starts charging when USB is connected.
The sketch transmits battery percentage and pulse count over BLE. The counter value is saved to non-volatile memory every 10 pulses.
#include <bluefruit.h>
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>
using namespace Adafruit_LittleFS_Namespace;
const int REED_PIN = 11; // D1, pin P.06 jn the board
const char FILENAME[] = "/gas.bin";
const uint32_t SAVE_EVERY = 10; // save every 10 pulses
struct SavedData {
uint32_t magic;
uint32_t count;
uint32_t checksum;
};
volatile uint32_t pulseCount = 0;
volatile bool updated = false;
uint32_t savedCount = 0;
uint8_t cachedBattery = 100;
File file(InternalFS);
// --- Battery ---
// Devider 100кΩ+100кΩ connected to RAW, GND and pin P0.31 (PIN_A7)
float readVDD() {
analogReference(AR_DEFAULT);
analogReadResolution(12);
analogRead(PIN_A7); // idle reading for stabilization
delay(5);
long sum = 0;
for (int i = 0; i < 10; i++) {
sum += analogRead(PIN_A7);
delay(2);
}
int raw = sum / 10;
// devider 1:2, base 3.6V, 12 bit
return raw * 3.6f / 4096.0f * 2.0f;
}
uint8_t measureBattery() {
float vdd = readVDD();
Serial.print("VDD: ");
Serial.print(vdd);
Serial.println("V");
// 18650: 4.2В = 100%, 3.0В = 0%
float pct = (vdd - 3.0f) / (4.2f - 3.0f) * 100.0f;
if (pct > 100) pct = 100;
if (pct < 0) pct = 0;
return (uint8_t)pct;
}
// --- Flash ---
uint32_t calcChecksum(uint32_t count) {
return count ^ 0xDEADBEEF;
}
void saveCount() {
SavedData data;
data.magic = 0xDEADBEEF;
data.count = pulseCount;
data.checksum = calcChecksum(pulseCount);
InternalFS.remove(FILENAME);
file.open(FILENAME, FILE_O_WRITE);
if (file) {
file.write((uint8_t*)&data, sizeof(data));
file.close();
savedCount = pulseCount;
Serial.print("Saved: ");
Serial.println(pulseCount);
} else {
Serial.println("Save FAILED!");
}
}
void loadCount() {
file.open(FILENAME, FILE_O_READ);
if (file) {
SavedData data;
file.read((uint8_t*)&data, sizeof(data));
file.close();
if (data.magic == 0xDEADBEEF &&
data.checksum == calcChecksum(data.count)) {
pulseCount = data.count;
savedCount = data.count;
Serial.print("Loaded OK: ");
Serial.println(pulseCount);
} else {
pulseCount = 0;
savedCount = 0;
Serial.println("WARNING: corrupted data, starting from 0!");
}
} else {
pulseCount = 0;
savedCount = 0;
Serial.println("No saved data, starting from 0");
}
}
// --- BLE BTHome ---
void startAdvertising() {
Bluefruit.Advertising.stop();
Bluefruit.Advertising.clearData();
Bluefruit.ScanResponse.clearData();
// BTHome v2: UUID 0xFCD2 + counter uint32 + battery %
uint8_t serviceData[10];
serviceData[0] = 0xD2; // UUID LSB
serviceData[1] = 0xFC; // UUID MSB
serviceData[2] = 0x40; // BTHome v2, no encryption
serviceData[3] = 0x01; // Object ID: battery % — first
serviceData[4] = cachedBattery;
serviceData[5] = 0x09; // Object ID: count uint32
serviceData[6] = pulseCount & 0xFF; // Counter data
serviceData[7] = (pulseCount >> 8) & 0xFF;
serviceData[8] = (pulseCount >> 16) & 0xFF;
serviceData[9] = (pulseCount >> 24) & 0xFF;
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_SERVICE_DATA, serviceData, 10);
Bluefruit.ScanResponse.addName();
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(160, 160);
Bluefruit.Advertising.start(0);
}
// --- Reed sensor interupt ---
void reedISR() {
static unsigned long lastTime = 0;
unsigned long now = millis();
if (now - lastTime > 100) { // debounce 100мс
pulseCount++;
updated = true;
lastTime = now;
}
}
// --- Setup ---
void setup() {
Serial.begin(115200);
for (int i = 0; i < 50; i++) {
if (Serial) break;
delay(100);
}
delay(500);
Serial.println("=== STARTING ===");
cachedBattery = measureBattery();
Serial.print("Battery: ");
Serial.print(cachedBattery);
Serial.println("%");
InternalFS.begin();
loadCount();
pinMode(REED_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(REED_PIN), reedISR, FALLING);
Bluefruit.begin();
Bluefruit.setName("GasCounter");
Bluefruit.setTxPower(4);
startAdvertising();
Serial.println("Gas counter started");
Serial.print("Current count: ");
Serial.println(pulseCount);
}
// --- Loop ---
void loop() {
if (Serial.available()) {
char cmd = Serial.read();
if (cmd == 'R' || cmd == 'r') {
pulseCount = 0;
savedCount = 0;
saveCount();
startAdvertising();
Serial.println("Counter RESET to 0!");
}
}
// Battery refresh once hour
static unsigned long lastBattUpdate = 0;
if (millis() - lastBattUpdate > 3600000UL) {
lastBattUpdate = millis();
Bluefruit.Advertising.stop();
cachedBattery = measureBattery();
startAdvertising();
Serial.print("Battery updated: ");
Serial.print(cachedBattery);
Serial.println("%");
}
if (updated) {
updated = false;
startAdvertising();
Serial.print("Pulse! Count: ");
Serial.println(pulseCount);
if (pulseCount - savedCount >= SAVE_EVERY) {
saveCount();
}
}
}
Interesting to test is it will be possible to use internal divider in purpose of battery measurement:
float readVDD() {
analogReference(AR_DEFAULT);
analogReadResolution(12);
analogRead(PIN_A2); // battery +
delay(5);
long sum = 0;
for (int i = 0; i < 10; i++) {
sum += analogRead(PIN_A2);
delay(2);
}
int raw = sum / 10;
// nice!nano divider: 806кОм + 2.0МОм → ~3.484
// Vref = 0.6V, gain = 1/6 → value = 3.6V
float voltage = raw * 3.6f / 4096.0f * (2000.0f + 806.0f) / 806.0f;
return voltage;
}
Do you mind to explain better. If B+ was internally connected to P0.04, why would you connect it to P0.31 as well? And what divider ratio you used ?
I’ve connected resistive divider to the P0.31, 100kOm to measure voltage of the battery
My pinouts might looks like that
Divider has two resistors… So you use 100k / 100k ?
I’m asking because v1 and v2 have different schematics. Your pinout above is for v1, but you wrote you have Nice Nano v2. Does your board has version printed?
Schematic for v1 shows that battery pin is connected to P0.04 through 800k/2M divider. V2 doesn’t show any connection.
Edit: looks like these ProMicro aliuexpress boards don’t respect schematic of nice!nano
I’ve ordered this board, listed as a “Supermini nRF52840,” but I’m not sure whether it’s a V1 or V2 version, even though V2 compatibility is mentioned. However, based on the pictures, the pin layout appears to be usable.
Please note that I didn’t say I have a Nice Nano; I meant that the board is Nice Nano–compatible.
In any case, you can use any available GPIO pins that suit your setup.
Of course. I was just curious about different version schematics. It’s kind of jungle.
The one on the photo does not have B+ connected to P0.04.
In the photo, B+ (P0.04, AIN2) is connected to the red wire (+3.7V battery) in the top-right corner of the board
.
I just drew the circuit to make it easier to understand.
But your actual board does not have that interconnection to P0.04. And there are other differences compared to genuine nice!nano as well.
A small note: the reed sensor sensitivity may not be sufficient at 40–50 AT.
Off-topic, but for anyone else trying to understand these nice!nano V2 clones labeled ProMicro or Supermini nRF52840;
Genuine nice!nano v1 has power path:
Battery/USB >> 3.3V external regulator >> chip VDD_NRF.
It has also onboard voltage divider connected to analog input P0.04.
Genuine nice!nano v2 has completely different approach, nrf52 is powered by battery/usb passed through sophisticated charger/power-path IC.
External voltage regulator powers only board VCC pin. NRF52 is powered at battery/usb voltage on VDDH.
No voltage measurement circuit.
This 3$ clone board uses same approach than nice!nano v2, but doesnt have power-path IC. It uses only mosfet switch to select battery/usb and that goes directly to chip VDDH (nRF52 has two internal regulators).
Battery charge IC is TP4054
Voltage regulator is ME6211 and powers only VCC pin, not connected to nRF52.
Batt pin is not connected to analog input P0.04.
All of these boards have capability to turn off VCC pin by setting P0.13 level.
Clone board does this by disabling ME6211 regulator.
A bit power optimized BLE sketch
#include <bluefruit.h>
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>
using namespace Adafruit_LittleFS_Namespace;
const int REED_PIN = 11;
const char FILENAME[] = "/gas.bin";
const uint32_t SAVE_EVERY = 10;
unsigned long lastModeSwitch = 0;
bool inBurstMode = true; // starting to burst
// --- Adv timings ---
// Adv timing: 3000 ms in idle, 500 ms after pulse
#define ADV_INTERVAL_IDLE_MS 3000 // ~3 sek off
#define ADV_INTERVAL_BURST_MS 500 // 0.5 sek after pulse
#define ADV_BURST_DURATION_MS 6000 // 6 sek "quick" adv after pulse, then idle
#define ADV_STOP_AFTER_MS 0 // total off adv if no pulses 60 sek (60000)
// set 0 to turn off
#define BATT_UPDATE_INTERVAL_MS 3600000UL // 1 hour
struct SavedData {
uint32_t magic;
uint32_t count;
uint32_t checksum;
};
volatile uint32_t pulseCount = 0;
volatile bool pulseFlag = false;
uint32_t savedCount = 0;
uint8_t cachedBattery = 100;
unsigned long lastPulseTime = 0; // millis() from last pulse
bool advRunning = false;
File file(InternalFS);
// --- Battery ---
float readVDD() {
analogReference(AR_DEFAULT);
analogReadResolution(12);
analogRead(PIN_A7);
delay(5);
long sum = 0;
for (int i = 0; i < 10; i++) {
sum += analogRead(PIN_A7);
delay(2);
}
return (sum / 10) * 3.6f / 4096.0f * 2.0f;
}
uint8_t measureBattery() {
float vdd = readVDD();
Serial.print("VDD: "); Serial.print(vdd); Serial.println("V");
float pct = (vdd - 3.0f) / (4.2f - 3.0f) * 100.0f;
if (pct > 100) pct = 100;
if (pct < 0) pct = 0;
return (uint8_t)pct;
}
// --- Flash ---
uint32_t calcChecksum(uint32_t count) { return count ^ 0xDEADBEEF; }
void saveCount() {
SavedData data = { 0xDEADBEEF, pulseCount, calcChecksum(pulseCount) };
InternalFS.remove(FILENAME);
file.open(FILENAME, FILE_O_WRITE);
if (file) {
file.write((uint8_t*)&data, sizeof(data));
file.close();
savedCount = pulseCount;
Serial.print("Saved: "); Serial.println(pulseCount);
} else {
Serial.println("Save FAILED!");
}
}
void loadCount() {
file.open(FILENAME, FILE_O_READ);
if (file) {
SavedData data;
file.read((uint8_t*)&data, sizeof(data));
file.close();
if (data.magic == 0xDEADBEEF && data.checksum == calcChecksum(data.count)) {
pulseCount = savedCount = data.count;
Serial.print("Loaded OK: "); Serial.println(pulseCount);
} else {
pulseCount = savedCount = 0;
Serial.println("WARNING: corrupted data, starting from 0!");
}
} else {
pulseCount = savedCount = 0;
Serial.println("No saved data, starting from 0");
}
}
// --- BLE ---
void buildServiceData(uint8_t* sd) {
sd[0] = 0xD2; sd[1] = 0xFC; // UUID FCD2
sd[2] = 0x40; // BTHome v2, no encryption
sd[3] = 0x01; sd[4] = cachedBattery; // battery %
sd[5] = 0x09; // count uint32
sd[6] = (pulseCount) & 0xFF;
sd[7] = (pulseCount >> 8) & 0xFF;
sd[8] = (pulseCount >> 16) & 0xFF;
sd[9] = (pulseCount >> 24) & 0xFF;
}
// intervalMs — time between adv packets in ms
void startAdvertising(uint16_t intervalMs) {
uint16_t units = (uint16_t)(intervalMs * 1000UL / 625); // ms → units 0.625ms
Bluefruit.Advertising.stop();
Bluefruit.Advertising.clearData();
Bluefruit.ScanResponse.clearData();
uint8_t sd[10];
buildServiceData(sd);
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_SERVICE_DATA, sd, 10);
Bluefruit.ScanResponse.addName();
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(units, units);
Bluefruit.Advertising.start(0);
advRunning = true;
}
void stopAdvertising() {
Bluefruit.Advertising.stop();
advRunning = false;
Serial.println("ADV stopped (idle power save)");
}
// --- Reed ISR ---
void reedISR() {
static unsigned long lastTime = 0;
unsigned long now = millis();
if (now - lastTime > 100) {
pulseCount++;
pulseFlag = true;
lastTime = now;
}
}
// --- Setup ---
void setup() {
Serial.begin(115200);
for (int i = 0; i < 50 && !Serial; i++) delay(100);
delay(500);
Serial.println("=== STARTING ===");
cachedBattery = measureBattery();
Serial.print("Battery: "); Serial.print(cachedBattery); Serial.println("%");
InternalFS.begin();
loadCount();
pinMode(REED_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(REED_PIN), reedISR, FALLING);
Bluefruit.begin();
Bluefruit.setName("GasCounter");
Bluefruit.setTxPower(4); // transmit power
// Start with "quick" timing, then idle
lastPulseTime = millis();
startAdvertising(ADV_INTERVAL_BURST_MS);
lastModeSwitch = millis();
Serial.println("Gas counter started");
Serial.print("Current count: "); Serial.println(pulseCount);
}
// --- Loop ---
void loop() {
// ── Serial reset ───────────────────────────────────────────────
if (Serial.available()) {
char cmd = Serial.read();
if (cmd == 'R' || cmd == 'r') {
pulseCount = savedCount = 0;
saveCount();
lastPulseTime = millis();
startAdvertising(ADV_INTERVAL_BURST_MS);
Serial.println("Counter RESET to 0!");
}
}
// ── Battery refresh once an hour ─────────────────────────────────────────
static unsigned long lastBattUpdate = 0;
if (millis() - lastBattUpdate > BATT_UPDATE_INTERVAL_MS) {
lastBattUpdate = millis();
cachedBattery = measureBattery();
// Restart adv with fresh data
if (advRunning) {
uint32_t timeSincePulse = millis() - lastPulseTime;
uint16_t interval = (timeSincePulse < ADV_BURST_DURATION_MS)
? ADV_INTERVAL_BURST_MS
: ADV_INTERVAL_IDLE_MS;
startAdvertising(interval);
}
Serial.print("Battery updated: "); Serial.print(cachedBattery); Serial.println("%");
}
// ── New pulse processing ───────────────────────────────────────────────
if (pulseFlag) {
pulseFlag = false;
lastPulseTime = millis();
inBurstMode = true;
// Refresh adv data and switch to в burst-mode
startAdvertising(ADV_INTERVAL_BURST_MS);
Serial.print("Pulse! Count: "); Serial.println(pulseCount);
if (pulseCount - savedCount >= SAVE_EVERY) {
saveCount();
}
}
// ── Timing management/stop adv ─────────────────────────────
if (advRunning) {
uint32_t timeSincePulse = millis() - lastPulseTime;
#if ADV_STOP_AFTER_MS > 0
// Turn off adv if no pulses
if (timeSincePulse > ADV_STOP_AFTER_MS) {
stopAdvertising();
} else
#endif
// Switching from burst to idle mode
if (timeSincePulse > ADV_BURST_DURATION_MS) {
if (inBurstMode) {
inBurstMode = false;
startAdvertising(ADV_INTERVAL_IDLE_MS);
Serial.println("ADV -> idle interval (3s)");
}
}
}
}
I had a chance to create EspHome config for Supermini nRF52840 with Zigbee version
Works at first glance, but further testing is required.
P.S. calculated time of life around 26 days on power module of two parallel old 18650 batteries





