Variable definition

The definition object of a declaration is where a variable is actually described. It is the same object whatever the component and whatever the scope, and it deliberately says more than a c declaration could: a c declaration knows how many bytes a value occupies, but not what the number in those bytes means. uint16_t Speed; and uint16_t Pressure; are the same declaration; uint16 scaled by 0.25 in Hz between 0 and 8000 and uint16 scaled by 0.001 in bar between 0 and 5 are two different things, and the difference is exactly what a calibration tool needs, what a consuming component has to agree with, and what a review of two json files can actually catch.

{
  "name": "ValueE",
  "kind": "measurement",
  "description": "Measurement used as the input quantity of AxisA",
  "datatype": "uint16",
  "unit": "Hz",
  "conversion": { "kind": "linear", "factor": 0.25 },
  "limits": { "min": 0, "max": 8000 },
  "init": 0,
  "volatile": true
}

The common attributes

Every kind of data object carries the attributes below. The kind specific ones - dimensions, size, input, axis, x_axis, y_axis - are described in the sections that follow, and a key that does not belong to the kind that was selected is rejected rather than ignored.

key

default

meaning

name

required

The identifier of the object, used unchanged as the c identifier and as the a2l name. Letters, digits and underscore, not starting with a digit, at most 128 characters - the ASAP2 1.6.1 limit, which is tighter than what a c compiler would accept and is therefore the one enforced.

kind

required

What sort of object this is: measurement, parameter, value_block, axis, curve or map. It is stated rather than inferred, and it decides which of the kind specific keys the definition may carry.

datatype

one of the two

The storage the target uses: boolean, uint8, sint8, uint16, sint16, uint32, sint32, uint64, sint64, float32 or float64. Exactly one of datatype and typename is stated.

typename

one of the two

The name of a declared type, stated instead of datatype: a scalar type fixes what the value means, a structure makes this a structured variable. An external type cannot be named here (type-kind): only a structure member may name one, because DDD knows neither its layout nor its meaning.

volatile

required

Whether the generated declaration carries the c qualifier of the same name, which forbids the compiler to assume it already knows the value. A measurement needs it when something outside the reading component writes the variable - an interrupt, a second core, a peripheral - and calibration data needs it when a calibration tool is to change the value in a running ecu, because without it the compiler is entitled to fold the initialiser into the code that reads it - within one translation unit at every optimisation level, -O0 included, and across them under -flto. The whole account, with the compiler output it was measured from, is in Generated artefacts. There is no default because there is no answer DDD could derive, and because the two answers have different costs that only the project can weigh: true keeps a value tunable while it runs and, on a typical toolchain, moves a calibration object out of read only memory, since the compiler treats a volatile access as a side effect and stops putting the object in .rodata; false leaves it in flash and lets the optimiser keep the value in a register. DDD states no preference and reports nothing about the choice - it renders what the description says, and generated artefacts follows what that costs into the image.

description

""

Free text. It is offered to the c templates as the text of the comment above the generated declaration - what surrounds it is theirs to decide - and it is the long identifier of the a2l object, so it is the sentence a calibration engineer reads next to the value. An object without one gets its own name as the a2l long identifier, which is legal and useless.

unit

""

The physical unit, as free text: Hz, degC, %. It is shown in the c comment in brackets, and it is part of the a2l COMPU_METHOD. Components sharing a variable have to agree on it, because two components using the same variable in different units is the failure that compiles and links and is wrong by a constant factor.

conversion

required beside datatype

How the stored number maps to the physical one; see Conversions. Required although the identity would be derivable - that it is derivable is why it is asked for - and stated by the declared type instead when typename names one.

limits

derived

An object with min and max, in physical units. When it is left out, DDD derives the limits from the datatype and the conversion, so the a2l always carries a range.

section

none

The linker section the object is placed in, named in the project’s sections file. A storage key like init: the producer states it, and an object without one goes wherever the toolchain’s defaults put it.

init

null

The initial value, in raw units. null means no initialiser is written at all and the startup code zero-initialises the object.

a2l

export

Per object settings for the a2l backend, and nothing else reads them.

Five of those are required, and none of them is something DDD should invent a value for, so the simplest possible definition still says five things:

{ "name": "Counter", "kind": "measurement", "datatype": "uint32", "conversion": {}, "volatile": false }

Note

limits and init are on different sides of the conversion, and that is not an inconsistency but the only arrangement that works. Limits are what a calibration engineer types into a tool, so they are physical; the initial value is what the compiler writes into the image, so it is raw. With a factor of 0.1 a variable whose limits are -40 and 150 degC is initialised with -400, which is -40.0 degC.

