Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .clang-format-hook-exclude
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
src/hal/sstar_padmux\.h
src/hal/ingenic_padmux\.h
src/hal/novatek_padmux\.h
src/padmux_names\.h
src/padmux_names\.c
28 changes: 19 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ CMake knobs worth knowing:
question the linker cannot answer — nothing here is dead code as far as it
knows.
- `-DIPCHW_PADMUX=all|none|"v1;v4;sstar"` selects which SoC families' pad-mux
tables go into `libipchw`. `sstar` and `ingenic` are families here too: the
vendor knob answers "can this build detect the SoC", this one answers "does
it carry the SoC's pad table", and they are deliberately separate. All of it
tables go into `libipchw`. `sstar`, `ingenic` and `novatek` are families
here too: the vendor knob answers "can this build detect the SoC", this one
answers "does it carry the SoC's pad table", and they are deliberately
separate. All of it
is ~75 KB on arm32 and nothing is dropped by `--gc-sections`, because
`regs_by_chip()` and `padmux_ops()` name every family from a single switch:
a consumer that wants one family has to say so. `v4` is the expensive
Expand Down Expand Up @@ -252,8 +253,8 @@ cJSON or prints diagnostics in those shared files must sit inside
window in four file-statics, no lock -- so serialise every caller yourself.
- Pad multiplexing spans `src/padmux.c` (the public entry points and the
backend dispatch), `src/reginfo.c` (1333 hand-entered HiSilicon/Goke rows
plus their backend), `src/hal/sstar_padmux.*` and
`src/hal/ingenic_padmux.*`. The three vendors select a pad's function three
plus their backend), `src/hal/sstar_padmux.*`, `src/hal/ingenic_padmux.*`
and `src/hal/novatek_padmux.*`. The four vendors select a pad's function four
different ways and only HiSilicon's is one register per pad, which is why
there is a `padmux_ops_t` seam rather than one table format. `docs/padmux.md`
has the mechanisms, the generators, and the traps -- read it before touching
Expand All @@ -266,12 +267,21 @@ cJSON or prints diagnostics in those shared files must sit inside
(hi3516ev300 has PWM2 and PWM3 on three pads each), and the spelling is not
portable (`PWM_OUT0` on V1, `PWM0` from V2, `PWM0_OUT1` on V5, `PWM0_MODE_4`
on SigmaStar; `SVB_PWM` and `PMC_PWM` are *different* controllers, so match
the prefix anchored, never anywhere in the string). The SigmaStar and
Ingenic tables are generated from vendor sources by `tools/gen_*_padmux.py`
and are in `.clang-format-hook-exclude`. `ipchw_padmux_by_func/_by_prefix/
_by_pad` are safe to call concurrently once the SoC has been detected;
the prefix anchored, never anywhere in the string). The SigmaStar,
Ingenic and Novatek tables are generated from vendor sources by
`tools/gen_*_padmux.py` and are in `.clang-format-hook-exclude`. Novatek's
generator is the odd one: the NA51089 SDK has no pin table, only the
driver's `pinmux_config_*()` code, so it compiles that code on the host with
every register write logged and runs it -- it needs a C compiler and the
SDK, and its `--selftest` needs only the compiler.
`ipchw_padmux_by_func/_by_prefix/_by_pad` are safe to call concurrently once the SoC has been detected;
detection itself is not, so call `getchipname()` once at startup.
`ipchw_padmux_get/_set` touch `/dev/mem` and must stay on one thread.
- `gpio get/set/scan` reach a GPIO controller per vendor: HiSilicon's bank
words in `src/reginfo.c` (`get_chip_gpio_adress()`), SigmaStar Infinity6C's
per-pad registers in `src/hal/sstar_gpio.c`, and the NA51089's four
DATA/DIR/SET/CLR banks in `src/hal/novatek_gpio.c`. Ingenic has none, so
only `gpio mux` and `reginfo --pads` work there.
- `src/fake_symbols.c` holds empty definitions of HiSilicon SDK audio symbols,
added when the Hi3518EV100 SDK was linked in; nothing in the current tree
references them. `src/stack.c` is a stack-protector shim and is not in the
Expand Down
15 changes: 10 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ endif()
# Same shape as IPCHW_VENDORS above, and for the same reason -- set(... CACHE)
# here means a parent project has to FORCE it.
set(IPCHW_PADMUX_FAMILIES v1 v2 v2a v3 v3a v4 v4a v5 3536c 3536d sstar
ingenic)
ingenic novatek)
set(IPCHW_PADMUX "all" CACHE STRING
"Pad-mux tables compiled into libipchw: 'all', 'none', or a subset of ${IPCHW_PADMUX_FAMILIES}")

