-
Software
11/16/2025 at 17:40 • 0 commentsThe nRF52840 is supported by Platformio, and inexpensive boards like the nice!nano can be programmed directly via their USB-port. I hoped that this would simplify software development, but there are two main issues: First, the nRF52 does not ship with a USB bootloader preinstalled, unlike the Raspberry Pi Pico. When using blank, factory-fresh chips, the only way to program them is through SWD. There’s not even a serial boot option by default.
A second problem is Zigbee. Its protocol stack is proprietary and managed by the Connectivity Standards Alliance. Without paying licensing fees, manufacturers are not allowed to use the protocol or even mention “Zigbee“ in their branding. Vendors provide prebuilt Zigbee stacks, that function as black boxes. Nordic uses the ZBOSS Zigbee protocol stack as a precompiled library, which is only fully usable within the Nordic SDK. As a result, when developing my own Zigbee board, I was tied to Nordic’s SDK and needed an SWD/JTAG programmer load the software.
There are several options for SWD debuggers, depending on the budget. The cheapest approach is buying a black pill STM32F4 board and flashing it with the Black Magic Probe (BMP) firmware. This turns the STM32 into a SWD debugger that can program the nRF52840. The BMP can then be selected as a debugger in VS Code. To support my board’s 2.2V logic, I added a level shifter to the STM32 board:
While this was a working solution for basic flashing, I found it to be unreliable and inconvenient. I later bought a Segger J-Link mini for around 60€. It’s by far the cheapest fully-fledged JTAG debugger, provided you only use it for non-commercial projects. And although the documentation doesn't state it, the J-Link works perfectly fine with a 2.2V target. I also bought a super-cheap pogo pin clamp that uses the test pads on the back of my PCB, so I don't have to solder on connector to every single board.
Now with a working way to flash the nRF52840, I began looking for a good starting point for my application. I had hoped that there were some official examples by Nordic, that I could base my code on. After all, a Zigbee temperature sensor is a very common type of device. But I had to realize that the latest version of the NRF Connect SDK (3.0.0) does not even include Zigbee support anymore. It requires a separate add-on which I could not get to work. Instead, I am using the slightly older version 2.6.0 with integrated Zigbee support. But even this version only comes with a small set of samples which demonstrate Zigbee communication between three Nordic evaluation boards. As a better starting point, I found Glen Akins Zigbee button project: https://bikerglen.com/blog/building-battery-powered-zigbee-buttons/
This project already implemented many components that I needed for my sensor, most importantly a Zigbee cluster definition.
Zigbee clusters are standardized sets of commands and attributes bundled together for specific device functions. This can be temperature, lighting, power consumption, etc. For my device, I had to combine the following clusters:
- Basic: General device info like firmware version, manufacturer, and power source.
- Identify: Lets the device identify itself, for example by flashing an LED.
- Temperature Measurement: For reading temperature values, including tolerances and min/max limits.
- Relative Humidity: Same as above but for humidity measurements
- On/off: Stores a binary state, e.g. for door sensors or switches.
- Power Configuration: Reports battery status.
These clusters are defined in the Zigbee Cluster Library which ensure that Zigbee devices can communicate across different manufacturers. And it gets a little more complicated: Zigbee uses a client-server model, so for each cluster, you have to know if your device stores the information or the coordinator. What’s the coordinator? There are three types of Zigbee devices:
- End devices: Typicall low-power nodes like sensors
- Router: Similar to End Devices but can forward messages within the network. These devise must remain powered.
- Coordinator: This is the central hub that manages the entire network.
There can be many routers and end devices in a network but only one coordinator. The coordinator can be a USB adapter running software like Zigbee2MQTT or Zigbee Home Automation (ZHA), which is part of Home Assistant. If all of your clusters are correctly defined and your device conforms to the standards, it will be able to join the network. During this joining procedure, the coordinator interviews the device to find out what clusters it is using and what attributes it can report. In a home automation software like Home Assistant the device will then be displayed correctly with its attributes and controls.
Debugging cluster definitions and figuring out the correct configuration was tedious. There are few examples on the internet and Nordic’s documentation for the ZBOSS stack is very limited. For example, I had the problem that the On/Off cluster for the door sensor is not directly recognized in Zigbee2MQTT. A better alternative would be the IAS Zone cluster, which is mostly meant for security systems. But this cluster need a specific IAS zone enrollment, separate from the regular joining procedure. After trying for a couple of hours I gave up on this, as there seem to be issues with Nordic’s implementation. The workaround is a custom converter, which tells Zigbee2MQTT that is should treat the on/off cluster as a contact sensor. Fortunately, ZHA supports the On/Off cluster natively without any modifications.
![]()
Another difficult topic is attribute reporting. My initial assumption was that my sensor would just send a temperature value to the coordinator at a fixed interval. But it’s not that simple, since the interval is negotiated with the coordinator. For example, if the device supports intervals from 1 min to 30 min, the coordinator might choose 5 min. The ZBOSS stack then sets a timer to wake the devices every 5 minutes to measure the temperature. In general, the ZBOSS stack manages all of the device’s timers, wakeups, and sleep states. You can configure interrupts for buttons and schedule periodic events within the stack, but ZBOSS manages when the device should wake up. To some degree, this simplifies programming but also makes debugging much trickier if some timer does not behave as expected. For example, when adding an LED indication for the contact sensor, the timer for turning the LED off was not properly scheduled, turning the device on every few minutes. This took me a lot of time to find and fix.
In comparison, the non-Zigbee software parts of the device, such as reading the HDC2080 were straightforward. Nordic supplies a vast number of libraries as part of Zephyr. And configuring pins can be done visually with a device tree configurator. This also simplifies working with different hardware revisions or evaluation boards.
Besides Zigbee-related issues, I had to apply some tweaks to improve the power consumption of the device. Initially, I configured the hall sensor pin as an edge interrupt, which should fire for both rising and falling edges (EDGE_BOTH). After some troubleshooting I found out that this kind of interrupts stops the controller from sleeping and draws a couple hundred microamps. The solution was switching to a level-triggered interrupt and inverting the trigger level after each activation.
To test my software, I have a few of the Zicada sensors running in my home automation network for the past few weeks. The software seems quite stable and I am very happy with the power consumption. During standby, the entire device runs on around 6uW. With temperature and humidity reporting happening every 5 minutes, the average power consumption is less than 17uW. In comparison, Ikea’s Parasoll sensor uses an average of around 22uW in standby alone.
Overall, the projects met my design goals: Inexpensive hardware, good smart home integration, and excellent power efficiency. Getting the software to a stable state was challenging, but I think that I have gotten the Zigbee stack under control for now. Time will tell how it performs long-term.
-
Electronics
11/16/2025 at 17:38 • 0 commentsThe main component of this project is the microcontroller which also has to serve as a Zigbee radio. Compared to Wi-Fi or Bluetooth, Zigbee support is less common in microcontrollers. Espressif offers the ESP32-C6, which seems like a good fit at first. However, due to its high power consumption in active mode and long boot-up time, it’s just not ideal for a battery-powered sensor. I like doing some rough calculations very early for battery powered projects like this. The math is straightforward: You always have a standby-mode current and an active-mode current. Then you factor in the time the device spends in each mode. Based on the battery capacity, you can then calculate an approximate battery life. This can be done quickly in Excel but Oregon Embedded’s online calculator is also very convenient:
https://oregonembedded.com/batterycalc.htm
Assuming 40mA during active mode and 3 seconds per measurement every 5 minutes, the ESP32-C6 would only run for around two months on a AAA cell. The standby current barely matters in this case as the device would waste the majority of its power during transmissions.
Besides Espressif, various other manufacturers like Nordic, TI and Silicon Labs offer low-power MCUs designed specifically for Zigbee applications. Based on good availability, price, and the fact that you can obtain an evaluation board easily, I chose the Nordic nRF52840. It’s based on a Cortex-M4, has plenty of I/O and it includes a number of low-power optimizations, like an on-chip DC/DC converter and a switch for supplying external circuitry. Nordic specifies a standby current of 1.5 µA and a peak transmit current of under 5 mA. Doing the same calculations as before, yields a theoretical battery life of over 4 years. Of course, real-world factors like battery self-discharge will shorten this considerably. But it does look promising!
One of my design goals was using a standard 1.6mm PCB. The nRF52840 uses quite a wild 7x7 mm QFN73 package with two rows of pins. Because of the density, not all of its pins are accessible without using plugged vias and a 4 layer PCB. This would not be an issue for this project since only few I/O pins are necessary - as long as none of the inaccessible pins need is essential. Unfortunately, one of these pins is the reset signal, which is quite important. I was almost considering switching to a different microcontroller before discovering Nordic’s nRF52840 Dongle, which also uses a low-cost 2-layer PCB. How did they solve this issue? I checked the design files and found that the reset-signal was simply routed through a few unused IO-pins. This seems a bit sloppy but if the manufacturer does it, so can I.
The Dongle design also integrates a compact 2.4 GHz trace antenna that happenes to match the width of my board, so I used their antenna layout as a starting point. In general, there’s nothing unprofessional about copying antenna designs from evaluation boards. Nordic even recommends this specifically, since these are proven and certified designs.
While most microcontrollers come with reference circuits from the manufacturer, Nordic provides a whole seven different circuit configurations for the nRF52840. These vary based on the power supply type and which features are enabled. Since I’m not using USB or NFC and have no use for the internal DC/DC converter, I chose configuration No. 6, which also happens to have the lowest part count. Nordic lists the 32.768 kHz clock crystal as optional, because the chip has an internal RTC crystal. But what is hidden a little deeper in the documentation is that using the external crystal reduces the power consumption by 1 µA vs using the internal one. I assume this is because the internal crystal needs more frequent calibration.
The nRF52 support a wide supply-voltage range, but with a minimum of 1.7 V, it can’t be powered directly from a 0.9-1.2 V AAA cell. That calls for a step-up (boost) converter. I previously used the MPC1640 in an earlier project to step up 3.3V to 5V for a low-power display, but its 19 µA quiescent current (current that the regulator itself consumes) would have meant too big of a hit to the battery life. Instead, I chose the TPS61098, which has less than 1 µA of quiescent current. On paper, it can start running at an input voltage of just 0.7 V, but the actual minimum depends on the output current. There are several variants of this device; I’m using the TPS61098, which supplies 2.2 V by default. The TPS610981 would be a drop-in replacement providing 3.3 V, which slightly increases the overall power consumption.
The TPS61098 is directly supplied by the AAA battery. Out of habit, I initially included a p-MOSFET between the TPS and the battery for reverse polarity protection for the first revision of the PCB. This is usually a simple way to block current if the battery is inserted the wrong way but it only works if the battery voltage is higher than the MOSFET’s gate-source threshold voltage. For a NiMH-based cell, the voltage is not enough and would require a MOSFET with an extremely low threshold voltage. Otherwise, the MOSFET’s resistance causes a voltage drop that prevents the boost converter from starting. The trace below shows the battery voltage (yellow), the voltage behind the input fuse (blue), the voltage behind the MOSFET (purple) and the 2.2V supply voltage (blue):
It’s a similar case when using a Schottky diode for polarity protection. Even its small voltage drop is too much if the whole 0.9-1.2V range of a rechargeable cell is to be used. Reverse polarity protection at such low voltages is a real challenge, so I ultimately went with a purely mechanical protection, where the 3D-printed case physically prevents the battery from being inserted the wrong way.
For temperature and humidity measurement, I’m using the TI HDC2080, because it is inexpensive, widely available and – unlike the HDC1080 – supports 2.2V operation. As with most of these sensors, the power draw is very low. The HDC2080 has a maximum sleep current of just 100nA so its negligible for the device’s battery life. The sensor communicates via i2c and includes a data-ready interrupt signal. Ttemperature sensors are usually placed at the edge of the PCB with some FR4 material around them milled away to reduce the thermal mass and make the sensor react faster to temperature changes. Because the PCB is what holds my battery contacts together, I did not want to impact the structural integrity too much, so I only have a c-shaped path milled around the sensor, which is centered on the board.
For the contact-sensor functionality, the device has to detect a nearby magnet. Some designs use classic reed switches, which consist of two wires in a tiny glass tube that make contact under a magnetic field. A more compact and durable type of magnetic sensor is a Hall-effect sensor, which works without any moving parts. I chose the Honeywell SM351LT, which draws only 360 nA, operates at 2.2 V and has a push-pull output. The push-pull part is significant because if you need a pull-up resistor for the switch and you leave your door open, that pull-up might consume more power than the whole rest of the electronics (for example 220 µA through a 10 kOhm resistor). I intentionally positioned the sensor right in the middle of the board, so the magnet can be detected regardless of the device’s orientation.
This is the first time I am building a device that has to run reliably on as little as 0.9V. As mentioned above, overlooking the threshold voltage of the polarity protection MOSFET was one of the mistakes that I made. But there are more lessons that I learned. For example, my first test for new boards is usually blinking an LED. Yet this tim, nothing happened despite the firmware running correctly. The issue turned out to be the LED itself. What I had not thought of at the time, was that different colored LEDs have different forward voltages. Blue especially is one of the worst colors to pick for a 2.2 V system because blue LEDs have typical forward voltages of 2.5-3.7 V. Suddenly, it made a lot more sense why Ikea would use a red LED for their AAA-powered Parasoll sensor (which internally also runs at around 2.2 V).
Another issue involved the input fuse. For a device like this, a fuse might not be as important as for a Lithium battery, but NiMH cells can overheat as well. Since the power consumption is so low, I initially used a resettable 100 mA polyfuse. The problem is that these fuses have a wide current range where their resistance will slowly increase. This caused the input voltage to drop below what the TPS61098 can handle whenever the nRF52 was transmitting. I solved this by switching to a 350 mA fuse which should still provide a good deal of protection.
But a low voltage also has its small perks. Since the microcontroller always runs at a higher voltage than its battery, measuring the battery voltage doesn’t require a voltage divider.
Overall, there aren’t many parts on the board and placing them on the PCB was not too difficult. I even had room for a 2x5-pin JTAG connector which came in very handy during the software development. The total part cost is also well below my original design goal of 10€. Here is a short rundown of what the parts currently cost, when ordering 10+ units:
- Nordic nRF52840: 2.73€
- TI TPS610981DSER boost converter: 0.79€
- Honeywell SM351LT hall effect switch: 0.28€
- TI HDC2080DMBR temperature and humidity sensor: 1.14€
- 2x MY-AAA-07 battery holder (Keystone 56 replacement): 0.14€
- Some neodymium magnets: around 0.20€
- Other small components: around 0.50€
- Circuit board: around 1€
- In total: 6,78€
Obviously, this does not account for the cost of shipping, taxes, tools like the soldering stencil and a programming tool.
-
Hardware
11/16/2025 at 17:36 • 0 comments![]()
In terms of design, smart home sensors are usually simple, unobtrusive devices and I wanted to keep it that way by using a white, 3D-printed case. Since the device can function as a contact sensor, it had to be small enough to be attached to a window or door frame. To figure out the necessary dimensions, I first searched for a suitable battery holder. Many common AAA holders consist of a plastic tray with two contacts, such as the Keystone 1020. However, since the case will be 3D-printed anyway, the whole plastic tray would take up unnecessary space. I ended up choosing the Keystone 56, which uses individual SMT-solderable spring contacts that add almost no length or height to the AAA cell. These sheet metal contacts are inexpensive and available from multiple manufacturers.
With two of these contacts mounted to one side of a PCB, the other side could be fully used for the electronic components. This means that the overall size of the device is defined by the AAA cell, the battery holders and a standard 1.6mm PCB. There was just one more important consideration: the antenna. It needs to stick out from under the battery to work efficiently. To keep the cost and size down, I opted for a quarter-wave trace antenna which adds around 6 mm to the PCB length. The negative antenna terminal sits right next to the antenna and becomes part of the RF ground.
The case mainly consists of two snap-fit shells that can be opened to replace the battery. Inside the front shell, there is another 3D-printed piece for holding the PCB in place and guiding the battery to its contacts. This inlay piece is also essential to ensure that the AAA cell can only be inserted the correct way. This is achieved by recessing the positive terminal slightly.
Zigbee Devices require some mechanism to start the pairing process, so there is a hole in the case for a pushbutton. This button is attached to the case via a small hinge. By changing the filament mid-print, the button gains a transparent window to the status led beneath it.
Another mini-version of the main device houses the magnets for contact sensing. I am using a set of six 2x5mm magnets, which seem to be strong enough to trigger the Hall effect switch.
Both the sensor and the magnet case can be mounted to a window or door frame using double-sided tape. When it’s time to replace the battery, the main part of the case can simply be pulled off.
-
Instroduction
11/16/2025 at 17:33 • 0 commentsWith open platforms like Home Assistant, smart homes have become quite popular over the past few years. I have accumulated some smart plugs, bulbs, and sensors myself, and I like Zigbee, because it supports low-power devices and decent ranges. One thing that always annoyed me was that most wireless sensors run on disposable coin cells. Even if they last for over a year, you always find yourself replacing batteries. Ikeas Parasoll contact sensor is one of the few devices powered by a (rechargeable) AAA battery but unfortunately it’s not free of design flaws. In general, there is a lack of open hardware or software among smart home sensors. I set out to change this by designing my own smart home device.
I wanted to build a Zigbee sensor primarily for temperature and humidity measurements. And since most of the hardware is identical, I would also throw in a contact sensor. After all, this just meant adding a cheap hall effect switch to the device, so it can detect when a magnet is in proximity. My initial design goals were as follows:
- The device should be powered by a single AAA cell like the Ikea Parasoll, so it needs to support voltages down to 0.9V.
- Despite the small battery, it should have at least a full year of battery life.
- It has to support smart home systems like Home Assistant and Zigbee2MQTT
- The parts should be easy to obtain and the overall cost per sensor should be below 10€.
Usually, I am cautious about investing too much time into a project before making certain that I can complete it. With a simple sensor like this, I was sure that the hardware side was manageable and the software could not be too complicated. After all, it just has to take measurements and send them to a Zigbee hub. That can’t be rocket science, can it? That was before I knew about things like Zigbee Clusters.
Max.K
