Skip to content

Commit

Permalink
Changed the non-interrupt mode usage
Browse files Browse the repository at this point in the history
  • Loading branch information
Xose Pérez committed Nov 8, 2016
1 parent 2767eb1 commit 1dfa68b
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 58 deletions.
16 changes: 10 additions & 6 deletions README.md
Expand Up @@ -27,19 +27,23 @@ The main features of the HLW8012 library are:
* You can specify the resistor values for your circuit.
* Optional manual calibration based on expected values.

## Notes
## Usage

Check the examples for indications on how to use the library.

### Interrupt driven mode

When using interrupts, values are monitored in the background. When calling the get***() methods the last sampled value is returned, this value might be up to a few seconds old if they are very low values. This is specially obvious when switching off the load. The new value of 0W or 0mA is ideally represented by infinite-length pulses. That means that the interrupt is not triggered, the value does not get updated and it will only timeout after 2 seconds (configurable through the pulse_timeout parameter in the begin() method). During that time lapse the library will still return the last non-zero value.

On the other hand, when not using interrupts, you have to let some time for the pulses in CF1 to stabilize before reading the value. This is the "interval" parameter in the handle() method that defaults to 3 seconds. The longer this value the more stable the readings (in particular for low values), but also decreases the frequency sampling. Also readings might take up to 2s (same pulse_timeout value before) and impact other processes (like WIFI in ESP8266).
### Non interrupt mode

**Interrupts** is the recommended way to go. Use non-interrupt approach and a low pulse_timeout (200ms) only if you are deploying a battery powered device and you care more about your device power consumption than about precission. But then you should know the HLW8012 takes about 15mW...
On the other hand, when not using interrupts, you have to let some time for the pulses in CF1 to stabilize before reading the value. So after calling setMode or toggleMode leave 2 seconds minimum before calling the get methods. The get method for the current mode will measure the pulse width and return the corresponding value, the other one will return the cached value (or 0 if none).

I've put together this library after doing a lot of tests with a Sonoff POW[2]. The HLW8012 datasheet (in the "docs" folder) gives some information but I couldn't find any about this issue in the CF1 line that requires some time for the pulse length to stabilize (apparently). Any help about that will be very welcome.
Use non-interrupt approach and a low pulse_timeout (200ms) only if you are deploying a battery powered device and you care more about your device power consumption than about precission. But then you should know the HLW8012 takes about 15mW...

## Usage
### Notes

Check the examples for indications on how to use the library.
I've put together this library after doing a lot of tests with a Sonoff POW[2]. The HLW8012 datasheet (in the "docs" folder) gives some information but I couldn't find any about this issue in the CF1 line that requires some time for the pulse length to stabilize (apparently). Any help about that will be very welcome.

## Manual calibration

Expand Down
48 changes: 29 additions & 19 deletions examples/HLW8012_Basic/HLW8012_Basic.ino
Expand Up @@ -9,8 +9,8 @@
#define CF1_PIN 13
#define CF_PIN 14

// Check values every 10 seconds
#define UPDATE_TIME 10000
// Check values every 2 seconds
#define UPDATE_TIME 2000

// Set SEL_PIN to HIGH to sample current
// This is the case for Itead's Sonoff POW, where a
Expand All @@ -25,14 +25,25 @@

HLW8012 hlw8012;

void unblockingDelay(unsigned long mseconds) {
unsigned long timeout = millis();
while ((millis() - timeout) < mseconds) delay(1);
}

