Skip to content

Commit

Permalink
Make time provider independent of PWM abstraction (#133)
Browse files Browse the repository at this point in the history
move time providing function to separate class

Since a platform can have multiple PWM-HALs (e.g. the one provided by
the platform, and additional ones like e.g. a PCA9685 HAL), but only
a single time provider, the code was split.

Each platform now has it's own time providing class, like e.g.
`class ArduinoClock`. An implementation consists of a
single static method: `static uint32_t millis()` that returns the
current time in millis.
  • Loading branch information
jandelgado authored Jan 2, 2025
1 parent defcc25 commit 394e176
Show file tree
Hide file tree
Showing 19 changed files with 153 additions and 117 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
pull_request:
branches:
- master
- version_5_rc0

name: run tests
jobs:
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ all: phony

lint: phony
cpplint --filter -readability/check \
--linelength=100\
--exclude test/catch2 \
--extensions=cpp,h,ino $(shell find . -maxdepth 3 \( ! -regex '.*/\..*' \) \
-type f -a \( -name "*\.cpp" -o -name "*\.h" -o -name "*\.ino" \) )
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -676,12 +676,12 @@ the `File` > `Examples` > `JLed` menu.

### Support new hardware

JLed uses a very thin hardware abstraction layer (hal) to abstract access to
the actual MCU/framework used (e.g. ESP32, ESP8266). The hal object encapsulate
access to the GPIO and time functionality of the MCU under the framework being
used. During the unit test, mocked hal instances are used, enabling tests to
check the generated output. The [Custom HAL project](examples/custom_hal)
provides an example for a user define HAL.
JLed uses a very thin hardware abstraction layer (HAL) to abstract access to
the actual MCU/framework used (e.g. ESP32, ESP8266). The HAL encapsulates
access to the GPIO and clock functionality of the MCU under the framework being
used. During the unit tests, mocked HAL instances are used, enabling tests to
check the generated output. The [Custom HAL example](examples/custom_hal)
provides an example for a user defined HAL.

## Unit tests

Expand Down
27 changes: 12 additions & 15 deletions examples/custom_hal/custom_hal.ino
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
// JLed custom HAL example.
// Copyright 2019 by Jan Delgado. All rights reserved.
// Copyright 2019,2025 by Jan Delgado. All rights reserved.
// https://github.com/jandelgado/jled

// we include jled_base.h instead of "jled.h" since we define our own JLed
// class using our custom HAL.
#include <jled_base.h>
#include <jled.h>

// a custom HAL for the Arduino, inverting output and ticking with half
// the speed. In general, a JLed HAL class must satisfy the following
// interface:
// a custom PWM HAL for the Arduino platform, inverting output.
// In general, a JLed HAL class must satisfy the following interface:
//
// class JledHal {
// class JLedHal {
// public:
// JledHal(PinType pin);
// JLedHal(PinType pin);
// void analogWrite(uint8_t val) const;
// uint32_t millis() const;
// }
//
class CustomHal {
Expand All @@ -32,19 +28,20 @@ class CustomHal {
::analogWrite(pin_, 255 - val);
}

uint32_t millis() const { return ::millis() >> 1; }

private:
mutable bool setup_ = false;
PinType pin_;
};

class JLed : public jled::TJLed<CustomHal, JLed> {
using jled::TJLed<CustomHal, JLed>::TJLed;

// a custom JLed class using our CustomHal and the default clock defined
// for the platform.
class CustomJLed : public jled::TJLed<CustomHal, jled::JLedClockType, CustomJLed> {
using jled::TJLed<CustomHal, jled::JLedClockType, CustomJLed>::TJLed;
};

// uses above defined CustomHal
auto led = JLed(LED_BUILTIN).Blink(1000, 1000).Repeat(5);
auto led = CustomJLed(LED_BUILTIN).Blink(1000, 1000).Repeat(5);

void setup() {}

Expand Down
8 changes: 6 additions & 2 deletions src/arduino_hal.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ class ArduinoHal {
::analogWrite(pin_, val);
}

uint32_t millis() const { return ::millis(); }

private:
mutable bool setup_ = false;
PinType pin_;
};

class ArduinoClock {
public:
static uint32_t millis() { return ::millis(); }
};

} // namespace jled
#endif // SRC_ARDUINO_HAL_H_
16 changes: 10 additions & 6 deletions src/esp32_hal.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,22 +125,26 @@ class Esp32Hal {
void analogWrite(uint8_t duty) const {
// Fixing if all bits in resolution is set = LEDC FULL ON
const uint32_t _duty = (duty == (1 << kLedcTimerResolution) - 1)
? 1 << kLedcTimerResolution
: duty;
? 1 << kLedcTimerResolution
: duty;

ledc_set_duty(kLedcSpeedMode, chan_, _duty);
ledc_update_duty(kLedcSpeedMode, chan_);
}

uint32_t millis() const {
return static_cast<uint32_t>(esp_timer_get_time() / 1000ULL);
}

PinType chan() const { return chan_; }

private:
static Esp32ChanMapper chanMapper_;
ledc_channel_t chan_;
};

