Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Virtual Analog Ports #2620

Open
JavierAder opened this issue Sep 19, 2024 · 17 comments
Open

Multiple Virtual Analog Ports #2620

JavierAder opened this issue Sep 19, 2024 · 17 comments
Labels
enhancement New feature or request

Comments

@JavierAder
Copy link

Description

Hello. It's been a while since I wrote something. Greetings to everyone.

Motivation: The ESP8266 has only one analog port, which does not allow measuring more than one analog sensor. Personally, I need to measure multiple temperatures using NTC sensors (the digital sensors supported by Espurna haven't worked well for me); but beyond that, this modification would allow the use of multiple analog sensors of any kind (pressure sensors, humidity sensors, etc.).

Proposal: Allow Espurna to use "multiple virtual analog ports" with the help of an external analog multiplexer and the use of digital ports.

The general idea can be seen here:
https://www.youtube.com/watch?v=OgaeEiHemU4
or here (9.2 Typical Application):
https://www.ti.com/lit/ds/symlink/cd4052b.pdf?ts=1726728167254

I suppose the main modification should be made in AnalogSensor, allowing several instances of it, each associated with different "virtual analog ports." What AnalogSensor should do is: before reading the real analog input (pin 0), set the corresponding input of the multiplexer, wait for a small delay, and then read the analog value (see _rawRead() and analogRead(pin)).

At the configuration level, to enable this functionality, one should set something like:

#EnableVAP=1 // enables virtual analog ports
#DVAP= 1 or 2 or 3 // Number of digital ports connected to the multiplexer
#DVAP0= digital port to use for specifying bit 0 of the multiplexer port address
#DVAP1= digital port to use for specifying bit 1 of the multiplexer port address
#DVAP2= digital port to use for specifying bit 2 of the multiplexer port address

With these modifications, it would be possible to read from up to 8 analog devices.

Solution

No response

Alternatives

No response

Additional context

No response

@JavierAder JavierAder added the enhancement New feature or request label Sep 19, 2024
@mcspr
Copy link
Collaborator

mcspr commented Sep 23, 2024

I suppose the main modification should be made in AnalogSensor, allowing several instances of it, each associated with different "virtual analog ports." What AnalogSensor should do is: before reading the real analog input (pin 0), set the corresponding input of the multiplexer, wait for a small delay, and then read the analog value (see _rawRead() and analogRead(pin)).

So, from the sensor configuration side, it would be enough to just provide it with TYPE of analog source and PIN / CHANNEL / some-kind-of-a-name-for-ID ?

Right now something similar is happening with GPIO pins and. e.g., relays w/ mcp pins, rfb pins, sonoff pins. Relay config asks for a certain TYPE of a pin, provider gives out a pin handler that API consumer promises to then use for reads & writes.
TYPE is configured externally, at boot or on-demand, and separate from relay side of things

I do not really like the word 'virtual' here, as it does continue to use real hardware channel and just requires some extra work before doing so :) Source does change, though.

With these modifications, it would be possible to read from up to 8 analog devices.

fwiw cd4052b pdf linked describes a general purpose multiplexer, so generic pin read & write can also use those as inputs & outputs and not just analog.

@JavierAder
Copy link
Author

Hi;
my idea is something like this.
sensor.cpp


#ifdef NTC_VIRTUAL_SUPPORT

NTCSensor createVirtualNTCSensor(int NTCx,size_t NTCx_SAMPLES,
      Delay NTCx_DELAY,unsigned long NTCx_R_UP,unsigned long NTCx_R_DOWN, double NTCx_INPUT_VOLTAGE,
        unsigned long NTCx_BETA ,unsigned long NTCx_R0,double NTCx_T0)
{

        auto* sensor = new NTCSensor();
        sensor->setSamples(NTCx_SAMPLES);
        sensor->setDelay(NTCx_DELAY) ;
        sensor->setUpstreamResistor(NTCx_R_UP);
        sensor->setDownstreamResistor(NTCx_R_DOWN);
        sensor->setInputVoltage(NTCx_INPUT_VOLTAGE);
        sensor->setBeta(NTCx_BETA);
        sensor->setR0(NTCx_R0);
        sensor->setT0(NTCx_T0);
        sensor->setVirtualPort(NTCx);
        return sensor;

}
#endif

#if NTC_SUPPORT
    #ifndef NTC_VIRTUAL_SUPPORT
    {
        auto* sensor = new NTCSensor();
        sensor->setSamples(NTC_SAMPLES);
        sensor->setDelay(NTC_DELAY) ;
        sensor->setUpstreamResistor(NTC_R_UP);
        sensor->setDownstreamResistor(NTC_R_DOWN);
        sensor->setInputVoltage(NTC_INPUT_VOLTAGE);
        sensor->setBeta(NTC_BETA);
        sensor->setR0(NTC_R0);
        sensor->setT0(NTC_T0);
        add(sensor);
    }
    #else
     {
        #if NTC0 
        {add(createVirtualNTCSensor(NTC0,NTC0_SAMPLES,NTC0_DELAY,NTC0_R_UP,NTC0_R_DOWN,NTC0_INPUT_VOLTAGE,
        NTC0_BETA ,NTC0_R0,NTC0_T0)); 
         }
        #endif
        #if NTC1 
        {add(createVirtualNTCSensor(NTC1,NTC1_SAMPLES,NTC1_DELAY,NTC1_R_UP,NTC1_R_DOWN,NTC1_INPUT_VOLTAGE,
        NTC1_BETA ,NTC1_R0,NTC1_T0)); 
         }
        #endif
        //TODO: do the same for NTC2...NTC7 or use macros while.....
        
        
    }
   
    #endif
#endif

AnalogSensor.h


       void  setVirtualPort(int  vport)
        {
            _vport = vport;
        }