Datatypes

The datatype names are DDD’s own, not c’s, because a description file is not a c file and the same description generates a2l as well. Each one maps to a c type and to an ASAP2 type:

datatype

bytes

c

a2l

raw range

boolean

1

bool

UBYTE

0 .. 1

uint8

1

uint8_t

UBYTE

0 .. 255

sint8

1

int8_t

SBYTE

-128 .. 127

uint16

2

uint16_t

UWORD

0 .. 65535

sint16

2

int16_t

SWORD

-32768 .. 32767

uint32

4

uint32_t

ULONG

0 .. 4294967295

sint32

4

int32_t

SLONG

-2147483648 .. 2147483647

uint64

8

uint64_t

A_UINT64

0 .. 18446744073709551615

sint64

8

int64_t

A_INT64

-9223372036854775808 .. 9223372036854775807

float32

4

float

FLOAT32_IEEE

the IEEE 754 single range

float64

8

double

FLOAT64_IEEE

the IEEE 754 double range

The c spellings come from <stdint.h> and <stdbool.h>. The model tells the templates which of the two a project actually needs - model.needs_stdbool is false for a project without a boolean - so the example templates include neither header for nothing. Literals are written with the suffix the type asks for, so the generated code survives -Wconversion: 0U for the unsigned types, 1.5F for float32, 18446744073709551615ULL for uint64.

Names, and what a name may not be

The name is used unchanged in the generated c, which means the description file can produce code that does not compile, and DDD checks for that rather than letting the compiler explain it in a generated file nobody wants to read. A name that is a c keyword, or that <stdint.h> or <stdbool.h> already declare, is refused:

$ ddd check reserved.ddd.json
reserved.ddd.json#component.interface[0].definition.name: error[reserved-identifier]: variable name 'signed' is reserved by the c language
reserved.ddd.json#component.interface[1].definition.name: error[reserved-identifier]: variable name 'uint8_t' is reserved by the c language
2 errors

So is a name that collides with something else DDD itself generates - an enumerator of an enum conversion lives in the same c namespace as a variable:

$ ddd check enumcoll.ddd.json
enumcoll.ddd.json#component.interface[0].definition.name: error[name-collision]: 'STATE_OFF' is declared as a variable and is also an enumerator of enum 'S_t'; both become the same c identifier
    note: enumcoll.ddd.json#component.interface[1].definition.conversion: enumerator declared here
1 error

Two names differing only in case compile perfectly well and are merely a warning, because they are legal and occasionally intended - but they are also the classic way for a value to be read from the wrong variable for a year:

$ ddd check similar.ddd.json
similar.ddd.json#component.interface[1].definition.name: warning[name-similar]: 'valuea' and 'ValueA' differ only in upper/lower case
    note: similar.ddd.json#component.interface[0].definition: other variable
1 warning

Initial values

init is a raw value: a scalar, or a nested list matching the shape of the object. Leaving it out is not the same as writing 0. With init absent no initialiser is generated at all, and the object lands in the zero-initialised section that the startup code clears, which for a large array is the difference between a few bytes of image and a few kilobytes:

/** Floating point measurement without a conversion [degC] */
float ValueC;
/** Component local measurement, kept out of the a2l */
uint16_t ValueD[8];

The value is raw rather than physical because the generated c carries it verbatim, and under a linear conversion most physical values are the exact image of no raw count, so a physical spelling would either round silently or refuse ordinary values. The reading in the other direction is always defined, and the tool states it wherever a person rather than a compiler is the audience: the editor hover and the table of ddd list show the physical value - or the enumerator name - beside the raw one.

A scalar given for an array shaped object initialises every element, which is what makes a table of a hundred identical starting values one character long instead of a hundred. The demo uses it for CurveB, whose "init": 200 covers all six points of the axis it lies over:

/** Second calibratable curve over the same shared axis [%] (calibration curve over AxisA) */
const uint8_t CurveB[6] = { 200U, 200U, 200U, 200U, 200U, 200U };

Everything about an initial value is checked against the object it belongs to. A value outside the raw range of the datatype, a fractional value in an integer object, and a nested list of the wrong shape are all errors, each naming what it actually is:

$ ddd check ranges.ddd.json
ranges.ddd.json#component.interface[0].definition.init: error[init-invalid]: init value 300 does not fit into uint8 (0 .. 255)
ranges.ddd.json#component.interface[2].definition.init: error[init-invalid]: init value 1.5 is written as a fractional number, but 'Fractional' has the integer datatype uint8
ranges.ddd.json#component.interface[1].definition.limits: warning[limits-out-of-range]: limits [0, 200] exceed the range [0, 127.5] that uint8 can represent with this conversion
2 errors, 1 warning

The warning in that transcript is the same idea applied to limits: a uint8 scaled by 0.5 reaches 127.5, so a maximum of 200 is a promise the storage cannot keep. It is a warning rather than an error because limits are a statement of intent about the data and never reach the compiler - but it is the finding that stops a calibration engineer from entering a value the software will silently wrap. Limits are also checked for being the right way round: min greater than max is refused when the file is read.

$ ddd check misc.ddd.json
misc.ddd.json#component.interface[0].definition.limits: error[schema]: Value error, min (10) is greater than max (5) (got: {'min': 10, 'max': 5})
1 error

The a2l block

The a2l object holds what one object asks of the a2l backend. Nothing else in DDD reads it, and a project generating no a2l can ignore it entirely.

key

default

meaning

export

unstated

Set to false to keep the object out of the a2l. The c code is generated as usual; only the calibration tool never sees it. The key has three states rather than two: unstated, which is what an object gets when it says nothing and means the same as true, and the two spelled out answers. See Who asks for an export for why the difference matters when several components declare the object.

format

null

The a2l FORMAT string: %, the total display width, a dot, the number of decimal places, as in "%8.3". It overrides the display format the conversion would otherwise imply.

display_identifier

null

An alternative name for the calibration tool to show, for a variable whose c identifier is unhelpful or too long to read in a measurement list.

{
  "name": "ValueLong",
  "kind": "measurement",
  "description": "Long name shown shorter in the tool",
  "datatype": "uint16",
  "unit": "Hz",
  "conversion": { "factor": 0.25 },
  "a2l": { "format": "%8.3", "display_identifier": "ValLong" },
  "volatile": false
}
/begin MEASUREMENT ValueLong "Long name shown shorter in the tool"
  UWORD CM_LIN_HZ 0 0 0 16383.75
  ECU_ADDRESS 0x00000000
  SYMBOL_LINK "ValueLong" 0
  FORMAT "%8.3"
  DISPLAY_IDENTIFIER ValLong
/end MEASUREMENT

export: false is the right setting for a component internal scratch variable that would only clutter the measurement list. The demo uses it twice, on ValueD and ValueK; both reach the c templates like every other object, and neither appears anywhere in DemoDevice.a2l, not even in the GROUP of the component that owns them.

Who asks for an export

export is the one a2l setting that is not the producer’s alone. Which signals a calibration engineer needs to see is not a property of whoever happens to write the variable: a component reading a value out of a library it does not own has as good a claim to measuring it as the library has to hiding it.

The stated answers are therefore combined rather than ranked. The object reaches the a2l if any declaration states true, and is left out only when every declaration that says anything says false; a declaration that stays silent is not a vote either way, and an object nobody says anything about is exported. Two consumers can never conflict over it, so there is no finding for a disagreement, and the verdict does not depend on which components an image happens to link.

This is also what makes the key safe to leave out. A dictionary written before the key existed, or handed over by a third party with no a2l block at all, exports its objects the way it always did rather than quietly emptying the a2l file.

Warning

export: false on an axis that a curve or a map refers to is overruled. The CHARACTERISTIC of the curve carries an AXIS_PTS_REF naming that axis, and an AXIS_PTS_REF without the AXIS_PTS it points at would not be a valid a2l file - the calibration tool would refuse the whole file, not just the one object. A referenced axis is therefore always exported.

The format string is constrained rather than passed through, and a value that does not match is refused when the file is read:

$ ddd check badfmt.ddd.json
badfmt.ddd.json#component.interface[0].definition.a2l.format: error[schema]: String should match pattern '^%\d*\.\d+$' (got: '%8')
1 error

The reason is that the value ends up inside a quoted a2l string literal. A quote or a backslash in it would unbalance that literal and no calibration tool would parse the file at all, which would cost a whole delivery for one typo in one description.

Kinds of data object

kind decides what the object is, and with it how it is stored and what the a2l calls it. The division that matters is between the one kind the software writes and the five it does not: a measurement is an online value that the software produces and the calibration tool only observes, while everything else is calibration data - the software never writes it, so it is generated const, and a calibration tool changes it through its address.

