diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index b8b6915c2..3db497dc2 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -11,6 +11,25 @@ #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32) #include "meshUtils.h" // vformat +#endif +#if defined(HAS_QMA6100P) && (defined(ARCH_NRF52) || defined(NRF52_SERIES) || defined(NRF52)) +#include "platform/nrf52/Nrf52Twim.h" +#include + +namespace +{ +bool probeQMA6100P(uint8_t address) +{ + uint8_t chipID = 0; + + Nrf52Twim::restoreBus(); + const bool readOk = Nrf52Twim::readRegister(address, SFE_QMA6100P_CHIP_ID, chipID); + const bool found = readOk && chipID == QMA6100P_CHIP_ID; + Nrf52Twim::restoreBus(); + + return found; +} +} // namespace #endif bool in_array(uint8_t *array, int size, uint8_t lookfor) @@ -277,11 +296,36 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) // 0x7C-0x7F Reserved for future purposes for (addr.address = 8; addr.address < 120; addr.address++) { +#if defined(HAS_QMA6100P) && (defined(ARCH_NRF52) || defined(NRF52_SERIES) || defined(NRF52)) + bool nrf52QmaFound = false; +#endif if (asize != 0) { if (!in_array(address, asize, (uint8_t)addr.address)) continue; LOG_DEBUG("Scan address 0x%x", (uint8_t)addr.address); } + // For QMA6100P candidates on nRF52, use bounded I2C probing; otherwise use normal Wire +#if defined(HAS_QMA6100P) && (defined(ARCH_NRF52) || defined(NRF52_SERIES) || defined(NRF52)) + if (addr.address == QMA6100P_ADDRESS_LOW || addr.address == QMA6100P_ADDRESS_HIGH) { + nrf52QmaFound = probeQMA6100P(addr.address); + err = nrf52QmaFound ? 0 : 2; + } else { + i2cBus->beginTransmission(addr.address); +#ifdef ARCH_PORTDUINO + err = 2; + if ((addr.address >= 0x30 && addr.address <= 0x37) || (addr.address >= 0x50 && addr.address <= 0x5F)) { + if (i2cBus->read() != -1) + err = 0; + } else { + err = i2cBus->writeQuick((uint8_t)0); + } + if (err != 0) + err = 2; +#else + err = i2cBus->endTransmission(); +#endif + } +#else i2cBus->beginTransmission(addr.address); #ifdef ARCH_PORTDUINO err = 2; @@ -295,6 +339,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) err = 2; #else err = i2cBus->endTransmission(); +#endif #endif type = NONE; if (err == 0) { @@ -750,7 +795,17 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) } break; } - SCAN_SIMPLE_CASE(BMM150_ADDR, BMM150, "BMM150", (uint8_t)addr.address); + case BMM150_ADDR: +#if defined(HAS_QMA6100P) && (defined(ARCH_NRF52) || defined(NRF52_SERIES) || defined(NRF52)) + if (nrf52QmaFound) { + logFoundDevice("QMA6100P", (uint8_t)addr.address); + type = QMA6100P; + break; + } +#endif + logFoundDevice("BMM150", (uint8_t)addr.address); + type = BMM150; + break; #ifdef HAS_TPS65233 SCAN_SIMPLE_CASE(TPS65233_ADDR, TPS65233, "TPS65233", (uint8_t)addr.address); #endif diff --git a/src/motion/QMA6100PSensor.cpp b/src/motion/QMA6100PSensor.cpp index a04837e80..151444fd8 100644 --- a/src/motion/QMA6100PSensor.cpp +++ b/src/motion/QMA6100PSensor.cpp @@ -2,9 +2,83 @@ #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && defined(HAS_QMA6100P) +#ifdef ARCH_NRF52 +#include "platform/nrf52/Nrf52Twim.h" +#endif + // Flag when an interrupt has been detected volatile static bool QMA6100P_IRQ = false; +#ifdef ARCH_NRF52 +namespace +{ +bool qma6100pUpdateRegister(uint8_t address, uint8_t reg, uint8_t (*mutate)(uint8_t)) +{ + uint8_t value = 0; + return Nrf52Twim::readRegister(address, reg, value) && Nrf52Twim::writeRegister(address, reg, mutate(value)); +} + +uint8_t qma6100pSetRange(uint8_t value) +{ + sfe_qma6100p_fsr_bitfield_t fsr; + fsr.all = value; + fsr.bits.range = QMA_6100P_MPU_ACCEL_SCALE; + return fsr.all; +} + +uint8_t qma6100pEnableAccel(uint8_t value) +{ + sfe_qma6100p_pm_bitfield_t pm; + pm.all = value; + pm.bits.mode_bit = 1; + return pm.all; +} + +uint8_t qma6100pConfigureIntPin(uint8_t value) +{ + return value | 0b00000010; +} + +uint8_t qma6100pConfigureIntLatch(uint8_t value) +{ + return value | 0b10000001; +} + +uint8_t qma6100pMapAnyMotionToInt1(uint8_t value) +{ + sfe_qma6100p_int_map1_bitfield_t intMap1; + intMap1.all = value; + intMap1.bits.int1_any_mot = 1; + return intMap1.all; +} + +bool qma6100pInitBounded(uint8_t address) +{ + // SFE_QMA6100P_SR: write 0xB6 to trigger soft-reset, then 0x00 to release + constexpr uint8_t QMA6100P_SOFT_RESET = 0xb6; + constexpr uint8_t QMA6100P_SOFT_RESET_RELEASE = 0x00; + // SFE_QMA6100P_INT_EN2: bits[2:0] = any-motion enable for Z, Y, X axes + constexpr uint8_t QMA6100P_INT_EN2_ANY_MOT_XYZ = 0b00000111; + + uint8_t chipID = 0; + + Nrf52Twim::restoreBus(); + const bool ok = Nrf52Twim::readRegister(address, SFE_QMA6100P_CHIP_ID, chipID) && chipID == QMA6100P_CHIP_ID && + Nrf52Twim::writeRegister(address, SFE_QMA6100P_SR, QMA6100P_SOFT_RESET) && + Nrf52Twim::writeRegister(address, SFE_QMA6100P_SR, QMA6100P_SOFT_RESET_RELEASE) && + qma6100pUpdateRegister(address, SFE_QMA6100P_FSR, qma6100pSetRange) && + qma6100pUpdateRegister(address, SFE_QMA6100P_PM, qma6100pEnableAccel) && + Nrf52Twim::writeRegister(address, SFE_QMA6100P_INT_EN2, QMA6100P_INT_EN2_ANY_MOT_XYZ) && + qma6100pUpdateRegister(address, SFE_QMA6100P_INT_MAP1, qma6100pMapAnyMotionToInt1) && + qma6100pUpdateRegister(address, SFE_QMA6100P_INTPINT_CONF, qma6100pConfigureIntPin) && + qma6100pUpdateRegister(address, SFE_QMA6100P_INT_CFG, qma6100pConfigureIntLatch); + Nrf52Twim::restoreBus(); + + return ok; +} +} // namespace +#endif + // Interrupt service routine void QMA6100PSetInterrupt() { @@ -15,6 +89,20 @@ QMA6100PSensor::QMA6100PSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor: bool QMA6100PSensor::init() { +#ifdef ARCH_NRF52 + if (!qma6100pInitBounded(device.address.address)) { + LOG_WARN("QMA6100P init failed"); + return false; + } + QMA6100P_IRQ = false; + +#ifdef QMA_6100P_INT_PIN + pinMode(QMA_6100P_INT_PIN, INPUT_PULLUP); + attachInterrupt(QMA_6100P_INT_PIN, QMA6100PSetInterrupt, FALLING); +#endif + + return true; +#else // Initialise the sensor sensor = QMA6100PSingleton::GetInstance(); if (!sensor->init(device)) @@ -22,6 +110,7 @@ bool QMA6100PSensor::init() // Enable simple Wake on Motion return sensor->setWakeOnMotion(); +#endif } #ifdef QMA_6100P_INT_PIN @@ -40,6 +129,16 @@ int32_t QMA6100PSensor::runOnce() int32_t QMA6100PSensor::runOnce() { +#ifdef ARCH_NRF52 + // restoreBus() is intentionally omitted here: it tears down and re-initialises Wire, + // which would be destructive on every poll. The bus is stable after init(); a failed + // read is handled gracefully below. + uint8_t tempVal = 0; + if (!Nrf52Twim::readRegister(device.address.address, SFE_QMA6100P_INT_ST0, tempVal)) { + LOG_DEBUG("QMA6100PS isWakeOnMotion failed to read interrupts"); + return MOTION_SENSOR_CHECK_INTERVAL_MS; + } +#else // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) uint8_t tempVal; @@ -47,6 +146,7 @@ int32_t QMA6100PSensor::runOnce() LOG_DEBUG("QMA6100PS isWakeOnMotion failed to read interrupts"); return MOTION_SENSOR_CHECK_INTERVAL_MS; } +#endif if ((tempVal & 7) != 0) { // Wake up! diff --git a/src/platform/nrf52/Nrf52Twim.cpp b/src/platform/nrf52/Nrf52Twim.cpp new file mode 100644 index 000000000..9198e654b --- /dev/null +++ b/src/platform/nrf52/Nrf52Twim.cpp @@ -0,0 +1,167 @@ +#include "Nrf52Twim.h" + +#if defined(ARCH_NRF52) && defined(HAS_QMA6100P) + +#include +#include + +namespace +{ +constexpr uint32_t i2cTimeoutUs = 5000; + +void stopTwim(NRF_TWIM_Type *twim); + +bool waitForEventOrError(volatile uint32_t &event, NRF_TWIM_Type *twim) +{ + const uint32_t start = micros(); + while (!event && !twim->EVENTS_ERROR) { + if ((uint32_t)(micros() - start) >= i2cTimeoutUs) { + stopTwim(twim); + return false; + } + } + return event && !twim->EVENTS_ERROR; +} + +void configureTwim(NRF_TWIM_Type *twim) +{ + twim->ENABLE = TWIM_ENABLE_ENABLE_Disabled; + twim->PSEL.SCL = PIN_WIRE_SCL; + twim->PSEL.SDA = PIN_WIRE_SDA; + twim->FREQUENCY = TWIM_FREQUENCY_FREQUENCY_K100; + // ENABLE is a volatile HW register: the peripheral must be disabled (above) before + // PSEL/FREQUENCY can be changed, then re-enabled here. Not a redundant store. + twim->ENABLE = TWIM_ENABLE_ENABLE_Enabled; // cppcheck-suppress redundantAssignment +} + +void clearTwimEvents(NRF_TWIM_Type *twim) +{ + twim->SHORTS = 0; + twim->EVENTS_STOPPED = 0; + twim->EVENTS_ERROR = 0; + twim->EVENTS_SUSPENDED = 0; + twim->EVENTS_RXSTARTED = 0; + twim->EVENTS_TXSTARTED = 0; + twim->EVENTS_LASTRX = 0; + twim->EVENTS_LASTTX = 0; + twim->ERRORSRC = TWIM_ERRORSRC_ANACK_Msk | TWIM_ERRORSRC_DNACK_Msk | TWIM_ERRORSRC_OVERRUN_Msk; +} + +void stopTwim(NRF_TWIM_Type *twim) +{ + twim->TASKS_STOP = 1; + const uint32_t start = micros(); + while (!twim->EVENTS_STOPPED && (uint32_t)(micros() - start) < i2cTimeoutUs) + ; + clearTwimEvents(twim); +} + +bool waitForStop(NRF_TWIM_Type *twim) +{ + const uint32_t start = micros(); + while (!twim->EVENTS_STOPPED && !twim->EVENTS_ERROR) { + if ((uint32_t)(micros() - start) >= i2cTimeoutUs) { + stopTwim(twim); + return false; + } + } + const bool stopped = twim->EVENTS_STOPPED && !twim->EVENTS_ERROR; + if (!stopped) + stopTwim(twim); + else + clearTwimEvents(twim); + return stopped; +} +} // namespace + +namespace Nrf52Twim +{ +void restoreBus() +{ +#if defined(TRACKER_T1000_E) + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); + pinMode(PIN_3V3_ACC_EN, OUTPUT); + digitalWrite(PIN_3V3_ACC_EN, HIGH); +#ifdef T1000X_SENSOR_EN_PIN + pinMode(T1000X_SENSOR_EN_PIN, OUTPUT); + digitalWrite(T1000X_SENSOR_EN_PIN, HIGH); +#endif + delay(20); +#endif + + Wire.end(); + + pinMode(PIN_WIRE_SCL, INPUT_PULLUP); + pinMode(PIN_WIRE_SDA, INPUT_PULLUP); + delayMicroseconds(10); + + for (uint8_t i = 0; i < 9 && digitalRead(PIN_WIRE_SDA) == LOW; i++) { + pinMode(PIN_WIRE_SCL, OUTPUT); + digitalWrite(PIN_WIRE_SCL, LOW); + delayMicroseconds(5); + pinMode(PIN_WIRE_SCL, INPUT_PULLUP); + delayMicroseconds(5); + } + + pinMode(PIN_WIRE_SDA, OUTPUT); + digitalWrite(PIN_WIRE_SDA, LOW); + delayMicroseconds(5); + pinMode(PIN_WIRE_SCL, INPUT_PULLUP); + delayMicroseconds(5); + pinMode(PIN_WIRE_SDA, INPUT_PULLUP); + delayMicroseconds(5); + + Wire.begin(); +} + +bool readRegister(uint8_t address, uint8_t reg, uint8_t &value) +{ + NRF_TWIM_Type *twim = NRF_TWIM0; + + configureTwim(twim); + clearTwimEvents(twim); + twim->ADDRESS = address; + twim->TXD.PTR = (uint32_t)® + twim->TXD.MAXCNT = 1; + // LASTTX_SUSPEND shortcut: peripheral suspends automatically after the TX + // byte, so EVENTS_SUSPENDED is set before we even poll — no clearing race. + twim->SHORTS = TWIM_SHORTS_LASTTX_SUSPEND_Msk; + twim->TASKS_RESUME = 1; + twim->TASKS_STARTTX = 1; + + if (!waitForEventOrError(twim->EVENTS_SUSPENDED, twim)) { + stopTwim(twim); + return false; + } + + twim->EVENTS_SUSPENDED = 0; + twim->EVENTS_LASTRX = 0; + twim->RXD.PTR = (uint32_t)&value; + twim->RXD.MAXCNT = 1; + twim->SHORTS = TWIM_SHORTS_LASTRX_STOP_Msk; + twim->TASKS_RESUME = 1; + twim->TASKS_STARTRX = 1; + + return waitForStop(twim) && twim->RXD.AMOUNT == 1; +} + +bool writeRegister(uint8_t address, uint8_t reg, uint8_t value) +{ + NRF_TWIM_Type *twim = NRF_TWIM0; + uint8_t data[] = {reg, value}; + + configureTwim(twim); + clearTwimEvents(twim); + twim->ADDRESS = address; + twim->TXD.PTR = (uint32_t)data; + twim->TXD.MAXCNT = sizeof(data); + twim->SHORTS = TWIM_SHORTS_LASTTX_STOP_Msk; + twim->TASKS_RESUME = 1; + twim->TASKS_STARTTX = 1; + + return waitForStop(twim); +} +} // namespace Nrf52Twim + +#endif diff --git a/src/platform/nrf52/Nrf52Twim.h b/src/platform/nrf52/Nrf52Twim.h new file mode 100644 index 000000000..7b07d7dd9 --- /dev/null +++ b/src/platform/nrf52/Nrf52Twim.h @@ -0,0 +1,18 @@ +#pragma once + +#include "configuration.h" + +// Only the QMA6100P probing path (T1000-E) uses this TWIM helper; gate it on +// HAS_QMA6100P so it isn't compiled/analyzed as dead code on other nRF52 boards. +#if defined(ARCH_NRF52) && defined(HAS_QMA6100P) + +#include + +namespace Nrf52Twim +{ +void restoreBus(); +bool readRegister(uint8_t address, uint8_t reg, uint8_t &value); +bool writeRegister(uint8_t address, uint8_t reg, uint8_t value); +} // namespace Nrf52Twim + +#endif