ChimeraTK-DeviceAccess 03.31.00
Loading...
Searching...
No Matches
JMAP map file format

This page describes the JMAP file format, which is a JSON based file format used to describe the register map of numeric-addressed devices.

It has been introduced to replace the classic plain-text map file format and to provide additional features which would not fit into the old format. The numeric-addressed backends (e.g. PCIe, XDMA, UIO, and the Dummy backend) can be told to use a JMAP file by passing the map parameter in the ChimeraTK device descriptor:

(pci:pciedevs5?map=mydevice.jmap)
(dummy?map=mydevice.jmap)

Commonly the register maps are generated by firmware tools like the FWK firmware framework, but they can just as well be written by hand in case no such generation tool exists.

Fundamental concepts

A JMAP file is a single JSON object with the following top-level keys:

  • mapFormatVersion - a version string of the file format (currently "1.0").
  • metadata - an object holding arbitrary key-value pairs describing the firmware (see Metadata).
  • interruptHandler - a description of the interrupt controllers (see Interrupts).
  • addressSpace - the dictionary of registers and modules making up the register map (each key is a register/module name).

The heart of the file is the addressSpace object. Each key is the name of a register, or of a module which groups other registers and modules below it; the object stored under a key describes that register/module. Registers are addressed by a hierarchical path, which is built from the addressSpace / children dictionary keys (the names of the modules and registers), separated by '/' or '.', e.g. APP/STATUS or APP.STATUS ('/' and '.' have synonymous meaning and can be exchanged arbitrarily). This path is used to access the register through register accessors, as described in Multi Value Registers (1D Register Accessors) and 2D Register Accessors.

Comments

JSON does not support comments natively. By convention, entries whose key starts with an underscore are treated as comments and ignored by the parser:

{
  "_": "This is a comment and is ignored",
  "_comment": "This is another comment and is ignored as well",
  "mapFormatVersion": "1.0",
  "metadata": {},
  "interruptHandler": {},
  "addressSpace": {}
}

Any unknown key not starting with an underscore is reserved for future use in later map format versions and hence should not be used as comments.

Register entry

A register is an entry carrying an address (or inheriting one from a parent module, see Modules and hierarchical names). This section describes the individual members of a register entry.

Address

The minimal register entry is stored under its name and needs only an address:

"addressSpace": {
  "SomeTopLevelRegister": {
    "address": { "type": "IO", "channel": 0, "offset": 32 }
  }
}

The address object has the following members:

  • type - either "IO" (the default, an address on the I/O bus) or "DMA". A DMA address is typically used for bulk data acquisition registers.
  • channel - the bus channel (bar) number, defaults to 0. For DMA addresses the channel is mapped to a device bar by adding an offset of 13, mirroring the convention of the old map file format in which DMA bars started at an offset of 13 (so a DMA channel of 0 corresponds to device bar 13).
  • offset - the byte offset of the register inside the channel, given either as a plain decimal number or as a hexadecimal string.

An address may also be omitted and inherited from a parent module; this is described together with modules in Modules and hierarchical names.

Access

The access member defines how a register may be accessed:

  • "RW" - read/write access (the default).
  • "RO" - read-only access, e.g. for status registers.
  • "WO" - write-only access, values cannot be read back.

Example:

"addressSpace": {
  "STATUS": {
    "access": "RO",
    "address": { "channel": 2, "offset": "0x8000" }
  }
}

Number of elements and bytes per element

By default a register holds a single element of 4 bytes. A register holding more than one element is declared by giving the number of elements and the size of each element:

  • numberOfElements - the number of elements, defaults to 1.
  • bytesPerElement - the size of a single element in bytes, defaults to 4.

Example:

"addressSpace": {
  "SomeTable": {
    "access": "WO",
    "numberOfElements": 16384,
    "bytesPerElement": 2,
    "address": { "channel": 0, "offset": 2048 }
  }
}

Representation