Whether it also ends up in read only memory is the other question, and the one volatile answers. const says who writes the object from inside the software, volatile says whether anything writes it from outside, and the two are independent: a parameter that a tool is meant to tune while the ecu runs is const volatile, and a parameter that is only ever changed between builds is plain const and stays in flash.

kind

extra keys

c

a2l

measurement

dimensions

writable variable

MEASUREMENT

parameter

const scalar, const volatile where stated

CHARACTERISTIC ... VALUE

value_block

dimensions (required)

const array, const volatile where stated

CHARACTERISTIC ... VAL_BLK

axis

size (required), input

const array [size], const volatile where stated

AXIS_PTS

curve

axis (required)

const array [size of the axis], const volatile where stated

CHARACTERISTIC ... CURVE

map

x_axis, y_axis (required)

const array [size of y][size of x], const volatile where stated

CHARACTERISTIC ... MAP

The extra keys are what the kind adds to the common attributes, which is why volatile is not among them: every kind states it, and where the answer is true the c column gains that word - a measurement becomes volatile uint16_t Speed; and a parameter const volatile uint16_t Gain = 3U;.

Every example below is taken from examples/demo/, and every generated fragment is what ddd generate examples/demo/demo.ddd.json -o build/gen -t examples/templates actually writes - the c ones as the example templates render them, the a2l ones as DDD writes them.

measurement

A measurement is a value the software computes and writes, declared with "kind": "measurement" like every other kind. One key is its own: dimensions, a list of array dimensions that is empty for a scalar - each an integer of at least 1, or the name of a declared constant of the project, mixed freely, so [4] and ["PRESSURE_CELLS", 4] are both shapes. The size of an axis follows the same rule. It is also the one kind that is not generated const, so the volatile every definition states is the whole of its qualifier - ValueB says true, which is the answer for a value written by an interrupt or by another task, and keeps the compiler from caching it in a register.

{
  "scope": "output",
  "definition": {
    "name": "ValueB",
    "kind": "measurement",
    "description": "Array measurement with four elements",
    "datatype": "uint16",
    "unit": "V",
    "dimensions": [4],
    "conversion": { "factor": 0.001 },
    "init": 0,
    "volatile": true
  }
}
/** Array measurement with four elements [V] */
volatile uint16_t ValueB[4] = { 0U, 0U, 0U, 0U };
/begin MEASUREMENT ValueB "Array measurement with four elements"
  UWORD CM_LIN_V 0 0 0 65.535
  ECU_ADDRESS 0x00000000
  SYMBOL_LINK "ValueB" 0
  MATRIX_DIM 4 1 1
/end MEASUREMENT

ValueB gives no limits, so the pair 0 65.535 in the a2l is derived: a uint16 reaches 65535 and the factor is 0.001. MATRIX_DIM lists the dimensions in the a2l’s x, y, z order, which is the reverse of the c subscripts - a uint8_t M3[2][3][4] becomes MATRIX_DIM 4 3 2. ASAP2 1.6.1 carries three dimensions there; an object with more is generated anyway, with a a2l-unrepresentable warning saying that only a 1.7 reader will understand the extra ones.

parameter

A parameter is a single calibratable constant: one value the software reads and the calibration tool writes. It has no extra keys at all, since a scalar has no shape - but like every other kind it states volatile, and a constant very much has a volatility. Volatility is not about whether the software ever assigns to the object; it is about whether the value can change while the program runs, and a calibratable constant exists precisely so that somebody outside the program can change it. Say false and the compiler is free to fold the initialiser into the code that reads the value wherever it can see it - at -O0 as well, since that is the front end substituting a value it knows rather than the optimiser at work - so a tool writing a new value through the object’s address changes memory the software has stopped reading. Say true and every read is a load from the address, at the price of the object leaving read only memory.

{
  "scope": "local",
  "definition": {
    "kind": "parameter",
    "name": "ParameterA",
    "description": "Single calibratable constant",
    "datatype": "uint16",
    "unit": "Hz",
    "conversion": { "factor": 0.25 },
    "limits": { "min": 500, "max": 1500 },
    "init": 3200,
    "volatile": false
  }
}
/** Single calibratable constant [Hz] (calibration parameter) */
const uint16_t ParameterA = 3200U;
/begin CHARACTERISTIC ParameterA "Single calibratable constant"
  VALUE 0x00000000 RL_VALUES_UWORD 0 CM_LIN_HZ 500 1500
  SYMBOL_LINK "ParameterA" 0
/end CHARACTERISTIC

