Latest on ESP32

The ESP8266 consumes all of my time. I expect to release what I have for the ESP32 this fall/winter for those that want to tinker. Getting it to the stability and compliance of the current unit is a huge expensive effort and I don’t see myself able to do that and support 10,000 current users at the same time.

Understandable. Reason I was asking was I’m preparing to move and the buyer of my place requested the IotaWatt’s to go with it. Figured why not since I can just replace them and wanted to see if possible the ESP32 would be coming out as a nice upgrade, but the older ESP8266 ones are also acceptable.

What would be the most time consuming? Is it more hardware work or more software one to be performed yet?

Overeasy, any plans to publish your esp32 firmware any time soon? I am really curious on your new sampling routine on the ESP32. SPI and/or bare metal work differently on the ESP32. I am starting a new project with a MCP3208 and using the ESP_IDF or android libraries I am getting sampling rates far below yours.

Are you talking about sampling AC power or just driving the MCP3208?

Mostly driving the MCP3208. Your approach, it seems to me, is to partly use esp8266 SPI and for the most part bitbang and write/read SPI registers.
Obviously this is different on a ESP32

You use standard SPI here:

SPI.beginTransaction(SPISettings(2000000,MSBFIRST,SPI_MODE0));
 
  rawV = readADC(Vchan) - offsetV;                    // Prime the pump

You loop through samples with reading registers

// hardware send 5 bit start + sgl/diff + port_addr
                                            
        SPI1U1 = (SPI1U1 & mask) | dataMask;               // Set number of bits 
        SPI1W0 = (0x18 | Iport) << 3;                      // Data left aligned in low byte 
        SPI1CMD |= SPIBUSY;                                // Start the SPI clock  

and:

// extract the rawV from the SPI hardware buffer and adjust with offset. 
                                                                    
        rawI = (word(*fifoPtr8 & 0x01, *(fifoPtr8+1)) << 3) + (*(fifoPtr8+2) >> 5) - offsetI;

All very nicely programmed. ESP32 documentation is very vague at times. In some way it claims that the ESP32 has so much more power and memory you don’t need to employ methods like yours. But with using ESP32 SPI Master driver I get much lower rates than your ESP8266 method - on a ESP8266 of course.

Just running the SPI/ADC is not that complicated. It’s simpler than the ESP8266.

I run the SPI at 2MHz:

vspi->beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0));

It’s been a couple of years since I coded this up, so my memory is a little off. I use the standard digitalWrite to lower and raise the CS pin.

For the actual SPI transaction I use a call to a modified lower level spiTransferBits that I renamed spiTransferBitsCB. There are two modifications:

The MCP3208 transaction is broken down into two SPI transactions. The first is the 5 bit write portion with start bit and channel select. The second is the 12 bit result. The reason for this is that when you run the MCP3208 ate 3.3V and 2MHz, the sample and hold charge time can be too short to fully charge the ADC sample capacitor. The cap charge time is increased by the inter-transaction delay time.

The second modification is to include the ability to specify a callback function to be invoked when the second SPI transaction initiates. This is used to do housekeeping and other sampling chores during the 6us data transfer from the ADC. It’s not necessary to use this callback to get the high transaction rate. I implement it as a lambda expression in the code as in:


                // Sample Current (I) channel

            digitalWrite(context.Ics, LOW);
           spiTransferBitsCB(spi, context.Iport, &spiOut, [ ](void* ptr){

                    // This code is a callback from the low level SPI
                    // while the SPI is reading the ADC.
                    // Here we do whatever housekeeping possible asynchronously.
                    // It should complete before the SPI transfer finishes. 
                   
                    .
                    .  // Some housekeeping code
                    .
     

                return;
            }, (void*)&context);

            digitalWrite(context.Ics, HIGH);            
            context.newI = (spiOut & 4095) - job->offsetI;

The modified spiTransferBits code follows:

//       spiTransferBitsCB()
//
//      Reworked spiTransferBit from ESP32/Arduino core.
//
//      Special purpose to perform MCP3208 transaction at high speed with 2MHz spi.
//
//      When running at 2MHz 750ns ADC sample time is inadequate and produces lower quality results of +/- 3 LSB.
//      By splitting into two transactions, the sample time defined by the rise of the 5th clock and fall of the 6th clock
//      is greatly increased to about 2ms, producing better results.
//
//      This code also allows for a callback during the second 14 bit transfer (7ms), which the sampler
//      uses to record results, do housekeeping and loop control.
//
//      The result is an effective ADC sample rate of about 80K SPS.
//
//      Returns boolean noWait to indicate if no wait for spi completion was required after return from the
//      callback, which means the callback did not complete in the alloted time.  This is used from time-to-time
//      as an aid to tuning the loop to balance the two spi calls and minimize sampling induced phase-shift.
//

