Skip to content

pinmux: publish the pad tables through libipchw - #190

Merged
openipc-ai merged 7 commits into
masterfrom
pinmux-library
Sep 14, 2026
Merged

openipc-ai merged 7 commits into
masterfrom
pinmux-library

Conversation

@openipc-ai

Copy link
Copy Markdown
Contributor

src/reginfo.c holds 1333 hand-entered pad rows across sixteen HiSilicon
tables plus SigmaStar and Ingenic: for each pad, the physical register, the
selector value that puts each named function on it, and the GPIO the pad
carries otherwise. All of it was executable-only, so the only way to ask
"which register and which value select PWM0 on this chip?" was to run
reginfo and parse the output — which is why consumers hand-copy the rows
they need, and why those copies go stale.

This publishes the data through libipchw without moving a single table row,
so git blame still answers for every address.

Commits

  1. gpio mux writes the selector field, not the low half-word. The write
    was val & 0xfff0 | new_func against a 32-bit register, which zeroes bits
    16–31 along with the selector. The selector width is not a constant either
    — low nibble on HiSilicon/Goke, low half-word on SigmaStar — and
    dump_regs() knew that by matching vendor strings while
    fill_enabled_gpios() had separately open-coded a bare 0xf. All three now
    share one padmux_func_mask().

    In fairness: I read every pad register on four lab boards (hi3516cv100,
    hi3518ev200, hi3516cv300, hi3516av300) and none of them currently has a bit
    above 15 set, so this is a correctness fix with no victim I can point at,
    not a bug report. Keying the mask on chip_generation instead of the vendor
    string is what makes the lookups below testable off-camera.

  2. reginfo.c becomes linkable. regs_by_chip() returned
    exit(EXIT_FAILURE) for an unknown SoC — inside a long-lived daemon that is
    an outage rather than a refusal, so it returns NULL and the CLI keeps its
    old behaviour through a regs_by_chip_or_die() wrapper.
    gpio_possible_ircut() takes the softer path instead and drops the ircut
    hint from its report rather than killing the process that asked for it. One
    #ifndef STANDALONE_LIBRARY fence after regs_by_chip() takes the
    print_usage() link edge and every printf with it. get_function() had
    no callers and is gone.

  3. The lookups. ipchw_padmux_by_func, _by_prefix and _by_pad on
    include/ipchw.h, filling a flat POD so muxctrl_reg_t stays private.
    Each returns the number of matches rather than the number written, so a
    caller whose array was too small learns the size it needed.

    Two things about this data surprise people, so both are in the contract and
    both have a test:

    • A function is not unique to a pad. hi3516ev300 offers PWM2 and PWM3 on
      three pads each and hi3516cv200 offers PWM0 on two, so "the pad for PWM3"
      is not a question with an answer. Callers enumerate.
    • The spelling is not portable. PWM_OUT0 on the V1 parts, PWM0 from
      V2 on, PWM0_OUT1 on V5 — which is what the prefix query is for. But
      SVB_PWM and PMC_PWM are different controllers, so the prefix match
      is anchored at offset 0, never a substring search.

    ev200 and ev300 share one SDK build and one register map but not one pad
    table, so the family cannot be resolved at compile time; the lookups read
    chip_generation and chip_name.

  4. IPCHW_PADMUX, so a consumer pays for the SoCs it targets. The tables
    are 46 KB on arm32 and regs_by_chip() names every one of them from a
    single switch, so --gc-sections drops nothing. Same shape as
    IPCHW_VENDORS directly above it. Measured, -Os -DSTANDALONE_LIBRARY,
    reginfo.c alone:

    selection bytes
    all ten families 46207
    v4 (ev200/ev300/gk7205/dv200 — four tables) 13613
    v4a 9811
    v2 5893
    v1 4667
    v3 4330
    none 353

    The macros are PUBLIC on the ipchw target so a consumer that trims the
    wrong family gets a compile error rather than IPCHW_PADMUX_NO_TABLE on a
    camera. The SigmaStar and Ingenic tables now follow their existing
    IPCHW_VENDOR_* macros — 10.6 KB nobody building for HiSilicon was using.

  5. CI runs the unit tests. cYAML_test has existed for years and no
    workflow ever ran it. The new native job builds and runs it alongside
    reginfo_test, then configures a trimmed build and runs the tests again.
    Also writes down what mem_reg() does not promise: it is not thread-safe
    (one cached window in four file-statics, no lock), and a false return
    means "/dev/mem would not open or mmap", not "that address is bad" — a bad
    address on live silicon raises SIGBUS.

