Странные мысли
Странные мысли о неглавном
Tuesday, February 27, 2018
USB charger for Nickel-Cadmium 5V battery pack
Disclaimer: I am an electronics amateur so if you see any major problems or have a better circuit please let me know.
The battery will power Atmega328 with LCD display + some additional circuitry . Overall current consumption in my case does not go above 100mA My battery pack is 4x 1.2 batteries in series. The voltage on the pack starts from 5.4V fully charged. At ~4.3V the display is not readable, so I will take this as the lower bound. In my case the powered circuit needs relatively low constant current it is possible just to connect the battery to Vcc and not to mess with stabilization.
I wanted something simple with the following basic properties:
1. Charged from any USB, provided it can supply enough power.
2. The powered circuit shall work from battery when unplugged
3. When USB charger is connected the circuit is powered from it exclusively, transition shall be seamless.
4. Once USB charger is plugged in, the battery is unplugged from the rest of the circuit and is charged. Nickel Cadmium requires constant current for charging, more about that below.
Here is my final circuit. The rest of this blog is explanation on its inner working. The KiCad project is as usually on github.
The following diagram provides the idea behind the circuit:
MC34063 is a driver with almost standard boost converter setup that is used to charge the battery. The difference between standard setup and this one in pin 5 which is not connected to output, but to the output of unit differential amplifier based on OPA237.
The differential amplifier calculates voltage drop on shunt resistor R8 which shall be 1.25V for 60mA passing over R8. The 60mA is a low speed charging current, specific to my battery pack and specified on the battery by the manufacturer.
To be fully charged my battery must receive 60mA constant current during 16-24 hours. There is fast charge option, when the battery receives higher current for less time, but this requires additional monitoring circuitry since battery may overheat. Thus, I used safer and simpler low speed charging approach.
Pin 5 on MC34063 is connected to internal comparator with voltage reference of 1.25V. When voltage on Pin 5 is higher than 1,25V the boost controller opens its switch allowing the C7 to discharge a little bit. When it is lower than 1..25V the switch is closed and C7 is charged again.
As you can see, boost converter paired with differential amplifier and shunt resistor plays a simple current source used to charge the battery.
Q3 MOSFET pair is set as normally open switch controlled by the VCHARGE signal. It connects the current source to the external charger when it is connected. Q1 is simply P-channel MSOFET acting as a normally closed switch controlled by VCHARGE. It disconnects the battery from the rest of the circuit when the external charger is connected.
The D1 diode has two purposes.
1. It protects the battery from discharging into connected but not active external charger (I am used to transport my devices with USB plugged in, while the charger apparently not in AC)
2. Without it battery voltage would serve as VCHARGE signal for Q1 and Q3 which is undesirable.
The C1 is used as an extra protection from ringings when plugging the USB charger and smooth transition from charger to battery and vise versa.
Power throughput of the circuit is limited by D1 when on external charger and by Q1 when on battery.
There are few drawbacks that I should mention:
1. The voltage drop and current limit of D1. Schottky diode may be used to reduce the voltage drop.
2. In some situations I have used this circuit with battery unplugged. It works, but there is one apparent issue: Q1 will be half open conducting 60mA from the current source. Not very efficient and may damage Q1 if not suitable for the current.
Friday, January 13, 2017
Simple electronic thermometer using STM32F030 and thermocouple. Part 1.
It has been a while since I blogged about my little thermometer project. Meanwhile I did some progress, but as usually put it aside because more urgent matters. Newertheless, lets try to continue.
I will try to split my progress into a number of smaller blogs, starting from basics and hoping to get some working results.
Thermocouples¶
As defined in wikipedia:
A thermocouple is an electrical device consisting of two dissimilar conductors forming electrical junctions at differing temperatures. A thermocouple produces a temperature-dependent voltage as a result of the thermoelectric effect, and this voltage can be interpreted to measure temperature. Thermocouples are a widely used type of temperature sensor.
So theoretically my digital thermometer shall perform a simple algorithm:
- Measure voltage value provided by thermocouple
- Take an empiric curve provided by NIST and do reverse lookup (from Volts to degrees Celsius) to figure out the temperature.
Not that easy!
Cold Junction Compensation¶
The web contains great number of explanations about the cold junction problem. For example the TI guide provides some insight.
Basically, thermocouple has two ends. One end is used to measure the target temperature it is called the Hot Junction. The other end is normally connected to a thermometer and is called the Cold Junction. The problem is that the Cold Junction forms kind of parasitic thermocouple with leads connected to the thermometer. Since the thermometer itself has non-zero temperature it provides distortion of the Hot Junction measurements.
The empiric curves provided by NIST assume the Cold Junction at temperature zero degrees Celsius. Thus, some action is needed to reverse the distortion provided by the Cold Junction.
Normally the problem solved by putting temperature sensor near the thermocouple connection. I will use a thermistor (see my previous blog) to figure out the temperature of the Cold Junction.
Now my theoretical thermometer's algorithm is more complicated:
- Measure voltage value provided by thermocouple. Lets call it $V_{therm}$
- Measure temperature at Cold Junction provided by thermistor. Lets call it $T_{CJ}$
- Use NIST curves to lookup distortion voltage corresponding to $T_{CJ}$. I will designate this value as $V_{CJ} = V_K(T_{CJ})$. Here $V_K()$ is an approximation of the Celsius to Volts curve for K-Type thermocouple. For other types of thermocouples it is same calculation but curves are different.
- Now it is possible to calculate the undistorted voltage at Hot Junction: $V_{HJ} = V_{therm} - V_{CJ}$. The same value we would measure with Cold Junction at zero degrees Celsius.
- Use reverse function $V^{-1}_K()$ to figure out temperature at Hot Junction: $T_{HJ} = V^{-1}_K(V_{HJ}) = V^{-1}_K(V_{therm} - V_{CJ}) = V^{-1}_K(V_{therm} - V_K(T_{CJ}))$
Better theoretically, but there are still couple of technical problems... More will come.
Friday, September 23, 2016
Making PCBs at home
I will not describe my process here, but my reference is a set of amazing videos by Dmitry Dementiev:
https://www.youtube.com/watch?v=hKJiR35kjpI
https://www.youtube.com/watch?v=El_O8IMgWUk
https://www.youtube.com/watch?v=yPS-ldzMQRI
It is all in Russian. There are similar English videos on youtube, but I followed Dimitry's since he describes things really well. If you don't understand Russian and still want to know how to do it - let me know, I will try to do some translation.
Anyway, here is my first home made PCB.
UV-Timer PCB immediately after etching |
UV-Timer PCB with photoresist removed |
It is a modified version of Dmitry's timer for UV LED lamp.
Here is my schematics:
- I have put another MOSFET and controlling resistors
- I removed controlling buttons. All control is done from rotary encoder
- I have wrote the firmware from scratch. Just for fun.
Board fully populated |
Bottom side with a few patches for a first version (fixed on github) |
Fully populated board |
Very first working version of firmware |
Thursday, August 18, 2016
Atmel-ICE Durability Upgrade
- It has 2x5 1.27mm pitch connectors for AVR and SAM chips. Atmel supplies connector cables if you buy full package but those are too fragile for my taste. Also they are lacking standard 2x3 and 2x5 2.57mm connectors found on most Arduinos.
- The USB connector is simply soldered on the board. No additional mechanical strengthening. Connector on my device at work has been broken off after an year of careful use.
Saturday, August 13, 2016
Tip Of the Day: Converting IPython notebook into a blogger post
- Start from instructions in this blog.
- If you need your markdown Latex and other formula to work, continue to this stackexchange answer.
Messing with Thermistors
Note: This is a IPython notebook that I converted to HTML especially for blogger. The original is on thermo github repository.
Intro
I want my electronic thermometer to measure temperatures higher than 300C. This basically means that I can't use a single thermistor or temperature sensor (e.g LM35). Those are limited to 200-300C max.I will use a cheap thermocouple. It will probably be less precise, but it is OK for my needs. The question is how to deal with cold junction compensation?
The simplest solution would be using IC like MAX6675. But my goal is not a simplest solution, but learning electronics and STM32 programming. I can always revert to MAX6675 if my own solution fails.
The hard way requires thermocouple voltage amplifier and cold junction compensation. I will leave thermocouple amplification for another blog. For now I will concentrate on a relatively simple problem of obtaining cold junction temperature.
The idea came from EEVBlog #419 where Dave taking apart the Fluke thermometer. That puppy has two massive thermocouple terminals with thermistor between them.
Further dealing with thermocouples is a subject for another blog, but for now I will deal with the thermistor.
Thermistors
Thermistor is a resistor that changes its resistance depending on temperature. Thermistors are cheap and come in different flavors of size, precision, resistance, etc...Important Note: When buying on Ebay ensure that seller provides resistance ($R_0$) at known temperature ($T_0$) and Beta value ($B $). I didn't paid attention when I bought my, but I was lucky enough to find that info in the order information and the values were suitable for my project.I have got $R_0=100KOhm$, $T_0=25C$ and $B=3950$. And the tolerance is 5%.
Also, another important characteristic is the tolerance.
Connecting to MCU
The next question was how do I connect that thing to my MCU? The idea came again from Adafruit learning site. Schematics is very simple:Here the COLDJT is directly connected to ADC_IN0 on MCU.
The COLDJT voltage is always between ground and $V_{cc}$ thus it always fits ADC range accepted by STM32F030. But will it be wide enough to have enough precision?
This depends on values of TP1 and R1.
Thermistor Resistance Range
Note: All (or most) the python code below is from thermo github repositoryFirst of all lets try to figure out thermistor resistance range. My thermistor will be used for measuring of thermocouple cold junction. It will be mounted inside thermometer box and used almost always indoor. Thus, it is more than safe to assume its target temperature range is from -20C to 70C.
From the Wikipedia article on thermistor:
$R=r_{\infty}e^{B\over{T}}$, where $r_{\infty}=R_0e^{-B\over{T_0}}$
Here $R_0$ is a resistance of the thermistor for temperature $T_0$ and $B$ is its beta value. Lets plot it
%pylab
%matplotlib inline
Using matplotlib backend: TkAgg Populating the interactive namespace from numpy and matplotlib
class Thermistor(object):
def __init__(self, r0=100000, beta=3950, t0=298.15):
self.r0 = r0 # Ohm
self.beta = beta
self.t0 = t0 # Kelvin
self.r_inf = self.r0 * numpy.e ** -(self.beta / self.t0)
def resistance(self, temp):
"""
Calculate resistance of a thermistor based on temperature in Kelvin
"""
return self.r_inf * (numpy.e ** (self.beta/temp))
T = np.arange(273.15-20, 273.15+70, 0.05)
thermistor = Thermistor()
temperature = np.vectorize(thermistor.resistance)
plt.plot(T, temperature(T))
print 'Maximal resistance: {}'.format(thermistor.resistance(273.15-20))
print 'Minimal resistance: {}'.format(thermistor.resistance(273.15+70))
Maximal resistance: 1053846.90206 Minimal resistance: 17598.3700852
class Sense(object):
def __init__(self,
vdd=3.3,
thermo_resistance_start=17598,
thermo_resistance_stop=1053846):
self.thermo_range = [thermo_resistance_start,
thermo_resistance_stop]
self.vdd = vdd
def voltage(self, sense, thermo):
"""
Calculate voltage on thermistor/sense resistor junction based on
their resistances
"""
return float(sense)/(sense+thermo) * self.vdd
def delta(self, sense):
"""
Calculate thermistor/sense voltage range depending on
sense resistance and required thermistor resistance ranges
"""
return abs(self.voltage(sense, self.thermo_range[0]) - \
self.voltage(sense, self.thermo_range[1]))
def optimum_sense(self, start, stop, step):
"""
Find sense resistance in specified interval that leads to a
maximum voltage range in thermistor/sense junction for specified
thermistor resistance ranges.
"""
sense_values = np.arange(start, stop, step)
delta = np.vectorize(self.delta)
deltas = delta(sense_values)
plt.plot(sense_values, deltas)
sense = sense_values[numpy.argmax(deltas)]
return (sense, [self.voltage(sense, t) for t in self.thermo_range])
sense = Sense()
(optimum_sense, vcjt_range) = sense.optimum_sense(10000, 300000, 1000)
print 'Optimum R1 value and corresponding voltage range {}, {}'.format(optimum_sense, vcjt_range)
Optimum R1 value and corresponding voltage range 136000, [2.921913045742783, 0.3771916701825278]
The graph above shows dependency of that difference on different values of R1. We have a single R1 value of 136KOhm that corresponds to the widest $V_{CJT}$ range 0.337V to 2.922V. Lets see if this is useful.
Does my schematic do what I want?
STM32F030 has 12-bit ADC. This means that $V_{CJT}$ will be sampled and compared with $V_{dd}$ and software will obtain a value in "ADC Clicks", lets call it $S_{CJT}$, it will have values in range [0..0xfff] where 0 corresoponds to $V_{CJT}=0$ and 0xfff corresponds to $V_{CJT}=3.3V$ or above: $$S_{CJT}=\lceil{V_{CJT}\over{V_{dd}}}0xfff\rceil$$Given that it is easy to calculate the range of $S_{CJT}$ values given the voltage range:
width = vcjt_range[0] - vcjt_range[1]
print 'Range width: {}'.format(width)
adc_width = int(width / 3.3 * 0xfff)
print 'Range width in ADC clicks: {}'.format(adc_width)
clicks_per_degree = adc_width / (70 - (-20))
print 'ADC clicks per degree Celsius: {}'.format(clicks_per_degree)
Range width: 2.54472137556 Range width in ADC clicks: 3157 ADC clicks per degree Celsius: 35
Good to go!
Converting to temperature units
The remaining question is how do I convert the ADC clicks into actual temperature value in Kelvin or Celsius?Lets make some schematic analysis.
$R_{TR1}={{V_{dd}-V_{CJT}}\over{I}}$, where $I$ is a current flowing over TR1 and R1
$I={{V_{CJT}}\over{R_{R1}}}$
Substituting $I$ in the first equation gives us $$R_{TR1}={R_{R1}\big({V_{dd}\over{V_{CJT}}}-1\big)}$$
Again from the Wikipedia article on thermistor: $$T={B\over{\ln{\big({R_{TR1}\over{r_{\infty}}}\big)}}}$$
Substituting the $R_{TR1}$ formula: $$T={B\over{\ln{\big({R_{R1}\over{r_{\infty}}}\big({V_{dd}\over{V_{CJT}}}-1\big)\big)}}}$$
Substituting the $S_{CJT}$ formula we get: $$T={B\over{\ln{\big({R_{R1}\over{r_{\infty}}}\big({0xfff\over{S_{CJT}}}-1\big)\big)}}}$$
Note: The resulting temperature is in Kelvin. Converting to Celsius is trivial.
The good news is that $V_{dd}$ magically disappeared from the equation. This means that the chip will compensate $V_{dd}$ fluctuations and there is no need to do that in software.
The bad news is that the software will need to do that $\ln()$ stuff.
But wait, my STM32F030 does not have FPU!
I can go Adafruit path and rely on compiler to insert floating point simulation code. But something in me screams "No Way!!!".
Luckily the solution is simpler than expected.
Throwing out floating point
Lets precalculate the function and put on chip its linear approximation.%matplotlib inline
# Ramer-Douglas-Peucker Algorithm implementation from
# git@github.com:fhirschmann/rdp.git
from rdp import rdp
class Thermistor(object):
def __init__(self, r0=100000, beta=3950, t0=298.15, r_sense=136000):
self.r0 = r0 # Ohm
self.beta = beta
self.t0 = t0 # Kelvin
self.r_sense = r_sense # Ohm
self.r_inf = self.r0 * numpy.e ** -(self.beta / self.t0)
def temp(self, v_adc):
"""
Accept ADC value from thermistor connected to ADC_IN0.
Return temperature in Kelvin
"""
return self.beta / numpy.log(
self.r_sense / self.r_inf * (float(0xfff) / v_adc - 1)
)
def resistance(self, temp):
"""
Calculate resistance of a thermistor based on temperature
"""
return self.r_inf * (numpy.e ** (self.beta/temp))
def temp_approximation(self, adc_start=100, adc_stop=3900, epsilon=0.3):
"""
Calculate linear approximation to temperature curve for a range of
ADC values.
"""
adc_values = numpy.arange(adc_start, adc_stop)
temperature = self.temp(adc_values)
return (adc_values, temperature, rdp(zip(adc_values, temperature), epsilon=epsilon))
# I probably need to put a trimmer to get exact value of 136KOhm, but I didn't have suitable one
# so I have put a pair of 67KOhm in series. Close enough to the optimum.
actual_sense_value = 67000 * 2
adc_stop = int( sense.voltage(actual_sense_value, sense.thermo_range[0]) * 0xfff / sense.vdd )
adc_start = int( sense.voltage(actual_sense_value, sense.thermo_range[1]) * 0xfff / sense.vdd )
thermo = Thermistor(r_sense=actual_sense_value)
(adc_clicks, temperature, temp_approx) = thermo.temp_approximation(adc_start, adc_stop)
plt.plot(adc_clicks, temperature)
plt.plot([v[0] for v in temp_approx], [v[1] for v in temp_approx])
import pprint
pprint.pprint(temp_approx)
[[461.0, 253.11222730625923], [699.0, 261.21337199513243], [1012.0, 269.5324152051872], [1355.0, 277.28655943093554], [2344.0, 298.12754663750894], [2626.0, 304.780313660113], [2865.0, 311.1343478978771], [3105.0, 318.6005327616949], [3307.0, 326.2646538317835], [3477.0, 334.36045885545735], [3618.0, 343.0345723209372]]
for (start_x, start_y), (end_x, end_y) in zip(temp_approx, temp_approx[1:]):
axis = np.arange(start_x, end_x)
delta = np.abs(np.interp(axis, [start_x, end_x], [start_y, end_y]) -
thermo.temp(axis))
plt.plot(delta)
Lets Implement!
Note: The latest and greatest is in the thermo github repositoryHere is my implementation of a simple function that converts ADC click value into Kelvin degrees. I have implemented binary search just to practice simple algorithm implementation, it took me some time to get it right. If I was asked to do this in a programming interview I have probably failed it miserably.
typedef struct _temp_point {
uint16_t adc_value;
uint32_t temp_kelvin;
} temp_point_t;
static temp_point_t steinhart_approximation[] = {
{461, 253112},
{699, 261213},
{1012, 269532},
{1355, 277286},
{2344, 298127},
{2626, 304780},
{2865, 311134},
{3105, 318600},
{3307, 326264},
{3477, 334360},
{3618, 34303})
};
/**
* @brief Convert ADC value into temperature in Kelvin
*
* Look-up an appropriate entry in the precalculated linear approximation table of the
* Steinhart-Hart function.
*
* @returns Linear approximation of the temperature as 32-bit integer that
* represents the temperature multiplied by 1000.
*/
uint32_t steinhart(uint16_t adc_value)
{
int size = sizeof(steinhart_approximation)/sizeof(temp_point_t);
int index, upper = size-1, lower = 0;
while(upper > lower) {
index = lower + (upper - lower) / 2;
if( adc_value >= steinhart_approximation[index].adc_value ) {
if (adc_value < steinhart_approximation[index+1].adc_value)
break;
lower = index+1;
} else if( adc_value < steinhart_approximation[index].adc_value ) {
if (adc_value >= steinhart_approximation[index-1].adc_value) {
index -= 1;
break;
}
upper = index;
}
}
if (upper == lower || adc_value == steinhart_approximation[index].adc_value)
return steinhart_approximation[index].temp_kelvin;
uint32_t linear_approx =
(steinhart_approximation[index+1].temp_kelvin -
steinhart_approximation[index].temp_kelvin) *
(adc_value - steinhart_approximation[index].adc_value) /
(steinhart_approximation[index+1].adc_value -
steinhart_approximation[index].adc_value) +
steinhart_approximation[index].temp_kelvin;
return linear_approx;
}