blob: ae07e1a3f0aeea0456a10a3af92dba466498e892 [file] [edit]
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright OpenBMC Authors
#include "fpga/fpga-tables.h"
#include <array>
#include <cmath>
#include <limits>
#include <string>
#include <vector>
#include <gtest/gtest.h>
namespace
{
const nsm_fpga_command wordRead = {
.op = NSM_FPGA_OP_READ32,
.station = 29,
.u = {.csr = {0x05000400ULL, 0}},
};
} // namespace
TEST(fpgaTables, mapP0IsBoundToItsIdentity)
{
const auto *p0 = nsm_fpga_map_p0();
ASSERT_NE(p0, nullptr);
EXPECT_EQ(p0->window.configuration_type, 2u);
EXPECT_EQ(p0->window.control_write, 0x1000u);
EXPECT_EQ(p0->window.status, 0x10a0u);
EXPECT_EQ(p0->window.compact, 0x10e0u);
EXPECT_EQ(p0->station_count, 38u);
}
TEST(fpgaTables, stationBySelectorFindsEachStationKind)
{
const auto *p0 = nsm_fpga_map_p0();
const auto *ddr = nsm_fpga_station_by_selector(p0, 29);
ASSERT_NE(ddr, nullptr);
EXPECT_EQ(ddr->selector, 29);
EXPECT_EQ(ddr->kind, NSM_FPGA_STATION_WORD);
EXPECT_EQ(ddr->first_byte, 0x05000200u);
EXPECT_EQ(ddr->last_byte, 0x0500080fu);
const auto *osfp = nsm_fpga_station_by_selector(p0, 1);
ASSERT_NE(osfp, nullptr);
EXPECT_EQ(osfp->kind, NSM_FPGA_STATION_I2C_BYTE);
EXPECT_EQ(nsm_fpga_station_by_selector(p0, 37)->kind,
NSM_FPGA_STATION_SDM);
EXPECT_EQ(nsm_fpga_station_by_selector(p0, 38), nullptr);
EXPECT_EQ(nsm_fpga_station_by_selector(nullptr, 28), nullptr);
}
TEST(fpgaTables, findRuleAndFindGroupRejectUnknownNames)
{
const auto *p0 = nsm_fpga_map_p0();
EXPECT_EQ(nsm_fpga_find_rule(p0, "HGX_FPGA_0_Anything"), nullptr);
EXPECT_EQ(nsm_fpga_find_group(p0, "Anything"), nullptr);
EXPECT_EQ(nsm_fpga_find_rule(p0, nullptr), nullptr);
}
TEST(fpgaTables, ruleValidAndWordLegalAcceptEveryRowOfEveryPart)
{
const auto *p0 = nsm_fpga_map_p0();
for (size_t p = 0; p < p0->part_count; ++p) {
const auto *part = p0->parts[p];
for (size_t g = 0; g < part->group_count; ++g) {
const auto &group = part->groups[g];
EXPECT_TRUE(nsm_fpga_commands_valid(p0, group.setup,
group.setup_count))
<< group.name;
EXPECT_TRUE(nsm_fpga_commands_valid(
p0, group.cleanup, group.cleanup_count))
<< group.name;
EXPECT_EQ(nsm_fpga_find_group(p0, group.name), &group);
}
for (size_t i = 0; i < part->rule_count; ++i) {
const auto &rule = part->rules[i];
EXPECT_TRUE(nsm_fpga_rule_valid(p0, &rule))
<< rule.name;
EXPECT_EQ(nsm_fpga_find_rule(p0, rule.name), &rule);
for (size_t c = 0; c < rule.command_count; ++c) {
const auto &cmd = rule.commands[c];
if (cmd.op == NSM_FPGA_OP_READ32 ||
cmd.op == NSM_FPGA_OP_WRITE32) {
EXPECT_TRUE(nsm_fpga_word_legal(
p0, cmd.station, cmd.op,
cmd.u.csr.address, cmd.u.csr.value,
0xf))
<< rule.name << " row " << c;
}
}
}
}
}
/* A state map for test rules: bit 0 set is enabled, anything else absent */
const nsm_fpga_state_entry testStates[] = {
{0x1, 0x1, NSM_FPGA_STATE_ENABLED, NSM_FPGA_HEALTH_OK},
{0, 0, NSM_FPGA_STATE_ABSENT, NSM_FPGA_HEALTH_OK}};
TEST(fpgaTables, ruleValidChecksStationAndWindow)
{
const auto *p0 = nsm_fpga_map_p0();
nsm_fpga_rule rule = {
.name = "Test_Rule",
.commands = &wordRead,
.command_count = 1,
.kind = NSM_FPGA_RULE_STATE,
.group = "Test",
.states = testStates,
.state_count = 2,
};
EXPECT_TRUE(nsm_fpga_rule_valid(p0, &rule));
nsm_fpga_command misaligned = wordRead;
misaligned.u.csr.address = 0x05000401;
rule.commands = &misaligned;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &rule));
nsm_fpga_command beyond = wordRead;
beyond.u.csr.address = 0x05000900;
rule.commands = &beyond;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &rule));
nsm_fpga_command onI2c = wordRead;
onI2c.station = 1;
rule.commands = &onI2c;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &rule));
nsm_fpga_command badPoll = wordRead;
badPoll.poll = NSM_FPGA_POLL_ANY + 1;
rule.commands = &badPoll;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &rule));
rule.commands = &wordRead;
rule.command_count = 0;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &rule));
EXPECT_FALSE(nsm_fpga_rule_valid(p0, nullptr));
}
TEST(fpgaTables, ruleWordReadsTheGroupBufferForRowlessRules)
{
const auto *p0 = nsm_fpga_map_p0();
nsm_fpga_rule rowless = {
.name = "Presence",
.kind = NSM_FPGA_RULE_STATE,
.numeric = {.buffer = NSM_FPGA_BUF_GROUP, .offset = 1},
.group = "Test",
.states = testStates,
.state_count = 2,
};
EXPECT_TRUE(nsm_fpga_rule_valid(p0, &rowless));
rowless.numeric.buffer = NSM_FPGA_BUF_ITEM;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &rowless));
rowless.numeric.buffer = NSM_FPGA_BUF_GROUP;
const std::array<uint8_t, 3> group = {0xaa, 0x12, 0x34};
uint32_t word = 0;
ASSERT_TRUE(nsm_fpga_rule_word(&rowless, nullptr, 0, group.data(),
group.size(), &word));
EXPECT_EQ(word, 0x3412u);
EXPECT_FALSE(
nsm_fpga_rule_word(&rowless, nullptr, 0, group.data(), 1, &word));
/* a rule with rows reads its own first word */
const nsm_fpga_rule withRows = {
.name = "Word", .commands = &wordRead, .command_count = 1};
const std::array<uint8_t, 4> item = {0x01, 0x00, 0x00, 0x80};
ASSERT_TRUE(nsm_fpga_rule_word(&withRows, item.data(), item.size(),
nullptr, 0, &word));
EXPECT_EQ(word, 0x80000001u);
EXPECT_FALSE(nsm_fpga_rule_word(&withRows, nullptr, 0, group.data(),
group.size(), &word));
}
TEST(fpgaTables, ruleValidChecksCleanupRowsToo)
{
const auto *p0 = nsm_fpga_map_p0();
const nsm_fpga_command ack = {
.op = NSM_FPGA_OP_ACK_CLEAR,
.station = 29,
.poll_mask = 1,
.u = {.csr = {0x0500045cULL, 0}},
};
nsm_fpga_rule rule = {
.name = "Test_Rule",
.commands = &wordRead,
.command_count = 1,
.cleanup = &ack,
.cleanup_count = 1,
.kind = NSM_FPGA_RULE_NUMERIC,
.group = "Test",
};
EXPECT_TRUE(nsm_fpga_rule_valid(p0, &rule));
nsm_fpga_command badAck = ack;
badAck.u.csr.address = 0x05000900;
rule.cleanup = &badAck;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &rule));
}
TEST(fpgaTables, commandsValidChecksPopByteAndCapturedRows)
{
const auto *p0 = nsm_fpga_map_p0();
const nsm_fpga_command pop = {
.op = NSM_FPGA_OP_POP,
.station = 37,
.poll_mask = 0x3,
.u = {.pop = {24, 20}},
};
EXPECT_TRUE(nsm_fpga_commands_valid(p0, &pop, 1));
nsm_fpga_command popNoMask = pop;
popNoMask.poll_mask = 0;
EXPECT_FALSE(nsm_fpga_commands_valid(p0, &popNoMask, 1));
nsm_fpga_command popBadOffset = pop;
popBadOffset.u.pop.data_address = 12;
EXPECT_FALSE(nsm_fpga_commands_valid(p0, &popBadOffset, 1));
const nsm_fpga_command byte = {
.op = NSM_FPGA_OP_BYTE,
.station = 1,
.u = {.i2c = {0x50, 0,
NSM_FPGA_I2C_READ | NSM_FPGA_I2C_STOP |
NSM_FPGA_I2C_READ_NACK}},
};
EXPECT_TRUE(nsm_fpga_commands_valid(p0, &byte, 1));
nsm_fpga_command byteOnWord = byte;
byteOnWord.station = 29;
EXPECT_FALSE(nsm_fpga_commands_valid(p0, &byteOnWord, 1));
nsm_fpga_command readWithData = byte;
readWithData.u.i2c.data = 1;
EXPECT_FALSE(nsm_fpga_commands_valid(p0, &readWithData, 1));
const nsm_fpga_command captured = {
.op = NSM_FPGA_OP_WRITE_CAPTURED,
.station = 1,
.u = {.captured = {0, 0x71, NSM_FPGA_I2C_START | NSM_FPGA_I2C_STOP,
0}},
};
EXPECT_TRUE(nsm_fpga_commands_valid(p0, &captured, 1));
EXPECT_TRUE(nsm_fpga_commands_valid(p0, nullptr, 0));
EXPECT_FALSE(nsm_fpga_commands_valid(p0, nullptr, 1));
}
TEST(fpgaTables, commandsValidAndXferEncodersHandleTransactionRows)
{
const auto *p0 = nsm_fpga_map_p0();
const nsm_fpga_command xfer = {
.op = NSM_FPGA_OP_I2C_XFER,
.station = 1,
.u = {.xfer = {0x50, {0x7e, 0x01, 0x11}, 3, 0, 0x6, 6}},
};
EXPECT_TRUE(nsm_fpga_commands_valid(p0, &xfer, 1));
EXPECT_EQ(nsm_fpga_xfer_opcode(&xfer.u.xfer), 0x58);
EXPECT_EQ(nsm_fpga_xfer_argument(0x50, xfer.u.xfer.write, 3),
0x11017e50u);
EXPECT_EQ(nsm_fpga_xfer_argument(0x71, xfer.u.xfer.write, 0), 0x71u);
nsm_fpga_command onWord = xfer;
onWord.station = 29;
EXPECT_FALSE(nsm_fpga_commands_valid(p0, &onWord, 1));
nsm_fpga_command tooMany = xfer;
tooMany.u.xfer.write_count = 4;
EXPECT_FALSE(nsm_fpga_commands_valid(p0, &tooMany, 1));
nsm_fpga_command longRead = xfer;
longRead.u.xfer.read_count = 5;
EXPECT_FALSE(nsm_fpga_commands_valid(p0, &longRead, 1));
nsm_fpga_command empty = xfer;
empty.u.xfer.write_count = 0;
empty.u.xfer.captured = 0;
EXPECT_FALSE(nsm_fpga_commands_valid(p0, &empty, 1));
nsm_fpga_command capturedBeyond = xfer;
capturedBeyond.u.xfer.write_count = 1;
EXPECT_FALSE(nsm_fpga_commands_valid(p0, &capturedBeyond, 1));
nsm_fpga_command reserved = xfer;
reserved.u.xfer.dev_addr = 0x07;
EXPECT_FALSE(nsm_fpga_commands_valid(p0, &reserved, 1));
EXPECT_EQ(nsm_fpga_xfer_opcode(nullptr), 0);
}
TEST(fpgaTables, i2cArgumentPacksAddressDataAndFlags)
{
const nsm_fpga_i2c_op write = {0x71, 0x02, NSM_FPGA_I2C_START};
EXPECT_EQ(nsm_fpga_i2c_argument(&write), 0x00020271u);
const nsm_fpga_i2c_op last = {0x50, 0,
NSM_FPGA_I2C_READ | NSM_FPGA_I2C_STOP |
NSM_FPGA_I2C_READ_NACK};
EXPECT_EQ(nsm_fpga_i2c_argument(&last), 0x000d0050u);
EXPECT_TRUE(nsm_fpga_i2c_argument_valid(nsm_fpga_i2c_argument(&last)));
EXPECT_EQ(nsm_fpga_i2c_argument(nullptr), 0u);
}
TEST(fpgaTables, compareHoldsReadsBuffersAndAppliesTheOperator)
{
const std::array<uint8_t, 4> item = {0x02, 0x00, 0x8e, 0x00};
const std::array<uint8_t, 2> group = {0x01, 0x30};
nsm_fpga_compare c = {.buffer = NSM_FPGA_BUF_ITEM,
.op = NSM_FPGA_CMP_EQ,
.width = 1,
.offset = 0,
.mask = 0x3,
.value = 2};
EXPECT_TRUE(nsm_fpga_compare_holds(&c, item.data(), item.size(),
group.data(), group.size(), 0));
c.op = NSM_FPGA_CMP_GE;
c.value = 1;
EXPECT_TRUE(nsm_fpga_compare_holds(&c, item.data(), item.size(),
group.data(), group.size(), 0));
c.op = NSM_FPGA_CMP_LE;
EXPECT_FALSE(nsm_fpga_compare_holds(&c, item.data(), item.size(),
group.data(), group.size(), 0));
/* group buffer, two byte word */
nsm_fpga_compare g = {.buffer = NSM_FPGA_BUF_GROUP,
.op = NSM_FPGA_CMP_EQ,
.width = 2,
.offset = 0,
.mask = 0xffff,
.value = 0x3001};
EXPECT_TRUE(nsm_fpga_compare_holds(&g, item.data(), item.size(),
group.data(), group.size(), 0));
/* out of range read is false, never true */
g.offset = 1;
EXPECT_FALSE(nsm_fpga_compare_holds(&g, item.data(), item.size(),
group.data(), group.size(), 0));
/* no buffer: compare the value just read */
const nsm_fpga_compare v = {.buffer = NSM_FPGA_BUF_NONE,
.op = NSM_FPGA_CMP_EQ,
.mask = 0x3,
.value = 0x1};
EXPECT_TRUE(nsm_fpga_compare_holds(&v, nullptr, 0, nullptr, 0, 0x5));
EXPECT_FALSE(nsm_fpga_compare_holds(&v, nullptr, 0, nullptr, 0, 0x6));
EXPECT_FALSE(
nsm_fpga_compare_holds(nullptr, nullptr, 0, nullptr, 0, 0));
}
TEST(fpgaTables, compareHoldsOnTheWordTestsTheValueJustRead)
{
nsm_fpga_compare w = {
.buffer = NSM_FPGA_BUF_WORD, .mask = 0x1, .value = 0x1};
EXPECT_TRUE(nsm_fpga_compare_holds(&w, nullptr, 0, nullptr, 0, 0x75));
EXPECT_FALSE(nsm_fpga_compare_holds(&w, nullptr, 0, nullptr, 0, 0x74));
// Row conditions may use it, rule conditions may not
static const nsm_fpga_command rows[] = {
{.op = NSM_FPGA_OP_READ32,
.station = 29,
.remember = 1,
.u = {.csr = {0x0500045cULL, 0}}},
{.op = NSM_FPGA_OP_ACK_CLEAR,
.station = 29,
.poll_mask = 0x1,
.u = {.csr = {0x0500045cULL, 0}},
.run_if = w}};
EXPECT_TRUE(nsm_fpga_commands_valid(nsm_fpga_map_p0(), rows, 2));
nsm_fpga_rule rule = {.name = "Word",
.commands = rows,
.command_count = 2,
.kind = NSM_FPGA_RULE_STATE,
.states = testStates,
.state_count =
sizeof(testStates) / sizeof(testStates[0]),
.run_if = w};
EXPECT_FALSE(nsm_fpga_rule_valid(nsm_fpga_map_p0(), &rule));
}
TEST(fpgaTables, pollSatisfiedChecksEachPollKind)
{
EXPECT_TRUE(nsm_fpga_poll_satisfied(NSM_FPGA_POLL_NONE, 0x1, 0));
EXPECT_TRUE(nsm_fpga_poll_satisfied(NSM_FPGA_POLL_SET, 0x3, 0x7));
EXPECT_FALSE(nsm_fpga_poll_satisfied(NSM_FPGA_POLL_SET, 0x3, 0x1));
EXPECT_TRUE(nsm_fpga_poll_satisfied(NSM_FPGA_POLL_ANY, 0xc, 0x4));
EXPECT_FALSE(nsm_fpga_poll_satisfied(NSM_FPGA_POLL_ANY, 0xc, 0x3));
EXPECT_FALSE(nsm_fpga_poll_satisfied(NSM_FPGA_POLL_ANY + 1, 0xc, 0x4));
}
TEST(fpgaTables, decodeNumericHandlesEveryFormat)
{
const double nan = std::numeric_limits<double>::quiet_NaN();
double value = 0.0;
/* a whole word, scaled */
const std::array<uint8_t, 4> word = {0x10, 0x00, 0x00, 0x00};
nsm_fpga_numeric_spec spec = {.sensor_type = "temperature",
.format = NSM_FPGA_FORMAT_FIXED,
.buffer = NSM_FPGA_BUF_ITEM,
.bit_width = 32,
.scale = 0.5,
.min_value = nan,
.max_value = 100.0};
ASSERT_TRUE(nsm_fpga_decode_numeric(&spec, word.data(), word.size(),
nullptr, 0, &value));
EXPECT_DOUBLE_EQ(value, 8.0);
/* above max is invalid */
spec.max_value = 7.0;
EXPECT_FALSE(nsm_fpga_decode_numeric(&spec, word.data(), word.size(),
nullptr, 0, &value));
/* FIXED: 11-bit signed field at bit 2, quarter degrees (IOSSM) */
const std::array<uint8_t, 4> iossm = {0x74, 0x02, 0x00, 0x00};
const nsm_fpga_numeric_spec fixed = {.sensor_type = "temperature",
.format = NSM_FPGA_FORMAT_FIXED,
.buffer = NSM_FPGA_BUF_ITEM,
.bit_shift = 2,
.bit_width = 11,
.is_signed = true,
.scale = 0.25,
.min_value = -40.0,
.max_value = 125.0};
ASSERT_TRUE(nsm_fpga_decode_numeric(&fixed, iossm.data(), iossm.size(),
nullptr, 0, &value));
EXPECT_DOUBLE_EQ(value, 39.25);
const std::array<uint8_t, 4> negative = {0xfc, 0x1f, 0x00, 0x00};
ASSERT_TRUE(nsm_fpga_decode_numeric(
&fixed, negative.data(), negative.size(), nullptr, 0, &value));
EXPECT_DOUBLE_EQ(value, -0.25);
/* SDM Q8.8 in the low 16 bits */
const std::array<uint8_t, 4> sdm = {0x60, 0x1e, 0x00, 0x00};
const nsm_fpga_numeric_spec q88 = {.sensor_type = "temperature",
.format = NSM_FPGA_FORMAT_FIXED,
.buffer = NSM_FPGA_BUF_ITEM,
.bit_shift = 0,
.bit_width = 16,
.is_signed = true,
.scale = 1.0 / 256.0,
.min_value = -40.0,
.max_value = 125.0};
ASSERT_TRUE(nsm_fpga_decode_numeric(&q88, sdm.data(), sdm.size(),
nullptr, 0, &value));
EXPECT_DOUBLE_EQ(value, 30.375);
/* big endian 16-bit, CMIS style */
const std::array<uint8_t, 2> be = {0x27, 0xc0};
const nsm_fpga_numeric_spec s16be = {.sensor_type = "temperature",
.format = NSM_FPGA_FORMAT_S16BE,
.buffer = NSM_FPGA_BUF_ITEM,
.scale = 1.0 / 256.0,
.min_value = nan,
.max_value = nan};
ASSERT_TRUE(nsm_fpga_decode_numeric(&s16be, be.data(), be.size(),
nullptr, 0, &value));
EXPECT_DOUBLE_EQ(value, 39.75);
/* a field that does not fit is invalid */
EXPECT_FALSE(
nsm_fpga_decode_numeric(&spec, word.data(), 2, nullptr, 0, &value));
EXPECT_FALSE(nsm_fpga_decode_numeric(nullptr, word.data(), word.size(),
nullptr, 0, &value));
}
TEST(fpgaTables, decodeElementWalksTheStride)
{
const double nan = std::numeric_limits<double>::quiet_NaN();
const std::array<uint8_t, 6> lanes = {0x00, 0x10, 0x00,
0x20, 0x00, 0x30};
const nsm_fpga_numeric_spec spec = {.sensor_type = "power",
.format = NSM_FPGA_FORMAT_U16BE,
.buffer = NSM_FPGA_BUF_ITEM,
.scale = 0.0001,
.min_value = nan,
.max_value = nan,
.element_count = 3,
.stride_bits = 16};
double value = 0.0;
ASSERT_TRUE(nsm_fpga_decode_element(&spec, 2, lanes.data(),
lanes.size(), nullptr, 0, &value));
EXPECT_DOUBLE_EQ(value, 0x30 * 0.0001);
EXPECT_FALSE(nsm_fpga_decode_element(&spec, 3, lanes.data(),
lanes.size(), nullptr, 0, &value));
}
TEST(fpgaTables, decodeElementWalksBitsOfAFixedField)
{
/* lane flags: bit i of the first byte, then the second byte */
const std::array<uint8_t, 2> flags = {0x05, 0x80};
const nsm_fpga_numeric_spec spec = {.sensor_type = "tx_fault",
.format = NSM_FPGA_FORMAT_FIXED,
.buffer = NSM_FPGA_BUF_ITEM,
.bit_width = 1,
.scale = 1.0,
.min_value = 0.0,
.max_value = 1.0,
.element_count = 32,
.stride_bits = 1};
double value = 0.0;
ASSERT_TRUE(nsm_fpga_decode_element(&spec, 0, flags.data(),
flags.size(), nullptr, 0, &value));
EXPECT_DOUBLE_EQ(value, 1.0);
ASSERT_TRUE(nsm_fpga_decode_element(&spec, 1, flags.data(),
flags.size(), nullptr, 0, &value));
EXPECT_DOUBLE_EQ(value, 0.0);
ASSERT_TRUE(nsm_fpga_decode_element(&spec, 15, flags.data(),
flags.size(), nullptr, 0, &value));
EXPECT_DOUBLE_EQ(value, 1.0);
/* the vector ends where the capture does */
EXPECT_FALSE(nsm_fpga_decode_element(&spec, 16, flags.data(),
flags.size(), nullptr, 0, &value));
/* a bit stride needs a FIXED field */
nsm_fpga_numeric_spec bytes = spec;
bytes.format = NSM_FPGA_FORMAT_U16BE;
EXPECT_FALSE(nsm_fpga_decode_element(&bytes, 0, flags.data(),
flags.size(), nullptr, 0, &value));
}
TEST(fpgaTables, commandsValidRequiresCheckRowsToReadABuffer)
{
const auto *p0 = nsm_fpga_map_p0();
const nsm_fpga_command check[] = {
{.op = NSM_FPGA_OP_CHECK,
.station = 1,
.expect = {.buffer = NSM_FPGA_BUF_GROUP,
.op = NSM_FPGA_CMP_EQ,
.width = 1,
.offset = 1,
.mask = 0x10,
.value = 0},
.on_expect_fail = NSM_FPGA_END_GROUP,
.group_state = NSM_FPGA_STATE_ABSENT}};
EXPECT_TRUE(nsm_fpga_commands_valid(p0, check, 1));
/* the state an ended group publishes must exist */
nsm_fpga_command odd = check[0];
odd.group_state = 9;
EXPECT_FALSE(nsm_fpga_commands_valid(p0, &odd, 1));
/* without a buffer a check has nothing to compare */
nsm_fpga_command bare = check[0];
bare.expect.buffer = NSM_FPGA_BUF_NONE;
EXPECT_FALSE(nsm_fpga_commands_valid(p0, &bare, 1));
}
TEST(fpgaTables, ruleValidChecksRunIfAndArraySpec)
{
const auto *p0 = nsm_fpga_map_p0();
static const nsm_fpga_command rows[] = {
{.op = NSM_FPGA_OP_BYTE,
.station = 1,
.capture = NSM_FPGA_BUF_ITEM,
.u = {.i2c = {0x50, 0,
NSM_FPGA_I2C_READ | NSM_FPGA_I2C_START |
NSM_FPGA_I2C_STOP | NSM_FPGA_I2C_READ_NACK}}}};
nsm_fpga_rule array = {.name = "Lanes",
.commands = rows,
.command_count = 1,
.kind = NSM_FPGA_RULE_ARRAY,
.numeric = {.format = NSM_FPGA_FORMAT_FIXED,
.buffer = NSM_FPGA_BUF_ITEM,
.bit_width = 1,
.scale = 1.0,
.element_count = 8,
.stride_bits = 1,
.vector = NSM_FPGA_VECTOR_TX_FAULT},
.run_if = {.buffer = NSM_FPGA_BUF_GROUP,
.op = NSM_FPGA_CMP_EQ,
.width = 1,
.offset = 5,
.mask = 0x0e,
.value = 0x06}};
EXPECT_TRUE(nsm_fpga_rule_valid(p0, &array));
array.numeric.element_count = 0;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &array));
array.numeric.element_count = 8;
array.numeric.format = NSM_FPGA_FORMAT_U16BE;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &array));
array.numeric.format = NSM_FPGA_FORMAT_FIXED;
/* run_if reads the group buffer only */
array.run_if.buffer = NSM_FPGA_BUF_ITEM;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &array));
array.run_if.buffer = NSM_FPGA_BUF_GROUP;
/* an array names the vector property it publishes */
array.numeric.vector = NSM_FPGA_VECTOR_NONE;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &array));
}
TEST(fpgaTables, decodeStateMapsTheWord)
{
const auto *p0 = nsm_fpga_map_p0();
/* a module state code under bits 1 to 3 */
static const nsm_fpga_state_entry moduleStates[] = {
{0x0e, 0x06, NSM_FPGA_STATE_ENABLED, NSM_FPGA_HEALTH_OK},
{0x0e, 0x04, NSM_FPGA_STATE_STARTING, NSM_FPGA_HEALTH_OK},
{0x0e, 0x02, NSM_FPGA_STATE_STANDBY_OFFLINE, NSM_FPGA_HEALTH_OK},
{0x0e, 0x0a, NSM_FPGA_STATE_FAULT, NSM_FPGA_HEALTH_CRITICAL},
{0, 0, NSM_FPGA_STATE_UNAVAILABLE_OFFLINE,
NSM_FPGA_HEALTH_WARNING}};
const nsm_fpga_rule module = {.name = "Module",
.kind = NSM_FPGA_RULE_STATE,
.numeric = {.buffer = NSM_FPGA_BUF_GROUP},
.states = moduleStates,
.state_count = 5};
EXPECT_TRUE(nsm_fpga_rule_valid(p0, &module));
uint8_t state = 0;
uint8_t health = 0;
EXPECT_TRUE(nsm_fpga_decode_state(&module, 0x06, &state, &health));
EXPECT_EQ(state, NSM_FPGA_STATE_ENABLED);
EXPECT_EQ(health, NSM_FPGA_HEALTH_OK);
EXPECT_TRUE(nsm_fpga_decode_state(&module, 0x0a, &state, &health));
EXPECT_EQ(state, NSM_FPGA_STATE_FAULT);
EXPECT_EQ(health, NSM_FPGA_HEALTH_CRITICAL);
EXPECT_TRUE(nsm_fpga_decode_state(&module, 0x02, &state, &health));
EXPECT_EQ(state, NSM_FPGA_STATE_STANDBY_OFFLINE);
/* an unknown code takes the map's default line */
EXPECT_TRUE(nsm_fpga_decode_state(&module, 0x0e, &state, &health));
EXPECT_EQ(state, NSM_FPGA_STATE_UNAVAILABLE_OFFLINE);
EXPECT_EQ(health, NSM_FPGA_HEALTH_WARNING);
/* a map without a default line matches nothing for other words */
static const nsm_fpga_state_entry ready[] = {
{0x8, 0x8, NSM_FPGA_STATE_ENABLED, NSM_FPGA_HEALTH_OK}};
const nsm_fpga_rule link = {.name = "Link",
.commands = &wordRead,
.command_count = 1,
.kind = NSM_FPGA_RULE_STATE,
.states = ready,
.state_count = 1};
EXPECT_TRUE(nsm_fpga_rule_valid(p0, &link));
EXPECT_TRUE(nsm_fpga_decode_state(&link, 0x18, &state, &health));
EXPECT_FALSE(nsm_fpga_decode_state(&link, 0x10, &state, &health));
/* a state rule needs a map, no other kind may carry one */
nsm_fpga_rule bare = link;
bare.state_count = 0;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &bare));
nsm_fpga_rule numeric = link;
numeric.kind = NSM_FPGA_RULE_NUMERIC;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &numeric));
/* a bad state or health code is refused */
static const nsm_fpga_state_entry odd[] = {
{0, 0, 9, NSM_FPGA_HEALTH_OK}};
nsm_fpga_rule broken = link;
broken.states = odd;
broken.state_count = 1;
EXPECT_FALSE(nsm_fpga_rule_valid(p0, &broken));
}
TEST(fpgaTables, decodeIdentityRendersHexAndText)
{
const std::array<uint8_t, 12> bytes = {
0x70, 0xb6, 0xc6, 0x45, 0x99, 0xa3, 0x7b, 0x20, 'A', 'B', ' ', ' '};
char text[NSM_FPGA_IDENTITY_TEXT_MAX];
const nsm_fpga_identity_field le = {.asset = NSM_FPGA_ASSET_SERIAL,
.format = NSM_FPGA_IDENTITY_HEX_LE,
.buffer = NSM_FPGA_BUF_ITEM,
.offset = 0,
.length = 8};
ASSERT_TRUE(nsm_fpga_decode_identity(&le, bytes.data(), bytes.size(),
nullptr, 0, text, sizeof(text)));
EXPECT_STREQ(text, "0x207ba39945c6b670");
const nsm_fpga_identity_field be = {.asset = NSM_FPGA_ASSET_SERIAL,
.format = NSM_FPGA_IDENTITY_HEX_BE,
.buffer = NSM_FPGA_BUF_ITEM,
.offset = 4,
.length = 4};
ASSERT_TRUE(nsm_fpga_decode_identity(&be, bytes.data(), bytes.size(),
nullptr, 0, text, sizeof(text)));
EXPECT_STREQ(text, "0x99a37b20");
const nsm_fpga_identity_field ascii = {.asset = NSM_FPGA_ASSET_PART,
.format =
NSM_FPGA_IDENTITY_ASCII,
.buffer = NSM_FPGA_BUF_ITEM,
.offset = 8,
.length = 4};
ASSERT_TRUE(nsm_fpga_decode_identity(&ascii, bytes.data(), bytes.size(),
nullptr, 0, text, sizeof(text)));
EXPECT_STREQ(text, "AB");
/* bytes beyond the buffer and a text that does not fit are refused */
EXPECT_FALSE(nsm_fpga_decode_identity(&le, bytes.data(), 7, nullptr, 0,
text, sizeof(text)));
EXPECT_FALSE(nsm_fpga_decode_identity(&be, bytes.data(), bytes.size(),
nullptr, 0, text, 10));
/* a rule without fields publishes the default serial number */
const nsm_fpga_rule rule = {.name = "Id",
.kind = NSM_FPGA_RULE_IDENTITY};
size_t count = 0;
const auto *fields = nsm_fpga_rule_identity(&rule, &count);
ASSERT_NE(fields, nullptr);
EXPECT_EQ(count, 1u);
EXPECT_EQ(fields[0].asset, NSM_FPGA_ASSET_SERIAL);
EXPECT_EQ(fields[0].length, 8);
/* an identity field with a bad format makes the rule invalid */
const nsm_fpga_identity_field bad = {.asset = NSM_FPGA_ASSET_SERIAL,
.format = 9,
.buffer = NSM_FPGA_BUF_ITEM,
.offset = 0,
.length = 4};
const nsm_fpga_command read = {.op = NSM_FPGA_OP_READ32,
.station = 29,
.capture = NSM_FPGA_BUF_ITEM,
.u = {.csr = {0x05000400ULL, 0}}};
const nsm_fpga_rule invalid = {.name = "Bad",
.commands = &read,
.command_count = 1,
.kind = NSM_FPGA_RULE_IDENTITY,
.identity = &bad,
.identity_count = 1};
EXPECT_FALSE(nsm_fpga_rule_valid(nsm_fpga_map_p0(), &invalid));
}
TEST(fpgaTables, stationBySelectorGivesSdmTheStrictKind)
{
const auto *p0 = nsm_fpga_map_p0();
const auto *sdm = nsm_fpga_station_by_selector(p0, 37);
ASSERT_NE(sdm, nullptr);
EXPECT_EQ(sdm->kind, NSM_FPGA_STATION_SDM);
}
TEST(fpgaTables, sdmWordLegalMirrorsThePolicy)
{
/* reads only at offsets 8, 20, 24, 32 */
EXPECT_TRUE(nsm_fpga_sdm_word_legal(NSM_FPGA_OP_READ32, 8, 0, 0xf));
EXPECT_TRUE(nsm_fpga_sdm_word_legal(NSM_FPGA_OP_READ32, 20, 0, 0xf));
EXPECT_TRUE(nsm_fpga_sdm_word_legal(NSM_FPGA_OP_READ32, 24, 0, 0xf));
EXPECT_TRUE(nsm_fpga_sdm_word_legal(NSM_FPGA_OP_READ32, 32, 0, 0xf));
EXPECT_FALSE(nsm_fpga_sdm_word_legal(NSM_FPGA_OP_READ32, 12, 0, 0xf));
/* temperature and voltage take the two-word form: header then argument.
The client field (bits 24 to 26) is ignored by the policy, so the
sent form with client 7 and the bare form both pass. */
EXPECT_TRUE(
nsm_fpga_sdm_word_legal(NSM_FPGA_OP_WRITE32, 0, 0x07001019u, 0xf));
EXPECT_TRUE(
nsm_fpga_sdm_word_legal(NSM_FPGA_OP_WRITE32, 0, 0x07001018u, 0xf));
EXPECT_TRUE(
nsm_fpga_sdm_word_legal(NSM_FPGA_OP_WRITE32, 0, 0x00001019u, 0xf));
EXPECT_TRUE(
nsm_fpga_sdm_word_legal(NSM_FPGA_OP_WRITE32, 0, 0x00001018u, 0xf));
EXPECT_TRUE(
nsm_fpga_sdm_word_legal(NSM_FPGA_OP_WRITE32, 4, 0x00000001u, 0xf));
/* the argument-less codes keep the single offset-4 header write */
EXPECT_TRUE(
nsm_fpga_sdm_word_legal(NSM_FPGA_OP_WRITE32, 4, 0x07000010u, 0xf));
EXPECT_TRUE(
nsm_fpga_sdm_word_legal(NSM_FPGA_OP_WRITE32, 4, 0x07000012u, 0xf));
EXPECT_TRUE(
nsm_fpga_sdm_word_legal(NSM_FPGA_OP_WRITE32, 4, 0x07000013u, 0xf));
/* the argument-less temperature header at offset 4 faults: rejected */
EXPECT_FALSE(
nsm_fpga_sdm_word_legal(NSM_FPGA_OP_WRITE32, 4, 0x07000019u, 0xf));
/* a temperature header at the wrong offset, or wrong mask, is rejected
*/
EXPECT_FALSE(
nsm_fpga_sdm_word_legal(NSM_FPGA_OP_WRITE32, 0, 0x00001010u, 0xf));
EXPECT_FALSE(
nsm_fpga_sdm_word_legal(NSM_FPGA_OP_WRITE32, 4, 0x00000001u, 0x3));
}
TEST(fpgaTables, wordLegalGuardsStationAndWindow)
{
const auto *p0 = nsm_fpga_map_p0();
/* SDM station: the old argument-less temperature write is rejected */
EXPECT_FALSE(nsm_fpga_word_legal(p0, 37, NSM_FPGA_OP_WRITE32, 4,
0x07000019u, 0xf));
/* the two-word temperature form is allowed */
EXPECT_TRUE(nsm_fpga_word_legal(p0, 37, NSM_FPGA_OP_WRITE32, 0,
0x00001019u, 0xf));
EXPECT_TRUE(nsm_fpga_word_legal(p0, 37, NSM_FPGA_OP_WRITE32, 4,
0x00000001u, 0xf));
EXPECT_TRUE(
nsm_fpga_word_legal(p0, 37, NSM_FPGA_OP_READ32, 24, 0, 0xf));
/* plain word station: alignment and window bounds enforced */
EXPECT_TRUE(nsm_fpga_word_legal(p0, 29, NSM_FPGA_OP_READ32,
0x05000400ULL, 0, 0xf));
EXPECT_FALSE(nsm_fpga_word_legal(p0, 29, NSM_FPGA_OP_READ32,
0x05000401ULL, 0, 0xf));
EXPECT_FALSE(nsm_fpga_word_legal(p0, 29, NSM_FPGA_OP_READ32,
0x05000900ULL, 0, 0xf));
/* an unknown station is never legal */
EXPECT_FALSE(
nsm_fpga_word_legal(p0, 38, NSM_FPGA_OP_READ32, 8, 0, 0xf));
}
namespace
{
/* The numeric decode model, computed independently of the library */
bool modelField(const nsm_fpga_numeric_spec &spec, const uint8_t *bytes,
size_t length, size_t offset, unsigned shift, double *value)
{
if (spec.buffer != NSM_FPGA_BUF_ITEM &&
spec.buffer != NSM_FPGA_BUF_GROUP) {
return false;
}
if (spec.format == NSM_FPGA_FORMAT_FIXED &&
(spec.bit_width == 0 || spec.bit_width > 32 ||
shift + spec.bit_width > 32)) {
return false;
}
const size_t width = spec.format == NSM_FPGA_FORMAT_FIXED
? (shift + spec.bit_width + 7) / 8
: 2;
if (offset > length || width > length - offset) {
return false;
}
uint64_t raw = 0;
for (size_t i = 0; i < width; ++i) {
raw |= (uint64_t)bytes[offset + i] << (8 * i);
}
double decoded = 0.0;
if (spec.format == NSM_FPGA_FORMAT_FIXED) {
uint64_t field =
(raw >> shift) & (((uint64_t)1 << spec.bit_width) - 1);
if (spec.is_signed &&
(field & ((uint64_t)1 << (spec.bit_width - 1))) != 0) {
field -= (uint64_t)1 << spec.bit_width;
}
decoded =
spec.is_signed ? (double)(int64_t)field : (double)field;
} else {
const uint64_t swapped =
((raw & 0xff) << 8) | ((raw >> 8) & 0xff);
decoded = spec.format == NSM_FPGA_FORMAT_S16BE
? (double)(int16_t)swapped
: (double)swapped;
}
decoded = decoded * spec.scale + spec.bias;
if (!std::isfinite(decoded) ||
(!std::isnan(spec.min_value) && decoded < spec.min_value) ||
(!std::isnan(spec.max_value) && decoded > spec.max_value)) {
return false;
}
*value = decoded;
return true;
}
/* A deterministic byte pattern varied by seed */
std::vector<uint8_t> patternBytes(size_t size, uint8_t seed)
{
std::vector<uint8_t> bytes(size);
for (size_t i = 0; i < size; ++i) {
bytes[i] = (uint8_t)(seed + i * 7);
}
return bytes;
}
} // namespace
TEST(fpgaTables, decodeNumericMatchesTheFormulaOnEveryPart)
{
const auto *p0 = nsm_fpga_map_p0();
const uint8_t seeds[] = {0x00, 0xff, 0x35};
for (size_t p = 0; p < p0->part_count; ++p) {
const auto *part = p0->parts[p];
for (size_t i = 0; i < part->rule_count; ++i) {
const auto &rule = part->rules[i];
if (rule.kind != NSM_FPGA_RULE_NUMERIC) {
continue;
}
const auto &spec = rule.numeric;
EXPECT_NE(spec.sensor_type, nullptr) << rule.name;
for (const uint8_t seed : seeds) {
auto bytes =
patternBytes(spec.offset + 8, seed);
double expected = 0.0;
const bool held = modelField(
spec, bytes.data(), bytes.size(),
spec.offset, spec.bit_shift, &expected);
double value = 0.0;
const bool decoded = nsm_fpga_decode_numeric(
&spec, bytes.data(), bytes.size(),
bytes.data(), bytes.size(), &value);
EXPECT_EQ(decoded, held) << rule.name;
if (decoded && held) {
EXPECT_DOUBLE_EQ(value, expected)
<< rule.name;
}
}
}
}
}
TEST(fpgaTables, decodeStateFindsTheFirstMatchOnEveryPart)
{
const auto *p0 = nsm_fpga_map_p0();
for (size_t p = 0; p < p0->part_count; ++p) {
const auto *part = p0->parts[p];
for (size_t i = 0; i < part->rule_count; ++i) {
const auto &rule = part->rules[i];
if (rule.kind != NSM_FPGA_RULE_STATE) {
continue;
}
for (size_t e = 0; e < rule.state_count; ++e) {
const uint32_t word = rule.states[e].value;
const nsm_fpga_state_entry *first = nullptr;
for (size_t j = 0; j < rule.state_count; ++j) {
if ((word & rule.states[j].mask) ==
rule.states[j].value) {
first = &rule.states[j];
break;
}
}
uint8_t state = 0xff;
uint8_t health = 0xff;
const bool decoded = nsm_fpga_decode_state(
&rule, word, &state, &health);
EXPECT_EQ(decoded, first != nullptr)
<< rule.name;
if (decoded && first != nullptr) {
EXPECT_EQ(state, first->state)
<< rule.name;
EXPECT_EQ(health, first->health)
<< rule.name;
}
}
}
}
}
TEST(fpgaTables, decodeElementWalksEveryArraySpecOfEveryPart)
{
const auto *p0 = nsm_fpga_map_p0();
for (size_t p = 0; p < p0->part_count; ++p) {
const auto *part = p0->parts[p];
for (size_t i = 0; i < part->rule_count; ++i) {
const auto &rule = part->rules[i];
if (rule.kind != NSM_FPGA_RULE_ARRAY) {
continue;
}
const auto &spec = rule.numeric;
const size_t bits =
spec.bit_shift +
spec.element_count * spec.stride_bits;
auto bytes =
patternBytes(spec.offset + bits / 8 + 8, 0x5a);
for (size_t e = 0; e < spec.element_count; ++e) {
const size_t bit =
spec.bit_shift + e * spec.stride_bits;
double expected = 0.0;
const bool held =
modelField(spec, bytes.data(), bytes.size(),
spec.offset + bit / 8,
(unsigned)(bit % 8), &expected);
double value = 0.0;
const bool decoded = nsm_fpga_decode_element(
&spec, e, bytes.data(), bytes.size(),
bytes.data(), bytes.size(), &value);
EXPECT_EQ(decoded, held) << rule.name;
if (decoded && held) {
EXPECT_DOUBLE_EQ(value, expected)
<< rule.name;
}
}
double value = 0.0;
EXPECT_FALSE(nsm_fpga_decode_element(
&spec, spec.element_count, bytes.data(),
bytes.size(), bytes.data(), bytes.size(), &value))
<< rule.name;
}
}
}
TEST(fpgaTables, decodeIdentityRendersEveryFieldOfEveryPart)
{
const auto *p0 = nsm_fpga_map_p0();
for (size_t p = 0; p < p0->part_count; ++p) {
const auto *part = p0->parts[p];
for (size_t i = 0; i < part->rule_count; ++i) {
const auto &rule = part->rules[i];
if (rule.kind != NSM_FPGA_RULE_IDENTITY) {
continue;
}
size_t count = 0;
const auto *fields =
nsm_fpga_rule_identity(&rule, &count);
ASSERT_NE(fields, nullptr);
for (size_t f = 0; f < count; ++f) {
const auto &field = fields[f];
std::vector<uint8_t> bytes(field.offset +
field.length);
for (size_t b = 0; b < bytes.size(); ++b) {
bytes[b] = (uint8_t)('A' + (b % 26));
}
const uint8_t *data =
bytes.data() + field.offset;
std::string expected;
if (field.format == NSM_FPGA_IDENTITY_ASCII) {
expected.assign((const char *)data,
field.length);
} else {
static const char digits[] =
"0123456789abcdef";
expected = "0x";
for (size_t b = 0; b < field.length;
++b) {
const uint8_t byte =
field.format ==
NSM_FPGA_IDENTITY_HEX_LE
? data[field.length -
1 - b]
: data[b];
expected.push_back(
digits[byte >> 4]);
expected.push_back(
digits[byte & 0xf]);
}
}
char out[NSM_FPGA_IDENTITY_TEXT_MAX] = {};
EXPECT_TRUE(nsm_fpga_decode_identity(
&field, bytes.data(), bytes.size(),
bytes.data(), bytes.size(), out,
sizeof(out)))
<< rule.name;
EXPECT_STREQ(out, expected.c_str());
}
}
}
}
TEST(fpgaTables, i2cArgumentAndXferEncodersPackEveryWireRow)
{
const auto *p0 = nsm_fpga_map_p0();
const auto checkRows = [](const nsm_fpga_command *rows, size_t count,
const char *name) {
for (size_t c = 0; c < count; ++c) {
const auto &cmd = rows[c];
if (cmd.op == NSM_FPGA_OP_BYTE) {
const auto &op = cmd.u.i2c;
const uint32_t argument =
(uint32_t)(op.dev_addr & 0x7f) |
((uint32_t)op.data << 8) |
((uint32_t)(op.flags & 0xf) << 16);
EXPECT_EQ(nsm_fpga_i2c_argument(&op), argument)
<< name;
EXPECT_EQ(op.dev_addr & 0x80, 0) << name;
} else if (cmd.op == NSM_FPGA_OP_I2C_XFER) {
const auto &op = cmd.u.xfer;
const uint8_t opcode =
(uint8_t)(0x40 |
((op.write_count & 0x3) << 3) |
(op.read_count & 0x7));
EXPECT_EQ(nsm_fpga_xfer_opcode(&op), opcode)
<< name;
uint32_t argument = op.dev_addr & 0x7fu;
for (size_t w = 0; w < op.write_count && w < 3;
++w) {
argument |= (uint32_t)op.write[w]
<< (8 * (w + 1));
}
EXPECT_EQ(
nsm_fpga_xfer_argument(
op.dev_addr, op.write, op.write_count),
argument)
<< name;
}
}
};
for (size_t p = 0; p < p0->part_count; ++p) {
const auto *part = p0->parts[p];
for (size_t g = 0; g < part->group_count; ++g) {
const auto &group = part->groups[g];
checkRows(group.setup, group.setup_count, group.name);
checkRows(group.cleanup, group.cleanup_count,
group.name);
}
for (size_t i = 0; i < part->rule_count; ++i) {
const auto &rule = part->rules[i];
checkRows(rule.commands, rule.command_count, rule.name);
checkRows(rule.cleanup, rule.cleanup_count, rule.name);
}
}
}