Compare commits

...
Author SHA1 Message Date
MarcelineVPQandClaude Opus 4.7 05ea46cbd3 docs(changelog): cut v0.6.1-oled-edition
CPU/Clock OLED diagnostics screen (PR #2) + battery_led disconnect
fix, on top of v0.6.0-oled-edition. Promotes the [Unreleased] section
to [0.6.1-oled-edition] with today's date.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 19:02:55 -06:00
63fcfb91ea feat(oled): add CPU / Clock diagnostics screen
New CPU/Clock OLED screen (kScreenCpu), between Diagnostics and BT
Signal in the K0 cycle. Shows:

- Set  - configured system clock (SYS_CLOCK_KHZ overclock target)
- Real - clk_sys from the RP2350 frequency counter, measured once on
  screen entry and cached (it is fixed at boot; avoids a per-frame
  multi-ms stall)
- Vcore - core voltage read back via vreg_get_voltage()
- Temp - on-die sensor via cpu_temp_raw_smoothed()

render_screen_cpu() is noinline like the other render functions (Thumb
literal-pool reach). Hardware-in-the-loop verified on Pico 2 W +
Waveshare Pico-OLED-1.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 07:31:13 -06:00
5425b21f00 feat(cpu): smoothed RP2350 CPU/Clock telemetry + 0xfc HID report
Add cpu_temp_raw_smoothed() (cmd.cpp/cmd.h): 256-sample average over a
slow EMA (alpha=0.15) so the RP2350 on-die temperature converges to the
true value instead of chasing per-sample 12-bit ADC noise (~1 LSB =
0.47 C). Single source of truth, ADC initialised in one place.

Also export CPU/Clock telemetry on HID feature report 0xfc (set_khz,
cached real_khz, vreg code, ADC ch4 raw). The handler is dormant: live
read over WebHID is not feasible (Chrome requires the report declared
in the HID descriptor, and declaring the OLED Edition vendor reports
breaks DualSense enumeration on Windows). It is harmless and left for a
possible future non-WebHID consumer. Link hardware_adc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 07:31:13 -06:00
MarcelineVPQandClaude Opus 4.7 4552fd2b41 docs(readme): bump OLED screen count 10 → 11 for CPU/Clock
Follow-up to #2 — the PR added a CPU/Clock OLED screen and bumped
kNumScreens from 10 to 11, but README still advertised 10 screens
in two places (header blurb + Features bullet).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 07:29:18 -06:00
MarcelineVPQandClaude Opus 4.7 f8d79b3007 ci(release): append SHA256SUMS to release notes + upload as an asset
Future releases get:
- A 'Checksums' section appended to the release body with sha256sum -c
  -compatible 'HASH  filename' lines for each .uf2 attached
- A SHA256SUMS.txt file uploaded alongside the .uf2 artifacts so users
  can verify with 'sha256sum -c SHA256SUMS.txt' after downloading

Web-config flasher already compares this against its own SHA-256 on the
'Use latest release' path, but the hashes on the release page let people
manually verify the bundled site UF2 too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 13:26:11 -06:00
MarcelineVPQandClaude Opus 4.7 b03415fdb8 ci(release): trigger web config rebuild on new firmware release
Adds a repository_dispatch step to the release workflow that pings the
DS5Dongle-OLED-Config-Web repo with event-type 'firmware-released' after
the UF2s are uploaded. The web repo's deploy.yml listens for that event
and re-runs its build, which re-fetches the latest release UF2 and
bundles it into the GH Pages site.

Requires a fine-grained PAT stored as WEB_REPO_DISPATCH_PAT secret on
this repo, scoped to:
  - Repository: MarcelineVPQ/DS5Dongle-OLED-Config-Web
  - Permissions: Actions: Write, Metadata: Read
If the secret isn't set, the step fails 401 and continue-on-error
swallows it — release upload still succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:30:50 -06:00
MarcelineVPQandClaude Opus 4.7 79d8841040 docs: CHANGELOG entries for v0.6.0-oled-edition tag + battery_led disconnect fix
Adds an [Unreleased] block for the fb68ea5 disconnect fix (cross-references
the upstream PR awalol/DS5Dongle#101), and a [0.6.0-oled-edition] entry
covering the GitHub release / tag that wraps the existing 0.6.0-rebase code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:54:57 -06:00
MarcelineVPQandClaude Opus 4.7 fb68ea5608 fix(batt-led): force LED off + disarm on disconnect
The low-battery blink would stay frozen mid-cycle (or briefly resume
during reconnect retries) after a controller died and lost connection,
because the stale-report check stopped toggling but never turned the
LED off, and the cached interrupt_in_data[52] still read low.

New `battery_led_on_disconnect()` clears blink state, forces the 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.
Called from bt.cpp's HCI_EVENT_DISCONNECTION_COMPLETE handler.

Stale-check in the tick also now forces LED off when it fires while a
blink was in progress (defense in depth for unclean disconnects).

Same bug exists in upstream awalol/DS5Dongle (their battery_led.cpp is
byte-identical to ours pre-fix) — Sura Academy reported it in their
Discord. Plan to send this back as a PR after we validate it locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 11:19:55 -06:00
10 changed files with 246 additions and 11 deletions
+34 -2
View File
@@ -104,8 +104,40 @@ jobs:
cmake --build build/debug --target ds5-bridge
cp build/debug/ds5-bridge-oled.uf2 "artifacts/ds5-bridge-oled-debug-${{ github.event.release.tag_name }}.uf2"
- name: Upload UF2 files to release
- name: Compute UF2 checksums + append to release notes
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.event.release.tag_name }}
run: |
# Generate SHA256SUMS.txt next to the UF2s
( cd artifacts && sha256sum *.uf2 > SHA256SUMS.txt )
cat artifacts/SHA256SUMS.txt
# Append the same hashes (markdown-formatted) to the existing release body
BODY=$(gh release view "$TAG" --json body --jq .body)
{
printf '%s\n' "$BODY"
printf '\n---\n\n## Checksums\n\n```\n'
cat artifacts/SHA256SUMS.txt
printf '```\n'
} > /tmp/release-notes.md
gh release edit "$TAG" --notes-file /tmp/release-notes.md
- name: Upload UF2 files + SHA256SUMS to release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload "${{ github.event.release.tag_name }}" artifacts/*.uf2 --clobber
gh release upload "${{ github.event.release.tag_name }}" artifacts/*.uf2 artifacts/SHA256SUMS.txt --clobber
# Notify the web config repo so its GH Pages deploy rebuilds and
# bundles this fresh UF2 into the site. If the WEB_REPO_DISPATCH_PAT
# secret isn't configured on this repo, the dispatch call fails 401
# and continue-on-error swallows it — the web app keeps serving the
# previously-bundled UF2 until its own deploy is manually triggered.
- name: Trigger web config rebuild
continue-on-error: true
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.WEB_REPO_DISPATCH_PAT }}
repository: MarcelineVPQ/DS5Dongle-OLED-Config-Web
event-type: firmware-released
client-payload: '{"tag": "${{ github.event.release.tag_name }}", "title": "${{ github.event.release.name }}"}'
+29
View File
@@ -6,6 +6,35 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
---
## [Unreleased]
---
## [0.6.1-oled-edition] — 2026-05-18
Tagged release of the v0.6.0-oled-edition follow-up. UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.1-oled-edition) (built by `.github/workflows/release.yml`).
### 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).
---
## [0.6.0-oled-edition] — 2026-05-17
Tagged release of the rebase below. UF2s attached to [the GitHub release](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases/tag/v0.6.0-oled-edition) (built by `.github/workflows/release.yml`). No code changes vs the rebase; tag exists so users can install from a stable artifact.
---
## [0.6.0-rebase] — 2026-05-17
Rebased onto upstream `awalol/DS5Dongle` `v0.6.0-hotfix`. All OLED Edition features preserved with no user-visible regression.
+1
View File
@@ -171,6 +171,7 @@ target_link_libraries(ds5-bridge
hardware_timer
hardware_flash
hardware_spi
hardware_adc
pico_btstack_classic
# pico_cyw43_arch_threadsafe_background
pico_cyw43_arch_poll
+2 -2
View File
@@ -4,7 +4,7 @@
> Turn a Raspberry Pi Pico2W into a wireless adapter for the DualSense (DS5) controller — with an optional on-board status display.
> **OLED Edition** is a fork of **[awalol/DS5Dongle](https://github.com/awalol/DS5Dongle)** (upstream) that adds an optional Pico-OLED-1.3 128×64 display add-on with 10 screens (status, 4-slot multi-controller pairing, lightbar color picker with favorites and effect presets, trigger test, gyro tilt, touchpad, diagnostics, BT signal strength, audio VU meters, and a persistent settings menu), plus a DS5 button-combo soft-reboot. Upstream is the authoritative source for the core bridge firmware; this fork tracks it and layers add-on features on top.
> **OLED Edition** is a fork of **[awalol/DS5Dongle](https://github.com/awalol/DS5Dongle)** (upstream) that adds an optional Pico-OLED-1.3 128×64 display add-on with 11 screens (status, 4-slot multi-controller pairing, lightbar color picker with favorites and effect presets, trigger test, gyro tilt, touchpad, diagnostics, CPU/clock, BT signal strength, audio VU meters, and a persistent settings menu), plus a DS5 button-combo soft-reboot. Upstream is the authoritative source for the core bridge firmware; this fork tracks it and layers add-on features on top.
## Overview
@@ -22,7 +22,7 @@ This project enables the Raspberry Pi Pico2W to function as a Bluetooth bridge f
**OLED Edition additions:**
- Optional Pico-OLED-1.3 status display with **10 screens** (status, slots, lightbar, trigger test, gyro tilt, touchpad, diagnostics, RSSI, VU meters, settings)
- Optional Pico-OLED-1.3 status display with **11 screens** (status, slots, lightbar, trigger test, gyro tilt, touchpad, diagnostics, CPU/clock, RSSI, VU meters, settings)
- **4-slot persistent multi-controller pairing** — bond up to four DualSenses, switch between them from the OLED, slot 0 reconnects automatically on boot
- **Lightbar color picker** with 4 user favorite slots + breathing / rainbow / fade effect presets
- **Persistent settings menu** for the 8 firmware config fields (haptics gain, speaker volume, polling rate, etc.) with hold-to-confirm Reset and Wipe-all-slots actions
+19 -1
View File
@@ -37,11 +37,29 @@ void battery_led_note_report(void) {
last_report_us = time_us_64();
}
void battery_led_on_disconnect(void) {
// Stop any in-progress blink and force the LED off immediately. Zero
// last_report_us so the tick's stale-check early-returns until a fresh
// 0x31 report arrives on the next connection — prevents the cached
// low-battery byte from re-arming a blink during reconnect retries.
blinking = false;
led_state = false;
last_report_us = 0;
last_toggle_us = 0;
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false);
}
void battery_led_tick(void) {
const uint64_t now = time_us_64();
if (last_report_us == 0 || (now - last_report_us) >= REPORT_STALE_US) {
// No fresh data — bt.cpp owns the LED while disconnected.
// No fresh data — bt.cpp owns the LED while disconnected. If we
// were mid-blink when the report went stale, force the LED off
// so it doesn't freeze in whichever half-cycle it was in.
if (blinking) {
blinking = false;
led_state = false;
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false);
}
return;
}
+8
View File
@@ -16,3 +16,11 @@ void battery_led_tick(void);
// has been copied into interrupt_in_data. Used to detect disconnection
// via stale-report timeout.
void battery_led_note_report(void);
// Call from the BT disconnect handler. Cancels any in-progress blink,
// forces the LED off, and arms the module so it ignores the cached
// (now-stale) battery byte until a fresh report arrives on the next
// connection. Without this, the LED can stay frozen in whichever state
// it was at the moment of disconnect, or briefly resume blinking during
// reconnect retries while interrupt_in_data[52] still reads low.
void battery_led_on_disconnect(void);
+6
View File
@@ -19,6 +19,9 @@
#include "state_mgr.h"
#include "pico/util/queue.h"
#include "slots.h"
#if ENABLE_BATT_LED
#include "battery_led.h"
#endif
#define MTU_CONTROL 672
#define MTU_INTERRUPT 672
@@ -454,6 +457,9 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p
hid_interrupt_cid = 0;
feature_data.clear();
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false);
#if ENABLE_BATT_LED
battery_led_on_disconnect();
#endif
printf("[HCI] Disconnected reason=0x%02X, start inquiry\n", reason);
gap_inquiry_start(30);
break;
+69 -1
View File
@@ -14,6 +14,39 @@
#include "device/usbd.h"
#include "pico/time.h"
#include "slots.h"
#include "hardware/clocks.h"
#include "hardware/adc.h"
#include "hardware/vreg.h"
uint16_t cpu_temp_raw_smoothed() {
// One-time ADC bring-up. This is the only place the ADC is initialised
// now (oled.cpp's CPU screen calls through here too). Runs on core0
// under the cooperative main loop; adc_select_input(4) is set before
// every read, so the shared ADC needs no locking.
static bool adc_ready = false;
if (!adc_ready) {
adc_init();
adc_set_temp_sensor_enabled(true);
adc_ready = true;
}
adc_select_input(4);
// The temp sensor has a shallow slope (-1.721 mV/C) and ~1 LSB ≈ 0.47 C,
// so a lone 12-bit sample swings several tenths of a degree frame to
// frame. Average a big block to kill that...
constexpr int kSamples = 256;
uint32_t acc = 0;
for (int i = 0; i < kSamples; i++) acc += adc_read();
const float mean = (float)acc / (float)kSamples;
// ...then a slow EMA so the displayed value glides to the true die
// temperature rather than mirroring the latest block. Seeded on the
// first call so it doesn't ramp up from zero.
static float ema = -1.0f;
if (ema < 0.0f) ema = mean;
else ema += (mean - ema) * 0.15f;
return (uint16_t)(ema + 0.5f);
}
bool is_pico_cmd(uint8_t report_id) {
if (report_id == 0xf6 ||
@@ -21,7 +54,8 @@ bool is_pico_cmd(uint8_t report_id) {
report_id == 0xf8 ||
report_id == 0xf9 ||
report_id == 0xfa ||
report_id == 0xfb
report_id == 0xfb ||
report_id == 0xfc
) {
return true;
}
@@ -93,6 +127,40 @@ uint16_t pico_cmd_get(uint8_t report_id, uint8_t *buffer, uint16_t reqlen) {
memcpy(buffer + 14, &hci_errs, 4);
return want;
}
if (report_id == 0xfc) {
// OLED Edition: CPU / Clock telemetry for the web emulator. 11 bytes:
// [0..3] set_khz uint32 configured clk_sys (SYS_CLOCK_KHZ)
// [4..7] real_khz uint32 measured clk_sys (cached, see below)
// [8] vcode uint8 vreg_get_voltage() raw enum code
// [9..10] temp_raw uint16 ADC ch4 12-bit reading
// The web side does the volts/temperature math (same formulas as
// render_screen_cpu) so the firmware HID path stays float-free.
constexpr uint16_t want = 11;
if (reqlen < want) {
printf("[HID] 0xfc reqlen=%u too small for cpu payload (%u)\n", reqlen, want);
return 0;
}
const uint32_t set_khz = (uint32_t)SYS_CLOCK_KHZ;
// clk_sys is fixed at boot and frequency_count_khz() busy-waits a few
// ms — measure exactly once (lazily) and cache. Doing it here on the
// first poll keeps it off the boot path; one ~ms stall in a single
// GET_REPORT is acceptable.
static uint32_t cached_real_khz = 0;
if (cached_real_khz == 0) {
cached_real_khz = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_CLK_SYS);
}
const uint16_t temp_raw = cpu_temp_raw_smoothed();
const uint8_t vcode = (uint8_t)vreg_get_voltage();
memcpy(buffer + 0, &set_khz, 4);
memcpy(buffer + 4, &cached_real_khz, 4);
buffer[8] = vcode;
memcpy(buffer + 9, &temp_raw, 2);
return want;
}
return 0;
}
+7
View File
@@ -11,4 +11,11 @@ bool is_pico_cmd(uint8_t report_id);
uint16_t pico_cmd_get(uint8_t report_id, uint8_t *buffer,uint16_t reqlen);
void pico_cmd_set(uint8_t report_id, uint8_t const *buffer,uint16_t bufsize);
// Smoothed RP2350 on-die temperature sensor reading (ADC input 4, 12-bit
// raw). A single sample is very noisy; this averages a large block and runs
// a slow EMA so the value converges to the true die temperature instead of
// chasing per-sample noise. Single source of truth — the OLED CPU screen and
// the 0xfc web telemetry both call this so device and web always agree.
uint16_t cpu_temp_raw_smoothed();
#endif //DS5_BRIDGE_CMD_H
+70 -4
View File
@@ -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;