mirror of
https://github.com/pelgraine/Meck.git
synced 2026-07-06 01:31:17 +02:00
339 lines
9.6 KiB
C++
339 lines
9.6 KiB
C++
#include "TechoCardBoard.h"
|
|
#include "variant.h"
|
|
#include <Wire.h>
|
|
#include <nrf_soc.h>
|
|
#include <InternalFileSystem.h>
|
|
using namespace Adafruit_LittleFS_Namespace;
|
|
|
|
void TechoCardBoard::begin() {
|
|
NRF52BoardDCDC::begin();
|
|
Serial.begin(115200);
|
|
|
|
// RT9080 3V3 rail: clean reset cycle (from Meshtastic PR #10267)
|
|
// Toggling EN HIGH→LOW→HIGH forces a clean power-on, preventing
|
|
// brown-out when LoRa TX fires at full power.
|
|
#if PIN_OLED_EN >= 0
|
|
pinMode(PIN_OLED_EN, OUTPUT);
|
|
digitalWrite(PIN_OLED_EN, HIGH);
|
|
delay(100);
|
|
digitalWrite(PIN_OLED_EN, LOW);
|
|
delay(100);
|
|
digitalWrite(PIN_OLED_EN, HIGH);
|
|
delay(100);
|
|
#endif
|
|
|
|
// Park peripheral enable pins LOW before setup runs
|
|
#if defined(HAS_GPS) && PIN_GPS_EN >= 0
|
|
pinMode(PIN_GPS_EN, OUTPUT);
|
|
digitalWrite(PIN_GPS_EN, LOW);
|
|
#endif
|
|
#if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0
|
|
pinMode(PIN_GPS_RF_EN, OUTPUT);
|
|
digitalWrite(PIN_GPS_RF_EN, LOW);
|
|
#endif
|
|
#if defined(HAS_BUZZER) && PIN_BUZZER >= 0
|
|
pinMode(PIN_BUZZER, OUTPUT);
|
|
digitalWrite(PIN_BUZZER, LOW);
|
|
#endif
|
|
#if defined(HAS_SPEAKER)
|
|
pinMode(PIN_SPK_EN, OUTPUT);
|
|
digitalWrite(PIN_SPK_EN, LOW);
|
|
#if PIN_SPK_EN2 >= 0
|
|
pinMode(PIN_SPK_EN2, OUTPUT);
|
|
digitalWrite(PIN_SPK_EN2, LOW);
|
|
#endif
|
|
#endif
|
|
|
|
// Enable GPS power after rail stabilises
|
|
#if defined(HAS_GPS) && PIN_GPS_EN >= 0
|
|
delay(10);
|
|
digitalWrite(PIN_GPS_EN, HIGH);
|
|
#endif
|
|
#if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0
|
|
digitalWrite(PIN_GPS_RF_EN, HIGH);
|
|
#endif
|
|
|
|
// Initialise GPS UART
|
|
#if defined(HAS_GPS)
|
|
Serial1.setPins(PIN_GPS_RX, PIN_GPS_TX);
|
|
Serial1.begin(GPS_BAUDRATE);
|
|
#endif
|
|
|
|
pinMode(PIN_VBAT_READ, INPUT);
|
|
pinMode(PIN_USER_BTN, INPUT);
|
|
|
|
// Initialise I2C -- must be done before display.begin() is called from main.cpp
|
|
Wire.begin();
|
|
Wire.setClock(400000);
|
|
|
|
// Initialise WS2812 NeoPixel chain (all off at boot)
|
|
// Force data line LOW before init to prevent stray HIGH latching green
|
|
#if defined(HAS_RGB_LED)
|
|
pinMode(PIN_RGB_LED_1, OUTPUT);
|
|
digitalWrite(PIN_RGB_LED_1, LOW);
|
|
delayMicroseconds(300); // WS2812 reset pulse is ~280us
|
|
_pixels.begin();
|
|
_pixels.clear();
|
|
_pixels.show();
|
|
#endif
|
|
}
|
|
|
|
void TechoCardBoard::enableGPS(bool enable) {
|
|
#if defined(HAS_GPS) && PIN_GPS_EN >= 0
|
|
digitalWrite(PIN_GPS_EN, enable ? HIGH : LOW);
|
|
#endif
|
|
#if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0
|
|
digitalWrite(PIN_GPS_RF_EN, enable ? HIGH : LOW);
|
|
#endif
|
|
}
|
|
|
|
float TechoCardBoard::getMCUTemperature() {
|
|
// SoftDevice owns the TEMP peripheral -- direct register access hard faults.
|
|
// Use sd_temp_get() when SoftDevice is enabled.
|
|
int32_t temp;
|
|
uint8_t sd_en = 0;
|
|
sd_softdevice_is_enabled(&sd_en);
|
|
if (sd_en) {
|
|
if (sd_temp_get(&temp) == NRF_SUCCESS) {
|
|
return temp * 0.25f;
|
|
}
|
|
return NAN;
|
|
}
|
|
// SoftDevice off -- fall back to parent's direct register access
|
|
return NRF52Board::getMCUTemperature();
|
|
}
|
|
|
|
void TechoCardBoard::enableSpeaker(bool enable) {
|
|
#if defined(HAS_SPEAKER)
|
|
digitalWrite(PIN_SPK_EN, enable ? HIGH : LOW);
|
|
#if PIN_SPK_EN2 >= 0
|
|
digitalWrite(PIN_SPK_EN2, enable ? HIGH : LOW);
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
void TechoCardBoard::setLED(uint8_t r, uint8_t g, uint8_t b) {
|
|
#if defined(HAS_RGB_LED)
|
|
uint32_t color = Adafruit_NeoPixel::Color(r, g, b);
|
|
for (int i = 0; i < NUM_NEOPIXELS; i++) {
|
|
_pixels.setPixelColor(i, color);
|
|
}
|
|
_pixels.show();
|
|
#else
|
|
(void)r; (void)g; (void)b;
|
|
#endif
|
|
}
|
|
|
|
void TechoCardBoard::ledOff() {
|
|
setLED(0, 0, 0);
|
|
}
|
|
|
|
void TechoCardBoard::setStatusLED(uint8_t led_index, uint32_t color) {
|
|
#if defined(HAS_RGB_LED)
|
|
if (led_index < NUM_NEOPIXELS) {
|
|
_pixels.setPixelColor(led_index, color);
|
|
_pixels.show();
|
|
}
|
|
#else
|
|
(void)led_index; (void)color;
|
|
#endif
|
|
}
|
|
|
|
void TechoCardBoard::buzz(uint16_t freq_hz, uint16_t duration_ms) {
|
|
#if defined(HAS_BUZZER) && PIN_BUZZER >= 0
|
|
if (freq_hz == 0 || duration_ms == 0) {
|
|
noTone(PIN_BUZZER);
|
|
return;
|
|
}
|
|
tone(PIN_BUZZER, freq_hz, duration_ms);
|
|
#else
|
|
(void)freq_hz; (void)duration_ms;
|
|
#endif
|
|
}
|
|
|
|
// =============================================================================
|
|
// BQ25896 Charger IC (I2C address 0x6B)
|
|
// =============================================================================
|
|
|
|
#define BQ25896_ADDR 0x6B
|
|
|
|
bool TechoCardBoard::probeCharger() {
|
|
if (!_chargerProbed) {
|
|
Wire.beginTransmission(BQ25896_ADDR);
|
|
_chargerPresent = (Wire.endTransmission() == 0);
|
|
_chargerProbed = true;
|
|
if (!_chargerPresent) {
|
|
Serial.println("BQ25896: not found at 0x6B");
|
|
}
|
|
}
|
|
return _chargerPresent;
|
|
}
|
|
|
|
uint8_t TechoCardBoard::readChargerReg(uint8_t reg) {
|
|
if (!probeCharger()) return 0;
|
|
Wire.beginTransmission(BQ25896_ADDR);
|
|
Wire.write(reg);
|
|
if (Wire.endTransmission(false) != 0) return 0;
|
|
Wire.requestFrom((uint8_t)BQ25896_ADDR, (uint8_t)1);
|
|
return Wire.available() ? Wire.read() : 0;
|
|
}
|
|
|
|
void TechoCardBoard::writeChargerReg(uint8_t reg, uint8_t val) {
|
|
Wire.beginTransmission(BQ25896_ADDR);
|
|
Wire.write(reg);
|
|
Wire.write(val);
|
|
Wire.endTransmission();
|
|
}
|
|
|
|
void TechoCardBoard::enableChargerADC() {
|
|
uint8_t reg02 = readChargerReg(0x02);
|
|
reg02 |= 0xC0; // CONV_RATE=1 (continuous) + CONV_START=1
|
|
writeChargerReg(0x02, reg02);
|
|
}
|
|
|
|
uint8_t TechoCardBoard::getChargeStatus() {
|
|
return (readChargerReg(0x0B) >> 3) & 0x03;
|
|
}
|
|
|
|
uint16_t TechoCardBoard::getChargerBattMV() {
|
|
return 2304 + (readChargerReg(0x0E) & 0x7F) * 20;
|
|
}
|
|
|
|
uint8_t TechoCardBoard::getChargerTSPCT() {
|
|
return 21 + (readChargerReg(0x10) & 0x7F);
|
|
}
|
|
|
|
// =============================================================================
|
|
// ICM20948 / AK09916 Compass
|
|
//
|
|
// Enable I2C bypass on the ICM20948 so the AK09916 magnetometer at 0x0C
|
|
// appears directly on Wire. Then set continuous measurement mode.
|
|
// =============================================================================
|
|
|
|
#define ICM20948_ADDR 0x68
|
|
#define AK09916_ADDR 0x0C
|
|
|
|
static uint8_t _i2c_rd(uint8_t addr, uint8_t reg) {
|
|
Wire.beginTransmission(addr);
|
|
Wire.write(reg);
|
|
if (Wire.endTransmission(false) != 0) return 0;
|
|
Wire.requestFrom(addr, (uint8_t)1);
|
|
return Wire.available() ? Wire.read() : 0;
|
|
}
|
|
|
|
static void _i2c_wr(uint8_t addr, uint8_t reg, uint8_t val) {
|
|
Wire.beginTransmission(addr);
|
|
Wire.write(reg);
|
|
Wire.write(val);
|
|
Wire.endTransmission();
|
|
}
|
|
|
|
bool TechoCardBoard::initCompass() {
|
|
if (_compassReady) return true;
|
|
|
|
// Bank 0
|
|
_i2c_wr(ICM20948_ADDR, 0x7F, 0x00);
|
|
|
|
// Check WHO_AM_I (expect 0xEA)
|
|
if (_i2c_rd(ICM20948_ADDR, 0x00) != 0xEA) return false;
|
|
|
|
// Wake up: auto clock, not sleep
|
|
_i2c_wr(ICM20948_ADDR, 0x06, 0x01);
|
|
delay(10);
|
|
|
|
// Enable I2C bypass so AK09916 is directly accessible
|
|
_i2c_wr(ICM20948_ADDR, 0x0F, 0x02);
|
|
delay(5);
|
|
|
|
// Check AK09916 WHO_AM_I (expect 0x09)
|
|
if (_i2c_rd(AK09916_ADDR, 0x01) != 0x09) return false;
|
|
|
|
// Leave in power-down -- readMag triggers single measurements on demand
|
|
_i2c_wr(AK09916_ADDR, 0x31, 0x00);
|
|
|
|
_compassReady = true;
|
|
return true;
|
|
}
|
|
|
|
bool TechoCardBoard::readMag(int16_t& mx, int16_t& my, int16_t& mz) {
|
|
if (!_compassReady) return false;
|
|
|
|
// Single-measurement mode: trigger one fresh measurement per call.
|
|
// Continuous mode gets disrupted by OLED I2C display writes sharing
|
|
// the bus through ICM20948 bypass, causing stale data.
|
|
_i2c_wr(AK09916_ADDR, 0x31, 0x01); // single measurement trigger
|
|
|
|
// Wait for data ready (measurement takes ~7.2ms)
|
|
for (int i = 0; i < 20; i++) {
|
|
if (_i2c_rd(AK09916_ADDR, 0x10) & 0x01) break;
|
|
delay(1);
|
|
}
|
|
|
|
// Burst read 6 data bytes + ST2 (must read ST2 to complete cycle)
|
|
Wire.beginTransmission(AK09916_ADDR);
|
|
Wire.write(0x11);
|
|
if (Wire.endTransmission(false) != 0) return false;
|
|
Wire.requestFrom((uint8_t)AK09916_ADDR, (uint8_t)7);
|
|
if (Wire.available() < 7) return false;
|
|
|
|
uint8_t buf[7];
|
|
for (int i = 0; i < 7; i++) buf[i] = Wire.read();
|
|
|
|
mx = (int16_t)(buf[1] << 8 | buf[0]);
|
|
my = (int16_t)(buf[3] << 8 | buf[2]);
|
|
mz = (int16_t)(buf[5] << 8 | buf[4]);
|
|
// buf[6] = ST2, read to unlatch
|
|
|
|
return true;
|
|
}
|
|
|
|
// Power down the AK09916 magnetometer and put the ICM20948 itself to sleep.
|
|
// Saves ~3-4mA when not actively viewing the compass page.
|
|
// Next call to initCompass() will fully re-initialise the chain.
|
|
void TechoCardBoard::sleepCompass() {
|
|
if (!_compassReady) return;
|
|
|
|
// Bank 0 (in case we drifted)
|
|
_i2c_wr(ICM20948_ADDR, 0x7F, 0x00);
|
|
|
|
// AK09916 CNTL2 = 0x00 -- power-down mode (stops continuous measurement)
|
|
_i2c_wr(AK09916_ADDR, 0x31, 0x00);
|
|
|
|
// ICM20948 PWR_MGMT_1 = 0x40 -- SLEEP bit set
|
|
_i2c_wr(ICM20948_ADDR, 0x06, 0x40);
|
|
|
|
_compassReady = false;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Compass calibration persistence
|
|
// =============================================================================
|
|
|
|
#define COMPASS_CAL_FILE "/compass_cal"
|
|
|
|
bool TechoCardBoard::loadCalibration() {
|
|
// InternalFS must already be initialised (done in main.cpp setup)
|
|
File file = InternalFS.open(COMPASS_CAL_FILE, FILE_O_READ);
|
|
if (file) {
|
|
int n = file.read((uint8_t*)&_cal, sizeof(_cal));
|
|
file.close();
|
|
if (n == (int)sizeof(_cal) && _cal.magic == COMPASS_CAL_MAGIC) {
|
|
return true;
|
|
}
|
|
}
|
|
// No valid calibration -- reset to identity (no correction)
|
|
_cal = { 0, 0, 0, 1.0f, 1.0f, 1.0f, 0 };
|
|
return false;
|
|
}
|
|
|
|
bool TechoCardBoard::saveCalibration(const CompassCalibration& cal) {
|
|
_cal = cal;
|
|
_cal.magic = COMPASS_CAL_MAGIC;
|
|
// Direct-write pattern: remove then create (nRF52 LittleFS compatible)
|
|
InternalFS.remove(COMPASS_CAL_FILE);
|
|
File file = InternalFS.open(COMPASS_CAL_FILE, FILE_O_WRITE);
|
|
if (!file) return false;
|
|
file.write((const uint8_t*)&_cal, sizeof(_cal));
|
|
file.close();
|
|
return true;
|
|
} |