Expand Down Expand Up @@ -100,19 +100,20 @@ foreach(_f IN LISTS _padmux_selected)
endforeach()
message(STATUS "libipchw pad-mux tables: ${_padmux_selected}")

# The two vendors whose pad-mux is its own backend rather than a table in
# The vendors whose pad-mux is its own backend rather than a table in
# reginfo.c. Selecting the family is what compiles the backend in; selecting
# the vendor HAL is what makes chip_generation reach it. They are separate
# questions, which is why they are separate knobs -- a consumer can carry the
# SigmaStar tables in a build that detects nothing else, which is exactly what
# reginfo_test does.
set(IPCHW_PADMUX_BACKENDS "")
foreach(_f sstar ingenic)
foreach(_f sstar ingenic novatek)
if(_f IN_LIST _padmux_selected)
list(APPEND IPCHW_PADMUX_BACKENDS src/hal/${_f}_padmux.c)
endif()
endforeach()
set(IPCHW_PADMUX_BACKENDS_ALL src/hal/sstar_padmux.c src/hal/ingenic_padmux.c)
set(IPCHW_PADMUX_BACKENDS_ALL src/hal/sstar_padmux.c src/hal/ingenic_padmux.c
src/hal/novatek_padmux.c)

# Which HiSilicon generations libipchw can identify. IPCHW_VENDORS does not
# reach the CODE: chipid.c dispatches to the HiSilicon HAL directly rather
Expand Down Expand Up @@ -279,7 +280,8 @@ set(_common_lib_src_tail
# Full set, used by the ipctool executable
set(COMMON_LIB_SRC_ALL ${_common_lib_src_head} ${IPCHW_HISI_SRC_ALL}
${_common_lib_src_tail} ${IPCHW_PADMUX_BACKENDS_ALL}
src/hal/sstar_gpio.c src/hal/sstar_gpio.h)
src/hal/sstar_gpio.c src/hal/sstar_gpio.h
src/hal/novatek_gpio.c src/hal/novatek_gpio.h)
foreach(_v IN LISTS IPCHW_OPTIONAL_VENDORS)
list(APPEND COMMON_LIB_SRC_ALL src/hal/${_v}.c src/hal/${_v}.h)
string(TOUPPER ${_v} _V)
Expand All @@ -299,6 +301,9 @@ foreach(_v IN LISTS _ipchw_selected)
if(_v STREQUAL "sstar")
list(APPEND COMMON_LIB_SRC src/hal/sstar_gpio.c src/hal/sstar_gpio.h)
endif()
if(_v STREQUAL "novatek")
list(APPEND COMMON_LIB_SRC src/hal/novatek_gpio.c src/hal/novatek_gpio.h)
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
endif()
string(TOUPPER ${_v} _V)
list(APPEND IPCHW_VENDOR_DEFS IPCHW_VENDOR_${_V})
endforeach()
Expand Down
16 changes: 16 additions & 0 deletions docs/gpio.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ GPIO pad, decoded from the pad's own one-byte register into what it reads
(`in`), what the SoC is driving (`out`) and whether it drives at all (`oe_n`,
1 meaning input/Hi-Z) — rather than the per-group data and direction words.

On Novatek (NT98562/NT98566) the pad is the kernel's GPIO number as well, and
its name is accepted too: `gpio get P_GPIO22`, `gpio get 54` and
`/sys/class/gpio/gpio54` are one wire (`P_GPIO(n)` is `0x20 + n`, and so on in
steps of `0x20` through C, P, S, L, D, H, A and DSI). The `5_6` form is refused
there, because it would name a different pad. `gpio scan` prints one `Pad:`
line per GPIO pad with its name, data word and direction.

## `gpio scan`

It prints a baseline table of every GPIO pad, then polls in a loop and reports
Expand Down Expand Up @@ -123,6 +130,9 @@ streamer maps the GPIO block, a board whose whole pad list holds only one or
two driving pads gets exactly those, and without a streamer every pad
currently driving low is reported.

Novatek does the same over its GPIO numbers: the controller is one page, so
"the streamer has it mapped" is one answer for every pad.

