Histograms are one of the most powerful ways to visualize measurement data. Instead of displaying individual samples, they reveal the statistical distribution of a signal. With a single glance, it becomes possible to estimate measurement noise, detect outliers, identify slow drift, or evaluate the stability of a sensor.
For quasistatic measurements, a histogram often provides far more useful information than a continuous stream of numerical values.
This applies particularly to quasistatic measurements, where the physical quantity itself is expected to remain nearly constant and only small variations are of interest. Typical examples include room temperature, reference voltages, battery voltages under constant load, humidity sensors in a stable environment, pressure sensors at constant pressure, or other measurements where the objective is not to follow rapid changes but to evaluate the stability of the measured value.
In these situations, a histogram immediately answers questions such as:
- How much does the measurement actually fluctuate?
- Is the sensor stable over time?
- Are there occasional outliers?
- Does the average value slowly drift?
Unfortunately, histograms have one significant drawback on small microcontrollers: they require memory.
The number of histogram bins determines both the measurement resolution and the required SRAM. A higher resolution demands more bins, and every bin requires its own counter. On microcontrollers such as the Arduino Uno R3, which provides only 2 kB of SRAM, practical limits are reached surprisingly quickly.
Consider a simple example. A 10-bit ADC produces 1024 possible values. If every ADC value is assigned its own histogram bin, 1024 counters are required. Using only 32-bit counters, the histogram alone already occupies more than 4 kB of SRAMβover twice the total memory available on an Arduino Uno.
The obvious solution is to reduce the number of bins. However, this also reduces the histogram's resolution, making it increasingly difficult to distinguish narrow distributions, small drifts, or subtle changes in sensor behavior.
This article presents a different approach.
Instead of attempting to cover the complete measurement range, the histogram automatically concentrates on the region that actually matters: the operating point of the signal.
During startup, a configurable number of initial measurements is collected to determine a permanent histogram center automatically. Once this center has been established, all subsequent measurements are accumulated in a fixed histogram consisting of only 21 bins:
- 10 bins below the center
- 1 center bin
- 10 bins above the center
As a result, the memory consumption remains constant, regardless of the measurement resolution or engineering units. On an Arduino Uno, the histogram itself occupies only 84 bytes of SRAM, corresponding to just 21 counters of 32 bits each.
Although extremely compact, this approach still provides an excellent visualization of measurement noise, sensor stability, outliers, and long-term drift. For quasistatic measurements it is often more informative than a conventional full-range histogram while requiring only a fraction of the memory.
Configuration
All user-adjustable settings are collected in a clearly structured configuration block near the beginning of the sketch.
The most important settings are:
| Parameter | Description |
|---|---|
ENABLE_TEST_MODE |
Enables the built-in Gaussian signal simulator. |
CENTER_SAMPLE_COUNT |
Number of initial measurements used to determine the permanent histogram center. |
DISCARD_CENTER_EXTREMES |
Optionally ignores the lowest and highest startup values before calculating the histogram center. |
DISCARD_STDDEV_EXTREMES |
Optionally ignores the lowest and highest startup values before calculating the standard deviation used for automatic bin-width selection. |
AUTO_BIN_WIDTH |
Automatically determines a suitable histogram bin width from the startup measurements. |
MANUAL_BIN_WIDTH |
Fixed bin width used when automatic bin-width selection is disabled. |
PRINT_INTERVAL_MINUTES |
Time between histogram updates. |
MAX_BAR_WIDTH |
Maximum width of the text-based histogram bars. |
Since all relevant settings are located in one place, adapting the sketch to different applications is straightforward.
Built-in Test Mode
The sketch can be tested immediately without connecting any hardware.
By default,
#define ENABLE_TEST_MODE 1
enables an internal Gaussian signal generator that continuously produces normally distributed measurement values.
Simply upload the sketch, open the Serial Monitor, and select 115200 baud.
After collecting the configured number of startup measurements, the histogram automatically begins to grow and is updated periodically. This allows the complete functionality to be verified before connecting a real sensor.
Using Your Own Measurements
Switching from the built-in simulator to real measurements requires only two simple changes.
First, disable the simulator:
#define ENABLE_TEST_MODE 0
Then replace YourValue inside the REAL MEASUREMENTS section of loop() with your own measurement:
measuredValue = YourValue; // Replace "YourValue" with your own measurement (variable, expression, or function call)
addMeasurement(measuredValue); // Leave this line unchanged. It adds one measurement to the histogram
Each call to addMeasurement() adds exactly one new measurement to the histogram.
The sketch does not determine the sampling interval. Measurements may be acquired every few milliseconds, once per second, once every minute, or even less frequently. The histogram simply processes every measurement supplied by the application.
This makes the sketch easy to integrate into existing projects without changing the application's measurement timing.
Example Results
Example A: Gaussian Test Signal (Default Configuration)
The sketch is supplied with a built-in Gaussian test signal enabled by default. This allows the histogram to be evaluated immediately without requiring any external hardware.
Auto-Centered Cumulative Text Histogram
========================================
Mode : Gaussian test signal
Test mean : 0.6000
Test deviation : 0.0050
Test delay : 100 ms
Histogram bins : 21
Bin width mode : Automatic
Center samples : 100
Center extremes : Lowest and highest discarded
Stddev extremes : Lowest and highest discarded
Histogram center determined
============================
Initial samples : 100
Center value : 0.599
Bin width : 0.002
Displayed range : 0.581 ... 0.618
Histogram collection started.
After the initialization phase, the cumulative histogram gradually develops into the expected bell-shaped distribution as additional samples are collected.
After 595 measurements
Histogram samples : 595
Inside range : 595 (100.00 %)
Below range : 0 (0.00 %)
Above range : 0 (0.00 %)
0.617:
0.615:
0.614: ββ 0.50 %
0.612: βββ 0.84 %
0.610: βββββ 1.51 %
0.608: ββββββββββββββ 4.20 %
0.607: βββββββββββββββ 4.54 %
0.605: βββββββββββββββββββββββββββββ 8.74 %
0.603: βββββββββββββββββββββββββββββββββββββ 11.26 %
0.601: βββββββββββββββββββββββββββββββββββββββββββββ 13.61 %
0.599: ββββββββββββββββββββββββββββββββββββββββββββββββββ 15.13 %
0.598: ββββββββββββββββββββββββββββββββββββββββββ 12.61 %
0.596: βββββββββββββββββββββββββββββββββββ 10.59 %
0.594: ββββββββββββββββββββββ 6.72 %
0.592: ββββββββββββββββ 4.87 %
0.591: βββββββββ 2.69 %
0.589: βββ 1.01 %
0.587: βββ 1.01 %
0.585: β 0.17 %
0.584:
0.582:
The automatic bin-width selection determined a bin width of 0.002, producing a smooth, nearly symmetric Gaussian distribution. All 595 samples remained within the displayed range, demonstrating that the automatically selected histogram parameters are well suited to the statistical properties of the generated signal.
This built-in test mode provides a convenient way to verify the sketch without connecting any external sensors before using it with real measurement data.
Example B: Real DS18B20 Measurements
The following examples demonstrate the same histogram algorithm using a real DS18B20 temperature sensor.
Auto-Centered Cumulative Text Histogram
========================================
Mode : Real measurements
Histogram bins : 21
Bin width mode : Automatic
Center samples : 100
Center extremes : Lowest and highest discarded
Stddev extremes : Lowest and highest discarded
Histogram center determined
============================
Initial samples : 100
Center value : 25.359
Bin width : 0.063
Displayed range : 24.703 ... 26.015
Histogram collection started.
After the initialization phase, the cumulative histogram evolves as additional measurements are collected.
Example 1 β Normal Room Conditions
After 57 measurements
Histogram samples : 57
Inside range : 57 (100.00 %)
Below range : 0 (0.00 %)
Above range : 0 (0.00 %)
25.422: ββββββ 10.53 %
25.359: ββββββββββββββββββββββββββββββββββββββββββββββββββ 89.47 %
25.297:
After 115 measurements
Histogram samples : 115
Inside range : 115 (100.00 %)
Below range : 0 (0.00 %)
Above range : 0 (0.00 %)
25.422: βββββ 8.70 %
25.359: ββββββββββββββββββββββββββββββββββββββββββββββββββ 89.57 %
25.297: β 1.74 %
After 173 measurements
Histogram samples : 173
Inside range : 173 (100.00 %)
Below range : 0 (0.00 %)
Above range : 0 (0.00 %)
25.422: βββββ 5.78 %
25.359: ββββββββββββββββββββββββββββββββββββββββββββββββββ 62.43 %
25.297: βββββββββββββββββββββββββ 31.79 %
After 231 measurements
Histogram samples : 231
Inside range : 231 (100.00 %)
Below range : 0 (0.00 %)
Above range : 0 (0.00 %)
25.422: ββββ 4.33 %
25.359: ββββββββββββββββββββββββββββββββββββββββββββββββββ 61.47 %
25.297: ββββββββββββββββββββββββββββ 34.20 %
The automatic bin-width selection detected the DS18B20's temperature resolution and selected a bin width of 0.063 Β°C (approximately the sensor's 0.0625 Β°C quantization step). Over time, the cumulative histogram reveals a slow drift toward the next lower quantization level while all measurements remain within the automatically selected display range.
Example 2 β Sensor Exposed to Airflow
The same sensor was exposed to the airflow of a small fan. The histogram configuration remained unchanged.
Auto-Centered Cumulative Text Histogram
========================================
Mode : Real measurements
Histogram bins : 21
Bin width mode : Automatic
Center samples : 100
Center extremes : Lowest and highest discarded
Stddev extremes : Lowest and highest discarded
Histogram center determined
============================
Initial samples : 100
Center value : 23.972
Bin width : 0.063
Displayed range : 23.316 ... 24.628
Histogram collection started.
After 58 measurements
Histogram samples : 58
Inside range : 58 (100.00 %)
Below range : 0 (0.00 %)
Above range : 0 (0.00 %)
24.034: βββββββββββββββββββββ 29.31 %
23.972: ββββββββββββββββββββββββββββββββββββββββββββββββββ 70.69 %
23.909:
After 116 measurements
Histogram samples : 116
Inside range : 116 (100.00 %)
Below range : 0 (0.00 %)
Above range : 0 (0.00 %)
24.034: βββββββββββ 16.38 %
23.972: ββββββββββββββββββββββββββββββββββββββββββββββββββ 77.59 %
23.909: ββββ 6.03 %
After 174 measurements
Histogram samples : 174
Inside range : 174 (100.00 %)
Below range : 0 (0.00 %)
Above range : 0 (0.00 %)
24.034: βββββββ 11.49 %
23.972: ββββββββββββββββββββββββββββββββββββββββββββββββββ 78.74 %
23.909: ββββββ 9.77 %
The automatic bin-width selection again detected the sensor's 12-bit resolution and selected a bin width of 0.063 Β°C without any manual adjustment.
Unlike the first example, the average temperature remains nearly constant while the airflow introduces small continuous fluctuations. Consequently, the cumulative histogram spreads over three adjacent quantization levels instead of gradually shifting toward a new operating point.
Together, these two experiments show how the same histogram can distinguish between slow long-term drift and short-term measurement variability without requiring any manual adjustment of the histogram parameters.
Conclusion
The Auto-Centered Cumulative Text Histogram demonstrates that meaningful statistical analysis is possible even on small microcontrollers with very limited SRAM.
By concentrating on the actual operating point instead of the complete measurement range, the sketch reduces memory consumption to only 84 bytes for the histogram itself while still providing an excellent visualization of measurement stability, noise, outliers, and slow drift.
If your project makes decisions based on measured values, this histogram can help answer questions such as:
- How stable is my sensor?
- Is my threshold too close to the noise?
- Has the measurement distribution changed over time?
For many quasistatic measurement tasks, this operating-point-centered approach is not merely a memory-saving compromiseβit is often the more useful way to evaluate sensor performance.
Sketch
/*
Auto-Centered Cumulative Text Histogram v1.1
============================================
This sketch is intended for quasistatic measurements whose value should
remain approximately constant.
Instead of allocating bins for the complete possible measurement range,
the sketch uses only 21 fixed bins:
10 bins below the center
1 center bin
10 bins above the center
The permanent histogram center and, by default, a suitable bin width are
calculated from the first CENTER_SAMPLE_COUNT measurements. The lowest and
highest initial values can optionally be discarded independently when
calculating the center and the startup standard deviation. This allows the
influence of startup outliers on both calculations to be configured
separately.
Automatic bin-width selection uses both the observed measurement step and
the startup standard deviation. The standard deviation can be calculated
either from the complete startup sample or from the trimmed startup sample.
This is convenient for most quasistatic measurements because no prior
knowledge of sensor resolution or noise is required.
Automatic selection should be disabled when a fixed bin width is part of
the measurement definition, when several measurements must use exactly the
same scale, or when the startup values do not represent the later operating
conditions. In these cases MANUAL_BIN_WIDTH provides a reproducible scale.
Each initial measurement is indicated by one dot. A space is inserted
after every ten dots to make the count easier to read:
Measurements:
.......... .......... .......... ..........
After the center has been determined, all subsequent measurements are
collected in the cumulative histogram. The center is not adjusted again,
so later drift remains visible.
While waiting for the next histogram output, one dot is printed per
second. This provides a visible indication that the sketch is running.
TEST MODE
---------
Test mode is enabled by default. It generates normally distributed
random values so that the sketch demonstrates itself immediately.
REAL MEASUREMENTS
-----------------
To use your own measurements:
1. Set ENABLE_TEST_MODE to 0.
2. Insert your own measurement in the REAL MEASUREMENTS section in loop():
measuredValue = YourValue;
addMeasurement(measuredValue);
Each call to addMeasurement() adds exactly one new measurement.
The histogram does not determine the sampling interval for real
measurements.
CUMULATIVE OPERATION
--------------------
After the center has been determined, the histogram accumulates all
subsequent measurements until the Arduino is reset or switched off.
*/
#include <Arduino.h>
#include <math.h>
// ============================================================================
// CONFIGURATION
// ============================================================================
// ----------------------------------------------------------------------------
// OPERATING MODE
// ----------------------------------------------------------------------------
//
// 0 = Use real measurements supplied by the user
// 1 = Use the built-in Gaussian test generator
//
#define ENABLE_TEST_MODE 1
// ----------------------------------------------------------------------------
// CENTER DETERMINATION
// ----------------------------------------------------------------------------
/*
The first CENTER_SAMPLE_COUNT measurements are used to determine the
permanent histogram center.
These initial measurements are not included in the histogram itself.
*/
constexpr uint16_t CENTER_SAMPLE_COUNT = 100;
/*
DISCARD_CENTER_EXTREMES and DISCARD_STDDEV_EXTREMES:
The default is to discard the startup extremes consistently for both
calculations. However, the two decisions are independent, so each can be
enabled or disabled separately if a particular application requires it.
*/
/*
When enabled, the lowest and highest values of the initial measurements
are discarded before calculating the center.
This reduces the influence of isolated startup outliers.
*/
constexpr bool DISCARD_CENTER_EXTREMES = true;
/*
When enabled, the lowest and highest values of the initial measurements
are also discarded before calculating the startup standard deviation used
for automatic bin-width selection.
This setting is independent of DISCARD_CENTER_EXTREMES.
*/
constexpr bool DISCARD_STDDEV_EXTREMES = true;
// ----------------------------------------------------------------------------
// TEST MODE
// ----------------------------------------------------------------------------
//
// These settings are used only when ENABLE_TEST_MODE is 1.
//
// TEST_MEAN_VALUE defines the center of the generated normal distribution.
//
// TEST_STANDARD_DEVIATION determines the width of the distribution.
//
// TEST_DELAY_MS determines the delay between generated test measurements.
//
constexpr float TEST_MEAN_VALUE = 0.600f;
constexpr float TEST_STANDARD_DEVIATION = 0.005f;
constexpr uint32_t TEST_DELAY_MS = 100;
// ----------------------------------------------------------------------------
// HISTOGRAM
// ----------------------------------------------------------------------------
//
// The histogram always contains 21 bins:
//
// 10 bins below the center
// 1 center bin
// 10 bins above the center
//
// The selected bin width determines the measurement range represented by
// each bin.
//
// Example:
//
// Automatically determined center = 0.600
// Selected bin width = 0.001
//
// The center bin then covers approximately:
//
// 0.5995 ... 0.6005
//
// The complete histogram covers approximately:
//
// 0.5895 ... 0.6105
//
// Measurements outside that range are counted separately.
//
constexpr size_t BIN_COUNT = 21;
/*
true = Determine the bin width automatically from the initial measurements.
false = Use MANUAL_BIN_WIDTH.
Automatic mode is suitable for most quasistatic measurements because it
adapts to both quantized sensors and continuously varying noisy signals.
Use manual mode when:
- a prescribed bin width is part of the measurement method,
- several measurements must be compared using exactly the same scale,
- startup conditions differ from the later operating conditions,
- or the expected resolution and useful scale are already known.
*/
constexpr bool AUTO_BIN_WIDTH = true;
/*
Used only when AUTO_BIN_WIDTH is false.
This value is also used as a safe fallback if automatic mode cannot derive
a useful width because all initial measurements are identical.
*/
constexpr float MANUAL_BIN_WIDTH = 0.001f;
/*
Runtime bin width selected after the initialization phase.
*/
float binWidth = MANUAL_BIN_WIDTH;
// ----------------------------------------------------------------------------
// HISTOGRAM OUTPUT
// ----------------------------------------------------------------------------
//
// PRINT_INTERVAL_MINUTES determines how often the cumulative histogram
// is printed.
//
// The default interval is one minute.
//
// MAX_BAR_WIDTH determines the maximum displayed bar length.
//
constexpr uint32_t PRINT_INTERVAL_MINUTES = 1;
constexpr size_t MAX_BAR_WIDTH = 50;
/*
The filled block character works in many serial monitors.
Replace it with "#" if the character is not displayed correctly.
*/
const char BAR_SYMBOL[] = "β";
// ============================================================================
// COMPILE-TIME CHECKS
// ============================================================================
static_assert(
BIN_COUNT >= 3 && (BIN_COUNT % 2) == 1,
"BIN_COUNT must be an odd number of at least 3."
);
static_assert(
MANUAL_BIN_WIDTH > 0.0f,
"MANUAL_BIN_WIDTH must be greater than zero."
);
static_assert(
CENTER_SAMPLE_COUNT > 0,
"CENTER_SAMPLE_COUNT must be greater than zero."
);
static_assert(
!DISCARD_CENTER_EXTREMES || CENTER_SAMPLE_COUNT >= 3,
"At least 3 center samples are required when discarding center extremes."
);
static_assert(
!DISCARD_STDDEV_EXTREMES || CENTER_SAMPLE_COUNT >= 4,
"At least 4 center samples are required when discarding standard deviation extremes."
);
static_assert(
PRINT_INTERVAL_MINUTES > 0,
"PRINT_INTERVAL_MINUTES must be greater than zero."
);
// ============================================================================
// DERIVED CONSTANTS
// ============================================================================
constexpr size_t CENTER_BIN = BIN_COUNT / 2;
constexpr uint32_t PRINT_INTERVAL_MS =
PRINT_INTERVAL_MINUTES * 60UL * 1000UL;
constexpr uint8_t PROGRESS_DOTS_PER_LINE = 60;
// ============================================================================
// HISTOGRAM DATA
// ============================================================================
/*
Fixed-size histogram.
On an Arduino Uno:
21 bins Γ 4 bytes = 84 bytes
*/
uint32_t histogram[BIN_COUNT] = {};
uint32_t histogramSamples = 0;
uint32_t belowRange = 0;
uint32_t aboveRange = 0;
// ============================================================================
// CENTER CALCULATION DATA
// ============================================================================
float centerSum = 0.0f;
float centerMinimum = INFINITY;
float centerMaximum = -INFINITY;
/*
Startup statistics used for automatic bin-width determination.
Welford's online algorithm calculates the standard deviation without
storing the individual initial measurements.
*/
float startupMean = 0.0f;
float startupM2 = 0.0f;
/*
The smallest observed non-zero difference between successive startup
measurements is used as an estimate of a quantized sensor step.
*/
float previousStartupValue = 0.0f;
float smallestObservedStep = INFINITY;
bool previousStartupValueValid = false;
uint16_t centerSamplesCollected = 0;
float histogramCenter = 0.0f;
bool centerReady = false;
// ============================================================================
// GENERAL DATA
// ============================================================================
float measuredValue = 0.0f;
uint32_t lastPrintTime = 0;
uint32_t lastProgressDotTime = 0;
uint8_t progressDotsInLine = 0;
bool progressLineActive = false;
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/*
Determine a suitable number of decimal places.
The final measurement output currently uses a fixed format of three
decimal places. This helper remains available for future formatting
changes.
*/
uint8_t determineDecimalPlaces(float value)
{
uint8_t decimalPlaces = 0;
while (value < 1.0f && decimalPlaces < 6) {
value *= 10.0f;
++decimalPlaces;
if (fabsf(value - roundf(value)) < 0.0001f) {
break;
}
}
return decimalPlaces;
}
/*
Return the lower boundary of the displayed histogram range.
The middle bin is centered exactly on histogramCenter.
*/
float getHistogramMinimum()
{
return histogramCenter -
(static_cast<float>(CENTER_BIN) + 0.5f) * binWidth;
}
/*
Return the upper boundary of the displayed histogram range.
*/
float getHistogramMaximum()
{
return histogramCenter +
(static_cast<float>(CENTER_BIN) + 0.5f) * binWidth;
}
/*
Return the displayed center value of one bin.
*/
float getBinCenter(size_t bin)
{
return histogramCenter +
(
static_cast<int>(bin) -
static_cast<int>(CENTER_BIN)
) * binWidth;
}
/*
Finish an active line of progress dots before other text is printed.
*/
void finishProgressLine()
{
if (progressLineActive) {
Serial.println();
progressLineActive = false;
progressDotsInLine = 0;
}
}
/*
Print one progress dot and start a new line after 60 dots.
*/
void printProgressDot()
{
Serial.print('.');
progressLineActive = true;
++progressDotsInLine;
if (progressDotsInLine >= PROGRESS_DOTS_PER_LINE) {
Serial.println();
progressLineActive = false;
progressDotsInLine = 0;
}
else if ((progressDotsInLine % 10) == 0) {
Serial.print(' ');
}
}
/*
Print the configured waiting time.
firstHistogram selects between:
Wait 1 minute for first histogram...
and:
Wait 1 minute for next histogram...
*/
void printWaitMessage(bool firstHistogram)
{
Serial.println();
Serial.print(F("Wait "));
Serial.print(PRINT_INTERVAL_MINUTES);
if (PRINT_INTERVAL_MINUTES == 1) {
Serial.print(F(" minute"));
}
else {
Serial.print(F(" minutes"));
}
if (firstHistogram) {
Serial.println(F(" for first histogram..."));
}
else {
Serial.println(F(" for next histogram..."));
}
progressDotsInLine = 0;
progressLineActive = false;
}
/*
Start a complete histogram waiting period.
The output timer begins when the histogram center has been determined,
not when the Arduino was switched on.
*/
void startHistogramWaitingPeriod(bool firstHistogram)
{
const uint32_t currentTime = millis();
lastPrintTime = currentTime;
lastProgressDotTime = currentTime;
printWaitMessage(firstHistogram);
}
// ============================================================================
// TEST GENERATOR
// ============================================================================
/*
Generate a normally distributed test value using the Box-Muller
transformation.
meanValue:
Center of the generated distribution.
standardDeviation:
Width of the generated distribution.
*/
float generateGaussValue(
float meanValue,
float standardDeviation = 0.005f)
{
/*
random(1, 10001) ensures that u1 is never zero because log(0)
is undefined.
*/
const float u1 =
static_cast<float>(random(1, 10001)) / 10000.0f;
const float u2 =
static_cast<float>(random(0, 10001)) / 10000.0f;
const float standardNormal =
sqrtf(-2.0f * logf(u1)) *
cosf(2.0f * PI * u2);
return meanValue + standardNormal * standardDeviation;
}
// ============================================================================
// CENTER DETERMINATION
// ============================================================================
/*
Add one of the initial measurements used to determine the permanent
histogram center.
*/
void addCenterMeasurement(float value)
{
centerSum += value;
/*
Update startup mean and variance without storing individual samples.
*/
const uint16_t newSampleCount = centerSamplesCollected + 1;
const float delta = value - startupMean;
startupMean += delta / static_cast<float>(newSampleCount);
startupM2 += delta * (value - startupMean);
/*
Estimate the smallest observed non-zero measurement step.
*/
if (previousStartupValueValid) {
const float step =
fabsf(value - previousStartupValue);
if (step > 0.0f && step < smallestObservedStep) {
smallestObservedStep = step;
}
}
previousStartupValue = value;
previousStartupValueValid = true;
if (value < centerMinimum) {
centerMinimum = value;
}
if (value > centerMaximum) {
centerMaximum = value;
}
++centerSamplesCollected;
/*
Each dot represents exactly one received initial measurement.
*/
printProgressDot();
/*
Continue collecting initial measurements until the configured count
has been reached.
*/
if (centerSamplesCollected < CENTER_SAMPLE_COUNT) {
return;
}
finishProgressLine();
/*
Calculate the permanent histogram center.
*/
if (DISCARD_CENTER_EXTREMES) {
histogramCenter =
(
centerSum -
centerMinimum -
centerMaximum
) /
static_cast<float>(CENTER_SAMPLE_COUNT - 2);
}
else {
histogramCenter =
centerSum /
static_cast<float>(CENTER_SAMPLE_COUNT);
}
/*
Determine the bin width.
The statistical candidate distributes approximately +/-4 standard
deviations over the ten bins on either side of the center.
For quantized sensors, the smallest observed non-zero step is selected
when it is larger than the statistical candidate.
*/
if (AUTO_BIN_WIDTH) {
float statisticalBinWidth = 0.0f;
if (CENTER_SAMPLE_COUNT > 1) {
float variance = 0.0f;
if (DISCARD_STDDEV_EXTREMES) {
uint16_t trimmedSampleCount = CENTER_SAMPLE_COUNT;
float trimmedMean = startupMean;
float trimmedM2 = startupM2;
const float extremes[] = {
centerMinimum,
centerMaximum
};
for (const float extreme : extremes) {
const uint16_t newSampleCount =
trimmedSampleCount - 1;
const float newMean =
(
static_cast<float>(trimmedSampleCount) * trimmedMean -
extreme
) /
static_cast<float>(newSampleCount);
trimmedM2 -=
(extreme - trimmedMean) *
(extreme - newMean);
trimmedMean = newMean;
trimmedSampleCount = newSampleCount;
}
variance =
trimmedM2 /
static_cast<float>(trimmedSampleCount - 1);
}
else {
variance =
startupM2 /
static_cast<float>(CENTER_SAMPLE_COUNT - 1);
}
const float standardDeviation =
sqrtf(max(variance, 0.0f));
statisticalBinWidth =
4.0f * standardDeviation /
static_cast<float>(CENTER_BIN);
}
binWidth = MANUAL_BIN_WIDTH;
if (statisticalBinWidth > binWidth) {
binWidth = statisticalBinWidth;
}
if (
smallestObservedStep < INFINITY &&
smallestObservedStep > binWidth
) {
binWidth = smallestObservedStep;
}
}
else {
binWidth = MANUAL_BIN_WIDTH;
}
centerReady = true;
/*
Immediately report the result of the initialization phase.
*/
constexpr uint8_t decimalPlaces = 3;
Serial.println();
Serial.println(F("Histogram center determined"));
Serial.println(F("============================"));
Serial.print(F("Initial samples : "));
Serial.println(CENTER_SAMPLE_COUNT);
Serial.print(F("Center value : "));
Serial.println(histogramCenter, decimalPlaces);
Serial.print(F("Bin width : "));
Serial.println(binWidth, 3);
Serial.print(F("Displayed range : "));
Serial.print(getHistogramMinimum(), decimalPlaces);
Serial.print(F(" ... "));
Serial.println(getHistogramMaximum(), decimalPlaces);
Serial.println(F("Histogram collection started."));
startHistogramWaitingPeriod(true);
}
// ============================================================================
// MEASUREMENT PROCESSING
// ============================================================================
/*
Add one newly generated measurement.
During initialization, measurements are used only to determine the
histogram center.
After initialization, all subsequent measurements are added to the
cumulative histogram.
*/
void addMeasurement(float value)
{
/*
Ignore invalid floating-point values.
*/
if (isnan(value) || isinf(value)) {
return;
}
/*
Initial center-determination phase.
*/
if (!centerReady) {
addCenterMeasurement(value);
return;
}
/*
Histogram collection phase.
*/
++histogramSamples;
const float histogramMinimum =
getHistogramMinimum();
const float histogramMaximum =
getHistogramMaximum();
/*
Count values outside the displayed histogram range separately.
*/
if (value < histogramMinimum) {
++belowRange;
return;
}
if (value >= histogramMaximum) {
++aboveRange;
return;
}
/*
Determine the corresponding bin.
The lower range boundary belongs to bin 0.
*/
const float position =
(value - histogramMinimum) / binWidth;
size_t bin =
static_cast<size_t>(position);
/*
Protect against floating-point rounding at the upper boundary.
*/
if (bin >= BIN_COUNT) {
bin = BIN_COUNT - 1;
}
++histogram[bin];
}
// ============================================================================
// HISTOGRAM OUTPUT
// ============================================================================
/*
Print an absolute count and its percentage of all histogram measurements.
*/
void printCountAndPercentage(uint32_t count)
{
float percentage = 0.0f;
if (histogramSamples > 0) {
percentage =
100.0f *
static_cast<float>(count) /
static_cast<float>(histogramSamples);
}
Serial.print(count);
Serial.print(F(" ("));
Serial.print(percentage, 2);
Serial.print(F(" %)"));
}
/*
Print the current cumulative histogram.
*/
void printHistogram()
{
finishProgressLine();
constexpr uint8_t decimalPlaces = 3;
Serial.println();
Serial.println(F("============================================================"));
Serial.println(F("Auto-Centered Cumulative Measurement Histogram"));
Serial.println(F("============================================================"));
#if ENABLE_TEST_MODE
Serial.println(F("Mode : Gaussian test signal"));
#else
Serial.println(F("Mode : Real measurements"));
#endif
/*
This case should normally occur only if real measurements are supplied
less frequently than the configured output interval.
*/
if (!centerReady) {
Serial.println(F("Status : Determining histogram center"));
Serial.print(F("Center samples : "));
Serial.print(centerSamplesCollected);
Serial.print(F(" / "));
Serial.println(CENTER_SAMPLE_COUNT);
Serial.println(F("============================================================"));
return;
}
const uint32_t insideRange =
histogramSamples - belowRange - aboveRange;
Serial.print(F("Center samples : "));
Serial.println(CENTER_SAMPLE_COUNT);
Serial.print(F("Histogram center : "));
Serial.println(histogramCenter, decimalPlaces);
Serial.print(F("Bin width : "));
Serial.println(binWidth, 3);
Serial.print(F("Displayed range : "));
Serial.print(getHistogramMinimum(), decimalPlaces);
Serial.print(F(" ... "));
Serial.println(getHistogramMaximum(), decimalPlaces);
Serial.print(F("Histogram samples : "));
Serial.println(histogramSamples);
Serial.print(F("Inside range : "));
printCountAndPercentage(insideRange);
Serial.println();
Serial.print(F("Below range : "));
printCountAndPercentage(belowRange);
Serial.println();
Serial.print(F("Above range : "));
printCountAndPercentage(aboveRange);
Serial.println();
Serial.println(F("------------------------------------------------------------"));
/*
Determine the largest bin for automatic bar scaling.
*/
uint32_t largestBin = 0;
for (size_t i = 0; i < BIN_COUNT; ++i) {
if (histogram[i] > largestBin) {
largestBin = histogram[i];
}
}
/*
Print the highest bin first.
*/
for (int i = static_cast<int>(BIN_COUNT) - 1;
i >= 0;
--i) {
const float binCenter =
getBinCenter(static_cast<size_t>(i));
Serial.print(binCenter, decimalPlaces);
Serial.print(F(": "));
size_t barLength = 0;
if (largestBin > 0 && histogram[i] > 0) {
const float relativeLength =
static_cast<float>(histogram[i]) /
static_cast<float>(largestBin);
barLength =
static_cast<size_t>(
relativeLength * MAX_BAR_WIDTH + 0.5f
);
/*
Every occupied bin should remain visible.
*/
if (barLength == 0) {
barLength = 1;
}
}
for (size_t character = 0;
character < barLength;
++character) {
Serial.print(BAR_SYMBOL);
}
/*
Keep the numerical result readable even when the bar is empty.
*/
if (histogram[i] > 0) {
float percentage = 0.0f;
if (histogramSamples > 0) {
percentage =
100.0f *
static_cast<float>(histogram[i]) /
static_cast<float>(histogramSamples);
}
Serial.print(F(" "));
Serial.print(percentage, 2);
Serial.print(F(" %"));
}
Serial.println();
}
Serial.println(F("============================================================"));
}
// ============================================================================
// PROGRESS AND OUTPUT TIMING
// ============================================================================
/*
Print one dot for every elapsed second while waiting for the next
histogram output.
This function is non-blocking and does not affect the user's measurement
timing.
*/
void updateProgressDots()
{
if (!centerReady) {
return;
}
const uint32_t currentTime = millis();
while (
static_cast<uint32_t>(
currentTime - lastProgressDotTime
) >= 1000UL
) {
lastProgressDotTime += 1000UL;
printProgressDot();
}
}
/*
Check whether it is time to print the next histogram.
The subtraction remains valid when millis() overflows.
*/
void updateHistogramOutput()
{
if (!centerReady) {
return;
}
const uint32_t currentTime = millis();
if (
static_cast<uint32_t>(
currentTime - lastPrintTime
) >= PRINT_INTERVAL_MS
) {
printHistogram();
startHistogramWaitingPeriod(false);
}
}
// ============================================================================
// ARDUINO SETUP
// ============================================================================
void setup()
{
Serial.begin(115200);
#if ENABLE_TEST_MODE
/*
An unused analog input normally provides enough electrical variation
for a changing pseudo-random seed.
*/
randomSeed(analogRead(A0));
#endif
Serial.println();
Serial.println(F("Auto-Centered Cumulative Text Histogram"));
Serial.println(F("========================================"));
#if ENABLE_TEST_MODE
Serial.println(F("Mode : Gaussian test signal"));
Serial.print(F("Test mean : "));
Serial.println(TEST_MEAN_VALUE, 4);
Serial.print(F("Test deviation : "));
Serial.println(TEST_STANDARD_DEVIATION, 4);
Serial.print(F("Test delay : "));
Serial.print(TEST_DELAY_MS);
Serial.println(F(" ms"));
#else
Serial.println(F("Mode : Real measurements"));
#endif
Serial.print(F("Histogram bins : "));
Serial.println(BIN_COUNT);
Serial.print(F("Bin width mode : "));
if (AUTO_BIN_WIDTH) {
Serial.println(F("Automatic"));
}
else {
Serial.println(F("Manual"));
Serial.print(F("Bin width : "));
Serial.println(MANUAL_BIN_WIDTH, 3);
}
Serial.print(F("Center samples : "));
Serial.println(CENTER_SAMPLE_COUNT);
if (DISCARD_CENTER_EXTREMES) {
Serial.println(F("Center extremes : Lowest and highest discarded"));
}
else {
Serial.println(F("Center extremes : Included"));
}
if (DISCARD_STDDEV_EXTREMES) {
Serial.println(F("Stddev extremes : Lowest and highest discarded"));
}
else {
Serial.println(F("Stddev extremes : Included"));
}
Serial.println();
Serial.print(F("Determining histogram center from "));
Serial.print(CENTER_SAMPLE_COUNT);
Serial.println(F(" initial measurements..."));
Serial.println(F("Measurements:"));
}
// ============================================================================
// ARDUINO LOOP
// ============================================================================
void loop()
{
#if ENABLE_TEST_MODE
// --------------------------------------------------------------------------
// TEST MODE
// --------------------------------------------------------------------------
measuredValue = generateGaussValue(
TEST_MEAN_VALUE,
TEST_STANDARD_DEVIATION
);
addMeasurement(measuredValue);
/*
This delay controls only the built-in test generator.
*/
delay(TEST_DELAY_MS);
#else
// --------------------------------------------------------------------------
// REAL MEASUREMENTS
// --------------------------------------------------------------------------
/*
Replace YourValue with your own measurement:
measuredValue = YourValue;
Example for a normalized analog input:
measuredValue = analogRead(A0) / 1023.0f;
Call addMeasurement() only when your own program has generated a new
measurement.
The histogram does not impose a sampling interval or delay.
*/
measuredValue = YourValue; // Replace "YourValue" with your own measurement (variable, expression, or function call)
addMeasurement(measuredValue); // Leave this line unchanged. It adds one measurement to the histogram
#endif
/*
The progress indicator and histogram output are checked in both
operating modes.
*/
updateProgressDots();
updateHistogramOutput();
}