The representation object describes how the raw data is interpreted by the application layer. The following members and values are supported:

  • type - one of "fixedPoint" (the default), "IEEE754" (single or double precision floating point, see below), "string" (an ASCII string) or "void" (used for pure interrupt sources, see Interrupts).
  • width - the width of the value in bits. For fixedPoint it may be smaller than the element size, in which case the remaining bits are ignored. Default: 32
  • fractionalBits - the number of fractional bits of a fixedPoint value; may be negative to describe integer values with a scaling factor. Must be 0 unless type is fixedPoint. Default: 0
  • isSigned - whether the value is interpreted as signed. Must be true for IEEE754 types. Default: false
  • bitShift - the bit offset of the value within the element. Default: 0

If the entire representation is omitted, it defaults to a 32-bit unsigned integer.

A single-precision floating point register:

"addressSpace": {
  "ExampleRegister": {
    "address": { "channel": 0, "offset": 32 },
    "representation": {
      "type": "fixedPoint",
      "width": 26,
      "fractionalBits": 12,
      "isSigned": true
    }
  }
}

Description and engineering unit

  • description - a free-form human-readable text describing the purpose of the register.
  • engineeringUnit - the physical unit of the register value, e.g. "mV".

Both are purely informational and may be omitted. Example:

"addressSpace": {
  "SomeTopLevelRegister": {
    "engineeringUnit": "mV",
    "description": "This is an example register",
    "address": { "channel": 0, "offset": 32 }
  }
}

Conditional register selection

It is possible to define sets of registers with different semantics or even layout that can be selected between with a dedicated selector register and an expected value of that register that determines the active set.

All alternative registers will be present in the catalog.

  • register Path to a register that determines which register set
  • value The value of register that enables this register
"addressSpace": {
  "value": {
    "address": { "channel": 0, "offset": 1246 },
    "representation": { "type": "fixedPoint", "width": 32 },
    "selectedBy": {
      "register": "APP.OUTPUT_SELECT",
      "value": 0
    },
  },
  "valueMean": {
    "address": { "channel": 0,"offset": 1246 },
    "representation": { "type": "IEEE754", "width": 32 },
    "selectedBy": {
      "register": "APP.OUTPUT_SELECT",
      "value": 1
    },
  },
}

Module-Level Selection Inheritance

It is possible to declare a selectedBy on a module or a parent register with children. This selection criterion is inherited by all descendant registers and channels that do not declare their own selectedBy. The nearest ancestor's selectedBy takes precedence.

Example:

"addressSpace": {
  "MODULE": {
    "selectedBy": { "register": "APP.OUTPUT_SELECT", "value": 3 },
    "children": {
      "REG_A": {
        "address": { "channel": 0, "offset": 32 },          // Inherits MODULE's selectedBy
        "representation": { "type": "fixedPoint", "width": 32 }
      },
      "REG_B": {                                            // Own selectedBy overrides
        "selectedBy": { "register": "CTRL.MODE", "value": 9 },
        "address": { "channel": 0, "offset": 36 },
        "representation": { "type": "fixedPoint", "width": 32 }
      }
    }
  }
}

Modules and hierarchical names

Modules

A register/module in the addressSpace without an address is a module: it groups nested registers and modules below it by providing a children dictionary keyed by the children's names, and contributes its own name (its addressSpace / children key) to the hierarchical path of everything beneath it. A module is not a register and carries no data of its own. Example (from simpleJsonFile.jmap):

"addressSpace": {
  "APP": {
    "children": {
      "STATUS": {
        "access": "RO",
        "address": { "type": "IO", "channel": 2, "offset": "0x8000" }
      },
      "CONTROL": {
        "access": "RW",
        "address": { "type": "IO", "channel": 2, "offset": "0x8004" }
      }
    }
  }
}

This yields the registers APP.STATUS and APP.CONTROL.

Note: a module simply becomes a register by adding an address. This can be used to describe a hierarchical address space by describing the raw address space e.g. for a firmware module at the higher level of the hierarchy, while adding the detailed registers in the firmware module's address space as children. Keep in mind that there is no technical necessity that all children have to have addresses inside the address space of their parents (although it is certainly a good convention). Example:

"addressSpace": {
  "APP": {
    "address": { "type": "IO", "channel": 2, "offset": "0x8000" },
    "numberOfElements": 2,
    "children": {
      "STATUS": {
        "access": "RO",
        "address": { "type": "IO", "channel": 2, "offset": "0x8000" },
        "representation": { "width": 3 }
      },
      "CONTROL": {
        "access": "RW",
        "address": { "type": "IO", "channel": 2, "offset": "0x8004" },
        "representation": { "width": 5 }
      }
    }
  }
}

This yields the same registers APP/STATUS and APP.CONTROL as before, but in addition a register APP for the raw area containing both registers.

Address inheritance

A child entry does not have to carry an address of its own: it inherits the address of the nearest ancestor that does. The inheritance works per field of the address entry, so e.g. only the type and channel may be inherited while the offset is overridden. Example (identical outcome to the last example above):

"addressSpace": {
  "APP": {
    "address": { "type": "IO", "channel": 2, "offset": "0x8000" },
    "numberOfElements": 2,
    "children": {
      "STATUS": {
        "access": "RO",
        "representation": { "width": 3 }
      },
      "CONTROL": {
        "access": "RW",
        "address": { "offset": "0x8004" },
        "representation": { "width": 5 }
      }
    }
  }
}

Advanced features

Interrupts

The interruptHandler top-level object describes interrupt controllers which are used to multiplex multiple interrupt sources in firmware into the same hardware interrupt request. Each key is an interrupt id and each entry carries a controller with its configuration and optionally nested subhandler entries. The content of the controller entry is backend-specific and extensible: the backend defines the controller type (e.g. INTC) and the data structure it carries, and the parser passes the entry on to the metadata verbatim without interpreting it:

"interruptHandler": {
  "3": {
    "INTC": { "path": "MY_INTC", "options": ["MER"], "version": 1 },
    "subhandler": {
      "0": {
        "INTC": { "path": "MY_INTC.SUB0", "options": [] }
      }
    }
  }
}

The JSON schema distributed with this project (schemas/jmap.schema.json) is advisory only and meant for interactive validation in editors; it is not enforced by the parser.

Registers can be triggered by an interrupt. The "triggeredByInterrupt" field contains the full interrupt id, which is an array of all ids and sub-ids as described in the interruptHandler object. Such registers are always read-only, hence the "triggeredByInterrupt" field is mutually exclusive with the "access" field.

Plain interrupts have a representation of type "void" and must not have an address. Example:

"addressSpace": {
  "PRIMARY_INTERRUPT_3": {
    "triggeredByInterrupt": [3],
    "representation": { "type": "void" }
  },
  "SUB_INTERRUPT_3_0_2": {
    "triggeredByInterrupt": [3,0,2],
    "representation": { "type": "void" }
  }
}

PRIMARY_INTERRUPT_3 describes the primary interrupt (IRQ) 3, while SUB_INTERRUPT_3_0_2 will be the interrupt number 2 of the subhandler 0 inside the handler for the primary interrupt 3 (i.e. interrupt 3:0:2).

When interrupts shall be used to trigger data read-out, they can be added to normal registers:

"addressSpace": {
  "TRIGGERED_DATA": {
    "address": { "offset": "0x8100" },
    "numberOfElements": 2048,
    "triggeredByInterrupt": [3,0,2],
    "representation": { "width": 24, "fractionalBits": 10, "isSigned": true }
  }
}

This will create a push-type register (supporting ChimeraTK::AccessMode::wait_for_new_data) that will receive new values when the interrupt 3:0:2 arrives by polling the data from the specified address. See Design: AsyncNDRegisterAccessor for more details.

2D multiplexed registers

