wollewald/ADS1115_WE

Ability to use result range

thijstriemstra opened this issue ยท 44 comments

I have 4 potentiometers that need to control MIDI signals (range 0-127). I successfully tested the continous and single shot examples only I wonder how to read all 4 channels in the continous example? The example only shows how to read channel 0. Or should I use the interrupt pin example for my usecase (which example though)? And how would I read a range of 0-127 instead of voltage from 0-5.08?

And another question: adc.getResult_mV() returns a float but I only see integers (e.g. between 0.0 and 5073.0), is this correct?

The ADS1115 has internally only one ADS and one conversion register in which the conversion results are stored. So you have to switch channels between the measurements. But you need to be careful, because you might still read the value of the former channel, because you can read the conversion register any time and not only when a conversion is ready. Therefore I would recommend to use the single-shot mode if you want to change channels. In this mode you can use the isBusy function to ensure you read the result after the latest conversion. This does not work in the continous mode. It's anyway better to use single shot mode for battery based projects. The ADS1115 will go into power down mode between the measurements.

And what do you mean with the range of 0 - 127? 0 - 127 Millivolt? Then I would choose the range up to 256 Millivolt.

The getResult_mV function uses:
float result = (rawResult * 1.0 / ADS1115_REG_FACTOR) * voltageRange;
rawResult, ADS1115_REG_FACTOR and voltageRange are indeed not float, but the result is. The precision of the ADS1115 is higher than 1 mV.

And thanks fr the pull requests - good to have some checking thoroughly. I think I'll wait until you found all issues, before rework.

And what do you mean with the range of 0 - 127? 0 - 127 Millivolt? Then I would choose the range up to 256 Millivolt.

A value between 0 - 127 instead of a voltage. I guess I can map the voltage of result_mV from 0 to 5073 and turn it into 0-127. Maybe a potentional new method for the library?

It's anyway better to use single shot mode for battery based projects. The ADS1115 will go into power down mode between the measurements.

I will go with that.

Others might need other ranges like 0-255, 0-511, but will think about it.

Oh sorry, I am not so familiar with this. I have no experience with contributions. How can I make you a contributor? I am so thankful for your contribution.

That's ok! Adding a method or option to return a different range would be nice, what about something like:

float setRange(float min, float max);

and now getResult_mV returns a different range.. Ah no, that won't work with such a method name. So maybe,

float getResult(float min, float max);

thoughts?

getResult as a function name might be a bit misleading. Maybe getResultWithRange or getMappedResult or similar?

getResultWithRange sounds good to me.

I will implement this, but this will now take some days.

It's anyway better to use single shot mode for battery based projects. The ADS1115 will go into power down mode between the measurements.
I will go with that.

@wollewald instead of polling for changes, is it possible to get a trigger on a interrupt pin when one of the 4 channels changes? I prefer this 'async' way of working.

ps. before I started using this library I expected to get a 16-bit result, with ADS1115 being 16-bit and all. So a value between โˆ’32,768 (โˆ’1 ร— 215) through 32,767 (215 โˆ’ 1).

  1. instead of polling for changes, is it possible to get a trigger on a interrupt pin when one of the 4 channels changes? I prefer this 'async' way of working.:
    Would be nice if this worked. You have to switch the channels manually. There's only one channel active at a time. And there's only one register where the result is stored (conversion register). And this register is compared to the upper and lower threshold register.

  2. ps. before I started using this library I expected to get a 16-bit result, with ADS1115 being 16-bit and all. So a value between โˆ’32,768 (โˆ’1 ร— 215) through 32,767 (215 โˆ’ 1).:
    Most people are interested in the voltage and not the raw value. The raw value in the conversion register depends on the range. If you choose 0 - 256 mV as voltage range, +32,767 would be +256 mV. If you choose 0 - 512 mV as range, then +32,767 is 512 mV and so on. Most users are just interested in voltage and not raw values. My intention was to make it simple to use. This also applies for the alert threshold values, which you define in Volt and not in raw values. With my library you can change the voltage range and the values in the threshold registers for the will be recalculated. So all for convenience. I was missing this in other libraries and this was my motivation to write an own one.

@wollewald : Most users are just interested in voltage and not raw values
If I understood correctly, @thijstriemstra wants to map the input analog range to a "int" value in the [0-127] range. It could be done with a gain multiplied with the rawResult. I am going to use the ADC with a resistor divider in each input, and it would be interesting if i could pass the divisor gain to retrieve the real voltage value before the divider:

as in 30V --> 100k/3k3 divider --> ADC.