......


   protected:
        int _vport= -1;

        static unsigned int _rawRead(uint8_t pin, size_t samples, Delay delay) {
            // TODO: system_adc_read_fast()? current implementation is using system_adc_read()
            // (which is even more sampling on top of ours)
            unsigned int last { 0 };
            unsigned int result { 0 };
            for (size_t sample = 0; sample < samples; ++sample) {
                const auto value = ::analogRead(pin);
                result = result + value - last;
                last = value;
                if (sample > 0) {
                    espurna::time::critical::delay(delay);
                    yield();
                }
            }

            return result;
        }

        unsigned int _rawRead() const {
             #if VAP_SUPPORT
             {
                if (_vport>=0)
                {s
                    setVirtualPort();
                    //TODO: delay for multiplexer?

                }
            } 
             #endif

            return _rawRead(0, _samples, _delay);
        }

        #if VAP_SUPPORT
        void setVirtualPort() const{
            //TODO: using _vport and #DVAP0,#DVAP1 and #DVAP2 set digital outputs

        }
        #endif

Names are tentative

@JavierAder
Copy link
Author

fwiw cd4052b pdf linked describes a general purpose multiplexer, so generic pin read & write can also use those as inputs & outputs and not just analog.

Yes, multiplexing is general, but my main restriction is that there is only one analog port.

@mcspr
Copy link
Collaborator

mcspr commented Sep 24, 2024

Right, sensor specific code is possible. What I mean is to separate things ever so slightly.

Just to play around with this... You can already override analogRead(uint8_t) definition

// in any .cpp file, global namespace
int multiplexer_read(uint8_t pin) {
  ???
}

extern "C" int analogRead(uint8_t pin) {
  switch (pin) {
  case A0: // 17
    return system_adc_read();

  case 0 ... 7: // or some other unused numbers in the 0...255 range
    return multiplexer_read(pin);
  }

  return 0;
}

Extend NTCSensor code to allow PIN value changes, make a setup() code to instantiate multiplexer and then change analogRead implementation to access the multiplexer pin. Any implementation details related to PIN switching timing would be apparent, e.g. is there a need for delayMicroseconds / delay from our side or not, etc.

What I meant is to integrate multiplexer system-wide, not sensor specifically.
Have you seen relay and button code related to providers?

  • system initiates multiplexer elsewhere. api extended to provide a type of analog data source. real hw analog source is a type, multiplexer is a type.
  • sensor code made aware of multiplexer through this 'type'. this not a virtual thing, just a type of analog data source.
  • sensor code also extended to some kind of ID (pin number, channel number). configuration assigns both TYPE and ID, sensor gets analog source data though the new api instead of using analogRead directly

@JavierAder
Copy link
Author

Right, sensor specific code is possible. What I mean is to separate things ever so slightly.

Just to play around with this... You can already override analogRead(uint8_t) definition

// in any .cpp file, global namespace
int multiplexer_read(uint8_t pin) {
  ???
}

extern "C" int analogRead(uint8_t pin) {
  switch (pin) {
  case A0: // 17
    return system_adc_read();

  case 0 ... 7: // or some other unused numbers in the 0...255 range
    return multiplexer_read(pin);
  }

  return 0;
}

Extend NTCSensor code to allow PIN value changes, make a setup() code to instantiate multiplexer and then change analogRead implementation to access the multiplexer pin. Any implementation details related to PIN switching timing would be apparent, e.g. is there a need for delayMicroseconds / delay from our side or not, etc.

Nice. It's not really necessary modify AnalogSensor, extending and overriding NTCSensor is enough; anyway I like modify AnalogSensor because my idea was support multiple analog sensor in general, not only ntc sensors (MICS2710 sensor for example)

What I meant is to integrate multiplexer system-wide, not sensor specifically. Have you seen relay and button code related to providers?

No much. It is related to the class DigitalPin?

  • system initiates multiplexer elsewhere. api extended to provide a type of analog data source. real hw analog source is a type, multiplexer is a type.
  • sensor code made aware of multiplexer through this 'type'. this not a virtual thing, just a type of analog data source.
  • sensor code also extended to some kind of ID (pin number, channel number). configuration assigns both TYPE and ID, sensor gets analog source data though the new api instead of using analogRead directly

I think I understand your idea, but it seems to me that making this extension for the entire system is too complex. The problem is that there are sensors that use more than one port, but for relays, buttons, and analog sensor, yes, because they use only one port.
With this extension many more buttons, relays and analog sensors could be supported.

@JavierAder
Copy link
Author

What I meant is to integrate multiplexer system-wide, not sensor specifically. Have you seen relay and button code related to providers?

  • system initiates multiplexer elsewhere. api extended to provide a type of analog data source. real hw analog source is a type, multiplexer is a type.
  • sensor code made aware of multiplexer through this 'type'. this not a virtual thing, just a type of analog data source.
  • sensor code also extended to some kind of ID (pin number, channel number). configuration assigns both TYPE and ID, sensor gets analog source data though the new api instead of using analogRead directly

Now I think I'm understanding your idea; I didn't know about providers support. To multiplex buttons I propose two new types of providers
BUTTON_PROVIDER_GPIO_MUX = 3
BUTTON_PROVIDER_ANALOG_MUX = 4
Also, in configuration, the following keys
muxAddress0= GPIO connected to bit 0 of mux address
muxAddress1= GPIO connected to bit 1 of mux address
....
muxAddressN= GPIO connected to bit N of mux address

Then, to define a digital multiplexed button, say, number 5, you would specify the following entries in the configuration

btnProv5=3
btnGpio5= (the 'real' port; the output of the mux)
btnMuxAddress5= (the address to set in muxAddress0, muxAddress1... muxAdressN before reading the 'real' port)
(everything else keys, the same)

Ok, but what code should be modified/extended? button.cpp?

@JavierAder
Copy link
Author

As a concrete example; using CD405xB and only 4 GPIO for support 8 buttons.
ButtonMUX

In runtime config:

muxAddress0= 5 //GPIO connected to Pin A of multiplexer
muxAddress1= 4 //GPIO connected to Pin B of multiplexer
muxAddress2= 0 //GPIO connected to Pin C of multiplexer

For BTN1

