feat: Add touch coordinate mapping and RTOS task yielding (#2481)

Co-authored-by: Julia Nguyen <julia@uxj.io>
This commit is contained in:
Justin Mitchell
2026-07-20 16:31:07 -04:00
committed by GitHub
co-authored by Julia Nguyen
parent c9188a7347
commit f42fab1c66
123 changed files with 3564 additions and 1380 deletions
+21 -91
View File
@@ -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
View File
@@ -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);
};
+12
View File
@@ -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);
+11
View File
@@ -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
View File
@@ -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) {
+10
View File
@@ -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
View File
@@ -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) {
+5 -4
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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.