So if I use (100.0+3.3)/3.3 * getResult_V() i get the correct result.
The idea of implementinggetResultWithRange()is simplified to getResultWithGain(float gain):
getResultWithGain(103.3/3.3).
In @thijstriemstra case, he could use getResultWithGain(128.0*max_pot_voltage/selected_ADC_range);

I'm afraid of getting out of the scope of the library, but it would be nice to be able to preset the gain for each channel with something like setChannelGain(ADS1115_MUX mux, float gain)
What do you think?

If I understood correctly, @thijstriemstra wants to map the input analog range to a "int" value in the [0-127] range.

Correct. Or any range I specify.

I looked into this today, and my assumption about the 16-bit values also came from the Adafruit ADS1115 library, e.g. https://github.com/adafruit/Adafruit_ADS1X15/blob/master/Adafruit_ADS1015.cpp#L102 I messed around with the bitshifting << 8 but didn't get anywhere.

@thijstriemstra
The ADC is indeed 16bits (15bits+sign). But the functions getResult and getResult_mV are returning floats, and there is no point in bitshifting them.
I have already implemented a getResult_Raw() method, which returns the ADC register as is, with no translation, but I don't think it's ready for a pull request. You can go to your library folder and overwrite ADS1115_WE.cpp and ADS1115_WE.h with my files from here.

You have to check which range you are using for the ADC, and see the potentiometer output voltage range. I assuming that it is 0-3.3V you can use the 4.096V voltage range. In this case, 3.3V is going to be read as 32767/4.096*3.3 = 26399. You could then (to spare instructions) bitshift to the right by 7 bits (divide by 128) and your 0-3.3V range would be read as an integer range of 0-207.

All of this can be implemented in this library, if @wollewald thinks it suits his conception. I already implemented (but didn't commit, as it turned-out smelly) the getResultWithRange() idea. As I'm not a seasoned developer, I'll have to work on it a bit to make it more understandable.

@thijstriemstra
or you can use
int16_t midiChannel = (int16_t) (getResult_V() * 128 / VoltageRange )

thanks for the feedback @martinbra, I'll give that a try. ps. I'm using 5V.

I have added a function getResultWithRange. I have not yet made a new release out of it. Maybe you want to play with it. There's an example sketch called Experimental_getResultWithRange(). Maybe I explain with an example: If you choose ADS1115_RANGE_4096 as range, you apply a voltage of 4.096 Volt to the input channel and you use getResultWithRange(0,127) you will get 127 as result. If you choose ADS1115_RANGE_2048, apply 2.048 Volt and use again getResultWithRange(0,127) you will again get 127 as a result. -2.048 Volt would give -127. So you always need to keep in mind which range you apply.

A different option would be to reflect the range you are using. If you uncomment line 168 in ADS1115_WE.cpp

rawResult = (int) (rawResult * (voltageRange * 1.0 / 6144));

you will only get a 127 if you choose the range ADS1115_RANGE_6144 and 6.144 Volt (which by the way you shouldn't do). If you would choose ADS1115_RANGE_4096, and you apply 4.096 Volt getResultWithRange(0,127) would return 4096/6144 * 127 = 84 (I am using int).

I would prefer the first option. Would think it's helpful?

I also thought about the getResultWithGain function. I think this can confuse users, since this shall be used for an external gain if I have understood the proposal correctly. But there's also the internal gain, which depends on the range. At least I find this quite confusing.

@wollewald, I think that your first option fits better, but I will still play with it later. See what happens if i use getResultWithRange(50,100) and we have a negative raw value for instance...

You understood correctly, the idea of getResultWithGain is to compensate for external gains before the ADC1115. And yes, it can confuse users.
I think that there will be some users trying to read an ACS712 module output or with voltage dividers... and will have some issues with the external gain. Maybe I should write a library for such a case if there is none already written (input adc value as Raw or float, gain parameters and output real value). It's simple and handy, and could also have some basic filtering.

Just a thought that it might be confusing. And I think it shouldn't be an issue to very simply calculate the (external) gain factor withing the program and not inside the library. But I am not fundamentally against this idea. And basically not a big problem to add.

Interesting that you mention ACS712. That's exactly the topic of my next blog post. For this sensor the internal gain of the ADS1115 is absolutely sufficient. If you go down to few milliampere current the output signal of the ACS712 becomes the limiting factor because of noise / reproducibility and not the ADS1115.

thanks! I will give it a try today. ps. seeing all the " Add files via upload" commits in the repository makes me wonder if all modifications are done using the github web ui? It would be great for the code quality of the repository to properly describe commits so others can see what changed and why.

@wollewald

Interesting that you mention ACS712. That's exactly the topic of my next blog post. For this sensor the internal gain of the ADS1115 is absolutely sufficient. If you go down to few milliampere current the output signal of the ACS712 becomes the limiting factor because of noise / reproducibility and not the ADS1115.

Nice, looking forward to de-rust my german and read your post!

I know that the internal gain of the ADS is sufficient to read the voltage output of the ACS. But one still has to convert the output voltage to the "current" value. And yeah, this sensor is a walking noise generator (mostly because of the hall)... But with proper filtering and offset calibration it can make wonders. I am using one of this in a certified grid-tied photovoltaic inverter, and I am able to stay within the standard harmonic distortions.

@thijstriemstra
Just tried and It works, but you have to use the ADS1115_RANGE_6144 and your range should be adc.getResultWithRange(0, 157); Since the converter range will return its max scale for 6.144V in this case. (128 is for 5V as 157 is for 6.144V).
My two cents:

  1. use a multiturn potentiometer. The regular ones have a low "granularity" to set the desired value.
  2. Put a range like -10 to 160 and saturate the returned value to 0-127, so that you have a little margin at both ends of the potentiometer.

I tested it just now and it works nicely. The voltage going into the ADS1115 is not stable and never reaches the max of 6.144V so the potentiometer's max value is 104.00 with _adc->getResultWithRange(0, 127);. Measuring the max voltage using the ADS1115, e.g. getResult_mV(), shows:

5046.00
5040.00
5043.00
5037.00
5046.00
5043.00
5046.00
5043.00

So this is my max voltage range I have to work with. 5046 is 127.

Also, I would prefer that the method returns an int because it will never return a practical float anyway afaik.

@thijstriemstra: getResultWithRange(int16_t min, int16_t max) already returns an int16_t.
As you told me before, I was aware that you were using 5.0V. So when you use the ADS1115_RANGE_6144 it would convert 6.144V to 32767, but since your max voltage is 5.0V, your max output would be 32767*5.0/6.144 =26665. For your range to work, you have to use a range of 0-157. In this case, 157 would be for 6.144V (unreachable), but 5V would be 128.

midi = adc.getResultWithRange(0, (int16_t) (128.0*adsRangeVoltage/potentiometerOutputMaxVoltage))
midi = adc.getResultWithRange(0, (int16_t) (128.0*6.144/5.0))
midi = adc.getResultWithRange(0, (int16_t) (157.2864))
midi = adc.getResultWithRange(0, 157);

I did a similar range conversion (as I'm using 3.3V) and it worked as intended, returning an int between 0 and 127.

@thijstriemstra:
Honestly speaking I have not uploaded the sketches with the github web ui. Seems to be another thing I have to learn. Is there an easy introduction into it somewhere? At least I usually launch new releases with some (rough) release notes. This time I haven't done it because I just wanted you to test and wanted to avoid I launch too many releases. Is there any option to put a test version of the lib on github, parallel to the current one?
@thijstriemstra , @martinbra:
What I could is add a third parameter to the getResultWithRange function which is the maximum voltage. So if you work with max 5000 mV and the result range is 0 - 127 range you would take getResultWithRange(0,127,5000). The user would only have to take care that his max Voltage is covered by the voltage range, in this case 6144 mV. Or I add both options, any views?

Once I am clear which version of the getResultWithRange function will make it nicer with some comments in the ADS1115_WE.h.
@martinbra:
sounds interesting what you do with the ACS712. I will not go that deep in my next post but it could be a good hint for people who want to optimize that there are options to do that.

What I could is add a third parameter to the getResultWithRange function which is the maximum voltage.

This makes sense to me.

Honestly speaking I have not uploaded the sketches with the github web ui.

Oh I thought you did because the commit messages use the default github messages. I use vscode with platformio for development (highly recommended), and git on the commandline.

@wollewald :

What I could is add a third parameter to the getResultWithRange function which is the maximum voltage.

Well thought!

Or I add both options, any views?

I think that keeping both options is the way to go. If the user will use the full voltage range, there is one less parameter to pass, and if he limits the input voltage, its easier to understand what the numbers in the function call are. In my case, 157 was a "magic number" to get to the 128 and has no direct connection with what i was expecting to receive from the function. getResultWithRange(0,127,5000) is more understandable than getResultWithRange(0,157), but using getResultWithRange(0,127) when matching the voltage range is cleaner.

I think that the library could also provide a function with the register raw value (getResult_Raw()?), for the more advanced user, and a inline function to return the voltageRange value (getVoltageRange_mV()?). How do you see it being used?

sounds interesting what you do with the ACS712. I will not go that deep in my next post but it could be a good hint for people who want to optimize that there are options to do that.

The only thing is that I use a dedicated DSP with 10bits ADs instead of the ADS1115 (think about 16 channels being sampled 22000 times a second).

@martinbra, @thijstriemstra:

thanks for your input. Now I am clear how I will do it. I will implement the getResultWithRange function with two and three parameters and I think the raw result function is also a good idea as well as the getVoltageRange function. All not a big thing, but need some quiet hours for this. And then I will also put in some meaningful comments to explain.

Perfect, thanks a lot. Just a reminder to update the keywords.txt file to include the function names you will add.

Should work now. I had to change the getResultWithRange() function a bit. In order to get results like e.g. from the Arduino UNO with standard settings, you would use getResultWithRange(-1023,1023,5000) . I the previous version it was getResultWithRange(0,1023,5000). The previous version did not worked with negative voltages applied to the ADS1115 channels, but not with negative ranges.

awesome, thanks @wollewald I will test it somewhere next week and report back. Feel free to close the issue though. I enjoyed discussion we had here and looking forward to some more in the future.

thanks for your input @thijstriemstra, I think the library is in good shape now. I will close the issue now the issue now.

@martinbra, also thanks to you, great input.

thanks @wollewald and @thijstriemstra, I will try the modifications soon but the release looks solid!

I'm testing the new implementation now but I'm doing something wrong because I see unexpected results:

using _adc->getResultWithRange(_rangeMin, _rangeMax, _maxVoltage); where

int16_t max_voltage = 5000,
int16_t range_min = 0,
int16_t range_max = 127,
ADS1115_RANGE voltage_range = ADS1115_RANGE_6144

The 0 position of the encoder shows 76 and the maximum position is 139 (?).. I expected 0 and 127.

Output trace, rotating encoder from 0 to max:

144779 - Panel 1: potentiometer: 76
146628 - Panel 1: potentiometer: 77
146671 - Panel 1: potentiometer: 78
146693 - Panel 1: potentiometer: 79
146736 - Panel 1: potentiometer: 81
146758 - Panel 1: potentiometer: 82
146801 - Panel 1: potentiometer: 83
146823 - Panel 1: potentiometer: 84
146845 - Panel 1: potentiometer: 85
146888 - Panel 1: potentiometer: 87
146910 - Panel 1: potentiometer: 88
146932 - Panel 1: potentiometer: 89
146954 - Panel 1: potentiometer: 90
146997 - Panel 1: potentiometer: 92
147019 - Panel 1: potentiometer: 93
147041 - Panel 1: potentiometer: 94
147084 - Panel 1: potentiometer: 95
147106 - Panel 1: potentiometer: 96
147128 - Panel 1: potentiometer: 98
147150 - Panel 1: potentiometer: 99
147172 - Panel 1: potentiometer: 100
147194 - Panel 1: potentiometer: 101
147216 - Panel 1: potentiometer: 102
147238 - Panel 1: potentiometer: 104
147260 - Panel 1: potentiometer: 105
147282 - Panel 1: potentiometer: 106
147304 - Panel 1: potentiometer: 107
147326 - Panel 1: potentiometer: 109
147369 - Panel 1: potentiometer: 110
147391 - Panel 1: potentiometer: 111
147434 - Panel 1: potentiometer: 112
147456 - Panel 1: potentiometer: 113
147499 - Panel 1: potentiometer: 115
147521 - Panel 1: potentiometer: 116
147564 - Panel 1: potentiometer: 117
147586 - Panel 1: potentiometer: 118
147629 - Panel 1: potentiometer: 119
147651 - Panel 1: potentiometer: 121
147673 - Panel 1: potentiometer: 122
147695 - Panel 1: potentiometer: 124
147717 - Panel 1: potentiometer: 125
147739 - Panel 1: potentiometer: 128
147761 - Panel 1: potentiometer: 130
147783 - Panel 1: potentiometer: 133
147805 - Panel 1: potentiometer: 134
147827 - Panel 1: potentiometer: 136
147849 - Panel 1: potentiometer: 138
147871 - Panel 1: potentiometer: 139

Even if you want to use a range of 0....127, you need to use -127....+127 as _rangeMin and _rangeMax. In my first experimental version you could use 0...127. For this experimental version I used internally in the ADS1115_WE.cpp the following function:

result = map(rawResult, 0, 32767, min, max);

for mapping. But then I noticed that this does not with negative ranges, e.g. -127...+127

In the newer version I used therefore:

result = map(rawResult, -32767, 32767, min, max);

And in this case you need to use -127...127 as range. I have to admit that this is confusing. And I thought quite while what is the best way.

And in this case you need to use -127...127 as range. I have to admit that this is confusing. And I thought quite while what is the best way.

Ah that makes sense in a way.. but the example could be clarified (with the 0-127 use-case maybe?). I will give it a try.

I think specifying the minimum should also respect the minimum. If I would want to measure negative voltages I would go for -127 till 127. what are your thoughts @martinbra?

I thought a fourth parameter, something like bool includeNegativeResults but this could also be handled by the first min parameter, or it'll become even more confusing..

I tried to explain this in the example sketch:

  • in order to get results equivalent to an Arduino UNO (10 bit, 5000 mV range), you
  • would choose getResultWithRange(-1023, 1023, 5000). A difference to the Arduino
  • UNO is that you can measure negative voltages.

Will think about the fourth parameter.

Oh man, I'm afraid of going down a rabbit hole now... before @wollewald provided his getResultWithRange() function I had written mine, and the result was the same as his and had to use -127,127 ... I had written the map() function without knowing it was available on the arduino platform.

Mathematically speaking, to set the map function we need any two points, given that we provide the correspondent points on the other scale. It can be min and max, or zero and max, as in, what a zero and max voltage in the input means to ones code.

I think either concepts works better for their intended use, and will be a bit confusing if used in the other way:

  1. getResultWithRange1(zero, max) is good when one is reading Channel to ground.
  2. getResultWithRange2(min, max) is good when one is reading Channel to Channel.

but...

when one is reading channel to channel, the purpose is usually to be a symmetrical reading, and using the zero-max range is not too strange. If one wants to map let's say -4.096V~+4.096V to (int) -1000~+1000, one would use getResultWithRange1(0,1000) and to map it to (int) 0~+1000, one would use getResultWithRange1(500,1000).. But, but, ok. It's strange.

What about getUnsignedResultWithRange(zero, max) and getSignedResultWithRange(min, max) ? Smelly names, since the return should always be signed in this case, because even for Ch-to-Gnd the ADS1115 can read negative values (if channel voltage is slightly under gnd.

Or we can keep getResultWithRange(min, max) as is for the current implementation and create getResultWithRange(min_zero, max, includeNegativeResults) as @thijstriemstra suggested (I'm sorry, I think that the name includeNegativeResults would be better named as includeNegativeRange or firstPointIsZeroReference.

I would suggest a step further: that we create a enumeration for the modes:

typedef enum ADS1115_RANGE_MODE {
	ADS1115_RANGE_MINMAX,
	ADS1115_RANGE_ZEROMAX,
	ADS1115_RANGE_AUTO
} rangeMode_t;

and as a pseudo code, the function would look like the following, with an auto mode detection based on last used mux configuration (as a private static variable to the library):

int16_t ADS1115_WE::getResultWithRange(int16_t min_zero, int16_t max, int16_t maxMillivolt, rangeMode_t mode){
	int16_t min = 0; //only needed for code clarity (we could reuse min_zero...)
	
	// detect mode by the expense of keeping track of last mux used in setCompareChannels()
	if (mode == ADS1115_RANGE_AUTO){
		if (mux >= ADS1115_COMP_0_GND){
			mode = ADS1115_RANGE_ZEROMAX; //sampling channel to GND
		}
		else {
			mode = ADS1115_RANGE_MINMAX; //sampling between two channels
		}
	}
	
	if (mode == ADS1115_RANGE_MINMAX){
		min = min_zero;
	}
	else if (mode == ADS1115_RANGE_ZEROMAX){
		// calculate min so that "minzero = (min+max)/2" (is exactly beetween min and max).
		min = min_zero - (max - minzero); // be carefull with sum order to avoid 16bit rollover.
	}
	return getResultWithRange(min,max,maxMillivolt)	
} 

and a rabbit hole it was.

Quick summary:

  • It will looks strange for either concepts, when using one implementation to read the other concept.
  • The current implementation is in my opinion, the clearest we could get for the "challenge". We receive a signed int even if we are reading only positive voltages, and should translate it to a signed range, thus, providing both ends, and not zero/middle and positive. It requires that the user understand this. The other way around would also requires some tinkering.
  • With this in mind, I believe that a "mode" selection or a firstPointIsZeroReference flag would fit well both cases.

So, what are your thoughts on this idea?

Oh wow! All good thoughts. Will think about this.