IR-cut is nearly always a *pair* of pads, driven in opposite directions to flip
the filter between its two positions. To confirm a pair, run `gpio scan` and
switch the camera between day and night mode in the web UI. Both pads change in
Expand Down Expand Up @@ -184,6 +194,12 @@ one register per pad and this particular trap does not apply, but the general
rule does: a pad on a working board is carrying something, and taking it away
stops whatever that was. `docs/padmux.md` has the mechanisms per vendor.

Novatek has the SigmaStar trap and one of its own. Muxing I2C3 onto a new pad
moves the peripheral's location field, so the pads it was on are left handed
over to nothing (`gpio mux` reports them as `?`). And on a board with a
parallel sensor, `gpio mux H_GPIO4 GPIO` cannot take one data line back without
switching the sensor mode off, so it switches it off.

If you do re-mux something by accident, `gpio mux <pad> <original function>`
puts it back; on SigmaStar restoring a pad to "idle but not GPIO-asserted" also
needs one `devmem` to clear the pad's GPIO-enable bit, which `gpio mux` has no
Expand Down
101 changes: 98 additions & 3 deletions docs/padmux.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ puts the pad back.
> pad to GPIO. A pad that a MISC-style table drop hides from `reginfo --pads`
> still answers here, which is the one place to see it.

## Three vendors, three mechanisms
## Four vendors, four mechanisms

This is the thing to understand before touching any of it. The API looks like
one question, and underneath it is three different pieces of silicon.
one question, and underneath it is four different pieces of silicon.

### HiSilicon and Goke -- a selector per pad

Expand Down Expand Up @@ -285,6 +285,53 @@ Direction is part of the same nibble as the mux, so "make this GPIO" has to
pick one: a pad already held as GPIO keeps what it had, one coming off a
peripheral becomes an input.

### Novatek -- a field per peripheral, and a gate per pad

NA51089 (NT98562, NT98566) is SigmaStar's arrangement with a second lock on
it. In the TOP block at `0xF0010000` each peripheral has a location field --
`REG5[7:6]` is I2C3, and 1, 2 and 3 put it on `P_GPIO21/22`, `C_GPIO11/12`
or `DSI_GPIO8/9` -- and each pad has a **gate bit** in one of eight bitmaps
at `TOP + 0xA0..0xE8`, where 1 keeps the pad a GPIO whatever any field says.
The vendor's `pinmux_config_*()` sets both, every time: the field, then
`GPIO_ID_EMUM_FUNC` into the gate of each pad it uses.

So a pad's alternatives are **claims**: a name and the field values under
which the pad carries it. `get()` takes the first claim that holds, with the
table sorted most specific first; `set()` writes a claim's fields and clears
the gate. Consequences:

- No row carries `IPCHW_PADMUX_F_RMW` except a GPIO row. A function is a field
in one register and a bit in another, and a consumer that writes only the
field leaves the pad a GPIO -- harmless, which is why the row's
`address`/`func_mask`/`func` still describe the field.
- **Moving a peripheral orphans its old pads.** Put I2C3 on `P_GPIO21` while
`C_GPIO11` had it and `C_GPIO11` keeps its gate cleared with nothing
claiming it: `get()` says it cannot name it, which is the truth. The vendor
code does the same and never gives those gates back.
- `set()` changes as little as it can. Several claims can spell one name --
`ETH_RMII` on `DSI_GPIO9` is one claim alone and another when `ETH_MDIO` is
also set -- and the one needing the fewest field changes from what is there
now wins. A competitor that still wins afterwards is dropped by zeroing a
field the target does not need: `ETH_MDIO` on `DSI_GPIO9`, the internal
PHY's management pin, clears `ETH` and so ends RMII.
- **The parallel and CCIR sensor modes ignore the gate.** They put pixel data
on the HSI pads while the vendor writes those gates to GPIO, so those claims
hold through it (`NVT_UNGATED` in the table). `gpio mux H_GPIO4 GPIO` on such
a board has to zero `SENSOR` to mean anything, and does -- the whole parallel
bus goes with it. The MIPI lanes on the same pads are the opposite: the gate
cleared and no field at all.
- Pads are the kernel's GPIO numbers and names: `C_GPIO(n)` is `n`,
`P_GPIO(n)` is `0x20 + n`, and so on through S, L, D, H, A and DSI. `gpio`
takes the number or the name (`P_GPIO22`, and the vendor's `MC17` and
`HSI_GPIO9` as well); the HiSilicon `5_2` form is refused, because it would
land on a different pad.

