| @@ -238,12 +238,7 @@ void RuntimeModeService::refreshPlcPollAddresses() | |||||
| } | } | ||||
| for (const LogicNode *node : nodes) | 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 "software_logic_executor.h" | ||||
| #include <algorithm> | #include <algorithm> | ||||
| #include <limits> | |||||
| #include <map> | #include <map> | ||||
| #include <type_traits> | #include <type_traits> | ||||
| @@ -76,6 +77,8 @@ void LogicTraceValues::clear() | |||||
| expressionValues.clear(); | expressionValues.clear(); | ||||
| rungValues.clear(); | rungValues.clear(); | ||||
| tonValues.clear(); | tonValues.clear(); | ||||
| counterValues.clear(); | |||||
| wordValues.clear(); | |||||
| } | } | ||||
| void LogicTraceSnapshot::clear() | void LogicTraceSnapshot::clear() | ||||
| @@ -95,6 +98,8 @@ LogicTraceSnapshot LogicTraceSnapshot::forLogic( | |||||
| projection.expressionValues = values->second.expressionValues; | projection.expressionValues = values->second.expressionValues; | ||||
| projection.rungValues = values->second.rungValues; | projection.rungValues = values->second.rungValues; | ||||
| projection.tonValues = values->second.tonValues; | projection.tonValues = values->second.tonValues; | ||||
| projection.counterValues = values->second.counterValues; | |||||
| projection.wordValues = values->second.wordValues; | |||||
| } | } | ||||
| return projection; | return projection; | ||||
| } | } | ||||
| @@ -133,13 +138,17 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||||
| const auto *output = std::get_if<CoilNodeConfig>(&rung.output->config); | const auto *output = std::get_if<CoilNodeConfig>(&rung.output->config); | ||||
| if (output == nullptr) | 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; | continue; | ||||
| } | } | ||||
| return failure( | return failure( | ||||
| LogicScanError::InvalidLogic, | LogicScanError::InvalidLogic, | ||||
| "梯形图输出不是线圈或 TON 指令", | |||||
| "梯形图输出不是有效的输出指令", | |||||
| logic.id, | logic.id, | ||||
| rung.id, | rung.id, | ||||
| rung.output->id); | rung.output->id); | ||||
| @@ -158,10 +167,10 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||||
| output_modes[output->address.index()] = output->mode; | 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(); | return success(); | ||||
| } | } | ||||
| @@ -169,7 +178,9 @@ LogicScanResult SoftwareLogicExecutor::validate( | |||||
| void SoftwareLogicExecutor::resetRuntime() | void SoftwareLogicExecutor::resetRuntime() | ||||
| { | { | ||||
| previous_edge_inputs_.clear(); | previous_edge_inputs_.clear(); | ||||
| previous_counter_inputs_.clear(); | |||||
| ton_states_.clear(); | ton_states_.clear(); | ||||
| counter_states_.clear(); | |||||
| } | } | ||||
| LogicScanResult SoftwareLogicExecutor::executeScan( | LogicScanResult SoftwareLogicExecutor::executeScan( | ||||
| @@ -227,6 +238,7 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||||
| } | } | ||||
| bool output_value = false; | bool output_value = false; | ||||
| result = executeOutput( | result = executeOutput( | ||||
| logic.id, | |||||
| *rung.output, | *rung.output, | ||||
| rung_value, | rung_value, | ||||
| repository, | repository, | ||||
| @@ -259,6 +271,8 @@ LogicScanResult SoftwareLogicExecutor::executeScanAt( | |||||
| trace->expressionValues = values->second.expressionValues; | trace->expressionValues = values->second.expressionValues; | ||||
| trace->rungValues = values->second.rungValues; | trace->rungValues = values->second.rungValues; | ||||
| trace->tonValues = values->second.tonValues; | 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; | *value = config.mode == ContactMode::NormallyOpen ? done : !done; | ||||
| return success(); | 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>) | else if constexpr (std::is_same_v<Config, CompareNodeConfig>) | ||||
| { | { | ||||
| const WordReadResult read = repository.readWord(config.address); | const WordReadResult read = repository.readWord(config.address); | ||||
| @@ -399,7 +419,51 @@ LogicScanResult SoftwareLogicExecutor::evaluateCondition( | |||||
| node.config); | 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( | LogicScanResult SoftwareLogicExecutor::executeOutput( | ||||
| const std::string &logic_id, | |||||
| const LogicNode &node, | const LogicNode &node, | ||||
| bool rung_value, | bool rung_value, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| @@ -448,12 +512,191 @@ LogicScanResult SoftwareLogicExecutor::executeOutput( | |||||
| return success(); | 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); | const auto *config = std::get_if<CoilNodeConfig>(&node.config); | ||||
| if (config == nullptr) | if (config == nullptr) | ||||
| { | { | ||||
| return failure( | return failure( | ||||
| LogicScanError::InvalidLogic, | LogicScanError::InvalidLogic, | ||||
| "梯形图输出不是线圈或 TON 指令", | |||||
| "梯形图输出不是有效的输出指令", | |||||
| {}, | {}, | ||||
| {}, | {}, | ||||
| node.id); | node.id); | ||||
| @@ -38,12 +38,29 @@ struct TonTraceValue | |||||
| int presetMs = 0; | 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 | struct LogicTraceValues | ||||
| { | { | ||||
| std::unordered_map<std::string, bool> nodeValues; | std::unordered_map<std::string, bool> nodeValues; | ||||
| std::unordered_map<std::string, bool> expressionValues; | std::unordered_map<std::string, bool> expressionValues; | ||||
| std::unordered_map<std::string, bool> rungValues; | std::unordered_map<std::string, bool> rungValues; | ||||
| std::unordered_map<std::string, TonTraceValue> tonValues; | std::unordered_map<std::string, TonTraceValue> tonValues; | ||||
| std::unordered_map<std::string, CounterTraceValue> counterValues; | |||||
| std::unordered_map<std::string, WordTraceValue> wordValues; | |||||
| void clear(); | void clear(); | ||||
| }; | }; | ||||
| @@ -96,13 +113,25 @@ private: | |||||
| LogicTraceValues *trace, | LogicTraceValues *trace, | ||||
| bool *value); | bool *value); | ||||
| LogicScanResult executeOutput( | LogicScanResult executeOutput( | ||||
| const std::string &logic_id, | |||||
| const LogicNode &node, | const LogicNode &node, | ||||
| bool rung_value, | bool rung_value, | ||||
| RegisterRepository &repository, | RegisterRepository &repository, | ||||
| TimePoint now, | TimePoint now, | ||||
| LogicTraceValues *trace, | LogicTraceValues *trace, | ||||
| bool *output_value); | 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_edge_inputs_; | ||||
| std::map<std::pair<std::string, std::string>, bool> previous_counter_inputs_; | |||||
| std::unordered_map<int, TonRuntimeState> ton_states_; | 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}; | 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) | LogicNode ton(const std::string &id, int timer_index, int preset_ms) | ||||
| { | { | ||||
| return {id, TonNodeConfig{TimerAddress{timer_index}, preset_ms}, true}; | 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, | LogicNode comparison(const std::string &id, int address, | ||||
| ComparisonOperator operation, std::int16_t value) | 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"); | "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() | void testNestedSeriesParallelExpression() | ||||
| { | { | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| @@ -462,6 +543,165 @@ void testMultipleTimersAndNetworkOrder() | |||||
| "a later scan must observe the completed TON through T contact"); | "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() | void testSetResetPairOnSameAddress() | ||||
| { | { | ||||
| VirtualRegisterRepository repository; | VirtualRegisterRepository repository; | ||||
| @@ -628,6 +868,8 @@ int main(int argc, char *argv[]) | |||||
| testEdgeContactsAreOneScanPulsesAndAreLogicScoped(); | testEdgeContactsAreOneScanPulsesAndAreLogicScoped(); | ||||
| testTonUsesElapsedTimeAndResetsAsNonRetentive(); | testTonUsesElapsedTimeAndResetsAsNonRetentive(); | ||||
| testMultipleTimersAndNetworkOrder(); | testMultipleTimersAndNetworkOrder(); | ||||
| testCountersUseRisingEdgesAndExternalDValues(); | |||||
| testMoveAndSaturatingArithmetic(); | |||||
| testSetResetPairOnSameAddress(); | testSetResetPairOnSameAddress(); | ||||
| testConflictingCoilsAreRejected(); | testConflictingCoilsAreRejected(); | ||||
| testHmiSimulationClosedLoop(); | testHmiSimulationClosedLoop(); | ||||
| @@ -185,6 +185,68 @@ void testModeTransitions() | |||||
| timer_rung.condition = ConditionExpression::fromNode(timer_contact); | timer_rung.condition = ConditionExpression::fromNode(timer_contact); | ||||
| timer_rung.output = timer_coil; | timer_rung.output = timer_coil; | ||||
| logic.rungs.push_back(timer_rung); | 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); | project.controlLogics.push_back(logic); | ||||
| require(service.mode() == ApplicationMode::Editing, | require(service.mode() == ApplicationMode::Editing, | ||||
| @@ -218,8 +280,13 @@ void testModeTransitions() | |||||
| {RegisterArea::M, 12}, {RegisterArea::M, 20}, | {RegisterArea::M, 12}, {RegisterArea::M, 20}, | ||||
| {RegisterArea::M, 21}, {RegisterArea::M, 22}, | {RegisterArea::M, 21}, {RegisterArea::M, 22}, | ||||
| {RegisterArea::M, 23}, {RegisterArea::M, 24}, | {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(); | gateway.completeInitialRead(); | ||||
| require(service.initialPlcReadCompleted(), | require(service.initialPlcReadCompleted(), | ||||
| "service must retain the initial PLC read state"); | "service must retain the initial PLC read state"); | ||||