Verification

  • reginfo_test sets chip_generation/chip_name by hand and asserts rows a
    real camera was measured against, so an edit that moves PWM1 off
    0x100C0010 fails there rather than on a bench. It links the real
    libipchw, which is what proves the lookups are exported and not merely
    present. It also sweeps every compiled-in family for two invariants worth
    having over 1333 hand-entered rows: no selector wider than its own field, and
    no GPIO name that fails to parse. Both hold today across all sixteen tables.
  • Cross-built with the OpenIPC hi3516cv100 toolchain and run on a lab
    hi3516cv100: reginfo output is byte-identical to the stock binary's, 87
    rows, and gpio scan still works. Same check on a lab hi3516ev300 (93 rows,
    PWM0–PWM3 decoded on the pads they belong to).
  • -Wall -Wextra clean on arm32 across v1, v4, every family and none.
  • The ipctool executable is byte-for-byte the size it was.

Note for reviewers

CV500regs genuinely lacks the HDMI and power-sequencer pads
(0x114F00000x114F000C, 0x112F00B0, 0x112F00B8) that DV300regs has,
and that is correct — hi3516cv500 has no HDMI. I checked, because a
downstream consumer carries one merged table for all three V4A chips and I
assumed the omission was a gap. It is the merged table that is wrong.

`ipctool gpio mux` read the pad register, then wrote back
`val & 0xfff0 | new_func`. These are 32-bit registers: on every one of
them that mask silently zeroed bits 16-31, so a mux change also threw away
whatever drive strength, pull or slew the boot had configured above the
half-word.

The selector width is not a constant either. HiSilicon and Goke put the
function in the low nibble and SigmaStar in the low half-word, and
`dump_regs()` already knew that -- by matching vendor strings, in a branch
`fill_enabled_gpios()` had separately open-coded as a bare `0xf`. Collect
all three on one `padmux_func_mask()` and read-modify-write against it.

The mask is keyed on `chip_generation` rather than on the vendor string it
used to come from. Same answer on every SoC that has a table -- Goke parts
report HISI_V4 -- and it means a caller that sets the generation directly
gets the right width without a live chip underneath it.
`src/reginfo.c` was executable-only. Everything in it that is worth
reusing -- 1333 hand-entered pad rows across sixteen HiSilicon tables plus
SigmaStar and Ingenic, and the per-SoC dispatch that picks the right one --
was therefore reachable only by running `ipctool`, and a consumer wanting
to ask "which register and which value select PWM0 on this chip?" had no
way in but to shell out and parse the output.

Three things stood between the file and `libipchw`, and this fixes all
three without moving a single table row, so `git blame` still answers for
every address in here:

- `regs_by_chip()` ended in `exit(EXIT_FAILURE)`. Inside a long-lived
  daemon an unrecognised SoC would then be an outage rather than a
  refusal, so it returns NULL and the CLI keeps its old behaviour through
  a `regs_by_chip_or_die()` wrapper. `fill_enabled_gpios()` takes the
  softer path instead: `gpio_possible_ircut()` now drops the ircut hint
  from its report rather than killing the process that asked for it.

- The command half calls `print_usage()`, which lives in main.c. One
  `#ifndef STANDALONE_LIBRARY` fence after `regs_by_chip()` -- the last
  function the tables need -- takes that link edge and every printf with
  it. `num2gpio_groupnum()` and `find_pinfunc()` move above the fence;
  they are pure and the lookups to come want them.

- `src/reginfo.c` moves from IPCTOOL_SRC to COMMON_LIB_SRC_BASE.

`get_function()` had no callers and is deleted.

The library pays nothing for this yet: with no exported entry point the
tables are still static and unreferenced, and reginfo.c compiles to an
empty object in libipchw. The executable is byte-for-byte the same size
and answers "Platform is not supported" to `reginfo` and `gpio mux` on an
unsupported host exactly as before.
The tables know that PWM1 on an hi3516ev200 is selector 1 in the register
at 0x100C0010, and that the pad is GPIO0_4 the rest of the time. Until now
the only way to get that out of ipctool was to run `reginfo` and parse the
output, so consumers hand-copied the rows they needed and the copies went
stale in the usual way.

Three lookups, on include/ipchw.h next to the identity strings:
by function name, by name prefix, and by pad. Each fills a flat POD, so
muxctrl_reg_t stays private, and each returns the number of MATCHES rather
than the number written -- a caller whose array was too small learns the
size it needed instead of quietly losing rows.

