4"""Gate: the C6 Kconfig defaults agree with the pin map they are derived from.
8``coprocessor/esp32c6/pins.env`` is the SINGLE SOURCE OF TRUTH for the
9RA8-host <-> ESP32-C6 SPI wiring. It is a plain ``KEY=value`` fragment because
10its consumers are shell (``build.sh``, ``flash.sh``) and, on the RA8 side,
11humans reading one file to learn the pinout.
13``coprocessor/esp32c6/sdkconfig.defaults`` carries the SAME numbers again in
14Kconfig syntax, because that is the only form esp-idf reads. It is a DERIVED
15artifact, and it is deliberately byte-stable: the bench-proven C6 image was
16built from exactly these lines, so it is verified against pins.env rather than
19Two files holding one fact drift silently, and a drift here is expensive: the
20build succeeds, the firmware flashes, and the SPI link simply never comes up
21because the C6 is driving a different pin than the RA8 is. Nothing downstream
22of the mistake can detect it -- which is what makes this a gate and not a
27Every SPI signal (CS, COPI/CIPO, SCK, DATA_READY, HANDSHAKE, RESET), the chip
28target, the flash size, and the dev-board preset. The last two are the
29interesting ones: esp-idf and esp-hosted-mcu encode both in the KEY
30(``CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y``, ``CONFIG_ESP_HOST_DEV_BOARD_NONE=y``)
31rather than in the value, so each is parsed out of the key name and compared
32against ``C6_FLASH_SIZE`` / ``C6_DEV_BOARD``.
34The dev-board preset is load-bearing and easy to miss. esp-hosted-mcu ships
35presets for Espressif's own dev boards, and selecting one OVERRIDES the SPI pin
36leaves -- so with any board but ``NONE`` selected, every pin this gate compares
37is silently ignored by the build and the C6 comes up on the preset's pins
38instead of ours. Comparing the enabled symbol's SUFFIX rather than merely
39asserting the symbol is present catches the drift in both of its shapes: the
40selection vanishing, and the selection moving to a different board.
42Two of the data signals carry Espressif's legacy names in their Kconfig symbols.
43Those symbols belong to upstream and cannot be renamed from here; the mapping
44table is where they are tied to this project's COPI/CIPO vocabulary.
46What is checked on the RA8 side
47-------------------------------
48pins.env also records where each signal LANDS on the EK-RA8D2 (MCU pin and
49J26 hole) and which SW4 DIP positions the link needs. That map IS restated in
50one other place -- ``port/esp-hosted/inc/ra8_esp_hosted_pins.h``, which the
51esp-hosted port compiles against -- so the two are diffed here. They have
52already drifted once: the port header was first written from the probe's
53candidate list while the module was disconnected, and the rebuilt harness was
54later characterised with HANDSHAKE and DATA_READY the other way round. A pin
55map that only one file knows is a pin map that will be wrong again.
57Independently of that diff, the map is checked for the failures that actually
58happen to a pin map: a signal named at one end and not the other, a pin or
59hole name that is not a pin or hole name, two signals silently claiming the
60same pin after a copy-paste, and a missing SW4 position. That bank is not
61incidental: SW4-4 ON with SW4-3 OFF holds the
62Pmod1 bus switches open, so J26-1..J26-4 never reach the MCU, and misreading
63it cost a full bench day chasing a harness that was fine.
65This is a pure text comparison of two committed files -- no esp-idf, no
66toolchain, no hardware -- so it runs in CI on any box. ``build.sh`` invokes
67this same script before it builds, so the bench and CI apply one rule.
71``--selftest`` drives the comparator with crafted file bodies: an agreeing
72pair must be silent, and a disagreeing pair, a missing key on either side, a
73missing flash-size key, and a dev-board preset that either moved to another
74board or stopped being selected must each be reported.
78 check_c6_pin_config.py # gate (fail on any drift)
79 check_c6_pin_config.py --selftest # prove the comparator both ways
81Exit 0 when the two files agree, 1 on drift or a failing selftest, 2 when a
85from __future__
import annotations
89from pathlib
import Path
91REPO_ROOT = Path(__file__).resolve().parents[2]
92C6_DIR = REPO_ROOT /
"coprocessor" /
"esp32c6"
93PINS_ENV = C6_DIR /
"pins.env"
94SDKCONFIG = C6_DIR /
"sdkconfig.defaults"
96 REPO_ROOT /
"libs" /
"third_party" /
"esp-hosted" /
"host" /
"esp_hosted_host_fw_ver.h"
107PIN_PAIRS: tuple[tuple[str, str, str], ...] = (
108 (
"CONFIG_ESP_SPI_HSPI_GPIO_CS",
"C6_PIN_CS",
"CS (Chip Select)"),
109 (
"CONFIG_ESP_SPI_HSPI_GPIO_MOSI",
"C6_PIN_COPI",
"COPI (Controller Out)"),
110 (
"CONFIG_ESP_SPI_HSPI_GPIO_MISO",
"C6_PIN_CIPO",
"CIPO (Controller In)"),
111 (
"CONFIG_ESP_SPI_HSPI_GPIO_CLK",
"C6_PIN_SCK",
"SCK (clock)"),
112 (
"CONFIG_ESP_SPI_GPIO_DATA_READY",
"C6_PIN_DATA_READY",
"DATA_READY"),
113 (
"CONFIG_ESP_SPI_GPIO_HANDSHAKE",
"C6_PIN_HANDSHAKE",
"HANDSHAKE"),
114 (
"CONFIG_ESP_SPI_GPIO_RESET",
"C6_PIN_RESET",
"RESET"),
118VALUE_PAIRS: tuple[tuple[str, str, str], ...] = (
119 (
"CONFIG_IDF_TARGET",
"ESP_TARGET",
"chip target"),
123_FLASHSIZE_RE = re.compile(
r"^CONFIG_ESPTOOLPY_FLASHSIZE_([0-9]+MB)$")
124FLASH_SIZE_KEY =
"C6_FLASH_SIZE"
129_DEV_BOARD_RE = re.compile(
r"^CONFIG_ESP_HOST_DEV_BOARD_([A-Z0-9_]+)$")
130DEV_BOARD_KEY =
"C6_DEV_BOARD"
135RA8_TRIPLES: tuple[tuple[str, str, str, str], ...] = (
136 (
"CS (Chip Select)",
"C6_PIN_CS",
"RA8_PIN_CS",
"RA8_J26_CS"),
137 (
"COPI (Controller Out)",
"C6_PIN_COPI",
"RA8_PIN_COPI",
"RA8_J26_COPI"),
138 (
"CIPO (Controller In)",
"C6_PIN_CIPO",
"RA8_PIN_CIPO",
"RA8_J26_CIPO"),
139 (
"SCK (clock)",
"C6_PIN_SCK",
"RA8_PIN_SCK",
"RA8_J26_SCK"),
140 (
"DATA_READY",
"C6_PIN_DATA_READY",
"RA8_PIN_DATA_READY",
"RA8_J26_DATA_READY"),
141 (
"HANDSHAKE",
"C6_PIN_HANDSHAKE",
"RA8_PIN_HANDSHAKE",
"RA8_J26_HANDSHAKE"),
142 (
"RESET",
"C6_PIN_RESET",
"RA8_PIN_RESET",
"RA8_J26_RESET"),
148SW4_KEYS: tuple[str, ...] = (
"RA8_SW4_1",
"RA8_SW4_2",
"RA8_SW4_3",
"RA8_SW4_4")
149SW4_VALUES: tuple[str, ...] = (
"ON",
"OFF")
157_RA8_PIN_RE = re.compile(
r"^P[0-9]{3}$")
159PORT_PIN_HEADER = REPO_ROOT /
"port" /
"esp-hosted" /
"inc" /
"ra8_esp_hosted_pins.h"
160"""The esp-hosted port's copy of the RA8-side map, diffed against pins.env."""
162PORT_PIN_ROWS: tuple[tuple[str, str, str], ...] = (
163 (
"CS",
"k_ra8_esp_hosted_pin_chip_select",
"RA8_PIN_CS"),
164 (
"COPI",
"k_ra8_esp_hosted_pin_copi",
"RA8_PIN_COPI"),
165 (
"CIPO",
"k_ra8_esp_hosted_pin_cipo",
"RA8_PIN_CIPO"),
166 (
"SCK",
"k_ra8_esp_hosted_pin_sck",
"RA8_PIN_SCK"),
167 (
"HANDSHAKE",
"k_ra8_esp_hosted_pin_handshake",
"RA8_PIN_HANDSHAKE"),
168 (
"DATA_READY",
"k_ra8_esp_hosted_pin_data_ready",
"RA8_PIN_DATA_READY"),
169 (
"RESET",
"k_ra8_esp_hosted_pin_reset",
"RA8_PIN_RESET"),
171"""Signal, the port header's enumerator, and the pins.env key it must match."""
173BOARD_SYMBOL_TO_PIN: dict[str, str] = {
174 "k_ra8_board_pmod1_spi_cs":
"P804",
175 "k_ra8_board_pmod1_spi_copi":
"P801",
176 "k_ra8_board_pmod1_spi_cipo":
"P802",
177 "k_ra8_board_pmod1_spi_sck":
"P803",
178 "k_ra8_board_pmod1_irq":
"P006",
179 "k_ra8_board_pmod1_reset":
"P402",
180 "k_ra8_board_pmod1_gpio_a":
"P412",
181 "k_ra8_board_pmod1_gpio_b":
"P413",
182 "k_ra8_pin_none":
"none",
184"""Board-layer Pmod1 enumerators and the MCU pin each names.
186The port header cites board symbols rather than pin numbers -- that is the
187point of the board layer -- so resolving them is what lets the two files be
188compared at all. The values come from
189``libs/ra8_board_ek_ra8d2/inc/ra8_board_ek_ra8d2_connectors.h``, which carries
190the board User's Manual citation for every row.
193_PORT_ROW_RE = re.compile(
194 r"(?P<enum>k_ra8_esp_hosted_pin_[a-z_]+)\s*=\s*\(uint16_t\)(?P<sym>k_ra8_[a-z0-9_]+)"
198def parse_port_header(text: str) -> dict[str, str]:
199 """Return {enumerator: board symbol} for every row of the port pin map."""
200 return {m.group(
"enum"): m.group(
"sym")
for m
in _PORT_ROW_RE.finditer(text)}
203def check_port_header(pins: dict[str, str], header: str) -> list[str]:
204 """Return one message per disagreement with the port's pin map.
207 pins: Parsed pins.env assignments (the source of truth).
208 header: Text of ``ra8_esp_hosted_pins.h``.
211 Human-readable findings; empty when the header agrees with pins.env.
213 rows = parse_port_header(header)
214 findings: list[str] = []
215 for label, enum_name, pin_key
in PORT_PIN_ROWS:
216 if enum_name
not in rows:
217 findings.append(f
"{label}: the port pin header does not define {enum_name}")
219 symbol = rows[enum_name]
220 if symbol
not in BOARD_SYMBOL_TO_PIN:
222 f
"{label}: the port pin header names {symbol}, which this gate cannot "
223 f
"resolve to an MCU pin; add it to BOARD_SYMBOL_TO_PIN"
226 if pin_key
not in pins:
227 findings.append(f
"{label}: pins.env is missing {pin_key}")
229 resolved = BOARD_SYMBOL_TO_PIN[symbol]
230 if resolved != pins[pin_key]:
232 f
"{label}: the port pin header says {symbol} ({resolved}) "
233 f
"but pins.env says {pin_key}={pins[pin_key]}"
238_RA8_HOLE_RE = re.compile(
r"^J26-([0-9]{1,2})$")
240_ASSIGN_RE = re.compile(
r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$")
246def parse_assignments(text: str) -> dict[str, str]:
247 """Parse ``KEY=value`` lines, ignoring comments and blanks.
249 Handles both file formats: the shell fragment and Kconfig defaults share
250 this syntax, and Kconfig's quoted string values are unquoted here so
251 ``CONFIG_IDF_TARGET="esp32c6"`` compares equal to ``ESP_TARGET=esp32c6``.
257 Mapping of key to unquoted value.
259 out: dict[str, str] = {}
260 for raw
in text.splitlines():
262 if not line
or line.startswith(
"#"):
264 match = _ASSIGN_RE.match(line)
267 key, value = match.group(1), match.group(2)
268 if len(value) >= _MIN_QUOTED_LEN
and value[0] == value[-1]
and value[0]
in "\"'":
274def _flash_size(sdk: dict[str, str]) -> str |
None:
275 """Return the flash size encoded in an enabled FLASHSIZE symbol.
278 sdk: Parsed sdkconfig assignments.
281 The size text (e.g. "16MB"), or None when no such symbol is enabled.
283 for key, value
in sdk.items():
284 match = _FLASHSIZE_RE.match(key)
285 if match
is not None and value ==
"y":
286 return match.group(1)
290def _dev_board(sdk: dict[str, str]) -> str |
None:
291 """Return the dev-board preset encoded in an enabled DEV_BOARD symbol.
294 sdk: Parsed sdkconfig assignments.
297 The board name lower-cased (e.g. "none"), or None when no such symbol
298 is enabled -- which is itself a finding, because the preset decides
299 whether our pin leaves are honoured at all.
301 for key, value
in sdk.items():
302 match = _DEV_BOARD_RE.match(key)
303 if match
is not None and value ==
"y":
304 return match.group(1).lower()
308def _check_one_signal(label: str, c6: str, ra8_pin: str, ra8_hole: str) -> list[str]:
309 """Return findings for one signal's RA8-side entry.
312 label: Human-readable signal name.
313 c6: C6 GPIO number as written in pins.env.
314 ra8_pin: RA8 landing pin, or "none".
315 ra8_hole: J26 hole, or "none".
318 Human-readable findings; empty when the entry is well-formed.
320 unwired_c6 = c6 == UNWIRED_C6
321 unwired_ra8 = ra8_pin == UNWIRED_RA8
and ra8_hole == UNWIRED_RA8
322 if unwired_c6 != unwired_ra8:
324 f
"{label}: half-recorded connection -- C6 side is "
325 f
"{'disconnected' if unwired_c6 else c6} but RA8 side is "
326 f
"{ra8_pin}/{ra8_hole}"
331 findings: list[str] = []
332 if _RA8_PIN_RE.match(ra8_pin)
is None:
333 findings.append(f
"{label}: RA8 pin {ra8_pin!r} is not a Pnnn pin name or 'none'")
334 if _RA8_HOLE_RE.match(ra8_hole)
is None:
335 findings.append(f
"{label}: J26 hole {ra8_hole!r} is not a J26-n hole or 'none'")
339def _check_uniqueness(pins: dict[str, str]) -> list[str]:
340 """Return findings for any RA8 pin or J26 hole claimed by two signals.
343 pins: Parsed pins.env assignments.
346 Human-readable findings; empty when every wired entry is unique.
348 findings: list[str] = []
349 for what, index
in ((
"RA8 pin", 2), (
"J26 hole", 3)):
350 seen: dict[str, str] = {}
351 for triple
in RA8_TRIPLES:
352 value = pins.get(triple[index], UNWIRED_RA8)
353 if value == UNWIRED_RA8:
356 findings.append(f
"{what} {value} is claimed by both {seen[value]} and {triple[0]}")
357 seen[value] = triple[0]
361def check_ra8_side(pins: dict[str, str]) -> list[str]:
362 """Return one message per defect in the RA8-side half of the pin map.
364 The C6-side numbers are checked against sdkconfig.defaults by ``compare``;
365 nothing downstream can check the RA8 side that way, because no second file
366 restates it. What is checkable is that the map is COMPLETE and INTERNALLY
367 CONSISTENT: every signal names both ends or neither, the names are
368 well-formed, no two signals claim one pin or one hole, and the SW4
369 positions the link depends on are recorded.
372 pins: Parsed pins.env assignments.
375 Human-readable findings; empty when the RA8 side is well-formed.
377 findings: list[str] = []
378 for label, c6_key, pin_key, hole_key
in RA8_TRIPLES:
379 missing = [k
for k
in (c6_key, pin_key, hole_key)
if k
not in pins]
381 findings.extend(f
"{label}: pins.env is missing {k}" for k
in missing)
383 findings.extend(_check_one_signal(label, pins[c6_key], pins[pin_key], pins[hole_key]))
385 findings.extend(_check_uniqueness(pins))
389 findings.append(f
"SW4 positions: pins.env is missing {key}")
390 elif pins[key]
not in SW4_VALUES:
391 findings.append(f
"SW4 positions: {key}={pins[key]!r} is not one of {SW4_VALUES}")
395FW_VERSION_KEY =
"ESP_HOSTED_MCU_FW_VERSION"
398_FW_VER_MACROS: tuple[str, str, str] = (
399 "ESP_HOSTED_VERSION_MAJOR_1",
400 "ESP_HOSTED_VERSION_MINOR_1",
401 "ESP_HOSTED_VERSION_PATCH_1",
405def parse_host_fw_version(text: str) -> str |
None:
406 """Return the vendored host driver's own version as ``major.minor.patch``.
409 text: Body of ``host/esp_hosted_host_fw_ver.h`` from the vendor tree.
412 The dotted version, or ``None`` when any of the three macros is absent
413 (which means the vendored header changed shape and this check can no
414 longer speak for it -- reported, never silently skipped).
416 parts: list[str] = []
417 for macro
in _FW_VER_MACROS:
418 match = re.search(rf
"^\s*#define\s+{re.escape(macro)}\s+(\d+)\s*$", text, re.MULTILINE)
421 parts.append(match.group(1))
422 return ".".join(parts)
425def check_fw_version_lock(pins: dict[str, str], header_text: str) -> list[str]:
426 """Return findings when the co-processor image and the host driver disagree.
428 ``pins.env`` names the esp-hosted-mcu firmware the CO-PROCESSOR image is
429 built from; ``esp_hosted_host_fw_ver.h`` states the version of the HOST
430 driver vendored into this tree. Both come from one pinned upstream commit,
431 so they must be identical -- and upstream itself compares them at run time
432 and warns about "RPC timeouts" when they are not.
434 This is a gate rather than a convention because the mismatch is otherwise
435 silent in CI and expensive on the bench:
436 ``examples/ek_ra8d2/hw_validated/c6/c6_fw_version`` asserts the
437 co-processor's reported version against these very macros, so bumping the
438 vendor pin without reflashing the C6 turns a hardware test red with no
439 hint, from here, of why.
442 pins: Parsed pins.env assignments.
443 header_text: Body of the vendored host firmware-version header.
446 Human-readable findings; empty when the two agree.
448 findings: list[str] = []
449 host = parse_host_fw_version(header_text)
452 "firmware version: could not read "
453 f
"{', '.join(_FW_VER_MACROS)} out of {HOST_FW_VER.name} "
454 "-- the vendored header changed shape"
457 if FW_VERSION_KEY
not in pins:
458 findings.append(f
"firmware version: pins.env is missing {FW_VERSION_KEY}")
460 if pins[FW_VERSION_KEY] != host:
462 f
"firmware version: pins.env {FW_VERSION_KEY}={pins[FW_VERSION_KEY]} "
463 f
"but the vendored host driver is {host} "
464 "-- host and co-processor must come from one upstream commit"
469def compare(pins: dict[str, str], sdk: dict[str, str]) -> list[str]:
470 """Return one message per disagreement between the two parsed files.
473 pins: Parsed pins.env assignments (the source of truth).
474 sdk: Parsed sdkconfig.defaults assignments (the derived artifact).
477 Human-readable findings; empty when the two agree.
479 findings: list[str] = []
481 for sdk_key, pin_key, label
in PIN_PAIRS + VALUE_PAIRS:
482 if pin_key
not in pins:
483 findings.append(f
"{label}: pins.env is missing {pin_key}")
485 if sdk_key
not in sdk:
486 findings.append(f
"{label}: sdkconfig.defaults is missing {sdk_key}")
488 if pins[pin_key] != sdk[sdk_key]:
490 f
"{label}: pins.env {pin_key}={pins[pin_key]} "
491 f
"but sdkconfig.defaults {sdk_key}={sdk[sdk_key]}"
494 size = _flash_size(sdk)
495 if FLASH_SIZE_KEY
not in pins:
496 findings.append(f
"flash size: pins.env is missing {FLASH_SIZE_KEY}")
499 "flash size: sdkconfig.defaults enables no CONFIG_ESPTOOLPY_FLASHSIZE_<n>MB symbol"
501 elif size != pins[FLASH_SIZE_KEY]:
503 f
"flash size: pins.env {FLASH_SIZE_KEY}={pins[FLASH_SIZE_KEY]} "
504 f
"but sdkconfig.defaults enables CONFIG_ESPTOOLPY_FLASHSIZE_{size}"
507 board = _dev_board(sdk)
508 if DEV_BOARD_KEY
not in pins:
509 findings.append(f
"dev board: pins.env is missing {DEV_BOARD_KEY}")
512 "dev board: sdkconfig.defaults enables no CONFIG_ESP_HOST_DEV_BOARD_<X> "
513 "symbol -- without an explicit selection the pin leaves above may be "
514 "overridden by whatever preset upstream defaults to"
516 elif board != pins[DEV_BOARD_KEY]:
518 f
"dev board: pins.env {DEV_BOARD_KEY}={pins[DEV_BOARD_KEY]} "
519 f
"but sdkconfig.defaults enables CONFIG_ESP_HOST_DEV_BOARD_{board.upper()} "
520 "-- a dev-board preset OVERRIDES the SPI pin leaves, so every pin "
521 "compared above would be ignored by the build"
524 findings.extend(check_ra8_side(pins))
527 header = PORT_PIN_HEADER.read_text(encoding=
"utf-8")
528 except OSError
as exc:
529 findings.append(f
"port pin header: cannot read {PORT_PIN_HEADER}: {exc}")
531 findings.extend(check_port_header(pins, header))
553RA8_PIN_DATA_READY=P402
554RA8_PIN_HANDSHAKE=P006
560RA8_J26_DATA_READY=J26-8
561RA8_J26_HANDSHAKE=J26-7
573CONFIG_IDF_TARGET="esp32c6"
574CONFIG_ESP_HOST_DEV_BOARD_NONE=y
575CONFIG_ESP_SPI_HSPI_GPIO_CS=0
576CONFIG_ESP_SPI_HSPI_GPIO_MOSI=1
577CONFIG_ESP_SPI_HSPI_GPIO_MISO=2
578CONFIG_ESP_SPI_HSPI_GPIO_CLK=3
579CONFIG_ESP_SPI_GPIO_DATA_READY=4
580CONFIG_ESP_SPI_GPIO_HANDSHAKE=6
581CONFIG_ESP_SPI_GPIO_RESET=-1
582CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
586def _selftest_cases_kconfig() -> list[tuple[str, str, str, bool]]:
587 """Return the crafted cases for the pins.env <-> sdkconfig.defaults half.
590 One case per drift shape between the two files, plus the agreeing
591 control that must stay silent. The dev-board preset has its own group
592 (``_selftest_cases_dev_board``).
595 (
"agreeing pair", _GOOD_PINS, _GOOD_SDK,
False),
597 "a pin number drifted",
599 _GOOD_SDK.replace(
"GPIO_CLK=3",
"GPIO_CLK=7"),
603 "COPI drifted (legacy upstream symbol)",
604 _GOOD_PINS.replace(
"C6_PIN_COPI=1",
"C6_PIN_COPI=9"),
609 "sdkconfig lost a symbol",
611 _GOOD_SDK.replace(
"CONFIG_ESP_SPI_GPIO_HANDSHAKE=6\n",
""),
615 "pins.env lost a key",
616 _GOOD_PINS.replace(
"C6_PIN_DATA_READY=4\n",
""),
621 "flash size drifted",
623 _GOOD_SDK.replace(
"FLASHSIZE_16MB=y",
"FLASHSIZE_4MB=y"),
627 "no flash size enabled at all",
630 "CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y",
"CONFIG_ESPTOOLPY_FLASHSIZE_16MB=n"
635 "chip target drifted",
637 _GOOD_SDK.replace(
'CONFIG_IDF_TARGET="esp32c6"',
'CONFIG_IDF_TARGET="esp32c3"'),
643def _selftest_cases_dev_board() -> list[tuple[str, str, str, bool]]:
644 """Return the crafted cases for the dev-board preset.
646 Its two drift shapes are distinct and only one of them is obvious. A
647 DIFFERENT board silently overrides every SPI pin leaf, so the build still
648 succeeds and the C6 comes up on the preset's pins; NO board at all leaves
649 the choice to whatever upstream happens to default to. Asserting merely
650 that the symbol is present would miss the first, which is the worse one.
653 One case per drift shape. There is no agreeing control here: the
654 agreeing pair in ``_selftest_cases_kconfig`` already carries a matching
655 ``C6_DEV_BOARD`` / ``CONFIG_ESP_HOST_DEV_BOARD_NONE`` pair, so this
656 rule's quiet direction is proven there rather than restated.
660 "dev board drifted to another preset",
663 "CONFIG_ESP_HOST_DEV_BOARD_NONE=y",
664 "CONFIG_ESP_HOST_DEV_BOARD_ESP32_C6_DEVKITC=y",
669 "dev board selection turned off",
672 "CONFIG_ESP_HOST_DEV_BOARD_NONE=y",
"CONFIG_ESP_HOST_DEV_BOARD_NONE=n"
677 "sdkconfig selects no dev board at all",
679 _GOOD_SDK.replace(
"CONFIG_ESP_HOST_DEV_BOARD_NONE=y\n",
""),
683 "pins.env stopped declaring the dev board",
684 _GOOD_PINS.replace(
"C6_DEV_BOARD=none\n",
""),
691def _selftest_cases_ra8_signals() -> list[tuple[str, str, str, bool]]:
692 """Return the crafted cases for one signal's RA8-side entry.
695 One case per way a single signal can be recorded wrongly: named at
696 one end only, disconnected on one side but not the other, or given a
697 pin or hole name that is not one.
701 "RA8 landing pin never recorded",
702 _GOOD_PINS.replace(
"RA8_PIN_HANDSHAKE=P006\n",
""),
707 "RA8 J26 hole never recorded",
708 _GOOD_PINS.replace(
"RA8_J26_SCK=J26-4\n",
""),
713 "wired on the C6 side, 'none' on the RA8 side",
714 _GOOD_PINS.replace(
"RA8_PIN_DATA_READY=P402",
"RA8_PIN_DATA_READY=none").replace(
715 "RA8_J26_DATA_READY=J26-8",
"RA8_J26_DATA_READY=none"
721 "RESET given an RA8 pin while the C6 side stays -1",
722 _GOOD_PINS.replace(
"RA8_PIN_RESET=none",
"RA8_PIN_RESET=P412").replace(
723 "RA8_J26_RESET=none",
"RA8_J26_RESET=J26-9"
729 "RA8 pin name malformed",
730 _GOOD_PINS.replace(
"RA8_PIN_CS=P804",
"RA8_PIN_CS=P80_4"),
735 "J26 hole malformed",
736 _GOOD_PINS.replace(
"RA8_J26_CS=J26-1",
"RA8_J26_CS=pin1"),
743def _selftest_cases_ra8_map() -> list[tuple[str, str, str, bool]]:
744 """Return the crafted cases for whole-map RA8-side defects.
747 One case per defect that only shows up across signals -- two of them
748 claiming one pin or one hole after a copy-paste -- plus the SW4 bank
753 "two signals claim one RA8 pin",
754 _GOOD_PINS.replace(
"RA8_PIN_COPI=P801",
"RA8_PIN_COPI=P804"),
759 "two signals claim one J26 hole",
760 _GOOD_PINS.replace(
"RA8_J26_CIPO=J26-3",
"RA8_J26_CIPO=J26-2"),
765 "SW4 position never recorded",
766 _GOOD_PINS.replace(
"RA8_SW4_3=ON\n",
""),
771 "SW4 position is not ON or OFF",
772 _GOOD_PINS.replace(
"RA8_SW4_4=OFF",
"RA8_SW4_4=maybe"),
779_GOOD_PORT_HEADER =
"""
780 k_ra8_esp_hosted_pin_chip_select = (uint16_t)k_ra8_board_pmod1_spi_cs,
781 k_ra8_esp_hosted_pin_copi = (uint16_t)k_ra8_board_pmod1_spi_copi,
782 k_ra8_esp_hosted_pin_cipo = (uint16_t)k_ra8_board_pmod1_spi_cipo,
783 k_ra8_esp_hosted_pin_sck = (uint16_t)k_ra8_board_pmod1_spi_sck,
784 k_ra8_esp_hosted_pin_handshake = (uint16_t)k_ra8_board_pmod1_irq,
785 k_ra8_esp_hosted_pin_data_ready = (uint16_t)k_ra8_board_pmod1_reset,
786 k_ra8_esp_hosted_pin_reset = (uint16_t)k_ra8_pin_none,
788"""A port pin map that agrees with ``_GOOD_PINS``."""
791def _selftest_cases_port_header() -> list[tuple[str, str, str, bool]]:
792 """Return crafted cases for the port-header half of the comparator.
794 The port header is a second statement of the RA8-side map, so the cases
795 here are the ways two copies drift: a swapped pair (the drift that really
796 happened), a signal the header does not define, and a board symbol the
800 ``(label, header_text, expect_findings)`` triples, widened to the
801 four-tuple shape the shared driver consumes.
803 swapped = _GOOD_PORT_HEADER.replace(
804 "k_ra8_esp_hosted_pin_handshake = (uint16_t)k_ra8_board_pmod1_irq",
805 "k_ra8_esp_hosted_pin_handshake = (uint16_t)k_ra8_board_pmod1_reset",
808 (
"port header agrees", _GOOD_PORT_HEADER,
"",
False),
809 (
"port header swaps HANDSHAKE onto the DATA_READY pin", swapped,
"",
True),
811 "port header omits a signal",
812 _GOOD_PORT_HEADER.replace(
813 " k_ra8_esp_hosted_pin_sck = (uint16_t)k_ra8_board_pmod1_spi_sck,\n",
""
819 "port header names an unresolvable board symbol",
820 _GOOD_PORT_HEADER.replace(
"k_ra8_board_pmod1_spi_cs",
"k_ra8_board_pmod9_spi_cs"),
827_GOOD_FW_VER_HEADER =
"""
828#define ESP_HOSTED_VERSION_MAJOR_1 2
829#define ESP_HOSTED_VERSION_MINOR_1 12
830#define ESP_HOSTED_VERSION_PATCH_1 11
832"""A vendored host firmware-version header stating 2.12.11."""
835def _selftest_cases_fw_version() -> list[tuple[str, str, bool]]:
836 """Return crafted cases for the host / co-processor version lock.
838 The ways two statements of one version drift: a vendor bump with no
839 co-processor reflash, a co-processor reflash with no vendor bump, a
840 pins.env that stopped declaring the version at all, and a vendored header
841 whose macros were renamed out from under the parser.
844 ``(label, header_text, expect_findings)`` triples. The pins.env side is
845 held at 2.12.11 except where the label says otherwise.
847 bumped = _GOOD_FW_VER_HEADER.replace(
"MINOR_1 12",
"MINOR_1 13")
848 renamed = _GOOD_FW_VER_HEADER.replace(
"ESP_HOSTED_VERSION_PATCH_1",
"ESP_HOSTED_VER_PATCH")
850 (
"firmware version agrees", _GOOD_FW_VER_HEADER,
False),
851 (
"vendored host bumped without a co-processor reflash", bumped,
True),
852 (
"vendored header macros renamed", renamed,
True),
856def _selftest_fw_version_failures() -> list[str]:
857 """Return one message per version-lock selftest case that misbehaved."""
861 good = parse_assignments(_GOOD_PINS)
862 good[FW_VERSION_KEY] =
"2.12.11"
864 for label, header, expect
in _selftest_cases_fw_version():
865 findings = check_fw_version_lock(good, header)
866 if bool(findings) != expect:
867 verb =
"reported nothing" if expect
else f
"reported {findings}"
868 out.append(f
" {label}: {verb}")
871 stale[FW_VERSION_KEY] =
"2.13.0"
872 if not check_fw_version_lock(stale, _GOOD_FW_VER_HEADER):
873 out.append(
" co-processor reflashed past the vendored host: reported nothing")
874 missing = {k: v
for k, v
in good.items()
if k != FW_VERSION_KEY}
875 if not check_fw_version_lock(missing, _GOOD_FW_VER_HEADER):
876 out.append(f
" pins.env missing {FW_VERSION_KEY}: reported nothing")
880def _selftest_cases() -> list[tuple[str, str, str, bool]]:
881 """Return every crafted case, across both halves of the comparator.
884 The concatenation of the four case groups.
887 _selftest_cases_kconfig()
888 + _selftest_cases_dev_board()
889 + _selftest_cases_ra8_signals()
890 + _selftest_cases_ra8_map()
894def _selftest_port_failures() -> list[str]:
895 """Return one message per port-header selftest case that misbehaved."""
896 good = parse_assignments(_GOOD_PINS)
898 for label, header, _unused, expect
in _selftest_cases_port_header():
899 findings = check_port_header(good, header)
900 if bool(findings) != expect:
901 verb =
"reported nothing" if expect
else f
"reported {findings}"
902 out.append(f
" {label}: {verb}")
906def selftest() -> int:
907 """Prove the comparator reports drift and stays silent on agreement.
910 EXIT_OK when every crafted case yields the expected verdict, else
913 failures: list[str] = []
914 for label, pins_text, sdk_text, expect
in _selftest_cases():
915 findings = compare(parse_assignments(pins_text), parse_assignments(sdk_text))
916 if bool(findings) != expect:
917 verb =
"reported nothing" if expect
else f
"reported {findings}"
918 failures.append(f
" {label}: {verb}")
919 failures.extend(_selftest_port_failures())
920 failures.extend(_selftest_fw_version_failures())
923 sys.stderr.write(
"check_c6_pin_config.py --selftest: FAILED\n\n")
924 sys.stderr.write(
"\n".join(failures) +
"\n")
925 sys.stderr.write(
"\nThe comparator does not detect drift as claimed.\n")
928 cases = _selftest_cases() + _selftest_cases_port_header()
929 cases += [(a, b,
"", c)
for a, b, c
in _selftest_cases_fw_version()]
930 fires = sum(1
for c
in cases
if c[3])
932 f
"check_c6_pin_config.py --selftest: OK "
933 f
"({len(cases)} cases: {fires} must fire, {len(cases) - fires} must stay quiet)."
938def main(argv: list[str]) -> int:
939 """Compare the committed pin map against the committed Kconfig defaults.
942 EXIT_OK when they agree, EXIT_FAIL on drift or a failing selftest,
943 EXIT_CONFIG when either file is missing.
945 if "--selftest" in argv[1:]:
948 for path
in (PINS_ENV, SDKCONFIG, HOST_FW_VER):
949 if not path.is_file():
950 sys.stderr.write(f
"check_c6_pin_config.py: FATAL -- missing {path}\n")
953 pins = parse_assignments(PINS_ENV.read_text(encoding=
"utf-8"))
954 sdk = parse_assignments(SDKCONFIG.read_text(encoding=
"utf-8"))
955 findings = compare(pins, sdk)
956 findings.extend(check_fw_version_lock(pins, HOST_FW_VER.read_text(encoding=
"utf-8")))
962 key_encoded_and_version = 3
963 checked = len(PIN_PAIRS) + len(VALUE_PAIRS) + key_encoded_and_version
964 ra8 = (len(RA8_TRIPLES) * 2) + len(SW4_KEYS)
966 f
"check_c6_pin_config.py: pins.env and sdkconfig.defaults agree "
967 f
"({checked} settings); dev board "
968 f
"{pins.get(DEV_BOARD_KEY, '?')} (no preset overriding our pins); "
969 f
"RA8-side map well-formed ({ra8} entries); "
970 f
"co-processor firmware {pins.get(FW_VERSION_KEY, '?')} matches the "
971 f
"vendored host driver."
975 sys.stderr.write(f
"check_c6_pin_config.py: {len(findings)} C6 config drift(s):\n")
976 for message
in findings:
977 sys.stderr.write(f
" {message}\n")
979 "\ncoprocessor/esp32c6/pins.env is the source of truth. Update\n"
980 "sdkconfig.defaults to match it (and reflash the C6 -- a stale image on\n"
981 "the board still carries the old pins).\n"
986if __name__ ==
"__main__":
987 sys.exit(
main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.