Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Always on display #1869

Merged
merged 15 commits into from
Aug 5, 2024
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: 112 additions & 12 deletions src/components/brightness/BrightnessController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,138 @@
#include <hal/nrf_gpio.h>
#include "displayapp/screens/Symbols.h"
#include "drivers/PinMap.h"
#include <libraries/delay/nrf_delay.h>
using namespace Pinetime::Controllers;

namespace {
// reinterpret_cast is not constexpr so this is the best we can do
static NRF_RTC_Type* const RTC = reinterpret_cast<NRF_RTC_Type*>(NRF_RTC2_BASE);
}

void BrightnessController::Init() {
nrf_gpio_cfg_output(PinMap::LcdBacklightLow);
nrf_gpio_cfg_output(PinMap::LcdBacklightMedium);
nrf_gpio_cfg_output(PinMap::LcdBacklightHigh);

nrf_gpio_pin_clear(PinMap::LcdBacklightLow);
nrf_gpio_pin_clear(PinMap::LcdBacklightMedium);
nrf_gpio_pin_clear(PinMap::LcdBacklightHigh);

static_assert(timerFrequency == 32768, "Change the prescaler below");
RTC->PRESCALER = 0;
// CC1 switches the backlight on (pin transitions from high to low) and resets the counter to 0
RTC->CC[1] = timerPeriod;
// Enable compare events for CC0,CC1
RTC->EVTEN = 0b0000'0000'0000'0011'0000'0000'0000'0000;
// Disable all interrupts
RTC->INTENCLR = 0b0000'0000'0000'1111'0000'0000'0000'0011;
Set(level);
}

void BrightnessController::ApplyBrightness(uint16_t rawBrightness) {
// The classic off, low, medium, high brightnesses are at {0, timerPeriod, timerPeriod*2, timerPeriod*3}
// These brightness levels do not use PWM: they only set/clear the corresponding pins
// Any brightness level between the above levels is achieved with efficient RTC based PWM on the next pin up
// E.g 2.5*timerPeriod corresponds to medium brightness with 50% PWM on the high pin
// Note: Raw brightness does not necessarily correspond to a linear perceived brightness

uint8_t pin;
if (rawBrightness > 2 * timerPeriod) {
rawBrightness -= 2 * timerPeriod;
pin = PinMap::LcdBacklightHigh;
} else if (rawBrightness > timerPeriod) {
rawBrightness -= timerPeriod;
pin = PinMap::LcdBacklightMedium;
} else {
pin = PinMap::LcdBacklightLow;
}
if (rawBrightness == timerPeriod || rawBrightness == 0) {
if (lastPin != UNSET) {
RTC->TASKS_STOP = 1;
nrf_delay_us(rtcStopTime);
nrf_ppi_channel_disable(ppiBacklightOff);
nrf_ppi_channel_disable(ppiBacklightOn);
nrfx_gpiote_out_uninit(lastPin);
nrf_gpio_cfg_output(lastPin);
}
lastPin = UNSET;
if (rawBrightness == 0) {
nrf_gpio_pin_set(pin);
} else {
nrf_gpio_pin_clear(pin);
}
} else {
// If the pin on which we are doing PWM is changing
// Disable old PWM channel (if exists) and set up new one
if (lastPin != pin) {
if (lastPin != UNSET) {
RTC->TASKS_STOP = 1;
nrf_delay_us(rtcStopTime);
nrf_ppi_channel_disable(ppiBacklightOff);
nrf_ppi_channel_disable(ppiBacklightOn);
nrfx_gpiote_out_uninit(lastPin);
nrf_gpio_cfg_output(lastPin);
}
nrfx_gpiote_out_config_t gpioteCfg = {.action = NRF_GPIOTE_POLARITY_TOGGLE,
.init_state = NRF_GPIOTE_INITIAL_VALUE_LOW,
.task_pin = true};
APP_ERROR_CHECK(nrfx_gpiote_out_init(pin, &gpioteCfg));
nrfx_gpiote_out_task_enable(pin);
nrf_ppi_channel_endpoint_setup(ppiBacklightOff,
reinterpret_cast<uint32_t>(&RTC->EVENTS_COMPARE[0]),
nrfx_gpiote_out_task_addr_get(pin));
nrf_ppi_channel_endpoint_setup(ppiBacklightOn,
reinterpret_cast<uint32_t>(&RTC->EVENTS_COMPARE[1]),
nrfx_gpiote_out_task_addr_get(pin));
nrf_ppi_fork_endpoint_setup(ppiBacklightOn, reinterpret_cast<uint32_t>(&RTC->TASKS_CLEAR));
nrf_ppi_channel_enable(ppiBacklightOff);
nrf_ppi_channel_enable(ppiBacklightOn);
} else {
// If the pin used for PWM isn't changing, we only need to set the pin state to the initial value (low)
RTC->TASKS_STOP = 1;
nrf_delay_us(rtcStopTime);
// Due to errata 20,179 and the intricacies of RTC timing, keep it simple: override the pin state
nrfx_gpiote_out_task_force(pin, false);
}
// CC0 switches the backlight off (pin transitions from low to high)
RTC->CC[0] = rawBrightness;
RTC->TASKS_CLEAR = 1;
RTC->TASKS_START = 1;
lastPin = pin;
}
switch (pin) {
case PinMap::LcdBacklightHigh:
nrf_gpio_pin_clear(PinMap::LcdBacklightLow);
nrf_gpio_pin_clear(PinMap::LcdBacklightMedium);
break;
case PinMap::LcdBacklightMedium:
nrf_gpio_pin_clear(PinMap::LcdBacklightLow);
nrf_gpio_pin_set(PinMap::LcdBacklightHigh);
break;
case PinMap::LcdBacklightLow:
nrf_gpio_pin_set(PinMap::LcdBacklightMedium);
nrf_gpio_pin_set(PinMap::LcdBacklightHigh);
}
}