btnProv1=3 //BUTTON_PROVIDER_GPIO_MUX
btnGpio1= 2 //GPIO connected to the output mulltiplexer, Pin COM
btnMuxAddress1 = 0 //Multiplexer channel to which the button is connected
.....

For BTN2

btnProv2=3 //BUTTON_PROVIDER_GPIO_MUX
btnGpio2= 2 //GPIO connected to the output mulltiplexer, Pin COM,SAME of BTN1
btnMuxAddress1 = 1 //Multiplexer channel to which the button is connected
.....

The same for BTN3... BTN8 changing btnMuxAddressX.

@mcspr
Copy link
Collaborator

mcspr commented Sep 28, 2024

Now I think I'm understanding your idea; I didn't know about providers support. To multiplex buttons I propose two new types of providers
BUTTON_PROVIDER_GPIO_MUX = 3
BUTTON_PROVIDER_ANALOG_MUX = 4
Also, in configuration, the following keys
muxAddress0= GPIO connected to bit 0 of mux address
muxAddress1= GPIO connected to bit 1 of mux address
....
muxAddressN= GPIO connected to bit N of mux address

Not quite the same as e.g. MCP support flag. It adds extra type for pin, but button continues to use GPIO provider and digital reads.

#define MCP23S08_SUPPORT 1

#define RELAY1_PIN 4
#define RELAY1_PIN_TYPE GPIO_TYPE_MCP23S08

#define BUTTON1_PIN 0
#define BUTTON1_PIN_TYPE GPIO_TYPE_MCP23S08

btnMuxAddress aka 'Multiplexer channel to which the hardware is connected' is btnGpio. Since the main use-case is digital access
Meaning, BUTTON config only knows about the MUX pin and only MUX config knowns about the hardware pins it controls. There is a limitation of available keywords, though, but I presumed it would be enough of a an abstraction.

muxType => cb450xb
muxComGpio => 2
muxGpio0 => 5
muxGpio1 => 4
muxGpio2 => 0
btnGpioType1 => cd450xb
btnGpio1 => 0
btnGpioType2 => cd450xb
btnGpio1 => 1

Analog buttons in this case also re-use the same config, changing btnProv to analog would still be able to access type and pin number which in turn would use a different proxy class to read specific MUX channel on ADC

@JavierAder
Copy link
Author

Nice.
Possible problems I see

constexpr size_t ButtonsMax { 32ul };

With multiplexers the number of buttons can potentially exceed 32