Function names are the vendor's, from the `@PAD[NAME]` annotations in
`plat-na51089/top.h` and then `top.csv`: `I2C3_2_SCL`, `UART2_2_RTS`, `SB_CK`
for SIF. Where neither names a pad the name is the option's own -- `SENSOR_12BITS`,
`MIPI_LVDS_CLK0`, `ETH_RMII` -- which, like SigmaStar's `I2C1_MODE_3`, names a
bus and not a wire.

## Pad numbers and names

`gpio_pad` is the number the kernel uses, and it is the number to speak to
Expand All @@ -295,6 +342,7 @@ every consumer:
| HiSilicon/Goke | `bank * 8 + pin`, parsed out of the `GPIO<b>_<p>` entry | `"GPIO5_2"` |
| SigmaStar | the vendor pad id, which is the gpiochip index (base 0) | `"PAD_SR_IO03"` |
| Ingenic | `port * 32 + pin` | `"PB25"` |
| Novatek | the Linux GPIO number, `group * 32 + pin` | `"P_GPIO22"` |

Function names are the vendor's own and are **not portable**, not even within
HiSilicon: `PWM_OUT0` on V1, `PWM0` from V2, `PWM0_OUT1` on V5. `"PWM"` also
Expand Down Expand Up @@ -332,6 +380,41 @@ tools/gen_ingenic_padmux.py \
Each generated header carries the exact command that made it, so the recipe
above is only the shape; the header is the record.

Novatek is generated differently, because its source is code and not a table:

```sh
# Novatek: the SDK root, which has BSP/linux-kernel and build/nvt-tools
tools/gen_novatek_padmux.py --sdk /path/to/na51089_linux_sdk \
> src/hal/novatek_padmux.h
```

There is no datasheet and no pin list in the NA51089 SDK, and
`na51089_pinmux_host.c` is 2200 lines of nested `if`/`else if`/`switch` with
the conflict checks interleaved. So the generator **runs** it: it cuts the
`pinmux_config_*()` functions out, wraps every `top_regN.bit.FIELD = value`
in a macro that logs the write, compiles that on the host against the SDK's
own `top_reg.h`/`top.h`, and calls every function with every combination of
up to four of its options (96,550 calls, about half a minute). A combination the
vendor refuses is retried after one option of another group, which is the
only way MIPI data lanes 1-3 appear at all: `pinmux_config_mipi_lvds()`
refuses them until the sensor group is in CSI mode.

The source is read for one thing: which braces enclose each write. A gate
write's conditions are the field writes the same call made in the blocks
around it -- `SEN_MCLK=1` for `S_GPIO0`, not the sensor mode a sibling block
of the same call selected. Every state the vendor code produced is then
replayed against the table, and three answers are acceptable: the pad reads
as what the code meant; it reads as nothing because a later write of the same
call moved its field away; or two claims hold, and the table carries a more
specific claim for the one the code meant. Anything else is fatal.

`top.h` and `top.csv` disagree with the code in three places, and
`ANNOTATION_ERRATA` in the generator says which side wins and why: the csv's
`P_GPIO29` for SIF CH2_2 (no such pad, the code uses `P_GPIO19`), `top.h`'s
`P_GPIO14` for PICNT2_1 (the code and the csv use `L_GPIO1`), and
`PIN_MISC_CFG_SP2CLK_3RD`, which does nothing at all, because
`pinmux_config_misc()` tests `PIN_SENSOR_CFG_SP2CLK_3RD` in its place.

Both generators take `--verify <header>`, which re-derives and diffs instead
of writing. That needs the SDK, so CI cannot run it;
`tools/gen_sstar_padmux.py --selftest` runs the vendor-source parsing against
Expand Down Expand Up @@ -389,11 +472,23 @@ in.
- **Other Ingenic parts.** T21, T23, T31 and T40 have tables. T10, T20, T30 and
T41 do not, and would each need a source of one of the three kinds above --
which, for the parts checked so far, does not exist in the vendor releases.
- **Other Novatek parts, and NA51089's display.** NA51055, NA51084, NA51090
and NA51103 have no table: their chip IDs are recognised and get none.
LCD, TV and HDMI on NA51089 are muxed by `pinmux_select_primary_lcd()`
from state the config functions only stash, and are not modelled; a pad
given to the display reads as cleared with nothing claiming it.
- **Novatek states two options create at once.** In 19,185 of the replayed
calls some pad is claimed twice -- a 12-bit sensor's pixel clock and MCLK2
both on `S_GPIO1`, say. The vendor code checks for neither and no board
does it. The table answers by the vendor's last write, which is the best
evidence there is and not a measurement.
- **Hardware.** Every claim here is verified against vendor source and on a
host. On top of that, HiSilicon, SigmaStar infinity6/6b0/6c, and Ingenic
T21, T23, T31 and SigmaStar infinity6e have been run on real cameras and
checked against an independent decode of their live registers. Ingenic T40
has not: nobody has had one.
and Novatek NA51089 have not: nobody here has had one. On a NA51089,
`/proc/nvt_info/nvt_pinmux/pinmux_summary` is the kernel's own decode of the
same registers and is the thing to compare `reginfo --pads` against.