void BrightnessController::Set(BrightnessController::Levels level) {
this->level = level;
switch (level) {
default:
case Levels::High:
nrf_gpio_pin_clear(PinMap::LcdBacklightLow);
nrf_gpio_pin_clear(PinMap::LcdBacklightMedium);
nrf_gpio_pin_clear(PinMap::LcdBacklightHigh);
ApplyBrightness(3 * timerPeriod);
break;
case Levels::Medium:
nrf_gpio_pin_clear(PinMap::LcdBacklightLow);
nrf_gpio_pin_clear(PinMap::LcdBacklightMedium);
nrf_gpio_pin_set(PinMap::LcdBacklightHigh);
ApplyBrightness(2 * timerPeriod);
break;
case Levels::Low:
nrf_gpio_pin_clear(PinMap::LcdBacklightLow);
nrf_gpio_pin_set(PinMap::LcdBacklightMedium);
nrf_gpio_pin_set(PinMap::LcdBacklightHigh);
ApplyBrightness(timerPeriod);
break;
case Levels::AlwaysOn:
ApplyBrightness(timerPeriod / 10);
break;
case Levels::Off:
nrf_gpio_pin_set(PinMap::LcdBacklightLow);
nrf_gpio_pin_set(PinMap::LcdBacklightMedium);
nrf_gpio_pin_set(PinMap::LcdBacklightHigh);
ApplyBrightness(0);
break;
}
}
Expand Down
24 changes: 23 additions & 1 deletion src/components/brightness/BrightnessController.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

#include <cstdint>

#include "nrf_ppi.h"
#include "nrfx_gpiote.h"