Two things about this data surprise people, so both are in the contract and
both have a test:

- A function is not unique to a pad. hi3516ev300 offers PWM2 and PWM3 on
  three pads each and hi3516cv200 offers PWM0 on two, so "the pad for PWM3"
  is not a question with an answer. Callers enumerate.
- The spelling is not portable. It is PWM_OUT0 on the V1 parts, PWM0 from
  V2 onward and PWM0_OUT1 on V5, which is what the prefix query is for --
  but "PWM" also prefixes SVB_PWM and PMC_PWM, and those are different
  controllers on different pads. Matching anywhere in the string instead of
  at the front would drive the sensor bias supply.

ev200 and ev300 share one SDK build and one register map but not one pad
table, so the family cannot be resolved at compile time; the lookups read
chip_generation and chip_name, which is also what lets the whole thing be
tested on a host. src/reginfo_test.c sets those two globals by hand and
asserts the rows a camera was actually measured against, so an edit that
moves PWM1 off 0x100C0010 fails here rather than on a bench. It links the
real libipchw, which is what proves the symbols are exported and not merely
present. It also sweeps every compiled-in family for two data invariants
worth having over 1333 hand-entered rows: no selector wider than its own
field, and no GPIO name that fails to parse.

Nothing here exits, prints, or touches /dev/mem.
The pad tables are 46 KB on arm32 and regs_by_chip() names every one of
them from a single switch, so --gc-sections drops none of it. That is fine
in a tool you copy to /tmp, and not fine at all in a daemon on an 8 MB NOR
board -- an OpenIPC nightly was recently lost because one image went 8 KB
over its cap.

IPCHW_PADMUX picks the families, in the same shape as IPCHW_VENDORS
directly above it: 'all' (the default, and what the ipctool executable
always gets), 'none', or a subset of
v1 v2 v2a v3 v3a v4 v4a v5 3536c 3536d. Each token brackets its tables and
its regs[] arrays together, and its arm of regs_by_chip() with them, so a
trimmed family reports IPCHW_PADMUX_NO_TABLE rather than silently reading
like a chip with no pads. The SigmaStar and Ingenic tables now follow their
existing IPCHW_VENDOR_* macros, which is 10.6 KB nobody building for
HiSilicon was using.

Measured, arm-linux-gnueabi-gcc -Os -DSTANDALONE_LIBRARY, reginfo.c alone:

    all ten families   46207
    v4                 13613   ev200/ev300/gk7205/dv200, four tables
    v4a                 9811
    v2                  5893
    v1                  4667
    v3                  4330
    none                 353

v4 is the worst case in the matrix and it is structural: one SDK build runs
on four chips with four different pad tables, so all four have to be there.

IPCHW_PADMUX_* is PUBLIC on the ipchw target, so a consumer that trims the
wrong family can #error on the missing macro at compile time instead of
meeting NO_TABLE on a camera. reginfo_test.c compiles its per-family cases
behind the same macros and asserts that a trimmed-out family is
distinguishable from an SoC that simply has no such pad.

The ipctool executable is byte-for-byte the size it was.
cYAML_test has existed for years and no workflow ever ran it. Add a native
job that builds and runs it alongside reginfo_test, then configures a
trimmed build (-DIPCHW_VENDORS=none -DIPCHW_PADMUX=v1) and runs the tests
again -- which is the only automatic check that the family gating actually
compiles and that a trimmed-out family stays distinguishable from an SoC
with no such pad.

mem_reg() keeps its mapping, offset, size and descriptor in four
function-local statics with no lock, and any address outside the cached
window unmaps it and maps another. Two threads on different windows will
unmap the mapping the other is about to dereference. That has always been
true and was written down nowhere; it matters more now that a daemon can
link this. Also note what a false return does and does not mean: it is
"/dev/mem would not open or mmap", not "that address is bad" -- a bad
address on live silicon raises SIGBUS.
`num2gpio_groupnum()` and `find_pinfunc()` were moved above the
STANDALONE_LIBRARY fence on the expectation that the pad-mux lookups would
want them. They did not: the lookups go the other way, from a table row to
a pad number, and they enumerate rather than stopping at the first match.
Both helpers exist solely for `gpio_mux_by()`, which parses argv, so they
belong on its side of the fence.

Left where they were they are two unused statics in every library build.
That is silent today -- the release flags carry -Wextra but not -Wall, and
-Wunused-function lives in -Wall -- which is exactly the kind of thing that
turns into noise the day someone tightens the flags. Checked with
-Wall -Wextra across v1, v4, every family and none: clean.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Publish pad-mux tables through libipchw

