In Blog 8, I mentioned some of the key difficulties in handling multiple analog signals such as those generated by thermistors and while working with the Arduino MKR WiFi 1010 to produce the results in Blog 9 was quite instructive, I felt that it would be good to address the ESP8266 issue especially as it’s something ntewinkel had raised in a blog post. In a comment, robogary mentioned that thermistor power was multiplexed as well, which led to a bit of a brainwave in how I might be able to multiplex thermistors for the ESP8266 using minimal parts, albeit with limitations. While it’s nice to have thought experiments, it’s always nicer to turn them into reality. As a result, I decided to pursue the question right to the very end, by implementing a multi-thermistor measurement system using an Adafruit Feather Huzzah (ESP8266-based) development board – going slightly off-track just to satisfy my curiosity.
Table of Contents
The Challenge
As previously noted, the ESP8266 has just a single ADC input, with a 10-bit resolution (theoretically) and a 0-1V voltage range. This is not particularly friendly in the case of multiple analog inputs, especially if powered from the 3.3V voltage rails, as it requires not only multiplexing but also scaling. The theoretical resolution is known to be difficult to achieve in practice, as high-level Wi-Fi power consumption spikes can cause power integrity issues that result in noisy readings.
The proper way would be to use a multiplexing chip as previously indicated, but perhaps there is a way to avoid the need entirely …
The Hacky Idea
A hacky idea came to mind when I was reminded that thermistor voltages are multiplexed – normally I would consider such a solution to be expensive to implement, but perhaps we could make things work by “abusing” the microcontroller in some way.
This is when I realised that setting a GPIO to “INPUT” mode means that it would have a high impedance (usually >1MΩ) which would mean it is almost open-circuit as compared to the resistances of the thermistors. Setting the GPIO to “OUTPUT” mode and writing a “HIGH” will cause it to source the Vcc rail, although potentially not very accurately depending on the current load. In the case of the thermistors, we are dealing with a fairly high resistance – if I choose a divider resistor of 10kΩ, even if the thermistor was shorted, the current draw at 3.3V could only be 0.33mA which should be well within the capabilities of the GPIO and low enough to maintain near-perfect rail voltage (in theory).
Therefore, we could have thermistors powered by GPIOs, but sharing a single divider resistor to ground. The multiplexing would be done just by toggling GPIO mode and output, with no multiplexer chip required. Unfortunately, as there is a voltage scaling requirement, taking the output and putting it into a voltage divider directly risks errors due to input resistances and the high resistance would also lead to additional ADC noise. As a result, a voltage-follower opamp is something I recommend, the output which could then be divided down into the ADC.
The final schematic looks like this, doing a very similar experiment to that of the MKR WiFi 1010, however just with four thermistors (4.7kΩ, 5kΩ, 10kΩ and 12kΩ) with a 10kΩ shared divider resistor. An MCP6273E rail-to-rail op-amp from the junk box was used, along with a 56kΩ/22kΩ divider. Lower resistances could be used, but I didn’t have the right ratios – the idea is that 3.3V must be scaled down to less than 1V (to avoid saturation). The above combination theoretically scales it down to approximately 0.930769V.
I decided to tear apart the breadboard with the MKR WiFi 1010 and rebuild it for the ESP8266 experiment -
The resulting construction is very similar, retaining the use of the same two BMP280 sensors as a reference point (even though they aren’t very accurate, as previously determined). The board was powered from a spare USB charger.
To drive this, I used the following code – again 4096 sample averaging is used to make it directly comparable to the MKR WiFi 1010 results, but this one does have a number of debug prints remaining. I did not strip them out, as I find changing the code for the ESP8266 could lead to unexpected failures in my experience. When hooked up to a power supply, the prints go nowhere and cause no harm. I also configured GPIO0 to give a short 100ms blip every time the packet is sent as a liveliness indication, as early attempts had the code crashing frequently (or dropping off Wi-Fi) in spite of plenty of scattered delay() functions.
// element14 Experimenting with Thermistors Design Challenge // Four Thermistors + Two BMP280 Sensors via Wi-Fi UDP // For ESP8266 using Digital Inputs Hack // by Gough Lui (goughlui.com) - Aug 2022 // No warranties provided or implied. #include <Wire.h> #include <ESP8266WiFi.h> #include <WiFiUdp.h> #include <Adafruit_BMP280.h> unsigned int localPort = 19999; char packetBuffer[512]; WiFiUDP Udp; unsigned int thermistorvals[4]; const byte pins[] = {12,13,14,16}; Adafruit_BMP280 bmp; Adafruit_BMP280 bmp2; void resetThermistors() { for (int i=0;i<4;i++) { pinMode(pins[i],INPUT); delay(0); } } void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); WiFi.begin("YOUR_SSID_HERE", "YOUR_PWD_HERE"); // YOUR WIFI HERE while(WiFi.status() != WL_CONNECTED) { //Serial.println("Waiting for Wi-Fi "); delay(500); } WiFi.persistent(true); WiFi.setAutoReconnect(true); Udp.begin(localPort); while (!bmp.begin()) { Serial.println("Could not find BMP280 at default address!"); delay(5000); } bmp.setSampling(Adafruit_BMP280::MODE_NORMAL, /* Operating Mode. */ Adafruit_BMP280::SAMPLING_X16, /* Temp. oversampling */ Adafruit_BMP280::SAMPLING_X16, /* Pressure oversampling */ Adafruit_BMP280::FILTER_X16, /* Filtering. */ Adafruit_BMP280::STANDBY_MS_500); /* Standby time. */ while (!bmp2.begin(BMP280_ADDRESS_ALT)) { Serial.println("Could not find BMP280 at secondary address!"); delay(5000); } bmp2.setSampling(Adafruit_BMP280::MODE_NORMAL, /* Operating Mode. */ Adafruit_BMP280::SAMPLING_X16, /* Temp. oversampling */ Adafruit_BMP280::SAMPLING_X16, /* Pressure oversampling */ Adafruit_BMP280::FILTER_X16, /* Filtering. */ Adafruit_BMP280::STANDBY_MS_500); /* Standby time. */ resetThermistors(); pinMode(0,OUTPUT); digitalWrite(0,HIGH); // LED Output } void loop() { for (int i=0;i<4;i++) { thermistorvals[i]=0; } delay(1); for (int i=0;i<4;i++) { resetThermistors(); pinMode(pins[i],OUTPUT); digitalWrite(pins[i],HIGH); delay(1); // Settling Time for (int j=0;j<4096;j++) { thermistorvals[i]+=analogRead(A0); delay(0); } digitalWrite(pins[i],LOW); delay(1); // Settling Time } Udp.beginPacket("xxx.xxx.xxx.xxx", 19999); // YOUR IP ADDRESS HERE int msglen = sprintf(packetBuffer,"TM,%d,%d,%d,%d,%f,%f#\r\n",thermistorvals[0],thermistorvals[1],thermistorvals[2],thermistorvals[3],bmp.readTemperature(),bmp2.readTemperature()); Udp.write(packetBuffer,msglen); Udp.endPacket(); Serial.print(packetBuffer); digitalWrite(0,LOW); delay(100); digitalWrite(0,HIGH); }
Hopeful Outcomes …
Through this experiment, I hope to determine whether using digital outputs to drive thermistors is going to cause issues (e.g. excessive noise, excessive voltage drop). The use of a single shared divider resistor which is non-ideal for some thermistors would be able to show just how important the divider resistor is in such a system. Furthermore, the ESP8266 has a 10-bit ADC which is famous for not being too quiet (especially when Wi-Fi is used, as is the case here), so it will be interesting to see if the averaging is good enough to achieve comparable accuracy to the MKR WiFi 1010. Finally, the ESP8266 is a bit of a “cranky” board to work with, especially if you distract it for too long, so this will be some fun to work.
Bumps Along the Road
A close look at the schematic shows that I’ve made some mark-ups regarding the pins. Initially, I had opted to use pins marked 12-15 but then I noticed the result for pin 15 was not correct. While the ESP8266 is known to have limited I/O, it turns out the I/O can also suffer limitations due to the implementation of the development board and auto-reset circuitry.
An excerpt from the schematic for the Adafruit Feather Huzzah shows that GPIO15 has a pull-down to ground, thus cannot be used. Conversely, GPIO2 has a pull-up to Vcc which also means it cannot be used for our purposes. Finally, GPIO0 is a bit ambiguous as it has a bit of a network connected to it for the auto-reset circuitry to set the mode. If this is interfered with, then upload of code to the board may not be possible automatically or the board will always be stuck in bootloader mode. To avoid leakage and potential trouble, I decided to also ignore this pin.
As a result, we are left only with four digital I/O pins we can use for this trick. This is an interesting coincidence – but perhaps if we wanted to expand this particular hack, using an I2C I/O Expander IC (e.g. the PCF8574, MCP23017, etc.) would easily add 8/16 digital channels for this kind of work.
Results
To ensure the correct interpretation of results, I measured the scaling voltage divider resistances as 55,700Ω and 21,940Ω according to my Keithley 2110 5.5-digit digital multimeter. This means the actual divider ratio results in a full-scale voltage of 0.932535V. The shared thermistor divider resistance was measured as 9,947.8Ω. Any offsets introduced by the opamp and ADC were considered negligible and were not measured or corrected for. Furthermore, any deviation from the “ideal” 3.3V reference coming from the GPIO pins was not accounted for as well for simplicity. This reflects the most “practical” circumstance of a straightforward (lazy) implementation.
Monitoring was performed over two-and-a-half days, comprising a total of 127,167 data-points recorded. The lower number is, in part, due to the speed of the analogRead() implementation on the ESP8266 which runs at about 10kHz. Thermistor resistances were converted to temperature using a fifth-order model (as per previous blog posts) for maximum accuracy.
The BMP280 measured the temperature throughout the experiment which ranged around 14°C to 20°C, with the initial temperature up to 22.3°C possibly due to handling the board. The temperature range is different, due to the influence of weather.
Temperature differences between BMP280 sensors averaged around 1°C which is very slightly higher than in the MKR WiFi 1010 experiment, but may also be due to a subtle change in position on the breadboard. It does suggest that some of the temperature bias exists due to sensor-to-sensor variation (e.g. due to manufacturing, handling, soldering, etc) although positioning effects cannot be ruled out.
Looking at the raw temperature traces shows us a few important things. Firstly, the 10kΩ and 12kΩ traces agree very well – suggesting the model and choice of divider resistor (10kΩ) was close to optimal. The 4k7Ω and 5kΩ traces, even though they are closer to the BMP280 average, exhibit significantly higher levels of noise and “jumps” of 1-2°C which are indicative of ADC non-linearities which averaging and “natural” circuit noise could not eliminate. This latter part is something michaelkellett had warned me about in a comment, so it was heartening to confirm it visually. The noise and jumps are also in part due to the sub-optimal divider resistor which would have led to low temperature resolution, as a single ADC step corresponds to a large fraction of a degree Celsius, so a smooth curve could only exist if we could reliably produce results at the sub-ADC-step level.
Dirunal variations in measured temperatures were similar, indicating there is a bit of a bias in temperatures of daytime over night-time. A hypothesis may be that the black/dark coloured epoxy beads may absorb more radiation than the silver-coloured canned BMP280 packages, causing slight temperature variations. The difference in package height may also contribute to this observed effect.
By plotting a histogram, the effects of low ADC resolution can be seen (noting this is the result after a 4096-sample average) manifesting as two peaks in the temperature difference distribution. The 10kΩ and 12kΩ results are fairly good, forming a single peak with a smaller shoulder, however the “base” of the peak still spans about 1°C which may be partly due to diurnal temperature differences and its absolute offset is about 2.5°C. This suggests that ADC noise may still a factor in making noisier temperature measurements, but also that the absolute offset may arise due to not compensating for ADC/opamp offsets and GPIO output voltage errors. For some applications, this is still very much sufficient.
Conclusion
In the end, with some care taken to understand the ESP8266 board’s design, it is possible to perform a “hack” to use digital I/O pins to drive thermistors and use a shared divider resistor to avoid the need for a multiplexing IC for multiple thermistor input.
The use of identically-valued thermistors is probably advisable to avoid resolution issues due to non-ideal divider resistor values. Such a set-up is also limited with the number of pins available as board designs may include pull-up/pull-downs on some pins. An I/O expander could be abused to increase the number of potential thermistors multiplexed, but the use of a proper multiplexing IC would eliminate such constraints but add to cost and complexity. This technique adds some uncertainty in the form of an imprecise supply voltage from the digital I/O pins and potential offsets occurring in the opamp buffer and ADC itself. Additional scaling resistors may also be vulnerable to temperature coefficient drift.
The 10-bit ADC of the ESP8266 was not as good as the 12-bit ADC of the MKR WiFi 1010, as expected, and deficiencies which could not be addressed through averaging started to appear, resulting in large steps in the result for the 4k7Ω and 5kΩ thermistor results which used a sub-optimal shared divider resistance of 10kΩ.
In the end, results did show about 2.5°C offset and 1°C spread as compared to the average of both BMP280 sensors, neither of which are highly accurate. The result is sufficient for many applications and could be improved if some of the uncertainties and offsets were measured.
Raw sample data from the multi-day experiment is also available for download as a zipped binary Excel workbook (due to the size) - ed8266thlog2_noformulas.zip
[[Characterising Thermistors Blog Index]]
- Blog #1: Characterising Thermistors - Introduction
- Blog #2: Characterising Thermistors - What's In The Box?
- Blog #3: Characterising Thermistors – A Quick Primer, Beta Value & Steinhart-Hart Coefficients
- Blog #4: Characterising Thermistors – An Inconvenient Truth, Taking Things to the Fifth Degree
- Blog #5: Characterising Thermistors – Measuring Resistance Is Not So Easy!
- Blog #6: Characterising Thermistors – Is Self-Heating a Problem or Not?
- Blog #7: Characterising Thermistors – Boiling, Freezing and Zapping the Truth Out of Them!
- Blog #8: Characterising Thermistors – Practically Running Multiple Thermistors
- Blog #9: Characterising Thermistors – Multi-T Results, Insulation R Redux, 5th Order Fits & Model Performance
- Blog #10: Characterising Thermistors – Multiple Thermistors on ESP8266
- Blog #11: Characterising Thermistors – Show Me Your Curves
- Blog #12: Characterising Thermistors – Sticking Rings on Tabs & Sinks, Absolutely Crushing It!
- Blog #13: Characterising Thermistors – Pulling Out, Overload, Response Time, Building a Flow Meter & Final Conclusion