## Adding a family

Expand Down
5 changes: 5 additions & 0 deletions src/hal/novatek.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ static bool nvt_get_chip_id() {
sizeof(buf))) {

reg = (uint16_t)strtol(buf, &endptr, 16);
/* The TOP block's chip-ID word, which is what tells the pad-mux
* table and the GPIO controller which SoC this is. Set for every ID
* the kernel hands over, named or not: a part with no table gets no
* table, which padmux_ops() says rather than guessing one. */
chip_generation = reg;
switch (reg) {
case 0x5021:
strcpy(chip_name, "NT98562");
Expand Down
78 changes: 78 additions & 0 deletions src/hal/novatek_gpio.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/* The NA51089 GPIO controller, as the vendor's gpio-nvt-na51089.c drives it.
*
* Four banks of eight words at 0xF0070000: DATA, DIR, SET and CLR, one word
* per pad group, so a pad's bit is its Linux GPIO number modulo 32 in word
* number / 32. SET and CLR are write-one aliases, which is how the kernel
* changes a level without a read-modify-write of the other pads; only DIR is
* read-modified-written, under the kernel's spinlock there and under the
* single-threaded contract of mem_reg() here.
*
* The per-group counts are nvt-gpio.h's *_GPIO_NUM: what the package bonds
* out, and so what the gpiochip accepts. */

#include "hal/novatek_gpio.h"

#include "chipid.h"
#include "hal/novatek.h"
#include "tools.h"

static const uint8_t group_pads[8] = {
23, /* C_GPIO, the "MC" pads */
26, /* P_GPIO */
9, /* S_GPIO */
10, /* L_GPIO */
8, /* D_GPIO */
12, /* H_GPIO, the HSI pads */
3, /* A_GPIO */
11, /* DSI_GPIO */
};

bool novatek_gpio_supported(void) { return chip_generation == CHIP_NA51089; }

bool novatek_gpio_valid(int pad) {
if (!novatek_gpio_supported() || pad < 0 || pad >= NVT_GPIO_NPADS)
return false;
return (pad & 31) < group_pads[pad >> 5];
}

uint32_t novatek_gpio_reg(int pad, unsigned bank) {
if (!novatek_gpio_valid(pad) || bank > NVT_GPIO_CLR || bank % 0x20)
return 0;
return NVT_GPIO_BASE + bank + (uint32_t)(pad >> 5) * 4;
}

bool novatek_gpio_window(uint32_t *base, uint32_t *len) {
if (!novatek_gpio_supported())
return false;
*base = NVT_GPIO_BASE;
*len = NVT_GPIO_CLR + 0x20;
return true;
}

bool novatek_gpio_read(int pad, bool *output, bool *level) {
uint32_t dir, data;
if (!novatek_gpio_valid(pad) ||
!mem_reg(novatek_gpio_reg(pad, NVT_GPIO_DIR), &dir, OP_READ) ||
!mem_reg(novatek_gpio_reg(pad, NVT_GPIO_DATA), &data, OP_READ))
return false;

uint32_t bit = 1u << (pad & 31);
*output = (dir & bit) != 0;
*level = (data & bit) != 0;
return true;
}

bool novatek_gpio_write(int pad, bool level) {
uint32_t dir;
uint32_t bit = 1u << (pad & 31);
if (!novatek_gpio_valid(pad) ||
!mem_reg(novatek_gpio_reg(pad, NVT_GPIO_DIR), &dir, OP_READ))
return false;
if (!(dir & bit)) {
dir |= bit;
if (!mem_reg(novatek_gpio_reg(pad, NVT_GPIO_DIR), &dir, OP_WRITE))
return false;
}
return mem_reg(novatek_gpio_reg(pad, level ? NVT_GPIO_SET : NVT_GPIO_CLR),
&bit, OP_WRITE);
}
Loading
Loading