✨ Enhancement 🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Publishes runtime pad-mux queries through libipchw without relocating existing SoC tables.
• Preserves non-selector register bits using generation-specific masks across mux operations.
• Supports family-trimmed builds and validates lookups and table invariants in CI.
Diagram

graph TD
  Config["Build selection"] --> Library["libipchw"] --> API["Padmux API"] --> Walker["Lookup walker"] --> Dispatch["SoC dispatch"] --> Tables["Compiled tables"]
  Consumer["Library consumer"] --> API
  CLI["ipctool CLI"] --> Walker
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Generate a dedicated pad-mux database
  • ➕ Could normalize naming and validate source data during generation
  • ➕ Would separate table data completely from CLI implementation
  • ➖ Relocating or regenerating 1,333 rows would obscure existing blame history
  • ➖ Introduces generator formats and maintenance without an authoritative machine-readable source
2. Expose internal table structures directly
  • ➕ Requires less lookup code
  • ➕ Allows consumers to implement arbitrary traversal
  • ➖ Leaks muxctrl_reg_t into the public ABI
  • ➖ Makes every consumer duplicate filtering, GPIO parsing, sizing, and error handling
  • ➖ Prevents internal table representation changes

Recommendation: The PR's stable POD API over the existing in-place tables is the best incremental design. It preserves provenance, keeps internal structures private, centralizes tricky enumeration semantics, and allows consumers to trim unused families; generation would be preferable only after establishing an authoritative data source.

Files changed (7) +782 / -62

Enhancement (1) +67 / -0
ipchw.hDefine the public pad-mux query API +67/-0

Define the public pad-mux query API

• Adds the public pad-mux result structure, explicit error codes, and exact-function, prefix, and GPIO-pad lookup functions. Documents result sizing, pointer lifetime, concurrency, and vendor-specific limitations.

include/ipchw.h

Tests (1) +297 / -0
reginfo_test.cValidate pad-mux APIs and table integrity +297/-0

Validate pad-mux APIs and table integrity

• Adds host-native tests for known hardware rows, duplicate function mappings, anchored prefixes, pad enumeration, count-before-sizing behavior, and nonfatal errors. Sweeps compiled tables for selector-width and GPIO-parsing invariants.

src/reginfo_test.c

Documentation (2) +41 / -3
CLAUDE.mdDocument pad-mux configuration, tests, and concurrency contracts +30/-3

Document pad-mux configuration, tests, and concurrency contracts

• Documents IPCHW_PADMUX family selection, the new lookup tests, and cross-family naming behavior. It also records SoC detection and mem_reg thread-safety constraints.

CLAUDE.md

tools.hDocument mem_reg safety and failure semantics +11/-0

Document mem_reg safety and failure semantics

• Clarifies that mem_reg is neither thread-safe nor reentrant because it shares a cached mapping. Documents that mapping failures return false while invalid live addresses may raise SIGBUS.

src/tools.h

Other (3) +377 / -59
pr-build-check.ymlRun native and trimmed pad-mux tests in CI +22/-0

Run native and trimmed pad-mux tests in CI

• Adds a native unit-test job for cYAML_test and reginfo_test. It repeats reginfo testing with only V1 pad tables and no optional vendor HALs to verify trimmed builds.

.github/workflows/pr-build-check.yml

CMakeLists.txtCompile selectable pad-mux tables into libipchw +56/-4

Compile selectable pad-mux tables into libipchw

• Adds validated IPCHW_PADMUX family selection and propagates selected macros to consumers. Moves reginfo into libipchw, publishes its include path, keeps every table in ipctool, and adds reginfo_test.

CMakeLists.txt

reginfo.cExport pad-mux lookups and preserve selector-adjacent bits +299/-55

Export pad-mux lookups and preserve selector-adjacent bits

• Makes SoC tables conditionally compilable and implements shared exact, prefix, and pad lookup traversal without exposing internal table structures. Centralizes selector masks, fixes read-modify-write behavior, and separates library-safe refusal handling from fatal CLI behavior.

src/reginfo.c

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 14, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Pin scans reject supported cameras ✓ Resolved 🐞 Bug ≡ Correctness
Description
fill_enabled_gpios() is declared to return bool but has only an explicit false return when
regs_by_chip() provides no pad table, while its successful table-scan path reaches the end without
returning true. On any supported SoC with a table, gpio_scan_cmd() and gpio_possible_ircut()
consume the indeterminate result as failure, potentially rejecting the platform or suppressing the
infrared-cut hint.
Code