A register holding several channels of time-multiplexed samples is declared by giving a flat channels entry together with numberOfElements and pitch:

  • numberOfElements - the number of samples (time slots) per channel.
  • pitch - the number of bytes between two samples of the same channel.
  • channels - the channels, given as a dictionary keyed by channel name. Each channel value has a byte offset relative to the register base address, a bytesPerElement and an optional representation. (Channel order in memory follows the offset, independent of the order of the dictionary keys.) An optional per-channel selectedBy selects a muxed channel (see Conditional channels).

Channels can have arbitrary names; single-word channels are simply indexed "0", "1", ... Example:

"addressSpace": {
  "DMA": {
    "address": { "type": "DMA", "offset": "0x0" },
    "numberOfElements": 16384,
    "pitch": 32,
    "channels": {
      "0": {
        "offset": 0,
        "bytesPerElement": 2,
        "representation": { "type": "fixedPoint", "width": 16 }
      },
      "1": {
        "offset": 2,
        "bytesPerElement": 2,
        "representation": { "type": "fixedPoint", "width": 12, "fractionalBits": 12 }
      }
    }
  }
}

Here DMA is a 2D register with two channels, each holding 16384 samples, with a pitch of 32 bytes between the samples of a channel (so there is space for more channels in this example).

Channel slices

For every channel, a read-only one-dimensional register slice is created automatically. The slice is named after the channel, e.g. DMA/0, DMA/1 in the example above. Each slice exposes the samples of that single channel as a one-dimensional array, reading them out of the 2D register with the correct stride. The channel's byte offset is folded into the slice address, so it can be addressed independently of the parent 2D register. Writing is deliberately not supported for these slices, since writing a single channel of a 2D register would require a read-modify-write cycle across the channels. If multiple slice accessors for the same 2D register shall be read simultaneously (without ChimeraTK::AccessMode::wait_for_new_data), it is highly recommended to add them all to the same ChimeraTK::TransferGroup to avoid transferring the full 2D data multiple times unnecessarily from the device.

Channel bit fields

A channel of a 2D register may declare a children dictionary of bit fields of its sample word, working like the top-level bit-field mechanism of a 1D register (see Bit fields). The children share the channel's byte offset and bytesPerElement as their word context. The channel itself may also be a single bit range by setting bitShift and width in its own representation.

For every channel a read-only slice <channel> is created exposing the parent sample word: byte-aligned, unshifted and not a bit range. For a channel with children, one read-only bit-range slice <channel>/<child> is created per child, extracting the child's bit range from every sample word. All these slices are read-only; writing is not supported. A child that is not a proper bit range of the channel word (spanning the whole word, or extending past it) is ignored. Reading a channel that is a bit range is done through its slice; the whole 2D register read is not supported for such channels.

Conditional channels

Similar to the Conditional register selection it is possible to define alternative sets of channels using the same semantics.

Example with two muxed layouts selected by DAQ.MUX_SEL:

"addressSpace": {
  "FD": {
    "address": { "type": "DMA", "offset": "0x81000" },
    "numberOfElements": 16384,
    "pitch": 64,
    "channels": {
      "AmplitudeCh0": {
        "selectedBy": { "register": "DAQ.MUX_SEL", "value": 0 },
        "bytesPerElement": 2,
        "offset": 0,
        "representation": { "type": "fixedPoint", "width": 16 }
      },
      "RawCh0": {
        "selectedBy": { "register": "DAQ.MUX_SEL", "value": 1 },
        "bytesPerElement": 4,
        "offset": 0,
        "representation": { "type": "fixedPoint", "width": 32 }
      },
      "RawCh1": {
        "selectedBy": { "register": "DAQ.MUX_SEL", "value": 1 },
        "bytesPerElement": 4,
        "offset": 4,
        "representation": { "type": "fixedPoint", "width": 32 }
      }
    }
  }
}
Single registers (scalar/1D)

The selectedBy feature works for single (scalar or 1D, non-2D) registers as well. In that case the selectedBy object is given on the register itself rather than per channel, and makes the whole register conditional: the register is only active while the selector register equals the given value. Example:

