ChimeraTK-DeviceAccess 03.31.00
Loading...
Searching...
No Matches
JsonMapFileParser.cc
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Deutsches Elektronen-Synchrotron DESY, MSK, ChimeraTK Project <chimeratk-support@desy.de>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4#include "JsonMapFileParser.h"
5
6#include "JsonExtensions.h" // IWYU pragma: keep - used indirectly by nlohmann json
8
9#include <nlohmann/json.hpp>
10
11#include <algorithm>
12#include <charconv>
13#include <map>
14#include <string>
15#include <vector>
16
17using json = nlohmann::json;
18
19namespace ChimeraTK::detail {
20
21 /********************************************************************************************************************/
22
23 struct JsonAddressSpaceEntry;
24
25 struct JsonMapFileParser::Imp {
26 std::pair<NumericAddressedRegisterCatalogue, MetadataCatalogue> parse(std::ifstream& stream);
27
28 std::string fileName;
29 NumericAddressedRegisterCatalogue catalogue;
30 MetadataCatalogue metadata;
31 };
32
33 /********************************************************************************************************************/
34
35 JsonMapFileParser::JsonMapFileParser(std::string fileName) : _theImp(std::make_unique<Imp>(std::move(fileName))) {}
36
37 JsonMapFileParser::~JsonMapFileParser() = default;
38
39 /********************************************************************************************************************/
40
41 std::pair<NumericAddressedRegisterCatalogue, MetadataCatalogue> JsonMapFileParser::parse(std::ifstream& stream) {
42 return _theImp->parse(stream);
43 }
44
45 /********************************************************************************************************************/
46 /********************************************************************************************************************/
47
48 // map Access enum to JSON as strings. Need to redefine the strongly typed enums as old-fashioned ones....
49 enum Access {
53 accessNotSet
54 };
55 NLOHMANN_JSON_SERIALIZE_ENUM(
56 Access, {{Access::READ_ONLY, "RO"}, {Access::READ_WRITE, "RW"}, {Access::WRITE_ONLY, "WO"}})
57
58 /********************************************************************************************************************/
59
60 // map RepresentationType enum to JSON as strings
61 enum RepresentationType {
66 representationNotSet
67 };
68 NLOHMANN_JSON_SERIALIZE_ENUM(RepresentationType,
69 {{RepresentationType::FIXED_POINT, "fixedPoint"}, {RepresentationType::IEEE754, "IEEE754"},
70 {RepresentationType::VOID, "void"}, {RepresentationType::ASCII, "string"}})
71
72 /********************************************************************************************************************/
73
74 // map AddressType enum to JSON as strings
75 enum AddressType { IO, DMA, addressTypeNotSet };
76 NLOHMANN_JSON_SERIALIZE_ENUM(AddressType, {{AddressType::IO, "IO"}, {AddressType::DMA, "DMA"}})
77
78 /********************************************************************************************************************/
79
80 // Allow hex string representation of values (but still accept plain int as well)
81 struct HexValue {
82 size_t v;
83
84 // NOLINTNEXTLINE(readability-identifier-naming)
85 friend void from_json(const json& j, HexValue& hv) {
86 if(j.is_string()) {
87 auto sdata = std::string(j);
88 try {
89 hv.v = std::stoll(sdata, nullptr, 0);
90 }
91 catch(std::invalid_argument& e) {
92 throw json::type_error::create(0, "Cannot parse string '" + sdata + "' as number.", &j);
93 }
94 catch(std::out_of_range& e) {
95 throw json::type_error::create(0, "Number '" + sdata + "' out of range.", &j);
96 }
97 }
98 else {
99 hv.v = j;
100 }
101 }
102
103 // NOLINTNEXTLINE(readability-identifier-naming)
104 friend void to_json(json& j, const HexValue& hv) { j = hv.v; }
105 };
106
107 /********************************************************************************************************************/
108 /********************************************************************************************************************/
109
112 struct JsonAddressSpaceEntry {
113 std::string engineeringUnit;
114 std::string description;
115 Access access{Access::accessNotSet};
116 std::vector<size_t> triggeredByInterrupt;
117 size_t numberOfElements{1};
118 size_t bytesPerElement{0};
119
120 struct DoubleBufferingInfo {
121 struct SecondAddress {
122 AddressType type{AddressType::DMA};
123 size_t channel{0};
124 HexValue offset{0};
125
126 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(SecondAddress, type, channel, offset)
127 };
128
129 SecondAddress secondaryBufferAddress;
130 std::string enableRegister;
131 std::string readBufferRegister;
132 size_t index{0};
133
134 void fill(NumericAddressedRegisterInfo& info) const {
135 info.doubleBuffer->address = secondaryBufferAddress.offset.v;
136 // The two buffers of a double-buffered register must live on the same BAR.
137 size_t secondaryBar =
138 secondaryBufferAddress.channel + (secondaryBufferAddress.type == AddressType::DMA ? 13 : 0);
139 if(secondaryBar != info.bar) {
140 throw ChimeraTK::logic_error("Register " + info.pathName +
141 ": double-buffered registers whose two buffers lie on different BARs are not supported (primary "
142 "BAR " +
143 std::to_string(info.bar) + ", secondary BAR " + std::to_string(secondaryBar) + ").");
144 }
145 info.doubleBuffer->enableRegisterPath = enableRegister;
146 info.doubleBuffer->inactiveBufferRegisterPath = readBufferRegister;
147 info.doubleBuffer->index = index;
148 }
149
150 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
151 DoubleBufferingInfo, secondaryBufferAddress, enableRegister, readBufferRegister, index)
152 };
153 std::optional<DoubleBufferingInfo> doubleBuffering;
154
155 struct Address {
156 AddressType type{AddressType::IO};
157 size_t channel{0};
158 HexValue offset{std::numeric_limits<size_t>::max()};
159
160 void fill(NumericAddressedRegisterInfo& info) const {
161 assert(type != AddressType::addressTypeNotSet);
162 info.address = offset.v;
163 info.bar = channel + (type == AddressType::DMA ? 13 : 0);
164 }
165
166 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Address, type, channel, offset)
167 } address{AddressType::addressTypeNotSet};
168
169 // Basic representation without sub elements
170 struct Representation {
171 RepresentationType type{RepresentationType::FIXED_POINT};
172 uint32_t width{type != RepresentationType::representationNotSet ? 32U : 0U};
173 int32_t fractionalBits{0};
174 bool isSigned{false};
175 uint32_t bitShift{0};
176
177 void fill(NumericAddressedRegisterInfo& info, size_t offset, size_t bytesPerElem) const {
178 if(type != RepresentationType::representationNotSet) {
179 info.channels.emplace_back(8 * offset, NumericAddressedRegisterInfo::Type(type), width, fractionalBits,
180 type != RepresentationType::IEEE754 ? isSigned : true,
181 DataType("int" + std::to_string(bytesPerElem * 8)));
182 info.channels.back().bitOffset += bitShift;
183 if(bitShift != 0) {
184 info.isBitRange = true;
185 }
186 }
187 else {
188 Representation().fill(info, offset, bytesPerElem);
189 }
190 }
191
192 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Representation, type, width, fractionalBits, isSigned, bitShift)
193 } representation{RepresentationType::representationNotSet};
194
195 // 'register' is a C++ keyword, so the JSON member is deserialized manually into 'regPath'.
196 struct SelectedBy {
197 std::string regPath;
198 int64_t value{0};
199
200 // NOLINTNEXTLINE(readability-identifier-naming)
201 friend void from_json(const nlohmann::json& j, SelectedBy& s) {
202 if(!j.contains("register")) {
203 throw ChimeraTK::logic_error("'selectedBy' requires a 'register' member.");
204 }
205 if(!j.contains("value")) {
206 throw ChimeraTK::logic_error("'selectedBy' requires a 'value' member.");
207 }
208 j.at("register").get_to(s.regPath);
209 j.at("value").get_to(s.value);
210 }
211
212 // NOLINTNEXTLINE(readability-identifier-naming)
213 friend void to_json(nlohmann::json& j, const SelectedBy& s) {
214 j = nlohmann::json{{"register", s.regPath}, {"value", s.value}};
215 }
216 };
217
218 struct ChannelChild;
219
220 struct Channel {
221 std::string engineeringUnit;
222 std::string description;
223 size_t offset;
224 size_t bytesPerElement{4};
225 Representation representation;
226 std::optional<SelectedBy> selectedBy;
227 std::map<std::string, ChannelChild> children;
228
229 void fill(NumericAddressedRegisterInfo& info, const std::optional<SelectedBy>& inheritedSelectedBy) const {
230 representation.fill(info, offset, bytesPerElement);
231 if(selectedBy || inheritedSelectedBy) {
232 const auto& selected = selectedBy ? *selectedBy : *inheritedSelectedBy;
233 RegisterPath selReg(selected.regPath);
234 selReg.setAltSeparator(".");
235 info.channels.back().selectedBy.emplace(selReg, selected.value);
236 }
237 }
238
239 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(
240 Channel, engineeringUnit, description, offset, bytesPerElement, representation, selectedBy, children)
241 };
242
243 // A bit-field child of a named channel of a 2D register. Works like the top-level bit-field mechanism of a 1D
244 // register, but is interpreted relative to the channel's sample word (the member byte offset/bytesPerElement).
245 struct ChannelChild {
246 std::string engineeringUnit;
247 std::string description;
248 Representation representation;
249
250 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(ChannelChild, engineeringUnit, description, representation)
251 };
252
253 // The channels, sorted by byte offset so that per-channel information (and thus the channel index of a
254 // 2D accessor) follows the natural memory order, independent of the lexically sorted map key. Returns the
255 // channel names together with the pointers so both the plain fill and the slice creation can use it.
256 [[nodiscard]] std::vector<std::pair<std::string, const Channel*>> channelsInOffsetOrder() const {
257 std::vector<std::pair<std::string, const Channel*>> result;
258 result.reserve(channels.size());
259 for(const auto& [channelName, channel] : channels) {
260 result.emplace_back(channelName, &channel);
261 }
262 std::ranges::stable_sort(
263 result, [](const auto& a, const auto& b) { return a.second->offset < b.second->offset; });
264 return result;
265 }
266
267 size_t pitch{0};
268 std::map<std::string, Channel> channels;
269
270 // Register-level selectedBy for scalar/1D (non-2D, non-channels) registers: makes the whole register conditional
271 // on a selector. For 2D registers the per-channel `Channel::selectedBy` is used instead; having both set is a map
272 // authoring error.
273 std::optional<SelectedBy> selectedBy;
274
275 void fill(NumericAddressedRegisterInfo& info, const std::string& name, const RegisterPath& parentName,
276 bool addressSetByParent, const std::optional<SelectedBy>& inheritedSelectedBy,
277 const std::string& selectedBySource) const {
278 info.pathName = parentName / name;
279 info.pathName.setAltSeparator(".");
280
281 if(triggeredByInterrupt.empty()) {
282 if(access != Access::accessNotSet) {
283 info.registerAccess = NumericAddressedRegisterInfo::Access(access);
284 }
285 else if(!addressSetByParent) {
287 }
288 }
289 else {
290 if(access != Access::accessNotSet) {
292 "Register " + info.pathName + ": 'access' and 'triggeredByInterrupt' are mutually exclusive.");
293 }
294 info.interruptId = triggeredByInterrupt;
296 }
297
298 if(representation.type != RepresentationType::VOID) {
299 if(address.type != AddressType::addressTypeNotSet) {
300 address.fill(info);
301 if(channels.empty()) {
302 auto bPerElem = (bytesPerElement != 0 ? bytesPerElement : 4); // create default if not set
303 info.elementPitchBits = bPerElem * 8;
304 info.nElements = numberOfElements;
305 representation.fill(info, 0, bPerElem);
306 applyRegisterSelectedBy(info, inheritedSelectedBy);
307 }
308 else {
309 info.elementPitchBits = pitch * 8;
310 info.nElements = numberOfElements;
311 // Iterate the channels sorted by byte offset (see channelsInOffsetOrder) so the per-channel information
312 // (and hence the channel index of the 2D accessor) stays in the natural memory order.
313 for(const auto& [channelName, channel] : channelsInOffsetOrder()) {
314 (void)channelName; // the channel name is the map key; the channel data carries its own offset
315 channel->fill(info, inheritedSelectedBy);
316 }
317 }
318 }
319 else if(addressSetByParent) {
320 if(channels.empty()) {
321 if(representation.type == RepresentationType::representationNotSet) {
322 throw ChimeraTK::logic_error("Representation not set for register " + parentName / name +
323 " which inherited the address from parent!");
324 }
325 // If bytesPerElement has not been set in the json file, take it from parent info
326 representation.fill(info, 0, (bytesPerElement != 0 ? bytesPerElement : info.elementPitchBits / 8));
327 applyRegisterSelectedBy(info, inheritedSelectedBy);
328 }
329 else {
330 throw ChimeraTK::logic_error("Address must be set for entries with channels: register " + info.pathName);
331 }
332 }
333 else {
334 throw ChimeraTK::logic_error("Address not set but representation given in register " + parentName / name);
335 }
336 }
337 else {
338 // VOID registers have no address — they are pure interrupt sources
339 if(address.type != AddressType::addressTypeNotSet) {
340 throw ChimeraTK::logic_error("Address is set for void-typed register " + info.pathName);
341 }
342 if(triggeredByInterrupt.empty()) {
344 "Void-typed register " + parentName / name + " needs 'triggeredByInterrupt' entry.");
345 }
346 info.nElements = 0;
347 info.dataDescriptor = DataDescriptor{DataDescriptor::FundamentalType::nodata};
348 info.interruptId = triggeredByInterrupt;
350 info.channels.clear();
351 info.channels.emplace_back(0, NumericAddressedRegisterInfo::Type::VOID, 0, 0, false);
352 }
353
354 if(doubleBuffering) {
355 info.doubleBuffer.emplace();
356 doubleBuffering->fill(info);
357 }
358 else {
359 info.doubleBuffer.reset();
360 }
361
362 info.description = description;
363 info.engineeringUnit = engineeringUnit;
364
365 // 'selectedBy' describes when data is valid to read, so it may only be applied to read-only registers.
366 // A writable register (READ_WRITE or WRITE_ONLY) carrying a 'selectedBy' (own, per-channel or inherited)
367 // is rejected here; the error points to the entry declaring the 'selectedBy' (the parent for inherited ones).
368 if((info.registerAccess != NumericAddressedRegisterInfo::Access::READ_ONLY) &&
369 (info.registerAccess != NumericAddressedRegisterInfo::Access::INTERRUPT)) {
370 for(const auto& channel : info.channels) {
371 if(channel.selectedBy) {
372 throw ChimeraTK::logic_error("Register " + info.pathName +
373 ": 'selectedBy' may only be used on read-only registers (Access::READ_ONLY or Access::INTERRUPT). "
374 "The offending 'selectedBy' is declared at '" +
375 selectedBySource + "'.");
376 }
377 }
378 }
379 }
380
381 // Apply a register-level 'selectedBy' (scalar/1D registers) to the register's single channel. Must only be called
382 // after 'representation.fill' created exactly one channel. A local 'selectedBy' overrides an inherited one.
383 void applyRegisterSelectedBy(
384 NumericAddressedRegisterInfo& info, const std::optional<SelectedBy>& inheritedSelectedBy) const {
385 if(selectedBy || inheritedSelectedBy) {
386 const auto& selected = selectedBy ? *selectedBy : *inheritedSelectedBy;
387 RegisterPath selReg(selected.regPath);
388 selReg.setAltSeparator(".");
389 info.channels.back().selectedBy.emplace(selReg, selected.value);
390 }
391 }
392
393 // Build the ChannelInfo of a channel or bit-field child slice from its 'representation'. The raw type always
394 // spans the whole sample word (wordBits), so the slice reads the element with its full word width in the
395 // underlying transport and extracts the range via the bit offset/width. Shared by the parent channel slice and
396 // the bit-field child slice creation.
397 static NumericAddressedRegisterInfo::ChannelInfo makeChannelInfo(const Representation& rep, size_t wordBits,
398 const std::optional<NumericAddressedRegisterInfo::SelectedBy>& selectedBy) {
399 return {rep.bitShift, NumericAddressedRegisterInfo::Type(rep.type), rep.width, rep.fractionalBits,
400 rep.type != RepresentationType::IEEE754 ? rep.isSigned : true, DataType("int" + std::to_string(wordBits)),
401 selectedBy};
402 }
403
404 // Add a slice register to the catalogue and, for a double-buffered register, its two buffer-view registers
405 // BUF0/BUF1, exactly as the parent 2D register's BUF0/BUF1 block but folded to the slice. The slice's own
406 // (already channel-shifted) secondary buffer address is reused for the BUF1 view, which is why both the parent
407 // channel slice and the inherited child slice can share this helper.
408 static void addSliceWithBufferViews(NumericAddressedRegisterCatalogue& catalogue,
409 NumericAddressedRegisterInfo& slice, const RegisterPath& slicePath) {
410 catalogue.addRegister(slice);
411 if(!slice.doubleBuffer.has_value()) {
412 return;
413 }
414 NumericAddressedRegisterInfo sliceBuf0 = slice;
415 sliceBuf0.pathName = slicePath + "/BUF0";
416 sliceBuf0.doubleBuffer.reset();
417 sliceBuf0.registerAccess = NumericAddressedRegisterInfo::Access::READ_ONLY;
418 sliceBuf0.computeDataDescriptor();
419 catalogue.addRegister(sliceBuf0);
420 NumericAddressedRegisterInfo sliceBuf1 = slice;
421 sliceBuf1.pathName = slicePath + "/BUF1";
422 sliceBuf1.doubleBuffer.reset();
423 sliceBuf1.address = slice.doubleBuffer->address;
424 sliceBuf1.registerAccess = NumericAddressedRegisterInfo::Access::READ_ONLY;
425 sliceBuf1.computeDataDescriptor();
426 catalogue.addRegister(sliceBuf1);
427 }
428
429 std::map<std::string, JsonAddressSpaceEntry> children;
430
431 void addInfos(NumericAddressedRegisterCatalogue& catalogue, const std::string& name, const RegisterPath& parentName,
432 bool addressSetByParent, const std::optional<SelectedBy>& inheritedSelectedBy = std::nullopt,
433 const std::string& inheritedSelectedBySource = "") const {
434 if(name.empty()) {
435 throw ChimeraTK::logic_error("Entry in module " + parentName + " has no name.");
436 }
437 // The effective selector for this subtree: a local 'selectedBy' overrides an inherited one.
438 const auto& effectiveSelectedBy = selectedBy ? selectedBy : inheritedSelectedBy;
439 // The entry declaring the effective selector: a local 'selectedBy' is declared at this entry's path, otherwise
440 // it is inherited from the entry that declared it further up (used for error messages pointing to the parent).
441 RegisterPath thisPath = parentName / name;
442 thisPath.setAltSeparator(".");
443 const std::string effectiveSelectedBySource = selectedBy ? std::string(thisPath) : inheritedSelectedBySource;
444 if(address.type != AddressType::addressTypeNotSet) {
445 // New address entry. Don't use parent information
446 NumericAddressedRegisterInfo my;
447 my.channels.clear(); // default constructor already creates a channel with default settings...
448 fill(my, name, parentName, addressSetByParent, effectiveSelectedBy, effectiveSelectedBySource);
449 my.computeDataDescriptor();
450 catalogue.addRegister(my);
451 if(!channels.empty()) {
452 // create one register entry per named channel of the 2D register: a read-only 1D slice of the
453 // 2D register. The channel's byte offset is folded into the address (so bitOffset == 0), and the full
454 // element pitch is kept as the stride between samples. Iterate sorted by byte offset (see
455 // channelsInOffsetOrder) so the created slice registers follow the natural memory order.
456 for(const auto& [channelName, channel] : channelsInOffsetOrder()) {
457 RegisterPath slicePath = my.pathName / channelName;
458 slicePath.setAltSeparator(".");
459 // A channel slice whose path collides with an already existing register is a map authoring error.
460 if(catalogue.hasRegister(slicePath)) {
462 "Channel slice '" + (slicePath) + "' collides with an already existing register.");
463 }
464 const auto& rep = channel->representation;
465
466 std::optional<NumericAddressedRegisterInfo::SelectedBy> channelSelectedBy = std::nullopt;
467 if(channel->selectedBy || effectiveSelectedBy) {
468 const auto& sb = channel->selectedBy ? *channel->selectedBy : *effectiveSelectedBy;
469 auto selReg = RegisterPath(sb.regPath);
470 selReg.setAltSeparator(".");
471 channelSelectedBy.emplace(selReg, sb.value);
472 }
473
474 auto wordBits = channel->bytesPerElement * 8;
475 // A channel slice of a non-interrupt 2D register is read-only: writing to a single channel of a 2D
476 // register would require a read-modify-write cycle across the channels, which is deliberately not
477 // supported. A slice of an interrupt-driven 2D register additionally advertises wait_for_new_data,
478 // since the whole 2D register (including all its channel slices) updates with the same interrupt.
479 auto sliceAccessType = (my.registerAccess == NumericAddressedRegisterInfo::Access::INTERRUPT) ?
482 NumericAddressedRegisterInfo slice(slicePath, my.bar, my.address + channel->offset, my.nElements,
483 my.elementPitchBits, {makeChannelInfo(rep, wordBits, channelSelectedBy)}, sliceAccessType,
484 my.interruptId, my.doubleBuffer);
485 // The slice's double-buffer configuration inherits the parent's, but its secondary buffer address
486 // is the parent's shifted by the channel byte offset, matching the slice's own data address and the
487 // slice's BUF1 buffer-view register created below.
488 if(slice.doubleBuffer.has_value()) {
489 slice.doubleBuffer->address += channel->offset;
490 }
491 slice.isBitRange = (rep.bitShift != 0);
492 slice.computeDataDescriptor();
493 slice.engineeringUnit = channel->engineeringUnit;
494 slice.description = channel->description;
495 addSliceWithBufferViews(catalogue, slice, slicePath);
496 // Create one read-only bit-range slice per bit-field child of the channel, at <channel>/<child>.
497 // The child slice behaves exactly like the parent channel slice (same address, stride, interrupt and
498 // double-buffer inheritance, including the BUF0/BUF1 buffer views), but extracts the child's bit range
499 // from every sample word. The word context is the channel's byte offset and bytesPerElement.
500 for(const auto& [childName, child] : channel->children) {
501 const auto& crep = child.representation;
502 // A child that is not a proper bit range of the channel word is unsupported and is ignored, so newer
503 // map files that use a not yet supported child feature still parse for the supported parts.
504 if((crep.bitShift == 0 && crep.width == wordBits) || (crep.bitShift + crep.width > wordBits)) {
505 continue;
506 }
507 RegisterPath childPath = slicePath / childName;
508 childPath.setAltSeparator(".");
509 // A child slice whose path collides with an already existing register is a map authoring error.
510 if(catalogue.hasRegister(childPath)) {
512 "Child slice '" + (childPath) + "' collides with an already existing register.");
513 }
514 NumericAddressedRegisterInfo childSlice = slice;
515 childSlice.pathName = childPath;
516 childSlice.channels = {makeChannelInfo(crep, wordBits, channelSelectedBy)};
517 childSlice.isBitRange = true;
518 childSlice.engineeringUnit = child.engineeringUnit;
519 childSlice.description = child.description;
520 childSlice.computeDataDescriptor();
521 addSliceWithBufferViews(catalogue, childSlice, childPath);
522 }
523 }
524 }
525 if(doubleBuffering.has_value()) {
526 // Create the .buf0 register as a copy of the main one
527 NumericAddressedRegisterInfo buf0Register = my;
528 buf0Register.pathName = my.pathName + "/BUF0";
529 buf0Register.doubleBuffer.reset(); // it's a simple view of the buffer
530 buf0Register.registerAccess = NumericAddressedRegisterInfo::Access::READ_ONLY;
531 buf0Register.computeDataDescriptor();
532 catalogue.addRegister(buf0Register);
533 NumericAddressedRegisterInfo buf1Register = my;
534 buf1Register.pathName = my.pathName + "/BUF1";
535 buf1Register.doubleBuffer.reset(); // it's a simple view of the buffer
536 buf1Register.address = doubleBuffering->secondaryBufferAddress.offset.v;
537 buf1Register.registerAccess = NumericAddressedRegisterInfo::Access::READ_ONLY;
538 buf1Register.computeDataDescriptor();
539 catalogue.addRegister(buf1Register);
540 }
541 }
542 else if(representation.type != RepresentationType::representationNotSet) {
543 // take over parent address (except void interrupt registers which don't have an address)
544 auto my = catalogue.getBackendRegister(parentName);
545 my.channels.clear(); // will be refilled from representation
546 fill(my, name, parentName, addressSetByParent, effectiveSelectedBy,
547 effectiveSelectedBySource); // only updates the name and the representation
548 my.computeDataDescriptor();
549 catalogue.addRegister(my);
550 }
551
552 for(const auto& [childName, child] : children) {
553 child.addInfos(catalogue, childName, parentName / name,
554 addressSetByParent || (address.type != AddressType::addressTypeNotSet), effectiveSelectedBy,
555 effectiveSelectedBySource);
556 }
557 }
558
559 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(JsonAddressSpaceEntry, engineeringUnit, description, access,
560 triggeredByInterrupt, numberOfElements, bytesPerElement, pitch, address, representation, children, channels,
561 doubleBuffering, selectedBy)
562 };
563
564 /********************************************************************************************************************/
565
566 struct InterruptHandlerEntry {
567 // The whole interrupt handler entry except its 'subhandler' member, kept verbatim as backend-specific metadata.
568 json raw;
569
570 std::map<std::string, InterruptHandlerEntry> subhandler;
571
572 void fill(const std::vector<size_t>& intId, MetadataCatalogue& metadata) const {
573 if(!intId.empty()) {
574 json jsonIntId;
575 jsonIntId = intId;
576 metadata.addMetadata("!" + jsonIntId.dump(), raw.dump());
577 }
578
579 for(const auto& [subIntId, handler] : subhandler) {
580 std::vector<size_t> qualifiedSubIntId = intId;
581 qualifiedSubIntId.push_back(std::stoll(subIntId));
582 handler.fill(qualifiedSubIntId, metadata);
583 }
584 }
585
586 // NOLINTNEXTLINE(readability-identifier-naming)
587 friend void from_json(const json& j, InterruptHandlerEntry& e) {
588 e.raw = j;
589 e.raw.erase("subhandler");
590 // Apply the common normalisation to the entry and to each controller data object it carries: drop keys starting
591 // with an underscore and default a missing 'version' to 1. The rest of the backend-specific data structure is
592 // preserved verbatim and passed on as metadata.
593 // First drop underscore-prefixed keys at the entry's top level (e.g. a "_comment" sibling of the controller key).
594 for(auto it = e.raw.begin(); it != e.raw.end();) {
595 if(!it.key().empty() && it.key().front() == '_') {
596 it = e.raw.erase(it);
597 }
598 else {
599 ++it;
600 }
601 }
602 // Then normalise each controller data object carried by the entry.
603 for(auto& value : e.raw) {
604 if(!value.is_object()) {
605 continue;
606 }
607 if(!value.contains("version")) {
608 value["version"] = 1;
609 }
610 for(auto it = value.begin(); it != value.end();) {
611 if(!it.key().empty() && it.key().front() == '_') {
612 it = value.erase(it);
613 }
614 else {
615 ++it;
616 }
617 }
618 }
619 if(j.contains("subhandler")) {
620 e.subhandler = j.at("subhandler").get<std::map<std::string, InterruptHandlerEntry>>();
621 }
622 }
623 };
624
625 /********************************************************************************************************************/
626 /********************************************************************************************************************/
627
628 std::pair<NumericAddressedRegisterCatalogue, MetadataCatalogue> JsonMapFileParser::Imp::parse(std::ifstream& stream) {
629 // read and parse JSON data
630 try {
631 auto data = json::parse(stream);
632
633 // Supported map format version (MAJOR.MINOR, no patch component).
634 constexpr uint32_t supportedMapFormatMajor = 1;
635 constexpr uint32_t supportedMapFormatMinor = 0;
636
637 // Check the map format version: an exactly two-component MAJOR.MINOR string whose components are non-empty
638 // sequences of decimal digits, compared numerically against the supported version. Each component is parsed with
639 // std::from_chars into a uint32_t; a malformed or oversized component is reported through the returned error
640 // code or a trailing character rather than by throwing, so no exception escapes the version check.
641 std::string ver = data.at("mapFormatVersion").get<std::string>();
642 uint32_t major = 0;
643 uint32_t minor = 0;
644
645 auto [p, e1] = std::from_chars(ver.data(), ver.data() + ver.size(), major);
646 if(e1 != std::errc{} || p == ver.data() + ver.size() || *p++ != '.') {
647 throw ChimeraTK::logic_error("Error parsing JMAP file, mapFormatVersion contains wrong version format");
648 }
649
650 auto [q, e2] = std::from_chars(p, ver.data() + ver.size(), minor);
651 if(e2 != std::errc{} || q != ver.data() + ver.size()) {
652 throw ChimeraTK::logic_error("Error parsing JMAP file, mapFormatVersion contains wrong version format");
653 }
654
655 if(major != supportedMapFormatMajor || minor != supportedMapFormatMinor) {
656 throw ChimeraTK::logic_error("Unsupported map format version '" + ver + "'. Supported version: 1.0");
657 }
658
659 // Parse the "addressSpace" entries recursively
660 std::map<std::string, JsonAddressSpaceEntry> addressSpace = data.at("addressSpace");
661 for(const auto& [addressSpaceName, entry] : addressSpace) {
662 entry.addInfos(catalogue, addressSpaceName, "/", /*addressSetByParent=*/false);
663 }
664
665 // Scan the catalogue for bit ranges.
666 // Afterwards, scan again for registers which have bit shift 0, a width smaller than their element size and that
667 // share their starting address with a bit range. They have to become bit ranges as well.
668 std::set<std::pair<uint64_t, uint64_t>> addressesWithBitRange;
669 for(auto& reg : catalogue) {
670 if(reg.isBitRange) {
671 addressesWithBitRange.insert({reg.bar, reg.address});
672 }
673 }
674 if(addressesWithBitRange.size()) {
675 // Compute the element data width (bytes per element times 8) from the single channel's raw type. This equals
676 // the element pitch only for non-strided registers; a strided channel slice has a larger element pitch than
677 // its element data width, so such slices must not be reclassified as bit ranges even when their element data
678 // width is smaller than the pitch or they share their address with a bit range.
679 auto elementDataWidth = [](const NumericAddressedRegisterInfo::ChannelInfo& c) {
680 // Obtain the element data width (bytes per element times 8) from the channel's raw type via
681 // DataType::getNumberOfBytes(); no std::bad_cast can arise because the switch covers every DataType value.
682 return c.getRawType().getNumberOfBytes() * 8;
683 };
684 for(auto& reg : catalogue) {
685 if((reg.channels.size() == 1) && (reg.channels[0].bitOffset == 0) &&
686 (reg.channels[0].width < elementDataWidth(reg.channels[0])) &&
687 (reg.elementPitchBits == elementDataWidth(reg.channels[0]))) {
688 if(addressesWithBitRange.find({reg.bar, reg.address}) != addressesWithBitRange.end()) {
689 reg.isBitRange = true;
690 }
691 }
692 }
693 }
694
695 // Validate every 'selectedBy' selector register referenced by any channel: (a) the referenced selector
696 // register must exist in the catalogue, and (b) it must not itself be conditionally enabled by a 'selectedBy'
697 // (a selector must be unconditionally readable, otherwise gating would depend on another gate).
698 for(const auto& reg : catalogue) {
699 for(const auto& channel : reg.channels) {
700 if(!channel.selectedBy) {
701 continue;
702 }
703 std::string regPath = channel.selectedBy->regPath;
704 std::string channelName = reg.getRegisterName();
705 // (a) The selector register must exist in the catalogue.
706 if(!catalogue.hasRegister(channel.selectedBy->regPath)) {
707 throw ChimeraTK::logic_error("Error parsing JSON map file '" + fileName + "': channel '" + channelName +
708 "' is gated by 'selectedBy' referencing undefined register '" + regPath + "'.");
709 }
710 // (b) The selector register must not itself be conditionally enabled by a 'selectedBy' of its own,
711 // since the gate it drives must be readable unconditionally to evaluate the selection.
712 auto selectorReg = catalogue.getBackendRegister(channel.selectedBy->regPath);
713 for(const auto& selectorChannel : selectorReg.channels) {
714 if(selectorChannel.selectedBy) {
715 throw ChimeraTK::logic_error("Error parsing JSON map file '" + fileName + "': selector register '" +
716 regPath + "' of channel '" + channelName +
717 "' is itself conditionally enabled by 'selectedBy'; a selector register must not be gated.");
718 }
719 }
720 }
721 }
722
723 for(const auto& entry : data.at("metadata").items()) {
724 if(entry.key().empty()) {
726 "Error parsing JSON map file '" + fileName + "': Metadata key must not be empty.");
727 }
728 if(entry.key()[0] == '_') {
729 continue;
730 }
731 metadata.addMetadata(entry.key(), entry.value());
732 }
733
734 // backwards compatibility: interrupt handler description is expected to be in metadata
735 InterruptHandlerEntry interruptHandler;
736 interruptHandler.subhandler = data.at("interruptHandler");
737 interruptHandler.fill({}, metadata);
738
739 return {std::move(catalogue), std::move(metadata)};
740 }
741 catch(const ChimeraTK::logic_error& e) {
742 throw ChimeraTK::logic_error("Error parsing JSON map file '" + fileName + "': " + e.what());
743 }
744 catch(const json::exception& e) {
745 throw ChimeraTK::logic_error("Error parsing JSON map file '" + fileName + "': " + e.what());
746 }
747 }
748
749 /********************************************************************************************************************/
750
751} // namespace ChimeraTK::detail
nlohmann::json json
Access
Enum describing the access mode of the register:
Exception thrown when a logic error has occured.
Definition Exception.h:51
const char * what() const noexcept override
Return the message describing what exactly went wrong.
Definition Exception.cpp:20
std::string to_string(const std::string &v)