The raw init of 3200 is 800 Hz with a factor of 0.25, comfortably inside the physical limits of 500 and 1500 that the calibration tool will enforce. local is the usual scope for a parameter, since data that only tunes one component has no business being visible to the others.

Every calibration object of the demo says "volatile": false, which is why all of them are plain const here. The same definition with true differs by one word in the c and by nothing at all in the a2l, since a calibration tool is told the same address either way - the difference is whether the code that reads the value goes back to that address:

/** Single calibratable constant [Hz] (calibration parameter) */
const volatile uint16_t ParameterA = 3200U;

value_block

A value block is an array of calibratable constants that is not indexed by an axis - a bit mask table, a set of coefficients, a lookup with an index the software computes itself. dimensions is required, since an array with no shape is a contradiction; a dimension may name a declared constant here exactly as on a measurement.

{
  "scope": "local",
  "definition": {
    "kind": "value_block",
    "name": "BlockA",
    "description": "Calibratable array of constants",
    "datatype": "uint8",
    "conversion": { "kind": "identity" },
    "dimensions": [8],
    "init": [0, 12, 28, 52, 84, 124, 180, 255],
    "volatile": false
  }
}
/** Calibratable array of constants (calibration value block) */
const uint8_t BlockA[8] = { 0U, 12U, 28U, 52U, 84U, 124U, 180U, 255U };
/begin CHARACTERISTIC BlockA "Calibratable array of constants"
  VAL_BLK 0x00000000 RL_VALUES_UBYTE 0 NO_COMPU_METHOD 0 255
  SYMBOL_LINK "BlockA" 0
  MATRIX_DIM 8 1 1
/end CHARACTERISTIC

dimensions may have more than one entry, and the c declaration then has one subscript per entry, in the order they are written: "dimensions": [2, 3] gives const uint8_t Block2D[2][3] and a MATRIX_DIM 3 2 1.

axis

An axis is the set of break points a curve or a map is interpolated over. It exists as an object of its own rather than as a property of the curve, and that is the single most useful thing about the way DDD models tables: several curves and maps over the same break points store them once, and a calibration engineer who moves a break point moves it for all of them. That is what the a2l calls a COM_AXIS, a common axis.

size is required and gives the number of points. input optionally names the measurement that indexes the axis - the physical quantity the tool should show along it. It has to be a measurement, and it has to exist.

{
  "scope": "output",
  "definition": {
    "kind": "axis",
    "name": "AxisA",
    "description": "Shared axis indexed by ValueE",
    "datatype": "uint16",
    "unit": "Hz",
    "conversion": { "factor": 0.25 },
    "limits": { "min": 0, "max": 8000 },
    "size": 6,
    "input": "ValueE",
    "init": [0, 3200, 6400, 12800, 19200, 32000],
    "volatile": false
  }
}
/** Shared axis indexed by ValueE [Hz] (calibration axis, 6 points) */
const uint16_t AxisA[6] = { 0U, 3200U, 6400U, 12800U, 19200U, 32000U };
/begin AXIS_PTS AxisA "Shared axis indexed by ValueE"
  0x00000000 ValueE RL_AXIS_UWORD 0 CM_LIN_HZ 6 0 8000
  SYMBOL_LINK "AxisA" 0
/end AXIS_PTS

The scope of an axis follows the same rule as everything else. AxisA is an output of Controller because UserInterface also puts a curve over it and therefore declares it as an input; AxisB, which only Controller uses, is local. Omitting input is allowed and produces NO_INPUT_QUANTITY in its place - honest, and less useful to whoever opens the file.

curve

A curve is a one dimensional calibratable table laid over one axis. It names that axis with axis and gives no shape of its own:

{
  "scope": "local",
  "definition": {
    "kind": "curve",
    "name": "CurveA",
    "description": "Calibratable curve over AxisA",
    "datatype": "uint16",
    "unit": "ms",
    "conversion": { "factor": 0.01 },
    "axis": "AxisA",
    "init": [1200, 900, 800, 750, 700, 650],
    "volatile": false
  }
}
/** Calibratable curve over AxisA [ms] (calibration curve over AxisA) */
const uint16_t CurveA[6] = { 1200U, 900U, 800U, 750U, 700U, 650U };
/begin CHARACTERISTIC CurveA "Calibratable curve over AxisA"
  CURVE 0x00000000 RL_VALUES_UWORD 0 CM_LIN_MS 0 655.35
  SYMBOL_LINK "CurveA" 0
  /begin AXIS_DESCR
    COM_AXIS ValueE CM_LIN_HZ 6 0 8000
    AXIS_PTS_REF AxisA
  /end AXIS_DESCR