namespace Pinetime {
namespace Controllers {
class BrightnessController {
public:
enum class Levels { Off, Low, Medium, High };
enum class Levels { Off, AlwaysOn, Low, Medium, High };
void Init();

void Set(Levels level);
Expand All @@ -20,6 +23,25 @@ namespace Pinetime {

private:
Levels level = Levels::High;
static constexpr uint8_t UNSET = UINT8_MAX;
JF002 marked this conversation as resolved.
Show resolved Hide resolved
uint8_t lastPin = UNSET;
// Maximum time (μs) it takes for the RTC to fully stop
static constexpr uint8_t rtcStopTime = 46;
// Frequency of timer used for PWM (Hz)
static constexpr uint16_t timerFrequency = 32768;
// Backlight PWM frequency (Hz)
static constexpr uint16_t pwmFreq = 1000;
// Wraparound point in timer ticks
// Defines the number of brightness levels between each pin
static constexpr uint16_t timerPeriod = timerFrequency / pwmFreq;
// Warning: nimble reserves some PPIs
// https://github.com/InfiniTimeOrg/InfiniTime/blob/034d83fe6baf1ab3875a34f8cee387e24410a824/src/libs/mynewt-nimble/nimble/drivers/nrf52/src/ble_phy.c#L53
// SpiMaster uses PPI 0 for an erratum workaround
// Channel 1, 2 should be free to use
static constexpr nrf_ppi_channel_t ppiBacklightOn = NRF_PPI_CHANNEL1;
static constexpr nrf_ppi_channel_t ppiBacklightOff = NRF_PPI_CHANNEL2;

void ApplyBrightness(uint16_t val);
};
}
}
19 changes: 18 additions & 1 deletion src/components/settings/Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,21 @@ namespace Pinetime {
return settings.screenTimeOut;
};

bool GetAlwaysOnDisplay() const {
return settings.alwaysOnDisplay && GetNotificationStatus() != Notification::Sleep;
};

void SetAlwaysOnDisplaySetting(bool state) {
if (state != settings.alwaysOnDisplay) {
settingsChanged = true;
}
settings.alwaysOnDisplay = state;
}

bool GetAlwaysOnDisplaySetting() const {
return settings.alwaysOnDisplay;
}

void SetShakeThreshold(uint16_t thresh) {
if (settings.shakeWakeThreshold != thresh) {
settings.shakeWakeThreshold = thresh;
Expand Down Expand Up @@ -286,13 +301,15 @@ namespace Pinetime {
private:
Pinetime::Controllers::FS& fs;

static constexpr uint32_t settingsVersion = 0x0007;
static constexpr uint32_t settingsVersion = 0x0008;

struct SettingsData {
uint32_t version = settingsVersion;
uint32_t stepsGoal = 10000;
uint32_t screenTimeOut = 15000;

bool alwaysOnDisplay = false;

ClockType clockType = ClockType::H24;
WeatherFormat weatherFormat = WeatherFormat::Metric;
Notification notificationStatus = Notification::On;
Expand Down
80 changes: 73 additions & 7 deletions src/displayapp/DisplayApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,36 @@ void DisplayApp::InitHw() {
lcd.Init();
}

TickType_t DisplayApp::CalculateSleepTime() {
TickType_t ticksElapsed = xTaskGetTickCount() - alwaysOnStartTime;
// Divide both the numerator and denominator by 8 to increase the number of ticks (frames) before the overflow tick is reached
TickType_t elapsedTarget = ROUNDED_DIV((configTICK_RATE_HZ / 8) * alwaysOnTickCount * alwaysOnRefreshPeriod, 1000 / 8);
// ROUNDED_DIV overflows when numerator + (denominator floordiv 2) > uint32 max
// in this case around 9 hours
constexpr TickType_t overflowTick = (UINT32_MAX - (1000 / 16)) / ((configTICK_RATE_HZ / 8) * alwaysOnRefreshPeriod);

// Assumptions

// Tick rate is multiple of 8
// Needed for division trick above
static_assert(configTICK_RATE_HZ % 8 == 0);

// Local tick count must always wraparound before the system tick count does
// As a static assert we can use 64 bit ints and therefore dodge overflows
// Always on overflow time (ms) < system tick overflow time (ms)
static_assert((uint64_t) overflowTick * (uint64_t) alwaysOnRefreshPeriod < (uint64_t) UINT32_MAX * 1000ULL / configTICK_RATE_HZ);

if (alwaysOnTickCount == overflowTick) {
alwaysOnTickCount = 0;
alwaysOnStartTime = xTaskGetTickCount();
}
if (elapsedTarget > ticksElapsed) {
return elapsedTarget - ticksElapsed;
} else {
return 0;
}
}

void DisplayApp::Refresh() {
auto LoadPreviousScreen = [this]() {
FullRefreshDirections returnDirection;
Expand Down Expand Up @@ -203,7 +233,29 @@ void DisplayApp::Refresh() {
TickType_t queueTimeout;
switch (state) {
case States::Idle:
queueTimeout = portMAX_DELAY;
if (settingsController.GetAlwaysOnDisplay()) {
if (!currentScreen->IsRunning()) {
LoadPreviousScreen();
}
// Check we've slept long enough
// Might not be true if the loop received an event
// If not true, then wait that amount of time
queueTimeout = CalculateSleepTime();
if (queueTimeout == 0) {
// Only advance the tick count when LVGL is done
// Otherwise keep running the task handler while it still has things to draw
// Note: under high graphics load, LVGL will always have more work to do
if (lv_task_handler() > 0) {
// Drop frames that we've missed if drawing/event handling took way longer than expected
while (queueTimeout == 0) {
alwaysOnTickCount += 1;
queueTimeout = CalculateSleepTime();
}
};
}
} else {
queueTimeout = portMAX_DELAY;
}
break;
case States::Running:
if (!currentScreen->IsRunning()) {
Expand Down Expand Up @@ -234,20 +286,34 @@ void DisplayApp::Refresh() {
case Messages::DimScreen:
DimScreen();
break;
case Messages::RestoreBrightness:
RestoreBrightness();
break;
case Messages::GoToSleep:
while (brightnessController.Level() != Controllers::BrightnessController::Levels::Off) {
while (brightnessController.Level() != Controllers::BrightnessController::Levels::Low) {
brightnessController.Lower();
vTaskDelay(100);
}
lcd.Sleep();
// Don't actually turn off the display for AlwaysOn mode
if (settingsController.GetAlwaysOnDisplay()) {
brightnessController.Set(Controllers::BrightnessController::Levels::AlwaysOn);
lcd.LowPowerOn();
// Record idle entry time
alwaysOnTickCount = 0;
alwaysOnStartTime = xTaskGetTickCount();
} else {
brightnessController.Set(Controllers::BrightnessController::Levels::Off);
lcd.Sleep();
}
PushMessageToSystemTask(Pinetime::System::Messages::OnDisplayTaskSleeping);
state = States::Idle;
break;
case Messages::NotifyDeviceActivity:
lv_disp_trig_activity(nullptr);
break;
case Messages::GoToRunning:
lcd.Wakeup();
if (settingsController.GetAlwaysOnDisplay()) {
lcd.LowPowerOff();
} else {
lcd.Wakeup();
}
lv_disp_trig_activity(nullptr);
ApplyBrightness();
state = States::Running;
Expand Down
7 changes: 7 additions & 0 deletions src/displayapp/DisplayApp.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ namespace Pinetime {
Utility::StaticStack<FullRefreshDirections, returnAppStackSize> appStackDirections;

bool isDimmed = false;

TickType_t CalculateSleepTime();
TickType_t alwaysOnTickCount;
TickType_t alwaysOnStartTime;
// If this is to be changed, make sure the actual always on refresh rate is changed
// by configuring the LCD refresh timings
static constexpr uint32_t alwaysOnRefreshPeriod = 500;
};
}
}
2 changes: 1 addition & 1 deletion src/displayapp/Messages.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Pinetime {
TimerDone,
BleFirmwareUpdateStarted,
DimScreen,
RestoreBrightness,
NotifyDeviceActivity,
ShowPairingKey,
AlarmTriggered,
Chime,
Expand Down
2 changes: 2 additions & 0 deletions src/displayapp/screens/FlashLight.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ namespace {
FlashLight::FlashLight(System::SystemTask& systemTask, Controllers::BrightnessController& brightnessController)
: systemTask {systemTask}, brightnessController {brightnessController} {

previousBrightnessLevel = brightnessController.Level();
brightnessController.Set(Controllers::BrightnessController::Levels::Low);

flashLight = lv_label_create(lv_scr_act(), nullptr);
Expand Down Expand Up @@ -52,6 +53,7 @@ FlashLight::FlashLight(System::SystemTask& systemTask, Controllers::BrightnessCo
FlashLight::~FlashLight() {
lv_obj_clean(lv_scr_act());
lv_obj_set_style_local_bg_color(lv_scr_act(), LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLACK);
brightnessController.Set(previousBrightnessLevel);
systemTask.PushMessage(Pinetime::System::Messages::EnableSleeping);
}

Expand Down
1 change: 1 addition & 0 deletions src/displayapp/screens/FlashLight.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace Pinetime {
Controllers::BrightnessController& brightnessController;

Controllers::BrightnessController::Levels brightnessLevel = Controllers::BrightnessController::Levels::High;
Controllers::BrightnessController::Levels previousBrightnessLevel;

lv_obj_t* flashLight;
lv_obj_t* backgroundAction;
Expand Down
Loading
Loading