#include "esp_attr.h"

#include "esp32-hal-spi.h"
#include "esp32-hal.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "rom/ets_sys.h"
#include "esp_attr.h"
#include "esp_intr.h"
#include "rom/gpio.h"
#include "soc/spi_reg.h"
#include "soc/spi_struct.h"
#include "soc/io_mux_reg.h"
#include "soc/gpio_sig_map.h"
#include "soc/dport_reg.h"
#include "soc/rtc.h"

struct spi_struct_t {
    spi_dev_t * dev;
#if !CONFIG_DISABLE_HAL_LOCKS
    xSemaphoreHandle lock;
#endif
    uint8_t num;
};

bool IRAM_ATTR spiTransferBitsCB(spi_t * spi, uint32_t port, uint32_t * out, void (*cb)(void*), void* cbParm)
{
    
    // hard coded for 19 bits split into 5 and 14 bit transactions.
    // ADC sample capacitor charges between rise of 5th clock and fall of 6th clock,
    // so splitting increases the sample time which produces more reliable results at 2MHz clk.

    bool noWait = false;

    // Setup 5 bit command transfer.
    // port is combined with start and sgl/diff bits and shifted to 
    // left align in the little-endian LSB which stores as the big-endian MSB for spi.
    
    spi->dev->data_buf[0] = (0b11000 | port) << 3;
    spi->dev->mosi_dlen.usr_mosi_dbitlen = (5 - 1);
    spi->dev->miso_dlen.usr_miso_dbitlen = (5 - 1);
    spi->dev->cmd.usr = 1;
    while(spi->dev->cmd.usr);

    // Now start the 14 bit result transfer

    spi->dev->mosi_dlen.usr_mosi_dbitlen = (14 - 1);
    spi->dev->miso_dlen.usr_miso_dbitlen = (14 - 1);
    spi->dev->data_buf[0] = 0;
    spi->dev->cmd.usr = 1;

    // Initiate the call back if requested

    if(cb) (*cb)(cbParm);

    // Now wait for completion (or not)

    if(spi->dev->cmd.usr){
        while(spi->dev->cmd.usr);
    }
    else {
        noWait = true;
    }
   
    // The result is in the high order 14 bits of the adc buffer in big-endian format.
    // This code extracts those bits and reformats to little-endian.

    uint32_t _out = spi->dev->data_buf[0];
    *out = (_out >> 10) | ((_out & 0xff) << 6);
       
    return noWait;
}

The IoTaWatt sampler that is using this code runs in a mostly dedicated processor. This is accomplished during startup in the the arduino environment by switching the arduino task to core 0, and later starting a FreeRTOS sampler task on core 1. All other tasks are assigned to core 0. You may not need to do that depending on your sampling requirements, but here’s how I do it.

void setup(){

        // Switch arduino framework to core 0

    if(xPortGetCoreID() == 1){
        xTaskCreateUniversal(loopTask, "looptask0", 6000, NULL, X_PRI_LOOP, &loopTaskHandle, 0);
        vTaskDelete(nullptr);
    }

Beyond that, the sampler task on core 1 receives work-order messages using XqueueReceive and sends the results to a post processor on core 0. Work-orders typically specify taking a set of samples of voltage and current for one AC cycle.

So, I’m looking at building my own iotawatt. I have 2 panels, one of which with 26 breakers. I was thinking of building 3 8266-based boards to handle all the breakers, but now I’m debating whether or not I should build those boards, or wait for this ESP32-based version to be available? Seems like it would be nice to have more power from the ESP-32, as well as having the option for an expansion board for the panel with 26 breakers. Which route would you go if you were in my position?

A bird in the hand….

Good to see the ESP32 plans (I’m late to the party!).

Any news on when the version will be ready for people to assemble themselves and help test? Very interesting progression of iotawatt.

Given the global shortages of components, is there any information available upon what specific ESP32 parts you’ve used? Just thinking if for those wanting to tinker in retirement, apart from making sure to source the correct parts, it might also be worth ordering parts upfront now… and hopefully avoid any delays, for when you release if come later this year.

:+1:

Given the mention of Ethernet and ESP32, this might be of interest to some…. and also supports POE.

TTGO T- Internet-POE ESP32 Ethernet adapters and download, extension kit
https://a.aliexpress.com/_mre2ZA4

1 Like

A post was split to a new topic: Three-Phase Three-Wire