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.

id

none

The identity of the object: twelve lowercase base32 characters, written by ddd id --assign into the producing declaration and stated by no consumer (consumer-identity). It survives a rename, which is what lets a delivery comparison report one as a rename; a producing declaration without one is reported as missing-id, at info.

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 or a calibration tool - and calibration data needs it when a 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. Free text does not mean unchecked: where the project has a units file, even one declaring nothing, every spelling is checked against its vocabulary as well (unknown-unit). A string has none.

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. A string states none; its range is the byte range of its datatype.

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.

raster

the component’s

The measurement raster the producing component updates the object in, written into the a2l as the DAQ event a calibration tool preselects. Stated by the producer only (consumer-raster), on a measurement only (raster-kind), and defaulting to what the component declares for everything it produces.

init

null

The initial value, in raw units. null means no initialiser is written at all and the startup code zero-initialises the object. A string object may write it as text; see Conversions.

a2l

export

Per object settings for the a2l backend, and nothing else reads them. A string takes no format.

extensions

{}

Settings for each named plugin, keyed by plugin name and validated against its own model. Stated by the producing declaration only (consumer-extension).

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.

ddd list prints a resolved project as those attributes, one line per variable, the initial value raw with its physical reading beside it. ValueF of the shipped demo is exactly the case the note describes:

$ ddd list examples/demo/demo.ddd.json
VARIABLE          KIND         DATATYPE  UNIT  SHAPE   INIT               PRODUCER               CONSUMERS
...
ValueF            measurement  sint16    degC  -       -400 (= -40 degC)  Controller             UserInterface
...

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  # a component naming objects 'signed' and 'uint8_t'
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  # a variable named after an enumerator of the same project
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  # a component declaring both 'ValueA' and 'valuea'
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];

A string object may state its init as text instead - printable ASCII, shorter than the dimension so that the terminator fits - and the c carries it as a string literal; the conversions page shows one.

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. So is a magnitude a floating point datatype cannot hold from below: 1e-50 is inside the range a float32 states and past the precision it has, so the object would start at zero rather than at the value written, and the generated c says so out loud - a compiler refuses 1e-50F rather than quietly zeroing it.

$ ddd check ranges.ddd.json  # a component whose init values and limits do not fit
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  # a component whose limits are the wrong way round
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  # an a2l format string of '%8'
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 a calibration tool measures, 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. A tool can write a measurement through its address as well, and a measurement it is meant to poke is one of the cases volatile is for; what the division is about is which kind the software writes.

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 all 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. An array holds at most 10 000 000 elements, the product of its dimensions, and a map the same over its two axes, because the dictionary, the a2l and the generated code carry every one of them; a larger one is schema - at dimensions, at an axis’s size, or at the whole declaration for a map, which writes neither - and the declaration is dropped. A shape states at most 64 dimensions, a cap on the list rather than on its product, because the walks that expand a shape descend once per dimension; a longer one is schema at dimensions and the declaration is dropped too. A measurement 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. What input names has to be a plain measurement: an instance of a declared structure is of kind measurement and is still refused (reference-kind), because it reaches the a2l as one record per value-holding member and none of its own, so an axis indexed by it would name a record the file does not carry.

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  # a curve whose init has one element fewer than its axis
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 - unless that component declared it local, which keeps it to itself. Three things are checked about such a reference: that the name exists at all, that it points at the right kind of thing, and that it does not reach into another component’s local object, which is local-conflict.

An object whose reference names nothing, or one of the wrong kind, is left out of the dictionary along with what refers to it, whatever severity the finding is given, and incomplete-project says so when the finding is silenced - see consistency checks.

$ ddd check refs.ddd.json  # curves naming a missing axis and a parameter as their axis
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  # axes naming a parameter and a missing object as their input
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.