xoseperez / espurna

Home automation firmware for ESP8266-based devices
http://tinkerman.cat
GNU General Public License v3.0
2.99k stars 636 forks source link

Multiple Virtual Analog Ports #2620

Open JavierAder opened 2 weeks ago

JavierAder commented 2 weeks ago

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

mcspr commented 1 week ago

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 commented 1 week ago

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 commented 1 week ago

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 commented 1 week ago

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?

JavierAder commented 1 week ago

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 commented 1 week ago

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 commented 6 days ago

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 commented 6 days ago

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.

https://github.com/xoseperez/espurna/blob/ac01afc8f02b2525c9ce86c431f783ea7576caf3/code/espurna/config/hardware.h#L5158 https://github.com/xoseperez/espurna/blob/ac01afc8f02b2525c9ce86c431f783ea7576caf3/code/espurna/config/hardware.h#L5161-L5162 https://github.com/xoseperez/espurna/blob/ac01afc8f02b2525c9ce86c431f783ea7576caf3/code/espurna/config/hardware.h#L5175-L5176

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 commented 6 days ago

Nice. Possible problems I see

https://github.com/xoseperez/espurna/blob/ac01afc8f02b2525c9ce86c431f783ea7576caf3/code/espurna/button.h#L20

With multiplexers the number of buttons can potentially exceed 32

https://github.com/xoseperez/espurna/blob/ac01afc8f02b2525c9ce86c431f783ea7576caf3/code/espurna/gpio.h#L85

Port conflict logic changes when there is a multiplexer

JavierAder commented 6 days ago

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 commented 3 days ago

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

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 commented 1 day ago

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.