feat: Add touch coordinate mapping and RTOS task yielding (#2481)
Co-authored-by: Julia Nguyen <julia@uxj.io>
This commit is contained in:
co-authored by
Julia Nguyen
parent
c9188a7347
commit
f42fab1c66
+21
-91
@@ -5,46 +5,11 @@
|
||||
#include <esp_sntp.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
HalClock halClock; // Singleton instance
|
||||
|
||||
// DS3231 register layout (BCD encoded):
|
||||
// 0x00: Seconds (bits 6-4 = tens, bits 3-0 = ones)
|
||||
// 0x01: Minutes (bits 6-4 = tens, bits 3-0 = ones)
|
||||
// 0x02: Hours (bit 6 = 12/24 mode, bits 5-4 = tens, bits 3-0 = ones)
|
||||
|
||||
static uint8_t bcdToDec(uint8_t bcd) { return ((bcd >> 4) * 10) + (bcd & 0x0F); }
|
||||
static uint8_t decToBcd(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); }
|
||||
|
||||
void HalClock::begin() {
|
||||
if (!gpio.deviceIsX3()) {
|
||||
_available = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// I2C is already initialised by HalPowerManager::begin() for X3.
|
||||
// Probe the DS3231 by reading the seconds register.
|
||||
Wire.beginTransmission(I2C_ADDR_DS3231);
|
||||
Wire.write(DS3231_SEC_REG);
|
||||
if (Wire.endTransmission(false) != 0) {
|
||||
LOG_INF("CLK", "DS3231 RTC not found");
|
||||
_available = false;
|
||||
return;
|
||||
}
|
||||
Wire.requestFrom(I2C_ADDR_DS3231, (uint8_t)1);
|
||||
if (Wire.available() < 1) {
|
||||
_available = false;
|
||||
return;
|
||||
}
|
||||
Wire.read(); // discard — just testing connectivity
|
||||
|
||||
_available = true;
|
||||
LOG_INF("CLK", "DS3231 RTC found");
|
||||
|
||||
// Prime the cache with an initial read
|
||||
uint8_t h, m;
|
||||
getTime(h, m);
|
||||
_available = _sdkRtc.begin();
|
||||
LOG_INF("CLK", _available ? "SDK RTC found" : "RTC not found");
|
||||
}
|
||||
|
||||
bool HalClock::getTime(uint8_t& hour, uint8_t& minute) const {
|
||||
@@ -57,44 +22,18 @@ bool HalClock::getTime(uint8_t& hour, uint8_t& minute) const {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read 3 bytes starting at register 0x00: seconds, minutes, hours
|
||||
Wire.beginTransmission(I2C_ADDR_DS3231);
|
||||
Wire.write(DS3231_SEC_REG);
|
||||
if (Wire.endTransmission(false) != 0) {
|
||||
Rtc::DateTime dt;
|
||||
if (!_sdkRtc.now(dt)) {
|
||||
if (!_hasCachedTime) return false;
|
||||
_lastPollMs = now;
|
||||
hour = _cachedHour;
|
||||
minute = _cachedMinute;
|
||||
return true;
|
||||
}
|
||||
Wire.requestFrom(I2C_ADDR_DS3231, (uint8_t)3);
|
||||
if (Wire.available() < 3) {
|
||||
if (!_hasCachedTime) return false;
|
||||
_lastPollMs = now;
|
||||
hour = _cachedHour;
|
||||
minute = _cachedMinute;
|
||||
return true;
|
||||
}
|
||||
|
||||
Wire.read(); // seconds — not needed
|
||||
const uint8_t rawMin = Wire.read();
|
||||
const uint8_t rawHour = Wire.read();
|
||||
|
||||
_cachedMinute = bcdToDec(rawMin & 0x7F);
|
||||
// Handle 12/24h mode: bit 6 high = 12h mode
|
||||
if (rawHour & 0x40) {
|
||||
// 12h mode: bit 5 = PM, bits 4-0 = hours (1-12)
|
||||
uint8_t h12 = bcdToDec(rawHour & 0x1F);
|
||||
bool pm = rawHour & 0x20;
|
||||
if (h12 == 12) h12 = 0;
|
||||
_cachedHour = pm ? (h12 + 12) : h12;
|
||||
} else {
|
||||
// 24h mode: bits 5-0 = hours (0-23)
|
||||
_cachedHour = bcdToDec(rawHour & 0x3F);
|
||||
}
|
||||
_cachedHour = dt.hour;
|
||||
_cachedMinute = dt.minute;
|
||||
_lastPollMs = now;
|
||||
_hasCachedTime = true;
|
||||
|
||||
hour = _cachedHour;
|
||||
minute = _cachedMinute;
|
||||
return true;
|
||||
@@ -127,28 +66,6 @@ bool HalClock::formatTime(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHou
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HalClock::writeTimeToRTC(uint8_t hour, uint8_t minute, uint8_t second) {
|
||||
assert(hour < 24);
|
||||
assert(minute < 60);
|
||||
assert(second < 60);
|
||||
Wire.beginTransmission(I2C_ADDR_DS3231);
|
||||
Wire.write(DS3231_SEC_REG); // Start at register 0x00
|
||||
Wire.write(decToBcd(second)); // 0x00: Seconds
|
||||
Wire.write(decToBcd(minute)); // 0x01: Minutes
|
||||
Wire.write(decToBcd(hour)); // 0x02: Hours (24h mode, bit 6 = 0)
|
||||
if (Wire.endTransmission() != 0) {
|
||||
LOG_ERR("CLK", "Failed to write time to DS3231");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Invalidate cache so next read fetches fresh data
|
||||
_lastPollMs = 0;
|
||||
_cachedHour = hour;
|
||||
_cachedMinute = minute;
|
||||
_hasCachedTime = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HalClock::syncFromNTP() {
|
||||
if (!_available) return false;
|
||||
|
||||
@@ -168,8 +85,21 @@ bool HalClock::syncFromNTP() {
|
||||
struct tm timeinfo;
|
||||
gmtime_r(&now, &timeinfo);
|
||||
|
||||
if (writeTimeToRTC(timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec)) {
|
||||
LOG_INF("CLK", "RTC set to %02d:%02d:%02d UTC", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
||||
Rtc::DateTime dt;
|
||||
dt.year = static_cast<uint16_t>(timeinfo.tm_year + 1900);
|
||||
dt.month = static_cast<uint8_t>(timeinfo.tm_mon + 1);
|
||||
dt.day = static_cast<uint8_t>(timeinfo.tm_mday);
|
||||
dt.hour = static_cast<uint8_t>(timeinfo.tm_hour);
|
||||
dt.minute = static_cast<uint8_t>(timeinfo.tm_min);
|
||||
dt.second = static_cast<uint8_t>(timeinfo.tm_sec);
|
||||
dt.weekday = static_cast<uint8_t>(timeinfo.tm_wday);
|
||||
if (_sdkRtc.set(dt)) {
|
||||
_lastPollMs = 0;
|
||||
_cachedHour = dt.hour;
|
||||
_cachedMinute = dt.minute;
|
||||
_hasCachedTime = true;
|
||||
LOG_INF("CLK", "RTC set to %04u-%02u-%02u %02u:%02u:%02u UTC", dt.year, dt.month, dt.day, dt.hour, dt.minute,
|
||||
dt.second);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
+5
-9
@@ -1,15 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#include "HalGPIO.h"
|
||||
#include <Rtc.h>
|
||||
|
||||
class HalClock;
|
||||
extern HalClock halClock; // Singleton
|
||||
|
||||
class HalClock {
|
||||
bool _available = false;
|
||||
mutable Rtc _sdkRtc;
|
||||
mutable uint8_t _cachedHour = 0;
|
||||
mutable uint8_t _cachedMinute = 0;
|
||||
mutable bool _hasCachedTime = false;
|
||||
@@ -18,10 +17,10 @@ class HalClock {
|
||||
static constexpr unsigned long CLOCK_POLL_MS = 10000; // 10 seconds
|
||||
|
||||
public:
|
||||
// Call after gpio.begin() and powerManager.begin() (I2C already initialised for X3)
|
||||
// Call after BoardConfig has selected the active device.
|
||||
void begin();
|
||||
|
||||
// True if the DS3231 RTC is present on this device
|
||||
// True if an RTC is present on this device
|
||||
bool isAvailable() const { return _available; }
|
||||
|
||||
// Get current hour (0-23) and minute (0-59).
|
||||
@@ -35,14 +34,11 @@ class HalClock {
|
||||
// Returns false if RTC is not available.
|
||||
bool formatTime(char* buf, size_t bufSize, uint8_t utcOffsetQuarterHoursBiased = 48, bool use12Hour = false) const;
|
||||
|
||||
// Sync the DS3231 RTC from an NTP server. Requires WiFi to be connected.
|
||||
// Sync the RTC from an NTP server. Requires WiFi to be connected.
|
||||
// Blocks for up to ~5s while waiting for SNTP response.
|
||||
// Returns true if the RTC was successfully updated.
|
||||
//
|
||||
// Debouncing (skip if already synced once) is enforced by the caller, not here,
|
||||
// so the HAL stays free of any app-layer settings dependency.
|
||||
bool syncFromNTP();
|
||||
|
||||
private:
|
||||
bool writeTimeToRTC(uint8_t hour, uint8_t minute, uint8_t second);
|
||||
};
|
||||
|
||||
@@ -65,6 +65,18 @@ void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen)
|
||||
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
|
||||
}
|
||||
|
||||
void HalDisplay::displayBufferAsync(HalDisplay::RefreshMode mode) {
|
||||
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
|
||||
einkDisplay.requestResync(1);
|
||||
}
|
||||
|
||||
einkDisplay.displayBufferAsyncNoShadow(convertRefreshMode(mode));
|
||||
}
|
||||
|
||||
void HalDisplay::waitRefreshComplete() { einkDisplay.waitRefreshComplete(); }
|
||||
|
||||
bool HalDisplay::supportsAsyncRefresh() const { return einkDisplay.supportsAsyncRefresh(); }
|
||||
|
||||
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
|
||||
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
|
||||
einkDisplay.requestResync(1);
|
||||
|
||||
@@ -39,6 +39,17 @@ class HalDisplay {
|
||||
bool fromProgmem = false) const;
|
||||
|
||||
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||
// Non-blocking refresh (shadow-free): starts the panel waveform and returns
|
||||
// while the panel refreshes on its own. The framebuffer must stay untouched
|
||||
// until waitRefreshComplete(), and the caller must rebuild the differential
|
||||
// baseline before the next differential update (the tiled grayscale cleanup
|
||||
// does). Panels without deferral fall back to a blocking refresh.
|
||||
void displayBufferAsync(RefreshMode mode = RefreshMode::FAST_REFRESH);
|
||||
// Block until a pending deferred refresh completes (no-op when none is).
|
||||
void waitRefreshComplete();
|
||||
// True when displayBufferAsync() genuinely overlaps (panel driver defers);
|
||||
// false where it falls back to a blocking refresh.
|
||||
bool supportsAsyncRefresh() const;
|
||||
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
|
||||
|
||||
// Power management
|
||||
|
||||
+53
-5
@@ -1,5 +1,6 @@
|
||||
#include <HalGPIO.h>
|
||||
#include <Logging.h>
|
||||
#include <PowerManager.h>
|
||||
#include <Preferences.h>
|
||||
#include <SPI.h>
|
||||
#include <Wire.h>
|
||||
@@ -191,15 +192,20 @@ HalGPIO::DeviceType detectDeviceTypeWithFingerprint() {
|
||||
} // namespace
|
||||
|
||||
void HalGPIO::begin() {
|
||||
inputMgr.begin();
|
||||
#if FREEINK_MCU_C3
|
||||
SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS);
|
||||
|
||||
_deviceType = detectDeviceTypeWithFingerprint();
|
||||
BoardConfig::selectDevice(deviceIsX3() ? BoardConfig::Board::XteinkX3 : BoardConfig::Board::XteinkX4);
|
||||
|
||||
if (deviceIsX4()) {
|
||||
pinMode(BAT_GPIO0, INPUT);
|
||||
pinMode(UART0_RXD, INPUT);
|
||||
}
|
||||
#else
|
||||
_deviceType = DeviceType::X4;
|
||||
#endif
|
||||
inputMgr.begin();
|
||||
}
|
||||
|
||||
void HalGPIO::update() {
|
||||
@@ -225,7 +231,44 @@ unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
|
||||
|
||||
unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); }
|
||||
|
||||
bool HalGPIO::hasTouch() const { return inputMgr.hasTouch(); }
|
||||
|
||||
bool HalGPIO::wasTouchTap(float& nx, float& ny) const { return inputMgr.wasTouchTap(nx, ny); }
|
||||
|
||||
bool HalGPIO::wasTouchDown(float& nx, float& ny) const { return inputMgr.wasTouchPressedAt(nx, ny); }
|
||||
|
||||
bool HalGPIO::isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const {
|
||||
return inputMgr.isTouchTapCandidate(nx, ny, heldMs);
|
||||
}
|
||||
|
||||
bool HalGPIO::isTouchHeldAt(float& nx, float& ny) const { return inputMgr.isTouchHeldAt(nx, ny); }
|
||||
|
||||
unsigned long HalGPIO::lastTouchHeldMs() const { return inputMgr.lastTouchHeldMs(); }
|
||||
|
||||
bool HalGPIO::wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const {
|
||||
return inputMgr.wasSwipe(nxStart, nyStart, nxEnd, nyEnd);
|
||||
}
|
||||
|
||||
bool HalGPIO::wasTouchActivity() const { return inputMgr.wasTouchActivity(); }
|
||||
|
||||
void HalGPIO::setSharedConfirmPowerShortPressEmitsPower(const bool enabled) {
|
||||
InputManager::setSharedConfirmPowerShortPressEmitsPower(enabled);
|
||||
}
|
||||
|
||||
bool HalGPIO::isXteinkDevice() const {
|
||||
return BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX3 ||
|
||||
BoardConfig::ACTIVE.board == BoardConfig::Board::XteinkX4;
|
||||
}
|
||||
|
||||
bool HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
|
||||
// Boards without a power button (or M5Paper's latch circuit) cannot verify a
|
||||
// hold; treat the wake as valid.
|
||||
if (BoardConfig::ACTIVE.input.power < 0) {
|
||||
return true;
|
||||
}
|
||||
#if defined(FREEINK_DEVICE_M5PAPER) && FREEINK_DEVICE_M5PAPER
|
||||
return true;
|
||||
#endif
|
||||
if (shortPressAllowed) {
|
||||
// Fast path - no duration check needed
|
||||
return true;
|
||||
@@ -271,8 +314,10 @@ bool HalGPIO::isUsbConnected() const {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// U0RXD/GPIO20 reads HIGH when USB is connected
|
||||
return digitalRead(UART0_RXD) == HIGH;
|
||||
if (BoardConfig::ACTIVE.usbDetect < 0) {
|
||||
return false;
|
||||
}
|
||||
return digitalRead(BoardConfig::ACTIVE.usbDetect) == HIGH;
|
||||
}
|
||||
|
||||
HalGPIO::WakeupReason HalGPIO::getWakeupReason() const {
|
||||
@@ -281,8 +326,11 @@ HalGPIO::WakeupReason HalGPIO::getWakeupReason() const {
|
||||
|
||||
const bool usbConnected = isUsbConnected();
|
||||
|
||||
if ((wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) ||
|
||||
(wakeupCause == ESP_SLEEP_WAKEUP_GPIO && resetReason == ESP_RST_DEEPSLEEP && usbConnected)) {
|
||||
if (resetReason == ESP_RST_DEEPSLEEP &&
|
||||
(wakeupCause == ESP_SLEEP_WAKEUP_GPIO || wakeupCause == ESP_SLEEP_WAKEUP_EXT1)) {
|
||||
return WakeupReason::PowerButton;
|
||||
}
|
||||
if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) {
|
||||
return WakeupReason::PowerButton;
|
||||
}
|
||||
if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_UNKNOWN && usbConnected) {
|
||||
|
||||
@@ -58,6 +58,7 @@ class HalGPIO {
|
||||
// Inline device type helpers for cleaner downstream checks
|
||||
inline bool deviceIsX3() const { return _deviceType == DeviceType::X3; }
|
||||
inline bool deviceIsX4() const { return _deviceType == DeviceType::X4; }
|
||||
bool isXteinkDevice() const;
|
||||
|
||||
// Start button GPIO and setup SPI for screen and SD card
|
||||
void begin();
|
||||
@@ -71,6 +72,15 @@ class HalGPIO {
|
||||
bool wasAnyReleased() const;
|
||||
unsigned long getHeldTime() const;
|
||||
unsigned long getPowerButtonHeldTime() const;
|
||||
bool hasTouch() const;
|
||||
bool wasTouchTap(float& nx, float& ny) const;
|
||||
bool wasTouchDown(float& nx, float& ny) const;
|
||||
bool isTouchTapCandidate(float& nx, float& ny, unsigned long& heldMs) const;
|
||||
bool isTouchHeldAt(float& nx, float& ny) const;
|
||||
unsigned long lastTouchHeldMs() const;
|
||||
bool wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const;
|
||||
bool wasTouchActivity() const;
|
||||
void setSharedConfirmPowerShortPressEmitsPower(bool enabled);
|
||||
|
||||
// Verify power button was held long enough after wakeup.
|
||||
// Returns true if verification succeeded, false if device should return to sleep.
|
||||
|
||||
+35
-50
@@ -1,8 +1,11 @@
|
||||
#include "HalPowerManager.h"
|
||||
|
||||
#include <BoardConfig.h>
|
||||
#include <Logging.h>
|
||||
#include <PowerManager.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_sleep.h>
|
||||
#include <soc/soc_caps.h>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
@@ -11,14 +14,8 @@
|
||||
HalPowerManager powerManager; // Singleton instance
|
||||
|
||||
void HalPowerManager::begin() {
|
||||
if (gpio.deviceIsX3()) {
|
||||
// X3 uses an I2C fuel gauge for battery monitoring.
|
||||
// I2C init must come AFTER gpio.begin() so early hardware detection/probes are finished.
|
||||
Wire.begin(X3_I2C_SDA, X3_I2C_SCL, X3_I2C_FREQ);
|
||||
Wire.setTimeOut(4);
|
||||
_batteryUseI2C = true;
|
||||
} else {
|
||||
pinMode(BAT_GPIO0, INPUT);
|
||||
if (BoardConfig::ACTIVE.batteryAdc >= 0) {
|
||||
pinMode(BoardConfig::ACTIVE.batteryAdc, INPUT);
|
||||
}
|
||||
normalFreq = getCpuFrequencyMhz();
|
||||
modeMutex = xSemaphoreCreateMutex();
|
||||
@@ -61,12 +58,6 @@ void HalPowerManager::setPowerSaving(bool enabled) {
|
||||
}
|
||||
|
||||
void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
|
||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
||||
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
||||
delay(50);
|
||||
gpio.update();
|
||||
}
|
||||
|
||||
#ifdef ENABLE_SERIAL_LOG
|
||||
// Tear down HWCDC so the host sees a clean disconnect and the peripheral
|
||||
// doesn't hold power domains that interfere with USB-powered GPIO wake.
|
||||
@@ -75,53 +66,47 @@ void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
|
||||
logSerial.end();
|
||||
#endif
|
||||
|
||||
// Pre-sleep routines from the original firmware
|
||||
// GPIO13 is connected to battery latch MOSFET, we need to make sure it's low during sleep
|
||||
// Note that this means the MCU will be completely powered off during sleep, including RTC
|
||||
constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13;
|
||||
gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level(GPIO_SPIWP, 0);
|
||||
esp_sleep_config_gpio_isolate();
|
||||
gpio_deep_sleep_hold_en();
|
||||
gpio_hold_en(GPIO_SPIWP);
|
||||
pinMode(InputManager::POWER_BUTTON_PIN, INPUT_PULLUP);
|
||||
// Arm the wakeup trigger *after* the button is released
|
||||
// Note: this is only useful for waking up on USB power. On battery, the MCU will be completely powered off, so the
|
||||
// power button is hard-wired to briefly provide power to the MCU, waking it up regardless of the wakeup source
|
||||
// configuration
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
// Enter Deep Sleep
|
||||
esp_deep_sleep_start();
|
||||
#if !SOC_PM_SUPPORT_EXT1_WAKEUP
|
||||
if (gpio.isXteinkDevice() && !gpio.deviceIsX3()) {
|
||||
// X4 GPIO13 is connected to the battery latch MOSFET. Keeping it low powers
|
||||
// the MCU off on battery, while the SDK wake source still handles USB power.
|
||||
constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13;
|
||||
gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level(GPIO_SPIWP, 0);
|
||||
gpio_hold_en(GPIO_SPIWP);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Cut the gated peripheral rails (touch/SD/EPD on boards like the Sticky) and
|
||||
// hold the enables off through deep sleep — otherwise the GT911 and SD card
|
||||
// stay powered all through "off" and drain the battery. No-op on boards with
|
||||
// no switched rails (X4/X3). Trade-off: no touch-to-wake; wake is the power
|
||||
// button. Must run after display.deepSleep() so the panel controller gets its
|
||||
// deep-sleep command while its rail is still up (enterDeepSleep() in main.cpp
|
||||
// guarantees that ordering).
|
||||
freeink::PowerManager::powerDownRailsForSleep();
|
||||
|
||||
// Waits for the power button to be physically released (so holding it doesn't
|
||||
// immediately wake the device again), then arms the wake source and sleeps.
|
||||
freeink::PowerManager::deepSleepUntilPowerButton();
|
||||
}
|
||||
|
||||
uint16_t HalPowerManager::getBatteryPercentage() const {
|
||||
if (_batteryUseI2C) {
|
||||
static const BatteryMonitor battery;
|
||||
if (BoardConfig::ACTIVE.batteryGauge.gaugeAddr != 0) {
|
||||
const unsigned long now = millis();
|
||||
if (_batteryLastPollMs != 0 && (now - _batteryLastPollMs) < BATTERY_POLL_MS) {
|
||||
return _batteryCachedPercent;
|
||||
}
|
||||
|
||||
// Read SOC directly from I2C fuel gauge (16-bit LE register).
|
||||
// On I2C error, keep last known value to avoid UI jitter/slowdowns.
|
||||
Wire.beginTransmission(I2C_ADDR_BQ27220);
|
||||
Wire.write(BQ27220_SOC_REG);
|
||||
if (Wire.endTransmission(false) != 0) {
|
||||
_batteryLastPollMs = now;
|
||||
return _batteryCachedPercent;
|
||||
}
|
||||
Wire.requestFrom(I2C_ADDR_BQ27220, (uint8_t)2);
|
||||
if (Wire.available() < 2) {
|
||||
_batteryLastPollMs = now;
|
||||
return _batteryCachedPercent;
|
||||
}
|
||||
const uint8_t lo = Wire.read();
|
||||
const uint8_t hi = Wire.read();
|
||||
const uint16_t soc = (hi << 8) | lo;
|
||||
_batteryCachedPercent = soc > 100 ? 100 : soc;
|
||||
_batteryLastPollMs = now;
|
||||
uint16_t percent = 0;
|
||||
if (!battery.readPercentageChecked(percent)) {
|
||||
return _batteryCachedPercent;
|
||||
}
|
||||
_batteryCachedPercent = percent;
|
||||
return _batteryCachedPercent;
|
||||
}
|
||||
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
|
||||
|
||||
// smooth the battery %.
|
||||
if (_batteryCachedPercent == 0) {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <BatteryMonitor.h>
|
||||
#include <InputManager.h>
|
||||
#include <Logging.h>
|
||||
#include <Wire.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include <cassert>
|
||||
@@ -18,8 +17,6 @@ class HalPowerManager {
|
||||
int normalFreq = 0; // MHz
|
||||
bool isLowPower = false;
|
||||
|
||||
// I2C fuel gauge configuration for X3 battery monitoring
|
||||
bool _batteryUseI2C = false; // True if using I2C fuel gauge (X3), false for ADC (X4)
|
||||
mutable int _batteryCachedPercent = 0; // Last read battery percentage (0-100)
|
||||
mutable unsigned long _batteryLastPollMs = 0; // Timestamp of last battery read in milliseconds
|
||||
|
||||
@@ -28,7 +25,11 @@ class HalPowerManager {
|
||||
SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode
|
||||
|
||||
public:
|
||||
static constexpr int LOW_POWER_FREQ = 10; // MHz
|
||||
#if BOARD_HAS_PSRAM
|
||||
static constexpr int LOW_POWER_FREQ = 80; // MHz
|
||||
#else
|
||||
static constexpr int LOW_POWER_FREQ = 10; // MHz
|
||||
#endif
|
||||
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms
|
||||
static constexpr unsigned long BATTERY_POLL_MS = 1500; // ms
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@ void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) {
|
||||
__real_panic_print_backtrace(frame, core);
|
||||
return;
|
||||
}
|
||||
|
||||
#if !__riscv
|
||||
__real_panic_print_backtrace(frame, core);
|
||||
return;
|
||||
#else
|
||||
for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) {
|
||||
panicStack[i].sp = 0;
|
||||
}
|
||||
@@ -65,6 +70,7 @@ void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) {
|
||||
}
|
||||
|
||||
__real_panic_print_backtrace(frame, core);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+28
-93
@@ -4,84 +4,29 @@
|
||||
|
||||
HalTiltSensor halTiltSensor; // Singleton instance
|
||||
|
||||
bool HalTiltSensor::writeReg(uint8_t reg, uint8_t val) const {
|
||||
Wire.beginTransmission(_i2cAddr);
|
||||
Wire.write(reg);
|
||||
Wire.write(val);
|
||||
return Wire.endTransmission() == 0;
|
||||
}
|
||||
|
||||
bool HalTiltSensor::readReg(uint8_t reg, uint8_t* val) const {
|
||||
Wire.beginTransmission(_i2cAddr);
|
||||
Wire.write(reg);
|
||||
if (Wire.endTransmission(false) != 0) {
|
||||
return false;
|
||||
}
|
||||
Wire.requestFrom(_i2cAddr, (uint8_t)1);
|
||||
if (Wire.available() < 1) {
|
||||
return false;
|
||||
}
|
||||
*val = Wire.read();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HalTiltSensor::readGyro(float& gx, float& gy, float& gz) const {
|
||||
Wire.beginTransmission(_i2cAddr);
|
||||
Wire.write(REG_GX_L); // Start reading at Gyro X Low
|
||||
if (Wire.endTransmission(false) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Wire.requestFrom(_i2cAddr, (uint8_t)6);
|
||||
if (Wire.available() < 6) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto readInt16 = [&]() -> int16_t {
|
||||
const uint8_t lo = Wire.read();
|
||||
const uint8_t hi = Wire.read();
|
||||
return static_cast<int16_t>((hi << 8) | lo);
|
||||
};
|
||||
|
||||
// If Full Scale is ±512 dps, the scale factor is 32768 / 512 = 64 LSB/dps
|
||||
constexpr float SCALE = 1.0f / 64.0f;
|
||||
gx = readInt16() * SCALE;
|
||||
gy = readInt16() * SCALE;
|
||||
gz = readInt16() * SCALE;
|
||||
Imu::Sample sample;
|
||||
if (!_sdkImu.read(sample)) return false;
|
||||
gx = sample.gx;
|
||||
gy = sample.gy;
|
||||
gz = sample.gz;
|
||||
return true;
|
||||
}
|
||||
|
||||
void HalTiltSensor::begin() {
|
||||
if (!gpio.deviceIsX3()) {
|
||||
_available = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Try primary address, then alternate
|
||||
uint8_t whoami = 0;
|
||||
_i2cAddr = I2C_ADDR_QMI8658;
|
||||
if (!readReg(QMI8658_WHO_AM_I_REG, &whoami) || whoami != QMI8658_WHO_AM_I_VALUE) {
|
||||
_i2cAddr = I2C_ADDR_QMI8658_ALT;
|
||||
if (!readReg(QMI8658_WHO_AM_I_REG, &whoami) || whoami != QMI8658_WHO_AM_I_VALUE) {
|
||||
LOG_ERR("GYR", "QMI8658 IMU not found");
|
||||
_available = false;
|
||||
return;
|
||||
_available = _sdkImu.begin();
|
||||
if (_available) {
|
||||
_initMs = millis();
|
||||
_lastPollMs = millis();
|
||||
// begin() leaves the sensors sampling; stand them by until tilt page turn
|
||||
// actually wakes them, so a disabled IMU doesn't drain the battery.
|
||||
if (!_sdkImu.sleep()) {
|
||||
LOG_ERR("GYR", "IMU standby failed");
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INF("GYR", "QMI8658 IMU found at 0x%02X", _i2cAddr);
|
||||
|
||||
if (!writeReg(REG_CTRL7, CTRL7_DISABLE_ALL) || !writeReg(REG_CTRL3, CTRL3_FS_512DPS | CTRL3_ODR_28HZ) ||
|
||||
!writeReg(REG_CTRL1, CTRL1_BASE | CTRL1_SENSOR_DISABLE)) {
|
||||
LOG_ERR("GYR", "QMI8658 register configuration failed");
|
||||
_available = false;
|
||||
LOG_INF("GYR", "SDK IMU initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
_available = true;
|
||||
_initMs = millis();
|
||||
_lastPollMs = millis();
|
||||
LOG_INF("GYR", "QMI8658 gyro initialized and put to sleep");
|
||||
LOG_ERR("GYR", "SDK IMU not found");
|
||||
}
|
||||
|
||||
bool HalTiltSensor::wake() {
|
||||
@@ -89,21 +34,16 @@ bool HalTiltSensor::wake() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for init to complete before waking
|
||||
if ((millis() - _initMs) < SLEEP_STABILIZE_MS) {
|
||||
if (!_sdkImu.wake()) {
|
||||
LOG_ERR("GYR", "IMU wake failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (writeReg(REG_CTRL1, CTRL1_BASE) && writeReg(REG_CTRL7, CTRL7_GYRO_ENABLE)) {
|
||||
_lastPollMs = millis();
|
||||
_lastTiltMs = millis();
|
||||
_wakeMs = millis();
|
||||
LOG_INF("GYR", "QMI8658 woke up");
|
||||
return true;
|
||||
} else {
|
||||
LOG_ERR("GYR", "Failed to wake QMI8658");
|
||||
return false;
|
||||
}
|
||||
_lastPollMs = millis();
|
||||
_lastTiltMs = millis();
|
||||
_wakeMs = millis();
|
||||
_isAwake = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HalTiltSensor::deepSleep() {
|
||||
@@ -111,20 +51,15 @@ bool HalTiltSensor::deepSleep() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((millis() - _wakeMs) < SLEEP_STABILIZE_MS) {
|
||||
if (!_sdkImu.sleep()) {
|
||||
LOG_ERR("GYR", "IMU sleep failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (writeReg(REG_CTRL7, CTRL7_DISABLE_ALL) && writeReg(REG_CTRL1, CTRL1_BASE | CTRL1_SENSOR_DISABLE)) {
|
||||
// Clear any residual state so it doesn't immediately trigger upon waking
|
||||
clearPendingEvents();
|
||||
_inTilt = false;
|
||||
LOG_INF("GYR", "QMI8658 entered sleep mode");
|
||||
return true;
|
||||
} else {
|
||||
LOG_ERR("GYR", "Failed to put QMI8658 to sleep");
|
||||
return false;
|
||||
}
|
||||
clearPendingEvents();
|
||||
_inTilt = false;
|
||||
_isAwake = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void HalTiltSensor::update(const uint8_t mode, const uint8_t orientation, const bool inReader) {
|
||||
|
||||
+6
-33
@@ -1,9 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#include "HalGPIO.h"
|
||||
#include <Imu.h>
|
||||
|
||||
// TODO: Move enums into new header and share with CrossPointSettings.h
|
||||
namespace CrossPointOrientation {
|
||||
@@ -19,7 +17,7 @@ extern HalTiltSensor halTiltSensor; // Singleton
|
||||
|
||||
class HalTiltSensor {
|
||||
bool _available = false;
|
||||
uint8_t _i2cAddr = 0;
|
||||
mutable Imu _sdkImu;
|
||||
|
||||
// Tilt gesture state machine
|
||||
bool _tiltForwardEvent = false; // Consumed by wasTiltedForward()
|
||||
@@ -37,47 +35,22 @@ class HalTiltSensor {
|
||||
static constexpr unsigned long COOLDOWN_MS = 600; // Minimum ms between triggers
|
||||
static constexpr unsigned long POLL_INTERVAL_MS = 50; // 20 Hz polling
|
||||
static constexpr unsigned long WAKE_STABILIZE_MS = 300; // Ignore readings after wake
|
||||
static constexpr unsigned long SLEEP_STABILIZE_MS = 15; // Sleep turn on/off delay
|
||||
|
||||
mutable unsigned long _lastPollMs = 0;
|
||||
|
||||
// --- QMI8658 registers ---
|
||||
static constexpr uint8_t REG_CTRL1 = 0x02;
|
||||
static constexpr uint8_t REG_CTRL3 = 0x04;
|
||||
static constexpr uint8_t REG_CTRL7 = 0x08;
|
||||
static constexpr uint8_t REG_GX_L = 0x3B;
|
||||
|
||||
// --- Register Bit Flags ---
|
||||
|
||||
// REG_CTRL1 (0x02)
|
||||
static constexpr uint8_t CTRL1_BIG_ENDIAN = (1 << 5); // 0x20: Default state (1 = Big Endian)
|
||||
static constexpr uint8_t CTRL1_AUTO_INC = (1 << 6); // 0x40: Enable address auto-increment
|
||||
static constexpr uint8_t CTRL1_SENSOR_DISABLE = (1 << 0); // 0x01: Power down sensor engine
|
||||
static constexpr uint8_t CTRL1_BASE = CTRL1_AUTO_INC | CTRL1_BIG_ENDIAN; // 0x60
|
||||
|
||||
// REG_CTRL3 (0x04) - Gyro Config
|
||||
static constexpr uint8_t CTRL3_FS_512DPS = (0b101 << 4); // Bits 6:4 = 101
|
||||
static constexpr uint8_t CTRL3_ODR_28HZ = 0b1000; // Bits 3:0 = 1000 (28.025 Hz)
|
||||
|
||||
// REG_CTRL7 (0x08) - Enable
|
||||
static constexpr uint8_t CTRL7_DISABLE_ALL = 0x00;
|
||||
static constexpr uint8_t CTRL7_GYRO_ENABLE = (1 << 1); // Bit 1 = 1
|
||||
|
||||
bool writeReg(uint8_t reg, uint8_t val) const;
|
||||
bool readReg(uint8_t reg, uint8_t* val) const;
|
||||
bool readGyro(float& gx, float& gy, float& gz) const;
|
||||
|
||||
public:
|
||||
// Call after gpio.begin() and powerManager.begin() (I2C already initialised for X3)
|
||||
// Call after BoardConfig has selected the active device.
|
||||
void begin();
|
||||
|
||||
// Enables the QMI8658 internal sensor engine
|
||||
// Enables tilt polling state
|
||||
bool wake();
|
||||
|
||||
// Puts the QMI8658 into a low-power standby state
|
||||
// Puts tilt polling state to sleep
|
||||
bool deepSleep();
|
||||
|
||||
// True if the QMI8658 IMU is present on this device
|
||||
// True if an IMU is present on this device
|
||||
bool isAvailable() const { return _available; }
|
||||
|
||||
// Poll the accelerometer and update tilt gesture state.
|
||||
|
||||
Reference in New Issue
Block a user