/end CHARACTERISTIC

The [6] in the c declaration was never written down anywhere: DDD takes it from AxisA, whose size is 6. Repeating the size on the curve would be a second place for it to be wrong, and a table with one more entry than its axis is the kind of mistake that reads correctly and interpolates rubbish. Since the shape is derived, the init data is checked against it:

$ ddd check badshape.ddd.json
badshape.ddd.json#component.interface[1].definition.init: error[init-invalid]: 'CurveS' has the shape [3] given by its axes: init has 2 elements, expected 3
1 error

The AXIS_DESCR block is where the sharing becomes visible. COM_AXIS says that the break points are not stored inside this characteristic but somewhere else, and AXIS_PTS_REF says where. CurveB in the demo carries exactly the same block, pointing at the same AxisA, and the six break points exist once in the image.

map

A map is a two dimensional calibratable table over two axes, named with x_axis and y_axis. Like a curve it declares no shape.

{
  "scope": "local",
  "definition": {
    "kind": "map",
    "name": "MapA",
    "description": "Calibratable map over AxisA and AxisB",
    "datatype": "sint8",
    "unit": "%",
    "conversion": { "factor": 0.5 },
    "x_axis": "AxisA",
    "y_axis": "AxisB",
    "init": [
      [20, 24, 28, 30, 32, 30],
      [18, 22, 26, 28, 30, 28],
      [12, 16, 20, 22, 24, 22],
      [6, 10, 14, 16, 18, 16]
    ],
    "volatile": false
  }
}

AxisA has 6 points and AxisB has 4, and the generated array is [4][6] - the size of y first, then the size of x. That is the row major layout, the one a2l calls ROW_DIR: a row of the c array is a row of the table, so a whole row of y is contiguous in memory and the interpolation walks it the way the hardware likes. Written in the same order in the json, the init data reads exactly like the table it is:

/** Calibratable map over AxisA and AxisB [%] (calibration map over AxisA and AxisB) */
const int8_t MapA[4][6] = {
    { 20, 24, 28, 30, 32, 30 },
    { 18, 22, 26, 28, 30, 28 },
    { 12, 16, 20, 22, 24, 22 },
    { 6, 10, 14, 16, 18, 16 }
};
/begin CHARACTERISTIC MapA "Calibratable map over AxisA and AxisB"
  MAP 0x00000000 RL_VALUES_SBYTE 0 CM_LIN_PCT -64 63.5
  SYMBOL_LINK "MapA" 0
  /begin AXIS_DESCR
    COM_AXIS ValueE CM_LIN_HZ 6 0 8000
    AXIS_PTS_REF AxisA
  /end AXIS_DESCR
  /begin AXIS_DESCR
    COM_AXIS ValueA CM_LIN_PCT 4 0 100
    AXIS_PTS_REF AxisB
  /end AXIS_DESCR
/end CHARACTERISTIC

The two AXIS_DESCR blocks are in x, y order, which is the a2l’s convention and the opposite of the c subscripts - one more reason not to have written the shape by hand. The limits -64 63.5 are derived, as MapA gives none: an sint8 scaled by 0.5 covers -64 to 63.5.

References between objects

axis, x_axis, y_axis and input name other objects, and the object they name may be declared by any component of the project - which is what makes a shared axis possible in the first place. Both halves of such a reference are checked: that the name exists at all, and that it points at the right kind of thing.

$ ddd check refs.ddd.json
refs.ddd.json#component.interface[1].definition.axis: error[unknown-reference]: curve 'CurveMissing' refers to 'NoSuchAxis' as its axis, but no component declares 'NoSuchAxis'
refs.ddd.json#component.interface[2].definition.axis: error[reference-kind]: the axis of curve 'CurveWrong' must be of kind 'axis', but 'NotAnAxis' is of kind 'parameter'
2 errors
$ ddd check axinput.ddd.json
axinput.ddd.json#component.interface[1].definition.input: error[reference-kind]: the input of axis 'Ax1' must be of kind 'measurement', but 'NotAMeas' is of kind 'parameter'
axinput.ddd.json#component.interface[2].definition.input: error[unknown-reference]: axis 'Ax2' refers to 'Nowhere' as its input, but no component declares 'Nowhere'
2 errors

Getting the kind wrong is the more interesting of the two, because it is the one that would otherwise produce an a2l a calibration tool accepts and then misreads: an AXIS_PTS_REF pointing at a CHARACTERISTIC is not break points, it is a table being read as if it were.