Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 81 additions & 43 deletions firmware/inc/GammaCorrection/GammaCorrection.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,56 @@
* FILE DESCRIPTION
* -------------------------------------------------------------------------------------------------------------------------------------------------*/
/** \file GammaCorrection.h
* \brief
* \brief Maps a linear brightness setting onto an exponential curve, so that equal
* steps of the setting look like equal steps of light
*
* \details
* \details The eye answers light roughly logarithmically (Weber-Fechner): going from
* 10 to 20 looks like as big a step as going from 100 to 200. A brightness
* that reaches the LEDs linearly therefore seems to rise quickly at the
* bottom and then hardly change across the upper half of the setting. An
* exponential curve in front of the LEDs undoes that. The background,
* including why exponential rather than a power curve is good enough for
* LEDs, is the article
* https://www.mikrocontroller.net/articles/LED-Fading
*
* Despite the class name this is that exponential curve, not a gamma (power)
* curve.
*
* The implementation is the one from the article's discussion page, section
* "Sparsamere Implementierung":
* https://www.mikrocontroller.net/articles/Diskussion:LED-Fading
* Written for ATtinys, so it needs neither a 256-entry table nor any
* multiplication:
*
* - The curve doubles every 16 steps, so 128 steps cover 8 doublings - from
* 1 to 255, the whole range of a byte. The setting is halved to those 128
* steps first.
* - Only the top doubling is stored: 16 values of 2^(7 + n/16) for n = 1..16,
* i.e. 133..255. Every lower doubling is the same 16 values shifted right,
* one bit per doubling. A step therefore picks its value with its low four
* bits and its shift with its upper three.
* - That only closes if steps = values per doubling * doublings, and the
* doublings are the bits of the result: 128 = 16 * 8. The static_assert
* below holds it.
*
* Properties worth knowing before changing anything:
*
* - 0 maps to 0. The exponential itself never reaches zero - the lowest step
* is 1 - and the source recommends exactly this answer for it: at a
* setting of 0 the LEDs are to get 0, not the faintest light. It is also
* what an uncorrected setting of 0 hands on; whether the display then
* goes dark is decided after this, not here.
* - The bottom is coarse. The settings 1..31 all give 1 and up to 63 give at
* most 3: 8 bits have nothing between 0, 1 and 2 to offer, so a curve
* that is exponential all the way down has to repeat itself there. The
* source calls 128 steps on 8 bits "verschwenderisch" for that reason.
* - The middle is dark: half the setting gives 16 of 255, about 6 %. That is
* the base of the curve, 256 over the whole range, fixed by the shift
* trick. The article suggests a base between 10 and 100 and says it takes
* trying out; a flatter curve would need a different table.
* - The stored values lie up to 1 below the formula above - 151 where it
* gives 152.2. They are kept as published: one 256th of a doubling is
* not something anybody sees.
*
******************************************************************************************************************************************************/
#ifndef _GAMMA_CORRECTION_H_
Expand All @@ -24,20 +71,6 @@
#include "Arduino.h"


/******************************************************************************************************************************************************
* G L O B A L C O N S T A N T M A C R O S
******************************************************************************************************************************************************/
/* GammaCorrection configuration parameter */


/* GammaCorrection parameter */


/******************************************************************************************************************************************************
* G L O B A L F U N C T I O N M A C R O S
******************************************************************************************************************************************************/


/******************************************************************************************************************************************************
* C L A S S G A M M A C O R R E C T I O N
******************************************************************************************************************************************************/
Expand All @@ -47,53 +80,58 @@ class GammaCorrection
* P U B L I C D A T A T Y P E S A N D S T R U C T U R E S
******************************************************************************************************************************************************/
public:
using Gamma7TableElementType = byte;
using TopDoublingElementType = byte;

/******************************************************************************************************************************************************
* P R I V A T E D A T A A N D F U N C T I O N S
******************************************************************************************************************************************************/
private:
static byte constexpr Gamma7TableNumberOfValues{16u};

