This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
ipctool is a single static C99 binary that runs on an IP camera or DVR and
reports its hardware as YAML: SoC, board, sensor, flash layout, RAM, firmware,
clocks. It probes hardware directly (/dev/mem, I2C/SPI, /proc, MTD), so
almost nothing useful executes on an x86 host. The same tree also builds
libipchw (a small static library exposing chip/sensor identity, see
include/ipchw.h) and ipcinfo (example/ipcinfo.c), a minimal consumer of it.
Shipped binaries are static musl builds for arm32 (the canonical target), mips32 and arm64, and are UPX-packed before release: the raw static-musl binary crashes at startup on legacy kernels (Linux <= 3.18, i.e. XiongMai and old HiSilicon SDK firmware), and the UPX stub sidesteps that.
Native build (compiles everything, runs the unit test; hardware paths return nothing on a PC):
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
./build/cYAML_test # exit 0 == all cases passedCross build, exactly as CI does it (PR check and release both use these toolchains):
# arm32 (HiSilicon/Goke V1..V5, XM, SigmaStar, ...): OpenIPC hi3516cv100 musleabi toolchain
# mips32 (Ingenic): OpenIPC ingenic-t31 musl toolchain
# arm64 (Hi3519DV500 etc.): Bootlin aarch64--musl--stable toolchain
export PATH=/opt/<toolchain_dir>/bin:$PATH
cmake -S . -B build-arm -DCMAKE_C_COMPILER=arm-openipc-linux-musleabi-gcc -DCMAKE_BUILD_TYPE=Release
cmake --build build-arm
upx build-arm/ipctool # match the release artefact before testing on a cameraToolchain URLs and directory names are in .github/workflows/pr-build-check.yml.
build* and build-arm* are gitignored.
CMake knobs worth knowing:
-
CMAKE_C_FLAGSis hard-reset to-std=gnu99at the top ofCMakeLists.txt, so-DCMAKE_C_FLAGS=...on the command line does not survive. Add flags inCMakeLists.txt(guarded by a cache option) instead. -
-DBUILD_SHARED_LIBS=ONis what drops the global-static. Needed for a dynamic/ASAN build (use a glibc cross toolchain that ships libasan; musl toolchains do not). -
-DIPCHW_VENDORS=all|none|"sstar;ingenic"selects which vendor HALs go intolibipchw. HiSilicon is always in — it is not in this knob's vocabulary, becausechipid.creaches it directly rather than through the vendor table. Theipctoolexecutable always carries every vendor. -
-DIPCHW_HISI=all|none|"v4"selects which HiSilicon generationslibipchwcan identify — the chip-ID table, a sensor-bus back-end per generation, and the temperature and die-ID readers. Roughly 8 KB on arm32 for the lot.Its default follows
IPCHW_VENDORS, because naming your silicon should not have to be done twice. A narrowing —-DIPCHW_VENDORS=ingenic— says the binary will never boot on a HiSilicon part, soIPCHW_HISIdefaults tonone. The two spellings that are not a narrowing keep every generation:allmeans "identify any camera this is dropped on", andnonemeans only the always-in vendors, which is HiSilicon. Both are howipcinfois built (its own default and the firmware package'snone), so neither moved when this was added; a narrowed consumer lost 8.3 KB. An explicit-DIPCHW_HISIalways wins.Do not reach for weak symbols or a section registry to replace this knob: it was tried, and it works by not pulling
hal_hisi.c.ofrom the archive, which deletes HiSilicon detection from every static consumer including the ones that want it. "Which silicon can this build identify" is a runtime 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 intolibipchw.sstar,ingenicandnovatekare 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, becauseregs_by_chip()andpadmux_ops()name every family from a single switch: a consumer that wants one family has to say so.v4is the expensive HiSilicon token (13.6 KB) because one SDK build runs on ev200, ev300, 3518ev300 and dv200, and those are four different tables;sstaris 25 KB for three families and ~2200 claims. The macros are PUBLIC on theipchwtarget so a consumer can#erroron a family it forgot. Theipctoolexecutable always carries every table. -
-DONLY_LIBRARY=ONbuilds justlibipchw.-DSKIP_VERSION=ONskips the git-derivedversion.c(generated bycmake/version.cmakeon every build). That generated file is compiled by one object library,ipctool_version, whichipctoolandipcinfoboth link. Do not put it back in their source lists: a custom-command output listed in two independent targets gets a copy of its rule in each target'sbuild.make, andmake -jNthen runs both copies at once and they rewrite the file under each other.SKIP_FUNDINGremoves the sponsorship banner from-h. -
Release flags are
-static -s -Os -ffunction-sections -Wl,--gc-sections -Wextra; binary size matters, these run from tmpfs on cameras with a few MB free.
./build/cYAML_test: covers the JSON-to-YAML printer (src/cjson/cYAML.c), including UTF-8 and invalid-byte escaping../build/reginfo_test: the pad-mux tables, theipchw_padmux_*lookups and theipchw_padmux_get/_setaccessors, on a host with no camera.chip_generationandchip_nameare plain globals, so setting them puts any SoC's tables in front of the code under test; the asserted rows are ones a real camera was measured against. It installs a fabricated register file through thepadmux_io_tseam insrc/padmux.h, which is the only way the write paths are exercised at all off a camera. Over every compiled-in family it sweeps structural invariants (a selector fits its field in place, one pad has one GPIO spelling, no pad offers a function twice or two functions through one selector value, no pad number in two places) and then sets every function of every pad and reads it back. It links the reallibipchw, which is what proves the lookups are exported and not merely present../build/longse_test: the_W_version extraction insrc/boards/longse.c, which is the only check that code gets -- no one on the project has a Longse camera. Note it uses a localCHECKmacro rather thanassert(): the release flags carry-DNDEBUG, so anassert()-based test compiles away to nothing and passes unconditionally../build/anyka_test: the Anyka HAL's parsers -- the/proc/cpuinfomachine string, the device tree's/memorycells, the chip-ID table, the model naming, the CIS clock divider and the media-memory arithmetic -- fed fixture files through the path argumentssrc/hal/anyka.hexposes for the purpose, with inputs taken from real boot logs. SameCHECKmacro aslongse_test. The detection itself, including the sensor power-up, was run on an AK3918EV200 with OpenIPC's kernel../build/ambarella_test: the/proc/cpuinfomachine string the Ambarella HAL parses, sameCHECKmacro and same reason.tools/test_pipeline.sh: hardware-free end-to-end check of the sensor driver extraction pipeline (trace_segment.py->trace_to_driver.py->gcc -fsyntax-only->trace_diff.py), plus the--selftestof everytools/*.pythat offers one -- found by running--helpand looking for the option, so a new one needs no wiring and cannot be forgotten. Those parse built-in fixtures because their--verifywants a vendor SDK or a data sheet that CI does not have. That the workflow still runs this script at all is asserted from the unit-tests job, not from here: a check that only runs once the thing it protects has already run protects nothing. Needs only python3 and gcc..github/workflows/pr-build-check.yml: every PR must build clean on arm32, mips32 and arm64 and passtest_pipeline.sh. Keep#ifdef __arm__/__mips__/__aarch64__guards consistent when touching arch-specific code..github/workflows/release.yml: push to master publishes a rollinglatestprerelease; av*tag publishes a real release. Assets areipctool,ipctool-mips32,ipctool-arm64plusipcinfo*..github/workflows/ci-tests.yml: pytest against real lab cameras, triggered byrepository_dispatchonly. Not runnable locally.
Real verification of hardware code means running the binary on a camera.
Cameras usually lack sftp-server, so copy with scp -O; /tmp is tmpfs.
.clang-format is LLVM style with 4-space indent. contributors.md asks for
the hook: ./scripts/git-pre-commit-format install. scripts/apply-format
reformats only the changed hunks of a diff -- but it only learns about
.clang-format-hook-exclude when the hook passes it in, so run
scripts/format-changed instead, or a generated header gets reflowed and stops
matching its generator.
src/main.chand-dispatches subcommands (gpio,reginfo,i2c*/spi*,clocks,trace, ...) before getopt, then handles-c/-s/-t, thenbackup/upload, and with no arguments builds the full YAML report.- The report is a cJSON tree:
build_yaml()calls onedetect_*()per section (detect_chip,detect_board,detect_ethernet,get_mtd_info,detect_ram,detect_firmare,detect_sensors,clocks_build_json) andcYAML_Print()renders it. Empty sections are dropped. New output goes into a cJSON object via theADD_PARAM*macros insrc/tools.h, which assume a local namedj_inner. getchipname()insrc/chipid.cis the "make sure detection ran" call and is memoised. It installs the generic HAL (setup_hal_fallback()), thenhw_detect_system()reads the UART0 base from/proc/iomemto recognise HiSilicon/Goke and XM SoCs by register base, falling back togeneric_detect_cpu(), which walks themanufacturers[]table against the/proc/cpuinfoHardware line. Each entry isdetect_fn+setup_hal_fn, compiled in per architecture and perIPCHW_VENDOR_<NAME>macro.
src/hal/common.h declares one global function pointer per hardware operation
(open_i2c_sensor_fd, i2c_read_register, i2c_change_addr,
hal_temperature, hal_totalmem, hal_chip_properties, hal_firmware_props,
hal_enable_sensor_clock, ...). setup_hal_fallback() fills them with generic
/dev/i2c-N implementations; each vendor's setup_hal_<vendor>() in
src/hal/<vendor>.c overrides what that SoC needs and sets two globals every
consumer depends on:
chip_generation: the switch key used byreginfo.c(pinmux tables),clocks.c(PLL/DDR decoders per HiSilicon family),bootrom.c,ptrace.c,watchdog.c,hal/hisi/ispreg.c, etc. HiSilicon values are theHISI_V*constants insrc/hal/hisi/hal_hisi.h; other vendors use their own small integers.getchipfamily()maps them to family names.possible_i2c_addrs: the per-vendor list of (sensor family, I2C addresses) that sensor probing iterates.
Adding an SoC therefore means: a detect function that fills chip_name and
chip_generation, a setup_hal_* that installs bus access, and then the
per-generation tables in each subcommand that should support it. Adding a new
vendor also needs an entry in IPCHW_OPTIONAL_VENDORS in CMakeLists.txt, an
include in hal/common.h, and a guarded row in manufacturers[].
Anyka (src/hal/anyka.c) shows what to do when the SoC hands you no clean
key. The vendor 3.4 kernels have no device tree and register no UART with
/proc/iomem, so the /proc/cpuinfo machine string is what names the part
(CLOUD39EV3_AK3918EV300_MNBD), while the chip-ID word at 0x08000000 names
only the generation -- one vendor kernel prints the same 0x20160100 as
AK3916, AK3918 or AK3919 depending on which CONFIG_CPU_AK39xx it was built
with, and 0x20120100 is both the Aimer39 AK3918's and some EV200s'. Board
strings often carry only the package (AK3918E80PIN is an EV200 on one board
and an EV300 on another), so the generation completes the name; a device
tree's root compatible (OpenIPC's and the EV330 vendor kernels) names the part
outright and wins. The AV130 keeps its ID at 0x080003fc instead. Reading the
registers is a refinement and never the gate, which is also what lets
detection survive a kernel that will not hand over /dev/mem.
Anyka sensors answer I2C only while the camera driver has them clocked and
out of reset, so on EV200/EV300 hal_enable_sensor_clock starts the CIS
clock (SYSCTRL 0x18, from the peripheral PLL in 0x14) and pulses the
reset/power-down GPIOs the device tree names for the camera node, and
hal_cleanup puts back exactly what it changed. It touches nothing when the
clock is already running, which is a pipeline streaming.
ipcinfo is in every camera image and the size check holds it to 1 KiB of
growth per PR, so it carries the least that works, behind
#ifndef STANDALONE_LIBRARY for the rest. It starts the CIS clock and takes
the part from the device tree; the sensor drivers' power-off leaves reset an
input, so that is enough where the camera node has no pwdn-gpio (those
drivers do assert power-down, on a board that has the pin). Only ipctool
pulses the GPIOs, works a part number out of a vendor kernel's board string,
and knows the PrimeSensor and BYD sensors, which have no tuning in the firmware
package.
Ambarella (src/hal/ambarella.c) stops at the machine string on purpose. The
exact part (S3L33M, S3L55M, S3L66, ...) is a word the DSP firmware writes,
reachable only through /dev/iav, so the report says S3L, plus family: S3Lm
when iav.ko is loaded. Total RAM is the
top of the DSP carve-out, because mem= keeps the kernel far below it: 118 MiB
of 512 MiB on the camera in #266, where the DRAM marking confirmed the figure.
The carve-out comes from /proc/device-tree/iavmem/reg, or from the boot log's
IAVMEM line on kernels with no /proc/device-tree, which Dahua's 3.10.73 is.
Only its end is trusted: Hikvision's kernel moves the split at boot and leaves
/iavmem stale, so media is what lies above Linux's RAM in /proc/iomem, and
with no such RAM listed it is left out, not guessed.
That line is printed once, so if the ring buffer has dropped it the report falls
back to what the kernel sees. Stock Dahua firmware has no /dev/i2c-* either:
its own sensor.ko owns the bus, so no sensor is reported there, and reaching
for the controller registers would race that driver. The same kernel refuses
execve() from writable filesystems, so /tmp/ipctool fails with "Success"
and exit code 2.
Sensor I2C addresses in tables are the 8-bit (write) form; the default
i2c_change_addr shifts right by one for the kernel, and some HALs install
i2c_change_plain_addr instead. Ingenic gates the sensor clock, which is why
hal_enable_sensor_clock runs before every probe rather than once.
src/sensors.c runs detect_possible_sensors(): for each address in
possible_i2c_addrs it calls the vendor-family probe (detect_sony_sensor,
detect_smartsens_sensor, ...) which reads ID registers through the HAL
pointers and fills a sensor_ctx_t. Several parts share IDs or latch into
each other after WDR cycles (IMX335/IMX347/IMX415, SP2305/OV2735), so probes
carry fingerprint logic; read the surrounding comments before reordering
them. getsensoridentity()/getsensorshort() are the memoised, mutex-guarded
public entry points used by libipchw consumers.
src/boards/common.c holds a table of (is_<vendor>_board, gather_<vendor>_board_info)
pairs; the first detector that matches and gathers successfully wins.
libipchw compiles chipid.c, sensors.c, hal/common.c, hal/hisi/* and
the selected vendor HALs with -DSTANDALONE_LIBRARY. Anything that touches
cJSON or prints diagnostics in those shared files must sit inside
#ifndef STANDALONE_LIBRARY, or the library build breaks.
ipctool trace(src/ptrace.c,src/hal/hisi/ptrace.c) is a ptrace-based syscall decoder for camera I/O; the syscall table is hard-coded for 32-bit ARM EABI and the command is compiled only under__arm__.- Register access goes through
mem_reg()insrc/tools.c, which mmaps/dev/memin 64 KiB windows and falls back toPAGE_SIZEwhen the kernel rejects the larger window. Kernels built with strict devmem filtering can refuse some ranges entirely. It is not thread-safe -- one cached 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.*,src/hal/ingenic_padmux.*andsrc/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 apadmux_ops_tseam rather than one table format.docs/padmux.mdhas the mechanisms, the generators, and the traps -- read it before touching any of this. The HiSilicon rows are indexed by selector value, so an unassigned value must appear as"reserved"or every function after it is off by one; a data sheet's "Software Multiplexed Pins" chapter lists alternatives by position with the holes closed up, which is how 19 rows of each of the V2 tables came to be wrong.tools/check_hisi_padmux.pydiffs a table against the register chapter. Two more that catch everyone: a function is not unique to a pad (hi3516ev300 has PWM2 and PWM3 on three pads each), and the spelling is not portable (PWM_OUT0on V1,PWM0from V2,PWM0_OUT1on V5,PWM0_MODE_4on SigmaStar;SVB_PWMandPMC_PWMare different controllers, so match the prefix anchored, never anywhere in the string). The SigmaStar, Ingenic and Novatek tables are generated from vendor sources bytools/gen_*_padmux.pyand are in.clang-format-hook-exclude. Novatek's generator is the odd one: the NA51089 SDK has no pin table, only the driver'spinmux_config_*()code, so it compiles that code on the host with every register write logged and runs it -- once per die, NA51089, NA51055 and NA51084 -- and it needs a C compiler and the SDK; its--selftestneeds only the compiler.ipchw_padmux_by_func/_by_prefix/_by_padare safe to call concurrently once the SoC has been detected; detection itself is not, so callgetchipname()once at startup.ipchw_padmux_get/_settouch/dev/memand must stay on one thread. gpio get/set/scanreach a GPIO controller per vendor: HiSilicon's bank words insrc/reginfo.c(get_chip_gpio_adress()), SigmaStar Infinity6C's per-pad registers insrc/hal/sstar_gpio.c, and the Novatek dies' four DATA/DIR/SET/CLR banks insrc/hal/novatek_gpio.c. Ingenic has none, so onlygpio muxandreginfo --padswork there.src/fake_symbols.cholds empty definitions of HiSilicon SDK audio symbols, added when the Hi3518EV100 SDK was linked in; nothing in the current tree references them.src/stack.cis a stack-protector shim and is not in the build.
tools/ holds the Python post-processing for ipctool trace
(trace_segment.py, trace_to_driver.py, trace_diff.py),
capture_sensor.sh (builds ipctool for ARM and captures a trace from a
Majestic or Sofia camera over ssh/telnet), and firmware helpers
(upgrade_bundle.py, binwalk.py, telnet_upload.py). The full workflow,
per-family decoder coverage and troubleshooting live in
docs/sensor-driver-extraction.md.