void calibrate() {

// Let some time to register values
unsigned long timeout = millis();
while ((millis() - timeout) < 10000) {
hlw8012.handle();
delay(1);
}
// Let's first read power, current and voltage
// with an interval in between to allow the signal to stabilise:

hlw8012.getActivePower();

hlw8012.setMode(MODE_CURRENT);
unblockingDelay(2000);
hlw8012.getCurrent();

hlw8012.setMode(MODE_VOLTAGE);
unblockingDelay(2000);
hlw8012.getVoltage();

// Calibrate using a 60W bulb (pure resistive) on a 230V line
hlw8012.expectedActivePower(60.0);
Expand Down Expand Up @@ -63,8 +74,8 @@ void setup() {
// * cf_pin, cf1_pin and sel_pin are GPIOs to the HLW8012 IC
// * currentWhen is the value in sel_pin to select current sampling
// * set use_interrupts to false, we will have to call handle() in the main loop to do the sampling
// * set pulse_timeout to 200ms for a fast response but losing precision (that's ~60W precision :( )
hlw8012.begin(CF_PIN, CF1_PIN, SEL_PIN, CURRENT_MODE, false, 200000);
// * set pulse_timeout to 500ms for a fast response but losing precision (that's ~24W precision :( )
hlw8012.begin(CF_PIN, CF1_PIN, SEL_PIN, CURRENT_MODE, false, 500000);

// These values are used to calculate current, voltage and power factors as per datasheet formula
// These are the nominal values for the Sonoff POW resistors:
Expand All @@ -87,24 +98,23 @@ void loop() {

static unsigned long last = millis();

// When not using interrupts you have to call handle() every so often with an optional
// interval value. This interval defaults to 3000ms but should not be less than 500ms. After this
// time the code will switch from voltage to current monitor and viceversa. So you will have
// new values for both after 2x this interval time.
hlw8012.handle(500);

// This UPDATE_TIME should be at least twice the previous time in handle() when not using interrupts
// or 1000ms when using interrupts
// This UPDATE_TIME should be at least twice the minimum time for the current or voltage
// signals to stabilize. Experimentally that's about 1 second.
if ((millis() - last) > UPDATE_TIME) {

last = millis();
Serial.print("[HLW] Active Power (W) : "); Serial.println(hlw8012.getActivePower());
Serial.print("[HLW] Voltage (V) : "); Serial.println(hlw8012.getVoltage());
Serial.print("[HLW] Current (A) : "); Serial.println(hlw8012.getCurrent());
Serial.print("[HLW] Active Power (W) : "); Serial.println(hlw8012.getActivePower());
Serial.print("[HLW] Apparent Power (VA) : "); Serial.println(hlw8012.getApparentPower());
Serial.print("[HLW] Power Factor (%) : "); Serial.println((int) (100 * hlw8012.getPowerFactor()));
Serial.println();

// When not using interrupts we have to manually switch to current or voltage monitor
// This means that every time we get into the conditional we only update one of them
// while the other will return the cached value.
hlw8012.toggleMode();

}

}
46 changes: 46 additions & 0 deletions examples/HLW8012_Basic_Sleep/HLW8012_Basic_Sleep.ino
@@ -0,0 +1,46 @@
#include <Arduino.h>
#include <LowPower.h>
#include "HLW8012.h"

#define SERIAL_BAUDRATE 115200

// GPIOs
#define SEL_PIN 5
#define CF1_PIN 13
#define CF_PIN 14

// Set SEL_PIN to HIGH to sample current
// This is the case for Itead's Sonoff POW, where a
// the SEL_PIN drives a transistor that pulls down
// the SEL pin in the HLW8012 when closed
#define CURRENT_MODE HIGH

// Maximum pulse width, a 2Hz pulse means a precission of ~24W
#define MAX_PULSE_WIDTH 500000

HLW8012 hlw8012;

void setup() {

// Init serial port and clean garbage
Serial.begin(SERIAL_BAUDRATE);
Serial.println();
Serial.println();

// Initialize HLW8012
hlw8012.begin(CF_PIN, CF1_PIN, SEL_PIN, CURRENT_MODE, false, MAX_PULSE_WIDTH);

}

void loop() {

// Sleep for 20 seconds
for (unsigned short i = 0; i < 5; i++ ) {
LowPower.powerDown(SLEEP_4S, ADC_OFF, BOD_OFF);
}

unsigned long start = millis();
Serial.print("[HLW] Active Power (W) : "); Serial.println(hlw8012.getActivePower());
Serial.print("[HLW] Awake for (ms) : "); Serial.println(millis() - start);

}
2 changes: 1 addition & 1 deletion examples/HLW8012_Interrupts/HLW8012_Interrupts.ino
Expand Up @@ -106,10 +106,10 @@ void loop() {
if ((millis() - last) > UPDATE_TIME) {

last = millis();
Serial.print("[HLW] Active Power (W) : "); Serial.println(hlw8012.getActivePower());
Serial.print("[HLW] Voltage (V) : "); Serial.println(hlw8012.getVoltage());
Serial.print("[HLW] Current (A) : "); Serial.println(hlw8012.getCurrent());
Serial.print("[HLW] Apparent Power (VA) : "); Serial.println(hlw8012.getApparentPower());
Serial.print("[HLW] Active Power (W) : "); Serial.println(hlw8012.getActivePower());
Serial.print("[HLW] Power Factor (%) : "); Serial.println((int) (100 * hlw8012.getPowerFactor()));
Serial.println();

Expand Down
61 changes: 32 additions & 29 deletions src/HLW8012.cpp
Expand Up @@ -50,47 +50,49 @@ void HLW8012::begin(

}

void HLW8012::handle(unsigned long interval) {

double T;
static unsigned long last_reading = millis();

// Safety check
if (_use_interrupts) return;

if ((millis() - last_reading) > interval) {

last_reading = millis();

T = 2 * pulseIn(_cf1_pin, HIGH, _pulse_timeout);
if (_mode == _current_mode) {
_current_pulse_width = T;
} else {
_voltage_pulse_width = T;
}
_mode = 1 - _mode;
digitalWrite(_sel_pin, _mode);

void HLW8012::setMode(hlw8012_mode_t mode) {
_mode = (mode == MODE_CURRENT) ? _current_mode : 1 - _current_mode;
digitalWrite(_sel_pin, _mode);
if (_use_interrupts) {
_last_cf1_interrupt = _first_cf1_interrupt = micros();
}
}

hlw8012_mode_t HLW8012::getMode() {
return (_mode == _current_mode) ? MODE_CURRENT : MODE_VOLTAGE;
}

double HLW8012::getCurrent() {
hlw8012_mode_t HLW8012::toggleMode() {
hlw8012_mode_t new_mode = getMode() == MODE_CURRENT ? MODE_VOLTAGE : MODE_CURRENT;
setMode(new_mode);
return new_mode;
}

// Update active power
if (_use_interrupts) getActivePower();
double HLW8012::getCurrent() {

// Power measurements are more sensitive to switch offs,
// so we first check if power is 0 to set _current to 0 too
if (_power == 0) {
_current = 0;
} else {
if (_use_interrupts) _checkCF1Signal();
_current = (_current_pulse_width > 0) ? _current_multiplier / _current_pulse_width / 2 : 0;
_current_pulse_width = 0;

} else if (_use_interrupts) {
_checkCF1Signal();

} else if (_mode == _current_mode) {
_current_pulse_width = pulseIn(_cf1_pin, HIGH, _pulse_timeout);
}

_current = (_current_pulse_width > 0) ? _current_multiplier / _current_pulse_width / 2 : 0;
return _current;

}

unsigned int HLW8012::getVoltage() {
if (_use_interrupts) _checkCF1Signal();
if (_use_interrupts) {
_checkCF1Signal();
} else if (_mode != _current_mode) {
_voltage_pulse_width = pulseIn(_cf1_pin, HIGH, _pulse_timeout);
}
_voltage = (_voltage_pulse_width > 0) ? _voltage_multiplier / _voltage_pulse_width / 2 : 0;
return _voltage;
}
Expand Down Expand Up @@ -191,6 +193,7 @@ void HLW8012::_checkCF1Signal() {
} else {
_voltage_pulse_width = 0;
}
toggleMode();
}
}

Expand Down
15 changes: 12 additions & 3 deletions src/HLW8012.h
Expand Up @@ -52,6 +52,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// will have no time to stabilise
#define PULSE_TIMEOUT 2000000

// CF1 mode
typedef enum {
MODE_CURRENT,
MODE_VOLTAGE
} hlw8012_mode_t;

class HLW8012 {

public:
Expand All @@ -64,9 +70,12 @@ class HLW8012 {
unsigned char cf1_pin,
unsigned char sel_pin,
unsigned char currentWhen = HIGH,
bool use_interrupts = false,
bool use_interrupts = true,
unsigned long pulse_timeout = PULSE_TIMEOUT);
void handle(unsigned long interval = READING_INTERVAL);

void setMode(hlw8012_mode_t mode);
hlw8012_mode_t getMode();
hlw8012_mode_t toggleMode();

double getCurrent();
unsigned int getVoltage();
Expand Down Expand Up @@ -113,7 +122,7 @@ class HLW8012 {
unsigned char _current_mode = HIGH;
unsigned char _mode;

bool _use_interrupts;
bool _use_interrupts = true;
volatile unsigned long _last_cf_interrupt = 0;
volatile unsigned long _last_cf1_interrupt = 0;
volatile unsigned long _first_cf1_interrupt = 0;
Expand Down

0 comments on commit 1dfa68b

Please sign in to comment.