velxio/backend/app/services/esp32_signals.py

83 lines
3.5 KiB
Python

"""ESP32 GPIO Matrix output signal source IDs.
Lifted from the ESP32 Technical Reference Manual (Espressif ESP32 TRM
section 4.11, "IO_MUX and GPIO Matrix"). Each output GPIO has a
configuration register `GPIO_FUNCx_OUT_SEL_CFG_REG[x]` whose low 9
bits (`FUNCx_OUT_SEL`) select one of 256 internal peripheral signals
to drive the pin. The constants below name the signals that velxio
actually emulates today; add more here when a new peripheral wants
to participate in the SignalRouter.
The signal ID range mirrors the QEMU plugin's interpretation of
`gpio_out_sel`; the existing worker code at
`esp32_worker.py:_refresh_ledc_gpio_map` already reads these values
out of the matrix via `qemu_picsimlab_get_internals(2)`.
"""
from __future__ import annotations
# ── LEDC (PWM peripheral) ─────────────────────────────────────────────────
# Per ESP32 Technical Reference Manual section 4.11, Table 4-3 (GPIO
# Matrix output signals):
# 71-78 → LEDC_HS_SIG_OUT[0..7] (high-speed channels 0-7)
# 79-86 → LEDC_LS_SIG_OUT[0..7] (low-speed channels 0-7)
#
# The legacy worker code at esp32_worker.py:426 used the off-by-one
# range 72-87; that masked itself because the channel index encoded in
# the 0x5000 duty callback (0..15) was internally consistent with the
# bogus signal-id math, so single-servo demos still appeared to work.
# Multi-servo projects (e.g. solar-tracker, project 5218f9e3) exposed
# the bug — `ledcAttachPin(13, 0)` actually writes signal 71 to
# gpio_out_sel[13], which the off-by-one scan REJECTED, so channel 0
# resolved to GPIO 12 (the next servo's pin, whose signal 72 WAS in
# range and was misinterpreted as channel 0).
SIG_LEDC_HS_CH0_OUT_IDX = 71 # add N for HS channel N (0..7)
SIG_LEDC_HS_CH_LAST = 78
SIG_LEDC_LS_CH0_OUT_IDX = 79 # add N for LS channel N (0..7)
SIG_LEDC_LS_CH_LAST = 86
def ledc_signal_for_channel(channel: int) -> int:
"""Map a velxio-style unified LEDC channel index (0..15) to its
GPIO Matrix signal source id.
The ESP32 LEDC hardware has two channel groups: 8 high-speed (HS)
and 8 low-speed (LS). velxio unifies them into a single 0..15
space where ch 0-7 = HS, ch 8-15 = LS (matches the encoding the
QEMU plugin emits on the 0x5000 duty callback).
"""
if not 0 <= channel < 16:
raise ValueError(f"ledc channel out of range: {channel}")
if channel < 8:
return SIG_LEDC_HS_CH0_OUT_IDX + channel
return SIG_LEDC_LS_CH0_OUT_IDX + (channel - 8)
def channel_for_ledc_signal(signal_id: int) -> int | None:
"""Inverse of :func:`ledc_signal_for_channel`. Returns None when
the signal id is not an LEDC channel."""
if SIG_LEDC_HS_CH0_OUT_IDX <= signal_id <= SIG_LEDC_HS_CH_LAST:
return signal_id - SIG_LEDC_HS_CH0_OUT_IDX
if SIG_LEDC_LS_CH0_OUT_IDX <= signal_id <= SIG_LEDC_LS_CH_LAST:
return 8 + (signal_id - SIG_LEDC_LS_CH0_OUT_IDX)
return None
# ── Sentinel for "GPIO not routed to any peripheral" ──────────────────────
# When `gpio_out_sel[N]` carries this value the pin is driven by
# normal GPIO output (the value in the GPIO_OUT_REG bit N), not a
# peripheral signal.
SIG_GPIO_DIRECT_OUT_IDX = 256
__all__ = [
"SIG_LEDC_HS_CH0_OUT_IDX",
"SIG_LEDC_HS_CH_LAST",
"SIG_LEDC_LS_CH0_OUT_IDX",
"SIG_LEDC_LS_CH_LAST",
"SIG_GPIO_DIRECT_OUT_IDX",
"ledc_signal_for_channel",
"channel_for_ledc_signal",
]