"addressSpace": {
  "SINGLE_MUXED": {
    "numberOfElements": 1,
    "selectedBy": { "register": "DAQ.MUX_SEL", "value": 0 },
    "address": { "channel": 0, "offset": 1246 },
    "bytesPerElement": 4,
    "representation": { "type": "fixedPoint", "width": 32 }
  }
}

Specifying selectedBy on a register that also has per-channel channels (a 2D register) is valid: it serves as the default selection for all channels that do not declare their own selectedBy (see Conditional channels and Module-Level Selection Inheritance). Per-channel selectedBy declarations always override this register-level default.

For a hardware multiplexer, several mutually exclusive scalar alternatives can be declared as separate registers that share the same address and differ only in their selectedBy value. Only one of them is active at a time, selected by the selector register:

"addressSpace": {
  "SINGLE_MUXED":     { "numberOfElements": 1, "selectedBy": { "register": "DAQ.MUX_SEL", "value": 0 }, "address": { "channel": 0, "offset": 1246 }, "bytesPerElement": 4, "representation": { "type": "fixedPoint", "width": 32 } },
  "SINGLE_MUXED_ALT": { "numberOfElements": 1, "selectedBy": { "register": "DAQ.MUX_SEL", "value": 1 }, "address": { "channel": 0, "offset": 1246 }, "bytesPerElement": 4, "representation": { "type": "fixedPoint", "width": 32 } }
}

Both alternatives map to the same address (1246); they differ only in the selector value, exactly like muxed channels inside a 2D register.

Double buffering

A register that is fed by double-buffered DMA hardware is marked with a doubleBuffering object. It contains:

  • secondaryBufferAddress - the address of the second (inactive) buffer, with the same members as a regular address.
  • enableRegister - path of the register used to enable double buffering.
  • readBufferRegister - path of the register reporting which buffer is currently active.
  • index - the index of the buffer pair within the control registers.

In addition to the main register, two read-only registers named NAME/BUF0 and NAME/BUF1 are created for the two buffers. Example:

"addressSpace": {
  "CTRL": {
    "triggeredByInterrupt": [3, 0, 1],
    "address": { "type": "DMA", "offset": "0x40000" },
    "doubleBuffering": {
      "secondaryBufferAddress": { "type": "DMA", "offset": "0x40200" },
      "enableRegister": "DAQ.DOUBLE_BUF.ENA",
      "readBufferRegister": "DAQ.DOUBLE_BUF.INACTIVE_BUF_ID",
      "index": 0
    },
    "numberOfElements": 16384,
    "pitch": 64,
    "channels": {
      "errorI": {
        "bytesPerElement": 2,
        "offset": 0,
        "representation": {
          "type": "fixedPoint",
          "width": 16,
          "fractionalBits": -2,
          "isSigned": true
        }
      }
    }
  }
}

Bit fields

A register can carry a children dictionary of entries which are bit fields of the register word. Each child key is the bit-field's name and its value a representation with bitShift and width; the children share the starting address of their parent register (see Address inheritance). The full word register itself is also accessible. Example:

"addressSpace": {
  "STATUS": {
    "access": "RO",
    "address": { "type": "IO", "channel": 2, "offset": "0x8000" },
    "bytesPerElement": 4,
    "children": {
      "ProbeLimiter": {
        "representation": { "width": 1 }
      },
      "ExternalInterlock": {
        "representation": { "bitShift": 1, "width": 1 }
      },
      "ErrorCounter": {
        "representation": {
          "bitShift": 2,
          "type": "fixedPoint",
          "width": 3,
          "fractionalBits": 0,
          "isSigned": false
        }
      }
    }
  }
}

Registers which share the starting address with a bit range and have a width smaller than their element size are automatically recognised as bit ranges as well. The same children mechanism is also available per channel of a 2D register (see Channel bit fields).

Metadata

The metadata object can carry any number of key-value pairs describing the firmware. Keys are required to be non-empty and must not start with an underscore. All entries are collected into the metadata catalogue and can be queried by application code. Example:

"metadata": {
  "mapfileRevision": "1.8.3-0-gdeadbeef",
  "someRandomEntry": "some random value"
}

See also