static constexpr Gamma7TableElementType Gamma7Table[] PROGMEM {
static constexpr byte ValuesPerDoubling{16u};
static constexpr byte NumberOfDoublings{sizeof(byte) * CHAR_BIT};
static constexpr byte NumberOfSteps{128u};

static_assert(ValuesPerDoubling * NumberOfDoublings == NumberOfSteps,
"GammaCorrection: the steps must be exactly the stored doubling shifted through every bit of the result");

/* 2^(7 + n/16) for n = 1..16, as published - see the file header for the 1 they lie
below the formula. */
static constexpr TopDoublingElementType TopDoublingTable[ValuesPerDoubling] PROGMEM {
133u, 139u, 145u, 151u, 158u, 165u, 172u, 180u, 188u, 196u, 205u, 214u, 224u, 234u, 244u, 255u
};

// functions
static Gamma7TableElementType getGamma7TableElement(byte Index) {
Gamma7TableElementType Gamma7TableElement;
memcpy_P(&Gamma7TableElement, &Gamma7Table[Index], sizeof(Gamma7TableElementType));
return Gamma7TableElement;
static TopDoublingElementType getTopDoublingElement(byte Index) {
TopDoublingElementType Element;
memcpy_P(&Element, &TopDoublingTable[Index], sizeof(TopDoublingElementType));
return Element;
}

/* The top doubling needs no shift, each one below it one bit more. */
static constexpr byte toShift(byte Step) {
return static_cast<byte>((NumberOfDoublings - 1u) - (Step / ValuesPerDoubling));
}

/**************************************************************************************************************************************************
calcGammaCorrectionValue()
**************************************************************************************************************************************************/
/*! \brief calculates the gamma correction value
* \details calculates the gamma correction value with the help of a small table
* and very efficient shift calculation
* param[in] ValueLinear 7 bit unsigned (0..127)
* \return exponential value (approx. 1.0443^x) (1..255)
*************************************************************************************************************************************************/
static byte calcGamma7CorrectionValue(byte ValueLinear) {
Gamma7TableElementType exponent{getGamma7TableElement(ValueLinear % Gamma7TableNumberOfValues)};
byte Log2OfResolution = numberOfBits<byte>();
return exponent >> ((Log2OfResolution - 1u) - (ValueLinear / Gamma7TableNumberOfValues));
/* About 1.0443^Step - 2^(1/16) per step - from 1 at step 0 to 255 at step 127. */
static byte toExponential(byte Step) {
return static_cast<byte>(getTopDoublingElement(Step % ValuesPerDoubling) >> toShift(Step));
}

static constexpr byte toStep(byte ValueLinear) { return static_cast<byte>(ValueLinear / 2u); }

/******************************************************************************************************************************************************
* P U B L I C F U N C T I O N S
******************************************************************************************************************************************************/
public:
constexpr GammaCorrection() { }


// get methods

// set methods

// methods
static byte calcCorrectedValue(byte ValueLinear) { return calcGamma7CorrectionValue(ValueLinear / 2u); }
/* 0 to 0, everything else onto the curve: see the file header for why 0 is answered
apart rather than by subtracting 1 from every result, which would take 255 down to
254 as well. */
static byte calcCorrectedValue(byte ValueLinear) {
if(ValueLinear == 0u) { return 0u; }
return toExponential(toStep(ValueLinear));
}
};

#endif
Expand Down
1 change: 1 addition & 0 deletions platform/simulator/tests/WordclockTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
int main()
{
testColourDimmingAcrossByteRange();
testGammaCorrectionCurve();
testSnakeLifetime();
testDisplayManagerLatch();
testShowNowProcedures();
Expand Down
1 change: 1 addition & 0 deletions platform/simulator/tests/cases.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

/* display_test.cpp */
void testColourDimmingAcrossByteRange();
void testGammaCorrectionCurve();
void testDisplayManagerLatch();
void testFadeDimsAndComesBack();
void testDisplayOffAndOnAgain();
Expand Down
22 changes: 22 additions & 0 deletions platform/simulator/tests/cases/display_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "DisplayCharacters.h"
#include "DisplayColor.h"
#include "DisplayManager.h"
#include "GammaCorrection.h"
#include "Pixels.h"
#include "RealTimeClock.h"

Expand All @@ -36,6 +37,27 @@ void testColourDimmingAcrossByteRange()
expect(color.getColorRed() == 255u, "dimming must preserve the configured colour");
}

/* The curve's ends, its shape and a value from each end of the shift range. The middle
ones are what the construction gives directly: one stored value, shifted once per
doubling below the top. */
void testGammaCorrectionCurve()
{
expect(GammaCorrection::calcCorrectedValue(0u) == 0u, "a setting of 0 must stay 0, not the faintest light");
expect(GammaCorrection::calcCorrectedValue(1u) == 1u, "the first step above 0 must be the faintest light there is");
expect(GammaCorrection::calcCorrectedValue(255u) == 255u, "the full setting must stay full");

bool rising = true;
for(unsigned value = 1u; value <= 255u; ++value) {
rising = rising && GammaCorrection::calcCorrectedValue(static_cast<byte>(value - 1u)) <= GammaCorrection::calcCorrectedValue(static_cast<byte>(value));
}
expect(rising, "the curve must never fall, not even where one doubling hands over to the next");

expect(GammaCorrection::calcCorrectedValue(31u) == 1u && GammaCorrection::calcCorrectedValue(32u) == 2u,
"the first doubling must end at setting 31 and the second begin at 32");
expect(GammaCorrection::calcCorrectedValue(128u) == 16u, "half the setting must be 133 shifted down three doublings");
expect(GammaCorrection::calcCorrectedValue(254u) == 255u, "the last step must be the top of the stored doubling, unshifted");
}

/* How much light the whole display is putting out, which is what a fade changes and a
redraw does not. */
uint32_t totalIntensity(const PixelBufferType& Pixels)
Expand Down
Loading