wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ArduinoCodeGen.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <algorithm>
7#include <functional>
8
9#include <QRegularExpression>
10#include <QSet>
11
13#include "App/Core/Common.h"
19#include "App/Element/IC.h"
20#include "App/Scene/Scene.h"
23#include "App/Wiring/Port.h"
24
25ArduinoCodeGen::ArduinoCodeGen(const QString &fileName, const QVector<GraphicElement *> &elements)
26 : m_file(fileName)
27 , m_elements(elements)
28{
29 if (!m_file.open(QIODevice::WriteOnly | QIODevice::Text)) {
30 throw PANDACEPTION("Could not open file for writing: %1", fileName);
31 }
32 m_stream.setDevice(&m_file);
33}
34
35QString ArduinoCodeGen::highLow(const Status val)
36{
37 return (val == Status::Active) ? "HIGH" : "LOW";
38}
39
40QString ArduinoCodeGen::removeForbiddenChars(const QString &input)
41{
42 return CodeGenUtils::removeForbiddenChars(input, true);
43}
44
45bool ArduinoCodeGen::isArduinoReserved(const QString &name)
46{
47 static const QSet<QString> reserved = {
48 // C++ keywords
49 "alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit",
50 "atomic_noexcept", "auto", "bitand", "bitor", "bool", "break", "case", "catch",
51 "char", "char8_t", "char16_t", "char32_t", "class", "compl", "concept",
52 "const", "consteval", "constexpr", "constinit", "const_cast", "continue",
53 "co_await", "co_return", "co_yield", "decltype", "default", "delete", "do",
54 "double", "dynamic_cast", "else", "enum", "explicit", "export", "extern",
55 "false", "float", "for", "friend", "goto", "if", "inline", "int", "long",
56 "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "or",
57 "or_eq", "private", "protected", "public", "reflexpr", "register",
58 "reinterpret_cast", "requires", "return", "short", "signed", "sizeof",
59 "static", "static_assert", "static_cast", "struct", "switch", "synchronized",
60 "template", "this", "thread_local", "throw", "true", "try", "typedef",
61 "typeid", "typename", "union", "unsigned", "using", "virtual", "void",
62 "volatile", "wchar_t", "while", "xor", "xor_eq",
63 // Arduino built-ins and functions (lowercase to match post-removeForbiddenChars names)
64 "high", "low", "input", "output", "input_pullup", "input_pulldown",
65 "led_builtin", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7",
66 "pinmode", "digitalwrite", "digitalread", "analogread", "analogwrite",
67 "analogreadresolution", "analogwriteresolution", "serial",
68 "tone", "notone", "delay", "millis", "micros", "setup", "loop"
69 };
70 return reserved.contains(name);
71}
72
73QString ArduinoCodeGen::otherPortName(Port *port)
74{
75 QSet<Port *> visited;
76 return otherPortNameImpl(port, visited);
77}
78
79QString ArduinoCodeGen::otherPortNameImpl(Port *port, QSet<Port *> &visited)
80{
81 if (!port) {
82 return "LOW"; // LCOV_EXCL_LINE -- every call site passes a real Port*
83 }
84
85 // Cycle detection: if we've already visited this port, don't continue. Only reachable
86 // via the wireless-Rx recursion below chaining back through a port already marked
87 // visited by the connection-graph branch further down — needs a specific multi-hop
88 // wireless/connection topology deliberately crafted to loop, not just a same-label Tx/Rx
89 // pair (a single wireless hop never revisits a port already in `visited`).
90 if (visited.contains(port)) {
91 const QString mapped = m_varMap.value(port); // LCOV_EXCL_LINE
92 return mapped.isEmpty() ? "LOW" : mapped; // LCOV_EXCL_LINE
93 } // LCOV_EXCL_LINE
94
95 if (port->connections().isEmpty()) {
96 // Check m_varMap first — IC input boundary nodes have no connections but a mapped variable
97 const QString mapped = m_varMap.value(port);
98 if (!mapped.isEmpty()) {
99 return mapped;
100 }
101 // Wireless Rx: resolve via the Tx node's input (what drives the transmitter)
102 auto *elm = port->graphicElement();
103 if (elm && elm->wirelessMode() == WirelessMode::Rx && !elm->label().isEmpty()) {
104 auto *txInputPort = m_txInputPorts.value(elm->label(), nullptr);
105 if (txInputPort) {
106 return otherPortNameImpl(txInputPort, visited);
107 }
108 }
109 return highLow(port->defaultValue());
110 }
111
112 auto *connection = port->connections().constFirst();
113 if (!connection) {
114 return highLow(port->defaultValue()); // LCOV_EXCL_LINE -- a Connection in a port's own list always has 2 valid endpoints
115 }
116
117 auto *otherPort = connection->otherPort(port);
118 if (!otherPort) {
119 return highLow(port->defaultValue()); // LCOV_EXCL_LINE -- see above
120 }
121
122 // Cycle detection: same as above — only reachable via a deliberately-crafted
123 // multi-hop wireless/connection cycle, not a plain circuit.
124 if (visited.contains(otherPort)) {
125 const QString mapped = m_varMap.value(otherPort); // LCOV_EXCL_LINE
126 return mapped.isEmpty() ? "LOW" : mapped; // LCOV_EXCL_LINE
127 } // LCOV_EXCL_LINE
128
129 // Mark both ports as visited
130 visited.insert(port);
131 visited.insert(otherPort);
132
133 const QString result = m_varMap.value(otherPort);
134 if (result.isEmpty()) {
135 // Not reachable via a real circuit: declareAuxVariablesRec() unconditionally assigns
136 // every element's outputs a variable name before assignVariablesRec()/otherPortName()
137 // ever runs, so a connected otherPort's m_varMap entry is always already populated.
138 return highLow(otherPort->defaultValue()); // LCOV_EXCL_LINE
139 }
140 return result;
141}
142
144{
145 try {
146 m_txInputPorts = Scene::wirelessTxInputPorts(m_elements);
147 m_hasSequential = hasNativeMemory(m_elements);
148
149 int requiredInputPins = 0;
150 int requiredOutputPins = 0;
151 for (auto *elm : m_elements) {
152 const auto type = elm->elementType();
153 if (type == ElementType::InputButton || type == ElementType::InputSwitch) {
154 requiredInputPins++;
155 } else if (type == ElementType::InputRotary) {
156 requiredInputPins += elm->outputSize();
157 }
158 }
159 for (auto *elm : m_elements) {
160 if (elm->elementGroup() == ElementGroup::Output) {
161 requiredOutputPins += static_cast<int>(elm->inputs().size());
162 }
163 }
164 const int totalRequiredPins = requiredInputPins + requiredOutputPins;
165 m_totalRequiredPins = totalRequiredPins;
166 m_selectedBoard = selectBoard(totalRequiredPins);
167 m_availablePins = m_selectedBoard.availablePins;
168
169 m_stream << "// ==================================================================== //" << Qt::endl;
170 m_stream << "// ======= This code was generated automatically by wiRedPanda ======== //" << Qt::endl;
171 m_stream << "// ==================================================================== //" << Qt::endl;
172 m_stream << "//" << Qt::endl;
173 m_stream << QString("// Target Board: %1").arg(m_selectedBoard.name) << Qt::endl;
174 m_stream << QString("// Pin Usage: %1/%2 pins").arg(totalRequiredPins).arg(m_selectedBoard.maxPins()) << Qt::endl;
175 m_stream << "//" << Qt::endl;
176 m_stream << Qt::endl
177 << Qt::endl;
178 m_stream << "#include <elapsedMillis.h>" << Qt::endl;
179 declareInputs();
180 declareOutputs();
181 declareAuxVariables();
182 if (m_hasSequential) {
183 // Phase flag for the non-blocking tick driver: flip-flops sample only
184 // while true; the post-commit re-settle runs with it false.
185 m_stream << "bool g_sample = true;" << Qt::endl << Qt::endl;
186 }
187 setup();
188 emitComputeLogicFunction();
189 if (m_hasSequential) {
190 emitCommitFlipFlops();
191 }
192 loop();
193 } catch (...) {
194 m_file.close();
195 m_file.remove();
196 throw;
197 }
198}
199
200void ArduinoCodeGen::declareInputs()
201{
202 int counter = 1;
203 m_stream << "/* ========= Inputs ========== */" << Qt::endl;
204
205 for (auto *elm : m_elements) {
206 const auto type = elm->elementType();
207
208 if ((type == ElementType::InputButton) || (type == ElementType::InputSwitch)) {
209 // Check if we have available pins before assigning
210 if (m_availablePins.isEmpty()) {
211 throwPinOverflow();
212 }
213
214 QString varName = elm->objectName() + QString::number(counter);
215 const QString label = elm->label();
216
217 if (!label.isEmpty()) {
218 varName += "_" + label;
219 }
220
221 varName = removeForbiddenChars(varName);
222 // Not reachable via any label: varName always has a numeric counter baked in
223 // (objectName()+counter, prepended with '_' by removeForbiddenChars() if that
224 // leading char is a digit), so it can never collide exactly with a bare reserved
225 // word — kept as a defensive backstop against a future naming-scheme change.
226 if (isArduinoReserved(varName)) { // LCOV_EXCL_LINE
227 varName.append('_'); // LCOV_EXCL_LINE
228 }
229 m_stream << QString("const int %1 = %2;").arg(varName, m_availablePins.constFirst()) << Qt::endl;
230 auto *outPort = elm->outputPort(0);
231 if (outPort) {
232 m_inputMap.append(MappedPin(elm, m_availablePins.constFirst(), varName, outPort, 0));
233 m_varMap[outPort] = varName + QString("_val");
234 }
235 m_availablePins.removeFirst();
236 ++counter;
237 } else if (type == ElementType::InputRotary) {
238 // Each rotary position maps to one digital input pin; exactly one is HIGH at a time.
239 const QString label = elm->label();
240 for (int i = 0; i < elm->outputSize(); ++i) {
241 if (m_availablePins.isEmpty()) {
242 throwPinOverflow();
243 }
244 QString varName = elm->objectName() + QString::number(counter) + "_pos" + QString::number(i);
245 if (!label.isEmpty()) {
246 varName = elm->objectName() + QString::number(counter) + "_" + label + "_pos" + QString::number(i);
247 }
248 varName = removeForbiddenChars(varName);
249 if (isArduinoReserved(varName)) { // LCOV_EXCL_LINE -- see declareInputs()'s identical guard above
250 varName.append('_'); // LCOV_EXCL_LINE
251 }
252 m_stream << QString("const int %1 = %2;").arg(varName, m_availablePins.constFirst()) << Qt::endl;
253 auto *outPort = elm->outputPort(i);
254 if (outPort) {
255 m_inputMap.append(MappedPin(elm, m_availablePins.constFirst(), varName, outPort, i));
256 m_varMap[outPort] = varName + "_val";
257 }
258 m_availablePins.removeFirst();
259 }
260 ++counter;
261 }
262 }
263
264 m_stream << Qt::endl;
265}
266
267void ArduinoCodeGen::declareOutputs()
268{
269 int counter = 1;
270 m_stream << "/* ========= Outputs ========== */" << Qt::endl;
271 for (auto *elm : m_elements) {
272 if (elm->elementGroup() == ElementGroup::Output) {
273 QString label = elm->label();
274 for (int i = 0; i < elm->inputs().size(); ++i) {
275 // Check if we have available pins before assigning
276 if (m_availablePins.isEmpty()) {
277 throwPinOverflow();
278 }
279
280 QString varName = elm->objectName() + QString::number(counter);
281 if (!label.isEmpty()) {
282 varName = QString("%1_%2").arg(varName, label);
283 }
284 Port *port = elm->inputPort(i);
285 if (port && !port->name().isEmpty()) {
286 varName = QString("%1_%2").arg(varName, port->name());
287 }
288 varName = removeForbiddenChars(varName);
289 if (isArduinoReserved(varName)) { // LCOV_EXCL_LINE -- see declareInputs()'s identical guard
290 varName.append('_'); // LCOV_EXCL_LINE
291 }
292 m_stream << QString("const int %1 = %2;").arg(varName, m_availablePins.constFirst()) << Qt::endl;
293 m_outputMap.append(MappedPin(elm, m_availablePins.constFirst(), varName, port, i));
294 m_availablePins.removeFirst();
295 }
296 ++counter;
297 }
298 }
299 m_stream << Qt::endl;
300}
301
302void ArduinoCodeGen::declareAuxVariablesRec(const QVector<GraphicElement *> &elements, const bool isBox, const QString &icPrefix)
303{
304 int counter = 0;
305
306 for (auto *elm : elements) {
307 if (elm->elementType() == ElementType::IC) {
308 auto *ic = qobject_cast<IC *>(elm);
309 if (!ic) continue;
310
311 m_stream << "// IC: " << CodeGenUtils::sanitizeComment(ic->label()) << Qt::endl;
312
313 // Include the full ancestor path (icPrefix) so names stay globally
314 // unique across repeated/nested sub-IC instances (register files, RAM,
315 // CPUs). Without it, structurally-identical sub-trees collide.
316 QString baseVarName = QString("aux_%1%2_%3").arg(icPrefix, removeForbiddenChars(ic->label()), QString::number(counter++));
317
318 for (int i = 0; i < ic->outputSize(); ++i) {
319 Port *externalPort = ic->outputPort(i);
320 if (externalPort) {
321 QString portVarName = baseVarName;
322 const QString portName = externalPort->name();
323 if (!portName.isEmpty()) {
324 portVarName += "_" + removeForbiddenChars(portName);
325 if (i > 0) { // LCOV_EXCL_LINE -- needs a real multi-output IC fixture; deferred, see .claude/COVERAGE_100_PLAN.md
326 portVarName += "_" + QString::number(i); // LCOV_EXCL_LINE
327 }
328 } else {
329 portVarName += "_out" + QString::number(i); // LCOV_EXCL_LINE -- needs an IC whose external output port has no name; deferred
330 }
331 m_varMap[externalPort] = portVarName;
332 if (!m_declaredVariables.contains(portVarName)) {
333 m_stream << "bool " << portVarName << " = " << highLow(externalPort->defaultValue()) << ";" << Qt::endl;
334 m_declaredVariables.append(portVarName);
335 }
336 }
337 }
338
339 if (!ic->internalElements().isEmpty()) {
340 const QString nestedPrefix = QString("%1%2_%3_").arg(icPrefix, removeForbiddenChars(ic->label()), QString::number(counter - 1));
341 declareAuxVariablesRec(ic->internalElements(), true, nestedPrefix);
342
343 for (int i = 0; i < ic->internalInputs().size(); ++i) {
344 Port *internalPort = ic->internalInputs().at(i);
345 if (m_varMap.value(internalPort).isEmpty()) {
346 const QString portVarName = QString("aux_ic_input_%1%2_%3_%4").arg(icPrefix, removeForbiddenChars(ic->label()), QString::number(counter - 1), QString::number(i));
347 m_varMap[internalPort] = portVarName;
348 if (!m_declaredVariables.contains(portVarName)) {
349 m_stream << "bool " << portVarName << " = LOW;" << Qt::endl;
350 m_declaredVariables.append(portVarName);
351 }
352 }
353 }
354 }
355
356 m_stream << "// End IC: " << CodeGenUtils::sanitizeComment(ic->label()) << Qt::endl;
357 continue;
358 }
359
360 const auto outputs = elm->outputs();
361
362 QString baseVarName;
363 if (!icPrefix.isEmpty()) {
364 baseVarName = QString("aux_%1%2_%3").arg(icPrefix, removeForbiddenChars(elm->objectName()), QString::number(counter++));
365 } else {
366 baseVarName = QString("aux_%1_%2").arg(removeForbiddenChars(elm->objectName()), QString::number(counter++));
367 }
368
369 if (outputs.size() == 1) {
370 Port *port = outputs.constFirst();
371
372 if (elm->elementType() == ElementType::InputVcc) {
373 m_varMap[port] = "HIGH";
374 continue;
375 }
376
377 if (elm->elementType() == ElementType::InputGnd) {
378 m_varMap[port] = "LOW";
379 continue;
380 }
381
382 if (m_varMap.value(port).isEmpty()) {
383 m_varMap[port] = baseVarName;
384 }
385 } else {
386 int portCounter = 0;
387 for (auto *port : outputs) {
388 QString portName = baseVarName + QString("_%1").arg(portCounter++);
389 if (!port->name().isEmpty()) {
390 portName += "_" + removeForbiddenChars(port->name());
391 }
392 m_varMap[port] = portName;
393 }
394 }
395
396 for (auto *port : outputs) {
397 const QString varName2 = m_varMap.value(port);
398 if (!m_declaredVariables.contains(varName2)) {
399 m_stream << "bool " << varName2 << " = " << highLow(port->defaultValue()) << ";" << Qt::endl;
400 m_declaredVariables.append(varName2);
401 }
402
403 // Staging variable for non-blocking sequential commit (see emitTickDriver).
404 if (elm->elementGroup() == ElementGroup::Memory) {
405 m_stream << "bool " << varName2 << "_next = " << highLow(port->defaultValue()) << ";" << Qt::endl;
406 }
407
408 switch (elm->elementType()) {
409 case ElementType::Clock: {
410 if (!isBox) {
411 auto *clk = qobject_cast<Clock *>(elm);
412 if (!clk) {
413 break; // LCOV_EXCL_LINE -- elementType() == Clock guarantees this cast succeeds
414 }
415 m_stream << "elapsedMillis " << varName2 << "_elapsed = 0;" << Qt::endl;
416 m_stream << "int " << varName2 << "_interval = " << (std::max)(1, static_cast<int>(1000.0 / clk->frequency())) << ";" << Qt::endl;
417 }
418 break;
419 }
420 case ElementType::DFlipFlop: {
421 m_stream << "bool " << varName2 << "_inclk = LOW;" << Qt::endl;
422 m_stream << "bool " << varName2 << "_last = LOW;" << Qt::endl;
423 break;
424 }
425 case ElementType::TFlipFlop:
426 case ElementType::SRFlipFlop:
427 case ElementType::JKFlipFlop: {
428 m_stream << "bool " << varName2 << "_inclk = LOW;" << Qt::endl;
429 break;
430 }
431 case ElementType::And:
432 case ElementType::AudioBox:
433 case ElementType::Buzzer:
434 case ElementType::DLatch:
435 case ElementType::Demux:
436 case ElementType::Display14:
437 case ElementType::Display16:
438 case ElementType::Display7:
439 case ElementType::IC:
440 case ElementType::InputButton:
441 case ElementType::InputGnd:
442 case ElementType::InputRotary:
443 case ElementType::InputSwitch:
444 case ElementType::InputVcc:
445 case ElementType::JKLatch:
446 case ElementType::Led:
447 case ElementType::Line:
448 case ElementType::Mux:
449 case ElementType::Nand:
450 case ElementType::Node:
451 case ElementType::Nor:
452 case ElementType::Not:
453 case ElementType::Or:
454 case ElementType::SRLatch:
455 case ElementType::Text:
456 case ElementType::TruthTable:
457 case ElementType::Unknown:
458 case ElementType::Xnor:
459 case ElementType::Xor:
460 break;
461 }
462 }
463 }
464}
465
466void ArduinoCodeGen::declareSequentialStateRec(const QVector<GraphicElement *> &elements, const bool topLevel)
467{
468 for (auto *elm : elements) {
469 if (elm->elementType() == ElementType::IC) {
470 if (auto *ic = qobject_cast<IC *>(elm)) {
471 declareSequentialStateRec(ic->internalElements(), false);
472 }
473 continue;
474 }
475
476 const auto outputs = elm->outputs();
477 if (outputs.isEmpty()) {
478 continue;
479 }
480 // Staging variables for non-blocking sequential commit, one per output.
481 if (elm->elementGroup() == ElementGroup::Memory) {
482 for (auto *port : outputs) {
483 const QString v = m_varMap.value(port);
484 if (!v.isEmpty()) {
485 m_stream << "bool " << v << "_next = " << highLow(port->defaultValue()) << ";" << Qt::endl;
486 }
487 }
488 }
489
490 const QString varName = m_varMap.value(outputs.constFirst());
491 if (varName.isEmpty()) {
492 // Not reachable: generateTestbench() (the only caller) always runs after
493 // generate(), which already had declareAuxVariablesRec() populate m_varMap for
494 // every element's outputs.
495 continue; // LCOV_EXCL_LINE
496 }
497
498 switch (elm->elementType()) {
499 case ElementType::Clock:
500 // Mirror declareAuxVariablesRec: only top-level clocks are time-driven.
501 if (topLevel) {
502 m_stream << "elapsedMillis " << varName << "_elapsed = 0;" << Qt::endl;
503 m_stream << "int " << varName << "_interval = 1000;" << Qt::endl;
504 }
505 break;
506 case ElementType::DFlipFlop:
507 m_stream << "bool " << varName << "_inclk = LOW;" << Qt::endl;
508 m_stream << "bool " << varName << "_last = LOW;" << Qt::endl;
509 break;
510 case ElementType::TFlipFlop:
511 case ElementType::SRFlipFlop:
512 case ElementType::JKFlipFlop:
513 m_stream << "bool " << varName << "_inclk = LOW;" << Qt::endl;
514 break;
515 case ElementType::And:
516 case ElementType::AudioBox:
517 case ElementType::Buzzer:
518 case ElementType::DLatch:
519 case ElementType::Demux:
520 case ElementType::Display14:
521 case ElementType::Display16:
522 case ElementType::Display7:
523 case ElementType::IC:
524 case ElementType::InputButton:
525 case ElementType::InputGnd:
526 case ElementType::InputRotary:
527 case ElementType::InputSwitch:
528 case ElementType::InputVcc:
529 case ElementType::JKLatch:
530 case ElementType::Led:
531 case ElementType::Line:
532 case ElementType::Mux:
533 case ElementType::Nand:
534 case ElementType::Node:
535 case ElementType::Nor:
536 case ElementType::Not:
537 case ElementType::Or:
538 case ElementType::SRLatch:
539 case ElementType::Text:
540 case ElementType::TruthTable:
541 case ElementType::Unknown:
542 case ElementType::Xnor:
543 case ElementType::Xor:
544 break;
545 }
546 }
547}
548
549bool ArduinoCodeGen::hasNativeMemory(const QVector<GraphicElement *> &elements)
550{
551 for (auto *elm : elements) {
552 if (elm->elementGroup() == ElementGroup::Memory) {
553 return true;
554 }
555 if (elm->elementType() == ElementType::IC) {
556 if (auto *ic = qobject_cast<IC *>(elm)) {
557 if (hasNativeMemory(ic->internalElements())) {
558 return true;
559 }
560 }
561 }
562 }
563 return false;
564}
565
566void ArduinoCodeGen::emitCommitFlipFlopsRec(const QVector<GraphicElement *> &elements)
567{
568 for (auto *elm : elements) {
569 if (elm->elementType() == ElementType::IC) {
570 if (auto *ic = qobject_cast<IC *>(elm)) {
571 emitCommitFlipFlopsRec(ic->internalElements());
572 }
573 continue;
574 }
575 if (elm->elementGroup() != ElementGroup::Memory) {
576 continue;
577 }
578 for (auto *port : elm->outputs()) {
579 const QString varName = m_varMap.value(port);
580 if (!varName.isEmpty()) {
581 m_stream << " " << varName << " = " << varName << "_next;" << Qt::endl;
582 }
583 }
584 }
585}
586
587void ArduinoCodeGen::emitCommitFlipFlops()
588{
589 m_stream << "void commitFlipFlops() {" << Qt::endl;
590 emitCommitFlipFlopsRec(m_elements);
591 m_stream << "}" << Qt::endl << Qt::endl;
592}
593
594void ArduinoCodeGen::emitTickDriver()
595{
596 // One simulation tick. Sequential sketches mirror the engine's non-blocking
597 // semantics: settle combinational logic while flip-flops sample into staging
598 // (_next) reading pre-edge state, commit all flip-flops at once, then settle
599 // again so combinational outputs reflect the new committed state.
600 if (m_hasSequential) {
601 m_stream << " g_sample = true;" << Qt::endl;
602 m_stream << " for (int s = 0; s < " << Simulation::kMaxSettleIterations << "; s++) { computeLogic(); }" << Qt::endl;
603 m_stream << " commitFlipFlops();" << Qt::endl;
604 m_stream << " g_sample = false;" << Qt::endl;
605 m_stream << " for (int s = 0; s < " << Simulation::kMaxSettleIterations << "; s++) { computeLogic(); }" << Qt::endl;
606 } else {
607 m_stream << " computeLogic();" << Qt::endl;
608 }
609}
610
611void ArduinoCodeGen::declareAuxVariables()
612{
613 m_stream << "/* ====== Aux. Variables ====== */" << Qt::endl;
614
615 for (const auto &pin : std::as_const(m_inputMap)) {
616 const QString valVarName = pin.m_varName + "_val";
617 if (!m_declaredVariables.contains(valVarName)) {
618 m_stream << "bool " << valVarName << " = LOW;" << Qt::endl;
619 m_declaredVariables.append(valVarName);
620 }
621 }
622
623 declareAuxVariablesRec(m_elements, false, {});
624 m_stream << Qt::endl;
625}
626
627void ArduinoCodeGen::emitFlipFlopBlock(GraphicElement *elm, const QString &typeName, const QString &firstOut,
628 const QString &secondOut, int clockInputIndex, int presetInputIndex,
629 int clearInputIndex, const std::function<void()> &edgeLogic,
630 const std::function<void()> &stateEpilogue)
631{
632 QString clk = otherPortName(elm->inputPort(clockInputIndex));
633 QString inclk = firstOut + "_inclk";
634 QString firstOutNext = firstOut + "_next";
635 QString secondOutNext = secondOut + "_next";
636
637 m_stream << QString(" //%1 FlipFlop").arg(typeName) << Qt::endl;
638 // Sample only during the settle phase (g_sample); the post-commit re-settle
639 // re-evaluates combinational logic without re-clocking. Outputs are staged to
640 // <out>_next and published together by commitFlipFlops() — non-blocking
641 // semantics so gated clocks read pre-edge state, matching the engine.
642 m_stream << QString(" if (g_sample) {") << Qt::endl;
643 m_stream << QString(" if (%1 && !%2) { ").arg(clk, inclk) << Qt::endl;
644
645 // Type-specific edge behavior (writes the _next staging variables).
646 edgeLogic();
647
648 m_stream << QString(" }") << Qt::endl;
649
650 // Preset/Clear logic (common to all edge-triggered flip-flops), staged.
651 QString prst = otherPortName(elm->inputPort(presetInputIndex));
652 QString clr = otherPortName(elm->inputPort(clearInputIndex));
653 m_stream << QString(" if (!%1 || !%2) { ").arg(prst, clr) << Qt::endl;
654 m_stream << QString(" %1 = !%2; //Preset").arg(firstOutNext, prst) << Qt::endl;
655 m_stream << QString(" %1 = !%2; //Clear").arg(secondOutNext, clr) << Qt::endl;
656 m_stream << QString(" }") << Qt::endl;
657
658 // Clock-level update for edge detection.
659 m_stream << " " << inclk << " = " << clk << ";" << Qt::endl;
660
661 // Per-sample epilogue (e.g. the one-tick data latch for D/T flip-flops).
662 if (stateEpilogue) {
663 stateEpilogue();
664 }
665
666 m_stream << QString(" }") << Qt::endl; // end if (g_sample)
667 m_stream << QString(" //End of %1 FlipFlop").arg(typeName) << Qt::endl;
668}
669
670void ArduinoCodeGen::setup()
671{
672 m_stream << "void setup() {" << Qt::endl;
673 for (const auto &pin : std::as_const(m_inputMap)) {
674 m_stream << " pinMode(" << pin.m_varName << ", INPUT);" << Qt::endl;
675 }
676 for (const auto &pin : std::as_const(m_outputMap)) {
677 m_stream << " pinMode(" << pin.m_varName << ", OUTPUT);" << Qt::endl;
678 }
679 m_stream << "}" << Qt::endl
680 << Qt::endl;
681}
682
683void ArduinoCodeGen::assignVariablesRec(const QVector<GraphicElement *> &elements)
684{
685 for (auto *elm : elements) {
686 if (elm->elementType() == ElementType::IC) {
687 auto *ic = qobject_cast<IC *>(elm);
688 if (!ic) continue;
689
690 m_stream << " // IC: " << CodeGenUtils::sanitizeComment(ic->label()) << Qt::endl;
691
692 for (int i = 0; i < ic->inputSize(); ++i) {
693 Port *externalPort = ic->inputPort(i);
694 Port *internalPort = ic->internalInputs().at(i);
695 const QString externalValue = otherPortName(externalPort);
696 const QString internalVar = m_varMap.value(internalPort);
697 m_stream << " " << internalVar << " = " << externalValue << ";" << Qt::endl;
698 }
699
700 if (!ic->internalElements().isEmpty()) {
701 IC *previousIC = m_currentIC;
702 m_currentIC = ic;
703 // Emit boundary input nodes before the rest so downstream gates
704 // never read a stale input on the first settle pass. The
705 // topological sort can place a boundary input after a consumer
706 // when the IC has feedback (its priorities take the legacy
707 // path), and a stale read transiently flips bistable latches —
708 // wrong, and divergent from the simulation, which excludes
709 // boundary inputs from its settle (IC::initializeSimulation).
710 QSet<GraphicElement *> boundaryInputs;
711 for (auto *port : ic->internalInputs()) {
712 if (auto *boundaryElement = port->graphicElement()) {
713 boundaryInputs.insert(boundaryElement);
714 }
715 }
716 auto sortedInternal = Scene::sortByTopology(ic->internalElements());
717 std::stable_partition(sortedInternal.begin(), sortedInternal.end(),
718 [&boundaryInputs](GraphicElement *e) { return boundaryInputs.contains(e); });
719 assignVariablesRec(sortedInternal);
720 m_currentIC = previousIC;
721 }
722
723 for (int i = 0; i < ic->outputSize(); ++i) {
724 Port *externalPort = ic->outputPort(i);
725 Port *internalPort = ic->internalOutputs().at(i);
726 const QString internalValue = m_varMap.value(internalPort);
727 const QString externalVar = m_varMap.value(externalPort);
728 m_stream << " " << externalVar << " = " << internalValue << ";" << Qt::endl;
729 }
730
731 m_stream << " // End IC: " << CodeGenUtils::sanitizeComment(ic->label()) << Qt::endl;
732 continue;
733 }
734
735 if (elm->inputs().isEmpty() || elm->outputs().isEmpty()) {
736 continue;
737 }
738
739 auto *outputPort0 = elm->outputPort(0);
740 if (!outputPort0) {
741 continue; // LCOV_EXCL_LINE -- elm->outputs() was just checked non-empty above
742 }
743 QString firstOut = m_varMap.value(outputPort0);
744 switch (elm->elementType()) {
745 case ElementType::DFlipFlop: emitDFlipFlop(elm, firstOut); break;
746 case ElementType::DLatch: emitDLatch(elm, firstOut); break;
747 case ElementType::JKFlipFlop: emitJKFlipFlop(elm, firstOut); break;
748 case ElementType::SRFlipFlop: emitSRFlipFlop(elm, firstOut); break;
749 case ElementType::TFlipFlop: emitTFlipFlop(elm, firstOut); break;
750 case ElementType::SRLatch: emitSRLatch(elm, firstOut); break;
751 case ElementType::Mux: emitMux(elm); break;
752 case ElementType::Demux: emitDemux(elm); break;
753 case ElementType::TruthTable: emitTruthTable(elm); break;
754 case ElementType::And:
755 case ElementType::Or:
756 case ElementType::Nand:
757 case ElementType::Nor:
758 case ElementType::Xor:
759 case ElementType::Xnor:
760 case ElementType::Not:
761 case ElementType::Node: assignLogicOperator(elm); break;
762 // Not reachable: IC is handled separately above (continue at the top of the loop),
763 // and every other type listed here is input-only or output-only, so it was already
764 // filtered by the inputs()/outputs()-empty check just above this switch.
765 case ElementType::AudioBox: // LCOV_EXCL_LINE
766 case ElementType::Buzzer:
767 case ElementType::Clock:
768 case ElementType::Display14:
769 case ElementType::Display16:
770 case ElementType::Display7:
771 case ElementType::IC:
772 case ElementType::InputButton:
773 case ElementType::InputGnd:
774 case ElementType::InputRotary:
775 case ElementType::InputSwitch:
776 case ElementType::InputVcc:
777 case ElementType::JKLatch:
778 case ElementType::Led:
779 case ElementType::Line:
780 case ElementType::Text:
781 case ElementType::Unknown:
782 throw PANDACEPTION("Element type not supported: %1", elm->objectName()); // LCOV_EXCL_LINE
783 }
784 }
785}
786
787void ArduinoCodeGen::emitDFlipFlop(GraphicElement *elm, const QString &firstOut)
788{
789 auto *outputPort1 = elm->outputPort(1);
790 if (!outputPort1) return;
791 QString secondOut = m_varMap.value(outputPort1);
792 QString firstOutNext = firstOut + "_next";
793 QString secondOutNext = secondOut + "_next";
794 QString data = otherPortName(elm->inputPort(0));
795 QString last = firstOut + "_last";
796 emitFlipFlopBlock(elm, "D", firstOut, secondOut, /*clk*/1, /*preset*/2, /*clear*/3,
797 [this, &last, &firstOutNext, &secondOutNext]() {
798 m_stream << QString(" %1 = %2;").arg(firstOutNext, last) << Qt::endl;
799 m_stream << QString(" %1 = !%2;").arg(secondOutNext, last) << Qt::endl;
800 },
801 [this, &last, &data]() {
802 m_stream << " " << last << " = " << data << ";" << Qt::endl;
803 });
804}
805
806void ArduinoCodeGen::emitDLatch(GraphicElement *elm, const QString &firstOut)
807{
808 auto *outputPort1 = elm->outputPort(1);
809 if (!outputPort1) return;
810 QString secondOut = m_varMap.value(outputPort1);
811 QString firstOutNext = firstOut + "_next";
812 QString secondOutNext = secondOut + "_next";
813 QString data = otherPortName(elm->inputPort(0));
814 QString clk = otherPortName(elm->inputPort(1));
815 m_stream << QString(" //D Latch") << Qt::endl;
816 m_stream << QString(" if (g_sample) {") << Qt::endl;
817 m_stream << QString(" if (%1) { ").arg(clk) << Qt::endl;
818 m_stream << QString(" %1 = %2;").arg(firstOutNext, data) << Qt::endl;
819 m_stream << QString(" %1 = !%2;").arg(secondOutNext, data) << Qt::endl;
820 m_stream << QString(" }") << Qt::endl;
821 m_stream << QString(" }") << Qt::endl;
822 m_stream << QString(" //End of D Latch") << Qt::endl;
823}
824
825void ArduinoCodeGen::emitJKFlipFlop(GraphicElement *elm, const QString &firstOut)
826{
827 auto *outputPort1 = elm->outputPort(1);
828 if (!outputPort1) return;
829 QString secondOut = m_varMap.value(outputPort1);
830 QString firstOutNext = firstOut + "_next";
831 QString secondOutNext = secondOut + "_next";
832 QString j = otherPortName(elm->inputPort(0));
833 QString k = otherPortName(elm->inputPort(2));
834 emitFlipFlopBlock(elm, "JK", firstOut, secondOut, /*clk*/1, /*preset*/3, /*clear*/4,
835 [this, &j, &k, &firstOut, &secondOut, &firstOutNext, &secondOutNext]() {
836 m_stream << QString(" if (%1 && %2) { ").arg(j, k) << Qt::endl;
837 m_stream << QString(" bool aux = %1;").arg(firstOut) << Qt::endl;
838 m_stream << QString(" %1 = %2;").arg(firstOutNext, secondOut) << Qt::endl;
839 m_stream << QString(" %1 = aux;").arg(secondOutNext) << Qt::endl;
840 m_stream << QString(" } else if (%1) {").arg(j) << Qt::endl;
841 m_stream << QString(" %1 = 1;").arg(firstOutNext) << Qt::endl;
842 m_stream << QString(" %1 = 0;").arg(secondOutNext) << Qt::endl;
843 m_stream << QString(" } else if (%1) {").arg(k) << Qt::endl;
844 m_stream << QString(" %1 = 0;").arg(firstOutNext) << Qt::endl;
845 m_stream << QString(" %1 = 1;").arg(secondOutNext) << Qt::endl;
846 m_stream << QString(" }") << Qt::endl;
847 });
848}
849
850void ArduinoCodeGen::emitSRFlipFlop(GraphicElement *elm, const QString &firstOut)
851{
852 auto *outputPort1 = elm->outputPort(1);
853 if (!outputPort1) return;
854 QString secondOut = m_varMap.value(outputPort1);
855 QString firstOutNext = firstOut + "_next";
856 QString secondOutNext = secondOut + "_next";
857 QString s = otherPortName(elm->inputPort(0));
858 QString r = otherPortName(elm->inputPort(2));
859 emitFlipFlopBlock(elm, "SR", firstOut, secondOut, /*clk*/1, /*preset*/3, /*clear*/4,
860 [this, &s, &r, &firstOutNext, &secondOutNext]() {
861 m_stream << QString(" if (%1 && %2) { ").arg(s, r) << Qt::endl;
862 m_stream << QString(" %1 = 1;").arg(firstOutNext) << Qt::endl;
863 m_stream << QString(" %1 = 1;").arg(secondOutNext) << Qt::endl;
864 m_stream << QString(" } else if (%1 != %2) {").arg(s, r) << Qt::endl;
865 m_stream << QString(" %1 = %2;").arg(firstOutNext, s) << Qt::endl;
866 m_stream << QString(" %1 = %2;").arg(secondOutNext, r) << Qt::endl;
867 m_stream << QString(" }") << Qt::endl;
868 });
869}
870
871void ArduinoCodeGen::emitTFlipFlop(GraphicElement *elm, const QString &firstOut)
872{
873 auto *outputPort1 = elm->outputPort(1);
874 if (!outputPort1) return;
875 QString secondOut = m_varMap.value(outputPort1);
876 QString firstOutNext = firstOut + "_next";
877 QString secondOutNext = secondOut + "_next";
878 QString t = otherPortName(elm->inputPort(0));
879 emitFlipFlopBlock(elm, "T", firstOut, secondOut, /*clk*/1, /*preset*/2, /*clear*/3,
880 [this, &t, &firstOut, &firstOutNext, &secondOutNext]() {
881 // Toggle reading the pre-edge Q: Q' = !Q, ~Q' = Q (old).
882 m_stream << QString(" if (%1) { ").arg(t) << Qt::endl;
883 m_stream << QString(" %1 = !%2;").arg(firstOutNext, firstOut) << Qt::endl;
884 m_stream << QString(" %1 = %2;").arg(secondOutNext, firstOut) << Qt::endl;
885 m_stream << QString(" }") << Qt::endl;
886 });
887}
888
889void ArduinoCodeGen::emitSRLatch(GraphicElement *elm, const QString &firstOut)
890{
891 auto *outputPort1 = elm->outputPort(1);
892 if (!outputPort1) return;
893 QString secondOut = m_varMap.value(outputPort1);
894 QString firstOutNext = firstOut + "_next";
895 QString secondOutNext = secondOut + "_next";
896 QString s = otherPortName(elm->inputPort(0));
897 QString r = otherPortName(elm->inputPort(1));
898 m_stream << QString(" //SR Latch") << Qt::endl;
899 m_stream << QString(" if (g_sample) {") << Qt::endl;
900 m_stream << QString(" if (%1 && %2) { ").arg(s, r) << Qt::endl;
901 m_stream << QString(" %1 = LOW;").arg(firstOutNext) << Qt::endl;
902 m_stream << QString(" %1 = LOW;").arg(secondOutNext) << Qt::endl;
903 m_stream << QString(" } else if (%1) { ").arg(s) << Qt::endl;
904 m_stream << QString(" %1 = HIGH;").arg(firstOutNext) << Qt::endl;
905 m_stream << QString(" %1 = LOW;").arg(secondOutNext) << Qt::endl;
906 m_stream << QString(" } else if (%1) { ").arg(r) << Qt::endl;
907 m_stream << QString(" %1 = LOW;").arg(firstOutNext) << Qt::endl;
908 m_stream << QString(" %1 = HIGH;").arg(secondOutNext) << Qt::endl;
909 m_stream << QString(" }") << Qt::endl;
910 m_stream << QString(" }") << Qt::endl;
911 m_stream << QString(" //End of SR Latch") << Qt::endl;
912}
913
914void ArduinoCodeGen::emitMux(GraphicElement *elm)
915{
916 const int totalInputs = elm->inputSize();
917 int numSelectLines = 1;
918 while (numSelectLines < 16 && (1 << numSelectLines) + numSelectLines < totalInputs) {
919 numSelectLines++;
920 }
921 const int numDataInputs = totalInputs - numSelectLines;
922 const QString output = m_varMap.value(elm->outputPort(0));
923 const QString selectValue = buildSelectExpression(elm, numDataInputs, numSelectLines);
924
925 m_stream << QString(" //Multiplexer") << Qt::endl;
926 for (int i = 0; i < numDataInputs; ++i) {
927 if (i == 0) {
928 m_stream << QString(" if ((%1) == %2) {").arg(selectValue).arg(i) << Qt::endl;
929 } else {
930 m_stream << QString(" } else if ((%1) == %2) {").arg(selectValue).arg(i) << Qt::endl;
931 }
932 m_stream << QString(" %1 = %2;").arg(output, otherPortName(elm->inputPort(i))) << Qt::endl;
933 }
934 m_stream << QString(" } else {") << Qt::endl;
935 m_stream << QString(" %1 = LOW;").arg(output) << Qt::endl;
936 m_stream << QString(" }") << Qt::endl;
937 m_stream << QString(" //End of Multiplexer") << Qt::endl;
938}
939
940void ArduinoCodeGen::emitDemux(GraphicElement *elm)
941{
942 const int numOutputs = elm->outputSize();
943 int numSelectLines = 1;
944 while (numSelectLines < 16 && (1 << numSelectLines) < numOutputs) {
945 numSelectLines++;
946 }
947 const QString dataInput = otherPortName(elm->inputPort(0));
948 const QString selectValue = buildSelectExpression(elm, 1, numSelectLines);
949
950 m_stream << QString(" //Demultiplexer") << Qt::endl;
951 for (int i = 0; i < numOutputs; ++i) {
952 m_stream << QString(" %1 = LOW;").arg(m_varMap.value(elm->outputPort(i))) << Qt::endl;
953 }
954 for (int i = 0; i < numOutputs; ++i) {
955 if (i == 0) {
956 m_stream << QString(" if ((%1) == %2) {").arg(selectValue).arg(i) << Qt::endl;
957 } else {
958 m_stream << QString(" } else if ((%1) == %2) {").arg(selectValue).arg(i) << Qt::endl;
959 }
960 m_stream << QString(" %1 = %2;").arg(m_varMap.value(elm->outputPort(i)), dataInput) << Qt::endl;
961 }
962 m_stream << QString(" }") << Qt::endl;
963 m_stream << QString(" //End of Demultiplexer") << Qt::endl;
964}
965
966void ArduinoCodeGen::emitTruthTable(GraphicElement *elm)
967{
968 auto *ttGraphic = dynamic_cast<TruthTable *>(elm);
969 if (!ttGraphic) return;
970
971 const QBitArray propositions = ttGraphic->key();
972 const int nInputs = elm->inputSize();
973 const int rows = 1 << nInputs;
974
975 QStringList inputSignalNames;
976 for (int i = 0; i < nInputs; ++i) {
977 QString signalName = otherPortName(elm->inputPort(i));
978 if (signalName == "LOW") {
979 signalName = "0";
980 } else if (signalName == "HIGH") {
981 signalName = "1";
982 } else if (signalName.isEmpty()) {
983 // Not reachable: otherPortName()/otherPortNameImpl() has no return path that
984 // yields an empty string — a disconnected input already resolves to "LOW"/"HIGH"
985 // via highLow(defaultValue()), never falling through to here.
986 m_stream << "// WARNING: Input " << i << " of TruthTable '" << elm->objectName() << "' appears disconnected. Assuming LOW." << Qt::endl; // LCOV_EXCL_LINE
987 signalName = "0"; // LCOV_EXCL_LINE
988 }
989 inputSignalNames << signalName;
990 }
991
992 QString indexCalculation;
993 for (int i = 0; i < nInputs; ++i) {
994 if (i == 0) {
995 indexCalculation = inputSignalNames[i];
996 } else {
997 indexCalculation = inputSignalNames[i] + " + (" + indexCalculation + " << 1)";
998 }
999 }
1000
1001 // One if/else chain per output (F19): output k reads key bits 256*k + row.
1002 // Emitting per-output keeps the single-output text byte-identical to the
1003 // historical form.
1004 for (int out = 0; out < elm->outputSize(); ++out) {
1005 const QString outputVarName = m_varMap.value(elm->outputPort(out));
1006 // Not reachable: declareAuxVariablesRec() unconditionally assigns every element
1007 // output (connected downstream or not) a variable name before emitTruthTable() ever
1008 // runs, so outputVarName is always already populated here.
1009 if (outputVarName.isEmpty()) { // LCOV_EXCL_LINE
1010 if (out == 0) { // LCOV_EXCL_LINE
1011 throw PANDACEPTION("Output variable not mapped for TruthTable: %1", elm->objectName()); // LCOV_EXCL_LINE
1012 } // LCOV_EXCL_LINE
1013 m_stream << "// TruthTable '" << elm->objectName() << "' output " << out << " is disconnected — no code emitted." << Qt::endl; // LCOV_EXCL_LINE
1014 continue; // LCOV_EXCL_LINE
1015 }
1016
1017 m_stream << QString(" //TruthTable") << Qt::endl;
1018 for (int i = 0; i < rows; ++i) {
1019 if (i == 0) {
1020 m_stream << QString(" if ((%1) == %2) {").arg(indexCalculation).arg(i) << Qt::endl;
1021 } else {
1022 m_stream << QString(" } else if ((%1) == %2) {").arg(indexCalculation).arg(i) << Qt::endl;
1023 }
1024 m_stream << QString(" %1 = %2;").arg(outputVarName, propositions.testBit(256 * out + i) ? "HIGH" : "LOW") << Qt::endl;
1025 }
1026 m_stream << QString(" } else {") << Qt::endl;
1027 m_stream << QString(" %1 = LOW;").arg(outputVarName) << Qt::endl;
1028 m_stream << QString(" }") << Qt::endl;
1029 m_stream << QString(" //End TruthTable") << Qt::endl;
1030 }
1031}
1032
1033QVector<ArduinoBoardConfig> ArduinoCodeGen::availableBoards() const
1034{
1035 static const QVector<ArduinoBoardConfig> boards = []() {
1036 QVector<ArduinoBoardConfig> b;
1037
1038 b.append({
1039 "Arduino UNO R3/R4",
1040 {"A0", "A1", "A2", "A3", "A4", "A5", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"},
1041 "Standard Arduino board with 20 I/O pins"
1042 });
1043
1044 b.append({
1045 "Arduino Nano",
1046 {"A0", "A1", "A2", "A3", "A4", "A5", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"},
1047 "Compact Arduino board with 20 I/O pins"
1048 });
1049
1050 QStringList megaPins;
1051 for (int i = 0; i <= 15; ++i) {
1052 megaPins.append(QString("A%1").arg(i));
1053 }
1054 for (int i = 2; i <= 53; ++i) {
1055 megaPins.append(QString::number(i));
1056 }
1057 b.append({
1058 "Arduino Mega 2560",
1059 megaPins,
1060 "High I/O count Arduino board with 70 I/O pins"
1061 });
1062
1063 b.append({
1064 "ESP32",
1065 {"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "A11", "A12", "A13", "A14", "A15", "A16", "A17",
1066 "2", "4", "5", "12", "13", "14", "15", "16", "17", "18", "19", "21", "22", "23", "25", "26", "27", "32", "33", "34"},
1067 "WiFi/Bluetooth enabled board with 36 I/O pins"
1068 });
1069
1070 return b;
1071 }();
1072 return boards;
1073}
1074
1075ArduinoBoardConfig ArduinoCodeGen::selectBoard(int requiredPins)
1076{
1077 const auto boards = availableBoards();
1078 for (const auto &board : boards) {
1079 if (board.maxPins() >= requiredPins) {
1080 return board;
1081 }
1082 }
1083 // None fits: fall back to the board with the most pins. Don't assume the list is ordered
1084 // by capacity — it isn't (ESP32's 38 < Mega's 68), so boards.last() could pick a smaller one.
1085 return *std::max_element(boards.cbegin(), boards.cend(),
1086 [](const ArduinoBoardConfig &a, const ArduinoBoardConfig &b) { return a.maxPins() < b.maxPins(); });
1087}
1088
1089void ArduinoCodeGen::throwPinOverflow() const
1090{
1091 throw PANDACEPTION("This circuit needs %1 I/O pins, but the largest supported board (%2) provides only %3. Reduce the number of inputs and outputs, or split the circuit.",
1092 QString::number(m_totalRequiredPins), m_selectedBoard.name, QString::number(m_selectedBoard.maxPins()));
1093}
1094
1095QString ArduinoCodeGen::buildSelectExpression(GraphicElement *elm, int startIndex, int numSelectLines)
1096{
1097 QString selectValue;
1098 for (int i = numSelectLines - 1; i >= 0; --i) {
1099 QString selectBit = otherPortName(elm->inputPort(startIndex + i));
1100 if (selectBit == "HIGH") selectBit = "1";
1101 else if (selectBit == "LOW") selectBit = "0";
1102 if (i == numSelectLines - 1) {
1103 selectValue = selectBit;
1104 } else {
1105 selectValue = selectBit + " + (" + selectValue + " << 1)";
1106 }
1107 }
1108 return selectValue;
1109} // LCOV_EXCL_LINE -- compiler-generated QString cleanup for an exception path buildSelectExpression() never takes
1110
1111void ArduinoCodeGen::assignLogicOperator(GraphicElement *elm)
1112{
1113 bool negate = false;
1114 bool parentheses = true;
1115 QString logicOperator;
1116 switch (elm->elementType()) {
1117 case ElementType::And: {
1118 logicOperator = "&&";
1119 break;
1120 }
1121 case ElementType::Or: {
1122 logicOperator = "||";
1123 break;
1124 }
1125 case ElementType::Nand: {
1126 logicOperator = "&&";
1127 negate = true;
1128 break;
1129 }
1130 case ElementType::Nor: {
1131 logicOperator = "||";
1132 negate = true;
1133 break;
1134 }
1135 case ElementType::Xor: {
1136 logicOperator = "^";
1137 break;
1138 }
1139 case ElementType::Xnor: {
1140 logicOperator = "^";
1141 negate = true;
1142 break;
1143 }
1144 case ElementType::Not: {
1145 negate = true;
1146 parentheses = false;
1147 break;
1148 }
1149 case ElementType::Node: {
1150 if (elm->outputs().size() == 1 && elm->inputs().size() == 1) {
1151 auto *outputPort = elm->outputPort(0);
1152 auto *inputPort = elm->inputPort(0);
1153 if (outputPort && inputPort) {
1154 QString varName = m_varMap.value(outputPort);
1155 QString inputValue = otherPortName(inputPort);
1156 // Not reachable: otherPortName() never returns an empty string, so this
1157 // IC-boundary fallback (for when the plain resolution comes back empty)
1158 // never triggers.
1159 if (inputValue.isEmpty() && m_currentIC) { // LCOV_EXCL_LINE
1160 for (int i = 0; i < m_currentIC->internalInputs().size(); ++i) { // LCOV_EXCL_LINE
1161 if (m_currentIC->internalInputs().at(i) == inputPort) { // LCOV_EXCL_LINE
1162 inputValue = m_varMap.value(inputPort); // LCOV_EXCL_LINE
1163 break; // LCOV_EXCL_LINE
1164 } // LCOV_EXCL_LINE
1165 } // LCOV_EXCL_LINE
1166 }
1167 m_stream << " " << varName << " = " << inputValue << ";" << Qt::endl;
1168 }
1169 }
1170 return;
1171 }
1172 // Not reachable: assignVariablesRec()'s switch is the only caller, and it only
1173 // dispatches here for And/Or/Nand/Nor/Xor/Xnor/Not/Node — every other case below is
1174 // handled by a different branch of that switch (or filtered out before reaching it).
1175 // ElementType::JKLatch is unreachable for a different, stronger reason: it's a
1176 // deprecated enum value with no live GraphicElement implementation at all
1177 // (ElementFactory::hasCreator(JKLatch) is false), so no real `elm` can ever report it.
1178 case ElementType::AudioBox: // LCOV_EXCL_LINE
1179 case ElementType::Buzzer:
1180 case ElementType::Clock:
1181 case ElementType::DFlipFlop:
1182 case ElementType::DLatch:
1183 case ElementType::Demux:
1184 case ElementType::Display14:
1185 case ElementType::Display16:
1186 case ElementType::Display7:
1187 case ElementType::IC:
1188 case ElementType::InputButton:
1189 case ElementType::InputGnd:
1190 case ElementType::InputRotary:
1191 case ElementType::InputSwitch:
1192 case ElementType::InputVcc:
1193 case ElementType::JKFlipFlop:
1194 case ElementType::JKLatch:
1195 case ElementType::Led:
1196 case ElementType::Line:
1197 case ElementType::Mux:
1198 case ElementType::SRFlipFlop:
1199 case ElementType::SRLatch:
1200 case ElementType::TFlipFlop:
1201 case ElementType::Text:
1202 case ElementType::TruthTable:
1203 case ElementType::Unknown:
1204 break; // LCOV_EXCL_LINE
1205 }
1206 if (elm->outputs().size() == 1) {
1207 auto *outputPort = elm->outputPort();
1208 if (!outputPort) return;
1209 QString varName = m_varMap.value(outputPort);
1210 Port *inPort = elm->inputPort();
1211 if (!inPort) return;
1212
1213 // Build the inner expression first
1214 QString innerExpr;
1215 innerExpr = otherPortName(inPort);
1216 for (int i = 1; i < elm->inputs().size(); ++i) {
1217 inPort = elm->inputs().at(i);
1218 innerExpr += " " + logicOperator + " ";
1219 innerExpr += otherPortName(inPort);
1220 }
1221
1222 QString finalExpr = innerExpr;
1223 if (negate) {
1224 if (parentheses) {
1225 finalExpr = "!(" + innerExpr + ")";
1226 } else {
1227 finalExpr = "!" + innerExpr;
1228 }
1229 }
1230
1231 m_stream << " " << varName << " = " << finalExpr << ";" << Qt::endl;
1232 } else {
1233 // Not reachable: every gate/Node type dispatched here (And/Or/Nand/Nor/Xor/Xnor/Not/
1234 // Node) always has exactly 1 output by construction.
1235 qWarning() << "assignLogicOperator: element" << elm->objectName() << "has" << elm->outputs().size() << "outputs (expected 1) — skipping"; // LCOV_EXCL_LINE
1236 }
1237}
1238
1239void ArduinoCodeGen::emitComputeLogicFunction()
1240{
1241 m_stream << "void computeLogic() {" << Qt::endl;
1242 m_stream << " // Assigning aux variables. //" << Qt::endl;
1243 assignVariablesRec(m_elements);
1244 m_stream << "}" << Qt::endl
1245 << Qt::endl;
1246}
1247
1248void ArduinoCodeGen::loop()
1249{
1250 m_stream << "void loop() {" << Qt::endl;
1251 m_stream << " // Reading input data //." << Qt::endl;
1252 for (const auto &pin : std::as_const(m_inputMap)) {
1253 m_stream << QString(" %1_val = digitalRead(%1);").arg(pin.m_varName) << Qt::endl;
1254 }
1255 m_stream << Qt::endl;
1256 m_stream << " // Updating clocks. //" << Qt::endl;
1257 for (auto *elm : m_elements) {
1258 if (elm->elementType() == ElementType::Clock) {
1259 const auto elmOutputs = elm->outputs();
1260 if (elmOutputs.isEmpty()) {
1261 continue; // LCOV_EXCL_LINE -- Clock always constructs with exactly one output port
1262 }
1263 QString varName = m_varMap.value(elmOutputs.constFirst());
1264 m_stream << QString(" if (%1_elapsed > %1_interval) {").arg(varName) << Qt::endl;
1265 m_stream << QString(" %1_elapsed = 0;").arg(varName) << Qt::endl;
1266 m_stream << QString(" %1 = ! %1;").arg(varName) << Qt::endl;
1267 m_stream << QString(" }") << Qt::endl;
1268 }
1269 }
1270 m_stream << Qt::endl;
1271 emitTickDriver();
1272 m_stream << Qt::endl;
1273 m_stream << " // Writing output data. //" << Qt::endl;
1274 for (const auto &pin : std::as_const(m_outputMap)) {
1275 if (pin.m_elm->elementType() == ElementType::Buzzer) {
1276 auto *buzzer = qobject_cast<Buzzer *>(pin.m_elm);
1277 if (!buzzer) continue;
1278 const QString inputSignal = otherPortName(buzzer->inputPort(0));
1279 const int frequency = static_cast<int>(buzzer->frequency());
1280 m_stream << QString(" if (%1) {").arg(inputSignal) << Qt::endl;
1281 m_stream << QString(" tone(%1, %2);").arg(pin.m_varName).arg(frequency) << Qt::endl;
1282 m_stream << QString(" } else {") << Qt::endl;
1283 m_stream << QString(" noTone(%1);").arg(pin.m_varName) << Qt::endl;
1284 m_stream << QString(" }") << Qt::endl;
1285 continue;
1286 }
1287 QString varName = otherPortName(pin.m_port);
1288 if (varName.isEmpty()) {
1289 // Not reachable: otherPortName()/otherPortNameImpl() has no return path that
1290 // yields an empty string (every branch returns "HIGH"/"LOW" or a real variable
1291 // name) — kept as a defensive backstop against a future change to that contract.
1292 varName = highLow(pin.m_port->defaultValue()); // LCOV_EXCL_LINE
1293 }
1294 m_stream << QString(" digitalWrite(%1, %2);").arg(pin.m_varName, varName) << Qt::endl;
1295 }
1296 m_stream << "}" << Qt::endl;
1297}
1298
1299void ArduinoCodeGen::generateTestbench(const QString &tbFileName, const QVector<TestVector> &vectors)
1300{
1301 QFile tbFile(tbFileName);
1302 if (!tbFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
1303 throw PANDACEPTION("Could not open testbench file: %1", tbFileName);
1304 }
1305
1306 QIODevice *savedDevice = m_stream.device();
1307 m_stream.setDevice(&tbFile);
1308 m_hasSequential = hasNativeMemory(m_elements);
1309
1310 try {
1311 m_stream << "// ============================================================ //" << Qt::endl;
1312 m_stream << "// ====== Testbench generated automatically by wiRedPanda ====== //" << Qt::endl;
1313 m_stream << "// ============================================================ //" << Qt::endl;
1314 m_stream << Qt::endl;
1315 m_stream << "#include <avr/sleep.h>" << Qt::endl;
1316 m_stream << "#include <elapsedMillis.h>" << Qt::endl;
1317 m_stream << Qt::endl;
1318
1319 // Input pin stubs (same variable names as production sketch; not used for I/O)
1320 m_stream << "/* ========= Inputs ========== */" << Qt::endl;
1321 for (const auto &pin : std::as_const(m_inputMap)) {
1322 m_stream << "const int " << pin.m_varName << " = 0;" << Qt::endl;
1323 }
1324 m_stream << Qt::endl;
1325
1326 // Output pin stubs
1327 m_stream << "/* ========= Outputs ========== */" << Qt::endl;
1328 for (const auto &pin : std::as_const(m_outputMap)) {
1329 m_stream << "const int " << pin.m_varName << " = 0;" << Qt::endl;
1330 }
1331 m_stream << Qt::endl;
1332
1333 // Aux variable declarations (same as production sketch)
1334 m_stream << "/* ====== Aux. Variables ====== */" << Qt::endl;
1335 for (const auto &varName : std::as_const(m_declaredVariables)) {
1336 m_stream << "bool " << varName << " = LOW;" << Qt::endl;
1337 }
1338 // Extra state variables for clocks and flip-flops, including those nested
1339 // inside ICs — computeLogic() recurses into them, so their _inclk/_last
1340 // state must be declared at every depth (not just top level).
1341 declareSequentialStateRec(m_elements, /*topLevel*/ true);
1342 if (m_hasSequential) {
1343 m_stream << "bool g_sample = true;" << Qt::endl;
1344 }
1345 m_stream << Qt::endl;
1346
1347 // computeLogic() function — identical to production sketch
1348 emitComputeLogicFunction();
1349 if (m_hasSequential) {
1350 emitCommitFlipFlops();
1351 }
1352
1353 // Build the list of output variable names (what feeds each output pin)
1354 QStringList outputVarNames;
1355 for (const auto &pin : std::as_const(m_outputMap)) {
1356 QString varName = otherPortName(pin.m_port);
1357 if (varName.isEmpty()) {
1358 varName = highLow(pin.m_port->defaultValue()); // LCOV_EXCL_LINE -- see loop()'s identical guard
1359 }
1360 outputVarNames.append(varName);
1361 }
1362
1363 // Test vector table
1364 const int numInputs = static_cast<int>(m_inputMap.size());
1365 const int numOutputs = static_cast<int>(m_outputMap.size());
1366 const int numTests = static_cast<int>(vectors.size());
1367
1368 m_stream << "/* ====== Test Vectors ====== */" << Qt::endl;
1369 m_stream << "struct TestVec {" << Qt::endl;
1370 m_stream << " bool in[" << (std::max)(1, numInputs) << "];" << Qt::endl;
1371 m_stream << " bool out[" << (std::max)(1, numOutputs) << "];" << Qt::endl;
1372 m_stream << "};" << Qt::endl;
1373 m_stream << Qt::endl;
1374 m_stream << "const int NUM_TESTS = " << numTests << ";" << Qt::endl;
1375 m_stream << "const TestVec VECTORS[" << (std::max)(1, numTests) << "] = {" << Qt::endl;
1376 for (const auto &v : vectors) {
1377 m_stream << " {{";
1378 for (int j = 0; j < v.inputs.size(); ++j) {
1379 if (j > 0) m_stream << ", ";
1380 m_stream << (v.inputs[j] ? "HIGH" : "LOW");
1381 }
1382 m_stream << "}, {";
1383 for (int k = 0; k < v.outputs.size(); ++k) {
1384 if (k > 0) m_stream << ", ";
1385 m_stream << (v.outputs[k] ? "HIGH" : "LOW");
1386 }
1387 m_stream << "}}," << Qt::endl;
1388 }
1389 m_stream << "};" << Qt::endl;
1390 m_stream << Qt::endl;
1391
1392 // setup() — runs all test vectors, reports via Serial
1393 m_stream << "void setup() {" << Qt::endl;
1394 m_stream << " Serial.begin(9600);" << Qt::endl;
1395 m_stream << " bool allPassed = true;" << Qt::endl;
1396 m_stream << " for (int t = 0; t < NUM_TESTS; t++) {" << Qt::endl;
1397 for (int j = 0; j < numInputs; ++j) {
1398 m_stream << " " << m_inputMap.at(j).m_varName << "_val = VECTORS[t].in[" << j << "];" << Qt::endl;
1399 }
1400 // One simulation tick. For sequential circuits this mirrors the engine's
1401 // non-blocking commit: settle while flip-flops sample into _next (reading
1402 // pre-edge state), commit all at once, then re-settle so combinational
1403 // outputs reflect the new state. Combinational/gate-built circuits just
1404 // settle to a fixed point (same bound as Simulation::iterativeSettle).
1405 if (m_hasSequential) {
1406 m_stream << " g_sample = true;" << Qt::endl;
1407 m_stream << " for (int s = 0; s < " << Simulation::kMaxSettleIterations
1408 << "; s++) { computeLogic(); }" << Qt::endl;
1409 m_stream << " commitFlipFlops();" << Qt::endl;
1410 m_stream << " g_sample = false;" << Qt::endl;
1411 m_stream << " for (int s = 0; s < " << Simulation::kMaxSettleIterations
1412 << "; s++) { computeLogic(); }" << Qt::endl;
1413 } else {
1414 m_stream << " for (int s = 0; s < " << Simulation::kMaxSettleIterations
1415 << "; s++) { computeLogic(); }" << Qt::endl;
1416 }
1417 m_stream << " bool pass = true;" << Qt::endl;
1418 for (int k = 0; k < numOutputs; ++k) {
1419 m_stream << " pass = pass && (" << outputVarNames.at(k) << " == VECTORS[t].out[" << k << "]);" << Qt::endl;
1420 }
1421 m_stream << " if (!pass) {" << Qt::endl;
1422 m_stream << " allPassed = false;" << Qt::endl;
1423 m_stream << " Serial.print(\"FAIL vector \");" << Qt::endl;
1424 m_stream << " Serial.println(t);" << Qt::endl;
1425 m_stream << " }" << Qt::endl;
1426 m_stream << " }" << Qt::endl;
1427 m_stream << " Serial.println(allPassed ? \"ALL PASS\" : \"SOME FAILED\");" << Qt::endl;
1428 m_stream << " Serial.flush();" << Qt::endl;
1429 m_stream << " set_sleep_mode(SLEEP_MODE_PWR_DOWN);" << Qt::endl;
1430 m_stream << " sleep_mode();" << Qt::endl;
1431 m_stream << "}" << Qt::endl;
1432 m_stream << Qt::endl;
1433
1434 m_stream << "void loop() {}" << Qt::endl;
1435 } catch (...) { // LCOV_EXCL_LINE -- defensive symmetry with generate()'s try/catch; no reachable throw currently exists inside this try block (every candidate is itself excluded above as unreachable)
1436 m_stream.setDevice(savedDevice); // LCOV_EXCL_LINE
1437 tbFile.close(); // LCOV_EXCL_LINE
1438 throw; // LCOV_EXCL_LINE
1439 } // LCOV_EXCL_LINE
1440
1441 m_stream.setDevice(savedDevice);
1442 tbFile.close();
1443}
Arduino sketch code generator: translates a circuit into an uploadable .ino file.
Graphic element for the buzzer tone output.
Graphic element for the real-time clock input.
Shared string utilities used by all code generators.
Common logging utilities, the Pandaception error type, and helper macros.
#define PANDACEPTION(msg,...)
Definition Common.h:98
Connection: a wire that connects an output port to an input port in the circuit scene.
Enums::Status Status
Definition Enums.h:106
Abstract base class for all graphical circuit elements.
Integrated Circuit (IC) graphic element that encapsulates a sub-circuit file.
Graphic element for the rotary switch input.
Port classes: Port (base), InputPort, and OutputPort.
Main circuit editing scene with undo/redo and user interaction.
Synchronous cycle-based simulation engine with event-driven clock support.
Graphic element for a user-programmable truth table.
void generate()
Generates the Arduino sketch and writes it to the output file.
void generateTestbench(const QString &tbFileName, const QVector< TestVector > &vectors)
ArduinoCodeGen(const QString &fileName, const QVector< GraphicElement * > &elements)
Constructs the code generator targeting fileName.
Abstract base class for all graphical circuit elements in wiRedPanda.
ElementType elementType() const
Returns the type identifier for this element.
int inputSize() const
Returns the current number of input ports.
InputPort * inputPort(const int index=0) const
Returns the input port at index (default 0).
int outputSize() const
Returns the current number of output ports.
OutputPort * outputPort(const int index=0) const
Returns the output port at index (default 0).
const QVector< OutputPort * > & outputs() const
Returns a const reference to the vector of all output ports.
const QVector< InputPort * > & inputs() const
Returns a const reference to the vector of all input ports.
Abstract base class for circuit element ports (connection endpoints).
Definition Port.h:39
GraphicElement * graphicElement()
Returns the graphic element that owns this port.
Definition Port.h:71
Status defaultValue() const
Returns the default status applied when the port is unconnected.
Definition Port.h:81
const QList< Connection * > & connections() const
Returns the list of wires attached to this port.
Definition Port.cpp:57
QString name() const
Returns the port's label text.
Definition Port.cpp:161
static QHash< QString, InputPort * > wirelessTxInputPorts(const QVector< GraphicElement * > &elements)
Returns a map from wireless channel label to the Tx node's input port.
Definition Scene.cpp:399
static QVector< GraphicElement * > sortByTopology(QVector< GraphicElement * > elements)
Returns elements sorted in topological dependency order (inputs first).
Definition Scene.cpp:371
static constexpr int kMaxSettleIterations
Definition Simulation.h:48
QString removeForbiddenChars(const QString &input, const bool stripFirst=false)
Converts input into a legal language identifier.
QString sanitizeComment(const QString &input)
Makes input safe to embed in a single-line "//" comment.
Describes an Arduino board's available GPIO pins.