diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f54455..272d53e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version ## [Unreleased] +### Added + +- **CPU / Clock diagnostics screen** (`kScreenCpu`, inserted between Diagnostics and BT Signal in the K0 cycle). Shows the configured system clock (`SYS_CLOCK_KHZ` — the overclock target), the *actually running* `clk_sys` measured live by the RP2350 on-chip frequency counter against the crystal reference, the core voltage read back from the regulator (`vreg_get_voltage()`, not the compile-time constant), and the RP2350 on-die temperature (ADC input 4). Pure read-only instrumentation; one-time ADC bring-up and no other code path uses the ADC, so it is conflict-free. `render_screen_cpu()` is `noinline` like the other render functions (Thumb literal-pool reach). Adds `hardware_adc` to `target_link_libraries`. The frequency-counter measurement (a multi-ms busy-wait) runs **once on screen entry** and is cached — `clk_sys` is fixed at boot, so only the temperature refreshes per frame, avoiding a per-frame BT/audio hitch while the screen is visible. `oled_loop()` gained a generic `screen_entered` flag for this. Hardware-verified on Pico 2 W + OLED. Also exported over a new HID feature report **`0xfc`** (`src/cmd.cpp`) — 11 bytes: set_khz, cached real_khz, vreg code, ADC ch4 raw — so the web config emulator can show live CPU telemetry (volts/temp math done web-side to keep the firmware HID path float-free). + +### Fixed (web telemetry — latent since the slots/diag reports landed) + +- **CPU/Clock temperature was a single noisy ADC sample.** The RP2350 temp sensor has a shallow slope (−1.721 mV/°C, ~1 LSB ≈ 0.47 °C) so a lone 12-bit reading swings several tenths of a degree per frame — the displayed value just mirrored the latest noisy sample instead of the true die temperature. New `cpu_temp_raw_smoothed()` in `src/cmd.cpp` averages a 256-sample block then runs a slow EMA (α=0.15, seeded on first call). It is the **single source of truth**: both `render_screen_cpu()` and the `0xfc` web telemetry call it, and the duplicated per-site ADC bring-up was removed (ADC now initialised in exactly one place). `oled.cpp` no longer touches the ADC directly (drops `hardware/adc.h`, adds `cmd.h`). +- **Live web telemetry over WebHID: not feasible on the target setup; abandoned.** A browser-side read-only diagnostic proved Chrome WebHID returns `NotAllowedError` for any report ID **not declared** in the parsed HID report descriptor (declared `0xF7`/`0xF8`/`0xF9` read fine; undeclared `0xFA`/`0xFB`/`0xFC` fail). Declaring them is therefore mandatory for the web read — but doing so (even applied atomically with a matching `wDescriptorLength`, correct bytes identical in shape to the working `0xF6`–`0xF9`, and a `bcdDevice` cache-bust bump) made the device fail to enumerate as a usable HID device on the user's real Windows machine in **two** independent attempts (Device Manager showed it; WebHID and the PlayStation Accessories app did not). The cloned DualSense HID report descriptor cannot be safely extended on this environment. Reverted to the original descriptor (`0x0141`/`0x01B5`, no vendor feature reports, `bcdDevice 0x0100`). Retained with **no USB impact**: the `0xfc` firmware handler and the temperature smoothing/`cpu_temp_raw_smoothed()`. The on-device CPU/Clock OLED screen is fully working and hardware-verified; the web preview's CPU screen stays on representative mock values (the slots/diagnostics web screens were never readable for the same root cause and are likewise mock-only when connected). + ### Fixed - **Low-battery LED keeps blinking after controller disconnect** (`fb68ea5`). When the DualSense's battery dropped low enough to trigger `battery_led_tick`'s blink and the controller subsequently disconnected (typically: battery fully depletes and the BT link drops), the Pico's onboard LED stayed frozen in whichever half-cycle it was in at the moment of disconnect; reconnect-retry windows could even briefly resume blinking. New `battery_led_on_disconnect()` clears blink state, forces LED off, and zeros `last_report_us` so the stale-check early-return blocks any new blink until a fresh 0x31 report arrives on the next connection. Stale-check in the tick also now forces LED off when it fires mid-blink (defense in depth for ungraceful disconnects). Reported by Sura Academy on Discord. Same bug present in upstream — sent back as [awalol/DS5Dongle#101](https://github.com/awalol/DS5Dongle/pull/101). diff --git a/src/oled.cpp b/src/oled.cpp index 0174a1a..2bed269 100644 --- a/src/oled.cpp +++ b/src/oled.cpp @@ -10,6 +10,9 @@ #include "hardware/spi.h" #include "hardware/gpio.h" #include "hardware/watchdog.h" +#include "hardware/clocks.h" +#include "hardware/vreg.h" +#include "cmd.h" #include "pico/time.h" extern uint8_t interrupt_in_data[63]; // defined in main.cpp @@ -70,10 +73,11 @@ constexpr int kScreenTriggers = 3; constexpr int kScreenGyro = 4; constexpr int kScreenTouchpad = 5; constexpr int kScreenDiag = 6; -constexpr int kScreenRssi = 7; -constexpr int kScreenVU = 8; -constexpr int kScreenSettings = 9; -constexpr int kNumScreens = 10; +constexpr int kScreenCpu = 7; +constexpr int kScreenRssi = 8; +constexpr int kScreenVU = 9; +constexpr int kScreenSettings = 10; +constexpr int kNumScreens = 11; int current_screen = 0; // Lightbar mode cycle: 0=LIVE, 1-4=FAV0-3, 5=BREATHING, 6=RAINBOW, 7=FADE @@ -601,6 +605,60 @@ __attribute__((noinline)) void render_screen_diag() { flush_fb(); } +__attribute__((noinline)) void render_screen_cpu(bool entered) { + fb_clear(); + draw_text(0, 0, "CPU / Clock"); + + char buf[24]; + + // Configured system clock — compile-time SYS_CLOCK_KHZ, set in main() + // via set_sys_clock_khz(). This is the *target*. + const uint32_t set_khz = (uint32_t)SYS_CLOCK_KHZ; + snprintf(buf, sizeof(buf), "Set : %lu MHz", (unsigned long)(set_khz / 1000u)); + draw_text(0, 12, buf); + + // Actually running clk_sys, measured by the on-chip frequency counter + // against the crystal reference (not just what we asked for). The counter + // busy-waits a few ms per call, so measure ONCE on screen entry and cache + // it — clk_sys is fixed at boot and never changes, so the temperature + // (which legitimately drifts) is the only thing worth refreshing per + // frame. cached_real_khz==0 also forces a (re)measure as a safety net. + static uint32_t cached_real_khz = 0; + if (entered || cached_real_khz == 0) { + cached_real_khz = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS); + } + const uint32_t real_khz = cached_real_khz; + snprintf(buf, sizeof(buf), "Real: %lu.%01lu MHz", + (unsigned long)(real_khz / 1000u), + (unsigned long)((real_khz % 1000u) / 100u)); + draw_text(0, 22, buf); + + // Core voltage actually programmed into the regulator, read back (not the + // compile-time constant). Codes 0..15 are linear 0.05 V steps from 0.55 V. + const int vcode = (int)vreg_get_voltage(); + if (vcode >= 0 && vcode <= 0b01111) { + const unsigned mv = 550u + 50u * (unsigned)vcode; + snprintf(buf, sizeof(buf), "Vcore: %u.%02u V", mv / 1000u, (mv % 1000u) / 10u); + } else { + snprintf(buf, sizeof(buf), "Vcore: code %d", vcode); + } + draw_text(0, 32, buf); + + // RP2350 on-die temperature sensor. Smoothed + averaged in cmd.cpp + // (single source of truth shared with the 0xfc web telemetry) so the + // reading converges to the true die temp instead of chasing ADC noise. + const uint16_t raw = cpu_temp_raw_smoothed(); + const float volts = (float)raw * 3.3f / 4096.0f; + const float temp_c = 27.0f - (volts - 0.706f) / 0.001721f; + const int t10 = (int)(temp_c * 10.0f + (temp_c >= 0 ? 0.5f : -0.5f)); + snprintf(buf, sizeof(buf), "Temp : %d.%d C", t10 / 10, + (t10 < 0 ? -t10 : t10) % 10); + draw_text(0, 42, buf); + + draw_text(0, 56, "K0=next K1=back"); + flush_fb(); +} + __attribute__((noinline)) void render_screen_triggers() { fb_clear(); draw_text(0, 0, "Trigger Test"); @@ -1204,6 +1262,13 @@ void oled_loop() { const bool idle = (now - last_activity_us) > kAutoDimUs; sh1107_set_contrast(idle ? kDimContrast : kBrightLevels[bright_idx]); + // True on the first render after navigating to a different screen. + // Lets a screen do expensive one-shot work on entry (the CPU screen + // caches its frequency-counter measurement here). + static int last_rendered_screen = -1; + const bool screen_entered = (current_screen != last_rendered_screen); + last_rendered_screen = current_screen; + switch (current_screen) { case kScreenStatus: render_screen(); break; case kScreenSlots: render_screen_slots(); break; @@ -1212,6 +1277,7 @@ void oled_loop() { case kScreenGyro: render_screen_gyro(); break; case kScreenTouchpad: render_screen_touchpad(); break; case kScreenDiag: render_screen_diag(); break; + case kScreenCpu: render_screen_cpu(screen_entered); break; case kScreenRssi: render_screen_rssi(); break; case kScreenVU: render_screen_vu(); break; case kScreenSettings: render_screen_settings(); break;