Files
wadamesh/variants/tdisplay_p4/target.cpp
T

226 lines
10 KiB
C++

#include <Arduino.h>
#include <Wire.h> // BQ27220 fuel gauge (getBattMilliVolts)
#include "target.h"
#include <helpers/ArduinoHelpers.h> // RadioNoiseListener / StdRNG
TDisplayP4Board board;
Xl9535 xl9535; // board-global expander (declared extern in Xl9535.h)
// SX1262/LR2021 on the P4 SPI (SCLK=2/MOSI=3/MISO=4, CS=24, BUSY=6). RESET + DIO1 are on the XL9535
// (P_LORA_RESET/DIO_1 == -1 = RADIOLIB_NC): reset is pulsed via the expander in radio_init(); with
// DIO1 == NC RadioLib polls for RX/TX-done instead of taking a hardware IRQ.
// TODO(device): if polling RX is unreliable, route DIO1 through the XL9535 INT (GPIO5).
static SPIClass spi(FSPI);
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
WRAPPER_CLASS radio_driver(radio, board);
#if defined(USE_LR2021)
static const uint32_t s_lr2021_rf_switch_dios[] = {
RADIOLIB_LR2021_DIO6,
RADIOLIB_LR2021_DIO7,
RADIOLIB_LR2021_DIO8,
RADIOLIB_LR2021_DIO10,
RADIOLIB_NC,
};
static const Module::RfSwitchMode_t s_lr2021_rf_switch_table[] = {
{ LR2021::MODE_STBY, { LOW, LOW, LOW, LOW, LOW } },
{ LR2021::MODE_RX, { LOW, LOW, HIGH, LOW, LOW } },
{ LR2021::MODE_TX, { LOW, LOW, HIGH, LOW, LOW } },
{ LR2021::MODE_RX_HF, { HIGH, LOW, LOW, HIGH, LOW } },
{ LR2021::MODE_TX_HF, { LOW, HIGH, LOW, HIGH, LOW } },
END_OF_MODE_TABLE,
};
#endif
DISPLAY_CLASS display; // RM69A10 MIPI-DSI
ESP32RTCClock fallback_clock;
ClockFloorRTC rtc_clock(fallback_clock);
#if ENV_INCLUDE_GPS
// Same approach as camillia-mt's working P4 port: the L76K is woken through the XL9535
// (Xl9535::powerOnSequence drives GPS_WAKE high before the UART opens), UART1 listens on
// GPIO22 / transmits on 23, and the link starts at 9600 and PROBES other rates until a
// checksummed NMEA sentence arrives. The receiver's rate depends on what last configured
// it (the chip's 9600 default, or 115200 persisted by LilyGo's factory firmware), and the
// core provider listened at one fixed rate, so a mismatch meant an open, silent UART.
// WadaNmeaLocationProvider saves the rate that works. No reset/enable GPIOs on this board.
#include "../../src/helpers/WadaNmeaLocationProvider.h"
WadaNmeaLocationProvider nmea(Serial1, &rtc_clock, -1 /*reset*/, -1 /*enable*/,
PIN_GPS_TX, PIN_GPS_RX, GPS_BAUD_RATE);
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
#else
EnvironmentSensorManager sensors;
#endif
void TDisplayP4Board::begin() {
ESP32Board::begin();
}
// BQ27220 fuel gauge (I2C_1 @0x55, same shared Wire bus as the XL9535/RTC/touch — TwoWire's
// internal lock makes each transaction atomic across tasks). Standard command reads: 16-bit
// little-endian at Voltage()=0x08 (mV). Reads are rate-limited and sanity-windowed; on any I2C
// hiccup the last good value is kept so the battery UI never flickers to nonsense.
// Returns false when the gauge did not answer. This USED to return 0 on an I2C
// failure, which is indistinguishable from a real reading of zero — and the gauge
// shares this bus with the XL9535 expander, the RTC and the touch controller, so a
// lost transaction now and then is normal rather than exceptional. The voltage
// reader below always range-checked its result and kept the last good value, but
// the state-of-charge reader took 0 at face value and reported 0%, which is what
// made the battery flicker between 0 and the real figure. Report the failure
// instead of encoding it as data.
static bool bq27220ReadU16(uint8_t cmd, uint16_t& out) {
Wire.beginTransmission(0x55);
Wire.write(cmd);
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom(0x55, 2) != 2) return false;
uint16_t lo = Wire.read(), hi = Wire.read();
out = (uint16_t)(lo | (hi << 8));
return true;
}
// StateOfCharge() = REG 0x2C, a percentage the gauge computes by coulomb counting
// against its learned pack profile. This is the number to trust, and the reason the
// battery readout was wrong (#273): we were reporting terminal VOLTAGE through a
// generic 3.3-4.2 V linear curve instead. Voltage is charger-driven, so plugging USB
// in pinned the display at 100% while the pack was nearly flat (one reporter read 71%
// here and 11% under Meck-P4, another watched it jump 51% -> 100% on plugging in), a
// resting-but-full pack never quite reached 100%, and "calibrate 100% here" captured a
// charging rail as full and made the error permanent.
//
// Returns -1 when the gauge does not answer or reports out of range, so the caller can
// fall back to the voltage curve rather than show a made-up number.
int TDisplayP4Board::getBattStateOfCharge() {
static uint32_t last_ms = 0;
static int last_pct = -1;
const uint32_t now = millis();
if (last_ms == 0 || now - last_ms >= 5000) {
last_ms = now;
// Keep the previous good percentage when the gauge does not answer, exactly as
// the voltage read does. Only a genuine in-range reply moves the number.
uint16_t soc = 0;
if (bq27220ReadU16(0x2C, soc) && soc <= 100) last_pct = (int)soc;
}
return last_pct;
}
uint16_t TDisplayP4Board::getBattMilliVolts() {
static uint32_t last_ms = 0;
static uint16_t last_mv = 3800; // pre-first-read placeholder
static bool logged = false;
const uint32_t now = millis();
if (last_ms == 0 || now - last_ms >= 5000) {
last_ms = now;
uint16_t mv = 0;
const bool mv_ok = bq27220ReadU16(0x08, mv); // Voltage(), mV
if (mv_ok && mv >= 2500 && mv <= 4600) {
last_mv = mv;
if (!logged) { printf("[BATT] BQ27220 alive: %u mV\n", mv); logged = true; }
} else if (!logged) {
printf("[BATT] BQ27220 read invalid (%u) — placeholder in use\n", mv);
logged = true;
}
}
return last_mv;
}
bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire); // PCF8563 RTC is on I2C_1 (shared Wire, SDA=7/SCL=8)
// This board's chip IS documented, so declare it rather than leaving the
// clock-source diagnostics to guess (#383). Unlike the Pager and the M9 this
// one still goes through the core's probe, so a read that loses the chip's VL
// bit is still only caught by ClockFloorRTC's plausibility bounds — which is
// what already catches this board's known "came back asserting 2043".
rtc_clock.noteHardwareClock(true);
const uint32_t retained = rtc_clock.getCurrentTime();
if (retained > ClockFloorRTC::MIN_VALID_EPOCH &&
retained <= ClockFloorRTC::MAX_PLAUSIBLE_EPOCH) {
rtc_clock.noteHardwareTime();
}
// The radio RESET is on the XL9535 for both P4 V1 module variants.
xl9535.radioReset();
spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI, P_LORA_NSS);
if (!radio.std_init(&spi)) return false;
#if defined(USE_LR2021)
radio.setRfSwitchTable(s_lr2021_rf_switch_dios, s_lr2021_rf_switch_table);
#endif
return true;
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // fresh random identity from radio RSSI noise
}
extern "C" bool tdisplay_p4_reset_c6() {
if (!xl9535.ok()) return false;
// Match the factory-era Meck/LilyGo Esp_At driver exactly: release first,
// assert for 50 ms, then release and allow one second for ESP-AT to boot.
xl9535.write(Xl9535::IO_C6_EN, true);
delay(50);
xl9535.write(Xl9535::IO_C6_EN, false);
delay(50);
xl9535.write(Xl9535::IO_C6_EN, true);
delay(1000);
return true;
}
extern "C" int hosted_reset_slave_callback() {
return tdisplay_p4_reset_c6() ? 0 : -1;
}
// ---- Software restart = full system reset --------------------------------------------------------
// esp_restart() on the ESP32-P4 is SW_CPU_RESET: esp_system_reset_modules_on_exit()
// (esp_system/port/soc/esp32p4/system_internal.c) resets the DMA engines, UARTs, SDMMC and crypto,
// but not the MIPI-DSI host or its DPI path. The display driver then initialises over a host still
// half-configured from the session that restarted, and the panel stays dark. A power cycle or the
// reset button is a full chip reset and always recovers it -- which is exactly what users saw: a
// setting that reboots came up with the screen off, and the NEXT reboot by hand was fine.
// (Same defect, same fix as camillia-mt's T-Display P4 port.)
//
// So a software restart becomes that full reset: the RTC watchdog's system reset, which resets the
// CPU and all peripherals and spares only the RTC domain. Done as a shutdown handler so it covers
// every restart path (rebootDevice, OTA, console, factory reset) without touching any of them.
//
// esp_restart() runs shutdown handlers newest first and this one never returns, so it is installed
// before anything else registers one (Wi-Fi's among them) to run LAST, after the others have done
// their work. RTC memory survives the system reset, which is how the next boot knows its watchdog
// reset was a deliberate restart rather than a hang.
#include <esp_system.h>
#include <hal/wdt_hal.h>
static constexpr uint32_t kFullRestartMagic = 0x50345253u; // "P4RS"
RTC_NOINIT_ATTR static uint32_t s_full_restart_marker;
static bool s_boot_was_full_restart = false;
static void tdisplayP4FullRestart() {
s_full_restart_marker = kFullRestartMagic;
wdt_hal_context_t rwdt = RWDT_HAL_CONTEXT_DEFAULT();
wdt_hal_write_protect_disable(&rwdt);
// Ticks of the slow clock: ~130 ms at 150 kHz, longer on a 32 kHz crystal. It only has to be short.
wdt_hal_config_stage(&rwdt, WDT_STAGE0, 20000, WDT_STAGE_ACTION_RESET_SYSTEM);
wdt_hal_enable(&rwdt);
wdt_hal_write_protect_enable(&rwdt);
while (true) {
}
}
void tdisplayP4InstallFullRestart() {
static bool s_installed = false;
if (s_installed) return;
// Latch (and clear) the marker left by the previous session's restart before anything can read it.
s_boot_was_full_restart = (s_full_restart_marker == kFullRestartMagic)
&& esp_reset_reason() == ESP_RST_WDT;
s_full_restart_marker = 0;
s_installed = (esp_register_shutdown_handler(tdisplayP4FullRestart) == ESP_OK);
}
esp_reset_reason_t tdisplayP4ResetReason() {
// Deliberate restarts arrive as RTC-watchdog resets on this board; report them as what they were,
// so the "restarted after a crash" prompt and Settings -> About do not call every reboot a crash.
return s_boot_was_full_restart ? ESP_RST_SW : esp_reset_reason();
}