class Esp32Clock {
public:
static uint32_t millis() {
return static_cast<uint32_t>(esp_timer_get_time() / 1000ULL);
}
};

} // namespace jled
#endif // SRC_ESP32_HAL_H_
9 changes: 8 additions & 1 deletion src/esp8266_hal.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ class Esp8266Hal {
// ESP8266 uses 10bit PWM range per default, scale value up
::analogWrite(pin_, Esp8266Hal::ScaleTo10Bit(val));
}
uint32_t millis() const { return ::millis(); }

protected:
// scale an 8bit value to 10bit: 0 -> 0, ..., 255 -> 1023,
Expand All @@ -49,5 +48,13 @@ class Esp8266Hal {
private:
PinType pin_;
};

class Esp8266Clock {
public:
static uint32_t millis() {
return ::millis();
}
};

} // namespace jled
#endif // SRC_ESP8266_HAL_H_
18 changes: 9 additions & 9 deletions src/jled.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,29 @@

#ifdef PICO_SDK_VERSION_MAJOR
#include "pico_hal.h" // NOLINT
namespace jled {using JLedHalType = PicoHal;}
namespace jled {using JLedHalType = PicoHal; using JLedClockType = PicoClock;}
#elif defined(__MBED__) && !defined(ARDUINO_API_VERSION)
#include "mbed_hal.h" // NOLINT
namespace jled {using JLedHalType = MbedHal;}
namespace jled {using JLedHalType = MbedHal; using JLedClockType = MbedClock;}
#elif defined(ESP32)
#include "esp32_hal.h" // NOLINT
namespace jled {using JLedHalType = Esp32Hal;}
namespace jled {using JLedHalType = Esp32Hal; using JLedClockType = Esp32Clock;}
#elif defined(ESP8266)
#include "esp8266_hal.h" // NOLINT
namespace jled {using JLedHalType = Esp8266Hal;}
namespace jled {using JLedHalType = Esp8266Hal; using JLedClockType = Esp8266Clock;}
#else
#include "arduino_hal.h" // NOLINT
namespace jled {using JLedHalType = ArduinoHal;}
namespace jled {using JLedHalType = ArduinoHal; using JLedClockType = ArduinoClock;}
#endif

