| @@ -238,12 +238,7 @@ void RuntimeModeService::refreshPlcPollAddresses() | |||
| } | |||
| for (const LogicNode *node : nodes) | |||
| { | |||
| const std::optional<RegisterAddress> address = | |||
| registerAddressForLogicNode(node->config); | |||
| if (address.has_value()) | |||
| { | |||
| addresses.push_back(*address); | |||
| } | |||
| collectRegisterAddressesForLogicNode(node->config, &addresses); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,6 +1,7 @@ | |||
| #include "software_logic_executor.h" | |||
| #include <algorithm> | |||
| #include <limits> | |||
| #include <map> | |||
| #include <type_traits> | |||
| @@ -76,6 +77,8 @@ void LogicTraceValues::clear() | |||
| expressionValues.clear(); | |||
| rungValues.clear(); | |||
| tonValues.clear(); | |||
| counterValues.clear(); | |||
| wordValues.clear(); | |||
| } | |||
| void LogicTraceSnapshot::clear() | |||
| @@ -95,6 +98,8 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( | |||
| projection.expressionValues = values->second.expressionValues; | |||
| projection.rungValues = values->second.rungValues; | |||
| projection.tonValues = values->second.tonValues; | |||
| projection.counterValues = values->second.counterValues; | |||
| projection.wordValues = values->second.wordValues; | |||
| } | |||
| return projection; | |||
| } | |||
| @@ -133,13 +138,17 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||
| const auto *output = std::get_if<CoilNodeConfig>(&rung.output->config); | |||
| if (output == nullptr) | |||
| { | |||
| if (std::holds_alternative<TonNodeConfig>(rung.output->config)) | |||
| if (std::holds_alternative<TonNodeConfig>(rung.output->config) | |||
| || std::holds_alternative<CounterNodeConfig>(rung.output->config) | |||
| || std::holds_alternative<MoveNodeConfig>(rung.output->config) | |||
| || std::holds_alternative<ArithmeticNodeConfig>( | |||
| rung.output->config)) | |||
| { | |||
| continue; | |||
| } | |||
| return failure( | |||
| LogicScanError::InvalidLogic, | |||
| "梯形图输出不是线圈或 TON 指令", | |||
| "梯形图输出不是有效的输出指令", | |||
| logic.id, | |||
| rung.id, | |||
| rung.output->id); | |||
| @@ -158,10 +167,10 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||
| output_modes[output->address.index()] = output->mode; | |||
| } | |||
| } | |||
| std::string timer_error; | |||
| if (!validateTimerReferencesForRunning(logics, &timer_error)) | |||
| std::string resource_error; | |||
| if (!validateLogicResourceReferencesForRunning(logics, &resource_error)) | |||
| { | |||
| return failure(LogicScanError::InvalidLogic, timer_error); | |||
| return failure(LogicScanError::InvalidLogic, resource_error); | |||
| } | |||
| return success(); | |||
| } | |||
| @@ -169,7 +178,9 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||
| void SoftwareLogicExecutor::resetRuntime() | |||
| { | |||
| previous_edge_inputs_.clear(); | |||
| previous_counter_inputs_.clear(); | |||
| ton_states_.clear(); | |||
| counter_states_.clear(); | |||
| } | |||
| LogicScanResult SoftwareLogicExecutor::executeScan( | |||
| @@ -227,6 +238,7 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||
| } | |||
| bool output_value = false; | |||
| result = executeOutput( | |||
| logic.id, | |||
| *rung.output, | |||
| rung_value, | |||
| repository, | |||
| @@ -259,6 +271,8 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||
| trace->expressionValues = values->second.expressionValues; | |||
| trace->rungValues = values->second.rungValues; | |||
| trace->tonValues = values->second.tonValues; | |||
| trace->counterValues = values->second.counterValues; | |||
| trace->wordValues = values->second.wordValues; | |||
| } | |||
| } | |||
| } | |||
| @@ -371,6 +385,12 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||
| *value = config.mode == ContactMode::NormallyOpen ? done : !done; | |||
| return success(); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CounterContactNodeConfig>) | |||
| { | |||
| const bool done = counter_states_[config.address.index()].done; | |||
| *value = config.mode == ContactMode::NormallyOpen ? done : !done; | |||
| return success(); | |||
| } | |||
| else if constexpr (std::is_same_v<Config, CompareNodeConfig>) | |||
| { | |||
| const WordReadResult read = repository.readWord(config.address); | |||
| @@ -399,7 +419,51 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||
| node.config); | |||
| } | |||
| LogicScanResult SoftwareLogicExecutor::readWordOperand( | |||
| const WordOperand &operand, | |||
| RegisterRepository &repository, | |||
| std::int16_t *value, | |||
| const std::string &node_id) const | |||
| { | |||
| if (value == nullptr) | |||
| { | |||
| return failure( | |||
| LogicScanError::InvalidLogic, | |||
| "缺少字操作数结果接收对象", | |||
| {}, | |||
| {}, | |||
| node_id); | |||
| } | |||
| if (operand.kind == WordOperandKind::Constant) | |||
| { | |||
| *value = operand.constant; | |||
| return success(); | |||
| } | |||
| if (operand.kind != WordOperandKind::Register) | |||
| { | |||
| return failure( | |||
| LogicScanError::InvalidLogic, | |||
| "字操作数类型无效", | |||
| {}, | |||
| {}, | |||
| node_id); | |||
| } | |||
| const WordReadResult read = repository.readWord(operand.address); | |||
| if (!read.succeeded) | |||
| { | |||
| return failure( | |||
| LogicScanError::RegisterReadFailed, | |||
| "读取寄存器失败:" + operand.address.toString(), | |||
| {}, | |||
| {}, | |||
| node_id); | |||
| } | |||
| *value = read.value; | |||
| return success(); | |||
| } | |||
| LogicScanResult SoftwareLogicExecutor::executeOutput( | |||
| const std::string &logic_id, | |||
| const LogicNode &node, | |||
| bool rung_value, | |||
| RegisterRepository &repository, | |||
| @@ -448,12 +512,191 @@ LogicScanResult SoftwareLogicExecutor::executeOutput( | |||
| return success(); | |||
| } | |||
| if (const auto *counter = std::get_if<CounterNodeConfig>(&node.config)) | |||
| { | |||
| const BitReadResult reset_read = repository.readBit(counter->resetAddress); | |||
| if (!reset_read.succeeded) | |||
| { | |||
| return failure( | |||
| LogicScanError::RegisterReadFailed, | |||
| "读取计数器复位输入失败:" + counter->resetAddress.toString(), | |||
| {}, | |||
| {}, | |||
| node.id); | |||
| } | |||
| std::int16_t preset = 0; | |||
| LogicScanResult result = readWordOperand( | |||
| counter->preset, repository, &preset, node.id); | |||
| if (!result.succeeded) | |||
| { | |||
| return result; | |||
| } | |||
| const std::int32_t bounded_preset = std::max<std::int32_t>(0, preset); | |||
| preset = static_cast<std::int16_t>( | |||
| std::min<std::int32_t>(bounded_preset, std::numeric_limits<std::int16_t>::max())); | |||
| const WordReadResult current_read = repository.readWord( | |||
| counter->currentValueAddress); | |||
| if (!current_read.succeeded) | |||
| { | |||
| return failure( | |||
| LogicScanError::RegisterReadFailed, | |||
| "读取计数当前值失败:" + counter->currentValueAddress.toString(), | |||
| {}, | |||
| {}, | |||
| node.id); | |||
| } | |||
| std::int32_t current = std::max<std::int32_t>(0, current_read.value); | |||
| current = std::min<std::int32_t>(current, std::numeric_limits<std::int16_t>::max()); | |||
| const auto runtime_key = std::make_pair(logic_id, node.id); | |||
| const bool previous = previous_counter_inputs_[runtime_key]; | |||
| const bool rising = rung_value && !previous; | |||
| previous_counter_inputs_[runtime_key] = rung_value; | |||
| bool done = counter_states_[counter->address.index()].done; | |||
| if (reset_read.value) | |||
| { | |||
| current = counter->mode == CounterMode::Up ? 0 : preset; | |||
| done = counter->mode == CounterMode::Down && current == 0; | |||
| } | |||
| else if (rising) | |||
| { | |||
| if (counter->mode == CounterMode::Up) | |||
| { | |||
| if (current < preset) | |||
| { | |||
| ++current; | |||
| } | |||
| done = current >= preset; | |||
| } | |||
| else | |||
| { | |||
| if (current > 0) | |||
| { | |||
| --current; | |||
| } | |||
| done = current <= 0; | |||
| } | |||
| } | |||
| if (counter->mode == CounterMode::Up && current >= preset) | |||
| { | |||
| done = true; | |||
| } | |||
| if (counter->mode == CounterMode::Down && current <= 0) | |||
| { | |||
| done = true; | |||
| } | |||
| const std::int16_t current_value = static_cast<std::int16_t>(current); | |||
| const RegisterWriteResult write = repository.writeWord( | |||
| counter->currentValueAddress, current_value); | |||
| if (!write.succeeded) | |||
| { | |||
| return failure( | |||
| LogicScanError::RegisterWriteFailed, | |||
| "写入计数当前值失败:" + counter->currentValueAddress.toString(), | |||
| {}, | |||
| {}, | |||
| node.id); | |||
| } | |||
| counter_states_[counter->address.index()].done = done; | |||
| *output_value = done; | |||
| if (trace != nullptr) | |||
| { | |||
| trace->counterValues[node.id] = { | |||
| rung_value, | |||
| reset_read.value, | |||
| done, | |||
| current_value, | |||
| preset}; | |||
| } | |||
| return success(); | |||
| } | |||
| if (const auto *move = std::get_if<MoveNodeConfig>(&node.config)) | |||
| { | |||
| if (!rung_value) | |||
| { | |||
| *output_value = false; | |||
| return success(); | |||
| } | |||
| std::int16_t source = 0; | |||
| LogicScanResult result = readWordOperand( | |||
| move->source, repository, &source, node.id); | |||
| if (!result.succeeded) | |||
| { | |||
| return result; | |||
| } | |||
| const RegisterWriteResult write = repository.writeWord( | |||
| move->destination, source); | |||
| if (!write.succeeded) | |||
| { | |||
| return failure( | |||
| LogicScanError::RegisterWriteFailed, | |||
| "MOVE 写入寄存器失败:" + move->destination.toString(), | |||
| {}, | |||
| {}, | |||
| node.id); | |||
| } | |||
| *output_value = true; | |||
| if (trace != nullptr) | |||
| { | |||
| trace->wordValues[node.id] = {source, false}; | |||
| } | |||
| return success(); | |||
| } | |||
| if (const auto *arithmetic = std::get_if<ArithmeticNodeConfig>(&node.config)) | |||
| { | |||
| if (!rung_value) | |||
| { | |||
| *output_value = false; | |||
| return success(); | |||
| } | |||
| std::int16_t left = 0; | |||
| std::int16_t right = 0; | |||
| LogicScanResult result = readWordOperand( | |||
| arithmetic->left, repository, &left, node.id); | |||
| if (!result.succeeded) | |||
| { | |||
| return result; | |||
| } | |||
| result = readWordOperand(arithmetic->right, repository, &right, node.id); | |||
| if (!result.succeeded) | |||
| { | |||
| return result; | |||
| } | |||
| const std::int32_t raw = arithmetic->operation == ArithmeticOperation::Add | |||
| ? static_cast<std::int32_t>(left) + right | |||
| : static_cast<std::int32_t>(left) - right; | |||
| const bool overflow = raw < std::numeric_limits<std::int16_t>::min() | |||
| || raw > std::numeric_limits<std::int16_t>::max(); | |||
| const std::int32_t bounded = std::max<std::int32_t>( | |||
| std::numeric_limits<std::int16_t>::min(), | |||
| std::min<std::int32_t>(raw, std::numeric_limits<std::int16_t>::max())); | |||
| const std::int16_t result_value = static_cast<std::int16_t>(bounded); | |||
| const RegisterWriteResult write = repository.writeWord( | |||
| arithmetic->destination, result_value); | |||
| if (!write.succeeded) | |||
| { | |||
| return failure( | |||
| LogicScanError::RegisterWriteFailed, | |||
| "算术指令写入寄存器失败:" + arithmetic->destination.toString(), | |||
| {}, | |||
| {}, | |||
| node.id); | |||
| } | |||
| *output_value = true; | |||
| if (trace != nullptr) | |||
| { | |||
| trace->wordValues[node.id] = {result_value, overflow}; | |||
| } | |||
| return success(); | |||
| } | |||
| const auto *config = std::get_if<CoilNodeConfig>(&node.config); | |||
| if (config == nullptr) | |||
| { | |||
| return failure( | |||
| LogicScanError::InvalidLogic, | |||
| "梯形图输出不是线圈或 TON 指令", | |||
| "梯形图输出不是有效的输出指令", | |||
| {}, | |||
| {}, | |||
| node.id); | |||
| @@ -38,12 +38,29 @@ struct TonTraceValue | |||
| int presetMs = 0; | |||
| }; | |||
| struct CounterTraceValue | |||
| { | |||
| bool input = false; | |||
| bool reset = false; | |||
| bool done = false; | |||
| std::int16_t value = 0; | |||
| std::int16_t preset = 0; | |||
| }; | |||
| struct WordTraceValue | |||
| { | |||
| std::int16_t value = 0; | |||
| bool overflow = false; | |||
| }; | |||
| struct LogicTraceValues | |||
| { | |||
| std::unordered_map<std::string, bool> nodeValues; | |||
| std::unordered_map<std::string, bool> expressionValues; | |||
| std::unordered_map<std::string, bool> rungValues; | |||
| std::unordered_map<std::string, TonTraceValue> tonValues; | |||
| std::unordered_map<std::string, CounterTraceValue> counterValues; | |||
| std::unordered_map<std::string, WordTraceValue> wordValues; | |||
| void clear(); | |||
| }; | |||
| @@ -96,13 +113,25 @@ private: | |||
| LogicTraceValues *trace, | |||
| bool *value); | |||
| LogicScanResult executeOutput( | |||
| const std::string &logic_id, | |||
| const LogicNode &node, | |||
| bool rung_value, | |||
| RegisterRepository &repository, | |||
| TimePoint now, | |||
| LogicTraceValues *trace, | |||
| bool *output_value); | |||
| LogicScanResult readWordOperand( | |||
| const WordOperand &operand, | |||
| RegisterRepository &repository, | |||
| std::int16_t *value, | |||
| const std::string &node_id) const; | |||
| std::map<std::pair<std::string, std::string>, bool> previous_edge_inputs_; | |||
| std::map<std::pair<std::string, std::string>, bool> previous_counter_inputs_; | |||
| std::unordered_map<int, TonRuntimeState> ton_states_; | |||
| struct CounterRuntimeState | |||
| { | |||
| bool done = false; | |||
| }; | |||
| std::unordered_map<int, CounterRuntimeState> counter_states_; | |||
| }; | |||
| @@ -44,11 +44,84 @@ LogicNode timerContact( | |||
| return {id, TimerContactNodeConfig{TimerAddress{timer_index}, mode}, true}; | |||
| } | |||
| LogicNode counterContact( | |||
| const std::string &id, | |||
| int counter_index, | |||
| ContactMode mode = ContactMode::NormallyOpen) | |||
| { | |||
| return {id, CounterContactNodeConfig{CounterAddress{counter_index}, mode}, true}; | |||
| } | |||
| LogicNode ton(const std::string &id, int timer_index, int preset_ms) | |||
| { | |||
| return {id, TonNodeConfig{TimerAddress{timer_index}, preset_ms}, true}; | |||
| } | |||
| WordOperand constantOperand(std::int16_t value) | |||
| { | |||
| return { | |||
| WordOperandKind::Constant, | |||
| RegisterAddress{RegisterArea::D, 0}, | |||
| value}; | |||
| } | |||
| WordOperand registerOperand(int address) | |||
| { | |||
| return { | |||
| WordOperandKind::Register, | |||
| RegisterAddress{RegisterArea::D, address}, | |||
| 0}; | |||
| } | |||
| LogicNode counter( | |||
| const std::string &id, | |||
| int counter_index, | |||
| CounterMode mode, | |||
| int current_address, | |||
| WordOperand preset, | |||
| int reset_address) | |||
| { | |||
| return { | |||
| id, | |||
| CounterNodeConfig{ | |||
| CounterAddress{counter_index}, | |||
| mode, | |||
| RegisterAddress{RegisterArea::D, current_address}, | |||
| preset, | |||
| RegisterAddress{RegisterArea::M, reset_address}}, | |||
| true}; | |||
| } | |||
| LogicNode move( | |||
| const std::string &id, | |||
| WordOperand source, | |||
| int destination) | |||
| { | |||
| return { | |||
| id, | |||
| MoveNodeConfig{ | |||
| source, | |||
| RegisterAddress{RegisterArea::D, destination}}, | |||
| true}; | |||
| } | |||
| LogicNode arithmetic( | |||
| const std::string &id, | |||
| ArithmeticOperation operation, | |||
| WordOperand left, | |||
| WordOperand right, | |||
| int destination) | |||
| { | |||
| return { | |||
| id, | |||
| ArithmeticNodeConfig{ | |||
| operation, | |||
| left, | |||
| right, | |||
| RegisterAddress{RegisterArea::D, destination}}, | |||
| true}; | |||
| } | |||
| LogicNode comparison(const std::string &id, int address, | |||
| ComparisonOperator operation, std::int16_t value) | |||
| { | |||
| @@ -132,6 +205,14 @@ void writeWord(RegisterRepository &repository, int address, std::int16_t value) | |||
| "test word write must succeed"); | |||
| } | |||
| std::int16_t readWord(RegisterRepository &repository, int address) | |||
| { | |||
| const WordReadResult result = repository.readWord( | |||
| RegisterAddress{RegisterArea::D, address}); | |||
| require(result.succeeded, "test word read must succeed"); | |||
| return result.value; | |||
| } | |||
| void testNestedSeriesParallelExpression() | |||
| { | |||
| VirtualRegisterRepository repository; | |||
| @@ -462,6 +543,165 @@ void testMultipleTimersAndNetworkOrder() | |||
| "a later scan must observe the completed TON through T contact"); | |||
| } | |||
| void testCountersUseRisingEdgesAndExternalDValues() | |||
| { | |||
| VirtualRegisterRepository repository; | |||
| SoftwareLogicExecutor executor; | |||
| const ControlLogic up_program = logic({ | |||
| rung( | |||
| "ctu-rung", | |||
| {{contact("count-input", 0)}}, | |||
| counter( | |||
| "ctu-0", | |||
| 0, | |||
| CounterMode::Up, | |||
| 0, | |||
| constantOperand(3), | |||
| 1)), | |||
| rung( | |||
| "ctu-done-rung", | |||
| {{counterContact("ctu-done", 0)}}, | |||
| coil("ctu-done-output", 2))}); | |||
| LogicTraceSnapshot trace; | |||
| require(executor.executeScan({up_program}, repository, &trace).succeeded, | |||
| "an inactive CTU must scan successfully"); | |||
| require(readWord(repository, 0) == 0 && !readBit(repository, 2), | |||
| "an inactive CTU must keep CV zero and Q false"); | |||
| writeBit(repository, 0, true); | |||
| require(executor.executeScan({up_program}, repository, &trace).succeeded, | |||
| "the first CTU rising edge must scan successfully"); | |||
| require(readWord(repository, 0) == 1, | |||
| "CTU must increment the external D current value on a rising edge"); | |||
| require(executor.executeScan({up_program}, repository, &trace).succeeded, | |||
| "a held CTU input must scan successfully"); | |||
| require(readWord(repository, 0) == 1, | |||
| "a held CTU input must not count every scan"); | |||
| for (int expected = 2; expected <= 3; ++expected) | |||
| { | |||
| writeBit(repository, 0, false); | |||
| require(executor.executeScan({up_program}, repository).succeeded, | |||
| "CTU falling preparation scan must succeed"); | |||
| writeBit(repository, 0, true); | |||
| require(executor.executeScan({up_program}, repository, &trace).succeeded, | |||
| "CTU repeated rising edge must succeed"); | |||
| require(readWord(repository, 0) == expected, | |||
| "CTU must increment exactly once for each rising edge"); | |||
| } | |||
| require(readBit(repository, 2) | |||
| && trace.counterValues.at("ctu-0").done, | |||
| "the C contact must observe CTU completion in a later network"); | |||
| writeBit(repository, 1, true); | |||
| require(executor.executeScan({up_program}, repository, &trace).succeeded, | |||
| "CTU reset must scan successfully"); | |||
| require(readWord(repository, 0) == 0 && !readBit(repository, 2), | |||
| "CTU reset must clear CV and the C completion state"); | |||
| executor.resetRuntime(); | |||
| repository.clear(); | |||
| const ControlLogic down_program = logic({ | |||
| rung( | |||
| "ctd-rung", | |||
| {{contact("down-input", 3)}}, | |||
| counter( | |||
| "ctd-1", | |||
| 1, | |||
| CounterMode::Down, | |||
| 1, | |||
| constantOperand(2), | |||
| 4)), | |||
| rung( | |||
| "ctd-done-rung", | |||
| {{counterContact("ctd-done", 1)}}, | |||
| coil("ctd-done-output", 5))}); | |||
| writeBit(repository, 4, true); | |||
| require(executor.executeScan({down_program}, repository).succeeded, | |||
| "CTD load reset must scan successfully"); | |||
| require(readWord(repository, 1) == 2, | |||
| "CTD reset must load PV into the external D current value"); | |||
| writeBit(repository, 4, false); | |||
| for (int expected = 1; expected >= 0; --expected) | |||
| { | |||
| writeBit(repository, 3, false); | |||
| require(executor.executeScan({down_program}, repository).succeeded, | |||
| "CTD falling preparation scan must succeed"); | |||
| writeBit(repository, 3, true); | |||
| require(executor.executeScan({down_program}, repository).succeeded, | |||
| "CTD rising edge must scan successfully"); | |||
| require(readWord(repository, 1) == expected, | |||
| "CTD must decrement exactly once for each rising edge"); | |||
| } | |||
| require(readBit(repository, 5), | |||
| "CTD completion contact must turn on when CV reaches zero"); | |||
| } | |||
| void testMoveAndSaturatingArithmetic() | |||
| { | |||
| VirtualRegisterRepository repository; | |||
| SoftwareLogicExecutor executor; | |||
| const ControlLogic program = logic({ | |||
| rung( | |||
| "move-rung", | |||
| {{contact("execute", 0)}}, | |||
| move("move", constantOperand(7), 10)), | |||
| rung( | |||
| "add-rung", | |||
| {{contact("execute-add", 0)}}, | |||
| arithmetic( | |||
| "add", | |||
| ArithmeticOperation::Add, | |||
| registerOperand(10), | |||
| constantOperand(32767), | |||
| 11)), | |||
| rung( | |||
| "sub-rung", | |||
| {{contact("execute-sub", 0)}}, | |||
| arithmetic( | |||
| "sub", | |||
| ArithmeticOperation::Subtract, | |||
| constantOperand(-32768), | |||
| constantOperand(1), | |||
| 12))}); | |||
| LogicTraceSnapshot trace; | |||
| require(executor.executeScan({program}, repository, &trace).succeeded, | |||
| "inactive data instructions must scan successfully"); | |||
| require(readWord(repository, 10) == 0, | |||
| "MOVE must not write while its rung is false"); | |||
| writeBit(repository, 0, true); | |||
| require(executor.executeScan({program}, repository, &trace).succeeded, | |||
| "active data instructions must scan successfully"); | |||
| require(readWord(repository, 10) == 7, | |||
| "MOVE must copy a constant into the destination D register"); | |||
| require(readWord(repository, 11) == 32767 | |||
| && trace.wordValues.at("add").overflow, | |||
| "ADD must saturate positive overflow and expose the overflow trace"); | |||
| require(readWord(repository, 12) == -32768 | |||
| && trace.wordValues.at("sub").overflow, | |||
| "SUB must saturate negative overflow and expose the overflow trace"); | |||
| const ControlLogic repeated_add = logic({ | |||
| rung( | |||
| "repeated-add-rung", | |||
| {{contact("repeated-add-input", 1)}}, | |||
| arithmetic( | |||
| "repeated-add", | |||
| ArithmeticOperation::Add, | |||
| registerOperand(20), | |||
| constantOperand(1), | |||
| 20))}); | |||
| writeBit(repository, 1, true); | |||
| require(executor.executeScan({repeated_add}, repository).succeeded | |||
| && executor.executeScan({repeated_add}, repository).succeeded, | |||
| "ADD must execute on every scan while its rung remains true"); | |||
| require(readWord(repository, 20) == 2, | |||
| "ADD with the same source and destination must accumulate by scan"); | |||
| } | |||
| void testSetResetPairOnSameAddress() | |||
| { | |||
| VirtualRegisterRepository repository; | |||
| @@ -628,6 +868,8 @@ int main(int argc, char *argv[]) | |||
| testEdgeContactsAreOneScanPulsesAndAreLogicScoped(); | |||
| testTonUsesElapsedTimeAndResetsAsNonRetentive(); | |||
| testMultipleTimersAndNetworkOrder(); | |||
| testCountersUseRisingEdgesAndExternalDValues(); | |||
| testMoveAndSaturatingArithmetic(); | |||
| testSetResetPairOnSameAddress(); | |||
| testConflictingCoilsAreRejected(); | |||
| testHmiSimulationClosedLoop(); | |||
| @@ -185,6 +185,68 @@ void testModeTransitions() | |||
| timer_rung.condition = ConditionExpression::fromNode(timer_contact); | |||
| timer_rung.output = timer_coil; | |||
| logic.rungs.push_back(timer_rung); | |||
| LogicNode counter_input; | |||
| counter_input.id = "poll-counter-input"; | |||
| counter_input.config = ContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 25}, ContactMode::NormallyOpen}; | |||
| LogicNode counter_output; | |||
| counter_output.id = "poll-counter"; | |||
| counter_output.config = CounterNodeConfig{ | |||
| CounterAddress{2}, | |||
| CounterMode::Up, | |||
| RegisterAddress{RegisterArea::D, 36}, | |||
| WordOperand{ | |||
| WordOperandKind::Register, | |||
| RegisterAddress{RegisterArea::D, 37}, | |||
| 0}, | |||
| RegisterAddress{RegisterArea::M, 26}}; | |||
| LadderRung counter_rung; | |||
| counter_rung.id = "poll-counter-rung"; | |||
| counter_rung.name = "Poll counter"; | |||
| counter_rung.condition = ConditionExpression::fromNode(counter_input); | |||
| counter_rung.output = counter_output; | |||
| logic.rungs.push_back(counter_rung); | |||
| LogicNode move_input; | |||
| move_input.id = "poll-move-input"; | |||
| move_input.config = ContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 27}, ContactMode::NormallyOpen}; | |||
| LogicNode move_output; | |||
| move_output.id = "poll-move"; | |||
| move_output.config = MoveNodeConfig{ | |||
| WordOperand{ | |||
| WordOperandKind::Register, | |||
| RegisterAddress{RegisterArea::D, 38}, | |||
| 0}, | |||
| RegisterAddress{RegisterArea::D, 39}}; | |||
| LadderRung move_rung; | |||
| move_rung.id = "poll-move-rung"; | |||
| move_rung.name = "Poll MOVE"; | |||
| move_rung.condition = ConditionExpression::fromNode(move_input); | |||
| move_rung.output = move_output; | |||
| logic.rungs.push_back(move_rung); | |||
| LogicNode add_input; | |||
| add_input.id = "poll-add-input"; | |||
| add_input.config = ContactNodeConfig{ | |||
| RegisterAddress{RegisterArea::M, 28}, ContactMode::NormallyOpen}; | |||
| LogicNode add_output; | |||
| add_output.id = "poll-add"; | |||
| add_output.config = ArithmeticNodeConfig{ | |||
| ArithmeticOperation::Add, | |||
| WordOperand{ | |||
| WordOperandKind::Register, | |||
| RegisterAddress{RegisterArea::D, 40}, | |||
| 0}, | |||
| WordOperand{ | |||
| WordOperandKind::Constant, | |||
| RegisterAddress{RegisterArea::D, 0}, | |||
| 1}, | |||
| RegisterAddress{RegisterArea::D, 41}}; | |||
| LadderRung add_rung; | |||
| add_rung.id = "poll-add-rung"; | |||
| add_rung.name = "Poll ADD"; | |||
| add_rung.condition = ConditionExpression::fromNode(add_input); | |||
| add_rung.output = add_output; | |||
| logic.rungs.push_back(add_rung); | |||
| project.controlLogics.push_back(logic); | |||
| require(service.mode() == ApplicationMode::Editing, | |||
| @@ -218,8 +280,13 @@ void testModeTransitions() | |||
| {RegisterArea::M, 12}, {RegisterArea::M, 20}, | |||
| {RegisterArea::M, 21}, {RegisterArea::M, 22}, | |||
| {RegisterArea::M, 23}, {RegisterArea::M, 24}, | |||
| {RegisterArea::D, 34}, {RegisterArea::D, 35}}), | |||
| "M/D logic and alarm addresses must be polled while T and comments stay offline-only"); | |||
| {RegisterArea::M, 25}, {RegisterArea::M, 26}, | |||
| {RegisterArea::M, 27}, {RegisterArea::M, 28}, | |||
| {RegisterArea::D, 34}, {RegisterArea::D, 35}, | |||
| {RegisterArea::D, 36}, {RegisterArea::D, 37}, | |||
| {RegisterArea::D, 38}, {RegisterArea::D, 39}, | |||
| {RegisterArea::D, 40}, {RegisterArea::D, 41}}), | |||
| "all instruction M/D references must be polled while T/C resources and comments stay offline-only"); | |||
| gateway.completeInitialRead(); | |||
| require(service.initialPlcReadCompleted(), | |||
| "service must retain the initial PLC read state"); | |||