From 4ee1c2b631a6a517eb300365693887f7e606e8b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Sun, 24 May 2026 11:22:19 +0200 Subject: [PATCH] probes: add probe-config profiles loaded from probes.yaml - new modules/probes/ parses probes.yaml (libyaml): a defaults: map applied on every jtag_open + named profiles: selected with `jtag_open ` (jtag_profiles lists them); each value is pushed into the script envvar store the driver reads at open time - ships a flashpro profile (ADBUS4 high-Z) that lets the IGLOO2 kit's embedded FlashPro (FT4232H, port 0) detect the chain - CLAUDE.md: decision entry for probes.yaml + a design note on the probe / JTAG-link / device config strategy (driver-neutral link layer) Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 66 +++++++++++ README.md | 7 +- modules/probes/CMakeLists.txt | 12 ++ modules/probes/probes.c | 213 ++++++++++++++++++++++++++++++++++ modules/probes/probes.h | 40 +++++++ modules/script/script.c | 56 ++++++++- probes.yaml | 27 +++++ 7 files changed, 419 insertions(+), 2 deletions(-) create mode 100644 modules/probes/CMakeLists.txt create mode 100644 modules/probes/probes.c create mode 100644 modules/probes/probes.h create mode 100644 probes.yaml diff --git a/CLAUDE.md b/CLAUDE.md index 16fe4f9..b0211ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,6 +116,21 @@ probe" if the libs are absent), it costs nothing to build in: disable with `-DBS_ENABLE_DIGILENT=OFF`. Adept Runtime is only needed at runtime to actually drive such a probe. +### Probe config profiles (probes.yaml) + +Probe wiring/electrical settings live in `probes.yaml` (parsed by +`modules/probes/`, libyaml), layered on top of the built-in +`config.script` defaults: a `defaults:` map applied on every `jtag_open` +(so opening without a profile is deterministic) plus named `profiles:` +selected with `jtag_open ` (`jtag_profiles` lists them). +Each key/value is pushed into the script envvar store the driver reads at +open time. The mechanism is driver-agnostic (any `set`-able probe var), +but today only the FTDI driver (and minimally the Linux-GPIO one) reads +config envvars, so profiles mostly tune FTDI. Motivating case: the +embedded FlashPro on Microsemi kits (FT4232H ch.A) needs ADBUS4 high-Z — +the `flashpro` profile sets `PROBE_FTDI_SET_PIN_DIR_ADBUS4: 0`. See the +config-strategy design note below for where this is headed. + ### Xilinx caveats On 7-Series / UltraScale / UltraScale+, `CCLK` is not a regular I/O @@ -127,6 +142,57 @@ The FPGA must be in a configurable state before loading the proxy. Issue `JPROGRAM` first to reset, then `CFG_IN` + shift the bitstream, then `JSTART` and check `DONE`. +## Probe / JTAG-link / device config strategy (design note) + +Partly built, partly planned. Guiding idea: **separate three concerns +that are conflated today, and express the shared JTAG-link settings in +driver-neutral terms that get resolved per session.** + +### The three concerns + +| Layer | What it owns | Where it lives | Applied | +|-------|-------------|----------------|---------| +| **Probe (sonde)** | driver + interface, pin map, buffer-enable, TRST/SRST pins, level-shift, *max TCK the adapter supports* | `probes.yaml` (done) | at `jtag_open` | +| **JTAG link** | TCK freq, RTCK, reset behaviour, chain layout — **driver-neutral names**, resolved to effective values | *missing today* | open, then refined after detect | +| **Device** | IDCODE/BSDL/IR/proxy/caveats, programming method, *max TCK the part/board tolerates* | `fpga_registry.yaml` (done) | after IDCODE match | + +### The smell that motivated this + +Frequency has no single home. Only the FTDI driver reads +`PROBE_FTDI_TCK_FREQ_KHZ`; our Digilent driver **hardcodes 4 MHz** +(`digilent_jtag_drv.c`); J-Link/LPT read nothing. And the `PROBE_FTDI_*` +namespace mixes probe *wiring* (pin map, ADBUS4) with *link* properties +(freq, RTCK, TRST timing). Frequency isn't an FTDI fact — it's a link +fact bounded by both the probe and the board/device. + +### Target model + +- **Driver-neutral link vars** (`tck_khz`, `rtck`, `reset`, …) set once + (by user / probe defaults / device). Each driver consumes them: our + Digilent driver reads them directly; the Viveris FTDI driver is fed + `PROBE_FTDI_TCK_FREQ_KHZ` via a thin translation shim at open (no + Viveris edit). J-Link/LPT can stay on their defaults. +- **Resolution**: effective `tck = min(user request, probe max, device/ + board max)`. A small session step computes it at open, and re-applies + after `jtag_autoinit` once the device (hence its cap / programming + method) is known — using the `jtag_close`→reopen seam if a re-init is + needed. This also dissolves the chicken-and-egg: a conservative link + default gets you to detection, then the device refines it. +- `probes.yaml` gains an optional `max_tck_khz`; `fpga_registry.yaml` + gains optional `max_tck_khz` + a `prog` method tag (`proxy_spi`/`svf`). + +### Phasing + +- **A** — one canonical `tck_khz` honoured by FTDI (shim) + Digilent + (read it): kills the immediate smell, one knob for the two real probes. +- **B** — device `max_tck_khz` + resolution after `jtag_autoinit`. +- **C** — generalise the other link settings (reset/RTCK) and wire the + `prog` method tag into backend dispatch (ties into the SVF player). + +What exists already: the **probe layer** (`probes.yaml`) and the +**device layer** (`fpga_registry.yaml`). The new work is the **JTAG-link +layer** in the middle. + ## Programming backends: beyond Xilinx external flash (design note) Not yet implemented — captured so the design is ready. Guiding vision: diff --git a/README.md b/README.md index ca306cc..3b217d1 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ the full walkthrough (probe → proxy → flash) lives in | Category | Commands | |----------|----------| | Script control | `set`, `print`, `print_env_var`, `if`, `goto`, `call`, `return`, `rand`, `init_array`, `system`, `pause` | -| Probe / chain | `jtag_probes`, `jtag_open`, `jtag_close`, `jtag_scan`, `jtag_autoinit`, `jtag_ndev`, `jtag_devices` | +| Probe / chain | `jtag_probes`, `jtag_open`, `jtag_close`, `jtag_profiles`, `jtag_scan`, `jtag_autoinit`, `jtag_ndev`, `jtag_devices` | | BSDL / pins | `jtag_bsdl`, `jtag_pins`, `jtag_mode`, `jtag_pin_dir`, `jtag_pin_set`, `jtag_pin_get`, `jtag_push_pop` | | I²C / MDIO / SPI over BS pins (EXTEST) | `jtag_i2c_scl`, `jtag_i2c_sda`, `jtag_i2c_rd`, `jtag_i2c_wr`, `jtag_mdio_mdc`, `jtag_mdio_io`, `jtag_mdio_rd`, `jtag_mdio_wr`, `jtag_spi_cs/mosi/miso/clk`, `jtag_spi_xfer` | | FPGA registry | `fpga_list`, `fpga_info` | @@ -121,6 +121,10 @@ Use `help ` for per-command help. - **FTDI** MPSSE (FT2232D/H, FT4232H, …) — see the `PROBE_FTDI_*` block in `modules/config/config.script` for pin mapping and TCK frequency. + Boards that wire the FT4232H differently (e.g. the embedded FlashPro on + Microsemi eval kits, which needs ADBUS4 left high-Z) are handled by a + **probe profile** in `probes.yaml`: `jtag_profiles` lists them, + `jtag_open ` applies one (e.g. `jtag_open 0 flashpro`). - **SEGGER J-Link** - **Linux GPIO** (sysfs; deprecated on recent kernels, libgpiod migration TBD) - **Digilent JTAG-SMT2 / SMT2-NC** — built in by default on Linux @@ -159,6 +163,7 @@ modules/ ├── os_interface/ Portable fs/network wrappers └── natsort/ Natural-order pin-name sorting fpga_registry.yaml FPGA registry (IDCODE → BSDL, IR opcodes, proxy, caveats) +probes.yaml Probe-config profiles (defaults + per-probe overrides) bsdl_files/ BSDL files for target FPGAs bscan_proxies/ BSCAN proxy bitstreams (MIT, from quartiq) scripts/ Example scripts diff --git a/modules/probes/CMakeLists.txt b/modules/probes/CMakeLists.txt new file mode 100644 index 0000000..79bf904 --- /dev/null +++ b/modules/probes/CMakeLists.txt @@ -0,0 +1,12 @@ +set(SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + +file(GLOB_RECURSE ALL_SOURCES "*.c") + +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..) + +add_library(probes ${ALL_SOURCES}) + +# probes.yaml is parsed at runtime via libyaml (same as the fpga module). +find_package(PkgConfig REQUIRED) +pkg_check_modules(YAML REQUIRED IMPORTED_TARGET yaml-0.1) +target_link_libraries(probes PUBLIC PkgConfig::YAML) diff --git a/modules/probes/probes.c b/modules/probes/probes.c new file mode 100644 index 0000000..316044a --- /dev/null +++ b/modules/probes/probes.c @@ -0,0 +1,213 @@ +#include +#include +#include +#include + +#include + +#include "probes.h" + +/* + * probes.yaml parser. Structure: + * + * defaults: + * VAR: value + * ... + * profiles: + * name: + * VAR: value + * other: {} + * + * Parsed once and cached. See probes.h for semantics. + */ + +#define DEFAULT_PROBES_FILE "probes.yaml" + +typedef struct { char *key; char *val; } kv; +typedef struct { char *name; kv *vars; int nvars; } profile; + +static kv *g_def = NULL; static int g_ndef = 0; +static profile *g_prof = NULL; static int g_nprof = 0; +static int g_loaded = 0; +static char g_source[1024] = ""; + +static char *xstrdup(const char *s) +{ + char *p; + size_t n; + if (!s) return NULL; + n = strlen(s) + 1; + p = (char *)malloc(n); + if (p) memcpy(p, s, n); + return p; +} + +static void add_kv(kv **arr, int *n, const char *k, const char *v) +{ + kv *t = (kv *)realloc(*arr, (*n + 1) * sizeof(kv)); + if (!t) { fprintf(stderr, "probes: out of memory\n"); return; } + *arr = t; + t[*n].key = xstrdup(k); + t[*n].val = xstrdup(v); + (*n)++; +} + +static profile *add_profile(const char *name) +{ + profile *t = (profile *)realloc(g_prof, (g_nprof + 1) * sizeof(profile)); + if (!t) { fprintf(stderr, "probes: out of memory\n"); return NULL; } + g_prof = t; + g_prof[g_nprof].name = xstrdup(name); + g_prof[g_nprof].vars = NULL; + g_prof[g_nprof].nvars = 0; + return &g_prof[g_nprof++]; +} + +/* Mapping context at each nesting depth. */ +enum { C_NONE = 0, C_ROOT, C_DEFAULTS, C_PROFILES, C_PROFBODY }; + +#define MAX_DEPTH 16 + +static int load(const char *path) +{ + FILE *f; + yaml_parser_t parser; + yaml_event_t ev; + int depth = 0, done = 0, rc = 0; + int ctx[MAX_DEPTH]; + char *key[MAX_DEPTH]; + profile *cur = NULL; + int i; + + f = fopen(path, "rb"); + if (!f) return -1; + if (!yaml_parser_initialize(&parser)) { fclose(f); return -1; } + yaml_parser_set_input_file(&parser, f); + + memset(ctx, 0, sizeof(ctx)); + memset(key, 0, sizeof(key)); + + while (!done) { + if (!yaml_parser_parse(&parser, &ev)) { + fprintf(stderr, "probes: YAML parse error in %s: %s (line %lu)\n", + path, parser.problem ? parser.problem : "?", + (unsigned long)parser.problem_mark.line + 1); + rc = -1; + break; + } + + switch (ev.type) { + case YAML_MAPPING_START_EVENT: { + int newc = C_NONE; + if (depth == 0) { + newc = C_ROOT; + } else { + int c = ctx[depth]; + const char *k = key[depth]; + if (c == C_ROOT) { + if (k && !strcmp(k, "defaults")) newc = C_DEFAULTS; + else if (k && !strcmp(k, "profiles")) newc = C_PROFILES; + } else if (c == C_PROFILES) { + cur = k ? add_profile(k) : NULL; + newc = C_PROFBODY; + } + } + if (depth < MAX_DEPTH - 1) depth++; + ctx[depth] = newc; + free(key[depth]); + key[depth] = NULL; + break; + } + case YAML_MAPPING_END_EVENT: + free(key[depth]); + key[depth] = NULL; + if (depth > 0) depth--; + break; + case YAML_SCALAR_EVENT: { + const char *v = (const char *)ev.data.scalar.value; + int c = ctx[depth]; + if (c == C_DEFAULTS) { + if (!key[depth]) { key[depth] = xstrdup(v); } + else { add_kv(&g_def, &g_ndef, key[depth], v); free(key[depth]); key[depth] = NULL; } + } else if (c == C_PROFBODY) { + if (!key[depth]) { key[depth] = xstrdup(v); } + else { if (cur) add_kv(&cur->vars, &cur->nvars, key[depth], v); free(key[depth]); key[depth] = NULL; } + } else { + /* C_ROOT / C_PROFILES: the scalar is a key/name whose + * value is the next mapping. */ + free(key[depth]); + key[depth] = xstrdup(v); + } + break; + } + case YAML_STREAM_END_EVENT: + done = 1; + break; + default: + break; + } + yaml_event_delete(&ev); + } + + for (i = 0; i < MAX_DEPTH; i++) free(key[i]); + yaml_parser_delete(&parser); + fclose(f); + return rc; +} + +static void ensure_loaded(void) +{ + const char *path; + + if (g_loaded) return; + g_loaded = 1; + + path = getenv("BS_PROBES"); + if (!path || !*path) path = DEFAULT_PROBES_FILE; + + if (load(path) == 0 && (g_ndef > 0 || g_nprof > 0)) { + strncpy(g_source, path, sizeof(g_source) - 1); + g_source[sizeof(g_source) - 1] = '\0'; + } + /* Silent when absent: probe profiles are optional. */ +} + +int probe_apply_defaults(probe_set_fn set, void *user) +{ + int i; + ensure_loaded(); + for (i = 0; i < g_ndef; i++) set(user, g_def[i].key, g_def[i].val); + return g_ndef; +} + +int probe_apply_profile(const char *name, probe_set_fn set, void *user) +{ + int i, j; + ensure_loaded(); + for (i = 0; i < g_nprof; i++) { + if (!strcmp(g_prof[i].name, name)) { + for (j = 0; j < g_prof[i].nvars; j++) + set(user, g_prof[i].vars[j].key, g_prof[i].vars[j].val); + return g_prof[i].nvars; + } + } + return -1; +} + +int probe_profile_count(void) +{ + ensure_loaded(); + return g_nprof; +} + +const char *probe_profile_name(int index) +{ + ensure_loaded(); + return (index >= 0 && index < g_nprof) ? g_prof[index].name : NULL; +} + +const char *probe_config_source(void) +{ + ensure_loaded(); + return g_source[0] ? g_source : NULL; +} diff --git a/modules/probes/probes.h b/modules/probes/probes.h new file mode 100644 index 0000000..8438396 --- /dev/null +++ b/modules/probes/probes.h @@ -0,0 +1,40 @@ +#ifndef _PROBES_H +#define _PROBES_H + +/* + * Probe configuration profiles, loaded at runtime from probes.yaml. + * + * The built-in config.script (and an optional CWD config.script) set the + * baseline probe variables. probes.yaml layers on top of that: + * - a `defaults:` map applied on every jtag_open (so opening without a + * profile restores a known baseline), then + * - a named `profiles:` map whose vars override the defaults for one + * probe (e.g. an embedded FlashPro that needs ADBUS4 high-Z). + * + * Each key/value is pushed into the script envvar store via the caller's + * callback — the same vars config.script sets and the driver reads at + * open time (jtagcore_getEnvVarValue). This keeps the parser decoupled + * from the script context. + * + * File lookup: $BS_PROBES, else "probes.yaml" relative to the current + * directory (same convention as fpga_registry.yaml). Absence is silent — + * profiles are optional. + */ + +/* Callback used to set one envvar. `user` is opaque to this module. */ +typedef void (*probe_set_fn)(void *user, const char *key, const char *value); + +/* Apply the defaults: map. Returns the number of vars applied (0 if none + * or no file). */ +int probe_apply_defaults(probe_set_fn set, void *user); + +/* Apply the named profile's vars on top of the defaults. Returns the + * number of vars applied, or -1 if no such profile exists. */ +int probe_apply_profile(const char *name, probe_set_fn set, void *user); + +/* Listing / diagnostics. */ +int probe_profile_count(void); +const char *probe_profile_name(int index); +const char *probe_config_source(void); /* path loaded, or NULL */ + +#endif diff --git a/modules/script/script.c b/modules/script/script.c index c393ec5..480c0a2 100644 --- a/modules/script/script.c +++ b/modules/script/script.c @@ -38,6 +38,7 @@ #include "bsdl_parser/bsdl_loader.h" #include "os_interface/os_interface.h" #include "fpga/fpga.h" +#include "probes/probes.h" #include "bscan_spi/bscan_spi.h" #include "spi_flash/spi_flash.h" @@ -1802,10 +1803,21 @@ static int cmd_print_probes_list(script_ctx *ctx, char *line) return JTAG_CORE_NO_ERROR; } +/* Push one probes.yaml key/value into the script envvar store — the same + * store config.script writes and the driver reads at open time. */ +static void probe_envvar_set(void *user, const char *key, const char *value) +{ + script_ctx *ctx = (script_ctx *)user; + setEnvVarDat((envvar_entry *)ctx->env, (char *)key, (char *)value); +} + const char *cmd_open_probe_help[] = { - "(int)", // Arguments "1(type) ..." + "(int) [profile](str)", // Arguments "1(type) ..." "Open a probe by its index from jtag_probes (0, 1, 2, ...).", "A raw 0x-prefixed probe id (as printed after the index) also works.", + "Optional [profile] applies a probe-config profile from probes.yaml", + "(e.g. 'flashpro'); jtag_profiles lists them. The profile's variables", + "override the probes.yaml defaults for this open.", "" }; static int cmd_open_probe(script_ctx *ctx, char *line) @@ -1813,6 +1825,7 @@ static int cmd_open_probe(script_ctx *ctx, char *line) int ret; int id; char probe_id[64]; + char profile[64]; jtag_core *jc; jc = (jtag_core *)ctx->app_ctx; @@ -1836,6 +1849,20 @@ static int cmd_open_probe(script_ctx *ctx, char *line) } } + // Apply the probes.yaml defaults, then the optional named profile, + // into the envvar store the driver reads when it initialises. + probe_apply_defaults(probe_envvar_set, ctx); + if (get_param(ctx, line, 2, profile) > 0) + { + if (probe_apply_profile(profile, probe_envvar_set, ctx) < 0) + { + ctx->script_printf(ctx, MSG_ERROR, + "Unknown probe profile '%s'. Run jtag_profiles to list them.\n", profile); + return JTAG_CORE_BAD_PARAMETER; + } + ctx->script_printf(ctx, MSG_INFO_0, "Applied probe profile '%s'.\n", profile); + } + ret = jtagcore_select_and_open_probe(jc, id); if (ret != JTAG_CORE_NO_ERROR) { @@ -1889,6 +1916,32 @@ static int cmd_close_probe(script_ctx *ctx, char *line) return JTAG_CORE_NO_ERROR; } +const char *cmd_profiles_help[] = { + "", + "List the probe-config profiles defined in probes.yaml.", + "Apply one when opening: jtag_open .", + "" +}; +static int cmd_profiles(script_ctx *ctx, char *line) +{ + int i, n; + const char *src; + + (void)line; + n = probe_profile_count(); + src = probe_config_source(); + if (!src) + { + ctx->script_printf(ctx, MSG_INFO_0, + "No probes.yaml loaded; using built-in / config.script defaults.\n"); + return JTAG_CORE_NO_ERROR; + } + ctx->script_printf(ctx, MSG_INFO_0, "%d probe profile(s) in %s:\n", n, src); + for (i = 0; i < n; i++) + ctx->script_printf(ctx, MSG_NONE, " %s\n", probe_profile_name(i)); + return JTAG_CORE_NO_ERROR; +} + const char *cmd_load_bsdl_help[] = { "(string) (int)", "Load/attach a bsdl file to a device into the chain.", @@ -3368,6 +3421,7 @@ cmd_list script_commands_list[] = {"jtag_probes", cmd_print_probes_list, cmd_print_probes_list_help}, {"jtag_open", cmd_open_probe, cmd_open_probe_help}, {"jtag_close", cmd_close_probe, cmd_close_probe_help}, + {"jtag_profiles", cmd_profiles, cmd_profiles_help}, {"jtag_autoinit", cmd_autoinit, cmd_autoinit_help}, {"jtag_scan", cmd_init_and_scan, cmd_init_and_scan_help}, {"jtag_ndev", cmd_print_nb_dev, cmd_print_nb_dev_help}, diff --git a/probes.yaml b/probes.yaml new file mode 100644 index 0000000..4cb1a53 --- /dev/null +++ b/probes.yaml @@ -0,0 +1,27 @@ +# bs_explorer probe-config profiles +# +# Loaded at runtime by modules/probes/, layered on top of the built-in +# config.script defaults. Looked up CWD-relative (run from the repo +# root), or via $BS_PROBES. +# +# defaults: applied on every `jtag_open` (restores a known baseline +# so opening without a profile is deterministic). +# profiles: named override sets; select with `jtag_open `. +# +# Each key is a probe variable (see modules/config/config.script for the +# full list); each value is what `set ` would assign. + +defaults: + # Baseline for a standalone FT2232H JTAG probe: drive ADBUS4 as the + # "JTAG buffer enable" output (matches the built-in default). + PROBE_FTDI_SET_PIN_DIR_ADBUS4: 1 + +profiles: + # Embedded FlashPro on Microsemi eval kits (FT4232H, JTAG on channel A + # = probe index 0). Its ADBUS4 must be left high-Z or the chain stays + # silent. Usage: jtag_open 0 flashpro + flashpro: + PROBE_FTDI_SET_PIN_DIR_ADBUS4: 0 + + # Plain FT2232H probe: nothing to override beyond the defaults. + ft2232h: {}