namespace jled {
class JLed : public TJLed<JLedHalType, JLed> {
using TJLed<JLedHalType, JLed>::TJLed;
class JLed : public TJLed<JLedHalType, JLedClockType, JLed> {
using TJLed<JLedHalType, JLedClockType, JLed>::TJLed;
};

// a group of JLed objects which can be controlled simultanously
class JLedSequence : public TJLedSequence<JLed, JLedSequence> {
using TJLedSequence<JLed, JLedSequence>::TJLedSequence;
class JLedSequence : public TJLedSequence<JLed, JLedClockType, JLedSequence> {
using TJLedSequence<JLed, JLedClockType, JLedSequence>::TJLedSequence;
};

}; // namespace jled
Expand Down
10 changes: 5 additions & 5 deletions src/jled_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ class CandleBrightnessEvaluator : public CloneableBrightnessEvaluator {
}
};

template <typename HalType, typename B>
template <typename HalType, typename Clock, typename B>
class TJLed {
protected:
// pointer to a (user defined) brightness evaluator.
Expand Down Expand Up @@ -218,7 +218,7 @@ class TJLed {

TJLed(const TJLed& rLed) : hal_{rLed.hal_} { *this = rLed; }

B& operator=(const TJLed<HalType, B>& rLed) {
B& operator=(const TJLed<HalType, Clock, B>& rLed) {
state_ = rLed.state_;
bLowActive_ = rLed.bLowActive_;
minBrightness_ = rLed.minBrightness_;
Expand Down Expand Up @@ -409,7 +409,7 @@ class TJLed {
// | func(t) |
// |<- num_repetitions times ->
bool Update(int16_t* pLast = nullptr) {
return Update(hal_.millis(), pLast);
return Update(Clock::millis(), pLast);
}

bool Update(uint32_t t, int16_t* pLast = nullptr) {
Expand Down Expand Up @@ -534,14 +534,14 @@ T* ptr(T* obj) {

// a group of JLed objects which can be controlled simultanously, in parallel
// or sequentially.
template <typename L, typename B>
template <typename L, typename Clock, typename B>
class TJLedSequence {
protected:
// update all leds parallel. Returns true while any of the JLeds is
// active, else false
bool UpdateParallel() {
auto result = false;
uint32_t t = ptr(leds_[0])->Hal().millis();
uint32_t t = Clock::millis();
for (auto i = 0u; i < n_; i++) {
result |= ptr(leds_[i])->Update(t);
}
Expand Down
12 changes: 8 additions & 4 deletions src/mbed_hal.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,18 @@ class MbedHal {
return *this;
}

uint32_t millis() const {
return Kernel::Clock::now().time_since_epoch().count();
}

private:
PinType pin_;
mutable PwmOut* pwmout_ = nullptr;
};

class MbedClock {
public:
static uint32_t millis() {
return Kernel::Clock::now().time_since_epoch().count();
}
};

} // namespace jled
#endif // __MBED__
#endif // SRC_MBED_HAL_H_
8 changes: 6 additions & 2 deletions src/pico_hal.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,15 @@ class PicoHal {
static_cast<uint32_t>(DUTY_100_PCT / 255) * val);
}

uint32_t millis() const { return to_ms_since_boot(get_absolute_time()); }

private:
uint slice_num_, channel_;
uint32_t top_ = 0;
};

class PicoClock {
public:
static uint32_t millis() { return to_ms_since_boot(get_absolute_time()); }
};

} // namespace jled
#endif // SRC_PICO_HAL_H_
20 changes: 10 additions & 10 deletions test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ CFLAGS=-std=c++14 -c -Wall -Wextra -I. -I../src -I./esp-idf \
-fno-omit-frame-pointer -fno-optimize-sibling-calls \
$(OPT)

LDFLAGS=-fprofile-arcs -ftest-coverage
LDFLAGS=-fprofile-arcs -ftest-coverage

CATCH=catch2/catch_amalgamated.cpp

TEST_ARDUINO_MOCK_SRC=Arduino.cpp test_arduino_mock.cpp test_main.cpp ${CATCH}
TEST_ARDUINO_MOCK_OBJECTS=$(TEST_ARDUINO_MOCK_SRC:.cpp=.o)
TEST_ARDUINO_MOCK_OBJECTS=$(TEST_ARDUINO_MOCK_SRC:.cpp=.o)

TEST_JLED_SRC=Arduino.cpp test_jled.cpp test_main.cpp ../src/jled_base.cpp ${CATCH}
TEST_JLED_OBJECTS=$(TEST_JLED_SRC:.cpp=.o)
TEST_JLED_OBJECTS=$(TEST_JLED_SRC:.cpp=.o)

TEST_JLED_SEQUENCE_SRC=Arduino.cpp test_jled_sequence.cpp test_main.cpp ../src/jled_base.cpp ${CATCH}
TEST_JLED_SEQUENCE_OBJECTS=$(TEST_JLED_SEQUENCE_SRC:.cpp=.o)
Expand Down Expand Up @@ -45,31 +45,31 @@ all: bin bin/test_arduino_mock \
bin/test_example_morse

bin/test_arduino_mock: $(TEST_ARDUINO_MOCK_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_ARDUINO_MOCK_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_ARDUINO_MOCK_OBJECTS)

bin/test_jled: $(TEST_JLED_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_JLED_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_JLED_OBJECTS)

bin/test_jled_sequence: $(TEST_JLED_SEQUENCE_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_JLED_SEQUENCE_OBJECTS)

bin/test_esp32_hal: CFLAGS += -DESP32
bin/test_esp32_hal: $(TEST_ESP32_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_ESP32_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_ESP32_OBJECTS)

bin/test_esp8266_hal: $(TEST_ESP8266_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_ESP8266_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_ESP8266_OBJECTS)

bin/test_mbed_hal: CFLAGS += -D__MBED__
bin/test_mbed_hal: $(TEST_MBED_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_MBED_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_MBED_OBJECTS)

bin/test_arduino_hal: $(TEST_ARDUINO_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_ARDUINO_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_ARDUINO_OBJECTS)

bin/test_example_morse: CFLAGS += -I../examples/morse
bin/test_example_morse: $(TEST_MORSE_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_MORSE_OBJECTS)
$(CXX) -o $@ $(LDFLAGS) $(TEST_MORSE_OBJECTS)

coverage: test
lcov --config-file=.lcovrc --directory ../src --directory .. --capture --output-file coverage.lcov --no-external
Expand Down
14 changes: 9 additions & 5 deletions test/hal_mock.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@ class HalMock {
explicit HalMock(PinType pin) : pin_(pin) {}

void analogWrite(uint8_t val) { val_ = val; }
time_t millis() const { return millis_; }

// mock functions
void SetMillis(time_t millis) { millis_ = millis; }
uint8_t Pin() const { return pin_; }
uint8_t Value() const { return val_; }

private:
time_t millis_ = 0;
uint8_t val_ = 0;
PinType pin_ = 0;
};

class TimeMock {
public:
static uint32_t& millis() {
static uint32_t millis_ = 0;
return millis_;
}
static void set_millis(uint32_t t) { TimeMock::millis() = t; }
};

#endif // TEST_HAL_MOCK_H_
Loading

0 comments on commit 394e176

Please sign in to comment.