Skip to content

Latest commit

 

History

History
161 lines (110 loc) · 11.2 KB

File metadata and controls

161 lines (110 loc) · 11.2 KB

CLAUDE.md

Guidance for Claude Code (and other AI coding agents) when working in this repository.

What this is

go-hardware-driver is a small C daemon that runs on GOcontroll Moduline embedded controllers (aarch64 Linux). At boot it reads /lib/firmware/gocontroll/modules.json, instantiates a per-slot module driver, and runs a fixed 100 Hz tick loop forever. Per-channel input values are published, and per-channel output commands are read, via plain ASCII files under /dev/shm/gocontroll/. The shm layout is the integration contract for downstream apps (Node-RED, Simulink-generated controllers, fleet apps, custom scripts) — they treat the driver as a hardware-abstraction layer.

It is one of three tightly coupled packages on the controller; understanding the boundaries matters:

Package Responsibility Key persisted artifact
go-modules Scan SPI bus, identify modules, flash firmware, write modules.json /lib/firmware/gocontroll/modules.json
go-hardware-driver (this repo) Read modules.json and run the per-slot module driver loop (100 Hz) /dev/shm/gocontroll/slot{N}/...
go-web-ui Browser-based config editor + status view reads/writes modules.json

modules.json is the single source of truth. The JSON schema is owned by go-modules and documented in GOcontroll-Architecture/modules/configuration.md — this driver is a strict consumer.

Build / package

Cross-compile target is aarch64-unknown-linux-gnu using aarch64-linux-gnu-gcc. Vendor sources come from the GOcontroll C support codebase (the GOcontroll-CodeBase repo). The Makefile expects GO_BASE to point at a checkout of that repo:

make GO_BASE=/path/to/GOcontroll-CodeBase

Output: build/go-hardware-driver.

For ad-hoc on-controller testing, building natively works too if libjson-c-dev, libiio-dev and gcc are installed; the cross-compiler flags can be overridden:

make GO_BASE=/path/to/codebase CC=gcc \
  CPPFLAGS="-Iinclude -I.../codebase/code -I.../codebase/code/modules -I/usr/include/json-c -DGOCONTROLL_LINUX -D_GNU_SOURCE -MMD -MP" \
  LDFLAGS="" LDLIBS="-ljson-c -liio -lrt -lpthread -lm"

The CI workflow (.github/workflows/build-package.yml) triggers on v* tags and produces an arm64 .deb plus a GitHub Release. Tag drives the version — the matching debian/changelog entry must be updated in the same commit.

Runtime architecture

Boot path

  1. main() installs signal handlers, calls shm_base_init() (creates /dev/shm/gocontroll/), and GO_board_get_hardware_version() (single per-process call — see "GOcontroll-CodeBase quirks" below).
  2. config_load("/lib/firmware/gocontroll/modules.json", …) parses the JSON. Schema must be 1.x.x1.x is forwards-compatible; major-version mismatch refuses to load. Empty / unknown / duplicate slots are tolerated (warning + skip) so the driver keeps running.
  3. For each non-empty slot the driver looks up the module-type ops via registry_lookup(). Type-name strings must match the JSON "module_type" field exactly.
  4. The per-slot init() is called. If init fails, the slot is skipped — the driver does not abort. This is important: a single misconfigured or unhealthy slot must never bring down the whole driver.
  5. The main loop ticks every 10 ms (CLOCK_MONOTONIC, absolute deadlines via clock_nanosleep with TIMER_ABSTIME). Each tick walks the active drivers and calls tick(). EINTR during the sleep is tolerated unless a stop signal arrived.
  6. On SIGINT / SIGTERM the loop exits cleanly, each driver's shutdown() is called.

Adding / modifying a module driver

Drivers implement a small ops table:

struct driver_ops {
    const char *type;                           /* matches modules.json "module_type" */
    int  (*init)(struct driver *drv, const struct slot_cfg *cfg);
    int  (*tick)(struct driver *drv);
    void (*shutdown)(struct driver *drv);
};

Concrete driver files live under src/modules/. A new driver must:

  1. Implement the four ops (or stub them — see src/modules/ir_communication.c for the canonical stub pattern that fails init with a clear message until full SPI support lands).
  2. Export the ops table as const struct driver_ops <name>_ops.
  3. Register it in src/registry.c (add extern declaration + entry in TABLE).
  4. Add the source file to OWN_SRCS in the Makefile.

The type string must exactly match the module_type written by go-modules. If the JSON name changes, both repos must be updated in lockstep.

SHM layout (integration contract)

/dev/shm/gocontroll/
├── slot{N}/
│   ├── channel{C}/
│   │   ├── value          # input: measured value (driver writes)
│   │   │                  # output: command (host writes)
│   │   ├── reset_value    # input only: pulse-counter reset value (host writes)
│   │   ├── reset_trigger  # input only: edge-triggered reset (host writes)
│   │   ├── current        # output feedback (mA, driver writes)
│   │   └── duty           # output feedback (driver writes)
│   ├── temperature       # module-level (output modules)
│   ├── ground
│   ├── supply
│   └── error_code        # output-10ch also has total_current
└── <name> -> slot{N}/channel{C}/value   (alias when channel.name is given)

All values are ASCII text (%d\n). Single atomic writes via pwrite(fd, buf, n, 0) — readers see either the old or the new value, never a mix. Downstream consumers should open() + pread() per sample (cheap on a tmpfs); the driver does the same. Don't switch to any binary or shared-memory layout without coordinating across all downstream tooling.

Pulse-counter / encoder reset

For input modules, each channel publishes reset_value (int32) and reset_trigger (uint8). The host requests a counter reset by:

  1. Writing the desired counter value to reset_value.
  2. Changing the byte in reset_trigger to a new value (any change = one reset pulse; e.g. monotonically increment).

The driver reads both each tick and delegates to the codebase pulse-counter reset helper, which only fires an SPI reset when the trigger byte changed since last seen — edge-triggered semantics, so the host does not have to self-clear. On driver restart both files are truncated to avoid a stale trigger from the previous run racing the fresh pulscounterResetTrigger=0 in the codebase and causing a spurious reset.

Real-time tuning

The systemd unit starts the driver during sysinit.target (before multi-user.target) with:

  • DefaultDependencies=no — bypass default ordering and hardening for minimum startup latency.
  • Nice=-20 — highest priority for SCHED_OTHER.
  • CPUAffinity=3 — pin on CPU 3, the same core as go-simulink. On the M1 (4 cores) CPUs 0–2 stay free for general system load; all real-time loops cluster on one isolated core for cache locality and predictable scheduling.

Don't add scheduling-class changes (SCHED_FIFO/SCHED_RR) without coordinating with go-simulink — they share a core.

modules.json consumption

Schema contract (current)

This driver only requires schema-major 1. The fields it reads:

  • Top-level: schema_version, controller, slots[].
  • Per-slot: slot, module_type, enabled, plus opaque module object and channels[] array. The latter two are parsed inside the per-module driver — config.c keeps them as opaque json_object * pointers owned by the root document.

Tolerant parsing:

  • Empty slots (no module_type) → warning + skipped, driver continues.
  • Unknown module_type → warning + skipped, driver continues.
  • Duplicate slot numbers → first occurrence wins, duplicates dropped with a warning.
  • enabled missing → defaults to true (so older modules.json keeps the slot active).

If you make the parser stricter, the M1 / L4 / HMI fleet in the field will start refusing to boot — don't do that without a controller-side migration plan and lining up the other two packages.

The enabled flag

Per-slot enabled: bool (added with go-modules 3.2.0 / driver 0.2.0). When false, main.c skips the slot completely — no reset pulse, no bootloader skip, no init(), no tick. The user uses this to hand a module to an external app that wants to drive it directly. Make sure new behaviour in main.c respects this: if you add reset-line wiggling or board-level setup that touches the slot before the per-driver init(), it must also be gated by sc->enabled.

GOcontroll-CodeBase quirks (vendor source)

The driver pulls in code/GO_board.c, code/GO_communication_modules.c, the per-module helpers under code/modules/, and supporting bits (print.c, GO_xcp.c, XcpStack.c, GO_memory.c, GO_fault.c, GO_controller_info.c). Things to know when touching code that interacts with them:

  • GO_board_get_hardware_version() is called exactly once in main() because it detects the controller and zeroes the global moduleOccupancy table as a side-effect. Calling it per-driver would wipe registrations of already-initialized slots. Don't move it.
  • moduleOccupancy and the module-set / module-slot helpers in the codebase carry process-wide state; the driver assumes single-threaded access (the 100 Hz tick loop is the only writer). Don't add background threads without working out the locking.
  • Some module helpers exit(-1) on bad arguments (slot out of range, contested slot). The driver itself never calls exit() — it skips the bad slot and keeps running. When using new codebase functions, check for exit paths and either gate the call or accept the process-level failure.

Conventions / gotchas

  • One slot's failure must never bring down the driver. Every per-driver init/tick error path skips and continues. Preserve that property.
  • Schema-tolerant parsing. Missing optional fields, unknown module types, duplicate slots: warn + carry on. Don't add hard-fail paths in config.c.
  • Atomic single-byte/word pwrite() for shm. No partial writes, no truncation, no rename-dance. Readers expect to see either the old or the new value.
  • Per-driver state in the ops struct's state pointer. Don't add globals to the per-module C files — slot 2 and slot 5 may run the same driver type at the same time.
  • Versioning. debian/changelog drives the version. Tag vX.Y.Z matches the top entry. Major bumps are the right signal when changing the shm layout or the modules.json schema in an incompatible way.
  • No interactive prompts. This is a systemd-managed daemon — no stdin, no getchar(), no TUI.

Related architecture documentation

The authoritative module schemas live in the GOcontroll-Architecture repo under modules/:

  • configuration.mdmodules.json JSON schema and runtime contract
  • naming.md — article-number format + module-type enumeration
  • pinning.md — per-module pin assignments per controller slot
  • spi.md — module SPI protocol reference
  • input-6ch.md, input-10ch.md, input-4-20ma.md, bridge-2ch.md, output-6ch.md, output-10ch.md, ir-communication.md — per-module specs (channel configs, wire-encoding, defaults, open questions)

When changing how the driver consumes modules.json or what it writes to /dev/shm/gocontroll/, update the matching architecture document(s) in the same change.