inline bool gpioLock(GpioBase& base, unsigned char pin, bool value,

Port conflict logic changes when there is a multiplexer

@JavierAder
Copy link
Author

On the other hand, to extend not only buttons, but also relays or LEDs (in general, to use a multiplexer as output), I think it is necessary to use not only a multiplexer, but also a buffer; this buffer is enabled using the additional port
(I'll upload a schematic later).
Say
muxBufferGPIO= //GPIO connected to the pin ENABLE of ouput buffer

@JavierAder
Copy link
Author

Hi. Apologies, but I've been rethinking these ideas and I think:

  • for digital input or output expansion, using multiplexers/demultiplexers seems unnecessarily complex to me. It seems much more elegant and simple to do it by supporting shift registers; that has the added advantage that only 3 ports need to be used. For example
    https://resources.altium.com/p/how-expand-input-and-output-microcontroller
    "A more elegant solution is to use serial clocking shift registers like the 74HC595 for output and 74HC165 for input. These ICs can be cascaded to each other with the limitation being the latency to shift the bytes to all the ICs. Using shift registers only involves three I/O pins on the microcontroller, regardless of the number of ICs."

  • for what I do think is almost insurmountable to use a multiplexer is to expand the number of analog inputs, which was my original problem.

Following up on this last idea, a perhaps simple way for future analog sensors to make use of multiplexing is to use a special encoding to specify their "pin" when calling analogRead(uint8_t pin):
pin=0 (0x00) analogRead works the same as now
pin=0x1pppppppp=128+ Address in the analog multiplexer
That is, the most significant bit is used to distinguish the standard analog reading from the reading using the multiplexer (ok, this is the same as you proposed before, but differentiating 0 as the normal pin; the current code uses analogRead(0) not analogRead(A0)).

To configure the analog multiplexer as a whole, the keys you proposed can be used, except muxComGpio (the analog multiplexer output must always be connected to pin A0 of the microcontroller).
Later I will try to define a sensor called NTCMuxSensor that simply extends NTCSensor by modifying the code that performs the reading to exemplify these ideas (obviously, many instances of NTCMuxSensor will be allowed).

@mcspr
Copy link
Collaborator

mcspr commented Oct 2, 2024

for digital input or output expansion, using multiplexers/demultiplexers seems unnecessarily complex to me. It seems much more elegant and simple to do it by supporting shift registers; that has the added advantage that only 3 ports need to be used. For example
https://resources.altium.com/p/how-expand-input-and-output-microcontroller
"A more elegant solution is to use serial clocking shift registers like the 74HC595 for output and 74HC165 for input. These ICs can be cascaded to each other with the limitation being the latency to shift the bytes to all the ICs. Using shift registers only involves three I/O pins on the microcontroller, regardless of the number of ICs."

Also true. Still, generic input support is a possibility? It does run into the case of not-the-best-tool-for-the-job, yes. Suppose, such multiplexer api can be cut out from allowing OUTPUTs, limiting pin abstraction to INPUTs only. Expander, shift registers, etc. can be allowed to support both.

pin=0 (0x00) analogRead works the same as now
pin=0x1pppppppp=128+ Address in the analog multiplexer

Ah. So, the Arduino side supports both by checking whether input is pin == 0 || pin == 17 (aka A0). I was reading espurna analog button code at that time, where I incidentally only added the A0 check.

Note that analogRead replacement is intended for 'variant' / 'board' / 'only-works-on-this-hw' override. e.g. https://github.com/esp8266/Arduino/blob/ccea72823ac50290bc05c67350d2be6626e65547/variants/wifi_slot/analogRead.cpp#L6

I do still lean to the idea of separating mux + analog and just analog through gpio type... But, still have to think about it some more.

@JavierAder
Copy link
Author

Well, here is my first attempt. Note that I added not only support for multiple NTCs but also for multiple Emon sensors (current support for multiple emon sensors requires additional hardware more complex than a simple multiplexer). I think this would be a good general scheme to support any other type of analog sensor in which more instances are required.
Hardwired in the code, using a single multiplexer, 3 NTC sensors and 2 Emon sensors would be supported, using a single multiplexer.
Obviously, feel totally free of any type of correction or suggestion.

New variables of preprocessor:

AnalogMux_SUPPORT
NTCMuxSensor_SUPPORT
EmonAnalogMuxSensor_SUPPORT

New settings:
TODO

Files:
sensor.cpp modified
AnalogMux.h added
NTCMuxSensor.h added
EmonAnalogMuxSensor.h added

AnalogMux.h

#include "espurna.h"

class AnalogMux{
    private:
    //TODO: use an array/vector for GPIOs associated with address pins of Multiplexer
    static uint8_t _muxGPIO0;
    static uint8_t _muxGPIO1;
    static uint8_t _muxGPIO2;

    public:
    static void setup(){
     //TODO
      //Using settings or defines, set GPIOs for addressing the multiplexer and configure
      // and locks pins as digital outs
      _muxGPIO0=5;
      _muxGPIO1=4;
      _muxGPIO2=0;

      
    }

    static void setAddressMux(uint8_t pin)
    {
        //TODO: decode pin as 1's and 0's, use that for set GPIOs, and then use the analogRead of system
        
        //Digital write to GPI0
        //Digital write to GPI1
        //...

        //delay before analog Read?
        //delay(?)

        
    }
};

NTCMuxSensor.h

#pragma once

#include "AnalogMux.h"
#include "NTCSensor.h"

class NTCMuxSensor : public NTCSensor {
    private:
    //Instances of all NTCMux created in setup()
    static  std::vector<NTCMuxSensor> _insts;


    public:
    static  std::vector<NTCMuxSensor> getSensors()
    {
      return _insts;
    }
    static void setup(){
     //TODO
      //Using settings or defines, set GPIOs for addressing the multiplexer and configure
      // and locks pins as digital outs
      //For now, for we create 3 ntc sensor
      NTCMuxSensor* sensor = new NTCMuxSensor();
      sensor->setAnalogPin(0);
      sensor->setSamples(1); //TODO: find values for real ntc sensors
      sensor->setDelay(0);
      sensor->setUpstreamResistor(100);
      sensor->setDownstreamResistor(100);
      sensor->setInputVoltage(1);
      sensor->setBeta(1);
      sensor->setR0(10);
      sensor->setT0(10);
      _insts.push_back(*sensor);
      sensor = new NTCMuxSensor();
      sensor->setAnalogPin(1);
      sensor->setSamples(1); //TODO: find values for real ntc sensors
      sensor->setDelay(0);
      sensor->setUpstreamResistor(100);
      sensor->setDownstreamResistor(100);
      sensor->setInputVoltage(1);
      sensor->setBeta(1);
      sensor->setR0(10);
      sensor->setT0(10);
      _insts.push_back(*sensor);
      sensor = new NTCMuxSensor();
      sensor->setAnalogPin(2);
      sensor->setSamples(1); //TODO: find values for real ntc sensors
      sensor->setDelay(0);
      sensor->setUpstreamResistor(100);
      sensor->setDownstreamResistor(100);
      sensor->setInputVoltage(1);
      sensor->setBeta(1);
      sensor->setR0(10);
      sensor->setT0(10);
      _insts.push_back(*sensor);


    }

    void setAnalogPin(uint8_t analogPin)
    {
      _analogPin=analogPin;   
    }
    
    // Descriptive name of the sensor
    String description() const override {
      //TODO, use _analogPin for description?
      return F("NTCMux @ TOUT");
    } 

    void pre() override {
      //Before read trhow A0 set the multiplexer 
      AnalogMux::setAddressMux(_analogPin);
      
      NTCSensor::pre();
    }
    protected:

    uint8_t _analogPin;  

};

EmonAnalogMuxSensor.h


#pragma once

#include "AnalogMux.h"

#include "EmonAnalogSensor.h"

class EmonAnalogMuxSensor : public EmonAnalogSensor{

    private:
    //Instances of all EmonAnalogMuxSensor created in setup()
    static  std::vector<EmonAnalogMuxSensor> _insts;


    public:
    static  std::vector<EmonAnalogMuxSensor> getSensors()
    {
      return _insts;
    }
    static void setup(){
     //TODO
      //Using settings or defines, set GPIOs for addressing the multiplexer and configure
      // and locks pins as digital outs
      //For now, for we create 2 sensor, one for current and one for voltage
      //
      EmonAnalogMuxSensor* sensor = new EmonAnalogMuxSensor();
      sensor->setAnalogPin(3); //0, 1 and 2 is used for NTC sensors exsmples
        //TODO: se values for real sensors
      _insts.push_back(*sensor);

      sensor = new EmonAnalogMuxSensor();
      sensor->setAnalogPin(4);

      _insts.push_back(*sensor);
    }

    void setAnalogPin(uint8_t analogPin)
    {
      _analogPin=analogPin;   
    }
    
    unsigned int analogRead() override {
    
         //Before read trhow A0 set the multiplexer 
        AnalogMux::setAddressMux(_analogPin);


        return EmonAnalogSensor::analogRead();
    }
 
    protected:

    uint8_t _analogPin;  



};

@JavierAder
Copy link
Author

Ah...
sensor.cpp:225

#if AnalogMux_SUPPORT
    #include "sensors/AnalogMux.h"
#endif

#if NTCMuxSensor_SUPPORT
    #include "sensors/NTCMuxSensor.h"
#endif

#if EmonAnalogMuxSensor_SUPPORT
    #include "sensors/EmonAnalogMuxSensor.h"
#endif

sensor.cpp:2619

#if AnalogMux_SUPPORT
    {
       AnalogMux::setup();
    }
#endif

#if NTCMuxSensor_SUPPORT
    {
       NTCMuxSensor::setup();
             //Add all sensors NTC 
       std::vector<NTCMuxSensor> sensors= NTCMuxSensor::getSensors();
       for (std::size_t i = 0; i < sensors.size(); i++) {
            NTCMuxSensor sensor = sensors[i];
            add(sensor);
        }
 
    }
#endif

#if EmonAnalogMuxSensor_SUPPORT
    {
       EmonAnalogMuxSensor::setup();
        //Add all sensors NTC 
       std::vector<EmonAnalogMuxSensor> sensors= NTCMuxSensor::getSensors();
       for (std::size_t i = 0; i < sensors.size(); i++) {
            NTCMuxSensor sensor = sensors[i];
            add(sensor);
        }

    }
#endif


@JavierAder
Copy link
Author

Hi. a more concrete version:
-Code was added to specify the NTC sensor data in a compact way in runtime settings; only one string for sensor
-Code was added to manage the multiplexer before the analog reading through the virtual pins (setAddressBeforeReading)
-TODO: get config strings from espurna

AnalogMux.h

#include "espurna.h"
#include "../gpio.h"

class AnalogMux
{
private:

    using Delay = espurna::duration::critical::Microseconds;
    static Delay _delay;
    // TODO: use an array/vector for GPIOs associated with address pins of Multiplexer
    static uint8_t _muxGPIO0;
    static uint8_t _muxGPIO1;
    static uint8_t _muxGPIO2;

    static std::vector<uint8_t> gpios;

    static int _error;

public:
    static void setup()
    {
        // TODO
        // Using settings or defines, set GPIOs for addressing the multiplexer and configure
        //  and locks pins as digital outs, and set delay
        _delay = Delay{ 100 };
        _muxGPIO0 = 5;
        _muxGPIO1 = 4;
        _muxGPIO2 = 0;

        // error, TODO debug print
        _error = SENSOR_ERROR_OK;

        // locks gpio and mode
        if (!gpioLock(_muxGPIO0))
        {
            // error, TODO debug print
            _error = SENSOR_ERROR_GPIO_USED;
        }
        pinMode(_muxGPIO0, OUTPUT);
        gpios.push_back(_muxGPIO0);

        if (!gpioLock(_muxGPIO1))
        {
            // error, TODO debug print
            _error = SENSOR_ERROR_GPIO_USED;
        }
        pinMode(_muxGPIO1, OUTPUT);
        gpios.push_back(_muxGPIO1);

        if (!gpioLock(_muxGPIO2))
        {

            // error, TODO debug print
            _error = SENSOR_ERROR_GPIO_USED;
        }
        pinMode(_muxGPIO2, OUTPUT);
        gpios.push_back(_muxGPIO2);
    }

    static void setAddressMuxBeforeRead(uint8_t pin)
    {
        if (_error)
            return;

        for (int i = 0; i < gpios.size(); i++)
        {
            uint8_t gpio = gpios[i];
            int bit = pin & 0x01;
            if (bit){
                digitalWrite(gpio, HIGH);
            }else{
                digitalWrite(gpio, LOW);

            }
            pin = pin >>1;
        }
        // TODO: 
        //If pin >0 -> error

        // delay before analog Read?
        espurna::time::critical::delay(_delay);
    }
};

NTCMuxSensor.h


#pragma once

#include "AnalogMux.h"
#include "NTCSensor.h"
#include "../settings_convert.h"

class NTCMuxSensor : public NTCSensor {
    private:
    //Instances of all NTCMux created in setup()
    static  std::vector<NTCMuxSensor> _insts;
    //helper

    // Helper
    static std::vector<String> splitConfig(String str)
    {
      std::vector<String> strings;
      char separator = ',';
      int startIndex = 0, endIndex = 0;
      for (int i = 0; i <= str.length(); i++)
      {

        // If we reached the end of the word or the end of the input.
        if (str[i] == separator || i == str.length())
        {
          endIndex = i;
          String temp;
          temp = str.substring(startIndex, endIndex);
          strings.push_back(temp);
          startIndex = endIndex + 1;
        }
      }
      return strings;
    }

    public:
    static  std::vector<NTCMuxSensor> getSensors()
    {
      return _insts;
    }



    static void setup(){
     //TODO: get allConfigs from settings

      //Using settings or defines, set GPIOs for addressing the multiplexer and configure
      // and locks pins as digital outs
      //For now, for we create 3 ntc sensor
      //FORMAT: 9 items separated by commas
      //AnalogPin:int,samples:int,delay:int,upResistor:long,downResistor:long,inputVoltage:double,beta:long,r0:long,t0:long
      //Examples: 10 k upResistor, voltage 1.0, beta 3799,R0 10000,T0 298.15 (25 C in Kelvin)
      String s1="0,1,0,10000,0,1.0,3799,10000,298.15";
      String s2="1,1,0,10000,0,1.0,3799,10000,298.15";
      String s3="2,1,0,10000,0,1.0,3799,10000,298.15";
      std::vector<String> allConfigs;
      allConfigs.push_back(s1);
      allConfigs.push_back(s2);
      allConfigs.push_back(s3);

      using namespace espurna::settings::internal;

      for (auto config: allConfigs) {
        std::vector<String> dataSensor = splitConfig(config);
        if (dataSensor.size() != 9)
        {
          //error:wrong number of items in config string
          //TODO: print debug?
          continue; 
        }
        NTCMuxSensor* sensor = new NTCMuxSensor();
        //TODO:check values
        int pin = convert<int>(dataSensor[0]);
        int samples = convert<int>(dataSensor[1]);
        int delay = convert<int>(dataSensor[2]);
        long rUp = convert<long>(dataSensor[3]);
        long rDown =convert<long>(dataSensor[4]);
        double voltage = convert<double>(dataSensor[5]);
        long beta = convert<long>(dataSensor[6]);
        long r0 = convert<long>(dataSensor[7]);
        double t0 = convert<double>(dataSensor[8]);

        sensor->setAnalogPin(pin);
        sensor->setSamples(samples); 
        sensor->setDelay(delay);
        sensor->setUpstreamResistor(rUp);
        sensor->setDownstreamResistor(rDown);
        sensor->setInputVoltage(voltage);
        sensor->setBeta(beta);
        sensor->setR0(r0);
        sensor->setT0(t0);
        _insts.push_back(*sensor);
      }


    }

    //API sensor

    void setAnalogPin(uint8_t analogPin)
    {
      _analogPin=analogPin;   
    }
    
    // Descriptive name of the sensor
    String description() const override {
      //TODO, use _analogPin for description?
      return F("NTCMux @ TOUT");
    } 

    void pre() override {
      //Before read trhow A0 set the multiplexer 
      AnalogMux::setAddressMuxBeforeRead(_analogPin);
      
      NTCSensor::pre();
    }
    protected:

    uint8_t _analogPin;  

};


I think the code is almost usable, although I'm not sure if I'm using the APIs correctly. Any suggestion is appreciated.

@JavierAder
Copy link
Author

Ok, a running version:
PrimerBuildAndando
In terminal:

set analogMux 50,14,15,16
set ntcMux1 0,1,0,10000,0,1.0,3799,10000,298.15
set ntcMux2 1,1,0,10000,0,1.0,3799,10000,298.15
set ntcMux3 2,1,0,10000,0,1.0,3799,10000,298.15
set ntcMux4 3,1,0,10000,0,1.0,3799,10000,298.15

config\custom.h


// ------------------------------------------------------------------------------
// Example file for custom.h
// Either copy and paste this file then rename removing the .example or create your
// own file: 'custom.h'
// This file allows users to create their own configurations.
// See 'code/espurna/config/general.h' for default settings.
//
// See: https://github.com/xoseperez/espurna/wiki/Software-features#enabling-features
// and 'code/platformio_override.ini.example' for more details.
// ------------------------------------------------------------------------------

//LOLIN with AnalogMux support
#if defined(NODEMCU_LOLIN_AM)

    // Info
    #define MANUFACTURER        "NODEMCU"
    #define DEVICE              "LOLIN_AM"

    // Buttons
    #define BUTTON1_PIN         0
    #define BUTTON1_CONFIG      BUTTON_PUSHBUTTON | BUTTON_DEFAULT_HIGH
    #define BUTTON1_RELAY       1

    // Hidden button will enter AP mode if dblclick and reset the device when long-long-clicked
    #define RELAY1_PIN          12
    #define RELAY1_TYPE         RELAY_TYPE_NORMAL

    // Light
    #define LED1_PIN            2
    #define LED1_PIN_INVERSE    1
	
	//AnalogMux and NTCMux
	#define SENSOR_SUPPORT     	1
	#define AnalogMux_SUPPORT 	1
	#define NTCMuxSensor_SUPPORT 1
	

#endif

And finally, diff

diff --git a/code/espurna/config/sensors.h b/code/espurna/config/sensors.h
index 727183e7..66babd7b 100644
--- a/code/espurna/config/sensors.h
+++ b/code/espurna/config/sensors.h
@@ -1510,7 +1510,8 @@
     MICS2710_SUPPORT || \
     MICS5525_SUPPORT || \
     NTC_SUPPORT || \
-    TMP3X_SUPPORT \
+    TMP3X_SUPPORT || \
+    AnalogMux_SUPPORT \
 )
 #undef ADC_MODE_VALUE
 #define ADC_MODE_VALUE ADC_TOUT
diff --git a/code/espurna/config/types.h b/code/espurna/config/types.h
index 81c30b8d..1725913c 100644
--- a/code/espurna/config/types.h
+++ b/code/espurna/config/types.h
@@ -332,6 +332,10 @@
 #define SENSOR_SM300D2_ID           43
 #define SENSOR_PM1006_ID            44
 #define SENSOR_INA219_ID            45
+#define SENSOR_ANALOG_MUX_ID        46
+#define SENSOR_NTC_MUX_ID           47
+
+
 
 //--------------------------------------------------------------------------------
 // Magnitudes
diff --git a/code/espurna/sensor.cpp b/code/espurna/sensor.cpp
index a2c4a188..6acfecff 100644
--- a/code/espurna/sensor.cpp
+++ b/code/espurna/sensor.cpp
@@ -222,6 +222,16 @@ Copyright (C) 2020-2022 by Maxim Prokhorov <prokhorov dot max at outlook dot com
     #include "sensors/PZEM004TV30Sensor.h"
 #endif
 
+#if AnalogMux_SUPPORT
+    #include "sensors/AnalogMux.h"
+#endif
+
+#if NTCMuxSensor_SUPPORT
+    #include "sensors/NTCMuxSensor.h"
+#endif
+
+
+
 #include "filters/LastFilter.h"
 #include "filters/MaxFilter.h"
 #include "filters/MedianFilter.h"
@@ -2601,6 +2611,32 @@ void load() {
         add(sensor);
     }
 #endif
+
+
+#if AnalogMux_SUPPORT
+    {
+       AnalogMux* am = AnalogMux::createInst();
+       add(am);
+       
+    }
+#endif
+
+#if NTCMuxSensor_SUPPORT
+    {
+       
+       //Add all sensors NTC 
+       NTCMuxSensorConfig c;
+       std::vector<NTCMuxSensor *> ntcSensors = c.getSensors();
+       for (std::size_t i = 0; i < ntcSensors.size(); i++) {
+            NTCMuxSensor* sensor = ntcSensors[i];
+            add(sensor);
+        }
+ 
+    }
+#endif
+
+
+
 }
 
 namespace units {
diff --git a/code/espurna/sensors/AnalogMux.h b/code/espurna/sensors/AnalogMux.h
new file mode 100644
index 00000000..4d9b7ed2
--- /dev/null
+++ b/code/espurna/sensors/AnalogMux.h
@@ -0,0 +1,178 @@
+
+#pragma once
+
+#include "espurna.h"
+#include "../gpio.h"
+#include "../settings.h"
+#include "../settings_convert.h"
+#include "BaseSensor.h"
+
+class AnalogMux : public BaseSensor
+{
+private:
+    static AnalogMux *_inst;
+
+    using Delay = espurna::duration::critical::Microseconds;
+    // static
+    Delay _delay;
+    int _access;
+
+    std::vector<uint8_t> _gpios;
+
+    // Helper
+    std::vector<String> splitConfig(String str)
+    {
+        std::vector<String> strings;
+        char separator = ',';
+        uint startIndex = 0, endIndex = 0;
+        for (uint i = 0; i <= str.length(); i++)
+        {
+
+            // If we reached the end of the word or the end of the input.
+            if (str[i] == separator || i == str.length())
+            {
+                endIndex = i;
+                String temp;
+                temp = str.substring(startIndex, endIndex);
+                strings.push_back(temp);
+                startIndex = endIndex + 1;
+            }
+        }
+        return strings;
+    }
+
+    void unlockGPIOs()
+    {
+        for (uint i = 0; i < _gpios.size(); i++)
+        {
+            uint8_t gpio = _gpios[i];
+            gpioUnlock(gpio);
+        }
+        _gpios.clear();
+    }
+
+public:
+    static AnalogMux *createInst();
+
+    static AnalogMux *Inst();
+
+    // static void setup()
+    //{
+    //  Sensor ID, must be unique
+    unsigned char id() const override
+    {
+        return SENSOR_ANALOG_MUX_ID;
+    }
+
+    // Number of available value slots
+    unsigned char count() const override
+    {
+        return 1;
+    }
+    // Descriptive name of the sensor
+    String description() const override
+    {
+        return "AnalogMux";
+    }
+    // Address of the sensor (it could be the GPIO or I2C address)
+    String address(unsigned char) const override
+    {
+        return "TODO";
+    }
+    // Type for slot # index
+    unsigned char type(unsigned char index) const override
+    {
+        if (index == 0)
+            return MAGNITUDE_COUNT;
+
+        return MAGNITUDE_NONE;
+    }
+
+    // Current value for slot # index
+    double value(unsigned char index) override
+    {
+        if (index == 0)
+            return _access;
+        return 0;
+    }
+    void begin() override
+    {
+        _access = 0;
+
+        if (_gpios.size() > 0)
+            unlockGPIOs();
+
+        using namespace espurna::settings::internal;
+        // FORMAT:analogMux=DelayBeforeRead(Microsecs),GPIO0,GPIO1....
+        String config = getSetting("analogMux");
+        // TODO:check in getSetting can return null
+        if (config == nullptr)
+            config = "";
+
+        std::vector<String> configs = splitConfig(config);
+
+        if (configs.size() < 2)
+        {
+            _error = SENSOR_ERROR_CONFIG;
+            return;
+        }
+        _delay = Delay{convert<int>(configs[0])};
+
+        for (uint i = 1; i < configs.size(); i++)
+        {
+
+            uint8_t muxGPIO = convert<int>(configs[i]);
+            // locks gpio and mode
+            if (!gpioLock(muxGPIO))
+            {
+                // error, TODO debug print
+                _error = SENSOR_ERROR_GPIO_USED;
+                return;
+            }
+            pinMode(muxGPIO, OUTPUT);
+            _gpios.push_back(muxGPIO);
+        }
+
+        _error = SENSOR_ERROR_OK;
+        _ready = true;
+    }
+
+    void setAddressMuxBeforeRead(uint8_t pin)
+    {
+        if (_error)
+            return;
+
+        for (uint i = 0; i < _gpios.size(); i++)
+        {
+            uint8_t gpio = _gpios[i];
+            int bit = pin & 0x01;
+            if (bit)
+            {
+                digitalWrite(gpio, HIGH);
+            }
+            else
+            {
+                digitalWrite(gpio, LOW);
+            }
+            pin = pin >> 1;
+        }
+        _access++;
+        // TODO:
+        // If pin >0 -> error
+
+        // delay before analog Read?
+        espurna::time::critical::delay(_delay);
+    }
+};
+
+AnalogMux* AnalogMux::_inst = nullptr;
+
+AnalogMux* AnalogMux::createInst()
+{
+    AnalogMux::_inst = new AnalogMux();
+    return _inst;
+}
+AnalogMux* AnalogMux::Inst()
+{
+    return _inst;
+}
\ No newline at end of file
diff --git a/code/espurna/sensors/AnalogSensor.h b/code/espurna/sensors/AnalogSensor.h
index c8ccbd63..b06d5107 100644
--- a/code/espurna/sensors/AnalogSensor.h
+++ b/code/espurna/sensors/AnalogSensor.h
@@ -160,8 +160,13 @@ class AnalogSensor : public BaseAnalogSensor {
         double _offset { 0.0 };
 };
 
+constexpr int AnalogSensor::RawBits;
+
 constexpr double AnalogSensor::RawMin;
 constexpr double AnalogSensor::RawMax;
 
 constexpr AnalogSensor::Delay AnalogSensor::DelayMin;
 constexpr AnalogSensor::Delay AnalogSensor::DelayMax;
+
+constexpr size_t AnalogSensor::SamplesMin; 
+constexpr size_t AnalogSensor::SamplesMax;
diff --git a/code/espurna/sensors/NTCMuxSensor.h b/code/espurna/sensors/NTCMuxSensor.h
new file mode 100644
index 00000000..c0c68fbf
--- /dev/null
+++ b/code/espurna/sensors/NTCMuxSensor.h
@@ -0,0 +1,170 @@
+#pragma once
+
+#include "AnalogMux.h"
+#include "NTCSensor.h"
+#include "AnalogSensor.h"
+#include "../settings.h"
+#include "../settings_convert.h"
+
+class NTCMuxSensor : public NTCSensor
+{
+private:
+  // static std::vector<NTCMuxSensor *> *_ntcSensors;
+  //  Helper
+  // static std::vector<String> splitConfig(String str);
+
+public:
+  // static std::size_t getSensors();
+  // static NTCMuxSensor *getSensor(std::size_t);
+
+  NTCMuxSensor()
+  {
+  }
+  void setAnalogPin(uint8_t analogPin)
+  {
+    _analogPin = analogPin;
+  }
+  unsigned char id() const override
+  {
+    return SENSOR_NTC_MUX_ID;
+  }
+
+  // Descriptive name of the sensor
+  String description() const override
+  {
+    // TODO, use _analogPin for description?
+    return "NTCMux "+String( _analogPin)+ " "+String(_input_voltage);
+  }
+
+  // Current value for slot # index
+  double value(unsigned char index) override
+  {
+    if (index == 0)
+    {
+      return _value;
+    }
+
+    return 0.0;
+  }
+  void pre() override
+  {
+    // Before read trhow A0 set the multiplexer
+    AnalogMux *am = AnalogMux::Inst();
+    if (am == nullptr)
+      return;
+    if (am->error())
+      return;
+    am->setAddressMuxBeforeRead(_analogPin);
+
+    NTCSensor::pre();
+  }
+
+protected:
+  uint8_t _analogPin;
+};
+
+class NTCMuxSensorConfig
+{
+public:
+  NTCMuxSensorConfig()
+  {
+  }
+
+  std::vector<String> splitConfig(String str)
+  {
+    std::vector<String> strings;
+    char separator = ',';
+    uint startIndex = 0, endIndex = 0;
+    for (uint i = 0; i <= str.length(); i++)
+    {
+
+      // If we reached the end of the word or the end of the input.
+      if (str[i] == separator || i == str.length())
+      {
+        endIndex = i;
+        String temp;
+        temp = str.substring(startIndex, endIndex);
+        strings.push_back(temp);
+        startIndex = endIndex + 1;
+      }
+    }
+    return strings;
+  }
+
+  std::vector<NTCMuxSensor *> getSensors()
+  {
+    std::vector<NTCMuxSensor *> ntcSensors;
+    // TODO: get allConfigs from settings
+
+    // Using settings or defines, set GPIOs for addressing the multiplexer and configure
+    //  and locks pins as digital outs
+    // For now, for we create 3 ntc sensor
+    // FORMAT: 9 items separated by commas
+    // AnalogPin:int,samples:int,delay:int,upResistor:long,downResistor:long,inputVoltage:double,beta:long,r0:long,t0:long
+    // Examples: 10 k upResistor, voltage 1.0, beta 3799,R0 10000,T0 298.15 (25 C in Kelvin)
+    // String s1="0,1,0,10000,0,1.0,3799,10000,298.15";
+    // String s2="1,1,0,10000,0,1.0,3799,10000,298.15";
+    // String s3="2,1,0,10000,0,1.0,3799,10000,298.15";
+
+    using namespace espurna::settings::internal;
+    // TODO: get all settings with prefix ntcMux
+    String s1 = getSetting("ntcMux1");
+    String s2 = getSetting("ntcMux2");
+    String s3 = getSetting("ntcMux3");
+    String s4 = getSetting("ntcMux4");
+
+    // TODO check if getSetting can return null
+    if (s1 == nullptr)
+      s1 = "";
+    if (s2 == nullptr)
+      s2 = "";
+    if (s3 == nullptr)
+      s3 = "";
+    if (s4 == nullptr)
+      s4 = "";
+
+    std::vector<String> allConfigs;
+    allConfigs.push_back(s1);
+    allConfigs.push_back(s2);
+    allConfigs.push_back(s3);
+    allConfigs.push_back(s4);
+
+    for (auto config : allConfigs)
+    {
+      std::vector<String> dataSensor = splitConfig(config);
+      if (dataSensor.size() != 9)
+      {
+        // error:wrong number of items in config string
+        // TODO: print debug?
+        continue;
+      }
+      NTCMuxSensor *sensor = new NTCMuxSensor();
+      // TODO:check values
+      int pin = convert<int>(dataSensor[0]);
+      int samples = convert<int>(dataSensor[1]);
+      int delay = convert<int>(dataSensor[2]);
+      long rUp = convert<long>(dataSensor[3]);
+      long rDown = convert<long>(dataSensor[4]);
+      double voltage = convert<double>(dataSensor[5]);
+      long beta = convert<long>(dataSensor[6]);
+      long r0 = convert<long>(dataSensor[7]);
+      double t0 = convert<double>(dataSensor[8]);
+
+      sensor->setAnalogPin(pin);
+      sensor->setSamples(samples);
+      sensor->setDelay(delay);
+      sensor->setUpstreamResistor(rUp);
+      sensor->setDownstreamResistor(rDown);
+      sensor->setInputVoltage(voltage);
+      sensor->setBeta(beta);
+      sensor->setR0(r0);
+      sensor->setT0(t0);
+      ntcSensors.push_back(sensor);
+    }
+    return ntcSensors;
+  }
+};
+
+// std::vector<NTCMuxSensor *> *NTCMuxSensor::_ntcSensors = nullptr;
+//  STATIC METHODS
+//   Helper
diff --git a/code/platformio_override.ini b/code/platformio_override.ini
new file mode 100644
index 00000000..1b9424f3
--- /dev/null
+++ b/code/platformio_override.ini
@@ -0,0 +1,4 @@
+[env:nodemcu-lolin-analog-mux]
+extends = env:esp8266-4m-base
+build_src_flags = -DNODEMCU_LOLIN_AM -DNOWSAUTH -DUSE_CUSTOM_H
+

TODO: get multiples config with prefix ntcMux

@JavierAder
Copy link
Author

Well, these days I realized two things:

  • the ESP8266 ADC besides having low resolution is very noisy (consecutively read temperatures could vary several degrees)
  • there is a way to support both multiple analog ports and a better ADC: ADS1115. It has a 16-bit ADC and an internal multiplexer that supports up to 4 channels (I think they can also be cascaded to support even more)
    https://www.ti.com/product/es-mx/ADS1115
    There is already code that uses that chip (EmonADS1X15Sensor.h) although not related to NTCs.

I think using ADS1115 should be the standard way to expand analog inputs, possibly at AnalogSensor level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants