The main hardware difference is the ESP8266 is a 3.3V device but the Arduino is 5V. The PZEM-021 is actually a mixture. The RN8208G metering chip is a 5V device. It is a SPI slave, the SPI master is an STM32 ARM processor that is 3.3V but with 5V tolerant inputs. That means the signals originating from the CPU can go straight into the ESP8266 but the data out from the RN8208G would need attenuating.
Copying Thomas, I removed the mains dropper components C1 and R1 and powered the PZEM-021 from an external supply. This allows it to measure voltages right down to zero instead of cutting out at 80V. I used this little 12V 1A supply recommended by Big Clive on YouTube.
I powered the ESP-12F from the same supply with a tiny 3.3V MP2307 buck converter module.
WARNING: when connected to PZEM-021 the 0V rail is at mains neutral potential. This has to be treated with the same precautions as live because if the mains lead happened to be be reversed or the neutral connection broke it would become live. Don't connect a USB programmer or a scope ground to the circuit unless you power it with a mains isolation transformer. Fortunately the ESP8266 can be programmed wirelessly from the Arduino IDE once a sketch with ArduinoOTA has been installed and this is much faster than USB.
It is a DiBond box with 3D printed frame, handles and rubber feet containing an isolation transformer and a small variac. It also has two 100W light bulbs in parallel with a bypass switch for optional current limiting, a different model of power meter and some 4mm jack sockets allowing me to attach my Mooshimeter to log voltage and current.
The first task was to solder wires on to the PZEM-021 PCB to bring power in and data out to my circuit.
I didn't see a need for the chip select / word latch signal and I took the data from the other end of R9 compared to Thomas. The signals are as follows:
- Red is 12V, black is ground.
- The word latch going to the display from chip U2 pin 7 on the left, used for synchronisation.
- MISO data coming from the energy chip via R9.
- The SPI clock coming from U2 pin 12.
- Strip some wire-wrap wire and trim the end to be the length of the flat part of the pin.
- Tin it. It doesn't hold much solder due to being such a small radius.
- Add plenty of liquid flux around the pin.
- Line the wire up along the top of the pin with an Andonstar ADSM201 microscope camera.
- Put a very small amount of solder on a small chisel bit.
- Press the iron on top of the wire and leave it long enough to conduct the heat through the wire to boil the flux and melt the solder on the pad below.
When I looked at the levels of the signals I got a bit of a surprise. I was expecting MISO on channel 2 to go to 5V but the other two come from the ARM and should be 3.3V signals.
They seem to have a 20kHz ripple that takes them up to 4V. I looked back at Thomas' oscilloscope pictures and see the same ripple there. It doesn't make any difference for him because he is using a 5V chip but I didn't want to stuff 4V into my ESP8266!
On further investigation I found that the whole 3.3V rail had this ripple on it. It comes from a Holtek HT7133-1 3.3V LDO regulator. The datasheet for that suggests 10uF decoupling capacitors on the input and output. The circuit has what looks like a 10uF tant on its input but the output decoupler is a tiny MLCC that looks too small to be 10uF. I added a 10uF electrolytic in parallel and that killed the oscillation. It is the blue radial cap across C1 in the photo above.
That just left the issue of the 5V signal to contend with. R9 turns out to be 1K so simply adding a 2K2 to ground drops the signal low enough.
It also changes the first eight bits from FF to 00. This is actually when the command byte is being sent to the meter chip on MOSI and MISO is tri-state, so it doesn't matter.
Hardware done, on to the firmware. Before starting the project I had seen that the ESP8266 has a spare SPI port available and I assumed it would be straightforward to just read a stream of 13 bytes, wrong! It wasn't because the SPI port it a relatively complex device with dozens of registers that doesn't just send and receive bytes. It actually works at the command level, expecting to send or receive a command and address and then send or receive status or data bytes.
When in host mode it is possible to configure it to send and receive arbitrary bytes, indeed that is what the Arduino SPI class does and it works on the ESP8266 more or less the same as it does on an AVR. In slave mode though it has to receive a command and address according to the technical manual and the command defines what happens next. The relevant sections are:
4.3.2. Communication Format Supported by Slave SPISlave ESP8266SPI communication format is almost the same as that of the master mode, i.e. command+address+read/write data, but the slave read/write operation has itshardware command and undeletable address, which is,On the face of it this looks like it will not work in this application, which is unbelievable that a SPI port can't read arbitrary data as a slave and is hard coded for emulating flash chips and the like.
- Command: a must; length: 3 ~ 16 bits; master output and slave input (MOSI).
- Address: a must; length: 1 ~ 32 bits; master output and slave input (MOSI).
4.3.3.Command Definition Supported by Slave SPIThe length of slave receiving command should at least be 3 bits. For low 3 bits, there are hardware reading and writing operation, which is,
- Read/write data: optional; length: 0 ~ 512 bits (64 Bytes); master output and slave input (MOSI) or master input and slave output (MISO).
- 010 (slave receiving) : Write the data sent by master into the register of slave data caching via MOSI, i.e. SPI_FLASH_C0 to SPI_FLASH_C15.
- 011 (slave sending):Send the data in the register of slave data caching (from SPI_FLASH_C0 to SPI_FLASH_C15) to master via MOSI.
- 110 (slave receiving and sending): Send slave data caching to MISO and write the master data in MOSI into data caching SPI_FLASH_C0 to SPI_FLASH_C15.
I couldn't find any proper register level documentation for the ESP8266 other than various .h files on the web. The Arduino library uses shortened names which are very cryptic but some headers use longer names. While searching for these I came across the technical manual for the ESP32 and realised it has SPI ports that are nearly the same. It also has register level documentation in it's technical manual so I was able to get a slightly better understanding. After a lot of experimentation I found a solution.
The first n bits get interpreted as a command, where n is 3 or more and defaults to 8. The start of the 13 byte sequence that I want to receive is always zero now that I added the pull down resistor. So it always receives command zero. The bottom three bits normally control what happens next and zero defaults to read status. However, it is possible to override this with user definable commands by setting SPI_SLV_CMD_DEFINE in SPI_SLAVE_REG. That makes the SPI_SLAVE3_REG define what the command values are for four different actions. By setting SPI_SLV_WRBUF_CMD_VALUE to zero and the other three values to nonzero I was able to make it interpret the command as write buffer, so the rest of the data gets written into the buffer.
Well that was the theory but I was missing two bytes at the beginning. This was because it was being interpreted as an address. The default address length is 24 bits. I changed it to 8 by setting SPI_USER1_REG and now I get the 12 bytes of data following the zero byte. Now this doesn't make sense because if the first 8 bits are being interpreted as an address where is the command coming from? The command length is set by SPI_USER2_REG and it defaults to 8. I was expecting to have to set it to say four and set the address length to four bits so that they could share the first byte but any value between 2 and 8 seems to work and makes no difference. I don't know if it is a bug in the chip or I don't understand something but I get to read my 12 bytes of data
So in conclusion you can't read a completely arbitrary SPI data stream but you can if the first few bits are a known value.
Here is my code that sets up the HSPI:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
void hspi_slave_begin() { | |
pinMode(SCK, SPECIAL); // Both inputs in slave mode | |
pinMode(MOSI, SPECIAL); | |
SPI1C = 0; // SPI_CTRL_REG MSB first, single bit data mode. | |
SPI1S = SPISE | SPISBE | SPISCD | 0x3E0;// SPI_SLAVE_REG, set slave mode, WR/RD BUF enable, CMD define, enable interrupts | |
SPI1U = SPIUSSE; // SPI_USER_REG. SPI_CK_I_EDGE | |
SPI1CLK = 0; // SPI_CLOCK_REG | |
SPI1U1 = 7 << SPILADDR; // SPI_USER1_REG, set address length to 8 bits | |
SPI1U2 = 7 << SPILCOMMAND; // SPI_USER2_REG, set command length to 8 bits | |
SPI1S1 = (length * 8 - 1) << SPIS1LBUF; // SPI_SLAVE1_REG, SPI_SLV_BUF_BITLEN = 12 bytes | |
SPI1S3 = 0xF1F200F3; // SPI_SLAVE3_REG,, Define command 0 to be write buffer, others something doesn't match | |
SPI1P = 1 << 19; // SPI_PIN_REG, Clock idle high, seems to cause contension on the clock pin if set to idle low. | |
ETS_SPI_INTR_ATTACH(_hspi_slave_isr_handler, 0); | |
ETS_SPI_INTR_ENABLE(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const int length = 12; // length of packet minus the first byte | |
uint8_t data[length]; // raw data received | |
volatile bool data_ready = false; // set by HSPI interrupt handler when data is received | |
void ICACHE_RAM_ATTR hspi_slave_isr_handler(void *) { | |
uint32_t istatus = SPIIR; | |
if(istatus & (1 << SPII1)) { //SPI1 ISR | |
uint32_t status = SPI1S; | |
SPI1S &= ~(0x3E0); //disable interrupts | |
SPI1S |= SPISSRES; //reset | |
SPI1S &= ~(0x1F); //clear interrupts | |
SPI1S |= (0x3E0); //enable interrupts | |
if(status & SPISWBIS) { | |
uint8_t *p = data; | |
for(int i = 0; i < length / 4; i++) { | |
uint32_t dword = SPI1W(i); | |
*p++ = dword; | |
*p++ = dword >> 8; | |
*p++ = dword >> 16; | |
*p++ = dword >> 24; | |
} | |
data_ready = true; | |
} | |
} else if(istatus & (1 << SPII0)) { //SPI0 ISR | |
SPI0S &= ~(0x3ff);//clear SPI ISR | |
} else if(istatus & (1 << SPII2)) {} //I2S ISR | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
int voltint = 0, currentint = 0, wattint = 0; // raw values | |
void ICACHE_RAM_ATTR sync_isr() { // Sync pin interrupt on falling edge | |
SPI1S |= SPISSRES; // Reset HSPI slave | |
SPI1S &= ~SPISSRES; | |
SPI1CMD = SPICMDUSR; // Start HSPI slave | |
static bool glitch = false; | |
if(data_ready) { // If a data has been received by the HSPI | |
data_ready = false; | |
int volts = (data[0] << 16) + (data[1] << 8) + data[2]; // assemble 24 bits | |
if(volts > 5 * voltint / 8 || glitch) { // reject samples that have been shifted right by noise on the clock line? | |
glitch = false; // sample accepted | |
voltint = volts; | |
currentint = (data[4] << 16) + (data[5] << 8) + data[6]; | |
wattint = (data[8] << 16) + (data[9] << 8) + data[10]; | |
if(wattint > 0xFF0000) // sometimes goes slightly negative with no load | |
wattint = 0; | |
++samples; | |
new_readings = true; // new raw readings ready | |
} | |
else | |
glitch = true; // only reject one sample | |
} | |
} |
Another oddity is that when there is no load and the voltage is relatively high the power reading would go slightly negative. I detect that and set it to zero.
The foreground loop looks for the new_readings flag to be set and does the conversion to real units:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
float voltfloat = 0, currentfloat = 0, wattfloat = 0; // real unit values | |
float voltscalefactor = 0.0307162746 / 256; // calibration constants vary from device to device | |
float currentscalefactor = 1.553 / 256000; | |
float wattscalefactor = 15.49521794871 / 2560; | |
// the loop function runs over and over again forever | |
void loop() { | |
digitalWrite ( GREEN_LED, !!(millis() & 512) ); // flash the green LED | |
server.handleClient(); // run web server | |
if(new_readings) { // set by interrupt when data received | |
voltfloat = voltint * voltscalefactor; // convert to real units | |
currentfloat = currentint * currentscalefactor; | |
wattfloat = wattint * wattscalefactor; | |
Serial.printf("V%5.1f, A%4.2f, W%5.1f", voltfloat, currentfloat, wattfloat); | |
for(int i = 0; i < length; ++i) | |
Serial.printf(" %02X", data[i]); | |
Serial.println(); | |
new_readings = false; | |
} | |
ArduinoOTA.handle(); | |
} |
So it took me longer than I anticipated to follow in Thomas' footsteps because of the complexity and lack of proper documentation for the ESP8266 HSPI peripheral but I got there in the end. I hope others find this as useful as I found Thomas' work.
By now you are probably wondering what this is all for. Well, while investigating a strange line regulation problem in a power supply I got annoyed that the mains voltage in our house is constantly varying, making it hard to make measurements of input versus output. This is what it looks like logged every 10 seconds over a couple of days using my Mooshimeter.
As well as the small modern variac shown above I have a old WM5 model that I got on eBay. The only data I could find on it was from 1955. It still works fine so I decided to automate it with a small stepper motor to maintain a specified voltage. I.e. make a WiFi control IOT variac.
Another DiBond and printed part creation but the front panel had to be acrylic to let the WiFi out. Fortunately the front of my bench faces towards my router. The ESP8266 controls the motor via a Pololu stepper driver.
The control interface is a simple web form that also shows the current readings using AJAX as described here.
The control algorithm is very simple. The motor is stepped at the specified speed by the error in voltage multiplied by the gain. The readings update every 200ms but they lag a lot. That causes overshoot if the gain is set high. It would probably benefit from a full PID controller but it works well enough for now. The deadband setting allows it to stay in a range without constantly moving because I don't want to wear out the variac. Servo control of a variac is a standard way of regulating mains but I have no idea how long the brushes last. Manual mode disables the motor.
The readings can be read and the settings changed from the command line or from a Python script using cURL. Here is an example that sweeps the voltage from 10 to 250 in 10 Volt steps and reads the current and power.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import pycurl | |
from StringIO import StringIO | |
from time import * | |
import re | |
def curl(url): | |
buffer = StringIO() | |
c = pycurl.Curl() | |
c.setopt(c.URL, url) | |
c.setopt(c.WRITEDATA, buffer) | |
c.perform() | |
c.close() | |
return buffer.getvalue() | |
regex = re.compile('(.*)V *(.*)A *(.*)W Pos *(.*)') | |
for v in xrange(10, 251, 10): | |
curl('http://variac/readings?deadband=0&set=&voltage=' + str(v)) | |
while True: | |
sleep(0.2) | |
readings = curl('http://variac/readings') | |
results = regex.match(readings).groups() | |
V = float(results[0]) | |
A = float(results[1]) | |
W = float(results[2]) | |
P = int(results[3]) | |
if abs(v -V) < 0.1: | |
break | |
print v, V, A, W, P | |
curl('http://variac/readings?manual=') |
I plan to use this to plot a power supply line regulation graph. For that I will need a way of reading the output voltage in my script. I have two scopes that have network APIs as well as my Mooshimeter that has a BLE interface. I just need to work out which is the easiest to access in Python.
Here is the full ESP8266 sketch.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ESP8266 Arduino sketch to control a variac with a stepper motor | |
// GNU GPL | |
// NopHead | |
// hydraraptor.blogspot.com | |
// | |
#include <ESP8266WiFi.h> //ESP8266 Core WiFi Library (you most likely already have this in your sketch) | |
#include <ArduinoOTA.h> | |
#include <DNSServer.h> //Local DNS Server used for redirecting all requests to the configuration portal | |
#include <ESP8266WebServer.h> //Local WebServer used to serve the configuration portal | |
#include "WiFiManager.h" | |
#include <ESP8266mDNS.h> | |
#include "esp8266_peri.h" | |
#include "ets_sys.h" | |
const int RED_LED = 4; // GPIO pins | |
const int GREEN_LED = 5; // Bi colour LED | |
const int STEP = 0; // Stepper driver STEP pin | |
const int EN = 2; // Stepper driver enable pin | |
const int SYNC = 12; // Display chip CS used for synchonization | |
const int DIR = 15; // Stepper driver enable | |
ESP8266WebServer server ( 80 ); | |
int target_v = 230; // target voltage from web form | |
int speed = 50; // speed in degrees per second | |
int gain = 20; // feadback gain, microsteps per volt | |
float deadband = 0.5; // deaband to avoid continual movement and wear | |
bool manual = true; // when true the motor is disabled | |
const int microsteps = 16; // configuration of the stepper driver | |
const int big_teeth = 78; // teeth on the big pulley | |
const int small_teeth = 16; // teeth on the motor pulley | |
const float steps_per_rev = 200.0 * microsteps * big_teeth / small_teeth; | |
int position = 0; // current motor postion relative to where it started | |
const int length = 12; // length of packet minus the first byte | |
uint8_t data[length]; // raw data received | |
volatile bool data_ready = false; // set by HSPI interrupt handler when data is received | |
volatile bool new_readings = false; // set by sync pin interrupt when readings are extracted from raw data | |
volatile uint32_t samples = 0; // count of samples received | |
int voltint = 0, currentint = 0, wattint = 0; // raw values | |
float voltfloat = 0, currentfloat = 0, wattfloat = 0; // real unit values | |
float voltscalefactor = 0.0307162746 / 256; // calibration constants vary from device to device | |
float currentscalefactor = 1.553 / 256000; | |
float wattscalefactor = 15.49521794871 / 2560; | |
long speed2us(int speed) { // convert speed in degrees per second to setp delay in microseconds | |
float steps_per_second = steps_per_rev * speed / 360.0; | |
return 1000000 / steps_per_second; | |
} | |
void spin(int speed, int steps) { // step the motor | |
digitalWrite(GREEN_LED, 1); | |
digitalWrite(DIR, steps < 0); | |
digitalWrite(EN, 0); | |
delay(1); | |
long uS = speed2us(speed); | |
position += steps; | |
steps = abs(steps); | |
while(steps--) { | |
digitalWrite(STEP, 1); | |
ESP.wdtFeed(); | |
digitalWrite(STEP, 0); | |
delayMicroseconds(uS); | |
} | |
} | |
const char MAIN_page[] PROGMEM = R"=====( | |
<html> | |
<head> | |
<meta name = "viewport" content = "width = device-width, initial-scale = 1.5, maximum-scale = 2.0, user-scalable=1", charset="UTF-8"/> | |
<title>Variac control</title> | |
<style> | |
body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; } | |
</style> | |
</head> | |
<body> | |
<h1>Variac Control</h1> | |
<form action="/" method="post"> | |
<table> | |
<tr><td>Voltage:</td> <td><input type="text" name="voltage" value="%d" size="3" maxlength="3"></td></tr> | |
<tr><td>Speed:</td> <td><input type="text" name="speed" value="%d" size="3" maxlength="3"></td></tr> | |
<tr><td>Gain:</td> <td><input type="text" name="gain" value="%d" size="3" maxlength="3"></td></tr> | |
<tr><td>Deadband:</td><td><input type="text" name="deadband" value="%3.1f" size="3" maxlength="3"></td></tr> | |
<tr><td><button type="submit" name="manual">%s</button></td><td><button type="submit" name="set">%s</button></td></tr> | |
</table> | |
<br/><div>Readings: <pre style="display:inline"><span id = "readings">---</span></pre></div> | |
</form> | |
<script> | |
setInterval(function() { | |
// Call a function repetatively with 2 Second interval | |
getData(); | |
}, 200); //200ms update rate | |
function getData() { | |
var xhttp = new XMLHttpRequest(); | |
xhttp.onreadystatechange = function() { | |
if (this.readyState == 4 && this.status == 200) { | |
document.getElementById("readings").innerHTML = | |
this.responseText; | |
} | |
}; | |
xhttp.open("GET", "readings", true); | |
xhttp.send(); | |
} | |
</script> | |
</body> | |
</html> | |
)====="; | |
void mainPage() { | |
char temp[100 + sizeof MAIN_page]; | |
snprintf_P ( temp, sizeof temp, MAIN_page, target_v, speed, gain, deadband, | |
manual ? "<strong>MANUAL</strong>" : "MANUAL", | |
manual ? "SET" : "<strong>SET</strong>" ); | |
server.send ( 200, "text/html", temp ); | |
} | |
int clip(int x, int low, int high) { | |
if(x < low) | |
return low; | |
if(x > high) | |
return high; | |
return x; | |
} | |
void handleSettings() { | |
if(server.hasArg("speed")) | |
speed = clip(server.arg("speed").toInt(), 1, 100); | |
if(server.hasArg("voltage")) | |
target_v =clip(server.arg("voltage").toInt(), 1, 260); | |
if(server.hasArg("gain")) | |
gain = clip(server.arg("gain").toInt(), 0, 100); | |
if(server.hasArg("deadband")) | |
deadband = server.arg("deadband").toFloat(); | |
if(server.hasArg("manual")) { | |
if(manual) | |
position = 0; | |
manual = true; | |
digitalWrite(EN, 1); | |
} | |
if(server.hasArg("set")) | |
manual = false; | |
} | |
void handleRoot() { | |
handleSettings(); | |
mainPage(); | |
} | |
void handleReadings() { | |
handleSettings(); | |
char temp[100]; | |
snprintf(temp, sizeof temp, "%5.1fV %4.3fA %5.1fW Pos %5d", voltfloat, currentfloat, wattfloat, position); | |
//snprintf(temp, sizeof temp, "%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", data[0], data[1],data[2], data[3], data[4], data[5], data[6], data[7],data[8], data[9], data[10], data[11]); | |
server.send(200, "text/plane", temp); //Send readings to client ajax request | |
} | |
void handleNotFound() { | |
String message = "File Not Found\n\n"; | |
message += "URI: "; | |
message += server.uri(); | |
message += "\nMethod: "; | |
message += ( server.method() == HTTP_GET ) ? "GET" : "POST"; | |
message += "\nArguments: "; | |
message += server.args(); | |
message += "\n"; | |
for ( uint8_t i = 0; i < server.args(); i++ ) { | |
message += " " + server.argName ( i ) + ": " + server.arg ( i ) + "\n"; | |
} | |
server.send ( 404, "text/plain", message ); | |
Serial.println(message); | |
} | |
void ICACHE_RAM_ATTR sync_isr() { // Sync pin interrupt on falling edge | |
SPI1S |= SPISSRES; // Reset HSPI slave | |
SPI1S &= ~SPISSRES; | |
SPI1CMD = SPICMDUSR; // Start HSPI slave | |
static bool glitch = false; | |
if(data_ready) { // If a data has been received by the HSPI | |
data_ready = false; | |
int volts = (data[0] << 16) + (data[1] << 8) + data[2]; // assemble 24 bits | |
if(volts > 5 * voltint / 8 || glitch) { // reject samples that have been shifted right by noise on the clock line? | |
glitch = false; // sample accepted | |
voltint = volts; | |
currentint = (data[4] << 16) + (data[5] << 8) + data[6]; | |
wattint = (data[8] << 16) + (data[9] << 8) + data[10]; | |
if(wattint > 0xFF0000) // sometimes goes slightly negative with no load | |
wattint = 0; | |
++samples; | |
new_readings = true; // new raw readings ready | |
} | |
else | |
glitch = true; // only reject one sample | |
} | |
} | |
void ICACHE_RAM_ATTR _hspi_slave_isr_handler(void *) { | |
uint32_t istatus = SPIIR; | |
if(istatus & (1 << SPII1)) { //SPI1 ISR | |
uint32_t status = SPI1S; | |
SPI1S &= ~(0x3E0); //disable interrupts | |
SPI1S |= SPISSRES; //reset | |
SPI1S &= ~(0x1F); //clear interrupts | |
SPI1S |= (0x3E0); //enable interrupts | |
if(status & SPISWBIS) { | |
uint8_t *p = data; | |
for(int i = 0; i < length / 4; i++) { | |
uint32_t dword = SPI1W(i); | |
*p++ = dword; | |
*p++ = dword >> 8; | |
*p++ = dword >> 16; | |
*p++ = dword >> 24; | |
} | |
data_ready = true; | |
} | |
} else if(istatus & (1 << SPII0)) { //SPI0 ISR | |
SPI0S &= ~(0x3ff);//clear SPI ISR | |
} else if(istatus & (1 << SPII2)) {} //I2S ISR | |
} | |
void hspi_slave_begin() { | |
pinMode(SCK, SPECIAL); // Both inputs in slave mode | |
pinMode(MOSI, SPECIAL); | |
SPI1C = 0; // SPI_CTRL_REG MSB first, single bit data mode. | |
SPI1S = SPISE | SPISBE | SPISCD | 0x3E0;// SPI_SLAVE_REG, set slave mode, WR/RD BUF enable, CMD define, enable interrupts | |
SPI1U = SPIUSSE; // SPI_USER_REG. SPI_CK_I_EDGE | |
SPI1CLK = 0; // SPI_CLOCK_REG | |
SPI1U1 = 7 << SPILADDR; // SPI_USER1_REG, set address length to 8 bits | |
SPI1U2 = 7 << SPILCOMMAND; // SPI_USER2_REG, set command length to 8 bits | |
SPI1S1 = (length * 8 - 1) << SPIS1LBUF; // SPI_SLAVE1_REG, SPI_SLV_BUF_BITLEN = 12 bytes | |
SPI1S3 = 0xF1F200F3; // SPI_SLAVE3_REG,, Define command 0 to be write buffer, others something doesn't match | |
SPI1P = 1 << 19; // SPI_PIN_REG, Clock idle high, seems to cause contension on the clock pin if set to idle low. | |
ETS_SPI_INTR_ATTACH(_hspi_slave_isr_handler, 0); | |
ETS_SPI_INTR_ENABLE(); | |
} | |
void hspi_slave_end() { | |
SPI1S |= SPISSRES; | |
SPI1CMD = 0; | |
pinMode(SCK, INPUT); // Return to inputs to avoid glicthing the PZEM-021 during reset | |
pinMode(MOSI, INPUT); | |
digitalWrite (GREEN_LED, 1); | |
digitalWrite (RED_LED, 1); | |
} | |
void setup() { | |
Serial.begin(115200); | |
Serial.println(); | |
Serial.println("Variac starting"); | |
pinMode(GREEN_LED, OUTPUT); | |
pinMode(RED_LED, OUTPUT); | |
pinMode(EN, OUTPUT); | |
pinMode(DIR, OUTPUT); | |
pinMode(STEP, OUTPUT); | |
digitalWrite(RED_LED, 0); | |
digitalWrite(GREEN_LED, 0); | |
digitalWrite(STEP, 0); | |
digitalWrite(DIR,0); | |
digitalWrite(EN,1); | |
//WiFi.disconnect(); | |
const char *hostname = "variac"; | |
digitalWrite ( RED_LED, 1 ); | |
WiFi.hostname(hostname); | |
WiFiManager wifiManager; | |
wifiManager.autoConnect(hostname, NULL); // Ap name same as host name | |
digitalWrite( RED_LED, 0 ); | |
digitalWrite( GREEN_LED, 1); | |
if ( MDNS.begin(hostname) ) | |
Serial.println ( "MDNS responder started" ); | |
server.on( "/", handleRoot ); | |
server.on("/readings", handleReadings ); | |
server.onNotFound( handleNotFound ); | |
server.begin(); | |
Serial.println ( "HTTP server started" ); | |
attachInterrupt(digitalPinToInterrupt(SYNC), sync_isr, FALLING); | |
hspi_slave_begin(); | |
// Port defaults to 8266 | |
// ArduinoOTA.setPort(8266); | |
// Hostname defaults to esp8266-[ChipID] | |
// ArduinoOTA.setHostname("myesp8266"); | |
// No authentication by default | |
// ArduinoOTA.setPassword("admin"); | |
// Password can be set with it's md5 value as well | |
// MD5(admin) = 21232f297a57a5a743894a0e4a801fc3 | |
// ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3"); | |
ArduinoOTA.onStart([]() { | |
String type; | |
if (ArduinoOTA.getCommand() == U_FLASH) | |
type = "sketch"; | |
else // U_SPIFFS | |
type = "filesystem"; | |
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() | |
Serial.println("Start updating " + type); | |
// Stop the interrupts | |
detachInterrupt(digitalPinToInterrupt(SYNC)); | |
hspi_slave_end(); | |
}); | |
ArduinoOTA.onEnd([]() { | |
Serial.println("\nEnd"); | |
}); | |
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { | |
Serial.printf("Progress: %u%%\r", 100 * progress / total); | |
static bool toggle; // toggle both leds so they flicker orange during update | |
toggle = !toggle; | |
digitalWrite (GREEN_LED, toggle); | |
digitalWrite (RED_LED, toggle); | |
}); | |
ArduinoOTA.onError([](ota_error_t error) { | |
Serial.printf("Error[%u]: ", error); | |
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); | |
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); | |
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); | |
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); | |
else if (error == OTA_END_ERROR) Serial.println("End Failed"); | |
}); | |
ArduinoOTA.begin(); | |
Serial.println("Ready"); | |
Serial.print("IP address: "); | |
Serial.println(WiFi.localIP()); | |
} | |
// the loop function runs over and over again forever | |
void loop() { | |
digitalWrite ( GREEN_LED, !!(millis() & 512) ); // flash the green LED | |
server.handleClient(); // run web server | |
if(new_readings) { // set by interrupt when data received | |
voltfloat = voltint * voltscalefactor; // convert to real units | |
currentfloat = currentint * currentscalefactor; | |
wattfloat = wattint * wattscalefactor; | |
Serial.printf("V%5.1f, A%4.2f, W%5.1f, Pos%5d", voltfloat, currentfloat, wattfloat, position); | |
for(int i = 0; i < length; ++i) | |
Serial.printf(" %02X", data[i]); | |
Serial.println(); | |
new_readings = false; | |
} | |
if(samples > 1) { // update motor when had more than two samples since last moved | |
float error = target_v - voltfloat; | |
if(fabs(error) > deadband / 2 && !manual) { | |
spin(speed, error * gain); | |
samples = 0; | |
} | |
} | |
ArduinoOTA.handle(); | |
} |