Hysteresis
This exists in science and nature and is broadly defined but basically something similar to this: Hysteresis is the dependence of the state of a system on its history. Anyway, its application here in the Arduino world is very concrete, generally to minimise oscillation between switching states.
Problem Area:
You've designed a system which determines a state based on an input. For example, it could be setting the brightness of a display based on
the ambient light measured via an analog pin. It could be a potentiometer which you want to use to input numbers on the range 0-9 into an Arduino. It could be thermostat which controls a heater. You discover problems with your display when the ambient light falls very close to the boundary of two brightness levels resulting in flicker. Or in the potentiometer example, you've set it close to the boundary of two values and it wanders between them. Or the heater constantly switches on and off when the ambient temperature is close to the thermostat setting.
Solutions:
There is a number of solutions. One is to add a time delay between changes of state and this prevents rapid oscillation between two states. This would minimise display flicker (etc.) in the example above but could make the system less responsive.
Another solution, which is the theme of this article, is to simply add some hysteresis, that is 'stickiness' or reluctance to leave the current state.
This hysteresis solution can be implemented in hardware or software. However the one presented here is a pure software solution.
Diagram
The diagram below illustrates the situation where you are reading an analog value in the range 0 to 1023 (say it is derived from light sensor) and you want to convert that into 5 levels that is level 0 through to level 4 (say to control the brightness of a display using pulse width modulation).
Broadly, the diagram illustrates that the conditions for leaving an existing state are different (within a margin, tougher) than entering it. Taking a detailed look, let's say the initial state of this system is that an analog value of 500 is read from the sensor. From the graph, it is clear that this yields an output level of 2. Now let's further assume that the light level drops slowly so the analog value moves downwards. At the point where the analog value reaches 380, the output level switches from 2 to 1. If, after dropping past 380, the analog value now starts rising, it must reach 420 before the output level switches back from 1 to 2. So, it is this difference (40 analog units in this case ) between leaving and entering adjacent states which provides the protection against flicker in this example. The arrowed squares in this graph are, incidentally, similar to the classic Schmitt trigger graph.
Software implementation.
This is a simple function which is parameterised for a (linear) potentiometer across the power rails with the wiper connected to analog pin A0 yielding values in the range 0 to 9. It can easily be adapted for other purposes such as controlling display flicker by altering the number of levels and the middle transition points between levels to suit the application.
/*
Hysteresis
This code example shows obtaining values 0-9 from a potentiometer wired
as a voltage divider to analog pin A0. These values remain stable even if the potentiometer
is set on a border point between two values.
The same principle could be used to set the brightness of a display
dependent on the ambient light, but free from flicker or switching a heater
on based on a thermostat etc.
6v6gt 03.feb.2018
*/
uint16_t getOutputLevel( uint16_t inputLevel ) {
//
// convert an input value into an output value with hysteresis to
// avoid instability at cutover points ( eliminate thrashing/flicker etc.)
//
// 6v6gt 03.feb.2018
// adjust these 4 constants to suit your application
// ========================================
// margin sets the 'stickyness' of the hysteresis or the relucatance to leave the current state.
// It is measured in units of the the input level. As a guide it is a few percent of the
// difference between two end points. Don't make the margin too wide or ranges may overlap.
const uint16_t margin = 10 ; // +/- 10
// set the number of output levels. These are numbered starting from 0.
const uint16_t numberOfLevelsOutput = 10 ; // 0..9
// define input to output conversion table/formula by specifying endpoints of the levels.
// the number of end points is equal to the number of output levels plus one.
// in the example below, output level 0 results from an input of between 0 and 112.
// 1 results from an input of between 113 and 212 etc.
const uint16_t endPointInput[ numberOfLevelsOutput + 1 ] = { 0, 112, 212, 312, 412, 512, 612, 712, 812, 912, 1023 } ;
// initial output level (usually zero)
const uint16_t initialOutputLevel = 0 ;
// ========================================
// the current output level is retained for the next calculation.
// Note: initial value of a static variable is set at compile time.
static uint16_t currentOutputLevel = initialOutputLevel ;
// get lower and upper bounds for currentOutputLevel
uint16_t lb = endPointInput[ currentOutputLevel ] ;
if ( currentOutputLevel > 0 ) lb -= margin ; // subtract margin
uint16_t ub = endPointInput[ currentOutputLevel + 1 ] ;
if ( currentOutputLevel < numberOfLevelsOutput ) ub += margin ; // add margin
// now test if input is between the outer margins for current output value
if ( inputLevel < lb || inputLevel > ub ) {
// determine new output level by scanning endPointInput array
uint16_t i;
for ( i = 0 ; i < numberOfLevelsOutput ; i++ ) {
if ( inputLevel >= endPointInput[ i ] && inputLevel <= endPointInput[ i + 1 ] ) break ;
}
currentOutputLevel = i ;
}
return currentOutputLevel ;
}
void setup() {
Serial.begin( 9600 ) ;
}
void loop() {
Serial.print( analogRead(A0) ) ;
Serial.print(" " ) ;
Serial.println( getOutputLevel( analogRead(A0) ) ) ;
delay ( 500 ) ;
}
Edit 1:
Post #25 contains an improved class implementation of this above code.