src/reginfo.c[3061]

+static bool fill_enabled_gpios(size_t *enabled, size_t GPIO_Groups) {
Evidence
The helper returns false only when regs_by_chip() fails to provide a pad table, then completes
its normal table scan without a return statement. Both callers immediately branch on this boolean as
a success indicator, so undefined return data controls whether the platform is reported as
unsupported and whether the IR-cut report is shown.

src/reginfo.c[3061-3088]
src/reginfo.c[3137-3141]
src/reginfo.c[3212-3217]
src/reginfo.c[3139-3141]
src/reginfo.c[3214-3217]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`fill_enabled_gpios()` returns `false` when no pad table is available, but does not return a value after successfully scanning a selected table. Its callers use the boolean result as a success indicator, so falling off the end causes them to branch on an indeterminate value.
## Fix Focus Areas
- src/reginfo.c[3061-3088]
- src/reginfo.c[3139-3141]
- src/reginfo.c[3214-3217]
## Recommended Fix
Add `return true;` after the table-walking loop in `fill_enabled_gpios()` completes, while retaining the existing early `return false;` for the missing-table path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can ask Qodo to dismiss a finding you disagree with, with your reason on record

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/reginfo.c
Making fill_enabled_gpios() return a bool, two commits ago, I added the
`return false` for "no pad table" and never added the `return true` for the
ordinary path. Both callers branch on the result, so on every supported SoC
they were deciding whether the platform is usable from whatever happened to
be in the return register. It passed CI and it passed a run on a camera,
because the garbage was truthy.

The default flags carry -Wextra but not -Wall, and -Wreturn-type lives in
-Wall, so nothing said a word. Turning it on as an error in every
configuration found four more, all of the same shape:

- `backup_with_yaml()` in main.c computes do_backup()'s verdict into `ret`
  and then drops it, so `ipctool backup <file>` has been exiting on
  whatever was in the register. That one is user-visible.
- `dummy_change_addr()` in sensors.c is an empty `static int` stub wired
  into the i2c_change_addr pointer; callers that check it were reading
  rubbish.
- `cv100_sensor_clksel()` and the RAW_DATA name lookup in ptrace.c are
  switches with no default. The first is genuinely exhaustive over a 3-bit
  field and now says so; the second decodes a value that came off a traced
  ioctl and is not bound to the enum at all, so "unknown" is a real answer
  it needed.

Verified on a lab hi3516cv100: `reginfo` and the full YAML report are both
byte-identical to the stock binary's, and the executable is the same size.
@openipc-ai

Copy link
Copy Markdown
Contributor Author

Good catch, and it was worse than it looked. Fixed in 3ee8992.

fill_enabled_gpios() got its return false for "no pad table" and never got the return true for the ordinary path, so on every supported SoC both callers were deciding whether the platform is usable from whatever happened to be in the return register. It passed CI, and it passed a run on a lab hi3516cv100, because the garbage was truthy.

What let it through is that the default flags carry -Wextra but not -Wall, and -Wreturn-type lives in -Wall. So the fix is the flag as well as the line — -Werror=return-type is now on in every configuration, and it immediately found four more of the same shape that were already in the tree:

  • backup_with_yaml() in main.c computes do_backup()'s verdict into ret and then drops it, so ipctool backup <file> has been exiting on whatever was in the register. That one is user-visible.
  • dummy_change_addr() in sensors.c is an empty static int stub wired into the i2c_change_addr pointer; callers that check it were reading rubbish.
  • cv100_sensor_clksel() and the RAW_DATA name lookup in ptrace.c are switches with no default. The first is genuinely exhaustive over a 3-bit field and now says so; the second decodes a value that came off a traced ioctl and is not bound to the enum at all, so "unknown" is a real answer it needed.

Verification, since the original also survived a smoke test:

  • lab hi3516cv100 — reginfo and the full YAML report are both byte-identical to the stock binary's output, and the executable is the same size.
  • lab hi3516ev300 — this board actually exercises the path the bug was on, and it reports possible-IR-cut-GPIO: 10,11. That is fill_enabled_gpios() returning true on the success path, on real silicon.

@openipc-ai
openipc-ai merged commit 1beb201 into master Sep 14, 2026
5 checks passed
@openipc-ai
openipc-ai deleted the pinmux-library branch September 14, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant